Repository navigation
feat(lab): TabBar, iOS 26's floating tab bar, with its selection lens and press tracking - #9700
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| lab/TabBar | 73.0 kB (new) · 1.8 kB CSS (new) | — | — · ✓ · ✗ |
| lab/TabBarItem | 3.8 kB (new) · 1.8 kB CSS (new) | — | — · ✓ · ✗ |
| lab/CommentThread | 317.3 kB (+92 B) · 2.4 kB CSS (0) | — | — · — · — |
7 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| lab/Comments | 322.2 kB (+85 B) · 2.5 kB CSS (0) | — | — · — · — |
| lab/Schedule | 103.0 kB (+66 B) · 3.9 kB CSS (0) | — | — · — · — |
| ButtonGroup | 40.6 kB (+62 B) · 3.7 kB CSS (0) | — | — · — · — |
| AlertDialog | 103.1 kB (+58 B) · 14.2 kB CSS (0) | — | — · — · — |
| lab/NavigationStack | 79.0 kB (+18 B) · 1.5 kB CSS (0) | — | — · — · — |
| lab/NavigationScreen | 79.0 kB (+17 B) · 1.5 kB CSS (0) | — | — · — · — |
| lab/PageControl | 72.3 kB (+13 B) · 709 B CSS (0) | — | — · — · — |
Package size:
- @astryxdesign/cli 3.49 MB packed (+15 B); no new dependencies
- @astryxdesign/core 6.89 MB packed (+819 B); no new dependencies
API changes: none in this pull request
Committed API report behind this build: cli 19 rows, core 25 rows; run pnpm api:report.
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
There was a problem hiding this comment.
Approval waits on an engineering owner's spec decision, and three independent changes are needed either way.
This pull request also changes Core, the interaction harness, Storybook and CLI data, so current records bind TabBar as a family:action-groups member. An engineering owner needs to decide each of these departures, or update the records first:
- Under a finger, a lift off the bar chooses the nearest tab, while
family:action-groupsFR3 says a lift outside the group acts on nothing. - The lens plays measured UIKit springs (and SearchField's bottom field plays measured frames) instead of motion roles, which
spec:AST-094FR4 forbids. - Under touch the moving lens mounts a copy of the items in a bar that is not a layer, beyond what
spec:AST-017FR60 allows. - A mouse or pen drag carries the lens, a new fine-pointer feel that
spec:AST-017FR59 says needs its own owner decision.
Required changes:
- Retarget the pull request onto its parent so it carries only the TabBar commit; against
mainit also lands #9399, #9699 and #9698, 12,769 authored lines in all. - Drive the press tracking through Core's
actionGroupTracker, re-exported from@astryxdesign/core/internal/touchas this change does for 14 other modules, instead of the recognizer and target resolution inuseTabBarLens.ts. - Move
@astryx.tabBar.more,@astryx.tabBar.cancelSearchand the matching changeset sentence to #9701, since nothing in this change reads them.
An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner.
[Automated review]
f277ee0 to
ba1aaaf
Compare
|
Round 1 is addressed in ba1aaaf, restacked on #9399's new head:
|
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on two spec departures, and two independent changes are needed either way.
The press now runs through Core's action-group tracker, and a slide off the bar cancels under every pointer, so those earlier points are resolved. This pull request also changes Core, the interaction harness, Storybook and CLI data, so current records bind TabBar. An engineering owner needs to approve these departures, or update the records first:
- The lens and the bar's flex play measured UIKit springs (and SearchField's bottom field plays measured frames) instead of motion roles, which
spec:AST-094FR4 forbids. - Under touch the moving lens mounts a copy of the items in a bar that is not a layer, beyond what
spec:AST-017FR60 allows.
Required changes:
- Land #9399, #9699 and #9698 first so this pull request carries only TabBar's commit, since against
mainit is 13,527 authored lines, including Core re-exports and two catalog strings nothing here reads. - Delete the Effect at
useTabBarLens.ts:684, because the tracker it disposes holds no timer, attribute or listener, so it synchronizes nothing (architecture:react-component-runtimeINV3).
An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
[Automated review]
ba1aaaf to
a7d0520
Compare
|
Round 2 is addressed in a7d0520. The no-op dispose Effect is gone, and the stale mouse-drag comments and the test's record citation are fixed. The two spec departures (FR4's measured springs, FR60's lens copy) are open cards with the engineering owner. The stack point stands until #9399, #9699 and #9698 land; until then, the body's compare link shows this PR's own commit. |
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on two spec departures, and one independent change is needed either way.
This pull request also changes Core, the interaction harness, Storybook and CLI data, so current records bind TabBar and SearchField; an engineering owner needs to approve these departures or update the records first:
- The lens and the bar's flex play measured UIKit springs, and SearchField plays a measured 383.3 ms activation clock and frame tables, instead of motion roles, which
spec:AST-094FR4 forbids. - Under touch the moving lens mounts a copy of the items in a bar that is not a layer, 23 extra elements in the Default story where the description counts 18, beyond what
spec:AST-017FR60 allows without an owner exception.
Required change:
- Land #9399, #9699 and #9698 first so this pull request carries only TabBar's commit, since against
mainit is 13,525 authored lines, above the 8,000-line owner-directed batch ceiling, including Core re-exports and two catalog strings nothing here reads.
An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
[Automated review]
a7d0520 to
6f87bcd
Compare
|
Round 3 is addressed in 6f87bcd, restacked on #9399's head 7b86fe2 and #9698's approved head:
|
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on two spec departures, and one independent change is needed either way.
This pull request also changes Core, the interaction harness, Storybook and CLI data, so current records bind TabBar and SearchField at this head; an engineering owner needs to approve these departures or update the records first:
- The lens and the bar's flex play measured UIKit springs, and SearchField plays a measured 383.3 ms activation clock and frame tables, instead of motion roles, which
spec:AST-094FR4 forbids. - Under touch the moving lens mounts a copy of the items in a bar that is not a layer, 23 extra elements in the Default story (52 in the tablist against 29 at rest), beyond what
spec:AST-017FR60 allows without an owner exception.
Required change:
- Land #9399, #9699 and #9698 first so this pull request carries only TabBar's commit, since against
mainit is 13,760 authored lines, above the 8,000-line owner-directed batch ceiling, including Core re-exports and two catalog strings nothing here reads.
The mouse click is now pinned to a single report, and the restating comments are gone. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
[Automated review]
6f87bcd to
81ca5e6
Compare
|
Restacked on #9699 over #9754, since #9399 won't merge, so SearchField is gone from this diff. The lens now uses Core's renamed internal motion engine ( |
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on two spec departures, and two independent changes are needed either way.
This pull request also changes Core, a Storybook story and CLI data, so current records bind TabBar at this head. An engineering owner needs to approve these departures or update the records first:
- The lens and the bar's flex play measured UIKit springs instead of motion roles, which
spec:AST-094FR4 forbids. - Under touch the moving lens mounts a copy of the items in a bar that is not a layer, beyond what
spec:AST-017FR60 allows without an owner exception with before and after counts.
Required changes:
- A second finger landing during a press must end it with nothing chosen (
family:action-groupsFR3), which Core'suseActionGroupTrackingalready does, so route TabBar's press through that hook, extended for the lens, instead of its own long-press recognizer. - Move
@astryx.tabBar.more,@astryx.tabBar.cancelSearchand the internal re-exports nothing here reads to #9701, which also lets the owner-directed batch cover this pull request's size.
The long comment above TabBar is still to be trimmed to its reasons (non-blocking). An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
[Automated review]
81ca5e6 to
b416f9f
Compare
|
/approve-spec b416f9f |
|
/approve-spec ec8e3a4 |
There was a problem hiding this comment.
Approved pending CI: the required test check failed only because the Chromium interaction leg 2/2 reached CI's 15-minute job limit with no test failing, just as TabBar's spec began, so a re-run of that job is the next step. That leg already took about 14.5 minutes at an earlier head, and TabBar's spec adds about a minute and a half to it, so it may need splitting.
#9754: approved. It has landed.
#9699: approved. An engineering owner approved this head. Merging main left its own lines unchanged, and AlertDialog and ButtonGroup keep their defaults.
#9700: approved. An engineering owner's /approve-spec for this head covers TabBar's measured springs, its touch lens copy and its missing component record, and no blocking finding remains. Three non-blocking test notes stay open: a stale test title (TabBar.interaction.browser.spec.ts:1043), a stale file header (tabBarModel.test.ts:8), and a copied pointer helper in TabBar.test.tsx that Lab's own test helpers now provide.
Stack: the stack passes as a whole. A pull request lands only when it and every pull request below it are approved.
[Automated review]
|
/approve-spec f9b11ba |
There was a problem hiding this comment.
Approved: an engineering owner approved this head, CI passes, and no blocking finding remains.
#9754: approved. It has landed.
#9699: approved. An engineering owner approved its new head; merging main left its own lines unchanged, and AlertDialog and ButtonGroup keep their defaults.
#9700: approved. An engineering owner's /approve-spec for this head covers TabBar's measured springs, its touch lens copy and its missing component record. Three non-blocking test notes stay open:
TabBar.interaction.browser.spec.ts:1043: the title's tie parenthetical points at a casetabBarModel.test.tsno longer has.tabBarModel.test.ts:8: the header still lists "the item a lift commits", which the file no longer tests.TabBar.test.tsx:69-92: the copiedcoarsePrimaryPointercan import Lab's own test helper (packages/lab/src/internal/__tests__/coarsePointer.ts) instead.
Stack: the stack passes as a whole. A pull request lands only when it and every pull request below it are approved.
[Automated review]
|
/approve-spec 92b3acb |
There was a problem hiding this comment.
Waiting on an engineering owner: TabBar's departures from current records need an owner decision at this head, and no blocking defect remains in the stack.
#9754: approved. It has landed.
#9699: approved. An engineering owner approved its new head; merging main left its own lines unchanged apart from one resolved import-path conflict in Core's internal touch entry.
#9700: waiting on an owner decision. Its own patch is unchanged, but the last owner approval names the previous head, so these departures need an engineering owner's decision again, or TabBar must conform, or the records must change first:
- Under touch, the lens and the bar's flex play UIKit's measured springs from
uikitTabBar.ts, key- and app-driven slides included, instead of motion roles (spec:AST-094FR4). - Under touch, the moving lens mounts a copy of the items in a bar that is not a layer (
spec:AST-017FR60). - TabBar has no component record, so nothing current settles its behavior, including the badged tab's spoken name.
An engineering owner records the decision by commenting /approve-spec 92b3acbefc3747e8c3be5ce5733a1677aae0c82f on this pull request, and the author, as an engineering owner, may do so. Four non-blocking notes (a 47-line JSDoc on useTabBarLens, two stale test details and a duplicated test helper) are in the review.
Stack: no blocking finding across the stack; CI's one failure (test-node (3/4), the WebKit routing test) comes from main, not this stack. A pull request lands only when it and every pull request below it are approved.
[Automated review]
| /** | ||
| * WHAT MOVES (uikitTabBar.ts, all measured on iOS 26.2) | ||
| * | ||
| * The lens is a CSS resting place over the selected item (TabBar.tsx) with a | ||
| * body inside it that this hook moves. `x` is its offset from the resting | ||
| * place, `scaleX` / `scaleY` its lift, and its `::before` the shadow that fades | ||
| * in as it lifts. Inside the body, a clip holds the selected copy of the items | ||
| * while the lens moves. The clip's background is the opaque fill, and its | ||
| * `::before` is the glass that fades in as the lens lifts. The copy is | ||
| * counter-transformed so the items stay put while the lens moves over them. Its | ||
| * curves derive from the body's (tabBarModel's `copyTransform`). Each copied | ||
| * item grows by the lift's magnification about its own centre. The copy mounts | ||
| * when a finger lands or a slide starts and goes once everything rests. At rest | ||
| * the selected tab draws its own look (TabBar.tsx). | ||
| * | ||
| * Under a finger or a pen in the touch presentation, iOS and Android alike. | ||
| * The press is a long press with no delay, so it begins on the touch, as | ||
| * UIKit's selection tracker does: | ||
| * - landing: the lens travels toward the finger (`lensTravel`) and lifts | ||
| * (`lift`: outset, glass, magnification), and the capsule flexes | ||
| * (`flexPress`); | ||
| * - dragging (past the long press's allowable movement): the lens chases the | ||
| * finger (`lensTrack`), rubber-banded past the first and last items; | ||
| * - lifting: the item nearest the finger is chosen, the lens settles onto it | ||
| * (`lensSettle`), the lift and the flex relax (`lift`, `flexRelease`). A | ||
| * cancelled touch settles back onto the selected item. | ||
| * - off the bar: the lens shows the selected item again, and a lift there | ||
| * chooses nothing. Sliding off cancels. | ||
| * The choice reaches the app (`onLift`) from the lift itself, right after the | ||
| * settle has started in the same task. The settle's first frame waits for the | ||
| * browser's commit, so the app rendering the new tab delays its start rather | ||
| * than freezing it midway. The report renders synchronously, and if the app | ||
| * keeps the old selection the lens settles back onto it. The finger is read in | ||
| * the capsule's own coordinates, before its flex scale, as UIKit reads it. | ||
| * Under the flex the lens drifts slightly away from the finger toward the ends, | ||
| * as on iOS. | ||
| * | ||
| * A selection change slides the body from where the lens was on screen to its | ||
| * new resting place. On `lensTravel` from rest (a key, the app), or continuing | ||
| * the settle a lift started. Every animation runs on the compositor; a finger's | ||
| * moves retarget them, so the lens keeps moving through a busy main thread. | ||
| * Under Reduce Motion the animator cross-fades instead of moving and a press | ||
| * scales nothing. Each spring plays as measured, from the UIKit table. | ||
| * | ||
| * Under a fine pointer nothing here runs. A press there is a plain button | ||
| * press: its click chooses the tab, and TabBar's CSS slides the lens. | ||
| */ |
There was a problem hiding this comment.
Miss, non-blocking (INV29): this JSDoc runs 47 lines and repeats the lens description in TabBar.tsx and uikitTabBar.ts's header. Keep the contract (what each ref drives, and that nothing runs under a fine pointer) and leave the spring list to uikitTabBar.ts.
There was a problem hiding this comment.
Approved: no blocking finding remains in the stack.
#9754: approved. It has landed.
#9699: approved. An engineering owner approved its current head.
#9700: approved. An engineering owner's approval for this head covers TabBar's measured springs, its touch lens copy and its missing component record; four non-blocking notes (a 47-line JSDoc on useTabBarLens, two stale test details and a duplicated test helper) are in the review.
Stack: passes. CI's one failure, the WebKit routing test in test-node (3/4), comes from main, not this stack, and a pull request lands only when it and every pull request below it are approved.
[Automated review]
|
/approve-spec c7adb12 |
There was a problem hiding this comment.
Changes requested: one blocking finding in #9700; the pull requests below it are approved.
#9754: approved. It has landed.
#9699: approved. An engineering owner approved its current head, and it has no blocking finding.
#9700: changes requested. An engineering owner's approval for this head covers TabBar's measured springs, its touch lens copy and its missing component record.
- Regression: merging
mainbrought in the interaction harness's requiredjudgesoption, andTabBar.interaction.browser.spec.tsstill callsholdAnimations(page)at lines 402, 812 and 937 andanimationStoryboardwithoutjudgesat line 2070, so CI'stypecheck,buildandtestchecks fail; name the motion each hold judges, for example{judges: {target: lensBody(page), property: 'translate'}}.
Four non-blocking notes (a 47-line JSDoc on useTabBarLens, two stale test details and a duplicated test helper) are in the review.
Stack: fails at this head on the finding above; a pull request lands only when it and every pull request below it are approved.
[Automated review]
Main's interaction harness now requires every hold sent from a test to name the motion it judges. Each TabBar case names what it samples after the hold: - the keyed slide: the lens body's and the lens copy's translate; - the finger landing: the capsule's scale, the lens body's scale and translate, and the lens clip's ::before opacity; - the lift: the capsule's and the lens body's scale; - the selection storyboard: the lens body's translate.
|
/approve-spec a309756 |
There was a problem hiding this comment.
Approved: every pull request in this stack is approved at its current head.
#9754: approved. It has landed.
#9699: approved. An engineering owner approved its current head, and it has no blocking finding.
#9700: approved. An engineering owner's approval for this head covers TabBar's measured springs, its touch lens copy and its missing component record, and CI passes at this head.
Four non-blocking notes stand in the review: a 47-line JSDoc on useTabBarLens, two stale test details and a duplicated test helper.
Stack: passes at this head; a pull request lands only when it and every pull request below it are approved.
[Automated review]
|
/approve-spec 04ca7ba |
There was a problem hiding this comment.
Approved: every pull request in this stack is approved at its current head, and CI passes.
#9699: approved. An engineering owner approved its current head, and it has no blocking finding.
#9700: approved. An engineering owner's approval for this head covers TabBar's measured springs, its touch lens copy and its missing component record, and TabBar's interaction cases pass in Chromium and WebKit at this head.
Four non-blocking notes stand in the review: a 47-line JSDoc on useTabBarLens, two stale test details and a duplicated test helper.
Stack: passes at this head; a pull request lands only when it and every pull request below it are approved.
[Automated review]
Stacked on #9699 (on #9754). Review only this PR's change: vjeux/astryx@a236965...270f341
User need
Phone apps on Astryx need iOS 26's bottom tab bar: top-level sections in a floating capsule, with a lens that answers a finger as UIKit does.
Evidence
From earlier drafts (iOS 26.2 simulator, Android emulator).
Problem and solution fit
Under a finger or a pen in the touch presentation the lens lifts into glass, follows a drag and settles on the tab nearest the lift on UIKit's springs while the bar flexes. Sliding off the bar or a second finger cancels. With no tab selected, a held finger paints its tab. Reselecting calls
onReselect.Desktop, before → after: a mouse or pen drag carried the lens and chose the tab under its release, and a held press painted its tab, fading on
--duration-fast. Now a fine pointer's press is a plain button press: a click chooses its tab; a drag chooses and paints nothing.Current specification
official-packages.jsonis regenerated, soastryx discoverlists TabBar (spec:AST-051FR11).Decisions
spec:AST-094FR4: under touch, the lens plays UIKit's measured springs fromuikitTabBar.ts. Specs decision, Nov 5 2026: gesture class per AST-094 FR45.spec:AST-017FR60: the lens's copy of the items. Open until promotion: the moving lens adds 23 elements in the 4-tab Default story while it moves (none at rest); promotion must meet AST-017 FR60 or record an approved exception. Specs decision, Nov 3 2026.Current records already decide: sliding off cancels (
family:action-groupsFR3), and the slide runs only in the touch presentation (DEC-1).Core's
useActionGroupTracking(#9699) owns the press; TabBar draws the lens.Public delta and caller burden
New Lab exports
TabBarandTabBarItemwith their props types;panelIdsets a tab'saria-controls:Design and alternatives
createAnimator.--duration-exitand--ease-exit, so the compositor runs the fade (spec:AST-094FR5, card 47). Under Reduce Motion the fade stays within 200 ms (design:reduced-motionDR2).Scope
Testing
Local gates at 270f341 (on #9699 a236965), node 24.22.1 from
.nvmrc:background-colortransition on the item box, not anopacityfade on its layer.touchmovein the touch presentation). On a build without the exit-token change, with a 300 ms theme exit under Reduce Motion a finger's release read 125 ms (must be 200), and with a 90 ms exit the paint's duration and curve differed from the tokens.panelIdrendered noaria-controls(unit).pnpm lint, 0 errors; Prettier; the CLI node suite, 335;review:pre.Still owed before ready: the WebKit CI profile ×10 and the busy-main-thread proof ×10 at this head, and the feel check on a phone.
Batch: L1 · Lab: iOS chrome (TabBar, SearchField, NavigationStack, PageControl)
Size: 5,027 added lines, about half tests. Minimize through More follow in a second batch.
Supersedes #8129, #8141 and #8703.
Stack: #9699 → #9700
Merge group: #9699, #9700
#9701, TabBar's second Lab batch, also builds on this PR.