paperclip-theme 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.
@@ -0,0 +1,815 @@
1
+ // src/ui/index.tsx
2
+ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
3
+ import {
4
+ usePluginData,
5
+ usePluginAction
6
+ } from "@paperclipai/plugin-sdk/ui";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var MAX_VISIBLE_PRESETS = 8;
9
+ var CARD_WIDTH = 192;
10
+ var CARD_GAP = 10;
11
+ var SWATCH_TOKENS = ["--background", "--primary", "--accent", "--chart-1", "--destructive"];
12
+ var STYLE_ELEMENT_ID = "blazo-theme-overrides";
13
+ function injectThemeCSS(theme) {
14
+ let el = document.getElementById(STYLE_ELEMENT_ID);
15
+ if (!el) {
16
+ el = document.createElement("style");
17
+ el.id = STYLE_ELEMENT_ID;
18
+ document.head.appendChild(el);
19
+ }
20
+ const entries = Object.entries(theme.tokens);
21
+ const radiusEntry = theme.radius ? ` --radius: ${theme.radius};` : "";
22
+ const lines = entries.map(([key, value]) => ` ${key}: ${value};`);
23
+ if (radiusEntry) lines.push(radiusEntry);
24
+ const darkToggle = theme.isDark ? "dark" : "light";
25
+ el.textContent = `:root { ${lines.join(" ")} }
26
+ `;
27
+ const htmlEl = document.documentElement;
28
+ htmlEl.classList.toggle("dark", theme.isDark);
29
+ htmlEl.classList.toggle("light", !theme.isDark);
30
+ htmlEl.style.colorScheme = darkToggle;
31
+ }
32
+ var TOKEN_GROUPS = [
33
+ {
34
+ label: "Surfaces",
35
+ description: "Background and card colors",
36
+ tokens: ["--background", "--card", "--muted", "--accent"]
37
+ },
38
+ {
39
+ label: "Text",
40
+ description: "Foreground and label colors",
41
+ tokens: ["--foreground", "--card-foreground", "--muted-foreground", "--accent-foreground"]
42
+ },
43
+ {
44
+ label: "Interactive",
45
+ description: "Buttons, links, and primary actions",
46
+ tokens: ["--primary", "--primary-foreground"]
47
+ },
48
+ {
49
+ label: "Feedback",
50
+ description: "Errors and destructive actions",
51
+ tokens: ["--destructive", "--destructive-foreground"]
52
+ },
53
+ {
54
+ label: "Structure",
55
+ description: "Borders and focus rings",
56
+ tokens: ["--border", "--input", "--ring"]
57
+ },
58
+ {
59
+ label: "Charts",
60
+ description: "Dashboard visualization colors",
61
+ tokens: ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"]
62
+ }
63
+ ];
64
+ function tokenDisplayName(token) {
65
+ return token.replace(/^--/, "").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
66
+ }
67
+ function oklchToHex(oklchStr) {
68
+ const canvas = document.createElement("canvas");
69
+ canvas.width = 1;
70
+ canvas.height = 1;
71
+ const ctx2d = canvas.getContext("2d");
72
+ if (!ctx2d) return "#000000";
73
+ ctx2d.fillStyle = oklchStr;
74
+ ctx2d.fillRect(0, 0, 1, 1);
75
+ const [r, g, b] = ctx2d.getImageData(0, 0, 1, 1).data;
76
+ return `#${(r ?? 0).toString(16).padStart(2, "0")}${(g ?? 0).toString(16).padStart(2, "0")}${(b ?? 0).toString(16).padStart(2, "0")}`;
77
+ }
78
+ function hexToOklch(hex) {
79
+ const canvas = document.createElement("canvas");
80
+ canvas.width = 1;
81
+ canvas.height = 1;
82
+ const ctx2d = canvas.getContext("2d");
83
+ if (!ctx2d) return "oklch(0% 0 0)";
84
+ ctx2d.fillStyle = hex;
85
+ ctx2d.fillRect(0, 0, 1, 1);
86
+ const [r, g, b] = ctx2d.getImageData(0, 0, 1, 1).data;
87
+ const rLin = srgbToLinear((r ?? 0) / 255);
88
+ const gLin = srgbToLinear((g ?? 0) / 255);
89
+ const bLin = srgbToLinear((b ?? 0) / 255);
90
+ const l = 0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin;
91
+ const m = 0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin;
92
+ const s = 0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin;
93
+ const lRoot = Math.cbrt(l);
94
+ const mRoot = Math.cbrt(m);
95
+ const sRoot = Math.cbrt(s);
96
+ const L = 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot;
97
+ const a = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot;
98
+ const bOklab = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot;
99
+ const C = Math.sqrt(a * a + bOklab * bOklab);
100
+ let H = Math.atan2(bOklab, a) * 180 / Math.PI;
101
+ if (H < 0) H += 360;
102
+ return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(3)} ${H.toFixed(1)})`;
103
+ }
104
+ function srgbToLinear(c) {
105
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
106
+ }
107
+ var styles = {
108
+ root: {
109
+ fontFamily: "var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif)",
110
+ color: "var(--foreground)",
111
+ maxWidth: 720,
112
+ display: "flex",
113
+ flexDirection: "column",
114
+ gap: 32
115
+ },
116
+ header: {
117
+ display: "flex",
118
+ flexDirection: "column",
119
+ gap: 6
120
+ },
121
+ title: {
122
+ fontSize: 20,
123
+ fontWeight: 600,
124
+ letterSpacing: "-0.01em",
125
+ margin: 0,
126
+ color: "var(--foreground)"
127
+ },
128
+ subtitle: {
129
+ fontSize: 13,
130
+ color: "var(--muted-foreground)",
131
+ margin: 0,
132
+ lineHeight: 1.5
133
+ },
134
+ section: {
135
+ display: "flex",
136
+ flexDirection: "column",
137
+ gap: 16
138
+ },
139
+ sectionHeader: {
140
+ display: "flex",
141
+ alignItems: "center",
142
+ justifyContent: "space-between",
143
+ margin: 0
144
+ },
145
+ sectionLabel: {
146
+ fontSize: 13,
147
+ fontWeight: 600,
148
+ textTransform: "uppercase",
149
+ letterSpacing: "0.04em",
150
+ color: "var(--muted-foreground)",
151
+ margin: 0
152
+ },
153
+ seeAllBtn: {
154
+ fontSize: 12,
155
+ fontWeight: 500,
156
+ color: "var(--primary)",
157
+ background: "none",
158
+ border: "none",
159
+ cursor: "pointer",
160
+ padding: "2px 0",
161
+ outline: "none",
162
+ transition: "opacity 150ms ease"
163
+ },
164
+ presetsScroller: {
165
+ overflowX: "auto",
166
+ overflowY: "hidden",
167
+ WebkitOverflowScrolling: "touch",
168
+ scrollbarWidth: "thin",
169
+ paddingBottom: 4
170
+ },
171
+ presetsTrack: {
172
+ display: "grid",
173
+ gridTemplateRows: "1fr 1fr",
174
+ gridAutoFlow: "column",
175
+ gridAutoColumns: `${CARD_WIDTH}px`,
176
+ gap: CARD_GAP
177
+ },
178
+ presetCard: (isActive) => ({
179
+ position: "relative",
180
+ width: CARD_WIDTH,
181
+ padding: "12px 14px",
182
+ borderRadius: 8,
183
+ border: `1.5px solid ${isActive ? "var(--primary)" : "var(--border)"}`,
184
+ background: isActive ? "var(--accent)" : "transparent",
185
+ cursor: "pointer",
186
+ transition: "all 150ms ease",
187
+ display: "flex",
188
+ flexDirection: "column",
189
+ gap: 5,
190
+ outline: "none",
191
+ boxSizing: "border-box",
192
+ textAlign: "left"
193
+ }),
194
+ presetName: {
195
+ fontSize: 13,
196
+ fontWeight: 500,
197
+ color: "var(--foreground)",
198
+ margin: 0,
199
+ whiteSpace: "nowrap",
200
+ overflow: "hidden",
201
+ textOverflow: "ellipsis"
202
+ },
203
+ presetDesc: {
204
+ fontSize: 11,
205
+ color: "var(--muted-foreground)",
206
+ margin: 0,
207
+ lineHeight: 1.35,
208
+ display: "-webkit-box",
209
+ WebkitLineClamp: 2,
210
+ WebkitBoxOrient: "vertical",
211
+ overflow: "hidden"
212
+ },
213
+ presetMeta: {
214
+ display: "flex",
215
+ alignItems: "center",
216
+ justifyContent: "space-between",
217
+ marginTop: 2
218
+ },
219
+ presetColors: {
220
+ display: "flex",
221
+ gap: 3
222
+ },
223
+ presetSwatch: (color) => ({
224
+ width: 14,
225
+ height: 14,
226
+ borderRadius: 3,
227
+ background: color,
228
+ border: "1px solid rgba(128,128,128,0.25)",
229
+ flexShrink: 0
230
+ }),
231
+ presetModeBadge: (isDark) => ({
232
+ fontSize: 9,
233
+ fontWeight: 600,
234
+ textTransform: "uppercase",
235
+ letterSpacing: "0.06em",
236
+ color: "var(--muted-foreground)",
237
+ background: "var(--muted)",
238
+ padding: "1px 5px",
239
+ borderRadius: 3,
240
+ lineHeight: "16px"
241
+ }),
242
+ activeBadge: {
243
+ position: "absolute",
244
+ top: 7,
245
+ right: 8,
246
+ fontSize: 9,
247
+ fontWeight: 600,
248
+ textTransform: "uppercase",
249
+ letterSpacing: "0.06em",
250
+ color: "var(--primary-foreground)",
251
+ background: "var(--primary)",
252
+ padding: "1px 6px",
253
+ borderRadius: 999,
254
+ lineHeight: "16px"
255
+ },
256
+ tokenGroup: {
257
+ display: "flex",
258
+ flexDirection: "column",
259
+ gap: 10,
260
+ padding: "16px 0",
261
+ borderBottom: "1px solid var(--border)"
262
+ },
263
+ tokenGroupHeader: {
264
+ display: "flex",
265
+ flexDirection: "column",
266
+ gap: 2
267
+ },
268
+ tokenGroupLabel: {
269
+ fontSize: 14,
270
+ fontWeight: 500,
271
+ color: "var(--foreground)",
272
+ margin: 0
273
+ },
274
+ tokenGroupDesc: {
275
+ fontSize: 12,
276
+ color: "var(--muted-foreground)",
277
+ margin: 0
278
+ },
279
+ tokenRow: {
280
+ display: "flex",
281
+ alignItems: "center",
282
+ gap: 12,
283
+ padding: "4px 0"
284
+ },
285
+ colorInput: {
286
+ width: 32,
287
+ height: 32,
288
+ border: "1.5px solid var(--border)",
289
+ borderRadius: 6,
290
+ padding: 0,
291
+ cursor: "pointer",
292
+ background: "none",
293
+ flexShrink: 0,
294
+ outline: "none"
295
+ },
296
+ tokenLabel: {
297
+ fontSize: 13,
298
+ fontWeight: 400,
299
+ color: "var(--foreground)",
300
+ flex: 1,
301
+ minWidth: 0
302
+ },
303
+ tokenValue: {
304
+ fontSize: 11,
305
+ fontFamily: "var(--font-mono, 'SF Mono', Consolas, monospace)",
306
+ color: "var(--muted-foreground)",
307
+ textAlign: "right",
308
+ whiteSpace: "nowrap",
309
+ overflow: "hidden",
310
+ textOverflow: "ellipsis",
311
+ maxWidth: 180
312
+ },
313
+ radiusSection: {
314
+ display: "flex",
315
+ alignItems: "center",
316
+ gap: 16,
317
+ padding: "8px 0"
318
+ },
319
+ rangeInput: {
320
+ flex: 1,
321
+ accentColor: "var(--primary)",
322
+ cursor: "pointer"
323
+ },
324
+ radiusValue: {
325
+ fontSize: 13,
326
+ fontFamily: "var(--font-mono, 'SF Mono', Consolas, monospace)",
327
+ color: "var(--muted-foreground)",
328
+ minWidth: 48,
329
+ textAlign: "right"
330
+ },
331
+ radiusPreview: (r) => ({
332
+ width: 32,
333
+ height: 32,
334
+ borderRadius: r,
335
+ border: "2px solid var(--primary)",
336
+ background: "var(--accent)",
337
+ flexShrink: 0
338
+ }),
339
+ actions: {
340
+ display: "flex",
341
+ gap: 10,
342
+ paddingTop: 8
343
+ },
344
+ btnPrimary: {
345
+ padding: "9px 20px",
346
+ borderRadius: 6,
347
+ border: "none",
348
+ background: "var(--primary)",
349
+ color: "var(--primary-foreground)",
350
+ fontSize: 13,
351
+ fontWeight: 500,
352
+ cursor: "pointer",
353
+ transition: "opacity 150ms ease",
354
+ outline: "none"
355
+ },
356
+ btnSecondary: {
357
+ padding: "9px 20px",
358
+ borderRadius: 6,
359
+ border: "1.5px solid var(--border)",
360
+ background: "transparent",
361
+ color: "var(--foreground)",
362
+ fontSize: 13,
363
+ fontWeight: 500,
364
+ cursor: "pointer",
365
+ transition: "all 150ms ease",
366
+ outline: "none"
367
+ },
368
+ saved: {
369
+ fontSize: 12,
370
+ color: "var(--muted-foreground)",
371
+ alignSelf: "center",
372
+ transition: "opacity 300ms ease"
373
+ },
374
+ overlay: {
375
+ position: "fixed",
376
+ inset: 0,
377
+ background: "rgba(0,0,0,0.55)",
378
+ display: "flex",
379
+ alignItems: "center",
380
+ justifyContent: "center",
381
+ zIndex: 9999,
382
+ backdropFilter: "blur(4px)"
383
+ },
384
+ modal: {
385
+ background: "var(--card)",
386
+ border: "1px solid var(--border)",
387
+ borderRadius: 12,
388
+ width: "min(640px, calc(100vw - 48px))",
389
+ maxHeight: "min(560px, calc(100vh - 80px))",
390
+ display: "flex",
391
+ flexDirection: "column",
392
+ boxShadow: "0 16px 48px rgba(0,0,0,0.25)",
393
+ overflow: "hidden"
394
+ },
395
+ modalHeader: {
396
+ display: "flex",
397
+ alignItems: "center",
398
+ justifyContent: "space-between",
399
+ padding: "16px 20px 0 20px"
400
+ },
401
+ modalTitle: {
402
+ fontSize: 16,
403
+ fontWeight: 600,
404
+ margin: 0,
405
+ color: "var(--foreground)"
406
+ },
407
+ modalCloseBtn: {
408
+ width: 28,
409
+ height: 28,
410
+ display: "flex",
411
+ alignItems: "center",
412
+ justifyContent: "center",
413
+ background: "none",
414
+ border: "none",
415
+ borderRadius: 6,
416
+ cursor: "pointer",
417
+ color: "var(--muted-foreground)",
418
+ fontSize: 18,
419
+ lineHeight: 1,
420
+ outline: "none",
421
+ transition: "background 150ms ease"
422
+ },
423
+ modalSearch: {
424
+ margin: "12px 20px",
425
+ padding: "8px 12px",
426
+ borderRadius: 6,
427
+ border: "1.5px solid var(--border)",
428
+ background: "var(--muted)",
429
+ color: "var(--foreground)",
430
+ fontSize: 13,
431
+ outline: "none",
432
+ width: "calc(100% - 40px)",
433
+ boxSizing: "border-box",
434
+ transition: "border-color 150ms ease"
435
+ },
436
+ modalCount: {
437
+ fontSize: 11,
438
+ color: "var(--muted-foreground)",
439
+ padding: "0 20px 8px 20px",
440
+ margin: 0
441
+ },
442
+ modalGrid: {
443
+ flex: 1,
444
+ overflowY: "auto",
445
+ padding: "0 20px 20px 20px",
446
+ display: "grid",
447
+ gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
448
+ gap: 10,
449
+ alignContent: "start"
450
+ }
451
+ };
452
+ function PresetCard({
453
+ preset,
454
+ isActive,
455
+ onSelect,
456
+ compact
457
+ }) {
458
+ const cardStyle = compact ? {
459
+ ...styles.presetCard(isActive),
460
+ width: "auto"
461
+ } : styles.presetCard(isActive);
462
+ return /* @__PURE__ */ jsxs(
463
+ "button",
464
+ {
465
+ type: "button",
466
+ style: cardStyle,
467
+ onClick: onSelect,
468
+ onMouseEnter: (e) => {
469
+ if (!isActive) e.currentTarget.style.borderColor = "var(--muted-foreground)";
470
+ },
471
+ onMouseLeave: (e) => {
472
+ if (!isActive) e.currentTarget.style.borderColor = "var(--border)";
473
+ },
474
+ children: [
475
+ isActive && /* @__PURE__ */ jsx("span", { style: styles.activeBadge, children: "Active" }),
476
+ /* @__PURE__ */ jsx("p", { style: styles.presetName, children: preset.name }),
477
+ /* @__PURE__ */ jsx("p", { style: styles.presetDesc, children: preset.description }),
478
+ /* @__PURE__ */ jsxs("div", { style: styles.presetMeta, children: [
479
+ /* @__PURE__ */ jsx("div", { style: styles.presetColors, children: SWATCH_TOKENS.map((t) => /* @__PURE__ */ jsx("div", { style: styles.presetSwatch(preset.tokens[t] ?? "#333") }, t)) }),
480
+ /* @__PURE__ */ jsx("span", { style: styles.presetModeBadge(preset.isDark), children: preset.isDark ? "Dark" : "Light" })
481
+ ] })
482
+ ]
483
+ }
484
+ );
485
+ }
486
+ function PresetModal({
487
+ presets,
488
+ activeId,
489
+ onSelect,
490
+ onClose
491
+ }) {
492
+ const [search, setSearch] = useState("");
493
+ const searchRef = useRef(null);
494
+ useEffect(() => {
495
+ searchRef.current?.focus();
496
+ }, []);
497
+ useEffect(() => {
498
+ const handleKey = (e) => {
499
+ if (e.key === "Escape") onClose();
500
+ };
501
+ document.addEventListener("keydown", handleKey);
502
+ return () => document.removeEventListener("keydown", handleKey);
503
+ }, [onClose]);
504
+ const filtered = useMemo(() => {
505
+ if (!search.trim()) return presets;
506
+ const q = search.toLowerCase().trim();
507
+ return presets.filter(
508
+ (p) => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q) || p.author.toLowerCase().includes(q) || (p.isDark ? "dark" : "light").includes(q)
509
+ );
510
+ }, [presets, search]);
511
+ return /* @__PURE__ */ jsx(
512
+ "div",
513
+ {
514
+ style: styles.overlay,
515
+ onClick: (e) => {
516
+ if (e.target === e.currentTarget) onClose();
517
+ },
518
+ children: /* @__PURE__ */ jsxs("div", { style: styles.modal, children: [
519
+ /* @__PURE__ */ jsxs("div", { style: styles.modalHeader, children: [
520
+ /* @__PURE__ */ jsx("h3", { style: styles.modalTitle, children: "All Themes" }),
521
+ /* @__PURE__ */ jsx(
522
+ "button",
523
+ {
524
+ type: "button",
525
+ style: styles.modalCloseBtn,
526
+ onClick: onClose,
527
+ onMouseEnter: (e) => {
528
+ e.currentTarget.style.background = "var(--muted)";
529
+ },
530
+ onMouseLeave: (e) => {
531
+ e.currentTarget.style.background = "none";
532
+ },
533
+ children: "\u2715"
534
+ }
535
+ )
536
+ ] }),
537
+ /* @__PURE__ */ jsx(
538
+ "input",
539
+ {
540
+ ref: searchRef,
541
+ type: "text",
542
+ placeholder: "Search themes...",
543
+ value: search,
544
+ onChange: (e) => setSearch(e.target.value),
545
+ style: styles.modalSearch,
546
+ onFocus: (e) => {
547
+ e.currentTarget.style.borderColor = "var(--primary)";
548
+ },
549
+ onBlur: (e) => {
550
+ e.currentTarget.style.borderColor = "var(--border)";
551
+ }
552
+ }
553
+ ),
554
+ /* @__PURE__ */ jsxs("p", { style: styles.modalCount, children: [
555
+ filtered.length,
556
+ " of ",
557
+ presets.length,
558
+ " themes"
559
+ ] }),
560
+ /* @__PURE__ */ jsxs("div", { style: styles.modalGrid, children: [
561
+ filtered.map((preset) => /* @__PURE__ */ jsx(
562
+ PresetCard,
563
+ {
564
+ preset,
565
+ isActive: activeId === preset.id,
566
+ onSelect: () => {
567
+ onSelect(preset);
568
+ onClose();
569
+ },
570
+ compact: true
571
+ },
572
+ preset.id
573
+ )),
574
+ filtered.length === 0 && /* @__PURE__ */ jsx("p", { style: { ...styles.subtitle, gridColumn: "1 / -1", textAlign: "center", padding: "32px 0" }, children: "No themes match your search." })
575
+ ] })
576
+ ] })
577
+ }
578
+ );
579
+ }
580
+ function TokenRow({
581
+ token,
582
+ value,
583
+ onChange
584
+ }) {
585
+ const hex = oklchToHex(value);
586
+ return /* @__PURE__ */ jsxs("div", { style: styles.tokenRow, children: [
587
+ /* @__PURE__ */ jsx(
588
+ "input",
589
+ {
590
+ type: "color",
591
+ style: styles.colorInput,
592
+ value: hex,
593
+ onChange: (e) => {
594
+ const newOklch = hexToOklch(e.target.value);
595
+ onChange(token, newOklch);
596
+ },
597
+ title: tokenDisplayName(token)
598
+ }
599
+ ),
600
+ /* @__PURE__ */ jsx("span", { style: styles.tokenLabel, children: tokenDisplayName(token) }),
601
+ /* @__PURE__ */ jsx("span", { style: styles.tokenValue, children: value })
602
+ ] });
603
+ }
604
+ function ThemeSettingsPage() {
605
+ const activeThemeResult = usePluginData("active-theme");
606
+ const presetsResult = usePluginData("presets");
607
+ const applyTheme = usePluginAction("apply-theme");
608
+ const resetTheme = usePluginAction("reset-theme");
609
+ const [localTheme, setLocalTheme] = useState(null);
610
+ const [saving, setSaving] = useState(false);
611
+ const [savedAt, setSavedAt] = useState(null);
612
+ const [hasUnsaved, setHasUnsaved] = useState(false);
613
+ const [showModal, setShowModal] = useState(false);
614
+ const initialLoadDone = useRef(false);
615
+ const presets = presetsResult.data ?? [];
616
+ const serverTheme = activeThemeResult.data ?? null;
617
+ const visiblePresets = presets.slice(0, MAX_VISIBLE_PRESETS);
618
+ const hasMore = presets.length > MAX_VISIBLE_PRESETS;
619
+ useEffect(() => {
620
+ if (initialLoadDone.current) return;
621
+ if (serverTheme) {
622
+ setLocalTheme(serverTheme);
623
+ injectThemeCSS(serverTheme);
624
+ initialLoadDone.current = true;
625
+ } else if (presets.length > 0 && activeThemeResult.data === null) {
626
+ initialLoadDone.current = true;
627
+ }
628
+ }, [serverTheme, presets, activeThemeResult.data]);
629
+ const selectPreset = useCallback(
630
+ (preset) => {
631
+ const next = { ...preset, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
632
+ setLocalTheme(next);
633
+ injectThemeCSS(next);
634
+ setHasUnsaved(true);
635
+ setSavedAt(null);
636
+ },
637
+ []
638
+ );
639
+ const updateToken = useCallback(
640
+ (token, value) => {
641
+ setLocalTheme((prev) => {
642
+ if (!prev) return prev;
643
+ const next = {
644
+ ...prev,
645
+ tokens: { ...prev.tokens, [token]: value },
646
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
647
+ };
648
+ injectThemeCSS(next);
649
+ return next;
650
+ });
651
+ setHasUnsaved(true);
652
+ setSavedAt(null);
653
+ },
654
+ []
655
+ );
656
+ const updateRadius = useCallback((value) => {
657
+ setLocalTheme((prev) => {
658
+ if (!prev) return prev;
659
+ const next = { ...prev, radius: value, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
660
+ injectThemeCSS(next);
661
+ return next;
662
+ });
663
+ setHasUnsaved(true);
664
+ setSavedAt(null);
665
+ }, []);
666
+ const handleSave = useCallback(async () => {
667
+ if (!localTheme) return;
668
+ setSaving(true);
669
+ try {
670
+ await applyTheme(localTheme);
671
+ setHasUnsaved(false);
672
+ setSavedAt((/* @__PURE__ */ new Date()).toLocaleTimeString());
673
+ } catch (err) {
674
+ console.error("Failed to save theme:", err);
675
+ } finally {
676
+ setSaving(false);
677
+ }
678
+ }, [localTheme, applyTheme]);
679
+ const handleReset = useCallback(async () => {
680
+ setSaving(true);
681
+ try {
682
+ const result = await resetTheme({});
683
+ const restored = result;
684
+ setLocalTheme(restored);
685
+ injectThemeCSS(restored);
686
+ setHasUnsaved(false);
687
+ setSavedAt(null);
688
+ } catch (err) {
689
+ console.error("Failed to reset theme:", err);
690
+ } finally {
691
+ setSaving(false);
692
+ }
693
+ }, [resetTheme]);
694
+ const radiusNum = parseFloat(localTheme?.radius ?? "0") || 0;
695
+ return /* @__PURE__ */ jsxs("div", { style: styles.root, children: [
696
+ /* @__PURE__ */ jsxs("div", { style: styles.header, children: [
697
+ /* @__PURE__ */ jsx("h2", { style: styles.title, children: "Theme" }),
698
+ /* @__PURE__ */ jsx("p", { style: styles.subtitle, children: "Choose a preset or fine-tune individual design tokens. Changes preview instantly." })
699
+ ] }),
700
+ /* @__PURE__ */ jsxs("div", { style: styles.section, children: [
701
+ /* @__PURE__ */ jsxs("div", { style: styles.sectionHeader, children: [
702
+ /* @__PURE__ */ jsx("p", { style: styles.sectionLabel, children: "Presets" }),
703
+ hasMore && /* @__PURE__ */ jsxs(
704
+ "button",
705
+ {
706
+ type: "button",
707
+ style: styles.seeAllBtn,
708
+ onClick: () => setShowModal(true),
709
+ onMouseEnter: (e) => {
710
+ e.currentTarget.style.opacity = "0.7";
711
+ },
712
+ onMouseLeave: (e) => {
713
+ e.currentTarget.style.opacity = "1";
714
+ },
715
+ children: [
716
+ "See all ",
717
+ presets.length,
718
+ " themes"
719
+ ]
720
+ }
721
+ )
722
+ ] }),
723
+ /* @__PURE__ */ jsx("div", { style: styles.presetsScroller, children: /* @__PURE__ */ jsx("div", { style: styles.presetsTrack, children: visiblePresets.map((preset) => /* @__PURE__ */ jsx(
724
+ PresetCard,
725
+ {
726
+ preset,
727
+ isActive: localTheme?.id === preset.id,
728
+ onSelect: () => selectPreset(preset)
729
+ },
730
+ preset.id
731
+ )) }) })
732
+ ] }),
733
+ showModal && /* @__PURE__ */ jsx(
734
+ PresetModal,
735
+ {
736
+ presets,
737
+ activeId: localTheme?.id,
738
+ onSelect: selectPreset,
739
+ onClose: () => setShowModal(false)
740
+ }
741
+ ),
742
+ localTheme && /* @__PURE__ */ jsxs("div", { style: styles.section, children: [
743
+ /* @__PURE__ */ jsx("p", { style: styles.sectionLabel, children: "Design Tokens" }),
744
+ TOKEN_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { style: styles.tokenGroup, children: [
745
+ /* @__PURE__ */ jsxs("div", { style: styles.tokenGroupHeader, children: [
746
+ /* @__PURE__ */ jsx("p", { style: styles.tokenGroupLabel, children: group.label }),
747
+ /* @__PURE__ */ jsx("p", { style: styles.tokenGroupDesc, children: group.description })
748
+ ] }),
749
+ group.tokens.map((token) => /* @__PURE__ */ jsx(
750
+ TokenRow,
751
+ {
752
+ token,
753
+ value: localTheme.tokens[token] ?? "",
754
+ onChange: updateToken
755
+ },
756
+ token
757
+ ))
758
+ ] }, group.label))
759
+ ] }),
760
+ localTheme && /* @__PURE__ */ jsxs("div", { style: styles.section, children: [
761
+ /* @__PURE__ */ jsx("p", { style: styles.sectionLabel, children: "Border Radius" }),
762
+ /* @__PURE__ */ jsxs("div", { style: styles.radiusSection, children: [
763
+ /* @__PURE__ */ jsx("div", { style: styles.radiusPreview(localTheme.radius || "0") }),
764
+ /* @__PURE__ */ jsx(
765
+ "input",
766
+ {
767
+ type: "range",
768
+ min: "0",
769
+ max: "1.5",
770
+ step: "0.125",
771
+ value: radiusNum,
772
+ onChange: (e) => updateRadius(`${e.target.value}rem`),
773
+ style: styles.rangeInput
774
+ }
775
+ ),
776
+ /* @__PURE__ */ jsx("span", { style: styles.radiusValue, children: localTheme.radius || "0" })
777
+ ] })
778
+ ] }),
779
+ /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
780
+ /* @__PURE__ */ jsx(
781
+ "button",
782
+ {
783
+ type: "button",
784
+ style: {
785
+ ...styles.btnPrimary,
786
+ opacity: saving || !hasUnsaved ? 0.5 : 1,
787
+ pointerEvents: saving || !hasUnsaved ? "none" : "auto"
788
+ },
789
+ onClick: handleSave,
790
+ disabled: saving || !hasUnsaved,
791
+ children: saving ? "Saving\u2026" : "Save Theme"
792
+ }
793
+ ),
794
+ /* @__PURE__ */ jsx(
795
+ "button",
796
+ {
797
+ type: "button",
798
+ style: styles.btnSecondary,
799
+ onClick: handleReset,
800
+ disabled: saving,
801
+ children: "Reset to Default"
802
+ }
803
+ ),
804
+ savedAt && /* @__PURE__ */ jsxs("span", { style: styles.saved, children: [
805
+ "Saved at ",
806
+ savedAt
807
+ ] }),
808
+ hasUnsaved && !savedAt && /* @__PURE__ */ jsx("span", { style: { ...styles.saved, color: "var(--chart-1)" }, children: "Unsaved changes" })
809
+ ] })
810
+ ] });
811
+ }
812
+ export {
813
+ ThemeSettingsPage
814
+ };
815
+ //# sourceMappingURL=index.js.map