Repository navigation
feat(lab): under a finger, NavigationStack's bar takes the iOS 26 look - #8396
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 needs an owner decision before it can be approved, and it needs changes either way. NavigationStack's touch bar look relies on spec:AST-094 FR13 and FR4, which are still in open spec pull requests (#8316, #8378, #8394), and main has no NavigationStack record, so an engineering owner needs to identify or provide the current spec that governs this appearance; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and as an engineering owner you may do so yourself. Independently of that decision, please:
- rebase onto
mainonce the 16 stacked pull requests land, so the diff is this change alone (it is 52,339 authored lines today against a 1,840-line commit); - keep Lab's back swipe off core's gesture tracker, as
architecture:interaction-modalityINV10 requires, or amend INV10 onmainfirst; - reclassify the row-press change that hides consumers'
xstyle::beforedecoration under a finger, which ships as[fix]; - record the
architecture:component-style-authoringINV7 element counts for the elements the bar change adds, and drop the nonexistent@astryxdesign/core/motion/experimentalpath from two changesets.
[Automated review]
| import { | ||
| PanGestureRecognizer, | ||
| ScreenEdgePanGestureRecognizer, | ||
| useGestureRecognizer, | ||
| } from '@astryxdesign/core/gestures'; |
There was a problem hiding this comment.
The back swipes now run on core's PanGestureRecognizer and ScreenEdgePanGestureRecognizer, and through them core's VelocityTracker, which the gestures barrel re-exports for Lab. architecture:interaction-modality INV10 says Lab cannot import the tracker and keeps one local tracker with the same blend and stillness interval. Either keep a Lab-local tracker here or amend INV10 on main first.
There was a problem hiding this comment.
This comes from the back swipe in #8108, which this pull request is stacked on, and its owner is taking INV10 up there. This pull request's own commit does not touch the tracker.
| opacity: { | ||
| default: null, | ||
| [COARSE]: { | ||
| default: 0, | ||
| [ENABLED]: {default: null, [PRESSED_ON]: 1, [PRESSED_HELD]: 1}, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Under a coarse pointer this gives every interactive row's ::before opacity: 0 at rest, so a consumer's existing xstyle ::before decoration disappears; list-row-press-compositor.md ships that as [fix]. spec:AST-017 DEC-1 asks for the consumer scenario that stops working to be named and kept out of a patch release.
There was a problem hiding this comment.
This is the row-press change from #8114, lower in the stack, and its owner has it.
| <div {...stylex.props(styles.edgeSentinelAnchor)}> | ||
| <div {...edge.sentinelProps} data-navigation-scroll-edge-sentinel /> | ||
| </div> | ||
| {props.children} | ||
| </div> | ||
| </div> | ||
| <div {...edge.layerProps} data-navigation-scroll-edge /> |
There was a problem hiding this comment.
The anchor, sentinel and veil mount on every screen under every pointer, beside the clip box and row wrapper at lines 791-792. architecture:component-style-authoring INV7 asks for before-and-after element counts for added elements and puts decoration such as the veil on a pseudo-element; the anchor could go if the content wrapper positioned the sentinel.
There was a problem hiding this comment.
Done at 9f51e05. The sentinel's positioned wrapper is gone: it sits at its static position in the scroller. The description records the element counts before and after, with the reason each remaining element is needed: the sentinel is what IntersectionObserver watches, the veil is a layer the shared animator fades with inline values, and the moving cut needs an overflow box and a counter-moved row to stay on the compositor.
There was a problem hiding this comment.
A correction to the veil's reason above. The animator can drive a pseudo-element, but it holds a pseudo-element's resting value with a filling animation, and that keeps the layer composited at rest. This hook is shared with AppShell. There, a composited layer under the sticky header makes iOS 26.2 drop the header's items for about two frames on every programmatic scroll. Reduced to plain HTML: with no layer, 0 of 5 takes blank; with a composited veil that never switches, 5 of 5 do. The description now says so.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [experimental] The `uikit` table in `@astryxdesign/core/motion/experimental` gains the navigation values the lab NavigationStack runs on, each measured on iOS 26.2: `navigation.contentPopHysteresis` (the content back swipe's 27 pt start), `navigation.interactivePopVelocitySamples` (the release velocity averages the last 3), `navigation.interactivePopRubberBand` (past its start the back swipe's screen follows at 0.71 of the finger) and `springs.navigationInteractivePopReturn` (its return when released there), `springs.navigationBackButton` (the back chevron's fade), and `navigation.backChevronPushDelayMs` (when the chevron starts fading in on a push). The catalog gains `@astryx.navigationStack.back`. |
There was a problem hiding this comment.
@astryxdesign/core/motion/experimental does not exist: core's exports list no such subpath and the motion modules are internal. Rewrite this as an internal [chore] note like the other motion changesets; uikit-navigation-large-title-rise.md has the same problem.
dfc5838 to
9f51e05
Compare
|
Thanks. At 9f51e05:
|
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved, and it needs changes either way. NavigationStack's touch bar appearance under a finger (no background or hairline, the veil once content is 10px under the bar, the covered bar cut at the top card's leading edge) has no current spec on main: spec:AST-094 is still in open pull requests, and its FR13 and FR4, as proposed in #8394, cover when a touch presentation applies and that its motion is themeable, not how the bar looks. An engineering owner needs to provide or identify the claim that governs this appearance, or record the decision by commenting /approve-spec <full head SHA> on this pull request; as an engineering owner, you may do so yourself. Independently of that decision:
- the veil's new
will-change: opacitycomposites AppShell's touch header at rest as well, the condition your own iOS 26.2 reduction says blanks its items on programmatic scrolls; scope it to NavigationStack and re-check AppShell, or carry the veil on a pseudo-element; - rebase onto
mainonce the 19 stacked pull requests land or are dropped, so the diff is this change alone (52,398 authored lines today); - the stacked code still runs Lab's back swipes on core's gesture tracker against
architecture:interaction-modalityINV10, ships the row-press::beforechange as[fix]although consumers must move their decoration, and changes AppShell's header andheightcontract without amendingcomponent:AppShell.
[Automated review]
| willChange: { | ||
| default: null, | ||
| '@media (pointer: coarse)': 'opacity', | ||
| }, |
There was a problem hiding this comment.
This composites the veil at rest for every caller under a finger, AppShell's touch header included. The pull request keeps the veil an element because a veil composited at rest under AppShell's sticky header blanks its items on iOS 26.2 programmatic scrolls, and these lines recreate that condition. Pass the will-change through NavigationStack's layerXstyle only and repeat the AppShell check on iOS 26.2, or carry the veil on a pseudo-element.
9f51e05 to
74e0a84
Compare
|
An update on the three points that come from the pull requests below this one:
|
There was a problem hiding this comment.
This needs two engineering-owner spec decisions before it can be approved, and it needs changes either way. NavigationStack's touch bar appearance under a finger (no background or hairline, the veil once content is 10px under the bar, the covered bar cut at the top card's leading edge) has no current spec on main, since spec:AST-094 is still in open pull requests and there is no NavigationStack record, and the back swipes running on core's gesture recognizers need the architecture:interaction-modality INV10 amendment you sent to be made on main. An engineering owner needs to provide or identify the claim for the appearance and update INV10, or record each decision by commenting /approve-spec <full head SHA> on this pull request; as an engineering owner, you may do so yourself. Independently of those decisions:
- Under a finger, the Large title story shows the large title through the clear bar beside the faded-in inline title from about 55px to 65px of scroll, so start the veil or clip the large title where it reaches the bar.
- Rebase onto
mainonce the stacked pull requests land or are dropped, so the diff is this change alone (52,635 authored lines today). - The stacked code still changes AppShell's header and
heightcontract without amendingcomponent:AppShell, hides a row'sxstyle::beforeunder a finger in a[fix](your reported fix is not in this head yet), and prunes search state in a layout effect againstarchitecture:react-component-runtimeINV1. - Add the
architecture:component-style-authoringINV6 gzip size report for NavigationStack and AppShell.
[Automated review]
| </div> | ||
| ) : null} | ||
| <div ref={content.ref}> | ||
| <div {...edge.sentinelProps} data-navigation-scroll-edge-sentinel /> |
There was a problem hiding this comment.
Under a finger the bar is now clear, but this sentinel sits below the large title, so the veil comes in only once the large title has passed the bar and 10px more, while the inline title takes over as soon as the large title's bottom passes the bar's (lines 529-547). In the Large title story, from 55px to 65px of scroll the inline title is at opacity 1, the veil at 0 and the large title still shows in the bar: the two titles overlap. With a mouse the opaque bar hides it. Starting the veil, or clipping the large title, where the large title reaches the bar would close the gap.
| // Where the screen's scroll edge sentinel sits: the top of its content, | ||
| // below the large title and the search field (an expanded large title is | ||
| // outside iOS's veil). The sentinel is absolutely positioned in it. | ||
| // The scroll edge sentinel stays at its static position, the top of the |
There was a problem hiding this comment.
This first paragraph still describes an absolutely positioned sentinel in a positioned wrapper; the paragraph after it says the sentinel stays at its static position with no wrapper. The first one can go.
| useLayoutEffect(() => { | ||
| const present = new Set(view.models.map(model => model.key)); | ||
| const forget = (records: Map<string, unknown> | Set<string>) => { | ||
| for (const key of [...records.keys()]) { | ||
| if (!present.has(key)) { | ||
| records.delete(key); | ||
| } | ||
| } | ||
| }; | ||
| forget(searchingRef.current); | ||
| forget(heldCollapsesRef.current); | ||
| forget(searchRestRef.current); | ||
| forget(freshLiftsRef.current); | ||
| setLifts(current => { | ||
| const gone = [...current.keys()].filter(key => !present.has(key)); | ||
| if (gone.length === 0) { | ||
| return current; | ||
| } | ||
| const next = new Map(current); | ||
| for (const key of gone) { | ||
| next.delete(key); | ||
| } | ||
| return next; | ||
| }); | ||
| }, [view.models]); |
There was a problem hiding this comment.
This layout effect sets state (setLifts) from view.models and names no external system, which architecture:react-component-runtime INV1 and INV3 rule out. Dropping the gone keys in the same update that produces the new view, or keeping the lift with the screen's mounted card, would avoid the repair render.
|
Posted by Cindy's agent on her behalf. This rides #8142: Cindy asked there for an approach within her guardrails (themeable visuals through the existing theme infrastructure, at most one new theme role, following the theming spec, respecting variants), and that same approach should cover NavigationStack. |
|
This follows the approach proposed on #8142 (#8142 (comment)): the same hook, the same single motion role, and the band painted from the screen's existing background token until NavigationStack's bar has a theme target. I'll update this pull request together with #8142 once the approach is agreed. |
74e0a84 to
8363522
Compare
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved, and it needs changes either way. NavigationStack's touch bar appearance (no background or hairline under a coarse pointer, the veil once a screen's content is 10px under the bar, the covered bar cut at the top card's edge) has no current spec: spec:AST-094 governs touch behaviour only and leaves each component's presentation to its own record, and main has no NavigationStack record. An engineering owner needs to state that appearance in a record on main, or record the decision by commenting /approve-spec <full head SHA> on this pull request; as an engineering owner, you may do so yourself. Independently of that decision:
registry-contract (preview)fails because Lab's entry now imports@astryxdesign/core/motion/*,@astryxdesign/core/gesturesand@astryxdesign/core/utils/touchPresentation, which core does not export andspec:AST-017FR21 keeps internal.- AppShell's import grows by 23,522 bytes gzip with code that works only under a coarse pointer, so report component sizes and load that code lazily or justify it, as
architecture:component-style-authoringINV6 asks. - Under a finger the large title shows through the clear bar beside the inline title for about 10px of scroll, so start the veil where the large title reaches the bar.
- Each screen's scroll edge effect adds its own document-level
scrollendlistener againstspec:AST-094FR24, and the veil's springs need to be FR4 motion roles. - Rebase onto
mainonce the stacked pull requests land, so the diff is this change alone (46,083 authored lines today).
[Automated review]
| springs, | ||
| } from '@astryxdesign/core/motion/uikit'; | ||
| import {useAnimator} from '@astryxdesign/core/motion/useAnimator'; | ||
| import {useScrollEdgeEffect} from '@astryxdesign/core/motion/useScrollEdgeEffect'; |
There was a problem hiding this comment.
Core's exports do not list ./motion/useScrollEdgeEffect (nor the other motion/*, gestures and utils/touchPresentation paths Lab now imports), and spec:AST-017 FR21 keeps them internal. Outside the workspace's own resolvers they do not resolve: CI's registry-contract (preview) fails at this head with 23 “Could not resolve” errors from Lab's built NavigationStack and SearchField files, this import among them.
| import {focusOutlineProps} from '../utils/focusOutline.stylex'; | ||
| import {useMediaQuery} from '../hooks/useMediaQuery'; | ||
| import {useTouchPresentation} from '../utils/touchPresentation'; | ||
| import {useScrollEdgeEffect} from '../motion/useScrollEdgeEffect'; |
There was a problem hiding this comment.
This static import brings the gesture host, the finger-scroll watch and the animator into every AppShell import, mouse included; CI's size analysis puts AppShell's import at 80,359 bytes gzip against 56,837 at the merge base. architecture:component-style-authoring INV6 asks for the size report and for coarse-pointer-only code over 2 KB to load lazily or be justified.
| const unsubscribeFingers = host.onFingersChange(updatePromotion); | ||
| // After the finger-scroll watch has heard the same `scrollend`. | ||
| const onScrollEnd = () => queueMicrotask(updatePromotion); | ||
| doc.addEventListener('scrollend', onScrollEnd, PASSIVE_CAPTURE); |
There was a problem hiding this comment.
One document-level scrollend listener per running effect, and NavigationStack runs one effect per mounted screen. spec:AST-094 FR24 says the number of document- or window-level touch listeners must not grow with component instances; the shared finger-scroll watch already keeps one per document (gestures/fingerScroll.ts).
| * Registers the bars table (motion/roles.ts). It lists no roles: the scroll | ||
| * edge effect's springs stay internal constants until the public | ||
| * motion-token set is decided (`spec:AST-094` FR4, DEC-11), and the switch, | ||
| * the extent and the veil are not motion (not springs, durations or curves). | ||
| * Nothing registers on import: the component that animates with this table | ||
| * (AppShell; Lab's NavigationStack) hands it to the scroll edge effect | ||
| * (motion/useScrollEdgeEffect.ts), which calls it once before its first | ||
| * animation (it is idempotent). | ||
| */ | ||
| export function registerBarsTable(): void { | ||
| registerMotionTable('bars', bars); |
There was a problem hiding this comment.
With no roles registered, the veil's fades play these springs as plain constants. spec:AST-094 FR4 asks for every spring a touch presentation plays to be a motion role named for its use; DEC-11 keeps the roles internal (not theme-settable) rather than removing them.
There was a problem hiding this comment.
This needs an engineering owner's spec decision before it can be approved, and it needs changes either way. NavigationStack's touch bar appearance (no background or hairline under a coarse pointer, the veil once a screen's content is 10px under the bar, the covered bar cut at the top card's edge) has no current spec: spec:AST-094 governs touch behaviour only and leaves each component's presentation to its own record, and main has no NavigationStack record, so an engineering owner needs to state that appearance in a record on main, or record the decision by commenting /approve-spec <full head SHA> on this pull request; as an engineering owner, you may do so yourself. Independently of that decision:
registry-contract (preview)fails because Lab now imports@astryxdesign/core/motion/*,@astryxdesign/core/gesturesand@astryxdesign/core/utils/touchPresentation, which core does not export, andarchitecture:react-component-runtimeINV13 onmainnow routes Lab through@astryxdesign/core/internal/touch.- Under a finger the large title shows through the clear bar beside the inline title for about 10px of scroll, so start the veil where the large title reaches the bar.
- Add the
architecture:component-style-authoringINV6 size report (CI measures AppShell's import 24,731 bytes gzip larger) and flag the touch-only code, which INV6 onmainnow routes through the shared internal touch modules rather than lazy loading. - Each screen's scroll edge effect adds its own document-level
scrollendlistener againstspec:AST-094FR24, and the veil's springs need to be FR4 motion roles. - Rebase onto
mainonce the stacked pull requests land, so the diff is this change alone (46,559 authored lines today).
[Automated review]
| springs, | ||
| } from '@astryxdesign/core/motion/uikit'; | ||
| import {useAnimator} from '@astryxdesign/core/motion/useAnimator'; | ||
| import {useScrollEdgeEffect} from '@astryxdesign/core/motion/useScrollEdgeEffect'; |
There was a problem hiding this comment.
architecture:react-component-runtime INV13, now on main, has Core and Lab reach core's touch modules through @astryxdesign/core/internal/touch, an ./internal/* subpath that spec:AST-017 FR21 allows and lint limits to Core and Lab. This import, like the other @astryxdesign/core/motion/*, gestures and utils/touchPresentation imports, resolves only inside the workspace: CI's registry-contract (preview) fails at this head with 28 unresolved imports from Lab's built NavigationStack and SearchField files.
| import {focusOutlineProps} from '../utils/focusOutline.stylex'; | ||
| import {useMediaQuery} from '../hooks/useMediaQuery'; | ||
| import {useTouchPresentation} from '../utils/touchPresentation'; | ||
| import {useScrollEdgeEffect} from '../motion/useScrollEdgeEffect'; |
There was a problem hiding this comment.
architecture:component-style-authoring INV6 on main no longer asks for lazy loading here: touch-only JavaScript over 2 KB is flagged and either comes from the shared internal touch modules (architecture:react-component-runtime INV13) or is justified, and the pull request reports base-against-head gzip sizes. CI measures AppShell's import at 56,989 bytes gzip at the merge base and 81,720 at this head; the report and the flag are what is missing.
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| lab/NavigationScreen | 🔴 78.1 kB (+3.0 kB) · 1.5 kB CSS (+434 B) | — | — · ✓ · ✗ |
| lab/NavigationStack | 🔴 78.1 kB (+3.0 kB) · 1.5 kB CSS (+434 B) | — | — · ✓ · ✓ |
Package size: measured; main has no measurement of the merge base to compare yet.
API changes: cli +14 · ~5 · −0 (against the merge base's committed report)
Committed API report behind this build: cli 19 rows, core 21 rows; run pnpm api:report.
19 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | cli |
./api · Types · type SearchResultEntry |
deprecated?: ComponentDeprecation | undefined |
import type {SearchResultEntry} from '@astryxdesign/cli/api'; const x: SearchResultEntry = {deprecated: …} |
| + | cli |
./authoring · Types · interface ComponentDeprecation |
interface ComponentDeprecation |
import type {ComponentDeprecation} from '@astryxdesign/cli/authoring' |
| + | cli |
./authoring · Types · interface ComponentIconSlotDoc |
interface ComponentIconSlotDoc |
import type {ComponentIconSlotDoc} from '@astryxdesign/cli/authoring' |
| + | cli |
./authoring · Types · interface ComponentIconSlotStateDoc |
interface ComponentIconSlotStateDoc |
import type {ComponentIconSlotStateDoc} from '@astryxdesign/cli/authoring' |
| + | cli |
./authoring · Types · interface MultiComponentDoc |
deprecated?: ComponentDeprecation |
import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {deprecated: …} |
| + | cli |
./authoring · Types · interface SingleComponentDoc |
deprecated?: ComponentDeprecation |
import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {deprecated: …} |
| + | cli |
./doc · Types · interface ComponentBaseDoc |
deprecated?: ComponentDeprecation |
import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {deprecated: …} |
| + | cli |
./doc · Types · interface SubComponentDoc |
deprecated?: ComponentDeprecation |
import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {deprecated: …} |
| + | cli |
./doc · Also exported from an earlier entry point · ComponentDeprecation |
ComponentDeprecation (./authoring) |
import {ComponentDeprecation} from '@astryxdesign/cli/doc' |
| + | cli |
./doc · Also exported from an earlier entry point · ComponentIconSlotDoc |
ComponentIconSlotDoc (./authoring) |
import {ComponentIconSlotDoc} from '@astryxdesign/cli/doc' |
| + | cli |
./doc · Also exported from an earlier entry point · ComponentIconSlotStateDoc |
ComponentIconSlotStateDoc (./authoring) |
import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/doc' |
| + | cli |
./template · Also exported from an earlier entry point · ComponentDeprecation |
ComponentDeprecation (./authoring) |
import {ComponentDeprecation} from '@astryxdesign/cli/template' |
| + | cli |
./template · Also exported from an earlier entry point · ComponentIconSlotDoc |
ComponentIconSlotDoc (./authoring) |
import {ComponentIconSlotDoc} from '@astryxdesign/cli/template' |
| + | cli |
./template · Also exported from an earlier entry point · ComponentIconSlotStateDoc |
ComponentIconSlotStateDoc (./authoring) |
import {ComponentIconSlotStateDoc} from '@astryxdesign/cli/template' |
| ~ | cli |
./api · Functions · function search |
function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "hook" | "theme" | "doc" | undefined; limit?: number | undefined }) => Promise<SearchResponse> → function search: (query: string, options?: { cwd?: string | undefined; type?: "template" | "component" | "doc" | "theme" | "hook" | undefined; limit?: number | undefined }) => Promise<SearchResponse> |
import {search} from '@astryxdesign/cli/api'; search(…) |
| ~ | cli |
./authoring · Types · interface MultiComponentDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {MultiComponentDoc} from '@astryxdesign/cli/authoring'; const x: MultiComponentDoc = {theming: …} |
| ~ | cli |
./authoring · Types · interface SingleComponentDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {SingleComponentDoc} from '@astryxdesign/cli/authoring'; const x: SingleComponentDoc = {theming: …} |
| ~ | cli |
./doc · Types · interface ComponentBaseDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {ComponentBaseDoc} from '@astryxdesign/cli/doc'; const x: ComponentBaseDoc = {theming: …} |
| ~ | cli |
./doc · Types · interface SubComponentDoc |
theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[] } → theming?: { container?: boolean; targets: ComponentThemingTarget[]; vars?: ComponentThemingVar[]; derived?: ComponentThemingDerivedVar[]; componentIcons?: ComponentIconSlotDoc[] } |
import type {SubComponentDoc} from '@astryxdesign/cli/doc'; const x: SubComponentDoc = {theming: …} |
Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.
Generated by PR Enrichment workflow | View full report
Both are open: meta#10191 moves NavigationStack's large title into the scroll area's content, and meta#10172 lets useFingerScroll tell its caller when a finger's scroll ends. They share no file.
Under a finger, NavigationStack's bars paint no background and no hairline, as iOS 26's do. Each screen carries a scroll edge effect: a veil of the body colour that fades in once its content has moved 10px under the bar, on UIKit's measured springs. A push, a pop and a back swipe carry each screen's veil with its card, and the bar a card covers is cut at that card's leading edge. A mouse keeps opaque bars. The veil and the UIKit bar constants live in Lab. The veil reads Core's finger-scroll hook, with its end call, and Core's internal touch entry also names the gesture host and the coarse-pointer check: export-only additions, with no change to Core's behavior.
571da6f to
cf0c8f1
Compare
There was a problem hiding this comment.
Changes requested for one text fix in Core's notes; the touch bar appearance itself is settled by the engineering owner's /approve-spec, and the spec-owner check passes on this head.
- Core's changeset and internal-entry comment still say
useFingerScrollis re-exported for Lab's ChatVirtualizer, which is not in the repository: restack onto #10172's current head, which made those mentions reader-neutral, or make the same edits in.changeset/finger-scroll-end.md,packages/core/src/internal/touch.ts, the gestures README, theinternal/touchtest name and the bundle test's comment.
The right-to-left chevron in the Scrolling story and the two restating comments are fixed, and CI has not run on this draft head yet.
[Automated review]
cf0c8f1 to
33464b1
Compare
|
/approve-spec 33464b1 |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, because it is a draft from a fork; it starts once the pull request is marked ready for review and a maintainer adds the ci:run label.
The earlier change request is resolved: Core's changeset and internal touch entry now describe the useFingerScroll re-export without naming a component that is not in the repository. An engineering owner recorded the touch bar look and the architecture:react-component-runtime INV13 sentence for this head, and mouse rendering matches the base. Merging this also lands the changes from #10172 and #10191 that this branch carries.
[Automated review]
|
/approve-spec 1ea8706 |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, because it is a draft from a fork; it starts once the pull request is marked ready for review and a maintainer adds the ci:run label.
The merge of main changes no line of this pull request, and the earlier approval's findings still hold: an engineering owner recorded the touch bar look for this head, the veil's finger-scroll read is covered by the current runtime record, and mouse rendering matches the base pixel for pixel. The NavigationStack browser specs pass on the merged head in Chromium.
[Automated review]
Under load, WebKit can report one moving layer's animated translate a frame late. The old mid-flight read then put the cut 2 to 3 px off the card's edge while the layers moved as one. The two cut specs now freeze the scene while the push runs, move every animation of the push to the same scene times, and read the rendered boxes there.
|
/approve-spec a450587 |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, because it is a draft from a fork; it starts once the pull request is marked ready for review and a maintainer adds the ci:run label.
Under a finger, NavigationStack's bars turn clear, each screen's veil switches on once its content is 10px under the bar, and a covered bar is cut at the top card's leading edge, while a mouse keeps the opaque bar. An engineering owner's /approve-spec for this head settles that look, and the new commit, which makes the two cut specs read the push at held scene times, passes with the rest of NavigationStack's browser specs. The size is fine for an owner-directed batch; the covered bar's cut could still land on its own if a smaller change helps.
[Automated review]
There was a problem hiding this comment.
Approval of this head waits on one engineering-owner decision; nothing else needs to change.
- NavigationStack's touch bar look (no bar background or hairline under a coarse pointer, a body-colour veil once a screen's content is 10px under the bar, the covered bar cut at the top card's leading edge) has no record on
main, and the/approve-specthat accepted it names the previous head. This head only mergesmainand leaves the patch unchanged, so an engineering owner records the decision for it by commenting/approve-spec <full head SHA>on this pull request (the author may do so as an engineering owner), or states the look in a NavigationStack record onmain.
The rest of the review carries over unchanged: no defects were found, the size is fine for an owner-directed batch, and CI on this head has no failure so far.
[Automated review]
|
/approve-spec 28db37b |
There was a problem hiding this comment.
Approved. An engineering owner's /approve-spec for this head settles NavigationStack's touch bar look (no bar background or hairline under a coarse pointer, a body-colour veil once a screen's content is 10px under the bar, the covered bar cut at the top card's leading edge), and no defects were found.
CI on this head has finished with no failures, and the size is fine for an owner-directed batch.
[Automated review]
On main, after #10172 and #10191.
Batch: L1 · Lab: iOS chrome (TabBar, SearchField, NavigationStack, PageControl)
User impact
Under a finger (
spec:AST-094FR13), NavigationStack's bars become iOS 26's: no background and no hairline. Once content is 10 px under the clear bar, a body-colour veil fades in behind the title. Each screen's veil rides its card, and a covered bar is cut at the top card's leading edge. A mouse keeps today's opaque bar.Visual authority
Lab work: Owner rule, Nov 4 2026 (Cindy): anything Lab-only gets an OK. AppShell and Core behavior are unchanged.
Before and after
Recorded at 8363522 (earlier base; veil and cut unchanged since). iOS 26.2 Safari, before (left) and after, a finger's drag:
A push, 4× slower: the incoming screen cuts the old title.
Android 15 emulator, mask off, then on:
State matrix
spec:AST-094FR4; Lab rule). A programmatic scroll switches it at once.Design
packages/lab/src/internal/useScrollEdgeEffect.ts).spec:AST-094FR24).architecture:component-style-authoringINV7): the veil and its sentinel per screen, the clip box and row per bar.architecture:react-component-runtimeINV13). The CSS is the mask gradient.Scope
Testing
Local gates at
a450587ca1, on mainf5cf9a5914. The root build, unit, Storybook, lint and accessibility runs are at1ea87065a6, which differs only in one interaction spec.pnpm exec vitest run packages/lab/src/NavigationStack packages/lab/src/internal packages/core/src/internal packages/core/src/__tests__/internalModulesNotExperimental.test.ts packages/core/src/gestures/__tests__/useFingerScroll.test.tsx: 12 files, 115 tests pass. A resource case mounts three screens: the effect adds no scroll listener of its own (spec:AST-094FR24). The layer drops only on the finger-scroll hook's end call, with no timer. A stack that starts hidden waits for a size before it measures.pnpm test:interactionoverpackages/lab/src/NavigationStack/__tests__/, against a freshpnpm storybook:build: Chromium 61 pass. WebKit, in the CI WebKit job's container profile (--cpus=4,CI=1): 54 pass and 7 Chromium-only skips.5764609c8abefore the main merge: the base with this change's story and specs fails 19 and passes 42 in Chromium. The failures are the veil spec's 9 cases, the 6 cut cases, the clear-bar case and a push during a push. Two more cases find an element by an attribute this change adds.33464b1f3e, each story at rest, base → head: Settings 42 → 46, End content 46 → 50, Pushed 97 → 107. Large title 116 → 120 and Scrolling 334 → 338. A mouse and a finger give the same counts.pnpm build, then rootpnpm typecheck: 0 errors.pnpm exec vitest run --project node scripts/core-internal-entries.test.mjs scripts/touch-support-bundle.test.mjs: 13 pass.CI=true pnpm lint: 0 errors. Prettier,check:changesets, changeset coverage andcheck:knowledgepass.pnpm review:pre, with this description: 0 findings.33464b1f3e: a finger drags the Scrolling story 240 px, in 24 moves of 16 ms. The veil fades in over 183 ms and 12 frames, in 3 of 3 runs..github/scripts/accessibility-audit.js --components NavigationStack, baseline gate): 0 violations.