slice-machine-ui 2.19.2-alpha.jp-figma-to-prismic.1 → 2.19.2-alpha.jp-figma-to-prismic.3

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 (31) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/{dTxDQVqdse1UxnwBqviBt → 8UM-Fh6SBL5e0B_msyLxt}/_buildManifest.js +1 -1
  3. package/out/_next/static/chunks/20-4cb8941c8aafa019.js +1 -0
  4. package/out/_next/static/chunks/593-97393b59cba3d429.js +1 -0
  5. package/out/_next/static/chunks/907-fbd4308471792876.js +1 -0
  6. package/out/_next/static/chunks/pages/{_app-45762da0ab33fb54.js → _app-a9ca8c9f371bd423.js} +1 -1
  7. package/out/_next/static/chunks/pages/{changes-7f24b37f5bf872ae.js → changes-b3f45dfeb5dc08f0.js} +1 -1
  8. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-1b47424a37b49dff.js → [customTypeId]-02278526092bcf5c.js} +1 -1
  9. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-385f933c203e8b16.js → [pageTypeId]-4d99de1b52de7c9b.js} +1 -1
  10. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-08cdeefc96106c0c.js → [variation]-330e5d545d9f6269.js} +1 -1
  11. package/out/_next/static/chunks/pages/{slices-bedcb854fbdca8cd.js → slices-de5f8cf4719e88c4.js} +1 -1
  12. package/out/changelog.html +1 -1
  13. package/out/changes.html +1 -1
  14. package/out/custom-types/[customTypeId].html +1 -1
  15. package/out/custom-types.html +1 -1
  16. package/out/index.html +1 -1
  17. package/out/labs.html +1 -1
  18. package/out/page-types/[pageTypeId].html +1 -1
  19. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  20. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  21. package/out/slices.html +1 -1
  22. package/package.json +3 -3
  23. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/CreateSliceFromImageModal.tsx +362 -198
  24. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +43 -11
  25. package/src/icons/FigmaIcon.tsx +78 -34
  26. package/src/legacy/components/ScreenshotChangesModal/index.tsx +1 -1
  27. package/test/src/modules/__fixtures__/serverState.ts +2 -0
  28. package/out/_next/static/chunks/34-28725deef8b874b1.js +0 -1
  29. package/out/_next/static/chunks/658-8231c0b729e0124a.js +0 -1
  30. package/out/_next/static/chunks/907-0225b94cf58be87d.js +0 -1
  31. /package/out/_next/static/{dTxDQVqdse1UxnwBqviBt → 8UM-Fh6SBL5e0B_msyLxt}/_ssgManifest.js +0 -0
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  BlankSlate,
3
- BlankSlateActions,
4
- BlankSlateDescription,
5
3
  BlankSlateIcon,
6
- BlankSlateTitle,
7
4
  Box,
8
5
  Button,
9
6
  Dialog,
@@ -16,6 +13,7 @@ import {
16
13
  FileDropZone,
17
14
  FileUploadButton,
18
15
  ScrollArea,
16
+ Text,
19
17
  } from "@prismicio/editor-ui";
20
18
  import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
21
19
  import { useEffect, useRef, useState } from "react";
@@ -27,6 +25,7 @@ import { getState, telemetry } from "@/apiClient";
27
25
  import { addAiFeedback } from "@/features/aiFeedback";
28
26
  import { useOnboarding } from "@/features/onboarding/useOnboarding";
29
27
  import { useAutoSync } from "@/features/sync/AutoSyncProvider";
28
+ import { FigmaIcon } from "@/icons/FigmaIcon";
30
29
  import { managerClient } from "@/managerClient";
31
30
  import useSliceMachineActions from "@/modules/useSliceMachineActions";
32
31
 
@@ -56,12 +55,12 @@ interface CreateSliceFromImageModalProps {
56
55
  export function CreateSliceFromImageModal(
57
56
  props: CreateSliceFromImageModalProps,
58
57
  ) {
59
- const { open, location, onSuccess, onClose } = props;
58
+ const { open, location, onClose, onSuccess } = props;
60
59
  const [slices, setSlices] = useState<Slice[]>([]);
61
- const [isCreatingSlices, setIsCreatingSlices] = useState(false);
62
60
  const { syncChanges } = useAutoSync();
63
61
  const { createSliceSuccess } = useSliceMachineActions();
64
62
  const { completeStep } = useOnboarding();
63
+ const existingSlices = useExistingSlices({ open });
65
64
 
66
65
  /**
67
66
  * Keeps track of the current instance id.
@@ -69,6 +68,21 @@ export function CreateSliceFromImageModal(
69
68
  */
70
69
  const id = useRef(crypto.randomUUID());
71
70
 
71
+ useHotkeys(
72
+ ["meta+v", "ctrl+v"],
73
+ (event) => {
74
+ event.preventDefault();
75
+ void handlePaste();
76
+ },
77
+ { enabled: open },
78
+ );
79
+
80
+ useEffect(() => {
81
+ if (slices.every((slice) => slice.status === "success")) {
82
+ void onAllComplete();
83
+ }
84
+ }, [slices]); // eslint-disable-line react-hooks/exhaustive-deps
85
+
72
86
  const setSlice = (args: {
73
87
  index: number;
74
88
  slice: (prevSlice: Slice) => Slice;
@@ -78,7 +92,7 @@ export function CreateSliceFromImageModal(
78
92
  };
79
93
 
80
94
  const onOpenChange = (open: boolean) => {
81
- if (open || isCreatingSlices) return;
95
+ if (open) return;
82
96
  onClose();
83
97
  id.current = crypto.randomUUID();
84
98
  setSlices([]);
@@ -92,17 +106,33 @@ export function CreateSliceFromImageModal(
92
106
  return;
93
107
  }
94
108
 
95
- setSlices(
96
- images.map((image) => ({
97
- status: "uploading",
98
- image,
99
- })),
100
- );
101
-
102
- images.forEach((image, index) => uploadImage({ index, image }));
109
+ const startIndex = slices.length;
110
+ setSlices((prevSlices) => [
111
+ ...prevSlices,
112
+ ...images.map(
113
+ (image): Slice => ({
114
+ status: "uploading",
115
+ source: "upload",
116
+ image,
117
+ }),
118
+ ),
119
+ ]);
120
+
121
+ images.forEach((imageData, relativeIndex) => {
122
+ const index = startIndex + relativeIndex;
123
+ void uploadImage({ index, imageData, source: "upload" });
124
+ });
103
125
  };
104
126
 
105
127
  const handlePaste = async () => {
128
+ if (!open) return;
129
+
130
+ // Don't allow pasting while uploads or generation are in progress
131
+ const isLoading = slices.some(
132
+ (slice) => slice.status === "uploading" || slice.status === "generating",
133
+ );
134
+ if (isLoading) return;
135
+
106
136
  const supportsClipboardRead =
107
137
  typeof navigator.clipboard?.read === "function";
108
138
 
@@ -124,12 +154,9 @@ export function CreateSliceFromImageModal(
124
154
 
125
155
  // Method 1: Try to extract image from clipboard image/png blob (preferred)
126
156
  for (const item of clipboardItems) {
127
- console.log("Clipboard item types:", item.types);
128
-
129
157
  const imageType = item.types.find((type) => type.startsWith("image/"));
130
158
  if (imageType !== undefined) {
131
159
  imageBlob = await item.getType(imageType);
132
- console.log("Extracted image from clipboard image type:", imageType);
133
160
  break;
134
161
  }
135
162
  }
@@ -146,14 +173,11 @@ export function CreateSliceFromImageModal(
146
173
  success = true;
147
174
  const data = result.data;
148
175
  imageName = `${data.name}.png`;
149
- console.log("Extracted name from text/plain JSON:", data);
150
176
 
151
177
  // Use base64 image as fallback if no blob was found
152
178
  if (!imageBlob) {
153
179
  const response = await fetch(data.image);
154
180
  imageBlob = await response.blob();
155
- console.log("Extracted image from base64 fallback");
156
- console.log("Image blob type:", imageBlob.type);
157
181
  }
158
182
  } else {
159
183
  console.warn("Clipboard data validation failed:", result.error);
@@ -168,11 +192,11 @@ export function CreateSliceFromImageModal(
168
192
  if (!imageBlob) {
169
193
  if (success) {
170
194
  toast.error(
171
- "Figma data found but image could not be extracted. Please try copying again from Figma.",
195
+ "Could not extract Figma data from clipboard. Please try copying again using the Prismic Figma plugin.",
172
196
  );
173
197
  } else {
174
198
  toast.error(
175
- "No Figma data found in clipboard. Make sure you've copied a frame from Figma using the Prismic plugin.",
199
+ "No Figma data found in clipboard. Make sure you've copied a design using the Prismic Figma plugin.",
176
200
  );
177
201
  }
178
202
  return;
@@ -188,20 +212,23 @@ export function CreateSliceFromImageModal(
188
212
  }
189
213
 
190
214
  // Create File object from blob and append to existing slices
191
- const file = new File([imageBlob], imageName, { type: imageBlob.type });
215
+ const imageData = new File([imageBlob], imageName, {
216
+ type: imageBlob.type,
217
+ });
192
218
  const newIndex = currentSliceCount;
193
219
 
194
220
  // Append new slice to existing ones
195
221
  setSlices((prevSlices) => [
196
222
  ...prevSlices,
197
223
  {
224
+ source: "figma",
198
225
  status: "uploading",
199
- image: file,
226
+ image: imageData,
200
227
  },
201
228
  ]);
202
229
 
203
230
  // Start uploading the new image
204
- uploadImage({ index: newIndex, image: file });
231
+ void uploadImage({ index: newIndex, imageData, source: "figma" });
205
232
 
206
233
  toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
207
234
  } catch (error) {
@@ -212,18 +239,12 @@ export function CreateSliceFromImageModal(
212
239
  }
213
240
  };
214
241
 
215
- // Enable paste with Cmd+V / Ctrl+V when modal is open
216
- useHotkeys(
217
- ["meta+v", "ctrl+v"],
218
- (event) => {
219
- event.preventDefault();
220
- void handlePaste();
221
- },
222
- { enabled: open },
223
- );
224
-
225
- const uploadImage = (args: { index: number; image: File }) => {
226
- const { index, image } = args;
242
+ const uploadImage = async (args: {
243
+ index: number;
244
+ imageData: File;
245
+ source: "figma" | "upload";
246
+ }) => {
247
+ const { index, imageData, source } = args;
227
248
  const currentId = id.current;
228
249
 
229
250
  setSlice({
@@ -231,32 +252,63 @@ export function CreateSliceFromImageModal(
231
252
  slice: (prevSlice) => ({
232
253
  ...prevSlice,
233
254
  status: "uploading",
255
+ image: imageData,
256
+ source,
234
257
  }),
235
258
  });
236
259
 
237
- getImageUrl({ image }).then(
238
- (imageUrl) => {
239
- if (currentId !== id.current) return;
240
- inferSlice({ index, imageUrl });
241
- },
242
- () => {
243
- if (currentId !== id.current) return;
244
- setSlice({
260
+ try {
261
+ const imageUrl = await getImageUrl({ image: imageData });
262
+ if (currentId !== id.current) return;
263
+
264
+ setSlice({
265
+ index,
266
+ slice: (prevSlice) => ({
267
+ ...prevSlice,
268
+ status: "pending",
269
+ thumbnailUrl: imageUrl,
270
+ }),
271
+ });
272
+ } catch {
273
+ if (currentId !== id.current) return;
274
+ setSlice({
275
+ index,
276
+ slice: (prevSlice) => ({
277
+ ...prevSlice,
278
+ status: "uploadError",
279
+ onRetry: () => void uploadImage({ index, imageData, source }),
280
+ }),
281
+ });
282
+ }
283
+ };
284
+
285
+ const generateAllPendingSlices = async () => {
286
+ const smConfig = await managerClient.project.getSliceMachineConfig();
287
+ const libraryID = smConfig?.libraries?.[0];
288
+ if (libraryID === undefined) {
289
+ throw new Error("No library found in the config.");
290
+ }
291
+
292
+ // Generate all pending slices simultaneously
293
+ slices.forEach((slice, index) => {
294
+ if (slice.status === "pending") {
295
+ void inferSlice({
245
296
  index,
246
- slice: (prevSlice) => ({
247
- ...prevSlice,
248
- status: "uploadError",
249
- onRetry: () => uploadImage({ index, image }),
250
- }),
297
+ imageUrl: slice.thumbnailUrl,
298
+ libraryID,
299
+ source: slice.source,
251
300
  });
252
- },
253
- );
301
+ }
302
+ });
254
303
  };
255
304
 
256
- const existingSlices = useExistingSlices({ open });
257
-
258
- const inferSlice = (args: { index: number; imageUrl: string }) => {
259
- const { index, imageUrl } = args;
305
+ const inferSlice = async (args: {
306
+ index: number;
307
+ imageUrl: string;
308
+ libraryID: string;
309
+ source: "figma" | "upload";
310
+ }) => {
311
+ const { index, imageUrl, libraryID, source } = args;
260
312
  const currentId = id.current;
261
313
 
262
314
  setSlice({
@@ -268,110 +320,190 @@ export function CreateSliceFromImageModal(
268
320
  }),
269
321
  });
270
322
 
271
- managerClient.customTypes.inferSlice({ imageUrl }).then(
272
- ({ slice, langSmithUrl }) => {
273
- if (currentId !== id.current) return;
274
-
275
- setSlices((prevSlices) =>
276
- prevSlices.map((prevSlice, i) =>
277
- i === index
278
- ? {
279
- ...prevSlice,
280
- status: "success",
281
- thumbnailUrl: imageUrl,
282
- model: sliceWithoutConflicts({
283
- existingSlices: existingSlices.current,
284
- newSlices: prevSlices,
285
- slice,
286
- }),
287
- langSmithUrl,
288
- }
289
- : prevSlice,
290
- ),
291
- );
292
- },
293
- () => {
294
- if (currentId !== id.current) return;
295
- setSlice({
296
- index,
297
- slice: (prevSlice) => ({
323
+ try {
324
+ const inferResult = await managerClient.customTypes.inferSlice({
325
+ source,
326
+ libraryID,
327
+ imageUrl,
328
+ });
329
+
330
+ if (currentId !== id.current) return;
331
+
332
+ setSlices((prevSlices) => {
333
+ return prevSlices.map((prevSlice, i) => {
334
+ if (i !== index) return prevSlice;
335
+ return {
298
336
  ...prevSlice,
299
- status: "generateError",
337
+ status: "success",
300
338
  thumbnailUrl: imageUrl,
301
- onRetry: () => inferSlice({ index, imageUrl }),
302
- }),
339
+ model: sliceWithoutConflicts({
340
+ existingSlices: existingSlices.current,
341
+ newSlices: slices,
342
+ slice: inferResult.slice,
343
+ }),
344
+ };
303
345
  });
346
+ });
347
+ } catch {
348
+ if (currentId !== id.current) return;
349
+ setSlice({
350
+ index,
351
+ slice: (prevSlice) => ({
352
+ ...prevSlice,
353
+ status: "generateError",
354
+ thumbnailUrl: imageUrl,
355
+ onRetry: () => {
356
+ void inferSlice({ index, imageUrl, libraryID, source });
357
+ },
358
+ }),
359
+ });
360
+ }
361
+ };
362
+
363
+ const onAllComplete = async () => {
364
+ const newSlices = slices.reduce<{ upload: NewSlice[]; figma: NewSlice[] }>(
365
+ (acc, slice) => {
366
+ if (slice.status === "success") {
367
+ if (slice.source === "upload") {
368
+ acc.upload.push(slice);
369
+ } else {
370
+ acc.figma.push(slice);
371
+ }
372
+ }
373
+ return acc;
304
374
  },
375
+ { upload: [], figma: [] },
305
376
  );
306
- };
307
377
 
308
- const onSubmit = () => {
309
- const newSlices = slices.reduce<NewSlice[]>((acc, slice) => {
310
- if (slice.status === "success") acc.push(slice);
311
- return acc;
312
- }, []);
313
- if (!newSlices.length) return;
378
+ if (!newSlices.upload.length && !newSlices.figma.length) return;
314
379
 
315
380
  const currentId = id.current;
316
- setIsCreatingSlices(true);
317
- addSlices(newSlices)
318
- .then(async ({ slices, library }) => {
319
- if (currentId !== id.current) return;
320
-
321
- const serverState = await getState();
322
- createSliceSuccess(serverState.libraries);
323
- syncChanges();
324
-
325
- onSuccess({ slices, library });
326
-
327
- setIsCreatingSlices(false);
328
- id.current = crypto.randomUUID();
329
- setSlices([]);
330
-
331
- void completeStep("createSlice");
332
-
333
- for (const { model, langSmithUrl } of slices) {
334
- void telemetry.track({
335
- event: "slice:created",
336
- id: model.id,
337
- name: model.name,
338
- library,
339
- location,
340
- mode: "ai",
341
- langSmithUrl,
342
- });
343
-
344
- addAiFeedback({
345
- type: "model",
346
- library,
347
- sliceId: model.id,
348
- variationId: model.variations[0].id,
349
- langSmithUrl,
350
- });
351
- }
352
- })
353
- .catch(() => {
354
- if (currentId !== id.current) return;
355
- setIsCreatingSlices(false);
356
- toast.error("An unexpected error happened while adding slices.");
357
- });
381
+ try {
382
+ // Only the slices generated from uploaded images need this step
383
+ const { slices, library } = await addSlices(newSlices.upload);
384
+ if (currentId !== id.current) return;
385
+
386
+ const serverState = await getState();
387
+ createSliceSuccess(serverState.libraries);
388
+ syncChanges();
389
+
390
+ const total = newSlices.upload.length + newSlices.figma.length;
391
+ toast.success(
392
+ `${total} new slice${total > 1 ? "s" : ""} successfully generated.`,
393
+ );
394
+
395
+ onSuccess({ slices: [...newSlices.upload, ...newSlices.figma], library });
396
+ id.current = crypto.randomUUID();
397
+ setSlices([]);
398
+
399
+ void completeStep("createSlice");
400
+
401
+ for (const { model, langSmithUrl } of slices) {
402
+ void telemetry.track({
403
+ event: "slice:created",
404
+ id: model.id,
405
+ name: model.name,
406
+ library,
407
+ location,
408
+ mode: "ai",
409
+ langSmithUrl,
410
+ });
411
+
412
+ addAiFeedback({
413
+ type: "model",
414
+ library,
415
+ sliceId: model.id,
416
+ variationId: model.variations[0].id,
417
+ langSmithUrl,
418
+ });
419
+ }
420
+ } catch {
421
+ if (currentId !== id.current) return;
422
+ toast.error("An unexpected error happened while adding slices.");
423
+ }
358
424
  };
359
425
 
360
- const areSlicesLoading = slices.some(
361
- (slice) => slice.status === "uploading" || slice.status === "generating",
362
- );
363
- const readySlices = slices.filter((slice) => slice.status === "success");
364
- const someSlicesReady = readySlices.length > 0;
426
+ const handleClose = () => {
427
+ id.current = crypto.randomUUID();
428
+ setSlices([]);
429
+ onClose();
430
+ };
431
+
432
+ const loadingSliceCount = slices.filter((slice) => {
433
+ return slice.status === "uploading" || slice.status === "generating";
434
+ }).length;
435
+
436
+ const pendingSliceCount = slices.filter((slice) => {
437
+ return slice.status === "pending";
438
+ }).length;
439
+
440
+ const hasTriggeredGeneration = slices.some((slice) => {
441
+ return slice.status === "generating" || slice.status === "success";
442
+ });
443
+
444
+ const generateSliceCount = loadingSliceCount + pendingSliceCount;
365
445
 
366
446
  return (
367
- <Dialog open={open} onOpenChange={onOpenChange}>
368
- <DialogHeader title="Generate from image" />
447
+ <Dialog
448
+ open={open}
449
+ onOpenChange={loadingSliceCount > 0 ? undefined : onOpenChange}
450
+ >
451
+ <DialogHeader title="Generate with AI" />
369
452
  <DialogContent gap={0}>
370
453
  <DialogDescription hidden>
371
454
  Upload images to generate slices with AI
372
455
  </DialogDescription>
373
456
  {slices.length === 0 ? (
374
- <Box padding={16} height="100%">
457
+ <Box
458
+ padding={16}
459
+ height="100%"
460
+ gap={16}
461
+ display="flex"
462
+ flexDirection="column"
463
+ >
464
+ <Box
465
+ display="flex"
466
+ gap={16}
467
+ alignItems="center"
468
+ backgroundColor="grey2"
469
+ padding={16}
470
+ borderRadius={12}
471
+ >
472
+ <Box display="flex" gap={8} alignItems="center" flexGrow={1}>
473
+ <Box
474
+ width={48}
475
+ height={48}
476
+ backgroundColor="grey12"
477
+ borderRadius="100%"
478
+ display="flex"
479
+ alignItems="center"
480
+ justifyContent="center"
481
+ >
482
+ <FigmaIcon variant="original" height={25} />
483
+ </Box>
484
+ <Box display="flex" flexDirection="column" flexGrow={1}>
485
+ <Text variant="bold">Want to work faster?</Text>
486
+ <Text variant="small" color="grey11">
487
+ Copy frames from Figma with the Slice Machine plugin and
488
+ paste them here.
489
+ </Text>
490
+ </Box>
491
+ </Box>
492
+ <Button
493
+ endIcon="arrowForward"
494
+ color="indigo"
495
+ onClick={() =>
496
+ window.open(
497
+ "https://www.figma.com/community/plugin/TODO",
498
+ "_blank",
499
+ )
500
+ }
501
+ sx={{ marginRight: 8 }}
502
+ invisible
503
+ >
504
+ Install plugin
505
+ </Button>
506
+ </Box>
375
507
  <FileDropZone
376
508
  onFilesSelected={onImagesSelected}
377
509
  assetType="image"
@@ -391,28 +523,61 @@ export function CreateSliceFromImageModal(
391
523
  </FileDropZone>
392
524
  </Box>
393
525
  ) : (
394
- <ScrollArea stableScrollbar={false}>
526
+ <>
395
527
  <Box
396
- display="grid"
397
- gridTemplateColumns="1fr 1fr"
398
- gap={16}
528
+ display="flex"
529
+ alignItems="center"
530
+ justifyContent="space-between"
399
531
  padding={16}
400
532
  >
401
- {slices.map((slice, index) => (
402
- <SliceCard slice={slice} key={`slice-${index}`} />
403
- ))}
533
+ <Text variant="h3">Design</Text>
534
+ <FileUploadButton
535
+ size="medium"
536
+ color="grey"
537
+ onFilesSelected={onImagesSelected}
538
+ startIcon="attachFile"
539
+ disabled={hasTriggeredGeneration}
540
+ >
541
+ Add images
542
+ </FileUploadButton>
404
543
  </Box>
405
- </ScrollArea>
544
+ <ScrollArea stableScrollbar={false}>
545
+ <Box
546
+ display="grid"
547
+ gridTemplateColumns="1fr 1fr"
548
+ gap={16}
549
+ padding={16}
550
+ >
551
+ {slices.map((slice, index) => (
552
+ <SliceCard slice={slice} key={`slice-${index}`} />
553
+ ))}
554
+ </Box>
555
+ </ScrollArea>
556
+ </>
406
557
  )}
407
558
 
408
559
  <DialogActions>
409
- <DialogCancelButton disabled={isCreatingSlices} />
560
+ <DialogCancelButton
561
+ size="medium"
562
+ onClick={handleClose}
563
+ disabled={loadingSliceCount > 0}
564
+ >
565
+ Close
566
+ </DialogCancelButton>
410
567
  <DialogActionButton
411
- disabled={!someSlicesReady || areSlicesLoading}
412
- loading={isCreatingSlices}
413
- onClick={onSubmit}
568
+ color="purple"
569
+ startIcon="autoFixHigh"
570
+ onClick={() => void generateAllPendingSlices()}
571
+ disabled={
572
+ hasTriggeredGeneration ||
573
+ loadingSliceCount > 0 ||
574
+ pendingSliceCount === 0
575
+ }
576
+ loading={hasTriggeredGeneration}
577
+ size="medium"
414
578
  >
415
- {getSubmitButtonLabel(location)} ({readySlices.length})
579
+ Generate {generateSliceCount > 0 ? `(${generateSliceCount}) ` : ""}
580
+ {generateSliceCount === 1 ? "Slice" : "Slices"}
416
581
  </DialogActionButton>
417
582
  </DialogActions>
418
583
  </DialogContent>
@@ -436,38 +601,49 @@ function UploadBlankSlate(props: {
436
601
  border
437
602
  borderStyle="dashed"
438
603
  borderColor={droppingFiles ? "purple9" : "grey6"}
604
+ borderRadius={12}
605
+ flexGrow={1}
439
606
  >
440
607
  <BlankSlate>
441
- <BlankSlateIcon
442
- lineColor="purple11"
443
- backgroundColor="purple5"
444
- name="cloudUpload"
445
- size="large"
446
- />
447
- <BlankSlateTitle>Upload your design images.</BlankSlateTitle>
448
- <BlankSlateDescription>
449
- Once uploaded, you can generate slices automatically using AI.
450
- </BlankSlateDescription>
451
- <BlankSlateActions>
452
- <FileUploadButton
453
- startIcon="attachFile"
454
- onFilesSelected={onFilesSelected}
455
- color="grey"
456
- >
457
- Add images
458
- </FileUploadButton>
459
- </BlankSlateActions>
460
- <BlankSlateDescription>Or</BlankSlateDescription>
461
- <BlankSlateActions>
462
- <Button
463
- size="small"
464
- startIcon="contentPaste"
465
- color="grey"
466
- onClick={onPaste}
608
+ <Box display="flex" flexDirection="column" gap={16} alignItems="center">
609
+ <BlankSlateIcon
610
+ lineColor="purple11"
611
+ backgroundColor="purple5"
612
+ name="cloudUpload"
613
+ size="large"
614
+ />
615
+ <Box
616
+ display="flex"
617
+ flexDirection="column"
618
+ gap={4}
619
+ alignItems="center"
467
620
  >
468
- Paste from Figma
469
- </Button>
470
- </BlankSlateActions>
621
+ <Text>Generate slices from your designs</Text>
622
+ <Text variant="small" color="grey11">
623
+ Upload your design images or paste them directly from Figma.
624
+ </Text>
625
+ </Box>
626
+ <Box display="flex" alignItems="center" gap={16}>
627
+ <Button
628
+ size="small"
629
+ renderStartIcon={() => (
630
+ <FigmaIcon variant="original" height={16} />
631
+ )}
632
+ color="grey"
633
+ onClick={onPaste}
634
+ >
635
+ Paste from Figma
636
+ </Button>
637
+ <FileUploadButton
638
+ size="small"
639
+ onFilesSelected={onFilesSelected}
640
+ color="purple"
641
+ invisible
642
+ >
643
+ Add images
644
+ </FileUploadButton>
645
+ </Box>
646
+ </Box>
471
647
  </BlankSlate>
472
648
  </Box>
473
649
  );
@@ -494,6 +670,7 @@ type NewSlice = {
494
670
  image: File;
495
671
  model: SharedSlice;
496
672
  langSmithUrl?: string;
673
+ source: "figma" | "upload";
497
674
  };
498
675
 
499
676
  /**
@@ -604,16 +781,3 @@ async function addSlices(newSlices: NewSlice[]) {
604
781
 
605
782
  return { library, slices };
606
783
  }
607
-
608
- const getSubmitButtonLabel = (
609
- location: "custom_type" | "page_type" | "slices",
610
- ) => {
611
- switch (location) {
612
- case "custom_type":
613
- return "Add to type";
614
- case "page_type":
615
- return "Add to page";
616
- case "slices":
617
- return "Add to slices";
618
- }
619
- };