Repository navigation
Releases: meta/astryx
Release list
Astryx v0.7.4
Astryx 0.7.4 — every published @astryxdesign/* package ships at this version.
OLD_VERSION=0.7.3 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.4 @astryxdesign/cli@0.7.4 # plus every other published @astryxdesign/* package you use, at 0.7.4
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New features
BottomSheetSwitchernow gives every step its own native<dialog>while preserving one scrim, focus containment, scroll locking, Escape handling, and focus return. The public props and defaults are unchanged. Its forwardedrefnow follows the dialog that hosts the active step. (#7720)
Fixes
-
Busy controls now announce “Loading” once through the shared polite announcement channel instead of competing with a second live Spinner. This covers
TextInput,TextArea,Selector,MultiSelector,CheckboxInput,CommandPalette,DateInput,DateRangeInput,DateTimeInput,TimeInput,FileInput,DropdownMenuSubMenu,Thumbnail,ChatMessageList,ComplexSelector,Typeahead,Tokenizer, andBaseTypeahead. Announcements made inside an open modal stay inside that modal. (#7920, #7930, #7939, #7942, #7948, #7950, #7954, #7956) -
ChatComposerInputannounces a trigger menu’s visible loading state once, while cached results that arrive before the loading row appears stay silent. (#7900) -
When a focused Carousel arrow reaches the end and becomes unavailable, focus moves to the scroll area without undoing the scroll. (#7926)
-
CommandPaletteItemexposes highlighted, selected, and disabled states on its theme target:.astryx-command-palette-item[data-highlighted] { /* … */ } .astryx-command-palette-item[data-selected] { /* … */ } .astryx-command-palette-item[data-disabled] { /* … */ }
Rendering, semantics, and props are unchanged. (#7967)
-
A busy
DateInputkeeps its ordinary appearance and shows its Spinner; only a disabled field is dimmed. (#7968) -
Selector, DropdownMenu, and ContextMenu options inside open, closed, or nested shadow roots now commit when clicked. (#7914)
-
useTypeaheadclears its pending type buffer when its component unmounts or becomes hidden, leaving no timer behind and restoring with an empty buffer. (#7938)
Documentation
BottomSheetSwitcheranatomy now describes the per-step dialog structure. (#7720)
@astryxdesign/cli
Experimental icon sets
Integrations can ship an icon set, projects can select one, agents can find icons by meaning, and copied templates can replace declared icons with the selected set. The feature is isolated under astryx experimental and does not change stable search results or template output unless a project opts in with experimental.iconSet. (#7759)
astryx experimental icon --list
astryx experimental icon "delete"
astryx experimental icon use <set>New features
- Docs, schema fields, and enum values can declare
stability: 'experimental'. The CLI labels experimental commands, keeps experimental docs out of stable search, and tells integration authors when an older supported CLI would drop the descriptor. (#7759) - Docs can render commands for each reader’s package manager with
{@command …},{@pm …}, and{@pm-only …}tokens.integration check --nextcan validate them before their findings become warnings in the next minor release. (#7868) - An integration doc can
replaceorextendan existing guide by route and place a sectionbeforeorafteranother section. Reads name the contributing package, and unresolved placement is reported as a non-failing note. (#7936) astryx doctor --built <path>checks a finished project, output folder, or HTML file for Astryx use and reports source-level purity issues when source is available. It reads files only and does not execute them. (#7886)astryx theme palette generatereceipts now identify the generator and candidate format without changing existing receipt fields. (#7678)- Integration validation reports what examples need to run wherever they are shown, including package availability, exported subpaths, props, browser-only dependencies, assets, and runnable code. These are notes today and become strict warnings in the next minor. (#7891)
- Project agent docs written by
astryx initnow tell agents to runastryx doctor --builtbefore delivering a build when the installed CLI supports it. (#7944)
Fixes
astryx doctor --builtno longer counts type-only imports as shipped Astryx use. (#7945)astryx discoverresults name their source package, and typo-tolerant lookup now matchesastryx search. (#7147)astryx searchfinds family members by their spelled-out names, lists representative members for a family-name query, and orders tied results by fit instead of alphabetically. (#7927, #7955, #7959)astryx integration verifynow reads the tarball filename npm 8 actually writes for scoped packages. (#7933)
Documentation
- Four block templates now say what they show and when to use them. (#7963)
- BottomSheetSwitcher templates describe the new per-step native dialogs. (#7720)
- The CommandPaletteItem showcase describes its rendered command and shortcut rows. (#7967)
- Lab ListInput showcase blocks are discoverable as templates. (#7953)
- Six guide titles now use plainer, consistent sentence case without changing their routes. (#7934)
Themes
Butter, Gothic, Matcha, Stone, and Y2K move their maintained light and dark themes to reviewed named palette stops. Public compatibility palette objects and metadata stay available, and apps can preserve the previous appearance by overriding the affected color, syntax, shadow, and component tokens. The CLI templates ship the same mappings. (#7669, #7670, #7671, #7672, #7675)
Contributors
Thanks to everyone who contributed to this release:
@cixzhang @ernestt @HelloOjasMutreja @imdreamrunner @josephfarina @rubyycheung @vjeux
Full Changelog: v0.7.3...v0.7.4
Astryx v0.7.3
Astryx 0.7.3 — every published @astryxdesign/* package ships at this version.
OLD_VERSION=0.7.2 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.3 @astryxdesign/cli@0.7.3 # plus every other published @astryxdesign/* package you use, at 0.7.3
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New Features
-
DropdownMenuSubMenutakesheaderandfooter, as the rootDropdownMenudoes, so a sub-menu can have a filter field above its rows, or an action such as "New folder" below them, outside the sub-menu's menu itself. With either, the sub-menu's surface (the flyout, or the drilled-in view on a phone) is a modal dialog named by its row: the row saysaria-haspopup="dialog", Tab and Shift+Tab walk the header, the rows and the footer and wrap, Escape returns to the row, and a pointer leaving the flyout closes it only once focus leaves too. AContextMenudoes not show a sub-menu'sheaderorfooter. Without them nothing changes.<DropdownMenuSubMenu label="Move to" + header={<TextInput label="Filter folders" isLabelHidden value={query} onChange={setQuery} />} + footer={<Button label="New folder" onClick={createFolder} />} > <DropdownMenuItem label="Folder A" onClick={() => move('a')} /> </DropdownMenuSubMenu>
Fixes
-
TimeInput's arrow keys ignore a fractional increment instead of writing a malformed time such as "14:30.5":adjustTimereturns the time unchanged for a fractional minute delta (#7823). -
Screen readers now announce a
Buttonthat is first rendered withisLoading(for example after a remount). Its visually hidden loading live region mounts empty and gets "Loading" on the next frame, because screen readers skip a live region that already holds its text when it appears. A Button that becomes loading after it mounts is unchanged, and a Button mounted idle does no extra render. -
ChatComposer: screen readers now announce a status message, errors assertively and warnings politely, once per occasion: a rerender does not repeat it, and a status that clears and then returns is announced again. Before, the status bar mounted as arole="alert"orrole="status"region already holding its text, which screen readers often skip. The message now goes through the shared announcement channel, and the visible bar no longer carries a live role. No API changes. -
ChatSystemMessage: a new chat notice inside aChatMessageListnow has one live region announcing it, the list's polite log, instead of also being a second live region nested in it (WCAG 2.2 SC 4.1.3). Its root no longer forcesrole="status", in either variant, so date and divider rows are no longer presented as status messages, and arolepassed to the component reaches its root. Tests that found a system message by thestatusrole should query it by its text instead. -
Keyboard and screen-reader users can now reach disabled
CheckboxInput,CheckboxListitems,Switch,SliderandFileInput, and hear that they are disabled. Before, they rendered the nativedisabledattribute unless adisabledMessagewas set, so Tab skipped them. Now every disabled control exposesaria-disabled="true"and keeps the Tab stop it has when enabled, while toggling, selecting, dragging, opening the file picker and anyonClickstay blocked, and its value is still left out of form submission. A focused disabled checkbox, switch, slider thumb or file input shows its keyboard focus indicator. No API changes. -
The agent guide that global mode writes to the home folder now tells agents to deliver a page built with Astryx, not only how to find things. Before, agents that found the right template could still build the page with a tool that cannot install packages and deliver a page with no Astryx in it. The guide now tells agents to work in a project folder (creating one if there is none); add
@astryxdesign/coreand@astryxdesign/clito a new project or one without Core, or, in a project that already has@astryxdesign/core, the CLI at the same version; runastryx initthere; follow the same template-first workflow and self-check the project agent docs give; and end by building the project and delivering that build. The guide stays version-free and names only commands every release keeps. The project agent docs are unchanged, and so is what@astryxdesign/core's install script prints: its copy of the shared agent-docs file only gains the same workflow lines.Also released in
@astryxdesign/cli. -
Keyboard and screen-reader users can now reach a disabled
ComplexSelectorand hear that it is unavailable (WCAG 2.2 SC 2.1.1 and 4.1.2). Before, its trigger rendered the nativedisabledattribute, so Tab skipped it and a focused trigger lost focus when disabled; now it exposesaria-disabled="true", keeps focus, shows its keyboard focus outline, and still opens nothing. A press or key on it reaches no handler: not the toggle, not a caller'sonClickoronKeyDown, and not an ancestor's click handler. -
Keyboard and screen-reader users can now reach a disabled
Calendarday orPaginationdot and hear that it is disabled. DisabledTreeListrows' expand toggle and action button, disabledPaginationdots and disabledCalendardays exposearia-disabled="true"instead of the nativedisabledattribute, while clicks, Enter and Space still do nothing and arrow keys still skip disabled days as before. No API changes. -
DateInput: keyboard and screen-reader users can now reach a disabled date field and hear that it is disabled. A disabled or busy field no longer renders nativedisabled: its input and calendar button keep their place in the focus order and exposearia-disabled, while typing and opening stay blocked, and a focused disabled field shows its focus ring. Tests that query a disabled DateInput withtoBeDisabled()or:disabledshould checkaria-disabled="true"instead. -
DateRangeInput: keyboard and screen-reader users can now reach a disabled field or preset and hear that it is disabled. A disabled or busy field and a disabled preset no longer render nativedisabled: the trigger, calendar button and presets keep their place in the focus order and exposearia-disabled, while opening and selecting stay blocked, and a focused disabled field shows its focus ring. Tests that query them withtoBeDisabled()or:disabledshould checkaria-disabled="true"instead (#7866). -
DateTimeInput: keyboard and screen-reader users can now reach a disabled or busy field and hear that it is disabled. It no longer renders nativedisabledon its text, native-picker or touch fields, or on its calendar and clock buttons: they keep their place in the focus order and exposearia-disabled, while typing, picking, opening and a caller'sonClickstay blocked, and a focused disabled field shows its focus ring.TimeInput's native picker button, which shares the time segment and stays out of the tab order, also moves fromdisabledtoaria-disabledand stays inert. Tests that query a disabled DateTimeInput withtoBeDisabled()or:disabledshould checkaria-disabled="true"instead. -
Apps that use Core's prebuilt CSS without a StyleX build step now get the default data colors: the prebuilt CSS defines the canonical
dataVarsdefaults, so compiled imports and raw CSS references paint correctly.Also released in
@astryxdesign/cli. -
Dialog,BottomSheetandBottomSheetSwitcherstay open when a file picker inside them is dismissed. A file input fires a bubblingcancelevent when its picker closes without a choice, and all three took that event for a close request of their own and closed (a switcher closed its whole flow). They now act only on acancelaimed at their own<dialog>, so Escape and platform close requests (the Android back gesture) behave as before. No API changes. -
DropdownMenuRadioGroupnow forwardsref, and its docs explain controlled selection, how to name the group, when the menu closes, and how it composes inside aDropdownMenu. -
astryx initwrites the agent docs at the project root, in files the project commits, so Codex, Gemini CLI, Copilot CLI and Claude Code all read the block from the project root or any folder below it (#7497). It no longer writes agent docs into a project's subfolder, the home folder, or a settings folder in it such as~/.gemini.AGENTS.mdholds the block, created when missing. Before, a project with onlyCLAUDE.mdgot the block only there, so Codex and Gemini CLI never saw it.- Claude Code skips
AGENTS.mdin a folder that has aCLAUDE.mdor.claude/CLAUDE.md, so the Claude Code file the project already has keeps a full copy: rootCLAUDE.md, else.claude/CLAUDE.md. It keeps the copy even when it importsAGENTS.md, because Claude Code does not follow that import in a session started in a subfolder; started at the root, Claude Code then reads two. When both exist, the copy goes in rootCLAUDE.md, which projects commit even when they ignore.claude/, and a block in.claude/CLAUDE.mdis removed. Init creates no Claude file.astryx upgraderefreshes every copy, except in a project at the top of a home settings folder (see the next entry). - Gemini CLI reads only
GEMINI.md: init adds a one-line import,@./AGENTS.md, creatingGEMINI.mdwhen it is missing. It adds none whenGEMINI.mdalready importsAGENTS.md, inline or on its own line. It adds the import even when the project's.gemini/settings.jsonhas Gemini readAGENTS.mdthroughcontext.fileName: Gemini loads those settings only when it starts at the project root, so a session started in a subfolder would otherwise get no copy. Started at th...
v0.7.2
Astryx 0.7.2 updates the stable @astryxdesign/* package set to this version.
OLD_VERSION=0.7.1 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.2 @astryxdesign/cli@0.7.2 # plus every other stable @astryxdesign/* package you use, at 0.7.2
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New Components
-
Promote
DrawerandDrawerHeaderfrom the canary-only Lab package to Core, imported from@astryxdesign/core/Draweror the package root. The stable package now ships the full-height side panel with modal and non-modal presentation, the shared dismissal stack, Dialog'spurposedismissal policy, aDrawerHeaderwhose close button appears when givenonOpenChange, and Dialog-style container padding (--spacing-4by default,paddingprop, or a theme'sdrawerpadding), plus Core documentation and runnable examples. Canary users importing from@astryxdesign/labswitch to@astryxdesign/core/Drawer.Also released in
@astryxdesign/cli.
New Features
-
An integration can set the command its users run the CLI with: a new
cliCommandfield inastryx.integration.*, such ascliCommand: 'acme astryx'. In a project that loads that integration, every command the CLI prints starts with it instead of the package-manager prefix (npx astryx,pnpm exec astryx): next-step hints, errors, suggestions, Doctor fixes,--help, the setup reminder, the agent-docs block, and doc text. If two loaded integrations set different commands, neither is used, and the newcli-commandcheck inastryx doctorwarns. An invalidcliCommandis ignored with aninvalid_cli_commandwarning; the rest of the manifest still loads.
Docs, including an integration's, are now written with bareastryx ...commands, andastryx docsprefixes them for the project when it prints them. JSON output keeps every command bare, as before. The install-time setup reminder from@astryxdesign/coreand@astryxdesign/clinames the command the project's package manager runs, instead of alwaysnpx @astryxdesign/cli.Also released in
@astryxdesign/cli. -
Adds the
@astryx.diffViewer.*keys to theencatalog so the labDiffViewerreads its file-status badges, line counts, line-number button labels, per-line "Added"/"Removed" labels, and empty-state messages from the translation runtime. No existing string changes; locales without a translation fall back to English. -
Itemacceptsalign="first-line", which centers each start and end slot on the label's first line. A small status dot, radio, checkbox, or timestamp beside a wrapping label now stays level with the first line, wherecenterdrops it to the middle of the row andstartleaves it above the line. A slot taller than the first line moves the text down rather than leaving the row, and a one-line row with no description looks the same ascenter.ListItem,RadioListItem, andCheckboxListItemaccept the samealignand pass it to their row. Themes can style these rows through theitemtarget'salignaxis. The default stayscenter, so existing rows don't change. -
i18n catalog: add the English messages used by the Lab CommentComposer for its input, image upload, inline quote, emoji action, cancel action, and retry affordance.
-
i18n catalog: add the English messages used by the Lab CommentThread for replies, reactions, edit history, resolution, errors, comment actions, and the unsaved-draft warning it announces when a dismissal would discard a reply or edit.
-
i18n catalog: add the English messages used by the Lab Comments panel for its region, empty/loading states, pagination, and posting errors.
-
i18n catalog: add the English messages used by the Lab EmojiPicker so locale catalogs and provider overrides can replace its labels, search status, loading state, and group names.
-
List: addedgeto choose where each row's text, dividers, and hover and selection backgrounds sit.padded(default) keeps today's look.flushdrops the rows' side padding, so text and dividers start on the same line as the list header and a heading above, with no container needed.outsetkeeps backgrounds' room around the text while reaching into the padding of a Card, Section, or Dialog, so text lands on the heading's line.bleedreaches rows to the container's edges with square corners, like Table rows in a Card.edgeCompensation="inline"is deprecated in favor ofedge="outset"and keeps working; its rows' outer room is no longer cut off by the list's swipe clip. Runastryx upgradeto rename it. -
Pagination:variant="compact"shows first/last buttons around previous/next whenhasFirstLastistrueand the page count is known. Compact still omits them by default, and theinputvariant still shows them unlesshasFirstLastisfalse. -
reset.css: in Chromium, the dropdown marker of an<input>with alistsits in the middle of the field. Chromium sets the marker on the field's line-height, so above a line-height of 1 it sits low, and the reset makes every field inherit the page's 1.5: the marker sat 3.5px below the middle of a 40px field, and further down on taller lines. Date and time fields, whose calendar icon uses the same part, keep their icon where it was. To keep the old placement, addinput[list]::-webkit-calendar-picker-indicator { line-height: inherit; }after the reset. -
SideNavItem: nested items now span the full width of the nav list, like top-level items, so their hover and selected fill lines up with the rows above them. Before, a nested item started 24px in from the list edge. The indent now sits inside the row, one--spacing-6step per nesting level, so icons and labels stay exactly where they were. The indent area is now part of the nested row, so it shows hover feedback and clicking it activates the item. Content placed directly inside aSideNavItemthat is not itself aSideNavItemis no longer inset by the children group. There is no supported way to keep the old inset look. -
TabMenupasses host attributes and DOM event handlers to its trigger button. The trigger keeps its button semantics, tab stop, and menu disclosure attributes; a caller'sonClickruns after the menu toggle. -
Tables draw a line on every cell edge, not only between rows, on both
Markdownand the RichText surfaces, and the grid closes at the table's rim. A reader following one row across never needed vertical lines, but someone editing a table has to aim at a cell and judge where a selection stops, and the empty 3×3 a toolbar insert produces was three horizontal rules with no sign it had three columns. The line is the same--color-borderalready used between rows, the rim merges with it into one rule rather than doubling at the edge, and both surfaces move together — the read and edit renderings match block for block, more closely than before.Also released in
@astryxdesign/richtext. -
TreeList: nested rows now span the full width of the list, like root rows, so their hover and selected fill lines up with the rows above them. Before, a nested row started at its indent. The indent now sits inside the row, so chevrons, icons and labels stay exactly where they were. The indent area is now part of the row, so it shows hover feedback, and clicking it activates the row: clicking a folder's indent toggles the folder, and clicking a row whose only action is its link anywhere outside its label follows the link, as Enter already did: modifier keys are kept, so Cmd/Ctrl-click opens a new tab, and a middle-click opens one too. Controls in a row's start or end content keep their own clicks, including role-based ones such as arole="switch"element, which no longer also toggle the row or run itsonClick. Guide lines draw above the rows, so a hover or selected fill doesn't break them, and they let clicks pass through to the row underneath. The one exception is a row with keyboard focus: it draws above the guides so they never cut its focus ring, which means that if it is also filled (selected or hovered), its fill covers the guides at that row. Padding set on a row through thetree-list-itemtheme target or an item'sclassName,styleorxstylestill composes with the indent.--tree-list-indentstill retunes the indent step. There is no supported way to keep the old inset rows.
Fixes
-
Allow MetadataList label width without an explicit position while preserving single- and multi-column label defaults (#6310)
-
AppShell: defer hiding the mobile nav's Activity boundary until its close animation finishes (#5701)
AppShellmounts the mobile nav drawer inside aReact.Activityboundary that flipped tomode="hidden"in the same commit as the drawer closing. React'sActivityhides its subtree withdisplay: none !importantsynchronously and re-runs the cleanup of every effect inside it, which cut offMobileNav's own slide-out CSS transition and cancelled its delayeddialog.close()before either could run. The drawer still closed correctly (a safety-net effect force-closed the native dialog on that same cleanup), just with the close animation skipped entirely.AppShellnow holds theActivityboundary visible for a beat after the drawer closes, strictly longer thanMobileNav's own worst-case close delay, soMobileNav's normal close effect gets a real chance to run its transition and its own timeddialog.close()beforeActivityever hides the subtree.MobileNavalso keeps its dialog rendered from the commit that closes it until its delayedclose()has run, instead of relying on the dialog's discretedisplaytransition to do so. A browser that does not hold that transition (WebKit) dropped the dialog todisplay: noneat once while it was still an open modal, so for about 200ms nothing was visible and the page stayed inert. The slide-out itself is unchanged. -
`Aspe...
Astryx v0.7.1
Astryx 0.7.1 updates the stable package set, including RichText's first stable release.
OLD_VERSION=0.7.0 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.1 @astryxdesign/cli@0.7.1 # add each other 0.7.x @astryxdesign/* package you use at 0.7.1
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New Features
-
Chat: mention people and topics by multi-word names with opt-in
hasMultiWordQueryonChatComposerTrigger(#6128)
SethasMultiWordQuery: trueon a trigger whose suggestions contain spaces, and typing@Jordan Lekeeps narrowing the menu instead of closing it at the first space. The query runs until a line break or about 64 characters, typed text is never changed unless a row is picked, and an unmatched multi-word phrase hides the menu unless the trigger setsemptySearchText. Triggers that don't opt in still end their query at a space.For every trigger: while results for the current query are still loading, Enter sends the message as typed and never picks a row from an earlier query. Results that arrive after a send or after the editor clears are dropped, so the menu never reopens over an empty composer. Escape and the arrow keys pass through while the menu is hidden, and IME keystrokes never pick a row.
-
CodeBlockgainsisDiff, a diff layer over anylanguage, and Markdown renders a fenceddiffcode block as tinted diff rows instead of plain text. (#7389)
WithisDiff, lines starting with+sit on a success-tinted row,-on an error-tinted row,@@hunk headers (and a---/+++file-header pair directly before one) on a quiet muted row, and the rest of every line keeps the language's syntax highlighting. The glyph renders in its own column so the code's indentation lines up on every row and stays in the copied text. A changed row's margin reads the change: the tint runs under the line-number gutter too and spans the full scroll width of a long line, and the line number and the glyph take the row's marker colour, while the code keeps the syntax theme's colours. These are five new syntax-theme slots —--color-syntax-added,--color-syntax-removed,--color-syntax-diff-header(row paints, defaulting to--color-success-mutedand--color-error-mutedat 25% strength and to--color-background-muted; a defined syntax theme that leaves the header paint unset gets none, its header rows sitting on its own background in its punctuation colour) and--color-syntax-added-marker,--color-syntax-removed-marker(defaulting to--color-text-greenand--color-text-redin the default syntax theme; a defined syntax theme that leaves them unset keeps its own line-number colour) — so existing and custom syntax themes need no change and may override them.Markdown maps a
diff-<lang>code fence to the layer over<lang>(labelled<lang> diff) and adiffcode fence to the layer over plaintext (labelleddiff), so a diff keeps its header, copy button, disclosure, and a viewport name that still saysdiff.A
diffcode fence that should stay plain text can usetext. A customcomponents.coderenderer is unchanged: it keeps receiving the fence's leading word characters inlanguage(difffor bothdiffanddiff-tsx, as today), so an existinglanguage === 'diff'check still matches, and receives nothing new. -
Clearable inputs: the clear button's tooltip now reads just "Clear" (localized) instead of repeating the field's label, so a long label no longer turns into awkward tooltip copy such as "Clear Assigned engineering partner". The button's accessible name is unchanged and still names the field, such as "Clear Search". Applies to every input that renders the shared clear button, including
TextInput,NumberInput, the date and time inputs,FileInput,Selector,MultiSelector,Typeahead, andTokenizer. Override the new@astryx.inputClearButton.tooltipcatalog key to change the wording. (#7476) -
Markdownfenced code blocks wrap long lines instead of scrolling them sideways. A reader on a narrow screen sees the whole line without a horizontal scroll, and whereMarkdownand RichText give a code block the same width, a long line takes the same number of lines in both. Only the appearance changes: the code's text, line numbers, syntax colours and what Copy returns are unchanged, and a code block whose lines no longer overflow also no longer takes the scroll region's Tab stop, as a code block that fits never has. To keep the old sideways-scrolling look, render code yourself through thecomponents.codeprop with aCodeBlockthat leavesisWrappedoff. (#7564) -
Selector, MultiSelector, and ComplexSelector gain a
secondaryvariant: a borderless, neutral-filled trigger matching secondary buttons in navigation and toolbar compositions. Inside anInputGroup, a secondary Selector or MultiSelector keeps its fill and takes the group's row height and connected edges. Existinginput(default) andghostbehavior is unchanged. (#7252)
Fixes
-
Typeahead,Tokenizer, andPowerSearch: focusing an empty field withhasEntriesOnFocusafter its search source changed now bootstraps entries from the new source. Previously the results cached from the old source were already invalidated but still blocked the bootstrap, so focusing or clicking into the field opened nothing until the user typed. Cached results that are still current are re-shown without a new bootstrap, as before. (#7157) -
BottomSheetreturns focus without a focus ring after a pointer close (a tap in WebKit, or a pointer close of a sheet opened from the keyboard) and keeps the ring after a keyboard close, including on aDropdownMenubottom-sheet trigger the caller renders (renderTrigger). The sheet now asks the browser not to treat that focus as keyboard focus, the way Dialog's focus moves do. (#7388) -
Keep DateInput's calendar selection empty after clearing the field. (#6935)
-
Calendar: PageUp and PageDown move the focused day by a month — into the month the grid shows, on the same day of the month where it exists — instead of by a week; a page from a borrowed adjacent-month day lands on that day of the month now shown rather than on the month's first day. With two months, a page or an arrow crossing a month edge from the first month now lands on the correct day (the second grid no longer overrides the first grid's target). An arrow or page pastmin/maxchanges nothing, and a page into a month with no enabled day keeps focus in the grid. (#7467) -
Calendar: the arrow keys, Home and End no longer land on a borrowed adjacent-month day (shown dimmed, exposed as disabled, and inert to Enter, Space and clicks). An arrow crosses into that month and focuses the real day there, including across the two grids of a two-month view; Home and End stop at the first or last enabled day of the week in the month shown. A month with no enabled day holds no Tab stop. (#7511) -
CalendarwithhasWeekNumbers: the blank cell above the week-number column is now acolumnheadernamed with the locale's own word for the week field (from CLDR, visually hidden), so every cell in the header row has a grid role and assistive technology reads the weekday header row without a stray unnamed cell. (#7509) -
ChatMessageno longer leaves empty space for an avatar, name, or metadata that renders nothing. Booleans and empty strings create no slot, and any other slot content that renders nothing, such as an empty Fragment or a component that returnsnull, collapses its wrapper. A name that renders no text falls back to the translated sender label, while numeric values such as0still render. (#6597) -
CodeBlock's code viewport is a keyboard Tab stop only while its content actually overflows, through the shared scroll capability: a short snippet no longer adds an unexplained stop to the page's Tab order, a wide or capped block still does, and a viewport that stops overflowing keeps focus where it is. Role and accessible name are unchanged. (#7424) -
CommandPaletteInputnow exposes the semantics and name that its visible state promises. (#7321)-
A standalone field with no popup attributes was
role="combobox"witharia-expanded="true"andaria-autocomplete="list"; it is nowrole="searchbox"with no popup state. Passrole="combobox", or your popup's own ARIA attributes, to keep the old semantics. A field that already passes popup attributes (aria-expanded,aria-controls,aria-owns,aria-haspopup, and the like) stays acomboboxand keeps them, and a field insideCommandPaletteis unchanged. -
The field's default accessible name was its placeholder (
Search…, or the caller'splaceholder); it is nowSearch commands, translated. Passlabelto keep a specific name, for examplelabel="Search…"for the old default. A caller whose customplaceholderserved as the name keeps working code and hears the generic name until it passeslabel. -
The CLI's async-search CommandPalette block names its field
Search files. -
endContent={true}no longer renders an empty trailing slot, andendContent={0}renders inside the trailing slot instead of after it.
Also released in
@astryxdesign/cli. -
-
A standalone
CommandPaletteItemnow keeps a caller-supplied id on its rendered option and ignores click callbacks while disabled, so labels and test hooks keep pointing at the option and a disabled command cannot run. (#7324) -
CommandPalettenow keeps dismissal, text entry, result focus, and empty-state announcements aligned with what the reader sees. (#6950)-
While no selectable result exists, the results container no longer exposes an empty
listbox, and the query field reportsaria-expanded="false"with noaria-controlsoraria-activedescendant. It was an expanded combobox controlling an empty listbox. -
Home and End move ...
-
Astryx v0.7.0
Astryx 0.7.0 — all @astryxdesign/* packages ship at this version.
This release makes forms easier to style, improves phone typography and touch menus, keeps Markdown transforms composable, and moves spacing to rem units.
Upgrading to 0.7.0
Start here:
-
Pin Core and the CLI to 0.7.0, plus every other stable Astryx package your project uses.
OLD_VERSION=0.6.8 # replace with the Core version your project uses today npm i @astryxdesign/core@0.7.0 @astryxdesign/cli@0.7.0 @astryxdesign/build@0.7.0 @astryxdesign/theme-neutral@0.7.0The example includes Neutral; update every other stable Astryx theme in your project in the same install step.
-
Run the 0.7.0 upgrade. It rewrites supported callsites, validates data-color ownership, and leaves a TODO where it cannot choose safely.
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .
-
Check the items below. Four need a manual code edit, ten affect CLI commands or scripts, four are visual or token changes to inspect in your app, and eight are handled or checked by the upgrade.
Biggest risk: props that used to be ignored now reach the DOM. Layout, MetadataList, NavHeadingMenuItem, and TopNavMegaMenuItem already accepted BaseProps, so there is no type error or codemod. Audit callsites that pass IDs, ARIA or data attributes, event handlers, or style props only if they relied on those props doing nothing.
Manual code edits — no codemod (4)
- Banner announcements are no longer inferred from status → call
useAnnounce()from the async transition, or add an explicit persistent role. - Previously ignored BaseProps now apply → audit the four components in the risk callout only where a passed prop was meant to stay inert.
- Complete icon registries require
upload→ add anuploadentry and artwork to every custom completeIconRegistry. - Standalone empty-operator filters no longer save on open → save at the callsite when opening should commit the filter, or skip opening it.
CLI commands and scripts — no codemod (10)
-
theme addnow imports built themes → change source-copying scripts totheme eject; existingtheme add --importcalls need no change.- npx @astryxdesign/cli theme add neutral ./src/theme --overwrite + npx @astryxdesign/cli theme eject neutral ./src/theme --overwrite
-
init --remove-agentsreportsremoved: false→ treatdata.removedas a boolean, not literaltrue. -
Conflicting CLI options are refused → remove the formerly ignored option from affected command pairs.
-
Invalid CLI input now fails → pass valid types, packages, limits, and arguments, and handle
ERR_AMBIGUOUS_COMPONENT. -
Empty positional arguments are rejected → omit
""when the intended operation is to list entries. -
Integration validation needs a package → run it where
package.jsonexists, or handle exit 1. -
Astryx-prefixed agent variables are removed → use
AGENTandAGENT_SESSION_ID; agent metadata has no replacement. -
The
layoutcommand group is removed → useastryx build,astryx template, orastryx docs layout. -
Private theme variables fail the build → fix every reported private
--_*input instead of relying on exit 0. -
Doctor fails on unloadable configured projects → install or repair the named integration, remove it from config, or fix the reported field.
Visual and token changes — inspect your app; usually no code (4)
reset.cssnow reaches pseudo-elements and search fields → do nothing unless you need the old browser result; then add a later-layer override.- Phone typography starts at 16px in four themes → take the new Neutral, Chocolate, Butter, and Stone scale, or add a touch-and-narrow override to keep 14px.
- Core spacing uses rem → CSS variable users do nothing; code that parses px must convert with the root size or read the computed length.
- Matcha and Y2K spacing uses rem → take the pixel-equal default at a 16px root, or override spacing tokens in px for fixed sizing.
Handled or checked by astryx upgrade (8)
- Data-color defaults move to StyleX → fix anything
validate-data-token-ownershipreports, then rebuild committed theme CSS. - Date and time input styling targets the control → review the
migrate-field-input-stylingrewrite or TODO. - Typeahead, Tokenizer, CheckboxList, RadioList, and Slider styling targets the field → review the generated
container*orcontrol*prop. - Selector styling targets the control → review the rewritten control styling and forwarded trigger props.
- Text input styling targets the control → review the rewritten control styling and native input ID.
- Theme styling uses current names and data attributes → review rewritten keys and selectors, especially any TODO for custom CSS.
- BottomSheet adds default content padding → review the generated
padding={0}or TODO; remove the prop when your theme already supplies padding. emptySearchResultsTextis removed → review the rename toemptySearchTextand any TODO on spreads or trigger objects.
See the complete visual guide and all 26 detailed migrations.
@astryxdesign/core
Breaking Changes
-
Stop deriving live-region roles from
Banner.status. Banners are non-live by default; calluseAnnouncefrom the actual async transition handler, or provide an explicitrole="status"for a persistent semantic mirror. (#6630)
role="alert"is now opt-in rather than automatic for warning and error banners.Old usage:
Bannerderivedrole="status"orrole="alert"from itsstatusvariant.Replacement: The caller sets the role explicitly, or announces with
useAnnounce.Migration: Call
useAnnouncefrom the actual async transition handler, or provide an explicitrole="status"for a persistent semantic mirror.Codemod: None — the right announcement depends on the callsite.
-
Layout, MetadataList, NavHeadingMenuItem and TopNavMegaMenuItem now apply the BaseProps they accept (id, role, aria-*, data-*, hidden, inert, dir, event handlers; TopNavMegaMenuItem also xstyle/className/style) instead of silently dropping them; NavHeadingMenu keeps arrow navigation when an item handler cancels. (#7331)
Migration: remove any of those props you passed that you relied on being ignored; a TopNavMegaMenuItem styled through xstyle/className/style now shows that styling.From #5656 by @harjothkhara.
-
BottomSheet pads its content by default, like Dialog. With no
paddingprop and no theme padding, the content box now pads by--spacing-4(it had no padding before) and publishes that inset. (#6884)
A sheet whose only child is a paddedSectionor aLayoutrenders as before, because both escape the inset. Content that supplies its own inset is now padded twice: drop that inset, or passpadding={0}to keep the unpadded content box.astryx upgradeshipspreserve-bottom-sheet-content-padding, which addspadding={0}where needed.Migration: pass
padding={0}to keep the unpadded content box, or drop your own inset to take the default;astryx upgradeaddspadding={0}to every BottomSheet that sets no padding unless its only child is a padded Section or a Layout. An app whose theme already setsbottom-sheetpadding on every edge is not affected by the new default; the codemod cannot see the theme, so remove thepadding={0}it adds there to keep the theme inset. A sheet whose content wraps aLayoutgets a TODO:padding={0}also removes the inset between that Layout's regions, which the Layout's ownpaddingdoes not restore. -
Require
uploadon completeIconRegistrytypes (#7329)
Old usage: A custom complete icon registry typed asIconRegistrythat omitted theuploadkey.Replacement:
IconRegistrynow requires all semanticIconNamekeys, includingupload.Migration: Add an
uploadentry drawn in your registry's icon style to complete icon registries typed asIconRegistry.Codemod: No
astryx upgradecodemod applies because icon artwork is bespoke design artwork tailored to each theme and cannot be automatically synthesized. -
Move canonical data-color defaults from generated theme CSS to the public StyleX
dataVarsgroup. (#6834)
ImportdataVarsfrom@astryxdesign/core/theme/dataTokens.stylexfor CSS-capable code. Canvas and other non-CSS APIs continue to useresolveThemeToken()oruseTheme().token(). Rebuild committed theme CSS withastryx theme build; concrete themes now emit only explicitly authored data-color overrides.Old usage: Generated theme CSS owned the canonical
--color-data-*root defaults.Replacement: The public StyleX
dataVarsgroup in@astryxdesign/core/theme/dataTokens.stylexowns the defaults; concrete themes emit only explicitly authored data-color overrides.Migration: Import
dataVarsfrom@astryxdesign/core/theme/dataTokens.stylexfor CSS-capable code. Canvas and other non-CSS APIs continue to useresolveThemeToken()oruseTheme().token(). Rebuild committed theme CSS withastryx theme build.Codemod:
astryx upgraderunsvalidate-data-token-ownership, which rewrites nothing and reports fallback-lessvar(--color-data-*)uses (whole or embedded source strings, and stylesheets) and stale generated theme CSS for manual migration.Also released in
@astryxdesign/cli. -
DateInput, DateRangeInput, DateTimeInput, and TimeInput no longer accept root
xstyle,className, orstyle. Style the field withcontainerXstyle/containerClassName/containerStyle(the Field box; no effect inside InputGroup or ahorizontal-labelsFormLayout) and the visual control box withcontrolXstyle/controlClassName/controlStyle. Base props such asref,id,data-*,aria-*, and DOM handlers reach the field's semantic control on ever...
Astryx v0.6.8
Astryx v0.6.8
Astryx v0.6.8 is a patch release focused on interaction reliability, accessible state, and clearer integration tooling.
Core
New features
- Table resize handles now appear when a pointer enters the header, making resizable columns discoverable while preserving focused and scrolling states (#6194).
DropdownMenuItemnow forwards host attributes and DOM event handlers to its row without replacing its built-in role, focus, and menu behavior (#7196).markdownSourceLinesPluginadds opt-in, 1-based inclusive source-line metadata to rendered Markdown blocks and custom renderers, with no change when the plugin is omitted (#7286).
Fixes
- AppShell preserves page content, local state, and input focus across the mobile navigation breakpoint (#7236).
- Calendar and DateRangeInput keep explicit controlled-empty range state, announce required and pending state correctly, and no longer restore a cleared range (#7297).
- DateTimeInput keeps its calendar selection empty after the field is cleared (#7298).
- A DropdownMenu opened by holding a finger stays open when that finger lifts from the trigger (#7241).
- Dragging a mouse press between linked DropdownMenu rows no longer starts the browser's native link drag; the row under release acts normally (#7197).
- Kbd uses the primary text token so key labels meet WCAG AA contrast over neutral keycaps across light and dark themes (#7100).
- Steppers inside Popover, Dialog, BottomSheet, MobileNav, Lightbox, Toast, and other layer roots no longer inherit an outer Stepper's connector gap (#7287).
- A claimed semantic Markdown fence renders through its plugin even when the host supplies
components.code; unclaimed fences still use the host renderer (#7284). - Table resize handles stay keyboard-reachable on touch-first devices, stop at the table boundary without vertical overscroll, and compose with sticky columns regardless of plugin order (#6194).
CLI
integration verifynow accepts the released minimum CLI versions for templatereplaces(0.6.4) andkeywords(0.6.6) instead of requiring unreleased0.7.0(#7265).- Generated theme guidance points new authors to the palette generator and keeps the broad color scale opt-in (#6795).
Contributors
Thanks to @AKnassa, @cixzhang, @ernestt, @Geervan, @josephfarina, @korkt-kim, @rubyycheung, and @vjeux.
Astryx 0.6.7
Astryx 0.6.7 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply@astryxdesign/core
New Features
-
Itemgains swipe actions for touch, perspec:AST-057.swipeActionsdeclares, per side, the verbs a sideways drag uncovers asItemSwipeAction[]({id?, label, icon?, onActivate, isDisabled?, variant?: 'neutral' | 'accent' | 'destructive', hasRemoval?}, outermost last);swipeBehaviorisreveal(the row rests open with every entry a real button; a long drag or a fling fires the outermost) orcommit(the row slides out and the outermost fires; nothing rests). After an entry fires the row springs back, or holds out when the entry hashasRemoval. A mouse never starts the drag, a mostly vertical drag stays the scroller's, a resting row closes on a pointer outside it. Available on a row whose role permits interactive descendants (alistitem, a role-less row);ListItempasses both props through. No element is added to a row: its root translates and its panels counter-translate.Listclips its rows in the inline axis (overflow-inline: clip). TypesItemSwipeAction,ItemSwipeActions,ItemSwipeActionVariantandItemSwipeBehaviorare exported. -
Drawer is a container, like Dialog. A new
paddingprop takes a spacing step, and a theme'spaddingondrawerpads the drawer's scrolling content area through container tokens instead of padding the panel. The padded content area publishes its inset, so a Section that is the drawer's only child, and bleed children such as Table and Divider, align against it, and a Layout inside picks the value up for its header, content, and footer regions. With neither set, the inset is--spacing-4, as in Dialog; passpadding={0}for a full-bleed content area. The block-end safe-area inset is preserved in every mode. -
An option can carry a secondary action;
MultiSelectorrenders it in a grid.
SelectorOptionDataandSearchableItemgainaction?: ReactNode: one node the caller renders and names — anIconButton, aButton, a menu trigger. InMultiSelector, once any option carries one the popup is arole="grid"whose rows pair the option with its action: Up/Down move rows, the inline-end arrow reaches the action (following RTL), Enter fires it, pointer and touch press it directly, and pressing it never changes the selection. The trigger advertisesaria-haspopup="grid". Nothing changes for options without an action.Selectorand the typeahead panel do not render the key yet and warn in development when an item carries one. -
useTableRowExpansionacceptspanelVariant, and the detail panel now sits on the surface behind the table by default instead of on a wash of its own. (#5995)
The panel row painted--color-background-mutedunconditionally, and being a<tr>the plugin builds itself, nothing a caller rendered could reach it.The panel is the row's continuation, not a surface of its own, so it now takes whatever the table sits on — the same thing the row does. That keeps the plugin unopinionated about the table's background: a table on a
Cardno longer stacks a third surface, and a striped table no longer paints a band in the same token as its own stripe, which read as a data row rather than as a detail.panelVariant: 'muted'keeps the wash for the case that wanted it — a bare table with no card, no dividers and no striping, where nothing else separates the panel from the data around it.This changes the default appearance. A table relying on the wash gets it back with
panelVariant: 'muted'; in dark themes there is nothing to get back, because--color-background-mutedis a low-alpha near-black that is close to invisible over a dark card — dark has effectively been renderingtransparentall along. -
useTableRowExpansionacceptshasRowClickExpansion, so a row opens when you click anywhere on it and not only on its chevron. (#5995)
useTableTreeDatahas had this since it shipped, under the same name and with the same behaviour. The detail-panel plugin is the other half of the same pair — one expands into child rows, one expands into a panel — and a caller who moved between them lost whole-row clicking without anything saying why. There was no way to add it back either: a click handler on the row has to know not to fire on a checkbox, a link or the end of a text drag, and none of that is reachable from the outside.Off by default, and pointer-only when on. The chevron button stays the accessible control, so keyboard and assistive-tech users are unaffected — this adds a shortcut for a mouse, not a second way to operate the table. Clicks that land on interactive cell content, clicks that end a text selection, and clicks on rows
getIsItemExpandablehas ruled out all pass through untouched. The chevron already stops propagation, so it does not toggle twice.Collapsed rows are wired up as well as expanded ones, which is most of the point: the row you want to click is the one that has not opened yet.
Fixes
-
A BottomSheet drag writes its transform straight to the sheet once per input sample and renders nothing in between (React state changes when the drag begins and ends, and when its layout split changes). Measured in Chromium: 4 commits for a 24-sample drag, down from 49. The release still animates from wherever the finger left the sheet.
-
BottomSheet hands a touch to the sheet at the scroll edge of the box under the finger, not the body's alone. Content that scrolls inside the body (a pinned header and footer around a scrolling middle, a grid) never moved the body's
scrollTop, so every pull down over that scrolled box dragged the sheet and the box could not be scrolled back by hand. From the box's top the pull still drags the sheet. -
BottomSheet no longer blocks pinch-zoom. A pinch that started on an open sheet (its handle, its content, or across both) did nothing, because the sheet claimed every touch gesture:
touch-action: noneon the sheet and handle,pan-yon the content. The sheet now leaves pinch to the browser (pinch-zoom, andpan-y pinch-zoomon the content), and a second finger (on the handle, on the content, or off the sheet, whichever lands first) ends any sheet drag in flight or about to start, so the page zooms and the sheet stays at its detent. One-finger drag and scroll are unchanged. -
A BottomSheet release is judged where the sheet would coast to at the finger's speed, not where the finger left it: a medium throw dismisses from short of the dismiss line and a gentle throw lands on the next detent instead of snapping back, the release speed is read over the finger's last 100ms rather than its last two samples, and a finger that rests before lifting releases no throw. A nudge under 48px still projects nothing.
-
A DropdownMenu opened by a mouse press returns focus to its trigger when it closes, instead of to the control that was focused before the press.
A mouse opens the menu on press-down, before the browser's own mousedown has focused the trigger, so the popover remembered the previously focused control and handed focus back there on Escape or after a pick. The trigger now takes focus as the press opens the menu, the state a click-open already had; keyboard opens, taps and the menu's own behavior are unchanged. -
A field status shown with
statusVariant="tooltip"is now announced to screen readers when it appears or changes, the same way the attached and detached message boxes are: errors assertively, warnings and successes politely.
Before, the tooltip placement only described the control, so a screen-reader user was not told that a validation message had appeared until they left and re-entered the field. This affects every input that offers the tooltip placement. The attached and detached placements are unchanged and are still announced once. -
FileInput: invoke changeAction and update optimistic state on clear and file selection
-
swizzle: rewrite every .stylex import to a deep path
astryx swizzle Button(and any component that imports a.stylexmodule from outside its own directory) emitted an import that collapsed to the directory barrel — e.g.@astryxdesign/core/utilsinstead of@astryxdesign/core/utils/interactionOverlay.stylex. The barrel does not re-export those StyleX symbols, so the swizzled file could not compile.rewriteImportsnow gives every*.stylexmodule the deep subpath. The exports generator (scripts/sync-exports.js) adds 16 specific subpath exports for the.stylexmodules swizzled components actually reference across directories:DateInput/tokens.stylex,Icon/IconSize.stylex,Indicator/indicator.markers.stylex,Layer/layerAnimations.stylex,Layer/layerTextReset.stylex,Layer/layerViewportInset.stylex,Layout/container.stylex,Layout/edgeCompensation.stylex,Layout/padding.stylex,NavItem/navItemStyles.stylex,Selector/selectorPresentation.stylex,Stack/stack.stylex,Stack/stackItem.stylex,Text/text.stylex,utils/focusOutline.stylex,utils/interactionOverlay.stylexThese are public API additions. Each module already ships in the npm tarball (in
dist/); only the exports map entry is new. No wildcard — a future cross-directory.styleximport needs a deliberate entry inSTATIC_EXPORTS. Precedent:./theme/dataTokens.stylex(AST-066 FR2). -
Keep grouped-row custom headers full-width while pinning their 16px disclosure button, and expose expansion state on the button. (#6251)
-
Keep grouped-row collapse chevrons pinned without shrinking custom headers, and increase them from 12px to 16px (#6222)
-
Keep layer portals inside their nearest dialog
-
Markdown: read an indented table with its own columns
Markdownno longer adds an empty first column to a table whose rows are indented, such as| a | b |. The indentation before a row's first pipe used to read as a cell of its own. -
Markdown: keep a definition-shaped line whos...
Astryx 0.6.6
Astryx 0.6.6 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply@astryxdesign/core
New Features
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUp
Themes drawuploadthroughicons.upload, separately fromarrowUp, so sort arrows and every otherarrowUpuse stay unchanged. Every bundled theme and theme template drawsuploadin its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with nouploadartwork shows the default upload-into-tray glyph there.A complete
IconRegistrymay still omituploadin this release. The next minor makes it required, so add anuploadentry to any registry you type asIconRegistry. -
BottomSheet is a container, like Dialog. A new
paddingprop takes a spacing step, and a theme'spaddingonbottom-sheetnow pads the sheet's content box through container tokens instead of padding the panel. The padded content box publishes its inset, so a Section that is the sheet's only child, and bleed children such as Table and Divider, align against it. With neither set, the content box stays unpadded as before. -
ComplexSelectorcan hang off a control the caller renders.
A newrenderTriggerrender prop renders the control the popup hangs off — a glyph in a list row, a chip, an icon button — in place of the selector's own field and button. Spread the given props onto it; the popup is anchored to it, keeps its dialog label fromlabel, opens on click or ArrowDown, and returns focus to the control on close. The existinghandleRefandonOpenChangework unchanged beside it. Off by default; existing selectors are unchanged. -
ContextMenu takes
triggerAs(div|span) so a reference inside prose can own a context menu without breaking the text flow. -
Export the canonical 56-token StyleX
dataVarsgroup for CSS-capable data visualization consumers. -
Export
decodeMarkdownCharacterReferencesfrom@astryxdesign/core/Markdown/parserand@astryxdesign/core/Markdown
It decodes character references the wayMarkdownrenders them —©,©, and©become©; unknown names and references without their semicolon stay as written — using the same tableMarkdownuses. It works on plain text, so leave code and backslash-escaped references out. The parser subpath has no use-client boundary, so server code can call it. -
Add
DropdownMenuGroup, a titledrole="group"of rows for compound-mode menus.
Theitemsdata API could title a group ({type: 'section', title, items}); a menu written with children — checkbox rows, radio groups, rows mounted only while open — could not.DropdownMenuGroup(alsoContextMenuGroupandBreadcrumbMenuGroup) renders arole="group"named by its heading througharia-labelledby; the heading shares the data mode's typography andastryx-dropdown-menu-section-headingtheme target, is not a menu item, and is skipped by arrow keys and typeahead. Existing menus are unchanged. -
The message a component shows when a query matched nothing is now
emptySearchTexteverywhere, and it takes aReactNode.
Selector,MultiSelector, andCommandPalettealready called itemptySearchTextand already accepted a node.Tokenizer,Typeahead,BaseTypeahead, and eachChatComposerInputtrigger called itemptySearchResultsTextand accepted only a string — so the same product could offer a "no results, create one" row in one component and not in its neighbour, and a builder who learned one had to discover the other.Nothing breaks. The type only widens, so every existing value stays valid, and
emptySearchResultsTextkeeps working exactly as released. Set both andemptySearchTextwins, with a development warning. Migration is the name alone.Deprecation lifecycle (
spec:AST-017FR28, FR31) — removal only in a later minor whose frozen manifest carries both ids of a pair: -
The layer runtime owns the viewport inset: one gutter, one cap, one fallback order, and one place an app declares a floating bar.
Every anchored layer — Popover, DropdownMenu and its submenus, Typeahead, Tooltip, HoverCard, the selectors — now keeps the same gutter from each viewport edge (the spacing-4 step or the device safe-area inset, whichever is larger) and is capped to the viewport. Four components used to carry their own copies of that gutter, and they had drifted. -
Add the first-party Markdown heading-links module.
createMarkdownHeadingLinks()is exported from@astryxdesign/core/Markdown/pluginsand gives every built-in h1–h6 a collision-safe generated fragment plus an accessible inline trailing#copy button, including headings nested in blockquotes and lists. Its frozen, versioned entry carries the namespace and safe URL base across compatible Core package copies without module-local state. The heading row usesuseContainerReveal: the button is hidden at fine-pointer rest, reveals on row hover or keyboard focus, and follows the canonical coarse/touch behavior. An unmodified tap, click, Enter, or Space copies the canonical URL without navigating, scrolling, or mutating the hash and briefly shows a check confirmation; failures stay silent. The same opaque plugin entry keeps Markdown-derived Outline aligned, supports Unicode NFKC slugs, an optional caller-owned namespace and safe permalink URL base, and leaves default Markdown plus custom heading renderers unchanged. -
Markdown: nested lists draw a different marker at each depth
Bulleted lists cycle disc, circle, and square, and numbered lists cycle decimal, lower-alpha, and lower-roman, by how many lists of either kind enclose them, so each level can be told apart from the levels beside it. Numbering keeps each list's start and items.ListandListItemkeep their public marker styles. -
Markdown plugins: read what a plugin declares, and render one plugin node as Markdown does
getMarkdownPluginCapabilities(plugin), from@astryxdesign/core/Markdown/plugins, reports whether a plugin declares syntax and whether it declares a transform, and nothing else; plugin entries stay opaque.MarkdownPluginNodeRenderer, from the new client-only@astryxdesign/core/Markdown/plugin-renderersubpath, renders one parsed extension node with the given plugins exactly asMarkdownpresents it — the plugin's renderer inside the same error boundary and suspense fallback, the same readable fallback text, and the same failure report — with no element of its own.Markdown's own output is unchanged. -
DropdownMenu takes a
triggerrender prop: hang a menu off any control.
triggerrenders the control the menu opens from — an IconButton, a chip, an avatar, a list row — and hands itDropdownMenuTriggerPropsto spread: the press model, the keyboard opens, the toggle click and the ARIA wiring. The menu is named by that control througharia-labelledby.buttonandtriggerare mutually exclusive (a dev warning). -
Menu arrows wrap and PageUp/PageDown page.
InDropdownMenu,ContextMenuandDropdownMenuSubMenu, ArrowDown on the last row wraps to the first and ArrowUp on the first to the last, as macOS menus do (Selectorkeeps clamping like a native select). PageDown and PageUp move to the last and first fully visible row of a scrolling menu, and pressed there again one viewport further, never wrapping. ArrowUp on the trigger opens the menu with the last row highlighted. The key that opened the menu no longer activates the first row through its auto-repeat, and typeahead ignores a key that is part of an input-method composition.useListFocusgainshasPaging. -
DropdownMenuItem takes
href: a menu row that navigates is a real link.
DropdownMenuItem(and a data-mode item) takeshref,targetandrel. The row renders as the anchor itself, withrole="menuitem", routed throughLinkProvider, so a ⌘-click, Ctrl-click or middle click keeps the browser's meaning and skipsonClick; a plain click runsonClick, closes the menu and navigates. The touch sheet renders the same item as a link row.onClicknow receives the click event. Enter and Space in every menu synthesize a click that carries the key's modifiers. -
DropdownMenu takes
menuMaxHeightto lift the 300px cap for a menu that must fit its rows; the viewport still bounds it. -
Menus and pickers act on the row under the pointer at release, and the highlight follows a held finger or mouse.
DropdownMenu,ContextMenu,DropdownMenuSubMenu,Selectorand the menu bottom sheet share one press model, the one macOS and iOS menus use: the row under the pointer when it is released is the row that acts, and the highlight follows a held pointer across the rows. A finger that lands on one row and lifts on another acts on the second — once; the click the browser aims at the first row is swallowed. A mouse released outside a menu closes it; a finger released outside leaves it open. A menu whose rows fit declarestouch-action: noneso a slide stays a slide; one that scrolls lets the browser pan it and ends the gesture. Menu rows no longer paint a pressed look where hover does not exist. New public hook:useMenuPress. -
A mouse opens a DropdownMenu on press and can drag straight into it; a finger held on the trigger opens it with the finger down.
ADropdownMenutrigger now opens its menu on a mouse press-down, and a drag from the trigger into the menu that lets go over a row picks it, as macOS menus do. The release of the opening press acts only after the pointer has entered the menu or the press has lasted about a third of a second, so a menu that opens under the pointer never picks a row nobody chose. Pressing the trigger of an open menu closes it without reopening in the same gesture. A tap still opens through its click; a finger held on the trigger for half a second opens the menu with the finger down, and a slid...
v0.6.5
Astryx 0.6.5 — all stable @astryxdesign/* packages ship at this version.
npx astryx upgrade --from 0.6.4 --apply@astryxdesign/core
New Features
- Stop bundling translator-only descriptions with the built-in English
fallback and add compact generated string-map modules for every shipped locale. Existing rich JSON catalog exports remain unchanged. - Popover and usePopover take a
paddingprop on the spacing scale (matching Card and Stack).padding={0}paints a flush surface for content that owns its own edges, such as a list of rows or a header with a rule; the default rung (3) is unchanged.
Fixes
- Button no longer overflows narrow rows: a labelled button can shrink below its label width and truncates the label with an ellipsis, while icon-only buttons stay square
- Prevent an empty Tokenizer input from creating a blank trailing row.
- SegmentedControl's default
huglayout is now capped at its container width, and its segments shrink and truncate their labels instead of overflowing narrow cards and phone rows - SelectableCard: keep disabled cards in sequential focus navigation with aria-disabled and gated interaction handlers.
- Selector's one-line trigger matches its size token when the theme's
--spacing-5is taller than the token can hold, instead of overshooting it. - Switch: announce busy/loading states through the persistent
useAnnouncelive region and localize the announcement via@astryx.switch.loading.
@astryxdesign/cli
New Features
astryx componentaccepts several exact selectors in one call. The JSON response keeps one ordered row per selector, including missing and ambiguous components, and text mode prints every row before exiting nonzero when any lookup fails. Thecomponent()API accepts selector arrays and always returnscomponent.batchfor an array, including empty and one-item arrays. Batches accept up to 100 selectors and reject larger arrays before lookup. The public API also exports sharedBatchResponseandBatchRowtypes for typed receipts.astryx discoverbrowses integrations: the ones a project has and, through discover sources, the ones it could add, with every version and what each one adds. It searches every kind of item and filters with--type,--installed,--available, and--limit. A project sets a source asdiscoverinastryx.config, and an integration exports one as adiscovernamed export. Discover only reads: it prints the command that adds a package and never runs it. Existing--jsonfields keep their meaning. A free-text query now always lists its matches, even an exact component name, andastryx discover <package>/<Name>opens one.astryx integration verifyis the new name ofastryx integration pack --check.
The check you run before publishing an integration now has a name that says what it does.astryx integration verifypacks the package with npm, installs the tarball into a temporary app, and checks that the app sees the same components, templates, themes, docs, and codemods. It takes no flags.astryx integration pack --checkstill works as a deprecated alias: it runs the same check with the same output, JSON, and exit codes, prints a note that namesintegration verify, and shows as deprecated in help. It will be removed in a later release. TheintegrationPackCheck()API and itsintegration.pack-checkJSON response do not change. With--json, a command group given an unknown subcommand now reportsERR_UNKNOWN_SUBCOMMANDand lists its subcommands, where it used to say JSON output is not supported.
Fixes
-
Fix the CLI's topic docs and how they print.
-
astryx doctorno longer reports an integration it could not check as absent or complete (#6619) -
upgrade'sfilesChangedcounts files, not (codemod, file) pairs (#6622)
One source file that four codemods each changed was reported as four files changed, sofilesChangedmatchedtransformsAppliedand the documented meaning, "Total files changed", was not true. The human summary said the same thing: "Found 4 changes across 4 files" for one file.filesChangedis now the count of distinct files.transformsAppliedis unchanged: a code or config codemod counts once for each file it changed, and a project codemod counts once. A file that both a core codemod and an integration codemod changed counts once infilesChanged. -
A parse error prints the Astryx error format in text mode.
astryx theme list --lang zh-Hansprinted Commander's own line —error: option '--lang <locale>' argument 'zh-Hans' is invalid…— while every other CLI error printsError: ….--jsonwas already correct (ERR_INVALID_LANG), so the two modes agreed only on the exit code.Commander writes that line before any Astryx code runs, so the JSON shim — the one place that already sees every parse failure — now suppresses it and writes the Astryx line itself, from the same message, for both modes. Every parse failure is covered: unknown option, unknown command, missing argument, and an invalid value for a global option.
--helpand--versionare untouched and still exit 0. -
The CLI reference now matches what the commands do. Every
--helpends with the command's examples and aMore:line that names its full docs page. Function docs show each parameter's default, mark required parameters, list the error codes each function throws, and use examples that run. The response-type list addshelp,version, andupgrade.registry, andastryx manifestnow listsupgrade.registryforupgrade. The--zh,--dense,--lang, and--detaildescriptions name the commands they change, and command summaries say when to use each command. Whenastryx templaterefuses to overwrite a file, it now says to re-run with--overwrite(or-f). Theupgradecommand page (astryx docs cli/commands/upgrade) now explains which files codemods never edit, what happens when one of them needs a change, and how to regenerate it. -
astryx integration pack --checknow checks the tarball when aprepack,prepare, orpostpackscript prints to stdout. Before, any lifecycle output made the check fail with "npm pack produced unparseable JSON output" before it looked at the tarball. A failing lifecycle script still fails the check, and its output stays in thepack_failedmessage. -
astryx doctor integration docsfails when a namespace doc or a placement fails, as its help says.
Such a failure hides the doc from the docs tree, so it now exits 1 with aninvalid_doc_grapherror instead of a warning. A link that names no doc still only warns, since it prints as written.doctor integration docsanddoctor integration componentsalso no longer print an[ok]line after a check that failed.A mistyped subcommand under
doctornow fails and lists the subcommands the group has:astryx doctor integrationsused to run the project checks, andastryx doctor integration bogusexited 0 in text though it exited 1 with--json. -
A package that ships a theme, or a doc section with an
id, now declares the CLI that can read it.
A stable CLI before 0.7.0 rejects both: it cannot read the typed theme descriptors thatastryx integration add themewrites, and it rejects a sectionid. Either way it hides the package's themes or doc topics with no warning.astryx integration add themenow adds"@astryxdesign/cli": ">=0.7.0"topeerDependencies, marked optional, andastryx integration verifyfails withthemes_need_cliorsection_ids_need_cliwhen a package needs that peer range and does not declare it. -
astryx integration verifyresolves every public import in the packed package, not in your source folder.
Before, its temporary app resolved your package's own name through the sourcepackage.json, so anexportstarget left out of the tarball still passed. It now fails withcomponent_export_missing, as an app that installs the tarball would. -
astryx theme addandastryx theme buildnow undo a failed write completely. Before, when one file failed to write after others were written, the written files kept their new content. Now every replaced file gets its previous content back, every new file is removed, and the error names any file that could not be restored. Both commands also refuse to replace a destination that is a symbolic link. (#6852)
Other Changes
- A code block's label now prints above the block instead of as a
// labelline inside it, so copied bash, CSS, JSON, and HTML stay valid. Table cells escape|, so a union type stays in one column. astryx search dark modesearches for both words; it used to drop every word after the first. A result that matches every word of a query, one of them by name or keyword, now outranks one that matches only some, and a section whose title or heading holds the whole query ranks near the top. Topics can declare searchkeywords, now a documented ReferenceDoc field, and a namespace'skeywordsnow count too. A query keeps its phrase when common words such asmake,build, orandrop out, soastryx search make an integrationfinds the integration guides, a plural of a doc's name matches it, one step below the exact name, and a component's name typed as words, such ascommand palette, finds the component. Outside an app, where@astryxdesign/coreis not installed,astryx searchsearches the docs instead of failing, and says so;--type component,hook, ortemplatestill needs Core.- Snippets that failed when copied now work: StyleX token imports, the
fr-FR.jsonlocale path, Tailwindrounded-lg,--color-background-muted, icon and color values, and the Cursor rule path. - Claims that did not match the code are corrected: the 30 shipped locales and how RTL mirroring works, what
astryx initwrites,--detail brieffor a shorter read, the Neutral and Matcha fonts, the components that need anchor positioning,gapsteps, Card's ...
Astryx v0.6.4
Astryx 0.6.4 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply@astryxdesign/core
New Features
-
Add Timer for standardized elapsed durations without React tick renders. (#6438)
UseTimerfor active-operation elapsed time. It starts from mount by default, accepts an earlier Unix-millisecondstartTime, offerselapsedandclockformats with adaptive cadence, and matches Timestamp typography props. -
DialogHeader: expose the start- and end-content wrappers as theme targets (#6415)
Addsdialog-header-start-contentanddialog-header-end-contentso themes can style the existing content-slot wrappers without relying on their DOM positions. The end-content slot also contains the optional close button. Default layout and behavior are unchanged. -
DialogHeadertitleandsubtitlenow accept anyReactNode, not only strings.
A title can carry inline markup and still renders inside the focusableh2that receives focus on open and names the Dialog; the accessible name is the title's text content. A subtitle can carry inline content such as aLink. String callers are unchanged. An empty-string, boolean, or nullish subtitle renders nothing, and a numeric0subtitle now renders inside the subtitle text. -
DropdownMenuItem,DropdownMenuCheckboxItemandDropdownMenuRadioItemforward arefto the row root (#6687).
The ref reaches the element carryingrole="menuitem"(ormenuitemcheckbox/menuitemradio), the wayItemandDropdownMenuDivideralready forward one, so a menu row can be registered with an element-keyed observer or overlay — an IntersectionObserver for an impression, a measurement, a debug outline — without a wrapper betweenrole="menu"and the row. -
Add
presentationto DateInput, DateTimeInput, and TimeInput (spec:AST-043) (#6628).
presentationnames every picker surface, distinguishing Astryx's desktop surface, Astryx's bottom sheet (including a new TimeInput sheet), the browser/OS picker, and — for TimeInput only — a plain typed field. DateInput and DateTimeInput accept five values:'popover' | 'bottom-sheet' | 'native' | 'adaptive-bottom-sheet' | 'adaptive-native'(default). TimeInput accepts those five plus'text-input', the typed field on every pointer, because that is the surface its releasednativePicker="never"already was.presentation="native"always shows native;adaptive-nativekeeps the released native fallbacks.nativePickeris deprecated but keeps working exactly as released (touch→adaptive-native,always→native,never→adaptive-bottom-sheet, ortext-inputfor TimeInput);presentationwins when both are set.astryx upgradeshipsmigrate-native-picker-to-presentationfor static callsites. -
List: addedgeCompensation="inline"to compensate for item content inset within container padding (#2626)
ListIteminsets its content by a density-dependent horizontal padding, so a list under a section heading reads as misaligned and consumers reach for negative-margin custom CSS.edgeCompensation="inline"onListcancels, per inline edge, the smaller of that inset and the container's published padding on that edge; zero-padding and full-bleed surfaces therefore stay in place, asymmetric container padding never over-cancels an edge, and headers, hover backgrounds, and selection backgrounds keep their existing geometry.Itemnow publishes its inline inset as--_item-inset-inlineand derives its ownpaddingInlinefrom it, and the clamped cancelling margin reads the same variable — so the cancel tracks density and theme overrides instead of mirroring hardcoded values. Themes that setpaddingInlineonitemfeed the variable automatically via the derived var registry. -
Markdown: add the first-party soft-breaks plugin (#6459)
UsemarkdownSoftBreaksPluginfrom@astryxdesign/core/Markdown/pluginsto render soft line endings as hard breaks without preprocessing source. The plugin matches the realremark-breakspackage through Astryx's supported adapter path while keeping code and other opaque content unchanged. -
Table: add a selection-aware bulk-actions wrapper that consumes
useTableSelectionStateoutput while keeping the selection plugin behavior-only. (#6474)
Fixes
- Preserve authored heading and text component styles when theme adaptations change the type scale (#6710)
- AvatarGroup keeps its overlap when avatars are wrapped in a HoverCard or Tooltip. Every avatar and the overflow indicator now take the overlap margin, and the group pads its start edge to match, so the overlap no longer depends on each avatar being a direct child of the group (#6736).
- Avatar initials skip punctuation: each word's initial is its first letter, digit or emoji, so
Northwind Workbench (automation)rendersNAinstead ofN(and“Ada” LovelacerendersALinstead of“L. Words with none of those, such as a lone-, are ignored, and a name made only of punctuation shows the default icon rather than a stray symbol (#6698). - Size an Astryx Icon without an explicit size to match its Button or
IconButton: 16px for small and medium controls, and 20px for large controls. (#5762) ChatComposerInput: an empty input no longer shrinks by 8px whenisDisabledflips totrue, which shifted anything bottom-aligned beside it (e.g. a send button in a grid row) (#6654).
The root'sminHeightwas set to the shared line-height only, not the paddingeditableandplaceholderboth add on top of it — normally immaterial, since the editable region reserves its own padded box even when empty. A disabled, emptycontentEditableregion stops reserving that empty line at all in Chromium, and the absolutely positioned placeholder standing in for it doesn't contribute to layout height, so the root fell back to just the line-height and lost the padding.minHeightnow explicitly accounts for both.- ChatComposerInput no longer scrolls for a short single-line draft with
maxRows={1}. Removing vertical padding also keeps the empty input and placeholder aligned when disabled. (#6733) - Keep numeric zero aligned in ChatMessageBubble name and metadata slots. (#6600)
The aligned wrappers are omitted for non-rendering scalar values (null,undefined, booleans, and the empty string), while numeric0remains visible inside the same inset as other slot content. - Render numeric zero when it is passed as the ChatMessageList empty state. A valid ReactNode should not disappear when the transcript is empty. (#6636)
- Omit empty ChatMessageMetadata slots and their separators (#6637).
Boolean and empty-string timestamp or footer values no longer leave a blank row or a stray dot. Numeric zero remains visible, and all delivery statuses keep their existing labels and icons. - Compose accepted
onClickhandlers with ChatSendButton's send and stop actions instead of replacing them (#6653). Consumers that usedonClickto replace sending should move that logic toonSend, because both handlers now run. - Allow long ChatSystemMessage content to wrap within narrow chat layouts instead of crossing the container edge (#6655).
- ChatTokenizedText now ignores empty token values so tokenized messages always finish rendering (#6679).
- ChatToolCalls now displays custom group labels, keeps collapsed details out of keyboard navigation, adds visible focus treatment, and uses readable secondary text for neutral metadata (#6680).
- Stop ChatLayout's scroll-to-bottom button from being a tab stop while it is invisible. At rest the layout's default
scrollButtonrenders hidden, butopacity: 0andpointer-events: noneleave it in sequential focus navigation — so a keyboard user's first Tab into any chat landed on a control with no visible focus indicator (WCAG 2.2 SC 2.4.7), and Enter scrolled the transcript. The hidden state now also setsvisibility: hidden, which the fade transition carries so the animation is unchanged; the visible button keeps its keyboard access. The pill's height and collapsed width now track--size-element-mdinstead of a hardcoded32px, so a theme that retunes the element scale can no longer make the pill clip its own Button. ChatLayout's consumer docs also gain thedensityprop, which was undocumented, and drop the claim that density adapts automatically to container width — it never did. A Chromium evidence spec pins the hidden/visible/re-hidden keyboard contract and the pill's containment of its Button under a size-retuning theme, and the RTL applicability ledger records why ChatLayout and its scroll button have no applicable RTL dimension. (#6466) - Make a theme that styles
chat-layout-scroll-buttonactually restyle the chat scroll-to-bottom pill. The documented target sat on the invisible full-width row that centres the pill, so abackgroundColoroverride painted a band across the chat dock while the pill kept the surface the theme asked to replace — and every automated check passed, because the override did reach an element. The target now rides the pill, which is what paints the fill, elevation, and radius (architecture:component-theming-surfaceINV4). The target keeps its name, stays a single target, and the component's props, DOM shape, ref target, and consumer passthrough are unchanged. A Chromium spec pins the placement and proves the repair by moving the target back and showing the pill go unstyled under a theme. (#6482) - Correct CheckboxIndicator theming, replacement-content, and focus guidance (#6745).
- Align CheckboxInput theming and label-icon guidance with shipped behavior (#6747).
- A CheckboxList option that is saving through
changeActionnow keeps its spinner, busy state, and re-toggle guard when another option is toggled before it settles (#6777). - Clicking a read-only CheckboxList option that has an
onClicknow fires that handler once, instead of re-dispatching the click until the browser's call stack ov...