@pixel-point/toolcraft 0.0.7 → 0.0.9
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/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +185 -23
- package/templates/runtime/contracts/component-contracts.ts +91 -36
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +4 -4
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +774 -17
- package/templates/runtime/react/controls-panel.tsx +155 -8
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
- package/templates/runtime/schema/define-toolcraft.ts +29 -3
- package/templates/runtime/schema/types.ts +7 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1424 -56
- package/templates/runtime/testing/performance.ts +710 -43
- package/templates/starter/AGENTS.md +10 -9
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
- package/templates/starter/docs/toolcraft/component-rules.md +46 -16
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +54 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +5 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
- package/templates/starter/src/app/starter-acceptance.ts +740 -24
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +3 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
- package/templates/ui/components/panel/panel-section.tsx +82 -6
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
closestCenter,
|
|
6
|
+
DndContext,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
type DragEndEvent,
|
|
10
|
+
useSensor,
|
|
11
|
+
useSensors,
|
|
12
|
+
} from "@dnd-kit/core";
|
|
13
|
+
import {
|
|
14
|
+
arrayMove,
|
|
15
|
+
rectSortingStrategy,
|
|
16
|
+
SortableContext,
|
|
17
|
+
sortableKeyboardCoordinates,
|
|
18
|
+
verticalListSortingStrategy,
|
|
19
|
+
useSortable,
|
|
20
|
+
} from "@dnd-kit/sortable";
|
|
21
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
22
|
+
import { CloudArrowUpIcon, PaperclipIcon, XIcon } from "@phosphor-icons/react";
|
|
5
23
|
|
|
6
24
|
import { cn } from "../../../lib/utils";
|
|
7
25
|
import { Button, Field } from "../../primitives";
|
|
8
26
|
|
|
27
|
+
export type FileDropAssetKind = "file" | "image";
|
|
28
|
+
|
|
9
29
|
export type FileDropPreview = {
|
|
10
30
|
alt?: string;
|
|
31
|
+
assetKind?: FileDropAssetKind;
|
|
32
|
+
fileName?: string;
|
|
11
33
|
id?: string;
|
|
12
34
|
size?: {
|
|
13
35
|
height: number;
|
|
@@ -18,11 +40,13 @@ export type FileDropPreview = {
|
|
|
18
40
|
|
|
19
41
|
export type FileDropControlProps = {
|
|
20
42
|
accept: string;
|
|
43
|
+
assetKind?: FileDropAssetKind;
|
|
21
44
|
multiple?: boolean;
|
|
22
45
|
onClear?: () => void;
|
|
23
46
|
onFileSelect?: (file: File) => void;
|
|
24
47
|
onFilesSelect?: (files: File[]) => void;
|
|
25
48
|
onPreviewRemove?: (preview: FileDropPreview, index: number) => void;
|
|
49
|
+
onPreviewReorder?: (orderedPreviews: FileDropPreview[]) => void;
|
|
26
50
|
preview?: FileDropPreview;
|
|
27
51
|
previews?: readonly FileDropPreview[];
|
|
28
52
|
};
|
|
@@ -76,21 +100,218 @@ function getPreviewImageStyle(size: FileDropPreview["size"]): React.CSSPropertie
|
|
|
76
100
|
};
|
|
77
101
|
}
|
|
78
102
|
|
|
103
|
+
function getPreviewKey(item: FileDropPreview, index: number): string {
|
|
104
|
+
return item.id ?? `${item.src}:${index}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function FileDropPlusGlyph({ className }: { className?: string }): React.JSX.Element {
|
|
108
|
+
return (
|
|
109
|
+
<svg
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
className={cn("flex-none", className)}
|
|
112
|
+
fill="none"
|
|
113
|
+
viewBox="0 0 14 14"
|
|
114
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
115
|
+
>
|
|
116
|
+
<path d="M7 2.5V11.5" stroke="currentColor" strokeLinecap="round" strokeWidth="1" />
|
|
117
|
+
<path d="M2.5 7H11.5" stroke="currentColor" strokeLinecap="round" strokeWidth="1" />
|
|
118
|
+
</svg>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type SortablePreviewTileProps = {
|
|
123
|
+
index: number;
|
|
124
|
+
isSortable: boolean;
|
|
125
|
+
item: FileDropPreview;
|
|
126
|
+
itemKey: string;
|
|
127
|
+
onPreviewRemove?: (preview: FileDropPreview, index: number) => void;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function SortablePreviewTile({
|
|
131
|
+
index,
|
|
132
|
+
isSortable,
|
|
133
|
+
item,
|
|
134
|
+
itemKey,
|
|
135
|
+
onPreviewRemove,
|
|
136
|
+
}: SortablePreviewTileProps): React.JSX.Element {
|
|
137
|
+
const { attributes, isDragging, listeners, setNodeRef, transform, transition } =
|
|
138
|
+
useSortable({
|
|
139
|
+
disabled: !isSortable,
|
|
140
|
+
id: itemKey,
|
|
141
|
+
});
|
|
142
|
+
const sortableTransform = transform
|
|
143
|
+
? {
|
|
144
|
+
...transform,
|
|
145
|
+
scaleX: isDragging ? 1.035 : transform.scaleX,
|
|
146
|
+
scaleY: isDragging ? 1.035 : transform.scaleY,
|
|
147
|
+
}
|
|
148
|
+
: null;
|
|
149
|
+
const style: React.CSSProperties = {
|
|
150
|
+
transform: sortableTransform ? CSS.Transform.toString(sortableTransform) : undefined,
|
|
151
|
+
transition,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
className={cn(
|
|
157
|
+
"relative aspect-square min-w-0 overflow-hidden rounded-[calc(var(--radius-lg)-4px)] bg-[color:color-mix(in_oklab,var(--foreground)_6%,transparent)] transition-[transform,box-shadow,opacity,border-color] duration-180 ease-out motion-reduce:transition-none",
|
|
158
|
+
isSortable &&
|
|
159
|
+
"cursor-grab touch-none select-none will-change-transform hover:shadow-[0_8px_18px_color-mix(in_oklab,var(--background)_30%,transparent)]",
|
|
160
|
+
isDragging &&
|
|
161
|
+
"z-10 cursor-grabbing opacity-90 shadow-[0_12px_24px_color-mix(in_oklab,var(--background)_55%,transparent)]",
|
|
162
|
+
)}
|
|
163
|
+
data-file-upload-preview-key={itemKey}
|
|
164
|
+
data-preview-dragging={isDragging ? "true" : undefined}
|
|
165
|
+
data-slot="file-upload-preview-item"
|
|
166
|
+
onClick={(event) => {
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
}}
|
|
169
|
+
ref={setNodeRef}
|
|
170
|
+
style={style}
|
|
171
|
+
{...(isSortable ? attributes : {})}
|
|
172
|
+
{...(isSortable ? listeners : {})}
|
|
173
|
+
>
|
|
174
|
+
<img
|
|
175
|
+
alt={item.alt ?? ""}
|
|
176
|
+
className="size-full object-cover"
|
|
177
|
+
draggable={false}
|
|
178
|
+
height={item.size?.height}
|
|
179
|
+
src={item.src}
|
|
180
|
+
width={item.size?.width}
|
|
181
|
+
/>
|
|
182
|
+
{onPreviewRemove ? (
|
|
183
|
+
<Button
|
|
184
|
+
aria-label={`Remove ${item.alt ?? "image"}`}
|
|
185
|
+
className="absolute top-0.5 right-0.5"
|
|
186
|
+
onClick={(event) => {
|
|
187
|
+
event.stopPropagation();
|
|
188
|
+
onPreviewRemove(item, index);
|
|
189
|
+
}}
|
|
190
|
+
onPointerDown={(event) => {
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
}}
|
|
193
|
+
size="icon-sm"
|
|
194
|
+
type="button"
|
|
195
|
+
variant="ghost"
|
|
196
|
+
>
|
|
197
|
+
<XIcon className="drop-shadow-[0_2px_1px_color-mix(in_oklab,var(--background)_80%,transparent)]" />
|
|
198
|
+
</Button>
|
|
199
|
+
) : null}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
type SortableFileRowProps = {
|
|
205
|
+
index: number;
|
|
206
|
+
isSortable: boolean;
|
|
207
|
+
item: FileDropPreview;
|
|
208
|
+
itemKey: string;
|
|
209
|
+
onPreviewRemove?: (preview: FileDropPreview, index: number) => void;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
function SortableFileRow({
|
|
213
|
+
index,
|
|
214
|
+
isSortable,
|
|
215
|
+
item,
|
|
216
|
+
itemKey,
|
|
217
|
+
onPreviewRemove,
|
|
218
|
+
}: SortableFileRowProps): React.JSX.Element {
|
|
219
|
+
const { attributes, isDragging, listeners, setNodeRef, transform, transition } =
|
|
220
|
+
useSortable({
|
|
221
|
+
disabled: !isSortable,
|
|
222
|
+
id: itemKey,
|
|
223
|
+
});
|
|
224
|
+
const style: React.CSSProperties = {
|
|
225
|
+
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
|
226
|
+
transition,
|
|
227
|
+
};
|
|
228
|
+
const fileName = item.fileName ?? item.alt ?? "Untitled file";
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div
|
|
232
|
+
className={cn(
|
|
233
|
+
"mx-1 flex h-8 min-w-0 items-center gap-2 pr-1 pl-[7px] text-left text-sm text-[color:color-mix(in_oklab,var(--foreground)_86%,transparent)] transition-[background-color,opacity,transform] duration-150 ease-out hover:bg-[color:color-mix(in_oklab,var(--foreground)_3%,transparent)] motion-reduce:transition-none",
|
|
234
|
+
isSortable && "cursor-grab touch-none select-none",
|
|
235
|
+
isDragging &&
|
|
236
|
+
"z-10 cursor-grabbing bg-[color:color-mix(in_oklab,var(--foreground)_5%,transparent)] opacity-90",
|
|
237
|
+
)}
|
|
238
|
+
data-file-upload-preview-key={itemKey}
|
|
239
|
+
data-preview-dragging={isDragging ? "true" : undefined}
|
|
240
|
+
data-slot="file-upload-file-item"
|
|
241
|
+
onClick={(event) => {
|
|
242
|
+
event.stopPropagation();
|
|
243
|
+
}}
|
|
244
|
+
ref={setNodeRef}
|
|
245
|
+
style={style}
|
|
246
|
+
{...(isSortable ? attributes : {})}
|
|
247
|
+
{...(isSortable ? listeners : {})}
|
|
248
|
+
>
|
|
249
|
+
<PaperclipIcon
|
|
250
|
+
aria-hidden="true"
|
|
251
|
+
className="size-4 flex-none text-[color:color-mix(in_oklab,var(--foreground)_48%,transparent)]"
|
|
252
|
+
weight="regular"
|
|
253
|
+
/>
|
|
254
|
+
<span
|
|
255
|
+
className="min-w-0 flex-1 overflow-hidden text-xs whitespace-nowrap [-webkit-mask-image:linear-gradient(to_right,black_calc(100%-20px),transparent)] [mask-image:linear-gradient(to_right,black_calc(100%-20px),transparent)]"
|
|
256
|
+
title={fileName}
|
|
257
|
+
>
|
|
258
|
+
{fileName}
|
|
259
|
+
</span>
|
|
260
|
+
{onPreviewRemove ? (
|
|
261
|
+
<Button
|
|
262
|
+
aria-label={`Remove ${fileName}`}
|
|
263
|
+
className="flex-none"
|
|
264
|
+
onClick={(event) => {
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
onPreviewRemove(item, index);
|
|
267
|
+
}}
|
|
268
|
+
onPointerDown={(event) => {
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
}}
|
|
271
|
+
size="icon-sm"
|
|
272
|
+
type="button"
|
|
273
|
+
variant="ghost"
|
|
274
|
+
>
|
|
275
|
+
<XIcon className="drop-shadow-[0_2px_1px_color-mix(in_oklab,var(--background)_80%,transparent)]" />
|
|
276
|
+
</Button>
|
|
277
|
+
) : null}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
79
282
|
export function FileDropControl({
|
|
80
283
|
accept,
|
|
284
|
+
assetKind = "image",
|
|
81
285
|
multiple = false,
|
|
82
286
|
onClear,
|
|
83
287
|
onFileSelect,
|
|
84
288
|
onFilesSelect,
|
|
85
289
|
onPreviewRemove,
|
|
290
|
+
onPreviewReorder,
|
|
86
291
|
preview,
|
|
87
292
|
previews,
|
|
88
293
|
}: FileDropControlProps): React.JSX.Element {
|
|
89
294
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
90
295
|
const [dragOver, setDragOver] = React.useState(false);
|
|
296
|
+
const sensors = useSensors(
|
|
297
|
+
useSensor(PointerSensor, {
|
|
298
|
+
activationConstraint: {
|
|
299
|
+
distance: 6,
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
useSensor(KeyboardSensor, {
|
|
303
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
91
306
|
const previewItems = previews ?? (preview ? [preview] : []);
|
|
92
307
|
const hasPreview = previewItems.some((item) => Boolean(item.src));
|
|
93
308
|
const shouldRenderPreviewGrid = multiple && previewItems.length > 1;
|
|
309
|
+
const shouldRenderFileList = assetKind === "file" && hasPreview;
|
|
310
|
+
const previewEntries = previewItems.map((item, index) => ({
|
|
311
|
+
item,
|
|
312
|
+
key: getPreviewKey(item, index),
|
|
313
|
+
}));
|
|
314
|
+
const previewKeys = previewEntries.map((entry) => entry.key);
|
|
94
315
|
|
|
95
316
|
function handleFiles(fileList: FileList | readonly File[] | undefined): void {
|
|
96
317
|
const files = Array.from(fileList ?? []);
|
|
@@ -116,6 +337,11 @@ export function FileDropControl({
|
|
|
116
337
|
inputRef.current?.click();
|
|
117
338
|
}
|
|
118
339
|
|
|
340
|
+
function handleDropTargetClick(event: React.MouseEvent<HTMLDivElement>): void {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
openFileDialog();
|
|
343
|
+
}
|
|
344
|
+
|
|
119
345
|
function handleDropTargetKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
|
|
120
346
|
if (event.key !== "Enter" && event.key !== " ") {
|
|
121
347
|
return;
|
|
@@ -125,6 +351,26 @@ export function FileDropControl({
|
|
|
125
351
|
openFileDialog();
|
|
126
352
|
}
|
|
127
353
|
|
|
354
|
+
function handlePreviewDragEnd(event: DragEndEvent): void {
|
|
355
|
+
const activeKey = String(event.active.id);
|
|
356
|
+
const overKey = event.over ? String(event.over.id) : null;
|
|
357
|
+
|
|
358
|
+
if (!onPreviewReorder || !overKey || activeKey === overKey) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const activeIndex = previewKeys.indexOf(activeKey);
|
|
363
|
+
const overIndex = previewKeys.indexOf(overKey);
|
|
364
|
+
|
|
365
|
+
if (activeIndex < 0 || overIndex < 0 || activeIndex === overIndex) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
onPreviewReorder(
|
|
370
|
+
arrayMove(previewEntries, activeIndex, overIndex).map((entry) => entry.item),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
128
374
|
return (
|
|
129
375
|
<Field className="min-w-0 gap-2">
|
|
130
376
|
<input
|
|
@@ -144,18 +390,32 @@ export function FileDropControl({
|
|
|
144
390
|
aria-label={
|
|
145
391
|
hasPreview
|
|
146
392
|
? multiple
|
|
147
|
-
?
|
|
148
|
-
|
|
393
|
+
? assetKind === "file"
|
|
394
|
+
? "Drop files"
|
|
395
|
+
: "Drop image files"
|
|
396
|
+
: assetKind === "file"
|
|
397
|
+
? "Replace file"
|
|
398
|
+
: "Replace image file"
|
|
149
399
|
: multiple
|
|
150
|
-
?
|
|
151
|
-
|
|
400
|
+
? assetKind === "file"
|
|
401
|
+
? "Browse files"
|
|
402
|
+
: "Browse image files"
|
|
403
|
+
: assetKind === "file"
|
|
404
|
+
? "Browse file"
|
|
405
|
+
: "Browse image file"
|
|
152
406
|
}
|
|
153
407
|
className={cn(
|
|
154
|
-
"group/file-upload relative flex min-h-16 w-full cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed border-[color:color-mix(in_oklab,var(--border)_18%,transparent)] bg-[color:color-mix(in_oklab,var(--foreground)_3%,transparent)] text-center shadow-none transition-[background-color,border-color,box-shadow] duration-150 ease-out
|
|
155
|
-
|
|
408
|
+
"group/file-upload relative flex min-h-16 w-full cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed border-[color:color-mix(in_oklab,var(--border)_18%,transparent)] bg-[color:color-mix(in_oklab,var(--foreground)_3%,transparent)] text-center shadow-none transition-[background-color,border-color,box-shadow] duration-150 ease-out data-[drag-over=true]:border-[color:color-mix(in_oklab,var(--link)_28%,transparent)] data-[drag-over=true]:bg-[color:color-mix(in_oklab,var(--link)_13%,transparent)] data-[drag-over=true]:shadow-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--background)]",
|
|
409
|
+
!shouldRenderFileList &&
|
|
410
|
+
"hover:border-[color:color-mix(in_oklab,var(--border)_35%,transparent)] hover:bg-[color:color-mix(in_oklab,var(--foreground)_6%,transparent)]",
|
|
411
|
+
hasPreview
|
|
412
|
+
? shouldRenderFileList
|
|
413
|
+
? "overflow-hidden p-1.5"
|
|
414
|
+
: "overflow-hidden p-2"
|
|
415
|
+
: "px-3 py-3",
|
|
156
416
|
)}
|
|
157
417
|
data-drag-over={dragOver}
|
|
158
|
-
onClick={
|
|
418
|
+
onClick={handleDropTargetClick}
|
|
159
419
|
onDragEnter={(event) => {
|
|
160
420
|
event.preventDefault();
|
|
161
421
|
setDragOver(true);
|
|
@@ -178,45 +438,72 @@ export function FileDropControl({
|
|
|
178
438
|
role="button"
|
|
179
439
|
tabIndex={0}
|
|
180
440
|
>
|
|
181
|
-
{
|
|
441
|
+
{shouldRenderFileList ? (
|
|
442
|
+
<div className="w-full" data-slot="file-upload-file-list">
|
|
443
|
+
<DndContext
|
|
444
|
+
collisionDetection={closestCenter}
|
|
445
|
+
onDragEnd={handlePreviewDragEnd}
|
|
446
|
+
sensors={sensors}
|
|
447
|
+
>
|
|
448
|
+
<SortableContext items={previewKeys} strategy={verticalListSortingStrategy}>
|
|
449
|
+
{previewEntries.map(({ item, key }, index) => (
|
|
450
|
+
<React.Fragment key={key}>
|
|
451
|
+
<SortableFileRow
|
|
452
|
+
index={index}
|
|
453
|
+
isSortable={Boolean(onPreviewReorder) && previewEntries.length > 1}
|
|
454
|
+
item={item}
|
|
455
|
+
itemKey={key}
|
|
456
|
+
onPreviewRemove={onPreviewRemove}
|
|
457
|
+
/>
|
|
458
|
+
<div
|
|
459
|
+
aria-hidden="true"
|
|
460
|
+
className="mx-1 h-px bg-[color:color-mix(in_oklab,var(--border)_5%,transparent)]"
|
|
461
|
+
data-slot="file-upload-file-divider"
|
|
462
|
+
/>
|
|
463
|
+
</React.Fragment>
|
|
464
|
+
))}
|
|
465
|
+
</SortableContext>
|
|
466
|
+
</DndContext>
|
|
467
|
+
<button
|
|
468
|
+
aria-label="Add a new file"
|
|
469
|
+
className="mx-1 box-border flex h-8 w-[calc(100%-0.5rem)] min-w-0 shrink-0 items-center justify-center gap-1.5 px-1 text-xs text-[color:color-mix(in_oklab,var(--foreground)_65%,transparent)] transition-[background-color,color] duration-150 ease-out hover:bg-[color:color-mix(in_oklab,var(--foreground)_3%,transparent)] hover:text-[color:var(--foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ring)]"
|
|
470
|
+
data-slot="file-upload-add-file"
|
|
471
|
+
onClick={(event) => {
|
|
472
|
+
event.stopPropagation();
|
|
473
|
+
openFileDialog();
|
|
474
|
+
}}
|
|
475
|
+
type="button"
|
|
476
|
+
>
|
|
477
|
+
<FileDropPlusGlyph className="size-3.5" />
|
|
478
|
+
<span className="font-medium">Add a new file</span>
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
) : shouldRenderPreviewGrid ? (
|
|
182
482
|
<div
|
|
183
483
|
className="grid w-full grid-cols-4 gap-2"
|
|
184
484
|
data-slot="file-upload-preview-grid"
|
|
185
485
|
>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
onClick={(event) => {
|
|
205
|
-
event.stopPropagation();
|
|
206
|
-
onPreviewRemove(item, index);
|
|
207
|
-
}}
|
|
208
|
-
size="icon-sm"
|
|
209
|
-
type="button"
|
|
210
|
-
variant="ghost"
|
|
211
|
-
>
|
|
212
|
-
<XIcon className="drop-shadow-[0_2px_1px_color-mix(in_oklab,var(--background)_80%,transparent)]" />
|
|
213
|
-
</Button>
|
|
214
|
-
) : null}
|
|
215
|
-
</div>
|
|
216
|
-
))}
|
|
486
|
+
<DndContext
|
|
487
|
+
collisionDetection={closestCenter}
|
|
488
|
+
onDragEnd={handlePreviewDragEnd}
|
|
489
|
+
sensors={sensors}
|
|
490
|
+
>
|
|
491
|
+
<SortableContext items={previewKeys} strategy={rectSortingStrategy}>
|
|
492
|
+
{previewEntries.map(({ item, key }, index) => (
|
|
493
|
+
<SortablePreviewTile
|
|
494
|
+
index={index}
|
|
495
|
+
isSortable={Boolean(onPreviewReorder)}
|
|
496
|
+
item={item}
|
|
497
|
+
itemKey={key}
|
|
498
|
+
key={key}
|
|
499
|
+
onPreviewRemove={onPreviewRemove}
|
|
500
|
+
/>
|
|
501
|
+
))}
|
|
502
|
+
</SortableContext>
|
|
503
|
+
</DndContext>
|
|
217
504
|
<button
|
|
218
505
|
aria-label="Add image files"
|
|
219
|
-
className="flex aspect-square min-w-0 items-center justify-center rounded-[calc(var(--radius-lg)-4px)] border border-[color:color-mix(in_oklab,var(--border)_5%,transparent)] bg-[color:color-mix(in_oklab,var(--foreground)_4%,transparent)] text-[color:color-mix(in_oklab,var(--foreground)_65%,transparent)] transition-[background-color,border-color,color] duration-150 ease-out hover:border-[color:color-mix(in_oklab,var(--border)
|
|
506
|
+
className="flex aspect-square min-w-0 items-center justify-center rounded-[calc(var(--radius-lg)-4px)] border border-[color:color-mix(in_oklab,var(--border)_5%,transparent)] bg-[color:color-mix(in_oklab,var(--foreground)_4%,transparent)] text-[color:color-mix(in_oklab,var(--foreground)_65%,transparent)] transition-[background-color,border-color,color] duration-150 ease-out hover:border-[color:color-mix(in_oklab,var(--border)_8%,transparent)] hover:bg-[color:color-mix(in_oklab,var(--foreground)_9%,transparent)] hover:text-[color:var(--foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ring)]"
|
|
220
507
|
data-slot="file-upload-add-preview"
|
|
221
508
|
onClick={(event) => {
|
|
222
509
|
event.stopPropagation();
|
|
@@ -224,7 +511,7 @@ export function FileDropControl({
|
|
|
224
511
|
}}
|
|
225
512
|
type="button"
|
|
226
513
|
>
|
|
227
|
-
<
|
|
514
|
+
<FileDropPlusGlyph className="size-3.5" />
|
|
228
515
|
</button>
|
|
229
516
|
</div>
|
|
230
517
|
) : hasPreview ? (
|
|
@@ -263,8 +550,17 @@ export function FileDropControl({
|
|
|
263
550
|
weight="light"
|
|
264
551
|
/>
|
|
265
552
|
<p className="m-0 flex max-w-full flex-col text-xs leading-tight text-[color:color-mix(in_oklab,var(--foreground)_60%,transparent)] transition-colors duration-150 ease-out group-hover/file-upload:text-[color:color-mix(in_oklab,var(--foreground)_85%,transparent)] group-data-[drag-over=true]/file-upload:text-[color:var(--link)]">
|
|
266
|
-
|
|
267
|
-
|
|
553
|
+
{assetKind === "file" ? (
|
|
554
|
+
<>
|
|
555
|
+
<span>Click to upload a file</span>
|
|
556
|
+
<span>or drag it onto the canvas</span>
|
|
557
|
+
</>
|
|
558
|
+
) : (
|
|
559
|
+
<>
|
|
560
|
+
<span>Click to upload an image</span>
|
|
561
|
+
<span>or drag it onto the canvas</span>
|
|
562
|
+
</>
|
|
563
|
+
)}
|
|
268
564
|
</p>
|
|
269
565
|
</>
|
|
270
566
|
)}
|
|
@@ -52,6 +52,7 @@ export {
|
|
|
52
52
|
getSurfaceHsvColor,
|
|
53
53
|
PaletteControl,
|
|
54
54
|
PaletteControl as Palette,
|
|
55
|
+
StyleGuideColorPicker,
|
|
55
56
|
} from "./color";
|
|
56
57
|
export type {
|
|
57
58
|
ColorFormatMode,
|
|
@@ -115,6 +116,7 @@ export type {
|
|
|
115
116
|
} from "./curves";
|
|
116
117
|
export { FileDropControl, FileDropControl as FileDrop } from "./file-drop";
|
|
117
118
|
export type {
|
|
119
|
+
FileDropAssetKind,
|
|
118
120
|
FileDropControlProps,
|
|
119
121
|
FileDropControlProps as FileDropProps,
|
|
120
122
|
FileDropPreview,
|
|
@@ -196,5 +198,6 @@ export type {
|
|
|
196
198
|
VectorControlProps as VectorProps,
|
|
197
199
|
VectorControlValue,
|
|
198
200
|
VectorControlValue as VectorValue,
|
|
201
|
+
VectorPadCoordinateMode,
|
|
199
202
|
VectorPadVariant,
|
|
200
203
|
} from "./vector";
|
|
@@ -12,6 +12,7 @@ export type RangeInputControlProps = {
|
|
|
12
12
|
end: string;
|
|
13
13
|
name: string;
|
|
14
14
|
onValueChange?: ControlValueChangeHandler<{ start: string; end: string }>;
|
|
15
|
+
showLabel?: boolean;
|
|
15
16
|
start: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -88,6 +89,7 @@ export function RangeInputControl({
|
|
|
88
89
|
end,
|
|
89
90
|
name,
|
|
90
91
|
onValueChange,
|
|
92
|
+
showLabel = true,
|
|
91
93
|
start,
|
|
92
94
|
}: RangeInputControlProps): React.JSX.Element {
|
|
93
95
|
const [currentValue, setCurrentValue] = React.useState({ end, start });
|
|
@@ -142,7 +144,7 @@ export function RangeInputControl({
|
|
|
142
144
|
if (shouldUseFullWidthRangeInputs(start, end)) {
|
|
143
145
|
return (
|
|
144
146
|
<Field className="h-fit min-w-0 gap-2">
|
|
145
|
-
<ControlFieldLabel>{name}</ControlFieldLabel>
|
|
147
|
+
{showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
|
|
146
148
|
<RangeInputs
|
|
147
149
|
className="w-full"
|
|
148
150
|
end={currentValue.end}
|
|
@@ -157,10 +159,16 @@ export function RangeInputControl({
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
return (
|
|
160
|
-
<Field
|
|
161
|
-
|
|
162
|
+
<Field
|
|
163
|
+
className={cn(
|
|
164
|
+
"h-fit min-w-0",
|
|
165
|
+
showLabel ? "items-center justify-between gap-3" : undefined,
|
|
166
|
+
)}
|
|
167
|
+
orientation={showLabel ? "horizontal" : "vertical"}
|
|
168
|
+
>
|
|
169
|
+
{showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
|
|
162
170
|
<RangeInputs
|
|
163
|
-
className="w-1/2 shrink-0"
|
|
171
|
+
className={showLabel ? "w-1/2 shrink-0" : "w-full"}
|
|
164
172
|
end={currentValue.end}
|
|
165
173
|
name={name}
|
|
166
174
|
onCancel={cancelDraft}
|
|
@@ -21,6 +21,7 @@ export type SelectControlInput = {
|
|
|
21
21
|
name: string;
|
|
22
22
|
onValueChange?: (value: string) => void;
|
|
23
23
|
options: readonly ControlOption[];
|
|
24
|
+
showLabel?: boolean;
|
|
24
25
|
value: string;
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -145,6 +146,7 @@ function SelectControlField({
|
|
|
145
146
|
name,
|
|
146
147
|
onValueChange,
|
|
147
148
|
options,
|
|
149
|
+
showLabel = true,
|
|
148
150
|
value,
|
|
149
151
|
}: SelectControlInput & {
|
|
150
152
|
layout?: SelectControlFieldLayout;
|
|
@@ -169,21 +171,24 @@ function SelectControlField({
|
|
|
169
171
|
return (
|
|
170
172
|
<Field
|
|
171
173
|
className={cn(
|
|
172
|
-
|
|
174
|
+
!showLabel
|
|
175
|
+
? "h-fit min-w-0"
|
|
176
|
+
: isStacked
|
|
173
177
|
? "h-fit min-w-0 gap-2"
|
|
174
178
|
: compactHorizontalFieldClassName,
|
|
175
179
|
)}
|
|
176
|
-
orientation={isStacked ? "vertical" : "horizontal"}
|
|
180
|
+
orientation={!showLabel || isStacked ? "vertical" : "horizontal"}
|
|
177
181
|
ref={fieldRef}
|
|
178
182
|
>
|
|
179
|
-
<ControlFieldLabel>{name}</ControlFieldLabel>
|
|
183
|
+
{showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
|
|
180
184
|
<div
|
|
181
185
|
className={cn(
|
|
182
186
|
"min-w-0",
|
|
183
|
-
isStacked ? "w-full" : "w-1/2 shrink-0",
|
|
187
|
+
!showLabel || isStacked ? "w-full" : "w-1/2 shrink-0",
|
|
184
188
|
)}
|
|
185
189
|
>
|
|
186
190
|
<StaticSelect
|
|
191
|
+
ariaLabel={showLabel ? undefined : name}
|
|
187
192
|
onValueChange={updateValue}
|
|
188
193
|
options={options}
|
|
189
194
|
popupMaxWidth={popupMaxWidth}
|
|
@@ -15,6 +15,7 @@ export type TextInputControlInput = {
|
|
|
15
15
|
defaultValue?: string;
|
|
16
16
|
name: string;
|
|
17
17
|
onValueChange?: ControlValueChangeHandler<string>;
|
|
18
|
+
showLabel?: boolean;
|
|
18
19
|
value: string;
|
|
19
20
|
};
|
|
20
21
|
|
|
@@ -51,6 +52,7 @@ function TextInputControlField({
|
|
|
51
52
|
defaultValue,
|
|
52
53
|
name,
|
|
53
54
|
onValueChange,
|
|
55
|
+
showLabel = true,
|
|
54
56
|
value,
|
|
55
57
|
}: TextInputControlInput): React.JSX.Element {
|
|
56
58
|
const [currentValue, setCurrentValue] = React.useState(value);
|
|
@@ -101,8 +103,9 @@ function TextInputControlField({
|
|
|
101
103
|
|
|
102
104
|
return (
|
|
103
105
|
<Field className="min-w-0 gap-2">
|
|
104
|
-
<ControlFieldLabel>{name}</ControlFieldLabel>
|
|
106
|
+
{showLabel ? <ControlFieldLabel>{name}</ControlFieldLabel> : null}
|
|
105
107
|
<Input
|
|
108
|
+
aria-label={showLabel ? undefined : name}
|
|
106
109
|
className="font-mono"
|
|
107
110
|
onBlur={commitOnBlur ? () => commitValue() : finishLiveHistoryGroup}
|
|
108
111
|
onChange={(event) => updateValue(event.target.value)}
|