Skip to content

feat(Lightbox): swipe between gallery photos like Photos - #8197

Draft
vjeux wants to merge 2199 commits into
meta:mainfrom
vjeux:uikit/c24-pager
Draft

vjeux wants to merge 2199 commits into
meta:mainfrom
vjeux:uikit/c24-pager

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8148 and #8146, and on #8665 and #8676 (gestures for every pointer), which this stack carries. Review only this PR's change: vjeux/astryx@1180c0f...2a1354c

Why

On a phone, a Lightbox gallery moves only through two small arrow buttons. People expect to swipe between photos as in Photos, with a zoomed photo panning to its edge before the next one comes.

What led to it

The UIKit-parity work for Astryx (spec:AST-094): after the zoom (#8148), paging, on #8146's shared pager.

Options and tradeoffs

  • CSS scroll snapping: native and small, but it cannot share a gesture with the pinch (a drifting pinch pages) or apply UIKit's release rule.
  • A pager on the gesture host (this PR): the row follows the finger, the release picks the page by UIKit's rule (one page at most) and settles on the compositor, and zoom and paging share the drag as nested scroll views do. The cost is JavaScript: 6.7 KB of gzip (88,555 → 95,206 bytes), and 0.2 KB of CSS.

Why this choice

Only the pager keeps a pinch from paging and lets a zoomed photo keep the drag until its edge, as Photos does. Pages sit 40 pt apart, Photos' measured gap.

How to check it

In the "Gallery with zoom" story, on a phone:

  1. Swipe left: the next photo slides in, and the counter reads 2 / 4.
  2. Drag a little and let go: the photo springs back.
  3. Zoom in, drag to the photo's edge, then drag again: the next photo comes.
  4. Pinch with a sideways drift: the photo zooms; nothing pages.
  5. Tap Next: the row slides.
  6. With a mouse, drag sideways, or scroll sideways on a trackpad: the row follows, as a finger's does.
  7. With Reduce Motion on, a swipe or Next fades instead of sliding.

Evidence

Safari on the iOS 26.2 simulator, a swipe left:

main this PR, with the main thread blocked 600 ms after the lift
main: nothing moves the next photo settles through the block

Android 15 emulator, Chromium: two swipes

The page settle's busy proof used to judge a 7 ms window, too short to judge anything: it tracked the gallery track, which is wider than the screen, so its edge stayed pinned to the screen's. It now tracks the next photo as it slides in, against a compositor-only control animation, and passes in both engines.

@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 7:16am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@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 30, 2026, 1:43 PM UTC

@github-actions

github-actions Bot commented Oct 24, 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

Lightbox (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 394B 394B 0B
Lines of Code 804 2456 -
Complexity Very High Very High (138) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 57.6KB +604B 12.4KB 12.4KB 0B
core/AppShell 58.0KB 58.6KB +547B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 40.1KB 40.7KB +631B 6.4KB 6.4KB 0B
core/Banner 48.0KB 48.5KB +592B 9.9KB 9.9KB 0B
core/BaseTypeahead 54.5KB 55.0KB +554B 9.9KB 9.9KB 0B
core/BottomSheet 33.9KB 33.9KB 0B 7.9KB 7.9KB 0B
core/BottomSheetSwitcher 14.8KB 14.8KB 0B 3.3KB 3.3KB 0B
core/BreadcrumbItem 66.5KB 67.3KB +784B 11.0KB 11.0KB 0B
core/Button 43.4KB 44.0KB +602B 9.2KB 9.2KB 0B
core/Calendar 55.1KB 55.6KB +600B 10.0KB 10.0KB 0B
core/Carousel 50.4KB 50.9KB +586B 10.2KB 10.2KB 0B
core/ChatComposer 60.8KB 61.4KB +593B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.9KB 56.5KB +570B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.9KB 56.5KB +570B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.6KB 47.2KB +585B 9.5KB 9.5KB 0B
core/ChatLayout 49.0KB 49.6KB +610B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.5KB 47.1KB +592B 9.6KB 9.6KB 0B
core/ChatSendButton 46.0KB 46.5KB +575B 9.2KB 9.2KB 0B
core/CheckboxInput 50.1KB 50.6KB +560B 9.3KB 9.3KB 0B
core/CheckboxListItem 54.2KB 54.8KB +546B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.4KB +626B 6.9KB 6.9KB 0B
core/CodeBlock 60.7KB 61.2KB +584B 10.7KB 10.7KB 0B
core/Collapsible 17.0KB 17.6KB +594B 4.2KB 4.2KB 0B
core/CommandPalette 51.0KB 51.6KB +618B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 9.0KB +596B 3.5KB 3.5KB 0B
core/ComplexSelector 56.0KB 56.5KB +580B 10.6KB 10.6KB 0B
core/ContextMenu 74.7KB 75.5KB +798B 14.6KB 14.6KB 0B
core/ContextMenuItem 53.0KB 53.6KB +561B 9.6KB 9.6KB 0B
core/DateInput 99.7KB 100.3KB +608B 14.8KB 14.8KB 0B
core/DateRangeInput 69.5KB 70.1KB +617B 11.2KB 11.2KB 0B
core/DateTimeInput 109.8KB 110.4KB +613B 15.2KB 15.2KB 0B
core/DialogHeader 49.0KB 49.5KB +580B 9.8KB 9.8KB 0B
core/DrawerHeader 49.1KB 49.6KB +552B 11.2KB 11.2KB 0B
core/DropdownMenu 72.8KB 73.6KB +759B 14.7KB 14.7KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.7KB +617B 5.6KB 5.6KB 0B
core/DropdownMenuItem 15.4KB 16.0KB +611B 5.2KB 5.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.8KB +607B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 58.2KB 58.9KB +726B 9.5KB 9.5KB 0B
core/FileInput 54.3KB 54.8KB +546B 10.3KB 10.3KB 0B
core/IconButton 43.5KB 44.1KB +602B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.9KB +606B 4.7KB 4.7KB 0B
core/LayerProvider 63.4KB 64.0KB +600B 10.1KB 10.1KB 0B
core/Lightbox 51.4KB 93.0KB +41.6KB ⚠️ 9.8KB 10.0KB +260B
core/Link 43.7KB 44.2KB +590B 7.6KB 7.6KB 0B
core/ListItem 53.7KB 54.2KB +554B 10.5KB 10.5KB 0B
core/Markdown 144.5KB 145.3KB +760B 17.4KB 17.4KB 0B
core/MobileNav 51.2KB 51.7KB +555B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.4KB 46.9KB +572B 9.4KB 9.4KB 0B
core/MoreMenu 73.2KB 73.9KB +745B 14.7KB 14.7KB 0B
core/MultiSelector 73.1KB 73.7KB +630B 14.6KB 14.6KB 0B
core/NumberInput 55.9KB 56.4KB +598B 10.7KB 10.7KB 0B
core/Outline 28.8KB 29.3KB +589B 4.0KB 4.0KB 0B
core/Pagination 84.9KB 85.7KB +775B 15.3KB 15.3KB 0B
core/Popover 50.0KB 50.6KB +604B 12.1KB 12.1KB 0B
core/PowerSearch 164.5KB 165.2KB +803B 19.9KB 19.9KB 0B
core/RadioListItem 52.4KB 52.9KB +571B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 9.2KB +597B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.4KB +623B 7.1KB 7.1KB 0B
core/Selector 76.5KB 77.2KB +779B 15.0KB 15.0KB 0B
core/SelectorOption 14.5KB 15.1KB +601B 5.1KB 5.1KB 0B
core/SideNav 58.1KB 58.7KB +588B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.8KB 47.3KB +573B 9.5KB 9.5KB 0B
core/SideNavHeading 54.9KB 55.4KB +564B 10.1KB 10.1KB 0B
core/SideNavItem 54.6KB 55.2KB +551B 10.1KB 10.1KB 0B
core/Step 32.6KB 33.1KB +582B 5.3KB 5.3KB 0B
core/Stepper 47.5KB 48.1KB +567B 9.6KB 9.6KB 0B
core/Switch 48.5KB 49.1KB +579B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.3KB +606B 4.1KB 4.1KB 0B
core/TabMenu 51.8KB 52.3KB +579B 10.0KB 10.0KB 0B
core/Table 84.0KB 84.8KB +800B 15.2KB 15.2KB 0B
core/TableCell 75.8KB 76.6KB +801B 14.9KB 14.9KB 0B
core/TableHeaderCell 75.5KB 76.3KB +822B 14.9KB 14.9KB 0B
core/TableSelectionToolbar 52.5KB 53.1KB +601B 11.2KB 11.2KB 0B
core/TextInput 53.7KB 54.3KB +579B 10.5KB 10.5KB 0B
core/Thumbnail 50.8KB 51.4KB +598B 11.0KB 11.0KB 0B
core/TimeInput 86.9KB 87.5KB +533B 13.8KB 13.8KB 0B
core/Timestamp 27.7KB 27.7KB +1B 9.5KB 9.5KB 0B
core/Toast 60.0KB 60.6KB +580B 9.7KB 9.7KB 0B
core/ToggleButton 44.3KB 44.8KB +563B 9.3KB 9.3KB 0B
core/Token 29.7KB 30.3KB +600B 4.5KB 4.5KB 0B
core/Tokenizer 68.8KB 69.3KB +611B 16.5KB 16.5KB 0B
core/TopNav 52.0KB 52.5KB +545B 10.4KB 10.4KB 0B
core/TopNavHeading 54.3KB 54.8KB +557B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.6KB +605B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.3KB 54.9KB +600B 10.8KB 10.8KB 0B
core/TopNavMegaMenuItem 9.1KB 9.7KB +603B 4.1KB 4.1KB 0B
core/TopNavMenu 53.7KB 54.3KB +580B 10.1KB 10.1KB 0B
core/TreeList 38.4KB 39.0KB +581B 4.7KB 4.7KB 0B
core/Typeahead 61.6KB 62.2KB +591B 11.3KB 11.3KB 0B
lab/ChatEmojiPicker N/A 65.8KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.4KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 94.7KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.1KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 257.6KB N/A N/A 2.4KB N/A
lab/Comments N/A 262.2KB N/A N/A 2.5KB N/A
lab/DiffViewer N/A 46.3KB N/A N/A 2.1KB N/A
lab/EmojiPicker N/A 63.6KB N/A N/A 722B N/A
lab/ListInput N/A 75.8KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 147.2KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 92.8KB N/A N/A 809B N/A
lab/Schedule N/A 60.5KB N/A N/A 3.9KB N/A
lab/TourStep N/A 56.0KB N/A N/A 590B N/A
lab/TransferList N/A 67.4KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.0KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.4KB 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 251.9KB 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/Lightbox +41.6KB ⚠️ +260B

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.9KB 35.0KB +84B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

57 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 37 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.

Approval waits on an engineering-owner decision: swipe paging for Lightbox galleries rests on the draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2) and on a draft component:Lightbox record that does not cover paging, so an engineering owner needs to approve or reject that direction, including its proposed amendments to architecture:theme-tokens, family:buttons, architecture:interaction-modality and spec:AST-025, by commenting /approve-spec <full head SHA> on this pull request (the author may do this as an engineering owner). Whatever that decision, these fixes are needed:

  • Make gallery pages that are not current inert and aria-hidden, since Tab can currently reach a link in the next item's caption, scrolling that item into view under the previous item's counter, and screen readers read the neighbours' captions.
  • Re-observe the zoom stage when the current page changes, because two items away from the opening one a rotation no longer re-fits a zoomed photo.
  • Stop usePager from reporting controlled index changes from an Effect (architecture:react-component-runtime INV2 and INV3) and return the track when a controlled index does not follow a swipe.
  • Land the stacked pull requests first so this one carries only its own change instead of about 31,000 lines.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Outdated
<div
key={i}
data-lightbox-page=""
data-current={i === index ? '' : undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pages that are not current stay in the tab order and the accessibility tree; only their <img> is hidden. With a four-item gallery open at 2 / 4, Tab from Previous reached a link in item 3's caption and the stage (overflow: hidden) scrolled 442 px, so item 3 showed under a 2 / 4 counter. Consider inert and aria-hidden="true" on every page without data-current.

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.

Right, off-screen pages stayed focusable and in the accessibility tree. Fixed in 2834675: every page but the current one is inert and aria-hidden. LightboxPaging checks the attributes and tabs through the dialog without ever entering an off-screen page.

});
observer.observe(stage);
return () => observer.disconnect();
// The stage element is stable for the dialog's lifetime.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In a gallery stageRef is pageMediaRef, which moves to a new element on every page, so this no longer holds: the observer stays on the opening page's box, and once that page unmounts (two items away) a rotation no longer re-fits a zoomed photo. Re-observe when the stage element changes, or observe the track, whose size every page shares.

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.

Right, the observer stayed on the opening page's box. Fixed in 2834675: the stage watches the track, whose size every page shares, and re-attaches whenever it turns on. LightboxPaging rotates two pages on, and the zoomed photo keeps its centre point.


useEffect(() => {
if (index != null && index !== pager.index) {
pager.goTo(index, {reason: 'controlled'});

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 Effect reports onIndexChange(index, 'controlled') through goTo and names no external system, which architecture:react-component-runtime INV2 and INV3 ask for. It also never returns the track when a controlled index refuses a swipe, because it runs only when index changes.

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 #8146 039d4e9, which e762b43 takes: a controlled move is the owner's own and is never reported, and a page the owner refuses returns the pager to index.

@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 on the draft touch-presentations spec (AST-094) that this pull request introduces as the authority for Lightbox swipe paging, including whether buttons and arrow keys keep sliding under Reduce Motion where its FR8 asks for a cross-fade. 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. Whatever the decision, these need fixing:

  • Pages beside the current photo stay focusable and exposed: Tab reaches photo 3's caption link and scrolls it into view under “2 / 4”.
  • A controlled parent that refuses a swipe is left with the swiped photo on screen.
  • Only the photo the dialog first rendered re-fits when a zoomed photo is rotated.
  • usePager reports controlled index changes from an Effect.
  • The diff carries #8045, #8146 and #8148 (31,552 authored lines); rebase or retarget it so it holds only the paging step.

[Automated review]

},
shouldBeginPan: direction => !stage.isZoomed() || stage.isAtEdge(direction),
onIndexChange: (next, reason) => {
if (reason === 'drag') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When index is controlled and the parent refuses the change, this drag report goes through setIndex, which only calls onIndexChange, and usePager re-syncs only when index changes, so the track stays on the swiped photo: after a swipe the viewer shows photo 3 under “2 / 4”. Return the pager to the controlled index when the parent keeps it, and add a test with a refusing parent.

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.

Right, the track stayed on the swiped photo under the old counter. Fixed in 2834675: a swipe's report renders the viewer once more, and when the parent kept its index the pager goes back to it. The new GalleryKeepsIndex story and LightboxPaging cover it (it stayed on the swiped photo before).

@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 is waiting on an engineering owner's decision about Lightbox swipe paging and the draft spec:AST-094 that would govern it: an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so if they are an engineering owner), or paging waits for a current Lightbox spec. Whatever that decision, these need changes:

  • Make the off-screen gallery pages inert, so screen readers and Tab no longer reach captions and controls that are not on screen.
  • Move the pager back to index when a controlled parent refuses a swipe, and stop calling onIndexChange from the controlled-sync Effect.
  • Report Lightbox's base-against-head gzip size (the import grows by about 31 KB gzip) and load the touch-only code lazily or justify it.
  • Re-observe the zoom stage when the current page changes, regenerate the CLI docs golden that fails test-node (1/4), and rebase onto the stacked pull requests so this one carries only the paging commit.

[Automated review]

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

  • Reduced motion. core/src/hooks/usePager.ts:353 handles reduced motion locally. Per design:reduced-motion (DR1–DR4), the animator is the one place that resolves it (touch cross-fades in 200 ms or less with no travel, a fine pointer changes at once), and core has no opt-out, so reducedMotion: 'keep' should be removed.

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.

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in 2834675, on #8146 @ f6b1827, which has no 'keep', and #8320 @ 41b46ea. The gallery passes no reduced-motion option of its own.

@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 is waiting on an engineering owner's decision about Lightbox swipe paging and the draft touch-presentations spec (AST-094) proposed to govern it: an engineering owner either approves that spec and the paging direction by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or paging waits for a current Lightbox spec. That decision has to settle AST-094's requirements, including FR4's rule that touch motion is not themable while this branch adds theme motion roles the pager reads, and how callers keep the fine-pointer behaviour on a touch device. Whatever the decision, these need changes:

  • Pause a gallery video when the viewer moves away from it; today a video the person started keeps playing, unseen and unreachable, on the neighbouring page.
  • Split the UIKit gesture table so Lightbox ships only the rows it reads, and report base and head sizes with the CSS delta, loading the touch-only code lazily or justifying its cost.
  • Remove the platform adaptation axis from the theme template, since defineTheme rejects it.
  • Rebase onto the stacked pull requests so this one carries only the paging commit, and split out the astryxStylex undefinedVariables option.

[Automated review]

Comment on lines +754 to +755
autoPlay={isCurrent && hasAutoPlay}
preload={isCurrent ? 'auto' : 'metadata'}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When this page stops being current only autoPlay and preload change, and neither pauses a playing video. The page stays mounted under key i as the previous neighbour, inert, so a video the person started keeps playing with its sound until they move two items away and the page unmounts. Consider pausing every non-current page's video when the index changes, with a paging test that plays a video.

Comment thread packages/cli/assets/theme.template.ts Outdated
Comment on lines +361 to +362
* are primary-pointer precision, contrast preference, motion preference, and
* platform (`ios` for the browsers of iPhone and iPad, `other` for the rest).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

defineTheme no longer accepts a platform condition: themeAdaptations.ts admits width, pointer, contrast and motion, and any other key throws is not supported. A theme author following this sentence gets a build error; consider restoring the base wording without platform.

* UIKit's gesture thresholds and velocity filter, each with its source.
* @experimental
*/
export const UIKIT_GESTURES = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lightbox imports recognizers that read this one object, so the swipe, rotation and screenEdgePan rows and every source string ship with every Lightbox import, which component-style-authoring INV6 rules out for tuning tables. Consider one export per recognizer's rows, with the provenance strings kept out of the runtime values.

vjeux added 8 commits October 27, 2026 16:13
…interaction harness's busy-window control) into meta#8148
…base of meta#8147

A busy-window proof can judge a component against a compositor-only control
animation recorded in the same window (startBusyControl, CONTROL_RGB,
busyWindowProof's control option), instead of a frame-rate bound that a
two-core runner's software compositor cannot meet.
…positor-only control

On a two-core runner the software compositor and the screencast share the
core the blocked main thread spins on, so every compositor animation
presents fewer frames there: the proof's frame-rate bound failed 7 runs in
10 pinned to two cores (the pr-interaction red on meta#8215). It now records
the interaction harness's control animation in the same window and passes
when the photo keeps pace with it (busyWindowProof's verdict); a
main-thread animation still fails.
…ainst a compositor-only control

The proof tracked the gallery track, which is wider than the screen, so the
tracked box stayed pinned to the screen's edge and read as settled 7 ms into
the block, though the settle ran another 275 ms: its frame-rate checks
passed on a window too short to judge. It now tracks the next photo as it
slides in, and judges it against the interaction harness's control
animation recorded in the same window, which also fails a window too short
to judge. Red: on the old marker the control verdict reads "too little to
judge"; green in both engines.
…ver pooled runs

A fade that lasts a fraction of a second leaves one blocked window only a few frames: in WebKit, or on a loaded machine, the control can present fewer than the four one window needs, and the proof fails as too little to judge, saying nothing about the animation. pooledBusyProof(runs) judges several runs of the same short animation as one long window is judged against the control: the control must move on at least four frames over all the runs, the component on 90% of them (one miss allowed), and in each run its longest stall may be at most one interval longer than that run's control's, since a stall belongs to its run. Red first: a 30 ms judged window holds at most three frames and one run fails as too little to judge; pooled over six runs, a compositor animation passes and a main-thread one fails, in Chromium and WebKit.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 28, 2026

@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 is waiting on an engineering owner to provide or identify the governing spec for Lightbox swipe paging and touch zoom, and to decide the new architecture:layer-runtime INV13 this branch adds: the Lightbox record is still a draft, the gestures here attach only under a touch pointer while spec:AST-094 FR19 and FR43 now attach gestures for every pointer, and spec:AST-017 FR59 asks for a separate decision on the new fine-pointer gallery layout. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner); it needs to settle whether the gestures stay touch-and-pen-only, whether the every-pointer caption layout stays, which paging motion roles the Lightbox record names, and whether INV13 belongs here. Whatever the decision, please address these:

  • With a mouse, keep controlled index changes instant: today an app-driven change slides over 300 ms, and a Next the parent refuses shows the next photo, then slides back.
  • Drop the hidden copies of other items' captions that reserve the caption row (architecture:component-style-authoring INV7), and give element counts for the added pages.
  • Report Lightbox's size against the base (CI measures +40,086 bytes gzip on the import) and justify the touch-only cost.
  • Get main's touch-module bundle check passing, and rebase onto the landed stack so this pull request carries only the paging change, with the lift module, exit copy and INV13 in their own pull request.

[Automated review]

// is not reported back. Idempotent: it moves only while the two differ.
useEffect(() => {
if (index != null && index !== pager.index) {
pager.goTo(index, {reason: 'controlled'});

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 a mouse this controlled sync still animates: goTo defaults to animated: true, so an app-driven index change while the viewer is open slides over 300 ms, and a Next the parent refuses slides back after Lightbox has jumped the pager forward. spec:AST-017 FR59 keeps fine-pointer behavior as it was; consider moving without animation unless the touch presentation applies.

if (!hasZoom || isVideo || next === zoom) {
return;
if (!isTouch) {
pager.goTo(next, {animated: false});

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 pre-move jumps the pager before the owner answers, so a controlled parent that keeps its index sees the next photo at once and then a 300 ms slide back from the controlled sync. Moving the pager only once the owner takes the change keeps a refused Next invisible, as on main.

Comment on lines +1028 to +1045
<div
aria-hidden
inert
{...stylex.props(
styles.captionStacked,
styles.captionSizer,
)}>
{unrenderedCaptions.map(j => (
<div
key={j}
{...stylex.props(
styles.caption,
styles.captionStacked,
)}>
{mediaArray[j].caption}
</div>
))}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These hidden copies of every caption no rendered page shows reserve the caption row, which architecture:component-style-authoring INV7 rules out ("Content renders once: no hidden twin to measure or reserve space"). A gallery of 50 captioned items mounts 47 of them, and any component in a caption runs once per item.

vjeux added 13 commits October 27, 2026 20:29
…ery pointer, wheel and trackpad scrolling; the harness's pooled busy proof) into meta#8147
…every pointer, wheel and trackpad scrolling; the harness's pooled busy proof) into meta#8148
…ckpad (spec:AST-094 FR43)

The zoom stage's pan, double tap, tap and catch attach for every pointer;
the pinch stays a finger's, and a ctrl-wheel reaches it through the
gesture core (meta#8676). A double-click is the stage's double tap (the zoom
core's own mouse path goes), a wheel pans a zoomed photo, a zoomed drag
rubber-bands at the edges and a released one coasts. The stage fills the
viewer for every pointer. A release follows the pointer that began it:
under reduced motion a finger's cross-fades (DR2) and a mouse's, a
trackpad's or a pen's completes at once on a fine primary pointer (DR3).

Tests: LightboxEveryPointer's zoom proofs (a ctrl-wheel zooms, a wheel
pans a zoomed photo, a released drag coasts); the fine-pointer and zoom
mode proofs that asserted the old mouse zoom flip to FR43's; the unit
tests compare a mouse's double-click with a finger's double tap. Docs in
English and Chinese; a patch changeset, and the touch zoom changeset no
longer says a mouse keeps the old zoom.

Red: under a mouse the double-click zoomed 2x about the centre, a ctrl-wheel did not zoom, a wheel did not pan a zoomed photo, a released drag did not coast and the stage did not fill the viewer; green after.
… of meta#8197

meta#8148 now carries the gesture core of meta#8676 and meta#8665, which attaches
recognizers for every pointer and reads the selection lock from the
pointer alone. Two adaptations follow: usePager calls locksSelectionFor
with the pointer only, and the bare pager's fine-pointer proof expects a
mouse drag to page (spec:AST-094 FR43).
… pointer) into meta#8197

The zoom stage keeps this PR's pan and catch conditions on meta#8148's
pointer lists, and the zoom core its surface and edge checks while its
mouse path goes. The stage's options and layout take meta#8148's ungated
forms; the touch presentation stays for the gallery's buttons and arrow
keys. The zoom target's fine-pointer proof lists the stage's claimed
touches.
…croll (spec:AST-094 FR43)

usePager attaches its pan and catch for every pointer instead of only
under a coarse primary pointer, and its pan takes a trackpad's or a
wheel's horizontal scroll through the gesture core. Lightbox's gallery
lets the mouse page too. A click beside a gallery photo with zoom on is
the stage's tap for every pointer, after the double-tap interval.

Tests: LightboxEveryPointer's paging proofs (a mouse drag sideways and a
trackpad's horizontal scroll page a gallery); the paging mode and
fine-pointer proofs and usePager's React test that asserted no input
under a fine pointer flip to FR43's. Docs in English and Chinese; the
every-pointer changeset gains paging, and the paging changeset no longer
says a mouse pages only with the buttons.

Red: under a mouse a sideways drag and a trackpad's horizontal scroll did not page, the pager attached nothing on a fine pointer and the gallery claimed no touches; green after.
…er (spec:AST-094 FR43)

The zoom stage's header, its enabled flag and the drag geometry still
spoke of fingers and pens only, of a mouse whose limits do not give, and
of a double-click on the released zoom; the hasZoom JSDoc still limited
the pinch and the double tap to a coarse pointer; the stage's styles
still said the touch layer ran on a touch screen only (FR19); and the
fine-pointer proofs' header still claimed touch work leaves that zoom
alone. They now say what the code does: every pointer reaches the
recognizers (the pinch is the fingers' and a ctrl-wheel's), the released
zoom is the keyboard's, and under Reduce Motion a finger's release
cross-fades while the others complete at once on a fine pointer.
Comments only.
…estures take every pointer) into meta#8197

The zoom stage's header keeps this PR's track and pager wording with
meta#8148's pointers, and the fine-pointer proofs' header adds this PR's
gallery to meta#8148's.
…very pointer (spec:AST-094 FR43)

The track's style still said the stage's touches went to it under the
touch layer only, the zoom stage's catch still spoke of a touch, and the
gallery's navigation comment tied the pager's slide to a finger, where
it follows the touch presentation. They now say what the code does.
Comments only.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
meta#8197 brings the zoom's and the pager's gestures for every pointer. The
drag to dismiss keeps its finger and pen pointer list (the zoom's shared
list is now every pointer's) and its touch-presentation gating until its
own change; its imports drop meta#8197's coarse-pointer read, and the
fine-pointer proofs keep meta#8197's gallery proof beside this PR's.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
…every pointer) into meta#8215

The zoom stage's header keeps this PR's dismissal after meta#8197's catch,
and the fine-pointer proofs' header adds this PR's photo without zoom to
meta#8197's.

@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 is waiting on an engineering owner to provide or identify the governing spec for Lightbox paging, its zoom gestures and the new gallery layout, and to decide the architecture:layer-runtime INV13 this branch adds: the Lightbox record is still a draft, spec:AST-094 leaves each component's touch presentation to its own record, and spec:AST-017 FR59 asks for a separate decision on the new fine-pointer layout. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). Whatever the decision, please address these:

  • With a mouse, keep controlled index changes instant: a Next the parent refuses shows the next photo and slides back, and an app-driven move slides.
  • Drop the hidden copies of other items' captions that reserve the caption row (architecture:component-style-authoring INV7), and give element counts for the added pages.
  • Get scripts/touch-support-bundle.test.mjs passing again: Core's root entry now bundles the gesture modules through Lightbox.
  • Show fine-pointer before-and-after for the gallery layout, and rebase onto the landed stack so this pull request carries only the paging change, with the lift module, exit copy and INV13 in their own pull request.

[Automated review]

// is not reported back. Idempotent: it moves only while the two differ.
useEffect(() => {
if (index != null && index !== pager.index) {
pager.goTo(index, {reason: 'controlled'});

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 a mouse this still slides: goTo defaults to animated: true (:1023), so a Next the parent refuses shows the next photo and slides back over 300 ms, and an app-driven index change slides where main swaps the photo. spec:AST-017 FR59 keeps fine-pointer behavior as it was; make a controlled move instant under a fine pointer and add a browser test for both cases.

{...stylex.props(styles.pageMedia)}>
{renderMedia(mediaArray[i], i === index)}
</div>
{i === index && unrenderedCaptions.length > 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.

These hidden copies of every caption no rendered page shows exist only to reserve the caption row. architecture:component-style-authoring INV7 says "Content renders once: no hidden twin to measure or reserve space"; size the row with CSS on the captions that are shown and drop the sizer.

This branch was successfully deployed

1 active deployment
Preview — 2a1354c6 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants