Skip to content

feat(press): a mouse's or a pen's press paints at once through the press controller - #8682

Draft
vjeux wants to merge 1938 commits into
meta:mainfrom
vjeux:uikit/c01-press-paint-fine
Draft

vjeux wants to merge 1938 commits into
meta:mainfrom
vjeux:uikit/c01-press-paint-fine

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Stack: on #8665 and #8118. Review this PR alone: vjeux/astryx@uikit/c01-press-paint-base...uikit/c01-press-paint-fine

User need

Part of the UIKit-parity work, which makes Astryx's presses behave the same on every device. With a mouse, a button's pressed color vanishes the instant you let go; under a finger it fades out over 200 ms. A pen waits 150 ms before a list row shows its press, like a finger.

Problem and solution fit

What led to it: #8646 asks for one press for every pointer (spec:AST-094 FR43), but the press controller handled only fingers, so a mouse fell back to CSS :active, which has no release to fade.

Now the mouse's main button and the pen go through it: the press shows at once, holds while down and fades on release. On a phone or tablet a mouse's release is a finger's (a control's 250 ms); a Space press keeps 200 ms. A right click, a Mac ctrl-click and a pen's barrel button show nothing; a link in a pressable row presses itself, not the row.

Current specification

spec:AST-094 FR11, FR43, DEC-21; component:Button FR15.

Public delta and caller burden

No API change.

Design and alternatives

  • A CSS fade out of :active: CSS can't animate a state that has ended.
  • A press handler per component: the same logic in dozens of places.
  • Chosen: the existing controller for every pointer (DEC-21). One place decides, and every press style already reads its state, so no CSS changes. The cost: a mouse press needs JavaScript, which every Astryx pressable loads on mount.

Testing

To check by hand, in Storybook on a laptop:

  1. Button story: press and hold with the mouse. It darkens at once and stays dark if you drag off; on release it fades over about 0.2 s.
  2. Right-click (ctrl-click on a Mac): nothing lights.
  3. In a list row with a link, click the link: only the link presses.
  4. With a pen, tap a row in a scrolling list: it lights at once.
  5. With reduced motion on, the press ends on release without animating.

What was run: a new browser test (FinePointerPress.interaction.browser.spec.ts) does 1, 2, 3 and 5 with a real mouse, in Chromium and WebKit. Before this change 6 of its 8 checks fail; after, all pass. Unit tests in utils/pressFeedback.test.ts cover dragging off, pens and the ctrl-click; utils/pressFeedbackMotionRoles.test.ts covers the flash and release clocks reading their roles. The existing press tests pass: the targeted interaction specs (Chromium 49 pass, 1 skip; WebKit 36 pass, 14 skip, twice each), the press a11y specs (37), pnpm test:ui (16,853 of 16,855; the two are MultiSelector's and Selector's lazy bottom-sheet cases, a load red #8642 fixes) and the node project, whose only failures fail the same way on main locally (story-tree.test.ts, release/cache.test.mjs's lock cases). The local gates are the evidence: pushes carry [skip ci], so no CI run is expected.

Evidence

  • Size (gzip): Button JS 46.9 → 47.2 KB (+293 B), CSS +0.

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.

Writing rubric results

component:Button FR15 now covers the mouse and pen. R1–R6 pass: no narration, research, references, rejected alternatives or open questions.

vjeux added 30 commits October 27, 2026 00:02
…px whatever the element's width) into the experimental touch exports
… move keeps that finger until it lifts) into the experimental touch exports
…than the band (spec:AST-094 FR18, DEC-22)

FR18 as amended in meta#8631 (DEC-22): the band's pan-x claim and 15 px start
hold only where the screen-edge pan is attached to an element within the
band; on an element that reaches beyond it, whose touch-action cannot
claim per touch, a touch in the band decides at FR19's 10 px like every
other claim. This restores the 10 px decision 3d85c30 removed and keeps
15 px on an element within the band. The cases pin DEC-22 both ways (a wide
element with a touch at x=4 begins at 10, an in-band strip at 15, and from
several edges at 10 on a wide element); the 10 px cases fail on 3d85c30.
…he edge band decides at 10 px again, DEC-22) into the experimental touch exports
…ouch decides at 15 px on an element within the band and at 10 px beyond it, DEC-22; meta#8641 bec2283) into meta#8270
…8641's moving-touch claim kept on this line's internals delivery) into meta#8315

# Conflicts:
#	packages/core/src/gestures/GestureArena.ts
The case walked ContextMenu, DropdownMenu, useLongPress and useMenuPress's
imports and failed on the UIKit gestures table: it kept that table out of
the menus' bundles, the lazy design the no-lazy-load ruling and direct
imports retire. The long-press numbers keep one source, still pinned by the
table, recognizer and hooks case and by the hooks reaching longPress.ts.
…8589's watcher test reads its end; the gestures-table gate retired) into meta#8360
…8589's watcher test reads its end; the gestures-table gate retired) into the every-pointer host
…xposes no ontouchstart is taken once

Chromium fixes ontouchstart when the page is created, so turning touch
emulation on later sends Touch Events without it: the host reads Pointer
Events for the finger and must not read its Touch Events too. A one-finger
long press fails here, the finger tracked twice.
…from their pointer events

install() reads a finger from its pointer events when the window has no
ontouchstart, but the four touch handlers still took the Touch Events a
browser sends anyway (Chromium after touch emulation turns on), tracking
the finger twice, so a one-finger long press never recognized. Each touch
handler now returns while usesTouchEvents is false, after noting the
fingers down, as install()'s comment promised.
…d tracking listeners) onto F2's fZ (meta#8360 23ade34: the controller on the shared input stream, FR24's touch-only gate)

Conflicts in utils/pressFeedback.ts and its test, resolved as one
controller: meta#8118's structure (a finger slot and a button slot; two
arming listeners at rest, the rest attached while a press is tracked)
with fZ's listener plumbing. Pointer, touch, drag, scroll, scrollend and
visibility input come through subscribeInput on the window's capture
phase; the keyboard stays on the window's bubble phase, outside the
stream, so a Space press sees the target's preventDefault. The finger's
touch-only listeners (touchend, touchcancel) are on only while a press
is tracked and the primary pointer is coarse, following it (FR24), and a
finger counts as down only where its lift is heard. Tests: meta#8118's
brake and listener cases run on a coarse primary pointer and read the
stream's window listeners; fZ's touchcancel case dispatches an event
with an empty touch list, as a browser's has.
…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 events) into the every-pointer host

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
… (gestures for every pointer) and meta#8118 through the union helper (496efa6)
vjeux added 5 commits October 29, 2026 15:01
…ame window

Both 'a dip held before its first frame lands once the hold is …' proofs
waited for no running animation, then one frame and a timeout, assuming
the next step starts within a frame. The dip lands on a timer due on the
call's own clock, though, and the proofs passed only because the evaluate
round trips covered the gap: in WebKit on a loaded box the window closed
first and the proof read the element mid-dip (opacity 0, translate none;
reproduced 1 in 4 with the round trips removed). They now wait on the
call's own finished, then on the browser's own animations still running
or waiting for their first frame, and read nothing from the animator.
…ment at once and settles then

A Reduce Motion dip whose landing runs more than its whole fade back in
late (a hold released long after the dip was due) played a 1 ms opacity
animation, which WebKit keeps pending until its next frame, and settled
the call on a 1 ms timer: the call's finished could resolve while the
element still showed opacity 0. The animator's notes say the element
appears at once there. Fails now: the landing plays one opacity animation
and the call has not settled.
…or the Space key's 200 ms, a list row's 200 ms

Measures each release on a touch-first device (component:Button FR15):
the release animation's own duration, read when `fading` is written;
`fading` lasting at least that clock; and `fading` going within a few
frames of the animation's end. Frames, not ms, bound the end, so a
loaded machine cannot fail it. Passes in Chromium and WebKit on this
head, before the fold onto meta#8117's new head, and is kept through it as
the proof that the fold changes no timing.
…#8890, meta#8360 045f666) into the press release

The release's 250 ms for a finger on a control stays where it was, in
the control press layer's CSS, and a mouse's or the Space key's press
keeps 200 ms through its input marker. The controller's own profile
clock goes with the profiles meta#8117 dropped: the release-end code from
meta#8890 waits for the running release animation, so a finger's release
still ends at 250 ms. This branch's own copy of that code gives way to
meta#8890's.
…element at once and settles

A Reduce Motion landing that runs at least the whole fade back in late
had nothing left of the cross-fade, but still played a 1 ms opacity
animation and settled the call 1 ms later. WebKit keeps that animation
pending until its next frame, so the call's finished could resolve with
the element still at opacity 0. Such a landing now writes the end opacity
at once (handing a borrowed one back to the stylesheet), plays nothing,
and settles the call then, as the animator's notes say.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39
vjeux added 21 commits October 29, 2026 16:05
…caps read the Reduce Motion constant) into the meta#8118 review base
…o the press release

The overlay's reduced-motion release stays this branch's own arms, which
already read the Reduce Motion constant (reducedMotionFadeConsts); the
controller's clock cap and the row layer's take meta#8890's and meta#8077's.
…ot the timing table

From review (an inline comment on meta#8077 about this line): the press
controller, which every pressable component loads, imported the whole
UIKit timing table for one value, its reduced-motion cap, while
architecture:component-style-authoring INV6 ships a component only the
rows of a shared table it uses. It now reads the fade's own export,
reducedMotionCrossFade.durationMs, as the animator does; the value is
the same 200 ms.

Red first: a bundle of pressFeedback.ts carried the table's other rows
(stateChangeMs, buttonReleaseMs) on the change before this one; it now
carries the fade alone.
…th dividers drawn

From review on meta#8077: component:List FR11's verification asks for a
pressed row between two divided rows, and no case used a list with
dividers. A touch case opens the Interactive story with hasDividers,
holds the middle row and releases it, and checks the divider above it and
its own divider below, by computed border and by the pixels of each
divider's strip, while held, halfway through the release fade and after
it; each strip must differ from the row's surface just inside it at rest,
so a match is a drawn divider.

It passes in Chromium and WebKit. A page style that hides a pressed row's
divider fails it while the row is held.
…duce Motion constant

meta#8077's FR4 check (interactionOverlay.cell.test.ts) asks every release
cap in the overlay to read the Reduce Motion constant through a min().
This branch's reduced-motion arms used the constant directly; they now
take min(<the release's clock>, the constant), so a control's 250 ms
and a mouse's or the Space key's 200 ms both cap at 200 ms, as before.
The build's reduced-motion check reads the cap.
… or press profile

The selection hold and the press profiles are gone from the base, so
the Item case and the shared checks' notes no longer mention a held
arm, a cell marker or a profile clock; the checks themselves are
unchanged.
… judges its first frame on that frame's time

'a spring started right before a block plays in full after it' waited a
fixed 300 ms of wall clock after the block, then read the screencast. In
the full interaction run on a loaded box no moving frame had reached the
screencast by then, and the proof failed on an empty read. It now waits
until the page shows the box a quarter of the way along and three more
frames reach the screencast. Its bound changes form, not strength: the
curve's own start (the handle's started) must come after the block, and
the first moving frame may be no further along than a curve started a
frame before the block ended, at that frame's own time, which is the old
30 px for a frame right after the block and still holds when a loaded
machine sends frames sparsely. A curve dated at the call is 107 px in.
) into the meta#8118 x meta#8360 union

meta#8118 now carries meta#8360 itself, so the union is meta#8118's tree; the old
union holds no commit meta#8118 lacks.
…to the press-paint base

The new meta#8118 and meta#8665's current head supersede the old union and the
old every-pointer gesture line; the old base holds no commit they lack.
…into the fine-pointer press paint

Carried regions take the base's text. This branch's own controller
changes are re-applied on the new controller: a mouse's or a pen's press
paints at once through it on every device, a pen never waits or brakes,
a pen's barrel button and a nested control take no surface press, and
only a Space press carries the input marker, so a mouse's release on a
touch-first device is a finger's (spec:AST-094 FR11, FR43). The press
profiles are gone from the base, so the clocks read one role each: the
release's the deselect's (timing.press.highlightOutMs), a longer release
animation being waited for, and the flash its own. The selection-hold
spec this branch edited is gone with the hold.
…e surface

releaseAnimationsOf counted only the press owner's own animations (on it,
or its ::before or ::after). A component that draws its release on a
descendant, such as CheckboxInput's release on its box wrapper's ::after,
then lost the end of it: fading went at the 200 ms clock while the 250 ms
release still ran, and the frame it went relaid and repainted the box.

It now also counts a CSS release animation on a descendant whose nearest
press owner (closest [data-astryx-pressable]) is this element, the same
nearest-owner rule the layers read by, so a press owner nested inside
plays its own press's release and never extends this one.

Red first: a unit case whose release runs on a box inside the row found
fading gone at the clock on the change before this one; it now holds to
the release's end. A second case keeps a nested press owner's release,
on it or inside it, out of the row's.
…ts own timeline

One run measured a 250 ms release whose `fading` lasted 241.6 ms on the
page's clock: an animation's start is a frame's time, which can come
before the write, so a bound on the page's clock read a whole release as
short. The check now takes the animation's start from its own timeline
(when it is ready) and the timeline's time when `fading` goes, and asks
that it ran to its end; WebKit drops the animation at the removal, so
its progress cannot be read there.
…ase counts when this is its nearest press owner, and meta#8360 d0f5fd5) into the meta#8118 review base
…eta#8360 d0f5fd5) into the press release

meta#8890's release end now counts a descendant's release animation when
this element is its nearest press owner, so a control whose layer is a
child's (a checkbox's box wrapper) keeps `fading` until that layer's
release ends.
…ndant release) into the fine-pointer press paint

The release clock's reduced-motion cap reads the Reduce Motion fade's
own export, as meta#8890 now does, beside this branch's role-read clock.
…ming table

The flash and release clocks resolved their roles from the timing table's
press group, which bundled the whole group into the controller: meta#8890's
check that the controller ships the Reduce Motion fade and none of the
table's other rows failed. They now resolve the same keys from a local
table holding the two values the timing table does (100 ms, and the
stylesheet's 200 ms release), so the clocks read their roles and only
those values ship (architecture:component-style-authoring INV6). [skip ci]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on hold for an engineering owner's decision on two spec points: the rewrite of component:Button FR15 to a 250 ms release for every pointer under a coarse primary pointer (with a new verification row), where FR15 on main gives every press 200 ms, and the new rule that a press on a nested link or button leaves its row unpainted, which no current spec states. An engineering owner records each decision by commenting /approve-spec <full head SHA> on this pull request, or by naming the governing spec; the author may do so as an engineering owner. Independently of that decision, these need changes before merging:

  • surfaceAt takes a row's first control as its activator, so a TreeList parent row with onClick or href shows no press when its label is pressed (packages/core/src/utils/pressFeedback.ts:632-640).
  • After a desktop click the hover overlay stays hidden until the release clears about 200 ms later, and under Reduce Motion the hovered control shows its resting fill for that time (packages/core/src/utils/interactionOverlay.stylex.ts:324-325, :346-347).
  • usePressFeedback.doc.mjs (lines 23, 28 and 61), the buttons family's press row, the stacked press-touch-down-highlight changeset and the input-marker comments still describe the old desktop press.
  • The diff against main carries the unmerged stack under #8665 and #8118; please land those and rebase until this pull request is only its own 15 files.

[Automated review]

control != null &&
control !== found &&
found.contains(control) &&
control !== found.querySelector(ACTIVATOR)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The first control in tree order is not always the surface's activator. A TreeList parent row renders its expand toggle before its own button or link, and an Item renders startContent before its invisible button, so a press on those rows' labels returns null here and the row shows no press, while a press on the toggle paints the whole row. Could the surface mark its activator explicitly? A test with a control before the activator would hold it.

':hover': hoverImage,
':active': pressedImage,
},
...pointerImageArms,
[PRESSED_ON]: pressedOverlayImage,
[PRESSED_FADING]: pressedOverlayImage,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With a hovering mouse this fading arm replaces the hover image with a press paint that fades to nothing, so after a desktop click a hovered control drops below its hover overlay within about 40 ms and gets hover back only when the attribute clears at about 200 ms; under Reduce Motion it shows its resting fill for those 200 ms. Keeping the hover layer under the fading press for a hover-capable pointer would avoid it.

},
],
usage: {
description:
'The touch press model every Astryx pressable uses, for a local component that paints its own press. Under a finger, CSS `:active` paints on the touch itself and can outlive the start of a scroll; the press model instead waits 150 ms before believing a press, cancels it the moment the finger travels 10 px or a scroll claims the gesture, never brings it back inside that gesture, answers a quick tap at the lift, and fades the release over 200 ms — the clocks a native list uses. One document-level controller does this for every marked element (installed by the first pressable to mount, removed by the last to unmount), so calling the hook adds no listener or state to your element. On a desktop a mouse press is still your `:active` rule: keep it, and drop it under `@media (pointer: coarse)` the way the built-in components do. That query reads the primary pointer, so on a phone or tablet it also drops `:active` for a connected mouse, trackpad or keyboard; the controller paints those presses there (at once, faded at the release), so nothing goes unanswered.',
'The touch press model every Astryx pressable uses, for a local component that paints its own press. Under a finger, CSS `:active` paints on the touch itself and can outlive the start of a scroll; the press model instead tracks the touch the way UIKit tracks a control: it highlights as the finger lands (after the content-touch delay inside a scroller, where a flick paints nothing), a quick tap answers at the lift, and a scroll that takes the touch cancels it with no fade. The release fades over the 250 ms of a UIButton (ease-in-out) under a coarse primary pointer and over 200 ms with any other; under reduced motion it is an opacity cross-fade of 200 ms or less on a touch screen and immediate otherwise. One document-level controller does this for every marked element (installed by the first pressable to mount, removed by the last to unmount), so calling the hook adds no listener or state to your element. On a desktop a mouse press is still your `:active` rule: keep it, and drop it under `@media (pointer: coarse)` the way the built-in components do. That query reads the primary pointer, so on a phone or tablet it also drops `:active` for a connected mouse, trackpad or keyboard; the controller paints those presses there (at once, faded at the release), so nothing goes unanswered.',

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 still tells builders "On a desktop a mouse press is still your :active rule: keep it", and lines 23 and 61 say the same, but the controller now paints a desktop mouse press itself, as line 38 says. Could these three strings describe the controller's mouse and pen press?

| FR12 | With `tooltip`, the tooltip attaches to the Button element itself (no wrapper element), opens above it, and its content is added to `aria-describedby` after any caller value. | Current source; real-browser accessibility test |
| FR13 | A standalone Button paints the shadow token for its `elevation`. Inside a ButtonGroup it paints no shadow and reflects `data-elevation="none"`, whatever `elevation` says. | `family:buttons` FR8–FR10; current source and tests |
| FR14 | The root carries `astryx-button` and reflects the effective `data-variant`, `data-size`, and `data-elevation`. | `family:buttons` FR10; `architecture:component-theming-surface`; tests |
| FR15 | Hover paints the shared interaction overlay on hover-capable pointers, and a press paints the shared pressed overlay through the press controller: at once under a mouse or a pen (`spec:AST-094` FR11's fine-pointer profile) and on FR11's control profile under a finger, fading out over 200 ms on release at `tempo` 1, scaled by the theme's `tempo` (`spec:AST-108` FR4). Under a coarse primary pointer the overlay paints on Button's own `::before`, present while pressed and through its fade-out, and a finger's, a mouse's or a pen's press fades over UIButton's measured release instead (250 ms at `tempo` 1, `timing.press.buttonReleaseMs`); a Space press keeps the 200 ms fade, as every press does with a fine primary pointer (`spec:AST-017` FR59). Without a reduced-motion preference, a standalone Button also scales to 0.98 on a mouse press, or while the touch press reads on; under reduced motion it MUST NOT scale (`design:reduced-motion` DR1). All of this paint follows the element's exposed disabled state: a Button that is natively disabled or exposes `aria-disabled="true"` (busy-only, disabled with a tooltip, or a caller's own `aria-disabled`) shows no overlay and no press scale, even when it stays operable. Inside a ButtonGroup the press scale does not apply. Button's hover and press transitions are fast. Under reduced motion they are instant with a fine primary pointer, and a release under a coarse primary pointer cross-fades within 200 ms instead (`design:reduced-motion` DR2, DR3). Implementation gap: the 0.98 press scale also applies under reduced motion; only its transition becomes instant. | Current source and tests; `spec:AST-094` FR11 and DEC-21 for the mouse and pen press |

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 gives a mouse's and a pen's release under a coarse primary pointer UIButton's 250 ms and a coarse-pointer cross-fade under reduced motion; FR15 on main gives every press 200 ms and an instant reduced-motion transition, so the rewrite needs an engineering owner's decision on this head.

'@astryxdesign/core': patch
---

[fix] Under a finger, a pressable (a button, card, link, list row or any other surface) now highlights in the frame the finger lands, as UIKit's controls do, instead of after a 150 ms delay. Inside a scroller it still waits that delay, so a flick paints nothing, and a scroll that takes a held press cancels it with no fade. A mouse on a desktop keeps `:active`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"A mouse on a desktop keeps :active." stops being true once press-fine-pointer.md ships in the same release.

const CONTROL_RELEASE_EASE = controlPressConsts.controlReleaseEase;
/**
* A mouse or Space press the controller paints on a touch-first device
* (pressFeedback.ts `PRESS_INPUT_ATTRIBUTE`): it keeps the fine pointer's

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pressButton now writes this marker only for a key, so a mouse press on a touch-first device no longer keeps 200 ms here; the same note at pressFeedback.ts:28-31 and :113-119 and interactionOverlay.stylex.ts:255-256 is stale too.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 2, 2026
…n prelude

StyleX 0.19.0's dev runtime keys a conditional rule whose prelude has more than one parenthesized condition by the prelude alone, so jsdom got only the first rule written under, say, @media (prefers-reduced-motion: reduce) and (pointer: coarse), and a test that read a later one read nothing. vitest.config.ts now names internal/test-utils/src/stylexInject.ts as the babel plugin's runtimeInjection: StyleX's own inject, plus each rule it drops, inserted once into a sheet of its own. Test-only: no StyleX code is patched, and the build keeps every rule as before. It comes out at the StyleX bump to a release that carries meta/stylex#1991, which fixes the key upstream.

Red first: the new stylexMultiConditionMedia test (two styles, and one style with two declarations, under one two-condition query) failed with StyleX's own injection; on meta#8226 + meta#8682 + main, Indicator/selectionMotion.test.ts failed 2 of 6. Both pass with it, and the full ui suite on main has the same results (15,951 passing, plus the new test).

This branch was successfully deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants