Skip to content

feat(core): UIScrollView zoom constants and the zoom stage geometry - #8147

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c24-zoom-geometry
Nov 6, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c24-zoom-geometry

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8147 → #8148 → #8197 → #8215, #9810 (both on #8197)

One commit on main 1dce318, which carries #9915's motion classes and #10068's shared theme-timing reader. This round rebases onto it and follows #9915's class for Lightbox: its main interaction is a gesture, so its zoom settles on the shared springs.settle role (spec:AST-094 FR45, DEC-26; component:Lightbox FR5 on main), not on UIKit's eased curve. Specs decision, Nov 4 2026: "Lightbox: shared springs.settle, no new zoom role". So zoomMedia drops the settle's curve and its length law, the zoom stage geometry drops the settle curves it built, and the record keeps main's text, without the Motion roles table this pull request had added. The measured settle stays in motion/MEASUREMENTS.md as a reference.

Why

Lightbox's touch zoom (#8148) has to feel like the iOS Photos viewer, which people compare it with on their own phones. This pull request adds the measured values and the pure geometry #8148 builds on. Nothing renders differently yet.

What changes

  • motion/uikit/zoomMedia.ts: past a maximum M the zoom reads M·(2 − M/s), with s the pinch's scale; past a minimum m it reads m ÷ (2 − s/m). Photos' aspect-fill double tap and a maximum zoom of 6 times the photo's pixel size are data, each with its measured source.
  • Lightbox/zoomStage.ts: where a zoomed photo rests, the transform that keeps a point in place, the drag's rubber band, and the double-tap and release targets. ZOOM_STEP (2) is Lightbox's own zoom step.
  • motion/MEASUREMENTS.md and motion/README.md list the zoom measurements. UIKit's eased zoom settle (100 ms plus 300 ms per unit of the zoom ratio's log, at most 400 ms) stays there as a reference only.
  • No record change: main's component:Lightbox FR5 already settles the zoom on a spring.

How to check it

Nothing changes on screen in this pull request. With #8148 on top, in the "Gallery with zoom" story on a phone:

  1. Pinch a photo past its maximum and let go: it springs back without bouncing.
  2. Double-tap a landscape photo: it fills the screen's height, and the point you tapped stays under your finger.
  3. Double-tap a portrait photo: it zooms 2×.

Writing rubric results

No record line changes in this pull request.

Testing

At this head (70a405d) on main 1dce318: pnpm install --frozen-lockfile exits 0; core tsc is clean; the motion and zoomStage unit files pass 891/891; pnpm check:knowledge passes. With #8148 on top (1b08e4d), the full Lightbox battery passes (that pull request lists it).

@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 Nov 5, 2026 3:42pm 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
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 5, 2026, 3:51 PM UTC

@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 is on hold for a spec decision: the UIKit motion foundation and the zoom geometry rest only on draft spec:AST-094, so an engineering owner (the author qualifies, as an engineering owner) needs to approve or amend its DEC-1, DEC-2, FR1–FR3 and FR5–FR6, together with the amendments it proposes to current records, by commenting /approve-spec <full head SHA> on this pull request. Independently of that decision, two changes are needed:

  • Run the new interaction proofs inside an existing CI owner job or the required test join instead of the separately routed pr-interaction job, which spec:AST-030 FR11 and FR7 do not allow.
  • Rebase or retarget so this pull request carries only its own zoom commit once its predecessors land; as issued it carries 13,842 authored lines of the stack.

[Automated review]

Comment thread .github/workflows/ci.yml
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

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 job routes itself (Check interaction scope) and no join requires it: the test join and .github/scripts/ci-test-join.mjs still list only test-ui, test-node, test-build, registry-contract and test-native-swift. spec:AST-030 FR11 says new coverage inside an existing surface must join those owners and must not create a separately routed lane or required context, and FR7 says an owned lane's failure must fail its join. Hosting these steps in an existing owner job (as pr-a11y hosts the other Chromium guards) or requiring this job from the test join would conform.

Comment thread packages/core/src/Lightbox/zoomStage.ts Outdated
* it rests with no gap at either edge. All constants come from
* motion/uikit/zoomMedia.ts and motion/uikit/scroll.ts.
*
* SYNC: When modified, update zoomStage.test.ts and useZoomStage.ts.

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.

useZoomStage.ts is not in the tree at this head (nor is the useZoomStage named on line 12), so this SYNC line points at nothing; point it at a file that exists, or add the hook together with its consumer.

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 line pointed at a file this commit doesn't have. Fixed in ff5cf88: the SYNC line names only zoomStage.test.ts here; #8148 adds useZoomStage.ts back together with the hook.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 70a405d

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
Lightbox · Storybook 55.0 kB (0) · 9.1 kB CSS (0) 1,126 (+241) ✓ · ✓ · ✓

Package size: @astryxdesign/core 6.59 MB packed (+9.9 kB); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 9 rows, core 8 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@vjeux
vjeux force-pushed the uikit/c24-zoom-geometry branch from 8bdfcde to 64a8d76 Compare October 24, 2026 19:40
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Oct 24, 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.

This pull request is on hold for a spec decision: the zoom geometry and the motion and gesture foundation it carries rest only on draft spec:AST-094, whose FR4 and DEC-1 keep touch motion out of themes, while this head also ships theme motion roles and a platform adaptation condition outside the closed vocabulary of spec:AST-012 FR2. An engineering owner (the author qualifies, as one) records the decision by commenting /approve-spec <full head SHA> once the record and the code agree, or asks for the record to be amended first. Independently of that decision, these changes are needed:

  • Rebase or retarget so this pull request carries only its own zoom commit once its predecessors land; as issued it carries 29,365 authored lines of the stack.
  • Run the interaction proofs inside an existing CI owner job or the required test join instead of the separately routed pr-interaction job, which spec:AST-030 FR11 and FR7 do not allow.
  • Mark zoomMedia @experimental, since it is exported as uikit.zoomMedia from @astryxdesign/core/motion/experimental and spec:AST-017 FR21 requires the marker on every declaration there.

[Automated review]


import {registerMotionTable} from '../roles';

export const zoomMedia = {

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.

zoomMedia is reachable as uikit.zoomMedia from @astryxdesign/core/motion/experimental (export * as uikit in motion/experimental.ts), but unlike the other declarations there it has no @experimental doc block, which spec:AST-017 FR21 requires ("Declarations carry @experimental"). Adding the doc block, and naming it in the uikit comment and in motion/experimental.test.ts, would conform.

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, FR21 asks for the marker on every declaration the experimental subpath exposes. Fixed in ff5cf88: zoomMedia carries a doc block with @experimental, the uikit comment names it, and motion/experimental.test.ts checks both (the doc-block check failed before).

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.

Update in b4db6a7: with #8076's experimental-subpath drop, zoomMedia no longer sits on an experimental subpath. It stays inside core, re-exported from motion/uikit with registerZoomMediaTable(), so the marker and motion/experimental.test.ts went with the subpath.

Comment thread packages/core/src/Lightbox/zoomStage.ts Outdated
);
}

const ease = cubicBezier(zoomMedia.settle.curve);

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 fixes zoomMedia.settle.curve when the module loads, but the same curve is registered as the motion role zoomMedia.settle.curve and listed by astryx docs motion as something a theme can retune, so no theme value can reach the settle. Either keep the curve out of the role registry or let settleCurves take the curve resolved for the element, whichever the spec:AST-094 FR4 decision calls for.

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 role was listed but no theme value could reach the settle. Fixed in ff5cf88: settleCurves takes the curve the element's theme resolves for zoomMedia.settle.curve (from #8148 on, the shared zoom stage, useZoomCore.ts, passes resolveMotion(stage, zoomMedia.settle.curve)). zoomStage.test.ts runs a linear theme value through it and failed before.

Comment thread .changeset/touch-presentation.md Outdated
'@astryxdesign/core': patch
---

[feat] `presentation="adaptive"` on DropdownMenu, ContextMenu, MoreMenu, Selector and MultiSelector chooses the bottom sheet on a compact touch screen: a coarse primary pointer and a viewport narrower than 744px or shorter than 500px (it was 768px wide and below). An iPad mini in portrait keeps the anchored popover; a phone in landscape now gets the sheet.

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.

The same query also decides sub-menu drill-in (useMenuDrillIn), and a sub-menu's presentation defaults to adaptive, so with no opt-in sub-menus now drill in on a phone in landscape and fly out on 744–768 px touch tablets. Worth naming here beside the root presentation.

@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 is on hold for its governing spec: the zoom geometry and the motion, gesture and theme-motion changes it carries rest on spec:AST-094, which is still proposed in #8378 and not on main, and no record yet covers the press-feedback selection lock or the new undefinedVariables build option. An engineering owner (the author is one) needs to land that record on main, or identify other current spec for these additions. Independently of that decision, these changes are needed:

  • Rebase or retarget so this pull request carries only its own zoom commit once its predecessors land; as issued it carries 34,536 authored lines of the stack.
  • Run the interaction proofs inside an existing CI owner job or the required test join instead of the separately routed pr-interaction job, which spec:AST-030 FR11 and FR7 do not allow.

[Automated review]

Comment thread packages/lab/tsconfig.json Outdated
@@ -2,7 +2,15 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
"rootDir": "./src",
"paths": {

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.

These paths let Lab typecheck imports of core modules that core's exports do not list (@astryxdesign/core/gestures/* includes the velocity tracker). A Lab file using them would fail for every Lab consumer, and architecture:interaction-modality INV10 says Lab cannot import the tracker. No Lab file uses them at this head; dropping the mappings here and in tsconfig.build.json would conform.

Comment thread .changeset/touch-selection-lock.md Outdated
'@astryxdesign/core': patch
---

[fix] Under a finger or pen, holding a control or a cell that shows the touch press (the press feedback surfaces) no longer starts text selection or the iOS callout, as on UIKit controls. Selection and the callout come back when the finger lifts. Text fields keep their selection, a link keeps its native menu and preview, and a mouse keeps desktop selection.

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.

spec:AST-017 FR7 reserves [fix] for corrections; turning off selection and the callout on every touch press surface is a new behaviour no current record asks for, so it reads as a capability ([feat]) once its authority is settled.

'@astryxdesign/build': patch
---

[fix] `astryxStylex` resolves `defineConsts` values read from another file. With the `{stylexOptions}` form (the one Storybook uses), a file that read only constants from a `.stylex.ts` module lost that import once StyleX compiled it, so the constants never reached the CSS: a constant used as a media-query key failed the build ("Invalid empty selector") and one used as a value shipped as a custom property nothing declares. A constant defined on the library side of the layer split and read on the product side stayed unresolved in either form. A production build now also warns, naming the variable and the rule that reads it, when its stylesheet reads a StyleX-generated custom property that nothing declares; set `undefinedVariables: 'error'` to fail the build instead.

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.

Besides the defineConsts correction, this [fix] adds a capability: the undefinedVariables option and a default production warning. spec:AST-017 FR7 tags compatible capabilities [feat], so splitting it into the fix and a [feat] for the option would conform.

/**
* Registers the table's springs and curves as motion roles a theme may
* retune; the limits, scales and duration law are geometry. Idempotent:
* Lightbox's touch module (Lightbox/useZoomStage.ts) calls it before its

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.

Lightbox/useZoomStage.ts does not exist at this head and nothing calls registerZoomMediaTable() yet; zoomStage.ts:12 has the same useZoomStage reference. Naming the module that will call it once it lands keeps the pointer true.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

On the review's points:

@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 is on hold for its governing spec: the zoom geometry and the motion, gesture and theme-motion changes it carries rest on spec:AST-094, which is still proposed in #8378 and not on main, and no record covers the press-feedback selection lock or the new undefinedVariables build option. An engineering owner (the author is one) needs to land that record on main, recording the decision with /approve-spec <full head SHA> on the pull request that carries it, or identify other current spec for these additions, and settle the selection lock and the build option too. Independently of that decision, these changes are needed:

  • Rebase or retarget so this pull request carries only its own zoom commit once its predecessors land; as issued it carries 37,668 authored lines of the stack.
  • Run the interaction proofs inside an existing CI owner job instead of their own routed interaction-scope and pr-interaction lane, which spec:AST-030 FR11 does not allow.
  • Drop Lab's tsconfig paths and the docsite and Sandbox resolvers for core modules core does not export: no Lab file uses them, and the gestures mapping reaches the velocity tracker Lab may not import.
  • Add real-browser evidence for the moved adaptive edges (852 × 393 and 768 × 1024 with a coarse pointer), as spec:AST-013 FR10 requires.

[Automated review]

Comment thread .github/workflows/ci.yml
echo 'applies=true' >> "$GITHUB_OUTPUT"
fi

pr-interaction:

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.

The test join now requires this job, which meets spec:AST-030 FR7, but the proofs still run in a lane of their own with their own routing job (interaction-scope, line 3759). FR11 says new coverage inside an existing surface "MUST join those owners and MUST NOT create a separately routed lane or required context", and the same record names pr-a11y as the pull-request owner that keeps fast interaction guards. Running these steps inside that owner, or inside test-ui, would conform.

'springs.alertDismiss': 'spring',
'springs.alertPresent': 'spring',
'springs.banner': 'spring',
'springs.bouncy': 'spring',

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.

Once released, a role id is theme input a theme pins, and this one (like springs.swiftUIDefault, springs.uikitDefault and timing.curves.caDefault) names a platform preset rather than a use. architecture:theme-tokens INV3 asks for purpose-led names, and #8378's FR4 renames these presets (springs.emphasis for .bouncy), so a theme that sets them now would break when that record lands.

Comment thread packages/cli/assets/docs/motion.doc.mjs Outdated
content: [
{
type: 'prose',
text: 'Touch motion has a table of springs, durations and curves measured on iPhone and iPad. Each value is a motion role named by its table path, such as `springs.sheetPresent` or `timing.curves.easeInOut`. A theme retunes a role through `motion.springs`, `motion.durations` or `motion.curves`, at its root or in an adaptation rule; a role it leaves alone keeps its measured default. A motion reads its roles as it starts, so a theme change applies from the next motion.',

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.

No shipped component calls resolveMotion or the animator at this head, so a theme that follows this section writes variables nothing reads yet. Saying so here and in the [feat] changeset, or shipping the fields with the first component that reads them, would keep the docs true.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Cindy decided touch text selection (recorded in #8454, spec:AST-094 FR41): a tap never selects text; a long press on an element whose hold does something (a long-press menu, a stepper's hold-repeat, a slider thumb, other hold-driven controls) does that action with no selection or loupe; anything without a hold action keeps native long-press selection. The system adds no Copy item.

This PR's selection lock should follow that rule: suppress selection only on hold-driven elements, not on every pressable.

vjeux, could you update the PR to match?

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

Recommending approval. The measured zoom values and the zoom-stage geometry match spec:AST-094 FR1–FR4 and component:Lightbox FR6, each settle axis now plays through the shared solveAnimatedPageScroll, and the FR5 settle sentence and Motion roles table have an engineering owner's approval for this commit. Three optional touch-ups are in the inline comments: a comment that calls fractions of the fitted box "px", a comment a clearer name would replace, and the lnRatioMs row's wording, which leaves out the absolute value FR5 now states.

[Automated review]

Comment on lines +398 to +399
// The photo point (in fitted-box px of `before`) at the old centre, then
// the same point in the new fitted box.

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.

Optional (INV29): u and v are divided by before.fit.width and before.fit.height, so they are fractions of the fitted box, not px. Suggested: "The photo point at the old centre, as a fraction of the fitted box. The new transform puts it at the new centre."

scale: number,
): ZoomTransform {
const {fit} = geometry;
// The fitted-box point under `anchor`.

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.

Optional (INV29): this comment explains what u and v hold. Naming them for it (for example photoX, photoY) makes the comment unnecessary.

| Role | Default | Use |
| ---------------------------- | ------- | ------------------------------------------------------ |
| `zoomMedia.settle.baseMs` | 100 ms | The zoom settle's length before its zoom-ratio term |
| `zoomMedia.settle.lnRatioMs` | 300 ms | The length added for each unit of the zoom ratio's log |

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.

Optional: FR5 now says |ln(zoom ratio)|; "each unit of the zoom ratio's log" drops the absolute value, so read alone a zoom out would shorten the settle. "The length added for each unit of |ln(zoom ratio)|" matches FR5.

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

Recommending approval. The measured zoom values and the zoom-stage geometry match spec:AST-094 FR1–FR4 and component:Lightbox FR6, the settle reuses the shared animated-page-scroll curve and rubber band, and the component:Lightbox record change (the FR5 settle sentence and the Motion roles table) carries an engineering owner's approval for this head. The zoom-stage, UIKit-table, motion-role and paging tests, the motion-role generator check, strict lint, the core typecheck and the knowledge and changeset checks pass.

Optional, non-blocking:

  • zoomStage.ts line 398: u and v are fractions of the fitted box, not px; the comment can say so.
  • zoomStage.ts line 211: naming u and v for what they hold (for example photoX, photoY) makes the comment unnecessary.
  • Lightbox.spec.md line 162: "each unit of the zoom ratio's log" drops the absolute value FR5 states; "each unit of |ln(zoom ratio)|" matches it.

[Automated review]

@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 8c1e3e6

@vjeux
vjeux force-pushed the uikit/c24-zoom-geometry branch from 8c1e3e6 to 366123e Compare November 3, 2026 17:06
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 2026
@cixzhang

cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 366123e

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 3, 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.

Recommending approval. The measured zoom values and the zoom-stage geometry match spec:AST-094 FR1–FR4 and component:Lightbox FR6, and the component:Lightbox FR5 settle sentence and Motion roles table carry an engineering owner's spec approval for this head. The unit tests reproduce the measured iOS numbers, and the generated motion-role table is in step with the registered roles.

Optional, non-blocking:

  • zoomStage.ts:398-399: u and v are fractions of the fitted box, not px, so the comment's unit is wrong.
  • zoomStage.ts:211: naming u and v for what they hold would replace the comment.
  • Lightbox.spec.md:162: "each unit of |ln(zoom ratio)|" would match FR5, where "the zoom ratio's log" drops the absolute value.

[Automated review]

@vjeux
vjeux force-pushed the uikit/c24-zoom-geometry branch from 366123e to 79ab09d Compare November 4, 2026 00:15
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 79ab09d

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

Recommending approval. The measured zoom values and the zoom-stage geometry match spec:AST-094 FR1–FR4 and component:Lightbox FR6: the settle curve is the timing.curves.move role, each settle axis reuses the shared animated page-scroll curve, the drag reuses the shared rubber band, and the tests reproduce the iOS 26.2 numbers. The component:Lightbox FR5 sentence and Motion roles table carry an engineering owner's /approve-spec for this exact head, and the generated motion role table lists the three new roles.

No blocking findings. The three optional line comments already on the pull request still apply: the unit in the comment at zoomStage.ts:398-399 (fractions of the fitted box, not px), naming u and v at zoomStage.ts:211, and "each unit of |ln(zoom ratio)|" for the lnRatioMs row in Lightbox.spec.md.

[Automated review]

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 1053544

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

Recommending approval, pending CI: the required checks are still running on this head.

The measured zoom values and the zoom-stage geometry match spec:AST-094 FR1–FR4 and component:Lightbox FR6: the settle curve is the timing.curves.move role, each settle axis reuses the shared animated page-scroll curve, the drag reuses the shared rubber band, and the tests reproduce the iOS 26.2 numbers. The component:Lightbox FR5 sentence and Motion roles table carry an engineering owner's /approve-spec for this exact head, and the generated motion role table lists the three new roles.

No blocking findings. The three optional line comments already on the pull request still apply: the unit in the comment at zoomStage.ts:398-399 (fractions of the fitted box, not px), naming u and v at zoomStage.ts:211, and "each unit of |ln(zoom ratio)|" for the lnRatioMs row in Lightbox.spec.md.

[Automated review]

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e62fa50

@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. The measured zoom table and the zoom-stage geometry match their iOS 26.2 numbers and component:Lightbox FR6, and the Lightbox record change carries an engineering owner's approval at this head. One blocking finding remains:

  • Regression: the zoom settle after a pinch released past a limit plays timing.curves.move from rest for 100 ms + 300 ms × |ln ratio| (zoomMedia.ts:65, zoomStage.ts:336-363), but current spec:AST-094 FR45, FR4 and DEC-26 make a pinch's settle a spring that starts at the release velocity and say no motion plays timing.curves.move, and this pull request leaves that record unamended beside the approved Lightbox FR5 that says the opposite.

To fix it, add the matching spec:AST-094 amendment to this pull request (FR45's list of exceptions, FR4's "no motion plays them" sentence and the FR45 verification row) so an engineering owner can approve it at the new head, or play the settle as a spring from the release velocity and restore FR5's list. The earlier optional notes on the zoomStage.ts comments and the lnRatioMs row wording still apply.

[Automated review]

* iPhone 17 Pro, CAAnimation read (timing function control points),
* Oct 23 2026
*/
curve: timing.curves.move,

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.

Regression (blocking): this makes the zoom settle after a pinch play timing.curves.move as a timed ease-in-out from rest. Current spec:AST-094 FR45 says motion a gesture drives, including "the settle … after" a pinch, "MUST play a spring … that starts at the gesture's velocity", and FR4 says timing.curves.move stays the measured reference "and no motion plays them". The approved Lightbox FR5 sentence does not amend AST-094. Either add the AST-094 amendment here (FR45's exceptions, FR4's sentence, the FR45 verification row) for an exact-head approval, or play the settle as a spring from the release velocity.

| FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled |
| FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled |
| FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap |
| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR45). The zoom settle plays `timing.curves.move` for `zoomMedia.settle.baseMs` plus `zoomMedia.settle.lnRatioMs` times \|ln(zoom ratio)\|, at most `zoomMedia.settle.maxMs`. Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap |

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.

Regression (blocking): this row now says the zoom settle plays timing.curves.move for a timed length, while spec:AST-094 FR45, unchanged here, still makes "the settle … after" a pinch a spring from the gesture's velocity, and its verification row fails a Lightbox photo pinch whose settle "starts from rest". The two current records would disagree; amend AST-094 in this pull request as well.

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e463256

@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. The measured zoom table and the zoom-stage geometry match their iOS 26.2 numbers and component:Lightbox FR6, and the Lightbox record change carries an engineering owner's approval at this head, but one blocking finding remains:

  • Regression: the zoom settle after a pinch released past a limit plays timing.curves.move from rest for 100 ms + 300 ms × |ln ratio| (zoomMedia.ts:65, zoomStage.ts:317-363), while current spec:AST-094 FR45, FR4 and DEC-26 still make a pinch's settle a spring that starts at the release velocity and the FR45 curve arm the new FR5 sentence cites is only proposed in #9915, so merging now leaves the two records disagreeing.

To fix it, merge after that FR45 amendment is on main and rebase onto it, or carry the amendment here for an engineering owner's approval at the new head, or play the settle as a spring from the release velocity and restore FR5's list. The earlier optional notes on the zoomStage.ts comments and the lnRatioMs row wording still apply.

[Automated review]

| FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled |
| FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled |
| FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap |
| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR45). The zoom settle plays `timing.curves.move` for `zoomMedia.settle.baseMs` plus `zoomMedia.settle.lnRatioMs` times \|ln(zoom ratio)\|, at most `zoomMedia.settle.maxMs`, per `spec:AST-094` FR45; iOS measures this settle keyed by zoom ratio. Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap |

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.

Blocking: "per spec:AST-094 FR45" cites a curve arm that FR45 on main does not have yet; current FR45 makes the settle after a pinch a spring that "starts at the gesture's velocity", FR4 says no motion plays timing.curves.move, and DEC-26 says any gesture uses a spring. This row can merge once the FR45 amendment proposed in #9915 is on main (rebased onto it), or with that amendment carried here.

The UIKit constants table gains zoomMedia, measured on iOS 26.2 in a
UIKit lab app and in Photos: the give past the zoom limits, and Photos'
double tap and maximum zoom. UIKit's eased zoom settle stays a
measurement only. Lightbox's main interaction is a gesture, so its zoom
settles on springs.settle (spec:AST-094 FR45).

Lightbox/zoomStage.ts is the pure geometry of a zooming scroll view in a
photo viewer, checked against those numbers. A double tap zooms to the
larger of filling the stage and 2x, about the tapped point, as
component:Lightbox FR6 asks. Nothing renders the stage yet: Lightbox's
touch zoom plays it in the next change.
@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 70a405d

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

Recommending approval, pending CI: one required check is still running on this head.

The measured zoom values and the zoom-stage geometry match the current touch-presentation spec (spec:AST-094 FR1–FR3: measured, sourced values and UIKit's rubber band) and component:Lightbox FR6 (a double tap to the larger of filling the viewer and 2×, about the tapped point), and the unit tests reproduce the iOS 26.2 numbers. The zoom settle that conflicted with spec:AST-094 FR45 is no longer part of this pull request.

Optional: the two earlier line comments on zoomStage.ts still apply (the unit in the comment at lines 343–344, and naming u and v at line 208).

[Automated review]

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 70a405d

This branch was successfully deployed

1 active deployment
Preview — 70a405d2 Deployed Nov 5, 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 A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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