@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.
- package/dist/cli.js +1393 -0
- package/package.json +43 -0
- package/src/client/app.tsx +109 -0
- package/src/client/components/shadow-controls.tsx +281 -0
- package/src/client/components/shadow-editor-panel.tsx +137 -0
- package/src/client/components/shadow-list.tsx +315 -0
- package/src/client/components/shadow-overview.tsx +106 -0
- package/src/client/components/shadow-preview.tsx +30 -0
- package/src/client/dist/assets/index-B06i4I-S.js +49 -0
- package/src/client/dist/assets/index-DL62bVtu.css +1 -0
- package/src/client/dist/index.html +13 -0
- package/src/client/index.html +13 -0
- package/src/client/main.tsx +5 -0
- package/src/client/styles.css +1 -0
|
@@ -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
|
+
}
|