Skip to content

refactor(Layer): one presentation hold keeps a layer's chosen presentation until its close settles - #8587

Open
cixzhang wants to merge 2 commits into
stack/8587-basefrom
feat/presentation-hold
Open

cixzhang wants to merge 2 commits into
stack/8587-basefrom
feat/presentation-hold

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8187 (Popover's touch presentation) and #8342 (the input hold, Layer/inputHold): the base branch stack/8587-base holds both, so the diff is this change alone; it retargets to main as they land.

What it does

One internal module with one job, the layer runtime's presentation hold (architecture:layer-runtime INV14): keep a layer's chosen presentation from the open until its close settles. packages/core/src/Layer/presentationHold.ts:

  • usePresentationHold({isOpen, presentation}) → {presentation, isHeld, release}. The render in which isOpen turns on holds presentation as it is then. It stays held, whatever presentation becomes, until the component calls release() when its close has settled.
  • createPresentationHold(): the same hold for code that chooses at an open event (take, current, isHeld, release).

It decides nothing about timing; the component says when its close has settled. It isn't the input hold (inputHold.ts, which drops input while a presentation animates). It isn't exported from the package or the Layer barrel (presentationHold.test.tsx locks that). It contains no component names.

Record

architecture:layer-runtime's current-gaps paragraph no longer says no shared presentation hold exists or that Popover builds its own; Owning code and verified_by name Layer/presentationHold.ts. INV14 itself is unchanged.

Writing rubric results

R1 narration: none; the current-gaps sentence and the Owning code entry state what the runtime owns now, in the present tense.
R2 research: none in the record.
R3 evidence: no evidence, basis or verification cell changed; verified_by names a test file.
R4 rejected alternatives: none added.
R5 change references: none.
R6 open questions: none touched.

First adopter: Popover

All three of Popover's latches move onto it:

Other components adopt it one pull request each: MobileNav (#8590) and Selector/MultiSelector (#8591) are open; Dialog, Lightbox and ToastViewport follow their touch-presentation pull requests, and BottomSheet its sheet stack. Known exception: Drawer follows side while open; adopting the shared hold would freeze it at open, a public behavior change for a later decision, so Drawer keeps its own exit latch.

No behavior change

Popover's existing tests are the oracle and pass unchanged: Popover.test.tsx, PopoverPresentation.test.tsx (the sheet latch, rotation and reopen cases) and the Layer suite, 370 tests in all, plus the module's own tests. The real-browser check is Popover's touch-presentation specs, which this PR's CI runs because the change touches the Popover folder.

Size (gzip KB, minified)

Method: a fresh packages/core dist at the base and at this head; an entry re-exporting the component; esbuild bundle, minify, splitting, ESM, packages external. Desktop is the static-import closure; phone is all chunks.

Component Desktop Phone
Popover 49.27 → 49.43 (+0.16) 68.85 → 69.01 (+0.16)
Drawer 25.14 → 25.15 (+0.01) 38.65 → 38.65 (+0.00)
DateRangeInput 56.12 → 56.12 (+0.00) 71.95 → 71.95 (+0.00)
Table 71.03 → 71.03 (+0.00) 90.73 → 90.73 (+0.00)
PowerSearch 136.42 → 136.43 (+0.01) 140.22 → 140.22 (+0.00)

Popover gains 0.16 KB for the shared module; later adopters reuse it.

Testing

  • pnpm vitest run --project ui packages/core/src/Layer packages/core/src/Popover: 370 pass.
  • tsc and eslint clean.
  • [chore] changeset (internal, no behavior change).

Co-authored with vjeux (the latches move from his Popover code).

…ation until its close settles

architecture:layer-runtime INV14 gives the layer runtime the presentation
hold: keeping a layer's chosen presentation from the open until the close
settles. Layer/presentationHold.ts (internal) is that one job:
createPresentationHold for code that chooses at an open event, and
usePresentationHold for a choice taken during render; the component releases
it when its close settles.

Popover adopts it for its three latches, with no behavior change: the
controlled surface (taken as isOpen turns on, let go as it turns off), the
sheet kept mounted until its dialog has closed, and the touch-or-pointer
presentation its close plays in (usePopoverSurfaceMotion). Not the input
hold (inputHold.ts). Not exported; a test locks that.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
@vercel

vercel Bot commented Oct 26, 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 26, 2026 8:51pm UTC

Request Review

@github-actions github-actions Bot added needs:spec-owner-review Current knowledge records await owner approval needs:design-review Affects visuals — Design should review labels Oct 26, 2026
@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 26, 2026, 10:04 PM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Popover (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 412B 412B 0B
Lines of Code 977 1418 -
Complexity Very High Very High (143) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 54.6KB 54.8KB +273B 12.4KB 12.4KB 0B
core/AppShell 55.7KB 55.9KB +278B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 39.9KB 40.2KB +315B 6.4KB 6.4KB 0B
core/Banner 47.8KB 48.1KB +301B 9.9KB 9.9KB 0B
core/BaseTypeahead 53.8KB 54.1KB +296B 9.9KB 9.9KB 0B
core/BottomSheet 26.0KB 28.6KB +2.6KB ⚠️ 7.7KB 7.7KB +1B
core/BottomSheetSwitcher 9.2KB 9.2KB 0B 3.3KB 3.3KB 0B
core/BreadcrumbItem 65.9KB 66.2KB +301B 11.0KB 11.0KB 0B
core/Button 43.3KB 43.6KB +284B 9.2KB 9.2KB 0B
core/Calendar 54.9KB 55.2KB +307B 10.0KB 10.0KB 0B
core/Carousel 50.2KB 50.3KB +79B 10.2KB 10.2KB 0B
core/ChatComposer 60.4KB 61.1KB +748B 10.8KB 10.8KB 0B
core/ChatComposerDrawer 23.9KB 23.9KB +15B 4.5KB 4.5KB 0B
core/ChatComposerInput 55.5KB 56.2KB +744B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.5KB 56.2KB +744B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.5KB 46.8KB +277B 9.5KB 9.5KB 0B
core/ChatLayout 48.8KB 64.6KB +15.7KB ⚠️ 9.9KB 10.0KB +39B
core/ChatLayoutScrollButton 46.4KB 46.6KB +267B 9.6KB 9.6KB 0B
core/ChatMessage 23.1KB 23.1KB +13B 3.5KB 3.5KB 0B
core/ChatMessageBubble 7.0KB 7.0KB 0B 3.6KB 3.6KB 0B
core/ChatMessageList 28.4KB 28.4KB +13B 7.8KB 7.8KB 0B
core/ChatMessageMetadata 25.8KB 25.8KB +12B 3.9KB 3.9KB 0B
core/ChatSendButton 45.8KB 46.1KB +277B 9.2KB 9.2KB 0B
core/ChatSystemMessage 6.7KB 6.7KB 0B 3.7KB 3.7KB 0B
core/ChatTokenizedText 6.9KB 6.9KB 0B 3.9KB 3.9KB 0B
core/ChatToolCalls 31.9KB 31.9KB +12B 8.9KB 8.9KB 0B
core/CheckboxInput 49.4KB 49.7KB +284B 9.3KB 9.3KB 0B
core/CheckboxListItem 53.6KB 53.8KB +277B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 60.5KB 60.7KB +287B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.0KB +291B 4.2KB 4.2KB 0B
core/CommandPalette 49.2KB 49.5KB +272B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 55.3KB 55.6KB +337B 10.6KB 10.6KB 0B
core/ContextMenu 77.3KB 77.2KB -82B 14.5KB 14.5KB +7B
core/ContextMenuItem 16.6KB 16.9KB +304B 5.2KB 5.2KB 0B
core/DateInput 92.5KB 94.3KB +1.9KB 14.6KB 14.6KB +6B
core/DateRangeInput 69.0KB 69.2KB +284B 11.2KB 11.2KB 0B
core/DateTimeInput 102.6KB 104.5KB +1.9KB 15.1KB 15.1KB +6B
core/DialogHeader 48.8KB 49.1KB +292B 9.8KB 9.8KB 0B
core/DrawerHeader 48.9KB 49.2KB +262B 11.2KB 11.2KB 0B
core/DropdownMenu 72.2KB 72.2KB -30B 14.6KB 14.6KB +7B
core/DropdownMenuCheckboxItem 16.1KB 16.4KB +284B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 15.7KB +287B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.5KB +275B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 57.5KB 57.8KB +268B 9.5KB 9.5KB 0B
core/FileInput 53.7KB 54.0KB +277B 10.3KB 10.3KB 0B
core/IconButton 43.3KB 43.6KB +284B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 63.2KB 63.5KB +307B 10.1KB 10.1KB 0B
core/Lightbox 49.5KB 49.8KB +284B 9.8KB 9.8KB 0B
core/Link 43.5KB 43.8KB +298B 7.6KB 7.6KB 0B
core/ListItem 53.0KB 53.3KB +286B 10.5KB 10.5KB 0B
core/Markdown 143.9KB 143.9KB -4B 17.2KB 17.2KB +4B
core/MobileNav 48.8KB 49.1KB +284B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.2KB 46.5KB +271B 9.4KB 9.4KB 0B
core/MoreMenu 72.5KB 72.4KB -36B 14.6KB 14.6KB +7B
core/MultiSelector 75.7KB 75.6KB -143B 14.5KB 14.5KB +4B
core/NumberInput 55.6KB 55.9KB +298B 10.7KB 10.7KB 0B
core/Outline 28.6KB 28.9KB +302B 4.0KB 4.0KB 0B
core/Pagination 87.3KB 87.6KB +285B 15.2KB 15.2KB +4B
core/Popover 49.8KB 62.6KB +12.7KB ⚠️ 12.0KB 12.0KB +4B
core/PowerSearch 147.7KB 149.3KB +1.6KB 18.5KB 18.5KB +4B
core/RadioListItem 52.1KB 52.4KB +289B 9.7KB 9.7KB 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 78.7KB 78.6KB -167B 14.8KB 14.8KB +5B
core/SelectorOption 14.5KB 14.8KB +251B 5.1KB 5.1KB 0B
core/SideNav 55.8KB 56.1KB +284B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.6KB 46.9KB +254B 9.5KB 9.5KB 0B
core/SideNavHeading 54.7KB 55.0KB +253B 10.1KB 10.1KB 0B
core/SideNavItem 54.4KB 54.7KB +277B 10.1KB 10.1KB 0B
core/Step 32.4KB 32.7KB +292B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.7KB +278B 9.6KB 9.6KB 0B
core/Switch 48.4KB 48.6KB +299B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 51.6KB 51.9KB +286B 10.0KB 10.0KB 0B
core/Table 83.5KB 83.4KB -105B 15.0KB 15.0KB +7B
core/TableCell 78.4KB 78.3KB -89B 14.6KB 14.7KB +7B
core/TableHeaderCell 78.1KB 78.0KB -118B 14.6KB 14.6KB +7B
core/TableSelectionToolbar 52.4KB 52.6KB +288B 11.2KB 11.2KB 0B
core/TextInput 53.1KB 53.4KB +270B 10.5KB 10.5KB 0B
core/Thumbnail 50.1KB 50.4KB +288B 11.0KB 11.0KB 0B
core/TimeInput 79.3KB 81.2KB +1.9KB 13.6KB 13.6KB +3B
core/Timestamp 27.5KB 27.5KB +13B 9.5KB 9.5KB 0B
core/Toast 59.8KB 60.1KB +268B 9.7KB 9.7KB 0B
core/ToggleButton 44.1KB 44.4KB +254B 9.3KB 9.3KB 0B
core/Token 29.6KB 29.9KB +302B 4.5KB 4.5KB 0B
core/Tokenizer 64.3KB 64.6KB +298B 11.7KB 11.7KB 0B
core/TopNav 49.7KB 49.9KB +287B 10.4KB 10.4KB 0B
core/TopNavHeading 54.2KB 54.4KB +246B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.2KB 54.4KB +274B 10.7KB 10.7KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 53.6KB 53.9KB +261B 10.1KB 10.1KB 0B
core/TreeList 32.5KB 32.8KB +287B 4.7KB 4.7KB 0B
core/Typeahead 61.0KB 61.2KB +274B 11.3KB 11.3KB 0B

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/BottomSheet +2.6KB ⚠️ +1B
core/ChatLayout +15.7KB ⚠️ +39B
core/Popover +12.7KB ⚠️ +4B

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 34.3KB 34.3KB +26B

Accessibility Audit

Status: 2 accessibility violation(s) found — 2 serious.

ChatComposer - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/19 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
ChatComposerInput - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/18 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Visual Regression

55 of 502 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 440,175
Chat Full AI Chat probe dark 438,105
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Chat Empty State probe light 122,185
Chat Empty State probe dark 120,076
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
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@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 can't be approved yet: it needs an engineering owner's decision on a spec addition, and three changes. Through the stacked exit-copy work, the diff against main adds INV13 ("One exit copy per closing layer") to the current architecture:layer-runtime record. An engineering owner records approval or rejection by commenting /approve-spec followed by the full head commit SHA on this pull request, and the author may do so if they are an engineering owner.

  • Land the parent pull requests (#8187, #8342 and the stack beneath them) and rebase, or retarget the base, so the diff carries only the 295-line presentation-hold commit instead of 38,390 authored lines.
  • Add touchHold to the useCallback dependency list at packages/core/src/Popover/usePopoverSurfaceMotion.ts:134, which currently fails the Lint check.
  • Update the Current gaps paragraph and Owning code in docs/architecture/layer-runtime.md, which still say no shared presentation hold exists and that Popover builds its own.

[Automated review]

source: boxOf(source),
}));
},
[motionFor],

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 callback calls touchHold.take(isTouch) (line 118) but lists only motionFor. Under CI's strict lint, @eslint-react/exhaustive-deps is an error, and the Lint job fails here. touchHold is stable, so listing it changes nothing at runtime:

[motionFor, touchHold],

Comment thread docs/architecture/layer-runtime.md Outdated
Comment on lines +534 to +540
INV14 and INV15 are not met yet, and `spec:AST-003` does not cover them. The
runtime owns the shared exit copy (INV13, `placeLayerExitCopy`); no shared
presentation hold, runtime backdrop tint or lift module exists, Dialog, Drawer,
BottomSheet, Lightbox, MobileNav, ToastViewport, and Selector each build their
own top-layer mounting, exit, or backdrop, Popover builds its own
presentation hold for its sheet presentation, and Popover latches its chosen
anchored presentation until the close settles.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Once this pull request's presentation-hold commit lands, "no shared presentation hold … exists" and "Popover builds its own presentation hold for its sheet presentation" are no longer true, because Popover's latches go through Layer/presentationHold.ts. Please say that the runtime owns the presentation hold and Popover uses it, keep the seven other components as the remaining gap, and add Layer/presentationHold.ts to Owning code.

…the record names the shared hold

usePopoverSurfaceMotion's returned callback reads touchHold, so it is in its
dependency list (lint). architecture:layer-runtime's current-gaps paragraph
no longer says no shared presentation hold exists or that Popover builds
its own: the runtime owns it (Layer/presentationHold.ts) and Popover uses
it; Owning code and verified_by name it.
@cixzhang
cixzhang changed the base branch from main to stack/8587-base October 26, 2026 20:42
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 99b0ba8

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

One small change is needed before approval: this pull request edits docs/architecture/layer-runtime.md, so please add a ## Writing rubric results section to the description with one line for each of R1–R6 from docs/contributing/spec-writing.md. The code needs no changes: Popover's three latches move onto the internal presentation hold with no behavior change, the Layer and Popover tests pass, Chromium renders the sheet and touch states exactly as before, and the record change is approved at this head. The failed Review signal job refused a stale 208-file count, so re-running it should publish review-required for this head.

[Automated review]

@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 looks ready to approve: Popover's three presentation latches move onto the layer runtime's internal presentation hold with no behavior change, the unchanged Layer and Popover tests pass, Chromium renders the sheet and touch states exactly as before, and the record change is approved at this head with the writing rubric results now in the description. One housekeeping step remains: the Review signal check failed on a stale 208-file count when the base changed, so re-running it should publish review-required for this head.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
# Conflicts:
#	docs/architecture/layer-runtime.md
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
Takes meta#8223's head, which carries meta#8187, meta#8342 and meta#8587 on main, and
meta#8329's, which carries meta#8909, meta#8340 and meta#8497 on meta#8360. They merge with no
conflict, and nothing in this pull request's own files needed resolving.
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Oct 31, 2026

This branch was successfully deployed

1 active deployment
Preview — 99b0ba88 Deployed Oct 26, 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant