Skip to content

feat(Lightbox): a gallery's caption sits in a two-line band - #9810

Draft
vjeux wants to merge 33 commits into
meta:mainfrom
vjeux:uikit/c24-caption-row
Draft

vjeux wants to merge 33 commits into
meta:mainfrom
vjeux:uikit/c24-caption-row

Conversation

@vjeux

@vjeux vjeux commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8197 → #9810

Stacked on #8197's 3963547, on main 993e1fa. This head, 9572a26, merges #8197's new head into e48e3e0. One conflict, in Lightbox.tsx: this pull request indents the gallery's track one level deeper, and #8197 gives each page its index there. The resolution keeps both. Before it, e48e3e0 answers CodeBunny's 04:34Z review: a caption delivery reads the caption first and commits once, vjeux@e48e3e0. The two commits before it answer the 03:52Z review, where the caption toggle takes its width from its own ResizeObserver delivery, and the 01:37Z review: the caption is measured in its ResizeObserver's deliveries, after layout, instead of in React's commit. Their base, 7968e0a, merges #8197's head into the previous head 92d50bf with no conflict, and this pull request's own diff is otherwise unchanged. Review this pull request's six commits: the caption band, vjeux@0fc994c, the answers to CodeBunny's 10:27Z review, vjeux@c83f03d, the restored description, vjeux@92d50bf, the measurement after layout, vjeux@218f7b4, the toggle's width from its delivery, vjeux@ac1101e, and one read and one commit per delivery, vjeux@e48e3e0

The design owner's option C for a gallery's captions (card 46): a constant photo area; the caption in a fixed two-line band; a longer caption clamps, and "more" opens the full caption over the bottom of the photo on a scrim; the full caption is always in the accessible description; no hidden sizing copies.

It answers the design owner's re-check (card 46C (Cindy)): the caption sat too close to the bottom, its second line was indented, its size was unusual, and the Close button showed an outline on a tap's opening. The caption must also meet 4.5:1 over a mid-grey photo (Specs decision, Nov 5 2026). Lightbox renders through Astryx parts on a media surface:

  • The viewer's content is inside MediaTheme (dark), so its controls, caption and counter take the theme's dark surface tokens and its rules for them. No Lightbox color is left beyond the controls' and caption's backing and the on-dark color (AR9).
  • The caption and the counter are Text in the body type, so both caption lines start at one edge. Owner decision, Nov 5 2026 (Cindy): body text.
  • A page with no caption shows no band and no backing (card 46D), and its photo stays put: in a gallery with any caption, every page reserves the band's space, transparent where the page has no caption, so every page's photo box, and its top, is the same.
  • "More" and "less" are a ghost Button one caption line tall, at the end of the caption's last line, so the caption keeps the band's whole width and the clamp's "…" comes just before "more" (card 46D). Its hit area reaches out toward 44 px under a finger, sideways and down, never into the line above. An open caption that scrolls in its panel keeps the toggle's width clear at the end of every line.
  • The caption's backing is dark enough for 4.5:1 over a mid-grey photo in every arm of docs(Lightbox): caption type comparison story for review (not for merge) #10084: the body caption measures 17.2:1 there.
  • The band's padding clears the screen's bottom edge, plus the safe area there.
  • Focus the viewer moves follows the last input in the viewer's own document, a Picture-in-Picture window's included. So a tap's opening paints no ring on Close, and a key's still does.
  • In a gallery, the keyboard's zoom button is the current page's photo box, so the caption band and "more" sit outside it, and Enter or Space on "more" toggles the caption. Visible difference: a keyboard user's zoom focus ring outlines the photo box, not the whole viewer area.
  • Paging away from "more", or from the focused photo, by a key, a button or a swipe, moves focus to the new page's photo, or to Close, through Core's shared focus-return helpers, so the next arrow still pages. "Less" keeps its focus. Each page renders its caption once: "more" opens the same element in place.
  • Closing the viewer returns focus to its opener through Core's shared return, which never scrolls the page.

This round answers the review on the rebased head: the toggle no longer sits inside the zoom button, "less" keeps its focus, and FR17 names the caption's backing. Out of scope: the backdrop keeps the page theme's --color-overlay and blur, as today. ::backdrop inherits from the dialog element, outside the media surface.

User impact

In a Lightbox gallery, every page has a caption band two caption lines tall under its photo, whatever its caption. So the photo area is the same on every page, and paging never resizes it. A caption longer than two lines shows two lines and a "more" button. "More" opens the whole caption over the bottom of the photo, on a dark backing, and "less" or paging closes it. Under a finger the button takes taps out toward 44 px, and its tap is its own, not the photo's. A screen reader gets the whole caption as the viewer's description, clamped or open.

Contrast

Measured on #10084's exact preview (4c07f5c, stacked on this pull request) in Chromium: default theme, a 402 px phone viewport, coarse pointer, text against what lies under it:

Caption In the band Open over a white photo Over a mid-grey photo (rgb 127) Over a black photo
Body (16 px, on-dark) 17.2:1 14.7:1 17.2:1 19.2:1
Supporting (14 px, secondary) 6.4:1 5.5:1 6.4:1 7.2:1
Supporting, primary color 17.2:1 14.7:1 17.2:1 19.2:1

Across the shipped themes (butter, chocolate, gothic, matcha, stone), body stays at or above 7.3:1, matcha the lowest. Supporting in its secondary color falls to 2.7:1 in matcha and 3.8:1 in chocolate over a white photo; the other themes give 5.0–5.6:1.

Record change

  • component:Lightbox FR1 gains: "In a gallery, Caption sits in a band two caption lines tall under every page's Media. A longer caption shows two lines and a "more" button that opens it in full over the bottom of the Media, on a scrim. The full caption is always the viewer's accessible description. The caption band uses Astryx parts: the caption text is Text with the body type, and "more"/"less" is a ghost Button, not hand-styled elements."
  • New FR17: "The viewer is a media surface: it renders inside MediaTheme (dark), so its controls and caption take the theme's dark surface tokens and the active theme's component rules (architecture:theme-application INV8), with no Lightbox-owned colors beyond AR9's backing, the caption's backing and the on-dark color." Its basis reads "Specs decision, Nov 5 2026". Its verification row names the unit file and the browser interaction spec, and states the caption's contrast over a white, a mid-grey and a black photo (body 14.7:1 over white).
  • spec:AST-038's existing-code row now reads: "Lightbox's caption renders through Text under MediaTheme (component:Lightbox)", with the rest of the row unchanged.

Writing rubric results

  • R1 narration: none. The new sentences state present behavior. The body type stands until the owner's pick on the 46C re-check; if the pick is supporting, FR1 and the code change together before merge.
  • R2 research in the record: one measured value, FR17's verification row's "body 14.7:1 over white". The design owner's side asked for the shipped type's ratio in that row, so it stays.
  • R3 evidence naming a change: none.
  • R4 rejected alternatives: none in the record.
  • R5 pull request or issue references: none in the record.
  • R6 answered open questions still listing options: none.

Visual authority

  • Existing component/module DD row reused or amended.
  • Durable component-local intent added to the optional Design decisions table by a team design owner.
  • Correction already settled by current family, design, theme, or objective accessibility authority.
  • New shared or cross-component direction settled in docs/design/.

The design owner chose option C on card 46 and asked for these fixes on card 46C; the record states them in FR1 and FR17.

How to check

  1. Open "Gallery with captions" on a phone. The first caption shows two lines at the body size, both starting at one edge, with "more" after the last line. Its band sits clear of the screen's bottom edge.
  2. Swipe through the four photos. The photo keeps one size on every page, and the band stays two lines tall. The third photo has no caption.
  3. On the first photo, tap "more". The whole caption opens over the bottom of the photo, on a dark backing, readable over the photo's light parts. Tap "less", or swipe to the next photo, to close it.
  4. Close the viewer and open it again with a tap: Close shows no outline. Open it from the keyboard: Close shows its focus ring.
  5. With VoiceOver on, the viewer's description reads the whole first caption.

State matrix

  • Captions: none, short, about two lines, longer than two lines. A page with no caption keeps the band's height empty, once any item in the gallery has a caption. A gallery with no captions has no band.
  • Pointers: a finger (the toggle's own tap, its 44 px target) and a mouse; both open and close the full caption, and the viewer stays open.
  • Opening: by a tap (no ring on Close) and by a key (Close's ring).
  • An open full caption too long for its panel (60 % of the viewport's height) scrolls under a finger.
  • Paging closes an open full caption and moves focus off "more".

Minimal DOM

Element counts for the current page's caption, measured in Chromium on Storybook builds. Main renders a gallery's caption as one Text under the photo, with no clamp and no toggle.

State main c56f4e1 this head
A caption that fits its two lines 1 4 7
A clamped caption, "more" n/a 12 11
The open caption, "less" n/a 12 11
A page with no caption, in a gallery with captions 0 1 1

Every captioned page renders the same elements in every state. The toggle is the only element that comes and goes, and only while the caption runs past two lines. Each element's job:

  1. The band: the page's caption row. It keeps the same space on every page, so every photo box matches, and it carries the backing.
  2. The caption panel: the element "more" opens over the photo, and the target of the toggle's aria-controls.
  3. The caption body: the toggle's containing block. It does not scroll with an open caption's lines, so the toggle stays in view.
  4. The caption Text: the caption itself, also the viewer's description.
  5. The line block: a block inside the clamp's box, so the clamp counts its lines and its floats wrap them. WebKit's clamp box ignores floats.
  6. and 7. Two floats: the toggle's place at the end of the last line. Without a toggle they have no size.
  7. The toggle: a ghost Button (4 elements with its label), only while the caption runs past two lines. It hosts its own position, so the slot that wrapped it is gone.

Testing

  • At this head (9572a26), on feat(Lightbox): swipe between gallery photos like Photos #8197's 3963547: Text and Lightbox units 196/196; core tsc 0. Lightbox a11y specs: 41/41 in Chromium, 39 in WebKit (2 skips). Lightbox interaction specs: 90 in Chromium (3 skips) and 84 in WebKit in a 4-CPU container, the CI profile (9 skips). feat(Lightbox): swipe between gallery photos like Photos #8197's "a zoomed photo keeps the drag until its edge" passes 5 of 5 there with 6 busy threads.

  • At this head (e48e3e0), CodeBunny's 04:34Z review (vjeux@e48e3e0):

    • Opening or closing the caption registered the band again behind the toggle. The new label resized the toggle in the same frame, so one ResizeObserver callback ran the toggle's width commit first and the band's caption read after that write, which forced a layout.
    • Now the band's and the toggle's callbacks only note what changed. One microtask after the observer's callback reads the caption and commits once, before the frame paints. The band no longer registers again when the caption opens or closes, since its box resizes then anyway.
    • Red first: 'no caption read follows a write in the same resize delivery, from opening through "more", "less" and paging' marks layout-affecting writes inside a resize delivery and its microtasks, and logs every caption geometry read after one. It fails 3 of 3 on ac1101e's build.
    • At this head: Text and Lightbox units 185/185; core tsc 0. Lightbox a11y specs: 41/41 in Chromium, 39 in WebKit (2 skips). Lightbox interaction specs: 90 in Chromium (3 skips) and 83 in WebKit in a 4-CPU container, the CI profile (9 skips), with the one failure main has too, LightboxZoomModes' Reduce Motion "+ then −" proof, which test(Lightbox): the Reduce Motion +/- proof freezes on the fade's dip #10446 fixes.
  • At this head (ac1101e), CodeBunny's 03:52Z review (vjeux@ac1101e):

    • Paging back onto a clamped caption delivered the band's entry and the new toggle's first entry in one ResizeObserver callback, band first. The band's update showed the toggle, and the toggle's callback then read its width after that write, which forced a layout. The width now comes from the delivery's own border-box size, and a hidden toggle's zero size keeps the last width.
    • Red first: "the caption toggle's width comes from its own delivery, never from a read of the toggle" logs every read of the toggle's width inside a resize delivery while paging back, and fails 3 of 3 on 218f7b4's build.
    • At this head: Text and Lightbox units 185/185; core tsc 0. Lightbox a11y specs: 41/41 in Chromium, 39 in WebKit (2 skips). Lightbox interaction specs: 89 in Chromium (3 skips) and 82 in WebKit in a 4-CPU container, the CI profile (9 skips), with the one failure main has too, LightboxZoomModes' Reduce Motion "+ then −" proof, which test(Lightbox): the Reduce Motion +/- proof freezes on the fade's dip #10446 fixes.
  • At this head (218f7b4), CodeBunny's 01:37Z review (vjeux@218f7b4):

    • The caption band and its toggle no longer read layout in React's commit, where each read forced a layout before the browser's own. Their sizes come from their ResizeObserver's deliveries, which follow layout. The update commits before the frame paints, so the first frame that shows a clamped caption still ends it in "more".
    • The band's observer watches the caption's box. The toggle stays mounted, hidden until the caption is clamped, so a delivery's commit changes attributes only and adds no element. The held truncation reads nothing by itself; Lightbox measures it.
    • Red first: "the caption is measured after layout, never in the task that opens the gallery or pages it" (402 × 874 and 1440 × 900) logs every caption geometry read inside the input's task, and fails on 92d50bf's build. "Opening a gallery on a clamped caption lays out no more than opening one without captions" counts 2 layouts against 1 in a Chromium trace there. "The caption opens, closes and pages with no ResizeObserver loop error", in the Storybook preview, fails on a build that added the toggle inside the delivery. The unit "reads nothing by itself: its caller measures" fails on the old useTruncation.
    • The first-frame proof now reads each frame after its ResizeObserver deliveries, which run after the frame's callbacks and before it paints. Without flushSync, it fails 3 of 3 in each engine.
    • At this head on main 84a2254: Text and Lightbox units 185/185; core tsc 0. Lightbox a11y specs: 41/41 in Chromium, 39 in WebKit (2 skips). Lightbox interaction specs: 88 in Chromium (3 skips) and 81 in WebKit in a 4-CPU container, the CI profile (9 skips), with one failure that main has too: LightboxZoomModes' Reduce Motion "+ then −" proof fails 5 of 30 on main 84a2254 and 7 of 30 here, against 0 of 40 on main a888b3d. test(Lightbox): the Reduce Motion +/- proof freezes on the fade's dip #10446 is its test-only fix.
  • At this head (ced33fe): the same own diff as c83f03d (patch-id unchanged), merged onto feat(Lightbox): swipe between gallery photos like Photos #8197's 3ee5207 without conflict.

  • At this head (c83f03d), CodeBunny's 10:27Z items and two pre-review follow-ups (vjeux@c83f03d):

    • A gallery caption whose text grows while its box keeps two lines now gets "more". The clamp comes from Text's own truncation check, through useHeldTruncation, a variant of useTruncation internal to core. It keeps its answer while the caption is open, so the toggle stays mounted, with its focus, as the caption closes. useTruncation's public options don't change, and plain useTruncation runs no new effect, so Text and Heading pay nothing new.
    • A closed dialog lays its caption out at no size, so Lightbox measures the caption again in the commit that opens the dialog, before the first paint. The first frame that shows a clamped caption already ends it in "more".
    • The changeset says that a gallery's caption keeps 4.5:1. A single photo's caption has no backing and measures 4.00:1 over a white page, on main too, so the record lists it as a gap in FR17, and fix(Lightbox): a single photo's caption sits on the caption's backing #10284 gives it the band's backing.
    • Red first: "a caption whose text grows while its box keeps two lines gets "more" at the end of its second line" fails on 0fc994c's build. "The first frame that shows the clamped caption at open already ends it in "more"" records every frame from before the tap, and fails 3/3 in Chromium and 3/3 in WebKit without the opening measure. The unit "adds a layout effect only to the held variant, never to useTruncation" fails with a useTruncation that adds one.
    • At this head on main bef866e: pnpm install --frozen-lockfile 0; Lightbox, Text and Heading units 476/476; pnpm check:knowledge and pnpm check:changesets pass; core tsc 0; strict ESLint 0; comment sentence lengths 0 over. Leak and public-text checks clean.
    • Pixel diff against a build of 0fc994c's own lines on main 2151c88: pages 1 to 4 of "Gallery with captions", with page 1 clamped and open, at 1440 × 900, 402 × 874 and 360 × 740, left to right and right to left. 60 of 60 screenshots are identical (30 Chromium, 30 WebKit).
    • On a Storybook build of the same commit on main 2151c88 (3e2d912; main has changed none of these files since): Chromium Lightbox a11y specs 40/40, WebKit 19/19; Chromium Lightbox Paging and EveryPointer interaction specs 40 passed (3 WebKit-only skips); WebKit 37 passed, and LightboxPaging "the page settle keeps advancing every frame while the main thread is blocked" fails as on 0fc994c: feat(Lightbox): swipe between gallery photos like Photos #8197's tempo commit fixes it below this pull request.
  • Red first:

    • The media surface unit cases fail on ded4664's product.
    • The Picture-in-Picture ring case fails on 01bc90d's product, before the viewer tracked its own document's input: "expected undefined to deeply equal { focusVisible: false }".
    • On a Storybook build of 4dded66 with the fix round's specs: the caption's contrast case measures 4.00:1, under 4.5; the arrow key on "more", the swipe from "more", the caption rendered once, and the on-dark color cases fail.
    • On a Storybook build of 5a2ba52: a button sits inside the zoom button (1, expected 0); Enter on "more" zooms the photo instead of opening the caption; after "less" the toggle loses focus. The case that pages from a focused photo passes on both builds and guards the new zoom button's focus.
  • At this head (0fc994c): Lightbox units 139/139; core tsc 0, strict ESLint 0, comment sentence lengths 0 over. On a Storybook build of this head: Chromium Lightbox a11y specs 38/38; WebKit LightboxViewport and LightboxKeyboard 17/17; Lightbox Paging, Zoom and EveryPointer specs 78 passed in Chromium (3 WebKit-only skips); WebKit Paging, EveryPointer and Keyboard 38 passed (5 skips).

    • Red first: "renders the same caption elements with or without its toggle, clamped or open" fails on c56f4e1's Lightbox.tsx (the clamped caption is DIV(DIV(SPAN(),SPAN())), the one that fits DIV()) and passes here.
    • Pixel diff against c56f4e1's build: 60 of 60 screenshots are identical, byte for byte, in Chromium and WebKit, at 1440 × 900, 402 × 874 and 360 × 740, left to right and right to left. The states: "more" collapsed, the caption open, two lines that fit, no caption, and one short line.
  • At c56f4e1: Lightbox units 138/138; core tsc 0, strict ESLint 0, comment sentence lengths 0 over, Prettier clean; pnpm check:knowledge and check:repo pass. On a Storybook build of this head: Chromium Lightbox a11y specs 38/38; Lightbox Paging, Zoom and EveryPointer specs 55 passed in Chromium (3 WebKit-only skips); WebKit Paging and EveryPointer plus the LightboxViewport caption cases 34 passed (5 skips).

    • Red first, LightboxViewport.a11y.browser.spec.ts: "a gallery page without a caption shows no caption backing, and every page's photo keeps its top" and "a clamped caption's "more" ends its second line, with no column of its own" (402 × 874 and 1440 × 900), and "… still ends its second line after the viewport narrows (402 → 360)" fail on 053f321's build and pass here. "An open caption scrolled to its end keeps every line beside "less" clear of it" (360 × 150, 390 × 150) fails on a build that keeps the inline place while the caption scrolls, and passes here. The unit case "follows the caption in reading order, and its place in the last line is hidden from assistive technology" fails on 053f321's product.
  • iPhone 17 Pro simulator (iOS 26.2, Safari), on c56f4e1's Storybook build, every frame viewed: every page's photo top is 189 pt; page 1's two lines end in "…" and "more" (its last line ends at 318 pt, "more" sits at 328–390 pt on that line); page 2's two lines fit with no toggle; page 3 has no caption and no backing (rgba(0, 0, 0, 0)); page 4's one line sits on its backing.

    pages 1 to 4 on the iOS 26.2 simulator

  • c56f4e1's preview on the iOS 26.2 simulator (Safari, every frame viewed), pages 1 to 4:

    pages 1 to 4 of the exact-head preview on the iOS 26.2 simulator

  • Desktop, Chromium at 1440 × 900. Before (main, which has no captioned gallery story: "Gallery" page 1 and its captionless page 2, and "With caption" for a long caption):

    main: a short caption under the photo main: a page with no caption main: a long caption on one line

    After (c56f4e1, "Gallery with captions"): "more" collapsed, the same caption expanded, the captionless page, and a short caption:

    a two-line caption ending in "…" and "more" the caption open over the photo, with "less" a page with no caption: no band a short caption on its backing

  • At 053f321: core tsc 0, strict ESLint 0, comment sentence lengths 0 over, Lightbox units 137/137. Red first: the new case "returns focus to the opener without scrolling the page when the viewer closes" fails on 57425e7's product, whose close focused the opener without preventScroll. On a Storybook build of this head: Chromium Lightbox a11y specs 31/31; Paging and EveryPointer 28 passed in Chromium (3 WebKit-only skips); Paging, EveryPointer and Keyboard 26 passed in WebKit (5 skips); the a11y audit finds 0 new violations; pnpm check:knowledge and check:repo pass.

  • At 57425e7 (053f321 without the shared focus return): the LightboxKeyboard spec 9/9 in Chromium; Lightbox Paging and EveryPointer specs 28 passed in Chromium (3 WebKit-only skips), and Paging, EveryPointer and Keyboard 26 passed in WebKit (5 skips).

  • At 40e8001, the same behavior before the focus handoff moved to the shared helpers (the head also differs by the FR17 basis cell and two comments):

    • core tsc 0; strict ESLint on every changed file 0; comment sentence lengths 0 over; pnpm check:knowledge and check:repo pass (check:changesets, check:i18n-catalog, check:i18n-runtime, check:use-client and check:package-boundaries at 65534b6, the same code without the viewport hook).
    • Lightbox units 136/136.
    • On a Storybook build: Chromium Lightbox interaction specs 78 passed (3 WebKit-only skips); WebKit 73 passed (8 skips); Chromium Lightbox a11y specs 31/31; the LightboxKeyboard spec in WebKit 9/9; the a11y audit finds 0 new violations.
  • Existing cases: the fine-pointer touch-action lists gain the media surface's auto; the scroll case's viewport is 240 px tall so the body-size caption still overflows its panel; the paging specs find the pager's viewport by data-lightbox-viewport, since the zoom button is now the photo box.

  • iPhone 17 Pro simulator (iOS 26.2, Safari), on 4dded66's build, every frame viewed: a tap's opening leaves Close without a ring, and a key's opening shows it; the caption is body Text (16 px in the default theme), its lines start at one edge, and the band keeps 12 px under them.

@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 7, 2026 6:18am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 3, 2026
@astracat-bot

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

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

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Nov 3, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs an owner decision before it can be approved, and it has independent fixes to make either way.

Owner decision: no Lightbox requirement yet covers gallery pages sharing one caption row, and this change pursues that direction while the design decision is open. The head also adds new requirements to the Lightbox record: AR9, the controls' backing and contrast with zoom on; and the zoom-settle formula with its motion roles. Approval needs a spec-gate owner to settle these: either approve the shared caption row and how it reserves its height (which today conflicts with the style-authoring rule that content renders once), or keep each caption under its own photo. An engineering owner records the decision by commenting /approve-spec <full head SHA>, and the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden copies of the unmounted pages' captions to reserve its height, which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • Against main the diff is 8,108 authored lines, because the three stacked pull requests ride along with this 250-line change, so land them first or retarget so this pull request carries only its own commit.
  • In a gallery with hasZoom, a click or tap on the reserved caption band under a photo without a caption no longer closes the viewer, while main and a gallery without zoom close it (useZoomStage.ts, the stage's tap only counts points inside the stage).
  • .changeset/lightbox-gallery-paging.md still says each caption sits "right under its photo", but it now sits at the bottom of the stage, 252 px below a landscape photo on a phone.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.

[Automated review]

Comment thread .changeset/lightbox-gallery-paging.md Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs an owner decision before it can be approved, and it has independent fixes to make either way.

Owner decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head also adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on) and the zoom-settle formula with its motion roles. A spec-gate owner needs to settle these: approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo. An engineering owner records the decision by commenting /approve-spec <full head SHA>, and the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden copies of the unmounted pages' captions to reserve its height (Lightbox.tsx, the caption sizer), which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • Desktop behavior change needs an owner decision: a mouse drag or a trackpad's sideways scroll now pages every gallery, with default props (Lightbox.tsx, the pager's allowedPointerTypes), so gate it to touch input or cite the decision that covers it.
  • Against main the diff is 8,175 authored lines, because the three pull requests below ride along with this 294-line change, and their bodies carry a different stack line, so land them first or make the stack declaration identical in all four.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs a spec owner's decision before it can be approved, and it has independent fixes to make either way.

Spec decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on), the zoom-settle formula, and its motion roles. A spec-gate owner needs to approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo, and approve or reject the new record text. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden, inert copies of the unrendered pages' captions to reserve its height (Lightbox.tsx, the caption sizer), which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • Desktop behavior change needs an owner decision: with default props a mouse drag or a trackpad's sideways scroll now pages every gallery (Lightbox.tsx, the pager's allowedPointerTypes), and with zoom a ctrl-wheel zooms the photo and a released mouse drag coasts (useZoomStage.ts), so gate these to touch input or cite the decision that covers them.
  • Against main the diff is 8,172 authored lines, because the three pull requests below ride along with this 294-line change and the stack line is not identical in every listed pull request, so land them first or make the stack declaration hold.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs a spec owner's decision before it can be approved, and it has independent fixes to make either way.

Spec decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on), the zoom-settle formula and its motion roles. A spec-gate owner needs to approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo, and approve or reject the new record text. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking findings:

  • The caption row renders hidden, inert copies of the unrendered pages' captions only to reserve its height (Lightbox.tsx, the caption sizer), which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.
  • Against main the diff is 8,442 authored lines, because the three pull requests below ride along with this 294-line change and #8215 does not contain #8197's head, so land the lower pull requests first or make every listed head contain the one below it.
  • Regression: the Chinese hasZoom description still says every zoomed drag pans with momentum, while a mouse drag now stops at the release, so narrow it as the English description was narrowed.

Drag paging is now limited to touch input, so a mouse and a trackpad page a gallery as they do on main.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request needs a spec owner's decision before it can be approved, and it has independent fixes to make either way.

Spec decision: no Lightbox requirement covers gallery pages sharing one caption row, and the head adds new requirements to the Lightbox record: AR9 (the controls' backing and contrast with zoom on), the zoom-settle formula and its motion roles, beside unrecorded video and index behaviours. A spec-gate owner needs to approve the shared caption row and how it reserves its height, which today conflicts with the style-authoring rule that content renders once, or keep each caption under its own photo, and approve or reject the new record text. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Blocking findings:

  • In a captioned gallery without zoom, a tap or a mouse click on the dark band between a photo and its caption, which now sits in the bottom row, leaves the viewer open, while main and a zoomable gallery close on it, because the hit test in Lightbox.tsx treats one box spanning photo and caption as the media.
  • The caption row renders hidden, inert copies of the unrendered pages' captions only to reserve its height, which the component style-authoring architecture's INV7 ("Content renders once") rules out unless an owner amends it.
  • The Lightbox record's FR5 requires the page settle to play named motion roles, and the head records it as playing the pager's own values instead, so register those values as roles or record an owner exception.
  • Against main the diff is 8,622 authored lines, because the three pull requests below ride along and #8215 does not contain #8197's head, so land the lower pull requests first or declare a stack line that every listed head satisfies.
  • Miss: the caption sizer picks captions with a truthiness test (item.caption && …) instead of isRenderable, so a 0 caption reserves no height and a true caption reserves an empty box.

The Chinese hasZoom description now matches the English one.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
The restack of meta#9810 on meta#8197's head e0eab2a, which merges main
again to keep meta#10406's lift record. No conflict; this pull request's
own diff is unchanged.
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 7, 2026
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec ec7c502

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 7, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: one blocking issue remains, and the record text is approved for this head.

  • Regression: the Stack: #8197 → #9810 → #10284 line no longer holds, because #10284's head does not contain this head, so this pull request is judged on its own and carries #8197's increment as 6,072 authored lines with no exemption; merge this head into #10284 so every listed head contains the one below.

Non-blocking, unchanged from the last round: FR17's verification row names a black photo that no contrast case runs, FR17's review state should read settled; implementation gap, and the GalleryKeepsIndex story has lost its description. CI has not run on this head, since every job skips while the pull request is a draft.

[Automated review]

A merge in this pull request dropped the JSDoc above the story, which
Storybook shows as its description. It comes back as on meta#8197.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 92d50bf

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: one blocking issue in #9810, and a pull request lands only when it and every pull request below it are approved.

#8197 — approve

Approve, with no blocking findings.

#9810 — request changes

  • Miss: opening a gallery on a caption longer than two lines makes the browser lay the viewer out again on the main thread (three forced layouts against main's one, one of them this pull request's toggle read), and paging onto or off such a caption forces layout at the two new reads, because the toggle's attach reads toggle.offsetWidth (Lightbox.tsx:937) and the band's attach runs the clamp check (Lightbox.tsx:910) right after the commit's writes and the task then writes and reads again, which the React runtime architecture's INV18 rules out; read both sizes in the observer deliveries instead, and keep the toggle's width across pages until its observer reports a change.

Non-blocking, unchanged: the black photo in FR17's verification row that no contrast case runs, FR17's review state, the long JSDoc in useZoomStage.ts, and the block inside a clamped Text.

Whole stack — fails

It fails on the finding above. CI has not run on this head because the pull request is a draft.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
vjeux added 3 commits November 6, 2026 18:24
Main's meta#10415 plays LightboxZoom's busy double-tap proof at tempo 5
through `slowZoom`, which this pull request replaced with the shared
`slowViewer`. The merge keeps meta#10415's three constants and calls
`slowViewer(page, ZOOM_TEMPO)` in the proof and its controls; every
other path is the automatic merge.
The restack of meta#9810 on meta#8197's head cc382a8, which merges main
again to port meta#10415's busy zoom tempo. No conflict; this pull
request's own diff is unchanged.
The caption band's attach read the clamp and the caption's lines in
React's commit, and the toggle's attach read its width there too, so
opening a gallery or paging forced layouts before the browser's own.
Both now read in their ResizeObserver's deliveries, which follow
layout, and commit before the frame paints, so "more" still shows from
the first frame. Such a commit only shows or hides the toggle, which
stays mounted, so it adds no element mid-delivery. The held truncation
reads nothing by itself: Lightbox measures it.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 218f7b4

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: one blocking issue in #9810; a pull request lands only when it and every pull request below it are approved.

#8197 — approve

Approve, with no blocking findings; its branch now conflicts with main in LightboxZoom.interaction.browser.spec.ts, which only #9810's merge resolves.

#9810 — request changes

  • Paging back onto a clamped caption (measured with the arrow key and with Previous) makes the browser lay the viewer out an extra time, because the new page's toggle reads toggle.offsetWidth (Lightbox.tsx:996) right after the band's flushSync un-hides it in the same ResizeObserver callback, which the React runtime architecture's INV18 rules out; take the width from the delivery's entry (borderBoxSize, skipping a zero size) and correct the comment at Lightbox.tsx:990-992.

Non-blocking, unchanged: the black photo in FR17's verification row that no contrast case runs, FR17's review state, the long JSDoc in useZoomStage.ts, and the block inside a clamped Text.

Whole stack — fails

It fails on the finding above, and CI has not run on this head because the pull request is a draft.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Paging back onto a clamped caption delivered the band's entry and the
toggle's first entry in one ResizeObserver callback, band first. The
band's update showed the toggle, and the toggle's callback then read
its offsetWidth after that write, which forced a layout. The width now
comes from the delivery's own border-box size. A hidden toggle's zero
size keeps the last width.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec ac1101e

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: one blocking issue in #9810; a pull request lands only when it and every pull request below it are approved.

#8197 — approve

Approve, with no blocking findings; its new head only merges main into one test, and it merges cleanly with today's main.

#9810 — request changes

  • Miss: each press of "more" or "less" still makes the browser lay the viewer out an extra time, because opening or closing the caption registers the band again behind the toggle (Lightbox.tsx:938-976), so the toggle's flushSync width commit (Lightbox.tsx:1001) runs first and the band's caption read (Lightbox.tsx:912 on "more", useTruncation.ts:154-163 on "less") follows that write in the same ResizeObserver callback, which the React runtime architecture's INV18 rules out; read the caption before any write in that delivery, for example with one listener for both entries or by not registering the band again when the caption opens or closes.

Non-blocking, unchanged: the black photo in FR17's verification row that no contrast case runs, FR17's review state, the long JSDoc in useZoomStage.ts, and the block inside a clamped Text.

Whole stack — fails

It fails on the finding above; CI has not run on this head because the pull request is a draft.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Opening or closing the caption registered the band again behind the
toggle. The new label resized the toggle in the same frame, so one
ResizeObserver callback ran the toggle's width commit first and the
band's caption read after that write, which forced a layout. Now both
callbacks only note what changed. One microtask after the callback
reads the caption and commits once, before the frame paints, and the
band no longer registers again when the caption opens or closes.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e48e3e0

vjeux added 3 commits November 6, 2026 21:28
The restack of meta#8197 on main 993e1fa, which brings meta#10436's
PowerSearch play fix for the story play guard. No conflict; this pull
request's own diff is unchanged.
The pager's onSettle reset every photo outside the page marked current.
On a slow runner the settle ends before the render that moves that
mark, so the photo left behind was still marked current and kept its
zoom. Each page now carries its index, and the reset skips only the
page the pager names.
The restack of meta#9810 on meta#8197's head 3963547, which merges main
and resets the photo left behind by the page the pager names. One
conflict, in Lightbox.tsx: this pull request indents the gallery's
track one level deeper, and meta#8197 gives each page its index there.
The resolution keeps both.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved pending CI: CI has not run on this head yet; as a draft from a fork, it runs once the pull request is marked ready and a maintainer adds the ci:run label.

#8197 — approve

No blocking findings.

#9810 — approve

No blocking findings: "more" and "less" now read the caption before any write, so neither press lays the viewer out an extra time.

Whole stack — passes

A pull request lands only when it and every pull request below it are approved.

[Automated review]

@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 9572a26

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved pending CI: CI has not run on this head yet; as a draft from a fork, it runs once the pull request is marked ready and a maintainer adds the ci:run label.

#8197 — approve

No blocking findings; unchanged since the last review.

#9810 — approve

No blocking findings. The new commit resets the photo left behind by the page the pager reports, so a settle that arrives before the new page is marked current no longer leaves the previous photo zoomed.

Non-blocking, worth acting on before #8197 lands:

  • Miss: that fix changes #8197's own onSettle, but #8197's branch still has the old reset, so #8197 landing alone would ship the race; push the fix to #8197's branch.

The earlier non-blocking notes (FR17's verification row and review state, the useZoomStage comment, the block inside the clamped caption) still stand.

Whole stack

Passes: both heads merge cleanly with main, and the stack's net behaviour matches its approved records. A pull request lands only when it and every pull request below it are approved.

[Automated review]

},
// The pager names the page it rests on. Its report can come before the
// render that marks that page current, so the page, not the mark, decides.
onSettle: settled => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (Miss): this fixes #8197's own onSettle, but #8197's branch still has the reset that skips the page marked current, so #8197 landing alone would keep a settle that reports before the render from resetting the photo it left. Push this commit to #8197's branch so the fix lands with the code it fixes.

This branch was successfully deployed

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

Labels

ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) 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