Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode-theme-editor",
"private": true,
"version": "1.2.0",
"version": "1.2.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
169 changes: 167 additions & 2 deletions public/import-export.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:]

Expand Down Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions src/app/themeEditorPageHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
8 changes: 6 additions & 2 deletions src/app/themeEditorPageHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,19 @@ 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
}

replaceModeDraft(mode, {
...draft.modes[mode],
tokenOverrides: modeTheme,
tokenOverrides: nextTheme,
})
}
}
10 changes: 10 additions & 0 deletions src/domain/theme/color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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(
Expand Down Expand Up @@ -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')
Expand Down
19 changes: 14 additions & 5 deletions src/domain/theme/colorCore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CSS_NAMED_COLORS } from './namedColors'

export type ParsedColor = {
r: number
g: number
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading