ompchamber 1.2.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -147
- package/dist/client/index.html +4 -2
- package/dist/client/static/css/index.5f630421b7.css +2 -0
- package/dist/client/static/js/77.0d81c74cf0.js +139 -0
- package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
- package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
- package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
- package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
- package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
- package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
- package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
- package/dist/client/static/js/async/e.93214d622b.js +79 -0
- package/dist/client/static/js/async/g.cb81980498.js +2 -0
- package/dist/client/static/js/async/h1.393f066d09.js +1 -0
- package/dist/client/static/js/async/jl.874d352278.js +1 -0
- package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
- package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
- package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
- package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
- package/dist/client/static/js/async/px.fc6877924b.js +1 -0
- package/dist/client/static/js/async/q.22588845a9.js +21 -0
- package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
- package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
- package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
- package/dist/client/static/js/async/vt.42416b707d.js +1 -0
- package/dist/client/static/js/async/x1.da8687159b.js +90 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
- package/dist/client/static/js/async/{yx.af2936c368.js → xn1.83a7784fe1.js} +3 -3
- package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
- package/dist/client/static/js/async/yv.d99d829136.js +1 -0
- package/dist/client/static/js/index.1fd9e1c543.js +38 -0
- package/package.json +3 -3
- package/src/client/components/common/ComposerTextarea.tsx +9 -1
- package/src/client/components/common/MarkdownRenderer.tsx +50 -12
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
- package/src/client/components/settings/Modal.tsx +11 -2
- package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
- package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
- package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
- package/src/client/components/workspace/SearchPanel.tsx +16 -3
- package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
- package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
- package/src/client/components/workspace/btw-panel/index.tsx +124 -45
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
- package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
- package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
- package/src/client/components/workspace/diff-panel/SplitView.tsx +4 -1
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +70 -65
- package/src/client/components/workspace/diff-panel/index.tsx +8 -1
- package/src/client/components/workspace/editor/index.tsx +18 -2
- package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
- package/src/client/components/workspace/file-explorer/index.tsx +86 -16
- package/src/client/components/workspace/git-panel/index.tsx +60 -78
- package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
- package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
- package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
- package/src/client/data/theme/terminal.ts +50 -52
- package/src/client/hooks/chat/btw/index.ts +139 -18
- package/src/client/hooks/chat/btw/intercept.ts +8 -8
- package/src/client/hooks/chat/btw/mode.ts +111 -16
- package/src/client/hooks/chat/composer/index.ts +10 -4
- package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
- package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
- package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
- package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
- package/src/client/hooks/chat/timeline/index.ts +8 -20
- package/src/client/hooks/chat/timeline/queue.ts +66 -9
- package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
- package/src/client/hooks/chat/timeline/scroll.ts +93 -20
- package/src/client/hooks/chat/timeline/session-load.ts +7 -1
- package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
- package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
- package/src/client/hooks/editor/save-file.ts +62 -0
- package/src/client/hooks/editor/use-file-editor.test.ts +61 -5
- package/src/client/hooks/editor/use-file-editor.ts +38 -29
- package/src/client/hooks/editor/use-file-transfer.ts +5 -2
- package/src/client/hooks/settings/providers.ts +34 -3
- package/src/client/hooks/ui/open-file.ts +7 -1
- package/src/client/hooks/ui/theme.ts +82 -57
- package/src/client/hooks/workspace/file-tabs.ts +5 -1
- package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
- package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
- package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
- package/src/client/hooks/workspace/search-stream.ts +24 -5
- package/src/client/main.tsx +6 -0
- package/src/client/tailwind.css +19 -56
- package/src/server/lib/btw/child.server.ts +41 -0
- package/src/server/lib/btw/lifecycle.server.ts +254 -0
- package/src/server/lib/btw/prompt.ts +13 -5
- package/src/server/lib/btw/registry.server.ts +13 -4
- package/src/server/lib/btw/runtime.server.ts +149 -156
- package/src/server/lib/btw/schema.server.ts +27 -0
- package/src/server/lib/btw/service.server.ts +106 -5
- package/src/server/lib/btw/store.server.ts +57 -6
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/settings-store.test.ts +65 -0
- package/src/server/lib/db/settings-store.ts +25 -0
- package/src/server/lib/fs/atomic-write.ts +42 -4
- package/src/server/lib/fs/config-lock.ts +74 -0
- package/src/server/lib/fs/git-ignore.ts +55 -5
- package/src/server/lib/fs/ripgrep.ts +119 -0
- package/src/server/lib/lifecycle/identity.ts +10 -64
- package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
- package/src/server/lib/lifecycle/proc/index.ts +38 -0
- package/src/server/lib/lifecycle/proc/linux.ts +80 -0
- package/src/server/lib/lifecycle/proc/ps.ts +86 -0
- package/src/server/lib/lifecycle/proc/types.ts +33 -0
- package/src/server/lib/lifecycle/proc/win32.ts +53 -0
- package/src/server/lib/models/provider-registry.server.ts +2 -2
- package/src/server/lib/models/registry.server.ts +1 -1
- package/src/server/lib/omp/config/behavior.ts +17 -17
- package/src/server/lib/omp/config/disabled-providers.ts +84 -35
- package/src/server/lib/omp/config/document.ts +128 -0
- package/src/server/lib/omp/config/extensions.ts +11 -10
- package/src/server/lib/omp/config/mcp.ts +4 -57
- package/src/server/lib/omp/config/model-overrides.ts +123 -0
- package/src/server/lib/omp/config/models-config.ts +92 -0
- package/src/server/lib/omp/config/models-validation.ts +159 -0
- package/src/server/lib/omp/config/path-scoped.ts +74 -0
- package/src/server/lib/omp/config/providers.ts +241 -211
- package/src/server/lib/omp/config/roles.ts +6 -14
- package/src/server/lib/omp/core/auth-credentials.ts +1 -1
- package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
- package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
- package/src/server/lib/omp/rpc/manager.ts +21 -21
- package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
- package/src/server/lib/omp/rpc/session-commands.ts +17 -29
- package/src/server/lib/omp/rpc/session-registry.ts +5 -66
- package/src/server/lib/omp/rpc/web-state.ts +0 -6
- package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
- package/src/server/lib/omp/session/auto-title.server.ts +95 -20
- package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
- package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
- package/src/server/lib/queue/delivery-harness.ts +88 -0
- package/src/server/lib/queue/delivery.server.test.ts +78 -149
- package/src/server/lib/queue/delivery.server.ts +105 -13
- package/src/server/lib/queue/store.server.ts +30 -5
- package/src/server/lib/terminal/foreground.test.ts +164 -0
- package/src/server/lib/terminal/foreground.ts +34 -6
- package/src/server/lib/terminal/history.ts +8 -7
- package/src/server/plugins/compress.ts +33 -5
- package/src/server/plugins/ssr.ts +14 -7
- package/src/server/routes/btw/topics.ts +89 -2
- package/src/server/routes/fs/action.test.ts +64 -0
- package/src/server/routes/fs/action.ts +10 -1
- package/src/server/routes/fs/git.ts +5 -6
- package/src/server/routes/fs/search.ts +17 -26
- package/src/server/routes/sessions/queue.ts +17 -4
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/model-override.ts +69 -0
- package/src/server/routes/settings/provider-models.ts +35 -12
- package/src/server/routes/settings/providers.ts +31 -1
- package/src/server/routes/settings/root.ts +8 -3
- package/src/shared/lib/chat/btw/client.ts +34 -0
- package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
- package/src/shared/lib/code/line-endings.test.ts +38 -0
- package/src/shared/lib/code/line-endings.ts +56 -0
- package/src/shared/lib/code/shiki-themes.ts +9 -5
- package/src/shared/lib/fs/file-tree.ts +17 -12
- package/src/shared/lib/markdown/autolink.test.ts +32 -0
- package/src/shared/lib/markdown/autolink.ts +50 -0
- package/src/shared/lib/markdown/document-urls.test.ts +68 -0
- package/src/shared/lib/markdown/document-urls.ts +118 -0
- package/src/shared/lib/markdown/marked.ts +27 -15
- package/src/shared/lib/markdown/mermaid.ts +2 -2
- package/src/shared/lib/models/catalog.ts +1 -1
- package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
- package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
- package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
- package/src/shared/lib/settings/client.ts +10 -4
- package/src/shared/lib/settings/diff.test.ts +31 -0
- package/src/shared/lib/settings/diff.ts +37 -0
- package/src/shared/lib/theme/catalog.test.ts +168 -0
- package/src/shared/lib/theme/catalog.ts +98 -0
- package/src/shared/lib/theme/color.ts +88 -0
- package/src/shared/lib/theme/css.ts +70 -0
- package/src/shared/lib/theme/palettes/chamber.ts +61 -0
- package/src/shared/lib/theme/palettes/dark.ts +313 -0
- package/src/shared/lib/theme/palettes/light.ts +313 -0
- package/src/shared/types/btw.ts +83 -4
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +8 -1
- package/src/shared/types/settings/state.ts +10 -4
- package/src/shared/types/theme.ts +48 -0
- package/dist/client/static/css/index.b938f4a693.css +0 -2
- package/dist/client/static/js/77.8022a800b5.js +0 -139
- package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
- package/dist/client/static/js/async/66.86544ad741.js +0 -93
- package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
- package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
- package/dist/client/static/js/async/e.6443e739a2.js +0 -79
- package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
- package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
- package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
- package/dist/client/static/js/async/m.2e1ea3db9e.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
- package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
- package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
- package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
- package/dist/client/static/js/async/vt.20406a5604.js +0 -1
- package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
- package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
- package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
- package/dist/client/static/js/index.5fe7a2cb64.js +0 -34
- package/src/client/components/workspace/btw-panel/Composer.tsx +0 -183
- package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
- /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{yx.af2936c368.js.LICENSE.txt → g.cb81980498.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.22588845a9.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.5fe7a2cb64.js.LICENSE.txt → index.1fd9e1c543.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The catalog is the single source of truth for what the chamber can paint, and
|
|
8
|
+
* every failure mode here is silent at runtime: an unknown id renders the
|
|
9
|
+
* default palette (a theme that "doesn't work"), a missing color renders as an
|
|
10
|
+
* empty declaration (an invisible swatch, a transparent panel), a wrong variant
|
|
11
|
+
* leaves a dark theme with light scrollbars and light-theme code blocks.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from 'bun:test';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_THEME_ID,
|
|
18
|
+
THEMES,
|
|
19
|
+
THEME_SWATCHES,
|
|
20
|
+
isDarkTheme,
|
|
21
|
+
resolveTheme,
|
|
22
|
+
resolveThemeId,
|
|
23
|
+
} from '@/shared/lib/theme/catalog';
|
|
24
|
+
import { themeStyleSheet } from '@/shared/lib/theme/css';
|
|
25
|
+
import { mixHex, parseHexColor } from '@/shared/lib/theme/color';
|
|
26
|
+
import { getXtermTheme } from '@/client/data/theme/terminal';
|
|
27
|
+
|
|
28
|
+
const COLOR_KEYS = ['canvas', 'paper', 'ink', 'error', 'success', 'info', 'warning', 'meta'] as const;
|
|
29
|
+
const COLOR_VALUE = /^(#[0-9a-f]{3}|#[0-9a-f]{6}|#[0-9a-f]{8})$/i;
|
|
30
|
+
|
|
31
|
+
describe('theme catalog', () => {
|
|
32
|
+
test('ships the chamber palettes plus every ported variant', () => {
|
|
33
|
+
expect(THEMES.length).toBe(45);
|
|
34
|
+
// 21 ported light + paper/contrast; 21 ported dark + one-dark-pro-soft.
|
|
35
|
+
expect(THEMES.filter((t) => t.variant === 'light').length).toBe(23);
|
|
36
|
+
expect(THEMES.filter((t) => t.variant === 'dark').length).toBe(22);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('ids are unique and each palette carries a paintable color for every slot', () => {
|
|
40
|
+
const ids = new Set<string>();
|
|
41
|
+
for (const theme of THEMES) {
|
|
42
|
+
expect(ids.has(theme.id)).toBe(false);
|
|
43
|
+
ids.add(theme.id);
|
|
44
|
+
expect(theme.name.length).toBeGreaterThan(0);
|
|
45
|
+
expect(theme.family.length).toBeGreaterThan(0);
|
|
46
|
+
for (const key of COLOR_KEYS) {
|
|
47
|
+
expect(theme[key]).toMatch(COLOR_VALUE);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('the default is the shipped E-Ink palette', () => {
|
|
53
|
+
expect(DEFAULT_THEME_ID).toBe('paper');
|
|
54
|
+
expect(resolveTheme(DEFAULT_THEME_ID).name).toBe('E-Ink Paper');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('an id the catalog does not know resolves to the default, never to nothing', () => {
|
|
58
|
+
// `noir` shipped until this change as a byte-identical alias of
|
|
59
|
+
// `one-dark-pro-soft`; a settings row naming it must still render.
|
|
60
|
+
for (const unknown of ['noir', 'nonsense', '', undefined, null]) {
|
|
61
|
+
expect(resolveTheme(unknown).id).toBe(DEFAULT_THEME_ID);
|
|
62
|
+
expect(resolveThemeId(unknown)).toBe(DEFAULT_THEME_ID);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('every ported family pairs a light entry with a dark one', () => {
|
|
67
|
+
const byFamily = new Map<string, Set<string>>();
|
|
68
|
+
for (const theme of THEMES) {
|
|
69
|
+
const variants = byFamily.get(theme.family) ?? new Set<string>();
|
|
70
|
+
variants.add(theme.variant);
|
|
71
|
+
byFamily.set(theme.family, variants);
|
|
72
|
+
}
|
|
73
|
+
// `paper`/`contrast` (light-only) and `one-dark-pro-soft` (dark-only) are
|
|
74
|
+
// the chamber's own single-variant palettes; every ported family has both.
|
|
75
|
+
const singles = [...byFamily].filter(([, variants]) => variants.size === 1).map(([family]) => family);
|
|
76
|
+
expect(singles.sort()).toEqual(['contrast', 'one-dark-pro-soft', 'paper']);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('swatches name every color the cards can show', () => {
|
|
80
|
+
expect(THEME_SWATCHES.map((s) => s.key)).toEqual([...COLOR_KEYS]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('generated stylesheet', () => {
|
|
85
|
+
const css = themeStyleSheet();
|
|
86
|
+
|
|
87
|
+
test('defines every palette under a selector that beats :root', () => {
|
|
88
|
+
for (const theme of THEMES) {
|
|
89
|
+
// `html[...]` (0,1,1) outranks the stylesheet's `:root` (0,1,0); a bare
|
|
90
|
+
// `[data-theme]` would tie and lose on document order.
|
|
91
|
+
expect(css).toContain(`html[data-theme="${theme.id}"]`);
|
|
92
|
+
}
|
|
93
|
+
expect(css.match(/html\[data-theme="/g)?.length).toBe(THEMES.length);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('carries each palette\'s colors and its color-scheme', () => {
|
|
97
|
+
expect(css).toContain('--theme-canvas: #1F2430;');
|
|
98
|
+
expect(css).toContain('color-scheme: dark;');
|
|
99
|
+
expect(css).toContain('color-scheme: light;');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('derives the hover shades instead of pinning a hex that fits one palette', () => {
|
|
103
|
+
expect(css).toContain('--theme-info-hover: color-mix(in srgb, var(--theme-info) 85%, var(--theme-ink));');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('theme variant verdict', () => {
|
|
108
|
+
test('follows the palette, not a hand-kept id list', () => {
|
|
109
|
+
expect(isDarkTheme('paper')).toBe(false);
|
|
110
|
+
expect(isDarkTheme('one-dark-pro-soft')).toBe(true);
|
|
111
|
+
expect(isDarkTheme('nord-dark')).toBe(true);
|
|
112
|
+
expect(isDarkTheme('nord-light')).toBe(false);
|
|
113
|
+
expect(isDarkTheme('tokyonight-light')).toBe(false);
|
|
114
|
+
expect(isDarkTheme('tokyonight-dark')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('every dark palette is dark and every light palette is not', () => {
|
|
118
|
+
for (const theme of THEMES) {
|
|
119
|
+
expect(isDarkTheme(theme.id)).toBe(theme.variant === 'dark');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('color math', () => {
|
|
125
|
+
test('parses the notations the ported palettes actually use', () => {
|
|
126
|
+
expect(parseHexColor('#FFF')).toEqual({ r: 255, g: 255, b: 255, a: 1 });
|
|
127
|
+
expect(parseHexColor('#f4f1ea')).toEqual({ r: 244, g: 241, b: 234, a: 1 });
|
|
128
|
+
expect(parseHexColor('#dbd7caee')?.a).toBeCloseTo(0.933, 3);
|
|
129
|
+
expect(parseHexColor('rebeccapurple')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('mixes toward the blend color and composites its alpha', () => {
|
|
133
|
+
expect(mixHex('#000000', '#ffffff', 0.5)).toBe('#808080');
|
|
134
|
+
expect(mixHex('#000000', '#ffffff', 0)).toBe('#000000');
|
|
135
|
+
expect(mixHex('#000000', '#ffffff', 1)).toBe('#ffffff');
|
|
136
|
+
// A fully transparent blend leaves the base untouched rather than
|
|
137
|
+
// producing a black hole.
|
|
138
|
+
expect(mixHex('#123456', '#ffffff00', 1)).toBe('#123456');
|
|
139
|
+
expect(mixHex('nonsense', '#ffffff', 1)).toBe('nonsense');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('terminal palette', () => {
|
|
144
|
+
test('follows the active theme rather than a fixed light/dark pair', () => {
|
|
145
|
+
const nord = getXtermTheme('nord-dark');
|
|
146
|
+
const paper = getXtermTheme('paper');
|
|
147
|
+
expect(nord.background).toBe('#1F2430');
|
|
148
|
+
expect(nord.foreground).toBe('#E5E9F0');
|
|
149
|
+
expect(paper.background).toBe('#f4f1ea');
|
|
150
|
+
expect(paper.foreground).toBe('#141310');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('emits literal colors — xterm cannot parse color-mix()', () => {
|
|
154
|
+
for (const theme of THEMES) {
|
|
155
|
+
for (const value of Object.values(getXtermTheme(theme.id))) {
|
|
156
|
+
expect(value).toMatch(COLOR_VALUE);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('takes its signal colors from the palette', () => {
|
|
162
|
+
const dracula = getXtermTheme('dracula-dark');
|
|
163
|
+
expect(dracula.red).toBe('#FF5555');
|
|
164
|
+
expect(dracula.green).toBe('#50FA7B');
|
|
165
|
+
expect(dracula.yellow).toBe('#FFB86C');
|
|
166
|
+
expect(dracula.cursor).toBe('#BD93F9');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The theme catalog: every palette the chamber can paint, the lookups the whole
|
|
8
|
+
* app resolves a stored theme id through, and the light/dark verdict that used
|
|
9
|
+
* to be re-derived by hand in four places (`hooks/ui/theme.ts`,
|
|
10
|
+
* `code/shiki-themes.ts`, `markdown/mermaid.ts`, `server/plugins/ssr.ts`) — each
|
|
11
|
+
* with its own hardcoded id list that a new theme would have silently missed.
|
|
12
|
+
*
|
|
13
|
+
* Pure data plus pure functions: no DOM, no CSS, no server imports, so the
|
|
14
|
+
* client, the SSR shell and the tests all read the same catalog.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ThemePalette, ThemeSwatch } from '@/shared/types/theme';
|
|
18
|
+
import { CHAMBER_PALETTES } from '@/shared/lib/theme/palettes/chamber';
|
|
19
|
+
import { DARK_PALETTES } from '@/shared/lib/theme/palettes/dark';
|
|
20
|
+
import { LIGHT_PALETTES } from '@/shared/lib/theme/palettes/light';
|
|
21
|
+
|
|
22
|
+
/** The shipped default, and the fallback for any id the catalog does not know. */
|
|
23
|
+
export const DEFAULT_THEME_ID = 'paper';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every id in the catalog, as a literal union. `SettingsState.theme` is typed
|
|
27
|
+
* with it, so a typo — or a theme dropped from the catalog — is a compile error
|
|
28
|
+
* at the writer instead of an unstyled page at runtime.
|
|
29
|
+
*/
|
|
30
|
+
export type ThemeId =
|
|
31
|
+
| (typeof CHAMBER_PALETTES)[number]['id']
|
|
32
|
+
| (typeof LIGHT_PALETTES)[number]['id']
|
|
33
|
+
| (typeof DARK_PALETTES)[number]['id'];
|
|
34
|
+
|
|
35
|
+
const ALL_PALETTES = [...CHAMBER_PALETTES, ...LIGHT_PALETTES, ...DARK_PALETTES];
|
|
36
|
+
|
|
37
|
+
const BY_ID: Record<string, ThemePalette | undefined> = Object.fromEntries(
|
|
38
|
+
ALL_PALETTES.map((theme) => [theme.id, theme]),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const DEFAULT_PALETTE: ThemePalette = CHAMBER_PALETTES[0];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every palette, in appearance-grid order: the chamber's own three first (the
|
|
45
|
+
* ones an existing install is already using), then each ported family with its
|
|
46
|
+
* light and dark entry adjacent.
|
|
47
|
+
*
|
|
48
|
+
* Pairing is by family key rather than by array index, so a re-port that
|
|
49
|
+
* updates one variant of a family cannot silently shift every pair after it.
|
|
50
|
+
*/
|
|
51
|
+
export const THEMES: readonly ThemePalette[] = (() => {
|
|
52
|
+
const families = new Map<string, ThemePalette[]>();
|
|
53
|
+
for (const palette of [...LIGHT_PALETTES, ...DARK_PALETTES]) {
|
|
54
|
+
const group = families.get(palette.family);
|
|
55
|
+
if (group) group.push(palette);
|
|
56
|
+
else families.set(palette.family, [palette]);
|
|
57
|
+
}
|
|
58
|
+
return [...CHAMBER_PALETTES, ...[...families.values()].flat()];
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a stored theme id to a palette the catalog can actually paint.
|
|
63
|
+
*
|
|
64
|
+
* A settings row can name a theme this build no longer ships — a downgrade, or
|
|
65
|
+
* the `noir` alias that was byte-identical to `one-dark-pro-soft` and is gone.
|
|
66
|
+
* Every reader goes through here, so an unknown id renders the default instead
|
|
67
|
+
* of an unstyled page.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveTheme(id: string | undefined | null): ThemePalette {
|
|
70
|
+
return (id ? BY_ID[id] : undefined) ?? DEFAULT_PALETTE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isDarkTheme(id: string | undefined | null): boolean {
|
|
74
|
+
return resolveTheme(id).variant === 'dark';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The catalog's own id for `id`, falling back to the default.
|
|
79
|
+
*
|
|
80
|
+
* The single narrowing point between a runtime string (a stored setting, a
|
|
81
|
+
* button's payload) and `ThemeId`: every palette's `id` is one of the union's
|
|
82
|
+
* members by construction, which is the fact the cast asserts.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveThemeId(id: string | undefined | null): ThemeId {
|
|
85
|
+
return resolveTheme(id).id as ThemeId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The eight slots a palette card shows, in paint order. */
|
|
89
|
+
export const THEME_SWATCHES: readonly ThemeSwatch[] = [
|
|
90
|
+
{ key: 'canvas', label: 'Canvas' },
|
|
91
|
+
{ key: 'paper', label: 'Surface' },
|
|
92
|
+
{ key: 'ink', label: 'Ink' },
|
|
93
|
+
{ key: 'error', label: 'Error' },
|
|
94
|
+
{ key: 'success', label: 'Success' },
|
|
95
|
+
{ key: 'info', label: 'Info' },
|
|
96
|
+
{ key: 'warning', label: 'Warning' },
|
|
97
|
+
{ key: 'meta', label: 'Accent' },
|
|
98
|
+
];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hex color arithmetic for the places that cannot use CSS.
|
|
8
|
+
*
|
|
9
|
+
* `color-mix()` covers every blend inside the document, but xterm parses theme
|
|
10
|
+
* colors itself (it accepts `#rgb`, `#rrggbb`, `rgb()`, `hsl()` and named
|
|
11
|
+
* colors — not `color-mix()`, in either the DOM or the WebGL renderer), so the
|
|
12
|
+
* terminal palette has to be computed here and handed over as literal hex.
|
|
13
|
+
*
|
|
14
|
+
* The palette values are ported verbatim from OpenChamber and are therefore not
|
|
15
|
+
* uniform: `#FFF` (jetbrains-light) and 8-digit alpha (`#dbd7caee`,
|
|
16
|
+
* vitesse-dark) both appear, and a blend has to composite the alpha rather than
|
|
17
|
+
* pretend it is opaque.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface Rgba {
|
|
21
|
+
r: number;
|
|
22
|
+
g: number;
|
|
23
|
+
b: number;
|
|
24
|
+
/** 0–1. */
|
|
25
|
+
a: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HEX_SHORT = /^#?([0-9a-f]{3})$/i;
|
|
29
|
+
const HEX_LONG = /^#?([0-9a-f]{6})$/i;
|
|
30
|
+
const HEX_ALPHA = /^#?([0-9a-f]{8})$/i;
|
|
31
|
+
|
|
32
|
+
/** Parse `#rgb`, `#rrggbb` or `#rrggbbaa`. `null` for anything else. */
|
|
33
|
+
export function parseHexColor(value: string): Rgba | null {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
const short = HEX_SHORT.exec(trimmed);
|
|
36
|
+
if (short) {
|
|
37
|
+
const [r, g, b] = short[1].split('');
|
|
38
|
+
return { r: parseInt(r + r, 16), g: parseInt(g + g, 16), b: parseInt(b + b, 16), a: 1 };
|
|
39
|
+
}
|
|
40
|
+
const long = HEX_LONG.exec(trimmed);
|
|
41
|
+
if (long) {
|
|
42
|
+
const n = parseInt(long[1], 16);
|
|
43
|
+
return { r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff, a: 1 };
|
|
44
|
+
}
|
|
45
|
+
const alpha = HEX_ALPHA.exec(trimmed);
|
|
46
|
+
if (alpha) {
|
|
47
|
+
const n = parseInt(alpha[1], 16);
|
|
48
|
+
return { r: (n >>> 24) & 0xff, g: (n >> 16) & 0xff, b: (n >> 8) & 0xff, a: (n & 0xff) / 255 };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toHex2(value: number): string {
|
|
54
|
+
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, '0');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function rgbaToHex({ r, g, b }: Rgba): string {
|
|
58
|
+
return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `ratio` of `blend` laid over `base` (0 = base, 1 = blend), compositing
|
|
63
|
+
* `blend`'s own alpha. Falls back to `base` when either value cannot be parsed,
|
|
64
|
+
* so a palette with an unexpected notation still yields a paintable color.
|
|
65
|
+
*/
|
|
66
|
+
export function mixHex(base: string, blend: string, ratio: number): string {
|
|
67
|
+
const under = parseHexColor(base);
|
|
68
|
+
const over = parseHexColor(blend);
|
|
69
|
+
if (!under || !over) return base;
|
|
70
|
+
const weight = over.a * ratio;
|
|
71
|
+
return rgbaToHex({
|
|
72
|
+
r: under.r * (1 - weight) + over.r * weight,
|
|
73
|
+
g: under.g * (1 - weight) + over.g * weight,
|
|
74
|
+
b: under.b * (1 - weight) + over.b * weight,
|
|
75
|
+
a: 1,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** WCAG relative luminance, used to order two palette colors light-to-dark. */
|
|
80
|
+
export function relativeLuminance(value: string): number {
|
|
81
|
+
const color = parseHexColor(value);
|
|
82
|
+
if (!color) return 0;
|
|
83
|
+
const channel = (raw: number) => {
|
|
84
|
+
const c = raw / 255;
|
|
85
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
86
|
+
};
|
|
87
|
+
return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
|
|
88
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `[data-theme]` CSS for every palette in the catalog, as one string.
|
|
8
|
+
*
|
|
9
|
+
* Generated rather than hand-written because the palette values already exist
|
|
10
|
+
* in TypeScript — the appearance cards render the same eight colors as chips —
|
|
11
|
+
* so a second hand-maintained copy in `tailwind.css` would be 45 blocks that
|
|
12
|
+
* drift from the cards the moment either side is edited.
|
|
13
|
+
*
|
|
14
|
+
* Two details are load-bearing:
|
|
15
|
+
*
|
|
16
|
+
* - **Selector specificity is `html[data-theme=…]`, not `[data-theme=…]`.**
|
|
17
|
+
* `tailwind.css` carries the default palette on `:root`, which is also
|
|
18
|
+
* (0,1,0); at equal specificity the later declaration wins by document order,
|
|
19
|
+
* so a bare attribute selector injected before the stylesheet would lose and
|
|
20
|
+
* every theme would render as `paper`.
|
|
21
|
+
* - **`color-scheme` is set per variant.** Scrollbars, form controls and the
|
|
22
|
+
* browser's own canvas background are painted by the UA from it; without it a
|
|
23
|
+
* dark theme keeps white scrollbars and a white overscroll area.
|
|
24
|
+
*
|
|
25
|
+
* No DOM here — the SSR shell inlines this, and the client injects the same
|
|
26
|
+
* string only when a served document predates it.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { ThemePalette } from '@/shared/types/theme';
|
|
30
|
+
import { THEMES } from '@/shared/lib/theme/catalog';
|
|
31
|
+
|
|
32
|
+
/** The `<style>` element's id: the SSR shell writes it, the client checks it. */
|
|
33
|
+
export const THEME_STYLE_ELEMENT_ID = 'omp-theme-palettes';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A hover shade of a signal color, mixed toward the theme's own ink so the
|
|
37
|
+
* direction follows the palette: darker on a light theme, lighter on a dark one.
|
|
38
|
+
* `--theme-info-hover` / `--theme-warning-hover` used to be fixed hex values in
|
|
39
|
+
* `tailwind.css` and could only be right for one palette.
|
|
40
|
+
*/
|
|
41
|
+
const HOVER_MIXES: ReadonlyArray<readonly [string, string]> = [
|
|
42
|
+
['--theme-info-hover', '--theme-info'],
|
|
43
|
+
['--theme-warning-hover', '--theme-warning'],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function paletteBlock(theme: ThemePalette): string {
|
|
47
|
+
const lines = [
|
|
48
|
+
` --theme-paper: ${theme.paper};`,
|
|
49
|
+
` --theme-canvas: ${theme.canvas};`,
|
|
50
|
+
` --theme-ink: ${theme.ink};`,
|
|
51
|
+
` --theme-error: ${theme.error};`,
|
|
52
|
+
` --theme-success: ${theme.success};`,
|
|
53
|
+
` --theme-info: ${theme.info};`,
|
|
54
|
+
` --theme-warning: ${theme.warning};`,
|
|
55
|
+
` --theme-meta: ${theme.meta};`,
|
|
56
|
+
];
|
|
57
|
+
for (const [name, source] of HOVER_MIXES) {
|
|
58
|
+
lines.push(` ${name}: color-mix(in srgb, var(${source}) 85%, var(--theme-ink));`);
|
|
59
|
+
}
|
|
60
|
+
lines.push(` color-scheme: ${theme.variant};`);
|
|
61
|
+
return `html[data-theme="${theme.id}"] {\n${lines.join('\n')}\n}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Cached: the palette set is fixed for the life of the process. */
|
|
65
|
+
let cached: string | null = null;
|
|
66
|
+
|
|
67
|
+
export function themeStyleSheet(): string {
|
|
68
|
+
cached ??= `/* Generated from src/shared/lib/theme/palettes — do not edit by hand. */\n${THEMES.map(paletteBlock).join('\n')}\n`;
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The chamber's own palettes, carried over from the hand-written
|
|
8
|
+
* `[data-theme]` blocks in `tailwind.css` with their values unchanged.
|
|
9
|
+
*
|
|
10
|
+
* They stay in the catalog rather than becoming OpenChamber ports so an
|
|
11
|
+
* existing install keeps the exact colors it was using: `paper` is the shipped
|
|
12
|
+
* default and `one-dark-pro-soft` is what every dark-theme user has selected
|
|
13
|
+
* until now.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ThemePalette } from '@/shared/types/theme';
|
|
17
|
+
|
|
18
|
+
export const CHAMBER_PALETTES = [
|
|
19
|
+
{
|
|
20
|
+
id: 'paper',
|
|
21
|
+
name: 'E-Ink Paper',
|
|
22
|
+
family: 'paper',
|
|
23
|
+
variant: 'light',
|
|
24
|
+
canvas: '#f4f1ea',
|
|
25
|
+
paper: '#faf8f3',
|
|
26
|
+
ink: '#141310',
|
|
27
|
+
error: '#c8321e',
|
|
28
|
+
success: '#047857',
|
|
29
|
+
info: '#0284c7',
|
|
30
|
+
warning: '#c2410c',
|
|
31
|
+
meta: '#4f46e5',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'contrast',
|
|
35
|
+
name: 'High Contrast',
|
|
36
|
+
family: 'contrast',
|
|
37
|
+
variant: 'light',
|
|
38
|
+
canvas: '#f4f4f4',
|
|
39
|
+
paper: '#ffffff',
|
|
40
|
+
ink: '#111111',
|
|
41
|
+
error: '#b42318',
|
|
42
|
+
success: '#067647',
|
|
43
|
+
info: '#0369a1',
|
|
44
|
+
warning: '#b54708',
|
|
45
|
+
meta: '#4338ca',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'one-dark-pro-soft',
|
|
49
|
+
name: 'One Dark Pro Soft',
|
|
50
|
+
family: 'one-dark-pro-soft',
|
|
51
|
+
variant: 'dark',
|
|
52
|
+
canvas: '#21252b',
|
|
53
|
+
paper: '#282c34',
|
|
54
|
+
ink: '#abb2bf',
|
|
55
|
+
error: '#e06c75',
|
|
56
|
+
success: '#34d399',
|
|
57
|
+
info: '#61afef',
|
|
58
|
+
warning: '#fb923c',
|
|
59
|
+
meta: '#818cf8',
|
|
60
|
+
},
|
|
61
|
+
] as const satisfies readonly ThemePalette[];
|