ruler-components 0.1.1 → 0.1.3

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 (118) hide show
  1. package/dist/index.d.mts +458 -0
  2. package/dist/index.d.ts +458 -0
  3. package/dist/index.js +3618 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +3467 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +21 -2
  8. package/AGENTS.md +0 -9
  9. package/CLAUDE.md +0 -1
  10. package/app/docs/[slug]/page.tsx +0 -147
  11. package/app/docs/layout.tsx +0 -26
  12. package/app/docs/page.tsx +0 -148
  13. package/app/favicon.ico +0 -0
  14. package/app/globals.css +0 -168
  15. package/app/icon.png +0 -0
  16. package/app/layout.tsx +0 -90
  17. package/app/opengraph-image.tsx +0 -10
  18. package/app/page.tsx +0 -34
  19. package/app/twitter-image.tsx +0 -10
  20. package/components/docs/accordion-doc-page.tsx +0 -352
  21. package/components/docs/alert-doc-page.tsx +0 -293
  22. package/components/docs/allinput-doc-page.tsx +0 -183
  23. package/components/docs/avatar-doc-page.tsx +0 -424
  24. package/components/docs/badge-doc-page.tsx +0 -363
  25. package/components/docs/button-doc-page.tsx +0 -406
  26. package/components/docs/card-doc-page.tsx +0 -364
  27. package/components/docs/checkbox-doc-page.tsx +0 -277
  28. package/components/docs/colors-doc-page.tsx +0 -430
  29. package/components/docs/dialog-doc-page.tsx +0 -364
  30. package/components/docs/docs-footer.tsx +0 -11
  31. package/components/docs/docs-mobile-menu-bar.tsx +0 -22
  32. package/components/docs/docs-mobile-nav-context.tsx +0 -27
  33. package/components/docs/docs-page.tsx +0 -263
  34. package/components/docs/docs-search.tsx +0 -303
  35. package/components/docs/docs-sidebar.tsx +0 -165
  36. package/components/docs/docs-topbar.tsx +0 -32
  37. package/components/docs/dropdown-doc-page.tsx +0 -522
  38. package/components/docs/getting-started-doc-page.tsx +0 -205
  39. package/components/docs/label-doc-page.tsx +0 -173
  40. package/components/docs/loaders-doc-page.tsx +0 -244
  41. package/components/docs/menu-doc-page.tsx +0 -373
  42. package/components/docs/modal-doc-page.tsx +0 -436
  43. package/components/docs/next-steps.tsx +0 -53
  44. package/components/docs/pagination-doc-page.tsx +0 -316
  45. package/components/docs/progress-doc-page.tsx +0 -387
  46. package/components/docs/radio-doc-page.tsx +0 -270
  47. package/components/docs/skeleton-doc-page.tsx +0 -230
  48. package/components/docs/stepper-doc-page.tsx +0 -309
  49. package/components/docs/switch-doc-page.tsx +0 -264
  50. package/components/docs/table-doc-page.tsx +0 -689
  51. package/components/docs/tabs-doc-page.tsx +0 -359
  52. package/components/docs/theming-doc-page.tsx +0 -243
  53. package/components/docs/tooltip-doc-page.tsx +0 -357
  54. package/components/docs/usage-section.tsx +0 -21
  55. package/components/icons/discord.tsx +0 -9
  56. package/components/icons/github.tsx +0 -9
  57. package/components/icons/x.tsx +0 -9
  58. package/components/installation-section.tsx +0 -34
  59. package/components/landing/feature-highlights.tsx +0 -105
  60. package/components/landing/footer-strip.tsx +0 -45
  61. package/components/landing/hero-card.tsx +0 -14
  62. package/components/landing/hero-spotlight.tsx +0 -126
  63. package/components/landing/hero.tsx +0 -64
  64. package/components/landing/navbar.tsx +0 -44
  65. package/components/landing/quick-start-section.tsx +0 -59
  66. package/components/landing/site-footer.tsx +0 -55
  67. package/components/theme-provider.tsx +0 -60
  68. package/components/ui/accordion.tsx +0 -174
  69. package/components/ui/alert.tsx +0 -103
  70. package/components/ui/allinput.tsx +0 -210
  71. package/components/ui/avatar.tsx +0 -179
  72. package/components/ui/badge.tsx +0 -52
  73. package/components/ui/button.tsx +0 -75
  74. package/components/ui/card.tsx +0 -105
  75. package/components/ui/checkbox.tsx +0 -130
  76. package/components/ui/code-block.tsx +0 -60
  77. package/components/ui/data-table.tsx +0 -235
  78. package/components/ui/dialog.tsx +0 -173
  79. package/components/ui/dropdown.tsx +0 -78
  80. package/components/ui/feature-card.tsx +0 -38
  81. package/components/ui/input.tsx +0 -44
  82. package/components/ui/label.tsx +0 -110
  83. package/components/ui/loaders.tsx +0 -139
  84. package/components/ui/menu.tsx +0 -118
  85. package/components/ui/modal.tsx +0 -399
  86. package/components/ui/package-manager-tabs.tsx +0 -40
  87. package/components/ui/pagination.tsx +0 -200
  88. package/components/ui/progress.tsx +0 -134
  89. package/components/ui/radio.tsx +0 -262
  90. package/components/ui/skeleton.tsx +0 -102
  91. package/components/ui/steppers.tsx +0 -309
  92. package/components/ui/switch.tsx +0 -153
  93. package/components/ui/table.tsx +0 -58
  94. package/components/ui/tabs.tsx +0 -166
  95. package/components/ui/theme-toggle.tsx +0 -60
  96. package/components/ui/tooltip.tsx +0 -238
  97. package/data/color-palette.ts +0 -35
  98. package/data/docs-nav.ts +0 -84
  99. package/eslint.config.mjs +0 -18
  100. package/index.ts +0 -8
  101. package/lib/cn.ts +0 -4
  102. package/lib/color.ts +0 -85
  103. package/lib/og-image.tsx +0 -138
  104. package/lib/theme.ts +0 -33
  105. package/next.config.ts +0 -7
  106. package/postcss.config.mjs +0 -7
  107. package/public/dark.png +0 -0
  108. package/public/dark1.png +0 -0
  109. package/public/dark2.png +0 -0
  110. package/public/file.svg +0 -1
  111. package/public/globe.svg +0 -1
  112. package/public/light.png +0 -0
  113. package/public/light1.png +0 -0
  114. package/public/next.svg +0 -1
  115. package/public/vercel.svg +0 -1
  116. package/public/video/hero-demo.mp4 +0 -0
  117. package/public/window.svg +0 -1
  118. package/tsconfig.json +0 -34
@@ -1,399 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { createPortal } from "react-dom";
5
- import { cva, type VariantProps } from "class-variance-authority";
6
- import { X } from "lucide-react";
7
- import { cn } from "@/lib/cn";
8
-
9
- const modalContentVariants = cva(
10
- [
11
- "relative z-10 flex w-full flex-col overflow-hidden rounded-2xl",
12
- "border border-zinc-200/90 bg-white text-zinc-900",
13
- "shadow-[0_20px_60px_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.04)]",
14
- "dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100",
15
- "dark:shadow-[0_20px_60px_rgba(0,0,0,0.45),0_0_0_1px_rgba(255,255,255,0.06)]",
16
- "transition-[opacity,transform] duration-200 ease-out",
17
- ],
18
- {
19
- variants: {
20
- size: {
21
- sm: "max-w-[400px]",
22
- md: "max-w-[440px]",
23
- lg: "max-w-lg",
24
- xl: "max-w-xl",
25
- },
26
- visible: {
27
- true: "opacity-100 scale-100",
28
- false: "opacity-0 scale-[0.97]",
29
- },
30
- },
31
- defaultVariants: { size: "md", visible: false },
32
- },
33
- );
34
-
35
- const overlayVariants = cva(
36
- [
37
- "absolute inset-0 bg-zinc-900/45",
38
- "transition-opacity duration-200 ease-out",
39
- "dark:bg-black/65",
40
- ],
41
- {
42
- variants: {
43
- visible: {
44
- true: "opacity-100",
45
- false: "opacity-0",
46
- },
47
- },
48
- defaultVariants: { visible: false },
49
- },
50
- );
51
-
52
- interface ModalContextValue {
53
- open: boolean;
54
- onOpenChange: (open: boolean) => void;
55
- }
56
-
57
- const ModalContext = React.createContext<ModalContextValue | null>(null);
58
-
59
- interface ModalContentContextValue {
60
- titleId: string;
61
- descriptionId: string;
62
- close: () => void;
63
- }
64
-
65
- const ModalContentContext = React.createContext<ModalContentContextValue | null>(null);
66
-
67
- function useModalContext() {
68
- const context = React.useContext(ModalContext);
69
- if (!context) {
70
- throw new Error("Modal components must be used within Modal");
71
- }
72
- return context;
73
- }
74
-
75
- function useModalContentContext() {
76
- const context = React.useContext(ModalContentContext);
77
- if (!context) {
78
- throw new Error("Modal subcomponents must be used within ModalContent");
79
- }
80
- return context;
81
- }
82
-
83
- export interface ModalProps {
84
- open?: boolean;
85
- onOpenChange?: (open: boolean) => void;
86
- defaultOpen?: boolean;
87
- children: React.ReactNode;
88
- }
89
-
90
- export function Modal({
91
- open: controlledOpen,
92
- onOpenChange,
93
- defaultOpen = false,
94
- children,
95
- }: ModalProps) {
96
- const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
97
- const open = controlledOpen ?? uncontrolledOpen;
98
-
99
- const handleOpenChange = React.useCallback(
100
- (next: boolean) => {
101
- if (controlledOpen === undefined) {
102
- setUncontrolledOpen(next);
103
- }
104
- onOpenChange?.(next);
105
- },
106
- [controlledOpen, onOpenChange],
107
- );
108
-
109
- return (
110
- <ModalContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
111
- {children}
112
- </ModalContext.Provider>
113
- );
114
- }
115
-
116
- export interface ModalTriggerProps extends React.ComponentPropsWithoutRef<"button"> {
117
- asChild?: boolean;
118
- }
119
-
120
- export function ModalTrigger({ asChild, children, onClick, ...props }: ModalTriggerProps) {
121
- const { onOpenChange } = useModalContext();
122
-
123
- const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
124
- onClick?.(event);
125
- if (!event.defaultPrevented) {
126
- onOpenChange(true);
127
- }
128
- };
129
-
130
- if (asChild && React.isValidElement(children)) {
131
- const child = children as React.ReactElement<{ onClick?: React.MouseEventHandler<HTMLElement> }>;
132
- return React.cloneElement(child, {
133
- onClick: (event: React.MouseEvent<HTMLElement>) => {
134
- child.props.onClick?.(event);
135
- if (!event.defaultPrevented) {
136
- onOpenChange(true);
137
- }
138
- },
139
- });
140
- }
141
-
142
- return (
143
- <button type="button" onClick={handleOpen} {...props}>
144
- {children}
145
- </button>
146
- );
147
- }
148
-
149
- export interface ModalContentProps
150
- extends React.HTMLAttributes<HTMLDivElement>,
151
- VariantProps<typeof modalContentVariants> {
152
- showCloseButton?: boolean;
153
- onClose?: () => void;
154
- }
155
-
156
- export function ModalContent({
157
- className,
158
- size,
159
- showCloseButton = true,
160
- onClose,
161
- children,
162
- ...props
163
- }: ModalContentProps) {
164
- const { open, onOpenChange } = useModalContext();
165
- const [mounted, setMounted] = React.useState(false);
166
- const [visible, setVisible] = React.useState(false);
167
- const contentRef = React.useRef<HTMLDivElement>(null);
168
- const titleId = React.useId();
169
- const descriptionId = React.useId();
170
-
171
- React.useEffect(() => setMounted(true), []);
172
-
173
- React.useEffect(() => {
174
- if (!open) {
175
- setVisible(false);
176
- document.body.style.overflow = "";
177
- return;
178
- }
179
-
180
- document.body.style.overflow = "hidden";
181
- const frame = requestAnimationFrame(() => setVisible(true));
182
-
183
- return () => {
184
- cancelAnimationFrame(frame);
185
- document.body.style.overflow = "";
186
- };
187
- }, [open]);
188
-
189
- const close = React.useCallback(() => {
190
- onClose?.();
191
- onOpenChange(false);
192
- }, [onClose, onOpenChange]);
193
-
194
- React.useEffect(() => {
195
- if (!open) return;
196
- const handleKeyDown = (event: KeyboardEvent) => {
197
- if (event.key === "Escape") close();
198
- };
199
- window.addEventListener("keydown", handleKeyDown);
200
- return () => window.removeEventListener("keydown", handleKeyDown);
201
- }, [open, close]);
202
-
203
- React.useEffect(() => {
204
- if (!open || !contentRef.current) return;
205
- const focusable = contentRef.current.querySelector<HTMLElement>(
206
- "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])",
207
- );
208
- focusable?.focus();
209
- }, [open, visible]);
210
-
211
- if (!mounted || !open) return null;
212
-
213
- const node = (
214
- <div className="fixed inset-0 z-[100] isolate flex items-center justify-center p-4 sm:p-6">
215
- <div
216
- className={overlayVariants({ visible })}
217
- onClick={close}
218
- aria-hidden
219
- />
220
- <div
221
- ref={contentRef}
222
- role="dialog"
223
- aria-modal="true"
224
- aria-labelledby={titleId}
225
- aria-describedby={descriptionId}
226
- className={cn(modalContentVariants({ size, visible }), className)}
227
- onClick={(e) => e.stopPropagation()}
228
- {...props}
229
- >
230
- {showCloseButton && (
231
- <button
232
- type="button"
233
- onClick={close}
234
- className="absolute right-4 top-4 z-10 inline-flex h-8 w-8 items-center justify-center rounded-lg text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
235
- aria-label="Close"
236
- >
237
- <X className="h-4 w-4" strokeWidth={2} />
238
- </button>
239
- )}
240
- <ModalContentContext.Provider value={{ titleId, descriptionId, close }}>
241
- {children}
242
- </ModalContentContext.Provider>
243
- </div>
244
- </div>
245
- );
246
-
247
- return createPortal(node, document.body);
248
- }
249
-
250
- export function ModalHeader({
251
- className,
252
- ...props
253
- }: React.HTMLAttributes<HTMLDivElement>) {
254
- return (
255
- <div
256
- className={cn("space-y-1 px-6 pt-6 pr-12", className)}
257
- {...props}
258
- />
259
- );
260
- }
261
-
262
- export function ModalTitle({
263
- className,
264
- ...props
265
- }: React.HTMLAttributes<HTMLHeadingElement>) {
266
- const { titleId } = useModalContentContext();
267
-
268
- return (
269
- <h2
270
- id={titleId}
271
- className={cn(
272
- "text-[15px] font-semibold leading-snug tracking-tight text-zinc-900 dark:text-white",
273
- className,
274
- )}
275
- {...props}
276
- />
277
- );
278
- }
279
-
280
- export function ModalDescription({
281
- className,
282
- ...props
283
- }: React.HTMLAttributes<HTMLParagraphElement>) {
284
- const { descriptionId } = useModalContentContext();
285
-
286
- return (
287
- <p
288
- id={descriptionId}
289
- className={cn(
290
- "text-[13px] leading-relaxed text-zinc-500 dark:text-zinc-400",
291
- className,
292
- )}
293
- {...props}
294
- />
295
- );
296
- }
297
-
298
- export function ModalBody({
299
- className,
300
- ...props
301
- }: React.HTMLAttributes<HTMLDivElement>) {
302
- return (
303
- <div
304
- className={cn("space-y-4 px-6 pt-5 pb-6", className)}
305
- {...props}
306
- />
307
- );
308
- }
309
-
310
- export function ModalFooter({
311
- className,
312
- ...props
313
- }: React.HTMLAttributes<HTMLDivElement>) {
314
- return (
315
- <div
316
- className={cn(
317
- "flex items-center justify-end gap-2 border-t border-zinc-200/90 px-6 py-4 dark:border-zinc-800",
318
- className,
319
- )}
320
- {...props}
321
- />
322
- );
323
- }
324
-
325
- export function ModalOption({
326
- className,
327
- icon,
328
- title,
329
- description,
330
- ...props
331
- }: React.HTMLAttributes<HTMLDivElement> & {
332
- icon?: React.ReactNode;
333
- title: string;
334
- description?: string;
335
- }) {
336
- return (
337
- <div
338
- className={cn(
339
- "flex gap-3 rounded-xl border border-zinc-200 bg-zinc-50/70 p-3.5",
340
- "dark:border-zinc-800 dark:bg-zinc-900/60",
341
- className,
342
- )}
343
- {...props}
344
- >
345
- {icon && (
346
- <div
347
- className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white text-zinc-500 shadow-sm ring-1 ring-zinc-200/80 dark:bg-zinc-950 dark:text-zinc-400 dark:ring-zinc-700"
348
- >
349
- {icon}
350
- </div>
351
- )}
352
- <div className="min-w-0">
353
- <p className="text-sm font-medium text-zinc-900 dark:text-white">{title}</p>
354
- {description && (
355
- <p className="mt-0.5 text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
356
- {description}
357
- </p>
358
- )}
359
- </div>
360
- </div>
361
- );
362
- }
363
-
364
- export interface ModalCloseProps extends React.ComponentPropsWithoutRef<"button"> {
365
- asChild?: boolean;
366
- }
367
-
368
- export function ModalClose({ asChild, children, onClick, ...props }: ModalCloseProps) {
369
- const { close } = useModalContentContext();
370
-
371
- if (asChild && React.isValidElement(children)) {
372
- return React.cloneElement(
373
- children as React.ReactElement<{ onClick?: React.MouseEventHandler }>,
374
- {
375
- onClick: (event: React.MouseEvent) => {
376
- (children as React.ReactElement<{ onClick?: React.MouseEventHandler }>).props.onClick?.(
377
- event,
378
- );
379
- close();
380
- },
381
- },
382
- );
383
- }
384
-
385
- return (
386
- <button
387
- type="button"
388
- onClick={(e) => {
389
- onClick?.(e);
390
- close();
391
- }}
392
- {...props}
393
- >
394
- {children}
395
- </button>
396
- );
397
- }
398
-
399
- export { modalContentVariants, overlayVariants };
@@ -1,40 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { cn } from "@/lib/cn";
5
- import { CodeBlock } from "@/components/ui/code-block";
6
-
7
- const PACKAGE_MANAGERS = [
8
- { id: "npm", label: "npm", command: "npm install ruler.ui" },
9
- { id: "yarn", label: "yarn", command: "yarn add ruler.ui" },
10
- { id: "pnpm", label: "pnpm", command: "pnpm add ruler.ui" },
11
- { id: "bun", label: "bun", command: "bun add ruler.ui" },
12
- ] as const;
13
-
14
- export function PackageManagerTabs() {
15
- const [active, setActive] = useState<(typeof PACKAGE_MANAGERS)[number]["id"]>("npm");
16
- const current = PACKAGE_MANAGERS.find((pm) => pm.id === active) ?? PACKAGE_MANAGERS[0];
17
-
18
- return (
19
- <div>
20
- <div className="flex min-w-0 items-center gap-5 overflow-x-auto border-b border-zinc-200 dark:border-zinc-800">
21
- {PACKAGE_MANAGERS.map((pm) => (
22
- <button
23
- key={pm.id}
24
- type="button"
25
- onClick={() => setActive(pm.id)}
26
- className={cn(
27
- "relative -mb-px border-b-2 px-0.5 py-2.5 text-sm font-medium transition-colors",
28
- active === pm.id
29
- ? "border-zinc-900 text-zinc-900 dark:border-white dark:text-zinc-50"
30
- : "border-transparent text-zinc-500 hover:text-zinc-700 dark:text-zinc-500 dark:hover:text-zinc-300",
31
- )}
32
- >
33
- {pm.label}
34
- </button>
35
- ))}
36
- </div>
37
- <CodeBlock code={current.command} className="mt-4" />
38
- </div>
39
- );
40
- }
@@ -1,200 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
5
- import { cn } from "@/lib/cn";
6
- import { Button } from "./button";
7
-
8
- function getPageList(current: number, total: number, siblingCount: number): (number | "...")[] {
9
- const totalSlots = siblingCount * 2 + 5;
10
- if (total <= totalSlots) return Array.from({ length: total }, (_, i) => i + 1);
11
-
12
- const left = Math.max(current - siblingCount, 1);
13
- const right = Math.min(current + siblingCount, total);
14
-
15
- const pages: (number | "...")[] = [1];
16
- if (left > 2) pages.push("...");
17
- for (let p = Math.max(left, 2); p <= Math.min(right, total - 1); p++) pages.push(p);
18
- if (right < total - 1) pages.push("...");
19
- if (total > 1) pages.push(total);
20
- return pages;
21
- }
22
-
23
- export interface PaginationProps {
24
- page: number;
25
- totalPages: number;
26
- onPageChange: (page: number) => void;
27
- siblingCount?: number;
28
- variant?: "default" | "simple";
29
- size?: "small" | "half" | "full";
30
- className?: string;
31
- }
32
-
33
- const navTransition =
34
- "transition-[color,background-color,border-color,box-shadow,transform,opacity,width] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]";
35
- const popIn = "animate-[pagination-pop_220ms_cubic-bezier(0.16,1,0.3,1)]";
36
- const focusRing = "focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900";
37
-
38
- const WIDTH_CLASSES = {
39
- small: "w-max max-w-full",
40
- half: "w-full min-w-0 sm:w-1/2 sm:min-w-max sm:max-w-full",
41
- full: "w-full min-w-0",
42
- };
43
-
44
- const BUTTON_SIZE = 32;
45
- const GAP = 4;
46
-
47
- export function Pagination({
48
- page,
49
- totalPages,
50
- onPageChange,
51
- siblingCount = 1,
52
- variant = "default",
53
- size = "small",
54
- className,
55
- }: PaginationProps) {
56
- const clampedTotal = Math.max(1, totalPages);
57
- const current = Math.min(Math.max(page, 1), clampedTotal);
58
- const goTo = (p: number) => onPageChange(Math.min(Math.max(p, 1), clampedTotal));
59
-
60
- const pages = React.useMemo(
61
- () => getPageList(current, clampedTotal, siblingCount),
62
- [current, clampedTotal, siblingCount],
63
- );
64
-
65
- const maxSlots = Math.min(clampedTotal, siblingCount * 2 + 5);
66
- const trackMinWidth = maxSlots * BUTTON_SIZE + (maxSlots - 1) * GAP;
67
-
68
- // FLIP: glide numbers/ellipses that persist across a page change to their new
69
- // position instead of snapping — scoped entirely to this component's own DOM,
70
- // so it never touches other Pagination instances on the page.
71
- const trackRef = React.useRef<HTMLDivElement>(null);
72
- const prevRectsRef = React.useRef(new Map<string, DOMRect>());
73
-
74
- React.useLayoutEffect(() => {
75
- const track = trackRef.current;
76
- if (!track) return;
77
- const prevRects = prevRectsRef.current;
78
- const nextRects = new Map<string, DOMRect>();
79
-
80
- track.querySelectorAll<HTMLElement>("[data-flip-key]").forEach((el) => {
81
- const key = el.dataset.flipKey!;
82
- const rect = el.getBoundingClientRect();
83
- nextRects.set(key, rect);
84
-
85
- const prev = prevRects.get(key);
86
- if (!prev) return;
87
- const dx = prev.left - rect.left;
88
- if (Math.abs(dx) < 0.5) return;
89
-
90
- el.style.transition = "none";
91
- el.style.transform = `translateX(${dx}px)`;
92
- el.getBoundingClientRect();
93
- requestAnimationFrame(() => {
94
- el.style.transition = "transform 220ms cubic-bezier(0.16,1,0.3,1)";
95
- el.style.transform = "";
96
- });
97
- });
98
-
99
- prevRectsRef.current = nextRects;
100
- }, [pages]);
101
-
102
- if (variant === "simple") {
103
- return (
104
- <nav
105
- aria-label="Pagination"
106
- className={cn(
107
- "flex items-center justify-between gap-2 overflow-x-auto rounded-full border border-zinc-200/70 bg-white p-1.5 shadow-[0_2px_8px_rgba(0,0,0,0.06)] transition-[width] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] dark:border-zinc-800/80 dark:bg-zinc-900 dark:shadow-[0_2px_8px_rgba(0,0,0,0.25)]",
108
- WIDTH_CLASSES[size],
109
- className,
110
- )}
111
- >
112
- <Button variant="ghost" size="sm" className={cn("rounded-full", navTransition, focusRing)} disabled={current <= 1} onClick={() => goTo(current - 1)}>
113
- <ChevronLeft size={15} />
114
- Previous
115
- </Button>
116
- <p className="min-w-[6.5rem] text-center text-xs tabular-nums text-zinc-500 dark:text-zinc-400">
117
- Page {current} of {clampedTotal}
118
- </p>
119
- <Button variant="ghost" size="sm" className={cn("rounded-full", navTransition, focusRing)} disabled={current >= clampedTotal} onClick={() => goTo(current + 1)}>
120
- Next
121
- <ChevronRight size={15} />
122
- </Button>
123
- </nav>
124
- );
125
- }
126
-
127
- return (
128
- <nav
129
- aria-label="Pagination"
130
- className={cn(
131
- "flex items-center gap-1 overflow-x-auto rounded-full border border-zinc-200/70 bg-white p-1 shadow-[0_2px_8px_rgba(0,0,0,0.06)] transition-[width] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] dark:border-zinc-800/80 dark:bg-zinc-900 dark:shadow-[0_2px_8px_rgba(0,0,0,0.25)]",
132
- WIDTH_CLASSES[size],
133
- className,
134
- )}
135
- >
136
- <Button
137
- variant="ghost"
138
- size="sm"
139
- className={cn("h-8 w-8 shrink-0 rounded-full p-0", navTransition, focusRing)}
140
- disabled={current <= 1}
141
- onClick={() => goTo(current - 1)}
142
- aria-label="Previous page"
143
- >
144
- <ChevronLeft size={15} />
145
- </Button>
146
-
147
- <div
148
- ref={trackRef}
149
- className="flex min-w-0 flex-1 items-center justify-center gap-1"
150
- style={size === "small" ? { minWidth: trackMinWidth } : undefined}
151
- >
152
- {pages.map((p, i) =>
153
- p === "..." ? (
154
- <span
155
- key={`ellipsis-${i}`}
156
- data-flip-key={`e-${i}`}
157
- className={cn("flex h-8 w-8 shrink-0 items-center justify-center text-zinc-400", popIn)}
158
- aria-hidden="true"
159
- >
160
- <MoreHorizontal size={15} />
161
- </span>
162
- ) : (
163
- <button
164
- key={p}
165
- type="button"
166
- data-flip-key={`p-${p}`}
167
- onClick={() => goTo(p)}
168
- aria-current={current === p ? "page" : undefined}
169
- aria-label={`Page ${p}`}
170
- className={cn(
171
- "h-8 w-8 shrink-0 cursor-pointer rounded-full text-sm font-medium tabular-nums outline-none",
172
- "focus-visible:ring-2 focus-visible:ring-zinc-900/15 focus-visible:ring-offset-2 dark:focus-visible:ring-white/20",
173
- "active:scale-90 active:transition-transform active:duration-100",
174
- navTransition,
175
- popIn,
176
- focusRing,
177
- current === p
178
- ? "bg-zinc-900 text-white shadow-[0_2px_6px_rgba(0,0,0,0.3)] dark:bg-white dark:text-zinc-900 dark:shadow-[0_2px_6px_rgba(0,0,0,0.5)]"
179
- : "text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100",
180
- )}
181
- >
182
- {p}
183
- </button>
184
- ),
185
- )}
186
- </div>
187
-
188
- <Button
189
- variant="ghost"
190
- size="sm"
191
- className={cn("h-8 w-8 shrink-0 rounded-full p-0", navTransition, focusRing)}
192
- disabled={current >= clampedTotal}
193
- onClick={() => goTo(current + 1)}
194
- aria-label="Next page"
195
- >
196
- <ChevronRight size={15} />
197
- </Button>
198
- </nav>
199
- );
200
- }