Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
874085f
feat(core): one click guard for the menu press and the gesture host
vjeux Oct 31, 2026
61ec301
fix(gestures): a touch handled late goes to what was drawn where the …
vjeux Oct 31, 2026
68ba414
spec(AST-094): a touch on moving content goes to what is drawn under …
vjeux Oct 31, 2026
a9c100b
fix(gestures): content that slid under a late touch takes nothing; re…
vjeux Oct 31, 2026
c23a02a
test(motion): the busy-window proof reads the control's clock from th…
vjeux Oct 31, 2026
9b5337d
fix(core): the haptic switch's private attribute has its owner module…
vjeux Nov 1, 2026
faf3330
refactor(core): one iOS WebKit check and one flat-tree walk, shared b…
vjeux Nov 1, 2026
b5cbf78
chore(changeset): the input stream's fix line covers only what ships
vjeux Nov 1, 2026
2742894
feat(core): a pan takes wheel and trackpad scrolling, and a pinch a c…
vjeux Oct 31, 2026
13b5bc9
feat(gestures): a trackpad scroll ends where its fingers lift, uncert…
vjeux Oct 31, 2026
bf5485d
feat(gestures): a followed trackpad pan keeps moving at the fingers' …
vjeux Oct 31, 2026
2317641
fix(gestures): a pan follows a trackpad only where its owner allows i…
vjeux Oct 31, 2026
8a3b119
feat(core): createAnimator and useAnimator animate one element
vjeux Nov 1, 2026
4fb7756
feat(core): createPanGesture and usePanGesture report a drag as value…
vjeux Oct 31, 2026
0a00169
feat(core): createDragToSnapPoints and useDragToSnapPoints settle an …
vjeux Nov 1, 2026
980f0d1
chore(core): the animator takes start: 'next-frame' for a call made i…
vjeux Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/animator-input-event-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the touch animator takes `start: 'next-frame'` for a call made in an input event's own task (a key, a click, a pointer, a touch). Such a call from rest starts at the first frame after that task, in Chromium and in WebKit, so a later listener of the same event that holds the page delays the motion instead of letting it skip into its curve. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/click-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture recognizers and the menu press model share one guard for the browser's click after a gesture that already acted, swallowing it for 400ms or until the next press. Keyboard and assistive-technology clicks (`detail` 0) always pass.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-hook-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture hooks call nothing back after unmount; `usePressTracker` brakes with the host by default and keeps one tracker, so an option change or a new ref never drops a press in progress; the recognizer's arena plumbing is `protected`. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-host-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: gesture host fixes from review. A claimed pen holds only its own stylus touches, and a finger is matched to its scroller, its target and a moving element along the flat tree, through slots and shadow roots. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-host-owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture host becomes the one document-level touch owner: the finger count, touch gates, and a kept press for continuous-tracking controls, whose element gets `touch-action: manipulation`. A scroll lock's page hold and the finger-scroll watch keep their own listeners and ask the host only about the touches it owns. A second finger fails a one-finger pan that has not claimed, so the page can pinch. A scroll-edge pan holds a lone finger from the first move that pulls away from an edge every scroller under it is at, so those moves are cancelled before it begins; with a second finger anywhere on the screen the page keeps its pinch. A press tracker whose hold acts (a stepper's hold-repeat) can bring a selection lock for its presses, as a recognizer can. Internal, for the touch components that adopt them; not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-recognizer-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: gesture recognizer and arena fixes from review; a pan claims nothing before it begins at 10 pt; one velocity tracker for every gesture (`spec:AST-100`: UIKit's measured blend, a scrolling surface's release through the pan's `scrollVelocity()`, and FR2's release rules). Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gesture-watcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: a recognizer created with `claimsTouches: false` is a watcher: passive listeners only, simultaneous with every recognizer, and it follows a touch the browser takes for a scroll to the lift. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gestures-every-pointer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: with the gesture host serving every pointer, a press tracker takes a mouse press as a press, painting at once with no scroll brake (AST-094 FR11); a finger's or a pen's hold that does something keeps text unselected under any primary pointer, a touchscreen laptop's included, while a mouse keeps its released behavior (FR41); a finger is read once where the browser sends Touch Events without exposing `ontouchstart`; and `perEventPointer`, from before FR43, is accepted and does nothing. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/gestures-wheel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: a pan follows a trackpad's two-finger scroll where its owner allows it (`allowedScrollTypes: ['continuous']`, UIKit's `allowedScrollTypesMask`; none by default), along its own axis and after the scrollers under the pointer, and never a mouse wheel's notches, which scroll what is under the pointer; the screen-edge pan takes none; and a pinch takes a ctrl-wheel from a trackpad or a mouse (AST-094 FR43, DEC-24). A watcher pan never takes the wheel, and a wheel another handler already prevented stays the page's. Input the deltas cannot place, Shift+wheel included, reads as a mouse wheel; a trackpad scroll a pan follows keeps moving at the fingers' speed until their lift is read, ends there still moving, and the momentum never moves it. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/input-stream-recognizers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture recognizers hear page-wide input through the same shared stream on the window's capture phase, so an app's window-capture `stopPropagation`, or a document-capture `stopImmediatePropagation` registered before Core, no longer hides a touch from them; a recognizer that must claim a touch mid-gesture still needs the touch to reach its element. Each event type's listener is passive unless a subscriber cancels it (the gesture host's `touchend` and `contextmenu` while a claiming recognizer or press tracker is attached, the page hold's `touchmove` while it holds, and the click guard's `click` while it is armed, and from the menu press model's first arm on), so a page of pressables registers no listener that can hold a scroll.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/input-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] Touch press feedback now hears scrolls and page hides on the window's capture phase, where its pointer listeners already were. Under an app's window-capture `stopPropagation`, or a document-capture `stopImmediatePropagation` registered before Core, a finger's pressed look now ends when the content under it scrolls, and every pressed look ends when the page is hidden.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/long-press-one-source.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: `useLongPress` takes its default duration and tolerance, 500ms and 10px on either axis, from the same source as the long-press gesture recognizer, so ContextMenu's long press behaves as before. A finger held on a DropdownMenu trigger passes its own 13px allowance instead (see the trigger drift fix).

@vjeux
7 changes: 7 additions & 0 deletions .changeset/menu-trigger-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] A finger held on a DropdownMenu trigger may drift up to 13px on each axis and still open the menu under every primary pointer, a phone's, a tablet's and a touchscreen laptop's alike, as AST-094 FR29 allows; it cancelled past 10px. No menu, row or trigger reads the pointer query.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/presented-hit-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] On iOS, a touch on an element the compositor animator is moving reaches what is drawn: the animator reports each moving element's offset from its layout box, and the gesture host sends the touch, press bounds and the tap's click there instead of to the layout position iOS WebKit hit-tests.

@vjeux
8 changes: 8 additions & 0 deletions .changeset/public-animator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch
---

[feat] `createAnimator(element)` and its hook `useAnimator(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. `animate` springs `x`, `y`, `scale`, `rotate` and `opacity` to a target on the state-change motion, `decay` coasts any of them from a release velocity, `set` jumps, `get` and `getVelocity` read, and `stop` halts; `animate` and `decay` return a promise. Reduce Motion is planned for every call. `createAnimator` is exported from the package root and `useAnimator` from `@astryxdesign/core/hooks`; the CLI's templates gain a playing-cards example of each.

@vjeux
8 changes: 8 additions & 0 deletions .changeset/public-drag-to-snap-points.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch
---

[feat] `createDragToSnapPoints` and its hook `useDragToSnapPoints` let 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 than `flickThreshold` (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, and `onSettle(index)` reports where it landed. `createDragToSnapPoints` is exported from the package root and `useDragToSnapPoints` from `@astryxdesign/core/hooks`; the CLI's templates gain a playing-card example of each.

@vjeux
8 changes: 8 additions & 0 deletions .changeset/public-pan-gesture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@astryxdesign/core': patch
'@astryxdesign/cli': patch
---

[feature] `createPanGesture(element, {axis?, onPan})` and `usePanGesture(ref, {axis?, onPan})` report a drag on an element as values: `onPan` gets `{state, translation, velocity}` as the drag begins, changes, ends at the release, or is cancelled, and a finger, a mouse and a pen all drive it. `axis` limits the drag to `'x'` or `'y'`; a drag along the other axis is not reported. `translation` counts from where the drag began, so an element that follows it does not jump, and `velocity` at `ended` is the release velocity in pixels per second. The drag claims its touch at UIKit's 10 pt and leaves presses and long presses alone. `destroy()` removes every listener; the hook creates one pan for the element behind `ref` and destroys it at unmount.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/touch-selection-lock-gestures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the gesture host applies a recognizer's own `selectionLock` to that recognizer's contact and imports no lock itself; a long press brings one only when it has a hold (a `minimumPressDuration` above 0), and `selectionLock: null` opts out. Not exported from the package entry point.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/touch-selection-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] Where the primary pointer is fine (a mouse, a trackpad, a touchscreen laptop, a desktop pen tablet), the press feedback controller no longer registers a touch listener: it reads the primary pointer at the first finger or pen press and keeps its `touchcancel` listener only while that pointer is coarse.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/use-finger-scroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: `useFingerScroll(scroller)` tells a finger's scroll from the program's, and the gestures README gains a guide to migrating a raw-touch drag onto the recognizers. Internal to the touch components: not exported from the package entry point or the gestures barrel.

@vjeux
2 changes: 1 addition & 1 deletion .github/scripts/ci-interaction-webkit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ describe('WebKit interaction proof routing', () => {
for (const untouched of [
'packages/core/src/Toast/Toast.tsx',
'packages/core/src/Selector/useSelectorPresentation.ts',
'packages/core/src/utils/clickGuard.ts',
'packages/core/src/utils/focusReturn.ts',
'packages/core/src/internal/focus.ts',
'packages/core/src/DateInput/DateInput.tsx',
'packages/core/locales/en.json',
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2047,7 +2047,7 @@ jobs:
id: scope
uses: ./.test-scope/.github/actions/test-scope
with:
paths: '^(packages/core/src/gestures/|packages/core/src/motion/|packages/core/src/internal/touch|packages/core/src/utils/(press|touch|primaryClick|isApplePlatform)|packages/core/src/hooks/(useMenuPress|menuPress|useLongPress|usePressFeedback)|internal/interaction-harness/|internal/a11y-spec/|playwright\.interaction\.config\.ts$|packages/themes/[^/]+/src/|apps/storybook/|\.github/workflows/ci\.yml$|package\.json$|pnpm-lock\.yaml$|\.github/actions/test-scope/)'
paths: '^(packages/core/src/gestures/|packages/core/src/motion/|packages/core/src/internal/touch|packages/core/src/utils/(press|touch|primaryClick|isApplePlatform|flatTree|inputStream|clickGuard|selectionLock|coarsePointer)|packages/core/src/privateAttributes/hapticSwitch\.stylex\.ts$|packages/core/src/hooks/(useMenuPress|menuPress|useLongPress|usePressFeedback|isRtlElement)|internal/interaction-harness/|internal/a11y-spec/|playwright\.interaction\.config\.ts$|packages/themes/[^/]+/src/|apps/storybook/|\.github/workflows/ci\.yml$|package\.json$|pnpm-lock\.yaml$|\.github/actions/test-scope/)'
outside-pull-request: skip
requires-head-file: playwright.interaction.config.ts

Expand Down
6 changes: 6 additions & 0 deletions apps/sandbox/scripts/next-export-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"/templates/",
"/templates/AlertDialogAsyncAction/",
"/templates/AlertDialogDeleteConfirmation/",
"/templates/AnimatorPlayingCards/",
"/templates/AnimatorPlayingCardsVanilla/",
"/templates/AppShellContentOnly/",
"/templates/AppShellMobileHookUsage/",
"/templates/AppShellShowcase/",
Expand Down Expand Up @@ -292,6 +294,8 @@
"/templates/DividerShowcase/",
"/templates/DividerVariants/",
"/templates/DividerVertical/",
"/templates/DragToSnapPointsCard/",
"/templates/DragToSnapPointsCardVanilla/",
"/templates/DrawerForm/",
"/templates/DrawerHeaderShowcase/",
"/templates/DrawerHeaderSubtitleEndContent/",
Expand Down Expand Up @@ -464,6 +468,8 @@
"/templates/PaginationPageSize/",
"/templates/PaginationVariants/",
"/templates/PaginationWithTable/",
"/templates/PanPlayingCard/",
"/templates/PanPlayingCardVanilla/",
"/templates/PopoverBottomSheetAlternative/",
"/templates/PopoverConfirmAction/",
"/templates/PopoverFilterPanel/",
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/react-component-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ those contracts.
the modules through an internal hook only. A component's own touch logic is
small glue in its own build over these modules. The gesture host attaches for
every pointer, with no touch-only query; under a fine pointer only a component that
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The modules are not public exports and are not
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The gesture host, press feedback, page hold, finger-scroll watch and click guard hear document-wide input through one capture-phase listener per event type on the window, present while a subscriber needs it. They run in subscription order, before any document or element listener; it is passive unless a subscriber cancels the event. The modules are not public exports and are not
documented for consumers; Core and Lab components reach them through
`@astryxdesign/core/internal/touch`, a subpath reserved for Astryx's own
packages: undocumented, outside the compatibility promise, versioned in
Expand Down
4 changes: 2 additions & 2 deletions docs/specs/AST-094-touch-presentations/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ a measured, sourced difference exists.
activation stays the browser's click, except that an action a
`family:action-groups` member tracks activates at the lift (FR26), and a toggle
under `spec:AST-020` FR9's slide-off exception (Switch, Checkbox,
CheckboxListItem) may also complete on a lift after the finger slid off.
CheckboxListItem) may also complete on a lift after the finger slid off. Where the engine hit-tests an element a compositor animation moves at the motion's start (iOS WebKit), a touch on moving content goes to what was drawn under the finger at touch-down, and a tap there (no recognizer claimed it, the browser did not take it, and it stayed within 45 pt) MUST activate it: the browser's own click for that touch is cancelled or swallowed, and that element receives one click, an editable focused first.
Under a fine pointer a control uses a third profile with no delay: its
press paint starts at once on `pointerdown`, through the same press
controller and release fade as a finger's, in place of CSS `:active`; the
Expand Down Expand Up @@ -312,7 +312,7 @@ a measured, sourced difference exists.
recognizer that must claim mid-gesture registers that blocking listener; an
element whose recognizers decide only at the lift keeps its `touchmove`
listener passive, and its `touchend` listener stays cancelable so the
gesture can cancel the click (FR21). Known limit: a native scroll that starts on an element with a
gesture can cancel the click (FR21). While an element that holds, or sits inside, an element with a recognizer that claims moving touches moves on the compositor (iOS WebKit), the document MAY carry a non-passive `touchmove` listener; it cancels only the moves of a touch a recognizer owns under this rule and comes off when the motion ends. While it is on, a native scroll that starts anywhere on the page waits for a free main thread. Known limit: a native scroll that starts on an element with a
blocking listener waits for a free main thread in iOS Safari and Chromium.
That is never worse than UIKit, whose scrolling stops on a busy main thread,
and a passive listener, or one added at `touchstart`, would hand diagonal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
"data-astryx-press",
"data-astryx-press"
],
"packages/core/src/gestures/haptics.ts": ["data-astryx-haptic-switch"],
"packages/core/src/gestures/touchActionRule.ts": ["data-astryx-touch-action"],
"packages/core/src/hooks/useAnnounce.ts": [
"data-astryx-live-region",
Expand Down
60 changes: 60 additions & 0 deletions internal/shadcn-registry/routes.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1959,6 +1959,21 @@
"path": "examples/context-menu/bottom-sheet",
"aliases": []
},
{
"name": "example-create-animator-playing-cards-in-plain-java-script",
"path": "examples/create-animator/playing-cards-in-plain-java-script",
"aliases": []
},
{
"name": "example-create-drag-to-snap-points-playing-card-in-plain-java-script",
"path": "examples/create-drag-to-snap-points/playing-card-in-plain-java-script",
"aliases": []
},
{
"name": "example-create-pan-gesture-a-playing-card-without-react",
"path": "examples/create-pan-gesture/a-playing-card-without-react",
"aliases": []
},
{
"name": "example-date-input-clearable",
"path": "examples/date-input/clearable",
Expand Down Expand Up @@ -3634,6 +3649,11 @@
"path": "examples/typeahead/with-validation",
"aliases": []
},
{
"name": "example-use-animator-playing-cards",
"path": "examples/use-animator/playing-cards",
"aliases": []
},
{
"name": "example-use-app-shell-mobile-custom-mobile-trigger",
"path": "examples/use-app-shell-mobile/custom-mobile-trigger",
Expand All @@ -3649,6 +3669,11 @@
"path": "examples/use-container-reveal/reveal-on-hover-row-actions",
"aliases": []
},
{
"name": "example-use-drag-to-snap-points-playing-card-on-three-places",
"path": "examples/use-drag-to-snap-points/playing-card-on-three-places",
"aliases": []
},
{
"name": "example-use-hover-card-profile-preview",
"path": "examples/use-hover-card/profile-preview",
Expand All @@ -3664,6 +3689,11 @@
"path": "examples/use-layer/anchored-layer",
"aliases": []
},
{
"name": "example-use-pan-gesture-a-playing-card",
"path": "examples/use-pan-gesture/a-playing-card",
"aliases": []
},
{
"name": "example-use-popover-quick-actions",
"path": "examples/use-popover/quick-actions",
Expand Down Expand Up @@ -3744,11 +3774,31 @@
"path": "examples/visually-hidden/supplementary-context",
"aliases": []
},
{
"name": "hook-create-animator",
"path": "hooks/create-animator",
"aliases": []
},
{
"name": "hook-create-drag-to-snap-points",
"path": "hooks/create-drag-to-snap-points",
"aliases": []
},
{
"name": "hook-create-pan-gesture",
"path": "hooks/create-pan-gesture",
"aliases": []
},
{
"name": "hook-use-animated-layout",
"path": "hooks/use-animated-layout",
"aliases": []
},
{
"name": "hook-use-animator",
"path": "hooks/use-animator",
"aliases": []
},
{
"name": "hook-use-announce",
"path": "hooks/use-announce",
Expand Down Expand Up @@ -3794,6 +3844,11 @@
"path": "hooks/use-dev-warning",
"aliases": []
},
{
"name": "hook-use-drag-to-snap-points",
"path": "hooks/use-drag-to-snap-points",
"aliases": []
},
{
"name": "hook-use-entry-animation",
"path": "hooks/use-entry-animation",
Expand Down Expand Up @@ -3904,6 +3959,11 @@
"path": "hooks/use-overflow",
"aliases": []
},
{
"name": "hook-use-pan-gesture",
"path": "hooks/use-pan-gesture",
"aliases": []
},
{
"name": "hook-use-popover",
"path": "hooks/use-popover",
Expand Down
Loading
Loading