Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
This pull request can't be approved yet: SegmentedControl's touch slide rests on the draft spec:AST-094, which has no owner approval, and its FR4 narrows the theme-tokens architecture's motion roles for touch presentations. Approval needs an engineering owner to approve or reject AST-094's DEC-1 (UIKit as the touch reference), FR4 and FR13 (the shared resolver and its 744 px / 500 px query) by commenting /approve-spec followed by the full head commit SHA on this pull request; the author may record it as an engineering owner, and the same decision should settle FR8's "never jumps" rule against the slide's instant jump under Reduce Motion. Independently of that decision, please:
- Rebase onto
mainonce #8052–#8087 land, so this pull request carries only the SegmentedControl commit instead of 14,162 authored lines. - Remove
springs.segmentedThumbor point it at the measuredsegmentedControl.selectionSpring, and mark the segmented measurements done. - Say in the stable-width changeset that each title's text now appears twice in the markup, and drop or land the
spec:AST-090reference in AST-094 FR7.
[Automated review]
| * @measure M-SG02 | ||
| * @measure M-SG03 | ||
| */ | ||
| segmentedThumb: {response: 0.3, dampingRatio: 0.75}, |
There was a problem hiding this comment.
segmentedThumb is still the GUESS (response 0.30 s, damping 0.75) that M-SG02 and M-SG03 were meant to replace, but this pull request measures the same slide as segmentedControl.selectionSpring (0.40 s, 0.85; segmented.ts:49). Remove it or point it at the measured spring, and mark the segmented measurements done in MEASUREMENTS.md.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] SegmentedControl segments keep their width when the selection changes. The selected label is heavier, so a segment sized to its own label grew as it was selected and shrank as it was deselected (about 2–3 px in a content-sized control), shifting its neighbors. Each label now reserves its selected width. |
There was a problem hiding this comment.
Worth saying here that each title's text now appears twice in the markup (the visible title and an aria-hidden twin): a consumer test that uses getByText('Grid') finds two nodes and throws, as this pull request's own tests did until they were scoped. Querying segments by role and name keeps working.
| 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 |
There was a problem hiding this comment.
No spec:AST-090 record exists at this head or on main, so the rule FR7 says it departs from cannot be read. Drop the reference, or land that record first and cite it once it exists.
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewComponent Size
Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.
|
| Component | JavaScript | CSS |
|---|---|---|
| core/DateTimeInput | +14.5KB |
+1.2KB |
| core/SegmentedControl | +13.8KB |
+1.3KB |
Bundle Size Summary
Gzip figures measure each package's entry file, not the code an import pulls in.
| Package | Entry (ESM) | Entry gzip before | Entry gzip after | Delta |
|---|---|---|---|---|
| @astryxdesign/core | 4.9KB | 1.2KB | 1.2KB | 0B |
| Package stylesheet | Gzip before | Gzip after | Delta |
|---|---|---|---|
| @astryxdesign/core | 35.4KB | 36.5KB | +1.1KB |
Accessibility Audit
Status: No accessibility violations detected.
Visual Regression
55 of 506 shot(s) changed.
A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.
| component | story | theme | mode | pixels |
|---|---|---|---|---|
| Selector | Indicator Space Bottom Sheet Narrow End RTL | neutral | light | 753,933 |
| Selector | Indicator Space Bottom Sheet Wide Start | neutral | light | 740,072 |
| RichTextEditor | Markdown parity: side by side | probe | dark | 558,840 |
| RichTextEditor | Markdown parity: side by side | probe | light | 540,756 |
| Chat | Full AI Chat | probe | light | 230,823 |
| Chat | Full AI Chat | probe | dark | 229,805 |
| AppShell | With Mobile Nav | probe | light | 217,010 |
| AppShell | With Mobile Nav | probe | dark | 216,960 |
| Collapsible | Dividers | probe | dark | 66,445 |
| Collapsible | Dividers | probe | light | 66,445 |
| AppShell | Playground | neutral | dark | 65,198 |
| AppShell | Playground | neutral | light | 63,788 |
| Chat | Default | neutral | dark | 36,304 |
| Chat | Default | neutral | light | 35,767 |
| Chat | Empty State | probe | light | 23,531 |
| Chat | Empty State | probe | dark | 22,622 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | dark | 15,378 |
| ChatAutoScroll | Scroll Behavior Comparison | neutral | light | 14,107 |
| Chat | With Attachments | probe | light | 13,406 |
| Chat | With Attachments | probe | dark | 11,232 |
and 35 more.
Generated by PR Enrichment workflow | Storybook | Sandbox | View full report
4c31899 to
1632191
Compare
There was a problem hiding this comment.
This pull request can't be approved yet: SegmentedControl's touch slide rests on the draft spec:AST-094, which has no owner decision, and its FR4 narrows the theme-tokens architecture's motion roles for touch presentations. Approval needs an engineering owner to approve or reject AST-094's DEC-1 (UIKit as the touch reference), FR4 and FR13 (the shared resolver and its 744 px / 500 px query) by commenting /approve-spec followed by the full head commit SHA on this pull request; the author may record it as an engineering owner, and the same decision should settle FR8's "never jumps" rule against the slide's instant move under Reduce Motion. Independently of that decision, please:
- Rebase onto
mainonce #8052–#8087 land, so this pull request carries only the SegmentedControl commit instead of 14,981 authored lines. - Remove
springs.segmentedThumbor point it at the measuredsegmentedControl.selectionSpring, and mark M-SG02 and M-SG03 done. - Say in the stable-width changeset that each title's text now appears twice in the markup, and drop or land the
spec:AST-090reference in AST-094 FR7.
[Automated review]
1632191 to
b53773b
Compare
There was a problem hiding this comment.
This pull request can't be approved yet: SegmentedControl's iOS slide rests on the draft spec:AST-094, and the code now departs from it, since themes can retune touch motion (FR4 and DEC-1 say they can't), UIKit motion runs only on iOS (FR13 keys it on any coarse pointer) and Reduce Motion moves the selection instantly (FR8 says it never jumps); the new motion roles and platform adaptation condition also need owner sign-off against the theme-tokens and theme-authoring architecture records. An engineering owner can approve that direction by commenting /approve-spec followed by the full head commit SHA once AST-094 says it (the author may do this as an engineering owner), or ask for the code to follow AST-094 as written. Independently of that decision, please:
- Land the predecessor pull requests and rebase so this one carries only the SegmentedControl commit (18,179 authored lines today; that commit alone is 2,386).
- Make
defineThemevalidate motion roles the same way whatever was imported first, and reject unknown role ids. - List only the motion roles a shipped component reads in
astryx docs motionand the theme examples. - Retire or alias
springs.segmentedThumb, drop or land thespec:AST-090reference in AST-094, and note the doubled label text in the stable-width changeset.
[Automated review]
| `${location}: a motion role is named by its table path, such as "sheet.present" or "springs.navigationPush".`, | ||
| ); | ||
| } | ||
| const registered = motionRole(id); |
There was a problem hiding this comment.
This kind check only runs when the role's table is already registered, and the theme path never imports the tables (the test preloads them at expandMotionRoles.test.ts:15). As a result, defineTheme({name, motion: {durations: {'springs.sheetPresent': 300}}}) returns a theme with --motion-springs-sheet-present: 300ms when no table is loaded, but throws once SegmentedControl has been imported, and a misspelt id is always accepted. Could the theme path load the registered roles (or a generated id-to-kind list) and reject unknown ids?
| content: [ | ||
| { | ||
| type: 'prose', | ||
| text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.', |
There was a problem hiding this comment.
At this head no sheet, alert, menu, navigation or scroll component reads these roles: only SegmentedControl does, through its three segmentedControl.* roles, and only on iPhone and iPad. A theme author who follows the example below and retunes springs.sheetPresent or springs.navigationPush changes nothing. Suggest documenting (and generating) only roles a shipped component reads, and saying iPhone and iPad here.
There was a problem hiding this comment.
This needs a maintainer decision before it can be approved: SegmentedControl's iOS slide, the theme-settable touch motion roles and the platform adaptation condition have no current spec, the code departs from the draft AST-094 (FR4, DEC-1 and FR13), and the roles and the condition also conflict with the current theme-tokens INV1 and AST-012 FR2's closed condition vocabulary. An engineering owner (the author is one) needs to settle whether touch motion is themable and whether the touch idiom is iOS-only, then either amend AST-094 and those theme records and record approval by commenting /approve-spec 4d9e0cbabfd03952cd8b3d56268cfd350cd0e7bc, or bring the code back to AST-094 as written. Independently, please split the 18,553-line stack so this pull request carries only the SegmentedControl commits, make defineTheme validate motion roles the same way whatever was imported first and reject unknown role ids (expandMotionRoles.ts:140-145), and document only roles a shipped component reads (motion.doc.mjs:151).
[Automated review]
| `${location}: a motion role is named by its table path, such as "sheet.present" or "springs.navigationPush".`, | ||
| ); | ||
| } | ||
| const registered = motionRole(id); |
There was a problem hiding this comment.
This kind check only runs when the role's table has already been imported, and the theme path imports none: defineTheme({motion: {durations: {'springs.sheetPresent': 300}}}) is accepted (writing 300ms) in a module that loads no table and throws once SegmentedControl has been imported, and a misspelt id such as segmentedControl.selectionSprng is accepted either way. Please give the theme path the registered roles (or a generated id-to-kind list) and reject unknown ids, with a test that does not preload ../motion/uikit.
There was a problem hiding this comment.
This check comes from #8257 (motion roles), which this branch carries unchanged. The fix belongs in that pull request; this branch picks it up when it restacks.
| content: [ | ||
| { | ||
| type: 'prose', | ||
| text: 'On a touch screen, sheets, alerts, menus, navigation and scrolling play the springs, durations and curves measured from iOS. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default.', |
There was a problem hiding this comment.
At this head only SegmentedControl reads motion roles, and only on iPhone and iPad; no sheet, alert, menu, navigation or scroll component reads any, so this sentence and the example below retune values nothing uses. Please document only roles a shipped component reads, and say iPhone and iPad rather than "a touch screen".
There was a problem hiding this comment.
This text and its generator come from #8257 (motion roles), which this branch carries unchanged. The fix belongs in that pull request; this branch picks it up when it restacks.
| * @measure M-SG02 | ||
| * @measure M-SG03 | ||
| */ | ||
| segmentedThumb: {response: 0.3, dampingRatio: 0.75}, |
There was a problem hiding this comment.
segmentedThumb is still tagged @source GUESS (M-SG02, M-SG03) while segmentedControl.selectionSpring holds the measured 0.40 s / 0.85 for the same slide, and it is published as the theme role --motion-springs-segmented-thumb that nothing reads. Please delete it or point it at the measured value.
There was a problem hiding this comment.
Fixed in b7efa54: springs.segmentedThumb is gone (nothing read it), the role table is regenerated, and M-SG02 and M-SG03 are marked done in MEASUREMENTS.md. A test now checks that the segmented slide has exactly one spring, the measured segmentedControl.selectionSpring.
| 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 |
There was a problem hiding this comment.
No spec:AST-090 record exists at this head or on main, so the rule FR7 departs from cannot be read (Current-state impact repeats it). Please drop the reference or cite the record once it exists.
There was a problem hiding this comment.
AST-094 comes from #8052, which this branch carries unchanged. The fix belongs in that pull request; this branch picks it up when it restacks.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] SegmentedControl segments keep their width when the selection changes. The selected label is heavier, so a segment sized to its own label grew as it was selected and shrank as it was deselected (about 2–3 px in a content-sized control), shifting its neighbors. Each label now reserves its selected width. |
There was a problem hiding this comment.
Each titled segment now renders its label twice (the visible title and an aria-hidden twin), so a consumer test that finds a segment with getByText gets two nodes and throws, as this pull request's own tests did until they were scoped. Please say so here and suggest querying segments by role and name.
4d9e0cb to
c47ace9
Compare
|
On the size: this branch is a stack. It is main with merges of the pull requests it depends on (#8258, which carries #8257 and #8067; #8062; #8087), each under its own review, and the SegmentedControl commits on top. The description's first line names the commits to review here. As the predecessors land, the diff shrinks to those commits alone. The touch-presentation query, the interaction harness and the stable segment width are already their own pull requests (#8062, #8067, #8087). |
There was a problem hiding this comment.
This needs an engineering owner's decision before it can be approved: SegmentedControl's iOS slide, the theme-settable touch motion roles, the platform adaptation condition and the iOS-only idiom have no current spec, the code departs from the draft AST-094 (FR4, DEC-1 and FR13), and the roles and the condition also conflict with the current theme-tokens INV1 and AST-012 FR2. An engineering owner (the author may record it as one) can either amend AST-094 and those records to match the code and record approval by commenting /approve-spec c47ace931357112ab6cb83738b21af21b7371c53, or the code can return to AST-094 as written. Independently of that decision, please:
- load the slide's motion code lazily or justify the 11,410 bytes gzip it adds to every SegmentedControl import (component-style-authoring INV6);
- build the adaptive compact query from the theme's
mdand the 500 px height edge instead of a hard-coded 744 px, so an iPad mini in portrait gets the sheet again (AST-012 FR9); - make
defineThemevalidate motion roles the same way whatever was imported first, and reject unknown role ids; - split the 18,645-line stack so this pull request carries only the SegmentedControl commits.
[Automated review]
| export const TOUCH_QUERY = '(pointer: coarse)'; | ||
|
|
||
| /** The viewport is compact: layout behaviours. */ | ||
| export const COMPACT_QUERY = '(width < 744px), (height < 500px)'; |
There was a problem hiding this comment.
Current AST-012 FR9 owns this definition: a screen is compact while its width is below the theme's effective md (768 by default) or its height is below 500, and every resolver that picks a presentation by size, this one named, reads it rather than keeping numbers of its own. With 744 here a coarse-pointer screen 744–767 px wide (an iPad mini in portrait) gets the popover, where both the base's (max-width: 768px) and FR9 give it the sheet. Could the query be built from the theme's md and the 500 edge, with the tests moved to FR9's 767/768 and 499/500 states?
| import { | ||
| createAnimator, | ||
| type Animator, | ||
| type MotionCurve, | ||
| type MotionHandle, | ||
| } from '../motion/animator'; | ||
| import {cubicBezier} from '../motion/bezier'; | ||
| import {resolveMotion} from '../motion/resolveMotion'; | ||
| import {segmentedControl} from '../motion/uikit'; |
There was a problem hiding this comment.
These static imports put the animator, the spring math, the role registry and the timing and scroll tables into every SegmentedControl import: a minified bundle of @astryxdesign/core/SegmentedControl grows from 36,140 to 47,550 bytes gzip, although only the iOS idiom runs this code. component-style-authoring INV6 asks touch-only work over 2 KB gzip to load lazily or be justified, and to ship a component only the table rows it uses. Could the controller be imported dynamically the first time the iOS idiom resolves?
c47ace9 to
b7efa54
Compare
meta#8076's new head carries main 70ea859 with meta#8073's writer of the press attribute on main's press controller, and its README names both writers. utils/pressFeedback.ts keeps that controller with this line's own changes on it: the pointer, drag, scroll and visibility listeners through the shared input stream (meta#8356) and touchcancel only while the primary pointer is coarse (meta#8270); the key path stays at the window's bubble phase, so a Space press the page cancels paints nothing. usePressTracker stays in its own module, one tracker for a component's life (meta#8315), so meta#8073's rebuilt-tracker hook and its test are not taken. The harness files are meta#8076's.
…s, and the action-group tracker writes its own The header and writePress called it the one writer of the press attribute, but main's action-group tracker writes its groups' presses itself. They now say what holds, as meta#8076's gestures README does.
…he press attribute The release note said every press painter writes data-astryx-press through one internal writer, but the action-group tracker (hooks/actionGroupTracker.ts) writes its groups' presses itself, as the gestures README and utils/pressFeedback.ts already say. The note now says paintPress writes every press it tracks and the action-group tracker writes its own.
…llowance under every primary pointer The useMenuPress record's FR9 puts the trigger's held finger under spec:AST-094 FR29 under every primary pointer (FR43), so a finger on a touchscreen laptop may drift 13 pt before the open, as on a phone. The trigger read (pointer: coarse) as its press started and took the long press's 10 px where the pointer was fine. Three cases fail until it does not: a 12 pt drift opens under a fine pointer too, a (12, 12) settle opens there, and nothing reads a pointer query, the trigger's press included.
…lowance under every primary pointer The useMenuPress record's FR9 governs the trigger's held finger by spec:AST-094 FR29 under every primary pointer (FR43). The trigger now hands the long press FR29's 13 px drift allowance whatever the primary pointer, so it no longer reads (pointer: coarse) as its press starts, and the menus drop their coarsePointer import. ContextMenu keeps the long press's own allowable movement. The changeset says the same.
Strict ESLint reported six unused bindings in files this pull request carries: the host's HAPTIC_SWITCH_ATTRIBUTE import, two unused imports in the pan's tests (scrollProbe, ScreenEdgePanGestureRecognizer), the swipe test's drift, the presented-hit-testing test's gestures, and a GestureHost test's before, which now asserts what it was read for: a second finger's moves leave the pan where it was.
…eal angular speed The release velocity of a rotation whose listener sets rotation = 0 on each change, at 2 degrees every 16 ms, read 4.363 rad/s against the 2.182 rad/s of the same stream without a re-base: the test asserted only more than 1.5. It now asserts the 2.18 rad/s (spec:AST-100 FR2), and fails.
The rotation's velocity tracker samples the raw unwrapped angle, which setting rotation does not move, so the setter's shift of the samples read each re-base as extra turn and doubled the release velocity of a listener that re-bases on every change. The setter no longer touches the tracker.
…mates while it runs The proof read the element's animations 230 ms in, after the dip had ended, and asserted only that every animated key is opacity, which an empty list passes: with the cross-fade removed from the plan (the move still landing), the proof still passed. It now reads the keyframe keys right after the call starts and asserts they include opacity and nothing else; the same mutant fails it. Chromium and WebKit pass. (F1's in-flight read from c648034.)
…roller, and its claimed moves are cancelled under pointer events Two comparisons stop at the shadow boundary, each failing a case: - once the host's listener on a recognizer's inner node has seen the finger, isFingerDownIn reads it as outside a light-DOM scroller around that root, so a scroll under it is not the finger's (FR20); - under inputMode 'pointer-events', a claimed pan on a shadow host cancels none of its finger's moves over the inner content, since the finger's target is the inner node and its touches reach the host retargeted (FR19).
…the flat tree isFingerDownIn and the pointer-events finger match compare with containsAlongFlatTree, as the scroll watch already does: a finger on content inside an open shadow root is down in the scroller around that root, and its touches, retargeted to the shadow host, match its pointer finger, so a claimed pan cancels its moves there (spec:AST-094 FR19, FR20).
…achments asks, and only then The host let the last attachment decide whether an element is an action group (useGestureRecognizer always passes the option), and never recomputed it at a detach: a plain attachment after the group's turned the group off, and a group's attachment released left a recognizer that never asked taking presses under a fine pointer. Fails until the host counts the attachments that ask.
…ents asks The host counts the attachments that pass actionGroup: true and keeps the element a group while one remains, recomputed at each detach. An attachment that passes false, as useGestureRecognizer does by default, no longer turns a group off, and a group's last asking attachment taking its leave ends the group.
… that content The presented hit test takes the document's elementFromPoint, which answers a shadow host for anything inside its open root, and compares with Node.contains, which stops at the boundary: a recognizer on a control inside a moving sheet's shadow root gets no finger, and the tap's click goes to the host. Fails until the hit test descends into open roots and compares along the flat tree.
Where a moving element is drawn away from its layout box, the host hit-tests what is drawn under the finger with the document's elementFromPoint, which answers a shadow host for anything inside its open root. The hit test now descends through each open root's own elementFromPoint and compares with the movers along the flat tree, so a recognizer on a control inside a moving sheet's shadow root gets its finger and the tap clicks that control.
…nt lets go The host's touch-action rules sit in an adopted sheet or a style element per document or shadow root, and were never removed, so a page whose last recognizer detached kept them, against spec:AST-094 FR24 (nothing touch-related remains after the last instance unmounts). Fails until a root's rules go with its last marked element.
…th its last marked element The touch-action writer records which elements it marked in each document or shadow root, and when the last one is unmarked (its last recognizer detached) it removes that root's adopted sheet or style element, so nothing touch-related remains after the last instance unmounts (spec:AST-094 FR24). A later mark writes the rules again; an element marked in another root first lets go of the old one.
… the inline style it reads - The motion registry, the host's engine gate and the README name the presented hit test as an engine workaround (spec:AST-013 FR6) with its review condition (re-measure on each iOS release; remove it once iOS WebKit hit-tests a compositor-moved element where it is drawn) and its regression fixture. - While a curve runs the animator holds the value it starts from inline, so presentation.ts and the README no longer say the inline style holds the end.
…core/internal/touch Core ships the internal entry @astryxdesign/core/internal/touch, which re-exports the gesture primitives for Astryx's own packages, and the Lab-import guard fails a Lab import of core outside core's exports (architecture:react-component-runtime INV13). The README and the barrel's header named internal workspace imports and said nothing here is exported; they now name that entry.
…ill needs the element, and the listeners that cancel The release note, the stream's header, the host's comment and the README said the stream is one passive listener per event type and that a document-capture stopPropagation no longer starves the recognizers. Such a stop never hid a listener on the same node, and some subscriptions cancel. They now say what changed: an app's window-capture stopPropagation, or a document-capture stopImmediatePropagation registered before core, no longer hides a touch; a recognizer that must claim still needs the touch to reach its element; the gesture host's touchend, click and contextmenu cancel while a claimer is attached and the page hold's touchmove while it holds; and the stream stays ahead of an app's window-capture listener only until a type's listener is re-added. The README's list of blocking touchmove listeners gains the claimed touch's target and the document while a claiming element moves.
…default it moves
The {stylexOptions} form of astryxStylex now passes StyleX
treeshakeCompensation: true unless the caller sets it. spec:AST-017 FR3
asks the release note to name the old and new default and the option
that restores the old one; it now does.
…er and for the two stops the input stream fixes - A still finger with sub-pixel jitter over a scroll view keeps its tap and scrolls nothing (spec:AST-094 FR18–FR22). - A tap is still recognized under an app's window-capture stopPropagation, and under a document-capture stopImmediatePropagation registered before core's: the recognizers hear touches on the window's capture phase. Chromium and WebKit pass.
…s once Where nothing that claims is attached, the host's touchend listener is passive and cannot cancel the engine's own click after a retargeted tap clicks what is drawn. The shared click guard swallows that late click whatever is attached; this case pins it, as asked on meta#8293.
…esture host's subscriptions The gesture host subscribes no click: the one cancelling click subscriber is the click guard's, added at a window's first arm and kept. The release note, the stream's header and the gestures README listed click with the host's touchend and contextmenu; they now name the guard's own.
…toryboards re-recorded on the new foundation
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about direction: the SegmentedControl touch slide and the new segmented.selectionSpring row in AST-094 FR4 are not stated by any current spec, because AST-094 leaves each touch presentation to the component's own record and the current SegmentedControl record names none. An engineering owner can record that decision by commenting /approve-spec 7cecbcc66f6508e5826c4489a7a6ff296371b891 on this pull request (the author may do so as an engineering owner), or the slide and its role can first be stated in the SegmentedControl record. Whatever that decision, these still need changes:
- Move
position: relativeandisolation: isolatefrom the container into the touch-only lens styles (SegmentedControl.tsx:106-107), so fine-pointer renders stay unchanged as AST-094 FR13 requires. - Rebase once #8076 and #8360 land, so this pull request carries only the slide; today it is about 30,400 authored lines.
- Add real iOS Safari and Android Chrome recordings, which AST-094's verification asks for, and justify the +1.3 KB gzip of touch-only CSS as the component size rule requires.
[Automated review]
| position: 'relative', | ||
| isolation: 'isolate', |
There was a problem hiding this comment.
These two declarations apply to every SegmentedControl under every pointer, so desktop and pre-hydration renders gain a positioned, isolated stacking context that only the touch lens needs. AST-094 FR13 has a touch presentation apply under a coarse primary pointer and only after hydration. Moving both into styles.lens, which is applied as isTouchPresentation && styles.lens, keeps fine-pointer output unchanged.
| | `springs.menuClose` | damping ratio 0.85, response 0.35 s | measured: UIMenu closing, iOS 26.2 simulator, 2026-10-23 | | ||
| | `springs.subMenuOpen` | damping ratio 0.85, response 0.4 s | measured: UIKit's own spring for UIMenu's sub-menu opening, iOS 26.2 simulator, 2026-10-24 | | ||
| | `springs.subMenuBack` | damping ratio 1, response 0.4 s | measured: UIKit's own spring for UIMenu's sub-menu header back, iOS 26.2 simulator, 2026-10-24 | | ||
| | `segmented.selectionSpring` | response 0.4 s, damping ratio 0.85 | measured: UISegmentedControl's selection slide, iOS 26.2 simulator, 2026-10-23 | |
There was a problem hiding this comment.
This row adds a normative motion role to AST-094 that current main does not state, and DEC-5 has the roles a component measures join through the generated table from that component's own record. It needs an engineering owner's decision (/approve-spec with this head's full SHA), or a home in the SegmentedControl record.
Depends on #8360. Review only this PR's change: vjeux/astryx@045f666...7cecbcc
User need
Astryx is making its core components behave like UIKit under a finger (
spec:AST-094). On a phone, picking another segment makes the selection jump; on iOS it slides from the old segment to the new one, so the eye can follow the change.Problem and solution fit
Under a finger every change slides (a tap, an arrow key or a new
value), continuing from where it is if interrupted. With Reduce Motion it cross-fades in place. A mouse keeps today's color fade.Current specification
The touch-presentation rows of the record owner's draft #8596 (its FR5–FR6);
spec:AST-094FR4, UIKit's measured spring as the internal rolesegmented.selectionSpring;design:reduced-motion.Public delta and caller burden
No API, prop or theme change; screen readers see the same radios. Gap: icons change tint at once.
Design and alternatives
::before, moved bytransform. Themes keep styling the selected segment, no element is added, and the slide keeps moving while JavaScript is busy.segmented-control-item, breaking themes); CSS transitions (they fade in place and cannot travel); UIKit's Reduce Motion scale-in (design:reduced-motionforbids scaling).Evidence
Scope
Writing rubric results
Testing
Automated, passing:
pnpm test:interaction SegmentedControl.interaction(real touch, Chromium and WebKit) checks the slide against the measured spring and the landing pixel for pixel;pnpm vitest run packages/core/src/SegmentedControl packages/core/src/motioncovers its logic. Recorded on the iOS 26.2 simulator and the Android emulator.