@thangnqdev/ui 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -206
  3. package/bin/cli.js +125 -0
  4. package/dist/bento/index.cjs.map +1 -1
  5. package/dist/bento/index.js +2 -2
  6. package/dist/bento/styles.css +2 -46
  7. package/dist/buttons/index.cjs +111 -18
  8. package/dist/buttons/index.cjs.map +1 -1
  9. package/dist/buttons/index.d.cts +1 -1
  10. package/dist/buttons/index.d.ts +1 -1
  11. package/dist/buttons/index.js +3 -3
  12. package/dist/buttons/styles.css +2 -6
  13. package/dist/{chunk-4S6L5V7X.js → chunk-2YJBW4E2.js} +2 -2
  14. package/dist/chunk-2YJBW4E2.js.map +1 -0
  15. package/dist/{chunk-F5Q7BTMH.js → chunk-4CRFNALF.js} +1011 -556
  16. package/dist/chunk-4CRFNALF.js.map +1 -0
  17. package/dist/{chunk-LWFRUJRU.js → chunk-4MS6ZHFQ.js} +225 -41
  18. package/dist/chunk-4MS6ZHFQ.js.map +1 -0
  19. package/dist/{chunk-2OXZK6IP.js → chunk-77KTYOMQ.js} +3 -3
  20. package/dist/chunk-77KTYOMQ.js.map +1 -0
  21. package/dist/{chunk-IABPUCAQ.js → chunk-ADO3SVUA.js} +2 -2
  22. package/dist/chunk-ADO3SVUA.js.map +1 -0
  23. package/dist/{chunk-GXI2H5XA.js → chunk-D7FB3TCA.js} +3 -3
  24. package/dist/chunk-D7FB3TCA.js.map +1 -0
  25. package/dist/chunk-DSEL7ELZ.js +314 -0
  26. package/dist/chunk-DSEL7ELZ.js.map +1 -0
  27. package/dist/{chunk-KJRBRTQE.js → chunk-EXFR2BFJ.js} +37 -28
  28. package/dist/chunk-EXFR2BFJ.js.map +1 -0
  29. package/dist/{chunk-PF4ZHGVW.js → chunk-GXWB6BSY.js} +116 -23
  30. package/dist/chunk-GXWB6BSY.js.map +1 -0
  31. package/dist/{chunk-66VWUJEI.js → chunk-I2FQBMVW.js} +3 -3
  32. package/dist/chunk-I2FQBMVW.js.map +1 -0
  33. package/dist/{chunk-EU2LPUYP.js → chunk-I5O2WCGF.js} +3 -3
  34. package/dist/chunk-I5O2WCGF.js.map +1 -0
  35. package/dist/{chunk-ATYGDTMK.js → chunk-ME4XILG5.js} +5 -5
  36. package/dist/chunk-ME4XILG5.js.map +1 -0
  37. package/dist/{chunk-5FZ6ZFII.js → chunk-PC5SZFZC.js} +373 -150
  38. package/dist/chunk-PC5SZFZC.js.map +1 -0
  39. package/dist/{chunk-C4SUXMI3.js → chunk-S6XJMYME.js} +3 -3
  40. package/dist/chunk-S6XJMYME.js.map +1 -0
  41. package/dist/{chunk-A7P3O5Z5.js → chunk-V52JOSDF.js} +3 -3
  42. package/dist/chunk-V52JOSDF.js.map +1 -0
  43. package/dist/{chunk-NFVONJUK.js → chunk-W5WNWTQC.js} +455 -211
  44. package/dist/chunk-W5WNWTQC.js.map +1 -0
  45. package/dist/{chunk-WAFHC4DQ.js → chunk-YJ64RI7J.js} +3 -3
  46. package/dist/chunk-YJ64RI7J.js.map +1 -0
  47. package/dist/chunk-YOCGBORV.js +155 -0
  48. package/dist/chunk-YOCGBORV.js.map +1 -0
  49. package/dist/chunk-ZEDXARXW.js +96 -0
  50. package/dist/chunk-ZEDXARXW.js.map +1 -0
  51. package/dist/components.css +2 -0
  52. package/dist/core/index.cjs +33 -24
  53. package/dist/core/index.cjs.map +1 -1
  54. package/dist/core/index.js +2 -2
  55. package/dist/core/styles.css +2 -134
  56. package/dist/data-display/index.cjs +770 -247
  57. package/dist/data-display/index.cjs.map +1 -1
  58. package/dist/data-display/index.d.cts +1 -1
  59. package/dist/data-display/index.d.ts +1 -1
  60. package/dist/data-display/index.js +5 -4
  61. package/dist/data-display/styles.css +2 -4
  62. package/dist/feedback/index.cjs.map +1 -1
  63. package/dist/feedback/index.js +2 -2
  64. package/dist/feedback/styles.css +2 -4
  65. package/dist/forms/index.cjs +1211 -459
  66. package/dist/forms/index.cjs.map +1 -1
  67. package/dist/forms/index.d.cts +1 -1
  68. package/dist/forms/index.d.ts +1 -1
  69. package/dist/forms/index.js +8 -8
  70. package/dist/forms/styles.css +2 -6
  71. package/dist/glass/index.cjs.map +1 -1
  72. package/dist/glass/index.js +2 -2
  73. package/dist/glass/styles.css +2 -81
  74. package/dist/{index-D6fE7w-b.d.cts → index-BGt_hecO.d.cts} +1 -0
  75. package/dist/{index-D6fE7w-b.d.ts → index-BGt_hecO.d.ts} +1 -0
  76. package/dist/{index-DzcWh1Hs.d.cts → index-BtDvpZCf.d.cts} +7 -0
  77. package/dist/{index-DzcWh1Hs.d.ts → index-BtDvpZCf.d.ts} +7 -0
  78. package/dist/{index-Cle6QzQl.d.ts → index-COX1sBUN.d.ts} +27 -3
  79. package/dist/{index-BkRqGNG1.d.ts → index-Cg8MuP7u.d.cts} +40 -13
  80. package/dist/{index-BkRqGNG1.d.cts → index-Cg8MuP7u.d.ts} +40 -13
  81. package/dist/{index-CenuF_P2.d.ts → index-DHkPRTvE.d.cts} +39 -8
  82. package/dist/{index-CenuF_P2.d.cts → index-DHkPRTvE.d.ts} +39 -8
  83. package/dist/{index-TyK-Ldpy.d.cts → index-Du_oht9k.d.cts} +27 -3
  84. package/dist/index.cjs +2481 -917
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.d.cts +5 -5
  87. package/dist/index.d.ts +5 -5
  88. package/dist/index.js +19 -19
  89. package/dist/layout/index.cjs.map +1 -1
  90. package/dist/layout/index.js +2 -2
  91. package/dist/layout/styles.css +2 -8
  92. package/dist/navigation/index.cjs +288 -48
  93. package/dist/navigation/index.cjs.map +1 -1
  94. package/dist/navigation/index.d.cts +1 -1
  95. package/dist/navigation/index.d.ts +1 -1
  96. package/dist/navigation/index.js +6 -6
  97. package/dist/navigation/styles.css +2 -4
  98. package/dist/overlays/index.cjs +566 -229
  99. package/dist/overlays/index.cjs.map +1 -1
  100. package/dist/overlays/index.d.cts +1 -1
  101. package/dist/overlays/index.d.ts +1 -1
  102. package/dist/overlays/index.js +7 -7
  103. package/dist/overlays/styles.css +2 -8
  104. package/dist/reset.css +45 -0
  105. package/dist/styles.css +1 -1
  106. package/dist/tokens.css +11 -0
  107. package/dist/typography/index.cjs.map +1 -1
  108. package/dist/typography/index.js +4 -4
  109. package/dist/typography/styles.css +2 -5
  110. package/llms.txt +211 -113
  111. package/package.json +167 -116
  112. package/scripts/postinstall.js +125 -0
  113. package/src/bento/BentoCard.tsx +140 -140
  114. package/src/bento/BentoGrid.tsx +50 -50
  115. package/src/bento/index.ts +2 -2
  116. package/src/bento/tokens.css +46 -46
  117. package/src/buttons/Button.tsx +104 -104
  118. package/src/buttons/ButtonGroup.tsx +43 -43
  119. package/src/buttons/IconButton.tsx +79 -79
  120. package/src/buttons/SplitButton.tsx +186 -86
  121. package/src/buttons/ToggleButton.tsx +157 -157
  122. package/src/buttons/index.ts +5 -5
  123. package/src/buttons/tokens.css +6 -6
  124. package/src/core/ThemeProvider.tsx +135 -123
  125. package/src/core/index.ts +2 -2
  126. package/src/core/tokens.css +134 -134
  127. package/src/data-display/Accordion.tsx +147 -147
  128. package/src/data-display/Avatar.tsx +134 -134
  129. package/src/data-display/Card.tsx +112 -112
  130. package/src/data-display/Chip.tsx +74 -74
  131. package/src/data-display/DataTablePro.tsx +299 -121
  132. package/src/data-display/StatCard.tsx +60 -60
  133. package/src/data-display/Table.tsx +74 -74
  134. package/src/data-display/Timeline.tsx +62 -62
  135. package/src/data-display/TreeView.tsx +243 -104
  136. package/src/data-display/tokens.css +4 -4
  137. package/src/feedback/Alert.tsx +80 -80
  138. package/src/feedback/EmptyState.tsx +45 -45
  139. package/src/feedback/Skeleton.tsx +66 -66
  140. package/src/feedback/Spinner.tsx +65 -65
  141. package/src/feedback/index.ts +5 -5
  142. package/src/feedback/tokens.css +4 -4
  143. package/src/forms/Checkbox.tsx +149 -69
  144. package/src/forms/CheckboxGroup.tsx +76 -29
  145. package/src/forms/Combobox.tsx +172 -28
  146. package/src/forms/DatePicker.tsx +385 -81
  147. package/src/forms/FileUpload.tsx +51 -7
  148. package/src/forms/FormField.tsx +115 -115
  149. package/src/forms/Input.tsx +151 -151
  150. package/src/forms/NumberInput.tsx +34 -20
  151. package/src/forms/PasswordInput.tsx +35 -35
  152. package/src/forms/PinInput.tsx +139 -125
  153. package/src/forms/Radio.tsx +142 -142
  154. package/src/forms/RangeSlider.tsx +237 -111
  155. package/src/forms/SearchInput.tsx +34 -34
  156. package/src/forms/Select.tsx +103 -103
  157. package/src/forms/Slider.tsx +85 -85
  158. package/src/forms/Textarea.tsx +114 -114
  159. package/src/forms/tokens.css +6 -6
  160. package/src/glass/GlassButton.tsx +59 -59
  161. package/src/glass/GlassCard.tsx +71 -71
  162. package/src/glass/index.ts +3 -3
  163. package/src/glass/tokens.css +81 -81
  164. package/src/index.ts +26 -26
  165. package/src/layout/AspectRatio.tsx +39 -39
  166. package/src/layout/Container.tsx +53 -53
  167. package/src/layout/Divider.tsx +61 -61
  168. package/src/layout/Flex.tsx +79 -79
  169. package/src/layout/Grid.tsx +124 -124
  170. package/src/layout/PageLayout.tsx +59 -59
  171. package/src/layout/Section.tsx +47 -47
  172. package/src/layout/Spacer.tsx +30 -30
  173. package/src/layout/Stack.tsx +88 -88
  174. package/src/layout/tokens.css +8 -8
  175. package/src/navigation/Breadcrumb.tsx +73 -73
  176. package/src/navigation/CommandPalette.tsx +142 -142
  177. package/src/navigation/DropdownMenu.tsx +244 -160
  178. package/src/navigation/Pagination.tsx +114 -114
  179. package/src/navigation/Stepper.tsx +93 -93
  180. package/src/navigation/Tabs.tsx +265 -133
  181. package/src/navigation/index.ts +6 -6
  182. package/src/navigation/tokens.css +4 -4
  183. package/src/overlays/AlertDialog.tsx +63 -54
  184. package/src/overlays/BottomSheet.tsx +30 -10
  185. package/src/overlays/ContextMenu.tsx +51 -9
  186. package/src/overlays/Drawer.tsx +160 -145
  187. package/src/overlays/FocusTrap.tsx +122 -49
  188. package/src/overlays/Modal.tsx +296 -219
  189. package/src/overlays/Popover.tsx +55 -25
  190. package/src/overlays/Portal.tsx +23 -23
  191. package/src/overlays/Toast.tsx +175 -137
  192. package/src/overlays/Tooltip.tsx +125 -100
  193. package/src/overlays/tokens.css +8 -8
  194. package/src/registry/components.ts +1368 -0
  195. package/src/styles/components.css +39 -0
  196. package/src/styles/index.css +2 -93
  197. package/src/styles/reset.css +45 -0
  198. package/src/styles/tokens.css +11 -2
  199. package/src/typography/Blockquote.tsx +32 -32
  200. package/src/typography/Code.tsx +89 -89
  201. package/src/typography/Heading.tsx +56 -56
  202. package/src/typography/Kbd.tsx +25 -25
  203. package/src/typography/Label.tsx +47 -47
  204. package/src/typography/Link.tsx +56 -56
  205. package/src/typography/Text.tsx +62 -62
  206. package/src/typography/index.ts +7 -7
  207. package/src/typography/tokens.css +5 -5
  208. package/src/utils/cn.ts +6 -6
  209. package/src/utils/scroll-lock.ts +19 -0
  210. package/src/utils/useControllableState.ts +54 -0
  211. package/ui-contract.json +192 -35
  212. package/dist/chunk-2OXZK6IP.js.map +0 -1
  213. package/dist/chunk-4S6L5V7X.js.map +0 -1
  214. package/dist/chunk-5FZ6ZFII.js.map +0 -1
  215. package/dist/chunk-66VWUJEI.js.map +0 -1
  216. package/dist/chunk-A7P3O5Z5.js.map +0 -1
  217. package/dist/chunk-ATYGDTMK.js.map +0 -1
  218. package/dist/chunk-C4SUXMI3.js.map +0 -1
  219. package/dist/chunk-EU2LPUYP.js.map +0 -1
  220. package/dist/chunk-F5Q7BTMH.js.map +0 -1
  221. package/dist/chunk-GXI2H5XA.js.map +0 -1
  222. package/dist/chunk-H3LCNPMC.js +0 -101
  223. package/dist/chunk-H3LCNPMC.js.map +0 -1
  224. package/dist/chunk-IABPUCAQ.js.map +0 -1
  225. package/dist/chunk-KJRBRTQE.js.map +0 -1
  226. package/dist/chunk-LBUBXUYQ.js +0 -40
  227. package/dist/chunk-LBUBXUYQ.js.map +0 -1
  228. package/dist/chunk-LWFRUJRU.js.map +0 -1
  229. package/dist/chunk-NFVONJUK.js.map +0 -1
  230. package/dist/chunk-PF4ZHGVW.js.map +0 -1
  231. package/dist/chunk-WAFHC4DQ.js.map +0 -1
  232. package/dist/chunk-X76KBH4Y.js +0 -71
  233. 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
+ }