Skip to content

v0.1.5

Latest

Choose a tag to compare

@cixzhang cixzhang released this 13 Jul 23:25
75215a4

@astryxdesign/core

New Features

  • AspectRatio: add a fit prop ('cover' | 'contain' | 'center') so the component sizes and positions its child instead of every consumer hand-writing width/height/objectFit on the child. cover/contain ship as zero-specificity baseline rules in reset.css keyed on a data-astryx-aspect-ratio-override marker 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; center centers the child at its natural size from the component's wrapper. When fit is omitted the child is left unstyled, preserving the existing contract (#2753)
  • CodeBlock: new syntaxTheme prop for per-instance syntax theme overrides. Shorthand for wrapping a single block in <SyntaxTheme theme={...}> — accepts a preset from @astryxdesign/core/theme/syntax or a theme created with defineSyntaxTheme() (#3360).
  • CollapsibleGroup: add a hasDividers prop (boolean, default false) so FAQ-style accordions get built-in row hairlines instead of hand-rolled borders, plus a density prop ('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-border tokens, and nested collapsibles never inherit row chrome (#3487).
  • New incident-console page 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 the Tools - Incident Console template category.
  • useLayer: new positioning: 'custom' context render option for consumers that author their own position styles (explicit anchor() insets, anchor-size() covers). Keeps the popover behavior and position-anchor wiring but derives no position styles from placement/alignment — including the automatic RTL mirroring, which becomes the consumer's responsibility (#3389).
  • New messaging-shell page template: Slack-style column frame (rail | sidebar | stream | thread panel) built on the Chat component family — dense rows, zero cards. Adds the Shell - Messaging template category.
  • Pagination: the dots variant 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. Run astryx upgrade to migrate call sites automatically. (#2889)
  • Add useHotkeys hook for global keyboard shortcuts. Registers one window keydown listener per hook instance with handlers kept in a ref (re-renders never re-subscribe). Combos like 'mod+k' or 'escape'mod maps to ⌘ on Apple platforms (same detection as Kbd) and Ctrl elsewhere. Skips typing targets (input/textarea/select/contenteditable) unless allowInInputs, skips defaultPrevented events, and calls preventDefault() on match. SSR-safe.

Fixes

  • Emit derived accent tokens as var(--color-accent) references (#3495)
    Generated themes now emit --color-text-accent and --color-icon-accent as var(--color-accent) and --color-accent-muted as color-mix() over the same reference, instead of baking resolved hex literals. Overriding --color-accent on any scope re-accents the whole subtree at runtime — no second theme build, no per-token overrides. --color-on-accent stays 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-*, and id attributes reach the DOM (#3876)
  • Wrap CommandPalette search updates in startTransition to resolve a React 19 warning (#3815)
  • Derive DropdownMenuItem escape hatches from BaseProps so it accepts xstyle, className, and style (#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 updating src (or fallbackSrc) 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 exposed aria-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 numberOfMonths to its 1 | 2 type at runtime. Out-of-range values (e.g. 0 rendered nothing, 1000 locked the page up in Array.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 the dir attribute; DOM order, labels, and handlers are unchanged. Also fixes the embedded calendars in DateInput, DateRangeInput, and DateTimeInput (#3388).
  • Card: draw the default variant'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, and ChatTokenizedText declared BaseProps but silently dropped data-*, aria-*, and id: they never captured ...rest nor spread it onto their rendered element. They now capture ...rest and 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-expanded and reference their detail panel via aria-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 ...rest capture, so any prop not explicitly named — data-testid, other data-* attributes, etc. — was silently dropped despite CheckboxInputProps (via BaseProps) 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 override checked, 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. The default variant 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 via aria-controls (the header already exposed aria-expanded). The region stays mounted when collapsed (CSS grid animation), so aria-controls is an always-resolvable reference. (#3723)
  • Collapsible: link the trigger to its content region with aria-controls, and give the content region a matching id. Completes the disclosure pattern so assistive tech can move from the trigger button to the region it shows/hides (previously only aria-expanded was set). (#3707)
  • Collapsible: the trigger button now shows a visible focus ring when focused via keyboard. The trigger's all: unset reset 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)
    useCollapsible treated the presence of onOpenChange as a signal that the component was controlled: its toggle handler fired the callback but skipped the internal state update, so an uncontrolled Collapsible given only onOpenChange (no isOpen) appeared stuck — the callback fired but the content never opened or closed. Control is now determined solely by whether isOpen was provided, mirroring how isOpen is derived. Uncontrolled usage drives internal state and fires onOpenChange; controlled usage still defers entirely to the parent.
  • SegmentedControlItem now forwards a consumer onClick, and add composeEventHandlers
    A consumer onClick on SegmentedControlItem was silently dropped — the internal selection handler clobbered it. It now runs alongside selection (call preventDefault() to opt out). The new composeEventHandlers utility 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-describedby and aria-busy like 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: timeIncrement is now typed as a literal union of sensible increments (1 | 5 | 10 | 15 | 30) instead of an open number, so negatives, fractions, and absurd values are rejected at the type level rather than silently accepted. Adds an exported DateTimeInputTimeIncrement type (#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 own onClick / onKeyDown / onFocus / onBlur after 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 via composeEventHandlers, consumer-first — the consumer's runs and can call preventDefault() to opt out of the built-in behavior.
  • Grid: with columns={{minWidth, max}}, columns that are present now fill the row when fewer than max fit. Previously the max cap 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 reach 1fr. (#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 hardcoded aria-hidden after 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/alignment start/end are 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 declared BaseProps but silently discarded className/style (destructured to unused vars) and dropped every other pass-through attribute (id, aria-*, data-* beyond data-testid, event handlers). It now merges className/style and spreads the remaining props onto the <dialog>, and composes a consumer onClick with the backdrop-dismiss handler via composeEventHandlers.
  • 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 NaN value (e.g. an upstream loaded / total * 100 with total 0) leaked the literal string "NaN" into aria-valuenow, the value label, and the fill width style. Non-finite value/max now route through the same empty-progress handling as max={0}.
  • ProgressBar: guard the value label against a zero max
    When max was 0 (or negative), the default value-label formatter produced NaN% (0/0) or Infinity%. That string was rendered visually and written into aria-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 of var(...) or color-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 declared BaseProps (so data-testid and other data-* attributes type-check) but silently dropped them: neither captured ...rest nor spread the remaining props onto its rendered element, so the attribute never reached the DOM. They now capture ...rest and spread it onto the radiogroup <div> / radio <button> (the same rest-spread fix applied to CheckboxInput in #3738), placed before the component's own role/aria-* so those can't be overridden.
  • SideNav: don't render the empty sticky-bottom container when collapsible.hasButton is false. Previously the footer container rendered whenever the sidebar was collapsible — even with the built-in button opted out and no footer/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.targets is the documented CSS surface of a component — the stable astryx-* classes it renders and the visual props it reflects as data attributes. It was hand-authored while the truth lived in themeProps() 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-invalid and 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 unparseable value (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 with Math.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> and data-astryx-theme directly (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)
    The isDisabled JSDoc claimed a disabled href "falls back to button". It does not: a disabled href is skipped at the link step and resolved by the remaining priority checks, so with no onClick and no interactive context it lands on 'inert' — as Token already relies on for disabled links. The isDisabled doc, 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 every themeProps() call against the theming.targets entries found 19 gaps across 16 components where a visual prop or state had no documented data-* selector, so the CLI theming table hid part of the themeable surface: missing target entries (Citation, ToggleButton, Banner content, OverlayScrim, NavHeadingMenu, NavHeadingMenuItem) and missing visualProps/states on 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" to labelSpacing="hug" on Switch. (#2889)
  • New incident-console page 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 the Tools - Incident Console template category.
  • New messaging-shell page template: Slack-style column frame (rail | sidebar | stream | thread panel) built on the Chat component family — dense rows, zero cards. Adds the Shell - Messaging template category.

Fixes

  • Fill viewport height across CLI page templates so the background covers the full page (#3762)
  • astryx init --features agents now 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 root AGENTS.md, which Hermes loads from the project root — unlike the .claude/CLAUDE.md default. Additive only: existing claude/cursor/codex/all/auto-detect behavior is unchanged. (#2187)
  • cli: astryx doctor now detects @astryxdesign/theme-* packages in pnpm projects. pnpm installs packages as symlinks into node_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 with light-dark() tokens no longer defeat <Theme mode="light|dark"> forcing (#3660)
  • runCodemods now returns writtenFiles, so astryx upgrade's post-codemod hooks (prettier/eslint formatting) actually run on core-codemod changes.
    The runner built the writtenFiles list internally but omitted it from its return object, so upgrade.mjs read codemodResult.writtenFiles ?? [] as always-empty and the configured hooks.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, failing prettier-format lint on migrated apps. The sibling integration-runner already returned writtenFiles correctly, 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 breaks next/font, so an SWC-based transform is required), and astryx docs styling gains 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-existent XDS-prefixed one, so variant="accentOutline" type-checks. Props with no augmentation point (closed unions like Button size or Heading type) are skipped instead of emitting dead augmentations, and the generated .variants.d.ts is now referenced from the theme's .d.ts so 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 beat astryx-base regardless 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:

Full Changelog: v0.1.4...v0.1.5