@thangnqdev/ui 0.1.1 → 0.2.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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -65
  3. package/dist/bento/index.cjs.map +1 -1
  4. package/dist/bento/index.js +2 -2
  5. package/dist/bento/styles.css +2 -46
  6. package/dist/buttons/index.cjs +111 -18
  7. package/dist/buttons/index.cjs.map +1 -1
  8. package/dist/buttons/index.d.cts +1 -1
  9. package/dist/buttons/index.d.ts +1 -1
  10. package/dist/buttons/index.js +3 -3
  11. package/dist/buttons/styles.css +2 -6
  12. package/dist/{chunk-4S6L5V7X.js → chunk-2YJBW4E2.js} +2 -2
  13. package/dist/chunk-2YJBW4E2.js.map +1 -0
  14. package/dist/{chunk-F5Q7BTMH.js → chunk-4CRFNALF.js} +1011 -556
  15. package/dist/chunk-4CRFNALF.js.map +1 -0
  16. package/dist/{chunk-LWFRUJRU.js → chunk-4MS6ZHFQ.js} +225 -41
  17. package/dist/chunk-4MS6ZHFQ.js.map +1 -0
  18. package/dist/{chunk-2OXZK6IP.js → chunk-77KTYOMQ.js} +3 -3
  19. package/dist/chunk-77KTYOMQ.js.map +1 -0
  20. package/dist/{chunk-IABPUCAQ.js → chunk-ADO3SVUA.js} +2 -2
  21. package/dist/chunk-ADO3SVUA.js.map +1 -0
  22. package/dist/{chunk-GXI2H5XA.js → chunk-D7FB3TCA.js} +3 -3
  23. package/dist/chunk-D7FB3TCA.js.map +1 -0
  24. package/dist/chunk-DSEL7ELZ.js +314 -0
  25. package/dist/chunk-DSEL7ELZ.js.map +1 -0
  26. package/dist/{chunk-KJRBRTQE.js → chunk-EXFR2BFJ.js} +37 -28
  27. package/dist/chunk-EXFR2BFJ.js.map +1 -0
  28. package/dist/{chunk-PF4ZHGVW.js → chunk-GXWB6BSY.js} +116 -23
  29. package/dist/chunk-GXWB6BSY.js.map +1 -0
  30. package/dist/{chunk-66VWUJEI.js → chunk-I2FQBMVW.js} +3 -3
  31. package/dist/chunk-I2FQBMVW.js.map +1 -0
  32. package/dist/{chunk-EU2LPUYP.js → chunk-I5O2WCGF.js} +3 -3
  33. package/dist/chunk-I5O2WCGF.js.map +1 -0
  34. package/dist/{chunk-ATYGDTMK.js → chunk-ME4XILG5.js} +5 -5
  35. package/dist/chunk-ME4XILG5.js.map +1 -0
  36. package/dist/{chunk-5FZ6ZFII.js → chunk-PC5SZFZC.js} +373 -150
  37. package/dist/chunk-PC5SZFZC.js.map +1 -0
  38. package/dist/{chunk-C4SUXMI3.js → chunk-S6XJMYME.js} +3 -3
  39. package/dist/chunk-S6XJMYME.js.map +1 -0
  40. package/dist/{chunk-A7P3O5Z5.js → chunk-V52JOSDF.js} +3 -3
  41. package/dist/chunk-V52JOSDF.js.map +1 -0
  42. package/dist/{chunk-NFVONJUK.js → chunk-W5WNWTQC.js} +455 -211
  43. package/dist/chunk-W5WNWTQC.js.map +1 -0
  44. package/dist/{chunk-WAFHC4DQ.js → chunk-YJ64RI7J.js} +3 -3
  45. package/dist/chunk-YJ64RI7J.js.map +1 -0
  46. package/dist/chunk-YOCGBORV.js +155 -0
  47. package/dist/chunk-YOCGBORV.js.map +1 -0
  48. package/dist/chunk-ZEDXARXW.js +96 -0
  49. package/dist/chunk-ZEDXARXW.js.map +1 -0
  50. package/dist/components.css +2 -0
  51. package/dist/core/index.cjs +33 -24
  52. package/dist/core/index.cjs.map +1 -1
  53. package/dist/core/index.js +2 -2
  54. package/dist/core/styles.css +2 -130
  55. package/dist/data-display/index.cjs +770 -247
  56. package/dist/data-display/index.cjs.map +1 -1
  57. package/dist/data-display/index.d.cts +1 -1
  58. package/dist/data-display/index.d.ts +1 -1
  59. package/dist/data-display/index.js +5 -4
  60. package/dist/data-display/styles.css +2 -4
  61. package/dist/feedback/index.cjs.map +1 -1
  62. package/dist/feedback/index.js +2 -2
  63. package/dist/feedback/styles.css +2 -4
  64. package/dist/forms/index.cjs +1211 -459
  65. package/dist/forms/index.cjs.map +1 -1
  66. package/dist/forms/index.d.cts +1 -1
  67. package/dist/forms/index.d.ts +1 -1
  68. package/dist/forms/index.js +8 -8
  69. package/dist/forms/styles.css +2 -6
  70. package/dist/glass/index.cjs.map +1 -1
  71. package/dist/glass/index.js +2 -2
  72. package/dist/glass/styles.css +2 -81
  73. package/dist/{index-D6fE7w-b.d.cts → index-BGt_hecO.d.cts} +1 -0
  74. package/dist/{index-D6fE7w-b.d.ts → index-BGt_hecO.d.ts} +1 -0
  75. package/dist/{index-DzcWh1Hs.d.cts → index-BtDvpZCf.d.cts} +7 -0
  76. package/dist/{index-DzcWh1Hs.d.ts → index-BtDvpZCf.d.ts} +7 -0
  77. package/dist/{index-Cle6QzQl.d.ts → index-COX1sBUN.d.ts} +27 -3
  78. package/dist/{index-BkRqGNG1.d.ts → index-Cg8MuP7u.d.cts} +40 -13
  79. package/dist/{index-BkRqGNG1.d.cts → index-Cg8MuP7u.d.ts} +40 -13
  80. package/dist/{index-CenuF_P2.d.ts → index-DHkPRTvE.d.cts} +39 -8
  81. package/dist/{index-CenuF_P2.d.cts → index-DHkPRTvE.d.ts} +39 -8
  82. package/dist/{index-TyK-Ldpy.d.cts → index-Du_oht9k.d.cts} +27 -3
  83. package/dist/index.cjs +2481 -917
  84. package/dist/index.cjs.map +1 -1
  85. package/dist/index.d.cts +5 -5
  86. package/dist/index.d.ts +5 -5
  87. package/dist/index.js +19 -19
  88. package/dist/layout/index.cjs.map +1 -1
  89. package/dist/layout/index.js +2 -2
  90. package/dist/layout/styles.css +2 -8
  91. package/dist/navigation/index.cjs +288 -48
  92. package/dist/navigation/index.cjs.map +1 -1
  93. package/dist/navigation/index.d.cts +1 -1
  94. package/dist/navigation/index.d.ts +1 -1
  95. package/dist/navigation/index.js +6 -6
  96. package/dist/navigation/styles.css +2 -4
  97. package/dist/overlays/index.cjs +566 -229
  98. package/dist/overlays/index.cjs.map +1 -1
  99. package/dist/overlays/index.d.cts +1 -1
  100. package/dist/overlays/index.d.ts +1 -1
  101. package/dist/overlays/index.js +7 -7
  102. package/dist/overlays/styles.css +2 -8
  103. package/dist/reset.css +45 -0
  104. package/dist/styles.css +1 -1
  105. package/dist/tokens.css +11 -0
  106. package/dist/typography/index.cjs.map +1 -1
  107. package/dist/typography/index.js +4 -4
  108. package/dist/typography/styles.css +2 -5
  109. package/llms.txt +211 -113
  110. package/package.json +160 -116
  111. package/src/bento/BentoCard.tsx +140 -140
  112. package/src/bento/BentoGrid.tsx +50 -50
  113. package/src/bento/index.ts +2 -2
  114. package/src/bento/tokens.css +46 -46
  115. package/src/buttons/Button.tsx +104 -104
  116. package/src/buttons/ButtonGroup.tsx +43 -43
  117. package/src/buttons/IconButton.tsx +79 -79
  118. package/src/buttons/SplitButton.tsx +186 -86
  119. package/src/buttons/ToggleButton.tsx +157 -157
  120. package/src/buttons/index.ts +5 -5
  121. package/src/buttons/tokens.css +6 -6
  122. package/src/core/ThemeProvider.tsx +135 -123
  123. package/src/core/index.ts +2 -2
  124. package/src/core/tokens.css +134 -130
  125. package/src/data-display/Accordion.tsx +147 -147
  126. package/src/data-display/Avatar.tsx +134 -134
  127. package/src/data-display/Card.tsx +112 -112
  128. package/src/data-display/Chip.tsx +74 -74
  129. package/src/data-display/DataTablePro.tsx +299 -121
  130. package/src/data-display/StatCard.tsx +60 -60
  131. package/src/data-display/Table.tsx +74 -74
  132. package/src/data-display/Timeline.tsx +62 -62
  133. package/src/data-display/TreeView.tsx +243 -104
  134. package/src/data-display/tokens.css +4 -4
  135. package/src/feedback/Alert.tsx +80 -80
  136. package/src/feedback/EmptyState.tsx +45 -45
  137. package/src/feedback/Skeleton.tsx +66 -66
  138. package/src/feedback/Spinner.tsx +65 -65
  139. package/src/feedback/index.ts +5 -5
  140. package/src/feedback/tokens.css +4 -4
  141. package/src/forms/Checkbox.tsx +149 -69
  142. package/src/forms/CheckboxGroup.tsx +76 -29
  143. package/src/forms/Combobox.tsx +172 -28
  144. package/src/forms/DatePicker.tsx +385 -81
  145. package/src/forms/FileUpload.tsx +51 -7
  146. package/src/forms/FormField.tsx +115 -115
  147. package/src/forms/Input.tsx +151 -151
  148. package/src/forms/NumberInput.tsx +34 -20
  149. package/src/forms/PasswordInput.tsx +35 -35
  150. package/src/forms/PinInput.tsx +139 -125
  151. package/src/forms/Radio.tsx +142 -142
  152. package/src/forms/RangeSlider.tsx +237 -111
  153. package/src/forms/SearchInput.tsx +34 -34
  154. package/src/forms/Select.tsx +103 -103
  155. package/src/forms/Slider.tsx +85 -85
  156. package/src/forms/Textarea.tsx +114 -114
  157. package/src/forms/tokens.css +6 -6
  158. package/src/glass/GlassButton.tsx +59 -59
  159. package/src/glass/GlassCard.tsx +71 -71
  160. package/src/glass/index.ts +3 -3
  161. package/src/glass/tokens.css +81 -81
  162. package/src/index.ts +26 -26
  163. package/src/layout/AspectRatio.tsx +39 -39
  164. package/src/layout/Container.tsx +53 -53
  165. package/src/layout/Divider.tsx +61 -61
  166. package/src/layout/Flex.tsx +79 -79
  167. package/src/layout/Grid.tsx +124 -124
  168. package/src/layout/PageLayout.tsx +59 -59
  169. package/src/layout/Section.tsx +47 -47
  170. package/src/layout/Spacer.tsx +30 -30
  171. package/src/layout/Stack.tsx +88 -88
  172. package/src/layout/tokens.css +8 -8
  173. package/src/navigation/Breadcrumb.tsx +73 -73
  174. package/src/navigation/CommandPalette.tsx +142 -142
  175. package/src/navigation/DropdownMenu.tsx +244 -160
  176. package/src/navigation/Pagination.tsx +114 -114
  177. package/src/navigation/Stepper.tsx +93 -93
  178. package/src/navigation/Tabs.tsx +265 -133
  179. package/src/navigation/index.ts +6 -6
  180. package/src/navigation/tokens.css +4 -4
  181. package/src/overlays/AlertDialog.tsx +63 -54
  182. package/src/overlays/BottomSheet.tsx +30 -10
  183. package/src/overlays/ContextMenu.tsx +51 -9
  184. package/src/overlays/Drawer.tsx +160 -145
  185. package/src/overlays/FocusTrap.tsx +122 -49
  186. package/src/overlays/Modal.tsx +296 -219
  187. package/src/overlays/Popover.tsx +55 -25
  188. package/src/overlays/Portal.tsx +23 -23
  189. package/src/overlays/Toast.tsx +175 -137
  190. package/src/overlays/Tooltip.tsx +125 -100
  191. package/src/overlays/tokens.css +8 -8
  192. package/src/registry/components.ts +1368 -0
  193. package/src/styles/components.css +39 -0
  194. package/src/styles/index.css +2 -93
  195. package/src/styles/reset.css +45 -0
  196. package/src/styles/tokens.css +11 -2
  197. package/src/typography/Blockquote.tsx +32 -32
  198. package/src/typography/Code.tsx +89 -89
  199. package/src/typography/Heading.tsx +56 -56
  200. package/src/typography/Kbd.tsx +25 -25
  201. package/src/typography/Label.tsx +47 -47
  202. package/src/typography/Link.tsx +56 -56
  203. package/src/typography/Text.tsx +62 -62
  204. package/src/typography/index.ts +7 -7
  205. package/src/typography/tokens.css +5 -5
  206. package/src/utils/cn.ts +6 -6
  207. package/src/utils/scroll-lock.ts +19 -0
  208. package/src/utils/useControllableState.ts +54 -0
  209. package/ui-contract.json +192 -35
  210. package/dist/chunk-2OXZK6IP.js.map +0 -1
  211. package/dist/chunk-4S6L5V7X.js.map +0 -1
  212. package/dist/chunk-5FZ6ZFII.js.map +0 -1
  213. package/dist/chunk-66VWUJEI.js.map +0 -1
  214. package/dist/chunk-A7P3O5Z5.js.map +0 -1
  215. package/dist/chunk-ATYGDTMK.js.map +0 -1
  216. package/dist/chunk-C4SUXMI3.js.map +0 -1
  217. package/dist/chunk-EU2LPUYP.js.map +0 -1
  218. package/dist/chunk-F5Q7BTMH.js.map +0 -1
  219. package/dist/chunk-GXI2H5XA.js.map +0 -1
  220. package/dist/chunk-H3LCNPMC.js +0 -101
  221. package/dist/chunk-H3LCNPMC.js.map +0 -1
  222. package/dist/chunk-IABPUCAQ.js.map +0 -1
  223. package/dist/chunk-KJRBRTQE.js.map +0 -1
  224. package/dist/chunk-LBUBXUYQ.js +0 -40
  225. package/dist/chunk-LBUBXUYQ.js.map +0 -1
  226. package/dist/chunk-LWFRUJRU.js.map +0 -1
  227. package/dist/chunk-NFVONJUK.js.map +0 -1
  228. package/dist/chunk-PF4ZHGVW.js.map +0 -1
  229. package/dist/chunk-WAFHC4DQ.js.map +0 -1
  230. package/dist/chunk-X76KBH4Y.js +0 -71
  231. package/dist/chunk-X76KBH4Y.js.map +0 -1
@@ -1,86 +1,186 @@
1
- import React, { forwardRef, useState, useRef, useEffect } from 'react';
2
- import { ChevronDown } from 'lucide-react';
3
- import { Button, ButtonProps } from './Button';
4
- import { cn } from '../utils/cn';
5
-
6
- export interface SplitButtonItem {
7
- label: React.ReactNode;
8
- icon?: React.ReactNode;
9
- onClick: () => void;
10
- disabled?: boolean;
11
- }
12
-
13
- export interface SplitButtonProps extends Omit<ButtonProps, 'onClick'> {
14
- items: SplitButtonItem[];
15
- onMainClick?: () => void;
16
- }
17
-
18
- export const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
19
- ({ className, items, onMainClick, variant = 'primary', size = 'md', children, disabled, ...props }, ref) => {
20
- const [isOpen, setIsOpen] = useState(false);
21
- const containerRef = useRef<HTMLDivElement>(null);
22
-
23
- useEffect(() => {
24
- const handleOutside = (e: MouseEvent) => {
25
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
26
- setIsOpen(false);
27
- }
28
- };
29
- if (isOpen) {
30
- document.addEventListener('mousedown', handleOutside);
31
- }
32
- return () => document.removeEventListener('mousedown', handleOutside);
33
- }, [isOpen]);
34
-
35
- return (
36
- <div ref={containerRef} className={cn('relative inline-flex', className)}>
37
- <div ref={ref} className="inline-flex rounded-[var(--ui-radius-md)] shadow-sm">
38
- <Button
39
- variant={variant}
40
- size={size}
41
- disabled={disabled}
42
- onClick={onMainClick}
43
- className="rounded-r-none border-r border-white/20 dark:border-black/20"
44
- {...props}
45
- >
46
- {children}
47
- </Button>
48
-
49
- <Button
50
- variant={variant}
51
- size={size}
52
- disabled={disabled}
53
- onClick={() => setIsOpen(!isOpen)}
54
- aria-expanded={isOpen}
55
- aria-haspopup="menu"
56
- className="rounded-l-none px-2"
57
- >
58
- <ChevronDown className={cn('w-4 h-4 transition-transform duration-200', isOpen && 'rotate-180')} />
59
- </Button>
60
- </div>
61
-
62
- {isOpen && (
63
- <div className="absolute top-full right-0 mt-1 w-48 rounded-[var(--ui-radius-md)] bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] shadow-lg py-1 z-50 animate-in fade-in zoom-in-95">
64
- {items.map((item, i) => (
65
- <button
66
- key={i}
67
- type="button"
68
- disabled={item.disabled}
69
- onClick={() => {
70
- item.onClick();
71
- setIsOpen(false);
72
- }}
73
- className="w-full flex items-center gap-2 px-3 py-2 text-left text-sm text-[var(--ui-text-primary)] hover:bg-[var(--ui-bg-tertiary)] disabled:opacity-50 disabled:pointer-events-none transition-colors"
74
- >
75
- {item.icon && <span className="flex-shrink-0">{item.icon}</span>}
76
- <span className="flex-1 truncate">{item.label}</span>
77
- </button>
78
- ))}
79
- </div>
80
- )}
81
- </div>
82
- );
83
- }
84
- );
85
-
86
- SplitButton.displayName = 'SplitButton';
1
+ import React, { forwardRef, useState, useRef, useEffect, useId } from 'react';
2
+ import { ChevronDown } from 'lucide-react';
3
+ import { Button, ButtonProps } from './Button';
4
+ import { cn } from '../utils/cn';
5
+
6
+ export interface SplitButtonItem {
7
+ label: React.ReactNode;
8
+ icon?: React.ReactNode;
9
+ onClick: () => void;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ export interface SplitButtonProps extends Omit<ButtonProps, 'onClick'> {
14
+ items: SplitButtonItem[];
15
+ onMainClick?: () => void;
16
+ menuAriaLabel?: string;
17
+ }
18
+
19
+ export const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
20
+ (
21
+ {
22
+ className,
23
+ items,
24
+ onMainClick,
25
+ variant = 'primary',
26
+ size = 'md',
27
+ children,
28
+ disabled,
29
+ menuAriaLabel = 'More actions',
30
+ ...props
31
+ },
32
+ ref
33
+ ) => {
34
+ const [isOpen, setIsOpen] = useState(false);
35
+ const [focusedIndex, setFocusedIndex] = useState<number>(0);
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+ const triggerBtnRef = useRef<HTMLButtonElement>(null);
38
+ const menuRef = useRef<HTMLDivElement>(null);
39
+ const menuId = `split-button-menu-${useId()}`;
40
+
41
+ const enabledItemsIndices = items
42
+ .map((item, idx) => (!item.disabled ? idx : -1))
43
+ .filter((idx) => idx !== -1);
44
+
45
+ useEffect(() => {
46
+ const handleOutside = (e: MouseEvent) => {
47
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
48
+ setIsOpen(false);
49
+ }
50
+ };
51
+ if (isOpen) {
52
+ document.addEventListener('mousedown', handleOutside);
53
+ }
54
+ return () => document.removeEventListener('mousedown', handleOutside);
55
+ }, [isOpen]);
56
+
57
+ // Auto-focus first item when menu opens
58
+ useEffect(() => {
59
+ if (isOpen && enabledItemsIndices.length > 0) {
60
+ setFocusedIndex(enabledItemsIndices[0]);
61
+ const timer = setTimeout(() => {
62
+ const itemEl = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]:not([aria-disabled="true"])');
63
+ itemEl?.focus();
64
+ }, 10);
65
+ return () => clearTimeout(timer);
66
+ }
67
+ }, [isOpen]);
68
+
69
+ const handleMenuKeyDown = (e: React.KeyboardEvent) => {
70
+ if (!isOpen) return;
71
+
72
+ if (e.key === 'ArrowDown') {
73
+ e.preventDefault();
74
+ const currentPos = enabledItemsIndices.indexOf(focusedIndex);
75
+ const nextPos = (currentPos + 1) % enabledItemsIndices.length;
76
+ const nextIdx = enabledItemsIndices[nextPos];
77
+ setFocusedIndex(nextIdx);
78
+ const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]');
79
+ buttons?.[nextIdx]?.focus();
80
+ } else if (e.key === 'ArrowUp') {
81
+ e.preventDefault();
82
+ const currentPos = enabledItemsIndices.indexOf(focusedIndex);
83
+ const prevPos = (currentPos - 1 + enabledItemsIndices.length) % enabledItemsIndices.length;
84
+ const prevIdx = enabledItemsIndices[prevPos];
85
+ setFocusedIndex(prevIdx);
86
+ const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]');
87
+ buttons?.[prevIdx]?.focus();
88
+ } else if (e.key === 'Home') {
89
+ e.preventDefault();
90
+ if (enabledItemsIndices.length > 0) {
91
+ const firstIdx = enabledItemsIndices[0];
92
+ setFocusedIndex(firstIdx);
93
+ const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]');
94
+ buttons?.[firstIdx]?.focus();
95
+ }
96
+ } else if (e.key === 'End') {
97
+ e.preventDefault();
98
+ if (enabledItemsIndices.length > 0) {
99
+ const lastIdx = enabledItemsIndices[enabledItemsIndices.length - 1];
100
+ setFocusedIndex(lastIdx);
101
+ const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]');
102
+ buttons?.[lastIdx]?.focus();
103
+ }
104
+ } else if (e.key === 'Escape') {
105
+ e.preventDefault();
106
+ setIsOpen(false);
107
+ triggerBtnRef.current?.focus();
108
+ }
109
+ };
110
+
111
+ return (
112
+ <div ref={containerRef} className={cn('relative inline-flex', className)}>
113
+ <div ref={ref} className="inline-flex rounded-[var(--ui-radius-md)] shadow-sm">
114
+ <Button
115
+ variant={variant}
116
+ size={size}
117
+ disabled={disabled}
118
+ onClick={onMainClick}
119
+ className="rounded-r-none border-r border-white/20 dark:border-black/20"
120
+ {...props}
121
+ >
122
+ {children}
123
+ </Button>
124
+
125
+ <Button
126
+ ref={triggerBtnRef}
127
+ variant={variant}
128
+ size={size}
129
+ disabled={disabled}
130
+ onClick={() => setIsOpen(!isOpen)}
131
+ onKeyDown={(e) => {
132
+ if (e.key === 'ArrowDown') {
133
+ e.preventDefault();
134
+ setIsOpen(true);
135
+ }
136
+ }}
137
+ aria-expanded={isOpen}
138
+ aria-haspopup="menu"
139
+ aria-controls={isOpen ? menuId : undefined}
140
+ aria-label={menuAriaLabel}
141
+ className="rounded-l-none px-2 cursor-pointer"
142
+ >
143
+ <ChevronDown className={cn('w-4 h-4 transition-transform duration-200', isOpen && 'rotate-180')} />
144
+ </Button>
145
+ </div>
146
+
147
+ {isOpen && (
148
+ <div
149
+ ref={menuRef}
150
+ id={menuId}
151
+ role="menu"
152
+ aria-orientation="vertical"
153
+ tabIndex={-1}
154
+ onKeyDown={handleMenuKeyDown}
155
+ className="absolute top-full right-0 mt-1 w-48 rounded-[var(--ui-radius-md)] bg-[var(--ui-bg-surface)] border border-[var(--ui-border-default)] shadow-lg py-1 z-50 animate-in fade-in zoom-in-95 focus:outline-none"
156
+ >
157
+ {items.map((item, i) => (
158
+ <button
159
+ key={i}
160
+ type="button"
161
+ role="menuitem"
162
+ tabIndex={focusedIndex === i ? 0 : -1}
163
+ aria-disabled={item.disabled}
164
+ disabled={item.disabled}
165
+ onClick={() => {
166
+ item.onClick();
167
+ setIsOpen(false);
168
+ triggerBtnRef.current?.focus();
169
+ }}
170
+ className={cn(
171
+ 'w-full flex items-center gap-2 px-3 py-2 text-left text-sm text-[var(--ui-text-primary)] hover:bg-[var(--ui-bg-tertiary)] focus:bg-[var(--ui-bg-tertiary)] focus:outline-none disabled:opacity-50 disabled:pointer-events-none transition-colors cursor-pointer',
172
+ focusedIndex === i && 'bg-[var(--ui-bg-tertiary)]'
173
+ )}
174
+ >
175
+ {item.icon && <span className="flex-shrink-0">{item.icon}</span>}
176
+ <span className="flex-1 truncate">{item.label}</span>
177
+ </button>
178
+ ))}
179
+ </div>
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+ );
185
+
186
+ SplitButton.displayName = 'SplitButton';
@@ -1,157 +1,157 @@
1
- import React, { forwardRef, createContext, useContext } from 'react';
2
- import { cva, type VariantProps } from 'class-variance-authority';
3
- import { cn } from '../utils/cn';
4
-
5
- export const toggleVariants = cva(
6
- 'inline-flex items-center justify-center text-sm font-medium transition-colors hover:bg-[var(--ui-bg-tertiary)] hover:text-[var(--ui-text-primary)] 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',
7
- {
8
- variants: {
9
- variant: {
10
- default: 'bg-transparent text-[var(--ui-text-secondary)]',
11
- outline:
12
- 'border border-[var(--ui-border-default)] bg-transparent hover:bg-[var(--ui-bg-tertiary)]',
13
- },
14
- size: {
15
- sm: 'h-8 px-2.5 text-xs rounded-[var(--ui-radius-sm)]',
16
- md: 'h-9 px-3 text-sm rounded-[var(--ui-radius-md)]',
17
- lg: 'h-10 px-4 text-base rounded-[var(--ui-radius-md)]',
18
- },
19
- },
20
- defaultVariants: {
21
- variant: 'default',
22
- size: 'md',
23
- },
24
- }
25
- );
26
-
27
- interface ToggleGroupContextType {
28
- value?: string | string[];
29
- onChange?: (val: string) => void;
30
- variant?: 'default' | 'outline';
31
- size?: 'sm' | 'md' | 'lg';
32
- }
33
-
34
- const ToggleGroupContext = createContext<ToggleGroupContextType>({});
35
-
36
- export interface ToggleButtonProps
37
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
- VariantProps<typeof toggleVariants> {
39
- pressed?: boolean;
40
- value?: string;
41
- }
42
-
43
- export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
44
- ({ className, pressed: customPressed, variant, size, value, onClick, children, ...props }, ref) => {
45
- const group = useContext(ToggleGroupContext);
46
-
47
- const isPressed =
48
- customPressed !== undefined
49
- ? customPressed
50
- : group.value !== undefined && value !== undefined
51
- ? Array.isArray(group.value)
52
- ? group.value.includes(value)
53
- : group.value === value
54
- : false;
55
-
56
- const currentVariant = variant || group.variant || 'default';
57
- const currentSize = size || group.size || 'md';
58
-
59
- const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
60
- if (value && group.onChange) {
61
- group.onChange(value);
62
- }
63
- onClick?.(e);
64
- };
65
-
66
- return (
67
- <button
68
- ref={ref}
69
- type="button"
70
- aria-pressed={isPressed}
71
- data-state={isPressed ? 'on' : 'off'}
72
- onClick={handleClick}
73
- className={cn(
74
- toggleVariants({ variant: currentVariant, size: currentSize }),
75
- isPressed &&
76
- 'bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)] font-semibold shadow-inner border-[var(--ui-border-strong)]',
77
- className
78
- )}
79
- {...props}
80
- >
81
- {children}
82
- </button>
83
- );
84
- }
85
- );
86
- ToggleButton.displayName = 'ToggleButton';
87
-
88
- export interface ToggleGroupProps extends React.HTMLAttributes<HTMLDivElement> {
89
- type?: 'single' | 'multiple';
90
- value?: string | string[];
91
- defaultValue?: string | string[];
92
- onValueChange?: (value: any) => void;
93
- variant?: 'default' | 'outline';
94
- size?: 'sm' | 'md' | 'lg';
95
- }
96
-
97
- export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
98
- (
99
- {
100
- className,
101
- type = 'single',
102
- value: controlledValue,
103
- defaultValue,
104
- onValueChange,
105
- variant = 'outline',
106
- size = 'md',
107
- children,
108
- ...props
109
- },
110
- ref
111
- ) => {
112
- const [stateValue, setStateValue] = React.useState<string | string[]>(
113
- defaultValue || (type === 'multiple' ? [] : '')
114
- );
115
-
116
- const currentValue = controlledValue !== undefined ? controlledValue : stateValue;
117
-
118
- const handleItemChange = (itemValue: string) => {
119
- if (type === 'single') {
120
- const next = currentValue === itemValue ? '' : itemValue;
121
- setStateValue(next);
122
- onValueChange?.(next);
123
- } else {
124
- const arr = Array.isArray(currentValue) ? currentValue : [];
125
- const next = arr.includes(itemValue)
126
- ? arr.filter((v) => v !== itemValue)
127
- : [...arr, itemValue];
128
- setStateValue(next);
129
- onValueChange?.(next);
130
- }
131
- };
132
-
133
- return (
134
- <ToggleGroupContext.Provider
135
- value={{
136
- value: currentValue,
137
- onChange: handleItemChange,
138
- variant,
139
- size,
140
- }}
141
- >
142
- <div
143
- ref={ref}
144
- role="group"
145
- className={cn(
146
- 'inline-flex items-center gap-1 rounded-[var(--ui-radius-md)] p-1 bg-[var(--ui-bg-secondary)] border border-[var(--ui-border-subtle)]',
147
- className
148
- )}
149
- {...props}
150
- >
151
- {children}
152
- </div>
153
- </ToggleGroupContext.Provider>
154
- );
155
- }
156
- );
157
- ToggleGroup.displayName = 'ToggleGroup';
1
+ import React, { forwardRef, createContext, useContext } from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../utils/cn';
4
+
5
+ export const toggleVariants = cva(
6
+ 'inline-flex items-center justify-center text-sm font-medium transition-colors hover:bg-[var(--ui-bg-tertiary)] hover:text-[var(--ui-text-primary)] 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',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'bg-transparent text-[var(--ui-text-secondary)]',
11
+ outline:
12
+ 'border border-[var(--ui-border-default)] bg-transparent hover:bg-[var(--ui-bg-tertiary)]',
13
+ },
14
+ size: {
15
+ sm: 'h-8 px-2.5 text-xs rounded-[var(--ui-radius-sm)]',
16
+ md: 'h-9 px-3 text-sm rounded-[var(--ui-radius-md)]',
17
+ lg: 'h-10 px-4 text-base rounded-[var(--ui-radius-md)]',
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: 'default',
22
+ size: 'md',
23
+ },
24
+ }
25
+ );
26
+
27
+ interface ToggleGroupContextType {
28
+ value?: string | string[];
29
+ onChange?: (val: string) => void;
30
+ variant?: 'default' | 'outline';
31
+ size?: 'sm' | 'md' | 'lg';
32
+ }
33
+
34
+ const ToggleGroupContext = createContext<ToggleGroupContextType>({});
35
+
36
+ export interface ToggleButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof toggleVariants> {
39
+ pressed?: boolean;
40
+ value?: string;
41
+ }
42
+
43
+ export const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(
44
+ ({ className, pressed: customPressed, variant, size, value, onClick, children, ...props }, ref) => {
45
+ const group = useContext(ToggleGroupContext);
46
+
47
+ const isPressed =
48
+ customPressed !== undefined
49
+ ? customPressed
50
+ : group.value !== undefined && value !== undefined
51
+ ? Array.isArray(group.value)
52
+ ? group.value.includes(value)
53
+ : group.value === value
54
+ : false;
55
+
56
+ const currentVariant = variant || group.variant || 'default';
57
+ const currentSize = size || group.size || 'md';
58
+
59
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
60
+ if (value && group.onChange) {
61
+ group.onChange(value);
62
+ }
63
+ onClick?.(e);
64
+ };
65
+
66
+ return (
67
+ <button
68
+ ref={ref}
69
+ type="button"
70
+ aria-pressed={isPressed}
71
+ data-state={isPressed ? 'on' : 'off'}
72
+ onClick={handleClick}
73
+ className={cn(
74
+ toggleVariants({ variant: currentVariant, size: currentSize }),
75
+ isPressed &&
76
+ 'bg-[var(--ui-bg-tertiary)] text-[var(--ui-text-primary)] font-semibold shadow-inner border-[var(--ui-border-strong)]',
77
+ className
78
+ )}
79
+ {...props}
80
+ >
81
+ {children}
82
+ </button>
83
+ );
84
+ }
85
+ );
86
+ ToggleButton.displayName = 'ToggleButton';
87
+
88
+ export interface ToggleGroupProps extends React.HTMLAttributes<HTMLDivElement> {
89
+ type?: 'single' | 'multiple';
90
+ value?: string | string[];
91
+ defaultValue?: string | string[];
92
+ onValueChange?: (value: any) => void;
93
+ variant?: 'default' | 'outline';
94
+ size?: 'sm' | 'md' | 'lg';
95
+ }
96
+
97
+ export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
98
+ (
99
+ {
100
+ className,
101
+ type = 'single',
102
+ value: controlledValue,
103
+ defaultValue,
104
+ onValueChange,
105
+ variant = 'outline',
106
+ size = 'md',
107
+ children,
108
+ ...props
109
+ },
110
+ ref
111
+ ) => {
112
+ const [stateValue, setStateValue] = React.useState<string | string[]>(
113
+ defaultValue || (type === 'multiple' ? [] : '')
114
+ );
115
+
116
+ const currentValue = controlledValue !== undefined ? controlledValue : stateValue;
117
+
118
+ const handleItemChange = (itemValue: string) => {
119
+ if (type === 'single') {
120
+ const next = currentValue === itemValue ? '' : itemValue;
121
+ setStateValue(next);
122
+ onValueChange?.(next);
123
+ } else {
124
+ const arr = Array.isArray(currentValue) ? currentValue : [];
125
+ const next = arr.includes(itemValue)
126
+ ? arr.filter((v) => v !== itemValue)
127
+ : [...arr, itemValue];
128
+ setStateValue(next);
129
+ onValueChange?.(next);
130
+ }
131
+ };
132
+
133
+ return (
134
+ <ToggleGroupContext.Provider
135
+ value={{
136
+ value: currentValue,
137
+ onChange: handleItemChange,
138
+ variant,
139
+ size,
140
+ }}
141
+ >
142
+ <div
143
+ ref={ref}
144
+ role="group"
145
+ className={cn(
146
+ 'inline-flex items-center gap-1 rounded-[var(--ui-radius-md)] p-1 bg-[var(--ui-bg-secondary)] border border-[var(--ui-border-subtle)]',
147
+ className
148
+ )}
149
+ {...props}
150
+ >
151
+ {children}
152
+ </div>
153
+ </ToggleGroupContext.Provider>
154
+ );
155
+ }
156
+ );
157
+ ToggleGroup.displayName = 'ToggleGroup';
@@ -1,5 +1,5 @@
1
- export * from './Button';
2
- export * from './IconButton';
3
- export * from './ButtonGroup';
4
- export * from './ToggleButton';
5
- export * from './SplitButton';
1
+ export * from './Button';
2
+ export * from './IconButton';
3
+ export * from './ButtonGroup';
4
+ export * from './ToggleButton';
5
+ export * from './SplitButton';
@@ -1,6 +1,6 @@
1
- /* Button Tokens */
2
- :root {
3
- --ui-btn-height-sm: 2rem;
4
- --ui-btn-height-md: 2.5rem;
5
- --ui-btn-height-lg: 3rem;
6
- }
1
+ /* Button Tokens */
2
+ :root {
3
+ --ui-btn-height-sm: 2rem;
4
+ --ui-btn-height-md: 2.5rem;
5
+ --ui-btn-height-lg: 3rem;
6
+ }