@thangnqdev/ui 0.1.0

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 (253) hide show
  1. package/README.md +196 -0
  2. package/dist/Label-DWOlgSNW.d.cts +14 -0
  3. package/dist/Label-DWOlgSNW.d.ts +14 -0
  4. package/dist/bento/index.cjs +140 -0
  5. package/dist/bento/index.cjs.map +1 -0
  6. package/dist/bento/index.d.cts +4 -0
  7. package/dist/bento/index.d.ts +4 -0
  8. package/dist/bento/index.js +4 -0
  9. package/dist/bento/index.js.map +1 -0
  10. package/dist/bento/styles.css +46 -0
  11. package/dist/buttons/index.cjs +365 -0
  12. package/dist/buttons/index.cjs.map +1 -0
  13. package/dist/buttons/index.d.cts +4 -0
  14. package/dist/buttons/index.d.ts +4 -0
  15. package/dist/buttons/index.js +5 -0
  16. package/dist/buttons/index.js.map +1 -0
  17. package/dist/buttons/styles.css +6 -0
  18. package/dist/chunk-2OXZK6IP.js +514 -0
  19. package/dist/chunk-2OXZK6IP.js.map +1 -0
  20. package/dist/chunk-4S6L5V7X.js +15 -0
  21. package/dist/chunk-4S6L5V7X.js.map +1 -0
  22. package/dist/chunk-5FZ6ZFII.js +728 -0
  23. package/dist/chunk-5FZ6ZFII.js.map +1 -0
  24. package/dist/chunk-66VWUJEI.js +86 -0
  25. package/dist/chunk-66VWUJEI.js.map +1 -0
  26. package/dist/chunk-A7P3O5Z5.js +163 -0
  27. package/dist/chunk-A7P3O5Z5.js.map +1 -0
  28. package/dist/chunk-ATYGDTMK.js +257 -0
  29. package/dist/chunk-ATYGDTMK.js.map +1 -0
  30. package/dist/chunk-C4SUXMI3.js +138 -0
  31. package/dist/chunk-C4SUXMI3.js.map +1 -0
  32. package/dist/chunk-EU2LPUYP.js +25 -0
  33. package/dist/chunk-EU2LPUYP.js.map +1 -0
  34. package/dist/chunk-F5Q7BTMH.js +1991 -0
  35. package/dist/chunk-F5Q7BTMH.js.map +1 -0
  36. package/dist/chunk-GXI2H5XA.js +46 -0
  37. package/dist/chunk-GXI2H5XA.js.map +1 -0
  38. package/dist/chunk-H3LCNPMC.js +101 -0
  39. package/dist/chunk-H3LCNPMC.js.map +1 -0
  40. package/dist/chunk-IABPUCAQ.js +19 -0
  41. package/dist/chunk-IABPUCAQ.js.map +1 -0
  42. package/dist/chunk-KJRBRTQE.js +101 -0
  43. package/dist/chunk-KJRBRTQE.js.map +1 -0
  44. package/dist/chunk-LBUBXUYQ.js +40 -0
  45. package/dist/chunk-LBUBXUYQ.js.map +1 -0
  46. package/dist/chunk-LWFRUJRU.js +537 -0
  47. package/dist/chunk-LWFRUJRU.js.map +1 -0
  48. package/dist/chunk-NFVONJUK.js +882 -0
  49. package/dist/chunk-NFVONJUK.js.map +1 -0
  50. package/dist/chunk-PF4ZHGVW.js +284 -0
  51. package/dist/chunk-PF4ZHGVW.js.map +1 -0
  52. package/dist/chunk-WAFHC4DQ.js +366 -0
  53. package/dist/chunk-WAFHC4DQ.js.map +1 -0
  54. package/dist/chunk-X76KBH4Y.js +71 -0
  55. package/dist/chunk-X76KBH4Y.js.map +1 -0
  56. package/dist/core/index.cjs +103 -0
  57. package/dist/core/index.cjs.map +1 -0
  58. package/dist/core/index.d.cts +3 -0
  59. package/dist/core/index.d.ts +3 -0
  60. package/dist/core/index.js +4 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/core/styles.css +128 -0
  63. package/dist/data-display/index.cjs +1023 -0
  64. package/dist/data-display/index.cjs.map +1 -0
  65. package/dist/data-display/index.d.cts +4 -0
  66. package/dist/data-display/index.d.ts +4 -0
  67. package/dist/data-display/index.js +6 -0
  68. package/dist/data-display/index.js.map +1 -0
  69. package/dist/data-display/styles.css +4 -0
  70. package/dist/feedback/index.cjs +368 -0
  71. package/dist/feedback/index.cjs.map +1 -0
  72. package/dist/feedback/index.d.cts +4 -0
  73. package/dist/feedback/index.d.ts +4 -0
  74. package/dist/feedback/index.js +4 -0
  75. package/dist/feedback/index.js.map +1 -0
  76. package/dist/feedback/styles.css +4 -0
  77. package/dist/forms/index.cjs +2287 -0
  78. package/dist/forms/index.cjs.map +1 -0
  79. package/dist/forms/index.d.cts +5 -0
  80. package/dist/forms/index.d.ts +5 -0
  81. package/dist/forms/index.js +10 -0
  82. package/dist/forms/index.js.map +1 -0
  83. package/dist/forms/styles.css +6 -0
  84. package/dist/glass/index.cjs +165 -0
  85. package/dist/glass/index.cjs.map +1 -0
  86. package/dist/glass/index.d.cts +4 -0
  87. package/dist/glass/index.d.ts +4 -0
  88. package/dist/glass/index.js +4 -0
  89. package/dist/glass/index.js.map +1 -0
  90. package/dist/glass/styles.css +81 -0
  91. package/dist/index-BTvkbCIj.d.cts +146 -0
  92. package/dist/index-BTvkbCIj.d.ts +146 -0
  93. package/dist/index-BkRqGNG1.d.cts +201 -0
  94. package/dist/index-BkRqGNG1.d.ts +201 -0
  95. package/dist/index-BlPcGeW-.d.cts +45 -0
  96. package/dist/index-BlPcGeW-.d.ts +45 -0
  97. package/dist/index-BzOCqvC6.d.cts +32 -0
  98. package/dist/index-BzOCqvC6.d.ts +32 -0
  99. package/dist/index-CCeWYAvn.d.cts +83 -0
  100. package/dist/index-COQ7uwt9.d.cts +44 -0
  101. package/dist/index-COQ7uwt9.d.ts +44 -0
  102. package/dist/index-CenuF_P2.d.cts +221 -0
  103. package/dist/index-CenuF_P2.d.ts +221 -0
  104. package/dist/index-Cle6QzQl.d.ts +371 -0
  105. package/dist/index-D6fE7w-b.d.cts +90 -0
  106. package/dist/index-D6fE7w-b.d.ts +90 -0
  107. package/dist/index-DR8cfJUG.d.ts +83 -0
  108. package/dist/index-DzcWh1Hs.d.cts +131 -0
  109. package/dist/index-DzcWh1Hs.d.ts +131 -0
  110. package/dist/index-TyK-Ldpy.d.cts +371 -0
  111. package/dist/index-ugj5s6a3.d.cts +95 -0
  112. package/dist/index-ugj5s6a3.d.ts +95 -0
  113. package/dist/index.cjs +6400 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +16 -0
  116. package/dist/index.d.ts +16 -0
  117. package/dist/index.js +21 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/layout/index.cjs +516 -0
  120. package/dist/layout/index.cjs.map +1 -0
  121. package/dist/layout/index.d.cts +4 -0
  122. package/dist/layout/index.d.ts +4 -0
  123. package/dist/layout/index.js +4 -0
  124. package/dist/layout/index.js.map +1 -0
  125. package/dist/layout/styles.css +8 -0
  126. package/dist/navigation/index.cjs +673 -0
  127. package/dist/navigation/index.cjs.map +1 -0
  128. package/dist/navigation/index.d.cts +2 -0
  129. package/dist/navigation/index.d.ts +2 -0
  130. package/dist/navigation/index.js +8 -0
  131. package/dist/navigation/index.js.map +1 -0
  132. package/dist/navigation/styles.css +4 -0
  133. package/dist/overlays/index.cjs +954 -0
  134. package/dist/overlays/index.cjs.map +1 -0
  135. package/dist/overlays/index.d.cts +4 -0
  136. package/dist/overlays/index.d.ts +4 -0
  137. package/dist/overlays/index.js +9 -0
  138. package/dist/overlays/index.js.map +1 -0
  139. package/dist/overlays/styles.css +8 -0
  140. package/dist/styles.css +333 -0
  141. package/dist/typography/index.cjs +311 -0
  142. package/dist/typography/index.cjs.map +1 -0
  143. package/dist/typography/index.d.cts +5 -0
  144. package/dist/typography/index.d.ts +5 -0
  145. package/dist/typography/index.js +6 -0
  146. package/dist/typography/index.js.map +1 -0
  147. package/dist/typography/styles.css +5 -0
  148. package/llms.txt +113 -0
  149. package/package.json +116 -0
  150. package/src/bento/BentoCard.tsx +140 -0
  151. package/src/bento/BentoGrid.tsx +50 -0
  152. package/src/bento/index.ts +2 -0
  153. package/src/bento/tokens.css +46 -0
  154. package/src/buttons/Button.tsx +104 -0
  155. package/src/buttons/ButtonGroup.tsx +43 -0
  156. package/src/buttons/IconButton.tsx +79 -0
  157. package/src/buttons/SplitButton.tsx +86 -0
  158. package/src/buttons/ToggleButton.tsx +157 -0
  159. package/src/buttons/index.ts +5 -0
  160. package/src/buttons/tokens.css +6 -0
  161. package/src/core/ThemeProvider.tsx +123 -0
  162. package/src/core/index.ts +2 -0
  163. package/src/core/tokens.css +128 -0
  164. package/src/data-display/Accordion.tsx +147 -0
  165. package/src/data-display/Avatar.tsx +134 -0
  166. package/src/data-display/Card.tsx +112 -0
  167. package/src/data-display/Carousel.tsx +130 -0
  168. package/src/data-display/Chip.tsx +74 -0
  169. package/src/data-display/DataTablePro.tsx +356 -0
  170. package/src/data-display/StatCard.tsx +60 -0
  171. package/src/data-display/Table.tsx +74 -0
  172. package/src/data-display/Timeline.tsx +62 -0
  173. package/src/data-display/TreeView.tsx +140 -0
  174. package/src/data-display/index.ts +10 -0
  175. package/src/data-display/tokens.css +4 -0
  176. package/src/feedback/Alert.tsx +80 -0
  177. package/src/feedback/EmptyState.tsx +45 -0
  178. package/src/feedback/ProgressBar.tsx +174 -0
  179. package/src/feedback/Skeleton.tsx +66 -0
  180. package/src/feedback/Spinner.tsx +65 -0
  181. package/src/feedback/index.ts +5 -0
  182. package/src/feedback/tokens.css +4 -0
  183. package/src/forms/Checkbox.tsx +69 -0
  184. package/src/forms/CheckboxGroup.tsx +29 -0
  185. package/src/forms/Combobox.tsx +368 -0
  186. package/src/forms/DatePicker.tsx +480 -0
  187. package/src/forms/FileUpload.tsx +222 -0
  188. package/src/forms/FormField.tsx +115 -0
  189. package/src/forms/Input.tsx +151 -0
  190. package/src/forms/MaskedInput.tsx +97 -0
  191. package/src/forms/NumberInput.tsx +248 -0
  192. package/src/forms/PasswordInput.tsx +35 -0
  193. package/src/forms/PinInput.tsx +125 -0
  194. package/src/forms/Radio.tsx +142 -0
  195. package/src/forms/RangeSlider.tsx +111 -0
  196. package/src/forms/SearchInput.tsx +34 -0
  197. package/src/forms/Select.tsx +103 -0
  198. package/src/forms/Slider.tsx +85 -0
  199. package/src/forms/Switch.tsx +95 -0
  200. package/src/forms/Textarea.tsx +114 -0
  201. package/src/forms/index.ts +18 -0
  202. package/src/forms/tokens.css +6 -0
  203. package/src/glass/GlassBadge.tsx +61 -0
  204. package/src/glass/GlassButton.tsx +59 -0
  205. package/src/glass/GlassCard.tsx +71 -0
  206. package/src/glass/index.ts +3 -0
  207. package/src/glass/tokens.css +81 -0
  208. package/src/index.ts +26 -0
  209. package/src/layout/AspectRatio.tsx +39 -0
  210. package/src/layout/Container.tsx +53 -0
  211. package/src/layout/Divider.tsx +61 -0
  212. package/src/layout/Flex.tsx +79 -0
  213. package/src/layout/Grid.tsx +124 -0
  214. package/src/layout/PageLayout.tsx +59 -0
  215. package/src/layout/ScrollArea.tsx +43 -0
  216. package/src/layout/Section.tsx +47 -0
  217. package/src/layout/Spacer.tsx +30 -0
  218. package/src/layout/Stack.tsx +88 -0
  219. package/src/layout/index.ts +10 -0
  220. package/src/layout/tokens.css +8 -0
  221. package/src/navigation/Breadcrumb.tsx +73 -0
  222. package/src/navigation/CommandPalette.tsx +142 -0
  223. package/src/navigation/DropdownMenu.tsx +160 -0
  224. package/src/navigation/Pagination.tsx +114 -0
  225. package/src/navigation/Stepper.tsx +93 -0
  226. package/src/navigation/Tabs.tsx +133 -0
  227. package/src/navigation/index.ts +6 -0
  228. package/src/navigation/tokens.css +4 -0
  229. package/src/overlays/AlertDialog.tsx +54 -0
  230. package/src/overlays/BottomSheet.tsx +123 -0
  231. package/src/overlays/ContextMenu.tsx +119 -0
  232. package/src/overlays/Drawer.tsx +145 -0
  233. package/src/overlays/FocusTrap.tsx +49 -0
  234. package/src/overlays/HoverCard.tsx +72 -0
  235. package/src/overlays/Modal.tsx +219 -0
  236. package/src/overlays/Popover.tsx +126 -0
  237. package/src/overlays/Portal.tsx +23 -0
  238. package/src/overlays/Toast.tsx +137 -0
  239. package/src/overlays/Tooltip.tsx +100 -0
  240. package/src/overlays/index.ts +11 -0
  241. package/src/overlays/tokens.css +8 -0
  242. package/src/styles/tokens.css +2 -0
  243. package/src/typography/Blockquote.tsx +32 -0
  244. package/src/typography/Code.tsx +89 -0
  245. package/src/typography/Heading.tsx +56 -0
  246. package/src/typography/Kbd.tsx +25 -0
  247. package/src/typography/Label.tsx +47 -0
  248. package/src/typography/Link.tsx +56 -0
  249. package/src/typography/Text.tsx +62 -0
  250. package/src/typography/index.ts +7 -0
  251. package/src/typography/tokens.css +5 -0
  252. package/src/utils/cn.ts +6 -0
  253. package/ui-contract.json +62 -0
@@ -0,0 +1,88 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../utils/cn';
4
+
5
+ export const stackVariants = cva('flex', {
6
+ variants: {
7
+ direction: {
8
+ row: 'flex-row',
9
+ 'row-reverse': 'flex-row-reverse',
10
+ column: 'flex-col',
11
+ 'column-reverse': 'flex-col-reverse',
12
+ },
13
+ gap: {
14
+ 0: 'gap-0',
15
+ 1: 'gap-1',
16
+ 2: 'gap-2',
17
+ 3: 'gap-3',
18
+ 4: 'gap-4',
19
+ 5: 'gap-5',
20
+ 6: 'gap-6',
21
+ 8: 'gap-8',
22
+ 10: 'gap-10',
23
+ 12: 'gap-12',
24
+ },
25
+ align: {
26
+ start: 'items-start',
27
+ center: 'items-center',
28
+ end: 'items-end',
29
+ stretch: 'items-stretch',
30
+ baseline: 'items-baseline',
31
+ },
32
+ justify: {
33
+ start: 'justify-start',
34
+ center: 'justify-center',
35
+ end: 'justify-end',
36
+ between: 'justify-between',
37
+ around: 'justify-around',
38
+ evenly: 'justify-evenly',
39
+ },
40
+ wrap: {
41
+ nowrap: 'flex-nowrap',
42
+ wrap: 'flex-wrap',
43
+ 'wrap-reverse': 'flex-wrap-reverse',
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ direction: 'column',
48
+ gap: 4,
49
+ align: 'stretch',
50
+ justify: 'start',
51
+ wrap: 'nowrap',
52
+ },
53
+ });
54
+
55
+ export interface StackProps
56
+ extends React.HTMLAttributes<HTMLDivElement>,
57
+ VariantProps<typeof stackVariants> {
58
+ as?: React.ElementType;
59
+ }
60
+
61
+ export const Stack = forwardRef<HTMLDivElement, StackProps>(
62
+ ({ className, direction, gap, align, justify, wrap, as: Component = 'div', children, ...props }, ref) => {
63
+ return (
64
+ <Component
65
+ ref={ref}
66
+ className={cn(stackVariants({ direction, gap, align, justify, wrap }), className)}
67
+ {...props}
68
+ >
69
+ {children}
70
+ </Component>
71
+ );
72
+ }
73
+ );
74
+ Stack.displayName = 'Stack';
75
+
76
+ export const HStack = forwardRef<HTMLDivElement, Omit<StackProps, 'direction'>>(
77
+ ({ align = 'center', ...props }, ref) => {
78
+ return <Stack ref={ref} direction="row" align={align} {...props} />;
79
+ }
80
+ );
81
+ HStack.displayName = 'HStack';
82
+
83
+ export const VStack = forwardRef<HTMLDivElement, Omit<StackProps, 'direction'>>(
84
+ ({ align = 'stretch', ...props }, ref) => {
85
+ return <Stack ref={ref} direction="column" align={align} {...props} />;
86
+ }
87
+ );
88
+ VStack.displayName = 'VStack';
@@ -0,0 +1,10 @@
1
+ export * from './Container';
2
+ export * from './Section';
3
+ export * from './Stack';
4
+ export * from './Grid';
5
+ export * from './Flex';
6
+ export * from './Divider';
7
+ export * from './Spacer';
8
+ export * from './AspectRatio';
9
+ export * from './PageLayout';
10
+ export * from './ScrollArea';
@@ -0,0 +1,8 @@
1
+ /* Layout Tokens */
2
+ :root {
3
+ --ui-container-sm: 640px;
4
+ --ui-container-md: 768px;
5
+ --ui-container-lg: 1024px;
6
+ --ui-container-xl: 1280px;
7
+ --ui-container-2xl: 1536px;
8
+ }
@@ -0,0 +1,73 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { ChevronRight } from 'lucide-react';
3
+ import { cn } from '../utils/cn';
4
+
5
+ export const Breadcrumb = forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
6
+ ({ className, children, ...props }, ref) => {
7
+ return (
8
+ <nav ref={ref} aria-label="Breadcrumb" className={cn('flex items-center text-sm', className)} {...props}>
9
+ <ol className="flex items-center gap-1.5 sm:gap-2">{children}</ol>
10
+ </nav>
11
+ );
12
+ }
13
+ );
14
+ Breadcrumb.displayName = 'Breadcrumb';
15
+
16
+ export const BreadcrumbItem = forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
17
+ ({ className, children, ...props }, ref) => {
18
+ return (
19
+ <li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props}>
20
+ {children}
21
+ </li>
22
+ );
23
+ }
24
+ );
25
+ BreadcrumbItem.displayName = 'BreadcrumbItem';
26
+
27
+ export interface BreadcrumbLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
28
+ as?: React.ElementType;
29
+ }
30
+
31
+ export const BreadcrumbLink = forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
32
+ ({ className, as: Component = 'a', children, ...props }, ref) => {
33
+ return (
34
+ <Component
35
+ ref={ref}
36
+ className={cn('text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] transition-colors font-medium', className)}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </Component>
41
+ );
42
+ }
43
+ );
44
+ BreadcrumbLink.displayName = 'BreadcrumbLink';
45
+
46
+ export const BreadcrumbPage = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
47
+ ({ className, children, ...props }, ref) => {
48
+ return (
49
+ <span
50
+ ref={ref}
51
+ role="link"
52
+ aria-disabled="true"
53
+ aria-current="page"
54
+ className={cn('font-semibold text-[var(--ui-text-primary)]', className)}
55
+ {...props}
56
+ >
57
+ {children}
58
+ </span>
59
+ );
60
+ }
61
+ );
62
+ BreadcrumbPage.displayName = 'BreadcrumbPage';
63
+
64
+ export const BreadcrumbSeparator = forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
65
+ ({ className, children, ...props }, ref) => {
66
+ return (
67
+ <li ref={ref} role="presentation" aria-hidden="true" className={cn('text-[var(--ui-text-muted)] opacity-60', className)} {...props}>
68
+ {children || <ChevronRight className="w-3.5 h-3.5" />}
69
+ </li>
70
+ );
71
+ }
72
+ );
73
+ BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
@@ -0,0 +1,142 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Search } from 'lucide-react';
3
+ import { Portal } from '../overlays/Portal';
4
+ import { FocusTrap } from '../overlays/FocusTrap';
5
+ import { Kbd } from '../typography/Kbd';
6
+ import { cn } from '../utils/cn';
7
+
8
+ export interface CommandItem {
9
+ id: string;
10
+ title: string;
11
+ category?: string;
12
+ icon?: React.ReactNode;
13
+ shortcut?: string;
14
+ onSelect: () => void;
15
+ }
16
+
17
+ export interface CommandPaletteProps {
18
+ isOpen: boolean;
19
+ onClose: () => void;
20
+ items: CommandItem[];
21
+ placeholder?: string;
22
+ }
23
+
24
+ export function CommandPalette({
25
+ isOpen,
26
+ onClose,
27
+ items,
28
+ placeholder = 'Type a command or search...',
29
+ }: CommandPaletteProps) {
30
+ const [query, setQuery] = useState('');
31
+ const [selectedIndex, setSelectedIndex] = useState(0);
32
+
33
+ const filteredItems = items.filter((item) =>
34
+ item.title.toLowerCase().includes(query.toLowerCase()) ||
35
+ item.category?.toLowerCase().includes(query.toLowerCase())
36
+ );
37
+
38
+ useEffect(() => {
39
+ setSelectedIndex(0);
40
+ }, [query]);
41
+
42
+ useEffect(() => {
43
+ if (!isOpen) {
44
+ setQuery('');
45
+ setSelectedIndex(0);
46
+ return;
47
+ }
48
+
49
+ const handleKeyDown = (e: KeyboardEvent) => {
50
+ if (e.key === 'Escape') {
51
+ onClose();
52
+ } else if (e.key === 'ArrowDown') {
53
+ e.preventDefault();
54
+ setSelectedIndex((prev) => (prev < filteredItems.length - 1 ? prev + 1 : 0));
55
+ } else if (e.key === 'ArrowUp') {
56
+ e.preventDefault();
57
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : filteredItems.length - 1));
58
+ } else if (e.key === 'Enter') {
59
+ e.preventDefault();
60
+ if (filteredItems[selectedIndex]) {
61
+ filteredItems[selectedIndex].onSelect();
62
+ onClose();
63
+ }
64
+ }
65
+ };
66
+
67
+ window.addEventListener('keydown', handleKeyDown);
68
+ return () => window.removeEventListener('keydown', handleKeyDown);
69
+ }, [isOpen, filteredItems, selectedIndex, onClose]);
70
+
71
+ if (!isOpen) return null;
72
+
73
+ return (
74
+ <Portal>
75
+ <FocusTrap active={isOpen}>
76
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-20 p-4 sm:p-6">
77
+ {/* Backdrop */}
78
+ <div
79
+ aria-hidden="true"
80
+ onClick={onClose}
81
+ className="fixed inset-0 bg-[var(--ui-bg-overlay)] backdrop-blur-sm transition-opacity animate-in fade-in duration-200"
82
+ />
83
+
84
+ {/* Palette Box */}
85
+ <div className="relative w-full max-w-xl rounded-[var(--ui-radius-xl)] bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] shadow-2xl overflow-hidden z-10 animate-in zoom-in-95 duration-150">
86
+ {/* Search Bar */}
87
+ <div className="flex items-center px-4 py-3 border-b border-[var(--ui-border-subtle)] gap-3">
88
+ <Search className="w-5 h-5 text-[var(--ui-text-muted)]" />
89
+ <input
90
+ type="text"
91
+ autoFocus
92
+ placeholder={placeholder}
93
+ value={query}
94
+ onChange={(e) => setQuery(e.target.value)}
95
+ className="flex-1 bg-transparent text-sm text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] focus:outline-none"
96
+ />
97
+ <Kbd>ESC</Kbd>
98
+ </div>
99
+
100
+ {/* List items */}
101
+ <div className="max-h-80 overflow-y-auto p-2">
102
+ {filteredItems.length === 0 ? (
103
+ <div className="py-8 text-center text-xs text-[var(--ui-text-muted)]">
104
+ No commands found matching "{query}".
105
+ </div>
106
+ ) : (
107
+ filteredItems.map((item, idx) => {
108
+ const isSelected = idx === selectedIndex;
109
+ return (
110
+ <div
111
+ key={item.id}
112
+ onClick={() => {
113
+ item.onSelect();
114
+ onClose();
115
+ }}
116
+ onMouseEnter={() => setSelectedIndex(idx)}
117
+ className={cn(
118
+ 'flex items-center gap-3 px-3 py-2.5 rounded-[var(--ui-radius-md)] cursor-pointer text-xs font-medium transition-colors',
119
+ isSelected
120
+ ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-fg)]'
121
+ : 'text-[var(--ui-text-primary)] hover:bg-[var(--ui-bg-tertiary)]'
122
+ )}
123
+ >
124
+ {item.icon && <span className="flex-shrink-0">{item.icon}</span>}
125
+ <span className="flex-1 truncate">{item.title}</span>
126
+ {item.category && (
127
+ <span className={cn('text-[10px] uppercase font-semibold tracking-wider opacity-70', isSelected ? 'text-white' : 'text-[var(--ui-text-muted)]')}>
128
+ {item.category}
129
+ </span>
130
+ )}
131
+ {item.shortcut && <Kbd>{item.shortcut}</Kbd>}
132
+ </div>
133
+ );
134
+ })
135
+ )}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </FocusTrap>
140
+ </Portal>
141
+ );
142
+ }
@@ -0,0 +1,160 @@
1
+ import React, { createContext, useContext, useState, useRef, useEffect, forwardRef } from 'react';
2
+ import { Portal } from '../overlays/Portal';
3
+ import { cn } from '../utils/cn';
4
+
5
+ interface DropdownContextType {
6
+ isOpen: boolean;
7
+ setIsOpen: (val: boolean) => void;
8
+ triggerRect: DOMRect | null;
9
+ setTriggerRect: (rect: DOMRect | null) => void;
10
+ }
11
+
12
+ const DropdownContext = createContext<DropdownContextType | null>(null);
13
+
14
+ export function useDropdown() {
15
+ const context = useContext(DropdownContext);
16
+ if (!context) throw new Error('DropdownMenu components must be used within <DropdownMenu />');
17
+ return context;
18
+ }
19
+
20
+ export interface DropdownMenuProps {
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ export function DropdownMenu({ children }: DropdownMenuProps) {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const [triggerRect, setTriggerRect] = useState<DOMRect | null>(null);
27
+
28
+ return (
29
+ <DropdownContext.Provider value={{ isOpen, setIsOpen, triggerRect, setTriggerRect }}>
30
+ <div className="relative inline-block text-left">{children}</div>
31
+ </DropdownContext.Provider>
32
+ );
33
+ }
34
+
35
+ export const DropdownMenuTrigger = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
36
+ ({ children, onClick, ...props }, ref) => {
37
+ const { isOpen, setIsOpen, setTriggerRect } = useDropdown();
38
+ const triggerRef = useRef<HTMLDivElement>(null);
39
+
40
+ const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
41
+ if (triggerRef.current) {
42
+ setTriggerRect(triggerRef.current.getBoundingClientRect());
43
+ }
44
+ setIsOpen(!isOpen);
45
+ onClick?.(e);
46
+ };
47
+
48
+ return (
49
+ <div ref={triggerRef} onClick={handleClick} className="inline-flex cursor-pointer" {...props}>
50
+ {children}
51
+ </div>
52
+ );
53
+ }
54
+ );
55
+ DropdownMenuTrigger.displayName = 'DropdownMenuTrigger';
56
+
57
+ export interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
58
+ align?: 'start' | 'end';
59
+ }
60
+
61
+ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
62
+ ({ className, align = 'start', children, ...props }, ref) => {
63
+ const { isOpen, setIsOpen, triggerRect } = useDropdown();
64
+ const menuRef = useRef<HTMLDivElement>(null);
65
+
66
+ useEffect(() => {
67
+ const handleOutside = (e: MouseEvent) => {
68
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
69
+ setIsOpen(false);
70
+ }
71
+ };
72
+ if (isOpen) {
73
+ document.addEventListener('mousedown', handleOutside);
74
+ }
75
+ return () => document.removeEventListener('mousedown', handleOutside);
76
+ }, [isOpen, setIsOpen]);
77
+
78
+ if (!isOpen || !triggerRect) return null;
79
+
80
+ const top = triggerRect.bottom + window.scrollY + 6;
81
+ const left =
82
+ align === 'end'
83
+ ? triggerRect.right + window.scrollX
84
+ : triggerRect.left + window.scrollX;
85
+
86
+ return (
87
+ <Portal>
88
+ <div
89
+ ref={menuRef}
90
+ style={{
91
+ position: 'absolute',
92
+ top: `${top}px`,
93
+ left: `${left}px`,
94
+ }}
95
+ className={cn(
96
+ 'z-50 min-w-[12rem] overflow-hidden rounded-[var(--ui-radius-lg)] border border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)] p-1 text-[var(--ui-text-primary)] shadow-xl animate-in fade-in zoom-in-95',
97
+ align === 'end' && '-translate-x-full',
98
+ className
99
+ )}
100
+ {...props}
101
+ >
102
+ {children}
103
+ </div>
104
+ </Portal>
105
+ );
106
+ }
107
+ );
108
+ DropdownMenuContent.displayName = 'DropdownMenuContent';
109
+
110
+ export interface DropdownMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
111
+ icon?: React.ReactNode;
112
+ shortcut?: string;
113
+ destructive?: boolean;
114
+ }
115
+
116
+ export const DropdownMenuItem = forwardRef<HTMLButtonElement, DropdownMenuItemProps>(
117
+ ({ className, icon, shortcut, destructive = false, children, onClick, ...props }, ref) => {
118
+ const { setIsOpen } = useDropdown();
119
+
120
+ return (
121
+ <button
122
+ ref={ref}
123
+ type="button"
124
+ onClick={(e) => {
125
+ onClick?.(e);
126
+ setIsOpen(false);
127
+ }}
128
+ className={cn(
129
+ 'relative flex w-full cursor-pointer select-none items-center gap-2 rounded-[var(--ui-radius-sm)] px-2.5 py-1.5 text-xs font-medium outline-none transition-colors hover:bg-[var(--ui-bg-tertiary)] disabled:pointer-events-none disabled:opacity-50 text-left',
130
+ destructive && 'text-[var(--ui-error)] hover:bg-[var(--ui-error-subtle)]',
131
+ className
132
+ )}
133
+ {...props}
134
+ >
135
+ {icon && <span className="flex-shrink-0 text-current">{icon}</span>}
136
+ <span className="flex-1 truncate">{children}</span>
137
+ {shortcut && <span className="ml-auto text-[10px] text-[var(--ui-text-muted)] tracking-widest">{shortcut}</span>}
138
+ </button>
139
+ );
140
+ }
141
+ );
142
+ DropdownMenuItem.displayName = 'DropdownMenuItem';
143
+
144
+ export const DropdownMenuSeparator = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
145
+ ({ className, ...props }, ref) => {
146
+ return <div ref={ref} className={cn('-mx-1 my-1 h-px bg-[var(--ui-border-subtle)]', className)} {...props} />;
147
+ }
148
+ );
149
+ DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
150
+
151
+ export const DropdownMenuLabel = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
152
+ ({ className, children, ...props }, ref) => {
153
+ return (
154
+ <div ref={ref} className={cn('px-2.5 py-1.5 text-[11px] font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider', className)} {...props}>
155
+ {children}
156
+ </div>
157
+ );
158
+ }
159
+ );
160
+ DropdownMenuLabel.displayName = 'DropdownMenuLabel';
@@ -0,0 +1,114 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
3
+ import { Button } from '../buttons/Button';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
7
+ currentPage: number;
8
+ totalPages: number;
9
+ onPageChange: (page: number) => void;
10
+ siblingCount?: number;
11
+ }
12
+
13
+ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
14
+ ({ className, currentPage, totalPages, onPageChange, siblingCount = 1, ...props }, ref) => {
15
+ const generatePages = () => {
16
+ const totalPageNumbers = siblingCount * 2 + 5;
17
+
18
+ if (totalPages <= totalPageNumbers) {
19
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
20
+ }
21
+
22
+ const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
23
+ const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages);
24
+
25
+ const shouldShowLeftDots = leftSiblingIndex > 2;
26
+ const shouldShowRightDots = rightSiblingIndex < totalPages - 2;
27
+
28
+ if (!shouldShowLeftDots && shouldShowRightDots) {
29
+ const leftItemCount = 3 + 2 * siblingCount;
30
+ const leftRange = Array.from({ length: leftItemCount }, (_, i) => i + 1);
31
+ return [...leftRange, 'DOTS', totalPages];
32
+ }
33
+
34
+ if (shouldShowLeftDots && !shouldShowRightDots) {
35
+ const rightItemCount = 3 + 2 * siblingCount;
36
+ const rightRange = Array.from(
37
+ { length: rightItemCount },
38
+ (_, i) => totalPages - rightItemCount + i + 1
39
+ );
40
+ return [1, 'DOTS', ...rightRange];
41
+ }
42
+
43
+ if (shouldShowLeftDots && shouldShowRightDots) {
44
+ const middleRange = Array.from(
45
+ { length: rightSiblingIndex - leftSiblingIndex + 1 },
46
+ (_, i) => leftSiblingIndex + i
47
+ );
48
+ return [1, 'DOTS', ...middleRange, 'DOTS', totalPages];
49
+ }
50
+
51
+ return [];
52
+ };
53
+
54
+ const pages = generatePages();
55
+
56
+ return (
57
+ <nav
58
+ ref={ref}
59
+ role="navigation"
60
+ aria-label="Pagination"
61
+ className={cn('flex items-center justify-center gap-1.5', className)}
62
+ {...props}
63
+ >
64
+ <Button
65
+ variant="outline"
66
+ size="sm"
67
+ disabled={currentPage <= 1}
68
+ onClick={() => onPageChange(currentPage - 1)}
69
+ aria-label="Go to previous page"
70
+ >
71
+ <ChevronLeft className="w-4 h-4" />
72
+ </Button>
73
+
74
+ {pages.map((p, index) => {
75
+ if (p === 'DOTS') {
76
+ return (
77
+ <span key={`dots-${index}`} className="flex items-center justify-center w-8 h-8 text-[var(--ui-text-muted)]">
78
+ <MoreHorizontal className="w-4 h-4" />
79
+ </span>
80
+ );
81
+ }
82
+
83
+ const pageNum = Number(p);
84
+ const isActive = pageNum === currentPage;
85
+
86
+ return (
87
+ <Button
88
+ key={pageNum}
89
+ variant={isActive ? 'primary' : 'ghost'}
90
+ size="sm"
91
+ aria-current={isActive ? 'page' : undefined}
92
+ onClick={() => onPageChange(pageNum)}
93
+ className="w-8 h-8 p-0 text-xs font-semibold"
94
+ >
95
+ {pageNum}
96
+ </Button>
97
+ );
98
+ })}
99
+
100
+ <Button
101
+ variant="outline"
102
+ size="sm"
103
+ disabled={currentPage >= totalPages}
104
+ onClick={() => onPageChange(currentPage + 1)}
105
+ aria-label="Go to next page"
106
+ >
107
+ <ChevronRight className="w-4 h-4" />
108
+ </Button>
109
+ </nav>
110
+ );
111
+ }
112
+ );
113
+
114
+ Pagination.displayName = 'Pagination';
@@ -0,0 +1,93 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { Check } from 'lucide-react';
3
+ import { cn } from '../utils/cn';
4
+
5
+ export interface StepItem {
6
+ title: string;
7
+ description?: string;
8
+ }
9
+
10
+ export interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ steps: StepItem[];
12
+ currentStep: number;
13
+ orientation?: 'horizontal' | 'vertical';
14
+ }
15
+
16
+ export const Stepper = forwardRef<HTMLDivElement, StepperProps>(
17
+ ({ className, steps, currentStep, orientation = 'horizontal', ...props }, ref) => {
18
+ return (
19
+ <div
20
+ ref={ref}
21
+ role="navigation"
22
+ aria-label="Progress Stepper"
23
+ className={cn(
24
+ 'flex',
25
+ orientation === 'horizontal'
26
+ ? 'flex-row items-center w-full justify-between'
27
+ : 'flex-col space-y-6',
28
+ className
29
+ )}
30
+ {...props}
31
+ >
32
+ {steps.map((step, index) => {
33
+ const isCompleted = index < currentStep;
34
+ const isActive = index === currentStep;
35
+ const isPending = index > currentStep;
36
+
37
+ return (
38
+ <div
39
+ key={index}
40
+ className={cn(
41
+ 'flex items-center',
42
+ orientation === 'horizontal' ? 'flex-1 last:flex-none' : 'flex-row items-start'
43
+ )}
44
+ >
45
+ <div className="flex items-center gap-3">
46
+ {/* Step indicator circle */}
47
+ <div
48
+ className={cn(
49
+ 'flex items-center justify-center w-8 h-8 rounded-full text-xs font-bold transition-colors flex-shrink-0',
50
+ isCompleted && 'bg-[var(--ui-primary)] text-[var(--ui-primary-fg)]',
51
+ isActive && 'border-2 border-[var(--ui-primary)] text-[var(--ui-primary)] bg-[var(--ui-bg-surface)] ring-4 ring-[var(--ui-primary)]/20',
52
+ isPending && 'border border-[var(--ui-border-default)] text-[var(--ui-text-muted)] bg-[var(--ui-bg-tertiary)]'
53
+ )}
54
+ >
55
+ {isCompleted ? <Check className="w-4 h-4 stroke-[3]" /> : index + 1}
56
+ </div>
57
+
58
+ {/* Step details */}
59
+ <div className="flex flex-col">
60
+ <span
61
+ className={cn(
62
+ 'text-xs font-semibold',
63
+ isActive ? 'text-[var(--ui-text-primary)]' : 'text-[var(--ui-text-muted)]'
64
+ )}
65
+ >
66
+ {step.title}
67
+ </span>
68
+ {step.description && (
69
+ <span className="text-[11px] text-[var(--ui-text-muted)] opacity-80">
70
+ {step.description}
71
+ </span>
72
+ )}
73
+ </div>
74
+ </div>
75
+
76
+ {/* Connecting line */}
77
+ {orientation === 'horizontal' && index < steps.length - 1 && (
78
+ <div
79
+ className={cn(
80
+ 'flex-1 h-0.5 mx-4 transition-colors',
81
+ isCompleted ? 'bg-[var(--ui-primary)]' : 'bg-[var(--ui-border-subtle)]'
82
+ )}
83
+ />
84
+ )}
85
+ </div>
86
+ );
87
+ })}
88
+ </div>
89
+ );
90
+ }
91
+ );
92
+
93
+ Stepper.displayName = 'Stepper';