Skip to content

v0.1.3

Latest

Choose a tag to compare

@cixzhang cixzhang released this 04 Jul 14:58
6d57c8d

@astryxdesign/core

Breaking Changes

  • DropdownMenu, ContextMenu, MoreMenu: removed the hasAutoFocus prop. Menus now always focus their first item on open (the correct APG menu-button behavior). Previously hasAutoFocus={false} left the menu keyboard-unreachable and undismissable — the prop existed only as an escape hatch for documentation previews, which no longer need it.

New Features

  • Calendar: weekStartsOn now also accepts a three-letter day name ('sun''sat', case-insensitive) in addition to the numeric 06, so the starting day is self-documenting at the call site (e.g. weekStartsOn="mon"). Numbers keep working unchanged. Adds an exported DayOfWeekName type and a normalizeDayOfWeek helper (#2843)
  • CheckboxInput/Switch/Slider/RadioList/CheckboxList/SegmentedControl/Tokenizer/PowerSearch: disabledMessage prop shows a tooltip explaining the disabled state (#3509)
  • DateInput can now be used inside InputGroup with shared addon styling and group label/description/status ARIA wiring (#3520).
  • DateTimeInput: add a timePlaceholder prop to customize the time-portion placeholder (previously hardcoded to "Select a time" with no override). placeholder continues to control the date portion; the focused typing hint (e.g. "e.g., 2:30 PM") is unchanged (#2729)
  • Toolbar, TabList, and SegmentedControl now show an ephemeral "← → to navigate" hint on first keyboard focus, teaching sighted users that arrow keys navigate within the group.
    Adds a showcase block and Storybook story for useKeyboardHint.
  • Export themeProps (and its ThemeProps/ClassProps/ClassValue/ThemeDataAttributes types) from @astryxdesign/core/utils, so packages building on core can generate the stable astryx class + data-* attribute surface through the public API instead of reaching into core internals.
  • MultiSelector can now be used inside InputGroup as a decorated single-line control, sharing the group label, description, status, and connected border treatment (#3520).
  • Typeahead, DateInput, DateRangeInput, DateTimeInput, and TimeInput: disabledMessage prop shows a tooltip explaining the disabled state on hover and keyboard focus, keeping the control focusable via aria-disabled while activation stays blocked (#3509)
  • Popover: expose hasLightDismiss and add hasEscapeDismiss so consumers can opt out of outside-click and Escape dismissal for explicit-dismiss surfaces like onboarding coachmarks. usePopover accepts the same new hasEscapeDismiss option; with it off, no handler is registered on the shared Escape stack so the key falls through untouched (#3287)
  • Selector/MultiSelector: disabledMessage prop shows a tooltip explaining the disabled state (#3347)
  • Selector can now be used inside InputGroup as a decorated single-line control, sharing the group label, description, status, and connected border treatment (#3520).
  • Stack/HStack/VStack: add padding, paddingInline, paddingBlock (spacing-scale inner padding) and isScrollable (overflow: auto) props; StackItem also gains isScrollable. These match the existing padding/isScrollable props on Card, LayoutContent, and LayoutPanel, so common frame layouts no longer need inline style={{}} for padding or the flex scroll-region pattern.
  • Standardize layout component sizing props. Add maxWidth/minHeight to Stack, Grid, and Center (matching Section/Card), migrate Layout/LayoutHeader/LayoutFooter/LayoutPanel sizing to the shared SizeValue type, and drop redundant xstyle/className/style re-declarations on Stack, StackItem, and Layout. No runtime behavior change.
  • Add a plugin-contributed right-click context-menu system to Table. Right-clicking a column header or a row shows a menu of actions aggregated from every enabled plugin (instead of the browser's generic menu).
  • TabList is now a single tab stop with arrow-key navigation between tabs (roving tabindex) — Arrow keys move focus, Home/End jump to the ends, disabled tabs are skipped, and focus wraps. This does not change the semantic roles (still <nav>/aria-current); the full tablist/tab/tabpanel conversion is tracked separately in #3335. Reference (#3343).
  • TextInput/NumberInput/TextArea/FileInput: disabledMessage prop shows a tooltip explaining the disabled state (#3509)
  • Add InputGroup compatibility for TimeInput (#3520)
  • TreeList now implements the full WAI-ARIA APG Tree View keyboard pattern. Roving tabindex places a single tab stop on the treeitem rows (defaulting to the selected item or the first enabled row), and arrow keys move focus in visible order: ArrowDown/ArrowUp step between visible rows (skipping disabled), ArrowRight expands a collapsed parent then enters its first child, ArrowLeft collapses an expanded parent or moves to the parent row, and Home/End jump to the first/last visible row. Enter and Space activate the row's action (or toggle a parent without its own action), and typeahead moves focus to the next row whose label matches the typed characters. Each treeitem now also exposes aria-level, aria-posinset, and aria-setsize. Builds on the interim keyboard-expandable toggle in (#3344). Part of the accessibility & keyboard-management program (#3343).
  • Add InputGroup support for Typeahead (#3520)
  • Add useAnnounce — an accessibility hook that speaks messages to screen readers through persistently-mounted, visually-hidden polite/assertive live regions. Because the regions are created once (not together with their content), announcements are reliable. Wired into Typeahead/BaseTypeahead to announce result counts and "no results found" during search, which were previously silent (#3343).
  • Add useKeyboardHint — shows an ephemeral "← → to navigate" badge anchored to the focused item when a composite widget first receives keyboard focus. Teaches sighted keyboard users that arrow keys navigate within a roving-tabindex group. Renders in the top layer (popover="manual") with CSS anchor positioning so it is never clipped by overflow containers. Auto-dismisses on first arrow press, timeout, or blur; does not re-show for that instance. aria-hidden (visual-only; screen-reader users already hear the role).
  • Add useTypeahead — a first-character (type-to-focus) search hook, and wire it into DropdownMenu. Typing a letter jumps to the next menu item whose label starts with it; repeated presses of the same letter cycle through matches; the buffer resets after 750ms; disabled items are skipped. The hook is additive and collection-agnostic (composes with useListFocus/useGridFocus via onMatch), so menus/listboxes gain APG typeahead which astryx previously lacked (menus-11, infra-14) (#3343).
  • useListFocus gains opt-in roving-tabindex ownership (hasRovingTabIndex), caret-aware arrow handling that leaves keys to nested text inputs and contenteditables (hasCaretGuard), RTL horizontal navigation (isRtl), a hasHomeEnd toggle, and orientation: 'both' for four-arrow navigation. Toolbar now uses it — it is a single tab stop and no longer steals the caret from a toolbar text input or composer (#3343).
  • Add VisuallyHidden — an accessibility primitive that renders content in the accessibility tree while hiding it visually. Use for accessible names on icon-only controls, aria-live announcement regions, and supplementary screen-reader context. Renders a <span> by default; use as for block/live-region elements (#3338).

Fixes

  • Announce file selection, page changes, and multi-select count via the live-region hook (#3343)
    FileInput now announces successful file selection, Pagination announces page changes, and MultiSelector announces selection-count changes, all through the shared visually-hidden polite live region so these previously-silent surfaces are audible to screen-reader users.
  • Avatar: an avatar with no name or alt is now decorative (role="presentation" + aria-hidden) instead of being announced with the meaningless generic name "Avatar". When named, the inner <img> uses an empty alt so the accessible name isn't announced twice (once by the role="img" wrapper, once by the image) (#3343).
  • Breadcrumbs: auto-detected current breadcrumb now places aria-current="page" on the item's content element (link/button/span), matching the explicit isCurrent path, instead of on the outer <li>. When the last breadcrumb is a link, the anchor itself now carries aria-current so screen readers announce it as the current page (#3343).
  • Breadcrumbs: BreadcrumbItem now forwards remaining BaseProps (id, aria-, role, event handlers, data-) to the underlying <li> element. Previously these props were accepted by TypeScript but silently dropped at runtime.
  • ButtonGroup: remove the invalid aria-orientation attribute from the role="group" element, which was flagged by axe (aria-allowed-attr). Orientation is still reflected via data-orientation and drives keyboard navigation and styling, so behavior is unchanged. Also fixes Schedule, which reuses ButtonGroup internally.
  • Calendar: cross-month keyboard navigation now resolves the focused date from the machine-readable data-date attribute instead of parsing the localized aria-label with new Date(). Previously, month-boundary arrow keys and PageUp/PageDown silently stopped working in non-English locales (e.g. fr-FR, ja-JP) where the label was unparseable (#3343).
  • Calendar: the month view now uses a valid ARIA grid structure — the weekday names are columnheader cells inside the grid, each week is a row whose direct children are gridcells, and week-number cells are rowheaders. Arrow-key navigation now lands on the correct dates when some days are disabled (via min/max/dateConstraints): moving up/down keeps the true 7-column geometry and skips disabled days to the same weekday, instead of shifting to the wrong weekday. (#3343)
  • Carousel, Lightbox: keyboard focus is no longer trapped on invisible or unmounted edge controls. Carousel's scroll left/right buttons, when at an exhausted edge (or when there is no overflow), were hidden with opacity: 0/pointer-events: none but stayed in the tab order, so keyboard users could focus invisible controls (WCAG 2.4.7); they are now disabled in that state (still mounted, removed from the tab order and a11y tree). Button-driven scrolling also now respects prefers-reduced-motion (uses behavior: 'auto' instead of hardcoded 'smooth'). In Lightbox gallery mode, the Prev/Next buttons previously unmounted at the range ends, so advancing onto the first/last item removed the focused control and dropped focus to <body>, dead-ending keyboard navigation; they now stay mounted and become disabled at the boundaries so focus stays within the dialog and arrow-key navigation keeps working (#3343)
  • Chat composer: the message input now uses role="combobox" when triggers (mentions, slash commands) are configured, and stays a plain role="textbox" otherwise. Combobox attributes (aria-expanded, aria-haspopup, aria-controls, aria-activedescendant) are only valid on a combobox, so applying them to a textbox was flagged by axe (aria-allowed-attr). (#3343)
  • ChatMessageList: add an isStreaming prop that marks the role="log" region aria-busy while an assistant message streams in. Previously the polite live region re-announced the accumulating partial text on every token; with isStreaming set for the duration of a stream, screen readers wait and announce the completed message once (#3343).
  • CheckboxInput: stop setting a redundant aria-checked="mixed" on the native <input type="checkbox"> for the indeterminate state. The native indeterminate DOM property (which browsers already map to aria-checked="mixed") is authoritative; the extra attribute could desync from or override the native state (#3343).
  • CheckboxList: the checkboxes are now wrapped in a role="group" named by the field label via aria-labelledby (and associated with the description/error), instead of a flat list with an orphaned label htmlFor. Screen-reader users now hear the group's name and context (#3343).
  • CheckboxListItem: drop the invalid aria-checked from the list row. aria-checked is not allowed on role="listitem" (axe: aria-allowed-attr); checked state is already conveyed by the row's inner checkbox. This also fixes Markdown task lists, which render task items through CheckboxListItem. (#3343)
  • Citation: linked number-variant badges keep their accent-muted background (previously rendered transparent), and the badge now uses the secondary text color (#3508)
  • Citation: only apply role="doc-noteref" on the linked (anchor) form. On a plain unlinked span the role is not permitted (axe: aria-allowed-role), so it is omitted there while the aria-label still names the citation. (#3343)
  • Selector/MultiSelector/Typeahead: the highlighted option is now scrolled into view during keyboard navigation, so arrow keys no longer move the highlight off-screen in long lists (matches CommandPalette) (#3343).
  • CommandPalette: the empty state ("No results") no longer flashes when typing further characters into an already-empty search. The empty state stays mounted for the full duration of the pending search instead of briefly unmounting and re-appearing.
  • ContextMenu: Escape now closes the menu even when it was opened without auto-focus (e.g. table row menus), via a document-level Escape listener instead of one that only fires when focus is inside the menu. Focus is also restored to the previously focused element on close, instead of falling to <body>. Escape during IME composition is ignored (#3343).
  • ContextMenu: can now be opened on touch (long-press) and by keyboard. A long-press (500ms, cancelled by a 10px finger move) opens the menu at the touch point — previously context menus were unreachable on iOS Safari, which never fires contextmenu on long-press. A keyboard-initiated contextmenu (Shift+F10 / the Menu key), whose coordinates are (0,0), now anchors the menu to the trigger's box instead of the viewport corner (#3343).
  • DateInput/DateTimeInput: the date field's calendar popover can now be opened from the keyboard with ArrowDown / Alt+ArrowDown (APG combobox), not just by clicking. DateTimeInput's time input no longer uses a hardcoded English aria-label="Time" — it defaults to "{label} time" (tied to the field label and localizable) and accepts an explicit timeLabel prop (#3343).
  • DateRangeInput: the preset sidebar is now a labeled role="group" of action buttons instead of a role="listbox" of role="option" buttons. The listbox/option roles announced a single-tab-stop listbox that contradicted the actual Tab-between-buttons interaction (no listbox keyboard model existed). The currently-applied preset is marked with aria-current rather than aria-selected (#3343).
  • docs.mjs: resolve the package directory with fileURLToPath so component docs work on Windows, where URL.pathname yields drive-letter paths like /D:/... that made --list silently print nothing and single-component lookup crash with ENOENT (#3331)
  • DropdownMenu: pressing Tab in an open menu now closes it (APG menu-button pattern) and returns focus to the trigger, instead of leaking focus into the page while the menu stayed open (#3343).
  • Escape now dismisses only the top-most open layer instead of closing every open layer at once — a popover or menu nested inside a Dialog no longer closes both on a single Escape press. Also guards against IME composition: pressing Escape to cancel a CJK/IME composition inside a Dialog or popover no longer closes the overlay (#3343).
  • Vertically center the optional/required indicator in FieldLabel. The label and optionalRequired styles never set an explicit lineHeight, so both fell back to line-height: normal (~1.2), producing mismatched line boxes (~16.8px for the 14px label vs ~14.4px for the 12px "∙ Required" text). With alignItems: center the smaller indicator centered within its shorter box and rendered visually high relative to the label.
  • FileInput: aria-describedby, aria-required, and aria-invalid now sit on the focusable role="button" control instead of the visually-hidden tabIndex={-1} file input that never receives focus. Screen-reader users now hear the field's help text, required state, and error state. The hidden native input is also marked aria-hidden since it is not focusable (#3343).
  • useFocusTrap: the focusable-element detection now includes contenteditable, media with controls, iframe, and an open <details>'s <summary>, and excludes elements hidden via display:none/visibility:hidden or inside inert/hidden subtrees. Previously a trapped surface whose only interactive content was (e.g.) a contenteditable composer could let Tab escape (infra-8).
  • Grid no longer writes grid-template-columns/grid-auto-rows as raw inline styles. Track templates now use StyleX dynamic styles (CSS-variable indirection), so consumer xstyle overrides — including responsive @media overrides — take effect instead of being defeated by inline styles.
  • NumberInput, DateInput, and DateTimeInput now set aria-invalid="true" and announce a short message (e.g. "Invalid number" / "Invalid date" / "Invalid time") via a visually-hidden role="alert" live region while the currently typed input is unparseable, instead of only dimming the text color and then silently reverting the value on blur. Screen-reader users now get feedback that their entry was rejected rather than silence, and the invalid state is no longer signaled by color alone (WCAG 3.3.1 Error Identification, 1.4.1 Use of Color). The revert-on-blur behavior is unchanged. (#3343)
  • InputGroup: grouped TextInput and NumberInput controls now include both the group label and their own label in the input name, while preserving the group's description and status associations (#3343).
  • InputGroup: the group is now named by the field label via aria-labelledby instead of a duplicated aria-label, and the label no longer carries an orphaned htmlFor (its inputId was never handed to a child). Uses the Field isGroupLabel/labelID support (#3343).
  • Kbd: the component is no longer entirely aria-hidden. It now exposes a spoken accessible name (e.g. "Command + K") built from screen-reader-friendly key labels, while the visual glyphs (⌘, ⇧, ↵, …) are hidden from assistive tech. Previously any shortcut communicated only via Kbd — including CommandPalette's footer hints — was invisible to screen-reader users (#3343).
  • Use Kbd for arrow-key navigation hints and space the hint farther from focused controls so outlines stay visible.
  • Layer/popover entry animations now honor prefers-reduced-motion. The shared layer slide/scale keyframes (used by DropdownMenu, Popover, HoverCard, Tooltip, Selector, and other popover surfaces) and the useEntryAnimation presets disable their keyframe animation under prefers-reduced-motion: reduce, so layers appear instantly instead of translating/scaling in (#3343).
  • useLayer: the popover toggle event listener is now removed when the layer element detaches or when the handler identity changes (a new onHide), instead of accumulating stale-closure listeners on the same element. This prevents duplicate/stale onHide firing over a layer's lifetime (#3343).
  • Link: hovering now shifts the link color via the hover tint (color-mix with --color-tint-hover, matching Slider/Switch/RadioList). This gives always-underlined links (hasUnderline) a visible hover affordance they previously lacked — their underline never changed on hover — without altering the default link's underline-on-hover behavior (#2852)
  • Link: external links (isExternalLink) now include visually-hidden "(opens in new tab)" text so screen-reader and cognitive-load users are told about the new-tab context change — previously only a decorative aria-hidden icon signalled it. The text is overridable via the new newTabLabel prop for localization (#3343).
  • DropdownMenu/ContextMenu: the role="menu" container now has an accessible name — DropdownMenu names it from the trigger's label, and ContextMenu exposes a menuLabel prop (default "Context menu"). ContextMenu also no longer places aria-haspopup="menu" on its role-less, non-focusable trigger wrapper, where it conveyed nothing useful to assistive tech (menus-13, menus-15).
  • NavHeadingMenu: onClick-only items (rendered without an href) now activate on Enter and Space. Previously these role="menuitem" elements had no keyboard activation, so keyboard and screen-reader users could focus them but not trigger them (#3333)
  • Pagination: coerce pageSize to a positive integer so 0/NaN/negative values no longer crash the dots variant (RangeError: Invalid array length) or render Infinity/NaN page counts; the Table pagination plugin applies the same guard since it computes totalPages independently (#3372)
  • usePopover: add a role ('dialog' | 'none') and isModal option so listbox and menu popups no longer announce a false modal dialog. Selector, MultiSelector, BaseTypeahead, PowerSearch, DropdownMenu, TabMenu, and the Chat mention menu now expose their own listbox/menu role instead of being wrapped in role="dialog" aria-modal="true" while focus stays on the trigger. Genuine dialog popovers are unchanged (#3343).
  • ProgressBar: determinate progress now uses role="progressbar" instead of role="meter". meter is for static gauges (disk usage, battery) that screen readers do not treat as live-updating task indicators; a progress bar conveys task completion and should be announced on update. Indeterminate progress was already progressbar (#3343).
  • RadioList: give an unselected radio group a deterministic keyboard tab stop. When focus enters a group with no selected value, the group now normalizes the entry point — first radio when tabbing forward, last radio when tabbing backward — matching the ARIA radio-group pattern. A selected value keeps its native tab stop, and moving between radios inside the group is never redirected. (#3390)
  • RadioList: the group is now named via aria-labelledby pointing at the field label element, and the label no longer carries an orphaned htmlFor (it pointed at an id no radio used, so clicking it did nothing and the group was double-labeled). Field/FieldLabel gain optional labelID and isGroupLabel props to support grouping controls (#3343).
  • Table, CodeBlock, and Markdown: the keyboard-focusable scroll containers now use role="group" instead of role="region". region is a landmark, so multiple same-named scroll regions on one page (e.g. several tables labelled "Table") triggered axe landmark-unique. group keeps the label and keyboard focusability without creating duplicate landmarks. (#3343)
  • CodeBlock/Table/Markdown: overflowing scroll regions are now keyboard-focusable (tabIndex, role="region") so keyboard users can scroll long code and wide tables. CodeBlock's Copy button no longer collapses the block when clicked, and is no longer nested inside the collapsible header's role="button" (#3343).
  • SegmentedControl: a disabled segment (including when the whole control is disabled) is no longer a keyboard tab stop. Previously the selected segment kept tabIndex={0} while disabled, so it was focusable but silently dead — arrow keys and activation did nothing (#3343).
  • SegmentedControl: keep the radiogroup reachable by Tab even when the current value matches no item (or the selected item is disabled). Previously a stale/unmatched value left every segment at tabIndex={-1}, so the whole control dropped out of the tab order. The first enabled segment is now promoted to the tab stop (#3343).
  • Selector/MultiSelector: Delete and Backspace now clear the value from the focused trigger when hasClear is set, so clearing a selection is no longer mouse-only. The clear button was already keyboard-reachable; this adds the keyboard shortcut path (#3343).
  • Selector/MultiSelector (hasSearch): the popup's search input is now the combobox — it carries role="combobox", aria-expanded, aria-autocomplete="list", aria-controls, and aria-activedescendant, so screen readers announce the highlighted option as ArrowUp/Down move it. Previously the search input was a bare searchbox while aria-activedescendant stayed on the (now-unfocused) trigger, leaving highlight changes silent. In hasSearch mode the trigger is a plain button that opens the listbox rather than a second combobox (#3343).
  • Selector, MultiSelector: the combobox trigger is now keyboard-focusable (tabIndex=0 when enabled). Previously it was tabIndex=-1, so keyboard and screen-reader users could not open or operate the control in the default (non-search) mode. The Clear button is now keyboard-reachable too (#3320)
  • useLayer: guard showPopover()/hidePopover() behind a feature check so overlays degrade gracefully instead of throwing a TypeError on browsers without the Popover API (Safari <17, Firefox <125) (#3343).
  • SideNavItem: for split-action items (a collapsible item with its own link/action), aria-current="page" now sits on the focusable link instead of the non-interactive wrapper <div>, so screen readers announce the current page on the actual navigation element (#3343).
  • SideNavHeading: label the product icon link with the heading text so it has an accessible name. When superheadingHref, headingHref, and a menu were all set, the icon link to the heading href rendered with no text or aria-label, so axe flagged it under link-name and screen readers announced an unlabeled link. (#3343)
  • Skeleton: the loading placeholder is now aria-hidden by default (it's decorative — the surrounding region conveys the loading/busy state) and its pulse animation is disabled under prefers-reduced-motion: reduce. The aria-hidden default can be overridden by consumers (#3343).
  • Spinner: the rotation animation now slows substantially under prefers-reduced-motion: reduce (matching ProgressBar) instead of spinning unconditionally. The role="status" "Loading" announcement still conveys busy state (#3343).
  • Table: proportional() and pixel() no longer throw when called from a React Server Component. The Table barrel carried a 'use client' directive that marked the pure column utilities as client functions; the directive now lives only on the component modules, and Table.doc.mjs documents which parts of the data-driven API are server-safe (#3457)
  • TabList: stop rendering aria-orientation on the nav element. aria-orientation is not an allowed attribute on the navigation role, so it triggered a critical axe aria-allowed-attr violation (also surfaced via Toolbar and ToolbarEdgeCompensation stories that reuse the same DOM). The orientation prop still drives arrow-key navigation and the keyboard hint. (#3343)
  • TimeInput/DateTimeInput: parse dotted meridiems correctly. "2:30 p.m." was silently accepted as 02:30 and "12 a.m." as noon because the meridiem-detection regex did not allow the dots that the AM/PM regexes accept; hasMeridiem is now derived from those regexes so they cannot drift (#3462)
  • Toast: keyboard users can now reach and manage notifications. Pressing F6 jumps focus into the toast viewport (the newest toast's first control, or the container). Dismissing a toast that holds focus now hands focus to a remaining toast — or restores the element focused before entering the viewport — instead of dropping to <body>. Auto-hide timers also pause while the window is blurred and resume on focus, so a toast no longer silently expires while you're in another window or tab (#3343)
  • Toast: remove the invalid aria-modal attribute from the notifications viewport. aria-modal is only valid on role="dialog" / alertdialog, so declaring it on the role="region" viewport was flagged by axe (aria-allowed-attr). Because the viewport renders on every page, this surfaced the violation across the whole app (#3343).
  • Tooltip: satisfy WCAG 1.4.13 (Content on Hover or Focus). Tooltips can now be dismissed with Escape while visible, and stay open when the pointer moves from the trigger onto the tooltip surface (a short hover bridge). useLayer context render props gain onMouseEnter/onMouseLeave on the layer container to support hoverable overlays.
  • TopNavHeading: give the logo an accessible name when it links to a destination. The logo image is decorative, so a logo wrapped in headingHref produced an unnamed link (axe: link-name). It is now labelled from heading (or a new optional logoLabel prop for logo-only headings). (#3343)
  • TreeList: parent rows can now be expanded and collapsed from the keyboard. Any item with children renders a real focusable toggle button with aria-expanded, so expansion no longer requires a mouse — previously items without an onClick/href had no focusable control at all (#3343).
  • Typeahead/Tokenizer: close the results dropdown when focus leaves the input (e.g. tabbing away), matching the existing outside-click and Escape dismissal. Previously the menu could stay open after the trigger lost focus.
  • ContextMenu and NavMenu heading menus now support first-character typeahead (type a letter to jump to the matching item), via the shared useTypeahead hook — matching DropdownMenu. MoreMenu inherits it through DropdownMenu (#3343).
  • useListFocus (menu/toolbar keyboard navigation): arrow keys, Home, and End now skip disabled items instead of stalling on one whose .focus() silently no-ops. This unfreezes keyboard navigation in NavHeadingMenu and Toolbar/ButtonGroup when a disabled control is present. The default item selector also now matches menuitemradio/menuitemcheckbox (#3343).

Documentation

  • Document Carousel's hasEdgeFade and padding props, supported by the component but missing from the props table, docsZh, and docsDense (same omission previously fixed for Text's justify and ToggleButton's isIconOnly). Also correct the Fade edges anatomy row to optional, since the mask is suppressible via hasEdgeFade (#3332)
  • Fix stale references in the core README: rename the "XDS CLI" section to "Astryx CLI", correct component names to their current bare exports (Layout, AppShell, TopNav, SideNav), and update remaining XDS brand mentions to Astryx.
  • Link: document type="inherit" for inline links. The value was already supported (forwarded to Text, which renders font-size/line-height: inherit), but undocumented — clarified the type prop JSDoc, added an inline-link example, and added regression tests covering the inherit/default-body behavior (#2927)
  • MetadataListItem: its docs page preview now renders inside a MetadataList wrapper with realistic defaults (#3318)

Other Changes

  • Consolidated 5 inline visually-hidden style blocks into the shared VisuallyHidden primitive (Button, Link, ProgressBar, Switch, TextArea); no behavior change.
  • Rename the Field labelElementID prop to labelID, matching the (part)ID naming convention used by the sibling props (inputID, descriptionID, messageID). The disambiguation between "the id applied to the label element" and inputID ("the control the label points at") now lives in the prop JSDoc rather than the name. Also renamed on FieldLabel and updated the RadioList/CheckboxList/InputGroup consumers. No behavior change. (#3343)
  • useGridFocus gains hasRovingTabIndex, handleFocus, and isRtl, matching the useListFocus API. Calendar now uses useGridFocus to own its roving tab stop, removing the unpublished useCalendarRovingTabindex hook.
  • SegmentedControl now uses the shared useListFocus roving-tabindex primitive instead of its inline keyboard handler and tab-stop repair effect; no behavior change.
    The component's ~60-line inline ArrowLeft/Right/Home/End handler and useIsomorphicLayoutEffect tab-stop repair are replaced by useListFocus({hasRovingTabIndex: true, wrap: true, orientation: 'horizontal'}), which owns the single roving tab stop, skips disabled radios, wraps, handles Home/End, and repairs the stop on mount/disable. Selection-follows-focus (APG radiogroup) is preserved via a container onFocus handler that selects the focused radio's value.
  • Move @stylexjs/stylex from dependencies to a required peerDependency (^0.18.3). A consumer who authors their own StyleX now shares a single runtime with astryx — resolution dedupes to their own install in both browser and Node — instead of silently getting a second copy on version drift. An incompatible StyleX version is now flagged at install (npm errors, pnpm/yarn warn) instead of resolving silently. Consumers who don't author StyleX are unaffected: the runtime is still required to render astryx components and is auto-installed by npm 7+ and pnpm.
  • New TableContextAction type and an optional contextMenuActions field on HeaderCellRenderProps / BodyCellRenderProps. Plugins append their actions inside the existing transformHeaderCell / transformBodyCell transforms; the table concatenates them across plugins (never overridden), the same way styles are merged.
  • The cell components (TableHeaderCell / TableCell) own the menu wrapper and render it around their own content via the ContextMenuActions prop, so the cell controls how the wrapper interacts with padding / content sizing — and row menus work without invalid <tr> nesting. Actions group with dividers and show a checkmark for the active item; when none are contributed, the native browser menu passes through.
  • contextMenuActions accepts either an array or a getter (() => TableContextAction[]); the getter is resolved lazily when the menu opens, so plugins with state-derived actions don't build arrays on every render. useTableSortable uses a getter so its checked/clear state always reflects the latest sort.
  • First contributor: useTableSortable adds "Sort ascending / Sort descending / Clear sort" on sortable headers.
  • TabList now uses useListFocus's built-in roving-tabindex support (hasRovingTabIndex) instead of a hand-rolled tab-stop repair effect. The hook owns the single tab stop — stamping tabindex 0/-1, repairing it on mount and as stops mount/unmount or toggle disabled, and keeping it in sync after clicks/programmatic focus via handleFocus on the nav. Individual Tabs still render tabIndex={isSelected ? 0 : -1} as the initial source of truth, which the hook's repair preserves. No behavior change.
  • useTreeFocus gains hasRovingTabIndex + handleFocus for internal tab-stop management; TreeList drops its inline activeId state and lets the hook own the roving tab stop (#3488).

@astryxdesign/cli

New Features

  • Add a hidden astryx blog command that reads the blog over the site's RSS feed and prints a post's plaintext (.txt) variant. The command is not shown in --help or the manifest and always reads from the canonical site origin.
  • Component discovery is now package-ownership aware: --package scoping, source resolution for integration components, and package-qualified JSON listings.
  • Strict config + integration v1 schema (integrations, issuesUrl, hooks.postCodemod) and new @astryxdesign/cli/integration export.
  • File-based codemod API (createCodemod/createConfigCodemod) with the @astryxdesign/cli/codemod export and integration codemod discovery in upgrade.
  • component, template, and upgrade now print a one-line non-blocking warning when a configured integration has validation issues, pointing to validate-integration.
  • Add a Kanban Board page template: color-coded status columns, draggable task cards with priority tags, and board toolbar.
  • Add frame-first layout guidance: new astryx docs layout topic (shell choice, region budgets, app archetypes, cards-vs-rows policy, responsive contracts), layout rules in the generated agent cheat sheet, and layout anti-patterns in docs principles.
  • Add a v0.1.3 config codemod that migrates astryx.config layout.components to experimental.xle.components.
  • Add v0.1.0 codemods for migrating declare module "@xds/core/..." type augmentations and .xds-* / [data-xds-theme] / @layer xds-theme CSS surfaces to their @astryxdesign/astryx-* equivalents.
  • Introduce the Project configuration API as the single entry point for reading resolved project config, components, templates, codemods, and issue routing, replacing loadConfig. Misconfigured integrations are now skipped with a warning during upgrade instead of hard-failing, and a new --skip-codemod flag lets you re-run past a failed codemod.
  • Add a Shell page-template category to the CLI: Top Nav, Side Nav, and Shell Nav app-shell scaffolds (#3245, #3246, #3247)
  • Static template authoring API (createPageTemplate/createBlockTemplate) with the @astryxdesign/cli/template export and type-driven, package-scoped template discovery.
  • Swizzle can now copy integration-owned components, rewrites escaping imports to the owning package, and routes maintainer feedback through config and integration issue URLs.
  • astryx theme build --watch: rebuild a theme automatically whenever the source file changes, until interrupted with Ctrl-C. Removes the manual re-run step (and the stale-CSS confusion that comes with forgetting it) from the theme-authoring loop. Each rebuild runs in a child process so a build error is contained and the watcher keeps running. Not supported with --json. (#3375)
  • Add the validate-integration command and integration issue model for checking an Astryx integration package's manifest and contributions.
  • XLE app-component registration moved into validated config under experimental.xle.components (object form), replacing the unvalidated layout.components read.

Fixes

  • Align the CLI error-code type declarations with the runtime error codes (add the missing ERR_AMBIGUOUS_TEMPLATE declaration).
  • Correct the doctor theme-wiring hint to reference the real astryx.theme config field (was xds.theme) and update the agent-docs check wording to say "Astryx".
  • Update the API/CLI parity harness for the package-qualified component --list shape, and make the component API reject a non-string name with a clean error instead of throwing.
  • The XDS-prefix drop codemod now runs as a mandatory v0.1.0 upgrade step, so upgrading from 0.0.x rewrites prefixed imports (useXDSTheme, XDSButton, XDSIconRegistry, ...) to their bare names alongside the @xds/_ → @astryxdesign/_ scope rename.
  • upgrade now runs core codemods before loading config, so a config codemod can repair an otherwise-invalid config; dry-run reports a fixable config and suggests the command to apply it.

Documentation

  • Blockquote: add "With Attribution" and "Testimonials" examples (#3385)
  • DateTimeInput and DateRangeInput: add example blocks so their docs pages have populated Examples sections and playground links (#2724)
  • Add copyable example blocks to 46 component docs pages that previously showed only a hero visual and an empty Examples section (#3481)
  • HoverCard: give the "Link Preview" example an interactive Link trigger so there is something to hover over (#2728)
  • Lightbox: add Gallery, Video, and Zoom examples and fix the playground preview (#3301)
  • Remove lingering references to the removed gap-report feature and swizzle gap flags; docs now reflect swizzle's maintainer feedback link.
  • Tab: add an interactive example showing icon and selectedIcon on the Tab docs page (#2765)
  • ToggleButtonGroup: add a vertical example block showing orientation="vertical" with single- and multi-select groups (#2707)

Other Changes

  • Integration codemod and template-doc loading now use the shared module-loader util instead of duplicating the jiti/import logic.
  • Extract the shared module-loading + conventional-file-discovery helpers used by config and integration loading into one internal util (no behavior change).
  • Remove the standalone gap-report command. Swizzle now prints a short maintainer feedback link instead of filing issues.
  • Load and validate user-authored config, integration, codemod, and template modules through one shared module loader; create* factories are now type-only and validation happens at load.
  • Remove the obsolete xds config-surface migration codemod and unify config codemod execution on the shared (file, api) runner used by integration codemods.

@astryxdesign/build

Fixes

  • Use pnpm build in the prepack script so publishing no longer fails the devEngines package-manager check (#3564).

Contributors

Thanks to everyone who contributed to this release:

Full Changelog: v0.1.2...v0.1.3