@pixel-point/toolcraft 0.0.6 → 0.0.8
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.test.mjs +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +123 -15
- package/templates/runtime/contracts/component-contracts.ts +93 -19
- package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.ts +2 -2
- 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 +590 -9
- package/templates/runtime/react/controls-panel.tsx +472 -10
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +2 -0
- package/templates/runtime/schema/define-toolcraft.ts +11 -3
- package/templates/runtime/schema/types.ts +27 -0
- package/templates/runtime/state/reducer.test.ts +357 -0
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/types.ts +12 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +13 -12
- package/templates/starter/docs/toolcraft/README.md +4 -3
- package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -7
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -7
- package/templates/starter/docs/toolcraft/component-rules.md +20 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +9 -5
- package/templates/starter/docs/toolcraft/performance.md +51 -13
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +11 -6
- package/templates/starter/docs/toolcraft/workflow.md +7 -10
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +4 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +67 -6
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
- package/templates/ui/components/controls/collection-actions/index.ts +4 -0
- 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/font-picker/font-picker-control.tsx +1 -6
- package/templates/ui/components/controls/index.ts +9 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
- package/templates/ui/index.ts +1 -0
|
@@ -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
|
)}
|
|
@@ -92,7 +92,6 @@ const defaultFontPickerFontSizePx = 16;
|
|
|
92
92
|
const defaultFontPickerColor = "#FFFFFF";
|
|
93
93
|
const defaultFontPickerOpacity = 100;
|
|
94
94
|
const minFontPickerFontSizePx = 1;
|
|
95
|
-
const maxFontPickerFontSizePx = 512;
|
|
96
95
|
|
|
97
96
|
const menuItemInteractionClassName =
|
|
98
97
|
"hover:bg-[color:color-mix(in_oklab,var(--muted-foreground)_10%,transparent)] hover:text-[color:var(--foreground)] focus:bg-[color:color-mix(in_oklab,var(--muted-foreground)_10%,transparent)] focus:text-[color:var(--foreground)]";
|
|
@@ -254,10 +253,7 @@ function normalizeFontPickerFontSize(value: unknown): number {
|
|
|
254
253
|
return defaultFontPickerFontSizePx;
|
|
255
254
|
}
|
|
256
255
|
|
|
257
|
-
return Math.
|
|
258
|
-
maxFontPickerFontSizePx,
|
|
259
|
-
Math.max(minFontPickerFontSizePx, Math.round(value)),
|
|
260
|
-
);
|
|
256
|
+
return Math.max(minFontPickerFontSizePx, Math.round(value));
|
|
261
257
|
}
|
|
262
258
|
|
|
263
259
|
function getFontFamilyStyle(font: FontPickerFontCatalogEntry | null): React.CSSProperties | undefined {
|
|
@@ -1131,7 +1127,6 @@ export function FontPickerControl({
|
|
|
1131
1127
|
aria-label="Font size"
|
|
1132
1128
|
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
1133
1129
|
disabled={disabled}
|
|
1134
|
-
max={maxFontPickerFontSizePx}
|
|
1135
1130
|
min={minFontPickerFontSizePx}
|
|
1136
1131
|
onBlur={() => commitFontSizeDraft()}
|
|
1137
1132
|
onChange={(event) => setFontSizeDraft(event.target.value)}
|
|
@@ -84,6 +84,14 @@ export type {
|
|
|
84
84
|
ChannelMixerControlProps as ChannelMixerProps,
|
|
85
85
|
ChannelMixerValues,
|
|
86
86
|
} from "./channel-mixer";
|
|
87
|
+
export {
|
|
88
|
+
CollectionActionsControl,
|
|
89
|
+
CollectionActionsControl as CollectionActions,
|
|
90
|
+
} from "./collection-actions";
|
|
91
|
+
export type {
|
|
92
|
+
CollectionActionsControlProps,
|
|
93
|
+
CollectionActionsControlProps as CollectionActionsProps,
|
|
94
|
+
} from "./collection-actions";
|
|
87
95
|
export { createControlHistoryGroupId } from "./control-types";
|
|
88
96
|
export type {
|
|
89
97
|
ControlChangeHistoryMode,
|
|
@@ -107,6 +115,7 @@ export type {
|
|
|
107
115
|
} from "./curves";
|
|
108
116
|
export { FileDropControl, FileDropControl as FileDrop } from "./file-drop";
|
|
109
117
|
export type {
|
|
118
|
+
FileDropAssetKind,
|
|
110
119
|
FileDropControlProps,
|
|
111
120
|
FileDropControlProps as FileDropProps,
|
|
112
121
|
FileDropPreview,
|
|
@@ -109,6 +109,10 @@ function PanelSectionCollapsibleBody({
|
|
|
109
109
|
}): React.JSX.Element | null {
|
|
110
110
|
const [shouldRender, setShouldRender] = React.useState(!collapsed);
|
|
111
111
|
const [isVisuallyCollapsed, setIsVisuallyCollapsed] = React.useState(collapsed);
|
|
112
|
+
const suppressControlTransitions =
|
|
113
|
+
useInitialPanelSectionControlTransitionSuppression(
|
|
114
|
+
`${collapsed ? "collapsed" : "expanded"}:${shouldRender ? "mounted" : "unmounted"}`,
|
|
115
|
+
);
|
|
112
116
|
|
|
113
117
|
React.useEffect(() => {
|
|
114
118
|
if (collapsed) {
|
|
@@ -155,11 +159,59 @@ function PanelSectionCollapsibleBody({
|
|
|
155
159
|
data-slot="panel-section-collapsible-body"
|
|
156
160
|
onTransitionEnd={handleTransitionEnd}
|
|
157
161
|
>
|
|
158
|
-
<div
|
|
162
|
+
<div
|
|
163
|
+
className="min-h-0 overflow-hidden"
|
|
164
|
+
data-toolcraft-controls-mounting={
|
|
165
|
+
suppressControlTransitions ? "true" : undefined
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</div>
|
|
159
170
|
</div>
|
|
160
171
|
);
|
|
161
172
|
}
|
|
162
173
|
|
|
174
|
+
function useInitialPanelSectionControlTransitionSuppression(
|
|
175
|
+
dependency: unknown,
|
|
176
|
+
): boolean {
|
|
177
|
+
const [suppressionState, setSuppressionState] = React.useState(() => ({
|
|
178
|
+
dependency,
|
|
179
|
+
isSuppressing: true,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
setSuppressionState({ dependency, isSuppressing: true });
|
|
184
|
+
|
|
185
|
+
if (typeof window === "undefined") {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
190
|
+
const timeout = window.setTimeout(() => {
|
|
191
|
+
setSuppressionState({ dependency, isSuppressing: false });
|
|
192
|
+
}, 0);
|
|
193
|
+
|
|
194
|
+
return () => window.clearTimeout(timeout);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let secondFrame = 0;
|
|
198
|
+
const firstFrame = window.requestAnimationFrame(() => {
|
|
199
|
+
secondFrame = window.requestAnimationFrame(() => {
|
|
200
|
+
setSuppressionState({ dependency, isSuppressing: false });
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
window.cancelAnimationFrame(firstFrame);
|
|
206
|
+
window.cancelAnimationFrame(secondFrame);
|
|
207
|
+
};
|
|
208
|
+
}, [dependency]);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
suppressionState.dependency !== dependency || suppressionState.isSuppressing
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
163
215
|
function getPanelSectionChildKey(
|
|
164
216
|
child: React.ReactNode,
|
|
165
217
|
index: number,
|