Skip to content

Commit 0b98f46

Browse files
Jing-yilinclaude
andcommitted
Menus are Radix's, and the device filter is Radix's Tabs
The right-click menus of a home card, a community card, a tab, Home and a canvas tab were popovers placed at the pointer by hand, with a pointerup workaround for macOS opening a menu on the press. They are Radix's ContextMenu now, which places, dismisses and navigates them by keyboard. The model picker and the agent menu are its DropdownMenu, radio groups. The effort slider and the history list stay native popovers: the slider wants the arrow keys a menu would take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c142ec2 commit 0b98f46

8 files changed

Lines changed: 597 additions & 698 deletions

File tree

‎canvas/src/CanvasStrip.tsx‎

Lines changed: 74 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { canvasIndex, LAYOUT_CHANGED } from "./canvasIndex";
66
import { groundEditable, setGround, useGround } from "./canvasGround";
77
import { ViewIcon } from "./CanvasTabBar";
88
import { shareUrl, sheetPageUrl, type CanvasTab } from "./canvasUrl";
9-
import { confirmTrash, openMenu, REVEAL, TRASH } from "./contextMenu";
9+
import {
10+
confirmTrash,
11+
MenuItem,
12+
MenuSeparator,
13+
REVEAL,
14+
RightClickMenu,
15+
TRASH,
16+
} from "./contextMenu";
1017
import { DocModeSwitch } from "./DocTab";
1118
import { FileText, LogoFigma, Plus } from "./geistIcons";
1219

@@ -39,8 +46,6 @@ export function CanvasStrip() {
3946
// Keyed by the page. The project's own view with no canvas has none, and the default ground.
4047
const page = activeTab.kind === "canvas" ? pageOf(activeTab) : undefined;
4148
const ground = useGround(editor, page);
42-
const [target, setTarget] = useState<CanvasTab | null>(null);
43-
const menu = useRef<HTMLDivElement>(null);
4449
const [renaming, setRenaming] = useState(() => sessionStorage.getItem(RENAME_KEY));
4550
// A name typed lands as a layout change, which the tab reads through the index.
4651
const [, relabel] = useReducer((n: number) => n + 1, 0);
@@ -62,7 +67,6 @@ export function CanvasStrip() {
6267
if (editor && made.current) openTab({ kind: "canvas", slug: made.current });
6368
if (editor) made.current = null;
6469
}, [editor, openTab]);
65-
const own = target?.kind === "canvas" && tab.kind !== "example" && canvasIndex().served;
6670
const folder = async (canvas: string, action: "reveal" | "delete") => {
6771
const response = await fetch(`${import.meta.env.BASE_URL}__sp/canvas-folder`, {
6872
method: "POST",
@@ -72,6 +76,45 @@ export function CanvasStrip() {
7276
if (!response.ok) return alert(await response.text());
7377
if (action === "delete") window.location.reload();
7478
};
79+
// Right-click menu: Copy link copies the window's address for that view. A canvas of the
80+
// project's own also shows its folder, or bins it after asking.
81+
const menuFor = (target: CanvasTab) => (
82+
<>
83+
<MenuItem
84+
onSelect={() =>
85+
navigator.clipboard.writeText(
86+
shareUrl(
87+
new URL(tabUrl({ ...tab, view: target }), location.href).href,
88+
canvasIndex().title,
89+
),
90+
)
91+
}
92+
>
93+
Copy link
94+
</MenuItem>
95+
{target.kind === "canvas" && tab.kind !== "example" && canvasIndex().served && (
96+
<>
97+
<MenuItem onSelect={() => folder(target.slug, "reveal")}>{REVEAL}</MenuItem>
98+
<MenuSeparator />
99+
<MenuItem
100+
className="sp-context-menu__danger"
101+
onSelect={async () => {
102+
if (
103+
await confirmTrash(
104+
shortName(target.slug),
105+
`${canvasIndex().canvasesDir}/${target.slug}`,
106+
"Its boards and everything pasted on it go with it.",
107+
)
108+
)
109+
void folder(target.slug, "delete");
110+
}}
111+
>
112+
{TRASH}
113+
</MenuItem>
114+
</>
115+
)}
116+
</>
117+
);
75118
const rename = (canvas: string, name: string) => {
76119
sessionStorage.removeItem(RENAME_KEY);
77120
setRenaming(null);
@@ -86,18 +129,18 @@ export function CanvasStrip() {
86129
return (
87130
<nav className="sp-canvas-tabs" aria-label="Canvases">
88131
{docsOf(tab).map(({ name, slug }) => (
89-
<button
90-
key={slug}
91-
type="button"
92-
className="sp-canvas-tab"
93-
aria-current={activeTab.kind === "doc" && activeTab.slug === slug ? "page" : undefined}
94-
title={name}
95-
onClick={() => openTab({ kind: "doc", slug })}
96-
onContextMenu={(event) => openMenu(event, menu, () => setTarget({ kind: "doc", slug }))}
97-
>
98-
<FileText />
99-
{name.replace(/\.md$/i, "")}
100-
</button>
132+
<RightClickMenu key={slug} menu={menuFor({ kind: "doc", slug })}>
133+
<button
134+
type="button"
135+
className="sp-canvas-tab"
136+
aria-current={activeTab.kind === "doc" && activeTab.slug === slug ? "page" : undefined}
137+
title={name}
138+
onClick={() => openTab({ kind: "doc", slug })}
139+
>
140+
<FileText />
141+
{name.replace(/\.md$/i, "")}
142+
</button>
143+
</RightClickMenu>
101144
))}
102145
{canvases.map((canvas) =>
103146
canvas === renaming ? (
@@ -121,23 +164,21 @@ export function CanvasStrip() {
121164
/>
122165
</label>
123166
) : (
124-
<button
125-
key={canvas}
126-
type="button"
127-
className="sp-canvas-tab"
128-
aria-current={canvas === here ? "page" : undefined}
129-
data-working={busy.includes(canvas) || undefined}
130-
onClick={() => openTab({ kind: "canvas", slug: canvas })}
131-
onDoubleClick={() =>
132-
tab.kind !== "example" && canvasIndex().served && setRenaming(canvas)
133-
}
134-
onContextMenu={(event) =>
135-
openMenu(event, menu, () => setTarget({ kind: "canvas", slug: canvas }))
136-
}
137-
>
138-
<ViewIcon view={{ kind: "canvas", slug: canvas }} />
139-
{shortName(canvas)}
140-
</button>
167+
<RightClickMenu key={canvas} menu={menuFor({ kind: "canvas", slug: canvas })}>
168+
<button
169+
type="button"
170+
className="sp-canvas-tab"
171+
aria-current={canvas === here ? "page" : undefined}
172+
data-working={busy.includes(canvas) || undefined}
173+
onClick={() => openTab({ kind: "canvas", slug: canvas })}
174+
onDoubleClick={() =>
175+
tab.kind !== "example" && canvasIndex().served && setRenaming(canvas)
176+
}
177+
>
178+
<ViewIcon view={{ kind: "canvas", slug: canvas }} />
179+
{shortName(canvas)}
180+
</button>
181+
</RightClickMenu>
141182
),
142183
)}
143184
{tab.kind !== "example" && canvasIndex().served && (
@@ -186,61 +227,6 @@ export function CanvasStrip() {
186227
<LogoFigma />
187228
</a>
188229
)}
189-
{/* Right-click menu: Copy link copies the window's address for that view. A canvas of the
190-
project's own also shows its folder, or bins it after asking. */}
191-
<div
192-
ref={menu}
193-
popover="auto"
194-
className="sp-context-menu"
195-
role="menu"
196-
onClickCapture={(event) => event.currentTarget.hidePopover()}
197-
>
198-
<button
199-
type="button"
200-
role="menuitem"
201-
className="sp-menu-row"
202-
onClick={() =>
203-
navigator.clipboard.writeText(
204-
shareUrl(
205-
new URL(tabUrl({ ...tab, view: target! }), location.href).href,
206-
canvasIndex().title,
207-
),
208-
)
209-
}
210-
>
211-
Copy link
212-
</button>
213-
{own && (
214-
<>
215-
<button
216-
type="button"
217-
role="menuitem"
218-
className="sp-menu-row"
219-
onClick={() => folder(target.slug, "reveal")}
220-
>
221-
{REVEAL}
222-
</button>
223-
<hr />
224-
<button
225-
type="button"
226-
role="menuitem"
227-
className="sp-menu-row sp-context-menu__danger"
228-
onClick={async () => {
229-
if (
230-
await confirmTrash(
231-
shortName(target.slug),
232-
`${canvasIndex().canvasesDir}/${target.slug}`,
233-
"Its boards and everything pasted on it go with it.",
234-
)
235-
)
236-
void folder(target.slug, "delete");
237-
}}
238-
>
239-
{TRASH}
240-
</button>
241-
</>
242-
)}
243-
</div>
244230
</nav>
245231
);
246232
}

0 commit comments

Comments
 (0)