@opencosmos/ui 1.3.1

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 (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
@@ -0,0 +1,18 @@
1
+ export * from './Accordion';
2
+ export * from './AspectRatio';
3
+ export * from './Carousel';
4
+ export * from './Collapsible';
5
+ export * from './Container';
6
+ export * from './CustomizerPanel';
7
+ export * from './DatePicker';
8
+ export * from './Footer';
9
+ export * from './Grid';
10
+ export * from './Header';
11
+ export * from './PageLayout';
12
+ export * from './PageTemplate';
13
+ export * from './Resizable';
14
+ export * from './ScrollArea';
15
+ export * from './Separator';
16
+ export * from './Sidebar';
17
+ export * from './Stack';
18
+ export * from './GlassSurface';
@@ -0,0 +1,159 @@
1
+ 'use client';
2
+
3
+ import { motion } from 'framer-motion';
4
+ import { useId } from 'react';
5
+ import { cn } from '../../lib/utils';
6
+
7
+ export interface AnimatedBeamProps {
8
+ className?: string;
9
+ containerRef: React.RefObject<HTMLElement | null>;
10
+ fromRef?: React.RefObject<HTMLElement | null>;
11
+ toRef?: React.RefObject<HTMLElement | null>;
12
+ curvature?: number;
13
+ reverse?: boolean;
14
+ pathColor?: string;
15
+ pathWidth?: number;
16
+ pathOpacity?: number;
17
+ gradientStartColor?: string;
18
+ gradientStopColor?: string;
19
+ delay?: number;
20
+ duration?: number;
21
+ startX?: number;
22
+ startY?: number;
23
+ endX?: number;
24
+ endY?: number;
25
+ }
26
+
27
+ import { useMotionPreference } from '../../hooks/useMotionPreference';
28
+
29
+ export const AnimatedBeam = ({
30
+ className,
31
+ containerRef,
32
+ fromRef,
33
+ toRef,
34
+ curvature = 0,
35
+ reverse = false,
36
+ duration = Math.random() * 3 + 4,
37
+ delay = 0,
38
+ pathColor = "gray",
39
+ pathWidth = 2,
40
+ pathOpacity = 0.2,
41
+ gradientStartColor = "#ffaa40",
42
+ gradientStopColor = "#9c40ff",
43
+ startX,
44
+ startY,
45
+ endX,
46
+ endY,
47
+ }: AnimatedBeamProps) => {
48
+ const { shouldAnimate, scale } = useMotionPreference();
49
+ const id = useId();
50
+ const pathId = `beam-path-${id}`;
51
+ const gradientId = `beam-gradient-${id}`;
52
+ const maskId = `beam-mask-${id}`;
53
+
54
+ // Calculate effective duration based on intensity
55
+ const durationMultiplier = scale > 0 ? (5 / scale) : 1;
56
+ const effectiveDuration = duration * durationMultiplier;
57
+
58
+ // ... (refs logic omitted as in original)
59
+
60
+ const x1 = startX || 0;
61
+ const y1 = startY || 0;
62
+ const x2 = endX || 0;
63
+ const y2 = endY || 0;
64
+
65
+ // Calculate Bezier control points
66
+ const midX = (x1 + x2) / 2;
67
+ const midY = (y1 + y2) / 2;
68
+
69
+ const controlY = y1;
70
+ const controlX = x2;
71
+
72
+ const path = `M ${x1},${y1} C ${x1 + curvature},${y1} ${x2 - curvature},${y2} ${x2},${y2}`;
73
+
74
+ return (
75
+ <svg
76
+ fill="none"
77
+ width="100%"
78
+ height="100%"
79
+ xmlns="http://www.w3.org/2000/svg"
80
+ className={cn("pointer-events-none absolute inset-0 transform-gpu overflow-visible", className)}
81
+ >
82
+ <defs>
83
+ <linearGradient id={gradientId} gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="0%">
84
+ <stop stopColor={gradientStartColor} stopOpacity="0" />
85
+ <stop stopColor={gradientStartColor} />
86
+ <stop offset="32.5%" stopColor={gradientStopColor} />
87
+ <stop offset="100%" stopColor={gradientStopColor} stopOpacity="0" />
88
+ </linearGradient>
89
+
90
+ <marker id={`arrow-${id}`} markerWidth="5" markerHeight="5" refX="2.5" refY="2.5" orient="auto" markerUnits="strokeWidth">
91
+ <circle cx="2.5" cy="2.5" r="2.5" fill={gradientStopColor} />
92
+ </marker>
93
+ </defs>
94
+
95
+ {/* Base Path (faint line) */}
96
+ <path
97
+ d={path}
98
+ stroke={pathColor}
99
+ strokeWidth={pathWidth}
100
+ strokeOpacity={pathOpacity}
101
+ strokeLinecap="round"
102
+ />
103
+
104
+ {/* Animated Path Mask */}
105
+ {shouldAnimate && (
106
+ <path
107
+ d={path}
108
+ stroke={`url(#${gradientId})`}
109
+ strokeWidth={pathWidth}
110
+ strokeLinecap="round"
111
+ fill="none"
112
+ >
113
+ <animate
114
+ attributeName="stroke-dasharray"
115
+ values={`0, 1000; 1000, 0`}
116
+ dur={`${effectiveDuration}s`}
117
+ repeatCount="indefinite"
118
+ />
119
+ </path>
120
+ )}
121
+
122
+ {/* Framer Motion Particle */}
123
+ {shouldAnimate && (
124
+ <motion.circle
125
+ r={pathWidth * 1.5}
126
+ fill={gradientStartColor}
127
+ initial={{ offsetDistance: "0%" }}
128
+ animate={{ offsetDistance: "100%" }}
129
+ transition={{
130
+ duration: effectiveDuration,
131
+ delay: delay,
132
+ ease: "easeInOut",
133
+ repeat: Infinity,
134
+ repeatDelay: 0.5
135
+ }}
136
+ style={{ offsetPath: `path('${path}')` }}
137
+ />
138
+ )}
139
+
140
+ {/* Secondary Particle for "Tail" effect */}
141
+ {shouldAnimate && (
142
+ <motion.circle
143
+ r={pathWidth}
144
+ fill={gradientStopColor}
145
+ initial={{ offsetDistance: "0%", opacity: 0 }}
146
+ animate={{ offsetDistance: "100%", opacity: [0, 1, 0] }}
147
+ transition={{
148
+ duration: effectiveDuration,
149
+ delay: delay + 0.05,
150
+ ease: "easeInOut",
151
+ repeat: Infinity,
152
+ repeatDelay: 0.5
153
+ }}
154
+ style={{ offsetPath: `path('${path}')` }}
155
+ />
156
+ )}
157
+ </svg>
158
+ );
159
+ };
@@ -0,0 +1,57 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect } from 'vitest'
3
+ import {
4
+ Breadcrumb,
5
+ BreadcrumbList,
6
+ BreadcrumbItem,
7
+ BreadcrumbLink,
8
+ BreadcrumbPage,
9
+ BreadcrumbSeparator,
10
+ } from './Breadcrumb'
11
+
12
+ describe('Breadcrumb', () => {
13
+ it('renders breadcrumb navigation', () => {
14
+ render(
15
+ <Breadcrumb>
16
+ <BreadcrumbList>
17
+ <BreadcrumbItem>
18
+ <BreadcrumbLink href="/">Home</BreadcrumbLink>
19
+ </BreadcrumbItem>
20
+ <BreadcrumbSeparator />
21
+ <BreadcrumbItem>
22
+ <BreadcrumbPage>Current</BreadcrumbPage>
23
+ </BreadcrumbItem>
24
+ </BreadcrumbList>
25
+ </Breadcrumb>
26
+ )
27
+ expect(screen.getByRole('navigation')).toBeInTheDocument()
28
+ expect(screen.getByText('Home')).toBeInTheDocument()
29
+ expect(screen.getByText('Current')).toBeInTheDocument()
30
+ })
31
+
32
+ it('renders links with correct href', () => {
33
+ render(
34
+ <Breadcrumb>
35
+ <BreadcrumbList>
36
+ <BreadcrumbItem>
37
+ <BreadcrumbLink href="/docs">Docs</BreadcrumbLink>
38
+ </BreadcrumbItem>
39
+ </BreadcrumbList>
40
+ </Breadcrumb>
41
+ )
42
+ expect(screen.getByRole('link', { name: /docs/i })).toHaveAttribute('href', '/docs')
43
+ })
44
+
45
+ it('marks current page with aria-current', () => {
46
+ render(
47
+ <Breadcrumb>
48
+ <BreadcrumbList>
49
+ <BreadcrumbItem>
50
+ <BreadcrumbPage>Settings</BreadcrumbPage>
51
+ </BreadcrumbItem>
52
+ </BreadcrumbList>
53
+ </Breadcrumb>
54
+ )
55
+ expect(screen.getByText('Settings')).toHaveAttribute('aria-current', 'page')
56
+ })
57
+ })
@@ -0,0 +1,119 @@
1
+ import * as React from "react"
2
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ const Breadcrumb = (
7
+ {
8
+ ref,
9
+ ...props
10
+ }: React.ComponentPropsWithoutRef<"nav"> & {
11
+ separator?: React.ReactNode
12
+ } & {
13
+ ref?: React.Ref<HTMLElement>;
14
+ }
15
+ ) => <nav ref={ref} aria-label="breadcrumb" {...props} />
16
+
17
+ const BreadcrumbList = (
18
+ {
19
+ ref,
20
+ className,
21
+ ...props
22
+ }: React.ComponentPropsWithoutRef<"ol"> & {
23
+ ref?: React.Ref<HTMLOListElement>;
24
+ }
25
+ ) => (<ol
26
+ ref={ref}
27
+ className={cn(
28
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
29
+ className
30
+ )}
31
+ {...props}
32
+ />)
33
+
34
+ const BreadcrumbItem = (
35
+ {
36
+ ref,
37
+ className,
38
+ ...props
39
+ }: React.ComponentPropsWithoutRef<"li"> & {
40
+ ref?: React.Ref<HTMLLIElement>;
41
+ }
42
+ ) => (<li
43
+ ref={ref}
44
+ className={cn("inline-flex items-center gap-1.5", className)}
45
+ {...props}
46
+ />)
47
+
48
+ const BreadcrumbLink = (
49
+ {
50
+ ref,
51
+ className,
52
+ ...props
53
+ }: React.ComponentPropsWithoutRef<"a"> & {
54
+ asChild?: boolean
55
+ } & {
56
+ ref?: React.Ref<HTMLAnchorElement>;
57
+ }
58
+ ) => (<a
59
+ ref={ref}
60
+ className={cn("transition-colors hover:text-foreground", className)}
61
+ {...props}
62
+ />)
63
+
64
+ const BreadcrumbPage = (
65
+ {
66
+ ref,
67
+ className,
68
+ ...props
69
+ }: React.ComponentPropsWithoutRef<"span"> & {
70
+ ref?: React.Ref<HTMLSpanElement>;
71
+ }
72
+ ) => (<span
73
+ ref={ref}
74
+ aria-current="page"
75
+ className={cn("font-normal text-foreground", className)}
76
+ {...props}
77
+ />)
78
+
79
+ const BreadcrumbSeparator = ({
80
+ children,
81
+ className,
82
+ ...props
83
+ }: React.ComponentProps<"li">) => (
84
+ <li
85
+ role="presentation"
86
+ aria-hidden="true"
87
+ className={cn("[&>svg]:size-3.5", className)}
88
+ {...props}
89
+ >
90
+ {children ?? <ChevronRight />}
91
+ </li>
92
+ )
93
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
94
+
95
+ const BreadcrumbEllipsis = ({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<"span">) => (
99
+ <span
100
+ role="presentation"
101
+ aria-hidden="true"
102
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
103
+ {...props}
104
+ >
105
+ <MoreHorizontal className="h-4 w-4" />
106
+ <span className="sr-only">More</span>
107
+ </span>
108
+ )
109
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
110
+
111
+ export {
112
+ Breadcrumb,
113
+ BreadcrumbList,
114
+ BreadcrumbItem,
115
+ BreadcrumbLink,
116
+ BreadcrumbPage,
117
+ BreadcrumbSeparator,
118
+ BreadcrumbEllipsis,
119
+ }
@@ -0,0 +1,221 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ // Legacy BreadcrumbItemLegacy interface (kept separate from shadcn Breadcrumb)
6
+ export interface BreadcrumbItemLegacy {
7
+ /**
8
+ * Display text for the breadcrumb
9
+ */
10
+ label: string;
11
+
12
+ /**
13
+ * Navigation URL (href for anchor tags)
14
+ * Omit for current/last item (will render as plain text)
15
+ */
16
+ href?: string;
17
+
18
+ /**
19
+ * Optional icon to display before the label
20
+ */
21
+ icon?: React.ReactNode;
22
+ }
23
+
24
+ export interface BreadcrumbsProps {
25
+ /**
26
+ * Array of breadcrumb items (hierarchy from root to current)
27
+ */
28
+ items: BreadcrumbItemLegacy[];
29
+
30
+ /**
31
+ * Visual style variant
32
+ * @default 'subtle'
33
+ */
34
+ variant?: 'subtle' | 'bold' | 'underline';
35
+
36
+ /**
37
+ * Custom separator between items
38
+ * @default '/'
39
+ */
40
+ separator?: React.ReactNode;
41
+
42
+ /**
43
+ * ARIA label for the navigation
44
+ * @default 'Breadcrumb'
45
+ */
46
+ ariaLabel?: string;
47
+
48
+ /**
49
+ * Additional CSS classes for customization
50
+ */
51
+ className?: string;
52
+
53
+ /**
54
+ * Callback fired when a breadcrumb link is clicked
55
+ */
56
+ onNavigate?: (item: BreadcrumbItemLegacy, index: number) => void;
57
+ }
58
+
59
+ /**
60
+ * Breadcrumbs Component
61
+ *
62
+ * A navigation component showing page hierarchy with clickable links.
63
+ *
64
+ * Features:
65
+ * - Three visual variants (subtle, bold, underline)
66
+ * - Customizable separators
67
+ * - Full ARIA accessibility
68
+ * - Theme-aware styling
69
+ * - Keyboard navigable
70
+ * - Current page indication
71
+ *
72
+ * Example:
73
+ * ```tsx
74
+ * <Breadcrumbs
75
+ * variant="subtle"
76
+ * items={[
77
+ * { label: 'Home', href: '/' },
78
+ * { label: 'Products', href: '/products' },
79
+ * { label: 'Laptop' }, // Current page (no href)
80
+ * ]}
81
+ * />
82
+ * ```
83
+ */
84
+ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
85
+ items,
86
+ variant = 'subtle',
87
+ separator = '/',
88
+ ariaLabel = 'Breadcrumb',
89
+ className = '',
90
+ onNavigate,
91
+ }) => {
92
+ // Return null if no items
93
+ if (!items || items.length === 0) {
94
+ return null;
95
+ }
96
+
97
+ // Truncate items for long paths: show first item + ... + last 2 items
98
+ const getDisplayItems = () => {
99
+ if (items.length <= 3) {
100
+ return items;
101
+ }
102
+
103
+ // For long paths: [first, ..., second-to-last, last]
104
+ return [
105
+ items[0],
106
+ { label: '...', href: undefined } as BreadcrumbItemLegacy, // Ellipsis
107
+ items[items.length - 2],
108
+ items[items.length - 1],
109
+ ];
110
+ };
111
+
112
+ const displayItems = getDisplayItems();
113
+
114
+ // Get variant-specific styles
115
+ const getVariantStyles = (isLink: boolean, isCurrent: boolean) => {
116
+ const baseStyles = 'text-sm transition-all duration-200';
117
+
118
+ if (variant === 'subtle') {
119
+ if (isCurrent) {
120
+ return `${baseStyles} text-[var(--color-text-muted)] font-medium`;
121
+ }
122
+ if (isLink) {
123
+ return `${baseStyles} text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-hover)] px-1.5 py-1.5 -mx-1.5 -my-1.5 rounded`;
124
+ }
125
+ }
126
+
127
+ if (variant === 'bold') {
128
+ if (isCurrent) {
129
+ return `${baseStyles} text-[var(--color-text-primary)] font-semibold`;
130
+ }
131
+ if (isLink) {
132
+ return `${baseStyles} text-[var(--color-primary)] hover:bg-[var(--color-text-primary)] hover:text-[var(--color-background)] font-medium px-1.5 py-1.5 -mx-1.5 -my-1.5 rounded`;
133
+ }
134
+ }
135
+
136
+ if (variant === 'underline') {
137
+ if (isCurrent) {
138
+ return `${baseStyles} text-[var(--color-text-primary)] font-medium`;
139
+ }
140
+ if (isLink) {
141
+ return `${baseStyles} text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] underline-offset-2 hover:underline decoration-[var(--color-primary)]/40 hover:decoration-[var(--color-primary)] decoration-1 hover:decoration-2 px-1.5 py-1.5 -mx-1.5 -my-1.5`;
142
+ }
143
+ }
144
+
145
+ return baseStyles;
146
+ };
147
+
148
+ const getLinkStyles = () => {
149
+ return `
150
+ focus-visible:outline-none
151
+ focus-visible:ring-2
152
+ focus-visible:ring-[var(--color-focus)]
153
+ focus-visible:ring-offset-2
154
+ active:scale-95
155
+ cursor-pointer
156
+ `;
157
+ };
158
+
159
+ const getSeparatorStyles = () => {
160
+ const baseStyles = 'mx-2 select-none';
161
+
162
+ if (variant === 'subtle') {
163
+ return `${baseStyles} text-[var(--color-text-muted)]`;
164
+ }
165
+ if (variant === 'bold') {
166
+ return `${baseStyles} text-[var(--color-border)] font-bold`;
167
+ }
168
+ if (variant === 'underline') {
169
+ return `${baseStyles} text-[var(--color-text-muted)]`;
170
+ }
171
+
172
+ return baseStyles;
173
+ };
174
+
175
+ return (
176
+ <nav aria-label={ariaLabel} className={className}>
177
+ <ol className="flex items-center flex-nowrap list-none m-0 p-0 overflow-x-auto scrollbar-hide">
178
+ {displayItems.map((item, index) => {
179
+ const isLast = index === displayItems.length - 1;
180
+ const isEllipsis = item.label === '...';
181
+ const isInteractive = item.href && !isLast && !isEllipsis;
182
+
183
+ return (
184
+ <li key={index} className="flex items-center flex-shrink-0">
185
+ {isInteractive ? (
186
+ <a
187
+ href={item.href}
188
+ onClick={(e) => {
189
+ if (onNavigate) {
190
+ onNavigate(item, index);
191
+ }
192
+ }}
193
+ className={`${getVariantStyles(true, false)} ${getLinkStyles()}`}
194
+ >
195
+ {item.icon && <span className="inline-flex mr-1.5">{item.icon}</span>}
196
+ {item.label}
197
+ </a>
198
+ ) : (
199
+ <span
200
+ className={isEllipsis ? 'text-sm text-[var(--color-text-muted)] px-1' : getVariantStyles(false, isLast)}
201
+ aria-current={isLast ? 'page' : undefined}
202
+ >
203
+ {item.icon && <span className="inline-flex mr-1.5">{item.icon}</span>}
204
+ {item.label}
205
+ </span>
206
+ )}
207
+
208
+ {!isLast && (
209
+ <span aria-hidden="true" className={getSeparatorStyles()}>
210
+ {separator}
211
+ </span>
212
+ )}
213
+ </li>
214
+ );
215
+ })}
216
+ </ol>
217
+ </nav>
218
+ );
219
+ };
220
+
221
+ Breadcrumbs.displayName = 'Breadcrumbs';