dsh-theme-neumorph 0.0.0-stage → 1.0.2

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) 2026 dsh-theme-neumorph 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 CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # DSH 新拟态主题 (dsh-theme-neumorph)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/wcnmamrs/dsh-theme-neumorph/actions/workflows/ci.yml/badge.svg)](https://github.com/wcnmamrs/dsh-theme-neumorph/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/dsh-theme-neumorph.svg)](https://www.npmjs.com/package/dsh-theme-neumorph)
5
+ [![license](https://img.shields.io/npm/l/dsh-theme-neumorph.svg)](LICENSE)
6
+
7
+ 给 Harness 网页界面换上一层**新拟态**(neumorphism / soft UI)皮肤:冷灰表面、钢蓝强调色、柔和的单向与双向软阴影,浅色与深色模式同时生效。
8
+
9
+ ## 截图
10
+
11
+ 深色模式(浅色模式同一套配色,换成 `#e0e5ec` 冷灰底):
12
+
13
+ | 首页与输入区 | 侧栏与会话列表 |
14
+ | --- | --- |
15
+ | ![首页](https://raw.githubusercontent.com/wcnmamrs/dsh-theme-neumorph/main/assets/screenshot-1.jpg) | ![侧栏](https://raw.githubusercontent.com/wcnmamrs/dsh-theme-neumorph/main/assets/screenshot-2.jpg) |
16
+
17
+ ## 它做了什么
18
+
19
+ 主题由两层组成,彼此独立、都可回退:
20
+
21
+ | 层 | 手段 | 作用 |
22
+ | --- | --- | --- |
23
+ | 颜色层 | `ctx.theme.overrideTokens()` | 覆盖 72 个 `--dsw-*` 颜色与圆角 token:表面、按钮填充、代码面板、浮动层、边框、文字、品牌色、状态色、圆角刻度 |
24
+ | 阴影层 | 注入一张 `<style>` | 新拟态必须靠成对的软阴影表达,而 token 只能装颜色,所以阴影单独成表 |
25
+
26
+ 配色基线:
27
+
28
+ - 冷灰 `#e0e5ec` 系表面,钢蓝 `#4a6b8a` 强调(深色模式自动切换为 `#262b31` / `#7ea6c9`)
29
+ - 高光 `rgba(163,177,198,.6)` 与暗部 `#ffffff` 组成双缘阴影;深色模式改用低透明度白光与深黑
30
+
31
+ 浮雕只落在四处:卡片柔和投影、输入区内凹、输入区大板与菜单/对话框的双向浮雕、侧栏沟槽。整宽、背景透明的控件(列表行、菜单项、分段控件)不带投影——它们一旦带投影,会在圆角之外画出一条“幻影边线”,看起来像多余的边框。
32
+
33
+ ## 边界
34
+
35
+ 主题只动宿主自己画的界面,不碰第三方插件:
36
+
37
+ - 宿主的类名是 CSS Module 的 `<hash>_<local>` 形式,规则按整类名锚定(如 `[class$="_card"]`),不会误伤卡片内部的零件。
38
+ - 每条子串锚定(`[class*="_title"]` 这类)都带 `:not([class*="__"])`:宿主是**单**下划线,别人家的 BEM 是**双**下划线,这一条挡住的正是 `block__title`。
39
+ - 没有一条规则按裸标签生效:`span` 不被当成文字,`button` 不被当成按钮。允许的唯一例外是宿主的表单与代码面(`textarea`、`input`、`select`、`pre`),测试会逐条核对。
40
+ - 文本框的焦点指示不用宿主的外扩轮廓(在软表面上像一圈多余的蓝边),改成画在框内侧的 1.5px 钢蓝内缘——键盘用户仍然看得到光标落在哪个框。
41
+
42
+ ## 目录结构
43
+
44
+ ```
45
+ .
46
+ ├── package.json # bundle 清单: dsh.bundle.patch + dsh.client
47
+ ├── cordis.patch.yml # 把本插件 insert 进 profile 的 Loader 树
48
+ ├── index.js # Host 半: 空实现,只作为 bundle 的一行存在
49
+ ├── client.js # Client 半: token 覆盖 + 阴影样式表
50
+ ├── locale/{zh,en}.json # 插件列表里的标题与描述
51
+ ├── icon.svg # 原创图标
52
+ ├── test/ # 不依赖浏览器的断言
53
+ │ ├── harness.mjs # 按 __ModuleLoader__ 契约加载 client.js
54
+ │ └── theme.test.mjs # 调色板 / 样式表结构 / 卸载路径
55
+ ├── scripts/
56
+ │ └── verify-manifest.mjs # 清单与打包自检
57
+ └── .github/workflows/ci.yml
58
+ ```
59
+
60
+ ## 实现要点
61
+
62
+ - Client 半通过 `window.__ModuleLoader__.load({ id, factory })` 注册,`factory` 返回 `{ inject: ['theme'], apply(ctx) }`。
63
+ - 所有副作用都包在 `ctx.effect()` 里并返回清理函数,插件停用时撤销 token 覆盖层并移除样式表,界面立刻回到原生外观。
64
+ - 若宿主某天拒绝含 `--dsw-radius-*` 的 token 表,会自动退回只含颜色的 66 项重试。
65
+ - 样式规则用整类名锚定宿主 CSS Module,哈希前缀随版本变化也不会失效。
66
+
67
+ ## 开发
68
+
69
+ 不需要浏览器,也不需要安装依赖(`node --test` 自带):
70
+
71
+ ```bash
72
+ npm test # 7 项断言:调色板、样式表结构、卸载路径
73
+ npm run check # 语法检查
74
+ node scripts/verify-manifest.mjs # 清单/打包自检
75
+ npm run verify # 以上全部
76
+ ```
77
+
78
+ 测试用一个假的 `window.__ModuleLoader__` 与 `document` 载入 `client.js`,因此断言的正是宿主实际调用的那条路径。其中三条是这次踩过坑之后加的护栏:
79
+
80
+ - 样式表里不允许出现裸标签选择器(`body span`、`body button`),
81
+ - 不允许出现第三方插件命名(曾经把 `dshwv-*` 的图标清空),
82
+ - 卡片只允许整类名锚定,不允许 `[class*="_card"]` 这种会命中 `_cardContent` 的子串锚定。
83
+
84
+ ## 许可
85
+
86
+ MIT
package/client.js ADDED
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Client half of the Neumorph theme.
3
+ *
4
+ * Neumorphism cannot be expressed with colour tokens alone: it needs paired
5
+ * soft shadows (a light rim and a dark rim) and rounded corners. So this
6
+ * plugin does three things:
7
+ *
8
+ * 1. stacks a token override layer on `ctx.theme` that collapses every
9
+ * surface token (base, layer-1..3, overlay, menu, selector, tip, button
10
+ * fills, code panels, scrollbars) onto one cool-grey family, so no hard
11
+ * white block survives to break the illusion;
12
+ * 2. rounds the shared radius tokens, so corners are soft everywhere;
13
+ * 3. injects a stylesheet that gives controls, fields, panels and the
14
+ * composer the raised / pressed shadow pair.
15
+ *
16
+ * Everything is registered inside `ctx.effect`, so disabling the plugin
17
+ * restores the host look exactly.
18
+ */
19
+
20
+ window.__ModuleLoader__.load({
21
+ id: 'dsh-theme-neumorph',
22
+ factory(require) {
23
+ const SOURCE = 'dsh-theme-neumorph';
24
+
25
+ /**
26
+ * The palette is a handful of families, not 72 unrelated values. Naming
27
+ * them once keeps the table honest: every surface below is literally the
28
+ * same colour, so nothing can drift back to a slightly-off grey.
29
+ */
30
+ /** The page itself: every surface, card and chip sits on this one colour. */
31
+ const SURFACE = { light: '#e0e5ec', dark: '#262b31' };
32
+ /** Chips that must vanish into the surface instead of drawing a plate. */
33
+ const FLAT = { light: 'transparent', dark: 'transparent' };
34
+ /** Scrollbar thumb, resting and hovered. */
35
+ const SCROLL = { light: '#c9d2dd', dark: '#3a424a' };
36
+ const SCROLL_HOVER = { light: '#b9c4d1', dark: '#4a535d' };
37
+ /** Hover wash for rows and triggers: a shade, never a second colour. */
38
+ const HOVER = { light: '#d8dfe8', dark: '#2c333b' };
39
+ /** Floating surfaces that must stay legible above everything else. */
40
+ const FLOAT = { light: '#333d49', dark: '#3a444f' };
41
+
42
+ /** Cool-grey soft-UI palette: one surface family, steel-blue accent. */
43
+ const TOKENS = {
44
+ // ---- surfaces: ONE colour, shadows do all the separating --------------
45
+ // Neumorphism is monochrome: every surface is the same colour as the page,
46
+ // and only light and shade tell them apart. Anything that used to be a
47
+ // slightly darker chip (icon plates, label plates, code bars, group
48
+ // headers) therefore disappears into the surface instead of showing as a
49
+ // rectangle behind the text.
50
+ '--dsw-alias-bg-base': SURFACE,
51
+ '--dsw-alias-bg-layer-1': SURFACE,
52
+ '--dsw-alias-bg-layer-2': SURFACE,
53
+ '--dsw-alias-bg-layer-3': SURFACE,
54
+ '--dsw-alias-bg-overlay': SURFACE,
55
+ '--dsw-alias-bg-module-platform': SURFACE,
56
+ '--dsw-alias-bg-document-preview': SURFACE,
57
+ '--dsw-alias-bg-multi-select': SURFACE,
58
+ '--dsw-specific-sidebar-fill': SURFACE,
59
+ '--dsw-specific-input-major': SURFACE,
60
+ '--dsw-specific-selector': FLAT,
61
+ '--dsw-specific-tip': FLAT,
62
+ '--dsw-specific-login-input': SURFACE,
63
+ '--dsw-menu-surface-fill': { light: '#e0e5ecf5', dark: '#262b31f5' },
64
+ '--dsw-alias-menu-group-header-fill': FLAT,
65
+ '--dsw-specific-bubble': FLAT,
66
+ '--dsw-specific-bubble-highlight': FLAT,
67
+ '--dsw-specific-sidebar-nav-item-active': FLAT,
68
+ '--dsw-specific-sidebar-nav-item-active-accent': FLAT,
69
+
70
+ // ---- button fills: the same colour, relief comes from the shadow -----
71
+ '--dsw-alias-button-elevated-fill': FLAT,
72
+ '--dsw-alias-button-floating-fill': FLAT,
73
+ '--dsw-alias-button-floating-hover': FLAT,
74
+ '--dsw-alias-button-ghost-active-fill': FLAT,
75
+ '--dsw-alias-button-ghost-active-hover': FLAT,
76
+ '--dsw-alias-button-primary-dimmed': FLAT,
77
+ '--dsw-alias-button-contrast-fill': { light: '#3a4450', dark: '#c9d3de' },
78
+ '--dsw-alias-interactive-bg-hover-solid': FLAT,
79
+
80
+ // ---- code and inline panels: recessed, not tinted --------------------
81
+ '--dsw-alias-markdown-code-block': FLAT,
82
+ '--dsw-alias-markdown-code-block-banner': FLAT,
83
+ '--dsw-alias-markdown-inline-code': FLAT,
84
+ '--dsw-alias-markdown-code-segment-selected': FLAT,
85
+ '--dsw-alias-markdown-code-segment-unselected': FLAT,
86
+ '--dsw-alias-markdown-citation': FLAT,
87
+ '--dsw-alias-markdown-tag': FLAT,
88
+ '--dsw-alias-markdown-placeholder': FLAT,
89
+
90
+ // ---- floating chrome: the only things allowed to differ --------------
91
+ '--dsw-alias-toast-bg': FLOAT,
92
+ '--dsw-alias-tooltip-bg': FLOAT,
93
+ '--dsw-alias-switch-thumb': { light: '#f2f5f9', dark: '#cfd6df' },
94
+ '--dsw-alias-scrollbar-bg-l1': SCROLL,
95
+ '--dsw-alias-scrollbar-bg-l2': SCROLL,
96
+ '--dsw-alias-scrollbar-hover-l1': SCROLL_HOVER,
97
+ '--dsw-alias-scrollbar-hover-l2': SCROLL_HOVER,
98
+
99
+ // ---- hairlines: effectively gone, shadows replace them ---------------
100
+ '--dsw-alias-settings-card-fill': SURFACE,
101
+ '--dsw-alias-onboarding-card-fill': SURFACE,
102
+ '--dsw-alias-onboarding-secondary-fill': SURFACE,
103
+ '--dsw-alias-turn-trigger-bg': SURFACE,
104
+ '--dsw-alias-turn-trigger-bg-hover': HOVER,
105
+ '--dsw-alias-button-tool-bar-fill': SURFACE,
106
+ '--dsw-alias-tooltip-key-bg': { light: '#3a444f', dark: '#3a444f' },
107
+ '--dsw-alias-bg-skeleton': HOVER,
108
+ '--dsw-alias-border-l1': { light: '#0000000a', dark: '#ffffff0a' },
109
+ '--dsw-alias-border-l2': { light: '#00000014', dark: '#ffffff14' },
110
+ '--dsw-alias-border-l3': { light: '#00000018', dark: '#ffffff18' },
111
+ '--dsw-alias-border-l4': { light: '#0000001e', dark: '#ffffff1e' },
112
+
113
+ // ---- type -------------------------------------------------------------
114
+ '--dsw-alias-label-primary': { light: '#333d49', dark: '#e4e9ef' },
115
+ '--dsw-alias-label-secondary': { light: '#6b7683', dark: '#9aa5b1' },
116
+ '--dsw-alias-label-tertiary': { light: '#7d8794', dark: '#8b96a3' },
117
+ '--dsw-alias-label-caption': { light: '#8d97a3', dark: '#7f8a97' },
118
+ '--dsw-alias-link': { light: '#4a6b8a', dark: '#8fb4d4' },
119
+
120
+ // ---- steel-blue accent ------------------------------------------------
121
+ '--dsw-alias-brand-primary': { light: '#4a6b8a', dark: '#7ea6c9' },
122
+ '--dsw-alias-button-info-fill': { light: '#4a6b8a', dark: '#5f86ab' },
123
+ '--dsw-alias-button-info-hover': { light: '#55789a', dark: '#6d93b7' },
124
+
125
+ // ---- states -----------------------------------------------------------
126
+ '--dsw-alias-state-error-primary': { light: '#b4463c', dark: '#e08379' },
127
+ '--dsw-alias-state-success-primary': { light: '#3f7d5f', dark: '#6fb894' },
128
+ '--dsw-alias-state-warn-primary': { light: '#a8722c', dark: '#d9a25c' },
129
+ '--dsw-alias-state-idle-primary': { light: '#b9c3cf', dark: '#5b6672' },
130
+
131
+ // ---- soft corners everywhere -----------------------------------------
132
+ '--dsw-radius-xs': { light: '6px', dark: '6px' },
133
+ '--dsw-radius-sm': { light: '10px', dark: '10px' },
134
+ '--dsw-radius-md': { light: '14px', dark: '14px' },
135
+ '--dsw-radius-lg': { light: '18px', dark: '18px' },
136
+ '--dsw-radius-xl': { light: '22px', dark: '22px' },
137
+ '--dsw-radius-panel': { light: '26px', dark: '26px' },
138
+ };
139
+
140
+ const CSS = `
141
+ body {
142
+ --qn-hi: #ffffff;
143
+ --qn-lo: rgba(163, 177, 198, 0.6);
144
+ --qn-out-sm: -2px -2px 5px var(--qn-hi), 3px 3px 6px var(--qn-lo);
145
+ --qn-out: -4px -4px 10px var(--qn-hi), 5px 5px 12px var(--qn-lo);
146
+ --qn-out-lg: -5px -5px 14px var(--qn-hi), 7px 7px 17px var(--qn-lo);
147
+ --qn-in-sm: inset 2px 2px 5px var(--qn-lo), inset -2px -2px 5px var(--qn-hi);
148
+ --qn-in: inset 3px 3px 7px var(--qn-lo), inset -3px -3px 7px var(--qn-hi);
149
+ /* Repeated controls and list rows cast downward only: a rim light on every
150
+ item in a stack reads as alternating high/low, which is exactly what
151
+ neumorphism must avoid. One light source, one direction. */
152
+ --qn-drop-1: rgba(163, 177, 198, 0.6);
153
+ --qn-drop-2: rgba(90, 110, 135, 0.1);
154
+ --qn-drop: 0 2px 6px var(--qn-drop-1), 0 1px 2px var(--qn-drop-2);
155
+ --qn-drop-lift: 0 3px 9px var(--qn-drop-1), 0 1px 3px var(--qn-drop-2);
156
+ }
157
+
158
+ body[data-ds-dark-theme] {
159
+ --qn-hi: rgba(255, 255, 255, 0.055);
160
+ --qn-lo: rgba(0, 0, 0, 0.62);
161
+ --qn-drop-1: rgba(0, 0, 0, 0.55);
162
+ --qn-drop-2: rgba(0, 0, 0, 0.35);
163
+ }
164
+
165
+ /* ---- text is flat ------------------------------------------------------
166
+ Only elements the harness itself styles are touched, and only by name:
167
+ every host class is a CSS-module pair of the form <hash>_<local>, so a
168
+ class that contains an underscore is host markup. Plugin UI names its own
169
+ elements (dshwv-menu-btn and friends) and is left alone.
170
+ Nothing here targets a bare tag: a span is not text until it says so. */
171
+ /* Same rule, for the pieces of text that are wrapped in a div rather than a
172
+ span: a label, a title, a value. They are content too. */
173
+ body :is([class*="_label"], [class*="_title"], [class*="_value"], [class*="_text"], [class*="_name"], [class*="_desc"], [class*="_caption"], [class*="_hint"], [class*="_meta"], [class*="_timestamp"]):not([class*="__"]) {
174
+ background-color: transparent !important;
175
+ box-shadow: none !important;
176
+ }
177
+
178
+ /* ---- controls: soft, raised --------------------------------------------
179
+ Buttons are objects again: a filled shape with the same paired light and
180
+ shade as the surface around it. Full-width rows and every control that
181
+ lives inside a menu, a tab strip or a picker are excluded, because a
182
+ shadow under a row that spans the panel reads as a stray line, and a
183
+ stack of them reads as alternating high and low. */
184
+ body :where(button, [role="button"])[class*="_"]:not([class*="__"]):not([aria-checked]):not([role="checkbox"]):not([role="radio"]):not([class*="_primary"]):not([role="menuitem"]):not([role="option"]):not([role="tab"]):not([class*="_callRow"]):not([class*="_panelRow"]):not([class*="_cardContent"]):not([class*="_row"]):not([class*="_tab"]):not([class*="_option"]):not([class*="_cell"]):not([class*="_switcher"]):not([class*="_segmented"]):not(:is([role="menu"], [role="tablist"], [role="listbox"], [data-menu-material], [class*="_tabs"], [class*="_groups"], [class*="_menu"]) *) {
185
+ border-radius: 12px !important;
186
+ border-color: transparent !important;
187
+ background-color: var(--dsw-alias-bg-base) !important;
188
+ box-shadow: var(--qn-out-sm) !important;
189
+ }
190
+
191
+ body :where(button, [role="button"])[class*="_"]:not([class*="__"]):not([aria-checked]):not([role="checkbox"]):not([role="radio"]):not([class*="_primary"]):not([role="menuitem"]):not([role="option"]):not([role="tab"]):not([class*="_callRow"]):not([class*="_panelRow"]):not([class*="_cardContent"]):not([class*="_row"]):not([class*="_tab"]):not([class*="_option"]):not([class*="_cell"]):not([class*="_switcher"]):not([class*="_segmented"]):not(:is([role="menu"], [role="tablist"], [role="listbox"], [data-menu-material], [class*="_tabs"], [class*="_groups"], [class*="_menu"]) *):hover:not(:disabled) {
192
+ box-shadow: var(--qn-out) !important;
193
+ }
194
+
195
+ body :where(button, [role="button"])[class*="_"]:not([class*="__"]):not([aria-checked]):not([role="checkbox"]):not([role="radio"]):not([class*="_primary"]):not([role="menuitem"]):not([role="option"]):not([role="tab"]):not([class*="_callRow"]):not([class*="_panelRow"]):not([class*="_cardContent"]):not([class*="_row"]):not([class*="_tab"]):not([class*="_option"]):not([class*="_cell"]):not([class*="_switcher"]):not([class*="_segmented"]):not(:is([role="menu"], [role="tablist"], [role="listbox"], [data-menu-material], [class*="_tabs"], [class*="_groups"], [class*="_menu"]) *):active:not(:disabled) {
196
+ box-shadow: var(--qn-in-sm) !important;
197
+ }
198
+
199
+ /* The send button is a filled pill and keeps one soft shadow of its own. */
200
+ body :where(button, [role="button"])[class*="_primary"]:not([class*="__"]) {
201
+ border-radius: 999px !important;
202
+ border-color: transparent !important;
203
+ box-shadow: 0 3px 8px rgba(74, 107, 138, 0.28) !important;
204
+ }
205
+
206
+ body :where(button, [role="button"])[class*="_primary"]:not([class*="__"]):hover:not(:disabled) {
207
+ box-shadow: 0 4px 10px rgba(74, 107, 138, 0.34) !important;
208
+ }
209
+
210
+ body :where(button, [role="button"])[class*="_primary"]:not([class*="__"]):active:not(:disabled) {
211
+ box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.26) !important;
212
+ }
213
+
214
+ /* ---- text fields: pressed into the surface ------------------------------ */
215
+ body :where(
216
+ textarea,
217
+ input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
218
+ [contenteditable="true"]
219
+ ) {
220
+ box-shadow: var(--qn-in) !important;
221
+ border-radius: 14px !important;
222
+ border-color: transparent !important;
223
+ }
224
+
225
+ /* ---- cards: the only panels that get relief ----------------------------
226
+ Deliberately narrow. Rows, tabs, chips, list items and every other small
227
+ block stay flat: a shadow on each of them turns the page into a field of
228
+ pillows, which is not what neumorphism is for. */
229
+ body [class$="_card"],
230
+ body [class*="_card "]:not([class*="__"]) {
231
+ box-shadow: var(--qn-drop) !important;
232
+ border-color: transparent !important;
233
+ }
234
+
235
+ /* ---- composer: the one big soft slab ------------------------------------ */
236
+ body [class*="_composerSeat"]:not([class*="__"]) [class$="_card"],
237
+ body [class*="_composerSeat"]:not([class*="__"]) [class*="_card "]:not([class*="__"]),
238
+ body [class*="_composerHero"]:not([class*="__"]) [class$="_card"],
239
+ body [class*="_composerHero"]:not([class*="__"]) [class*="_card "]:not([class*="__"]) {
240
+ box-shadow: var(--qn-out) !important;
241
+ border-radius: 22px !important;
242
+ border-color: transparent !important;
243
+ }
244
+
245
+ /* ---- floating surfaces: menus, dialogs, tooltips ------------------------ */
246
+ body :where([role="menu"], [role="dialog"], [role="listbox"], [role="tooltip"]),
247
+ body [data-menu-material],
248
+ body :is([class$="_menu"], [class*="_menu "], [class$="_dialog"], [class*="_dialog "]):not([class*="__"]) {
249
+ border-radius: 20px !important;
250
+ box-shadow: var(--qn-out-lg) !important;
251
+ }
252
+
253
+ /* The menu panel carries the fill; its material layer has to inherit the
254
+ rounding, otherwise the list paints over the corners. */
255
+ body [data-menu-material] > [aria-hidden="true"] {
256
+ border-radius: inherit !important;
257
+ }
258
+
259
+ /* ---- session tree: one soft row per session ----------------------------- */
260
+ body [class*="_sessionRow"]:not([class*="__"]),
261
+ body [class*="_projectRow"]:not([class*="__"]) {
262
+ border-radius: 12px !important;
263
+ box-shadow: var(--qn-drop) !important;
264
+ }
265
+
266
+ body [class*="_sessionRow"]:not([class*="__"]):hover,
267
+ body [class*="_projectRow"]:not([class*="__"]):hover {
268
+ box-shadow: var(--qn-drop-lift) !important;
269
+ }
270
+
271
+ /* ---- code blocks: recessed ---------------------------------------------- */
272
+ body :where(pre) {
273
+ box-shadow: var(--qn-in-sm) !important;
274
+ border-radius: 14px !important;
275
+ border-color: transparent !important;
276
+ }
277
+
278
+ /* ---- sidebar: a faint groove instead of a divider line ------------------ */
279
+ body [class*="_sidebarCol"]:not([class*="__"]) {
280
+ box-shadow: inset -8px 0 14px -14px var(--qn-lo) !important;
281
+ }
282
+
283
+ /* ---- focus: a pressed rim, drawn inside the field ----------------------
284
+ The host ring is an outer halo, which on a soft surface reads as a stray
285
+ blue edge beside the box (and it cannot be seen at all once the field has
286
+ the recessed shadow). So the outer ring is dropped and replaced by a rim
287
+ inside the field, in the same accent a selected control uses. It exists
288
+ only while the field has focus: without it a keyboard user has no way to
289
+ tell which box the caret is in. */
290
+ body :where(textarea, input, select, [contenteditable="true"]):focus {
291
+ outline: none !important;
292
+ }
293
+
294
+ body :where(textarea, input, select, [contenteditable="true"]):focus-visible {
295
+ outline: none !important;
296
+ box-shadow: var(--qn-in), inset 0 0 0 1.5px var(--dsw-alias-brand-primary) !important;
297
+ }
298
+
299
+ body :where(button, [role="button"], a):focus-visible {
300
+ outline: 2px solid var(--dsw-alias-brand-primary);
301
+ outline-offset: 2px;
302
+ }
303
+
304
+ `;
305
+
306
+ return {
307
+ inject: ['theme'],
308
+ apply(ctx) {
309
+ // Colour table: `overrideTokens` returns the disposer for exactly this
310
+ // layer. The radius tokens are the only non-colour entries, so they are
311
+ // what gets dropped first if a host build ever rejects the table.
312
+ const overrideTokens = () => {
313
+ try {
314
+ return ctx.theme.overrideTokens(SOURCE, TOKENS);
315
+ } catch (error) {
316
+ const colorsOnly = Object.fromEntries(
317
+ Object.entries(TOKENS).filter(([name]) => !name.startsWith('--dsw-radius-')),
318
+ );
319
+ console.warn('[dsh-theme-neumorph] full token table rejected, retrying without radius tokens', error);
320
+ return ctx.theme.overrideTokens(SOURCE, colorsOnly);
321
+ }
322
+ };
323
+
324
+ // Everything the plugin adds is created and released inside one effect:
325
+ // a stylesheet inserted before the effect registered would outlive a
326
+ // throw here, and a second apply would stack a second copy of it.
327
+ ctx.effect(() => {
328
+ // Shadow layer: neumorphism needs paired soft shadows, and a theme
329
+ // token can only carry a colour, so the shadows are a stylesheet of
330
+ // their own.
331
+ const style = document.createElement('style');
332
+ style.dataset.dshTheme = SOURCE;
333
+ style.textContent = CSS;
334
+ const root = document.head ?? document.documentElement;
335
+ root.appendChild(style);
336
+
337
+ let disposeTokens = null;
338
+ try {
339
+ disposeTokens = overrideTokens();
340
+ } catch (error) {
341
+ // A host that rejects the table outright still gets the shadows, and
342
+ // the sheet is still owned by this effect: report, do not throw.
343
+ console.warn('[dsh-theme-neumorph] token layer rejected, shadows only', error);
344
+ }
345
+
346
+ return () => {
347
+ disposeTokens?.();
348
+ style.remove();
349
+ };
350
+ });
351
+ },
352
+ };
353
+ },
354
+ });
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-theme-neumorph
3
+ name: dsh-theme-neumorph
package/icon.svg ADDED
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
2
+ <defs>
3
+ <linearGradient id="qn-surface" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#eef2f7" />
5
+ <stop offset="1" stop-color="#d5dce6" />
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="48" height="48" rx="12" fill="#e0e5ec" />
9
+ <rect x="10" y="10" width="28" height="28" rx="9" fill="url(#qn-surface)" />
10
+ <path d="M10 19a9 9 0 0 1 9-9h10a9 9 0 0 1 9 9v10a9 9 0 0 1-9 9H19a9 9 0 0 1-9-9z"
11
+ fill="none" stroke="#ffffff" stroke-width="2" opacity="0.9" />
12
+ <circle cx="24" cy="24" r="6" fill="#4a6b8a" />
13
+ <circle cx="22" cy="22" r="2" fill="#ffffff" opacity="0.55" />
14
+ </svg>
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Host half of the Neumorph theme bundle.
3
+ *
4
+ * Everything the theme does is a pure Client concern: `client.js` stacks a
5
+ * token override layer on `ctx.theme` and injects the soft-UI shadow
6
+ * stylesheet. This half only needs to exist as the bundle's Host row.
7
+ */
8
+ export function apply() {}
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH Neumorph theme",
4
+ "description": "Cool-grey surfaces, steel-blue accent, soft shadows."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH 新拟态主题",
4
+ "description": "冷灰表面、钢蓝强调、柔和阴影。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "dsh-theme-neumorph",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.2",
4
+ "description": "DSH 新拟态主题插件:冷灰表面、钢蓝强调、柔和阴影。",
5
+ "license": "MIT",
6
+ "author": "wcnmamrs",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./client": "./client.js",
11
+ "./locale/*.json": "./locale/*.json",
12
+ "./icon": "./icon.svg"
13
+ },
14
+ "files": [
15
+ "index.js",
16
+ "client.js",
17
+ "cordis.patch.yml",
18
+ "locale/*.json",
19
+ "icon.svg",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "icon": "./icon.svg",
24
+ "keywords": [
25
+ "dsh",
26
+ "dsh-plugin",
27
+ "deepseek-harness",
28
+ "theme",
29
+ "neumorphism",
30
+ "soft-ui",
31
+ "skin"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/wcnmamrs/dsh-theme-neumorph.git"
36
+ },
37
+ "homepage": "https://github.com/wcnmamrs/dsh-theme-neumorph#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/wcnmamrs/dsh-theme-neumorph/issues"
40
+ },
41
+ "dsh": {
42
+ "bundle": {
43
+ "patch": "./cordis.patch.yml"
44
+ },
45
+ "client": {
46
+ "platform": "web",
47
+ "immediately": true,
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-ui-theme"
50
+ ]
51
+ },
52
+ "manifestVersion": 1
53
+ },
54
+ "engines": {
55
+ "node": ">=20"
56
+ },
57
+ "scripts": {
58
+ "test": "node --test test/*.test.mjs",
59
+ "check": "node --check client.js && node --check index.js",
60
+ "verify": "npm run check && npm test",
61
+ "pack:check": "npm pack --dry-run"
62
+ }
63
+ }