dsh-theme-studio 0.1.0 → 0.3.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.
@@ -8,33 +8,44 @@ var zh = {
8
8
  presetHint: "\u9009\u62E9\u4E00\u4E2A\u9884\u8BBE\u914D\u8272\u65B9\u6848",
9
9
  noPreset: "\u65E0\uFF08\u4F7F\u7528\u9ED8\u8BA4\uFF09",
10
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",
11
+ accentHint: "\u540C\u65F6\u4F5C\u7528\u4E8E\u4E3B\u6309\u94AE\u3001\u94FE\u63A5\u3001\u5BFC\u822A\u9009\u4E2D\u6001\u4E0E\u60AC\u505C\u9AD8\u4EAE",
12
+ accentPlaceholder: "#4d6bfe",
13
+ accentApplying: "\u5F53\u524D\u751F\u6548\uFF1A",
14
+ darkAccentSection: "\u6697\u8272\u6A21\u5F0F\u5F3A\u8C03\u8272",
15
+ darkAccentHint: "\u6697\u8272\u6A21\u5F0F\u4E0B\u4F7F\u7528\u7684\u5F3A\u8C03\u8272\uFF0C\u7559\u7A7A\u5219\u6CBF\u7528\u4E0A\u9762\u7684\u989C\u8272",
16
+ preserveContrast: "\u5DF2\u81EA\u52A8\u63D0\u4EAE\u4EE5\u4FDD\u8BC1\u6697\u8272\u4E0B\u53EF\u8BFB",
17
+ tokenNote: "\u672C\u9762\u677F\u53EA\u5199 dsh \u771F\u5B9E\u5B58\u5728\u7684\u8BBE\u8BA1\u4EE4\u724C\uFF08{count} \u4E2A\uFF0C\u5747\u4ECE dsh \u6837\u5F0F\u8868\u6838\u5B9E\uFF09\u3002dsh \u6309\u7EC4\u4EF6\u786C\u7F16\u7801\u4E86\u5706\u89D2\u4E14\u672A\u63D0\u4F9B\u52A8\u753B\u4EE4\u724C\uFF0C\u56E0\u6B64\u4E0D\u63D0\u4F9B\u5706\u89D2\u8C03\u8282\uFF0C\u52A8\u753B\u5F00\u5173\u6539\u7528\u6837\u5F0F\u8868\u5B9E\u73B0\u3002",
13
18
  densitySection: "\u5BC6\u5EA6",
14
19
  densityCompact: "\u7D27\u51D1",
15
20
  densityComfortable: "\u8212\u9002",
16
21
  densitySpacious: "\u5BBD\u677E",
17
22
  radiusSection: "\u5706\u89D2",
18
- radiusSharp: "\u9510\u5229",
19
- radiusRounded: "\u5706\u89D2",
20
- radiusSoft: "\u67D4\u548C",
21
23
  fontSection: "\u5B57\u4F53",
22
24
  fontSystem: "\u7CFB\u7EDF",
23
25
  fontMono: "\u7B49\u5BBD",
24
26
  fontSerif: "\u886C\u7EBF",
25
27
  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
+ customCssHint: "\u8986\u76D6 dsh \u4EE4\u724C\uFF0C\u6BCF\u884C\u4E00\u4E2A `--dsw-* \u6216 --dsh-*: value;`",
29
+ customCssPlaceholder: "--dsw-alias-brand-primary: #ff0000;\n--dsw-alias-label-primary: #111111;",
30
+ behaviorSection: "\u884C\u4E3A",
31
+ animations: "\u754C\u9762\u52A8\u753B",
32
+ animationsHint: "\u5173\u95ED\u53EF\u51CF\u5C11\u52A8\u753B\u5E72\u6270\uFF08\u7528\u6837\u5F0F\u8868\u628A\u8FC7\u6E21\u65F6\u957F\u538B\u5230\u8FD1\u96F6\uFF0C\u4E8B\u4EF6\u4ECD\u4F1A\u89E6\u53D1\uFF09",
33
+ ioSection: "\u5BFC\u5165 / \u5BFC\u51FA",
34
+ export: "\u5BFC\u51FA\u4E3B\u9898",
35
+ import: "\u5BFC\u5165\u4E3B\u9898",
36
+ importHint: "\u7C98\u8D34\u5BFC\u51FA\u7684 JSON\uFF0C\u6216\u4E0A\u4F20 .json \u6587\u4EF6",
37
+ importPlaceholder: '{"preset":"nord","accentColor":"#5e81ac",...}',
38
+ importFile: "\u9009\u62E9\u6587\u4EF6",
39
+ importSuccess: "\u4E3B\u9898\u5DF2\u5BFC\u5165",
40
+ importFailed: "\u5BFC\u5165\u5931\u8D25\uFF1A\u4E0D\u662F\u6709\u6548\u7684\u4E3B\u9898 JSON",
41
+ exportSuccess: "\u4E3B\u9898\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F",
42
+ exportFailed: "\u590D\u5236\u5931\u8D25\uFF0C\u5DF2\u586B\u5165\u4E0B\u65B9\u6587\u672C\u6846",
28
43
  preview: "\u9884\u89C8",
29
- apply: "\u5E94\u7528",
30
44
  reset: "\u91CD\u7F6E\u4E3A\u9ED8\u8BA4",
31
45
  resetConfirm: "\u786E\u5B9A\u8981\u91CD\u7F6E\u6240\u6709\u4E3B\u9898\u8BBE\u7F6E\u5417\uFF1F",
32
- applied: "\u4E3B\u9898\u5DF2\u5E94\u7528",
33
46
  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"
47
+ link: "\u94FE\u63A5",
48
+ text: "\u8FD9\u662F\u4E00\u6BB5\u793A\u4F8B\u6587\u672C\uFF0C\u7528\u4E8E\u9884\u89C8\u5B57\u53F7\u4E0E\u5B57\u4F53\u3002"
38
49
  };
39
50
  var en = {
40
51
  nav: "Theme Studio",
@@ -44,164 +55,367 @@ var en = {
44
55
  presetHint: "Choose a pre-built color scheme",
45
56
  noPreset: "None (use default)",
46
57
  accentSection: "Accent Color",
47
- accentHint: "Custom accent color, overrides the preset",
48
- accentPlaceholder: "#4B8BBE",
58
+ accentHint: "Custom accent color \u2014 drives buttons, links, the active nav item and hover tints",
59
+ accentPlaceholder: "#4d6bfe",
60
+ accentApplying: "Currently applied:",
61
+ darkAccentSection: "Dark Mode Accent",
62
+ darkAccentHint: "Accent used in dark mode; leave empty to reuse the color above",
63
+ preserveContrast: "Lightened automatically for dark-mode readability",
64
+ tokenNote: "This panel only writes design tokens that actually exist in dsh ({count} verified against its stylesheets). dsh hardcodes per-component corner radii and ships no motion tokens, so there is no radius control and the animation toggle is implemented as a stylesheet.",
49
65
  densitySection: "Density",
50
66
  densityCompact: "Compact",
51
67
  densityComfortable: "Comfortable",
52
68
  densitySpacious: "Spacious",
53
69
  radiusSection: "Border Radius",
54
- radiusSharp: "Sharp",
55
- radiusRounded: "Rounded",
56
- radiusSoft: "Soft",
70
+ radiusUnavailable: "dsh ships no radius token, so this cannot be adjusted",
57
71
  fontSection: "Font Family",
58
72
  fontSystem: "System",
59
73
  fontMono: "Monospace",
60
74
  fontSerif: "Serif",
61
75
  customCssSection: "Custom CSS",
62
- customCssHint: "Override CSS custom properties directly, one `--property: value;` per line",
63
- customCssPlaceholder: "--accent: #ff0000;\n--border: 1px solid red;",
76
+ customCssHint: "Override dsh tokens, one `--dsw-* or --dsh-*: value;` per line",
77
+ customCssPlaceholder: "--dsw-alias-brand-primary: #ff0000;\n--dsw-alias-label-primary: #111111;",
78
+ behaviorSection: "Behavior",
79
+ animations: "UI Animations",
80
+ animationsHint: "Turn off to reduce motion (a stylesheet compresses transition durations to near-zero so events still fire)",
81
+ ioSection: "Import / Export",
82
+ export: "Export Theme",
83
+ import: "Import Theme",
84
+ importHint: "Paste exported JSON, or upload a .json file",
85
+ importPlaceholder: '{"preset":"nord","accentColor":"#5e81ac",...}',
86
+ importFile: "Choose File",
87
+ importSuccess: "Theme imported",
88
+ importFailed: "Import failed: not a valid theme JSON",
89
+ exportSuccess: "Theme copied to clipboard",
90
+ exportFailed: "Copy failed \u2014 filled into the box below",
64
91
  preview: "Preview",
65
- apply: "Apply",
66
92
  reset: "Reset to Default",
67
93
  resetConfirm: "Reset all theme settings to default?",
68
- applied: "Theme applied",
69
94
  resetted: "Reset to default theme",
70
- card: "Card",
71
- button: "Button",
72
- badge: "Badge",
73
- text: "This is sample text to preview the theme."
95
+ link: "Link",
96
+ text: "Sample text for previewing size and font family."
74
97
  };
75
98
 
76
99
  // src/client/view.tsx
77
- import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
100
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef, useState as useState2 } from "react";
78
101
 
79
102
  // src/types.ts
80
103
  var DEFAULT_PREFERENCES = {
81
104
  preset: null,
82
105
  accentColor: null,
106
+ darkAccentColor: null,
83
107
  density: "comfortable",
84
- radius: "rounded",
85
108
  fontFamily: "system",
109
+ animations: true,
86
110
  customCss: ""
87
111
  };
88
112
  var STORAGE_KEY = "dsh-theme-studio";
113
+ var STYLE_ELEMENT_ID = "dsh-theme-studio-overrides";
89
114
 
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
- }
115
+ // src/tokens.ts
116
+ var VERIFIED_TOKENS = [
117
+ // Accent / business
118
+ "--dsw-alias-state-business-primary",
119
+ "--dsw-alias-state-business-tertiary",
120
+ "--dsw-alias-brand-primary",
121
+ "--dsw-alias-brand-text",
122
+ "--dsw-alias-link",
123
+ "--dsw-alias-interactive-bg-hover",
124
+ "--dsw-alias-interactive-bg-hover-accent",
125
+ "--dsw-alias-button-primary-fill",
126
+ "--dsw-alias-button-primary-hover",
127
+ // Typography
128
+ "--dsw-font-family",
129
+ "--dsw-font-mono",
130
+ "--dsw-font-xs-13",
131
+ "--dsw-font-xs-strong-13",
132
+ "--dsw-font-xxs-12",
133
+ "--dsw-font-xxxs-11",
134
+ "--dsw-font-s-14",
135
+ "--dsw-font-l-20",
136
+ "--dsh-content-font-size",
137
+ "--dsh-content-font-delta"
164
138
  ];
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"
139
+ var VERIFIED = new Set(VERIFIED_TOKENS);
140
+ function tint(hex, alpha) {
141
+ const value = hex.trim().replace(/^#/, "");
142
+ let r;
143
+ let g;
144
+ let b;
145
+ if (value.length === 3) {
146
+ r = parseInt(value[0] + value[0], 16);
147
+ g = parseInt(value[1] + value[1], 16);
148
+ b = parseInt(value[2] + value[2], 16);
149
+ } else if (value.length === 6) {
150
+ r = parseInt(value.slice(0, 2), 16);
151
+ g = parseInt(value.slice(2, 4), 16);
152
+ b = parseInt(value.slice(4, 6), 16);
153
+ } else {
154
+ return hex;
177
155
  }
156
+ if ([r, g, b].some((n) => Number.isNaN(n))) return hex;
157
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
158
+ }
159
+ function accentTokens(hex) {
160
+ return {
161
+ "--dsw-alias-state-business-primary": hex,
162
+ "--dsw-alias-state-business-tertiary": tint(hex, 0.12),
163
+ "--dsw-alias-brand-primary": hex,
164
+ "--dsw-alias-brand-text": hex,
165
+ "--dsw-alias-link": hex,
166
+ "--dsw-alias-interactive-bg-hover-accent": tint(hex, 0.1),
167
+ "--dsw-alias-button-primary-fill": hex,
168
+ "--dsw-alias-button-primary-hover": hex
169
+ };
170
+ }
171
+ var DENSITY_TOKENS = {
172
+ compact: { "--dsh-content-font-size": "13px" },
173
+ comfortable: { "--dsh-content-font-size": "14px" },
174
+ spacious: { "--dsh-content-font-size": "15px" }
178
175
  };
179
- var RADIUS_TOKENS = {
180
- sharp: {
181
- "--dsh-radius-small": "2px",
182
- "--dsh-radius-medium": "4px",
183
- "--dsh-radius-large": "6px"
176
+ var FONT_TOKENS = {
177
+ system: {
178
+ "--dsw-font-family": "system-ui, -apple-system, 'Segoe UI', sans-serif",
179
+ "--dsw-font-mono": "'Cascadia Code', 'JetBrains Mono', Consolas, monospace"
184
180
  },
185
- rounded: {
186
- "--dsh-radius-small": "6px",
187
- "--dsh-radius-medium": "10px",
188
- "--dsh-radius-large": "14px"
181
+ mono: {
182
+ "--dsw-font-family": "'Cascadia Code', 'JetBrains Mono', Consolas, monospace",
183
+ "--dsw-font-mono": "'Cascadia Code', 'JetBrains Mono', Consolas, monospace"
189
184
  },
190
- soft: {
191
- "--dsh-radius-small": "10px",
192
- "--dsh-radius-medium": "16px",
193
- "--dsh-radius-large": "22px"
185
+ serif: {
186
+ "--dsw-font-family": "Georgia, 'Times New Roman', serif",
187
+ "--dsw-font-mono": "'Cascadia Code', Consolas, monospace"
194
188
  }
195
189
  };
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
- };
190
+ var MOTION_OFF_CSS = `
191
+ *, *::before, *::after {
192
+ transition-duration: 0.001ms !important;
193
+ transition-delay: 0ms !important;
194
+ animation-duration: 0.001ms !important;
195
+ animation-delay: 0ms !important;
196
+ animation-iteration-count: 1 !important;
197
+ scroll-behavior: auto !important;
198
+ }
199
+ `.trim();
200
+
201
+ // src/themes.ts
202
+ function preset(id, name, description, lightAccent, darkAccent) {
203
+ const base = { id, name, description, tokens: accentTokens(lightAccent) };
204
+ if (darkAccent !== void 0 && darkAccent.toLowerCase() !== lightAccent.toLowerCase()) {
205
+ base.darkTokens = accentTokens(darkAccent);
206
+ }
207
+ return base;
208
+ }
209
+ var PRESETS = [
210
+ preset("ocean", "Ocean", "Deep blue, close to dsh default", "#4d6bfe"),
211
+ preset("forest", "Forest", "Calm green", "#16a34a", "#22c55e"),
212
+ preset("sunset", "Sunset", "Warm orange", "#ea580c", "#fb923c"),
213
+ preset("monochrome", "Monochrome", "No color accent at all", "#5c5c5c", "#9e9e9e"),
214
+ preset("nord", "Nord", "Frost blue", "#5e81ac", "#88c0d0"),
215
+ preset("dracula", "Dracula", "Purple, brighter on dark", "#7c5cd6", "#bd93f9"),
216
+ preset("gruvbox", "Gruvbox", "Retro warm earth tones", "#b57614", "#fe8019"),
217
+ preset("solarized", "Solarized", "Schoonover precision palette", "#268bd2", "#5fa8d3"),
218
+ preset("tokyo-night", "Tokyo Night", "City lights blue", "#3d59a1", "#7aa2f7"),
219
+ preset("catppuccin", "Catppuccin", "Soft pastel", "#7287fd", "#cba6f7"),
220
+ preset("rose", "Ros\xE9", "Muted pink", "#b3276b", "#f472b6"),
221
+ preset("ember", "Ember", "Deep red", "#b91c1c", "#ef4444")
222
+ ];
201
223
  function getPreset(id) {
202
224
  return PRESETS.find((p) => p.id === id);
203
225
  }
204
226
 
227
+ // src/io.ts
228
+ function exportTheme(prefs, name) {
229
+ const payload = {
230
+ $schema: "dsh-theme-studio/v1",
231
+ preferences: prefs,
232
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
233
+ };
234
+ if (name !== void 0) payload.name = name;
235
+ return JSON.stringify(payload, null, 2);
236
+ }
237
+ var DENSITIES = /* @__PURE__ */ new Set(["compact", "comfortable", "spacious"]);
238
+ var FONTS = /* @__PURE__ */ new Set(["system", "mono", "serif"]);
239
+ function asColor(value) {
240
+ if (typeof value !== "string") return null;
241
+ const trimmed = value.trim();
242
+ if (trimmed === "") return null;
243
+ return /^#[0-9a-fA-F]{3,8}$/.test(trimmed) ? trimmed : null;
244
+ }
245
+ function parseTheme(raw) {
246
+ let parsed;
247
+ try {
248
+ parsed = JSON.parse(raw);
249
+ } catch {
250
+ return null;
251
+ }
252
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
253
+ const doc = parsed;
254
+ const source = doc["preferences"] !== null && typeof doc["preferences"] === "object" && !Array.isArray(doc["preferences"]) ? doc["preferences"] : doc;
255
+ const result = { ...DEFAULT_PREFERENCES };
256
+ if (typeof source["preset"] === "string" || source["preset"] === null) {
257
+ result.preset = source["preset"];
258
+ }
259
+ result.accentColor = asColor(source["accentColor"]);
260
+ result.darkAccentColor = asColor(source["darkAccentColor"]);
261
+ if (typeof source["density"] === "string" && DENSITIES.has(source["density"])) {
262
+ result.density = source["density"];
263
+ }
264
+ if (typeof source["fontFamily"] === "string" && FONTS.has(source["fontFamily"])) {
265
+ result.fontFamily = source["fontFamily"];
266
+ }
267
+ if (typeof source["animations"] === "boolean") result.animations = source["animations"];
268
+ if (typeof source["customCss"] === "string") result.customCss = source["customCss"];
269
+ return result;
270
+ }
271
+ function hexToRgb(hex) {
272
+ const value = hex.trim().replace(/^#/, "");
273
+ if (value.length === 3) {
274
+ const [r, g, b] = value.split("");
275
+ return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];
276
+ }
277
+ if (value.length === 6) {
278
+ return [
279
+ parseInt(value.slice(0, 2), 16),
280
+ parseInt(value.slice(2, 4), 16),
281
+ parseInt(value.slice(4, 6), 16)
282
+ ];
283
+ }
284
+ return null;
285
+ }
286
+ function luminance(hex) {
287
+ const rgb = hexToRgb(hex);
288
+ if (rgb === null) return 0;
289
+ const [r, g, b] = rgb.map((c) => {
290
+ const s = c / 255;
291
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
292
+ });
293
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
294
+ }
295
+ function rgbToHex(r, g, b) {
296
+ const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
297
+ return "#" + [r, g, b].map((n) => clamp(n).toString(16).padStart(2, "0")).join("");
298
+ }
299
+ function lighten(hex, amount) {
300
+ const rgb = hexToRgb(hex);
301
+ if (rgb === null) return hex;
302
+ const [r, g, b] = rgb;
303
+ return rgbToHex(
304
+ r + (255 - r) * amount,
305
+ g + (255 - g) * amount,
306
+ b + (255 - b) * amount
307
+ );
308
+ }
309
+ function ensureDarkContrast(hex) {
310
+ const MIN_LUMINANCE = 0.18;
311
+ if (hexToRgb(hex) === null) return { color: hex, adjusted: false };
312
+ if (luminance(hex) >= MIN_LUMINANCE) return { color: hex, adjusted: false };
313
+ let current = hex;
314
+ for (let i = 0; i < 10; i += 1) {
315
+ current = lighten(current, 0.12);
316
+ if (luminance(current) >= MIN_LUMINANCE) return { color: current, adjusted: true };
317
+ }
318
+ return { color: current, adjusted: true };
319
+ }
320
+
321
+ // src/apply.ts
322
+ function themeTarget() {
323
+ if (typeof document === "undefined") return null;
324
+ return document.body ?? null;
325
+ }
326
+ function isDshToken(name) {
327
+ return /^--dsh-\S+$/.test(name) || /^--dsw-\S+$/.test(name);
328
+ }
329
+ function isDarkMode() {
330
+ if (typeof document === "undefined") return false;
331
+ return document.body.hasAttribute("data-ds-dark-theme");
332
+ }
333
+ function observeDarkMode(onChange) {
334
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
335
+ return () => {
336
+ };
337
+ }
338
+ const observer = new MutationObserver(() => onChange(isDarkMode()));
339
+ observer.observe(document.body, {
340
+ attributes: true,
341
+ attributeFilter: ["data-ds-dark-theme"]
342
+ });
343
+ return () => observer.disconnect();
344
+ }
345
+ function resolveAccent(prefs, dark) {
346
+ const chosen = dark ? prefs.darkAccentColor ?? prefs.accentColor : prefs.accentColor;
347
+ if (chosen === null || chosen.trim() === "") return { accent: null, adjusted: false };
348
+ if (!dark) return { accent: chosen, adjusted: false };
349
+ const guarded = ensureDarkContrast(chosen);
350
+ return { accent: guarded.color, adjusted: guarded.adjusted };
351
+ }
352
+ function parseCustomCss(css, allow) {
353
+ const out = {};
354
+ for (const line of css.split("\n")) {
355
+ const match = line.match(/^\s*(--[\w-]+)\s*:\s*([^;{}/*]+?)\s*;?\s*$/);
356
+ if (match === null) continue;
357
+ if (!allow(match[1])) continue;
358
+ out[match[1]] = match[2];
359
+ }
360
+ return out;
361
+ }
362
+ function clearManagedProperties(fullCustomCss = "") {
363
+ const target = themeTarget();
364
+ if (target === null) return;
365
+ const names = /* @__PURE__ */ new Set([
366
+ ...Object.keys(accentTokens("#000000")),
367
+ ...Object.values(DENSITY_TOKENS).flatMap((t) => Object.keys(t)),
368
+ ...Object.values(FONT_TOKENS).flatMap((t) => Object.keys(t))
369
+ ]);
370
+ for (const line of fullCustomCss.split("\n")) {
371
+ const match = line.match(/^\s*(--[\w-]+)\s*:/);
372
+ if (match !== null) names.add(match[1]);
373
+ }
374
+ for (const name of names) target.style.removeProperty(name);
375
+ }
376
+ function motionStylesheet() {
377
+ if (typeof document === "undefined") return null;
378
+ let el = document.getElementById(STYLE_ELEMENT_ID);
379
+ if (el === null) {
380
+ el = document.createElement("style");
381
+ el.id = STYLE_ELEMENT_ID;
382
+ document.head.appendChild(el);
383
+ }
384
+ return el;
385
+ }
386
+ function applyTheme(prefs, dark = isDarkMode()) {
387
+ const target = themeTarget();
388
+ if (target === null) return false;
389
+ clearManagedProperties(prefs.customCss);
390
+ const tokens = {};
391
+ if (prefs.preset !== null) {
392
+ const found = getPreset(prefs.preset);
393
+ if (found !== void 0) {
394
+ Object.assign(tokens, found.tokens);
395
+ if (dark && found.darkTokens !== void 0) Object.assign(tokens, found.darkTokens);
396
+ }
397
+ }
398
+ const { accent, adjusted } = resolveAccent(prefs, dark);
399
+ if (accent !== null) Object.assign(tokens, accentTokens(accent));
400
+ Object.assign(tokens, DENSITY_TOKENS[prefs.density] ?? {});
401
+ Object.assign(tokens, FONT_TOKENS[prefs.fontFamily] ?? {});
402
+ for (const [name, value] of Object.entries(tokens)) {
403
+ target.style.setProperty(name, value);
404
+ }
405
+ const custom = parseCustomCss(prefs.customCss, isDshToken);
406
+ for (const [name, value] of Object.entries(custom)) {
407
+ target.style.setProperty(name, value);
408
+ }
409
+ const sheet = motionStylesheet();
410
+ if (sheet !== null) sheet.textContent = prefs.animations ? "" : MOTION_OFF_CSS;
411
+ return adjusted;
412
+ }
413
+ function resetTheme() {
414
+ clearManagedProperties();
415
+ const sheet = typeof document !== "undefined" ? document.getElementById(STYLE_ELEMENT_ID) : null;
416
+ if (sheet !== null) sheet.textContent = "";
417
+ }
418
+
205
419
  // src/client/ui.tsx
206
420
  import {
207
421
  createContext,
@@ -570,7 +784,7 @@ function ToastProvider({ children }) {
570
784
  fontWeight: 500,
571
785
  boxShadow: "0 6px 20px rgba(0,0,0,0.2)",
572
786
  color: "#fff",
573
- background: t.type === "success" ? T.success : t.type === "error" ? T.error : T.accent,
787
+ background: t.type === "success" ? T.success : t.type === "error" ? T.error : t.type === "warning" ? T.warning : T.accent,
574
788
  maxWidth: 360
575
789
  },
576
790
  children: t.message
@@ -669,59 +883,9 @@ function savePreferences(prefs) {
669
883
  if (typeof localStorage === "undefined") return;
670
884
  localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
671
885
  }
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";
886
+ function PresetCard({ preset: preset2, selected, onClick }) {
887
+ const accent = preset2.tokens["--dsw-alias-state-business-primary"] ?? "#4d6bfe";
888
+ const darkAccent = preset2.darkTokens?.["--dsw-alias-state-business-primary"];
725
889
  return /* @__PURE__ */ jsxs2(
726
890
  "div",
727
891
  {
@@ -730,23 +894,31 @@ function PresetCard({ preset, selected, onClick }) {
730
894
  cursor: "pointer",
731
895
  borderRadius: 10,
732
896
  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))",
897
+ border: selected ? `2px solid ${accent}` : "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
898
+ background: selected ? "var(--dsw-alias-interactive-bg-hover-accent, rgba(77,107,254,0.08))" : "transparent",
735
899
  transition: "all 0.15s ease"
736
900
  },
737
901
  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" })
902
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }, children: [
903
+ /* @__PURE__ */ jsxs2("span", { style: { display: "flex", gap: 3 }, children: [
904
+ /* @__PURE__ */ jsx2("i", { style: { width: 14, height: 14, borderRadius: "50%", background: accent, display: "block" } }),
905
+ darkAccent !== void 0 && /* @__PURE__ */ jsx2("i", { style: { width: 14, height: 14, borderRadius: "50%", background: darkAccent, display: "block" } })
906
+ ] }),
907
+ /* @__PURE__ */ jsx2("strong", { style: { fontSize: 13 }, children: preset2.name }),
908
+ selected && /* @__PURE__ */ jsx2(Badge, { color: "success", children: "\u2713" })
742
909
  ] }),
743
- /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.6 }, children: preset.description })
910
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.65, lineHeight: 1.4 }, children: preset2.description })
744
911
  ]
745
912
  }
746
913
  );
747
914
  }
748
915
  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(
916
+ return /* @__PURE__ */ jsx2("div", { style: {
917
+ display: "inline-flex",
918
+ borderRadius: 8,
919
+ overflow: "hidden",
920
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
921
+ }, children: options.map((opt) => /* @__PURE__ */ jsx2(
750
922
  "button",
751
923
  {
752
924
  type: "button",
@@ -758,8 +930,8 @@ function Segmented({ value, options, onChange }) {
758
930
  fontSize: 13,
759
931
  fontWeight: 500,
760
932
  fontFamily: "inherit",
761
- background: value === opt.value ? "var(--accent, #4B8BBE)" : "transparent",
762
- color: value === opt.value ? "#fff" : "var(--text-primary, inherit)",
933
+ background: value === opt.value ? "var(--dsw-alias-button-primary-fill, #4d6bfe)" : "transparent",
934
+ color: value === opt.value ? "#fff" : "inherit",
763
935
  transition: "all 0.15s ease"
764
936
  },
765
937
  children: opt.label
@@ -767,55 +939,265 @@ function Segmented({ value, options, onChange }) {
767
939
  opt.value
768
940
  )) });
769
941
  }
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" })
942
+ function ColorRow({ value, onChange, placeholder }) {
943
+ return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: [
944
+ /* @__PURE__ */ jsx2(
945
+ "input",
946
+ {
947
+ type: "color",
948
+ value: value ?? "#4d6bfe",
949
+ onChange: (e) => onChange(e.target.value),
950
+ style: {
951
+ width: 40,
952
+ height: 40,
953
+ cursor: "pointer",
954
+ borderRadius: 8,
955
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
956
+ }
957
+ }
958
+ ),
959
+ /* @__PURE__ */ jsx2(
960
+ Input,
961
+ {
962
+ value: value ?? "",
963
+ onChange: (e) => onChange(e.target.value.trim() === "" ? null : e.target.value),
964
+ placeholder,
965
+ style: { maxWidth: 200 }
966
+ }
967
+ ),
968
+ value !== null && /* @__PURE__ */ jsx2(Button, { variant: "ghost", size: "sm", onClick: () => onChange(null), children: "\u2715" })
969
+ ] });
970
+ }
971
+ function PreviewArea({ t, dark, accentApplied }) {
972
+ return /* @__PURE__ */ jsx2(
973
+ Card,
974
+ {
975
+ title: t("preview"),
976
+ icon: "\u{1F441}",
977
+ actions: /* @__PURE__ */ jsx2(Badge, { color: dark ? "info" : "warning", children: dark ? "\u{1F319} dark" : "\u2600 light" }),
978
+ children: /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
979
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 14, alignItems: "center", flexWrap: "wrap" }, children: [
980
+ /* @__PURE__ */ jsxs2("span", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 12 }, children: [
981
+ /* @__PURE__ */ jsx2("i", { style: {
982
+ width: 18,
983
+ height: 18,
984
+ borderRadius: 4,
985
+ display: "block",
986
+ background: "var(--dsw-alias-state-business-primary, #4d6bfe)"
987
+ } }),
988
+ "accent"
989
+ ] }),
990
+ /* @__PURE__ */ jsxs2("span", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 12 }, children: [
991
+ /* @__PURE__ */ jsx2("i", { style: {
992
+ width: 18,
993
+ height: 18,
994
+ borderRadius: 4,
995
+ display: "block",
996
+ background: "var(--dsw-alias-button-primary-fill, #4d6bfe)"
997
+ } }),
998
+ "button"
999
+ ] }),
1000
+ /* @__PURE__ */ jsxs2("span", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 12 }, children: [
1001
+ /* @__PURE__ */ jsx2("i", { style: {
1002
+ width: 18,
1003
+ height: 18,
1004
+ borderRadius: 4,
1005
+ display: "block",
1006
+ background: "var(--dsw-alias-label-primary, currentColor)"
1007
+ } }),
1008
+ "label"
1009
+ ] }),
1010
+ /* @__PURE__ */ jsxs2("span", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 12 }, children: [
1011
+ /* @__PURE__ */ jsx2("i", { style: {
1012
+ width: 18,
1013
+ height: 18,
1014
+ borderRadius: 4,
1015
+ display: "block",
1016
+ background: "var(--dsw-alias-bg-layer-1, rgba(128,128,128,0.1))",
1017
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
1018
+ } }),
1019
+ "surface"
1020
+ ] })
1021
+ ] }),
1022
+ /* @__PURE__ */ jsxs2("div", { style: {
1023
+ padding: 12,
1024
+ borderRadius: 10,
1025
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
1026
+ background: "var(--dsw-alias-bg-layer-1, transparent)"
1027
+ }, children: [
1028
+ /* @__PURE__ */ jsx2(
1029
+ "div",
1030
+ {
1031
+ style: {
1032
+ fontSize: "var(--dsh-content-font-size, 14px)",
1033
+ fontFamily: "var(--dsw-font-family, inherit)",
1034
+ marginBottom: 10
1035
+ },
1036
+ children: t("text")
1037
+ }
1038
+ ),
1039
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [
1040
+ /* @__PURE__ */ jsx2("span", { style: {
1041
+ display: "inline-block",
1042
+ padding: "7px 16px",
1043
+ borderRadius: 7,
1044
+ background: "var(--dsw-alias-button-primary-fill, #4d6bfe)",
1045
+ color: "var(--dsw-alias-label-primary-inverted, #fff)",
1046
+ fontSize: 13
1047
+ }, children: t("button") }),
1048
+ /* @__PURE__ */ jsx2("span", { style: {
1049
+ display: "inline-block",
1050
+ padding: "7px 16px",
1051
+ borderRadius: 7,
1052
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
1053
+ fontSize: 13
1054
+ }, children: t("link") }),
1055
+ /* @__PURE__ */ jsx2(
1056
+ "a",
1057
+ {
1058
+ href: "#preview",
1059
+ onClick: (e) => e.preventDefault(),
1060
+ style: { color: "var(--dsw-alias-link, #4d6bfe)", fontSize: 13 },
1061
+ children: t("link")
1062
+ }
1063
+ )
1064
+ ] })
1065
+ ] }),
1066
+ accentApplied !== null && /* @__PURE__ */ jsxs2("div", { style: { fontSize: 11, opacity: 0.6, fontFamily: "var(--dsw-font-mono, monospace)" }, children: [
1067
+ t("accentApplying"),
1068
+ " ",
1069
+ accentApplied
1070
+ ] })
784
1071
  ] })
785
- ] })
786
- ] }) });
1072
+ }
1073
+ );
1074
+ }
1075
+ function TokenCoverage({ t }) {
1076
+ return /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.55, lineHeight: 1.5 }, children: t("tokenNote", { count: VERIFIED_TOKENS.length }) });
787
1077
  }
788
1078
  function ThemePanelInner({ t }) {
789
1079
  const toast = useToast();
790
1080
  const [prefs, setPrefs] = useState2(loadPreferences);
1081
+ const [dark, setDark] = useState2(isDarkMode);
1082
+ const [contrastAdjusted, setContrastAdjusted] = useState2(false);
791
1083
  const [confirmReset, setConfirmReset] = useState2(false);
1084
+ const [importText, setImportText] = useState2("");
1085
+ const [importOpen, setImportOpen] = useState2(false);
1086
+ const fileInput = useRef(null);
792
1087
  useEffect2(() => {
793
- applyTheme(prefs);
1088
+ setContrastAdjusted(applyTheme(prefs, dark));
794
1089
  savePreferences(prefs);
795
- }, [prefs]);
1090
+ }, [prefs, dark]);
1091
+ useEffect2(() => observeDarkMode(setDark), []);
1092
+ useEffect2(() => () => {
1093
+ applyTheme(loadPreferences(), isDarkMode());
1094
+ }, []);
796
1095
  const update = useCallback2((key, value) => {
797
1096
  setPrefs((p) => ({ ...p, [key]: value }));
798
1097
  }, []);
799
1098
  const handleReset = useCallback2(() => {
800
- clearTheme();
1099
+ resetTheme();
801
1100
  setPrefs({ ...DEFAULT_PREFERENCES });
1101
+ setConfirmReset(false);
802
1102
  toast("success", t("resetted"));
803
1103
  }, [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: [
1104
+ const handleExport = useCallback2(() => {
1105
+ const json = exportTheme(prefs);
1106
+ const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
1107
+ if (clipboard === void 0) {
1108
+ setImportText(json);
1109
+ setImportOpen(true);
1110
+ toast("warning", t("exportFailed"));
1111
+ return;
1112
+ }
1113
+ clipboard.writeText(json).then(
1114
+ () => toast("success", t("exportSuccess")),
1115
+ () => {
1116
+ setImportText(json);
1117
+ setImportOpen(true);
1118
+ toast("warning", t("exportFailed"));
1119
+ }
1120
+ );
1121
+ }, [prefs, t, toast]);
1122
+ const runImport = useCallback2((raw) => {
1123
+ const parsed = parseTheme(raw);
1124
+ if (parsed === null) {
1125
+ toast("error", t("importFailed"));
1126
+ return;
1127
+ }
1128
+ setPrefs(parsed);
1129
+ setImportOpen(false);
1130
+ setImportText("");
1131
+ toast("success", t("importSuccess"));
1132
+ }, [t, toast]);
1133
+ const handleFile = useCallback2((file) => {
1134
+ if (file === void 0) return;
1135
+ file.text().then(runImport, () => toast("error", t("importFailed")));
1136
+ }, [runImport, t, toast]);
1137
+ const { accent: accentApplied } = resolveAccent(prefs, dark);
1138
+ return /* @__PURE__ */ jsxs2("div", { style: { display: "flex", flexDirection: "column", gap: 16, maxWidth: 860 }, children: [
1139
+ /* @__PURE__ */ jsxs2("header", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }, children: [
806
1140
  /* @__PURE__ */ jsxs2("strong", { style: { fontSize: 15 }, children: [
807
1141
  "\u{1F3A8} ",
808
1142
  t("title")
809
1143
  ] }),
810
1144
  /* @__PURE__ */ jsx2("span", { style: { fontSize: 12, opacity: 0.6 }, children: t("subtitle") }),
811
1145
  /* @__PURE__ */ jsx2("span", { style: { flex: 1 } }),
1146
+ /* @__PURE__ */ jsxs2(Button, { variant: "secondary", size: "sm", onClick: handleExport, children: [
1147
+ "\u2934 ",
1148
+ t("export")
1149
+ ] }),
1150
+ /* @__PURE__ */ jsxs2(Button, { variant: "secondary", size: "sm", onClick: () => setImportOpen((v) => !v), children: [
1151
+ "\u2935 ",
1152
+ t("import")
1153
+ ] }),
812
1154
  /* @__PURE__ */ jsxs2(Button, { variant: "danger", size: "sm", onClick: () => setConfirmReset(true), children: [
813
1155
  "\u21BA ",
814
1156
  t("reset")
815
1157
  ] })
816
1158
  ] }),
1159
+ importOpen && /* @__PURE__ */ jsxs2(Card, { title: t("import"), icon: "\u2935", children: [
1160
+ /* @__PURE__ */ jsx2(Field, { label: t("import"), hint: t("importHint"), children: /* @__PURE__ */ jsx2(
1161
+ Textarea,
1162
+ {
1163
+ value: importText,
1164
+ onChange: (e) => setImportText(e.target.value),
1165
+ placeholder: t("importPlaceholder"),
1166
+ rows: 5
1167
+ }
1168
+ ) }),
1169
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8, marginTop: 8 }, children: [
1170
+ /* @__PURE__ */ jsx2(
1171
+ Button,
1172
+ {
1173
+ variant: "primary",
1174
+ size: "sm",
1175
+ onClick: () => runImport(importText),
1176
+ disabled: importText.trim() === "",
1177
+ children: t("import")
1178
+ }
1179
+ ),
1180
+ /* @__PURE__ */ jsxs2(Button, { variant: "secondary", size: "sm", onClick: () => fileInput.current?.click(), children: [
1181
+ "\u{1F4C4} ",
1182
+ t("importFile")
1183
+ ] }),
1184
+ /* @__PURE__ */ jsx2(
1185
+ "input",
1186
+ {
1187
+ ref: fileInput,
1188
+ type: "file",
1189
+ accept: "application/json,.json",
1190
+ style: { display: "none" },
1191
+ onChange: (e) => {
1192
+ handleFile(e.target.files?.[0]);
1193
+ e.target.value = "";
1194
+ }
1195
+ }
1196
+ )
1197
+ ] })
1198
+ ] }),
817
1199
  /* @__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: [
1200
+ /* @__PURE__ */ jsxs2("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))", gap: 10 }, children: [
819
1201
  /* @__PURE__ */ jsx2(
820
1202
  PresetCard,
821
1203
  {
@@ -836,28 +1218,28 @@ function ThemePanelInner({ t }) {
836
1218
  ] }),
837
1219
  /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F308}", children: t("accentSection") }),
838
1220
  /* @__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") })
1221
+ /* @__PURE__ */ jsx2(
1222
+ ColorRow,
1223
+ {
1224
+ value: prefs.accentColor,
1225
+ onChange: (v) => update("accentColor", v),
1226
+ placeholder: t("accentPlaceholder")
1227
+ }
1228
+ ),
1229
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.55, marginTop: 6 }, children: t("accentHint") }),
1230
+ /* @__PURE__ */ jsx2("div", { style: { height: 14 } }),
1231
+ /* @__PURE__ */ jsx2(Field, { label: t("darkAccentSection"), hint: t("darkAccentHint"), children: /* @__PURE__ */ jsx2(
1232
+ ColorRow,
1233
+ {
1234
+ value: prefs.darkAccentColor,
1235
+ onChange: (v) => update("darkAccentColor", v),
1236
+ placeholder: t("accentPlaceholder")
1237
+ }
1238
+ ) }),
1239
+ contrastAdjusted && /* @__PURE__ */ jsx2("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsxs2(Badge, { color: "warning", children: [
1240
+ "\u26A0 ",
1241
+ t("preserveContrast")
1242
+ ] }) })
861
1243
  ] }),
862
1244
  /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F4D0}", children: t("densitySection") }),
863
1245
  /* @__PURE__ */ jsx2(
@@ -872,19 +1254,6 @@ function ThemePanelInner({ t }) {
872
1254
  ]
873
1255
  }
874
1256
  ),
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
1257
  /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u{1F524}", children: t("fontSection") }),
889
1258
  /* @__PURE__ */ jsx2(
890
1259
  Segmented,
@@ -898,6 +1267,23 @@ function ThemePanelInner({ t }) {
898
1267
  ]
899
1268
  }
900
1269
  ),
1270
+ /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u2699", children: t("behaviorSection") }),
1271
+ /* @__PURE__ */ jsxs2(Card, { children: [
1272
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
1273
+ /* @__PURE__ */ jsx2(
1274
+ "input",
1275
+ {
1276
+ id: "theme-animations",
1277
+ type: "checkbox",
1278
+ checked: prefs.animations,
1279
+ onChange: (e) => update("animations", e.target.checked),
1280
+ style: { width: 16, height: 16, cursor: "pointer" }
1281
+ }
1282
+ ),
1283
+ /* @__PURE__ */ jsx2("label", { htmlFor: "theme-animations", style: { fontSize: 13, cursor: "pointer" }, children: t("animations") })
1284
+ ] }),
1285
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, opacity: 0.55, marginTop: 6, marginLeft: 26 }, children: t("animationsHint") })
1286
+ ] }),
901
1287
  /* @__PURE__ */ jsx2(SectionTitle, { icon: "\u270F\uFE0F", children: t("customCssSection") }),
902
1288
  /* @__PURE__ */ jsx2(Card, { children: /* @__PURE__ */ jsx2(Field, { label: t("customCssSection"), hint: t("customCssHint"), children: /* @__PURE__ */ jsx2(
903
1289
  Textarea,
@@ -908,7 +1294,8 @@ function ThemePanelInner({ t }) {
908
1294
  rows: 4
909
1295
  }
910
1296
  ) }) }),
911
- /* @__PURE__ */ jsx2(PreviewArea, { t }),
1297
+ /* @__PURE__ */ jsx2(PreviewArea, { t, dark, accentApplied }),
1298
+ /* @__PURE__ */ jsx2(TokenCoverage, { t }),
912
1299
  confirmReset && /* @__PURE__ */ jsx2(
913
1300
  ConfirmDialog,
914
1301
  {