From 82633d97f5d44be6fd2498a74a35bbc88539a615 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 22 May 2026 15:21:36 +0300 Subject: [PATCH] fix: update color values in Flexoki themes and enhance theme conversion script --- .../ui/src/lib/theme/themes/flexoki-dark.json | 14 +- .../src/lib/theme/themes/flexoki-light.json | 28 +-- scripts/convert-vscode-theme.cjs | 185 ++++++++++++++++-- 3 files changed, 188 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/lib/theme/themes/flexoki-dark.json b/packages/ui/src/lib/theme/themes/flexoki-dark.json index 576b9c3e..b0d6e43c 100644 --- a/packages/ui/src/lib/theme/themes/flexoki-dark.json +++ b/packages/ui/src/lib/theme/themes/flexoki-dark.json @@ -17,12 +17,12 @@ "base": "#DA702C", "hover": "#DA702C", "active": "#F9AE77", - "foreground": "#151313", + "foreground": "#171515", "muted": "#DA702C80", "emphasis": "#F9AE77" }, "surface": { - "background": "#151313", + "background": "#171515", "foreground": "#CECDC3", "muted": "#1C1B1A", "mutedForeground": "#807e79", @@ -45,19 +45,19 @@ }, "status": { "error": "#D14D41", - "errorForeground": "#151313", + "errorForeground": "#171515", "errorBackground": "#AF302920", "errorBorder": "#AF302950", "warning": "#DA702C", - "warningForeground": "#151313", + "warningForeground": "#171515", "warningBackground": "#BC521520", "warningBorder": "#BC521550", "success": "#A0AF54", - "successForeground": "#151313", + "successForeground": "#171515", "successBackground": "#66800B20", "successBorder": "#66800B50", "info": "#4385BE", - "infoForeground": "#151313", + "infoForeground": "#171515", "infoBackground": "#205EA620", "infoBorder": "#205EA650" }, @@ -145,7 +145,7 @@ "userMessage": "#CECDC3", "userMessageBackground": "#27180E", "assistantMessage": "#CECDC3", - "assistantMessageBackground": "#151313", + "assistantMessageBackground": "#171515", "timestamp": "#878580", "divider": "#343331" }, diff --git a/packages/ui/src/lib/theme/themes/flexoki-light.json b/packages/ui/src/lib/theme/themes/flexoki-light.json index 0f9345ca..b8b20188 100644 --- a/packages/ui/src/lib/theme/themes/flexoki-light.json +++ b/packages/ui/src/lib/theme/themes/flexoki-light.json @@ -17,19 +17,19 @@ "base": "#BC5215", "hover": "#F9AE77", "active": "#DA702C", - "foreground": "#FFFCF0", + "foreground": "#fffdf4", "muted": "#BC521580", "emphasis": "#F9AE77" }, "surface": { - "background": "#FFFCF0", + "background": "#fffdf4", "foreground": "#100F0F", - "muted": "#F2F0E5", + "muted": "#f6f5ee", "mutedForeground": "#686663", - "elevated": "#fdf6ec", + "elevated": "#fbfaf2", "elevatedForeground": "#100F0F", "overlay": "#100F0F20", - "subtle": "#F6F0E6" + "subtle": "#f7f4ec" }, "interactive": { "border": "#DAD8CE", @@ -45,19 +45,19 @@ }, "status": { "error": "#AF3029", - "errorForeground": "#FFFCF0", + "errorForeground": "#fffdf4", "errorBackground": "#D14D4120", "errorBorder": "#D14D4150", "warning": "#BC5215", - "warningForeground": "#FFFCF0", + "warningForeground": "#fffdf4", "warningBackground": "#DA702C20", "warningBorder": "#DA702C50", "success": "#66800B", - "successForeground": "#FFFCF0", + "successForeground": "#fffdf4", "successBackground": "#879A3920", "successBorder": "#879A3950", "info": "#205EA6", - "infoForeground": "#FFFCF0", + "infoForeground": "#fffdf4", "infoBackground": "#4385BE20", "infoBorder": "#4385BE50" }, @@ -70,7 +70,7 @@ }, "syntax": { "base": { - "background": "#F2F0E5", + "background": "#f6f5ee", "foreground": "#100F0F", "comment": "#6F6E69", "keyword": "#205EA6", @@ -136,21 +136,21 @@ "link": "#205EA6", "linkHover": "#4385BE", "inlineCode": "#24837B", - "inlineCodeBackground": "#F2F0E5", + "inlineCodeBackground": "#f6f5ee", "blockquote": "#6F6E69", "blockquoteBorder": "#DAD8CE", "listMarker": "#AD830199" }, "chat": { "userMessage": "#100F0F", - "userMessageBackground": "#F6F0E6", + "userMessageBackground": "#f7f4ec", "assistantMessage": "#100F0F", - "assistantMessageBackground": "#FFFCF0", + "assistantMessageBackground": "#fffdf4", "timestamp": "#6F6E69", "divider": "#DAD8CE" }, "tools": { - "background": "#F2F0E550", + "background": "#f6f5ee50", "border": "#b4b2a980", "headerHover": "#DAD8CE", "icon": "#6F6E69", diff --git a/scripts/convert-vscode-theme.cjs b/scripts/convert-vscode-theme.cjs index 7c6b4bff..c4201f88 100644 --- a/scripts/convert-vscode-theme.cjs +++ b/scripts/convert-vscode-theme.cjs @@ -32,6 +32,7 @@ * - colors (UI colors) * - tokenColors (TextMate scopes) * - semanticTokenColors (optional, LSP tokens) + * Also accepts Zed themes with a `themes[].style` object. */ const fs = require('fs'); @@ -50,11 +51,49 @@ const THEMES_DIR = path.join( const PRESETS_PATH = path.join(THEMES_DIR, 'presets.ts'); function stripJsonComments(jsonString) { - // Remove single-line comments (// ...) - jsonString = jsonString.replace(/\/\/.*$/gm, ''); - // Remove multi-line comments (/* ... */) - jsonString = jsonString.replace(/\/\*[\s\S]*?\*\//g, ''); - return jsonString; + let output = ''; + let inString = false; + let escaped = false; + + for (let i = 0; i < jsonString.length; i++) { + const char = jsonString[i]; + const next = jsonString[i + 1]; + + if (inString) { + output += char; + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === '"') { + inString = false; + } + continue; + } + + if (char === '"') { + inString = true; + output += char; + continue; + } + + if (char === '/' && next === '/') { + while (i < jsonString.length && jsonString[i] !== '\n') i++; + output += '\n'; + continue; + } + + if (char === '/' && next === '*') { + i += 2; + while (i < jsonString.length && !(jsonString[i] === '*' && jsonString[i + 1] === '/')) i++; + i++; + continue; + } + + output += char; + } + + return output; } function convertVsCodeTheme(vscodeThemePath) { @@ -63,14 +102,15 @@ function convertVsCodeTheme(vscodeThemePath) { const cleanContent = stripJsonComments(rawContent); const vscodeTheme = JSON.parse(cleanContent); - const colors = vscodeTheme.colors || {}; - const tokenColors = vscodeTheme.tokenColors || []; - const semanticTokenColors = vscodeTheme.semanticTokenColors || {}; + const normalizedTheme = normalizeThemeSource(vscodeTheme); + const colors = normalizedTheme.colors || {}; + const tokenColors = normalizedTheme.tokenColors || []; + const semanticTokenColors = normalizedTheme.semanticTokenColors || {}; // Determine variant - const isDark = vscodeTheme.type === 'dark' || - colors['editor.background']?.toLowerCase() < '#808080' || - colors['workbench.colorTheme']?.includes('dark'); + const isDark = normalizedTheme.type === 'dark' || + colors['editor.background']?.toLowerCase() < '#808080' || + colors['workbench.colorTheme']?.includes('dark'); const variant = isDark ? 'dark' : 'light'; @@ -82,7 +122,7 @@ function convertVsCodeTheme(vscodeThemePath) { .replace(/\b\w/g, (c) => c.toUpperCase()); // Capitalize each word }; - const themeName = toTitleCase(vscodeTheme.name || 'Untitled Theme'); + const themeName = toTitleCase(normalizedTheme.name || 'Untitled Theme'); // Extract color sets for derived sections const primaryColors = extractPrimaryColors(colors, semanticTokenColors); @@ -93,18 +133,20 @@ function convertVsCodeTheme(vscodeThemePath) { // Build OpenChamber theme const openchamberTheme = { metadata: { - id: vscodeTheme.name?.toLowerCase().replace(/[^a-z0-9]+/g, '-') + `-${variant}`, + id: makeThemeId(normalizedTheme.name || 'untitled-theme', variant), name: themeName, - description: vscodeTheme.description || `Converted from VS Code theme`, - version: vscodeTheme.version || '1.0.0', + description: normalizedTheme.description || `Converted from ${normalizedTheme.sourceFormat} theme`, + author: normalizedTheme.author, + version: normalizedTheme.version || '1.0.0', variant: variant, - tags: [variant, 'converted', 'vscode'] + tags: [variant, 'converted', normalizedTheme.sourceFormat] }, colors: { primary: primaryColors, surface: surfaceColors, interactive: extractInteractiveColors(colors), status: statusColors, + pr: generatePrColors(statusColors, syntaxColors), syntax: syntaxColors, markdown: generateMarkdownColors(primaryColors, surfaceColors, syntaxColors), chat: generateChatColors(surfaceColors), @@ -116,6 +158,92 @@ function convertVsCodeTheme(vscodeThemePath) { return openchamberTheme; } +function generatePrColors(status, syntax) { + return { + open: status.success, + draft: syntax.base.comment, + blocked: status.warning, + merged: syntax.tokens?.interface || syntax.base.type, + closed: status.error, + }; +} + +function makeThemeId(name, variant) { + const base = name + .toLowerCase() + .replace(/\b(light|dark)\b/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + + return `${base || 'theme'}-${variant}`; +} + +function normalizeThemeSource(theme) { + if (Array.isArray(theme.themes) && theme.themes[0]?.style) { + return normalizeZedTheme(theme); + } + + return { + ...theme, + sourceFormat: 'vscode', + }; +} + +function normalizeZedTheme(theme) { + const zedTheme = theme.themes[0]; + const style = zedTheme.style || {}; + const syntax = style.syntax || {}; + const syntaxColor = (key) => syntax[key]?.color || undefined; + + return { + name: zedTheme.name || theme.name, + author: theme.author, + description: `Converted from Zed theme ${zedTheme.name || theme.name}`, + version: theme.version || '1.0.0', + type: zedTheme.appearance === 'light' ? 'light' : 'dark', + sourceFormat: 'zed', + colors: { + 'button.background': style['text.accent'] || style['border.focused'], + 'button.hoverBackground': style['border.selected'] || style['text.accent'], + 'button.foreground': style.background, + 'activityBarBadge.background': style['text.accent'], + 'editor.background': style['editor.background'] || style.background, + 'editor.foreground': style['editor.foreground'] || style.text, + 'editor.lineHighlightBackground': style['editor.active_line.background'] || style['element.selected'], + 'editor.selectionBackground': style['element.selected'], + 'editorCursor.foreground': style['text.accent'] || style.text, + 'editorWhitespace.foreground': style['editor.invisible'] || style.border, + 'editorLineNumber.foreground': style['editor.line_number'], + 'editorLineNumber.activeForeground': style['editor.active_line_number'], + 'focusBorder': style['border.focused'], + 'input.border': style.border, + 'inputOption.activeBorder': style['border.selected'] || style['border.focused'], + 'sideBar.background': style['panel.background'] || style['surface.background'], + 'editorError.foreground': style.error, + 'editorWarning.foreground': style.warning, + 'editorInfo.foreground': style.info, + 'gitDecoration.addedResourceForeground': style.created || style['version_control.added'], + 'gitDecoration.deletedResourceForeground': style.deleted || style['version_control.deleted'], + }, + tokenColors: [], + semanticTokenColors: { + comment: syntaxColor('comment'), + keyword: syntaxColor('keyword'), + string: syntaxColor('string'), + number: syntaxColor('number'), + function: syntaxColor('function'), + method: syntaxColor('function.definition') || syntaxColor('function'), + variable: syntaxColor('variable'), + type: syntaxColor('type'), + class: syntaxColor('type'), + interface: syntaxColor('type.interface'), + property: syntaxColor('property'), + operator: syntaxColor('operator'), + enumMember: syntaxColor('type.enum.member') || syntaxColor('boolean'), + }, + }; +} + // Generate markdown colors from theme palette function generateMarkdownColors(primary, surface, syntax) { return { @@ -323,19 +451,40 @@ function extractSyntaxColors(tokenColors, semanticTokenColors, colors) { method: getColor('method', ['entity.name.function.method'], '#879A39'), variableProperty: getColor('property', ['variable.other.object.property'], '#de956a'), variableOther: getColor('variable', ['variable.other'], '#879A39'), + variableGlobal: getColor('variable', ['variable.other.global'], '#CE5D97'), + variableLocal: getColor('variable', ['variable.other.local'], editorFg), + parameter: getColor('parameter', ['variable.parameter'], editorFg), + constant: getColor('constant', ['constant'], editorFg), class: getColor('class', ['entity.name.type.class'], '#e0a98eff'), className: getColor('class', ['entity.name.type.class'], '#e0a98eff'), interface: getColor('interface', ['entity.name.type.interface'], '#e0a98eff'), + struct: getColor('struct', ['entity.name.type.struct'], '#e0a98eff'), + enum: getColor('enum', ['entity.name.type.enum'], '#e0a98eff'), + typeParameter: getColor('typeParameter', ['entity.name.type.parameter'], '#e0a98eff'), + namespace: getColor('namespace', ['entity.name.namespace'], '#D0A215'), + module: getColor('module', ['entity.name.module'], '#D14D41'), tag: getColor('function', ['entity.name.tag'], '#4385BE'), + jsxTag: getColor('function', ['entity.name.tag'], '#CE5D97'), + tagAttribute: getColor('attribute', ['entity.other.attribute-name'], '#D0A215'), + tagAttributeValue: getColor('string', ['string'], '#3AA99F'), boolean: getColor('enumMember', ['constant.language.boolean'], '#e2ad4a'), + decorator: getColor('decorator', ['meta.decorator'], '#D0A215'), + label: getColor('label', ['entity.name.label'], '#CE5D97'), + punctuation: getColor('punctuation', ['punctuation'], '#878580'), + macro: getColor('macro', ['entity.name.function.preprocessor'], '#4385BE'), + preprocessor: getColor('preprocessor', ['keyword.control.directive'], '#CE5D97'), + regex: getColor('string', ['string.regexp'], '#3AA99F'), url: getColor('string', ['string.other.link'], '#4385BE'), - key: getColor('property', ['meta.object-literal.key'], '#DA702C') + key: getColor('property', ['meta.object-literal.key'], '#DA702C'), + exception: getColor('exception', ['entity.name.exception'], '#CE5D97') }, highlights: { diffAdded: colors['gitDecoration.addedResourceForeground'] || '#879A39', diffAddedBackground: (colors['gitDecoration.addedResourceForeground'] || '#879A39') + '20', diffRemoved: colors['gitDecoration.deletedResourceForeground'] || '#D14D41', diffRemovedBackground: (colors['gitDecoration.deletedResourceForeground'] || '#D14D41') + '20', + diffModified: colors['editorInfo.foreground'] || '#4385BE', + diffModifiedBackground: (colors['editorInfo.foreground'] || '#4385BE') + '20', lineNumber: colors['editorLineNumber.foreground'] || '#878580', lineNumberActive: colors['editorLineNumber.activeForeground'] || editorFg } @@ -432,7 +581,7 @@ function registerThemeInPresets(themeId) { } const arrayEndIndex = lines.findIndex( - (line, index) => index > arrayStartIndex && /^\s*\];\s*$/.test(line), + (line, index) => index > arrayStartIndex && /^\s*\](?:\.map\(|;)/.test(line), ); if (arrayEndIndex === -1) { throw new Error('Unable to find presetThemes array end (];) in presets.ts');