From 3a343a71a876d4e12ffb3029853d637e114977a4 Mon Sep 17 00:00:00 2001 From: kostiantyn-kugot Date: Mon, 23 Mar 2026 21:19:44 +0100 Subject: [PATCH 1/2] fix: support named colors and single-mode theme imports --- public/import-export.sh | 169 ++++++++++++++++++- src/app/themeEditorPageHelpers.test.ts | 51 ++++++ src/app/themeEditorPageHelpers.ts | 8 +- src/domain/theme/color.test.ts | 10 ++ src/domain/theme/colorCore.ts | 19 ++- src/domain/theme/namedColors.ts | 150 ++++++++++++++++ src/features/editor/JsonThemeEditor.test.tsx | 78 ++++++++- src/features/editor/jsonThemeEditorParser.ts | 86 ++++++++-- 8 files changed, 544 insertions(+), 27 deletions(-) create mode 100644 src/app/themeEditorPageHelpers.test.ts create mode 100644 src/domain/theme/namedColors.ts diff --git a/public/import-export.sh b/public/import-export.sh index fd9c745..9491aba 100644 --- a/public/import-export.sh +++ b/public/import-export.sh @@ -340,13 +340,169 @@ def parse_alpha(value: str) -> int: RGBA_PATTERN = re.compile(r'^rgba?\((.+)\)$', re.IGNORECASE) +COLOR_NAME_HEX = { + 'aliceblue': '#f0f8ff', + 'antiquewhite': '#faebd7', + 'aqua': '#00ffff', + 'aquamarine': '#7fffd4', + 'azure': '#f0ffff', + 'beige': '#f5f5dc', + 'bisque': '#ffe4c4', + 'black': '#000000', + 'blanchedalmond': '#ffebcd', + 'blue': '#0000ff', + 'blueviolet': '#8a2be2', + 'brown': '#a52a2a', + 'burlywood': '#deb887', + 'cadetblue': '#5f9ea0', + 'chartreuse': '#7fff00', + 'chocolate': '#d2691e', + 'coral': '#ff7f50', + 'cornflowerblue': '#6495ed', + 'cornsilk': '#fff8dc', + 'crimson': '#dc143c', + 'cyan': '#00ffff', + 'darkblue': '#00008b', + 'darkcyan': '#008b8b', + 'darkgoldenrod': '#b8860b', + 'darkgray': '#a9a9a9', + 'darkgreen': '#006400', + 'darkgrey': '#a9a9a9', + 'darkkhaki': '#bdb76b', + 'darkmagenta': '#8b008b', + 'darkolivegreen': '#556b2f', + 'darkorange': '#ff8c00', + 'darkorchid': '#9932cc', + 'darkred': '#8b0000', + 'darksalmon': '#e9967a', + 'darkseagreen': '#8fbc8f', + 'darkslateblue': '#483d8b', + 'darkslategray': '#2f4f4f', + 'darkslategrey': '#2f4f4f', + 'darkturquoise': '#00ced1', + 'darkviolet': '#9400d3', + 'deeppink': '#ff1493', + 'deepskyblue': '#00bfff', + 'dimgray': '#696969', + 'dimgrey': '#696969', + 'dodgerblue': '#1e90ff', + 'firebrick': '#b22222', + 'floralwhite': '#fffaf0', + 'forestgreen': '#228b22', + 'fuchsia': '#ff00ff', + 'gainsboro': '#dcdcdc', + 'ghostwhite': '#f8f8ff', + 'gold': '#ffd700', + 'goldenrod': '#daa520', + 'gray': '#808080', + 'green': '#008000', + 'greenyellow': '#adff2f', + 'grey': '#808080', + 'honeydew': '#f0fff0', + 'hotpink': '#ff69b4', + 'indianred': '#cd5c5c', + 'indigo': '#4b0082', + 'ivory': '#fffff0', + 'khaki': '#f0e68c', + 'lavender': '#e6e6fa', + 'lavenderblush': '#fff0f5', + 'lawngreen': '#7cfc00', + 'lemonchiffon': '#fffacd', + 'lightblue': '#add8e6', + 'lightcoral': '#f08080', + 'lightcyan': '#e0ffff', + 'lightgoldenrodyellow': '#fafad2', + 'lightgray': '#d3d3d3', + 'lightgreen': '#90ee90', + 'lightgrey': '#d3d3d3', + 'lightpink': '#ffb6c1', + 'lightsalmon': '#ffa07a', + 'lightseagreen': '#20b2aa', + 'lightskyblue': '#87cefa', + 'lightslategray': '#778899', + 'lightslategrey': '#778899', + 'lightsteelblue': '#b0c4de', + 'lightyellow': '#ffffe0', + 'lime': '#00ff00', + 'limegreen': '#32cd32', + 'linen': '#faf0e6', + 'magenta': '#ff00ff', + 'maroon': '#800000', + 'mediumaquamarine': '#66cdaa', + 'mediumblue': '#0000cd', + 'mediumorchid': '#ba55d3', + 'mediumpurple': '#9370db', + 'mediumseagreen': '#3cb371', + 'mediumslateblue': '#7b68ee', + 'mediumspringgreen': '#00fa9a', + 'mediumturquoise': '#48d1cc', + 'mediumvioletred': '#c71585', + 'midnightblue': '#191970', + 'mintcream': '#f5fffa', + 'mistyrose': '#ffe4e1', + 'moccasin': '#ffe4b5', + 'navajowhite': '#ffdead', + 'navy': '#000080', + 'oldlace': '#fdf5e6', + 'olive': '#808000', + 'olivedrab': '#6b8e23', + 'orange': '#ffa500', + 'orangered': '#ff4500', + 'orchid': '#da70d6', + 'palegoldenrod': '#eee8aa', + 'palegreen': '#98fb98', + 'paleturquoise': '#afeeee', + 'palevioletred': '#db7093', + 'papayawhip': '#ffefd5', + 'peachpuff': '#ffdab9', + 'peru': '#cd853f', + 'pink': '#ffc0cb', + 'plum': '#dda0dd', + 'powderblue': '#b0e0e6', + 'purple': '#800080', + 'rebeccapurple': '#663399', + 'red': '#ff0000', + 'rosybrown': '#bc8f8f', + 'royalblue': '#4169e1', + 'saddlebrown': '#8b4513', + 'salmon': '#fa8072', + 'sandybrown': '#f4a460', + 'seagreen': '#2e8b57', + 'seashell': '#fff5ee', + 'sienna': '#a0522d', + 'silver': '#c0c0c0', + 'skyblue': '#87ceeb', + 'slateblue': '#6a5acd', + 'slategray': '#708090', + 'slategrey': '#708090', + 'snow': '#fffafa', + 'springgreen': '#00ff7f', + 'steelblue': '#4682b4', + 'tan': '#d2b48c', + 'teal': '#008080', + 'thistle': '#d8bfd8', + 'tomato': '#ff6347', + 'turquoise': '#40e0d0', + 'violet': '#ee82ee', + 'wheat': '#f5deb3', + 'white': '#ffffff', + 'whitesmoke': '#f5f5f5', + 'yellow': '#ffff00', + 'yellowgreen': '#9acd32', +} + def parse_color(value: str): normalized = value.strip().lower() - if normalized == 'transparent': + if normalized in ('transparent', 'none'): return 0, 0, 0, 0 + named_color = COLOR_NAME_HEX.get(normalized) + + if named_color is not None: + normalized = named_color + if normalized.startswith('#'): hex_value = normalized[1:] @@ -408,12 +564,21 @@ def normalize_theme_file(data: dict): if isinstance(value, dict): dark = value.get('dark') light = value.get('light') + + if dark is None and light is None: + raise SystemExit(f'theme token {token} must contain a color string or dark/light color strings') + + if dark is None: + dark = light + + if light is None: + light = dark else: dark = value light = value if not isinstance(dark, str) or not isinstance(light, str): - raise SystemExit(f'theme token {token} must contain color strings for dark and light') + raise SystemExit(f'theme token {token} must contain color strings') dark_rgba = parse_color(dark) light_rgba = parse_color(light) diff --git a/src/app/themeEditorPageHelpers.test.ts b/src/app/themeEditorPageHelpers.test.ts new file mode 100644 index 0000000..6402760 --- /dev/null +++ b/src/app/themeEditorPageHelpers.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from 'vitest' +import { createDefaultThemeDraft } from '../domain/theme/createDefaultThemeDraft' +import { THEME_TOKEN_NAMES } from '../domain/theme/model' +import { selectExportThemeFile } from '../state/selectors' +import { applyJsonModeThemes } from './themeEditorPageHelpers' + +describe('applyJsonModeThemes', () => { + it('merges partial mode updates into the current resolved theme', () => { + const draft = createDefaultThemeDraft() + const replaceModeDraft = vi.fn() + const currentDarkTheme = selectExportThemeFile(draft, 'dark').theme + + applyJsonModeThemes( + draft, + [...THEME_TOKEN_NAMES], + { + dark: { + text: '#008080', + }, + }, + replaceModeDraft, + ) + + expect(replaceModeDraft).toHaveBeenCalledTimes(1) + expect(replaceModeDraft).toHaveBeenCalledWith('dark', { + ...draft.modes.dark, + tokenOverrides: { + ...currentDarkTheme, + text: '#008080', + }, + }) + }) + + it('skips replacements when a partial update does not change the resolved theme', () => { + const draft = createDefaultThemeDraft() + const replaceModeDraft = vi.fn() + + applyJsonModeThemes( + draft, + [...THEME_TOKEN_NAMES], + { + dark: { + text: selectExportThemeFile(draft, 'dark').theme.text, + }, + }, + replaceModeDraft, + ) + + expect(replaceModeDraft).not.toHaveBeenCalled() + }) +}) diff --git a/src/app/themeEditorPageHelpers.ts b/src/app/themeEditorPageHelpers.ts index fffe074..7d13965 100644 --- a/src/app/themeEditorPageHelpers.ts +++ b/src/app/themeEditorPageHelpers.ts @@ -52,7 +52,11 @@ export function applyJsonModeThemes( } const currentThemeFile = selectExportThemeFile(draft, mode) - const hasChanges = tokenNames.some((token) => currentThemeFile.theme[token] !== modeTheme[token]) + const nextTheme = { + ...currentThemeFile.theme, + ...modeTheme, + } satisfies ThemeTokens + const hasChanges = tokenNames.some((token) => currentThemeFile.theme[token] !== nextTheme[token]) if (!hasChanges) { continue @@ -60,7 +64,7 @@ export function applyJsonModeThemes( replaceModeDraft(mode, { ...draft.modes[mode], - tokenOverrides: modeTheme, + tokenOverrides: nextTheme, }) } } diff --git a/src/domain/theme/color.test.ts b/src/domain/theme/color.test.ts index 6301e30..bf6e4d0 100644 --- a/src/domain/theme/color.test.ts +++ b/src/domain/theme/color.test.ts @@ -24,10 +24,13 @@ describe('color helpers', () => { expect(normalizeColorValue(' #AbC ')).toBe('#aabbcc') expect(normalizeColorValue('#abcd')).toBe('#aabbccdd') expect(normalizeColorValue('#ABCDEF')).toBe('#abcdef') + expect(normalizeColorValue('Teal')).toBe('#008080') + expect(normalizeColorValue('rebeccapurple')).toBe('#663399') expect(normalizeColorValue('none')).toBe('transparent') expect(normalizeColorValue('transparent')).toBe('transparent') expect(normalizeColorValue('rgb(1, 2, 3)')).toBeNull() expect(isColorValue('#123456')).toBe(true) + expect(isColorValue('teal')).toBe(true) expect(isColorValue('oops')).toBe(false) }) @@ -50,6 +53,12 @@ describe('color helpers', () => { b: 0, a: 0, }) + expect(parseColor('teal')).toEqual({ + r: 0, + g: 128, + b: 128, + a: 1, + }) expect(parseColor('invalid')).toBeNull() expect( @@ -95,6 +104,7 @@ describe('color helpers', () => { it('normalizes equality and color input helpers', () => { expect(areColorValuesEqual('#abc', '#aabbcc')).toBe(true) expect(areColorValuesEqual('transparent', 'none')).toBe(true) + expect(areColorValuesEqual('teal', '#008080')).toBe(true) expect(areColorValuesEqual('#abc', '#000000')).toBe(false) expect(getColorInputValue('#12345680')).toBe('#123456') diff --git a/src/domain/theme/colorCore.ts b/src/domain/theme/colorCore.ts index 53b4fa3..2ea9ac2 100644 --- a/src/domain/theme/colorCore.ts +++ b/src/domain/theme/colorCore.ts @@ -1,3 +1,5 @@ +import { CSS_NAMED_COLORS } from './namedColors' + export type ParsedColor = { r: number g: number @@ -29,17 +31,24 @@ export function clampAlpha(value: number) { export function normalizeColorValue(value: string) { const trimmed = value.trim() + const lowered = trimmed.toLowerCase() - if (TRANSPARENT_COLOR_PATTERN.test(trimmed)) { + if (TRANSPARENT_COLOR_PATTERN.test(lowered)) { return 'transparent' } - if (HEX_3_COLOR_PATTERN.test(trimmed) || HEX_4_COLOR_PATTERN.test(trimmed)) { - return expandShortHex(trimmed.toLowerCase()) + if (HEX_3_COLOR_PATTERN.test(lowered) || HEX_4_COLOR_PATTERN.test(lowered)) { + return expandShortHex(lowered) + } + + if (HEX_6_COLOR_PATTERN.test(lowered) || HEX_8_COLOR_PATTERN.test(lowered)) { + return lowered } - if (HEX_6_COLOR_PATTERN.test(trimmed) || HEX_8_COLOR_PATTERN.test(trimmed)) { - return trimmed.toLowerCase() + const namedColor = CSS_NAMED_COLORS[lowered as keyof typeof CSS_NAMED_COLORS] + + if (namedColor !== undefined) { + return namedColor } return null diff --git a/src/domain/theme/namedColors.ts b/src/domain/theme/namedColors.ts new file mode 100644 index 0000000..27167e7 --- /dev/null +++ b/src/domain/theme/namedColors.ts @@ -0,0 +1,150 @@ +export const CSS_NAMED_COLORS = { + aliceblue: '#f0f8ff', + antiquewhite: '#faebd7', + aqua: '#00ffff', + aquamarine: '#7fffd4', + azure: '#f0ffff', + beige: '#f5f5dc', + bisque: '#ffe4c4', + black: '#000000', + blanchedalmond: '#ffebcd', + blue: '#0000ff', + blueviolet: '#8a2be2', + brown: '#a52a2a', + burlywood: '#deb887', + cadetblue: '#5f9ea0', + chartreuse: '#7fff00', + chocolate: '#d2691e', + coral: '#ff7f50', + cornflowerblue: '#6495ed', + cornsilk: '#fff8dc', + crimson: '#dc143c', + cyan: '#00ffff', + darkblue: '#00008b', + darkcyan: '#008b8b', + darkgoldenrod: '#b8860b', + darkgray: '#a9a9a9', + darkgreen: '#006400', + darkgrey: '#a9a9a9', + darkkhaki: '#bdb76b', + darkmagenta: '#8b008b', + darkolivegreen: '#556b2f', + darkorange: '#ff8c00', + darkorchid: '#9932cc', + darkred: '#8b0000', + darksalmon: '#e9967a', + darkseagreen: '#8fbc8f', + darkslateblue: '#483d8b', + darkslategray: '#2f4f4f', + darkslategrey: '#2f4f4f', + darkturquoise: '#00ced1', + darkviolet: '#9400d3', + deeppink: '#ff1493', + deepskyblue: '#00bfff', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1e90ff', + firebrick: '#b22222', + floralwhite: '#fffaf0', + forestgreen: '#228b22', + fuchsia: '#ff00ff', + gainsboro: '#dcdcdc', + ghostwhite: '#f8f8ff', + gold: '#ffd700', + goldenrod: '#daa520', + gray: '#808080', + green: '#008000', + greenyellow: '#adff2f', + grey: '#808080', + honeydew: '#f0fff0', + hotpink: '#ff69b4', + indianred: '#cd5c5c', + indigo: '#4b0082', + ivory: '#fffff0', + khaki: '#f0e68c', + lavender: '#e6e6fa', + lavenderblush: '#fff0f5', + lawngreen: '#7cfc00', + lemonchiffon: '#fffacd', + lightblue: '#add8e6', + lightcoral: '#f08080', + lightcyan: '#e0ffff', + lightgoldenrodyellow: '#fafad2', + lightgray: '#d3d3d3', + lightgreen: '#90ee90', + lightgrey: '#d3d3d3', + lightpink: '#ffb6c1', + lightsalmon: '#ffa07a', + lightseagreen: '#20b2aa', + lightskyblue: '#87cefa', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#b0c4de', + lightyellow: '#ffffe0', + lime: '#00ff00', + limegreen: '#32cd32', + linen: '#faf0e6', + magenta: '#ff00ff', + maroon: '#800000', + mediumaquamarine: '#66cdaa', + mediumblue: '#0000cd', + mediumorchid: '#ba55d3', + mediumpurple: '#9370db', + mediumseagreen: '#3cb371', + mediumslateblue: '#7b68ee', + mediumspringgreen: '#00fa9a', + mediumturquoise: '#48d1cc', + mediumvioletred: '#c71585', + midnightblue: '#191970', + mintcream: '#f5fffa', + mistyrose: '#ffe4e1', + moccasin: '#ffe4b5', + navajowhite: '#ffdead', + navy: '#000080', + oldlace: '#fdf5e6', + olive: '#808000', + olivedrab: '#6b8e23', + orange: '#ffa500', + orangered: '#ff4500', + orchid: '#da70d6', + palegoldenrod: '#eee8aa', + palegreen: '#98fb98', + paleturquoise: '#afeeee', + palevioletred: '#db7093', + papayawhip: '#ffefd5', + peachpuff: '#ffdab9', + peru: '#cd853f', + pink: '#ffc0cb', + plum: '#dda0dd', + powderblue: '#b0e0e6', + purple: '#800080', + rebeccapurple: '#663399', + red: '#ff0000', + rosybrown: '#bc8f8f', + royalblue: '#4169e1', + saddlebrown: '#8b4513', + salmon: '#fa8072', + sandybrown: '#f4a460', + seagreen: '#2e8b57', + seashell: '#fff5ee', + sienna: '#a0522d', + silver: '#c0c0c0', + skyblue: '#87ceeb', + slateblue: '#6a5acd', + slategray: '#708090', + slategrey: '#708090', + snow: '#fffafa', + springgreen: '#00ff7f', + steelblue: '#4682b4', + tan: '#d2b48c', + teal: '#008080', + thistle: '#d8bfd8', + tomato: '#ff6347', + turquoise: '#40e0d0', + violet: '#ee82ee', + wheat: '#f5deb3', + white: '#ffffff', + whitesmoke: '#f5f5f5', + yellow: '#ffff00', + yellowgreen: '#9acd32', +} as const diff --git a/src/features/editor/JsonThemeEditor.test.tsx b/src/features/editor/JsonThemeEditor.test.tsx index e095035..c087734 100644 --- a/src/features/editor/JsonThemeEditor.test.tsx +++ b/src/features/editor/JsonThemeEditor.test.tsx @@ -66,7 +66,7 @@ describe('JsonThemeEditor', () => { { $schema: 'https://opencode.ai/theme.json', defs: { - 'dark-text': '#111111', + 'dark-text': 'teal', 'light-text': '#EEE', }, theme: exportCombinedThemeFile(darkTheme as typeof props.themeFile.theme, lightTheme as typeof props.themeFile.theme).theme, @@ -81,7 +81,7 @@ describe('JsonThemeEditor', () => { expect(payload.dark).toEqual( expect.objectContaining({ - text: '#111111', + text: '#008080', }), ) expect(payload.light).toEqual( @@ -91,6 +91,70 @@ describe('JsonThemeEditor', () => { ) }) + it('applies a dark-only combined edit and leaves light mode unchanged', () => { + const props = buildProps('dark') + const darkOnlyTheme = Object.fromEntries( + props.tokenNames.map((token) => [token, { dark: token === 'text' ? 'teal' : props.combinedThemeFile.theme[token].dark }]), + ) + + render() + + fireEvent.change(screen.getByLabelText('Theme JSON editor'), { + target: { + value: JSON.stringify( + { + $schema: 'https://opencode.ai/theme.json', + theme: darkOnlyTheme, + }, + null, + 2, + ), + }, + }) + + const payload = props.onChange.mock.lastCall?.[0] + + expect(payload.dark).toEqual( + expect.objectContaining({ + text: '#008080', + }), + ) + expect(payload.light).toBeUndefined() + expect(screen.queryByRole('status')).not.toBeInTheDocument() + }) + + it('applies a light-only combined edit and leaves dark mode unchanged', () => { + const props = buildProps('dark') + const lightOnlyTheme = Object.fromEntries( + props.tokenNames.map((token) => [token, { light: token === 'text' ? 'teal' : props.combinedThemeFile.theme[token].light }]), + ) + + render() + + fireEvent.change(screen.getByLabelText('Theme JSON editor'), { + target: { + value: JSON.stringify( + { + $schema: 'https://opencode.ai/theme.json', + theme: lightOnlyTheme, + }, + null, + 2, + ), + }, + }) + + const payload = props.onChange.mock.lastCall?.[0] + + expect(payload.dark).toBeUndefined() + expect(payload.light).toEqual( + expect.objectContaining({ + text: '#008080', + }), + ) + expect(screen.queryByRole('status')).not.toBeInTheDocument() + }) + it('shows the current mixed-format validation error', () => { const props = buildProps('dark') @@ -182,11 +246,17 @@ describe('JsonThemeEditor', () => { props.onChange(modeThemes) if (modeThemes.dark) { - setDarkTheme(modeThemes.dark) + setDarkTheme((current) => ({ + ...current, + ...modeThemes.dark, + })) } if (modeThemes.light) { - setLightTheme(modeThemes.light) + setLightTheme((current) => ({ + ...current, + ...modeThemes.light, + })) } }} /> diff --git a/src/features/editor/jsonThemeEditorParser.ts b/src/features/editor/jsonThemeEditorParser.ts index 56bbb90..6fb5131 100644 --- a/src/features/editor/jsonThemeEditorParser.ts +++ b/src/features/editor/jsonThemeEditorParser.ts @@ -7,7 +7,7 @@ import { import { normalizeColorValue } from '../../domain/theme/color' import type { ThemeMode, ThemeTokenName, ThemeTokens } from '../../domain/theme/model' -export type JsonThemeModeUpdates = Partial> +export type JsonThemeModeUpdates = Partial>> export type ParseJsonThemeResult = | { @@ -127,31 +127,92 @@ function parseCombinedTheme( ): ParseJsonThemeResult { const darkTheme = {} as ThemeTokens const lightTheme = {} as ThemeTokens + const darkModeUpdates: Partial = {} + const lightModeUpdates: Partial = {} + let hasDarkValues = false + let hasLightValues = false for (const token of tokenNames) { const tokenValue = theme[token] - if (!isRecord(tokenValue) || typeof tokenValue.dark !== 'string' || typeof tokenValue.light !== 'string') { + if (!isRecord(tokenValue)) { return { ok: false, - error: `Token \`${token}\` must include string \`dark\` and \`light\` values`, + error: `Token \`${token}\` must be an object with string \`dark\` and/or \`light\` values`, } } - const resolvedDark = resolveThemeTokenColor(tokenValue.dark, defs, token) + const rawDark = tokenValue.dark + const rawLight = tokenValue.light - if (!resolvedDark.ok) { + if (rawDark !== undefined && typeof rawDark !== 'string') { + return { + ok: false, + error: `Token \`${token}\` \`dark\` value must be a string when provided`, + } + } + + if (rawLight !== undefined && typeof rawLight !== 'string') { + return { + ok: false, + error: `Token \`${token}\` \`light\` value must be a string when provided`, + } + } + + if (rawDark === undefined && rawLight === undefined) { + return { + ok: false, + error: `Token \`${token}\` must include a string \`dark\` or \`light\` value`, + } + } + + const resolvedDark = + rawDark === undefined + ? null + : resolveThemeTokenColor(rawDark, defs, token) + + if (resolvedDark && !resolvedDark.ok) { return { ok: false, error: resolvedDark.error } } - const resolvedLight = resolveThemeTokenColor(tokenValue.light, defs, token) + const resolvedLight = + rawLight === undefined + ? null + : resolveThemeTokenColor(rawLight, defs, token) - if (!resolvedLight.ok) { + if (resolvedLight && !resolvedLight.ok) { return { ok: false, error: resolvedLight.error } } - darkTheme[token] = resolvedDark.value - lightTheme[token] = resolvedLight.value + if (resolvedDark) { + darkModeUpdates[token] = resolvedDark.value + darkTheme[token] = resolvedDark.value + hasDarkValues = true + } + + if (resolvedLight) { + lightModeUpdates[token] = resolvedLight.value + lightTheme[token] = resolvedLight.value + hasLightValues = true + } + + if (!resolvedDark && resolvedLight) { + darkTheme[token] = resolvedLight.value + } + + if (!resolvedLight && resolvedDark) { + lightTheme[token] = resolvedDark.value + } + } + + const modeThemes: JsonThemeModeUpdates = {} + + if (hasDarkValues) { + modeThemes.dark = darkModeUpdates + } + + if (hasLightValues) { + modeThemes.light = lightModeUpdates } return { @@ -159,10 +220,7 @@ function parseCombinedTheme( value: { format: 'combined', themeFile: exportCombinedThemeFile(darkTheme, lightTheme), - modeThemes: { - dark: darkTheme, - light: lightTheme, - }, + modeThemes, }, } } @@ -244,7 +302,7 @@ export function parseJsonThemeFile(value: string, tokenNames: ThemeTokenName[], return { ok: false, - error: `Token \`${token}\` must be a string or an object with \`dark\` and \`light\``, + error: `Token \`${token}\` must be a string or an object with \`dark\` and/or \`light\``, } } From d9bc6798973344c404a43f8dfe90669f5927e403 Mon Sep 17 00:00:00 2001 From: kostiantyn-kugot Date: Mon, 23 Mar 2026 21:20:19 +0100 Subject: [PATCH 2/2] chore(release): 1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e39d9fe..cd04b89 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opencode-theme-editor", "private": true, - "version": "1.2.0", + "version": "1.2.1", "type": "module", "scripts": { "dev": "vite",