slice-machine-ui 2.19.1 → 2.19.2-alpha.jp-figma-to-prismic.2

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 (37) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/125-00b909bdbab2ca15.js +1 -0
  3. package/out/_next/static/chunks/20-4cb8941c8aafa019.js +1 -0
  4. package/out/_next/static/chunks/{344-fdb3008f4bb3b0c1.js → 344-b64f09e670634ed1.js} +1 -1
  5. package/out/_next/static/chunks/{500-d3989390f5e8da53.js → 444-d39213143f782fec.js} +1 -1
  6. package/out/_next/static/chunks/593-97393b59cba3d429.js +1 -0
  7. package/out/_next/static/chunks/66-d9d3bcb5d041cb6d.js +1 -0
  8. package/out/_next/static/chunks/907-88dafe5c1e80dead.js +1 -0
  9. package/out/_next/static/chunks/pages/{_app-664e26e8e0083aaa.js → _app-b73cf0344465689d.js} +1 -1
  10. package/out/_next/static/chunks/pages/{changes-8af4acbb8f974cb2.js → changes-b3f45dfeb5dc08f0.js} +1 -1
  11. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-af9376721beb489e.js → [customTypeId]-02278526092bcf5c.js} +1 -1
  12. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-a24665e91b882169.js → [pageTypeId]-4d99de1b52de7c9b.js} +1 -1
  13. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-e973a443d8b8a75d.js → [variation]-330e5d545d9f6269.js} +1 -1
  14. package/out/_next/static/chunks/pages/{slices-81c1c3f1bcad60f4.js → slices-de5f8cf4719e88c4.js} +1 -1
  15. package/out/_next/static/j0_D1z-ZN75hJO-JvwC0X/_buildManifest.js +1 -0
  16. package/out/changelog.html +1 -1
  17. package/out/changes.html +1 -1
  18. package/out/custom-types/[customTypeId].html +1 -1
  19. package/out/custom-types.html +1 -1
  20. package/out/index.html +1 -1
  21. package/out/labs.html +1 -1
  22. package/out/page-types/[pageTypeId].html +1 -1
  23. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  24. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  25. package/out/slices.html +1 -1
  26. package/package.json +5 -4
  27. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/CreateSliceFromImageModal.tsx +364 -155
  28. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +26 -9
  29. package/src/icons/FigmaIcon.tsx +78 -34
  30. package/src/legacy/components/ScreenshotChangesModal/index.tsx +1 -1
  31. package/test/src/modules/__fixtures__/serverState.ts +1 -0
  32. package/out/_next/static/chunks/34-28725deef8b874b1.js +0 -1
  33. package/out/_next/static/chunks/658-8231c0b729e0124a.js +0 -1
  34. package/out/_next/static/chunks/907-180eb33eefccc237.js +0 -1
  35. package/out/_next/static/chunks/918-fa4f2563cb5fd014.js +0 -1
  36. package/out/_next/static/mWW0JPKbrqF9bfSpOlAsb/_buildManifest.js +0 -1
  37. /package/out/_next/static/{mWW0JPKbrqF9bfSpOlAsb → j0_D1z-ZN75hJO-JvwC0X}/_ssgManifest.js +0 -0
@@ -1,12 +1,9 @@
1
1
  import {
2
2
  BlankSlate,
3
- BlankSlateActions,
4
- BlankSlateDescription,
5
3
  BlankSlateIcon,
6
- BlankSlateTitle,
7
4
  Box,
5
+ Button,
8
6
  Dialog,
9
- DialogActionButton,
10
7
  DialogActions,
11
8
  DialogCancelButton,
12
9
  DialogContent,
@@ -15,20 +12,30 @@ import {
15
12
  FileDropZone,
16
13
  FileUploadButton,
17
14
  ScrollArea,
15
+ Text,
18
16
  } from "@prismicio/editor-ui";
19
17
  import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
20
18
  import { useEffect, useRef, useState } from "react";
19
+ import { useHotkeys } from "react-hotkeys-hook";
21
20
  import { toast } from "react-toastify";
21
+ import { z } from "zod";
22
22
 
23
23
  import { getState, telemetry } from "@/apiClient";
24
24
  import { addAiFeedback } from "@/features/aiFeedback";
25
25
  import { useOnboarding } from "@/features/onboarding/useOnboarding";
26
26
  import { useAutoSync } from "@/features/sync/AutoSyncProvider";
27
+ import { FigmaIcon } from "@/icons/FigmaIcon";
27
28
  import { managerClient } from "@/managerClient";
28
29
  import useSliceMachineActions from "@/modules/useSliceMachineActions";
29
30
 
30
31
  import { Slice, SliceCard } from "./SliceCard";
31
32
 
33
+ const clipboardDataSchema = z.object({
34
+ __type: z.literal("figma-to-prismic/clipboard-data"),
35
+ name: z.string(),
36
+ image: z.string().startsWith("data:image/"),
37
+ });
38
+
32
39
  const IMAGE_UPLOAD_LIMIT = 10;
33
40
 
34
41
  interface CreateSliceFromImageModalProps {
@@ -47,12 +54,12 @@ interface CreateSliceFromImageModalProps {
47
54
  export function CreateSliceFromImageModal(
48
55
  props: CreateSliceFromImageModalProps,
49
56
  ) {
50
- const { open, location, onSuccess, onClose } = props;
57
+ const { open, location, onClose } = props;
51
58
  const [slices, setSlices] = useState<Slice[]>([]);
52
- const [isCreatingSlices, setIsCreatingSlices] = useState(false);
53
59
  const { syncChanges } = useAutoSync();
54
60
  const { createSliceSuccess } = useSliceMachineActions();
55
61
  const { completeStep } = useOnboarding();
62
+ const existingSlices = useExistingSlices({ open });
56
63
 
57
64
  /**
58
65
  * Keeps track of the current instance id.
@@ -60,6 +67,21 @@ export function CreateSliceFromImageModal(
60
67
  */
61
68
  const id = useRef(crypto.randomUUID());
62
69
 
70
+ useHotkeys(
71
+ ["meta+v", "ctrl+v"],
72
+ (event) => {
73
+ event.preventDefault();
74
+ void handlePaste();
75
+ },
76
+ { enabled: open },
77
+ );
78
+
79
+ useEffect(() => {
80
+ if (slices.every((slice) => slice.status === "success")) {
81
+ void onAllComplete();
82
+ }
83
+ }, [slices]); // eslint-disable-line react-hooks/exhaustive-deps
84
+
63
85
  const setSlice = (args: {
64
86
  index: number;
65
87
  slice: (prevSlice: Slice) => Slice;
@@ -69,7 +91,7 @@ export function CreateSliceFromImageModal(
69
91
  };
70
92
 
71
93
  const onOpenChange = (open: boolean) => {
72
- if (open || isCreatingSlices) return;
94
+ if (open) return;
73
95
  onClose();
74
96
  id.current = crypto.randomUUID();
75
97
  setSlices([]);
@@ -86,48 +108,170 @@ export function CreateSliceFromImageModal(
86
108
  setSlices(
87
109
  images.map((image) => ({
88
110
  status: "uploading",
111
+ source: "upload",
89
112
  image,
90
113
  })),
91
114
  );
92
115
 
93
- images.forEach((image, index) => uploadImage({ index, image }));
116
+ images.forEach((imageData, index) => {
117
+ void generateSlice({ index, imageData, source: "upload" });
118
+ });
94
119
  };
95
120
 
96
- const uploadImage = (args: { index: number; image: File }) => {
97
- const { index, image } = args;
121
+ const handlePaste = async () => {
122
+ // Limit just to one paste at a time for now
123
+ if (!open || slices.length > 0) return;
124
+
125
+ const supportsClipboardRead =
126
+ typeof navigator.clipboard?.read === "function";
127
+
128
+ if (!supportsClipboardRead) {
129
+ toast.error("Clipboard paste is not supported in this browser.");
130
+ return;
131
+ }
132
+
133
+ try {
134
+ const clipboardItems = await navigator.clipboard.read();
135
+ if (clipboardItems.length === 0) {
136
+ toast.error("No data found in clipboard.");
137
+ return;
138
+ }
139
+
140
+ let imageName = "pasted-image.png";
141
+ let imageBlob: Blob | null = null;
142
+ let success = false;
143
+
144
+ // Method 1: Try to extract image from clipboard image/png blob (preferred)
145
+ for (const item of clipboardItems) {
146
+ const imageType = item.types.find((type) => type.startsWith("image/"));
147
+ if (imageType !== undefined) {
148
+ imageBlob = await item.getType(imageType);
149
+ break;
150
+ }
151
+ }
152
+
153
+ // Method 2: Read JSON from text/plain to get metadata and base64 image as fallback
154
+ for (const item of clipboardItems) {
155
+ if (item.types.includes("text/plain")) {
156
+ try {
157
+ const textBlob = await item.getType("text/plain");
158
+ const text = await textBlob.text();
159
+
160
+ const result = clipboardDataSchema.safeParse(JSON.parse(text));
161
+ if (result.success) {
162
+ success = true;
163
+ const data = result.data;
164
+ imageName = `${data.name}.png`;
165
+
166
+ // Use base64 image as fallback if no blob was found
167
+ if (!imageBlob) {
168
+ const response = await fetch(data.image);
169
+ imageBlob = await response.blob();
170
+ }
171
+ } else {
172
+ console.warn("Clipboard data validation failed:", result.error);
173
+ }
174
+ } catch (error) {
175
+ console.warn("Failed to parse JSON from clipboard:", error);
176
+ // Continue - we may still have imageBlob from Method 1
177
+ }
178
+ }
179
+ }
180
+
181
+ if (!imageBlob) {
182
+ if (success) {
183
+ toast.error(
184
+ "Could not extract Figma data from clipboard. Please try copying again using the Prismic Figma plugin.",
185
+ );
186
+ } else {
187
+ toast.error(
188
+ "No Figma data found in clipboard. Make sure you've copied a design using the Prismic Figma plugin.",
189
+ );
190
+ }
191
+ return;
192
+ }
193
+
194
+ // Check if we're at the limit
195
+ const currentSliceCount = slices.length;
196
+ if (currentSliceCount >= IMAGE_UPLOAD_LIMIT) {
197
+ toast.error(
198
+ `You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
199
+ );
200
+ return;
201
+ }
202
+
203
+ // Create File object from blob and append to existing slices
204
+ const imageData = new File([imageBlob], imageName, {
205
+ type: imageBlob.type,
206
+ });
207
+ const newIndex = currentSliceCount;
208
+
209
+ // Append new slice to existing ones
210
+ setSlices((prevSlices) => [
211
+ ...prevSlices,
212
+ {
213
+ source: "figma",
214
+ status: "uploading",
215
+ image: imageData,
216
+ },
217
+ ]);
218
+
219
+ // Start uploading the new image
220
+ void generateSlice({ index: newIndex, imageData, source: "figma" });
221
+
222
+ toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
223
+ } catch (error) {
224
+ console.error("Failed to paste from clipboard:", error);
225
+ toast.error(
226
+ "Failed to paste from clipboard. Please check browser permissions and try again.",
227
+ );
228
+ }
229
+ };
230
+
231
+ const generateSlice = async (args: {
232
+ index: number;
233
+ imageData: File;
234
+ source: "figma" | "upload";
235
+ }) => {
236
+ const { index, imageData, source } = args;
98
237
  const currentId = id.current;
99
238
 
239
+ const smConfig = await managerClient.project.getSliceMachineConfig();
240
+ const libraryID = smConfig?.libraries?.[0];
241
+ if (libraryID === undefined) {
242
+ throw new Error("No library found in the config.");
243
+ }
244
+
100
245
  setSlice({
101
246
  index,
102
- slice: (prevSlice) => ({
103
- ...prevSlice,
104
- status: "uploading",
105
- }),
247
+ slice: (prevSlice) => ({ ...prevSlice, status: "uploading" }),
106
248
  });
107
249
 
108
- getImageUrl({ image }).then(
109
- (imageUrl) => {
110
- if (currentId !== id.current) return;
111
- inferSlice({ index, imageUrl });
112
- },
113
- () => {
114
- if (currentId !== id.current) return;
115
- setSlice({
116
- index,
117
- slice: (prevSlice) => ({
118
- ...prevSlice,
119
- status: "uploadError",
120
- onRetry: () => uploadImage({ index, image }),
121
- }),
122
- });
123
- },
124
- );
250
+ try {
251
+ const imageUrl = await getImageUrl({ image: imageData });
252
+ if (currentId !== id.current) return;
253
+
254
+ void inferSlice({ index, imageUrl, libraryID, source });
255
+ } catch {
256
+ if (currentId !== id.current) return;
257
+ setSlice({
258
+ index,
259
+ slice: (prevSlice) => ({
260
+ ...prevSlice,
261
+ status: "uploadError",
262
+ onRetry: () => void generateSlice({ index, imageData, source }),
263
+ }),
264
+ });
265
+ }
125
266
  };
126
267
 
127
- const existingSlices = useExistingSlices({ open });
128
-
129
- const inferSlice = (args: { index: number; imageUrl: string }) => {
130
- const { index, imageUrl } = args;
268
+ const inferSlice = async (args: {
269
+ index: number;
270
+ imageUrl: string;
271
+ libraryID: string;
272
+ source: "figma" | "upload";
273
+ }) => {
274
+ const { index, imageUrl, libraryID, source } = args;
131
275
  const currentId = id.current;
132
276
 
133
277
  setSlice({
@@ -139,100 +283,106 @@ export function CreateSliceFromImageModal(
139
283
  }),
140
284
  });
141
285
 
142
- managerClient.customTypes.inferSlice({ imageUrl }).then(
143
- ({ slice, langSmithUrl }) => {
144
- if (currentId !== id.current) return;
145
-
146
- setSlices((prevSlices) =>
147
- prevSlices.map((prevSlice, i) =>
148
- i === index
149
- ? {
150
- ...prevSlice,
151
- status: "success",
152
- thumbnailUrl: imageUrl,
153
- model: sliceWithoutConflicts({
154
- existingSlices: existingSlices.current,
155
- newSlices: prevSlices,
156
- slice,
157
- }),
158
- langSmithUrl,
159
- }
160
- : prevSlice,
161
- ),
162
- );
163
- },
164
- () => {
165
- if (currentId !== id.current) return;
166
- setSlice({
167
- index,
168
- slice: (prevSlice) => ({
286
+ try {
287
+ const inferResult = await managerClient.customTypes.inferSlice({
288
+ source,
289
+ libraryID,
290
+ imageUrl,
291
+ });
292
+
293
+ if (currentId !== id.current) return;
294
+
295
+ setSlices((prevSlices) => {
296
+ return prevSlices.map((prevSlice, i) => {
297
+ if (i !== index) return prevSlice;
298
+ return {
169
299
  ...prevSlice,
170
- status: "generateError",
300
+ status: "success",
171
301
  thumbnailUrl: imageUrl,
172
- onRetry: () => inferSlice({ index, imageUrl }),
173
- }),
302
+ model: sliceWithoutConflicts({
303
+ existingSlices: existingSlices.current,
304
+ newSlices: slices,
305
+ slice: inferResult.slice,
306
+ }),
307
+ };
174
308
  });
175
- },
176
- );
309
+ });
310
+ } catch {
311
+ if (currentId !== id.current) return;
312
+ setSlice({
313
+ index,
314
+ slice: (prevSlice) => ({
315
+ ...prevSlice,
316
+ status: "generateError",
317
+ thumbnailUrl: imageUrl,
318
+ onRetry: () => {
319
+ void inferSlice({ index, imageUrl, libraryID, source });
320
+ },
321
+ }),
322
+ });
323
+ }
177
324
  };
178
325
 
179
- const onSubmit = () => {
326
+ const onAllComplete = async () => {
180
327
  const newSlices = slices.reduce<NewSlice[]>((acc, slice) => {
181
- if (slice.status === "success") acc.push(slice);
328
+ if (slice.status === "success") {
329
+ acc.push(slice);
330
+ }
182
331
  return acc;
183
332
  }, []);
333
+
184
334
  if (!newSlices.length) return;
185
335
 
186
336
  const currentId = id.current;
187
- setIsCreatingSlices(true);
188
- addSlices(newSlices)
189
- .then(async ({ slices, library }) => {
190
- if (currentId !== id.current) return;
191
-
192
- const serverState = await getState();
193
- createSliceSuccess(serverState.libraries);
194
- syncChanges();
195
-
196
- onSuccess({ slices, library });
197
-
198
- setIsCreatingSlices(false);
199
- id.current = crypto.randomUUID();
200
- setSlices([]);
201
-
202
- void completeStep("createSlice");
203
-
204
- for (const { model, langSmithUrl } of slices) {
205
- void telemetry.track({
206
- event: "slice:created",
207
- id: model.id,
208
- name: model.name,
209
- library,
210
- location,
211
- mode: "ai",
212
- langSmithUrl,
213
- });
214
-
215
- addAiFeedback({
216
- type: "model",
217
- library,
218
- sliceId: model.id,
219
- variationId: model.variations[0].id,
220
- langSmithUrl,
221
- });
222
- }
223
- })
224
- .catch(() => {
225
- if (currentId !== id.current) return;
226
- setIsCreatingSlices(false);
227
- toast.error("An unexpected error happened while adding slices.");
228
- });
337
+ try {
338
+ // Only the slices generated from uploaded images need this step
339
+ const { slices, library } = await addSlices(
340
+ newSlices.filter((slice) => slice.source === "upload"),
341
+ );
342
+ if (currentId !== id.current) return;
343
+
344
+ id.current = crypto.randomUUID();
345
+
346
+ const serverState = await getState();
347
+ createSliceSuccess(serverState.libraries);
348
+ syncChanges();
349
+
350
+ void completeStep("createSlice");
351
+
352
+ for (const { model, langSmithUrl } of slices) {
353
+ void telemetry.track({
354
+ event: "slice:created",
355
+ id: model.id,
356
+ name: model.name,
357
+ library,
358
+ location,
359
+ mode: "ai",
360
+ langSmithUrl,
361
+ });
362
+
363
+ addAiFeedback({
364
+ type: "model",
365
+ library,
366
+ sliceId: model.id,
367
+ variationId: model.variations[0].id,
368
+ langSmithUrl,
369
+ });
370
+ }
371
+
372
+ toast.success(
373
+ `${newSlices.length} new slice${
374
+ newSlices.length > 1 ? "s" : ""
375
+ } successfully generated.`,
376
+ );
377
+ } catch {
378
+ if (currentId !== id.current) return;
379
+ toast.error("An unexpected error happened while adding slices.");
380
+ }
229
381
  };
230
382
 
231
383
  const areSlicesLoading = slices.some(
232
384
  (slice) => slice.status === "uploading" || slice.status === "generating",
233
385
  );
234
- const readySlices = slices.filter((slice) => slice.status === "success");
235
- const someSlicesReady = readySlices.length > 0;
236
386
 
237
387
  return (
238
388
  <Dialog open={open} onOpenChange={onOpenChange}>
@@ -242,7 +392,56 @@ export function CreateSliceFromImageModal(
242
392
  Upload images to generate slices with AI
243
393
  </DialogDescription>
244
394
  {slices.length === 0 ? (
245
- <Box padding={16} height="100%">
395
+ <Box
396
+ padding={16}
397
+ height="100%"
398
+ gap={16}
399
+ display="flex"
400
+ flexDirection="column"
401
+ >
402
+ <Box
403
+ display="flex"
404
+ gap={16}
405
+ alignItems="center"
406
+ backgroundColor="grey2"
407
+ padding={16}
408
+ borderRadius={12}
409
+ >
410
+ <Box display="flex" gap={8} alignItems="center" flexGrow={1}>
411
+ <Box
412
+ width={48}
413
+ height={48}
414
+ backgroundColor="grey12"
415
+ borderRadius="100%"
416
+ display="flex"
417
+ alignItems="center"
418
+ justifyContent="center"
419
+ >
420
+ <FigmaIcon variant="original" height={25} />
421
+ </Box>
422
+ <Box display="flex" flexDirection="column" flexGrow={1}>
423
+ <Text variant="bold">Want to work faster?</Text>
424
+ <Text variant="small" color="grey11">
425
+ Copy frames from Figma with the Slice Machine plugin and
426
+ paste them here.
427
+ </Text>
428
+ </Box>
429
+ </Box>
430
+ <Button
431
+ endIcon="arrowForward"
432
+ color="indigo"
433
+ onClick={() =>
434
+ window.open(
435
+ "https://www.figma.com/community/plugin/TODO",
436
+ "_blank",
437
+ )
438
+ }
439
+ sx={{ marginRight: 8 }}
440
+ invisible
441
+ >
442
+ Install plugin
443
+ </Button>
444
+ </Box>
246
445
  <FileDropZone
247
446
  onFilesSelected={onImagesSelected}
248
447
  assetType="image"
@@ -250,11 +449,15 @@ export function CreateSliceFromImageModal(
250
449
  overlay={
251
450
  <UploadBlankSlate
252
451
  onFilesSelected={onImagesSelected}
452
+ onPaste={() => void handlePaste()}
253
453
  droppingFiles
254
454
  />
255
455
  }
256
456
  >
257
- <UploadBlankSlate onFilesSelected={onImagesSelected} />
457
+ <UploadBlankSlate
458
+ onFilesSelected={onImagesSelected}
459
+ onPaste={() => void handlePaste()}
460
+ />
258
461
  </FileDropZone>
259
462
  </Box>
260
463
  ) : (
@@ -273,14 +476,9 @@ export function CreateSliceFromImageModal(
273
476
  )}
274
477
 
275
478
  <DialogActions>
276
- <DialogCancelButton disabled={isCreatingSlices} />
277
- <DialogActionButton
278
- disabled={!someSlicesReady || areSlicesLoading}
279
- loading={isCreatingSlices}
280
- onClick={onSubmit}
281
- >
282
- {getSubmitButtonLabel(location)} ({readySlices.length})
283
- </DialogActionButton>
479
+ <DialogCancelButton disabled={areSlicesLoading}>
480
+ Close
481
+ </DialogCancelButton>
284
482
  </DialogActions>
285
483
  </DialogContent>
286
484
  </Dialog>
@@ -290,8 +488,9 @@ export function CreateSliceFromImageModal(
290
488
  function UploadBlankSlate(props: {
291
489
  droppingFiles?: boolean;
292
490
  onFilesSelected: (files: File[]) => void;
491
+ onPaste: () => void;
293
492
  }) {
294
- const { droppingFiles = false, onFilesSelected } = props;
493
+ const { droppingFiles = false, onFilesSelected, onPaste } = props;
295
494
 
296
495
  return (
297
496
  <Box
@@ -302,27 +501,49 @@ function UploadBlankSlate(props: {
302
501
  border
303
502
  borderStyle="dashed"
304
503
  borderColor={droppingFiles ? "purple9" : "grey6"}
504
+ borderRadius={12}
505
+ flexGrow={1}
305
506
  >
306
507
  <BlankSlate>
307
- <BlankSlateIcon
308
- lineColor="purple11"
309
- backgroundColor="purple5"
310
- name="cloudUpload"
311
- size="large"
312
- />
313
- <BlankSlateTitle>Upload your design images.</BlankSlateTitle>
314
- <BlankSlateDescription>
315
- Once uploaded, you can generate slices automatically using AI.
316
- </BlankSlateDescription>
317
- <BlankSlateActions>
318
- <FileUploadButton
319
- startIcon="attachFile"
320
- onFilesSelected={onFilesSelected}
321
- color="grey"
508
+ <Box display="flex" flexDirection="column" gap={16} alignItems="center">
509
+ <BlankSlateIcon
510
+ lineColor="purple11"
511
+ backgroundColor="purple5"
512
+ name="cloudUpload"
513
+ size="large"
514
+ />
515
+ <Box
516
+ display="flex"
517
+ flexDirection="column"
518
+ gap={4}
519
+ alignItems="center"
322
520
  >
323
- Add images
324
- </FileUploadButton>
325
- </BlankSlateActions>
521
+ <Text>Generate slices from your designs</Text>
522
+ <Text variant="small" color="grey11">
523
+ Upload your design images or paste them directly from Figma.
524
+ </Text>
525
+ </Box>
526
+ <Box display="flex" alignItems="center" gap={16}>
527
+ <Button
528
+ size="small"
529
+ renderStartIcon={() => (
530
+ <FigmaIcon variant="original" height={16} />
531
+ )}
532
+ color="grey"
533
+ onClick={onPaste}
534
+ >
535
+ Paste from Figma
536
+ </Button>
537
+ <FileUploadButton
538
+ size="small"
539
+ onFilesSelected={onFilesSelected}
540
+ color="purple"
541
+ invisible
542
+ >
543
+ Add images
544
+ </FileUploadButton>
545
+ </Box>
546
+ </Box>
326
547
  </BlankSlate>
327
548
  </Box>
328
549
  );
@@ -349,6 +570,7 @@ type NewSlice = {
349
570
  image: File;
350
571
  model: SharedSlice;
351
572
  langSmithUrl?: string;
573
+ source: "figma" | "upload";
352
574
  };
353
575
 
354
576
  /**
@@ -459,16 +681,3 @@ async function addSlices(newSlices: NewSlice[]) {
459
681
 
460
682
  return { library, slices };
461
683
  }
462
-
463
- const getSubmitButtonLabel = (
464
- location: "custom_type" | "page_type" | "slices",
465
- ) => {
466
- switch (location) {
467
- case "custom_type":
468
- return "Add to type";
469
- case "page_type":
470
- return "Add to page";
471
- case "slices":
472
- return "Add to slices";
473
- }
474
- };