Astryx v0.6.8
Astryx v0.6.8 is a patch release focused on interaction reliability, accessible state, and clearer integration tooling.
- 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).
- 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).
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).
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
-
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.
-
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 whose label is over 999 characters
Markdownno longer drops a line shaped like a link reference definition whose label holds more than 999 characters. CommonMark allows no such definition, so the line now shows as text, as a reference with that label already did. -
Markdown: end a code block left open at the end of a message on its last line of code When a message ended inside a code block that was never closed,
Markdownread the message's final line ending as one more, empty line of code, unlike a closed code block and unlike the streamed render of the same message. A real blank line before the end is still code, and while a message streams, an open code block now shows the blank lines already written instead of adding them later. -
Markdown: keep a quoted blank line at the end of a code block left open A code block left open inside a block quote keeps a blank quoted line at its end as code, as CommonMark reads it. Only the document's own final line ending is left out of an open code block.
-
Markdown: keep two lists apart while streaming when their bullets or indentation differ While a message streamed,
Markdownjoined two lists a blank line apart whenever both were bulleted, or both numbered with the same delimiter — even when their bullets (-then*) or indentation differed, where the finished document shows two lists. The streamed render now keeps them apart, as the finished document does. -
MobileNav no longer blocks pinch-zoom while it is open. The open nav covers the whole viewport and declared
touch-action: none(withpan-yon its content), so a pinch anywhere on screen did nothing. It now declarespinch-zoom(andpan-y pinch-zoomon the content): pans are still kept from reaching the page behind, and a pinch zooms the page. -
MultiSelector: name the listbox of a bottom sheet without search from the component's label, so screen readers announce it
-
Added a
@astryx.richTextEditor.*catalog namespace to the shared message catalog, so the RichText editor's toolbar labels, block-format options, link dialog, and Tab escape hint can be translated and overridden throughInternationalizationProvider. The editor's character counter now announces through the existing@astryx.textArea.characters*messages, so it is translated in every locale TextArea already ships. -
In a collapsed SideNav, pressing a SideNavHeading menu trigger right after hovering it now keeps the hover-opened menu open, as the click guard intends. The browser was dismissing the menu on the press because the trigger sat outside the panel; the collapsed trigger is now the panel's native invoker, the same wiring TopNavMenu uses.
-
Keep editable content from toggling tree and expansion rows The shared click guard that row-expansion and tree-data use to decide whether a row click should toggle missed
[contenteditable]elements. Clicking or typing inside editable content in a row toggled it. Restored the exclusion with tests on both paths. -
Whole-row-click expansion reuses the shared clickable-container guards instead of its own selector list. (#5995)
useTableRowExpansionanduseTableTreeDataeach carried a hand-rolled copy of "did this click belong to something else" — the same nine selectors, written twice.useClickableContaineralready owns that rule for every clickable surface in the system, and itsINTERACTIVE_SELECTORSlist is the fuller one: it also coversrole="link",radio,switch,tab,menuitem,option,combobox,listbox,slider,spinbuttonand[data-pressable-container], and it excludes[aria-readonly="true"].Both plugins now call the hook's
hasInteractiveAncestorandhasTextSelection, newly exported for containers that cannot use the hook itself — a<tr>assembled insidetransformBodyRowhas no ref to hand it.Two behaviour changes fall out of sharing, both fixes:
-
Turn only the chevron glyph in
useTableRowExpansion, not the button beneath it. (#5995) The rotation was on the<button>, which is the hit target and carries the hover chip, so opening a row swung that rounded rectangle and its highlight a quarter turn along with the arrow — most visible mid-animation, where the chip passes through a diamond. A finished 90° turn on a 24px rounded square lands back on itself, which is why this only shows up in motion.The transform now sits on the glyph and the button stays put. The two transitions move onto
--duration-fastand--ease-standardin the same pass, matching whatTableRowalready uses for its own hover transition. -
Draw the row divider below a
useTableRowExpansiondetail panel rather than above it. (#5995) An expanded row and its panel are one unit, but the divider was landing between them: the row drew its own bottom border, which put a line between the row and the detail it had just opened, and the panel drew none, so it ran flush into the next row. Both halves of that are backwards — the pair was split down the middle and then fused to the row below.The expanded row now gives up its border and the panel takes one. On a table with no row dividers the suppression removes a border that was never there and the panel's is never applied, so neither side has to consult the divider mode; only the panel does, to know whether to draw at all. The panel row carries
tableRowMarker, which is howTableCellscopes its "no trailing line under the last row" rule, so an expanded last row still ends the table cleanly. -
Start the
useTableRowExpansiondetail panel at the first column rather than at the row edge. (#5995) The panel is one cell spanning the whole row with a flat20pxinline padding, so its content began under the chevron — a column to the left of every label it describes.It now indents by the chevron column's fixed width plus the inline padding a cell of that density gives its own content, read off the table context so it follows
density, and written as a logical property so RTL mirrors it. It is not configurable: a panel starting anywhere else reads as a misalignment rather than as a choice.The chevron column's width cannot be a token reference — the layout does arithmetic on it — but it is the pixel value of
--spacing-10, which is how the indent spells it. A test pins the two together so a change to the scale cannot silently unalign them. -
Tablezebra striping counts data rows only, so expanding a row no longer inverts the stripe of every row below it. (#5995) A row-expansion detail panel is appended as a sibling<tr>in the same<tbody>, and striping is:nth-child(even)— so the panel took a stripe turn of its own and pushed every row after it onto the opposite one. Opening a single row repainted half the table.The stripe now counts
:nth-child(even of :not([data-expansion-panel])). A panel is not a row and is not counted as one, so the data rows keep their parity whatever is open.of Shas been Baseline since 2023, inside Astryx's support floor (AST-013). Below it the stripe rule is dropped rather than misapplied: an unstriped table, not a mis-striped one.Tables without the row-expansion plugin are unaffected — nothing else in the system emits
data-expansion-panel. -
Table columns without an explicit width now keep a compact readability floor and use the existing horizontal Scroll region when their combined minimums do not fit.
- The README's CDN template command and the icon and token hints in the DropdownMenu item and Indicator docs name the scoped
npx @astryxdesign/cli, which runs this CLI whether or not it is installed. Barenpx astryxfetches an unrelated npm package until the CLI is a dependency. - align statusVariant documentation and test coverage across ComplexSelector, Tokenizer, and Typeahead
-
A click on a composed control the short list missed — a
role="tab", a segmentedrole="radio", aSliderin a cell — no longer toggles the row underneath it. -
The text-selection guard is scoped to the row instead of asking the document for any selection at all. Text selected elsewhere on the page no longer makes every row in the table inert.
The walk also stops at the row rather than climbing to
document.body, so an interactive ancestor of the whole table cannot suppress row clicks.
-
Apps can opt into package-managed themes with
astryx theme add <slug> --import. The command records the theme in one generated app module with its production CSS, optional font CSS, and default slug. Usetheme removeandtheme useto manage that record, and passthemes[defaultThemeSlug]to<Theme>. Plainastryx theme addkeeps its released source-copy behavior, options, stdout, exit status, and everytheme.adddata field. It now warns thattheme ejectis the explicit source-fork command andtheme add --importis the way to use the package-managed theme. Its machine result adds theDEP-0005deprecation entry; the cleanup isCLN-0005in a later scheduled minor.Use
defineTheme({extends: importedTheme, ...})for ordinary customization.theme ejectcreates an independent local fork with its descriptor. JSON callers receivetheme.appfrom import, remove, and use, ortheme.ejectfrom eject.Existing bundled source copies stay where they are and keep their bytes. Run
astryx upgrade --from 0.6.4 --path . --applyto add the missing unmaintained descriptor beside each bundled copy insrc/themes. Until then, theme commands skip those copies andtheme listand doctor name them as unmigrated. Package integration themes keep their released complete-directory copy, including the authoring descriptor.ASTRYX_THEMEis no longer read. In a project with a generated app theme module, component metadata reads that record's default built theme. Without the module, the releasedpackage.json#astryx.themelookup keeps its meaning. Doctor loads every recorded built module through the same theme adapter, so a broken runtime import failstheme-ownersinstead of passing.theme buildalso writes<out>.css.d.tsso TypeScript accepts generated CSS imports. Check mode stays compatible with released output sets that do not have this new stub yet.Integration themes can export built modules and stylesheets.
astryx integration add themecreates those exports, andintegration verifychecks the exported paths against the packed package and source. Missing partner exports alone do not block packing. Every declared export must resolve; staleness is a warning for an incomplete set and an error for the complete importable module and stylesheet pair. -
Every result now names the package it comes from. A
--jsonresult about one artifact (a component and each of its projections, a doc topic, index, section, or docs-tree node, a template, a hook, andswizzle) carriespackagein its envelope, directly aftertype. A result that lists artifacts gives each item its ownpackage: everysearchhit,build's start, alternatives, blocks, and components, a doc's sections, a docs-tree node's children,--blocksentries, and thecomponent --listandupgrade --listentries. Text names the same package;--source,--showcase, and a template's source still print only the source on stdout and name the package on stderr. Existing fields are unchanged. -
Deprecate the
astryx layoutcommand group Theastryx layoutcommand group (expand,check,grammar) is deprecated. Useastryx buildto choose the template to start from,astryx templateto scaffold it, andastryx docs layoutfor layout guidance.In human mode each invocation prints a stderr warning naming the replacement. In JSON mode the response envelope carries machine-readable deprecation metadata in its
metafield. Canonical stdout, exit codes, and thelayout.expand/layout.check/layout.grammarresponse schemas are unchanged.Deprecation lifecycle (
spec:AST-017FR28, FR31) — removal only in a later minor whose frozen manifest carries both ids of a pair: -
An integration component can replace a Core component, once its package opts in An
@acme/navcomponent whose doc setsreplaces: 'SideNav'takes overSideNavfor unqualified component detail, batch selectors, everycomponent --listdetail level,search,swizzle, and gap-report routing, when@acme/navdeclares"@astryxdesign/cli": ">=0.6.7"inpeerDependencies(optional inpeerDependenciesMeta). Every result names its package,--package @astryxdesign/corestill selects the original Core component, and the replacement keeps answering to its own name.component SideNav --package @acme/navalso selects it.Any
@astryxdesign/clirange that starts at 0.6.7 or later is the opt-in, including the>=0.7.0thatintegration add doc --parentwrote in 0.6.4 and 0.6.5, andintegration add themein 0.6.5. A package whose range admits an earlier CLI keeps its component under its own name and Core stays selected, and an app that loads it sees no new output; its author gets warnings naming the range to add, fromastryx doctor integration componentsandintegration verify. For a package that declares the range,astryx doctor integration componentsreports a missing Core target, an invalid value, a component named after a different Core component, or two replacements for one target in the package as errors and exits 1. When several packages replace one target, an explicitly configured package beats an autolinked one, the later configured package wins, and Doctor warns. -
Add the model comparison page template with responsive sticky context and accessible interactions. (#6251)
-
New
table-collapsiblepage template: several tables on one page, each in a collapsible card with its own columns, for groups that do not share a schema. Every table sorts on its own, one time range in the page header drives all of them, and rows expand in place into a full-width history chart. Below 720px each table folds to its name and headline figure. (#6065)
-
Printed commands name the scoped
@astryxdesign/clipackage whenever theastryxbin is not installed for the project -
Compact the generated agent-docs block: fewer lines, same behavioral coverage
-
astryx doctorkeeps itsthemescheck, with the same id, label and fields, beside the new theme checks, so scripts that read it by id keep working. Without a generated theme module it reports what it did before: whether an@astryxdesign/theme-*package is installed, and whether the app's package.json names a theme inastryx.theme. Its fix now namestheme add --import. With a generated module it reports the overall result of the theme checks. In a workspace,themesreads the app's own package.json, the file the CLI reads when it resolves the theme, rather than the package.json beside the rootnode_modules. It no longer counts theASTRYX_THEMEvariable, which the CLI does not read, as a wired theme. -
Doctor says why it skipped a check and what it checked, and
doctor integration validateno longer crashes on a folder it cannot read -
upgrade: exclude node_modules from integration codemod discovery When an integration declared its codemods root as the package root (
codemods: "./"), the version-folder scan treatednode_modules,.git,__tests__, and__fixtures__as version folders and loaded their files as codemods. The sameSKIP_DIRSfilter that the recursive file walk already used is now also applied to the top-level version-folder enumeration. -
Align the layout deprecation (DEP-0006) with the documented envelope and precedent The layout command's JSON envelope now emits
meta.deprecations: [{id, replacements}], matching the response schema doc and the DEP-0005 theme add precedent. ThedeprecatedCommandDoc field renders in--helpand the manifest. The programmatic API exports (layoutExpand,layoutCheck,layoutGrammar) carry@deprecatedin their declarations and generated types. Command and function reference pages name DEP-0006 and the replacement. The DEP-0006 record names its direct authority. All released data fields are unchanged. -
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). -
template: show a grouped summary by default instead of the full catalog Bare
astryx template(no name, no--list) now shows templates grouped by category with counts — about 21 KB instead of the full 149 KB catalog. The full catalog with descriptions is still reachable viaastryx template --list.JSON output is unchanged —
--json templatestill returns the completetemplate.listresponse. This is a text-only rendering change under AST-017 FR11: the machine-readable response schema is the contract; text formatting is not.Trade-off: the grouped output still includes every template id (so
template <id>works from a copy-paste), at the cost of ~21 KB. An even shorter summary (type + count only, no ids) would be ~500 bytes but would require a second step to discover any id. The current shape serves both human scanning and agent copy-paste without a round-trip. -
template: bare
template --typeno longer prints an empty group header -
A section read on a docs-tree namespace answers from the guide that has the section again, so
astryx docs layout side-panelsand the other layout section reads released in 0.6.6 work after the layout split.docs(route, section)returns the samedocs.detail.sectionthe guide's own section read returns, found by key, then by title, in--denseand--zhtoo. When no guide or more than one has the section, the read fails withERR_UNKNOWN_SECTIONand names the guides to read it from. -
astryx searchranks a component, hook, or template that a query word names above a doc that matched only by keyword, heading, or text.font sizefinds Text first again instead of a typography guide. A doc the query names, such asfont setupormigration, or one whose title the query holds, such asresizable side panels, keeps its place. Result scores do not change; only the order between domains moves. -
astryx searchfinds a guide when the query is its route or title in the other number:side panelfinds the side panels guide first again, andheader and footerfinds headers and footers. A section's heading still doesn't count as its topic's name, sofont sizekeeps finding Text first. -
Fix comparison table category from "Table - Frozen Column" to "Table - Comparison" The category described an implementation detail (the frozen label column) instead of what the template is for. Builders searching by layout type now find it under Comparison.
-
Template and build tell you what a scaffolded template needs that your project lacks. When
astryx template <name> <path>scaffolds a file that imports packages your project does not have (e.g.@heroicons/react,recharts,lucide-react), the receipt now includes a ready-to-run install command with the project's package manager and version ranges from the CLI workspace.astryx buildshows the same note on its start template.When a scaffolded template uses StyleX and the project has no compiler plugin configured, the receipt warns and points to
astryx docs styling-overview. The agent-docs block no longer says "don't use xstyle" flat-out — it acknowledges that some templates need it and links the setup doc.detectStylingSystemnow recognizes the official@stylexjs/rollup-plugin,@stylexjs/webpack-plugin, and@stylexjs/nextjs-pluginalongside the existing entries. -
astryx theme buildprints one line per built theme, plus one line naming fonts the themes do not load. Before, every theme printed the same install example and font recipe, so a build of many themes printed the same blocks again and again.--detail fullprints the install example and font recipe, once for a batch instead of once per theme; for one theme it prints what the default printed before. Only the text report changes:--jsonoutput,--check, exit codes and written files are unchanged. -
Wherever the CLI shows the
<Theme>wrapper, it now also shows whereThemecomes from:import {Theme} from '@astryxdesign/core'. This covers theastryx initnext steps,theme addandtheme buildoutput, the doctor fix for an unimported theme module,buildhelp, and the theme guides. -
astryx theme add <slug> --importnames the npm package that owns the added theme, in its JSON envelope (package, directly aftertype) and in its text output, the same way other results about one artifact do. A local theme has no package, so its result has nopackage, andtheme removeandtheme useare unchanged.astryx theme add --listkeeps its JSON. Its text now namestheme add <slug> --importto use a theme andtheme eject <slug>to fork one, instead of the deprecated copy form.astryx doctoradds atheme-managementcheck, and focusedtheme-*checks once a project has a generated theme module. -
One-off commands in a classic Yarn (1.x) project use
npx @astryxdesign/cli …instead ofyarn dlx, which classic Yarn does not have
-
astryx docs migration,internationalization,styling,styling-libraries,typographyandtokensstill work and now list focused guides. Each section is also readable under its guide, for exampleastryx docs tokens/tokens-spacingorastryx docs styling/tokens-and-setup stylex-setup, andastryx docs <topic> <section>keeps working: every section key these topics had still opens the same section.astryx docs tokens --depth all --detail fullprints every token table, and atoken-reftotokenskeeps resolving through the guide that holds the table. Integrations: these six names are now docs-tree sections, not topics, so an integration doc that declaresextendsorreplaceswith one of them is reported as naming no topic, and its content no longer shows. To keep that content, ship it as your own topic under a new name, or as a guide in your package's own section (astryx integration add doc <name> --parent <your-section>). Topics that are still flat, such asthemeorcolor, can still be extended or replaced.(#7184)
-
astryx docs layoutstill works and now lists the focused guides. Each section is also readable under its guide, for exampleastryx docs layout/side-panelsorastryx docs layout/scaffold shell, andastryx docs layout <section>keeps working. (#7125) -
astryx docs layoutopens with its overview again, andastryx docs author-a-themeanduse-a-themerestore guidance the theme split dropped: what a badextendsbase does, how adaptation rules are validated, what__builtmeans, the icon-registry specifier traps,registerTheme, and wiring built themes in development too. (#7204) -
Add a choose-a-path styling overview and restore the import-based workflow across the theme guides. (#7134)
-
Split the theme documentation into focused guides for using and authoring themes, with a concise overview that points to both workflows. (#7141)
-
A hint uses
npx astryx …(orpnpm exec,yarn,bunx) only when theastryxbin is innode_modules/.bin, in the project or a folder above it. Otherwise it printsnpx @astryxdesign/cli …(pnpm dlx,yarn dlx,bunx), as it already did when the CLI ran from an npx or dlx cache. -
A global install, or a workspace install used in a fresh package, used to print
npx astryx …. The npm package namedastryxis not this CLI, so following that hint fetched an unrelated package. -
astryx blogfollows the same rule, and the incident console template names the scoped package.Classification: contract-restoring. JSON shapes are unchanged; only hint text changes, and projects with the CLI installed keep
npx astryx …. -
deprecation
DEP-0006/ cleanupCLN-0006—astryx layoutcommand group (expand,check,grammar) -
A check that needs the loaded project quotes the reason the CLI could not load it, instead of only "Skipped".
-
The config check warns when astryx.config imports but the CLI cannot load the project from it. Before, it said the config loaded cleanly.
-
Integration contributions name the integrations they checked; with none loaded, the check reports info and says there is nothing to check.
-
The agent-docs check reads every file init can write (Hermes included) and looks in the project root as well as the working directory, so running doctor from a subfolder no longer reports that there are no agent docs.
-
astryx doctor integration validatenames a folder it cannot read and keeps checking. A declared root that is a file or that cannot be read is reported asinvalid_rootorunreadable_rootinstead of a raw error or a crash.Classification: contract-restoring. No check becomes stricter: every new finding is a warning or info, and the only exit-code change is that an unreadable folder outside every root no longer crashes validation.
-
Whenever the CLI prints its scoped one-off form (it ran from an npx or dlx cache, or the
astryxbin is not installed for the project), a Yarn project gotyarn dlx @astryxdesign/cli …. On classic Yarn that command fails. -
Classic Yarn is read from the declared
packageManagerversion, theyarn.lockheader (# yarn lockfile v1), a committed.yarnrc(without.yarnrc.yml), or the runner. Yarn 2 and later keepyarn dlx.Classification: contract-restoring. JSON shapes are unchanged; only hint text changes.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
- Each theme package now exports
/fonts.cssbeside/builtand/theme.css. Import it when the theme uses non-system fonts.
Thanks to everyone who contributed to this release:
@AKnassa @cixzhang @ernestt @humbertovirtudes @imdreamrunner @josephfarina @Lee-Dongwook @ManoharPaturi @vjeux
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.6...v0.6.7
Astryx 0.6.6
Astryx 0.6.6 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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/MarkdownIt 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. In
DropdownMenu,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. A
DropdownMenutrigger 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 slide then picks.useMenuPressgainsonTriggerPress,triggerProps,isTriggerClickFromPressandlongPressDelayMs. -
DropdownMenuSubMenu drills in on a phone instead of opening a flyout. When a coarse pointer opened the menu, a sub-menu row replaces the menu's rows with its own and a "Back to " row, in the same box; Back, Escape or ArrowLeft return to the row. Works in compound and data mode, inside
DropdownMenuandContextMenu;presentation(flyout|drill-in|adaptive) overrides the policy. -
MultiSelectorcan offer aCreate "<query>"row for a search that matches nothing. WithhasSearch, the newhasCreateswitch puts aCreate "<query>"row first in the list when the trimmed query equals no option label under the search's own case-insensitive matching and the options have loaded. Picking it, or Enter with nothing highlighted, callsonChangewith the query appended to the value and a second argument{type: 'create', query}(exported asMultiSelectorChange), then clears the search; the caller adds an option for the new value in that same update. Every other change passes no descriptor, so existing one-argument handlers are unchanged.hasCreatewithouthasSearchwarns in development and offers nothing. Off by default. -
MultiSelectorcan hang off a control the caller renders. A newrenderTriggerrender prop renders the control the panel 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 listbox is anchored to it, named bylabel, takes focus on open, and focus returns to the control on close.handleRef(open/close/toggle/isOpen, theComplexSelectorHandleshape) andonOpenChangelet the caller open the panel from a keystroke elsewhere and observe every open and close. All three are off by default; existing selectors are unchanged. -
Sub-menu flyouts stay open while the pointer travels toward them, and a press on a sub-menu row opens it without ever closing the menu. In
DropdownMenuSubMenuthe flyout now stays open while the mouse moves from the row toward the flyout inside the triangle to its near edge, and closes after the existing delay once the pointer has left both the row and that triangle, so a diagonal path to the flyout no longer folds it. A click or release on a sub-menu row opens its flyout; on an open one it confirms the flyout and moves focus into it instead of toggling it shut, as macOS sub-menu rows do.useMenuHovergainsflyoutRefand passes the leave event toonMouseLeave. -
Touch press model: under a coarse pointer the bare
:activearm is dropped and a delegated, document-level controller paints the press the way a native list does — nothing for 150 ms, then the full pressed overlay on the next frame; cancelled with no fade by 10 px of travel or by a scroll claiming the gesture, and dead until a new touch; a tap shorter than the delay paints at the lift; the release fades over 200 ms, a real fade on every surface: the pressed overlay is the pressed token at--astryx-press-alpha, a registered custom property (@property, syntax<number>) the release arm animates 1 → 0, declared once by the shared overlay styles as--_press-paintand read by whatever paints it. The controller writesdata-astryx-press="on"|"fading"on the nearest element markeddata-astryx-pressable, which every Astryx surface that paints a press now carries; a mouse keeps:active. Public API for a local pressable:usePressFeedback()from@astryxdesign/core/hooks(returns the marker to spread; installs the controller on first mount) andinteractionOverlayStylesfrom@astryxdesign/core/utils(compose one of its variants on the marked element). A press is themed through--color-overlay-pressed, which the hold, the flash and the fade all read.
-
Selector and MultiSelector announce the empty-state message they actually show, and announce it on every path that reaches one. Two defects, one cause — the live region was fed from the props instead of from what rendered:
-
Typeahead,Tokenizer, andPowerSearch(which composesTokenizer): clicking the search/combobox input after the dropdown closed without a blur now reopens it.BaseTypeaheadonly ever opened its dropdown in response to a realfocusevent. Any flow that closes the dropdown while leaving the input focused — selecting a result (which re-focuses the input internally after clearing it), pressing Escape, or a composing component (PowerSearch's token add/remove, e.g.) imperatively re-focusing the same input once it's done — dispatches no newfocusevent, sincefocus()is a no-op on an element that's already the active element. The input looked focused and clickable, but clicking it did nothing until the user clicked elsewhere first and back.BaseTypeaheadnow also opens on click, specifically when the input was already focused before the click began (checked atpointerdown, before the browser's own default action moves focus — a click that itself just caused the input to gain focus is left to the existing focus path, so the two don't double-fire a bootstrap fetch on a single first click).Only
TypeaheadandTokenizercomposeBaseTypeaheaddirectly —Selector,MultiSelector,CommandPalette, andDateTimeInputuse their own separate combobox implementations (which only followBaseTypeahead's conventions, not its code) and are unaffected by this change.Fixes #6845.
-
Card: yield to a flex row or grid track instead of widening it to the card's content. A card no longer holds its row or
1frgrid track at its min-content width, so a long unbroken value (an ID, a hash) inside a card can no longer push side-by-side cards past a phone screen. Cards that fit are unchanged. Content that cannot wrap is clipped at the card edge, as it already was for cards with an explicitwidth; truncate such values withText maxLines={1}and give wide content its own scroll region. An explicitwidthis now the card's preferred width in a row rather than a floor. To hold it, wrap the card inStackItemin a flex row, or set a consumerminWidthon the Card in Grid. -
Chat: center inline tokens on the line box using 1lh and vertical-align top, fixing vertical misalignment against adjacent text and CJK characters.
-
Prevent disabled
ClickableCardlinks from retaining an activatable destination. -
Make Code's complete public API and theme target discoverable in component documentation.
-
Keep collapsible code blocks named and recoverable when header controls disappear, honor zero-pixel height limits, and document the public root ref.
-
Expose Collapsible open, disabled, position, and divided states to themes, and document grouped state ownership and root customization.
-
Preserve
CollapsibleGroupcontext identity when a controlled string value is unchanged, avoiding unnecessary grouped-item rerenders. -
Let CommandPaletteFooter wrap translated guidance on narrow screens, correct its composition example, and add owned audit coverage.
-
Field-based inputs (TextInput, Selector, DateInput, and the rest of the input family) can now be shrunk by their row: the Field root resets its automatic minimum size, so a filter bar of a search box and selectors no longer pushes past its container on phones
-
Defer a layer
show()that arrives while another popover is mid show/hide, so a tooltip trigger regaining focus from a closing popover no longer throwsInvalidStateError. -
Markdown: read angle-bracket link destinations to their closing bracket
Markdownnow reads an angle-bracket destination as CommonMark specifies: parentheses inside the brackets are part of the address, so[a](<b(c>)links tob(c); an escaped bracket inside is part of it too, so<b\>c>isb>c; and a line ending inside the brackets makes the text no link. Unsafe schemes are refused as before. -
Markdown keeps a backslash unless it escapes punctuation, and a backslash at the end of a line is a line break
C:\Users\Adarendered asC:UsersAda: any character after a backslash swallowed it. As CommonMark specifies, a backslash now escapes only ASCII punctuation (\*,\#,\\, …); before a letter, digit, space, or other character it stays as written. A backslash at the end of a line is a hard line break. Image alt text follows the same rule. -
Markdown: cap list and blockquote nesting, so deep input cannot crash rendering
Markdownnow nests lists and blockquotes at most 100 levels deep, as it already caps emphasis; content nested deeper reads as text. A list or blockquote nested thousands of levels deep, as a crafted message can be, used to overflow the stack and throw while parsing. -
Markdown: read a block quote marker as CommonMark does
Markdownnow reads a line that starts with up to three spaces and>as a block quote, whether or not a space follows the>:>quote,> quote, and>>> nestedare quotes, as CommonMark specifies. These lines used to show as plain text with their>marks. -
Markdown: start a new list when the bullet changes
Markdownnow starts a new list when a bullet list's marker changes —- athen* bare two lists — as CommonMark specifies, and as ordered lists already did when their delimiter changes. -
Markdown shows character references such as
&,©, and©as the characters they nameFish & chips © 2026rendered with the references spelled out. Named references (every name in the HTML standard) and decimal or hexadecimal numeric references in text now render as their characters, as CommonMark specifies; inside inline code and code blocks they stay exactly as written. An unknown name or a reference without its closing;stays literal, and a decoded character is never read as Markdown syntax. -
Markdown: close a code span only at a backtick string of the same length
Markdownnow ends a code span at the next run of exactly as many backticks as opened it, never at part of a longer run, and reads runs of any length, as CommonMark specifies.`onetwo`` is one code span holding`` `, and four or more backticks open and close spans too. A run with no closer of its length stays text. -
Markdown makes a hard line break from two spaces or a backslash before a Windows (CRLF) line ending Documents saved with CRLF line endings lost their hard line breaks: the carriage return sat between the trailing spaces or backslash and the line feed, so neither was recognized and the lines ran together. Both now break the line exactly as they do in an LF document, in paragraphs, links, and block quotes and while streaming; code spans, code blocks, and table cells are unchanged.
-
Markdown: read a code fence's language after spaces
Markdownnow reads a fenced code block's language as the first word of its info string after any spaces, as CommonMark specifies, so~~~ jsand``` jsare JavaScript blocks rather than blocks with no language. Fences written without a space read as before. -
Markdown: give an image the plain text of its description as alt text
Markdownnow reads an image's description as inline content and uses its plain text as the alt text, as CommonMark specifies, so![`a]b` *c*](u)has the alta]b crather than the raw source with its backticks and asterisks. -
Markdown image alt text shows character references and escapes as the characters they name
gave the image the alt textFish & chips, so a screen reader announced "amp". Alt text now resolves character references and backslash escapes the same way body text does, for inline, standalone, and reference-style images; an escaped&and unknown names stay literal. -
Markdown: read an ATX heading indented up to three spaces as a heading
Markdownnow reads a heading line indented by up to three spaces, such as# Title, as a heading, as CommonMark specifies. Such lines used to show as plain text with their#marks, except at the very start of a streamed message. -
Markdown: read a code fence indented up to three spaces as CommonMark does
Markdownnow reads a code fence indented by up to three spaces as a code block, and a closing fence may be indented the same way, as CommonMark specifies. Each code line loses as much indentation as the opening fence has. A closing fence has only spaces or tabs after it, so a line such as```jsinside an open block stays part of the code. Such fences used to show as raw backticks or tildes in a paragraph, and an indented closing fence left the block open to the end of the document. -
Markdown: show a link whose destination opens with
<but is no angle-bracket destination as textMarkdownnow shows[a](<b>c>),[a](<b), and[link](<foo\>)as text, as CommonMark specifies: a destination that opens with<must be one whole angle-bracket destination, with no line ending inside, even after a backslash. Such links used to fall back to a link to the whole text between the parentheses. -
Markdown: keep lazy continuation lines fast in deeply nested input
Markdownno longer slows to seconds on a deeply nested blockquote or list followed by a lazy continuation line, as a crafted message can be: checking whether the line continues a paragraph now reuses what the parser already read at each level, and checking a long line for a thematic break no longer copies it. Lists and blockquotes nest at most exactly 100 levels deep. -
Markdown: find link destinations in linear time
Markdownno longer searches the rest of the text for every link or image whose destination never closes, which made a message with many of them take seconds to render. Each parenthesis now pairs once per text, so such input renders in milliseconds; links read as before. -
Markdown: decode character references and backslash escapes in link destinations, and close link text at an unescaped bracket A link or image destination now reads as CommonMark specifies:
[x](https://a.com/?a=1&b=2)links tohttps://a.com/?a=1&b=2,[x](a\)b)links toa)b, and reference definitions decode the same way. The URL safety check runs on the decoded destination, so an encoded unsafe scheme such asjavascript:is refused like the plain one. Link text and image alternative text close at the first unescaped], so[a\]b](u)is a link with the texta]b. -
Markdown: pair link text brackets as CommonMark does
Markdownnow pairs the brackets of link text as CommonMark specifies: link text may hold balanced brackets, so[a [b] c](u)is one link; the innermost bracket makes the link, so[a [b](u)links onlyb; and a link inside link text wins, so[a [b](u) c](v)shows the outer brackets as text instead of nesting links. -
Markdown: a code span in link text hides its brackets
Markdownno longer ends link text at a]inside a code span, since code spans bind tighter than links (CommonMark).[`a]b`](/u)links the codea]b, and[`[x](javascript:y)`](/rel)links the code[x](javascript:y)to/relrather than reading a link inside the code. -
Markdown links and images go to their destination when the source also gives them a title
[notes](https://example.com/notes "Release notes")linked tohttps://example.com/notes "Release notes"— an address that does not exist — and a titled image pointed at a source that could not load. A title after the destination, in double quotes, single quotes, or parentheses, is no longer part of the link or image address, and a destination in angle brackets may contain spaces. Content in any other shape keeps its meaning. -
Markdown: keep content indented into a list item in the item after a blank line
Markdownnow keeps lines indented to a list item's content in that item after a blank line, as CommonMark specifies: a nested list, a fenced code block, or another paragraph under a numbered step stays in the step, and the numbering continues after it. Such content used to end the list, so the sub-items showed as a separate list and a step's code block showed as raw text. -
Markdown: a list that mixes task and plain items shows each task item's checkbox In a list such as
- [x] Done,- Plain,- [ ] Open, each task item now shows its own read-only checkbox, checked or open and named by its text, where its marker would be, and each plain item keeps its marker; the list stays one list. Before, the task items lost their checked state and showed bullets. Lists of only task items are unchanged. -
Markdown: pair emphasis and strong markers by CommonMark's rules, so strong inside emphasis keeps its strong Runs of
*and_now pair as CommonMark specifies: by which side of a word they touch, by the nearest compatible opener, and by the rule of three.*see **bold** more*and*see **bold***render the bold inside the emphasis,**bold *both***renders the emphasis inside the strong,__foo, __bar__, baz__nests, and an escaped\*inside emphasis stays literal. Before, the first matching marker closed emphasis early and left stray*or_in the text.***text***still renders as strong around emphasis. Streaming closes an unfinished**boldbefore trailing spaces, so the partial text keeps its formatting. -
Markdown: keep the indentation of a message's first line
Markdownnow keeps the indentation of a message's first line, as it already did for every later line: two bullets indented by the same amount stay side by side, an indented numbered list keeps each item, and an indented table keeps its columns. A message that started with indented content used to render it differently from the same text after its first line. -
Markdown: end a list at a thematic break
Markdownnow reads a line such as* * *or- - -after a list item as a thematic break that ends the list, as CommonMark specifies, rather than as another item holding a nested list. -
Markdown: check a line's trailing spaces in linear time
Markdownno longer slows to seconds on a line holding a long run of spaces before its last word, as a crafted message can: deciding whether trailing spaces make a hard line break now counts them from the end of the line instead of matching a pattern that retried from every space. Line breaks read as before. -
A DropdownMenu returns focus to its trigger after a pointer dismissal, without painting a focus ring.
DropdownMenuused to blur its trigger after a pointer pick or an outside press, dropping focus to the page so the next arrow key went nowhere. Focus now returns to the trigger with the focus ring suppressed after pointer input, as the bottom-sheet presentation already did, and stays visible after a keyboard pick. A press outside that landed on a focusable control keeps focus there. -
A nav or menu trigger disabled while a hover is in flight no longer opens its surface. Hover intent schedules an open after a short delay. Disabling the trigger in that window left the scheduled open to land anyway, on a surface whose handlers were already inert — so it opened and nothing could dismiss it. The pending intent is now abandoned when the integration is disabled.
-
A Layout nested in AppShell content, or in any other Layout, no longer inherits the outer Layout's
padding. Its header, panels, content, and footer keep their default inset (or the enclosing Card, Section, or Dialog padding) instead of rendering flush against the content edge. An explicitpaddingon the nested Layout still wins. -
Restore Outline's visible keyboard focus indicator.
-
Refresh stale cached field entries when
PowerSearchreopens an already-focused input after its search source changes (#6845). -
Preserve consumer className on ToggleButton while retaining its theme classes.
-
Navigation URL safety: refuse a data URL with spaces before its media type The shared URL safety check now ignores spaces after a URL's scheme before it compares the scheme, as a data URL's media type does, so
data: text/html,…is refused likedata:text/html,…. In Markdown this also refuses links, images, and angle autolinks whose destination decodes to that form, such asdata: text/html,…. Accepted URLs are returned as before. -
Let chat follow reach the bottom when browsers round scroll offsets.
-
Toolbar's center slot no longer clips its content, so focus rings, box-shadows, and the selected-tab indicator of a TabList in
centerContentare drawn in full. Center content that cannot shrink and is wider than the space between the start and end slots now overflows it instead of being cut off. -
Forward accepted DOM props such as
id,data-*, andonClickto the Typeahead and Tokenizer root elements. Preserve existing ref, styling, anddata-testidtargets and Typeahead's built-in focus and edit behavior inside InputGroup.
-
emptyTextandemptySearchTextaccept aReactNode, but the region spoke the value only when it was a string and announced the built-in default otherwise. A product that put a link or a "create one" row in the dead end showed one message and announced another, so the screen-reader user was told something the sighted user was not reading. -
An empty result that arrived after the keystroke — an async load landing with nothing that matches an active query — was never announced at all. The message sat on screen and the region stayed silent.
Both components now read the rendered message out of the DOM and announce that, from one place that watches the panel's state rather than the keystroke. An element is announced as written, text a child component generates is announced correctly, and anything marked
aria-hiddenis left out of the announcement exactly as it is left out of the screen. A loading panel still announces nothing. -
deprecation
DEP-0001/ cleanupCLN-0001—Tokenizer.emptySearchResultsText -
deprecation
DEP-0002/ cleanupCLN-0002—Typeahead.emptySearchResultsText -
deprecation
DEP-0003/ cleanupCLN-0003—BaseTypeahead.emptySearchResultsText -
deprecation
DEP-0004/ cleanupCLN-0004—ChatComposerTrigger.emptySearchResultsText -
An explicit
widthonPopoverandmenuWidthonDropdownMenuorTypeaheadrender at their size up to the viewport. They used to be capped to the room beside the trigger, so a 352px menu opened from a control near a panel edge rendered 274px wide. -
A layer that does not fit beside its trigger flips; one that fits on neither side keeps its size and slides into view while its trigger is on screen. Once the trigger has left the viewport the layer holds its position and size instead of chasing the edge.
-
An app that floats a persistent bar over a viewport edge — a phone navigation bar — declares it once, as
inseton theLayerProviderit already mounts:<LayerProvider inset={{blockEnd: 56}}>. Every anchored layer then ends above the bar, and the toast viewport rises by the same amount, so one bar is declared once for both. Every edge defaults to zero, so nothing moves by default; an existingtoast.insetkeeps its meaning as the toast-only override.One additive prop (
LayerProvider.inset, typeLayerInset); no other prop, type, or default changes.spec:AST-059holds the decisions; theCore/Layerstories show each behavior.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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. -
Templates declare their
keywords, andbuildtells a part of a page from a page by the components the project can use (#6805) -
buildchooses where to start with a checked-in table of word weights blended with the page ranker -
astryx docs <route> --depth <levels>reads as far down the docs tree as you ask, from one doc to everything below it.--depth 0reads only the namespace you name,--depth 1adds the docs right below it (what a read without--depthshows), and--depth allgoes to the bottom. With--depth,--detailsets how much of each doc below shows:brief(the default) is one line each, named by where it sits so you can open it,compactadds its sections, andfullprints it whole. Soastryx docs cli --depth allis a map of every CLI doc, andastryx docs cli/integrations --depth all --detail fullprints the integration guides as one read. Where a read stops, a namespace says how many docs sit below it.--jsonreturns the same tree asdocs.node: each child carries its ownslotswhile the read goes deeper,childCountwhere it stops, and its text atcompactorfull.docs()takes the samedepthanddetailoptions. Reads without--depthare unchanged. -
Point at
discoverwhere people look for things to add Nothing an agent reads nameddiscover, so agents asked to find a theme searched the package registry instead. The agent blockastryx initwrites now listsdiscover <words>(integrations you could add, and the ones you have),theme listends withMore themes in packages you could add: astryx discover theme, and a text search ends withMore in packages you could add: astryx discover <query>(except--type hook, since no integration adds hooks). JSON output is unchanged. -
integration add theme --from <base>forks an existing theme as the starting point instead of a blank scaffold. The new theme copies the base's source files — renamed and rewritten for the new slug — with no link back. Use--fromwhen you want to change a lot; for a small change that stays linked, useextendsindefineTheme.
-
Say when a command did nothing:
upgradereportssourcePathFound, the integration checks reportvalidated. Two commands could legitimately do nothing and produce an envelope identical to a clean success. Both now carry the fact in a field of their own response instead of only in human text.astryx upgradedefaults--pathto./src. A project laid out asapp/(or a typo) skipped every code codemod and still reported exit 0,filesChanged: 0,errors: []and "Upgrade complete". The only warning was a log line--jsonsuppresses by design.upgrade.runnow carriessourcePathFound, and the human completion line names the directory it did not find.astryx doctor integration validate|components|docs|templatesreturned{name: null, version: null, issues: []}and exit 0 when no integration manifest was found — the same shape as a validated, healthy integration. All four envelopes now carryvalidated, false only when nothing was inspected. -
astryx manifest --jsonnow takes each command's examples from its CommandDoc and its response types from the API function it wraps, so no example differs from the documented one andupgradelistsupgrade.registry, whichupgrade --registry --jsonalready emits. -
astryx template <name> <path>andastryx layout expandnow say when they replaced Astryx demo media. Thetemplate.copyandlayout.expandreceipts carrydemoMediaReplaced, the number of demo image and video references that became placeholders (one per reference, however many fixture paths its URL carries), and the text output names the file to update (whenlayout expandprints the code instead, the same line follows it as a comment, so the output is still valid TSX). Nothing about the copy itself changed. -
astryx template <name>andtemplate()now return the same source thatastryx template <name> <path>writes, and say how many Astryx demo media references they replaced. Demo images and videos that only Astryx's own previews serve are replaced the same way in both, so code copied from the printed source no longer points at media your project doesn't have.template.showgainsdemoMediaReplaced(0 when the template carried none); in text mode the count is stated on stderr so the printed source stays exact. -
A write that fails reports ERR_WRITE_FAILED instead of a raw Node errno.
astryx templateinto an unwritable directory returned{"error": "EACCES: permission denied, open '/home/you/project/readonly/x.tsx'", "code": "ERR_UNKNOWN"}, andswizzlereturned themkdirequivalent. Two things were wrong: ERR_WRITE_FAILED is already in the frozen error registry for exactly this case, and the message carried an absolute host path where every other Astryx message names its target relative to the project.Both now throw ERR_WRITE_FAILED with the errno kept (it is the part that says what to fix) and the target named relative to the project. Nothing is left half-written: a
swizzlethat fails part-way removes the files it already copied and puts back any it replaced before it reports the error, and the message names any file it could not restore. -
When
discover --availableruns without a discover source, the CLI now explains what discover sources are and where to find Astryx packages on npm, instead of the misleading message that told users to add package names they had no way to find. The basediscoverwith no integrations also gains a pointer to npm and the integrations docs. -
A package with a namespace doc or a placed guide needs
@astryxdesign/cli0.6.4, not 0.7.0. Published 0.6.4 reads an integration's docs tree: it lists the namespace and reads each guide placed in it. 0.6.3 rejects a namespace doc and hides every doc topic the package ships.integration add doc --parentwrote"@astryxdesign/cli": ">=0.7.0", a range no released CLI satisfies, andintegration verifyfailed a docs-tree package whose CLI peer started at 0.6.4.integration add doc --parentnow writes">=0.6.4", marked optional, andintegration verifyaccepts it. A template that setsreplacesorkeywordsstill needs">=0.7.0". -
gap-reportfails when a listed integration cannot load, instead of reporting a clean result. An integration whoseastryx.integrationmodule throws on import or fails validation was left out of the handler set. With no other handler, the report fell through to the built-in GitHub fallback for Core: the command exited 0 withconsent_required, printed nothing on stderr, and offered--confirm-publicto file on Core's public tracker a report the integration might have been meant to receive.The unloadable integration now records a failed delivery in its config position, with the load error and the fix in its message. Like any handler, it turns the fallback off, so the report never goes to another package's tracker. The command exits 1, and a failed or partial report now prints each failed delivery on stderr as well as in the receipt.
-
astryx integration add theme <name> --from <base>now adds the packages the copied theme files import todependencies. A fork of a bundled theme such asneutralcopies itsicons.tsx, which importslucide-react, but the package did not declare it. Soastryx theme buildon the fork failed with "Cannot find module 'lucide-react'", and an app that installed the package hit the same error.--fromnow adds each package the copied files import, at the range the bundled themes use. It leaves out Core and React, which every Astryx app already has, and anything the package already declares. -
astryx integration packwithout--checknow points only atastryx integration verify. It used to say "Pass --check to verify the integration tarball, or runastryx integration verify", which sent people to the deprecated spelling. It now says thatintegration packis nowintegration verify, and thatnpm packbuilds the tarball. The error code and exit code are unchanged, andintegration pack --checkstill runs the same check as before. -
Search: differentiate all-word matches by total quality; index themes Within the all-words tier, every candidate whose strongest token hit was a keyword scored the same (e.g. 157), regardless of how the other query words matched. A doc matching both words by keyword outranked nothing, and
search "how to use a theme"put API reference docs above the consumer theme guide.The bonus now uses the sum of ALL token scores instead of just the strongest, so a candidate matching every word by keyword outranks one matching keyword + prose. The
themedoc also gains consumer-facing keywords so it surfaces for questions like "how to use a theme" and "how to apply a theme".Themes are now a search domain: bundled and integration-provided themes appear in results with their slug, displayName, package, and the
astryx theme addcommand.--type themefilters to them. Like--type doc, it works outside an app, where an open search now covers the docs and themes. Search help, the manifest, and the API reference list the new domain and its result fields. -
Search ranks a theme's description as prose, not as keywords A word a theme shares with your query only through its description, such as
minimal,focusorcontent, now ranks the theme like any other description instead of like a declared keyword, so it no longer lands above the components, hooks and docs that declare that word. A theme still comes first for its own slug or display name:astryx search neutralfinds the Neutral theme first. -
A package that ships a theme or a doc section
idneeds@astryxdesign/cli0.6.4, not 0.7.0. Published 0.6.4 reads typed theme descriptors and section ids; 0.6.3 rejects both and hides the package's themes or doc topics. The 0.6.5 notes said a stable CLI before 0.7.0 rejects them, sointegration add themewrote"@astryxdesign/cli": ">=0.7.0", a range no released CLI satisfies, andintegration verifyfailed a theme or section-id package whose CLI peer started at 0.6.4.integration add themenow writes">=0.6.4", marked optional, andintegration verifyaccepts it for themes and section ids. A template that setsreplacesorkeywordsstill needs">=0.7.0". -
theme buildresolves real icon imports from the selected theme instead of matching comment or string contents. Generated modules preserve named, aliased, default, and namespace registry imports, plus inherited icons with child overrides. Normal builds and--checkreject unsupported inline registries withERR_THEME_INVALIDbefore generating or writing output. Move such a registry into its own module and import it into the theme file. -
astryx theme buildin an app uses the app's installed@astryxdesign/core, and says to install Core when there is none. Run one-off withnpx @astryxdesign/cli, it failed with "Build @astryxdesign/core first (e.g.pnpm -F @astryxdesign/core build)" even when the app had Core installed, because it looked for Core only next to the CLI. It now generates with the Core the project installed, the same Core the app's<Theme>runs on. In an app without Core, the error now says to install it (npm install @astryxdesign/core). The build command stays only for the Astryx repository itself. The error code (ERR_CORE_NOT_FOUND) is unchanged.
-
TemplateDocgains an optionalkeywordslist: the ideas, domains, and other names a builder might use for what the template serves.parseTemplatevalidates it, discovery carries it from every template source,astryx searchmatches it as it matches a template's description, andastryx buildranks page templates on it. Each Core page template's closing list of ideas moved out of itsdescriptionintokeywords, so descriptions describe the layout. -
buildstarts a part of a page where it lives (spec:AST-048FR3), and the project's own components say what a part is: an idea whose head noun is a word of a component's name or keywords, Core's or an integration's, asks for a part, unless the noun names a family of page templates or the idea lists three or more pieces. A part starts from the base template of the family the idea names, else from the app shell; a change to an existing page or part (an idea whose "existing" names a page family or a component, such as "the existing table", and that does not ask for a new page) starts from the app shell. The start's reason says which case applies and why. -
A family's base template leads its family unless a variant matches two terms of its own; a base that cannot start on its own never displaces the variant that leads.
Integration templates that set
keywordsneed@astryxdesign/cli0.7.0 or later. The template metadata object is strict, so a stable CLI before 0.7.0 rejects the field, drops that template, and hides the package's doc topics; onlytemplate --listandsearchprint a warning.integration verifyfails a package whose template setskeywordsuntil it declares@astryxdesign/cli >=0.7.0, as it does forreplaces. -
api/build/kit/weights.mjsscores each candidate start (the app shell and every ready page template) from the idea's stemmed words using the tables inweights.json, and blends those scores with the ranker's own. The blend decides the start of every whole page; a part or an edit starts where it did before, and the ranker's pick of a template the tables do not list stands. Without a weights file the ranker's pick stands. The response's shape is unchanged.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
-
Add a shared
uploadicon, and use it for FileInput's upload affordance instead of the directionalarrowUpThemes 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.
Thanks to everyone who contributed to this release:
@AstryxBot @cixzhang @Geervan @HelloOjasMutreja @imdreamrunner @jiunshinn @josephfarina @kentonquatman @korkt-kim @markselby9 @rubyycheung @thedjpetersen @vjeux
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.5...v0.6.6
v0.6.5
Astryx 0.6.5 — all stable @astryxdesign/* packages ship at this version.
npx astryx upgrade --from 0.6.4 --apply
- 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.
- 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.
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.
-
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)
- 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 radius, and the Next.js StyleX example. The deprecated bare classes are still emitted and will be removed in a later release. astryx docs tokenslists all 258 tokens, adding the data visualization and syntax groups, and shows both halves of everylight-dark()value.- Long sections are split, vague titles renamed, and the
--denseand Chinese versions no longer drop blocks. Eleven long section keys are shorter, such asastryx docs styling stylex-setup, and every old key still resolves. An integration section that extends a Core topic by a section's old title still replaces that section. astryx integration add codemod --tohelp says it takes the Core version whose upgrade runs the codemod.- The agent block that
astryx initwrites now saysupgrade --from <old version> --apply;upgrade --applyalone stops with "Missing required --from". - The contributor-only sections, on adding a semantic icon and on strings and text direction inside components, moved to CONTRIBUTING.md.
- An installed dependency whose
astryx.integration.*manifest cannot be loaded is still kept out of the loaded set, butimplicit-integrationsnow names it and says it contributes nothing. Before, doctor said that no installed dependency ships a manifest. The check stays informational, andastryx doctor integration validate <package>gives the details. implicit-integrationslists only the roots that exist on disk. A package whose declared roots are missing is reported as contributing nothing, with the missing roots named. Before, it listed every root the manifest declared.provider-identitysays how many loaded integrations it could not read, instead of counting only the readable ones.
Thanks to everyone who contributed to this release:
@Geervan @imdreamrunner @josephfarina @nynexman4464 @thedjpetersen @vjeux
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.4...v0.6.5
Astryx v0.6.4
Astryx 0.6.4 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply
-
Add Timer for standardized elapsed durations without React tick renders. (#6438) Use
Timerfor 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) Adds
dialog-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) Use
markdownSoftBreaksPluginfrom@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)
- 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 overflows (#6777). - Give inert Citation references a supported accessible name, and document their linked and inert root contract.
- CodeBlock no longer crashes on custom tokenizer types outside the built-in grammar (a dotted type such as
keyword.control.sqlthrew frominsertRuleand took the whole block down). The generated::highlight()name and--color-syntax-*custom property now pass throughCSS.escapebefore entering the dynamic stylesheet, so every type the CSS parser accepts (dotted, digit-led,_private, non-ASCII) keeps its colour, and no token type can reach outside its own highlight rule. A rule the engine still refuses costs that type its colour, never the block. (#5528) Dialogand 30 other components no longer lose theirborderandbackgroundresets in the shipped CSS. The sources used theborder: 'none'andbackground: 'none' | 'transparent'shorthands, which StyleX's default property-specificity mode drops silently, so the declarations never reachedastryx.css; a consumer that does not loadreset.csssaw the UA<dialog>frame. They are now theborderWidth/borderStyle/backgroundColorlonghands. No API change.- Apply the same narrow blocked-scheme rule to native links, custom routers, clickable surfaces, and Markdown links. Rejected destinations stay visible without navigating or invoking a router, including structured URLs with a separate protocol. Ordinary URLs, safe custom schemes, downloads, and accepted router-object identity are preserved; Markdown image/resource policy is unchanged. (#5524)
- Preserve Tailwind font weights when using the theme bridge. (#6479)
- Ignore IME key events (
isComposingorkeyCode229) in useHotkeys, even with allowInInputs enabled, without preventing their default behavior (#6773). - Keep hug-layout segmented controls content-sized in flex containers (#6643)
- Keep a reopened top layer at the front of Escape dismissal order. (#6073)
- Prevent ancestor text formatting and surface/group context from leaking into Layer content. (#6457) Layer content now starts with theme body typography and neutral text formatting. Core layer content no longer inherits accidental ancestor surface/group membership, including group-owned disabled state, selection, callbacks, and label associations. Intentional groups and required providers created inside the layer still apply. Unrelated contexts, explicit props, themes, and styling overrides remain unchanged.
ListwithhasDividersno longer draws a divider after the lastListItem. The last-item reset used theborderBlockEndshorthand, which StyleX's default property-specificity mode drops silently, so it never reached the shipped CSS; it is now theborderBlockEndWidthlonghand. (#6420)- Localize PowerSearch boolean operators in Japanese and Korean and use locale-appropriate ellipses in Chinese. (#6783)
- Markdown: keep lazy continuation lines inside blockquotes and list items (#6752) Wrapped paragraph lines may omit repeated blockquote markers or list indentation without escaping their owning container. Nested, ordered, unordered, and task-list continuations now preserve their rendered structure, text projection, and source range.
- Markdown tables now use the
spacing-2token (8px) on every header and body cell edge. The tighter inline spacing leaves more room for content in narrow reading columns while keeping rows comfortably readable. - Markdown: render escaped table pipes literally in code spans (#6642) A
\|used to keep a pipe inside a table cell now displays as|in inline code, matching prose cells without exposing the structural backslash. Completed inline-code spans render only their parsed contents inside<code>, without source backticks; standalone inline code otherwise remains unchanged. - Markdown: table columns keep a content-derived width floor and headers stop truncating (#6708). A Markdown table in a narrow reading column no longer squashes every column to a few characters. Each column keeps a floor derived from its own content, measured in characters on the cell's text box, so a column is never narrower than its longest unbreakable token and cell padding does not eat into the floor. Identifiers, URLs, and inline code stay whole, header labels wrap instead of ellipsizing, and a table that is genuinely wider than its container scrolls in Table's own scroll region — which is now the table's only scroll viewport, accessible name, and keyboard stop.
- Align titled sections in DropdownMenu and ContextMenu bottom sheets with their spacious action rows (#6694).
- Keep MetadataList side labels readable beside long badges in narrow containers by allowing value columns to shrink, including numeric columns and custom label widths. (#6598)
- PowerSearch:
enum_listvalue menus now show every value instead of the typeahead default of 10. (#6481) - Selector keeps compact single-line triggers aligned with their size tokens, including icons and clear controls.
- Name the no-search bottom-sheet
Selectorlistbox from the component'slabelso Chromium exposes it with an accessible name; the searchable sheet and popover paths keep their existing trigger relationship. (#6395) - Keep Tailwind bridge tokens reference-only so utilities follow the active theme without emitting competing runtime declarations.
- Keep authored theme declarations inside their CSS boundaries. Drop only an unsafe declaration, preserve valid CSS values and legacy token generation, and continue compiling the rest of the theme. Runtime reports dropped declarations on the console; theme builds include them in the existing receipt warnings. CSS generators accept an optional warning-text array for build collectors, without a callback API or additional exported diagnostic types. (#5529)
- Document CheckboxList's
isReadOnlyprop, and separate the select-all block example's rows withhasDividersinstead of placing a Divider inside the options list (#6777). - Clarify that CheckboxListItem reads
isCheckedandonCheckinside a CheckboxList withoutvalue(such as a select-all item), and requiresvalueonly when the parent CheckboxList has avaluearray (#6778). - Clarify that CheckIndicator is the selection mark itself and does not render persistent control chrome.
-
Add a reusable Item document tabs block template (#6652)
-
astryx build "<idea>"now always names a page template to start from (#6707). A page template carries the page frame, the spacing, and the section rhythm. A page composed from components has to rediscover them. Before, the kit recommended scaffolding only when a template matched almost by name. Other ideas got "use it as a layout reference", which pointed at a 35-line--skeleton, and an idea that no template matched got "frame with AppShell, then compose". Now: -
Link docs by identity, and let integrations add to the docs tree. (#6626) A doc links another doc inside its text with
{@link [<provider>:]<kind>:<name>}, such as{@link command:doctor}. The CLI prints each link as theastryx docscommand that opens the doc; a link that names no doc prints as written, andastryx doctorwarns on it. An older CLI prints the link as plain text.reference,workflow, andcollectionblocks stay in namespace docs.An integration can ship namespace docs and place its guides in them. They show up in
astryx docsbesidecli, with the same moves, links, and search, andastryx doctor integration docschecks them before the package ships. A namespace doc in an integration's docs directory no longer fails to load, andastryx integration add doc <name> --parent <namespace>writes a placed guide. A CLI release that does not read the docs tree can hide every doc topic of a package that ships a namespace doc or a placed guide, with nothing saying why, so--parentdeclares the CLI that reads them as an optional@astryxdesign/clipeer, andastryx integration pack --checkfails a package that ships either one without it.The CLI keeps its own routes: an integration's flat topic or namespace named
cli,unorganized, or after one of the CLI's topics (compared without case) is withdrawn from the tree, its name opens the CLI's doc, and doctor warns.Every flat topic now sits in
astryx docs unorganized, under its own name, so every doc has a place in the tree with a way up and across. Search hits for a topic name the level and the package that wrote it.New guide:
astryx docs cli/writing-docs. -
In text,
astryx docs <topic>lists the topic's sections when it has more than one. (#6626) Read one withastryx docs <topic> <section>, or print the whole topic with--full.--densestill prints the whole dense doc, so the agent bootstrap in AGENTS.md reads the same as before. The JSON contract is unchanged:astryx docs <topic> --jsonanddocs(topic)in@astryxdesign/cli/apistill return the whole topic (docs.detail), and--indexreturns its section list. -
Read the CLI's docs as a tree, one level at a time. (#6498, #6626)
astryx docs clilists the CLI's guides and reference.astryx docs cli/commandslists every command,astryx docs cli/apilists the API's functions, schemas, and enums, and a route such asastryx docs cli/api/functions/searchprints one doc.--jsonreturnsdocs.nodefor a namespace or typed doc, identified by its doc identity (a generated level hasid: null). The text ofastryx docslists the docs tree's namespaces first; its--jsonkeepsdataas the topic list and adds them inmeta.namespaces. Every command, API function, schema, and enum doc the CLI ships declares thenamespacethat reads it:astryx doctorfails when one has none or names one nothing reads, and warns when one has no route in the tree. -
Export
themeTemplate()from@astryxdesign/cli/api, matching the documented API behindastryx theme template. (#6626) -
The integration guide moved from
astryx docs cli-integrationstoastryx docs cli/integrations. (#6626) Documentation names and routes are mutable catalog data underspec:AST-017/FR45, so the move is nonbreaking and needs no compatibility alias. The guide now lives in the CLI's docs tree, undercli. Useastryx docs cli/integrations, including section reads such asastryx docs cli/integrations components. The old name no longer resolves. The docsite page stays at/docs/cli-integrations. -
astryx searchfinds the smallest doc part that answers. (#6626) A doc hit is now one section (astryx docs cli/integrations codemods), one docs-tree route (astryx docs cli/api/functions/assert-response), or a topic's index, never a whole-topic read. Typed docs match by their own name and by the identifiers they define, such as error codes.astryx search --type docworks without@astryxdesign/core.astryx docslists the docs tree first, and a namespace shows each child's own name when its route name differs.Every docs read now ends with its moves:
Up, plusPreviousandNextfor a section or a docs-tree page, andRelatedfor a typed doc (its command or API function, and its related docs).--jsoncarries them aslinks(up,previous,next,related). Each search hit carriesparent, the command that opens the level above it, and a docs-tree hit carriespackage. -
A doc section can include another doc instead of copying it. Put a
referenceblock in the section, such as{type: 'reference', target: '@astryxdesign/cli:schema:integration', projection: {fields: ['components', 'docs']}}, andastryx docsprints those two fields of the integration manifest from the schema's own doc, then the command that opens it. A reference block includes a schema, command, function, or enum doc.projection.fieldskeeps only the named fields of a schema, andpresentationisfull(the default),compact(no code blocks), orsummary. A reference to any other doc shows its title and summary. A read inlines the block, so--jsonstill returns only the stable block kinds.astryx doctor integration docsfails when a block names a doc, field, or projection it cannot include, and a read marks what is missing. Links also find the CLI's authoring schemas now:schema:integrationopensastryx docs authoring integration. -
astryx doctornow fails when a type that@astryxdesign/cli/authoringexports has no doc inastryx docs authoring, or when a listed self-doc documents nothing the package exports. The message names each type, the module that declares it, and the self-doc that is missing or not listed. (#6498) -
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. -
Prepare integration template replacement before its supported package boundary. (#6265, #6626) An integration template can set
replacesin its own metadata to a Core template id. Unqualified template lookup and discovery surfaces use a valid replacement, while--package @astryxdesign/corestill selects the original. Missing targets, type mismatches, a declaration on a template that cannot be used, and duplicate declarations fail closed and are reported byastryx doctor integration templates.When separate configured packages replace one target, the package configured later wins with a warning. Explicitly configured packages always precede autolinked ones. When only autolinked packages conflict, the dependency listed later in package.json wins with a warning and the CLI recommends explicit configuration. Invalid contribution kinds remain reportable without hiding other valid kinds, and invalid template or component files do not hide valid siblings.
This implementation may ship in final 0.6.x for forward validation, but an integration package that uses
replacesmust still require@astryxdesign/cli >=0.7.0. Earlier CLIs reject the field and withhold that package's templates and doc topics. No supported latest-stable integration consumer can enter the replacement path yet, so replacement selection and its mutable catalog results are patch-compatible pre-publication behavior.Existing same-id
IntegrationTemplateConflictresponses keep their released warning-only shape on 0.6.x. The optionalTemplateListEntry.replacesfield is additive; at or after 0.7.0, replacement-specificrelationship,replaces, andseverity: 'info'conflict fields require one deliberate projection update across runtime, types, generated reference, terminal output, documentation, and tests. A package-version bump alone leaves the warning-only shape unchanged. -
Add ScrollableArea block templates so the component page has worked examples. All six share one content vocabulary — a workspace file panel of labelled sections holding a two-column grid of muted cards — so the only thing that changes between examples is the behavior each one demonstrates: block-axis scrolling,
axis="inline"withisFullBleed, sticky section labels,axis="both", sticky pass-through againststickyContainment="always", andoverscrollallow against contain. (#6490) -
Add the Tree Table page template (#6195) A hierarchical table where every parent row is derived from its children, shown as a code repository: folders roll up the newest commit beneath them, columns resize, sorting is scoped to siblings so branches never interleave, arrow keys walk the tree per the APG treegrid pattern, and search prunes to the branches that match. Selecting a folder drives a header trail that folds its middle into a menu once the path outgrows the bar. Supporting repository chrome — header actions, an About sidebar and a README rendered with
Markdown— puts the table in the context that makes its rollups legible. -
Add opt-in typed documentation graph contracts and stable provider-aware documentation identity without breaking existing topic readers. (#6471) New
NamespaceDoc,AuthoredDocKind, provider identity types, semantic graph-block types, section IDs,astryx docs <topic> --index, and section-key reads are additive. The publicReferenceContentBlockunion keeps its 0.6.x members so existing exhaustive renderers continue to compile; graph-onlyworkflow,collection, andreferenceblocks are exported separately asGraphContentBlockand are accepted byNamespaceDoc.Existing authored topics continue to load and read as before. Duplicate title-derived keys receive deterministic suffixed index keys, titles with no Latin letters or digits receive deterministic
section-Nkeys, ambiguous title queries keep returning the first match, and legacy extension sections without IDs continue to merge by exact title. Explicit new section IDs remain validated. The new progressive-disclosure Doctor audit reports compatibility issues as warnings, and existing full-topic text output keeps its 0.6.x formatting.Every doc section can opt into a stable
id.astryx docs <topic> --index(docs(topic, undefined, {index: true})) returns the topic's section index (docs.index), andastryx docs <topic> <key>reads one section. A topic read still returns the whole doc.astryx docs authoringdocuments every authoring schema, one section each.Provider-ID conflicts are now visible instead of being dropped without a word: the package being authored wins, otherwise the first-loaded provider wins, and every command plus
astryx doctorreports the set-aside package. -
Integration themes use typed same-stem descriptors, not a central catalog. (#6498) A themes root no longer holds
manifest.json, the theme catalog thatastryx integration add themewrote in 0.6 (stable since 0.6.3). Each theme carries a strongly typed<name>Theme.doc.mjsbeside its source instead, and a themes root that still holds the catalog is refused. To migrate an integration package, runastryx upgrade --from 0.6.3 --path . --applyin it: a codemod writes each theme's descriptor from its catalog entry and removes the catalog. Until a package is migrated, apps that install it get none of its themes or doc topics.Theme discovery reads descriptors and checks integration theme sources without executing them, and
theme addcopies an integration theme's complete directory.astryx doctor integration validatewarns about a folder in the themes root that looks like a theme but is not read as one. New component, topic, and template scaffolds also emit type-annotated.doc.mjs; released.template.*inputs remain readable.
-
The published authoring types now type-check in projects that use
"moduleResolution": "nodenext"or have no Node types installed. Relative imports inside them name their files, andPostCodemodCommand'senvno longer needs Node's types. (#6492) -
Drop
gpt-tokenizerfrom the CLI's peer dependencies. Nothing in the CLI imports it, but npm and pnpm install a required peer by default, so every install of the CLI pulled in about 53 MB it never used. (#6508) -
astryx init,astryx init --remove-agentsandastryx upgrade --applyno longer edit or delete a file outside the project through an agent file or.claude/directory that is a symlink pointing there. Init reports a path-safety error for that file and exits 1,init --remove-agentsfails withERR_PATH_TRAVERSALand exits 1 instead of reporting the block removed, and upgrade reports the refresh as failed, before any file is written. (#6524) -
The text output of
astryx componentandastryx hookno longer adds non-ASCII characters of its own. Empty table cells show-instead of an em dash, the brief view's import hint reads<- from, derived properties and deprecated targets use->, and brief prop and parameter lists are separated by commas instead of middle dots. Text that comes from the docs themselves is unchanged. (#6553) -
astryx inithuman output is now plain ASCII, including the per-file linesinit --remove-agentsprints. Status lines use[ok]instead of a check glyph, and dashes, bullets and arrows print as-and->.--jsonoutput is unchanged. (#6540) -
astryx upgradehuman output is now plain ASCII. Progress and codemod lines use[ok],!and!!instead of check, warning and cross glyphs, and dashes and arrows print as-and->, including in codemod titles listed by--list.--jsonoutput is unchanged. (#6539) -
Commands that write files no longer follow a dangling symlink out of the project: when the target, or a directory on the way to it, links to a missing path outside the project root, the command now fails with
ERR_PATH_TRAVERSALinstead of creating the file there. A symlink escape reported byintegration addnow carriesERR_PATH_TRAVERSALtoo, instead of an unregisteredPATH_TRAVERSALcode. (#6513) -
astryx blogtext output now labels the feed URLfeedUrl, matching its--jsonkey, and prints every post field the JSON carries: the list adds each post's description, date, authors and link, and a post read adds its metadata above the body. (#6526) -
astryx build "<idea>"no longer prints asetup:line in its text output. That field existed only in the text, never in the--jsonkit, so the two views disagreed. The same guidance is in the no-queryastryx buildplaybook. (#6570) -
astryx build --jsonwith no query, andbuild()with no query, now return the playbook itself — a title, the ordered steps with their commands, the on-system rules, and related lookups — instead of only{playbook: true}. The terminal output is rendered from the same data, andplaybook: trueis still there. (#6566) -
The programmatic
component()API now rejectsdetailandlangvalues that theastryx componentcommand rejects, with the same codes (ERR_INVALID_DETAIL,ERR_INVALID_LANG). It used to fall back silently: an unknowndetailreturned the name list, and an unknownlangreturned English. (#6576) -
The programmatic
component(name, {cwd, blocks: true})now discovers blocks from thecwdit is given, as every other slice already does. It used to read blocks from the process working directory, so a caller pointing at another project got that directory's blocks, or none. (#6580) -
parentDocinastryx component <Name> --jsonis now a documented part of thecomponent.detailresponse. The field appears when a sub-component such asHStackis scoped out of its parent's doc. It is in the published response type and thecomponent()reference, and the text output now shows it asparentDoc: Stack. (#6575) -
astryx component <Name>no longer prints a "Related block templates" list that--jsonnever carried, so the text output shows only what the JSON result holds. The same blocks are still listed byastryx component <Name> --blocks, in text and JSON. (#6574) -
astryx component <Name> --package <pkg>no longer ignores--sourceand--blockswhen the package publishes docs through the legacyastryx.docsfield.--sourcenow fails withERR_NO_SOURCE, and--blocksreturns the blocks, the same answers as without--package. Before, both flags silently returned the plain doc. (#6577) -
astryx component --listnow prints the right import for components from packages that publish docs through the legacyastryx.docsfield. It used to show an@astryxdesign/corepath for them. The JSON list entries now carry the sameimportthatastryx component <Name>reports for each of those components. (#6578) -
astryx integration add component <Name>now refuses withERR_FILE_EXISTSwhen a component doc anywhere under the components root already uses that name, for examplecomponents/<Name>/<Name>.doc.mjs. Before, it wrote a second<Name>beside the first and reported success, andastryx component <Name>then showed the new scaffold instead of the authored component.--dry-runrefuses the same way. (#6557) -
The
debugentry of theAstryxConfigtype and ofastryx docs authoring confignow states how handlers from integrations combine with the app's own: the app's runs first, then each integration's in load order, a handler that throws is skipped without affecting the others or the command, and{"astryx": {"inheritDebug": false}}refuses inherited handlers. The type no longer claims that leavingdebugout records nothing. (#6514) -
The text output of
astryx discover(the package list and a single package) now shows every field its--jsonentry carries, includingcategoryandversion, which only the JSON used to include. (#6521) -
astryx doctortext output now uses the same field names as--json: each check prints itsidandlabel(the label was shown ascheck, and the id was missing), and the summary printspass,warn,fail, andinfounder asummaryheading instead of a prose line. (#6516) -
astryx template --helpandastryx layout expand --helpnow explain how the path argument is read: a path ending in a source-file extension is the file to write, anything else is a directory that getspage.tsx, the block's file name, or<Name>.tsx.layout expandandlayout checkalso document-for stdin and that--filewins over the argument, and--overwriteno longer mentions a prompt the CLI never shows. (#6571) -
The CLI no longer reads or sets the
ASTRYX_LATEST_VERSIONenvironment variable. Its only effect was anFYI: A newer version of @astryxdesign/core ...line on stderr afterastryx componentandastryx docs, and a CLI run cannot set a variable for later runs, so the line appeared only when the variable was set by hand. Commands now print the same output whether or not it is set. (#6554) -
The
@astryxdesign/cli/jsontypes now declareapiVersiononCLIError,CLIUnsupportedError, and the success envelope thatparseResponseandassertResponsereturn, matching what every--jsonenvelope carries. Code that constructs aCLIErrorvalue by hand, for example in a test double, now has to includeapiVersion. (#6555) -
astryx <command> --help(includingastryx manifest --help) now ends with the command's documented exit codes, and each command inastryx manifest --jsoncarries them asexitCodes: [{code, when}].astryx doctor --helpshows them once, and thelayoutanddiscoverexit codes now say when they apply: bareastryx layoutexits 1, and a blankdiscoverquery exits 1 when packages are discovered. (#6586) -
astryx gap-report --helpandastryx manifestnow describe thecomponentargument and say thatcomponent,--category, and--reasonare required unless--list-categoriesis set, with the character limits the command enforces. The manifest listed the argument with an empty description, and nothing said these inputs were required. (#6518) -
astryx theme buildnow fails withERR_THEME_INVALID, before writing anything, when a custom Heading type's standalone rule has no usable declaration: every value is blank, or the compiler dropped every declaration. It previously wrote CSS with an empty or missing rule and still added the type to the generatedHeadingTypeMap. (#6547) -
With
--json,astryx help <unknown-command>and a command group run without a subcommand (such asastryx layout --json) now return an error envelope, withERR_UNKNOWN_COMMANDorERR_MISSING_ARGUMENT, instead of a success-shaped help envelope. Both still exit 1, as they do without--json. (#6550) -
astryx hook <name>text output no longer lists block templates that the--jsonenvelope does not carry. It now names the related components and points toastryx component <name> --blocks, which returns their block templates as JSON. (#6537) -
"astryx": {"inheritDebug": false}in package.json now also refuses thedebughandler of an autolinked integration in a project that has noastryx.config. The setting used to be read only beside a config file, so without one those handlers still received every run. (#6520) -
Programmatic
init()now confines the starter template it scaffolds withtemplateNameto the project directory. Asrcsymlink that points outside the project is rejected withERR_PATH_TRAVERSALbefore anything is written. (#6538) -
astryx integration add --helpand the CLI manifest now define every control: the name format for each kind, that--typedefaults topage, that--totakes an exact semver version, and that--replacesand--extendscan't be combined. TheintegrationAdd()docs say the same. Behavior is unchanged. (#6558) -
One integration that cannot load at all (a configured package that is not installed, has no manifest or more than one, or a package being authored with two manifests) no longer takes every other integration down with it. It is reported as that package's integration issue, the other integrations keep contributing, and
astryx discoverno longer exits 1 because of it. (#6519) -
One integration whose templates root cannot be read, for example a manifest that points
templatesat a file, no longer makesastryx templatefail with a raw filesystem error. That package's templates are skipped with the usual one-line warning, and core templates and every other integration's templates still list and resolve. (#6523) -
The
--jsonoption's description inastryx --help, in the manifest, and in the CLI README now lists every envelope field:{ apiVersion, type, data, meta? }on success and{ apiVersion, error, code, suggestions? }on failure. It used to omitapiVersion,meta, and the stablecodefield that consumers branch on. (#6549) -
astryx layout expand <expr> <dir>now refuses the write, withERR_PATH_TRAVERSAL, when<dir>/<Name>.tsxis a symlink to an existing file outside the project. Before, only the directory was checked, so the generated TSX replaced the file the link pointed at. (#6564) -
astryx layout expand <expr> <path>now labels its text fieldscomponentsUsedandtodos, the keys the--jsonoutput uses, instead ofComponentsandTODOs. (#6569) -
astryx layout expandnow caps every*Nrepeat at 10000 copies. Repeated table rows skipped the cap, and a huge count on any element was still walked copy by copy before the cap applied, soB*999999999could hang or run out of memory.astryx layout grammarnow states the cap. (#6565) -
astryx layout check -andastryx layout expand -now stop reading stdin at 5 MB and fail withERR_INVALID_ARGUMENT, the same size cap--filealready had. Before, an endless or oversized pipe was buffered whole until memory ran out. (#6572) -
When a command's module fails to load, running that command with
--jsonnow prints one error envelope (ERR_UNKNOWN, with the load error in the message) instead of printing nothing to stdout. Without--jsonthe error is still printed to stderr, and the exit code is still 1 in both modes. (#6551) -
astryx manifestwithout--jsonnow labels each command's namename:, the same key the JSON manifest uses, instead ofcommand:. (#6552) -
astryx theme build --outnow reports a path that leaves the working directory withERR_PATH_TRAVERSAL, and an output directory it cannot create withERR_WRITE_FAILED, instead of unregistered codes such asPATH_TRAVERSALorEEXIST. The programmaticthemeBuild()throws the same codes as anAstryxError. (#6543) -
astryx theme palette generatenow writes candidate JSON in the canonical form theastryx-oklch-v1recipe pins, so a JSON candidate and thecandidateSha256in its receipt match the recipe's reference fixtures byte for byte. Before, thestopsarray was printed on one line, which changed the bytes and the digest of every JSON candidate. (#6531) -
astryx theme palette generateandgenerateTonalPalette()now reject aneutralProfilethe recipe does not define, even when the request has no neutral family. Before, such a request produced a candidate whose receipt recorded the unknown profile as part of the normalized request. (#6534) -
astryx theme palette generatenow reports an output or preview path it cannot use, such as one below a regular file, with the stableERR_WRITE_FAILEDcode. Before, the--jsonerror envelope carried the raw system error name, such asENOTDIR, as itscode. (#6533) -
--jsonoutput is one envelope again whenastryx.configor an integration manifest prints while it loads. Anything a project module writes to stdout during its load now goes to stderr. (#6581) -
A
--jsonerror envelope'scodeis now always one of the documented error codes. A failure that carried a Node.js system code, such asENOTDIRorEACCESfrom a failed write, used to put that code in the envelope; it now reportsERR_UNKNOWN, and the original message is unchanged. (#6548) -
The 0.6
rename-resizable-pixel-boundsupgrade codemod now also renamesminSizePx/maxSizePxin static inlineuseResizableconfigurations called through a namespace import (Astryx.useResizable({...})) or wrapped inas constorsatisfies. These were left unchanged before. (#6541) -
astryx searchnow fails withERR_CORE_NOT_FOUND, likecomponentandhook, when@astryxdesign/corecannot be found, instead of the catch-allERR_UNKNOWN. (#6525) -
astryx search --limitnow refuses a value that is not a positive integer, such as1.5or5abc, withERR_INVALID_ARGUMENTand exit 1, assearch({limit})already did, instead of silently truncating it. (#6528) -
astryx searchtext output now prints every field its--jsonresults carry:titlefor doc results andkindfor template results were missing. The--verbosehelp now says what it adds: each result's score and match reason. (#6527) -
search()from@astryxdesign/cli/apiis now declared to returnSearchResponse, as its docs say, so TypeScript sees each result'sSearchResultEntryfields instead of a bareobject. (#6529) -
astryx swizzleno longer writes outside the project through a symlink in the output folder. When the component folder or one of its files is a symlink that points outside the project, the command now fails withERR_PATH_TRAVERSALbefore it writes anything. (#6573) -
The
astryx swizzle --overwritehelp and manifest entry no longer says it skips a prompt. The CLI never prompts. The entry now says that without--overwrite, existing files fail the command withERR_FILE_EXISTSand nothing is written. (#6579) -
astryx template <name> <dir>now refuses the write, withERR_PATH_TRAVERSAL, when the file it would create in that directory is a symlink to an existing file outside the project. Before, only the directory was checked, so--overwritefollowed the link and replaced the file it pointed at. (#6563) -
astryx template <name> <path>andastryx layout expandnow replace demo media only when a path starts with/template-assets/, so third-party URLs and product paths that merely contain that text are left alone. Demo media in a subdirectory, with a query string, or with characters such as@in the file name is now replaced whole instead of being corrupted or left behind. A demo media reference that cannot be replaced safely, such as one with no file suffix or one built at runtime, now fails the copy with its path. (#6596) -
astryx theme addnow copies every file a theme catalog lists byte for byte, so a theme that ships a font or an image arrives intact. Before, it decoded each file as text, which corrupted any file that was not UTF-8. (#6530) -
astryx theme addno longer writes through a symlink that already sits at the temporary name it stages each file under. A link that leads outside the project now fails withERR_PATH_TRAVERSAL, and any other entry at that name fails the copy instead of being overwritten. (#6535) -
astryx theme buildandastryx theme palette generatenow print plain ASCII: status lines use[ok],[warn],[error],[fail], and[note]markers instead of symbol glyphs, and theme build messages drop em dashes and ellipses. The reworded font and private-variable messages also appear in the--jsonreceipt'snoticesandwarnings. Generated theme files are unchanged. (#6544) -
astryx theme build --helpand the capability manifest now state each flag's default and which flag combinations are refused (--familywith--outor--watch,--checkwith--watch,--watchwith--json,--outwith more than one file), with the error code the refusal returns. (#6546) -
The
themeBuild()reference and thetheme.buildresponse-type entry (and the CLI README table generated from it) now document every field of the receipt by name, includingnotices, the advisories (such as a font the theme names but does not load) that had no documentation. (#6545) -
astryx theme templatenow reports a file it cannot write with the stableERR_WRITE_FAILEDcode. Before, the--jsonerror envelope carried the raw system error name, such asEEXISTorEISDIR, as itscode. (#6532) -
astryx upgrade --jsonnow prints exactly one JSON envelope when a post-codemod hook prints output. Anything a hook'sbuildCommandwrites goes to stderr, so stdout carries only the result. (#6536) -
The published
UpgradeListEntrytype now declaresoptional, the boolean everyastryx upgrade --list --jsonentry already carries, so typed callers can read it without a cast. (#6542) -
astryx doctor integration validate,templates,components, anddocsnow show theinvalid_package_jsonerror when a local integration's package.json can't be parsed. Before, the text output said noastryx.integration.*file was found and hid the error, because the JSON reported a nullname, which means no manifest. In that casedata.nameis now(local package). (#6559) -
astryx docs authoringnow says which docs-graph features are not built yet: theplacement,aliasesandaudiencefields, the workflow, collection and reference blocks, namespace docs, and the artifact, doc and instance identities. It had described them as working, but a topic that uses them fails to load. A namespace doc in an integration's docs directory now fails with a message that names it, instead of reporting missing topic fields. (#6492) -
astryx doctor integration validatenow ends each finding about a misplaced contribution, a stray codemod file, a mis-named codemod folder, a component doc without its source, or a component source without a doc with a fix that works when followed as written: where to move the file and the manifest line to add, the version folder a codemod belongs in, or the file to add. Codemod files are named by their path inside the package instead of an absolute path. A hidden component doc no longer draws the missing-doc warning, and a codemods root at the package root no longer reports the manifest as a stray codemod. (#6498) -
An integration topic that
extendsanother no longer renames it.astryx docs themewith an extension installed used to print the extension's own title and description; the topic now keeps its own, and the extension only adds or replaces sections. A topic thatreplacesanother still renames it. (#6498) -
Use extensionless subpath specifiers for generated integration imports (#6288)
integrationAddComponentandintegrationAddTemplatenow emit extensionless public import specifiers (@pkg/components/MyWidgetinstead of@pkg/components/MyWidget.tsx) and map them to source files through the packageexportsfield. Consumer imports no longer expose the package's source extension or requireallowImportingTsExtensions.integrationPackChecknow rejects public specifiers ending in.tsxor.tsand validates each exact import from the packed artifact through Node's package resolver. Packages without a usable public export fail the check, and the result does not depend on project-local TypeScript. -
Make every command's text output match its --json data (#6616)
upgrade --listtext now renders each codemod from the JSON result (name, title, version, optional) instead of the API logger, with no(undefined)rows.theme targetsprints one line per target via the formatter kit's inline layout and now shows className.docslist shows the package field. A manifest-driven parity test covers the commands this PR fixes and catches future regressions in the same family; commands with known deferred divergences are covered by envelope and exit-code checks and have allowlist entries that explain the gap. -
Correct the palette authoring types.
TonalPaletteCandidate's description had landed onTonalPaletteAnchor, so the generated.d.tsdocumented the wrong type and left the candidate bare.TonalPaletteFamilyInput— the type an author writes by hand — had no property descriptions, andneutralProfilenever said what its four values do. (#6168) [fix] Give the generation receipt a real type.generationReceiptwasRecord<string, unknown>; it is nowTonalPaletteGenerationReceipt, withTonalPaletteRampDiagnostics,TonalPaletteCoordinationDiagnostics, andTonalPaletteNormalizedRequestbeside it. The generator's internal typedefs point at the same types, so the compiler holds the documentation true instead of letting it drift.[docs] Replace the stale
xdscommand name withastryxacross 79 lines of API type docs in 11 files, and realign the invocation tables. Codemods and changelogs that reference the old name are untouched — migrating it is their job. -
Protect generated, vendored, ignored, linked, dependency, and out-of-root files from upgrade codemods using working-tree declarations. Upgrades now run declared regeneration hooks, recheck protected outputs, and report incomplete changes in human and JSON results (#6692).
-
Restrict the authoring factory codemod to Astryx imports (#6335)
-
Restrict the status-variant and Avatar-size upgrade codemods to static props on verified imported JSX components, avoiding unrelated literal rewrites. (#6445)
-
Test and fixture files under a codemod version folder are no longer loaded as codemods. Every
.ts/.mjs/.jsfile under a version folder was loaded and validated, so a test colocated with its transform failed validation and — a definition error being a hard error — took every codemod in that version with it, whileupgradeapplied nothing and reported success. Reserved names:*.test.*,*.spec.*,*.fixture.*, and anything under__tests__/or__fixtures__/. (#6230) -
A stamped component doc (
type: 'component') that documents several components withcomponentsnow loads, as the publishedComponentDoctype allows. It used to fail with "props: expected array". Each entry must name its component; an entry without anamefails at load instead of later in a reader. (#6492) -
Keep authored theme declarations inside their CSS boundaries. Drop only an unsafe declaration, preserve valid CSS values and legacy token generation, and continue compiling the rest of the theme. Runtime reports dropped declarations on the console; theme builds include them in the existing receipt warnings. CSS generators accept an optional warning-text array for build collectors, without a callback API or additional exported diagnostic types. (#5529)
-
Make the Toolbar — Table Filter block's filters actually filter, and bring the row up to the pattern the Filterable Table page template demonstrates: each closed selector doubles as its own filter chip, clauses fold from the end into a count as the row narrows, and the result count, clear all, and a column picker follow the clauses. (#6478)
astryx docs authoringnow documents theDebugEventadebughandler receives and theGapReportHandlercontract, field by field. Both types were exported from@astryxdesign/cli/authoringwith no section of their own. (#6498)astryx docs authoringnow matches the published authoring types field for field, and a test keeps it that way: every field is listed, with its real type and whether it is required. Three entries were wrong: a component doc'susageis optional on sub-component docs, a command option'sdefaultmay also be a boolean or a list, and a codemod'stypeis'code'or'config'. (#6492)- Document CheckboxList's
isReadOnlyprop, and separate the select-all block example's rows withhasDividersinstead of placing a Divider inside the options list (#6777). - The documented exit codes for
astryx buildnow say that a query exits 1 when@astryxdesign/corecannot be found, while the playbook (astryx buildwith no query) needs no core. (#6592) astryx discover --componentsis now documented as what it does: in the package list it prints every component of each package instead of the first 10 and a "+N more" count, and it changes nothing in--json. It was described as "List components only". The help also gains an example. (#6522)astryx init --helpand the manifest now say how init's flags interact:--remove-agentsonly removes the managed block and ignores the install flags,--alloverrides--features, and--agentand--agent-docs-pathapply only when agent docs are installed, with an explicit path taking precedence. The documented exit codes now include an--agent-docs-pathoutside the project (exit 1) and no longer list two template cases the CLI cannot reach. (#6589)astryx theme add --overwriteandastryx upgrade --install-depsno longer describe a prompt the CLI never shows; each now says what happens without the flag (ERR_FILE_EXISTSwith nothing written, orERR_DEP_MISSING).ERR_FILE_EXISTSis described as "Refused to overwrite an existing file." without the "non-interactive mode" qualifier; its meaning is unchanged. (#6593)- The CLI README now shows
apiVersionin every hand-written envelope shape and example, and listsastryx search --verbosein place of--detail, which needs a level and does not add a result's score or reason. (#6587) - The response-type docs, and the CLI README table generated from them, now name the fields of the
component.detail,docs.index,search,build.kit,gap-report.file,theme.build,theme.targets, andintegration.pack-checkresponses by their JSON keys, includingparentDoc,hint,notices,deprecatedFor, each gap-report delivery's fields, and the pack-check contribution identities and issue fields. Thecomponent.detailresponse type declaresparentDoc. (#6588) astryx template --helpand the manifest now say which flags win when they are combined:--cdnoverrides everything else and writes to its value, else to<path>, elsecdn.template.html;--listignores a name, a path,--skeletonand--overwrite; and--skeletonneeds a name and writes nothing. (#6591)astryx upgrade --helpand the manifest now state how its flags combine:--listignores every other flag,--registryrefuses--listand the migration flags, and--codemodis the only way to run an optional codemod and also skips the ShadCN composition check. The--fromhelp names the legacy@xds/corefallback, and the documented exit codes now include a missing core, a missing jscodeshift, an invalidastryx.config, a post-codemod hook failure and the refused flag combinations. (#6590)astryx docs authoringnow says where loading a doc is looser than its published type (stamped component and function docs, older generic docs, and templates), and which doc kinds accept fields they do not know. Nothing about how docs load has changed. (#6492)
-
startnames the template to scaffold, with thetemplate <id> --type page <path>command that selects it (an integration replacement through the Core id it replaces), abasis, a one-linereason, andalternatives: the next two templates. A page ranker built for the long ideas builders write ("ops dashboard with a KPI row, a sortable table and a trend chart") picks it. Every matched word counts, weighted by how rare it is among page templates. The words before the first "with", ":" or "," name the page's family, a container ("in a modal") names its frame, and a word that only modifies another ("product" in "product response") counts half. A family's base template (dashboard,settings) leads its family unless a variant's own words outweigh it. Family words come from the templates' own ids.basisisdirectwhen the ranker's pick is also search's direct match,closestwhen it is not, andfallbackwhen nothing has the evidence to lead and the page starts from theshell-top-navapp shell (blankwhen that is not available). A template that is not ready yet is never the start. -
Search matches words more strictly. A term matches inside a name or keyword only at the start of one of its words ("input" finds
TextInput, "file" no longer finds "profile"), plurals and stems count as the same word, and typo tolerance applies only to one-word lookups of words long enough that one edit rarely makes another word ("site" no longer matches "side", nor "cable" "table"). -
Blocks and components that matched only one description word of a multi-word idea are no longer offered.
-
The text output has four sections: TEMPLATE (with its reason and command), OTHER TEMPLATES, BLOCKS, and COMPONENTS (with the frame and foundation names). They replace RECOMMENDED START and PAGE TEMPLATES. Search's page matches (
pages) move to one line, with their full entries under--verbose. Descriptions stop at their first sentence, and blocks and components show the top three, each with one shared command line.--verboseshows every block and component, with full descriptions, import paths, and match reasons. The recommended command always scaffolds (template <id> --type page <path>), where for most ideas it used to print a--skeletonorcomponent AppShell.The
buildplaybook, the generated agent docs, and theworking-with-aiandlayoutguides now start every page from a template.Compatibility: the
build.kitJSON only gainsstart. Every existing field keeps its shape and meaning. Which pages, blocks, and components are listed, anddirectMatch, change with the stricter matching, as ranking results do. The human-readable output is reorganized. Options, exit codes, and thebuild.helpshape are unchanged, andbuildstill writes nothing. The same matching changes the ranking ofsearchresults. -
Docs reads go through one internal compiler.
astryx docs(anddocs()),astryx doctorandastryx searchread compiled topic nodes instead of each loading, merging, translating and linking doc files on its own. Output is unchanged. (#6484)
- Keep PostCSS and Vite processing inside
@astryxdesign/build's declared dependency boundary (#6605) Packed consumers no longer depend on workspace hoisting to find PostCSS helpers or CSS compatibility processors. The package now owns Autoprefixer, Browserslist, and Lightning CSS, and a clean isolated-install test exercises the published tarball's PostCSS helper and Vite output. - Make source builds fail on unsupported StyleX declarations and load shared StyleX output from every Vite HTML entry. Nested pseudo-elements and
stylex.keyframes()now have production-build regression coverage, and the maintained capability registry tracks the installed StyleX version.
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
- Describe Neutral as Figtree typography and add the font-loading snippet; the README claimed system fonts while the theme declares Figtree. (#5991)
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
- Ship a typed
ThemeDocdescriptor beside each first-party theme source. (#6498)
Thanks to everyone who contributed to this release:
@AKnassa @bhamodi @cixzhang @ejhammond @ernestt @fullstackhacker @harjothkhara @HelloOjasMutreja @humbertovirtudes @imdreamrunner @jiunshinn @josephfarina @korkt-kim @ksying @kyu-rong @light-merlin-dark @nynexman4464 @rubyycheung @vjeux
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.3...v0.6.4
v0.6.3
Astryx 0.6.3 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply
-
Show each shared InputClearButton's contextual action label in a tooltip, giving sighted users the same specific action name already available to assistive technology. (#6354)
-
Add a
collapsedSummarycomposition slot to ChatComposerDrawer that replaces the complete collapsed-summary anatomy when provided; omitting it preserves the existing Badge and label. (#5399) -
Add
endContentEdgeCompensationto DialogHeader so callers can select which axes receive fixed compensation while preserving automatic close-action compensation by default. (#6465) -
Add a Canvas Editor page template with a layered artboard, resizable layer and property panels, live text/image controls, zoom, document tabs, and a themed fixed-size canvas. (#6237)
-
Markdown: add native typed frontmatter metadata (#6381) Use
createMarkdownFrontmatter()from@astryxdesign/core/Markdown/pluginsto decode a document-start key/value block into typed metadata, keep unfinished streaming metadata hidden, and remove completed metadata syntax from rendered Markdown. -
Markdown: add the core plugin protocol (#6340) Use
createMarkdownPlugin()and Markdown'spluginsprop to compose bounded source syntax, immutable typed AST transforms, and extension renderers. ImportparseMarkdownAst()orparseInlineAst()from@astryxdesign/core/Markdown/parserwhen server code needs the canonical tree. The same ordered plugins work with parser entry points and Markdown-derived Outline items, while omitted or empty plugin lists preserve existing behavior. -
Markdown: add a limited Remark compatibility adapter (#6345) Import
createMarkdownRemarkTransform()from@astryxdesign/core/Markdown/remarkto run one synchronous transform-only Remark plugin over the documented MDAST subset. Every invocation gets a fresh mutable tree and an isolated file, and each plugin's compatibility is proven by fixtures rather than assumed: async work, parser or compiler plugins, processor state, raw HTML, unsupported nodes, forged positions, and metadata Astryx cannot represent keep the last valid readable document and report one diagnostic. The adapter is a separate entry point, so it stays out of every bundle that does not import it. -
Markdown: add a semantic code-fence transform helper (#6343) Use
createMarkdownFenceTransform()to annotate declared fenced-code languages with typed extension data while standard pluginrenderersown presentation and text projection.components.coderetains precedence, and declined, missing, or failed proposals preserve Markdown's accessible, copyableCodeBlockfallback. -
Markdown: add an immutable source-decoration helper (#6344) Use
createMarkdownSourceDecoration()to attach non-visual metadata to every block a validated UTF-16 source range touches, andgetMarkdownSourceDecorations()to read it back in a later plugin. It works through<Markdown>and Outline with no extra parser options, resolves independently of the order earlier transforms left blocks in, and appears on the settled document rather than on partial streaming chunks, so a decoration never appears and then vanishes. Metadata lives in one versioned Astryx-owned envelope that never merges foreign node data. Rendered output, copyable text, accessible names, heading ids, focus order, navigation, and source provenance are unchanged.Markdown transforms also got faster, and a plugin now always observes a fully immutable tree — including blocks a Core helper carried over untouched. Core-authored helpers now validate the nodes a caller's callback produced and run on a trusted path that skips the whole-tree validation and freezing applied to plugin-authored output, freezing walks only what a transform changed, a plugin list that contributes no inline syntax no longer costs anything per source character, and the helpers' per-match allocations and rebuilds are gone. The representative three-helper set now adds about 20 percent over an empty pipeline, inside its 25 percent budget, down from roughly 3.3x.
-
Markdown: add an immutable text-transform helper (#6342) Use
createMarkdownTextTransform()to replace matching prose with typed Markdown nodes while preserving links, images, code, math, citations, and existing extension syntax as protected contexts. -
Add pressed feedback to CheckboxInput, Collapsible, Link, Slider, Switch, TabList, RadioList, and unselected SegmentedControl items. Enabled controls paint
--color-overlay-pressedon their interaction surface during pointer hold or drag; disabled controls and selected SegmentedControl items keep their existing surfaces. (#6380) -
Let a TreeList item carry row styles (#6238)
TreeListItemDatagainsxstyle,className, andstyle, applied to the item's row element, so row-scoped primitives such asuseContainerRevealcan isolate each row's hover and focus state.
-
Make overflowing BottomSheet text keyboard reachable with a named scroll-body tab stop. Add shared focus-time keyboard delegation to
useScrollableArea: forward Tab may enter the first native link/button directly, while inputs, composite widgets, and nested scroll owners retain the viewport stop. Reverse traversal skips the delegated viewport; pointer/programmatic focus and content changes never trigger delegation. Sheet scroll containment now applies only while content overflows and uses the sharedcontainpolicy, which permits native edge feedback. (#6301) -
Keep ChatComposer's public composition contracts aligned: custom inputs now submit the value they supply, disabled default editors expose their state to assistive technology, and an explicitly shown Stop action remains pointer-operable while editing is disabled. (#6398)
-
Keep collapsed
ChatComposerDrawercontent out of keyboard and assistive-technology navigation, and show the shared focus indicator on its disclosure control. (#6416) -
Keep programmatic ChatComposerInput edits observable, deliver dropped files through
onFiles, and letonPasteintercept text before default token conversion. (#6419) -
Export
ChatComposerTokenElementPropsand forward supported span props and refs fromChatComposerTokenElement(#6443). -
Keep the chat dictation control disabled when speech recognition is unavailable, and preserve theme control over its clipping feedback (#6444).
-
Clip the resizable SideNav handle within the sidebar bounds (#6196)
-
scope the 16px text-control font-size floor to iOS with
@supports (-webkit-touch-callout: none)inside the coarse-pointer query, so Android and touch-screen laptops keep the theme's type scale instead of an inflated 16px (#6085; fixes #6015) -
Kbd: render the
escandreturnaliases with the same glyphs and accessible names asescapeandenter(#5657) This partial fix for #5403 normalizes the two unambiguous aliases already accepted byuseHotkeys. Kbd now rendersescasEscwith the accessible nameEscape, andreturnas↵with the accessible nameEnter. Its lookup tables now also avoid prototype-chain collisions when rendering arbitrary key names. The platform-specific rendering contract formetaand display choice forspaceremain unresolved in #5403 pending separate API and design decisions. -
LayoutFooter: add playground wrapper and default children for docsite preview (#6341) Prevents the properties-tab preview on the docsite from rendering an empty stage by wrapping LayoutFooter inside a Layout scaffold with representative footer content in the footer slot.
-
PowerSearch: switching the field or operator while the value menu is open now shows the new field's options instead of the old ones. (#6357)
-
Move focusable ProgressBar target marks outside the
progressbarsubtree (#6248) -
Selector: collapse an option's mark column when its resolved selection indicator renders nothing. Unselected rows using the default check indicator no longer lose label width to an empty wrapper. (#5619)
-
Paint Slider marks inside the filled region with the accent fill color while leaving unfilled-side marks unchanged. (#6455)
-
Use the Slider track token for unfilled tick marks so marks and rails stay aligned across themes. (#6461)
-
Apply the theme body font to the theme scope root so components using inherited typography no longer fall back to browser serif. (#6450)
-
Keep ToggleButton callbacks synchronous while routing pressed Actions through Button's
clickActionpathway, preserving cancellation and optimistic pending feedback. (#6463) -
Spinner: animate the arc's dash offset instead of rotating the ring, fixing residual wobble on iOS Safari (#6311; fixes #6253) The earlier fix for #3617 added
willChange: 'transform'to the rotating<svg>, which smooths the rotation's motion but does nothing about how WebKit rasterizes a rotating stroked shape's rounded cap on each frame. Rotating the whole ring still visibly wobbled on iOS Safari.Animates
stroke-dashoffseton the stationary arc<circle>instead of rotating the<svg>, so the shape never rotates and WebKit never re-rasterizes the cap at an intermediate angle. Confirmed against a real iOS Safari device by the issue reporter. -
Table: contain overscroll only while its inner viewport can scroll, so a fitting table no longer creates a dead scroll zone in its parent. (#6410)
-
Keep a ToggleButton's own
isDisabledwhen its ToggleButtonGroup does not disable anything. The group always supplies anisDisabledboolean, so the previous??fallback never ran and an enabled group re-enabled a member that had disabled itself — the member selected on click, and a member carrying atooltipstayed operable while looking unavailable. A disabled group still disables every member; it just cannot re-enable one. (#6356)
Thanks to everyone who contributed to this release:
@aldentan @athz @cixzhang @ernestt @harjothkhara @HelloOjasMutreja @jiunshinn @kentonquatman @korkt-kim @ManoharPaturi @nynexman4464 @rupesh-kumar-sah @vjeux
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.2...v0.6.3
Astryx 0.6.2
Astryx 0.6.2 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply
-
Markdown: add an opt-in math renderer (#6312) Supply
components.mathto parse$…$inline math and$$…$$display math. The renderer receives the delimiter-free expression asvalueand its placement asdisplay: 'inline' | 'block', so applications can connect their preferred math typesetter without preprocessing Markdown or accepting raw HTML.Math is off unless the renderer is present. Direct parser callers can opt in with
MathParseOptions({math: true}) and incremental callers createIncrementalParseState<true>viacreateIncrementalState<true>(). Those overloads returnInlineNodeWithMathorBlockNodeWithMath; default, legacy-set,math: false, andParseOptions- annotated calls retain the existingInlineNodeandBlockNodeunions, so exhaustive consumers do not gain a case unless they opt in. Existing Markdown parsing and rendering stay unchanged by default; code stays opaque, escaped and unmatched delimiters stay literal, and inline plugins skip math. Incremental parsing preserves full-parse results when display math is nested in ordinary or task lists and blockquotes, including quote-depth transitions, with LF or CRLF and with source ranges enabled. -
DialogHeader exposes theme targets for its header gap, title/subtitle gap, and close-icon size. (#6240)
-
Expose DateRangeInput preset theming targets (#6223)
-
Expose the Slider interactive control as a theme target (#6225)
-
Expose FileInput's upload icon as a mode-aware theme target (#5417)
-
Add a
--spinner-arc-fractionpublic var to Spinner, so a theme can change how much of the ring the moving arc covers (defaults to 0.375, a 135deg sweep), the same way it already retheme diameter, stroke width, and color. (#5845)
- Adds the
@astryx.chatTypingIndicator.*catalog keys so the lab ChatTypingIndicator can build its typing status from the translation runtime instead of English literals, joining names withIntl.ListFormatfor the active locale. English output is unchanged. (#6220) - Spinner: the default assistive label now comes from the translation catalog (
@astryx.spinner.loading) instead of a literal"Loading"in component source, so a localized app translates the status. An explicitaria-labeland a visible string label still take precedence, in that order. (#6217)
- Add
musepreset toastryx init --agenttargetingAGENTS.mdfor Muse Code. (#6045) - Build one keyed artifact trio for a selected theme family (#6268)
-
doctor: range-check every peer against the project's own node_modules, so a peer that is only reachable from the ambient environment no longer reads as installed (#5327)
checkPeerDepsresolved each peer withrequire.resolve(name, {paths: [cwd]}). Node foldsNODE_PATHinto that lookup regardless ofpaths, so a peer merely reachable from the ambient environment resolved, and doctor reported nothing while the project itself was missing it. It now walks the project's ownnode_modulesand reads eachpackage.jsonoff disk, so a missing peer is reported and an installed one is checked against the declared range.Yarn Plug'n'Play projects have no
node_modulesfor that walk to find, so the lookup asks the PnP runtime when the walk comes up empty. PnP resolves from the project's own dependency graph and ignoresNODE_PATH, which keeps the answer project-local. A PnP project now gets its peers range-checked too — previouslyrequire.resolvecould confirm a peer was present there but not read its version, because Core does not export./package.json. -
Component loader now reads default-export
.doc.mjsfiles (the shapeintegration add componentwrites), fixing a crash wherecomponentandsearchcould not load generated docs. Humancomponentdetail and list views now use the API-resolved import specifier instead of recomputing from core, so integration components report their package-authored import.pack --checknow reports an error when a component doc cannot be loaded instead of silently approving. (#6291) -
Fix
theme buildemitting invalid JS identifiers for theme names containing hyphens or dots followed by digits. The output identifier is now derived deterministically fromtheme.nameby camelCasing across-and.separators (underscores are preserved as valid identifier characters). Names likechaos-07correctly producechaos07Themeinstead of the unparseablechaos-07Theme. (#6289) -
Make staged writes portable across filesystems that reject hard links. The create-only publisher now falls back from
linkSynctocopyFileSyncwithCOPYFILE_EXCLforEPERMandEXDEV, while preserving no-clobber, concurrent-creator safety, compare-and-swap replacements, symlink rejection, rollback, and temporary-file cleanup. (#6287) -
theme build: only treat a core the theme's own node_modules chain can reach as one a CommonJS dependency could reach, so an ambient-only core no longer fails the build with ERR_CORE_INCOMPATIBLE (#5327)
patchCommonJsrequired@astryxdesign/corefrom the theme file to see whether it could wrapdefineThemefor.cjsdependencies.requirefoldsNODE_PATHin, and pnpm's isolated layout puts every package innode_modules/.pnpm/node_modules, so a core no dependency of the theme could reach answered that lookup. Wrapping it fails on arequire(esm)namespace, and the reported coverage gap then rejected any theme whose lineage was unobserved. The lookup now walks the theme's ownnode_moduleschain first, the same waydoctorresolves peers.
Thanks to everyone who contributed to this release:
@athz @cixzhang @freddymeta @Han5991 @HelloOjasMutreja @josephfarina @ksying @Kyujenius @oliprovscode
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.1...v0.6.2
v0.6.1
Astryx 0.6.1 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply
- Remove the prefix requirement from theme-local tokens (#6285)
- Add ScrollableArea and useScrollableArea for accessible, logical-axis native scrolling with explicit overscroll policy. (#6262) Scrollable viewports now become keyboard reachable only while content effectively overflows, preserve logical edge state across writing modes, apply contained overscroll only on active axes, avoid capturing Sticky while fitting unless explicitly requested, expose standard container sizing props, and integrate optional content padding plus opt-in full bleed with the shared container geometry system.
- BaseTypeahead: preserve input props and keep results accessible in narrow layouts (#6179) BaseTypeahead now forwards its inherited DOM and styling props to the combobox input, preserves native input attributes unless a defined legacy alias overrides them, keeps empty result lists valid for assistive technology, counts visible characters for
minQueryLength, and keeps both the popup and long result content within viewport gutters. - BottomSheetSwitcher: let the topmost nested layer handle Escape before a non-modal flow. (#6184)
- BreadcrumbItem preserves valid outside focus on menu light dismiss and labels menus from rich trigger content (#6206)
- Center: preserve component-owned axis reflection and correct the horizontal-centering example. (#6207)
- Localize Chart accessibility text and complete its consumer guidance. (#6247)
- defer clear focus restoration for pointer/touch taps to prevent page scroll jumps while preserving synchronous focus restoration on keyboard activation and properly composing
onPointerDowninInputClearButton(#5440) - Prefer canonical component target names in maintained themes and new examples while preserving deprecated runtime aliases and released bare prop/state selector classes through the 0.7.0 removal window. Theme discovery labels deprecated targets, theme build warns with each exact canonical replacement, and
astryx upgrade --applyprovides the forward-compatible bare-selector migration. (#6126) - Field inputs no longer paint above the sticky AppShell header while scrolling (#5689). Field now contains its local stacking layers (the input surface's z-index and the attached status layer) behind an
isolation: isolateboundary on the field surface, so they cannot compete with page-level stacking; the AppShell header keeps its normal stacking level. - TextInput's
onEnterno longer fires for the Enter that commits an IME conversion (Japanese/Chinese/Korean input);onKeyDownstill receives the raw event. (#6082)
- AspectRatio: show the
ratioprop in its JSX form (#6093) The best-practice line told readers to express the ratio as a fraction like16/9without showing it in JSX, and nothing else in the CLI output givesratioan example. Rewrites it toratio={16 / 9}and names the string form as a type error.
-
Load an installed integration even when no
astryx.confignames it (#6202) A package the project declares as a dependency, and that ships a rootastryx.integration.*manifest, is now loaded on sight — no config entry required. A scaffold that adds the dependency and writes no config used to leave the integration invisible: its components, templates, docs and codemods all reported as missing, which is indistinguishable from not having installed it at all.Only DECLARED dependencies are probed —
dependencies,devDependenciesandoptionalDependencies— and only by key.node_modulesis never walked, so a transitive dependency of a dependency cannot contribute; and because the value is never parsed, a dependency that is not a semver range (npm:aliases,workspace:,file:,link:,catalog:) resolves like any other. Identity comes from the resolved package's ownname, so an aliased dependency reports the package it actually is, and two dependency keys naming one package load it once.An explicit
astryx.configentry keeps its precedence and its position, and a dependency whose manifest fails to load is dropped quietly rather than reported as the consuming project's problem.astryx doctorgains animplicit-integrationsline naming each integration linked this way, the package.json field that declared it, and what it contributes — so an author can answer "why can the CLI see this?" without reading the CLI's source, and an unused-dependency check has something to read that says the dependency is load-bearing. The line is always informational, so the doctor CI gate is unaffected. -
Replace executable gap-report writers with composable handlers. (#6200) Gap reports now fan out to every configured handler — project config first, then each loaded integration in config order — instead of selecting one writer. Each handler gets its own report copy and an abort signal under a 30 s budget. A failed handler cannot stop later handlers, and the aggregate receipt shows every outcome.
Public types:
GapReportHandlerreplacesGapReportWriter; the handler receives a normalizedGapReportevent and returns a strictGapReportHandlerReceipt. Project config gains agapReportfield; the integration named export uses the same type. -
Add integration authoring and packed-package verification.
astryx integration add <kind> <name>and the per-kindintegrationAddComponent,integrationAddDoc,integrationAddTemplate,integrationAddCodemod,integrationAddAgentDoc, andintegrationAddThemeAPIs write complete contributions. Existing component, docs, template, and theme commands see the package being authored without publishing it first.astryx integration pack --checkproves the same contributions survive the npm tarball and that packed components remain available through their public imports. Doctor now names source-only components, unreachable metadata, codemods outside a version folder, and invalid version folders. (#6245) -
Remove the prefix requirement from theme-local tokens (#6285)
-
Add upgrade receipts and safe three-way reconciliation for ShadCN-copied compositions. (#6228)
-
Let integration packages contribute source themes (#6245) An integration can declare a themes root using the same bundle shape as Astryx's built-in themes. Installed themes now appear in
theme list, andtheme addcan copy one by owner.
- build: recommend
template <name> --skeletonin the kit payload when the top page is not a direct match, matching what the renderer already tells a human (#6255) - Center: preserve component-owned axis reflection and correct the horizontal-centering example. (#6207)
astryx component <Name>'s plain-text output always showedimport {Name} from '@astryxdesign/core/...', even for a component owned by an integration package. The JSON response already resolved the import against the correct owner, but the command's text formatter recomputed its own hint via the core-only resolver and ignored that value. (#5294) The command now uses the already-resolvedimportfield from the component's detail response, so the plain-text output matches the JSON output and shows the integration's own package.- Prefer canonical component target names in maintained themes and new examples while preserving deprecated runtime aliases and released bare prop/state selector classes through the 0.7.0 removal window. Theme discovery labels deprecated targets, theme build warns with each exact canonical replacement, and
astryx upgrade --applyprovides the forward-compatible bare-selector migration. (#6126) componentandsearchnow report the same import specifier for an integration component, resolved once infoundation/discovery/component-discovery.mjs.searchpreviously returned the bare package name, which does not resolve for a package whose components are exported behind subpaths. (#6203)- Keep ShadCN composition upgrades safe in JavaScript projects and publish precompiled JSX with strict TypeScript declarations. (#6246)
- Make generated ShadCN compositions match the exact bytes written by the stock client, include package peer dependencies, and require full-catalog install/build coverage in CI. (#6231)
- Keep copied integration theme files inside the target project. (#6270)
- Show all seven dashboard page templates in the templates gallery and playground. (#6264)
- Prefer canonical component target names in maintained themes and new examples while preserving deprecated runtime aliases and released bare prop/state selector classes through the 0.7.0 removal window. Theme discovery labels deprecated targets, theme build warns with each exact canonical replacement, and
astryx upgrade --applyprovides the forward-compatible bare-selector migration. (#6126)
- Prefer canonical component target names in maintained themes and new examples while preserving deprecated runtime aliases and released bare prop/state selector classes through the 0.7.0 removal window. Theme discovery labels deprecated targets, theme build warns with each exact canonical replacement, and
astryx upgrade --applyprovides the forward-compatible bare-selector migration. (#6126)
- Prefer canonical component target names in maintained themes and new examples while preserving deprecated runtime aliases and released bare prop/state selector classes through the 0.7.0 removal window. Theme discovery labels deprecated targets, theme build warns with each exact canonical replacement, and
astryx upgrade --applyprovides the forward-compatible bare-selector migration. (#6126)
Thanks to everyone who contributed to this release:
@andrskr @cixzhang @Cypher-Aura-19 @ernestt @Geervan @josephfarina @kentonquatman @Kyujenius @ManoharPaturi
Full Changelog: https://github.com/facebook/astryx/compare/v0.6.0...v0.6.1
Astryx v0.6.0
Astryx 0.6.0 — all @astryxdesign/* packages ship at this version.
npx astryx upgrade --apply
-
Remove deprecated focus-direction overrides, the hooks-path
isImeKeyEventre-export, and Resizable pixel-bound aliases. Codemod: Runnpx astryx upgrade --applybefore updating to 0.6.0. It removes focus-hookisRtl, movesisImeKeyEventimports to@astryxdesign/core/utils, and renamesminSizePx/maxSizePxtominSize/maxSize. -
Stop emitting deprecated bare prop and state classes such as
.primary,.sm, and.checked. Components retain their stableastryx-*target classes and reflect visual props and runtime states through explicitdata-*attributes; generated runtime and built theme CSS now uses that same selector contract. Runastryx upgrade --applyto migrate safely identifiable selectors in.cssfiles when a known Astryx target and v0.5.4 value have one or more known meanings. For example:.astryx-button.primary→.astryx-button:is(.primary, [data-variant="primary"]).astryx-button.sm→.astryx-button:is(.sm, [data-size="sm"]).astryx-switch.checked→.astryx-switch:is(.checked, [data-checked="checked"])
The codemod parses CSS selector syntax and never rewrites declarations, comments, JavaScript/TypeScript strings, unqualified classes, or custom/unknown qualified classes. Each known value becomes a specificity-preserving
:is(...)union containing the original class arm plus every v0.5.4 reflected data-attribute arm. The class arm keeps consumer-suppliedclassNamematches working; the attribute arms match v0.6 props and states. You can narrow the union later when class provenance or prop-axis intent is known. Search for unqualified old value selectors such as.primaryor.smand migrate those manually only where Astryx usage is confirmed. Migrate selectors embedded in JavaScript or TypeScript manually with the same rules.Semantic
defineTheme({components})keys such asvariant:primaryandcheckeddo not change. If you prebuild a custom theme, rerunastryx theme build <theme-file>after upgrading and deploy the regenerated.css,.js,.d.ts, and optional.variants.d.tsartifacts together. A built theme is marked__built: true, so the runtime intentionally does not regenerate stale CSS.Exported theme helpers keep their return/container shapes but intentionally return different selector bytes:
themePropsreturns the stable target class (plus target-name compatibility aliases), without bare prop/state classes; its reflecteddata-*attributes are unchanged.parseStyleKeyreturns data-attribute selector suffixes instead of.value,.prop-N, or.statesuffixes.generateThemeRuleskeeps its array contract and ordering; non-base component selectors use reflected attributes.generateThemeRulesSplitkeeps{component, prose};componentselector bytes change andproseis unchanged.generateOnMediaCSSkeeps its scoped string contract; component selector bytes change.generateThemeCSSkeeps{prose, component}and the same layers/scopes;componentinherits the new selectors andproseis unchanged.
-
Restrict
Stepper'shorizontalOptions.minimumStepWidthto a pixel number and remove compact-layout implementation fields fromuseStepperContext. Replace CSS-length thresholds such as'7rem'with their intended pixel number. CallregisterStep(index, {getIsDisabled})instead of passing a disabled boolean; the options object is optional.StepperContextValuekeeps transition history and step registration, while step count, compact state, summary-portal coordination, and threshold measurement remain package-internal. -
Add ordered environmental adaptations to
defineThemeThemes can now opt into CSS-first token, theme-local token, and component changes for named viewport widths, primary-pointer precision, contrast preference, and motion preference:defineTheme({ name: 'acme', adaptations: { widthBreakpoints: {sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536}, rules: [ { when: {width: {from: 'lg', below: 'xl'}, pointer: 'coarse'}, value: {tokens: {'--size-element-md': '44px'}}, }, ], }, });
Condition fields are ANDed.
width.fromis inclusive,width.belowis exclusive, and rules cascade in declaration order so later matching writes win. Theme extension preserves the effective breakpoint map and inherited rule order; static builds retain the metadata needed for source-equivalent extension.AppShellnow acceptsxland2xlformobileNav.breakpointand resolves all five names through the nearest Theme. Mobile mode now uses the documented exclusive boundary (width < breakpoint), so an AppShell exactly at the named point renders the wider layout instead of the mobile layout.defineThemenow validates the token values authored inside an adaptation rule, rejecting non-string scalars and arrays with a length other than two instead of emitting them. Root and on-media token input keeps its existing acceptance unchanged, so themes that pass values through casts or spreads keep building. It also validates the combined portable and theme-local token graph for every reachable set of matching adaptation rules, rejecting cycles before CSS is emitted. Component writes in a rule use the same target, axis, value-domain, and extension validation as rootcomponents; a rule may not be the only place a custom value is enrolled, because generated type augmentation is unconditional.astryx theme buildtreats the adaptation generator as a core capability rather than a baseline requirement, so a theme with no adaptation intent still builds against an older installed@astryxdesign/coreand emits the same CSS as before. A theme that does carry adaptation intent — valid rules, a customwidthBreakpointsmap, or present-but-malformed adaptation metadata — fails against such a core before any output is written, withERR_CORE_INCOMPATIBLEnaming the missinggenerateAdaptationCSSexport. A complete default width map with no rules asks for nothing and still builds. Where an older core'sdefineThemedrops adaptations while resolving, the build records each rawdefineTheme()input and associates it with the theme it produced, so only the selected theme's lineage decides. An unobservable selected ancestor (including a CommonJS source package whose ESM core namespace cannot be wrapped) fails closed; an unused adaptive theme elsewhere in the import graph does not affect a plain build. The same capture preserves raw typography, color, radius, and motion axis metadata in old-core-built artifacts, allowing later current-core children to resolve partial adaptation axes exactly as if they extended the source theme.
- Banner exposes a
banner-frametheme target on the visible frame that owns whole-banner elevation and the elevated-card silhouette. The target reflectscontainerandelevation; existing Banner targets and default rendering are unchanged. CollapsibleandCollapsibleGroupacceptchevronPosition="start" | "end". The default remainsend, preserving the released trailing chevron.startmoves the disclosure arrow ahead of the label for tree/file-browser-style rows: it points inward toward content when collapsed, mirrors under RTL, and turns downward when expanded. Set the position onCollapsibleGroupwhen direct items should share it. An individualCollapsiblemay override the group, while a Collapsible nested inside an item's content starts a new presentation scope and keeps its own default.- Add a named font-weight override to Heading with precedence over its visual type and semantic-level defaults.
- Add an
autoCompleteprop to TextInput and TextArea, forwarded to the native control unchanged.
- ChatComposerInput: drop
aria-multilineonce triggers make the editable a comboboxaria-multilinewas hardcoded on the contenteditable element whileuseTriggerMenuowns its role, so configuringtriggersswitched the role tocombobox— which ARIA 1.2 does not listaria-multilineunder — and axe flaggedaria-allowed-attr(critical) on the 8 ChatComposerInput trigger stories and the 2 ChatLayout stories that render one. Moves the attribute into the hook'sariaProps, where the role and the attributes whose validity depends on it are decided together. - CheckboxListItem: the visible
descriptionis now the checkbox's accessible description, so the browser computes a distinct description instead of none. Item ids the description element it already renders and publishes that id to the content it renders in a slot, which keeps a plain string description's automatic single-line truncation. CheckboxInput now merges a consumer-suppliedaria-describedbywith its own description, status, and disabled-reason ids rather than replacing it. No public API changes. - CheckboxListItem: a ReactNode
labelnow names the checkbox from its visible text througharia-labelledby, the way RadioListItem already does, instead of falling back to the generic name "Checkbox".aria-labelstill replaces that name; a rich label with no text at all needs it, as it does for RadioListItem. The dev-time warning that asked foraria-labelon every rich label is gone. - Prevented removed Resizable bounds from being silently ignored and kept ambiguous spread migrations behavior-preserving (#6124)
- Keep hover from auto-scrolling open option lists (#6077) In a scrollable listbox whose highlight follows the pointer, scrolling the highlighted option into view moved the next option under the stationary pointer, whose mouseenter re-highlighted and scrolled again — an endless loop with no user input. This was already fixed for DropdownMenu and Chat; it now covers the remaining combobox-style paths through a shared highlight owner: Selector, MultiSelector, Typeahead, DateTimeInput, and CommandPalette hover highlights move only the highlight, while keyboard navigation still scrolls the highlighted option into view.
- Slider keeps its focus ring hidden for modifier-only key presses after a pointer drag while preserving keyboard navigation (#5469)
- Layout: keep content scrollbars at the content area's outer edge when
contentWidthis set. Without panels,LayoutContentspans the available Layout width and aligns its children tocontentWidthinternally. With exactly one panel, the panel stays aligned to thecontentWidthframe while content extends across the opposite open area. A two-panel layout keeps the complete composition constrained. - Popover: apply same-gesture reopen protection through every opening path, focus genuine caller content regardless of activation modality, and keep the generated fallback close control hidden until keyboard users reach it.
- SideNavItem: a consumer-provided
aria-labelno longer gets overwritten by the collapsed-rail fallback, in both the icon-only and popover-trigger paths.
-
Add ordered environmental adaptations to
defineThemeThemes can now opt into CSS-first token, theme-local token, and component changes for named viewport widths, primary-pointer precision, contrast preference, and motion preference:defineTheme({ name: 'acme', adaptations: { widthBreakpoints: {sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536}, rules: [ { when: {width: {from: 'lg', below: 'xl'}, pointer: 'coarse'}, value: {tokens: {'--size-element-md': '44px'}}, }, ], }, });
Condition fields are ANDed.
width.fromis inclusive,width.belowis exclusive, and rules cascade in declaration order so later matching writes win. Theme extension preserves the effective breakpoint map and inherited rule order; static builds retain the metadata needed for source-equivalent extension.AppShellnow acceptsxland2xlformobileNav.breakpointand resolves all five names through the nearest Theme. Mobile mode now uses the documented exclusive boundary (width < breakpoint), so an AppShell exactly at the named point renders the wider layout instead of the mobile layout.defineThemenow validates the token values authored inside an adaptation rule, rejecting non-string scalars and arrays with a length other than two instead of emitting them. Root and on-media token input keeps its existing acceptance unchanged, so themes that pass values through casts or spreads keep building. It also validates the combined portable and theme-local token graph for every reachable set of matching adaptation rules, rejecting cycles before CSS is emitted. Component writes in a rule use the same target, axis, value-domain, and extension validation as rootcomponents; a rule may not be the only place a custom value is enrolled, because generated type augmentation is unconditional.astryx theme buildtreats the adaptation generator as a core capability rather than a baseline requirement, so a theme with no adaptation intent still builds against an older installed@astryxdesign/coreand emits the same CSS as before. A theme that does carry adaptation intent — valid rules, a customwidthBreakpointsmap, or present-but-malformed adaptation metadata — fails against such a core before any output is written, withERR_CORE_INCOMPATIBLEnaming the missinggenerateAdaptationCSSexport. A complete default width map with no rules asks for nothing and still builds. Where an older core'sdefineThemedrops adaptations while resolving, the build records each rawdefineTheme()input and associates it with the theme it produced, so only the selected theme's lineage decides. An unobservable selected ancestor (including a CommonJS source package whose ESM core namespace cannot be wrapped) fails closed; an unused adaptive theme elsewhere in the import graph does not affect a plain build. The same capture preserves raw typography, color, radius, and motion axis metadata in old-core-built artifacts, allowing later current-core children to resolve partial adaptation axes exactly as if they extended the source theme.
- Let integration manifests add managed agent guidance
- Add an authoring-time OKLCH palette generator with a pure API, terminal and HTML previews, typed palette output, custom stops, deterministic receipts, and overwrite protection. [feat] Expose exact solid black and white values as
neutralPalettes.blackandneutralPalettes.whitefor use in semantic theme tokens. - Every command now reports what it returned in its debug logs, and a new command cannot skip it. A command's action returns a
CommandResult— either{kind: 'results', count, resultKind, ...}or{kind: 'none'}for the commands whose work is an effect (build, init, upgrade, doctor). The CommandDoc converter records it centrally, soresultCount,emptyResult,resultKind, anddirectMatchare now populated forcomponent,docs,hook,template,theme list/add/targets,discover,blog,swizzle --list,upgrade --list,layout grammar, andmanifest, not justsearchandbuild.resultKindgainstheme,integration,migration,command, andnone; a null now means the run never reached an answer rather than "this command has nothing to say". That is a change of meaning on an existing field, so recorded runs are nowschemaVersion: 3— a consumer that counted nulls as "commands with nothing to report" should branch on the version before mixing old rows with new ones. - Add
doctor integrationchecks for structural validation and Core template, component, and doc overlaps (#6173). - Add an experimental shadcn Registry compatibility guide and doc-derived registry identity metadata. It explains the package boundary, stable organized paths, copied composition model, upgrade behavior, and when to use the richer Astryx CLI.
- Add
astryx upgradetransforms for the Core 0.6 deprecated-API removals: focus direction overrides, the hooks-path IME helper import, and Resizable pixel-bound aliases.
- Prevented removed Resizable bounds from being silently ignored and kept ambiguous spread migrations behavior-preserving (#6124)
- Add a conservative
astryx upgrade --applymigration for the Core bare selector-class removal. The transform parses.cssselector syntax, rewrites exact v0.5.4 target/value pairs to behavior-preserving old-class/data-attribute unions, covers unbounded values that v0.5.4 emitted, and leaves unknown consumer classes unchanged. - Preserve
@pathagent doc imports and remove previously duplicated managed blocks (#6164) - Refresh the Collapsible block templates with complete, current examples for single, multiple, controlled, divided, standalone, and grouped usage. The controlled step example keeps one valid step open so its progress label and Previous/Next actions never enter an invalid “Step 0” state.
- Report the fixture path when a template demo asset has an unsupported format (#6039)
- Align Doctor help and README examples with the shipped command tree and output format (#6197).
- Clarify how to build themes with imported icon registries, including the current omission of inline registries and the separate registry compilation step. The theme guide distinguishes a missing compiled registry from an extensionless source import: the former breaks both loading and bundling, while the latter can resolve in a bundler when the source remains beside the generated module. English, dense, and Chinese guidance now explains how output paths and
--icons-specifieraffect resolution.
withAstryx()refuses a Turbopack config instead of building an unstyled app. Every alias the helper installs lives innextConfig.webpack, which Turbopack never calls, so the app resolved@astryxdesign/*to dist while PostCSS compiled the library from source — disjoint class names, an exit code of 0, and an unstyled page. It now throws, naming both ways out:--webpack, or drop the helper and consume the pre-built package. Also warns when the merged alias map claims none of the packages, which reaches the same unstyled state by another route. (#6109)
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
- Add an authoring-time OKLCH palette generator with a pure API, terminal and HTML previews, typed palette output, custom stops, deterministic receipts, and overwrite protection. [feat] Expose exact solid black and white values as
neutralPalettes.blackandneutralPalettes.whitefor use in semantic theme tokens.
- Align Neutral light-mode foreground colors to darker palette stops.
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
- Requires
@astryxdesign/core@0.6.0as part of the coordinated stable release. Upgrade Core and this theme together.
Thanks to everyone who contributed to this release:
@cixzhang @ernestt @faga295 @freddymeta @Hashim1999164 @HelloOjasMutreja @imdreamrunner @jiunshinn @joaodotwork @josephfarina @kentonquatman @Kyujenius @rubyycheung
Full Changelog: https://github.com/facebook/astryx/compare/v0.5.4...v0.6.0
Astryx v0.5.4
[!WARNING] Stepper context compatibility: v0.5.3 changed the package-exported
StepperContextValue/useStepperContextshape, and v0.5.4 does not repair that compatibility break. Ordinary<Stepper>and<Step>usage is unaffected, but consumers that call the context hook directly or constructStepperContextValueshould remain on v0.5.2 while a source-compatible repair is evaluated. See #5659.
Astryx 0.5.4 updates the fixed-version core package family.
npx astryx upgrade --apply
- DropdownMenu keeps focus where it is when a controlled menu mounts already open. ArrowDown on the focused trigger enters an already-open menu without requiring a close and reopen. (#5976)
- DropdownMenuRadioGroup now renders a working, selectable menu in the docsite properties preview. (#5976)
- CLI integrations preserve block showcase metadata, letting packages ship their own docsite previews. Charts now includes its primary bar-chart showcase. (#5583)
- Stepper documentation now accurately describes the exported context surface while compatibility work continues. (#6088)
Thanks to @Kyujenius and @cixzhang.
Full Changelog: https://github.com/facebook/astryx/compare/v0.5.3...v0.5.4