Skip to content

feat(core): experimental keyboard subpath for the keyboard layout guide - #8194

Closed
vjeux wants to merge 46 commits into
meta:mainfrom
vjeux:uikit/c08-keyboard-experimental
Closed

vjeux wants to merge 46 commits into
meta:mainfrom
vjeux:uikit/c08-keyboard-experimental

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8076, #8287 (and their predecessors); review the last commit.

User need

Lab components that keep content above the software keyboard can't reach core's keyboard layout guide. A bottom-docked search field is the first one, and it needs the same timing ChatLayout's composer uses in #8176 (iOS) and #8287 (Android). Lab components may import only core's experimental subpaths, and the guide is internal (../keyboard). Without it, each one would re-measure the visual viewport and trail the keyboard by the lag #8176 measured.

Problem and solution fit

One more experimental subpath, built like #8076's motion and gesture subpaths. It exposes the guide and rideKeyboard as they already exist, and adds one thin hook so a React component subscribes for its lifetime and registers its field for Chrome's overlay.

The keyboard layout guide is core's single source for the software keyboard: BottomSheet's internal useMobileKeyboard moves onto it in a follow-up pull request (the guide gains the pan-independent keyboard height and a per-report notification it needs, and its focus-reveal refusal moves into the keyboard module), with every existing BottomSheet keyboard test passing unchanged.

Current specification

spec:AST-017 FR21 (experimental subpaths: outside the stable promise, never re-exported from the root or an ordinary subpath, @experimental on every exposed declaration). #8076 is the precedent.

Public delta and caller burden

@astryxdesign/core/keyboard/experimental (new):

  • getKeyboardLayoutGuide(window) → {inset, transition, presentation(time?), subscribe(listener), profile, overlayWhileFocused(element)}
  • useKeyboardLayoutGuide(onTransition?, {overlayFor?}) → the guide (null on a server). It calls the latest listener with each transition, registers overlayFor's element while mounted, and unsubscribes on unmount.
  • rideKeyboard(animator, values, transition): the keyboard's own motion from transition.start (UIKit's spring on iOS, Android's curve on Chrome).
  • types KeyboardLayoutGuide, KeyboardTransition ({kind, from, to, start, velocity, animated, predicted, curve}), KeyboardTransitionKind, KeyboardCurve, KeyboardProfile, UseKeyboardLayoutGuideOptions
const {ref, animator} = useAnimator();
useKeyboardLayoutGuide(t => rideKeyboard(animator, {y: -t.to}, t), {overlayFor: ref});

The keyboard constants stay in the motion subpath's uikit namespace (uikit.keyboard, now @experimental). Nothing changes for stable callers. Proposed; pending owner review: the subpath name and the hook's shape.

Design and alternatives

  • Hook plus function, like useAnimator and createAnimator: React callers get lifetime-bound subscription, and other code keeps the function.
  • Rejected: adding the guide to gestures/experimental (the keyboard is not a gesture), and a stable export (the API is days old).

Evidence

  • experimental.test.ts: the subpath exports the surface. Root, hooks and utils re-export none of it, and package.json maps the subpath.
  • The hook tests check that a re-render with a new function calls the new one and not the old, that unmount unsubscribes, and that on Chrome overlayFor's field overlays the keyboard while it has focus and lets go on blur and unmount.
  • The CLI lists useKeyboardLayoutGuide under astryx search experimental (golden updated), with stability: 'experimental' and its import path. Stable search output is unchanged.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  • vitest: keyboard, ChatLayout's keyboard hook and the Android table, 6 files / 46 tests (new: on Chrome, overlayFor's field overlays the keyboard while it has focus and lets go on blur and unmount; the subpath exports rideKeyboard).
  • CLI: the experimental-isolation goldens (only search-experimental gains this hook), the golden reads and hook discovery pass on this head's tree (3 files, 58 tests).
  • UPDATE_SHADCN_ROUTE_LOCK=1 node apps/docsite/scripts/generate-data.mjs: the route lock gains hooks/use-keyboard-layout-guide.
  • Core typecheck, eslint and prettier on touched files, sync-exports --check, check:repo, check:changesets, check:sync, add-copyright --check, cli-api-types-verify, lab:readiness:check.
  • Docsite unit tests: 53 files pass. The 3 that need docsite generate and built packages (blog headings, interactive preview, playground scope) were not run locally.

vjeux added 27 commits October 23, 2026 20:05
Adds internal/interaction-harness (@astryxdesign/interaction-harness) and
playwright.interaction.config.ts: phone-sized touch projects in Chromium and
WebKit for *.interaction.browser.spec.ts specs.

Touch scripts are data (tap, drag with hold and rest, flick with an exact
release velocity and velocity profiles, hold-then-drag, pinch, rotate, any
path, a second finger mid-gesture), played as real engine input: CDP
Input.dispatchTouchEvent with scripted timestamps in Chromium, WebKit's own
input command in WebKit. The harness's self-test proves the timestamps, a
native fling and multi-touch delivery; a pr-interaction CI job runs the
proofs against each PR's Storybook build, and a container script runs the
WebKit project where host libraries are missing.
Springs, scroll physics, timing curves, navigation values and gesture
thresholds for the touch presentations of core components, each with its
source (Apple documentation, a WWDC session, WebKit's UIKit-matched source,
or a measurement of UIKit on the iOS 26.2 simulator) or labelled a guess
with its measurement task. A test enforces the source rules on every domain
file, in their JSDoc and typed forms, and checks the list of simulator
measurements still owed.
Pure functions for the touch presentations: a closed-form damped spring
with every Apple parameterization and Core Animation's settling duration,
a spring chasing a moving target, UIScrollView's coast, rubber band and
release projection. Tests hold them to an RK4 integration, WebKit's spring
solver, the WWDC conversion equations and values read from UIKit, and play
the constants table through them.
…rena

An internal gesture system for touch presentations, not exported from the
package. GestureRecognizer carries UIKit's states and allowed transitions,
discrete and continuous recognizers, require-to-fail, simultaneous
recognition on request, shouldBegin and shouldReceiveTouch,
cancelsTouchesInView and the touch delays. GestureArena settles each touch
sequence without the DOM: participants, resolution order, claims, delivery
to the view, click dispositions (allow, suppress, defer and replay) and
timers as deadlines on the event clock. Its rules are tested with scripted
recognizers, independent of any real one.
…sholds

Tap recognizes at the lift within 45 pt of movement, taps 0.35 s and 45 pt
apart, and a 1.5 s hold; long press begins at exactly touch-down + 0.5 s on
the event clock within 10 pt, even when the main thread was blocked, and a
zero duration begins on the touch itself. The arena tests with these real
recognizers cover tap against double tap, delegate requirements, deferred
clicks and cancellation.
Touch behaviours key on a coarse primary pointer, layout behaviours on a
compact viewport, and presentations that need both (a sheet in place of a
popover) ask for both. utils/touchPresentation.ts holds the three queries,
SSR-safe hooks (useTouchPresentation; useMotionPresentation for components
with no interaction to infer from: UIKit motion under a finger, theme
tokens under a mouse) and a one-moment read. Styles write the same queries
as StyleX media keys, and a test keeps core's compact-touch keys spelled as
the shared query.

A compact viewport is narrower than 744 px or shorter than 500 px, the
closest a size gets to UIKit's compact size class: an iPad mini in portrait
keeps the anchored popover and a phone in landscape gets the sheet; a
Max-size iPhone in landscape is the known miss. The adaptive popover-or-
sheet presentation reads the shared query; its docs, stories and tests
follow.
Pure functions for the touch presentations: a closed-form damped spring
with every Apple parameterization and Core Animation's settling duration,
a spring chasing a moving target, UIScrollView's coast, rubber band and
release projection. Tests hold them to an RK4 integration, WebKit's spring
solver, the WWDC conversion equations and values read from UIKit, and play
the constants table through them.
createAnimator plays springs and UIScrollView coasts as Web Animations
keyframes on translate, rotate, scale and opacity, on one explicit clock, so
they keep running while JavaScript is busy. Presentation values and
velocities come from the curves, so a finger can catch any animation and a
new target keeps the velocity; the end state is committed inline and held by
the keyframes through a main-thread block, and a finished animation retires
when its own time reaches the curve's end, so a sampler can pause it; Reduce
Motion substitutes UIKit's cross-fade.
… Chromium

The Chromium driver sent a lift of one finger of several as a touchMove
listing the fingers still down, assuming CDP releases a point a move
omits. It does not: the lifted finger stayed down. A touchEnd that lists
the lifting fingers releases exactly those while the others keep moving,
so the driver sends that, and a self-test lifts one of two fingers and
checks the release, the other finger's moves and the touchend's
remaining touches.

Playwright's WebKit sends that touchend with no remaining touches, which
iOS never does. The harness does not work around it: drivers expose
partialLifts, and PARTIAL_LIFT_SKIP is the reason a proof cites when it
skips; the README lists it with the other engine limits.
Pause every Web Animation, seek it to a time and read the presented
transform and opacity, then compare the curve with oracles that share no
code with the motion under test: cubic-bezier, the SwiftUI spring
(response, dampingFraction, initial velocity), UIScrollView deceleration
and UIKit's rubber band. Storyboards photograph seeked animations at
t = 0, 16.7, 33.3 ... ms or each step of a scripted touch, and write a PNG
contact sheet plus the JSON trace a spec asserts, under the gitignored
test-results/interaction-evidence. A timer gate holds a component's own
timers while a filmstrip is taken. pnpm storyboards runs the tagged tests.
…sented values

An animation that keeps its end value in a run of identical final
keyframes (held so a later interruption can read it) reported its whole
duration as the curve: storyboards filmed the hold, waitForAnimation's
interval reached past the landing, and waitForRest, which also required
every animation to leave the running state and read only transform, timed
out. The curve now ends where that run starts (HeldAnimation.curveEndMs,
waitForAnimation's to, the default storyboard times), and rest is no
animation pending or before its curve's end plus the presented transform,
translate, rotate, scale and opacity (and ::backdrop opacity) still for 3
frames, whatever the playState. sampleCurve composes translate, rotate and
scale with transform, so curves of the individual properties are sampled.
Timestamped composited frames (Chromium's screencast and WebKit's own),
a marker the frames can find, and the busy-main-thread proof: block the
page's main thread mid-flight and show the element still moving every
frame, with a requestAnimationFrame animation that freezes as the
negative control. Chromium trace counts of Layout, Paint and style work
inside an animation window, and long tasks. The exemplar spec proves the
BottomSheet drag to a detent with all of it: the drag follows the
finger, a flick dismisses, the settle matches its timing function, keeps
advancing through a blocked main thread in Chromium and WebKit, does no
Layout or Paint, and gets a drag and settle storyboard.
…ed machine

The proof asserted a stall under 40 ms and near-exact frame counts, so a
dropped screencast frame or two on a loaded runner failed it, and frames
delivered out of order read as the element stepping backwards.
busyWindowProof now returns a verdict that tolerates what load does to a
screencast and nothing a frozen animation does: a median frame interval
of at most 20 ms, no stall longer than three screencast intervals, and
at least 90% of the window's whole frame intervals presenting a new
position (one miss always allowed), with the raw numbers in a summary.
Screencast frames are sorted by presentation time, the cadence comes
from the frames around the window, and blockMainThread can block from
inside the page in the frame an animation has run, so a short settle is
still judged at length on a busy machine. The exemplar judges its settle
until it is within 2 px of rest, since the screencast rounds to whole
pixels. A unit test covers the limits; the rAF negative control still
fails every criterion.
The pan begins past 10 pt on either axis and leaves that travel out of its
translation, as UIPanGestureRecognizer does; axis and one-way directions
with axisRatio, axis lock, finger changes without jumps, scrollEdge, and an
opt-in non-UIKit scrollHandoff. VelocityTracker defaults to UIKit's filter
(0.2 x the newest event's velocity + 0.8 x the previous one's), which
reproduces every value UIKit reported on recorded streams.
…zers

The rest of UIKit's recognizer set on the gesture system: swipe recognized
during the motion, pinch measured from the starting span moved by its
hysteresis, unwrapped rotation, edge pans resolved against the writing
direction that fail at touch-down outside their band, and hover for mice
and hovering pens.
The touch-action an element gets from its recognizers (pinch-zoom stays
unless a pinch or rotation recognizer is attached), what the scrollers
under a touch could still do along the scroll chain, and UIKit's haptic
feedback kinds on the web: navigator.vibrate where it exists, the switch
tap haptic in iOS Safari.
…s takeover

One host per document routes fingers (touch events, paired with their
pointer events), mice and pens to the recognizers on the hit path. A
recognizer that owns a touch cancels its touchmove through a non-passive
listener; pointercancel, an uncancelable move or a scroll mean the browser
took the touch; a claimed touch's touchend is cancelled and its click
swallowed, a deferred click replayed when its recognizer fails.
pointerIdOf names the pointer behind a tracked contact, so a held finger
can be handed to code that follows pointers.
… and hooks

PressTracker applies UIControl's events and its 70 pt tracking margin, the
content-touch delay inside scrollers, the quick-tap flash and the scroll
brake, with two profiles: control (UIButton) and cell (a row: delayed
highlight everywhere, cancelled past the pan hysteresis, no flash).
paintPress is the one writer of data-astryx-press and never clobbers a held
row. installPressAttribute paints every marked surface with one tracker
per touch, picks the profile from the marker's value and releases on its
clock. useGestureRecognizer attaches a recognizer with a stable identity
and no render per move.
The same touch scripts run in Chromium (CDP touches with timestamps) and
WebKit (its own touch input): touch-action per element, claims over a
scroller, no ghost click, scroll edge, pinch on its element only, a second
finger, and a long press at exactly 500 ms under a blocked main thread.
The README documents the system for component authors.
…tarts at the commit

play() bakes curves given as value and velocity functions of time (a spring
chasing a moving target, a fitted UIKit curve) like a spring.
createAnimator(dialog, {pseudoElement: '::backdrop'}) drives a
pseudo-element: a static filling animation holds its end value.
animatorFor(element) shares one animator per element, so callers animating
different axes compose in one translate track; atRest() waits out every
curve on its properties, and rebase() moves them by a layout shift keeping
their springs. A call from rest gets no start time, so the browser starts it
at its commit, as Core Animation does, and handle.started reports that
start. Every animation and set() promotes its channels to a compositing
layer in the same task (will-change, or a host custom property for a
pseudo-element) until 500 ms after the last motion: Mobile Safari only runs
a script-started curve through a block on an element that already has a
layer. Under Reduce Motion the substituted cross-fade can follow a
presentation's own spring, and crossfadeAs gives a slide that changes no
opacity its direction. The header lists the iOS Safari limits measured on
the simulator.
…eraction harness

useAnimator wraps the animator for React without re-rendering per frame.
Browser proofs on the interaction harness, in Chromium and WebKit on the
phone profile: a spring keeps advancing on every screencast frame through a
1 s main-thread block and every frame sits on its analytic curve; a spring
ending inside the block keeps its end state; no Layout or Paint runs while
it animates; the engine plays the curve when seeked; a finished animation
leaves within a frame or two of the curve's end; a dialog's ::backdrop
follows the curve.
A keyboard layout guide (packages/core/src/keyboard) reads the software
keyboard from the visual viewport and focus changes, dating each motion back
to where Mobile Safari's keyboard really is (motion/uikit/keyboard.ts holds
the measured lags). ChatLayout's dock rides the keyboard on its spring on
the compositor, the transcript keeps room to scroll above it and stays at
its newest message, and a tap on the composer's text focuses it without the
page-wide reveal scroll.
sync-exports discovers a subpath for every directory with an index.ts; the
keyboard layout guide is internal to core, like gestures.
vjeux added 9 commits October 24, 2026 08:57
A touch presentation is now also an idiom: ios for WebKit on iOS or iPadOS
under a coarse pointer (CSS.supports('-webkit-touch-callout: none'), which
no other engine implements), default everywhere else, Android included,
which keeps today's behaviour until a Material variant exists.
useTouchPresentation and readTouchPresentation report it, useTouchIdiom
reads it alone, and useMotionPresentation now chooses UIKit motion only in
the iOS idiom. Styles gate the same way with two nested StyleX keys, the
media query outside and the supports query inside (IOS_TOUCH_STYLE_KEYS),
as core's 16px input floor already does; a test keeps every such key in
core spelled and nested that way. A real-engine spec reads Chromium as an
Android phone and WebKit as default.
…ir own proofs

Under Reduce Motion the dock still rides the keyboard spring, with no
cross-fade, as the iOS keyboard keeps sliding. Focus that comes back while
the hide is under way turns the dock back onto the keyboard: every frame
stays within the hide's own fastest step.
Every spring, duration (a key ending in Ms) and curve of a motion table is
a motion role named by its table path (springs.sheetPresent,
timing.curves.easeInOut, sheet.present). defineTheme({motion}) takes
springs, durations and curves keyed by role, at the root or in an
adaptation rule, and writes one CSS variable per role it sets
(--motion-springs-sheet-present: 0.6s 0.1); a role it leaves out keeps the
table's measured value. The duration scale becomes all-or-nothing so a
theme can set roles alone. Adaptation rules gain a platform condition (ios
or other) that lowers to @supports (-webkit-touch-callout: none) around the
media block. Gesture thresholds and the arbitration timings (the
content-touch delay, the scroll brake) are not roles.

The motion docs carry a table of every role with its CSS variable and
default, generated from the tables (pnpm generate:motion-roles, checked in
check:repo).
# Conflicts:
#	packages/core/src/motion/README.md
…me sets it

resolveMotion(element, role) reads a motion role's CSS variable from the
element's theme scope (the nearest [data-astryx-theme], or the root) when a
motion starts: a style read, no layout, cached per scope until a theme
attribute changes, theme CSS is inserted or edited, the window resizes or a
pointer, contrast or reduced-motion preference changes. An empty or
malformed value, a detached element or a value that is not a role gives the
table's value back. The animator resolves every spring it is handed, the
Reduce Motion fade's duration, curve and spring, and a decay's edge bounce,
so call sites keep passing table entries. A browser proof seeks two boxes
animated with springs.sheetPresent, one inside a theme that sets it: each
plays its own spring within 0.05 px in Chromium and WebKit.
…tations

@astryxdesign/core/motion/experimental exports the compositor animator
(createAnimator, useAnimator), the spring, rubber-band and cubic-bezier math,
and the UIKit constants table as the read-only uikit namespace.
@astryxdesign/core/gestures/experimental exports the gesture recognizers,
useGestureRecognizer, usePressTracker, the touch presentation resolver and
playHaptic. Both follow spec:AST-017's experimental boundary: never
re-exported from the package root or an ordinary subpath, @experimental on
every exposed declaration, stability: 'experimental' docs. The internal
gestures barrel gets no subpath.
…ring proof names its events

WebKit under parallel load dropped a frame during the focus-returning
proof, and the doubled step read as a jump. Each step is now judged
against the hide's fastest speed over the time it took. When the rendering
proof fails, its message lists every rendering event of the rise and the
long tasks, in ms after the report.
vjeux added 7 commits October 24, 2026 11:50
…tor animation update

On a loaded machine Chrome may deliver a compositor animation's start
time late and make one rendering pass for it by itself: style
recalculations whose only reason is Animation, on the animated elements,
with no script before them, then one Layout and Paint. The rendering
proof counted it as the rise's work (about 1 run in 10 with parallel
workers).

traceRendering takes {invalidations: true} to record recalculation
reasons, the animations and whether they run on the compositor, and
script; countBetween's skipCompositorAnimationUpdates leaves out exactly
that pass. Work the page causes, and an animation the compositor cannot
run, still counts.
…with its slide

Chrome reports the keyboard at the start of its slide, once, and pans the
page to reveal a covered field. The keyboard layout guide now has a
chromium profile (every browser but iOS's): transitions start at their
report and play Android's IME curve (motion/android/keyboard.ts, theme
roles androidKeyboard.*), with nothing dated back or predicted.

A field that keeps itself above the keyboard registers with
overlayWhileFocused: while it has focus the guide sets
navigator.virtualKeyboard.overlaysContent (only if the page had not, and
restores only what it set), reads the keyboard's height from
geometrychange and ignores Chrome's transient viewport resizes.
ChatLayout's composer registers; rideKeyboard moves the dock on the
spring (iOS) or the curve (Chrome).
@astryxdesign/core/keyboard/experimental exports getKeyboardLayoutGuide, the
useKeyboardLayoutGuide hook and the KeyboardLayoutGuide / KeyboardTransition
types, so lab components can keep content above the software keyboard. Like
the motion and gesture subpaths (spec:AST-017 FR21): never re-exported from
the package root or an ordinary subpath, @experimental on every exposed
declaration, a stability: 'experimental' hook doc.
@vjeux
vjeux force-pushed the uikit/c08-keyboard-experimental branch from 212c816 to 647be21 Compare October 24, 2026 19:02
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
… for the search field's keyboard overlay

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request needs an engineering-owner decision before it can be approved: no current spec covers the standalone @astryxdesign/core/keyboard/experimental subpath (spec:AST-017 FR21 describes only a component's own /experimental subpath), and the draft spec:AST-094 it adds is unapproved, with an FR4 that forbids the theme motion roles the same branch introduces. An engineering owner can identify the governing spec for the subpath and approve or reject AST-094, including whether FR4 or the theme motion roles give way, by commenting /approve-spec <full head SHA> on this pull request; the author, who is an engineering owner, may record that decision too. Whatever the decision, these still need changes:

  • Rebase once #8045, #8062, #8067, #8074, #8076, #8176, #8258 and #8287 land, so this pull request carries only its own commit (about 460 lines) instead of about 31,700.
  • Have useKeyboardLayoutGuide report a keyboard that is already up when a component mounts, and fix its overlayFor example, which passes useAnimator()'s callback ref where a RefObject is required.
  • Publish the five experimental hook pages under marked experimental routes (spec:AST-017 FR27), and reword the changesets that call exported, in-use code internal or leave out the sub-menu change.
  • Place the caret under the finger for textarea and input composers (or claim only contenteditable targets), and name the system that useGestureRecognizer's setEnabled effect synchronizes.

[Automated review]

if (guide == null) {
return undefined;
}
return guide.subscribe(transition => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A component that mounts while the keyboard is already up gets no call here: the guide's first subscription re-measures without publishing (keyboardLayoutGuide.ts:286-290), so the documented usage leaves a docked element under the keyboard until it next moves. ChatLayout special-cases this (Chat/useChatKeyboard.ts:197-206). Could the hook deliver the current state as a non-animated change, or the docs say to read guide.inset after mount?

* @example
* ```
* const {ref, animator} = useAnimator();
* useKeyboardLayoutGuide(t => rideKeyboard(animator, {y: -t.to}, t), {overlayFor: ref});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useAnimator() returns a callback ref (motion/useAnimator.ts:35), but overlayFor is a RefObject<Element | null> (line 42), so this example fails to type-check (TS2322), and with the error silenced overlayFor.current is undefined and the field never overlays. Could overlayFor also take a callback ref or an element, or the example use a useRef on the field?

return null;
}
const editable = element.closest<HTMLElement>(
'[contenteditable]:not([contenteditable="false"]), textarea, input',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This claims taps on textarea and input too, but focusAtPoint only places the caret for contenteditable elements (lines 111-120). A caller's textarea composer gets focus with the caret wherever it last was, instead of under the finger as the header (lines 34-37) says. Could this claim only contenteditable targets, or set the selection for text controls?

},
{
"name": "hook-use-keyboard-layout-guide",
"path": "hooks/use-keyboard-layout-guide",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This experimental hook gets an ordinary public route next to the stable hooks; spec:AST-017 FR27 asks experimental docs to use marked experimental routes. The same applies to hooks/use-animator, hooks/use-gesture-recognizer, hooks/use-press-tracker and hooks/use-touch-presentation.

'@astryxdesign/core': patch
---

[feat] `presentation="adaptive"` on DropdownMenu, ContextMenu, MoreMenu, Selector and MultiSelector chooses the bottom sheet on a compact touch screen: a coarse primary pointer and a viewport narrower than 744px or shorter than 500px (it was 768px wide and below). An iPad mini in portrait keeps the anchored popover; a phone in landscape now gets the sheet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same query also decides sub-menu rows, whose presentation defaults to adaptive (DropdownMenu/DropdownMenuSubMenu.tsx:485): they now drill in on phones in landscape and open a flyout on 744–768 px tablets. spec:AST-017 FR3 asks the changeset to name that change and the option that restores the old behavior (presentation="flyout" or "drill-in").

'@astryxdesign/core': patch
---

[chore] Internal: tap and long-press gesture recognizers with UIKit's measured thresholds. Not exported from the package entry point; no component uses them yet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this head these recognizers are exported from @astryxdesign/core/gestures/experimental, and ChatLayout uses the tap recognizer, so "Internal" and "no component uses them yet" will mislead release-note readers (public-component-api INV12). The same wording is in eleven other changesets, such as gesture-host.md, uikit-motion-animator.md and uikit-motion-use-animator.md.

}, [ref, recognizer, manageStyle]);

useIsomorphicLayoutEffect(() => {
recognizer.setEnabled(isEnabled);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning isEnabled off mid-gesture cancels the recognizer, which calls every listener, so onGesture runs from this layout effect. react-component-runtime INV2 and INV3 ask such an Effect to name the external system it synchronizes; a short comment here, and a note in the hook's docs that disabling reports cancelled, would cover it.

* is everything else. Pair `ios` with `pointer: 'coarse'` for a touch
* profile, such as touch motion tuned per platform.
*/
platform?: 'ios' | 'other';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

architecture:theme-authoring-contract says an adaptation change "preserves the fixed condition vocabulary", and draft AST-094 FR4 keeps touch motion out of themes, which is what this condition is documented for. This needs the theme owners' decision, or a pull request of its own.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-090 is not on main or in this branch, so this proposed amendment has no record for its owners to rule on. Could it point at the record that exists, or drop the reference until that record lands?

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Closing: spec:AST-099 keeps the keyboard height internal (FR3, and INV11 in the react-component-runtime record), so there's nothing to export. The shared keyboard module is in #8176 (e3408f3).

@vjeux vjeux closed this Oct 25, 2026
@vjeux
vjeux deleted the uikit/c08-keyboard-experimental branch October 25, 2026 01:52
@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Requested change, following two decided rules:

So drop the public @astryxdesign/core/keyboard/experimental subpath (useKeyboardLayoutGuide and the four hooks), and keep them internal in packages/core/src/keyboard/. Components consume them there, and Lab uses internal workspace imports.

Other items from review:

  • rebase after the stack below lands;
  • handle a keyboard that's already open at mount;
  • fix the overlayFor example's type;
  • the experimental routes go away with the subpath (spec:AST-017 FR27).

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

#8194 stays closed: its module is internal in packages/core/src/keyboard/ (#8176), with no public subpath or export (spec:AST-017 FR21, spec:AST-099 FR3). A keyboard already up at mount is covered there (39572f6). overlayFor isn't in #8176.

This branch was successfully deployed

1 active deployment
Preview — 647be214 Deployed Oct 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants