@nonamelego/dsh-catppuccin 0.4.2 → 0.5.0-beta.0

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 ADDED
@@ -0,0 +1,304 @@
1
+ <h3 align="center">
2
+ <img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/logos/exports/1544x1544_circle.png" width="100" alt="Logo"/><br/>
3
+ <img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/misc/transparent.png" height="30" width="0px"/>
4
+ Catppuccin for <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a>
5
+ <img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/misc/transparent.png" height="30" width="0px"/>
6
+ </h3>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/NoNameLeGo/dsh-catppuccin-theme/stargazers"><img src="https://img.shields.io/github/stars/NoNameLeGo/dsh-catppuccin-theme?colorA=363a4f&colorB=b7bdf8&style=for-the-badge"></a>
10
+ <a href="https://github.com/NoNameLeGo/dsh-catppuccin-theme/issues"><img src="https://img.shields.io/github/issues/NoNameLeGo/dsh-catppuccin-theme?colorA=363a4f&colorB=f5a97f&style=for-the-badge"></a>
11
+ <a href="https://github.com/NoNameLeGo/dsh-catppuccin-theme/contributors"><img src="https://img.shields.io/github/contributors/NoNameLeGo/dsh-catppuccin-theme?colorA=363a4f&colorB=a6da95&style=for-the-badge"></a>
12
+ <a href="https://www.npmjs.com/package/@nonamelego/dsh-catppuccin"><img src="https://img.shields.io/npm/v/@nonamelego/dsh-catppuccin?colorA=363a4f&colorB=a6da95&style=for-the-badge"></a>
13
+ <a href="https://www.npmjs.com/package/@nonamelego/dsh-catppuccin"><img src="https://img.shields.io/npm/dt/@nonamelego/dsh-catppuccin?colorA=363a4f&colorB=f5a97f&style=for-the-badge"></a>
14
+ </p>
15
+
16
+ **English** | [中文](README.md)
17
+
18
+ > A theme & glassmorphism plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — Catppuccin flavours for the **Web GUI**, **DSH Desktop** and **dsh-TUI**, plus a switchable frosted-glass skin. Listed on [Awesome DSH Plugin](https://awesome-dsh-plugin.com/p/NoNameLeGo__dsh-catppuccin-theme/).
19
+
20
+ ## Contents
21
+
22
+ - [Introduction](#introduction)
23
+ - [Features](#features)
24
+ - [Previews](#previews)
25
+ - [Installation](#installation)
26
+ - [Usage](#usage)
27
+ - [Glassmorphism](#glassmorphism)
28
+ - [Development](#development)
29
+ - [🙋 FAQ](#-faq)
30
+ - [💝 Credits](#-credits)
31
+
32
+ <p align="center">
33
+ <img src="assets/previews/combined.png" width="100%" alt="DeepSeek Harness under the four Catppuccin flavours"/>
34
+ <br/><br/>
35
+ <img src="assets/previews/glass-combined.png" width="100%" alt="Glass skin · Latte & Mocha"/>
36
+ </p>
37
+
38
+ ## Introduction
39
+
40
+ A [Catppuccin](https://github.com/catppuccin/catppuccin) theme plugin for
41
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — one package that fits
42
+ the **Web GUI** (`dsh web`), **DSH Desktop** and **dsh-TUI** alike: full recolouring plus a
43
+ glass skin on Web / Desktop, and the four official theme palettes auto-synced to the TUI.
44
+
45
+ It ships all four Catppuccin flavours — **Latte**, **Frappé**, **Macchiato**, **Mocha** —
46
+ remapping the whole interface to the matching palette, with a **Catppuccin** row right below
47
+ **Settings → General → Appearance** for one-click switching. Your choice is persisted and
48
+ restored after restarts.
49
+
50
+ It also includes an optional **glassmorphism** skin: the top bar, sidebar, composer,
51
+ stats line, trajectory view, chat bubbles and the new-session button become frosted-glass
52
+ cards, with adjustable blur, frost and backdrop brightness. Glass colours follow the active
53
+ Catppuccin theme automatically.
54
+
55
+ ## Features
56
+
57
+ - 🎨 Four themes: Latte (light), Frappé / Macchiato / Mocha (dark)
58
+ - 🧩 Registered into the official theme system, on a par with the built-in light / dark / system themes
59
+ - 🎯 Full-UI colour coverage — not just one or two accent colours
60
+ - ⚙️ One-click switch in Settings, persisted and restored across restarts
61
+ - 🌐 Bilingual UI (Chinese / English, follows the system language)
62
+ - 🪟 **Glass skin**: frosted glass for the top bar / sidebar / composer / stats line /
63
+ trajectory view / chat bubbles / new-session button, one-click toggle in Settings;
64
+ mica & compatibility modes, adjustable blur, frost and backdrop brightness
65
+ (interaction reference: [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin))
66
+ - 🌫️ **Glass details**: gradient blur bands at the top/bottom page edges, a floating glass
67
+ rail when the sidebar is collapsed, a solid background in the theme's own base colour —
68
+ content softens as it scrolls under the panes
69
+ - 🎨 Glass colours follow the current Catppuccin theme
70
+ - 🔄 **Update check**: one-click "Check for updates" in Settings compares the latest npm
71
+ version and gives you a copyable upgrade command
72
+ - 💻 **dsh-TUI terminal themes**: one command installs into dsh-TUI; the four themes sync to
73
+ `~/.dsh-tui/themes/` automatically (see [Installation · dsh-TUI](#dsh-tui-terminal-themes))
74
+
75
+ ## Previews
76
+
77
+ Actual screenshots from a local GUI (the header image is a diagonal blend of the four):
78
+
79
+ <details>
80
+ <summary>🌻 Latte (light)</summary>
81
+ <img src="assets/previews/latte.png"/>
82
+ </details>
83
+ <details>
84
+ <summary>🪴 Frappé (dark)</summary>
85
+ <img src="assets/previews/frappe.png"/>
86
+ </details>
87
+ <details>
88
+ <summary>🌺 Macchiato (dark)</summary>
89
+ <img src="assets/previews/macchiato.png"/>
90
+ </details>
91
+ <details>
92
+ <summary>🌿 Mocha (dark)</summary>
93
+ <img src="assets/previews/mocha.png"/>
94
+ </details>
95
+
96
+ ### Glass skin (Mica mode)
97
+
98
+ The frosted-glass effect in light (Latte) and dark (Mocha): the top bar, sidebar,
99
+ chat bubbles, composer and stats line are all glass cards; messages soften as they scroll
100
+ past the page edges; the background is the theme's solid base colour:
101
+
102
+ <details>
103
+ <summary>🌻 Latte (light glass)</summary>
104
+ <img src="assets/previews/glass-latte.png"/>
105
+ </details>
106
+ <details>
107
+ <summary>🌿 Mocha (dark glass)</summary>
108
+ <img src="assets/previews/glass-mocha.png"/>
109
+ </details>
110
+
111
+ ## Installation
112
+
113
+ ### Option 1: from npm (recommended)
114
+
115
+ ```sh
116
+ dsh plugin --profile web add @nonamelego/dsh-catppuccin
117
+ ```
118
+
119
+ Restart `dsh web` after installing — `dsh plugin` adds it to the profile's bundles.
120
+ Use the profile name of your choice in place of `web` (e.g. `headless`).
121
+
122
+ **[DSH Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop)**: the desktop build's
123
+ active profile is named `desktop`, so run:
124
+
125
+ ```sh
126
+ dsh plugin --profile desktop add @nonamelego/dsh-catppuccin
127
+ ```
128
+
129
+ Run it in the DSH terminal of the desktop app (`dsh plugin` defaults to the active profile).
130
+ Installing from the repo works the same way: `dsh plugin --profile desktop add https://github.com/NoNameLeGo/dsh-catppuccin-theme`.
131
+
132
+ ### Option 2: from the repository
133
+
134
+ ```sh
135
+ dsh plugin --profile web add https://github.com/NoNameLeGo/dsh-catppuccin-theme
136
+ ```
137
+
138
+ When installing from git, pnpm may ask you to allow build scripts — follow pnpm's prompt
139
+ and add the package to the profile's `pnpm-workspace.yaml` `allowBuilds`, then run it again.
140
+
141
+ ### dsh-TUI (terminal) themes
142
+
143
+ The same package covers the TUI. Install it into the dsh-tui profile:
144
+
145
+ ```sh
146
+ dsh plugin --profile dsh-tui add @nonamelego/dsh-catppuccin
147
+ ```
148
+
149
+ Installing from the repository works the same way (use the git form while the npm release
150
+ is pending). The package ships a tiny theme-sync plugin row
151
+ (`dsh-catppuccin-tui-themes`, no service dependencies): on every dsh-TUI start it syncs the
152
+ four theme JSONs to `~/.dsh-tui/themes/`, so upgrades pick up the new palettes. After
153
+ installing, launch `dsh --profile dsh-tui` and pick the theme with `/theme` —
154
+ **Catppuccin Latte / Frappé / Macchiato / Mocha**, or jump straight to it with
155
+ `/theme catppuccin-mocha` (your choice persists across restarts).
156
+
157
+ > 💡 Already installed the plugin for the Web GUI and also use dsh-TUI? No need to install
158
+ > it twice: every Web start auto-syncs the themes to `~/.dsh-tui/themes/` (only when the
159
+ > directory already exists).
160
+
161
+ > 📁 Prefer not to install the package? Copy `themes/*.json` into
162
+ > `~/.dsh-tui/themes/` (Windows: `%USERPROFILE%\.dsh-tui\themes\`) by hand — you just won't
163
+ > get updates automatically.
164
+
165
+ > ⚠️ `catppuccin-*.json` belongs to this plugin and is overwritten on sync; rename the files
166
+ > if you want custom themes.
167
+
168
+ > 💡 TUI themes only style the TUI itself — the terminal background is up to your terminal.
169
+ > Pairing it with the matching Catppuccin flavour (see the
170
+ > [Catppuccin ports list](https://github.com/catppuccin/catppuccin#-ports)) looks best.
171
+
172
+ ## Usage
173
+
174
+ 1. Open the Web GUI (default `http://127.0.0.1:3080`), or the DSH Desktop app.
175
+ 2. Go to **Settings → General**.
176
+ 3. Find the **Catppuccin** row below **Appearance** and pick a flavour:
177
+ **Latte** (light), **Frappé**, **Macchiato** or **Mocha** (dark).
178
+ 4. Choosing **Follow system** reverts to the official theme — it restores the preference
179
+ you had before enabling Catppuccin (light / dark / follow system) instead of forcing a reset.
180
+
181
+ ### Glass skin
182
+
183
+ Right below the **Catppuccin theme** row in **Settings → General** you'll find the **Glass** row:
184
+
185
+ - **Master switch**: on — the top bar, sidebar, composer, stats line and trajectory view
186
+ become frosted glass; off — the UI reverts to stock instantly (no refresh needed).
187
+ - **Mode**: **Mica** turns the interface into floating frosted cards; **Compatibility** keeps
188
+ the stock layout and swaps only the material.
189
+ - **Presets**: **Clear / Standard / Frosted** one-click presets; fine-tune with the sliders
190
+ afterwards (a preset lights up when the current knob values match it).
191
+ - **Blur** (0–40 px) and **Frost** (0–100%): the blur radius and opacity of the glass.
192
+ - **Backdrop brightness**: dark mode darkens 0–50, light mode brightens 50–100 (50 = as-is),
193
+ mixed straight into the solid background.
194
+
195
+ Glass colours follow the active theme live; all settings persist across restarts.
196
+
197
+ ### Check for plugin updates
198
+
199
+ In **Settings → General**, right below the **Glass** row:
200
+
201
+ - Clicking **Check for updates** compares the latest npm version with the installed one:
202
+ up to date → shows the current version; newer → shows the new version plus a copyable
203
+ upgrade command (the profile name is detected automatically, falling back to `web`).
204
+ - Locally linked / source installs (`link:` / `file:` / git) don't show an npm upgrade
205
+ command — you'll be told to `git pull` or rebuild instead.
206
+ - Channel policy: stable builds follow the `latest` tag; prereleases follow both `latest`
207
+ and `beta` (the upgrade command automatically carries `@beta`). Offline or network
208
+ failures show the reason and offer a retry.
209
+
210
+ ## Glassmorphism
211
+
212
+ **Glassmorphism** is a visual style in which panels look like frosted glass — translucent
213
+ fills, backdrop blur (`backdrop-filter: blur()`), and glass details (rim, inner highlight,
214
+ soft shadow) — letting the content behind show through, softened.
215
+
216
+ What this plugin does:
217
+
218
+ - **Seven glass areas**: top bar, sidebar, composer, stats line, trajectory view, chat
219
+ bubbles and the new-session button; in Mica mode they become rounded floating cards and
220
+ chat content scrolls *under* the glass, blurred; the collapsed sidebar becomes a
221
+ floating rail at the edge of the chat area;
222
+ - **Page-edge gradient blur**: a blur band at the top and bottom of the viewport so
223
+ messages are softened as they melt past the edges (borrowed from
224
+ [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin)'s
225
+ Aqua skin);
226
+ - **Theme-following colours**: Latte is light glass, Mocha dark glass — switching flavours
227
+ recolours instantly. The page ground is the theme's solid colour; the brightness knob
228
+ mixes white/black straight into it;
229
+ - **One-click toggle**: off restores the stock UI exactly; uninstalling the plugin leaves
230
+ nothing behind.
231
+
232
+ ## Development
233
+
234
+ ```sh
235
+ pnpm install
236
+ pnpm typecheck # tsc --noEmit type check
237
+ pnpm test # vitest palette-coverage tests
238
+ pnpm build # tsdown build -> lib/index.js (host) + lib/client.js (browser)
239
+ ```
240
+
241
+ Palettes are produced by a generator script — after editing
242
+ `scripts/generate-palettes.mjs`, rerun:
243
+
244
+ ```sh
245
+ node scripts/generate-palettes.mjs
246
+ ```
247
+
248
+ The changelog draft is generated from your conventional commits (bilingual `EN:` support
249
+ in commit bodies):
250
+
251
+ ```sh
252
+ pnpm changelog:gen # print the draft since the last tag
253
+ pnpm changelog:gen -- --write # write it into the [Unreleased] section
254
+ ```
255
+
256
+ ### Local link debugging
257
+
258
+ Clone the repo, link it into a profile and add it to the bundles (use your own paths):
259
+
260
+ ```sh
261
+ pnpm --dir C:\Users\LeGo\.dsh\profiles\web add link:D:\Vibe-Coding\dsh-catppuccin
262
+ ```
263
+
264
+ Then add `@nonamelego/dsh-catppuccin` to the profile's `package.json`
265
+ `dsh.profile.bundles` and restart `dsh web`. For DSH Desktop use
266
+ `~/.dsh/profiles/desktop` instead.
267
+
268
+ ## 🙋 FAQ
269
+
270
+ - **Q: "Why don't I see the Catppuccin themes in the Appearance row?"**\
271
+ A: The stock Appearance row only lists the built-in light / dark / follow-system
272
+ preferences. The four flavours live in the **Catppuccin** row right below it.
273
+ - **Q: "How is my theme choice remembered?"**
274
+ A: The choice persists in DSH's official settings document (the `catppuccin` namespace,
275
+ stored under the DSH home, shared across every DSH instance on the machine), with
276
+ localStorage as an in-browser cache and cross-tab sync. So plain `dsh web`,
277
+ `dsh web --port <custom>`, and **DSH Desktop** (which boots on a fresh random loopback
278
+ port every launch) all restore the preference automatically. The glass switch and every
279
+ knob persist the same way. Since 0.5.0, a legacy `catppuccin-state.json` is migrated
280
+ into the settings document once on first launch (the file is kept as a rollback copy).
281
+ - **Q: "How do I know if this plugin has a new version?"**
282
+ A: Settings → General → **Check Catppuccin plugin updates** compares against npm in one
283
+ click and gives a copyable upgrade command; or run
284
+ `dsh plugin --profile web update @nonamelego/dsh-catppuccin` manually (re-`add` the
285
+ latest version works too). In [DSH Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop),
286
+ use `desktop` as the profile name, or just run `dsh plugin update` in the app's DSH terminal.
287
+
288
+ ## 💝 Credits
289
+
290
+ - [Catppuccin](https://github.com/catppuccin/catppuccin) for the palettes and port templates
291
+ - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) for the plugin system
292
+ - [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin)
293
+ for the glass-skin interaction and implementation reference (mica / compatibility modes,
294
+ blur / frost / brightness knobs)
295
+
296
+ &nbsp;
297
+
298
+ <p align="center">
299
+ <img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/footers/gray0_ctp_on_line.svg?sanitize=true" />
300
+ </p>
301
+
302
+ <p align="center">
303
+ Copyright &copy; 2021-present <a href="https://github.com/catppuccin/catppuccin" target="_blank">Catppuccin Org</a>
304
+ </p>
package/README.md CHANGED
@@ -13,6 +13,8 @@
13
13
  <a href="https://www.npmjs.com/package/@nonamelego/dsh-catppuccin"><img src="https://img.shields.io/npm/dt/@nonamelego/dsh-catppuccin?colorA=363a4f&colorB=f5a97f&style=for-the-badge"></a>
14
14
  </p>
15
15
 
16
+ **中文** | [English](README.en.md)
17
+
16
18
  ## 目录
17
19
 
18
20
  - [简介](#简介)
@@ -251,11 +253,12 @@ pnpm --dir C:\Users\LeGo\.dsh\profiles\web add link:D:\Vibe-Coding\dsh-catppucci
251
253
  A: 官方外观行只列出内置的浅色/深色/跟随系统偏好。四个主题在它正下方的
252
254
  **Catppuccin** 行里。
253
255
  - Q: **_"我的主题选择是怎么记住的?"_**
254
- A: 选择持久保存在 DSH home 下的 `catppuccin-state.json`(同一台机器的
255
- DSH 共享这份偏好),浏览器 localStorage 作为即时缓存与多标签页同步。
256
- 因此普通 `dsh web`、`dsh web --port` 自定义端口,以及 **DSH Desktop**
257
- (每次启动用随机回环端口)都能跨重启自动恢复;玻璃质感开关与各旋钮同样
258
- 持久保存。
256
+ A: 选择持久保存在 DSH 的官方 settings 文档(`catppuccin` namespace,存于
257
+ DSH home 下,同一台机器的 DSH 共享这份偏好),浏览器 localStorage 作为
258
+ 即时缓存与多标签页同步。因此普通 `dsh web`、`dsh web --port` 自定义端口,
259
+ 以及 **DSH Desktop**(每次启动用随机回环端口)都能跨重启自动恢复;玻璃质感
260
+ 开关与各旋钮同样持久保存。0.5.0 起旧版 `catppuccin-state.json` 会在首次
261
+ 启动时一次性迁移进 settings 文档(文件保留作回退)。
259
262
  - Q: **_"怎么知道这个插件有没有新版本?"_**
260
263
  A: 设置 → 常规 → **检查 Catppuccin 插件更新** 一键检测本插件在 npm 上的最新版本,
261
264
  发现新版会给出可复制的升级命令;也可以随时手动执行
package/lib/client.js CHANGED
@@ -1037,8 +1037,9 @@ window.__ModuleLoader__.load({
1037
1037
  observer.disconnect();
1038
1038
  };
1039
1039
  }
1040
- /** Host route answering the durable state (GET reads, PUT writes). */
1041
- const STATE_ROUTE_PATH = "/catppuccin/state";
1040
+ /** Settings namespace both halves address: Host registers it, Client binds
1041
+ * it through `ctx.settingsScope` (see `src/settings-catppuccin.ts`). */
1042
+ const CATPPUCCIN_SETTINGS_NS = "catppuccin";
1042
1043
  /** The four registered Catppuccin theme ids. MUST stay in sync with the
1043
1044
  * `themeId` of `CATPPUCCIN_FLAVORS` in `src/client/palettes.ts` — guarded by
1044
1045
  * `tests/state.spec.ts` — because the persisted flavour value IS the theme id. */
@@ -1098,6 +1099,23 @@ window.__ModuleLoader__.load({
1098
1099
  const expected = defaultState();
1099
1100
  return state.flavor === expected.flavor && state.glass.enabled === expected.glass.enabled && state.glass.mode === expected.glass.mode && state.glass.blur === expected.glass.blur && state.glass.frost === expected.glass.frost && state.glass.brightness === expected.glass.brightness;
1100
1101
  }
1102
+ /** Drop the synthetic `version` field: full state → settings-document section. */
1103
+ function settingsSectionFromState(state) {
1104
+ return {
1105
+ flavor: state.flavor,
1106
+ glass: { ...state.glass }
1107
+ };
1108
+ }
1109
+ /** Lift a settings-document section back into the full state-shaped contract
1110
+ * (re-sanitized so a hand-edited document can never poison the plugin). */
1111
+ function stateFromSettingsSection(section) {
1112
+ return sanitizeState(section);
1113
+ }
1114
+ /** Whether two settings sections are field-for-field equal (used to skip
1115
+ * redundant mirror writes and to recognize the Host echoing our own write). */
1116
+ function settingsSectionsEqual(a, b) {
1117
+ 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;
1118
+ }
1101
1119
  //#endregion
1102
1120
  //#region src/client/glass/glass-layer.ts
1103
1121
  /** html attribute selecting the glass layer: CSS hooks and page effects. */
@@ -1927,75 +1945,98 @@ window.__ModuleLoader__.load({
1927
1945
  //#endregion
1928
1946
  //#region src/client/state-sync.ts
1929
1947
  /**
1930
- * Browser half of the durable Catppuccin state a thin wrapper over the
1931
- * Host's `/catppuccin/state` route (same-origin relative fetch, so it works on
1932
- * DSH Desktop's per-launch random loopback port). The Host file is the source
1933
- * of truth; this module adds the fetch wrapper plus a debounced trailing
1934
- * writer so rapid knob drags coalesce into a single PUT. Every failure is
1935
- * non-fatal: the plugin keeps working from browser localStorage alone.
1948
+ * Bind the Catppuccin namespace on this context. The scope derives from the
1949
+ * shared document mirror (no wire read of its own) and its life is bound to
1950
+ * the calling fiber, so binding never blocks on the settings transport.
1936
1951
  */
1937
- /** Read the durable state from the Host file (same-origin GET). */
1938
- async function readDurableStateRemote() {
1939
- try {
1940
- const response = await fetch(STATE_ROUTE_PATH, { headers: { accept: "application/json" } });
1941
- if (!response.ok) return {
1942
- available: false,
1943
- state: null
1944
- };
1945
- const payload = await response.json();
1946
- if (payload.ok !== true) return {
1947
- available: false,
1948
- state: null
1949
- };
1950
- return {
1951
- available: true,
1952
- state: payload.state === null || payload.state === void 0 ? null : sanitizeState(payload.state)
1953
- };
1954
- } catch {
1955
- return {
1956
- available: false,
1957
- state: null
1958
- };
1959
- }
1952
+ function bindCatppuccinScope(ctx) {
1953
+ return ctx.settingsScope.bind({ namespace: CATPPUCCIN_SETTINGS_NS });
1960
1954
  }
1961
- /** Write the durable state to the Host file (same-origin PUT). */
1962
- async function persistDurableState(state) {
1955
+ /** Whether the scope is usable for durable persistence: the document is
1956
+ * resolved (`ready`) and the Host persistence is active (`host`). A `memory`
1957
+ * scope (non-loopback pages / absent transport) or a not-yet-ready one is
1958
+ * NOT usable — the caller keeps localStorage as the only store. */
1959
+ function isScopeUsable(snapshot) {
1960
+ return snapshot.status === "ready" && snapshot.mode === "host";
1961
+ }
1962
+ /** The local state contract (with the synthetic `version`) for a usable
1963
+ * scope snapshot; `null` while the section is not resolved. */
1964
+ function durableStateFromSnapshot(snapshot) {
1965
+ if (!isScopeUsable(snapshot) || snapshot.value === void 0) return null;
1966
+ return stateFromSettingsSection(snapshot.value);
1967
+ }
1968
+ /** One atomic mutation covering the whole local state (revision-fenced).
1969
+ * Writing every field keeps the section self-contained — a partial write
1970
+ * could otherwise leave a knob stuck on a stale override after a document
1971
+ * edit elsewhere. */
1972
+ function stateToMutateOps(state) {
1973
+ const section = settingsSectionFromState(state);
1974
+ return [
1975
+ {
1976
+ op: "set",
1977
+ path: ["flavor"],
1978
+ value: section.flavor
1979
+ },
1980
+ {
1981
+ op: "set",
1982
+ path: ["glass", "enabled"],
1983
+ value: section.glass.enabled
1984
+ },
1985
+ {
1986
+ op: "set",
1987
+ path: ["glass", "mode"],
1988
+ value: section.glass.mode
1989
+ },
1990
+ {
1991
+ op: "set",
1992
+ path: ["glass", "blur"],
1993
+ value: section.glass.blur
1994
+ },
1995
+ {
1996
+ op: "set",
1997
+ path: ["glass", "frost"],
1998
+ value: section.glass.frost
1999
+ },
2000
+ {
2001
+ op: "set",
2002
+ path: ["glass", "brightness"],
2003
+ value: section.glass.brightness
2004
+ }
2005
+ ];
2006
+ }
2007
+ /** Persist the local state through the scope as one atomic mutation.
2008
+ * Resolves `true` when the write was issued. */
2009
+ async function persistStateToScope(scope, state) {
1963
2010
  try {
1964
- return (await fetch(STATE_ROUTE_PATH, {
1965
- method: "PUT",
1966
- headers: {
1967
- accept: "application/json",
1968
- "content-type": "application/json"
1969
- },
1970
- body: JSON.stringify(sanitizeState(state))
1971
- })).ok;
2011
+ await scope.mutate(stateToMutateOps(state));
2012
+ return true;
1972
2013
  } catch {
1973
2014
  return false;
1974
2015
  }
1975
2016
  }
1976
2017
  let pendingTimer;
1977
- let pendingGetState;
2018
+ let pendingWrite;
1978
2019
  /**
1979
2020
  * Debounced trailing persist: after a burst of changes (a slider drag emits a
1980
- * stream of snapshot updates) the latest state is written to the Host once,
1981
- * a short beat after the last change. Pass an accessor so the snapshot is
1982
- * taken at flush time, never stale.
2021
+ * stream of snapshot updates) the write happens once, a short beat after the
2022
+ * last change. Pass an accessor so the state snapshot is taken at flush time,
2023
+ * never stale.
1983
2024
  */
1984
- function scheduleDurablePersist(getState, delayMs = 300) {
2025
+ function scheduleDurablePersist(write, delayMs = 300) {
1985
2026
  if (pendingTimer !== void 0) window.clearTimeout(pendingTimer);
1986
- pendingGetState = getState;
2027
+ pendingWrite = write;
1987
2028
  pendingTimer = window.setTimeout(() => {
1988
2029
  pendingTimer = void 0;
1989
- const state = pendingGetState;
1990
- pendingGetState = void 0;
1991
- if (state !== void 0) persistDurableState(state()).catch(() => {});
2030
+ const run = pendingWrite;
2031
+ pendingWrite = void 0;
2032
+ if (run !== void 0) Promise.resolve().then(run).catch(() => {});
1992
2033
  }, delayMs);
1993
2034
  }
1994
2035
  /** Drop any queued persist (plugin unload / page teardown). */
1995
2036
  function cancelDurablePersist() {
1996
2037
  if (pendingTimer !== void 0) window.clearTimeout(pendingTimer);
1997
2038
  pendingTimer = void 0;
1998
- pendingGetState = void 0;
2039
+ pendingWrite = void 0;
1999
2040
  }
2000
2041
  //#endregion
2001
2042
  //#region \0dsh-css:/home/runner/work/dsh-catppuccin-theme/dsh-catppuccin-theme/src/client/glass/glass.module.css.mjs
@@ -2086,11 +2127,13 @@ window.__ModuleLoader__.load({
2086
2127
  if (preference !== "light" && preference !== "dark") return false;
2087
2128
  return preference === livePick;
2088
2129
  }
2089
- /** Required services: slots + locale (settings rows) and theme (register + switch). */
2130
+ /** Required services: slots + locale (settings rows), theme (register +
2131
+ * switch), and the settings scope (durable persistence). */
2090
2132
  const inject = [
2091
2133
  "slots",
2092
2134
  "locale",
2093
- "theme"
2135
+ "theme",
2136
+ "settingsScope"
2094
2137
  ];
2095
2138
  /**
2096
2139
  * Register the Catppuccin dictionaries, the four flavour themes, and the
@@ -2121,11 +2164,16 @@ window.__ModuleLoader__.load({
2121
2164
  };
2122
2165
  }, "catppuccin: flavour themes");
2123
2166
  const glass = new GlassLayer(ctx);
2167
+ const scope = bindCatppuccinScope(ctx);
2124
2168
  const buildLocalState = () => ({
2125
2169
  version: 1,
2126
2170
  flavor: readFlavor(),
2127
2171
  glass: glass.getRemoteState()
2128
2172
  });
2173
+ const persistLocal = () => {
2174
+ if (!isScopeUsable(scope.getSnapshot())) return;
2175
+ persistStateToScope(scope, buildLocalState());
2176
+ };
2129
2177
  ctx.effect(() => {
2130
2178
  let liveBuiltinPick = null;
2131
2179
  const originalSetTheme = theme.setTheme;
@@ -2155,22 +2203,26 @@ window.__ModuleLoader__.load({
2155
2203
  };
2156
2204
  window.addEventListener("storage", onStorage);
2157
2205
  const offGlass = glass.subscribe(() => {
2158
- scheduleDurablePersist(buildLocalState);
2206
+ scheduleDurablePersist(persistLocal);
2159
2207
  });
2160
- let cancelled = false;
2161
- readDurableStateRemote().then((result) => {
2162
- if (cancelled) return;
2163
- if (!result.available) return;
2164
- if (result.state === null) {
2165
- if (!isDefaultState(buildLocalState())) scheduleDurablePersist(buildLocalState);
2208
+ const applyScopeSnapshot = () => {
2209
+ const snapshot = scope.getSnapshot();
2210
+ const state = durableStateFromSnapshot(snapshot);
2211
+ if (state === null) return;
2212
+ const local = buildLocalState();
2213
+ if (snapshot.user === void 0) {
2214
+ if (!isDefaultState(local)) scheduleDurablePersist(persistLocal);
2166
2215
  return;
2167
2216
  }
2168
- writeFlavor(result.state.flavor);
2169
- glass.applyRemote(result.state.glass);
2217
+ if (settingsSectionsEqual(settingsSectionFromState(local), settingsSectionFromState(state))) return;
2218
+ writeFlavor(state.flavor);
2219
+ glass.applyRemote(state.glass);
2170
2220
  applyDesired();
2171
- });
2221
+ };
2222
+ applyScopeSnapshot();
2223
+ const offScope = scope.subscribe(applyScopeSnapshot);
2172
2224
  return () => {
2173
- cancelled = true;
2225
+ offScope();
2174
2226
  offGlass();
2175
2227
  disposer();
2176
2228
  window.removeEventListener("storage", onStorage);
@@ -2193,14 +2245,14 @@ window.__ModuleLoader__.load({
2193
2245
  if (choice === "off") {
2194
2246
  writeFlavor("off");
2195
2247
  theme.setTheme(readRestoredPreference());
2196
- scheduleDurablePersist(buildLocalState);
2248
+ scheduleDurablePersist(persistLocal);
2197
2249
  return;
2198
2250
  }
2199
2251
  const flavor = flavorInfo(choice);
2200
2252
  if (!flavor) return;
2201
2253
  writeFlavor(flavor.themeId);
2202
2254
  theme.setTheme(flavor.themeId);
2203
- scheduleDurablePersist(buildLocalState);
2255
+ scheduleDurablePersist(persistLocal);
2204
2256
  }
2205
2257
  });
2206
2258
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({