diff --git a/packages/theme/src/tui/defaults.ts b/packages/theme/src/tui/defaults.ts index 268bf6593c5..7f3d0100a4c 100644 --- a/packages/theme/src/tui/defaults.ts +++ b/packages/theme/src/tui/defaults.ts @@ -136,9 +136,10 @@ export const DEFAULT_THEME = { }, background: { default: "$hue.neutral.200", - surface: { - offset: "$hue.neutral.300", - overlay: "$hue.neutral.400", + raised: { + base: "$hue.neutral.300", + high: "$hue.neutral.400", + max: "$hue.neutral.500", }, action: { primary: { @@ -161,7 +162,7 @@ export const DEFAULT_THEME = { }, formfield: { default: "$background.default", - $hovered: "$background.surface.offset", + $hovered: "$background.raised.base", $focused: "$background.action.primary.default", $pressed: "$hue.interactive.800", $disabled: "$background.default", @@ -220,14 +221,14 @@ export const DEFAULT_THEME = { "@context:elevated": { text: { action: { primary: { default: "$hue.neutral.100" } } }, background: { - default: "$background.surface.offset", - action: { primary: { default: "$hue.interactive.500", $hovered: "$background.surface.overlay" } }, + default: "$background.raised.base", + action: { primary: { default: "$hue.interactive.500", $hovered: "$background.raised.high" } }, }, }, "@context:overlay": { text: { action: { primary: { default: "$hue.neutral.100" } } }, background: { - default: "$background.surface.overlay", + default: "$background.raised.high", action: { primary: { default: "$hue.interactive.500" } }, }, }, @@ -357,9 +358,10 @@ export const DEFAULT_THEME = { }, background: { default: "$hue.neutral.800", - surface: { - offset: "$hue.neutral.700", - overlay: "$hue.neutral.600", + raised: { + base: "$hue.neutral.700", + high: "$hue.neutral.600", + max: "$hue.neutral.500", }, action: { primary: { @@ -382,7 +384,7 @@ export const DEFAULT_THEME = { }, formfield: { default: "$background.default", - $hovered: "$background.surface.offset", + $hovered: "$background.raised.base", $focused: "$background.action.primary.default", $pressed: "$hue.interactive.800", $disabled: "$background.default", @@ -441,14 +443,14 @@ export const DEFAULT_THEME = { "@context:elevated": { text: { action: { primary: { default: "$hue.neutral.200" } } }, background: { - default: "$background.surface.offset", - action: { primary: { default: "$hue.interactive.400", $hovered: "$background.surface.overlay" } }, + default: "$background.raised.base", + action: { primary: { default: "$hue.interactive.400", $hovered: "$background.raised.high" } }, }, }, "@context:overlay": { text: { action: { primary: { default: "$hue.neutral.200" } } }, background: { - default: "$background.surface.overlay", + default: "$background.raised.high", action: { primary: { default: "$hue.interactive.400" } }, }, }, diff --git a/packages/theme/src/tui/fallback.ts b/packages/theme/src/tui/fallback.ts index 1b738df08f3..7c210313dd9 100644 --- a/packages/theme/src/tui/fallback.ts +++ b/packages/theme/src/tui/fallback.ts @@ -15,7 +15,7 @@ export function fallback(mode: Mode): ThemeTokensDefinition { }, background: { default: red, - surface: { offset: red, overlay: red }, + raised: { base: red, high: red, max: red }, action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])), formfield: { default: red }, feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])), diff --git a/packages/theme/src/tui/schema.ts b/packages/theme/src/tui/schema.ts index 4d5387a75a8..f3213f13152 100644 --- a/packages/theme/src/tui/schema.ts +++ b/packages/theme/src/tui/schema.ts @@ -129,10 +129,11 @@ export type TextDefinition = Schema.Schema.Type const BackgroundDefinition = Schema.Struct({ default: Schema.optional(ColorValue), - surface: Schema.optional( + raised: Schema.optional( Schema.Struct({ - offset: Schema.optional(ColorValue), - overlay: Schema.optional(ColorValue), + base: Schema.optional(ColorValue), + high: Schema.optional(ColorValue), + max: Schema.optional(ColorValue), }), ), action: Schema.optional(ActionColorDefinition), diff --git a/packages/theme/src/tui/types.ts b/packages/theme/src/tui/types.ts index 1b821f0b582..12a76c8cfe6 100644 --- a/packages/theme/src/tui/types.ts +++ b/packages/theme/src/tui/types.ts @@ -40,9 +40,10 @@ export type ResolvedThemeTokens = { } readonly background: { readonly default: RGBA - readonly surface: { - readonly offset: RGBA - readonly overlay: RGBA + readonly raised: { + readonly base: RGBA + readonly high: RGBA + readonly max: RGBA } readonly action: Readonly> readonly formfield: FormfieldColor diff --git a/packages/theme/src/tui/v1-migrate.ts b/packages/theme/src/tui/v1-migrate.ts index 4307c635721..d27a8859c50 100644 --- a/packages/theme/src/tui/v1-migrate.ts +++ b/packages/theme/src/tui/v1-migrate.ts @@ -57,6 +57,7 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition { 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" + const backgroundRaisedMax = "$hue.neutral.500" return referenceHues({ hue: { @@ -102,9 +103,10 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition { }, background: { default: background, - surface: { - offset: backgroundPanel, - overlay: backgroundMenu, + raised: { + base: backgroundPanel, + high: backgroundMenu, + max: backgroundRaisedMax, }, action: { primary: { default: "transparent", $hovered: backgroundPanel, $focused: primary, $selected: "transparent" }, @@ -173,11 +175,11 @@ function migrateMode(theme: Theme, mode: Mode): FileThemeDefinition { }, "@context:elevated": { background: { - default: "$background.surface.offset", - action: { primary: { $hovered: "$background.surface.overlay" } }, + default: "$background.raised.base", + action: { primary: { $hovered: "$background.raised.high" } }, }, }, - "@context:overlay": { background: { default: "$background.surface.overlay" } }, + "@context:overlay": { background: { default: "$background.raised.high" } }, }) } diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index db5218d67e4..60448d9f8b0 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -350,7 +350,7 @@ export function Prompt(props: PromptProps) { createEffect(() => { if (!input || input.isDestroyed) return - input.cursorColor = disabled() ? theme.background.surface.offset : theme.text.default + input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default if (config.cursor) input.cursorStyle = config.cursor }) @@ -1642,7 +1642,7 @@ export function Prompt(props: PromptProps) { }) const maxHeight = createMemo(() => Math.max(6, Math.floor(dimensions().height / 3))) - const promptBg = createMemo(() => theme.raise(theme.background.surface.offset)) + const promptBg = createMemo(() => theme.raise(theme.background.raised.base)) return ( <> @@ -1799,7 +1799,7 @@ export function Prompt(props: PromptProps) { setTimeout(() => { // setTimeout is a workaround and needs to be addressed properly if (!input || input.isDestroyed) return - input.cursorColor = disabled() ? theme.background.surface.offset : theme.text.default + input.cursorColor = disabled() ? theme.background.raised.base : theme.text.default if (config.cursor) input.cursorStyle = config.cursor }, 0) }} @@ -1818,7 +1818,7 @@ export function Prompt(props: PromptProps) { r.stopPropagation() }} focusedBackgroundColor="transparent" - cursorColor={disabled() ? theme.background.surface.offset : theme.text.default} + cursorColor={disabled() ? theme.background.raised.base : theme.text.default} syntaxStyle={syntax()} /> diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index 4d19e8f6bd9..f76f4dd350b 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/packages/tui/src/component/session-tabs.tsx @@ -1530,7 +1530,7 @@ function HorizontalSessionTabs(props: { const lifted = (hovered() === tab.sessionID || dragged()) && !selected() const base = lifted ? theme.background.action.primary.hovered : theme.background.default // A dragged tab lifts to full selected elevation while it is held. - return tint(base, theme.raise(theme.background.surface.offset), dragged() ? 1 : selection()) + return tint(base, theme.raise(theme.background.raised.base), dragged() ? 1 : selection()) }) const pulseColor = () => tint(background(), theme.text.default, 0.45) // The edge flash washes toward a brighter stop on the same background-to-text ramp, diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index f5a40e85db8..f664d4da5cd 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -887,7 +887,7 @@ export function DiffViewerContent(props: { ) edge.backgroundColor = entry && reviewedFileNames().has(entry.file.file) - ? theme.background.surface.overlay + ? theme.background.raised.high : theme.diff.background.context } renderer.registerLifecyclePass(edge) @@ -911,7 +911,7 @@ export function DiffViewerContent(props: { {(entry, index) => { const reviewed = () => reviewedFileNames().has(entry.file.file) const background = () => - reviewed() ? theme.background.surface.overlay : theme.diff.background.context + reviewed() ? theme.background.raised.high : theme.diff.background.context const image = () => isDiffImageFile(entry.file.file) const countsWidth = () => (image() ? 6 : String(entry.file.additions).length + String(entry.file.deletions).length + 5) + diff --git a/packages/tui/src/mini/theme.ts b/packages/tui/src/mini/theme.ts index be3f5da556b..c8018c8068e 100644 --- a/packages/tui/src/mini/theme.ts +++ b/packages/tui/src/mini/theme.ts @@ -161,7 +161,7 @@ function map( surface: exact(elevated.background.default), pane: exact(theme.contextual.overlay.background.default), border: exact(theme.border.default), - line: exact(theme.background.surface.overlay), + line: exact(theme.background.raised.high), }, entry: { system: { body: scrollback(theme.text.subdued) }, @@ -174,7 +174,7 @@ function map( splash: { left: nearestIndexed(indexed, theme.text.subdued), right: nearestIndexed(indexed, theme.text.default), - leftShadow: nearestIndexed(indexed, theme.background.surface.offset), + leftShadow: nearestIndexed(indexed, theme.background.raised.base), }, block: { text: scrollback(theme.text.default), diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 0e142cceb8f..c833c9fea2d 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1292,7 +1292,7 @@ export function Session(props: { paddingLeft: 1, visible: showScrollbar(), trackOptions: { - backgroundColor: theme.raise(theme.background.surface.offset), + backgroundColor: theme.raise(theme.background.raised.base), foregroundColor: theme.border.default, }, }} @@ -1836,7 +1836,7 @@ function SessionReasoningGroupView(props: { (props: DialogSelectProps) { backgroundColor={ active() ? actionFocused() - ? theme.background.surface.overlay + ? theme.background.raised.high : (option.bg ?? theme.background.action.primary.focused) : RGBA.fromInts(0, 0, 0, 0) } diff --git a/packages/tui/test/mini/theme.test.ts b/packages/tui/test/mini/theme.test.ts index d15e277f6a9..32c78ef194f 100644 --- a/packages/tui/test/mini/theme.test.ts +++ b/packages/tui/test/mini/theme.test.ts @@ -99,7 +99,7 @@ function expectFooter(actual: RunTheme, theme: ResolvedTheme) { surface: theme.contextual.elevated.background.default, pane: theme.contextual.overlay.background.default, border: theme.border.default, - line: theme.background.surface.overlay, + line: theme.background.raised.high, } Object.entries(expected).forEach(([key, color]) => { expect(rgba(actual.footer[key as keyof typeof expected]).toInts()).toEqual(color.toInts()) diff --git a/packages/tui/test/theme/v2/component.test.ts b/packages/tui/test/theme/v2/component.test.ts index 264455030ee..20afe840a46 100644 --- a/packages/tui/test/theme/v2/component.test.ts +++ b/packages/tui/test/theme/v2/component.test.ts @@ -19,12 +19,12 @@ test("provides reactive properties, states, contexts, and color operations", () expect(theme.hue.interactive[500]).toBe(resolved().hue.interactive[500]) expect(theme.hue.gray[200]).toBe(resolved().hue.gray[200]) expect(theme.categorical.map((scale) => scale[500])).toEqual(resolved().categorical.map((scale) => scale[500])) - expect(theme.increase(theme.background.surface.offset, 1)).toBe(resolved().hue.neutral[400]) - expect(theme.raise(theme.background.surface.offset)).toBe(resolved().hue.neutral[400]) + expect(theme.increase(theme.background.raised.base, 1)).toBe(resolved().hue.neutral[400]) + expect(theme.raise(theme.background.raised.base)).toBe(resolved().hue.neutral[400]) expect(theme.decrease(theme.hue.red[300], 2)).toBe(resolved().hue.red[100]) expect(theme.increase(theme.hue.red[900], 3)).toBe(resolved().hue.red[900]) expect(theme.decrease(theme.hue.red[100], 3)).toBe(resolved().hue.red[100]) - expect(theme.source(theme.background.surface.offset)).toEqual({ hue: "neutral", step: 300 }) + expect(theme.source(theme.background.raised.base)).toEqual({ hue: "neutral", step: 300 }) const equivalent = RGBA.fromInts(...resolved().hue.green[500].toInts()) expect(theme.source(equivalent)).toBeUndefined() expect(theme.increase(equivalent, 1)).toBe(equivalent) @@ -46,8 +46,8 @@ test("provides reactive properties, states, contexts, and color operations", () expect(theme.background.formfield.selected).toBe(resolved().background.formfield.selected) expect(theme.background.formfield.focused).toBe(resolved().background.formfield.focused) expect(theme.background.formfield.disabled).toBe(resolved().background.formfield.disabled) - expect(theme.background.surface.offset).toBe(resolved().background.surface.offset) - expect(theme.background.surface.overlay).toBe(resolved().background.surface.overlay) + expect(theme.background.raised.base).toBe(resolved().background.raised.base) + expect(theme.background.raised.high).toBe(resolved().background.raised.high) expect(theme.scrollbar.default).toBe(resolved().scrollbar.default) expect(theme.diff.text.added).toBe(resolved().diff.text.added) @@ -58,14 +58,14 @@ test("provides reactive properties, states, contexts, and color operations", () expect(current().background.action.primary.focused).toBe( resolved().contextual.elevated.background.action.primary.focused, ) - expect(current().background.action.primary.hovered).toBe(resolved().background.surface.overlay) + expect(current().background.action.primary.hovered).toBe(resolved().background.raised.high) expect(current().background.formfield.selected).toBe(resolved().contextual.elevated.background.formfield.selected) setResolved(resolveTheme(selectTheme(DEFAULT_THEME, "dark"))) setMode("dark") expect(current().text.default).toBe(resolved().contextual.elevated.text.default) - expect(current().decrease(current().background.surface.offset, 1)).toBe(resolved().hue.neutral[600]) - expect(current().raise(current().background.surface.offset)).toBe(resolved().hue.neutral[600]) + expect(current().decrease(current().background.raised.base, 1)).toBe(resolved().hue.neutral[600]) + expect(current().raise(current().background.raised.base)).toBe(resolved().hue.neutral[600]) }) test("a stable component theme view follows presentation context changes", () => { diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index 0bccdf05afd..3b5a5d93785 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -71,21 +71,21 @@ test("resolves independent definitions and hue aliases", () => { expect(lightTheme.categorical[0]).toBe(lightTheme.hue.blue) expect(lightTheme.source(lightTheme.hue.blue[500])).toEqual({ hue: "blue", step: 500 }) expect(lightTheme.source(lightTheme.hue.neutral[200])).toEqual({ hue: "neutral", step: 200 }) - expect(lightTheme.source(lightTheme.background.surface.offset)).toEqual({ hue: "neutral", step: 300 }) + expect(lightTheme.source(lightTheme.background.raised.base)).toEqual({ hue: "neutral", step: 300 }) expect(lightTheme.increase(lightTheme.hue.red[100])).toBe(lightTheme.hue.red[200]) expect(lightTheme.decrease(lightTheme.hue.red[200])).toBe(lightTheme.hue.red[100]) expect(lightTheme.contextual.elevated.increase(lightTheme.hue.red[100])).toBe(lightTheme.hue.red[200]) expect(lightTheme.text.default).toBeInstanceOf(RGBA) expect(darkTheme.background.default).toBeInstanceOf(RGBA) - expect(lightTheme.background.surface.offset).toBe(lightTheme.hue.neutral[300]) - expect(lightTheme.background.surface.overlay).toBe(lightTheme.hue.neutral[400]) + expect(lightTheme.background.raised.base).toBe(lightTheme.hue.neutral[300]) + expect(lightTheme.background.raised.high).toBe(lightTheme.hue.neutral[400]) expect(lightTheme.syntax.keyword).toBeInstanceOf(RGBA) expect(lightTheme.text.action.primary.default).toBe(lightTheme.hue.neutral[200]) expect(lightTheme.contextual.elevated.background.action.primary.default).toBe(lightTheme.hue.interactive[500]) - expect(lightTheme.contextual.elevated.background.default).toBe(lightTheme.background.surface.offset) + expect(lightTheme.contextual.elevated.background.default).toBe(lightTheme.background.raised.base) expect(lightTheme.contextual.elevated.text.action.primary.default).toBe(lightTheme.hue.neutral[100]) expect(lightTheme.contextual.overlay.background.action.primary.default).toBe(lightTheme.hue.interactive[500]) - expect(lightTheme.contextual.overlay.background.default).toBe(lightTheme.background.surface.overlay) + expect(lightTheme.contextual.overlay.background.default).toBe(lightTheme.background.raised.high) expect(lightTheme.contextual.overlay.text.action.primary.default).toBe(lightTheme.hue.neutral[100]) expect(darkTheme.contextual.elevated.background.action.primary.default).toBe(darkTheme.hue.interactive[400]) expect(darkTheme.contextual.elevated.text.action.primary.default).toBe(darkTheme.hue.neutral[200]) @@ -244,7 +244,7 @@ test("resolves elevated hover surfaces from direct colors", () => { const theme = resolveSource( { version: 2, - light: { background: { surface: { offset: "#123456", overlay: "#234567" } } }, + light: { background: { raised: { base: "#123456", high: "#234567" } } }, dark: {}, }, "light", diff --git a/packages/tui/test/theme/v2/types.typecheck.ts b/packages/tui/test/theme/v2/types.typecheck.ts index c0c3e73d5e1..de0ec73c665 100644 --- a/packages/tui/test/theme/v2/types.typecheck.ts +++ b/packages/tui/test/theme/v2/types.typecheck.ts @@ -19,7 +19,7 @@ const text = { const background = { default: "$hue.neutral.100", - surface: { offset: "$hue.neutral.200", overlay: "$hue.neutral.300" }, + raised: { base: "$hue.neutral.200", high: "$hue.neutral.300", max: "$hue.neutral.400" }, action: { primary: { default: "$hue.interactive.600", diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index 33c9feba4b6..1ca2b291b6b 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -24,17 +24,17 @@ test("migrates resolved V1 modes into V2 tokens", () => { 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.200") - expect(migrated.light.background?.surface?.offset).toBe("$hue.neutral.300") - expect(migrated.light.background?.surface?.overlay).toBe("$hue.neutral.400") + expect(migrated.light.background?.raised?.base).toBe("$hue.neutral.300") + expect(migrated.light.background?.raised?.high).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.dark.background?.raised?.base).toBe("$hue.neutral.700") + expect(migrated.dark.background?.raised?.high).toBe("$hue.neutral.600") expect(migrated.light.text?.action?.primary?.default).toBe("$text.default") expect(migrated.light.text?.action?.secondary?.default).toBe("$text.subdued") expect(migrated.light.text?.action?.secondary?.$hovered).toBe("$text.default") expect(migrated.light.background?.action?.primary?.$selected).toBe("transparent") - expect(resolved.background.surface.offset.toInts()).toEqual(legacy.backgroundPanel.toInts()) - expect(resolved.background.surface.overlay.toInts()).toEqual(legacy.backgroundElement.toInts()) + expect(resolved.background.raised.base.toInts()).toEqual(legacy.backgroundPanel.toInts()) + expect(resolved.background.raised.high.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())