Skip to content

feat(lab): under a finger, NavigationStack's bar takes the iOS 26 look - #8396

Merged
cixzhang merged 9 commits into
meta:mainfrom
vjeux:uikit/c16-stack-bar
Nov 6, 2026
Merged

cixzhang merged 9 commits into
meta:mainfrom
vjeux:uikit/c16-stack-bar

Conversation

@vjeux

@vjeux vjeux commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

On main, after #10172 and #10191.

Batch: L1 · Lab: iOS chrome (TabBar, SearchField, NavigationStack, PageControl)

User impact

Under a finger (spec:AST-094 FR13), 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:

iOS: the veil fades in

A push, 4× slower: the incoming screen cuts the old title.

iOS: a push cuts the old title

Android 15 emulator, mask off, then on:

Android: mask off

Android: mask on

State matrix

  • A finger's scroll fades the veil on UIKit's measured springs, Lab-only constants as NavigationStack's own (spec:AST-094 FR4; Lab rule). A programmatic scroll switches it at once.
  • The cut follows the top card's leading edge, on its spring or the finger, mirrored in RTL.
  • Reduce Motion: titles swap at once. Reduce Transparency: the veil is an opaque band.
  • A screen a pop reveals keeps its own veil; the bar under it stays cut away. A large title hands over to the veil with the bar's title.
  • A stack that starts hidden measures once sized.
  • A mouse: opaque bars, no cut. The cut follows the pointer turning fine or coarse while a screen is covered.

Design

  • Behavior module: Lab's internal scroll edge effect (packages/lab/src/internal/useScrollEdgeEffect.ts).
  • The veil adds no scroll listener or timer; Core's finger-scroll hook (feat(core): useFingerScroll tells its caller when a finger's scroll ends #10172) reports the scroll's end (spec:AST-094 FR24).
  • Elements added (architecture:component-style-authoring INV7): the veil and its sentinel per screen, the clip box and row per bar.
  • Size (INV6, gzip, base → head): JavaScript 76,584 → 79,702 B, CSS 1,087 → 1,521 B. The JavaScript is the veil and the bar cut. They run only under a coarse pointer, on Core's shared touch modules (architecture:react-component-runtime INV13). The CSS is the mask gradient.

Scope

  • Appearance is the primary intent.
  • Behavior, API, interaction model, and unrelated layout changes were removed or split. Core gets two export-only names on its internal touch entry, with no behavior change.
  • The evidence shows the changed pixels actively running, not only source or computed styles.
  • Public text and artifacts contain no internal Meta context.

Testing

Local gates at a450587ca1, on main f5cf9a5914. The root build, unit, Storybook, lint and accessibility runs are at 1ea87065a6, 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-094 FR24). 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:interaction over packages/lab/src/NavigationStack/__tests__/, against a fresh pnpm 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.
  • The two cut specs freeze the scene while the push runs and read the rendered boxes at eight shared scene times. A mid-flight read can catch one WebKit layer a frame late. In the WebKit container, eight cut specs ×10 at one CPU with four workers: 70 pass, 10 Chromium-only skips. The previous spec ×5 at four CPUs: 230 pass, 30 skips.
  • Red-first for the cut specs: a planted 24 ms late start of the cut fails both, in both engines (gaps of 19 to 50 px). The previous reader, given one layer a frame late, read 2.4 px where the boxes read 0.01 px.
  • Red-first, on 5764609c8a before 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.
  • Without the line that parks a bar a pop reveals, the root's bar shows 0.3 of the stack (expected 0). Without the re-settle on a pointer change, a pointer turning coarse leaves 0.7 showing.
  • Element counts in Chromium at 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 root pnpm 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 and check:knowledge pass. pnpm review:pre, with this description: 0 findings.
  • Measured in Chromium (iPhone 13 profile) at 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.
  • Accessibility audit (.github/scripts/accessibility-audit.js --components NavigationStack, baseline gate): 0 violations.
  • Earlier evidence at 8363522, on the iOS 26.2 simulator (iPhone 17 Pro, Safari): a finger's fade runs 61 animated frames. A programmatic return shows no animation and no blank title frame, in 3 of 3 runs.
  • On the Android 15 emulator (Chromium 158), a drag fades the veil in 63 animated frames. Under the veil the title keeps 9.5:1 / 7.6:1 contrast at worst, the back chevron 5.4:1 / 5.9:1.

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 6, 2026 8:45am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 6, 2026, 9:15 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 main once 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-modality INV10 requires, or amend INV10 on main first;
  • reclassify the row-press change that hides consumers' xstyle ::before decoration under a finger, which ships as [fix];
  • record the architecture:component-style-authoring INV7 element counts for the elements the bar change adds, and drop the nonexistent @astryxdesign/core/motion/experimental path from two changesets.

[Automated review]

Comment on lines +38 to +42
import {
PanGestureRecognizer,
ScreenEdgePanGestureRecognizer,
useGestureRecognizer,
} from '@astryxdesign/core/gestures';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +435 to +441
opacity: {
default: null,
[COARSE]: {
default: 0,
[ENABLED]: {default: null, [PRESSED_ON]: 1, [PRESSED_HELD]: 1},
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the row-press change from #8114, lower in the stack, and its owner has it.

Comment on lines +677 to +683
<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 />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #8107 (c6dc13d) and #8185 (01ba8a0), both merged into this head at 9f51e05.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 25, 2026
@vjeux
vjeux force-pushed the uikit/c16-stack-bar branch from dfc5838 to 9f51e05 Compare October 25, 2026 11:11
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. At 9f51e05:

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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: opacity composites 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 main once 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-modality INV10, ships the row-press ::before change as [fix] although consumers must move their decoration, and changes AppShell's header and height contract without amending component:AppShell.

[Automated review]

Comment on lines +116 to +119
willChange: {
default: null,
'@media (pointer: coarse)': 'opacity',
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@vjeux
vjeux force-pushed the uikit/c16-stack-bar branch from 9f51e05 to 74e0a84 Compare October 25, 2026 12:22
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

An update on the three points that come from the pull requests below this one:

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 main once 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 height contract without amending component:AppShell, hides a row's xstyle ::before under a finger in a [fix] (your reported fix is not in this head yet), and prunes search state in a layout effect against architecture:react-component-runtime INV1.
  • Add the architecture:component-style-authoring INV6 gzip size report for NavigationStack and AppShell.

[Automated review]

</div>
) : null}
<div ref={content.ref}>
<div {...edge.sentinelProps} data-navigation-scroll-edge-sentinel />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +412 to +415
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +2228 to +2252
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@cixzhang

Copy link
Copy Markdown
Contributor

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.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/gestures and @astryxdesign/core/utils/touchPresentation, which core does not export and spec:AST-017 FR21 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-authoring INV6 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 scrollend listener against spec:AST-094 FR24, and the veil's springs need to be FR4 motion roles.
  • Rebase onto main once 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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread packages/core/src/AppShell/AppShell.tsx Outdated
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {useMediaQuery} from '../hooks/useMediaQuery';
import {useTouchPresentation} from '../utils/touchPresentation';
import {useScrollEdgeEffect} from '../motion/useScrollEdgeEffect';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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).

Comment thread packages/core/src/motion/uikit/bars.ts Outdated
Comment on lines +127 to +137
* 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/gestures and @astryxdesign/core/utils/touchPresentation, which core does not export, and architecture:react-component-runtime INV13 on main now 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-authoring INV6 size report (CI measures AppShell's import 24,731 bytes gzip larger) and flag the touch-only code, which INV6 on main now routes through the shared internal touch modules rather than lazy loading.
  • Each screen's scroll edge effect adds its own document-level scrollend listener against spec:AST-094 FR24, and the veil's springs need to be FR4 motion roles.
  • Rebase onto main once 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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread packages/core/src/AppShell/AppShell.tsx Outdated
import {focusOutlineProps} from '../utils/focusOutline.stylex';
import {useMediaQuery} from '../hooks/useMediaQuery';
import {useTouchPresentation} from '../utils/touchPresentation';
import {useScrollEdgeEffect} from '../motion/useScrollEdgeEffect';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 28db37b

Storybook · Sandbox · built from this PR's head

Modified components

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

vjeux added 2 commits November 5, 2026 18:51
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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 useFingerScroll is 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, the internal/touch test 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]

@cixzhang
cixzhang force-pushed the uikit/c16-stack-bar branch from cf0c8f1 to 33464b1 Compare November 6, 2026 03:58
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 33464b1

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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]

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 1ea8706

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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]

@vjeux
vjeux marked this pull request as ready for review November 6, 2026 06:12
@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 6, 2026
@cixzhang
cixzhang marked this pull request as draft November 6, 2026 06:23
@cixzhang
cixzhang marked this pull request as ready for review November 6, 2026 06:23
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.
@vjeux
vjeux marked this pull request as draft November 6, 2026 07:50
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec a450587

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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]

@vjeux
vjeux marked this pull request as ready for review November 6, 2026 08:11

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-spec that accepted it names the previous head. This head only merges main and 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 on main.

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]

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 28db37b

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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]

@cixzhang
cixzhang merged commit 0081c11 into meta:main Nov 6, 2026
69 checks passed
@vjeux
vjeux deleted the uikit/c16-stack-bar branch November 6, 2026 09:23

This branch was successfully deployed

1 active deployment
Preview — 28db37b2 Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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