@justanarthur/modo-ui 0.6.0-canary.2 → 0.6.0-canary.3
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 +19 -19
- package/components/accordion/examples.mdx +55 -0
- package/components/accordion/index.tsx +50 -26
- package/components/card/examples.mdx +33 -0
- package/components/card/index.tsx +41 -9
- package/components/copy/examples.mdx +32 -0
- package/components/copy/index.tsx +203 -23
- package/components/inputs/color-picker/examples.mdx +31 -0
- package/components/inputs/color-picker/index.tsx +32 -6
- package/components/inputs/combobox/examples.mdx +84 -1
- package/components/inputs/combobox/index.tsx +153 -62
- package/components/inputs/combobox/remote-demo.tsx +5 -1
- package/components/inputs/date-time/calendar/examples.mdx +11 -0
- package/components/inputs/date-time/calendar/use-month-window.ts +33 -23
- package/components/inputs/date-time/date-picker/examples.mdx +8 -0
- package/components/inputs/date-time/date-picker/masked-date-input.tsx +6 -3
- package/components/inputs/date-time/date-range-picker/examples.mdx +8 -0
- package/components/inputs/date-time/date-range-picker/index.tsx +4 -3
- package/components/inputs/date-time/picker-shell.tsx +6 -2
- package/components/inputs/file-dropzone/examples.mdx +8 -0
- package/components/inputs/file-dropzone/index.tsx +25 -5
- package/components/inputs/number-input/examples.mdx +19 -10
- package/components/inputs/number-input/index.tsx +12 -5
- package/components/inputs/select/examples.mdx +68 -0
- package/components/inputs/select/index.tsx +268 -87
- package/components/inputs/slider/examples.mdx +24 -0
- package/components/inputs/slider/index.tsx +236 -103
- package/components/inputs/textarea/examples.mdx +32 -0
- package/components/inputs/textarea/index.tsx +49 -3
- package/components/navigation/navigation-menu/examples.mdx +27 -0
- package/components/navigation/navigation-menu/index.tsx +128 -37
- package/components/overlays/dropdown/examples.mdx +143 -1
- package/components/overlays/dropdown/index.tsx +94 -28
- package/components/overlays/dropdown/menu-item.tsx +77 -31
- package/components/overlays/dropdown/menu-parts.tsx +78 -17
- package/components/table/examples.mdx +2 -2
- package/components/table/index.tsx +13 -3
- package/dist/blocks/sidebar-app/index.js +9 -9
- package/dist/components/AI/input-message/index.js +4 -4
- package/dist/components/accordion/index.d.ts +24 -9
- package/dist/components/accordion/index.js +1 -1
- package/dist/components/card/index.d.ts +14 -4
- package/dist/components/card/index.js +1 -1
- package/dist/components/copy/index.d.ts +41 -8
- package/dist/components/copy/index.js +4 -3
- package/dist/components/inputs/color-picker/index.d.ts +9 -1
- package/dist/components/inputs/color-picker/index.js +5 -5
- package/dist/components/inputs/combobox/index.d.ts +30 -8
- package/dist/components/inputs/combobox/index.js +2 -2
- package/dist/components/inputs/date-time/calendar/index.js +1 -1
- package/dist/components/inputs/date-time/date-picker/index.d.ts +3 -1
- package/dist/components/inputs/date-time/date-picker/index.js +8 -8
- package/dist/components/inputs/date-time/date-range-picker/index.d.ts +3 -1
- package/dist/components/inputs/date-time/date-range-picker/index.js +8 -8
- package/dist/components/inputs/date-time/duration-picker/index.d.ts +3 -1
- package/dist/components/inputs/date-time/duration-picker/index.js +6 -6
- package/dist/components/inputs/date-time/month-picker/index.d.ts +3 -1
- package/dist/components/inputs/date-time/month-picker/index.js +6 -6
- package/dist/components/inputs/date-time/time-picker/index.d.ts +3 -1
- package/dist/components/inputs/date-time/time-picker/index.js +6 -6
- package/dist/components/inputs/field/index.js +2 -2
- package/dist/components/inputs/file-dropzone/index.d.ts +4 -2
- package/dist/components/inputs/file-dropzone/index.js +1 -1
- package/dist/components/inputs/number-input/index.d.ts +6 -3
- package/dist/components/inputs/number-input/index.js +1 -1
- package/dist/components/inputs/search/index.js +5 -5
- package/dist/components/inputs/select/index.d.ts +68 -22
- package/dist/components/inputs/select/index.js +4 -3
- package/dist/components/inputs/slider/index.d.ts +7 -0
- package/dist/components/inputs/slider/index.js +1 -1
- package/dist/components/inputs/textarea/index.d.ts +16 -1
- package/dist/components/inputs/textarea/index.js +1 -1
- package/dist/components/navigation/navigation-menu/index.d.ts +27 -15
- package/dist/components/navigation/navigation-menu/index.js +2 -2
- package/dist/components/navigation/pagination/index.js +5 -4
- package/dist/components/navigation/sidebar/index.js +6 -6
- package/dist/components/overlays/alert-dialog/index.js +4 -4
- package/dist/components/overlays/command/index.js +4 -4
- package/dist/components/overlays/context-menu/index.d.ts +7 -1
- package/dist/components/overlays/context-menu/index.js +5 -4
- package/dist/components/overlays/dialog/index.js +3 -3
- package/dist/components/overlays/dropdown/index.d.ts +53 -9
- package/dist/components/overlays/dropdown/index.js +4 -3
- package/dist/components/overlays/popover/index.js +3 -3
- package/dist/components/overlays/preview-card/index.js +2 -2
- package/dist/components/overlays/sheet/index.js +4 -4
- package/dist/components/overlays/toast/index.js +3 -3
- package/dist/components/overlays/tooltip/index.js +2 -2
- package/dist/components/table/index.js +2 -2
- package/dist/index.d.ts +609 -408
- package/dist/index.js +39 -39
- package/dist/lib/row-order.js +1 -1
- package/dist/lib/slot.d.ts +15 -2
- package/dist/lib/slot.js +8 -2
- package/dist/primitives/morph/index.js +2 -2
- package/dist/shared/{components-mkkbbgb7.js → components-02c81nsv.js} +5 -5
- package/dist/shared/{components-5nsn4w8c.js → components-09gadh7e.js} +1 -1
- package/dist/shared/components-0d6zzjjz.js +58 -0
- package/dist/shared/{components-jaw36d5v.js → components-1dwmqj3g.js} +1 -1
- package/dist/shared/{components-5mkga9hj.js → components-35tmfjtw.js} +67 -25
- package/dist/shared/{components-hbk69y68.js → components-48rp8zeh.js} +1 -1
- package/dist/shared/{components-2mt9te6d.js → components-64c8kvmt.js} +59 -20
- package/dist/shared/{components-faww1hy7.js → components-6b1gamqy.js} +129 -40
- package/dist/shared/{components-h9vrr1j2.js → components-6mwvgrq9.js} +1 -1
- package/dist/shared/{components-ncdgtg6b.js → components-8hb3r1fv.js} +1 -1
- package/dist/shared/{components-ajhdkhjz.js → components-8yqkpbqa.js} +40 -18
- package/dist/shared/{components-vqpzqwcg.js → components-9gx18g9j.js} +1 -1
- package/dist/shared/{components-gjd5v54y.js → components-a4evvq8h.js} +4 -4
- package/dist/shared/{components-m1m46jdv.js → components-avrrksxk.js} +153 -57
- package/dist/shared/{components-q3pbax3p.js → components-cfn30fa8.js} +2 -2
- package/dist/shared/{components-j71xp9xx.js → components-cr081ppn.js} +1 -1
- package/dist/shared/{components-mc4d4296.js → components-daqy7ydp.js} +5 -4
- package/dist/shared/{components-7xew2ewk.js → components-dm2ed0sg.js} +1 -1
- package/dist/shared/{components-mjnzv5vj.js → components-e5jyw7g9.js} +3 -3
- package/dist/shared/{components-jamcmdsd.js → components-f1f0qvxg.js} +1 -1
- package/dist/shared/{components-nbvtmgme.js → components-fz1pnkzq.js} +143 -14
- package/dist/shared/{components-79q3xxwr.js → components-gqkjcw7c.js} +2 -2
- package/dist/shared/{components-5t2f1kq7.js → components-j4rs3qka.js} +1 -1
- package/dist/shared/{components-d6vf38m5.js → components-jkctvwz9.js} +127 -84
- package/dist/shared/{components-tarz14gk.js → components-jmft19kz.js} +1 -1
- package/dist/shared/{components-zbvjzh1p.js → components-k57dtvgp.js} +4 -2
- package/dist/shared/{components-4bwsd46w.js → components-krtx3m5b.js} +21 -8
- package/dist/shared/{components-q5h7v6rr.js → components-n1e28m0k.js} +1 -1
- package/dist/shared/{components-7ntvvegr.js → components-n5cnn0s3.js} +14 -2
- package/dist/shared/{components-s79c7tq3.js → components-n9q8dcyf.js} +6 -5
- package/dist/shared/{components-5s0nfqvw.js → components-nv4awn05.js} +32 -2
- package/dist/shared/{components-ae9fncz4.js → components-p9mzwfbt.js} +10 -6
- package/dist/shared/{components-0x6mx1p5.js → components-rz7wh3jx.js} +1 -1
- package/dist/shared/{components-30zcc9pe.js → components-w0x0ne38.js} +1 -1
- package/dist/shared/{components-k1b0vhcq.js → components-x2h0zwad.js} +1 -1
- package/dist/shared/{components-peqej82e.js → components-xmepz8d9.js} +3 -3
- package/dist/styles.css +25 -9
- package/lib/row-order.tsx +16 -5
- package/lib/slot.ts +71 -3
- package/package.json +1 -1
- package/dist/shared/components-mkezgkgk.js +0 -10
- package/dist/shared/{components-gvbapfen.js → components-6kqksbjb.js} +0 -0
- package/dist/shared/{components-dy5w47zg.js → components-7s7pykk5.js} +1 -1
- /package/dist/shared/{components-0jmqhx2k.js → components-tjtjmjjr.js} +0 -0
package/README.md
CHANGED
|
@@ -174,11 +174,11 @@ positioning) are ported.
|
|
|
174
174
|
| — (new) | `lib/field-classes.ts` | Local addition: the field ladder FF repeats per text field (rest → muted fill on hover or a group's `data-active` → card fill on focus; `data-invalid` keeps a destructive ring), as `fieldVariants` + `fieldIconClasses`, shared by Input, Textarea, Combobox and Sidebar's input; `useFieldBox` (variant, state attributes incl. `readOnly` / `required`, radius, inset) + `focusControlOnPress`, the box Input, Textarea, NumberInput, Combobox's fields and the date/time pickers' field build on, so each takes `variant`; `FieldStateContext`, which hands a `Field`'s state to its control; the pure class tables live in `field-variants.ts` and are re-exported here |
|
|
175
175
|
| — (new) | `lib/field-variants.ts` | Local addition: the field ladder's pure class tables (box, icon, adornment, action, label/description insets), split from `field-classes.ts` so a React Server Component can call them |
|
|
176
176
|
| — (new) | `lib/date-locale.ts` | Local addition: what the date and time pickers read off a react-day-picker (date-fns) locale — the hour cycle (Intl), typed dates, times and months in the locale's formats (date-fns `parse` with `P`…`PPPP`), field names for column labels (`Intl.DisplayNames`); `resolveLocale` (a BCP 47 tag → an Intl-backed date-fns Locale, after utegsk/ui's `localeFromTag`), `formatTime` on an explicit hour cycle, `parseDateTimeText` / `parseMonthNameText` |
|
|
177
|
-
| — (new) | `lib/row-order.tsx` | Local addition: document-order row numbering for menus and lists (`useRowOrder`, `useRowSlot`, `RowOrderContext`), so a row's `index` is optional; unlike `row-index.tsx` (direct children) it reaches rows anywhere below the panel; labels, separators and groups are numbered without a rect, so a selection run never melts across them. Lifted out of Dropdown |
|
|
177
|
+
| — (new) | `lib/row-order.tsx` | Local addition: document-order row numbering for menus and lists (`useRowOrder`, `useRowSlot`, `RowOrderContext`), so a row's `index` is optional; unlike `row-index.tsx` (direct children) it reaches rows anywhere below the panel; labels, separators and groups are numbered without a rect, so a selection run never melts across them. Lifted out of Dropdown; an explicit `index` on every row skips the DOM sort |
|
|
178
178
|
| — (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
179
|
| — (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
180
|
| — (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) |
|
|
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`) |
|
|
182
182
|
| — (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
183
|
| — (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
184
|
| — (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) |
|
|
@@ -232,33 +232,33 @@ only what is specific to it:
|
|
|
232
232
|
|
|
233
233
|
| Item | Upstream | Local | Specific modifications |
|
|
234
234
|
|---|---|---|---|
|
|
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` |
|
|
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` |
|
|
236
236
|
| `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
237
|
| `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
238
|
| `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
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) |
|
|
240
|
-
| `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 |
|
|
241
|
-
| `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) |
|
|
240
|
+
| `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
|
+
| `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
242
|
| `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 |
|
|
243
|
-
| `ColorPicker` | `registry/default/color-picker.tsx` | `components/inputs/color-picker/index.tsx` | `@/registry/radix/{slider,tooltip}` → the Base siblings `../slider` / `../../overlays/tooltip`; `noUncheckedIndexedAccess` guards on regex captures, `rgb()`/`hsl()`/`oklch()` parts, `itemRects[i]` and the Slider's array value; static `.Popover`; uncontrolled support was already upstream (verified, kept); `ColorPicker.Popover` (goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier`) and the format menu morph out of their triggers, replacing the motion wrappers and the ignored `actionsRef`; the color types and math (`parseColor`, `buildParsed`, conversions, formatting) in `variants.ts` (server-callable) and re-exported |
|
|
244
|
-
| `Combobox` | `registry/base/combobox.tsx` | `components/inputs/combobox/index.tsx` | root de-generified (the parser needs a plain function plus a same-file `interface ComboboxProps`): items gained an index signature, `value`/`defaultValue` are `string \| string[]` and `onValueChange` is a union of the two handler shapes; `ComboboxValue<Multiple>` still exported; `creatable` added; `React.*` type refs replaced by named type imports; statics `.Input/.Chips/.Content/.List/.Item/.Empty`; the popup morphs out of the field (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Combobox.Content`), replacing the `scaleY` wrapper and the `actionsRef` deferred unmount; `loading` (the chevron blurs into a `loader-circle` spinner, a status row under the list, the empty message held back) and `loadingLabel`; controlled query (`inputValue` / `defaultInputValue` / `onInputValueChange`); `filter={null}` shows the items as given; `readOnly` (Base UI's, plus the field box's read-only state; no clear or chip ✕); controlled popup (`open` / `defaultOpen` / `onOpenChange`); `footer` on `Combobox.Content` (a node or `({ close, query }) => node`); object items' `label` may be any node (unclipped in rows and chips) with `textValue` to filter, fill the field and name the chip |
|
|
243
|
+
| `ColorPicker` | `registry/default/color-picker.tsx` | `components/inputs/color-picker/index.tsx` | `@/registry/radix/{slider,tooltip}` → the Base siblings `../slider` / `../../overlays/tooltip`; `noUncheckedIndexedAccess` guards on regex captures, `rgb()`/`hsl()`/`oklch()` parts, `itemRects[i]` and the Slider's array value; static `.Popover`; uncontrolled support was already upstream (verified, kept); `ColorPicker.Popover` (goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier`) and the format menu morph out of their triggers, replacing the motion wrappers and the ignored `actionsRef`; the color types and math (`parseColor`, `buildParsed`, conversions, formatting) in `variants.ts` (server-callable) and re-exported; `ColorPicker.Popover` takes `disabled`, `readOnly`, `rootClassName` and `removeLabel`; swatches and tiles keep a hairline above the color |
|
|
244
|
+
| `Combobox` | `registry/base/combobox.tsx` | `components/inputs/combobox/index.tsx` | root de-generified (the parser needs a plain function plus a same-file `interface ComboboxProps`): items gained an index signature, `value`/`defaultValue` are `string \| string[]` and `onValueChange` is a union of the two handler shapes; `ComboboxValue<Multiple>` still exported; `creatable` added; `React.*` type refs replaced by named type imports; statics `.Input/.Chips/.Content/.List/.Item/.Empty`; the popup morphs out of the field (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Combobox.Content`), replacing the `scaleY` wrapper and the `actionsRef` deferred unmount; `loading` (the chevron blurs into a `loader-circle` spinner, a status row under the list, the empty message held back) and `loadingLabel`; controlled query (`inputValue` / `defaultInputValue` / `onInputValueChange`); `filter={null}` shows the items as given; `readOnly` (Base UI's, plus the field box's read-only state; no clear or chip ✕); controlled popup (`open` / `defaultOpen` / `onOpenChange`); `footer` on `Combobox.Content` (a node or `({ close, query }) => node`); object items' `label` may be any node (unclipped in rows and chips) with `textValue` to filter, fill the field and name the chip; fields read the surrounding Field's invalid state (`invalid` prop, `error` forces it), `hideTrigger`, a root `labels` object (`open` / `clear` / `remove(label)` / `placeholder`), and `onInputValueChange(value, details)` forwarding Base UI's change details |
|
|
245
245
|
| `Command` | — (local) | `components/overlays/command/index.tsx` | Base UI Autocomplete rendered inline in Dialog's panel (`position="top"`, no ✕), morphing from its trigger with goo; grouped/flat command data filtered by label + keywords; one fluid highlight follows Base UI's highlight; ⌘K/Ctrl+K `hotkey` (one palette answers a press); fresh palette per open; statics `.Trigger/.Content/.Palette/.Shortcut`; `CommandList`/`CommandListRoot` exported for Search; `onOpenChange(open, details)` with Base UI's change details (`reason`, `event`, `cancel()`) |
|
|
246
246
|
| `ContextMenu` | — (local; see [Morph sources](#morph-sources)) | `components/overlays/context-menu/index.tsx` | Base UI ContextMenu (right click, long press) with the morph layer growing from the press point (`from="pointer"` default, goo on `spring.goo`); rows are Dropdown's `MenuItem`, `Dropdown.Label`, `Dropdown.Separator` with fluid hover; `ContextMenu.Item` adds `shortcut`; `size` on the root; statics `.Trigger/.Content/.Item/.Label/.Separator` |
|
|
247
247
|
| `Dialog` | `registry/base/dialog.tsx` | `components/overlays/dialog/index.tsx` | `DialogProps` and `DialogContentProps` fully re-declared (the slot props now come from `lib/slot.ts`); statics attached as expando properties (`Dialog.Trigger = …`), so `<Dialog.Content>` types without a cast; FF's "With a sidebar" example replaced by "Surfaces inside a dialog" (written before the Sidebar was ported); the panel morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Dialog.Content`), the backdrop fades with the morph's progress, the root holds the open state and scopes `Morph.Part` pairs; `onOpenChange(open, details)` hears Base UI's change details and can `cancel()` (one-argument handlers still work), and `createDialogChangeDetails` builds them for the library's own changes; `disablePointerDismissal` on the root; `Dialog.Content` types and forwards `initialFocus` / `finalFocus` and takes `popupClassName` (merged after the popup's width) |
|
|
248
|
-
| `Dropdown` | `registry/base/dropdown.tsx` | `components/overlays/dropdown/index.tsx` | uncontrolled selection and uncontrolled search filtering (below); `items[next].focus()` guarded; statics `.Menu/.Trigger/.Content/.Item/.Label/.Separator/.Search/.Empty`; FF's "Create from the query" section skipped — adding a row to the list is real consumer state; the popup menu morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Dropdown.Content`), replacing the `scaleY` wrapper and the ignored `actionsRef`; popup rows hand activation to the Menu item's `onClick` so Enter / Space pick; shadcn DropdownMenu parity: children rows with an optional index (`lib/row-order.tsx`), `inset` / `shortcut` rows, a cancelable `onSelect(event)`, `asChild` on the trigger, a keyboard focus ring, `keepMounted`, and the parts in `menu-parts.tsx` (see the headers) |
|
|
249
|
-
| `DatePicker`, `DateRangePicker`, `TimePicker`, `DurationPicker`, `MonthPicker` | — (local; props after shadcn's date picker and utegsk/ui's pickers) | `components/inputs/date-time/<id>/index.tsx` (+ the group's `picker-shell.tsx`, `time-columns.tsx`) | a field on the field ladder whose panel morphs out of it with Combobox's options (goo by default; `from` / `effect` / `hideSource` / `tier`, in place with `effect="morph"` + `hideSource`), or a bottom `Sheet` under 640px; one hour column with AM/PM on a 12-hour clock; `columnsSide` / `timeColumnsSide`; the panel header types the value in the locale's formats; `value` / `defaultValue` / `onValueChange` with `null` for empty — a `Date`, react-day-picker's `DateRange`, `"HH:mm"`, a date-fns `Duration`, a month's first day; `locale` is a react-day-picker locale throughout; `picker-shell.tsx` takes an `editor`, `triggerLabel` and `readOnly`; `picker-shell.tsx` also takes `controlRef` / `controlProps` / `formValues` / `required` (visually hidden inputs for native forms) and exports `PickerControlProps` (form + aria props every picker forwards); every picker is a `forwardRef` with `readOnly`, `labels`, string `locale`; `DateRangePicker` adds `editable`, `withTime`, `hourCycle`, `hidden`, `startName` / `endName`, `startInputProps` / `endInputProps` |
|
|
248
|
+
| `Dropdown` | `registry/base/dropdown.tsx` | `components/overlays/dropdown/index.tsx` | uncontrolled selection and uncontrolled search filtering (below); `items[next].focus()` guarded; statics `.Menu/.Trigger/.Content/.Item/.Label/.Separator/.Search/.Empty`; FF's "Create from the query" section skipped — adding a row to the list is real consumer state; the popup menu morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Dropdown.Content`), replacing the `scaleY` wrapper and the ignored `actionsRef`; popup rows hand activation to the Menu item's `onClick` so Enter / Space pick; shadcn DropdownMenu parity: children rows with an optional index (`lib/row-order.tsx`), `inset` / `shortcut` rows, a cancelable `onSelect(event)`, `asChild` on the trigger, a keyboard focus ring, `keepMounted`, and the parts in `menu-parts.tsx` (see the headers); `render`/`asChild` and `textValue` on rows; Menu `onOpenChange(open, details)` and `modal`; Content/SubContent `finalFocus`, modo-side `initialFocus`, `collisionPadding`, `container`, `positionerClassName`, popup HTML attributes; Content `alignOffset`, `collisionBoundary` |
|
|
249
|
+
| `DatePicker`, `DateRangePicker`, `TimePicker`, `DurationPicker`, `MonthPicker` | — (local; props after shadcn's date picker and utegsk/ui's pickers) | `components/inputs/date-time/<id>/index.tsx` (+ the group's `picker-shell.tsx`, `time-columns.tsx`) | a field on the field ladder whose panel morphs out of it with Combobox's options (goo by default; `from` / `effect` / `hideSource` / `tier`, in place with `effect="morph"` + `hideSource`), or a bottom `Sheet` under 640px; one hour column with AM/PM on a 12-hour clock; `columnsSide` / `timeColumnsSide`; the panel header types the value in the locale's formats; `value` / `defaultValue` / `onValueChange` with `null` for empty — a `Date`, react-day-picker's `DateRange`, `"HH:mm"`, a date-fns `Duration`, a month's first day; `locale` is a react-day-picker locale throughout; `picker-shell.tsx` takes an `editor`, `triggerLabel` and `readOnly`; `picker-shell.tsx` also takes `controlRef` / `controlProps` / `formValues` / `required` (visually hidden inputs for native forms) and exports `PickerControlProps` (form + aria props every picker forwards); every picker is a `forwardRef` with `readOnly`, `labels`, string `locale`; `DateRangePicker` adds `editable`, `withTime`, `hourCycle`, `hidden`, `startName` / `endName`, `startInputProps` / `endInputProps`; `controlProps` reaches an editable field's input after its defaults (`autoComplete`, `inputMode`); the range panel header wraps with `withTime` |
|
|
250
250
|
| `DatePicker` | — (local; the masked input after utegsk/ui's `MaskedDateInput`) | `components/inputs/date-time/date-picker/index.tsx` (+ `masked-date-input.tsx`) | `editable` masked input in the field (Escape restores the last real date), `labels`, `formatDate`, `actions`, `readOnly`, string values (date-only strings read as local days) with an invalid state; `hidden`, `required` (no deselect), `unselectable` (`masked-date-input.tsx` takes `clearable`) |
|
|
251
|
-
| — | `registry/default/menu-item.tsx` | `components/overlays/dropdown/menu-item.tsx` | a row with no `checked` derives it from a self-managed panel and toggles the panel's state; `sourceIndex` carries the authored index so selection survives filter re-indexing; `label` and `index` optional (`children` rows numbered in document order), `inset`, `shortcut`, cancelable `onSelect(event)`; checkbox and radio rows report to the panel for the merged selection background |
|
|
252
|
-
| — | — (local) | `components/overlays/dropdown/menu-parts.tsx` | `Dropdown.Group`, the `Dropdown.Shortcut` re-export, value-keyed `Dropdown.CheckboxItem`, `Dropdown.RadioGroup` / `Dropdown.RadioItem`, `Dropdown.Sub` / `.SubTrigger` / `.SubContent` (a submenu morphs out of its row), and `useMenuPanel`, the state both popups share (keyboard focus ring, the submenu trigger's pinned highlight, value-checked backgrounds) |
|
|
251
|
+
| — | `registry/default/menu-item.tsx` | `components/overlays/dropdown/menu-item.tsx` | a row with no `checked` derives it from a self-managed panel and toggles the panel's state; `sourceIndex` carries the authored index so selection survives filter re-indexing; `label` and `index` optional (`children` rows numbered in document order), `inset`, `shortcut`, cancelable `onSelect(event)`; checkbox and radio rows report to the panel for the merged selection background; `render`/`asChild` and `textValue` on rows |
|
|
252
|
+
| — | — (local) | `components/overlays/dropdown/menu-parts.tsx` | `Dropdown.Group`, the `Dropdown.Shortcut` re-export, value-keyed `Dropdown.CheckboxItem`, `Dropdown.RadioGroup` / `Dropdown.RadioItem`, `Dropdown.Sub` / `.SubTrigger` / `.SubContent` (a submenu morphs out of its row), and `useMenuPanel`, the state both popups share (keyboard focus ring, the submenu trigger's pinned highlight, value-checked backgrounds); `Sub` / `SubContent` take `finalFocus`, modo-side `initialFocus`, `collisionPadding`, `container`, `positionerClassName` and popup HTML attributes |
|
|
253
253
|
| — | `registry/default/dropdown-search.tsx` | `components/overlays/dropdown/dropdown-search.tsx` | `filter` mode: `value`/`onValueChange` optional, the field reads and writes the query held by the panel (`DropdownFilterContext`) |
|
|
254
|
-
| `Copy` (upstream `InputCopy`) | `registry/default/input-copy.tsx` | `components/copy/index.tsx` | renamed, out of the inputs group; `@/registry/radix/tooltip` → `../overlays/tooltip`; `useRef<ReturnType<typeof setTimeout> \| null>(null)` (React 18 overloads); defaults spelled into the prop prose; the tooltip comment's "Radix" reads "Base UI"; the copied state morphs: the glyph's two sheets melt through the goo filter into a status-tinted disc behind the check (✕ on failure) and split back |
|
|
254
|
+
| `Copy` (upstream `InputCopy`) | `registry/default/input-copy.tsx` | `components/copy/index.tsx` | renamed, out of the inputs group; `@/registry/radix/tooltip` → `../overlays/tooltip`; `useRef<ReturnType<typeof setTimeout> \| null>(null)` (React 18 overloads); defaults spelled into the prop prose; the tooltip comment's "Radix" reads "Base UI"; the copied state morphs: the glyph's two sheets melt through the goo filter into a status-tinted disc behind the check (✕ on failure) and split back; local `chip` and `glyph` variants, `labels`, live-region announcement; ref typed `HTMLElement` |
|
|
255
255
|
| `Field` | — (local) | `components/inputs/field/index.tsx` | Base UI Field with no form library: `.Label` / `.Description` / `.Error` / `.Control`, inset to the text in the field box (`lib/field-variants.ts`); the field's state (invalid, also from native validity; disabled, read-only, required) reaches any control on `useFieldBox` through `FieldStateContext` |
|
|
256
|
-
| `FileDropzone` | — (local) | `components/inputs/file-dropzone/index.tsx` (+ `file-row.tsx`, `variants.ts`, `upload-demo.tsx`) | a label around a visually hidden file input, joined to Base UI's Field; hands picked files over and keeps none (no upload logic); drag verdicts accept / partial (some will be skipped) / reject, refusing only drags where nothing can pass; `required` announced, not native; `.Row` (`FileRow`: status and progress); the pure accept / size / count helpers (`acceptsFile`, `dragVerdict`, `sortFiles`, `formatFileSize`) in `variants.ts`; `upload-demo.tsx` is the docs stage |
|
|
256
|
+
| `FileDropzone` | — (local) | `components/inputs/file-dropzone/index.tsx` (+ `file-row.tsx`, `variants.ts`, `upload-demo.tsx`) | a label around a visually hidden file input, joined to Base UI's Field; hands picked files over and keeps none (no upload logic); drag verdicts accept / partial (some will be skipped) / reject, refusing only drags where nothing can pass; `required` announced, not native; `.Row` (`FileRow`: status and progress); the pure accept / size / count helpers (`acceptsFile`, `dragVerdict`, `sortFiles`, `formatFileSize`) in `variants.ts`; `upload-demo.tsx` is the docs stage; rest `<label>` props and `style` on the root |
|
|
257
257
|
| `Input`, `Label`, `InputGroup` | `registry/default/input-group.tsx` | `components/inputs/input/index.tsx` | `InputField` split into `Input` (the ringed box, standalone: own hover outside a group, `invalid`, `size`) and `Label` (now its own item `components/inputs/label/index.tsx`, re-exported from `inputs/input`; rendered through `Field.Label` in a field), composed back by `InputGroup.Field`; item renamed `input`, `Input` the default export; uncontrolled mode (`InputHTMLAttributes`' own `defaultValue` omitted in favour of the local one); static `.Field`; `startAdornment` / `endAdornment`, `readOnly`, `required` (both also from a surrounding `Field`), `InputAction` (an icon button inside the box) and `InputAdornment`; a `type` swap keeps the caret and selection |
|
|
258
258
|
| `Label` | `registry/default/input-group.tsx` | `components/inputs/label/index.tsx` | split out of InputField; invalid, size; data-disabled dims it; re-exported from inputs/input |
|
|
259
259
|
| `InputMessage` | `registry/default/input-message.tsx` | `components/AI/input-message/index.tsx` | uncontrolled twins for value, files, queue and status (below); `FilePreviewTile` turned into a `forwardRef` (it exits inside `<AnimatePresence mode="popLayout">`, which measures through a ref); guards on `queue[0]`, `history[i]`, `suggestions[i]` and the `moveQueued` swap; FF's Playground section skipped and the surrounding transcript dropped (`ChatMessage` is not ported) |
|
|
260
|
-
| `NavigationMenu` | — (local) | `components/navigation/navigation-menu/index.tsx` | Base UI NavigationMenu; one shared flyout morphing from its trigger (anchored to the List, frame springs to each content's size, content crossfades by activation direction); fluid hover on strip and panels; one keyboard ring per panel; `Indicator` is a GooIndicator under the open/current item |
|
|
261
|
-
| `NumberInput` | — (local) | `components/inputs/number-input/index.tsx` | Base UI NumberField on the field box (`useFieldBox`), steppers as field actions inside the ring, Intl `format`, scrub |
|
|
260
|
+
| `NavigationMenu` | — (local) | `components/navigation/navigation-menu/index.tsx` | Base UI NavigationMenu; one shared flyout morphing from its trigger (anchored to the List, frame springs to each content's size, content crossfades by activation direction); fluid hover on strip and panels; one keyboard ring per panel; `Indicator` is a GooIndicator under the open/current item; Root takes ref and nav attributes; Trigger and Content `asChild` keep the child's element and ref |
|
|
261
|
+
| `NumberInput` | — (local) | `components/inputs/number-input/index.tsx` | Base UI NumberField on the field box (`useFieldBox`), steppers as field actions inside the ring, Intl `format`, scrub; `inputProps` spread last on the `<input>`; `onValueChange(value, details)` |
|
|
262
262
|
| `Pagination` | — (local, from the skill's authoring.md template) | `components/navigation/pagination/index.tsx` | pages strip (GooIndicator + fluid hover) or jump-to-page field; edges; page-size Select; labels as props |
|
|
263
263
|
| — | `registry/default/file-thumbnail.tsx` | `components/AI/input-message/file-thumbnail.tsx` | the lazy `import("pdfjs-dist")` is kept as-is — verified against modo's esbuild settings, it lands in a ~790 kB chunk fetched only when a PDF is attached; the worker still comes from jsDelivr, so no bundler-side worker config is needed; the `@next/next/no-img-element` eslint-disable dropped |
|
|
264
264
|
| `Popover` | — (local; after beUI's gooey popover and motion-primitives' morphing popover, see [Morph sources](#morph-sources)) | `components/overlays/popover/index.tsx` | Base UI Popover with the morph layer (goo by default); `openOnHover` on the trigger; statics `.Trigger/.Content/.Title/.Description/.Close`; `Popover.Content` forwards `anchor` (the morph's source when it is an element), `alignOffset`, `collisionPadding`, `collisionBoundary`, `initialFocus`, `finalFocus`; `onOpenChange(open, details)` with Base UI's change details (`cancel()`) |
|
|
@@ -266,15 +266,15 @@ only what is specific to it:
|
|
|
266
266
|
| `Progress` | — (local) | `components/progress/index.tsx` (+ `progress.css`, `step-demo.tsx`) | Base UI Progress; the determinate fill is a full-width bar slid with `translateX` on `spring.moderate` (after FF's CarouselDots fill, never scaled); with no `value` a segment sweeps the track; `.Spinner` draws Button's figure-eight loading glyph; both loops on `--duration-loop` (`progress.css`); `step-demo.tsx` is the docs stage |
|
|
267
267
|
| `Radio` (upstream `RadioItem`), `Radio.Group` (upstream `RadioGroup`) | `registry/base/radio-group.tsx` | `components/inputs/radio/index.tsx` | renamed; rows numbered by the group (`lib/row-index.tsx`), so `index` is optional; uncontrolled index and value modes; group context carries a `selectIndex` setter every item calls, so items need no `selected`/`onSelect`; arrow-key focus guarded; selected background is a `GooIndicator`; `label` optional, any node; a `Radio` without one is a bare circle, Base UI's Radio.Root (picked by `value`, `id` / `<label htmlFor>`, keeping its border when picked) laid out freely inside a value-mode group, which then draws no rows and leaves the arrow keys to Base UI; `disabled` on a row, or on the group (which dims as a whole); the group's `name` / `required` reach Base UI's RadioGroup for forms |
|
|
268
268
|
| `Search` | — (local; after beUI's morphing search, see [Morph sources](#morph-sources)) | `components/inputs/search/index.tsx` | pill or icon button springing its own width in place (`spring.slow`), growing over its neighbors (`align`); with `items`, Autocomplete suggestions ooze out of the field via the morph layer (goo on `spring.goo`, `effect`/`tier`) with Command's rows; Escape closes → clears → collapses; blur collapses when empty; `shortcut`; `<search>` landmark + form, `onSubmit` |
|
|
269
|
-
| `Select` | `registry/base/select.tsx` | `components/inputs/select/index.tsx` | members re-declared one per line (the parser drops members whose type spans lines); upstream's uncontrolled `defaultValue` verified and kept rather than rerouted through `useControllableState`; expando statics `.Trigger/.Content/.Item/.Group/.Label/.Separator`; the popup morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Select.Content`), replacing the `scaleY` wrapper and the `actionsRef` deferred unmount; `Select.Item`'s `index` is optional: rows take their place in document order (`lib/row-order.tsx`), inside groups, conditionals and maps, and the checked row is found by its own index; `textValue` for typeahead; a node label is shown as is (unclipped) in the rows and, through Base UI's `items`, in the trigger; `Select.Value` part, with `Select.Trigger` children replacing its value slot; `side` / `align` / `sideOffset` on `Select.Content`; `triggerVariants` in `variants.ts` (server-callable) and re-exported; the keyboard ring is the shared `lib/focus-ring.tsx` |
|
|
269
|
+
| `Select` | `registry/base/select.tsx` | `components/inputs/select/index.tsx` | members re-declared one per line (the parser drops members whose type spans lines); upstream's uncontrolled `defaultValue` verified and kept rather than rerouted through `useControllableState`; expando statics `.Trigger/.Content/.Item/.Group/.Label/.Separator`; the popup morphs out of its trigger (morph layer, goo on `spring.goo`; `from` / `effect` / `hideSource` / `tier` on `Select.Content`), replacing the `scaleY` wrapper and the `actionsRef` deferred unmount; `Select.Item`'s `index` is optional: rows take their place in document order (`lib/row-order.tsx`), inside groups, conditionals and maps, and the checked row is found by its own index; `textValue` for typeahead; a node label is shown as is (unclipped) in the rows and, through Base UI's `items`, in the trigger; `Select.Value` part, with `Select.Trigger` children replacing its value slot; `side` / `align` / `sideOffset` on `Select.Content`; `triggerVariants` in `variants.ts` (server-callable) and re-exported; the keyboard ring is the shared `lib/focus-ring.tsx`; `open` / `defaultOpen` / `onOpenChange(open, details)` (the 300ms pick ack runs only while uncontrolled); `Select.Content` `alignOffset` / `collisionPadding` / `finalFocus` / rest HTML attributes; `Select.Trigger` / `Select.Item` `render` / `asChild`; `Select.Trigger` `wrapperClassName`; `Select.Value` and `Select.Group` forward ref |
|
|
270
270
|
| `Sheet` | — (local; replaces beUI's bottom sheet and drawer, see [Morph sources](#morph-sources)) | `components/overlays/sheet/index.tsx` | Base UI Drawer (swipe to dismiss, snap points, `Drawer.Viewport` for the swipe) with the morph growing from the sheet's own edge; a swipe-dismissed sheet slides on off its edge from the release point (the morph's `exit`); `side`, `snapPoints`, `defaultSnapPoint`; Dialog's parts and `DialogState` (Base UI's Drawer parts are Dialog's); statics as Dialog's plus `.Provider` / `.Indent`; `onOpenChange(open, details)` with Base UI's change details (`reason`, `event`, `cancel()`); `disablePointerDismissal` on the root; `Sheet.Content` takes `side` (overrides the root's, the swipe follows), `initialFocus` / `finalFocus` and `popupClassName` (merged after `PLACEMENT`) |
|
|
271
271
|
| `Sidebar` | `registry/base/sidebar.tsx` (+ `registry/default/sidebar-core.tsx`, `sidebar-menu.tsx`; `app/globals.css` `.scroll-divider`) | `components/navigation/sidebar/index.tsx` (+ `sidebar-core.tsx`, `sidebar-menu.tsx`, `sidebar.css`) | the mobile sheet is the DS's `Sheet` (goo from its edge, swipe to dismiss) instead of a Dialog with a framer slide; no collapsed peek: `openOnHover` on the provider opens the sidebar in flow while the pointer is between its edge and the far side of a trigger beside it, the trigger switching between open and on hover (`panel-*-dashed` glyphs added to the icon context); the active row backgrounds are `GooIndicator`s and the hover highlight the liquid `FluidHoverHighlight`; uncontrolled selection (`value` on the buttons, `value` / `defaultValue` / `onValueChange` on `Sidebar`) and collapsible rows (`Sidebar.MenuItem collapsible`, below); statics `.Provider/.Trigger/.Rail/.Inset/.Input/.Header/.Content/.Footer/.Separator/.Group/.GroupLabel/.GroupAction/.GroupActions/.GroupContent/.Menu/.MenuItem/.MenuButton/.MenuAction/.MenuActions/.MenuBadge/.MenuSkeleton/.MenuSub/.MenuSubItem/.MenuSubButton` through a `SidebarComponent` cast; `fontVariationSettings` → `weight-*`, `text-[Npx]` → type roles, hex focus ring → `ring-focus-ring`, `duration-80\|160\|240` → tiers; `sidebarMenuButtonVariants` and the `SIDEBAR_*` constants in `variants.ts` (server-callable; `variants.ts` files are bunup entries); `inert` through `lib/inert.ts` |
|
|
272
|
-
| `Slider` | `registry/base/slider.tsx` | `components/inputs/slider/index.tsx` | `var(--color-accent)` → `var(--accent)` (no `--color-*` aliases here); uncontrolled support on the public wrapper, with both engines still fully controlled; `SliderProps extends Omit<SliderEngineProps, "value" \| "onChange">`; React 18 ref types (`MutableRefObject`); indexed reads of the value/step/pip arrays asserted; `aria-label` names the thumbs ahead of the visible `label` |
|
|
272
|
+
| `Slider` | `registry/base/slider.tsx` | `components/inputs/slider/index.tsx` | `var(--color-accent)` → `var(--accent)` (no `--color-*` aliases here); uncontrolled support on the public wrapper, with both engines still fully controlled; `SliderProps extends Omit<SliderEngineProps, "value" \| "onChange">`; React 18 ref types (`MutableRefObject`); indexed reads of the value/step/pip arrays asserted; `aria-label` names the thumbs ahead of the visible `label`; `orientation="vertical"` (compact engine, min at bottom) and `onValueCommit` (both engines; drag end, keyboard step, value edit) |
|
|
273
273
|
| `Switch` | `registry/base/switch.tsx` | `components/inputs/switch/index.tsx` | uncontrolled support; every in-body read goes through the resolved `isChecked` and a `toggle()` that also calls `onToggle`; the thumb is a `GooIndicator` in Base UI's Thumb (stretches as it travels), dragged x held in state; local `description` (muted line, `aria-describedby`) and `trackSide`; local `Switch.Group`: switches as settings rows (track at the end) in one outlined fieldset, after an outlined `Card.Group` (shared hairlines dropped around the fluid hover highlight), rows numbered by the group; `label` optional, any node; without one (outside a group) the switch is the bare track, Base UI's Switch.Root, which toggles itself (click, keyboard, a `<label htmlFor>`) through `onCheckedChange`, with the thumb drag on the track and the toggle that follows a drag swallowed; `name` / `value` / `required` reach Base UI's hidden input in both modes |
|
|
274
|
-
| `Table` | `registry/default/table.tsx` | `components/table/index.tsx` (+ `sort-demo.tsx`) | statics `.Header/.Body/.Row/.Head/.Cell`; `TableProps` / `TableRowProps` exported; local additions: Footer, Caption, Empty, sortable Head (`sortDirection`/`defaultSortDirection`/`onSort`), `stacked` + Cell `label` (stacked cells `items-start`), `containerClassName`; unindexed body rows numbered in document order (`lib/row-order`) |
|
|
274
|
+
| `Table` | `registry/default/table.tsx` | `components/table/index.tsx` (+ `sort-demo.tsx`) | statics `.Header/.Body/.Row/.Head/.Cell`; `TableProps` / `TableRowProps` exported; local additions: Footer, Caption, Empty, sortable Head (`sortDirection`/`defaultSortDirection`/`onSort`), `stacked` + Cell `label` (stacked cells `items-start`), `containerClassName`; unindexed body rows numbered in document order (`lib/row-order`); weight by section; header-only border melt; stacked values in foreground |
|
|
275
275
|
| `Tabs` | `registry/base/tabs.tsx` | `components/navigation/tabs/index.tsx` | `defaultSelectedIndex` added as the index-mode twin of the existing `defaultValue`; indicator and hover pill are `GooIndicator`s (the hover pill returns to the active tab as it fades); statics `.List/.Item/.Panel`; `Tabs.Item` node `label` (inert ghost copy), `href` link tabs |
|
|
276
276
|
| `TabsSubtle` | `registry/base/tabs-subtle.tsx` | `components/navigation/tabs-subtle/index.tsx` | uncontrolled mode; panels may be authored inside the root (below); the tab list's ref write goes through `MutableRefObject`; selected and hover pills are `GooIndicator`s; statics `.Item/.Panel` |
|
|
277
|
-
| `Textarea` | — (local; Input's multi-line twin) | `components/inputs/textarea/index.tsx` | Base UI `Field.Control` rendered as a `<textarea>`; the field ladder from `lib/field-classes.ts`; padding sized so one row matches the control height; grows from `minRows` to `maxRows` (InputMessage's measure), then scrolls; `value` / `defaultValue` / `onChange`; `readOnly` / `required` (also from a surrounding `Field`) |
|
|
277
|
+
| `Textarea` | — (local; Input's multi-line twin) | `components/inputs/textarea/index.tsx` | Base UI `Field.Control` rendered as a `<textarea>`; the field ladder from `lib/field-classes.ts`; padding sized so one row matches the control height; grows from `minRows` to `maxRows` (InputMessage's measure), then scrolls; `value` / `defaultValue` / `onChange`; `readOnly` / `required` (also from a surrounding `Field`); neither `value` nor `onChange` means a DOM-held uncontrolled `<textarea>` (ref-readable, form-reset aware) |
|
|
278
278
|
| `ThinkingIndicator` | `registry/default/thinking-indicator.tsx` | `components/AI/thinking-indicator/index.tsx` | upstream prop JSDoc kept (it already matches the FF API table); the `.shimmer-text` rule it rides lives in `base.css`; local `variant="goo"` (three dots merging and splitting through `GooFilter`, plain label; default stays `shimmer`); reduced motion also follows MotionConfig |
|
|
279
279
|
| `Toast` | — (local; after Sileo, see [Morph sources](#morph-sources)) | `components/overlays/toast/index.tsx` (+ `manager.ts`, `global-demo.tsx`) | Base UI Toast (queue, timers, swipe, announcements) with Sileo's pill melting into a body through the shared `GooFilter`; autopilot expand/collapse, six states on the `--status-*` tokens, promise toasts; `Toast` is the provider + viewport, `Toast.Trigger` shows one, `useToast()` the in-tree API (plus `toasts`, `update`, `dismiss`); `manager.ts` holds one module-level Base UI `createToastManager()` exported as the global `toast()` (`.success/.error/.warning/.info/.loading/.promise/.update/.dismiss`, each returning `{ id, update, dismiss }`), queuing calls made before a provider subscribes (dropped on the server); `<Toast manager={toast}>` renders them (`manager` takes any source with a `subscribe`); ReactNode title/description, `icon` as a component or node, `action` as `{ label, onClick }` or a node, `duration` as an alias of `timeout`; a loading toast stays until its type changes, then leaves on the timeout; status glyphs via `useIcons()` instead of direct lucide imports; `global-demo.tsx` is the docs stage; `Toast.Action` / `Toast.Close` (Base UI's parts as compact Buttons, `render` / `asChild`) for custom action nodes |
|
|
280
280
|
| `Tooltip` | `registry/base/tooltip.tsx` | `components/overlays/tooltip/index.tsx` | `className`/`children` docs filled in; statics `.Provider` / `.PortalContainer`; the slide + fade replaced by the morph layer (goo from the trigger on `spring.goo`, `from` / `effect` / `tier` props; `followCursor` tooltips fade); a hand-off between grouped tooltips skips the morph (Base UI's `data-instant`); local compound parts `TooltipRoot` (`open` / `defaultOpen` / `onOpenChange`, `delay` or `delayDuration`, `closeDelay`, `followCursor`), `TooltipTrigger` (`render` or Radix-style `asChild`; drops the slot's `nativeButton`) and `TooltipContent` (`side`, `align`, `sideOffset`, `alignOffset`, `collisionPadding`, `positionerClassName`, morph options, `render` / `asChild` for the label, rest props on the popup), also as statics `.Root .Trigger .Content`; `Tooltip` without `content` is the root, and the flat form gains `open` / `defaultOpen` / `align` / `alignOffset` / `collisionPadding` / `delay` / `closeDelay`; `TooltipProvider` takes `delay` / `closeDelay` / `timeout` and Radix's `delayDuration` / `skipDelayDuration` (Base UI name wins; a negative skip clamps to 0) |
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Accordion from './index'
|
|
2
|
+
import { ArrowRight } from 'lucide-react'
|
|
2
3
|
|
|
3
4
|
# Standalone
|
|
4
5
|
|
|
@@ -146,3 +147,57 @@ Each panel drips out of the row where it was pressed (its middle, from the keybo
|
|
|
146
147
|
</Accordion.Content>
|
|
147
148
|
</Accordion.Item>
|
|
148
149
|
</Accordion.Group>
|
|
150
|
+
|
|
151
|
+
# Without the chevron
|
|
152
|
+
|
|
153
|
+
`icon={false}` drops the glyph at the row's end, for a row whose own content says it opens. A component passed as `icon` replaces the chevron and turns a quarter turn on open, so give it a right-pointing glyph.
|
|
154
|
+
|
|
155
|
+
<Accordion.Group type="single" collapsible defaultValue="plain">
|
|
156
|
+
<Accordion.Item value="plain" index={0}>
|
|
157
|
+
<Accordion.Trigger icon={false}>No chevron</Accordion.Trigger>
|
|
158
|
+
<Accordion.Content>
|
|
159
|
+
The label takes the whole row; hover and the open tint still say what it does.
|
|
160
|
+
</Accordion.Content>
|
|
161
|
+
</Accordion.Item>
|
|
162
|
+
<Accordion.Item value="default" index={1}>
|
|
163
|
+
<Accordion.Trigger>Default chevron</Accordion.Trigger>
|
|
164
|
+
<Accordion.Content>
|
|
165
|
+
The same row with the icon context's chevron.
|
|
166
|
+
</Accordion.Content>
|
|
167
|
+
</Accordion.Item>
|
|
168
|
+
<Accordion.Item value="custom" index={2}>
|
|
169
|
+
<Accordion.Trigger icon={ArrowRight}>Custom glyph</Accordion.Trigger>
|
|
170
|
+
<Accordion.Content>
|
|
171
|
+
An arrow that points down once the item is open.
|
|
172
|
+
</Accordion.Content>
|
|
173
|
+
</Accordion.Item>
|
|
174
|
+
</Accordion.Group>
|
|
175
|
+
|
|
176
|
+
# Rich headers
|
|
177
|
+
|
|
178
|
+
By default the trigger renders its label twice: a hidden semibold copy reserves the width, so the label gets heavier on open without moving. A header made of components would be duplicated in the DOM, so `weightShift={false}` renders it once and leaves the weight to its content. `contentClassName` is merged over the inner row's inset, padding and muted body text: the first panel below turns its text to full strength, the second takes a wider inset and the caption size. The label lives inside the trigger `<button>`, so build it from phrasing content (spans, as below), never a `div`-based header. Text that fixes its own colour stays that colour when the item opens: the chevron, the highlight and the open tint are what signal the state.
|
|
179
|
+
|
|
180
|
+
<Accordion.Group type="multiple" defaultValue={['one']}>
|
|
181
|
+
<Accordion.Item value="one" index={0}>
|
|
182
|
+
<Accordion.Trigger weightShift={false}>
|
|
183
|
+
<span className="flex flex-col">
|
|
184
|
+
<span className="text-subtitle text-foreground">Pro plan</span>
|
|
185
|
+
<span className="text-caption text-muted-foreground">Billed yearly, cancel any time</span>
|
|
186
|
+
</span>
|
|
187
|
+
</Accordion.Trigger>
|
|
188
|
+
<Accordion.Content contentClassName="text-foreground">
|
|
189
|
+
Unlimited projects and priority support. The header above is in the DOM once.
|
|
190
|
+
</Accordion.Content>
|
|
191
|
+
</Accordion.Item>
|
|
192
|
+
<Accordion.Item value="two" index={1}>
|
|
193
|
+
<Accordion.Trigger weightShift={false} icon={false}>
|
|
194
|
+
<span className="flex flex-col">
|
|
195
|
+
<span className="text-subtitle text-foreground">Team plan</span>
|
|
196
|
+
<span className="text-caption text-muted-foreground">Per seat, no chevron</span>
|
|
197
|
+
</span>
|
|
198
|
+
</Accordion.Trigger>
|
|
199
|
+
<Accordion.Content contentClassName="px-6 text-caption">
|
|
200
|
+
Shared workspaces and roles.
|
|
201
|
+
</Accordion.Content>
|
|
202
|
+
</Accordion.Item>
|
|
203
|
+
</Accordion.Group>
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
66
66
|
|
|
67
67
|
import { FluidHoverHighlight } from '../../lib/fluid-hover-highlight'
|
|
68
|
-
import { useIcon } from '../../lib/icon-context'
|
|
68
|
+
import { type IconComponent, useIcon } from '../../lib/icon-context'
|
|
69
69
|
import { GooFilter } from '../../lib/morph-layers'
|
|
70
70
|
import { useReduceMotion } from '../../lib/reduced-motion'
|
|
71
71
|
import { useShape } from '../../lib/shape-context'
|
|
@@ -514,9 +514,11 @@ type AccordionComponent = ForwardRefExoticComponent<AccordionProps & RefAttribut
|
|
|
514
514
|
*
|
|
515
515
|
* Statics: Accordion.Group (the grouped root: type, collapsible,
|
|
516
516
|
* defaultValue, value, onValueChange, highlight, size), Accordion.Item (value,
|
|
517
|
-
* index, disabled), Accordion.Trigger (the row that toggles its item
|
|
518
|
-
*
|
|
519
|
-
*
|
|
517
|
+
* index, disabled), Accordion.Trigger (the row that toggles its item: `icon`
|
|
518
|
+
* swaps or hides the chevron, `weightShift={false}` renders a rich label once)
|
|
519
|
+
* and Accordion.Content (the collapsible panel: `effect` — `'goo'` or a plain
|
|
520
|
+
* `'morph'` — the spring `tier`, `'slow'` or `'moderate'`, and
|
|
521
|
+
* `contentClassName` for the inner row).
|
|
520
522
|
*
|
|
521
523
|
* @example {@include ./examples.mdx}
|
|
522
524
|
*/
|
|
@@ -669,11 +671,16 @@ AccordionItem.displayName = 'AccordionItem'
|
|
|
669
671
|
interface AccordionTriggerProps extends HTMLAttributes<HTMLButtonElement> {
|
|
670
672
|
/** Trigger label content. */
|
|
671
673
|
children: ReactNode
|
|
674
|
+
/** 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
|
+
icon?: IconComponent | false
|
|
676
|
+
/** 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`. */
|
|
677
|
+
weightShift?: boolean
|
|
672
678
|
}
|
|
673
679
|
|
|
674
680
|
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
|
|
675
|
-
({ children, className, ...props }, ref) => {
|
|
681
|
+
({ children, className, icon, weightShift = true, ...props }, ref) => {
|
|
676
682
|
const ChevronRight = useIcon('chevron-right')
|
|
683
|
+
const Glyph = icon === false ? null : (icon ?? ChevronRight)
|
|
677
684
|
const groupCtx = useAccordionGroup()
|
|
678
685
|
const { index, isOpen, triggerRef, highlight, press } = useAccordionItemContext()
|
|
679
686
|
const shape = useShape()
|
|
@@ -704,36 +711,50 @@ const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
|
|
|
704
711
|
{...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
|
|
705
712
|
>
|
|
706
713
|
{/* Label with dual-layer text */}
|
|
707
|
-
|
|
708
|
-
<span className=
|
|
709
|
-
|
|
714
|
+
{weightShift ? (
|
|
715
|
+
<span className={cn('inline-grid flex-1 text-left', sizeClasses.text)}>
|
|
716
|
+
<span className="col-start-1 row-start-1 invisible weight-semibold" aria-hidden="true">
|
|
717
|
+
{children}
|
|
718
|
+
</span>
|
|
719
|
+
<span
|
|
720
|
+
className={cn(
|
|
721
|
+
'col-start-1 row-start-1 transition-[color,font-variation-settings] duration-fast',
|
|
722
|
+
isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
|
|
723
|
+
isOpen ? 'weight-semibold' : 'weight-normal',
|
|
724
|
+
)}
|
|
725
|
+
>
|
|
726
|
+
{children}
|
|
727
|
+
</span>
|
|
710
728
|
</span>
|
|
729
|
+
) : (
|
|
711
730
|
<span
|
|
712
731
|
className={cn(
|
|
713
|
-
'
|
|
732
|
+
'block min-w-0 flex-1 text-left transition-[color] duration-fast',
|
|
733
|
+
sizeClasses.text,
|
|
714
734
|
isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
|
|
715
|
-
isOpen ? 'weight-semibold' : 'weight-normal',
|
|
716
735
|
)}
|
|
717
736
|
>
|
|
718
737
|
{children}
|
|
719
738
|
</span>
|
|
720
|
-
|
|
739
|
+
)}
|
|
721
740
|
|
|
722
741
|
{/* Chevron */}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
742
|
+
{Glyph && (
|
|
743
|
+
<motion.span
|
|
744
|
+
className="shrink-0 inline-flex items-center justify-center"
|
|
745
|
+
animate={{ rotate: isOpen ? 90 : 0 }}
|
|
746
|
+
transition={spring.fast}
|
|
747
|
+
>
|
|
748
|
+
<Glyph
|
|
749
|
+
size={sizeClasses.icon}
|
|
750
|
+
strokeWidth={isOpen || isActive ? 2 : 1.5}
|
|
751
|
+
className={cn(
|
|
752
|
+
'transition-[color,stroke-width] duration-fast',
|
|
753
|
+
isOpen || isActive ? 'text-foreground' : 'text-muted-foreground',
|
|
754
|
+
)}
|
|
755
|
+
/>
|
|
756
|
+
</motion.span>
|
|
757
|
+
)}
|
|
737
758
|
</AccordionPrimitive.Trigger>
|
|
738
759
|
</AccordionPrimitive.Header>
|
|
739
760
|
)
|
|
@@ -798,6 +819,8 @@ interface AccordionContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
798
819
|
effect?: 'goo' | 'morph'
|
|
799
820
|
/** Spring tier the panel opens on; it closes on the tier's faster exit tween. Defaults to `'slow'`. */
|
|
800
821
|
tier?: 'moderate' | 'slow'
|
|
822
|
+
/** Classes for the inner row that holds the children, merged over its defaults (the row inset, the top and bottom padding, the muted body text), so `px-0 text-foreground` takes the content flush and full-strength. */
|
|
823
|
+
contentClassName?: string
|
|
801
824
|
}
|
|
802
825
|
|
|
803
826
|
interface Rect {
|
|
@@ -828,7 +851,7 @@ function place(el: HTMLElement | null, { x, y, w, h, r }: Rect) {
|
|
|
828
851
|
}
|
|
829
852
|
|
|
830
853
|
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
|
|
831
|
-
({ children, className, effect = 'goo', tier = 'slow', ...props }, ref) => {
|
|
854
|
+
({ children, className, effect = 'goo', tier = 'slow', contentClassName, ...props }, ref) => {
|
|
832
855
|
const groupCtx = useAccordionGroup()
|
|
833
856
|
const { isOpen, highlight, press } = useAccordionItemContext()
|
|
834
857
|
const shape = useShape()
|
|
@@ -1008,6 +1031,7 @@ const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
|
|
|
1008
1031
|
sizeClasses.px,
|
|
1009
1032
|
sizeClasses.text,
|
|
1010
1033
|
sizeClasses.variant === 'compact' ? 'pb-2.5' : 'pb-3',
|
|
1034
|
+
contentClassName,
|
|
1011
1035
|
)}
|
|
1012
1036
|
>
|
|
1013
1037
|
{children}
|
|
@@ -210,3 +210,36 @@ edge and spaced like the parts. In an inline card it stands upright.
|
|
|
210
210
|
</Card>
|
|
211
211
|
</Card.Group>
|
|
212
212
|
</div>
|
|
213
|
+
|
|
214
|
+
# Heading level
|
|
215
|
+
|
|
216
|
+
`Card.Title` is a `span` by default. `as` renders it as a heading level for the page's outline (`h2`, `h3`, …) or as another tag; the look stays the same. The title below is a third-level heading, since this docs page already has its own `h1` and `h2` sections.
|
|
217
|
+
|
|
218
|
+
<div className="w-full max-w-[360px]">
|
|
219
|
+
<Card border="outlined">
|
|
220
|
+
<Card.Header>
|
|
221
|
+
<Card.Title as="h3">Notifications</Card.Title>
|
|
222
|
+
<Card.Description>Where we send what you subscribed to.</Card.Description>
|
|
223
|
+
</Card.Header>
|
|
224
|
+
<Card.Content bodyText className="text-muted-foreground">Email and push, daily digest.</Card.Content>
|
|
225
|
+
</Card>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
# Body text
|
|
229
|
+
|
|
230
|
+
`bodyText` on `Card.Content` sets the size ladder's body size (one step down in a compact region), so plain text inside no longer takes the size of the page around the card. It is off by default because content inherits its size, and a host that has been relying on that would see its text change. Both cards sit in a region set in the title size.
|
|
231
|
+
|
|
232
|
+
<div className="flex w-full max-w-[560px] flex-col gap-4 text-title">
|
|
233
|
+
<Card border="outlined">
|
|
234
|
+
<Card.Header>
|
|
235
|
+
<Card.Title>Inherited</Card.Title>
|
|
236
|
+
</Card.Header>
|
|
237
|
+
<Card.Content className="text-muted-foreground">The content takes the title size of its surroundings.</Card.Content>
|
|
238
|
+
</Card>
|
|
239
|
+
<Card border="outlined">
|
|
240
|
+
<Card.Header>
|
|
241
|
+
<Card.Title>Body text</Card.Title>
|
|
242
|
+
</Card.Header>
|
|
243
|
+
<Card.Content bodyText className="text-muted-foreground">The content takes the ladder's body size.</Card.Content>
|
|
244
|
+
</Card>
|
|
245
|
+
</div>
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
* - `Card.Divider` / `CardDivider` (local): a hairline across the card between
|
|
27
27
|
* its parts (full-bleed, spaced like the parts), vertical in an inline card;
|
|
28
28
|
* the Separator primitive in the card's divider tone.
|
|
29
|
+
* - `CardTitle` takes `as` (a heading level or another tag; a `span` by default)
|
|
30
|
+
* and `CardContent` takes `bodyText` (the ladder's body size instead of the
|
|
31
|
+
* inherited one), both local and opt-in.
|
|
29
32
|
*/
|
|
30
33
|
|
|
31
34
|
import {
|
|
@@ -38,6 +41,7 @@ import {
|
|
|
38
41
|
isValidElement,
|
|
39
42
|
type ReactElement,
|
|
40
43
|
type ReactNode,
|
|
44
|
+
type Ref,
|
|
41
45
|
type RefAttributes,
|
|
42
46
|
useContext,
|
|
43
47
|
useMemo,
|
|
@@ -290,7 +294,8 @@ type CardComponent = ForwardRefExoticComponent<CardProps & RefAttributes<HTMLDiv
|
|
|
290
294
|
* Statics: Card.Group (orientation "card" | "inline", columns, border "none"
|
|
291
295
|
* | "outlined", separated, fluidHover), Card.Header (title + description
|
|
292
296
|
* grid, pins Card.Action top-right), Card.Title (weight-animates when the card
|
|
293
|
-
* is selected), Card.Description,
|
|
297
|
+
* is selected; `as` renders it as a heading level), Card.Description,
|
|
298
|
+
* Card.Action, Card.Content (`bodyText` sets the body size), Card.Footer (the
|
|
294
299
|
* actions row; it wraps), Card.Divider (a hairline across the card between
|
|
295
300
|
* its parts, vertical in an inline card), Card.Media (an icon tile, a logo or a [logoA, logoB] pair),
|
|
296
301
|
* Card.Image (full-bleed banner, or a leading image when inline),
|
|
@@ -580,8 +585,15 @@ CardHeader.displayName = 'CardHeader'
|
|
|
580
585
|
|
|
581
586
|
// ── CardTitle ────────────────────────────────────────────
|
|
582
587
|
|
|
583
|
-
|
|
584
|
-
|
|
588
|
+
type CardTitleTag = 'span' | 'div' | 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
589
|
+
|
|
590
|
+
interface CardTitleProps extends HTMLAttributes<HTMLSpanElement> {
|
|
591
|
+
/** The element the title renders as: a heading level (`'h2'`, `'h3'`, …) for a page outline, or another tag. The title keeps its look whatever the tag. Defaults to `'span'`. */
|
|
592
|
+
as?: CardTitleTag
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const CardTitle = forwardRef<HTMLSpanElement, CardTitleProps>(
|
|
596
|
+
({ as: Tag = 'span', className, children, ...props }, ref) => {
|
|
585
597
|
const { emphasized, orientation } = useContext(CardContext)
|
|
586
598
|
const sizeClasses = useSize()
|
|
587
599
|
const compact = sizeClasses.variant === 'compact'
|
|
@@ -599,11 +611,12 @@ const CardTitle = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
|
|
599
611
|
// width so the ellipsis falls inside the visible box. All of it is inert
|
|
600
612
|
// while titles wrap normally.
|
|
601
613
|
return (
|
|
602
|
-
<
|
|
603
|
-
ref={ref}
|
|
614
|
+
<Tag
|
|
615
|
+
ref={ref as Ref<HTMLHeadingElement>}
|
|
604
616
|
data-slot="card-title"
|
|
605
617
|
className={cn(
|
|
606
618
|
'inline-grid grid-cols-[minmax(0,1fr)] leading-snug',
|
|
619
|
+
Tag !== 'span' && 'm-0',
|
|
607
620
|
compact ? 'text-subtitle-compact' : 'text-subtitle',
|
|
608
621
|
className,
|
|
609
622
|
)}
|
|
@@ -630,7 +643,7 @@ const CardTitle = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
|
|
630
643
|
>
|
|
631
644
|
{children}
|
|
632
645
|
</span>
|
|
633
|
-
</
|
|
646
|
+
</Tag>
|
|
634
647
|
)
|
|
635
648
|
},
|
|
636
649
|
)
|
|
@@ -677,7 +690,12 @@ CardAction.displayName = 'CardAction'
|
|
|
677
690
|
|
|
678
691
|
// ── CardContent ──────────────────────────────────────────
|
|
679
692
|
|
|
680
|
-
|
|
693
|
+
interface CardContentProps extends HTMLAttributes<HTMLDivElement> {
|
|
694
|
+
/** Sets the size ladder's body text (`text-body`, one step down in a compact region) on the content, so plain text there no longer takes the size of whatever surrounds the card. Off, the content inherits it, as before. Defaults to `false`. */
|
|
695
|
+
bodyText?: boolean
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const CardContent = forwardRef<HTMLDivElement, CardContentProps>(({ bodyText = false, className, ...props }, ref) => {
|
|
681
699
|
const { orientation } = useContext(CardContext)
|
|
682
700
|
const sizeClasses = useSize()
|
|
683
701
|
const compact = sizeClasses.variant === 'compact'
|
|
@@ -685,7 +703,11 @@ const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>((
|
|
|
685
703
|
<div
|
|
686
704
|
ref={ref}
|
|
687
705
|
data-slot="card-content"
|
|
688
|
-
className={cn(
|
|
706
|
+
className={cn(
|
|
707
|
+
orientation === 'inline' ? '' : compact ? 'px-3 pt-2.5' : 'px-4 pt-3',
|
|
708
|
+
bodyText && sizeClasses.text,
|
|
709
|
+
className,
|
|
710
|
+
)}
|
|
689
711
|
{...props}
|
|
690
712
|
/>
|
|
691
713
|
)
|
|
@@ -1017,7 +1039,17 @@ Object.assign(Card, {
|
|
|
1017
1039
|
Button: CardButton,
|
|
1018
1040
|
})
|
|
1019
1041
|
|
|
1020
|
-
export type {
|
|
1042
|
+
export type {
|
|
1043
|
+
CardBorder,
|
|
1044
|
+
CardButtonProps,
|
|
1045
|
+
CardButtonVariant,
|
|
1046
|
+
CardContentProps,
|
|
1047
|
+
CardGroupProps,
|
|
1048
|
+
CardLogo,
|
|
1049
|
+
CardProps,
|
|
1050
|
+
CardTitleProps,
|
|
1051
|
+
CardTitleTag,
|
|
1052
|
+
}
|
|
1021
1053
|
export {
|
|
1022
1054
|
Card,
|
|
1023
1055
|
CardAction,
|
|
@@ -85,3 +85,35 @@ their own); in an app it would be where you bump a counter.
|
|
|
85
85
|
onCopy={() => {}}
|
|
86
86
|
/>
|
|
87
87
|
</div>
|
|
88
|
+
|
|
89
|
+
# Inline chip
|
|
90
|
+
|
|
91
|
+
`variant="chip"` is an inline `<span>` around `children`: it sits in a sentence, copies its own text (or `value` when the text is not what you want copied), and shows the glyph after it. It is a real button, so Tab reaches it and Enter or Space copies; the result is announced to a screen reader through a live region.
|
|
92
|
+
|
|
93
|
+
<p className="text-body text-muted-foreground">
|
|
94
|
+
Run <Copy variant="chip">bun add @base-ui/react</Copy> to install, or copy the registry
|
|
95
|
+
entry <Copy variant="chip" value="https://fluidfunctionalism.com/r/input-copy.json">input-copy</Copy> instead.
|
|
96
|
+
In a dense region the chip steps down: <Copy variant="chip" size="compact">bun run dev</Copy>.
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
# Glyph only
|
|
100
|
+
|
|
101
|
+
`variant="glyph"` is the copy glyph with no visible value, for a place that already shows it. Name it with `aria-label`: that is its tooltip and its accessible name. A press copies `value`, the glyph melts into the check, and the result is announced.
|
|
102
|
+
|
|
103
|
+
<div className="flex items-center gap-1 text-body">
|
|
104
|
+
<span className="font-mono text-foreground">INV-2026-0042</span>
|
|
105
|
+
<Copy variant="glyph" value="INV-2026-0042" aria-label="Copy invoice number" />
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
# Other languages
|
|
109
|
+
|
|
110
|
+
`labels` replaces the words of the control (tooltip, accessible name, the button word, the announcement); keys you leave out keep their English default.
|
|
111
|
+
|
|
112
|
+
<div className="flex w-72 flex-col gap-4">
|
|
113
|
+
<Copy
|
|
114
|
+
variant="button"
|
|
115
|
+
value="npm i modo"
|
|
116
|
+
labels={{ word: 'Kopírovať', copied: 'Skopírované', failed: 'Kopírovanie zlyhalo', failedWord: 'Chyba' }}
|
|
117
|
+
/>
|
|
118
|
+
<Copy variant="glyph" value="npm i modo" aria-label="Kopírovať príkaz" labels={{ copied: 'Skopírované' }} />
|
|
119
|
+
</div>
|