dsh-theme-studio 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release.
6
+ - 6 pre-built theme presets (Ocean, Forest, Sunset, Monochrome, Nord, Dracula).
7
+ - Custom accent color picker.
8
+ - Density, border radius, and font family controls.
9
+ - Custom CSS variable override textarea.
10
+ - Live preview area.
11
+ - Preferences persisted in localStorage.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # dsh-theme-studio
2
+
3
+ A [dsh](https://github.com/deepseek-ai/deepseek-harness) plugin that lets you customize the UI theme: pick pre-built color presets, set a custom accent color, adjust density, border radius, and font family, or write custom CSS variable overrides.
4
+
5
+ ## Features
6
+
7
+ - **6 pre-built presets**: Ocean, Forest, Sunset, Monochrome, Nord, Dracula
8
+ - **Custom accent color** picker with hex input
9
+ - **Density** control: Compact / Comfortable / Spacious
10
+ - **Border radius** control: Sharp / Rounded / Soft
11
+ - **Font family** selection: System / Monospace / Serif
12
+ - **Custom CSS** textarea for advanced `--property: value;` overrides
13
+ - **Live preview** with badges, cards, and buttons
14
+ - **Instant apply** — changes take effect immediately via CSS custom properties
15
+ - **Persistent** — preferences saved in `localStorage`, no server round-trips
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ dsh plugin add dsh-theme-studio
21
+ ```
22
+
23
+ Or from source:
24
+
25
+ ```bash
26
+ git clone https://github.com/hj01857655/dsh-theme-studio.git
27
+ cd dsh-theme-studio
28
+ npm install && npm run build
29
+ dsh plugin add link:.
30
+ ```
31
+
32
+ ## How it works
33
+
34
+ The plugin registers a `settings.section` slot (order 47) that renders a Theme Studio page in the dsh Settings panel. All theme state is client-side: preferences persist in `localStorage` and are applied by setting CSS custom properties on `document.documentElement.style`.
35
+
36
+ The overrides target dsh's `--dsw-alias-*` semantic token layer, so they cascade through every component that reads the design tokens — no component patching required.
37
+
38
+ ## License
39
+
40
+ MIT
@@ -0,0 +1,9 @@
1
+ bundle:
2
+ - id: theme-studio
3
+ target: web
4
+ patch:
5
+ settings.section:
6
+ - id: theme-studio
7
+ order: 47
8
+ label: Theme Studio
9
+ locale: theme-studio
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Host half of dsh-theme-studio.
3
+ *
4
+ * A purely client-side plugin — the host half exists only to satisfy the Cordis
5
+ * bundle contract (every plugin needs a host entry point with a cordis.patch.yml).
6
+ * No routes, no storage, no services: all theme state lives in the browser's
7
+ * localStorage and is applied via CSS custom properties on document.documentElement.
8
+ *
9
+ * @module index
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ export declare function apply(_ctx: Context): void;
package/lib/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Host half of dsh-theme-studio.
3
+ *
4
+ * A purely client-side plugin — the host half exists only to satisfy the Cordis
5
+ * bundle contract (every plugin needs a host entry point with a cordis.patch.yml).
6
+ * No routes, no storage, no services: all theme state lives in the browser's
7
+ * localStorage and is applied via CSS custom properties on document.documentElement.
8
+ *
9
+ * @module index
10
+ */
11
+ export function apply(_ctx) {
12
+ // Intentionally empty. The client half (src/client/index.tsx) registers the
13
+ // settings section; the host half has no work to do.
14
+ }
@@ -0,0 +1,947 @@
1
+ // src/client/locales.ts
2
+ var NS = "theme-studio";
3
+ var zh = {
4
+ nav: "\u4E3B\u9898\u5DE5\u4F5C\u5BA4",
5
+ title: "\u4E3B\u9898\u5DE5\u4F5C\u5BA4",
6
+ subtitle: "\u81EA\u5B9A\u4E49 dsh \u754C\u9762\u5916\u89C2",
7
+ presetSection: "\u9884\u8BBE\u4E3B\u9898",
8
+ presetHint: "\u9009\u62E9\u4E00\u4E2A\u9884\u8BBE\u914D\u8272\u65B9\u6848",
9
+ noPreset: "\u65E0\uFF08\u4F7F\u7528\u9ED8\u8BA4\uFF09",
10
+ accentSection: "\u5F3A\u8C03\u8272",
11
+ accentHint: "\u81EA\u5B9A\u4E49\u754C\u9762\u5F3A\u8C03\u8272\uFF0C\u8986\u76D6\u9884\u8BBE\u4E2D\u7684\u5F3A\u8C03\u8272",
12
+ accentPlaceholder: "#4B8BBE",
13
+ densitySection: "\u5BC6\u5EA6",
14
+ densityCompact: "\u7D27\u51D1",
15
+ densityComfortable: "\u8212\u9002",
16
+ densitySpacious: "\u5BBD\u677E",
17
+ radiusSection: "\u5706\u89D2",
18
+ radiusSharp: "\u9510\u5229",
19
+ radiusRounded: "\u5706\u89D2",
20
+ radiusSoft: "\u67D4\u548C",
21
+ fontSection: "\u5B57\u4F53",
22
+ fontSystem: "\u7CFB\u7EDF",
23
+ fontMono: "\u7B49\u5BBD",
24
+ fontSerif: "\u886C\u7EBF",
25
+ customCssSection: "\u81EA\u5B9A\u4E49 CSS",
26
+ customCssHint: "\u76F4\u63A5\u8986\u76D6 CSS \u81EA\u5B9A\u4E49\u5C5E\u6027\uFF0C\u6BCF\u884C\u4E00\u4E2A `--property: value;`",
27
+ customCssPlaceholder: "--accent: #ff0000;\n--border: 1px solid red;",
28
+ preview: "\u9884\u89C8",
29
+ apply: "\u5E94\u7528",
30
+ reset: "\u91CD\u7F6E\u4E3A\u9ED8\u8BA4",
31
+ resetConfirm: "\u786E\u5B9A\u8981\u91CD\u7F6E\u6240\u6709\u4E3B\u9898\u8BBE\u7F6E\u5417\uFF1F",
32
+ applied: "\u4E3B\u9898\u5DF2\u5E94\u7528",
33
+ resetted: "\u5DF2\u91CD\u7F6E\u4E3A\u9ED8\u8BA4\u4E3B\u9898",
34
+ card: "\u5361\u7247",
35
+ button: "\u6309\u94AE",
36
+ badge: "\u6807\u7B7E",
37
+ text: "\u8FD9\u662F\u4E00\u6BB5\u793A\u4F8B\u6587\u672C\uFF0C\u7528\u4E8E\u9884\u89C8\u4E3B\u9898\u6548\u679C\u3002"
38
+ };
39
+ var en = {
40
+ nav: "Theme Studio",
41
+ title: "Theme Studio",
42
+ subtitle: "Customize the dsh UI appearance",
43
+ presetSection: "Preset Themes",
44
+ presetHint: "Choose a pre-built color scheme",
45
+ noPreset: "None (use default)",
46
+ accentSection: "Accent Color",
47
+ accentHint: "Custom accent color, overrides the preset",
48
+ accentPlaceholder: "#4B8BBE",
49
+ densitySection: "Density",
50
+ densityCompact: "Compact",
51
+ densityComfortable: "Comfortable",
52
+ densitySpacious: "Spacious",
53
+ radiusSection: "Border Radius",
54
+ radiusSharp: "Sharp",
55
+ radiusRounded: "Rounded",
56
+ radiusSoft: "Soft",
57
+ fontSection: "Font Family",
58
+ fontSystem: "System",
59
+ fontMono: "Monospace",
60
+ fontSerif: "Serif",
61
+ customCssSection: "Custom CSS",
62
+ customCssHint: "Override CSS custom properties directly, one `--property: value;` per line",
63
+ customCssPlaceholder: "--accent: #ff0000;\n--border: 1px solid red;",
64
+ preview: "Preview",
65
+ apply: "Apply",
66
+ reset: "Reset to Default",
67
+ resetConfirm: "Reset all theme settings to default?",
68
+ applied: "Theme applied",
69
+ resetted: "Reset to default theme",
70
+ card: "Card",
71
+ button: "Button",
72
+ badge: "Badge",
73
+ text: "This is sample text to preview the theme."
74
+ };
75
+
76
+ // src/client/view.tsx
77
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
78
+
79
+ // src/types.ts
80
+ var DEFAULT_PREFERENCES = {
81
+ preset: null,
82
+ accentColor: null,
83
+ density: "comfortable",
84
+ radius: "rounded",
85
+ fontFamily: "system",
86
+ customCss: ""
87
+ };
88
+ var STORAGE_KEY = "dsh-theme-studio";
89
+
90
+ // src/themes.ts
91
+ var PRESETS = [
92
+ {
93
+ id: "ocean",
94
+ name: "Ocean",
95
+ description: "Deep blue accent with cool neutrals",
96
+ tokens: {
97
+ "--accent": "#2563eb",
98
+ "--accent-hover": "#1d4ed8",
99
+ "--dsw-alias-state-business-primary": "#2563eb",
100
+ "--dsw-alias-state-business-secondary": "#dbeafe",
101
+ "--dsw-alias-interactive-bg-hover": "rgba(37,99,235,0.08)"
102
+ }
103
+ },
104
+ {
105
+ id: "forest",
106
+ name: "Forest",
107
+ description: "Green accent with earthy neutrals",
108
+ tokens: {
109
+ "--accent": "#16a34a",
110
+ "--accent-hover": "#15803d",
111
+ "--dsw-alias-state-business-primary": "#16a34a",
112
+ "--dsw-alias-state-business-secondary": "#dcfce7",
113
+ "--dsw-alias-interactive-bg-hover": "rgba(22,163,74,0.08)"
114
+ }
115
+ },
116
+ {
117
+ id: "sunset",
118
+ name: "Sunset",
119
+ description: "Warm orange accent with amber highlights",
120
+ tokens: {
121
+ "--accent": "#ea580c",
122
+ "--accent-hover": "#c2410c",
123
+ "--dsw-alias-state-business-primary": "#ea580c",
124
+ "--dsw-alias-state-business-secondary": "#fed7aa",
125
+ "--dsw-alias-interactive-bg-hover": "rgba(234,88,12,0.08)"
126
+ }
127
+ },
128
+ {
129
+ id: "monochrome",
130
+ name: "Monochrome",
131
+ description: "Grayscale only \u2014 no color accent",
132
+ tokens: {
133
+ "--accent": "#404040",
134
+ "--accent-hover": "#262626",
135
+ "--dsw-alias-state-business-primary": "#404040",
136
+ "--dsw-alias-state-business-secondary": "#e5e5e5",
137
+ "--dsw-alias-interactive-bg-hover": "rgba(64,64,64,0.08)"
138
+ }
139
+ },
140
+ {
141
+ id: "nord",
142
+ name: "Nord",
143
+ description: "Frost blue inspired by the Nord palette",
144
+ tokens: {
145
+ "--accent": "#5e81ac",
146
+ "--accent-hover": "#4c6f96",
147
+ "--dsw-alias-state-business-primary": "#5e81ac",
148
+ "--dsw-alias-state-business-secondary": "#e5ebf1",
149
+ "--dsw-alias-interactive-bg-hover": "rgba(94,129,172,0.08)"
150
+ }
151
+ },
152
+ {
153
+ id: "dracula",
154
+ name: "Dracula",
155
+ description: "Purple accent with dark mode flair",
156
+ tokens: {
157
+ "--accent": "#bd93f9",
158
+ "--accent-hover": "#a678e8",
159
+ "--dsw-alias-state-business-primary": "#bd93f9",
160
+ "--dsw-alias-state-business-secondary": "#3a3a5c",
161
+ "--dsw-alias-interactive-bg-hover": "rgba(189,147,249,0.12)"
162
+ }
163
+ }
164
+ ];
165
+ var DENSITY_TOKENS = {
166
+ compact: {
167
+ "--dsh-content-font-size": "13px",
168
+ "--dsh-spacing-unit": "4px"
169
+ },
170
+ comfortable: {
171
+ "--dsh-content-font-size": "14px",
172
+ "--dsh-spacing-unit": "6px"
173
+ },
174
+ spacious: {
175
+ "--dsh-content-font-size": "15px",
176
+ "--dsh-spacing-unit": "8px"
177
+ }
178
+ };
179
+ var RADIUS_TOKENS = {
180
+ sharp: {
181
+ "--dsh-radius-small": "2px",
182
+ "--dsh-radius-medium": "4px",
183
+ "--dsh-radius-large": "6px"
184
+ },
185
+ rounded: {
186
+ "--dsh-radius-small": "6px",
187
+ "--dsh-radius-medium": "10px",
188
+ "--dsh-radius-large": "14px"
189
+ },
190
+ soft: {
191
+ "--dsh-radius-small": "10px",
192
+ "--dsh-radius-medium": "16px",
193
+ "--dsh-radius-large": "22px"
194
+ }
195
+ };
196
+ var FONT_TOKENS = {
197
+ system: { "--dsh-font-family": "system-ui, -apple-system, sans-serif" },
198
+ mono: { "--dsh-font-family": "'JetBrains Mono', 'Fira Code', monospace" },
199
+ serif: { "--dsh-font-family": "'Georgia', 'Times New Roman', serif" }
200
+ };
201
+ function getPreset(id) {
202
+ return PRESETS.find((p) => p.id === id);
203
+ }
204
+
205
+ // src/client/ui.tsx
206
+ import {
207
+ createContext,
208
+ useCallback,
209
+ useContext,
210
+ useEffect,
211
+ useMemo,
212
+ useState
213
+ } from "react";
214
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
215
+ var T = {
216
+ accent: "var(--accent, #4B8BBE)",
217
+ accentHover: "var(--accent-hover, #3a6f9e)",
218
+ error: "var(--error, #e53935)",
219
+ success: "var(--success, #2e7d32)",
220
+ warning: "var(--warning, #ed6c02)",
221
+ bg: "var(--bg-secondary, rgba(128,128,128,0.04))",
222
+ bgHover: "var(--bg-tertiary, rgba(128,128,128,0.08))",
223
+ border: "var(--border, rgba(128,128,128,0.2))",
224
+ text: "var(--text-primary, inherit)",
225
+ muted: "var(--text-secondary, rgba(128,128,128,0.65))",
226
+ surface: "var(--bg-primary, #fff)"
227
+ };
228
+ var injected = false;
229
+ function ensureStyles() {
230
+ if (injected || typeof document === "undefined") return;
231
+ injected = true;
232
+ const el = document.createElement("style");
233
+ el.textContent = `
234
+ @keyframes dsh-fade-in{from{opacity:0}to{opacity:1}}
235
+ @keyframes dsh-slide-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
236
+ @keyframes dsh-spin{to{transform:rotate(360deg)}}
237
+ .dsh-modal-overlay{animation:dsh-fade-in .15s ease}
238
+ .dsh-modal-body{animation:dsh-slide-up .2s ease}
239
+ .dsh-toast{animation:dsh-slide-up .2s ease}
240
+ .dsh-spin{animation:dsh-spin .6s linear infinite}
241
+ `;
242
+ document.head.appendChild(el);
243
+ }
244
+ function Modal({
245
+ title,
246
+ onClose,
247
+ children,
248
+ footer,
249
+ width = 520
250
+ }) {
251
+ useEffect(() => {
252
+ ensureStyles();
253
+ const handler = (e) => {
254
+ if (e.key === "Escape") onClose();
255
+ };
256
+ document.addEventListener("keydown", handler);
257
+ return () => document.removeEventListener("keydown", handler);
258
+ }, [onClose]);
259
+ return /* @__PURE__ */ jsx(
260
+ "div",
261
+ {
262
+ className: "dsh-modal-overlay",
263
+ style: {
264
+ position: "fixed",
265
+ inset: 0,
266
+ zIndex: 1e3,
267
+ display: "flex",
268
+ alignItems: "center",
269
+ justifyContent: "center",
270
+ background: "rgba(0,0,0,0.45)",
271
+ backdropFilter: "blur(3px)",
272
+ WebkitBackdropFilter: "blur(3px)"
273
+ },
274
+ onClick: onClose,
275
+ children: /* @__PURE__ */ jsxs(
276
+ "div",
277
+ {
278
+ className: "dsh-modal-body",
279
+ style: {
280
+ background: T.surface,
281
+ borderRadius: 14,
282
+ boxShadow: "0 12px 40px rgba(0,0,0,0.25)",
283
+ width: `min(92vw, ${width}px)`,
284
+ maxHeight: "85vh",
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ overflow: "hidden"
288
+ },
289
+ onClick: (e) => e.stopPropagation(),
290
+ children: [
291
+ /* @__PURE__ */ jsxs(
292
+ "div",
293
+ {
294
+ style: {
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "space-between",
298
+ padding: "16px 22px",
299
+ borderBottom: `1px solid ${T.border}`
300
+ },
301
+ children: [
302
+ /* @__PURE__ */ jsx("h3", { style: { margin: 0, fontSize: 15, fontWeight: 600 }, children: title }),
303
+ /* @__PURE__ */ jsx(
304
+ "button",
305
+ {
306
+ type: "button",
307
+ onClick: onClose,
308
+ style: {
309
+ background: "none",
310
+ border: "none",
311
+ cursor: "pointer",
312
+ fontSize: 20,
313
+ color: T.muted,
314
+ padding: "0 4px",
315
+ lineHeight: 1,
316
+ borderRadius: 4
317
+ },
318
+ "aria-label": "Close",
319
+ children: "\xD7"
320
+ }
321
+ )
322
+ ]
323
+ }
324
+ ),
325
+ /* @__PURE__ */ jsx("div", { style: { padding: 22, overflow: "auto", flex: 1 }, children }),
326
+ footer !== void 0 && /* @__PURE__ */ jsx(
327
+ "div",
328
+ {
329
+ style: {
330
+ padding: "14px 22px",
331
+ borderTop: `1px solid ${T.border}`,
332
+ display: "flex",
333
+ justifyContent: "flex-end",
334
+ gap: 10,
335
+ background: T.bg
336
+ },
337
+ children: footer
338
+ }
339
+ )
340
+ ]
341
+ }
342
+ )
343
+ }
344
+ );
345
+ }
346
+ function ConfirmDialog({
347
+ title,
348
+ message,
349
+ confirmLabel,
350
+ cancelLabel = "Cancel",
351
+ onConfirm,
352
+ onClose,
353
+ danger
354
+ }) {
355
+ return /* @__PURE__ */ jsx(
356
+ Modal,
357
+ {
358
+ title,
359
+ onClose,
360
+ width: 420,
361
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
362
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onClose, children: cancelLabel }),
363
+ /* @__PURE__ */ jsx(
364
+ Button,
365
+ {
366
+ variant: danger === true ? "danger" : "primary",
367
+ onClick: () => {
368
+ onConfirm();
369
+ onClose();
370
+ },
371
+ children: confirmLabel
372
+ }
373
+ )
374
+ ] }),
375
+ children: /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: 14, lineHeight: 1.6 }, children: message })
376
+ }
377
+ );
378
+ }
379
+ function Button({
380
+ variant = "secondary",
381
+ size = "md",
382
+ children,
383
+ style,
384
+ ...rest
385
+ }) {
386
+ const base = {
387
+ cursor: "pointer",
388
+ borderRadius: 7,
389
+ fontWeight: 500,
390
+ transition: "all 0.15s ease",
391
+ border: "none",
392
+ display: "inline-flex",
393
+ alignItems: "center",
394
+ gap: 4,
395
+ fontSize: size === "sm" ? 12 : 13,
396
+ padding: size === "sm" ? "5px 12px" : "8px 18px",
397
+ fontFamily: "inherit"
398
+ };
399
+ const variants = {
400
+ primary: { background: T.accent, color: "#fff" },
401
+ secondary: {
402
+ background: T.bg,
403
+ color: T.text,
404
+ border: `1px solid ${T.border}`
405
+ },
406
+ danger: {
407
+ background: "transparent",
408
+ color: T.error,
409
+ border: `1px solid ${T.error}`
410
+ },
411
+ ghost: { background: "transparent", color: T.muted, border: "none" }
412
+ };
413
+ return /* @__PURE__ */ jsx("button", { type: "button", style: { ...base, ...variants[variant], ...style }, ...rest, children });
414
+ }
415
+ function Card({
416
+ title,
417
+ icon,
418
+ children,
419
+ actions,
420
+ style,
421
+ padding = 14
422
+ }) {
423
+ return /* @__PURE__ */ jsxs(
424
+ "div",
425
+ {
426
+ style: {
427
+ borderRadius: 10,
428
+ border: `1px solid ${T.border}`,
429
+ background: T.bg,
430
+ overflow: "hidden",
431
+ ...style
432
+ },
433
+ children: [
434
+ (title !== void 0 || actions !== void 0) && /* @__PURE__ */ jsxs(
435
+ "div",
436
+ {
437
+ style: {
438
+ display: "flex",
439
+ alignItems: "center",
440
+ justifyContent: "space-between",
441
+ padding: "12px 16px",
442
+ borderBottom: title !== void 0 ? `1px solid ${T.border}` : "none"
443
+ },
444
+ children: [
445
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 13, fontWeight: 600, display: "flex", alignItems: "center", gap: 6 }, children: [
446
+ icon !== void 0 && /* @__PURE__ */ jsx("span", { children: icon }),
447
+ title
448
+ ] }),
449
+ actions
450
+ ]
451
+ }
452
+ ),
453
+ /* @__PURE__ */ jsx("div", { style: { padding }, children })
454
+ ]
455
+ }
456
+ );
457
+ }
458
+ function Badge({
459
+ children,
460
+ color = "default"
461
+ }) {
462
+ const colors = {
463
+ default: { bg: "rgba(128,128,128,0.12)", fg: T.muted },
464
+ success: { bg: "rgba(46,125,50,0.12)", fg: T.success },
465
+ error: { bg: "rgba(229,57,53,0.12)", fg: T.error },
466
+ warning: { bg: "rgba(245,124,0,0.12)", fg: T.warning },
467
+ info: { bg: "rgba(75,139,190,0.12)", fg: T.accent }
468
+ };
469
+ const c = colors[color];
470
+ return /* @__PURE__ */ jsx(
471
+ "span",
472
+ {
473
+ style: {
474
+ display: "inline-block",
475
+ padding: "2px 9px",
476
+ borderRadius: 10,
477
+ fontSize: 11,
478
+ fontWeight: 600,
479
+ background: c.bg,
480
+ color: c.fg
481
+ },
482
+ children
483
+ }
484
+ );
485
+ }
486
+ function Input({
487
+ style,
488
+ ...rest
489
+ }) {
490
+ return /* @__PURE__ */ jsx(
491
+ "input",
492
+ {
493
+ ...rest,
494
+ style: {
495
+ fontSize: 13,
496
+ padding: "8px 12px",
497
+ borderRadius: 7,
498
+ border: `1px solid ${T.border}`,
499
+ background: T.surface,
500
+ color: T.text,
501
+ width: "100%",
502
+ boxSizing: "border-box",
503
+ fontFamily: "inherit",
504
+ outline: "none",
505
+ transition: "border-color 0.15s",
506
+ ...style
507
+ }
508
+ }
509
+ );
510
+ }
511
+ function Textarea({
512
+ style,
513
+ ...rest
514
+ }) {
515
+ return /* @__PURE__ */ jsx(
516
+ "textarea",
517
+ {
518
+ ...rest,
519
+ style: {
520
+ fontSize: 13,
521
+ padding: "8px 12px",
522
+ borderRadius: 7,
523
+ border: `1px solid ${T.border}`,
524
+ background: T.surface,
525
+ color: T.text,
526
+ width: "100%",
527
+ boxSizing: "border-box",
528
+ fontFamily: "inherit",
529
+ outline: "none",
530
+ resize: "vertical",
531
+ lineHeight: 1.5,
532
+ ...style
533
+ }
534
+ }
535
+ );
536
+ }
537
+ var ToastContext = createContext(
538
+ () => {
539
+ }
540
+ );
541
+ function ToastProvider({ children }) {
542
+ const [toasts, setToasts] = useState([]);
543
+ const show = useCallback((type, message) => {
544
+ const id = Date.now() + Math.random();
545
+ setToasts((t) => [...t, { id, type, message }]);
546
+ setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3500);
547
+ }, []);
548
+ return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: show, children: [
549
+ children,
550
+ toasts.length > 0 && /* @__PURE__ */ jsx(
551
+ "div",
552
+ {
553
+ style: {
554
+ position: "fixed",
555
+ bottom: 24,
556
+ right: 24,
557
+ zIndex: 2e3,
558
+ display: "flex",
559
+ flexDirection: "column",
560
+ gap: 8
561
+ },
562
+ children: toasts.map((t) => /* @__PURE__ */ jsx(
563
+ "div",
564
+ {
565
+ className: "dsh-toast",
566
+ style: {
567
+ padding: "12px 18px",
568
+ borderRadius: 8,
569
+ fontSize: 13,
570
+ fontWeight: 500,
571
+ boxShadow: "0 6px 20px rgba(0,0,0,0.2)",
572
+ color: "#fff",
573
+ background: t.type === "success" ? T.success : t.type === "error" ? T.error : T.accent,
574
+ maxWidth: 360
575
+ },
576
+ children: t.message
577
+ },
578
+ t.id
579
+ ))
580
+ }
581
+ )
582
+ ] });
583
+ }
584
+ function useToast() {
585
+ return useContext(ToastContext);
586
+ }
587
+ function SectionTitle({
588
+ icon,
589
+ children,
590
+ actions
591
+ }) {
592
+ return /* @__PURE__ */ jsxs(
593
+ "div",
594
+ {
595
+ style: {
596
+ display: "flex",
597
+ alignItems: "center",
598
+ justifyContent: "space-between",
599
+ marginTop: 6,
600
+ marginBottom: 8
601
+ },
602
+ children: [
603
+ /* @__PURE__ */ jsxs(
604
+ "span",
605
+ {
606
+ style: {
607
+ fontSize: 14,
608
+ fontWeight: 600,
609
+ display: "flex",
610
+ alignItems: "center",
611
+ gap: 6
612
+ },
613
+ children: [
614
+ icon !== void 0 && /* @__PURE__ */ jsx("span", { children: icon }),
615
+ children
616
+ ]
617
+ }
618
+ ),
619
+ actions
620
+ ]
621
+ }
622
+ );
623
+ }
624
+ var tableStyles = {
625
+ table: {
626
+ borderCollapse: "collapse",
627
+ width: "100%"
628
+ },
629
+ th: {
630
+ textAlign: "left",
631
+ padding: "8px 12px 8px 0",
632
+ fontWeight: 600,
633
+ fontSize: 12,
634
+ color: T.muted,
635
+ borderBottom: `1px solid ${T.border}`
636
+ },
637
+ td: {
638
+ padding: "8px 12px 8px 0",
639
+ fontSize: 13,
640
+ borderBottom: "1px solid rgba(128,128,128,0.08)"
641
+ },
642
+ clickRow: { cursor: "pointer" }
643
+ };
644
+ function Field({
645
+ label,
646
+ children,
647
+ hint
648
+ }) {
649
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
650
+ /* @__PURE__ */ jsx("label", { style: { fontSize: 12, fontWeight: 600, color: T.text }, children: label }),
651
+ children,
652
+ hint !== void 0 && /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: T.muted }, children: hint })
653
+ ] });
654
+ }
655
+
656
+ // src/client/view.tsx
657
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
658
+ function loadPreferences() {
659
+ if (typeof localStorage === "undefined") return { ...DEFAULT_PREFERENCES };
660
+ try {
661
+ const raw = localStorage.getItem(STORAGE_KEY);
662
+ if (raw === null) return { ...DEFAULT_PREFERENCES };
663
+ return { ...DEFAULT_PREFERENCES, ...JSON.parse(raw) };
664
+ } catch {
665
+ return { ...DEFAULT_PREFERENCES };
666
+ }
667
+ }
668
+ function savePreferences(prefs) {
669
+ if (typeof localStorage === "undefined") return;
670
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
671
+ }
672
+ function applyTheme(prefs) {
673
+ if (typeof document === "undefined") return;
674
+ const root = document.documentElement;
675
+ const allTokens = {};
676
+ if (prefs.preset !== null) {
677
+ const preset = getPreset(prefs.preset);
678
+ if (preset !== void 0) Object.assign(allTokens, preset.tokens);
679
+ }
680
+ if (prefs.accentColor !== null && prefs.accentColor.trim() !== "") {
681
+ allTokens["--accent"] = prefs.accentColor;
682
+ }
683
+ Object.assign(allTokens, DENSITY_TOKENS[prefs.density] ?? {});
684
+ Object.assign(allTokens, RADIUS_TOKENS[prefs.radius] ?? {});
685
+ Object.assign(allTokens, FONT_TOKENS[prefs.fontFamily] ?? {});
686
+ for (const [prop, value] of Object.entries(allTokens)) {
687
+ root.style.setProperty(prop, value);
688
+ }
689
+ if (prefs.customCss.trim() !== "") {
690
+ for (const line of prefs.customCss.split("\n")) {
691
+ const match = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
692
+ if (match !== null) {
693
+ root.style.setProperty(match[1], match[2]);
694
+ }
695
+ }
696
+ }
697
+ }
698
+ function clearTheme() {
699
+ if (typeof document === "undefined") return;
700
+ const root = document.documentElement;
701
+ const known = /* @__PURE__ */ new Set([
702
+ "--accent",
703
+ "--accent-hover",
704
+ "--dsh-content-font-size",
705
+ "--dsh-spacing-unit",
706
+ "--dsh-radius-small",
707
+ "--dsh-radius-medium",
708
+ "--dsh-radius-large",
709
+ "--dsh-font-family",
710
+ "--dsw-alias-state-business-primary",
711
+ "--dsw-alias-state-business-secondary",
712
+ "--dsw-alias-interactive-bg-hover"
713
+ ]);
714
+ for (const prop of known) root.style.removeProperty(prop);
715
+ const prefs = loadPreferences();
716
+ if (prefs.customCss.trim() !== "") {
717
+ for (const line of prefs.customCss.split("\n")) {
718
+ const match = line.match(/^\s*(--[\w-]+)\s*:/);
719
+ if (match !== null) root.style.removeProperty(match[1]);
720
+ }
721
+ }
722
+ }
723
+ function PresetCard({ preset, selected, onClick }) {
724
+ const accent = preset.tokens["--accent"] ?? "#4B8BBE";
725
+ return /* @__PURE__ */ jsxs2(
726
+ "div",
727
+ {
728
+ onClick,
729
+ style: {
730
+ cursor: "pointer",
731
+ borderRadius: 10,
732
+ padding: 12,
733
+ border: selected ? "2px solid var(--accent, #4B8BBE)" : "1px solid var(--border, rgba(128,128,128,0.2))",
734
+ background: selected ? "rgba(75,139,190,0.06)" : "var(--bg-secondary, rgba(128,128,128,0.04))",
735
+ transition: "all 0.15s ease"
736
+ },
737
+ children: [
738
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }, children: [
739
+ /* @__PURE__ */ jsx2("div", { style: { width: 16, height: 16, borderRadius: "50%", background: accent, border: "1px solid rgba(128,128,128,0.2)" } }),
740
+ /* @__PURE__ */ jsx2("strong", { style: { fontSize: 13 }, children: preset.name }),
741
+ selected && /* @__PURE__ */ jsx2(Badge, { color: "info", children: "\u2713" })
742
+ ] }),
743
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.6 }, children: preset.description })
744
+ ]
745
+ }
746
+ );
747
+ }
748
+ function Segmented({ value, options, onChange }) {
749
+ return /* @__PURE__ */ jsx2("div", { style: { display: "inline-flex", borderRadius: 8, overflow: "hidden", border: "1px solid var(--border, rgba(128,128,128,0.2))" }, children: options.map((opt) => /* @__PURE__ */ jsx2(
750
+ "button",
751
+ {
752
+ type: "button",
753
+ onClick: () => onChange(opt.value),
754
+ style: {
755
+ cursor: "pointer",
756
+ border: "none",
757
+ padding: "7px 16px",
758
+ fontSize: 13,
759
+ fontWeight: 500,
760
+ fontFamily: "inherit",
761
+ background: value === opt.value ? "var(--accent, #4B8BBE)" : "transparent",
762
+ color: value === opt.value ? "#fff" : "var(--text-primary, inherit)",
763
+ transition: "all 0.15s ease"
764
+ },
765
+ children: opt.label
766
+ },
767
+ opt.value
768
+ )) });
769
+ }
770
+ function PreviewArea({ t }) {
771
+ return /* @__PURE__ */ jsx2(Card, { title: t("preview"), icon: "\u{1F441}", children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: [
772
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
773
+ /* @__PURE__ */ jsx2(Badge, { color: "info", children: "Info" }),
774
+ /* @__PURE__ */ jsx2(Badge, { color: "success", children: "Success" }),
775
+ /* @__PURE__ */ jsx2(Badge, { color: "warning", children: "Warning" }),
776
+ /* @__PURE__ */ jsx2(Badge, { color: "error", children: "Error" })
777
+ ] }),
778
+ /* @__PURE__ */ jsxs2(Card, { children: [
779
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 13, marginBottom: 8 }, children: t("text") }),
780
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8 }, children: [
781
+ /* @__PURE__ */ jsx2(Button, { variant: "primary", size: "sm", children: t("button") }),
782
+ /* @__PURE__ */ jsx2(Button, { variant: "secondary", size: "sm", children: "Secondary" }),
783
+ /* @__PURE__ */ jsx2(Button, { variant: "danger", size: "sm", children: "Danger" })
784
+ ] })
785
+ ] })
786
+ ] }) });
787
+ }
788
+ function ThemePanelInner({ t }) {
789
+ const toast = useToast();
790
+ const [prefs, setPrefs] = useState2(loadPreferences);
791
+ const [confirmReset, setConfirmReset] = useState2(false);
792
+ useEffect2(() => {
793
+ applyTheme(prefs);
794
+ savePreferences(prefs);
795
+ }, [prefs]);
796
+ const update = useCallback2((key, value) => {
797
+ setPrefs((p) => ({ ...p, [key]: value }));
798
+ }, []);
799
+ const handleReset = useCallback2(() => {
800
+ clearTheme();
801
+ setPrefs({ ...DEFAULT_PREFERENCES });
802
+ toast("success", t("resetted"));
803
+ }, [t, toast]);
804
+ return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: 16, maxWidth: 820 }, children: [
805
+ /* @__PURE__ */ jsxs2("header", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
806
+ /* @__PURE__ */ jsxs2("strong", { style: { fontSize: 15 }, children: [
807
+ "\u{1F3A8} ",
808
+ t("title")
809
+ ] }),
810
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, opacity: 0.6 }, children: t("subtitle") }),
811
+ /* @__PURE__ */ jsx2("span", { style: { flex: 1 } }),
812
+ /* @__PURE__ */ jsxs2(Button, { variant: "danger", size: "sm", onClick: () => setConfirmReset(true), children: [
813
+ "\u21BA ",
814
+ t("reset")
815
+ ] })
816
+ ] }),
817
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F3AD}", children: t("presetSection") }),
818
+ /* @__PURE__ */ jsxs2("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: 10 }, children: [
819
+ /* @__PURE__ */ jsx2(
820
+ PresetCard,
821
+ {
822
+ preset: { id: "none", name: t("noPreset"), description: "", tokens: {} },
823
+ selected: prefs.preset === null,
824
+ onClick: () => update("preset", null)
825
+ }
826
+ ),
827
+ PRESETS.map((p) => /* @__PURE__ */ jsx2(
828
+ PresetCard,
829
+ {
830
+ preset: p,
831
+ selected: prefs.preset === p.id,
832
+ onClick: () => update("preset", p.id)
833
+ },
834
+ p.id
835
+ ))
836
+ ] }),
837
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F308}", children: t("accentSection") }),
838
+ /* @__PURE__ */ jsxs2(Card, { children: [
839
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: [
840
+ /* @__PURE__ */ jsx2(
841
+ "input",
842
+ {
843
+ type: "color",
844
+ value: prefs.accentColor ?? "#4B8BBE",
845
+ onChange: (e) => update("accentColor", e.target.value),
846
+ style: { width: 40, height: 40, cursor: "pointer", borderRadius: 8, border: "1px solid var(--border, rgba(128,128,128,0.2))" }
847
+ }
848
+ ),
849
+ /* @__PURE__ */ jsx2(
850
+ Input,
851
+ {
852
+ value: prefs.accentColor ?? "",
853
+ onChange: (e) => update("accentColor", e.target.value || null),
854
+ placeholder: t("accentPlaceholder"),
855
+ style: { maxWidth: 200 }
856
+ }
857
+ ),
858
+ prefs.accentColor !== null && /* @__PURE__ */ jsx2(Button, { variant: "ghost", size: "sm", onClick: () => update("accentColor", null), children: "\u2715" })
859
+ ] }),
860
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.5, marginTop: 6 }, children: t("accentHint") })
861
+ ] }),
862
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F4D0}", children: t("densitySection") }),
863
+ /* @__PURE__ */ jsx2(
864
+ Segmented,
865
+ {
866
+ value: prefs.density,
867
+ onChange: (v) => update("density", v),
868
+ options: [
869
+ { value: "compact", label: t("densityCompact") },
870
+ { value: "comfortable", label: t("densityComfortable") },
871
+ { value: "spacious", label: t("densitySpacious") }
872
+ ]
873
+ }
874
+ ),
875
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u2B1C", children: t("radiusSection") }),
876
+ /* @__PURE__ */ jsx2(
877
+ Segmented,
878
+ {
879
+ value: prefs.radius,
880
+ onChange: (v) => update("radius", v),
881
+ options: [
882
+ { value: "sharp", label: t("radiusSharp") },
883
+ { value: "rounded", label: t("radiusRounded") },
884
+ { value: "soft", label: t("radiusSoft") }
885
+ ]
886
+ }
887
+ ),
888
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F524}", children: t("fontSection") }),
889
+ /* @__PURE__ */ jsx2(
890
+ Segmented,
891
+ {
892
+ value: prefs.fontFamily,
893
+ onChange: (v) => update("fontFamily", v),
894
+ options: [
895
+ { value: "system", label: t("fontSystem") },
896
+ { value: "mono", label: t("fontMono") },
897
+ { value: "serif", label: t("fontSerif") }
898
+ ]
899
+ }
900
+ ),
901
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u270F\uFE0F", children: t("customCssSection") }),
902
+ /* @__PURE__ */ jsx2(Card, { children: /* @__PURE__ */ jsx2(Field, { label: t("customCssSection"), hint: t("customCssHint"), children: /* @__PURE__ */ jsx2(
903
+ Textarea,
904
+ {
905
+ value: prefs.customCss,
906
+ onChange: (e) => update("customCss", e.target.value),
907
+ placeholder: t("customCssPlaceholder"),
908
+ rows: 4
909
+ }
910
+ ) }) }),
911
+ /* @__PURE__ */ jsx2(PreviewArea, { t }),
912
+ confirmReset && /* @__PURE__ */ jsx2(
913
+ ConfirmDialog,
914
+ {
915
+ title: t("reset"),
916
+ message: t("resetConfirm"),
917
+ confirmLabel: t("reset"),
918
+ danger: true,
919
+ onConfirm: handleReset,
920
+ onClose: () => setConfirmReset(false)
921
+ }
922
+ )
923
+ ] });
924
+ }
925
+ function ThemePanel({ t }) {
926
+ return /* @__PURE__ */ jsx2(ToastProvider, { children: /* @__PURE__ */ jsx2(ThemePanelInner, { t }) });
927
+ }
928
+
929
+ // src/client/index.tsx
930
+ var inject = ["slots", "locale"];
931
+ function apply(ctx) {
932
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-theme-studio: dictionaries");
933
+ ctx.slots.inject("settings.section", () => ctx.slots.register(
934
+ {
935
+ name: "settings.section",
936
+ id: "theme-studio",
937
+ order: 47,
938
+ label: () => ctx.locale.bind(NS)("nav"),
939
+ locale: NS
940
+ },
941
+ ThemePanel
942
+ ));
943
+ }
944
+ export {
945
+ apply,
946
+ inject
947
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pre-built theme presets for dsh-theme-studio.
3
+ *
4
+ * Each preset overrides a set of CSS custom properties on document.documentElement.
5
+ * The property names target dsh's `--dsw-alias-*` semantic layer so the overrides
6
+ * cascade through every component that reads the design tokens.
7
+ *
8
+ * @module themes
9
+ */
10
+ import type { ThemePreset } from './types.js';
11
+ export declare const PRESETS: readonly ThemePreset[];
12
+ /** Density → CSS custom property overrides */
13
+ export declare const DENSITY_TOKENS: Record<string, Record<string, string>>;
14
+ /** Radius → CSS custom property overrides */
15
+ export declare const RADIUS_TOKENS: Record<string, Record<string, string>>;
16
+ /** Font family → CSS custom property overrides */
17
+ export declare const FONT_TOKENS: Record<string, Record<string, string>>;
18
+ /** Look up a preset by id. */
19
+ export declare function getPreset(id: string): ThemePreset | undefined;
package/lib/themes.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pre-built theme presets for dsh-theme-studio.
3
+ *
4
+ * Each preset overrides a set of CSS custom properties on document.documentElement.
5
+ * The property names target dsh's `--dsw-alias-*` semantic layer so the overrides
6
+ * cascade through every component that reads the design tokens.
7
+ *
8
+ * @module themes
9
+ */
10
+ export const PRESETS = [
11
+ {
12
+ id: 'ocean',
13
+ name: 'Ocean',
14
+ description: 'Deep blue accent with cool neutrals',
15
+ tokens: {
16
+ '--accent': '#2563eb',
17
+ '--accent-hover': '#1d4ed8',
18
+ '--dsw-alias-state-business-primary': '#2563eb',
19
+ '--dsw-alias-state-business-secondary': '#dbeafe',
20
+ '--dsw-alias-interactive-bg-hover': 'rgba(37,99,235,0.08)',
21
+ },
22
+ },
23
+ {
24
+ id: 'forest',
25
+ name: 'Forest',
26
+ description: 'Green accent with earthy neutrals',
27
+ tokens: {
28
+ '--accent': '#16a34a',
29
+ '--accent-hover': '#15803d',
30
+ '--dsw-alias-state-business-primary': '#16a34a',
31
+ '--dsw-alias-state-business-secondary': '#dcfce7',
32
+ '--dsw-alias-interactive-bg-hover': 'rgba(22,163,74,0.08)',
33
+ },
34
+ },
35
+ {
36
+ id: 'sunset',
37
+ name: 'Sunset',
38
+ description: 'Warm orange accent with amber highlights',
39
+ tokens: {
40
+ '--accent': '#ea580c',
41
+ '--accent-hover': '#c2410c',
42
+ '--dsw-alias-state-business-primary': '#ea580c',
43
+ '--dsw-alias-state-business-secondary': '#fed7aa',
44
+ '--dsw-alias-interactive-bg-hover': 'rgba(234,88,12,0.08)',
45
+ },
46
+ },
47
+ {
48
+ id: 'monochrome',
49
+ name: 'Monochrome',
50
+ description: 'Grayscale only — no color accent',
51
+ tokens: {
52
+ '--accent': '#404040',
53
+ '--accent-hover': '#262626',
54
+ '--dsw-alias-state-business-primary': '#404040',
55
+ '--dsw-alias-state-business-secondary': '#e5e5e5',
56
+ '--dsw-alias-interactive-bg-hover': 'rgba(64,64,64,0.08)',
57
+ },
58
+ },
59
+ {
60
+ id: 'nord',
61
+ name: 'Nord',
62
+ description: 'Frost blue inspired by the Nord palette',
63
+ tokens: {
64
+ '--accent': '#5e81ac',
65
+ '--accent-hover': '#4c6f96',
66
+ '--dsw-alias-state-business-primary': '#5e81ac',
67
+ '--dsw-alias-state-business-secondary': '#e5ebf1',
68
+ '--dsw-alias-interactive-bg-hover': 'rgba(94,129,172,0.08)',
69
+ },
70
+ },
71
+ {
72
+ id: 'dracula',
73
+ name: 'Dracula',
74
+ description: 'Purple accent with dark mode flair',
75
+ tokens: {
76
+ '--accent': '#bd93f9',
77
+ '--accent-hover': '#a678e8',
78
+ '--dsw-alias-state-business-primary': '#bd93f9',
79
+ '--dsw-alias-state-business-secondary': '#3a3a5c',
80
+ '--dsw-alias-interactive-bg-hover': 'rgba(189,147,249,0.12)',
81
+ },
82
+ },
83
+ ];
84
+ /** Density → CSS custom property overrides */
85
+ export const DENSITY_TOKENS = {
86
+ compact: {
87
+ '--dsh-content-font-size': '13px',
88
+ '--dsh-spacing-unit': '4px',
89
+ },
90
+ comfortable: {
91
+ '--dsh-content-font-size': '14px',
92
+ '--dsh-spacing-unit': '6px',
93
+ },
94
+ spacious: {
95
+ '--dsh-content-font-size': '15px',
96
+ '--dsh-spacing-unit': '8px',
97
+ },
98
+ };
99
+ /** Radius → CSS custom property overrides */
100
+ export const RADIUS_TOKENS = {
101
+ sharp: {
102
+ '--dsh-radius-small': '2px',
103
+ '--dsh-radius-medium': '4px',
104
+ '--dsh-radius-large': '6px',
105
+ },
106
+ rounded: {
107
+ '--dsh-radius-small': '6px',
108
+ '--dsh-radius-medium': '10px',
109
+ '--dsh-radius-large': '14px',
110
+ },
111
+ soft: {
112
+ '--dsh-radius-small': '10px',
113
+ '--dsh-radius-medium': '16px',
114
+ '--dsh-radius-large': '22px',
115
+ },
116
+ };
117
+ /** Font family → CSS custom property overrides */
118
+ export const FONT_TOKENS = {
119
+ system: { '--dsh-font-family': "system-ui, -apple-system, sans-serif" },
120
+ mono: { '--dsh-font-family': "'JetBrains Mono', 'Fira Code', monospace" },
121
+ serif: { '--dsh-font-family': "'Georgia', 'Times New Roman', serif" },
122
+ };
123
+ /** Look up a preset by id. */
124
+ export function getPreset(id) {
125
+ return PRESETS.find((p) => p.id === id);
126
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared types for dsh-theme-studio.
3
+ *
4
+ * @module types
5
+ */
6
+ /** A named preset that overrides a set of CSS custom properties. */
7
+ export interface ThemePreset {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ /** CSS custom property → value, applied to document.documentElement.style */
12
+ tokens: Record<string, string>;
13
+ }
14
+ /** User-selectable density preference. */
15
+ export type Density = 'compact' | 'comfortable' | 'spacious';
16
+ /** User-selectable border-radius preference. */
17
+ export type Radius = 'sharp' | 'rounded' | 'soft';
18
+ /** User-selectable font family preference. */
19
+ export type FontFamily = 'system' | 'mono' | 'serif';
20
+ /** Complete theme preferences persisted in localStorage. */
21
+ export interface ThemePreferences {
22
+ preset: string | null;
23
+ accentColor: string | null;
24
+ density: Density;
25
+ radius: Radius;
26
+ fontFamily: FontFamily;
27
+ customCss: string;
28
+ }
29
+ /** Default preferences — nothing overridden, dsh's built-in theme wins. */
30
+ export declare const DEFAULT_PREFERENCES: ThemePreferences;
31
+ /** localStorage key for persisting preferences. */
32
+ export declare const STORAGE_KEY = "dsh-theme-studio";
package/lib/types.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared types for dsh-theme-studio.
3
+ *
4
+ * @module types
5
+ */
6
+ /** Default preferences — nothing overridden, dsh's built-in theme wins. */
7
+ export const DEFAULT_PREFERENCES = {
8
+ preset: null,
9
+ accentColor: null,
10
+ density: 'comfortable',
11
+ radius: 'rounded',
12
+ fontFamily: 'system',
13
+ customCss: '',
14
+ };
15
+ /** localStorage key for persisting preferences. */
16
+ export const STORAGE_KEY = 'dsh-theme-studio';
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "dsh-theme-studio",
3
+ "version": "0.1.0",
4
+ "description": "Customize the dsh UI theme: pick presets, set accent colors, adjust density, border radius, and font family, or write custom CSS variable overrides.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./lib/client.d.ts",
15
+ "default": "./lib/theme-studio.web.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": ["lib", "cordis.patch.yml", "README.md", "CHANGELOG.md"],
20
+ "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "theme", "appearance", "customization"],
21
+ "dsh": {
22
+ "bundle": { "patch": "./cordis.patch.yml" },
23
+ "client": {
24
+ "platform": "web",
25
+ "inject": [
26
+ "@deepseek-ai/dsh-client-ui-renderer",
27
+ "@deepseek-ai/dsh-client-locale",
28
+ "@deepseek-ai/dsh-client-ui-settings"
29
+ ]
30
+ }
31
+ },
32
+ "license": "MIT",
33
+ "author": "hj01857655",
34
+ "repository": { "type": "git", "url": "git+https://github.com/hj01857655/dsh-theme-studio.git" },
35
+ "bugs": { "url": "https://github.com/hj01857655/dsh-theme-studio/issues" },
36
+ "homepage": "https://github.com/hj01857655/dsh-theme-studio#readme",
37
+ "engines": { "node": ">=20" },
38
+ "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1" },
39
+ "devDependencies": {
40
+ "@deepseek-ai/cordis": "^4.0.2",
41
+ "@types/node": "^22.20.3",
42
+ "@types/react": "^19.3.0",
43
+ "esbuild": "^0.28.2",
44
+ "react": "^19.3.0",
45
+ "react-dom": "^19.3.0",
46
+ "typescript": "^5.6.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json && node scripts/bundle-client.mjs",
50
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
51
+ "test": "tsc -p tsconfig.json && node --test",
52
+ "prepublishOnly": "npm run build && npm test"
53
+ }
54
+ }