@octanejs/shadcn 0.0.8 → 0.0.10

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/README.md CHANGED
@@ -13,20 +13,187 @@ Upstream pin: `shadcn-ui/ui@4baadbc6` + CLI `shadcn@4.14.1`.
13
13
  radix-backed Tier-2 components, and the first Tier-3 composites (Sidebar with
14
14
  `useSidebar`/`useIsMobile`, Field), plus `cn()` and the default neutral theme
15
15
  tokens (`@octanejs/shadcn/theme.css`).
16
- - **Two consumption modes**: import from the package entry, or install
16
+ - **Two consumption modes**: import from the package subpaths, or install
17
17
  components AS SOURCE with the upstream shadcn CLI against the generated
18
- registry (`registry/`, 47 items — `pnpm registry:build` / `registry:check`).
18
+ registry (`registry/` — `pnpm registry:build` / `registry:check`).
19
+ - **Three primitive bases in one registry**, selected the way shadcn selects its
20
+ own: `components.json`'s `style` field, substituted into the registry URL. See
21
+ [Installing with the shadcn CLI](#installing-with-the-shadcn-cli).
19
22
  - **Styling flavors**: components are migrating to the default-Tailwind
20
23
  (utilities-inlined) flavor — class strings work against any Tailwind v4 build
21
24
  with the shadcn theme tokens. The not-yet-migrated families still carry the
22
25
  upstream `cn-*` semantic hooks and need a shadcn style sheet (e.g.
23
26
  `style-nova`). `status.json` lists exactly which families are which.
24
27
 
28
+ ## Component coverage
29
+
30
+ Each primitive base ships the families its primitives support, exactly as upstream's do.
31
+ Switching `style` changes which primitive a component is built on — the `data-slot` contract and
32
+ component names stay identical.
33
+
34
+ <!-- BEGIN COVERAGE -->
35
+
36
+ **44 families** — Radix 44/44 · React Aria 33/44 · Base UI 35/44
37
+
38
+ ✅ ported · — not ported yet (fair game) · ⛔ blocked, see notes below
39
+
40
+ | Family | Radix | React Aria | Base UI |
41
+ | --- | :---: | :---: | :---: |
42
+ | `accordion` | ✅ | ✅ | ✅ |
43
+ | `alert` | ✅ | ✅ | ✅ |
44
+ | `alert-dialog` | ✅ | ✅ | ✅ |
45
+ | `aspect-ratio` | ✅ | ✅ | ✅ |
46
+ | `avatar` | ✅ | ✅ | ✅ |
47
+ | `badge` | ✅ | ✅ | ✅ |
48
+ | `breadcrumb` | ✅ | ✅ | ✅ |
49
+ | `button` | ✅ | ✅ | ✅ |
50
+ | `card` | ✅ | ✅ | ✅ |
51
+ | `checkbox` | ✅ | ✅ | ✅ |
52
+ | `collapsible` | ✅ | ✅ | ✅ |
53
+ | `context-menu` | ✅ | ⛔ | — |
54
+ | `dialog` | ✅ | ✅ | ✅ |
55
+ | `dropdown-menu` | ✅ | ⛔ | ✅ |
56
+ | `empty` | ✅ | ✅ | ✅ |
57
+ | `field` | ✅ | ✅ | ✅ |
58
+ | `hover-card` | ✅ | ⛔ | — |
59
+ | `input` | ✅ | ✅ | ✅ |
60
+ | `item` | ✅ | ✅ | ✅ |
61
+ | `kbd` | ✅ | ✅ | ✅ |
62
+ | `label` | ✅ | ✅ | ✅ |
63
+ | `menubar` | ✅ | ⛔ | — |
64
+ | `native-select` | ✅ | ✅ | ✅ |
65
+ | `navigation-menu` | ✅ | ⛔ | ⛔ |
66
+ | `pagination` | ✅ | ✅ | ✅ |
67
+ | `popover` | ✅ | ✅ | ✅ |
68
+ | `progress` | ✅ | ⛔ | ✅ |
69
+ | `radio-group` | ✅ | ✅ | ✅ |
70
+ | `scroll-area` | ✅ | ✅ | ⛔ |
71
+ | `select` | ✅ | ⛔ | ⛔ |
72
+ | `separator` | ✅ | ✅ | ✅ |
73
+ | `sheet` | ✅ | ✅ | ✅ |
74
+ | `sidebar` | ✅ | ⛔ | — |
75
+ | `skeleton` | ✅ | ✅ | ✅ |
76
+ | `slider` | ✅ | ✅ | ✅ |
77
+ | `sonner` | ✅ | ⛔ | ⛔ |
78
+ | `spinner` | ✅ | ✅ | ✅ |
79
+ | `switch` | ✅ | ✅ | ✅ |
80
+ | `table` | ✅ | ✅ | ✅ |
81
+ | `tabs` | ✅ | ✅ | ⛔ |
82
+ | `textarea` | ✅ | ✅ | ✅ |
83
+ | `toggle` | ✅ | ✅ | ✅ |
84
+ | `toggle-group` | ✅ | ⛔ | ✅ |
85
+ | `tooltip` | ✅ | ⛔ | ✅ |
86
+
87
+ **Blocked**
88
+
89
+ | Base | Family | Reason |
90
+ | --- | --- | --- |
91
+ | Base UI | `navigation-menu` | @octanejs/base-ui has no navigation-menu primitive |
92
+ | Base UI | `scroll-area` | @octanejs/base-ui has no scroll-area primitive |
93
+ | Base UI | `select` | @octanejs/base-ui has no select primitive |
94
+ | Base UI | `sonner` | needs next-themes, which has no octane binding |
95
+ | Base UI | `tabs` | @octanejs/base-ui has no tabs primitive |
96
+ | React Aria | `context-menu` | no counterpart in upstream’s aria base |
97
+ | React Aria | `dropdown-menu` | no counterpart in upstream’s aria base |
98
+ | React Aria | `hover-card` | no counterpart in upstream’s aria base |
99
+ | React Aria | `menubar` | no counterpart in upstream’s aria base |
100
+ | React Aria | `navigation-menu` | no counterpart in upstream’s aria base |
101
+ | React Aria | `progress` | no counterpart in upstream’s aria base |
102
+ | React Aria | `select` | needs input-group |
103
+ | React Aria | `sidebar` | no counterpart in upstream’s aria base |
104
+ | React Aria | `sonner` | needs next-themes, which has no octane binding |
105
+ | React Aria | `toggle-group` | no counterpart in upstream’s aria base |
106
+ | React Aria | `tooltip` | no counterpart in upstream’s aria base |
107
+
108
+ <!-- END COVERAGE -->
109
+
110
+ The table is generated from the sources by `pnpm coverage:build` and gated by `coverage:check`,
111
+ so it cannot drift as families land.
112
+
113
+ ## Installing with the shadcn CLI
114
+
115
+ The registry is consumed by the **upstream** `shadcn` CLI — nothing octane-specific is
116
+ required on the consumer side.
117
+
118
+ Add the registry to your `components.json` and pick a base with `style`:
119
+
120
+ ```jsonc
121
+ {
122
+ "style": "base-nova", // base-nova | radix-nova | aria-nova
123
+ "registries": {
124
+ "@octane": "https://octanejs.dev/r/styles/{style}/{name}.json"
125
+ }
126
+ }
127
+ ```
128
+
129
+ ```bash
130
+ npx shadcn add @octane/button
131
+ ```
132
+
133
+ `{style}` and `{name}` are the only placeholders the CLI substitutes, and it never parses the
134
+ style string — so the base is chosen entirely by which tree the registry serves. This is the
135
+ same mechanism shadcn uses upstream, where its own registry is
136
+ `<host>/styles/{style}/{name}.json`.
137
+
138
+ | `style` | Primitives | Families |
139
+ | --- | --- | --- |
140
+ | `base-nova` *(default)* | `@octanejs/base-ui` | 21 |
141
+ | `radix-nova` | `@octanejs/radix` | 44 |
142
+ | `aria-nova` | `@octanejs/aria` | 33 |
143
+
144
+ The bases deliberately ship different family counts, exactly as upstream's do — a base only
145
+ has the families its primitives support. Switching `style` changes which primitive a component
146
+ is built on; the `data-slot` contract and component names stay identical, so your markup and
147
+ styling do not change.
148
+
149
+ A registry URL without the `{style}` segment resolves to the default style, so
150
+ `https://octanejs.dev/r/{name}.json` serves the Base UI variant.
151
+
152
+ ### Serving it locally
153
+
154
+ ```bash
155
+ pnpm --dir packages/shadcn registry:build # regenerate from src/
156
+ pnpm --dir packages/shadcn registry:serve # http://localhost:4517
157
+ ```
158
+
159
+ Then point `components.json` at `http://localhost:4517/styles/{style}/{name}.json`, which is
160
+ what `playground/octane` does.
161
+
162
+ ## Importing from the package
163
+
164
+ There is no barrel entry. Every family has its own subpath, so an app pulls in only what it
165
+ uses — the Radix base at the bare name, the others under their base:
166
+
25
167
  ```tsx
26
- import { Button, Dialog, Tabs } from '@octanejs/shadcn';
168
+ // Radix base (the bare subpaths)
169
+ import { Button } from '@octanejs/shadcn/Button';
170
+ import { Dialog, DialogContent, DialogTitle } from '@octanejs/shadcn/Dialog';
171
+
172
+ // React Aria base
173
+ import { Button } from '@octanejs/shadcn/react-aria/Button';
174
+
175
+ // Base UI base
176
+ import {
177
+ AlertDialog,
178
+ AlertDialogAction,
179
+ AlertDialogCancel,
180
+ AlertDialogContent,
181
+ AlertDialogDescription,
182
+ AlertDialogFooter,
183
+ AlertDialogHeader,
184
+ AlertDialogMedia,
185
+ AlertDialogTitle,
186
+ AlertDialogTrigger,
187
+ } from '@octanejs/shadcn/base-ui/AlertDialog';
188
+
27
189
  import '@octanejs/shadcn/theme.css';
28
190
  ```
29
191
 
192
+ Subpaths are `PascalCase` and map to the family, not the export: `AlertDialogTrigger` and its
193
+ siblings all come from `.../AlertDialog`. `cn` lives at `@octanejs/shadcn/cn`, shared types at
194
+ `@octanejs/shadcn/types`, and `useIsMobile` at `@octanejs/shadcn/hooks/use-mobile`.
195
+
196
+
30
197
  Octane adaptations are uniform and documented per file: no `"use client"`,
31
198
  refs as props, native events (`onInput` per keystroke), `asChild` and Portal
32
199
  children composed as element descriptors, and children forwarding through the
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@octanejs/shadcn",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=22"
7
+ "node": ">=22.22.2"
8
8
  },
9
9
  "description": "shadcn/ui for the octane renderer — the Radix-base shadcn component distribution ported onto octane: cva-variant components preserving the upstream data-slot/class contract, built on @octanejs/radix, published as importable source with a shadcn-CLI-compatible registry planned.",
10
10
  "author": {
@@ -108,22 +108,36 @@
108
108
  "./base-ui/Alert": "./src/bases/base-ui/ui/alert.tsrx",
109
109
  "./base-ui/AlertDialog": "./src/bases/base-ui/ui/alert-dialog.tsrx",
110
110
  "./base-ui/AspectRatio": "./src/bases/base-ui/ui/aspect-ratio.tsrx",
111
+ "./base-ui/Avatar": "./src/bases/base-ui/ui/avatar.tsrx",
112
+ "./base-ui/Badge": "./src/bases/base-ui/ui/badge.tsrx",
113
+ "./base-ui/Breadcrumb": "./src/bases/base-ui/ui/breadcrumb.tsrx",
111
114
  "./base-ui/Button": "./src/bases/base-ui/ui/button.tsrx",
112
115
  "./base-ui/Card": "./src/bases/base-ui/ui/card.tsrx",
113
116
  "./base-ui/Checkbox": "./src/bases/base-ui/ui/checkbox.tsrx",
117
+ "./base-ui/Collapsible": "./src/bases/base-ui/ui/collapsible.tsrx",
114
118
  "./base-ui/Dialog": "./src/bases/base-ui/ui/dialog.tsrx",
119
+ "./base-ui/DropdownMenu": "./src/bases/base-ui/ui/dropdown-menu.tsrx",
115
120
  "./base-ui/Empty": "./src/bases/base-ui/ui/empty.tsrx",
121
+ "./base-ui/Field": "./src/bases/base-ui/ui/field.tsrx",
116
122
  "./base-ui/Input": "./src/bases/base-ui/ui/input.tsrx",
123
+ "./base-ui/Item": "./src/bases/base-ui/ui/item.tsrx",
117
124
  "./base-ui/Kbd": "./src/bases/base-ui/ui/kbd.tsrx",
118
125
  "./base-ui/Label": "./src/bases/base-ui/ui/label.tsrx",
119
126
  "./base-ui/NativeSelect": "./src/bases/base-ui/ui/native-select.tsrx",
127
+ "./base-ui/Pagination": "./src/bases/base-ui/ui/pagination.tsrx",
120
128
  "./base-ui/Popover": "./src/bases/base-ui/ui/popover.tsrx",
129
+ "./base-ui/Progress": "./src/bases/base-ui/ui/progress.tsrx",
121
130
  "./base-ui/RadioGroup": "./src/bases/base-ui/ui/radio-group.tsrx",
122
131
  "./base-ui/Separator": "./src/bases/base-ui/ui/separator.tsrx",
132
+ "./base-ui/Sheet": "./src/bases/base-ui/ui/sheet.tsrx",
123
133
  "./base-ui/Skeleton": "./src/bases/base-ui/ui/skeleton.tsrx",
134
+ "./base-ui/Slider": "./src/bases/base-ui/ui/slider.tsrx",
124
135
  "./base-ui/Spinner": "./src/bases/base-ui/ui/spinner.tsrx",
125
136
  "./base-ui/Switch": "./src/bases/base-ui/ui/switch.tsrx",
137
+ "./base-ui/Table": "./src/bases/base-ui/ui/table.tsrx",
126
138
  "./base-ui/Textarea": "./src/bases/base-ui/ui/textarea.tsrx",
139
+ "./base-ui/Toggle": "./src/bases/base-ui/ui/toggle.tsrx",
140
+ "./base-ui/ToggleGroup": "./src/bases/base-ui/ui/toggle-group.tsrx",
127
141
  "./base-ui/Tooltip": "./src/bases/base-ui/ui/tooltip.tsrx",
128
142
  "./cn": "./src/lib/utils.ts",
129
143
  "./types": "./src/lib/types.ts",
@@ -134,14 +148,14 @@
134
148
  "class-variance-authority": "0.7.1",
135
149
  "clsx": "^2.1.1",
136
150
  "tailwind-merge": "3.6.0",
137
- "@octanejs/aria": "0.0.17",
138
- "@octanejs/base-ui": "0.1.21",
139
- "@octanejs/lucide": "0.1.18",
140
- "@octanejs/radix": "0.1.22",
141
- "@octanejs/sonner": "0.1.18"
151
+ "@octanejs/aria": "0.0.19",
152
+ "@octanejs/base-ui": "0.1.23",
153
+ "@octanejs/lucide": "0.1.20",
154
+ "@octanejs/radix": "0.1.24",
155
+ "@octanejs/sonner": "0.1.20"
142
156
  },
143
157
  "peerDependencies": {
144
- "octane": "0.1.23"
158
+ "octane": "0.1.25"
145
159
  },
146
160
  "devDependencies": {
147
161
  "@tsrx/react": "^0.2.56",
@@ -151,12 +165,15 @@
151
165
  "react": "^19.2.7",
152
166
  "react-dom": "^19.2.7",
153
167
  "vitest": "^4.1.10",
154
- "octane": "0.1.23"
168
+ "octane": "0.1.25"
155
169
  },
156
170
  "scripts": {
157
171
  "test": "vitest run",
158
172
  "registry:build": "node scripts/build-registry.mjs",
159
173
  "registry:check": "node scripts/build-registry.mjs --check",
174
+ "registry:serve": "node scripts/serve-registry.mjs",
175
+ "coverage:build": "node scripts/build-coverage-table.mjs",
176
+ "coverage:check": "node scripts/build-coverage-table.mjs --check",
160
177
  "typecheck": "node scripts/typecheck.mjs"
161
178
  }
162
179
  }
@@ -0,0 +1,89 @@
1
+ // Base UI base avatar — runs on @octanejs/base-ui's Avatar (Root/Image/Fallback), a direct
2
+ // one-to-one mapping with the radix base.
3
+ //
4
+ // `AvatarBadge`, `AvatarGroup` and `AvatarGroupCount` are plain host elements in every base —
5
+ // upstream defines no primitive for them — so they are unchanged from the radix source.
6
+ //
7
+ // Nothing here keys off a primitive-emitted attribute: the `data-[size=…]` variants read the
8
+ // `data-size` this component writes itself, and the primitive emits no state attributes at all
9
+ // (verified by rendering it — Root and Fallback are bare spans). So unlike the overlays and the
10
+ // form controls, there is no dialect to get wrong.
11
+ //
12
+ // UNVERIFIED PROVENANCE: class strings come from this package's radix base rather than
13
+ // transcribed from upstream's Base UI source.
14
+ import { Avatar as AvatarPrimitive } from '@octanejs/base-ui/avatar';
15
+
16
+ import { cn } from '../../../lib/utils';
17
+
18
+ type Props = { className?: string } & Record<string, unknown>;
19
+
20
+ export interface AvatarProps extends Props {
21
+ size?: 'default' | 'sm' | 'lg';
22
+ }
23
+
24
+ export function Avatar({ className, size = 'default', ...props }: AvatarProps) @{
25
+ <AvatarPrimitive.Root
26
+ data-slot="avatar"
27
+ data-size={size}
28
+ className={cn(
29
+ 'group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ }
35
+
36
+ export function AvatarImage({ className, ...props }: Props) @{
37
+ <AvatarPrimitive.Image
38
+ data-slot="avatar-image"
39
+ className={cn('aspect-square size-full rounded-full object-cover', className)}
40
+ {...props}
41
+ />
42
+ }
43
+
44
+ export function AvatarFallback({ className, ...props }: Props) @{
45
+ <AvatarPrimitive.Fallback
46
+ data-slot="avatar-fallback"
47
+ className={cn(
48
+ 'flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs',
49
+ className,
50
+ )}
51
+ {...props}
52
+ />
53
+ }
54
+
55
+ export function AvatarBadge({ className, ...props }: Props) @{
56
+ <span
57
+ data-slot="avatar-badge"
58
+ className={cn(
59
+ 'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
60
+ 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
61
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
62
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
63
+ className,
64
+ )}
65
+ {...props}
66
+ />
67
+ }
68
+
69
+ export function AvatarGroup({ className, ...props }: Props) @{
70
+ <div
71
+ data-slot="avatar-group"
72
+ className={cn(
73
+ 'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ }
79
+
80
+ export function AvatarGroupCount({ className, ...props }: Props) @{
81
+ <div
82
+ data-slot="avatar-group-count"
83
+ className={cn(
84
+ 'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
85
+ className,
86
+ )}
87
+ {...props}
88
+ />
89
+ }
@@ -0,0 +1,56 @@
1
+ // Base UI base badge — a plain host `<span>` carrying the shared cva strings.
2
+ //
3
+ // The cva block is byte-identical to the radix and React Aria bases (checked, not assumed), and no
4
+ // primitive is involved: badge is a span with `data-slot` and `data-variant`, and nothing
5
+ // underneath emits state. So there is no dialect here to get wrong, and no motion to mistranslate.
6
+ //
7
+ // NO ESCAPE HATCH, DELIBERATELY. Upstream's other bases each let a badge render as a different
8
+ // element, and each does it differently: radix takes `asChild` and swaps in `Slot`; React Aria
9
+ // takes a `render` FUNCTION and calls it with the computed props. Base UI has no Slot, and its own
10
+ // convention is a `render` ELEMENT prop backed by `useRender` — but badge is not a Base UI
11
+ // primitive, so nothing here decides which of those shapes upstream's Base UI badge actually
12
+ // exposes, or whether it exposes one at all.
13
+ //
14
+ // Guessing would publish an API this base may not have, and an API shape is not something a later
15
+ // commit can quietly correct. Adding the hatch once the upstream source is available is additive
16
+ // and non-breaking; shipping the wrong one is not. This follows button.tsrx in the same base,
17
+ // which omits `LinkButton` for the same reason. A consumer needing a different element today can
18
+ // spread `badgeVariants({ variant })` onto it directly, which is what the class export is for.
19
+ import { cva, type VariantProps } from 'class-variance-authority';
20
+
21
+ import { cn } from '../../../lib/utils';
22
+ import type { HostProps } from '../../../lib/types';
23
+
24
+ export const badgeVariants = cva(
25
+ 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
26
+ {
27
+ variants: {
28
+ variant: {
29
+ default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
30
+ secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
31
+ destructive: 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
32
+ outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
33
+ ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
34
+ link: 'text-primary underline-offset-4 hover:underline',
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: 'default',
39
+ },
40
+ },
41
+ );
42
+
43
+ export interface BadgeProps extends HostProps<'span'> {
44
+ variant?: VariantProps<typeof badgeVariants>['variant'];
45
+ /** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
46
+ asChild?: never;
47
+ }
48
+
49
+ export function Badge({ className, variant = 'default', ...props }: BadgeProps) @{
50
+ <span
51
+ data-slot="badge"
52
+ data-variant={variant}
53
+ className={cn(badgeVariants({ variant }), className)}
54
+ {...props}
55
+ />
56
+ }
@@ -0,0 +1,109 @@
1
+ // Base UI base breadcrumb — class strings from the maintainer-supplied radix source, verbatim.
2
+ // Six of the seven parts are plain host elements with no primitive underneath and no state
3
+ // attributes, so they carry across unchanged and there is no dialect to get wrong.
4
+ //
5
+ // `BreadcrumbLink` IS MISSING ITS ESCAPE HATCH, and unlike badge that costs something real: the
6
+ // `asChild` branch is how a router's Link gets plugged in, which is most of what this family is
7
+ // for. It is still left out, for the same reason:
8
+ //
9
+ // radix `asChild` swaps in `Slot`, a pure prop-merger
10
+ // React Aria routes through RAC's own `Link` primitive with a `render` prop
11
+ // Base UI has neither — no Slot, and no Link primitive
12
+ //
13
+ // Base UI's house convention is a `render` ELEMENT prop backed by `useRender`, and this base
14
+ // already relies on it where a real primitive offers it (see pagination). But breadcrumb has no
15
+ // primitive, so nothing here settles whether upstream's Base UI breadcrumb implements `render`
16
+ // itself, and an API shape is not something a later commit can quietly correct — adding it is
17
+ // additive, shipping the wrong spelling is breaking.
18
+ //
19
+ // Until then a router link still works, it just has to carry the classes itself:
20
+ //
21
+ // <BreadcrumbItem>
22
+ // <RouterLink className="transition-colors hover:text-foreground">Home</RouterLink>
23
+ // </BreadcrumbItem>
24
+ //
25
+ // which loses only the `data-slot="breadcrumb-link"` hook.
26
+ import { ChevronRightIcon, MoreHorizontalIcon } from '@octanejs/lucide';
27
+ import { type OctaneNode } from 'octane';
28
+
29
+ import { cn } from '../../../lib/utils';
30
+
31
+ type Props = { className?: string } & Record<string, unknown>;
32
+
33
+ export function Breadcrumb({ className, ...props }: Props) @{
34
+ <nav aria-label="breadcrumb" data-slot="breadcrumb" className={cn(className)} {...props} />
35
+ }
36
+
37
+ export function BreadcrumbList({ className, ...props }: Props) @{
38
+ <ol
39
+ data-slot="breadcrumb-list"
40
+ className={cn(
41
+ 'flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground',
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ }
47
+
48
+ export function BreadcrumbItem({ className, ...props }: Props) @{
49
+ <li
50
+ data-slot="breadcrumb-item"
51
+ className={cn('inline-flex items-center gap-1', className)}
52
+ {...props}
53
+ />
54
+ }
55
+
56
+ export interface BreadcrumbLinkProps extends Props {
57
+ /**
58
+ * NOT SUPPORTED in this base — see the header. Declared as `never` so markup carrying it over
59
+ * from the radix base fails to compile instead of silently rendering the anchor AND the child,
60
+ * which nests one link inside another.
61
+ */
62
+ asChild?: never;
63
+ }
64
+
65
+ export function BreadcrumbLink({ className, ...props }: BreadcrumbLinkProps) @{
66
+ <a
67
+ data-slot="breadcrumb-link"
68
+ className={cn('transition-colors hover:text-foreground', className)}
69
+ {...props}
70
+ />
71
+ }
72
+
73
+ export function BreadcrumbPage({ className, ...props }: Props) @{
74
+ <span
75
+ data-slot="breadcrumb-page"
76
+ role="link"
77
+ aria-disabled="true"
78
+ aria-current="page"
79
+ className={cn('font-normal text-foreground', className)}
80
+ {...props}
81
+ />
82
+ }
83
+
84
+ export function BreadcrumbSeparator(props: Props & { children?: OctaneNode }) @{
85
+ const { className, children: _children, ...rest } = props;
86
+
87
+ <li
88
+ data-slot="breadcrumb-separator"
89
+ role="presentation"
90
+ aria-hidden="true"
91
+ className={cn('[&>svg]:size-3.5', className)}
92
+ {...rest}
93
+ >
94
+ {props.children ?? <ChevronRightIcon className="cn-rtl-flip" />}
95
+ </li>
96
+ }
97
+
98
+ export function BreadcrumbEllipsis({ className, ...props }: Props) @{
99
+ <span
100
+ data-slot="breadcrumb-ellipsis"
101
+ role="presentation"
102
+ aria-hidden="true"
103
+ className={cn('flex size-5 items-center justify-center [&>svg]:size-4', className)}
104
+ {...props}
105
+ >
106
+ <MoreHorizontalIcon />
107
+ <span className="sr-only">More</span>
108
+ </span>
109
+ }
@@ -0,0 +1,25 @@
1
+ // Base UI base collapsible — runs on @octanejs/base-ui's Collapsible.
2
+ //
3
+ // PART MAPPING: shadcn names the content part `CollapsibleContent` while the Base UI part
4
+ // underneath is `Collapsible.Panel` — the same rename this base already makes for `accordion`.
5
+ // The exported names and all three `data-slot` values stay as shadcn defines them, so consumers
6
+ // see no difference between bases.
7
+ //
8
+ // No class strings: upstream ships this family unstyled in every base, leaving layout to the
9
+ // consumer. There is correspondingly no styling risk here, and nothing to verify against a
10
+ // class-string source.
11
+ import { Collapsible as CollapsiblePrimitive } from '@octanejs/base-ui/collapsible';
12
+
13
+ type Props = Record<string, unknown>;
14
+
15
+ export function Collapsible(props: Props) @{
16
+ <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
17
+ }
18
+
19
+ export function CollapsibleTrigger(props: Props) @{
20
+ <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
21
+ }
22
+
23
+ export function CollapsibleContent(props: Props) @{
24
+ <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
25
+ }