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.
- package/README.md +7 -4
- package/client.js +54 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# DSH 新拟态主题 (dsh-theme-neumorph)
|
|
2
2
|
|
|
3
3
|
[](https://github.com/wcnmamrs/dsh-theme-neumorph/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/dsh-theme-neumorph)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
7
|
给 Harness 网页界面换上一层**新拟态**(neumorphism / soft UI)皮肤:冷灰表面、钢蓝强调色、柔和的单向与双向软阴影,浅色与深色模式同时生效。
|
|
6
8
|
|
|
@@ -10,7 +12,7 @@
|
|
|
10
12
|
|
|
11
13
|
| 首页与输入区 | 侧栏与会话列表 |
|
|
12
14
|
| --- | --- |
|
|
13
|
-
|  |  |
|
|
15
|
+
|  |  |
|
|
14
16
|
|
|
15
17
|
## 它做了什么
|
|
16
18
|
|
|
@@ -33,8 +35,9 @@
|
|
|
33
35
|
主题只动宿主自己画的界面,不碰第三方插件:
|
|
34
36
|
|
|
35
37
|
- 宿主的类名是 CSS Module 的 `<hash>_<local>` 形式,规则按整类名锚定(如 `[class$="_card"]`),不会误伤卡片内部的零件。
|
|
36
|
-
-
|
|
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 表,会自动退回只含颜色的
|
|
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"],
|
|
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:
|
|
287
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
});
|