@astryxdesign/core
New Features
- AspectRatio: add a
fitprop ('cover' | 'contain' | 'center') so the component sizes and positions its child instead of every consumer hand-writingwidth/height/objectFiton the child.cover/containship as zero-specificity baseline rules inreset.csskeyed on adata-astryx-aspect-ratio-overridemarker the component sets on the child's direct parent (direct-child selectors, no dependence on internal structure or the theming surface), so a child's own styles always win and self-sized children are unchanged;centercenters the child at its natural size from the component's wrapper. Whenfitis omitted the child is left unstyled, preserving the existing contract (#2753) - CodeBlock: new
syntaxThemeprop for per-instance syntax theme overrides. Shorthand for wrapping a single block in<SyntaxTheme theme={...}>— accepts a preset from@astryxdesign/core/theme/syntaxor a theme created withdefineSyntaxTheme()(#3360). - CollapsibleGroup: add a
hasDividersprop (boolean, defaultfalse) so FAQ-style accordions get built-in row hairlines instead of hand-rolled borders, plus adensityprop ('compact' | 'balanced' | 'spacious') controlling row padding. When dividers are enabled the group renders a wrapper div (astryx-collapsible-group) and items default to'balanced'density; without dividers the group keeps its DOM-less contract and existing renders are unchanged. Borders use the themed--border-width/--color-bordertokens, and nested collapsibles never inherit row chrome (#3487). - New
incident-consolepage template: an on-call incident response console demonstrating the frame-first tracker archetype — grouped dense incident rows (StatusDot severity, Token state), PowerSearch filtering, status segmented control, and a resizable inspector panel with metadata and timeline. Adds theTools - Incident Consoletemplate category. - useLayer: new
positioning: 'custom'context render option for consumers that author their own position styles (explicitanchor()insets,anchor-size()covers). Keeps the popover behavior andposition-anchorwiring but derives no position styles fromplacement/alignment— including the automatic RTL mirroring, which becomes the consumer's responsibility (#3389). - New
messaging-shellpage template: Slack-style column frame (rail | sidebar | stream | thread panel) built on the Chat component family — dense rows, zero cards. Adds theShell - Messagingtemplate category. - Pagination: the
dotsvariant is now keyboard navigable via the shared useListFocus primitive. With a dot focused, Left/Right arrows move between dots and Home/End jump to the first/last, the active page follows focus (roving tabindex), and Up/Down are left to the browser so vertical scrolling is unaffected. (#3681) - Switch: rename the labelSpacing
"default"value to"hug";"default"keeps working as a deprecated alias. Runastryx upgradeto migrate call sites automatically. (#2889) - Add
useHotkeyshook for global keyboard shortcuts. Registers one windowkeydownlistener per hook instance with handlers kept in a ref (re-renders never re-subscribe). Combos like'mod+k'or'escape'—modmaps to ⌘ on Apple platforms (same detection as Kbd) and Ctrl elsewhere. Skips typing targets (input/textarea/select/contenteditable) unlessallowInInputs, skipsdefaultPreventedevents, and callspreventDefault()on match. SSR-safe.
Fixes
- Emit derived accent tokens as var(--color-accent) references (#3495)
Generated themes now emit--color-text-accentand--color-icon-accentasvar(--color-accent)and--color-accent-mutedascolor-mix()over the same reference, instead of baking resolved hex literals. Overriding--color-accenton any scope re-accents the whole subtree at runtime — no second theme build, no per-token overrides.--color-on-accentstays resolved because it is a contrast computation CSS cannot express. - adjustTime guards non-finite deltas and wraps negatives in O(1) (#3583)
A non-finite deltaMinutes previously spun the wrap-around loop forever (-Infinity — tab freeze) or produced the corrupt string "NaN:NaN" (NaN) that could be committed into consumer form state through DateTimeInput's timeIncrement path. Non-finite deltas now return the input time unchanged, and the negative wrap-around uses double-modulo instead of a loop. - Forward rest props on AlertDialog and AppShell so consumer
data-*,aria-*, andidattributes reach the DOM (#3876) - Wrap CommandPalette search updates in
startTransitionto resolve a React 19 warning (#3815) - Derive DropdownMenuItem escape hatches from BaseProps so it accepts
xstyle,className, andstyle(#3687) - Order rest spread before explicit ARIA props on Item and InputGroup so consumer-set ARIA attributes are no longer clobbered (#3751)
- Avatar: retry a changed src/fallbackSrc after a load error
A failed image load latched a boolean error flag that never reset, so updatingsrc(orfallbackSrc) to a valid URL kept rendering the initials fallback forever. The error state now tracks the exact URL that failed, so a changed source gets a fresh load attempt. - Banner: the expand/collapse toggle is now linked to its content region via
aria-controls, completing the disclosure pattern (the toggle already exposedaria-expanded). (#3719) - Calendar: mark today's date cell with
aria-current="date". Previously "today" was conveyed only visually, so screen-reader users could not identify the current date (WAI-ARIA date-picker pattern). (#3708) - Calendar: move aria-selected from the day button onto its gridcell wrapper. A plain button (implicit role "button") does not permit aria-selected, which tripped the axe aria-allowed-attr rule; the selection state belongs on the gridcell role in an ARIA grid. (#3343)
- Calendar / DateInput / DateTimeInput: defensively clamp
numberOfMonthsto its1 | 2type at runtime. Out-of-range values (e.g.0rendered nothing,1000locked the page up inArray.from({length})) fall back to a single month. DateInput and DateTimeInput inherit the guard since they forward the prop to<Calendar>(#2704) - Calendar: month navigation chevrons now mirror under
dir="rtl"so "Previous month" points outward (visually right) and "Next month" points left, instead of both pointing inward at the month label. The mirror is a CSS-only StyleX conditional transform keyed on thedirattribute; DOM order, labels, and handlers are unchanged. Also fixes the embedded calendars in DateInput, DateRangeInput, and DateTimeInput (#3388). - Card: draw the
defaultvariant's border inside its padding and drop the invisible border from the other variants, so a card's total inset (border + padding) matches the spacing token exactly instead of being 1px larger on every side. (#3712) - Carousel: make the horizontal scroll container keyboard-focusable (
tabIndex={0}) so keyboard-only users can scroll it with arrow keys. Previously the scrollable region had no keyboard access (axe: scrollable-region-focusable). (#3343) - Chat components forward pass-through props (data-, aria-, id) to the DOM
ChatDictationButton,ChatLayoutScrollButton,ChatMessageMetadata,ChatSystemMessage, andChatTokenizedTextdeclaredBasePropsbut silently droppeddata-*,aria-*, andid: they never captured...restnor spread it onto their rendered element. They now capture...restand spread it onto their primary element after the merged className/style, with any component-owned attribute (e.g.role) set afterward so it still wins. - Chat: tool-call rows and the tool-calls group header now expose complete disclosure semantics — expandable call rows announce
aria-expandedand reference their detail panel viaaria-controls, and the group header references its content region. Previously an expandable call row was announced as a plain button with no indication it opens anything. (#3720) - ChatComposerInput preserves composer submit with child onChange (#2330)
- CheckboxInput now forwards rest props (including
data-testid) to the underlying<input>. Previously the component used a closed destructuring list with no...restcapture, so any prop not explicitly named —data-testid, otherdata-*attributes, etc. — was silently dropped despiteCheckboxInputProps(viaBaseProps) typing them as valid. Every sibling input component (TextInput, Selector, Slider, Button, Badge) already forwards rest props; CheckboxInput was the sole outlier. Rest is spread before the component's own named attributes on the<input>, so it cannot overridechecked,disabled,type, or any other explicitly-set prop. - ClickableCard: remove the faint hover "ring" on borderless variants (everything except
default) by dropping their invisible border. Thedefaultvariant now draws its border inside the padding so outer dimensions stay identical across variants, and its border color emphasizes on hover. (#3712) - CodeBlock: announce "Copied" via a polite live region when the copy button is used. Previously success was signalled only by swapping the button's
aria-label, which screen readers don't reliably announce. (#3709) - CodeBlock: restart the copied-indicator timer on rapid re-copy
Each copy click armed an independent 2s reset timer, so copying again within the window let the first click's timer revert the "Copied" indicator early. The timer now restarts on every copy and is cleared on unmount. - CodeBlock: complete the collapsible header's disclosure pattern. It now shows the standard accent focus ring on
:focus-visible— previously it defined no focus style and fell back to the browser's default outline, unlike the system's other disclosure controls (Collapsible, TabMenu) — and links to the code region it shows/hides viaaria-controls(the header already exposedaria-expanded). The region stays mounted when collapsed (CSS grid animation), soaria-controlsis an always-resolvable reference. (#3723) - Collapsible: link the trigger to its content region with
aria-controls, and give the content region a matchingid. Completes the disclosure pattern so assistive tech can move from the trigger button to the region it shows/hides (previously onlyaria-expandedwas set). (#3707) - Collapsible: the trigger button now shows a visible focus ring when focused via keyboard. The trigger's
all: unsetreset removed the browser's default outline without restoring a replacement, leaving keyboard focus invisible (WCAG 2.4.7). (#3722) - Keep Collapsible self-toggling when uncontrolled with onOpenChange (#3785)
useCollapsibletreated the presence ofonOpenChangeas a signal that the component was controlled: itstogglehandler fired the callback but skipped the internal state update, so an uncontrolled Collapsible given onlyonOpenChange(noisOpen) appeared stuck — the callback fired but the content never opened or closed. Control is now determined solely by whetherisOpenwas provided, mirroring howisOpenis derived. Uncontrolled usage drives internal state and firesonOpenChange; controlled usage still defers entirely to the parent. - SegmentedControlItem now forwards a consumer onClick, and add composeEventHandlers
A consumeronClickonSegmentedControlItemwas silently dropped — the internal selection handler clobbered it. It now runs alongside selection (callpreventDefault()to opt out). The newcomposeEventHandlersutility chains handlers in order and stops when one prevents default, so components that own an interaction can also honor a consumer handler for the same event. - ContextMenu now anchors the menu to the right-click point relative to its context, so it follows the content on scroll and auto-flips at viewport edges instead of staying at a fixed screen position (#3465).
- DateTimeInput: the embedded time input now carries
aria-describedbyandaria-busylike the date input, so screen-reader users keep access to the field's description, status message, and disabled message when focused on the time half. (#3716) - DateTimeInput:
timeIncrementis now typed as a literal union of sensible increments (1 | 5 | 10 | 15 | 30) instead of an opennumber, so negatives, fractions, and absurd values are rejected at the type level rather than silently accepted. Adds an exportedDateTimeInputTimeIncrementtype (#2725) - focusableSelector: match only real links (
a[href],area[href]) instead of any element with an href attribute. Previously non-focusable elements carrying href could be treated as tab stops by useFocusTrap. (#3714) - useFocusTrap: focus now returns to the previously-focused element when a trap deactivates, so closing a Popover (Escape or light-dismiss) no longer drops keyboard focus to the page body. Components that already restore focus themselves are unaffected — the trap only restores when focus would otherwise be lost. (#3732)
- Forward consumer event handlers in SegmentedControl, CheckboxListItem, and SideNavCollapseButton
These components set their ownonClick/onKeyDown/onFocus/onBlurafter spreading{...rest}(or destructured the consumer's handler and never used it), so a consumer-supplied handler for the same event was silently dropped. They now compose the consumer's handler with the built-in one viacomposeEventHandlers, consumer-first — the consumer's runs and can callpreventDefault()to opt out of the built-in behavior. - Grid: with
columns={{minWidth, max}}, columns that are present now fill the row when fewer thanmaxfit. Previously themaxcap was applied to each track's max size, so a layout collapsing to a single column (e.g. on mobile) left dead space on the right instead of stretching to full width. The cap now limits the column count while letting present columns reach1fr. (#3391) - Icon: allow string (registry) icons to be made meaningful.
aria-hidden="true"is now applied before the prop spread, so consumers can override it (e.g.aria-hidden={false}+role="img"+aria-label) for a standalone informational icon. Previously registry-mode icons hardcodedaria-hiddenafter the spread, making it impossible to override — inconsistent with component-mode icons, which already allowed it. Default behavior (decorative, hidden) is unchanged. (#3710) - Layer/Popover: anchor-positioned popovers now mirror in RTL.
placement/alignmentstart/endare logical (inline-start/inline-end), so DropdownMenu, Selector, Typeahead, date inputs, and every other context-mode popover opens toward the correct side under RTL automatically via CSS. LTR behavior is unchanged (#3389). - Lightbox: navigating between images now announces the new image and its position (e.g. "Sunset over the bay, 3 of 12") to screen readers via a polite live region. Previously only the visual counter updated. (#3727)
- MobileNav forwards pass-through attributes and applies consumer className/style
MobileNav declaredBasePropsbut silently discardedclassName/style(destructured to unused vars) and dropped every other pass-through attribute (id,aria-*,data-*beyonddata-testid, event handlers). It now mergesclassName/styleand spreads the remaining props onto the<dialog>, and composes a consumeronClickwith the backdrop-dismiss handler viacomposeEventHandlers. - Forward unhandled pass-through attributes (
data-testid,aria-*,id, etc.) to the primary rendered element of Switch, Pagination, RadioListItem, SideNavSection, TableHeader/TableBody/TableFooter, and TopNavMegaMenuFeaturedCard. These components previously dropped attributes not explicitly consumed, so test hooks and accessibility attributes silently disappeared. - NumberInput with hasClear commits null when cleared from the keyboard (#3599)
Deleting the text and blurring (or pressing Enter) silently reverted to the previous value — only the X button honored the clearable contract. An emptied input now commits onChange(null) on blur/Enter when hasClear is set; non-clearable inputs keep the revert behavior. - paginateData clamps invalid page numbers instead of slicing from the end (#3593)
A negative page fed a negative start index to Array.slice, which counts from the end of the data — page -1 returned the dataset's tail dressed up as a page — and fractional pages returned slices straddling two pages. page now gets the same coercion pageSize received in #3380. - PowerSearch: changes to the visible result count are now announced to screen readers via a polite live region, mirroring Typeahead's wording. Previously the count only updated visually. (#3726)
- ProgressBar: treat a non-finite value/max as empty progress
A NaNvalue(e.g. an upstreamloaded / total * 100with total 0) leaked the literal string "NaN" intoaria-valuenow, the value label, and the fill width style. Non-finitevalue/maxnow route through the same empty-progress handling asmax={0}. - ProgressBar: guard the value label against a zero max
Whenmaxwas0(or negative), the default value-label formatter producedNaN%(0/0) orInfinity%. That string was rendered visually and written intoaria-valuetext, so screen readers announced "NaN percent". Guard it the same way the fill percentage already is (max > 0 ? … : 0). - Resizable: keyboard resizing now works. The keydown handler was attached to a non-focusable child of the separator, so Arrow/Home/End keys never fired for keyboard users; it now lives on the focusable
role="separator"element per the WAI-ARIA window-splitter pattern. (#3729) - ResizeHandle: release window drag listeners when unmounted mid-drag
A drag in flight when the handle unmounts never receives its pointerup, so the window pointermove/pointerup/pointercancel listeners leaked — every subsequent pointer move kept resizing the still-mounted region, and the body cursor/user-select overrides stuck. Unmount now tears down the in-flight drag's listeners and restores the body styles. useTheme()/resolveThemeTokens()now resolve derived tokens that reference other tokens (e.g.--color-text-accent: var(--color-accent)) to concrete raw values, following reference chains iteratively. Supported CSS color functions (color-mix(in srgb, …)) are evaluated against those values, so canvas/SVG/data-viz consumers get usable colors instead ofvar(...)orcolor-mix(...)strings (#3697).
Also adds shared color parsing/formatting primitives (parseHex,parseRgb,parseColor,formatHex,formatColor) under@astryxdesign/core/utils, unifying the color parsers previously duplicated across the theme layer.- SegmentedControl: tabbing through no longer rewrites the value (#3597)
When value matched no enabled item (initial empty state, stale server value, or the selected item disabled), the roving tab stop fell back to the first enabled radio and selection-follows-focus fired onChange with it — a keyboard user mutated the form just by tabbing past the control. Selection now only follows focus moves within the group (arrow/Home/End); entering focus is a pure focus move, and click selection is unchanged. - SegmentedControl and SegmentedControlItem forward data-testid to the DOM
Both components declaredBaseProps(sodata-testidand otherdata-*attributes type-check) but silently dropped them: neither captured...restnor spread the remaining props onto its rendered element, so the attribute never reached the DOM. They now capture...restand spread it onto the radiogroup<div>/ radio<button>(the same rest-spread fix applied toCheckboxInputin #3738), placed before the component's ownrole/aria-*so those can't be overridden. - SideNav: don't render the empty sticky-bottom container when
collapsible.hasButtonis false. Previously the footer container rendered whenever the sidebar was collapsible — even with the built-in button opted out and nofooter/footerIcons— leaving an empty, bordered container at the bottom of the nav. The container now renders only when it has visible content (a footer, footer icons, or the built-in collapse button). (#3603) - Markdown/useStreamingText: streaming text now respects
prefers-reduced-motion— the per-character reveal snaps to the full text and the entry fade is disabled for users who prefer reduced motion, matching the convention already used by Spinner, Skeleton, ProgressBar, and Chat. (#3730) - Table: always render a numeric aria-valuenow on the column resize handle. The focusable resize separator omitted aria-valuenow before its width was measured, which failed the axe aria-required-attr rule (a focusable role="separator" requires aria-valuenow); it now falls back to the column minWidth. (#3343)
- Table rows re-render when a field is removed from the row object (#3595)
The row memo compared only the keys of the new item, so a field cleared by omission (optimistic update, server response) never invalidated the memo and the cell kept rendering the deleted value indefinitely. A key-count check now catches removed properties. - Table selection: select-all no longer reads checked over an empty filtered table (#3591)
With rows selected and a filter matching nothing, the union-based all-selected check treated the invisible selection as "all selected": the header checkbox rendered checked over an empty table, and deselect-all was a no-op because the hidden keys count as frozen. Zero actionable rows now reads as not-all-selected; the frozen-selection preservation itself is unchanged. - Table sort: group NaN cells with null instead of corrupting the order (#3585)
A NaN cell hit the numeric fast path in defaultCompare, and the NaN comparator result reads as "equal" to Array.sort — making the comparator inconsistent and silently mis-ordering the other, valid rows. NaN now sorts to the end alongside null/undefined. - Table: column headers now render with
scope="col"so screen readers correctly associate data cells with their column headers. Consumer-provided scope via column header props still takes precedence. (#3715) - TabList: the overflow tab menu now uses a roving tabindex — one tab stop with arrow-key navigation between items — instead of making every overflow item a separate tab stop. (#3728)
- Document the full themeProps() selector surface and guard it against drift (#3741)
theming.targetsis the documented CSS surface of a component — the stableastryx-*classes it renders and the visual props it reflects as data attributes. It was hand-authored while the truth lived inthemeProps()calls in the source, and nothing kept the two in agreement, so it drifted twice (#3652, #3680) and was drifted again. - Thumbnail: give the labeled root a group role so its accessible name is valid. Previously the file name was set via aria-label on a plain div with no role, which axe flags as aria-prohibited-attr (serious) because a generic element cannot carry a name. (#3343)
- TimeInput: typed-invalid input (e.g. an out-of-range time that will be reverted on blur) now sets
aria-invalidand announces "Invalid time" via an assertive live region, matching DateInput, NumberInput, and DateTimeInput. Previously only the visual red border signalled the invalid state. (#3718) - Timestamp: render nothing instead of crashing on an unparseable value
An unparseablevalue(a malformed date string, or a NaN timestamp from missing data) produced an Invalid Date whose formatting throws "Invalid time value", crashing the whole tree. Invalid values now render nothing and log a console warning instead. - Timestamp: keep relative time within its own tier
Relative-time tiers guarded the raw diff with<but displayed the count withMath.round, so just under a boundary it rendered "60 minutes ago" / "24 hours ago" instead of "1 hour ago" / "1 day ago" (and the same in the future direction). Floor the per-tier count so it can never reach the next tier. - Toast fallback viewport resolves the app's theme mode instead of OS preference (#3743)
The fallback mounts via createRoot() on a disconnected tree, so Toast's useTheme() couldn't see ThemeContext and fell back to prefers-color-scheme — when that disagreed with<Theme mode>, the toast's inverted-surface text/icon could compute to the same color as its own background. useToast's fallback container now mirrors<html data-theme>anddata-astryx-themedirectly (kept live via MutationObserver), and useTheme() itself falls back to reading<html data-theme>before assuming OS preference when no ThemeContext ancestor is reachable — the mechanism that actually resolves Toast's JS-computed mode for disconnected trees like this one. - Toast: onHide fires exactly once, and a paused auto-hide timer survives viewport re-renders (#3589)
A second dismissal during the exit transition (double-click, auto-timer plus manual dismiss()) double-fired onHide; and because the timer effect depended on the viewport's per-render onDismiss identity, any other toast arriving or leaving silently restarted — and un-paused — every mounted toast's auto-hide timer. Exiting toasts are now tracked in a ref before onHide fires, and the timer reads onDismiss through a ref so it only restarts on a genuine duration change and respects the paused state. - Typeahead/Tokenizer discard out-of-order async search results (#3587)
Each search now claims a new generation, so a slow response for an abandoned query can no longer overwrite the results of the current one (previously the last response to resolve always won, and Enter could select an item from a query the user had already replaced).
Documentation
- Correct Carousel doc drift and translate its Chinese usage block (#3532)
The Carousel docs described behaviors the component doesn't have: there is no scroll-driven scale effect on items, and the prev/next buttons are driven by per-edge overflow (each appears when the content can scroll in that direction), not by hover or platform. Descriptions now match the source. Also translates the docsZh usage block, which was still in English. - DialogHeader: add property-editor defaults and example blocks (#2719)
The DialogHeader docsite page rendered an empty preview because the properties editor had no default prop values, and it had no example blocks. Add playground defaults (title, subtitle, divider) so the preview is meaningful out of the box, plus example blocks covering a basic header, a header with a close button, and one with start/end content. - Correct useInteractiveRole isDisabled docs for disabled href (#3786)
TheisDisabledJSDoc claimed a disabledhref"falls back to button". It does not: a disabledhrefis skipped at the link step and resolved by the remaining priority checks, so with noonClickand no interactive context it lands on'inert'— asTokenalready relies on for disabled links. TheisDisableddoc, the step-1 inline comment, and the priority summary now describe the actual behavior. Docs only; no runtime change. - Document when playground.overlay applies: components with no inline containment (MobileNav, Lightbox) use it, while Dialog, AlertDialog, and CommandPalette intentionally keep their contained isInline previews so knobs stay usable. Adds regression tests guarding both shapes (#3657)
- Lightbox Properties preview shows the overlay open trigger instead of an empty stage (playground.overlay)
- Document the full
themeProps()selector surface in component theming targets. A sweep of everythemeProps()call against thetheming.targetsentries found 19 gaps across 16 components where a visual prop or state had no documenteddata-*selector, so the CLI theming table hid part of the themeable surface: missing target entries (Citation, ToggleButton, Banner content, OverlayScrim, NavHeadingMenu, NavHeadingMenuItem) and missingvisualProps/stateson existing entries (Chat message density, Checkbox, Code color, CodeBlock container, DropdownMenu item size, SegmentedControl item, SelectableCard variant, SideNav item, Timestamp format, Tokenizer status, TopNav item selected, TreeList item density, Typeahead size).
@astryxdesign/cli
New Features
- Add a v0.1.5 upgrade codemod that renames
labelSpacing="default"tolabelSpacing="hug"on Switch. (#2889) - New
incident-consolepage template: an on-call incident response console demonstrating the frame-first tracker archetype — grouped dense incident rows (StatusDot severity, Token state), PowerSearch filtering, status segmented control, and a resizable inspector panel with metadata and timeline. Adds theTools - Incident Consoletemplate category. - New
messaging-shellpage template: Slack-style column frame (rail | sidebar | stream | thread panel) built on the Chat component family — dense rows, zero cards. Adds theShell - Messagingtemplate category.
Fixes
- Fill viewport height across CLI page templates so the background covers the full page (#3762)
astryx init --features agentsnow supports--agent hermes. The preset injects the component index into an existing.hermes.md/HERMES.md(Hermes Agent's top-priority project-context files) and otherwise creates rootAGENTS.md, which Hermes loads from the project root — unlike the.claude/CLAUDE.mddefault. Additive only: existingclaude/cursor/codex/all/auto-detect behavior is unchanged. (#2187)- cli:
astryx doctornow detects@astryxdesign/theme-*packages in pnpm projects. pnpm installs packages as symlinks intonode_modules/.pnpm, and the theme scan only accepted real directories, so every symlinked theme package was skipped and doctor warned that none were installed (#3530). - Make
astryx theme build's color-scheme declaration mode-aware, so built themes withlight-dark()tokens no longer defeat<Theme mode="light|dark">forcing (#3660) runCodemodsnow returnswrittenFiles, soastryx upgrade's post-codemod hooks (prettier/eslint formatting) actually run on core-codemod changes.
The runner built thewrittenFileslist internally but omitted it from its return object, soupgrade.mjsreadcodemodResult.writtenFiles ?? []as always-empty and the configuredhooks.postCodemod(e.g.prettier --write,eslint --fix) received no files and silently skipped. As a result, jscodeshift's default double-quote output ("@astryxdesign/core/Button") was never reformatted to the project's style, failingprettier-formatlint on migrated apps. The siblingintegration-runneralready returnedwrittenFilescorrectly, so integration-codemod changes were formatted while core-codemod changes were not.astryx swizzle: swizzled components ship raw StyleX source that needs a build-time StyleX compiler, and without one they render unstyled with no error. The command now prints a StyleX build-setup note after copying (including the Next.js caveat that the StyleX Babel plugin disables SWC and breaksnext/font, so an SWC-based transform is required), andastryx docs stylinggains a "StyleX Build Setup" section covering per-bundler setup. (#3373)astryx theme build: custom component variants declared in a theme (e.g.button['variant:accentOutline']) now generate a type augmentation against the component's real interface (ButtonVariantMap) instead of a non-existentXDS-prefixed one, sovariant="accentOutline"type-checks. Props with no augmentation point (closed unions like Buttonsizeor Headingtype) are skipped instead of emitting dead augmentations, and the generated.variants.d.tsis now referenced from the theme's.d.tsso the augmentation actually loads. (#3371)
Documentation
- CodeBlock: terminal-style dark block template (syntaxTheme preset)
- Add cascade-layer safety guidance to the migration guide (
astryx docs migration): a Cascade Layer Safety audit checklist (unlayered styles and later layers both beatastryx-baseregardless of specificity, classify every stylesheet into a layer deliberately, layer Tailwind preflight on both v3 and v4) and a Foundation Smoke Test section (one page with Button/TextInput/Card/Table plus a non-zero-padding assertion) so a broken layer order fails before feature work instead of after N migrated screens. The getting-started guide now points to it from the theme CSS step. - NavHeadingMenu: add a playground config and showcase block so the Overview tab has a working preview (#2698)
- NavHeadingMenu: constrain the showcase SideNav to a shorter height so the heading no longer appears to float at the top of the Overview preview (#2698)
@astryxdesign/build
Other Changes
- Use node: protocol prefix for Node.js builtin imports (#3737)
Contributors
Thanks to everyone who contributed to this release:
- @AKnassa
- @Geervan
- @Han5991
- @arham766
- @Arman-Luthra
- @bhamodi
- @cixzhang
- @dmitriy-bty
- @durvesh1992
- @ejhammond
- @harjothkhara
- @is-jain
- @jiunshinn
- @josephfarina
- @kentonquatman
- @let-sunny
- @raphaelroshanM
- @syntaxsawdust
- @thedjpetersen
- @zeroryu
Full Changelog: v0.1.4...v0.1.5