@dsh-plugin/dsh-ui-experience 0.0.0-stage → 1.0.0-dev.43485427852

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/lib/index.js ADDED
@@ -0,0 +1,584 @@
1
+ // src/index.ts
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { readFile as readFileAsync, stat as statAsync } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, isAbsolute, join } from "node:path";
6
+ import z from "@deepseek-ai/schemastery";
7
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
8
+
9
+ // src/shared.ts
10
+ var REMOTE_SERVICE = "uiExperience";
11
+ var BOOT_ATTRIBUTE = "data-dsh-uix-matugen";
12
+ var DEFAULT_COLORS_PATH = "~/.local/state/quickshell/user/generated/colors.json";
13
+ var DEFAULT_TOKENS_PATH = "~/.local/state/quickshell/user/generated/dsh-tokens.json";
14
+
15
+ // src/index.ts
16
+ var LOG_FILE = join(homedir(), ".dsh", "ui-experience.log");
17
+ var LOG_MAX_BYTES = 512 * 1024;
18
+ function logLine(message) {
19
+ try {
20
+ const dir = dirname(LOG_FILE);
21
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
22
+ if (existsSync(LOG_FILE) && statSync(LOG_FILE).size > LOG_MAX_BYTES) writeFileSync(LOG_FILE, "");
23
+ appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
24
+ `);
25
+ } catch (error) {
26
+ console.error(`[ui-experience] failed to write ${LOG_FILE}: ${error.message}`);
27
+ }
28
+ }
29
+ var BORDER_WIDTH = /^(?:0|[0-9]+(?:\.[0-9]+)?(?:px|rem|em))$/;
30
+ var PATH_PATTERN = /^(?:~$|~\/|\/)/;
31
+ var Config = z.object({
32
+ active: z.boolean().default(false).volatile(),
33
+ borderWidth: z.string().pattern(BORDER_WIDTH).default("0.5px").volatile(),
34
+ colorsPath: z.string().pattern(PATH_PATTERN).default(DEFAULT_COLORS_PATH).volatile(),
35
+ tokensPath: z.string().pattern(PATH_PATTERN).default(DEFAULT_TOKENS_PATH).volatile()
36
+ });
37
+ var REQUIRED_COLOR_KEYS = [
38
+ "surface_container_lowest",
39
+ "surface_container_low",
40
+ "surface_container",
41
+ "surface_container_high",
42
+ "surface_container_highest",
43
+ "surface_bright",
44
+ "on_surface",
45
+ "on_surface_variant",
46
+ "outline",
47
+ "outline_variant",
48
+ "primary",
49
+ "primary_container",
50
+ "on_primary",
51
+ "secondary",
52
+ "secondary_container",
53
+ "tertiary",
54
+ "tertiary_container",
55
+ "error",
56
+ "error_container",
57
+ "inverse_surface",
58
+ "inverse_on_surface",
59
+ "scrim",
60
+ "shadow"
61
+ ];
62
+ var HEX_COLOR = /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/;
63
+ var CSS_VALUE_FORBIDDEN = /[{};<>\n\r]|\/\*|\*\//;
64
+ var CSS_VALUE_MAX_LENGTH = 256;
65
+ function isSafeTokenValue(value) {
66
+ return value.length > 0 && value.length <= CSS_VALUE_MAX_LENGTH && !CSS_VALUE_FORBIDDEN.test(value);
67
+ }
68
+ var REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
69
+ function mapColors(colors) {
70
+ const surface = [
71
+ colors.surface_container_lowest,
72
+ colors.surface_container_low,
73
+ colors.surface_container,
74
+ colors.surface_container_high,
75
+ colors.surface_container_highest,
76
+ colors.surface_bright
77
+ ];
78
+ return {
79
+ // ── surfaces ──────────────────────────────────────────────
80
+ "--dsw-alias-bg-base": surface[1],
81
+ "--dsw-alias-bg-layer-1": surface[2],
82
+ "--dsw-alias-bg-layer-2": surface[3],
83
+ "--dsw-alias-bg-layer-3": surface[4],
84
+ "--dsw-alias-bg-layer-4": surface[5],
85
+ "--dsw-alias-bg-overlay": `color-mix(in srgb, ${surface[5]}, white 8%)`,
86
+ // 内置主题的遮罩是半透明黑,直接取 scrim 会得到不透明纯黑,盖住下层界面。
87
+ "--dsw-alias-bg-mask-1": `color-mix(in srgb, ${colors.scrim}, transparent 50%)`,
88
+ "--dsw-alias-bg-module-platform": surface[4],
89
+ "--dsw-alias-bg-skeleton": surface[4],
90
+ // ── borders — uniform outline_variant to avoid visual fragmentation ──
91
+ "--dsw-alias-border-l1": colors.outline_variant,
92
+ "--dsw-alias-border-l2": colors.outline_variant,
93
+ "--dsw-alias-border-l2-darkmode-thin": colors.outline_variant,
94
+ "--dsw-alias-border-l3": colors.outline_variant,
95
+ "--dsw-alias-border-l4": colors.outline_variant,
96
+ // ── brand & labels ────────────────────────────────────────
97
+ "--dsw-alias-brand-primary": colors.primary,
98
+ "--dsw-alias-brand-primary-new-colorprimary-new-color": colors.primary,
99
+ "--dsw-alias-label-primary": colors.on_surface,
100
+ "--dsw-alias-label-secondary": colors.on_surface_variant,
101
+ "--dsw-alias-label-tertiary": colors.on_surface_variant,
102
+ "--dsw-alias-label-caption": colors.on_surface_variant,
103
+ "--dsw-alias-label-dimmed": colors.outline,
104
+ "--dsw-alias-label-error": colors.error,
105
+ "--dsw-alias-label-primary-foreground": colors.on_primary,
106
+ "--dsw-alias-label-shimmer": colors.on_surface_variant,
107
+ "--dsw-alias-link": colors.primary,
108
+ // ── interactive ───────────────────────────────────────────
109
+ "--dsw-alias-interactive-bg-hover": surface[4],
110
+ "--dsw-alias-interactive-bg-active": surface[5],
111
+ "--dsw-alias-interactive-bg-hover-danger": colors.error_container,
112
+ // ── buttons ───────────────────────────────────────────────
113
+ "--dsw-alias-button-primary-fill": colors.primary,
114
+ "--dsw-alias-button-primary-hover": colors.primary,
115
+ "--dsw-alias-button-info-fill": colors.primary,
116
+ "--dsw-alias-button-ghost-active-fill": surface[4],
117
+ "--dsw-alias-button-ghost-active-border": colors.outline,
118
+ "--dsw-alias-button-tool-bar-fill": surface[4],
119
+ "--dsw-alias-button-tool-bar-hover": surface[5],
120
+ "--dsw-alias-label-on-color": colors.on_primary,
121
+ // ── states ────────────────────────────────────────────────
122
+ "--dsw-alias-state-error-primary": colors.error,
123
+ "--dsw-alias-state-idle-primary": colors.outline,
124
+ "--dsw-alias-state-success-primary": colors.tertiary,
125
+ "--dsw-alias-state-success-tertiary": colors.tertiary_container,
126
+ "--dsw-alias-state-warn-primary": colors.secondary,
127
+ "--dsw-alias-state-warn-label": colors.secondary,
128
+ "--dsw-alias-state-warn-tertiary": colors.secondary_container,
129
+ "--dsw-alias-state-business-primary": colors.primary,
130
+ // ── switch ────────────────────────────────────────────────
131
+ // switch-thumb 是关闭态滑块色,语义为中性前景,内置主题暗色取
132
+ // neutral-bluish-400(纯灰)、浅色取近白。映射成 primary 会让关闭态带上
133
+ // 强调色,映射成 on_surface_variant 又会把前景色渗进整体观感;这里不做
134
+ // 覆盖,保持原生纯灰,开关只在开启态轨道显示强调色。
135
+ // ── menu & overlay ────────────────────────────────────────
136
+ "--dsw-alias-menu-icon": colors.on_surface_variant,
137
+ "--dsw-alias-menu-group-header-fill": surface[4],
138
+ "--dsw-alias-hovercard-bg": surface[4],
139
+ "--dsw-alias-hovercard-label-primary": colors.on_surface,
140
+ // 官方契约里 tooltip 与 toast 始终是深色气泡配浅色文字,key 块是气泡色
141
+ // 混 18% 白。Material 的 inverse_* 在深色调色板下方向相反(浅底深字),
142
+ // 会让继承 label 色的键位提示变成深底深字,故这里改用容器面与前景色。
143
+ "--dsw-alias-tooltip-bg": surface[4],
144
+ "--dsw-alias-tooltip-label": colors.on_surface,
145
+ "--dsw-alias-tooltip-key-bg": `color-mix(in srgb, ${surface[4]}, white 18%)`,
146
+ "--dsw-alias-toast-bg": surface[4],
147
+ "--dsw-alias-toast-label": colors.on_surface,
148
+ "--dsw-alias-toast-action": colors.primary,
149
+ // ── scrollbar ─────────────────────────────────────────────
150
+ "--dsw-alias-scrollbar-bg-l2": surface[2],
151
+ "--dsw-alias-scrollbar-hover-l2": surface[4],
152
+ // ── markdown & code ───────────────────────────────────────
153
+ "--dsw-alias-markdown-code-block": surface[2],
154
+ "--dsw-alias-markdown-code-block-banner": surface[3],
155
+ "--dsw-alias-markdown-inline-code": surface[4],
156
+ "--dsw-alias-markdown-tag": surface[4],
157
+ // ── JSON tree ─────────────────────────────────────────────
158
+ "--dsw-alias-json-tree-hover": surface[4],
159
+ "--dsw-alias-json-tree-icon": colors.on_surface_variant,
160
+ "--dsw-alias-json-tree-keyword": colors.primary,
161
+ "--dsw-alias-json-tree-number": colors.tertiary,
162
+ "--dsw-alias-json-tree-property": colors.on_surface,
163
+ "--dsw-alias-json-tree-punctuation": colors.on_surface_variant,
164
+ "--dsw-alias-json-tree-string": colors.secondary,
165
+ // ── diff ──────────────────────────────────────────────────
166
+ "--dsw-alias-code-diff-added": surface[4],
167
+ "--dsw-alias-code-diff-deleted": surface[4],
168
+ "--dsw-alias-file-diff-added-bg": surface[2],
169
+ "--dsw-alias-file-diff-added-marker": colors.tertiary,
170
+ "--dsw-alias-file-diff-deleted-bg": surface[2],
171
+ "--dsw-alias-file-diff-deleted-marker": colors.error,
172
+ // ── app-specific ──────────────────────────────────────────
173
+ "--dsw-specific-sidebar-fill": surface[0],
174
+ "--dsw-specific-input-major": surface[2],
175
+ "--dsw-specific-bubble": surface[2],
176
+ "--dsw-specific-menu": surface[3],
177
+ "--dsw-menu-surface-fill": surface[3],
178
+ "--dsw-focus-ring-color": colors.primary,
179
+ // ── plugin-side surfaces & fills ──────────────────────────
180
+ "--dsw-alias-bg-document-preview": surface[2],
181
+ "--dsw-alias-bg-l1": surface[2],
182
+ "--dsw-alias-bg-mask-drop": `color-mix(in srgb, ${colors.scrim}, transparent 30%)`,
183
+ "--dsw-alias-card-fill": surface[2],
184
+ "--dsw-alias-card-fill-hover": surface[3],
185
+ "--dsw-alias-card-tile-fill": surface[2],
186
+ "--dsw-alias-fill-l1": surface[2],
187
+ "--dsw-alias-fill-l2": surface[4],
188
+ "--dsw-alias-fill-tsp-secondary": surface[3],
189
+ "--dsw-alias-settings-card-fill": surface[2],
190
+ "--dsw-alias-settings-card-stroke": colors.outline_variant,
191
+ "--dsw-alias-illustration-background": surface[1],
192
+ "--dsw-alias-illustration-backing": surface[2],
193
+ "--dsw-alias-illustration-detail": surface[3],
194
+ "--dsw-alias-illustration-inset": surface[0],
195
+ "--dsw-alias-illustration-shadow": colors.shadow,
196
+ "--dsw-alias-illustration-surface": surface[4],
197
+ // ── plugin-side borders & separators ──────────────────────
198
+ "--dsw-alias-border-inverted": colors.inverse_surface,
199
+ "--dsw-alias-border-selected": colors.primary,
200
+ "--dsw-alias-separator-primary": colors.outline_variant,
201
+ // ── plugin-side buttons ───────────────────────────────────
202
+ "--dsw-alias-button-contrast-fill": colors.inverse_surface,
203
+ "--dsw-alias-button-elevated-fill": surface[4],
204
+ "--dsw-alias-button-floating-fill": surface[4],
205
+ "--dsw-alias-button-floating-hover": surface[5],
206
+ "--dsw-alias-button-info-hover": colors.primary_container,
207
+ // ── plugin-side labels ────────────────────────────────────
208
+ "--dsw-alias-label-deep-diving": colors.primary,
209
+ "--dsw-alias-label-deep-diving-shimmer": colors.secondary,
210
+ "--dsw-alias-label-primary-bluish": colors.on_surface,
211
+ "--dsw-alias-label-primary-dimmed": colors.on_surface_variant,
212
+ "--dsw-alias-label-primary-inverted": colors.inverse_on_surface,
213
+ "--dsw-alias-label-quaternary": colors.outline,
214
+ "--dsw-alias-hovercard-label-secondary": colors.on_surface_variant,
215
+ "--dsw-alias-hovercard-label-tertiary": colors.on_surface_variant,
216
+ // ── plugin-side interactive & states ──────────────────────
217
+ "--dsw-alias-interactive-bg-hover-accent": colors.primary_container,
218
+ "--dsw-alias-interactive-bg-hover-solid": surface[5],
219
+ "--dsw-alias-state-business-tertiary": colors.primary_container,
220
+ "--dsw-alias-state-error-secondary": colors.error_container,
221
+ "--dsw-alias-state-success-secondary": colors.tertiary_container,
222
+ "--dsw-alias-state-warn-secondary": colors.secondary_container,
223
+ "--dsw-alias-trajectory-accent": colors.primary,
224
+ // ── charts, citations, onboarding, turn triggers ──────────
225
+ "--dsw-alias-chart-messages": colors.primary,
226
+ "--dsw-alias-chart-system": colors.secondary,
227
+ "--dsw-alias-chart-tools": colors.tertiary,
228
+ "--dsw-alias-markdown-citation": surface[4],
229
+ "--dsw-alias-onboarding-accent": colors.primary,
230
+ "--dsw-alias-onboarding-card-fill": surface[2],
231
+ "--dsw-alias-onboarding-checkbox-border": colors.outline,
232
+ "--dsw-alias-onboarding-secondary-fill": surface[4],
233
+ "--dsw-alias-turn-trigger-bg": surface[4],
234
+ "--dsw-alias-turn-trigger-bg-hover": surface[5],
235
+ // ── plugin-side scrollbars ────────────────────────────────
236
+ "--dsw-alias-scrollbar-bg-l1": surface[0],
237
+ "--dsw-alias-scrollbar-hover-l1": surface[2],
238
+ // ── diff gutters ──────────────────────────────────────────
239
+ "--dsw-alias-file-diff-added-gutter": colors.tertiary_container,
240
+ "--dsw-alias-file-diff-deleted-gutter": colors.error_container,
241
+ // ── sidebar & misc specifics ──────────────────────────────
242
+ // 侧栏玻璃按钮是侧栏内层,取比侧栏底色高一级,避免与底色重合。tint 与
243
+ // wash 只在 macOS 上作为透明渐变叠加在侧栏之上,这里保持与底色同值。
244
+ "--dsw-specific-sidebar-glass-fill": surface[1],
245
+ "--dsw-specific-sidebar-glass-hover": surface[2],
246
+ "--dsw-specific-sidebar-glass-collapsed-fill": surface[1],
247
+ "--dsw-specific-sidebar-glass-collapsed-hover": surface[2],
248
+ "--dsw-specific-sidebar-nav-item-active": surface[4],
249
+ // 该 token 也作为「推荐」徽标等元素的文字底色,文字色是 button-info-fill
250
+ // (映射为 primary),因此这里必须用容器色保证对比。
251
+ "--dsw-specific-sidebar-nav-item-active-accent": colors.primary_container,
252
+ "--dsw-specific-sidebar-right-flash": surface[3],
253
+ "--dsw-specific-sidebar-tint": surface[0],
254
+ "--dsw-specific-sidebar-wash-bottom": surface[0],
255
+ "--dsw-specific-sidebar-wash-bottom-clear": surface[0],
256
+ "--dsw-specific-sidebar-wash-top": surface[0],
257
+ "--dsw-specific-sidebar-wash-top-clear": surface[0],
258
+ "--dsw-specific-file-upload-track": surface[4],
259
+ "--dsw-specific-guide-hero": colors.primary,
260
+ "--dsw-specific-selector": surface[4]
261
+ };
262
+ }
263
+ function validateColors(value) {
264
+ if (typeof value !== "object" || value === null) {
265
+ throw new Error("matugen colors file is not a JSON object");
266
+ }
267
+ const colors = value;
268
+ for (const key of REQUIRED_COLOR_KEYS) {
269
+ const color = colors[key];
270
+ if (typeof color !== "string" || !HEX_COLOR.test(color)) {
271
+ throw new Error(`matugen colors file: role "${key}" is missing or not a hex color`);
272
+ }
273
+ }
274
+ return colors;
275
+ }
276
+ function validateTokenDocument(value) {
277
+ if (typeof value !== "object" || value === null) {
278
+ throw new Error('dsh-tokens file must be { "light": {...}, "dark": {...} } token tables');
279
+ }
280
+ const document = value;
281
+ for (const mode of ["light", "dark"]) {
282
+ const table = document[mode];
283
+ if (typeof table !== "object" || table === null) {
284
+ throw new Error(`dsh-tokens file: missing "${mode}" token table`);
285
+ }
286
+ for (const [name, color] of Object.entries(table)) {
287
+ if (!name.startsWith("--dsw-")) {
288
+ throw new Error(`dsh-tokens file: invalid token name ${JSON.stringify(name)}`);
289
+ }
290
+ if (typeof color !== "string" || !isSafeTokenValue(color)) {
291
+ throw new Error(`dsh-tokens file: token "${name}" must be a CSS value without braces, semicolons, or angle brackets, got ${JSON.stringify(color)}`);
292
+ }
293
+ }
294
+ }
295
+ return document;
296
+ }
297
+ function resolvePath(raw, label) {
298
+ const trimmed = raw.trim();
299
+ if (trimmed === "") throw new Error(`ui-experience: ${label} is empty; set it in the plugin configuration`);
300
+ const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : trimmed;
301
+ if (!isAbsolute(expanded)) {
302
+ throw new Error(`ui-experience: ${label} must be absolute or start with "~/": ${raw}`);
303
+ }
304
+ return expanded;
305
+ }
306
+ function effectivePaths(config) {
307
+ return { colors: config.colorsPath.get(), tokens: config.tokensPath.get() };
308
+ }
309
+ var paletteCache;
310
+ var PALETTE_MAX_BYTES = 4 * 1024 * 1024;
311
+ var readFileText = (path) => readFileAsync(path, "utf8");
312
+ var statFile = (path) => statAsync(path);
313
+ function resolveSources(config) {
314
+ const paths = effectivePaths(config);
315
+ try {
316
+ const tokensFile = resolvePath(paths.tokens, "tokensPath");
317
+ const colorsFile = resolvePath(paths.colors, "colorsPath");
318
+ return {
319
+ ok: true,
320
+ sources: {
321
+ paths,
322
+ tokensFile,
323
+ colorsFile,
324
+ hasTokens: existsSync(tokensFile),
325
+ hasColors: existsSync(colorsFile)
326
+ }
327
+ };
328
+ } catch (error) {
329
+ return { ok: false, paths, status: "error", message: error.message };
330
+ }
331
+ }
332
+ function composePalette(sources, mapped, tokens) {
333
+ if (mapped === void 0 && tokens === void 0) {
334
+ throw new Error(`no palette file at tokensPath (${sources.tokensFile}) or colorsPath (${sources.colorsFile})`);
335
+ }
336
+ if (mapped === void 0) return tokens;
337
+ if (tokens === void 0) return mapped;
338
+ return {
339
+ light: { ...mapped.light, ...tokens.light },
340
+ dark: { ...mapped.dark, ...tokens.dark }
341
+ };
342
+ }
343
+ function auditCoverage(composed, tokens, mapped) {
344
+ if (tokens === void 0) return;
345
+ const provided = Object.keys(tokens.dark).length;
346
+ const total = Object.keys(composed.dark).length;
347
+ if (mapped === void 0) {
348
+ logLine(`tokensPath has ${provided} tokens and no colorsPath base is present; every other token keeps the built-in palette value`);
349
+ return;
350
+ }
351
+ logLine(`palette composed from colorsPath mapping (${Object.keys(mapped.dark).length} tokens) + tokensPath overrides (${provided} tokens) = ${total} tokens`);
352
+ }
353
+ function parseMapped(text) {
354
+ const table = mapColors(validateColors(JSON.parse(text)));
355
+ return { light: table, dark: table };
356
+ }
357
+ function loadPartSync(path, exists, parse) {
358
+ if (!exists) return { key: "-", has: false };
359
+ const stat = statSync(path);
360
+ if (stat.size > PALETTE_MAX_BYTES) {
361
+ throw new Error(`${path} is ${stat.size} bytes, over the ${PALETTE_MAX_BYTES} byte limit`);
362
+ }
363
+ return { key: `${path}@${stat.mtimeMs}:${stat.size}`, has: true, modes: parse(readFileSync(path, "utf8")) };
364
+ }
365
+ function readPaletteSync(config) {
366
+ const resolved = resolveSources(config);
367
+ if (!resolved.ok) return resolved;
368
+ const { sources } = resolved;
369
+ if (!sources.hasTokens && !sources.hasColors) {
370
+ return {
371
+ ok: false,
372
+ paths: sources.paths,
373
+ status: "missing",
374
+ message: `no palette file at tokensPath (${sources.tokensFile}) or colorsPath (${sources.colorsFile})`
375
+ };
376
+ }
377
+ try {
378
+ const mapped = loadPartSync(sources.colorsFile, sources.hasColors, parseMapped);
379
+ const tokens = loadPartSync(sources.tokensFile, sources.hasTokens, (text) => validateTokenDocument(JSON.parse(text)));
380
+ const version = `${mapped.key}|${tokens.key}`;
381
+ if (paletteCache !== void 0 && paletteCache.version === version) {
382
+ return { ok: true, paths: sources.paths, palette: paletteCache };
383
+ }
384
+ const modes = composePalette(sources, mapped.modes, tokens.modes);
385
+ auditCoverage(modes, tokens.modes, mapped.modes);
386
+ paletteCache = { source: sources.paths.tokens, version, tokens: modes };
387
+ return { ok: true, paths: sources.paths, palette: paletteCache };
388
+ } catch (error) {
389
+ return {
390
+ ok: false,
391
+ paths: sources.paths,
392
+ status: "error",
393
+ message: `cannot read palette: ${error.message}`
394
+ };
395
+ }
396
+ }
397
+ async function loadPart(path, exists, parse) {
398
+ if (!exists) return { key: "-", has: false };
399
+ const stat = await statFile(path);
400
+ if (stat.size > PALETTE_MAX_BYTES) {
401
+ throw new Error(`${path} is ${stat.size} bytes, over the ${PALETTE_MAX_BYTES} byte limit`);
402
+ }
403
+ return { key: `${path}@${stat.mtimeMs}:${stat.size}`, has: true, modes: parse(await readFileText(path)) };
404
+ }
405
+ async function readPalette(config) {
406
+ const resolved = resolveSources(config);
407
+ if (!resolved.ok) return resolved;
408
+ const { sources } = resolved;
409
+ if (!sources.hasTokens && !sources.hasColors) {
410
+ return {
411
+ ok: false,
412
+ paths: sources.paths,
413
+ status: "missing",
414
+ message: `no palette file at tokensPath (${sources.tokensFile}) or colorsPath (${sources.colorsFile})`
415
+ };
416
+ }
417
+ try {
418
+ const mapped = await loadPart(sources.colorsFile, sources.hasColors, parseMapped);
419
+ const tokens = await loadPart(sources.tokensFile, sources.hasTokens, (text) => validateTokenDocument(JSON.parse(text)));
420
+ const version = `${mapped.key}|${tokens.key}`;
421
+ if (paletteCache !== void 0 && paletteCache.version === version) {
422
+ return { ok: true, paths: sources.paths, palette: paletteCache };
423
+ }
424
+ const modes = composePalette(sources, mapped.modes, tokens.modes);
425
+ auditCoverage(modes, tokens.modes, mapped.modes);
426
+ paletteCache = { source: sources.paths.tokens, version, tokens: modes };
427
+ return { ok: true, paths: sources.paths, palette: paletteCache };
428
+ } catch (error) {
429
+ return {
430
+ ok: false,
431
+ paths: sources.paths,
432
+ status: "error",
433
+ message: `cannot read palette: ${error.message}`
434
+ };
435
+ }
436
+ }
437
+ function bootColor(value, mode) {
438
+ if (value !== void 0 && HEX_COLOR.test(value)) return value;
439
+ const fallback = mode === "light" ? "#fff" : "#151517";
440
+ logLine(`boot paint: ${mode} base color ${JSON.stringify(value)} is not a hex color; using ${fallback}`);
441
+ return fallback;
442
+ }
443
+ function bootRows(palette) {
444
+ const light = bootColor(palette.tokens.light["--dsw-alias-bg-base"], "light");
445
+ const dark = bootColor(palette.tokens.dark["--dsw-alias-bg-base"], "dark");
446
+ const scope = `html[${BOOT_ATTRIBUTE}]`;
447
+ const text = `${scope} body[data-ds-dark-theme]{background-color:${dark};--dsh-boot-bg:${dark}}${scope} body:not([data-ds-dark-theme]){background-color:${light};--dsh-boot-bg:${light}}`;
448
+ return [
449
+ { kind: "style", text },
450
+ {
451
+ kind: "script",
452
+ placement: "body",
453
+ text: `document.documentElement.toggleAttribute(${JSON.stringify(BOOT_ATTRIBUTE)}, true)`
454
+ }
455
+ ];
456
+ }
457
+ var UiExperienceService = class extends TypertRemoteService {
458
+ constructor(ctx, config, entry, editor) {
459
+ super(ctx, REMOTE_SERVICE);
460
+ this.config = config;
461
+ this.entry = entry;
462
+ this.editor = editor;
463
+ }
464
+ config;
465
+ entry;
466
+ editor;
467
+ /**
468
+ * Remote poll endpoint: report the palette when it changed since the caller's
469
+ * known version, and always report the live switch, border width, and paths.
470
+ * @param args - the caller's last seen version.
471
+ * @returns the poll payload.
472
+ */
473
+ async poll(args) {
474
+ const read = await readPalette(this.config);
475
+ const active = this.config.active.get();
476
+ const borderWidth = this.config.borderWidth.get();
477
+ if (!read.ok) {
478
+ return { status: read.status, message: read.message, active, borderWidth, paths: read.paths };
479
+ }
480
+ const { palette, paths } = read;
481
+ if (args?.known === palette.version) {
482
+ return { status: "ok", changed: false, version: palette.version, source: palette.source, active, borderWidth, paths };
483
+ }
484
+ return {
485
+ status: "ok",
486
+ changed: true,
487
+ version: palette.version,
488
+ tokens: palette.tokens,
489
+ source: palette.source,
490
+ active,
491
+ borderWidth,
492
+ paths
493
+ };
494
+ }
495
+ /**
496
+ * Remote endpoint persisting the Matugen mode switch.
497
+ * @param args - the requested switch state.
498
+ * @returns the persisted state.
499
+ */
500
+ async setActive(args) {
501
+ const next = args?.active;
502
+ if (typeof next !== "boolean") throw new Error("ui-experience: setActive requires a boolean `active`");
503
+ await this.persist((current) => ({ ...current, active: next }));
504
+ logLine(`setActive(${next}) persisted`);
505
+ return { active: next };
506
+ }
507
+ /**
508
+ * Remote endpoint persisting the palette paths.
509
+ * @param args - the paths to write; an omitted field keeps its value.
510
+ * @returns the paths now in effect.
511
+ */
512
+ async setPaths(args) {
513
+ const colors = typeof args?.colorsPath === "string" ? args.colorsPath.trim() : void 0;
514
+ const tokens = typeof args?.tokensPath === "string" ? args.tokensPath.trim() : void 0;
515
+ if (colors === void 0 && tokens === void 0) {
516
+ throw new Error("ui-experience: setPaths requires `colorsPath` or `tokensPath`");
517
+ }
518
+ if (colors !== void 0) resolvePath(colors, "colorsPath");
519
+ if (tokens !== void 0) resolvePath(tokens, "tokensPath");
520
+ await this.persist((current) => ({
521
+ ...current,
522
+ ...colors === void 0 ? {} : { colorsPath: colors },
523
+ ...tokens === void 0 ? {} : { tokensPath: tokens }
524
+ }));
525
+ const paths = {
526
+ colors: colors ?? this.config.colorsPath.get(),
527
+ tokens: tokens ?? this.config.tokensPath.get()
528
+ };
529
+ logLine(`setPaths: colors=${paths.colors} tokens=${paths.tokens}`);
530
+ return { paths };
531
+ }
532
+ /**
533
+ * Write one configuration change through the config editor, logging the
534
+ * failure before it reaches the caller.
535
+ * @param change - derives the next raw config from the current one.
536
+ */
537
+ async persist(change) {
538
+ const { entry, editor } = this;
539
+ if (entry === void 0 || editor === void 0) {
540
+ throw new Error("ui-experience: the plugin entry or the config editor is unavailable; configuration cannot be saved in this profile");
541
+ }
542
+ try {
543
+ await editor.edit(entry, change);
544
+ } catch (error) {
545
+ logLine(`config edit FAILED: ${error.stack ?? error.message}`);
546
+ throw error;
547
+ }
548
+ }
549
+ };
550
+ Object.defineProperty(UiExperienceService.prototype, REMOTE_METHOD_DESCRIPTOR, {
551
+ configurable: true,
552
+ value: Object.freeze({
553
+ version: 1,
554
+ methods: Object.freeze([
555
+ Object.freeze({ method: "poll", invocation: Object.freeze({ kind: "direct" }) }),
556
+ Object.freeze({ method: "setActive", invocation: Object.freeze({ kind: "direct" }) }),
557
+ Object.freeze({ method: "setPaths", invocation: Object.freeze({ kind: "direct" }) })
558
+ ])
559
+ })
560
+ });
561
+ var inject = ["configEditor"];
562
+ function apply(ctx, config) {
563
+ logLine(`apply: active=${String(config.active.get())} borderWidth=${config.borderWidth.get()} colorsPath=${config.colorsPath.get()} tokensPath=${config.tokensPath.get()}`);
564
+ try {
565
+ new UiExperienceService(ctx, config, ctx.fiber.entry, ctx.configEditor);
566
+ } catch (error) {
567
+ console.error(`[ui-experience] failed to register host service: ${error.message}`);
568
+ throw error;
569
+ }
570
+ ctx.on("webserver/index-inject", (table) => {
571
+ if (!config.active.get()) return;
572
+ const read = readPaletteSync(config);
573
+ if (!read.ok) {
574
+ logLine(`boot paint skipped: ${read.message}`);
575
+ return;
576
+ }
577
+ table.push(...bootRows(read.palette));
578
+ });
579
+ }
580
+ export {
581
+ Config,
582
+ apply,
583
+ inject
584
+ };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Interface experience",
4
+ "description": "Make the DSH interface follow the Material You palette matugen generates on the system. After enabling Settings → General → Matugen palette sync, the palette is layered over the current theme and follows the file whenever matugen regenerates it. Expand Palette file paths on the same row to rewrite the matugen colors file or the DSH token file path (both default under ~/.local/state/quickshell/user/generated/, the token file wins when present). Palette files are read locally and never uploaded; turning the switch off returns the native colors. Also unifies the interface border width (config option borderWidth, default 0.5px); that override applies only while the switch is on and narrows decorative borders such as markdown quote rules."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "界面体验",
4
+ "description": "让 DSH 界面跟随系统 matugen 生成的 Material You 配色:在「设置 → 通用设置 → Matugen 配色跟随」开启后,界面配色作为叠加层套在当前主题上,并随配色文件重新生成自动更新。可在同一行展开「配色文件路径」,改写 matugen 颜色文件或 DSH 令牌文件的路径(默认 ~/.local/state/quickshell/user/generated/ 下,令牌文件优先)。配色只在本地读取,不上传。关闭开关后回到原生配色。另统一界面边框宽度(配置项 borderWidth,默认 0.5px)。该覆盖仅在开关开启时生效,且会同时压窄正文引用块等装饰性边框。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@dsh-plugin/dsh-ui-experience",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-dev.43485427852",
4
+ "type": "module",
5
+ "description": "UI experience enhancements for DeepSeek Harness: follow the system matugen Material You palette as the interface theme.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dsh-plugins/dsh-ui-experience.git"
10
+ },
11
+ "homepage": "https://github.com/dsh-plugins/dsh-ui-experience",
12
+ "bugs": {
13
+ "url": "https://github.com/dsh-plugins/dsh-ui-experience/issues"
14
+ },
15
+ "main": "lib/index.js",
16
+ "exports": {
17
+ ".": "./lib/index.js",
18
+ "./client": "./lib/client.js",
19
+ "./locale/*.json": "./locale/*.json",
20
+ "./icon": "./icon.svg"
21
+ },
22
+ "files": [
23
+ "lib/index.js",
24
+ "lib/client.js",
25
+ "cordis.patch.yml",
26
+ "locale/*.json",
27
+ "icon.svg"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json && node build.mjs",
31
+ "typecheck": "tsc -p tsconfig.json"
32
+ },
33
+ "dependencies": {
34
+ "@deepseek-ai/dsh-typert-protocol": "^0.2.2-rc.2",
35
+ "@deepseek-ai/schemastery": "^3.19.0"
36
+ },
37
+ "dsh": {
38
+ "bundle": {
39
+ "patch": "./cordis.patch.yml"
40
+ },
41
+ "client": {
42
+ "platform": "web",
43
+ "immediately": true,
44
+ "inject": [
45
+ "@deepseek-ai/dsh-api-remotes",
46
+ "@deepseek-ai/dsh-client-ui-theme"
47
+ ]
48
+ }
49
+ },
50
+ "engines": {
51
+ "dsh": ">=0.2.0-0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^26.6.6",
55
+ "esbuild": "^0.28.2",
56
+ "typescript": "^5.9.3"
57
+ }
58
+ }