Desktop: custom context menus everywhere, never the browser's menu

One ContextMenu in the design system (keyboard, type-ahead, submenus, check items,
disabled reasons, Shift+F10 / Menu key, viewport placement, light and dark). The
native menu is prevented globally; text fields keep Cut / Copy / Paste / Select all.
Menus on sidebar and portfolio apps, app hero, releases, store art, live art,
reviews, checks, editor fields, catalog, prices, accounts, app folders, ignored.
This commit is contained in:
CRTkafa 2026-10-23 08:59:14 +08:00
parent 944b24fe59
commit dece83c3e8
23 changed files with 839 additions and 28 deletions

View file

@ -17,6 +17,8 @@ import { RolloutBar } from '../../ui/charts';
import { Card, Empty, Notice, PhaseChip, phaseLabel, Skeleton, storeName } from '../../ui/kit';
import { EmptyState, Section, StatusPill, tintStyle, useTint } from '../../ui/ds';
import { AppleSubmitCard, PlayProductionCard } from './Submit';
import { useContextMenu } from '../../ui/ContextMenu';
import { releaseMenu } from '../../ui/app-menu';
import '../../styles/releases.css';
const STAGE_KEY: Record<(typeof STAGES)[number], Key> = { build: 'stage.build', testing: 'stage.testing', review: 'stage.review', live: 'stage.live', rollout: 'stage.rollout' };
@ -49,8 +51,10 @@ function LaneRow({ lane, version, st, app }: { lane: Lane; version: string; st:
const ph = r ? phaseLabel(r) : null;
const rolling = lane.at === STAGES.length - 1 && lane.share !== null;
const ringTone = lane.tone === 'bad' ? 'bad' : (lane.share ?? 0) >= 1 ? 'ok' : 'accent';
const d = useData();
const menu = useContextMenu();
return (
<article className="tl-lane rel-store" data-store={lane.store} data-tone={lane.tone} aria-label={storeName(lane.store)}>
<article {...menu(() => releaseMenu(d, app, lane.store, r?.version ?? version, r?.build))} className="tl-lane rel-store" data-store={lane.store} data-tone={lane.tone} aria-label={storeName(lane.store)}>
<header className="rel-store-h">
<span className="rel-mark"><StoreGlyph store={lane.store} /></span>
<div className="rel-who">
@ -168,6 +172,7 @@ function Train({ title, train, tf, tone, app, st }: { title: string; train: Rele
export function ReleasesTab({ app }: { app: UnifiedApp }) {
const d = useData();
const menu = useContextMenu();
const l = d.liveFor(app);
const project = app.project;
const hue = useTint(d.iconFor(app), app.name);
@ -221,7 +226,10 @@ export function ReleasesTab({ app }: { app: UnifiedApp }) {
<thead><tr><th>{t('col.version')}</th><th>App Store</th><th>{t('col.build')}</th><th>Google Play</th><th>{t('col.versionCode')}</th><th>{t('col.track')}</th></tr></thead>
<tbody>
{history.map(h => (
<tr key={h.version}>
<tr key={h.version} {...menu(() => [
...(h.ios ? [{ label: 'App Store', icon: 'phone' as const, submenu: releaseMenu(d, app, 'ios', h.ios.version, h.ios.build) }] : []),
...(h.android ? [{ label: 'Google Play', icon: 'phone' as const, submenu: releaseMenu(d, app, 'android', h.android.version, h.android.build) }] : []),
])}>
<td className="strong num rel-hv">{[...new Set([h.version, h.ios?.version, h.android?.version].filter(Boolean))].join(' / ')}</td>
<td>{h.ios ? <PhaseChip r={h.ios} /> : <span className="muted small">–</span>}</td>
<td className="num muted">{h.ios?.build ?? '–'}</td>