dsh-theme-triptych 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 dsh-theme-keyshub contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # dsh-theme-triptych
2
+
3
+ DeepSeek Harness(DSH)外观主题插件包 —— 把 [keyshub.top](https://keyshub.top) 的三种主题色调与壁纸带进 DSH Web GUI。
4
+
5
+ 三套主题逐字取自参考工程 `isolo-news/web/src/config/themes.ts`,三张壁纸与线上 Hero 背景图(`image_0/2/4_yi19x4.jpg`)SHA-256 完全一致。
6
+
7
+ | 主题 | 氛围 | 主色 / 次色 | 色系 |
8
+ | --- | --- | --- | --- |
9
+ | **NEXUS** | 赛博霓虹(暗色) | `#00f0ff` 青 / `#ff2d95` 品红 | 深蓝底 + 荧光点缀 |
10
+ | **COMIKET** | 温暖纸感(亮色) | `#ff6b2b` 橙 / `#3b9eff` 蓝 | 暖纸白底 |
11
+ | **IRONCORE** | 机械工业(暗色) | `#f0a030` 琥珀 / `#4ecdc4` 青绿 | 铁灰底 + 警示橙 |
12
+
13
+ ## 特性
14
+
15
+ - 三套主题通过标准 `theme.register` 注册,与 DSH 内置 Light / Dark / System 完全互通(`theme.setTheme`)。
16
+ - 每套主题自带壁纸:自定义 token `--dsw-theme-wallpaper` 由主题 presenter 自动写入 `<body>` 内联变量,表面色(`bg-base / layer-1 / layer-2 / overlay / sidebar-fill`)改为半透明同色系,壁纸从底层透出,形成毛玻璃观感。
17
+ - 两处零侵入切换入口(均为 additive 槽位,`replaceRisk: none`):
18
+ - **设置 → 通用 → “外观主题 / Theme”**(order 11,紧跟系统 Appearance 行):三张壁纸预览卡片 + “默认 System”;
19
+ - **侧边栏底部 “主题外观” 调色板按钮**(order 10):弹出四选项菜单,窄栏(rail)下自动收成纯图标。
20
+ - 卸载即还原:插件停用/更新时主题、样式、槽位全部自动清理。
21
+
22
+ ## 工作原理(DSH 客户端插件格式)
23
+
24
+ 这是一个标准 DSH **双面 `dsh.client` 包**:
25
+
26
+ - `package.json` 声明 `dsh.client: { platform: "web" }` 与 `exports["./client"]`;
27
+ - **Node 半部**(`lib/index.js`):作为组合中的一个 loader 行挂载,用 `webServer` 注册三条精确路由(`/dsh-theme-triptych/*.jpg`),从包内 `assets/` 提供壁纸;
28
+ - **浏览器半部**(`lib/client.js`):`__ModuleLoader__.load` 工厂包,注册主题、注入壁纸样式、挂载两个切换入口;
29
+ - 宿主扫描到该行后自动把客户端包编入 `window.__DSH_BOOT__`,浏览器按需加载 —— **无需重新构建 DSH Web 产物**。
30
+
31
+ ## 安装(任选其一)
32
+
33
+ > 要求:本机已安装 DSH(`dsh` CLI 可用),并能访问 npm registry。
34
+
35
+ ### 方式 A:官方安装脚本(推荐)
36
+
37
+ ```bash
38
+ # Linux / macOS / Git Bash on Windows
39
+ bash install/install.sh # 默认 profile: web;可传参: bash install/install.sh <profile>
40
+ # 或 PowerShell
41
+ powershell -ExecutionPolicy Bypass -File install/install.ps1 # 可传 -Profile <name>
42
+ ```
43
+
44
+ 脚本会:① 在 `$DSH_HOME/profiles/<profile>` 中 `pnpm add dsh-theme-triptych`(无 pnpm 则用 npm);② 向 `cordis.patch.yml` 追加 loader 插入项;③ 提示重启。
45
+
46
+ ### 方式 B:手动三步
47
+
48
+ ```bash
49
+ # 1) 安装包(在 DSH 部署的 profile 目录下)
50
+ cd "$DSH_HOME/profiles/web" # Windows: %USERPROFILE%\.dsh\profiles\web
51
+ pnpm add dsh-theme-triptych # 或 npm install dsh-theme-triptych
52
+
53
+ # 2) 在 cordis.patch.yml 末尾追加(若文件内容是 [],整体替换为下面块)
54
+ ```
55
+
56
+ ```yaml
57
+ - insert:
58
+ - id: dsh-theme-triptych
59
+ name: dsh-theme-triptych
60
+ ```
61
+
62
+ ```bash
63
+ # 3) 重启 DSH
64
+ dsh --profile web
65
+ ```
66
+
67
+ ### 验证
68
+
69
+ ```bash
70
+ # 壁纸路由(应返回 200 与 image/jpeg)
71
+ curl -I http://127.0.0.1:3080/dsh-theme-triptych/nexus.jpg
72
+ # 打开 Web GUI:侧边栏底部出现调色板按钮;设置 → 通用 出现“外观主题 / Theme”
73
+ ```
74
+
75
+ ## 卸载
76
+
77
+ ```bash
78
+ cd "$DSH_HOME/profiles/web"
79
+ pnpm remove dsh-theme-triptych
80
+ # 并从 cordis.patch.yml 删除 installer 追加的 insert 块
81
+ ```
82
+
83
+ ## 从源码构建与测试
84
+
85
+ ```bash
86
+ node scripts/build.mjs # 生成 lib/client.js(__ModuleLoader__ 工厂包装)与 lib/index.js
87
+ node test/smoke.mjs # 无 DSH 运行时依赖的冒烟测试(bundle 契约 + 壁纸路由)
88
+ ```
89
+
90
+ ## 发布(维护者)
91
+
92
+ DSH 插件没有独立的“提交审核”市场 —— 官方分发渠道就是 **npm**(见 deepseek-ai 官方文档 [`docs/user/develop/basic/publish.zh.md`](https://github.com/deepseek-ai/DeepSeek-Harness/blob/master/docs/user/develop/basic/publish.zh.md)),“能被搜到”取决于 npm 元数据与 GitHub 主题:
93
+
94
+ 1. **发布到 npm**(官方渠道,`npm search` / `pnpm add` 可发现):
95
+ - 本地:`node scripts/build.mjs` 后 `npm publish --access public`(需 npm 账号)。
96
+ - 自动:打 tag 触发 `.github/workflows/release.yml`(需在仓库 Secrets 配置 `NPM_TOKEN`):
97
+ ```bash
98
+ git tag v0.1.0 && git push origin v0.1.0
99
+ ```
100
+ 2. **优化 npm 可搜索性**:`description` 含 “DeepSeek Harness”,`keywords` 含 `deepseek-harness` / `dsh` / `dsh-plugin`(本包已配置);补全 `repository` / `homepage` 字段让 npm 页面展示 GitHub 链接。
101
+ 3. **让社区插件市场收录**(非官方,但覆盖面最广):多数社区市场(如 [`DSH-Plugins-Marketplace`](https://github.com/bradeGithub/DSH-Plugins-Marketplace)、[`AwesomeHou/dsh-plugin-marketplace`](https://github.com/AwesomeHou/dsh-plugin-marketplace))实时同步 GitHub **`dsh-plugin` topic** 下的仓库,或索引 npm 上 `dsh-plugin-*` 名称的包。在 GitHub 仓库页 **About → Topics** 添加 `dsh-plugin`(以及 `deepseek-harness`、`dsh`、`theme`),无需额外申请即可被这些市场自动收录。
102
+ 4. 可选:向社区维护的 awesome 列表提交 PR(如 [`awesome-dsh-plugin`](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin))。
103
+
104
+ ## 未经安装的试用(动态插件方式)
105
+
106
+ 若只想在当前 DSH 会话里临时体验(无需改动部署),可在 GUI 会话中把 `src/index.js` / `src/client/index.js` 作为 `cordis_define` 的 `code.host` / `code.client` 声明一个动态插件并运行(需在 Run 卡片中允许)。重启后失效。
107
+
108
+ ## 许可
109
+
110
+ MIT。壁纸图片来自 keyshub.top 项目(`isolo-news/web/public/images`),版权归原作者所有。
111
+
112
+ ---
113
+
114
+ # English
115
+
116
+ `dsh-theme-triptych` brings the three appearance themes of [keyshub.top](https://keyshub.top) — **NEXUS** (neon cyan/pink, dark), **COMIKET** (warm paper, light) and **IRONCORE** (industrial amber/teal, dark) — to the DeepSeek Harness Web GUI, wallpapers included.
117
+
118
+ It is a standard DSH dual-face `dsh.client` package: the node half serves the three wallpapers over `webServer` routes (`/dsh-theme-triptych/*.jpg`), the browser half registers the themes (translucent surfaces over a wallpaper via the `--dsw-theme-wallpaper` token), and both a **Settings → General → 外观主题 row** and a **sidebar-footer palette button** switch themes. No web rebuild is required — the host serves the client bundle from disk (`/plugins/<id>/client.js`).
119
+
120
+ **Install:** `bash install/install.sh` (or manual: `pnpm add dsh-theme-triptych` inside `$DSH_HOME/profiles/<profile>`, append the `- insert:` block for `dsh-theme-triptych` to `cordis.patch.yml`, restart `dsh --profile web`).
Binary file
Binary file
Binary file
package/lib/client.js ADDED
@@ -0,0 +1,353 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-theme-triptych",
3
+ factory: (require) => {
4
+ "use strict";
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ var React = require("react");
9
+ /**
10
+ * dsh-theme-triptych — browser half (plain JS, no build-time imports).
11
+ *
12
+ * scripts/build.mjs wraps this file into lib/client.js with the
13
+ * __ModuleLoader__.load factory contract; the wrapper injects `React`
14
+ * via require("react") and assigns exports.apply / exports.inject.
15
+ * Keep this file free of import/export statements.
16
+ *
17
+ * What it does:
18
+ * 1. theme.register — three themes (NEXUS dark / COMIKET light / IRONCORE dark)
19
+ * mapping the keyshub palettes onto the DSH --dsw-alias-* tokens. Surface
20
+ * tokens are semi-transparent so the wallpaper shows through (glassmorphism).
21
+ * A custom token --dsw-theme-wallpaper carries the wallpaper URL; the theme
22
+ * presenter applies any active-theme token to <body> inline CSS variables,
23
+ * so switching themes swaps the wallpaper with zero DOM code.
24
+ * 2. One <style> element (fiber-owned) with the body wallpaper rule and the
25
+ * switcher UI styles; removed on plugin stop (the module system also tags
26
+ * and claims it for HMR bookkeeping).
27
+ * 3. Two additive switcher entry points:
28
+ * - settings.general.item row "外观主题 / Theme" (order 11, right after
29
+ * the built-in Appearance row) with three wallpaper cards + System.
30
+ * - sidebar.footer.action palette button (order 10) with a popover menu.
31
+ * Both are additive list entries — replaceRisk none; nothing shipped is
32
+ * shadowed.
33
+ */
34
+
35
+ /* ── palettes (verbatim from keyshub.top / isolo-news web/src/config/themes.ts) ── */
36
+
37
+ const THEME_SPECS = [
38
+ {
39
+ id: 'nexus', displayName: 'NEXUS', colorScheme: 'dark', wallpaper: '/dsh-theme-triptych/nexus.jpg',
40
+ // Transparency model: the backdrop is nearly open so the wallpaper reads
41
+ // clearly; only content surfaces (message cards, inputs) keep a
42
+ // semi-transparent mask for legibility, and popovers stay near-opaque.
43
+ alpha: { base: 0.35, layer1: 0.78, layer2: 0.7, overlay: 0.94, sidebar: 0.66 },
44
+ colors: {
45
+ primary: '#00f0ff', primaryDark: '#00b8c5', secondary: '#ff2d95', bgBase: '#0a0e1a', bgLayer1: '#151c2e',
46
+ bgLayer2: '#1a2035', bgTertiary: '#1a2035', bgOverlay: '#1e2640', sidebar: '#111827',
47
+ border1: 'rgba(0, 240, 255, 0.15)', border2: 'rgba(0, 240, 255, 0.4)',
48
+ text1: '#e8edf5', text2: '#8892a8', text3: '#5a6478', success: '#00ff88', warn: '#ffaa00', error: '#ff3366',
49
+ },
50
+ },
51
+ {
52
+ id: 'comiket', displayName: 'COMIKET', colorScheme: 'light', wallpaper: '/dsh-theme-triptych/comiket.jpg',
53
+ alpha: { base: 0.4, layer1: 0.82, layer2: 0.76, overlay: 0.95, sidebar: 0.7 },
54
+ colors: {
55
+ primary: '#ff6b2b', primaryDark: '#cc5522', secondary: '#3b9eff', bgBase: '#faf8f5', bgLayer1: '#ffffff',
56
+ bgLayer2: '#ebe6de', bgTertiary: '#ebe6de', bgOverlay: '#ffffff', sidebar: '#f2eee8',
57
+ border1: 'rgba(0, 0, 0, 0.1)', border2: 'rgba(0, 0, 0, 0.24)',
58
+ text1: '#2a2520', text2: '#6b6158', text3: '#9e958b', success: '#22c55e', warn: '#f59e0b', error: '#ef4444',
59
+ },
60
+ },
61
+ {
62
+ id: 'ironcore', displayName: 'IRONCORE', colorScheme: 'dark', wallpaper: '/dsh-theme-triptych/ironcore.jpg',
63
+ alpha: { base: 0.35, layer1: 0.78, layer2: 0.7, overlay: 0.94, sidebar: 0.66 },
64
+ colors: {
65
+ primary: '#f0a030', primaryDark: '#c08020', secondary: '#4ecdc4', bgBase: '#121418', bgLayer1: '#1e2128',
66
+ bgLayer2: '#22262e', bgTertiary: '#22262e', bgOverlay: '#282c35', sidebar: '#1a1d24',
67
+ border1: 'rgba(240, 160, 48, 0.12)', border2: 'rgba(240, 160, 48, 0.36)',
68
+ text1: '#d8dce5', text2: '#808898', text3: '#555c6c', success: '#4ecdc4', warn: '#f0a030', error: '#e74c5e',
69
+ },
70
+ },
71
+ ]
72
+
73
+ /* ── helpers ── */
74
+
75
+ function hexToRgba(hex, alpha) {
76
+ const h = String(hex).replace('#', '')
77
+ const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h
78
+ const n = parseInt(full, 16)
79
+ return 'rgba(' + ((n >> 16) & 255) + ', ' + ((n >> 8) & 255) + ', ' + (n & 255) + ', ' + alpha + ')'
80
+ }
81
+
82
+ function buildThemeDef(spec) {
83
+ const c = spec.colors
84
+ const a = spec.alpha
85
+ return {
86
+ id: spec.id,
87
+ displayName: spec.displayName,
88
+ colorScheme: spec.colorScheme,
89
+ tokens: {
90
+ /* ── surfaces ── */
91
+ '--dsw-alias-bg-base': hexToRgba(c.bgBase, a.base),
92
+ '--dsw-alias-bg-layer-1': hexToRgba(c.bgLayer1, a.layer1),
93
+ '--dsw-alias-bg-layer-2': hexToRgba(c.bgLayer2, a.layer2),
94
+ '--dsw-alias-bg-layer-3': hexToRgba(c.bgLayer2, 0.92),
95
+ '--dsw-alias-bg-overlay': hexToRgba(c.bgOverlay, a.overlay),
96
+ '--dsw-specific-sidebar-fill': hexToRgba(c.sidebar, a.sidebar),
97
+ /* ── strokes ── */
98
+ '--dsw-alias-border-l1': c.border1,
99
+ '--dsw-alias-border-l2': c.border2,
100
+ '--dsw-alias-border-l2-darkmode-thin': c.border1,
101
+ '--dsw-alias-border-l3': hexToRgba(c.primary, 0.3),
102
+ '--dsw-alias-border-l4': hexToRgba(c.primary, 0.4),
103
+ /* ── brand / labels ── */
104
+ '--dsw-alias-brand-primary': c.primary,
105
+ '--dsw-alias-label-primary': c.text1,
106
+ '--dsw-alias-label-secondary': c.text2,
107
+ '--dsw-alias-label-tertiary': c.text3,
108
+ '--dsw-alias-label-caption': c.text3,
109
+ '--dsw-alias-label-dimmed': c.text3,
110
+ /* ── state accents ──
111
+ business-primary is the UI's main interactive accent: active
112
+ conversation tab text/bar, active folder icon, tool status, composer
113
+ caret and pending dot — it must be the theme's PRIMARY hue (e.g.
114
+ COMIKET orange), not the secondary. */
115
+ '--dsw-alias-state-success-primary': c.success,
116
+ '--dsw-alias-state-warn-primary': c.warn,
117
+ '--dsw-alias-state-error-primary': c.error,
118
+ '--dsw-alias-state-business-primary': c.primary,
119
+ '--dsw-alias-state-business-tertiary': hexToRgba(c.primary, 0.18),
120
+ /* ── buttons (composer + generic) ──
121
+ info-fill is the composer send circle (white glyph is hard-coded, so
122
+ it uses the darker primary variant for contrast); tool-bar fill drives
123
+ the composer tool-row buttons. */
124
+ '--dsw-alias-button-info-fill': c.primaryDark,
125
+ '--dsw-alias-button-info-hover': c.primary,
126
+ '--dsw-alias-button-primary-fill': c.primary,
127
+ '--dsw-alias-button-primary-hover': c.primaryDark,
128
+ '--dsw-alias-button-primary-dimmed': hexToRgba(c.primary, 0.35),
129
+ '--dsw-alias-button-tool-bar-fill': hexToRgba(c.primary, 0.22),
130
+ '--dsw-alias-button-tool-bar-hover': hexToRgba(c.primary, 0.34),
131
+ '--dsw-alias-button-tool-bar-fill-invisible': hexToRgba(c.primary, 0.13),
132
+ '--dsw-alias-button-ghost-active-fill': hexToRgba(c.primary, 0.14),
133
+ '--dsw-alias-button-ghost-active-hover': hexToRgba(c.primary, 0.2),
134
+ '--dsw-alias-button-ghost-active-border': hexToRgba(c.primary, 0.45),
135
+ '--dsw-alias-button-floating-fill': hexToRgba(c.bgLayer1, 0.92),
136
+ '--dsw-alias-button-floating-hover': hexToRgba(c.bgLayer2, 0.92),
137
+ /* ── interactive hovers (message actions, chips, lists) ── */
138
+ '--dsw-alias-interactive-bg-hover': hexToRgba(c.primary, 0.1),
139
+ '--dsw-alias-interactive-bg-hover-solid': hexToRgba(c.primary, 0.18),
140
+ '--dsw-alias-interactive-bg-active': hexToRgba(c.primary, 0.16),
141
+ '--dsw-alias-interactive-bg-hover-accent': hexToRgba(c.primary, 0.22),
142
+ /* ── system output cards: code blocks, tool status, bubbles ── */
143
+ '--dsw-alias-markdown-code-block': hexToRgba(c.bgTertiary, 0.82),
144
+ '--dsw-alias-markdown-code-block-banner': hexToRgba(c.bgTertiary, 0.9),
145
+ '--dsw-alias-markdown-inline-code': hexToRgba(c.bgTertiary, 0.75),
146
+ '--dsw-alias-markdown-tag': hexToRgba(c.bgTertiary, 0.75),
147
+ '--dsw-alias-markdown-placeholder': hexToRgba(c.bgTertiary, 0.55),
148
+ '--dsw-alias-markdown-citation': hexToRgba(c.bgTertiary, 0.6),
149
+ /* ── specific seats: input card, message bubbles, attach, nav ── */
150
+ '--dsw-specific-input-major': hexToRgba(c.bgLayer1, 0.9),
151
+ '--dsw-specific-bubble': hexToRgba(c.bgLayer1, 0.85),
152
+ '--dsw-specific-bubble-highlight': hexToRgba(c.bgLayer2, 0.85),
153
+ '--dsw-specific-selector': hexToRgba(c.primary, 0.22),
154
+ '--dsw-specific-tip': hexToRgba(c.bgTertiary, 0.7),
155
+ '--dsw-specific-sidebar-nav-item-active': hexToRgba(c.primary, 0.14),
156
+ '--dsw-specific-sidebar-nav-item-hover': hexToRgba(c.primary, 0.09),
157
+ '--dsw-specific-sidebar-nav-item-active-accent': hexToRgba(c.primary, 0.3),
158
+ /* ── wallpaper (custom token consumed by the WALLPAPER_CSS rule) ── */
159
+ '--dsw-theme-wallpaper': "url('" + spec.wallpaper + "')",
160
+ },
161
+ }
162
+ }
163
+
164
+ /* ── styles ── */
165
+
166
+ const WALLPAPER_CSS = `
167
+ body {
168
+ background-image: var(--dsw-theme-wallpaper, none);
169
+ background-size: cover;
170
+ background-position: center;
171
+ background-attachment: fixed;
172
+ background-repeat: no-repeat;
173
+ }
174
+ `
175
+
176
+ const UI_CSS = `
177
+ .dswt-row { display: flex; flex-direction: column; gap: 10px; padding: 4px 0 14px; }
178
+ .dswt-title { font-size: 13px; font-weight: 500; color: var(--dsw-alias-label-secondary); }
179
+ .dswt-cards { display: flex; gap: 10px; flex-wrap: wrap; }
180
+ .dswt-card { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 6px; border: none; background: transparent; border-radius: 10px; cursor: pointer; }
181
+ .dswt-card:hover { background: var(--dsw-alias-bg-layer-2); }
182
+ .dswt-preview { width: 108px; height: 60px; border-radius: 8px; background-size: cover; background-position: center; border: 1px solid var(--dsw-alias-border-l1); position: relative; overflow: hidden; box-sizing: border-box; }
183
+ .dswt-dots { position: absolute; left: 6px; bottom: 6px; display: flex; gap: 4px; }
184
+ .dswt-dot { width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.4); }
185
+ .dswt-name { font-size: 12px; color: var(--dsw-alias-label-primary); text-align: center; }
186
+ .dswt-card[data-active='true'] .dswt-preview { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
187
+ .dswt-card[data-active='true'] .dswt-name { color: var(--dsw-alias-brand-primary); }
188
+ .dswt-system .dswt-preview { background: linear-gradient(135deg, var(--dsw-alias-bg-layer-1), var(--dsw-alias-bg-layer-2)); display: flex; align-items: center; justify-content: center; }
189
+ .dswt-system-mark { font-size: 24px; line-height: 1; opacity: 0.8; color: var(--dsw-alias-label-secondary); }
190
+ .dswt-foot { width: 100%; }
191
+ .dswt-foot-btn { display: flex; align-items: center; justify-content: flex-start; gap: 8px; width: 100%; padding: 7px 10px; border: none; background: transparent; color: var(--dsw-alias-label-primary); border-radius: 8px; cursor: pointer; font-size: 13px; }
192
+ .dswt-foot-btn:hover, .dswt-foot-btn[data-open='true'] { background: var(--dsw-alias-bg-layer-2); }
193
+ .dswt-rail { justify-content: center; padding: 7px 0; }
194
+ .dswt-palette { width: 16px; height: 16px; border-radius: 5px; flex: none; background: conic-gradient(from 90deg, #00f0ff, #ff2d95, #f0a030, #4ecdc4, #00f0ff); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25); }
195
+ .dswt-foot-label { font-size: 13px; }
196
+ .dswt-pop { position: fixed; left: 10px; bottom: 68px; z-index: 200; min-width: 216px; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; padding: 8px; display: flex; flex-direction: column; gap: 2px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35); }
197
+ .dswt-pop-backdrop { position: fixed; inset: 0; z-index: 190; background: transparent; border: none; cursor: default; }
198
+ .dswt-pop-item { display: flex; align-items: center; gap: 9px; padding: 7px 8px; border: none; background: transparent; color: var(--dsw-alias-label-primary); border-radius: 8px; cursor: pointer; font-size: 13px; text-align: left; width: 100%; }
199
+ .dswt-pop-item:hover { background: var(--dsw-alias-bg-layer-2); }
200
+ .dswt-pop-item[data-active='true'] { color: var(--dsw-alias-brand-primary); }
201
+ .dswt-pop-swatch { width: 22px; height: 22px; border-radius: 6px; background-size: cover; background-position: center; border: 1px solid var(--dsw-alias-border-l1); flex: none; }
202
+ .dswt-pop-swatch-system { background: linear-gradient(135deg, var(--dsw-alias-bg-layer-1), var(--dsw-alias-bg-layer-2)); display: flex; align-items: center; justify-content: center; font-size: 12px; color: var(--dsw-alias-label-secondary); }
203
+ `
204
+
205
+ /* ── switcher UI data ── */
206
+
207
+ const ROW_CARDS = [
208
+ { id: 'nexus', name: 'NEXUS', preview: '/dsh-theme-triptych/nexus.jpg', dots: ['#00f0ff', '#ff2d95'] },
209
+ { id: 'comiket', name: 'COMIKET', preview: '/dsh-theme-triptych/comiket.jpg', dots: ['#ff6b2b', '#3b9eff'] },
210
+ { id: 'ironcore', name: 'IRONCORE', preview: '/dsh-theme-triptych/ironcore.jpg', dots: ['#f0a030', '#4ecdc4'] },
211
+ ]
212
+
213
+ const QUICK_CARDS = [
214
+ { id: 'nexus', name: 'NEXUS 赛博霓虹', swatch: '/dsh-theme-triptych/nexus.jpg' },
215
+ { id: 'comiket', name: 'COMIKET 温暖纸感', swatch: '/dsh-theme-triptych/comiket.jpg' },
216
+ { id: 'ironcore', name: 'IRONCORE 机械工业', swatch: '/dsh-theme-triptych/ironcore.jpg' },
217
+ ]
218
+
219
+ function isCustom(id) {
220
+ return id === 'nexus' || id === 'comiket' || id === 'ironcore'
221
+ }
222
+
223
+ /* ── components (React.createElement only; React is injected by the wrapper) ── */
224
+
225
+ function ThemeRow(props) {
226
+ const [pref, setPref] = React.useState(props.snapshot())
227
+ React.useEffect(() => props.subscribe(setPref), [])
228
+ return React.createElement('div', { className: 'dswt-row' },
229
+ React.createElement('div', { className: 'dswt-title' }, '外观主题 / Theme'),
230
+ React.createElement('div', { className: 'dswt-cards' },
231
+ ROW_CARDS.map((card) => React.createElement('button', {
232
+ key: card.id, type: 'button', className: 'dswt-card',
233
+ 'data-active': pref === card.id || undefined,
234
+ onClick: () => props.setTheme(card.id),
235
+ },
236
+ React.createElement('div', { className: 'dswt-preview', style: { backgroundImage: "url('" + card.preview + "')" } },
237
+ React.createElement('div', { className: 'dswt-dots' },
238
+ card.dots.map((d) => React.createElement('span', { key: d, className: 'dswt-dot', style: { background: d } })),
239
+ ),
240
+ ),
241
+ React.createElement('div', { className: 'dswt-name' }, card.name),
242
+ )),
243
+ React.createElement('button', {
244
+ type: 'button', className: 'dswt-card dswt-system',
245
+ 'data-active': !isCustom(pref) || undefined,
246
+ onClick: () => props.setTheme('system'),
247
+ },
248
+ React.createElement('div', { className: 'dswt-preview' },
249
+ React.createElement('span', { className: 'dswt-system-mark' }, '◐'),
250
+ ),
251
+ React.createElement('div', { className: 'dswt-name' }, '默认 System'),
252
+ ),
253
+ ),
254
+ )
255
+ }
256
+
257
+ function ThemeFooterAction(props) {
258
+ const [pref, setPref] = React.useState(props.snapshot())
259
+ const [open, setOpen] = React.useState(false)
260
+ React.useEffect(() => props.subscribe(setPref), [])
261
+ const btn = React.createElement('button', {
262
+ type: 'button',
263
+ className: props.wide ? 'dswt-foot-btn' : 'dswt-foot-btn dswt-rail',
264
+ 'data-open': open || undefined,
265
+ 'aria-label': '主题外观',
266
+ 'aria-expanded': open || undefined,
267
+ onClick: () => setOpen(!open),
268
+ },
269
+ React.createElement('span', { className: 'dswt-palette' }),
270
+ props.wide ? React.createElement('span', { className: 'dswt-foot-label' }, '主题外观') : null,
271
+ )
272
+ const pop = React.createElement('div', { className: 'dswt-pop', role: 'menu', 'aria-label': '主题外观' },
273
+ QUICK_CARDS.map((item) => React.createElement('button', {
274
+ key: item.id, type: 'button', role: 'menuitem', className: 'dswt-pop-item',
275
+ 'data-active': pref === item.id || undefined,
276
+ onClick: () => { props.setTheme(item.id); setOpen(false) },
277
+ },
278
+ React.createElement('span', { className: 'dswt-pop-swatch', style: { backgroundImage: "url('" + item.swatch + "')" } }),
279
+ React.createElement('span', null, item.name),
280
+ )),
281
+ React.createElement('button', {
282
+ key: 'system', type: 'button', role: 'menuitem', className: 'dswt-pop-item',
283
+ 'data-active': !isCustom(pref) || undefined,
284
+ onClick: () => { props.setTheme('system'); setOpen(false) },
285
+ },
286
+ React.createElement('span', { className: 'dswt-pop-swatch dswt-pop-swatch-system' }, '◐'),
287
+ React.createElement('span', null, '默认 System'),
288
+ ),
289
+ )
290
+ return React.createElement('div', { className: 'dswt-foot' },
291
+ btn,
292
+ open ? React.createElement(React.Fragment, null,
293
+ React.createElement('button', { type: 'button', className: 'dswt-pop-backdrop', 'aria-hidden': 'true', tabIndex: -1, onClick: () => setOpen(false) }),
294
+ pop,
295
+ ) : null,
296
+ )
297
+ }
298
+
299
+ /* ── plugin face (the wrapper assigns exports.apply / exports.inject) ── */
300
+
301
+ /** Hard service dependencies: theme registry and the slot system. */
302
+ const inject = ['theme', 'slots']
303
+
304
+ function apply(ctx) {
305
+ const theme = ctx.theme
306
+ const slots = ctx.slots
307
+
308
+ // 1. Register the three themes; disposers are fiber-owned so a stop or
309
+ // update unregisters them (an active custom preference resets safely).
310
+ for (const spec of THEME_SPECS) {
311
+ const dispose = theme.register(buildThemeDef(spec))
312
+ ctx.effect(() => dispose)
313
+ }
314
+
315
+ // 2. Wallpaper rule + switcher styles, fiber-owned. NOTE: ctx.effect(execute)
316
+ // runs `execute` immediately and treats its RETURN as the unload cleanup —
317
+ // `() => { style.remove() }` would remove the styles right away, so the
318
+ // body returns a cleanup function instead.
319
+ const style = document.createElement('style')
320
+ style.textContent = WALLPAPER_CSS + '\n' + UI_CSS
321
+ document.head.appendChild(style)
322
+ ctx.effect(() => () => { style.remove() })
323
+
324
+ // 3. Live preference state for both entry points.
325
+ let preference = theme.getTheme().preference
326
+ const listeners = new Set()
327
+ const disposeEvent = ctx.on('theme/change', (snapshot) => {
328
+ preference = snapshot.preference
329
+ for (const fn of Array.from(listeners)) fn(preference)
330
+ })
331
+ ctx.effect(() => disposeEvent)
332
+ const face = () => ({
333
+ subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn) } },
334
+ snapshot: () => preference,
335
+ setTheme: (id) => theme.setTheme(id),
336
+ })
337
+
338
+ // 4. Additive entry points.
339
+ slots.inject('settings.general.item', () => slots.register(
340
+ { name: 'settings.general.item', id: 'dsh-theme-triptych-appearance', order: 11, inject: face },
341
+ ThemeRow,
342
+ ))
343
+ slots.inject('sidebar.footer.action', () => slots.register(
344
+ { name: 'sidebar.footer.action', id: 'dsh-theme-triptych-quick', order: 10, inject: face },
345
+ ThemeFooterAction,
346
+ ))
347
+ }
348
+
349
+ exports.apply = apply;
350
+ exports.inject = inject;
351
+ return module.exports;
352
+ }
353
+ });
package/lib/index.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * dsh-theme-triptych — node half.
3
+ *
4
+ * Mounts as an ordinary host loader row in the deployment composition. Its one
5
+ * job is serving the three wallpaper images over the deployment's own HTTP
6
+ * carrier (`webServer`) so the browser theme can reference same-origin URLs.
7
+ * The browser half ships through exports["./client"] (see src/client/index.js).
8
+ *
9
+ * The row is a hard injection of `webServer` (same as the client-modules node
10
+ * half): without it the fiber parks until the carrier exists — a bare
11
+ * ctx.get() at apply time races boot order and silently registers nothing.
12
+ */
13
+ import { readFile } from 'node:fs/promises'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ /** Package-root assets directory (lib/../assets). */
17
+ const ASSETS_DIR = fileURLToPath(new URL('../assets/', import.meta.url))
18
+
19
+ /** Route table: URL path → asset file. Keep in sync with src/client/index.js WALLPAPER URLs. */
20
+ const ROUTES = [
21
+ { path: '/dsh-theme-triptych/nexus.jpg', file: 'image_nexus.jpg', contentType: 'image/jpeg' },
22
+ { path: '/dsh-theme-triptych/comiket.jpg', file: 'image_comiket.jpg', contentType: 'image/jpeg' },
23
+ { path: '/dsh-theme-triptych/ironcore.jpg', file: 'image_ironcore.jpg', contentType: 'image/jpeg' },
24
+ ]
25
+
26
+ /** Hard dependency: the browser HTTP carrier (the fiber waits for it). */
27
+ export const inject = ['webServer']
28
+
29
+ /** Host plugin body. */
30
+ export function apply(ctx) {
31
+ const webServer = ctx.webServer
32
+ for (const route of ROUTES) {
33
+ const dispose = webServer.register({
34
+ kind: 'exact',
35
+ path: route.path,
36
+ handler: async (req, res) => {
37
+ try {
38
+ const bytes = await readFile(ASSETS_DIR + route.file)
39
+ res.writeHead(200, {
40
+ 'content-type': route.contentType,
41
+ 'content-length': String(bytes.length),
42
+ 'cache-control': 'public, max-age=86400',
43
+ })
44
+ res.end(bytes)
45
+ } catch (err) {
46
+ try {
47
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
48
+ res.end('wallpaper not found')
49
+ } catch (err2) {
50
+ /* response already sent or connection gone */
51
+ }
52
+ }
53
+ },
54
+ })
55
+ ctx.effect(() => dispose)
56
+ }
57
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "dsh-theme-triptych",
3
+ "version": "0.1.0",
4
+ "description": "DeepSeek Harness appearance theme pack — NEXUS / COMIKET / IRONCORE, three themed editions (palettes from keyshub.top / isolo-news) with wallpapers, a settings row and a sidebar quick switcher",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/iMocking/dsh-theme-triptych.git"
13
+ },
14
+ "homepage": "https://github.com/iMocking/dsh-theme-triptych",
15
+ "bugs": {
16
+ "url": "https://github.com/iMocking/dsh-theme-triptych/issues"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "default": "./lib/index.js"
21
+ },
22
+ "./client": {
23
+ "default": "./lib/client.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "dsh": {
28
+ "client": {
29
+ "inject": [
30
+ "@deepseek-ai/dsh-client-runtime",
31
+ "@deepseek-ai/dsh-client-ui-theme"
32
+ ],
33
+ "platform": "web"
34
+ }
35
+ },
36
+ "files": [
37
+ "lib",
38
+ "assets",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "node scripts/build.mjs",
44
+ "test": "node test/smoke.mjs"
45
+ },
46
+ "keywords": [
47
+ "deepseek-harness",
48
+ "deepseek",
49
+ "dsh",
50
+ "dsh-plugin",
51
+ "plugin",
52
+ "theme",
53
+ "wallpaper",
54
+ "appearance",
55
+ "triptych",
56
+ "nexus",
57
+ "comiket",
58
+ "ironcore",
59
+ "cordis",
60
+ "client-plugin"
61
+ ],
62
+ "license": "MIT"
63
+ }