diff --git a/package-lock.json b/package-lock.json index c426117..eb82853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3852,9 +3852,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", - "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" diff --git a/package.json b/package.json index 7c5f4f8..09bef28 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "vite-plus": "^0.3.1" }, "overrides": { - "dompurify": "3.4.13" + "dompurify": "3.4.16" }, "engines": { "node": ">=24.0.0" diff --git a/src/pages/docs/catalog.ts b/src/pages/docs/catalog.ts index 9952427..610c508 100644 --- a/src/pages/docs/catalog.ts +++ b/src/pages/docs/catalog.ts @@ -1069,15 +1069,13 @@ const overlays = componentPages('Overlays', [ themes: ['dropdown-menu', 'context-menu'], }, { - title: 'Drawer and Sheet', - // Not primitive-less like the experimentalControls set above — both are - // straight aliases of @askrjs/ui/dialog (askr-themes/src/entries/ - // {drawer,sheet}.ts). Sheet gets real data-side positioning CSS; Drawer - // has none at all — neither has distinct gesture or animation behavior - // of its own beyond what Dialog already provides. + title: 'Sheet', + path: 'drawer-and-sheet', + // Preserve the published documentation URL. Sheet composes Dialog behavior + // with side-aware content and styled text/layout parts. status: 'experimental', - ui: [], - themes: ['drawer', 'sheet'], + ui: ['dialog'], + themes: ['sheet'], }, ]); const navigation = componentPages('Navigation and chrome', [ @@ -1137,16 +1135,13 @@ const feedback = componentPages('Disclosure and feedback', [ themes: ['progress'], }, { - title: 'Toast and Sonner', - // Toast (from @askrjs/ui) is a real, working headless primitive. Sonner - // (themes/sonner, aliasing Toaster) is not a queueing/stacking/dismiss - // notifier system the way shadcn's sonner or the original Sonner library - // is — it's a bare presentational div wrapper with no imperative toast() - // API, timing, or stacking logic of its own. Don't let the shared name - // imply parity with those. + title: 'Toast and Toaster', + path: 'toast-and-sonner', + // Preserve the published URL. Toast owns notification behavior; Toaster is + // a presentational host available through the retained aggregate entry. status: 'limited', ui: ['toast'], - themes: ['toast', 'sonner'], + themes: ['toast', 'components'], }, { // Stat (Stat/StatLabel/StatValue/StatDescription) IS a real export, but diff --git a/src/pages/docs/component-demos.tsx b/src/pages/docs/component-demos.tsx index 7a306ac..d34c4c9 100644 --- a/src/pages/docs/component-demos.tsx +++ b/src/pages/docs/component-demos.tsx @@ -324,7 +324,7 @@ const demos: Record = { 'Open actions and select an item.', MenuDemo ), - 'Toast and Sonner': defineDemo( + 'Toast and Toaster': defineDemo( 'Try Toast', 'Dismiss transient feedback from the viewport.', ToastDemo diff --git a/src/pages/docs/heading-bodies.ts b/src/pages/docs/heading-bodies.ts index 14a6f18..0df46cb 100644 --- a/src/pages/docs/heading-bodies.ts +++ b/src/pages/docs/heading-bodies.ts @@ -196,7 +196,7 @@ export const headingOverrides: Readonly< 'component-page-contract': "Every component page in this section follows the same shape — purpose, install and import, live examples, anatomy, state model, keyboard and accessibility, styling and tokens, an API reference grounded in the real prop types, edge cases, and related pages — so once you've read one you know how to navigate the rest. Pages link out to the specific @askrjs/ui and @askrjs/themes subpath exports they cover rather than the package roots, which keeps import statements copy-pasteable. If a page has no @askrjs/ui subpath listed, that's not an oversight: it means the component genuinely has no headless counterpart and ships only as a themed component.", 'experimental-families': - 'A handful of component families are marked experimental — Combobox and Command, Calendar and Date Picker, Native Select and Input OTP, and Drawer and Sheet — and all four share the same reason: none of them has an @askrjs/ui headless primitive backing them. They exist only in @askrjs/themes today, which means their behavior and markup are more likely to change shape as a headless version gets built out. Treat their APIs as less stable than the rest of the catalog and expect follow-up releases to adjust them.', + 'Combobox and Command, Calendar and Date Picker, and Native Select and Input OTP are experimental themed controls without a dedicated headless family. Sheet is also marked experimental, but it composes the supported Dialog behavior and adds theme-owned side positioning. Check each page for its actual behavior owner and stability.', 'headless-and-themed-layers': "Askr's component system is split into two packages that ship separately: @askrjs/ui, which owns interaction behavior, focus handling, and ARIA wiring with no visual opinion at all, and @askrjs/themes, which owns the default look, CSS tokens, and a styled component catalog built on top of it. When a headless primitive exists — Button, Dialog, Select, and similar — the themed version literally imports and wraps it rather than reimplementing behavior, so keyboard handling and state management stay in one place. Reach for @askrjs/ui directly when you're writing your own design system from scratch, or @askrjs/themes when you want a shipped, styled result.", 'package-boundaries': @@ -211,7 +211,7 @@ export const headingOverrides: Readonly< 'install-and-import': 'Install `@askrjs/ui` for the headless behavior and `@askrjs/themes` for the styled defaults. Pull Accordion pieces from `@askrjs/ui/accordion` (headless) or `@askrjs/themes/accordion` (styled), and Collapsible pieces from `@askrjs/ui/collapsible` or `@askrjs/themes/collapsible` — the two components ship as separate subpaths in both packages, not one combined module.', 'keyboard-and-accessibility': - 'Triggers in both components carry `role="button"` semantics with `aria-expanded` and `aria-controls`, and respond to Enter or Space to toggle open state — behavior the `COLLAPSIBLE_A11Y_CONTRACT` and `ACCORDION_A11Y_CONTRACT` constants codify directly. Accordion goes a step further, wrapping its panels in `role="region"` and giving triggers arrow-key roving focus governed by `orientation` and `loop`, since a set of triggers benefits from list-style navigation between them. Collapsible skips that roving-focus layer entirely, since managing a single trigger leaves nothing to navigate across.', + 'Triggers expose aria-expanded and aria-controls and support keyboard activation. Accordion additionally coordinates focus among its triggers according to orientation and loop; Collapsible owns a single disclosure. Test the rendered relationships, focus movement and open state through those public components.', 'live-examples': 'The example composes the themed Accordion and Collapsible directly. Once it is running in your own app, open devtools and watch `data-state` and `aria-expanded` change as you toggle an item — those attributes are the styling and testing contract, so confirm your markup produces them before layering custom CSS on top.', purpose: @@ -259,7 +259,7 @@ export const headingOverrides: Readonly< purpose: 'None of Alert, Badge, Empty, Skeleton, Spinner, or Stat ships a headless counterpart in `@askrjs/ui` — each lives only in the themes package, since their behavior amounts to static markup and styling rather than anything complex enough to justify a separate logic layer. Stat is the odd one out in a different way: it has no dedicated `@askrjs/themes/stat` subpath, so you reach it through the shared `@askrjs/themes/components` barrel instead of importing it on its own.', 'related-pages': - "See Progress for a determinate alternative to Spinner's indeterminate look, and Toast and Sonner for time-boxed feedback instead of a persistent inline Alert.", + "See Progress for a determinate alternative to Spinner's indeterminate look, and Toast and Toaster for time-boxed feedback instead of a persistent inline Alert.", 'state-model': "None of these six own any state — every one of them is presentational, driven entirely by the props you pass on each render, and that includes Stat. Alert accepts an `onDismiss` callback for a caller-owned dismiss button, but the decision to mount or unmount the alert is still yours; Alert doesn't track its own visibility internally.", 'styling-and-tokens': @@ -274,7 +274,7 @@ export const headingOverrides: Readonly< 'install-and-import': "Import behavior from `@askrjs/ui/alert-dialog` and styling from `@askrjs/themes/alert-dialog`, both real subpaths. `AlertDialogAction` and `AlertDialogCancel` really are `DialogClose` under alias names with no logic of their own, and `AlertDialogOverlay`/`AlertDialogTitle`/`AlertDialogDescription` pass straight through to Dialog's parts — but `AlertDialogTrigger` and `AlertDialogContent` aren't bare re-exports; both wrap the underlying Dialog part with alert-dialog-specific dismissal guarding, so don't assume every piece in this tree is a zero-logic alias just because most of them are.", 'keyboard-and-accessibility': - 'The a11y contract calls for both a primary action and a cancel action to be present — `ALERT_DIALOG_A11Y_CONTRACT.ACTION_REQUIREMENTS` flags both as required, so don\'t ship an alert dialog with only a single button. `aria-labelledby`/`aria-describedby` from `DialogTitle`/`DialogDescription` are wired up the same way Dialog does it, but `role="alertdialog"` is not applied automatically — the component defaults to Dialog\'s own role, so set `role="alertdialog"` on `AlertDialogContent` explicitly if you want assistive tech to treat it as an interruption needing an explicit response rather than a plain dialog.', + 'Provide both a primary action and a cancel action. DialogTitle and DialogDescription supply accessible naming and description. Set role="alertdialog" on AlertDialogContent when the interaction requires an interrupting confirmation, and test focus placement, keyboard activation and cancellation in the composed tree.', 'live-examples': "Structure mirrors Dialog: `AlertDialog` wraps an `AlertDialogTrigger` and a portal containing `AlertDialogContent`, `AlertDialogTitle`, and `AlertDialogDescription`. The part that's actually distinct is the footer — pair `AlertDialogCancel` with `AlertDialogAction` so there's always an explicit way to back out alongside the destructive action.", purpose: @@ -335,13 +335,13 @@ export const headingOverrides: Readonly< 'install-and-import': "`asChild` and `ref` aren't separately imported — they're just props defined on every component's prop union, e.g. `ButtonProps = ButtonNativeProps | ButtonAsChildProps`. `VisuallyHidden` is a normal component import: `import { VisuallyHidden } from '@askrjs/ui'`.", 'keyboard-and-accessibility': - 'When `asChild` renders onto a non-natively-interactive element (a `
` or `` standing in for a button), the component compensates by adding `aria-disabled` and `tabindex="-1"` in place of the native `disabled` attribute, and by ensuring the element is keyboard-focusable — this is exactly the branch `BUTTON_A11Y_CONTRACT.DISABLED_ATTRIBUTES.asChild` documents. `VisuallyHidden` content stays in the accessibility tree and the tab order (when focusable) even though it\'s clipped from the visual viewport.', + 'Disabled native controls use the disabled attribute. When asChild targets an element without native disabled behavior, verify aria-disabled, tab order and suppressed activation. VisuallyHidden keeps its content available to assistive technology while removing it from visual layout.', 'live-examples': '`` renders an anchor with button press semantics instead of a nested ``, which keeps the DOM valid and avoids nested interactive elements. `Loading results` renders text that\'s readable by screen readers but not visible on screen, commonly paired with a spinner or icon-only control that needs an accessible name.', purpose: "Two patterns repeat across almost every component in @askrjs/ui: the `asChild` prop for rendering onto a caller-supplied element instead of the component's default tag, and a shared `Ref` type for forwarding refs through that polymorphism. This page covers both, plus VisuallyHidden, the one component whose entire purpose is an accessibility utility rather than an interactive widget.", 'related-pages': - "See Interaction Policies for how each component's `*_A11Y_CONTRACT` documents the asChild-vs-native attribute differences shown here. See Icon Contract for another small, focused accessibility convention that layers on top of components like Button.", + 'See Interaction Policies for native and asChild behavior and how to test the rendered attributes. See Icon Contract for accessible icon labeling.', 'state-model': "Neither `asChild` nor ref forwarding carries state — they're purely about which DOM element a component's behavior attaches to. State-bearing props like `checked` or `value` work identically regardless of which branch of the union you're in.", 'styling-and-tokens': @@ -366,7 +366,7 @@ export const headingOverrides: Readonly< 'state-model': "Avatar tracks one real piece of state: `AvatarLoadingStatus`, typed as `'idle' | 'loading' | 'loaded' | 'error'`, exposed through `AvatarImage`'s `onLoadingStatusChange` callback so you can react to load failures yourself. Item's state is purely presentational — `active?: boolean` and `variant?: 'default' | 'outline' | 'muted'` control appearance, but Item doesn't manage selection or interaction state; if a row needs to respond to clicks or track selected-ness, that logic lives in your own component or in whatever interactive element you put inside it.", 'styling-and-tokens': - "Both components mark their internal parts with `data-slot` hooks — `data-avatar`, `data-avatar-image`, `data-avatar-fallback` for Avatar per its `AVATAR_A11Y_CONTRACT` — which is the surface you should target with CSS rather than reaching into internal structure. Item's `size` prop (`'default' | 'sm' | 'xs'`) and `variant` prop map to `data-size`/`data-variant` attributes that drive gap, padding, and background changes off the theme's general `--ak-space-*` scale — not the separate `--ak-density-*` family, which is scoped to form-control sizing rather than list rows — so a compact `Item` list still lines up visually with the rest of the theme's spacing rhythm, just not through that specific token family.", + 'Target the published data-slot and data-state hooks rather than private child structure. Item size and variant attributes select theme spacing and presentation; inspect the resulting DOM and styles when composing a custom theme.', }, '/docs/components/breadcrumb-and-pagination': { anatomy: @@ -522,11 +522,11 @@ export const headingOverrides: Readonly< 'install-and-import': "These primitives live in @askrjs/ui: `import { FocusScope, DismissableLayer, VisuallyHidden } from '@askrjs/ui'`, or import them individually from `@askrjs/ui/focus-scope`, `@askrjs/ui/dismissable-layer`, and so on if you want narrower imports. They're building blocks other components — Dialog, Popover, Dropdown — already use internally, so you'll often meet them indirectly before you ever import one yourself.", 'keyboard-and-accessibility': - "FocusScope's behavior is described by `FOCUS_SCOPE_A11Y_CONTRACT`, which declares `trapped`, `loop`, and `restoreFocus` as its supported features — trapping tab order inside the scope, wrapping focus at the edges, and returning focus to wherever it came from on unmount. VisuallyHidden's contract declares its strategy as `visually-hidden-but-screen-reader-visible` rendered on a `span`, which is exactly what you want for screen-reader-only labels that shouldn't take up layout space.", + 'FocusScope exposes trapped, loop and restoreFocus behavior. Verify Tab containment, edge wrapping and focus restoration on teardown through the rendered tree. VisuallyHidden provides labels that remain available to assistive technology.', 'live-examples': 'A typical modal composes FocusScope and DismissableLayer directly around its content: FocusScope traps tab order inside the dialog and restores focus to the trigger on close, while DismissableLayer listens for the escape key and outside pointer events to close it. Dialog itself is built this way internally, which is why wrapping your own overlay content in the same two primitives gets you the same behavior without writing a single keydown handler.', purpose: - "Askr's headless primitives are built to be composed rather than configured — instead of a dozen props controlling every visual detail, you assemble small, focused pieces like FocusScope and DismissableLayer around your own markup. Accessibility isn't bolted on afterward; each primitive ships a documented contract (role, ARIA attributes, data hooks) that you can inspect directly in its `*_A11Y_CONTRACT` export.", + 'Compose the public primitives around explicit markup. Their supported contracts are component props, observable roles and ARIA relationships, keyboard behavior and focus ownership. Test those outcomes in your own composition.', 'related-pages': 'See ARIA and Ref Utilities for the lower-level attribute and ref helpers these primitives build on, and Focus and Dismissal for a deeper look at how FocusScope and DismissableLayer are used across overlay components specifically.', 'state-model': @@ -610,7 +610,7 @@ export const headingOverrides: Readonly< purpose: "Trapping focus and blocking interaction with everything behind it is what Dialog does at its core, forcing the user to either act or dismiss before continuing. Reach for it when a task needs undivided attention — a form, a confirmation step, a detail view — and letting the user ignore it and keep scrolling would be the wrong call. When the content is purely informational and doesn't need to block the rest of the page, Popover is the better fit.", 'related-pages': - 'Alert Dialog reuses this exact component tree under the hood for the case where you specifically need a confirm/cancel action pair. Drawer and Sheet are also Dialog under a different name in the themed layer, just with slide-in positioning instead of a centered panel. If what you actually want is a non-modal floating panel, see Popover.', + 'Alert Dialog reuses Dialog behavior for confirmation flows. Sheet adds themed edge positioning while retaining the Dialog state, focus and dismissal model. Use Popover for a floating panel with its own interaction model.', 'state-model': "A dialog's open state is just a boolean, driven by `open`, `defaultOpen`, and `onOpenChange` on the root `Dialog` component. `modal` controls whether interaction with the rest of the page is blocked while it's open — leave it on unless you have a specific reason to allow background interaction. There's no separate loading or error state baked in; anything beyond open/closed is left to whatever you render inside `DialogContent`.", 'styling-and-tokens': @@ -618,24 +618,24 @@ export const headingOverrides: Readonly< }, '/docs/components/drawer-and-sheet': { anatomy: - '`DrawerContent`/`SheetContent` are `DialogContent` under a different name, so they inherit the same overlay-plus-content structure: `DrawerTrigger` opens it, `DrawerPortal` mounts it outside the normal DOM flow, `DrawerOverlay` is the backdrop, and `DrawerTitle`/`DrawerDescription` supply the accessible name and description. Swap the `Drawer` prefix for `Sheet` and the story is identical.', - api: 'The full Dialog prop surface is inherited: `DrawerContent`/`SheetContent` take `forceMount`, `role`, `onEscapeKeyDown`, `onPointerDownOutside`, `onInteractOutside`, and `onDismiss`; `DrawerTrigger`/`SheetTrigger` and `DrawerClose`/`SheetClose` take standard button-like props. There are no drawer- or sheet-specific props like `side` or `size` in the current release.', + 'Sheet composes Sheet, SheetTrigger, SheetPortal, SheetOverlay and SheetContent with SheetTitle, SheetDescription and SheetClose. Root, trigger, portal, overlay and close reuse Dialog; the themed content and text parts provide Sheet-specific slots.', + api: 'Sheet accepts the Dialog root props open, defaultOpen, onOpenChange and modal. SheetContent adds side="top" | "right" | "bottom" | "left", defaulting to right, and forwards its remaining props to DialogContent. SheetHeader and SheetFooter provide layout slots.', 'edge-cases': - "If your design calls for a genuine edge-anchored panel with its own slide transition, treat Drawer and Sheet as a naming convention today, not a distinct component — you're responsible for the positioning CSS and any width/height constraints. Because both names compile to the same module as Dialog, mixing `Drawer` and `Sheet` imports in one tree carries no behavioral risk, but it also means renaming one to the other later is a no-op beyond your own class names.", + 'Test the chosen side at narrow widths and with long content. Sheet relies on Dialog for focus and dismissal; side changes presentation rather than creating another open-state owner. Provide an accessible title and an explicit close action.', 'install-and-import': - "Headless control for these components comes from `@askrjs/ui/dialog` — no separate `@askrjs/ui/drawer` or `@askrjs/ui/sheet` package exists. Themed usage is different: import from `@askrjs/themes/drawer` and `@askrjs/themes/sheet`, which compile to the same output as `@askrjs/themes/dialog` under the hood. The only visible difference is naming — you'll get `Drawer`/`Sheet`-prefixed exports such as `DrawerContent` and `SheetTrigger` instead of `Dialog`-prefixed ones.", + 'Import Sheet and its parts from @askrjs/themes/sheet or the supported @askrjs/themes/components aggregate. Its headless behavior comes from @askrjs/ui/dialog. For 0.5, migrate retired Drawer aliases to the corresponding Dialog components; use Sheet when its side positioning is intended.', 'keyboard-and-accessibility': - "Because the implementation is Dialog's, Escape-to-close, focus trapping, and the `dialog`/`alertdialog` role handling all carry over unchanged — `DialogContentOwnProps`' `onEscapeKeyDown`, `onPointerDownOutside`, and `onDismiss` hooks are available under the Drawer/Sheet names too. Don't assume drawer-specific screen-reader behavior (like announcing a direction of entry) exists; it doesn't yet.", + 'Sheet inherits Dialog focus and dismissal behavior. Verify Escape, outside interactions, accessible title and description, and return focus through your composed trigger and content. The side prop does not add gesture or directional screen-reader semantics.', 'live-examples': - "A Drawer or Sheet example looks exactly like a Dialog example with the names swapped: a root component wrapping a trigger and a portal-rendered content block with title, description, and close affordances. There is currently no `side` prop or edge-anchored slide-in animation distinguishing them visually from a centered modal — that's the clearest way to see the alias in practice.", + 'Open the Sheet example and inspect its edge positioning and accessible title. Set side="left" on SheetContent to move it to the left edge, then close it and verify focus returns to the trigger.', purpose: - "Drawer and Sheet are themed components for panel-style overlays — a drawer sliding in from an edge, a sheet covering more of the viewport — but as shipped today they're both exported as direct aliases of `Dialog` under `@askrjs/themes`, with no headless `@askrjs/ui` primitive of their own. That's why this page is marked experimental: the names exist to signal intent in your markup, but the underlying behavior, positioning, and CSS are currently identical to Dialog.", + 'Sheet provides a themed edge panel built on Dialog behavior. Its content adds side positioning and Sheet-specific slots, while Dialog remains the owner of open state, focus and dismissal.', 'related-pages': - 'See Dialog for the primitive Drawer and Sheet are currently built from, including its full controlled-state and dismissal-handling API. See Alert Dialog for the confirmation-flow variant of the same underlying pattern.', + 'See Dialog for the underlying open-state, focus and dismissal model, and Alert Dialog for confirmation flows.', 'state-model': - "Open state follows Dialog's shape exactly: `open`, `defaultOpen`, `onOpenChange`, and `modal` on the root component. There is no drawer-specific state, like which edge it's anchored to, because that behavior hasn't been built out separately from Dialog yet.", + 'Open state is owned by the Dialog-derived Sheet root through open, defaultOpen and onOpenChange. The SheetContent side prop is presentation input and does not create another state store.', 'styling-and-tokens': - "There's no dedicated `drawer.css` or `sheet.css` in the default theme — both ride on the same overlay CSS and `data-state`/`data-slot` attributes as Dialog. If you want an actual slide-in panel from an edge, you'll need to add that positioning and transition CSS yourself against the Dialog-derived markup, since the shipped styles only render it as a centered modal.", + 'The default overlay stylesheet targets data-slot="sheet-content" and data-side for edge positioning. SheetHeader, SheetFooter, SheetTitle and SheetDescription expose their own slots; theme tokens control the shared surface, border and motion presentation.', }, '/docs/components/focus-and-dismissal': { anatomy: @@ -652,7 +652,7 @@ export const headingOverrides: Readonly< purpose: "FocusScope and DismissableLayer are the two headless primitives every overlay in @askrjs/ui (Dialog, Popover, Dropdown, Menu, Tooltip, HoverCard, Menubar) is built on. FocusScope handles trapping and restoring keyboard focus; DismissableLayer handles closing on outside interaction. They're exported and usable directly if you're building a custom overlay that isn't already covered by the shipped components.", 'related-pages': - 'See the Dialog, Popover, Dropdown, and Tooltip component pages for how these primitives get composed into full overlays. See Interaction Policies for how to read the `FOCUS_SCOPE_A11Y_CONTRACT` and `DISMISSABLE_LAYER_A11Y_CONTRACT` constants.', + 'See Dialog and Popover for complete overlay composition, Interaction Policies for keyboard and pointer behavior, and ARIA and Ref Utilities for asChild and ref ownership.', 'state-model': "Neither primitive owns open/closed state itself — that stays with the consumer (or the overlay component wrapping them, like DialogContent). FocusScope's props (`trapped`, `loop`, `restoreFocus`) and DismissableLayer's callbacks are configuration, not state; you mount and unmount them in response to state that lives elsewhere.", 'styling-and-tokens': @@ -684,11 +684,11 @@ export const headingOverrides: Readonly< "For components that exist in both packages, the themed version is a thin wrapper: it renders the headless component and attaches classes and data attributes for styling. For components that exist only in @askrjs/themes — Tabs, Sidebar, Card, Badge, Combobox, and others — there's no wrapping happening at all, because there's nothing headless to wrap. Those components implement their own behavior directly inside the themed package.", api: "Headless primitives in @askrjs/ui commonly support an `asChild` prop, which renders their behavior and ARIA attributes onto a child element you provide instead of injecting an extra wrapper node — FocusScope, DismissableLayer, and VisuallyHidden all support it, alongside the interactive controls. Themed components generally don't need `asChild` themselves since they already compose the primitive underneath, but understanding the pattern matters once you start pairing @askrjs/ui directly with your own markup.", 'edge-cases': - "Don't go looking for an @askrjs/ui subpath for every themed component — Tabs, Sidebar, Card, Badge, Combobox, Calendar, Drawer, Sheet, Native Select, and Input OTP have none, and importing `@askrjs/ui/tabs` will simply fail because it doesn't exist. If you need headless behavior for one of these, you'll have to build it yourself on top of the lower-level primitives like FocusScope and DismissableLayer rather than expecting a ready-made export.", + 'Some themed components, including Tabs, Sidebar, Card, Badge, Combobox, Calendar, Native Select and Input OTP, have no matching headless subpath. Sheet uses the existing Dialog family for behavior and adds themed parts; use @askrjs/ui/dialog when composing that behavior directly.', 'install-and-import': "Install both packages together: `npm install @askrjs/ui @askrjs/askr` followed by `npm install @askrjs/themes`. Import the full surface from the package root (`import { Button, Dialog } from '@askrjs/ui'`) when convenience matters more than bundle size, or use per-component subpaths (`import { Button } from '@askrjs/ui/button'`, `import { Button } from '@askrjs/themes/button'`) when you want smaller, more explicit imports.", 'keyboard-and-accessibility': - 'Keyboard handling and ARIA wiring are implemented exactly once, in @askrjs/ui, against a documented contract — Dialog exposes `DIALOG_A11Y_CONTRACT` with its role, `aria-modal`, `aria-labelledby`, and trigger attributes; Select exposes `SELECT_A11Y_CONTRACT` with its roles and `aria-selected` wiring. Themed components inherit this for free when they wrap a headless primitive, which is a big part of why the split exists in the first place. Components without a headless counterpart have to implement their own accessibility from scratch, so scrutinize those more carefully.', + 'Headless UI components own keyboard handling, focus and ARIA relationships. Themed wrappers reuse that behavior. Verify the actual role, labeling, selection and focus outcomes for your composition, especially when using a styling-only component that has no headless counterpart.', 'live-examples': "`@askrjs/ui/button` gives you an unstyled Button with real press handling and keyboard support but no CSS at all. `@askrjs/themes/button` gives you a Button that looks finished out of the box — because under the hood it wraps the exact same headless Button and layers styling and data-attribute hooks on top. Swap one import for the other and the behavior doesn't change, only the appearance does.", purpose: @@ -751,7 +751,7 @@ export const headingOverrides: Readonly< 'install-and-import': 'Two packages cover Input: @askrjs/ui plus @askrjs/askr for the headless behavior, and @askrjs/themes if you also want default styling applied. `Input` and `DebouncedInput` live at `@askrjs/ui/input` in headless form, or you can pull `Input` from `@askrjs/themes/input` to get the same component wired to theme CSS. Since themes re-exports the @askrjs/ui Input rather than layering a separate wrapper around it, props and behavior stay identical no matter which path you import from.', 'keyboard-and-accessibility': - 'Because Input is a real , it inherits full native keyboard and screen-reader behavior with no extra ARIA wiring required from the component. It supports labeling via a