@bearmenu/ui 0.5.2 → 0.6.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 (223) hide show
  1. package/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
  2. package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
  3. package/dist/chunk-D5QHEHXJ.js +26 -0
  4. package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
  5. package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
  6. package/dist/chunk-RIDXFY6M.js +38 -0
  7. package/dist/chunk-UUJVXN2O.js +82 -0
  8. package/dist/chunk-WKMLZDUT.js +122 -0
  9. package/dist/chunk-XK2ZDQVE.js +122 -0
  10. package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
  11. package/dist/chunk-YJ6OBKZJ.js +32 -0
  12. package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
  13. package/dist/chunk-ZFNOCP6W.js +106 -0
  14. package/dist/components/accordion.js +11 -5
  15. package/dist/components/alert-dialog.js +5 -3
  16. package/dist/components/alert.d.ts +5 -2
  17. package/dist/components/alert.js +25 -9
  18. package/dist/components/avatar.d.ts +17 -2
  19. package/dist/components/avatar.js +91 -12
  20. package/dist/components/badge.d.ts +1 -1
  21. package/dist/components/badge.js +1 -1
  22. package/dist/components/breadcrumb.js +5 -2
  23. package/dist/components/button-group.d.ts +14 -0
  24. package/dist/components/button-group.js +39 -0
  25. package/dist/components/button.d.ts +4 -2
  26. package/dist/components/button.js +1 -1
  27. package/dist/components/calendar.js +3 -3
  28. package/dist/components/card.d.ts +13 -5
  29. package/dist/components/card.js +2 -2
  30. package/dist/components/carousel.d.ts +20 -3
  31. package/dist/components/carousel.js +72 -12
  32. package/dist/components/checkbox.js +1 -1
  33. package/dist/components/circular-progress.d.ts +16 -0
  34. package/dist/components/circular-progress.js +105 -0
  35. package/dist/components/combobox.d.ts +27 -0
  36. package/dist/components/combobox.js +102 -0
  37. package/dist/components/command.d.ts +29 -16
  38. package/dist/components/command.js +2 -2
  39. package/dist/components/currency-input.d.ts +1 -0
  40. package/dist/components/currency-input.js +73 -52
  41. package/dist/components/date-input.d.ts +10 -0
  42. package/dist/components/date-input.js +40 -0
  43. package/dist/components/description-list.d.ts +15 -0
  44. package/dist/components/description-list.js +67 -0
  45. package/dist/components/dialog.d.ts +13 -2
  46. package/dist/components/dialog.js +1 -1
  47. package/dist/components/drawer.d.ts +5 -2
  48. package/dist/components/drawer.js +47 -9
  49. package/dist/components/dropdown-menu.d.ts +7 -2
  50. package/dist/components/dropdown-menu.js +22 -6
  51. package/dist/components/empty-state.js +2 -2
  52. package/dist/components/field.d.ts +22 -0
  53. package/dist/components/field.js +130 -0
  54. package/dist/components/form.d.ts +2 -1
  55. package/dist/components/form.js +11 -1
  56. package/dist/components/haptic-button.js +3 -3
  57. package/dist/components/input-group.d.ts +18 -0
  58. package/dist/components/input-group.js +92 -0
  59. package/dist/components/input.d.ts +8 -2
  60. package/dist/components/input.js +1 -1
  61. package/dist/components/item.d.ts +23 -0
  62. package/dist/components/item.js +120 -0
  63. package/dist/components/kbd.d.ts +13 -0
  64. package/dist/components/kbd.js +49 -0
  65. package/dist/components/lazy-markdown.d.ts +3 -1
  66. package/dist/components/lazy-markdown.js +7 -7
  67. package/dist/components/link-button.js +1 -1
  68. package/dist/components/multi-select-combobox.js +92 -83
  69. package/dist/components/multi-select-menu.d.ts +0 -4
  70. package/dist/components/multi-select-menu.js +28 -17
  71. package/dist/components/native-select.d.ts +12 -0
  72. package/dist/components/native-select.js +49 -0
  73. package/dist/components/pagination.d.ts +5 -2
  74. package/dist/components/pagination.js +66 -7
  75. package/dist/components/place-about-tab.js +4 -4
  76. package/dist/components/place-card.js +1 -1
  77. package/dist/components/place-details-mobile.js +4 -4
  78. package/dist/components/popover.d.ts +7 -1
  79. package/dist/components/popover.js +1 -1
  80. package/dist/components/progress.d.ts +5 -1
  81. package/dist/components/progress.js +17 -3
  82. package/dist/components/radio-group.js +12 -3
  83. package/dist/components/rating.d.ts +2 -0
  84. package/dist/components/rating.js +88 -70
  85. package/dist/components/search-bar.d.ts +1 -2
  86. package/dist/components/search-bar.js +37 -60
  87. package/dist/components/searchable-select.js +76 -72
  88. package/dist/components/select.d.ts +5 -1
  89. package/dist/components/select.js +30 -14
  90. package/dist/components/selection-bar.d.ts +11 -0
  91. package/dist/components/selection-bar.js +65 -0
  92. package/dist/components/separator.js +3 -2
  93. package/dist/components/sheet.d.ts +1 -1
  94. package/dist/components/sheet.js +1 -1
  95. package/dist/components/skeleton-card.js +1 -1
  96. package/dist/components/skeleton.js +1 -1
  97. package/dist/components/slider.js +21 -4
  98. package/dist/components/sliding-panels.js +9 -6
  99. package/dist/components/spinner.d.ts +17 -0
  100. package/dist/components/spinner.js +50 -0
  101. package/dist/components/stepper.d.ts +18 -0
  102. package/dist/components/stepper.js +199 -0
  103. package/dist/components/switch.js +4 -3
  104. package/dist/components/table.d.ts +23 -5
  105. package/dist/components/table.js +121 -35
  106. package/dist/components/tabs.d.ts +3 -1
  107. package/dist/components/tabs.js +37 -9
  108. package/dist/components/tag-group.d.ts +9 -0
  109. package/dist/components/tag-group.js +26 -0
  110. package/dist/components/tag.d.ts +17 -0
  111. package/dist/components/tag.js +61 -0
  112. package/dist/components/text.d.ts +3 -3
  113. package/dist/components/text.js +1 -1
  114. package/dist/components/textarea.js +2 -1
  115. package/dist/components/toast.js +1 -1
  116. package/dist/components/toaster.js +1 -1
  117. package/dist/components/toggle-group.js +1 -1
  118. package/dist/components/toggle.js +1 -1
  119. package/dist/components/tooltip.d.ts +3 -2
  120. package/dist/components/tooltip.js +16 -2
  121. package/package.json +44 -26
  122. package/src/components/accordion.tsx +11 -6
  123. package/src/components/alert-dialog.tsx +4 -2
  124. package/src/components/alert.tsx +20 -8
  125. package/src/components/avatar.tsx +106 -13
  126. package/src/components/badge.tsx +12 -11
  127. package/src/components/breadcrumb.tsx +5 -2
  128. package/src/components/button-group.stories.tsx +52 -0
  129. package/src/components/button-group.tsx +40 -0
  130. package/src/components/button.test.tsx +174 -0
  131. package/src/components/button.tsx +42 -19
  132. package/src/components/calendar.tsx +2 -2
  133. package/src/components/card.tsx +55 -13
  134. package/src/components/carousel.tsx +85 -13
  135. package/src/components/checkbox.test.tsx +58 -0
  136. package/src/components/checkbox.tsx +8 -3
  137. package/src/components/circular-progress.stories.tsx +39 -0
  138. package/src/components/circular-progress.tsx +102 -0
  139. package/src/components/collapsible.stories.tsx +7 -3
  140. package/src/components/combobox.stories.tsx +82 -0
  141. package/src/components/combobox.test.tsx +118 -0
  142. package/src/components/combobox.tsx +136 -0
  143. package/src/components/command.test.tsx +91 -0
  144. package/src/components/command.tsx +38 -15
  145. package/src/components/currency-input.test.tsx +63 -0
  146. package/src/components/currency-input.tsx +84 -66
  147. package/src/components/date-input.stories.tsx +31 -0
  148. package/src/components/date-input.tsx +43 -0
  149. package/src/components/description-list.stories.tsx +62 -0
  150. package/src/components/description-list.tsx +68 -0
  151. package/src/components/dialog.test.tsx +84 -0
  152. package/src/components/dialog.tsx +64 -24
  153. package/src/components/drawer.tsx +72 -23
  154. package/src/components/dropdown-menu.test.tsx +180 -0
  155. package/src/components/dropdown-menu.tsx +31 -6
  156. package/src/components/field.stories.tsx +36 -0
  157. package/src/components/field.test.tsx +157 -0
  158. package/src/components/field.tsx +146 -0
  159. package/src/components/form.stories.tsx +3 -2
  160. package/src/components/form.test.tsx +95 -0
  161. package/src/components/form.tsx +9 -0
  162. package/src/components/haptic-button.tsx +1 -1
  163. package/src/components/input-group.stories.tsx +61 -0
  164. package/src/components/input-group.tsx +99 -0
  165. package/src/components/input.test.tsx +103 -0
  166. package/src/components/input.tsx +24 -7
  167. package/src/components/item.stories.tsx +52 -0
  168. package/src/components/item.tsx +115 -0
  169. package/src/components/kbd.stories.tsx +33 -0
  170. package/src/components/kbd.tsx +46 -0
  171. package/src/components/lazy-markdown.tsx +10 -7
  172. package/src/components/multi-select-combobox.test.tsx +101 -0
  173. package/src/components/multi-select-combobox.tsx +85 -82
  174. package/src/components/multi-select-menu.tsx +49 -41
  175. package/src/components/native-select.stories.tsx +52 -0
  176. package/src/components/native-select.tsx +45 -0
  177. package/src/components/pagination.tsx +64 -5
  178. package/src/components/popover.tsx +66 -3
  179. package/src/components/progress.tsx +27 -4
  180. package/src/components/radio-group.test.tsx +64 -0
  181. package/src/components/radio-group.tsx +11 -3
  182. package/src/components/rating.test.tsx +112 -0
  183. package/src/components/rating.tsx +97 -82
  184. package/src/components/search-bar.stories.tsx +1 -32
  185. package/src/components/search-bar.tsx +22 -59
  186. package/src/components/searchable-select.tsx +69 -73
  187. package/src/components/select.tsx +34 -19
  188. package/src/components/selection-bar.stories.tsx +37 -0
  189. package/src/components/selection-bar.tsx +65 -0
  190. package/src/components/separator.tsx +5 -2
  191. package/src/components/sheet.tsx +1 -1
  192. package/src/components/skeleton.tsx +2 -2
  193. package/src/components/slider.test.tsx +45 -0
  194. package/src/components/slider.tsx +31 -12
  195. package/src/components/sliding-panels.tsx +8 -5
  196. package/src/components/spinner.stories.tsx +35 -0
  197. package/src/components/spinner.tsx +50 -0
  198. package/src/components/stepper.stories.tsx +71 -0
  199. package/src/components/stepper.tsx +220 -0
  200. package/src/components/switch.test.tsx +52 -0
  201. package/src/components/switch.tsx +4 -3
  202. package/src/components/table.tsx +140 -35
  203. package/src/components/tabs.test.tsx +107 -0
  204. package/src/components/tabs.tsx +70 -23
  205. package/src/components/tag-group.stories.tsx +39 -0
  206. package/src/components/tag-group.tsx +28 -0
  207. package/src/components/tag.stories.tsx +50 -0
  208. package/src/components/tag.tsx +63 -0
  209. package/src/components/text.test.tsx +1 -1
  210. package/src/components/text.tsx +19 -3
  211. package/src/components/textarea.test.tsx +48 -0
  212. package/src/components/textarea.tsx +2 -1
  213. package/src/components/toast.tsx +4 -3
  214. package/src/components/toggle.tsx +2 -2
  215. package/src/components/tooltip.tsx +12 -2
  216. package/src/globals.css +126 -90
  217. package/dist/chunk-2BMJRXQX.js +0 -80
  218. package/dist/chunk-4AVS6CV4.js +0 -89
  219. package/dist/chunk-4UO73SKC.js +0 -26
  220. package/dist/chunk-CW24PLDW.js +0 -24
  221. package/dist/chunk-IQASCH2V.js +0 -25
  222. package/dist/chunk-NKHH23YK.js +0 -31
  223. package/dist/chunk-QJ54Z7TC.js +0 -83
@@ -5,7 +5,7 @@ import type { DialogProps } from "@radix-ui/react-dialog";
5
5
  import { Command as CommandPrimitive } from "cmdk";
6
6
  import { Search } from "lucide-react";
7
7
  import { cn } from "../lib/utils";
8
- import { Dialog, DialogContent } from "./dialog";
8
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./dialog";
9
9
 
10
10
  const Command = React.forwardRef<
11
11
  React.ElementRef<typeof CommandPrimitive>,
@@ -13,8 +13,9 @@ const Command = React.forwardRef<
13
13
  >(({ className, ...props }, ref) => (
14
14
  <CommandPrimitive
15
15
  ref={ref}
16
+ data-slot="command"
16
17
  className={cn(
17
- "flex h-full w-full flex-col overflow-hidden rounded-xl glass border border-border/50",
18
+ "flex h-full w-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground",
18
19
  className
19
20
  )}
20
21
  {...props}
@@ -22,12 +23,24 @@ const Command = React.forwardRef<
22
23
  ));
23
24
  Command.displayName = CommandPrimitive.displayName;
24
25
 
25
- type CommandDialogProps = DialogProps;
26
+ type CommandDialogProps = DialogProps & {
27
+ title?: string;
28
+ description?: string;
29
+ };
26
30
 
27
- const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
31
+ const CommandDialog = ({
32
+ title = "Command palette",
33
+ description = "Search for commands and actions",
34
+ children,
35
+ ...props
36
+ }: CommandDialogProps) => {
28
37
  return (
29
38
  <Dialog {...props}>
30
- <DialogContent className="overflow-hidden p-0 shadow-2xl">
39
+ <DialogContent className="overflow-hidden p-0">
40
+ <DialogHeader className="sr-only">
41
+ <DialogTitle>{title}</DialogTitle>
42
+ <DialogDescription>{description}</DialogDescription>
43
+ </DialogHeader>
31
44
  <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
32
45
  {children}
33
46
  </Command>
@@ -40,19 +53,18 @@ const CommandInput = React.forwardRef<
40
53
  React.ElementRef<typeof CommandPrimitive.Input>,
41
54
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
42
55
  >(({ className, ...props }, ref) => (
43
- <div className="flex items-center border-b border-border/50 px-3" cmdk-input-wrapper="">
44
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
56
+ <div className="flex items-center border-b border-border px-3" cmdk-input-wrapper="">
57
+ <Search className="mr-2 size-4 shrink-0 opacity-50" />
45
58
  <CommandPrimitive.Input
46
59
  ref={ref}
47
60
  className={cn(
48
- "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
61
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-base outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
49
62
  className
50
63
  )}
51
64
  {...props}
52
65
  />
53
66
  </div>
54
67
  ));
55
-
56
68
  CommandInput.displayName = CommandPrimitive.Input.displayName;
57
69
 
58
70
  const CommandList = React.forwardRef<
@@ -65,7 +77,6 @@ const CommandList = React.forwardRef<
65
77
  {...props}
66
78
  />
67
79
  ));
68
-
69
80
  CommandList.displayName = CommandPrimitive.List.displayName;
70
81
 
71
82
  const CommandEmpty = React.forwardRef<
@@ -74,13 +85,24 @@ const CommandEmpty = React.forwardRef<
74
85
  >(({ className, ...props }, ref) => (
75
86
  <CommandPrimitive.Empty
76
87
  ref={ref}
77
- className={cn("py-6 text-center text-sm", className)}
88
+ className={cn("py-6 text-center text-sm text-muted-foreground", className)}
78
89
  {...props}
79
90
  />
80
91
  ));
81
-
82
92
  CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
83
93
 
94
+ const CommandLoading = React.forwardRef<
95
+ React.ElementRef<typeof CommandPrimitive.Loading>,
96
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Loading>
97
+ >(({ className, ...props }, ref) => (
98
+ <CommandPrimitive.Loading
99
+ ref={ref}
100
+ className={cn("py-6 text-center text-sm text-muted-foreground", className)}
101
+ {...props}
102
+ />
103
+ ));
104
+ CommandLoading.displayName = "CommandLoading";
105
+
84
106
  const CommandGroup = React.forwardRef<
85
107
  React.ElementRef<typeof CommandPrimitive.Group>,
86
108
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
@@ -94,7 +116,6 @@ const CommandGroup = React.forwardRef<
94
116
  {...props}
95
117
  />
96
118
  ));
97
-
98
119
  CommandGroup.displayName = CommandPrimitive.Group.displayName;
99
120
 
100
121
  const CommandSeparator = React.forwardRef<
@@ -116,13 +137,14 @@ const CommandItem = React.forwardRef<
116
137
  <CommandPrimitive.Item
117
138
  ref={ref}
118
139
  className={cn(
119
- "relative flex cursor-default select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 transition-all duration-200 hover:bg-accent/50",
140
+ "relative flex cursor-default select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none transition-colors",
141
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
142
+ "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
120
143
  className
121
144
  )}
122
145
  {...props}
123
146
  />
124
147
  ));
125
-
126
148
  CommandItem.displayName = CommandPrimitive.Item.displayName;
127
149
 
128
150
  const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
@@ -141,6 +163,7 @@ export {
141
163
  CommandInput,
142
164
  CommandList,
143
165
  CommandEmpty,
166
+ CommandLoading,
144
167
  CommandGroup,
145
168
  CommandItem,
146
169
  CommandShortcut,
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { CurrencyInput } from "./currency-input";
5
+
6
+ describe("CurrencyInput", () => {
7
+ it("renders with the default `$` symbol", () => {
8
+ render(<CurrencyInput value={null} onChange={() => {}} />);
9
+ expect(screen.getByText("$")).toBeDefined();
10
+ });
11
+
12
+ it("renders a custom currency symbol", () => {
13
+ render(<CurrencyInput value={null} onChange={() => {}} currency="RON" />);
14
+ expect(screen.getByText("RON")).toBeDefined();
15
+ });
16
+
17
+ it("widens left padding for multi-char currency symbols", () => {
18
+ render(<CurrencyInput value={null} onChange={() => {}} currency="RON" />);
19
+ expect(screen.getByRole("textbox").className).toContain("pl-12");
20
+ });
21
+
22
+ it("formats the blurred value with locale separators", () => {
23
+ render(<CurrencyInput value={1234.5} onChange={() => {}} />);
24
+ expect(screen.getByRole("textbox")).toHaveValue("1,234.5");
25
+ });
26
+
27
+ it("fires onChange with a parsed number when typing", async () => {
28
+ const user = userEvent.setup();
29
+ const onChange = vi.fn();
30
+ render(<CurrencyInput value={null} onChange={onChange} />);
31
+ const input = screen.getByRole("textbox");
32
+ await user.type(input, "42");
33
+ expect(onChange).toHaveBeenLastCalledWith(42);
34
+ });
35
+
36
+ it("renders an error message and links it via aria-describedby", () => {
37
+ render(
38
+ <CurrencyInput value={null} onChange={() => {}} error="Required" id="amt" />
39
+ );
40
+ const input = screen.getByRole("textbox");
41
+ expect(input).toHaveAttribute("aria-invalid", "true");
42
+ expect(input).toHaveAttribute("aria-describedby", "amt-error");
43
+ expect(screen.getByText("Required").id).toBe("amt-error");
44
+ });
45
+
46
+ it("does not set aria-invalid when no error", () => {
47
+ render(<CurrencyInput value={null} onChange={() => {}} />);
48
+ expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-invalid");
49
+ });
50
+
51
+ it("blocks non-numeric character entry", async () => {
52
+ const user = userEvent.setup();
53
+ const onChange = vi.fn();
54
+ render(<CurrencyInput value={null} onChange={onChange} />);
55
+ await user.type(screen.getByRole("textbox"), "abc");
56
+ expect(onChange).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it("respects disabled", () => {
60
+ render(<CurrencyInput value={null} onChange={() => {}} disabled />);
61
+ expect(screen.getByRole("textbox")).toBeDisabled();
62
+ });
63
+ });
@@ -8,60 +8,79 @@ interface CurrencyInputProps {
8
8
  value: number | null;
9
9
  onChange: (value: number | null) => void;
10
10
  placeholder?: string;
11
- currency?: string; // default '$'
12
- locale?: string; // default 'en-US'
11
+ currency?: string;
12
+ locale?: string;
13
13
  min?: number;
14
14
  max?: number;
15
15
  disabled?: boolean;
16
16
  className?: string;
17
17
  error?: string;
18
+ id?: string;
18
19
  }
19
20
 
21
+ const ALLOWED_NAVIGATION_KEYS = new Set([
22
+ "Backspace",
23
+ "Tab",
24
+ "Escape",
25
+ "Enter",
26
+ "Delete",
27
+ "Home",
28
+ "End",
29
+ "ArrowLeft",
30
+ "ArrowRight",
31
+ "ArrowUp",
32
+ "ArrowDown",
33
+ ]);
34
+
35
+ const ALLOWED_SHORTCUT_KEYS = new Set(["a", "c", "v", "x", "z"]);
36
+
20
37
  const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
21
38
  (
22
39
  {
23
40
  value,
24
41
  onChange,
25
42
  placeholder = "Enter amount",
26
- currency: _currency = "$",
43
+ currency = "$",
27
44
  locale = "en-US",
28
45
  min,
29
46
  max,
30
47
  disabled = false,
31
48
  className,
32
49
  error,
50
+ id: providedId,
33
51
  ...props
34
52
  },
35
53
  ref
36
54
  ) => {
55
+ const reactId = React.useId();
56
+ const id = providedId ?? reactId;
57
+ const errorId = `${id}-error`;
58
+
37
59
  const [displayValue, setDisplayValue] = React.useState("");
38
60
  const [isFocused, setIsFocused] = React.useState(false);
39
61
 
40
- // Format number for display
41
- const formatNumber = (num: number | null): string => {
42
- if (num === null || isNaN(num)) return "";
43
- return new Intl.NumberFormat(locale, {
44
- minimumFractionDigits: 0,
45
- maximumFractionDigits: 2,
46
- }).format(num);
47
- };
62
+ const formatNumber = React.useCallback(
63
+ (num: number | null): string => {
64
+ if (num === null || isNaN(num)) return "";
65
+ return new Intl.NumberFormat(locale, {
66
+ minimumFractionDigits: 0,
67
+ maximumFractionDigits: 2,
68
+ }).format(num);
69
+ },
70
+ [locale]
71
+ );
48
72
 
49
- // Parse input value to number
50
73
  const parseInput = (input: string): number | null => {
51
- // Remove currency symbol and non-numeric characters except decimal point
52
74
  const cleaned = input.replace(/[^\d.-]/g, "");
53
75
  const parsed = parseFloat(cleaned);
54
76
  return isNaN(parsed) ? null : parsed;
55
77
  };
56
78
 
57
- // Update display value when prop value changes
58
79
  React.useEffect(() => {
59
80
  if (!isFocused) {
60
81
  setDisplayValue(formatNumber(value));
61
82
  }
62
- // formatNumber is inline and recreated each render; locale is the actual dep
63
- // eslint-disable-next-line react-hooks/exhaustive-deps
64
- }, [value, isFocused, locale]);
83
+ }, [value, isFocused, formatNumber]);
65
84
 
66
85
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
67
86
  const inputValue = e.target.value;
@@ -69,14 +88,9 @@ const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
69
88
 
70
89
  const parsedValue = parseInput(inputValue);
71
90
 
72
- // Validate min/max constraints
73
91
  if (parsedValue !== null) {
74
- if (min !== undefined && parsedValue < min) {
75
- return; // Don't update if below minimum
76
- }
77
- if (max !== undefined && parsedValue > max) {
78
- return; // Don't update if above maximum
79
- }
92
+ if (min !== undefined && parsedValue < min) return;
93
+ if (max !== undefined && parsedValue > max) return;
80
94
  }
81
95
 
82
96
  onChange(parsedValue);
@@ -84,7 +98,6 @@ const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
84
98
 
85
99
  const handleFocus = () => {
86
100
  setIsFocused(true);
87
- // Show raw number when focused for easier editing
88
101
  if (value !== null && value !== undefined && !isNaN(value)) {
89
102
  setDisplayValue(value.toString());
90
103
  }
@@ -92,7 +105,6 @@ const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
92
105
 
93
106
  const handleBlur = () => {
94
107
  setIsFocused(false);
95
- // Format the value when losing focus
96
108
  if (value !== null && value !== undefined && !isNaN(value)) {
97
109
  setDisplayValue(formatNumber(value));
98
110
  } else {
@@ -101,53 +113,59 @@ const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputProps>(
101
113
  };
102
114
 
103
115
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
104
- // Allow: backspace, delete, tab, escape, enter, home, end, left, right, up, down
105
- if ([8, 9, 27, 13, 46, 35, 36, 37, 38, 39, 40].includes(e.keyCode)) {
106
- return;
107
- }
116
+ if (ALLOWED_NAVIGATION_KEYS.has(e.key)) return;
108
117
 
109
- // Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X, Ctrl+Z
110
- if ((e.ctrlKey || e.metaKey) && [65, 67, 86, 88, 90].includes(e.keyCode)) {
111
- return;
112
- }
118
+ if ((e.ctrlKey || e.metaKey) && ALLOWED_SHORTCUT_KEYS.has(e.key.toLowerCase())) return;
113
119
 
114
- // Allow: numbers, decimal point, minus sign
115
- if (
116
- (e.keyCode >= 48 && e.keyCode <= 57) || // 0-9
117
- (e.keyCode >= 96 && e.keyCode <= 105) || // numpad 0-9
118
- e.keyCode === 190 ||
119
- e.keyCode === 110 || // decimal point
120
- e.keyCode === 189 ||
121
- e.keyCode === 109
122
- ) {
123
- // minus sign
124
- return;
125
- }
120
+ if (/^[0-9]$/.test(e.key)) return;
121
+
122
+ if (e.key === "." || e.key === "-") return;
126
123
 
127
- // Prevent all other keys
128
124
  e.preventDefault();
129
125
  };
130
126
 
127
+ const symbolLength = currency.length;
128
+ const inputPaddingClass =
129
+ symbolLength <= 1
130
+ ? "pl-8"
131
+ : symbolLength === 2
132
+ ? "pl-10"
133
+ : symbolLength === 3
134
+ ? "pl-12"
135
+ : "pl-14";
136
+
131
137
  return (
132
- <div className="relative">
133
- <Input
134
- ref={ref}
135
- type="text"
136
- value={displayValue}
137
- onChange={handleInputChange}
138
- onFocus={handleFocus}
139
- onBlur={handleBlur}
140
- onKeyDown={handleKeyDown}
141
- placeholder={placeholder}
142
- disabled={disabled}
143
- className={cn(
144
- "pl-8", // Space for currency symbol
145
- error && "border-destructive focus-visible:ring-destructive",
146
- className
147
- )}
148
- {...props}
149
- />
150
- {error && <p className="text-sm text-destructive mt-1 ml-1">{error}</p>}
138
+ <div className="w-full">
139
+ <div className="relative">
140
+ <span
141
+ aria-hidden
142
+ className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm font-medium text-muted-foreground"
143
+ >
144
+ {currency}
145
+ </span>
146
+ <Input
147
+ ref={ref}
148
+ id={id}
149
+ type="text"
150
+ inputMode="decimal"
151
+ value={displayValue}
152
+ onChange={handleInputChange}
153
+ onFocus={handleFocus}
154
+ onBlur={handleBlur}
155
+ onKeyDown={handleKeyDown}
156
+ placeholder={placeholder}
157
+ disabled={disabled}
158
+ aria-invalid={error ? true : undefined}
159
+ aria-describedby={error ? errorId : undefined}
160
+ className={cn(inputPaddingClass, className)}
161
+ {...props}
162
+ />
163
+ </div>
164
+ {error && (
165
+ <p id={errorId} className="ml-1 mt-1 text-sm text-destructive">
166
+ {error}
167
+ </p>
168
+ )}
151
169
  </div>
152
170
  );
153
171
  }
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { DateInput } from "./date-input";
3
+
4
+ const meta = {
5
+ title: "Components/DateInput",
6
+ component: DateInput,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof DateInput>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ render: () => <DateInput className="w-64" />,
15
+ };
16
+
17
+ export const Types: Story = {
18
+ render: () => (
19
+ <div className="flex w-64 flex-col gap-3">
20
+ <DateInput type="date" />
21
+ <DateInput type="datetime-local" />
22
+ <DateInput type="time" />
23
+ <DateInput type="month" />
24
+ <DateInput type="week" />
25
+ </div>
26
+ ),
27
+ };
28
+
29
+ export const WithoutIcon: Story = {
30
+ render: () => <DateInput className="w-64" showIcon={false} />,
31
+ };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Calendar as CalendarIcon } from "lucide-react";
5
+ import { Input } from "./input";
6
+ import { cn } from "../lib/utils";
7
+
8
+ type DateInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
9
+ /** Native input type. Defaults to "date". */
10
+ type?: "date" | "datetime-local" | "time" | "month" | "week";
11
+ size?: "sm" | "default" | "lg";
12
+ showIcon?: boolean;
13
+ };
14
+
15
+ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
16
+ ({ className, type = "date", size = "default", showIcon = true, ...props }, ref) => (
17
+ <div className="relative w-full">
18
+ {showIcon && (
19
+ <CalendarIcon
20
+ aria-hidden
21
+ className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
22
+ />
23
+ )}
24
+ <Input
25
+ ref={ref}
26
+ type={type}
27
+ data-slot="date-input"
28
+ size={size}
29
+ className={cn(
30
+ showIcon && "pl-9",
31
+ // Hide the native calendar picker indicator — we render our own icon.
32
+ // The trigger stays on the input itself so click-anywhere still opens the picker.
33
+ "[&::-webkit-calendar-picker-indicator]:opacity-0 [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:inset-0 [&::-webkit-calendar-picker-indicator]:w-full [&::-webkit-calendar-picker-indicator]:h-full [&::-webkit-calendar-picker-indicator]:cursor-pointer",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ </div>
39
+ )
40
+ );
41
+ DateInput.displayName = "DateInput";
42
+
43
+ export { DateInput };
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ DescriptionList,
4
+ DescriptionItem,
5
+ DescriptionTerm,
6
+ DescriptionDetail,
7
+ } from "./description-list";
8
+
9
+ const meta = {
10
+ title: "Components/DescriptionList",
11
+ component: DescriptionList,
12
+ tags: ["autodocs"],
13
+ } satisfies Meta<typeof DescriptionList>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const items = [
19
+ { term: "Name", detail: "Adrian Sterea" },
20
+ { term: "Role", detail: "Senior frontend engineer" },
21
+ { term: "Location", detail: "Bucharest, Romania" },
22
+ { term: "Status", detail: "Active" },
23
+ ];
24
+
25
+ export const Stacked: Story = {
26
+ render: () => (
27
+ <DescriptionList className="w-80">
28
+ {items.map((item) => (
29
+ <DescriptionItem key={item.term}>
30
+ <DescriptionTerm>{item.term}</DescriptionTerm>
31
+ <DescriptionDetail>{item.detail}</DescriptionDetail>
32
+ </DescriptionItem>
33
+ ))}
34
+ </DescriptionList>
35
+ ),
36
+ };
37
+
38
+ export const Grid: Story = {
39
+ render: () => (
40
+ <DescriptionList layout="grid" className="w-96">
41
+ {items.map((item) => (
42
+ <DescriptionItem key={item.term}>
43
+ <DescriptionTerm>{item.term}</DescriptionTerm>
44
+ <DescriptionDetail>{item.detail}</DescriptionDetail>
45
+ </DescriptionItem>
46
+ ))}
47
+ </DescriptionList>
48
+ ),
49
+ };
50
+
51
+ export const Inline: Story = {
52
+ render: () => (
53
+ <DescriptionList layout="inline">
54
+ {items.slice(0, 3).map((item) => (
55
+ <DescriptionItem key={item.term}>
56
+ <DescriptionTerm>{item.term}</DescriptionTerm>
57
+ <DescriptionDetail>{item.detail}</DescriptionDetail>
58
+ </DescriptionItem>
59
+ ))}
60
+ </DescriptionList>
61
+ ),
62
+ };
@@ -0,0 +1,68 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const descriptionListVariants = cva("text-sm", {
6
+ variants: {
7
+ layout: {
8
+ stacked: "flex flex-col gap-3 [&>[data-slot=description-item]]:flex [&>[data-slot=description-item]]:flex-col [&>[data-slot=description-item]]:gap-0.5",
9
+ grid: "grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 [&>[data-slot=description-item]]:contents [&_[data-slot=description-term]]:text-muted-foreground [&_[data-slot=description-detail]]:text-foreground",
10
+ inline: "flex flex-wrap items-center gap-x-4 gap-y-1 [&_[data-slot=description-term]]:after:content-[':'] [&_[data-slot=description-term]]:after:ml-0.5",
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ layout: "stacked",
15
+ },
16
+ });
17
+
18
+ const DescriptionList = React.forwardRef<
19
+ HTMLDListElement,
20
+ React.HTMLAttributes<HTMLDListElement> & VariantProps<typeof descriptionListVariants>
21
+ >(({ className, layout, ...props }, ref) => (
22
+ <dl
23
+ ref={ref}
24
+ data-slot="description-list"
25
+ className={cn(descriptionListVariants({ layout }), className)}
26
+ {...props}
27
+ />
28
+ ));
29
+ DescriptionList.displayName = "DescriptionList";
30
+
31
+ const DescriptionItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
32
+ ({ className, ...props }, ref) => (
33
+ <div ref={ref} data-slot="description-item" className={cn(className)} {...props} />
34
+ )
35
+ );
36
+ DescriptionItem.displayName = "DescriptionItem";
37
+
38
+ const DescriptionTerm = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
39
+ ({ className, ...props }, ref) => (
40
+ <dt
41
+ ref={ref as React.Ref<HTMLElement>}
42
+ data-slot="description-term"
43
+ className={cn("text-xs font-medium uppercase tracking-wide text-muted-foreground", className)}
44
+ {...props}
45
+ />
46
+ )
47
+ );
48
+ DescriptionTerm.displayName = "DescriptionTerm";
49
+
50
+ const DescriptionDetail = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
51
+ ({ className, ...props }, ref) => (
52
+ <dd
53
+ ref={ref as React.Ref<HTMLElement>}
54
+ data-slot="description-detail"
55
+ className={cn("text-sm text-foreground", className)}
56
+ {...props}
57
+ />
58
+ )
59
+ );
60
+ DescriptionDetail.displayName = "DescriptionDetail";
61
+
62
+ export {
63
+ DescriptionList,
64
+ DescriptionItem,
65
+ DescriptionTerm,
66
+ DescriptionDetail,
67
+ descriptionListVariants,
68
+ };