mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
feat: add accent color control and launcher for Open Design (#683)
* feat: add accent color control and launcher for Open Design * fix: remove launcher binary from PR * test: cover accent appearance edge cases --------- Co-authored-by: ferasbusiness666 <ferasbusiness666@users.noreply.github.com>
This commit is contained in:
parent
9af288652c
commit
576dfed9e1
23 changed files with 371 additions and 21 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ out/
|
|||
.tmp/
|
||||
.DS_Store
|
||||
*.log
|
||||
*.exe
|
||||
.vite
|
||||
.astro/
|
||||
.vscode
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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) を参照。
|
||||
|
|
|
|||
|
|
@ -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`를 실행하세요.
|
||||
|
||||
첫 번째 로드 시:
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)。
|
||||
|
|
|
|||
|
|
@ -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)。
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* `<html>` 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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<SettingsSection>(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 (
|
||||
<section className="settings-section">
|
||||
|
|
@ -2044,6 +2062,33 @@ function AppearanceSection({
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="field">
|
||||
<span className="field-label">Accent color</span>
|
||||
<div className="pet-swatches" role="radiogroup" aria-label="Accent color">
|
||||
{ACCENT_SWATCHES.map((color) => {
|
||||
const active = currentAccent === color;
|
||||
return (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={`pet-swatch${active ? ' active' : ''}`}
|
||||
style={{ background: color }}
|
||||
aria-label={color === DEFAULT_ACCENT_COLOR ? 'Default accent color' : color}
|
||||
aria-checked={active}
|
||||
role="radio"
|
||||
onClick={() => setAccentColor(color === DEFAULT_ACCENT_COLOR ? undefined : color)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<input
|
||||
type="color"
|
||||
aria-label="Custom accent color"
|
||||
className="pet-swatch-picker"
|
||||
value={currentAccent}
|
||||
onChange={(e) => setAccentColor(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
52
apps/web/src/state/appearance.ts
Normal file
52
apps/web/src/state/appearance.ts
Normal file
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
73
apps/web/tests/state/appearance.test.ts
Normal file
73
apps/web/tests/state/appearance.test.ts
Normal file
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
|
|
@ -237,6 +237,28 @@ describe('loadConfig', () => {
|
|||
expect(config.apiProtocol).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('preserves a valid saved accent color', () => {
|
||||
const savedConfig: Partial<AppConfig> = {
|
||||
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<AppConfig> = {
|
||||
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');
|
||||
|
||||
|
|
|
|||
95
tools/launcher/OpenDesignLauncher.cs
Normal file
95
tools/launcher/OpenDesignLauncher.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
tools/launcher/README.md
Normal file
34
tools/launcher/README.md
Normal file
|
|
@ -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
|
||||
```
|
||||
Loading…
Reference in a new issue