@dust-tt/sparkle 0.7.0 → 0.7.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 (45) hide show
  1. package/dist/cjs/index.js +10 -10
  2. package/dist/cjs/index.js.map +4 -4
  3. package/dist/esm/components/Citation.d.ts +7 -1
  4. package/dist/esm/components/Citation.d.ts.map +1 -1
  5. package/dist/esm/components/Citation.js +24 -13
  6. package/dist/esm/components/Citation.js.map +1 -1
  7. package/dist/esm/components/ImagePreview.d.ts +28 -0
  8. package/dist/esm/components/ImagePreview.d.ts.map +1 -0
  9. package/dist/esm/components/ImagePreview.js +100 -0
  10. package/dist/esm/components/ImagePreview.js.map +1 -0
  11. package/dist/esm/components/ImageZoomDialog.d.ts +26 -0
  12. package/dist/esm/components/ImageZoomDialog.d.ts.map +1 -0
  13. package/dist/esm/components/ImageZoomDialog.js +46 -0
  14. package/dist/esm/components/ImageZoomDialog.js.map +1 -0
  15. package/dist/esm/components/InteractiveImageGrid.d.ts.map +1 -1
  16. package/dist/esm/components/InteractiveImageGrid.js +25 -67
  17. package/dist/esm/components/InteractiveImageGrid.js.map +1 -1
  18. package/dist/esm/components/index.d.ts +5 -1
  19. package/dist/esm/components/index.d.ts.map +1 -1
  20. package/dist/esm/components/index.js +2 -0
  21. package/dist/esm/components/index.js.map +1 -1
  22. package/dist/esm/lottie/dragArea.d.ts +32 -32
  23. package/dist/esm/lottie/spinnerColor.d.ts +64 -64
  24. package/dist/esm/lottie/spinnerColorLG.d.ts +133 -133
  25. package/dist/esm/lottie/spinnerColorXS.d.ts +11 -11
  26. package/dist/esm/lottie/spinnerDark.d.ts +72 -72
  27. package/dist/esm/lottie/spinnerDarkLG.d.ts +134 -134
  28. package/dist/esm/lottie/spinnerDarkXS.d.ts +22 -22
  29. package/dist/esm/lottie/spinnerLight.d.ts +9 -9
  30. package/dist/esm/lottie/spinnerLightLG.d.ts +10 -10
  31. package/dist/esm/lottie/spinnerLightXS.d.ts +22 -22
  32. package/dist/esm/stories/Citation.stories.d.ts.map +1 -1
  33. package/dist/esm/stories/Citation.stories.js +7 -15
  34. package/dist/esm/stories/Citation.stories.js.map +1 -1
  35. package/dist/esm/stories/ImagePreview.stories.d.ts +45 -0
  36. package/dist/esm/stories/ImagePreview.stories.d.ts.map +1 -0
  37. package/dist/esm/stories/ImagePreview.stories.js +83 -0
  38. package/dist/esm/stories/ImagePreview.stories.js.map +1 -0
  39. package/dist/sparkle.css +28 -28
  40. package/package.json +1 -1
  41. package/src/components/Citation.tsx +52 -41
  42. package/src/components/ImagePreview.tsx +244 -0
  43. package/src/components/ImageZoomDialog.tsx +151 -0
  44. package/src/components/InteractiveImageGrid.tsx +60 -267
  45. package/src/components/index.ts +16 -1
@@ -1,140 +1,9 @@
1
1
  import React from "react";
2
2
 
3
- import {
4
- Button,
5
- Dialog,
6
- DialogClose,
7
- DialogContent,
8
- DialogTrigger,
9
- Spinner,
10
- } from "@sparkle/components/";
11
- import {
12
- ArrowDownOnSquareIcon,
13
- ChevronLeftIcon,
14
- ChevronRightIcon,
15
- XMarkIcon,
16
- } from "@sparkle/icons/app";
3
+ import { ImagePreview } from "@sparkle/components/ImagePreview";
4
+ import { ImageZoomDialog } from "@sparkle/components/ImageZoomDialog";
17
5
  import { cn } from "@sparkle/lib/utils";
18
6
 
19
- interface ImageLoadingStateProps {
20
- className?: string;
21
- size?: "sm" | "md" | "lg";
22
- }
23
-
24
- const ImageLoadingState = React.forwardRef<
25
- HTMLDivElement,
26
- ImageLoadingStateProps
27
- >(({ className, size = "lg" }, ref) => {
28
- return (
29
- <div
30
- ref={ref}
31
- className={cn(
32
- "s-mx-auto s-flex s-aspect-square s-w-full s-min-w-[50vh]",
33
- "s-max-w-[80vh] s-items-center s-justify-center",
34
- "s-bg-muted-background dark:s-bg-muted-background-night",
35
- className
36
- )}
37
- >
38
- <Spinner variant="dark" size={size} />
39
- </div>
40
- );
41
- });
42
-
43
- ImageLoadingState.displayName = "ImageLoadingState";
44
- interface ImagePreviewProps {
45
- image: {
46
- alt: string;
47
- downloadUrl?: string;
48
- imageUrl?: string;
49
- isLoading?: boolean;
50
- title: string;
51
- };
52
- onClick: () => void;
53
- onDownload: (e: React.MouseEvent) => Promise<void>;
54
- onClose?: (e: React.MouseEvent) => void;
55
- }
56
-
57
- const ImagePreview = React.forwardRef<HTMLDivElement, ImagePreviewProps>(
58
- ({ image, onClick, onDownload, onClose }, ref) => {
59
- return (
60
- <div
61
- ref={ref}
62
- onClick={onClick}
63
- className={cn(
64
- "s-group/preview s-relative s-aspect-square",
65
- "s-cursor-pointer s-overflow-hidden s-rounded-2xl",
66
- "s-bg-muted-background dark:s-bg-muted-background-night"
67
- )}
68
- >
69
- {image.isLoading ? (
70
- <div className="s-flex s-h-full s-w-full s-items-center s-justify-center">
71
- <Spinner variant="dark" size="md" />
72
- </div>
73
- ) : (
74
- <>
75
- <img
76
- src={image.imageUrl}
77
- alt={image.alt}
78
- className="s-h-full s-w-full s-rounded-2xl s-object-cover"
79
- />
80
- {/* Blur overlay with filename - hidden by default, shown on hover */}
81
- <div
82
- className={cn(
83
- "s-absolute s-inset-0 s-z-10",
84
- "s-flex s-items-center s-justify-center",
85
- "s-bg-primary-100/80 dark:s-bg-primary-100-night/80",
86
- "s-backdrop-blur-sm",
87
- "s-opacity-0 s-transition s-duration-200",
88
- "group-hover/preview:s-opacity-100"
89
- )}
90
- >
91
- <span
92
- className={cn(
93
- "s-max-w-[90%] s-truncate s-px-2 s-text-center",
94
- "s-text-sm s-font-medium",
95
- "s-text-foreground dark:s-text-foreground-night"
96
- )}
97
- >
98
- {image.title}
99
- </span>
100
- </div>
101
- <div
102
- className={cn(
103
- "s-absolute s-right-1 s-top-1 s-z-10 s-flex",
104
- "s-opacity-0 s-transition-opacity s-duration-200",
105
- "group-hover/preview:s-opacity-100"
106
- )}
107
- >
108
- {onClose && (
109
- <Button
110
- variant="ghost"
111
- size="xs"
112
- icon={XMarkIcon}
113
- className="s-text-white dark:s-text-white"
114
- tooltip="Remove"
115
- onClick={onClose}
116
- />
117
- )}
118
- {!onClose && (
119
- <Button
120
- variant="ghost"
121
- size="xs"
122
- icon={ArrowDownOnSquareIcon}
123
- className="s-text-white dark:s-text-white"
124
- tooltip="Download"
125
- onClick={onDownload}
126
- />
127
- )}
128
- </div>
129
- </>
130
- )}
131
- </div>
132
- );
133
- }
134
- );
135
-
136
- ImagePreview.displayName = "ImagePreview";
137
-
138
7
  const SIZE_CLASSES = {
139
8
  sm: "s-h-24 s-w-24",
140
9
  md: "s-h-48 s-w-48",
@@ -165,7 +34,6 @@ function InteractiveImageGrid({
165
34
  const [currentImageIndex, setCurrentImageIndex] = React.useState<
166
35
  number | null
167
36
  >(null);
168
- const [imageLoaded, setImageLoaded] = React.useState(false);
169
37
 
170
38
  const handleNext = React.useCallback(() => {
171
39
  if (currentImageIndex === null) {
@@ -183,29 +51,12 @@ function InteractiveImageGrid({
183
51
  );
184
52
  }, [currentImageIndex, images.length]);
185
53
 
186
- const handleDownload = React.useCallback(
187
- async (downloadUrl?: string, title?: string) => {
188
- if (!downloadUrl || !title) {
189
- return;
190
- }
191
-
192
- // Create a hidden link and click it.
193
- const link = document.createElement("a");
194
- link.href = downloadUrl;
195
- link.download = title;
196
- document.body.appendChild(link);
197
- link.click();
198
- document.body.removeChild(link);
199
- },
200
- []
201
- );
202
-
54
+ // Keyboard navigation for the zoomed image
203
55
  React.useEffect(() => {
204
56
  if (currentImageIndex === null) {
205
57
  return;
206
58
  }
207
59
 
208
- // Only handle keyboard events if the image is zoomed.
209
60
  const handleKeyDown = (e: KeyboardEvent) => {
210
61
  if (e.key === "ArrowRight") {
211
62
  handleNext();
@@ -218,126 +69,68 @@ function InteractiveImageGrid({
218
69
  return () => window.removeEventListener("keydown", handleKeyDown);
219
70
  }, [currentImageIndex, handleNext, handlePrevious]);
220
71
 
72
+ const currentImage =
73
+ currentImageIndex !== null ? images[currentImageIndex] : null;
74
+
221
75
  return (
222
- <Dialog
223
- open={currentImageIndex !== null}
224
- onOpenChange={(open) => !open && setCurrentImageIndex(null)}
225
- >
226
- <DialogTrigger asChild>
227
- <div className={cn("s-@container", className)}>
228
- {images.length === 1 ? (
229
- <div className={SIZE_CLASSES[size]}>
76
+ <>
77
+ <div className={cn("s-@container", className)}>
78
+ {images.length === 1 ? (
79
+ <div className={SIZE_CLASSES[size]}>
80
+ <ImagePreview
81
+ imgSrc={images[0].imageUrl ?? ""}
82
+ alt={images[0].alt}
83
+ title={images[0].title}
84
+ downloadUrl={images[0].downloadUrl}
85
+ isLoading={images[0].isLoading}
86
+ onClick={() => setCurrentImageIndex(0)}
87
+ onClose={onClose ? () => onClose() : undefined}
88
+ variant="embedded"
89
+ titlePosition="center"
90
+ manageZoomDialog={false}
91
+ />
92
+ </div>
93
+ ) : (
94
+ <div className="s-grid s-grid-cols-2 s-gap-2 @xxs:s-grid-cols-3 @xs:s-grid-cols-4">
95
+ {images.map((image, idx) => (
230
96
  <ImagePreview
231
- image={images[0]}
232
- onClick={() => {
233
- setCurrentImageIndex(0);
234
- }}
235
- onDownload={async (e) => {
236
- e.stopPropagation();
237
- await handleDownload(images[0].downloadUrl, images[0].title);
238
- }}
239
- onClose={
240
- onClose
241
- ? (e) => {
242
- e.stopPropagation();
243
- onClose();
244
- }
245
- : undefined
246
- }
247
- />
248
- </div>
249
- ) : (
250
- <div className="s-grid s-grid-cols-2 s-gap-2 @xxs:s-grid-cols-3 @xs:s-grid-cols-4">
251
- {images.map((image, idx) => (
252
- <ImagePreview
253
- key={idx}
254
- image={image}
255
- onClick={() => {
256
- setCurrentImageIndex(idx);
257
- }}
258
- onDownload={async (e) => {
259
- e.stopPropagation();
260
- await handleDownload(image.downloadUrl, image.title);
261
- }}
262
- />
263
- ))}
264
- </div>
265
- )}
266
- </div>
267
- </DialogTrigger>
268
- <DialogContent size="xl" className="s-max-w-[90vw] s-overflow-hidden s-p-3">
269
- {currentImageIndex !== null && (
270
- <div className="s-relative s-flex s-items-center s-justify-center s-gap-2">
271
- {/* Previous button */}
272
- {images.length > 1 && (
273
- <Button
274
- variant="ghost"
275
- size="sm"
276
- icon={ChevronLeftIcon}
277
- onClick={(e) => {
278
- e.stopPropagation();
279
- handlePrevious();
280
- }}
97
+ key={idx}
98
+ imgSrc={image.imageUrl ?? ""}
99
+ alt={image.alt}
100
+ title={image.title}
101
+ downloadUrl={image.downloadUrl}
102
+ isLoading={image.isLoading}
103
+ onClick={() => setCurrentImageIndex(idx)}
104
+ variant="embedded"
105
+ titlePosition="center"
106
+ manageZoomDialog={false}
281
107
  />
282
- )}
283
-
284
- {/* Image container with overlay buttons */}
285
- <div className="s-relative">
286
- {images[currentImageIndex].isLoading ? (
287
- <ImageLoadingState size="lg" />
288
- ) : (
289
- <>
290
- <img
291
- src={images[currentImageIndex].imageUrl}
292
- alt={images[currentImageIndex].alt}
293
- className="s-max-h-full s-max-w-full s-rounded-lg s-object-contain"
294
- onLoad={() => setImageLoaded(true)}
295
- />
296
- {/* Close button - top right of image */}
297
- <DialogClose asChild>
298
- <Button
299
- variant="outline"
300
- size="xs"
301
- icon={XMarkIcon}
302
- className="s-absolute s-right-2 s-top-2"
303
- />
304
- </DialogClose>
305
- {/* Download button - bottom right of image */}
306
- {imageLoaded && (
307
- <Button
308
- variant="outline"
309
- size="xs"
310
- icon={ArrowDownOnSquareIcon}
311
- tooltip="Download"
312
- className="s-absolute s-bottom-2 s-right-2"
313
- onClick={async () => {
314
- await handleDownload(
315
- images[currentImageIndex].downloadUrl,
316
- images[currentImageIndex].title
317
- );
318
- }}
319
- />
320
- )}
321
- </>
322
- )}
323
- </div>
324
-
325
- {/* Next button */}
326
- {images.length > 1 && (
327
- <Button
328
- variant="ghost"
329
- size="sm"
330
- icon={ChevronRightIcon}
331
- onClick={(e) => {
332
- e.stopPropagation();
333
- handleNext();
334
- }}
335
- />
336
- )}
108
+ ))}
337
109
  </div>
338
110
  )}
339
- </DialogContent>
340
- </Dialog>
111
+ </div>
112
+ <ImageZoomDialog
113
+ open={currentImageIndex !== null}
114
+ onOpenChange={(open) => !open && setCurrentImageIndex(null)}
115
+ image={{
116
+ src: currentImage?.imageUrl ?? "",
117
+ alt: currentImage?.alt,
118
+ title: currentImage?.title,
119
+ downloadUrl: currentImage?.downloadUrl,
120
+ isLoading: currentImage?.isLoading,
121
+ }}
122
+ navigation={
123
+ images.length > 1
124
+ ? {
125
+ onPrevious: handlePrevious,
126
+ onNext: handleNext,
127
+ hasPrevious: true,
128
+ hasNext: true,
129
+ }
130
+ : undefined
131
+ }
132
+ />
133
+ </>
341
134
  );
342
135
  }
343
136
 
@@ -44,7 +44,10 @@ export {
44
44
  ContentMessageInline,
45
45
  } from "./ContentMessage";
46
46
  export { ContextItem } from "./ContextItem";
47
- export type { ConversationListItemProps, ReplySectionProps } from "./ConversationListItem";
47
+ export type {
48
+ ConversationListItemProps,
49
+ ReplySectionProps,
50
+ } from "./ConversationListItem";
48
51
  export { ConversationListItem, ReplySection } from "./ConversationListItem";
49
52
  export {
50
53
  ConversationContainer,
@@ -115,6 +118,18 @@ export { Div3D, Hover3D } from "./Hover3D";
115
118
  export { Hoverable } from "./Hoverable";
116
119
  export { DoubleIcon, Icon } from "./Icon";
117
120
  export { IconButton } from "./IconButton";
121
+ export type {
122
+ ImagePreviewProps,
123
+ ImagePreviewTitlePositionType,
124
+ ImagePreviewVariantType,
125
+ } from "./ImagePreview";
126
+ export {
127
+ IMAGE_PREVIEW_TITLE_POSITIONS,
128
+ IMAGE_PREVIEW_VARIANTS,
129
+ ImagePreview,
130
+ } from "./ImagePreview";
131
+ export type { ImageZoomDialogProps } from "./ImageZoomDialog";
132
+ export { downloadFile, ImageZoomDialog } from "./ImageZoomDialog";
118
133
  export { Input } from "./Input";
119
134
  export { InteractiveImageGrid } from "./InteractiveImageGrid";
120
135
  export { Label } from "./Label";