@elabs-ai/components-ui 4.1.0 → 4.2.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-JKPPTL63.js → chunk-FCH4ZN6G.js} +12 -3
- package/dist/chunk-FCH4ZN6G.js.map +1 -0
- package/dist/form.js +3 -3
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +126 -35
- package/dist/index.js +3346 -2079
- package/dist/index.js.map +1 -1
- package/dist/lib/cn.js +10 -1
- package/dist/lib/cn.js.map +1 -1
- package/package.json +3 -3
- package/src/__contract__/attribution-panel.contract.test.tsx +49 -0
- package/src/__contract__/badge.contract.test.tsx +49 -0
- package/src/__contract__/bounded-number.contract.test.tsx +49 -0
- package/src/__contract__/button.contract.test.tsx +49 -0
- package/src/__contract__/color-picker.contract.test.tsx +49 -0
- package/src/__contract__/combobox.contract.test.tsx +49 -0
- package/src/__contract__/command-trigger.contract.test.tsx +49 -0
- package/src/__contract__/copyable-value.contract.test.tsx +49 -0
- package/src/__contract__/date-picker.contract.test.tsx +49 -0
- package/src/__contract__/date-range-picker.contract.test.tsx +49 -0
- package/src/__contract__/empty-state.contract.test.tsx +49 -0
- package/src/__contract__/error-state.contract.test.tsx +49 -0
- package/src/__contract__/icon-button.contract.test.tsx +49 -0
- package/src/__contract__/input.contract.test.tsx +49 -0
- package/src/__contract__/key-value-editor.contract.test.tsx +49 -0
- package/src/__contract__/list-editor.contract.test.tsx +49 -0
- package/src/__contract__/loading-state.contract.test.tsx +49 -0
- package/src/__contract__/metric-card.contract.test.tsx +49 -0
- package/src/__contract__/model-picker.contract.test.tsx +49 -0
- package/src/__contract__/nav-notifications.contract.test.tsx +49 -0
- package/src/__contract__/number-input.contract.test.tsx +49 -0
- package/src/__contract__/rating.contract.test.tsx +49 -0
- package/src/__contract__/schema-form.contract.test.tsx +49 -0
- package/src/__contract__/section-header.contract.test.tsx +49 -0
- package/src/__contract__/segmented-field.contract.test.tsx +49 -0
- package/src/__contract__/slider-number.contract.test.tsx +49 -0
- package/src/__contract__/status-badge.contract.test.tsx +49 -0
- package/src/__contract__/tag-input.contract.test.tsx +49 -0
- package/src/__contract__/text.contract.test.tsx +49 -0
- package/src/__contract__/textarea.contract.test.tsx +49 -0
- package/src/__contract__/timeline.contract.test.tsx +49 -0
- package/src/__contract__/toggle.contract.test.tsx +49 -0
- package/src/__contract__/transfer.contract.test.tsx +49 -0
- package/src/__contract__/tree-select.contract.test.tsx +49 -0
- package/src/__contract__/tree.contract.test.tsx +49 -0
- package/src/__contract__/virtual-select.contract.test.tsx +49 -0
- package/src/__contract__/workspace-picker.contract.test.tsx +49 -0
- package/src/components/accordion/accordion.tsx +13 -4
- package/src/components/alert/alert.tsx +11 -3
- package/src/components/alert-dialog/alert-dialog.test.tsx +32 -0
- package/src/components/alert-dialog/alert-dialog.tsx +28 -6
- package/src/components/app-shell/app-shell.test.tsx +99 -0
- package/src/components/app-shell/app-shell.tsx +29 -10
- package/src/components/attribution-panel/attribution-panel.tsx +2 -0
- package/src/components/attribution-panel/attributions.generated.ts +2 -2
- package/src/components/avatar/avatar.tsx +4 -1
- package/src/components/badge/badge.test.tsx +8 -0
- package/src/components/badge/badge.tsx +14 -4
- package/src/components/bento-grid/bento-grid.test.tsx +23 -2
- package/src/components/bento-grid/bento-grid.tsx +10 -0
- package/src/components/bounded-number/bounded-number.test.tsx +1 -1
- package/src/components/bounded-number/bounded-number.tsx +78 -75
- package/src/components/breadcrumb/breadcrumb.tsx +3 -2
- package/src/components/button/button.tsx +18 -12
- package/src/components/button-group/button-group.tsx +1 -1
- package/src/components/calendar/calendar.tsx +3 -2
- package/src/components/card/card.test.tsx +24 -2
- package/src/components/card/card.tsx +47 -6
- package/src/components/carousel/carousel.test.tsx +169 -0
- package/src/components/carousel/carousel.tsx +93 -32
- package/src/components/change-review/change-review.tsx +38 -29
- package/src/components/checkbox/checkbox.tsx +1 -0
- package/src/components/collapsible-panel/use-collapsible-panel.ts +0 -3
- package/src/components/color-picker/color-picker.tsx +17 -11
- package/src/components/combobox/combobox.stories.tsx +10 -0
- package/src/components/combobox/combobox.test.tsx +54 -0
- package/src/components/combobox/combobox.tsx +25 -11
- package/src/components/command/command.test.tsx +15 -0
- package/src/components/command/command.tsx +8 -6
- package/src/components/context-menu/context-menu.tsx +16 -7
- package/src/components/context-rail/context-rail.tsx +4 -4
- package/src/components/copyable-value/copyable-value.stories.tsx +10 -0
- package/src/components/copyable-value/copyable-value.test.tsx +28 -1
- package/src/components/copyable-value/copyable-value.tsx +12 -1
- package/src/components/date-picker/date-picker.stories.tsx +9 -0
- package/src/components/date-picker/date-picker.test.tsx +52 -0
- package/src/components/date-picker/date-picker.tsx +13 -15
- package/src/components/date-range-picker/date-range-picker.stories.tsx +9 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +76 -1
- package/src/components/date-range-picker/date-range-picker.tsx +39 -28
- package/src/components/descriptions/descriptions.tsx +1 -1
- package/src/components/dialog/dialog.test.tsx +8 -3
- package/src/components/dialog/dialog.tsx +12 -4
- package/src/components/drawer/drawer.tsx +23 -6
- package/src/components/dropdown-menu/dropdown-menu.tsx +4 -4
- package/src/components/field/field.tsx +2 -0
- package/src/components/field-row/field-row.tsx +2 -0
- package/src/components/file-upload/file-upload.test.tsx +83 -10
- package/src/components/file-upload/file-upload.tsx +150 -28
- package/src/components/form/form.tsx +4 -2
- package/src/components/hover-card/hover-card.tsx +1 -0
- package/src/components/icon-button/icon-button.tsx +1 -1
- package/src/components/input/input.tsx +7 -3
- package/src/components/input-group/input-group.tsx +6 -6
- package/src/components/input-otp/input-otp.tsx +3 -1
- package/src/components/keyboard-shortcuts/keyboard-shortcuts.tsx +2 -2
- package/src/components/label/label.tsx +1 -1
- package/src/components/list-editor/list-editor.test.tsx +24 -0
- package/src/components/list-editor/list-editor.tsx +126 -52
- package/src/components/locale-provider/locale-keys.test.ts +134 -0
- package/src/components/locale-provider/messages.ts +321 -0
- package/src/components/mention-input/mention-input.stories.tsx +3 -3
- package/src/components/mention-input/mention-input.test.tsx +51 -1
- package/src/components/mention-input/mention-input.tsx +58 -8
- package/src/components/menubar/menubar.tsx +16 -6
- package/src/components/nav-main/nav-main.tsx +7 -3
- package/src/components/nav-notifications/nav-notifications.test.tsx +11 -0
- package/src/components/nav-notifications/nav-notifications.tsx +11 -5
- package/src/components/nav-user/nav-user.tsx +5 -5
- package/src/components/navigation-menu/navigation-menu.tsx +16 -4
- package/src/components/number-input/number-input.stories.tsx +7 -3
- package/src/components/number-input/number-input.test.tsx +47 -0
- package/src/components/number-input/number-input.tsx +65 -24
- package/src/components/pagination/pagination.test.tsx +20 -1
- package/src/components/pagination/pagination.tsx +18 -14
- package/src/components/popover/popover.tsx +1 -0
- package/src/components/progress/progress.tsx +2 -0
- package/src/components/rating/rating.tsx +2 -0
- package/src/components/resizable/resizable.test.tsx +76 -0
- package/src/components/resizable/resizable.tsx +31 -5
- package/src/components/revision-timeline/revision-timeline.tsx +7 -2
- package/src/components/schema-form/schema-form-store.ts +506 -0
- package/src/components/schema-form/schema-form.test.tsx +122 -1
- package/src/components/schema-form/schema-form.tsx +163 -327
- package/src/components/section-header/section-header.tsx +2 -2
- package/src/components/select/select.tsx +7 -5
- package/src/components/separator/separator.tsx +1 -0
- package/src/components/sheet/sheet.test.tsx +39 -0
- package/src/components/sheet/sheet.tsx +40 -9
- package/src/components/side-dock/side-dock.test.tsx +26 -0
- package/src/components/side-dock/side-dock.tsx +15 -2
- package/src/components/sidebar/sidebar.test.tsx +35 -1
- package/src/components/sidebar/sidebar.tsx +20 -6
- package/src/components/skeleton/skeleton.tsx +1 -0
- package/src/components/slider-number/slider-number.stories.tsx +4 -1
- package/src/components/slider-number/slider-number.test.tsx +1 -1
- package/src/components/sonner/sonner.tsx +2 -0
- package/src/components/spinner/spinner.test.tsx +18 -0
- package/src/components/spinner/spinner.tsx +14 -4
- package/src/components/state-panel/state-panel.test.tsx +3 -3
- package/src/components/state-panel/state-panel.tsx +15 -8
- package/src/components/switch/switch.tsx +2 -0
- package/src/components/table/table.stories.tsx +3 -1
- package/src/components/table/table.test.tsx +9 -2
- package/src/components/table/table.tsx +18 -8
- package/src/components/tabs/index.ts +10 -1
- package/src/components/tabs/tabs.stories.tsx +48 -0
- package/src/components/tabs/tabs.test.tsx +67 -0
- package/src/components/tabs/tabs.tsx +175 -77
- package/src/components/tag-input/tag-input.tsx +4 -2
- package/src/components/team-switcher/team-switcher.tsx +3 -3
- package/src/components/textarea/textarea.tsx +1 -1
- package/src/components/theme-switcher/theme-switcher.stories.tsx +76 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +182 -0
- package/src/components/theme-switcher/theme-switcher.tsx +134 -10
- package/src/components/timeline/timeline.tsx +0 -3
- package/src/components/toggle/toggle.tsx +7 -2
- package/src/components/toggle-group/toggle-group.tsx +2 -0
- package/src/components/tooltip/tooltip.tsx +2 -1
- package/src/components/top-nav/top-nav.tsx +1 -1
- package/src/components/transfer/transfer.tsx +35 -30
- package/src/components/tree/tree.test.tsx +41 -0
- package/src/components/tree/tree.tsx +77 -22
- package/src/components/tree/use-tree-keyboard.ts +37 -14
- package/src/components/tree-select/tree-select.stories.tsx +9 -0
- package/src/components/tree-select/tree-select.test.tsx +5 -0
- package/src/components/tree-select/tree-select.tsx +11 -14
- package/src/components/typography/prose.test.tsx +3 -2
- package/src/components/typography/prose.tsx +3 -3
- package/src/components/virtual-select/virtual-select.stories.tsx +9 -0
- package/src/components/virtual-select/virtual-select.test.tsx +28 -2
- package/src/components/virtual-select/virtual-select.tsx +56 -18
- package/src/components/wizard/wizard.test.tsx +9 -0
- package/src/components/wizard/wizard.tsx +12 -4
- package/src/index.ts +5 -0
- package/src/lib/cn.ts +9 -0
- package/src/lib/csv.ts +48 -0
- package/src/lib/diff-rows.ts +2 -0
- package/src/lib/merge-refs.ts +2 -0
- package/src/lib/use-controllable-state.ts +47 -0
- package/src/lib/use-mobile.test.tsx +47 -0
- package/src/lib/use-mobile.ts +26 -10
- package/dist/chunk-JKPPTL63.js.map +0 -1
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// a2ui.exposed: yes
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { forwardRef,
|
|
4
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
5
5
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
6
6
|
import type { DateRange } from "react-day-picker";
|
|
7
7
|
import { cn } from "../../lib/cn";
|
|
8
|
+
import { useControllableState } from "../../lib/use-controllable-state";
|
|
9
|
+
import { useIsMobile } from "../../lib/use-mobile";
|
|
8
10
|
import { Button } from "../button";
|
|
9
11
|
import { Calendar } from "../calendar";
|
|
12
|
+
import { useLocale } from "../locale-provider";
|
|
10
13
|
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
11
14
|
|
|
15
|
+
/** Below this width, the calendar collapses to a single month (Tailwind `sm`). */
|
|
16
|
+
const NARROW_BREAKPOINT = 640;
|
|
17
|
+
|
|
12
18
|
export type { DateRange };
|
|
13
19
|
|
|
14
20
|
export interface DateRangePreset {
|
|
@@ -16,7 +22,10 @@ export interface DateRangePreset {
|
|
|
16
22
|
getRange: () => DateRange;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
export interface DateRangePickerProps
|
|
25
|
+
export interface DateRangePickerProps extends Omit<
|
|
26
|
+
HTMLAttributes<HTMLButtonElement>,
|
|
27
|
+
"onSelect" | "children" | "value" | "defaultValue"
|
|
28
|
+
> {
|
|
20
29
|
value?: DateRange;
|
|
21
30
|
defaultValue?: DateRange;
|
|
22
31
|
onValueChange?: (range?: DateRange) => void;
|
|
@@ -26,16 +35,6 @@ export interface DateRangePickerProps {
|
|
|
26
35
|
className?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
const fmt = (date: Date) =>
|
|
30
|
-
new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date);
|
|
31
|
-
|
|
32
|
-
function formatRange(range: DateRange | undefined, placeholder: string): string {
|
|
33
|
-
if (!range) return placeholder;
|
|
34
|
-
if (range.from && range.to) return `${fmt(range.from)} – ${fmt(range.to)}`;
|
|
35
|
-
if (range.from) return `${fmt(range.from)} – …`;
|
|
36
|
-
return placeholder;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
38
|
/**
|
|
40
39
|
* Date range picker (Popover + Calendar in range mode).
|
|
41
40
|
* Supports optional preset quick-picks, controlled or uncontrolled.
|
|
@@ -50,24 +49,31 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
50
49
|
numberOfMonths = 2,
|
|
51
50
|
placeholder = "Pick a date range",
|
|
52
51
|
className,
|
|
52
|
+
...props
|
|
53
53
|
},
|
|
54
54
|
ref,
|
|
55
55
|
) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const { formatDate } = useLocale();
|
|
57
|
+
const [range, setRange] = useControllableState<DateRange | undefined>(
|
|
58
|
+
value,
|
|
59
|
+
defaultValue,
|
|
60
|
+
onValueChange,
|
|
61
|
+
);
|
|
60
62
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const fmt = (date: Date) => formatDate(date, { dateStyle: "medium" });
|
|
64
|
+
const formatRange = (r: DateRange | undefined): string => {
|
|
65
|
+
if (!r) return placeholder;
|
|
66
|
+
if (r.from && r.to) return `${fmt(r.from)} – ${fmt(r.to)}`;
|
|
67
|
+
if (r.from) return `${fmt(r.from)} – …`;
|
|
68
|
+
return placeholder;
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
const hasRange = Boolean(range?.from);
|
|
70
|
-
const label = formatRange(range
|
|
72
|
+
const label = formatRange(range);
|
|
73
|
+
|
|
74
|
+
// Below `sm`, two side-by-side months push the popover off-screen (#reviewed).
|
|
75
|
+
const isNarrow = useIsMobile(NARROW_BREAKPOINT);
|
|
76
|
+
const monthsToShow = isNarrow ? 1 : numberOfMonths;
|
|
71
77
|
|
|
72
78
|
return (
|
|
73
79
|
<Popover>
|
|
@@ -76,8 +82,9 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
76
82
|
ref={ref}
|
|
77
83
|
variant="outline"
|
|
78
84
|
aria-haspopup="dialog"
|
|
85
|
+
{...props}
|
|
79
86
|
className={cn(
|
|
80
|
-
"w-
|
|
87
|
+
"w-full justify-start gap-2 text-start font-normal",
|
|
81
88
|
!hasRange && "text-muted-foreground",
|
|
82
89
|
className,
|
|
83
90
|
)}
|
|
@@ -86,10 +93,14 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
86
93
|
{label}
|
|
87
94
|
</Button>
|
|
88
95
|
</PopoverTrigger>
|
|
89
|
-
<PopoverContent
|
|
96
|
+
<PopoverContent
|
|
97
|
+
className="w-auto max-w-[calc(100vw-1rem)] p-0"
|
|
98
|
+
align="start"
|
|
99
|
+
collisionPadding={8}
|
|
100
|
+
>
|
|
90
101
|
{presets && presets.length > 0 ? (
|
|
91
102
|
<div className="flex">
|
|
92
|
-
<div className="flex flex-col gap-1 border-
|
|
103
|
+
<div className="flex flex-col gap-1 border-e border-border p-2">
|
|
93
104
|
{presets.map((preset) => (
|
|
94
105
|
<Button
|
|
95
106
|
key={preset.label}
|
|
@@ -104,7 +115,7 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
104
115
|
</div>
|
|
105
116
|
<Calendar
|
|
106
117
|
mode="range"
|
|
107
|
-
numberOfMonths={
|
|
118
|
+
numberOfMonths={monthsToShow}
|
|
108
119
|
selected={range}
|
|
109
120
|
onSelect={setRange}
|
|
110
121
|
autoFocus
|
|
@@ -113,7 +124,7 @@ export const DateRangePicker = forwardRef<HTMLButtonElement, DateRangePickerProp
|
|
|
113
124
|
) : (
|
|
114
125
|
<Calendar
|
|
115
126
|
mode="range"
|
|
116
|
-
numberOfMonths={
|
|
127
|
+
numberOfMonths={monthsToShow}
|
|
117
128
|
selected={range}
|
|
118
129
|
onSelect={setRange}
|
|
119
130
|
autoFocus
|
|
@@ -3,7 +3,7 @@ import { createContext, forwardRef, use, type HTMLAttributes } from "react";
|
|
|
3
3
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
5
|
|
|
6
|
-
const descriptionsVariants = cva("grid gap-x-6 gap-y-3 text-
|
|
6
|
+
const descriptionsVariants = cva("grid gap-x-6 gap-y-3 text-body", {
|
|
7
7
|
variants: {
|
|
8
8
|
columns: {
|
|
9
9
|
1: "grid-cols-1",
|
|
@@ -47,11 +47,16 @@ describe("DialogContent size variants", () => {
|
|
|
47
47
|
expect(content.className).toContain("max-w-3xl");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it("size='full' renders max-w-[95vw] and h-[
|
|
50
|
+
it("size='full' renders max-w-[95vw] and h-[90dvh] — dvh, not vh (mobile browser chrome)", () => {
|
|
51
51
|
render(<Wrapper size="full" />);
|
|
52
52
|
const content = screen.getByRole("dialog");
|
|
53
53
|
expect(content.className).toContain("max-w-[95vw]");
|
|
54
|
-
expect(content.className).toContain("h-[
|
|
54
|
+
expect(content.className).toContain("h-[90dvh]");
|
|
55
|
+
expect(content.className).toContain("max-h-[90dvh]");
|
|
56
|
+
// `vh` on mobile Safari/Chrome is measured against the viewport with the
|
|
57
|
+
// browser chrome collapsed, so a plain `vh` value overflows past what is
|
|
58
|
+
// actually visible while the address bar is shown.
|
|
59
|
+
expect(content.className).not.toMatch(/(?<!d)vh\]/);
|
|
55
60
|
});
|
|
56
61
|
});
|
|
57
62
|
|
|
@@ -106,7 +111,7 @@ describe("DialogContent height/scroll regime (#341)", () => {
|
|
|
106
111
|
// later variant utility, so `full` still lets its body own the scroll.
|
|
107
112
|
expect(content.className).toContain("overflow-hidden");
|
|
108
113
|
expect(content.className).not.toContain("overflow-y-auto");
|
|
109
|
-
expect(content.className).toContain("max-h-[
|
|
114
|
+
expect(content.className).toContain("max-h-[90dvh]");
|
|
110
115
|
expect(content.className).not.toContain("max-h-[calc(100dvh-2rem)]");
|
|
111
116
|
});
|
|
112
117
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
forwardRef,
|
|
3
5
|
useCallback,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
12
14
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
13
15
|
import { cn } from "../../lib/cn";
|
|
16
|
+
import { useLocale } from "../locale-provider";
|
|
14
17
|
import { Heading, Text } from "../typography";
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -55,7 +58,11 @@ export const dialogContentVariants = cva(
|
|
|
55
58
|
sm: "max-w-sm",
|
|
56
59
|
lg: "max-w-lg",
|
|
57
60
|
xl: "max-w-3xl",
|
|
58
|
-
|
|
61
|
+
// `dvh` (dynamic viewport height), not `vh`: on mobile Safari/Chrome
|
|
62
|
+
// `vh` is measured against the LARGEST viewport (browser chrome
|
|
63
|
+
// collapsed), so `size="full"` overflows past the visible viewport
|
|
64
|
+
// while the address bar is showing.
|
|
65
|
+
full: "max-w-[95vw] h-[90dvh] max-h-[90dvh] overflow-hidden",
|
|
59
66
|
},
|
|
60
67
|
},
|
|
61
68
|
defaultVariants: { size: "lg" },
|
|
@@ -76,7 +83,7 @@ export const DialogOverlay = forwardRef<
|
|
|
76
83
|
ref={ref}
|
|
77
84
|
data-slot="dialog-overlay"
|
|
78
85
|
className={cn(
|
|
79
|
-
"fixed inset-0 z-50 bg-
|
|
86
|
+
"fixed inset-0 z-50 bg-overlay backdrop-blur-overlay",
|
|
80
87
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
81
88
|
className,
|
|
82
89
|
)}
|
|
@@ -90,6 +97,7 @@ export const DialogContent = forwardRef<
|
|
|
90
97
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
91
98
|
VariantProps<typeof dialogContentVariants>
|
|
92
99
|
>(function DialogContent({ className, children, size, onOpenAutoFocus, ...props }, ref) {
|
|
100
|
+
const { t } = useLocale();
|
|
93
101
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
94
102
|
const setRefs = useCallback(
|
|
95
103
|
(node: HTMLDivElement | null) => {
|
|
@@ -150,7 +158,7 @@ export const DialogContent = forwardRef<
|
|
|
150
158
|
<DialogPrimitive.Close
|
|
151
159
|
data-slot="dialog-close"
|
|
152
160
|
className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-ring"
|
|
153
|
-
aria-label="
|
|
161
|
+
aria-label={t("close")}
|
|
154
162
|
>
|
|
155
163
|
<svg
|
|
156
164
|
width="16"
|
|
@@ -213,7 +221,7 @@ export const DialogDescription = forwardRef<
|
|
|
213
221
|
<DialogPrimitive.Description
|
|
214
222
|
ref={ref}
|
|
215
223
|
data-slot="dialog-description"
|
|
216
|
-
className={cn("text-
|
|
224
|
+
className={cn("text-body text-muted-foreground", className)}
|
|
217
225
|
{...props}
|
|
218
226
|
/>
|
|
219
227
|
);
|
|
@@ -25,7 +25,8 @@ const DrawerOverlay = forwardRef<
|
|
|
25
25
|
return (
|
|
26
26
|
<DrawerPrimitive.Overlay
|
|
27
27
|
ref={ref}
|
|
28
|
-
|
|
28
|
+
data-slot="drawer-overlay"
|
|
29
|
+
className={cn("fixed inset-0 z-50 bg-overlay", className)}
|
|
29
30
|
{...props}
|
|
30
31
|
/>
|
|
31
32
|
);
|
|
@@ -40,13 +41,14 @@ export const DrawerContent = forwardRef<
|
|
|
40
41
|
<DrawerOverlay />
|
|
41
42
|
<DrawerPrimitive.Content
|
|
42
43
|
ref={ref}
|
|
44
|
+
data-slot="drawer-content"
|
|
43
45
|
className={cn(
|
|
44
46
|
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col overscroll-contain rounded-t-xl border bg-card text-card-foreground",
|
|
45
47
|
className,
|
|
46
48
|
)}
|
|
47
49
|
{...props}
|
|
48
50
|
>
|
|
49
|
-
<div className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
|
|
51
|
+
<div data-slot="drawer-handle" className="mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted" />
|
|
50
52
|
{children}
|
|
51
53
|
</DrawerPrimitive.Content>
|
|
52
54
|
</DrawerPortal>
|
|
@@ -54,19 +56,33 @@ export const DrawerContent = forwardRef<
|
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
export function DrawerHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
57
|
-
return
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="drawer-header"
|
|
62
|
+
className={cn("grid gap-1.5 p-4 text-center sm:text-start", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
58
66
|
}
|
|
59
67
|
export function DrawerFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
60
|
-
return
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-slot="drawer-footer"
|
|
71
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
61
75
|
}
|
|
62
76
|
export const DrawerTitle = forwardRef<
|
|
63
77
|
ElementRef<typeof DrawerPrimitive.Title>,
|
|
64
78
|
ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
65
79
|
>(function DrawerTitle({ className, ...props }, ref) {
|
|
66
80
|
return (
|
|
81
|
+
// `text-title`, matching `DialogTitle` (#286) — was `text-lg font-semibold`.
|
|
67
82
|
<DrawerPrimitive.Title
|
|
68
83
|
ref={ref}
|
|
69
|
-
|
|
84
|
+
data-slot="drawer-title"
|
|
85
|
+
className={cn("text-title leading-none", className)}
|
|
70
86
|
{...props}
|
|
71
87
|
/>
|
|
72
88
|
);
|
|
@@ -78,7 +94,8 @@ export const DrawerDescription = forwardRef<
|
|
|
78
94
|
return (
|
|
79
95
|
<DrawerPrimitive.Description
|
|
80
96
|
ref={ref}
|
|
81
|
-
|
|
97
|
+
data-slot="drawer-description"
|
|
98
|
+
className={cn("text-body text-muted-foreground", className)}
|
|
82
99
|
{...props}
|
|
83
100
|
/>
|
|
84
101
|
);
|
|
@@ -109,7 +109,7 @@ export const DropdownMenuContent = forwardRef<
|
|
|
109
109
|
ref={ref}
|
|
110
110
|
sideOffset={sideOffset}
|
|
111
111
|
className={cn(
|
|
112
|
-
"z-50 min-w-[
|
|
112
|
+
"z-50 min-w-[12rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
|
|
113
113
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
|
114
114
|
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
115
115
|
className,
|
|
@@ -128,7 +128,7 @@ export const DropdownMenuItem = forwardRef<
|
|
|
128
128
|
<DropdownMenuPrimitive.Item
|
|
129
129
|
ref={ref}
|
|
130
130
|
className={cn(
|
|
131
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
131
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast",
|
|
132
132
|
"focus:bg-accent focus:text-accent-foreground",
|
|
133
133
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
134
134
|
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
@@ -147,7 +147,7 @@ export const DropdownMenuLabel = forwardRef<
|
|
|
147
147
|
return (
|
|
148
148
|
<DropdownMenuPrimitive.Label
|
|
149
149
|
ref={ref}
|
|
150
|
-
className={cn("px-2 py-1.5 text-
|
|
150
|
+
className={cn("px-2 py-1.5 text-meta font-semibold text-muted-foreground", className)}
|
|
151
151
|
{...props}
|
|
152
152
|
/>
|
|
153
153
|
);
|
|
@@ -172,7 +172,7 @@ export function DropdownMenuShortcut({
|
|
|
172
172
|
}: React.HTMLAttributes<HTMLSpanElement>) {
|
|
173
173
|
return (
|
|
174
174
|
<span
|
|
175
|
-
className={cn("ms-auto text-
|
|
175
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
176
176
|
{...props}
|
|
177
177
|
/>
|
|
178
178
|
);
|
|
@@ -15,21 +15,28 @@ function makeUploadFile(name = "test.txt", size = 1024): UploadFile {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
describe("FileUpload", () => {
|
|
18
|
-
it("
|
|
18
|
+
it("default dropzone exposes exactly one control: the Browse files button", () => {
|
|
19
19
|
render(
|
|
20
20
|
<FileUpload>
|
|
21
21
|
<FileUploadDropzone />
|
|
22
22
|
</FileUpload>,
|
|
23
23
|
);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expect(
|
|
29
|
-
|
|
30
|
-
expect(
|
|
31
|
-
expect(
|
|
32
|
-
|
|
24
|
+
const dropzone = document.querySelector("[data-slot='file-upload-dropzone']")!;
|
|
25
|
+
// The zone is a plain drop surface — no role, no tab stop — so the inner
|
|
26
|
+
// button is never nested inside another interactive element (axe
|
|
27
|
+
// `nested-interactive` / `aria-allowed-role`).
|
|
28
|
+
expect(dropzone.tagName.toLowerCase()).toBe("div");
|
|
29
|
+
expect(dropzone).not.toHaveAttribute("role");
|
|
30
|
+
expect(dropzone).not.toHaveAttribute("tabIndex");
|
|
31
|
+
expect(screen.getAllByRole("button")).toHaveLength(1);
|
|
32
|
+
|
|
33
|
+
const input = document.querySelector("input[type='file']") as HTMLInputElement;
|
|
34
|
+
const clickSpy = vi.spyOn(input, "click");
|
|
35
|
+
fireEvent.click(screen.getByRole("button", { name: "Browse files" }));
|
|
36
|
+
// One activation opens the picker once (the zone's click must not re-fire it).
|
|
37
|
+
expect(clickSpy).toHaveBeenCalledTimes(1);
|
|
38
|
+
fireEvent.click(dropzone);
|
|
39
|
+
expect(clickSpy).toHaveBeenCalledTimes(2);
|
|
33
40
|
});
|
|
34
41
|
|
|
35
42
|
it("hidden file input has sr-only class (visually hidden but real)", () => {
|
|
@@ -156,6 +163,72 @@ describe("FileUpload", () => {
|
|
|
156
163
|
expect(onFilesChange).toHaveBeenCalledWith([]);
|
|
157
164
|
});
|
|
158
165
|
|
|
166
|
+
it("drag-and-drop rejects files that don't match `accept` and reports why", () => {
|
|
167
|
+
const onFilesChange = vi.fn();
|
|
168
|
+
const onFilesRejected = vi.fn();
|
|
169
|
+
render(
|
|
170
|
+
<FileUpload
|
|
171
|
+
files={[]}
|
|
172
|
+
onFilesChange={onFilesChange}
|
|
173
|
+
onFilesRejected={onFilesRejected}
|
|
174
|
+
accept=".png"
|
|
175
|
+
>
|
|
176
|
+
<FileUploadDropzone />
|
|
177
|
+
</FileUpload>,
|
|
178
|
+
);
|
|
179
|
+
const dropzone = document.querySelector("[data-slot='file-upload-dropzone']")!;
|
|
180
|
+
const wrongType = new File(["x"], "notes.txt", { type: "text/plain" });
|
|
181
|
+
fireEvent.drop(dropzone, { dataTransfer: { files: [wrongType] } });
|
|
182
|
+
|
|
183
|
+
// A drop bypasses the native <input accept> filter entirely — without
|
|
184
|
+
// this, a dragged-in .txt would be added even though the picker would
|
|
185
|
+
// never have offered it.
|
|
186
|
+
expect(onFilesChange).toHaveBeenCalledWith([]);
|
|
187
|
+
expect(onFilesRejected).toHaveBeenCalledWith([{ file: wrongType, reason: "accept" }]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("drag-and-drop of several files when multiple=false keeps only the first, rejects the rest", () => {
|
|
191
|
+
const onFilesChange = vi.fn();
|
|
192
|
+
const onFilesRejected = vi.fn();
|
|
193
|
+
render(
|
|
194
|
+
<FileUpload files={[]} onFilesChange={onFilesChange} onFilesRejected={onFilesRejected}>
|
|
195
|
+
<FileUploadDropzone />
|
|
196
|
+
</FileUpload>,
|
|
197
|
+
);
|
|
198
|
+
const dropzone = document.querySelector("[data-slot='file-upload-dropzone']")!;
|
|
199
|
+
const first = new File(["a"], "a.txt", { type: "text/plain" });
|
|
200
|
+
const second = new File(["b"], "b.txt", { type: "text/plain" });
|
|
201
|
+
fireEvent.drop(dropzone, { dataTransfer: { files: [first, second] } });
|
|
202
|
+
|
|
203
|
+
// `multiple` is only enforced by the browser for the picker dialog, never
|
|
204
|
+
// for a raw DataTransfer drop.
|
|
205
|
+
expect(onFilesChange.mock.calls[0]![0]).toHaveLength(1);
|
|
206
|
+
expect(onFilesChange.mock.calls[0]![0][0].file.name).toBe("a.txt");
|
|
207
|
+
expect(onFilesRejected).toHaveBeenCalledWith([{ file: second, reason: "multiple" }]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("a custom-children dropzone still opens the picker on Enter/Space (keyboard reachable)", () => {
|
|
211
|
+
render(
|
|
212
|
+
<FileUpload>
|
|
213
|
+
<FileUploadDropzone>Drop it here</FileUploadDropzone>
|
|
214
|
+
</FileUpload>,
|
|
215
|
+
);
|
|
216
|
+
// With custom `children` there is no fallback "Browse files" button —
|
|
217
|
+
// the zone itself must be the one control: a tab stop answering Enter/Space.
|
|
218
|
+
const dropzone = screen.getByRole("button");
|
|
219
|
+
expect(dropzone).toHaveTextContent("Drop it here");
|
|
220
|
+
expect(dropzone).toHaveAttribute("data-slot", "file-upload-dropzone");
|
|
221
|
+
expect(dropzone).toHaveAttribute("tabIndex", "0");
|
|
222
|
+
|
|
223
|
+
const input = document.querySelector("input[type='file']") as HTMLInputElement;
|
|
224
|
+
const clickSpy = vi.spyOn(input, "click");
|
|
225
|
+
fireEvent.keyDown(dropzone, { key: "Enter" });
|
|
226
|
+
expect(clickSpy).toHaveBeenCalledTimes(1);
|
|
227
|
+
|
|
228
|
+
fireEvent.keyDown(dropzone, { key: " " });
|
|
229
|
+
expect(clickSpy).toHaveBeenCalledTimes(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
159
232
|
it("disabled prop disables the file input and remove buttons", () => {
|
|
160
233
|
const f = makeUploadFile();
|
|
161
234
|
render(
|