Repository navigation
Astryx 0.7.10 — all @astryxdesign/* packages ship at this version.
OLD_VERSION=0.7.9 # replace with the Core version your project uses today
npm install @astryxdesign/core@0.7.10 @astryxdesign/cli@0.7.10 # plus every other @astryxdesign/* package you use, at 0.7.10
npx @astryxdesign/cli upgrade --from "$OLD_VERSION" --apply --path .@astryxdesign/core
New Features
-
Carousel's
hasSnapalso takesuseSnapPaging(), from@astryxdesign/core/SnapPaging: iOS paging on native snap's stops, under a finger and on a trackpad swipe. A flick moves one view, or one item withuseSnapPaging({pages: 'item'}). A mouse wheel keeps native snapping. Only a carousel given the hook's value loads the pager, andhasSnapalone is unchanged. (#8153) -
Lightbox: a ctrl-wheel or a trackpad pinch now zooms a photo, and a trackpad's scroll pans a zoomed photo. (#8148)
-
Lightbox's zoom now works under a finger the way the iOS Photos viewer does, on a phone, a tablet or a touchscreen laptop. Pinch the photo to zoom around your fingers, and move them to pan while you zoom. Past the zoom limits the photo gives way, then springs back when you let go, at the speed your fingers had. Double-tap zooms until the photo fills the screen, at least 2×, and keeps the tapped point under your finger; double-tap again to go back. On a phone or a tablet, a zoomed photo pans with momentum, stretches past its edges and settles back. The keyboard keeps the zoom it had, 2× about the centre, and keyboard panning now stops at the photo's edges instead of carrying it out of view. Every zoom, a double tap's and a key's included, settles on the same spring. (#8148)
The photo's stage fills the viewer. A pinch on the photo no longer zooms the page, and a pinch on the controls or caption still does. WithhasZoomon, a tap on the dark area beside the photo closes the viewer after the double-tap wait instead of at once; a click still closes it at once. The zoom animations run on the compositor, so they stay smooth while the page is busy. WithhasZoomoff, nothing changes. WithhasZoom, Close, Previous, Next and the counter sit on a dark translucent backing, so they stay readable over a zoomed photo. -
createDragToSnapPointsand its hookuseDragToSnapPointslet a drag move an element along one axis and settle it on snap points (px offsets from its resting position) at the release: the nearest one, the next one in its direction after a flick faster thanflickThreshold(default 1008 px/s), or the one it started from when the release moves back. The settle continues the release velocity,settleTo(index)settles without a drag, andonSettle(index)reports where it landed.createDragToSnapPointsis exported from the package root anduseDragToSnapPointsfrom@astryxdesign/core/hooks; the CLI's templates gain a playing-card example of each. (#8658)Also released in
@astryxdesign/cli.
Fixes
-
Animated layout: the glide, the in-place swap and the edge reveal play the theme's move curve (
--ease-move) over--duration-medium-mininstead of a spring, so they follow the theme'scharacterandtempoand land with the motion around them. Reduce Motion is unchanged: a cross-fade under a coarse pointer, an immediate change otherwise. The glides of AppShell's content area, Collapsible, Table's row expansion and TreeList change feel: 225 ms in the Neutral theme, 310 ms with no theme, instead of about 735 ms. A theme whose move easing is alinear()orsteps()function glides on it, as the browser plays it. The segmented control's selection, which reads its timing the same way, now plays such an easing too instead of moving at once. (#10236) -
Two
AppShells on one page no longer share the main region's id. The first mounted shell keepsid="astryx-app-shell-main", so single-shell pages, skip links and selectors that name it keep working. Each other shell's main region gets its own generated id, which its skip link targets. (#10199) -
BottomSheet: a sheet a touch drag closes leaves on its close spring, starting at the speed of the drag, and its scrim fades out with it on one clock, at the theme's tempo. A BottomSheetSwitcher flow's scrim does too. A mouse or pen drag is unchanged. (#8105)
-
A
BottomSheetthat opens, on its own or pushed over another sheet, modal or not, now focuses itsdata-autofocusfield (aTextInputwithhasAutoFocus) in every browser; for example, a pushed "Edit delivery note" sheet opens on its note field. ABottomSheetSwitcherstep with such a field opens on it too. Before, Chromium left focus on the sheet's panel, so on Android a person had to tap the field before typing. A sheet with no such field still focuses where the browser's own dialog focusing puts it. (#10156) -
Under a finger or a pen, holding the box of a CheckboxInput, a CheckboxList row, a RadioList option or a Switch no longer selects the text beside it, and lifting it activates the control as a tap does. On iOS such a hold selected the label's text and, except on a Switch, toggled nothing. A long press that opens a ContextMenu or runs a row's own long press no longer also toggles a box under it, and a row that opens its own menu from its contextmenu keeps its box unchanged. A long press on the label still selects its text, and a mouse is unchanged. (#10091)
-
CalendaranduseCalendarNavigationkeep moving the visible month whenonFocusDateChangeis given withoutfocusDate. The focus date is controlled only when both props are present, as the imperativenavigateTohandle already treated it; but the Previous/Next buttons and keyboard month crossing switched on the callback alone, so a listener-only calendar reported the new month and then stayed on the old one, with no way to move. The callback still fires in every mode. (#10180) -
Inserting a token through
ChatComposerInput's handle (insertToken) now reports the new value throughonChangeonce, as typing does, and hides the placeholder. Before, a controlled host's Send button stayed disabled or sent a stale prompt. Theai-chat-landingpage template drops the input-event workaround it used to sync the composer after an insert. (#10167)Also released in
@astryxdesign/cli. -
Code: the
codetheme target now reflectssizeasdata-size, so a theme can restyle code that inherits the surrounding text size (size="inherit") differently from code at the default code size. Whensizeis omitted, nodata-sizeattribute is rendered. (#10186) -
CodeBlockandCarousel: the code viewport (while the code overflows) and the carousel scroller draw the theme's focus ring instead of the browser's default, and a theme's--focus-outline-*tokens reach them. Both rings sit inside their component's clipped box, so they show whole. The Carousel ring is painted by the carousel itself rather than the scroller, so the scroller's edge-fade mask no longer fades it, at rest or mid-scroll. The Carousel ring is drawn by an internal shared overlay. (#8875) -
DateInput, TimeInput, DateTimeInput:
valuenow accepts an explicitundefined. In a project withexactOptionalPropertyTypes,value={state}failed to compile whenstateheld whatonChangegave, becauseonChangesendsundefinedfor a cleared field and an optional property no longer took one. A controlled field no longer needs a cast there. (#10129, part of #8961) -
Dialog: when the first
data-autofocusrequest is hidden, inert,aria-hiddenor cannot take focus, the next eligible request takes initial focus. This also holds for a Dialog rendered inside an inert region of the page or in an iframe or Picture-in-Picture window (#10105, part of #4182) -
FieldLabel'slabelTooltipis now an info button after the label, named "About {label}", so keyboard and screen-reader users can reach the help (WCAG 2.2 2.1.1, 4.1.2), and a click or key press on it opens the help without focusing or toggling the field. (#6414) -
Gestures: a pan on its own scroller takes a trackpad's scroll on the axis its touch-action keeps, as it takes a finger's; a scroller inside it still takes the wheel first. A recognizer attached with
manageStyle: falseleaves the element's styles to its caller for that recognizer only. (#10212) -
Touch gestures work on a component rendered into another window's document, such as a same-origin iframe or a Document Picture-in-Picture window. The gesture host checked nodes and touches against this window's classes, so it ignored that window's own. A finger's scroll, a layer's input hold, the press delay inside a scroll view and the iOS selection lock read that window's nodes too. (#10224)
-
Animated layout: on iOS Safari, an Astryx gesture on gliding content goes to what is drawn under the finger. Gestures here are gesture recognizers, long presses, and Switch, Slider and SegmentedControl presses. Before, such a touch went to the element that sat under the finger when the glide started. A plain tap that no Astryx gesture handles still goes to that element until the glide ends, at most the theme's move duration. (#10239)
-
Lightbox: a double-click now zooms 2× about the clicked point instead of about the centre, and the next goes back; the keyboard keeps its 2× zoom about the centre. (#8148)
-
Link: inside a
<Text maxLines>the link now truncates with the text. There its root lays out inline, in the text's lines, so the clamp can cut through the link, and its keyboard focus ring is inset into each line, so the clamp keeps the ring's top and bottom edges visible. Elsewhere, and for a Link that sets its owndisplayormaxLinesor renders as a button, the root keeps itsinline-flexbox. A Link with block-level content, such as anHStackwith an icon, is not supported inside a clamped Text, because it breaks the clamp; place that composition outside the clamp. (#6021, fixed in #6038) -
MobileNav: the drawer's content region is now a navigation landmark (
<nav>) named like the drawer's dialog, so someone moving by landmarks on a phone finds the drawer's links, as they find SideNav's on a wide screen. SideNav items and a TopNav's drawer content both sit inside that one landmark. (#10130) -
Under a coarse primary pointer (a touch screen as the main input), a button's touch press release now runs on the compositor. It keeps fading while the page's script is busy, instead of freezing at full strength and snapping off, and it paints nothing per frame. The release keeps the theme's exit motion timing. With any other pointer, a finger on a touch screen beside a mouse included, nothing changes. Under a coarse primary pointer, while a press is on or fading, Button paints it on its own
::beforeand setsisolation: isolate. At rest it adds neither, so a::beforeyou style, throughxstyleor aclassNamerule, merges into that layer only during a press. Style a child element to keep it unchanged. (#9783) -
Under a coarse pointer, a
SegmentedControlsegment's touch target grows toward 44 px through an invisible hit area, stopping halfway to a neighbor. No segment changes size, and nothing changes under a mouse. (#10137) -
SideNav: during a collapse, the copy of the expanded rail now paints at the rail's own level, just below the rail, instead of at
z-index: 1with the rail raised to2. Page content stacks against the leaving rail as it does against the nav: a positioned sibling with noz-indexno longer sits under the copy, and content positioned above the nav also covers the copy. The motion's timing and curve are unchanged, and Reduce Motion stays instant. (#10145) -
Slider's value tooltip stays on its thumb in every frame of a drag and of the coast after a finger lifts. It trailed the thumb by up to 14 px, because it was anchored to a box that moved only when the owner rendered the new value. The tooltip now moves with the thumb by the same translate, and its anchor box keeps its place until the drag ends. (#10228) -
DropdownMenu,ContextMenuandBreadcrumbItemmenus without aheaderorfooter: Tab or Shift+Tab on a row inside an open sub-menu flyout now closes every open level and the menu, callsonOpenChange(false)once, and lets focus move on from the trigger, as Tab already did from a top-level row (WAI-ARIA Menu Button). Before, the menu stayed open. A menu or sub-menu with aheaderorfooterkeeps its Tab cycle. (#8870) -
useTableSelectionState,useTableTreeState,useTableColumnSettingsStateanduseTableColumnResizeread their latest values as last committed. A render that React discards, as a suspended transition does, no longer reaches their handlers. "Select all rows" now selects the rows on screen, not those of a page still loading. A row's expand toggle, a column toggle and a keyboard resize also start from what is on screen. (#10176) -
Timestamp: every value in the hover card now has a name for screen readers (WCAG 2.2 1.3.1). A copy button sits inside its row's value, so the card passes the description-list check, and a row without a label is named by its time zone's long name, which is visually hidden. (#9041)
-
A bottom toast now keeps its 16 px gap above the bottom safe-area inset instead of sitting on the inset's edge. Under a coarse pointer it clears at least 34 px plus that gap, even on a page that reports no inset (Safari reports one only with
viewport-fit=cover). (#8047) -
An empty toast viewport now has no box, so iOS Safari's bottom toolbar returns to the page color once the last toast leaves. Before, a toast swiped out through the bottom edge left its color on the toolbar until the page reloaded. (#8047)
-
A touch on a toast is now decided by its first move. A first move toward the toast's edge belongs to the swipe and never scrolls the page; any other first move leaves the page its scroll. The shared gesture host makes that claim, so importing Toast now brings in the host: about 24 KB gzip on its own (Toast from about 74 KB to 99 KB), and nothing more on a page whose other touch components already load it. LayerProvider without a Toast import is unchanged. (#8047)
-
A quick flick toward a toast's edge now dismisses it whatever its height. Before, a flick also had to travel 48 px (the distance that already dismisses a one-line toast), so on any toast up to 120 px tall a flick could only snap back; and its speed was timed by when the browser handled the touches rather than when they happened, which in browsers that deliver moves on the next frame made a quick flick look up to twice as slow. (#8047)
-
useImageModesamples the image once for aregionwritten inline. The effect depended on the region object's identity, so the documented call shape (a fresh object literal each render) re-fetched and re-decoded the image on every render, and each result re-rendered, without end. The effect now depends on the region's four numbers. (#10183)
Performance
-
On a touch screen, a control that grows its touch target toward 44 px (CheckboxInput, RadioList, Switch, List rows, InfoTip, action groups) measures its neighbours in a shared pass. Each pass used to match every element on the page against the list of interactive selectors. The pass now keeps that list until an element is added, removed or moved, an attribute the list reads changes, or a control starts or stops its watch. It still reads every neighbour's box in each pass, so the touch targets are the same. Measured in Chromium at 4× CPU slowdown on a page with 240 interactive elements, a pass during a scroll took about 70% less time. (#10242)
-
While a Tooltip is open, the observer that follows its trigger through a scroll no longer reads layout to re-arm. It used to read the trigger's box once or twice and the viewport's size on each frame of a scroll. It now takes them from its own report. The Tooltip still checks its placement once per move, on the same frames as before. (#10235)
Documentation
-
ButtonGroup's docs no longer promise that arrow keys skip disabled members. Arrow keys still move between members, Home and End still jump to the ends, and the group is still one Tab stop. (#10203) -
FieldStatus: the guidance now says that a field's status is announced when its message or type changes, not again when a repeated submit fails with the same errors, and shows how the form announces such a repeated submit, with unchanged errors, once with
useAnnounce(the error count and the first error) and moves focus to the first invalid field. (#10122)
Other Changes
-
Internal: Carousel's Shift+wheel handler carries a lint suppression for the new passive-handler rule until it moves to a native listener. Apps see no change. (#10248)
-
Internal: Core's internal touch entry also names, for Lab's NavigationStack, the gesture host and the coarse-pointer check. Its scroll edge effect reads the internal finger-scroll hook and its end call. Apps import nothing new. (#8396)
-
Internal:
@astryxdesign/core/internal/touchalso re-exports an element's shared animator, the Reduce Motion cross-fade, and the coarse-pointer touch hit area's hook and layer, for Lab's PageControl. It is an internal entry reserved for Astryx's own packages, outside the public API. (#9669) -
Internal:
useFingerScroll(scroller, {onEnd})callsonEndeach time a finger's scroll on the scroller ends, and the hook is re-exported from the internal touch subpath for Lab components that watch a finger's scroll. Internal entries are outside the public API and its compatibility promise; the hook is not exported from the package entry point or the gestures barrel. (#10172) -
Internal: an internal entry reserved for Astryx's own packages exposes the Dialog's title id, inline state and
syncDefaultName()(a header whose title renders after the dialog mounted asks the dialog to look for its name again), so Lab's upcoming DialogHeroHeader can name the dialog and request title focus the way DialogHeader does. Adata-autofocus="dialog-title"request is tried after every explicit request, in preparation for that header; nothing a consumer sees changes (#10105, part of #4182) -
Two source comments, LayerProvider's inset note and ToastViewport's file header, are split into shorter sentences. Nothing that runs changes. (#10244)
-
Internal: the motion foundation's
resolveSettleMsgives a spring role's settling time at the theme's tempo, so a CSS part that plays with the spring can start and land with it. Not exported from the package entry point. (#8105) -
A toast's card no longer runs a hidden
transformtransition, with itstransitionrunandtransitionendevents, when a Dialog, a modal Drawer or a scrim sheet opens or closes over it. The toast already stayed in place. (#10223) -
Internal: the UIKit constants table gains UIScrollView zooming for a photo viewer, measured on iOS 26.2. It holds the give past the zoom limits and Photos' aspect-fill double tap and maximum zoom. The zoom stage geometry that Lightbox's touch zoom is built on comes with it. Neither is exported from the package entry point, and Lightbox's touch zoom reads them. (#8147)
@astryxdesign/cli
New Features
astryx discoverlists the experimental PageControl component from@astryxdesign/lab. (#9684)
Fixes
- The CLI no longer lists
@astryxdesign/theme-neutral,@astryxdesign/labor@astryxdesign/chartsas optional peers. Before, a package manager could keep a theme from an older release as the CLI's peer after an app removed the theme and upgraded Core, and then warn that the theme's Core peer was not met.@astryxdesign/corestays the CLI's one optional peer. In a Yarn Plug'n'Play workspace member, or a pnpm workspace member withhoist: false, a legacypackage.json#astryx.themeofneutralnow makesastryx componentwarn that it cannot resolve the theme. Add the theme to the app withastryx theme add neutralinstead. (#10232)
Documentation
-
A new
useMultiCombobox — Issue Labelsexample block builds a Labels field onuseMultiCombobox: typing filters the labels, the arrow keys move the active option while focus stays in the field, Enter or a click toggles a label and the list stays open, and selected labels show as removable tokens above the field. Copy it withastryx template useMultiComboboxHookUsage <path>. (#9578) -
A
useSnapPaging — Onboarding Pagesexample block: onboarding cards in a Carousel withhasSnap={useSnapPaging()}move one view per flick, and a switch moves one card per flick through the hook'spagesoption. Copy it withastryx template CarouselSnapPaging <path>. (#8153)
Contributors
Thanks to everyone who contributed to this release:
@cixzhang @GuillaumeMMM @HelloOjasMutreja @jiunshinn @kyu-rong @ManoharPaturi @thecodebasedot @vjeux
Full Changelog: v0.7.9...v0.7.10