@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.
- package/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
- package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
- package/dist/chunk-D5QHEHXJ.js +26 -0
- package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
- package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
- package/dist/chunk-RIDXFY6M.js +38 -0
- package/dist/chunk-UUJVXN2O.js +82 -0
- package/dist/chunk-WKMLZDUT.js +122 -0
- package/dist/chunk-XK2ZDQVE.js +122 -0
- package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
- package/dist/chunk-YJ6OBKZJ.js +32 -0
- package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
- package/dist/chunk-ZFNOCP6W.js +106 -0
- package/dist/components/accordion.js +11 -5
- package/dist/components/alert-dialog.js +5 -3
- package/dist/components/alert.d.ts +4 -1
- package/dist/components/alert.js +25 -9
- package/dist/components/avatar.d.ts +17 -2
- package/dist/components/avatar.js +91 -12
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/breadcrumb.js +5 -2
- package/dist/components/button-group.d.ts +14 -0
- package/dist/components/button-group.js +39 -0
- package/dist/components/button.d.ts +3 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.d.ts +13 -5
- package/dist/components/card.js +2 -2
- package/dist/components/carousel.d.ts +20 -3
- package/dist/components/carousel.js +72 -12
- package/dist/components/checkbox.js +1 -1
- package/dist/components/circular-progress.d.ts +16 -0
- package/dist/components/circular-progress.js +105 -0
- package/dist/components/combobox.d.ts +27 -0
- package/dist/components/combobox.js +102 -0
- package/dist/components/command.d.ts +16 -3
- package/dist/components/command.js +2 -2
- package/dist/components/currency-input.d.ts +1 -0
- package/dist/components/currency-input.js +73 -52
- package/dist/components/date-input.d.ts +10 -0
- package/dist/components/date-input.js +40 -0
- package/dist/components/description-list.d.ts +15 -0
- package/dist/components/description-list.js +67 -0
- package/dist/components/dialog.d.ts +13 -2
- package/dist/components/dialog.js +1 -1
- package/dist/components/drawer.d.ts +5 -2
- package/dist/components/drawer.js +47 -9
- package/dist/components/dropdown-menu.d.ts +7 -2
- package/dist/components/dropdown-menu.js +22 -6
- package/dist/components/empty-state.js +2 -2
- package/dist/components/field.d.ts +22 -0
- package/dist/components/field.js +130 -0
- package/dist/components/form.d.ts +2 -1
- package/dist/components/form.js +11 -1
- package/dist/components/haptic-button.js +3 -3
- package/dist/components/input-group.d.ts +18 -0
- package/dist/components/input-group.js +92 -0
- package/dist/components/input.d.ts +8 -2
- package/dist/components/input.js +1 -1
- package/dist/components/item.d.ts +23 -0
- package/dist/components/item.js +120 -0
- package/dist/components/kbd.d.ts +13 -0
- package/dist/components/kbd.js +49 -0
- package/dist/components/lazy-markdown.d.ts +3 -1
- package/dist/components/lazy-markdown.js +7 -7
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +92 -83
- package/dist/components/multi-select-menu.d.ts +0 -4
- package/dist/components/multi-select-menu.js +28 -17
- package/dist/components/native-select.d.ts +12 -0
- package/dist/components/native-select.js +49 -0
- package/dist/components/pagination.d.ts +5 -2
- package/dist/components/pagination.js +66 -7
- package/dist/components/place-about-tab.js +4 -4
- package/dist/components/place-card.js +1 -1
- package/dist/components/place-details-mobile.js +4 -4
- package/dist/components/popover.d.ts +7 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/progress.d.ts +5 -1
- package/dist/components/progress.js +17 -3
- package/dist/components/radio-group.js +12 -3
- package/dist/components/rating.d.ts +2 -0
- package/dist/components/rating.js +88 -70
- package/dist/components/scroll-fade.d.ts +15 -0
- package/dist/components/scroll-fade.js +116 -0
- package/dist/components/search-bar.d.ts +1 -2
- package/dist/components/search-bar.js +37 -60
- package/dist/components/searchable-select.js +76 -72
- package/dist/components/select.d.ts +5 -1
- package/dist/components/select.js +30 -14
- package/dist/components/selection-bar.d.ts +11 -0
- package/dist/components/selection-bar.js +65 -0
- package/dist/components/separator.js +3 -2
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/slider.js +21 -4
- package/dist/components/sliding-panels.js +9 -6
- package/dist/components/spinner.d.ts +17 -0
- package/dist/components/spinner.js +50 -0
- package/dist/components/stepper.d.ts +18 -0
- package/dist/components/stepper.js +199 -0
- package/dist/components/switch.js +4 -3
- package/dist/components/table.d.ts +23 -5
- package/dist/components/table.js +121 -35
- package/dist/components/tabs.d.ts +3 -1
- package/dist/components/tabs.js +37 -9
- package/dist/components/tag-group.d.ts +9 -0
- package/dist/components/tag-group.js +26 -0
- package/dist/components/tag.d.ts +17 -0
- package/dist/components/tag.js +61 -0
- package/dist/components/text.d.ts +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/textarea.js +2 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +3 -2
- package/dist/components/tooltip.js +16 -2
- package/package.json +45 -26
- package/src/components/accordion.tsx +11 -6
- package/src/components/alert-dialog.tsx +4 -2
- package/src/components/alert.tsx +20 -8
- package/src/components/avatar.tsx +106 -13
- package/src/components/badge.tsx +12 -11
- package/src/components/breadcrumb.tsx +5 -2
- package/src/components/button-group.stories.tsx +52 -0
- package/src/components/button-group.tsx +40 -0
- package/src/components/button.test.tsx +174 -0
- package/src/components/button.tsx +42 -19
- package/src/components/calendar.tsx +2 -2
- package/src/components/card.tsx +55 -13
- package/src/components/carousel.tsx +85 -13
- package/src/components/checkbox.test.tsx +58 -0
- package/src/components/checkbox.tsx +8 -3
- package/src/components/circular-progress.stories.tsx +39 -0
- package/src/components/circular-progress.tsx +102 -0
- package/src/components/collapsible.stories.tsx +7 -3
- package/src/components/combobox.stories.tsx +82 -0
- package/src/components/combobox.test.tsx +118 -0
- package/src/components/combobox.tsx +136 -0
- package/src/components/command.test.tsx +91 -0
- package/src/components/command.tsx +38 -15
- package/src/components/currency-input.test.tsx +63 -0
- package/src/components/currency-input.tsx +84 -66
- package/src/components/date-input.stories.tsx +31 -0
- package/src/components/date-input.tsx +43 -0
- package/src/components/description-list.stories.tsx +62 -0
- package/src/components/description-list.tsx +68 -0
- package/src/components/dialog.test.tsx +84 -0
- package/src/components/dialog.tsx +64 -24
- package/src/components/drawer.tsx +72 -23
- package/src/components/dropdown-menu.test.tsx +180 -0
- package/src/components/dropdown-menu.tsx +31 -6
- package/src/components/field.stories.tsx +36 -0
- package/src/components/field.test.tsx +157 -0
- package/src/components/field.tsx +146 -0
- package/src/components/form.stories.tsx +3 -2
- package/src/components/form.test.tsx +95 -0
- package/src/components/form.tsx +9 -0
- package/src/components/haptic-button.tsx +1 -1
- package/src/components/input-group.stories.tsx +61 -0
- package/src/components/input-group.tsx +99 -0
- package/src/components/input.test.tsx +103 -0
- package/src/components/input.tsx +24 -7
- package/src/components/item.stories.tsx +52 -0
- package/src/components/item.tsx +115 -0
- package/src/components/kbd.stories.tsx +33 -0
- package/src/components/kbd.tsx +46 -0
- package/src/components/lazy-markdown.tsx +10 -7
- package/src/components/multi-select-combobox.test.tsx +101 -0
- package/src/components/multi-select-combobox.tsx +85 -82
- package/src/components/multi-select-menu.tsx +49 -41
- package/src/components/native-select.stories.tsx +52 -0
- package/src/components/native-select.tsx +45 -0
- package/src/components/pagination.tsx +64 -5
- package/src/components/popover.tsx +66 -3
- package/src/components/progress.tsx +27 -4
- package/src/components/radio-group.test.tsx +64 -0
- package/src/components/radio-group.tsx +11 -3
- package/src/components/rating.test.tsx +112 -0
- package/src/components/rating.tsx +97 -82
- package/src/components/scroll-fade.stories.tsx +98 -0
- package/src/components/scroll-fade.tsx +127 -0
- package/src/components/search-bar.stories.tsx +1 -32
- package/src/components/search-bar.tsx +22 -59
- package/src/components/searchable-select.tsx +69 -73
- package/src/components/select.tsx +34 -19
- package/src/components/selection-bar.stories.tsx +37 -0
- package/src/components/selection-bar.tsx +65 -0
- package/src/components/separator.tsx +5 -2
- package/src/components/sheet.tsx +1 -1
- package/src/components/skeleton.tsx +2 -2
- package/src/components/slider.test.tsx +45 -0
- package/src/components/slider.tsx +31 -12
- package/src/components/sliding-panels.tsx +8 -5
- package/src/components/spinner.stories.tsx +35 -0
- package/src/components/spinner.tsx +50 -0
- package/src/components/stepper.stories.tsx +71 -0
- package/src/components/stepper.tsx +220 -0
- package/src/components/switch.test.tsx +52 -0
- package/src/components/switch.tsx +4 -3
- package/src/components/table.tsx +140 -35
- package/src/components/tabs.test.tsx +107 -0
- package/src/components/tabs.tsx +70 -23
- package/src/components/tag-group.stories.tsx +39 -0
- package/src/components/tag-group.tsx +28 -0
- package/src/components/tag.stories.tsx +50 -0
- package/src/components/tag.tsx +63 -0
- package/src/components/text.test.tsx +1 -1
- package/src/components/text.tsx +19 -3
- package/src/components/textarea.test.tsx +48 -0
- package/src/components/textarea.tsx +2 -1
- package/src/components/toast.tsx +4 -3
- package/src/components/toggle.tsx +2 -2
- package/src/components/tooltip.tsx +12 -2
- package/src/globals.css +126 -90
- package/dist/chunk-2BMJRXQX.js +0 -80
- package/dist/chunk-4AVS6CV4.js +0 -89
- package/dist/chunk-4UO73SKC.js +0 -26
- package/dist/chunk-CW24PLDW.js +0 -24
- package/dist/chunk-IQASCH2V.js +0 -25
- package/dist/chunk-NKHH23YK.js +0 -31
- 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;
|
|
12
|
-
locale?: string;
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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="
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
34
|
-
>(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
};
|