@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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -7
  3. package/THIRD_PARTY_NOTICES.md +17 -0
  4. package/dist/index.js +988 -640
  5. package/dist/index.js.map +1 -1
  6. package/package.json +41 -41
  7. package/src/animations.css +42 -383
  8. package/src/components/data-display/accordion.tsx +22 -10
  9. package/src/components/data-display/filter-chip.stories.tsx +38 -0
  10. package/src/components/data-display/filter-chip.tsx +10 -4
  11. package/src/components/data-display/index.ts +5 -0
  12. package/src/components/data-display/metric-card.stories.tsx +48 -0
  13. package/src/components/data-display/metric-card.tsx +20 -0
  14. package/src/components/data-display/sort-controls.stories.tsx +45 -0
  15. package/src/components/data-display/sort-controls.tsx +99 -0
  16. package/src/components/data-display/sort-row.tsx +74 -0
  17. package/src/components/data-display/table.tsx +5 -9
  18. package/src/components/feedback/alert.tsx +4 -4
  19. package/src/components/feedback/empty-state.tsx +16 -6
  20. package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
  21. package/src/components/feedback/file-upload-status-badge.tsx +33 -0
  22. package/src/components/feedback/file-upload-status.stories.tsx +57 -0
  23. package/src/components/feedback/file-upload-status.tsx +152 -0
  24. package/src/components/feedback/index.ts +9 -0
  25. package/src/components/feedback/progress-bar.stories.tsx +39 -0
  26. package/src/components/feedback/progress-bar.tsx +21 -0
  27. package/src/components/feedback/progress.stories.tsx +8 -6
  28. package/src/components/feedback/progress.tsx +3 -3
  29. package/src/components/feedback/skeleton.stories.tsx +36 -0
  30. package/src/components/feedback/skeleton.tsx +7 -0
  31. package/src/components/feedback/spinner.tsx +2 -2
  32. package/src/components/feedback/update-guard.stories.tsx +32 -0
  33. package/src/components/feedback/update-guard.tsx +280 -0
  34. package/src/components/forms/calendar.tsx +14 -11
  35. package/src/components/forms/date-month-select.stories.tsx +26 -0
  36. package/src/components/forms/date-month-select.tsx +70 -0
  37. package/src/components/forms/date-picker.tsx +3 -3
  38. package/src/components/forms/drop-zone.stories.tsx +56 -0
  39. package/src/components/forms/drop-zone.tsx +146 -0
  40. package/src/components/forms/entity-select.stories.tsx +65 -0
  41. package/src/components/forms/entity-select.tsx +79 -0
  42. package/src/components/forms/field.tsx +3 -3
  43. package/src/components/forms/group-by-select.stories.tsx +26 -0
  44. package/src/components/forms/group-by-select.tsx +47 -0
  45. package/src/components/forms/index.ts +10 -0
  46. package/src/components/forms/input.stories.tsx +4 -4
  47. package/src/components/forms/input.tsx +1 -1
  48. package/src/components/forms/passkey-management.stories.tsx +81 -0
  49. package/src/components/forms/passkey-management.tsx +138 -0
  50. package/src/components/forms/radio-group.tsx +14 -13
  51. package/src/components/forms/select.stories.tsx +4 -4
  52. package/src/components/forms/select.tsx +47 -48
  53. package/src/components/forms/slider.stories.tsx +5 -2
  54. package/src/components/forms/slider.tsx +39 -17
  55. package/src/components/forms/stepper.tsx +4 -4
  56. package/src/components/forms/switch.stories.tsx +1 -1
  57. package/src/components/forms/switch.tsx +2 -2
  58. package/src/components/forms/textarea.stories.tsx +2 -2
  59. package/src/components/forms/textarea.tsx +1 -1
  60. package/src/components/layout/index.ts +3 -0
  61. package/src/components/layout/scroll-area.stories.tsx +4 -4
  62. package/src/components/layout/scroll-area.tsx +23 -21
  63. package/src/components/layout/section-intro.stories.tsx +29 -0
  64. package/src/components/layout/section-intro.tsx +31 -0
  65. package/src/components/layout/surface-panel.stories.tsx +32 -0
  66. package/src/components/layout/surface-panel.tsx +15 -0
  67. package/src/components/navigation/app-navigation.stories.tsx +2 -2
  68. package/src/components/navigation/app-navigation.tsx +13 -13
  69. package/src/components/navigation/index.ts +4 -0
  70. package/src/components/navigation/pagination-controls.stories.tsx +41 -0
  71. package/src/components/navigation/pagination-controls.tsx +50 -0
  72. package/src/components/navigation/route-link.stories.tsx +30 -0
  73. package/src/components/navigation/route-link.tsx +42 -0
  74. package/src/components/navigation/tabs.stories.tsx +5 -5
  75. package/src/components/navigation/tabs.tsx +8 -8
  76. package/src/components/overlays/alert-dialog.tsx +22 -25
  77. package/src/components/overlays/command.tsx +98 -26
  78. package/src/components/overlays/dialog.tsx +5 -5
  79. package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
  80. package/src/components/overlays/dropdown-menu.tsx +49 -31
  81. package/src/components/overlays/popover.stories.tsx +2 -2
  82. package/src/components/overlays/popover.tsx +43 -14
  83. package/src/components/overlays/sheet.stories.tsx +2 -2
  84. package/src/components/overlays/sheet.tsx +51 -17
  85. package/src/components/primitives/avatar.tsx +9 -5
  86. package/src/components/primitives/badge.tsx +11 -12
  87. package/src/components/primitives/button.tsx +49 -23
  88. package/src/components/primitives/card.stories.tsx +2 -2
  89. package/src/components/primitives/card.tsx +2 -5
  90. package/src/components/primitives/index.ts +2 -0
  91. package/src/components/primitives/label.stories.tsx +1 -1
  92. package/src/components/primitives/label.tsx +5 -6
  93. package/src/components/primitives/status-badge.stories.tsx +52 -0
  94. package/src/components/primitives/status-badge.tsx +33 -0
  95. package/src/constants/chart-colors.ts +7 -7
  96. package/src/hooks/index.ts +1 -0
  97. package/src/hooks/use-sort.ts +20 -0
  98. package/src/index.ts +30 -11
  99. package/src/styles.css +100 -974
  100. package/src/tokens/chart-colors.stories.tsx +192 -0
  101. package/src/tokens/color-systems.stories.tsx +20 -112
  102. package/src/tokens/foundations.stories.tsx +58 -100
  103. package/src/tokens/generated/dark.colors.generated.ts +29 -0
  104. package/src/tokens/generated/dark.css +29 -0
  105. package/src/tokens/generated/foundations.css +148 -0
  106. package/src/tokens/generated/foundations.generated.ts +451 -0
  107. package/src/tokens/generated/light.colors.generated.ts +29 -0
  108. package/src/tokens/generated/light.css +29 -0
  109. package/src/tokens/index.ts +79 -16
  110. package/src/tokens/layout.stories.tsx +23 -91
  111. package/src/tokens/colors.ts +0 -156
  112. package/src/tokens/motion.ts +0 -51
  113. package/src/tokens/radii.ts +0 -10
  114. package/src/tokens/shadows.ts +0 -34
  115. package/src/tokens/spacing.ts +0 -30
  116. 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-secondary aria-invalid:ring-destructive/20 aria-invalid:border-destructive focus-visible:ring-focus 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",
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}
@@ -1,2 +1,5 @@
1
1
  export { ScrollArea } from "./scroll-area";
2
2
  export type { ScrollAreaProps } from "./scroll-area";
3
+ export { SectionIntro } from "./section-intro";
4
+ export type { SectionIntroProps } from "./section-intro";
5
+ export { SurfacePanel } from "./surface-panel";
@@ -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-primary font-medium">{item.label}</span>
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-primary font-medium">{item.label}</span>
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-primary font-medium">{item.label}</span>
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-primary/70 text-sm">{item.label}</span>
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.HTMLAttributes<HTMLDivElement> {
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
- <div
13
- ref={ref}
14
- className={cn(
15
- "scrollbar-hide flex overflow-auto",
16
- orientation === "horizontal" && "flex-row",
17
- orientation === "vertical" && "flex-col",
18
- snap !== "none" && orientation === "horizontal" && "snap-x snap-mandatory",
19
- snap !== "none" && orientation === "vertical" && "snap-y snap-mandatory",
20
- className,
21
- )}
22
- {...props}
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(`snap-${snap}`, (child.props as { className?: string }).className),
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
- return child;
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 &lt;section&gt;.</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 style={{ padding: "2rem" }}>
37
- <p style={{ color: "var(--muted-foreground, #888)", fontSize: 14 }}>{message}</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
- ? "navigation-link navigation-brand shrink-0"
75
+ ? "inline-flex min-h-9 shrink-0 items-center rounded-md px-3 py-2 text-sm font-medium"
76
76
  : variant
77
- ? "navigation-cta"
78
- : "navigation-link",
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="navigation-shell">
87
- <nav className="navigation" aria-label={ariaLabel}>
88
- <div className="navigation-inner">
89
- <div className="navigation-start">
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="navigation-end">
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="navigation-mobile">
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 className="navigation-panel">
119
+ <SheetContent>
120
120
  <SheetTitle className="sr-only">Navigation menu</SheetTitle>
121
121
 
122
- <div className="navigation-panel-links">
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="navigation-panel-end">
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="navigation-panel-content">{endContent}</div>}
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-secondary p-4 text-sm">
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-secondary p-4 text-sm">
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-secondary p-4 text-sm">Overview content</p>
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-secondary p-4 text-sm">Analytics content</p>
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-secondary p-4 text-sm">Reports content</p>
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 * as TabsPrimitive from "@radix-ui/react-tabs";
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-panel flex h-auto w-full items-center gap-1 rounded-full border p-1",
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.Trigger>) {
23
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Tab>) {
24
24
  return (
25
- <TabsPrimitive.Trigger
25
+ <TabsPrimitive.Tab
26
26
  data-slot="tabs-trigger"
27
27
  className={cn(
28
- "text-primary focus-visible:ring-focus data-[state=active]:bg-panel data-[state=active]:text-primary data-[state=active]:border-subtle 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-[state=active]:border",
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.Content>) {
36
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Panel>) {
37
37
  return (
38
- <TabsPrimitive.Content
38
+ <TabsPrimitive.Panel
39
39
  data-slot="tabs-content"
40
40
  className={cn(
41
- "focus-visible:bg-accent/10 mt-2 rounded-md focus-visible:border focus-visible:outline-none",
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}