From f43563b2f4e018270f7b9dc1452daf6d71f18879 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 28 Oct 2026 20:00:16 -0400 Subject: [PATCH] fix(theme): give migrated V1 dialogs an opaque background (#60016) --- .../theme/src/tui/v1-migrate-new-designs.ts | 15 +++++--- packages/theme/src/tui/v1-migrate.ts | 14 +++++++- packages/tui/test/theme/v2/v1-migrate.test.ts | 36 +++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/theme/src/tui/v1-migrate-new-designs.ts b/packages/theme/src/tui/v1-migrate-new-designs.ts index 76c935a7a8b..98061ea9f62 100644 --- a/packages/theme/src/tui/v1-migrate-new-designs.ts +++ b/packages/theme/src/tui/v1-migrate-new-designs.ts @@ -9,6 +9,7 @@ import { categoricalTokens, chromaticHues, detectMode, + dialogBackground, hex, inferHues, referenceHues, @@ -28,7 +29,7 @@ export function migrateV1NewDesigns(theme: ThemeV1Json): ThemeDocument { const darkMode = detectMode(dark) if (lightMode === darkMode) { const definition = migrateMode(lightMode === "light" ? light : dark, lightMode) - if (lightMode === "light") return { base: base(definition), light: lightOverrides(definition) } + if (lightMode === "light") return { base: base(definition), light: lightOverrides(definition, light) } return { base: base(definition), dark: { hue: definition.hue } } } } @@ -36,7 +37,7 @@ export function migrateV1NewDesigns(theme: ThemeV1Json): ThemeDocument { const darkDefinition = migrateMode(dark, "dark") return { base: base(lightDefinition), - light: lightOverrides(lightDefinition), + light: lightOverrides(lightDefinition, light), dark: darkDefinition, } } @@ -53,8 +54,12 @@ const lightDialog = { }, } satisfies ModeDefinition["@dialog"] -function lightOverrides(definition: ThemeDefinition): ModeDefinition { - return { hue: definition.hue, "@dialog": lightDialog } +function lightOverrides(definition: ThemeDefinition, theme: Theme): ModeDefinition { + if (theme.backgroundPanel.a === 1) return { hue: definition.hue, "@dialog": lightDialog } + return { + hue: definition.hue, + "@dialog": { ...lightDialog, background: { ...lightDialog.background, base: dialogBackground(theme) } }, + } } // Mirrors the opencode2 token layout: text at 100/200/300, the page at 900, raised surfaces at 700/600/500, @@ -125,7 +130,7 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition { ...paletteTokens(theme), "@dialog": { background: { - base: "$background.raised.base", + base: dialogBackground(theme), action: { primary: { $hovered: "$background.raised.max" }, secondary: { $hovered: "$background.raised.max" }, diff --git a/packages/theme/src/tui/v1-migrate.ts b/packages/theme/src/tui/v1-migrate.ts index 659b803c21d..a7ad1df4500 100644 --- a/packages/theme/src/tui/v1-migrate.ts +++ b/packages/theme/src/tui/v1-migrate.ts @@ -217,13 +217,25 @@ function migrateMode(theme: Theme, mode: Mode): ThemeDefinition { }, "@dialog": { background: { - base: "$background.raised.base", + base: dialogBackground(theme), action: { primary: { $hovered: "$background.raised.high" } }, }, }, }) } +// Dialogs float over the session, so a see-through panel would let the content underneath show through them. +// Fall back to the first visible surface color, made opaque. +export function dialogBackground(theme: Theme) { + if (theme.backgroundPanel.a === 1) return "$background.raised.base" + const visible = [theme.backgroundPanel, theme.backgroundMenu, theme.backgroundElement, theme.background].find( + (color) => color.a > 0, + ) + if (!visible) return luminance(theme.text) > 0.5 ? "#000000" : "#ffffff" + const [red, green, blue] = visible.toInts() + return hexInts(red, green, blue, 255) +} + export function referenceHues(theme: ThemeDefinition): ThemeDefinition { const definitions = theme.hue as Record>> | undefined if (!definitions) return theme diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index b78fce7d2b8..51ea522fe17 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -279,6 +279,42 @@ test("collapses identical V1 backgrounds when both variants infer one mode", () expect(selectThemeMode(migratedLight, "dark").mode).toBe("light") }) +test("gives dialogs an opaque background when V1 panels are see-through", () => { + const lucent = DEFAULT_THEMES["lucent-orng"]! + for (const mode of ["dark", "light"] as const) { + const legacy = resolveV1(lucent, mode) + const dialog = resolveThemeDocument(migrateV1(lucent), mode).surface("dialog") + expect(legacy.backgroundPanel.a).toBe(0) + const [red, green, blue] = legacy.backgroundMenu.toInts() + expect(legacy.backgroundMenu.a).toBeLessThan(1) + expect(dialog.background.base.toInts()).toEqual([red, green, blue, 255]) + } + + const source = structuredClone(opencodeV1) + source.theme.background = "transparent" + source.theme.backgroundPanel = "transparent" + source.theme.backgroundElement = "transparent" + source.theme.backgroundMenu = "transparent" + expect(hex(resolveThemeDocument(migrateV1(source), "dark").surface("dialog").background.base)).toBe("#000000") + expect(hex(resolveThemeDocument(migrateV1(source), "light").surface("dialog").background.base)).toBe("#ffffff") + + const opaque = resolveThemeDocument(migrateV1(opencodeV1), "dark") + expect(hex(opaque.surface("dialog").background.base)).toBe(hex(opaque.background.raised.base)) +}) + +test("gives new designs dialogs an opaque background when V1 panels are see-through", () => { + const lucent = DEFAULT_THEMES["lucent-orng"]! + for (const mode of ["dark", "light"] as const) { + const [red, green, blue] = resolveV1(lucent, mode).backgroundMenu.toInts() + const dialog = resolveThemeDocument(migrateV1NewDesigns(lucent), mode).surface("dialog") + expect(dialog.background.base.toInts()).toEqual([red, green, blue, 255]) + } + + const opaque = migrateV1NewDesigns(DEFAULT_THEMES.tokyonight!) + expect(opaque.base["@dialog"]?.background?.base).toBe("$background.raised.base") + expect(opaque.light?.["@dialog"]?.background?.base).toBe("$hue.neutral.800") +}) + test("keeps both modes when a shared background has different contrast", () => { const source = structuredClone(opencodeV1) source.theme.background = "#808080"