@ponti-studios/ui 0.8.0 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ponti-studios/ui",
3
- "version": "0.8.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "description": "Shared UI component library for Ponti Studios",
6
6
  "license": "MIT",
@@ -5,7 +5,6 @@ import {
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
7
7
  DropdownMenuGroup,
8
- DropdownMenuItem,
9
8
  DropdownMenuLabel,
10
9
  DropdownMenuSeparator,
11
10
  DropdownMenuTrigger,
@@ -57,10 +56,9 @@ export function SortControls({
57
56
  .filter((_, i) => i !== index)
58
57
  .map((option) => option.field);
59
58
  return (
60
- <DropdownMenuItem
59
+ <div
61
60
  key={sort.field}
62
- onSelect={(e) => e.preventDefault()}
63
- className={`p-0 focus:bg-transparent ${index === focusedSortIndex ? "bg-muted" : ""}`}
61
+ className={`rounded-sm ${index === focusedSortIndex ? "bg-muted" : ""}`}
64
62
  >
65
63
  <SortRow
66
64
  sortOption={sort}
@@ -70,27 +68,26 @@ export function SortControls({
70
68
  updateSortOption={updateSortOption}
71
69
  removeSortOption={removeSortOption}
72
70
  />
73
- </DropdownMenuItem>
71
+ </div>
74
72
  );
75
73
  })}
76
74
  </DropdownMenuGroup>
77
75
  {availableFieldsToAdd.length > 0 && (
78
76
  <>
79
77
  <DropdownMenuSeparator />
80
- <DropdownMenuItem
81
- onSelect={(e) => {
82
- e.preventDefault();
78
+ <button
79
+ type="button"
80
+ onClick={() => {
83
81
  const firstAvailable = availableFieldsToAdd[0];
84
82
  if (firstAvailable) {
85
83
  addSortOption({ field: firstAvailable, direction: "desc" });
86
84
  }
87
85
  }}
88
- disabled={availableFieldsToAdd.length === 0}
89
- className="mt-1"
86
+ className="hover:bg-muted focus-visible:ring-ring mt-1 flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:ring-2 focus-visible:ring-offset-2"
90
87
  >
91
88
  <PlusCircle className="mr-2 size-4" />
92
89
  Add Sort Criterion
93
- </DropdownMenuItem>
90
+ </button>
94
91
  </>
95
92
  )}
96
93
  </DropdownMenuContent>
@@ -1,7 +1,13 @@
1
+ import { ArrowDownWideNarrow, ArrowUpWideNarrow, X } from "lucide-react";
2
+
1
3
  import type { SortField, SortOption } from "../../hooks/sort.types";
2
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../forms/select";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "../overlays/dropdown-menu";
3
10
  import { Button } from "../primitives/button";
4
- import { X } from "lucide-react";
5
11
 
6
12
  export interface SortRowProps {
7
13
  sortOption: SortOption;
@@ -12,6 +18,10 @@ export interface SortRowProps {
12
18
  removeSortOption: (index: number) => void;
13
19
  }
14
20
 
21
+ function fieldLabel(field: SortField) {
22
+ return field.charAt(0).toUpperCase() + field.slice(1);
23
+ }
24
+
15
25
  export function SortRow({
16
26
  sortOption,
17
27
  index,
@@ -25,47 +35,51 @@ export function SortRow({
25
35
  );
26
36
 
27
37
  return (
28
- <div key={sortOption.field} className="flex items-center gap-2">
29
- <Select
30
- value={sortOption.field}
31
- onValueChange={(value) => updateSortOption(index, { ...sortOption, field: value })}
32
- >
33
- <SelectTrigger className="h-8 w-[130px] text-xs">
34
- <SelectValue placeholder="Select field" />
35
- </SelectTrigger>
36
- <SelectContent>
38
+ <div className="flex w-full items-center gap-1">
39
+ <DropdownMenu>
40
+ <DropdownMenuTrigger asChild>
41
+ <button
42
+ type="button"
43
+ className="hover:bg-muted focus-visible:ring-ring flex-1 rounded-md px-2 py-1.5 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
44
+ >
45
+ {fieldLabel(sortOption.field)}
46
+ </button>
47
+ </DropdownMenuTrigger>
48
+ <DropdownMenuContent>
37
49
  {availableFieldsForCurrentSelect.map((field) => (
38
- <SelectItem key={field} value={field} className="text-xs">
39
- {field.charAt(0).toUpperCase() + field.slice(1)}
40
- </SelectItem>
50
+ <DropdownMenuItem
51
+ key={field}
52
+ onClick={() => updateSortOption(index, { ...sortOption, field })}
53
+ >
54
+ {fieldLabel(field)}
55
+ </DropdownMenuItem>
41
56
  ))}
42
- </SelectContent>
43
- </Select>
44
- <Select
45
- value={sortOption.direction}
46
- onValueChange={(value) => {
47
- if (value === "asc" || value === "desc") {
48
- updateSortOption(index, { ...sortOption, direction: value });
49
- }
50
- }}
57
+ </DropdownMenuContent>
58
+ </DropdownMenu>
59
+ <Button
60
+ variant="ghost"
61
+ size="sm"
62
+ className="size-8 p-0"
63
+ aria-label={sortOption.direction === "asc" ? "Sorted ascending" : "Sorted descending"}
64
+ onClick={() =>
65
+ updateSortOption(index, {
66
+ ...sortOption,
67
+ direction: sortOption.direction === "asc" ? "desc" : "asc",
68
+ })
69
+ }
51
70
  >
52
- <SelectTrigger className="h-8 w-[110px] text-xs">
53
- <SelectValue placeholder="Direction" />
54
- </SelectTrigger>
55
- <SelectContent>
56
- <SelectItem value="asc" className="text-xs">
57
- Ascending
58
- </SelectItem>
59
- <SelectItem value="desc" className="text-xs">
60
- Descending
61
- </SelectItem>
62
- </SelectContent>
63
- </Select>
71
+ {sortOption.direction === "asc" ? (
72
+ <ArrowUpWideNarrow className="size-4" />
73
+ ) : (
74
+ <ArrowDownWideNarrow className="size-4" />
75
+ )}
76
+ </Button>
64
77
  <Button
65
78
  variant="ghost"
66
79
  size="sm"
67
80
  onClick={() => removeSortOption(index)}
68
81
  className="size-8 p-0"
82
+ aria-label="Remove sort criterion"
69
83
  >
70
84
  <X className="text-muted-foreground size-4" />
71
85
  </Button>
@@ -1,11 +1,12 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import * as React from "react";
3
3
 
4
- import { AppNavigation } from "./app-navigation";
4
+ import { Button } from "../primitives/button";
5
+ import { Navigation } from "./app-navigation";
5
6
 
6
- const meta: Meta<typeof AppNavigation> = {
7
+ const meta: Meta<typeof Navigation> = {
7
8
  title: "Navigation/AppNavigation",
8
- component: AppNavigation,
9
+ component: Navigation,
9
10
  tags: ["autodocs"],
10
11
  parameters: {
11
12
  layout: "fullscreen",
@@ -18,7 +19,7 @@ type Story = StoryObj<typeof meta>;
18
19
  // Shared helpers
19
20
  // ---------------------------------------------------------------------------
20
21
 
21
- const defaultLinks: Array<{ href: string; label: string }> = [
22
+ const defaultLinks = [
22
23
  { href: "/features", label: "Features" },
23
24
  { href: "/pricing", label: "Pricing" },
24
25
  { href: "/blog", label: "Blog" },
@@ -45,12 +46,25 @@ function PageContent({
45
46
  export const Default: Story = {
46
47
  render: () => (
47
48
  <>
48
- <AppNavigation
49
- brand="Acme"
50
- brandHref="/"
51
- links={defaultLinks}
52
- cta={{ href: "/get-started", label: "Get started", variant: "default" }}
53
- />
49
+ <Navigation>
50
+ <Navigation.Brand>
51
+ <a href="/">Acme</a>
52
+ </Navigation.Brand>
53
+
54
+ <Navigation.List>
55
+ {defaultLinks.map((link) => (
56
+ <Navigation.Item key={link.href} href={link.href}>
57
+ {link.label}
58
+ </Navigation.Item>
59
+ ))}
60
+ </Navigation.List>
61
+
62
+ <Navigation.Action>
63
+ <Button size="md" asChild>
64
+ <a href="/get-started">Get started</a>
65
+ </Button>
66
+ </Navigation.Action>
67
+ </Navigation>
54
68
  <PageContent />
55
69
  </>
56
70
  ),
@@ -59,22 +73,99 @@ export const Default: Story = {
59
73
  export const NoCta: Story = {
60
74
  render: () => (
61
75
  <>
62
- <AppNavigation brand="Acme" brandHref="/" links={defaultLinks} />
63
- <PageContent message="Same nav, no CTA button." />
76
+ <Navigation>
77
+ <Navigation.Brand>
78
+ <a href="/">Acme</a>
79
+ </Navigation.Brand>
80
+ <Navigation.List>
81
+ {defaultLinks.map((link) => (
82
+ <Navigation.Item key={link.href} href={link.href}>
83
+ {link.label}
84
+ </Navigation.Item>
85
+ ))}
86
+ </Navigation.List>
87
+ </Navigation>
88
+ <PageContent message="Same nav, no Action content." />
64
89
  </>
65
90
  ),
66
91
  };
67
92
 
68
- export const OutlineCta: Story = {
93
+ /**
94
+ * `Navigation.Item` (and any of the other pieces) support the same
95
+ * `asChild` composition as `Button`: pass your router's `Link` (or
96
+ * anything else) as the single child. This story stands in for a router
97
+ * `Link` with a plain anchor that no-ops on click, since Storybook has no
98
+ * router.
99
+ */
100
+ export const WithCustomRouterLink: Story = {
101
+ render: () => {
102
+ const activeHref = "/pricing";
103
+ return (
104
+ <>
105
+ <Navigation>
106
+ <Navigation.Brand>
107
+ <a href="/" data-router-link="stand-in" onClick={(e) => e.preventDefault()}>
108
+ Acme
109
+ </a>
110
+ </Navigation.Brand>
111
+
112
+ <Navigation.List>
113
+ {defaultLinks.map((link) => (
114
+ <Navigation.Item key={link.href} asChild active={link.href === activeHref}>
115
+ <a href={link.href} data-router-link="stand-in" onClick={(e) => e.preventDefault()}>
116
+ {link.label}
117
+ </a>
118
+ </Navigation.Item>
119
+ ))}
120
+ </Navigation.List>
121
+
122
+ <Navigation.Action>
123
+ <Button size="md" asChild>
124
+ <a href="/get-started" data-router-link="stand-in" onClick={(e) => e.preventDefault()}>
125
+ Get started
126
+ </a>
127
+ </Button>
128
+ </Navigation.Action>
129
+ </Navigation>
130
+ <PageContent message="Every link here is a custom element composed via asChild - open the elements panel to see data-router-link on each one, and the /pricing item's data-active/aria-current." />
131
+ </>
132
+ );
133
+ },
134
+ };
135
+
136
+ /**
137
+ * Composition means arbitrary content can sit alongside the link list and
138
+ * action area without any prop-shape changes to `Navigation` itself.
139
+ */
140
+ export const WithExtraContent: Story = {
69
141
  render: () => (
70
142
  <>
71
- <AppNavigation
72
- brand="Acme"
73
- brandHref="/"
74
- links={defaultLinks}
75
- cta={{ href: "/get-started", label: "Get started", variant: "outline" }}
76
- />
77
- <PageContent message="The outline CTA keeps the same navigation geometry with a quieter emphasis." />
143
+ <Navigation>
144
+ <Navigation.Brand>
145
+ <a href="/">Acme</a>
146
+ </Navigation.Brand>
147
+
148
+ <Navigation.List>
149
+ {defaultLinks.slice(0, 2).map((link) => (
150
+ <Navigation.Item key={link.href} href={link.href}>
151
+ {link.label}
152
+ </Navigation.Item>
153
+ ))}
154
+ </Navigation.List>
155
+
156
+ <input
157
+ type="search"
158
+ placeholder="Search..."
159
+ className="h-9 w-40 rounded-md border bg-background px-3 text-sm"
160
+ />
161
+
162
+ <Navigation.Action>
163
+ <Button size="md" variant="outline" asChild>
164
+ <a href="/get-started">Get started</a>
165
+ </Button>
166
+ </Navigation.Action>
167
+ </Navigation>
168
+ <PageContent message="A search input sits directly between the link list and the action area — no endContent prop needed." />
78
169
  </>
79
170
  ),
80
171
  };
@@ -1,160 +1,182 @@
1
1
  import { Menu } from "lucide-react";
2
- import type { ComponentPropsWithoutRef } from "react";
2
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
3
  import * as React from "react";
4
4
 
5
5
  import { cn } from "../../lib/utils";
6
+ import { Slot } from "../../lib/slot";
6
7
  import { Sheet, SheetClose, SheetContent, SheetTitle, SheetTrigger } from "../overlays/sheet";
7
8
  import { Button } from "../primitives/button";
8
9
 
9
- export interface AppNavigationLink {
10
- href: string;
11
- label: string;
10
+ /** True while rendering inside the mobile Sheet copy of the nav content. */
11
+ const MobileMenuContext = React.createContext(false);
12
+
13
+ export interface NavigationRootProps {
14
+ children: ReactNode;
15
+ ariaLabel?: string;
16
+ className?: string;
12
17
  }
13
18
 
14
- export interface AppNavigationCta {
15
- href: string;
16
- label: string;
17
- variant?: "default" | "outline";
19
+ /**
20
+ * Sticky nav shell. Compose it with `Navigation.Brand`, `Navigation.List`
21
+ * (containing `Navigation.Item`s), and any other content — a CTA `Button`,
22
+ * a search input, a notification bell — as plain children. `Navigation`
23
+ * only owns the responsive chrome: `Navigation.Brand` stays visible at
24
+ * every width, everything else collapses into a Sheet-based mobile menu
25
+ * below `sm`, reusing the exact elements you composed (not a parallel data
26
+ * shape) so `Navigation.Item`'s active state and `asChild` composition
27
+ * behave identically in both places.
28
+ */
29
+ function NavigationRoot({ children, ariaLabel = "Primary navigation", className }: NavigationRootProps) {
30
+ const [mobileOpen, setMobileOpen] = React.useState(false);
31
+
32
+ const items = React.Children.toArray(children);
33
+ const brand = items.find((child) => React.isValidElement(child) && child.type === NavigationBrand);
34
+ const rest = items.filter((child) => child !== brand);
35
+
36
+ return (
37
+ <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">
38
+ <nav
39
+ className={cn("flex min-h-14 w-full max-w-7xl items-center justify-between gap-6", className)}
40
+ aria-label={ariaLabel}
41
+ >
42
+ <div className="flex min-w-0 items-center">{brand}</div>
43
+
44
+ <div className="ml-auto hidden min-w-0 items-center gap-6 sm:flex">{rest}</div>
45
+
46
+ <div className="ml-auto flex items-center sm:hidden">
47
+ <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
48
+ <SheetTrigger asChild>
49
+ <Button
50
+ variant="outline"
51
+ size="icon"
52
+ aria-label={mobileOpen ? "Close menu" : "Open menu"}
53
+ >
54
+ <Menu className="size-4" aria-hidden="true" />
55
+ </Button>
56
+ </SheetTrigger>
57
+ <SheetContent>
58
+ <SheetTitle className="sr-only">Navigation menu</SheetTitle>
59
+ <div className="mt-8 flex flex-1 flex-col gap-4">
60
+ <MobileMenuContext.Provider value={true}>{rest}</MobileMenuContext.Provider>
61
+ </div>
62
+ </SheetContent>
63
+ </Sheet>
64
+ </div>
65
+ </nav>
66
+ </header>
67
+ );
18
68
  }
19
69
 
20
- export interface AppNavigationProps {
21
- brand?: React.ReactNode;
22
- brandHref?: string;
23
- endContent?: React.ReactNode;
24
- links?: AppNavigationLink[];
25
- cta?: AppNavigationCta;
26
- ariaLabel?: string;
27
- /** Current pathname used to highlight the active link. */
28
- activeHref?: string;
70
+ export interface NavigationBrandProps {
71
+ children: ReactNode;
72
+ className?: string;
29
73
  }
30
74
 
31
- interface NavigationLinkOwnProps {
32
- active?: boolean;
33
- brand?: boolean;
75
+ /** Always visible, at every viewport width — compose your own link/logo markup directly. */
76
+ function NavigationBrand({ children, className }: NavigationBrandProps) {
77
+ return <div className={cn("flex items-center font-semibold", className)}>{children}</div>;
34
78
  }
35
79
 
36
- export type NavigationLinkProps = NavigationLinkOwnProps &
37
- Omit<ComponentPropsWithoutRef<"a">, keyof NavigationLinkOwnProps>;
80
+ export interface NavigationListProps {
81
+ children: ReactNode;
82
+ className?: string;
83
+ }
38
84
 
39
- function NavigationLink({
40
- children,
41
- active = false,
42
- brand = false,
43
- className,
44
- ...props
45
- }: NavigationLinkProps) {
85
+ /** Semantic `<ul>` of `Navigation.Item`s. Lays out horizontally on desktop, stacks inside the mobile Sheet. */
86
+ function NavigationList({ children, className }: NavigationListProps) {
87
+ const isMobile = React.useContext(MobileMenuContext);
46
88
  return (
47
- <a
48
- {...props}
89
+ <ul
49
90
  className={cn(
50
- "inline-flex h-9 max-h-9 shrink-0 items-center px-3 py-2 text-sm font-medium",
51
- !brand && "hover:bg-muted hover:text-muted-foreground",
91
+ "flex items-center gap-1",
92
+ isMobile && "flex-col items-stretch gap-1",
52
93
  className,
53
94
  )}
54
- aria-current={active ? "page" : undefined}
55
95
  >
56
96
  {children}
57
- </a>
97
+ </ul>
58
98
  );
59
99
  }
60
100
 
61
- function NavigationCta({ href, label }: AppNavigationCta) {
62
- return (
63
- <Button asChild size="md">
64
- <a href={href}>{label}</a>
65
- </Button>
66
- );
101
+ interface NavigationItemOwnProps {
102
+ active?: boolean;
103
+ /** Apply the item styling to a single child (e.g. your router's `Link`) instead of rendering an `<a>`. */
104
+ asChild?: boolean;
67
105
  }
68
106
 
69
- function NavigationRoot({
70
- brand,
71
- brandHref = "/",
72
- endContent,
73
- links,
74
- cta,
75
- ariaLabel = "Primary navigation",
76
- activeHref,
77
- }: AppNavigationProps) {
78
- const [mobileOpen, setMobileOpen] = React.useState(false);
107
+ export type NavigationItemProps = NavigationItemOwnProps &
108
+ Omit<ComponentPropsWithoutRef<"a">, keyof NavigationItemOwnProps>;
109
+
110
+ /** A single nav link, wrapped in `<li>`. Auto-closes the mobile Sheet on click when rendered there. */
111
+ function NavigationItem({ active = false, asChild = false, className, children, ...props }: NavigationItemProps) {
112
+ const isMobile = React.useContext(MobileMenuContext);
113
+ const itemClassName = cn(
114
+ "inline-flex h-9 max-h-9 shrink-0 items-center px-3 py-2 text-sm font-medium hover:bg-muted hover:text-muted-foreground data-[active]:font-semibold data-[active]:text-foreground",
115
+ className,
116
+ );
79
117
 
80
- const isActive = (href: string) => {
81
- if (!activeHref) return false;
82
- if (href === activeHref) return true;
83
- // Nested routes (e.g. /work/123) keep the parent link active.
84
- return href !== "/" && activeHref.startsWith(`${href}/`);
85
- };
118
+ const content = asChild ? (
119
+ <Slot
120
+ {...props}
121
+ data-active={active || undefined}
122
+ aria-current={active ? "page" : undefined}
123
+ className={itemClassName}
124
+ >
125
+ {children}
126
+ </Slot>
127
+ ) : (
128
+ <a
129
+ {...props}
130
+ data-active={active || undefined}
131
+ aria-current={active ? "page" : undefined}
132
+ className={itemClassName}
133
+ >
134
+ {children}
135
+ </a>
136
+ );
137
+
138
+ return <li>{isMobile ? <SheetClose asChild>{content}</SheetClose> : content}</li>;
139
+ }
140
+
141
+ export interface NavigationActionProps {
142
+ children: ReactNode;
143
+ className?: string;
144
+ }
86
145
 
146
+ /**
147
+ * Layout wrapper for a CTA, avatar menu, theme toggle, etc. — bring your
148
+ * own `Button`, no dedicated CTA component needed. Content here does not
149
+ * auto-close the mobile Sheet on click (it may hold more than one
150
+ * element, or non-navigational controls) — use `Navigation.Item` instead
151
+ * for a link that should close the menu when tapped.
152
+ */
153
+ function NavigationAction({ children, className }: NavigationActionProps) {
154
+ const isMobile = React.useContext(MobileMenuContext);
87
155
  return (
88
- <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">
89
- <nav className="w-full max-w-7xl" aria-label={ariaLabel}>
90
- <div className="flex min-h-14 items-center justify-between gap-6">
91
- <div className="flex min-w-0 items-center">
92
- {brand && (
93
- <NavigationLink href={brandHref} brand>
94
- {brand}
95
- </NavigationLink>
96
- )}
97
- </div>
98
-
99
- <div className="ml-auto hidden min-w-0 items-center gap-1 sm:flex">
100
- {links?.map((link) => (
101
- <NavigationLink key={link.href} href={link.href} active={isActive(link.href)}>
102
- {link.label}
103
- </NavigationLink>
104
- ))}
105
- {cta && <NavigationCta {...cta} />}
106
- {endContent}
107
- </div>
108
-
109
- <div className="ml-auto flex items-center sm:hidden">
110
- <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
111
- <SheetTrigger asChild>
112
- <Button
113
- variant="outline"
114
- size="icon"
115
- aria-label={mobileOpen ? "Close menu" : "Open menu"}
116
- >
117
- <Menu className="size-4" aria-hidden="true" />
118
- </Button>
119
- </SheetTrigger>
120
- <SheetContent>
121
- <SheetTitle className="sr-only">Navigation menu</SheetTitle>
122
-
123
- <div className="mt-8 flex flex-1 flex-col gap-1">
124
- {links?.map((link) => (
125
- <SheetClose asChild key={link.href}>
126
- <NavigationLink href={link.href} active={isActive(link.href)}>
127
- {link.label}
128
- </NavigationLink>
129
- </SheetClose>
130
- ))}
131
- </div>
132
-
133
- {(cta || endContent) && (
134
- <div className="flex flex-col gap-4 border-t pt-4">
135
- {cta && (
136
- <SheetClose asChild>
137
- <NavigationCta {...cta} />
138
- </SheetClose>
139
- )}
140
- {endContent && <div className="flex items-center gap-3">{endContent}</div>}
141
- </div>
142
- )}
143
- </SheetContent>
144
- </Sheet>
145
- </div>
146
- </div>
147
- </nav>
148
- </header>
156
+ <div
157
+ className={cn(
158
+ "flex items-center gap-3",
159
+ isMobile && "mt-2 flex-col items-stretch gap-3 border-t pt-4",
160
+ className,
161
+ )}
162
+ >
163
+ {children}
164
+ </div>
149
165
  );
150
166
  }
151
167
 
152
168
  type NavigationComponent = typeof NavigationRoot & {
153
- Link: typeof NavigationLink;
169
+ Brand: typeof NavigationBrand;
170
+ List: typeof NavigationList;
171
+ Item: typeof NavigationItem;
172
+ Action: typeof NavigationAction;
154
173
  };
155
174
 
156
175
  export const Navigation: NavigationComponent = Object.assign(NavigationRoot, {
157
- Link: NavigationLink,
176
+ Brand: NavigationBrand,
177
+ List: NavigationList,
178
+ Item: NavigationItem,
179
+ Action: NavigationAction,
158
180
  });
159
181
 
160
182
  /** @deprecated Use `Navigation` instead. */
@@ -1,9 +1,10 @@
1
1
  export { AppNavigation, Navigation } from "./app-navigation";
2
2
  export type {
3
- AppNavigationCta,
4
- AppNavigationLink,
5
- AppNavigationProps,
6
- NavigationLinkProps,
3
+ NavigationActionProps,
4
+ NavigationBrandProps,
5
+ NavigationItemProps,
6
+ NavigationListProps,
7
+ NavigationRootProps,
7
8
  } from "./app-navigation";
8
9
  export { PaginationControls } from "./pagination-controls";
9
10
  export type { PaginationControlsProps } from "./pagination-controls";
@@ -15,7 +15,7 @@ export function PaginationControls({
15
15
  onPageChange,
16
16
  }: PaginationControlsProps) {
17
17
  return (
18
- <div className="border-border bg-background text-foreground inline-flex items-center overflow-hidden rounded-full border text-sm">
18
+ <div className="border-border bg-background text-foreground inline-flex w-fit shrink-0 items-center self-start overflow-hidden rounded-full border text-sm">
19
19
  <Button
20
20
  type="button"
21
21
  variant="outline"
@@ -0,0 +1,29 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import type { ReactElement } from "react";
3
+ import * as React from "react";
4
+
5
+ export interface SlotProps extends React.HTMLAttributes<HTMLElement> {
6
+ children?: React.ReactNode;
7
+ }
8
+
9
+ /**
10
+ * Generic `asChild` primitive: merges the props it receives onto its single
11
+ * child instead of rendering its own DOM element. Lets compound components
12
+ * (e.g. `Navigation.Item`) apply base styling/aria wiring to whatever the
13
+ * app renders — a router `Link`, a plain `<a>`, anything — without the app
14
+ * passing a component as a prop; it's composed as JSX children instead.
15
+ *
16
+ * Uses `mergeProps` (already a dependency via `@base-ui/react`) so
17
+ * event handlers chain instead of one overwriting the other, and
18
+ * `className`/`style` merge instead of replacing.
19
+ */
20
+ export const Slot = React.forwardRef<HTMLElement, SlotProps>(({ children, ...props }, ref) => {
21
+ if (!React.isValidElement(children)) return null;
22
+ const child = children as ReactElement<Record<string, unknown>>;
23
+ const merged = mergeProps<"div">(
24
+ props as React.ComponentPropsWithRef<"div">,
25
+ child.props as React.ComponentPropsWithRef<"div">,
26
+ );
27
+ return React.cloneElement(child, { ...merged, ref } as Record<string, unknown>);
28
+ });
29
+ Slot.displayName = "Slot";
@@ -43,14 +43,16 @@
43
43
  animation-name: zoom-out;
44
44
  }
45
45
 
46
- /* shadcn New York semantic contract. Values are generated from the DTCG
47
- * source; this file only names the roles consumed by web components. */
46
+ /*
47
+ Values are generated from the DTCG source;
48
+ this file only names the roles consumed by web components.
49
+ */
48
50
  @theme inline {
49
51
  --color-background: var(--background);
50
52
  --color-foreground: var(--text-primary);
51
53
  --color-card: var(--card);
52
54
  --color-card-foreground: var(--text-primary);
53
- --color-popover: var(--popover);
55
+ --color-popover: var(--background);
54
56
  --color-popover-foreground: var(--text-primary);
55
57
  --color-primary: var(--primary);
56
58
  --color-primary-foreground: var(--primary-foreground);