facebook/astryx
 Watch   
 Star   
 Fork   
7 hours ago
astryx

Astryx v0.6.8

Astryx v0.6.8

Astryx v0.6.8 is a patch release focused on interaction reliability, accessible state, and clearer integration tooling.

Core

New features

  • Table resize handles now appear when a pointer enters the header, making resizable columns discoverable while preserving focused and scrolling states (#6194).
  • DropdownMenuItem now forwards host attributes and DOM event handlers to its row without replacing its built-in role, focus, and menu behavior (#7196).
  • markdownSourceLinesPlugin adds opt-in, 1-based inclusive source-line metadata to rendered Markdown blocks and custom renderers, with no change when the plugin is omitted (#7286).

Fixes

  • AppShell preserves page content, local state, and input focus across the mobile navigation breakpoint (#7236).
  • Calendar and DateRangeInput keep explicit controlled-empty range state, announce required and pending state correctly, and no longer restore a cleared range (#7297).
  • DateTimeInput keeps its calendar selection empty after the field is cleared (#7298).
  • A DropdownMenu opened by holding a finger stays open when that finger lifts from the trigger (#7241).
  • Dragging a mouse press between linked DropdownMenu rows no longer starts the browser's native link drag; the row under release acts normally (#7197).
  • Kbd uses the primary text token so key labels meet WCAG AA contrast over neutral keycaps across light and dark themes (#7100).
  • Steppers inside Popover, Dialog, BottomSheet, MobileNav, Lightbox, Toast, and other layer roots no longer inherit an outer Stepper's connector gap (#7287).
  • A claimed semantic Markdown fence renders through its plugin even when the host supplies components.code; unclaimed fences still use the host renderer (#7284).
  • Table resize handles stay keyboard-reachable on touch-first devices, stop at the table boundary without vertical overscroll, and compose with sticky columns regardless of plugin order (#6194).

CLI

  • integration verify now accepts the released minimum CLI versions for template replaces (0.6.4) and keywords (0.6.6) instead of requiring unreleased 0.7.0 (#7265).
  • Generated theme guidance points new authors to the palette generator and keeps the broad color scale opt-in (#6795).

Contributors

Thanks to @AKnassa, @cixzhang, @ernestt, @Geervan, @josephfarina, @korkt-kim, @rubyycheung, and @vjeux.

1 days ago
astryx

Astryx 0.6.7

Astryx 0.6.7 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

New Features

  • Item gains swipe actions for touch, per spec:AST-057. swipeActions declares, per side, the verbs a sideways drag uncovers as ItemSwipeAction[] ({id?, label, icon?, onActivate, isDisabled?, variant?: 'neutral' | 'accent' | 'destructive', hasRemoval?}, outermost last); swipeBehavior is reveal (the row rests open with every entry a real button; a long drag or a fling fires the outermost) or commit (the row slides out and the outermost fires; nothing rests). After an entry fires the row springs back, or holds out when the entry has hasRemoval. 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 (a listitem, a role-less row); ListItem passes both props through. No element is added to a row: its root translates and its panels counter-translate. List clips its rows in the inline axis (overflow-inline: clip). Types ItemSwipeAction, ItemSwipeActions, ItemSwipeActionVariant and ItemSwipeBehavior are exported.

  • Drawer is a container, like Dialog. A new padding prop takes a spacing step, and a theme's padding on drawer pads 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; pass padding={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; MultiSelector renders it in a grid. SelectorOptionData and SearchableItem gain action?: ReactNode: one node the caller renders and names — an IconButton, a Button, a menu trigger. In MultiSelector, once any option carries one the popup is a role="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 advertises aria-haspopup="grid". Nothing changes for options without an action. Selector and the typeahead panel do not render the key yet and warn in development when an item carries one.

  • useTableRowExpansion accepts panelVariant, 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-muted unconditionally, 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 Card no 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-muted is a low-alpha near-black that is close to invisible over a dark card — dark has effectively been rendering transparent all along.

  • useTableRowExpansion accepts hasRowClickExpansion, so a row opens when you click anywhere on it and not only on its chevron. (#5995) useTableTreeData has 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 getIsItemExpandable has ruled out all pass through untouched. The chevron already stops propagation, so it does not toggle twice.

    Collapsed rows are wired up as well as expanded ones, which is most of the point: the row you want to click is the one that has not opened yet.

Fixes

  • A BottomSheet drag writes its transform straight to the sheet once per input sample and renders nothing in between (React state changes when the drag begins and ends, and when its layout split changes). Measured in Chromium: 4 commits for a 24-sample drag, down from 49. The release still animates from wherever the finger left the sheet.

  • BottomSheet hands a touch to the sheet at the scroll edge of the box under the finger, not the body's alone. Content that scrolls inside the body (a pinned header and footer around a scrolling middle, a grid) never moved the body's scrollTop, so every pull down over that scrolled box dragged the sheet and the box could not be scrolled back by hand. From the box's top the pull still drags the sheet.

  • BottomSheet no longer blocks pinch-zoom. A pinch that started on an open sheet (its handle, its content, or across both) did nothing, because the sheet claimed every touch gesture: touch-action: none on the sheet and handle, pan-y on the content. The sheet now leaves pinch to the browser (pinch-zoom, and pan-y pinch-zoom on 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 .stylex module from outside its own directory) emitted an import that collapsed to the directory barrel — e.g. @astryxdesign/core/utils instead of @astryxdesign/core/utils/interactionOverlay.stylex. The barrel does not re-export those StyleX symbols, so the swizzled file could not compile.

    rewriteImports now gives every *.stylex module the deep subpath. The exports generator (scripts/sync-exports.js) adds 16 specific subpath exports for the .stylex modules 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.stylex

    These 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 .stylex import needs a deliberate entry in STATIC_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 Markdown no 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 Markdown no 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, Markdown read 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, Markdown joined 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 (with pan-y on its content), so a pinch anywhere on screen did nothing. It now declares pinch-zoom (and pan-y pinch-zoom on 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 through InternationalizationProvider. 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) useTableRowExpansion and useTableTreeData each carried a hand-rolled copy of "did this click belong to something else" — the same nine selectors, written twice. useClickableContainer already owns that rule for every clickable surface in the system, and its INTERACTIVE_SELECTORS list is the fuller one: it also covers role="link", radio, switch, tab, menuitem, option, combobox, listbox, slider, spinbutton and [data-pressable-container], and it excludes [aria-readonly="true"].

    Both plugins now call the hook's hasInteractiveAncestor and hasTextSelection, newly exported for containers that cannot use the hook itself — a <tr> assembled inside transformBodyRow has 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-fast and --ease-standard in the same pass, matching what TableRow already uses for its own hover transition.

  • Draw the row divider below a useTableRowExpansion detail 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 how TableCell scopes its "no trailing line under the last row" rule, so an expanded last row still ends the table cleanly.

  • Start the useTableRowExpansion detail panel at the first column rather than at the row edge. (#5995) The panel is one cell spanning the whole row with a flat 20px inline 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.

  • Table zebra 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 S has 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.

Documentation

  • 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. Bare npx astryx fetches an unrelated npm package until the CLI is a dependency.
  • align statusVariant documentation and test coverage across ComplexSelector, Tokenizer, and Typeahead

Other Changes

  • A click on a composed control the short list missed — a role="tab", a segmented role="radio", a Slider in 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.

@astryxdesign/cli

New Features

  • 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. Use theme remove and theme use to manage that record, and pass themes[defaultThemeSlug] to <Theme>. Plain astryx theme add keeps its released source-copy behavior, options, stdout, exit status, and every theme.add data field. It now warns that theme eject is the explicit source-fork command and theme add --import is the way to use the package-managed theme. Its machine result adds the DEP-0005 deprecation entry; the cleanup is CLN-0005 in a later scheduled minor.

    Use defineTheme({extends: importedTheme, ...}) for ordinary customization. theme eject creates an independent local fork with its descriptor. JSON callers receive theme.app from import, remove, and use, or theme.eject from eject.

    Existing bundled source copies stay where they are and keep their bytes. Run astryx upgrade --from 0.6.4 --path . --apply to add the missing unmaintained descriptor beside each bundled copy in src/themes. Until then, theme commands skip those copies and theme list and doctor name them as unmigrated. Package integration themes keep their released complete-directory copy, including the authoring descriptor.

    ASTRYX_THEME is 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 released package.json#astryx.theme lookup keeps its meaning. Doctor loads every recorded built module through the same theme adapter, so a broken runtime import fails theme-owners instead of passing.

    theme build also writes <out>.css.d.ts so 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 theme creates those exports, and integration verify checks 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 --json result about one artifact (a component and each of its projections, a doc topic, index, section, or docs-tree node, a template, a hook, and swizzle) carries package in its envelope, directly after type. A result that lists artifacts gives each item its own package: every search hit, build's start, alternatives, blocks, and components, a doc's sections, a docs-tree node's children, --blocks entries, and the component --list and upgrade --list entries. 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 layout command group The astryx layout command group (expand, check, grammar) is deprecated. Use astryx build to choose the template to start from, astryx template to scaffold it, and astryx docs layout for 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 meta field. Canonical stdout, exit codes, and the layout.expand / layout.check / layout.grammar response schemas are unchanged.

    Deprecation lifecycle (spec:AST-017 FR28, 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/nav component whose doc sets replaces: 'SideNav' takes over SideNav for unqualified component detail, batch selectors, every component --list detail level, search, swizzle, and gap-report routing, when @acme/nav declares "@astryxdesign/cli": ">=0.6.7" in peerDependencies (optional in peerDependenciesMeta). Every result names its package, --package @astryxdesign/core still selects the original Core component, and the replacement keeps answering to its own name. component SideNav --package @acme/nav also selects it.

    Any @astryxdesign/cli range that starts at 0.6.7 or later is the opt-in, including the >=0.7.0 that integration add doc --parent wrote in 0.6.4 and 0.6.5, and integration add theme in 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, from astryx doctor integration components and integration verify. For a package that declares the range, astryx doctor integration components reports 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-collapsible page 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)

Fixes

  • Printed commands name the scoped @astryxdesign/cli package whenever the astryx bin is not installed for the project

  • Compact the generated agent-docs block: fewer lines, same behavioral coverage

  • astryx doctor keeps its themes check, 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 in astryx.theme. Its fix now names theme add --import. With a generated module it reports the overall result of the theme checks. In a workspace, themes reads the app's own package.json, the file the CLI reads when it resolves the theme, rather than the package.json beside the root node_modules. It no longer counts the ASTRYX_THEME variable, which the CLI does not read, as a wired theme.

  • Doctor says why it skipped a check and what it checked, and doctor integration validate no 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 treated node_modules, .git, __tests__, and __fixtures__ as version folders and loaded their files as codemods. The same SKIP_DIRS filter 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. The deprecated CommandDoc field renders in --help and the manifest. The programmatic API exports (layoutExpand, layoutCheck, layoutGrammar) carry @deprecated in 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 .stylex module from outside its own directory) emitted an import that collapsed to the directory barrel — e.g. @astryxdesign/core/utils instead of @astryxdesign/core/utils/interactionOverlay.stylex. The barrel does not re-export those StyleX symbols, so the swizzled file could not compile.

    rewriteImports now gives every *.stylex module the deep subpath. The exports generator (scripts/sync-exports.js) adds 16 specific subpath exports for the .stylex modules 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.stylex

    These 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 .stylex import needs a deliberate entry in STATIC_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 via astryx template --list.

    JSON output is unchanged — --json template still returns the complete template.list response. 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 --type no 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-panels and the other layout section reads released in 0.6.6 work after the layout split. docs(route, section) returns the same docs.detail.section the guide's own section read returns, found by key, then by title, in --dense and --zh too. When no guide or more than one has the section, the read fails with ERR_UNKNOWN_SECTION and names the guides to read it from.

  • astryx search ranks a component, hook, or template that a query word names above a doc that matched only by keyword, heading, or text. font size finds Text first again instead of a typography guide. A doc the query names, such as font setup or migration, or one whose title the query holds, such as resizable side panels, keeps its place. Result scores do not change; only the order between domains moves.

  • astryx search finds a guide when the query is its route or title in the other number: side panel finds the side panels guide first again, and header and footer finds headers and footers. A section's heading still doesn't count as its topic's name, so font size keeps 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 build shows 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.

    detectStylingSystem now recognizes the official @stylexjs/rollup-plugin, @stylexjs/webpack-plugin, and @stylexjs/nextjs-plugin alongside the existing entries.

  • astryx theme build prints 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 full prints 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: --json output, --check, exit codes and written files are unchanged.

  • Wherever the CLI shows the <Theme> wrapper, it now also shows where Theme comes from: import {Theme} from '@astryxdesign/core'. This covers the astryx init next steps, theme add and theme build output, the doctor fix for an unimported theme module, build help, and the theme guides.

  • astryx theme add <slug> --import names the npm package that owns the added theme, in its JSON envelope (package, directly after type) and in its text output, the same way other results about one artifact do. A local theme has no package, so its result has no package, and theme remove and theme use are unchanged. astryx theme add --list keeps its JSON. Its text now names theme add <slug> --import to use a theme and theme eject <slug> to fork one, instead of the deprecated copy form.

    astryx doctor adds a theme-management check, and focused theme-* checks once a project has a generated theme module.

  • One-off commands in a classic Yarn (1.x) project use npx @astryxdesign/cli … instead of yarn dlx, which classic Yarn does not have

Documentation

  • astryx docs migration, internationalization, styling, styling-libraries, typography and tokens still work and now list focused guides. Each section is also readable under its guide, for example astryx docs tokens/tokens-spacing or astryx docs styling/tokens-and-setup stylex-setup, and astryx docs <topic> <section> keeps working: every section key these topics had still opens the same section. astryx docs tokens --depth all --detail full prints every token table, and a token-ref to tokens keeps resolving through the guide that holds the table. Integrations: these six names are now docs-tree sections, not topics, so an integration doc that declares extends or replaces with 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 as theme or color, can still be extended or replaced.

    (#7184)

  • astryx docs layout still works and now lists the focused guides. Each section is also readable under its guide, for example astryx docs layout/side-panels or astryx docs layout/scaffold shell, and astryx docs layout <section> keeps working. (#7125)

  • astryx docs layout opens with its overview again, and astryx docs author-a-theme and use-a-theme restore guidance the theme split dropped: what a bad extends base does, how adaptation rules are validated, what __built means, 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)

Other Changes

  • A hint uses npx astryx … (or pnpm exec, yarn, bunx) only when the astryx bin is in node_modules/.bin, in the project or a folder above it. Otherwise it prints npx @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 named astryx is not this CLI, so following that hint fetched an unrelated package.

  • astryx blog follows 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 / cleanup CLN-0006 — astryx layout command 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 validate names a folder it cannot read and keeps checking. A declared root that is a file or that cannot be read is reported as invalid_root or unreadable_root instead 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 astryx bin is not installed for the project), a Yarn project got yarn dlx @astryxdesign/cli …. On classic Yarn that command fails.

  • Classic Yarn is read from the declared packageManager version, the yarn.lock header (# yarn lockfile v1), a committed .yarnrc (without .yarnrc.yml), or the runner. Yarn 2 and later keep yarn dlx.

    Classification: contract-restoring. JSON shapes are unchanged; only hint text changes.

@astryxdesign/theme-butter

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

@astryxdesign/theme-chocolate

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

@astryxdesign/theme-gothic

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

@astryxdesign/theme-matcha

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

@astryxdesign/theme-neutral

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

@astryxdesign/theme-stone

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

@astryxdesign/theme-y2k

New Features

  • Each theme package now exports /fonts.css beside /built and /theme.css. Import it when the theme uses non-system fonts.

Contributors

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

3 days ago
astryx

Astryx 0.6.6

Astryx 0.6.6 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

  • BottomSheet is a container, like Dialog. A new padding prop takes a spacing step, and a theme's padding on bottom-sheet now 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.

  • ComplexSelector can hang off a control the caller renders. A new renderTrigger render 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 from label, opens on click or ArrowDown, and returns focus to the control on close. The existing handleRef and onOpenChange work 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 dataVars group for CSS-capable data visualization consumers.

  • Export decodeMarkdownCharacterReferences from @astryxdesign/core/Markdown/parser and @astryxdesign/core/Markdown It decodes character references the way Markdown renders them — &copy;, &#169;, and &#xA9; become ©; unknown names and references without their semicolon stay as written — using the same table Markdown uses. 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 titled role="group" of rows for compound-mode menus. The items data 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 (also ContextMenuGroup and BreadcrumbMenuGroup) renders a role="group" named by its heading through aria-labelledby; the heading shares the data mode's typography and astryx-dropdown-menu-section-heading theme 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 emptySearchText everywhere, and it takes a ReactNode. Selector, MultiSelector, and CommandPalette already called it emptySearchText and already accepted a node. Tokenizer, Typeahead, BaseTypeahead, and each ChatComposerInput trigger called it emptySearchResultsText and 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 emptySearchResultsText keeps working exactly as released. Set both and emptySearchText wins, with a development warning. Migration is the name alone.

    Deprecation lifecycle (spec:AST-017 FR28, 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/plugins and 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 uses useContainerReveal: 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. List and ListItem keep 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-renderer subpath, renders one parsed extension node with the given plugins exactly as Markdown presents 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 trigger render prop: hang a menu off any control. trigger renders the control the menu opens from — an IconButton, a chip, an avatar, a list row — and hands it DropdownMenuTriggerProps to spread: the press model, the keyboard opens, the toggle click and the ARIA wiring. The menu is named by that control through aria-labelledby. button and trigger are mutually exclusive (a dev warning).

  • Menu arrows wrap and PageUp/PageDown page. In DropdownMenu, ContextMenu and DropdownMenuSubMenu, ArrowDown on the last row wraps to the first and ArrowUp on the first to the last, as macOS menus do (Selector keeps 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. useListFocus gains hasPaging.

  • DropdownMenuItem takes href: a menu row that navigates is a real link. DropdownMenuItem (and a data-mode item) takes href, target and rel. The row renders as the anchor itself, with role="menuitem", routed through LinkProvider, so a ⌘-click, Ctrl-click or middle click keeps the browser's meaning and skips onClick; a plain click runs onClick, closes the menu and navigates. The touch sheet renders the same item as a link row. onClick now receives the click event. Enter and Space in every menu synthesize a click that carries the key's modifiers.

  • DropdownMenu takes menuMaxHeight to 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, Selector and 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 declares touch-action: none so 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 DropdownMenu trigger 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. useMenuPress gains onTriggerPress, triggerProps, isTriggerClickFromPress and longPressDelayMs.

  • 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 DropdownMenu and ContextMenu; presentation (flyout | drill-in | adaptive) overrides the policy.

  • MultiSelector can offer a Create "<query>" row for a search that matches nothing. With hasSearch, the new hasCreate switch puts a Create "<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, calls onChange with the query appended to the value and a second argument {type: 'create', query} (exported as MultiSelectorChange), 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. hasCreate without hasSearch warns in development and offers nothing. Off by default.

  • MultiSelector can hang off a control the caller renders. A new renderTrigger render 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 by label, takes focus on open, and focus returns to the control on close. handleRef (open/close/toggle/isOpen, the ComplexSelectorHandle shape) and onOpenChange let 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 DropdownMenuSubMenu the 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. useMenuHover gains flyoutRef and passes the leave event to onMouseLeave.

  • Touch press model: under a coarse pointer the bare :active arm 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-paint and read by whatever paints it. The controller writes data-astryx-press="on"|"fading" on the nearest element marked data-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) and interactionOverlayStyles from @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.

Fixes

  • 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, and PowerSearch (which composes Tokenizer): clicking the search/combobox input after the dropdown closed without a blur now reopens it. BaseTypeahead only ever opened its dropdown in response to a real focus event. 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 new focus event, since focus() 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.

    BaseTypeahead now also opens on click, specifically when the input was already focused before the click began (checked at pointerdown, 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 Typeahead and Tokenizer compose BaseTypeahead directly — Selector, MultiSelector, CommandPalette, and DateTimeInput use their own separate combobox implementations (which only follow BaseTypeahead'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 1fr grid 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 explicit width; truncate such values with Text maxLines={1} and give wide content its own scroll region. An explicit width is now the card's preferred width in a row rather than a floor. To hold it, wrap the card in StackItem in a flex row, or set a consumer minWidth on 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 ClickableCard links 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 CollapsibleGroup context 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 throws InvalidStateError.

  • Markdown: read angle-bracket link destinations to their closing bracket Markdown now reads an angle-bracket destination as CommonMark specifies: parentheses inside the brackets are part of the address, so [a](<b(c>) links to b(c); an escaped bracket inside is part of it too, so <b\>c> is b>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\Ada rendered as C: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 Markdown now 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 Markdown now 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 >>> nested are quotes, as CommonMark specifies. These lines used to show as plain text with their > marks.

  • Markdown: start a new list when the bullet changes Markdown now starts a new list when a bullet list's marker changes — - a then * b are two lists — as CommonMark specifies, and as ordered lists already did when their delimiter changes.

  • Markdown shows character references such as &amp;, &copy;, and &#169; as the characters they name Fish &amp; chips &copy; 2026 rendered 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 Markdown now 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. `one two`` 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 Markdown now reads a fenced code block's language as the first word of its info string after any spaces, as CommonMark specifies, so ~~~ js and ``` js are 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 Markdown now 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 alt a]b c rather than the raw source with its backticks and asterisks.

  • Markdown image alt text shows character references and escapes as the characters they name ![Fish &amp; chips](…) gave the image the alt text Fish &amp; 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 Markdown now 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 Markdown now 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 ```js inside 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 text Markdown now 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 Markdown no 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 Markdown no 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&amp;b=2) links to https://a.com/?a=1&b=2, [x](a\)b) links to a)b, and reference definitions decode the same way. The URL safety check runs on the decoded destination, so an encoded unsafe scheme such as &#106;avascript: 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 text a]b.

  • Markdown: pair link text brackets as CommonMark does Markdown now 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 only b; 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 Markdown no longer ends link text at a ] inside a code span, since code spans bind tighter than links (CommonMark). [`a]b`](/u) links the code a]b, and [`[x](javascript:y)`](/rel) links the code [x](javascript:y) to /rel rather 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 to https://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 Markdown now 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 **bold before trailing spaces, so the partial text keeps its formatting.

  • Markdown: keep the indentation of a message's first line Markdown now 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 Markdown now 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 Markdown no 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. DropdownMenu used 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 explicit padding on the nested Layout still wins.

  • Restore Outline's visible keyboard focus indicator.

  • Refresh stale cached field entries when PowerSearch reopens 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 like data:text/html,…. In Markdown this also refuses links, images, and angle autolinks whose destination decodes to that form, such as data:&#32;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 centerContent are 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-*, and onClick to the Typeahead and Tokenizer root elements. Preserve existing ref, styling, and data-testid targets and Typeahead's built-in focus and edit behavior inside InputGroup.

Other Changes

  • emptyText and emptySearchText accept a ReactNode, 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-hidden is left out of the announcement exactly as it is left out of the screen. A loading panel still announces nothing.

  • deprecation DEP-0001 / cleanup CLN-0001 — Tokenizer.emptySearchResultsText

  • deprecation DEP-0002 / cleanup CLN-0002 — Typeahead.emptySearchResultsText

  • deprecation DEP-0003 / cleanup CLN-0003 — BaseTypeahead.emptySearchResultsText

  • deprecation DEP-0004 / cleanup CLN-0004 — ChatComposerTrigger.emptySearchResultsText

  • An explicit width on Popover and menuWidth on DropdownMenu or Typeahead render 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 inset on the LayerProvider it 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 existing toast.inset keeps its meaning as the toast-only override.

    One additive prop (LayerProvider.inset, type LayerInset); no other prop, type, or default changes. spec:AST-059 holds the decisions; the Core/Layer stories show each behavior.

@astryxdesign/cli

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

  • Templates declare their keywords, and build tells a part of a page from a page by the components the project can use (#6805)

  • build chooses 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 0 reads only the namespace you name, --depth 1 adds the docs right below it (what a read without --depth shows), and --depth all goes to the bottom. With --depth, --detail sets how much of each doc below shows: brief (the default) is one line each, named by where it sits so you can open it, compact adds its sections, and full prints it whole. So astryx docs cli --depth all is a map of every CLI doc, and astryx docs cli/integrations --depth all --detail full prints the integration guides as one read. Where a read stops, a namespace says how many docs sit below it. --json returns the same tree as docs.node: each child carries its own slots while the read goes deeper, childCount where it stops, and its text at compact or full. docs() takes the same depth and detail options. Reads without --depth are unchanged.

  • Point at discover where people look for things to add Nothing an agent reads named discover, so agents asked to find a theme searched the package registry instead. The agent block astryx init writes now lists discover <words> (integrations you could add, and the ones you have), theme list ends with More themes in packages you could add: astryx discover theme, and a text search ends with More 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 --from when you want to change a lot; for a small change that stays linked, use extends in defineTheme.

Fixes

  • Say when a command did nothing: upgrade reports sourcePathFound, the integration checks report validated. 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 upgrade defaults --path to ./src. A project laid out as app/ (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 --json suppresses by design. upgrade.run now carries sourcePathFound, and the human completion line names the directory it did not find.

    astryx doctor integration validate|components|docs|templates returned {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 carry validated, false only when nothing was inspected.

  • astryx manifest --json now 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 and upgrade lists upgrade.registry, which upgrade --registry --json already emits.

  • astryx template <name> <path> and astryx layout expand now say when they replaced Astryx demo media. The template.copy and layout.expand receipts carry demoMediaReplaced, 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 (when layout expand prints 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> and template() now return the same source that astryx 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.show gains demoMediaReplaced (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 template into an unwritable directory returned {"error": "EACCES: permission denied, open '/home/you/project/readonly/x.tsx'", "code": "ERR_UNKNOWN"}, and swizzle returned the mkdir equivalent. 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 swizzle that 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 --available runs 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 base discover with no integrations also gains a pointer to npm and the integrations docs.

  • A package with a namespace doc or a placed guide needs @astryxdesign/cli 0.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 --parent wrote "@astryxdesign/cli": ">=0.7.0", a range no released CLI satisfies, and integration verify failed a docs-tree package whose CLI peer started at 0.6.4. integration add doc --parent now writes ">=0.6.4", marked optional, and integration verify accepts it. A template that sets replaces or keywords still needs ">=0.7.0".

  • gap-report fails when a listed integration cannot load, instead of reporting a clean result. An integration whose astryx.integration module 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 with consent_required, printed nothing on stderr, and offered --confirm-public to 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 to dependencies. A fork of a bundled theme such as neutral copies its icons.tsx, which imports lucide-react, but the package did not declare it. So astryx theme build on the fork failed with "Cannot find module 'lucide-react'", and an app that installed the package hit the same error. --from now 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 pack without --check now points only at astryx integration verify. It used to say "Pass --check to verify the integration tarball, or run astryx integration verify", which sent people to the deprecated spelling. It now says that integration pack is now integration verify, and that npm pack builds the tarball. The error code and exit code are unchanged, and integration pack --check still 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 theme doc 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 add command. --type theme filters 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, focus or content, 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 neutral finds the Neutral theme first.

  • A package that ships a theme or a doc section id needs @astryxdesign/cli 0.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, so integration add theme wrote "@astryxdesign/cli": ">=0.7.0", a range no released CLI satisfies, and integration verify failed a theme or section-id package whose CLI peer started at 0.6.4. integration add theme now writes ">=0.6.4", marked optional, and integration verify accepts it for themes and section ids. A template that sets replaces or keywords still needs ">=0.7.0".

  • theme build resolves 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 --check reject unsupported inline registries with ERR_THEME_INVALID before generating or writing output. Move such a registry into its own module and import it into the theme file.

  • astryx theme build in an app uses the app's installed @astryxdesign/core, and says to install Core when there is none. Run one-off with npx @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.

Other Changes

  • TemplateDoc gains an optional keywords list: the ideas, domains, and other names a builder might use for what the template serves. parseTemplate validates it, discovery carries it from every template source, astryx search matches it as it matches a template's description, and astryx build ranks page templates on it. Each Core page template's closing list of ideas moved out of its description into keywords, so descriptions describe the layout.

  • build starts a part of a page where it lives (spec:AST-048 FR3), 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 keywords need @astryxdesign/cli 0.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; only template --list and search print a warning. integration verify fails a package whose template sets keywords until it declares @astryxdesign/cli >=0.7.0, as it does for replaces.

  • api/build/kit/weights.mjs scores each candidate start (the app shell and every ready page template) from the idea's stemmed words using the tables in weights.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.

@astryxdesign/theme-butter

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

@astryxdesign/theme-chocolate

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

@astryxdesign/theme-gothic

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

@astryxdesign/theme-matcha

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

@astryxdesign/theme-neutral

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

@astryxdesign/theme-stone

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

@astryxdesign/theme-y2k

New Features

  • Add a shared upload icon, and use it for FileInput's upload affordance instead of the directional arrowUp Themes draw upload through icons.upload, separately from arrowUp, so sort arrows and every other arrowUp use stay unchanged. Every bundled theme and theme template draws upload in its own icon style. FileInput keeps its icon size, placement, color, and accessibility in both modes; a theme with no upload artwork shows the default upload-into-tray glyph there.

    A complete IconRegistry may still omit upload in this release. The next minor makes it required, so add an upload entry to any registry you type as IconRegistry.

Contributors

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

8 days ago
astryx

v0.6.5

Astryx 0.6.5 — all stable @astryxdesign/* packages ship at this version.

npx astryx upgrade --from 0.6.4 --apply

@astryxdesign/core

New Features

  • Stop bundling translator-only descriptions with the built-in English fallback and add compact generated string-map modules for every shipped locale. Existing rich JSON catalog exports remain unchanged.
  • Popover and usePopover take a padding prop on the spacing scale (matching Card and Stack). padding={0} paints a flush surface for content that owns its own edges, such as a list of rows or a header with a rule; the default rung (3) is unchanged.

Fixes

  • Button no longer overflows narrow rows: a labelled button can shrink below its label width and truncates the label with an ellipsis, while icon-only buttons stay square
  • Prevent an empty Tokenizer input from creating a blank trailing row.
  • SegmentedControl's default hug layout 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-5 is taller than the token can hold, instead of overshooting it.
  • Switch: announce busy/loading states through the persistent useAnnounce live region and localize the announcement via @astryx.switch.loading.

@astryxdesign/cli

New Features

  • astryx component accepts 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. The component() API accepts selector arrays and always returns component.batch for 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 shared BatchResponse and BatchRow types for typed receipts.
  • astryx discover browses 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 as discover in astryx.config, and an integration exports one as a discover named export. Discover only reads: it prints the command that adds a package and never runs it. Existing --json fields keep their meaning. A free-text query now always lists its matches, even an exact component name, and astryx discover <package>/<Name> opens one.
  • astryx integration verify is the new name of astryx integration pack --check. The check you run before publishing an integration now has a name that says what it does. astryx integration verify packs 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 --check still works as a deprecated alias: it runs the same check with the same output, JSON, and exit codes, prints a note that names integration verify, and shows as deprecated in help. It will be removed in a later release. The integrationPackCheck() API and its integration.pack-check JSON response do not change. With --json, a command group given an unknown subcommand now reports ERR_UNKNOWN_SUBCOMMAND and lists its subcommands, where it used to say JSON output is not supported.

Fixes

  • Fix the CLI's topic docs and how they print.

  • astryx doctor no longer reports an integration it could not check as absent or complete (#6619)

  • upgrade's filesChanged counts files, not (codemod, file) pairs (#6622) One source file that four codemods each changed was reported as four files changed, so filesChanged matched transformsApplied and 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.

    filesChanged is now the count of distinct files. transformsApplied is 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 in filesChanged.

  • A parse error prints the Astryx error format in text mode. astryx theme list --lang zh-Hans printed Commander's own line — error: option '--lang <locale>' argument 'zh-Hans' is invalid… — while every other CLI error prints Error: …. --json was 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. --help and --version are untouched and still exit 0.

  • The CLI reference now matches what the commands do. Every --help ends with the command's examples and a More: 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 adds help, version, and upgrade.registry, and astryx manifest now lists upgrade.registry for upgrade. The --zh, --dense, --lang, and --detail descriptions name the commands they change, and command summaries say when to use each command. When astryx template refuses to overwrite a file, it now says to re-run with --overwrite (or -f). The upgrade command 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 --check now checks the tarball when a prepack, prepare, or postpack script 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 the pack_failed message.

  • astryx doctor integration docs fails 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 an invalid_doc_graph error instead of a warning. A link that names no doc still only warns, since it prints as written. doctor integration docs and doctor integration components also no longer print an [ok] line after a check that failed.

    A mistyped subcommand under doctor now fails and lists the subcommands the group has: astryx doctor integrations used to run the project checks, and astryx doctor integration bogus exited 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 that astryx integration add theme writes, and it rejects a section id. Either way it hides the package's themes or doc topics with no warning. astryx integration add theme now adds "@astryxdesign/cli": ">=0.7.0" to peerDependencies, marked optional, and astryx integration verify fails with themes_need_cli or section_ids_need_cli when a package needs that peer range and does not declare it.

  • astryx integration verify resolves every public import in the packed package, not in your source folder. Before, its temporary app resolved your package's own name through the source package.json, so an exports target left out of the tarball still passed. It now fails with component_export_missing, as an app that installs the tarball would.

  • astryx theme add and astryx theme build now undo a failed write completely. Before, when one file failed to write after others were written, the written files kept their new content. Now every replaced file gets its previous content back, every new file is removed, and the error names any file that could not be restored. Both commands also refuse to replace a destination that is a symbolic link. (#6852)

Other Changes

  • A code block's label now prints above the block instead of as a // label line 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 mode searches 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 search keywords, now a documented ReferenceDoc field, and a namespace's keywords now count too. A query keeps its phrase when common words such as make, build, or an drop out, so astryx search make an integration finds 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 as command palette, finds the component. Outside an app, where @astryxdesign/core is not installed, astryx search searches the docs instead of failing, and says so; --type component, hook, or template still needs Core.
  • Snippets that failed when copied now work: StyleX token imports, the fr-FR.json locale path, Tailwind rounded-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 init writes, --detail brief for a shorter read, the Neutral and Matcha fonts, the components that need anchor positioning, gap steps, 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 tokens lists all 258 tokens, adding the data visualization and syntax groups, and shows both halves of every light-dark() value.
  • Long sections are split, vague titles renamed, and the --dense and Chinese versions no longer drop blocks. Eleven long section keys are shorter, such as astryx 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 --to help says it takes the Core version whose upgrade runs the codemod.
  • The agent block that astryx init writes now says upgrade --from <old version> --apply; upgrade --apply alone 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, but implicit-integrations now names it and says it contributes nothing. Before, doctor said that no installed dependency ships a manifest. The check stays informational, and astryx doctor integration validate <package> gives the details.
  • implicit-integrations lists 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-identity says how many loaded integrations it could not read, instead of counting only the readable ones.

Contributors

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

9 days ago
astryx

Astryx v0.6.4

Astryx 0.6.4 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

New Features

  • Add Timer for standardized elapsed durations without React tick renders. (#6438) Use Timer for active-operation elapsed time. It starts from mount by default, accepts an earlier Unix-millisecond startTime, offers elapsed and clock formats with adaptive cadence, and matches Timestamp typography props.

  • DialogHeader: expose the start- and end-content wrappers as theme targets (#6415) Adds dialog-header-start-content and dialog-header-end-content so 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.

  • DialogHeader title and subtitle now accept any ReactNode, not only strings. A title can carry inline markup and still renders inside the focusable h2 that 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 a Link. String callers are unchanged. An empty-string, boolean, or nullish subtitle renders nothing, and a numeric 0 subtitle now renders inside the subtitle text.

  • DropdownMenuItem, DropdownMenuCheckboxItem and DropdownMenuRadioItem forward a ref to the row root (#6687). The ref reaches the element carrying role="menuitem" (or menuitemcheckbox / menuitemradio), the way Item and DropdownMenuDivider already 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 between role="menu" and the row.

  • Add presentation to DateInput, DateTimeInput, and TimeInput (spec:AST-043) (#6628). presentation names 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 released nativePicker="never" already was. presentation="native" always shows native; adaptive-native keeps the released native fallbacks.

    nativePicker is deprecated but keeps working exactly as released (touch→adaptive-native, always→native, never→adaptive-bottom-sheet, or text-input for TimeInput); presentation wins when both are set. astryx upgrade ships migrate-native-picker-to-presentation for static callsites.

  • List: add edgeCompensation="inline" to compensate for item content inset within container padding (#2626) ListItem insets 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" on List cancels, 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.

    Item now publishes its inline inset as --_item-inset-inline and derives its own paddingInline from 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 set paddingInline on item feed the variable automatically via the derived var registry.

  • Markdown: add the first-party soft-breaks plugin (#6459) Use markdownSoftBreaksPlugin from @astryxdesign/core/Markdown/plugins to render soft line endings as hard breaks without preprocessing source. The plugin matches the real remark-breaks package through Astryx's supported adapter path while keeping code and other opaque content unchanged.

  • Table: add a selection-aware bulk-actions wrapper that consumes useTableSelectionState output while keeping the selection plugin behavior-only. (#6474)

Fixes

  • Preserve authored heading and text component styles when theme adaptations change the type scale (#6710)
  • AvatarGroup keeps its overlap when avatars are wrapped in a HoverCard or Tooltip. Every avatar and the overflow indicator now take the overlap margin, and the group pads its start edge to match, so the overlap no longer depends on each avatar being a direct child of the group (#6736).
  • Avatar initials skip punctuation: each word's initial is its first letter, digit or emoji, so Northwind Workbench (automation) renders NA instead of N( and “Ada” Lovelace renders AL instead 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 when isDisabled flips to true, which shifted anything bottom-aligned beside it (e.g. a send button in a grid row) (#6654). The root's minHeight was set to the shared line-height only, not the padding editable and placeholder both add on top of it — normally immaterial, since the editable region reserves its own padded box even when empty. A disabled, empty contentEditable region 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. minHeight now 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 numeric 0 remains 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 onClick handlers with ChatSendButton's send and stop actions instead of replacing them (#6653). Consumers that used onClick to replace sending should move that logic to onSend, 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 scrollButton renders hidden, but opacity: 0 and pointer-events: none leave 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 sets visibility: 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-md instead of a hardcoded 32px, so a theme that retunes the element scale can no longer make the pill clip its own Button. ChatLayout's consumer docs also gain the density prop, 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-button actually restyle the chat scroll-to-bottom pill. The documented target sat on the invisible full-width row that centres the pill, so a backgroundColor override 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-surface INV4). 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 changeAction now 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 onClick now 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.sql threw from insertRule and took the whole block down). The generated ::highlight() name and --color-syntax-* custom property now pass through CSS.escape before 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)
  • Dialog and 30 other components no longer lose their border and background resets in the shipped CSS. The sources used the border: 'none' and background: 'none' | 'transparent' shorthands, which StyleX's default property-specificity mode drops silently, so the declarations never reached astryx.css; a consumer that does not load reset.css saw the UA <dialog> frame. They are now the borderWidth / borderStyle / backgroundColor longhands. 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 (isComposing or keyCode 229) 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.
  • List with hasDividers no longer draws a divider after the last ListItem. The last-item reset used the borderBlockEnd shorthand, which StyleX's default property-specificity mode drops silently, so it never reached the shipped CSS; it is now the borderBlockEndWidth longhand. (#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-2 token (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_list value 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 Selector listbox from the component's label so 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)

Documentation

  • Document CheckboxList's isReadOnly prop, and separate the select-all block example's rows with hasDividers instead of placing a Divider inside the options list (#6777).
  • Clarify that CheckboxListItem reads isChecked and onCheck inside a CheckboxList without value (such as a select-all item), and requires value only when the parent CheckboxList has a value array (#6778).
  • Clarify that CheckIndicator is the selection mark itself and does not render persistent control chrome.

@astryxdesign/cli

New Features

  • 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 the astryx docs command that opens the doc; a link that names no doc prints as written, and astryx doctor warns on it. An older CLI prints the link as plain text. reference, workflow, and collection blocks stay in namespace docs.

    An integration can ship namespace docs and place its guides in them. They show up in astryx docs beside cli, with the same moves, links, and search, and astryx doctor integration docs checks them before the package ships. A namespace doc in an integration's docs directory no longer fails to load, and astryx 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 --parent declares the CLI that reads them as an optional @astryxdesign/cli peer, and astryx integration pack --check fails 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 with astryx docs <topic> <section>, or print the whole topic with --full. --dense still 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> --json and docs(topic) in @astryxdesign/cli/api still return the whole topic (docs.detail), and --index returns its section list.

  • Read the CLI's docs as a tree, one level at a time. (#6498, #6626) astryx docs cli lists the CLI's guides and reference. astryx docs cli/commands lists every command, astryx docs cli/api lists the API's functions, schemas, and enums, and a route such as astryx docs cli/api/functions/search prints one doc. --json returns docs.node for a namespace or typed doc, identified by its doc identity (a generated level has id: null). The text of astryx docs lists the docs tree's namespaces first; its --json keeps data as the topic list and adds them in meta.namespaces. Every command, API function, schema, and enum doc the CLI ships declares the namespace that reads it: astryx doctor fails 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 behind astryx theme template. (#6626)

  • The integration guide moved from astryx docs cli-integrations to astryx docs cli/integrations. (#6626) Documentation names and routes are mutable catalog data under spec:AST-017/FR45, so the move is nonbreaking and needs no compatibility alias. The guide now lives in the CLI's docs tree, under cli. Use astryx docs cli/integrations, including section reads such as astryx docs cli/integrations components. The old name no longer resolves. The docsite page stays at /docs/cli-integrations.

  • astryx search finds 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 doc works without @astryxdesign/core. astryx docs lists 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, plus Previous and Next for a section or a docs-tree page, and Related for a typed doc (its command or API function, and its related docs). --json carries them as links (up, previous, next, related). Each search hit carries parent, the command that opens the level above it, and a docs-tree hit carries package.

  • A doc section can include another doc instead of copying it. Put a reference block in the section, such as {type: 'reference', target: '@astryxdesign/cli:schema:integration', projection: {fields: ['components', 'docs']}}, and astryx docs prints 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.fields keeps only the named fields of a schema, and presentation is full (the default), compact (no code blocks), or summary. A reference to any other doc shows its title and summary. A read inlines the block, so --json still returns only the stable block kinds. astryx doctor integration docs fails 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:integration opens astryx docs authoring integration.

  • astryx doctor now fails when a type that @astryxdesign/cli/authoring exports has no doc in astryx 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 presentation to DateInput, DateTimeInput, and TimeInput (spec:AST-043) (#6628). presentation names 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 released nativePicker="never" already was. presentation="native" always shows native; adaptive-native keeps the released native fallbacks.

    nativePicker is deprecated but keeps working exactly as released (touch→adaptive-native, always→native, never→adaptive-bottom-sheet, or text-input for TimeInput); presentation wins when both are set. astryx upgrade ships migrate-native-picker-to-presentation for static callsites.

  • Prepare integration template replacement before its supported package boundary. (#6265, #6626) An integration template can set replaces in its own metadata to a Core template id. Unqualified template lookup and discovery surfaces use a valid replacement, while --package @astryxdesign/core still selects the original. Missing targets, type mismatches, a declaration on a template that cannot be used, and duplicate declarations fail closed and are reported by astryx 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 replaces must 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 IntegrationTemplateConflict responses keep their released warning-only shape on 0.6.x. The optional TemplateListEntry.replaces field is additive; at or after 0.7.0, replacement-specific relationship, replaces, and severity: '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" with isFullBleed, sticky section labels, axis="both", sticky pass-through against stickyContainment="always", and overscroll allow 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 public ReferenceContentBlock union keeps its 0.6.x members so existing exhaustive renderers continue to compile; graph-only workflow, collection, and reference blocks are exported separately as GraphContentBlock and are accepted by NamespaceDoc.

    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-N keys, 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), and astryx docs <topic> <key> reads one section. A topic read still returns the whole doc. astryx docs authoring documents 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 doctor reports 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 that astryx integration add theme wrote in 0.6 (stable since 0.6.3). Each theme carries a strongly typed <name>Theme.doc.mjs beside its source instead, and a themes root that still holds the catalog is refused. To migrate an integration package, run astryx upgrade --from 0.6.3 --path . --apply in 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 add copies an integration theme's complete directory. astryx doctor integration validate warns 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.

Fixes

  • 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, and PostCodemodCommand's env no longer needs Node's types. (#6492)

  • Drop gpt-tokenizer from 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-agents and astryx upgrade --apply no 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-agents fails with ERR_PATH_TRAVERSAL and 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 component and astryx hook no 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 init human output is now plain ASCII, including the per-file lines init --remove-agents prints. Status lines use [ok] instead of a check glyph, and dashes, bullets and arrows print as - and ->. --json output is unchanged. (#6540)

  • astryx upgrade human 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. --json output 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_TRAVERSAL instead of creating the file there. A symlink escape reported by integration add now carries ERR_PATH_TRAVERSAL too, instead of an unregistered PATH_TRAVERSAL code. (#6513)

  • astryx blog text output now labels the feed URL feedUrl, matching its --json key, 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 a setup: line in its text output. That field existed only in the text, never in the --json kit, so the two views disagreed. The same guidance is in the no-query astryx build playbook. (#6570)

  • astryx build --json with no query, and build() 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, and playbook: true is still there. (#6566)

  • The programmatic component() API now rejects detail and lang values that the astryx component command rejects, with the same codes (ERR_INVALID_DETAIL, ERR_INVALID_LANG). It used to fall back silently: an unknown detail returned the name list, and an unknown lang returned English. (#6576)

  • The programmatic component(name, {cwd, blocks: true}) now discovers blocks from the cwd it 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)

  • parentDoc in astryx component <Name> --json is now a documented part of the component.detail response. The field appears when a sub-component such as HStack is scoped out of its parent's doc. It is in the published response type and the component() reference, and the text output now shows it as parentDoc: Stack. (#6575)

  • astryx component <Name> no longer prints a "Related block templates" list that --json never carried, so the text output shows only what the JSON result holds. The same blocks are still listed by astryx component <Name> --blocks, in text and JSON. (#6574)

  • astryx component <Name> --package <pkg> no longer ignores --source and --blocks when the package publishes docs through the legacy astryx.docs field. --source now fails with ERR_NO_SOURCE, and --blocks returns the blocks, the same answers as without --package. Before, both flags silently returned the plain doc. (#6577)

  • astryx component --list now prints the right import for components from packages that publish docs through the legacy astryx.docs field. It used to show an @astryxdesign/core path for them. The JSON list entries now carry the same import that astryx component <Name> reports for each of those components. (#6578)

  • astryx integration add component <Name> now refuses with ERR_FILE_EXISTS when a component doc anywhere under the components root already uses that name, for example components/<Name>/<Name>.doc.mjs. Before, it wrote a second <Name> beside the first and reported success, and astryx component <Name> then showed the new scaffold instead of the authored component. --dry-run refuses the same way. (#6557)

  • The debug entry of the AstryxConfig type and of astryx docs authoring config now 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 leaving debug out records nothing. (#6514)

  • The text output of astryx discover (the package list and a single package) now shows every field its --json entry carries, including category and version, which only the JSON used to include. (#6521)

  • astryx doctor text output now uses the same field names as --json: each check prints its id and label (the label was shown as check, and the id was missing), and the summary prints pass, warn, fail, and info under a summary heading instead of a prose line. (#6516)

  • astryx template --help and astryx layout expand --help now 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 gets page.tsx, the block's file name, or <Name>.tsx. layout expand and layout check also document - for stdin and that --file wins over the argument, and --overwrite no longer mentions a prompt the CLI never shows. (#6571)

  • The CLI no longer reads or sets the ASTRYX_LATEST_VERSION environment variable. Its only effect was an FYI: A newer version of @astryxdesign/core ... line on stderr after astryx component and astryx 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/json types now declare apiVersion on CLIError, CLIUnsupportedError, and the success envelope that parseResponse and assertResponse return, matching what every --json envelope carries. Code that constructs a CLIError value by hand, for example in a test double, now has to include apiVersion. (#6555)

  • astryx <command> --help (including astryx manifest --help) now ends with the command's documented exit codes, and each command in astryx manifest --json carries them as exitCodes: [{code, when}]. astryx doctor --help shows them once, and the layout and discover exit codes now say when they apply: bare astryx layout exits 1, and a blank discover query exits 1 when packages are discovered. (#6586)

  • astryx gap-report --help and astryx manifest now describe the component argument and say that component, --category, and --reason are required unless --list-categories is 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 build now fails with ERR_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 generated HeadingTypeMap. (#6547)

  • With --json, astryx help <unknown-command> and a command group run without a subcommand (such as astryx layout --json) now return an error envelope, with ERR_UNKNOWN_COMMAND or ERR_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 --json envelope does not carry. It now names the related components and points to astryx component <name> --blocks, which returns their block templates as JSON. (#6537)

  • "astryx": {"inheritDebug": false} in package.json now also refuses the debug handler of an autolinked integration in a project that has no astryx.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 with templateName to the project directory. A src symlink that points outside the project is rejected with ERR_PATH_TRAVERSAL before anything is written. (#6538)

  • astryx integration add --help and the CLI manifest now define every control: the name format for each kind, that --type defaults to page, that --to takes an exact semver version, and that --replaces and --extends can't be combined. The integrationAdd() 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 discover no longer exits 1 because of it. (#6519)

  • One integration whose templates root cannot be read, for example a manifest that points templates at a file, no longer makes astryx template fail 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 --json option's description in astryx --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 omit apiVersion, meta, and the stable code field that consumers branch on. (#6549)

  • astryx layout expand <expr> <dir> now refuses the write, with ERR_PATH_TRAVERSAL, when <dir>/<Name>.tsx is 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 fields componentsUsed and todos, the keys the --json output uses, instead of Components and TODOs. (#6569)

  • astryx layout expand now caps every *N repeat 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, so B*999999999 could hang or run out of memory. astryx layout grammar now states the cap. (#6565)

  • astryx layout check - and astryx layout expand - now stop reading stdin at 5 MB and fail with ERR_INVALID_ARGUMENT, the same size cap --file already 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 --json now prints one error envelope (ERR_UNKNOWN, with the load error in the message) instead of printing nothing to stdout. Without --json the error is still printed to stderr, and the exit code is still 1 in both modes. (#6551)

  • astryx manifest without --json now labels each command's name name:, the same key the JSON manifest uses, instead of command:. (#6552)

  • astryx theme build --out now reports a path that leaves the working directory with ERR_PATH_TRAVERSAL, and an output directory it cannot create with ERR_WRITE_FAILED, instead of unregistered codes such as PATH_TRAVERSAL or EEXIST. The programmatic themeBuild() throws the same codes as an AstryxError. (#6543)

  • astryx theme palette generate now writes candidate JSON in the canonical form the astryx-oklch-v1 recipe pins, so a JSON candidate and the candidateSha256 in its receipt match the recipe's reference fixtures byte for byte. Before, the stops array was printed on one line, which changed the bytes and the digest of every JSON candidate. (#6531)

  • astryx theme palette generate and generateTonalPalette() now reject a neutralProfile the 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 generate now reports an output or preview path it cannot use, such as one below a regular file, with the stable ERR_WRITE_FAILED code. Before, the --json error envelope carried the raw system error name, such as ENOTDIR, as its code. (#6533)

  • --json output is one envelope again when astryx.config or an integration manifest prints while it loads. Anything a project module writes to stdout during its load now goes to stderr. (#6581)

  • A --json error envelope's code is now always one of the documented error codes. A failure that carried a Node.js system code, such as ENOTDIR or EACCES from a failed write, used to put that code in the envelope; it now reports ERR_UNKNOWN, and the original message is unchanged. (#6548)

  • The 0.6 rename-resizable-pixel-bounds upgrade codemod now also renames minSizePx/maxSizePx in static inline useResizable configurations called through a namespace import (Astryx.useResizable({...})) or wrapped in as const or satisfies. These were left unchanged before. (#6541)

  • astryx search now fails with ERR_CORE_NOT_FOUND, like component and hook, when @astryxdesign/core cannot be found, instead of the catch-all ERR_UNKNOWN. (#6525)

  • astryx search --limit now refuses a value that is not a positive integer, such as 1.5 or 5abc, with ERR_INVALID_ARGUMENT and exit 1, as search({limit}) already did, instead of silently truncating it. (#6528)

  • astryx search text output now prints every field its --json results carry: title for doc results and kind for template results were missing. The --verbose help now says what it adds: each result's score and match reason. (#6527)

  • search() from @astryxdesign/cli/api is now declared to return SearchResponse, as its docs say, so TypeScript sees each result's SearchResultEntry fields instead of a bare object. (#6529)

  • astryx swizzle no 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 with ERR_PATH_TRAVERSAL before it writes anything. (#6573)

  • The astryx swizzle --overwrite help 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 with ERR_FILE_EXISTS and nothing is written. (#6579)

  • astryx template <name> <dir> now refuses the write, with ERR_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 --overwrite followed the link and replaced the file it pointed at. (#6563)

  • astryx template <name> <path> and astryx layout expand now 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 add now 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 add no 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 with ERR_PATH_TRAVERSAL, and any other entry at that name fails the copy instead of being overwritten. (#6535)

  • astryx theme build and astryx theme palette generate now 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 --json receipt's notices and warnings. Generated theme files are unchanged. (#6544)

  • astryx theme build --help and the capability manifest now state each flag's default and which flag combinations are refused (--family with --out or --watch, --check with --watch, --watch with --json, --out with more than one file), with the error code the refusal returns. (#6546)

  • The themeBuild() reference and the theme.build response-type entry (and the CLI README table generated from it) now document every field of the receipt by name, including notices, the advisories (such as a font the theme names but does not load) that had no documentation. (#6545)

  • astryx theme template now reports a file it cannot write with the stable ERR_WRITE_FAILED code. Before, the --json error envelope carried the raw system error name, such as EEXIST or EISDIR, as its code. (#6532)

  • astryx upgrade --json now prints exactly one JSON envelope when a post-codemod hook prints output. Anything a hook's buildCommand writes goes to stderr, so stdout carries only the result. (#6536)

  • The published UpgradeListEntry type now declares optional, the boolean every astryx upgrade --list --json entry already carries, so typed callers can read it without a cast. (#6542)

  • astryx doctor integration validate, templates, components, and docs now show the invalid_package_json error when a local integration's package.json can't be parsed. Before, the text output said no astryx.integration.* file was found and hid the error, because the JSON reported a null name, which means no manifest. In that case data.name is now (local package). (#6559)

  • astryx docs authoring now says which docs-graph features are not built yet: the placement, aliases and audience fields, 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 validate now 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 extends another no longer renames it. astryx docs theme with 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 that replaces another still renames it. (#6498)

  • Use extensionless subpath specifiers for generated integration imports (#6288) integrationAddComponent and integrationAddTemplate now emit extensionless public import specifiers (@pkg/components/MyWidget instead of @pkg/components/MyWidget.tsx) and map them to source files through the package exports field. Consumer imports no longer expose the package's source extension or require allowImportingTsExtensions.

    integrationPackCheck now rejects public specifiers ending in .tsx or .ts and 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 --list text now renders each codemod from the JSON result (name, title, version, optional) instead of the API logger, with no (undefined) rows. theme targets prints one line per target via the formatter kit's inline layout and now shows className. docs list 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 on TonalPaletteAnchor, so the generated .d.ts documented the wrong type and left the candidate bare. TonalPaletteFamilyInput — the type an author writes by hand — had no property descriptions, and neutralProfile never said what its four values do. (#6168) [fix] Give the generation receipt a real type. generationReceipt was Record<string, unknown>; it is now TonalPaletteGenerationReceipt, with TonalPaletteRampDiagnostics, TonalPaletteCoordinationDiagnostics, and TonalPaletteNormalizedRequest beside 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 xds command name with astryx across 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/.js file 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, while upgrade applied 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 with components now loads, as the published ComponentDoc type allows. It used to fail with "props: expected array". Each entry must name its component; an entry without a name fails 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)

Documentation

  • astryx docs authoring now documents the DebugEvent a debug handler receives and the GapReportHandler contract, field by field. Both types were exported from @astryxdesign/cli/authoring with no section of their own. (#6498)
  • astryx docs authoring now 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's usage is optional on sub-component docs, a command option's default may also be a boolean or a list, and a codemod's type is 'code' or 'config'. (#6492)
  • Document CheckboxList's isReadOnly prop, and separate the select-all block example's rows with hasDividers instead of placing a Divider inside the options list (#6777).
  • The documented exit codes for astryx build now say that a query exits 1 when @astryxdesign/core cannot be found, while the playbook (astryx build with no query) needs no core. (#6592)
  • astryx discover --components is 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 --help and the manifest now say how init's flags interact: --remove-agents only removes the managed block and ignores the install flags, --all overrides --features, and --agent and --agent-docs-path apply only when agent docs are installed, with an explicit path taking precedence. The documented exit codes now include an --agent-docs-path outside the project (exit 1) and no longer list two template cases the CLI cannot reach. (#6589)
  • astryx theme add --overwrite and astryx upgrade --install-deps no longer describe a prompt the CLI never shows; each now says what happens without the flag (ERR_FILE_EXISTS with nothing written, or ERR_DEP_MISSING). ERR_FILE_EXISTS is described as "Refused to overwrite an existing file." without the "non-interactive mode" qualifier; its meaning is unchanged. (#6593)
  • The CLI README now shows apiVersion in every hand-written envelope shape and example, and lists astryx search --verbose in 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, and integration.pack-check responses by their JSON keys, including parentDoc, hint, notices, deprecatedFor, each gap-report delivery's fields, and the pack-check contribution identities and issue fields. The component.detail response type declares parentDoc. (#6588)
  • astryx template --help and the manifest now say which flags win when they are combined: --cdn overrides everything else and writes to its value, else to <path>, else cdn.template.html; --list ignores a name, a path, --skeleton and --overwrite; and --skeleton needs a name and writes nothing. (#6591)
  • astryx upgrade --help and the manifest now state how its flags combine: --list ignores every other flag, --registry refuses --list and the migration flags, and --codemod is the only way to run an optional codemod and also skips the ShadCN composition check. The --from help names the legacy @xds/core fallback, and the documented exit codes now include a missing core, a missing jscodeshift, an invalid astryx.config, a post-codemod hook failure and the refused flag combinations. (#6590)
  • astryx docs authoring now 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)

Other Changes

  • start names the template to scaffold, with the template <id> --type page <path> command that selects it (an integration replacement through the Core id it replaces), a basis, a one-line reason, and alternatives: 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. basis is direct when the ranker's pick is also search's direct match, closest when it is not, and fallback when nothing has the evidence to lead and the page starts from the shell-top-nav app shell (blank when 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. --verbose shows 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 --skeleton or component AppShell.

    The build playbook, the generated agent docs, and the working-with-ai and layout guides now start every page from a template.

    Compatibility: the build.kit JSON only gains start. Every existing field keeps its shape and meaning. Which pages, blocks, and components are listed, and directMatch, change with the stricter matching, as ranking results do. The human-readable output is reorganized. Options, exit codes, and the build.help shape are unchanged, and build still writes nothing. The same matching changes the ranking of search results.

  • Docs reads go through one internal compiler. astryx docs (and docs()), astryx doctor and astryx search read compiled topic nodes instead of each loading, merging, translating and linking doc files on its own. Output is unchanged. (#6484)

@astryxdesign/build

Fixes

  • 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.

@astryxdesign/theme-butter

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

@astryxdesign/theme-chocolate

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

@astryxdesign/theme-gothic

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

@astryxdesign/theme-matcha

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

@astryxdesign/theme-neutral

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

Documentation

  • Describe Neutral as Figtree typography and add the font-loading snippet; the README claimed system fonts while the theme declares Figtree. (#5991)

@astryxdesign/theme-stone

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

@astryxdesign/theme-y2k

New Features

  • Ship a typed ThemeDoc descriptor beside each first-party theme source. (#6498)

Contributors

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

17 days ago
astryx

v0.6.3

Astryx 0.6.3 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

New Features

  • 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 collapsedSummary composition slot to ChatComposerDrawer that replaces the complete collapsed-summary anatomy when provided; omitting it preserves the existing Badge and label. (#5399)

  • Add endContentEdgeCompensation to 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/plugins to 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's plugins prop to compose bounded source syntax, immutable typed AST transforms, and extension renderers. Import parseMarkdownAst() or parseInlineAst() from @astryxdesign/core/Markdown/parser when 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/remark to 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 plugin renderers own presentation and text projection. components.code retains precedence, and declined, missing, or failed proposals preserve Markdown's accessible, copyable CodeBlock fallback.

  • 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, and getMarkdownSourceDecorations() 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-pressed on 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) TreeListItemData gains xstyle, className, and style, applied to the item's row element, so row-scoped primitives such as useContainerReveal can isolate each row's hover and focus state.

Fixes

  • 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 shared contain policy, 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 ChatComposerDrawer content 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 let onPaste intercept text before default token conversion. (#6419)

  • Export ChatComposerTokenElementProps and forward supported span props and refs from ChatComposerTokenElement (#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 esc and return aliases with the same glyphs and accessible names as escape and enter (#5657) This partial fix for #5403 normalizes the two unambiguous aliases already accepted by useHotkeys. Kbd now renders esc as Esc with the accessible name Escape, and return as ↵ with the accessible name Enter. Its lookup tables now also avoid prototype-chain collisions when rendering arbitrary key names. The platform-specific rendering contract for meta and display choice for space remain 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 progressbar subtree (#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 clickAction pathway, 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-dashoffset on 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 isDisabled when its ToggleButtonGroup does not disable anything. The group always supplies an isDisabled boolean, 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 a tooltip stayed operable while looking unavailable. A disabled group still disables every member; it just cannot re-enable one. (#6356)

Contributors

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

25 days ago
astryx

Astryx 0.6.2

Astryx 0.6.2 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

New Features

  • Markdown: add an opt-in math renderer (#6312) Supply components.math to parse $…$ inline math and $$…$$ display math. The renderer receives the delimiter-free expression as value and its placement as display: '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 create IncrementalParseState<true> via createIncrementalState<true>(). Those overloads return InlineNodeWithMath or BlockNodeWithMath; default, legacy-set, math: false, and ParseOptions- annotated calls retain the existing InlineNode and BlockNode unions, 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-fraction public 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)

Fixes

  • 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 with Intl.ListFormat for 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 explicit aria-label and a visible string label still take precedence, in that order. (#6217)

@astryxdesign/cli

New Features

  • Add muse preset to astryx init --agent targeting AGENTS.md for Muse Code. (#6045)
  • Build one keyed artifact trio for a selected theme family (#6268)

Fixes

  • 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) checkPeerDeps resolved each peer with require.resolve(name, {paths: [cwd]}). Node folds NODE_PATH into that lookup regardless of paths, 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 own node_modules and reads each package.json off 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_modules for 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 ignores NODE_PATH, which keeps the answer project-local. A PnP project now gets its peers range-checked too — previously require.resolve could 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.mjs files (the shape integration add component writes), fixing a crash where component and search could not load generated docs. Human component detail and list views now use the API-resolved import specifier instead of recomputing from core, so integration components report their package-authored import. pack --check now reports an error when a component doc cannot be loaded instead of silently approving. (#6291)

  • Fix theme build emitting invalid JS identifiers for theme names containing hyphens or dots followed by digits. The output identifier is now derived deterministically from theme.name by camelCasing across - and . separators (underscores are preserved as valid identifier characters). Names like chaos-07 correctly produce chaos07Theme instead of the unparseable chaos-07Theme. (#6289)

  • Make staged writes portable across filesystems that reject hard links. The create-only publisher now falls back from linkSync to copyFileSync with COPYFILE_EXCL for EPERM and EXDEV, 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) patchCommonJs required @astryxdesign/core from the theme file to see whether it could wrap defineTheme for .cjs dependencies. require folds NODE_PATH in, and pnpm's isolated layout puts every package in node_modules/.pnpm/node_modules, so a core no dependency of the theme could reach answered that lookup. Wrapping it fails on a require(esm) namespace, and the reported coverage gap then rejected any theme whose lineage was unobserved. The lookup now walks the theme's own node_modules chain first, the same way doctor resolves peers.

Contributors

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

27 days ago
astryx

v0.6.1

Astryx 0.6.1 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

New Features

  • 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.

Fixes

  • 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 onPointerDown in InputClearButton (#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 --apply provides 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: isolate boundary on the field surface, so they cannot compete with page-level stacking; the AppShell header keeps its normal stacking level.
  • TextInput's onEnter no longer fires for the Enter that commits an IME conversion (Japanese/Chinese/Korean input); onKeyDown still receives the raw event. (#6082)

Documentation

  • AspectRatio: show the ratio prop in its JSX form (#6093) The best-practice line told readers to express the ratio as a fraction like 16/9 without showing it in JSX, and nothing else in the CLI output gives ratio an example. Rewrites it to ratio={16 / 9} and names the string form as a type error.

@astryxdesign/cli

New Features

  • Load an installed integration even when no astryx.config names it (#6202) A package the project declares as a dependency, and that ships a root astryx.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, devDependencies and optionalDependencies — and only by key. node_modules is 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 own name, so an aliased dependency reports the package it actually is, and two dependency keys naming one package load it once.

    An explicit astryx.config entry 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 doctor gains an implicit-integrations line 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: GapReportHandler replaces GapReportWriter; the handler receives a normalized GapReport event and returns a strict GapReportHandlerReceipt. Project config gains a gapReport field; the integration named export uses the same type.

  • Add integration authoring and packed-package verification. astryx integration add <kind> <name> and the per-kind integrationAddComponent, integrationAddDoc, integrationAddTemplate, integrationAddCodemod, integrationAddAgentDoc, and integrationAddTheme APIs write complete contributions. Existing component, docs, template, and theme commands see the package being authored without publishing it first. astryx integration pack --check proves 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, and theme add can copy one by owner.

Fixes

  • build: recommend template <name> --skeleton in 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 showed import {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-resolved import field 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 --apply provides the forward-compatible bare-selector migration. (#6126)
  • component and search now report the same import specifier for an integration component, resolved once in foundation/discovery/component-discovery.mjs. search previously 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)

@astryxdesign/theme-butter

Fixes

  • 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 --apply provides the forward-compatible bare-selector migration. (#6126)

@astryxdesign/theme-neutral

Fixes

  • 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 --apply provides the forward-compatible bare-selector migration. (#6126)

@astryxdesign/theme-stone

Fixes

  • 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 --apply provides the forward-compatible bare-selector migration. (#6126)

Contributors

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

2026-09-10 23:52:17
astryx

Astryx v0.6.0

Astryx 0.6.0 — all @astryxdesign/* packages ship at this version.

npx astryx upgrade --apply

@astryxdesign/core

Breaking Changes

  • Remove deprecated focus-direction overrides, the hooks-path isImeKeyEvent re-export, and Resizable pixel-bound aliases. Codemod: Run npx astryx upgrade --apply before updating to 0.6.0. It removes focus-hook isRtl, moves isImeKeyEvent imports to @astryxdesign/core/utils, and renames minSizePx/maxSizePx to minSize/maxSize.

  • Stop emitting deprecated bare prop and state classes such as .primary, .sm, and .checked. Components retain their stable astryx-* target classes and reflect visual props and runtime states through explicit data-* attributes; generated runtime and built theme CSS now uses that same selector contract. Run astryx upgrade --apply to migrate safely identifiable selectors in .css files 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-supplied className matches 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 .primary or .sm and 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 as variant:primary and checked do not change. If you prebuild a custom theme, rerun astryx theme build <theme-file> after upgrading and deploy the regenerated .css, .js, .d.ts, and optional .variants.d.ts artifacts 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:

    • themeProps returns the stable target class (plus target-name compatibility aliases), without bare prop/state classes; its reflected data-* attributes are unchanged.
    • parseStyleKey returns data-attribute selector suffixes instead of .value, .prop-N, or .state suffixes.
    • generateThemeRules keeps its array contract and ordering; non-base component selectors use reflected attributes.
    • generateThemeRulesSplit keeps {component, prose}; component selector bytes change and prose is unchanged.
    • generateOnMediaCSS keeps its scoped string contract; component selector bytes change.
    • generateThemeCSS keeps {prose, component} and the same layers/scopes; component inherits the new selectors and prose is unchanged.
  • Restrict Stepper's horizontalOptions.minimumStepWidth to a pixel number and remove compact-layout implementation fields from useStepperContext. Replace CSS-length thresholds such as '7rem' with their intended pixel number. Call registerStep(index, {getIsDisabled}) instead of passing a disabled boolean; the options object is optional. StepperContextValue keeps transition history and step registration, while step count, compact state, summary-portal coordination, and threshold measurement remain package-internal.

  • Add ordered environmental adaptations to defineTheme Themes 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.from is inclusive, width.below is 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.

    AppShell now accepts xl and 2xl for mobileNav.breakpoint and 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.

    defineTheme now 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 root components; a rule may not be the only place a custom value is enrolled, because generated type augmentation is unconditional.

    astryx theme build treats 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/core and emits the same CSS as before. A theme that does carry adaptation intent — valid rules, a custom widthBreakpoints map, or present-but-malformed adaptation metadata — fails against such a core before any output is written, with ERR_CORE_INCOMPATIBLE naming the missing generateAdaptationCSS export. A complete default width map with no rules asks for nothing and still builds. Where an older core's defineTheme drops adaptations while resolving, the build records each raw defineTheme() 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.

New Features

  • Banner exposes a banner-frame theme target on the visible frame that owns whole-banner elevation and the elevated-card silhouette. The target reflects container and elevation; existing Banner targets and default rendering are unchanged.
  • Collapsible and CollapsibleGroup accept chevronPosition="start" | "end". The default remains end, preserving the released trailing chevron. start moves 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 on CollapsibleGroup when direct items should share it. An individual Collapsible may 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 autoComplete prop to TextInput and TextArea, forwarded to the native control unchanged.

Fixes

  • ChatComposerInput: drop aria-multiline once triggers make the editable a combobox aria-multiline was hardcoded on the contenteditable element while useTriggerMenu owns its role, so configuring triggers switched the role to combobox — which ARIA 1.2 does not list aria-multiline under — and axe flagged aria-allowed-attr (critical) on the 8 ChatComposerInput trigger stories and the 2 ChatLayout stories that render one. Moves the attribute into the hook's ariaProps, where the role and the attributes whose validity depends on it are decided together.
  • CheckboxListItem: the visible description is 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-supplied aria-describedby with its own description, status, and disabled-reason ids rather than replacing it. No public API changes.
  • CheckboxListItem: a ReactNode label now names the checkbox from its visible text through aria-labelledby, the way RadioListItem already does, instead of falling back to the generic name "Checkbox". aria-label still replaces that name; a rich label with no text at all needs it, as it does for RadioListItem. The dev-time warning that asked for aria-label on 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 contentWidth is set. Without panels, LayoutContent spans the available Layout width and aligns its children to contentWidth internally. With exactly one panel, the panel stays aligned to the contentWidth frame 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-label no longer gets overwritten by the collapsed-rail fallback, in both the icon-only and popover-trigger paths.

@astryxdesign/cli

Breaking Changes

  • Add ordered environmental adaptations to defineTheme Themes 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.from is inclusive, width.below is 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.

    AppShell now accepts xl and 2xl for mobileNav.breakpoint and 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.

    defineTheme now 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 root components; a rule may not be the only place a custom value is enrolled, because generated type augmentation is unconditional.

    astryx theme build treats 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/core and emits the same CSS as before. A theme that does carry adaptation intent — valid rules, a custom widthBreakpoints map, or present-but-malformed adaptation metadata — fails against such a core before any output is written, with ERR_CORE_INCOMPATIBLE naming the missing generateAdaptationCSS export. A complete default width map with no rules asks for nothing and still builds. Where an older core's defineTheme drops adaptations while resolving, the build records each raw defineTheme() 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.

New Features

  • 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.black and neutralPalettes.white for 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, so resultCount, emptyResult, resultKind, and directMatch are now populated for component, docs, hook, template, theme list/add/targets, discover, blog, swizzle --list, upgrade --list, layout grammar, and manifest, not just search and build. resultKind gains theme, integration, migration, command, and none; 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 now schemaVersion: 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 integration checks 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 upgrade transforms for the Core 0.6 deprecated-API removals: focus direction overrides, the hooks-path IME helper import, and Resizable pixel-bound aliases.

Fixes

  • Prevented removed Resizable bounds from being silently ignored and kept ambiguous spread migrations behavior-preserving (#6124)
  • Add a conservative astryx upgrade --apply migration for the Core bare selector-class removal. The transform parses .css selector 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 @path agent 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)

Documentation

  • 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-specifier affect resolution.

@astryxdesign/build

Fixes

  • withAstryx() refuses a Turbopack config instead of building an unstyled app. Every alias the helper installs lives in nextConfig.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)

@astryxdesign/theme-butter

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

@astryxdesign/theme-chocolate

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

@astryxdesign/theme-gothic

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

@astryxdesign/theme-matcha

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

@astryxdesign/theme-neutral

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

New Features

  • 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.black and neutralPalettes.white for use in semantic theme tokens.

Fixes

  • Align Neutral light-mode foreground colors to darker palette stops.

@astryxdesign/theme-stone

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

@astryxdesign/theme-y2k

Breaking Changes

  • Requires @astryxdesign/core@0.6.0 as part of the coordinated stable release. Upgrade Core and this theme together.

Contributors

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

2026-09-07 16:11:56
astryx

Astryx v0.5.4

[!WARNING] Stepper context compatibility: v0.5.3 changed the package-exported StepperContextValue / useStepperContext shape, 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 construct StepperContextValue should 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

Fixes

  • 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 and docsite

  • 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)

Contributors

Thanks to @Kyujenius and @cixzhang.

Full Changelog: https://github.com/facebook/astryx/compare/v0.5.3...v0.5.4