3 hours ago
astryx

Astryx v0.7.0

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

This release makes forms easier to style, improves phone typography and touch menus, keeps Markdown transforms composable, and moves spacing to rem units.

Upgrading to 0.7.0

Start here:

  1. Pin Core and the CLI to 0.7.0, plus every other stable Astryx package your project uses.

    OLD_VERSION=0.6.8 # replace with the Core version your project uses today
    npm i @astryxdesign/core@0.7.0 @astryxdesign/cli@0.7.0 @astryxdesign/build@0.7.0 @astryxdesign/theme-neutral@0.7.0

    The example includes Neutral; update every other stable Astryx theme in your project in the same install step.

  2. Run the 0.7.0 upgrade. It rewrites supported callsites, validates data-color ownership, and leaves a TODO where it cannot choose safely.

    npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .
  3. Check the items below. Four need a manual code edit, ten affect CLI commands or scripts, four are visual or token changes to inspect in your app, and eight are handled or checked by the upgrade.

Biggest risk: props that used to be ignored now reach the DOM. Layout, MetadataList, NavHeadingMenuItem, and TopNavMegaMenuItem already accepted BaseProps, so there is no type error or codemod. Audit callsites that pass IDs, ARIA or data attributes, event handlers, or style props only if they relied on those props doing nothing.

Manual code edits — no codemod (4)

  • Banner announcements are no longer inferred from status → call useAnnounce() from the async transition, or add an explicit persistent role.
  • Previously ignored BaseProps now apply → audit the four components in the risk callout only where a passed prop was meant to stay inert.
  • Complete icon registries require upload → add an upload entry and artwork to every custom complete IconRegistry.
  • Standalone empty-operator filters no longer save on open → save at the callsite when opening should commit the filter, or skip opening it.

CLI commands and scripts — no codemod (10)

  • theme add now imports built themes → change source-copying scripts to theme eject; existing theme add --import calls need no change.

    - npx @astryxdesign/cli theme add neutral ./src/theme --overwrite
    + npx @astryxdesign/cli theme eject neutral ./src/theme --overwrite
  • init --remove-agents reports removed: false → treat data.removed as a boolean, not literal true.

  • Conflicting CLI options are refused → remove the formerly ignored option from affected command pairs.

  • Invalid CLI input now fails → pass valid types, packages, limits, and arguments, and handle ERR_AMBIGUOUS_COMPONENT.

  • Empty positional arguments are rejected → omit "" when the intended operation is to list entries.

  • Integration validation needs a package → run it where package.json exists, or handle exit 1.

  • Astryx-prefixed agent variables are removed → use AGENT and AGENT_SESSION_ID; agent metadata has no replacement.

  • The layout command group is removed → use astryx build, astryx template, or astryx docs layout.

  • Private theme variables fail the build → fix every reported private --_* input instead of relying on exit 0.

  • Doctor fails on unloadable configured projects → install or repair the named integration, remove it from config, or fix the reported field.

Visual and token changes — inspect your app; usually no code (4)

  • reset.css now reaches pseudo-elements and search fields → do nothing unless you need the old browser result; then add a later-layer override.
  • Phone typography starts at 16px in four themes → take the new Neutral, Chocolate, Butter, and Stone scale, or add a touch-and-narrow override to keep 14px.
  • Core spacing uses rem → CSS variable users do nothing; code that parses px must convert with the root size or read the computed length.
  • Matcha and Y2K spacing uses rem → take the pixel-equal default at a 16px root, or override spacing tokens in px for fixed sizing.

Handled or checked by astryx upgrade (8)

  • Data-color defaults move to StyleX → fix anything validate-data-token-ownership reports, then rebuild committed theme CSS.
  • Date and time input styling targets the control → review the migrate-field-input-styling rewrite or TODO.
  • Typeahead, Tokenizer, CheckboxList, RadioList, and Slider styling targets the field → review the generated container* or control* prop.
  • Selector styling targets the control → review the rewritten control styling and forwarded trigger props.
  • Text input styling targets the control → review the rewritten control styling and native input ID.
  • Theme styling uses current names and data attributes → review rewritten keys and selectors, especially any TODO for custom CSS.
  • BottomSheet adds default content padding → review the generated padding={0} or TODO; remove the prop when your theme already supplies padding.
  • emptySearchResultsText is removed → review the rename to emptySearchText and any TODO on spreads or trigger objects.

See the complete visual guide and all 26 detailed migrations in #7333; its public URL goes live after npm and the GitHub Release.

@astryxdesign/core

Breaking Changes

  • Stop deriving live-region roles from Banner.status. Banners are non-live by default; call useAnnounce from the actual async transition handler, or provide an explicit role="status" for a persistent semantic mirror. (#6630) role="alert" is now opt-in rather than automatic for warning and error banners.

    Old usage: Banner derived role="status" or role="alert" from its status variant.

    Replacement: The caller sets the role explicitly, or announces with useAnnounce.

    Migration: Call useAnnounce from the actual async transition handler, or provide an explicit role="status" for a persistent semantic mirror.

    Codemod: None — the right announcement depends on the callsite.

  • Layout, MetadataList, NavHeadingMenuItem and TopNavMegaMenuItem now apply the BaseProps they accept (id, role, aria-*, data-*, hidden, inert, dir, event handlers; TopNavMegaMenuItem also xstyle/className/style) instead of silently dropping them; NavHeadingMenu keeps arrow navigation when an item handler cancels. (#7331) Migration: remove any of those props you passed that you relied on being ignored; a TopNavMegaMenuItem styled through xstyle/className/style now shows that styling.

    From #5656 by @harjothkhara.

  • BottomSheet pads its content by default, like Dialog. With no padding prop and no theme padding, the content box now pads by --spacing-4 (it had no padding before) and publishes that inset. (#6884) A sheet whose only child is a padded Section or a Layout renders as before, because both escape the inset. Content that supplies its own inset is now padded twice: drop that inset, or pass padding={0} to keep the unpadded content box. astryx upgrade ships preserve-bottom-sheet-content-padding, which adds padding={0} where needed.

    Migration: pass padding={0} to keep the unpadded content box, or drop your own inset to take the default; astryx upgrade adds padding={0} to every BottomSheet that sets no padding unless its only child is a padded Section or a Layout. An app whose theme already sets bottom-sheet padding on every edge is not affected by the new default; the codemod cannot see the theme, so remove the padding={0} it adds there to keep the theme inset. A sheet whose content wraps a Layout gets a TODO: padding={0} also removes the inset between that Layout's regions, which the Layout's own padding does not restore.

  • Require upload on complete IconRegistry types (#7329) Old usage: A custom complete icon registry typed as IconRegistry that omitted the upload key.

    Replacement: IconRegistry now requires all semantic IconName keys, including upload.

    Migration: Add an upload entry drawn in your registry's icon style to complete icon registries typed as IconRegistry.

    Codemod: No astryx upgrade codemod applies because icon artwork is bespoke design artwork tailored to each theme and cannot be automatically synthesized.

  • Move canonical data-color defaults from generated theme CSS to the public StyleX dataVars group. (#6834) Import dataVars from @astryxdesign/core/theme/dataTokens.stylex for CSS-capable code. Canvas and other non-CSS APIs continue to use resolveThemeToken() or useTheme().token(). Rebuild committed theme CSS with astryx theme build; concrete themes now emit only explicitly authored data-color overrides.

    Old usage: Generated theme CSS owned the canonical --color-data-* root defaults.

    Replacement: The public StyleX dataVars group in @astryxdesign/core/theme/dataTokens.stylex owns the defaults; concrete themes emit only explicitly authored data-color overrides.

    Migration: Import dataVars from @astryxdesign/core/theme/dataTokens.stylex for CSS-capable code. Canvas and other non-CSS APIs continue to use resolveThemeToken() or useTheme().token(). Rebuild committed theme CSS with astryx theme build.

    Codemod: astryx upgrade runs validate-data-token-ownership, which rewrites nothing and reports fallback-less var(--color-data-*) uses (whole or embedded source strings, and stylesheets) and stale generated theme CSS for manual migration.

    Also released in @astryxdesign/cli.

  • DateInput, DateRangeInput, DateTimeInput, and TimeInput no longer accept root xstyle, className, or style. Style the field with containerXstyle/containerClassName/containerStyle (the Field box; no effect inside InputGroup or a horizontal-labels FormLayout) and the visual control box with controlXstyle/controlClassName/controlStyle. Base props such as ref, id, data-*, aria-*, and DOM handlers reach the field's semantic control on every surface (the date, time, or date-time input; DateRangeInput's range trigger) instead of the wrapper, TimeInput no longer drops them, and a caller id is now honored there. A caller aria-describedby is added to the component's own description ids, and a caller aria-labelledby is read after the field's own label (on DateRangeInput, after its label and selected range); a caller aria-label on DateRangeInput replaces the trigger's name. (#7337) Migration: rename root xstyle/className/style on these components to controlXstyle/controlClassName/controlStyle, which style the same element as before (for DateTimeInput, the row holding the date and time segments). Rename by hand, or run astryx upgrade; it runs migrate-field-input-styling, which rewrites JSX usage; styling passed through a wrapper's own props or a spread is never rewritten and now fails type-checking at that callsite, so rename it there. Untyped callers get a development warning and the value is ignored. A DOM handler or data-* attribute that relied on landing on the wrapper now lands on the control; a handler still runs after the component's own and still receives events the component handled.

    From #5656 by @harjothkhara.

  • Typeahead, Tokenizer, CheckboxList, RadioList, and Slider no longer accept root xstyle, className, or style. Style the field with containerXstyle/containerClassName/containerStyle (the Field box; no effect inside InputGroup or a horizontal-labels FormLayout) and the visual control box with controlXstyle/controlClassName/controlStyle. Base props now reach the primary semantic control: the combobox input for Typeahead and Tokenizer, the role="group"/role="radiogroup" element for CheckboxList and RadioList, and the role="slider" thumb for Slider (the role="group" control in range mode). A caller aria-describedby is kept alongside the field's own description and status, on these components and on TextInput, TextArea, and NumberInput, which previously dropped it. CLI templates use the new props. (#7339) Migration: rename root xstyle/className/style to the part they styled before: containerXstyle/containerClassName/containerStyle on all five components, except a Typeahead inside InputGroup, whose root styling painted the control surface and moves to controlXstyle/controlClassName/controlStyle. The release's astryx upgrade codemod, migrate-field-input-styling, performs these renames on JSX usage.

    Not rewritten by the codemod, so update by hand:

    • ref and id, plus data-* (except Typeahead and Tokenizer data-testid, which marked the control surface), aria-*, and DOM event handlers such as onClick, previously landed on the outer Field (or were dropped by RadioList and Slider). They now land on the primary semantic control. Update code that measured, queried, or listened on the outer field, and wrap the component in your own element when you need an outer boundary.
    • Typeahead and Tokenizer refs are now HTMLInputElement.
    • A consumer onKeyDown on Typeahead or Tokenizer still runs after the built-in key handling, cannot cancel it, and still fires for keys on the clear button and tokens; for keys on the input it now runs on the combobox input itself.
    • RadioList and Slider used to drop DOM event handlers; they now run after the component's own handling, so a Slider onPointerDown, onKeyDown, onFocus, or onBlur that never fired before now fires (on the thumb, or on the group in range mode).
    • Tokenizer's onFocus/onBlur keep their meaning: focus entering or leaving the field.
    • Styling passed through a wrapper's own props or a spread is not rewritten and now fails type-checking at that callsite.

    Untyped callers get a development warning and the value is ignored.

    From #5656 by @harjothkhara.

    Also released in @astryxdesign/cli.

  • Selector, MultiSelector, and ComplexSelector no longer accept root xstyle, className, or style. Style the field with containerXstyle/containerClassName/containerStyle (the Field box; no effect inside InputGroup, a horizontal-labels FormLayout, or with renderTrigger) and the visual control box with controlXstyle/controlClassName/controlStyle. Base props such as ref, id, data-* (including data-testid), aria-*, and DOM handlers reach the trigger button that carries the combobox role (or, with renderTrigger, the caller's control), and a caller aria-describedby is kept alongside the field's own description and status instead of being replaced. Handlers keep their existing order: a ComplexSelector onClick on its own trigger can still cancel opening, and newly forwarded handlers (including those reaching a renderTrigger control) run after the built-in toggle and keyboard handling. CLI templates use the new props. (#7341) Migration: rename root xstyle/className/style on these components to controlXstyle/controlClassName/controlStyle, which style the same element as before. Not a rename: a data-testid or other base prop now lands on the trigger button rather than the bordered box around it (query the box with .closest() if a test measured it), and with renderTrigger base props and ref now reach the caller's control. Rename JSX usage by hand, or run astryx upgrade; it runs migrate-field-input-styling, which does not follow styling passed through a wrapper's own props or a spread, which now fails type-checking at that callsite, so rename it there. Untyped callers get a development warning and the value is ignored.

    From #5656 by @harjothkhara.

    Also released in @astryxdesign/cli.

  • TextInput, TextArea, NumberInput, and FileInput no longer accept root xstyle, className, or style. Style the field with containerXstyle/containerClassName/containerStyle (the Field box; no effect inside InputGroup or a horizontal-labels FormLayout) and the visual control box with controlXstyle/controlClassName/controlStyle. Base props such as ref, id, data-*, aria-*, and DOM handlers reach the native control, and a caller id is now honored there. CLI templates use the new props. (#7336) Migration: rename root xstyle/className/style on these components to controlXstyle/controlClassName/controlStyle, which style the same element as before. astryx upgrade rewrites JSX usage; styling passed through a wrapper's own props or a spread is not rewritten and now fails type-checking at that callsite, so rename it there. Untyped callers get a development warning and the value is ignored. Not rewritten by the codemod: a caller id used to be replaced by a generated one and now reaches the native control (the label stays associated), so remove an id you passed but relied on being ignored. Other base props already reached the native control and are unchanged.

    From #5656 by @harjothkhara.

    Also released in @astryxdesign/cli.

  • PowerSearchFilterEditor no longer calls onSave when it opens on an already-complete empty-operator filter (such as "is empty"). (#7347) Old usage: opening a standalone PowerSearchFilterEditor on a complete empty-operator filter saved that filter immediately, with no Apply rendered for it, and saved it again whenever the editor remounted.

    Replacement: the editor saves only from a user choice in it. Opening it saves nothing.

    Migration: if you relied on that save-on-open, call your own onSave handler with the filter at the point where you open the editor (or skip opening the editor for empty-operator filters, as PowerSearch now does). This cannot be rewritten by a codemod, because the right place to commit depends on your code.

  • Remove the deprecated component-theming compatibility surface scheduled for 0.7.0. Components now emit only canonical target classes and reflected data-* prop/state selectors. Theme discovery and build output no longer expose deprecated targets, and ThemePropsOptions plus ComponentThemingTarget.deprecatedFor are removed. The theme targets --json response drops the ThemeTargetEntry.deprecatedFor field, because no target it lists is deprecated. (#7302) Migration: run the required codemod migrate-deprecated-theme-surface with astryx upgrade --from <your 0.6 version> --apply --path . after upgrading.

    The codemod renames removed target keys inside theme components maps (an imported defineTheme() call, an object typed as an Astryx theme, or a theme object with a static name), renames target classes in CSS, and replaces target-qualified bare prop/value/state classes with reflected data-* selectors, using every bare class the final 0.6 release emitted. It leaves dynamic/computed theme keys, unqualified or unknown classes, and CSS embedded in JavaScript/TypeScript strings for manual review. It adds TODO(astryx upgrade) when old and canonical keys coexist, and when a bare class Astryx once emitted has no known meaning on the target it qualifies.

    The four per-component clear-icon aliases all become the shared input-clear-icon, which styles every input's clear icon. In CSS the codemod keeps each rule on its component with :where(.astryx-<component> *), which adds no specificity. Theme keys cannot carry that scope, so the codemod adds a TODO naming the CSS selector to use.

    Code that passed {legacyNames} to themeProps() or set deprecatedFor in a component doc should delete that argument or field; the canonical target is unchanged.

    Rename theme keys and CSS target classes with this complete mapping:

    • base-table → table
    • checkbox → checkbox-indicator
    • codeblock → code-block
    • codeblock-copy-button → code-block-copy-button
    • codeblock-header → code-block-header
    • codeblock-title → code-block-title
    • date-input-clear-icon → input-clear-icon
    • date-range-input-clear-icon → input-clear-icon
    • hovercard → hover-card
    • multi-selector-clear-icon → input-clear-icon
    • navicon → nav-icon
    • popover-surface → popover
    • progressbar → progress-bar
    • progressbar-fill → progress-bar-fill
    • progressbar-mark → progress-bar-mark
    • progressbar-track → progress-bar-track
    • radio → radio-indicator
    • radio-dot → radio-indicator-dot
    • selector-clear-icon → input-clear-icon
    • statusdot → status-dot
    • textarea → text-area

    For custom CSS, replace removed bare selectors with the corresponding reflected attribute on a canonical target—for example, .astryx-button.primary.sm becomes .astryx-button[data-variant="primary"][data-size="sm"], and .astryx-switch.checked becomes .astryx-switch[data-checked="checked"]. Review unqualified classes such as .primary manually because they may be application-owned.

    Also released in @astryxdesign/cli.

  • Remove the deprecated emptySearchResultsText prop. Use emptySearchText. (#7289) Old usage: emptySearchResultsText on Tokenizer, Typeahead, and BaseTypeahead, and the emptySearchResultsText key on a ChatComposerInput trigger.

    Replacement: emptySearchText, shipped in 0.6.6. It takes a ReactNode, so every old string value stays valid. The old name is no longer in the types and is no longer read; a JavaScript caller that still passes it sees the default empty message.

    Migration: Rename emptySearchResultsText to emptySearchText. Where both are set, keep emptySearchText and delete the old one — unless emptySearchText could be null or undefined: 0.6.x then fell back to the old value, so move that value into emptySearchText (for example emptySearchText={value ?? 'Old copy'}).

    Codemod: astryx upgrade runs rename-empty-search-results-text, a straight rename at the JSX boundary: the prop on imported Tokenizer, Typeahead, and BaseTypeahead elements, and the key on trigger objects written inline in ChatComposerInput's triggers, each renamed as written. Like every Astryx codemod it does not follow variables, helpers, spreads, wrappers, or types; where it cannot attribute the value — emptySearchText already set, any spread onto one of these elements (whether or not the element writes the old name), a triggers value not written inline, a trigger object with a spread or computed key (whether or not it writes the old name) — it leaves a TODO instead of choosing a winner. The type checker reports any literal emptySearchResultsText it did not reach; the TODOs cover the values that reach an element through a spread, which it cannot.

  • reset.css now reaches ::before, ::after, ::placeholder and the search field's cancel button, as its comments document. Three of its rules wrote the pseudo-element inside :where(), where pseudo-elements are invalid, so browsers dropped those parts of the selector and the rules never applied to them. Each pseudo-element now follows the :where(), as in :where(*)::before, and the rules stay in @layer reset, so any later layer or unlayered style still overrides them. (#7355) What changes on a page that loads the reset: a ::before or ::after with padding or a border used to size as content-box and now sizes as border-box; setting only border-width on one used to draw nothing and now draws a solid currentColor border; a placeholder with no color of its own used to show the browser's grey and now shows --color-text-secondary (or that token's own default, light-dark(#4E606F, #AAAFB5), when no Astryx CSS defines it); and a type="search" field no longer shows the browser's cancel button.

    To keep the old result for one of your own elements, set it in your own CSS: box-sizing: content-box or border-style: none on the pseudo-element, a color on the ::placeholder, or appearance: auto on the field's ::-webkit-search-cancel-button. There is no codemod: whether a stylesheet relied on the old result cannot be read from its source.

  • Spacing tokens are rem (--spacing-2: 0.5rem), so gaps and padding follow the reader's root font size the way the type scale does. Every token value is unchanged at the default 16px root. (#7359) One change shows at the default root: the useTableRowExpansion chevron now sits centered in its fixed 40px column instead of running past its edge (its button moves from 24–48px to 8–32px of the column). Under a theme with a larger spacing scale, such as matcha, the chevron is visible again and the detail panel lines up with the first column instead of starting 20px past it.

    Old usage: The --spacing-* defaults were fixed px (--spacing-2: 8px), so useTheme().token(), resolveThemeToken(), tokenDefaults and spacingDefaults returned px strings, and spacing stayed the same size when a reader raised the browser font size.

    Replacement: The defaults are rem with the same value at a 16px root (--spacing-2: 0.5rem, --spacing-4: 1rem), and those readers return the rem string.

    Migration: CSS that reads var(--spacing-*) needs no change. Code that parses a spacing token as a px number converts the rem value with the root font size, or reads the element's computed length. A theme that wants fixed px spacing sets the tokens back, for example defineTheme({tokens: {'--spacing-2': '8px'}}).

    Codemod: None. A token read and parsed at runtime cannot be found and rewritten statically, and CSS uses need no rewrite.

    Also released in @astryxdesign/cli.

New Features

  • DropdownMenu takes header and footer: content above and below the rows that sits in the popover beside the menu, never inside it. This is the place for a filter field or a "Create" button. The menu still holds only its rows, so it stays a valid menu to a screen reader. The controls keep their own keys and presses, and Tab walks header, menu, footer. The bottom sheet shows them above and below the actions. (#7344)

  • DropdownMenuSubMenu passes host attributes, DOM event handlers and a ref to its trigger row, as DropdownMenuItem does: a sub-menu row can carry aria-keyshortcuts, a data-* mark or an id, and a caller can reach the row element. The row keeps its own role, tab stop and popup state; a caller's onClick, onKeyDown, onPointerMove, onMouseEnter and onMouseLeave run after the row's own handling, except for an event the row consumes: the keys that open the flyout (the open arrow, Enter and Space) and a click on a disabled row. A caller's id names the row the flyout is labelled by. (#7318)

Fixes

  • A standalone BottomSheet now joins the shared layer dismissal stack, so one Escape press closes only the top-most layer. A Popover, Dialog, or menu opened inside the sheet closes first, and the next press closes the sheet. purpose="required" still blocks Escape without letting the press close a layer behind it, a sheet that is animating out keeps the press, and the native close request answers with the same top-most and IME composition rules. (#7225)

  • ContextMenu puts only its four touch handlers on the trigger element. It used to spread everything useLongPress returns, which since the press model includes the hook's imperative start, moveTo and cancel, so React warned "does not recognize the moveTo prop" and "Invalid values for props start, cancel" on the first ContextMenu of a page. Long-press opening is unchanged. (#7322)

  • DateInput: run changeAction with optimistic busy feedback on the native picker surface (#7243)

  • Dialog's initial focus and the focus it returns on close now follow the last input: the shared focus indicator is hidden after a mouse click or tap and shown after keyboard input, including focus returned after a keyboard open and a mouse close (#7306). Text fields keep their own focus treatment. (#7313)

  • A DropdownMenu hung off its own control through renderTrigger names a drilled-in sub-menu's Back row after the menu: "Back to <the menu's aria-label>", or the name of the control it hangs off, instead of "Back to Menu". (#7304)

  • Lightbox shows the next or previous image unzoomed from its first frame. The zoom and pan reset when the image changes now happens in the same update, instead of painting the new image at the previous image's zoom for one frame. (#7351)

  • With markdownSourceLinesPlugin installed, a transform plugin no longer falls back to its readable fallback on documents that contain a list or a quote. Blocks nested in a list item or a quote carry lines without offsets, and the transform check now accepts those Core-assigned lines while still refusing a transform that adds, shifts, or forges them. (#7345)

  • A two-finger pinch over an open DropdownMenu, sub-menu flyout, ContextMenu or Selector list now zooms the page. The menus declared touch-action: none while their rows fit and pan-y once they scroll, and neither allows a pinch, so a phone could not zoom the page while a menu was open. They now declare pinch-zoom and pan-y pinch-zoom; a one-finger slide over the rows and the press model behave as before. (#7201)

  • DropdownMenu, ContextMenu and sub-menus no longer special-case text fields and buttons placed among their rows. This reverts an unreleased change. A menu holds only menu items; a control that is not a row belongs outside the menu. (#7338)

  • The PowerSearch filter editor saves an "empty" operator filter (such as "is empty") exactly once, from the choice that completes it. Choosing such an operator, or switching to a field whose default operator is one, now saves from that choice instead of from an effect. The filter is no longer saved again when the parent re-renders with a new onSave, or twice in React StrictMode. Re-choosing the current operator or field saves nothing, and focus still returns to the search input after the editor closes. Clicking an existing empty-operator token in PowerSearch commits it once without opening the built-in editor; a components.empty.Editor override still opens. (#7347)

  • The PowerSearch filter editor saves once when a value that saves on choice is picked (an enum option, a relative-date preset, a suggestion). The save no longer runs inside a React state updater, which React may call more than once, so it no longer fires twice in StrictMode. (#7350)

  • Toasts stay visible and clickable while a Dialog is open. The ToastViewport from LayerProvider (or the one useToast mounts on its own) now moves into the open Dialog, and back when it closes. Before, a Dialog opened after the viewport mounted painted over it, and clicking a toast's close button hit the backdrop and closed the Dialog. Moving the viewport does not reset visible toasts or their timers. A viewport rendered with isTopLayer={false} keeps rendering in place. (#7238)

  • DateTimeInput's touch picker no longer paints a stale frame. Clearing the value from outside clears the time display in the same update, instead of showing the previous time for one frame, and narrowing max (or raising min) while the sheet is open keeps the visible month in range in the same update, instead of showing the out-of-range month and then jumping. (#7349)

Performance

  • An Item without swipeActions no longer bundles the swipe gesture. The panels and the drag that uncovers them now live in a module Item loads on demand the first time a row has swipe actions, so an app whose rows never swipe ships about 2.7 KB (gzip) less on its first load and a row that does swipe keeps its DOM, props and behavior; its gesture becomes live once that module has arrived, which on a first load is a moment after the row paints. (#7296)

Documentation

  • Describe CommandPaletteList as the list CommandPalette renders itself, and as a component to render directly only for a standalone results list. (#7370)

@astryxdesign/cli

Breaking Changes

  • theme add now imports built themes; use theme eject to copy theme source. (#7138) The old contract copied theme source with theme add <slug> [path] [--overwrite] [--package <package>] and returned theme.add JSON. The ThemeAddResponse type and the theme.add response type are removed, with their ThemeAddResponse.type, ThemeAddResponse.data, and ThemeAddResponse.meta fields. The replacement imports the built theme with theme add <slug> [--package <package>] and returns theme.app; --import remains an accepted no-op. Plain theme add now returns theme.app with the owner package directly after type. Local themes carry no package.

    A path after theme add now exits 1 with ERR_INVALID_ARGUMENT, and --overwrite exits 1 with ERR_INVALID_OPTION; neither writes files. To migrate a script that copied source, run theme eject with the same slug, path, --overwrite, and --package arguments. A script that already runs theme add --import keeps the same meaning and needs no change.

  • init --remove-agents says when there was nothing to remove. (#6623) Migration: a consumer reading data.removed as a boolean keeps working; a consumer that narrowed on the literal true must accept the corrected value.

  • Four commands refuse silently ignored option pairs. (#7132) Migration: remove the option the command was ignoring.

  • Refuse invalid input that used to silently succeed. (#7149) Migration: pass a valid type or package, an integer limit, or the documented arguments, and catch ERR_AMBIGUOUS_COMPONENT for a multi-package name.

  • An empty-string argument no longer discards the rest of the command. (#6620) Migration: remove the empty string from the invocation; omit the argument to list.

  • doctor integration validate exits 1 when there is no package.json. (#7150) A missing package.json now produces a no_package error, so the existing error-severity exit rule applies. A directory with package.json but no integration manifest still exits 0.

    Migration: run the command in a directory that has a package.json, or handle exit 1 for that case.

  • Doctor fails when the CLI cannot load what astryx.config asks for (#7130) A new configured-integrations check fails when an integration named in astryx.config cannot be loaded: not installed, installed without a manifest, or a manifest that throws or fails its schema. Such an integration is withdrawn whole and contributes nothing. The config check fails, instead of warning, when astryx.config imports but the CLI cannot load the project from it.

    Unchanged: a problem inside one contribution kind (an invalid component doc, a missing or unreadable root, or an unresolved replacement) stays a warning under integration-issues, and an autolinked dependency that cannot be loaded stays informational.

    No codemod applies: nothing in a project's source changes; install the integration, repair its manifest, or edit integrations in astryx.config.

    What counts as configured is every name astryx.config lists, including a package set aside for a provider conflict and the package being authored when its own config names it. Migration: install the integration, fix its manifest, or remove it from integrations; fix the astryx.config field the message names.

  • Stop reading the Astryx-owned agent variables ASTRYX_AGENT_ID, ASTRYX_AGENT_SESSION_ID, and ASTRYX_AGENT_METADATA (#7127) The theme-import change already stopped reading ASTRYX_THEME; these three were the last Astryx-owned agent variables. A test now fails on any process.env.ASTRYX_* read in the CLI source.

    Migration: set AGENT and AGENT_SESSION_ID, which the CLI already reads, instead of the Astryx-prefixed variants. ASTRYX_AGENT_METADATA has no replacement variable.

  • Remove the astryx layout command group. (#7137) Removes the deprecated astryx layout command group (expand, check, grammar), its three response types (layout.expand, layout.check, layout.grammar), and the programmatic API exports (layoutExpand, layoutCheck, layoutGrammar).

    Use astryx build to choose the template to start from, astryx template to scaffold it, and astryx docs layout for layout guidance. The layout guide is unaffected. The error codes ERR_LAYOUT_PARSE and ERR_LAYOUT_INVALID are retained.

    Removed JSON ids: LayoutBlockReference.mode LayoutBlockReference.name LayoutCheckOptions.cwd LayoutCheckOptions.form LayoutCheckOptions.loose LayoutCheckResponse.data LayoutCheckResponse.data.compact LayoutCheckResponse.data.errors LayoutCheckResponse.data.form LayoutCheckResponse.data.outline LayoutCheckResponse.data.valid LayoutCheckResponse.data.warnings LayoutCheckResponse.type LayoutExpandOptions.cwd LayoutExpandOptions.form LayoutExpandOptions.loose LayoutExpandOptions.name LayoutExpandOptions.targetPath LayoutExpandResponse.data LayoutExpandResponse.data.blocksReferenced LayoutExpandResponse.data.code LayoutExpandResponse.data.componentsUsed LayoutExpandResponse.data.demoMediaReplaced LayoutExpandResponse.data.form LayoutExpandResponse.data.states LayoutExpandResponse.data.todos LayoutExpandResponse.data.warnings LayoutExpandResponse.data.written LayoutExpandResponse.type LayoutGrammarOptions.cwd LayoutGrammarResponse.data LayoutGrammarResponse.data.aliases LayoutGrammarResponse.data.text LayoutGrammarResponse.type LayoutIssue.col LayoutIssue.formatted LayoutIssue.line LayoutIssue.message LayoutIssue.suggestions

  • theme build exits 1 when it reports private --_* variables. (#6872) The old contract reported private-variable errors but exited 0. The corrected contract keeps the same warnings, receipts, generated files, and --check output, but exits 1.

    To migrate, a CI step that relied on exit 0 must fix the theme's private --_* inputs.

New Features

  • astryx upgrade adds padding={0} to BottomSheet elements that do not set padding, keeping their unpadded content box now that BottomSheet pads like Dialog. Sheets whose only child is a Section or a Layout are left unchanged. A sheet with a Layout anywhere in its content also gets a TODO comment, because padding={0} removes the inset between that Layout's regions and the Layout's own padding restores only its outer edges. A shadowed BottomSheet binding is left alone, and a second run changes nothing. (#6884)

  • astryx upgrade runs migrate-field-input-styling, which renames root xstyle/className/style on Field-composed inputs to the part they styled before: controlXstyle/controlClassName/controlStyle on TextInput, TextArea, NumberInput, FileInput, DateInput, DateRangeInput, DateTimeInput, TimeInput, Selector, MultiSelector, and ComplexSelector, and containerXstyle/containerClassName/containerStyle on Typeahead, Tokenizer, CheckboxList, RadioList, and Slider. It rewrites JSX written directly on an Astryx import (named, aliased, or namespace). Spreads and wrappers that forward their own styling props are left for manual migration, and a callsite that already sets the target prop gets a TODO instead of a rename. (#7342)

  • astryx upgrade renames emptySearchResultsText to emptySearchText on Tokenizer, Typeahead, BaseTypeahead, and ChatComposerInput trigger objects, for the release that removes the old name. (#7289)

Fixes

  • template and build follow the project's package manager. The nearest directory from the target up to the workspace root that has a packageManager field or a lockfile decides. With neither, the command uses npm, whatever package manager launched the CLI. When that directory's lockfiles disagree and nothing it commits picks one, installCommand is null and the note names the missing packages, the directory, and the lockfiles that disagree, instead of guessing a command that would write a second lockfile. (#7343)

  • Teach generated palette references in the full custom-theme authoring guide. (#7268)

Documentation

  • Clarify release-branch codemod promotion and version ownership. (#7293)

Themes (@astryxdesign/theme-*)

Breaking Changes

  • matcha's and y2k's spacing steps are rem (--spacing-2: 0.75rem), so their gaps and padding follow the reader's root font size like the core scale. Every step is the same pixel value at the default 16px root; a reader with a larger root font size now sees proportionally larger spacing on these themes. The CLI's copies of both theme templates and the annotated theme template's spacing example change the same way. (#7374) Migration: nothing to change to take the new behaviour. To keep fixed pixel spacing on one of these themes, extend it and override the steps in px, which is honoured as written: defineTheme({name: 'app', extends: matchaTheme, tokens: {'--spacing-2': '12px'}}).

    Also released in @astryxdesign/cli (templates).

  • On touch devices narrower than the lg breakpoint ((width < 1024px) and (pointer: coarse)), the 14px-base first-party themes now floor their type base to 16px and re-derive the ratio so Display 1 keeps its desktop size: neutral and chocolate (base 14, ratio 1.2 → 1.1736) and butter and stone (base 14, ratio 1.25 → 1.2225). On phones, body, label and code text rise from 14px to 16px; supporting text, the headings and Display 2 and 3 grow by 1–2px; only Display 1 holds its desktop size. Desktop sizes are unchanged. gothic, matcha and y2k already use a 16px base and need no rule. The CLI's theme templates carry the same rule, so a newly scaffolded theme gets it too. (#6699) An app theme that extends one of these four themes and sets its own typography.scale or type-size tokens inherits this rule, and on touch + narrow screens the rule replaces those values (a base-18 child drops to a 16px body and a 42px Display 1). Append your own rule for the same condition with the values you want; a later rule for the same condition wins.

    Migration: nothing to change to take the new phone scale. To keep the previous 14px phone scale in an app, extend the theme and append a later rule for the same condition: defineTheme({name: 'app', extends: neutralTheme, adaptations: {rules: [{when: {pointer: 'coarse', width: {below: 'lg'}}, value: {typography: {scale: {base: 14, ratio: 1.2}}}}]}}). For butter and stone use ratio: 1.25 and also set tokens: {'--text-supporting-size': '12px'} in that rule's value, because those themes pin supporting text at 12px rather than the 11px the scale alone gives. This cannot be rewritten by a codemod: whether an app wants the floor is a product choice.

    Also released in @astryxdesign/cli (templates).

Fixes

  • Generate Neutral's runtime palette references from the stops its theme uses, and fail verification when the package or CLI template copy drifts. (#7255)

    Also released in @astryxdesign/cli (templates).

@astryxdesign/build

Fixes

  • Preserve native light-dark() token values in Vite source builds by defaulting Lightning CSS to Astryx's supported browser floor. (#7330)

Contributors

Thanks to everyone who contributed to this release:

@cixzhang @Geervan @harjothkhara @imdreamrunner @josephfarina @ksying @rubyycheung @vjeux

Full Changelog: https://github.com/facebook/astryx/compare/v0.6.8...v0.7.0

4 hours ago
router

Release 2026-10-12 23:58

Release 2026-10-12 23:58

Changes

Performance

  • skip unused Start request setup (#8451) (740b0ce177) by @schiller-manuel
  • reuse prepared SSR request locations (#8441) (d6ba115d57) by @schiller-manuel

Packages

  • @tanstack/react-router@1.170.44
  • @tanstack/react-start@1.169.1
  • @tanstack/react-start-client@1.168.42
  • @tanstack/react-start-rsc@0.1.62
  • @tanstack/react-start-server@1.167.49
  • @tanstack/router-cli@1.167.43
  • @tanstack/router-core@1.171.37
  • @tanstack/router-generator@1.167.43
  • @tanstack/router-plugin@1.168.45
  • @tanstack/router-vite-plugin@1.167.45
  • @tanstack/solid-router@1.170.41
  • @tanstack/solid-start@1.169.1
  • @tanstack/solid-start-client@1.168.40
  • @tanstack/solid-start-server@1.167.47
  • @tanstack/start-client-core@1.170.37
  • @tanstack/start-plugin-core@1.171.52
  • @tanstack/start-server-core@1.169.42
  • @tanstack/start-static-server-functions@1.167.42
  • @tanstack/start-storage-context@1.167.39
  • @tanstack/vue-router@1.170.40
  • @tanstack/vue-start@1.169.1
  • @tanstack/vue-start-client@1.167.43
  • @tanstack/vue-start-server@1.167.47
4 hours ago
router

@tanstack/vue-router@1.170.40

Patch Changes

  • Updated dependencies [d6ba115]:
    • @tanstack/router-core@1.171.37
4 hours ago
router

@tanstack/vue-start@1.169.1

Patch Changes

  • Updated dependencies [740b0ce, d6ba115]:
    • @tanstack/start-server-core@1.169.42
    • @tanstack/start-plugin-core@1.171.52
    • @tanstack/vue-start-server@1.167.47
    • @tanstack/start-client-core@1.170.37
    • @tanstack/vue-router@1.170.40
    • @tanstack/vue-start-client@1.167.43
4 hours ago
router

@tanstack/start-static-server-functions@1.167.42

Patch Changes

  • Updated dependencies []:
    • @tanstack/react-start@1.169.1
    • @tanstack/solid-start@1.169.1
    • @tanstack/start-client-core@1.170.37
4 hours ago
router

@tanstack/vue-start-server@1.167.47

Patch Changes

  • Updated dependencies [740b0ce, d6ba115]:
    • @tanstack/start-server-core@1.169.42
    • @tanstack/router-core@1.171.37
    • @tanstack/vue-router@1.170.40
4 hours ago
router

@tanstack/start-plugin-core@1.171.52

Patch Changes

  • Updated dependencies [740b0ce, d6ba115]:
    • @tanstack/start-server-core@1.169.42
    • @tanstack/router-core@1.171.37
    • @tanstack/router-generator@1.167.43
    • @tanstack/router-plugin@1.168.45
4 hours ago
router

@tanstack/start-storage-context@1.167.39

Patch Changes

  • #8451 740b0ce - Skip request middleware bookkeeping when no request middleware is configured, and stop copying serialization adapters when the router adds none.

    Server route handlers and route middleware now receive the router's decoded pathname, the same one used for route matching. Characters that would change how the path is read, such as %2F, %3F, and %23, stay encoded.

  • Updated dependencies [d6ba115]:

    • @tanstack/router-core@1.171.37
4 hours ago
router

@tanstack/vue-start-client@1.167.43

Patch Changes

  • Updated dependencies [d6ba115]:
    • @tanstack/router-core@1.171.37
    • @tanstack/start-client-core@1.170.37
    • @tanstack/vue-router@1.170.40
4 hours ago
router

@tanstack/start-server-core@1.169.42

Patch Changes

  • #8451 740b0ce - Skip request middleware bookkeeping when no request middleware is configured, and stop copying serialization adapters when the router adds none.

    Server route handlers and route middleware now receive the router's decoded pathname, the same one used for route matching. Characters that would change how the path is read, such as %2F, %3F, and %23, stay encoded.

  • #8441 d6ba115 - Reuse the parsed request location during SSR instead of repeating input rewrites. Match server routes against the app router's parsed pathname while preserving encoded pathnames for server handlers and middleware.

  • Updated dependencies [740b0ce, d6ba115]:

    • @tanstack/start-storage-context@1.167.39
    • @tanstack/router-core@1.171.37
    • @tanstack/start-client-core@1.170.37