@justanarthur/modo-ui 0.6.0-canary.4 → 0.6.0-canary.6

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 (93) hide show
  1. package/DESIGN.md +12 -3
  2. package/README.md +10 -8
  3. package/components/accordion/examples.mdx +24 -0
  4. package/components/accordion/index.tsx +26 -13
  5. package/components/button/index.tsx +11 -3
  6. package/components/button/variants.ts +5 -3
  7. package/components/card/index.tsx +3 -2
  8. package/components/inputs/field/index.tsx +3 -2
  9. package/components/inputs/select/index.tsx +2 -1
  10. package/components/overlays/dialog/index.tsx +2 -2
  11. package/components/overlays/dropdown/index.tsx +8 -2
  12. package/components/overlays/popover/index.tsx +2 -2
  13. package/components/overlays/tooltip/index.tsx +2 -2
  14. package/dist/base.css +35 -4
  15. package/dist/blocks/sidebar-app/index.js +9 -9
  16. package/dist/components/AI/input-message/index.js +5 -5
  17. package/dist/components/accordion/index.d.ts +22 -9
  18. package/dist/components/accordion/index.js +2 -1
  19. package/dist/components/button/index.js +2 -2
  20. package/dist/components/button/variants.js +1 -1
  21. package/dist/components/card/index.d.ts +3 -3
  22. package/dist/components/copy/index.js +3 -3
  23. package/dist/components/inputs/color-picker/index.js +3 -3
  24. package/dist/components/inputs/date-time/date-picker/index.js +7 -7
  25. package/dist/components/inputs/date-time/date-range-picker/index.js +7 -7
  26. package/dist/components/inputs/date-time/duration-picker/index.js +7 -7
  27. package/dist/components/inputs/date-time/month-picker/index.js +7 -7
  28. package/dist/components/inputs/date-time/time-picker/index.js +7 -7
  29. package/dist/components/inputs/field/index.js +2 -2
  30. package/dist/components/inputs/file-dropzone/index.js +3 -3
  31. package/dist/components/inputs/search/index.js +6 -6
  32. package/dist/components/inputs/select/index.js +2 -2
  33. package/dist/components/navigation/pagination/index.js +5 -5
  34. package/dist/components/navigation/sidebar/index.js +7 -7
  35. package/dist/components/overlays/alert-dialog/index.js +5 -5
  36. package/dist/components/overlays/command/index.js +5 -5
  37. package/dist/components/overlays/context-menu/index.js +3 -3
  38. package/dist/components/overlays/dialog/index.js +4 -4
  39. package/dist/components/overlays/dropdown/index.js +2 -2
  40. package/dist/components/overlays/popover/index.js +2 -2
  41. package/dist/components/overlays/sheet/index.js +5 -5
  42. package/dist/components/overlays/toast/index.js +4 -4
  43. package/dist/components/overlays/tooltip/index.js +2 -2
  44. package/dist/index.d.ts +102 -100
  45. package/dist/index.js +31 -31
  46. package/dist/lib/slot.d.ts +8 -1
  47. package/dist/lib/slot.js +3 -1
  48. package/dist/preset.d.ts +4 -0
  49. package/dist/preset.js +1 -0
  50. package/dist/primitives/morph/index.js +3 -3
  51. package/dist/primitives/motion/index.js +3 -3
  52. package/dist/shared/{components-09gadh7e.js → components-030n1m1v.js} +6 -4
  53. package/dist/shared/{components-8hb3r1fv.js → components-0rpek9d8.js} +2 -2
  54. package/dist/shared/{components-w0x0ne38.js → components-1edyddfg.js} +1 -1
  55. package/dist/shared/{components-cr081ppn.js → components-3qssegme.js} +2 -2
  56. package/dist/shared/{components-xmepz8d9.js → components-43brg0gv.js} +2 -2
  57. package/dist/shared/{components-58rjf115.js → components-47226105.js} +4 -4
  58. package/dist/shared/{components-7d4r7hh1.js → components-4swzve2d.js} +3 -3
  59. package/dist/shared/{components-f0xfpkhh.js → components-4vfm04wq.js} +2 -2
  60. package/dist/shared/{components-n9q8dcyf.js → components-59my5pwr.js} +1 -1
  61. package/dist/shared/{components-1dwmqj3g.js → components-621xezcz.js} +1 -1
  62. package/dist/shared/{components-n1e28m0k.js → components-96sd8h9m.js} +6 -3
  63. package/dist/shared/{components-cfn30fa8.js → components-9n43qf8y.js} +3 -3
  64. package/dist/shared/{components-x2h0zwad.js → components-aj7f4v4v.js} +1 -1
  65. package/dist/shared/{components-9gx18g9j.js → components-b92nqk44.js} +5 -3
  66. package/dist/shared/{components-jmft19kz.js → components-bwy8775y.js} +2 -2
  67. package/dist/shared/{components-n5cnn0s3.js → components-c6cz9nw6.js} +1 -1
  68. package/dist/shared/{components-6kqksbjb.js → components-c96k4gfs.js} +1 -1
  69. package/dist/shared/{components-p9mzwfbt.js → components-d7nhksd0.js} +10 -4
  70. package/dist/shared/{components-waz4pq58.js → components-dg6s5gjg.js} +1 -1
  71. package/dist/shared/{components-48rp8zeh.js → components-ep3d57kp.js} +2 -2
  72. package/dist/shared/{components-j4rs3qka.js → components-hpe3avtw.js} +1 -1
  73. package/dist/shared/{components-6b1gamqy.js → components-hx4a4cvm.js} +4 -2
  74. package/dist/shared/{components-tknms5b7.js → components-khcnzj3g.js} +7 -4
  75. package/dist/shared/{components-0d6zzjjz.js → components-mrbaxt8q.js} +5 -1
  76. package/dist/shared/{components-gqkjcw7c.js → components-n155gkhr.js} +3 -3
  77. package/dist/shared/{components-f1f0qvxg.js → components-nd1baxp1.js} +1 -1
  78. package/dist/shared/{components-fz1pnkzq.js → components-q4z71n18.js} +1 -1
  79. package/dist/shared/{components-b1f2bjf1.js → components-t7c6c4t4.js} +1 -1
  80. package/dist/shared/{components-8yqkpbqa.js → components-vbh98t9d.js} +1 -1
  81. package/dist/shared/{components-rz7wh3jx.js → components-vqqapw72.js} +5 -3
  82. package/dist/shared/{components-dm2ed0sg.js → components-zj640a90.js} +1 -1
  83. package/dist/styles.css +46 -11
  84. package/dist/tailwind.css +2 -0
  85. package/dist/tokens/colors.css +224 -0
  86. package/dist/tokens/motion.css +40 -0
  87. package/dist/tokens/radius.css +10 -0
  88. package/dist/tokens/spacing.css +9 -0
  89. package/dist/tokens/typography.css +119 -0
  90. package/dist/tokens.css +27 -0
  91. package/lib/slot.ts +14 -2
  92. package/package.json +27 -1
  93. package/preset.ts +2 -0
package/DESIGN.md CHANGED
@@ -349,12 +349,12 @@ fun; the only overshoot in the system is the slow tier's 0.12 and the goo's 0.15
349
349
  Two text colors, a greyscale ground, one blue.
350
350
 
351
351
  - **Foreground (`text-foreground`)** — near-black ink in light, near-white in dark. Headings, labels,
352
- values, selected items, the primary button's fill.
352
+ values, selected items, the primary button's fill (until `--primary` is re-pointed).
353
353
  - **Muted foreground (`text-muted-foreground`)** — the only secondary text color: descriptions, captions,
354
354
  placeholders, idle icons, idle tab labels. Never an opacity step of foreground (`text-foreground/60`
355
355
  is wrong); a third text tone does not exist.
356
356
  - **Background (`bg-background`)** — the page, equal to surface 1. Also the text color on the primary
357
- button and the tooltip (`text-background`).
357
+ button (until `--primary-foreground` is re-pointed) and the tooltip (`text-background`).
358
358
  - **Surfaces (`bg-surface-1` … `bg-surface-8`)** — the elevation ladder (see Elevation & Depth). Prefer a
359
359
  surface level over `card`/`background` for anything that floats or nests.
360
360
  - **Muted (`bg-muted`)** — a recessed track: the segmented tabs' well, a sunken field at rest.
@@ -367,6 +367,15 @@ Two text colors, a greyscale ground, one blue.
367
367
  forced scopes. Hairlines only, 1px.
368
368
  - **Brand (`bg-brand`, `ring-focus-ring`)** — `focus-ring` is `brand`: every keyboard ring is a 1px blue
369
369
  ring 2px outside the element. The single theming point for focus.
370
+ - **Primary (`bg-primary`, `text-primary-foreground`)** — the primary Button's theme hooks, so a host can
371
+ recolour it without forking the component: `--primary` (the fill, default `foreground`), `--primary-hover`
372
+ and `--primary-active` (default: `--primary` mixed 10% / 20% toward `background`), `--primary-foreground`
373
+ (the label, default `background`) and `--shadow-button-primary` (an extra shadow list drawn on the fill
374
+ layer, after its press ring: a lit edge, an outline, a drop; default the invisible `0 0 #0000`). All
375
+ five default to the ink look, so nothing moves until one is overridden. Re-point them on
376
+ `:root, .light, .dark` (zero-specificity defaults, so any stylesheet order wins), and set the hover and
377
+ active steps with the fill, since their defaults mix from the `--primary` they are declared with.
378
+ Other ink fills (checkbox, switch, progress, …) still read `foreground`.
370
379
  - **Destructive (`text-destructive`, `bg-destructive-light`)** — errors and destructive confirms only.
371
380
  - **Tinted actions (`text-destructive-strong` on `bg-destructive-wash`, `text-success-strong` on
372
381
  `bg-success-wash`)** — the destructive and success buttons: the hue as text, dark enough for AA on its
@@ -537,7 +546,7 @@ The signature behaviors:
537
546
  Each item lives at `<tier>/[<group>/]<name>/index.tsx` and composes from Base UI (behavior) plus the
538
547
  shared systems above (look and motion). Ready-made patterns to reach for before writing anything:
539
548
 
540
- - **Buttons** — `primary` (ink fill), `secondary` (accent fill), `tertiary` (transparent, a 1px
549
+ - **Buttons** — `primary` (ink fill by default, the `--primary` theme hooks), `secondary` (accent fill), `tertiary` (transparent, a 1px
541
550
  `border` ring that moves inward on press), `ghost` (transparent, muted text that turns foreground over
542
551
  the hover overlay). A component token with no `backgroundColor` is transparent over its substrate.
543
552
  Sizes `default` 36 / `compact` 28 / `icon` / `icon-compact`. Icons sit 4px closer to their edge than
package/README.md CHANGED
@@ -75,7 +75,9 @@ import 'virtual:uno.css'
75
75
  import '@justanarthur/modo-ui/base.css'
76
76
  ```
77
77
 
78
- `tokens.css` holds the CSS variables alone.
78
+ `tokens.css` holds the CSS variables alone. Each token file also ships on its
79
+ own, unchanged, as `@justanarthur/modo-ui/tokens/<group>.css` (`colors`, `motion`,
80
+ `radius`, `spacing`, `typography`), for a host whose modo viewer reads `tokens/*.css`.
79
81
 
80
82
  ### Theme
81
83
 
@@ -133,13 +135,13 @@ positioning) are ported.
133
135
  |---|---|
134
136
  | `AGENTS.md` | The ui-specific guide for coding agents (where things live, the rules that matter most, the checks); `CLAUDE.md` imports it. |
135
137
  | `DESIGN.md` | The design language in Google's [DESIGN.md](https://github.com/google-labs-code/design.md) format: the tokens as front matter, the rules as prose. Agents building here follow the `modo-ui` skill (`.claude/skills/modo-ui` at the repo root). |
136
- | `tokens/*.css` | Colors, typography (`--font-sans`, `--text-*`), motion (`--duration-*`), radius and spacing. |
137
- | `base.css` | Theme switching, base styles, focus fallback, scrollbars, shimmer/spinner keyframes and `.scroll-fade`. Shipped in the package. |
138
+ | `tokens/*.css` | Colors, typography (`--font-sans`, `--text-*`), motion (`--duration-*`), radius and spacing. Shipped per group as `dist/tokens/*.css` (`./tokens/*`). |
139
+ | `base.css` | Theme switching, base styles, focus fallback, scrollbars, shimmer/spinner keyframes and `.scroll-fade`. Shipped in the package. The inline `code` chip applies only inside the docs shell, `data-modo="prose"` and `.modo-prose`. |
138
140
  | `global.css` | The docs site's stylesheet (`modo.config.ts` `css`): imports `base.css`, then the modo-only focus revert and the docs chrome. |
139
141
  | `preset.ts` | The UnoCSS preset (theme and token utilities), shipped as `@justanarthur/modo-ui/unocss`. |
140
142
  | `uno.config.ts`, `vite.ts` | The UnoCSS setup of the docs site and the prebuilt CSS (see below). |
141
143
  | `index.ts`, `bunup.config.ts`, `scripts/build-css.ts` | The package build: `bun run build` writes the components, their types and the stylesheets to `dist/`. The docs site builds with `bun run build:docs` into `docs-dist/`. |
142
- | `scripts/exports.ts`, `scripts/use-client.ts`, `scripts/check-singletons.ts` | Packaging around that build. `exports.ts` (first step of `bun run build`) writes one `./lib/<name>` export per `lib/` module into `package.json`, plus `./components/*/variants` and `./blocks/*/variants` for the items' server-callable `variants.ts` entries; commit the `package.json` it leaves. `use-client.ts` (after bunup) puts `"use client"` on the emitted modules that render, call a hook or create a context, or use a binding from one; pure helpers (`cn`, class tables, springs, `variants.ts`) and re-export entries stay directive-free so a React Server Component can call them (`--list` prints the split). `check-singletons.ts` (`bun run check:dist`) checks that every React context and the `toast()` manager exist once in `dist/`, and that every entry has its `.d.ts`. |
144
+ | `scripts/exports.ts`, `scripts/use-client.ts`, `scripts/check-singletons.ts` | Packaging around that build. `exports.ts` (first step of `bun run build`) writes one `./lib/<name>` export per `lib/` module into `package.json`, plus `./components/*/variants` and `./blocks/*/variants` for the items' server-callable `variants.ts` entries; commit the `package.json` it leaves. `use-client.ts` (after bunup) puts `"use client"` on the emitted modules that render, call a hook or create a context, or use a binding from one; pure helpers (`cn`, class tables, springs, `variants.ts`) and re-export entries stay directive-free so a React Server Component can call them (`--list` prints the split). `check-singletons.ts` (`bun run check:dist`) checks that every React context and the `toast()` manager exist once in `dist/`, and that every entry has its `.d.ts`. `typesVersions` in `package.json` maps every public subpath to `dist/**/*.d.ts`, so `moduleResolution: node` consumers do not type-check the shipped source. |
143
145
  | `lib/` | FF's shared system files (upstream `lib/`, `hooks/` and the fluid-hover highlight, flat, upstream names), imported relatively by items. modo only scans the tier folders, so it never mistakes them for items. |
144
146
  | `modo.components.tsx` | Components only the docs site uses: the `Icon`, `Select` and `DocsNav` shell-slot adapters and the `ThemeSwitcher` panel item, picked in `modo.config.ts` by export name (`./modo.components.tsx#Select`). |
145
147
  | `primitives/<id>/` | FF's "System" pages: `index.tsx` (component + TSDoc prose) and `examples.mdx`, pulled in by `@example {@include ./examples.mdx}`. |
@@ -178,7 +180,7 @@ positioning) are ported.
178
180
  | — (new) | `lib/inert.ts` | Local addition: `inert` that holds on React 18 and 19 (18 passes it through as an unknown attribute and needs `''`, 19 reads a boolean and drops `''`): `inertProps(on)` to spread, `markInert` as a ref for an element that is always inert; Sidebar's folded and collapsed parts, Tabs' ghost label |
179
181
  | — (new) | `lib/focus-ring.tsx` | Local addition: `FocusRing`, the one keyboard ring a list panel draws, 2px outside the focused row and springing between rows (a snap under reduced motion); radius from the surrounding shape, or pinned with `shape` (popups pin `rounded`); Dropdown and its submenus, NavigationMenu, Select |
180
182
  | — (new) | `lib/scroll-column.tsx` | Local addition: `ScrollColumn`, the scrolling option list beside a calendar (months, years, hours, minutes) or a strip above it: the pick gets a dot, a run of `inView` options one band, the followed option stays centred; fluid hover, listbox semantics, arrow keys; `dividedColumns` puts a hairline between neighbours; `layout="grid"` (`columns` 2–6, 2-D fluid hover, row-wise arrow keys) after utegsk/ui's 0.14 grid picker |
181
- | — (new) | `lib/slot.ts` | Local addition: Dialog's `render` / `asChild` slot shape (`SlotProps`, `slotRender`), shared by every overlay part that wraps a control; `children` fill a `render` element that has none of its own (Base UI semantics), and `nativeButton` reaches the Base UI triggers and closes (`false` for a `<div>` or link); `cloneSlot` / `mergeSlotProps` / `composeRefs` are Radix Slot's merge (child props win, `on*` handlers chain child-first, `className` joins, `style` merges, refs compose; React 18 `forwardRef` and React 19 `props.ref`) |
183
+ | — (new) | `lib/slot.ts` | Local addition: Dialog's `render` / `asChild` slot shape (`SlotProps`, `slotRender`), shared by every overlay part that wraps a control; `children` fill a `render` element that has none of its own (Base UI semantics), and `nativeButton` reaches the Base UI triggers and closes (`false` for a `<div>` or link); `cloneSlot` / `mergeSlotProps` / `composeRefs` are Radix Slot's merge (child props win, `on*` handlers chain child-first, `className` joins, `style` merges, refs compose; React 18 `forwardRef` and React 19 `props.ref`); `slotId(el, id)` hands a slot element's own `id` to the Base UI part, so triggers and Field.Control register the id the DOM carries |
182
184
  | — (new) | `lib/morph-part.tsx` | Local addition: `MorphPart` (shared parts over a motion `layoutId`, after motion-primitives' morphing dialog) and the scope contexts overlays provide |
183
185
  | — (new) | `lib/goo-indicator.tsx` | Local addition: `GooIndicator` (melts between items, a lingering blob follows it) and `GooLayer` (goo over the shapes' coverage, filled with their own token, on only while moving), on the engine's `GOO_MATRIX` / `GOO_BLUR_RATIO` |
184
186
  | — (new) | `lib/morph-layers.tsx` | Local addition: `MorphSurface`, the renderer (shapes layer with beUI's goo filter plus a `--shadow-color` hairline around the melted shape, surface background and shadow, clipped content) |
@@ -187,7 +189,7 @@ positioning) are ported.
187
189
 
188
190
  | Upstream | Local |
189
191
  |---|---|
190
- | `app/globals.css` §1 tokens | `tokens/colors.css`, with HEX converted to oklch. The surface ladder and shadows live in `primitives/surface/surface.css`. Local addition: the `--status-*` roles (toast states), by reference to the badge palette, `--destructive` and `--brand`. |
192
+ | `app/globals.css` §1 tokens | `tokens/colors.css`, with HEX converted to oklch. The surface ladder and shadows live in `primitives/surface/surface.css`. Local addition: the `--status-*` roles (toast states), by reference to the badge palette, `--destructive` and `--brand`; and the primary-action hooks `--primary`, `--primary-hover`, `--primary-active`, `--primary-foreground`, `--shadow-button-primary` (defaults: the ink, its 90% / 80% mixes toward the background, the background, a no-op shadow), read by Button's `primary` variant and by `bg-primary` / `text-primary-foreground`. |
191
193
  | `app/globals.css` §2 theme switching | `base.css` (`color-scheme`, `--overlay`) and `surface.css` (`--shadow-N` per scheme) |
192
194
  | `app/globals.css` §3 `@theme inline` | `preset.ts` `theme` |
193
195
  | `app/globals.css` `--fs-*` + `@utility text-*` | `tokens/typography.css` `--text-*` and `preset.ts` `utilities` |
@@ -232,11 +234,11 @@ only what is specific to it:
232
234
 
233
235
  | Item | Upstream | Local | Specific modifications |
234
236
  |---|---|---|---|
235
- | `Accordion` | `registry/base/accordion.tsx` | `components/accordion/index.tsx` | open state runs through `useControllableState` (see [Behavior additions](#behavior-additions)); standalone `Accordion` accepts `highlight` and passes it to its items as their default; the panel morphs out of its row (a shape grows from the press point into the item, content clipped by it, layout height on its bottom edge; the `"item"` tint is that shape melted into the row through `GooFilter`), `effect` / `tier` on `Accordion.Content`; the group's hover highlight and focus ring snap with reflowed rows; statics `.Group/.Item/.Trigger/.Content`; local Trigger `icon` / `weightShift`, Content `contentClassName` |
237
+ | `Accordion` | `registry/base/accordion.tsx` | `components/accordion/index.tsx` | open state runs through `useControllableState` (see [Behavior additions](#behavior-additions)); standalone `Accordion` accepts `highlight` and passes it to its items as their default; the panel morphs out of its row (a shape grows from the press point into the item, content clipped by it, layout height on its bottom edge; the `"item"` tint is that shape melted into the row through `GooFilter`), `effect` / `tier` on `Accordion.Content`; the group's hover highlight and focus ring snap with reflowed rows; statics `.Group/.Item/.Trigger/.Content`; local Trigger `icon` / `weightShift`, Content `contentClassName`; `Accordion.Trigger` takes `render` / `asChild` (`lib/slot.ts`) |
236
238
  | `AlertDialog` | — (local) | `components/overlays/alert-dialog/index.tsx` (+ `pending-demo.tsx`) | Base UI AlertDialog root (always modal, no outside-press dismissal) over Dialog's own parts and `DialogState`, so it morphs like Dialog; `AlertDialog.Content` drops the ✕ by default; statics as Dialog's; `AlertDialog.Action` (promise-aware confirm: Button `loading`, every close blocked via `details.cancel()` while pending, closes on resolve, `onError` on reject; a controlled `pending` makes a plain `onClick` close once `pending` goes true and back to false) and `AlertDialog.Cancel` (Close as a ghost Button, disabled while pending); `pending-demo.tsx` is the docs stage; `onOpenChange(open, details)` with Base UI's change details (an action's own close reports `'imperative-action'`) |
237
239
  | `Avatar` | — (local) | `components/avatar/index.tsx` | Base UI Avatar (image loading status, the fallback's `delay`); one control tall (36 / 28px compact), `shape` circle or square; a 1px inset image edge after FF's media outline (`outline-image-edge`); the image fades in on the fast tier; statics `.Image/.Fallback` |
238
240
  | `Badge` | `registry/default/badge.tsx` | `components/badge/index.tsx` | `variant` re-declared (it came from cva's `VariantProps` upstream); changes animate: a new label or variant springs the width (inner content span, `spring.moderate`) while the label blur-crossfades, a new color fades, the dot scales in and out; "Changes" example over a local `change-demo.tsx` stage; `badgeColors`, `badgeVariants` and the size aliases moved to `variants.ts` (server-callable) and re-exported; local variant `outline` (the dot badge's neutral hairline without the dot; `color` has no effect) |
239
- | `Button` | `registry/base/button.tsx` | `components/button/index.tsx` | `variant`/`disabled`/`children` re-declared; extra Sizes / Active / As-child / Loading morph examples; `loading` folds the surface into a circle around the spinner and back (`spring.moderate`, box unchanged), label and spinner crossfade, `aria-busy`, the disabled dim fades; "Loading morph" example over a local `loading-demo.tsx` stage; also serves modo's chrome Button (`shell.Button`); `buttonVariants` and the size aliases moved to `variants.ts` (server-callable) and re-exported; local variants for shadcn-style hosts: `outline` (renders as `tertiary`), `destructive` / `success` (their `-strong` hue as text on a translucent wash that deepens on hover and press, never a solid fill), `link` (underlined foreground text on the control height); size `xs` → `compact`; `buttonVariants` resolves the size aliases and takes `surface` (fill/ring/wash/hover/press/`aria-disabled` as root classes, for an anchor host); `loading` keeps focus (`focusableWhenDisabled`; `asChild` sets `aria-disabled` and blocks clicks) |
241
+ | `Button` | `registry/base/button.tsx` | `components/button/index.tsx` | `variant`/`disabled`/`children` re-declared; extra Sizes / Active / As-child / Loading morph examples; `loading` folds the surface into a circle around the spinner and back (`spring.moderate`, box unchanged), label and spinner crossfade, `aria-busy`, the disabled dim fades; "Loading morph" example over a local `loading-demo.tsx` stage; also serves modo's chrome Button (`shell.Button`); `buttonVariants` and the size aliases moved to `variants.ts` (server-callable) and re-exported; local variants for shadcn-style hosts: `outline` (renders as `tertiary`), `destructive` / `success` (their `-strong` hue as text on a translucent wash that deepens on hover and press, never a solid fill), `link` (underlined foreground text on the control height); size `xs` → `compact`; the `primary` variant reads the `--primary*` / `--shadow-button-primary` hooks (the same ink look by default); `buttonVariants` resolves the size aliases and takes `surface` (fill/ring/wash/hover/press/`aria-disabled` as root classes, for an anchor host); `loading` keeps focus (`focusableWhenDisabled`; `asChild` sets `aria-disabled` and blocks clicks) |
240
242
  | `Calendar` | — (local, on react-day-picker) | `components/inputs/date-time/calendar/index.tsx` (+ `use-month-window.ts`) | DayPicker props pass through (as shadcn's Calendar); styled through `classNames` and custom `Month` / `Day` / `DayButton` (no react-day-picker stylesheet); one scrolling list of months rendered as a 25-month window that moves as you scroll; a pinned weekday row, muted weekends, today's dot; FF's fills (the pick `bg-foreground text-background` like the primary button, a range `bg-active`, its preview `bg-hover`); month and year columns that mark the month being read and jump on click, before or after the days (`columnsSide`), turning into strips when narrow; `extraColumns` / `extraColumnsSide` for the date picker's time; `columnLabels`; a named `CalendarProps` and a `ReactElement` return; the month window's edge margin is capped, so a list with no height of its own no longer loops |
241
243
  | `Card` | `registry/default/card.tsx` | `components/card/index.tsx` | `next/link` → plain anchors (stretched card link, `CardButton`'s `href` form); statics `.Group/.Header/.Title/.Description/.Action/.Content/.Footer/.Media/.Image/.Eyebrow/.Feature/.Button/.Divider`; local `border="outlined"` on a standalone card (the outlined group's hairline frame; inside a group the group decides); local `Card.Divider` (a full-bleed hairline between the card's parts, vertical in an inline card; the Separator primitive); local Title `as`, Content `bodyText` |
242
244
  | `Checkbox` (upstream `CheckboxItem`), `Checkbox.Group` (upstream `CheckboxGroup`) | `registry/base/checkbox-group.tsx` | `components/inputs/checkbox/index.tsx` | renamed; the row stands alone too (own state from `defaultChecked`, `onCheckedChange`, own hover and focus ring, `size`); rows numbered by the group (`lib/row-index.tsx`), so `index` is optional; uncontrolled mode + group-level callback; item `checked`/`onToggle` optional, falling back to group context; arrow-key focus guarded for `noUncheckedIndexedAccess`; shared `useSelectionRuns`, picks melt and split through the goo; `label` optional, any node (named via `aria-labelledby`); without a label and outside a group the checkbox is a bare box, Base UI's Checkbox.Root itself (`id` / `<label htmlFor>`, `aria-label`, `name` / `value` / `required` for forms), keeping its border when checked; `indeterminate` / `checked="indeterminate"` shows the icon set's `minus`, swapped with the self-drawing check in one cell; `disabled` dims a row and takes it out of the group's hover and arrow keys |
@@ -1,4 +1,5 @@
1
1
  import Accordion from './index'
2
+ import Button from '../button'
2
3
  import { ArrowRight } from 'lucide-react'
3
4
 
4
5
  # Standalone
@@ -201,3 +202,26 @@ By default the trigger renders its label twice: a hidden semibold copy reserves
201
202
  </Accordion.Content>
202
203
  </Accordion.Item>
203
204
  </Accordion.Group>
205
+
206
+ # Your own trigger
207
+
208
+ `asChild` (or `render`) makes your element the trigger, so a Button, a link-styled control or anything else can open the item. It is rendered as it is: the row's label copy, the chevron and the row padding are not added, so the element brings its own content and look. Base UI still wires `aria-expanded`, `aria-controls`, `data-panel-open` and the keys onto it, so style the open state with `data-[panel-open]`.
209
+
210
+ <Accordion.Group type="single" collapsible defaultValue="one">
211
+ <Accordion.Item value="one" index={0}>
212
+ <Accordion.Trigger asChild>
213
+ <Button variant="ghost" className="w-full justify-start">Billing</Button>
214
+ </Accordion.Trigger>
215
+ <Accordion.Content>
216
+ Invoices, payment methods and receipts.
217
+ </Accordion.Content>
218
+ </Accordion.Item>
219
+ <Accordion.Item value="two" index={1}>
220
+ <Accordion.Trigger render={<Button variant="ghost" className="w-full justify-start" />}>
221
+ Security
222
+ </Accordion.Trigger>
223
+ <Accordion.Content>
224
+ Passwords, sessions and two-factor.
225
+ </Accordion.Content>
226
+ </Accordion.Item>
227
+ </Accordion.Group>
@@ -39,6 +39,9 @@
39
39
  * focus ring snap with rows a morph pushes around (instead of springing
40
40
  * after them over the content) until the pointer or focus moves. `effect`
41
41
  * and `tier` on `AccordionContent`; reduced motion snaps, as before.
42
+ * - `AccordionTrigger` takes `render` / `asChild` (`lib/slot.ts`, local): the element is
43
+ * Base UI's Trigger as it is, inside the same row and header, with no row look, label
44
+ * copy or chevron. Its own `id` is the one Base UI registers (`slotId`).
42
45
  */
43
46
 
44
47
  import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
@@ -70,6 +73,7 @@ import { GooFilter } from '../../lib/morph-layers'
70
73
  import { useReduceMotion } from '../../lib/reduced-motion'
71
74
  import { useShape } from '../../lib/shape-context'
72
75
  import { type SizeVariant, useSize } from '../../lib/size-context'
76
+ import { type SlotProps, slotId, slotRender } from '../../lib/slot'
73
77
  import { spring } from '../../lib/springs'
74
78
  import { useControllableState } from '../../lib/use-controllable-state'
75
79
  import { useFluidHover, useRegisterFluidHoverItem } from '../../lib/use-fluid-hover'
@@ -515,7 +519,9 @@ type AccordionComponent = ForwardRefExoticComponent<AccordionProps & RefAttribut
515
519
  * Statics: Accordion.Group (the grouped root: type, collapsible,
516
520
  * defaultValue, value, onValueChange, highlight, size), Accordion.Item (value,
517
521
  * index, disabled), Accordion.Trigger (the row that toggles its item: `icon`
518
- * swaps or hides the chevron, `weightShift={false}` renders a rich label once)
522
+ * swaps or hides the chevron, `weightShift={false}` renders a rich label once,
523
+ * `render` / `asChild` make your own element the trigger, unstyled and without
524
+ * chevron or label copy)
519
525
  * and Accordion.Content (the collapsible panel: `effect` — `'goo'` or a plain
520
526
  * `'morph'` — the spring `tier`, `'slow'` or `'moderate'`, and
521
527
  * `contentClassName` for the inner row).
@@ -668,9 +674,9 @@ AccordionItem.displayName = 'AccordionItem'
668
674
 
669
675
  // ─── AccordionTrigger ────────────────────────────────────────────────────────
670
676
 
671
- interface AccordionTriggerProps extends HTMLAttributes<HTMLButtonElement> {
672
- /** Trigger label content. */
673
- children: ReactNode
677
+ interface AccordionTriggerProps extends Omit<SlotProps, 'children'> {
678
+ /** Trigger label content. With `asChild`, the single child element that becomes the trigger instead, and `render` takes an element the same way: that element is rendered as it is, the whole trigger, look included. It gets Base UI's `aria-expanded`, `aria-controls`, `data-panel-open` and key handling, and sits in the same row, but the label copy, `weightShift`, `icon` and chevron are not rendered (give it its own content), nor are the trigger's padding and ring. */
679
+ children?: ReactNode
674
680
  /** The glyph at the row's end, which turns a quarter turn when the item opens (so give it a right-pointing one), or `false` for none. Defaults to the icon context's `chevron-right`. */
675
681
  icon?: IconComponent | false
676
682
  /** Whether the label gets heavier while the item is open. On, the label renders twice in one grid cell (a hidden semibold copy reserves its width, so nothing reflows); off, it renders once and keeps whatever weight its content has, which suits rich content such as a whole `Card.Header` that would otherwise be duplicated in the DOM. The label sits inside the trigger `<button>`, so give it phrasing content (spans), not block elements such as a `div`-based header. Rich content that sets its own text colour does not change on open: only the chevron, the highlight and the open tint signal the state. Defaults to `true`. */
@@ -678,7 +684,8 @@ interface AccordionTriggerProps extends HTMLAttributes<HTMLButtonElement> {
678
684
  }
679
685
 
680
686
  const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
681
- ({ children, className, icon, weightShift = true, ...props }, ref) => {
687
+ ({ children, className, icon, weightShift = true, render, asChild, nativeButton, ...props }, ref) => {
688
+ const el = slotRender(render, asChild, children)
682
689
  const ChevronRight = useIcon('chevron-right')
683
690
  const Glyph = icon === false ? null : (icon ?? ChevronRight)
684
691
  const groupCtx = useAccordionGroup()
@@ -703,15 +710,21 @@ const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
703
710
  <AccordionPrimitive.Header render={<div />}>
704
711
  <AccordionPrimitive.Trigger
705
712
  ref={ref as React.Ref<HTMLElement>}
706
- className={cn(
707
- `relative z-10 flex items-center ${sizeClasses.gap} ${shape.item} ${sizeClasses.px} ${sizeClasses.variant === 'compact' ? 'py-1' : 'py-2'} w-full cursor-pointer outline-none select-none`,
708
- !groupCtx?.grouped && 'focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:ring-offset-0',
709
- className,
710
- )}
713
+ // A `render` / `asChild` element is the whole trigger: its own look, no row padding or ring.
714
+ className={
715
+ el
716
+ ? className
717
+ : cn(
718
+ `relative z-10 flex items-center ${sizeClasses.gap} ${shape.item} ${sizeClasses.px} ${sizeClasses.variant === 'compact' ? 'py-1' : 'py-2'} w-full cursor-pointer outline-none select-none`,
719
+ !groupCtx?.grouped &&
720
+ 'focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:ring-offset-0',
721
+ className,
722
+ )
723
+ }
711
724
  {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
725
+ {...(el ? { render: el, nativeButton, id: slotId(el, props.id) } : {})}
712
726
  >
713
- {/* Label with dual-layer text */}
714
- {weightShift ? (
727
+ {el ? undefined : weightShift ? (
715
728
  <span className={cn('inline-grid flex-1 text-left', sizeClasses.text)}>
716
729
  <span className="col-start-1 row-start-1 invisible weight-semibold" aria-hidden="true">
717
730
  {children}
@@ -739,7 +752,7 @@ const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
739
752
  )}
740
753
 
741
754
  {/* Chevron */}
742
- {Glyph && (
755
+ {!el && Glyph && (
743
756
  <motion.span
744
757
  className="shrink-0 inline-flex items-center justify-center"
745
758
  animate={{ rotate: isOpen ? 90 : 0 }}
@@ -37,6 +37,12 @@
37
37
  * never a solid fill; `link` is foreground text on the control height with no padding,
38
38
  * whose always-drawn underline fades in on hover (and shows while `active`). Size `xs`
39
39
  * resolves to `compact`: the ladder has two steps.
40
+ * - The `primary` fill and label read the theme hooks `--primary`, `--primary-hover`,
41
+ * `--primary-active`, `--primary-foreground` and `--shadow-button-primary`
42
+ * (`tokens/colors.css`; `bg-primary` / `text-primary-foreground`) instead of the
43
+ * ink and `text-background`, so a host can recolour it. Defaults are the old values.
44
+ * The shadow hook is appended to the layer's own shadow (it sits on the fill layer:
45
+ * a shadow on the root would be covered by it). `variants.ts` mirrors it on the root.
40
46
  */
41
47
 
42
48
  import { Button as ButtonPrimitive } from '@base-ui/react/button'
@@ -94,10 +100,12 @@ interface ButtonProps
94
100
  Pressing collapses the spread, shrinking the surface by exactly 1px per
95
101
  side at any width — a scale would warp (2% of a 400px button is 8px
96
102
  sideways but under 1px vertically). Fill colors are opaque color-mix()es
97
- rather than alpha so the fill and its spread ring never seam. */
103
+ rather than alpha so the fill and its spread ring never seam. The primary
104
+ fill reads the `--primary*` hooks; `--shadow-button-primary` joins the
105
+ layer's shadow list after the ring, at rest and pressed. */
98
106
  const bgVariants: Record<string, string> = {
99
107
  primary:
100
- '[--btn-bg:var(--foreground)] group-hover:[--btn-bg:color-mix(in_oklab,var(--foreground)_90%,var(--background))] group-active:[--btn-bg:color-mix(in_oklab,var(--foreground)_80%,var(--background))] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
108
+ '[--btn-bg:var(--primary)] group-hover:[--btn-bg:var(--primary-hover)] group-active:[--btn-bg:var(--primary-active)] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg),var(--shadow-button-primary)] group-active:shadow-[0_0_0_0px_var(--btn-bg),var(--shadow-button-primary)]',
101
109
  secondary:
102
110
  '[--btn-bg:var(--accent)] group-hover:[--btn-bg:color-mix(in_oklab,var(--accent)_80%,var(--background))] group-active:[--btn-bg:var(--accent)] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
103
111
  // The border ring is an outer 1px shadow at rest that hands off to an
@@ -124,7 +132,7 @@ const bgVariants: Record<string, string> = {
124
132
  geometric press-collapse still reacts on top. */
125
133
  const activeBgVariants: Record<string, string> = {
126
134
  primary:
127
- '[--btn-bg:color-mix(in_oklab,var(--foreground)_80%,var(--background))] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
135
+ '[--btn-bg:var(--primary-active)] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg),var(--shadow-button-primary)] group-active:shadow-[0_0_0_0px_var(--btn-bg),var(--shadow-button-primary)]',
128
136
  secondary:
129
137
  '[--btn-bg:var(--accent)] bg-[var(--btn-bg)] shadow-[0_0_0_1px_var(--btn-bg)] group-active:shadow-[0_0_0_0px_var(--btn-bg)]',
130
138
  tertiary:
@@ -7,7 +7,9 @@
7
7
  * - The class recipe (`buttonVariants`) and the size aliases, split out of
8
8
  * `index.tsx` so server code can call them: no JSX, hooks or contexts here.
9
9
  * `index.tsx` re-exports them.
10
- * - Styling reads DS tokens (see `index.tsx`).
10
+ * - Styling reads DS tokens (see `index.tsx`). The `primary` variant reads the theme hooks
11
+ * `--primary*` and `--shadow-button-primary` (`tokens/colors.css`), also on the `surface`
12
+ * root (the shadow rides `shadow-[…]`, so it composes with the focus ring).
11
13
  * - Variants `outline` (an alias of `tertiary`), `destructive`, `success` and
12
14
  * `link`, and the `xs` size alias (→ `compact`), for hosts with a
13
15
  * shadcn-style API; see `index.tsx` for the look.
@@ -34,7 +36,7 @@ const SURFACED: SurfacedVariant[] = ['primary', 'secondary', 'tertiary', 'outlin
34
36
  (`index.tsx` bgVariants); plain `hover:` / `active:` instead of `group-*`. */
35
37
  const surfaceVariants: Record<SurfacedVariant, string> = {
36
38
  primary:
37
- '[--btn-bg:var(--foreground)] hover:[--btn-bg:color-mix(in_oklab,var(--foreground)_90%,var(--background))] active:[--btn-bg:color-mix(in_oklab,var(--foreground)_80%,var(--background))] bg-[var(--btn-bg)]',
39
+ '[--btn-bg:var(--primary)] hover:[--btn-bg:var(--primary-hover)] active:[--btn-bg:var(--primary-active)] bg-[var(--btn-bg)] shadow-[var(--shadow-button-primary)]',
38
40
  secondary:
39
41
  '[--btn-bg:var(--accent)] hover:[--btn-bg:color-mix(in_oklab,var(--accent)_80%,var(--background))] active:[--btn-bg:var(--accent)] bg-[var(--btn-bg)]',
40
42
  tertiary: 'shadow-[inset_0_0_0_1px_var(--border)] hover:bg-hover active:bg-active',
@@ -54,7 +56,7 @@ const buttonRecipe = cva(
54
56
  {
55
57
  variants: {
56
58
  variant: {
57
- primary: 'text-background',
59
+ primary: 'text-primary-foreground',
58
60
  secondary: 'text-foreground',
59
61
  tertiary: 'text-foreground',
60
62
  outline: 'text-foreground',
@@ -39,6 +39,7 @@ import {
39
39
  forwardRef,
40
40
  type HTMLAttributes,
41
41
  isValidElement,
42
+ type MouseEventHandler,
42
43
  type ReactElement,
43
44
  type ReactNode,
44
45
  type Ref,
@@ -235,8 +236,8 @@ CardGroup.displayName = 'CardGroup'
235
236
  interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick'> {
236
237
  /** Compositional parts: Card.Image, Card.Media, Card.Header, Card.Content, Card.Footer. */
237
238
  children?: ReactNode
238
- /** Makes the whole card a clickable target (stretched button); fluid hover previews it. */
239
- onClick?: () => void
239
+ /** Makes the whole card a clickable target (stretched button); fluid hover previews it. Receives the click event, or takes no argument. */
240
+ onClick?: MouseEventHandler<HTMLElement>
240
241
  /** Makes the whole card a link (stretched anchor). */
241
242
  href?: string
242
243
  /** Opens href in a new tab. Defaults to `false`. */
@@ -19,7 +19,7 @@ import {
19
19
  } from 'react'
20
20
  import { FIELD_TEXT_INSET, FieldStateContext, useFieldState } from '../../../lib/field-classes'
21
21
  import { type SizeVariant, useSize } from '../../../lib/size-context'
22
- import { slotRender } from '../../../lib/slot'
22
+ import { slotId, slotRender } from '../../../lib/slot'
23
23
  import { cn } from '../../../lib/utils'
24
24
  import { SizeProvider } from '../../../primitives/sizes'
25
25
  import { Label } from '../input'
@@ -143,7 +143,8 @@ interface FieldControlProps {
143
143
  }
144
144
 
145
145
  function FieldControl({ render, asChild, children }: FieldControlProps) {
146
- return <BaseField.Control render={slotRender(render, asChild, children)} />
146
+ const el = slotRender(render, asChild, children)
147
+ return <BaseField.Control render={el} id={slotId(el)} />
147
148
  }
148
149
 
149
150
  interface ProvideFieldStateProps {
@@ -88,7 +88,7 @@ import { isDisabledRow, popupScrollAreaClass, popupViewportClass } from '../../.
88
88
  import { RowOrderContext, useRowOrder, useRowSlot } from '../../../lib/row-order'
89
89
  import { shapeMap, useShape } from '../../../lib/shape-context'
90
90
  import { type SizeVariant, useSize } from '../../../lib/size-context'
91
- import { type SlotProps, slotRender } from '../../../lib/slot'
91
+ import { type SlotProps, slotId, slotRender } from '../../../lib/slot'
92
92
  import { spring } from '../../../lib/springs'
93
93
  import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
94
94
  import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
@@ -482,6 +482,7 @@ const SelectTrigger = forwardRef<HTMLButtonElement, SelectTriggerProps>(
482
482
  )
483
483
  }
484
484
  {...props}
485
+ id={slotId(el, props.id)}
485
486
  >
486
487
  <span className={cn('flex items-center min-w-0 flex-1', sizeClasses.gap)}>
487
488
  {Icon && (
@@ -63,7 +63,7 @@ import { MorphSurface } from '../../../lib/morph-layers'
63
63
  import { MorphPartScope } from '../../../lib/morph-part'
64
64
  import { useShape } from '../../../lib/shape-context'
65
65
  import { useSize, useSizeVariant } from '../../../lib/size-context'
66
- import { type SlotProps, slotRender } from '../../../lib/slot'
66
+ import { type SlotProps, slotId, slotRender } from '../../../lib/slot'
67
67
  import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
68
68
  import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
69
69
  import { useControllableState } from '../../../lib/use-controllable-state'
@@ -86,7 +86,7 @@ const useDialogState = () => useContext(DialogContext)
86
86
  const DialogTrigger = forwardRef<HTMLButtonElement, DialogSlotProps>(({ render, asChild, children, ...props }, ref) => {
87
87
  const el = slotRender(render, asChild, children)
88
88
  return el ? (
89
- <DialogPrimitive.Trigger ref={ref} render={el} {...props} />
89
+ <DialogPrimitive.Trigger ref={ref} render={el} {...props} id={slotId(el, props.id)} />
90
90
  ) : (
91
91
  <DialogPrimitive.Trigger ref={ref} {...props}>
92
92
  {children}
@@ -119,6 +119,7 @@ import { isDisabledRow, popupScrollAreaClass, popupViewportClass } from '../../.
119
119
  import { RowOrderContext, useRowOrder, useRowSlot } from '../../../lib/row-order'
120
120
  import { shapeMap } from '../../../lib/shape-context'
121
121
  import { type SizeVariant, useSize } from '../../../lib/size-context'
122
+ import { slotId } from '../../../lib/slot'
122
123
  import { spring } from '../../../lib/springs'
123
124
  import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
124
125
  import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
@@ -754,9 +755,14 @@ type DropdownTriggerProps = MenuTriggerProps & {
754
755
  const DropdownTrigger = forwardRef<HTMLButtonElement, DropdownTriggerProps>(
755
756
  ({ asChild, render, children, ...props }, ref) =>
756
757
  asChild && isValidElement(children) ? (
757
- <BaseMenu.Trigger ref={ref} render={children as ReactElement<Record<string, unknown>>} {...props} />
758
+ <BaseMenu.Trigger
759
+ ref={ref}
760
+ render={children as ReactElement<Record<string, unknown>>}
761
+ {...props}
762
+ id={slotId(children, props.id)}
763
+ />
758
764
  ) : (
759
- <BaseMenu.Trigger ref={ref} render={render} {...props}>
765
+ <BaseMenu.Trigger ref={ref} render={render} {...props} id={slotId(render, props.id)}>
760
766
  {children}
761
767
  </BaseMenu.Trigger>
762
768
  ),
@@ -25,7 +25,7 @@ import {
25
25
  } from 'react'
26
26
  import { MorphSurface } from '../../../lib/morph-layers'
27
27
  import { useShape } from '../../../lib/shape-context'
28
- import { type SlotProps, slotRender } from '../../../lib/slot'
28
+ import { type SlotProps, slotId, slotRender } from '../../../lib/slot'
29
29
  import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
30
30
  import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
31
31
  import { useControllableState } from '../../../lib/use-controllable-state'
@@ -52,7 +52,7 @@ const PopoverTrigger = forwardRef<HTMLButtonElement, PopoverTriggerProps>(
52
52
  ({ render, asChild, children, ...props }, ref) => {
53
53
  const el = slotRender(render, asChild, children)
54
54
  return el ? (
55
- <PopoverPrimitive.Trigger ref={ref} render={el} {...props} />
55
+ <PopoverPrimitive.Trigger ref={ref} render={el} {...props} id={slotId(el, props.id)} />
56
56
  ) : (
57
57
  <PopoverPrimitive.Trigger ref={ref} {...props}>
58
58
  {children}
@@ -58,7 +58,7 @@ import {
58
58
  } from 'react'
59
59
  import { MorphSurface } from '../../../lib/morph-layers'
60
60
  import { useShape } from '../../../lib/shape-context'
61
- import { type SlotProps, slotRender } from '../../../lib/slot'
61
+ import { type SlotProps, slotId, slotRender } from '../../../lib/slot'
62
62
  import { type MorphOrigin, useMorph, useMorphOrigin } from '../../../lib/use-morph'
63
63
  import { cn } from '../../../lib/utils'
64
64
 
@@ -263,7 +263,7 @@ const TooltipTrigger = forwardRef<HTMLButtonElement, TooltipTriggerProps>(
263
263
  // trigger inherits it from the provider.
264
264
  const shared = { ref, delay, closeDelay, onPointerMove: followCursor ? handleMove : onPointerMove, ...props }
265
265
  return el ? (
266
- <TooltipPrimitive.Trigger {...shared} render={el} />
266
+ <TooltipPrimitive.Trigger {...shared} render={el} id={slotId(el, props.id)} />
267
267
  ) : (
268
268
  <TooltipPrimitive.Trigger {...shared}>{children}</TooltipPrimitive.Trigger>
269
269
  )
package/dist/base.css CHANGED
@@ -162,6 +162,33 @@
162
162
  --syntax-sign: var(--muted-foreground);
163
163
  }
164
164
 
165
+ /* ── Primary action (theme hooks) ──
166
+ What the primary Button paints, as hooks a host re-points to recolour it
167
+ (a brand fill with a lit edge) without forking the component:
168
+ --primary the fill. Defaults to the ink.
169
+ --primary-hover its hover fill, 10% toward --background.
170
+ --primary-active its pressed fill, 20% toward --background.
171
+ --primary-foreground the label on the fill. Defaults to --background.
172
+ --shadow-button-primary an extra shadow list for the fill layer, drawn on
173
+ top of the layer's own press ring (a highlight,
174
+ an outline, a drop). The default is an invisible
175
+ no-op that stays valid inside a comma list.
176
+ All five default to what the primary Button always drew, so nothing
177
+ changes until one is overridden. Override --primary-hover and
178
+ --primary-active with it, since their defaults mix from the value
179
+ --primary has where they are declared. Re-point them on `:root, .light,
180
+ .dark` (or on `:root` alone when the value is the same in both schemes).
181
+ `bg-primary`, `text-primary` and `text-primary-foreground` read them.
182
+ Zero specificity (:where): a host's override wins from any stylesheet
183
+ order, `<html class="dark">` included. */
184
+ :where(:root, .light, .dark) {
185
+ --primary: var(--foreground);
186
+ --primary-hover: color-mix(in oklab, var(--primary) 90%, var(--background));
187
+ --primary-active: color-mix(in oklab, var(--primary) 80%, var(--background));
188
+ --primary-foreground: var(--background);
189
+ --shadow-button-primary: 0 0 #0000;
190
+ }
191
+
165
192
  /* ── Badge palette ──
166
193
  Badge's `color` hues (upstream hex, as exact oklch). Constant across
167
194
  schemes; the solid variant mixes each 15% into --background. */
@@ -402,6 +429,9 @@
402
429
  * :focus-visible fallback), html.transitioning, native scrollbars,
403
430
  * .scrollbar-hide, shimmer + spinner keyframes, .scroll-fade(-x).
404
431
  *
432
+ * Local scope change: the inline code chip applies inside the docs shell, a
433
+ * `data-modo="prose"` block or a `.modo-prose` container, not to every <code>.
434
+ *
405
435
  * Local value changes: the inline code chip and the shimmer stops read color
406
436
  * tokens instead of upstream's hex (light code chip `--muted`, dark
407
437
  * `--surface-4`; shimmer dim → bright runs `--muted-foreground` →
@@ -495,10 +525,11 @@ body {
495
525
  text-wrap: pretty;
496
526
  }
497
527
 
498
- /* Inline code chips: bare <code> in prose gets its own ground. Excludes
499
- code inside <pre>. Base layer, so a page can still override with
500
- utilities. */
501
- code:not(pre code) {
528
+ /* Inline code chips, for prose that opts in: the docs shell (`data-modo="app"`),
529
+ a modo prose block (`data-modo="prose"`) or a `.modo-prose` container. A bare
530
+ <code> in an app is left alone. Excludes code inside <pre>. Base layer, so a
531
+ page can still override with utilities. */
532
+ :where([data-modo="app"], [data-modo="prose"], .modo-prose) code:not(pre code) {
502
533
  background: light-dark(var(--muted), var(--surface-4));
503
534
  border-radius: 4px;
504
535
  padding: 1px 4px;
@@ -1,24 +1,24 @@
1
1
  import {
2
2
  SidebarApp2
3
- } from "../../shared/components-58rjf115.js";
4
- import"../../shared/components-cfn30fa8.js";
3
+ } from "../../shared/components-47226105.js";
4
+ import"../../shared/components-9n43qf8y.js";
5
5
  import"../../shared/components-kswxnm3h.js";
6
6
  import"../../shared/components-rrbpmfx4.js";
7
- import"../../shared/components-f1f0qvxg.js";
7
+ import"../../shared/components-nd1baxp1.js";
8
8
  import"../../shared/components-6zng858q.js";
9
9
  import"../../shared/components-124epb7f.js";
10
10
  import"../../shared/components-z3v0n0de.js";
11
- import"../../shared/components-tknms5b7.js";
11
+ import"../../shared/components-khcnzj3g.js";
12
12
  import"../../shared/components-8tt3xbb5.js";
13
13
  import"../../shared/components-tz4gaf1v.js";
14
14
  import"../../shared/components-8nwsqjn8.js";
15
15
  import"../../shared/components-9as3rf68.js";
16
- import"../../shared/components-rz7wh3jx.js";
16
+ import"../../shared/components-vqqapw72.js";
17
17
  import"../../shared/components-krtx3m5b.js";
18
- import"../../shared/components-09gadh7e.js";
18
+ import"../../shared/components-030n1m1v.js";
19
19
  import"../../shared/components-h6p86be3.js";
20
20
  import"../../shared/components-vf45j12c.js";
21
- import"../../shared/components-0d6zzjjz.js";
21
+ import"../../shared/components-mrbaxt8q.js";
22
22
  import"../../shared/components-4y816hwp.js";
23
23
  import"../../shared/components-b1e239g3.js";
24
24
  import"../../shared/components-dhgk6mbt.js";
@@ -28,8 +28,8 @@ import"../../shared/components-pw06rbxq.js";
28
28
  import"../../shared/components-yhgq0qxx.js";
29
29
  import"../../shared/components-awra5sff.js";
30
30
  import"../../shared/components-dcc02hs2.js";
31
- import"../../shared/components-7d4r7hh1.js";
32
- import"../../shared/components-f0xfpkhh.js";
31
+ import"../../shared/components-4swzve2d.js";
32
+ import"../../shared/components-4vfm04wq.js";
33
33
  import"../../shared/components-tz1fv35x.js";
34
34
  import"../../shared/components-2z48g4fp.js";
35
35
  import"../../shared/components-drxrtnp2.js";
@@ -2,10 +2,10 @@ import {
2
2
  FileThumbnail2,
3
3
  InputMessage2,
4
4
  input_message_default
5
- } from "../../../shared/components-jmft19kz.js";
6
- import"../../../shared/components-rz7wh3jx.js";
5
+ } from "../../../shared/components-bwy8775y.js";
6
+ import"../../../shared/components-vqqapw72.js";
7
7
  import"../../../shared/components-vf45j12c.js";
8
- import"../../../shared/components-0d6zzjjz.js";
8
+ import"../../../shared/components-mrbaxt8q.js";
9
9
  import"../../../shared/components-4y816hwp.js";
10
10
  import"../../../shared/components-b1e239g3.js";
11
11
  import"../../../shared/components-dhgk6mbt.js";
@@ -15,8 +15,8 @@ import"../../../shared/components-pw06rbxq.js";
15
15
  import"../../../shared/components-yhgq0qxx.js";
16
16
  import"../../../shared/components-awra5sff.js";
17
17
  import"../../../shared/components-dcc02hs2.js";
18
- import"../../../shared/components-7d4r7hh1.js";
19
- import"../../../shared/components-f0xfpkhh.js";
18
+ import"../../../shared/components-4swzve2d.js";
19
+ import"../../../shared/components-4vfm04wq.js";
20
20
  import"../../../shared/components-tz1fv35x.js";
21
21
  import"../../../shared/components-de99ykt7.js";
22
22
  import"../../../shared/components-3ey0hzpb.js";