@bearmenu/ui 0.5.2 → 0.6.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 (227) 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 +4 -1
  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 +3 -1
  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 +16 -3
  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/scroll-fade.d.ts +15 -0
  86. package/dist/components/scroll-fade.js +116 -0
  87. package/dist/components/search-bar.d.ts +1 -2
  88. package/dist/components/search-bar.js +37 -60
  89. package/dist/components/searchable-select.js +76 -72
  90. package/dist/components/select.d.ts +5 -1
  91. package/dist/components/select.js +30 -14
  92. package/dist/components/selection-bar.d.ts +11 -0
  93. package/dist/components/selection-bar.js +65 -0
  94. package/dist/components/separator.js +3 -2
  95. package/dist/components/sheet.d.ts +1 -1
  96. package/dist/components/sheet.js +1 -1
  97. package/dist/components/skeleton-card.js +1 -1
  98. package/dist/components/skeleton.js +1 -1
  99. package/dist/components/slider.js +21 -4
  100. package/dist/components/sliding-panels.js +9 -6
  101. package/dist/components/spinner.d.ts +17 -0
  102. package/dist/components/spinner.js +50 -0
  103. package/dist/components/stepper.d.ts +18 -0
  104. package/dist/components/stepper.js +199 -0
  105. package/dist/components/switch.js +4 -3
  106. package/dist/components/table.d.ts +23 -5
  107. package/dist/components/table.js +121 -35
  108. package/dist/components/tabs.d.ts +3 -1
  109. package/dist/components/tabs.js +37 -9
  110. package/dist/components/tag-group.d.ts +9 -0
  111. package/dist/components/tag-group.js +26 -0
  112. package/dist/components/tag.d.ts +17 -0
  113. package/dist/components/tag.js +61 -0
  114. package/dist/components/text.d.ts +1 -1
  115. package/dist/components/text.js +1 -1
  116. package/dist/components/textarea.js +2 -1
  117. package/dist/components/toast.js +1 -1
  118. package/dist/components/toaster.js +1 -1
  119. package/dist/components/toggle-group.js +1 -1
  120. package/dist/components/toggle.js +1 -1
  121. package/dist/components/tooltip.d.ts +3 -2
  122. package/dist/components/tooltip.js +16 -2
  123. package/package.json +45 -26
  124. package/src/components/accordion.tsx +11 -6
  125. package/src/components/alert-dialog.tsx +4 -2
  126. package/src/components/alert.tsx +20 -8
  127. package/src/components/avatar.tsx +106 -13
  128. package/src/components/badge.tsx +12 -11
  129. package/src/components/breadcrumb.tsx +5 -2
  130. package/src/components/button-group.stories.tsx +52 -0
  131. package/src/components/button-group.tsx +40 -0
  132. package/src/components/button.test.tsx +174 -0
  133. package/src/components/button.tsx +42 -19
  134. package/src/components/calendar.tsx +2 -2
  135. package/src/components/card.tsx +55 -13
  136. package/src/components/carousel.tsx +85 -13
  137. package/src/components/checkbox.test.tsx +58 -0
  138. package/src/components/checkbox.tsx +8 -3
  139. package/src/components/circular-progress.stories.tsx +39 -0
  140. package/src/components/circular-progress.tsx +102 -0
  141. package/src/components/collapsible.stories.tsx +7 -3
  142. package/src/components/combobox.stories.tsx +82 -0
  143. package/src/components/combobox.test.tsx +118 -0
  144. package/src/components/combobox.tsx +136 -0
  145. package/src/components/command.test.tsx +91 -0
  146. package/src/components/command.tsx +38 -15
  147. package/src/components/currency-input.test.tsx +63 -0
  148. package/src/components/currency-input.tsx +84 -66
  149. package/src/components/date-input.stories.tsx +31 -0
  150. package/src/components/date-input.tsx +43 -0
  151. package/src/components/description-list.stories.tsx +62 -0
  152. package/src/components/description-list.tsx +68 -0
  153. package/src/components/dialog.test.tsx +84 -0
  154. package/src/components/dialog.tsx +64 -24
  155. package/src/components/drawer.tsx +72 -23
  156. package/src/components/dropdown-menu.test.tsx +180 -0
  157. package/src/components/dropdown-menu.tsx +31 -6
  158. package/src/components/field.stories.tsx +36 -0
  159. package/src/components/field.test.tsx +157 -0
  160. package/src/components/field.tsx +146 -0
  161. package/src/components/form.stories.tsx +3 -2
  162. package/src/components/form.test.tsx +95 -0
  163. package/src/components/form.tsx +9 -0
  164. package/src/components/haptic-button.tsx +1 -1
  165. package/src/components/input-group.stories.tsx +61 -0
  166. package/src/components/input-group.tsx +99 -0
  167. package/src/components/input.test.tsx +103 -0
  168. package/src/components/input.tsx +24 -7
  169. package/src/components/item.stories.tsx +52 -0
  170. package/src/components/item.tsx +115 -0
  171. package/src/components/kbd.stories.tsx +33 -0
  172. package/src/components/kbd.tsx +46 -0
  173. package/src/components/lazy-markdown.tsx +10 -7
  174. package/src/components/multi-select-combobox.test.tsx +101 -0
  175. package/src/components/multi-select-combobox.tsx +85 -82
  176. package/src/components/multi-select-menu.tsx +49 -41
  177. package/src/components/native-select.stories.tsx +52 -0
  178. package/src/components/native-select.tsx +45 -0
  179. package/src/components/pagination.tsx +64 -5
  180. package/src/components/popover.tsx +66 -3
  181. package/src/components/progress.tsx +27 -4
  182. package/src/components/radio-group.test.tsx +64 -0
  183. package/src/components/radio-group.tsx +11 -3
  184. package/src/components/rating.test.tsx +112 -0
  185. package/src/components/rating.tsx +97 -82
  186. package/src/components/scroll-fade.stories.tsx +98 -0
  187. package/src/components/scroll-fade.tsx +127 -0
  188. package/src/components/search-bar.stories.tsx +1 -32
  189. package/src/components/search-bar.tsx +22 -59
  190. package/src/components/searchable-select.tsx +69 -73
  191. package/src/components/select.tsx +34 -19
  192. package/src/components/selection-bar.stories.tsx +37 -0
  193. package/src/components/selection-bar.tsx +65 -0
  194. package/src/components/separator.tsx +5 -2
  195. package/src/components/sheet.tsx +1 -1
  196. package/src/components/skeleton.tsx +2 -2
  197. package/src/components/slider.test.tsx +45 -0
  198. package/src/components/slider.tsx +31 -12
  199. package/src/components/sliding-panels.tsx +8 -5
  200. package/src/components/spinner.stories.tsx +35 -0
  201. package/src/components/spinner.tsx +50 -0
  202. package/src/components/stepper.stories.tsx +71 -0
  203. package/src/components/stepper.tsx +220 -0
  204. package/src/components/switch.test.tsx +52 -0
  205. package/src/components/switch.tsx +4 -3
  206. package/src/components/table.tsx +140 -35
  207. package/src/components/tabs.test.tsx +107 -0
  208. package/src/components/tabs.tsx +70 -23
  209. package/src/components/tag-group.stories.tsx +39 -0
  210. package/src/components/tag-group.tsx +28 -0
  211. package/src/components/tag.stories.tsx +50 -0
  212. package/src/components/tag.tsx +63 -0
  213. package/src/components/text.test.tsx +1 -1
  214. package/src/components/text.tsx +19 -3
  215. package/src/components/textarea.test.tsx +48 -0
  216. package/src/components/textarea.tsx +2 -1
  217. package/src/components/toast.tsx +4 -3
  218. package/src/components/toggle.tsx +2 -2
  219. package/src/components/tooltip.tsx +12 -2
  220. package/src/globals.css +126 -90
  221. package/dist/chunk-2BMJRXQX.js +0 -80
  222. package/dist/chunk-4AVS6CV4.js +0 -89
  223. package/dist/chunk-4UO73SKC.js +0 -26
  224. package/dist/chunk-CW24PLDW.js +0 -24
  225. package/dist/chunk-IQASCH2V.js +0 -25
  226. package/dist/chunk-NKHH23YK.js +0 -31
  227. package/dist/chunk-QJ54Z7TC.js +0 -83
@@ -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
+ };
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogTrigger,
12
+ DialogClose,
13
+ } from "./dialog";
14
+
15
+ function TestDialog({ defaultOpen, showCloseButton }: { defaultOpen?: boolean; showCloseButton?: boolean }) {
16
+ return (
17
+ <Dialog defaultOpen={defaultOpen}>
18
+ <DialogTrigger>Open</DialogTrigger>
19
+ <DialogContent showCloseButton={showCloseButton}>
20
+ <DialogHeader>
21
+ <DialogTitle>Title</DialogTitle>
22
+ <DialogDescription>Description</DialogDescription>
23
+ </DialogHeader>
24
+ <DialogFooter>
25
+ <DialogClose data-testid="footer-close">Dismiss</DialogClose>
26
+ </DialogFooter>
27
+ </DialogContent>
28
+ </Dialog>
29
+ );
30
+ }
31
+
32
+ describe("Dialog", () => {
33
+ it("is closed by default", () => {
34
+ render(<TestDialog />);
35
+ expect(screen.queryByRole("dialog")).toBeNull();
36
+ });
37
+
38
+ it("opens on trigger click", async () => {
39
+ const user = userEvent.setup();
40
+ render(<TestDialog />);
41
+ await user.click(screen.getByText("Open"));
42
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
43
+ });
44
+
45
+ it("renders title and description with aria links", () => {
46
+ render(<TestDialog defaultOpen />);
47
+ const dialog = screen.getByRole("dialog");
48
+ const title = screen.getByText("Title");
49
+ const desc = screen.getByText("Description");
50
+ expect(dialog.getAttribute("aria-labelledby")).toBe(title.id);
51
+ expect(dialog.getAttribute("aria-describedby")).toBe(desc.id);
52
+ });
53
+
54
+ it("renders close button by default", () => {
55
+ render(<TestDialog defaultOpen />);
56
+ expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument();
57
+ });
58
+
59
+ it("hides X close button when showCloseButton=false", () => {
60
+ render(<TestDialog defaultOpen showCloseButton={false} />);
61
+ expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
62
+ expect(screen.getByTestId("footer-close")).toBeInTheDocument();
63
+ });
64
+
65
+ it("closes on Escape key", async () => {
66
+ const user = userEvent.setup();
67
+ render(<TestDialog defaultOpen />);
68
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
69
+ await user.keyboard("{Escape}");
70
+ expect(screen.queryByRole("dialog")).toBeNull();
71
+ });
72
+
73
+ it("closes when DialogClose is clicked", async () => {
74
+ const user = userEvent.setup();
75
+ render(<TestDialog defaultOpen />);
76
+ await user.click(screen.getByTestId("footer-close"));
77
+ expect(screen.queryByRole("dialog")).toBeNull();
78
+ });
79
+
80
+ it("applies data-slot attributes for styling hooks", () => {
81
+ render(<TestDialog defaultOpen />);
82
+ expect(screen.getByRole("dialog").getAttribute("data-slot")).toBe("dialog-content");
83
+ });
84
+ });
@@ -2,15 +2,13 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { cva, type VariantProps } from "class-variance-authority";
5
6
  import { X } from "lucide-react";
6
7
  import { cn } from "../lib/utils";
7
8
 
8
9
  const Dialog = DialogPrimitive.Root;
9
-
10
10
  const DialogTrigger = DialogPrimitive.Trigger;
11
-
12
11
  const DialogPortal = DialogPrimitive.Portal;
13
-
14
12
  const DialogClose = DialogPrimitive.Close;
15
13
 
16
14
  const DialogOverlay = React.forwardRef<
@@ -19,6 +17,7 @@ const DialogOverlay = React.forwardRef<
19
17
  >(({ className, ...props }, ref) => (
20
18
  <DialogPrimitive.Overlay
21
19
  ref={ref}
20
+ data-slot="dialog-overlay"
22
21
  className={cn(
23
22
  "fixed inset-0 z-[1000] bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
24
23
  className
@@ -28,37 +27,75 @@ const DialogOverlay = React.forwardRef<
28
27
  ));
29
28
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
30
29
 
30
+ const dialogContentVariants = cva(
31
+ "fixed left-[50%] top-[50%] z-[1000] grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 bg-card p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-2xl border border-border",
32
+ {
33
+ variants: {
34
+ size: {
35
+ sm: "max-w-sm",
36
+ default: "max-w-lg",
37
+ lg: "max-w-2xl",
38
+ xl: "max-w-4xl",
39
+ full: "max-w-[95vw] max-h-[95vh]",
40
+ },
41
+ scrollBehavior: {
42
+ inside: "max-h-[85vh] overflow-hidden",
43
+ outside: "",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ size: "default",
48
+ scrollBehavior: "outside",
49
+ },
50
+ }
51
+ );
52
+
53
+ type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
54
+ VariantProps<typeof dialogContentVariants> & {
55
+ showCloseButton?: boolean;
56
+ };
57
+
31
58
  const DialogContent = React.forwardRef<
32
59
  React.ElementRef<typeof DialogPrimitive.Content>,
33
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
34
- >(({ className, children, ...props }, ref) => (
35
- <DialogPortal>
36
- <DialogOverlay />
37
- <DialogPrimitive.Content
38
- ref={ref}
39
- className={cn(
40
- "fixed left-[50%] top-[50%] z-[1000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 bg-card p-6 shadow-2xl duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-2xl border border-border",
41
- className
42
- )}
43
- {...props}
44
- >
45
- {children}
46
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-muted p-2">
47
- <X className="h-4 w-4" />
48
- <span className="sr-only">Close</span>
49
- </DialogPrimitive.Close>
50
- </DialogPrimitive.Content>
51
- </DialogPortal>
52
- ));
60
+ DialogContentProps
61
+ >(
62
+ (
63
+ { className, children, size, scrollBehavior, showCloseButton = true, ...props },
64
+ ref
65
+ ) => (
66
+ <DialogPortal>
67
+ <DialogOverlay />
68
+ <DialogPrimitive.Content
69
+ ref={ref}
70
+ data-slot="dialog-content"
71
+ className={cn(dialogContentVariants({ size, scrollBehavior }), className)}
72
+ {...props}
73
+ >
74
+ {children}
75
+ {showCloseButton && (
76
+ <DialogPrimitive.Close className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground opacity-70 transition-opacity hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent">
77
+ <X className="size-4" />
78
+ <span className="sr-only">Close</span>
79
+ </DialogPrimitive.Close>
80
+ )}
81
+ </DialogPrimitive.Content>
82
+ </DialogPortal>
83
+ )
84
+ );
53
85
  DialogContent.displayName = DialogPrimitive.Content.displayName;
54
86
 
55
87
  const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
56
- <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
88
+ <div
89
+ data-slot="dialog-header"
90
+ className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
91
+ {...props}
92
+ />
57
93
  );
58
94
  DialogHeader.displayName = "DialogHeader";
59
95
 
60
96
  const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
61
97
  <div
98
+ data-slot="dialog-footer"
62
99
  className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
63
100
  {...props}
64
101
  />
@@ -71,6 +108,7 @@ const DialogTitle = React.forwardRef<
71
108
  >(({ className, ...props }, ref) => (
72
109
  <DialogPrimitive.Title
73
110
  ref={ref}
111
+ data-slot="dialog-title"
74
112
  className={cn("text-lg font-semibold leading-none tracking-tight", className)}
75
113
  {...props}
76
114
  />
@@ -83,6 +121,7 @@ const DialogDescription = React.forwardRef<
83
121
  >(({ className, ...props }, ref) => (
84
122
  <DialogPrimitive.Description
85
123
  ref={ref}
124
+ data-slot="dialog-description"
86
125
  className={cn("text-sm text-muted-foreground", className)}
87
126
  {...props}
88
127
  />
@@ -100,4 +139,5 @@ export {
100
139
  DialogFooter,
101
140
  DialogTitle,
102
141
  DialogDescription,
142
+ dialogContentVariants,
103
143
  };