@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.
Files changed (62) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +185 -23
  7. package/templates/runtime/contracts/component-contracts.ts +91 -36
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +4 -4
  10. package/templates/runtime/export/export.test.ts +31 -0
  11. package/templates/runtime/export/export.ts +41 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +77 -1
  13. package/templates/runtime/react/canvas-shell.tsx +178 -23
  14. package/templates/runtime/react/control-conditions.ts +166 -0
  15. package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
  16. package/templates/runtime/react/controls-panel.test.tsx +774 -17
  17. package/templates/runtime/react/controls-panel.tsx +155 -8
  18. package/templates/runtime/react/media-file.ts +19 -0
  19. package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
  20. package/templates/runtime/schema/define-toolcraft.ts +29 -3
  21. package/templates/runtime/schema/types.ts +7 -0
  22. package/templates/runtime/state/reducer.test.ts +304 -0
  23. package/templates/runtime/state/reducer.ts +148 -9
  24. package/templates/runtime/state/types.ts +10 -2
  25. package/templates/runtime/testing/performance.test.ts +1424 -56
  26. package/templates/runtime/testing/performance.ts +710 -43
  27. package/templates/starter/AGENTS.md +10 -9
  28. package/templates/starter/docs/toolcraft/README.md +1 -1
  29. package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
  30. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
  31. package/templates/starter/docs/toolcraft/component-rules.md +46 -16
  32. package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
  33. package/templates/starter/docs/toolcraft/performance.md +54 -6
  34. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  35. package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
  36. package/templates/starter/docs/toolcraft/workflow.md +2 -2
  37. package/templates/starter/e2e/app-performance.spec.ts +136 -3
  38. package/templates/starter/e2e/performance-helpers.ts +197 -0
  39. package/templates/starter/gitignore +1 -0
  40. package/templates/starter/package.json +5 -0
  41. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  42. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  43. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  44. package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
  45. package/templates/starter/src/app/starter-acceptance.ts +740 -24
  46. package/templates/starter/src/app/starter-performance.test.ts +66 -5
  47. package/templates/ui/components/control-layout/index.tsx +8 -3
  48. package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
  49. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  50. package/templates/ui/components/controls/color/index.ts +4 -1
  51. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  52. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  53. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
  54. package/templates/ui/components/controls/file-drop/index.ts +1 -1
  55. package/templates/ui/components/controls/index.ts +3 -0
  56. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  57. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  58. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  59. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  60. package/templates/ui/components/controls/vector/index.ts +1 -0
  61. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  62. 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 { CloudArrowUpIcon, PlusIcon, XIcon } from "@phosphor-icons/react";
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
- ? "Drop image files"
148
- : "Replace image file"
393
+ ? assetKind === "file"
394
+ ? "Drop files"
395
+ : "Drop image files"
396
+ : assetKind === "file"
397
+ ? "Replace file"
398
+ : "Replace image file"
149
399
  : multiple
150
- ? "Browse image files"
151
- : "Browse image file"
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 hover:border-[color:color-mix(in_oklab,var(--border)_35%,transparent)] hover:bg-[color:color-mix(in_oklab,var(--foreground)_6%,transparent)] 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)]",
155
- hasPreview ? "overflow-hidden p-2" : "px-3 py-3",
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={openFileDialog}
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
- {shouldRenderPreviewGrid ? (
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
- {previewItems.map((item, index) => (
187
- <div
188
- className="relative aspect-square min-w-0 overflow-hidden rounded-[calc(var(--radius-lg)-4px)] bg-[color:color-mix(in_oklab,var(--foreground)_6%,transparent)]"
189
- data-slot="file-upload-preview-item"
190
- key={item.id ?? `${item.src}-${index}`}
191
- >
192
- <img
193
- alt={item.alt ?? ""}
194
- className="size-full object-cover"
195
- draggable={false}
196
- height={item.size?.height}
197
- src={item.src}
198
- width={item.size?.width}
199
- />
200
- {onPreviewRemove ? (
201
- <Button
202
- aria-label={`Remove ${item.alt ?? "image"}`}
203
- className="absolute top-1 right-1"
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)_35%,transparent)] hover:bg-[color:color-mix(in_oklab,var(--foreground)_7%,transparent)] hover:text-[color:var(--foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--ring)]"
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
- <PlusIcon className="size-4" weight="regular" />
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
- <span>Click to upload an image</span>
267
- <span>or drag it onto the canvas</span>
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
  )}
@@ -2,4 +2,4 @@
2
2
 
3
3
  export { FileDropControl } from "./file-drop-control";
4
4
  export type { FileDropControlProps } from "./file-drop-control";
5
- export type { FileDropPreview } from "./file-drop-control";
5
+ export type { FileDropAssetKind, FileDropPreview } from "./file-drop-control";
@@ -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 className="h-fit min-w-0 items-center justify-between gap-3" orientation="horizontal">
161
- <ControlFieldLabel>{name}</ControlFieldLabel>
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
- isStacked
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}
@@ -27,7 +27,6 @@ const compactSliderValueUnits = new Set([
27
27
  "vh",
28
28
  "vmin",
29
29
  "vmax",
30
- "x",
31
30
  "s",
32
31
  "ms",
33
32
  ]);
@@ -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)}
@@ -4,5 +4,6 @@ export { VectorControl } from "./vector-control";
4
4
  export type {
5
5
  VectorControlProps,
6
6
  VectorControlValue,
7
+ VectorPadCoordinateMode,
7
8
  VectorPadVariant,
8
9
  } from "./vector-control";