@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.
@@ -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
- <tr
730
- className={cn(
731
- "s-group/dt-row s-justify-center s-border-b s-transition-colors s-duration-300 s-ease-out",
732
- "s-border-separator dark:s-border-separator-night",
733
- onClick &&
734
- "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",
735
- props["data-selected"] && "s-bg-muted/50 dark:s-bg-muted/50",
736
- widthClassName,
737
- className
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
- onClick={onClick || undefined}
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>{menuItems.map(renderMenuItem)}</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
+ }
@@ -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
- <Icon visual={GlobeAltIcon} size="sm" />
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
- <Icon visual={GlobeAltIcon} size="sm" />
194
+ <FaviconIcon websiteUrl="https://stackoverflow.com" size="sm" />
187
195
  </CitationIcons>
188
- <CitationTitle>Hello</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)