Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
|
/approve-spec 578373e |
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
Changes requested. The spec side is settled: an engineering owner approved ContextMenu FR11 at this head. Each item below still needs a fix before this can merge.
- The growth and its early-lift return read the generic
--duration-medium,--duration-fastand--ease-movetokens, whichspec:AST-094FR4 does not allow for touch motion ("MUST be a motion role named for its use"), so amend FR4 in this pull request or move the growth onto named motion roles. - If
isDisabledturns true while a finger-opened menu is up, the touch layer unmounts and disposes its animator without restoring the trigger's inlinescale, so the trigger stays about 7% larger after the menu closes (contextMenuPressFeedback.ts, the unmount cleanup). - ContextMenu now has two outside-press paths, the shared
watchOutsidePressfor touch and its ownmousedownEffect with a different shadow-root rule for everything else, so extend the shared watch to close at the press for a mouse and a pen and delete the Effect (architecture:react-component-runtimeINV16). useContextMenuPressFeedbackruns its own attach, replace and dispose lifecycle for its animator and press observer, so move both ontouseAttachment, which the same layer'suseGestureRecognizeralready uses.- The outside-press changeset says "No component passes these options yet", but ContextMenu passes all three at this head, so reword it.
- The diff against
mainis 4,013 authored lines because it carries #9814 and #8125, so land those first or base this branch on #8125 to leave this pull request's own 1,915 lines.
A few smaller comment and wording notes are inline.
[Automated review]
578373e to
0d1fdb7
Compare
|
/approve-spec 0d1fdb7 |
There was a problem hiding this comment.
Changes requested. The spec side is settled: an engineering owner approved ContextMenu FR11, and that approval still covers this head.
- The growth and its early-lift return play the generic
--duration-medium,--duration-fastand--ease-movetokens, whichspec:AST-094FR1 and FR4 do not yet allow for touch motion, so rebase after #9706 lands and cite its FR45 in FR11, or amend FR1 and FR4 here. ContextMenuTouchLayersubscribes to its recognizer's transitions from an Effect whose only dependency never changes, so add that listener in the recognizer factory passed touseGestureRecognizerand delete the Effect.ContextMenuPressRecognizergains anonPressStartoption that only a unit test passes, and its doc says the press feedback starts from it, so remove the option.- The diff against
mainis 4,132 authored lines because it carries #9814 and #8125, so land those first, or declare the stack with one identicalStack: #9814 → #8125 → #9816line in all three descriptions.
Thanks for fixing the stuck trigger scale and moving the press feedback onto useAttachment. The changeset and the story still say the trigger "springs back" after an early lift, which FR11 now times as a curve on the theme's tokens.
[Automated review]
0d1fdb7 to
caf271c
Compare
|
/approve-spec caf271c |
There was a problem hiding this comment.
Changes requested for this stack: #8125 is ready, and #9814 and #9816 each need one fix. A pull request lands only when it and every pull request below it are approved.
#9814: changes requested
- Miss: the title and
.changeset/outside-press-pan-watch.mdsay the watch can now "judge a press inside a shadow root", but a press inside an open shadow root was already judged through the event's composed path and every new test uses a closed root, so please say "closed shadow root" in both.
#8125: approved
Approved pending CI: CI has not run on this head yet, and it lands after #9814.
#9816: changes requested
- Miss:
PressTimingandtimedCurveincontextMenuPressTiming.ts, andthemeTimingincontextMenuPressFeedback.ts, copyThemeTiming,timedCurveandthemeTimingOffromSegmentedControl/useSegmentedSelectionMotion.ts, so please move them into one shared motion module that both components call. - Non-blocking: ContextMenu FR11 at this head no longer cites the measurement behind the 400 ms commit and 735 ms display, or the tests that verify them, which #8125's FR11 did.
Whole stack
The combined change adds no public API and follows spec:AST-094 and ContextMenu FR11, but it passes only once the two fixes above land.
[Automated review]
b168c85 to
037a530
Compare
|
/approve-spec 037a530 |
A finger that crosses 11.5 px at 650 ms while the page is blocked past the menu's moment, until 760, 800 or 900 ms, ends the hold when its coalesced move arrives, as separate moves do. A still finger behind a block gets its menu once no move comes. Every case fakes the page's clock from 0, and the on-time browser case checks that the moment's own timer task shows the menu.
When the menu's timer runs more than a frame late, the page was blocked, and the finger's moves from before the moment may still be queued. The menu then waits up to 50 ms, counted from when the page runs again, so those moves are judged at their own times first. On an idle page the timer runs on time, and nothing waits. The record's FR16 gap line states the shift and its 50 ms bound.
There was a problem hiding this comment.
Changes requested. The component:ContextMenu FR12 record change is owner-approved at this head, and the menu now opens at 735 ms after an on-time move, which fixes the last review's first item; three items remain.
- On a busy page, a late coalesced move whose earlier sample drifts past 11.5 px before 735 ms still opens the menu, because the arena runs the 735 ms timer for the move's last sample before
followHoldjudges the earlier samples, andisBehindInputsees no recent move after a still hold (GestureArena.ts:360,contextMenuPress.ts:176-195and:259-270), againstspec:AST-094FR16; run timers only up to each sample's time (or judge the samples before the moment), treat a moment timer that runs late as behind, and add both cases beside the waiting ones. spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review, and the description says this head still needs them.- The
Stack: #8125 → #9816 → #8133 → #8201line does not hold, because #8133's head still merges this branch's previous head, so this pull request is judged alone and its 6,068 authored lines get no stack exemption; restack #8133 and #8201 onto this head.
Non-blocking: the ContextMenu record's End-user impact row still says "does not yet grow"; leave that gap to Current-state impact.
It brings meta#8125's fix for a page blocked past the menu's moment: a late menu timer waits up to 50 ms for the moves queued behind the block, so they are judged at their own times first.
The press's units drive the page's time with the gesture arena's own injected clock, the test harness's manual clock, instead of faking performance.now(). A move can reach the page after its own time, and a blocked page runs the menu's timer late on that clock. A blocked main thread until 900 ms now shows the still finger's menu at 950 ms.
The press measured how late a move or its timer ran with performance.now(). It now reads the gesture arena's injected clock, whose new pageTime() returns the clock's own reading. In a browser that clock is performance.now(), so nothing changes there; the units drive it with the test harness's manual clock.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the record changes, and two items need changes.
- The pull request adds
component:ContextMenuFR12 (the growth onsprings.contextMenuLiftfrom 150 ms, the 400 ms commit and the 735 ms display) and a newspec:AST-094"FR16 gap" line, and no engineering owner has approved them at this head; an engineering owner records the decision by commenting/approve-spec <full head SHA>on this pull request, and the author may do so as an engineering owner. spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review, and the description says this head still needs them.- The
Stack: #8125 → #9816 → #8133 → #8201line does not hold, because #8133's head still merges this branch's previous head, so this pull request is judged alone and its 6,155 authored lines get no stack exemption; restack #8133 and #8201 onto this head.
The late, coalesced drift is fixed: a moment timer that runs late now waits for moves queued behind the block, and the new cases pin it. Non-blocking: the ContextMenu record's End-user impact row still says "does not yet grow"; leave that gap to Current-state impact.
It brings meta#8125's final head: the press reads the page's time from the gesture arena's injected clock, which is performance.now() in a browser, so nothing changes there.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the record changes, and two items need changes.
- The pull request adds
component:ContextMenuFR12 (the growth onsprings.contextMenuLiftfrom 150 ms, the 400 ms commit and the 735 ms display) and a newspec:AST-094"FR16 gap" line (the menu can show up to 50 ms after a blocked page runs again), and no engineering owner has approved them at this head; an engineering owner records the decision by commenting/approve-spec <full head SHA>on this pull request, and the author may do so as an engineering owner. spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review, and the description says this head still needs them.- The
Stack: #8125 → #9816 → #8133 → #8201line does not hold, because neither #8133's nor #8201's head contains this head, so this pull request is judged alone and its 6,147 authored lines get no stack exemption; restack #8133 and #8201 onto this head.
Moving the press's lateness checks onto the gesture arena's clock changes nothing in a browser and lets the unit cases model a blocked page directly. Non-blocking: the ContextMenu record's End-user impact row still says "does not yet grow"; leave that gap to Current-state impact.
The press feedback kept every mounted trigger in a module-level set to find the trigger a press mark belongs to. Each trigger now carries the private context-menu-trigger attribute while its press feedback is attached, and closest() finds it. The attribute's owner module is privateAttributes/contextMenuTrigger.stylex.ts.
The Ownership section names the private attribute's owner module, its writer and its closest reader.
ContextMenuTouch.ts only re-exported the press recognizer for the touch layer, its one importer. The layer now imports contextMenuPress.ts.
A finger crosses 11.5 px at 700 ms while the page is blocked from 600 ms to 850 ms, so its move reaches the page after the menu's moment. The hold must still end, with no menu. The case checks that the move reached the page behind the block and within the press's 50 ms wait.
AST-094's current state says why the press keeps a 50 ms wait beside the arena's one-task yield, and names the arena's page time and the touch fields the press reads. The FR16 gap line stays as it was. Layer runtime's outsidePress owner line names ContextMenu's watch and its pressTypes option.
The record now links motion/uikit/contextMenu.ts, the context menu's measured UIKit table, by path. Its public-contract line says that an omitted presentation means the touch model under a coarse pointer, with presentation="popover" as the way back, instead of no change.
A ContextMenu rendered into a same-origin iframe gets a 120 ms tap stamped on that window's clock. It must open no menu and keep its click. The gesture host reads each document's own clock since the second-document clock fix.
component:ContextMenu's Ownership names contextMenuPress.ts and ContextMenuTouchLayer.tsx: the touch hold's recognizer, and the layer that attaches it and opens the menu. Records only, no code.
left a comment
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the record changes, and five items need changes.
- The pull request adds
component:ContextMenuFR12 (the growth onsprings.contextMenuLiftfrom 150 ms, the 400 ms commit and the 735 ms display) with new Ownership and path-applicability lines, plusspec:AST-094's "FR16 gap" and "Two waits" lines, and no engineering owner has approved them at this head; an engineering owner records the decision by commenting/approve-spec <full head SHA>on this pull request, and the author may do so as an engineering owner. - Regression: since main's per-document gesture clock, a ContextMenu in a same-origin iframe or Picture-in-Picture window starts its growth in the first frame and dates it on the page's clock, because
watchFramesFromincontextMenuPressFeedback.tsreads the page'sperformance.now()and frames while the onset is on the trigger's own document clock. privateAttributes/contextMenuTrigger.stylex.tsis a new internal shared module that no current record names, so it needs an engineering owner's API decision (approving the Ownership line at this head records it).- Newly live check:
motion/uikit/contextMenu.tsis a new internal shared module that no current record names, so it needs the same owner decision (the path-applicability line). spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review, and the description says this head still needs them.- The
Stack: #8125 → #9816 → #8133 → #8201line does not hold because #8201's head does not contain this head, so this pull request is judged alone and its 6,328 authored lines get no stack exemption; restack #8133 and #8201 onto this head.
The unit suites this change reaches pass at this head, and a desktop right-click renders the same menu as main. Non-blocking: drop "does not yet" from the record's End-user impact row, and the stopOnsetRef comment restates the name.
| * trigger a press belongs to. No package entry exports it. | ||
| */ | ||
|
|
||
| const CONTEXT_MENU_TRIGGER = 'data-astryx-context-menu-trigger'; |
There was a problem hiding this comment.
This is a new internal shared module (imported by ContextMenu) that no current record names, so it needs an engineering owner's API decision that says why existing modules are not enough. The proposed Ownership line in component:ContextMenu names it, so an engineering owner's /approve-spec for this head settles it.
| * Every leaf has one or more `@source`. A GUESS value also has the `@measure` | ||
| * task that confirms or replaces it. uikit.test.ts enforces both. | ||
| */ | ||
| export const contextMenu = { |
There was a problem hiding this comment.
Newly live check: this is a new internal shared module (imported by ContextMenu's modules) that no current record names, so it needs an engineering owner's API decision. The proposed path-applicability block in component:ContextMenu names it, as component:Switch names motion/uikit/switch.ts, so the same /approve-spec settles it.
The touch layer's header read as if any second finger ended the hold. The recognizer fails on a lift, a second finger or a scroll only before the commit. The timeline's header, the spec's header and one spec comment named a press feedback, its growing copy and its scales, which this code does not have. Comments only.
The press feedback ships here. So the timeline's header names it, its growing copy and its scales, and the long-press spec's header and its scroll helper name it again. Comments only.
A ContextMenu in a same-origin iframe or a Picture-in-Picture window gets its touches on that window's clock. Its growth must start in the first frame at or after that clock's onset, 150 ms after the landing.
The gesture host times each document's touches on its own window's clock, so a trigger in a same-origin iframe or a Picture-in-Picture window reports its landing on that clock. The press feedback still watched the page's frames and clock, so the growth started in the first frame, dated on the page's clock. It now reads the trigger's window for its frames and clock. For another window, its animator runs on that window's clock and frames; the page's own document keeps the defaults. A comment that only restated a name goes.
The touch grow and the trigger lift are a gap that Current-state impact states against FR5. So the End-user impact row drops its "does not yet" sentence. Records only.
The block repeated what the hook's and the touch layer's JSDoc say. One line stays: only the recognizer's failure or cancel returns the growth. Comment only.
left a comment
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the record changes, and four items need changes.
- The pull request adds
component:ContextMenuFR12 (the growth onsprings.contextMenuLiftfrom 150 ms, the 400 ms commit and the 735 ms display) with new Ownership and path-applicability lines, plusspec:AST-094's "FR16 gap" and "Two waits" lines, and no engineering owner has approved them at this head; an engineering owner records the decision by commenting/approve-spec <full head SHA>on this pull request, and the author may do so as an engineering owner. privateAttributes/contextMenuTrigger.stylex.tsis a new internal shared module that no current record names, so it needs an engineering owner's API decision (approving the Ownership line at this head records it).motion/uikit/contextMenu.tsis a new internal shared module that no current record names, so it needs the same owner decision (the path-applicability line).spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review, and the description says this head still needs them.- The
Stack: #8125 → #9816 → #8133 → #8201line does not hold because #8201's head contains none of the pull requests below it, so this pull request is judged alone and its 6,406 authored lines get no stack exemption; restack #8133 and #8201 onto this head.
The growth in an iframe or Picture-in-Picture window now starts at that window's own onset, and its new case fails before the fix. The unit suites this change reaches pass, and a desktop right-click renders the same menu as main.
Stack: #8125 → #9816 → #8133 → #8201
Merge group: #8125, #9816, #8133
Behavior module: ContextMenu's press feedback.
User need
A held finger should see the trigger grow from 150 ms, as UIKit's view does, before the menu at 735 ms.
Problem and solution fit
springs.contextMenuLift.springs.contextMenuDismisswhen the hold ends with no menu or the menu closes, and holds while the menu is up.architecture:layer-runtimeINV22).Current specification
component:ContextMenuFR5 (since #9915) makes the long press a gesture component: the lift playssprings.contextMenuLift, the returnsprings.contextMenuDismiss(spec:AST-094FR7, FR45). FR12 adds the growth on those springs.design:reduced-motiongoverns.Public delta and caller burden
No API change. A block trigger grows about 7%; its docs ask for room. Its children sit in one inner box, laid out as before. ContextMenu carries core's motion engine eagerly, like 10 components on main. Eager JS gzip (
example-vite), over #8125: +14.7 KB with one ContextMenu; +1.2 KB after a Switch.Design and alternatives
Evidence
spec:AST-094Platform support:Scope
Writing rubric results
Over
component:ContextMenu: FR12 adds the growth on FR5's springs beside the hold's internal moments; Ownership names the trigger mark's module. Owner-gated.Testing
This round, at 8fb305f, from review:
Local checks at 1d6ec37, one worker (8fb305f changes one comment):
test:ui --changed): 485 files, 14,214 passed;The previous round, at 850a9c2:
data-astryx-context-menu-trigger, written and read through one owner module. A nested trigger still grows alone.component:ContextMenu's Ownership names that module, its writer and its reader.ContextMenuTouch.tsgoes, so the touch layer importscontextMenuPressandcontextMenuPressFeedbackdirectly. A press that ends with no menu springs back and releases what the commit mounted. The long-press spec keeps both pull requests' cases.Local checks at 850a9c2, one worker:
test:ui --changed): 485 files, 14,213 passed;The previous head, 0b1f31c, merged #8125's head cae3644 with no conflict. There the press reads the page's time from the arena's injected clock, which is
performance.now()in a browser. Before it, 2a36eba merged #8125's 45bfb44. There a menu timer that runs more than a frame late waits up to 50 ms for moves queued behind the block. 782cdbd merged #8125's cad79ec. There the press judges each coalesced sample at its own time. The menu waits at its moment only when the page is behind its input, so an on-time finger gets it at 735 ms. e25ec8c merged #8125's 88cc0d8, and its one conflict dropped the touch layer's pointer follow. c346da0 merged #8125's a4e0404, and three commits answer the review of 2ef1714:Under a fine pointer, all 31 ContextMenu story captures match main's pixel for pixel: 18 stories at rest, 13 with the menu open. That ran at 55f84be, whose
ContextMenu.tsxdiffers from this head's in one comment line. The change adds no publicdata-*attribute; 4e93b5f's privatedata-astryx-context-menu-triggerhas its owner module (INV23).Local checks at 0b1f31c, one worker:
The previous head, 2ef1714, sat on a merge of #8125's head a5a0e12 (704cff3). The one conflict put two new browser cases side by side in the long-press spec. Three commits follow:
architecture:layer-runtimeINV21 and INV22 (spec:AST-059DEC-9 and DEC-10, from#11024). A finger's or a pen's press leaves the open menu as an unpressed open. A layout case checks block, row and grid triggers against plain boxes.Red first, at fb27cdc (the same tree as 06c942c), in Chromium and WebKit: the press cases fail. The menu's box matched, but the trigger above the anchor carried scale 1.0735, and the anchor carried an inline counter-scale and translate. The layout case passed, as it should with no inner box.
Local checks at 9de7a99, which differs from this head only in that changeset sentence, one worker:
Before and after, a finger held near the card's corner, at 450, 900 and 1,400 ms. The top row is before the inner box, the bottom row this head. The menu opens at the same place under the finger, and only the card grows: capture.
At earlier heads, below, "this head" means cc254d7.
Android exit timing (contextmenu-default): main none/70 ms, head none/90 ms (6 interleaved rounds; main max gap 107 ms)Frames, not timers, decide the onset: Chromium holds a page's timers for about 100 ms after it handles a touchstart.
Replayed onto main cd748d0 after #9814's squash: 1cedda2 carries this pull request's earlier own diff (old head 63c7a6c; its message lists the folded commits), and the newer commits sit on top of it. It sits on #8125's 3d78dae, which now carries the menu's commit inside its deciding task and its commit-time mount; with those, this head's ContextMenu files equal 0c316d9's plus #8125's second-document test, its 735 ms proof, its PR1 sentences and one lint-directive comment line, so the menu behaves the same; the tree also carries main 92784f6, which #8125 merges; c9d07a9 and cc254d7 merge #8125's heads. FR8 keeps main's text from #10160 in this pull request's table. Red first: 791357d (a touch 90 ms late, 3 of 3 red before the frame watch).
Touch to first grown frame, Chromium phone profile: 170–173 ms at normal speed, 164–182 ms at 4× CPU, and 166–167 ms with a 60 ms input lag (183 ms, at 1.0110, before the frame watch). Each first frame shows the spring's one-frame value, 1.0033, after an exact 1.000. A quick tap starts no animation and writes no style. The touch handlers' cost does not change: 1.50 / 1.85 ms at 1× (1.50 / 1.70 ms before) and 6.60 / 7.95 ms at 4× (6.70 / 8.30 ms before).
A lift between the commit and 735 ms shows the menu 2–3 ms into the lift's own dispatch in WebKit (17 ms before #8125's commit-time mount). The lift proof, d44956d, asserts that dispatch, not a time. Under Reduce Motion, the 735 ms proof also wants the show inside its deciding task (4edb900).
App-level size, eager JS gzip of the
example-vitepage in a Vite production build, #8125's head 69e6dd8 to 8fb305f: no change as shipped; +14.7 KB with one ContextMenu; +1.2 KB with one ContextMenu and one Switch.lazy-sheet-bundle.test.mjskeeps #8208's sheet andrideKeyboardon demand; ContextMenu's row now expects the animator eager, since its growth needs the animator at the touch.Local checks at an earlier head, cc254d7, which adds only #8125's two record sentences: check:knowledge. At c9d07a9:
ContextMenuLongPressin Chromium (21 passed, 1 skipped) and in WebKit under the CI profile (17 passed, 5 skipped). At 343df06, which differs only in #8125's 735 ms proof: core typecheck; the ContextMenu, Layer and DropdownMenu units (1,135 passed);CI=true pnpm lint(0 errors); check:knowledge. At the stack's top on the same main: the ui suite (22,774 passed;DropdownMenuLazySheet's keyboard-open case failed once under load and passes alone on main and here) and test:node (17,446 passed; the one failure is the WebKit leg budget, see #8133). Earlier versions of this description saidCI=true pnpm lint, but those runs werepnpm lint, which reports@eslint-react/dom-no-flush-synconly as a warning; that claim is corrected here.The Android run at an earlier head, cc254d7 (Chrome with Gboard; normal, 4× CPU and animations removed): the growth starts 151 ms after the landing (164 ms at 4×), with no back-dated start, and peaks at 1.0746; an early lift returns in 314 / 327 ms and the return as the menu closes takes 297 / 307 ms; with animations removed there is no growth and no return. Earlier, at 0c316d9 (Chrome with Gboard; normal ×2, 4× CPU ×2 and animations removed ×2), whose ContextMenu files this head carries apart from one lint-directive comment line (it also carries main's own changes): the growth animation's start time is 152–165 ms after the landing, and it starts from 1.000 (first frames 1.000, then 1.002–1.005), with no back-dated jump. Both returns take about 285–354 ms, and Reduce Motion shows no growth. At 63c7a6c, an earlier head, the animation was created about 238 ms after the landing with a back-dated start, so its first painted frame was already at 1.016–1.039 after an exact 1.000.
Public and internal API delta
Public: none.
Internal, new:
privateAttributes/contextMenuTrigger.stylex.ts: the owner module of the privatedata-astryx-context-menu-triggerattribute (architecture:public-component-apiINV23), named incomponent:ContextMenu's Ownership.markContextMenuTrigger(trigger)marks a trigger while its touch press feedback is attached and returns the call that unmarks it;closestContextMenuTrigger(element)finds the trigger a press belongs to. Usage:const unmark = markContextMenuTrigger(trigger);.Why: a press on a nested trigger grows only that trigger. Existing ones considered:
data-astryx-pressable, which also sits on every pressable control inside a trigger. So the nearest one can be a nested control, not the trigger. A module-level set of mounted triggers came before; the mark replaces it.ContextMenu/contextMenuPressFeedback.ts:useContextMenuPressFeedback, the trigger's growth (component:ContextMenuFR12), used only by the touch layer. Usage:const feedback = useContextMenuPressFeedback(triggerRef, visualRef, isShown, isFollowedRef, landedAtRef, followsFingerRef);.Why: the growth's timing is ContextMenu's own. It starts 150 ms after the landing the recognizer records, holds while the menu is up, and plays UIKit's springs. Existing ones considered: core's press machine (
utils/pressFeedback.ts), whose press mark the hook reads for a pen's press. The hook watches that mark through the shared mutation observer (utils/sharedMutationObserver.ts), scoped to the trigger. An engineering owner's decision on a callback from the press path instead is pending.Lines, over #8125's head: product +544/−17 (
contextMenuPressFeedback.ts+360,ContextMenu.tsx+59/−5,ContextMenuTouchLayer.tsx+57/−4,motion/uikit/contextMenu.ts+35/−8,privateAttributes/contextMenuTrigger.stylex.ts+27,contextMenuPress.ts+6), tests +2,155/−122, records +39/−34, docs +19/−4, story +119/−1, changesets +1/−1.