@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.
Files changed (218) hide show
  1. package/components/ui/.gitkeep +0 -0
  2. package/components/ui/accordion.stories.tsx +231 -0
  3. package/components/ui/accordion.tsx +250 -0
  4. package/components/ui/app-shell.stories.tsx +270 -0
  5. package/components/ui/app-shell.tsx +491 -0
  6. package/components/ui/avatar.stories.tsx +174 -0
  7. package/components/ui/avatar.tsx +257 -0
  8. package/components/ui/badge.stories.tsx +127 -0
  9. package/components/ui/badge.tsx +146 -0
  10. package/components/ui/breadcrumb.stories.tsx +92 -0
  11. package/components/ui/breadcrumb.tsx +302 -0
  12. package/components/ui/button.stories.tsx +186 -0
  13. package/components/ui/button.tsx +128 -0
  14. package/components/ui/card.stories.tsx +279 -0
  15. package/components/ui/card.tsx +250 -0
  16. package/components/ui/checkbox.stories.tsx +93 -0
  17. package/components/ui/checkbox.tsx +131 -0
  18. package/components/ui/combobox.stories.tsx +489 -0
  19. package/components/ui/combobox.tsx +874 -0
  20. package/components/ui/context-menu.stories.tsx +202 -0
  21. package/components/ui/context-menu.tsx +309 -0
  22. package/components/ui/data-table.stories.tsx +227 -0
  23. package/components/ui/data-table.tsx +539 -0
  24. package/components/ui/date-picker.stories.tsx +225 -0
  25. package/components/ui/date-picker.tsx +597 -0
  26. package/components/ui/dialog.stories.tsx +193 -0
  27. package/components/ui/dialog.tsx +262 -0
  28. package/components/ui/divider.stories.tsx +84 -0
  29. package/components/ui/divider.tsx +135 -0
  30. package/components/ui/drawer.stories.tsx +218 -0
  31. package/components/ui/drawer.tsx +329 -0
  32. package/components/ui/dropdown-menu.stories.tsx +270 -0
  33. package/components/ui/dropdown-menu.tsx +353 -0
  34. package/components/ui/empty-state.stories.tsx +121 -0
  35. package/components/ui/empty-state.tsx +289 -0
  36. package/components/ui/field-group.stories.tsx +201 -0
  37. package/components/ui/field-group.tsx +276 -0
  38. package/components/ui/form.stories.tsx +219 -0
  39. package/components/ui/form.tsx +542 -0
  40. package/components/ui/input.stories.tsx +154 -0
  41. package/components/ui/input.tsx +208 -0
  42. package/components/ui/label.stories.tsx +84 -0
  43. package/components/ui/label.tsx +98 -0
  44. package/components/ui/page-header.stories.tsx +136 -0
  45. package/components/ui/page-header.tsx +315 -0
  46. package/components/ui/pagination.stories.tsx +136 -0
  47. package/components/ui/pagination.tsx +427 -0
  48. package/components/ui/popover.stories.tsx +212 -0
  49. package/components/ui/popover.tsx +167 -0
  50. package/components/ui/radio-group.stories.tsx +96 -0
  51. package/components/ui/radio-group.tsx +250 -0
  52. package/components/ui/select.stories.tsx +203 -0
  53. package/components/ui/select.tsx +318 -0
  54. package/components/ui/sidebar.stories.tsx +186 -0
  55. package/components/ui/sidebar.tsx +623 -0
  56. package/components/ui/skeleton.stories.tsx +131 -0
  57. package/components/ui/skeleton.tsx +311 -0
  58. package/components/ui/switch.stories.tsx +74 -0
  59. package/components/ui/switch.tsx +186 -0
  60. package/components/ui/table.stories.tsx +107 -0
  61. package/components/ui/table.tsx +285 -0
  62. package/components/ui/tabs.stories.tsx +222 -0
  63. package/components/ui/tabs.tsx +287 -0
  64. package/components/ui/textarea.stories.tsx +96 -0
  65. package/components/ui/textarea.tsx +182 -0
  66. package/components/ui/toast.stories.tsx +169 -0
  67. package/components/ui/toast.tsx +250 -0
  68. package/components/ui/tooltip.stories.tsx +146 -0
  69. package/components/ui/tooltip.tsx +156 -0
  70. package/components/ui/top-bar.stories.tsx +182 -0
  71. package/components/ui/top-bar.tsx +155 -0
  72. package/dist/components/ui/accordion.d.ts +45 -0
  73. package/dist/components/ui/accordion.d.ts.map +1 -0
  74. package/dist/components/ui/accordion.js +99 -0
  75. package/dist/components/ui/accordion.js.map +1 -0
  76. package/dist/components/ui/app-shell.d.ts +70 -0
  77. package/dist/components/ui/app-shell.d.ts.map +1 -0
  78. package/dist/components/ui/app-shell.js +199 -0
  79. package/dist/components/ui/app-shell.js.map +1 -0
  80. package/dist/components/ui/avatar.d.ts +41 -0
  81. package/dist/components/ui/avatar.d.ts.map +1 -0
  82. package/dist/components/ui/avatar.js +104 -0
  83. package/dist/components/ui/avatar.js.map +1 -0
  84. package/dist/components/ui/badge.d.ts +27 -0
  85. package/dist/components/ui/badge.d.ts.map +1 -0
  86. package/dist/components/ui/badge.js +65 -0
  87. package/dist/components/ui/badge.js.map +1 -0
  88. package/dist/components/ui/breadcrumb.d.ts +35 -0
  89. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  90. package/dist/components/ui/breadcrumb.js +88 -0
  91. package/dist/components/ui/breadcrumb.js.map +1 -0
  92. package/dist/components/ui/button.d.ts +26 -0
  93. package/dist/components/ui/button.d.ts.map +1 -0
  94. package/dist/components/ui/button.js +73 -0
  95. package/dist/components/ui/button.js.map +1 -0
  96. package/dist/components/ui/card.d.ts +52 -0
  97. package/dist/components/ui/card.d.ts.map +1 -0
  98. package/dist/components/ui/card.js +96 -0
  99. package/dist/components/ui/card.js.map +1 -0
  100. package/dist/components/ui/checkbox.d.ts +18 -0
  101. package/dist/components/ui/checkbox.d.ts.map +1 -0
  102. package/dist/components/ui/checkbox.js +59 -0
  103. package/dist/components/ui/checkbox.js.map +1 -0
  104. package/dist/components/ui/combobox.d.ts +194 -0
  105. package/dist/components/ui/combobox.d.ts.map +1 -0
  106. package/dist/components/ui/combobox.js +361 -0
  107. package/dist/components/ui/combobox.js.map +1 -0
  108. package/dist/components/ui/context-menu.d.ts +46 -0
  109. package/dist/components/ui/context-menu.d.ts.map +1 -0
  110. package/dist/components/ui/context-menu.js +95 -0
  111. package/dist/components/ui/context-menu.js.map +1 -0
  112. package/dist/components/ui/data-table.d.ts +53 -0
  113. package/dist/components/ui/data-table.d.ts.map +1 -0
  114. package/dist/components/ui/data-table.js +163 -0
  115. package/dist/components/ui/data-table.js.map +1 -0
  116. package/dist/components/ui/date-picker.d.ts +103 -0
  117. package/dist/components/ui/date-picker.d.ts.map +1 -0
  118. package/dist/components/ui/date-picker.js +306 -0
  119. package/dist/components/ui/date-picker.js.map +1 -0
  120. package/dist/components/ui/dialog.d.ts +40 -0
  121. package/dist/components/ui/dialog.d.ts.map +1 -0
  122. package/dist/components/ui/dialog.js +110 -0
  123. package/dist/components/ui/dialog.js.map +1 -0
  124. package/dist/components/ui/divider.d.ts +30 -0
  125. package/dist/components/ui/divider.d.ts.map +1 -0
  126. package/dist/components/ui/divider.js +62 -0
  127. package/dist/components/ui/divider.js.map +1 -0
  128. package/dist/components/ui/drawer.d.ts +56 -0
  129. package/dist/components/ui/drawer.d.ts.map +1 -0
  130. package/dist/components/ui/drawer.js +147 -0
  131. package/dist/components/ui/drawer.js.map +1 -0
  132. package/dist/components/ui/dropdown-menu.d.ts +63 -0
  133. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  134. package/dist/components/ui/dropdown-menu.js +116 -0
  135. package/dist/components/ui/dropdown-menu.js.map +1 -0
  136. package/dist/components/ui/empty-state.d.ts +43 -0
  137. package/dist/components/ui/empty-state.d.ts.map +1 -0
  138. package/dist/components/ui/empty-state.js +128 -0
  139. package/dist/components/ui/empty-state.js.map +1 -0
  140. package/dist/components/ui/field-group.d.ts +38 -0
  141. package/dist/components/ui/field-group.d.ts.map +1 -0
  142. package/dist/components/ui/field-group.js +107 -0
  143. package/dist/components/ui/field-group.js.map +1 -0
  144. package/dist/components/ui/form.d.ts +67 -0
  145. package/dist/components/ui/form.d.ts.map +1 -0
  146. package/dist/components/ui/form.js +286 -0
  147. package/dist/components/ui/form.js.map +1 -0
  148. package/dist/components/ui/input.d.ts +36 -0
  149. package/dist/components/ui/input.d.ts.map +1 -0
  150. package/dist/components/ui/input.js +99 -0
  151. package/dist/components/ui/input.js.map +1 -0
  152. package/dist/components/ui/label.d.ts +37 -0
  153. package/dist/components/ui/label.d.ts.map +1 -0
  154. package/dist/components/ui/label.js +34 -0
  155. package/dist/components/ui/label.js.map +1 -0
  156. package/dist/components/ui/page-header.d.ts +65 -0
  157. package/dist/components/ui/page-header.d.ts.map +1 -0
  158. package/dist/components/ui/page-header.js +140 -0
  159. package/dist/components/ui/page-header.js.map +1 -0
  160. package/dist/components/ui/pagination.d.ts +67 -0
  161. package/dist/components/ui/pagination.d.ts.map +1 -0
  162. package/dist/components/ui/pagination.js +109 -0
  163. package/dist/components/ui/pagination.js.map +1 -0
  164. package/dist/components/ui/popover.d.ts +28 -0
  165. package/dist/components/ui/popover.d.ts.map +1 -0
  166. package/dist/components/ui/popover.js +85 -0
  167. package/dist/components/ui/popover.js.map +1 -0
  168. package/dist/components/ui/radio-group.d.ts +35 -0
  169. package/dist/components/ui/radio-group.d.ts.map +1 -0
  170. package/dist/components/ui/radio-group.js +103 -0
  171. package/dist/components/ui/radio-group.js.map +1 -0
  172. package/dist/components/ui/select.d.ts +42 -0
  173. package/dist/components/ui/select.d.ts.map +1 -0
  174. package/dist/components/ui/select.js +86 -0
  175. package/dist/components/ui/select.js.map +1 -0
  176. package/dist/components/ui/sidebar.d.ts +59 -0
  177. package/dist/components/ui/sidebar.d.ts.map +1 -0
  178. package/dist/components/ui/sidebar.js +189 -0
  179. package/dist/components/ui/sidebar.js.map +1 -0
  180. package/dist/components/ui/skeleton.d.ts +77 -0
  181. package/dist/components/ui/skeleton.d.ts.map +1 -0
  182. package/dist/components/ui/skeleton.js +115 -0
  183. package/dist/components/ui/skeleton.js.map +1 -0
  184. package/dist/components/ui/switch.d.ts +26 -0
  185. package/dist/components/ui/switch.d.ts.map +1 -0
  186. package/dist/components/ui/switch.js +84 -0
  187. package/dist/components/ui/switch.js.map +1 -0
  188. package/dist/components/ui/table.d.ts +52 -0
  189. package/dist/components/ui/table.d.ts.map +1 -0
  190. package/dist/components/ui/table.js +109 -0
  191. package/dist/components/ui/table.js.map +1 -0
  192. package/dist/components/ui/tabs.d.ts +42 -0
  193. package/dist/components/ui/tabs.d.ts.map +1 -0
  194. package/dist/components/ui/tabs.js +163 -0
  195. package/dist/components/ui/tabs.js.map +1 -0
  196. package/dist/components/ui/textarea.d.ts +26 -0
  197. package/dist/components/ui/textarea.d.ts.map +1 -0
  198. package/dist/components/ui/textarea.js +96 -0
  199. package/dist/components/ui/textarea.js.map +1 -0
  200. package/dist/components/ui/toast.d.ts +77 -0
  201. package/dist/components/ui/toast.d.ts.map +1 -0
  202. package/dist/components/ui/toast.js +141 -0
  203. package/dist/components/ui/toast.js.map +1 -0
  204. package/dist/components/ui/tooltip.d.ts +31 -0
  205. package/dist/components/ui/tooltip.d.ts.map +1 -0
  206. package/dist/components/ui/tooltip.js +71 -0
  207. package/dist/components/ui/tooltip.js.map +1 -0
  208. package/dist/components/ui/top-bar.d.ts +30 -0
  209. package/dist/components/ui/top-bar.d.ts.map +1 -0
  210. package/dist/components/ui/top-bar.js +64 -0
  211. package/dist/components/ui/top-bar.js.map +1 -0
  212. package/dist/lib/utils.d.ts +3 -0
  213. package/dist/lib/utils.d.ts.map +1 -0
  214. package/dist/lib/utils.js +6 -0
  215. package/dist/lib/utils.js.map +1 -0
  216. package/lib/utils.ts +6 -0
  217. package/package.json +112 -0
  218. 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=&quot;{side}&quot;
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=&quot;{align}&quot;
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=&quot;fit&quot;</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=&quot;trigger&quot; (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=&quot;{w}&quot;</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=&quot;{p}&quot;</p>
207
+ </PopoverContent>
208
+ </Popover>
209
+ ))}
210
+ </div>
211
+ ),
212
+ };