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",
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\``,
}
}