fix(theme): give migrated V1 dialogs an opaque background (#60016)

This commit is contained in:
James Long
2026-10-28 20:00:16 -04:00
committed by GitHub
Unverified
parent 77a0fd6f3b
commit f43563b2f4
3 changed files with 59 additions and 6 deletions
@@ -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" },
+13 -1
View File
@@ -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<string, string | Partial<Record<HueStep, string>>> | undefined
if (!definitions) return theme
@@ -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"