@designtools/shadows 0.1.11 → 0.1.12

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.
@@ -1,287 +0,0 @@
1
- import { useState, useEffect, useCallback } from "react";
2
- import {
3
- PlusIcon,
4
- TrashIcon,
5
- MoveIcon,
6
- } from "@radix-ui/react-icons";
7
-
8
- interface ShadowLayer {
9
- offsetX: string;
10
- offsetY: string;
11
- blur: string;
12
- spread: string;
13
- color: string;
14
- inset: boolean;
15
- }
16
-
17
- import type { PreviewSettings } from "./shadow-preview-settings.js";
18
-
19
- interface ShadowControlsProps {
20
- shadow: { value: string; layers: ShadowLayer[] };
21
- onPreview: (value: string) => void;
22
- onSave: (value: string) => void;
23
- previewSettings: PreviewSettings;
24
- }
25
-
26
- export function ShadowControls({
27
- shadow,
28
- onPreview,
29
- onSave,
30
- previewSettings,
31
- }: ShadowControlsProps) {
32
- const [layers, setLayers] = useState<ShadowLayer[]>(() =>
33
- shadow.layers.length > 0
34
- ? shadow.layers
35
- : [{ offsetX: "0", offsetY: "4px", blur: "6px", spread: "-1px", color: "rgb(0 0 0 / 0.1)", inset: false }]
36
- );
37
-
38
- const formatValue = useCallback((layers: ShadowLayer[]) => {
39
- if (layers.length === 0) return "none";
40
- return layers
41
- .map((l) => {
42
- const parts: string[] = [];
43
- if (l.inset) parts.push("inset");
44
- parts.push(l.offsetX, l.offsetY, l.blur, l.spread, l.color);
45
- return parts.join(" ");
46
- })
47
- .join(", ");
48
- }, []);
49
-
50
- const updateLayer = useCallback(
51
- (index: number, update: Partial<ShadowLayer>) => {
52
- const newLayers = layers.map((l, i) =>
53
- i === index ? { ...l, ...update } : l
54
- );
55
- setLayers(newLayers);
56
- onPreview(formatValue(newLayers));
57
- },
58
- [layers, onPreview, formatValue]
59
- );
60
-
61
- const addLayer = () => {
62
- const newLayers = [
63
- ...layers,
64
- { offsetX: "0", offsetY: "4px", blur: "8px", spread: "0", color: "rgb(0 0 0 / 0.1)", inset: false },
65
- ];
66
- setLayers(newLayers);
67
- onPreview(formatValue(newLayers));
68
- };
69
-
70
- const removeLayer = (index: number) => {
71
- const newLayers = layers.filter((_, i) => i !== index);
72
- setLayers(newLayers);
73
- onPreview(formatValue(newLayers));
74
- };
75
-
76
- return (
77
- <div className="flex flex-col gap-3">
78
- {/* Preview */}
79
- <div
80
- className="flex items-center justify-center p-6 rounded-lg"
81
- style={{
82
- background: previewSettings.previewBg,
83
- }}
84
- >
85
- <div
86
- className="w-20 h-20 rounded-lg"
87
- style={{
88
- background: "white",
89
- boxShadow: formatValue(layers),
90
- border: previewSettings.showBorder
91
- ? `1px solid ${previewSettings.borderColor}`
92
- : "none",
93
- }}
94
- />
95
- </div>
96
-
97
- {/* Layers */}
98
- {layers.map((layer, i) => (
99
- <LayerEditor
100
- key={i}
101
- layer={layer}
102
- index={i}
103
- total={layers.length}
104
- onChange={(update) => updateLayer(i, update)}
105
- onRemove={() => removeLayer(i)}
106
- />
107
- ))}
108
-
109
- {/* Add layer button */}
110
- <button
111
- onClick={addLayer}
112
- className="flex items-center justify-center gap-1 py-1.5 rounded text-[11px] cursor-pointer"
113
- style={{
114
- background: "var(--studio-input-bg)",
115
- border: "1px solid var(--studio-border-subtle)",
116
- color: "var(--studio-text-muted)",
117
- }}
118
- >
119
- <PlusIcon style={{ width: 12, height: 12 }} />
120
- Add Layer
121
- </button>
122
-
123
- {/* Save */}
124
- <button
125
- onClick={() => onSave(formatValue(layers))}
126
- className="w-full py-1.5 rounded text-[11px] font-medium cursor-pointer"
127
- style={{
128
- background: "var(--studio-accent)",
129
- color: "white",
130
- border: "none",
131
- }}
132
- >
133
- Save to File
134
- </button>
135
- </div>
136
- );
137
- }
138
-
139
- function LayerEditor({
140
- layer,
141
- index,
142
- total,
143
- onChange,
144
- onRemove,
145
- }: {
146
- layer: ShadowLayer;
147
- index: number;
148
- total: number;
149
- onChange: (update: Partial<ShadowLayer>) => void;
150
- onRemove: () => void;
151
- }) {
152
- const parseNum = (v: string): number => parseFloat(v) || 0;
153
-
154
- return (
155
- <div
156
- className="rounded-lg p-2.5"
157
- style={{
158
- background: "var(--studio-input-bg)",
159
- border: "1px solid var(--studio-border-subtle)",
160
- }}
161
- >
162
- {/* Layer header */}
163
- <div className="flex items-center justify-between mb-2">
164
- <span
165
- className="text-[9px] font-semibold uppercase tracking-wide"
166
- style={{ color: "var(--studio-text-dimmed)" }}
167
- >
168
- Layer {index + 1}
169
- </span>
170
- <div className="flex items-center gap-1">
171
- <button
172
- onClick={() => onChange({ inset: !layer.inset })}
173
- className={`studio-bp-btn ${layer.inset ? "active" : ""}`}
174
- style={{ fontSize: 9, padding: "1px 5px" }}
175
- >
176
- inset
177
- </button>
178
- {total > 1 && (
179
- <button
180
- onClick={onRemove}
181
- className="studio-icon-btn"
182
- style={{ width: 20, height: 20 }}
183
- >
184
- <TrashIcon style={{ width: 10, height: 10 }} />
185
- </button>
186
- )}
187
- </div>
188
- </div>
189
-
190
- {/* Controls grid */}
191
- <div className="grid grid-cols-2 gap-x-2 gap-y-1.5">
192
- <SliderField
193
- label="X Offset"
194
- value={parseNum(layer.offsetX)}
195
- min={-50}
196
- max={50}
197
- unit="px"
198
- onChange={(v) => onChange({ offsetX: `${v}px` })}
199
- />
200
- <SliderField
201
- label="Y Offset"
202
- value={parseNum(layer.offsetY)}
203
- min={-50}
204
- max={50}
205
- unit="px"
206
- onChange={(v) => onChange({ offsetY: `${v}px` })}
207
- />
208
- <SliderField
209
- label="Blur"
210
- value={parseNum(layer.blur)}
211
- min={0}
212
- max={100}
213
- unit="px"
214
- onChange={(v) => onChange({ blur: `${v}px` })}
215
- />
216
- <SliderField
217
- label="Spread"
218
- value={parseNum(layer.spread)}
219
- min={-50}
220
- max={50}
221
- unit="px"
222
- onChange={(v) => onChange({ spread: `${v}px` })}
223
- />
224
- </div>
225
-
226
- {/* Color */}
227
- <div className="mt-1.5">
228
- <div
229
- className="text-[9px] font-medium mb-0.5"
230
- style={{ color: "var(--studio-text-dimmed)" }}
231
- >
232
- Color
233
- </div>
234
- <input
235
- type="text"
236
- value={layer.color}
237
- onChange={(e) => onChange({ color: e.target.value })}
238
- className="studio-input w-full"
239
- style={{ fontSize: 10 }}
240
- />
241
- </div>
242
- </div>
243
- );
244
- }
245
-
246
- function SliderField({
247
- label,
248
- value,
249
- min,
250
- max,
251
- unit,
252
- onChange,
253
- }: {
254
- label: string;
255
- value: number;
256
- min: number;
257
- max: number;
258
- unit: string;
259
- onChange: (v: number) => void;
260
- }) {
261
- return (
262
- <div>
263
- <div className="flex items-center justify-between">
264
- <span
265
- className="text-[9px] font-medium"
266
- style={{ color: "var(--studio-text-dimmed)" }}
267
- >
268
- {label}
269
- </span>
270
- <span
271
- className="text-[9px] font-mono"
272
- style={{ color: "var(--studio-text-muted)" }}
273
- >
274
- {value}{unit}
275
- </span>
276
- </div>
277
- <input
278
- type="range"
279
- min={min}
280
- max={max}
281
- value={value}
282
- onChange={(e) => onChange(parseInt(e.target.value, 10))}
283
- className="w-full"
284
- />
285
- </div>
286
- );
287
- }
@@ -1,163 +0,0 @@
1
- import { useState } from "react";
2
- import {
3
- ShadowIcon,
4
- MixerHorizontalIcon,
5
- GridIcon,
6
- } from "@radix-ui/react-icons";
7
- import type { ShadowsScanData } from "../app.js";
8
- import type { ElementData } from "@designtools/core/client/lib/iframe-bridge";
9
- import { ShadowList } from "./shadow-list.js";
10
- import { ShadowOverview } from "./shadow-overview.js";
11
- import {
12
- ShadowPreviewSettingsButton,
13
- DEFAULT_PREVIEW_SETTINGS,
14
- type PreviewSettings,
15
- } from "./shadow-preview-settings.js";
16
-
17
- type ViewMode = "list" | "overview";
18
-
19
- interface ShadowEditorPanelProps {
20
- scanData: ShadowsScanData | null;
21
- selectedElement: ElementData | null;
22
- onPreviewShadow: (variableName: string, value: string, shadowName?: string) => void;
23
- }
24
-
25
- export function ShadowEditorPanel({
26
- scanData,
27
- selectedElement,
28
- onPreviewShadow,
29
- }: ShadowEditorPanelProps) {
30
- const [viewMode, setViewMode] = useState<ViewMode>("list");
31
- const [previewSettings, setPreviewSettings] = useState<PreviewSettings>(
32
- DEFAULT_PREVIEW_SETTINGS
33
- );
34
-
35
- if (!scanData) {
36
- return (
37
- <div
38
- className="flex flex-col border-l studio-scrollbar"
39
- style={{
40
- width: 340,
41
- minWidth: 340,
42
- background: "var(--studio-surface)",
43
- borderColor: "var(--studio-border)",
44
- }}
45
- >
46
- <div
47
- className="px-4 py-3 text-[11px]"
48
- style={{ color: "var(--studio-text-dimmed)" }}
49
- >
50
- Scanning project...
51
- </div>
52
- </div>
53
- );
54
- }
55
-
56
- const modeConfig: Record<ViewMode, { icon: any; label: string }> = {
57
- list: { icon: MixerHorizontalIcon, label: "Edit" },
58
- overview: { icon: GridIcon, label: "Preview" },
59
- };
60
-
61
- return (
62
- <div
63
- className="flex flex-col border-l studio-scrollbar overflow-y-auto"
64
- style={{
65
- width: 340,
66
- minWidth: 340,
67
- background: "var(--studio-surface)",
68
- borderColor: "var(--studio-border)",
69
- }}
70
- >
71
- {/* Header */}
72
- <div
73
- className="flex items-center gap-2 px-4 py-3 border-b shrink-0"
74
- style={{ borderColor: "var(--studio-border)" }}
75
- >
76
- <div
77
- className="w-5 h-5 rounded flex items-center justify-center shrink-0"
78
- style={{ background: "var(--studio-accent-muted)" }}
79
- >
80
- <ShadowIcon
81
- style={{ width: 12, height: 12, color: "var(--studio-accent)" }}
82
- />
83
- </div>
84
- <div className="flex-1 min-w-0">
85
- <span
86
- className="text-[12px] font-semibold"
87
- style={{ color: "var(--studio-text)" }}
88
- >
89
- Shadows
90
- </span>
91
- <div
92
- className="text-[10px]"
93
- style={{ color: "var(--studio-text-dimmed)" }}
94
- >
95
- {scanData.shadows.shadows.length} shadow
96
- {scanData.shadows.shadows.length !== 1 ? "s" : ""} found
97
- </div>
98
- </div>
99
- <ShadowPreviewSettingsButton
100
- settings={previewSettings}
101
- onChange={setPreviewSettings}
102
- />
103
- </div>
104
-
105
- {/* View mode switcher */}
106
- <div
107
- className="px-4 py-2.5 border-b shrink-0"
108
- style={{ borderColor: "var(--studio-border)" }}
109
- >
110
- <div className="studio-segmented" style={{ width: "100%" }}>
111
- {(Object.keys(modeConfig) as ViewMode[]).map((mode) => {
112
- const cfg = modeConfig[mode];
113
- return (
114
- <button
115
- key={mode}
116
- onClick={() => setViewMode(mode)}
117
- className={viewMode === mode ? "active" : ""}
118
- style={{ flex: 1 }}
119
- >
120
- <cfg.icon style={{ width: 12, height: 12 }} />
121
- {cfg.label}
122
- </button>
123
- );
124
- })}
125
- </div>
126
- </div>
127
-
128
- {/* No CSS file warning */}
129
- {!scanData.shadows.cssFilePath && (
130
- <div
131
- className="mx-4 mt-2.5 mb-0 px-3 py-2 rounded-md text-[10px] shrink-0"
132
- style={{
133
- background: "rgb(255 243 205)",
134
- color: "rgb(133 100 4)",
135
- border: "1px solid rgb(255 224 130)",
136
- lineHeight: 1.4,
137
- }}
138
- >
139
- No CSS file found. Run this tool from your app directory to enable editing.
140
- </div>
141
- )}
142
-
143
- {/* Content */}
144
- <div className="flex-1 overflow-y-auto studio-scrollbar">
145
- {viewMode === "list" && (
146
- <ShadowList
147
- shadows={scanData.shadows.shadows}
148
- cssFilePath={scanData.shadows.cssFilePath}
149
- stylingType={scanData.shadows.stylingType}
150
- onPreviewShadow={onPreviewShadow}
151
- previewSettings={previewSettings}
152
- />
153
- )}
154
- {viewMode === "overview" && (
155
- <ShadowOverview
156
- shadows={scanData.shadows.shadows}
157
- previewSettings={previewSettings}
158
- />
159
- )}
160
- </div>
161
- </div>
162
- );
163
- }