diff --git a/.gitignore b/.gitignore index 632942a24..9fe896f44 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out/ .tmp/ .DS_Store *.log +*.exe .vite .astro/ .vscode diff --git a/README.ar.md b/README.ar.md index 23affa90e..17960b823 100644 --- a/README.ar.md +++ b/README.ar.md @@ -322,6 +322,8 @@ pnpm tools-dev run web # open the web URL printed by tools-dev ``` +مشغّل Windows: ابنِ `OpenDesign.exe` بنفسك باتباع التعليمات في `tools/launcher/README.md`، أو نزّله من GitHub Releases. بعد ذلك ضعه في جذر المستودع وانقر عليه مرتين ليشغّل `pnpm install` عند الحاجة ثم يبدأ Open Design عبر `pnpm tools-dev`. + متطلّبات البيئة: Node `~24` و pnpm `10.33.x`. أدوات `nvm`/`fnm` اختيارية فقط؛ إن استخدمت إحداها فشغّل `nvm install 24 && nvm use 24` أو `fnm install 24 && fnm use 24` قبل `pnpm install`. لتشغيل سطح المكتب / الخلفية، إعادة التشغيل بمنافذ ثابتة، وفحوص dispatcher توليد الوسائط (`OD_BIN`، `OD_DAEMON_URL`، `apps/daemon/dist/cli.js`) راجع [`QUICKSTART.md`](QUICKSTART.md). diff --git a/README.de.md b/README.de.md index 5a444f87a..6f8125e02 100644 --- a/README.de.md +++ b/README.de.md @@ -319,6 +319,8 @@ pnpm tools-dev run web # open the web URL printed by tools-dev ``` +Windows-Launcher: Erstellen Sie `OpenDesign.exe` selbst mit der Anleitung in `tools/launcher/README.md`, oder laden Sie ihn aus GitHub Releases herunter. Legen Sie die Datei danach in den Repository-Stamm und doppelklicken Sie sie, um bei Bedarf `pnpm install` auszuführen und Open Design mit `pnpm tools-dev` zu starten. + Umgebungsanforderungen: Node `~24` und pnpm `10.33.x`. `nvm`/`fnm` sind nur optionale Helfer; wenn Sie eines davon nutzen, führen Sie vor `pnpm install` `nvm install 24 && nvm use 24` oder `fnm install 24 && fnm use 24` aus. Für Desktop-/Background-Start, Fixed-Port-Restarts und Media-Generation-Dispatcher-Checks (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`) siehe [`QUICKSTART.de.md`](QUICKSTART.de.md). diff --git a/README.es.md b/README.es.md index 11385f597..0cc613866 100644 --- a/README.es.md +++ b/README.es.md @@ -310,6 +310,8 @@ pnpm tools-dev run web # open the web URL printed by tools-dev ``` +Lanzador de Windows: compila `OpenDesign.exe` con las instrucciones de `tools/launcher/README.md` o descárgalo desde GitHub Releases. Después colócalo en la raíz del repo y haz doble clic para ejecutar `pnpm install` si hace falta e iniciar Open Design con `pnpm tools-dev`. + Requisitos de entorno: Node `~24` y pnpm `10.33.x`. `nvm`/`fnm` son helpers opcionales; si usas uno, ejecuta `nvm install 24 && nvm use 24` o `fnm install 24 && fnm use 24` antes de `pnpm install`. Para arranque desktop/background, reinicios con puerto fijo y checks del dispatcher de media generation (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`), consulta [`QUICKSTART.md`](QUICKSTART.md). diff --git a/README.fr.md b/README.fr.md index cd6c94bdb..d44c3d2e3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -320,6 +320,8 @@ pnpm tools-dev run web # open the web URL printed by tools-dev ``` +Lanceur Windows : compilez `OpenDesign.exe` avec les instructions de `tools/launcher/README.md`, ou téléchargez-le depuis GitHub Releases. Placez-le ensuite à la racine du dépôt et double-cliquez dessus pour lancer `pnpm install` si nécessaire, puis démarrer Open Design avec `pnpm tools-dev`. + Prérequis : Node `~24` et pnpm `10.33.x`. `nvm` / `fnm` ne sont que des aides facultatives ; si vous en utilisez un, lancez `nvm install 24 && nvm use 24` ou `fnm install 24 && fnm use 24` avant `pnpm install`. Pour le démarrage desktop/background, les redémarrages sur ports fixes et les checks du dispatcher de génération média (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`), voir [`QUICKSTART.fr.md`](QUICKSTART.fr.md). diff --git a/README.ja-JP.md b/README.ja-JP.md index d87db80b7..6e9c6c0eb 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -320,6 +320,8 @@ pnpm tools-dev run web # tools-dev が出力した Web URL を開く ``` +Windows ランチャー: `tools/launcher/README.md` の手順で `OpenDesign.exe` を自分でビルドするか、GitHub Releases からダウンロードします。その後、リポジトリのルートに置いてダブルクリックすると、必要に応じて `pnpm install` を実行し、`pnpm tools-dev` で Open Design を起動します。 + 環境要件:Node `~24`、pnpm `10.33.x`。`nvm` / `fnm` はあくまでオプションのヘルパーです。使用する場合は `pnpm install` の前に `nvm install 24 && nvm use 24` または `fnm install 24 && fnm use 24` を実行してください。 デスクトップ / バックグラウンド起動、固定ポート再起動、メディア生成ディスパッチャの確認(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)は [`QUICKSTART.ja-JP.md`](QUICKSTART.ja-JP.md) を参照。 diff --git a/README.ko.md b/README.ko.md index f75823249..3226cb9e7 100644 --- a/README.ko.md +++ b/README.ko.md @@ -319,6 +319,8 @@ pnpm tools-dev run web # tools-dev가 출력한 web URL을 여세요 ``` +Windows 런처: `tools/launcher/README.md`의 안내에 따라 `OpenDesign.exe`를 직접 빌드하거나 GitHub Releases에서 다운로드하세요. 그런 다음 저장소 루트에 두고 두 번 클릭하면 필요할 때 `pnpm install`을 실행한 뒤 `pnpm tools-dev`로 Open Design을 시작합니다. + 환경 요구사항: Node `~24`와 pnpm `10.33.x`. `nvm` / `fnm`은 선택적 보조 도구일 뿐입니다; 사용한다면 `pnpm install` 전에 `nvm install 24 && nvm use 24` 또는 `fnm install 24 && fnm use 24`를 실행하세요. 첫 번째 로드 시: diff --git a/README.md b/README.md index 2949cfed7..faf23d0a6 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,8 @@ pnpm tools-dev run web # open the web URL printed by tools-dev ``` +Windows launcher: build `OpenDesign.exe` yourself with the instructions in `tools/launcher/README.md`, or download it from GitHub Releases. Then place it in the repo root and double-click it to run `pnpm install` if needed and start Open Design with `pnpm tools-dev`. + Environment requirements: Node `~24` and pnpm `10.33.x`. `nvm`/`fnm` are optional helpers only; if you use one, run `nvm install 24 && nvm use 24` or `fnm install 24 && fnm use 24` before `pnpm install`. For desktop/background startup, fixed-port restarts, and media generation dispatcher checks (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`), see [`QUICKSTART.md`](QUICKSTART.md). diff --git a/README.pt-BR.md b/README.pt-BR.md index 3ea114ca6..e109be71e 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -320,6 +320,8 @@ pnpm tools-dev run web # open the web URL printed by tools-dev ``` +Inicializador do Windows: compile `OpenDesign.exe` com as instruções em `tools/launcher/README.md` ou baixe-o pelo GitHub Releases. Depois coloque-o na raiz do repo e dê dois cliques para executar `pnpm install` se necessário e iniciar o Open Design com `pnpm tools-dev`. + Requisitos de ambiente: Node `~24` e pnpm `10.33.x`. `nvm`/`fnm` são apenas helpers opcionais; se você usa um, rode `nvm install 24 && nvm use 24` ou `fnm install 24 && fnm use 24` antes do `pnpm install`. Para startup desktop/background, restart com porta fixa e checagens do dispatcher de geração de mídia (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`), veja [`QUICKSTART.pt-BR.md`](QUICKSTART.pt-BR.md). diff --git a/README.ru.md b/README.ru.md index 0e70c59ce..85c5469c6 100644 --- a/README.ru.md +++ b/README.ru.md @@ -320,6 +320,8 @@ pnpm tools-dev run web # откройте web URL, который напечатает tools-dev ``` +Лаунчер Windows: соберите `OpenDesign.exe` самостоятельно по инструкции в `tools/launcher/README.md` или скачайте его из GitHub Releases. Затем поместите файл в корень репозитория и дважды щёлкните его, чтобы при необходимости выполнить `pnpm install` и запустить Open Design через `pnpm tools-dev`. + Требования к окружению: Node `~24` и pnpm `10.33.x`. `nvm`/`fnm` — только вспомогательные инструменты; если вы ими пользуетесь, выполните `nvm install 24 && nvm use 24` или `fnm install 24 && fnm use 24` перед `pnpm install`. Для desktop/background startup, перезапуска на фиксированных портах и проверки dispatcher’а media generation (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`) смотрите [`QUICKSTART.md`](QUICKSTART.md). diff --git a/README.uk.md b/README.uk.md index 8993de9b2..62c090c8a 100644 --- a/README.uk.md +++ b/README.uk.md @@ -320,6 +320,8 @@ pnpm tools-dev run web # відкрийте URL у браузері, який виведе tools-dev ``` +Лаунчер Windows: зберіть `OpenDesign.exe` самостійно за інструкцією в `tools/launcher/README.md` або завантажте його з GitHub Releases. Потім покладіть файл у корінь репозиторію й двічі клацніть його, щоб за потреби виконати `pnpm install` і запустити Open Design через `pnpm tools-dev`. + Вимоги до середовища: Node `~24` та pnpm `10.33.x`. `nvm`/`fnm` є лише додатковими помічниками; якщо ви використовуєте один з них, запустіть `nvm install 24 && nvm use 24` або `fnm install 24 && fnm use 24` перед `pnpm install`. Для запуску desktop/background, перезапусків з фіксованими портами та перевірок диспетчера генерації медіа (`OD_BIN`, `OD_DAEMON_URL`, `apps/daemon/dist/cli.js`), див. [`QUICKSTART.md`](QUICKSTART.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 4dcc7a4f8..1b06cd714 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -319,6 +319,8 @@ pnpm tools-dev run web # 打开 tools-dev 输出的 web URL ``` +Windows 启动器:请按照 `tools/launcher/README.md` 中的说明自行构建 `OpenDesign.exe`,或从 GitHub Releases 下载。然后将它放到仓库根目录并双击;它会在需要时运行 `pnpm install`,再用 `pnpm tools-dev` 启动 Open Design。 + 环境要求:Node `~24`,pnpm `10.33.x`。`nvm` / `fnm` 只是可选辅助工具,不是项目必需步骤;如果使用它们,先执行 `nvm install 24 && nvm use 24` 或 `fnm install 24 && fnm use 24`,再运行 `pnpm install`。 桌面端/后台启动、固定端口重启,以及 media 生成派发器检查(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)见 [`QUICKSTART.zh-CN.md`](QUICKSTART.zh-CN.md)。 diff --git a/README.zh-TW.md b/README.zh-TW.md index 9718591d3..e6291eca3 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -317,6 +317,8 @@ pnpm tools-dev run web # 開啟 tools-dev 輸出的 web URL ``` +Windows 啟動器:請依照 `tools/launcher/README.md` 的說明自行建置 `OpenDesign.exe`,或從 GitHub Releases 下載。接著將它放到 repo 根目錄並雙擊;它會在需要時執行 `pnpm install`,再用 `pnpm tools-dev` 啟動 Open Design。 + 環境要求:Node `~24`,pnpm `10.33.x`。`nvm` / `fnm` 只是可選輔助工具,不是專案必需步驟;如果使用它們,先執行 `nvm install 24 && nvm use 24` 或 `fnm install 24 && fnm use 24`,再執行 `pnpm install`。 桌面版/後臺啟動、固定埠重啟,以及 media 生成派發器檢查(`OD_BIN`、`OD_DAEMON_URL`、`apps/daemon/dist/cli.js`)見 [`QUICKSTART.md`](QUICKSTART.md)。 diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index a90aa6e4a..57250a2fe 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -23,8 +23,10 @@ export const viewport: Viewport = { * preference without a flash of unstyled content. It reads the same * localStorage key used by `state/config.ts` and sets `data-theme` on * `` immediately — before any CSS or React paint. + * Keep the accent variable mix ratios in sync with `accentVars()` in + * `src/state/appearance.ts`; this script cannot import application modules. */ -const themeInitScript = `(function(){try{var t=JSON.parse(localStorage.getItem('open-design:config')||'{}').theme;if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();`; +const themeInitScript = `(function(){try{var c=JSON.parse(localStorage.getItem('open-design:config')||'{}');var t=c.theme;if(t==='light'||t==='dark')document.documentElement.setAttribute('data-theme',t);var a=typeof c.accentColor==='string'&&/^#[0-9a-fA-F]{6}$/.test(c.accentColor.trim())?c.accentColor.trim().toLowerCase():'';if(a){var s=document.documentElement.style;s.setProperty('--accent',a);s.setProperty('--accent-strong','color-mix(in srgb, '+a+' 86%, var(--text-strong))');s.setProperty('--accent-soft','color-mix(in srgb, '+a+' 22%, var(--bg-panel))');s.setProperty('--accent-tint','color-mix(in srgb, '+a+' 12%, var(--bg-panel))');s.setProperty('--accent-hover','color-mix(in srgb, '+a+' 90%, var(--text-strong))');}}catch(e){}})();`; export default function RootLayout({ children }: { children: ReactNode }) { return ( diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 60663d5af..1a73a883d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -29,6 +29,7 @@ import { syncConfigToDaemon, syncMediaProvidersToDaemon, } from './state/config'; +import { applyAppearanceToDocument } from './state/appearance'; import { createProject, deleteProject as deleteProjectApi, @@ -90,13 +91,11 @@ export function App() { // live theme switch in Settings applies atomically — no 1-frame flash of // the old theme. Safe here because the component tree is ssr:false. useLayoutEffect(() => { - const theme = config.theme ?? 'system'; - if (theme === 'system') { - document.documentElement.removeAttribute('data-theme'); - } else { - document.documentElement.setAttribute('data-theme', theme); - } - }, [config.theme]); + applyAppearanceToDocument({ + theme: config.theme ?? 'system', + accentColor: config.accentColor, + }); + }, [config.theme, config.accentColor]); // Tell the daemon what the user is currently looking at, so the MCP // server can surface it as `get_active_context` to a coding agent in diff --git a/apps/web/src/components/SettingsDialog.tsx b/apps/web/src/components/SettingsDialog.tsx index 018ae6586..1239074e2 100644 --- a/apps/web/src/components/SettingsDialog.tsx +++ b/apps/web/src/components/SettingsDialog.tsx @@ -21,6 +21,10 @@ import { MEDIA_PROVIDERS } from '../media/models'; import type { MediaProvider } from '../media/models'; import { PetSettings } from './pet/PetSettings'; import { LibrarySection } from './LibrarySection'; +import { + applyAppearanceToDocument, + normalizeAccentColor, +} from '../state/appearance'; import { FAILURE_SOUNDS, SUCCESS_SOUNDS, @@ -372,15 +376,13 @@ export function SettingsDialog({ // On Save, App's useLayoutEffect fires after unmount and applies the new // saved theme, so this cleanup is effectively a no-op in that path. useLayoutEffect(() => { - const saved = initial.theme ?? 'system'; return () => { - if (saved === 'system') { - document.documentElement.removeAttribute('data-theme'); - } else { - document.documentElement.setAttribute('data-theme', saved); - } + applyAppearanceToDocument({ + theme: initial.theme ?? 'system', + accentColor: initial.accentColor, + }); }; - }, [initial.theme]); + }, [initial.theme, initial.accentColor]); const [showApiKey, setShowApiKey] = useState(false); const [languageOpen, setLanguageOpen] = useState(false); const [activeSection, setActiveSection] = useState(initialSection); @@ -2003,6 +2005,18 @@ const THEMES: Array<{ value: AppTheme; labelKey: 'settings.themeSystem' | 'setti { value: 'dark', labelKey: 'settings.themeDark' }, ]; +const DEFAULT_ACCENT_COLOR = '#c96442'; +const ACCENT_SWATCHES = [ + DEFAULT_ACCENT_COLOR, + '#2563eb', + '#7c3aed', + '#059669', + '#dc2626', + '#d97706', + '#0891b2', + '#db2777', +] as const; + function AppearanceSection({ cfg, setCfg, @@ -2012,16 +2026,20 @@ function AppearanceSection({ }) { const { t } = useI18n(); const current = cfg.theme ?? 'system'; + const currentAccent = normalizeAccentColor(cfg.accentColor) ?? DEFAULT_ACCENT_COLOR; // Apply the draft theme immediately so the user sees a live preview // before hitting Save. SettingsDialog's cleanup reverts this on cancel. useLayoutEffect(() => { - if (current === 'system') { - document.documentElement.removeAttribute('data-theme'); - } else { - document.documentElement.setAttribute('data-theme', current); - } - }, [current]); + applyAppearanceToDocument({ + theme: current, + accentColor: cfg.accentColor, + }); + }, [current, cfg.accentColor]); + + const setAccentColor = (color: string | undefined) => { + setCfg((c) => ({ ...c, accentColor: color ? normalizeAccentColor(color) ?? c.accentColor : undefined })); + }; return (
@@ -2044,6 +2062,33 @@ function AppearanceSection({ ))} +
+ Accent color +
+ {ACCENT_SWATCHES.map((color) => { + const active = currentAccent === color; + return ( +
+
); } diff --git a/apps/web/src/state/appearance.ts b/apps/web/src/state/appearance.ts new file mode 100644 index 000000000..965d3d0f3 --- /dev/null +++ b/apps/web/src/state/appearance.ts @@ -0,0 +1,52 @@ +import type { AppTheme } from '../types'; + +const ACCENT_VARS = [ + '--accent', + '--accent-strong', + '--accent-soft', + '--accent-tint', + '--accent-hover', +] as const; + +export function normalizeAccentColor(value: unknown): string | null { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return /^#[0-9a-fA-F]{6}$/.test(trimmed) ? trimmed.toLowerCase() : null; +} + +function accentVars(accentColor: string): Record<(typeof ACCENT_VARS)[number], string> { + return { + '--accent': accentColor, + // Keep these mix ratios in sync with the pre-hydration script in app/layout.tsx. + '--accent-strong': `color-mix(in srgb, ${accentColor} 86%, var(--text-strong))`, + '--accent-soft': `color-mix(in srgb, ${accentColor} 22%, var(--bg-panel))`, + '--accent-tint': `color-mix(in srgb, ${accentColor} 12%, var(--bg-panel))`, + '--accent-hover': `color-mix(in srgb, ${accentColor} 90%, var(--text-strong))`, + }; +} + +export function applyAppearanceToDocument({ + theme, + accentColor, +}: { + theme?: AppTheme; + accentColor?: string; +}): void { + const root = document.documentElement; + if (theme === 'light' || theme === 'dark') { + root.setAttribute('data-theme', theme); + } else { + root.removeAttribute('data-theme'); + } + + const normalized = normalizeAccentColor(accentColor); + if (!normalized) { + for (const name of ACCENT_VARS) root.style.removeProperty(name); + return; + } + + const vars = accentVars(normalized); + for (const name of ACCENT_VARS) { + root.style.setProperty(name, vars[name]); + } +} diff --git a/apps/web/src/state/config.ts b/apps/web/src/state/config.ts index 0e5e59d84..85e1657ce 100644 --- a/apps/web/src/state/config.ts +++ b/apps/web/src/state/config.ts @@ -7,6 +7,7 @@ import type { NotificationsConfig, PetConfig, } from '../types'; +import { normalizeAccentColor } from './appearance'; import { DEFAULT_FAILURE_SOUND_ID, DEFAULT_SUCCESS_SOUND_ID, @@ -237,6 +238,7 @@ export function loadConfig(): AppConfig { composio: { ...(parsed.composio ?? {}) }, agentModels: { ...(parsed.agentModels ?? {}) }, agentCliEnv: { ...(parsed.agentCliEnv ?? {}) }, + accentColor: normalizeAccentColor(parsed.accentColor) ?? DEFAULT_CONFIG.accentColor, pet: normalizePet(parsed.pet), notifications: normalizeNotifications(parsed.notifications), }; diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index f1b8d475d..3481f41d1 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -251,6 +251,7 @@ export interface AppConfig { skillId: string | null; designSystemId: string | null; theme?: AppTheme; + accentColor?: string; // True once the user has been through the welcome onboarding modal at // least once (saved or skipped). Bootstrap skips the auto-popup when // this is set so refreshing the page doesn't re-prompt. diff --git a/apps/web/tests/state/appearance.test.ts b/apps/web/tests/state/appearance.test.ts new file mode 100644 index 000000000..280773cc3 --- /dev/null +++ b/apps/web/tests/state/appearance.test.ts @@ -0,0 +1,73 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, it } from 'vitest'; +import { + applyAppearanceToDocument, + normalizeAccentColor, +} from '../../src/state/appearance'; + +describe('normalizeAccentColor', () => { + it('accepts six-digit hex colors and normalizes casing', () => { + expect(normalizeAccentColor(' #4F46E5 ')).toBe('#4f46e5'); + }); + + it('rejects invalid accent colors', () => { + expect(normalizeAccentColor('blue')).toBeNull(); + expect(normalizeAccentColor('#123')).toBeNull(); + expect(normalizeAccentColor('#12345g')).toBeNull(); + }); +}); + +describe('applyAppearanceToDocument', () => { + afterEach(() => { + document.documentElement.removeAttribute('data-theme'); + document.documentElement.style.removeProperty('--accent'); + document.documentElement.style.removeProperty('--accent-strong'); + document.documentElement.style.removeProperty('--accent-soft'); + document.documentElement.style.removeProperty('--accent-tint'); + document.documentElement.style.removeProperty('--accent-hover'); + }); + + it('applies the saved theme and accent variables to the root element', () => { + applyAppearanceToDocument({ theme: 'dark', accentColor: '#4F46E5' }); + + expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); + expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#4f46e5'); + expect(document.documentElement.style.getPropertyValue('--accent-hover')).toContain('#4f46e5'); + }); + + it('applies accent variables while clearing an explicit theme for system mode', () => { + document.documentElement.setAttribute('data-theme', 'dark'); + + applyAppearanceToDocument({ theme: 'system', accentColor: '#10B981' }); + + expect(document.documentElement.hasAttribute('data-theme')).toBe(false); + expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#10b981'); + expect(document.documentElement.style.getPropertyValue('--accent-strong')).toContain('#10b981'); + expect(document.documentElement.style.getPropertyValue('--accent-soft')).toContain('#10b981'); + expect(document.documentElement.style.getPropertyValue('--accent-tint')).toContain('#10b981'); + expect(document.documentElement.style.getPropertyValue('--accent-hover')).toContain('#10b981'); + }); + + it('replaces existing accent variables when the saved color changes', () => { + applyAppearanceToDocument({ theme: 'light', accentColor: '#4F46E5' }); + + applyAppearanceToDocument({ theme: 'light', accentColor: '#EF4444' }); + + expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#ef4444'); + expect(document.documentElement.style.getPropertyValue('--accent-strong')).toContain('#ef4444'); + expect(document.documentElement.style.getPropertyValue('--accent-strong')).not.toContain('#4f46e5'); + expect(document.documentElement.style.getPropertyValue('--accent-soft')).toContain('#ef4444'); + expect(document.documentElement.style.getPropertyValue('--accent-tint')).toContain('#ef4444'); + expect(document.documentElement.style.getPropertyValue('--accent-hover')).toContain('#ef4444'); + }); + + it('clears accent overrides when no valid accent is configured', () => { + document.documentElement.style.setProperty('--accent', '#4f46e5'); + + applyAppearanceToDocument({ theme: 'system', accentColor: 'not-a-color' }); + + expect(document.documentElement.hasAttribute('data-theme')).toBe(false); + expect(document.documentElement.style.getPropertyValue('--accent')).toBe(''); + }); +}); diff --git a/apps/web/tests/state/config.test.ts b/apps/web/tests/state/config.test.ts index af59acb1d..b130c361d 100644 --- a/apps/web/tests/state/config.test.ts +++ b/apps/web/tests/state/config.test.ts @@ -237,6 +237,28 @@ describe('loadConfig', () => { expect(config.apiProtocol).toBe('anthropic'); }); + it('preserves a valid saved accent color', () => { + const savedConfig: Partial = { + theme: 'dark', + accentColor: '#4F46E5', + }; + store.set('open-design:config', JSON.stringify(savedConfig)); + + const config = loadConfig(); + + expect(config.theme).toBe('dark'); + expect(config.accentColor).toBe('#4f46e5'); + }); + + it('falls back to the default accent color for malformed saved colors', () => { + const savedConfig: Partial = { + accentColor: 'blue', + }; + store.set('open-design:config', JSON.stringify(savedConfig)); + + expect(loadConfig().accentColor).toBe(DEFAULT_CONFIG.accentColor); + }); + it('returns defaults for malformed localStorage JSON', () => { store.set('open-design:config', '{broken-json'); diff --git a/tools/launcher/OpenDesignLauncher.cs b/tools/launcher/OpenDesignLauncher.cs new file mode 100644 index 000000000..9a057091c --- /dev/null +++ b/tools/launcher/OpenDesignLauncher.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace OpenDesignLauncher +{ + internal static class Program + { + private static int Main() + { + Console.Title = "Open Design Launcher"; + Console.WriteLine("Open Design launcher"); + Console.WriteLine(); + + string repoRoot = FindRepoRoot(AppDomain.CurrentDomain.BaseDirectory); + if (repoRoot == null) + { + Console.Error.WriteLine("Could not find the Open Design repository root."); + Console.Error.WriteLine("Place OpenDesign.exe in the repository root next to package.json."); + Pause(); + return 1; + } + + Console.WriteLine("Repository: " + repoRoot); + + if (!Directory.Exists(Path.Combine(repoRoot, "node_modules", ".pnpm"))) + { + Console.WriteLine("Dependencies are missing. Running pnpm install first..."); + // Requires corepack (bundled with Node 16.9+) so future maintainers know the dependency. + int installExit = RunCommand(repoRoot, "corepack pnpm install"); + if (installExit != 0) + { + Console.Error.WriteLine("pnpm install failed with exit code " + installExit + "."); + Pause(); + return installExit; + } + } + + Console.WriteLine("Starting Open Design with pnpm tools-dev..."); + Console.WriteLine(); + int exitCode = RunCommand(repoRoot, "corepack pnpm tools-dev"); + if (exitCode != 0) + { + Console.Error.WriteLine(); + Console.Error.WriteLine("Open Design exited with code " + exitCode + "."); + Pause(); + } + else + { + Console.WriteLine(); + Console.WriteLine("Open Design command completed. Press any key to close this window."); + Console.ReadKey(true); + } + + return exitCode; + } + + private static string FindRepoRoot(string startDirectory) + { + DirectoryInfo current = new DirectoryInfo(startDirectory); + while (current != null) + { + string packageJson = Path.Combine(current.FullName, "package.json"); + string workspace = Path.Combine(current.FullName, "pnpm-workspace.yaml"); + if (File.Exists(packageJson) && File.Exists(workspace)) + { + return current.FullName; + } + current = current.Parent; + } + return null; + } + + private static int RunCommand(string workingDirectory, string command) + { + ProcessStartInfo info = new ProcessStartInfo(); + info.FileName = "cmd.exe"; + info.Arguments = "/d /c \"" + command + "\""; + info.WorkingDirectory = workingDirectory; + info.UseShellExecute = false; + + using (Process process = Process.Start(info)) + { + process.WaitForExit(); + return process.ExitCode; + } + } + + private static void Pause() + { + Console.WriteLine("Press any key to close this window."); + Console.ReadKey(true); + } + } +} diff --git a/tools/launcher/README.md b/tools/launcher/README.md new file mode 100644 index 000000000..3b3a86158 --- /dev/null +++ b/tools/launcher/README.md @@ -0,0 +1,34 @@ +# Open Design Windows Launcher + +`OpenDesignLauncher.cs` builds a small Windows console executable that starts the +local development app without typing the normal commands by hand. + +The compiled `OpenDesign.exe` is intentionally not committed to git. Build it +locally when you want the shortcut, or download a trusted build from GitHub +Releases if the project publishes one. + +## Build + +From the repository root on Windows: + +```powershell +C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /out:OpenDesign.exe /win32icon:tools\pack\resources\win\icon.ico tools\launcher\OpenDesignLauncher.cs +``` + +If your Windows installation only has the 32-bit .NET Framework compiler, use: + +```powershell +C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:exe /out:OpenDesign.exe /win32icon:tools\pack\resources\win\icon.ico tools\launcher\OpenDesignLauncher.cs +``` + +## Run + +Place the built `OpenDesign.exe` in the repository root next to `package.json`, +then double-click it. + +The launcher checks for dependencies and runs: + +```powershell +corepack pnpm install +corepack pnpm tools-dev +```