@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
|
@@ -25,6 +25,7 @@ export const Menubar = forwardRef<
|
|
|
25
25
|
return (
|
|
26
26
|
<MenubarPrimitive.Root
|
|
27
27
|
ref={ref}
|
|
28
|
+
data-slot="menubar"
|
|
28
29
|
className={cn("flex h-9 items-center gap-1 rounded-md bg-card p-1 shadow-ring-sm", className)}
|
|
29
30
|
{...props}
|
|
30
31
|
/>
|
|
@@ -38,8 +39,9 @@ export const MenubarTrigger = forwardRef<
|
|
|
38
39
|
return (
|
|
39
40
|
<MenubarPrimitive.Trigger
|
|
40
41
|
ref={ref}
|
|
42
|
+
data-slot="menubar-trigger"
|
|
41
43
|
className={cn(
|
|
42
|
-
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-
|
|
44
|
+
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-body font-medium outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
|
|
43
45
|
className,
|
|
44
46
|
)}
|
|
45
47
|
{...props}
|
|
@@ -58,6 +60,7 @@ export const MenubarContent = forwardRef<
|
|
|
58
60
|
<MenubarPrimitive.Portal>
|
|
59
61
|
<MenubarPrimitive.Content
|
|
60
62
|
ref={ref}
|
|
63
|
+
data-slot="menubar-content"
|
|
61
64
|
align={align}
|
|
62
65
|
alignOffset={alignOffset}
|
|
63
66
|
sideOffset={sideOffset}
|
|
@@ -80,8 +83,9 @@ export const MenubarItem = forwardRef<
|
|
|
80
83
|
return (
|
|
81
84
|
<MenubarPrimitive.Item
|
|
82
85
|
ref={ref}
|
|
86
|
+
data-slot="menubar-item"
|
|
83
87
|
className={cn(
|
|
84
|
-
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-
|
|
88
|
+
"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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
85
89
|
inset && "ps-8",
|
|
86
90
|
className,
|
|
87
91
|
)}
|
|
@@ -97,8 +101,9 @@ export const MenubarSubTrigger = forwardRef<
|
|
|
97
101
|
return (
|
|
98
102
|
<MenubarPrimitive.SubTrigger
|
|
99
103
|
ref={ref}
|
|
104
|
+
data-slot="menubar-sub-trigger"
|
|
100
105
|
className={cn(
|
|
101
|
-
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-
|
|
106
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
|
|
102
107
|
inset && "ps-8",
|
|
103
108
|
className,
|
|
104
109
|
)}
|
|
@@ -117,6 +122,7 @@ export const MenubarSubContent = forwardRef<
|
|
|
117
122
|
return (
|
|
118
123
|
<MenubarPrimitive.SubContent
|
|
119
124
|
ref={ref}
|
|
125
|
+
data-slot="menubar-sub-content"
|
|
120
126
|
className={cn(
|
|
121
127
|
"z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
|
|
122
128
|
"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",
|
|
@@ -135,9 +141,10 @@ export const MenubarCheckboxItem = forwardRef<
|
|
|
135
141
|
return (
|
|
136
142
|
<MenubarPrimitive.CheckboxItem
|
|
137
143
|
ref={ref}
|
|
144
|
+
data-slot="menubar-checkbox-item"
|
|
138
145
|
checked={checked}
|
|
139
146
|
className={cn(
|
|
140
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-
|
|
147
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-body outline-none transition-colors duration-fast focus:bg-accent",
|
|
141
148
|
className,
|
|
142
149
|
)}
|
|
143
150
|
{...props}
|
|
@@ -159,8 +166,9 @@ export const MenubarRadioItem = forwardRef<
|
|
|
159
166
|
return (
|
|
160
167
|
<MenubarPrimitive.RadioItem
|
|
161
168
|
ref={ref}
|
|
169
|
+
data-slot="menubar-radio-item"
|
|
162
170
|
className={cn(
|
|
163
|
-
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-
|
|
171
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-body outline-none transition-colors duration-fast focus:bg-accent",
|
|
164
172
|
className,
|
|
165
173
|
)}
|
|
166
174
|
{...props}
|
|
@@ -182,6 +190,7 @@ export const MenubarSeparator = forwardRef<
|
|
|
182
190
|
return (
|
|
183
191
|
<MenubarPrimitive.Separator
|
|
184
192
|
ref={ref}
|
|
193
|
+
data-slot="menubar-separator"
|
|
185
194
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
186
195
|
{...props}
|
|
187
196
|
/>
|
|
@@ -191,7 +200,8 @@ export const MenubarSeparator = forwardRef<
|
|
|
191
200
|
export function MenubarShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
192
201
|
return (
|
|
193
202
|
<span
|
|
194
|
-
|
|
203
|
+
data-slot="menubar-shortcut"
|
|
204
|
+
className={cn("ms-auto text-meta tracking-widest text-muted-foreground", className)}
|
|
195
205
|
{...props}
|
|
196
206
|
/>
|
|
197
207
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Shared collapsible sidebar navigation primitive (issue #99).
|
|
3
5
|
* Promoted from `registry/blocks/sidebar-02/nav-main.tsx`.
|
|
@@ -90,7 +92,7 @@ export function NavMain({ routes, className }: NavMainProps) {
|
|
|
90
92
|
>
|
|
91
93
|
{route.icon}
|
|
92
94
|
{!isCollapsed && (
|
|
93
|
-
<span className="ms-2 flex-1 text-
|
|
95
|
+
<span className="ms-2 flex-1 text-body font-medium">{route.title}</span>
|
|
94
96
|
)}
|
|
95
97
|
{!isCollapsed && (
|
|
96
98
|
<span className="ms-auto">
|
|
@@ -114,7 +116,7 @@ export function NavMain({ routes, className }: NavMainProps) {
|
|
|
114
116
|
<SidebarMenuSubButton asChild>
|
|
115
117
|
<a
|
|
116
118
|
href={subRoute.link}
|
|
117
|
-
className="flex items-center rounded-md px-4 py-1.5 text-
|
|
119
|
+
className="flex items-center rounded-md px-4 py-1.5 text-body font-medium text-sidebar-muted-foreground"
|
|
118
120
|
>
|
|
119
121
|
{subRoute.title}
|
|
120
122
|
</a>
|
|
@@ -135,7 +137,9 @@ export function NavMain({ routes, className }: NavMainProps) {
|
|
|
135
137
|
)}
|
|
136
138
|
>
|
|
137
139
|
{route.icon}
|
|
138
|
-
{!isCollapsed &&
|
|
140
|
+
{!isCollapsed && (
|
|
141
|
+
<span className="ms-2 text-body font-medium">{route.title}</span>
|
|
142
|
+
)}
|
|
139
143
|
</a>
|
|
140
144
|
</SidebarMenuButton>
|
|
141
145
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
3
4
|
import { NavNotifications } from "./nav-notifications";
|
|
4
5
|
|
|
5
6
|
describe("NavNotifications", () => {
|
|
@@ -12,4 +13,14 @@ describe("NavNotifications", () => {
|
|
|
12
13
|
render(<NavNotifications notifications={notifications} />);
|
|
13
14
|
expect(screen.getByRole("button", { name: "Open notifications" })).toBeInTheDocument();
|
|
14
15
|
});
|
|
16
|
+
|
|
17
|
+
it("caps the menu width so it never overflows a narrow viewport", async () => {
|
|
18
|
+
const user = userEvent.setup();
|
|
19
|
+
render(<NavNotifications notifications={notifications} />);
|
|
20
|
+
await user.click(screen.getByRole("button", { name: "Open notifications" }));
|
|
21
|
+
const menu = await screen.findByRole("menu");
|
|
22
|
+
// Without a max-width cap, the fixed `w-80` (320px) menu forces horizontal
|
|
23
|
+
// overflow on any viewport narrower than ~336px.
|
|
24
|
+
expect(menu.className).toContain("max-w-[calc(100vw-2rem)]");
|
|
25
|
+
});
|
|
15
26
|
});
|
|
@@ -81,7 +81,7 @@ export function NavNotifications({
|
|
|
81
81
|
variant="ghost"
|
|
82
82
|
size="icon"
|
|
83
83
|
className={cn("rounded-full", className)}
|
|
84
|
-
aria-label="
|
|
84
|
+
aria-label={t("ui.navNotifications.trigger")}
|
|
85
85
|
>
|
|
86
86
|
<BellIcon className="size-5" />
|
|
87
87
|
</Button>
|
|
@@ -89,6 +89,9 @@ export function NavNotifications({
|
|
|
89
89
|
<DropdownMenuContent
|
|
90
90
|
side={side}
|
|
91
91
|
align={align}
|
|
92
|
+
// Keeps the menu clear of the viewport edge instead of sitting flush
|
|
93
|
+
// against it on a narrow phone screen.
|
|
94
|
+
collisionPadding={16}
|
|
92
95
|
className={cn(
|
|
93
96
|
// The vertical inset belongs to the SIDE-opening rail placement, where
|
|
94
97
|
// the menu runs alongside its trigger and needs clearance from it. A
|
|
@@ -96,7 +99,10 @@ export function NavNotifications({
|
|
|
96
99
|
// and the inset would just hang it 24px off the bar. Derived from
|
|
97
100
|
// `side` rather than exposed as a second className seam.
|
|
98
101
|
(side === "left" || side === "right") && "my-6",
|
|
99
|
-
|
|
102
|
+
// `w-80` (320px) is wider than some phone viewports once the 16px
|
|
103
|
+
// collision padding on both sides is subtracted — cap it so the menu
|
|
104
|
+
// shrinks instead of forcing horizontal overflow.
|
|
105
|
+
"w-80 max-w-[calc(100vw-2rem)]",
|
|
100
106
|
)}
|
|
101
107
|
>
|
|
102
108
|
<DropdownMenuLabel>{t("ui.navNotifications.label")}</DropdownMenuLabel>
|
|
@@ -107,13 +113,13 @@ export function NavNotifications({
|
|
|
107
113
|
<AvatarFallback>{fallback}</AvatarFallback>
|
|
108
114
|
</Avatar>
|
|
109
115
|
<div className="flex flex-col">
|
|
110
|
-
<span className="text-
|
|
111
|
-
<span className="text-
|
|
116
|
+
<span className="text-body font-medium">{text}</span>
|
|
117
|
+
<span className="text-meta text-muted-foreground">{time}</span>
|
|
112
118
|
</div>
|
|
113
119
|
</DropdownMenuItem>
|
|
114
120
|
))}
|
|
115
121
|
<DropdownMenuSeparator />
|
|
116
|
-
<DropdownMenuItem className="justify-center text-
|
|
122
|
+
<DropdownMenuItem className="justify-center text-body text-muted-foreground">
|
|
117
123
|
{viewAllLabel}
|
|
118
124
|
</DropdownMenuItem>
|
|
119
125
|
</DropdownMenuContent>
|
|
@@ -54,9 +54,9 @@ export function NavUser({ user }: NavUserProps) {
|
|
|
54
54
|
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
55
55
|
<AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
|
|
56
56
|
</Avatar>
|
|
57
|
-
<div className="grid flex-1 text-start text-
|
|
57
|
+
<div className="grid flex-1 text-start text-body leading-tight">
|
|
58
58
|
<span className="truncate font-medium">{user.name}</span>
|
|
59
|
-
<span className="truncate text-
|
|
59
|
+
<span className="truncate text-meta">{user.email}</span>
|
|
60
60
|
</div>
|
|
61
61
|
<ChevronsUpDown className="ms-auto size-4" />
|
|
62
62
|
</SidebarMenuButton>
|
|
@@ -68,14 +68,14 @@ export function NavUser({ user }: NavUserProps) {
|
|
|
68
68
|
sideOffset={4}
|
|
69
69
|
>
|
|
70
70
|
<DropdownMenuLabel className="p-0 font-normal">
|
|
71
|
-
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-
|
|
71
|
+
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-body">
|
|
72
72
|
<Avatar className="h-8 w-8 rounded-lg">
|
|
73
73
|
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
74
74
|
<AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
|
|
75
75
|
</Avatar>
|
|
76
|
-
<div className="grid flex-1 text-start text-
|
|
76
|
+
<div className="grid flex-1 text-start text-body leading-tight">
|
|
77
77
|
<span className="truncate font-medium">{user.name}</span>
|
|
78
|
-
<span className="truncate text-
|
|
78
|
+
<span className="truncate text-meta">{user.email}</span>
|
|
79
79
|
</div>
|
|
80
80
|
</div>
|
|
81
81
|
</DropdownMenuLabel>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createContext,
|
|
3
5
|
forwardRef,
|
|
@@ -150,6 +152,7 @@ export const NavigationMenu = forwardRef<
|
|
|
150
152
|
<NavigationMenuGuardContext.Provider value={guard}>
|
|
151
153
|
<NavigationMenuPrimitive.Root
|
|
152
154
|
ref={ref}
|
|
155
|
+
data-slot="navigation-menu"
|
|
153
156
|
value={currentValue}
|
|
154
157
|
onValueChange={handleValueChange}
|
|
155
158
|
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
|
|
@@ -169,6 +172,7 @@ export const NavigationMenuList = forwardRef<
|
|
|
169
172
|
return (
|
|
170
173
|
<NavigationMenuPrimitive.List
|
|
171
174
|
ref={ref}
|
|
175
|
+
data-slot="navigation-menu-list"
|
|
172
176
|
className={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
|
|
173
177
|
{...props}
|
|
174
178
|
/>
|
|
@@ -183,13 +187,18 @@ export const NavigationMenuItem = forwardRef<
|
|
|
183
187
|
const value = valueProp ?? autoValue;
|
|
184
188
|
return (
|
|
185
189
|
<NavigationMenuItemValueContext.Provider value={value}>
|
|
186
|
-
<NavigationMenuPrimitive.Item
|
|
190
|
+
<NavigationMenuPrimitive.Item
|
|
191
|
+
ref={ref}
|
|
192
|
+
data-slot="navigation-menu-item"
|
|
193
|
+
value={value}
|
|
194
|
+
{...props}
|
|
195
|
+
/>
|
|
187
196
|
</NavigationMenuItemValueContext.Provider>
|
|
188
197
|
);
|
|
189
198
|
});
|
|
190
199
|
|
|
191
200
|
export const navigationMenuTriggerStyle = cva(
|
|
192
|
-
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-
|
|
201
|
+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-body font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent",
|
|
193
202
|
);
|
|
194
203
|
|
|
195
204
|
export const NavigationMenuTrigger = forwardRef<
|
|
@@ -211,6 +220,7 @@ export const NavigationMenuTrigger = forwardRef<
|
|
|
211
220
|
return (
|
|
212
221
|
<NavigationMenuPrimitive.Trigger
|
|
213
222
|
ref={ref}
|
|
223
|
+
data-slot="navigation-menu-trigger"
|
|
214
224
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
215
225
|
onPointerEnter={(event) => {
|
|
216
226
|
onPointerEnter?.(event);
|
|
@@ -242,8 +252,9 @@ export const NavigationMenuContent = forwardRef<
|
|
|
242
252
|
return (
|
|
243
253
|
<NavigationMenuPrimitive.Content
|
|
244
254
|
ref={ref}
|
|
255
|
+
data-slot="navigation-menu-content"
|
|
245
256
|
className={cn(
|
|
246
|
-
"
|
|
257
|
+
"start-0 top-0 w-full p-2 md:absolute md:w-auto",
|
|
247
258
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out",
|
|
248
259
|
"data-[motion=from-end]:slide-in-from-right-2 data-[motion=from-start]:slide-in-from-left-2 data-[motion=to-end]:slide-out-to-right-2 data-[motion=to-start]:slide-out-to-left-2",
|
|
249
260
|
"data-[motion^=from-]:[--tw-ease:var(--ease-entrance)]",
|
|
@@ -261,9 +272,10 @@ export const NavigationMenuViewport = forwardRef<
|
|
|
261
272
|
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
262
273
|
>(function NavigationMenuViewport({ className, ...props }, ref) {
|
|
263
274
|
return (
|
|
264
|
-
<div className="absolute
|
|
275
|
+
<div className="absolute start-0 top-full flex justify-center">
|
|
265
276
|
<NavigationMenuPrimitive.Viewport
|
|
266
277
|
ref={ref}
|
|
278
|
+
data-slot="navigation-menu-viewport"
|
|
267
279
|
className={cn(
|
|
268
280
|
"relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
269
281
|
"origin-top transition-[width,height] duration-base ease-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:zoom-in-90 data-[state=closed]:zoom-out-95",
|
|
@@ -76,13 +76,17 @@ type Story = StoryObj<typeof meta>;
|
|
|
76
76
|
|
|
77
77
|
export const Default: Story = {
|
|
78
78
|
args: { defaultValue: 5, min: 0, max: 10 },
|
|
79
|
-
// Clicks the Increase button and confirms the value increments.
|
|
79
|
+
// Clicks the Increase button and confirms the value increments. The input is a
|
|
80
|
+
// locale-formatted text spinbutton, so the displayed value is a string and the
|
|
81
|
+
// numeric value is announced via aria-valuenow.
|
|
80
82
|
play: async ({ canvas, userEvent }) => {
|
|
81
83
|
const input = canvas.getByRole("spinbutton");
|
|
82
|
-
await expect(input).toHaveValue(5);
|
|
84
|
+
await expect(input).toHaveValue("5");
|
|
85
|
+
await expect(input).toHaveAttribute("aria-valuenow", "5");
|
|
83
86
|
const increaseBtn = canvas.getByRole("button", { name: /increase/i });
|
|
84
87
|
await userEvent.click(increaseBtn);
|
|
85
|
-
await expect(input).toHaveValue(6);
|
|
88
|
+
await expect(input).toHaveValue("6");
|
|
89
|
+
await expect(input).toHaveAttribute("aria-valuenow", "6");
|
|
86
90
|
},
|
|
87
91
|
};
|
|
88
92
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
1
2
|
import { describe, expect, it, vi } from "vitest";
|
|
2
3
|
import { render, screen } from "@testing-library/react";
|
|
3
4
|
import userEvent from "@testing-library/user-event";
|
|
4
5
|
import { NumberInput } from "./number-input";
|
|
6
|
+
import { LocaleProvider } from "../locale-provider";
|
|
5
7
|
|
|
6
8
|
describe("NumberInput", () => {
|
|
7
9
|
it("renders a spinbutton input", () => {
|
|
@@ -93,4 +95,49 @@ describe("NumberInput", () => {
|
|
|
93
95
|
await userEvent.keyboard("{ArrowDown}");
|
|
94
96
|
expect(onValueChange).toHaveBeenLastCalledWith(5);
|
|
95
97
|
});
|
|
98
|
+
|
|
99
|
+
it("forwards the ref to the underlying <input>, not the wrapper", () => {
|
|
100
|
+
const ref = createRef<HTMLInputElement>();
|
|
101
|
+
render(<NumberInput aria-label="Qty" defaultValue={5} ref={ref} />);
|
|
102
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
103
|
+
expect(ref.current).toBe(screen.getByRole("spinbutton", { name: "Qty" }));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not hard-fix a width — the caller's className is not fighting a default", () => {
|
|
107
|
+
render(<NumberInput aria-label="Qty" defaultValue={5} className="w-24" />);
|
|
108
|
+
const group = screen.getByRole("group");
|
|
109
|
+
expect(group.className).not.toMatch(/\bw-36\b/);
|
|
110
|
+
expect(group.className).toMatch(/\bw-24\b/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rounds fp-noisy step increments to the step's precision (0.1 + 0.1 + 0.1)", async () => {
|
|
114
|
+
const onValueChange = vi.fn();
|
|
115
|
+
render(
|
|
116
|
+
<NumberInput aria-label="Amount" defaultValue={0} step={0.1} onValueChange={onValueChange} />,
|
|
117
|
+
);
|
|
118
|
+
const increaseBtn = screen.getByRole("button", { name: "Increase" });
|
|
119
|
+
await userEvent.click(increaseBtn);
|
|
120
|
+
await userEvent.click(increaseBtn);
|
|
121
|
+
await userEvent.click(increaseBtn);
|
|
122
|
+
expect(onValueChange).toHaveBeenLastCalledWith(0.3);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("parses a locale-formatted number using the active locale's separators (de-DE: comma decimal)", async () => {
|
|
126
|
+
const onValueChange = vi.fn();
|
|
127
|
+
render(
|
|
128
|
+
<LocaleProvider locale="de-DE">
|
|
129
|
+
<NumberInput
|
|
130
|
+
aria-label="Betrag"
|
|
131
|
+
defaultValue={0}
|
|
132
|
+
step={0.1}
|
|
133
|
+
onValueChange={onValueChange}
|
|
134
|
+
/>
|
|
135
|
+
</LocaleProvider>,
|
|
136
|
+
);
|
|
137
|
+
const input = screen.getByRole("spinbutton", { name: "Betrag" });
|
|
138
|
+
await userEvent.clear(input);
|
|
139
|
+
await userEvent.type(input, "1.234,5");
|
|
140
|
+
await userEvent.tab();
|
|
141
|
+
expect(onValueChange).toHaveBeenLastCalledWith(1234.5);
|
|
142
|
+
});
|
|
96
143
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// a2ui.exposed: yes
|
|
2
4
|
import { forwardRef, useCallback, useRef, useState, type HTMLAttributes } from "react";
|
|
3
5
|
import { Minus, Plus } from "lucide-react";
|
|
4
|
-
import { cn } from "../../lib/cn";
|
|
5
6
|
import { InputGroup, InputGroupButton, InputGroupInput } from "../input-group";
|
|
7
|
+
import { useLocale } from "../locale-provider";
|
|
6
8
|
|
|
7
9
|
export interface NumberInputProps extends Omit<
|
|
8
10
|
HTMLAttributes<HTMLDivElement>,
|
|
@@ -56,11 +58,37 @@ function clampValue(val: number, min?: number, max?: number): number {
|
|
|
56
58
|
return result;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
/** Number of decimal places implied by a step (e.g. 0.1 → 1, 1 → 0). */
|
|
62
|
+
function stepDecimals(step: number): number {
|
|
63
|
+
if (!isFinite(step) || step === 0) return 0;
|
|
64
|
+
const s = String(step);
|
|
65
|
+
const i = s.indexOf(".");
|
|
66
|
+
return i === -1 ? 0 : s.length - i - 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Round `value` to the decimal precision implied by `step`, avoiding fp noise (0.1+0.2). */
|
|
70
|
+
function roundToStep(value: number, step: number): number {
|
|
71
|
+
const decimals = stepDecimals(step);
|
|
72
|
+
if (decimals === 0) return Math.round(value);
|
|
73
|
+
const factor = 10 ** decimals;
|
|
74
|
+
return Math.round(value * factor) / factor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The active locale's decimal and group separator characters (e.g. "," / "." in de-DE). */
|
|
78
|
+
function getLocaleSeparators(locale: string): { decimal: string; group: string } {
|
|
79
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(1234.5);
|
|
80
|
+
const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
81
|
+
const group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
82
|
+
return { decimal, group };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Parse a locale-formatted numeric string (group + decimal separators) into a number. */
|
|
86
|
+
function parseLocaleNumber(raw: string, locale: string): number {
|
|
87
|
+
const { decimal, group } = getLocaleSeparators(locale);
|
|
88
|
+
let normalized = raw.trim();
|
|
89
|
+
if (group) normalized = normalized.split(group).join("");
|
|
90
|
+
if (decimal !== ".") normalized = normalized.split(decimal).join(".");
|
|
91
|
+
return Number(normalized);
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
/**
|
|
@@ -69,7 +97,7 @@ function formatNum(value: number, formatOptions?: Intl.NumberFormatOptions): str
|
|
|
69
97
|
* Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
|
|
70
98
|
* Keyboard ↑/↓ = ±step; Shift+↑/↓ = ±step×10. Clamps on blur (not per keystroke).
|
|
71
99
|
*/
|
|
72
|
-
export const NumberInput = forwardRef<
|
|
100
|
+
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(function NumberInput(
|
|
73
101
|
{
|
|
74
102
|
value: valueProp,
|
|
75
103
|
defaultValue,
|
|
@@ -94,14 +122,20 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
|
|
|
94
122
|
},
|
|
95
123
|
ref,
|
|
96
124
|
) {
|
|
125
|
+
const { locale, formatNumber, t } = useLocale();
|
|
97
126
|
const isControlled = valueProp !== undefined;
|
|
98
127
|
|
|
128
|
+
const formatValue = useCallback(
|
|
129
|
+
(n: number) => formatNumber(n, formatOptions),
|
|
130
|
+
[formatNumber, formatOptions],
|
|
131
|
+
);
|
|
132
|
+
|
|
99
133
|
const [internalValue, setInternalValue] = useState<number | null>(
|
|
100
134
|
defaultValue !== undefined ? defaultValue : null,
|
|
101
135
|
);
|
|
102
136
|
const [inputText, setInputText] = useState<string>(() => {
|
|
103
137
|
const initial = isControlled ? valueProp : defaultValue;
|
|
104
|
-
return initial != null ?
|
|
138
|
+
return initial != null ? formatValue(initial) : "";
|
|
105
139
|
});
|
|
106
140
|
// Track whether the user is actively editing to avoid overwriting their input
|
|
107
141
|
const isEditing = useRef(false);
|
|
@@ -120,12 +154,12 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
|
|
|
120
154
|
(delta: number) => {
|
|
121
155
|
if (disabled || readOnly) return;
|
|
122
156
|
const base = currentValue ?? 0;
|
|
123
|
-
let next = base + delta;
|
|
157
|
+
let next = roundToStep(base + delta, step);
|
|
124
158
|
if (clamp) next = clampValue(next, min, max);
|
|
125
|
-
setInputText(
|
|
159
|
+
setInputText(formatValue(next));
|
|
126
160
|
commit(next);
|
|
127
161
|
},
|
|
128
|
-
[disabled, readOnly, currentValue, clamp, min, max,
|
|
162
|
+
[disabled, readOnly, currentValue, step, clamp, min, max, formatValue, commit],
|
|
129
163
|
);
|
|
130
164
|
|
|
131
165
|
const handleKeyDown = useCallback(
|
|
@@ -152,22 +186,23 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
|
|
|
152
186
|
commit(null);
|
|
153
187
|
return;
|
|
154
188
|
}
|
|
155
|
-
const parsed =
|
|
189
|
+
const parsed = parseLocaleNumber(raw, locale);
|
|
156
190
|
if (isNaN(parsed)) {
|
|
157
191
|
// Revert to last known good value
|
|
158
|
-
setInputText(currentValue != null ?
|
|
192
|
+
setInputText(currentValue != null ? formatValue(currentValue) : "");
|
|
159
193
|
return;
|
|
160
194
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
195
|
+
const rounded = roundToStep(parsed, step);
|
|
196
|
+
const clamped = clamp ? clampValue(rounded, min, max) : rounded;
|
|
197
|
+
setInputText(formatValue(clamped));
|
|
163
198
|
commit(clamped);
|
|
164
|
-
}, [inputText, currentValue, clamp, min, max,
|
|
199
|
+
}, [inputText, currentValue, clamp, min, max, step, locale, formatValue, commit]);
|
|
165
200
|
|
|
166
201
|
// Keep display text in sync with controlled value changes (only when not editing)
|
|
167
202
|
const prevControlledValue = useRef(valueProp);
|
|
168
203
|
if (isControlled && valueProp !== prevControlledValue.current && !isEditing.current) {
|
|
169
204
|
prevControlledValue.current = valueProp;
|
|
170
|
-
const nextText = valueProp != null ?
|
|
205
|
+
const nextText = valueProp != null ? formatValue(valueProp) : "";
|
|
171
206
|
if (nextText !== inputText) setInputText(nextText);
|
|
172
207
|
}
|
|
173
208
|
|
|
@@ -177,18 +212,24 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
|
|
|
177
212
|
disabled || readOnly || (max !== undefined && (currentValue ?? 0) >= max);
|
|
178
213
|
|
|
179
214
|
return (
|
|
180
|
-
<InputGroup
|
|
215
|
+
<InputGroup className={className} {...props}>
|
|
181
216
|
<InputGroupButton
|
|
182
|
-
aria-label="
|
|
217
|
+
aria-label={t("ui.numberInput.decrease")}
|
|
183
218
|
tabIndex={-1}
|
|
184
219
|
disabled={decrementDisabled}
|
|
185
220
|
onClick={() => applyStep(-step)}
|
|
186
|
-
className="rounded-
|
|
221
|
+
className="rounded-e-none"
|
|
187
222
|
>
|
|
188
223
|
<Minus aria-hidden="true" />
|
|
189
224
|
</InputGroupButton>
|
|
190
225
|
<InputGroupInput
|
|
191
|
-
|
|
226
|
+
ref={ref}
|
|
227
|
+
type="text"
|
|
228
|
+
inputMode="decimal"
|
|
229
|
+
role="spinbutton"
|
|
230
|
+
aria-valuenow={currentValue ?? undefined}
|
|
231
|
+
aria-valuemin={min}
|
|
232
|
+
aria-valuemax={max}
|
|
192
233
|
id={id}
|
|
193
234
|
name={name}
|
|
194
235
|
value={inputText}
|
|
@@ -200,17 +241,17 @@ export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function
|
|
|
200
241
|
aria-describedby={ariaDescribedby}
|
|
201
242
|
aria-invalid={ariaInvalid}
|
|
202
243
|
aria-required={ariaRequired}
|
|
203
|
-
className="text-center
|
|
244
|
+
className="text-center"
|
|
204
245
|
onChange={handleChange}
|
|
205
246
|
onKeyDown={handleKeyDown}
|
|
206
247
|
onBlur={handleBlur}
|
|
207
248
|
/>
|
|
208
249
|
<InputGroupButton
|
|
209
|
-
aria-label="
|
|
250
|
+
aria-label={t("ui.numberInput.increase")}
|
|
210
251
|
tabIndex={-1}
|
|
211
252
|
disabled={incrementDisabled}
|
|
212
253
|
onClick={() => applyStep(step)}
|
|
213
|
-
className="rounded-
|
|
254
|
+
className="rounded-s-none"
|
|
214
255
|
>
|
|
215
256
|
<Plus aria-hidden="true" />
|
|
216
257
|
</InputGroupButton>
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import { render, screen } from "@testing-library/react";
|
|
3
4
|
import { LocaleProvider } from "../locale-provider";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PaginationEllipsis,
|
|
7
|
+
PaginationLink,
|
|
8
|
+
PaginationNext,
|
|
9
|
+
PaginationPrevious,
|
|
10
|
+
} from "./pagination";
|
|
11
|
+
|
|
12
|
+
describe("PaginationLink — forwardRef", () => {
|
|
13
|
+
it("forwards its ref to the rendered <a>", () => {
|
|
14
|
+
const ref = createRef<HTMLAnchorElement>();
|
|
15
|
+
render(
|
|
16
|
+
<PaginationLink ref={ref} href="#2">
|
|
17
|
+
2
|
|
18
|
+
</PaginationLink>,
|
|
19
|
+
);
|
|
20
|
+
expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
|
|
21
|
+
expect(ref.current).toBe(screen.getByText("2"));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
5
24
|
|
|
6
25
|
/**
|
|
7
26
|
* Pagination had no test file before #18 — its three microcopy sites
|
|
@@ -5,10 +5,11 @@ import { buttonVariants } from "../button";
|
|
|
5
5
|
import { useLocale } from "../locale-provider";
|
|
6
6
|
|
|
7
7
|
export function Pagination({ className, ...props }: ComponentProps<"nav">) {
|
|
8
|
+
const { t } = useLocale();
|
|
8
9
|
return (
|
|
9
10
|
<nav
|
|
10
11
|
role="navigation"
|
|
11
|
-
aria-label="pagination"
|
|
12
|
+
aria-label={t("ui.pagination.label")}
|
|
12
13
|
className={cn("mx-auto flex w-full justify-center", className)}
|
|
13
14
|
{...props}
|
|
14
15
|
/>
|
|
@@ -27,19 +28,22 @@ export const PaginationItem = forwardRef<HTMLLIElement, ComponentProps<"li">>(
|
|
|
27
28
|
},
|
|
28
29
|
);
|
|
29
30
|
type PaginationLinkProps = { isActive?: boolean } & ComponentProps<"a">;
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
export const PaginationLink = forwardRef<HTMLAnchorElement, PaginationLinkProps>(
|
|
32
|
+
function PaginationLink({ className, isActive, ...props }, ref) {
|
|
33
|
+
return (
|
|
34
|
+
<a
|
|
35
|
+
ref={ref}
|
|
36
|
+
aria-current={isActive ? "page" : undefined}
|
|
37
|
+
className={cn(
|
|
38
|
+
buttonVariants({ variant: isActive ? "outline" : "ghost", size: "icon" }),
|
|
39
|
+
"cursor-pointer",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
43
47
|
export function PaginationPrevious({ className, ...props }: ComponentProps<"a">) {
|
|
44
48
|
const { t } = useLocale();
|
|
45
49
|
return (
|