dsh-theme-neumorph 1.0.2 → 1.0.4
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 +25 -13
- package/client.js +128 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,10 +34,14 @@
|
|
|
34
34
|
|
|
35
35
|
主题只动宿主自己画的界面,不碰第三方插件:
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
37
|
+
- 宿主同时用着**两套** CSS Module 命名:预构建样式表是 `_<local>_<hash>_<line>`(`_card_7gxqk_107`),组件从 JS 注入的样式是 `<hash>_<local>`(`wSkVaW_card`)。所以一个类名要同时锚定两次——`[class$="_card"]` 与 `[class*="_card_"]`——只写一种就会有一半界面纹丝不动。
|
|
38
|
+
- 凡是要**加**规则的地方,锚定的都是**完整的 local 名**,而且一定用下划线夹住(`[class*="_card_"]` 而不是 `[class*="_card"]`),否则会串到 `_cardContent_x_y`、`_labelChip` 这种更长的零件上,卡片的每一块又各得一层投影。只有**排除**项才允许宽泛的片段(`button:not([class*="_row"])` 这类),因为排除范围宽一点只会少上一条浮雕,不会多毁一块界面。
|
|
39
|
+
- 每个类名锚定都带 `:not([class*="__"])`:宿主是**单**下划线,别人家的 BEM 是**双**下划线,这一条挡住的正是 `panel__card`。
|
|
40
|
+
- 裸标签只出现在三处:宿主的表单与代码面(`textarea`、`input`、`select`、`pre`)、可编辑文本(`[contenteditable="true"]`),以及必须同时带宿主类名的控件(`button` / `[role="button"]` / `a` + `[class*="_"]`)。`span` 不被当成文字。
|
|
41
|
+
- 不强制按钮底色。主题里所有表面是同一个颜色,透明按钮本来就与表面齐平;而宿主有些控件是**功能填充**(附件删除键的圆盘用 `--dsw-alias-button-contrast-fill`、叉号用 `--dsw-alias-label-primary-inverted`),一旦被灌成表面色,叉号就在近白底上消失了。所以按钮只加圆角与浮雕,填充留给宿主。
|
|
42
|
+
- 文本会被拍平,但表单控件不会:宿主的重命名输入框类名就叫 `ymsBma_name`,它在文字规则里必须被 `:not(input):not(textarea):not(select)` 放行,否则那块输入框看起来就是一行纯文本。
|
|
43
|
+
- 不是每个 token 都是"板子"。引用竖线(`--dsw-alias-markdown-citation`)、幽灵按钮的选中态(`--dsw-alias-button-ghost-active-fill`)、行内代码(`--dsw-alias-markdown-inline-code`)是宿主的**信号**,置成 `transparent` 等于把它们删掉,所以它们保留一层浅色,而不是跟其他表面一起消失。
|
|
44
|
+
- 文本框的焦点指示不用宿主的外扩轮廓(在软表面上像一圈多余的蓝边),改成画在框内侧的 1.5px 钢蓝内缘;`outline-offset: -2px` 与内凹阴影叠加而不是替换它。checkbox / radio / 原生控件完全不碰,宿主自己的焦点环留给它们。
|
|
41
45
|
|
|
42
46
|
## 目录结构
|
|
43
47
|
|
|
@@ -60,26 +64,34 @@
|
|
|
60
64
|
## 实现要点
|
|
61
65
|
|
|
62
66
|
- Client 半通过 `window.__ModuleLoader__.load({ id, factory })` 注册,`factory` 返回 `{ inject: ['theme'], apply(ctx) }`。
|
|
63
|
-
- 所有副作用都包在 `ctx.effect()`
|
|
64
|
-
- 若宿主某天拒绝含 `--dsw-radius-*` 的 token
|
|
65
|
-
-
|
|
67
|
+
- 所有副作用都包在 `ctx.effect()` 里并返回清理函数:样式表在 effect 内插入(effect 被拒就一张纸都不留),重复 apply 时先删掉自己上一张,停用时先移除样式表再撤销 token 覆盖层,界面立刻回到原生外观。
|
|
68
|
+
- 若宿主某天拒绝含 `--dsw-radius-*` 的 token 表,会退回只含颜色的 66 项重试一次;连重试也被拒时保留阴影层并告警,而不是把主题整个掀掉。这是一条防御路径——目前这版宿主只校验 token 值的形状(是否 `{ light, dark }`),不校验名字,所以实际上不会走到。
|
|
69
|
+
- 颜色层之外的一切都用宿主类名锚定 CSS Module,哈希前缀与文件名随版本变化都不会失效;只有上面那几个裸标签例外。
|
|
66
70
|
|
|
67
71
|
## 开发
|
|
68
72
|
|
|
69
73
|
不需要浏览器,也不需要安装依赖(`node --test` 自带):
|
|
70
74
|
|
|
71
75
|
```bash
|
|
72
|
-
npm test #
|
|
76
|
+
npm test # 19 项断言:调色板、样式表结构、生命周期、卸载路径
|
|
73
77
|
npm run check # 语法检查
|
|
74
78
|
node scripts/verify-manifest.mjs # 清单/打包自检
|
|
75
79
|
npm run verify # 以上全部
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
测试用一个假的 `window.__ModuleLoader__` 与 `document` 载入 `client.js
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
82
|
+
测试用一个假的 `window.__ModuleLoader__` 与 `document` 载入 `client.js`,因此断言的正是宿主实际调用的那条路径。这个假 `document` 会记录"谁在什么时机被插进哪个根",所以下面这些都能测出来:
|
|
83
|
+
|
|
84
|
+
- 样式表没有按裸标签生效(`span` 不被当成文字),
|
|
85
|
+
- 没有第三方插件命名(曾经把 `dshwv-*` 的图标清空),
|
|
86
|
+
- 类名锚定必须用下划线夹住,且每个 local 的两套命名都在(只写一套是曾经踩过的坑:一半界面没有浮雕),
|
|
87
|
+
- 每个锚定都带 BEM 护栏;排除项不算,
|
|
88
|
+
- 焦点环存在、画在框内、不吞掉内凹阴影、不碰 checkbox / radio,且全文没有一处 `outline: none`,
|
|
89
|
+
- 样式表在 `ctx.effect` 内插入:宿主拒绝注册 effect 时一张纸都不留,
|
|
90
|
+
- 连续 apply 两次只留下一张样式表,
|
|
91
|
+
- 浮雕落在卡片、输入区大板、菜单/对话框与会话行上,且各用各的阴影级;按钮、输入框、悬停态、发送胶囊各自保着圆角与阴影(只断言"字符串出现过"就够,因为这里保护的是"规则还在"),
|
|
92
|
+
- 没有任何按钮规则会强制底色(附件删除键上的叉号就是这样变成隐形的),且对比度圆盘还留着自己的填充,
|
|
93
|
+
- 信号类 token 不会被置成 `transparent`(引用竖线、幽灵按钮选中态、行内代码),
|
|
94
|
+
- 宿主拒绝插入样式表时,那张表会被撤回、异常照旧抛出。
|
|
83
95
|
|
|
84
96
|
## 许可
|
|
85
97
|
|
package/client.js
CHANGED
|
@@ -71,19 +71,19 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
'--dsw-alias-button-elevated-fill': FLAT,
|
|
72
72
|
'--dsw-alias-button-floating-fill': FLAT,
|
|
73
73
|
'--dsw-alias-button-floating-hover': FLAT,
|
|
74
|
-
'--dsw-alias-button-ghost-active-fill':
|
|
74
|
+
'--dsw-alias-button-ghost-active-fill': HOVER,
|
|
75
75
|
'--dsw-alias-button-ghost-active-hover': FLAT,
|
|
76
|
-
'--dsw-alias-
|
|
76
|
+
'--dsw-alias-fill-tsp-secondary': SURFACE,
|
|
77
77
|
'--dsw-alias-button-contrast-fill': { light: '#3a4450', dark: '#c9d3de' },
|
|
78
78
|
'--dsw-alias-interactive-bg-hover-solid': FLAT,
|
|
79
79
|
|
|
80
80
|
// ---- code and inline panels: recessed, not tinted --------------------
|
|
81
81
|
'--dsw-alias-markdown-code-block': FLAT,
|
|
82
82
|
'--dsw-alias-markdown-code-block-banner': FLAT,
|
|
83
|
-
'--dsw-alias-markdown-inline-code':
|
|
83
|
+
'--dsw-alias-markdown-inline-code': HOVER,
|
|
84
84
|
'--dsw-alias-markdown-code-segment-selected': FLAT,
|
|
85
85
|
'--dsw-alias-markdown-code-segment-unselected': FLAT,
|
|
86
|
-
'--dsw-alias-markdown-citation':
|
|
86
|
+
'--dsw-alias-markdown-citation': SCROLL_HOVER,
|
|
87
87
|
'--dsw-alias-markdown-tag': FLAT,
|
|
88
88
|
'--dsw-alias-markdown-placeholder': FLAT,
|
|
89
89
|
|
|
@@ -137,6 +137,35 @@ window.__ModuleLoader__.load({
|
|
|
137
137
|
'--dsw-radius-panel': { light: '26px', dark: '26px' },
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The host ships two CSS-module naming conventions at once:
|
|
142
|
+
*
|
|
143
|
+
* - the pre-built app stylesheet names its classes `_<local>_<hash>_<line>`
|
|
144
|
+
* (`_card_7gxqk_107`);
|
|
145
|
+
* - the component styles injected from JS name theirs `<hash>_<local>`
|
|
146
|
+
* (`wSkVaW_card`).
|
|
147
|
+
*
|
|
148
|
+
* So an anchor for a local name has to cover both, and it must not straddle
|
|
149
|
+
* a longer local: `_card_` never matches `_cardContent_1a2b_9`, while a bare
|
|
150
|
+
* `[class*="_card"]` would. Every anchor also refuses a double underscore,
|
|
151
|
+
* because that is how a plugin names its own elements (`panel__card`) and no
|
|
152
|
+
* host class contains one.
|
|
153
|
+
*/
|
|
154
|
+
const guard = (anchor) => `${anchor}:not([class*="__"])`;
|
|
155
|
+
|
|
156
|
+
/** Anchors on a complete local name, in both host conventions. */
|
|
157
|
+
const host = (...locals) =>
|
|
158
|
+
locals
|
|
159
|
+
.flatMap((local) => [`[class$="_${local}"]`, `[class*="_${local}_"]`])
|
|
160
|
+
.map(guard)
|
|
161
|
+
.join(',\n ');
|
|
162
|
+
|
|
163
|
+
/** A host button: anything the host painted with a class, minus the cases
|
|
164
|
+
* below. Kept as one constant so the three states cannot drift apart. */
|
|
165
|
+
const BUTTON_SELECTOR = `body :where(button, [role="button"])[class*="_"]:not([class*="__"]):not([aria-checked]):not([role="checkbox"]):not([role="radio"]):not(:is(${host(
|
|
166
|
+
'primary',
|
|
167
|
+
)})):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"]) *)`;
|
|
168
|
+
|
|
140
169
|
const CSS = `
|
|
141
170
|
body {
|
|
142
171
|
--qn-hi: #ffffff;
|
|
@@ -163,51 +192,78 @@ body[data-ds-dark-theme] {
|
|
|
163
192
|
}
|
|
164
193
|
|
|
165
194
|
/* ---- text is flat ------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
elements
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
body :is(
|
|
195
|
+
Text never carries a plate of its own: a label sits on the surface, it does
|
|
196
|
+
not sit in a slightly different rectangle. Only host markup is touched, and
|
|
197
|
+
only by a complete local name (see the host helper above), so neither a
|
|
198
|
+
plugin's own elements nor a longer host part ('_labelChip') is repainted.
|
|
199
|
+
Form controls are excluded even when their local reads like text, because a
|
|
200
|
+
rename field named '_name' is still a field with a plate of its own. Nothing
|
|
201
|
+
here targets a bare tag: a span is not text until the host says so. */
|
|
202
|
+
body :is(${host(
|
|
203
|
+
'label',
|
|
204
|
+
'title',
|
|
205
|
+
'value',
|
|
206
|
+
'text',
|
|
207
|
+
'name',
|
|
208
|
+
'desc',
|
|
209
|
+
'caption',
|
|
210
|
+
'hint',
|
|
211
|
+
'meta',
|
|
212
|
+
'timestamp',
|
|
213
|
+
'itemLabel',
|
|
214
|
+
'runStateLabel',
|
|
215
|
+
'dockHintLabel',
|
|
216
|
+
'clickableLabel',
|
|
217
|
+
'labelGroup',
|
|
218
|
+
'tabTitle',
|
|
219
|
+
'floatTitle',
|
|
220
|
+
'failedTitle',
|
|
221
|
+
'stringValue',
|
|
222
|
+
'numberValue',
|
|
223
|
+
'otherValue',
|
|
224
|
+
'keywordValue',
|
|
225
|
+
'popoverText',
|
|
226
|
+
)}):not(input):not(textarea):not(select):not([contenteditable="true"]) {
|
|
174
227
|
background-color: transparent !important;
|
|
175
228
|
box-shadow: none !important;
|
|
176
229
|
}
|
|
177
230
|
|
|
178
231
|
/* ---- controls: soft, raised --------------------------------------------
|
|
179
|
-
Buttons are objects again: a
|
|
180
|
-
|
|
232
|
+
Buttons are objects again: a shape with the same paired light and shade as
|
|
233
|
+
the surface around it. The fill is deliberately not forced: every surface in
|
|
234
|
+
this theme is one colour, so a transparent button already sits flush on it,
|
|
235
|
+
while a host control that paints a functional disc (the attachment remove
|
|
236
|
+
button uses '--dsw-alias-button-contrast-fill') must keep it or its glyph
|
|
237
|
+
becomes invisible. Full-width rows and every control that
|
|
181
238
|
lives inside a menu, a tab strip or a picker are excluded, because a
|
|
182
239
|
shadow under a row that spans the panel reads as a stray line, and a
|
|
183
240
|
stack of them reads as alternating high and low. */
|
|
184
|
-
|
|
241
|
+
${BUTTON_SELECTOR} {
|
|
185
242
|
border-radius: 12px !important;
|
|
186
243
|
border-color: transparent !important;
|
|
187
|
-
background-color: var(--dsw-alias-bg-base) !important;
|
|
188
244
|
box-shadow: var(--qn-out-sm) !important;
|
|
189
245
|
}
|
|
190
246
|
|
|
191
|
-
|
|
247
|
+
${BUTTON_SELECTOR}:hover:not(:disabled) {
|
|
192
248
|
box-shadow: var(--qn-out) !important;
|
|
193
249
|
}
|
|
194
250
|
|
|
195
|
-
|
|
251
|
+
${BUTTON_SELECTOR}:active:not(:disabled) {
|
|
196
252
|
box-shadow: var(--qn-in-sm) !important;
|
|
197
253
|
}
|
|
198
254
|
|
|
199
255
|
/* The send button is a filled pill and keeps one soft shadow of its own. */
|
|
200
|
-
body :where(button, [role="button"])
|
|
256
|
+
body :where(button, [role="button"]):is(${host('primary')}) {
|
|
201
257
|
border-radius: 999px !important;
|
|
202
258
|
border-color: transparent !important;
|
|
203
259
|
box-shadow: 0 3px 8px rgba(74, 107, 138, 0.28) !important;
|
|
204
260
|
}
|
|
205
261
|
|
|
206
|
-
body :where(button, [role="button"])
|
|
262
|
+
body :where(button, [role="button"]):is(${host('primary')}):hover:not(:disabled) {
|
|
207
263
|
box-shadow: 0 4px 10px rgba(74, 107, 138, 0.34) !important;
|
|
208
264
|
}
|
|
209
265
|
|
|
210
|
-
body :where(button, [role="button"])
|
|
266
|
+
body :where(button, [role="button"]):is(${host('primary')}):active:not(:disabled) {
|
|
211
267
|
box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.26) !important;
|
|
212
268
|
}
|
|
213
269
|
|
|
@@ -226,17 +282,14 @@ body :where(
|
|
|
226
282
|
Deliberately narrow. Rows, tabs, chips, list items and every other small
|
|
227
283
|
block stay flat: a shadow on each of them turns the page into a field of
|
|
228
284
|
pillows, which is not what neumorphism is for. */
|
|
229
|
-
body
|
|
230
|
-
body [class*="_card "]:not([class*="__"]) {
|
|
285
|
+
body :is(${host('card')}) {
|
|
231
286
|
box-shadow: var(--qn-drop) !important;
|
|
232
287
|
border-color: transparent !important;
|
|
233
288
|
}
|
|
234
289
|
|
|
235
290
|
/* ---- composer: the one big soft slab ------------------------------------ */
|
|
236
|
-
body
|
|
237
|
-
body
|
|
238
|
-
body [class*="_composerHero"]:not([class*="__"]) [class$="_card"],
|
|
239
|
-
body [class*="_composerHero"]:not([class*="__"]) [class*="_card "]:not([class*="__"]) {
|
|
291
|
+
body :is(${host('composerSeat')}) :is(${host('card')}),
|
|
292
|
+
body :is(${host('composerHero')}) :is(${host('card')}) {
|
|
240
293
|
box-shadow: var(--qn-out) !important;
|
|
241
294
|
border-radius: 22px !important;
|
|
242
295
|
border-color: transparent !important;
|
|
@@ -245,7 +298,7 @@ body [class*="_composerHero"]:not([class*="__"]) [class*="_card "]:not([class*="
|
|
|
245
298
|
/* ---- floating surfaces: menus, dialogs, tooltips ------------------------ */
|
|
246
299
|
body :where([role="menu"], [role="dialog"], [role="listbox"], [role="tooltip"]),
|
|
247
300
|
body [data-menu-material],
|
|
248
|
-
body :is(
|
|
301
|
+
body :is(${host('menu', 'dialog')}) {
|
|
249
302
|
border-radius: 20px !important;
|
|
250
303
|
box-shadow: var(--qn-out-lg) !important;
|
|
251
304
|
}
|
|
@@ -257,14 +310,12 @@ body [data-menu-material] > [aria-hidden="true"] {
|
|
|
257
310
|
}
|
|
258
311
|
|
|
259
312
|
/* ---- session tree: one soft row per session ----------------------------- */
|
|
260
|
-
body
|
|
261
|
-
body [class*="_projectRow"]:not([class*="__"]) {
|
|
313
|
+
body :is(${host('sessionRow', 'projectRow')}) {
|
|
262
314
|
border-radius: 12px !important;
|
|
263
315
|
box-shadow: var(--qn-drop) !important;
|
|
264
316
|
}
|
|
265
317
|
|
|
266
|
-
body
|
|
267
|
-
body [class*="_projectRow"]:not([class*="__"]):hover {
|
|
318
|
+
body :is(${host('sessionRow', 'projectRow')}):hover {
|
|
268
319
|
box-shadow: var(--qn-drop-lift) !important;
|
|
269
320
|
}
|
|
270
321
|
|
|
@@ -276,31 +327,38 @@ body :where(pre) {
|
|
|
276
327
|
}
|
|
277
328
|
|
|
278
329
|
/* ---- sidebar: a faint groove instead of a divider line ------------------ */
|
|
279
|
-
body
|
|
330
|
+
body :is(${host('sidebarCol')}) {
|
|
280
331
|
box-shadow: inset -8px 0 14px -14px var(--qn-lo) !important;
|
|
281
332
|
}
|
|
282
333
|
|
|
283
|
-
/* ---- focus: a
|
|
284
|
-
The host ring
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
334
|
+
/* ---- focus: a ring, drawn inside the field -----------------------------
|
|
335
|
+
The host draws its ring outside the box, which on a soft surface reads as a
|
|
336
|
+
stray edge floating beside the field. So the ring is moved inward instead of
|
|
337
|
+
removed: an outline with a negative offset follows the border radius, composes
|
|
338
|
+
with the recessed shadow instead of replacing it, and still exists for every
|
|
339
|
+
control type - including the checkable ones this sheet never restyles, which
|
|
340
|
+
keep the host ring untouched. A theme that sets outline: none on a focus
|
|
341
|
+
state leaves a keyboard user with no idea where they are; this one does not. */
|
|
342
|
+
body :where(
|
|
343
|
+
textarea,
|
|
344
|
+
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
|
|
345
|
+
select,
|
|
346
|
+
[contenteditable="true"]
|
|
347
|
+
):focus-visible {
|
|
348
|
+
outline: 1.5px solid var(--dsw-alias-brand-primary) !important;
|
|
349
|
+
outline-offset: -2px !important;
|
|
297
350
|
}
|
|
298
351
|
|
|
352
|
+
/* Buttons and links keep a ring outside the shape, where it cannot be confused
|
|
353
|
+
with the relief, but it is declared important so that every control in the
|
|
354
|
+
product shows the same theme accent: the host draws its own ring inside
|
|
355
|
+
component-scoped rules (a business-blue ring with a positive offset), and
|
|
356
|
+
without the flag half the interface would stay blue while the fields are
|
|
357
|
+
steel. */
|
|
299
358
|
body :where(button, [role="button"], a):focus-visible {
|
|
300
|
-
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
301
|
-
outline-offset: 2px;
|
|
359
|
+
outline: 2px solid var(--dsw-alias-brand-primary) !important;
|
|
360
|
+
outline-offset: 2px !important;
|
|
302
361
|
}
|
|
303
|
-
|
|
304
362
|
`;
|
|
305
363
|
|
|
306
364
|
return {
|
|
@@ -323,29 +381,43 @@ body :where(button, [role="button"], a):focus-visible {
|
|
|
323
381
|
|
|
324
382
|
// Everything the plugin adds is created and released inside one effect:
|
|
325
383
|
// a stylesheet inserted before the effect registered would outlive a
|
|
326
|
-
// throw
|
|
384
|
+
// throw, and a second apply would stack a second copy of it. So the old
|
|
385
|
+
// sheet of this source is dropped first, and a throw after insertion
|
|
386
|
+
// takes the sheet back out again.
|
|
327
387
|
ctx.effect(() => {
|
|
388
|
+
const root = document.head ?? document.documentElement;
|
|
389
|
+
for (const stale of document.querySelectorAll?.(`style[data-dsh-theme="${SOURCE}"]`) ?? []) {
|
|
390
|
+
stale.remove();
|
|
391
|
+
}
|
|
392
|
+
|
|
328
393
|
// Shadow layer: neumorphism needs paired soft shadows, and a theme
|
|
329
394
|
// token can only carry a colour, so the shadows are a stylesheet of
|
|
330
395
|
// their own.
|
|
331
396
|
const style = document.createElement('style');
|
|
332
397
|
style.dataset.dshTheme = SOURCE;
|
|
333
398
|
style.textContent = CSS;
|
|
334
|
-
const root = document.head ?? document.documentElement;
|
|
335
|
-
root.appendChild(style);
|
|
336
399
|
|
|
337
400
|
let disposeTokens = null;
|
|
338
401
|
try {
|
|
402
|
+
root.appendChild(style);
|
|
339
403
|
disposeTokens = overrideTokens();
|
|
340
404
|
} catch (error) {
|
|
341
|
-
//
|
|
342
|
-
//
|
|
405
|
+
// Whatever happened, this effect owns what it inserted: give it back
|
|
406
|
+
// before getting out of the way. The one exception is the token
|
|
407
|
+
// table, which has a colour-only retry of its own; if even that is
|
|
408
|
+
// rejected the shadows are still worth keeping, so report and carry on.
|
|
409
|
+
if (style.parentNode === null && disposeTokens === null) {
|
|
410
|
+
style.remove();
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
343
413
|
console.warn('[dsh-theme-neumorph] token layer rejected, shadows only', error);
|
|
344
414
|
}
|
|
345
415
|
|
|
346
416
|
return () => {
|
|
347
|
-
|
|
417
|
+
// Sheet first: one frame of host colours is better than one frame of
|
|
418
|
+
// plugin shadows over the host's own palette.
|
|
348
419
|
style.remove();
|
|
420
|
+
disposeTokens?.();
|
|
349
421
|
};
|
|
350
422
|
});
|
|
351
423
|
},
|