@bearmenu/ui 0.5.1 → 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.
- 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 +5 -2
- 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 +4 -2
- 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 +29 -16
- 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/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 +15 -13
- 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/sticky-container.js +1 -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 +3 -3
- 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 +44 -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/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 +14 -12
- 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/sticky-container.tsx +1 -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 +142 -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
|
@@ -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
|
|
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 = ({
|
|
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
|
|
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
|
|
44
|
-
<Search className="mr-2
|
|
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-
|
|
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
|
|
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;
|
|
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
|
+
};
|