Skip to content

feat(Wheel): a trackpad scroll and a mouse wheel drive the picker wheel's pan (spec:AST-094 FR43) - #8698

Draft
vjeux wants to merge 1996 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-fr43
Draft

vjeux wants to merge 1996 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-fr43

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8480 (the wheel stack's top) and #8676 (a pan takes wheel and trackpad scrolling). Review only this PR's change: vjeux/astryx@f30a46d...2d68021

User need

On a touch device with a trackpad or mouse, such as an iPad with a keyboard, a two-finger scroll over a sheet's picker wheel stopped dead where the scroll ended: the wheel moved by each scroll step and snapped onto a row 160 ms after the last one. iOS lets the wheel keep the scroll's speed, coast on and rest on a row. This makes the touch wheel do the same.

Problem and solution fit

What led to it: Astryx's UIKit-parity work, and the owner's decision that gestures work for every pointer (spec:AST-094 FR43, approved in #8646): a pan also takes wheel and trackpad scrolling. #8676 gives the pan that input; this hands the wheel's scrolling to its pan.

  • PickerWheel's pan now allows both scroll kinds, and the wheel's own wheel listener is gone. A scroll drives the same pan, release plan and rest rule as a finger.
  • A ctrl-scroll is how a trackpad pinch arrives; the column has no pinch, so it stays the page's zoom.

Current specification

spec:AST-094 FR43 and FR24 (one listener per element).

Public delta and caller burden

No API change. Size (INV6): DateInput −144 B gzip JavaScript (TimeInput −138, DateTimeInput −139), CSS unchanged.

Design and alternatives

  • Keep the wheel's own listener and add a coast to it. Smaller step, but it would copy the pan's release plan and leave two listeners on the column, which FR24 rules out.
  • Chosen: let the pan take the scroll, so a scroll and a finger share one release plan and the host keeps its one listener. The wheel code gets shorter.

Evidence

  • Before: ten quick 24 px scroll steps left the wheel exactly where the scroll ended, in jsdom and in Chromium with real wheel input.
  • After: the wheel coasts on more than two rows and rests on one, in 10 of 10 Chromium runs, and the page doesn't scroll.
  • No recording: the simulator rig has no trackpad input, and WebKit's phone emulation has no wheel events.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

On a touch device with a trackpad (or Chromium's phone emulation with a mouse wheel), open Storybook's TimeInput › Bottom Sheet With Min Max:

  1. Scroll the minutes quickly with two fingers: the wheel coasts on and rests on a row.
  2. Scroll slowly: it rests on the nearest row.
  3. Ctrl-scroll over a wheel: the page zooms; the wheel doesn't turn.

Automated: PickerWheel.test.tsx covers each step (the quick scroll fails without this change), and the Chromium spec drives real wheel input. Not this change: CI's root-entry bundle check fails on main's test, which #8671 fixes.

vjeux added 30 commits October 27, 2026 01:26
…n before the click guard

internal/motion.ts (meta#8562) re-exports UIKIT_CLICK from the barrel, so a
tree carrying both fails typecheck and the Storybook build since this
pull request dropped it. The barrel keeps it, with the click guard's
window, and the table of every gesture group still leaves it out. Fails
until the barrel exports it.
…'s window

This pull request dropped UIKIT_CLICK when the click guard took the
stray-click window over, but importers written before it read the barrel's
UIKIT_CLICK (internal/motion.ts in meta#8562 re-exports it), so a tree carrying
both failed typecheck and the Storybook build. UIKIT_CLICK stays in the
barrel, deprecated, with the guard's 400 ms (CLICK_GUARD_WINDOW_MS), and
UIKIT_GESTURES still leaves it out.
…ter proofs

spec:AST-094 FR43 has gestures attach for every pointer, so each FR43 proof needs a pen and a wheel as real engine input, not only a finger. webInputDriver(page) adds wheel({x, y, dx, dy, ctrl}) and pen(points): in Chromium through CDP Input.dispatchMouseEvent, with a wheel step's deltas scaled back so the page receives exactly its CSS px (Chromium divides them by the device scale factor times the visual viewport's scale: a raw 120 px step reached a phone page as 40), and a pen stroke with pointerType pen, pressure, tilt and scripted timestamps; in WebKit through page.mouse.wheel in a desktop context. WebKit's phone emulation has no wheel and Playwright has no pen for WebKit: the driver's wheels and pens flags say so, with WHEEL_SKIP and PEN_SKIP. The input log also records wheel steps and pointer pressure.
…ter events) into the every-pointer host

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
… follow-ups

A helper for lanes on F2's line: the harness files (README, the harness self-test, index.ts) resolved by the harness owner, keeping both sides' instruments.
…k) into the FR43 harness helper

Keeps the helper's harness self-test green in WebKit too: meta#8535 fixes the backward case's flake under load.
On a page whose wheel listeners are all passive (every page with the input log), Chromium answers Input.dispatchMouseEvent before the page has the wheel step and delivers it afterwards, so a log read right after wheel() could miss it. wheel() now arms a one-shot passive capture listener before the dispatch and waits until the page has the step (2 s limit), in WebKit too. The ctrl-only note was wrong: a ctrl step reaches an all-passive page as well, and nothing zooms. The docs cite spec:AST-100 FR4, which is current.
Bundled with the size report's esbuild options, none of Core's public
entries carries a byte of gestures/ until a component imports it
(architecture:react-component-runtime INV13). The click guard in utils/
imports the haptic switch's attribute from gestures/, so the root and hooks
entries carry gestures/hapticSwitch.ts: both cases fail until it moves.
…blic entries carry no gesture code

The click guard (utils/clickGuard.ts) imported HAPTIC_SWITCH_ATTRIBUTE from
gestures/hapticSwitch.ts, which put gesture code in Core's root and hooks
entries. The attribute moves to utils/hapticSwitch.ts, as the coarse-pointer
query did; the click guard, the gesture host and haptics read it there, and
gestures/hapticSwitch.ts re-exports it, deprecated, for importers written
before the move. The module-graph case follows the new path.
…der a fine pointer (spec:AST-094 FR24, FR19)

FR24 as revised at c46ace4: a claiming recognizer's touchmove listener
for a finger counts outside the one-listener budget. Under a fine pointer a
horizontal pan's element carries its non-passive touchmove beside its one
pointerdown, so a touchscreen laptop's finger claims as on a phone; a tap,
which claims only at its lift, keeps its one listener. A one-way pan (a
toast's swipe) writes no touch-action, leaving the browser every direction,
so its element's listener, there before the finger lands, is the only way
it claims. In a desktop browser context the fixture's claiming elements
carry that listener and nothing else, and the page none. All fail while the
blocking listener is a coarse pointer's only.
…chmove for every pointer (spec:AST-094 FR24, FR19)

FR24 at c46ace4 counts a claiming recognizer's touchmove listener for a
finger outside the one-listener budget, so a fine pointer's element keeps
it beside its press's pointerdown: a touchscreen laptop's finger claims by
cancelling its moves, as on a phone, instead of resting on touch-action
alone. A kept press (a hold-repeat) keeps its listener too. The two cases
that counted only the pointerdown now count both.
…claims (spec:AST-094 FR43, FR41)

A mouse drag starts selecting text before a pan claims at 10 px, so a pan
over text (a toast, a pager page, a sheet) selected it while it moved.
Once the pan claims, the selection the drag started goes and the element
refuses one until the lift; a mouse click's selection is left alone, as a
tap claims only at its lift. The pan case fails until the host locks on a
mouse or a pen claim.
…laims (spec:AST-094 FR43, FR41)

The host locked selection only for a finger or a pen at touch-down under a
coarse pointer, so a mouse drag that a pan claimed kept selecting the text
under it. The arena's claim now locks a mouse or a pen contact: the
selection its drag started goes and the claiming recognizers' elements
refuse one with inline styles until the pointer lifts (lockDragSelection,
utils/selectionLock.ts, which shares lockSelection's refcounted styles). A
tap claims at its lift, so a click keeps its selection.
… the drag selection lock and the one-read finger

The README's fine-pointer listener budget names a claiming recognizer's
blocking touchmove beside the press listener (FR24, FR19), and a mouse or
pen drag a recognizer claims takes no selection (FR41). The changeset says
so, and that a finger is read once where the browser sends Touch Events
without exposing ontouchstart.
…s (spec:AST-094 FR43)

A ctrl-click is macOS's context click, so like a right click it starts no
press, while a ctrl-click elsewhere still presses. isPrimaryClick
(utils/primaryClick.ts) names the rule once, for the host and for the
press controller; the test helper now takes a button and the ctrl key.
Fails until the host checks it.
…k never did (spec:AST-094 FR43)

isPrimaryClick (utils/primaryClick.ts, on utils/isApplePlatform.ts) is the
one rule: the main button, and not a ctrl-click on macOS or iPadOS, which
is that platform's context click. The host's armed and installed press
paths use it, and the press controller shares it.
… public entries carry no gesture code) into the every-pointer host
…xperimental touch exports

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

# Conflicts:
#	packages/core/src/motion/README.md
…licy) into the animator's presentation start
…two frames

In Chromium the press reaches the compositor with no page round trip, so a stroke sent right after a navigation could carry the previous document's page scale: about:blank's in the phone project put a press at 100 px at 41.02 px (9 of 10 strokes when the driver predates the navigation). pen() now waits for two presented frames first, through the harness's own frames. The README's WebKit wheel row also says the step moves the mouse there and holds Control as a key.
…nd a pinch a ctrl-wheel (spec:AST-094 FR43)

A pan takes scroll input only for the scroll types it allows (UIKit's
allowedScrollTypesMask, none by default, so its element gets no wheel
listener), along its own axis, after the scrollers under the pointer, as
decided at a wheel gesture's first event; a line- or page-mode delta or a
whole pixel notch is a mouse wheel's (discrete), the rest a trackpad's. A
pinch takes a ctrl-wheel (a trackpad pinch), its scale following
exp(-deltaY / 100). A wheel gesture ends 100 ms after its last event, keeps
the page's selection, and the listener goes when the pan is disabled. The
manual clock runs its timers on runDue. Fails until the host takes wheels.
vjeux added 12 commits October 29, 2026 13:45
…o their own changesets

In the same release meta#8179's time columns loop, so "no shipped column loops
yet" would contradict it; the columns that loop say so themselves.
…heirs) into uikit/c21-wheel-rest-and-spin
…k's fixes) into uikit/c21-wheel-fr43

The two harness files that conflicted take the clean merge of meta#8340 and
meta#8676, the heads that carry them.
…tep a frame from the change that ends it

Against the control, a frame with no new component position was a miss. On an easing tail the component moves under one cast step a frame, which the screencast cannot show on every frame: Lightbox's dismiss-cancel proof in WebKit missed one of the control's 12 to 14 frames in 7 of 10 runs, each time at its spring's last plateau (210, 210, then 208 on WebKit's 2 px grid). Such a run of unchanged frames now counts as moved when the change that ends it is a single cast step. A freeze ends in a catch-up of more than one step and stays a miss, as does a run that no change ends, and the component must still show a new position on at least minJudgedFrames of the control's frames, so a frame-counted animation that stands still through the block and resumes one step a frame fails too. The cast step is read from the tracked value's changes in the judged part rather than from its positions: the photo's position once the block ended (207) sat off WebKit's 2 px grid and made the step read as 1 px.

Red first: the tail, pooled tail, off-grid commit and frame-counted freeze cases fail on the previous busy.ts; the single-miss, slow freeze and stop-for-good cases still fail. Lightbox's five busy proofs with this judgment: 30 of 30 in Chromium, 39 of 40 in WebKit (the other presented 3 frames in its judged part, under the 4 a window needs). The harness's own proofs still pass a compositor animation with no miss and fail the rAF one (0 of 39 to 47 control frames) in both engines.
…awn PM row, and the 1 s busy proof follows rows in turn

- The clamped-turn proof taps the PM row where the cylinder draws it.
- UIPickerView's cylinder foreshortens a row as it turns away from the band
  and leaves the band's row to the band's copy, so no one row stays in
  sight, moving a pixel a frame, for the whole 1 s block. Rows take turns:
  each is followed from three and a half rows below the band until it
  reaches the band's edge, and the harness judges each stretch, which
  together cover the whole block.
… of waiting for its pan to end

A trackpad's scroll or a smooth mouse wheel with no momentum of its own
stops sending wheel at its last step, and the gesture host ends the pan
100 ms later. The rows held still for those 100 ms and then started the
coast at full speed. Now, once a continuous stream pauses for half again
its own interval (20-50 ms), the wheel glides on from where it is with the
stream's speed, as the release would; that glide is the release when the
pan ends, and a wheel that arrives during it takes the rows back from
where they are. A mouse wheel's notches keep settling by position.
… turn through the 1 s block) into uikit/c21-wheel-fr43
A stream sends a wheel every frame it moves, and the browser dispatches a
frame's input before its animation frame callbacks, so the glide now
starts at the first frame that follows one with no wheel. A busy page
delays that frame along with the timer that ends the pan, where a timer of
its own fired late under load (the browser proof held still 98 and 148 ms
on a busy box). The browser proof counts frames instead of milliseconds,
and drives a genuinely quick stream through CDP, without the driver's wait
for each step's delivery.
…ng curve only when it is at most a frame longer than the run before it

astracat found that the sub-step credit let pooledBusyProof pass a rAF loop that steps a fixed 1 or 2 px a frame: in each 70 ms block the cast still shows the loop's last steps, then it stands still and goes on one step a frame without catching up, so its run ended in a single step. With the previous busy.ts the self-test's frame-counted loop passed the pooled proof in 5 of 5 Chromium runs.

A slowing curve lengthens its runs a frame at a time, so a run now counts as moved only when it is at most one frame longer than the component's run before it; a stall is longer than the runs before it. A judged part with no change reads the finest cast step, 0.5 px, so a larger jump never passes for one step.

The self-test adds the frame-counted loop, at 1 and 2 px a frame, to the pooled proof's mutants: it fails every pool (Chromium 10 of 10, moved on 2 to 8 of 7 to 12 control frames; WebKit 10 of 10, 0 or 1 of 11 or 12). The timed rAF mutant can read as moved on one control frame, presented as the block starts, so its assertion takes 0 or 1 and also requires its stall to exceed the control's (Chromium showed 1 of 40 once).

Red first: the frame-counted stall, the pooled frame-counted loop and the finest-step cases fail on the previous busy.ts. Lightbox's five busy proofs with this judgment: 30 of 30 in Chromium; 33 of 40 in WebKit with no miss, the other 7 presenting too few control frames to judge.

@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 spec decision: the wheel and trackpad pan itself is covered by spec:AST-094 FR43 on main, but this head also writes requirements main does not have (the FR41 selection rule for a claimed mouse or pen drag, component:DateInput FR13, PR3, DD1 and DEC-1, and component:DateTimeInput FR8). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, or rejects the text so it moves to its own spec pull request, and the author may do so as an engineering owner. Whatever that decision, the branch needs these changes before it can be approved:

  • The diff against main carries the whole unmerged stack (232 files, about 41.4k changed lines); land #8480, #8676 and the pull requests beneath them first, or retarget, so this pull request is its own four-file change.
  • When a wheel arrives during the glide, the rows keep the glide's travel and add the wheel's whole step (the isGliding re-base in PickerWheel.tsx), so a trackpad stream slower than the screen's frame rate runs the rows ahead of the fingers; treat a frame as a pause only after the stream's own interval, or drop the glide's travel on a quick resume, and prove it with a stream at half the frame rate.
  • Add iPadOS Safari evidence for the iPad case, a trackpad before and after against main, and this head's base-to-head size report for DateInput, DateTimeInput and PowerSearch.

[Automated review]

Comment on lines +822 to +834
if (stream?.isGliding === true) {
// More wheel after the stream paused: the scroll takes the rows
// back from the glide where they are, and moves them by this
// wheel's step.
const shown = motion.stop();
drag = {
start: fingerOffset(shown, currentLayout) + stream.translation,
};
dragRef.current = drag;
}
const offset = dragOffset(drag.start - translation, currentLayout);
showRows(offset / ROW_HEIGHT, offset / ROW_HEIGHT);
motion.setOffset(offset, {ticks: true});

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 a wheel arrives during the glide, this re-base starts from where the glide has carried the rows (motion.stop()) and then adds the wheel's whole step, so the glide's travel is kept on top of the scroll. Inside a stream whose wheels arrive less often than frames (a 120 Hz screen with a 60 to 90 Hz trackpad stream), every empty frame starts a glide (:760-771) and the rows run ahead of the fingers, about 1.5x at 60 Hz on 120 Hz. Consider treating a frame as a pause only once the stream's own interval since its last wheel has passed, or dropping the glide's travel when a wheel resumes within that interval, and add a proof that drives the stream at half the frame rate.

vjeux added 12 commits October 29, 2026 16:42
…its wheels' look and exception; the band note matches the drawing

- DateInput's DD1 and PR3 now cover its own month and year wheels; the
  TimeInput and DateTimeInput sheets record their wheels' UIPickerView look
  (DD1) and the INV7 exception for the labels drawn three times (PR2) in
  their own records, which own those sheets. TimeInput's sheet has 250
  elements under a finger against 286 under a mouse; DateTimeInput's open
  sheet 377 against 470.
- The band-magnification note in picker.ts says what UIKit draws and what
  the wheel draws: each band label magnified about its own centre, as
  wheelColumns.ts records. The value is unchanged.
… long a frame takes

The proof's guard compared the longest frame gap of two openings under CPU
throttling, and the load on the machine running it moved that gap more than
the wheel did: at one head the 200-year opening's worst gap ran from 266 to
516 ms, against 183 to 233 ms for two years, so the guard failed now and
then on a busy machine. It now counts the year wheel's elements the opening
writes or adds, from the tap until the wheel rests: 1 for two years and 19
for 200, the same on every run. A cost per year shows as 200 or more.
Counting needs no CPU throttling, so the proof runs in WebKit too. It waits
for the wheel to rest instead of a fixed 1.5 s, and the sheet's wait no
longer swallows a failure.
…il cancelled, and its compositor run is blocked only once on the compositor

C23 saw the control self-test fail under load at eight workers: "660 ms judged at 16.6 ms/frame: 28 frames presented, 0/28 moved". Its animations ran once, for 1,600 ms from before the screencast started, so on a loaded runner, where starting the cast takes long, the compositor's animation had ended before the block and nothing moved in the judged part.

Both animations now go back and forth until cancelled, as the pooled self-test's do, and the compositor's block uses `during`, so it starts only once that animation runs on the compositor.

Red first: with the setup stretched to 1.8 s, as a loaded runner takes, the previous test fails with exactly that read (0/39 moved) and this one passes. Normal runs at eight workers: 10 of 10 in Chromium and 10 of 10 in WebKit.
… hesitation drops the glide's travel

A trackpad can send its wheels less often than the screen draws: at 60 Hz on
a 120 Hz screen, every other frame has no wheel. Taking each such frame as
the stream's pause started a glide there, and the next wheel added its whole
step to where the glide had carried the rows, so they ran ahead of the
fingers: six 24 px steps every other frame moved them 170 to 187 px instead
of 144.

The pause is now the first frame one and a half of the stream's own
intervals (the longer of its last two gaps, read from when each wheel was
sent) after its last wheel. A wheel that comes within three intervals,
during a glide that a hesitation began, resumes the same scroll: the glide's
travel is dropped, and the rows hold where it left them until the scroll
passes it, so they never step back. A longer pause still keeps the glide's
place, as before.
…pending owner approval [skip ci]

FR13 and DEC-1 settle where a wheel rests. The turn of a shown wheel onto a
value changed from outside, as when a bound clamps the time after another
wheel turned, sat inside FR13 and DateTimeInput FR8, so both read it as
settled by DEC-1, which doesn't decide it. It is now DateInput FR14, the
shared wheel's own requirement, a verified observation pending owner
approval; DateTimeInput FR8 keeps only the rest, and the two tests that prove
the turn cite FR14.
…nst a compositor-only control [skip ci]

The 1 s busy proof marked one row and asked that every frame the compositor
presented show it moved. Two things made it fail on a busy machine. Late in
the block the coast moves about a pixel a frame, under the screencast's
step, so a frame delivered early shows no change; and over a whole second
the coast travels further than the column shows, so the marked row was out
of sight at one end, turned and faded at the other.

It now follows markers that ride the strip itself, every three rows in three
colours, placed before the flick so the block meets them already on screen.
They move with the coast and never turn or fade, and each is judged while
within reach of the band; together they cover the block. The judgment is
busyWindowProof's against a compositor-only control recorded in the same
frames (meta#8906): the coast moves on every frame the control moves, a run
under one step that a single step ends counts as moved, and its longest
stall is no longer than the control's.
…wheel-uikit-look

# Conflicts:
#	packages/core/src/DateInput/DateInput.spec.md
… into uikit/c21-wheel-uikit-look

# Conflicts:
#	packages/core/src/DateInput/__tests__/Wheel.interaction.browser.spec.ts
#	packages/core/src/DateInput/__tests__/WheelActivation.interaction.browser.spec.ts
…R14 [skip ci]

meta#8131 made the turn its own requirement, DateInput FR14, pending owner
approval; the wheel's park note and its test said FR13.
…ip's animation [skip ci]

They counted the frames until the rows showed a new place after the
scroll, which adds the frames a busy machine takes to show an animation's
first step to the wheel's own decision. They now read the frame at which the
glide's animation runs on the strip, which must be the stream's pause or the
frame after it, and then that the rows moved on.

@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 spec decision: the wheel and trackpad pan itself is covered by spec:AST-094 FR43 on main, but this head also writes requirements main does not have (the FR41 selection rule for a claimed mouse or pen drag, component:DateInput FR13, FR14, PR3, DD1 and DEC-1, component:DateTimeInput FR8, PR2 and DD1, and PR2 and DD1 in component:TimeInput). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, or rejects the text so it moves to its own spec pull request, and the author may do so as an engineering owner. Whatever that decision, the branch needs these changes before it can be approved:

  • The diff against main carries the whole unmerged stack (234 files, about 42.9k changed lines); land #8480, #8676 and the pull requests beneath them first, or retarget, so this pull request is its own five-file change.
  • The hesitation hold in PickerWheel.tsx takes its direction from the glide, so a slow scroll whose pause snaps back to the row behind, or a scroll that reverses right after a pause, stays pinned where the glide stopped; take the direction from the scroll and add tests for both cases.
  • Add iPadOS Safari evidence for the iPad case, a trackpad before and after against main, and this head’s base-to-head size report for DateInput, DateTimeInput, TimeInput and PowerSearch.

[Automated review]

);
drag = {
start: drag.start,
hold: {offset: shown, direction: Math.sign(shown - pan)},

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 hold takes its direction from the glide (shown - pan), and each later wheel keeps the rows at held while (offset - held) * direction < 0 (:879-887). That only holds back a glide that ran ahead of the scroll. When the glide went against the next wheel, as a slow stream under picker.release.minimumSpeed does when it snaps back to the row behind (wheelPhysics.ts:373-387), or when the person reverses right after the pause, every resumed wheel satisfies the test and the rows stay pinned while the fingers keep moving. Consider taking the direction from the scroll (the sign of the resumed wheel’s step), holding only when the glide stopped ahead of the scroll, and adding jsdom cases for a slow stream that hesitates less than half a row past a row and for a reversal after a hesitation.

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants