v0.1.9 is an all-patch release — no breaking changes, so no codemods required.
New Features
- Avatar: add a
tooltip?: string | booleanprop for a name-on-hover tooltip. Omitting it (ortrue) shows the avatar'snameon hover and keyboard focus; a string shows that text instead (no need to wrap inTooltip);falsedisables it. Avatar owns the tooltip via the existing Tooltip hook, so there's no extra wrapper DOM. Because this adds a default tooltip to every existing named Avatar, settooltip={false}when you supply your ownTooltip/HoverCardoverlay. The rootaria-label(alt || name) is unchanged; the default name tooltip is visual-only (noaria-describedbydouble-announce), while a custom string tooltip is exposed as a description. Decorative avatars (noname/alt) get no tooltip. (#4164) - BreadcrumbItem gains a
menuprop that turns a crumb into a menu trigger for switching between sibling destinations. It accepts the same item API as DropdownMenu/MoreMenu/ContextMenu (aDropdownMenuOption[]array or composed item children), so existing menu-item definitions drop into a breadcrumb with no rewrite. The item components are also re-exported underBreadcrumb*aliases. - Calendar: make the today/selected day-cell ring precisely themeable. The day cell now reflects a compound
markerstate (today-only/today-in-range) that maps 1:1 to the treatment actually drawn, sodefineTheme({components: {'calendar-day': {'marker:today-only': {...}}}})targets exactly those states without over-matching or needing a:not()exclusion. Default rendering is unchanged. - Calendar: add a dedicated
astryx-calendar-navtheme target for the prev/next month-nav buttons, so consumers can theme the nav controls (color, radius, per-direction, disabled edge) without reaching every Button via the globalastryx-buttonhandle. Reflectsnav(prev/next) and thedisabledstate as data attributes. - ChatComposer: make custom inputs first-class.
useChatComposerContext()and its types are now public, so any input in theinputslot can readvalue/onChange/onSubmit/canSend/placeholder/isDisabledand drive the shell's send button. Inputs can register a focus control oninputControlRefso click-to-focus works for any input shape (not justcontenteditable/textarea); the shell keeps a DOM-query fallback for uninstrumented inputs. - ChatComposerInput: add an
onKeyDownseam so consumers can host platform- or app-specific key handling — e.g.preventDefault()Enter to insert a newline on a touch keyboard, or submit on Cmd/Ctrl+Enter. Enter also no longer submits mid-IME-composition. - CommandPalette: add a dedicated
astryx-command-palette-group-headingtheme target on the group heading, so consumers can theme just the heading (e.g. its padding or typography) viadefineThemeinstead of a fragile structural selector. The group root keeps its ownastryx-command-palette-grouptarget. - DateInput gains a
formatprop for the committed date value, reusing Timestamp'sformatvocabulary so the same literal renders the same date shape in both components. Named values aredate_long(the default, "March 21, 2026"),date("Mar 21, 2026"),date_weekday("Wed, Mar 21, 2026"), andsystem_date("2026-03-21"); a(value) => stringfunction is also accepted for custom output. Thedate_longdefault is byte-identical to DateInput's previous long-month rendering, so existing usage is unchanged. This also extends Timestamp with two new shared members,date_longanddate_weekday, giving the two components full value parity on the date-only formats. Formatting applies only to the committed value, never to text being typed. - Add an
elevationprop to configurable surfaces — Card, ClickableCard, SelectableCard, Button, IconButton, ButtonGroup, and Banner take the full'none' | 'low' | 'med' | 'high'scale; ChatComposer takes'none' | 'low'. Defaults preserve today's appearance (noneeverywhere except ChatComposer'slow), so nothing changes unless you opt in. (#4146) - OverflowList: add
maxVisibleItemsto cap the number of visible items (the ceiling partner tominVisibleItems) andmaxRowsfor bounded multi-row wrapping — items wrap onto up to N rows, then collapse into the overflow indicator. Both props are optional and default to off, so single-line behavior is unchanged. See #4176. - Add
useTableRowStatus, a plugin that prepends a narrow column
signaling per-row status. - Thumbnail: add
showRemoveOnprop —'hover'(default) reveals the remove button on hover or keyboard focus and keeps it visible on touch;'always'shows it at rest. - Table tree: add an optional expand-all/collapse-all header control (#4142)
useTableTreeStatenow returns an aggregateisAllExpandedstate (true/false/'indeterminate') and threadsexpandAll/collapseAllintotreeConfig.useTableTreeDatagains ahasExpandAllControlprop: when set, it renders an expand-all/collapse-all toggle in the tree column header, wired to that state, so consumers no longer need to hand-roll external buttons. Flat data stays a full no-op. This is the first affordance folded in fromuseTableRowExpansionas part of converging the two tree plugins. - TreeList: add a dedicated
astryx-tree-list-chevrontheme target for the expand/collapse toggle, so consumers can theme the chevron (color, per open/closed state) viadefineThemeinstead of reaching it through the functional[data-tree-toggle]attribute. Reflects the open/closed state as adata-stateattribute (expanded/collapsed); the functionaldata-tree-togglehook is unchanged. - TreeList: add a stable
astryx-tree-list-guidetheme target on the hierarchy guide (connector) line elements, so consumers can recolor or hide the guides throughdefineTheme(e.g.backgroundColor, ordisplay: 'none'to hide them) instead of hiding the built-in connectors and reimplementing them with unlayered CSS. - TreeList: add a dedicated
astryx-tree-list-item-labeltheme target on the item's label text, so consumers can theme just the label (e.g. bold the selected item's label) viadefineThemeinstead of a fragilebutton:not([data-tree-toggle]) > spanstructural selector. Reflects the row'sselectedstate as adata-selectedattribute on the label. - CLI: full API coverage for the
build,swizzle,layout, andvalidatecommands — each is now scriptable through the./apibarrel with the CLI as a thin parse → API call → render wrapper.buildgains--jsonoutput. Behavior is unchanged for existing command usage. (#4302)
Fixes
- AvatarStatusDot: pair each variant with a distinct built-in shape — success stays a filled dot, neutral renders as a ring, error gets a minus bar — so status no longer relies on colour alone (WCAG 2.1 SC 1.4.1, #4143). A rendered
iconreplaces the shape glyph at sizes where icons fit; themes can target the new stableastryx-avatar-status-dot-glyphclass and itsdata-shapeattribute — a stroked inline<svg>painted from the dot'scurrentColor. - Button: link-rendered buttons (
href) now exposearia-busywhile loading, matching the<button>branch. Previously an interruptible loading link showed the spinner and announced "Loading" but carried no machine-readable busy state. - Calendar only marks in-month date cells as today, preventing duplicate today indicators in multi-month views.
- Carousel: slides now expose APG slide semantics (role=group, aria-roledescription="slide", "Slide N of M" labels) instead of anonymous divs.
- Honor
prefers-reduced-motionin ChatToolCalls (chevron rotation, expand/collapse), ChatLayoutScrollButton (pill show/hide), and ChatDictationButton (equalizer bars). - Chat/useChatStreamScroll: the scroll-follow spring now respects
prefers-reduced-motion— locked following,scrollToBottom(), andlock()fall back to the existing instant jump, so the transcript still tracks the bottom without animated travel. Follow-up promised in #3800. - Chat: the composer drawer toggle now references its disclosed content via aria-controls, so assistive tech can navigate from the toggle to the drawer.
- ChatSendButton now forwards
className,style, and pass-through attributes (data-*,aria-*, and other rest props) to the rendered button. Previously these were silently dropped. (#4190) - Chat: tool-call error details are now exposed to screen readers and keyboard users instead of living only in a hover-only title attribute.
- CheckboxInput: the indeterminate mark now uses the
--radius-fulltoken instead of a hardcoded radius, for token consistency. No visual change. - CheckboxList: items with rich (non-string) labels can now provide an accessibleLabel so their checkbox no longer announces as the literal "Checkbox".
- CodeBlock: collapsed code regions are now inert, so keyboard focus can no longer land on the invisible scroll container while collapsed.
- CommandPalette: forward BaseProps pass-through attributes (className, style, xstyle, data-, aria-) to the underlying Dialog. Previously these were silently dropped.
- CommandPalette: the search input (role=combobox) now has an accessible name by default, from the new label prop or the visible placeholder. Previously screen readers announced a nameless combobox.
- DateTimeInput: ArrowDown (and Alt+ArrowDown) in the date field now opens the calendar popover from the keyboard, matching DateInput and the advertised combobox pattern.
- Dialog: modals are now automatically labelled by their DialogHeader title via aria-labelledby, matching AlertDialog. Unnamed open dialogs warn in development.
- Dialog: the entry animation is disabled under prefers-reduced-motion, matching the Layer animation guards.
- Fix Divider rest-prop spread order so consumer-passed HTML attributes cannot overwrite the component's
role="separator"oraria-orientation. - FieldLabel now forwards className, style, xstyle, and pass-through attributes (data-, aria-, event handlers) to the rendered element. Previously these were accepted by the type but silently dropped.
- Field/FieldStatus: status and error messages are now announced through persistent live regions, so they are reliably read by screen readers regardless of when they appear.
- FileInput: the trigger's accessible name now includes the selected filenames, so screen-reader users can review what is attached when refocusing the control.
- Prevent PowerSearch edit popover from closing when selecting multi-select options via Enter (#4245)
- Selector/MultiSelector: PageUp/PageDown now jump the active option to the first/last match while searching, complementing Home/End which stay on text-caret movement.
- HoverCard: popups now expose a named dialog when the new label prop is set, and honest group semantics otherwise. Previously every hover card announced as an unnamed dialog.
- Item: aria-selected is now emitted only when the item's role permits it (option, tab, treeitem, grid cells), removing invalid ARIA from plain list items.
- MobileNav: the toggle button now exposes
aria-expandedand references the nav drawer viaaria-controls, so screen-reader users can tell whether the drawer is open and what the button controls. (#3721) - MultiSelector: searching within the options popover now announces the number of matching results ("3 results" / "No results found") to screen readers, mirroring Selector and Typeahead. Previously filtering happened silently.
- SideNav/TopNav: collapsed heading popovers no longer wrap their menus in a modal dialog, and the heading button is no longer an invalid child of the menu.
- Slider now clamps controlled values to its minimum and maximum before positioning the thumb and exposing ARIA values.
- Slider: required sliders now convey the required state to screen readers through the thumb's accessible description (aria-required is invalid on the slider role).
- Make the CLI's
.mjssources fully strict-typecheckable (checkJs + JSDoc)
Annotated the entire CLI package sotsconfig.strict.json(fullstrictcheckJsoversrc,bin,scripts,docs, and the emittedtemplates) reports zero errors — down from 1717. Fixes are JSDoc-only: no runtime logic changed,.mjsstays.mjs. Strict checking also surfaced and corrected several type-contract drifts: theupgrade.runresponse type (declared adepsUpdatedfield the command never emits, and omitted the realintegrations/filesChanged/transformsApplied/errors), registered the emittedtheme.list/theme.add/layout.*response types in the--jsonenvelope union, and addedcategory?toReferenceSectionin core's docs types (reference docs already emit it). - Table: the selection plugin accepts getRowLabel so row checkboxes announce which row they select, instead of an undifferentiated "Select row".
- Text: an explicit
sizenow overrides the font-size of a themedtype. Thesizeclass lived in a lower cascade layer (astryx-base) than a theme's per-type font-size rule (astryx-theme), so<Text type="supporting" size="xsm">silently kept the type's size. Themes now re-emit the size classes in the theme layer sosizewins as documented. - Thumbnail: replace the hover box-shadow on interactive tiles with the same
::afteroverlay treatment ClickableCard and SelectableCard use. All three now tint on hover with--color-overlay-hover(and--color-overlay-pressedon press), so interactive feedback is consistent across the card family. - Thumbnail/TopNavMegaMenuFeaturedCard: images without alt text are now explicitly decorative instead of silently empty-alt, matching Avatar's handling.
- Thumbnail: the overlaid remove button now uses a fixed
--color-overlayscrim with an--color-on-darkicon instead of adapting to the image's luminance, so it has reliable contrast on any image. - Tokenizer: adding and removing tokens (including Backspace on an empty input) is now announced to screen readers. Previously tokens appeared and disappeared silently.
- TopNav: TopNavMegaMenu triggers now expose aria-controls, and the panel is a labeled group instead of an invalid modal-dialog-wrapped menu.
- TopNav: TopNavMenu popups now expose proper menu semantics (no modal dialog wrapper) with the full APG keyboard pattern (roving tabindex, arrow keys, typeahead).
- Align two
--jsoncontract shapes with what the CLI actually emits - Register all emitted response types in the
--jsonenvelope union
Three response types were defined, exported, and emitted by commands but never added toCLIAnyResponse— the union thatjsonOut()type-checks payloads against:component.full,component.detail.blocks, andupgrade.status. Because their discriminators were missing from the map,jsonOut('upgrade.status', …)(and the two component variants) were rejected by the type-checker, and their payload shapes weren't actually being validated.build.helphad no response type at all. Added aBuildHelpResponsetype and wired all four into the union so every--jsonenvelope the CLI can emit is now type-checked against a declared shape. - Type
detectPackageManagerhonestly soastryx doctor's "no lockfile" branch is reachable
detectPackageManagerreturns'npx'as the sentinel for "nothing detected", but its return type only listed'yarn' | 'pnpm' | 'bun' | 'npm'. Type-checkers therefore treateddoctor'spm !== 'npx'guard as a dead comparison — the "No lockfile detected — defaulting to npm/npx" message looked unreachable and was at risk of being "cleaned up". The return type is nowPackageManager | 'npx'and detection narrows via a shared type predicate, so the guard is honest and the branch is preserved. - Drop the dead
cwdparameter fromgetLatestVersion
checkForUpdatecalledgetLatestVersion(cwd)and the JSDoc advertised acwdparameter, but the function takes no arguments — it only reads the$ASTRYX_LATEST_VERSIONenv var, so the passedcwdwas silently ignored. Removed the phantom parameter and its doc so the signature matches the behavior. No functional change to the update-nudge output. - Scope the
sourceresolve condition to @astryxdesign packages in withAstryx
withAstryxset webpack'sconditionNamesto['source', …]globally, which resolved any dependency shipping asourceexport to its raw TypeScript — not just Astryx packages. Third-party deps that ship asourceexport (e.g.lexical, pulled in by the new RichTextEditor lab component) were then fed untranspiled.tsthrough Next's babel and failed on syntax likedeclareclass fields.
Documentation
- docs(Tokenizer, PowerSearch): document and test the existing startIcon prop
Both components have shipped a workingstartIcon?: ReactNode | IconTypeprop for a while (PowerSearchforwards it verbatim to the internalTokenizer, and it's already exercised in Storybook), but neither's.doc.mjsdocumented it and neither had test coverage — so it was invisible toastryx component <Name> --dense, the docsite props table, and anyone (human or AI) relying on those as the source of truth. No behavior change; adds the missing props-table entries (en/zh/dense) and colocated tests confirming the icon renders and forwards correctly.
Other Changes
- Compact per-row status signal (error, warning, unread, etc.) without a
dedicated status column: a colored status dot by default, or an icon when provided. getStatus(item)maps a row to{color, icon?, label?}, ornullfor no
indicator.coloraccepts a semantic status name (success/error/warning/accent/red/green/etc.) mapped to a theme token, or a raw CSS color as an escape hatch.iconrenders the status as a shape signifier instead of the dot, which is
more accessible than color alone when multiple statuses coexist.labelsupplies the accessible name.- Memoize
getStatuswithuseCallbackfor a stable plugin identity. swizzle.copypayloads always includepackageandusesStyleX(both covered by tests), butSwizzleCopyResponse.datadidn't declare them — the call site cast the payload toRecord<string, unknown>to sidestep the mismatch. Added both fields to the type and dropped the loose cast so the payload is type-checked.- The error
suggestionsshape was declared as{name, reason}(reason required) in the JSON envelope / API error contract, but some call sites emit bare{name}(e.g. candidate component names on swizzle). Introduced a single canonicalSuggestiontype (reason?optional) and referenced it everywhere so the contract matches the emitted data.
Contributors
@AKnassa, @bhamodi, @cixzhang, @ejc3, @freddymeta, @HelloOjasMutreja, @humbertovirtudes, @josephfarina, @kentonquatman, @Kevinjohn, @potatowagon, @saadpocalypse, @yyq1025
Full Changelog: v0.1.8...v0.1.9