@lateralus-ai/shipping-ui 2.0.0-dev.0 → 2.0.0-dev.11

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 (80) hide show
  1. package/dist/components/EmptyState.d.ts +1 -1
  2. package/dist/components/Entry.d.ts +27 -7
  3. package/dist/components/Modal.d.ts +36 -0
  4. package/dist/components/Tabs.d.ts +23 -8
  5. package/dist/components/index.d.ts +3 -2
  6. package/dist/index.cjs +32 -32
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.esm.js +4209 -3711
  9. package/dist/patterns/Search/ResultRow.d.ts +16 -7
  10. package/dist/patterns/Search/SearchModal.d.ts +84 -2
  11. package/dist/patterns/Search/index.d.ts +2 -2
  12. package/dist/primitives/Badge.d.ts +7 -4
  13. package/dist/primitives/Button.d.ts +17 -8
  14. package/dist/primitives/button-styles.d.ts +4 -0
  15. package/dist/primitives/index.d.ts +4 -2
  16. package/dist/style.css +1 -1
  17. package/dist/tailwind-theme.d.ts +4 -4
  18. package/dist/theme-entry-BUK3MJUJ.mjs +261 -0
  19. package/dist/theme-entry-ygTpGaNC.js +1 -0
  20. package/dist/theme-entry.d.ts +2 -0
  21. package/dist/theme.cjs +1 -0
  22. package/dist/theme.d.ts +2 -0
  23. package/dist/theme.esm.js +5 -0
  24. package/package.json +10 -3
  25. package/src/components/EmptyState.tsx +2 -2
  26. package/src/components/Entry.tsx +119 -45
  27. package/src/components/Modal.tsx +141 -0
  28. package/src/components/Tabs.tsx +146 -48
  29. package/src/components/index.ts +43 -13
  30. package/src/icons/arrow-paths.ts +8 -8
  31. package/src/icons/chevron-paths.ts +17 -17
  32. package/src/icons/icons-data.ts +656 -656
  33. package/src/index.ts +76 -50
  34. package/src/patterns/Search/ResultRow.tsx +44 -52
  35. package/src/patterns/Search/SearchModal.tsx +310 -88
  36. package/src/patterns/Search/index.ts +31 -5
  37. package/src/patterns/Sidebar/assets/logo-compliance-mark.png +5 -5
  38. package/src/patterns/Sidebar/assets/logo-compliance-mask.png +13 -13
  39. package/src/patterns/Sidebar/assets/logo-compliance.svg +17 -17
  40. package/src/patterns/Sidebar/assets/logo-technical-avatar.svg +17 -17
  41. package/src/patterns/Sidebar/assets/logo-technical-mark.png +5 -5
  42. package/src/patterns/Sidebar/assets/logo-technical.svg +16 -16
  43. package/src/patterns/Skeleton/Skeleton.tsx +56 -57
  44. package/src/primitives/Badge.tsx +20 -10
  45. package/src/primitives/Button.tsx +206 -37
  46. package/src/primitives/button-styles.ts +92 -0
  47. package/src/primitives/index.ts +7 -2
  48. package/src/stories/canvases/ButtonsCanvas.tsx +94 -44
  49. package/src/stories/canvases/ButtonsMatrixCanvas.tsx +8 -7
  50. package/src/stories/canvases/ContentCanvas.tsx +157 -88
  51. package/src/stories/canvases/SearchCanvas.tsx +150 -65
  52. package/src/stories/canvases/figma-buttons-layout.ts +83 -0
  53. package/src/stories/canvases/helpers.tsx +26 -43
  54. package/src/stories/components/Buttons.stories.tsx +11 -11
  55. package/src/stories/components/Chat.stories.tsx +11 -11
  56. package/src/stories/components/Content.stories.tsx +11 -11
  57. package/src/stories/components/Core.stories.tsx +11 -11
  58. package/src/stories/components/DomainForms.stories.tsx +11 -11
  59. package/src/stories/components/Filters.stories.tsx +11 -11
  60. package/src/stories/components/Forms.stories.tsx +11 -11
  61. package/src/stories/components/Icons.stories.tsx +11 -11
  62. package/src/stories/components/Illustrations.stories.tsx +11 -11
  63. package/src/stories/components/Library.stories.tsx +11 -11
  64. package/src/stories/components/Modals.stories.tsx +11 -11
  65. package/src/stories/components/Report.stories.tsx +11 -11
  66. package/src/stories/components/ReportLayout.stories.tsx +11 -11
  67. package/src/stories/components/Search.stories.tsx +11 -11
  68. package/src/stories/components/Settings.stories.tsx +11 -11
  69. package/src/stories/components/Ships.stories.tsx +11 -11
  70. package/src/stories/components/SidebarLayouts.stories.tsx +11 -11
  71. package/src/stories/components/Skeletons.stories.tsx +11 -11
  72. package/src/stories/components/Workflows.stories.tsx +11 -11
  73. package/src/stories/style-guide/Buttons.stories.tsx +11 -11
  74. package/src/stories/style-guide/ColorTokens.stories.tsx +11 -11
  75. package/src/stories/style-guide/Colors.stories.tsx +11 -11
  76. package/src/stories/style-guide/RaiseLevels.stories.tsx +11 -11
  77. package/src/stories/style-guide/Typography.stories.tsx +11 -11
  78. package/src/tailwind-theme.ts +4 -4
  79. package/src/theme-entry.ts +2 -0
  80. package/tailwind.config.ts +12 -0
@@ -1,13 +1,13 @@
1
- <svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g id="Mask" clip-path="url(#clip0_0_4)">
3
- <g id="icon-compliance.svg">
4
- <path id="Vector" d="M8.30333 5.84667L5.18333 14.2917H6.33833V14.2883C6.34 14.2883 6.34167 14.29 6.34167 14.29L8.90667 11.7267H9.06667L11.6283 14.2917H12.8233L9.71667 5.84667H8.305H8.30333ZM9.215 10.4433H8.75667L7.15167 12.0483L8.89 7.25167H9.10167L10.825 12.055L9.21667 10.4433H9.215Z" fill="var(--fill-0, #232323)"/>
5
- <path id="Vector_2" d="M8.98833 4.85833C9.87167 4.85833 10.6817 5.23 11.3117 5.84667H12.7967C11.925 4.54667 10.5433 3.70667 8.98833 3.70667C7.43333 3.70667 6.05167 4.54667 5.18 5.84667H6.665C7.29333 5.23 8.105 4.85833 8.98833 4.85833Z" fill="var(--fill-0, #232323)"/>
6
- </g>
7
- </g>
8
- <defs>
9
- <clipPath id="clip0_0_4">
10
- <rect width="18" height="18" fill="white"/>
11
- </clipPath>
12
- </defs>
13
- </svg>
1
+ <svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g id="Mask" clip-path="url(#clip0_0_4)">
3
+ <g id="icon-compliance.svg">
4
+ <path id="Vector" d="M8.30333 5.84667L5.18333 14.2917H6.33833V14.2883C6.34 14.2883 6.34167 14.29 6.34167 14.29L8.90667 11.7267H9.06667L11.6283 14.2917H12.8233L9.71667 5.84667H8.305H8.30333ZM9.215 10.4433H8.75667L7.15167 12.0483L8.89 7.25167H9.10167L10.825 12.055L9.21667 10.4433H9.215Z" fill="var(--fill-0, #232323)"/>
5
+ <path id="Vector_2" d="M8.98833 4.85833C9.87167 4.85833 10.6817 5.23 11.3117 5.84667H12.7967C11.925 4.54667 10.5433 3.70667 8.98833 3.70667C7.43333 3.70667 6.05167 4.54667 5.18 5.84667H6.665C7.29333 5.23 8.105 4.85833 8.98833 4.85833Z" fill="var(--fill-0, #232323)"/>
6
+ </g>
7
+ </g>
8
+ <defs>
9
+ <clipPath id="clip0_0_4">
10
+ <rect width="18" height="18" fill="white"/>
11
+ </clipPath>
12
+ </defs>
13
+ </svg>
@@ -1,17 +1,17 @@
1
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect width="18" height="18" fill="#F5F5F5"/>
3
- <g id="Core">
4
- <path d="M-330 -683C-330 -694.046 -321.046 -703 -310 -703H382C393.046 -703 402 -694.046 402 -683V927C402 938.046 393.046 947 382 947H-310C-321.046 947 -330 938.046 -330 927V-683Z" fill="#FDFDFC"/>
5
- <g id="Content">
6
- <g id="Avatar">
7
- <rect x="-273.5" y="-30.5" width="619" height="79" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
8
- <g id="Chief=Technical, Size=18">
9
- <path d="M0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9Z" fill="#FDFDFC"/>
10
- <g id="Logo">
11
- <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="#262420"/>
12
- </g>
13
- </g>
14
- </g>
15
- </g>
16
- </g>
17
- </svg>
1
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="18" height="18" fill="#F5F5F5"/>
3
+ <g id="Core">
4
+ <path d="M-330 -683C-330 -694.046 -321.046 -703 -310 -703H382C393.046 -703 402 -694.046 402 -683V927C402 938.046 393.046 947 382 947H-310C-321.046 947 -330 938.046 -330 927V-683Z" fill="#FDFDFC"/>
5
+ <g id="Content">
6
+ <g id="Avatar">
7
+ <rect x="-273.5" y="-30.5" width="619" height="79" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
8
+ <g id="Chief=Technical, Size=18">
9
+ <path d="M0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9Z" fill="#FDFDFC"/>
10
+ <g id="Logo">
11
+ <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="#262420"/>
12
+ </g>
13
+ </g>
14
+ </g>
15
+ </g>
16
+ </g>
17
+ </svg>
@@ -1,17 +1,17 @@
1
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect width="18" height="18" fill="#F5F5F5"/>
3
- <g id="Core">
4
- <path d="M-330 -683C-330 -694.046 -321.046 -703 -310 -703H382C393.046 -703 402 -694.046 402 -683V927C402 938.046 393.046 947 382 947H-310C-321.046 947 -330 938.046 -330 927V-683Z" fill="#FDFDFC"/>
5
- <g id="Content">
6
- <g id="Avatar">
7
- <rect x="-273.5" y="-30.5" width="619" height="79" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
8
- <g id="Chief=Technical, Size=18">
9
- <path d="M0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9Z" fill="#FDFDFC"/>
10
- <g id="Logo">
11
- <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="#262420"/>
12
- </g>
13
- </g>
14
- </g>
15
- </g>
16
- </g>
17
- </svg>
1
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="18" height="18" fill="#F5F5F5"/>
3
+ <g id="Core">
4
+ <path d="M-330 -683C-330 -694.046 -321.046 -703 -310 -703H382C393.046 -703 402 -694.046 402 -683V927C402 938.046 393.046 947 382 947H-310C-321.046 947 -330 938.046 -330 927V-683Z" fill="#FDFDFC"/>
5
+ <g id="Content">
6
+ <g id="Avatar">
7
+ <rect x="-273.5" y="-30.5" width="619" height="79" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
8
+ <g id="Chief=Technical, Size=18">
9
+ <path d="M0 9C0 4.02944 4.02944 0 9 0C13.9706 0 18 4.02944 18 9C18 13.9706 13.9706 18 9 18C4.02944 18 0 13.9706 0 9Z" fill="#FDFDFC"/>
10
+ <g id="Logo">
11
+ <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="#262420"/>
12
+ </g>
13
+ </g>
14
+ </g>
15
+ </g>
16
+ </g>
17
+ </svg>
@@ -1,5 +1,5 @@
1
- <svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g id="Logo">
3
- <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="var(--fill-0, #262420)"/>
4
- </g>
5
- </svg>
1
+ <svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g id="Logo">
3
+ <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="var(--fill-0, #262420)"/>
4
+ </g>
5
+ </svg>
@@ -1,16 +1,16 @@
1
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect width="18" height="18" fill="#F5F5F5"/>
3
- <g id="Core">
4
- <path d="M-330 -683C-330 -694.046 -321.046 -703 -310 -703H382C393.046 -703 402 -694.046 402 -683V927C402 938.046 393.046 947 382 947H-310C-321.046 947 -330 938.046 -330 927V-683Z" fill="white"/>
5
- <g id="Content">
6
- <g id="Avatar">
7
- <rect x="-273.5" y="-30.5" width="619" height="79" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
8
- <g id="Chief=Technical, Size=18">
9
- <g id="Logo">
10
- <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="#262420"/>
11
- </g>
12
- </g>
13
- </g>
14
- </g>
15
- </g>
16
- </svg>
1
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="18" height="18" fill="#F5F5F5"/>
3
+ <g id="Core">
4
+ <path d="M-330 -683C-330 -694.046 -321.046 -703 -310 -703H382C393.046 -703 402 -694.046 402 -683V927C402 938.046 393.046 947 382 947H-310C-321.046 947 -330 938.046 -330 927V-683Z" fill="white"/>
5
+ <g id="Content">
6
+ <g id="Avatar">
7
+ <rect x="-273.5" y="-30.5" width="619" height="79" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
8
+ <g id="Chief=Technical, Size=18">
9
+ <g id="Logo">
10
+ <path id="Glyph" d="M9.89258 5.62793L13.2041 14.625H11.9316L9.20117 11.8926H9.03223L6.29785 14.623C6.29785 14.623 6.29573 14.6211 6.29395 14.6211V14.625H5.0625L8.38867 5.62793H9.89258ZM9.01172 7.12305L7.16016 12.2334L8.87109 10.5234H9.36133L11.0762 12.2402L9.23828 7.12305H9.01172ZM12.6904 3.375V4.64355H5.61328V3.375H12.6904Z" fill="#262420"/>
11
+ </g>
12
+ </g>
13
+ </g>
14
+ </g>
15
+ </g>
16
+ </svg>
@@ -1,57 +1,56 @@
1
- import { cn } from "../../utils/cn";
2
-
3
- export type SkeletonVariant = "content" | "search";
4
-
5
- export type SkeletonProps = {
6
- variant?: SkeletonVariant;
7
- className?: string;
8
- };
9
-
10
- const ContentSkeleton = () => (
11
- <div className="flex flex-col gap-4 p-4" aria-hidden>
12
- <div className="flex items-center gap-3">
13
- <div className="size-8 animate-pulse rounded-full bg-grey-200" />
14
- <div className="flex flex-1 flex-col gap-2">
15
- <div className="h-4 w-1/3 animate-pulse rounded bg-grey-200" />
16
- <div className="h-3 w-1/4 animate-pulse rounded bg-grey-100" />
17
- </div>
18
- </div>
19
- <div className="flex flex-col gap-2">
20
- <div className="h-4 w-full animate-pulse rounded bg-grey-200" />
21
- <div className="h-4 w-5/6 animate-pulse rounded bg-grey-200" />
22
- <div className="h-4 w-2/3 animate-pulse rounded bg-grey-100" />
23
- </div>
24
- <div className="h-24 animate-pulse rounded-control bg-grey-100" />
25
- </div>
26
- );
27
-
28
- const SearchSkeleton = () => (
29
- <div className="flex flex-col gap-3 p-4" aria-hidden>
30
- <div className="h-10 animate-pulse rounded-control bg-grey-200" />
31
- <div className="flex gap-2">
32
- <div className="h-7 w-16 animate-pulse rounded-full bg-grey-100" />
33
- <div className="h-7 w-20 animate-pulse rounded-full bg-grey-100" />
34
- <div className="h-7 w-14 animate-pulse rounded-full bg-grey-100" />
35
- </div>
36
- {Array.from({ length: 4 }).map((_, index) => (
37
- <div key={index} className="flex items-center gap-3 py-1">
38
- <div className="size-5 animate-pulse rounded bg-grey-200" />
39
- <div className="flex flex-1 flex-col gap-1.5">
40
- <div className="h-3.5 w-3/4 animate-pulse rounded bg-grey-200" />
41
- <div className="h-3 w-1/2 animate-pulse rounded bg-grey-100" />
42
- </div>
43
- </div>
44
- ))}
45
- </div>
46
- );
47
-
48
- export const Skeleton = ({ variant = "content", className }: SkeletonProps) => (
49
- <div
50
- className={cn("rounded-control bg-background-primary", className)}
51
- role="status"
52
- aria-label="Loading"
53
- data-variant={variant}
54
- >
55
- {variant === "content" ? <ContentSkeleton /> : <SearchSkeleton />}
56
- </div>
57
- );
1
+ import { cn } from "../../utils/cn";
2
+
3
+ export type SkeletonVariant = "content" | "search";
4
+
5
+ export type SkeletonProps = {
6
+ variant?: SkeletonVariant;
7
+ className?: string;
8
+ };
9
+
10
+ const ContentSkeleton = () => (
11
+ <div className="flex flex-col gap-4 p-4" aria-hidden>
12
+ <div className="flex items-center gap-3">
13
+ <div className="size-8 animate-pulse rounded-full bg-grey-200" />
14
+ <div className="flex flex-1 flex-col gap-2">
15
+ <div className="h-4 w-1/3 animate-pulse rounded bg-grey-200" />
16
+ <div className="h-3 w-1/4 animate-pulse rounded bg-grey-100" />
17
+ </div>
18
+ </div>
19
+ <div className="flex flex-col gap-2">
20
+ <div className="h-4 w-full animate-pulse rounded bg-grey-200" />
21
+ <div className="h-4 w-5/6 animate-pulse rounded bg-grey-200" />
22
+ <div className="h-4 w-2/3 animate-pulse rounded bg-grey-100" />
23
+ </div>
24
+ <div className="h-24 animate-pulse rounded-control bg-grey-100" />
25
+ </div>
26
+ );
27
+
28
+ /** Figma Search loading — 8 rows of icon + bar (no input/pills). */
29
+ const SearchSkeleton = () => {
30
+ const widths = [250, 300, 220, 255, 250, 300, 220, 255];
31
+
32
+ return (
33
+ <div className="flex flex-col gap-1.5 p-4" aria-hidden>
34
+ {widths.map((width, index) => (
35
+ <div key={index} className="flex items-start gap-2 py-2">
36
+ <div className="size-3 shrink-0 animate-pulse rounded-md bg-grey-200" />
37
+ <div
38
+ className="h-3 animate-pulse rounded-md bg-grey-200"
39
+ style={{ width }}
40
+ />
41
+ </div>
42
+ ))}
43
+ </div>
44
+ );
45
+ };
46
+
47
+ export const Skeleton = ({ variant = "content", className }: SkeletonProps) => (
48
+ <div
49
+ className={cn("rounded-control bg-background-primary", className)}
50
+ role="status"
51
+ aria-label="Loading"
52
+ data-variant={variant}
53
+ >
54
+ {variant === "content" ? <ContentSkeleton /> : <SearchSkeleton />}
55
+ </div>
56
+ );
@@ -1,30 +1,40 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { cn } from "../utils/cn";
3
3
 
4
- type BadgeColor = "blue" | "green" | "red" | "orange" | "purple" | "grey";
5
- type BadgeType = "label" | "icon";
4
+ export type BadgeColor = "blue" | "green" | "red" | "orange" | "purple" | "grey";
5
+ export type BadgeType = "label" | "icon";
6
6
 
7
7
  export type BadgeProps = {
8
8
  color: BadgeColor;
9
+ /** `label` grows with content (pill). `icon` is a compact circle for a glyph/digit. */
9
10
  type?: BadgeType;
10
11
  children: ReactNode;
11
12
  className?: string;
12
13
  };
13
14
 
14
15
  const colorStyles: Record<BadgeColor, string> = {
15
- blue: "bg-meta-blue text-meta-on-blue",
16
- green: "bg-meta-green text-meta-on-green",
17
- red: "bg-meta-red text-meta-on-red",
18
- orange: "bg-meta-orange text-meta-on-orange",
19
- purple: "bg-meta-purple text-meta-on-purple",
16
+ blue: "bg-meta-blue text-meta-onBlue",
17
+ green: "bg-meta-green text-meta-onGreen",
18
+ red: "bg-meta-red text-meta-onRed",
19
+ orange: "bg-meta-orange text-meta-onOrange",
20
+ purple: "bg-meta-purple text-meta-onPurple",
20
21
  grey: "bg-grey-100 text-display-on-light-secondary",
21
22
  };
22
23
 
23
- export const Badge = ({ color, type = "label", children, className }: BadgeProps) => (
24
+ /**
25
+ * Figma Badge (4365:74363) — padding + rounded-full; no fixed width on labels.
26
+ */
27
+ export const Badge = ({
28
+ color,
29
+ type = "label",
30
+ children,
31
+ className,
32
+ }: BadgeProps) => (
24
33
  <span
25
34
  className={cn(
26
- "inline-flex shrink-0 items-center justify-center text-footnote-em",
27
- type === "label" ? "rounded-control px-2 py-0.5" : "size-6 rounded-full",
35
+ "inline-flex shrink-0 items-center justify-center text-footnote-em leading-none",
36
+ type === "label" && "rounded-full px-1.5 py-0.5",
37
+ type === "icon" && "size-5 rounded-full [&>svg]:size-3",
28
38
  colorStyles[color],
29
39
  className,
30
40
  )}
@@ -1,67 +1,236 @@
1
- import { type ButtonHTMLAttributes, type ReactNode } from "react";
1
+ import {
2
+ type ButtonHTMLAttributes,
3
+ type ComponentType,
4
+ isValidElement,
5
+ type ReactNode,
6
+ } from "react";
7
+ import { ChevronIcon } from "../icons";
8
+ import type { IconProps } from "../icons";
2
9
  import { cn } from "../utils/cn";
10
+ import {
11
+ DropdownMenu,
12
+ DropdownMenuItem,
13
+ } from "./DropdownMenu";
14
+ import {
15
+ getButtonAppearanceClasses,
16
+ getButtonInteractiveClasses,
17
+ type ButtonAppearance,
18
+ type ButtonHierarchy,
19
+ } from "./button-styles";
3
20
 
4
- type ButtonHierarchy = "primary" | "secondary" | "tertiary" | "quaternary";
5
- type ButtonOutcome = "action" | "destructive";
21
+ export type ButtonDropdownOption = {
22
+ label: string;
23
+ onSelect: () => void;
24
+ destructive?: boolean;
25
+ disabled?: boolean;
26
+ icon?: ComponentType<IconProps>;
27
+ };
6
28
 
7
- export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
29
+ export type ButtonProps = Omit<
30
+ ButtonHTMLAttributes<HTMLButtonElement>,
31
+ "children"
32
+ > & {
8
33
  hierarchy?: ButtonHierarchy;
9
- outcome?: ButtonOutcome;
10
34
  disabled?: boolean;
11
- startIcon?: ReactNode;
12
- endIcon?: ReactNode;
35
+ icon?: ComponentType<IconProps> | ReactNode;
36
+ startIcon?: ComponentType<IconProps> | ReactNode;
37
+ dropdownOptions?: ButtonDropdownOption[];
38
+ dropdownOpen?: boolean;
39
+ onDropdownOpenChange?: (open: boolean) => void;
40
+ forcedState?: ButtonAppearance;
13
41
  children: ReactNode;
14
42
  };
15
43
 
16
- const baseStyles =
17
- "inline-flex min-h-10 items-center justify-center gap-2 rounded-control px-2 text-caption-1 transition-colors disabled:cursor-not-allowed";
18
-
19
- const actionStyles: Record<ButtonHierarchy, string> = {
20
- primary:
21
- "bg-action-primary-idle text-action-primary-on-idle hover:bg-action-primary-hover hover:text-action-primary-on-hover disabled:bg-action-primary-disabled disabled:text-action-primary-on-disabled",
22
- secondary:
23
- "border border-divider-primary bg-action-secondary-idle text-action-secondary-on-idle hover:bg-action-secondary-hover hover:text-action-secondary-on-hover disabled:bg-action-secondary-disabled disabled:text-action-secondary-on-disabled",
24
- tertiary:
25
- "bg-action-tertiary-idle text-action-tertiary-on-idle hover:bg-action-tertiary-hover hover:text-action-tertiary-on-hover disabled:bg-action-tertiary-disabled disabled:text-action-tertiary-on-disabled",
26
- quaternary:
27
- "bg-action-quaternary-idle text-action-quaternary-on-idle hover:bg-action-quaternary-hover hover:text-action-quaternary-on-hover disabled:bg-action-quaternary-disabled disabled:text-action-quaternary-on-disabled",
44
+ const renderIcon = (icon?: ComponentType<IconProps> | ReactNode) => {
45
+ if (!icon) return null;
46
+
47
+ if (isValidElement(icon)) {
48
+ return <span className="shrink-0 [&>svg]:size-4">{icon}</span>;
49
+ }
50
+
51
+ if (
52
+ typeof icon === "string" ||
53
+ typeof icon === "number" ||
54
+ typeof icon === "boolean"
55
+ ) {
56
+ return <span className="shrink-0 [&>svg]:size-4">{icon}</span>;
57
+ }
58
+
59
+ const Icon = icon as ComponentType<IconProps>;
60
+ return <Icon size="small" className="shrink-0" />;
28
61
  };
29
62
 
30
- const destructiveStyles: Record<ButtonHierarchy, string> = {
31
- primary:
32
- "bg-action-destructive-idle text-action-destructive-on-idle hover:bg-action-destructive-hover hover:text-action-destructive-on-hover disabled:bg-action-destructive-disabled disabled:text-action-destructive-on-disabled",
33
- secondary:
34
- "border border-red-300 bg-grey-50 text-red-500 hover:bg-red-50 hover:text-red-700 disabled:bg-grey-50 disabled:text-red-300",
35
- tertiary:
36
- "bg-white text-red-500 hover:bg-red-50 hover:text-red-700 disabled:bg-white disabled:text-red-300",
37
- quaternary:
38
- "bg-white text-red-500 hover:bg-grey-100 hover:text-red-700 disabled:bg-white disabled:text-red-300",
63
+ const getAppearance = (
64
+ disabled: boolean,
65
+ forcedState?: ButtonAppearance,
66
+ ): ButtonAppearance => {
67
+ if (forcedState) return forcedState;
68
+ if (disabled) return "disabled";
69
+ return "idle";
70
+ };
71
+
72
+ const getSegmentClasses = (
73
+ hierarchy: ButtonHierarchy,
74
+ appearance: ButtonAppearance,
75
+ forcedState?: ButtonAppearance,
76
+ ) => {
77
+ if (forcedState) {
78
+ return getButtonAppearanceClasses(hierarchy, appearance);
79
+ }
80
+
81
+ if (appearance === "disabled") {
82
+ return getButtonAppearanceClasses(hierarchy, "disabled");
83
+ }
84
+
85
+ if (appearance === "hover") {
86
+ return getButtonAppearanceClasses(hierarchy, "hover");
87
+ }
88
+
89
+ return getButtonInteractiveClasses(hierarchy, false);
90
+ };
91
+
92
+ const getCursorClass = (disabled: boolean) =>
93
+ disabled ? "cursor-not-allowed" : "cursor-pointer";
94
+
95
+ const withSecondarySplitBorders = (
96
+ hierarchy: ButtonHierarchy,
97
+ segment: "main" | "dropdown",
98
+ classes: string,
99
+ ) => {
100
+ if (hierarchy !== "secondary") return classes;
101
+
102
+ return cn(
103
+ classes,
104
+ segment === "main" ? "border-r-0" : "border-l-0",
105
+ );
39
106
  };
40
107
 
41
108
  export const Button = ({
42
109
  hierarchy = "primary",
43
- outcome = "action",
44
110
  disabled = false,
111
+ icon,
45
112
  startIcon,
46
- endIcon,
113
+ dropdownOptions,
114
+ dropdownOpen,
115
+ onDropdownOpenChange,
116
+ forcedState,
47
117
  children,
48
118
  className,
49
119
  type = "button",
120
+ onClick,
50
121
  ...props
51
122
  }: ButtonProps) => {
52
- const variantStyles =
53
- outcome === "destructive" ? destructiveStyles[hierarchy] : actionStyles[hierarchy];
123
+ const resolvedIcon = icon ?? startIcon;
124
+ const renderedIcon = renderIcon(resolvedIcon);
125
+ const hasIcon = renderedIcon != null;
126
+ const hasDropdown = Boolean(dropdownOptions?.length);
127
+ const dropdownDisabled = !dropdownOptions?.some((option) => !option.disabled);
128
+ const mainAppearance = getAppearance(disabled, forcedState);
129
+ const dropdownAppearance = getAppearance(dropdownDisabled, forcedState);
130
+ const mainClasses = getSegmentClasses(
131
+ hierarchy,
132
+ mainAppearance,
133
+ forcedState,
134
+ );
135
+ const dropdownClasses = getSegmentClasses(
136
+ hierarchy,
137
+ dropdownAppearance,
138
+ forcedState,
139
+ );
54
140
 
55
- return (
141
+ /** Label-only: centered text. With start icon: gap between icon and label. */
142
+ const content = (
143
+ <span
144
+ className={cn(
145
+ "flex min-h-6 items-center justify-center",
146
+ hasIcon && "gap-2",
147
+ )}
148
+ >
149
+ {renderedIcon}
150
+ <span>{children}</span>
151
+ </span>
152
+ );
153
+
154
+ if (!hasDropdown) {
155
+ return (
156
+ <button
157
+ type={type}
158
+ disabled={disabled}
159
+ onClick={onClick}
160
+ className={cn(
161
+ "inline-flex min-h-10 items-center justify-center rounded-control p-2 text-caption-1 tracking-[0.32px] transition-colors",
162
+ getCursorClass(disabled),
163
+ mainClasses,
164
+ className,
165
+ )}
166
+ {...props}
167
+ >
168
+ {content}
169
+ </button>
170
+ );
171
+ }
172
+
173
+ const mainButton = (
56
174
  <button
57
175
  type={type}
58
176
  disabled={disabled}
59
- className={cn(baseStyles, variantStyles, className)}
177
+ onClick={onClick}
178
+ className={cn(
179
+ "inline-flex min-h-10 items-center justify-center rounded-none rounded-l-control p-2 text-caption-1 tracking-[0.32px] transition-colors",
180
+ getCursorClass(disabled),
181
+ withSecondarySplitBorders(hierarchy, "main", mainClasses),
182
+ )}
60
183
  {...props}
61
184
  >
62
- {startIcon && <span className="shrink-0 [&>svg]:size-4">{startIcon}</span>}
63
- <span>{children}</span>
64
- {endIcon && <span className="shrink-0 [&>svg]:size-4">{endIcon}</span>}
185
+ {content}
186
+ </button>
187
+ );
188
+
189
+ const dropdownTrigger = (
190
+ <button
191
+ type="button"
192
+ disabled={dropdownDisabled}
193
+ aria-label="Open menu"
194
+ className={cn(
195
+ "inline-flex min-h-10 shrink-0 items-center justify-center rounded-none rounded-r-control px-1 transition-colors",
196
+ getCursorClass(dropdownDisabled),
197
+ withSecondarySplitBorders(hierarchy, "dropdown", dropdownClasses),
198
+ )}
199
+ >
200
+ <ChevronIcon direction="down" size="small" />
65
201
  </button>
66
202
  );
203
+
204
+ return (
205
+ <div
206
+ className={cn(
207
+ "inline-flex min-h-10 items-stretch overflow-hidden rounded-control",
208
+ className,
209
+ )}
210
+ >
211
+ {mainButton}
212
+ <DropdownMenu
213
+ open={dropdownOpen}
214
+ onOpenChange={onDropdownOpenChange}
215
+ align="end"
216
+ trigger={dropdownTrigger}
217
+ contentClassName="min-w-[11rem]"
218
+ >
219
+ {dropdownOptions?.map((option) => (
220
+ <DropdownMenuItem
221
+ key={option.label}
222
+ disabled={option.disabled}
223
+ onSelect={option.onSelect}
224
+ className={cn(
225
+ option.destructive &&
226
+ "text-red-600 hover:bg-red-50 focus:bg-red-50 focus:text-red-600",
227
+ )}
228
+ >
229
+ {option.icon ? renderIcon(option.icon) : null}
230
+ {option.label}
231
+ </DropdownMenuItem>
232
+ ))}
233
+ </DropdownMenu>
234
+ </div>
235
+ );
67
236
  };