Skip to content

Releases: meta/astryx

Astryx v0.7.4

Choose a tag to compare

@cixzhang cixzhang released this 23 Oct 17:18
704757f

Astryx 0.7.4 — every published @astryxdesign/* package ships at this version.

OLD_VERSION=0.7.3  # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.4 @astryxdesign/cli@0.7.4  # plus every other published @astryxdesign/* package you use, at 0.7.4
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .

@astryxdesign/core

New features

  • BottomSheetSwitcher now gives every step its own native <dialog> while preserving one scrim, focus containment, scroll locking, Escape handling, and focus return. The public props and defaults are unchanged. Its forwarded ref now follows the dialog that hosts the active step. (#7720)

Fixes

  • Busy controls now announce “Loading” once through the shared polite announcement channel instead of competing with a second live Spinner. This covers TextInput, TextArea, Selector, MultiSelector, CheckboxInput, CommandPalette, DateInput, DateRangeInput, DateTimeInput, TimeInput, FileInput, DropdownMenuSubMenu, Thumbnail, ChatMessageList, ComplexSelector, Typeahead, Tokenizer, and BaseTypeahead. Announcements made inside an open modal stay inside that modal. (#7920, #7930, #7939, #7942, #7948, #7950, #7954, #7956)

  • ChatComposerInput announces a trigger menu’s visible loading state once, while cached results that arrive before the loading row appears stay silent. (#7900)

  • When a focused Carousel arrow reaches the end and becomes unavailable, focus moves to the scroll area without undoing the scroll. (#7926)

  • CommandPaletteItem exposes highlighted, selected, and disabled states on its theme target:

    .astryx-command-palette-item[data-highlighted] { /* … */ }
    .astryx-command-palette-item[data-selected] { /* … */ }
    .astryx-command-palette-item[data-disabled] { /* … */ }

    Rendering, semantics, and props are unchanged. (#7967)

  • A busy DateInput keeps its ordinary appearance and shows its Spinner; only a disabled field is dimmed. (#7968)

  • Selector, DropdownMenu, and ContextMenu options inside open, closed, or nested shadow roots now commit when clicked. (#7914)

  • useTypeahead clears its pending type buffer when its component unmounts or becomes hidden, leaving no timer behind and restoring with an empty buffer. (#7938)

Documentation

  • BottomSheetSwitcher anatomy now describes the per-step dialog structure. (#7720)

@astryxdesign/cli

Experimental icon sets

Integrations can ship an icon set, projects can select one, agents can find icons by meaning, and copied templates can replace declared icons with the selected set. The feature is isolated under astryx experimental and does not change stable search results or template output unless a project opts in with experimental.iconSet. (#7759)

astryx experimental icon --list
astryx experimental icon "delete"
astryx experimental icon use <set>

New features

  • Docs, schema fields, and enum values can declare stability: 'experimental'. The CLI labels experimental commands, keeps experimental docs out of stable search, and tells integration authors when an older supported CLI would drop the descriptor. (#7759)
  • Docs can render commands for each reader’s package manager with {@command …}, {@pm …}, and {@pm-only …} tokens. integration check --next can validate them before their findings become warnings in the next minor release. (#7868)
  • An integration doc can replace or extend an existing guide by route and place a section before or after another section. Reads name the contributing package, and unresolved placement is reported as a non-failing note. (#7936)
  • astryx doctor --built <path> checks a finished project, output folder, or HTML file for Astryx use and reports source-level purity issues when source is available. It reads files only and does not execute them. (#7886)
  • astryx theme palette generate receipts now identify the generator and candidate format without changing existing receipt fields. (#7678)
  • Integration validation reports what examples need to run wherever they are shown, including package availability, exported subpaths, props, browser-only dependencies, assets, and runnable code. These are notes today and become strict warnings in the next minor. (#7891)
  • Project agent docs written by astryx init now tell agents to run astryx doctor --built before delivering a build when the installed CLI supports it. (#7944)

Fixes

  • astryx doctor --built no longer counts type-only imports as shipped Astryx use. (#7945)
  • astryx discover results name their source package, and typo-tolerant lookup now matches astryx search. (#7147)
  • astryx search finds family members by their spelled-out names, lists representative members for a family-name query, and orders tied results by fit instead of alphabetically. (#7927, #7955, #7959)
  • astryx integration verify now reads the tarball filename npm 8 actually writes for scoped packages. (#7933)

Documentation

  • Four block templates now say what they show and when to use them. (#7963)
  • BottomSheetSwitcher templates describe the new per-step native dialogs. (#7720)
  • The CommandPaletteItem showcase describes its rendered command and shortcut rows. (#7967)
  • Lab ListInput showcase blocks are discoverable as templates. (#7953)
  • Six guide titles now use plainer, consistent sentence case without changing their routes. (#7934)

Themes

Butter, Gothic, Matcha, Stone, and Y2K move their maintained light and dark themes to reviewed named palette stops. Public compatibility palette objects and metadata stay available, and apps can preserve the previous appearance by overriding the affected color, syntax, shadow, and component tokens. The CLI templates ship the same mappings. (#7669, #7670, #7671, #7672, #7675)

Contributors

Thanks to everyone who contributed to this release:

@cixzhang @ernestt @HelloOjasMutreja @imdreamrunner @josephfarina @rubyycheung @vjeux

Full Changelog: v0.7.3...v0.7.4

Astryx v0.7.3

Choose a tag to compare

@cixzhang cixzhang released this 22 Oct 17:30
ca439c8

Astryx 0.7.3 — every published @astryxdesign/* package ships at this version.

OLD_VERSION=0.7.2  # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.3 @astryxdesign/cli@0.7.3  # plus every other published @astryxdesign/* package you use, at 0.7.3
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .

@astryxdesign/core

New Features

  • DropdownMenuSubMenu takes header and footer, as the root DropdownMenu does, so a sub-menu can have a filter field above its rows, or an action such as "New folder" below them, outside the sub-menu's menu itself. With either, the sub-menu's surface (the flyout, or the drilled-in view on a phone) is a modal dialog named by its row: the row says aria-haspopup="dialog", Tab and Shift+Tab walk the header, the rows and the footer and wrap, Escape returns to the row, and a pointer leaving the flyout closes it only once focus leaves too. A ContextMenu does not show a sub-menu's header or footer. Without them nothing changes.

     <DropdownMenuSubMenu
       label="Move to"
    +  header={<TextInput label="Filter folders" isLabelHidden value={query} onChange={setQuery} />}
    +  footer={<Button label="New folder" onClick={createFolder} />}
     >
       <DropdownMenuItem label="Folder A" onClick={() => move('a')} />
     </DropdownMenuSubMenu>

Fixes

  • TimeInput's arrow keys ignore a fractional increment instead of writing a malformed time such as "14:30.5": adjustTime returns the time unchanged for a fractional minute delta (#7823).

  • Screen readers now announce a Button that is first rendered with isLoading (for example after a remount). Its visually hidden loading live region mounts empty and gets "Loading" on the next frame, because screen readers skip a live region that already holds its text when it appears. A Button that becomes loading after it mounts is unchanged, and a Button mounted idle does no extra render.

  • ChatComposer: screen readers now announce a status message, errors assertively and warnings politely, once per occasion: a rerender does not repeat it, and a status that clears and then returns is announced again. Before, the status bar mounted as a role="alert" or role="status" region already holding its text, which screen readers often skip. The message now goes through the shared announcement channel, and the visible bar no longer carries a live role. No API changes.

  • ChatSystemMessage: a new chat notice inside a ChatMessageList now has one live region announcing it, the list's polite log, instead of also being a second live region nested in it (WCAG 2.2 SC 4.1.3). Its root no longer forces role="status", in either variant, so date and divider rows are no longer presented as status messages, and a role passed to the component reaches its root. Tests that found a system message by the status role should query it by its text instead.

  • Keyboard and screen-reader users can now reach disabled CheckboxInput, CheckboxList items, Switch, Slider and FileInput, and hear that they are disabled. Before, they rendered the native disabled attribute unless a disabledMessage was set, so Tab skipped them. Now every disabled control exposes aria-disabled="true" and keeps the Tab stop it has when enabled, while toggling, selecting, dragging, opening the file picker and any onClick stay blocked, and its value is still left out of form submission. A focused disabled checkbox, switch, slider thumb or file input shows its keyboard focus indicator. No API changes.

  • The agent guide that global mode writes to the home folder now tells agents to deliver a page built with Astryx, not only how to find things. Before, agents that found the right template could still build the page with a tool that cannot install packages and deliver a page with no Astryx in it. The guide now tells agents to work in a project folder (creating one if there is none); add @astryxdesign/core and @astryxdesign/cli to a new project or one without Core, or, in a project that already has @astryxdesign/core, the CLI at the same version; run astryx init there; follow the same template-first workflow and self-check the project agent docs give; and end by building the project and delivering that build. The guide stays version-free and names only commands every release keeps. The project agent docs are unchanged, and so is what @astryxdesign/core's install script prints: its copy of the shared agent-docs file only gains the same workflow lines.

    Also released in @astryxdesign/cli.

  • Keyboard and screen-reader users can now reach a disabled ComplexSelector and hear that it is unavailable (WCAG 2.2 SC 2.1.1 and 4.1.2). Before, its trigger rendered the native disabled attribute, so Tab skipped it and a focused trigger lost focus when disabled; now it exposes aria-disabled="true", keeps focus, shows its keyboard focus outline, and still opens nothing. A press or key on it reaches no handler: not the toggle, not a caller's onClick or onKeyDown, and not an ancestor's click handler.

  • Keyboard and screen-reader users can now reach a disabled Calendar day or Pagination dot and hear that it is disabled. Disabled TreeList rows' expand toggle and action button, disabled Pagination dots and disabled Calendar days expose aria-disabled="true" instead of the native disabled attribute, while clicks, Enter and Space still do nothing and arrow keys still skip disabled days as before. No API changes.

  • DateInput: keyboard and screen-reader users can now reach a disabled date field and hear that it is disabled. A disabled or busy field no longer renders native disabled: its input and calendar button keep their place in the focus order and expose aria-disabled, while typing and opening stay blocked, and a focused disabled field shows its focus ring. Tests that query a disabled DateInput with toBeDisabled() or :disabled should check aria-disabled="true" instead.

  • DateRangeInput: keyboard and screen-reader users can now reach a disabled field or preset and hear that it is disabled. A disabled or busy field and a disabled preset no longer render native disabled: the trigger, calendar button and presets keep their place in the focus order and expose aria-disabled, while opening and selecting stay blocked, and a focused disabled field shows its focus ring. Tests that query them with toBeDisabled() or :disabled should check aria-disabled="true" instead (#7866).

  • DateTimeInput: keyboard and screen-reader users can now reach a disabled or busy field and hear that it is disabled. It no longer renders native disabled on its text, native-picker or touch fields, or on its calendar and clock buttons: they keep their place in the focus order and expose aria-disabled, while typing, picking, opening and a caller's onClick stay blocked, and a focused disabled field shows its focus ring. TimeInput's native picker button, which shares the time segment and stays out of the tab order, also moves from disabled to aria-disabled and stays inert. Tests that query a disabled DateTimeInput with toBeDisabled() or :disabled should check aria-disabled="true" instead.

  • Apps that use Core's prebuilt CSS without a StyleX build step now get the default data colors: the prebuilt CSS defines the canonical dataVars defaults, so compiled imports and raw CSS references paint correctly.

    Also released in @astryxdesign/cli.

  • Dialog, BottomSheet and BottomSheetSwitcher stay open when a file picker inside them is dismissed. A file input fires a bubbling cancel event when its picker closes without a choice, and all three took that event for a close request of their own and closed (a switcher closed its whole flow). They now act only on a cancel aimed at their own <dialog>, so Escape and platform close requests (the Android back gesture) behave as before. No API changes.

  • DropdownMenuRadioGroup now forwards ref, and its docs explain controlled selection, how to name the group, when the menu closes, and how it composes inside a DropdownMenu.

  • astryx init writes the agent docs at the project root, in files the project commits, so Codex, Gemini CLI, Copilot CLI and Claude Code all read the block from the project root or any folder below it (#7497). It no longer writes agent docs into a project's subfolder, the home folder, or a settings folder in it such as ~/.gemini.

    • AGENTS.md holds the block, created when missing. Before, a project with only CLAUDE.md got the block only there, so Codex and Gemini CLI never saw it.
    • Claude Code skips AGENTS.md in a folder that has a CLAUDE.md or .claude/CLAUDE.md, so the Claude Code file the project already has keeps a full copy: root CLAUDE.md, else .claude/CLAUDE.md. It keeps the copy even when it imports AGENTS.md, because Claude Code does not follow that import in a session started in a subfolder; started at the root, Claude Code then reads two. When both exist, the copy goes in root CLAUDE.md, which projects commit even when they ignore .claude/, and a block in .claude/CLAUDE.md is removed. Init creates no Claude file. astryx upgrade refreshes every copy, except in a project at the top of a home settings folder (see the next entry).
    • Gemini CLI reads only GEMINI.md: init adds a one-line import, @./AGENTS.md, creating GEMINI.md when it is missing. It adds none when GEMINI.md already imports AGENTS.md, inline or on its own line. It adds the import even when the project's .gemini/settings.json has Gemini read AGENTS.md through context.fileName: Gemini loads those settings only when it starts at the project root, so a session started in a subfolder would otherwise get no copy. Started at th...
Read more

v0.7.2

Choose a tag to compare

@cixzhang cixzhang released this 21 Oct 16:23
14c0ec6

Astryx 0.7.2 updates the stable @astryxdesign/* package set to this version.

OLD_VERSION=0.7.1  # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.2 @astryxdesign/cli@0.7.2  # plus every other stable @astryxdesign/* package you use, at 0.7.2
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .

@astryxdesign/core

New Components

  • Promote Drawer and DrawerHeader from the canary-only Lab package to Core, imported from @astryxdesign/core/Drawer or the package root. The stable package now ships the full-height side panel with modal and non-modal presentation, the shared dismissal stack, Dialog's purpose dismissal policy, a DrawerHeader whose close button appears when given onOpenChange, and Dialog-style container padding (--spacing-4 by default, padding prop, or a theme's drawer padding), plus Core documentation and runnable examples. Canary users importing from @astryxdesign/lab switch to @astryxdesign/core/Drawer.

    Also released in @astryxdesign/cli.

New Features

  • An integration can set the command its users run the CLI with: a new cliCommand field in astryx.integration.*, such as cliCommand: 'acme astryx'. In a project that loads that integration, every command the CLI prints starts with it instead of the package-manager prefix (npx astryx, pnpm exec astryx): next-step hints, errors, suggestions, Doctor fixes, --help, the setup reminder, the agent-docs block, and doc text. If two loaded integrations set different commands, neither is used, and the new cli-command check in astryx doctor warns. An invalid cliCommand is ignored with an invalid_cli_command warning; the rest of the manifest still loads.
    Docs, including an integration's, are now written with bare astryx ... commands, and astryx docs prefixes them for the project when it prints them. JSON output keeps every command bare, as before. The install-time setup reminder from @astryxdesign/core and @astryxdesign/cli names the command the project's package manager runs, instead of always npx @astryxdesign/cli.

    Also released in @astryxdesign/cli.

  • Adds the @astryx.diffViewer.* keys to the en catalog so the lab DiffViewer reads its file-status badges, line counts, line-number button labels, per-line "Added"/"Removed" labels, and empty-state messages from the translation runtime. No existing string changes; locales without a translation fall back to English.

  • Item accepts align="first-line", which centers each start and end slot on the label's first line. A small status dot, radio, checkbox, or timestamp beside a wrapping label now stays level with the first line, where center drops it to the middle of the row and start leaves it above the line. A slot taller than the first line moves the text down rather than leaving the row, and a one-line row with no description looks the same as center. ListItem, RadioListItem, and CheckboxListItem accept the same align and pass it to their row. Themes can style these rows through the item target's align axis. The default stays center, so existing rows don't change.

  • i18n catalog: add the English messages used by the Lab CommentComposer for its input, image upload, inline quote, emoji action, cancel action, and retry affordance.

  • i18n catalog: add the English messages used by the Lab CommentThread for replies, reactions, edit history, resolution, errors, comment actions, and the unsaved-draft warning it announces when a dismissal would discard a reply or edit.

  • i18n catalog: add the English messages used by the Lab Comments panel for its region, empty/loading states, pagination, and posting errors.

  • i18n catalog: add the English messages used by the Lab EmojiPicker so locale catalogs and provider overrides can replace its labels, search status, loading state, and group names.

  • List: add edge to choose where each row's text, dividers, and hover and selection backgrounds sit. padded (default) keeps today's look. flush drops the rows' side padding, so text and dividers start on the same line as the list header and a heading above, with no container needed. outset keeps backgrounds' room around the text while reaching into the padding of a Card, Section, or Dialog, so text lands on the heading's line. bleed reaches rows to the container's edges with square corners, like Table rows in a Card. edgeCompensation="inline" is deprecated in favor of edge="outset" and keeps working; its rows' outer room is no longer cut off by the list's swipe clip. Run astryx upgrade to rename it.

  • Pagination: variant="compact" shows first/last buttons around previous/next when hasFirstLast is true and the page count is known. Compact still omits them by default, and the input variant still shows them unless hasFirstLast is false.

  • reset.css: in Chromium, the dropdown marker of an <input> with a list sits in the middle of the field. Chromium sets the marker on the field's line-height, so above a line-height of 1 it sits low, and the reset makes every field inherit the page's 1.5: the marker sat 3.5px below the middle of a 40px field, and further down on taller lines. Date and time fields, whose calendar icon uses the same part, keep their icon where it was. To keep the old placement, add input[list]::-webkit-calendar-picker-indicator { line-height: inherit; } after the reset.

  • SideNavItem: nested items now span the full width of the nav list, like top-level items, so their hover and selected fill lines up with the rows above them. Before, a nested item started 24px in from the list edge. The indent now sits inside the row, one --spacing-6 step per nesting level, so icons and labels stay exactly where they were. The indent area is now part of the nested row, so it shows hover feedback and clicking it activates the item. Content placed directly inside a SideNavItem that is not itself a SideNavItem is no longer inset by the children group. There is no supported way to keep the old inset look.

  • TabMenu passes host attributes and DOM event handlers to its trigger button. The trigger keeps its button semantics, tab stop, and menu disclosure attributes; a caller's onClick runs after the menu toggle.

  • Tables draw a line on every cell edge, not only between rows, on both Markdown and the RichText surfaces, and the grid closes at the table's rim. A reader following one row across never needed vertical lines, but someone editing a table has to aim at a cell and judge where a selection stops, and the empty 3×3 a toolbar insert produces was three horizontal rules with no sign it had three columns. The line is the same --color-border already used between rows, the rim merges with it into one rule rather than doubling at the edge, and both surfaces move together — the read and edit renderings match block for block, more closely than before.

    Also released in @astryxdesign/richtext.

  • TreeList: nested rows now span the full width of the list, like root rows, so their hover and selected fill lines up with the rows above them. Before, a nested row started at its indent. The indent now sits inside the row, so chevrons, icons and labels stay exactly where they were. The indent area is now part of the row, so it shows hover feedback, and clicking it activates the row: clicking a folder's indent toggles the folder, and clicking a row whose only action is its link anywhere outside its label follows the link, as Enter already did: modifier keys are kept, so Cmd/Ctrl-click opens a new tab, and a middle-click opens one too. Controls in a row's start or end content keep their own clicks, including role-based ones such as a role="switch" element, which no longer also toggle the row or run its onClick. Guide lines draw above the rows, so a hover or selected fill doesn't break them, and they let clicks pass through to the row underneath. The one exception is a row with keyboard focus: it draws above the guides so they never cut its focus ring, which means that if it is also filled (selected or hovered), its fill covers the guides at that row. Padding set on a row through the tree-list-item theme target or an item's className, style or xstyle still composes with the indent. --tree-list-indent still retunes the indent step. There is no supported way to keep the old inset rows.

Fixes

  • Allow MetadataList label width without an explicit position while preserving single- and multi-column label defaults (#6310)

  • AppShell: defer hiding the mobile nav's Activity boundary until its close animation finishes (#5701)
    AppShell mounts the mobile nav drawer inside a React.Activity boundary that flipped to mode="hidden" in the same commit as the drawer closing. React's Activity hides its subtree with display: none !important synchronously and re-runs the cleanup of every effect inside it, which cut off MobileNav's own slide-out CSS transition and cancelled its delayed dialog.close() before either could run. The drawer still closed correctly (a safety-net effect force-closed the native dialog on that same cleanup), just with the close animation skipped entirely.

    AppShell now holds the Activity boundary visible for a beat after the drawer closes, strictly longer than MobileNav's own worst-case close delay, so MobileNav's normal close effect gets a real chance to run its transition and its own timed dialog.close() before Activity ever hides the subtree.

    MobileNav also keeps its dialog rendered from the commit that closes it until its delayed close() has run, instead of relying on the dialog's discrete display transition to do so. A browser that does not hold that transition (WebKit) dropped the dialog to display: none at once while it was still an open modal, so for about 200ms nothing was visible and the page stayed inert. The slide-out itself is unchanged.

  • `Aspe...

Read more

Astryx v0.7.1

Choose a tag to compare

@cixzhang cixzhang released this 15 Oct 19:21
44f7244

Astryx 0.7.1 updates the stable package set, including RichText's first stable release.

OLD_VERSION=0.7.0  # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.1 @astryxdesign/cli@0.7.1  # add each other 0.7.x @astryxdesign/* package you use at 0.7.1
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .

@astryxdesign/core

New Features

  • Chat: mention people and topics by multi-word names with opt-in hasMultiWordQuery on ChatComposerTrigger (#6128)
    Set hasMultiWordQuery: true on a trigger whose suggestions contain spaces, and typing @Jordan Le keeps narrowing the menu instead of closing it at the first space. The query runs until a line break or about 64 characters, typed text is never changed unless a row is picked, and an unmatched multi-word phrase hides the menu unless the trigger sets emptySearchText. Triggers that don't opt in still end their query at a space.

    For every trigger: while results for the current query are still loading, Enter sends the message as typed and never picks a row from an earlier query. Results that arrive after a send or after the editor clears are dropped, so the menu never reopens over an empty composer. Escape and the arrow keys pass through while the menu is hidden, and IME keystrokes never pick a row.

  • CodeBlock gains isDiff, a diff layer over any language, and Markdown renders a fenced diff code block as tinted diff rows instead of plain text. (#7389)
    With isDiff, lines starting with + sit on a success-tinted row, - on an error-tinted row, @@ hunk headers (and a --- /+++ file-header pair directly before one) on a quiet muted row, and the rest of every line keeps the language's syntax highlighting. The glyph renders in its own column so the code's indentation lines up on every row and stays in the copied text. A changed row's margin reads the change: the tint runs under the line-number gutter too and spans the full scroll width of a long line, and the line number and the glyph take the row's marker colour, while the code keeps the syntax theme's colours. These are five new syntax-theme slots — --color-syntax-added, --color-syntax-removed, --color-syntax-diff-header (row paints, defaulting to --color-success-muted and --color-error-muted at 25% strength and to --color-background-muted; a defined syntax theme that leaves the header paint unset gets none, its header rows sitting on its own background in its punctuation colour) and --color-syntax-added-marker, --color-syntax-removed-marker (defaulting to --color-text-green and --color-text-red in the default syntax theme; a defined syntax theme that leaves them unset keeps its own line-number colour) — so existing and custom syntax themes need no change and may override them.

    Markdown maps a diff-<lang> code fence to the layer over <lang> (labelled <lang> diff) and a diff code fence to the layer over plaintext (labelled diff), so a diff keeps its header, copy button, disclosure, and a viewport name that still says diff.

    A diff code fence that should stay plain text can use text. A custom components.code renderer is unchanged: it keeps receiving the fence's leading word characters in language (diff for both diff and diff-tsx, as today), so an existing language === 'diff' check still matches, and receives nothing new.

  • Clearable inputs: the clear button's tooltip now reads just "Clear" (localized) instead of repeating the field's label, so a long label no longer turns into awkward tooltip copy such as "Clear Assigned engineering partner". The button's accessible name is unchanged and still names the field, such as "Clear Search". Applies to every input that renders the shared clear button, including TextInput, NumberInput, the date and time inputs, FileInput, Selector, MultiSelector, Typeahead, and Tokenizer. Override the new @astryx.inputClearButton.tooltip catalog key to change the wording. (#7476)

  • Markdown fenced code blocks wrap long lines instead of scrolling them sideways. A reader on a narrow screen sees the whole line without a horizontal scroll, and where Markdown and RichText give a code block the same width, a long line takes the same number of lines in both. Only the appearance changes: the code's text, line numbers, syntax colours and what Copy returns are unchanged, and a code block whose lines no longer overflow also no longer takes the scroll region's Tab stop, as a code block that fits never has. To keep the old sideways-scrolling look, render code yourself through the components.code prop with a CodeBlock that leaves isWrapped off. (#7564)

  • Selector, MultiSelector, and ComplexSelector gain a secondary variant: a borderless, neutral-filled trigger matching secondary buttons in navigation and toolbar compositions. Inside an InputGroup, a secondary Selector or MultiSelector keeps its fill and takes the group's row height and connected edges. Existing input (default) and ghost behavior is unchanged. (#7252)

Fixes

  • Typeahead, Tokenizer, and PowerSearch: focusing an empty field with hasEntriesOnFocus after its search source changed now bootstraps entries from the new source. Previously the results cached from the old source were already invalidated but still blocked the bootstrap, so focusing or clicking into the field opened nothing until the user typed. Cached results that are still current are re-shown without a new bootstrap, as before. (#7157)

  • BottomSheet returns focus without a focus ring after a pointer close (a tap in WebKit, or a pointer close of a sheet opened from the keyboard) and keeps the ring after a keyboard close, including on a DropdownMenu bottom-sheet trigger the caller renders (renderTrigger). The sheet now asks the browser not to treat that focus as keyboard focus, the way Dialog's focus moves do. (#7388)

  • Keep DateInput's calendar selection empty after clearing the field. (#6935)

  • Calendar: PageUp and PageDown move the focused day by a month — into the month the grid shows, on the same day of the month where it exists — instead of by a week; a page from a borrowed adjacent-month day lands on that day of the month now shown rather than on the month's first day. With two months, a page or an arrow crossing a month edge from the first month now lands on the correct day (the second grid no longer overrides the first grid's target). An arrow or page past min/max changes nothing, and a page into a month with no enabled day keeps focus in the grid. (#7467)

  • Calendar: the arrow keys, Home and End no longer land on a borrowed adjacent-month day (shown dimmed, exposed as disabled, and inert to Enter, Space and clicks). An arrow crosses into that month and focuses the real day there, including across the two grids of a two-month view; Home and End stop at the first or last enabled day of the week in the month shown. A month with no enabled day holds no Tab stop. (#7511)

  • Calendar with hasWeekNumbers: the blank cell above the week-number column is now a columnheader named with the locale's own word for the week field (from CLDR, visually hidden), so every cell in the header row has a grid role and assistive technology reads the weekday header row without a stray unnamed cell. (#7509)

  • ChatMessage no longer leaves empty space for an avatar, name, or metadata that renders nothing. Booleans and empty strings create no slot, and any other slot content that renders nothing, such as an empty Fragment or a component that returns null, collapses its wrapper. A name that renders no text falls back to the translated sender label, while numeric values such as 0 still render. (#6597)

  • CodeBlock's code viewport is a keyboard Tab stop only while its content actually overflows, through the shared scroll capability: a short snippet no longer adds an unexplained stop to the page's Tab order, a wide or capped block still does, and a viewport that stops overflowing keeps focus where it is. Role and accessible name are unchanged. (#7424)

  • CommandPaletteInput now exposes the semantics and name that its visible state promises. (#7321)

    • A standalone field with no popup attributes was role="combobox" with aria-expanded="true" and aria-autocomplete="list"; it is now role="searchbox" with no popup state. Pass role="combobox", or your popup's own ARIA attributes, to keep the old semantics. A field that already passes popup attributes (aria-expanded, aria-controls, aria-owns, aria-haspopup, and the like) stays a combobox and keeps them, and a field inside CommandPalette is unchanged.

    • The field's default accessible name was its placeholder (Search…, or the caller's placeholder); it is now Search commands, translated. Pass label to keep a specific name, for example label="Search…" for the old default. A caller whose custom placeholder served as the name keeps working code and hears the generic name until it passes label.

    • The CLI's async-search CommandPalette block names its field Search files.

    • endContent={true} no longer renders an empty trailing slot, and endContent={0} renders inside the trailing slot instead of after it.

    Also released in @astryxdesign/cli.

  • A standalone CommandPaletteItem now keeps a caller-supplied id on its rendered option and ignores click callbacks while disabled, so labels and test hooks keep pointing at the option and a disabled command cannot run. (#7324)

  • CommandPalette now keeps dismissal, text entry, result focus, and empty-state announcements aligned with what the reader sees. (#6950)

    • While no selectable result exists, the results container no longer exposes an empty listbox, and the query field reports aria-expanded="false" with no aria-controls or aria-activedescendant. It was an expanded combobox controlling an empty listbox.

    • Home and End move ...

Read more

Astryx v0.7.0

Choose a tag to compare

@cixzhang cixzhang released this 13 Oct 01:19
5c4c56b

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.

@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 ever...

Read more

Astryx v0.6.8

Choose a tag to compare

@cixzhang cixzhang released this 10 Oct 19:34
b5cec38

Astryx v0.6.8

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

Core

New features

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

Fixes

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

CLI

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

Contributors

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

Astryx 0.6.7

Choose a tag to compare

@cixzhang cixzhang released this 09 Oct 18:57
a1e15fa

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

npx astryx upgrade --apply

@astryxdesign/core

New Features

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

  • Drawer is a container, like Dialog. A new padding prop takes a spacing step, and a theme's padding on drawer pads the drawer's scrolling content area through container tokens instead of padding the panel. The padded content area publishes its inset, so a Section that is the drawer's only child, and bleed children such as Table and Divider, align against it, and a Layout inside picks the value up for its header, content, and footer regions. With neither set, the inset is --spacing-4, as in Dialog; pass padding={0} for a full-bleed content area. The block-end safe-area inset is preserved in every mode.

  • An option can carry a secondary action; MultiSelector renders it in a grid.
    SelectorOptionData and SearchableItem gain action?: ReactNode: one node the caller renders and names — an IconButton, a Button, a menu trigger. In MultiSelector, once any option carries one the popup is a role="grid" whose rows pair the option with its action: Up/Down move rows, the inline-end arrow reaches the action (following RTL), Enter fires it, pointer and touch press it directly, and pressing it never changes the selection. The trigger advertises aria-haspopup="grid". Nothing changes for options without an action. Selector and the typeahead panel do not render the key yet and warn in development when an item carries one.

  • useTableRowExpansion accepts panelVariant, and the detail panel now sits on the surface behind the table by default instead of on a wash of its own. (#5995)
    The panel row painted --color-background-muted unconditionally, and being a <tr> the plugin builds itself, nothing a caller rendered could reach it.

    The panel is the row's continuation, not a surface of its own, so it now takes whatever the table sits on — the same thing the row does. That keeps the plugin unopinionated about the table's background: a table on a Card no longer stacks a third surface, and a striped table no longer paints a band in the same token as its own stripe, which read as a data row rather than as a detail.

    panelVariant: 'muted' keeps the wash for the case that wanted it — a bare table with no card, no dividers and no striping, where nothing else separates the panel from the data around it.

    This changes the default appearance. A table relying on the wash gets it back with panelVariant: 'muted'; in dark themes there is nothing to get back, because --color-background-muted is a low-alpha near-black that is close to invisible over a dark card — dark has effectively been rendering transparent all along.

  • useTableRowExpansion accepts hasRowClickExpansion, so a row opens when you click anywhere on it and not only on its chevron. (#5995)
    useTableTreeData has had this since it shipped, under the same name and with the same behaviour. The detail-panel plugin is the other half of the same pair — one expands into child rows, one expands into a panel — and a caller who moved between them lost whole-row clicking without anything saying why. There was no way to add it back either: a click handler on the row has to know not to fire on a checkbox, a link or the end of a text drag, and none of that is reachable from the outside.

    Off by default, and pointer-only when on. The chevron button stays the accessible control, so keyboard and assistive-tech users are unaffected — this adds a shortcut for a mouse, not a second way to operate the table. Clicks that land on interactive cell content, clicks that end a text selection, and clicks on rows getIsItemExpandable has ruled out all pass through untouched. The chevron already stops propagation, so it does not toggle twice.

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

Fixes

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

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

  • BottomSheet no longer blocks pinch-zoom. A pinch that started on an open sheet (its handle, its content, or across both) did nothing, because the sheet claimed every touch gesture: touch-action: none on the sheet and handle, pan-y on the content. The sheet now leaves pinch to the browser (pinch-zoom, and pan-y pinch-zoom on the content), and a second finger (on the handle, on the content, or off the sheet, whichever lands first) ends any sheet drag in flight or about to start, so the page zooms and the sheet stays at its detent. One-finger drag and scroll are unchanged.

  • A BottomSheet release is judged where the sheet would coast to at the finger's speed, not where the finger left it: a medium throw dismisses from short of the dismiss line and a gentle throw lands on the next detent instead of snapping back, the release speed is read over the finger's last 100ms rather than its last two samples, and a finger that rests before lifting releases no throw. A nudge under 48px still projects nothing.

  • A DropdownMenu opened by a mouse press returns focus to its trigger when it closes, instead of to the control that was focused before the press.
    A mouse opens the menu on press-down, before the browser's own mousedown has focused the trigger, so the popover remembered the previously focused control and handed focus back there on Escape or after a pick. The trigger now takes focus as the press opens the menu, the state a click-open already had; keyboard opens, taps and the menu's own behavior are unchanged.

  • A field status shown with statusVariant="tooltip" is now announced to screen readers when it appears or changes, the same way the attached and detached message boxes are: errors assertively, warnings and successes politely.
    Before, the tooltip placement only described the control, so a screen-reader user was not told that a validation message had appeared until they left and re-entered the field. This affects every input that offers the tooltip placement. The attached and detached placements are unchanged and are still announced once.

  • FileInput: invoke changeAction and update optimistic state on clear and file selection

  • swizzle: rewrite every .stylex import to a deep path
    astryx swizzle Button (and any component that imports a .stylex module from outside its own directory) emitted an import that collapsed to the directory barrel — e.g. @astryxdesign/core/utils instead of @astryxdesign/core/utils/interactionOverlay.stylex. The barrel does not re-export those StyleX symbols, so the swizzled file could not compile.

    rewriteImports now gives every *.stylex module the deep subpath. The exports generator (scripts/sync-exports.js) adds 16 specific subpath exports for the .stylex modules swizzled components actually reference across directories:

    DateInput/tokens.stylex, Icon/IconSize.stylex, Indicator/indicator.markers.stylex, Layer/layerAnimations.stylex, Layer/layerTextReset.stylex, Layer/layerViewportInset.stylex, Layout/container.stylex, Layout/edgeCompensation.stylex, Layout/padding.stylex, NavItem/navItemStyles.stylex, Selector/selectorPresentation.stylex, Stack/stack.stylex, Stack/stackItem.stylex, Text/text.stylex, utils/focusOutline.stylex, utils/interactionOverlay.stylex

    These are public API additions. Each module already ships in the npm tarball (in dist/); only the exports map entry is new. No wildcard — a future cross-directory .stylex import needs a deliberate entry in STATIC_EXPORTS. Precedent: ./theme/dataTokens.stylex (AST-066 FR2).

  • Keep grouped-row custom headers full-width while pinning their 16px disclosure button, and expose expansion state on the button. (#6251)

  • Keep grouped-row collapse chevrons pinned without shrinking custom headers, and increase them from 12px to 16px (#6222)

  • Keep layer portals inside their nearest dialog

  • Markdown: read an indented table with its own columns
    Markdown no longer adds an empty first column to a table whose rows are indented, such as | a | b |. The indentation before a row's first pipe used to read as a cell of its own.

  • Markdown: keep a definition-shaped line whos...

Read more

Astryx 0.6.6

Choose a tag to compare

@cixzhang cixzhang released this 07 Oct 23:32
d623c40

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

npx astryx upgrade --apply

@astryxdesign/core

New Features

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

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

  • BottomSheet is a container, like Dialog. A new padding prop takes a spacing step, and a theme's padding on bottom-sheet now pads the sheet's content box through container tokens instead of padding the panel. The padded content box publishes its inset, so a Section that is the sheet's only child, and bleed children such as Table and Divider, align against it. With neither set, the content box stays unpadded as before.

  • ComplexSelector can hang off a control the caller renders.
    A new renderTrigger render prop renders the control the popup hangs off — a glyph in a list row, a chip, an icon button — in place of the selector's own field and button. Spread the given props onto it; the popup is anchored to it, keeps its dialog label from label, opens on click or ArrowDown, and returns focus to the control on close. The existing handleRef and onOpenChange work unchanged beside it. Off by default; existing selectors are unchanged.

  • ContextMenu takes triggerAs (div | span) so a reference inside prose can own a context menu without breaking the text flow.

  • Export the canonical 56-token StyleX dataVars group for CSS-capable data visualization consumers.

  • Export decodeMarkdownCharacterReferences from @astryxdesign/core/Markdown/parser and @astryxdesign/core/Markdown
    It decodes character references the way Markdown renders them — &copy;, &#169;, and &#xA9; become ©; unknown names and references without their semicolon stay as written — using the same table Markdown uses. It works on plain text, so leave code and backslash-escaped references out. The parser subpath has no use-client boundary, so server code can call it.

  • Add DropdownMenuGroup, a titled role="group" of rows for compound-mode menus.
    The items data API could title a group ({type: 'section', title, items}); a menu written with children — checkbox rows, radio groups, rows mounted only while open — could not. DropdownMenuGroup (also ContextMenuGroup and BreadcrumbMenuGroup) renders a role="group" named by its heading through aria-labelledby; the heading shares the data mode's typography and astryx-dropdown-menu-section-heading theme target, is not a menu item, and is skipped by arrow keys and typeahead. Existing menus are unchanged.

  • The message a component shows when a query matched nothing is now emptySearchText everywhere, and it takes a ReactNode.
    Selector, MultiSelector, and CommandPalette already called it emptySearchText and already accepted a node. Tokenizer, Typeahead, BaseTypeahead, and each ChatComposerInput trigger called it emptySearchResultsText and accepted only a string — so the same product could offer a "no results, create one" row in one component and not in its neighbour, and a builder who learned one had to discover the other.

    Nothing breaks. The type only widens, so every existing value stays valid, and emptySearchResultsText keeps working exactly as released. Set both and emptySearchText wins, with a development warning. Migration is the name alone.

    Deprecation lifecycle (spec:AST-017 FR28, FR31) — removal only in a later minor whose frozen manifest carries both ids of a pair:

  • The layer runtime owns the viewport inset: one gutter, one cap, one fallback order, and one place an app declares a floating bar.
    Every anchored layer — Popover, DropdownMenu and its submenus, Typeahead, Tooltip, HoverCard, the selectors — now keeps the same gutter from each viewport edge (the spacing-4 step or the device safe-area inset, whichever is larger) and is capped to the viewport. Four components used to carry their own copies of that gutter, and they had drifted.

  • Add the first-party Markdown heading-links module.
    createMarkdownHeadingLinks() is exported from @astryxdesign/core/Markdown/plugins and gives every built-in h1–h6 a collision-safe generated fragment plus an accessible inline trailing # copy button, including headings nested in blockquotes and lists. Its frozen, versioned entry carries the namespace and safe URL base across compatible Core package copies without module-local state. The heading row uses useContainerReveal: the button is hidden at fine-pointer rest, reveals on row hover or keyboard focus, and follows the canonical coarse/touch behavior. An unmodified tap, click, Enter, or Space copies the canonical URL without navigating, scrolling, or mutating the hash and briefly shows a check confirmation; failures stay silent. The same opaque plugin entry keeps Markdown-derived Outline aligned, supports Unicode NFKC slugs, an optional caller-owned namespace and safe permalink URL base, and leaves default Markdown plus custom heading renderers unchanged.

  • Markdown: nested lists draw a different marker at each depth
    Bulleted lists cycle disc, circle, and square, and numbered lists cycle decimal, lower-alpha, and lower-roman, by how many lists of either kind enclose them, so each level can be told apart from the levels beside it. Numbering keeps each list's start and items. List and ListItem keep their public marker styles.

  • Markdown plugins: read what a plugin declares, and render one plugin node as Markdown does
    getMarkdownPluginCapabilities(plugin), from @astryxdesign/core/Markdown/plugins, reports whether a plugin declares syntax and whether it declares a transform, and nothing else; plugin entries stay opaque. MarkdownPluginNodeRenderer, from the new client-only @astryxdesign/core/Markdown/plugin-renderer subpath, renders one parsed extension node with the given plugins exactly as Markdown presents it — the plugin's renderer inside the same error boundary and suspense fallback, the same readable fallback text, and the same failure report — with no element of its own. Markdown's own output is unchanged.

  • DropdownMenu takes a trigger render prop: hang a menu off any control.
    trigger renders the control the menu opens from — an IconButton, a chip, an avatar, a list row — and hands it DropdownMenuTriggerProps to spread: the press model, the keyboard opens, the toggle click and the ARIA wiring. The menu is named by that control through aria-labelledby. button and trigger are mutually exclusive (a dev warning).

  • Menu arrows wrap and PageUp/PageDown page.
    In DropdownMenu, ContextMenu and DropdownMenuSubMenu, ArrowDown on the last row wraps to the first and ArrowUp on the first to the last, as macOS menus do (Selector keeps clamping like a native select). PageDown and PageUp move to the last and first fully visible row of a scrolling menu, and pressed there again one viewport further, never wrapping. ArrowUp on the trigger opens the menu with the last row highlighted. The key that opened the menu no longer activates the first row through its auto-repeat, and typeahead ignores a key that is part of an input-method composition. useListFocus gains hasPaging.

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

  • DropdownMenu takes menuMaxHeight to lift the 300px cap for a menu that must fit its rows; the viewport still bounds it.

  • Menus and pickers act on the row under the pointer at release, and the highlight follows a held finger or mouse.
    DropdownMenu, ContextMenu, DropdownMenuSubMenu, Selector and the menu bottom sheet share one press model, the one macOS and iOS menus use: the row under the pointer when it is released is the row that acts, and the highlight follows a held pointer across the rows. A finger that lands on one row and lifts on another acts on the second — once; the click the browser aims at the first row is swallowed. A mouse released outside a menu closes it; a finger released outside leaves it open. A menu whose rows fit declares touch-action: none so a slide stays a slide; one that scrolls lets the browser pan it and ends the gesture. Menu rows no longer paint a pressed look where hover does not exist. New public hook: useMenuPress.

  • A mouse opens a DropdownMenu on press and can drag straight into it; a finger held on the trigger opens it with the finger down.
    A DropdownMenu trigger now opens its menu on a mouse press-down, and a drag from the trigger into the menu that lets go over a row picks it, as macOS menus do. The release of the opening press acts only after the pointer has entered the menu or the press has lasted about a third of a second, so a menu that opens under the pointer never picks a row nobody chose. Pressing the trigger of an open menu closes it without reopening in the same gesture. A tap still opens through its click; a finger held on the trigger for half a second opens the menu with the finger down, and a slid...

Read more

v0.6.5

Choose a tag to compare

@cixzhang cixzhang released this 03 Oct 20:40
e730c89

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

npx astryx upgrade --from 0.6.4 --apply

@astryxdesign/core

New Features

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

Fixes

  • Button no longer overflows narrow rows: a labelled button can shrink below its label width and truncates the label with an ellipsis, while icon-only buttons stay square
  • Prevent an empty Tokenizer input from creating a blank trailing row.
  • SegmentedControl's default hug layout is now capped at its container width, and its segments shrink and truncate their labels instead of overflowing narrow cards and phone rows
  • SelectableCard: keep disabled cards in sequential focus navigation with aria-disabled and gated interaction handlers.
  • Selector's one-line trigger matches its size token when the theme's --spacing-5 is taller than the token can hold, instead of overshooting it.
  • Switch: announce busy/loading states through the persistent useAnnounce live region and localize the announcement via @astryx.switch.loading.

@astryxdesign/cli

New Features

  • astryx component accepts several exact selectors in one call. The JSON response keeps one ordered row per selector, including missing and ambiguous components, and text mode prints every row before exiting nonzero when any lookup fails. The component() API accepts selector arrays and always returns component.batch for an array, including empty and one-item arrays. Batches accept up to 100 selectors and reject larger arrays before lookup. The public API also exports shared BatchResponse and BatchRow types for typed receipts.
  • astryx discover browses integrations: the ones a project has and, through discover sources, the ones it could add, with every version and what each one adds. It searches every kind of item and filters with --type, --installed, --available, and --limit. A project sets a source as discover in astryx.config, and an integration exports one as a discover named export. Discover only reads: it prints the command that adds a package and never runs it. Existing --json fields keep their meaning. A free-text query now always lists its matches, even an exact component name, and astryx discover <package>/<Name> opens one.
  • astryx integration verify is the new name of astryx integration pack --check.
    The check you run before publishing an integration now has a name that says what it does. astryx integration verify packs the package with npm, installs the tarball into a temporary app, and checks that the app sees the same components, templates, themes, docs, and codemods. It takes no flags. astryx integration pack --check still works as a deprecated alias: it runs the same check with the same output, JSON, and exit codes, prints a note that names integration verify, and shows as deprecated in help. It will be removed in a later release. The integrationPackCheck() API and its integration.pack-check JSON response do not change. With --json, a command group given an unknown subcommand now reports ERR_UNKNOWN_SUBCOMMAND and lists its subcommands, where it used to say JSON output is not supported.

Fixes

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

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

  • upgrade's filesChanged counts files, not (codemod, file) pairs (#6622)
    One source file that four codemods each changed was reported as four files changed, so filesChanged matched transformsApplied and the documented meaning, "Total files changed", was not true. The human summary said the same thing: "Found 4 changes across 4 files" for one file.

    filesChanged is now the count of distinct files. transformsApplied is unchanged: a code or config codemod counts once for each file it changed, and a project codemod counts once. A file that both a core codemod and an integration codemod changed counts once in filesChanged.

  • A parse error prints the Astryx error format in text mode.
    astryx theme list --lang zh-Hans printed Commander's own line — error: option '--lang <locale>' argument 'zh-Hans' is invalid… — while every other CLI error prints Error: …. --json was already correct (ERR_INVALID_LANG), so the two modes agreed only on the exit code.

    Commander writes that line before any Astryx code runs, so the JSON shim — the one place that already sees every parse failure — now suppresses it and writes the Astryx line itself, from the same message, for both modes. Every parse failure is covered: unknown option, unknown command, missing argument, and an invalid value for a global option. --help and --version are untouched and still exit 0.

  • The CLI reference now matches what the commands do. Every --help ends with the command's examples and a More: line that names its full docs page. Function docs show each parameter's default, mark required parameters, list the error codes each function throws, and use examples that run. The response-type list adds help, version, and upgrade.registry, and astryx manifest now lists upgrade.registry for upgrade. The --zh, --dense, --lang, and --detail descriptions name the commands they change, and command summaries say when to use each command. When astryx template refuses to overwrite a file, it now says to re-run with --overwrite (or -f). The upgrade command page (astryx docs cli/commands/upgrade) now explains which files codemods never edit, what happens when one of them needs a change, and how to regenerate it.

  • astryx integration pack --check now checks the tarball when a prepack, prepare, or postpack script prints to stdout. Before, any lifecycle output made the check fail with "npm pack produced unparseable JSON output" before it looked at the tarball. A failing lifecycle script still fails the check, and its output stays in the pack_failed message.

  • astryx doctor integration docs fails when a namespace doc or a placement fails, as its help says.
    Such a failure hides the doc from the docs tree, so it now exits 1 with an invalid_doc_graph error instead of a warning. A link that names no doc still only warns, since it prints as written. doctor integration docs and doctor integration components also no longer print an [ok] line after a check that failed.

    A mistyped subcommand under doctor now fails and lists the subcommands the group has: astryx doctor integrations used to run the project checks, and astryx doctor integration bogus exited 0 in text though it exited 1 with --json.

  • A package that ships a theme, or a doc section with an id, now declares the CLI that can read it.
    A stable CLI before 0.7.0 rejects both: it cannot read the typed theme descriptors that astryx integration add theme writes, and it rejects a section id. Either way it hides the package's themes or doc topics with no warning. astryx integration add theme now adds "@astryxdesign/cli": ">=0.7.0" to peerDependencies, marked optional, and astryx integration verify fails with themes_need_cli or section_ids_need_cli when a package needs that peer range and does not declare it.

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

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

Other Changes

  • A code block's label now prints above the block instead of as a // label line inside it, so copied bash, CSS, JSON, and HTML stay valid. Table cells escape |, so a union type stays in one column.
  • astryx search dark mode searches for both words; it used to drop every word after the first. A result that matches every word of a query, one of them by name or keyword, now outranks one that matches only some, and a section whose title or heading holds the whole query ranks near the top. Topics can declare search keywords, now a documented ReferenceDoc field, and a namespace's keywords now count too. A query keeps its phrase when common words such as make, build, or an drop out, so astryx search make an integration finds the integration guides, a plural of a doc's name matches it, one step below the exact name, and a component's name typed as words, such as command palette, finds the component. Outside an app, where @astryxdesign/core is not installed, astryx search searches the docs instead of failing, and says so; --type component, hook, or template still needs Core.
  • Snippets that failed when copied now work: StyleX token imports, the fr-FR.json locale path, Tailwind rounded-lg, --color-background-muted, icon and color values, and the Cursor rule path.
  • Claims that did not match the code are corrected: the 30 shipped locales and how RTL mirroring works, what astryx init writes, --detail brief for a shorter read, the Neutral and Matcha fonts, the components that need anchor positioning, gap steps, Card's ...
Read more

Astryx v0.6.4

Choose a tag to compare

@cixzhang cixzhang released this 01 Oct 21:20
a51b1fb

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

npx astryx upgrade --apply

@astryxdesign/core

New Features

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

  • DialogHeader: expose the start- and end-content wrappers as theme targets (#6415)
    Adds dialog-header-start-content and dialog-header-end-content so themes can style the existing content-slot wrappers without relying on their DOM positions. The end-content slot also contains the optional close button. Default layout and behavior are unchanged.

  • DialogHeader title and subtitle now accept any ReactNode, not only strings.
    A title can carry inline markup and still renders inside the focusable h2 that receives focus on open and names the Dialog; the accessible name is the title's text content. A subtitle can carry inline content such as a Link. String callers are unchanged. An empty-string, boolean, or nullish subtitle renders nothing, and a numeric 0 subtitle now renders inside the subtitle text.

  • DropdownMenuItem, DropdownMenuCheckboxItem and DropdownMenuRadioItem forward a ref to the row root (#6687).
    The ref reaches the element carrying role="menuitem" (or menuitemcheckbox / menuitemradio), the way Item and DropdownMenuDivider already forward one, so a menu row can be registered with an element-keyed observer or overlay — an IntersectionObserver for an impression, a measurement, a debug outline — without a wrapper between role="menu" and the row.

  • Add presentation to DateInput, DateTimeInput, and TimeInput (spec:AST-043) (#6628).
    presentation names every picker surface, distinguishing Astryx's desktop surface, Astryx's bottom sheet (including a new TimeInput sheet), the browser/OS picker, and — for TimeInput only — a plain typed field. DateInput and DateTimeInput accept five values: 'popover' | 'bottom-sheet' | 'native' | 'adaptive-bottom-sheet' | 'adaptive-native' (default). TimeInput accepts those five plus 'text-input', the typed field on every pointer, because that is the surface its released nativePicker="never" already was. presentation="native" always shows native; adaptive-native keeps the released native fallbacks.

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

  • List: add edgeCompensation="inline" to compensate for item content inset within container padding (#2626)
    ListItem insets its content by a density-dependent horizontal padding, so a list under a section heading reads as misaligned and consumers reach for negative-margin custom CSS. edgeCompensation="inline" on List cancels, per inline edge, the smaller of that inset and the container's published padding on that edge; zero-padding and full-bleed surfaces therefore stay in place, asymmetric container padding never over-cancels an edge, and headers, hover backgrounds, and selection backgrounds keep their existing geometry.

    Item now publishes its inline inset as --_item-inset-inline and derives its own paddingInline from it, and the clamped cancelling margin reads the same variable — so the cancel tracks density and theme overrides instead of mirroring hardcoded values. Themes that set paddingInline on item feed the variable automatically via the derived var registry.

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

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

Fixes

  • Preserve authored heading and text component styles when theme adaptations change the type scale (#6710)
  • AvatarGroup keeps its overlap when avatars are wrapped in a HoverCard or Tooltip. Every avatar and the overflow indicator now take the overlap margin, and the group pads its start edge to match, so the overlap no longer depends on each avatar being a direct child of the group (#6736).
  • Avatar initials skip punctuation: each word's initial is its first letter, digit or emoji, so Northwind Workbench (automation) renders NA instead of N( and “Ada” Lovelace renders AL instead of “L. Words with none of those, such as a lone -, are ignored, and a name made only of punctuation shows the default icon rather than a stray symbol (#6698).
  • Size an Astryx Icon without an explicit size to match its Button or
    IconButton: 16px for small and medium controls, and 20px for large controls. (#5762)
  • ChatComposerInput: an empty input no longer shrinks by 8px when isDisabled flips to true, which shifted anything bottom-aligned beside it (e.g. a send button in a grid row) (#6654).
    The root's minHeight was set to the shared line-height only, not the padding editable and placeholder both add on top of it — normally immaterial, since the editable region reserves its own padded box even when empty. A disabled, empty contentEditable region stops reserving that empty line at all in Chromium, and the absolutely positioned placeholder standing in for it doesn't contribute to layout height, so the root fell back to just the line-height and lost the padding. minHeight now explicitly accounts for both.
  • ChatComposerInput no longer scrolls for a short single-line draft with maxRows={1}. Removing vertical padding also keeps the empty input and placeholder aligned when disabled. (#6733)
  • Keep numeric zero aligned in ChatMessageBubble name and metadata slots. (#6600)
    The aligned wrappers are omitted for non-rendering scalar values (null, undefined, booleans, and the empty string), while numeric 0 remains visible inside the same inset as other slot content.
  • Render numeric zero when it is passed as the ChatMessageList empty state. A valid ReactNode should not disappear when the transcript is empty. (#6636)
  • Omit empty ChatMessageMetadata slots and their separators (#6637).
    Boolean and empty-string timestamp or footer values no longer leave a blank row or a stray dot. Numeric zero remains visible, and all delivery statuses keep their existing labels and icons.
  • Compose accepted onClick handlers with ChatSendButton's send and stop actions instead of replacing them (#6653). Consumers that used onClick to replace sending should move that logic to onSend, because both handlers now run.
  • Allow long ChatSystemMessage content to wrap within narrow chat layouts instead of crossing the container edge (#6655).
  • ChatTokenizedText now ignores empty token values so tokenized messages always finish rendering (#6679).
  • ChatToolCalls now displays custom group labels, keeps collapsed details out of keyboard navigation, adds visible focus treatment, and uses readable secondary text for neutral metadata (#6680).
  • Stop ChatLayout's scroll-to-bottom button from being a tab stop while it is invisible. At rest the layout's default scrollButton renders hidden, but opacity: 0 and pointer-events: none leave it in sequential focus navigation — so a keyboard user's first Tab into any chat landed on a control with no visible focus indicator (WCAG 2.2 SC 2.4.7), and Enter scrolled the transcript. The hidden state now also sets visibility: hidden, which the fade transition carries so the animation is unchanged; the visible button keeps its keyboard access. The pill's height and collapsed width now track --size-element-md instead of a hardcoded 32px, so a theme that retunes the element scale can no longer make the pill clip its own Button. ChatLayout's consumer docs also gain the density prop, which was undocumented, and drop the claim that density adapts automatically to container width — it never did. A Chromium evidence spec pins the hidden/visible/re-hidden keyboard contract and the pill's containment of its Button under a size-retuning theme, and the RTL applicability ledger records why ChatLayout and its scroll button have no applicable RTL dimension. (#6466)
  • Make a theme that styles chat-layout-scroll-button actually restyle the chat scroll-to-bottom pill. The documented target sat on the invisible full-width row that centres the pill, so a backgroundColor override painted a band across the chat dock while the pill kept the surface the theme asked to replace — and every automated check passed, because the override did reach an element. The target now rides the pill, which is what paints the fill, elevation, and radius (architecture:component-theming-surface INV4). The target keeps its name, stays a single target, and the component's props, DOM shape, ref target, and consumer passthrough are unchanged. A Chromium spec pins the placement and proves the repair by moving the target back and showing the pill go unstyled under a theme. (#6482)
  • Correct CheckboxIndicator theming, replacement-content, and focus guidance (#6745).
  • Align CheckboxInput theming and label-icon guidance with shipped behavior (#6747).
  • A CheckboxList option that is saving through changeAction now keeps its spinner, busy state, and re-toggle guard when another option is toggled before it settles (#6777).
  • Clicking a read-only CheckboxList option that has an onClick now fires that handler once, instead of re-dispatching the click until the browser's call stack ov...
Read more