@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,133 @@
1
+ import React, { createContext, useContext, useState, forwardRef } from 'react';
2
+ import { cn } from '../utils/cn';
3
+
4
+ interface TabsContextType {
5
+ activeTab: string;
6
+ setActiveTab: (tab: string) => void;
7
+ variant?: 'pills' | 'line' | 'enclosed';
8
+ }
9
+
10
+ const TabsContext = createContext<TabsContextType | null>(null);
11
+
12
+ export function useTabs() {
13
+ const context = useContext(TabsContext);
14
+ if (!context) throw new Error('Tabs compound components must be used within <Tabs />');
15
+ return context;
16
+ }
17
+
18
+ export interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
19
+ defaultValue?: string;
20
+ value?: string;
21
+ onValueChange?: (value: string) => void;
22
+ variant?: 'pills' | 'line' | 'enclosed';
23
+ }
24
+
25
+ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
26
+ ({ defaultValue = '', value: controlledValue, onValueChange, variant = 'pills', className, children, ...props }, ref) => {
27
+ const [selectedTab, setSelectedTab] = useState(defaultValue);
28
+ const activeTab = controlledValue !== undefined ? controlledValue : selectedTab;
29
+
30
+ const handleTabChange = (val: string) => {
31
+ setSelectedTab(val);
32
+ onValueChange?.(val);
33
+ };
34
+
35
+ return (
36
+ <TabsContext.Provider value={{ activeTab, setActiveTab: handleTabChange, variant }}>
37
+ <div ref={ref} className={cn('w-full flex flex-col space-y-4', className)} {...props}>
38
+ {children}
39
+ </div>
40
+ </TabsContext.Provider>
41
+ );
42
+ }
43
+ );
44
+ Tabs.displayName = 'Tabs';
45
+
46
+ export const TabsList = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
47
+ ({ className, children, ...props }, ref) => {
48
+ const { variant } = useTabs();
49
+
50
+ return (
51
+ <div
52
+ ref={ref}
53
+ role="tablist"
54
+ className={cn(
55
+ 'inline-flex items-center',
56
+ variant === 'pills' && 'p-1 rounded-[var(--ui-radius-lg)] bg-[var(--ui-bg-secondary)] border border-[var(--ui-border-subtle)] gap-1',
57
+ variant === 'line' && 'border-b border-[var(--ui-border-default)] gap-6',
58
+ variant === 'enclosed' && 'border-b border-[var(--ui-border-default)] gap-1',
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ {children}
64
+ </div>
65
+ );
66
+ }
67
+ );
68
+ TabsList.displayName = 'TabsList';
69
+
70
+ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
71
+ value: string;
72
+ }
73
+
74
+ export const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
75
+ ({ className, value, children, ...props }, ref) => {
76
+ const { activeTab, setActiveTab, variant } = useTabs();
77
+ const isActive = activeTab === value;
78
+
79
+ return (
80
+ <button
81
+ ref={ref}
82
+ role="tab"
83
+ type="button"
84
+ aria-selected={isActive}
85
+ onClick={() => setActiveTab(value)}
86
+ className={cn(
87
+ 'inline-flex items-center justify-center font-medium text-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary)] disabled:pointer-events-none disabled:opacity-50 select-none cursor-pointer',
88
+ variant === 'pills' &&
89
+ 'px-3.5 py-1.5 rounded-[var(--ui-radius-md)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)]',
90
+ variant === 'pills' && isActive &&
91
+ 'bg-[var(--ui-bg-surface)] text-[var(--ui-text-primary)] font-semibold shadow-sm',
92
+ variant === 'line' &&
93
+ 'pb-3 pt-2 text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] border-b-2 border-transparent -mb-[1px]',
94
+ variant === 'line' && isActive &&
95
+ 'border-[var(--ui-primary)] text-[var(--ui-primary)] font-semibold',
96
+ variant === 'enclosed' &&
97
+ 'px-4 py-2 rounded-t-[var(--ui-radius-md)] border border-transparent text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] -mb-[1px]',
98
+ variant === 'enclosed' && isActive &&
99
+ 'bg-[var(--ui-bg-surface)] border-[var(--ui-border-default)] border-b-transparent text-[var(--ui-text-primary)] font-semibold',
100
+ className
101
+ )}
102
+ {...props}
103
+ >
104
+ {children}
105
+ </button>
106
+ );
107
+ }
108
+ );
109
+ TabsTrigger.displayName = 'TabsTrigger';
110
+
111
+ export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
112
+ value: string;
113
+ }
114
+
115
+ export const TabsContent = forwardRef<HTMLDivElement, TabsContentProps>(
116
+ ({ className, value, children, ...props }, ref) => {
117
+ const { activeTab } = useTabs();
118
+ if (activeTab !== value) return null;
119
+
120
+ return (
121
+ <div
122
+ ref={ref}
123
+ role="tabpanel"
124
+ tabIndex={0}
125
+ className={cn('focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary)] animate-in fade-in duration-150', className)}
126
+ {...props}
127
+ >
128
+ {children}
129
+ </div>
130
+ );
131
+ }
132
+ );
133
+ TabsContent.displayName = 'TabsContent';
@@ -0,0 +1,6 @@
1
+ export * from './Tabs';
2
+ export * from './Breadcrumb';
3
+ export * from './Pagination';
4
+ export * from './Stepper';
5
+ export * from './DropdownMenu';
6
+ export * from './CommandPalette';
@@ -0,0 +1,4 @@
1
+ /* Navigation Tokens */
2
+ :root {
3
+ --ui-nav-height: 4rem;
4
+ }
@@ -0,0 +1,54 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalFooter, ModalCloseButton } from './Modal';
3
+ import { Button } from '../buttons/Button';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export interface AlertDialogProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onConfirm: () => void;
10
+ title: string;
11
+ description: string;
12
+ confirmText?: string;
13
+ cancelText?: string;
14
+ variant?: 'danger' | 'primary';
15
+ isLoading?: boolean;
16
+ }
17
+
18
+ export function AlertDialog({
19
+ isOpen,
20
+ onClose,
21
+ onConfirm,
22
+ title,
23
+ description,
24
+ confirmText = 'Confirm',
25
+ cancelText = 'Cancel',
26
+ variant = 'danger',
27
+ isLoading = false,
28
+ }: AlertDialogProps) {
29
+ return (
30
+ <Modal isOpen={isOpen} onClose={onClose}>
31
+ <ModalOverlay />
32
+ <ModalContent size="sm">
33
+ <ModalCloseButton />
34
+ <ModalHeader>
35
+ <ModalTitle>{title}</ModalTitle>
36
+ <ModalDescription>{description}</ModalDescription>
37
+ </ModalHeader>
38
+ <ModalFooter>
39
+ <Button variant="outline" size="sm" onClick={onClose} disabled={isLoading}>
40
+ {cancelText}
41
+ </Button>
42
+ <Button
43
+ variant={variant === 'danger' ? 'danger' : 'primary'}
44
+ size="sm"
45
+ loading={isLoading}
46
+ onClick={onConfirm}
47
+ >
48
+ {confirmText}
49
+ </Button>
50
+ </ModalFooter>
51
+ </ModalContent>
52
+ </Modal>
53
+ );
54
+ }
@@ -0,0 +1,123 @@
1
+ import React, { forwardRef, useEffect, useRef } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import { cn } from '../utils/cn';
4
+ import { Portal } from './Portal';
5
+ import { FocusTrap } from './FocusTrap';
6
+
7
+ export interface BottomSheetProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ children: React.ReactNode;
11
+ title?: string;
12
+ description?: string;
13
+ className?: string;
14
+ showHandle?: boolean;
15
+ maxHeight?: string;
16
+ }
17
+
18
+ export const BottomSheet = forwardRef<HTMLDivElement, BottomSheetProps>(
19
+ (
20
+ {
21
+ isOpen,
22
+ onClose,
23
+ children,
24
+ title,
25
+ description,
26
+ className,
27
+ showHandle = true,
28
+ maxHeight = 'max-h-[85vh]',
29
+ },
30
+ ref
31
+ ) => {
32
+ const sheetRef = useRef<HTMLDivElement | null>(null);
33
+
34
+ // Escape key listener
35
+ useEffect(() => {
36
+ const handleKeyDown = (e: KeyboardEvent) => {
37
+ if (e.key === 'Escape' && isOpen) {
38
+ onClose();
39
+ }
40
+ };
41
+ window.addEventListener('keydown', handleKeyDown);
42
+ return () => window.removeEventListener('keydown', handleKeyDown);
43
+ }, [isOpen, onClose]);
44
+
45
+ if (!isOpen) return null;
46
+
47
+ return (
48
+ <Portal>
49
+ <FocusTrap active={isOpen}>
50
+ <div className="fixed inset-0 z-50 flex flex-col justify-end">
51
+ {/* Backdrop */}
52
+ <div
53
+ onClick={onClose}
54
+ className="fixed inset-0 bg-black/60 backdrop-blur-md transition-opacity animate-in fade-in duration-300"
55
+ aria-hidden="true"
56
+ />
57
+
58
+ {/* Slide-up Sheet Panel */}
59
+ <div
60
+ ref={ref}
61
+ className={cn(
62
+ 'relative z-50 w-full max-w-2xl mx-auto rounded-t-[2rem] border-t border-x border-[var(--ui-border-default)] bg-[var(--ui-bg-surface)] backdrop-blur-3xl shadow-2xl overflow-hidden flex flex-col transition-transform animate-in slide-in-from-bottom duration-300',
63
+ maxHeight,
64
+ className
65
+ )}
66
+ >
67
+ {/* Drag Handle */}
68
+ {showHandle && (
69
+ <div
70
+ onClick={onClose}
71
+ className="w-full py-3 flex items-center justify-center cursor-grab active:cursor-grabbing"
72
+ >
73
+ <div className="w-12 h-1.5 rounded-full bg-[var(--ui-text-muted)]/40 hover:bg-[var(--ui-text-muted)] transition-colors" />
74
+ </div>
75
+ )}
76
+
77
+ {/* Header */}
78
+ {(title || description) && (
79
+ <div className="px-6 pb-3 flex items-center justify-between border-b border-[var(--ui-border-subtle)]">
80
+ <div>
81
+ {title && <h3 className="text-base font-bold tracking-tight text-[var(--ui-text-primary)]">{title}</h3>}
82
+ {description && <p className="text-xs text-[var(--ui-text-muted)]">{description}</p>}
83
+ </div>
84
+ <button
85
+ type="button"
86
+ onClick={onClose}
87
+ className="p-1.5 rounded-full hover:bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] transition-colors"
88
+ >
89
+ <X className="w-4 h-4" />
90
+ </button>
91
+ </div>
92
+ )}
93
+
94
+ {/* Content Body */}
95
+ <div className="p-6 overflow-y-auto flex-1 space-y-4">
96
+ {children}
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </FocusTrap>
101
+ </Portal>
102
+ );
103
+ }
104
+ );
105
+ BottomSheet.displayName = 'BottomSheet';
106
+
107
+ export const BottomSheetHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, children, ...props }) => (
108
+ <div className={cn('px-6 pb-2', className)} {...props}>
109
+ {children}
110
+ </div>
111
+ );
112
+
113
+ export const BottomSheetBody: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, children, ...props }) => (
114
+ <div className={cn('px-6 py-2 overflow-y-auto', className)} {...props}>
115
+ {children}
116
+ </div>
117
+ );
118
+
119
+ export const BottomSheetFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, children, ...props }) => (
120
+ <div className={cn('p-6 pt-3 border-t border-[var(--ui-border-subtle)] flex items-center justify-end gap-3', className)} {...props}>
121
+ {children}
122
+ </div>
123
+ );
@@ -0,0 +1,119 @@
1
+ import React, { useState, useEffect, useRef, forwardRef } from 'react';
2
+ import { cn } from '../utils/cn';
3
+ import { Portal } from './Portal';
4
+ import { Kbd } from '../typography/Kbd';
5
+
6
+ export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ icon?: React.ReactNode;
8
+ shortcut?: string;
9
+ disabled?: boolean;
10
+ destructive?: boolean;
11
+ onSelect?: () => void;
12
+ }
13
+
14
+ export const ContextMenuItem: React.FC<ContextMenuItemProps> = ({
15
+ icon,
16
+ shortcut,
17
+ disabled = false,
18
+ destructive = false,
19
+ onSelect,
20
+ children,
21
+ className,
22
+ ...props
23
+ }) => {
24
+ return (
25
+ <div
26
+ role="menuitem"
27
+ aria-disabled={disabled}
28
+ onClick={() => {
29
+ if (!disabled) onSelect?.();
30
+ }}
31
+ className={cn(
32
+ 'flex items-center justify-between px-3 py-1.5 rounded-xl text-xs font-medium cursor-pointer transition-colors select-none',
33
+ disabled && 'opacity-40 cursor-not-allowed pointer-events-none',
34
+ destructive
35
+ ? 'text-rose-500 hover:bg-rose-500/10'
36
+ : 'text-[var(--ui-text-primary)] hover:bg-[var(--ui-bg-tertiary)]',
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ <div className="flex items-center gap-2 truncate">
42
+ {icon && <span className="w-4 h-4 shrink-0">{icon}</span>}
43
+ <span className="truncate">{children}</span>
44
+ </div>
45
+
46
+ {shortcut && <Kbd className="text-[10px] py-0 px-1">{shortcut}</Kbd>}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export const ContextMenuSeparator: React.FC = () => (
52
+ <div className="h-px my-1 bg-[var(--ui-border-subtle)]" role="separator" />
53
+ );
54
+
55
+ export interface ContextMenuProps {
56
+ children: React.ReactNode;
57
+ menuContent: React.ReactNode;
58
+ className?: string;
59
+ }
60
+
61
+ export const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(
62
+ ({ children, menuContent, className }, ref) => {
63
+ const [isOpen, setIsOpen] = useState(false);
64
+ const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
65
+ const menuRef = useRef<HTMLDivElement | null>(null);
66
+
67
+ const handleContextMenu = (e: React.MouseEvent) => {
68
+ e.preventDefault();
69
+ const x = Math.min(window.innerWidth - 220, e.clientX);
70
+ const y = Math.min(window.innerHeight - 260, e.clientY);
71
+ setPosition({ x, y });
72
+ setIsOpen(true);
73
+ };
74
+
75
+ useEffect(() => {
76
+ const handleClickOutside = (e: MouseEvent) => {
77
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
78
+ setIsOpen(false);
79
+ }
80
+ };
81
+ const handleKeyDown = (e: KeyboardEvent) => {
82
+ if (e.key === 'Escape') setIsOpen(false);
83
+ };
84
+
85
+ if (isOpen) {
86
+ window.addEventListener('click', handleClickOutside);
87
+ window.addEventListener('contextmenu', handleClickOutside);
88
+ window.addEventListener('keydown', handleKeyDown);
89
+ }
90
+ return () => {
91
+ window.removeEventListener('click', handleClickOutside);
92
+ window.removeEventListener('contextmenu', handleClickOutside);
93
+ window.removeEventListener('keydown', handleKeyDown);
94
+ };
95
+ }, [isOpen]);
96
+
97
+ return (
98
+ <>
99
+ <div ref={ref} onContextMenu={handleContextMenu} className={className}>
100
+ {children}
101
+ </div>
102
+
103
+ {isOpen && (
104
+ <Portal>
105
+ <div
106
+ ref={menuRef}
107
+ style={{ top: `${position.y}px`, left: `${position.x}px` }}
108
+ onClick={() => setIsOpen(false)}
109
+ className="fixed z-50 min-w-48 p-1.5 rounded-2xl bg-[var(--ui-bg-surface)]/90 backdrop-blur-3xl border border-[var(--ui-border-default)] shadow-2xl space-y-0.5 animate-in fade-in zoom-in-95 duration-150"
110
+ >
111
+ {menuContent}
112
+ </div>
113
+ </Portal>
114
+ )}
115
+ </>
116
+ );
117
+ }
118
+ );
119
+ ContextMenu.displayName = 'ContextMenu';
@@ -0,0 +1,145 @@
1
+ import React, { forwardRef, createContext, useContext, useEffect } from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { X } from 'lucide-react';
4
+ import { Portal } from './Portal';
5
+ import { FocusTrap } from './FocusTrap';
6
+ import { cn } from '../utils/cn';
7
+
8
+ interface DrawerContextType {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ }
12
+
13
+ const DrawerContext = createContext<DrawerContextType | null>(null);
14
+
15
+ export function useDrawer() {
16
+ const context = useContext(DrawerContext);
17
+ if (!context) {
18
+ throw new Error('Drawer compound components must be used within a <Drawer />');
19
+ }
20
+ return context;
21
+ }
22
+
23
+ export const drawerVariants = cva(
24
+ 'fixed z-50 bg-[var(--ui-bg-surface)] text-[var(--ui-text-primary)] border-[var(--ui-border-default)] shadow-2xl transition-transform duration-300 ease-in-out flex flex-col',
25
+ {
26
+ variants: {
27
+ placement: {
28
+ right: 'inset-y-0 right-0 h-full w-full sm:max-w-md border-l animate-in slide-in-from-right',
29
+ left: 'inset-y-0 left-0 h-full w-full sm:max-w-md border-r animate-in slide-in-from-left',
30
+ top: 'inset-x-0 top-0 w-full max-h-[80vh] border-b animate-in slide-in-from-top',
31
+ bottom: 'inset-x-0 bottom-0 w-full max-h-[80vh] border-t rounded-t-[var(--ui-radius-xl)] animate-in slide-in-from-bottom',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ placement: 'right',
36
+ },
37
+ }
38
+ );
39
+
40
+ export interface DrawerProps extends VariantProps<typeof drawerVariants> {
41
+ isOpen: boolean;
42
+ onClose: () => void;
43
+ children: React.ReactNode;
44
+ closeOnEsc?: boolean;
45
+ }
46
+
47
+ export function Drawer({
48
+ isOpen,
49
+ onClose,
50
+ placement = 'right',
51
+ children,
52
+ closeOnEsc = true,
53
+ }: DrawerProps) {
54
+ useEffect(() => {
55
+ if (!isOpen) return;
56
+
57
+ const handleKeyDown = (e: KeyboardEvent) => {
58
+ if (closeOnEsc && e.key === 'Escape') {
59
+ onClose();
60
+ }
61
+ };
62
+
63
+ const originalOverflow = document.body.style.overflow;
64
+ document.body.style.overflow = 'hidden';
65
+
66
+ window.addEventListener('keydown', handleKeyDown);
67
+ return () => {
68
+ window.removeEventListener('keydown', handleKeyDown);
69
+ document.body.style.overflow = originalOverflow;
70
+ };
71
+ }, [isOpen, closeOnEsc, onClose]);
72
+
73
+ if (!isOpen) return null;
74
+
75
+ return (
76
+ <Portal>
77
+ <DrawerContext.Provider value={{ isOpen, onClose }}>
78
+ <FocusTrap active={isOpen}>
79
+ <div className="fixed inset-0 z-50 overflow-hidden">
80
+ {/* Backdrop */}
81
+ <div
82
+ aria-hidden="true"
83
+ onClick={onClose}
84
+ className="fixed inset-0 bg-[var(--ui-bg-overlay)] backdrop-blur-sm transition-opacity animate-in fade-in duration-200"
85
+ />
86
+ {/* Drawer Panel */}
87
+ <div className={cn(drawerVariants({ placement }))}>
88
+ {children}
89
+ </div>
90
+ </div>
91
+ </FocusTrap>
92
+ </DrawerContext.Provider>
93
+ </Portal>
94
+ );
95
+ }
96
+
97
+ export const DrawerHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
98
+ ({ className, children, ...props }, ref) => {
99
+ const { onClose } = useDrawer();
100
+ return (
101
+ <div
102
+ ref={ref}
103
+ className={cn('flex items-center justify-between p-5 border-b border-[var(--ui-border-subtle)]', className)}
104
+ {...props}
105
+ >
106
+ <div className="flex-1">{children}</div>
107
+ <button
108
+ type="button"
109
+ onClick={onClose}
110
+ aria-label="Close drawer"
111
+ className="p-1.5 rounded-full text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] hover:bg-[var(--ui-bg-tertiary)] transition-colors ml-4"
112
+ >
113
+ <X className="w-4 h-4" />
114
+ </button>
115
+ </div>
116
+ );
117
+ }
118
+ );
119
+ DrawerHeader.displayName = 'DrawerHeader';
120
+
121
+ export const DrawerBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
122
+ ({ className, children, ...props }, ref) => {
123
+ return (
124
+ <div ref={ref} className={cn('flex-1 p-5 overflow-y-auto', className)} {...props}>
125
+ {children}
126
+ </div>
127
+ );
128
+ }
129
+ );
130
+ DrawerBody.displayName = 'DrawerBody';
131
+
132
+ export const DrawerFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
133
+ ({ className, children, ...props }, ref) => {
134
+ return (
135
+ <div
136
+ ref={ref}
137
+ className={cn('p-4 border-t border-[var(--ui-border-subtle)] bg-[var(--ui-bg-secondary)] flex items-center justify-end gap-3', className)}
138
+ {...props}
139
+ >
140
+ {children}
141
+ </div>
142
+ );
143
+ }
144
+ );
145
+ DrawerFooter.displayName = 'DrawerFooter';
@@ -0,0 +1,49 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ export interface FocusTrapProps {
4
+ children: React.ReactNode;
5
+ active?: boolean;
6
+ }
7
+
8
+ const FOCUSABLE_SELECTOR =
9
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
10
+
11
+ export function FocusTrap({ children, active = true }: FocusTrapProps) {
12
+ const rootRef = useRef<HTMLDivElement>(null);
13
+
14
+ useEffect(() => {
15
+ if (!active) return;
16
+
17
+ const root = rootRef.current;
18
+ if (!root) return;
19
+
20
+ const focusable = root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
21
+ const first = focusable[0];
22
+ const last = focusable[focusable.length - 1];
23
+
24
+ if (first) {
25
+ first.focus();
26
+ }
27
+
28
+ const handleKeyDown = (e: KeyboardEvent) => {
29
+ if (e.key !== 'Tab') return;
30
+
31
+ if (e.shiftKey) {
32
+ if (document.activeElement === first) {
33
+ e.preventDefault();
34
+ last?.focus();
35
+ }
36
+ } else {
37
+ if (document.activeElement === last) {
38
+ e.preventDefault();
39
+ first?.focus();
40
+ }
41
+ }
42
+ };
43
+
44
+ window.addEventListener('keydown', handleKeyDown);
45
+ return () => window.removeEventListener('keydown', handleKeyDown);
46
+ }, [active]);
47
+
48
+ return <div ref={rootRef} className="contents">{children}</div>;
49
+ }