feat: theme family system with 12 color palettes + NavRail redesign
Theme system:
- Two-layer architecture: mode (light/dark/system via next-themes) + theme family (color palette via CSS variables)
- 12 theme families: Default, GitHub, Rosé Pine, Tokyo Night, Everforest, Nord, Kanagawa, Night Owl, Poimandres, Vesper, Horizon, SynthWave '84
- Per-family code block highlighting for Prism, HLJS, and Shiki (no more generic oneLight/oneDark fallback)
- Anti-FOUC: synchronous script in <head> reads localStorage and sets data-theme-family before paint
- Theme JSON files in themes/ directory, auto-discovered by server-side loader
- OKLCH color space for all values; CSS specificity html[data-theme-family] beats :root
UI changes:
- NavRail: moved Settings to bottom (replacing theme toggle), removed light/dark toggle button
- Settings: merged General controls into one card with dividers; Appearance section with live Shiki code preview
- Replaced hardcoded blue-500/blue-600 with primary/primary-foreground across 19 components
- Appearance settings: pill mode selector (light/dark/system), theme family dropdown, live code preview
New files:
- src/lib/theme/{types,loader,render-css,context,code-themes}.ts — core theme infrastructure
- src/components/layout/ThemeFamilyProvider.tsx — client context provider
- src/components/settings/AppearanceSection.tsx — appearance settings UI
- src/hooks/useAppTheme.ts — combined theme hook
- themes/*.json — 12 theme family definitions
- docs/handover/theme-system.md — handover documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ad66f82c4
commit
4da188bfe5
53 files changed
+2446
-144
No files matched your search
@@ -11,3 +11,4 @@
|
||||
| agent-tooling-todo-bridge.md | SDK → SSE → DB 事件流、TodoWrite 字段映射、去重策略 |
|
||||
| bridge-system.md | 多 IM 远程桥接系统架构(目录结构、数据流、设计决策) |
|
||||
| assistant-workspace.md | 助理工作区:人格/记忆文件、对话式引导、自动触发、确定性落盘 |
|
||||
| theme-system.md | 主题家族系统:两层架构、JSON schema、代码高亮三条渲染链、12 个主题清单 |
|
||||
@@ -0,0 +1,82 @@
|
||||
# Theme Family System / 主题家族系统
|
||||
|
||||
## 概述
|
||||
|
||||
两层主题系统:`mode`(light/dark/system,由 next-themes 管理)+ `theme family`(色板,由自定义 context + CSS 变量管理)。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
themes/*.json → 定义色板(OKLCH)、代码主题映射
|
||||
src/lib/theme/loader.ts → 服务端读取 + 校验 JSON,按 order 排序
|
||||
src/lib/theme/render-css.ts → 生成 html[data-theme-family="x"] 规则
|
||||
src/app/layout.tsx → <head> 注入反闪烁脚本 + <style> 块
|
||||
src/lib/theme/context.ts + ThemeFamilyProvider → 客户端状态 + localStorage
|
||||
```
|
||||
|
||||
**CSS 特异性**:`html[data-theme-family]` (0,1,0) > `:root` (0,0,1);`html.dark[data-theme-family]` (0,2,0) > `.dark` (0,1,0)。globals.css 中的 `:root` / `.dark` 块作为兜底。
|
||||
|
||||
## 主题 JSON Schema
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"id": "night-owl", // 唯一 ID,对应 data-theme-family 值
|
||||
"label": "Night Owl", // 显示名
|
||||
"order": 7, // 排序权重
|
||||
"description": "...", // 可选短描述
|
||||
"codeTheme": { // Prism / HLJS 代码高亮
|
||||
"light": "coy", // 键名对应 code-themes.ts 中的 PRISM_THEME_MAP / HLJS_THEME_MAP
|
||||
"dark": "nightOwl"
|
||||
},
|
||||
"shikiTheme": { // Shiki 代码高亮(ai-elements)
|
||||
"light": "night-owl-light", // 值为 Shiki BundledTheme 名
|
||||
"dark": "night-owl"
|
||||
},
|
||||
"light": { /* 31 个 ThemeColors 键,oklch 值 */ },
|
||||
"dark": { /* 同上 */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 代码高亮三条渲染链
|
||||
|
||||
| 消费组件 | 高亮器 | 解析函数 |
|
||||
|---------|--------|---------|
|
||||
| ChatCodeBlock (`CodeBlock.tsx`) | Prism | `resolvePrismStyle()` |
|
||||
| DocPreview / FilePreview | HLJS | `resolveHljsStyle()` |
|
||||
| AI Elements CodeBlock (`code-block.tsx`) | Shiki | `resolveShikiThemes()` |
|
||||
| Settings Preview (`AppearanceSection.tsx`) | Shiki | `codeToHtml()` |
|
||||
|
||||
三条链路都通过 `useThemeFamily()` 取得当前 family,再查找对应的 `codeTheme` / `shikiTheme` 映射。
|
||||
|
||||
## 当前主题清单(12 个)
|
||||
|
||||
| ID | Order | Light Code | Dark Code | Shiki Light | Shiki Dark |
|
||||
|----|-------|-----------|----------|------------|-----------|
|
||||
| default | 0 | oneLight | oneDark | github-light | github-dark |
|
||||
| github | 1 | ghcolors | vscDarkPlus | github-light-default | github-dark-default |
|
||||
| rose-pine | 2 | duotoneLight | duotoneSea | rose-pine-dawn | rose-pine |
|
||||
| tokyo-night | 3 | materialLight | materialOceanic | github-light | tokyo-night |
|
||||
| everforest | 4 | gruvboxLight | duotoneForest | everforest-light | everforest-dark |
|
||||
| nord | 5 | coldarkCold | nord | github-light | nord |
|
||||
| kanagawa | 6 | vs | duotoneEarth | kanagawa-lotus | kanagawa-wave |
|
||||
| night-owl | 7 | coy | nightOwl | night-owl-light | night-owl |
|
||||
| poimandres | 8 | coldarkCold | coldarkDark | min-light | poimandres |
|
||||
| vesper | 9 | gruvboxLight | twilight | min-light | vesper |
|
||||
| horizon | 10 | duotoneLight | materialDark | github-light | horizon |
|
||||
| synthwave84 | 11 | prism | synthwave84 | min-light | synthwave-84 |
|
||||
|
||||
## 新增主题
|
||||
|
||||
1. 在 `themes/` 目录创建 JSON 文件,遵循上述 schema
|
||||
2. 填写所有 31 个 ThemeColors 键(light + dark),值用 OKLCH
|
||||
3. `codeTheme` 键名必须在 `code-themes.ts` 的 `PRISM_THEME_MAP` / `HLJS_THEME_MAP` 中存在
|
||||
4. `shikiTheme` 值必须是 Shiki `BundledTheme`
|
||||
5. Loader 自动发现——无需改代码
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
- **不用 Catppuccin / Solarized / Monokai 占位**:已删除早期占位主题,只保留有完整 light+dark 对且在两种模式下都有明显差异的 family
|
||||
- **Rose Pine 合并**:Dawn 是 light 变体,Base 是 dark 变体,Moon 的 light 和 Base 一样 → 只保留一个 rose-pine family
|
||||
- **Tokyo Night 合并**:Storm 和 Base 在 light 模式几乎无差异 → 只保留一个 tokyo-night family,light 使用真实 TN Light 色板
|
||||
- **硬编码蓝色清理**:全局 `blue-*` / `indigo-*` Tailwind 类已替换为 `primary` 语义色,保留语义状态色(green/red/yellow)
|
||||
- **Settings 预览用 Shiki**:与 ai-elements 实际渲染一致,而非 Prism 近似
|
||||
@@ -36,6 +36,9 @@ extraResources:
|
||||
- from: public/
|
||||
to: standalone/public/
|
||||
filter: ["**/*"]
|
||||
- from: themes/
|
||||
to: standalone/themes/
|
||||
filter: ["*.json"]
|
||||
- from: build/
|
||||
to: .
|
||||
filter: ["icon.*"]
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Unit tests for theme family loader — tests the real public functions.
|
||||
*
|
||||
* Run with: npx tsx --test src/__tests__/unit/theme-loader.test.ts
|
||||
*
|
||||
* Strategy: stub process.cwd() to a temp dir and call _resetCache() before
|
||||
* each test to get fresh loader results.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { getAllThemeFamilies, getThemeFamilyMetas, _resetCache } from '../../lib/theme/loader';
|
||||
|
||||
/** Helper to create a minimal valid ThemeColors object. */
|
||||
function makeColors(base: string) {
|
||||
return {
|
||||
background: base, foreground: base, card: base, cardForeground: base,
|
||||
popover: base, popoverForeground: base, primary: base, primaryForeground: base,
|
||||
secondary: base, secondaryForeground: base, muted: base, mutedForeground: base,
|
||||
accent: base, accentForeground: base, destructive: base, border: base,
|
||||
input: base, ring: base, chart1: base, chart2: base, chart3: base,
|
||||
chart4: base, chart5: base, sidebar: base, sidebarForeground: base,
|
||||
sidebarPrimary: base, sidebarPrimaryForeground: base, sidebarAccent: base,
|
||||
sidebarAccentForeground: base, sidebarBorder: base, sidebarRing: base,
|
||||
};
|
||||
}
|
||||
|
||||
function makeTheme(id: string, order: number, color = 'oklch(0.5 0 0)') {
|
||||
return { id, label: id, order, light: makeColors(color), dark: makeColors(color) };
|
||||
}
|
||||
|
||||
let tmpDir: string;
|
||||
const origCwd = process.cwd;
|
||||
|
||||
describe('theme-loader: getAllThemeFamilies', () => {
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'theme-loader-test-'));
|
||||
fs.mkdirSync(path.join(tmpDir, 'themes'));
|
||||
_resetCache();
|
||||
process.cwd = () => tmpDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.cwd = origCwd;
|
||||
_resetCache();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads valid theme files and sorts by order', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'b.json'), JSON.stringify(makeTheme('beta', 2)));
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'a.json'), JSON.stringify(makeTheme('alpha', 1)));
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'default.json'), JSON.stringify(makeTheme('default', 0)));
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
assert.equal(families.length, 3);
|
||||
assert.equal(families[0].id, 'default');
|
||||
assert.equal(families[1].id, 'alpha');
|
||||
assert.equal(families[2].id, 'beta');
|
||||
});
|
||||
|
||||
it('skips invalid JSON files and still loads valid ones', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'good.json'), JSON.stringify(makeTheme('good', 1)));
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'bad.json'), 'not valid json {{{');
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'incomplete.json'), JSON.stringify({ id: 'incomplete', label: 'X' }));
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
const ids = families.map(f => f.id);
|
||||
|
||||
assert.ok(ids.includes('good'), 'valid theme loaded');
|
||||
assert.ok(ids.includes('default'), 'default synthesized');
|
||||
assert.ok(!ids.includes('incomplete'), 'incomplete theme skipped');
|
||||
});
|
||||
|
||||
it('synthesizes default when no default.json exists', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'custom.json'), JSON.stringify(makeTheme('custom', 5)));
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
assert.ok(families.some(f => f.id === 'default'), 'default family exists');
|
||||
assert.equal(families[0].id, 'default', 'default sorts first');
|
||||
assert.equal(families[0].light.primary, 'oklch(0.546 0.245 262.881)', 'uses hardcoded fallback');
|
||||
});
|
||||
|
||||
it('handles missing themes directory gracefully', () => {
|
||||
fs.rmSync(path.join(tmpDir, 'themes'), { recursive: true });
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
assert.equal(families.length, 1);
|
||||
assert.equal(families[0].id, 'default');
|
||||
});
|
||||
|
||||
it('ignores non-JSON files', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'readme.md'), '# Themes');
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'valid.json'), JSON.stringify(makeTheme('valid', 1)));
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
assert.equal(families.length, 2); // valid + synthesized default
|
||||
});
|
||||
|
||||
it('rejects themes missing chart/sidebar color keys', () => {
|
||||
// Create a theme with only the old required keys (no chart*, no sidebar*)
|
||||
const partialColors = {
|
||||
background: 'x', foreground: 'x', card: 'x', cardForeground: 'x',
|
||||
popover: 'x', popoverForeground: 'x', primary: 'x', primaryForeground: 'x',
|
||||
secondary: 'x', secondaryForeground: 'x', muted: 'x', mutedForeground: 'x',
|
||||
accent: 'x', accentForeground: 'x', destructive: 'x', border: 'x',
|
||||
input: 'x', ring: 'x',
|
||||
};
|
||||
const partial = { id: 'partial', label: 'Partial', order: 1, light: partialColors, dark: partialColors };
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'partial.json'), JSON.stringify(partial));
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
assert.ok(!families.some(f => f.id === 'partial'), 'theme missing chart/sidebar keys is rejected');
|
||||
});
|
||||
|
||||
it('strips invalid codeTheme/shikiTheme but keeps the theme', () => {
|
||||
const theme = {
|
||||
...makeTheme('fixable', 1),
|
||||
codeTheme: { light: 123, dark: 'oneDark' }, // light is wrong type
|
||||
shikiTheme: 'not-an-object', // completely wrong
|
||||
};
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'fixable.json'), JSON.stringify(theme));
|
||||
|
||||
const families = getAllThemeFamilies();
|
||||
const loaded = families.find(f => f.id === 'fixable');
|
||||
assert.ok(loaded, 'theme itself is still loaded');
|
||||
assert.equal(loaded!.codeTheme, undefined, 'invalid codeTheme stripped');
|
||||
assert.equal(loaded!.shikiTheme, undefined, 'invalid shikiTheme stripped');
|
||||
});
|
||||
|
||||
it('caches results across calls', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'default.json'), JSON.stringify(makeTheme('default', 0)));
|
||||
|
||||
const first = getAllThemeFamilies();
|
||||
// Add another file after first load
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'extra.json'), JSON.stringify(makeTheme('extra', 1)));
|
||||
const second = getAllThemeFamilies();
|
||||
|
||||
assert.strictEqual(first, second, 'returns same reference from cache');
|
||||
assert.equal(second.length, 1, 'does not pick up new files without cache reset');
|
||||
});
|
||||
});
|
||||
|
||||
describe('theme-loader: getThemeFamilyMetas', () => {
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'theme-meta-test-'));
|
||||
fs.mkdirSync(path.join(tmpDir, 'themes'));
|
||||
_resetCache();
|
||||
process.cwd = () => tmpDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.cwd = origCwd;
|
||||
_resetCache();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns lightweight meta with preview colors and codeTheme', () => {
|
||||
const theme = {
|
||||
...makeTheme('test', 0, 'oklch(0.5 0.1 200)'),
|
||||
description: 'A test theme',
|
||||
codeTheme: { light: 'oneLight', dark: 'oneDark' },
|
||||
shikiTheme: { light: 'one-light', dark: 'one-dark-pro' },
|
||||
};
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'test.json'), JSON.stringify(theme));
|
||||
|
||||
const metas = getThemeFamilyMetas();
|
||||
const testMeta = metas.find(m => m.id === 'test');
|
||||
|
||||
assert.ok(testMeta, 'meta exists');
|
||||
assert.equal(testMeta!.label, 'test');
|
||||
assert.equal(testMeta!.description, 'A test theme');
|
||||
assert.ok(testMeta!.previewColors, 'has preview colors');
|
||||
assert.equal(testMeta!.previewColors!.primaryLight, 'oklch(0.5 0.1 200)');
|
||||
assert.ok(testMeta!.codeTheme, 'has codeTheme');
|
||||
assert.equal(testMeta!.codeTheme!.dark, 'oneDark');
|
||||
assert.ok(testMeta!.shikiTheme, 'has shikiTheme');
|
||||
assert.equal(testMeta!.shikiTheme!.dark, 'one-dark-pro');
|
||||
});
|
||||
|
||||
it('meta does not include full color data', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'themes', 'default.json'), JSON.stringify(makeTheme('default', 0)));
|
||||
|
||||
const metas = getThemeFamilyMetas();
|
||||
const meta = metas[0] as unknown as Record<string, unknown>;
|
||||
|
||||
assert.ok(!('light' in meta), 'no light colors in meta');
|
||||
assert.ok(!('dark' in meta), 'no dark colors in meta');
|
||||
assert.ok(!('order' in meta), 'no order in meta');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Unit tests for theme family CSS renderer.
|
||||
*
|
||||
* Run with: npx tsx --test src/__tests__/unit/theme-render-css.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { colorKeyToCssVar, renderThemeFamilyCSS } from '../../lib/theme/render-css';
|
||||
import type { ThemeFamily } from '../../lib/theme/types';
|
||||
|
||||
describe('colorKeyToCssVar', () => {
|
||||
it('should convert simple keys', () => {
|
||||
assert.equal(colorKeyToCssVar('background'), '--background');
|
||||
assert.equal(colorKeyToCssVar('foreground'), '--foreground');
|
||||
assert.equal(colorKeyToCssVar('primary'), '--primary');
|
||||
});
|
||||
|
||||
it('should convert camelCase to kebab-case', () => {
|
||||
assert.equal(colorKeyToCssVar('cardForeground'), '--card-foreground');
|
||||
assert.equal(colorKeyToCssVar('primaryForeground'), '--primary-foreground');
|
||||
assert.equal(colorKeyToCssVar('mutedForeground'), '--muted-foreground');
|
||||
assert.equal(colorKeyToCssVar('sidebarPrimaryForeground'), '--sidebar-primary-foreground');
|
||||
});
|
||||
|
||||
it('should handle numbered keys', () => {
|
||||
assert.equal(colorKeyToCssVar('chart1'), '--chart-1');
|
||||
assert.equal(colorKeyToCssVar('chart5'), '--chart-5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderThemeFamilyCSS', () => {
|
||||
const makeColors = (val: string) => ({
|
||||
background: val,
|
||||
foreground: val,
|
||||
card: val,
|
||||
cardForeground: val,
|
||||
popover: val,
|
||||
popoverForeground: val,
|
||||
primary: val,
|
||||
primaryForeground: val,
|
||||
secondary: val,
|
||||
secondaryForeground: val,
|
||||
muted: val,
|
||||
mutedForeground: val,
|
||||
accent: val,
|
||||
accentForeground: val,
|
||||
destructive: val,
|
||||
border: val,
|
||||
input: val,
|
||||
ring: val,
|
||||
chart1: val,
|
||||
chart2: val,
|
||||
chart3: val,
|
||||
chart4: val,
|
||||
chart5: val,
|
||||
sidebar: val,
|
||||
sidebarForeground: val,
|
||||
sidebarPrimary: val,
|
||||
sidebarPrimaryForeground: val,
|
||||
sidebarAccent: val,
|
||||
sidebarAccentForeground: val,
|
||||
sidebarBorder: val,
|
||||
sidebarRing: val,
|
||||
});
|
||||
|
||||
const testFamily: ThemeFamily = {
|
||||
id: 'test',
|
||||
label: 'Test',
|
||||
order: 0,
|
||||
light: makeColors('oklch(1 0 0)'),
|
||||
dark: makeColors('oklch(0.1 0 0)'),
|
||||
};
|
||||
|
||||
it('should generate light and dark blocks', () => {
|
||||
const css = renderThemeFamilyCSS([testFamily]);
|
||||
assert.ok(css.includes('html[data-theme-family="test"]'));
|
||||
assert.ok(css.includes('html.dark[data-theme-family="test"]'));
|
||||
});
|
||||
|
||||
it('should include correct CSS variable assignments', () => {
|
||||
const css = renderThemeFamilyCSS([testFamily]);
|
||||
assert.ok(css.includes('--background: oklch(1 0 0);'));
|
||||
assert.ok(css.includes('--background: oklch(0.1 0 0);'));
|
||||
assert.ok(css.includes('--card-foreground:'));
|
||||
assert.ok(css.includes('--sidebar-primary-foreground:'));
|
||||
});
|
||||
|
||||
it('should handle multiple families', () => {
|
||||
const family2: ThemeFamily = {
|
||||
id: 'second',
|
||||
label: 'Second',
|
||||
order: 1,
|
||||
light: makeColors('oklch(0.5 0 0)'),
|
||||
dark: makeColors('oklch(0.2 0 0)'),
|
||||
};
|
||||
const css = renderThemeFamilyCSS([testFamily, family2]);
|
||||
assert.ok(css.includes('html[data-theme-family="test"]'));
|
||||
assert.ok(css.includes('html[data-theme-family="second"]'));
|
||||
assert.ok(css.includes('html.dark[data-theme-family="second"]'));
|
||||
});
|
||||
|
||||
it('should return empty string for empty input', () => {
|
||||
const css = renderThemeFamilyCSS([]);
|
||||
assert.equal(css, '');
|
||||
});
|
||||
});
|
||||
+10
-10
@@ -123,12 +123,12 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
/* Selection color — derives from primary */
|
||||
::selection {
|
||||
background: oklch(0.7 0.15 260 / 30%);
|
||||
background: color-mix(in oklch, var(--primary) 30%, transparent);
|
||||
}
|
||||
.dark ::selection {
|
||||
background: oklch(0.6 0.2 260 / 35%);
|
||||
background: color-mix(in oklch, var(--primary) 35%, transparent);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
@@ -140,17 +140,17 @@
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.7 0 0 / 25%);
|
||||
background: color-mix(in oklch, var(--muted-foreground) 25%, transparent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.6 0 0 / 40%);
|
||||
background: color-mix(in oklch, var(--muted-foreground) 40%, transparent);
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.5 0 0 / 30%);
|
||||
background: color-mix(in oklch, var(--muted-foreground) 30%, transparent);
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.5 0 0 / 50%);
|
||||
background: color-mix(in oklch, var(--muted-foreground) 50%, transparent);
|
||||
}
|
||||
|
||||
/* Prose (markdown) refinements */
|
||||
@@ -179,16 +179,16 @@
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
.prose blockquote {
|
||||
border-left-color: oklch(0.7 0.15 260 / 40%);
|
||||
border-left-color: color-mix(in oklch, var(--primary) 40%, transparent);
|
||||
font-style: normal;
|
||||
}
|
||||
.prose a {
|
||||
color: oklch(0.55 0.2 250);
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.dark .prose a {
|
||||
color: oklch(0.7 0.18 250);
|
||||
color: var(--primary);
|
||||
}
|
||||
.prose hr {
|
||||
border-color: var(--border);
|
||||
|
||||
+18
-3
@@ -2,8 +2,11 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/layout/ThemeProvider";
|
||||
import { ThemeFamilyProvider } from "@/components/layout/ThemeFamilyProvider";
|
||||
import { I18nProvider } from "@/components/layout/I18nProvider";
|
||||
import { AppShell } from "@/components/layout/AppShell";
|
||||
import { getAllThemeFamilies, getThemeFamilyMetas } from "@/lib/theme/loader";
|
||||
import { renderThemeFamilyCSS } from "@/lib/theme/render-css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -25,15 +28,27 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const families = getAllThemeFamilies();
|
||||
const familiesMeta = getThemeFamilyMetas();
|
||||
const themeFamilyCSS = renderThemeFamilyCSS(families);
|
||||
const validIds = families.map((f) => f.id);
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Anti-FOUC: set data-theme-family from localStorage, validate against known IDs */}
|
||||
<script dangerouslySetInnerHTML={{ __html: `(function(){try{var v=${JSON.stringify(validIds)};var f=localStorage.getItem('codepilot_theme_family')||'default';if(v.indexOf(f)<0)f='default';document.documentElement.setAttribute('data-theme-family',f)}catch(e){}})();` }} />
|
||||
<style id="theme-family-vars" dangerouslySetInnerHTML={{ __html: themeFamilyCSS }} />
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
</I18nProvider>
|
||||
<ThemeFamilyProvider families={familiesMeta}>
|
||||
<I18nProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
</I18nProvider>
|
||||
</ThemeFamilyProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useThemeFamily } from "@/lib/theme/context";
|
||||
import { resolveShikiTheme, resolveShikiThemes, SHIKI_DEFAULT_LIGHT, SHIKI_DEFAULT_DARK } from "@/lib/theme/code-themes";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import {
|
||||
createContext,
|
||||
@@ -121,7 +123,7 @@ const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||
code: "",
|
||||
});
|
||||
|
||||
// Highlighter cache (singleton per language)
|
||||
// Highlighter cache keyed by "lang:lightTheme:darkTheme"
|
||||
const highlighterCache = new Map<
|
||||
string,
|
||||
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
|
||||
@@ -133,36 +135,45 @@ const tokensCache = new Map<string, TokenizedCode>();
|
||||
// Subscribers for async token updates
|
||||
const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
|
||||
|
||||
const getTokensCacheKey = (code: string, language: BundledLanguage) => {
|
||||
const getTokensCacheKey = (code: string, language: BundledLanguage, lightTheme: BundledTheme, darkTheme: BundledTheme) => {
|
||||
const start = code.slice(0, 100);
|
||||
const end = code.length > 100 ? code.slice(-100) : "";
|
||||
return `${language}:${code.length}:${start}:${end}`;
|
||||
return `${language}:${lightTheme}:${darkTheme}:${code.length}:${start}:${end}`;
|
||||
};
|
||||
|
||||
const isBundledLanguage = (lang: string): lang is BundledLanguage =>
|
||||
lang in bundledLanguages || lang === "text" || lang === "plaintext";
|
||||
|
||||
const getHighlighter = (
|
||||
language: BundledLanguage
|
||||
language: BundledLanguage,
|
||||
lightTheme: BundledTheme = SHIKI_DEFAULT_LIGHT,
|
||||
darkTheme: BundledTheme = SHIKI_DEFAULT_DARK,
|
||||
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
|
||||
// Normalize unknown languages to "text" before hitting Shiki
|
||||
const safeLang = isBundledLanguage(language) ? language : ("text" as BundledLanguage);
|
||||
const cacheKey = `${safeLang}:${lightTheme}:${darkTheme}`;
|
||||
|
||||
const cached = highlighterCache.get(safeLang);
|
||||
const cached = highlighterCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const highlighterPromise = createHighlighter({
|
||||
langs: [safeLang],
|
||||
themes: ["github-light", "github-dark"],
|
||||
themes: [lightTheme, darkTheme],
|
||||
}).catch(() => {
|
||||
// Language not supported by Shiki — fall back to plain text
|
||||
highlighterCache.delete(safeLang);
|
||||
return getHighlighter("text" as BundledLanguage);
|
||||
// Language or theme not supported — fall back to plain text + default themes.
|
||||
// Using default themes avoids infinite retry if the *theme* was the problem.
|
||||
highlighterCache.delete(cacheKey);
|
||||
const useFallbackThemes =
|
||||
lightTheme !== SHIKI_DEFAULT_LIGHT || darkTheme !== SHIKI_DEFAULT_DARK;
|
||||
if (useFallbackThemes) {
|
||||
return getHighlighter("text" as BundledLanguage, SHIKI_DEFAULT_LIGHT, SHIKI_DEFAULT_DARK);
|
||||
}
|
||||
return getHighlighter("text" as BundledLanguage, lightTheme, darkTheme);
|
||||
});
|
||||
|
||||
highlighterCache.set(safeLang, highlighterPromise);
|
||||
highlighterCache.set(cacheKey, highlighterPromise);
|
||||
return highlighterPromise;
|
||||
};
|
||||
|
||||
@@ -187,9 +198,11 @@ export const highlightCode = (
|
||||
code: string,
|
||||
language: BundledLanguage,
|
||||
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
|
||||
callback?: (result: TokenizedCode) => void
|
||||
callback?: (result: TokenizedCode) => void,
|
||||
lightTheme: BundledTheme = SHIKI_DEFAULT_LIGHT,
|
||||
darkTheme: BundledTheme = SHIKI_DEFAULT_DARK,
|
||||
): TokenizedCode | null => {
|
||||
const tokensCacheKey = getTokensCacheKey(code, language);
|
||||
const tokensCacheKey = getTokensCacheKey(code, language, lightTheme, darkTheme);
|
||||
|
||||
// Return cached result if available
|
||||
const cached = tokensCache.get(tokensCacheKey);
|
||||
@@ -206,7 +219,7 @@ export const highlightCode = (
|
||||
}
|
||||
|
||||
// Start highlighting in background - fire-and-forget async pattern
|
||||
getHighlighter(language)
|
||||
getHighlighter(language, lightTheme, darkTheme)
|
||||
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
|
||||
.then((highlighter) => {
|
||||
const availableLangs = highlighter.getLoadedLanguages();
|
||||
@@ -215,8 +228,8 @@ export const highlightCode = (
|
||||
const result = highlighter.codeToTokens(code, {
|
||||
lang: langToUse,
|
||||
themes: {
|
||||
dark: "github-dark",
|
||||
light: "github-light",
|
||||
dark: darkTheme,
|
||||
light: lightTheme,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -387,6 +400,13 @@ export const CodeBlockActions = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
/** Resolve Shiki theme pair from the current theme family. */
|
||||
function useShikiThemes(): { light: BundledTheme; dark: BundledTheme } {
|
||||
const { family, families } = useThemeFamily();
|
||||
const shikiTheme = resolveShikiTheme(families, family);
|
||||
return resolveShikiThemes(shikiTheme);
|
||||
}
|
||||
|
||||
export const CodeBlockContent = ({
|
||||
code,
|
||||
language,
|
||||
@@ -396,18 +416,20 @@ export const CodeBlockContent = ({
|
||||
language: BundledLanguage;
|
||||
showLineNumbers?: boolean;
|
||||
}) => {
|
||||
const { light: lightTheme, dark: darkTheme } = useShikiThemes();
|
||||
|
||||
// Memoized raw tokens for immediate display
|
||||
const rawTokens = useMemo(() => createRawTokens(code), [code]);
|
||||
|
||||
// Try to get cached result synchronously, otherwise use raw tokens
|
||||
const syncTokenized = useMemo(
|
||||
() => highlightCode(code, language) ?? rawTokens,
|
||||
[code, language, rawTokens]
|
||||
() => highlightCode(code, language, undefined, lightTheme, darkTheme) ?? rawTokens,
|
||||
[code, language, rawTokens, lightTheme, darkTheme]
|
||||
);
|
||||
|
||||
// Track async highlighting results keyed by code+language to avoid stale state
|
||||
// Track async highlighting results keyed by code+language+themes to avoid stale state
|
||||
const [asyncResult, setAsyncResult] = useState<{ key: string; tokens: TokenizedCode } | null>(null);
|
||||
const resultKey = `${code}:${language}`;
|
||||
const resultKey = `${code}:${language}:${lightTheme}:${darkTheme}`;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -415,16 +437,16 @@ export const CodeBlockContent = ({
|
||||
// Subscribe to async highlighting result
|
||||
highlightCode(code, language, (result) => {
|
||||
if (!cancelled) {
|
||||
setAsyncResult({ key: `${code}:${language}`, tokens: result });
|
||||
setAsyncResult({ key: `${code}:${language}:${lightTheme}:${darkTheme}`, tokens: result });
|
||||
}
|
||||
});
|
||||
}, lightTheme, darkTheme);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [code, language]);
|
||||
}, [code, language, lightTheme, darkTheme]);
|
||||
|
||||
// Only use async result if it matches the current code+language
|
||||
// Only use async result if it matches the current code+language+themes
|
||||
const tokenized = (asyncResult && asyncResult.key === resultKey) ? asyncResult.tokens : syncTokenized;
|
||||
|
||||
return (
|
||||
|
||||
@@ -257,7 +257,7 @@ export function BridgeSection() {
|
||||
<div
|
||||
className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${
|
||||
isEnabled
|
||||
? "border-blue-500/50 bg-blue-500/5"
|
||||
? "border-primary/50 bg-primary/5"
|
||||
: "border-border/50"
|
||||
}`}
|
||||
>
|
||||
@@ -275,8 +275,8 @@ export function BridgeSection() {
|
||||
/>
|
||||
</div>
|
||||
{isEnabled && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-blue-500/10 px-3 py-2 text-xs text-blue-600 dark:text-blue-400">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-blue-500" />
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-primary/10 px-3 py-2 text-xs text-primary">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
|
||||
{t("bridge.activeHint")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark, vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { useThemeFamily } from '@/lib/theme/context';
|
||||
import { resolveCodeTheme, resolvePrismStyle } from '@/lib/theme/code-themes';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { IconSvgElement } from "@hugeicons/react";
|
||||
import {
|
||||
@@ -41,6 +44,14 @@ interface CodeBlockProps {
|
||||
maxCollapsedLines?: number;
|
||||
}
|
||||
|
||||
/** Resolve the Prism syntax highlighting style for CodeBlock. */
|
||||
function useCodeBlockTheme(isTerminal: boolean, isDark: boolean) {
|
||||
const { family, families } = useThemeFamily();
|
||||
if (isTerminal) return vscDarkPlus;
|
||||
const codeTheme = resolveCodeTheme(families, family);
|
||||
return resolvePrismStyle(codeTheme, isDark);
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
code,
|
||||
language = 'text',
|
||||
@@ -53,6 +64,8 @@ export function CodeBlock({
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const codeContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [animatingHeight, setAnimatingHeight] = useState<string | undefined>(undefined);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDark = resolvedTheme === 'dark';
|
||||
|
||||
const lines = useMemo(() => code.split('\n'), [code]);
|
||||
const totalLines = lines.length;
|
||||
@@ -93,17 +106,14 @@ export function CodeBlock({
|
||||
}
|
||||
const currentHeight = container.scrollHeight;
|
||||
if (!expanded) {
|
||||
// Expanding: set current height, then switch to auto after transition
|
||||
setAnimatingHeight(`${currentHeight}px`);
|
||||
setExpanded(true);
|
||||
requestAnimationFrame(() => {
|
||||
// Measure the full height after content change
|
||||
const fullHeight = container.scrollHeight;
|
||||
setAnimatingHeight(`${fullHeight}px`);
|
||||
setTimeout(() => setAnimatingHeight(undefined), 300);
|
||||
});
|
||||
} else {
|
||||
// Collapsing: set current height, then reduce
|
||||
setAnimatingHeight(`${currentHeight}px`);
|
||||
requestAnimationFrame(() => {
|
||||
const collapsedH = maxCollapsedLines * 1.5 + 1.5;
|
||||
@@ -118,35 +128,50 @@ export function CodeBlock({
|
||||
|
||||
const languageIconData = getLanguageIcon(language);
|
||||
|
||||
const theme = isTerminal ? vscDarkPlus : oneDark;
|
||||
const theme = useCodeBlockTheme(isTerminal, isDark);
|
||||
|
||||
return (
|
||||
<div className="relative group not-prose my-3 rounded-lg overflow-hidden border border-zinc-700/50">
|
||||
<div className={cn(
|
||||
"relative group not-prose my-3 rounded-lg overflow-hidden",
|
||||
isTerminal
|
||||
? "border border-zinc-700/50"
|
||||
: "border border-border"
|
||||
)}>
|
||||
{/* Header bar */}
|
||||
<div className={cn(
|
||||
"flex items-center justify-between px-4 py-1.5 text-xs",
|
||||
isTerminal
|
||||
? "bg-zinc-950 text-zinc-400"
|
||||
: "bg-zinc-800 dark:bg-zinc-900 text-zinc-400"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<HugeiconsIcon icon={languageIconData} className={cn(
|
||||
"h-3.5 w-3.5 shrink-0",
|
||||
isTerminal ? "text-green-400" : "text-zinc-400",
|
||||
isTerminal ? "text-green-400" : "text-muted-foreground",
|
||||
)} />
|
||||
{filename && (
|
||||
<span className="truncate text-zinc-300 font-medium">{filename}</span>
|
||||
<span className={cn(
|
||||
"truncate font-medium",
|
||||
isTerminal ? "text-zinc-300" : "text-foreground"
|
||||
)}>{filename}</span>
|
||||
)}
|
||||
{filename && <span className="text-zinc-600">|</span>}
|
||||
{filename && <span className="text-muted-foreground/50">|</span>}
|
||||
<span className={cn(
|
||||
"bg-zinc-700/50 rounded px-1.5 py-0.5",
|
||||
isTerminal && "text-green-400",
|
||||
"rounded px-1.5 py-0.5",
|
||||
isTerminal
|
||||
? "bg-zinc-700/50 text-green-400"
|
||||
: "bg-accent text-accent-foreground"
|
||||
)}>{language.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-2 shrink-0">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors"
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors",
|
||||
isTerminal
|
||||
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Copy code"
|
||||
>
|
||||
{copied ? (
|
||||
@@ -163,7 +188,12 @@ export function CodeBlock({
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyMarkdown}
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors"
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors",
|
||||
isTerminal
|
||||
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
title="Copy as Markdown"
|
||||
>
|
||||
{copiedMarkdown ? (
|
||||
@@ -197,7 +227,8 @@ export function CodeBlock({
|
||||
lineNumberStyle={{
|
||||
minWidth: '2.5em',
|
||||
paddingRight: '1em',
|
||||
color: '#3a3a48',
|
||||
color: 'var(--muted-foreground)',
|
||||
opacity: 0.4,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
customStyle={{
|
||||
@@ -220,7 +251,7 @@ export function CodeBlock({
|
||||
"absolute bottom-0 left-0 right-0 h-16 pointer-events-none",
|
||||
isTerminal
|
||||
? "bg-gradient-to-t from-[#0a0a0a] to-transparent"
|
||||
: "bg-gradient-to-t from-[#282c34] to-transparent"
|
||||
: "bg-gradient-to-t from-muted to-transparent"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
@@ -233,7 +264,7 @@ export function CodeBlock({
|
||||
"flex w-full items-center justify-center gap-1.5 py-1.5 text-xs transition-colors",
|
||||
isTerminal
|
||||
? "bg-zinc-950 text-zinc-400 hover:text-zinc-200"
|
||||
: "bg-zinc-800 dark:bg-zinc-900 text-zinc-400 hover:text-zinc-200"
|
||||
: "bg-muted text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{expanded ? (
|
||||
|
||||
@@ -178,7 +178,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
|
||||
className="flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
|
||||
onClick={() => handleNavigate(dir.path)}
|
||||
>
|
||||
<HugeiconsIcon icon={Folder01Icon} className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
<HugeiconsIcon icon={Folder01Icon} className="h-4 w-4 shrink-0 text-primary" />
|
||||
<span className="truncate">{dir.name}</span>
|
||||
<HugeiconsIcon icon={ArrowRight01Icon} className="ml-auto h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
@@ -287,7 +287,7 @@ export function ImageGenConfirmation({
|
||||
'px-2.5 py-1 text-xs font-medium rounded-md border transition-colors',
|
||||
'disabled:opacity-60 disabled:cursor-not-allowed',
|
||||
aspectRatio === ratio
|
||||
? 'border-blue-500 bg-blue-500/10 text-blue-600 dark:text-blue-400'
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border/60 text-muted-foreground hover:text-foreground hover:border-foreground/30'
|
||||
)}
|
||||
>
|
||||
@@ -313,7 +313,7 @@ export function ImageGenConfirmation({
|
||||
'px-3 py-1 text-xs font-medium rounded-md border transition-colors',
|
||||
'disabled:opacity-60 disabled:cursor-not-allowed',
|
||||
resolution === res
|
||||
? 'border-blue-500 bg-blue-500/10 text-blue-600 dark:text-blue-400'
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border/60 text-muted-foreground hover:text-foreground hover:border-foreground/30'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ImageGenToggle() {
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-full px-2.5 h-7 text-xs font-medium border transition-all',
|
||||
state.enabled
|
||||
? 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/30'
|
||||
? 'bg-primary/15 text-primary border-primary/30'
|
||||
: 'text-muted-foreground border-border/60 hover:text-foreground hover:border-foreground/30 hover:bg-accent/50'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1133,15 +1133,15 @@ export function MessageInput({
|
||||
{/* Command badge */}
|
||||
{badge && (
|
||||
<div className="flex w-full items-center gap-1.5 px-3 pt-2.5 pb-0 order-first">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400 pl-2.5 pr-1.5 py-1 text-xs font-medium border border-blue-500/20">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 text-primary pl-2.5 pr-1.5 py-1 text-xs font-medium border border-primary/20">
|
||||
<span className="font-mono">{badge.command}</span>
|
||||
{badge.description && (
|
||||
<span className="text-blue-500/60 dark:text-blue-400/60 text-[10px]">{badge.description}</span>
|
||||
<span className="text-primary/60 text-[10px]">{badge.description}</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeBadge}
|
||||
className="ml-0.5 rounded-full p-0.5 hover:bg-blue-500/20 transition-colors"
|
||||
className="ml-0.5 rounded-full p-0.5 hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<svg className="h-3 w-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 3l6 6M9 3l-6 6" />
|
||||
|
||||
@@ -106,7 +106,7 @@ function RewindButton({ sessionId, userMessageId }: { sessionId: string; userMes
|
||||
</span>
|
||||
<button
|
||||
onClick={handleRewind}
|
||||
className="text-[10px] text-blue-600 dark:text-blue-400 hover:underline"
|
||||
className="text-[10px] text-primary hover:underline"
|
||||
>
|
||||
{t('messageList.rewindConfirm' as TranslationKey)}
|
||||
</button>
|
||||
|
||||
@@ -100,8 +100,8 @@ function StatusIndicator({ status }: { status: ToolStatus }) {
|
||||
case 'running':
|
||||
return (
|
||||
<span className="relative flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-30" />
|
||||
<HugeiconsIcon icon={Loading02Icon} className="relative h-3.5 w-3.5 animate-spin text-blue-500" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-30" />
|
||||
<HugeiconsIcon icon={Loading02Icon} className="relative h-3.5 w-3.5 animate-spin text-primary" />
|
||||
</span>
|
||||
);
|
||||
case 'success':
|
||||
@@ -282,13 +282,13 @@ export function ToolCallBlock({
|
||||
};
|
||||
|
||||
const statusBorderColor = {
|
||||
running: 'border-blue-500/70',
|
||||
running: 'border-primary/70',
|
||||
success: 'border-green-500/50',
|
||||
error: 'border-red-500/60',
|
||||
}[status];
|
||||
|
||||
const statusBgColor = {
|
||||
running: 'bg-blue-500/[0.03] dark:bg-blue-500/[0.05]',
|
||||
running: 'bg-primary/[0.03] dark:bg-primary/[0.05]',
|
||||
success: 'bg-transparent',
|
||||
error: 'bg-red-500/[0.03] dark:bg-red-500/[0.05]',
|
||||
}[status];
|
||||
@@ -309,11 +309,11 @@ export function ToolCallBlock({
|
||||
)}
|
||||
<HugeiconsIcon icon={toolIconData} className={cn(
|
||||
"h-3.5 w-3.5 shrink-0",
|
||||
category === 'read' && "text-blue-500",
|
||||
category === 'read' && "text-primary",
|
||||
category === 'write' && "text-amber-500",
|
||||
category === 'bash' && "text-green-500",
|
||||
category === 'search' && "text-indigo-500",
|
||||
category === 'other' && "text-zinc-500",
|
||||
category === 'search' && "text-primary",
|
||||
category === 'other' && "text-muted-foreground",
|
||||
)} />
|
||||
<span className="font-mono text-xs truncate flex-1 text-foreground/80">{summary}</span>
|
||||
<div className="flex items-center gap-2 shrink-0 ml-2">
|
||||
|
||||
@@ -13,7 +13,7 @@ export function BatchExecutionItem({ item }: BatchExecutionItemProps) {
|
||||
|
||||
const statusColor = {
|
||||
pending: 'text-muted-foreground',
|
||||
processing: 'text-blue-500',
|
||||
processing: 'text-primary',
|
||||
completed: 'text-green-500',
|
||||
failed: 'text-red-500',
|
||||
cancelled: 'text-muted-foreground',
|
||||
@@ -37,7 +37,7 @@ export function BatchExecutionItem({ item }: BatchExecutionItemProps) {
|
||||
{/* Status indicator */}
|
||||
<div className="shrink-0">
|
||||
{item.status === 'processing' ? (
|
||||
<div className="w-4 h-4 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
|
||||
<div className="w-4 h-4 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
||||
) : item.status === 'completed' ? (
|
||||
<div className="w-4 h-4 rounded-full bg-green-500/20 flex items-center justify-center">
|
||||
<svg className="w-2.5 h-2.5 text-green-500" viewBox="0 0 12 12" fill="none">
|
||||
|
||||
@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Cancel01Icon, Copy01Icon, Tick01Icon, Loading02Icon } from "@hugeicons/core-free-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { useThemeFamily } from "@/lib/theme/context";
|
||||
import { resolveCodeTheme, resolveHljsStyle } from "@/lib/theme/code-themes";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { cjk } from "@streamdown/cjk";
|
||||
import { code } from "@streamdown/code";
|
||||
@@ -216,13 +216,21 @@ function ViewModeToggle({
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve hljs style from the current theme family + mode. */
|
||||
function useDocCodeTheme(isDark: boolean) {
|
||||
const { family, families } = useThemeFamily();
|
||||
const codeTheme = resolveCodeTheme(families, family);
|
||||
return resolveHljsStyle(codeTheme, isDark);
|
||||
}
|
||||
|
||||
/** Source code view using react-syntax-highlighter */
|
||||
function SourceView({ preview, isDark }: { preview: FilePreviewType; isDark: boolean }) {
|
||||
const hljsStyle = useDocCodeTheme(isDark);
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<SyntaxHighlighter
|
||||
language={preview.language}
|
||||
style={isDark ? atomOneDark : atomOneLight}
|
||||
style={hljsStyle}
|
||||
showLineNumbers
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
@@ -235,7 +243,8 @@ function SourceView({ preview, isDark }: { preview: FilePreviewType; isDark: boo
|
||||
lineNumberStyle={{
|
||||
minWidth: "2.5em",
|
||||
paddingRight: "8px",
|
||||
color: isDark ? "#636d83" : "#9ca3af",
|
||||
color: "var(--muted-foreground)",
|
||||
opacity: 0.5,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -70,7 +70,7 @@ function StepIcon({ status }: { status: string }) {
|
||||
case "success":
|
||||
return <HugeiconsIcon icon={Tick01Icon} className="size-4 text-emerald-500" />;
|
||||
case "running":
|
||||
return <HugeiconsIcon icon={Loading02Icon} className="size-4 text-blue-500 animate-spin" />;
|
||||
return <HugeiconsIcon icon={Loading02Icon} className="size-4 text-primary animate-spin" />;
|
||||
case "failed":
|
||||
return <HugeiconsIcon icon={Cancel01Icon} className="size-4 text-red-500" />;
|
||||
case "skipped":
|
||||
@@ -223,11 +223,11 @@ export function InstallWizard({
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setPhase("checking"); // eslint-disable-line react-hooks/set-state-in-effect -- reset state before async check
|
||||
setLogs([]); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setProgress(null); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setCopied(false); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setCopiedBrew(false); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setPrereqs(null); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setLogs([]);
|
||||
setProgress(null);
|
||||
setCopied(false);
|
||||
setCopiedBrew(false);
|
||||
setPrereqs(null);
|
||||
checkPrereqs();
|
||||
}
|
||||
return () => {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useSyncExternalStore } from "react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
Message02Icon,
|
||||
@@ -12,8 +10,6 @@ import {
|
||||
Image01Icon,
|
||||
Settings02Icon,
|
||||
Wifi01Icon,
|
||||
Moon02Icon,
|
||||
Sun02Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -40,13 +36,11 @@ const navItems = [
|
||||
{ href: "/mcp", label: "MCP", icon: Plug01Icon },
|
||||
{ href: "/gallery", label: "Gallery", icon: Image01Icon },
|
||||
{ href: "/bridge", label: "Bridge", icon: Wifi01Icon },
|
||||
{ href: "/settings", label: "Settings", icon: Settings02Icon },
|
||||
] as const;
|
||||
|
||||
export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermissionsActive }: NavRailProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navLabelKeys: Record<string, TranslationKey> = {
|
||||
'Chats': 'nav.chats',
|
||||
@@ -54,11 +48,9 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
|
||||
'MCP': 'extensions.mcpServers',
|
||||
'Gallery': 'gallery.title',
|
||||
'Bridge': 'nav.bridge',
|
||||
'Settings': 'nav.settings',
|
||||
};
|
||||
const emptySubscribe = useCallback(() => () => {}, []);
|
||||
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
|
||||
const isChatRoute = pathname === "/chat" || pathname.startsWith("/chat/");
|
||||
const isSettingsActive = pathname === "/settings" || pathname.startsWith("/settings/");
|
||||
|
||||
return (
|
||||
<aside className="flex w-14 shrink-0 flex-col items-center bg-sidebar pb-3 pt-10">
|
||||
@@ -110,12 +102,6 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
|
||||
<span className="sr-only">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
{item.href === "/settings" && hasUpdate && (
|
||||
<span className={cn(
|
||||
"absolute top-0.5 right-0.5 h-2 w-2 rounded-full",
|
||||
readyToInstall ? "bg-green-500 animate-pulse" : "bg-blue-500"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
@@ -125,7 +111,7 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Bottom: skip-permissions indicator + theme toggle */}
|
||||
{/* Bottom: skip-permissions indicator + settings */}
|
||||
<div className="mt-auto flex flex-col items-center gap-2">
|
||||
{skipPermissionsActive && (
|
||||
<Tooltip>
|
||||
@@ -140,28 +126,33 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
|
||||
<TooltipContent side="right">{t('nav.autoApproveOn')}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{mounted && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="relative">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<HugeiconsIcon icon={Sun02Icon} className="h-4 w-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Moon02Icon} className="h-4 w-4" />
|
||||
className={cn(
|
||||
"h-9 w-9",
|
||||
isSettingsActive && "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||
)}
|
||||
<span className="sr-only">{t('nav.toggleTheme')}</span>
|
||||
>
|
||||
<Link href="/settings">
|
||||
<HugeiconsIcon icon={Settings02Icon} className="h-4 w-4" />
|
||||
<span className="sr-only">{t('nav.settings')}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{theme === "dark" ? t('nav.lightMode') : t('nav.darkMode')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<span className={cn(
|
||||
"absolute top-0.5 right-0.5 h-2 w-2 rounded-full",
|
||||
readyToInstall ? "bg-green-500 animate-pulse" : "bg-primary"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{t('nav.settings')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -113,7 +113,7 @@ export function SplitColumn({ sessionId, isActive, onClose, onFocus }: SplitColu
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 min-w-0 flex-col overflow-hidden rounded-md border-2 transition-colors",
|
||||
isActive ? "border-blue-500" : "border-transparent"
|
||||
isActive ? "border-primary" : "border-transparent"
|
||||
)}
|
||||
onClick={onFocus}
|
||||
>
|
||||
@@ -129,7 +129,7 @@ export function SplitColumn({ sessionId, isActive, onClose, onFocus }: SplitColu
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 min-w-0 flex-col overflow-hidden rounded-md border-2 transition-colors",
|
||||
isActive ? "border-blue-500" : "border-transparent"
|
||||
isActive ? "border-primary" : "border-transparent"
|
||||
)}
|
||||
onClick={onFocus}
|
||||
>
|
||||
@@ -144,7 +144,7 @@ export function SplitColumn({ sessionId, isActive, onClose, onFocus }: SplitColu
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 min-w-0 flex-col overflow-hidden rounded-md border-2 transition-colors",
|
||||
isActive ? "border-blue-500" : "border-transparent"
|
||||
isActive ? "border-primary" : "border-transparent"
|
||||
)}
|
||||
onClick={onFocus}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, type ReactNode } from 'react';
|
||||
import { ThemeFamilyContext } from '@/lib/theme/context';
|
||||
import type { ThemeFamilyMeta } from '@/lib/theme/types';
|
||||
|
||||
const STORAGE_KEY = 'codepilot_theme_family';
|
||||
|
||||
function getInitialFamily(): string {
|
||||
// Read from DOM attribute (set by anti-FOUC script before hydration)
|
||||
if (typeof document !== 'undefined') {
|
||||
return document.documentElement.dataset.themeFamily || 'default';
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
|
||||
interface ThemeFamilyProviderProps {
|
||||
families: ThemeFamilyMeta[];
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ThemeFamilyProvider({ families, children }: ThemeFamilyProviderProps) {
|
||||
const [rawFamily, setFamilyState] = useState(getInitialFamily);
|
||||
|
||||
// Derive validated family: if stored ID no longer exists in loaded themes, use 'default'.
|
||||
const isValid = families.length === 0 || families.some((f) => f.id === rawFamily);
|
||||
const family = isValid ? rawFamily : 'default';
|
||||
|
||||
// When the derived family differs from raw (i.e. fallback occurred),
|
||||
// sync DOM attribute and localStorage. This effect does NOT call setState,
|
||||
// avoiding the cascading-render lint error.
|
||||
useEffect(() => {
|
||||
if (family !== rawFamily) {
|
||||
document.documentElement.setAttribute('data-theme-family', family);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, family);
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}
|
||||
}, [family, rawFamily]);
|
||||
|
||||
const setFamily = useCallback((id: string) => {
|
||||
setFamilyState(id);
|
||||
// Sync to DOM immediately for instant visual update
|
||||
document.documentElement.setAttribute('data-theme-family', id);
|
||||
// Persist to localStorage (sole source of truth for theme family)
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, id);
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeFamilyContext.Provider value={{ family, setFamily, families }}>
|
||||
{children}
|
||||
</ThemeFamilyContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export function UpdateBanner() {
|
||||
if (!updateInfo?.isNativeUpdate || !updateInfo.readyToInstall) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 border-b border-blue-500/20 bg-blue-500/10 px-4 py-1.5 text-sm">
|
||||
<div className="flex items-center justify-center gap-3 border-b border-primary/20 bg-primary/10 px-4 py-1.5 text-sm">
|
||||
<span>{t('update.readyToInstall', { version: updateInfo.latestVersion })}</span>
|
||||
<Button size="sm" variant="outline" className="h-6 text-xs" onClick={quitAndInstall}>
|
||||
{t('update.restartNow')}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function UpdateDialog() {
|
||||
ol: ({ children }) => <ol className="mb-2 list-decimal pl-4 text-sm">{children}</ol>,
|
||||
li: ({ children }) => <li className="mb-0.5">{children}</li>,
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 underline">
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className="text-primary underline">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
@@ -88,7 +88,7 @@ export function UpdateDialog() {
|
||||
<div className="space-y-1">
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-500 transition-all"
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${Math.min(downloadProgress!, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -277,7 +277,7 @@ export function McpManager() {
|
||||
<span className={`h-2 w-2 rounded-full shrink-0 ${
|
||||
s.status === 'connected' ? 'bg-green-500' :
|
||||
s.status === 'failed' ? 'bg-red-500' :
|
||||
s.status === 'pending' ? 'bg-blue-500' :
|
||||
s.status === 'pending' ? 'bg-primary' :
|
||||
s.status === 'disabled' ? 'bg-gray-400' :
|
||||
'bg-yellow-500'
|
||||
}`} />
|
||||
|
||||
@@ -28,7 +28,7 @@ function getServerTypeInfo(server: MCPServer) {
|
||||
const type = server.type || 'stdio';
|
||||
switch (type) {
|
||||
case 'sse':
|
||||
return { label: 'SSE', icon: Wifi01Icon, color: 'text-blue-500' };
|
||||
return { label: 'SSE', icon: Wifi01Icon, color: 'text-primary' };
|
||||
case 'http':
|
||||
return { label: 'HTTP', icon: GlobeIcon, color: 'text-green-500' };
|
||||
default:
|
||||
@@ -45,7 +45,7 @@ function getStatusBadge(status: McpRuntimeStatus['status']) {
|
||||
case 'needs-auth':
|
||||
return { label: 'Auth Required', className: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20' };
|
||||
case 'pending':
|
||||
return { label: 'Pending', className: 'bg-blue-500/10 text-blue-600 border-blue-500/20' };
|
||||
return { label: 'Pending', className: 'bg-primary/10 text-primary border-primary/20' };
|
||||
case 'disabled':
|
||||
return { label: 'Disabled', className: 'bg-gray-500/10 text-gray-500 border-gray-500/20' };
|
||||
default:
|
||||
|
||||
@@ -6,9 +6,19 @@ import { ArrowLeft01Icon, Copy01Icon, Tick01Icon, Loading02Icon } from "@hugeico
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { useThemeFamily } from "@/lib/theme/context";
|
||||
import { resolveCodeTheme, resolveHljsStyle } from "@/lib/theme/code-themes";
|
||||
import { usePanel } from "@/hooks/usePanel";
|
||||
|
||||
function useFilePreviewCodeTheme() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { family, families } = useThemeFamily();
|
||||
const isDark = resolvedTheme === "dark";
|
||||
const codeTheme = resolveCodeTheme(families, family);
|
||||
return resolveHljsStyle(codeTheme, isDark);
|
||||
}
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { FilePreview as FilePreviewType } from "@/types";
|
||||
|
||||
@@ -20,6 +30,7 @@ interface FilePreviewProps {
|
||||
export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
const { workingDirectory } = usePanel();
|
||||
const { t } = useTranslation();
|
||||
const hljsStyle = useFilePreviewCodeTheme();
|
||||
const [preview, setPreview] = useState<FilePreviewType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -119,7 +130,7 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
<div className="rounded-md border border-border text-xs">
|
||||
<SyntaxHighlighter
|
||||
language={preview.language}
|
||||
style={atomOneDark}
|
||||
style={hljsStyle}
|
||||
showLineNumbers
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
@@ -131,7 +142,8 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
|
||||
lineNumberStyle={{
|
||||
minWidth: "2.5em",
|
||||
paddingRight: "8px",
|
||||
color: "#636d83",
|
||||
color: "var(--muted-foreground)",
|
||||
opacity: 0.5,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ interface TaskCardProps {
|
||||
|
||||
const statusColors: Record<TaskStatus, string> = {
|
||||
pending: "bg-yellow-500",
|
||||
in_progress: "bg-blue-500",
|
||||
in_progress: "bg-primary",
|
||||
completed: "bg-green-500",
|
||||
failed: "bg-red-500",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { codeToHtml, type BundledTheme } from "shiki";
|
||||
import { useThemeFamily } from "@/lib/theme/context";
|
||||
import {
|
||||
resolveShikiTheme,
|
||||
resolveShikiThemes,
|
||||
} from "@/lib/theme/code-themes";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { Sun02Icon, Moon02Icon, ComputerDesk01Icon } from "@hugeicons/core-free-icons";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// ── Theme Mode Pill Selector ────────────────────────────────────────
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ value: "light", icon: Sun02Icon, labelKey: "settings.modeLight" as const },
|
||||
{ value: "dark", icon: Moon02Icon, labelKey: "settings.modeDark" as const },
|
||||
{ value: "system", icon: ComputerDesk01Icon, labelKey: "settings.modeSystem" as const },
|
||||
] as const;
|
||||
|
||||
function ThemeModePills({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex items-center rounded-lg border border-border/50 p-1 gap-1" role="radiogroup">
|
||||
{MODE_OPTIONS.map((opt) => {
|
||||
const selected = value === opt.value;
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => onChange(opt.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
selected
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={opt.icon} className="h-3.5 w-3.5" />
|
||||
{t(opt.labelKey)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Shiki Code Preview ──────────────────────────────────────────────
|
||||
|
||||
const PREVIEW_CODE = `function greet(name: string) {
|
||||
const time = new Date().getHours();
|
||||
if (time < 12) return \`Good morning, \${name}\`;
|
||||
return \`Hello, \${name}\`;
|
||||
}`;
|
||||
|
||||
function ShikiCodePreview({ isDark }: { isDark: boolean }) {
|
||||
const { family, families } = useThemeFamily();
|
||||
const shikiMapping = resolveShikiTheme(families, family);
|
||||
const { light, dark } = resolveShikiThemes(shikiMapping);
|
||||
const theme: BundledTheme = isDark ? dark : light;
|
||||
const [html, setHtml] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
codeToHtml(PREVIEW_CODE, {
|
||||
lang: "typescript",
|
||||
theme,
|
||||
}).then((result) => {
|
||||
if (!cancelled) setHtml(result);
|
||||
}).catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 text-xs bg-muted text-muted-foreground">
|
||||
<span className="font-medium">preview.ts</span>
|
||||
<span className="rounded bg-accent px-1.5 py-0.5 text-accent-foreground">TypeScript</span>
|
||||
</div>
|
||||
{html ? (
|
||||
<div
|
||||
className="shiki-preview [&_pre]:!m-0 [&_pre]:!rounded-none [&_pre]:!text-xs [&_pre]:!leading-relaxed [&_pre]:!p-2 [&_code]:!text-xs"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-24 flex items-center justify-center text-xs text-muted-foreground">
|
||||
Loading…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── UI Token Preview ────────────────────────────────────────────────
|
||||
|
||||
function UIPreview() {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button className="rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
|
||||
Primary
|
||||
</button>
|
||||
<button className="rounded-md bg-secondary px-3 py-1 text-xs font-medium text-secondary-foreground">
|
||||
Secondary
|
||||
</button>
|
||||
<button className="rounded-md bg-destructive px-3 py-1 text-xs font-medium text-primary-foreground">
|
||||
Destructive
|
||||
</button>
|
||||
<span className="inline-flex items-center rounded-full bg-accent px-2.5 py-0.5 text-[10px] font-medium text-accent-foreground">
|
||||
Badge
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full border border-border bg-card px-2.5 py-0.5 text-[10px] text-card-foreground">
|
||||
Card
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-muted px-2.5 py-0.5 text-[10px] text-muted-foreground">
|
||||
Muted
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Appearance Section ─────────────────────────────────────────
|
||||
|
||||
export function AppearanceSection() {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const { family, setFamily, families } = useThemeFamily();
|
||||
const { t } = useTranslation();
|
||||
const isDark = resolvedTheme === "dark";
|
||||
|
||||
const mounted = useSyncExternalStore(
|
||||
() => () => {},
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Section header — outside card */}
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("settings.appearance")}</h2>
|
||||
<p className="text-xs text-muted-foreground">{t("settings.appearanceDesc")}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
{/* Mode */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-xs font-medium">{t("settings.themeMode")}</h3>
|
||||
<p className="text-[11px] text-muted-foreground">{t("settings.themeModeDesc")}</p>
|
||||
</div>
|
||||
<ThemeModePills value={theme || "system"} onChange={setTheme} />
|
||||
</div>
|
||||
|
||||
{theme === "system" && resolvedTheme && (
|
||||
<p className="text-[11px] text-muted-foreground pl-1">
|
||||
{resolvedTheme === "dark" ? t("settings.modeDark") : t("settings.modeLight")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Family */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-xs font-medium">{t("settings.themeFamily")}</h3>
|
||||
<p className="text-[11px] text-muted-foreground">{t("settings.themeFamilyDesc")}</p>
|
||||
</div>
|
||||
<Select value={family} onValueChange={setFamily}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{families.map((f) => (
|
||||
<SelectItem key={f.id} value={f.id}>
|
||||
<span className="flex items-center gap-2">
|
||||
{f.previewColors && (
|
||||
<span className="flex gap-0.5">
|
||||
<span
|
||||
className="inline-block h-3 w-3 rounded-full border border-border/30"
|
||||
style={{ background: f.previewColors.primaryLight }}
|
||||
/>
|
||||
<span
|
||||
className="inline-block h-3 w-3 rounded-full border border-border/30"
|
||||
style={{ background: f.previewColors.primaryDark }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs">{f.label}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Preview */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-xs font-medium text-muted-foreground">Preview</h3>
|
||||
<UIPreview />
|
||||
<ShikiCodePreview isDark={isDark} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { AppearanceSection } from "./AppearanceSection";
|
||||
|
||||
function UpdateCard() {
|
||||
const { updateInfo, checking, checkForUpdates, downloadUpdate, quitAndInstall, setShowDialog } = useUpdate();
|
||||
@@ -75,7 +76,7 @@ function UpdateCard() {
|
||||
{updateInfo.updateAvailable ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : isDownloading ? 'bg-yellow-500 animate-pulse' : 'bg-blue-500'}`} />
|
||||
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : isDownloading ? 'bg-yellow-500 animate-pulse' : 'bg-primary'}`} />
|
||||
<span className="text-sm">
|
||||
{updateInfo.readyToInstall
|
||||
? t('update.readyToInstall', { version: updateInfo.latestVersion })
|
||||
@@ -98,7 +99,7 @@ function UpdateCard() {
|
||||
{isDownloading && (
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-500 transition-all"
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${Math.min(updateInfo.downloadProgress!, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -209,8 +210,9 @@ export function GeneralSection() {
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<UpdateCard />
|
||||
|
||||
{/* Auto-approve toggle */}
|
||||
<div className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
|
||||
{/* General settings card */}
|
||||
<div className={`rounded-lg border p-4 space-y-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
|
||||
{/* Auto-approve toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t('settings.autoApproveTitle')}</h2>
|
||||
@@ -225,15 +227,15 @@ export function GeneralSection() {
|
||||
/>
|
||||
</div>
|
||||
{skipPermissions && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
|
||||
<div className="flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500" />
|
||||
{t('settings.autoApproveWarning')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Language picker */}
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Language picker */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t('settings.language')}</h2>
|
||||
@@ -250,10 +252,10 @@ export function GeneralSection() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thinking mode */}
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Thinking mode */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t('settings.thinkingMode' as TranslationKey)}</h2>
|
||||
@@ -272,6 +274,9 @@ export function GeneralSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Appearance */}
|
||||
<AppearanceSection />
|
||||
|
||||
{/* Account info */}
|
||||
{accountInfo && (
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
|
||||
@@ -134,7 +134,7 @@ export function CreateSkillDialog({
|
||||
className={cn(
|
||||
"flex-1 flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors",
|
||||
scope === "project"
|
||||
? "border-blue-500/50 bg-blue-500/10 text-blue-600 dark:text-blue-400"
|
||||
? "border-primary/50 bg-primary/10 text-primary"
|
||||
: "border-border hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -140,7 +140,7 @@ export function InstallProgressDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{phase === "running" && (
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-blue-500" />
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-primary" />
|
||||
)}
|
||||
{phase === "success" && (
|
||||
<HugeiconsIcon icon={CheckmarkCircle02Icon} className="h-5 w-5 text-green-500" />
|
||||
|
||||
@@ -126,8 +126,8 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
|
||||
: skill.source === "installed"
|
||||
? "border-orange-500/40 text-orange-600 dark:text-orange-400"
|
||||
: skill.source === "plugin"
|
||||
? "border-indigo-500/40 text-indigo-600 dark:text-indigo-400"
|
||||
: "border-blue-500/40 text-blue-600 dark:text-blue-400"
|
||||
? "border-primary/40 text-primary"
|
||||
: "border-primary/40 text-primary"
|
||||
)}
|
||||
>
|
||||
{skill.source === "global" ? (
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useThemeFamily } from '@/lib/theme/context';
|
||||
|
||||
export function useAppTheme() {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const { family, setFamily, families } = useThemeFamily();
|
||||
|
||||
return {
|
||||
mode: theme,
|
||||
setMode: setTheme,
|
||||
resolvedMode: resolvedTheme,
|
||||
family,
|
||||
setFamily,
|
||||
families,
|
||||
};
|
||||
}
|
||||
@@ -85,6 +85,17 @@ const en = {
|
||||
'settings.languageDesc': 'Choose the display language for the interface',
|
||||
'settings.usage': 'Usage',
|
||||
|
||||
// ── Settings: Appearance ──────────────────────────────────────
|
||||
'settings.appearance': 'Appearance',
|
||||
'settings.appearanceDesc': 'Customize the look and feel of CodePilot',
|
||||
'settings.themeMode': 'Theme Mode',
|
||||
'settings.themeModeDesc': 'Choose between light, dark, or system preference',
|
||||
'settings.themeFamily': 'Color Theme',
|
||||
'settings.themeFamilyDesc': 'Select a color palette for the interface',
|
||||
'settings.modeLight': 'Light',
|
||||
'settings.modeDark': 'Dark',
|
||||
'settings.modeSystem': 'System',
|
||||
|
||||
// ── Settings: Usage Stats ───────────────────────────────────
|
||||
'usage.totalTokens': 'Total Tokens',
|
||||
'usage.totalCost': 'Total Cost',
|
||||
|
||||
@@ -82,6 +82,17 @@ const zh: Record<TranslationKey, string> = {
|
||||
'settings.languageDesc': '选择界面显示语言',
|
||||
'settings.usage': '用量统计',
|
||||
|
||||
// ── Settings: Appearance ──────────────────────────────────────
|
||||
'settings.appearance': '外观',
|
||||
'settings.appearanceDesc': '自定义 CodePilot 的外观和风格',
|
||||
'settings.themeMode': '主题模式',
|
||||
'settings.themeModeDesc': '选择浅色、深色或跟随系统',
|
||||
'settings.themeFamily': '颜色主题',
|
||||
'settings.themeFamilyDesc': '为界面选择一套配色方案',
|
||||
'settings.modeLight': '浅色',
|
||||
'settings.modeDark': '深色',
|
||||
'settings.modeSystem': '跟随系统',
|
||||
|
||||
// ── Settings: Usage Stats ───────────────────────────────────
|
||||
'usage.totalTokens': '总 Token',
|
||||
'usage.totalCost': '总费用',
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Centralized code theme mappings for Prism, HLJS, and Shiki.
|
||||
*
|
||||
* Each theme family JSON can specify `codeTheme: { light, dark }` and
|
||||
* `shikiTheme: { light, dark }` using the keys defined below.
|
||||
* Components should use the hooks exported here rather than maintaining
|
||||
* local maps.
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { BundledTheme } from 'shiki';
|
||||
|
||||
// ── Prism (react-syntax-highlighter/prism) ──────────────────────────────
|
||||
import {
|
||||
oneDark,
|
||||
oneLight,
|
||||
vscDarkPlus,
|
||||
nord as nordPrism,
|
||||
solarizedDarkAtom,
|
||||
solarizedlight as solarizedLightPrism,
|
||||
nightOwl as nightOwlPrism,
|
||||
dracula as draculaPrism,
|
||||
gruvboxDark as gruvboxDarkPrism,
|
||||
gruvboxLight as gruvboxLightPrism,
|
||||
ghcolors,
|
||||
synthwave84 as synthwave84Prism,
|
||||
materialDark as materialDarkPrism,
|
||||
materialOceanic as materialOceanicPrism,
|
||||
duotoneSea as duotoneSeaPrism,
|
||||
coldarkDark as coldarkDarkPrism,
|
||||
coldarkCold as coldarkColdPrism,
|
||||
materialLight as materialLightPrism,
|
||||
duotoneLight as duotoneLightPrism,
|
||||
vs as vsPrism,
|
||||
coy as coyPrism,
|
||||
prism as prismDefault,
|
||||
duotoneEarth as duotoneEarthPrism,
|
||||
duotoneForest as duotoneForestPrism,
|
||||
twilight as twilightPrism,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
type SyntaxStyle = Record<string, CSSProperties>;
|
||||
|
||||
/** Prism theme name → style object. Keys match `codeTheme.dark` / `codeTheme.light` in theme JSONs. */
|
||||
export const PRISM_THEME_MAP: Record<string, SyntaxStyle> = {
|
||||
oneDark,
|
||||
oneLight,
|
||||
vscDarkPlus,
|
||||
nord: nordPrism,
|
||||
solarizedDarkAtom,
|
||||
solarizedlight: solarizedLightPrism,
|
||||
nightOwl: nightOwlPrism,
|
||||
dracula: draculaPrism,
|
||||
gruvboxDark: gruvboxDarkPrism,
|
||||
gruvboxLight: gruvboxLightPrism,
|
||||
ghcolors,
|
||||
synthwave84: synthwave84Prism,
|
||||
materialDark: materialDarkPrism,
|
||||
materialOceanic: materialOceanicPrism,
|
||||
duotoneSea: duotoneSeaPrism,
|
||||
coldarkDark: coldarkDarkPrism,
|
||||
coldarkCold: coldarkColdPrism,
|
||||
materialLight: materialLightPrism,
|
||||
duotoneLight: duotoneLightPrism,
|
||||
vs: vsPrism,
|
||||
coy: coyPrism,
|
||||
prism: prismDefault,
|
||||
duotoneEarth: duotoneEarthPrism,
|
||||
duotoneForest: duotoneForestPrism,
|
||||
twilight: twilightPrism,
|
||||
};
|
||||
|
||||
export const PRISM_DEFAULT_DARK: SyntaxStyle = oneDark;
|
||||
export const PRISM_DEFAULT_LIGHT: SyntaxStyle = oneLight;
|
||||
|
||||
// ── HLJS (react-syntax-highlighter/hljs) ────────────────────────────────
|
||||
import {
|
||||
atomOneDark,
|
||||
atomOneLight,
|
||||
nord as nordHljs,
|
||||
solarizedDark,
|
||||
solarizedLight,
|
||||
nightOwl as nightOwlHljs,
|
||||
dracula as draculaHljs,
|
||||
gruvboxDark as gruvboxDarkHljs,
|
||||
gruvboxLight as gruvboxLightHljs,
|
||||
github as githubHljs,
|
||||
vs2015 as vs2015Hljs,
|
||||
monokaiSublime as monokaiSublimeHljs,
|
||||
vs as vsHljs,
|
||||
idea as ideaHljs,
|
||||
lightfair as lightfairHljs,
|
||||
xcode as xcodeHljs,
|
||||
zenburn as zenburnHljs,
|
||||
darcula as darculaHljs,
|
||||
obsidian as obsidianHljs,
|
||||
} from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
|
||||
type HljsStyle = Record<string, CSSProperties>;
|
||||
|
||||
/** HLJS theme name → style object. Same keys as Prism map where possible. */
|
||||
export const HLJS_THEME_MAP: Record<string, HljsStyle> = {
|
||||
oneDark: atomOneDark,
|
||||
oneLight: atomOneLight,
|
||||
nord: nordHljs,
|
||||
solarizedDarkAtom: solarizedDark,
|
||||
solarizedlight: solarizedLight,
|
||||
nightOwl: nightOwlHljs,
|
||||
dracula: draculaHljs,
|
||||
gruvboxDark: gruvboxDarkHljs,
|
||||
gruvboxLight: gruvboxLightHljs,
|
||||
ghcolors: githubHljs,
|
||||
synthwave84: monokaiSublimeHljs,
|
||||
materialDark: vs2015Hljs,
|
||||
materialOceanic: atomOneDark,
|
||||
duotoneSea: atomOneDark,
|
||||
coldarkDark: atomOneDark,
|
||||
coldarkCold: atomOneLight,
|
||||
materialLight: ideaHljs,
|
||||
duotoneLight: atomOneLight,
|
||||
vs: vsHljs,
|
||||
coy: xcodeHljs,
|
||||
prism: lightfairHljs,
|
||||
duotoneEarth: darculaHljs,
|
||||
duotoneForest: zenburnHljs,
|
||||
twilight: obsidianHljs,
|
||||
};
|
||||
|
||||
export const HLJS_DEFAULT_DARK: HljsStyle = atomOneDark;
|
||||
export const HLJS_DEFAULT_LIGHT: HljsStyle = atomOneLight;
|
||||
|
||||
// ── Shiki ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const SHIKI_DEFAULT_LIGHT: BundledTheme = 'github-light';
|
||||
export const SHIKI_DEFAULT_DARK: BundledTheme = 'github-dark';
|
||||
|
||||
// ── Resolution helpers ──────────────────────────────────────────────────
|
||||
|
||||
import type { ThemeFamilyMeta, CodeThemeMapping } from './types';
|
||||
|
||||
/**
|
||||
* Resolve a code-theme mapping from the current family metadata.
|
||||
* Returns `undefined` if family has no `codeTheme`.
|
||||
*/
|
||||
export function resolveCodeTheme(
|
||||
families: ThemeFamilyMeta[],
|
||||
familyId: string,
|
||||
): CodeThemeMapping | undefined {
|
||||
return families.find((f) => f.id === familyId)?.codeTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a shiki-theme mapping from the current family metadata.
|
||||
* Returns `undefined` if family has no `shikiTheme`.
|
||||
*/
|
||||
export function resolveShikiTheme(
|
||||
families: ThemeFamilyMeta[],
|
||||
familyId: string,
|
||||
): CodeThemeMapping | undefined {
|
||||
return families.find((f) => f.id === familyId)?.shikiTheme;
|
||||
}
|
||||
|
||||
/** Pick a Prism style for the given mode. Falls back to oneDark / oneLight. */
|
||||
export function resolvePrismStyle(
|
||||
codeTheme: CodeThemeMapping | undefined,
|
||||
isDark: boolean,
|
||||
): SyntaxStyle {
|
||||
const name = isDark ? codeTheme?.dark : codeTheme?.light;
|
||||
if (name && PRISM_THEME_MAP[name]) return PRISM_THEME_MAP[name];
|
||||
return isDark ? PRISM_DEFAULT_DARK : PRISM_DEFAULT_LIGHT;
|
||||
}
|
||||
|
||||
/** Pick an HLJS style for the given mode. Falls back to atomOneDark / atomOneLight. */
|
||||
export function resolveHljsStyle(
|
||||
codeTheme: CodeThemeMapping | undefined,
|
||||
isDark: boolean,
|
||||
): HljsStyle {
|
||||
const name = isDark ? codeTheme?.dark : codeTheme?.light;
|
||||
if (name && HLJS_THEME_MAP[name]) return HLJS_THEME_MAP[name];
|
||||
return isDark ? HLJS_DEFAULT_DARK : HLJS_DEFAULT_LIGHT;
|
||||
}
|
||||
|
||||
/** Pick a Shiki BundledTheme pair. Falls back to github-light / github-dark. */
|
||||
export function resolveShikiThemes(
|
||||
shikiTheme: CodeThemeMapping | undefined,
|
||||
): { light: BundledTheme; dark: BundledTheme } {
|
||||
return {
|
||||
light: (shikiTheme?.light ?? SHIKI_DEFAULT_LIGHT) as BundledTheme,
|
||||
dark: (shikiTheme?.dark ?? SHIKI_DEFAULT_DARK) as BundledTheme,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { ThemeFamilyMeta } from './types';
|
||||
|
||||
export interface ThemeFamilyContextValue {
|
||||
family: string;
|
||||
setFamily: (id: string) => void;
|
||||
families: ThemeFamilyMeta[];
|
||||
}
|
||||
|
||||
export const ThemeFamilyContext = createContext<ThemeFamilyContextValue>({
|
||||
family: 'default',
|
||||
setFamily: () => {},
|
||||
families: [],
|
||||
});
|
||||
|
||||
export function useThemeFamily() {
|
||||
return useContext(ThemeFamilyContext);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { ThemeFamily, ThemeFamilyMeta, ThemeColors } from './types';
|
||||
|
||||
/** Hardcoded fallback matching globals.css :root values. */
|
||||
const FALLBACK_LIGHT: ThemeColors = {
|
||||
background: 'oklch(1 0 0)',
|
||||
foreground: 'oklch(0.147 0.004 49.25)',
|
||||
card: 'oklch(1 0 0)',
|
||||
cardForeground: 'oklch(0.147 0.004 49.25)',
|
||||
popover: 'oklch(1 0 0)',
|
||||
popoverForeground: 'oklch(0.147 0.004 49.25)',
|
||||
primary: 'oklch(0.546 0.245 262.881)',
|
||||
primaryForeground: 'oklch(0.985 0.001 106.423)',
|
||||
secondary: 'oklch(0.97 0.001 106.424)',
|
||||
secondaryForeground: 'oklch(0.216 0.006 56.043)',
|
||||
muted: 'oklch(0.97 0.001 106.424)',
|
||||
mutedForeground: 'oklch(0.553 0.013 58.071)',
|
||||
accent: 'oklch(0.97 0.001 106.424)',
|
||||
accentForeground: 'oklch(0.216 0.006 56.043)',
|
||||
destructive: 'oklch(0.577 0.245 27.325)',
|
||||
border: 'oklch(0.923 0.003 48.717)',
|
||||
input: 'oklch(0.923 0.003 48.717)',
|
||||
ring: 'oklch(0.546 0.245 262.881)',
|
||||
chart1: 'oklch(0.546 0.245 262.881)',
|
||||
chart2: 'oklch(0.6 0.118 184.704)',
|
||||
chart3: 'oklch(0.398 0.07 227.392)',
|
||||
chart4: 'oklch(0.828 0.189 84.429)',
|
||||
chart5: 'oklch(0.769 0.188 70.08)',
|
||||
sidebar: 'oklch(0.985 0.001 106.423)',
|
||||
sidebarForeground: 'oklch(0.147 0.004 49.25)',
|
||||
sidebarPrimary: 'oklch(0.546 0.245 262.881)',
|
||||
sidebarPrimaryForeground: 'oklch(0.985 0.001 106.423)',
|
||||
sidebarAccent: 'oklch(0.97 0.001 106.424)',
|
||||
sidebarAccentForeground: 'oklch(0.216 0.006 56.043)',
|
||||
sidebarBorder: 'oklch(0.923 0.003 48.717)',
|
||||
sidebarRing: 'oklch(0.546 0.245 262.881)',
|
||||
};
|
||||
|
||||
const FALLBACK_DARK: ThemeColors = {
|
||||
background: 'oklch(0.147 0.004 49.25)',
|
||||
foreground: 'oklch(0.985 0.001 106.423)',
|
||||
card: 'oklch(0.216 0.006 56.043)',
|
||||
cardForeground: 'oklch(0.985 0.001 106.423)',
|
||||
popover: 'oklch(0.216 0.006 56.043)',
|
||||
popoverForeground: 'oklch(0.985 0.001 106.423)',
|
||||
primary: 'oklch(0.623 0.214 259.815)',
|
||||
primaryForeground: 'oklch(0.985 0.001 106.423)',
|
||||
secondary: 'oklch(0.268 0.007 34.298)',
|
||||
secondaryForeground: 'oklch(0.985 0.001 106.423)',
|
||||
muted: 'oklch(0.268 0.007 34.298)',
|
||||
mutedForeground: 'oklch(0.709 0.01 56.259)',
|
||||
accent: 'oklch(0.268 0.007 34.298)',
|
||||
accentForeground: 'oklch(0.985 0.001 106.423)',
|
||||
destructive: 'oklch(0.704 0.191 22.216)',
|
||||
border: 'oklch(1 0 0 / 10%)',
|
||||
input: 'oklch(1 0 0 / 15%)',
|
||||
ring: 'oklch(0.623 0.214 259.815)',
|
||||
chart1: 'oklch(0.623 0.214 259.815)',
|
||||
chart2: 'oklch(0.696 0.17 162.48)',
|
||||
chart3: 'oklch(0.769 0.188 70.08)',
|
||||
chart4: 'oklch(0.627 0.265 303.9)',
|
||||
chart5: 'oklch(0.645 0.246 16.439)',
|
||||
sidebar: 'oklch(0.216 0.006 56.043)',
|
||||
sidebarForeground: 'oklch(0.985 0.001 106.423)',
|
||||
sidebarPrimary: 'oklch(0.623 0.214 259.815)',
|
||||
sidebarPrimaryForeground: 'oklch(0.985 0.001 106.423)',
|
||||
sidebarAccent: 'oklch(0.268 0.007 34.298)',
|
||||
sidebarAccentForeground: 'oklch(0.985 0.001 106.423)',
|
||||
sidebarBorder: 'oklch(1 0 0 / 10%)',
|
||||
sidebarRing: 'oklch(0.623 0.214 259.815)',
|
||||
};
|
||||
|
||||
/** Every key in ThemeColors is required — must match types.ts exactly. */
|
||||
const REQUIRED_COLOR_KEYS: (keyof ThemeColors)[] = [
|
||||
'background', 'foreground', 'card', 'cardForeground',
|
||||
'popover', 'popoverForeground', 'primary', 'primaryForeground',
|
||||
'secondary', 'secondaryForeground', 'muted', 'mutedForeground',
|
||||
'accent', 'accentForeground', 'destructive', 'border', 'input', 'ring',
|
||||
'chart1', 'chart2', 'chart3', 'chart4', 'chart5',
|
||||
'sidebar', 'sidebarForeground', 'sidebarPrimary', 'sidebarPrimaryForeground',
|
||||
'sidebarAccent', 'sidebarAccentForeground', 'sidebarBorder', 'sidebarRing',
|
||||
];
|
||||
|
||||
function isValidThemeColors(obj: unknown): obj is ThemeColors {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
const record = obj as Record<string, unknown>;
|
||||
return REQUIRED_COLOR_KEYS.every(
|
||||
(key) => typeof record[key] === 'string' && record[key] !== ''
|
||||
);
|
||||
}
|
||||
|
||||
/** Check whether a value is a valid { light: string, dark: string } mapping. */
|
||||
function isValidCodeThemeMapping(obj: unknown): boolean {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
const rec = obj as Record<string, unknown>;
|
||||
return typeof rec.light === 'string' && rec.light !== '' &&
|
||||
typeof rec.dark === 'string' && rec.dark !== '';
|
||||
}
|
||||
|
||||
function isValidThemeFamily(obj: unknown): obj is ThemeFamily {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
const record = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof record.id === 'string' &&
|
||||
typeof record.label === 'string' &&
|
||||
typeof record.order === 'number' &&
|
||||
isValidThemeColors(record.light) &&
|
||||
isValidThemeColors(record.dark)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip invalid codeTheme / shikiTheme from a loaded theme.
|
||||
* This way a bad mapping doesn't break the whole theme; consumers fall back to defaults.
|
||||
*/
|
||||
function sanitizeThemeMappings(theme: ThemeFamily): void {
|
||||
if (theme.codeTheme !== undefined && !isValidCodeThemeMapping(theme.codeTheme)) {
|
||||
console.warn(`[theme-loader] Stripping invalid codeTheme from theme "${theme.id}"`);
|
||||
delete theme.codeTheme;
|
||||
}
|
||||
if (theme.shikiTheme !== undefined && !isValidCodeThemeMapping(theme.shikiTheme)) {
|
||||
console.warn(`[theme-loader] Stripping invalid shikiTheme from theme "${theme.id}"`);
|
||||
delete theme.shikiTheme;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the themes directory. Works for both dev and Electron packaged app. */
|
||||
function resolveThemesDir(): string {
|
||||
// In Electron packaged app, process.resourcesPath points to resources/
|
||||
// and themes are at standalone/themes/ inside resources.
|
||||
const electronPath = process.env.RESOURCES_PATH
|
||||
? path.join(process.env.RESOURCES_PATH, 'standalone', 'themes')
|
||||
: null;
|
||||
|
||||
const devPath = path.resolve(process.cwd(), 'themes');
|
||||
|
||||
if (electronPath && fs.existsSync(electronPath)) return electronPath;
|
||||
return devPath;
|
||||
}
|
||||
|
||||
// Module-level cache (runs once per process)
|
||||
let cachedFamilies: ThemeFamily[] | null = null;
|
||||
|
||||
/** Reset the module-level cache. Test-only. */
|
||||
export function _resetCache() {
|
||||
cachedFamilies = null;
|
||||
}
|
||||
|
||||
/** Load and validate all theme families from themes/*.json. */
|
||||
export function getAllThemeFamilies(): ThemeFamily[] {
|
||||
if (cachedFamilies) return cachedFamilies;
|
||||
|
||||
const themesDir = resolveThemesDir();
|
||||
const families: ThemeFamily[] = [];
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(themesDir)) {
|
||||
console.warn(`[theme-loader] Themes directory not found: ${themesDir}`);
|
||||
} else {
|
||||
const files = fs.readdirSync(themesDir).filter((f) => f.endsWith('.json'));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(themesDir, file), 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (isValidThemeFamily(parsed)) {
|
||||
sanitizeThemeMappings(parsed);
|
||||
families.push(parsed);
|
||||
} else {
|
||||
console.warn(`[theme-loader] Skipping invalid theme file: ${file}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[theme-loader] Failed to parse ${file}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[theme-loader] Error reading themes directory:', err);
|
||||
}
|
||||
|
||||
// Ensure a default family always exists
|
||||
if (!families.some((f) => f.id === 'default')) {
|
||||
families.push({
|
||||
id: 'default',
|
||||
label: 'Default',
|
||||
order: 0,
|
||||
light: FALLBACK_LIGHT,
|
||||
dark: FALLBACK_DARK,
|
||||
});
|
||||
}
|
||||
|
||||
families.sort((a, b) => a.order - b.order);
|
||||
cachedFamilies = families;
|
||||
return families;
|
||||
}
|
||||
|
||||
/** Lightweight metadata for client-side use. */
|
||||
export function getThemeFamilyMetas(): ThemeFamilyMeta[] {
|
||||
return getAllThemeFamilies().map((f) => ({
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
description: f.description,
|
||||
previewColors: {
|
||||
primaryLight: f.light.primary,
|
||||
primaryDark: f.dark.primary,
|
||||
accentLight: f.light.accent,
|
||||
backgroundLight: f.light.background,
|
||||
},
|
||||
codeTheme: f.codeTheme,
|
||||
shikiTheme: f.shikiTheme,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ThemeFamily, ThemeColors } from './types';
|
||||
|
||||
/** Convert camelCase key to CSS variable name: cardForeground → --card-foreground */
|
||||
export function colorKeyToCssVar(key: string): string {
|
||||
// Insert hyphens before uppercase letters, then lowercase
|
||||
// Special handling for numbered keys like chart1 → --chart-1
|
||||
const kebab = key
|
||||
.replace(/([A-Z])/g, '-$1')
|
||||
.replace(/(\d+)/g, '-$1')
|
||||
.toLowerCase();
|
||||
return `--${kebab}`;
|
||||
}
|
||||
|
||||
function renderColorBlock(colors: ThemeColors): string {
|
||||
const lines: string[] = [];
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
lines.push(` ${colorKeyToCssVar(key)}: ${value};`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS for all theme families.
|
||||
*
|
||||
* Specificity:
|
||||
* html[data-theme-family="x"] → (0,1,0) — beats :root (0,0,1)
|
||||
* html.dark[data-theme-family="x"] → (0,2,0) — beats .dark (0,1,0)
|
||||
*
|
||||
* The globals.css :root/.dark blocks remain as fallback when data-theme-family is absent.
|
||||
*/
|
||||
export function renderThemeFamilyCSS(families: ThemeFamily[]): string {
|
||||
const blocks: string[] = [];
|
||||
|
||||
for (const family of families) {
|
||||
// Light mode
|
||||
blocks.push(
|
||||
`html[data-theme-family="${family.id}"] {\n${renderColorBlock(family.light)}\n}`
|
||||
);
|
||||
// Dark mode
|
||||
blocks.push(
|
||||
`html.dark[data-theme-family="${family.id}"] {\n${renderColorBlock(family.dark)}\n}`
|
||||
);
|
||||
}
|
||||
|
||||
return blocks.join('\n\n');
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Theme Family system types.
|
||||
*
|
||||
* A "theme family" controls the color palette independently of light/dark mode.
|
||||
* Mode is handled by next-themes; family is a second layer via CSS variables.
|
||||
*/
|
||||
|
||||
/** All CSS variable keys matching globals.css :root block (camelCase). */
|
||||
export interface ThemeColors {
|
||||
background: string;
|
||||
foreground: string;
|
||||
card: string;
|
||||
cardForeground: string;
|
||||
popover: string;
|
||||
popoverForeground: string;
|
||||
primary: string;
|
||||
primaryForeground: string;
|
||||
secondary: string;
|
||||
secondaryForeground: string;
|
||||
muted: string;
|
||||
mutedForeground: string;
|
||||
accent: string;
|
||||
accentForeground: string;
|
||||
destructive: string;
|
||||
border: string;
|
||||
input: string;
|
||||
ring: string;
|
||||
chart1: string;
|
||||
chart2: string;
|
||||
chart3: string;
|
||||
chart4: string;
|
||||
chart5: string;
|
||||
sidebar: string;
|
||||
sidebarForeground: string;
|
||||
sidebarPrimary: string;
|
||||
sidebarPrimaryForeground: string;
|
||||
sidebarAccent: string;
|
||||
sidebarAccentForeground: string;
|
||||
sidebarBorder: string;
|
||||
sidebarRing: string;
|
||||
}
|
||||
|
||||
/** Maps a code highlighting theme name for light and dark modes. */
|
||||
export interface CodeThemeMapping {
|
||||
light: string;
|
||||
dark: string;
|
||||
}
|
||||
|
||||
/** Full theme family definition (loaded from JSON). */
|
||||
export interface ThemeFamily {
|
||||
id: string;
|
||||
label: string;
|
||||
order: number;
|
||||
description?: string;
|
||||
light: ThemeColors;
|
||||
dark: ThemeColors;
|
||||
codeTheme?: CodeThemeMapping;
|
||||
/** Shiki theme names (used by ai-elements code blocks). */
|
||||
shikiTheme?: CodeThemeMapping;
|
||||
}
|
||||
|
||||
/** Lightweight metadata for client-side use (selector UI). */
|
||||
export interface ThemeFamilyMeta {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
/** Preview colors for the selector (primary light, primary dark, accent light, background light). */
|
||||
previewColors?: {
|
||||
primaryLight: string;
|
||||
primaryDark: string;
|
||||
accentLight: string;
|
||||
backgroundLight: string;
|
||||
};
|
||||
/** Code highlighting theme names (keys into react-syntax-highlighter theme maps). */
|
||||
codeTheme?: CodeThemeMapping;
|
||||
/** Shiki theme names for ai-elements code blocks. */
|
||||
shikiTheme?: CodeThemeMapping;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "default",
|
||||
"label": "Default",
|
||||
"order": 0,
|
||||
"description": "The default CodePilot color scheme",
|
||||
"codeTheme": {
|
||||
"light": "oneLight",
|
||||
"dark": "oneDark"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "github-light",
|
||||
"dark": "github-dark"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(1 0 0)",
|
||||
"foreground": "oklch(0.147 0.004 49.25)",
|
||||
"card": "oklch(1 0 0)",
|
||||
"cardForeground": "oklch(0.147 0.004 49.25)",
|
||||
"popover": "oklch(1 0 0)",
|
||||
"popoverForeground": "oklch(0.147 0.004 49.25)",
|
||||
"primary": "oklch(0.546 0.245 262.881)",
|
||||
"primaryForeground": "oklch(0.985 0.001 106.423)",
|
||||
"secondary": "oklch(0.97 0.001 106.424)",
|
||||
"secondaryForeground": "oklch(0.216 0.006 56.043)",
|
||||
"muted": "oklch(0.97 0.001 106.424)",
|
||||
"mutedForeground": "oklch(0.553 0.013 58.071)",
|
||||
"accent": "oklch(0.97 0.001 106.424)",
|
||||
"accentForeground": "oklch(0.216 0.006 56.043)",
|
||||
"destructive": "oklch(0.577 0.245 27.325)",
|
||||
"border": "oklch(0.923 0.003 48.717)",
|
||||
"input": "oklch(0.923 0.003 48.717)",
|
||||
"ring": "oklch(0.546 0.245 262.881)",
|
||||
"chart1": "oklch(0.546 0.245 262.881)",
|
||||
"chart2": "oklch(0.6 0.118 184.704)",
|
||||
"chart3": "oklch(0.398 0.07 227.392)",
|
||||
"chart4": "oklch(0.828 0.189 84.429)",
|
||||
"chart5": "oklch(0.769 0.188 70.08)",
|
||||
"sidebar": "oklch(0.985 0.001 106.423)",
|
||||
"sidebarForeground": "oklch(0.147 0.004 49.25)",
|
||||
"sidebarPrimary": "oklch(0.546 0.245 262.881)",
|
||||
"sidebarPrimaryForeground": "oklch(0.985 0.001 106.423)",
|
||||
"sidebarAccent": "oklch(0.97 0.001 106.424)",
|
||||
"sidebarAccentForeground": "oklch(0.216 0.006 56.043)",
|
||||
"sidebarBorder": "oklch(0.923 0.003 48.717)",
|
||||
"sidebarRing": "oklch(0.546 0.245 262.881)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.147 0.004 49.25)",
|
||||
"foreground": "oklch(0.985 0.001 106.423)",
|
||||
"card": "oklch(0.216 0.006 56.043)",
|
||||
"cardForeground": "oklch(0.985 0.001 106.423)",
|
||||
"popover": "oklch(0.216 0.006 56.043)",
|
||||
"popoverForeground": "oklch(0.985 0.001 106.423)",
|
||||
"primary": "oklch(0.623 0.214 259.815)",
|
||||
"primaryForeground": "oklch(0.985 0.001 106.423)",
|
||||
"secondary": "oklch(0.268 0.007 34.298)",
|
||||
"secondaryForeground": "oklch(0.985 0.001 106.423)",
|
||||
"muted": "oklch(0.268 0.007 34.298)",
|
||||
"mutedForeground": "oklch(0.709 0.01 56.259)",
|
||||
"accent": "oklch(0.268 0.007 34.298)",
|
||||
"accentForeground": "oklch(0.985 0.001 106.423)",
|
||||
"destructive": "oklch(0.704 0.191 22.216)",
|
||||
"border": "oklch(1 0 0 / 10%)",
|
||||
"input": "oklch(1 0 0 / 15%)",
|
||||
"ring": "oklch(0.623 0.214 259.815)",
|
||||
"chart1": "oklch(0.623 0.214 259.815)",
|
||||
"chart2": "oklch(0.696 0.17 162.48)",
|
||||
"chart3": "oklch(0.769 0.188 70.08)",
|
||||
"chart4": "oklch(0.627 0.265 303.9)",
|
||||
"chart5": "oklch(0.645 0.246 16.439)",
|
||||
"sidebar": "oklch(0.216 0.006 56.043)",
|
||||
"sidebarForeground": "oklch(0.985 0.001 106.423)",
|
||||
"sidebarPrimary": "oklch(0.623 0.214 259.815)",
|
||||
"sidebarPrimaryForeground": "oklch(0.985 0.001 106.423)",
|
||||
"sidebarAccent": "oklch(0.268 0.007 34.298)",
|
||||
"sidebarAccentForeground": "oklch(0.985 0.001 106.423)",
|
||||
"sidebarBorder": "oklch(1 0 0 / 10%)",
|
||||
"sidebarRing": "oklch(0.623 0.214 259.815)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "everforest",
|
||||
"label": "Everforest",
|
||||
"order": 4,
|
||||
"description": "Comfortable green-toned palette inspired by natural landscapes",
|
||||
"codeTheme": {
|
||||
"light": "gruvboxLight",
|
||||
"dark": "duotoneForest"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "everforest-light",
|
||||
"dark": "everforest-dark"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.974 0.026 90.1)",
|
||||
"foreground": "oklch(0.515 0.021 232.9)",
|
||||
"card": "oklch(0.952 0.031 98.9)",
|
||||
"cardForeground": "oklch(0.515 0.021 232.9)",
|
||||
"popover": "oklch(0.952 0.031 98.9)",
|
||||
"popoverForeground": "oklch(0.515 0.021 232.9)",
|
||||
"primary": "oklch(0.720 0.122 125.2)",
|
||||
"primaryForeground": "oklch(0.974 0.026 90.1)",
|
||||
"secondary": "oklch(0.937 0.031 98.9)",
|
||||
"secondaryForeground": "oklch(0.515 0.021 232.9)",
|
||||
"muted": "oklch(0.937 0.031 98.9)",
|
||||
"mutedForeground": "oklch(0.689 0.024 141.2)",
|
||||
"accent": "oklch(0.937 0.031 98.9)",
|
||||
"accentForeground": "oklch(0.515 0.021 232.9)",
|
||||
"destructive": "oklch(0.671 0.200 25.2)",
|
||||
"border": "oklch(0.892 0.028 98.1)",
|
||||
"input": "oklch(0.892 0.028 98.1)",
|
||||
"ring": "oklch(0.720 0.122 125.2)",
|
||||
"chart1": "oklch(0.720 0.122 125.2)",
|
||||
"chart2": "oklch(0.600 0.090 220.0)",
|
||||
"chart3": "oklch(0.680 0.140 60.0)",
|
||||
"chart4": "oklch(0.620 0.120 310.0)",
|
||||
"chart5": "oklch(0.550 0.100 180.0)",
|
||||
"sidebar": "oklch(0.974 0.026 90.1)",
|
||||
"sidebarForeground": "oklch(0.515 0.021 232.9)",
|
||||
"sidebarPrimary": "oklch(0.720 0.122 125.2)",
|
||||
"sidebarPrimaryForeground": "oklch(0.974 0.026 90.1)",
|
||||
"sidebarAccent": "oklch(0.937 0.031 98.9)",
|
||||
"sidebarAccentForeground": "oklch(0.515 0.021 232.9)",
|
||||
"sidebarBorder": "oklch(0.892 0.028 98.1)",
|
||||
"sidebarRing": "oklch(0.720 0.122 125.2)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.324 0.015 240.4)",
|
||||
"foreground": "oklch(0.830 0.041 86.1)",
|
||||
"card": "oklch(0.360 0.017 227.1)",
|
||||
"cardForeground": "oklch(0.830 0.041 86.1)",
|
||||
"popover": "oklch(0.360 0.017 227.1)",
|
||||
"popoverForeground": "oklch(0.830 0.041 86.1)",
|
||||
"primary": "oklch(0.773 0.091 125.8)",
|
||||
"primaryForeground": "oklch(0.324 0.015 240.4)",
|
||||
"secondary": "oklch(0.394 0.017 227.0)",
|
||||
"secondaryForeground": "oklch(0.830 0.041 86.1)",
|
||||
"muted": "oklch(0.394 0.017 227.0)",
|
||||
"mutedForeground": "oklch(0.647 0.020 155.6)",
|
||||
"accent": "oklch(0.394 0.017 227.0)",
|
||||
"accentForeground": "oklch(0.830 0.041 86.1)",
|
||||
"destructive": "oklch(0.709 0.128 19.6)",
|
||||
"border": "oklch(0.455 0.015 236.9)",
|
||||
"input": "oklch(0.394 0.017 227.0)",
|
||||
"ring": "oklch(0.773 0.091 125.8)",
|
||||
"chart1": "oklch(0.773 0.091 125.8)",
|
||||
"chart2": "oklch(0.650 0.090 220.0)",
|
||||
"chart3": "oklch(0.750 0.130 60.0)",
|
||||
"chart4": "oklch(0.680 0.120 310.0)",
|
||||
"chart5": "oklch(0.600 0.100 180.0)",
|
||||
"sidebar": "oklch(0.324 0.015 240.4)",
|
||||
"sidebarForeground": "oklch(0.830 0.041 86.1)",
|
||||
"sidebarPrimary": "oklch(0.773 0.091 125.8)",
|
||||
"sidebarPrimaryForeground": "oklch(0.324 0.015 240.4)",
|
||||
"sidebarAccent": "oklch(0.394 0.017 227.0)",
|
||||
"sidebarAccentForeground": "oklch(0.830 0.041 86.1)",
|
||||
"sidebarBorder": "oklch(0.268 0.011 237.1)",
|
||||
"sidebarRing": "oklch(0.773 0.091 125.8)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "github",
|
||||
"label": "GitHub",
|
||||
"order": 1,
|
||||
"description": "Clean and familiar, inspired by GitHub's Primer design system",
|
||||
"codeTheme": {
|
||||
"light": "ghcolors",
|
||||
"dark": "vscDarkPlus"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "github-light-default",
|
||||
"dark": "github-dark-default"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(1.000 0 0)",
|
||||
"foreground": "oklch(0.254 0.011 254.0)",
|
||||
"card": "oklch(1.000 0 0)",
|
||||
"cardForeground": "oklch(0.254 0.011 254.0)",
|
||||
"popover": "oklch(1.000 0 0)",
|
||||
"popoverForeground": "oklch(0.254 0.011 254.0)",
|
||||
"primary": "oklch(0.540 0.191 257.5)",
|
||||
"primaryForeground": "oklch(1.000 0 0)",
|
||||
"secondary": "oklch(0.947 0.007 247.9)",
|
||||
"secondaryForeground": "oklch(0.338 0.015 252.3)",
|
||||
"muted": "oklch(0.947 0.007 247.9)",
|
||||
"mutedForeground": "oklch(0.531 0.017 251.3)",
|
||||
"accent": "oklch(0.978 0.003 247.9)",
|
||||
"accentForeground": "oklch(0.338 0.015 252.3)",
|
||||
"destructive": "oklch(0.552 0.205 24.5)",
|
||||
"border": "oklch(0.876 0.012 248.0)",
|
||||
"input": "oklch(0.876 0.012 248.0)",
|
||||
"ring": "oklch(0.540 0.191 257.5)",
|
||||
"chart1": "oklch(0.540 0.191 257.5)",
|
||||
"chart2": "oklch(0.546 0.147 146.3)",
|
||||
"chart3": "oklch(0.700 0.160 48.0)",
|
||||
"chart4": "oklch(0.620 0.165 310.0)",
|
||||
"chart5": "oklch(0.600 0.120 185.0)",
|
||||
"sidebar": "oklch(0.978 0.003 247.9)",
|
||||
"sidebarForeground": "oklch(0.254 0.011 254.0)",
|
||||
"sidebarPrimary": "oklch(0.540 0.191 257.5)",
|
||||
"sidebarPrimaryForeground": "oklch(1.000 0 0)",
|
||||
"sidebarAccent": "oklch(0.947 0.007 247.9)",
|
||||
"sidebarAccentForeground": "oklch(0.338 0.015 252.3)",
|
||||
"sidebarBorder": "oklch(0.876 0.012 248.0)",
|
||||
"sidebarRing": "oklch(0.540 0.191 257.5)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.176 0.014 258.4)",
|
||||
"foreground": "oklch(0.943 0.011 243.7)",
|
||||
"card": "oklch(0.220 0.016 256.8)",
|
||||
"cardForeground": "oklch(0.943 0.011 243.7)",
|
||||
"popover": "oklch(0.220 0.016 256.8)",
|
||||
"popoverForeground": "oklch(0.943 0.011 243.7)",
|
||||
"primary": "oklch(0.569 0.202 259.7)",
|
||||
"primaryForeground": "oklch(1.000 0 0)",
|
||||
"secondary": "oklch(0.267 0.015 256.8)",
|
||||
"secondaryForeground": "oklch(0.857 0.014 248.0)",
|
||||
"muted": "oklch(0.267 0.015 256.8)",
|
||||
"mutedForeground": "oklch(0.662 0.018 250.9)",
|
||||
"accent": "oklch(0.246 0.015 256.8)",
|
||||
"accentForeground": "oklch(0.943 0.011 243.7)",
|
||||
"destructive": "oklch(0.665 0.205 27.0)",
|
||||
"border": "oklch(0.330 0.015 252.3)",
|
||||
"input": "oklch(0.330 0.015 252.3)",
|
||||
"ring": "oklch(0.569 0.202 259.7)",
|
||||
"chart1": "oklch(0.569 0.202 259.7)",
|
||||
"chart2": "oklch(0.600 0.140 146.0)",
|
||||
"chart3": "oklch(0.750 0.150 48.0)",
|
||||
"chart4": "oklch(0.680 0.170 310.0)",
|
||||
"chart5": "oklch(0.650 0.120 185.0)",
|
||||
"sidebar": "oklch(0.104 0.019 248.3)",
|
||||
"sidebarForeground": "oklch(0.943 0.011 243.7)",
|
||||
"sidebarPrimary": "oklch(0.569 0.202 259.7)",
|
||||
"sidebarPrimaryForeground": "oklch(1.000 0 0)",
|
||||
"sidebarAccent": "oklch(0.267 0.015 256.8)",
|
||||
"sidebarAccentForeground": "oklch(0.943 0.011 243.7)",
|
||||
"sidebarBorder": "oklch(0.330 0.015 252.3)",
|
||||
"sidebarRing": "oklch(0.569 0.202 259.7)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "horizon",
|
||||
"label": "Horizon",
|
||||
"order": 10,
|
||||
"description": "Warm sunset palette with rosy pink accents",
|
||||
"codeTheme": {
|
||||
"light": "duotoneLight",
|
||||
"dark": "materialDark"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "github-light",
|
||||
"dark": "horizon"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.965 0.015 33.1)",
|
||||
"foreground": "oklch(0.237 0.016 274.1)",
|
||||
"card": "oklch(0.913 0.038 36.2)",
|
||||
"cardForeground": "oklch(0.237 0.016 274.1)",
|
||||
"popover": "oklch(0.913 0.038 36.2)",
|
||||
"popoverForeground": "oklch(0.237 0.016 274.1)",
|
||||
"primary": "oklch(0.567 0.222 18.8)",
|
||||
"primaryForeground": "oklch(0.965 0.015 33.1)",
|
||||
"secondary": "oklch(0.883 0.045 29.9)",
|
||||
"secondaryForeground": "oklch(0.237 0.016 274.1)",
|
||||
"muted": "oklch(0.883 0.045 29.9)",
|
||||
"mutedForeground": "oklch(0.553 0.056 280.7)",
|
||||
"accent": "oklch(0.913 0.038 36.2)",
|
||||
"accentForeground": "oklch(0.237 0.016 274.1)",
|
||||
"destructive": "oklch(0.567 0.222 18.8)",
|
||||
"border": "oklch(0.837 0.060 32.6)",
|
||||
"input": "oklch(0.913 0.038 36.2)",
|
||||
"ring": "oklch(0.567 0.222 18.8)",
|
||||
"chart1": "oklch(0.567 0.222 18.8)",
|
||||
"chart2": "oklch(0.781 0.163 162.1)",
|
||||
"chart3": "oklch(0.834 0.091 47.9)",
|
||||
"chart4": "oklch(0.732 0.123 215.7)",
|
||||
"chart5": "oklch(0.676 0.157 312.8)",
|
||||
"sidebar": "oklch(0.913 0.038 36.2)",
|
||||
"sidebarForeground": "oklch(0.237 0.016 274.1)",
|
||||
"sidebarPrimary": "oklch(0.567 0.222 18.8)",
|
||||
"sidebarPrimaryForeground": "oklch(0.965 0.015 33.1)",
|
||||
"sidebarAccent": "oklch(0.883 0.045 29.9)",
|
||||
"sidebarAccentForeground": "oklch(0.237 0.016 274.1)",
|
||||
"sidebarBorder": "oklch(0.837 0.060 32.6)",
|
||||
"sidebarRing": "oklch(0.567 0.222 18.8)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.237 0.016 274.1)",
|
||||
"foreground": "oklch(0.880 0.004 236.5)",
|
||||
"card": "oklch(0.267 0.021 276.8)",
|
||||
"cardForeground": "oklch(0.880 0.004 236.5)",
|
||||
"popover": "oklch(0.267 0.021 276.8)",
|
||||
"popoverForeground": "oklch(0.880 0.004 236.5)",
|
||||
"primary": "oklch(0.653 0.186 9.0)",
|
||||
"primaryForeground": "oklch(0.965 0.015 33.1)",
|
||||
"secondary": "oklch(0.313 0.025 278.5)",
|
||||
"secondaryForeground": "oklch(0.880 0.004 236.5)",
|
||||
"muted": "oklch(0.313 0.025 278.5)",
|
||||
"mutedForeground": "oklch(0.553 0.056 280.7)",
|
||||
"accent": "oklch(0.267 0.021 276.8)",
|
||||
"accentForeground": "oklch(0.880 0.004 236.5)",
|
||||
"destructive": "oklch(0.653 0.186 9.0)",
|
||||
"border": "oklch(0.313 0.025 278.5)",
|
||||
"input": "oklch(0.267 0.021 276.8)",
|
||||
"ring": "oklch(0.653 0.186 9.0)",
|
||||
"chart1": "oklch(0.653 0.186 9.0)",
|
||||
"chart2": "oklch(0.781 0.163 162.1)",
|
||||
"chart3": "oklch(0.834 0.091 47.9)",
|
||||
"chart4": "oklch(0.732 0.123 215.7)",
|
||||
"chart5": "oklch(0.676 0.157 312.8)",
|
||||
"sidebar": "oklch(0.237 0.016 274.1)",
|
||||
"sidebarForeground": "oklch(0.880 0.004 236.5)",
|
||||
"sidebarPrimary": "oklch(0.653 0.186 9.0)",
|
||||
"sidebarPrimaryForeground": "oklch(0.965 0.015 33.1)",
|
||||
"sidebarAccent": "oklch(0.267 0.021 276.8)",
|
||||
"sidebarAccentForeground": "oklch(0.880 0.004 236.5)",
|
||||
"sidebarBorder": "oklch(0.267 0.021 276.8)",
|
||||
"sidebarRing": "oklch(0.653 0.186 9.0)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "kanagawa",
|
||||
"label": "Kanagawa",
|
||||
"order": 6,
|
||||
"description": "Ink-wash palette with warm parchment tones",
|
||||
"codeTheme": {
|
||||
"light": "vs",
|
||||
"dark": "duotoneEarth"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "kanagawa-lotus",
|
||||
"dark": "kanagawa-wave"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.936 0.063 101.6)",
|
||||
"foreground": "oklch(0.452 0.026 285.3)",
|
||||
"card": "oklch(0.887 0.077 98.0)",
|
||||
"cardForeground": "oklch(0.452 0.026 285.3)",
|
||||
"popover": "oklch(0.887 0.077 98.0)",
|
||||
"popoverForeground": "oklch(0.452 0.026 285.3)",
|
||||
"primary": "oklch(0.521 0.086 261.5)",
|
||||
"primaryForeground": "oklch(0.958 0.057 101.0)",
|
||||
"secondary": "oklch(0.868 0.055 99.4)",
|
||||
"secondaryForeground": "oklch(0.452 0.026 285.3)",
|
||||
"muted": "oklch(0.868 0.055 99.4)",
|
||||
"mutedForeground": "oklch(0.628 0.013 101.6)",
|
||||
"accent": "oklch(0.887 0.077 98.0)",
|
||||
"accentForeground": "oklch(0.452 0.026 285.3)",
|
||||
"destructive": "oklch(0.571 0.171 16.5)",
|
||||
"border": "oklch(0.831 0.083 96.4)",
|
||||
"input": "oklch(0.887 0.077 98.0)",
|
||||
"ring": "oklch(0.521 0.086 261.5)",
|
||||
"chart1": "oklch(0.521 0.086 261.5)",
|
||||
"chart2": "oklch(0.748 0.112 128.4)",
|
||||
"chart3": "oklch(0.833 0.090 81.6)",
|
||||
"chart4": "oklch(0.638 0.087 301.7)",
|
||||
"chart5": "oklch(0.571 0.171 16.5)",
|
||||
"sidebar": "oklch(0.958 0.057 101.0)",
|
||||
"sidebarForeground": "oklch(0.452 0.026 285.3)",
|
||||
"sidebarPrimary": "oklch(0.521 0.086 261.5)",
|
||||
"sidebarPrimaryForeground": "oklch(0.958 0.057 101.0)",
|
||||
"sidebarAccent": "oklch(0.887 0.077 98.0)",
|
||||
"sidebarAccentForeground": "oklch(0.452 0.026 285.3)",
|
||||
"sidebarBorder": "oklch(0.831 0.083 96.4)",
|
||||
"sidebarRing": "oklch(0.521 0.086 261.5)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.243 0.017 285.1)",
|
||||
"foreground": "oklch(0.876 0.039 99.1)",
|
||||
"card": "oklch(0.291 0.024 284.8)",
|
||||
"cardForeground": "oklch(0.876 0.039 99.1)",
|
||||
"popover": "oklch(0.291 0.024 284.8)",
|
||||
"popoverForeground": "oklch(0.876 0.039 99.1)",
|
||||
"primary": "oklch(0.694 0.095 263.7)",
|
||||
"primaryForeground": "oklch(0.243 0.017 285.1)",
|
||||
"secondary": "oklch(0.340 0.028 284.8)",
|
||||
"secondaryForeground": "oklch(0.876 0.039 99.1)",
|
||||
"muted": "oklch(0.340 0.028 284.8)",
|
||||
"mutedForeground": "oklch(0.547 0.012 101.0)",
|
||||
"accent": "oklch(0.291 0.024 284.8)",
|
||||
"accentForeground": "oklch(0.876 0.039 99.1)",
|
||||
"destructive": "oklch(0.559 0.167 23.1)",
|
||||
"border": "oklch(0.340 0.028 284.8)",
|
||||
"input": "oklch(0.291 0.024 284.8)",
|
||||
"ring": "oklch(0.694 0.095 263.7)",
|
||||
"chart1": "oklch(0.694 0.095 263.7)",
|
||||
"chart2": "oklch(0.748 0.112 128.4)",
|
||||
"chart3": "oklch(0.833 0.090 81.6)",
|
||||
"chart4": "oklch(0.638 0.087 301.7)",
|
||||
"chart5": "oklch(0.693 0.197 22.1)",
|
||||
"sidebar": "oklch(0.243 0.017 285.1)",
|
||||
"sidebarForeground": "oklch(0.876 0.039 99.1)",
|
||||
"sidebarPrimary": "oklch(0.694 0.095 263.7)",
|
||||
"sidebarPrimaryForeground": "oklch(0.243 0.017 285.1)",
|
||||
"sidebarAccent": "oklch(0.291 0.024 284.8)",
|
||||
"sidebarAccentForeground": "oklch(0.876 0.039 99.1)",
|
||||
"sidebarBorder": "oklch(0.291 0.024 284.8)",
|
||||
"sidebarRing": "oklch(0.694 0.095 263.7)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "night-owl",
|
||||
"label": "Night Owl",
|
||||
"order": 7,
|
||||
"description": "Deep navy palette for night-time coding",
|
||||
"codeTheme": {
|
||||
"light": "coy",
|
||||
"dark": "nightOwl"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "night-owl-light",
|
||||
"dark": "night-owl"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.988 0.000 89.9)",
|
||||
"foreground": "oklch(0.377 0.034 286.9)",
|
||||
"card": "oklch(0.955 0.000 89.9)",
|
||||
"cardForeground": "oklch(0.377 0.034 286.9)",
|
||||
"popover": "oklch(0.955 0.000 89.9)",
|
||||
"popoverForeground": "oklch(0.377 0.034 286.9)",
|
||||
"primary": "oklch(0.582 0.156 263.2)",
|
||||
"primaryForeground": "oklch(0.988 0.000 89.9)",
|
||||
"secondary": "oklch(0.907 0.000 89.9)",
|
||||
"secondaryForeground": "oklch(0.377 0.034 286.9)",
|
||||
"muted": "oklch(0.907 0.000 89.9)",
|
||||
"mutedForeground": "oklch(0.714 0.030 228.3)",
|
||||
"accent": "oklch(0.955 0.000 89.9)",
|
||||
"accentForeground": "oklch(0.377 0.034 286.9)",
|
||||
"destructive": "oklch(0.601 0.198 26.1)",
|
||||
"border": "oklch(0.885 0.000 89.9)",
|
||||
"input": "oklch(0.955 0.000 89.9)",
|
||||
"ring": "oklch(0.582 0.156 263.2)",
|
||||
"chart1": "oklch(0.582 0.156 263.2)",
|
||||
"chart2": "oklch(0.646 0.103 186.6)",
|
||||
"chart3": "oklch(0.761 0.155 88.2)",
|
||||
"chart4": "oklch(0.564 0.186 311.7)",
|
||||
"chart5": "oklch(0.577 0.135 22.2)",
|
||||
"sidebar": "oklch(0.955 0.000 89.9)",
|
||||
"sidebarForeground": "oklch(0.377 0.034 286.9)",
|
||||
"sidebarPrimary": "oklch(0.582 0.156 263.2)",
|
||||
"sidebarPrimaryForeground": "oklch(0.988 0.000 89.9)",
|
||||
"sidebarAccent": "oklch(0.907 0.000 89.9)",
|
||||
"sidebarAccentForeground": "oklch(0.377 0.034 286.9)",
|
||||
"sidebarBorder": "oklch(0.885 0.000 89.9)",
|
||||
"sidebarRing": "oklch(0.582 0.156 263.2)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.193 0.045 244.0)",
|
||||
"foreground": "oklch(0.898 0.020 260.2)",
|
||||
"card": "oklch(0.273 0.058 247.3)",
|
||||
"cardForeground": "oklch(0.898 0.020 260.2)",
|
||||
"popover": "oklch(0.273 0.058 247.3)",
|
||||
"popoverForeground": "oklch(0.898 0.020 260.2)",
|
||||
"primary": "oklch(0.744 0.131 264.3)",
|
||||
"primaryForeground": "oklch(0.193 0.045 244.0)",
|
||||
"secondary": "oklch(0.341 0.056 244.6)",
|
||||
"secondaryForeground": "oklch(0.898 0.020 260.2)",
|
||||
"muted": "oklch(0.341 0.056 244.6)",
|
||||
"mutedForeground": "oklch(0.553 0.024 196.5)",
|
||||
"accent": "oklch(0.273 0.058 247.3)",
|
||||
"accentForeground": "oklch(0.898 0.020 260.2)",
|
||||
"destructive": "oklch(0.654 0.193 25.1)",
|
||||
"border": "oklch(0.287 0.051 244.4)",
|
||||
"input": "oklch(0.273 0.058 247.3)",
|
||||
"ring": "oklch(0.744 0.131 264.3)",
|
||||
"chart1": "oklch(0.744 0.131 264.3)",
|
||||
"chart2": "oklch(0.780 0.204 150.7)",
|
||||
"chart3": "oklch(0.834 0.153 127.4)",
|
||||
"chart4": "oklch(0.743 0.135 311.1)",
|
||||
"chart5": "oklch(0.749 0.138 37.5)",
|
||||
"sidebar": "oklch(0.193 0.045 244.0)",
|
||||
"sidebarForeground": "oklch(0.898 0.020 260.2)",
|
||||
"sidebarPrimary": "oklch(0.744 0.131 264.3)",
|
||||
"sidebarPrimaryForeground": "oklch(0.193 0.045 244.0)",
|
||||
"sidebarAccent": "oklch(0.273 0.058 247.3)",
|
||||
"sidebarAccentForeground": "oklch(0.898 0.020 260.2)",
|
||||
"sidebarBorder": "oklch(0.273 0.058 247.3)",
|
||||
"sidebarRing": "oklch(0.744 0.131 264.3)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "nord",
|
||||
"label": "Nord",
|
||||
"order": 5,
|
||||
"description": "Arctic, north-bluish color palette from the Polar Night",
|
||||
"codeTheme": {
|
||||
"light": "coldarkCold",
|
||||
"dark": "nord"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "github-light",
|
||||
"dark": "nord"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.951 0.007 260.7)",
|
||||
"foreground": "oklch(0.324 0.023 264.2)",
|
||||
"card": "oklch(0.933 0.010 261.8)",
|
||||
"cardForeground": "oklch(0.324 0.023 264.2)",
|
||||
"popover": "oklch(0.933 0.010 261.8)",
|
||||
"popoverForeground": "oklch(0.324 0.023 264.2)",
|
||||
"primary": "oklch(0.594 0.077 254.0)",
|
||||
"primaryForeground": "oklch(0.951 0.007 260.7)",
|
||||
"secondary": "oklch(0.899 0.016 262.7)",
|
||||
"secondaryForeground": "oklch(0.324 0.023 264.2)",
|
||||
"muted": "oklch(0.899 0.016 262.7)",
|
||||
"mutedForeground": "oklch(0.452 0.035 264.1)",
|
||||
"accent": "oklch(0.899 0.016 262.7)",
|
||||
"accentForeground": "oklch(0.324 0.023 264.2)",
|
||||
"destructive": "oklch(0.606 0.121 15.3)",
|
||||
"border": "oklch(0.899 0.016 262.7)",
|
||||
"input": "oklch(0.933 0.010 261.8)",
|
||||
"ring": "oklch(0.594 0.077 254.0)",
|
||||
"chart1": "oklch(0.594 0.077 254.0)",
|
||||
"chart2": "oklch(0.775 0.062 217.5)",
|
||||
"chart3": "oklch(0.606 0.121 15.3)",
|
||||
"chart4": "oklch(0.730 0.070 90.0)",
|
||||
"chart5": "oklch(0.650 0.100 170.0)",
|
||||
"sidebar": "oklch(0.933 0.010 261.8)",
|
||||
"sidebarForeground": "oklch(0.324 0.023 264.2)",
|
||||
"sidebarPrimary": "oklch(0.594 0.077 254.0)",
|
||||
"sidebarPrimaryForeground": "oklch(0.951 0.007 260.7)",
|
||||
"sidebarAccent": "oklch(0.899 0.016 262.7)",
|
||||
"sidebarAccentForeground": "oklch(0.324 0.023 264.2)",
|
||||
"sidebarBorder": "oklch(0.899 0.016 262.7)",
|
||||
"sidebarRing": "oklch(0.594 0.077 254.0)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.324 0.023 264.2)",
|
||||
"foreground": "oklch(0.899 0.016 262.7)",
|
||||
"card": "oklch(0.379 0.029 266.5)",
|
||||
"cardForeground": "oklch(0.899 0.016 262.7)",
|
||||
"popover": "oklch(0.379 0.029 266.5)",
|
||||
"popoverForeground": "oklch(0.899 0.016 262.7)",
|
||||
"primary": "oklch(0.775 0.062 217.5)",
|
||||
"primaryForeground": "oklch(0.324 0.023 264.2)",
|
||||
"secondary": "oklch(0.416 0.032 264.1)",
|
||||
"secondaryForeground": "oklch(0.899 0.016 262.7)",
|
||||
"muted": "oklch(0.416 0.032 264.1)",
|
||||
"mutedForeground": "oklch(0.697 0.059 248.7)",
|
||||
"accent": "oklch(0.416 0.032 264.1)",
|
||||
"accentForeground": "oklch(0.899 0.016 262.7)",
|
||||
"destructive": "oklch(0.606 0.121 15.3)",
|
||||
"border": "oklch(0.379 0.029 266.5)",
|
||||
"input": "oklch(0.379 0.029 266.5)",
|
||||
"ring": "oklch(0.775 0.062 217.5)",
|
||||
"chart1": "oklch(0.775 0.062 217.5)",
|
||||
"chart2": "oklch(0.697 0.059 248.7)",
|
||||
"chart3": "oklch(0.606 0.121 15.3)",
|
||||
"chart4": "oklch(0.770 0.070 90.0)",
|
||||
"chart5": "oklch(0.700 0.100 170.0)",
|
||||
"sidebar": "oklch(0.324 0.023 264.2)",
|
||||
"sidebarForeground": "oklch(0.899 0.016 262.7)",
|
||||
"sidebarPrimary": "oklch(0.775 0.062 217.5)",
|
||||
"sidebarPrimaryForeground": "oklch(0.324 0.023 264.2)",
|
||||
"sidebarAccent": "oklch(0.416 0.032 264.1)",
|
||||
"sidebarAccentForeground": "oklch(0.899 0.016 262.7)",
|
||||
"sidebarBorder": "oklch(0.379 0.029 266.5)",
|
||||
"sidebarRing": "oklch(0.775 0.062 217.5)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "poimandres",
|
||||
"label": "Poimandres",
|
||||
"order": 8,
|
||||
"description": "Muted dark palette with soft teal and lavender accents",
|
||||
"codeTheme": {
|
||||
"light": "coldarkCold",
|
||||
"dark": "coldarkDark"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "min-light",
|
||||
"dark": "poimandres"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.980 0.005 286.3)",
|
||||
"foreground": "oklch(0.324 0.023 274.8)",
|
||||
"card": "oklch(0.950 0.005 286.3)",
|
||||
"cardForeground": "oklch(0.324 0.023 274.8)",
|
||||
"popover": "oklch(0.950 0.005 286.3)",
|
||||
"popoverForeground": "oklch(0.324 0.023 274.8)",
|
||||
"primary": "oklch(0.528 0.096 248.8)",
|
||||
"primaryForeground": "oklch(0.980 0.005 286.3)",
|
||||
"secondary": "oklch(0.899 0.008 286.2)",
|
||||
"secondaryForeground": "oklch(0.324 0.023 274.8)",
|
||||
"muted": "oklch(0.899 0.008 286.2)",
|
||||
"mutedForeground": "oklch(0.593 0.051 276.7)",
|
||||
"accent": "oklch(0.950 0.005 286.3)",
|
||||
"accentForeground": "oklch(0.324 0.023 274.8)",
|
||||
"destructive": "oklch(0.562 0.136 354.3)",
|
||||
"border": "oklch(0.872 0.011 286.2)",
|
||||
"input": "oklch(0.950 0.005 286.3)",
|
||||
"ring": "oklch(0.528 0.096 248.8)",
|
||||
"chart1": "oklch(0.528 0.096 248.8)",
|
||||
"chart2": "oklch(0.646 0.103 186.6)",
|
||||
"chart3": "oklch(0.562 0.136 354.3)",
|
||||
"chart4": "oklch(0.593 0.051 276.7)",
|
||||
"chart5": "oklch(0.761 0.155 88.2)",
|
||||
"sidebar": "oklch(0.950 0.005 286.3)",
|
||||
"sidebarForeground": "oklch(0.324 0.023 274.8)",
|
||||
"sidebarPrimary": "oklch(0.528 0.096 248.8)",
|
||||
"sidebarPrimaryForeground": "oklch(0.980 0.005 286.3)",
|
||||
"sidebarAccent": "oklch(0.899 0.008 286.2)",
|
||||
"sidebarAccentForeground": "oklch(0.324 0.023 274.8)",
|
||||
"sidebarBorder": "oklch(0.872 0.011 286.2)",
|
||||
"sidebarRing": "oklch(0.528 0.096 248.8)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.237 0.020 271.8)",
|
||||
"foreground": "oklch(0.751 0.048 277.1)",
|
||||
"card": "oklch(0.289 0.024 264.1)",
|
||||
"cardForeground": "oklch(0.751 0.048 277.1)",
|
||||
"popover": "oklch(0.289 0.024 264.1)",
|
||||
"popoverForeground": "oklch(0.751 0.048 277.1)",
|
||||
"primary": "oklch(0.863 0.071 247.4)",
|
||||
"primaryForeground": "oklch(0.237 0.020 271.8)",
|
||||
"secondary": "oklch(0.324 0.023 274.8)",
|
||||
"secondaryForeground": "oklch(0.751 0.048 277.1)",
|
||||
"muted": "oklch(0.324 0.023 274.8)",
|
||||
"mutedForeground": "oklch(0.593 0.051 276.7)",
|
||||
"accent": "oklch(0.289 0.024 264.1)",
|
||||
"accentForeground": "oklch(0.949 0.020 245.6)",
|
||||
"destructive": "oklch(0.654 0.146 349.8)",
|
||||
"border": "oklch(0.324 0.023 274.8)",
|
||||
"input": "oklch(0.289 0.024 264.1)",
|
||||
"ring": "oklch(0.863 0.071 247.4)",
|
||||
"chart1": "oklch(0.863 0.071 247.4)",
|
||||
"chart2": "oklch(0.837 0.125 176.5)",
|
||||
"chart3": "oklch(0.944 0.040 323.3)",
|
||||
"chart4": "oklch(0.756 0.061 247.1)",
|
||||
"chart5": "oklch(0.654 0.146 349.8)",
|
||||
"sidebar": "oklch(0.216 0.018 275.0)",
|
||||
"sidebarForeground": "oklch(0.751 0.048 277.1)",
|
||||
"sidebarPrimary": "oklch(0.863 0.071 247.4)",
|
||||
"sidebarPrimaryForeground": "oklch(0.237 0.020 271.8)",
|
||||
"sidebarAccent": "oklch(0.289 0.024 264.1)",
|
||||
"sidebarAccentForeground": "oklch(0.949 0.020 245.6)",
|
||||
"sidebarBorder": "oklch(0.289 0.024 264.1)",
|
||||
"sidebarRing": "oklch(0.863 0.071 247.4)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "rose-pine",
|
||||
"label": "Rosé Pine",
|
||||
"order": 2,
|
||||
"description": "All natural pine, faux fur and a bit of soho vibes",
|
||||
"codeTheme": {
|
||||
"light": "duotoneLight",
|
||||
"dark": "duotoneSea"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "rose-pine-dawn",
|
||||
"dark": "rose-pine"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.970 0.011 71.9)",
|
||||
"foreground": "oklch(0.460 0.063 289.6)",
|
||||
"card": "oklch(0.987 0.011 76.6)",
|
||||
"cardForeground": "oklch(0.460 0.063 289.6)",
|
||||
"popover": "oklch(0.987 0.011 76.6)",
|
||||
"popoverForeground": "oklch(0.460 0.063 289.6)",
|
||||
"primary": "oklch(0.696 0.106 23.0)",
|
||||
"primaryForeground": "oklch(0.970 0.011 71.9)",
|
||||
"secondary": "oklch(0.939 0.015 64.3)",
|
||||
"secondaryForeground": "oklch(0.460 0.063 289.6)",
|
||||
"muted": "oklch(0.939 0.015 64.3)",
|
||||
"mutedForeground": "oklch(0.673 0.027 298.4)",
|
||||
"accent": "oklch(0.950 0.010 58.2)",
|
||||
"accentForeground": "oklch(0.460 0.063 289.6)",
|
||||
"destructive": "oklch(0.599 0.107 2.7)",
|
||||
"border": "oklch(0.892 0.006 31.1)",
|
||||
"input": "oklch(0.892 0.006 31.1)",
|
||||
"ring": "oklch(0.696 0.106 23.0)",
|
||||
"chart1": "oklch(0.696 0.106 23.0)",
|
||||
"chart2": "oklch(0.629 0.066 210.1)",
|
||||
"chart3": "oklch(0.617 0.074 305.7)",
|
||||
"chart4": "oklch(0.755 0.146 69.7)",
|
||||
"chart5": "oklch(0.491 0.077 228.0)",
|
||||
"sidebar": "oklch(0.970 0.011 71.9)",
|
||||
"sidebarForeground": "oklch(0.460 0.063 289.6)",
|
||||
"sidebarPrimary": "oklch(0.696 0.106 23.0)",
|
||||
"sidebarPrimaryForeground": "oklch(0.970 0.011 71.9)",
|
||||
"sidebarAccent": "oklch(0.939 0.015 64.3)",
|
||||
"sidebarAccentForeground": "oklch(0.460 0.063 289.6)",
|
||||
"sidebarBorder": "oklch(0.892 0.006 31.1)",
|
||||
"sidebarRing": "oklch(0.696 0.106 23.0)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.213 0.025 291.1)",
|
||||
"foreground": "oklch(0.909 0.030 290.0)",
|
||||
"card": "oklch(0.271 0.042 289.2)",
|
||||
"cardForeground": "oklch(0.909 0.030 290.0)",
|
||||
"popover": "oklch(0.271 0.042 289.2)",
|
||||
"popoverForeground": "oklch(0.909 0.030 290.0)",
|
||||
"primary": "oklch(0.836 0.054 21.1)",
|
||||
"primaryForeground": "oklch(0.213 0.025 291.1)",
|
||||
"secondary": "oklch(0.309 0.036 291.0)",
|
||||
"secondaryForeground": "oklch(0.909 0.030 290.0)",
|
||||
"muted": "oklch(0.309 0.036 291.0)",
|
||||
"mutedForeground": "oklch(0.538 0.044 291.4)",
|
||||
"accent": "oklch(0.251 0.026 287.6)",
|
||||
"accentForeground": "oklch(0.909 0.030 290.0)",
|
||||
"destructive": "oklch(0.698 0.156 4.2)",
|
||||
"border": "oklch(0.372 0.036 291.1)",
|
||||
"input": "oklch(0.372 0.036 291.1)",
|
||||
"ring": "oklch(0.836 0.054 21.1)",
|
||||
"chart1": "oklch(0.836 0.054 21.1)",
|
||||
"chart2": "oklch(0.822 0.054 209.6)",
|
||||
"chart3": "oklch(0.776 0.095 305.0)",
|
||||
"chart4": "oklch(0.843 0.110 74.6)",
|
||||
"chart5": "oklch(0.528 0.079 227.7)",
|
||||
"sidebar": "oklch(0.213 0.025 291.1)",
|
||||
"sidebarForeground": "oklch(0.909 0.030 290.0)",
|
||||
"sidebarPrimary": "oklch(0.836 0.054 21.1)",
|
||||
"sidebarPrimaryForeground": "oklch(0.213 0.025 291.1)",
|
||||
"sidebarAccent": "oklch(0.251 0.026 287.6)",
|
||||
"sidebarAccentForeground": "oklch(0.909 0.030 290.0)",
|
||||
"sidebarBorder": "oklch(0.372 0.036 291.1)",
|
||||
"sidebarRing": "oklch(0.836 0.054 21.1)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "synthwave84",
|
||||
"label": "SynthWave '84",
|
||||
"order": 11,
|
||||
"description": "Retro-futuristic neon palette with purple haze",
|
||||
"codeTheme": {
|
||||
"light": "prism",
|
||||
"dark": "synthwave84"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "min-light",
|
||||
"dark": "synthwave-84"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.966 0.022 309.6)",
|
||||
"foreground": "oklch(0.270 0.045 300.7)",
|
||||
"card": "oklch(0.932 0.027 307.0)",
|
||||
"cardForeground": "oklch(0.270 0.045 300.7)",
|
||||
"popover": "oklch(0.932 0.027 307.0)",
|
||||
"popoverForeground": "oklch(0.270 0.045 300.7)",
|
||||
"primary": "oklch(0.560 0.183 333.8)",
|
||||
"primaryForeground": "oklch(0.966 0.022 309.6)",
|
||||
"secondary": "oklch(0.887 0.037 306.4)",
|
||||
"secondaryForeground": "oklch(0.270 0.045 300.7)",
|
||||
"muted": "oklch(0.887 0.037 306.4)",
|
||||
"mutedForeground": "oklch(0.517 0.069 300.0)",
|
||||
"accent": "oklch(0.932 0.027 307.0)",
|
||||
"accentForeground": "oklch(0.270 0.045 300.7)",
|
||||
"destructive": "oklch(0.555 0.206 18.8)",
|
||||
"border": "oklch(0.851 0.040 304.9)",
|
||||
"input": "oklch(0.932 0.027 307.0)",
|
||||
"ring": "oklch(0.560 0.183 333.8)",
|
||||
"chart1": "oklch(0.560 0.183 333.8)",
|
||||
"chart2": "oklch(0.781 0.163 162.1)",
|
||||
"chart3": "oklch(0.761 0.155 88.2)",
|
||||
"chart4": "oklch(0.582 0.156 263.2)",
|
||||
"chart5": "oklch(0.574 0.148 30.4)",
|
||||
"sidebar": "oklch(0.932 0.027 307.0)",
|
||||
"sidebarForeground": "oklch(0.270 0.045 300.7)",
|
||||
"sidebarPrimary": "oklch(0.560 0.183 333.8)",
|
||||
"sidebarPrimaryForeground": "oklch(0.966 0.022 309.6)",
|
||||
"sidebarAccent": "oklch(0.887 0.037 306.4)",
|
||||
"sidebarAccentForeground": "oklch(0.270 0.045 300.7)",
|
||||
"sidebarBorder": "oklch(0.851 0.040 304.9)",
|
||||
"sidebarRing": "oklch(0.560 0.183 333.8)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.268 0.033 291.6)",
|
||||
"foreground": "oklch(0.792 0.000 89.9)",
|
||||
"card": "oklch(0.270 0.045 300.7)",
|
||||
"cardForeground": "oklch(0.792 0.000 89.9)",
|
||||
"popover": "oklch(0.270 0.045 300.7)",
|
||||
"popoverForeground": "oklch(0.792 0.000 89.9)",
|
||||
"primary": "oklch(0.769 0.189 339.1)",
|
||||
"primaryForeground": "oklch(0.268 0.033 291.6)",
|
||||
"secondary": "oklch(0.313 0.067 295.6)",
|
||||
"secondaryForeground": "oklch(0.792 0.000 89.9)",
|
||||
"muted": "oklch(0.313 0.067 295.6)",
|
||||
"mutedForeground": "oklch(0.650 0.075 277.9)",
|
||||
"accent": "oklch(0.270 0.045 300.7)",
|
||||
"accentForeground": "oklch(0.792 0.000 89.9)",
|
||||
"destructive": "oklch(0.664 0.221 22.4)",
|
||||
"border": "oklch(0.313 0.067 295.6)",
|
||||
"input": "oklch(0.270 0.045 300.7)",
|
||||
"ring": "oklch(0.769 0.189 339.1)",
|
||||
"chart1": "oklch(0.769 0.189 339.1)",
|
||||
"chart2": "oklch(0.892 0.144 193.2)",
|
||||
"chart3": "oklch(0.904 0.150 95.3)",
|
||||
"chart4": "oklch(0.697 0.152 302.3)",
|
||||
"chart5": "oklch(0.731 0.153 27.2)",
|
||||
"sidebar": "oklch(0.233 0.038 292.3)",
|
||||
"sidebarForeground": "oklch(0.792 0.000 89.9)",
|
||||
"sidebarPrimary": "oklch(0.769 0.189 339.1)",
|
||||
"sidebarPrimaryForeground": "oklch(0.268 0.033 291.6)",
|
||||
"sidebarAccent": "oklch(0.270 0.045 300.7)",
|
||||
"sidebarAccentForeground": "oklch(0.792 0.000 89.9)",
|
||||
"sidebarBorder": "oklch(0.270 0.045 300.7)",
|
||||
"sidebarRing": "oklch(0.769 0.189 339.1)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "tokyo-night",
|
||||
"label": "Tokyo Night",
|
||||
"order": 3,
|
||||
"description": "Neon-lit cityscape with deep blue undertones",
|
||||
"codeTheme": {
|
||||
"light": "materialLight",
|
||||
"dark": "materialOceanic"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "github-light",
|
||||
"dark": "tokyo-night"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.929 0.008 278.6)",
|
||||
"foreground": "oklch(0.360 0.053 273.4)",
|
||||
"card": "oklch(0.895 0.010 273.3)",
|
||||
"cardForeground": "oklch(0.360 0.053 273.4)",
|
||||
"popover": "oklch(0.895 0.010 273.3)",
|
||||
"popoverForeground": "oklch(0.360 0.053 273.4)",
|
||||
"primary": "oklch(0.476 0.140 260.2)",
|
||||
"primaryForeground": "oklch(1.000 0.000 89.9)",
|
||||
"secondary": "oklch(0.883 0.010 273.3)",
|
||||
"secondaryForeground": "oklch(0.358 0.031 269.6)",
|
||||
"muted": "oklch(0.883 0.010 273.3)",
|
||||
"mutedForeground": "oklch(0.555 0.022 279.2)",
|
||||
"accent": "oklch(0.914 0.008 278.6)",
|
||||
"accentForeground": "oklch(0.358 0.031 269.6)",
|
||||
"destructive": "oklch(0.480 0.100 9.5)",
|
||||
"border": "oklch(0.815 0.007 277.1)",
|
||||
"input": "oklch(0.883 0.010 273.3)",
|
||||
"ring": "oklch(0.476 0.140 260.2)",
|
||||
"chart1": "oklch(0.476 0.140 260.2)",
|
||||
"chart2": "oklch(0.700 0.040 275.4)",
|
||||
"chart3": "oklch(0.580 0.110 165.0)",
|
||||
"chart4": "oklch(0.550 0.120 310.0)",
|
||||
"chart5": "oklch(0.600 0.130 20.0)",
|
||||
"sidebar": "oklch(0.883 0.010 273.3)",
|
||||
"sidebarForeground": "oklch(0.358 0.031 269.6)",
|
||||
"sidebarPrimary": "oklch(0.476 0.140 260.2)",
|
||||
"sidebarPrimaryForeground": "oklch(1.000 0.000 89.9)",
|
||||
"sidebarAccent": "oklch(0.914 0.008 278.6)",
|
||||
"sidebarAccentForeground": "oklch(0.358 0.031 269.6)",
|
||||
"sidebarBorder": "oklch(0.815 0.007 277.1)",
|
||||
"sidebarRing": "oklch(0.476 0.140 260.2)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.226 0.021 280.5)",
|
||||
"foreground": "oklch(0.767 0.054 275.5)",
|
||||
"card": "oklch(0.261 0.034 274.2)",
|
||||
"cardForeground": "oklch(0.767 0.054 275.5)",
|
||||
"popover": "oklch(0.261 0.034 274.2)",
|
||||
"popoverForeground": "oklch(0.767 0.054 275.5)",
|
||||
"primary": "oklch(0.719 0.132 264.2)",
|
||||
"primaryForeground": "oklch(0.226 0.021 280.5)",
|
||||
"secondary": "oklch(0.282 0.036 274.7)",
|
||||
"secondaryForeground": "oklch(0.846 0.061 274.8)",
|
||||
"muted": "oklch(0.306 0.037 273.2)",
|
||||
"mutedForeground": "oklch(0.496 0.068 274.4)",
|
||||
"accent": "oklch(0.259 0.025 274.4)",
|
||||
"accentForeground": "oklch(0.846 0.061 274.8)",
|
||||
"destructive": "oklch(0.723 0.159 10.3)",
|
||||
"border": "oklch(0.266 0.028 281.3)",
|
||||
"input": "oklch(0.240 0.031 275.6)",
|
||||
"ring": "oklch(0.719 0.132 264.2)",
|
||||
"chart1": "oklch(0.719 0.132 264.2)",
|
||||
"chart2": "oklch(0.700 0.120 85.0)",
|
||||
"chart3": "oklch(0.630 0.120 165.0)",
|
||||
"chart4": "oklch(0.650 0.120 310.0)",
|
||||
"chart5": "oklch(0.723 0.159 10.3)",
|
||||
"sidebar": "oklch(0.204 0.016 284.9)",
|
||||
"sidebarForeground": "oklch(0.767 0.054 275.5)",
|
||||
"sidebarPrimary": "oklch(0.719 0.132 264.2)",
|
||||
"sidebarPrimaryForeground": "oklch(0.226 0.021 280.5)",
|
||||
"sidebarAccent": "oklch(0.259 0.025 274.4)",
|
||||
"sidebarAccentForeground": "oklch(0.846 0.061 274.8)",
|
||||
"sidebarBorder": "oklch(0.175 0.008 285.5)",
|
||||
"sidebarRing": "oklch(0.719 0.132 264.2)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"id": "vesper",
|
||||
"label": "Vesper",
|
||||
"order": 9,
|
||||
"description": "Warm monochrome palette with amber highlights",
|
||||
"codeTheme": {
|
||||
"light": "gruvboxLight",
|
||||
"dark": "twilight"
|
||||
},
|
||||
"shikiTheme": {
|
||||
"light": "min-light",
|
||||
"dark": "vesper"
|
||||
},
|
||||
"light": {
|
||||
"background": "oklch(0.980 0.005 78.3)",
|
||||
"foreground": "oklch(0.344 0.007 48.5)",
|
||||
"card": "oklch(0.945 0.009 78.3)",
|
||||
"cardForeground": "oklch(0.344 0.007 48.5)",
|
||||
"popover": "oklch(0.945 0.009 78.3)",
|
||||
"popoverForeground": "oklch(0.344 0.007 48.5)",
|
||||
"primary": "oklch(0.638 0.120 59.5)",
|
||||
"primaryForeground": "oklch(0.980 0.005 78.3)",
|
||||
"secondary": "oklch(0.909 0.015 80.7)",
|
||||
"secondaryForeground": "oklch(0.344 0.007 48.5)",
|
||||
"muted": "oklch(0.909 0.015 80.7)",
|
||||
"mutedForeground": "oklch(0.617 0.009 56.2)",
|
||||
"accent": "oklch(0.945 0.009 78.3)",
|
||||
"accentForeground": "oklch(0.344 0.007 48.5)",
|
||||
"destructive": "oklch(0.574 0.148 30.4)",
|
||||
"border": "oklch(0.878 0.015 80.7)",
|
||||
"input": "oklch(0.945 0.009 78.3)",
|
||||
"ring": "oklch(0.638 0.120 59.5)",
|
||||
"chart1": "oklch(0.638 0.120 59.5)",
|
||||
"chart2": "oklch(0.646 0.103 186.6)",
|
||||
"chart3": "oklch(0.574 0.148 30.4)",
|
||||
"chart4": "oklch(0.564 0.186 311.7)",
|
||||
"chart5": "oklch(0.582 0.156 263.2)",
|
||||
"sidebar": "oklch(0.945 0.009 78.3)",
|
||||
"sidebarForeground": "oklch(0.344 0.007 48.5)",
|
||||
"sidebarPrimary": "oklch(0.638 0.120 59.5)",
|
||||
"sidebarPrimaryForeground": "oklch(0.980 0.005 78.3)",
|
||||
"sidebarAccent": "oklch(0.909 0.015 80.7)",
|
||||
"sidebarAccentForeground": "oklch(0.344 0.007 48.5)",
|
||||
"sidebarBorder": "oklch(0.878 0.015 80.7)",
|
||||
"sidebarRing": "oklch(0.638 0.120 59.5)"
|
||||
},
|
||||
"dark": {
|
||||
"background": "oklch(0.173 0.000 89.9)",
|
||||
"foreground": "oklch(0.779 0.000 89.9)",
|
||||
"card": "oklch(0.226 0.000 89.9)",
|
||||
"cardForeground": "oklch(0.779 0.000 89.9)",
|
||||
"popover": "oklch(0.226 0.000 89.9)",
|
||||
"popoverForeground": "oklch(0.779 0.000 89.9)",
|
||||
"primary": "oklch(0.869 0.088 60.7)",
|
||||
"primaryForeground": "oklch(0.173 0.000 89.9)",
|
||||
"secondary": "oklch(0.277 0.000 89.9)",
|
||||
"secondaryForeground": "oklch(0.779 0.000 89.9)",
|
||||
"muted": "oklch(0.277 0.000 89.9)",
|
||||
"mutedForeground": "oklch(0.528 0.000 89.9)",
|
||||
"accent": "oklch(0.226 0.000 89.9)",
|
||||
"accentForeground": "oklch(0.779 0.000 89.9)",
|
||||
"destructive": "oklch(0.788 0.103 31.3)",
|
||||
"border": "oklch(0.277 0.000 89.9)",
|
||||
"input": "oklch(0.226 0.000 89.9)",
|
||||
"ring": "oklch(0.869 0.088 60.7)",
|
||||
"chart1": "oklch(0.869 0.088 60.7)",
|
||||
"chart2": "oklch(0.930 0.103 175.1)",
|
||||
"chart3": "oklch(0.783 0.111 278.4)",
|
||||
"chart4": "oklch(0.833 0.093 19.2)",
|
||||
"chart5": "oklch(0.832 0.092 293.3)",
|
||||
"sidebar": "oklch(0.145 0.000 89.9)",
|
||||
"sidebarForeground": "oklch(0.779 0.000 89.9)",
|
||||
"sidebarPrimary": "oklch(0.869 0.088 60.7)",
|
||||
"sidebarPrimaryForeground": "oklch(0.173 0.000 89.9)",
|
||||
"sidebarAccent": "oklch(0.226 0.000 89.9)",
|
||||
"sidebarAccentForeground": "oklch(0.779 0.000 89.9)",
|
||||
"sidebarBorder": "oklch(0.226 0.000 89.9)",
|
||||
"sidebarRing": "oklch(0.869 0.088 60.7)"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user