diff --git a/packages/theme/src/tui/categorical.ts b/packages/theme/src/tui/categorical.ts new file mode 100644 index 00000000000..f0c90947659 --- /dev/null +++ b/packages/theme/src/tui/categorical.ts @@ -0,0 +1,3 @@ +import type { HueName } from "./schema.js" + +export const DEFAULT_CATEGORICAL = ["blue", "purple", "green", "orange", "red", "cyan"] as const satisfies readonly HueName[] diff --git a/packages/theme/src/tui/defaults.ts b/packages/theme/src/tui/defaults.ts deleted file mode 100644 index 2e72787cd78..00000000000 --- a/packages/theme/src/tui/defaults.ts +++ /dev/null @@ -1,452 +0,0 @@ -import type { BaseThemeDefinition, HueName, Mode, ThemeDefinition, ThemeDocument } from "./schema.js" - -export const DEFAULT_CATEGORICAL = [ - "blue", - "purple", - "green", - "orange", - "red", - "cyan", -] as const satisfies readonly HueName[] - -const modes = { - light: { - hue: { - gray: { - 100: "#111827", - 200: "#1f2937", - 300: "#374151", - 400: "#4b5563", - 500: "#6b7280", - 600: "#9ca3af", - 700: "#d1d5db", - 800: "#e5e7eb", - 900: "#f3f4f6", - }, - red: { - 100: "#7f1d1d", - 200: "#991b1b", - 300: "#b91c1c", - 400: "#dc2626", - 500: "#ef4444", - 600: "#f87171", - 700: "#fca5a5", - 800: "#fecaca", - 900: "#fee2e2", - }, - orange: { - 100: "#7c2d12", - 200: "#9a3412", - 300: "#c2410c", - 400: "#ea580c", - 500: "#f97316", - 600: "#fb923c", - 700: "#fdba74", - 800: "#fed7aa", - 900: "#ffedd5", - }, - yellow: { - 100: "#713f12", - 200: "#854d0e", - 300: "#a16207", - 400: "#ca8a04", - 500: "#eab308", - 600: "#facc15", - 700: "#fde047", - 800: "#fef08a", - 900: "#fef9c3", - }, - green: { - 100: "#14532d", - 200: "#166534", - 300: "#15803d", - 400: "#16a34a", - 500: "#22c55e", - 600: "#4ade80", - 700: "#86efac", - 800: "#bbf7d0", - 900: "#dcfce7", - }, - cyan: { - 100: "#164e63", - 200: "#155e75", - 300: "#0e7490", - 400: "#0891b2", - 500: "#06b6d4", - 600: "#22d3ee", - 700: "#67e8f9", - 800: "#a5f3fc", - 900: "#cffafe", - }, - blue: { - 100: "#1e3a8a", - 200: "#1e40af", - 300: "#1d4ed8", - 400: "#2563eb", - 500: "#3b82f6", - 600: "#60a5fa", - 700: "#93c5fd", - 800: "#bfdbfe", - 900: "#dbeafe", - }, - purple: { - 100: "#581c87", - 200: "#6b21a8", - 300: "#7e22ce", - 400: "#9333ea", - 500: "#a855f7", - 600: "#c084fc", - 700: "#d8b4fe", - 800: "#e9d5ff", - 900: "#f3e8ff", - }, - accent: "$hue.blue", - interactive: "$hue.blue", - neutral: "$hue.gray", - }, - categorical: DEFAULT_CATEGORICAL, - text: { - base: "$hue.neutral.200", - muted: "$hue.neutral.400", - action: { - primary: { base: "$hue.neutral.800", $disabled: "$hue.neutral.500" }, - secondary: { base: "$text.muted", $hovered: "$text.base" }, - destructive: { base: "$hue.red.800", $disabled: "$hue.neutral.500" }, - }, - formfield: { - base: "$hue.neutral.200", - $focused: "$text.action.primary.base", - $pressed: "$hue.neutral.800", - $disabled: "$hue.neutral.500", - $selected: "$hue.interactive.300", - }, - status: { - running: "$hue.interactive.200", - question: "$text.status.unread", - permission: "$text.status.unread", - unread: "$hue.accent.200", - }, - feedback: { - error: { base: "$hue.red.300", muted: "$hue.red.400" }, - warning: { base: "$hue.yellow.200", muted: "$hue.yellow.300" }, - success: { base: "$hue.green.300", muted: "$hue.green.400" }, - info: { base: "$hue.cyan.300", muted: "$hue.cyan.400" }, - }, - }, - background: { - base: "$hue.neutral.800", - raised: { - base: "$hue.neutral.700", - high: "$hue.neutral.600", - max: "$hue.neutral.500", - }, - action: { - primary: { - base: "$hue.interactive.400", - $hovered: "$hue.interactive.300", - $focused: "$hue.interactive.300", - $pressed: "$hue.interactive.200", - $selected: "$hue.interactive.300", - $disabled: "$hue.neutral.700", - }, - secondary: { base: "transparent" }, - destructive: { - base: "$hue.red.400", - $hovered: "$hue.red.300", - $focused: "$hue.red.300", - $pressed: "$hue.red.200", - $selected: "$hue.red.300", - $disabled: "$hue.neutral.700", - }, - }, - formfield: { - base: "$background.base", - $hovered: "$background.raised.base", - $focused: "$background.action.primary.base", - $pressed: "$hue.interactive.200", - $disabled: "$background.base", - $selected: "$background.formfield.base", - }, - feedback: { - error: { base: "$background.base" }, - warning: { base: "$background.base" }, - success: { base: "$background.base" }, - info: { base: "$background.base" }, - }, - }, - border: { base: "$hue.neutral.700" }, - scrollbar: { base: "$hue.neutral.600" }, - diff: { - text: { - added: "$hue.green.300", - removed: "$hue.red.300", - context: "$hue.neutral.100", - hunkHeader: "$hue.purple.400", - }, - background: { added: "$hue.green.900", removed: "$hue.red.900", context: "$hue.neutral.900" }, - highlight: { added: "$hue.green.400", removed: "$hue.red.400" }, - lineNumber: { - text: "$hue.neutral.400", - background: { added: "$hue.green.800", removed: "$hue.red.800" }, - }, - }, - syntax: { - comment: "$hue.neutral.400", - keyword: "$hue.purple.400", - function: "$hue.accent.400", - variable: "$hue.neutral.100", - string: "$hue.green.300", - number: "$hue.yellow.200", - type: "$hue.yellow.500", - operator: "$hue.cyan.400", - punctuation: "$hue.neutral.100", - }, - markdown: { - text: "$hue.neutral.100", - heading: "$hue.purple.400", - link: "$hue.accent.400", - linkText: "$hue.cyan.400", - code: "$hue.green.300", - blockQuote: "$hue.neutral.400", - emphasis: "$hue.yellow.500", - strong: "$hue.neutral.100", - horizontalRule: "$hue.neutral.700", - listItem: "$hue.accent.400", - listEnumeration: "$hue.cyan.400", - image: "$hue.accent.400", - imageText: "$hue.cyan.400", - codeBlock: "$hue.neutral.100", - }, - "@dialog": { - text: { action: { primary: { base: "$hue.neutral.900" } } }, - background: { - base: "$background.raised.base", - action: { primary: { base: "$hue.interactive.500", $hovered: "$background.raised.high" } }, - }, - }, - }, - dark: { - hue: { - gray: { - 100: "#f3f4f6", - 200: "#e5e7eb", - 300: "#d1d5db", - 400: "#9ca3af", - 500: "#6b7280", - 600: "#4b5563", - 700: "#374151", - 800: "#1f2937", - 900: "#111827", - }, - red: { - 100: "#fee2e2", - 200: "#fecaca", - 300: "#fca5a5", - 400: "#f87171", - 500: "#ef4444", - 600: "#dc2626", - 700: "#b91c1c", - 800: "#991b1b", - 900: "#7f1d1d", - }, - orange: { - 100: "#ffedd5", - 200: "#fed7aa", - 300: "#fdba74", - 400: "#fb923c", - 500: "#f97316", - 600: "#ea580c", - 700: "#c2410c", - 800: "#9a3412", - 900: "#7c2d12", - }, - yellow: { - 100: "#fef9c3", - 200: "#fef08a", - 300: "#fde047", - 400: "#facc15", - 500: "#eab308", - 600: "#ca8a04", - 700: "#a16207", - 800: "#854d0e", - 900: "#713f12", - }, - green: { - 100: "#dcfce7", - 200: "#bbf7d0", - 300: "#86efac", - 400: "#4ade80", - 500: "#22c55e", - 600: "#16a34a", - 700: "#15803d", - 800: "#166534", - 900: "#14532d", - }, - cyan: { - 100: "#cffafe", - 200: "#a5f3fc", - 300: "#67e8f9", - 400: "#22d3ee", - 500: "#06b6d4", - 600: "#0891b2", - 700: "#0e7490", - 800: "#155e75", - 900: "#164e63", - }, - blue: { - 100: "#dbeafe", - 200: "#bfdbfe", - 300: "#93c5fd", - 400: "#60a5fa", - 500: "#3b82f6", - 600: "#2563eb", - 700: "#1d4ed8", - 800: "#1e40af", - 900: "#1e3a8a", - }, - purple: { - 100: "#f3e8ff", - 200: "#e9d5ff", - 300: "#d8b4fe", - 400: "#c084fc", - 500: "#a855f7", - 600: "#9333ea", - 700: "#7e22ce", - 800: "#6b21a8", - 900: "#581c87", - }, - accent: "$hue.blue", - interactive: "$hue.blue", - neutral: "$hue.gray", - }, - categorical: DEFAULT_CATEGORICAL, - text: { - base: "$hue.neutral.200", - muted: "$hue.neutral.400", - action: { - primary: { base: "$hue.neutral.200", $disabled: "$hue.neutral.500" }, - secondary: { base: "$text.muted", $hovered: "$text.base" }, - destructive: { base: "$hue.red.200", $disabled: "$hue.neutral.500" }, - }, - formfield: { - base: "$hue.neutral.200", - $focused: "$text.action.primary.base", - $pressed: "$hue.neutral.200", - $disabled: "$hue.neutral.500", - $selected: "$hue.interactive.500", - }, - status: { - running: "$hue.interactive.200", - question: "$text.status.unread", - permission: "$text.status.unread", - unread: "$hue.accent.200", - }, - feedback: { - error: { base: "$hue.red.300", muted: "$hue.red.400" }, - warning: { base: "$hue.yellow.200", muted: "$hue.yellow.300" }, - success: { base: "$hue.green.300", muted: "$hue.green.400" }, - info: { base: "$hue.cyan.300", muted: "$hue.cyan.400" }, - }, - }, - background: { - base: "$hue.neutral.800", - raised: { - base: "$hue.neutral.700", - high: "$hue.neutral.600", - max: "$hue.neutral.500", - }, - action: { - primary: { - base: "$hue.interactive.500", - $hovered: "$hue.interactive.600", - $focused: "$hue.interactive.600", - $pressed: "$hue.interactive.800", - $selected: "$hue.interactive.600", - $disabled: "$hue.neutral.800", - }, - secondary: { base: "transparent" }, - destructive: { - base: "$hue.red.600", - $hovered: "$hue.red.700", - $focused: "$hue.red.700", - $pressed: "$hue.red.800", - $selected: "$hue.red.700", - $disabled: "$hue.neutral.800", - }, - }, - formfield: { - base: "$background.base", - $hovered: "$background.raised.base", - $focused: "$background.action.primary.base", - $pressed: "$hue.interactive.800", - $disabled: "$background.base", - $selected: "$background.formfield.base", - }, - feedback: { - error: { base: "$background.base" }, - warning: { base: "$background.base" }, - success: { base: "$background.base" }, - info: { base: "$background.base" }, - }, - }, - border: { base: "$hue.neutral.700" }, - scrollbar: { base: "$hue.neutral.600" }, - diff: { - text: { - added: "$hue.green.300", - removed: "$hue.red.300", - context: "$hue.neutral.100", - hunkHeader: "$hue.purple.400", - }, - background: { added: "$hue.green.900", removed: "$hue.red.900", context: "$hue.neutral.900" }, - highlight: { added: "$hue.green.400", removed: "$hue.red.400" }, - lineNumber: { - text: "$hue.neutral.400", - background: { added: "$hue.green.800", removed: "$hue.red.800" }, - }, - }, - syntax: { - comment: "$hue.neutral.400", - keyword: "$hue.purple.400", - function: "$hue.accent.400", - variable: "$hue.neutral.100", - string: "$hue.green.300", - number: "$hue.yellow.200", - type: "$hue.yellow.500", - operator: "$hue.cyan.400", - punctuation: "$hue.neutral.100", - }, - markdown: { - text: "$hue.neutral.100", - heading: "$hue.purple.400", - link: "$hue.accent.400", - linkText: "$hue.cyan.400", - code: "$hue.green.300", - blockQuote: "$hue.neutral.400", - emphasis: "$hue.yellow.500", - strong: "$hue.neutral.100", - horizontalRule: "$hue.neutral.700", - listItem: "$hue.accent.400", - listEnumeration: "$hue.cyan.400", - image: "$hue.accent.400", - imageText: "$hue.cyan.400", - codeBlock: "$hue.neutral.100", - }, - "@dialog": { - text: { action: { primary: { base: "$hue.neutral.200" } } }, - background: { - base: "$background.raised.base", - action: { primary: { base: "$hue.interactive.400", $hovered: "$background.raised.high" } }, - }, - }, - }, -} satisfies Record - -const { hue: _, ...base } = modes.light - -export const DEFAULT_THEME = { - version: 2, - base: base satisfies BaseThemeDefinition, - light: { hue: modes.light.hue }, - dark: modes.dark, -} satisfies ThemeDocument diff --git a/packages/theme/src/tui/index.ts b/packages/theme/src/tui/index.ts index 1e8b78cebe8..a9bca0c12ce 100644 --- a/packages/theme/src/tui/index.ts +++ b/packages/theme/src/tui/index.ts @@ -42,7 +42,7 @@ export type { ResolvedThemeTokens, StatefulColor, } from "./types.js" -export { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults.js" +export { DEFAULT_CATEGORICAL } from "./categorical.js" export { expandTheme } from "./expand.js" export { migrateV1 } from "./v1-migrate.js" export { resolveTheme, resolveThemeDocument, themeDecodeError } from "./resolve.js" diff --git a/packages/theme/src/tui/schema.ts b/packages/theme/src/tui/schema.ts index e55017d3781..9ab7c58d99d 100644 --- a/packages/theme/src/tui/schema.ts +++ b/packages/theme/src/tui/schema.ts @@ -93,14 +93,6 @@ const TextDefinition = Schema.Struct({ muted: Schema.optional(ColorValue), action: Schema.optional(ActionColorDefinition), formfield: Schema.optional(StatefulColorDefinition), - status: Schema.optional( - Schema.Struct({ - running: Schema.optional(ColorValue), - question: Schema.optional(ColorValue), - permission: Schema.optional(ColorValue), - unread: Schema.optional(ColorValue), - }), - ), feedback: Schema.optional( Schema.Struct({ error: Schema.optional(TextFeedbackDefinition), @@ -234,12 +226,6 @@ const CompleteThemeTokensDefinition = Schema.Struct({ muted: ColorValue, action: CompleteActionColorDefinition, formfield: CompleteStatefulColorDefinition, - status: Schema.Struct({ - running: ColorValue, - question: ColorValue, - permission: ColorValue, - unread: ColorValue, - }), feedback: Schema.Struct({ error: CompleteTextFeedbackDefinition, warning: CompleteTextFeedbackDefinition, diff --git a/packages/theme/src/tui/types.ts b/packages/theme/src/tui/types.ts index c76148e6f93..480711018ab 100644 --- a/packages/theme/src/tui/types.ts +++ b/packages/theme/src/tui/types.ts @@ -34,12 +34,6 @@ export type ResolvedThemeTokens = { readonly muted: RGBA readonly action: Readonly> readonly formfield: FormfieldColor - readonly status: { - readonly running: RGBA - readonly question: RGBA - readonly permission: RGBA - readonly unread: RGBA - } readonly feedback: Readonly> } readonly background: { diff --git a/packages/theme/src/tui/v1-migrate.ts b/packages/theme/src/tui/v1-migrate.ts index a4039acfd25..828f22f4474 100644 --- a/packages/theme/src/tui/v1-migrate.ts +++ b/packages/theme/src/tui/v1-migrate.ts @@ -1,6 +1,6 @@ import { RGBA } from "@opentui/core" import { oklchToHex, rgbToOklch } from "./color.js" -import { DEFAULT_CATEGORICAL } from "./defaults.js" +import { DEFAULT_CATEGORICAL } from "./categorical.js" import type { BaseThemeDefinition, HueDefinition, Mode, ThemeDefinition, ThemeDocument } from "./index.js" import { HueStep } from "./schema.js" import type { Theme, ThemeV1Json } from "./v1.js" @@ -130,12 +130,6 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition { $disabled: textMuted, $selected: primary, }, - status: { - running: "$hue.interactive.200", - question: "$text.status.unread", - permission: "$text.status.unread", - unread: "$hue.accent.200", - }, feedback: { error: { base: color("error") }, warning: { base: color("warning") }, diff --git a/packages/theme/test/status.test.ts b/packages/theme/test/status.test.ts deleted file mode 100644 index a0feb649cf1..00000000000 --- a/packages/theme/test/status.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { expect, test } from "bun:test" -import { RGBA } from "@opentui/core" -import { Schema } from "effect" -import { DEFAULT_THEME, ThemeDocument, migrateV1, resolveThemeDocument, selectTheme } from "../src/tui/index.js" -import type { ThemeV1Json } from "../src/tui/v1.js" - -test.each(["light", "dark"] as const)("built-in %s themes resolve status colors", async (mode) => { - const source: ThemeV1Json = await Bun.file( - new URL("../../tui/src/theme/assets/opencode.json", import.meta.url), - ).json() - for (const document of [DEFAULT_THEME, migrateV1(source)]) { - const theme = resolveThemeDocument(document, mode) - expect(theme.text.status.running.equals(theme.hue.interactive[200])).toBeTrue() - expect(theme.text.status.question.equals(theme.text.status.unread)).toBeTrue() - expect(theme.text.status.permission.equals(theme.text.status.unread)).toBeTrue() - expect(theme.text.status.unread.equals(theme.hue.accent[200])).toBeTrue() - expect(theme.surface("dialog").text.status).toEqual(theme.text.status) - } -}) - -test.each(["light", "dark"] as const)("custom %s themes inherit the unread attention color", (mode) => { - const base = selectTheme(DEFAULT_THEME, mode) - const definition = { - ...base, - hue: { ...base.hue, accent: "$hue.purple" }, - text: { - ...base.text, - status: { ...base.text.status, unread: "#abcdef" }, - }, - } - const { hue, ...tokens } = definition - const theme = resolveThemeDocument( - Schema.decodeUnknownSync(ThemeDocument)({ - version: 2, - base: tokens, - [mode]: { hue }, - }), - mode, - ) - expect(theme.text.status.unread.equals(RGBA.fromHex("#abcdef"))).toBeTrue() - expect(theme.text.status.question.equals(theme.text.status.unread)).toBeTrue() - expect(theme.text.status.permission.equals(theme.text.status.unread)).toBeTrue() - expect(theme.surface("dialog").text.status).toEqual(theme.text.status) -}) - -test.each(["light", "dark"] as const)("custom %s themes inherit and override status colors", (mode) => { - const base = selectTheme(DEFAULT_THEME, mode) - const definition = { - ...base, - hue: { ...base.hue, interactive: "$hue.purple", accent: "$hue.orange" }, - text: { - ...base.text, - status: { - ...base.text.status, - question: "#123456", - permission: "#654321", - }, - }, - } - const { hue, ...tokens } = definition - const theme = resolveThemeDocument( - Schema.decodeUnknownSync(ThemeDocument)({ - version: 2, - base: tokens, - [mode]: { hue }, - }), - mode, - ) - expect(theme.text.status.running.equals(theme.hue.purple[200])).toBeTrue() - expect(theme.text.status.unread.equals(theme.hue.orange[200])).toBeTrue() - expect(theme.text.status.question.equals(RGBA.fromHex("#123456"))).toBeTrue() - expect(theme.text.status.permission.equals(RGBA.fromHex("#654321"))).toBeTrue() -}) diff --git a/packages/theme/test/theme.test.ts b/packages/theme/test/theme.test.ts new file mode 100644 index 00000000000..1e763af0cef --- /dev/null +++ b/packages/theme/test/theme.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { migrateV1, resolveThemeDocument } from "../src/tui/index.js" +import type { ThemeV1Json } from "../src/tui/v1.js" + +const source: ThemeV1Json = await Bun.file( + new URL("../../tui/src/theme/assets/opencode.json", import.meta.url), +).json() +const document = migrateV1(source) + +test.each(["light", "dark"] as const)("resolves %s themes without status tokens", (mode) => { + const theme = resolveThemeDocument(document, mode) + expect("status" in theme.text).toBeFalse() + expect(theme.hue.accent[800]).toBeDefined() + expect(theme.hue.interactive[800]).toBeDefined() +}) diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index 7611a3abc5e..2a681b5a758 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/packages/tui/src/component/session-tabs.tsx @@ -120,7 +120,7 @@ const glowTextColor = (base: RGBA, glow: RGBA, index: number, width: number, lev tint(base, glow, 0.12 * unreadGlowIntensity(index, width) * level) function tabFeedbackColor(status: SessionTabsStatus, theme: ReturnType) { - if (status.attention) return theme.text.status[status.attention] + if (status.attention) return theme.hue.interactive[800] if (status.unread === "error") return theme.text.feedback.error.base return undefined } @@ -528,8 +528,8 @@ function VerticalSessionTabs(props: { const compact = createMemo(() => width() < SESSION_TABS_COMPACT_BREAKPOINT) const tooltipWidth = () => Math.min(54, dimensions().width - width()) const stride = () => (compact() ? 2 : 3) - const unreadColor = () => theme.text.status.unread - const activeNumber = () => theme.text.status.running + const unreadColor = () => theme.hue.interactive[800] + const activeNumber = () => theme.hue.accent[800] const idleNumber = () => tint(theme.text.formfield.base, background(), 0.55) const separatorUpperPulseColor = createMemo(() => tint(background(), theme.text.base, 0.04)) const separatorLowerPulseColor = createMemo(() => tint(background(), theme.text.base, 0.05)) @@ -1291,8 +1291,8 @@ function HorizontalSessionTabs(props: { onCleanup(clearCloseHold) // A captured drag ends with a synthetic up on its drop target; do not turn that into a click. let suppressClick = false - const unreadColor = () => theme.text.status.unread - const activeNumber = () => theme.text.status.running + const unreadColor = () => theme.hue.interactive[800] + const activeNumber = () => theme.hue.accent[800] const idleNumber = () => tint(theme.text.formfield.base, theme.background.base, 0.55) const newTab = () => tabs.newTab?.() ?? false const activeID = createMemo(() => (newTab() ? NEW_SESSION_TAB.sessionID : tabs.current())) diff --git a/packages/tui/src/feature-plugins/prompt/btw.tsx b/packages/tui/src/feature-plugins/prompt/btw.tsx index c4c7c68e6b6..3eae7461a36 100644 --- a/packages/tui/src/feature-plugins/prompt/btw.tsx +++ b/packages/tui/src/feature-plugins/prompt/btw.tsx @@ -32,7 +32,7 @@ export default Plugin.define({ return ( 0}> - /btw + /btw ) diff --git a/packages/tui/src/feature-plugins/system/storybook/one-cell-spinner.tsx b/packages/tui/src/feature-plugins/system/storybook/one-cell-spinner.tsx index 91d1725366a..fe260c0c029 100644 --- a/packages/tui/src/feature-plugins/system/storybook/one-cell-spinner.tsx +++ b/packages/tui/src/feature-plugins/system/storybook/one-cell-spinner.tsx @@ -181,7 +181,7 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) { animations={animations()} paused={paused()} glow={glow()} - color={theme.text.status.running} + color={theme.hue.accent[800]} /> )} @@ -236,7 +236,7 @@ function OneCellSpinnerStory(props: { context: Plugin.Context }) { animations={animations()} paused={paused()} glow={glow()} - color={theme.text.status.running} + color={theme.hue.accent[800]} /> esc stop diff --git a/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx b/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx index 57225030c7f..8d7613ef3fb 100644 --- a/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx +++ b/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx @@ -284,10 +284,10 @@ function SessionTabsStory(props: { context: Plugin.Context }) { { text: "", color: theme.text.base }, ) if (status.attention === "question") - lines.push({ text: "? Which approach should I take?", color: theme.text.status.question }) + lines.push({ text: "? Which approach should I take?", color: theme.hue.interactive[800] }) else if (status.attention === "permission") - lines.push({ text: "! Waiting for permission to run the command", color: theme.text.status.permission }) - else if (status.busy) lines.push({ text: "● Working…", color: theme.text.status.running }) + lines.push({ text: "! Waiting for permission to run the command", color: theme.hue.interactive[800] }) + else if (status.busy) lines.push({ text: "● Working…", color: theme.hue.accent[800] }) else if (outcome === "failed") lines.push({ text: `✗ bun run test failed — 3 tests failing in ${file}`, diff --git a/packages/tui/src/mini/theme.ts b/packages/tui/src/mini/theme.ts index de9b4069334..75c8623403b 100644 --- a/packages/tui/src/mini/theme.ts +++ b/packages/tui/src/mini/theme.ts @@ -1,6 +1,6 @@ import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core" import { generateSyntax, resolveThemeDocument, themeModes, type ResolvedTheme } from "@opencode/theme/tui" -import { allThemes, DEFAULT_THEMES, isThemeSource, parseTheme, type ThemeDocumentSource } from "../theme" +import { allThemes, getOpenCodeTheme, isThemeSource, parseTheme, type ThemeDocumentSource } from "../theme" import { ansiToRgba } from "../theme/color" import { discoverThemes } from "../theme/discovery" import { generateSystem, terminalMode } from "../theme/system" @@ -142,9 +142,9 @@ function map( formfieldFocusedBg: exact(theme.background.formfield.focused), formfieldFocusedText: exact(theme.text.formfield.focused), selection: exact(theme.text.formfield.selected), - running: exact(theme.text.status.running), - question: exact(theme.text.status.question), - permission: exact(theme.text.status.permission), + running: exact(theme.hue.accent[800]), + question: exact(theme.hue.interactive[800]), + permission: exact(theme.hue.interactive[800]), success: exact(theme.text.feedback.success.base), link: exact(theme.markdown.link), categorical: dedupeWith( @@ -192,11 +192,11 @@ function map( } export const RUN_THEME_FALLBACK = map( - resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), "dark"), + resolveThemeDocument(parseTheme(getOpenCodeTheme()), "dark"), ansiPalette, ) export const RUN_THEME_FALLBACK_LIGHT = map( - resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), "light"), + resolveThemeDocument(parseTheme(getOpenCodeTheme()), "light"), ansiPalette, ) @@ -292,7 +292,7 @@ export async function resolveRunTheme( if (themeModes(document).includes(mode)) return resolveThemeDocument(document, mode) }) .catch(() => undefined) - const theme = resolved ?? resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), mode) + const theme = resolved ?? resolveThemeDocument(parseTheme(getOpenCodeTheme()), mode) const indexed = colors ? ansiPalette.map((color, index) => (colors.palette[index] ? RGBA.fromIndex(index, colors.palette[index]!) : color)) : ansiPalette @@ -312,6 +312,6 @@ async function themeSource( const custom = await discoverThemes( configDirectories(process.env.OPENCODE_CONFIG_DIR ?? Global.Path.config, process.cwd()), ) - const source = custom[name] ?? allThemes()[name] ?? DEFAULT_THEMES.opencode - return isThemeSource(source) ? source : DEFAULT_THEMES.opencode + const source = custom[name] ?? allThemes()[name] ?? getOpenCodeTheme() + return isThemeSource(source) ? source : getOpenCodeTheme() } diff --git a/packages/tui/src/theme/assets/v2/opencode.json b/packages/tui/src/theme/assets/v2/opencode.json new file mode 100644 index 00000000000..3c3b1646ab4 --- /dev/null +++ b/packages/tui/src/theme/assets/v2/opencode.json @@ -0,0 +1,390 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "version": 2, + "base": { + "categorical": [ + "purple", + "orange", + "green", + "blue", + "red" + ], + "text": { + "base": "$hue.neutral.200", + "muted": "$hue.neutral.400", + "action": { + "primary": { + "base": "$text.base", + "$disabled": "$hue.neutral.400", + "$focused": "$hue.neutral.900", + "$selected": "$hue.interactive.200" + }, + "secondary": { + "base": "$text.muted", + "$hovered": "$text.base" + }, + "destructive": { + "base": "$hue.neutral.900", + "$disabled": "$hue.neutral.400" + } + }, + "formfield": { + "base": "$hue.neutral.200", + "$hovered": "$hue.interactive.200", + "$focused": "$hue.interactive.200", + "$pressed": "$hue.interactive.200", + "$disabled": "$hue.neutral.400", + "$selected": "$hue.interactive.200" + }, + "feedback": { + "error": { + "base": "$hue.red.200" + }, + "warning": { + "base": "$hue.accent.200" + }, + "success": { + "base": "$hue.green.200" + }, + "info": { + "base": "$hue.cyan.200" + } + } + }, + "background": { + "base": "$hue.neutral.800", + "raised": { + "base": "$hue.neutral.700", + "high": "$hue.neutral.600", + "max": "$hue.neutral.500" + }, + "action": { + "primary": { + "base": "transparent", + "$hovered": "$hue.neutral.700", + "$focused": "$hue.interactive.200", + "$selected": "transparent" + }, + "secondary": { + "base": "transparent" + }, + "destructive": { + "base": "$hue.red.200" + } + }, + "formfield": { + "base": "$background.base" + }, + "feedback": { + "error": { + "base": "$background.base" + }, + "warning": { + "base": "$background.base" + }, + "success": { + "base": "$background.base" + }, + "info": { + "base": "$background.base" + } + } + }, + "border": { + "base": "#b8b8b8" + }, + "scrollbar": { + "base": "#a0a0a0" + }, + "diff": { + "text": { + "added": "#1e725c", + "removed": "#c53b53", + "context": "#7086b5", + "hunkHeader": "#7086b5" + }, + "background": { + "added": "#d5e5d5", + "removed": "#f7d8db", + "context": "$hue.neutral.700" + }, + "highlight": { + "added": "#4db380", + "removed": "#f52a65" + }, + "lineNumber": { + "text": "#595959", + "background": { + "added": "#c5d5c5", + "removed": "#e7c8cb" + } + } + }, + "syntax": { + "comment": "$hue.neutral.400", + "keyword": "$hue.accent.200", + "function": "$hue.interactive.200", + "variable": "$hue.red.200", + "string": "$hue.green.200", + "number": "$hue.accent.200", + "type": "#b0851f", + "operator": "$hue.cyan.200", + "punctuation": "$hue.neutral.200" + }, + "markdown": { + "text": "$hue.neutral.200", + "heading": "$hue.accent.200", + "link": "$hue.interactive.200", + "linkText": "$hue.cyan.200", + "code": "$hue.green.200", + "blockQuote": "#b0851f", + "emphasis": "#b0851f", + "strong": "$hue.accent.200", + "horizontalRule": "$hue.neutral.400", + "listItem": "$hue.interactive.200", + "listEnumeration": "$hue.cyan.200", + "image": "$hue.interactive.200", + "imageText": "$hue.cyan.200", + "codeBlock": "$hue.neutral.200" + }, + "@dialog": { + "background": { + "base": "$background.raised.base", + "action": { + "primary": { + "$hovered": "$background.raised.high" + } + } + } + } + }, + "light": { + "hue": { + "gray": { + "100": "#000000", + "200": "#1a1a1a", + "300": "#4e4e4e", + "400": "#8a8a8a", + "500": "#bebebe", + "600": "#f5f5f5", + "700": "#fafafa", + "800": "#ffffff", + "900": "#ffffff" + }, + "red": { + "100": "#c20f26", + "200": "#d1383d", + "300": "#e05352", + "400": "#ee6b68", + "500": "#fc837d", + "600": "#fda19b", + "700": "#fcbeb9", + "800": "#fed8d5", + "900": "#fff2f0" + }, + "orange": { + "100": "#c7811e", + "200": "#d68c27", + "300": "#df9a44", + "400": "#e8a85c", + "500": "#f1b671", + "600": "#fac386", + "700": "#fcd4a8", + "800": "#fee3c7", + "900": "#fef3e7" + }, + "yellow": "$hue.gray", + "green": { + "100": "#1f8c44", + "200": "#3d9a57", + "300": "#55a96a", + "400": "#6cb77c", + "500": "#81c68f", + "600": "#96d5a3", + "700": "#ace4b6", + "800": "#c1f3ca", + "900": "#e1fee6" + }, + "cyan": { + "100": "#077786", + "200": "#318795", + "300": "#4c97a4", + "400": "#64a8b4", + "500": "#7bb8c4", + "600": "#92c9d4", + "700": "#a8dbe4", + "800": "#bfecf4", + "900": "#e3fafe" + }, + "blue": { + "100": "#226bcc", + "200": "#3b7dd8", + "300": "#518ee4", + "400": "#67a0f0", + "500": "#7db1fc", + "600": "#9cc3fa", + "700": "#b7d4fd", + "800": "#d4e5fd", + "900": "#f0f6fe" + }, + "purple": { + "100": "#6b47a7", + "200": "#7b5bb6", + "300": "#8c6fc5", + "400": "#9d83d4", + "500": "#af98e3", + "600": "#c1acf2", + "700": "#d3c3fc", + "800": "#e4dbfd", + "900": "#f6f3ff" + }, + "accent": "$hue.orange", + "interactive": "$hue.blue", + "neutral": "$hue.gray" + } + }, + "dark": { + "hue": { + "gray": { + "100": "#ffffff", + "200": "#eeeeee", + "300": "#b5b5b5", + "400": "#808080", + "500": "#4c4c4c", + "600": "#1e1e1e", + "700": "#141414", + "800": "#0a0a0a", + "900": "#030303" + }, + "red": { + "100": "#fd7e87", + "200": "#e06c75", + "300": "#c35a63", + "400": "#a74952", + "500": "#8c3941", + "600": "#722931", + "700": "#591921", + "800": "#410a13", + "900": "#280207" + }, + "orange": { + "100": "#fddac5", + "200": "#fab283", + "300": "#d8976c", + "400": "#b67c56", + "500": "#966341", + "600": "#774a2c", + "700": "#593319", + "800": "#3d1d06", + "900": "#1f0b01" + }, + "yellow": "$hue.gray", + "green": { + "100": "#96f7a7", + "200": "#7fd88f", + "300": "#68b977", + "400": "#539c61", + "500": "#3d7f4b", + "600": "#296336", + "700": "#144922", + "800": "#00300f", + "900": "#011705" + }, + "cyan": { + "100": "#68d0dd", + "200": "#56b6c2", + "300": "#449da7", + "400": "#33848e", + "500": "#226c75", + "600": "#0f555d", + "700": "#093e44", + "800": "#02292e", + "900": "#001518" + }, + "blue": { + "100": "#82b4fb", + "200": "#5c9cf5", + "300": "#4c86d6", + "400": "#3c70b8", + "500": "#2c5b9b", + "600": "#1d477f", + "700": "#0f3364", + "800": "#02214a", + "900": "#01112b" + }, + "purple": { + "100": "#b38ff4", + "200": "#9d7cd8", + "300": "#8869bd", + "400": "#7357a2", + "500": "#5f4688", + "600": "#4b356f", + "700": "#392557", + "800": "#271640", + "900": "#17072b" + }, + "accent": "$hue.purple", + "interactive": "$hue.orange", + "neutral": "$hue.gray" + }, + "categorical": [ + "blue", + "purple", + "green", + "orange", + "red" + ], + "text": { + "action": { + "primary": { + "$focused": "$hue.neutral.800" + }, + "destructive": { + "base": "$hue.neutral.800" + } + }, + "feedback": { + "warning": { + "base": "#f5a742" + } + } + }, + "border": { + "base": "#484848" + }, + "scrollbar": { + "base": "#606060" + }, + "diff": { + "text": { + "added": "#4fd6be", + "context": "#828bb8", + "hunkHeader": "#828bb8" + }, + "background": { + "added": "#20303b", + "removed": "#37222c" + }, + "highlight": { + "added": "#b8db87", + "removed": "#e26a75" + }, + "lineNumber": { + "text": "#8f8f8f", + "background": { + "added": "#1b2b34", + "removed": "#2d1f26" + } + } + }, + "syntax": { + "number": "#f5a742", + "type": "#e5c07b" + }, + "markdown": { + "blockQuote": "#e5c07b", + "emphasis": "#e5c07b", + "strong": "#f5a742" + } + } +} diff --git a/packages/tui/src/theme/index.ts b/packages/tui/src/theme/index.ts index 91318219e34..600357ea02a 100644 --- a/packages/tui/src/theme/index.ts +++ b/packages/tui/src/theme/index.ts @@ -1,7 +1,8 @@ import { Schema } from "effect" -import { migrateV1, resolveThemeDocument, ThemeDocument, themeDecodeError } from "@opencode/theme/tui" +import { migrateV1, resolveThemeDocument, ThemeDocument, themeDecodeError, type ModeDefinition } from "@opencode/theme/tui" import { resolveThemeColors } from "./resolve" import { DEFAULT_THEMES, type Theme, type ThemeV1Json } from "./v1" +import opencode from "./assets/v2/opencode.json" with { type: "json" } export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeV1Json } from "./v1" export { resolveThemeDocument, type ThemeDocument } @@ -14,11 +15,23 @@ let systemTheme: ThemeDocumentSource | undefined const listeners = new Set<(themes: Record) => void>() const parsed = new WeakMap() const decodeThemeDocument = Schema.decodeUnknownSync(ThemeDocument, { reportInput: true }) +let opencodeTheme: (ThemeDocument & { + readonly light: ModeDefinition + readonly dark: ModeDefinition +}) | undefined + +export function getOpenCodeTheme() { + if (opencodeTheme) return opencodeTheme + const decoded = decodeThemeDocument(opencode) as NonNullable + opencodeTheme = decoded + return decoded +} function listThemes(): Record { // Priority: defaults < plugin installs < custom files < generated system. const themes: Record = { ...DEFAULT_THEMES, + opencode: getOpenCodeTheme(), ...pluginThemes, ...customThemes, } diff --git a/packages/tui/src/theme/v1.ts b/packages/tui/src/theme/v1.ts index c1b3426fa80..4c9ca5d36e1 100644 --- a/packages/tui/src/theme/v1.ts +++ b/packages/tui/src/theme/v1.ts @@ -22,7 +22,6 @@ import monokai from "./assets/monokai.json" with { type: "json" } import nightowl from "./assets/nightowl.json" with { type: "json" } import nord from "./assets/nord.json" with { type: "json" } import onedark from "./assets/one-dark.json" with { type: "json" } -import opencode from "./assets/opencode.json" with { type: "json" } import orng from "./assets/orng.json" with { type: "json" } import osakaJade from "./assets/osaka-jade.json" with { type: "json" } import palenight from "./assets/palenight.json" with { type: "json" } @@ -58,7 +57,6 @@ export const DEFAULT_THEMES: Record = { nord, ["one-dark"]: onedark, ["osaka-jade"]: osakaJade, - opencode, orng, ["lucent-orng"]: lucentOrng, palenight, diff --git a/packages/tui/test/cli/tui/theme-mode.test.tsx b/packages/tui/test/cli/tui/theme-mode.test.tsx index d9d781685c2..4951c76054d 100644 --- a/packages/tui/test/cli/tui/theme-mode.test.tsx +++ b/packages/tui/test/cli/tui/theme-mode.test.tsx @@ -2,9 +2,10 @@ import { testRender } from "@opentui/solid" import { expect, test } from "bun:test" import { RGBA } from "@opentui/core" -import { DEFAULT_THEME } from "@opencode/theme/tui" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { DEFAULT_THEMES } from "../../../src/theme" +import { getOpenCodeTheme } from "../../../src/theme" +import opencodeSource from "../../../src/theme/assets/opencode.json" with { type: "json" } +import type { ThemeV1Json } from "@opencode/theme/tui/v1" import { ConfigProvider } from "../../../src/config" import { ThemeContextProvider, ThemeProvider, type ThemeError, useTheme, useThemes } from "../../../src/context/theme" @@ -17,19 +18,20 @@ async function wait(fn: () => boolean) { } test("uses an available mode while retaining the pinned preference", async () => { - const lightOnly = structuredClone(DEFAULT_THEMES.opencode) + const opencodeV1 = opencodeSource as ThemeV1Json + const lightOnly = structuredClone(opencodeV1) lightOnly.theme.background = "#eeeeee" lightOnly.theme.text = "#111111" - const dual = structuredClone(DEFAULT_THEMES.opencode) + const dual = structuredClone(opencodeV1) dual.theme.background = { light: "#eeeeee", dark: "#111111" } dual.theme.text = { light: "#111111", dark: "#eeeeee" } - const darkOnly = structuredClone(DEFAULT_THEMES.opencode) + const darkOnly = structuredClone(opencodeV1) darkOnly.theme.background = "#111111" darkOnly.theme.text = "#eeeeee" const native = { version: 2, - base: { ...DEFAULT_THEME.base, text: { ...DEFAULT_THEME.base.text, base: "#abcdef" } }, - dark: { hue: DEFAULT_THEME.dark.hue }, + base: { ...getOpenCodeTheme().base, text: { ...getOpenCodeTheme().base.text, base: "#abcdef" } }, + dark: { hue: getOpenCodeTheme().dark.hue }, } as const let themes: ReturnType | undefined diff --git a/packages/tui/test/component/session-tabs-status.test.tsx b/packages/tui/test/component/session-tabs-status.test.tsx index 9548237eb00..a29534fe233 100644 --- a/packages/tui/test/component/session-tabs-status.test.tsx +++ b/packages/tui/test/component/session-tabs-status.test.tsx @@ -139,7 +139,7 @@ for (const orientation of ["horizontal", "vertical"] as const) { .captureSpans() .lines.flatMap((line) => line.spans) .find((span) => span.text.trim() === (attention === "question" ? "?" : "!"))?.fg - expect(indicatorColor()?.toInts()).toEqual(theme.text.status[attention].toInts()) + expect(indicatorColor()?.toInts()).toEqual(theme.hue.interactive[800].toInts()) const glow = () => { const colors = app .captureSpans() @@ -156,7 +156,7 @@ for (const orientation of ["horizontal", "vertical"] as const) { expect(full).toBeGreaterThan(0) setActive("first") await app.renderOnce() - expect(indicatorColor()?.toInts()).toEqual(theme.text.status[attention].toInts()) + expect(indicatorColor()?.toInts()).toEqual(theme.hue.interactive[800].toInts()) const dim = glow() expect(dim).toBeGreaterThan(0) expect(dim).toBeLessThan(full) @@ -172,7 +172,7 @@ for (const orientation of ["horizontal", "vertical"] as const) { .lines.flatMap((line) => line.spans) .find((span) => span.text.trim() === glyph)?.fg expect(color?.toInts()).toEqual( - (unread === "error" ? theme.text.feedback.error.base : theme.text.status.unread).toInts(), + (unread === "error" ? theme.text.feedback.error.base : theme.hue.interactive[800]).toInts(), ) await app.mockMouse.click(1, orientation === "vertical" ? 1 : 0) await app.renderOnce() diff --git a/packages/tui/test/mini/footer.test.ts b/packages/tui/test/mini/footer.test.ts index aaedb8bb741..2b819e03fda 100644 --- a/packages/tui/test/mini/footer.test.ts +++ b/packages/tui/test/mini/footer.test.ts @@ -1,7 +1,6 @@ import { expect, test } from "bun:test" import { createTestRenderer } from "@opentui/core/testing" import { CliRenderEvents, RGBA, TextRenderable } from "@opentui/core" -import { DEFAULT_THEME } from "@opencode/theme/tui" import path from "node:path" import { Writable } from "node:stream" import { coalesceProgressCommit, resolveRunAgent, RunFooter } from "../../src/mini/footer" @@ -12,6 +11,7 @@ import type { MiniSettingChange, MiniSettings, RunAgent, RunTuiConfig, StreamCom import { createTuiResolvedConfig } from "../fixture/tui-runtime" import { tmpdir } from "../fixture/fixture" import { createFooterApiFixture } from "./fixture/footer-api" +import { getOpenCodeTheme } from "../../src/theme" function progress(input: Partial = {}): StreamCommit { return { @@ -373,8 +373,8 @@ test("explicit theme refresh reloads custom colors without a palette event", asy path.join(tmp.path, "themes", "mini-refresh.json"), JSON.stringify({ version: 2, - base: { ...DEFAULT_THEME.base, text: { ...DEFAULT_THEME.base.text, base: color } }, - dark: { hue: DEFAULT_THEME.dark.hue }, + base: { ...getOpenCodeTheme().base, text: { ...getOpenCodeTheme().base.text, base: color } }, + dark: { hue: getOpenCodeTheme().dark.hue }, }), ) await app.footer.refreshTheme() diff --git a/packages/tui/test/mini/theme.test.ts b/packages/tui/test/mini/theme.test.ts index 242b800a758..938fc36d482 100644 --- a/packages/tui/test/mini/theme.test.ts +++ b/packages/tui/test/mini/theme.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test" import path from "node:path" import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core" -import { DEFAULT_THEME, resolveThemeDocument, selectTheme, type ResolvedTheme } from "@opencode/theme/tui" +import { resolveThemeDocument, selectTheme, type ResolvedTheme } from "@opencode/theme/tui" import { RUN_THEME_MONO, RUN_THEME_FALLBACK, @@ -9,7 +9,7 @@ import { resolveRunTheme, type RunTheme, } from "../../src/mini/theme" -import { DEFAULT_THEMES, parseTheme } from "../../src/theme" +import { DEFAULT_THEMES, getOpenCodeTheme, parseTheme } from "../../src/theme" import { generateSystem } from "../../src/theme/system" import { tmpdir } from "../fixture/fixture" @@ -90,9 +90,9 @@ function expectFooter(actual: RunTheme, theme: ResolvedTheme) { formfieldFocusedBg: theme.background.formfield.focused, formfieldFocusedText: theme.text.formfield.focused, selection: theme.text.formfield.selected, - running: theme.text.status.running, - question: theme.text.status.question, - permission: theme.text.status.permission, + running: theme.hue.accent[800], + question: theme.hue.interactive[800], + permission: theme.hue.interactive[800], success: theme.text.feedback.success.base, link: theme.markdown.link, shade: theme.background.raised.base, @@ -136,7 +136,10 @@ test.each(["light", "dark"] as const)("uses shared %s defaults and named built-i const colors = terminalColors({}, mode) for (const name of [undefined, "opencode", "tokyonight"] as const) { const theme = await resolveRunTheme(renderer({ colors }), { name, mode }) - const expected = resolveThemeDocument(parseTheme(DEFAULT_THEMES[name ?? "opencode"]), mode) + const expected = resolveThemeDocument( + name === undefined || name === "opencode" ? getOpenCodeTheme() : parseTheme(DEFAULT_THEMES[name]), + mode, + ) try { expectFooter(theme, expected) expect(rgba(theme.background).toInts()).toEqual(RGBA.fromHex(colors.defaultBackground!).toInts()) @@ -187,7 +190,7 @@ test.each(["light", "dark"] as const)("shares the %s system scheme while retaini test.each(["light", "dark"] as const)( "loads complete %s custom theme files", async (mode) => { - const base = selectTheme(DEFAULT_THEME, mode) + const base = selectTheme(getOpenCodeTheme(), mode) const definition = { ...base, text: { @@ -236,7 +239,7 @@ test.each(["light", "dark"] as const)( test.each(["light", "dark"] as const)( "falls back to shared %s defaults for unknown or invalid themes", async (mode) => { - const expected = resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), mode) + const expected = resolveThemeDocument(getOpenCodeTheme(), mode) for (const source of [ { version: 2, [mode]: { categorical: [] } }, { version: 2, [mode]: { text: { base: "$missing" } } }, @@ -263,7 +266,10 @@ test.each(["light", "dark"] as const)("resolves dark-only Aura on an automatic % }) const theme = await resolveRunTheme(renderer({ colors }), { name: "aura" }) try { - expectFooter(theme, resolveThemeDocument(parseTheme(DEFAULT_THEMES[mode === "light" ? "opencode" : "aura"]), mode)) + expectFooter( + theme, + resolveThemeDocument(mode === "light" ? getOpenCodeTheme() : parseTheme(DEFAULT_THEMES.aura), mode), + ) expect(rgba(theme.background).toInts()).toEqual(RGBA.fromHex(colors.defaultBackground!).toInts()) } finally { theme.block.syntax?.destroy() @@ -273,7 +279,7 @@ test.each(["light", "dark"] as const)("resolves dark-only Aura on an automatic % test.each(["light", "dark"] as const)( "falls back only for unsupported modes of a %s-only custom theme", async (mode) => { - const base = selectTheme(DEFAULT_THEME, mode) + const base = selectTheme(getOpenCodeTheme(), mode) const definition = { ...base, text: { ...base.text, base: "#123456" } } const { hue, ...tokens } = definition const source = { version: 2, base: tokens, [mode]: { hue } } @@ -286,7 +292,7 @@ test.each(["light", "dark"] as const)( try { expectFooter( theme, - resolveThemeDocument(parseTheme(requested === mode ? source : DEFAULT_THEMES.opencode), requested), + resolveThemeDocument(requested === mode ? parseTheme(source) : getOpenCodeTheme(), requested), ) } finally { theme.block.syntax?.destroy() @@ -296,7 +302,7 @@ test.each(["light", "dark"] as const)( ) test.each(["light", "dark"] as const)("handles unavailable palettes in %s mode", async (mode) => { - const expected = resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), mode) + const expected = resolveThemeDocument(getOpenCodeTheme(), mode) for (const input of [ { fail: true }, { colors: emptyColors }, @@ -332,7 +338,7 @@ test("uses refreshed background brightness rather than stale mode or ANSI slot z }) test.each(["light", "dark"] as const)("follows physical %s mode over opposite configuration", async (mode) => { - const expected = resolveThemeDocument(parseTheme(DEFAULT_THEMES.opencode), mode) + const expected = resolveThemeDocument(getOpenCodeTheme(), mode) for (const fail of [false, true]) { const theme = await resolveRunTheme(renderer({ colors: terminalColors({}, mode), themeMode: mode, fail }), { mode: mode === "light" ? "dark" : "light", @@ -350,7 +356,7 @@ test.each(["system", "opencode"])( async (name) => { const colors = terminalColors() const expected = resolveThemeDocument( - parseTheme(name === "system" ? generateSystem(colors, "light") : DEFAULT_THEMES.opencode), + name === "system" ? parseTheme(generateSystem(colors, "light")) : getOpenCodeTheme(), "light", ) const input = { colors, fail: false, themeMode: "dark" as "dark" | "light" } diff --git a/packages/tui/test/theme.test.ts b/packages/tui/test/theme.test.ts index 470a4b17090..4c0dcdd8a30 100644 --- a/packages/tui/test/theme.test.ts +++ b/packages/tui/test/theme.test.ts @@ -2,21 +2,29 @@ import { expect, test } from "bun:test" import { mkdir, writeFile } from "node:fs/promises" import path from "node:path" import type { TerminalColors } from "@opentui/core" -import { DEFAULT_THEME } from "@opencode/theme/tui" -import { DEFAULT_THEMES, hasTheme, parseTheme, resolveTheme } from "../src/theme" +import { allThemes, hasTheme, getOpenCodeTheme, parseTheme, resolveTheme } from "../src/theme" import { discoverThemes } from "../src/theme/discovery" import { configDirectories } from "../src/util/config-directories" import { terminalMode } from "../src/theme/system" +import opencodeSource from "../src/theme/assets/opencode.json" with { type: "json" } +import type { ThemeV1Json } from "@opencode/theme/tui/v1" import { tmpdir } from "./fixture/fixture" +const opencodeV1 = opencodeSource as ThemeV1Json + test("parseTheme delegates malformed V1 sources and rejects unknown versions", () => { expect(() => parseTheme({})).toThrow() expect(() => parseTheme({ version: 3 })).toThrow("Unsupported theme version: 3") }) +test("registers opencode as a native V2 theme", () => { + expect(allThemes().opencode).toBe(getOpenCodeTheme()) + expect(parseTheme(getOpenCodeTheme()).version).toBe(2) +}) + test("parses unversioned and explicit V1 themes lazily once", () => { - const unversioned = structuredClone(DEFAULT_THEMES.opencode) - const explicit = { ...structuredClone(DEFAULT_THEMES.opencode), version: 1 } + const unversioned = structuredClone(opencodeV1) + const explicit = { ...structuredClone(opencodeV1), version: 1 } const first = parseTheme(unversioned, "unversioned") const second = parseTheme(explicit, "explicit") @@ -29,8 +37,8 @@ test("parses unversioned and explicit V1 themes lazily once", () => { test("decodes native V2 themes lazily once", () => { const source = { version: 2, - base: DEFAULT_THEME.base, - light: { ...DEFAULT_THEME.light, categorical: ["red"] }, + base: getOpenCodeTheme().base, + light: { ...getOpenCodeTheme().light, categorical: ["red"] }, } as const const document = parseTheme(source) @@ -45,7 +53,7 @@ test("rejects invalid V2 themes when parsing", () => { }) test("rejects invalid V1 themes when parsing", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.defs = { ...source.defs, one: "two", two: "one" } source.theme.primary = "one" @@ -53,8 +61,8 @@ test("rejects invalid V1 themes when parsing", () => { }) test("replacement sources receive independent parse caches", () => { - const first = structuredClone(DEFAULT_THEMES.opencode) - const second = structuredClone(DEFAULT_THEMES.opencode) + const first = structuredClone(opencodeV1) + const second = structuredClone(opencodeV1) second.theme.primary = "#123456" const previous = parseTheme(first) @@ -69,14 +77,14 @@ test("hasTheme checks theme presence", () => { }) test("resolveTheme rejects circular color refs", () => { - const item = structuredClone(DEFAULT_THEMES.opencode) + const item = structuredClone(opencodeV1) item.defs = { ...item.defs, one: "two", two: "one" } item.theme.primary = "one" expect(() => resolveTheme(item, "dark")).toThrow("Circular color reference") }) test("resolveTheme preserves full theme numeric color and marker semantics", () => { - const item = structuredClone(DEFAULT_THEMES.opencode) + const item = structuredClone(opencodeV1) item.theme.primary = 6 delete item.theme.selectedListItemText diff --git a/packages/tui/test/theme/v2/component.test.ts b/packages/tui/test/theme/v2/component.test.ts index 9894077229c..b77a0bcaaa1 100644 --- a/packages/tui/test/theme/v2/component.test.ts +++ b/packages/tui/test/theme/v2/component.test.ts @@ -1,11 +1,12 @@ import { expect, test } from "bun:test" import { createSignal } from "solid-js" import { RGBA } from "@opentui/core" -import { DEFAULT_THEME, resolveTheme, selectTheme } from "@opencode/theme/tui" +import { resolveTheme, selectTheme } from "@opencode/theme/tui" import { createComponentTheme } from "../../../src/theme/component" +import { getOpenCodeTheme } from "../../../src/theme" test("provides reactive properties, states, surfaces, and color operations", () => { - const [resolved, setResolved] = createSignal(resolveTheme(selectTheme(DEFAULT_THEME, "light"))) + const [resolved, setResolved] = createSignal(resolveTheme(selectTheme(getOpenCodeTheme(), "light"))) const theme = createComponentTheme(resolved) const current = theme.surface("dialog") @@ -78,7 +79,7 @@ test("provides reactive properties, states, surfaces, and color operations", () expect(current.background.action.primary.hovered).toBe(resolved().background.raised.high) expect(current.background.formfield.selected).toBe(resolved().surface("dialog").background.formfield.selected) - setResolved(resolveTheme(selectTheme(DEFAULT_THEME, "dark"))) + setResolved(resolveTheme(selectTheme(getOpenCodeTheme(), "dark"))) expect(current.text.base).toBe(resolved().surface("dialog").text.base) expect(current.background.base).toBe(resolved().background.raised.base) expect(current.decrease(current.background.raised.base, 1)).toBe(resolved().hue.neutral[600]) diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index e53a8aba3f0..d4a18066487 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -2,7 +2,6 @@ import { expect, test } from "bun:test" import { RGBA } from "@opentui/core" import { BaseHue, - DEFAULT_THEME, generateSyntax, resolveTheme, resolveThemeDocument, @@ -10,10 +9,20 @@ import { type Mode, type ThemeDefinition, } from "@opencode/theme/tui" -import { parseTheme, type ThemeDocumentSource } from "../../../src/theme" +import { getOpenCodeTheme, parseTheme, type ThemeDocumentSource } from "../../../src/theme" -const light = selectTheme(DEFAULT_THEME, "light") -const dark = selectTheme(DEFAULT_THEME, "dark") +const opencodeLight = selectTheme(getOpenCodeTheme(), "light") +const opencodeDark = selectTheme(getOpenCodeTheme(), "dark") +const light = { + ...opencodeLight, + categorical: ["blue", "purple"], + hue: { ...opencodeLight.hue, accent: "$hue.blue", interactive: "$hue.blue", neutral: "$hue.gray" }, +} satisfies ThemeDefinition +const dark = { + ...opencodeDark, + categorical: ["blue", "purple"], + hue: { ...opencodeDark.hue, accent: "$hue.blue", interactive: "$hue.blue", neutral: "$hue.gray" }, +} satisfies ThemeDefinition test("orders light hues dark-to-light and dark hues light-to-dark", () => { const lightTheme = resolveTheme(light) @@ -102,16 +111,16 @@ test("resolves independent definitions and hue aliases", () => { expect(lightTheme.background.raised.base).toBe(lightTheme.hue.neutral[700]) expect(lightTheme.background.raised.high).toBe(lightTheme.hue.neutral[600]) expect(lightTheme.syntax.keyword).toBeInstanceOf(RGBA) - expect(lightTheme.text.action.primary.base).toBe(lightTheme.hue.neutral[800]) + expect(lightTheme.text.action.primary.base).toBe(lightTheme.text.base) // Surfaces re-resolve the palette after applying their theme-provided overrides. const dialog = lightTheme.surface("dialog") expect(dialog.background.base).toBe(lightTheme.background.raised.base) expect(dialog.background.formfield.base).toBe(lightTheme.background.raised.base) expect(dialog.background.feedback.error.base).toBe(lightTheme.background.raised.base) expect(dialog.background.action.primary.hovered).toBe(lightTheme.background.raised.high) - expect(dialog.background.action.primary.base).toBe(lightTheme.hue.interactive[500]) - expect(dialog.background.action.primary.focused).toBe(lightTheme.hue.interactive[500]) - expect(dialog.text.action.primary.base).toBe(lightTheme.hue.neutral[900]) + expect(dialog.background.action.primary.base.equals(lightTheme.background.action.primary.base)).toBeTrue() + expect(dialog.background.action.primary.focused.equals(lightTheme.background.action.primary.focused)).toBeTrue() + expect(dialog.text.action.primary.base.equals(lightTheme.text.action.primary.base)).toBeTrue() expect(dialog.surface("dialog")).toBe(dialog) expect(darkTheme.surface("dialog").background.base).toBe(darkTheme.background.raised.base) }) diff --git a/packages/tui/test/theme/v2/select.test.ts b/packages/tui/test/theme/v2/select.test.ts index 1d6fa1bd114..2a4bc59cab3 100644 --- a/packages/tui/test/theme/v2/select.test.ts +++ b/packages/tui/test/theme/v2/select.test.ts @@ -1,30 +1,25 @@ import { expect, test } from "bun:test" -import { DEFAULT_THEME, selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "@opencode/theme/tui" +import { selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "@opencode/theme/tui" +import { getOpenCodeTheme } from "../../../src/theme" test("selects complete light and dark themes independently", () => { - expect(selectTheme(DEFAULT_THEME)).toEqual({ ...DEFAULT_THEME.base, ...DEFAULT_THEME.light }) - expect(selectTheme(DEFAULT_THEME, "light")).toEqual({ ...DEFAULT_THEME.base, ...DEFAULT_THEME.light }) - expect(selectTheme(DEFAULT_THEME, "dark")).toEqual({ ...DEFAULT_THEME.base, ...DEFAULT_THEME.dark }) - expect(selectThemeMode(DEFAULT_THEME, "dark")).toEqual({ - theme: { ...DEFAULT_THEME.base, ...DEFAULT_THEME.dark }, - mode: "dark", - }) + const light = selectTheme(getOpenCodeTheme(), "light") + const dark = selectTheme(getOpenCodeTheme(), "dark") + expect(selectTheme(getOpenCodeTheme())).toEqual(light) + expect(light.hue).toEqual(getOpenCodeTheme().light.hue) + expect(light.text).toEqual(getOpenCodeTheme().base.text) + expect(dark.hue).toEqual(getOpenCodeTheme().dark.hue) + expect(selectThemeMode(getOpenCodeTheme(), "dark")).toEqual({ theme: dark, mode: "dark" }) }) test("selects the available mode when the requested mode is missing", () => { - const lightOnly = { version: 2, base: DEFAULT_THEME.base, light: DEFAULT_THEME.light } as const - const darkOnly = { version: 2, base: DEFAULT_THEME.base, dark: DEFAULT_THEME.dark } as const + const lightOnly = { version: 2, base: getOpenCodeTheme().base, light: getOpenCodeTheme().light } as const + const darkOnly = { version: 2, base: getOpenCodeTheme().base, dark: getOpenCodeTheme().dark } as const expect(themeModes(lightOnly)).toEqual(["light"]) expect(themeModes(darkOnly)).toEqual(["dark"]) expect(supportsThemeMode(lightOnly, "light")).toBeTrue() expect(supportsThemeMode(lightOnly, "dark")).toBeFalse() - expect(selectThemeMode(lightOnly, "dark")).toEqual({ - theme: { ...DEFAULT_THEME.base, ...DEFAULT_THEME.light }, - mode: "light", - }) - expect(selectThemeMode(darkOnly, "light")).toEqual({ - theme: { ...DEFAULT_THEME.base, ...DEFAULT_THEME.dark }, - mode: "dark", - }) + expect(selectThemeMode(lightOnly, "dark")).toEqual({ theme: selectTheme(getOpenCodeTheme(), "light"), mode: "light" }) + expect(selectThemeMode(darkOnly, "light")).toEqual({ theme: selectTheme(getOpenCodeTheme(), "dark"), mode: "dark" }) }) diff --git a/packages/tui/test/theme/v2/types.typecheck.ts b/packages/tui/test/theme/v2/types.typecheck.ts index 0a4f0fc5aba..2d82eaa964b 100644 --- a/packages/tui/test/theme/v2/types.typecheck.ts +++ b/packages/tui/test/theme/v2/types.typecheck.ts @@ -1,5 +1,6 @@ -import { DEFAULT_THEME, selectTheme } from "@opencode/theme/tui" +import { selectTheme } from "@opencode/theme/tui" import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeDocument } from "@opencode/theme/tui" +import { getOpenCodeTheme } from "../../../src/theme" const text = { base: "$hue.neutral.900", @@ -39,18 +40,18 @@ const background = { } satisfies BackgroundDefinition const definition = { - ...selectTheme(DEFAULT_THEME, "light"), + ...selectTheme(getOpenCodeTheme(), "light"), "@dialog": { background: { base: "$background.raised.base" } }, } satisfies ThemeDefinition export const document = { version: 2, - base: DEFAULT_THEME.base, + base: getOpenCodeTheme().base, light: { hue: definition.hue }, dark: definition, } satisfies ThemeDocument -export const lightOnly = { version: 2, base: DEFAULT_THEME.base, light: { hue: definition.hue } } satisfies ThemeDocument -export const darkOnly = { version: 2, base: DEFAULT_THEME.base, dark: definition } satisfies ThemeDocument +export const lightOnly = { version: 2, base: getOpenCodeTheme().base, light: { hue: definition.hue } } satisfies ThemeDocument +export const darkOnly = { version: 2, base: getOpenCodeTheme().base, dark: definition } satisfies ThemeDocument // @ts-expect-error A theme document must provide at least one mode. export const empty = { version: 2 } satisfies ThemeDocument // @ts-expect-error A base mode must be complete; partial tokens are only valid under @dialog. diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index 2aa21890f0b..4fef13e9b68 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -1,18 +1,23 @@ import { expect, test } from "bun:test" import { DEFAULT_CATEGORICAL, - DEFAULT_THEME, migrateV1, resolveThemeDocument, selectThemeMode, themeModes, } from "@opencode/theme/tui" -import { DEFAULT_THEMES, resolveTheme as resolveV1 } from "../../../src/theme" +import { DEFAULT_THEMES, getOpenCodeTheme, resolveTheme as resolveV1 } from "../../../src/theme" +import opencodeSource from "../../../src/theme/assets/opencode.json" with { type: "json" } +import type { ThemeV1Json } from "@opencode/theme/tui/v1" + +const opencodeV1 = opencodeSource as ThemeV1Json +const opencodeLight = resolveThemeDocument(getOpenCodeTheme(), "light") +const opencodeDark = resolveThemeDocument(getOpenCodeTheme(), "dark") test("migrates resolved V1 modes into V2 tokens", () => { - const migrated = migrateV1(DEFAULT_THEMES.opencode) + const migrated = migrateV1(opencodeV1) if (!migrated.light || !migrated.dark) throw new Error("Expected both modes") - const legacy = resolveV1(DEFAULT_THEMES.opencode, "light") + const legacy = resolveV1(opencodeV1, "light") const resolved = resolveThemeDocument(migrated, "light") expect(migrated.base.categorical?.length).toBeGreaterThan(0) @@ -52,7 +57,7 @@ test("migrates resolved V1 modes into V2 tokens", () => { }) test("references generated hues from matching token colors", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.theme.border = source.theme.primary source.theme.borderActive = source.theme.accent source.theme.syntaxKeyword = source.theme.error @@ -68,7 +73,7 @@ test("references generated hues from matching token colors", () => { }) test("infers chromatic hues, anchors light and dark colors, and aliases ambiguous hues to gray", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) const ambiguous = { light: "#808080", dark: "#808080" } source.theme.accent = ambiguous source.theme.warning = ambiguous @@ -101,10 +106,28 @@ test("infers chromatic hues, anchors light and dark colors, and aliases ambiguou }) test("orders categorical hues by V1 semantic color mapping", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) + const colors = { + light: { + red: "#fca5a5", + orange: "#fdba74", + yellow: "#fde047", + green: "#86efac", + blue: "#93c5fd", + purple: "#d8b4fe", + }, + dark: { + red: "#b91c1c", + orange: "#c2410c", + yellow: "#a16207", + green: "#15803d", + blue: "#1d4ed8", + purple: "#7e22ce", + }, + } as const const mapped = (name: "red" | "orange" | "yellow" | "green" | "blue" | "purple") => ({ - light: DEFAULT_THEME.light.hue[name][300], - dark: DEFAULT_THEME.dark.hue[name][300], + light: colors.light[name], + dark: colors.dark[name], }) source.theme.secondary = mapped("purple") source.theme.accent = mapped("orange") @@ -122,11 +145,11 @@ test("orders categorical hues by V1 semantic color mapping", () => { }) test("gives accent and primary ownership of their inferred hues", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) - source.theme.success = DEFAULT_THEME.light.hue.orange[700] - source.theme.accent = DEFAULT_THEME.light.hue.orange[600] - source.theme.info = DEFAULT_THEME.light.hue.blue[700] - source.theme.primary = DEFAULT_THEME.light.hue.blue[600] + const source = structuredClone(opencodeV1) + source.theme.success = hex(opencodeLight.hue.orange[700]) + source.theme.accent = hex(opencodeLight.hue.orange[600]) + source.theme.info = hex(opencodeLight.hue.blue[700]) + source.theme.primary = hex(opencodeLight.hue.blue[600]) const migrated = migrateV1(source) if (!migrated.light) throw new Error("Expected light mode") @@ -139,7 +162,7 @@ test("gives accent and primary ownership of their inferred hues", () => { expect(migrated.light.hue?.accent).toBe("$hue.orange") expect(migrated.light.hue?.interactive).toBe("$hue.blue") - source.theme.primary = DEFAULT_THEME.light.hue.orange[500] + source.theme.primary = hex(opencodeLight.hue.orange[500]) const collisionMode = migrateV1(source).light const collision = collisionMode?.hue?.orange if (typeof collision !== "object") throw new Error("Expected concrete orange scale") @@ -149,7 +172,7 @@ test("gives accent and primary ownership of their inferred hues", () => { }) test("uses default categorical hues when V1 semantic colors are ambiguous", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.theme.secondary = "transparent" source.theme.accent = "transparent" source.theme.success = "transparent" @@ -163,7 +186,7 @@ test("uses default categorical hues when V1 semantic colors are ambiguous", () = }) test("builds and extrapolates gray from V1 surfaces and text without using menus or borders", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.theme.background = { light: "#eeeeee", dark: "#111111" } source.theme.backgroundPanel = { light: "#dddddd", dark: "#222222" } source.theme.backgroundElement = { light: "#cccccc", dark: "#333333" } @@ -202,7 +225,7 @@ test("builds and extrapolates gray from V1 surfaces and text without using menus }) test("uses the base text reference for primary actions on transparent backgrounds", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.theme.background = "transparent" source.theme.primary = { light: "#ffffff", dark: "#000000" } delete source.theme.selectedListItemText @@ -214,7 +237,7 @@ test("uses the base text reference for primary actions on transparent background }) test("retains V1 circular reference errors", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.defs = { ...source.defs, one: "two", two: "one" } source.theme.primary = "one" @@ -231,7 +254,7 @@ test("migrates every built-in V1 theme in its supported modes", () => { }) test("collapses identical V1 backgrounds when both variants infer one mode", () => { - const dark = structuredClone(DEFAULT_THEMES.opencode) + const dark = structuredClone(opencodeV1) dark.theme.background = "#111111" dark.theme.text = "#eeeeee" const migratedDark = migrateV1(dark) @@ -240,7 +263,7 @@ test("collapses identical V1 backgrounds when both variants infer one mode", () expect(themeModes(migratedDark)).toEqual(["dark"]) expect(selectThemeMode(migratedDark, "light").mode).toBe("dark") - const light = structuredClone(DEFAULT_THEMES.opencode) + const light = structuredClone(opencodeV1) light.theme.background = "#eeeeee" light.theme.text = "#111111" const migratedLight = migrateV1(light) @@ -251,7 +274,7 @@ test("collapses identical V1 backgrounds when both variants infer one mode", () }) test("keeps both modes when a shared background has different contrast", () => { - const source = structuredClone(DEFAULT_THEMES.opencode) + const source = structuredClone(opencodeV1) source.theme.background = "#808080" source.theme.text = { light: "#111111", dark: "#eeeeee" } const migrated = migrateV1(source) diff --git a/services/www/src/docs/content/themes.mdx b/services/www/src/docs/content/themes.mdx index ebaf39ad7af..999f92c52a4 100644 --- a/services/www/src/docs/content/themes.mdx +++ b/services/www/src/docs/content/themes.mdx @@ -227,7 +227,7 @@ palette: | Group | Purpose | | ------------ | ------------------------------------------------------------- | -| `text` | Base, muted, action, form-field, status, and feedback text | +| `text` | Base, muted, action, form-field, and feedback text | | `background` | Base, surfaces, actions, form fields, and feedback fills | | `border` | Borders | | `scrollbar` | Scrollbars |