dsh-theme-neumorph 1.0.1 → 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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/client.js +54 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # DSH 新拟态主题 (dsh-theme-neumorph)
2
2
 
3
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)
4
6
 
5
7
  给 Harness 网页界面换上一层**新拟态**(neumorphism / soft UI)皮肤:冷灰表面、钢蓝强调色、柔和的单向与双向软阴影,浅色与深色模式同时生效。
6
8
 
@@ -10,7 +12,7 @@
10
12
 
11
13
  | 首页与输入区 | 侧栏与会话列表 |
12
14
  | --- | --- |
13
- | ![首页](assets/screenshot-1.jpg) | ![侧栏](assets/screenshot-2.jpg) |
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) |
14
16
 
15
17
  ## 它做了什么
16
18
 
@@ -33,8 +35,9 @@
33
35
  主题只动宿主自己画的界面,不碰第三方插件:
34
36
 
35
37
  - 宿主的类名是 CSS Module 的 `<hash>_<local>` 形式,规则按整类名锚定(如 `[class$="_card"]`),不会误伤卡片内部的零件。
36
- - 插件的界面用自己的命名(`dshwv-*`、BEM 之类),不被命中。
37
- - 没有一条规则按裸标签生效:`span` 不被当成文字,`button` 不被当成按钮。
38
+ - 每条子串锚定(`[class*="_title"]` 这类)都带 `:not([class*="__"])`:宿主是**单**下划线,别人家的 BEM 是**双**下划线,这一条挡住的正是 `block__title`。
39
+ - 没有一条规则按裸标签生效:`span` 不被当成文字,`button` 不被当成按钮。允许的唯一例外是宿主的表单与代码面(`textarea`、`input`、`select`、`pre`),测试会逐条核对。
40
+ - 文本框的焦点指示不用宿主的外扩轮廓(在软表面上像一圈多余的蓝边),改成画在框内侧的 1.5px 钢蓝内缘——键盘用户仍然看得到光标落在哪个框。
38
41
 
39
42
  ## 目录结构
40
43
 
@@ -58,7 +61,7 @@
58
61
 
59
62
  - Client 半通过 `window.__ModuleLoader__.load({ id, factory })` 注册,`factory` 返回 `{ inject: ['theme'], apply(ctx) }`。
60
63
  - 所有副作用都包在 `ctx.effect()` 里并返回清理函数,插件停用时撤销 token 覆盖层并移除样式表,界面立刻回到原生外观。
61
- - 若宿主某天拒绝含 `--dsw-radius-*` 的 token 表,会自动退回只含颜色的 58 项重试。
64
+ - 若宿主某天拒绝含 `--dsw-radius-*` 的 token 表,会自动退回只含颜色的 66 项重试。
62
65
  - 样式规则用整类名锚定宿主 CSS Module,哈希前缀随版本变化也不会失效。
63
66
 
64
67
  ## 开发
package/client.js CHANGED
@@ -170,7 +170,7 @@ body[data-ds-dark-theme] {
170
170
  Nothing here targets a bare tag: a span is not text until it says so. */
171
171
  /* Same rule, for the pieces of text that are wrapped in a div rather than a
172
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"]) {
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
174
  background-color: transparent !important;
175
175
  box-shadow: none !important;
176
176
  }
@@ -181,33 +181,33 @@ body :is([class*="_label"], [class*="_title"], [class*="_value"], [class*="_text
181
181
  lives inside a menu, a tab strip or a picker are excluded, because a
182
182
  shadow under a row that spans the panel reads as a stray line, and a
183
183
  stack of them reads as alternating high and low. */
184
- body :where(button, [role="button"])[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"]) *) {
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
185
  border-radius: 12px !important;
186
186
  border-color: transparent !important;
187
187
  background-color: var(--dsw-alias-bg-base) !important;
188
188
  box-shadow: var(--qn-out-sm) !important;
189
189
  }
190
190
 
191
- body :where(button, [role="button"])[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) {
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
192
  box-shadow: var(--qn-out) !important;
193
193
  }
194
194
 
195
- body :where(button, [role="button"])[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) {
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
196
  box-shadow: var(--qn-in-sm) !important;
197
197
  }
198
198
 
199
199
  /* The send button is a filled pill and keeps one soft shadow of its own. */
200
- body :where(button, [role="button"])[class*="_primary"] {
200
+ body :where(button, [role="button"])[class*="_primary"]:not([class*="__"]) {
201
201
  border-radius: 999px !important;
202
202
  border-color: transparent !important;
203
203
  box-shadow: 0 3px 8px rgba(74, 107, 138, 0.28) !important;
204
204
  }
205
205
 
206
- body :where(button, [role="button"])[class*="_primary"]:hover:not(:disabled) {
206
+ body :where(button, [role="button"])[class*="_primary"]:not([class*="__"]):hover:not(:disabled) {
207
207
  box-shadow: 0 4px 10px rgba(74, 107, 138, 0.34) !important;
208
208
  }
209
209
 
210
- body :where(button, [role="button"])[class*="_primary"]:active:not(:disabled) {
210
+ body :where(button, [role="button"])[class*="_primary"]:not([class*="__"]):active:not(:disabled) {
211
211
  box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.26) !important;
212
212
  }
213
213
 
@@ -226,16 +226,17 @@ body :where(
226
226
  Deliberately narrow. Rows, tabs, chips, list items and every other small
227
227
  block stay flat: a shadow on each of them turns the page into a field of
228
228
  pillows, which is not what neumorphism is for. */
229
- body [class$="_card"], [class*="_card "], [class$="Card"], [class*="Card "] {
229
+ body [class$="_card"],
230
+ body [class*="_card "]:not([class*="__"]) {
230
231
  box-shadow: var(--qn-drop) !important;
231
232
  border-color: transparent !important;
232
233
  }
233
234
 
234
235
  /* ---- composer: the one big soft slab ------------------------------------ */
235
- body [class*="_composerSeat"] [class$="_card"],
236
- body [class*="_composerSeat"] [class*="_card "],
237
- body [class*="_composerHero"] [class$="_card"],
238
- body [class*="_composerHero"] [class*="_card "] {
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*="__"]) {
239
240
  box-shadow: var(--qn-out) !important;
240
241
  border-radius: 22px !important;
241
242
  border-color: transparent !important;
@@ -244,7 +245,7 @@ body [class*="_composerHero"] [class*="_card "] {
244
245
  /* ---- floating surfaces: menus, dialogs, tooltips ------------------------ */
245
246
  body :where([role="menu"], [role="dialog"], [role="listbox"], [role="tooltip"]),
246
247
  body [data-menu-material],
247
- body :is([class$="_menu"], [class*="_menu "], [class$="_dialog"], [class*="_dialog "]) {
248
+ body :is([class$="_menu"], [class*="_menu "], [class$="_dialog"], [class*="_dialog "]):not([class*="__"]) {
248
249
  border-radius: 20px !important;
249
250
  box-shadow: var(--qn-out-lg) !important;
250
251
  }
@@ -255,19 +256,15 @@ body [data-menu-material] > [aria-hidden="true"] {
255
256
  border-radius: inherit !important;
256
257
  }
257
258
 
258
- body [data-menu-material] {
259
- overflow: hidden !important;
260
- }
261
-
262
259
  /* ---- session tree: one soft row per session ----------------------------- */
263
- body [class*="_sessionRow"],
264
- body [class*="_projectRow"] {
260
+ body [class*="_sessionRow"]:not([class*="__"]),
261
+ body [class*="_projectRow"]:not([class*="__"]) {
265
262
  border-radius: 12px !important;
266
263
  box-shadow: var(--qn-drop) !important;
267
264
  }
268
265
 
269
- body [class*="_sessionRow"]:hover,
270
- body [class*="_projectRow"]:hover {
266
+ body [class*="_sessionRow"]:not([class*="__"]):hover,
267
+ body [class*="_projectRow"]:not([class*="__"]):hover {
271
268
  box-shadow: var(--qn-drop-lift) !important;
272
269
  }
273
270
 
@@ -279,14 +276,24 @@ body :where(pre) {
279
276
  }
280
277
 
281
278
  /* ---- sidebar: a faint groove instead of a divider line ------------------ */
282
- body [class*="_sidebarCol"] {
279
+ body [class*="_sidebarCol"]:not([class*="__"]) {
283
280
  box-shadow: inset -8px 0 14px -14px var(--qn-lo) !important;
284
281
  }
285
282
 
286
- /* ---- focus: visible on controls, never a ring on the text fields -------- */
287
- body :where(textarea, input, select, [contenteditable="true"]):focus,
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
+
288
294
  body :where(textarea, input, select, [contenteditable="true"]):focus-visible {
289
295
  outline: none !important;
296
+ box-shadow: var(--qn-in), inset 0 0 0 1.5px var(--dsw-alias-brand-primary) !important;
290
297
  }
291
298
 
292
299
  body :where(button, [role="button"], a):focus-visible {
@@ -299,14 +306,7 @@ body :where(button, [role="button"], a):focus-visible {
299
306
  return {
300
307
  inject: ['theme'],
301
308
  apply(ctx) {
302
- // Shadow layer: neumorphism needs paired soft shadows, and a theme token
303
- // can only carry a colour, so the shadows are a stylesheet of their own.
304
- const style = document.createElement('style');
305
- style.dataset.dshTheme = SOURCE;
306
- style.textContent = CSS;
307
- document.head.appendChild(style);
308
-
309
- // Colour layer: `overrideTokens` returns the disposer for exactly this
309
+ // Colour table: `overrideTokens` returns the disposer for exactly this
310
310
  // layer. The radius tokens are the only non-colour entries, so they are
311
311
  // what gets dropped first if a host build ever rejects the table.
312
312
  const overrideTokens = () => {
@@ -321,10 +321,30 @@ body :where(button, [role="button"], a):focus-visible {
321
321
  }
322
322
  };
323
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.
324
327
  ctx.effect(() => {
325
- const disposeTokens = overrideTokens();
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
+
326
346
  return () => {
327
- disposeTokens();
347
+ disposeTokens?.();
328
348
  style.remove();
329
349
  };
330
350
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theme-neumorph",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "DSH 新拟态主题插件:冷灰表面、钢蓝强调、柔和阴影。",
5
5
  "license": "MIT",
6
6
  "author": "wcnmamrs",