@designtools/shadows 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,315 @@
1
+ import { useState, useRef } from "react";
2
+ import {
3
+ ChevronRightIcon,
4
+ ChevronDownIcon,
5
+ Pencil1Icon,
6
+ BookmarkIcon,
7
+ CheckIcon,
8
+ } from "@radix-ui/react-icons";
9
+ import { ShadowControls } from "./shadow-controls.js";
10
+ import { ShadowPreview } from "./shadow-preview.js";
11
+
12
+ interface ShadowListProps {
13
+ shadows: any[];
14
+ cssFilePath: string;
15
+ stylingType: string;
16
+ onPreviewShadow: (variableName: string, value: string) => void;
17
+ }
18
+
19
+ export function ShadowList({
20
+ shadows,
21
+ cssFilePath,
22
+ stylingType,
23
+ onPreviewShadow,
24
+ }: ShadowListProps) {
25
+ const [expandedShadow, setExpandedShadow] = useState<string | null>(null);
26
+ const [editMode, setEditMode] = useState<Record<string, "custom" | "preset">>({});
27
+
28
+ return (
29
+ <div>
30
+ {shadows.map((shadow: any) => (
31
+ <ShadowRow
32
+ key={shadow.name}
33
+ shadow={shadow}
34
+ isExpanded={expandedShadow === shadow.name}
35
+ onToggle={() =>
36
+ setExpandedShadow(
37
+ expandedShadow === shadow.name ? null : shadow.name
38
+ )
39
+ }
40
+ mode={editMode[shadow.name] || "custom"}
41
+ onModeChange={(mode) =>
42
+ setEditMode({ ...editMode, [shadow.name]: mode })
43
+ }
44
+ cssFilePath={cssFilePath}
45
+ stylingType={stylingType}
46
+ onPreviewShadow={onPreviewShadow}
47
+ />
48
+ ))}
49
+
50
+ {shadows.length === 0 && (
51
+ <div
52
+ className="px-4 py-6 text-center text-[11px]"
53
+ style={{ color: "var(--studio-text-dimmed)" }}
54
+ >
55
+ No shadows found. Add shadow CSS variables to your global CSS file.
56
+ </div>
57
+ )}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function ShadowRow({
63
+ shadow,
64
+ isExpanded,
65
+ onToggle,
66
+ mode,
67
+ onModeChange,
68
+ cssFilePath,
69
+ stylingType,
70
+ onPreviewShadow,
71
+ }: {
72
+ shadow: any;
73
+ isExpanded: boolean;
74
+ onToggle: () => void;
75
+ mode: "custom" | "preset";
76
+ onModeChange: (mode: "custom" | "preset") => void;
77
+ cssFilePath: string;
78
+ stylingType: string;
79
+ onPreviewShadow: (variableName: string, value: string) => void;
80
+ }) {
81
+ const [saving, setSaving] = useState(false);
82
+
83
+ const handleSave = async (newValue: string) => {
84
+ setSaving(true);
85
+ try {
86
+ // Design token shadows write to their own endpoint
87
+ if (shadow.source === "design-token" && shadow.tokenFilePath && shadow.tokenPath) {
88
+ const res = await fetch("/api/shadows/design-token", {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify({
92
+ filePath: shadow.tokenFilePath,
93
+ tokenPath: shadow.tokenPath,
94
+ value: newValue,
95
+ }),
96
+ });
97
+ const data = await res.json();
98
+ if (!data.ok) console.error("Design token save failed:", data.error);
99
+ } else {
100
+ // CSS/SCSS shadow writes
101
+ const variableName = shadow.sassVariable || shadow.cssVariable || `--${shadow.name}`;
102
+ let selector: string;
103
+ let filePath = cssFilePath;
104
+
105
+ if (stylingType === "tailwind-v4") {
106
+ selector = "@theme";
107
+ } else if (stylingType === "bootstrap" && shadow.sassVariable) {
108
+ selector = "scss";
109
+ // Use the SCSS file if available, fall back to CSS file
110
+ filePath = shadow.filePath || cssFilePath;
111
+ } else {
112
+ selector = ":root";
113
+ }
114
+
115
+ const endpoint = shadow.isOverridden ? "/api/shadows" : "/api/shadows/create";
116
+ const res = await fetch(endpoint, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({
120
+ filePath,
121
+ variableName,
122
+ value: newValue,
123
+ selector,
124
+ }),
125
+ });
126
+ const data = await res.json();
127
+ if (!data.ok) console.error("Shadow save failed:", data.error);
128
+ }
129
+ } catch (err) {
130
+ console.error("Shadow save error:", err);
131
+ }
132
+ setTimeout(() => setSaving(false), 1000);
133
+ };
134
+
135
+ return (
136
+ <div style={{ borderTop: "1px solid var(--studio-border-subtle)" }}>
137
+ <button
138
+ onClick={onToggle}
139
+ className="studio-section-hdr"
140
+ style={{ gap: 8 }}
141
+ >
142
+ {isExpanded ? <ChevronDownIcon /> : <ChevronRightIcon />}
143
+ <ShadowPreview value={shadow.value} size={24} />
144
+ <span
145
+ className="flex-1 text-[11px] font-mono truncate text-left"
146
+ style={{
147
+ color: "var(--studio-text)",
148
+ fontWeight: 500,
149
+ textTransform: "none",
150
+ letterSpacing: 0,
151
+ }}
152
+ >
153
+ {shadow.name}
154
+ </span>
155
+ {shadow.source === "framework-preset" && !shadow.isOverridden && (
156
+ <span
157
+ className="text-[9px] px-1.5 py-0.5 rounded"
158
+ style={{
159
+ background: "var(--studio-input-bg)",
160
+ color: "var(--studio-text-dimmed)",
161
+ }}
162
+ >
163
+ preset
164
+ </span>
165
+ )}
166
+ {shadow.source === "design-token" && (
167
+ <span
168
+ className="text-[9px] px-1.5 py-0.5 rounded"
169
+ style={{
170
+ background: "var(--studio-input-bg)",
171
+ color: "var(--studio-text-dimmed)",
172
+ }}
173
+ >
174
+ token
175
+ </span>
176
+ )}
177
+ {saving && (
178
+ <span
179
+ className="flex items-center gap-0.5 text-[10px]"
180
+ style={{ color: "var(--studio-success)" }}
181
+ >
182
+ <CheckIcon style={{ width: 10, height: 10 }} />
183
+ </span>
184
+ )}
185
+ </button>
186
+
187
+ {isExpanded && (
188
+ <div className="px-4 pb-3">
189
+ {/* Edit mode toggle */}
190
+ <div className="studio-segmented mb-3" style={{ width: "100%" }}>
191
+ <button
192
+ onClick={() => onModeChange("custom")}
193
+ className={mode === "custom" ? "active" : ""}
194
+ style={{ flex: 1 }}
195
+ >
196
+ <Pencil1Icon style={{ width: 12, height: 12 }} />
197
+ Custom
198
+ </button>
199
+ <button
200
+ onClick={() => onModeChange("preset")}
201
+ className={mode === "preset" ? "active" : ""}
202
+ style={{ flex: 1 }}
203
+ >
204
+ <BookmarkIcon style={{ width: 12, height: 12 }} />
205
+ Use Preset
206
+ </button>
207
+ </div>
208
+
209
+ {mode === "custom" ? (
210
+ <ShadowControls
211
+ shadow={shadow}
212
+ onPreview={(value) => {
213
+ const varName = shadow.cssVariable || `--${shadow.name}`;
214
+ onPreviewShadow(varName, value);
215
+ }}
216
+ onSave={handleSave}
217
+ />
218
+ ) : (
219
+ <PresetPicker
220
+ currentValue={shadow.value}
221
+ onSelect={(value) => {
222
+ const varName = shadow.cssVariable || `--${shadow.name}`;
223
+ onPreviewShadow(varName, value);
224
+ handleSave(value);
225
+ }}
226
+ />
227
+ )}
228
+ </div>
229
+ )}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ function PresetPicker({
235
+ currentValue,
236
+ onSelect,
237
+ }: {
238
+ currentValue: string;
239
+ onSelect: (value: string) => void;
240
+ }) {
241
+ // Import presets inline to avoid circular deps
242
+ const presets = [
243
+ { name: "Soft Small", value: "0 1px 2px 0 rgb(0 0 0 / 0.03), 0 1px 3px 0 rgb(0 0 0 / 0.06)", category: "subtle" },
244
+ { name: "Soft Medium", value: "0 2px 8px -2px rgb(0 0 0 / 0.05), 0 4px 12px -2px rgb(0 0 0 / 0.08)", category: "subtle" },
245
+ { name: "Soft Large", value: "0 4px 16px -4px rgb(0 0 0 / 0.08), 0 8px 24px -4px rgb(0 0 0 / 0.1)", category: "subtle" },
246
+ { name: "Card", value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", category: "medium" },
247
+ { name: "Dropdown", value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", category: "medium" },
248
+ { name: "Modal", value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)", category: "medium" },
249
+ { name: "Elevated", value: "0 25px 50px -12px rgb(0 0 0 / 0.25)", category: "dramatic" },
250
+ { name: "Floating", value: "0 20px 60px -15px rgb(0 0 0 / 0.3)", category: "dramatic" },
251
+ { name: "Layered Small", value: "0 1px 1px rgb(0 0 0 / 0.04), 0 2px 2px rgb(0 0 0 / 0.04), 0 4px 4px rgb(0 0 0 / 0.04)", category: "layered" },
252
+ { name: "Layered Medium", value: "0 1px 1px rgb(0 0 0 / 0.03), 0 2px 2px rgb(0 0 0 / 0.03), 0 4px 4px rgb(0 0 0 / 0.03), 0 8px 8px rgb(0 0 0 / 0.03), 0 16px 16px rgb(0 0 0 / 0.03)", category: "layered" },
253
+ { name: "Blue Glow", value: "0 4px 14px 0 rgb(59 130 246 / 0.3)", category: "colored" },
254
+ { name: "Purple Glow", value: "0 4px 14px 0 rgb(147 51 234 / 0.3)", category: "colored" },
255
+ { name: "None", value: "none", category: "reset" },
256
+ ];
257
+
258
+ const categories = ["subtle", "medium", "dramatic", "layered", "colored", "reset"];
259
+ const categoryLabels: Record<string, string> = {
260
+ subtle: "Subtle",
261
+ medium: "Medium",
262
+ dramatic: "Dramatic",
263
+ layered: "Layered",
264
+ colored: "Colored",
265
+ reset: "Reset",
266
+ };
267
+
268
+ return (
269
+ <div className="flex flex-col gap-2">
270
+ {categories.map((cat) => {
271
+ const catPresets = presets.filter((p) => p.category === cat);
272
+ if (catPresets.length === 0) return null;
273
+ return (
274
+ <div key={cat}>
275
+ <div
276
+ className="text-[9px] font-semibold uppercase tracking-wide mb-1"
277
+ style={{ color: "var(--studio-text-dimmed)" }}
278
+ >
279
+ {categoryLabels[cat]}
280
+ </div>
281
+ <div className="flex flex-col gap-1">
282
+ {catPresets.map((preset) => (
283
+ <button
284
+ key={preset.name}
285
+ onClick={() => onSelect(preset.value)}
286
+ className="flex items-center gap-2 p-1.5 rounded text-left w-full"
287
+ style={{
288
+ background:
289
+ currentValue === preset.value
290
+ ? "var(--studio-accent-muted)"
291
+ : "transparent",
292
+ border: "1px solid",
293
+ borderColor:
294
+ currentValue === preset.value
295
+ ? "var(--studio-accent)"
296
+ : "var(--studio-border-subtle)",
297
+ cursor: "pointer",
298
+ }}
299
+ >
300
+ <ShadowPreview value={preset.value} size={32} />
301
+ <span
302
+ className="text-[11px]"
303
+ style={{ color: "var(--studio-text)" }}
304
+ >
305
+ {preset.name}
306
+ </span>
307
+ </button>
308
+ ))}
309
+ </div>
310
+ </div>
311
+ );
312
+ })}
313
+ </div>
314
+ );
315
+ }
@@ -0,0 +1,106 @@
1
+ import { useState } from "react";
2
+ import { ShadowPreview } from "./shadow-preview.js";
3
+
4
+ interface ShadowOverviewProps {
5
+ shadows: any[];
6
+ }
7
+
8
+ const BACKGROUNDS = [
9
+ { name: "White", value: "white" },
10
+ { name: "Light", value: "#f5f5f5" },
11
+ { name: "Dark", value: "#1a1a2e" },
12
+ { name: "Blue", value: "#e0f2fe" },
13
+ { name: "Warm", value: "#fef3c7" },
14
+ ];
15
+
16
+ export function ShadowOverview({ shadows }: ShadowOverviewProps) {
17
+ const [activeBg, setActiveBg] = useState("white");
18
+ const [previewSize, setPreviewSize] = useState(64);
19
+
20
+ return (
21
+ <div>
22
+ {/* Background selector */}
23
+ <div className="px-4 py-2 flex items-center gap-2">
24
+ <span
25
+ className="text-[9px] font-semibold uppercase tracking-wide shrink-0"
26
+ style={{ color: "var(--studio-text-dimmed)" }}
27
+ >
28
+ BG
29
+ </span>
30
+ <div className="flex gap-1">
31
+ {BACKGROUNDS.map((bg) => (
32
+ <button
33
+ key={bg.value}
34
+ onClick={() => setActiveBg(bg.value)}
35
+ className="shrink-0 cursor-pointer"
36
+ title={bg.name}
37
+ style={{
38
+ width: 18,
39
+ height: 18,
40
+ borderRadius: 3,
41
+ background: bg.value,
42
+ border:
43
+ activeBg === bg.value
44
+ ? "2px solid var(--studio-accent)"
45
+ : "1px solid var(--studio-border)",
46
+ }}
47
+ />
48
+ ))}
49
+ </div>
50
+ <div className="flex-1" />
51
+ <select
52
+ value={previewSize}
53
+ onChange={(e) => setPreviewSize(parseInt(e.target.value))}
54
+ className="studio-select"
55
+ style={{ fontSize: 10, width: 50 }}
56
+ >
57
+ <option value={48}>S</option>
58
+ <option value={64}>M</option>
59
+ <option value={96}>L</option>
60
+ </select>
61
+ </div>
62
+
63
+ {/* Shadow grid */}
64
+ <div
65
+ className="grid gap-4 px-4 py-3"
66
+ style={{
67
+ gridTemplateColumns: `repeat(auto-fill, minmax(${previewSize + 24}px, 1fr))`,
68
+ }}
69
+ >
70
+ {shadows.map((shadow: any) => (
71
+ <div key={shadow.name} className="flex flex-col items-center gap-1.5">
72
+ <div
73
+ className="flex items-center justify-center rounded-lg p-3"
74
+ style={{
75
+ background: activeBg,
76
+ width: previewSize + 24,
77
+ height: previewSize + 24,
78
+ }}
79
+ >
80
+ <ShadowPreview
81
+ value={shadow.value}
82
+ size={previewSize}
83
+ background={activeBg}
84
+ />
85
+ </div>
86
+ <span
87
+ className="text-[9px] font-mono text-center truncate w-full"
88
+ style={{ color: "var(--studio-text-muted)" }}
89
+ >
90
+ {shadow.name}
91
+ </span>
92
+ </div>
93
+ ))}
94
+ </div>
95
+
96
+ {shadows.length === 0 && (
97
+ <div
98
+ className="px-4 py-6 text-center text-[11px]"
99
+ style={{ color: "var(--studio-text-dimmed)" }}
100
+ >
101
+ No shadows to display
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,30 @@
1
+ interface ShadowPreviewProps {
2
+ value: string;
3
+ size?: number;
4
+ shape?: "square" | "rounded" | "circle";
5
+ background?: string;
6
+ }
7
+
8
+ export function ShadowPreview({
9
+ value,
10
+ size = 32,
11
+ shape = "rounded",
12
+ background = "white",
13
+ }: ShadowPreviewProps) {
14
+ const borderRadius =
15
+ shape === "circle" ? "50%" : shape === "rounded" ? "4px" : "0";
16
+
17
+ return (
18
+ <div
19
+ className="shrink-0"
20
+ style={{
21
+ width: size,
22
+ height: size,
23
+ borderRadius,
24
+ background,
25
+ boxShadow: value,
26
+ border: "1px solid var(--studio-border-subtle)",
27
+ }}
28
+ />
29
+ );
30
+ }