mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
fix(tui): soften theme scale extremes (#37555)
This commit is contained in:
@@ -44,7 +44,7 @@ function requestOptions(form: FormWithLocation) {
|
||||
|
||||
export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
const client = useClient()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const { themeV2, mode: themeMode } = useTheme().contextual("elevated")
|
||||
const renderer = useRenderer()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const keymap = Keymap.use()
|
||||
@@ -626,7 +626,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
<box
|
||||
backgroundColor={themeV2.background()}
|
||||
border={["left"]}
|
||||
borderColor={themeV2.hue.accent(500)}
|
||||
borderColor={themeV2.hue.interactive(themeMode() === "light" ? 800 : 200)}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
>
|
||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||
@@ -729,11 +729,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
{external().url}
|
||||
</text>
|
||||
<text
|
||||
fg={
|
||||
store.answers[external().key] === true
|
||||
? themeV2.text.feedback.success()
|
||||
: themeV2.text.subdued()
|
||||
}
|
||||
fg={store.answers[external().key] === true ? themeV2.text.feedback.success() : themeV2.text.subdued()}
|
||||
>
|
||||
{store.answers[external().key] === true
|
||||
? "✓ Acknowledged"
|
||||
@@ -799,25 +795,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<box
|
||||
backgroundColor={
|
||||
active() ? themeV2.background.formfield("focused") : themeV2.background()
|
||||
}
|
||||
backgroundColor={active() ? themeV2.background.formfield("focused") : themeV2.background()}
|
||||
paddingRight={1}
|
||||
>
|
||||
<text fg={themeV2.text.formfield(active() ? "focused" : "default")}>
|
||||
{`${i() + 1}.`}
|
||||
</text>
|
||||
<text fg={themeV2.text.formfield(active() ? "focused" : "default")}>{`${i() + 1}.`}</text>
|
||||
</box>
|
||||
<box
|
||||
backgroundColor={
|
||||
active() ? themeV2.background.formfield("focused") : themeV2.background()
|
||||
}
|
||||
backgroundColor={active() ? themeV2.background.formfield("focused") : themeV2.background()}
|
||||
>
|
||||
<text
|
||||
fg={themeV2.text.formfield(
|
||||
active() ? "focused" : picked() ? "selected" : "default",
|
||||
)}
|
||||
>
|
||||
<text fg={themeV2.text.formfield(active() ? "focused" : picked() ? "selected" : "default")}>
|
||||
{multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label}
|
||||
</text>
|
||||
</box>
|
||||
@@ -852,9 +838,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
{`${rows().length + 1}.`}
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
backgroundColor={other() ? themeV2.background.formfield("focused") : themeV2.background()}
|
||||
>
|
||||
<box backgroundColor={other() ? themeV2.background.formfield("focused") : themeV2.background()}>
|
||||
<text
|
||||
fg={
|
||||
other()
|
||||
@@ -926,9 +910,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
||||
<span style={{ fg: themeV2.text.subdued() }}>{truncate(formLabel(item), 40)}:</span>{" "}
|
||||
<span
|
||||
style={{
|
||||
fg: acknowledged()
|
||||
? themeV2.text.feedback.success()
|
||||
: themeV2.text.feedback.error(),
|
||||
fg: acknowledged() ? themeV2.text.feedback.success() : themeV2.text.feedback.error(),
|
||||
}}
|
||||
>
|
||||
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
|
||||
|
||||
@@ -97,18 +97,18 @@ export const DEFAULT_THEME = {
|
||||
neutral: "$hue.gray",
|
||||
},
|
||||
text: {
|
||||
default: "$hue.neutral.900",
|
||||
default: "$hue.neutral.800",
|
||||
subdued: "$hue.neutral.600",
|
||||
action: {
|
||||
primary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" },
|
||||
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" },
|
||||
primary: { default: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
|
||||
destructive: { default: "$hue.red.200", $disabled: "$hue.neutral.500" },
|
||||
},
|
||||
formfield: {
|
||||
default: "$hue.neutral.900",
|
||||
default: "$hue.neutral.800",
|
||||
$focused: "$text.action.primary.default",
|
||||
$pressed: "$hue.neutral.100",
|
||||
$pressed: "$hue.neutral.200",
|
||||
$disabled: "$hue.neutral.500",
|
||||
$selected: "$hue.interactive.600",
|
||||
$selected: "$hue.interactive.700",
|
||||
},
|
||||
feedback: {
|
||||
error: { default: "$hue.red.700", subdued: "$hue.red.600" },
|
||||
@@ -118,10 +118,10 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
},
|
||||
background: {
|
||||
default: "$hue.neutral.100",
|
||||
default: "$hue.neutral.200",
|
||||
surface: {
|
||||
offset: "$hue.neutral.200",
|
||||
overlay: "$hue.neutral.300",
|
||||
offset: "$hue.neutral.300",
|
||||
overlay: "$hue.neutral.400",
|
||||
},
|
||||
action: {
|
||||
primary: {
|
||||
@@ -309,16 +309,16 @@ export const DEFAULT_THEME = {
|
||||
neutral: "$hue.gray",
|
||||
},
|
||||
text: {
|
||||
default: "$hue.neutral.100",
|
||||
default: "$hue.neutral.200",
|
||||
subdued: "$hue.neutral.400",
|
||||
action: {
|
||||
primary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" },
|
||||
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" },
|
||||
primary: { default: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
|
||||
destructive: { default: "$hue.red.200", $disabled: "$hue.neutral.500" },
|
||||
},
|
||||
formfield: {
|
||||
default: "$hue.neutral.100",
|
||||
default: "$hue.neutral.200",
|
||||
$focused: "$text.action.primary.default",
|
||||
$pressed: "$hue.neutral.100",
|
||||
$pressed: "$hue.neutral.200",
|
||||
$disabled: "$hue.neutral.500",
|
||||
$selected: "$hue.interactive.500",
|
||||
},
|
||||
@@ -330,10 +330,10 @@ export const DEFAULT_THEME = {
|
||||
},
|
||||
},
|
||||
background: {
|
||||
default: "$hue.neutral.900",
|
||||
default: "$hue.neutral.800",
|
||||
surface: {
|
||||
offset: "$hue.neutral.800",
|
||||
overlay: "$hue.neutral.700",
|
||||
offset: "$hue.neutral.700",
|
||||
overlay: "$hue.neutral.600",
|
||||
},
|
||||
action: {
|
||||
primary: {
|
||||
@@ -412,14 +412,14 @@ export const DEFAULT_THEME = {
|
||||
codeBlock: "$hue.neutral.100",
|
||||
},
|
||||
"@context:elevated": {
|
||||
text: { action: { primary: { default: "$hue.neutral.100" } } },
|
||||
text: { action: { primary: { default: "$hue.neutral.200" } } },
|
||||
background: {
|
||||
default: "$background.surface.offset",
|
||||
action: { primary: { default: "$hue.interactive.400", $hovered: "$background.surface.overlay" } },
|
||||
},
|
||||
},
|
||||
"@context:overlay": {
|
||||
text: { action: { primary: { default: "$hue.neutral.900" } } },
|
||||
text: { action: { primary: { default: "$hue.neutral.200" } } },
|
||||
background: {
|
||||
default: "$background.surface.overlay",
|
||||
action: { primary: { default: "$hue.interactive.400" } },
|
||||
|
||||
@@ -26,12 +26,12 @@ function migrateMode(theme: Theme, mode: "light" | "dark"): ThemeFile["light"] {
|
||||
const selected = hex(selectedForeground(theme, theme.primary))
|
||||
const destructive = hex(selectedForeground(theme, theme.error))
|
||||
const hues = inferHues(theme, mode)
|
||||
const text = mode === "light" ? "$hue.neutral.900" : "$hue.neutral.100"
|
||||
const textMuted = mode === "light" ? "$hue.neutral.700" : "$hue.neutral.300"
|
||||
const primary = mode === "light" ? "$hue.interactive.900" : "$hue.interactive.100"
|
||||
const background = mode === "light" ? "$hue.neutral.100" : "$hue.neutral.900"
|
||||
const backgroundPanel = mode === "light" ? "$hue.neutral.200" : "$hue.neutral.800"
|
||||
const backgroundMenu = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700"
|
||||
const text = mode === "light" ? "$hue.neutral.800" : "$hue.neutral.200"
|
||||
const textMuted = mode === "light" ? "$hue.neutral.600" : "$hue.neutral.400"
|
||||
const primary = mode === "light" ? "$hue.interactive.800" : "$hue.interactive.200"
|
||||
const background = mode === "light" ? "$hue.neutral.200" : "$hue.neutral.800"
|
||||
const backgroundPanel = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700"
|
||||
const backgroundMenu = mode === "light" ? "$hue.neutral.400" : "$hue.neutral.600"
|
||||
|
||||
return {
|
||||
hue: {
|
||||
@@ -233,13 +233,13 @@ function selectedForeground(theme: Theme, background: RGBA) {
|
||||
|
||||
function hueScale(color: RGBA, mode: "light" | "dark") {
|
||||
const value = toOklch(color)
|
||||
const anchor = mode === "light" ? 900 : 100
|
||||
const anchor = mode === "light" ? 800 : 200
|
||||
const endpoint = mode === "light" ? Math.max(0.97, value.l) : Math.min(0.18, value.l)
|
||||
const alpha = color.toInts()[3]
|
||||
return Object.fromEntries(
|
||||
HueStep.literals.map((step) => {
|
||||
if (step === anchor) return [step, hex(color)]
|
||||
const progress = mode === "light" ? (900 - step) / 800 : (step - 100) / 800
|
||||
const progress = mode === "light" ? (anchor - step) / (anchor - 100) : (step - anchor) / (900 - anchor)
|
||||
const generated = oklchToHex({
|
||||
l: value.l + (endpoint - value.l) * progress,
|
||||
c: value.c * (1 - progress * 0.5),
|
||||
@@ -256,8 +256,14 @@ function neutralScale(theme: Theme, mode: "light" | "dark") {
|
||||
HueStep.literals.map((step) => {
|
||||
const exact = anchors.find((anchor) => anchor.step === step)
|
||||
if (exact) return [step, hex(exact.color)]
|
||||
const lower = anchors.filter((anchor) => anchor.step < step).at(-1)!
|
||||
const upper = anchors.find((anchor) => anchor.step > step)!
|
||||
const first = anchors[0]!
|
||||
const last = anchors.at(-1)!
|
||||
const [lower, upper] =
|
||||
step < first.step
|
||||
? [first, anchors[1]!]
|
||||
: step > last.step
|
||||
? [anchors.at(-2)!, last]
|
||||
: [anchors.filter((anchor) => anchor.step < step).at(-1)!, anchors.find((anchor) => anchor.step > step)!]
|
||||
return [step, interpolate(lower.color, upper.color, (step - lower.step) / (upper.step - lower.step))]
|
||||
}),
|
||||
) as Record<HueStep, string>
|
||||
@@ -265,11 +271,11 @@ function neutralScale(theme: Theme, mode: "light" | "dark") {
|
||||
|
||||
function neutralAnchors(theme: Theme, mode: "light" | "dark") {
|
||||
const light: { step: HueStep; color: RGBA }[] = [
|
||||
{ step: 100, color: theme.background },
|
||||
{ step: 200, color: theme.backgroundPanel },
|
||||
{ step: 300, color: theme.backgroundElement || theme.backgroundMenu },
|
||||
{ step: 700, color: theme.textMuted },
|
||||
{ step: 900, color: theme.text },
|
||||
{ step: 200, color: theme.background },
|
||||
{ step: 300, color: theme.backgroundPanel },
|
||||
{ step: 400, color: theme.backgroundElement || theme.backgroundMenu },
|
||||
{ step: 600, color: theme.textMuted },
|
||||
{ step: 800, color: theme.text },
|
||||
]
|
||||
if (mode === "light") return light
|
||||
return light.toReversed().map((source) => ({ ...source, step: (1000 - source.step) as HueStep }))
|
||||
@@ -278,13 +284,18 @@ function neutralAnchors(theme: Theme, mode: "light" | "dark") {
|
||||
function interpolate(first: RGBA, second: RGBA, amount: number) {
|
||||
const start = toOklch(first)
|
||||
const end = toOklch(second)
|
||||
const hue = ((((end.h - start.h) % 360) + 540) % 360) - 180
|
||||
const startHue = Number.isFinite(start.h) ? start.h : Number.isFinite(end.h) ? end.h : 0
|
||||
const endHue = Number.isFinite(end.h) ? end.h : startHue
|
||||
const hue = ((((endHue - startHue) % 360) + 540) % 360) - 180
|
||||
const generated = oklchToHex({
|
||||
l: start.l + (end.l - start.l) * amount,
|
||||
c: start.c + (end.c - start.c) * amount,
|
||||
h: start.h + hue * amount,
|
||||
h: startHue + hue * amount,
|
||||
})
|
||||
const alpha = Math.round(first.toInts()[3] + (second.toInts()[3] - first.toInts()[3]) * amount)
|
||||
const alpha = Math.max(
|
||||
0,
|
||||
Math.min(255, Math.round(first.toInts()[3] + (second.toInts()[3] - first.toInts()[3]) * amount)),
|
||||
)
|
||||
return alpha === 255 ? generated : `${generated}${byte(alpha)}`
|
||||
}
|
||||
|
||||
|
||||
@@ -14,44 +14,38 @@ test("migrates resolved V1 modes into literal V2 tokens", () => {
|
||||
if (typeof migrated.light.hue?.accent !== "object" || typeof migrated.light.hue.interactive !== "object") {
|
||||
throw new Error("Expected concrete accent and interactive scales")
|
||||
}
|
||||
expect(migrated.light.hue.accent[900]).toBe(hex(legacy.accent))
|
||||
expect(migrated.light.hue.interactive[900]).toBe(hex(legacy.primary))
|
||||
expect(migrated.light.text?.default).toBe("$hue.neutral.900")
|
||||
expect(migrated.light.text?.subdued).toBe("$hue.neutral.700")
|
||||
expect(migrated.light.hue.accent[800]).toBe(hex(legacy.accent))
|
||||
expect(migrated.light.hue.interactive[800]).toBe(hex(legacy.primary))
|
||||
expect(migrated.light.text?.default).toBe("$hue.neutral.800")
|
||||
expect(migrated.light.text?.subdued).toBe("$hue.neutral.600")
|
||||
expect(migrated.light.background?.action?.primary?.default).toBe("transparent")
|
||||
expect(migrated.light.background?.default).toBe("$hue.neutral.100")
|
||||
expect(migrated.light.background?.surface?.offset).toBe("$hue.neutral.200")
|
||||
expect(migrated.light.background?.surface?.overlay).toBe("$hue.neutral.300")
|
||||
expect(migrated.dark.background?.default).toBe("$hue.neutral.900")
|
||||
expect(migrated.dark.background?.surface?.offset).toBe("$hue.neutral.800")
|
||||
expect(migrated.dark.background?.surface?.overlay).toBe("$hue.neutral.700")
|
||||
expect(migrated.light.background?.default).toBe("$hue.neutral.200")
|
||||
expect(migrated.light.background?.surface?.offset).toBe("$hue.neutral.300")
|
||||
expect(migrated.light.background?.surface?.overlay).toBe("$hue.neutral.400")
|
||||
expect(migrated.dark.background?.default).toBe("$hue.neutral.800")
|
||||
expect(migrated.dark.background?.surface?.offset).toBe("$hue.neutral.700")
|
||||
expect(migrated.dark.background?.surface?.overlay).toBe("$hue.neutral.600")
|
||||
expect(migrated.light.text?.action?.primary?.default).toBe("$text.default")
|
||||
expect(migrated.light.background?.action?.primary?.$selected).toBe("$hue.interactive.900")
|
||||
expect(migrated.light.background?.action?.primary?.$selected).toBe("$hue.interactive.800")
|
||||
expect(migrated.light.scrollbar?.default).toBe(hex(legacy.borderActive))
|
||||
expect(migrated.light.diff?.lineNumber?.background?.removed).toBe(hex(legacy.diffRemovedLineNumberBg))
|
||||
expect(migrated.light.markdown?.emphasis).toBe(hex(legacy.markdownEmph))
|
||||
expect(resolved.background.surface.offset.toInts()).toEqual(legacy.backgroundPanel.toInts())
|
||||
expect(resolved.background.surface.overlay.toInts()).toEqual(legacy.backgroundMenu.toInts())
|
||||
expect(resolved.background.surface.overlay.toInts()).toEqual(legacy.backgroundElement.toInts())
|
||||
expect(resolved.background.formfield.selected.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.background.formfield.focused.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.text.formfield.default.toInts()).toEqual(legacy.text.toInts())
|
||||
expect(resolved.text.formfield.selected.toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.text.formfield.focused.toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.hue.accent[900].toInts()).toEqual(legacy.accent.toInts())
|
||||
expect(resolved.hue.interactive[900].toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.hue.accent[800].toInts()).toEqual(legacy.accent.toInts())
|
||||
expect(resolved.hue.interactive[800].toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.background.action.primary.selected.toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.text.action.primary.selected.toInts()).toEqual(legacy.primary.toInts())
|
||||
expect(resolved.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts())
|
||||
expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual(
|
||||
legacy.backgroundPanel.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual(legacy.backgroundPanel.toInts())
|
||||
expect(resolved.contexts["@context:elevated"]?.background.action.primary.default.toInts()).toEqual([0, 0, 0, 0])
|
||||
expect(resolved.contexts["@context:elevated"]?.text.action.primary.default.toInts()).toEqual(
|
||||
legacy.text.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:overlay"]?.background.default.toInts()).toEqual(
|
||||
legacy.backgroundMenu.toInts(),
|
||||
)
|
||||
expect(resolved.contexts["@context:elevated"]?.text.action.primary.default.toInts()).toEqual(legacy.text.toInts())
|
||||
expect(resolved.contexts["@context:overlay"]?.background.default.toInts()).toEqual(legacy.backgroundMenu.toInts())
|
||||
expect(resolved.contexts["@context:overlay"]?.background.action.primary.default.toInts()).toEqual([0, 0, 0, 0])
|
||||
})
|
||||
|
||||
@@ -71,8 +65,10 @@ test("infers chromatic hues, anchors light and dark colors, and aliases ambiguou
|
||||
const darkRed = migrated.dark.hue?.red
|
||||
if (typeof lightRed !== "object" || typeof darkRed !== "object") throw new Error("Expected generated red scales")
|
||||
|
||||
expect(lightRed[900]).toBe("#ff6666")
|
||||
expect(darkRed[100]).toBe("#450000")
|
||||
expect(lightRed[800]).toBe("#ff6666")
|
||||
expect(darkRed[200]).toBe("#450000")
|
||||
expect(lightRed[900]).not.toBe(lightRed[800])
|
||||
expect(darkRed[100]).not.toBe(darkRed[200])
|
||||
expect(migrated.light.hue?.orange).toBe("$hue.gray")
|
||||
expect(migrated.light.hue?.yellow).toBe("$hue.gray")
|
||||
expect(migrated.light.hue?.green).toBe("$hue.gray")
|
||||
@@ -85,8 +81,13 @@ test("infers chromatic hues, anchors light and dark colors, and aliases ambiguou
|
||||
expect(() => resolveThemeFile(migrated, "dark")).not.toThrow()
|
||||
})
|
||||
|
||||
test("builds gray from V1 surfaces and text without using menus or borders", () => {
|
||||
test("builds and extrapolates gray from V1 surfaces and text without using menus or borders", () => {
|
||||
const source = structuredClone(DEFAULT_THEMES.opencode)
|
||||
source.theme.background = { light: "#eeeeee", dark: "#111111" }
|
||||
source.theme.backgroundPanel = { light: "#dddddd", dark: "#222222" }
|
||||
source.theme.backgroundElement = { light: "#cccccc", dark: "#333333" }
|
||||
source.theme.textMuted = { light: "#777777", dark: "#999999" }
|
||||
source.theme.text = { light: "#333333", dark: "#dddddd" }
|
||||
source.theme.backgroundMenu = { light: "#ededed", dark: "#252525" }
|
||||
const light = resolveV1(source, "light")
|
||||
const dark = resolveV1(source, "dark")
|
||||
@@ -95,16 +96,20 @@ test("builds gray from V1 surfaces and text without using menus or borders", ()
|
||||
const darkGray = migrated.dark.hue?.gray
|
||||
if (typeof lightGray !== "object" || typeof darkGray !== "object") throw new Error("Expected concrete gray scales")
|
||||
|
||||
expect(lightGray[100]).toBe(hex(light.background))
|
||||
expect(lightGray[200]).toBe(hex(light.backgroundPanel))
|
||||
expect(lightGray[300]).toBe(hex(light.backgroundElement))
|
||||
expect(lightGray[700]).toBe(hex(light.textMuted))
|
||||
expect(lightGray[900]).toBe(hex(light.text))
|
||||
expect(darkGray[100]).toBe(hex(dark.text))
|
||||
expect(darkGray[300]).toBe(hex(dark.textMuted))
|
||||
expect(darkGray[700]).toBe(hex(dark.backgroundElement))
|
||||
expect(darkGray[800]).toBe(hex(dark.backgroundPanel))
|
||||
expect(darkGray[900]).toBe(hex(dark.background))
|
||||
expect(lightGray[100]).not.toBe(lightGray[200])
|
||||
expect(lightGray[200]).toBe(hex(light.background))
|
||||
expect(lightGray[300]).toBe(hex(light.backgroundPanel))
|
||||
expect(lightGray[400]).toBe(hex(light.backgroundElement))
|
||||
expect(lightGray[600]).toBe(hex(light.textMuted))
|
||||
expect(lightGray[800]).toBe(hex(light.text))
|
||||
expect(lightGray[900]).not.toBe(lightGray[800])
|
||||
expect(darkGray[100]).not.toBe(darkGray[200])
|
||||
expect(darkGray[200]).toBe(hex(dark.text))
|
||||
expect(darkGray[400]).toBe(hex(dark.textMuted))
|
||||
expect(darkGray[600]).toBe(hex(dark.backgroundElement))
|
||||
expect(darkGray[700]).toBe(hex(dark.backgroundPanel))
|
||||
expect(darkGray[800]).toBe(hex(dark.background))
|
||||
expect(darkGray[900]).not.toBe(darkGray[800])
|
||||
|
||||
source.theme.borderSubtle = "#ff00ff"
|
||||
source.theme.border = "#00ff00"
|
||||
|
||||
Reference in New Issue
Block a user