@ponti-studios/ui 0.7.1 → 1.0.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 (42) hide show
  1. package/README.md +13 -0
  2. package/package.json +22 -20
  3. package/src/components/data-display/accordion.tsx +1 -1
  4. package/src/components/data-display/sort-controls.tsx +3 -3
  5. package/src/components/feedback/index.ts +0 -2
  6. package/src/components/forms/date-month-select.tsx +12 -3
  7. package/src/components/forms/date-picker.tsx +16 -12
  8. package/src/components/forms/drop-zone.tsx +1 -1
  9. package/src/components/forms/index.ts +4 -8
  10. package/src/components/forms/passkey-management.tsx +4 -10
  11. package/src/components/forms/radio-group.tsx +8 -4
  12. package/src/components/forms/select.tsx +1 -1
  13. package/src/components/forms/text-field.tsx +40 -5
  14. package/src/components/navigation/app-navigation.stories.tsx +111 -21
  15. package/src/components/navigation/app-navigation.tsx +165 -137
  16. package/src/components/navigation/index.ts +8 -4
  17. package/src/components/navigation/pagination-controls.stories.tsx +1 -1
  18. package/src/components/navigation/pagination-controls.tsx +10 -14
  19. package/src/components/overlays/dropdown-menu.stories.tsx +2 -2
  20. package/src/components/primitives/card.tsx +6 -10
  21. package/src/components/primitives/label.stories.tsx +0 -18
  22. package/src/components/primitives/label.tsx +12 -12
  23. package/src/constants/chart-colors.ts +16 -16
  24. package/src/lib/slot.tsx +29 -0
  25. package/src/styles/animations.css +0 -22
  26. package/src/styles/index.css +60 -6
  27. package/src/styles/tokens/color-systems.stories.tsx +2 -1
  28. package/src/styles/tokens/generated/light.colors.generated.ts +3 -3
  29. package/src/styles/tokens/generated/light.css +3 -3
  30. package/src/styles/tokens/source/light.tokens.json +3 -3
  31. package/src/styles/tokens/style-dictionary.light-storybook.json +16 -0
  32. package/src/components/feedback/file-upload-status-badge.stories.tsx +0 -28
  33. package/src/components/feedback/file-upload-status-badge.tsx +0 -33
  34. package/src/components/forms/field.stories.tsx +0 -62
  35. package/src/components/forms/field.tsx +0 -65
  36. package/src/components/forms/field.types.ts +0 -7
  37. package/src/components/forms/group-by-select.stories.tsx +0 -26
  38. package/src/components/forms/group-by-select.tsx +0 -47
  39. package/src/components/navigation/route-link.stories.tsx +0 -30
  40. package/src/components/navigation/route-link.tsx +0 -42
  41. package/src/hooks/use-derived-filter-state.stories.tsx +0 -136
  42. package/src/hooks/use-filter-state.stories.tsx +0 -102
package/README.md CHANGED
@@ -11,6 +11,19 @@ The design system is documented in `docs/`:
11
11
  - [Patterns](./docs/patterns.md) — screen composition rules
12
12
  - [Review gates](./docs/review-gates.md) — pass/fail review criteria
13
13
 
14
+ ## Development
15
+
16
+ Common development workflows are available through the `Makefile`:
17
+
18
+ ```bash
19
+ make check # tokens, source contracts, and typecheck
20
+ make tokens-build # regenerate token artifacts
21
+ make storybook # build Storybook themes and start Storybook
22
+ make test-a11y # run light and dark accessibility tests
23
+ ```
24
+
25
+ The existing `pnpm run` scripts delegate to these targets for compatibility.
26
+
14
27
  ## Release
15
28
 
16
29
  Releases are owned by [release-please](https://github.com/googleapis/release-please), not by hand-edited version numbers or manually pushed tags. The `.github/workflows/publish.yml` workflow runs after a successful `validate` push to `main`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ponti-studios/ui",
3
- "version": "0.7.1",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "description": "Shared UI component library for Ponti Studios",
6
6
  "license": "MIT",
@@ -82,18 +82,20 @@
82
82
  "registry": "https://registry.npmjs.org"
83
83
  },
84
84
  "scripts": {
85
- "build": "tsc --noEmit",
85
+ "build": "make build",
86
86
  "dev": "vite",
87
- "storybook": "style-dictionary build --config src/styles/tokens/style-dictionary.dark-storybook.json && storybook dev -p 6006",
88
- "check:source": "node src/styles/tokens/scripts/check-source.mjs",
89
- "test": "vitest --run --config vitest.config.ts",
90
- "test:a11y": "vitest --run --config vitest.config.ts",
91
- "tokens:build": "style-dictionary build --config src/styles/tokens/style-dictionary.foundations.json && style-dictionary build --config src/styles/tokens/style-dictionary.light.json && style-dictionary build --config src/styles/tokens/style-dictionary.dark.json && style-dictionary build --config src/styles/tokens/style-dictionary.dark-storybook.json",
92
- "tokens:check": "pnpm run tokens:build && node src/styles/tokens/scripts/check-foundations.mjs && git diff --exit-code -- src/styles/tokens/generated",
93
- "typecheck": "tsc --noEmit",
94
- "lint": "oxlint src --fix",
95
- "format": "oxfmt src --write",
96
- "prepack": "pnpm run tokens:check && pnpm run check:source && pnpm run typecheck"
87
+ "storybook": "make storybook",
88
+ "check:source": "make check-source",
89
+ "test": "make test",
90
+ "test:a11y": "make test-a11y",
91
+ "test:a11y:light": "make test-a11y-light",
92
+ "test:a11y:dark": "make test-a11y-dark",
93
+ "tokens:build": "make tokens-build",
94
+ "tokens:check": "make tokens-check",
95
+ "typecheck": "make typecheck",
96
+ "lint": "make lint",
97
+ "format": "make format",
98
+ "prepack": "make prepack"
97
99
  },
98
100
  "dependencies": {
99
101
  "@base-ui/react": "^1.6.0",
@@ -107,13 +109,13 @@
107
109
  },
108
110
  "devDependencies": {
109
111
  "@chromatic-com/storybook": "^5.2.1",
110
- "@storybook/addon-a11y": "10.5.3",
111
- "@storybook/addon-docs": "^10.5.3",
112
- "@storybook/addon-links": "^10.5.3",
113
- "@storybook/addon-onboarding": "^10.5.3",
114
- "@storybook/addon-themes": "10.5.3",
115
- "@storybook/addon-vitest": "10.5.3",
116
- "@storybook/react-vite": "^10.5.3",
112
+ "@storybook/addon-a11y": "10.5.6",
113
+ "@storybook/addon-docs": "10.5.6",
114
+ "@storybook/addon-links": "10.5.6",
115
+ "@storybook/addon-onboarding": "10.5.6",
116
+ "@storybook/addon-themes": "10.5.6",
117
+ "@storybook/addon-vitest": "10.5.6",
118
+ "@storybook/react-vite": "10.5.6",
117
119
  "@tailwindcss/vite": "^4.3.3",
118
120
  "@types/node": "^26.1.1",
119
121
  "@types/react": "^19.2.17",
@@ -127,7 +129,7 @@
127
129
  "react": "^19.2.7",
128
130
  "react-dom": "^19.2.7",
129
131
  "react-native": "0.86.0",
130
- "storybook": "^10.5.3",
132
+ "storybook": "10.5.6",
131
133
  "style-dictionary": "5.5.0",
132
134
  "tailwindcss": "^4.3.3",
133
135
  "typescript": "6.0.3",
@@ -46,7 +46,7 @@ function AccordionTrigger({
46
46
  <AccordionPrimitive.Trigger
47
47
  data-slot="accordion-trigger"
48
48
  className={cn(
49
- "flex w-full flex-1 items-center justify-between gap-4 px-4 py-3 text-left text-sm font-semibold disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
49
+ "text-muted-foreground flex w-full flex-1 items-center justify-between gap-4 px-4 py-3 text-left text-sm font-semibold disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
50
50
  className,
51
51
  )}
52
52
  {...props}
@@ -48,10 +48,10 @@ export function SortControls({
48
48
  </Button>
49
49
  </DropdownMenuTrigger>
50
50
  <DropdownMenuContent className="w-80 space-y-2 p-2">
51
- <DropdownMenuLabel>Define Sort Order</DropdownMenuLabel>
52
- <DropdownMenuSeparator />
53
-
54
51
  <DropdownMenuGroup className="space-y-1">
52
+ <DropdownMenuLabel>Define Sort Order</DropdownMenuLabel>
53
+ <DropdownMenuSeparator />
54
+
55
55
  {sortOptions.map((sort, index) => {
56
56
  const usedFields = sortOptions
57
57
  .filter((_, i) => i !== index)
@@ -3,8 +3,6 @@ export { EmptyState } from "./empty-state";
3
3
  export type { EmptyStateProps } from "./empty-state";
4
4
  export { FileUploadStatus } from "./file-upload-status";
5
5
  export type { FileUploadState, FileUploadStats } from "./file-upload-status";
6
- export { FileUploadStatusBadge } from "./file-upload-status-badge";
7
- export type { FileUploadStatusValue } from "./file-upload-status-badge";
8
6
  export { Progress } from "./progress";
9
7
  export { ProgressBar } from "./progress-bar";
10
8
  export type { ProgressBarProps } from "./progress-bar";
@@ -1,6 +1,7 @@
1
1
  import { Calendar } from "lucide-react";
2
2
  import { useMemo } from "react";
3
3
 
4
+ import { cn } from "../../lib/utils";
4
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
5
6
 
6
7
  interface MonthOption {
@@ -40,13 +41,21 @@ export function DateMonthSelect({
40
41
  }, [monthsBack]);
41
42
 
42
43
  return (
43
- <div className="flex items-center gap-2">
44
- <Calendar className="size-4" />
44
+ <div className="flex items-center">
45
+ <div className="flex items-center justify-center size-10 border rounded-l-xl bg-surface-2 text-text-1">
46
+ <Calendar className="size-4" />
47
+ </div>
45
48
  <Select
46
49
  value={selectedMonthYear}
47
50
  onValueChange={(v: string | null) => v != null && onMonthChange(v)}
48
51
  >
49
- <SelectTrigger className={className} aria-label={placeholder}>
52
+ <SelectTrigger
53
+ className={cn(
54
+ "rounded-r-xl rounded-l-none min-h-10 max-h-10 bg-surface-2 text-text-1 border-l-0",
55
+ className,
56
+ )}
57
+ aria-label={placeholder}
58
+ >
50
59
  <SelectValue placeholder={placeholder} />
51
60
  </SelectTrigger>
52
61
  <SelectContent>
@@ -1,9 +1,9 @@
1
1
  import { format } from "date-fns";
2
2
  import { CalendarIcon } from "lucide-react";
3
3
 
4
- import { Button } from "../primitives/button";
5
4
  import { Calendar } from "../forms/calendar";
6
5
  import { Popover, PopoverContent, PopoverTrigger } from "../overlays/popover";
6
+ import { Button } from "../primitives/button";
7
7
 
8
8
  export interface DatePickerProps {
9
9
  value: Date | undefined;
@@ -40,17 +40,21 @@ export function DatePicker({
40
40
  </label>
41
41
  ) : null}
42
42
  <Popover>
43
- <PopoverTrigger asChild>
44
- <Button
45
- id={id}
46
- variant={variant}
47
- disabled={disabled}
48
- className="bg-muted placeholder:text-muted-foreground w-full justify-start text-left font-normal"
49
- >
50
- <CalendarIcon className="mr-2 size-4" />
51
- {value ? format(value, dateFormat) : <span>{placeholder}</span>}
52
- </Button>
53
- </PopoverTrigger>
43
+ <div className="flex w-full items-center">
44
+ <div className="border-border bg-surface-2 text-text-1 flex size-10 shrink-0 items-center justify-center rounded-l-xl border">
45
+ <CalendarIcon className="size-4" aria-hidden />
46
+ </div>
47
+ <PopoverTrigger asChild>
48
+ <Button
49
+ id={id}
50
+ variant={variant}
51
+ disabled={disabled}
52
+ className="border-border bg-surface-2 text-text-1 placeholder:text-muted-foreground min-h-10 max-h-10 w-full justify-start rounded-l-none rounded-r-xl border-l-0 text-left font-normal"
53
+ >
54
+ {value ? format(value, dateFormat) : <span>{placeholder}</span>}
55
+ </Button>
56
+ </PopoverTrigger>
57
+ </div>
54
58
  <PopoverContent className="bg-popover w-auto p-0" align={popoverAlign}>
55
59
  <Calendar mode="single" selected={value} onSelect={onSelect} />
56
60
  </PopoverContent>
@@ -126,7 +126,7 @@ export function DropZone({
126
126
  >
127
127
  <div className="space-y-2 text-center">
128
128
  <p className="text-muted-foreground">
129
- {label} <span className="text-foreground font-medium">click to browse</span>
129
+ {label} <span className="text-accent font-medium">click to browse</span>
130
130
  </p>
131
131
  {helpText ? <p className="text-muted-foreground text-sm">{helpText}</p> : null}
132
132
  </div>
@@ -1,17 +1,13 @@
1
+ export { FilterSelect } from "../filters/filter-select";
2
+ export type { FilterSelectOption, FilterSelectProps } from "../filters/filter-select";
1
3
  export { Calendar } from "./calendar";
2
4
  export type { CalendarProps } from "./calendar";
3
- export { DatePicker } from "./date-picker";
4
- export type { DatePickerProps } from "./date-picker";
5
5
  export { DateMonthSelect, getCurrentMonthYear } from "./date-month-select";
6
6
  export type { DateMonthSelectProps } from "./date-month-select";
7
+ export { DatePicker } from "./date-picker";
8
+ export type { DatePickerProps } from "./date-picker";
7
9
  export { DropZone } from "./drop-zone";
8
10
  export type { DropZoneProps } from "./drop-zone";
9
- export { Field } from "./field";
10
- export type { FieldProps } from "./field";
11
- export { FilterSelect } from "../filters/filter-select";
12
- export type { FilterSelectOption, FilterSelectProps } from "../filters/filter-select";
13
- export { GroupBySelect } from "./group-by-select";
14
- export type { GroupBySelectProps } from "./group-by-select";
15
11
  export { Input } from "./input";
16
12
  export { PasskeyManagement } from "./passkey-management";
17
13
  export type { PasskeyManagementProps, PasskeyRecord } from "./passkey-management";
@@ -68,17 +68,11 @@ export function PasskeyManagement({
68
68
  return (
69
69
  <section aria-labelledby="passkey-heading" className="space-y-4">
70
70
  <div className="flex items-center justify-between">
71
- <div>
72
- <h2 id="passkey-heading" className="text-foreground text-sm font-medium">
73
- Passkeys
74
- </h2>
75
- <p className="text-muted-foreground text-sm">
76
- Sign in without a password using biometrics or a security key.
77
- </p>
78
- </div>
71
+ <h2 id="passkey-heading" className="text-foreground text-sm font-medium">
72
+ Passkeys
73
+ </h2>
79
74
  <Button
80
75
  type="button"
81
- variant="outline"
82
76
  size="sm"
83
77
  onClick={() => void handleAdd()}
84
78
  disabled={adding}
@@ -107,7 +101,7 @@ export function PasskeyManagement({
107
101
  {passkeys.map((pk) => (
108
102
  <li
109
103
  key={pk.id}
110
- className="border-border flex items-center justify-between border px-4 py-3 text-sm"
104
+ className="border-border rounded flex items-center justify-between border px-4 py-3 text-sm"
111
105
  >
112
106
  <div className="flex items-center gap-3">
113
107
  <KeyRound className="text-muted-foreground size-4 shrink-0" aria-hidden />
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
4
3
  import { Radio as RadioPrimitive } from "@base-ui/react/radio";
4
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
5
5
  import { Circle } from "lucide-react";
6
6
  import * as React from "react";
7
7
 
@@ -23,13 +23,17 @@ const RadioGroupItem = React.forwardRef<
23
23
  <RadioPrimitive.Root
24
24
  ref={ref}
25
25
  className={cn(
26
- "border-primary bg-background text-foreground focus-visible:border-ring focus-visible:bg-muted/25 focus-visible:ring-ring aspect-square h-6 w-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
26
+ "border-primary bg-background focus-visible:border-ring focus-visible:bg-muted/25 focus-visible:ring-ring aspect-square size-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
27
+ "flex items-center justify-center",
27
28
  className,
28
29
  )}
29
30
  {...props}
30
31
  >
31
- <RadioPrimitive.Indicator className="flex items-center justify-center">
32
- <Circle className="fill-primary h-4 w-4" />
32
+ <RadioPrimitive.Indicator
33
+ keepMounted
34
+ className="data-unchecked:opacity-0 data-checked:opacity-100 data-starting-style:animate-fade-in data-ending-style:animate-fade-out"
35
+ >
36
+ <Circle className="fill-primary size-5" />
33
37
  </RadioPrimitive.Indicator>
34
38
  </RadioPrimitive.Root>
35
39
  );
@@ -71,7 +71,7 @@ function SelectContent({
71
71
  <SelectPrimitive.Popup
72
72
  data-slot="select-content"
73
73
  className={cn(
74
- "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
74
+ "bg-background text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
75
75
  className,
76
76
  )}
77
77
  {...props}
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
 
3
- import { Field } from "./field";
3
+ import { cn } from "../../lib/utils";
4
4
  import { Input } from "./input";
5
5
  import type { TextFieldBaseProps, TextFieldType } from "./text-field.types";
6
6
 
@@ -26,14 +26,49 @@ function TextField({
26
26
  type = "text",
27
27
  ...inputProps
28
28
  }: TextFieldProps) {
29
+ const generatedId = React.useId();
30
+ const inputId = id ?? generatedId;
31
+ const descriptionId = `${inputId}-description`;
32
+ const errorId = `${inputId}-error`;
33
+
29
34
  if (!label && !helpText && !error) {
30
- return <Input className={className} disabled={disabled} type={type} {...inputProps} />;
35
+ return (
36
+ <Input id={inputId} className={className} disabled={disabled} type={type} {...inputProps} />
37
+ );
31
38
  }
32
39
 
33
40
  return (
34
- <Field id={id} label={label} helpText={helpText} error={error}>
35
- <Input className={className} disabled={disabled} type={type} {...inputProps} />
36
- </Field>
41
+ <div className="flex flex-col gap-1.5">
42
+ {label && (
43
+ <label
44
+ htmlFor={inputId}
45
+ className={cn(
46
+ "text-foreground text-sm font-medium",
47
+ inputProps.required && "after:text-destructive-text after:ml-0.5 after:content-['*']",
48
+ )}
49
+ >
50
+ {label}
51
+ </label>
52
+ )}
53
+ <Input
54
+ id={inputId}
55
+ className={className}
56
+ disabled={disabled}
57
+ type={type}
58
+ aria-describedby={error ? errorId : helpText ? descriptionId : undefined}
59
+ aria-invalid={error ? true : undefined}
60
+ {...inputProps}
61
+ />
62
+ {error ? (
63
+ <p id={errorId} role="alert" className="text-destructive-text text-sm">
64
+ {error}
65
+ </p>
66
+ ) : helpText ? (
67
+ <p id={descriptionId} className="text-muted-foreground text-sm">
68
+ {helpText}
69
+ </p>
70
+ ) : null}
71
+ </div>
37
72
  );
38
73
  }
39
74
 
@@ -1,17 +1,17 @@
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",
12
13
  },
13
14
  };
14
-
15
15
  export default meta;
16
16
  type Story = StoryObj<typeof meta>;
17
17
 
@@ -19,7 +19,7 @@ type Story = StoryObj<typeof meta>;
19
19
  // Shared helpers
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
- const defaultLinks: Array<{ href: string; label: string }> = [
22
+ const defaultLinks = [
23
23
  { href: "/features", label: "Features" },
24
24
  { href: "/pricing", label: "Pricing" },
25
25
  { href: "/blog", label: "Blog" },
@@ -46,12 +46,25 @@ function PageContent({
46
46
  export const Default: Story = {
47
47
  render: () => (
48
48
  <>
49
- <AppNavigation
50
- brand="Acme"
51
- brandHref="/"
52
- links={defaultLinks}
53
- cta={{ href: "/get-started", label: "Get started", variant: "default" }}
54
- />
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>
55
68
  <PageContent />
56
69
  </>
57
70
  ),
@@ -60,22 +73,99 @@ export const Default: Story = {
60
73
  export const NoCta: Story = {
61
74
  render: () => (
62
75
  <>
63
- <AppNavigation brand="Acme" brandHref="/" links={defaultLinks} />
64
- <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." />
65
89
  </>
66
90
  ),
67
91
  };
68
92
 
69
- 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 = {
70
141
  render: () => (
71
142
  <>
72
- <AppNavigation
73
- brand="Acme"
74
- brandHref="/"
75
- links={defaultLinks}
76
- cta={{ href: "/get-started", label: "Get started", variant: "outline" }}
77
- />
78
- <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." />
79
169
  </>
80
170
  ),
81
171
  };