@ponti-studios/ui 0.1.2 → 0.4.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/LICENSE +21 -0
- package/README.md +7 -7
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/dist/index.js +988 -640
- package/dist/index.js.map +1 -1
- package/package.json +41 -41
- package/src/animations.css +42 -383
- package/src/components/data-display/accordion.tsx +22 -10
- package/src/components/data-display/filter-chip.stories.tsx +38 -0
- package/src/components/data-display/filter-chip.tsx +10 -4
- package/src/components/data-display/index.ts +5 -0
- package/src/components/data-display/metric-card.stories.tsx +48 -0
- package/src/components/data-display/metric-card.tsx +20 -0
- package/src/components/data-display/sort-controls.stories.tsx +45 -0
- package/src/components/data-display/sort-controls.tsx +99 -0
- package/src/components/data-display/sort-row.tsx +74 -0
- package/src/components/data-display/table.tsx +5 -9
- package/src/components/feedback/alert.tsx +4 -4
- package/src/components/feedback/empty-state.tsx +16 -6
- package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
- package/src/components/feedback/file-upload-status-badge.tsx +33 -0
- package/src/components/feedback/file-upload-status.stories.tsx +57 -0
- package/src/components/feedback/file-upload-status.tsx +152 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/feedback/progress-bar.stories.tsx +39 -0
- package/src/components/feedback/progress-bar.tsx +21 -0
- package/src/components/feedback/progress.stories.tsx +8 -6
- package/src/components/feedback/progress.tsx +3 -3
- package/src/components/feedback/skeleton.stories.tsx +36 -0
- package/src/components/feedback/skeleton.tsx +7 -0
- package/src/components/feedback/spinner.tsx +2 -2
- package/src/components/feedback/update-guard.stories.tsx +32 -0
- package/src/components/feedback/update-guard.tsx +280 -0
- package/src/components/forms/calendar.tsx +14 -11
- package/src/components/forms/date-month-select.stories.tsx +26 -0
- package/src/components/forms/date-month-select.tsx +70 -0
- package/src/components/forms/date-picker.tsx +3 -3
- package/src/components/forms/drop-zone.stories.tsx +56 -0
- package/src/components/forms/drop-zone.tsx +146 -0
- package/src/components/forms/entity-select.stories.tsx +65 -0
- package/src/components/forms/entity-select.tsx +79 -0
- package/src/components/forms/field.tsx +3 -3
- package/src/components/forms/group-by-select.stories.tsx +26 -0
- package/src/components/forms/group-by-select.tsx +47 -0
- package/src/components/forms/index.ts +10 -0
- package/src/components/forms/input.stories.tsx +4 -4
- package/src/components/forms/input.tsx +1 -1
- package/src/components/forms/passkey-management.stories.tsx +81 -0
- package/src/components/forms/passkey-management.tsx +138 -0
- package/src/components/forms/radio-group.tsx +14 -13
- package/src/components/forms/select.stories.tsx +4 -4
- package/src/components/forms/select.tsx +47 -48
- package/src/components/forms/slider.stories.tsx +5 -2
- package/src/components/forms/slider.tsx +39 -17
- package/src/components/forms/stepper.tsx +4 -4
- package/src/components/forms/switch.stories.tsx +1 -1
- package/src/components/forms/switch.tsx +2 -2
- package/src/components/forms/textarea.stories.tsx +2 -2
- package/src/components/forms/textarea.tsx +1 -1
- package/src/components/layout/index.ts +3 -0
- package/src/components/layout/scroll-area.stories.tsx +4 -4
- package/src/components/layout/scroll-area.tsx +23 -21
- package/src/components/layout/section-intro.stories.tsx +29 -0
- package/src/components/layout/section-intro.tsx +31 -0
- package/src/components/layout/surface-panel.stories.tsx +32 -0
- package/src/components/layout/surface-panel.tsx +15 -0
- package/src/components/navigation/app-navigation.stories.tsx +2 -2
- package/src/components/navigation/app-navigation.tsx +13 -13
- package/src/components/navigation/index.ts +4 -0
- package/src/components/navigation/pagination-controls.stories.tsx +41 -0
- package/src/components/navigation/pagination-controls.tsx +50 -0
- package/src/components/navigation/route-link.stories.tsx +30 -0
- package/src/components/navigation/route-link.tsx +42 -0
- package/src/components/navigation/tabs.stories.tsx +5 -5
- package/src/components/navigation/tabs.tsx +8 -8
- package/src/components/overlays/alert-dialog.tsx +22 -25
- package/src/components/overlays/command.tsx +98 -26
- package/src/components/overlays/dialog.tsx +5 -5
- package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
- package/src/components/overlays/dropdown-menu.tsx +49 -31
- package/src/components/overlays/popover.stories.tsx +2 -2
- package/src/components/overlays/popover.tsx +43 -14
- package/src/components/overlays/sheet.stories.tsx +2 -2
- package/src/components/overlays/sheet.tsx +51 -17
- package/src/components/primitives/avatar.tsx +9 -5
- package/src/components/primitives/badge.tsx +11 -12
- package/src/components/primitives/button.tsx +49 -23
- package/src/components/primitives/card.stories.tsx +2 -2
- package/src/components/primitives/card.tsx +2 -5
- package/src/components/primitives/index.ts +2 -0
- package/src/components/primitives/label.stories.tsx +1 -1
- package/src/components/primitives/label.tsx +5 -6
- package/src/components/primitives/status-badge.stories.tsx +52 -0
- package/src/components/primitives/status-badge.tsx +33 -0
- package/src/constants/chart-colors.ts +7 -7
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-sort.ts +20 -0
- package/src/index.ts +30 -11
- package/src/styles.css +100 -974
- package/src/tokens/chart-colors.stories.tsx +192 -0
- package/src/tokens/color-systems.stories.tsx +20 -112
- package/src/tokens/foundations.stories.tsx +58 -100
- package/src/tokens/generated/dark.colors.generated.ts +29 -0
- package/src/tokens/generated/dark.css +29 -0
- package/src/tokens/generated/foundations.css +148 -0
- package/src/tokens/generated/foundations.generated.ts +451 -0
- package/src/tokens/generated/light.colors.generated.ts +29 -0
- package/src/tokens/generated/light.css +29 -0
- package/src/tokens/index.ts +79 -16
- package/src/tokens/layout.stories.tsx +23 -91
- package/src/tokens/colors.ts +0 -156
- package/src/tokens/motion.ts +0 -51
- package/src/tokens/radii.ts +0 -10
- package/src/tokens/shadows.ts +0 -34
- package/src/tokens/spacing.ts +0 -30
- package/src/tokens/typography.ts +0 -42
|
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
|
7
7
|
<textarea
|
|
8
8
|
data-slot="textarea"
|
|
9
9
|
className={cn(
|
|
10
|
-
"placeholder:text-
|
|
10
|
+
"placeholder:text-muted-foreground aria-invalid:ring-destructive/20 aria-invalid:border-destructive focus-visible:ring-ring border-input flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
11
11
|
className,
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
@@ -25,7 +25,7 @@ export const Default: Story = {
|
|
|
25
25
|
className="flex size-40 shrink-0 items-center justify-center rounded-lg"
|
|
26
26
|
style={{ backgroundColor: item.bg }}
|
|
27
27
|
>
|
|
28
|
-
<span className="text-
|
|
28
|
+
<span className="text-foreground font-medium">{item.label}</span>
|
|
29
29
|
</div>
|
|
30
30
|
)),
|
|
31
31
|
},
|
|
@@ -41,7 +41,7 @@ export const WithSnap: Story = {
|
|
|
41
41
|
className="flex size-48 shrink-0 items-center justify-center rounded-lg"
|
|
42
42
|
style={{ backgroundColor: item.bg }}
|
|
43
43
|
>
|
|
44
|
-
<span className="text-
|
|
44
|
+
<span className="text-foreground font-medium">{item.label}</span>
|
|
45
45
|
</div>
|
|
46
46
|
)),
|
|
47
47
|
},
|
|
@@ -57,7 +57,7 @@ export const Vertical: Story = {
|
|
|
57
57
|
className="flex h-24 shrink-0 items-center justify-center rounded-lg"
|
|
58
58
|
style={{ backgroundColor: item.bg }}
|
|
59
59
|
>
|
|
60
|
-
<span className="text-
|
|
60
|
+
<span className="text-foreground font-medium">{item.label}</span>
|
|
61
61
|
</div>
|
|
62
62
|
)),
|
|
63
63
|
},
|
|
@@ -74,7 +74,7 @@ export const ScreenshotsGallery: Story = {
|
|
|
74
74
|
className="flex aspect-video w-72 shrink-0 items-center justify-center overflow-hidden rounded-xl border"
|
|
75
75
|
style={{ backgroundColor: item.bg }}
|
|
76
76
|
>
|
|
77
|
-
<span className="text-
|
|
77
|
+
<span className="text-foreground/70 text-sm">{item.label}</span>
|
|
78
78
|
</div>
|
|
79
79
|
))}
|
|
80
80
|
</ScrollArea>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
|
|
1
2
|
import * as React from "react";
|
|
3
|
+
|
|
2
4
|
import { cn } from "../../lib/utils";
|
|
3
5
|
|
|
4
|
-
interface ScrollAreaProps extends React.
|
|
6
|
+
interface ScrollAreaProps extends React.ComponentProps<typeof ScrollAreaPrimitive.Root> {
|
|
5
7
|
orientation?: "horizontal" | "vertical";
|
|
6
8
|
snap?: "start" | "center" | "none";
|
|
7
9
|
}
|
|
@@ -9,31 +11,31 @@ interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
9
11
|
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
10
12
|
({ className, children, orientation = "horizontal", snap = "none", ...props }, ref) => {
|
|
11
13
|
return (
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
>
|
|
24
|
-
{React.Children.map(children, (child) => {
|
|
25
|
-
if (!React.isValidElement(child)) return child;
|
|
26
|
-
if (snap !== "none") {
|
|
14
|
+
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative", className)} {...props}>
|
|
15
|
+
<ScrollAreaPrimitive.Viewport
|
|
16
|
+
className={cn(
|
|
17
|
+
"scrollbar-hide flex overflow-auto",
|
|
18
|
+
orientation === "horizontal" ? "flex-row" : "flex-col",
|
|
19
|
+
snap !== "none" && (orientation === "horizontal" ? "snap-x" : "snap-y"),
|
|
20
|
+
snap !== "none" && "snap-mandatory",
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
{React.Children.map(children, (child) => {
|
|
24
|
+
if (!React.isValidElement(child)) return child;
|
|
27
25
|
return React.cloneElement(child as React.ReactElement<{ className?: string }>, {
|
|
28
|
-
className: cn(
|
|
26
|
+
className: cn(
|
|
27
|
+
snap !== "none" && orientation === "horizontal" && `snap-${snap}`,
|
|
28
|
+
snap !== "none" && orientation === "vertical" && `snap-${snap}`,
|
|
29
|
+
(child.props as { className?: string }).className,
|
|
30
|
+
),
|
|
29
31
|
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
</div>
|
|
32
|
+
})}
|
|
33
|
+
</ScrollAreaPrimitive.Viewport>
|
|
34
|
+
</ScrollAreaPrimitive.Root>
|
|
34
35
|
);
|
|
35
36
|
},
|
|
36
37
|
);
|
|
38
|
+
|
|
37
39
|
ScrollArea.displayName = "ScrollArea";
|
|
38
40
|
|
|
39
41
|
export { ScrollArea };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../primitives/button";
|
|
4
|
+
import { SectionIntro } from "./section-intro";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Layout/SectionIntro",
|
|
8
|
+
component: SectionIntro,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof SectionIntro>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
title: "Accounts",
|
|
18
|
+
description: "Connect and manage the accounts you track.",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const WithEyebrowAndActions: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
eyebrow: "Settings",
|
|
25
|
+
title: "Security",
|
|
26
|
+
description: "Manage how you sign in and keep your account secure.",
|
|
27
|
+
actions: <Button variant="outline">Add passkey</Button>,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface SectionIntroProps {
|
|
4
|
+
eyebrow?: ReactNode;
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
actions?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Page-level heading composition: eyebrow label, title, description, and an
|
|
12
|
+
* optional trailing actions slot.
|
|
13
|
+
*/
|
|
14
|
+
export function SectionIntro({ eyebrow, title, description, actions }: SectionIntroProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex items-start justify-between gap-6">
|
|
17
|
+
<div className="min-w-0 flex-1">
|
|
18
|
+
{eyebrow ? (
|
|
19
|
+
<div className="text-muted-foreground mb-3 text-[11px] font-medium tracking-[0.18em] uppercase">
|
|
20
|
+
{eyebrow}
|
|
21
|
+
</div>
|
|
22
|
+
) : null}
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
<h1 className="text-title-2 text-foreground font-semibold tracking-tight">{title}</h1>
|
|
25
|
+
{description ? <p className="text-muted-foreground text-sm">{description}</p> : null}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { SurfacePanel } from "./surface-panel";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Layout/SurfacePanel",
|
|
7
|
+
component: SurfacePanel,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof SurfacePanel>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<div className="w-80">
|
|
17
|
+
<SurfacePanel>
|
|
18
|
+
<p className="text-muted-foreground text-sm">Panel content goes here.</p>
|
|
19
|
+
</SurfacePanel>
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const AsSection: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<div className="w-80">
|
|
27
|
+
<SurfacePanel as="section" aria-label="Example panel">
|
|
28
|
+
<p className="text-muted-foreground text-sm">Rendered as a semantic <section>.</p>
|
|
29
|
+
</SurfacePanel>
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType } from "react";
|
|
2
|
+
|
|
3
|
+
type SurfacePanelProps<T extends ElementType = "div"> = { as?: T } & Omit<
|
|
4
|
+
ComponentPropsWithoutRef<T>,
|
|
5
|
+
"as" | "className"
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
/** Rounded, bordered surface panel — a generic content container. */
|
|
9
|
+
export function SurfacePanel<T extends ElementType = "div">({
|
|
10
|
+
as,
|
|
11
|
+
...props
|
|
12
|
+
}: SurfacePanelProps<T>) {
|
|
13
|
+
const Component = as ?? "div";
|
|
14
|
+
return <Component className="border-border bg-surface rounded-3xl border p-5" {...props} />;
|
|
15
|
+
}
|
|
@@ -33,8 +33,8 @@ function PageContent({
|
|
|
33
33
|
message?: string;
|
|
34
34
|
}) {
|
|
35
35
|
return (
|
|
36
|
-
<div
|
|
37
|
-
<p
|
|
36
|
+
<div className="p-8">
|
|
37
|
+
<p className="text-muted-foreground text-sm">{message}</p>
|
|
38
38
|
</div>
|
|
39
39
|
);
|
|
40
40
|
}
|
|
@@ -72,10 +72,10 @@ export function AppNavigation({
|
|
|
72
72
|
...linkProps,
|
|
73
73
|
...(linkProp === "to" ? { to: href } : { href }),
|
|
74
74
|
className: brand
|
|
75
|
-
? "
|
|
75
|
+
? "inline-flex min-h-9 shrink-0 items-center rounded-md px-3 py-2 text-sm font-medium"
|
|
76
76
|
: variant
|
|
77
|
-
? "
|
|
78
|
-
: "
|
|
77
|
+
? "inline-flex min-h-9 shrink-0 items-center justify-center rounded-md px-3 py-2 text-sm font-medium"
|
|
78
|
+
: "inline-flex min-h-9 shrink-0 items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-muted hover:text-muted-foreground",
|
|
79
79
|
"aria-current": active ? "page" : undefined,
|
|
80
80
|
"data-variant": variant,
|
|
81
81
|
},
|
|
@@ -83,14 +83,14 @@ export function AppNavigation({
|
|
|
83
83
|
);
|
|
84
84
|
|
|
85
85
|
return (
|
|
86
|
-
<header className="
|
|
87
|
-
<nav className="
|
|
88
|
-
<div className="
|
|
89
|
-
<div className="
|
|
86
|
+
<header className="bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex w-full justify-center border-b px-4 backdrop-blur">
|
|
87
|
+
<nav className="w-full max-w-7xl" aria-label={ariaLabel}>
|
|
88
|
+
<div className="flex min-h-14 items-center justify-between gap-6">
|
|
89
|
+
<div className="flex min-w-0 items-center">
|
|
90
90
|
{brand && createLink({ href: brandHref, children: brand, brand: true })}
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
-
<div className="
|
|
93
|
+
<div className="ml-auto hidden min-w-0 items-center gap-1 sm:flex">
|
|
94
94
|
{links?.map((link) => (
|
|
95
95
|
<React.Fragment key={link.href}>
|
|
96
96
|
{createLink({ href: link.href, children: link.label, active: isActive(link.href) })}
|
|
@@ -105,7 +105,7 @@ export function AppNavigation({
|
|
|
105
105
|
{endContent}
|
|
106
106
|
</div>
|
|
107
107
|
|
|
108
|
-
<div className="
|
|
108
|
+
<div className="ml-auto flex items-center sm:hidden">
|
|
109
109
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
|
110
110
|
<SheetTrigger asChild>
|
|
111
111
|
<Button
|
|
@@ -116,10 +116,10 @@ export function AppNavigation({
|
|
|
116
116
|
<Menu className="size-4" aria-hidden="true" />
|
|
117
117
|
</Button>
|
|
118
118
|
</SheetTrigger>
|
|
119
|
-
<SheetContent
|
|
119
|
+
<SheetContent>
|
|
120
120
|
<SheetTitle className="sr-only">Navigation menu</SheetTitle>
|
|
121
121
|
|
|
122
|
-
<div className="
|
|
122
|
+
<div className="mt-8 flex flex-1 flex-col gap-1">
|
|
123
123
|
{links?.map((link) => (
|
|
124
124
|
<SheetClose asChild key={link.href}>
|
|
125
125
|
{createLink({
|
|
@@ -132,7 +132,7 @@ export function AppNavigation({
|
|
|
132
132
|
</div>
|
|
133
133
|
|
|
134
134
|
{(cta || endContent) && (
|
|
135
|
-
<div className="
|
|
135
|
+
<div className="flex flex-col gap-4 border-t pt-4">
|
|
136
136
|
{cta && (
|
|
137
137
|
<SheetClose asChild>
|
|
138
138
|
{createLink({
|
|
@@ -142,7 +142,7 @@ export function AppNavigation({
|
|
|
142
142
|
})}
|
|
143
143
|
</SheetClose>
|
|
144
144
|
)}
|
|
145
|
-
{endContent && <div className="
|
|
145
|
+
{endContent && <div className="flex items-center gap-3">{endContent}</div>}
|
|
146
146
|
</div>
|
|
147
147
|
)}
|
|
148
148
|
</SheetContent>
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { AppNavigation } from "./app-navigation";
|
|
2
2
|
export type { AppNavigationCta, AppNavigationLink, AppNavigationProps } from "./app-navigation";
|
|
3
|
+
export { PaginationControls } from "./pagination-controls";
|
|
4
|
+
export type { PaginationControlsProps } from "./pagination-controls";
|
|
5
|
+
export { RouteLink } from "./route-link";
|
|
6
|
+
export type { RouteLinkProps } from "./route-link";
|
|
3
7
|
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { PaginationControls } from "./pagination-controls";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Navigation/PaginationControls",
|
|
8
|
+
component: PaginationControls,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof PaginationControls>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
function PaginationPreview({ totalPages }: { totalPages: number }) {
|
|
16
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
17
|
+
return (
|
|
18
|
+
<PaginationControls
|
|
19
|
+
currentPage={currentPage}
|
|
20
|
+
totalPages={totalPages}
|
|
21
|
+
onPageChange={setCurrentPage}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const baseArgs = {
|
|
27
|
+
currentPage: 0,
|
|
28
|
+
totalPages: 5,
|
|
29
|
+
onPageChange: () => {},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
args: baseArgs,
|
|
34
|
+
render: () => <PaginationPreview totalPages={5} />,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const SinglePage: Story = {
|
|
38
|
+
name: "Single page (renders nothing)",
|
|
39
|
+
args: { ...baseArgs, totalPages: 1 },
|
|
40
|
+
render: () => <PaginationPreview totalPages={1} />,
|
|
41
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../primitives/button";
|
|
4
|
+
|
|
5
|
+
export interface PaginationControlsProps {
|
|
6
|
+
currentPage: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
onPageChange: (page: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Zero-indexed prev/next pagination control. Renders nothing for a single page. */
|
|
12
|
+
export function PaginationControls({
|
|
13
|
+
currentPage,
|
|
14
|
+
totalPages,
|
|
15
|
+
onPageChange,
|
|
16
|
+
}: PaginationControlsProps) {
|
|
17
|
+
if (totalPages <= 1) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="text-muted-foreground flex items-center justify-end gap-2 text-sm">
|
|
23
|
+
<Button
|
|
24
|
+
type="button"
|
|
25
|
+
variant="outline"
|
|
26
|
+
size="sm"
|
|
27
|
+
className="border-dashed"
|
|
28
|
+
onClick={() => onPageChange(currentPage - 1)}
|
|
29
|
+
disabled={currentPage === 0}
|
|
30
|
+
>
|
|
31
|
+
<ChevronLeft className="size-4" aria-hidden />
|
|
32
|
+
Previous
|
|
33
|
+
</Button>
|
|
34
|
+
<span className="text-foreground text-xs">
|
|
35
|
+
Page {currentPage + 1} of {totalPages}
|
|
36
|
+
</span>
|
|
37
|
+
<Button
|
|
38
|
+
type="button"
|
|
39
|
+
variant="outline"
|
|
40
|
+
size="sm"
|
|
41
|
+
className="border-dashed"
|
|
42
|
+
onClick={() => onPageChange(currentPage + 1)}
|
|
43
|
+
disabled={currentPage >= totalPages - 1}
|
|
44
|
+
>
|
|
45
|
+
Next
|
|
46
|
+
<ChevronRight className="size-4" aria-hidden />
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { RouteLink } from "./route-link";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Navigation/RouteLink",
|
|
7
|
+
component: RouteLink,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof RouteLink>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
children: "View accounts",
|
|
17
|
+
href: "#",
|
|
18
|
+
className: "text-accent underline",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const WithOnNavigate: Story = {
|
|
23
|
+
name: "With onNavigate callback",
|
|
24
|
+
args: {
|
|
25
|
+
children: "Go to analytics",
|
|
26
|
+
href: "#",
|
|
27
|
+
className: "text-accent underline",
|
|
28
|
+
onNavigate: () => console.log("route loading started"),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, MouseEvent, ReactNode } from "react";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
type RouteLinkOwnProps<T extends ElementType> = {
|
|
5
|
+
as?: T;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/** Called before the click is forwarded — e.g. to flip a route-loading store. */
|
|
8
|
+
onNavigate?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type RouteLinkProps<T extends ElementType = "a"> = RouteLinkOwnProps<T> &
|
|
12
|
+
Omit<ComponentPropsWithoutRef<T>, keyof RouteLinkOwnProps<T>>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Polymorphic link that fires `onNavigate` before `onClick`. Pass your
|
|
16
|
+
* router's `Link` component as `as` for client-side navigation (e.g.
|
|
17
|
+
* `as={Link} to="/accounts"`); the host app owns any route-loading state via
|
|
18
|
+
* `onNavigate` instead of a hardcoded store.
|
|
19
|
+
*/
|
|
20
|
+
export function RouteLink<T extends ElementType = "a">({
|
|
21
|
+
as,
|
|
22
|
+
children,
|
|
23
|
+
onNavigate,
|
|
24
|
+
onClick,
|
|
25
|
+
...props
|
|
26
|
+
}: RouteLinkProps<T>) {
|
|
27
|
+
const Component = (as ?? "a") as ElementType;
|
|
28
|
+
|
|
29
|
+
const handleClick = useCallback(
|
|
30
|
+
(e: MouseEvent) => {
|
|
31
|
+
onNavigate?.();
|
|
32
|
+
(onClick as ((e: MouseEvent) => void) | undefined)?.(e);
|
|
33
|
+
},
|
|
34
|
+
[onNavigate, onClick],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Component onClick={handleClick} {...props}>
|
|
39
|
+
{children}
|
|
40
|
+
</Component>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -19,12 +19,12 @@ export const Default: Story = {
|
|
|
19
19
|
<TabsTrigger value="password">Password</TabsTrigger>
|
|
20
20
|
</TabsList>
|
|
21
21
|
<TabsContent value="account">
|
|
22
|
-
<p className="text-
|
|
22
|
+
<p className="text-muted-foreground p-4 text-sm">
|
|
23
23
|
Make changes to your account here. Click save when you're done.
|
|
24
24
|
</p>
|
|
25
25
|
</TabsContent>
|
|
26
26
|
<TabsContent value="password">
|
|
27
|
-
<p className="text-
|
|
27
|
+
<p className="text-muted-foreground p-4 text-sm">
|
|
28
28
|
Change your password here. After saving, you'll be logged out.
|
|
29
29
|
</p>
|
|
30
30
|
</TabsContent>
|
|
@@ -41,13 +41,13 @@ export const LineVariant: Story = {
|
|
|
41
41
|
<TabsTrigger value="tab3">Reports</TabsTrigger>
|
|
42
42
|
</TabsList>
|
|
43
43
|
<TabsContent value="tab1">
|
|
44
|
-
<p className="text-
|
|
44
|
+
<p className="text-muted-foreground p-4 text-sm">Overview content</p>
|
|
45
45
|
</TabsContent>
|
|
46
46
|
<TabsContent value="tab2">
|
|
47
|
-
<p className="text-
|
|
47
|
+
<p className="text-muted-foreground p-4 text-sm">Analytics content</p>
|
|
48
48
|
</TabsContent>
|
|
49
49
|
<TabsContent value="tab3">
|
|
50
|
-
<p className="text-
|
|
50
|
+
<p className="text-muted-foreground p-4 text-sm">Reports content</p>
|
|
51
51
|
</TabsContent>
|
|
52
52
|
</Tabs>
|
|
53
53
|
),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
@@ -12,7 +12,7 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
|
|
|
12
12
|
<TabsPrimitive.List
|
|
13
13
|
data-slot="tabs-list"
|
|
14
14
|
className={cn(
|
|
15
|
-
"bg-
|
|
15
|
+
"bg-muted flex h-auto w-full items-center gap-1 rounded-full border p-1",
|
|
16
16
|
className,
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -20,12 +20,12 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.
|
|
23
|
+
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Tab>) {
|
|
24
24
|
return (
|
|
25
|
-
<TabsPrimitive.
|
|
25
|
+
<TabsPrimitive.Tab
|
|
26
26
|
data-slot="tabs-trigger"
|
|
27
27
|
className={cn(
|
|
28
|
-
"text-
|
|
28
|
+
"text-foreground focus-visible:ring-ring data-active:bg-background data-active:text-foreground data-active:border-border flex flex-1 items-center justify-center gap-2 rounded-full px-3 py-2 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-active:border",
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -33,12 +33,12 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.
|
|
36
|
+
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Panel>) {
|
|
37
37
|
return (
|
|
38
|
-
<TabsPrimitive.
|
|
38
|
+
<TabsPrimitive.Panel
|
|
39
39
|
data-slot="tabs-content"
|
|
40
40
|
className={cn(
|
|
41
|
-
"focus-visible:bg-
|
|
41
|
+
"focus-visible:bg-muted/10 mt-2 rounded-md focus-visible:border focus-visible:outline-none",
|
|
42
42
|
className,
|
|
43
43
|
)}
|
|
44
44
|
{...props}
|