@nonamelego/dsh-catppuccin 0.6.4 → 0.6.5-beta.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.en.md +22 -8
- package/README.md +15 -7
- package/lib/client.js +246 -21
- package/lib/client.js.map +1 -1
- package/lib/index.js +938 -2
- package/lib/index.js.map +1 -1
- package/package.json +9 -2
package/README.en.md
CHANGED
|
@@ -56,7 +56,7 @@ glass skin on Web / Desktop, and the four official theme palettes auto-synced to
|
|
|
56
56
|
|
|
57
57
|
It ships all four Catppuccin flavours — **Latte**, **Frappé**, **Macchiato**, **Mocha** —
|
|
58
58
|
remapping the whole interface to the matching palette, with a **settings section of its own**
|
|
59
|
-
(**Settings → Catppuccin**: flavour, code highlighting, token overrides, glass, update check).
|
|
59
|
+
(**Settings → Catppuccin**: flavour, code highlighting, token overrides, glass, update check, feedback).
|
|
60
60
|
Your choice is persisted and restored after restarts.
|
|
61
61
|
|
|
62
62
|
It also includes an optional **glassmorphism** skin: the top bar, sidebar, composer,
|
|
@@ -69,7 +69,7 @@ Catppuccin theme automatically.
|
|
|
69
69
|
- 🎨 Four themes: Latte (light), Frappé / Macchiato / Mocha (dark)
|
|
70
70
|
- 🧩 Registered into the official theme system, on a par with the built-in light / dark / system themes
|
|
71
71
|
- 🎯 Full-UI colour coverage — not just one or two accent colours
|
|
72
|
-
- ⚙️ One-click switch
|
|
72
|
+
- ⚙️ One-click switch on its own Settings section, persisted and restored across restarts
|
|
73
73
|
- 🔧 **Custom token overrides**: override individual colour tokens with `--dsw-* var: value` pairs (e.g. turn comments blue); persisted alongside the selected flavour
|
|
74
74
|
- 🖍️ **Code-block highlight style**: default / italic-comments shiki themes
|
|
75
75
|
- 🌐 Seven UI languages (Chinese / English / Japanese / Korean / Spanish / French / German, follows the system language)
|
|
@@ -80,7 +80,7 @@ Catppuccin theme automatically.
|
|
|
80
80
|
(interaction reference: [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin))
|
|
81
81
|
- 🌫️ **Glass details**: gradient blur bands at the top/bottom page edges, a floating glass
|
|
82
82
|
rail when the sidebar is collapsed, an ambient theme-following ground (a same-family
|
|
83
|
-
wash
|
|
83
|
+
wash; the two soft glows are an **opt-in switch, off by default**) — content softens as it scrolls under the panes
|
|
84
84
|
- 🎨 Glass colours follow the current Catppuccin theme
|
|
85
85
|
- 🔄 **Update check**: one-click "Check for updates" in Settings compares the latest npm
|
|
86
86
|
version and gives you a copyable upgrade command; **auto-check is on by default** (once at
|
|
@@ -260,6 +260,14 @@ The **Glass** section lives on the **Settings → Catppuccin** page:
|
|
|
260
260
|
- **Backdrop brightness**: dark mode darkens 0–50, light mode brightens 50–100 (50 = as-is),
|
|
261
261
|
mixed straight into the ground; at the extremes the ambience fades out and the ground
|
|
262
262
|
collapses back to pure white / pure black.
|
|
263
|
+
- **Ground glows**: adds two screen-pinned soft glows on top of the ground (the `0.6.3` look),
|
|
264
|
+
**off by default** — they do not follow the panes, so each pane transmits a different slice
|
|
265
|
+
of them and the top-left corner brightens visibly when on.
|
|
266
|
+
- **In-place previews**: a **ground sample strip** in the glass section (drag brightness or
|
|
267
|
+
toggle the glows and it changes on the spot — it is painted by the page ground's own rule);
|
|
268
|
+
a **code sample** under the highlight picker (reads `--shiki-token-*`, so switching to
|
|
269
|
+
italic-comments recolours it immediately); and **Show the UI** next to the mode picker,
|
|
270
|
+
which closes Settings so you can look at the real layout.
|
|
263
271
|
|
|
264
272
|
Glass colours follow the active theme live; all settings persist across restarts.
|
|
265
273
|
|
|
@@ -296,8 +304,9 @@ What this plugin does:
|
|
|
296
304
|
bubbles and the new-session button; in Mica mode they become rounded floating cards and
|
|
297
305
|
chat content scrolls *under* the glass, blurred; the collapsed sidebar becomes a
|
|
298
306
|
floating rail at the edge of the chat area (the "performance & usage" stats line is
|
|
299
|
-
*not*
|
|
300
|
-
|
|
307
|
+
*not* a glass card and stays outside it, but since `0.6.4` it sits on a **frosted
|
|
308
|
+
ground-coloured strip** so the scrolling transcript cannot read through the readouts —
|
|
309
|
+
see the `0.6.2` "fused band removed" entry and issue #29 in `0.6.4`);
|
|
301
310
|
- **Page-edge gradient blur**: a blur band at the top and bottom of the viewport so
|
|
302
311
|
messages are softened as they melt past the edges (borrowed from
|
|
303
312
|
[DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin)'s
|
|
@@ -371,10 +380,15 @@ theme/glass actually reached the sidebar in that run), so no conclusion is drawn
|
|
|
371
380
|
| DSH | `>=0.1.7-rc.1` (the settings page consumes the official `dsh-client-ui-primitives` `SegmentedControl`, which only exists from `0.1.7-alpha.1`; the legacy `settingsScope` code path is kept, but 0.1.5 / 0.1.6 hosts are no longer declared supported) |
|
|
372
381
|
| Node.js | `>=20` |
|
|
373
382
|
| Profile | `web` (the Web GUI and both desktop shells run the web UI and share this plugin); desktop profiles are named `desktop` |
|
|
374
|
-
| Verified exact version | `0.1.7-rc.1`: installed, started, persisted a setting to disk and restored it across a restart in a real profile ([evidence](docs/issue-15-settings-seam-0.1.7.md)); `0.1.7-rc.2`: boot-level e2e and live-page sampling of the glass layer (issues #16 / #17); `0.1.5-rc.
|
|
383
|
+
| Verified exact version | `0.2.1-rc.2`: real-device acceptance on the official desktop shell — plugin boots, tokens apply, glass recipe intact, **settings writes really reach `cordis.patch.yml`** (they did not before the fix) and are adopted back after a restart ([audit](docs/desktop-0.2.1-adaptation-audit.md)); `0.2.0-rc.2`: boot-level e2e on the official shell's bundled runtime (19/19), real desktop-window pixels and the title-bar colour chain, `configForms` persistence ([audit](docs/desktop-0.2.0-adaptation-audit.md)); `0.1.7-rc.1`: installed, started, persisted a setting to disk and restored it across a restart in a real profile ([evidence](docs/issue-15-settings-seam-0.1.7.md)); `0.1.7-rc.2`: boot-level e2e and live-page sampling of the glass layer (issues #16 / #17); `0.1.5-rc.1`: boot-level e2e rerun under the CI recipe (**historical**, below the current floor) |
|
|
375
384
|
|
|
376
|
-
|
|
377
|
-
`
|
|
385
|
+
`dsh.compatibility` (`dsh` / `dshReleases` / `dshOperations`) in `package.json` **declares only the
|
|
386
|
+
three currently supported lines**: `0.1.7-rc.1` / `0.2.0-rc.2` / `0.2.1-rc.2` (floor `>=0.1.7-rc.1`).
|
|
387
|
+
Earlier `0.1.5-rc.*` / `0.1.7-alpha.*` runs are records of what was verified **at the time** and are no
|
|
388
|
+
longer declared.
|
|
389
|
+
|
|
390
|
+
Colour coverage is based on `dsh-v0.2.0-rc.2`'s `design-platform.css`: all 190 `--dsw-*` tokens per
|
|
391
|
+
scheme (static 77 / alias 101 / specific 11 / non-family 1), including the 17 aliases added in 0.2.0.
|
|
378
392
|
|
|
379
393
|
### Permissions and external access
|
|
380
394
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Web / 桌面端做全界面换色与玻璃质感,TUI 端自动同步四套官
|
|
|
53
53
|
|
|
54
54
|
它内置 Catppuccin 的四个主题——**Latte**、**Frappé**、**Macchiato**、**Mocha**——
|
|
55
55
|
把整个界面的配色都换成对应的 Catppuccin 色板;并在设置里拥有**自己的独立分区**
|
|
56
|
-
(**设置 → Catppuccin
|
|
56
|
+
(**设置 → Catppuccin**:风味、代码高亮、自定义覆盖、玻璃质感、更新检查、反馈与支持都在这一页),
|
|
57
57
|
选择会自动保存、重启自动恢复。
|
|
58
58
|
|
|
59
59
|
同时内置一套可开关的**玻璃质感**(Glassmorphism)皮肤:顶栏、侧边栏、
|
|
@@ -66,7 +66,7 @@ Catppuccin 主题。
|
|
|
66
66
|
- 🎨 四个主题:Latte(浅色)、Frappé / Macchiato / Mocha(深色)
|
|
67
67
|
- 🧩 接入官方主题系统,与内置浅色 / 深色 / 跟随系统主题平级
|
|
68
68
|
- 🎯 全界面配色覆盖,不只是一两个强调色
|
|
69
|
-
- ⚙️
|
|
69
|
+
- ⚙️ 设置页独立分区一键切换,选择自动保存、重启自动恢复
|
|
70
70
|
- 🔧 **自定义 token 覆盖**:按「`--dsw-* 变量: 值`」逐条覆盖单个配色 token(例如把注释色换成蓝色),与所选风味一起持久保存
|
|
71
71
|
- 🖍️ **代码块高亮风格**:默认 / 注释斜体(italic-comments)两套 shiki 风格可选
|
|
72
72
|
- 🌐 中 / 英 / 日 / 韩 / 西 / 法 / 德七语文案(跟随系统语言)
|
|
@@ -75,7 +75,7 @@ Catppuccin 主题。
|
|
|
75
75
|
只给输入框卡片与浮层上玻璃),模糊度、磨砂度、
|
|
76
76
|
背景亮度自由调节(交互参考 [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin))
|
|
77
77
|
- 🌫️ **玻璃拟态细节**:页面上下边缘渐变模糊、折叠侧边栏悬浮玻璃、
|
|
78
|
-
|
|
78
|
+
随主题的氛围地面(同色系渐变;两枚柔光斑是**可选开关、默认关**)——内容滚入视口边缘时柔化穿过,
|
|
79
79
|
玻璃透着地面的明暗,层次更立体
|
|
80
80
|
- 🎨 玻璃配色自动跟随当前 Catppuccin 主题
|
|
81
81
|
- 🔄 **检查 Catppuccin 插件更新**:设置页一键检测本插件(dsh-catppuccin)在 npm 上的最新版本,发现新版直接给出可复制的升级命令;**默认开启自动检查**(启动后一次 + 每 6 小时),更新渠道可选稳定版 / Beta
|
|
@@ -229,6 +229,11 @@ dsh plugin --profile dsh-tui add https://github.com/NoNameLeGo/dsh-catppuccin-th
|
|
|
229
229
|
不透明度。
|
|
230
230
|
- **背景亮度**:深色模式 0–50 压暗、浅色模式 50–100 提亮(50 为原样),
|
|
231
231
|
直接调和进地面底色;拉到两端时氛围层淡出,回到纯白 / 纯黑。
|
|
232
|
+
- **地面光斑**:在地面渐变上叠两枚**固定于屏幕**的柔光斑(`0.6.3` 的观感),**默认关**——
|
|
233
|
+
光斑不跟面板走,每块玻璃透出的是它的不同部位,打开后左上角会明显偏亮。
|
|
234
|
+
- **就地预览**:玻璃区里有一条**地面样本条**(拖亮度或切光斑,它当场变化——取的就是页面地面那套色);
|
|
235
|
+
代码高亮下方有**代码样本**(直接读 `--shiki-token-*`,切「柔和注释」即刻变色);
|
|
236
|
+
模式选择旁的 **看一眼主界面** 会关掉设置页,让你直接看真实布局。
|
|
232
237
|
|
|
233
238
|
玻璃配色自动跟随当前主题,切换 Latte / Frappé / Macchiato / Mocha 时即时
|
|
234
239
|
变色;所有设置跨重启自动恢复。
|
|
@@ -263,8 +268,8 @@ dsh plugin --profile dsh-tui add https://github.com/NoNameLeGo/dsh-catppuccin-th
|
|
|
263
268
|
- **六个区域玻璃化**:顶栏、侧边栏、输入框、轨迹视图、聊天气泡
|
|
264
269
|
和新会话按钮;云母模式下成为带圆角的悬浮卡片,聊天内容滚动时从玻璃
|
|
265
270
|
下方穿过、被模糊;折叠侧边栏时导航条同样悬浮在聊天区边缘
|
|
266
|
-
|
|
267
|
-
CHANGELOG 的 `0.6.2
|
|
271
|
+
(「性能与用量」统计行不套玻璃卡、留在卡外,但自 `0.6.4` 起它下面有一条**同色磨砂背板**,
|
|
272
|
+
防止滚动正文从状态栏读数底下透出——见 CHANGELOG 的 `0.6.2`「拆除融合带」与 `0.6.4` 的 issue #29);
|
|
268
273
|
- **页面边缘渐变模糊**:视口上下各有一条渐变模糊带,消息滚到边缘时被
|
|
269
274
|
柔化穿过——内容在边界「融化」(借鉴
|
|
270
275
|
[DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin)
|
|
@@ -328,9 +333,12 @@ console.table({ backdropFilter: cs.backdropFilter, background: cs.backgroundColo
|
|
|
328
333
|
| DSH | `>=0.1.7-rc.1`(设置页消费官方 `dsh-client-ui-primitives` 的 `SegmentedControl`,该导出自 `0.1.7-alpha.1` 起才有;旧的 `settingsScope` 通道代码保留,但 0.1.5 / 0.1.6 宿主不再声明支持) |
|
|
329
334
|
| Node.js | `>=20` |
|
|
330
335
|
| Profile | `web`(Web GUI 与两个桌面壳都启动 web 界面,共用本插件);桌面端默认 profile 名为 `desktop`(已声明) |
|
|
331
|
-
| 已验证的具体版本 | `0.2.1-rc.2`:官方桌面壳真机验收——插件正常启动、token 逐项生效、玻璃配方完整,**改设置真写回 `cordis.patch.yml`(修复前不写)**、重启后从文档采纳回来([审计](docs/desktop-0.2.1-adaptation-audit.md));`0.2.0-rc.2`:官方桌面壳自带运行时的启动级 e2e(19/19)、真机桌面窗口像素与标题栏取色链路、`configForms` 落盘([审计](docs/desktop-0.2.0-adaptation-audit.md));`0.1.7-rc.1`:真实 profile 上完成安装、启动、改设置落盘与重启恢复([证据](docs/issue-15-settings-seam-0.1.7.md));`0.1.7-rc.2`:启动级 e2e 与玻璃层的真页采样(issue #16 / #17);`0.1.5-rc.1`:按 CI 口径复跑的启动级 e2e
|
|
336
|
+
| 已验证的具体版本 | `0.2.1-rc.2`:官方桌面壳真机验收——插件正常启动、token 逐项生效、玻璃配方完整,**改设置真写回 `cordis.patch.yml`(修复前不写)**、重启后从文档采纳回来([审计](docs/desktop-0.2.1-adaptation-audit.md));`0.2.0-rc.2`:官方桌面壳自带运行时的启动级 e2e(19/19)、真机桌面窗口像素与标题栏取色链路、`configForms` 落盘([审计](docs/desktop-0.2.0-adaptation-audit.md));`0.1.7-rc.1`:真实 profile 上完成安装、启动、改设置落盘与重启恢复([证据](docs/issue-15-settings-seam-0.1.7.md));`0.1.7-rc.2`:启动级 e2e 与玻璃层的真页采样(issue #16 / #17);`0.1.5-rc.1`:按 CI 口径复跑的启动级 e2e(**历史记录**,已低于当前兼容下限) |
|
|
337
|
+
|
|
338
|
+
`package.json` 的 `dsh.compatibility`(`dsh` / `dshReleases` / `dshOperations`)**只声明当前支持的三条**:
|
|
339
|
+
`0.1.7-rc.1` / `0.2.0-rc.2` / `0.2.1-rc.2`(兼容下限 `>=0.1.7-rc.1`);更早的 `0.1.5-rc.*`
|
|
340
|
+
与 `0.1.7-alpha.*` 是**当时**的验证记录,不在声明内。
|
|
332
341
|
|
|
333
|
-
以上也是 `package.json` 里 `dsh.compatibility`(`dsh` / `dshReleases` / `dshOperations`)的机器可读版本。
|
|
334
342
|
色彩覆盖以 `dsh-v0.2.0-rc.2` 的 `design-platform.css` 为基线:每方案 190 个 `--dsw-*` token(static 77 /
|
|
335
343
|
alias 101 / specific 11 / 非三族 1)全覆盖,含 0.2.0 新增的 17 个 alias。
|
|
336
344
|
|
package/lib/client.js
CHANGED
|
@@ -683,10 +683,88 @@ window.__ModuleLoader__.load({
|
|
|
683
683
|
function stateFromSettingsSection(section) {
|
|
684
684
|
return sanitizeState(section);
|
|
685
685
|
}
|
|
686
|
+
/** Field-for-field equality of two override maps — the same rule
|
|
687
|
+
* {@link settingsSectionsEqual} applies to them (both directions, so a key
|
|
688
|
+
* that only one side holds makes them differ). */
|
|
689
|
+
function sameOverrides(a, b) {
|
|
690
|
+
const keys = Object.keys(a);
|
|
691
|
+
return keys.length === Object.keys(b).length && keys.every((key) => b[key] === a[key]);
|
|
692
|
+
}
|
|
686
693
|
/** Whether two settings sections are field-for-field equal (used to skip
|
|
687
694
|
* redundant mirror writes and to recognize the Host echoing our own write). */
|
|
688
695
|
function settingsSectionsEqual(a, b) {
|
|
689
|
-
return a.flavor === b.flavor && a.glass.enabled === b.glass.enabled && a.glass.mode === b.glass.mode && a.glass.blur === b.glass.blur && a.glass.frost === b.glass.frost && a.glass.brightness === b.glass.brightness && a.glass.glow === b.glass.glow && a.autoCheck === b.autoCheck && a.updateChannel === b.updateChannel && a.shikiStyle === b.shikiStyle &&
|
|
696
|
+
return a.flavor === b.flavor && a.glass.enabled === b.glass.enabled && a.glass.mode === b.glass.mode && a.glass.blur === b.glass.blur && a.glass.frost === b.glass.frost && a.glass.brightness === b.glass.brightness && a.glass.glow === b.glass.glow && a.autoCheck === b.autoCheck && a.updateChannel === b.updateChannel && a.shikiStyle === b.shikiStyle && sameOverrides(a.overrides, b.overrides);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Replay the local changes a window made BEFORE the durable channel became
|
|
700
|
+
* usable onto the document state — the issue #30 fix.
|
|
701
|
+
*
|
|
702
|
+
* The R2 mechanism (`pendingLocalPush` in `src/client/index.ts`) exists so a
|
|
703
|
+
* change made during the boot window is not rolled back by the first snapshot
|
|
704
|
+
* the settings scope resolves. It used to replay the WHOLE local state, and on
|
|
705
|
+
* a fresh origin (empty localStorage: a new port, a cleared site, a webview
|
|
706
|
+
* partition) "the whole local state" is nothing but the shipped defaults — so
|
|
707
|
+
* the first snapshot rewrote the document to `flavor: off`, `glass.enabled:
|
|
708
|
+
* false`, `mode: mica`. The trigger is not even a user action: the glass layer
|
|
709
|
+
* republishes on every `theme/change`, and `ui-theme` adopts its preference on
|
|
710
|
+
* every boot.
|
|
711
|
+
*
|
|
712
|
+
* This replays only the fields whose value moved away from `baseline` — the
|
|
713
|
+
* local section as of the last moment the window knew it was in sync with the
|
|
714
|
+
* document (boot, or the last adoption / successful write). Everything else
|
|
715
|
+
* keeps the document's value, and `changed === false` tells the caller to take
|
|
716
|
+
* the ordinary "document wins" adoption path instead of writing anything.
|
|
717
|
+
*
|
|
718
|
+
* ⚠️ Every field of {@link CatppuccinSettingsSection} must be carried here.
|
|
719
|
+
* A missing one is SILENT in both directions — a boot-window change to it is
|
|
720
|
+
* dropped, and (because the caller's whole-state fallback is gone) nothing
|
|
721
|
+
* else would notice. Same rule as `stateToMutateOps` (issue #26), and
|
|
722
|
+
* `tests/state.spec.ts` holds a drift guard that fails when a field is added
|
|
723
|
+
* without a mutant here.
|
|
724
|
+
*
|
|
725
|
+
* Known gap (accepted): a delta cannot tell "untouched" from "explicitly set
|
|
726
|
+
* to the current default", so setting a field BACK to its default value inside
|
|
727
|
+
* the boot window yields to the document. The window is sub-second and the
|
|
728
|
+
* direction is "document wins", i.e. no durable data is lost.
|
|
729
|
+
*
|
|
730
|
+
* @param document - the section the settings document resolved (wins).
|
|
731
|
+
* @param baseline - the local section at the last sync point.
|
|
732
|
+
* @param local - the local section right now.
|
|
733
|
+
* @returns the merged state plus whether anything actually differed.
|
|
734
|
+
*/
|
|
735
|
+
function replayLocalChanges(document, baseline, local) {
|
|
736
|
+
const merged = settingsSectionFromState(document);
|
|
737
|
+
let changed = false;
|
|
738
|
+
const carry = (key) => {
|
|
739
|
+
if (local[key] !== baseline[key]) {
|
|
740
|
+
merged[key] = local[key];
|
|
741
|
+
changed = true;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const carryGlass = (key) => {
|
|
745
|
+
if (local.glass[key] !== baseline.glass[key]) {
|
|
746
|
+
merged.glass[key] = local.glass[key];
|
|
747
|
+
changed = true;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
carry("flavor");
|
|
751
|
+
carryGlass("enabled");
|
|
752
|
+
carryGlass("mode");
|
|
753
|
+
carryGlass("blur");
|
|
754
|
+
carryGlass("frost");
|
|
755
|
+
carryGlass("brightness");
|
|
756
|
+
carryGlass("glow");
|
|
757
|
+
carry("autoCheck");
|
|
758
|
+
carry("updateChannel");
|
|
759
|
+
carry("shikiStyle");
|
|
760
|
+
if (!sameOverrides(local.overrides, baseline.overrides)) {
|
|
761
|
+
merged.overrides = { ...local.overrides };
|
|
762
|
+
changed = true;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
state: sanitizeState(merged),
|
|
766
|
+
changed
|
|
767
|
+
};
|
|
690
768
|
}
|
|
691
769
|
//#endregion
|
|
692
770
|
//#region src/client/glass/glass-layer.ts
|
|
@@ -3371,6 +3449,46 @@ window.__ModuleLoader__.load({
|
|
|
3371
3449
|
}
|
|
3372
3450
|
];
|
|
3373
3451
|
//#endregion
|
|
3452
|
+
//#region src/boot-palette.ts
|
|
3453
|
+
/** Attribute marking the injected `<style>` so the Client can find it. */
|
|
3454
|
+
const BOOT_PALETTE_ATTR = "data-dsh-catppuccin-boot";
|
|
3455
|
+
//#endregion
|
|
3456
|
+
//#region src/client/boot-palette.ts
|
|
3457
|
+
/**
|
|
3458
|
+
* Client half of the pre-paint palette (issue #30 补充, 2026-10-27).
|
|
3459
|
+
*
|
|
3460
|
+
* The Host injects a `<style data-dsh-catppuccin-boot>` into the index HTML so
|
|
3461
|
+
* the first frame is already the persisted flavour (see `src/boot-palette.ts`).
|
|
3462
|
+
* That sheet is only a stand-in: its selector is `html body` (0,0,2), which
|
|
3463
|
+
* outranks the official `body` token block, so leaving it in place would pin
|
|
3464
|
+
* every token and defeat later theme switches (the user picks Light or turns the
|
|
3465
|
+
* flavour off and nothing changes).
|
|
3466
|
+
*
|
|
3467
|
+
* So it is removed the moment the flavour theme actually takes over — which is
|
|
3468
|
+
* NOT the same instant as "setTheme was called":
|
|
3469
|
+
* - a reload where the runtime preference already equals the flavour id never
|
|
3470
|
+
* calls setTheme (see `applyDesired`'s early return), and
|
|
3471
|
+
* - a built-in pick (Light / Dark / System) must remove it too, because the
|
|
3472
|
+
* sheet belongs to a flavour that is no longer wanted.
|
|
3473
|
+
* The restore effect therefore clears it from every one of those points plus a
|
|
3474
|
+
* fallback timer and the effect teardown.
|
|
3475
|
+
*/
|
|
3476
|
+
/**
|
|
3477
|
+
* Remove every injected pre-paint sheet from this document.
|
|
3478
|
+
*
|
|
3479
|
+
* Idempotent and safe when nothing was injected (hosts without the index
|
|
3480
|
+
* injection seam, `flavor: off`, a document that already cleared it).
|
|
3481
|
+
* @returns whether an element was actually removed (diagnostics and tests).
|
|
3482
|
+
*/
|
|
3483
|
+
function clearBootPalette() {
|
|
3484
|
+
let removed = false;
|
|
3485
|
+
for (const element of document.querySelectorAll(`[${BOOT_PALETTE_ATTR}]`)) {
|
|
3486
|
+
element.remove();
|
|
3487
|
+
removed = true;
|
|
3488
|
+
}
|
|
3489
|
+
return removed;
|
|
3490
|
+
}
|
|
3491
|
+
//#endregion
|
|
3374
3492
|
//#region src/client/shiki-tokens.ts
|
|
3375
3493
|
/**
|
|
3376
3494
|
* Canonical Catppuccin syntax colours per flavour.
|
|
@@ -4160,29 +4278,57 @@ window.__ModuleLoader__.load({
|
|
|
4160
4278
|
const reapplyThemePrefs = () => {
|
|
4161
4279
|
const active = theme.getTheme().preference;
|
|
4162
4280
|
const flavor = flavorInfo(active);
|
|
4163
|
-
if (flavor === void 0
|
|
4164
|
-
|
|
4281
|
+
if (flavor === void 0) return;
|
|
4282
|
+
const dispose = themeDisposers.get(flavor.themeId);
|
|
4283
|
+
if (dispose === void 0) return;
|
|
4284
|
+
themeDisposers.delete(flavor.themeId);
|
|
4285
|
+
dispose();
|
|
4165
4286
|
registerThemeFor(flavor);
|
|
4287
|
+
try {
|
|
4288
|
+
theme.setTheme(flavor.themeId);
|
|
4289
|
+
} catch {}
|
|
4166
4290
|
};
|
|
4167
4291
|
let lastWrittenSection;
|
|
4168
4292
|
let handleStaleConflict;
|
|
4293
|
+
let applyRestoredPreference;
|
|
4169
4294
|
let pendingLocalPush = false;
|
|
4295
|
+
let pendingLocalSection = null;
|
|
4296
|
+
let maxSeenRevision;
|
|
4297
|
+
let revisionKind;
|
|
4298
|
+
let lastDocumentSection = null;
|
|
4299
|
+
let syncedSection = settingsSectionFromState(buildLocalState());
|
|
4170
4300
|
const baseRevisionTracker = createBaseRevisionTracker();
|
|
4171
4301
|
const queuePersist = () => {
|
|
4172
|
-
|
|
4302
|
+
const localSection = settingsSectionFromState(buildLocalState());
|
|
4303
|
+
if ((lastDocumentSection === null || !settingsSectionsEqual(localSection, lastDocumentSection)) && !settingsSectionsEqual(localSection, syncedSection)) {
|
|
4304
|
+
pendingLocalSection = localSection;
|
|
4305
|
+
if (!isScopeUsable(scope.getSnapshot())) pendingLocalPush = true;
|
|
4306
|
+
}
|
|
4173
4307
|
baseRevisionTracker.capture(() => scope.getSnapshot().revision);
|
|
4174
4308
|
scheduleDurablePersist(persistLocal);
|
|
4175
4309
|
};
|
|
4176
4310
|
const persistLocal = () => {
|
|
4177
4311
|
const baseRevision = baseRevisionTracker.take();
|
|
4178
|
-
if (!isScopeUsable(scope.getSnapshot()))
|
|
4312
|
+
if (!isScopeUsable(scope.getSnapshot())) {
|
|
4313
|
+
pendingLocalSection = null;
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4179
4316
|
const state = buildLocalState();
|
|
4317
|
+
const flushed = settingsSectionFromState(state);
|
|
4180
4318
|
persistStateToScope(scope, state, {
|
|
4181
4319
|
baseRevision,
|
|
4182
4320
|
lastWrittenSection
|
|
4183
4321
|
}).then((outcome) => {
|
|
4184
|
-
|
|
4185
|
-
|
|
4322
|
+
const settled = pendingLocalSection !== null && settingsSectionsEqual(pendingLocalSection, flushed);
|
|
4323
|
+
if (outcome === "written") {
|
|
4324
|
+
lastWrittenSection = flushed;
|
|
4325
|
+
syncedSection = flushed;
|
|
4326
|
+
if (settled) pendingLocalSection = null;
|
|
4327
|
+
} else if (outcome === "noop") {
|
|
4328
|
+
syncedSection = flushed;
|
|
4329
|
+
if (settled) pendingLocalSection = null;
|
|
4330
|
+
} else if (outcome === "stale") {
|
|
4331
|
+
if (settled) pendingLocalSection = null;
|
|
4186
4332
|
emitConflict();
|
|
4187
4333
|
handleStaleConflict?.();
|
|
4188
4334
|
}
|
|
@@ -4190,28 +4336,49 @@ window.__ModuleLoader__.load({
|
|
|
4190
4336
|
};
|
|
4191
4337
|
const AUTO_CHECK_BOOT_DELAY_MS = 3e3;
|
|
4192
4338
|
const AUTO_CHECK_INTERVAL_MS = 216e5;
|
|
4339
|
+
/** How long the Host's pre-paint palette may survive before it is removed
|
|
4340
|
+
* unconditionally (issue #30 补充). Generous on purpose: removing it EARLY
|
|
4341
|
+
* re-exposes the official palette for one frame, which is the bug. */
|
|
4342
|
+
const BOOT_PALETTE_FALLBACK_MS = 1e4;
|
|
4193
4343
|
let lastAutoResult = null;
|
|
4194
4344
|
let armAutoCheck;
|
|
4195
4345
|
ctx.effect(() => {
|
|
4196
4346
|
let liveBuiltinPick = null;
|
|
4347
|
+
const bootPaletteFallback = window.setTimeout(() => {
|
|
4348
|
+
clearBootPalette();
|
|
4349
|
+
}, BOOT_PALETTE_FALLBACK_MS);
|
|
4197
4350
|
const originalSetTheme = theme.setTheme;
|
|
4198
|
-
const
|
|
4199
|
-
liveBuiltinPick = isBuiltinPreference(id) ? id : null;
|
|
4351
|
+
const commitTheme = (id, record) => {
|
|
4352
|
+
if (record) liveBuiltinPick = isBuiltinPreference(id) ? id : null;
|
|
4200
4353
|
originalSetTheme.call(theme, id);
|
|
4354
|
+
clearBootPalette();
|
|
4355
|
+
};
|
|
4356
|
+
const restoreGuardSetTheme = (id) => {
|
|
4357
|
+
commitTheme(id, true);
|
|
4201
4358
|
};
|
|
4202
4359
|
theme.setTheme = restoreGuardSetTheme;
|
|
4360
|
+
applyRestoredPreference = (id) => {
|
|
4361
|
+
commitTheme(id, false);
|
|
4362
|
+
};
|
|
4203
4363
|
let restorePending = false;
|
|
4364
|
+
let pendingDesired;
|
|
4204
4365
|
const scheduleRestore = (desired) => {
|
|
4366
|
+
pendingDesired = desired;
|
|
4205
4367
|
if (restorePending) return;
|
|
4206
4368
|
restorePending = true;
|
|
4207
4369
|
queueMicrotask(() => {
|
|
4208
4370
|
restorePending = false;
|
|
4371
|
+
const wanted = pendingDesired;
|
|
4372
|
+
pendingDesired = void 0;
|
|
4209
4373
|
const preference = theme.getTheme().preference;
|
|
4210
|
-
if (preference ===
|
|
4374
|
+
if (preference === wanted) {
|
|
4375
|
+
clearBootPalette();
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4211
4378
|
if (builtinPickWins(preference, liveBuiltinPick)) return;
|
|
4212
|
-
ensureThemeRegistered(
|
|
4379
|
+
ensureThemeRegistered(wanted);
|
|
4213
4380
|
try {
|
|
4214
|
-
|
|
4381
|
+
commitTheme(wanted, false);
|
|
4215
4382
|
} catch {}
|
|
4216
4383
|
});
|
|
4217
4384
|
};
|
|
@@ -4220,11 +4387,17 @@ window.__ModuleLoader__.load({
|
|
|
4220
4387
|
const desired = readFlavor();
|
|
4221
4388
|
if (desired === "off") {
|
|
4222
4389
|
const preference = theme.getTheme().preference;
|
|
4223
|
-
if (readExplicitFlavorOff()
|
|
4390
|
+
if (readExplicitFlavorOff()) {
|
|
4391
|
+
clearBootPalette();
|
|
4392
|
+
if (flavorFromThemeId(preference) !== "off") scheduleRestore(readRestoredPreference());
|
|
4393
|
+
}
|
|
4224
4394
|
return;
|
|
4225
4395
|
}
|
|
4226
4396
|
const preference = theme.getTheme().preference;
|
|
4227
|
-
if (preference === desired)
|
|
4397
|
+
if (preference === desired) {
|
|
4398
|
+
clearBootPalette();
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4228
4401
|
if (builtinPickWins(preference, liveBuiltinPick)) return;
|
|
4229
4402
|
scheduleRestore(desired);
|
|
4230
4403
|
};
|
|
@@ -4242,7 +4415,7 @@ window.__ModuleLoader__.load({
|
|
|
4242
4415
|
}
|
|
4243
4416
|
if (flavorFromThemeId(theme.getTheme().preference) === "off") return;
|
|
4244
4417
|
try {
|
|
4245
|
-
|
|
4418
|
+
commitTheme(readRestoredPreference(), false);
|
|
4246
4419
|
} catch {}
|
|
4247
4420
|
};
|
|
4248
4421
|
window.addEventListener("storage", onStorage);
|
|
@@ -4253,18 +4426,52 @@ window.__ModuleLoader__.load({
|
|
|
4253
4426
|
const snapshot = scope.getSnapshot();
|
|
4254
4427
|
const state = durableStateFromSnapshot(snapshot);
|
|
4255
4428
|
if (state === null) return;
|
|
4429
|
+
const revision = snapshot.revision;
|
|
4430
|
+
if (revision !== void 0) {
|
|
4431
|
+
if (revisionKind !== scope.kind) {
|
|
4432
|
+
revisionKind = scope.kind;
|
|
4433
|
+
maxSeenRevision = void 0;
|
|
4434
|
+
}
|
|
4435
|
+
if (maxSeenRevision !== void 0 && revision < maxSeenRevision) return;
|
|
4436
|
+
maxSeenRevision = Math.max(maxSeenRevision ?? revision, revision);
|
|
4437
|
+
}
|
|
4256
4438
|
const local = buildLocalState();
|
|
4257
4439
|
if (pendingLocalPush) {
|
|
4258
4440
|
pendingLocalPush = false;
|
|
4259
|
-
|
|
4260
|
-
|
|
4441
|
+
const replayed = replayLocalChanges(state, syncedSection, settingsSectionFromState(local));
|
|
4442
|
+
if (replayed.changed) {
|
|
4443
|
+
writeFlavor(replayed.state.flavor);
|
|
4444
|
+
glass.applyRemote(replayed.state.glass);
|
|
4445
|
+
writeAutoCheck(replayed.state.autoCheck);
|
|
4446
|
+
writeUpdateChannel(replayed.state.updateChannel);
|
|
4447
|
+
writeOverrides(replayed.state.overrides);
|
|
4448
|
+
writeShikiStyle(replayed.state.shikiStyle);
|
|
4449
|
+
emitPrefs();
|
|
4450
|
+
reapplyThemePrefs();
|
|
4451
|
+
applyDesired();
|
|
4452
|
+
syncedSection = settingsSectionFromState(replayed.state);
|
|
4453
|
+
lastDocumentSection = settingsSectionFromState(state);
|
|
4454
|
+
queuePersist();
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4261
4457
|
}
|
|
4262
4458
|
if (!scope.hasUserLayer(snapshot)) {
|
|
4263
4459
|
if (!isDefaultState(local)) queuePersist();
|
|
4264
4460
|
return;
|
|
4265
4461
|
}
|
|
4266
|
-
|
|
4267
|
-
|
|
4462
|
+
const pushCleaned = hasUnpersistableOverrides(snapshot.value?.overrides);
|
|
4463
|
+
const localSection = settingsSectionFromState(local);
|
|
4464
|
+
const documentSection = settingsSectionFromState(state);
|
|
4465
|
+
if (settingsSectionsEqual(localSection, documentSection)) {
|
|
4466
|
+
lastDocumentSection = documentSection;
|
|
4467
|
+
return;
|
|
4468
|
+
}
|
|
4469
|
+
if (pendingLocalSection !== null && !settingsSectionsEqual(pendingLocalSection, documentSection)) return;
|
|
4470
|
+
cancelDurablePersist();
|
|
4471
|
+
baseRevisionTracker.take();
|
|
4472
|
+
pendingLocalSection = null;
|
|
4473
|
+
lastDocumentSection = documentSection;
|
|
4474
|
+
syncedSection = documentSection;
|
|
4268
4475
|
writeFlavor(state.flavor);
|
|
4269
4476
|
glass.applyRemote(state.glass);
|
|
4270
4477
|
writeAutoCheck(state.autoCheck);
|
|
@@ -4274,6 +4481,7 @@ window.__ModuleLoader__.load({
|
|
|
4274
4481
|
emitPrefs();
|
|
4275
4482
|
reapplyThemePrefs();
|
|
4276
4483
|
applyDesired();
|
|
4484
|
+
if (pushCleaned) queuePersist();
|
|
4277
4485
|
};
|
|
4278
4486
|
applyScopeSnapshot();
|
|
4279
4487
|
const offScope = scope.subscribe(applyScopeSnapshot);
|
|
@@ -4307,10 +4515,13 @@ window.__ModuleLoader__.load({
|
|
|
4307
4515
|
offGlass();
|
|
4308
4516
|
disposer();
|
|
4309
4517
|
window.removeEventListener("storage", onStorage);
|
|
4518
|
+
window.clearTimeout(bootPaletteFallback);
|
|
4519
|
+
clearBootPalette();
|
|
4310
4520
|
window.clearTimeout(bootTimer);
|
|
4311
4521
|
window.clearInterval(intervalTimer);
|
|
4312
4522
|
cancelDurablePersist();
|
|
4313
4523
|
handleStaleConflict = void 0;
|
|
4524
|
+
applyRestoredPreference = void 0;
|
|
4314
4525
|
armAutoCheck = void 0;
|
|
4315
4526
|
if (theme.setTheme === restoreGuardSetTheme) theme.setTheme = originalSetTheme;
|
|
4316
4527
|
else console.warn("[dsh-catppuccin] theme.setTheme was replaced by another extension; leaving it in place");
|
|
@@ -4331,18 +4542,32 @@ window.__ModuleLoader__.load({
|
|
|
4331
4542
|
if (choice === "off") {
|
|
4332
4543
|
writeFlavor("off");
|
|
4333
4544
|
try {
|
|
4334
|
-
|
|
4545
|
+
if (applyRestoredPreference !== void 0) applyRestoredPreference(readRestoredPreference());
|
|
4546
|
+
else theme.setTheme(readRestoredPreference());
|
|
4335
4547
|
} catch {}
|
|
4336
4548
|
queuePersist();
|
|
4337
4549
|
return;
|
|
4338
4550
|
}
|
|
4339
4551
|
const flavor = flavorInfo(choice);
|
|
4340
4552
|
if (!flavor) return;
|
|
4553
|
+
const previous = readFlavor();
|
|
4341
4554
|
writeFlavor(flavor.themeId);
|
|
4342
4555
|
ensureThemeRegistered(flavor.themeId);
|
|
4343
4556
|
try {
|
|
4344
4557
|
theme.setTheme(flavor.themeId);
|
|
4345
|
-
} catch {
|
|
4558
|
+
} catch {
|
|
4559
|
+
themeDisposers.get(flavor.themeId)?.();
|
|
4560
|
+
themeDisposers.delete(flavor.themeId);
|
|
4561
|
+
registerThemeFor(flavor);
|
|
4562
|
+
try {
|
|
4563
|
+
theme.setTheme(flavor.themeId);
|
|
4564
|
+
} catch {
|
|
4565
|
+
writeFlavor(previous);
|
|
4566
|
+
emitPrefs();
|
|
4567
|
+
emitConflict();
|
|
4568
|
+
return;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4346
4571
|
queuePersist();
|
|
4347
4572
|
},
|
|
4348
4573
|
overrides: overridesSnapshot,
|