@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,47 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs'
5
+
6
+ describe('Tabs', () => {
7
+ const renderTabs = (defaultValue = 'tab1') =>
8
+ render(
9
+ <Tabs defaultValue={defaultValue}>
10
+ <TabsList>
11
+ <TabsTrigger value="tab1">Tab 1</TabsTrigger>
12
+ <TabsTrigger value="tab2">Tab 2</TabsTrigger>
13
+ <TabsTrigger value="tab3">Tab 3</TabsTrigger>
14
+ </TabsList>
15
+ <TabsContent value="tab1">Content for tab 1</TabsContent>
16
+ <TabsContent value="tab2">Content for tab 2</TabsContent>
17
+ <TabsContent value="tab3">Content for tab 3</TabsContent>
18
+ </Tabs>
19
+ )
20
+
21
+ it('renders tabs with content', () => {
22
+ renderTabs()
23
+ expect(screen.getByRole('tab', { name: /tab 1/i })).toBeInTheDocument()
24
+ expect(screen.getByRole('tab', { name: /tab 2/i })).toBeInTheDocument()
25
+ expect(screen.getByRole('tab', { name: /tab 3/i })).toBeInTheDocument()
26
+ expect(screen.getByText('Content for tab 1')).toBeInTheDocument()
27
+ })
28
+
29
+ it('switches content on tab click', async () => {
30
+ const user = userEvent.setup()
31
+ renderTabs()
32
+
33
+ expect(screen.getByText('Content for tab 1')).toBeInTheDocument()
34
+ expect(screen.queryByText('Content for tab 2')).not.toBeInTheDocument()
35
+
36
+ await user.click(screen.getByRole('tab', { name: /tab 2/i }))
37
+ expect(screen.queryByText('Content for tab 1')).not.toBeInTheDocument()
38
+ expect(screen.getByText('Content for tab 2')).toBeInTheDocument()
39
+ })
40
+
41
+ it('defaults to first tab', () => {
42
+ renderTabs('tab1')
43
+ const tab1 = screen.getByRole('tab', { name: /tab 1/i })
44
+ expect(tab1).toHaveAttribute('data-state', 'active')
45
+ expect(screen.getByText('Content for tab 1')).toBeInTheDocument()
46
+ })
47
+ })
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ const Tabs = TabsPrimitive.Root
8
+
9
+ const TabsList = (
10
+ {
11
+ ref,
12
+ className,
13
+ ...props
14
+ }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
15
+ ref?: React.Ref<React.ElementRef<typeof TabsPrimitive.List>>;
16
+ }
17
+ ) => (<TabsPrimitive.List
18
+ ref={ref}
19
+ className={cn(
20
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
21
+ className
22
+ )}
23
+ {...props}
24
+ />)
25
+
26
+ const TabsTrigger = (
27
+ {
28
+ ref,
29
+ className,
30
+ ...props
31
+ }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
32
+ ref?: React.Ref<React.ElementRef<typeof TabsPrimitive.Trigger>>;
33
+ }
34
+ ) => (<TabsPrimitive.Trigger
35
+ ref={ref}
36
+ className={cn(
37
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
38
+ className
39
+ )}
40
+ {...props}
41
+ />)
42
+
43
+ const TabsContent = (
44
+ {
45
+ ref,
46
+ className,
47
+ ...props
48
+ }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> & {
49
+ ref?: React.Ref<React.ElementRef<typeof TabsPrimitive.Content>>;
50
+ }
51
+ ) => (<TabsPrimitive.Content
52
+ ref={ref}
53
+ className={cn(
54
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
55
+ className
56
+ )}
57
+ {...props}
58
+ />)
59
+
60
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,90 @@
1
+ 'use client';;
2
+ import React from 'react';
3
+ import { FilterButton } from '../forms/FilterButton';
4
+
5
+ export interface TertiaryNavItem {
6
+ id: string;
7
+ label: string;
8
+ }
9
+
10
+ export interface TertiaryNavProps {
11
+ /**
12
+ * Array of navigation items
13
+ */
14
+ items: TertiaryNavItem[];
15
+ /**
16
+ * Currently active item ID
17
+ */
18
+ activeId: string;
19
+ /**
20
+ * Callback when an item is selected
21
+ */
22
+ onItemChange: (id: string) => void;
23
+ /**
24
+ * Additional className for customization
25
+ */
26
+ className?: string;
27
+ /**
28
+ * Sticky behavior mode
29
+ * - 'stacked': Positions automatically below SecondaryNav (default)
30
+ * - 'standalone': Sticks to top of viewport (0px)
31
+ * @default 'stacked'
32
+ */
33
+ mode?: 'stacked' | 'standalone';
34
+ /**
35
+ * Top offset for sticky positioning.
36
+ * Overrides 'mode' if provided.
37
+ */
38
+ top?: string;
39
+ }
40
+
41
+ /**
42
+ * Tertiary Navigation Component
43
+ * ... (keep existing comment block) ...
44
+ */
45
+ export const TertiaryNav = (
46
+ {
47
+ ref,
48
+ items,
49
+ activeId,
50
+ onItemChange,
51
+ mode = 'stacked',
52
+ top,
53
+ className = ''
54
+ }: TertiaryNavProps & {
55
+ ref?: React.Ref<HTMLElement>;
56
+ }
57
+ ) => {
58
+ // Determine sticky position based on mode, unless manually overridden
59
+ const stickyClass = top || (mode === 'stacked' ? 'top-32 lg:top-36' : 'top-0');
60
+
61
+ return (
62
+ <nav
63
+ ref={ref}
64
+ className={`
65
+ sticky ${stickyClass} z-30
66
+ bg-[var(--color-surface)]/60 backdrop-blur-md
67
+ border-b border-[var(--color-border)]
68
+ ${className}
69
+ `}
70
+ aria-label="Tertiary navigation"
71
+ >
72
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
73
+ <div className="flex items-center gap-2 overflow-x-auto py-3 scrollbar-hide">
74
+ {items.map((item) => (
75
+ <FilterButton
76
+ key={item.id}
77
+ onClick={() => onItemChange(item.id)}
78
+ active={activeId === item.id}
79
+ shape="rounded"
80
+ aria-current={activeId === item.id ? 'page' : undefined}
81
+ className="whitespace-nowrap"
82
+ >
83
+ {item.label}
84
+ </FilterButton>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ </nav>
89
+ );
90
+ };
@@ -0,0 +1,10 @@
1
+ export * from './Breadcrumb';
2
+ export * from './Breadcrumbs';
3
+ export * from './Command';
4
+ export * from './Menubar';
5
+ export * from './NavLink';
6
+ export * from './NavigationMenu';
7
+ export * from './Pagination';
8
+ export * from './SecondaryNav';
9
+ export * from './Tabs';
10
+ export * from './TertiaryNav';
@@ -0,0 +1,69 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogTrigger,
7
+ AlertDialogContent,
8
+ AlertDialogHeader,
9
+ AlertDialogTitle,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ } from './AlertDialog'
15
+
16
+ describe('AlertDialog', () => {
17
+ it('renders trigger', () => {
18
+ render(
19
+ <AlertDialog>
20
+ <AlertDialogTrigger>Delete</AlertDialogTrigger>
21
+ <AlertDialogContent>
22
+ <AlertDialogTitle>Confirm</AlertDialogTitle>
23
+ <AlertDialogDescription>Are you sure?</AlertDialogDescription>
24
+ </AlertDialogContent>
25
+ </AlertDialog>
26
+ )
27
+ expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument()
28
+ })
29
+
30
+ it('opens on trigger click', async () => {
31
+ const user = userEvent.setup()
32
+ render(
33
+ <AlertDialog>
34
+ <AlertDialogTrigger>Delete</AlertDialogTrigger>
35
+ <AlertDialogContent>
36
+ <AlertDialogTitle>Confirm Delete</AlertDialogTitle>
37
+ <AlertDialogDescription>This cannot be undone.</AlertDialogDescription>
38
+ </AlertDialogContent>
39
+ </AlertDialog>
40
+ )
41
+
42
+ expect(screen.queryByText('Confirm Delete')).not.toBeInTheDocument()
43
+ await user.click(screen.getByRole('button', { name: /delete/i }))
44
+ expect(screen.getByText('Confirm Delete')).toBeInTheDocument()
45
+ expect(screen.getByText('This cannot be undone.')).toBeInTheDocument()
46
+ })
47
+
48
+ it('renders action and cancel buttons', async () => {
49
+ const user = userEvent.setup()
50
+ const onAction = vi.fn()
51
+ render(
52
+ <AlertDialog>
53
+ <AlertDialogTrigger>Delete</AlertDialogTrigger>
54
+ <AlertDialogContent>
55
+ <AlertDialogTitle>Confirm</AlertDialogTitle>
56
+ <AlertDialogDescription>Sure?</AlertDialogDescription>
57
+ <AlertDialogFooter>
58
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
59
+ <AlertDialogAction onClick={onAction}>Confirm</AlertDialogAction>
60
+ </AlertDialogFooter>
61
+ </AlertDialogContent>
62
+ </AlertDialog>
63
+ )
64
+
65
+ await user.click(screen.getByRole('button', { name: /delete/i }))
66
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
67
+ expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument()
68
+ })
69
+ })
@@ -0,0 +1,166 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import { buttonVariants } from "../actions/Button"
7
+
8
+ const AlertDialog = AlertDialogPrimitive.Root
9
+
10
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
11
+
12
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
13
+
14
+ const AlertDialogOverlay = (
15
+ {
16
+ ref,
17
+ className,
18
+ style,
19
+ ...props
20
+ }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> & {
21
+ ref?: React.Ref<React.ElementRef<typeof AlertDialogPrimitive.Overlay>>;
22
+ }
23
+ ) => (<AlertDialogPrimitive.Overlay
24
+ className={cn(
25
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
26
+ className
27
+ )}
28
+ style={{
29
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
30
+ zIndex: 50,
31
+ ...style,
32
+ }}
33
+ {...props}
34
+ ref={ref}
35
+ />)
36
+
37
+ const AlertDialogContent = (
38
+ {
39
+ ref,
40
+ className,
41
+ style,
42
+ ...props
43
+ }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & {
44
+ ref?: React.Ref<React.ElementRef<typeof AlertDialogPrimitive.Content>>;
45
+ }
46
+ ) => (<AlertDialogPortal>
47
+ <AlertDialogOverlay />
48
+ <AlertDialogPrimitive.Content
49
+ ref={ref}
50
+ className={cn(
51
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
52
+ className
53
+ )}
54
+ style={{
55
+ backgroundColor: 'var(--color-background, #ffffff)',
56
+ border: '1px solid var(--color-border, #d4d4d4)',
57
+ borderRadius: '0.75rem',
58
+ boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
59
+ zIndex: 50,
60
+ ...style,
61
+ }}
62
+ {...props}
63
+ />
64
+ </AlertDialogPortal>)
65
+
66
+ const AlertDialogHeader = ({
67
+ className,
68
+ ...props
69
+ }: React.HTMLAttributes<HTMLDivElement>) => (
70
+ <div
71
+ className={cn(
72
+ "flex flex-col space-y-2 text-center sm:text-left",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ )
78
+ AlertDialogHeader.displayName = "AlertDialogHeader"
79
+
80
+ const AlertDialogFooter = ({
81
+ className,
82
+ ...props
83
+ }: React.HTMLAttributes<HTMLDivElement>) => (
84
+ <div
85
+ className={cn(
86
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ AlertDialogFooter.displayName = "AlertDialogFooter"
93
+
94
+ const AlertDialogTitle = (
95
+ {
96
+ ref,
97
+ className,
98
+ ...props
99
+ }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> & {
100
+ ref?: React.Ref<React.ElementRef<typeof AlertDialogPrimitive.Title>>;
101
+ }
102
+ ) => (<AlertDialogPrimitive.Title
103
+ ref={ref}
104
+ className={cn("text-lg font-semibold", className)}
105
+ {...props}
106
+ />)
107
+
108
+ const AlertDialogDescription = (
109
+ {
110
+ ref,
111
+ className,
112
+ ...props
113
+ }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> & {
114
+ ref?: React.Ref<React.ElementRef<typeof AlertDialogPrimitive.Description>>;
115
+ }
116
+ ) => (<AlertDialogPrimitive.Description
117
+ ref={ref}
118
+ className={cn("text-sm text-muted-foreground", className)}
119
+ {...props}
120
+ />)
121
+
122
+ const AlertDialogAction = (
123
+ {
124
+ ref,
125
+ className,
126
+ ...props
127
+ }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> & {
128
+ ref?: React.Ref<React.ElementRef<typeof AlertDialogPrimitive.Action>>;
129
+ }
130
+ ) => (<AlertDialogPrimitive.Action
131
+ ref={ref}
132
+ className={cn(buttonVariants(), className)}
133
+ {...props}
134
+ />)
135
+
136
+ const AlertDialogCancel = (
137
+ {
138
+ ref,
139
+ className,
140
+ ...props
141
+ }: React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> & {
142
+ ref?: React.Ref<React.ElementRef<typeof AlertDialogPrimitive.Cancel>>;
143
+ }
144
+ ) => (<AlertDialogPrimitive.Cancel
145
+ ref={ref}
146
+ className={cn(
147
+ buttonVariants({ variant: "outline" }),
148
+ "mt-2 sm:mt-0",
149
+ className
150
+ )}
151
+ {...props}
152
+ />)
153
+
154
+ export {
155
+ AlertDialog,
156
+ AlertDialogPortal,
157
+ AlertDialogOverlay,
158
+ AlertDialogTrigger,
159
+ AlertDialogContent,
160
+ AlertDialogHeader,
161
+ AlertDialogFooter,
162
+ AlertDialogTitle,
163
+ AlertDialogDescription,
164
+ AlertDialogAction,
165
+ AlertDialogCancel,
166
+ }
@@ -0,0 +1,243 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
4
+ import { Check, ChevronRight, Circle } from "lucide-react"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const ContextMenu = ContextMenuPrimitive.Root
9
+
10
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
11
+
12
+ const ContextMenuGroup = ContextMenuPrimitive.Group
13
+
14
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
15
+
16
+ const ContextMenuSub = ContextMenuPrimitive.Sub
17
+
18
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
19
+
20
+ const ContextMenuSubTrigger = (
21
+ {
22
+ ref,
23
+ className,
24
+ inset,
25
+ children,
26
+ ...props
27
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
28
+ inset?: boolean
29
+ } & {
30
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>>;
31
+ }
32
+ ) => (<ContextMenuPrimitive.SubTrigger
33
+ ref={ref}
34
+ className={cn(
35
+ "flex cursor-default select-none items-center rounded-xs px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
36
+ inset && "pl-8",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <ChevronRight className="ml-auto h-4 w-4" />
43
+ </ContextMenuPrimitive.SubTrigger>)
44
+
45
+ const ContextMenuSubContent = (
46
+ {
47
+ ref,
48
+ className,
49
+ style,
50
+ ...props
51
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> & {
52
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.SubContent>>;
53
+ }
54
+ ) => (<ContextMenuPrimitive.SubContent
55
+ ref={ref}
56
+ className={cn(
57
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
58
+ className
59
+ )}
60
+ style={{
61
+ backgroundColor: 'var(--color-popover, #ffffff)',
62
+ color: 'var(--color-popover-foreground, #0a0a0a)',
63
+ border: '1px solid var(--color-border, #d4d4d4)',
64
+ borderRadius: 'var(--radius, 0.5rem)',
65
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
66
+ zIndex: 50,
67
+ overflow: 'hidden',
68
+ ...style,
69
+ }}
70
+ {...props}
71
+ />)
72
+
73
+ const ContextMenuContent = (
74
+ {
75
+ ref,
76
+ className,
77
+ style,
78
+ ...props
79
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> & {
80
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.Content>>;
81
+ }
82
+ ) => (<ContextMenuPrimitive.Portal>
83
+ <ContextMenuPrimitive.Content
84
+ ref={ref}
85
+ className={cn(
86
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
87
+ className
88
+ )}
89
+ style={{
90
+ backgroundColor: 'var(--color-popover, #ffffff)',
91
+ color: 'var(--color-popover-foreground, #0a0a0a)',
92
+ border: '1px solid var(--color-border, #d4d4d4)',
93
+ borderRadius: 'var(--radius, 0.5rem)',
94
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
95
+ zIndex: 50,
96
+ overflow: 'hidden',
97
+ ...style,
98
+ }}
99
+ {...props}
100
+ />
101
+ </ContextMenuPrimitive.Portal>)
102
+
103
+ const ContextMenuItem = (
104
+ {
105
+ ref,
106
+ className,
107
+ inset,
108
+ ...props
109
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
110
+ inset?: boolean
111
+ } & {
112
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.Item>>;
113
+ }
114
+ ) => (<ContextMenuPrimitive.Item
115
+ ref={ref}
116
+ className={cn(
117
+ "relative flex cursor-default select-none items-center rounded-xs px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
118
+ inset && "pl-8",
119
+ className
120
+ )}
121
+ {...props}
122
+ />)
123
+
124
+ const ContextMenuCheckboxItem = (
125
+ {
126
+ ref,
127
+ className,
128
+ children,
129
+ checked,
130
+ ...props
131
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & {
132
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>>;
133
+ }
134
+ ) => (<ContextMenuPrimitive.CheckboxItem
135
+ ref={ref}
136
+ className={cn(
137
+ "relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
138
+ className
139
+ )}
140
+ checked={checked}
141
+ {...props}
142
+ >
143
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
144
+ <ContextMenuPrimitive.ItemIndicator>
145
+ <Check className="h-4 w-4" />
146
+ </ContextMenuPrimitive.ItemIndicator>
147
+ </span>
148
+ {children}
149
+ </ContextMenuPrimitive.CheckboxItem>)
150
+
151
+ const ContextMenuRadioItem = (
152
+ {
153
+ ref,
154
+ className,
155
+ children,
156
+ ...props
157
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & {
158
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.RadioItem>>;
159
+ }
160
+ ) => (<ContextMenuPrimitive.RadioItem
161
+ ref={ref}
162
+ className={cn(
163
+ "relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
164
+ className
165
+ )}
166
+ {...props}
167
+ >
168
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
169
+ <ContextMenuPrimitive.ItemIndicator>
170
+ <Circle className="h-2 w-2 fill-current" />
171
+ </ContextMenuPrimitive.ItemIndicator>
172
+ </span>
173
+ {children}
174
+ </ContextMenuPrimitive.RadioItem>)
175
+
176
+ const ContextMenuLabel = (
177
+ {
178
+ ref,
179
+ className,
180
+ inset,
181
+ ...props
182
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
183
+ inset?: boolean
184
+ } & {
185
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.Label>>;
186
+ }
187
+ ) => (<ContextMenuPrimitive.Label
188
+ ref={ref}
189
+ className={cn(
190
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
191
+ inset && "pl-8",
192
+ className
193
+ )}
194
+ {...props}
195
+ />)
196
+
197
+ const ContextMenuSeparator = (
198
+ {
199
+ ref,
200
+ className,
201
+ ...props
202
+ }: React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> & {
203
+ ref?: React.Ref<React.ElementRef<typeof ContextMenuPrimitive.Separator>>;
204
+ }
205
+ ) => (<ContextMenuPrimitive.Separator
206
+ ref={ref}
207
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
208
+ {...props}
209
+ />)
210
+
211
+ const ContextMenuShortcut = ({
212
+ className,
213
+ ...props
214
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
215
+ return (
216
+ <span
217
+ className={cn(
218
+ "ml-auto text-xs tracking-widest text-muted-foreground",
219
+ className
220
+ )}
221
+ {...props}
222
+ />
223
+ )
224
+ }
225
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
226
+
227
+ export {
228
+ ContextMenu,
229
+ ContextMenuTrigger,
230
+ ContextMenuContent,
231
+ ContextMenuItem,
232
+ ContextMenuCheckboxItem,
233
+ ContextMenuRadioItem,
234
+ ContextMenuLabel,
235
+ ContextMenuSeparator,
236
+ ContextMenuShortcut,
237
+ ContextMenuGroup,
238
+ ContextMenuPortal,
239
+ ContextMenuSub,
240
+ ContextMenuSubContent,
241
+ ContextMenuSubTrigger,
242
+ ContextMenuRadioGroup,
243
+ }