@@ -109,6 +126,45 @@ export function AppearanceSection() {
/>
))}
+ {recommendedPalettePair && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('settings.appearance.recommendedPalette', {
+ style: t(activeStyleDefinition.labelKey),
+ })}
+
+
+ {t('settings.appearance.recommendedPaletteDescription', {
+ palette: t(paletteDefinition(recommendedPalettePair.day).labelKey),
+ })}
+
+
+
+
+ )}
diff --git a/ui/src/theme/noFlashTheme.spec.ts b/ui/src/theme/noFlashTheme.spec.ts
index 851d236fd..5a513b599 100644
--- a/ui/src/theme/noFlashTheme.spec.ts
+++ b/ui/src/theme/noFlashTheme.spec.ts
@@ -64,4 +64,20 @@ describe('no-flash theme bootstrap', () => {
expect(applyNoFlashTheme({ uiStyle: 'win98' }, false).uiStyle).toBe('win98')
expect(applyNoFlashTheme({ uiStyle: 'aqua' }, false).uiStyle).toBe('default')
})
+
+ it('restores the Windows Classic palette before first paint', () => {
+ const state = {
+ theme: 'auto',
+ dayPalette: 'windows-classic',
+ nightPalette: 'windows-classic',
+ uiStyle: 'win98',
+ }
+ expect(applyNoFlashTheme(state, false)).toEqual({
+ theme: 'auto',
+ dayPalette: 'windows-classic',
+ nightPalette: 'windows-classic',
+ uiStyle: 'win98',
+ palette: 'windows-classic',
+ })
+ })
})
diff --git a/ui/src/theme/palette.css b/ui/src/theme/palette.css
index a2949e624..de9e298e4 100644
--- a/ui/src/theme/palette.css
+++ b/ui/src/theme/palette.css
@@ -284,6 +284,97 @@
);
}
+/* Windows 98's application chrome: system silver, navy selection, white
+ document fields, and a black console. The paired Win98 style profile owns
+ bevels and square geometry; this card remains independently selectable. */
+:root[data-palette="windows-classic"],
+[data-palette-preview="windows-classic"] {
+ color-scheme: light;
+
+ --background: #c0c0c0;
+ --foreground: #000000;
+ --card: #c0c0c0;
+ --card-foreground: #000000;
+ --popover: #c0c0c0;
+ --popover-foreground: #000000;
+ --primary: #000080;
+ --primary-foreground: #ffffff;
+ --secondary: #d4d0c8;
+ --secondary-foreground: #000000;
+ --muted: #a0a0a0;
+ --muted-foreground: #3f3f3f;
+ --accent: rgba(0, 0, 128, 0.12);
+ --accent-foreground: #000000;
+ --destructive: #800000;
+ --destructive-foreground: #ffffff;
+ --border: #808080;
+ --input: #808080;
+ --ring: #000080;
+
+ --accent-strong: rgba(0, 0, 128, 0.2);
+ --primary-muted: rgba(0, 0, 128, 0.18);
+ --success: #006400;
+ --success-foreground: #ffffff;
+ --warning: #7f4f00;
+ --warning-foreground: #ffffff;
+ --warning-background: #ffffe1;
+ --warning-border: #808000;
+ --info: #000080;
+ --info-foreground: #ffffff;
+ --ai-action: #800080;
+ --ai-action-foreground: #ffffff;
+ --backdrop: rgba(0, 0, 0, 0.48);
+ --code-background: rgba(255, 255, 255, 0.42);
+ --report-canvas: #ffffff;
+ --shadow-color: #000000;
+
+ --terminal-background: #000000;
+ --terminal-foreground: #c0c0c0;
+ --terminal-cursor: #ffffff;
+ --terminal-cursor-accent: #000000;
+ --terminal-selection-background: #000080;
+ --terminal-selection-foreground: #ffffff;
+ --terminal-black: #000000;
+ --terminal-red: #ff5f5f;
+ --terminal-green: #00aa00;
+ --terminal-yellow: #ffff00;
+ --terminal-blue: #6f6fff;
+ --terminal-magenta: #ff5fff;
+ --terminal-cyan: #00aaaa;
+ --terminal-white: #c0c0c0;
+ --terminal-bright-black: #808080;
+ --terminal-bright-red: #ff0000;
+ --terminal-bright-green: #00ff00;
+ --terminal-bright-yellow: #ffff00;
+ --terminal-bright-blue: #8080ff;
+ --terminal-bright-magenta: #ff00ff;
+ --terminal-bright-cyan: #00ffff;
+ --terminal-bright-white: #ffffff;
+
+ --chart-1: #000080;
+ --chart-2: #006400;
+ --chart-3: #7f4f00;
+ --chart-4: #800080;
+ --chart-5: #008080;
+ --chart-positive: #006400;
+ --chart-negative: #800000;
+ --chart-positive-muted: rgba(0, 100, 0, 0.32);
+ --chart-negative-muted: rgba(128, 0, 0, 0.32);
+ --chart-axis: #3f3f3f;
+ --chart-grid: #808080;
+
+ --sidebar: #c0c0c0;
+ --sidebar-foreground: #000000;
+ --sidebar-primary: #000080;
+ --sidebar-primary-foreground: #ffffff;
+ --sidebar-accent: #000080;
+ --sidebar-accent-foreground: #ffffff;
+ --sidebar-border: #808080;
+ --sidebar-ring: #000080;
+
+ --app-background-wash: none;
+}
+
:root[data-palette="graphite"],
[data-palette-preview="graphite"] {
color-scheme: dark;
diff --git a/ui/src/theme/palettes.ts b/ui/src/theme/palettes.ts
index 7ae884fd5..96f862ccb 100644
--- a/ui/src/theme/palettes.ts
+++ b/ui/src/theme/palettes.ts
@@ -2,6 +2,7 @@ export type ThemePaletteId =
| 'paper'
| 'porcelain'
| 'linen'
+ | 'windows-classic'
| 'graphite'
| 'midnight'
| 'moss'
@@ -29,6 +30,7 @@ export const THEME_PALETTES = [
{ id: 'paper', appearance: 'light', labelKey: 'theme.palette.paper', descriptionKey: 'theme.paletteDescription.paper' },
{ id: 'porcelain', appearance: 'light', labelKey: 'theme.palette.porcelain', descriptionKey: 'theme.paletteDescription.porcelain' },
{ id: 'linen', appearance: 'light', labelKey: 'theme.palette.linen', descriptionKey: 'theme.paletteDescription.linen' },
+ { id: 'windows-classic', appearance: 'light', labelKey: 'theme.palette.windows-classic', descriptionKey: 'theme.paletteDescription.windows-classic' },
{ id: 'graphite', appearance: 'dark', labelKey: 'theme.palette.graphite', descriptionKey: 'theme.paletteDescription.graphite' },
{ id: 'midnight', appearance: 'dark', labelKey: 'theme.palette.midnight', descriptionKey: 'theme.paletteDescription.midnight' },
{ id: 'moss', appearance: 'dark', labelKey: 'theme.palette.moss', descriptionKey: 'theme.paletteDescription.moss' },
diff --git a/ui/src/theme/semanticColors.spec.ts b/ui/src/theme/semanticColors.spec.ts
index c8a8b6ea9..2c9ee0873 100644
--- a/ui/src/theme/semanticColors.spec.ts
+++ b/ui/src/theme/semanticColors.spec.ts
@@ -39,7 +39,7 @@ const CORE_TOKENS = [
] as const
const PALETTE_IDS: readonly ThemePaletteId[] = [
- 'paper', 'porcelain', 'linen', 'graphite', 'midnight', 'moss', 'iris',
+ 'paper', 'porcelain', 'linen', 'windows-classic', 'graphite', 'midnight', 'moss', 'iris',
]
const ALLOWED_LITERAL_COLOR_FILES = new Set([
@@ -98,12 +98,12 @@ function hexContrast(a: string, b: string): number {
}
describe('semantic color contract', () => {
- it('ships one universal library with seven complete semantic cards', () => {
+ it('ships one universal library with eight complete semantic cards', () => {
expect(THEME_PALETTES.map(({ id }) => id)).toEqual(PALETTE_IDS)
expect(THEME_PALETTES.map(({ appearance }) => appearance)).toEqual([
- 'light', 'light', 'light', 'dark', 'dark', 'dark', 'dark',
+ 'light', 'light', 'light', 'light', 'dark', 'dark', 'dark', 'dark',
])
- expect(new Set(PALETTE_IDS).size).toBe(7)
+ expect(new Set(PALETTE_IDS).size).toBe(8)
for (const id of PALETTE_IDS) {
const block = paletteBlock(id)
@@ -114,7 +114,7 @@ describe('semantic color contract', () => {
}
})
- it('keeps every semantic token symmetric across all seven cards', () => {
+ it('keeps every semantic token symmetric across all eight cards', () => {
const tokens = [...paletteBlock('paper').matchAll(/^\s*(--[\w-]+):/gm)].map((match) => match[1])
expect(tokens.length).toBeGreaterThan(CORE_TOKENS.length)
@@ -149,7 +149,7 @@ describe('semantic color contract', () => {
it('keeps the new product and ANSI text roles at normal-text contrast', () => {
const ansiTokens = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] as const
- for (const id of ['linen', 'moss', 'iris'] as const) {
+ for (const id of ['linen', 'windows-classic', 'moss', 'iris'] as const) {
expect(hexContrast(paletteToken(id, 'foreground'), paletteToken(id, 'background')), id).toBeGreaterThanOrEqual(4.5)
expect(hexContrast(paletteToken(id, 'muted-foreground'), paletteToken(id, 'background')), id).toBeGreaterThanOrEqual(4.5)
expect(hexContrast(paletteToken(id, 'primary-foreground'), paletteToken(id, 'primary')), id).toBeGreaterThanOrEqual(4.5)
@@ -177,6 +177,7 @@ describe('semantic color contract', () => {
expect(paletteAppearance('midnight')).toBe('dark')
expect(paletteAppearance('paper')).toBe('light')
expect(paletteAppearance('linen')).toBe('light')
+ expect(paletteAppearance('windows-classic')).toBe('light')
expect(paletteAppearance('moss')).toBe('dark')
expect(paletteAppearance('iris')).toBe('dark')
expect(palette).not.toMatch(/data-theme|prefers-color-scheme/)
diff --git a/ui/src/theme/store.spec.ts b/ui/src/theme/store.spec.ts
index aeaed8440..077e0260e 100644
--- a/ui/src/theme/store.spec.ts
+++ b/ui/src/theme/store.spec.ts
@@ -60,4 +60,18 @@ describe('theme preference persistence', () => {
expect(normalizeThemePreferences({ uiStyle: 'aqua' }).uiStyle).toBe('default')
})
+
+ it('accepts Windows Classic in either palette slot', () => {
+ expect(normalizeThemePreferences({
+ theme: 'auto',
+ dayPalette: 'windows-classic',
+ nightPalette: 'windows-classic',
+ uiStyle: 'win98',
+ })).toEqual({
+ theme: 'auto',
+ dayPalette: 'windows-classic',
+ nightPalette: 'windows-classic',
+ uiStyle: 'win98',
+ })
+ })
})
diff --git a/ui/src/theme/styleProfiles.ts b/ui/src/theme/styleProfiles.ts
index 82a993526..35b41a53c 100644
--- a/ui/src/theme/styleProfiles.ts
+++ b/ui/src/theme/styleProfiles.ts
@@ -1,9 +1,15 @@
+import type { ThemePaletteId } from './palettes'
+
export type UiStyleProfileId = 'default' | 'win98' | 'broker-classic'
export interface UiStyleProfileDefinition {
readonly id: UiStyleProfileId
readonly labelKey: `theme.uiStyle.${UiStyleProfileId}`
readonly descriptionKey: `theme.uiStyleDescription.${UiStyleProfileId}`
+ readonly recommendedPalettePair?: Readonly<{
+ day: ThemePaletteId
+ night: ThemePaletteId
+ }>
}
export const DEFAULT_UI_STYLE_PROFILE: UiStyleProfileId = 'default'
@@ -18,6 +24,10 @@ export const UI_STYLE_PROFILES = [
id: 'win98',
labelKey: 'theme.uiStyle.win98',
descriptionKey: 'theme.uiStyleDescription.win98',
+ recommendedPalettePair: {
+ day: 'windows-classic',
+ night: 'windows-classic',
+ },
},
{
id: 'broker-classic',