ruler-components 0.1.1 → 0.1.2
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/index.d.mts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +534 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +498 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +21 -2
- package/AGENTS.md +0 -9
- package/CLAUDE.md +0 -1
- package/app/docs/[slug]/page.tsx +0 -147
- package/app/docs/layout.tsx +0 -26
- package/app/docs/page.tsx +0 -148
- package/app/favicon.ico +0 -0
- package/app/globals.css +0 -168
- package/app/icon.png +0 -0
- package/app/layout.tsx +0 -90
- package/app/opengraph-image.tsx +0 -10
- package/app/page.tsx +0 -34
- package/app/twitter-image.tsx +0 -10
- package/components/docs/accordion-doc-page.tsx +0 -352
- package/components/docs/alert-doc-page.tsx +0 -293
- package/components/docs/allinput-doc-page.tsx +0 -183
- package/components/docs/avatar-doc-page.tsx +0 -424
- package/components/docs/badge-doc-page.tsx +0 -363
- package/components/docs/button-doc-page.tsx +0 -406
- package/components/docs/card-doc-page.tsx +0 -364
- package/components/docs/checkbox-doc-page.tsx +0 -277
- package/components/docs/colors-doc-page.tsx +0 -430
- package/components/docs/dialog-doc-page.tsx +0 -364
- package/components/docs/docs-footer.tsx +0 -11
- package/components/docs/docs-mobile-menu-bar.tsx +0 -22
- package/components/docs/docs-mobile-nav-context.tsx +0 -27
- package/components/docs/docs-page.tsx +0 -263
- package/components/docs/docs-search.tsx +0 -303
- package/components/docs/docs-sidebar.tsx +0 -165
- package/components/docs/docs-topbar.tsx +0 -32
- package/components/docs/dropdown-doc-page.tsx +0 -522
- package/components/docs/getting-started-doc-page.tsx +0 -205
- package/components/docs/label-doc-page.tsx +0 -173
- package/components/docs/loaders-doc-page.tsx +0 -244
- package/components/docs/menu-doc-page.tsx +0 -373
- package/components/docs/modal-doc-page.tsx +0 -436
- package/components/docs/next-steps.tsx +0 -53
- package/components/docs/pagination-doc-page.tsx +0 -316
- package/components/docs/progress-doc-page.tsx +0 -387
- package/components/docs/radio-doc-page.tsx +0 -270
- package/components/docs/skeleton-doc-page.tsx +0 -230
- package/components/docs/stepper-doc-page.tsx +0 -309
- package/components/docs/switch-doc-page.tsx +0 -264
- package/components/docs/table-doc-page.tsx +0 -689
- package/components/docs/tabs-doc-page.tsx +0 -359
- package/components/docs/theming-doc-page.tsx +0 -243
- package/components/docs/tooltip-doc-page.tsx +0 -357
- package/components/docs/usage-section.tsx +0 -21
- package/components/icons/discord.tsx +0 -9
- package/components/icons/github.tsx +0 -9
- package/components/icons/x.tsx +0 -9
- package/components/installation-section.tsx +0 -34
- package/components/landing/feature-highlights.tsx +0 -105
- package/components/landing/footer-strip.tsx +0 -45
- package/components/landing/hero-card.tsx +0 -14
- package/components/landing/hero-spotlight.tsx +0 -126
- package/components/landing/hero.tsx +0 -64
- package/components/landing/navbar.tsx +0 -44
- package/components/landing/quick-start-section.tsx +0 -59
- package/components/landing/site-footer.tsx +0 -55
- package/components/theme-provider.tsx +0 -60
- package/components/ui/accordion.tsx +0 -174
- package/components/ui/alert.tsx +0 -103
- package/components/ui/allinput.tsx +0 -210
- package/components/ui/avatar.tsx +0 -179
- package/components/ui/badge.tsx +0 -52
- package/components/ui/button.tsx +0 -75
- package/components/ui/card.tsx +0 -105
- package/components/ui/checkbox.tsx +0 -130
- package/components/ui/code-block.tsx +0 -60
- package/components/ui/data-table.tsx +0 -235
- package/components/ui/dialog.tsx +0 -173
- package/components/ui/dropdown.tsx +0 -78
- package/components/ui/feature-card.tsx +0 -38
- package/components/ui/input.tsx +0 -44
- package/components/ui/label.tsx +0 -110
- package/components/ui/loaders.tsx +0 -139
- package/components/ui/menu.tsx +0 -118
- package/components/ui/modal.tsx +0 -399
- package/components/ui/package-manager-tabs.tsx +0 -40
- package/components/ui/pagination.tsx +0 -200
- package/components/ui/progress.tsx +0 -134
- package/components/ui/radio.tsx +0 -262
- package/components/ui/skeleton.tsx +0 -102
- package/components/ui/steppers.tsx +0 -309
- package/components/ui/switch.tsx +0 -153
- package/components/ui/table.tsx +0 -58
- package/components/ui/tabs.tsx +0 -166
- package/components/ui/theme-toggle.tsx +0 -60
- package/components/ui/tooltip.tsx +0 -238
- package/data/color-palette.ts +0 -35
- package/data/docs-nav.ts +0 -84
- package/eslint.config.mjs +0 -18
- package/index.ts +0 -8
- package/lib/cn.ts +0 -4
- package/lib/color.ts +0 -85
- package/lib/og-image.tsx +0 -138
- package/lib/theme.ts +0 -33
- package/next.config.ts +0 -7
- package/postcss.config.mjs +0 -7
- package/public/dark.png +0 -0
- package/public/dark1.png +0 -0
- package/public/dark2.png +0 -0
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/light.png +0 -0
- package/public/light1.png +0 -0
- package/public/next.svg +0 -1
- package/public/vercel.svg +0 -1
- package/public/video/hero-demo.mp4 +0 -0
- package/public/window.svg +0 -1
- package/tsconfig.json +0 -34
package/components/ui/modal.tsx
DELETED
|
@@ -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
|
-
}
|