dsh-theme-gallery 0.1.4 → 0.1.5

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 +26 -0
  2. package/lib/client.js +182 -31
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -114,6 +114,32 @@ dsh plugin --profile web add dsh-theme-gallery
114
114
  只在你想跟源码时用它。本包**没有构建步骤**,所以不会出现"缺 `lib/` 目录"那类失败;
115
115
  它的代价是 pnpm 可能要求你为构建脚本授权(`allowBuilds`)。
116
116
 
117
+ ### 更新到新版本
118
+
119
+ **先说清楚:DSH 桌面版本身不会提示第三方插件有更新**,所以「发现新版」和「执行更新」这两件事得靠下面的渠道。
120
+
121
+ **怎么知道有新版本?**
122
+
123
+ | 渠道 | 说明 |
124
+ |---|---|
125
+ | **插件市场** | 装 [DSH-Plugins-Marketplace](https://github.com/bradeGithub/DSH-Plugins-Marketplace) 后,卡片会显示 **「已装 v0.1.4 → v0.1.6」** 并给**更新**按钮 —— 目前最省事的路径 |
126
+ | **GitHub Releases** | 在本仓库点 **Watch → Custom → Releases only**,有新版本会收到邮件 |
127
+ | **对照版本号** | 插件面板右上角显示**你装的版本**(如 `v0.1.4`),与 [npm 页面](https://www.npmjs.com/package/dsh-theme-gallery) 上的版本号直接对照 |
128
+
129
+ **怎么更新?**
130
+
131
+ 1. **设置 → 插件 → 添加插件** → 再填一次 `dsh-theme-gallery` → **重启**(装上 `^x.y.z` 范围内的最新版)
132
+ 2. 想要**强制拿最新**(含 0.1.x → 0.2.0 这类次版本号变更):**先移除、再添加**
133
+ 3. 装了市场的:点卡片上的**更新**按钮
134
+ 4. Web / CLI profile:`dsh plugin --profile web add dsh-theme-gallery`
135
+
136
+ **版本范围语义**:安装时会记录成 **`^0.1.4`**,即 `>=0.1.4 <0.2.0` ——
137
+ `0.1.5` / `0.1.9` 这类补丁会在重装时自动带上;**`0.2.0` 不会**(npm 对 `0.x` 的惯例是
138
+ 把次版本号变更视为可能破坏性),那种情况按第 2 条显式重装即可。
139
+
140
+ > ⚠️ 从 **Release 的 `.tgz`** 安装的用户**没有更新机制**(tgz 是一次性快照,只能重新下载)——
141
+ > 想长期跟更新,建议用**包名安装**。
142
+
117
143
  ### 兼容性与权限
118
144
 
119
145
  | 项 | 说明 |
package/lib/client.js CHANGED
@@ -105,6 +105,21 @@ window.__ModuleLoader__.load({
105
105
  */
106
106
  const READING = { bg: '#FFFFFF', alpha: 0.62, blur: 3, maxWidth: 640 }
107
107
 
108
+ /**
109
+ * The package version this bundle was built from.
110
+ *
111
+ * Written by `scripts/embed-themes.mjs` from package.json, because the browser
112
+ * half cannot read its own manifest: the client module system resolves `require`
113
+ * against the platform seed table and other plugins' boot-graph rows, not
114
+ * against this package's files. The panel shows it so a reader can compare what
115
+ * they have with what npm publishes.
116
+ *
117
+ * Kept honest by two checks: `scripts/publish-check.mjs` compares it with
118
+ * package.json, and the release workflow fails when re-running the embed step
119
+ * changes a tracked file.
120
+ */
121
+ const BUNDLED_VERSION = '0.1.5'
122
+
108
123
  /**
109
124
  * Themes this package contributes, inlined from lib/themes/*.json.
110
125
  *
@@ -2767,6 +2782,28 @@ window.__ModuleLoader__.load({
2767
2782
  return BUNDLED_THEMES.find((theme) => theme.id === id)
2768
2783
  }
2769
2784
 
2785
+ /**
2786
+ * Read one token out of a bundled skin, resolving the pair form to the skin's own scheme.
2787
+ *
2788
+ * The bundled skins store most tokens as `{ light, dark }` pairs even though a skin itself is
2789
+ * single-scheme. `flatten` collapses them for `register`; this is the same read, exposed
2790
+ * separately because the accent diagnostic needs ONE token's declared value without building
2791
+ * the whole registrable definition — and because that diagnostic lives at factory scope, where
2792
+ * `flatten` (declared inside `mountGallery`) is not reachable. Reaching for it from here is the
2793
+ * exact scope error `tests/check-scope-reach.mjs` exists to catch.
2794
+ * @param definition - a bundled skin.
2795
+ * @param name - the token name.
2796
+ * @returns the declared value, or undefined when the skin has no such token.
2797
+ */
2798
+ function declaredToken(definition, name) {
2799
+ const raw = definition?.tokens?.[name]
2800
+ if (raw === undefined) return undefined
2801
+ // Deliberately not `raw?.[…]`: a skin whose token is neither a string nor a pair is a data
2802
+ // error, and `flatten` (the other caller) must keep failing loudly on it rather than
2803
+ // registering an unset variable that paints nothing.
2804
+ return typeof raw === 'string' ? raw : raw[definition.colorScheme]
2805
+ }
2806
+
2770
2807
  /**
2771
2808
  * Disposer of the active accent layer, when one is stacked.
2772
2809
  *
@@ -2779,6 +2816,20 @@ window.__ModuleLoader__.load({
2779
2816
  */
2780
2817
  let accentLayerDispose
2781
2818
 
2819
+ /**
2820
+ * The accent colour {@link accentLayerDispose} was built from.
2821
+ *
2822
+ * Declared here for the same reason as the disposer above: `syncAccent` both reads and writes
2823
+ * it, and it is called during mount, so a `let` initialised later in the body would be in its
2824
+ * temporal dead zone at the first call.
2825
+ *
2826
+ * It exists so that re-stacking can be skipped when the colour has not moved. Without it every
2827
+ * `theme/change` — including ones this plugin caused and ones caused by something else —
2828
+ * disposed and re-created the layer, which emits again.
2829
+ * @type {string|undefined}
2830
+ */
2831
+ let stackedAccent
2832
+
2782
2833
  /**
2783
2834
  * The plugin context, published for the factory-level helpers.
2784
2835
  *
@@ -2827,15 +2878,46 @@ window.__ModuleLoader__.load({
2827
2878
  // `try` swallowed it, and the accent marker silently never appeared. The module-level `ctx`
2828
2879
  // below is assigned at mount and read here.
2829
2880
  if (ctx === undefined) return
2830
- if (accentLayerDispose !== undefined) {
2831
- accentLayerDispose()
2832
- accentLayerDispose = undefined
2833
- }
2834
- if (typeof accent !== 'string' || accent === '') return
2835
- accentLayerDispose = ctx.theme.overrideTokens('theme-gallery: accent', {
2836
- '--dsw-alias-button-ghost-active-fill': { light: `${accent}29`, dark: `${accent}29` },
2837
- '--dsw-alias-button-ghost-active-border': { light: accent, dark: accent },
2838
- '--dsw-alias-button-ghost-active-hover': { light: `${accent}47`, dark: `${accent}47` },
2881
+ const wanted = typeof accent === 'string' && accent !== '' ? accent : undefined
2882
+ // ── WHY BOTH A SKIP AND THE SELF-EMIT GUARD ARE NEEDED ───────────────────────────────────
2883
+ //
2884
+ // `overrideTokens` and the disposer it returns BOTH emit `theme/change`, and this function
2885
+ // is reached from `publish()`, which is itself driven by that event:
2886
+ //
2887
+ // publish → syncSkin → syncAccent → overrideTokens → theme/change → publish → …
2888
+ //
2889
+ // Every level nests inside the previous CALL, so nothing ever yields to the event loop. The
2890
+ // stack therefore grows until the engine refuses to grow it further and throws
2891
+ // `RangeError: Maximum call stack size exceeded` — and because every level has its own
2892
+ // `try/catch` (see `syncSkin`), each one logs the SAME message on the way out. That is why
2893
+ // the console showed a flood of "could not stack the accent layer" lines rather than one
2894
+ // error: one flood, hundreds of identical lines, each from a different depth.
2895
+ //
2896
+ // Fixing it takes both halves, and they are not interchangeable:
2897
+ //
2898
+ // • the SKIP stops the churn — an accent that has not moved does not need a new layer, so
2899
+ // an unrelated `theme/change` (the shell's `adopt()`, a streamed render, another plugin)
2900
+ // no longer replaces the layer and causes a repaint;
2901
+ // • the GUARD stops the RE-ENTRY — it is what makes the `theme/change` emitted by our own
2902
+ // write invisible to the subscription, so the cycle cannot close even when the colour
2903
+ // genuinely did change.
2904
+ //
2905
+ // Only the guard is a correctness requirement; the skip is what keeps the layer stable. The
2906
+ // same shape is used by `stackSkinTokens` for the palette and by `syncReadingLayer`.
2907
+ if (wanted === stackedAccent) return
2908
+ emitting(() => {
2909
+ if (accentLayerDispose !== undefined) {
2910
+ accentLayerDispose()
2911
+ accentLayerDispose = undefined
2912
+ }
2913
+ stackedAccent = undefined
2914
+ if (wanted === undefined) return
2915
+ accentLayerDispose = ctx.theme.overrideTokens('theme-gallery: accent', {
2916
+ '--dsw-alias-button-ghost-active-fill': { light: `${wanted}29`, dark: `${wanted}29` },
2917
+ '--dsw-alias-button-ghost-active-border': { light: wanted, dark: wanted },
2918
+ '--dsw-alias-button-ghost-active-hover': { light: `${wanted}47`, dark: `${wanted}47` },
2919
+ })
2920
+ stackedAccent = wanted
2839
2921
  })
2840
2922
  }
2841
2923
 
@@ -2852,8 +2934,27 @@ window.__ModuleLoader__.load({
2852
2934
  *
2853
2935
  * The service makes this checkable without any guesswork: `composeActive` folds every
2854
2936
  * override layer into `snapshot.active.tokens` before publishing, so the composed value is
2855
- * readable straight off `getTheme()`. If the layer is registered, the token IS there; if it
2856
- * is not, the token is absent or still the built-in value.
2937
+ * readable straight off `getTheme()`.
2938
+ *
2939
+ * ── WHAT CAN AND CANNOT PROVE IT ─────────────────────────────────────────
2940
+ *
2941
+ * The composed VALUE is the only channel that carries information, and only when it differs
2942
+ * from what the skin declares for that same token:
2943
+ *
2944
+ * • both bundled skins already ship `button-ghost-active-*` in their own palettes, AND the
2945
+ * palette layer supplies them too — so the token COUNT is 67 either way. An earlier
2946
+ * comment here claimed a count that "does not move" disproves the layer; for these skins
2947
+ * it cannot move, so the claim was wrong and the count is reported for context only;
2948
+ * • 梦海游鱼 declares `#2B74B5` for that token but its accent is `#FFD166`, so reading back
2949
+ * `#FFD166` proves the accent layer composed OVER the palette — the palette alone cannot
2950
+ * produce it;
2951
+ * • 山青婷彩's accent is `#E88BB0` and it declares `#E88BB0` for the same token, so its
2952
+ * reading is identical whether or not the layer is there. That is a property of the SKIN,
2953
+ * not of the layer, and the verdict below says so instead of implying the reading proved
2954
+ * something.
2955
+ *
2956
+ * The verdict is therefore computed, never asserted: it compares the composed value with the
2957
+ * skin's declared value and with the accent the layer was built from.
2857
2958
  * @returns the reading, as one segment of the panel line.
2858
2959
  */
2859
2960
  function describeAccentLayer() {
@@ -2864,16 +2965,31 @@ window.__ModuleLoader__.load({
2864
2965
  const active = snapshot?.active
2865
2966
  const tokens = active?.tokens ?? {}
2866
2967
  const TOKEN = '--dsw-alias-button-ghost-active-border'
2867
- const value = tokens[TOKEN]
2968
+ const composed = tokens[TOKEN]
2969
+ const definition = typeof active?.id === 'string' ? bundledTheme(active.id) : undefined
2970
+ const declared = declaredToken(definition, TOKEN)
2971
+ const accent = definition?.accent
2868
2972
  const layers = snapshot?.overrides === undefined
2869
2973
  ? '(服务未暴露)'
2870
2974
  : String(snapshot.overrides.size ?? snapshot.overrides.length ?? '?')
2871
- // Its OWN key count too: layers folding into `active.tokens` is the mechanism, so a
2872
- // token count that does not move when the layer is stacked also disproves it.
2975
+ // Computed, not assumed — see the note above. `不可分辨` is a real answer: it says this
2976
+ // skin's own value coincides with its accent, so this reading proves nothing either way.
2977
+ const verdict = accent === undefined
2978
+ ? '非本包皮肤(无法判断)'
2979
+ : accent === declared
2980
+ ? '不可分辨(强调色=皮肤自备值)'
2981
+ : composed === accent
2982
+ ? '生效(强调色压过自备值)'
2983
+ : composed === declared
2984
+ ? '未生效(仍是自备值)'
2985
+ : `未知(既非强调色也非自备值)`
2873
2986
  return `激活层[id=${active?.id ?? '?'}`
2874
2987
  + ` 层数=${layers}`
2875
- + ` token数=${Object.keys(tokens).length}`
2876
- + ` ${TOKEN.replace('--dsw-alias-', '')}=${value === undefined ? '(缺失)' : String(value)}]`
2988
+ + ` token数=${Object.keys(tokens).length}(自备同名令牌,不构成判据)`
2989
+ + ` ${TOKEN.replace('--dsw-alias-', '')}=${composed === undefined ? '(缺失)' : String(composed)}`
2990
+ + ` 自备=${declared === undefined ? '(无)' : String(declared)}`
2991
+ + ` 强调色=${accent ?? '(无)'}`
2992
+ + ` 判据=${verdict}]`
2877
2993
  } catch (error) {
2878
2994
  return `激活层[读取失败: ${String(error && error.message ? error.message : error)}]`
2879
2995
  }
@@ -2922,6 +3038,9 @@ window.__ModuleLoader__.load({
2922
3038
  jsx('span', { className: 'tg-title', children: t('title') }),
2923
3039
  jsx('span', { className: 'tg-hint', children: t('hint') }),
2924
3040
  jsx('span', { className: 'tg-hint', children: t('count', { count: ids.length }) }),
3041
+ // What this copy is, so a reader can compare it with the version npm
3042
+ // publishes without digging through the profile.
3043
+ jsx('span', { className: 'tg-hint', children: `v${BUNDLED_VERSION}` }),
2925
3044
  ],
2926
3045
  }),
2927
3046
  debugEnabled() ? jsx('div', { className: 'tg-debug', children: themeDiagnostics(selected) }) : null,
@@ -3725,8 +3844,10 @@ window.__ModuleLoader__.load({
3725
3844
  */
3726
3845
  function flatten(definition) {
3727
3846
  const tokens = {}
3728
- for (const [name, value] of Object.entries(definition.tokens ?? {})) {
3729
- tokens[name] = typeof value === 'string' ? value : value[definition.colorScheme]
3847
+ for (const name of Object.keys(definition.tokens ?? {})) {
3848
+ // Delegated so the pair rule has exactly ONE implementation — the accent diagnostic reads
3849
+ // a single token through the same helper, and two copies of this rule would drift.
3850
+ tokens[name] = declaredToken(definition, name)
3730
3851
  }
3731
3852
  return { ...definition, tokens }
3732
3853
  }
@@ -4101,6 +4222,17 @@ window.__ModuleLoader__.load({
4101
4222
  let readingTag
4102
4223
  /** Disposer of the active reading token layer, when one is stacked. */
4103
4224
  let readingLayerDispose
4225
+ /**
4226
+ * The lightened ground {@link readingLayerDispose} was built from.
4227
+ *
4228
+ * Same role as `stackedAccent`: `syncReadingLayer` writes to the theme service, and it is
4229
+ * scheduled from a `MutationObserver` on `document.body` — so without this, every batch of
4230
+ * DOM mutations would replace the layer, which emits `theme/change`, which repaints the
4231
+ * shell, which mutates the DOM again. That is a loop through the microtask queue rather than
4232
+ * through the stack, so it would burn CPU steadily instead of throwing.
4233
+ * @type {string|undefined}
4234
+ */
4235
+ let stackedReading
4104
4236
 
4105
4237
  /**
4106
4238
  * Install the reading rule, once per plugin lifetime.
@@ -4205,19 +4337,31 @@ window.__ModuleLoader__.load({
4205
4337
  function syncReadingLayer() {
4206
4338
  if (typeof document === 'undefined') return
4207
4339
  const reading = document.body !== null && document.body.hasAttribute(READING_ATTRIBUTE)
4208
- if (!reading) {
4340
+ // Computed before the skip test, because the skip compares VALUES. `lighten` is pure, so
4341
+ // the same document state always yields the same pair and the comparison is exact.
4342
+ const wanted = reading ? lighten(READING.bg, READING.alpha) : undefined
4343
+ const wantedDark = reading
4344
+ // Lightened ground for both palettes: a light theme wants a softer wash, a dark one a
4345
+ // slightly raised surface. Both keep the theme's hue.
4346
+ ? lighten(READING.bg, Math.max(0, READING.alpha - 0.2))
4347
+ : undefined
4348
+
4349
+ // Both writes below emit `theme/change`, and this function is reached from a
4350
+ // `MutationObserver` on `document.body` — the same tree the shell repaints when the theme
4351
+ // changes. See the comment in `syncAccent` for the full shape; the short version is that
4352
+ // an unguarded write here lets the layer drive its own trigger.
4353
+ if (wanted === stackedReading) return
4354
+ emitting(() => {
4209
4355
  if (readingLayerDispose !== undefined) {
4210
4356
  readingLayerDispose()
4211
4357
  readingLayerDispose = undefined
4212
4358
  }
4213
- return
4214
- }
4215
- // Lightened ground for both palettes: a light theme wants a softer wash,
4216
- // a dark one a slightly raised surface. Both keep the theme's hue.
4217
- const lightened = lighten(READING.bg, READING.alpha)
4218
- const darkLightened = lighten(READING.bg, Math.max(0, READING.alpha - 0.2))
4219
- readingLayerDispose = ctx.theme.overrideTokens('theme-gallery: reading', {
4220
- '--dsw-alias-bg-base': { light: lightened, dark: darkLightened },
4359
+ stackedReading = undefined
4360
+ if (wanted === undefined) return
4361
+ readingLayerDispose = ctx.theme.overrideTokens('theme-gallery: reading', {
4362
+ '--dsw-alias-bg-base': { light: wanted, dark: wantedDark },
4363
+ })
4364
+ stackedReading = wanted
4221
4365
  })
4222
4366
  }
4223
4367
 
@@ -4246,10 +4390,17 @@ window.__ModuleLoader__.load({
4246
4390
  observer.disconnect()
4247
4391
  if (readingTag !== undefined) readingTag.remove()
4248
4392
  readingTag = undefined
4249
- if (readingLayerDispose !== undefined) {
4250
- readingLayerDispose()
4251
- readingLayerDispose = undefined
4252
- }
4393
+ // Guarded for the same reason as the writes in `syncReadingLayer`, and the remembered
4394
+ // value is cleared with the layer: leaving it set would make that function's skip test
4395
+ // believe a layer is still stacked after this one removed it, and it would then refuse
4396
+ // to re-stack.
4397
+ emitting(() => {
4398
+ if (readingLayerDispose !== undefined) {
4399
+ readingLayerDispose()
4400
+ readingLayerDispose = undefined
4401
+ }
4402
+ stackedReading = undefined
4403
+ })
4253
4404
  document.body.removeAttribute(READING_ATTRIBUTE)
4254
4405
  const centre = centreColumn()
4255
4406
  if (centre !== null) centre.style.removeProperty('--dsh-reading-width')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theme-gallery",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A community theme gallery for DeepSeek Harness (DSH): JSON-defined skins with full-screen coverage and ported sidebar scenery, switched from the plugin's own sidebar panel — adding skins needs no code change",
5
5
  "author": "renjie2026 (https://github.com/renjie2026)",
6
6
  "license": "MIT",
@@ -61,7 +61,8 @@
61
61
  "test:scopelogic": "node tests/check-scope-reach-logic.mjs",
62
62
  "test:preview": "node scripts/build-ambient-preview.mjs",
63
63
  "test:calls": "node tests/check-undefined-calls.mjs",
64
- "test": "npm run test:schema && npm run test:host && npm run test:store && npm run test:order && npm run test:tdz && npm run test:theme && npm run test:ambient && npm run test:boot && npm run test:safety && npm run test:module && npm run test:bounded && npm run test:bootpath && npm run test:scope && npm run test:scopelogic && npm run test:calls",
64
+ "test:emitguard": "node tests/check-self-emit-guard.mjs && node tests/check-self-emit-guard-logic.mjs",
65
+ "test": "npm run test:schema && npm run test:host && npm run test:store && npm run test:order && npm run test:tdz && npm run test:theme && npm run test:ambient && npm run test:boot && npm run test:safety && npm run test:module && npm run test:bounded && npm run test:bootpath && npm run test:scope && npm run test:scopelogic && npm run test:calls && npm run test:emitguard",
65
66
  "prepack": "node scripts/embed-themes.mjs",
66
67
  "publish:check": "node scripts/publish-check.mjs",
67
68
  "embed-themes": "node scripts/embed-themes.mjs",