@dust-tt/sparkle 0.2.625-rc-1 → 0.2.625
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/dist/cjs/index.js +1 -1
- package/dist/esm/components/DataTable.d.ts +3 -2
- package/dist/esm/components/DataTable.d.ts.map +1 -1
- package/dist/esm/components/DataTable.js +59 -33
- package/dist/esm/components/DataTable.js.map +1 -1
- package/dist/esm/components/FaviconIcon.d.ts +15 -0
- package/dist/esm/components/FaviconIcon.d.ts.map +1 -0
- package/dist/esm/components/FaviconIcon.js +56 -0
- package/dist/esm/components/FaviconIcon.js.map +1 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.d.ts.map +1 -1
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/components/index.js.map +1 -1
- package/dist/esm/stories/Citation.stories.d.ts.map +1 -1
- package/dist/esm/stories/Citation.stories.js +13 -4
- package/dist/esm/stories/Citation.stories.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DataTable.tsx +132 -66
- package/src/components/FaviconIcon.tsx +95 -0
- package/src/components/index.ts +1 -0
- package/src/stories/Citation.stories.tsx +18 -3
|
@@ -68,8 +68,8 @@ declare module "@tanstack/react-table" {
|
|
|
68
68
|
|
|
69
69
|
interface TBaseData {
|
|
70
70
|
onClick?: () => void;
|
|
71
|
-
moreMenuItems?: DropdownMenuItemProps[];
|
|
72
71
|
dropdownMenuProps?: React.ComponentPropsWithoutRef<typeof DropdownMenu>;
|
|
72
|
+
menuItems?: MenuItem[];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
interface ColumnBreakpoint {
|
|
@@ -286,6 +286,7 @@ export function DataTable<TData extends TBaseData>({
|
|
|
286
286
|
onClick={
|
|
287
287
|
enableRowSelection ? handleRowClick : row.original.onClick
|
|
288
288
|
}
|
|
289
|
+
rowData={row.original}
|
|
289
290
|
{...(enableRowSelection && {
|
|
290
291
|
"data-selected": row.getIsSelected(),
|
|
291
292
|
})}
|
|
@@ -562,6 +563,7 @@ export function ScrollableDataTable<TData extends TBaseData>({
|
|
|
562
563
|
onClick={
|
|
563
564
|
enableRowSelection ? handleRowClick : row.original.onClick
|
|
564
565
|
}
|
|
566
|
+
rowData={row.original}
|
|
565
567
|
className="s-absolute s-w-full"
|
|
566
568
|
{...(enableRowSelection && {
|
|
567
569
|
"data-selected": row.getIsSelected(),
|
|
@@ -716,6 +718,7 @@ interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
|
716
718
|
onClick?: () => void;
|
|
717
719
|
widthClassName: string;
|
|
718
720
|
"data-selected"?: boolean;
|
|
721
|
+
rowData?: TBaseData;
|
|
719
722
|
}
|
|
720
723
|
|
|
721
724
|
DataTable.Row = function Row({
|
|
@@ -723,24 +726,70 @@ DataTable.Row = function Row({
|
|
|
723
726
|
className,
|
|
724
727
|
onClick,
|
|
725
728
|
widthClassName,
|
|
729
|
+
rowData,
|
|
726
730
|
...props
|
|
727
731
|
}: RowProps) {
|
|
732
|
+
const [contextMenuPosition, setContextMenuPosition] = useState<{
|
|
733
|
+
x: number;
|
|
734
|
+
y: number;
|
|
735
|
+
} | null>(null);
|
|
736
|
+
|
|
737
|
+
const handleContextMenu = (event: React.MouseEvent) => {
|
|
738
|
+
if (!rowData?.menuItems?.length) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
event.preventDefault();
|
|
743
|
+
setContextMenuPosition({ x: event.clientX, y: event.clientY });
|
|
744
|
+
};
|
|
745
|
+
|
|
728
746
|
return (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
747
|
+
<>
|
|
748
|
+
<tr
|
|
749
|
+
className={cn(
|
|
750
|
+
"s-group/dt-row s-justify-center s-border-b s-transition-colors s-duration-300 s-ease-out",
|
|
751
|
+
"s-border-separator dark:s-border-separator-night",
|
|
752
|
+
onClick &&
|
|
753
|
+
"s-cursor-pointer [&:hover:not(:has(input:hover)):not(:has(button:hover))]:s-bg-muted dark:[&:hover:not(:has(input:hover)):not(:has(button:hover))]:s-bg-muted-night",
|
|
754
|
+
props["data-selected"] && "s-bg-muted/50 dark:s-bg-muted/50",
|
|
755
|
+
widthClassName,
|
|
756
|
+
className
|
|
757
|
+
)}
|
|
758
|
+
onClick={onClick || undefined}
|
|
759
|
+
onContextMenu={handleContextMenu}
|
|
760
|
+
{...props}
|
|
761
|
+
>
|
|
762
|
+
{children}
|
|
763
|
+
</tr>
|
|
764
|
+
|
|
765
|
+
{contextMenuPosition && rowData?.menuItems?.length && (
|
|
766
|
+
<DropdownMenu
|
|
767
|
+
open={!!contextMenuPosition}
|
|
768
|
+
onOpenChange={(open) => !open && setContextMenuPosition(null)}
|
|
769
|
+
modal={false}
|
|
770
|
+
>
|
|
771
|
+
<DropdownMenuPortal>
|
|
772
|
+
<DropdownMenuContent
|
|
773
|
+
align="start"
|
|
774
|
+
className="s-whitespace-nowrap"
|
|
775
|
+
style={{
|
|
776
|
+
position: "fixed",
|
|
777
|
+
left: contextMenuPosition?.x || 0,
|
|
778
|
+
top: contextMenuPosition?.y || 0,
|
|
779
|
+
}}
|
|
780
|
+
>
|
|
781
|
+
<DropdownMenuGroup>
|
|
782
|
+
{rowData?.menuItems?.map((item, index) =>
|
|
783
|
+
renderMenuItem(item, index, () =>
|
|
784
|
+
setContextMenuPosition(null)
|
|
785
|
+
)
|
|
786
|
+
)}
|
|
787
|
+
</DropdownMenuGroup>
|
|
788
|
+
</DropdownMenuContent>
|
|
789
|
+
</DropdownMenuPortal>
|
|
790
|
+
</DropdownMenu>
|
|
738
791
|
)}
|
|
739
|
-
|
|
740
|
-
{...props}
|
|
741
|
-
>
|
|
742
|
-
{children}
|
|
743
|
-
</tr>
|
|
792
|
+
</>
|
|
744
793
|
);
|
|
745
794
|
};
|
|
746
795
|
|
|
@@ -769,6 +818,71 @@ interface SubmenuMenuItem extends BaseMenuItem {
|
|
|
769
818
|
|
|
770
819
|
export type MenuItem = RegularMenuItem | SubmenuMenuItem;
|
|
771
820
|
|
|
821
|
+
// Shared menu rendering functions
|
|
822
|
+
const renderSubmenuItem = (
|
|
823
|
+
item: SubmenuMenuItem,
|
|
824
|
+
index: number,
|
|
825
|
+
onItemClick?: () => void
|
|
826
|
+
) => (
|
|
827
|
+
<DropdownMenuSub key={`${item.label}-${index}`}>
|
|
828
|
+
<DropdownMenuSubTrigger label={item.label} disabled={item.disabled} />
|
|
829
|
+
<DropdownMenuPortal>
|
|
830
|
+
<DropdownMenuSubContent>
|
|
831
|
+
<ScrollArea
|
|
832
|
+
className="s-min-w-24 s-flex s-max-h-72 s-flex-col"
|
|
833
|
+
hideScrollBar
|
|
834
|
+
>
|
|
835
|
+
{item.items.map((subItem) => (
|
|
836
|
+
<DropdownMenuItem
|
|
837
|
+
key={subItem.id}
|
|
838
|
+
label={subItem.name}
|
|
839
|
+
onClick={(event) => {
|
|
840
|
+
event.stopPropagation();
|
|
841
|
+
item.onSelect(subItem.id);
|
|
842
|
+
onItemClick?.();
|
|
843
|
+
}}
|
|
844
|
+
/>
|
|
845
|
+
))}
|
|
846
|
+
<ScrollBar className="s-py-0" />
|
|
847
|
+
</ScrollArea>
|
|
848
|
+
</DropdownMenuSubContent>
|
|
849
|
+
</DropdownMenuPortal>
|
|
850
|
+
</DropdownMenuSub>
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const renderRegularItem = (
|
|
854
|
+
item: RegularMenuItem,
|
|
855
|
+
index: number,
|
|
856
|
+
onItemClick?: () => void
|
|
857
|
+
) => {
|
|
858
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
859
|
+
const { kind, ...itemProps } = item;
|
|
860
|
+
return (
|
|
861
|
+
<DropdownMenuItem
|
|
862
|
+
key={`item-${index}`}
|
|
863
|
+
{...itemProps}
|
|
864
|
+
onClick={(event) => {
|
|
865
|
+
event.stopPropagation();
|
|
866
|
+
itemProps.onClick?.(event);
|
|
867
|
+
onItemClick?.();
|
|
868
|
+
}}
|
|
869
|
+
/>
|
|
870
|
+
);
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const renderMenuItem = (
|
|
874
|
+
item: MenuItem,
|
|
875
|
+
index: number,
|
|
876
|
+
onItemClick?: () => void
|
|
877
|
+
) => {
|
|
878
|
+
switch (item.kind) {
|
|
879
|
+
case "submenu":
|
|
880
|
+
return renderSubmenuItem(item, index, onItemClick);
|
|
881
|
+
case "item":
|
|
882
|
+
return renderRegularItem(item, index, onItemClick);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
772
886
|
export interface DataTableMoreButtonProps {
|
|
773
887
|
className?: string;
|
|
774
888
|
menuItems?: MenuItem[];
|
|
@@ -787,56 +901,6 @@ DataTable.MoreButton = function MoreButton({
|
|
|
787
901
|
return null;
|
|
788
902
|
}
|
|
789
903
|
|
|
790
|
-
const renderSubmenuItem = (item: SubmenuMenuItem, index: number) => (
|
|
791
|
-
<DropdownMenuSub key={`${item.label}-${index}`}>
|
|
792
|
-
<DropdownMenuSubTrigger label={item.label} disabled={item.disabled} />
|
|
793
|
-
<DropdownMenuPortal>
|
|
794
|
-
<DropdownMenuSubContent>
|
|
795
|
-
<ScrollArea
|
|
796
|
-
className="s-min-w-24 s-flex s-max-h-72 s-flex-col"
|
|
797
|
-
hideScrollBar
|
|
798
|
-
>
|
|
799
|
-
{item.items.map((subItem) => (
|
|
800
|
-
<DropdownMenuItem
|
|
801
|
-
key={subItem.id}
|
|
802
|
-
label={subItem.name}
|
|
803
|
-
onClick={(event) => {
|
|
804
|
-
event.stopPropagation();
|
|
805
|
-
item.onSelect(subItem.id);
|
|
806
|
-
}}
|
|
807
|
-
/>
|
|
808
|
-
))}
|
|
809
|
-
<ScrollBar className="s-py-0" />
|
|
810
|
-
</ScrollArea>
|
|
811
|
-
</DropdownMenuSubContent>
|
|
812
|
-
</DropdownMenuPortal>
|
|
813
|
-
</DropdownMenuSub>
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
const renderRegularItem = (item: RegularMenuItem, index: number) => {
|
|
817
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
818
|
-
const { kind, ...itemProps } = item;
|
|
819
|
-
return (
|
|
820
|
-
<DropdownMenuItem
|
|
821
|
-
key={`item-${index}`}
|
|
822
|
-
{...itemProps}
|
|
823
|
-
onClick={(event) => {
|
|
824
|
-
event.stopPropagation();
|
|
825
|
-
itemProps.onClick?.(event);
|
|
826
|
-
}}
|
|
827
|
-
/>
|
|
828
|
-
);
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
const renderMenuItem = (item: MenuItem, index: number) => {
|
|
832
|
-
switch (item.kind) {
|
|
833
|
-
case "submenu":
|
|
834
|
-
return renderSubmenuItem(item, index);
|
|
835
|
-
case "item":
|
|
836
|
-
return renderRegularItem(item, index);
|
|
837
|
-
}
|
|
838
|
-
};
|
|
839
|
-
|
|
840
904
|
return (
|
|
841
905
|
<DropdownMenu modal={false} {...dropdownMenuProps}>
|
|
842
906
|
<DropdownMenuTrigger
|
|
@@ -854,7 +918,9 @@ DataTable.MoreButton = function MoreButton({
|
|
|
854
918
|
</DropdownMenuTrigger>
|
|
855
919
|
|
|
856
920
|
<DropdownMenuContent align="end">
|
|
857
|
-
<DropdownMenuGroup>
|
|
921
|
+
<DropdownMenuGroup>
|
|
922
|
+
{menuItems.map((item, index) => renderMenuItem(item, index))}
|
|
923
|
+
</DropdownMenuGroup>
|
|
858
924
|
</DropdownMenuContent>
|
|
859
925
|
</DropdownMenu>
|
|
860
926
|
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
import React, { useCallback, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { GlobeAltIcon } from "@sparkle/icons";
|
|
5
|
+
import { cn } from "@sparkle/lib/utils";
|
|
6
|
+
|
|
7
|
+
const faviconVariants = cva("", {
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: "s-w-4 s-h-4",
|
|
11
|
+
md: "s-w-5 s-h-5",
|
|
12
|
+
lg: "s-w-6 s-h-6",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "sm",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
interface FaviconIconProps {
|
|
21
|
+
faviconUrl?: string;
|
|
22
|
+
websiteUrl?: string;
|
|
23
|
+
size?: "sm" | "md" | "lg";
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Component that displays a website favicon with fallback to GlobeAltIcon
|
|
29
|
+
* If faviconUrl is provided, uses that. If websiteUrl is provided, generates favicon URL.
|
|
30
|
+
* Falls back to GlobeAltIcon if favicon fails to load.
|
|
31
|
+
*/
|
|
32
|
+
export function FaviconIcon({
|
|
33
|
+
faviconUrl,
|
|
34
|
+
websiteUrl,
|
|
35
|
+
size = "sm",
|
|
36
|
+
className,
|
|
37
|
+
}: FaviconIconProps) {
|
|
38
|
+
const [hasError, setHasError] = useState(false);
|
|
39
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
40
|
+
|
|
41
|
+
const handleError = useCallback(() => {
|
|
42
|
+
setHasError(true);
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const handleLoad = useCallback(() => {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
// Determine favicon URL
|
|
51
|
+
let finalFaviconUrl = faviconUrl;
|
|
52
|
+
if (!finalFaviconUrl && websiteUrl) {
|
|
53
|
+
try {
|
|
54
|
+
const domain = new URL(websiteUrl).hostname;
|
|
55
|
+
finalFaviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=16`;
|
|
56
|
+
} catch {
|
|
57
|
+
// Invalid URL, fallback to icon
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If no favicon URL or it failed to load, show fallback icon
|
|
62
|
+
if (!finalFaviconUrl || hasError) {
|
|
63
|
+
return <GlobeAltIcon className={className} />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
"s-flex s-items-center s-justify-center s-relative",
|
|
70
|
+
faviconVariants({ size }),
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<img
|
|
75
|
+
src={finalFaviconUrl}
|
|
76
|
+
alt="Website icon"
|
|
77
|
+
className={cn("s-object-contain", faviconVariants({ size }))}
|
|
78
|
+
onError={handleError}
|
|
79
|
+
onLoad={handleLoad}
|
|
80
|
+
style={{
|
|
81
|
+
opacity: isLoading ? 0 : 1,
|
|
82
|
+
transition: "opacity 0.2s ease-in-out",
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
{(isLoading || hasError) && (
|
|
86
|
+
<GlobeAltIcon
|
|
87
|
+
className={cn(
|
|
88
|
+
faviconVariants({ size }),
|
|
89
|
+
isLoading ? "s-absolute s-inset-0" : "s-hidden"
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -93,6 +93,7 @@ export { default as DropzoneOverlay } from "./DropzoneOverlay";
|
|
|
93
93
|
export type { EmojiMartData } from "./EmojiPicker";
|
|
94
94
|
export { DataEmojiMart, EmojiPicker } from "./EmojiPicker";
|
|
95
95
|
export { EmptyCTA, EmptyCTAButton } from "./EmptyCTA";
|
|
96
|
+
export { FaviconIcon } from "./FaviconIcon";
|
|
96
97
|
export { FilterChips } from "./FilterChips";
|
|
97
98
|
export { Div3D, Hover3D } from "./Hover3D";
|
|
98
99
|
export { Hoverable } from "./Hoverable";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CitationTitle,
|
|
14
14
|
DocumentIcon,
|
|
15
15
|
ExternalLinkIcon,
|
|
16
|
+
FaviconIcon,
|
|
16
17
|
GlobeAltIcon,
|
|
17
18
|
Icon,
|
|
18
19
|
ImageIcon,
|
|
@@ -69,10 +70,17 @@ export const CitationsExample = () => (
|
|
|
69
70
|
</Citation>
|
|
70
71
|
<Citation onClick={() => alert("Card clicked")} className="s-w-48">
|
|
71
72
|
<CitationIcons>
|
|
72
|
-
<
|
|
73
|
+
<FaviconIcon websiteUrl="https://www.linkedin.com" size="sm" />
|
|
73
74
|
</CitationIcons>
|
|
74
75
|
<CitationTitle>Linkedin, Edouard Wautier</CitationTitle>
|
|
75
76
|
</Citation>
|
|
77
|
+
|
|
78
|
+
<Citation onClick={() => alert("Card clicked")} className="s-w-48">
|
|
79
|
+
<CitationIcons>
|
|
80
|
+
<FaviconIcon websiteUrl="https://github.com" size="sm" />
|
|
81
|
+
</CitationIcons>
|
|
82
|
+
<CitationTitle>GitHub Repository</CitationTitle>
|
|
83
|
+
</Citation>
|
|
76
84
|
|
|
77
85
|
<Citation onClick={() => alert("Card clicked")} className="s-w-48">
|
|
78
86
|
<CitationImage imgSrc="https://dust.tt/static/droidavatar/Droid_Lime_3.jpg" />
|
|
@@ -183,9 +191,16 @@ export const CitationsExample = () => (
|
|
|
183
191
|
<Citation onClick={() => alert("Close action clicked")}>
|
|
184
192
|
<CitationIcons>
|
|
185
193
|
<CitationIndex>4</CitationIndex>
|
|
186
|
-
<
|
|
194
|
+
<FaviconIcon websiteUrl="https://stackoverflow.com" size="sm" />
|
|
187
195
|
</CitationIcons>
|
|
188
|
-
<CitationTitle>
|
|
196
|
+
<CitationTitle>Stack Overflow Answer</CitationTitle>
|
|
197
|
+
</Citation>
|
|
198
|
+
<Citation onClick={() => alert("Close action clicked")}>
|
|
199
|
+
<CitationIcons>
|
|
200
|
+
<CitationIndex>5</CitationIndex>
|
|
201
|
+
<FaviconIcon websiteUrl="https://www.wikipedia.org" size="sm" />
|
|
202
|
+
</CitationIcons>
|
|
203
|
+
<CitationTitle>Wikipedia Article</CitationTitle>
|
|
189
204
|
</Citation>
|
|
190
205
|
</CitationGrid>
|
|
191
206
|
Example of interactive content (list variant)
|