Skip to content
Draft
Show file tree
Hide file tree
Changes from 140 commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
3dd581e
feat(core): one progress module for the motion foundation
vjeux Nov 3, 2026
3cfb3b1
refactor(animatedLayout): take the settle spring's progress from prog…
cixzhang Nov 3, 2026
273da1e
feat(core): the rect morph's paths, the rect a grow-out presentation …
vjeux Nov 3, 2026
f1149cd
Merge remote-tracking branch 'origin/main' into motion/morph-path
cixzhang Nov 3, 2026
131ca14
feat(core): one rect morph for touch presentations that grow out of a…
vjeux Nov 3, 2026
f19df27
fix(rectMorph): a handed-in animator stays on the element through a c…
cixzhang Nov 3, 2026
e3a43d0
fix(motion): one timed curve keeps a retargeted velocity, for the pag…
cixzhang Nov 4, 2026
c99210a
fix(rectMorph): a timed motion from rest plays its easing as given
cixzhang Nov 4, 2026
3272828
docs(rectMorph): the timing follows the component's motion record
cixzhang Nov 5, 2026
2bcc49a
feat(ContextMenu): open from a held finger on iOS 26's timeline
vjeux Nov 7, 2026
812d3a4
test(ContextMenu): the hold's menu commits in its deciding task (red)
vjeux Nov 7, 2026
0776b40
fix(ContextMenu): the hold commits its menu in the task that decides it
vjeux Nov 7, 2026
e8f5ca1
test(ContextMenu): the menu mounts, closed, at the hold's commit (red)
vjeux Nov 7, 2026
0fbd309
fix(ContextMenu): the hold mounts the menu at its commit, closed
vjeux Nov 7, 2026
32e75c7
test(ContextMenu): a mouse press closes the menu in its own document
vjeux Nov 7, 2026
ba77ca8
Merge commit 'c8b4cba38181263ffa8fcc897975b61154cb91b0' into HEAD
vjeux Nov 7, 2026
df6fd04
docs(ContextMenu): PR1 names the outside-press watch and the touch query
vjeux Nov 7, 2026
1cedda2
feat(ContextMenu): a held finger grows the trigger on the lift spring
vjeux Nov 7, 2026
7f9d428
test(ContextMenu): each animation hold names the motion it judges
vjeux Nov 7, 2026
791357d
test(ContextMenu): a late touch grows from rest at its onset (red)
vjeux Nov 7, 2026
6fd83b2
fix(ContextMenu): a frame, not a timer, starts the touch growth
vjeux Nov 7, 2026
d44956d
test(ContextMenu): the lift shows the menu inside its own dispatch
vjeux Nov 7, 2026
409e581
test(ContextMenu): the menu loads the animator at once for its growth
vjeux Nov 7, 2026
abb70e1
test(ContextMenu): a starved page fails the 735 ms proof by name
vjeux Nov 7, 2026
983e46f
feat(ContextMenu): the finger that opened the menu slides to an item
vjeux Nov 7, 2026
4edb900
test(ContextMenu): Reduce Motion's menu shows in its deciding task too
vjeux Nov 7, 2026
b8a9055
Merge commit '62c7802e873ed3d020192e8218e5e1bcd4f8fad2' into HEAD
vjeux Nov 7, 2026
8130a0b
Merge commit '8da39a830b6bc4b8d3dc93e9192c7c15f2feb974' into HEAD
vjeux Nov 7, 2026
8da39a8
Merge commit 'b8a9055733b01d1a2379923035f598d405df7606' into HEAD
vjeux Nov 7, 2026
e66ff1d
Merge commit '92784f658939912fae623dd497cd35e499e13878' into HEAD
vjeux Nov 8, 2026
08a3ad5
Merge commit '343df06699191ed4ff94b606ae0247671988db2e' into HEAD
vjeux Nov 8, 2026
343df06
Merge commit 'e66ff1d4f8d74ee9cce277cc7cb3ac14ed1368a0' into HEAD
vjeux Nov 8, 2026
ad37823
Merge commit 'c9d07a9136573e6e87da98745d5afb84a271cf59' into HEAD
vjeux Nov 8, 2026
24c03c1
test(ContextMenu): a starved page's late timer fails by name in WebKit
vjeux Nov 8, 2026
c9d07a9
Merge commit '24c03c1e10039a5eb52569848433ab8f7643419f' into HEAD
vjeux Nov 8, 2026
0ac4cb7
ci: record ContextMenuLongPress's WebKit time for the leg count
vjeux Nov 8, 2026
339bf7e
docs(ContextMenu): PR1 names each presentation's query subscriptions
vjeux Nov 8, 2026
3d78dae
docs(ContextMenu): the record names the touch open's focus gap
vjeux Nov 8, 2026
cc254d7
Merge commit '3d78dae488ec298c7ddefea756032ee181008321' into HEAD
vjeux Nov 8, 2026
f62eada
Merge commit 'cc254d737d56e644530fb8334162ca895be65539' into HEAD
vjeux Nov 8, 2026
df642e0
Merge main into motion/morph-path
vjeux Nov 8, 2026
8c8f726
Merge motion/morph-path into motion/rect-morph
vjeux Nov 8, 2026
3fdca8f
fix(rectMorph): AST-094 links the morph; the proof imports a safe test
vjeux Nov 8, 2026
d4903e2
test(SnapPaging): the pager's bundle lists the progress module
vjeux Nov 8, 2026
1b794af
test(motion): the popover's proofs read Popover's live geometry
vjeux Nov 8, 2026
5dd6a6a
Merge motion/morph-path into motion/rect-morph
vjeux Nov 8, 2026
3bfa524
refactor(motion): one eased progress, with a retargeted velocity
vjeux Nov 8, 2026
ad7f8fb
test(motion): the popover's turn-around reads Popover's live curves
vjeux Nov 8, 2026
99d352d
Merge motion/morph-path into motion/rect-morph
vjeux Nov 8, 2026
20dc83f
Merge branch 'main' into uikit/c20-contextmenu-press
cixzhang Nov 8, 2026
4c936d1
Merge branch 'uikit/c20-contextmenu-press' into uikit/c20-contextmenu…
cixzhang Nov 8, 2026
33a3ee8
docs(changeset): the rect morph's note names the eased-progress move
vjeux Nov 8, 2026
292bbe9
Merge branch 'uikit/c20-contextmenu-growth' into uikit/c20-contextmen…
cixzhang Nov 8, 2026
218a277
Merge main into uikit/c20-contextmenu-scrub
vjeux Nov 8, 2026
2bf8f8a
Merge main into motion/rect-morph
vjeux Nov 9, 2026
822013a
test(motion): the popover's turn-around compares opacity as drawn
vjeux Nov 9, 2026
2651e17
Merge motion/morph-path into motion/rect-morph
vjeux Nov 9, 2026
6354fa3
fix(motion): a popover's morph never reads an opacity below 0
vjeux Nov 10, 2026
dcfa346
Merge motion/morph-path into motion/rect-morph
vjeux Nov 10, 2026
6b6310c
test(motion): the morph proofs drop the menu's old straight line
vjeux Nov 10, 2026
9d6c0a2
feat(lift): one opt-in lift-from-trigger presentation
vjeux Oct 26, 2026
a13c9b0
docs(lift): the lift's springs are the caller's, not motion roles
vjeux Oct 26, 2026
b3e79df
refactor(lift): the lift holds input through the layer runtime's inpu…
vjeux Oct 26, 2026
68c64c1
refactor(liftFromTrigger): the lift-from-trigger module takes its rec…
vjeux Oct 26, 2026
d2c974e
test(liftFromTrigger): a lifted list item leaves its list valid and t…
vjeux Oct 27, 2026
e0eb8e1
fix(liftFromTrigger): an li holds a list item's spot, and takes no nu…
vjeux Oct 27, 2026
08cb1b7
test(liftFromTrigger): the zoom's copy of a source, through the lift …
vjeux Oct 27, 2026
d5fb378
feat(liftFromTrigger): liftCopy, the zoom's copy of a source through …
vjeux Oct 27, 2026
9d4bf2b
test(liftFromTrigger): browser proofs of the zoom's copy
vjeux Oct 27, 2026
5ef5f36
docs(layer-runtime): the lift-from-trigger module exists
vjeux Oct 27, 2026
1d704a2
fix(liftFromTrigger): each spring reaches the rect morph as a motion
vjeux Nov 8, 2026
23bf224
refactor(liftFromTrigger): the caller names the lift's marks
vjeux Nov 8, 2026
fe3b5c2
docs(liftFromTrigger): comments say why, not which record
vjeux Nov 8, 2026
6a58c11
test(liftFromTrigger): a finger taps the growing lift, from the harness
vjeux Nov 8, 2026
8e4f4c5
docs(liftFromTrigger): comment sentences keep to 25 words
vjeux Nov 8, 2026
96ef395
test(liftFromTrigger): a tall source fits between the gutters
vjeux Nov 8, 2026
da70e02
fix(liftFromTrigger): the lift fits between the gutters on both axes
vjeux Nov 8, 2026
67116fb
test(liftFromTrigger): a pinch on the lifted backdrop zooms the page
vjeux Nov 8, 2026
ff7e468
fix(liftFromTrigger): the lift layer keeps the page's pinch-zoom
vjeux Nov 8, 2026
9e7b0b4
test(liftFromTrigger): a drop mid-rise turns the tint with its velocity
vjeux Nov 8, 2026
917d51d
fix(liftFromTrigger): the tint fades on the animator's backdrop channel
vjeux Nov 8, 2026
50f150c
refactor(liftFromTrigger): the shadow rides a rect morph of its own
vjeux Nov 8, 2026
87b4151
test(liftFromTrigger): a dropped row's margin returns with no transition
vjeux Nov 8, 2026
29677fd
fix(liftFromTrigger): the restore flushes style with a non-geometry read
vjeux Nov 8, 2026
5f36262
fix(liftFromTrigger): the lifted trigger casts no shadow
vjeux Nov 10, 2026
d2e52c0
test(liftFromTrigger): the growth sample reads its animations at 30 ms
vjeux Nov 10, 2026
bae645b
fix(liftFromTrigger): a source lifts at its layout width
vjeux Nov 10, 2026
4e989c6
feat(liftFromTrigger): a surface can show as the layer shows
vjeux Nov 10, 2026
a0eec83
feat(liftFromTrigger): the caller can pin styles with the lift's own
vjeux Nov 10, 2026
cb0cc0b
Merge the lift module into the ContextMenu stack
vjeux Nov 10, 2026
a33130e
feat(ContextMenu): a held finger lifts a block trigger above a tint
vjeux Nov 10, 2026
bed2a23
feat(ContextMenu): a lifted trigger with no box gets a platter
vjeux Nov 10, 2026
0887567
docs(ContextMenu): the lift decision records the plate rule
vjeux Nov 10, 2026
c3fbad3
fix(ContextMenu): the menu shows as the lift's layer shows
vjeux Nov 10, 2026
1acc3b3
docs(ContextMenu): the records and notes say ContextMenu lifts
vjeux Nov 10, 2026
dd5acaa
refactor(ContextMenu): the plate rides the lift's pin
vjeux Nov 10, 2026
a9188e6
Merge main
vjeux Nov 10, 2026
15de4d7
test(ContextMenu): cases for the plate behind a lifted trigger
vjeux Nov 10, 2026
4692ba8
fix(ContextMenu): the plate follows what a lifted trigger shows
vjeux Nov 10, 2026
a5a0e12
test(ContextMenu): a finger that lifts before 400 ms is a plain tap
vjeux Nov 10, 2026
1e206ad
chore(sandbox): the lift page holds more trigger shapes
vjeux Nov 10, 2026
382edd5
docs(ContextMenu): shorter sentences in the plate comments
vjeux Nov 10, 2026
5d52587
test(ContextMenu): the lift cases wait for conditions, not for time
vjeux Nov 10, 2026
cc5afbd
docs(ContextMenu): a new decision replaces DEC-2's element count
vjeux Nov 10, 2026
704cff3
Merge #8125's head into the press feedback
vjeux Nov 10, 2026
06c942c
test(ContextMenu): a press leaves the open menu as an unpressed open
vjeux Nov 10, 2026
597e158
fix(ContextMenu): the press grows the trigger's visual, not its anchor
vjeux Nov 10, 2026
2ef1714
test(ContextMenu): the long-press cases wait for conditions, not time
vjeux Nov 10, 2026
ec0cc34
Merge #9816's head into the slide to pick
vjeux Nov 10, 2026
8b33ba1
Merge #8133's head into the lift
vjeux Nov 10, 2026
a6253f3
test(ContextMenu): a late drift past 11.5 px, a known Chromium gap
vjeux Nov 10, 2026
d26b15b
test(ContextMenu): a set-height trigger keeps a full-height child
vjeux Nov 10, 2026
3c39a05
test(ContextMenu): a late drift past 11.5 px cancels in every engine
vjeux Nov 10, 2026
a4e0404
fix(ContextMenu): the menu waits for the finger's late moves
vjeux Nov 10, 2026
7d23141
fix(ContextMenu): the trigger's visual takes the trigger's height
vjeux Nov 10, 2026
a75cf09
test(ContextMenu): the inner visual box adds no attribute
vjeux Nov 10, 2026
c346da0
Merge #8125's head into the press feedback
vjeux Nov 10, 2026
dbb295a
fix(ContextMenu): the lift and its plate read the trigger's visual
vjeux Nov 10, 2026
4c88c64
docs(ContextMenu): FR5's gap names only what remains
vjeux Nov 10, 2026
3b76a8c
test(ContextMenu): a finger inside the tolerance at the menu's moment
vjeux Nov 10, 2026
88cc0d8
fix(ContextMenu): the press reads each move's own time from the arena
vjeux Nov 10, 2026
e25ec8c
Merge #8125's head into the press feedback
vjeux Nov 10, 2026
967b5f6
Merge #9816's head into the slide to pick
vjeux Nov 10, 2026
548ccb5
Merge #8133's head into the lift
vjeux Nov 10, 2026
180083f
test(ContextMenu): the outside tap waits for the lift to settle
vjeux Nov 10, 2026
ba4a81e
docs(ContextMenu): the lift's placement effect names what it syncs
vjeux Nov 10, 2026
46bc0da
test(ContextMenu): coalesced late samples and an on-time move
vjeux Nov 10, 2026
cad79ec
fix(ContextMenu): judge each sample at its time, wait only when late
vjeux Nov 10, 2026
782cdbd
Merge #8125's head into the press feedback
vjeux Nov 10, 2026
ac3dab7
Merge #9816's head into the slide to pick
vjeux Nov 10, 2026
889aded
test(ContextMenu): moves held behind a block decide before the menu
vjeux Nov 11, 2026
45bfb44
fix(ContextMenu): a late menu timer waits for moves behind the block
vjeux Nov 11, 2026
2a36eba
Merge #8125's head into the press feedback
vjeux Nov 11, 2026
d743922
test(ContextMenu): the press's page time comes from the arena's clock
vjeux Nov 11, 2026
cae3644
fix(ContextMenu): the press reads the page's time from the arena
vjeux Nov 11, 2026
b80098f
Merge #9816's head into the slide to pick
vjeux Nov 11, 2026
0b1f31c
Merge #8125's head into the press feedback
vjeux Nov 11, 2026
17cab2a
test(ContextMenu): the stays-put case reads the menu as laid out
vjeux Nov 11, 2026
61f0365
Merge #8133's head into the lift
vjeux Nov 11, 2026
d096b78
test(ContextMenu): the inline stays-put case reads the menu as laid out
vjeux Nov 11, 2026
6db12ac
docs(useMenuPress): name continueGesture in the handoff row
vjeux Nov 11, 2026
4e93b5f
refactor(ContextMenu): a private mark finds the trigger, not a registry
vjeux Nov 11, 2026
f908089
docs(ContextMenu): name the context-menu-trigger mark's owner
vjeux Nov 11, 2026
b830181
docs(ContextMenu): the Chinese summary says slide to pick, too
vjeux Nov 11, 2026
03eb7d7
refactor(ContextMenu): the touch layer imports its recognizer directly
vjeux Nov 11, 2026
f37d633
test(ContextMenu): a drift queued behind a block still ends the hold
vjeux Nov 11, 2026
8575ec7
docs(records): why the press keeps its own wait, and outsidePress types
vjeux Nov 11, 2026
22ae97e
docs(ContextMenu): name the measured table and the moved default
vjeux Nov 11, 2026
7c8cb86
Merge main
vjeux Nov 11, 2026
56b2f62
test(ContextMenu): a finger in a second document on its own clock
vjeux Nov 11, 2026
650a125
Merge main
vjeux Nov 11, 2026
fac565a
docs(ContextMenu): the record owns the touch hold's two modules
vjeux Nov 11, 2026
850a9c2
Merge #8125's next head into the press feedback
vjeux Nov 11, 2026
3f21ccf
Merge the press feedback's next head into the slide to pick
vjeux Nov 11, 2026
69e6dd8
docs(ContextMenu): the hold's comments match what this code does
vjeux Nov 11, 2026
da14211
Merge #8125's comment fixes into the press feedback
vjeux Nov 11, 2026
cecb382
docs(ContextMenu): the timeline and the spec name the press feedback
vjeux Nov 11, 2026
3e17bd2
test(ContextMenu): a growth in a second document times its onset
vjeux Nov 11, 2026
69a6851
fix(ContextMenu): a growth runs on its trigger window's clock
vjeux Nov 11, 2026
1d6ec37
docs(ContextMenu): the End-user impact row states no build status
vjeux Nov 11, 2026
8fb305f
docs(ContextMenu): the press feedback's file comment keeps one line
vjeux Nov 11, 2026
4b1f2a7
Merge the press feedback's iframe-clock fix into the slide to pick
vjeux Nov 11, 2026
6f2621d
Merge #8133's head into the lift
vjeux Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/contextmenu-outside-press-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] ContextMenu closes on an outside press through the layer's outside-press watch. A mouse or pen press closes it at its `mousedown`, in the menu's own document, so a menu in an iframe or a Picture-in-Picture window no longer closes on a press in the page.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/contextmenu-touch-lift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] ContextMenu where the primary pointer is coarse and the menu opens at the finger: when the held finger opens the menu on a block trigger, the trigger lifts out of the page, as on iOS. It grows by up to 15% above a tinted page, its spot in the page held, and the menu fades in beside it, below when it fits, else above. A tap on the tint closes the menu and reaches nothing beneath; the trigger returns to its place. The lifted trigger casts no shadow; one with no background of its own, such as a row of text, lifts on the theme's surface, rounded. An inline trigger, the bottom sheet, `presentation="popover"` and a menu opened by mouse or keyboard keep today's look.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/contextmenu-touch-slide-to-pick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] ContextMenu where the primary pointer is coarse and the menu opens at the finger (`presentation` unset, or `adaptive` where it stays a popover): after the held finger opens the menu, the same finger may slide onto an action without lifting, as on iOS. Lifting over an action picks it, and lifting over no action leaves the menu open. The finger picks only after it has moved 10 px in the open menu, so a still lift picks nothing.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/contextmenu-touch-timeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] ContextMenu opens from a held finger on iOS 26's measured timeline where the primary pointer is coarse. A block trigger whose menu opens at the finger starts to grow 150 ms after the finger lands, toward its held size on UIKit's lift spring, played on the compositor, and the hold keeps UIKit's measured moments. The growth scales an inner box that holds the trigger's children, so the menu never takes the press's scale. At 400 ms the press takes the finger: its lift no longer sends a click, and the page cannot scroll under it. The menu opens at 735 ms if the finger is still down, or as soon as it lifts after 400 ms. A finger that lifts before 400 ms, or drifts more than 11.5 px on either axis before the menu opens, gets no menu, and the trigger springs back to its size. A touch outside the menu closes it as it begins to pan, or at its lift. Over a lifted trigger's tint that pan scrolls nothing; the next one scrolls the page. The trigger returns to its size when the menu closes. A touch open lights no row: the menu takes the focus, and a row lights only under the finger, as in UIKit (keyboard and pointer opens still focus the first item). A second finger cancels the press before 400 ms; after it, the menu still opens. A fine pointer (a mouse) keeps the right-click menu, and a finger there keeps today's 500 ms long press. Pass `presentation="popover"` to keep today's menu: the menu at the pointer, opened by a finger held for 500 ms.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/lift-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: one opt-in lift-from-trigger presentation for touch surfaces. The trigger itself lifts into the top layer above a tinted backdrop; a surface grows out of the press beside it on the shared rect morph and shrinks back into it on the layer runtime's exit copy; the trigger returns to its place. Translate, scale and opacity only; nothing mounted at rest, three elements while lifted, and no shadow. A zoom from a thumbnail lifts a copy of the thumbnail's picture into the viewer instead, while the thumbnail hides in place. Not exported from the package entry point; ContextMenu's touch lift uses it, and nothing else does yet.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/motion-morph-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the rect morph's paths, the geometry touch presentations share when they grow out of a source rect and shrink back into it. `lerpPath` (a straight line between two rects) and `edgeGrowPath` (UIKit's popover, growing out of a point on its edge) name the rect an element shows at each progress, and say how an interrupted motion continues in their own law; `morphCurves` turns a path into compositor curves (`translate`, `scale` and a companion `opacity`). Not exported from the package entry point; ContextMenu's touch lift uses `lerpPath`, through the lift module.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/motion-rect-morph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the rect morph, one way for a touch presentation to grow out of a source rect and shrink back into it. `createRectMorph` presents an element along a path (`lerpPath`, `edgeGrowPath`) and dismisses it back on a copy or on itself, each continuing the other from the rect on screen. Each play takes the timing the component's motion record names (AST-094 FR45): a duration and easing, or a spring, starting from the velocity it is given. Reduced Motion is left to the animator. `easedProgress` moves to the progress helpers and keeps a retargeted velocity; `timedCurve` joins them. Not exported from the package entry point; ContextMenu's touch lift uses it, through the lift module.

@vjeux
1 change: 1 addition & 0 deletions .github/playwright-leg-durations.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"packages/core/src/Chat/__tests__/ChatLayoutKeyboard.interaction.browser.spec.ts": 29.9,
"packages/core/src/Chat/__tests__/ChatLayoutPanelView.interaction.browser.spec.ts": 4.1,
"packages/core/src/Chat/__tests__/ChatMessageAvatar.interaction.browser.spec.ts": 2.2,
"packages/core/src/ContextMenu/__tests__/ContextMenuLongPress.interaction.browser.spec.ts": 58.3,
"packages/core/src/DateInput/__tests__/DateInputFocusLeave.interaction.browser.spec.ts": 4.2,
"packages/core/src/DateInput/__tests__/MonthScroller.interaction.browser.spec.ts": 8.5,
"packages/core/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts": 12.1,
Expand Down
1 change: 1 addition & 0 deletions apps/sandbox/scripts/next-export-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"/pages/codeblock-perf/",
"/pages/color-studio/",
"/pages/component-scores/",
"/pages/context-menu-lift/",
"/pages/dictation-lab/",
"/pages/diffviewer-perf/",
"/pages/doc-discover/",
Expand Down
309 changes: 309 additions & 0 deletions apps/sandbox/src/app/(raw)/pages/context-menu-lift/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
* @file page.tsx
* @input ContextMenu and the components a page wraps in one
* @output A phone page of ContextMenus in place: list rows, cards, table
* cells, thumbnails, the page's edges, a scroller and a clipping box
* @position A raw Sandbox page (/pages/context-menu-lift/) with no sandbox
* frame. The document scrolls, so a held finger lifts each trigger where a
* page puts it, with the page's own scroll.
*/

'use client';

import * as stylex from '@stylexjs/stylex';
import {Card} from '@astryxdesign/core/Card';
import {ContextMenu} from '@astryxdesign/core/ContextMenu';
import {Divider} from '@astryxdesign/core/Divider';
import {Grid} from '@astryxdesign/core/Grid';
import {Item} from '@astryxdesign/core/Item';
import {HStack, VStack} from '@astryxdesign/core/Layout';
import {Switch} from '@astryxdesign/core/Switch';
import {
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from '@astryxdesign/core/Table';
import {Heading, Text} from '@astryxdesign/core/Text';
import {Thumbnail} from '@astryxdesign/core/Thumbnail';
import {useThemeControls} from '../../../providers';

const MENU = [
{label: 'Copy', onClick: () => {}},
{label: 'Share', onClick: () => {}},
{label: 'Rename', onClick: () => {}},
{label: 'Delete', onClick: () => {}},
];

const ROWS = Array.from({length: 24}, (_, i) => ({
id: i + 1,
name: `Message ${i + 1}`,
preview: 'Hold the row to lift it. Tap the dimmed page to close.',
}));

const FILES = [
{name: 'Budget.xlsx', owner: 'Ana', size: '24 KB'},
{name: 'Roadmap.pdf', owner: 'Ben', size: '1.2 MB'},
{name: 'Logo.svg', owner: 'Chris', size: '8 KB'},
{name: 'Notes.txt', owner: 'Dana', size: '2 KB'},
];

const HUES = [210, 20, 140, 280, 50, 330];

// A flat swatch as an image, so the page needs no network for its pictures.
function swatch(hue: number): string {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96"><rect width="96" height="96" fill="hsl(${hue} 70% 55%)"/></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}

const styles = stylex.create({
// The rows at the page's very top and bottom touch its edges.
edgeRow: {paddingInline: 0},
scroller: {
overflowX: 'auto',
paddingBlock: 8,
},
scrollerCard: {flexShrink: 0},
clip: {
overflow: 'hidden',
height: 120,
},
// The trigger fills its table cell.
cellTrigger: {display: 'block'},
iconRow: {
display: 'flex',
alignItems: 'center',
gap: 8,
},
outlined: {
padding: 12,
borderWidth: 1,
borderStyle: 'solid',
borderColor: 'currentColor',
borderRadius: 12,
},
translucent: {
padding: 12,
borderRadius: 12,
backgroundColor: 'rgba(59, 130, 246, 0.3)',
},
pseudoFill: {
position: 'relative',
isolation: 'isolate',
padding: 12,
'::before': {
content: '""',
position: 'absolute',
inset: 0,
zIndex: -1,
borderRadius: 12,
backgroundColor: 'rgba(16, 185, 129, 1)',
},
},
});

export default function ContextMenuLiftPage() {
const {mode, setMode} = useThemeControls();
return (
<VStack gap={0}>
<ContextMenu items={MENU} label="Top row">
<Item
label="Top edge"
description="The page's first row, against the top of the screen"
xstyle={styles.edgeRow}
/>
</ContextMenu>
<VStack gap={6} padding={4}>
<VStack gap={2}>
<Heading level={1}>ContextMenu in place</Heading>
<Text>
Hold any row, card, cell or picture until its menu shows. The page
scrolls on its own; tap the dimmed page to close a menu.
</Text>
<Switch
label="Dark mode"
value={mode === 'dark'}
onChange={isOn => setMode(isOn ? 'dark' : 'light')}
/>
</VStack>

<VStack gap={2}>
<Heading level={2}>Rows in a long list</Heading>
<VStack gap={0}>
{ROWS.map((row, i) => (
<VStack key={row.id} gap={0}>
{i > 0 ? <Divider /> : null}
<ContextMenu items={MENU} label={row.name}>
<Item label={row.name} description={row.preview} />
</ContextMenu>
</VStack>
))}
</VStack>
</VStack>

<VStack gap={2}>
<Heading level={2}>Cards in a grid</Heading>
<Grid columns={2} gap={3}>
{HUES.map((hue, i) => (
<ContextMenu key={hue} items={MENU} label={`Card ${i + 1}`}>
<Card>
<VStack gap={1}>
<Thumbnail src={swatch(hue)} alt="" />
<Text weight="semibold">{`Card ${i + 1}`}</Text>
<Text type="supporting">A card in a two-column grid</Text>
</VStack>
</Card>
</ContextMenu>
))}
</Grid>
</VStack>

<VStack gap={2}>
<Heading level={2}>Table cells</Heading>
<Text type="supporting">
A row has no wrapper, so each name cell holds the trigger.
</Text>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Owner</TableHeaderCell>
<TableHeaderCell>Size</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{FILES.map(file => (
<TableRow key={file.name}>
<TableCell>
<ContextMenu
items={MENU}
label={file.name}
triggerXstyle={styles.cellTrigger}>
<Text>{file.name}</Text>
</ContextMenu>
</TableCell>
<TableCell>{file.owner}</TableCell>
<TableCell>{file.size}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</VStack>

<VStack gap={2}>
<Heading level={2}>Thumbnails</Heading>
<HStack gap={2}>
{HUES.slice(0, 4).map((hue, i) => (
<ContextMenu key={hue} items={MENU} label={`Picture ${i + 1}`}>
<Thumbnail src={swatch(hue)} alt={`Picture ${i + 1}`} />
</ContextMenu>
))}
</HStack>
</VStack>

<VStack gap={2}>
<Heading level={2}>Other shapes</Heading>
<Text>
A trigger with no surface of its own lifts on a plate. One with a
fill of its own lifts as it is.
</Text>
<Grid columns={2} gap={3}>
<ContextMenu items={MENU} label="Row with an icon">
<div {...stylex.props(styles.iconRow)}>
<svg width={20} height={20} viewBox="0 0 20 20" aria-hidden>
<circle cx={10} cy={10} r={8} fill="currentColor" />
</svg>
<Text>A row with an icon</Text>
</div>
</ContextMenu>
<ContextMenu items={MENU} label="Outlined box">
<div {...stylex.props(styles.outlined)}>
<Text>An outlined box</Text>
</div>
</ContextMenu>
<ContextMenu items={MENU} label="Translucent box">
<div {...stylex.props(styles.translucent)}>
<Text>A translucent fill</Text>
</div>
</ContextMenu>
<ContextMenu items={MENU} label="Filled box">
<div {...stylex.props(styles.pseudoFill)}>
<Text>A fill from a pseudo-element</Text>
</div>
</ContextMenu>
<ContextMenu items={MENU} label="Inline image">
<img
src={swatch(HUES[3])}
alt="Purple swatch"
width={64}
height={64}
/>
</ContextMenu>
</Grid>
</VStack>

<VStack gap={2}>
<Heading level={2}>The side edges</Heading>
<HStack gap={2} justify="between">
<ContextMenu items={MENU} label="Left edge">
<Card padding={2}>
<Text>Left edge</Text>
</Card>
</ContextMenu>
<ContextMenu items={MENU} label="Right edge">
<Card padding={2}>
<Text>Right edge</Text>
</Card>
</ContextMenu>
</HStack>
</VStack>

<VStack gap={2}>
<Heading level={2}>Inside a scroller</Heading>
<HStack gap={3} xstyle={styles.scroller}>
{HUES.map((hue, i) => (
<ContextMenu
key={hue}
items={MENU}
label={`Scrolled card ${i + 1}`}>
<Card width={160} xstyle={styles.scrollerCard}>
<VStack gap={1}>
<Thumbnail src={swatch(hue)} alt="" />
<Text>{`Scrolled card ${i + 1}`}</Text>
</VStack>
</Card>
</ContextMenu>
))}
</HStack>
</VStack>

<VStack gap={2}>
<Heading level={2}>Inside a clipping box</Heading>
<Card padding={0} xstyle={styles.clip}>
<VStack gap={0}>
{ROWS.slice(0, 4).map(row => (
<ContextMenu key={row.id} items={MENU} label={row.name}>
<Item
label={`Clipped ${row.name}`}
description="Its box hides what overflows"
/>
</ContextMenu>
))}
</VStack>
</Card>
</VStack>
</VStack>
<ContextMenu items={MENU} label="Bottom row">
<Item
label="Bottom edge"
description="The page's last row, against the bottom of the screen"
xstyle={styles.edgeRow}
/>
</ContextMenu>
</VStack>
);
}
6 changes: 6 additions & 0 deletions apps/sandbox/src/app/sandboxPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export const categories: SandboxCategory[] = [
description:
'Every built-in Text type at the default size, on a bare page that scrolls',
},
{
name: 'ContextMenu in Place',
href: '/pages/context-menu-lift/',
description:
'ContextMenus on list rows, cards, table cells and pictures, at the page edges and in clipping boxes, on a bare page that scrolls',
},
],
},
{
Expand Down
Loading