fix(tui): soften theme scale extremes (#37555)

This commit is contained in:
James Long
2026-07-20 09:20:20 -04:00
committed by GitHub
parent a9895a1e8a
commit 6702ce0d3f
4 changed files with 98 additions and 100 deletions
+9 -27
View File
@@ -44,7 +44,7 @@ function requestOptions(form: FormWithLocation) {
export function FormPrompt(props: { form: FormWithLocation }) { export function FormPrompt(props: { form: FormWithLocation }) {
const client = useClient() const client = useClient()
const { themeV2 } = useTheme().contextual("elevated") const { themeV2, mode: themeMode } = useTheme().contextual("elevated")
const renderer = useRenderer() const renderer = useRenderer()
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
const keymap = Keymap.use() const keymap = Keymap.use()
@@ -626,7 +626,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
<box <box
backgroundColor={themeV2.background()} backgroundColor={themeV2.background()}
border={["left"]} border={["left"]}
borderColor={themeV2.hue.accent(500)} borderColor={themeV2.hue.interactive(themeMode() === "light" ? 800 : 200)}
customBorderChars={SplitBorder.customBorderChars} customBorderChars={SplitBorder.customBorderChars}
> >
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}> <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
@@ -729,11 +729,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{external().url} {external().url}
</text> </text>
<text <text
fg={ fg={store.answers[external().key] === true ? themeV2.text.feedback.success() : themeV2.text.subdued()}
store.answers[external().key] === true
? themeV2.text.feedback.success()
: themeV2.text.subdued()
}
> >
{store.answers[external().key] === true {store.answers[external().key] === true
? "✓ Acknowledged" ? "✓ Acknowledged"
@@ -799,25 +795,15 @@ export function FormPrompt(props: { form: FormWithLocation }) {
> >
<box flexDirection="row"> <box flexDirection="row">
<box <box
backgroundColor={ backgroundColor={active() ? themeV2.background.formfield("focused") : themeV2.background()}
active() ? themeV2.background.formfield("focused") : themeV2.background()
}
paddingRight={1} paddingRight={1}
> >
<text fg={themeV2.text.formfield(active() ? "focused" : "default")}> <text fg={themeV2.text.formfield(active() ? "focused" : "default")}>{`${i() + 1}.`}</text>
{`${i() + 1}.`}
</text>
</box> </box>
<box <box
backgroundColor={ backgroundColor={active() ? themeV2.background.formfield("focused") : themeV2.background()}
active() ? themeV2.background.formfield("focused") : themeV2.background()
}
> >
<text <text fg={themeV2.text.formfield(active() ? "focused" : picked() ? "selected" : "default")}>
fg={themeV2.text.formfield(
active() ? "focused" : picked() ? "selected" : "default",
)}
>
{multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label} {multi() ? `[${picked() ? "✓" : " "}] ${row.label}` : row.label}
</text> </text>
</box> </box>
@@ -852,9 +838,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
{`${rows().length + 1}.`} {`${rows().length + 1}.`}
</text> </text>
</box> </box>
<box <box backgroundColor={other() ? themeV2.background.formfield("focused") : themeV2.background()}>
backgroundColor={other() ? themeV2.background.formfield("focused") : themeV2.background()}
>
<text <text
fg={ fg={
other() 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: themeV2.text.subdued() }}>{truncate(formLabel(item), 40)}:</span>{" "}
<span <span
style={{ style={{
fg: acknowledged() fg: acknowledged() ? themeV2.text.feedback.success() : themeV2.text.feedback.error(),
? themeV2.text.feedback.success()
: themeV2.text.feedback.error(),
}} }}
> >
{acknowledged() ? "Acknowledged" : "(acknowledgement required)"} {acknowledged() ? "Acknowledged" : "(acknowledgement required)"}
+19 -19
View File
@@ -97,18 +97,18 @@ export const DEFAULT_THEME = {
neutral: "$hue.gray", neutral: "$hue.gray",
}, },
text: { text: {
default: "$hue.neutral.900", default: "$hue.neutral.800",
subdued: "$hue.neutral.600", subdued: "$hue.neutral.600",
action: { action: {
primary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" }, primary: { default: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" }, destructive: { default: "$hue.red.200", $disabled: "$hue.neutral.500" },
}, },
formfield: { formfield: {
default: "$hue.neutral.900", default: "$hue.neutral.800",
$focused: "$text.action.primary.default", $focused: "$text.action.primary.default",
$pressed: "$hue.neutral.100", $pressed: "$hue.neutral.200",
$disabled: "$hue.neutral.500", $disabled: "$hue.neutral.500",
$selected: "$hue.interactive.600", $selected: "$hue.interactive.700",
}, },
feedback: { feedback: {
error: { default: "$hue.red.700", subdued: "$hue.red.600" }, error: { default: "$hue.red.700", subdued: "$hue.red.600" },
@@ -118,10 +118,10 @@ export const DEFAULT_THEME = {
}, },
}, },
background: { background: {
default: "$hue.neutral.100", default: "$hue.neutral.200",
surface: { surface: {
offset: "$hue.neutral.200", offset: "$hue.neutral.300",
overlay: "$hue.neutral.300", overlay: "$hue.neutral.400",
}, },
action: { action: {
primary: { primary: {
@@ -309,16 +309,16 @@ export const DEFAULT_THEME = {
neutral: "$hue.gray", neutral: "$hue.gray",
}, },
text: { text: {
default: "$hue.neutral.100", default: "$hue.neutral.200",
subdued: "$hue.neutral.400", subdued: "$hue.neutral.400",
action: { action: {
primary: { default: "$hue.neutral.100", $disabled: "$hue.neutral.500" }, primary: { default: "$hue.neutral.200", $disabled: "$hue.neutral.500" },
destructive: { default: "$hue.red.100", $disabled: "$hue.neutral.500" }, destructive: { default: "$hue.red.200", $disabled: "$hue.neutral.500" },
}, },
formfield: { formfield: {
default: "$hue.neutral.100", default: "$hue.neutral.200",
$focused: "$text.action.primary.default", $focused: "$text.action.primary.default",
$pressed: "$hue.neutral.100", $pressed: "$hue.neutral.200",
$disabled: "$hue.neutral.500", $disabled: "$hue.neutral.500",
$selected: "$hue.interactive.500", $selected: "$hue.interactive.500",
}, },
@@ -330,10 +330,10 @@ export const DEFAULT_THEME = {
}, },
}, },
background: { background: {
default: "$hue.neutral.900", default: "$hue.neutral.800",
surface: { surface: {
offset: "$hue.neutral.800", offset: "$hue.neutral.700",
overlay: "$hue.neutral.700", overlay: "$hue.neutral.600",
}, },
action: { action: {
primary: { primary: {
@@ -412,14 +412,14 @@ export const DEFAULT_THEME = {
codeBlock: "$hue.neutral.100", codeBlock: "$hue.neutral.100",
}, },
"@context:elevated": { "@context:elevated": {
text: { action: { primary: { default: "$hue.neutral.100" } } }, text: { action: { primary: { default: "$hue.neutral.200" } } },
background: { background: {
default: "$background.surface.offset", default: "$background.surface.offset",
action: { primary: { default: "$hue.interactive.400", $hovered: "$background.surface.overlay" } }, action: { primary: { default: "$hue.interactive.400", $hovered: "$background.surface.overlay" } },
}, },
}, },
"@context:overlay": { "@context:overlay": {
text: { action: { primary: { default: "$hue.neutral.900" } } }, text: { action: { primary: { default: "$hue.neutral.200" } } },
background: { background: {
default: "$background.surface.overlay", default: "$background.surface.overlay",
action: { primary: { default: "$hue.interactive.400" } }, action: { primary: { default: "$hue.interactive.400" } },
+29 -18
View File
@@ -26,12 +26,12 @@ function migrateMode(theme: Theme, mode: "light" | "dark"): ThemeFile["light"] {
const selected = hex(selectedForeground(theme, theme.primary)) const selected = hex(selectedForeground(theme, theme.primary))
const destructive = hex(selectedForeground(theme, theme.error)) const destructive = hex(selectedForeground(theme, theme.error))
const hues = inferHues(theme, mode) const hues = inferHues(theme, mode)
const text = mode === "light" ? "$hue.neutral.900" : "$hue.neutral.100" const text = mode === "light" ? "$hue.neutral.800" : "$hue.neutral.200"
const textMuted = mode === "light" ? "$hue.neutral.700" : "$hue.neutral.300" const textMuted = mode === "light" ? "$hue.neutral.600" : "$hue.neutral.400"
const primary = mode === "light" ? "$hue.interactive.900" : "$hue.interactive.100" const primary = mode === "light" ? "$hue.interactive.800" : "$hue.interactive.200"
const background = mode === "light" ? "$hue.neutral.100" : "$hue.neutral.900" const background = mode === "light" ? "$hue.neutral.200" : "$hue.neutral.800"
const backgroundPanel = mode === "light" ? "$hue.neutral.200" : "$hue.neutral.800" const backgroundPanel = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700"
const backgroundMenu = mode === "light" ? "$hue.neutral.300" : "$hue.neutral.700" const backgroundMenu = mode === "light" ? "$hue.neutral.400" : "$hue.neutral.600"
return { return {
hue: { hue: {
@@ -233,13 +233,13 @@ function selectedForeground(theme: Theme, background: RGBA) {
function hueScale(color: RGBA, mode: "light" | "dark") { function hueScale(color: RGBA, mode: "light" | "dark") {
const value = toOklch(color) 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 endpoint = mode === "light" ? Math.max(0.97, value.l) : Math.min(0.18, value.l)
const alpha = color.toInts()[3] const alpha = color.toInts()[3]
return Object.fromEntries( return Object.fromEntries(
HueStep.literals.map((step) => { HueStep.literals.map((step) => {
if (step === anchor) return [step, hex(color)] 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({ const generated = oklchToHex({
l: value.l + (endpoint - value.l) * progress, l: value.l + (endpoint - value.l) * progress,
c: value.c * (1 - progress * 0.5), c: value.c * (1 - progress * 0.5),
@@ -256,8 +256,14 @@ function neutralScale(theme: Theme, mode: "light" | "dark") {
HueStep.literals.map((step) => { HueStep.literals.map((step) => {
const exact = anchors.find((anchor) => anchor.step === step) const exact = anchors.find((anchor) => anchor.step === step)
if (exact) return [step, hex(exact.color)] if (exact) return [step, hex(exact.color)]
const lower = anchors.filter((anchor) => anchor.step < step).at(-1)! const first = anchors[0]!
const upper = anchors.find((anchor) => anchor.step > step)! 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))] return [step, interpolate(lower.color, upper.color, (step - lower.step) / (upper.step - lower.step))]
}), }),
) as Record<HueStep, string> ) as Record<HueStep, string>
@@ -265,11 +271,11 @@ function neutralScale(theme: Theme, mode: "light" | "dark") {
function neutralAnchors(theme: Theme, mode: "light" | "dark") { function neutralAnchors(theme: Theme, mode: "light" | "dark") {
const light: { step: HueStep; color: RGBA }[] = [ const light: { step: HueStep; color: RGBA }[] = [
{ step: 100, color: theme.background }, { step: 200, color: theme.background },
{ step: 200, color: theme.backgroundPanel }, { step: 300, color: theme.backgroundPanel },
{ step: 300, color: theme.backgroundElement || theme.backgroundMenu }, { step: 400, color: theme.backgroundElement || theme.backgroundMenu },
{ step: 700, color: theme.textMuted }, { step: 600, color: theme.textMuted },
{ step: 900, color: theme.text }, { step: 800, color: theme.text },
] ]
if (mode === "light") return light if (mode === "light") return light
return light.toReversed().map((source) => ({ ...source, step: (1000 - source.step) as HueStep })) 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) { function interpolate(first: RGBA, second: RGBA, amount: number) {
const start = toOklch(first) const start = toOklch(first)
const end = toOklch(second) 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({ const generated = oklchToHex({
l: start.l + (end.l - start.l) * amount, l: start.l + (end.l - start.l) * amount,
c: start.c + (end.c - start.c) * 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)}` return alpha === 255 ? generated : `${generated}${byte(alpha)}`
} }
+41 -36
View File
@@ -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") { if (typeof migrated.light.hue?.accent !== "object" || typeof migrated.light.hue.interactive !== "object") {
throw new Error("Expected concrete accent and interactive scales") throw new Error("Expected concrete accent and interactive scales")
} }
expect(migrated.light.hue.accent[900]).toBe(hex(legacy.accent)) expect(migrated.light.hue.accent[800]).toBe(hex(legacy.accent))
expect(migrated.light.hue.interactive[900]).toBe(hex(legacy.primary)) expect(migrated.light.hue.interactive[800]).toBe(hex(legacy.primary))
expect(migrated.light.text?.default).toBe("$hue.neutral.900") expect(migrated.light.text?.default).toBe("$hue.neutral.800")
expect(migrated.light.text?.subdued).toBe("$hue.neutral.700") expect(migrated.light.text?.subdued).toBe("$hue.neutral.600")
expect(migrated.light.background?.action?.primary?.default).toBe("transparent") expect(migrated.light.background?.action?.primary?.default).toBe("transparent")
expect(migrated.light.background?.default).toBe("$hue.neutral.100") expect(migrated.light.background?.default).toBe("$hue.neutral.200")
expect(migrated.light.background?.surface?.offset).toBe("$hue.neutral.200") expect(migrated.light.background?.surface?.offset).toBe("$hue.neutral.300")
expect(migrated.light.background?.surface?.overlay).toBe("$hue.neutral.300") expect(migrated.light.background?.surface?.overlay).toBe("$hue.neutral.400")
expect(migrated.dark.background?.default).toBe("$hue.neutral.900") expect(migrated.dark.background?.default).toBe("$hue.neutral.800")
expect(migrated.dark.background?.surface?.offset).toBe("$hue.neutral.800") expect(migrated.dark.background?.surface?.offset).toBe("$hue.neutral.700")
expect(migrated.dark.background?.surface?.overlay).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.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.scrollbar?.default).toBe(hex(legacy.borderActive))
expect(migrated.light.diff?.lineNumber?.background?.removed).toBe(hex(legacy.diffRemovedLineNumberBg)) expect(migrated.light.diff?.lineNumber?.background?.removed).toBe(hex(legacy.diffRemovedLineNumberBg))
expect(migrated.light.markdown?.emphasis).toBe(hex(legacy.markdownEmph)) expect(migrated.light.markdown?.emphasis).toBe(hex(legacy.markdownEmph))
expect(resolved.background.surface.offset.toInts()).toEqual(legacy.backgroundPanel.toInts()) 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.selected.toInts()).toEqual(legacy.background.toInts())
expect(resolved.background.formfield.focused.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.default.toInts()).toEqual(legacy.text.toInts())
expect(resolved.text.formfield.selected.toInts()).toEqual(legacy.primary.toInts()) expect(resolved.text.formfield.selected.toInts()).toEqual(legacy.primary.toInts())
expect(resolved.text.formfield.focused.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.accent[800].toInts()).toEqual(legacy.accent.toInts())
expect(resolved.hue.interactive[900].toInts()).toEqual(legacy.primary.toInts()) expect(resolved.hue.interactive[800].toInts()).toEqual(legacy.primary.toInts())
expect(resolved.background.action.primary.selected.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.text.action.primary.selected.toInts()).toEqual(legacy.primary.toInts())
expect(resolved.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts()) expect(resolved.background.feedback.error.default.toInts()).toEqual(legacy.background.toInts())
expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual( expect(resolved.contexts["@context:elevated"]?.background.default.toInts()).toEqual(legacy.backgroundPanel.toInts())
legacy.backgroundPanel.toInts(),
)
expect(resolved.contexts["@context:elevated"]?.background.action.primary.default.toInts()).toEqual([0, 0, 0, 0]) 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( expect(resolved.contexts["@context:elevated"]?.text.action.primary.default.toInts()).toEqual(legacy.text.toInts())
legacy.text.toInts(), expect(resolved.contexts["@context:overlay"]?.background.default.toInts()).toEqual(legacy.backgroundMenu.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]) 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 const darkRed = migrated.dark.hue?.red
if (typeof lightRed !== "object" || typeof darkRed !== "object") throw new Error("Expected generated red scales") if (typeof lightRed !== "object" || typeof darkRed !== "object") throw new Error("Expected generated red scales")
expect(lightRed[900]).toBe("#ff6666") expect(lightRed[800]).toBe("#ff6666")
expect(darkRed[100]).toBe("#450000") 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?.orange).toBe("$hue.gray")
expect(migrated.light.hue?.yellow).toBe("$hue.gray") expect(migrated.light.hue?.yellow).toBe("$hue.gray")
expect(migrated.light.hue?.green).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() 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) 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" } source.theme.backgroundMenu = { light: "#ededed", dark: "#252525" }
const light = resolveV1(source, "light") const light = resolveV1(source, "light")
const dark = resolveV1(source, "dark") 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 const darkGray = migrated.dark.hue?.gray
if (typeof lightGray !== "object" || typeof darkGray !== "object") throw new Error("Expected concrete gray scales") if (typeof lightGray !== "object" || typeof darkGray !== "object") throw new Error("Expected concrete gray scales")
expect(lightGray[100]).toBe(hex(light.background)) expect(lightGray[100]).not.toBe(lightGray[200])
expect(lightGray[200]).toBe(hex(light.backgroundPanel)) expect(lightGray[200]).toBe(hex(light.background))
expect(lightGray[300]).toBe(hex(light.backgroundElement)) expect(lightGray[300]).toBe(hex(light.backgroundPanel))
expect(lightGray[700]).toBe(hex(light.textMuted)) expect(lightGray[400]).toBe(hex(light.backgroundElement))
expect(lightGray[900]).toBe(hex(light.text)) expect(lightGray[600]).toBe(hex(light.textMuted))
expect(darkGray[100]).toBe(hex(dark.text)) expect(lightGray[800]).toBe(hex(light.text))
expect(darkGray[300]).toBe(hex(dark.textMuted)) expect(lightGray[900]).not.toBe(lightGray[800])
expect(darkGray[700]).toBe(hex(dark.backgroundElement)) expect(darkGray[100]).not.toBe(darkGray[200])
expect(darkGray[800]).toBe(hex(dark.backgroundPanel)) expect(darkGray[200]).toBe(hex(dark.text))
expect(darkGray[900]).toBe(hex(dark.background)) 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.borderSubtle = "#ff00ff"
source.theme.border = "#00ff00" source.theme.border = "#00ff00"