Skip to content

feat(TopNav): under a finger, the phone bar's end items sit on an iOS 26 capsule - #8188

Draft
vjeux wants to merge 1380 commits into
meta:mainfrom
vjeux:uikit/c16-bar-capsules
Draft

vjeux wants to merge 1380 commits into
meta:mainfrom
vjeux:uikit/c16-bar-capsules

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8142 (the scroll edge effect). Review only this PR's change: vjeux/astryx@4fe659b...8ba07f7

Carries #8562 (Core's internal entries) as a dependency; if the entries land differently, this follows her decision.

User impact

Under a finger (one touch model on every touch device; spec:AST-094 FR13), TopNav's end icons float bare over content scrolling under the bar; iOS 26 puts bar buttons on capsules. A mouse keeps today's bar.

Visual authority

Owner decisions (proposed; pending owner review):

  1. Under a finger (the literal @media (pointer: coarse) key) the phone bar is 54px tall with the default tokens (iOS 26.2) instead of 48, its items --spacing-4 from the sides.
  2. The capsule paints --color-background-surface, opaque, with no blur or shadow. iOS's content-following colour is not modelled: over a surface-coloured page it shows only over content.
  3. All end items share one capsule. iOS gives a text item its own; splitting them is a follow-up.

Before and after

With the default tokens, the bar is 54px with its items on the top 44px. The end items share one fully rounded capsule: a --size-element-lg cell plus --spacing-1 above and below (44px), each control where UIKit centres its cells (--spacing-1 in, --spacing-4 apart).

iOS 26.2 Safari, #8142 (left) and this branch (right), scrolled (top) and at rest (bottom):

iOS 26.2 Safari: #8142 (left) and this branch (right), scrolled (top) and at rest (bottom)

Dark mode (P4) and right to left (P5), #8142 (top) and this branch (bottom):

P4: dark mode, the capsule over the scrolled content

P5: right to left, the capsule at the inline end

Android 15, Chromium 158 (emulator), main (left) and this branch (right):

Android: main's opaque bar; this branch's capsule over the content

State matrix

  • Light and dark: the surface token.
  • A mouse: no surface or size, the old 4px gap.
  • An empty end group draws no capsule.
  • RTL: logical padding puts the capsule at the inline end.
  • Reduce Transparency: nothing changes; the capsule is opaque.

Size (INV6, analyze-pr, gzip, base → head): TopNav's import 53,375 → 53,507 B of JavaScript; its CSS 10,610 → 10,779 B.

Scope

  • Appearance is the primary intent.
  • Behavior, API, interaction model, and unrelated layout changes were removed or split.
  • The evidence shows the changed pixels actively running, not only source or computed styles.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run packages/core/src/{TopNav,motion,AppShell,theme,hooks,internal}: 95 files, 3006 tests pass. TopNavBarCapsule.test.tsx reads the raw StyleX rules: the bar and capsule declarations (token expressions) under the coarse pointer alone, none gated on WebKit for iOS; the mouse path, first asserting a fine pointer; the empty end group; and the default tokens give the geometry measured in bars.ts.
  • pnpm test:interaction TopNavBarCapsule AppShellScrollEdge against a fresh pnpm storybook:build: 19 pass in Chromium; 15 pass and 4 Chromium-only skips in WebKit (official Playwright image). Bar height, capsule geometry and radius; its colour equals the theme's surface (a probe beside it); no shadow; button centres 22/74; names and order; a real touch on the toggle opens the drawer; right to left (the capsule 16px from the left); Reduce Transparency changes nothing (Chromium); a mouse keeps today's bar; a still.
  • iOS 26.2 simulator (iPhone 17 Pro, Safari), at b4c395b (this PR's commit before the latest base merge): the recordings above. Read page-side: a 54px bar, a 96 × 44 capsule 16px from the edge, radius 22px, rgb(255, 255, 255) (the light surface), no shadow, buttons centred at 22/74.
  • Android 15 emulator (Chromium 158), at b4c395b: the still above.
  • scripts/touch-support-bundle.test.mjs › "is absent from the root entry" is red: AppShell's touch bar imports the gesture host statically, as INV13 prescribes, so the test's whole-root claim can't hold; F1's per-component bundle test, test(core): a touch component's import carries its gesture code, and nothing else does #8671, lists AppShell as a gesture user.
  • pnpm a11y:audit -- --components TopNav,AppShell: no new violations. pnpm exec vitest run --project node packages/cli/test: 309 pass (the search icon golden counts the new dense-docs line: 220 → 221 matches). Root pnpm typecheck after pnpm build (CI's order), check:changesets, check:knowledge, the CI duration guard, internalModulesNotExperimental, scripts/lab-core-imports.test.mjs, eslint, prettier and the pre-commit checks pass.

@vercel

vercel Bot commented Oct 24, 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 Oct 28, 2026 3:36pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 28, 2026, 4:09 PM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Preview availability: Storybook and Sandbox are not both ready on the exact-head Vercel preview. CI concluded failure.

Modified Components

AppShell (@astryxdesign/core)
Metric Before After Delta
Entry File (ESM) 785B 785B 0B
Lines of Code 746 859 -
Complexity Very High Very High (155) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.3KB 57.6KB +290B 12.5KB 12.5KB 0B
core/AppShell 66.8KB 89.5KB +22.7KB ⚠️ 11.8KB 12.1KB +334B
core/AvatarGroupOverflow 40.3KB 40.6KB +302B 6.4KB 6.4KB 0B
core/Banner 48.5KB 48.8KB +289B 10.1KB 10.1KB 0B
core/BaseTypeahead 55.0KB 55.2KB +273B 10.1KB 10.1KB 0B
core/BreadcrumbItem 67.3KB 67.6KB +266B 11.2KB 11.2KB 0B
core/Button 43.7KB 43.9KB +274B 9.3KB 9.3KB 0B
core/Calendar 55.6KB 55.9KB +260B 10.2KB 10.2KB 0B
core/Carousel 50.9KB 51.1KB +265B 10.3KB 10.3KB 0B
core/ChatComposer 61.3KB 61.6KB +257B 11.0KB 11.0KB 0B
core/ChatComposerInput 56.2KB 56.4KB +256B 10.2KB 10.2KB 0B
core/ChatComposerTokenElement 56.2KB 56.4KB +260B 10.2KB 10.2KB 0B
core/ChatDictationButton 47.2KB 47.4KB +275B 9.6KB 9.6KB 0B
core/ChatLayout 49.5KB 49.8KB +303B 10.1KB 10.1KB 0B
core/ChatLayoutScrollButton 47.0KB 47.3KB +278B 9.7KB 9.7KB 0B
core/ChatSendButton 46.4KB 46.7KB +277B 9.3KB 9.3KB 0B
core/CheckboxInput 50.5KB 50.8KB +264B 9.5KB 9.5KB 0B
core/CheckboxListItem 54.7KB 54.9KB +275B 10.6KB 10.6KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 61.2KB 61.4KB +272B 10.8KB 10.8KB 0B
core/Collapsible 18.7KB 19.0KB +262B 4.4KB 4.4KB 0B
core/CommandPalette 51.5KB 51.8KB +269B 12.0KB 12.0KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 56.5KB 56.8KB +286B 10.8KB 10.8KB 0B
core/ContextMenu 75.3KB 75.6KB +281B 14.8KB 14.8KB 0B
core/ContextMenuItem 53.5KB 53.8KB +259B 9.8KB 9.8KB 0B
core/DateInput 100.2KB 100.5KB +288B 14.9KB 14.9KB 0B
core/DateRangeInput 70.1KB 70.4KB +277B 11.4KB 11.4KB 0B
core/DateTimeInput 110.4KB 110.7KB +289B 15.4KB 15.4KB 0B
core/DialogHeader 49.5KB 49.8KB +295B 10.0KB 10.0KB 0B
core/DrawerHeader 49.6KB 49.8KB +261B 11.4KB 11.4KB 0B
core/DropdownMenu 73.4KB 73.7KB +280B 15.0KB 15.0KB 0B
core/DropdownMenuCheckboxItem 16.4KB 16.6KB +276B 5.7KB 5.7KB 0B
core/DropdownMenuItem 15.6KB 15.9KB +268B 5.3KB 5.3KB 0B
core/DropdownMenuRadioItem 16.4KB 16.7KB +275B 5.7KB 5.7KB 0B
core/DropdownMenuSubMenu 58.7KB 59.0KB +263B 9.7KB 9.7KB 0B
core/FileInput 54.8KB 55.0KB +267B 10.5KB 10.5KB 0B
core/IconButton 43.7KB 44.0KB +279B 9.3KB 9.3KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 64.0KB 64.3KB +269B 10.3KB 10.3KB 0B
core/Lightbox 51.9KB 52.1KB +254B 10.0KB 10.0KB 0B
core/Link 44.1KB 44.4KB +274B 7.7KB 7.7KB 0B
core/ListItem 54.1KB 54.4KB +253B 10.6KB 10.6KB 0B
core/Markdown 145.2KB 145.5KB +268B 17.8KB 17.8KB 0B
core/MobileNav 51.7KB 51.9KB +240B 10.2KB 10.2KB 0B
core/MobileNavToggle 46.9KB 47.1KB +270B 9.6KB 9.6KB 0B
core/MoreMenu 73.7KB 74.0KB +284B 15.0KB 15.0KB 0B
core/MultiSelector 73.6KB 73.9KB +295B 14.8KB 14.8KB 0B
core/NumberInput 56.4KB 56.7KB +307B 10.9KB 10.9KB 0B
core/Outline 29.0KB 29.3KB +286B 4.0KB 4.0KB 0B
core/Pagination 85.5KB 85.7KB +263B 15.5KB 15.5KB 0B
core/Popover 50.3KB 50.5KB +278B 12.2KB 12.2KB 0B
core/PowerSearch 166.6KB 166.9KB +305B 20.1KB 20.1KB 0B
core/RadioListItem 52.8KB 53.1KB +276B 9.8KB 9.8KB 0B
core/SegmentedControlItem 8.6KB 8.9KB +285B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.1KB +294B 7.1KB 7.1KB 0B
core/Selector 77.0KB 77.3KB +277B 15.2KB 15.2KB 0B
core/SelectorOption 14.7KB 15.0KB +262B 5.2KB 5.2KB 0B
core/SideNav 64.6KB 64.9KB +249B 10.7KB 10.7KB 0B
core/SideNavCollapseButton 47.3KB 47.5KB +266B 9.6KB 9.6KB 0B
core/SideNavHeading 59.2KB 59.4KB +273B 10.4KB 10.4KB 0B
core/SideNavItem 55.3KB 55.5KB +258B 10.3KB 10.3KB 0B
core/Step 33.0KB 33.3KB +274B 5.5KB 5.5KB 0B
core/Stepper 48.1KB 48.3KB +270B 9.7KB 9.7KB 0B
core/Switch 49.0KB 49.3KB +279B 9.5KB 9.5KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 55.9KB 56.2KB +261B 10.2KB 10.2KB 0B
core/Table 84.7KB 84.9KB +259B 15.6KB 15.6KB 0B
core/TableCell 76.5KB 76.7KB +280B 15.3KB 15.3KB 0B
core/TableHeaderCell 76.1KB 76.3KB +261B 15.0KB 15.0KB 0B
core/TableSelectionToolbar 52.8KB 53.1KB +277B 11.3KB 11.3KB 0B
core/TextInput 54.2KB 54.5KB +265B 10.6KB 10.6KB 0B
core/Thumbnail 51.4KB 51.6KB +271B 11.1KB 11.1KB 0B
core/TimeInput 87.5KB 87.7KB +220B 13.9KB 13.9KB 0B
core/Timestamp 27.9KB 27.9KB -1B 9.7KB 9.7KB 0B
core/Toast 60.6KB 60.8KB +270B 9.9KB 9.9KB 0B
core/ToggleButton 44.5KB 44.8KB +245B 9.4KB 9.4KB 0B
core/Token 30.2KB 30.4KB +268B 4.6KB 4.6KB 0B
core/Tokenizer 69.3KB 69.6KB +257B 16.7KB 16.7KB 0B
core/TopNav 52.5KB 52.9KB +393B 10.5KB 10.7KB +168B
core/TopNavHeading 58.6KB 58.9KB +284B 10.2KB 10.2KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 55.0KB 55.3KB +271B 11.0KB 11.0KB 0B
core/TopNavMegaMenuFeaturedCard 6.7KB 6.7KB 0B 3.4KB 3.4KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 58.3KB 58.5KB +265B 10.3KB 10.3KB 0B
core/TreeList 40.4KB 40.6KB +273B 4.8KB 4.8KB 0B
core/Typeahead 62.2KB 62.4KB +275B 11.4KB 11.4KB 0B
lab/ChatEmojiPicker N/A 66.0KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.6KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 95.0KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.2KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 217.0KB N/A N/A 2.4KB N/A
lab/Comments N/A 221.6KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.4KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.8KB N/A N/A 722B N/A
lab/ListInput N/A 76.1KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 147.5KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 93.0KB N/A N/A 809B N/A
lab/Schedule N/A 60.7KB N/A N/A 3.9KB N/A
lab/TourStep N/A 55.9KB N/A N/A 590B N/A
lab/TransferList N/A 67.7KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.3KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.7KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 213.3KB N/A N/A 454B N/A
richtext/RichTextView N/A 252.1KB N/A N/A 2.3KB N/A

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

⚠️ Touch-only size budget

These components grew past the touch-only budget (2 KB gzip JavaScript or 1 KB gzip CSS per component). If the growth runs only on a coarse pointer, load its JavaScript lazily so the import does not carry it, or justify the cost in the pull request description; CSS stays in the package stylesheet, so justify it.

Component JavaScript CSS
core/AppShell +22.7KB ⚠️ +334B

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 35.4KB 35.8KB +415B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 504 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request needs an engineering owner's decision before it can be approved: no current spec covers TopNav's touch phone bar, its glass capsule or the new --color-background-glass token, and the draft touch-presentations spec it carries is not approved. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Separately, the @astryxdesign/cli docs and theme template changes need a Changeset naming that package (the lint check fails), and a phone bar with no end items shrinks to 42px with its title against the top edge instead of the documented 54px bar. Landing the stacked predecessors first would also let this pull request show only its own commit. [Automated review]

Comment on lines +105 to +112
paddingBlockStart: {
default: spacingVars['--spacing-2'],
'@media (pointer: coarse)': 0,
},
paddingBlockEnd: {
default: spacingVars['--spacing-2'],
'@media (pointer: coarse)': '10px',
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On a coarse pointer these two paddings add up to 54px only when the 44px capsule renders. A phone bar with no endContent and no drawer content measures 42px with its heading against the top edge (48px and centred on the base), while the Changeset and TopNav.doc.mjs promise a 54px bar. A 54px minimum block size under (pointer: coarse) with the items on a 44px row would keep the geometry; the empty-end-group test could assert it.

@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The glass token also changes shipped @astryxdesign/cli files (packages/cli/assets/docs/tree/tokens-color.doc.mjs, tokens.doc.mjs, packages/cli/assets/theme.template.ts), and the lint job's Changeset coverage step fails because no Changeset names that package. Adding '@astryxdesign/cli' here would clear it; the text could also name the old touch bar (48px, 8px padding, bare end icons) and the token override as the way to restyle the capsule.

const {container} = renderPhoneBar(profile());
const capsule = container.querySelector('[data-bar-capsule]')!;
expect(capsule).not.toBeNull();
expect(capsule.contains(screen.getByRole('button', {name: 'Profile'})));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This expect(...) has no matcher, so it never checks that the Profile button is inside the capsule; .toBe(true) would make it assert.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Touch gating. core/src/TopNav/TopNav.tsx:103 writes its own (pointer: coarse) check. This conflicts with AST-012 FR2 (touch is resolved once, through the closed adaptation vocabulary) and public-component-api INV16. Resolve through the shared touch resolver key so behavior matches the spec. If this behavior genuinely can't be expressed through the resolver, please raise it here as a spec question instead of shipping a separate check.

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation. The head quoted here predates the change; the next push replaces TopNav's own (pointer: coarse) check with the resolver's shared iOS touch keys (IOS_TOUCH_STYLE_KEYS from touchPresentation) and useTouchIdiom() in script, and the keys' remaining literal spelling goes away when the resolver exports them as constants (in progress). A mouse and an Android finger keep today's bar.

@vjeux
vjeux force-pushed the uikit/c16-bar-capsules branch from 7bd8556 to 94fed56 Compare October 25, 2026 02:35
@vjeux
vjeux requested a review from potatowagon as a code owner October 25, 2026 02:35
@vjeux vjeux changed the title feat(TopNav): on touch, the phone bar's end items sit on an iOS 26 glass capsule feat(TopNav): under a finger, the phone bar's end items sit on an iOS 26 glass capsule Oct 25, 2026
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 94fed56: the bar and its capsule now key on a coarse pointer, one touch model with Android included (#8316), with no iOS gate, and a mouse keeps today's bar. The key stays the literal @media (pointer: coarse), pinned to the resolver's constant by #8331's spelling test, because the StyleX build gives an imported at-rule key a different priority. TopNav has no pointer check of its own in script.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 25, 2026
@vjeux
vjeux marked this pull request as draft October 25, 2026 03:45
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Converting to draft: the capsule's glass material waits on the look-and-feel decision (the 03:41 note on #8283). I'll bring it back when that decision lands.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request needs an engineering owner's decision before it can be approved: no current spec covers TopNav's touch phone bar or its glass capsule, and the new --color-background-glass token relies on this branch's amendment to the theme-token grammar (architecture:theme-tokens INV10), which is not approved yet. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Independently of that decision, please:

  • regenerate and commit the native Swift theme (pnpm -F @astryxdesign/native generate:swift-ios), whose drift test fails in CI test-node;
  • keep the touch bar 54px when it has no end items (a heading-only bar is 42px with its title against the top) and draw no capsule when the end content renders nothing;
  • name the old touch bar and @astryxdesign/cli in the Changeset, add the base-against-head component size report (AppShell now imports the animator and gesture host for a touch-only effect), and add the missing matcher at TopNavBarCapsule.test.tsx:136.

Landing the stacked predecessors first would also let this pull request show only its own commit. [Automated review]

},
paddingBlockStart: {
default: spacingVars['--spacing-2'],
'@media (pointer: coarse)': 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Under a coarse pointer the bar has 0 top and 10px bottom padding, which makes 54px only when the 44px capsule renders. With no endContent and no drawer content the bar measures 42px with the heading flush against its top edge (48px and centred on the base), while the Changeset and TopNav.doc.mjs promise a 54px bar. A minBlockSize: '54px' under the same key, with the items on a 44px top row, would keep the geometry; the empty-end-group test could assert it.

const showToggle = hasMobileDrawerContent && hasAutoToggle;
// An empty end group draws no capsule.
const hasEndItems =
(endContent != null && endContent !== false) || showToggle;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reads the endContent value, not what renders: endContent={<>{signedIn && <Button … />}</>} with signedIn false, or a component that returns null, draws a 12 × 44 glass capsule with no children. Hiding the capsule when it is empty (display: {default: 'flex', ':empty': 'none'} on mobileBarEndCapsule) would cover every case.

// platter: the theme's surface at 85 %, so content scrolling under the bar
// still shows faintly through it. iOS 26.2 paints it rgb(251, 251, 255) over
// its light bar and rgb(25, 25, 25) over its dark one; no blur.
'--color-background-glass':

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new token also has to reach the committed native Swift theme: swiftIOSTheme.test.ts (drift G13) fails in CI test-node (1/4) with “Tokens.swift is stale; run pnpm -F @astryxdesign/native generate:swift-ios”. Running that command and committing the regenerated files clears it.

@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This commit also changes shipped @astryxdesign/cli files (tokens-color.doc.mjs, tokens.doc.mjs, theme.template.ts), so '@astryxdesign/cli': patch belongs here; today the lint gate passes only through a predecessor's Changeset. The text could also name the old touch bar (48px, 8px padding, bare end icons) and the token as the way to restyle the capsule, as AST-017 FR3 asks of an appearance change.

const {container} = renderPhoneBar(profile());
const capsule = container.querySelector('[data-bar-capsule]')!;
expect(capsule).not.toBeNull();
expect(capsule.contains(screen.getByRole('button', {name: 'Profile'})));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This expect(...) has no matcher, so it never checks that the Profile button is inside the capsule; .toBe(true) makes it assert.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Thanks for this! The same guardrails Cindy set on #8142 apply here. Could you propose an approach that fits them?

  • Visuals aren't hardcoded in the component; they're themeable. That covers the touch bar's height and insets and the end items' capsule fill.
  • New theme data must go through our existing theme infrastructure, with at most one new theme role, not a one-off token. --color-background-glass names a material it isn't.
  • Themeable parts keep following the theming spec, and TopNav's existing variants are respected.

The element count already complies: this commit adds no elements.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Here is the same approach as on #8142, applied to the phone bar.

Height and insets, through the existing target. The touch geometry moves onto the top-nav target in its mobile-bar mode and is built from existing tokens. The 16px side insets are --spacing-4. The items row is the capsule: --size-element-lg (UIKit's 36pt bar-button cell) plus --spacing-1 above and below, so 44px. Only the 10px under it is UIKit-measured geometry. top-nav declares paddingInline, paddingBlockStart and paddingBlockEnd as guaranteed properties, so a theme changes all of it through components['top-nav'], either with the mode state or in an adaptation with when: {pointer: 'coarse'}, and the bar's height follows. There is no height literal and no new key.

Capsule fill, with no new token. --color-background-glass goes. The capsule paints --color-background-muted, the existing fill for tracks and grouped controls (ProgressBar's track, InputGroupText). In the base tokens it is a translucent tint, so the capsule shows over a body or a surface bar, in light and dark. A theme retunes it through that token. That adds no new theme role.

Theming spec. No new target, prop or attribute. The capsule is TopNav's existing Flex area (end content and the drawer toggle), painted in one mode. The guaranteed paddings get compiler and runtime evidence on top-nav.

Variants. Only mobile-bar under a finger changes. default and drawer render as today, and a mouse keeps today's phone bar. Inside AppShell, the bar's colour stays the header's, as proposed on #8142.

If this works for you, I'll implement it and post evidence.

@vjeux
vjeux force-pushed the uikit/c16-bar-capsules branch from 94fed56 to b4c395b Compare October 26, 2026 17:07
vjeux added 2 commits October 27, 2026 02:33
…xperimental touch exports

The motion README's Files table is the union: main's rows, with meta#8663's
resolveSnapPoint.ts and selectPage.ts, plus this branch's, in main's
order.

# Conflicts:
#	packages/core/src/motion/README.md
vjeux added 13 commits October 27, 2026 03:29
…ntries and internal/motion

Main's resolveSnapPoint test (meta#8663) keeps the release policy internal:
not on the package root, not in a barrel, and no package export naming
motion. The last check also caught ./internal/motion, Core's internal
entry for Astryx's own packages (architecture:react-component-runtime
INV13), which re-exports no release policy. The export check now covers
the public keys only, and a new assertion keeps the claim as strong:
internal/motion must not re-export resolveSnapPoint either.

On this branch with main the test failed on ./internal/motion; it now
passes, and it fails when internal/motion re-exports resolveSnapPoint.
vjeux added 7 commits October 27, 2026 05:05
… a mouse press

At the document, an event from inside an open shadow root is retargeted to
the shadow host, and so is a touch's target, so the host looked for
participants from the shadow host and found none inside the root: the
browser then sent its own mousedown and click. Both cases, a finger and a
mouse, attach a tap to an element in the shadow root. Fail until the host
resolves targets through the composed path.
…composed path

Inside an open shadow root the document's listeners see the shadow host,
in event.target and in a touch's target alike, so a recognizer on an
element in the root never got its finger, and nothing claimed it. landedOn
takes the composed path's first element for the event's own target and for
a touch whose target is the event's (another finger keeps its own): a
touch's start, a pointer's press and its pairing, the last touch a scroll
reads, a cancelled pointer's touch and hover.
…he host resolves targets through the composed path) into meta#8320
…s its divider with a coarse-pointer rule, and drops its layer where no scrollend fires

Review findings on meta#8142:

- With a mouse, a variant added by module augmentation painted a surface
  header. A page-scrolling shell now keeps each variant's own fill (none for
  an augmented one) and adds only the coarse-pointer transparent rule.
- The section divider was dropped through the resolver's guess, so a server
  render with `defaultIsMobile` lost it. It stays in the markup, and a
  coarse-pointer rule takes its width away on the touch bar.
- Where the browser sends no `scrollend`, the layer stayed promoted after a
  fling (re-checked only at a finger change). The scroller's scroll events
  now time a re-check 200 ms after the last one, past the finger-scroll
  watch's 150 ms spell.
- One document listener per effect: `scrollend`, or `scroll` where the
  browser sends none. This base has no shared input stream (it arrives with
  meta#8665's line); the effect moves onto it there.
- The WebKit workaround names its removal condition.

Red first: the five new cases fail on ae06884.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request needs an engineering owner's decision before it can be approved: no current spec covers TopNav's touch capsule, so an engineering owner needs to approve or redirect it, or name the record that governs it, by commenting /approve-spec <full head SHA>; the author may do so as an engineering owner. Independently of that decision, please:

  • keep the default look's sizes under touch, as spec:AST-094 FR42 and DEC-20 require, since the 54px bar, 16px insets and 44px end group belong in a theme adaptation unless those claims are amended;
  • keep the touch bar's height and a centred heading when it has no end items, and draw no capsule when the end content renders nothing;
  • add the missing matcher at TopNavBarCapsule.test.tsx:138 and name the old touch bar in the Changeset.

scripts/touch-support-bundle.test.mjs also fails on this head because of the AppShell change from #8142, and landing the stacked predecessors first would let this pull request show only its own change. [Automated review]

padding: spacingVars['--spacing-2'],
paddingInline: {
default: spacingVars['--spacing-2'],
'@media (pointer: coarse)': spacingVars['--spacing-4'],

Copy link
Copy Markdown

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 widens the insets from 8px to 16px, and the block paddings below grow the bar from 48px to 54px. spec:AST-094 FR42 says the default look makes no box size increase for phones or touch and that mobile size increases belong to theme adaptations (DEC-20). Keeping the default boxes and moving this geometry into a theme adaptation (when: {pointer: 'coarse'}) would conform, unless an engineering owner amends FR42/DEC-20.

},
paddingBlockEnd: {
default: spacingVars['--spacing-2'],
'@media (pointer: coarse)': '10px',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With 0 above and 10px below, the bar is 54px only while the 44px capsule renders. A heading-only phone bar (no endContent, no drawer content) measures 42px under a finger with its heading against the top edge, where the base gives 48px with the heading centred. A minimum block size of the capsule's height plus 10px under the same key would keep the geometry, and the empty-end-group test could assert it.

const showToggle = hasMobileDrawerContent && hasAutoToggle;
// An empty end group draws no capsule.
const hasEndItems =
(endContent != null && endContent !== false) || showToggle;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reads the endContent value, not what renders: endContent={<>{signedIn && <Button … />}</>} with signedIn false, or a component that returns null, draws a 12 × 44 capsule with no children. display: {default: 'flex', ':empty': 'none'} on mobileBarEndCapsule would cover every case.

const {container} = renderPhoneBar(profile());
const capsule = container.querySelector('[data-bar-capsule]')!;
expect(capsule).not.toBeNull();
expect(capsule.contains(screen.getByRole('button', {name: 'Profile'})));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This expect(...) has no matcher, so it never checks that the Profile button is inside the capsule; .toBe(true) would make it assert.

'@astryxdesign/core': patch
---

[feat] TopNav's phone bar under a finger (one touch model on every touch device, iOS and Android alike) takes the iOS 26 bar's geometry, and its end items sit together on one capsule. The geometry comes from the theme's spacing and size tokens: with the defaults the bar is 54px tall, with its items on the top 44px and 16px from the screen's sides, and the capsule is 44px tall and fully rounded, placing each control where UIKit centres its 36px cells. The capsule paints the theme's `--color-background-surface`, with no shadow and nothing blurred. No token is added. With a mouse the bar is unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-017 FR3 asks an appearance Changeset to name the old and the new look. This one describes the new touch bar only; naming the old one (48px, 8px insets, bare end icons) would complete it.

Resolves the conflict in the search-icon goldens: main and this branch
both moved the "icon" match count. Regenerated with
`vitest run packages/cli/test/experimental-isolation.test.mjs --update`
on the merged tree; the goldens differ from main's only by this branch's
TopNav docs line (224 matches for main's 223).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval waits on an engineering owner's decision: the touch phone bar's 54px height and 44px end group contradict spec:AST-094 FR42 and DEC-20, and no current spec governs the surface capsule, so an owner needs to approve the capsule and either amend FR42/DEC-20 on main or move the growth into a theme adaptation, recording that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do this as an engineering owner). Whatever that decision is, these changes are still needed:

  • Split out the carried #8142/#8562 stack, which makes up 27,773 of the 28,319 authored lines and includes the failing root-entry test in scripts/touch-support-bundle.test.mjs.
  • Fix the 42px heading-only bar, the empty capsule drawn when end content renders nothing, the missing matcher at TopNavBarCapsule.test.tsx:138, and the Changeset's missing old look.

[Automated review]

This branch was successfully deployed

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

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants