@emara/ui 1.1.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/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
RiArrowLeftDoubleLine,
|
|
6
|
+
RiArrowLeftSLine,
|
|
7
|
+
RiArrowRightDoubleLine,
|
|
8
|
+
RiArrowRightSLine,
|
|
9
|
+
RiMoreLine,
|
|
10
|
+
} from "@remixicon/react";
|
|
11
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
12
|
+
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
import { buttonVariants } from "./button";
|
|
15
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
16
|
+
|
|
17
|
+
// Per docs/emara-ui-phase-4-components.md §2.
|
|
18
|
+
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
// 2.1 Primitives
|
|
21
|
+
// ===========================================================================
|
|
22
|
+
|
|
23
|
+
const Pagination = forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(function Pagination(
|
|
24
|
+
{ className, ...props },
|
|
25
|
+
ref,
|
|
26
|
+
) {
|
|
27
|
+
return (
|
|
28
|
+
<nav
|
|
29
|
+
ref={ref}
|
|
30
|
+
role="navigation"
|
|
31
|
+
aria-label="Pagination"
|
|
32
|
+
className={cn("flex w-full items-center justify-between gap-3", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
Pagination.displayName = "Pagination";
|
|
38
|
+
|
|
39
|
+
const PaginationContent = forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(
|
|
40
|
+
function PaginationContent({ className, ...props }, ref) {
|
|
41
|
+
return (
|
|
42
|
+
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
PaginationContent.displayName = "PaginationContent";
|
|
47
|
+
|
|
48
|
+
const PaginationItem = forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
|
|
49
|
+
function PaginationItem({ className, ...props }, ref) {
|
|
50
|
+
return <li ref={ref} className={cn("", className)} {...props} />;
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
PaginationItem.displayName = "PaginationItem";
|
|
54
|
+
|
|
55
|
+
const paginationLinkVariants = cva(
|
|
56
|
+
"inline-flex items-center justify-center select-none transition-colors gap-1 cursor-pointer disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
57
|
+
{
|
|
58
|
+
variants: {
|
|
59
|
+
size: {
|
|
60
|
+
sm: "h-8 min-w-8 text-xs rounded-md px-2.5",
|
|
61
|
+
md: "h-9 min-w-9 text-sm rounded-md px-3",
|
|
62
|
+
lg: "h-10 min-w-10 text-base rounded-md px-3.5",
|
|
63
|
+
},
|
|
64
|
+
active: {
|
|
65
|
+
true: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
66
|
+
false:
|
|
67
|
+
"text-foreground hover:bg-accent hover:text-accent-foreground border border-input bg-background",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
defaultVariants: { size: "md", active: false },
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
type PaginationLinkProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
75
|
+
VariantProps<typeof paginationLinkVariants> & {
|
|
76
|
+
isActive?: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const PaginationLink = forwardRef<HTMLButtonElement, PaginationLinkProps>(function PaginationLink(
|
|
80
|
+
{ className, size, active, isActive, type = "button", ...props },
|
|
81
|
+
ref,
|
|
82
|
+
) {
|
|
83
|
+
const resolvedActive = active ?? isActive ?? false;
|
|
84
|
+
return (
|
|
85
|
+
<button
|
|
86
|
+
ref={ref}
|
|
87
|
+
type={type}
|
|
88
|
+
aria-current={resolvedActive ? "page" : undefined}
|
|
89
|
+
className={cn(paginationLinkVariants({ size, active: resolvedActive }), className)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
PaginationLink.displayName = "PaginationLink";
|
|
95
|
+
|
|
96
|
+
// Prev / Next ----------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
type PaginationDirectionalProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
99
|
+
VariantProps<typeof paginationLinkVariants> & {
|
|
100
|
+
label?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const PaginationPrevious = forwardRef<HTMLButtonElement, PaginationDirectionalProps>(
|
|
104
|
+
function PaginationPrevious(
|
|
105
|
+
{ className, size, label = "Go to previous page", type = "button", ...props },
|
|
106
|
+
ref,
|
|
107
|
+
) {
|
|
108
|
+
// Use `buttonVariants` shape for outline look; primary visual is the link variant.
|
|
109
|
+
void buttonVariants;
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
ref={ref}
|
|
113
|
+
type={type}
|
|
114
|
+
aria-label={label}
|
|
115
|
+
className={cn(
|
|
116
|
+
paginationLinkVariants({ size, active: false }),
|
|
117
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
{...props}
|
|
121
|
+
>
|
|
122
|
+
<RiArrowLeftSLine className="rtl-mirror" />
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
PaginationPrevious.displayName = "PaginationPrevious";
|
|
128
|
+
|
|
129
|
+
const PaginationNext = forwardRef<HTMLButtonElement, PaginationDirectionalProps>(
|
|
130
|
+
function PaginationNext(
|
|
131
|
+
{ className, size, label = "Go to next page", type = "button", ...props },
|
|
132
|
+
ref,
|
|
133
|
+
) {
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
ref={ref}
|
|
137
|
+
type={type}
|
|
138
|
+
aria-label={label}
|
|
139
|
+
className={cn(
|
|
140
|
+
paginationLinkVariants({ size, active: false }),
|
|
141
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
<RiArrowRightSLine className="rtl-mirror" />
|
|
147
|
+
</button>
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
PaginationNext.displayName = "PaginationNext";
|
|
152
|
+
|
|
153
|
+
const PaginationFirst = forwardRef<HTMLButtonElement, PaginationDirectionalProps>(
|
|
154
|
+
function PaginationFirst(
|
|
155
|
+
{ className, size, label = "Go to first page", type = "button", ...props },
|
|
156
|
+
ref,
|
|
157
|
+
) {
|
|
158
|
+
return (
|
|
159
|
+
<button
|
|
160
|
+
ref={ref}
|
|
161
|
+
type={type}
|
|
162
|
+
aria-label={label}
|
|
163
|
+
className={cn(
|
|
164
|
+
paginationLinkVariants({ size, active: false }),
|
|
165
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
166
|
+
className,
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
<RiArrowLeftDoubleLine className="rtl-mirror" />
|
|
171
|
+
</button>
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
PaginationFirst.displayName = "PaginationFirst";
|
|
176
|
+
|
|
177
|
+
const PaginationLast = forwardRef<HTMLButtonElement, PaginationDirectionalProps>(
|
|
178
|
+
function PaginationLast(
|
|
179
|
+
{ className, size, label = "Go to last page", type = "button", ...props },
|
|
180
|
+
ref,
|
|
181
|
+
) {
|
|
182
|
+
return (
|
|
183
|
+
<button
|
|
184
|
+
ref={ref}
|
|
185
|
+
type={type}
|
|
186
|
+
aria-label={label}
|
|
187
|
+
className={cn(
|
|
188
|
+
paginationLinkVariants({ size, active: false }),
|
|
189
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
190
|
+
className,
|
|
191
|
+
)}
|
|
192
|
+
{...props}
|
|
193
|
+
>
|
|
194
|
+
<RiArrowRightDoubleLine className="rtl-mirror" />
|
|
195
|
+
</button>
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
PaginationLast.displayName = "PaginationLast";
|
|
200
|
+
|
|
201
|
+
const PaginationEllipsis = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
202
|
+
function PaginationEllipsis({ className, ...props }, ref) {
|
|
203
|
+
return (
|
|
204
|
+
<span
|
|
205
|
+
ref={ref}
|
|
206
|
+
aria-hidden="true"
|
|
207
|
+
className={cn(
|
|
208
|
+
"text-muted-foreground inline-flex h-9 min-w-9 items-center justify-center",
|
|
209
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
210
|
+
className,
|
|
211
|
+
)}
|
|
212
|
+
{...props}
|
|
213
|
+
>
|
|
214
|
+
<RiMoreLine />
|
|
215
|
+
<span className="sr-only">More pages</span>
|
|
216
|
+
</span>
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
221
|
+
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
// 2.2 DataPagination
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
|
|
226
|
+
interface DataPaginationProps {
|
|
227
|
+
page: number;
|
|
228
|
+
pageCount: number;
|
|
229
|
+
onPageChange: (page: number) => void;
|
|
230
|
+
pageSize?: number;
|
|
231
|
+
pageSizeOptions?: number[];
|
|
232
|
+
onPageSizeChange?: (size: number) => void;
|
|
233
|
+
totalItems?: number;
|
|
234
|
+
siblingCount?: number;
|
|
235
|
+
boundaryCount?: number;
|
|
236
|
+
showInfo?: boolean;
|
|
237
|
+
showPageSize?: boolean;
|
|
238
|
+
showFirstLast?: boolean;
|
|
239
|
+
size?: "sm" | "md" | "lg";
|
|
240
|
+
disabled?: boolean;
|
|
241
|
+
loading?: boolean;
|
|
242
|
+
className?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Builds the list of page tokens to render. */
|
|
246
|
+
function buildPageList(
|
|
247
|
+
page: number,
|
|
248
|
+
pageCount: number,
|
|
249
|
+
siblingCount: number,
|
|
250
|
+
boundaryCount: number,
|
|
251
|
+
): Array<number | "ellipsis-start" | "ellipsis-end"> {
|
|
252
|
+
if (pageCount <= 0) return [];
|
|
253
|
+
const totalNumbers = siblingCount * 2 + boundaryCount * 2 + 3;
|
|
254
|
+
if (pageCount <= totalNumbers) {
|
|
255
|
+
return Array.from({ length: pageCount }, (_, i) => i + 1);
|
|
256
|
+
}
|
|
257
|
+
const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1);
|
|
258
|
+
const endPages = Array.from(
|
|
259
|
+
{ length: boundaryCount },
|
|
260
|
+
(_, i) => pageCount - boundaryCount + i + 1,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const siblingsStart = Math.max(
|
|
264
|
+
Math.min(page - siblingCount, pageCount - boundaryCount - siblingCount * 2 - 1),
|
|
265
|
+
boundaryCount + 2,
|
|
266
|
+
);
|
|
267
|
+
const siblingsEnd = Math.min(
|
|
268
|
+
Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),
|
|
269
|
+
pageCount - boundaryCount - 1,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const middle = Array.from(
|
|
273
|
+
{ length: Math.max(siblingsEnd - siblingsStart + 1, 0) },
|
|
274
|
+
(_, i) => siblingsStart + i,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const result: Array<number | "ellipsis-start" | "ellipsis-end"> = [...startPages];
|
|
278
|
+
if (siblingsStart > boundaryCount + 2) result.push("ellipsis-start");
|
|
279
|
+
else if (boundaryCount + 1 < pageCount - boundaryCount) result.push(boundaryCount + 1);
|
|
280
|
+
result.push(...middle);
|
|
281
|
+
if (siblingsEnd < pageCount - boundaryCount - 1) result.push("ellipsis-end");
|
|
282
|
+
else if (pageCount - boundaryCount > boundaryCount) result.push(pageCount - boundaryCount);
|
|
283
|
+
result.push(...endPages);
|
|
284
|
+
return Array.from(new Set(result));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const DataPagination = forwardRef<HTMLElement, DataPaginationProps>(function DataPagination(
|
|
288
|
+
{
|
|
289
|
+
page,
|
|
290
|
+
pageCount,
|
|
291
|
+
onPageChange,
|
|
292
|
+
pageSize,
|
|
293
|
+
pageSizeOptions,
|
|
294
|
+
onPageSizeChange,
|
|
295
|
+
totalItems,
|
|
296
|
+
siblingCount = 1,
|
|
297
|
+
boundaryCount = 1,
|
|
298
|
+
showInfo = true,
|
|
299
|
+
showPageSize,
|
|
300
|
+
showFirstLast = false,
|
|
301
|
+
size = "md",
|
|
302
|
+
disabled = false,
|
|
303
|
+
loading = false,
|
|
304
|
+
className,
|
|
305
|
+
},
|
|
306
|
+
ref,
|
|
307
|
+
) {
|
|
308
|
+
const pages = useMemo(
|
|
309
|
+
() => buildPageList(page, pageCount, siblingCount, boundaryCount),
|
|
310
|
+
[page, pageCount, siblingCount, boundaryCount],
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const isDisabled = disabled || loading;
|
|
314
|
+
const onFirst = () => onPageChange(1);
|
|
315
|
+
const onPrev = () => onPageChange(Math.max(1, page - 1));
|
|
316
|
+
const onNext = () => onPageChange(Math.min(pageCount, page + 1));
|
|
317
|
+
const onLast = () => onPageChange(pageCount);
|
|
318
|
+
|
|
319
|
+
const start = pageSize && totalItems ? Math.min((page - 1) * pageSize + 1, totalItems) : null;
|
|
320
|
+
const end = pageSize && totalItems ? Math.min(page * pageSize, totalItems) : null;
|
|
321
|
+
|
|
322
|
+
const showSizeSelector =
|
|
323
|
+
(showPageSize ?? Boolean(pageSizeOptions && pageSizeOptions.length > 0)) &&
|
|
324
|
+
pageSizeOptions &&
|
|
325
|
+
pageSize !== undefined &&
|
|
326
|
+
onPageSizeChange;
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<Pagination ref={ref} className={cn("flex-wrap gap-3", className)}>
|
|
330
|
+
{showInfo && start !== null && end !== null && totalItems !== undefined ? (
|
|
331
|
+
// Announce range changes to screen readers without re-announcing the
|
|
332
|
+
// whole pagination — `aria-live="polite"` + `aria-atomic` updates
|
|
333
|
+
// only the count text when page changes. Per spec phase-4 §2.2.
|
|
334
|
+
<div
|
|
335
|
+
className="text-muted-foreground text-sm tabular-nums"
|
|
336
|
+
aria-live="polite"
|
|
337
|
+
aria-atomic="true"
|
|
338
|
+
>
|
|
339
|
+
Showing {start}–{end} of {totalItems}
|
|
340
|
+
</div>
|
|
341
|
+
) : (
|
|
342
|
+
<span />
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
<PaginationContent>
|
|
346
|
+
{showFirstLast ? (
|
|
347
|
+
<PaginationItem>
|
|
348
|
+
<PaginationFirst size={size} onClick={onFirst} disabled={isDisabled || page <= 1} />
|
|
349
|
+
</PaginationItem>
|
|
350
|
+
) : null}
|
|
351
|
+
<PaginationItem>
|
|
352
|
+
<PaginationPrevious size={size} onClick={onPrev} disabled={isDisabled || page <= 1} />
|
|
353
|
+
</PaginationItem>
|
|
354
|
+
|
|
355
|
+
{pages.map((p) =>
|
|
356
|
+
typeof p === "number" ? (
|
|
357
|
+
<PaginationItem key={p}>
|
|
358
|
+
<PaginationLink
|
|
359
|
+
size={size}
|
|
360
|
+
isActive={p === page}
|
|
361
|
+
aria-label={`Go to page ${p}`}
|
|
362
|
+
disabled={isDisabled}
|
|
363
|
+
onClick={() => onPageChange(p)}
|
|
364
|
+
>
|
|
365
|
+
{p}
|
|
366
|
+
</PaginationLink>
|
|
367
|
+
</PaginationItem>
|
|
368
|
+
) : (
|
|
369
|
+
<PaginationItem key={p}>
|
|
370
|
+
<PaginationEllipsis />
|
|
371
|
+
</PaginationItem>
|
|
372
|
+
),
|
|
373
|
+
)}
|
|
374
|
+
|
|
375
|
+
<PaginationItem>
|
|
376
|
+
<PaginationNext size={size} onClick={onNext} disabled={isDisabled || page >= pageCount} />
|
|
377
|
+
</PaginationItem>
|
|
378
|
+
{showFirstLast ? (
|
|
379
|
+
<PaginationItem>
|
|
380
|
+
<PaginationLast
|
|
381
|
+
size={size}
|
|
382
|
+
onClick={onLast}
|
|
383
|
+
disabled={isDisabled || page >= pageCount}
|
|
384
|
+
/>
|
|
385
|
+
</PaginationItem>
|
|
386
|
+
) : null}
|
|
387
|
+
</PaginationContent>
|
|
388
|
+
|
|
389
|
+
{showSizeSelector ? (
|
|
390
|
+
<Select
|
|
391
|
+
value={String(pageSize)}
|
|
392
|
+
onValueChange={(v) => onPageSizeChange?.(Number(v))}
|
|
393
|
+
disabled={isDisabled}
|
|
394
|
+
>
|
|
395
|
+
<SelectTrigger size={size} aria-label="Rows per page" className="w-auto">
|
|
396
|
+
<SelectValue />
|
|
397
|
+
</SelectTrigger>
|
|
398
|
+
<SelectContent>
|
|
399
|
+
{pageSizeOptions.map((opt) => (
|
|
400
|
+
<SelectItem key={opt} value={String(opt)}>
|
|
401
|
+
{opt} per page
|
|
402
|
+
</SelectItem>
|
|
403
|
+
))}
|
|
404
|
+
</SelectContent>
|
|
405
|
+
</Select>
|
|
406
|
+
) : (
|
|
407
|
+
<span />
|
|
408
|
+
)}
|
|
409
|
+
</Pagination>
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
DataPagination.displayName = "DataPagination";
|
|
413
|
+
|
|
414
|
+
export {
|
|
415
|
+
Pagination,
|
|
416
|
+
PaginationContent,
|
|
417
|
+
PaginationItem,
|
|
418
|
+
PaginationLink,
|
|
419
|
+
PaginationPrevious,
|
|
420
|
+
PaginationNext,
|
|
421
|
+
PaginationFirst,
|
|
422
|
+
PaginationLast,
|
|
423
|
+
PaginationEllipsis,
|
|
424
|
+
DataPagination,
|
|
425
|
+
paginationLinkVariants,
|
|
426
|
+
};
|
|
427
|
+
export type { PaginationLinkProps, PaginationDirectionalProps, DataPaginationProps };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { RiInformationLine } from "@remixicon/react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import { Input } from "./input";
|
|
6
|
+
import { Label } from "./label";
|
|
7
|
+
import {
|
|
8
|
+
Popover,
|
|
9
|
+
PopoverContent,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
} from "./popover";
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof Popover> = {
|
|
14
|
+
title: "Foundations/Popover",
|
|
15
|
+
component: Popover,
|
|
16
|
+
parameters: { layout: "centered" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<Popover>
|
|
25
|
+
<PopoverTrigger asChild>
|
|
26
|
+
<Button variant="outline">Open popover</Button>
|
|
27
|
+
</PopoverTrigger>
|
|
28
|
+
<PopoverContent>
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
<h3 className="text-sm font-semibold">Quick note</h3>
|
|
31
|
+
<p className="text-sm text-muted-foreground">
|
|
32
|
+
Popovers are non-modal floating panels.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
</PopoverContent>
|
|
36
|
+
</Popover>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Sides: Story = {
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="grid grid-cols-2 gap-12">
|
|
43
|
+
{(["top", "right", "bottom", "left"] as const).map((side) => (
|
|
44
|
+
<Popover key={side}>
|
|
45
|
+
<PopoverTrigger asChild>
|
|
46
|
+
<Button variant="outline" size="sm">
|
|
47
|
+
side="{side}"
|
|
48
|
+
</Button>
|
|
49
|
+
</PopoverTrigger>
|
|
50
|
+
<PopoverContent side={side} width="fit">
|
|
51
|
+
<p className="text-sm">Anchored {side}.</p>
|
|
52
|
+
</PopoverContent>
|
|
53
|
+
</Popover>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Aligns: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<div className="flex gap-4">
|
|
62
|
+
{(["start", "center", "end"] as const).map((align) => (
|
|
63
|
+
<Popover key={align}>
|
|
64
|
+
<PopoverTrigger asChild>
|
|
65
|
+
<Button variant="outline" size="sm">
|
|
66
|
+
align="{align}"
|
|
67
|
+
</Button>
|
|
68
|
+
</PopoverTrigger>
|
|
69
|
+
<PopoverContent align={align} width="fit">
|
|
70
|
+
<p className="text-sm">align={align}</p>
|
|
71
|
+
</PopoverContent>
|
|
72
|
+
</Popover>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const WithArrow: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<Popover defaultOpen>
|
|
81
|
+
<PopoverTrigger asChild>
|
|
82
|
+
<Button variant="outline">With arrow</Button>
|
|
83
|
+
</PopoverTrigger>
|
|
84
|
+
<PopoverContent arrow>
|
|
85
|
+
<p className="text-sm">An arrow points at the trigger.</p>
|
|
86
|
+
</PopoverContent>
|
|
87
|
+
</Popover>
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const WidthFit: Story = {
|
|
92
|
+
render: () => (
|
|
93
|
+
<Popover defaultOpen>
|
|
94
|
+
<PopoverTrigger asChild>
|
|
95
|
+
<Button variant="outline">width="fit"</Button>
|
|
96
|
+
</PopoverTrigger>
|
|
97
|
+
<PopoverContent width="fit">
|
|
98
|
+
<p className="text-sm whitespace-nowrap">Sized to content width.</p>
|
|
99
|
+
</PopoverContent>
|
|
100
|
+
</Popover>
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const WidthTrigger: Story = {
|
|
105
|
+
render: () => (
|
|
106
|
+
<Popover defaultOpen>
|
|
107
|
+
<PopoverTrigger asChild>
|
|
108
|
+
<Button variant="outline" className="w-72">
|
|
109
|
+
width="trigger" (button is 18rem)
|
|
110
|
+
</Button>
|
|
111
|
+
</PopoverTrigger>
|
|
112
|
+
<PopoverContent width="trigger">
|
|
113
|
+
<p className="text-sm">Matches the trigger width.</p>
|
|
114
|
+
</PopoverContent>
|
|
115
|
+
</Popover>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const WidthPresets: Story = {
|
|
120
|
+
render: () => (
|
|
121
|
+
<div className="flex gap-3">
|
|
122
|
+
{(["sm", "md", "lg", "xl"] as const).map((w) => (
|
|
123
|
+
<Popover key={w}>
|
|
124
|
+
<PopoverTrigger asChild>
|
|
125
|
+
<Button variant="outline" size="sm">
|
|
126
|
+
{w}
|
|
127
|
+
</Button>
|
|
128
|
+
</PopoverTrigger>
|
|
129
|
+
<PopoverContent width={w}>
|
|
130
|
+
<p className="text-sm">width="{w}"</p>
|
|
131
|
+
</PopoverContent>
|
|
132
|
+
</Popover>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const ScrollableMaxHeight: Story = {
|
|
139
|
+
render: () => (
|
|
140
|
+
<Popover defaultOpen>
|
|
141
|
+
<PopoverTrigger asChild>
|
|
142
|
+
<Button variant="outline">maxHeight=200</Button>
|
|
143
|
+
</PopoverTrigger>
|
|
144
|
+
<PopoverContent width="md" maxHeight={200}>
|
|
145
|
+
<ul className="space-y-2 text-sm">
|
|
146
|
+
{Array.from({ length: 20 }).map((_, i) => (
|
|
147
|
+
<li key={i}>Item {i + 1}</li>
|
|
148
|
+
))}
|
|
149
|
+
</ul>
|
|
150
|
+
</PopoverContent>
|
|
151
|
+
</Popover>
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const FormInside: Story = {
|
|
156
|
+
render: () => (
|
|
157
|
+
<Popover>
|
|
158
|
+
<PopoverTrigger asChild>
|
|
159
|
+
<Button>Edit dimensions</Button>
|
|
160
|
+
</PopoverTrigger>
|
|
161
|
+
<PopoverContent width="md">
|
|
162
|
+
<div className="space-y-3">
|
|
163
|
+
<div className="space-y-1">
|
|
164
|
+
<Label htmlFor="width" size="sm">
|
|
165
|
+
Width
|
|
166
|
+
</Label>
|
|
167
|
+
<Input id="width" type="number" defaultValue="100" size="sm" />
|
|
168
|
+
</div>
|
|
169
|
+
<div className="space-y-1">
|
|
170
|
+
<Label htmlFor="height" size="sm">
|
|
171
|
+
Height
|
|
172
|
+
</Label>
|
|
173
|
+
<Input id="height" type="number" defaultValue="100" size="sm" />
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</PopoverContent>
|
|
177
|
+
</Popover>
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const Loading: Story = {
|
|
182
|
+
render: () => (
|
|
183
|
+
<Popover defaultOpen>
|
|
184
|
+
<PopoverTrigger asChild>
|
|
185
|
+
<Button variant="outline">
|
|
186
|
+
<RiInformationLine />
|
|
187
|
+
Loading
|
|
188
|
+
</Button>
|
|
189
|
+
</PopoverTrigger>
|
|
190
|
+
<PopoverContent width="md" loading />
|
|
191
|
+
</Popover>
|
|
192
|
+
),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const PaddingScale: Story = {
|
|
196
|
+
render: () => (
|
|
197
|
+
<div className="flex gap-3">
|
|
198
|
+
{(["none", "sm", "md", "lg"] as const).map((p) => (
|
|
199
|
+
<Popover key={p}>
|
|
200
|
+
<PopoverTrigger asChild>
|
|
201
|
+
<Button variant="outline" size="sm">
|
|
202
|
+
{p}
|
|
203
|
+
</Button>
|
|
204
|
+
</PopoverTrigger>
|
|
205
|
+
<PopoverContent padding={p}>
|
|
206
|
+
<p className="text-sm">padding="{p}"</p>
|
|
207
|
+
</PopoverContent>
|
|
208
|
+
</Popover>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
),
|
|
212
|
+
};
|