Repository navigation
Astryx v0.7.8
Astryx 0.7.8 — all @astryxdesign/* packages ship at this version.
OLD_VERSION=0.7.7 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.8 @astryxdesign/cli@0.7.8 # plus every other @astryxdesign/* package you use, at 0.7.8
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New Features
-
In Chrome on Android, on a touch screen, a Tall BottomSheet places the focused field just above the on-screen keyboard when Chrome reports it, then rides the rest of Gboard's slide; through the hide the sheet's scroll room stays while its content rides down, so nothing jumps while the keyboard is still on screen. Under Reduce Motion it cross-fades instead of moving. The browser-support guide says so.
Also released in
@astryxdesign/cli. -
Calendar: weekday headers show the locale's abbreviated names where all seven fit in four characters, and keep the short names otherwise. English now reads
Sun Mon Tueinstead ofSu Mo Tu; French readsdim. lun. mar., and Spanish, Swedish, Norwegian, Romanian, Greek, Serbian, Turkish, German, and Traditional Chinese switch the same way. Portuguese (Portugal), Arabic, Hebrew, Polish, Danish, and Vietnamese, whose abbreviated names are longer, keep their short names. A locale Astryx ships no translations for shows its own names instead of English: the abbreviated names where they fit the same way (IndonesianMin Sen Sel), else the narrow ones (SwahiliS M T). The popovers of DateInput, DateRangeInput, and DateTimeInput follow because they render Calendar. The touch bottom sheets of DateInput and DateTimeInput now show the same names as Calendar in every locale: Portuguese (Portugal) no longer runs whole words such asdomingotogether, and Polish, Danish, Vietnamese, Arabic, and Hebrew switch to their short names there. -
CheckboxListtakesisOptional, asRadioListandCheckboxInputdo. It shows the optional indicator after the group label, so an optional checkbox group can say so, including as the exception in aFormLayoutwithdefaultOptionality="required". UnderdefaultOptionality="optional"the indicator stays hidden, as for every field. A checkbox group takes no required state from a form default: with or withoutisOptional, its checkboxes carry noaria-requiredfrom it, and it adds no nativerequired. -
CheckboxListtakesisRequired. The group label shows the required indicator by the same rules as every input field. While no box is checked, the first box that is neither disabled nor read-only carries nativerequired, so the browser blocks submit and moves focus to it with its own message. Once any box is checked, the form submits. A disabled or read-only group requires nothing. Show your own error text throughstatus. -
CodeBlock adds startLineNumber so code excerpts show real file line numbers
-
A component doc can mark its component deprecated with
deprecated: {replacement, description}: thenameof the component to use instead and the modernization route.astryx searchresults andastryx buildkit entries for that component carry the field asdeprecated(adeprecated:line in their text output), and the docsite shows a Deprecated badge in the component's page header (beside any stability badge) and a notice at the top of its page that names and links the replacement, and labels it Deprecated on its components index tile and in site search.BottomSheetSwitchersets it, namingBottomSheet; the component keeps working unchanged.Also released in
@astryxdesign/cli. -
CheckboxInput can leave its rounded-square press layer off over a first-party replacement indicator that paints its own press. Indicators in this release are unchanged.
-
PowerSearch gains a touch surface through a new
presentationprop,'popover' | 'bottom-sheet' | 'adaptive', typed by the new exportedPowerSearchPresentation, with'adaptive'the default.bottom-sheetmakes the whole field one trigger showing display-only filter tokens, and moves filter editing into a flow of tall modal sheets that stack: each step opens above the one that led to it, and dismissing a step reveals the one beneath, so no sheet has a Back action. A Filters sheet lists the active filters as rows that edit a filter, each with a trailing remove button, under a fixed header with Add filter and over a fixed footer with Clear all and Done; with no filter it shows one centered Add filter. The flow edits a draft of the filter list: adding, editing and removing a filter, and Clear all, change the draft, and Done applies it by callingonChangeonce per edit, in order, each with the list after that edit; a swipe, scrim or Escape close of a changed draft asks before discarding it, a clean one closes directly, and Done on a field that became read-only while the flow was open drops the draft. Clear all asks for confirmation, removes the editable filters, keeps read-only ones, and leaves Filters open. An Add filter sheet lists the fields grouped (searchable from eight fields), or the content search with the field typeahead's suggestions listed inline, and leaves its search unfocused until tapped; a field whose only operator takes no value adds at once. A value step stages operator → value and confirms from its footer, the editor on the sheet's surface, or in muted sections beside the operator choice. Closed vocabularies are full-width rows, numbers stage as typed, andstring_list/entity_listvalues edit through the Tokenizer's own sheet presentation titled with the field label.adaptiveuses the sheet on compact coarse-pointer screens and the popover otherwise, resolved the same way Selector resolves it. The sheet flow loads on demand, only when a field presents it. New@astryx.powersearch.sheet.*catalog keys for the sheets' strings; no new theming target.
The API change is additive: the prop is new and optional, and no existing prop, type or export is removed or narrowed. The behavior changes only on compact coarse-pointer screens (phones), which is what'adaptive'is for: there a PowerSearch with nopresentationshows the trigger and the sheet flow, a filter reachesonChangewhen Done applies the sheet's draft, andmenuWidthandtokenOverflowBehaviorhave no surface, with a development warning that names them. With a mouse or trackpad, on a screen that is not compact, in the server render and while hydrating, nothing changes. Passpresentation="popover"to keep the previous behavior on every device. Shipping this default in a patch is a product decision of the engineering owner, agreed with the owner of the compatibility policy. -
createAnimator(element)and its hookuseAnimator(ref)animate one element with the motion Astryx's components use; the hook takes the element's ref, so a gesture hook can share the element.animatespringsx,y,scale,rotateandopacityto a target on the state-change motion,decaycoasts any of them from a release velocity,setjumps,getandgetVelocityread, andstophalts;animateanddecayreturn a promise. Reduce Motion is planned for every call.createAnimatoris exported from the package root anduseAnimatorfrom@astryxdesign/core/hooks; the CLI's templates gain a playing-cards example of each.Also released in
@astryxdesign/cli. -
Table: let row-expansion and tree plugins include row identities in localized expand and collapse labels.
-
Tokenizer's
presentationprop is stable, and its default is'adaptive'.presentationtakes'popover' | 'bottom-sheet' | 'adaptive', typed by the new exportedTokenizerPresentation. The API change is additive: the prop is new and optional, and no existing stable prop, type or export is removed or narrowed (TokenizerProps['ref']keeps its input type, and the component also accepts a ref typedRef<HTMLInputElement | HTMLButtonElement>). The behavior changes only on compact coarse-pointer screens (phones), which is what'adaptive'is for: there a Tokenizer with nopresentationshows a trigger that opens the bottom sheet, the sheet edits a draft soonChangereports once, on Save,refreceives the trigger button, andrenderItem,renderToken,endContent,hasClear,hasEntriesOnFocus,menuWidth,tokenOverflowBehaviorandhasAutoFocusare not rendered, with one development warning that names them. With a mouse or trackpad, on a screen that is not compact, in the server render and while hydrating, nothing changes. Passpresentation="popover"to keep the previous behavior on every device. Shipping this default in a patch is a product decision of the engineering owner, agreed with the owner of the compatibility policy. -
useTooltipreturnsshow()andhide().show()opens the tooltip and keeps it open untilhide(), Escape, a press outside, or focus leaving its trigger, and its text is announced once.hide()closes it. Both fireonShowandonHide. Under a controlledisOpen, they only call those callbacks. Use them to build a toggle-style trigger: your own button calls them on each press, setsaria-expandedandaria-controls={describedBy}, and keeps its own accessible name. -
On a touch device with a trackpad or a mouse, such as a tablet with a keyboard, the picker wheels in the TimeInput, DateTimeInput and DateInput bottom sheets now take a trackpad's two-finger scroll as a finger: the rows follow the fingers, coast on at their lift, and rest on a row. One mouse-wheel notch turns a wheel one row, as an arrow key does, with no coast. Before, both moved the rows by each scroll's own distance, so a trackpad's momentum ran at the browser's rate and one notch moved several rows. A ctrl-scroll still zooms the page.
-
Under a coarse primary pointer, as on a phone or a tablet, the picker wheels in the TimeInput, DateTimeInput and DateInput bottom sheets now move like UIPickerView. Their rows keep the 28 px pitch and lie flat, fading toward the edges of the column in every browser; the scrolling column under a mouse tilts and fades its rows only where the browser supports scroll-driven animations, and shows a plain list elsewhere. The row under the band is in semibold accent text on its plate while the wheel rests and while a finger drags it; no row is marked while the wheel coasts, snaps or turns. A flick coasts at UIKit's deceleration onto the row UIKit's projection picks. A finger landing on a moving wheel stops it. A tap on a row turns it onto the band, and a value changed from outside while the sheet is open, as when a bound clamps the time, turns the wheel onto its row. Past the first or last row, a drag rubber-bands and the wheel bounces back. As on iOS, the value commits when the wheel comes to rest, and the coast keeps running while the page's main thread is busy. Arrow keys and clicks on a row still commit at once. Under reduced motion a flick still coasts, as scrolling does; a tap, a key or a slow release cross-fades onto its row.
presentation="popover"keeps the previous surface; no value keeps the native-scroll column under a finger. Each row that reaches the band ticks with a short vibration where the browser supports it, as on Android; iOS Safari has no vibration. Under a fine primary pointer, as with a mouse on a desktop, the wheels keep the scrolling column.
Fixes
-
AlertDialog: under the touch presentation, an action lifted while the alert stays open now ends its release fade on the theme's tempo-scaled clock, as the stylesheet's fade does, instead of at a fixed 200 ms.
-
AlertDialog: under the touch presentation, its two actions are tracked as one group for a finger and a pen, the way iOS tracks an alert's actions. A press that lands on one action and slides to the other runs the action it lifts on, and lifting off both runs nothing. The pressed action highlights at once, and the highlight follows the finger between the actions. A second finger or the window losing focus ends the press with nothing run. A press on the title or the message is not tracked. A mouse press stays the native button's, and the keyboard and assistive technology keep each button's own activation.
-
Motion: a call made while an animation settles, with the element already still on screen, waits for the browser's first frame as a call from rest does. A busy page no longer skips the start of the new motion, a jump of up to about 14 px at a Switch's return.
-
BottomSheet: a sheet whose height moves by a fraction of a pixel as it starts closing now slides off instead of vanishing. This happens in Chromium, for example, when a sheet that fits its content is dismissed by a drag. The browser replaces the exit's transform transition when that transition's end value changes mid-flight. The exit treated the cancelled transition as its end and closed the dialog before the sheet had moved. It now waits for the replacement transition to finish.
-
BottomSheet: on iOS Safari the grabber stays drawn while the sheet moves between detents, instead of vanishing until the sheet comes to rest.
-
Calendar: day numbers and week numbers now use the locale's digits, the same as the month heading. Under
InternationalizationProvider locale="fa-IR"the days read۱ ۲ ۳…, and under"ar-EG"they read١ ٢ ٣…; English is unchanged. The month panes of DateInput's touch calendar follow the same rule. The dates themselves do not change: each number is still the Gregorian day. -
Calendar: Shift+Page Up and Shift+Page Down now move the focused day to the same date one year earlier or later, as in the WAI-ARIA date picker pattern, instead of one month. A February 29 lands on February 28 in a year without one. The year step follows the same rules as Page Up and Page Down: it changes nothing past
minormax, and on a day thatdateConstraintsdisables it focuses that month's first enabled day.
useGridFocusnow passes the key event toonPageUpandonPageDown, so a grid can readevent.shiftKey; callbacks that take no argument keep working.Fixes #8947.
-
CheckboxInput,SwitchandCheckboxList: in aFormLayoutwithdefaultOptionality="required", an unchecked box, an off switch or an empty checkbox group no longer carriesaria-required. A screen reader used to say "required", which on a checkbox means "must be ticked", while the form still submitted. Unchecked or off is an answer, so these controls take no required state from the form default. ACheckboxInputorSwitchis required only through its ownisRequired, which still setsaria-requiredand nativerequired. The "Optional" and "Required" labels follow the same rules as on every other field. No code change is needed (WCAG 4.1.2). -
clampTimewithout seconds now gives the nearest whole minute inside a bound that falls between minutes. Before,clampTime('08:00', '09:00:30')gave09:00, a time outside the range. -
Closing a
ComplexSelectorby clicking into another control now leaves focus in that control instead of pulling it back to the selector, asSelectorandMultiSelectoralready do. Escape, a close from the content, or a click on nothing focusable still returns focus to the trigger. A caller-rendered trigger (renderTrigger) still gets focus back on every close. -
Typing a numeric date into
DateInputorDateTimeInputno longer reports month-first dates on the way in day-first locales. -
The theme token
--duration-exit, the standard dismissal duration, now exists. It defaults tovar(--duration-fast), so it follows a theme'stempothrough that token, and a theme can set it like any duration token. Before, a theme or component that readvar(--duration-exit)found no value. The motion and token docs, and the theme template's token list, name it.Also released in
@astryxdesign/cli. -
A text selection that an action's own handlers make when a claimed mouse or pen press lifts on it now stays. Before, the drag's selection hold put the press's selection back over it. When the handlers change nothing, the press's selection still comes back, as before.
-
Under
focusTrigger="auto", aHoverCardnow decides whether focus opens it when the focus arrives, asTooltipdoes. A roving-tabindex trigger that wastabindex="-1"when the card attached, such as a toolbar member, opens on arrow-key focus, and a focusable trigger such as a<summary>or an SVG link opens on focus. -
A
HoverCardwhose trigger is text written around an expression, such asEdited {when}, now wraps that text in its focusable trigger, so hover and keyboard focus open the card, the trigger carries its popup attributes or description, and it shows the same hover cue as a plain text trigger. Before, such text had no trigger at all. -
When a
HoverCard's trigger element is replaced after mount, by new children or by a child component that swaps its own root element, the new element now gets the card's popup attributes or description and opens the card on hover and focus. Before, it stayed unwired until the card next opened or closed or itslabelchanged. -
Icon: a labelled icon component that hides itself unless a prop replaces it, such as a Heroicons icon, is now exposed to assistive technology as an image with its
label. Before, the component's ownaria-hidden="true"stayed on the SVG and hid the label. If a parent control already names the icon, removelabelso the name is not announced twice. -
Icon'srefnow reaches the rendered<svg>for a semantic or namespaced name, as its type promises. Before, it reached the<svg>only wheniconwas a component; for a name, the ref stayed empty. For a name whose glyph has no<svg>, the ref isnull. -
InfoTip renders only its button and its help: the
display: contentswrapper around the button is gone, and the button is the tooltip's trigger directly, built onuseTooltip'sshow()andhide(). Hover and keyboard focus still preview the help, and a click, tap, Enter, or Space still pins it until Escape, a second press, a press outside, or focus leaving. A mouse press on a previewed help keeps it in view as it pins, and a press that ends off the button does not pin it. A right-click on a previewed help now closes it at once, as it does on any tooltip trigger. Styles that reached the button through a wrapper selector now select the button itself. -
BottomSheet reads the on-screen keyboard from core's shared keyboard module. It now takes over focus moves into a fully expanded Tall sheet wherever a top-level document has a visual viewport and the primary pointer is coarse, Chrome and Firefox on Android included, instead of only under an iOS user agent. A pinch zoom no longer reads as a keyboard, and a pinch-zoomed page still reads the keyboard's full height. The sheet lays its keyboard room out through its own style rather than a CSS custom property. The browser-support guide documents Chrome on Android's
interactive-widgetviewport setting. -
A layout motion made late in a long main-thread task, such as a tap on a slow device, now plays instead of being skipped: under Reduce Motion the side navigation rail's cross-fade showed no fade at all after such a block. A layout change's animations start on the frame time the page last read only while that frame is at most 34 ms old; otherwise they start at the moment the change makes its first animation, so the block delays the motion. A glide such a change retargets carries on from where the compositor has it. An edge that rides such a glide, as the side navigation rail's does, moves with it. In Safari, a change that reverses such an edge mid-motion reads where the glide shows on the motion's own clock, so the edge no longer trails the content. Such a reversal also starts the rail's and the content area's fades from that moment, so the rail's icons no longer flash back. A change keeps one start from its first animation through the flush of its glides, and the parts riding those glides, however long the rest of the commit runs. A change that begins after a block since the last layout motion takes a fresh start, unless it joins an earlier change in the same area whose glides have not started yet. A side navigation collapse or expand made before the rail first paints, other than by its own toggle, lands at once, so a restored state never animates on load.
-
Lightbox: a viewer that unmounts while it is open (rendered only while open, or removed with its view) now returns focus to the element that opened it, when that element is still on the page, instead of dropping it to the page. A control the user pressed to remove the viewer keeps its focus. -
useLongPresstimes a hold on the touch events' own timestamps (spec:AST-094FR16). When a busy page handles the lift late, a finger held for the full delay still reports its long press before the lift acts, instead of losing it, so a ContextMenu's long press opens on a loaded page too. A touch the page handled late no longer lengthens the hold, and a lift stamped before the deadline still cancels the press when its timer ran late.onTouchEndandonTouchCanceltake the touch event, optionally; spreading the handlers passes it. -
Tooltip and HoverCard: a pen held on the trigger before it lifts no longer closes the tooltip or card it just opened.
-
PowerSearch: a date-only
date_absolutevalue (isDateOnlyunset ortrue) reads as one day in one zone — on the token, in the accessible label and announcements, and in the edit popover's date editor — and the editor writes the day picked. Before, the token showed the device's day, the label showed a date and a time intimezoneID, and the editor read and wrote the UTC day without receivingtimezoneID, so west of UTC a filter opened on the day after the one its token showed and re-picking that day moved the value a day earlier. Now a date-only value (isDateOnlyunset or true) is a calendar day intimezoneID, else the UTC day: the token shows that day; the accessible label andformatFilterValueshow that day with no time of day (UTC midnight March 9 in Los Angeles read "Mar 8, 2024, 4:00 PM" and reads "Mar 9, 2024"); the editor opens on it; and a picked day stores as that zone's midnight — withtimezoneIDset this is a different instant from the UTC midnight stored before, so a caller that derived the day from the instant's UTC date reads it intimezoneIDinstead, and a value the old editor stored as UTC midnight under atimezoneIDwest of UTC now reads the day before in the editor on every device, and on the token for a viewer at or east of UTC (UTC midnight March 9 underAmerica/Los_Angelesreads March 8 everywhere; a Los Angeles viewer's token and the label already did), the day it would have stored — re-save such values, or read them as UTC days; withtimezoneIDunset no stored value changes. Each absolutedate_rangepart is read and written as a calendar day the same way in the range editor. An operator withisDateOnly: falsekeeps its label; its token and editor change as its own entry describes. In the sheet presentation the rows and the value step read and write the same day. The exportedPowerSearchTokenand acomponentsToken override receivetimezoneID(a new optional prop onPowerSearchTokenProps), andPowerSearchTokenshows that day too, where it showed the label's date and time. A[fix], not a breaking change: the released popover read one value in three zones, so nothing valid relied on how a day was stored, andtimezoneIDis documented for date formatting. AcomponentsEditor override andPowerSearchFilterEditorreceivetimezoneID. -
PowerSearch: a
date_absoluteoperator withisDateOnly: falsegets the time of day the option names. The edit popover shows a time field beside the date picker, the token shows the date and the time, and both read and write the instant intimezoneIDwhen it is set, else the viewer's zone, so the time typed is the time shown. Before,isDateOnlywas read nowhere: everydate_absoluteedited in the date picker and stored UTC midnight of the picked day. Only operators that setisDateOnly: falsechange: a value saved from their editor now carries the time entered where it carried midnight, and their token shows the time;isDateOnlyunset ortrueis unchanged, and stored values are read as the instants they are. -
Under a finger, a press's release now lasts until its fade has played. The touch press controller ended the release on a clock started at the lift, so when the page's script delayed the fade's first frame, the release was cut short mid-fade. It now ends when the press's release animation ends, on the surface or on a part inside it that a component draws the release on, or on its clock where no release animation runs (an instant release, a surface without one, or an engine without Web Animations). This applies to every surface the touch press controller drives: buttons, switches, checkboxes, cards, menu rows and list rows.
-
A server-rendered
useResizableregion whosedefaultSizeis a percentage and that has nocontainerRefhydrates with the server's markup, then resolves the percentage against the window, instead of keeping the size the server resolved. -
A server-rendered
useResizableregion with anautoSaveIdhydrates with the server's markup and takes its saved size in the next commit, so hydration no longer reports a mismatch and the handle announces the saved size. -
A disabled
ResizeHandlekeeps its tab stop. Before, it left the tab order, so keyboard users could not reach it to hear that it is disabled. Now it stays in the tab order, as a disabled control keeps its tab stop, and it still ignores pointer and keyboard input and says so witharia-disabled. -
ResizeHandle's grab zone stays centred on the divider whateverpillPlacementsays, which now moves only the pill. An inline zone takes at most half of a pane shorter than it, and an overlay Handle takes at most half of the pane it sits in. A short item, such as a 30-minute calendar event resized from its edge, keeps its own drag, and a drag on the pill still resizes wherever the pill is drawn. -
useScrollLock, and so Dialog, BottomSheet, Drawer, Lightbox and MobileNav, keeps the page where it is while an overlay is open. The lock used to pin<body>withposition: fixedat minus the scroll offset and scroll back on release: the page's offset read 0 for the whole lock, and on iOS Safari the jump to the top brought the toolbar back, where it stayed after the overlay closed. Now the page getsoverflow: hidden(on the root, or on<body>when the body's own overflow is the one the viewport uses), and where the primary pointer is coarse (phones and tablets), a one-finger pan that would scroll the page is cancelled, which iOS Safari needs because it scrolls anoverflow: hiddenpage under a finger. Pans that a scroller inside the overlay can take, drags on a native range input, pinch-zoom, and one-finger drags on a pinch-zoomed page (they pan the zoomed view) still work. The lock no longer pins<body>, andwindow.scrollYkeeps its value while the lock holds. The lock cancels those pans itself, leaving a touch a gesture recognizer owns to it, and judges each pan after a small slop so a swipe's first wobble does not fix the wrong axis. It follows the browser's scroll chain, so an overlay rendered inside a scrolling container still holds the page: a modal dialog's pans go to the page, not to that container. It also setsoverscroll-behavior-y: noneon the root and<body>, which stops the page's overscroll bounce while an overlay is open. MobileNav now takes the same lock: while it is open, the page's bounce stops and one-finger pans are held as behind any other modal, and closing it while another modal holds the page keeps that modal's scrollbar gutter. -
A
Selector's open option list is now named by the field's label in every presentation. In the popover without search, the list was named by the trigger, so a screen reader announced the selected value, or the placeholder when nothing was selected (for example "Banana" instead of "Fruit"), against WCAG 2.2 SC 4.1.2.getByLabelText(<label>)now matches the list as well as the trigger. -
A disabled
Sliderno longer runs a caller'sonPointerDown,onPointerMove,onPointerUp,onPointerCancel,onKeyDown,onClickor any otheron*prop. Before, a press, drag or key on a disabled slider still reached them, for both the single and the range form. Now a disabledSliderruns no caller handler other thanonFocusandonBlur, as a disabledTextInputandTextAreado (spec:AST-085FR2). An enabledSlideris unchanged. No API changes. -
Spinner keeps turning while a script runs. The ring used to move by repainting its arc on the main thread every frame, so it stopped for as long as the page ran JavaScript, which is exactly when a loading indicator is on screen. The arc now sits in its own layer that the browser turns on its compositor: through a 700 ms main-thread block it keeps turning at full rate, and it paints nothing per frame. The ring looks the same at rest and turns at the same speed; sizes, shades, the themeable vars and reduced motion are unchanged.
-
In a
Tablewith resizable columns, the resize handle's name and width value no longer join the column header's name. Before, screen readers read "Name 60" with every cell in the column. -
Under a finger, the time wheels in the TimeInput and DateTimeInput bottom sheets now follow iOS's date picker:
- The AM/PM column follows the hour. Turning the hour from 11 to 12 now gives 12 PM, not 12 AM, and the AM/PM column turns onto PM. From 12 PM to 1 it stays PM.
- A bound that falls between whole minutes, such as
min="09:00:30"without a seconds column, now commits the nearest whole minute inside it, with a finger as with a mouse.
With a mouse, each column still turns on its own.
-
Toast: a toast updated throughuniqueID(collisionBehavior: 'overwrite') now updates in place, so focus on its controls stays put instead of dropping to the page. If the update removes the focused control, focus moves to a control in the toasts (such as a dismiss button) or, with none, back to where it was before entering the toasts. Dismissing a focused toast when only toasts without controls remain also returns focus there. An update that makes a toast auto-hide keeps its timer held while focus is inside it. Outside a modal, toasts in aToastViewportno longer carry their own live region, so each toast and each update is announced once, by the viewport; while a modal hosts the viewport, each toast announces itself, because the modal hides the viewport's channel. -
Tokenizer's bottom-sheet presentation (
presentation="bottom-sheet", and the defaultadaptiveon compact touch screens): a confirmed Clear all keeps focus on the Clear all action, now disabled with its reason, instead of moving it to the search field, which raised the on-screen keyboard over the rows the person had just cleared. The inline presentation is unchanged. -
Typeahead,Tokenizer,PowerSearch: a search no longer stays busy forever when React renders a new search source and then discards that render, for example a transition that suspends and that a newer update replaces before it commits. The search of the source that stays now finishes: its results show, and the loading indicator andaria-busyclear. -
In the DateInput, TimeInput and DateTimeInput bottom sheets, a wheel's Arrow, Page, Home and End keys move through its enabled values in time order, skipping disabled ones, and stop at the first or last enabled value, with a mouse as with a finger. On a 12-hour hour wheel 12 PM comes before 1 PM, so ArrowDown from 12 PM moves to 1 PM, and a key past the last allowed hour commits nothing.
-
DateInput: with a mouse, the rows of the bottom-sheet picker's columns meet WCAG AA contrast at every scroll position; enabled rows fade by the theme's text colors instead of opacity.
Performance
-
BottomSheet: opening a sheet no longer forces a layout to measure its height while it mounts; the sheet reads its height from the browser's own layout instead. On a phone-sized page with a 4× slowed CPU, a Selector's sheet shows its first frame about one frame sooner (about 110 ms to 94 ms after the tap) and the open does about 7 ms less forced layout. The open motion and the sheet's size are unchanged.
-
Popover, DropdownMenu, Carousel, ContextMenu, DateRangeInput, Markdown, MoreMenu and Table no longer load BottomSheet's container padding styles up front. Popover loads a BottomSheet on demand for its phone presentation, and a bundler that splits code put the padding module both of them use into one chunk that loads with the component, keeping every export either side uses.
paddingStylesandoverlayPaddingResetnow have their own modules, still exported from@astryxdesign/core/Layout/padding.stylex, andisFocusDetachedhas its own module, still exported from@astryxdesign/core/utils. A component that uses BottomSheet's padding itself still loads it. Rendering is unchanged.
Documentation
-
Source comments above 88 kept Effects in Core now name the external system each one syncs. Nothing a consumer imports or renders changes.
-
Hook docs for
useLightbox, soastryx hook useLightboxandastryx searchreturn it, and auseLightbox — Gallery Gridexample block: a grid of photo thumbnails that opens a fullscreen gallery at the clicked item.Also released in
@astryxdesign/cli. -
Icon: list the ten palette
colorvalues and namespacediconkeys, and documentclassName,style, andrefin every doc language. -
IconButton: document
isInterruptible,width, the form props (type,name,value,form), and the link props (href,as,target,rel), and say that a disabled IconButton with a tooltip stays focusable througharia-disabled. -
Icon's documentedcolortype now lists every value the component takes: the ten semantic colors and the ten palette colors (blue,red,green,gray,cyan,teal,yellow,orange,pink,purple). Before, the type listed only the semantic colors, though the description named the palette. Native platforms still draw only the semantic colors. -
The source comment above the Effect that copies an
Overlay's first-child corner radius onto its wrapper now says why it stays an Effect. New tests pin when the radius is read. Nothing a consumer imports or renders changes. -
TypeaheadItem: the docs say that the
groupprop has no effect, and that results are grouped throughitem.auxiliaryData.groupon search-source items.
Other Changes
-
Internal: the undocumented
@astryxdesign/core/internal/touch-sheetentry is removed. It re-exported the touch sheets' confirmation, search-drag dismissal and draft rules for the Lab prototypes of the Tokenizer and PowerSearch sheet presentations, which no longer exist; Core's sheets import the same modules by relative path. Internal entries are outside the public API and its compatibility promise. -
The configuration-object
useResizablebuilds on one internal resize region, through the same size model as before, so the region's state has one implementation. -
The internal resize factory can drive a plain handle element: it gives the element the separator's role, name, value and tab stop, and answers its pointer drag, keys, Enter and double-click. ResizeHandle and the factory derive the separator's attributes from one helper, with no behavior change.
-
Internal:
useLongPressruns its long-press detection through an internal detector with no React, so a press model that runs without React can share it. The hook and its handlers behave as before;onLongPressnow also comes from the committed render. Not exported from the package entry point. -
Internal: one progress module for the motion foundation.
progressCurvegives a spring's progress from 0 to 1 as a closed-form curve,mapProgressmaps any curve from another (a companion's dim following a sheet, a clamped range),mapRangeis the clamped linear map they share, andpresentedProgressVelocityreads the velocity an interrupted motion continues from. One velocity unit throughout: progress per second. Nothing public changes; no component imports it yet. -
Internal: the touch press controller leaves the press of an action that its group's own tracker paints (
data-astryx-press-group-item) to that tracker, so one writer paints each press. A key's press on the action still paints as before. -
Internal: press tracking imports only the UIKit timing table's press clocks, so Switch, Slider, SegmentedControl and DateTimeInput, the controls built on press tracking, no longer ship the whole table. Nothing changes in how a press behaves.
-
Internal: the scroll lock's touch guard asks the gesture system's native-scroll test whether an element under a finger can scroll, instead of keeping its own copy. The copy left out
overflow: overlay, which current Chromium and WebKit compute asauto, so pages there behave the same. -
Internal: SelectableCard reads an internal input context that Lab's SelectableCardGroup uses to render its cards as a native radio group. SelectableCard's public props and behavior do not change.
-
Internal: a horizontal Stepper stops measuring its list from the cleanup that the list's ref callback returns, not from an Effect. It still measures before the first paint, and it still stops when the list detaches or the Stepper turns vertical. Nothing a consumer imports or renders changes.
-
Internal: a theme's reduced-motion mark and its readers are in place for
reducedMotion: 'always'. No theme can set it yet, so nothing changes for anyone. -
Internal: the Tokenizer bottom sheet checks its result rows for
actionwhile it renders, not in an Effect after the commit. The development warning, its text and the rows are unchanged. Not exported from the package entry point. -
Internal: the rules UIDatePicker adds to its wheels, measured on iOS 26.2, join the UIKit constants table: the month/year wheels changing the date, the AM/PM column following the hour, the turn back onto a bound, and the day clamped to the month. Each rule comes with its source. None is exported from the package entry point.
-
Internal: browser proofs for the touch picker wheel in Chromium and WebKit, which a finger gets on every engine. A flick coasts on UIKit's curve onto the row it projects and commits on arrival. A finger landing on a coasting wheel stops it, and it then eases onto the nearest row. A storyboard films a flick and its coast. In Chromium, the coast also keeps advancing every frame while the main thread is blocked, and does no Layout or Paint per frame. Under reduced motion, a flick still coasts as momentum with its rows flat, and a tap cross-fades onto its row, animating opacity alone for 200 ms or less. Opening the month and year wheels writes no element per year, and each year is drawn once. Under a mouse, a drag still scrolls the column onto a row.
-
Internal: the Wheel's two presentations, the scrolling column under a mouse and the picker wheel under a finger, share one listbox hook for their ids, ARIA attributes, Arrow, Page, Home and End keys, and row clicks. Each keeps where its moves start: the scrolling column steps from the row under its band, and the picker wheel from the row it is heading to.
-
Internal: the picker wheel turns from script, for the time columns that use it. Under a finger,
spinToon the wheel's ref turns it onto a row on the tap spin, then commits it as a rest would; under a fine pointer it commits the row at once. Either way it reports whether that row was committed: it refuses a disabled row and an index outside the rows, and a turn the wheel's unmounting cuts short reports that it did not arrive. Nothing public changes: the wheel is not exported from the package entry point. -
Internal: UIPickerView's values measured on iOS 26.2 join the touch motion tables as internal constants: the row pitch, the picker's height and its rubber band, the coast's stop speed and velocity floor, the snap at rest, the tap spin and the selection tick. The pure math a wheel moves with comes with them. It projects the row a release rests on as UIScrollView sums its deceleration, coasts with the rate re-solved to stop on that row, snaps, bounces at an end, rubber-bands, and spins to a tapped row. It is checked against UIKit's recorded frames. Nothing public changes: none of it is exported from the package entry point, and themes do not set these values.
@astryxdesign/richtext
New Features
- In
RichTextEditor, Cmd+A (Ctrl+A on other platforms) in a code block or a table cell now selects the code or the cell text first, and the whole document on the next press. Anywhere else it still selects the whole document at once.
@astryxdesign/cli
New Features
astryx upgradegains thepromote-tokenizer-presentationcodemod. It rewrites a staticexperimental={{presentation: x}}on Tokenizer, the 0.7.7 spelling that Tokenizer no longer reads, topresentation={x}. Where it cannot prove the rewrite, including any Tokenizer that spreads props (a spread may also setpresentation), it leaves the call unchanged with a TODO.
Fixes
astryx componenttakes a component's exact name in another case as that component's, never a module's that only shares the spelling.tokenizeropens Tokenizer's doc instead of CodeBlock's, which it reached throughCodeBlock/tokenizer.ts.--sourcewithtokenizer,slider,switchorprogressBarno longer printsCodeBlock/tokenizer.tsor the motion tables inmotion/uikit/, and answersERR_NO_SOURCE, as--source badgedoes.
Performance
-
integrationComponentConflicts(),integrationDocConflicts()andintegrationCheck()from@astryxdesign/cli/api, anddoctor integration components,doctor integration docsandintegration check, find Core's components from the folder listings they already read, with far fewer file system calls. Their results are unchanged. -
A run of
astryxloads only the command it names, so it starts with less work:astryx --versionuses about 0.31 s less CPU, andastryx docs --json,astryx component --listandastryx theme buildabout 0.25–0.3 s less. Help, the manifest and an unknown command still load every command. Output and exit codes are unchanged. -
astryx searchandastryx buildparse a page template's source for the components it renders only when the query could match one of them. A query no page renders parses only the few pages whose source holds an escape, and a first search for it uses about 20% less CPU. A query whose words name components on most pages still parses most of them and can take up to about 4% more. Results are unchanged.
Documentation
-
The browser-support guide has a section on the on-screen keyboard: a fully expanded Tall BottomSheet measures it as the layout viewport's height minus the visual viewport's, and keeps the focused field above it in Safari on iOS, Firefox for Android and Chrome on Android. In Chrome, by default (
interactive-widget=resizes-visual), the sheet adds its scroll room once Chrome reports the keyboard; withinteractive-widget=resizes-contentChrome shrinks the page for the keyboard itself and the sheet changes nothing. Your app owns its viewport<meta>element; Astryx never sets it. -
A new
useAnnounce — Filter Result Countexample block shows the hook on a product filter: once typing pauses for 500 ms, a screen reader hears the result count ("3 results", or "No results"). The count has its ownhasReplaceinstance, as the hook's docs advise for a changing value. Copy it withastryx template useAnnounceHookUsage <path>. -
A new
useClipboard — Share Linkexample block shows the hook on a share card: Copy writes the invite link, the button reads "Copied" whileisCopiedis true and resets on the hook's timer, and a screen reader hears "Link copied". Copy it withastryx template useClipboardHookUsage <path>. -
A new
useFocusTrap — Quick Edit Panelexample block traps focus in a custom quick-edit panel that is not built on Dialog:focusFirstmoves focus to its field on open, Tab and Shift+Tab stay inside it, Escape closes it throughonEscape, and closing returns focus to the Quick edit button. Copy it withastryx template useFocusTrapHookUsage <path>. -
A new
useHotkeys — Search and Help Shortcutsexample block shows the hook on a page header: "/" focuses the search field and "?" opens the shortcut help, typing either character in the field does not trigger them, and a switch turns the single-key shortcuts off throughisDisabled. Copy it withastryx template useHotkeysHookUsage <path>. -
A new
useInputStatusIcon — Coupon Code Statusexample block shows a custom bordered input that takes its status affordance fromuseInputStatusIcon: a wrong code shows the error status and the valid code shows success, and the variant switch moves between a glyph with a message below (attached), no icon on the field (detached), and a focusable button with a tooltip that keyboard and screen-reader users can reach (tooltip). Copy it withastryx template useInputStatusIconHookUsage <path>. -
The
useKeyboardHint — Arrow-key Hintexample block now callsuseKeyboardHintitself instead of relying on Toolbar, which calls the hook internally. It is a five-star rating with one Tab stop, where Left and Right change the rating: tabbing into it shows the "← → to navigate" hint, the first arrow press dismisses it, and tabbing back in does not show it again. Copy it withastryx template useKeyboardHintHookUsage <path>. -
A new
useLocale — Invoice Summaryexample block formats an invoice date and amount withIntlusing the locale fromuseLocale, so switching the provider from English to German changes "Nov 2, 2026 ·$1,234.50" to "2. Nov. 2026 · 1.234,50 $ " with no second locale source. Copy it withastryx template useLocaleHookUsage <path>. -
A new
useScrollableArea — Activity Log Panelexample block shows a fixed-height Activity log whose entries scroll.useScrollableAreamakes the panel a keyboard tab stop only when its entries overflow, shows a top or bottom edge shadow only where entries are hidden, and stops a scroll at the end from scrolling the page. Add entry flips it from not scrollable to scrollable. Copy it withastryx template useScrollableAreaHookUsage <path>. -
A new
useTreeFocus — Keyboard File Treeexample block builds a small file tree from plain rows with tree roles and gives it the WAI-ARIA tree keyboard model withuseTreeFocus: arrow keys move over the visible rows and open, enter, leave or close folders, Home and End jump to the ends, a typed letter jumps to a matching name, Enter opens a file, and one row is the tab stop. Copy it withastryx template useTreeFocusHookUsage <path>. -
A new
useTypeahead — Type to Focus a Countryexample block adds type-to-focus to a country list of buttons. Typing "can" focuses Canada, pressing "b" again and again cycles through Belgium, Brazil and Bulgaria, and "e" finds Éire with the accent ignored.useTypeaheadreports each match throughonMatch, anduseListFocusmoves focus. Copy it withastryx template useTypeaheadHookUsage <path>.
Other Changes
-
Internal: the file read behind
astryx doctor --builttakes its file limit as an option, 20,000 unless a caller passes one, and the checks' messages name the limit the read used. The command's checks and output are unchanged. Not exported from the package entry point. -
astryx discoverno longer lists the Lab componentsMobileTokenizerandMobilePowerSearch, which are removed from@astryxdesign/lab; their touch flows are Core's Tokenizer and PowerSearchpresentationsheets. -
Internal: the public-import check's
declaredTargetsandmissingNamesare exported from their foundation module, so the docsite's production registry judges a copied example's imports by the same rule. The CLI's checks and their output are unchanged. Not exported from the package entry point. -
Internal: a gap-report handler's time budget starts once its module has loaded. Starting its worker and loading the module have the default deadline, so a short budget in a contract test no longer covers a slow start on a busy machine. The 30-second handler budget is unchanged.
-
Internal: generating the CLI's type declarations no longer fails when builds in two checkouts run at once on one machine, and it no longer needs pnpm on PATH.
-
The Tokenizer blocks and page templates that show a prop the bottom sheet does not render (
hasClear,endContent,tokenOverflowBehavior,hasEntriesOnFocus,renderToken) passpresentation="popover", so they keep the inline field on phones under Tokenizer's new'adaptive'default.
Contributors
Thanks to everyone who contributed to this release:
@10k11 @cixzhang @duytbp @emilyhorsman @ernestt @Geervan @humbertovirtudes @imdreamrunner @korkt-kim @ksying @kyu-rong @MannXo @nbozidarevic @nynexman4464 @parinith-web @vjeux
Full Changelog: v0.7.7...v0.7.8