Desktop: compact app hero on every tab but Releases; Ctrl+K lists the current app's menu actions; Alt+Up/Down switches apps; shortcuts list; no focus ring on code-focused headings
This commit is contained in:
parent
a656240bfb
commit
7c18b3f7dd
7 changed files with 63 additions and 10 deletions
|
|
@ -26,6 +26,7 @@ import { ErrorBoundary } from './ui/ErrorBoundary';
|
|||
import { Toasts } from './ui/Toasts';
|
||||
import { useContextMenu } from './ui/ContextMenu';
|
||||
import { appMenu } from './ui/app-menu';
|
||||
import { isEditable, isItem, type MenuInput } from './ui/menu-logic';
|
||||
import { worstTone } from './ui/ds';
|
||||
import { attention } from './lib/model';
|
||||
|
||||
|
|
@ -53,6 +54,15 @@ export function App({ onTheme }: { onTheme: () => void }) {
|
|||
const [showIgnored, setShowIgnored] = useShowIgnored();
|
||||
const ignoredApps = useMemo(() => d.apps.filter(a => a.ignored).sort((a, b) => a.name.localeCompare(b.name)), [d.apps]);
|
||||
const listed = showIgnored ? [...visible, ...ignoredApps] : visible;
|
||||
const groups = d.studios.length ? groupByStudio(listed.map(a => ({ app: a })), d.studios) : [{ studio: null as string | null, rows: listed.map(a => ({ app: a })) }];
|
||||
// Alt+Up / Alt+Down: the previous / next app in sidebar order (keeps the tab when the other app has it)
|
||||
const sideOrder = groups.flatMap(x => x.rows.map(r => r.app));
|
||||
const stepApp = useCallback((dir: 1 | -1) => {
|
||||
if (!sideOrder.length) return;
|
||||
const i = route.screen === 'app' ? sideOrder.findIndex(a => a.key === route.appId) : -1;
|
||||
const next = sideOrder[i < 0 ? (dir === 1 ? 0 : sideOrder.length - 1) : (i + dir + sideOrder.length) % sideOrder.length]!;
|
||||
go({ screen: 'app', appId: next.key, tab: route.screen === 'app' && tabsFor(!!next.project).includes(route.tab) ? route.tab : 'releases' });
|
||||
}, [sideOrder, route, go]);
|
||||
const toggleGroup = (id: string) => setCollapsed(c => {
|
||||
const next = c.includes(id) ? c.filter(x => x !== id) : [...c, id];
|
||||
try { localStorage.setItem(SIDE_KEY, JSON.stringify(next)); } catch { /* private mode */ }
|
||||
|
|
@ -89,6 +99,7 @@ export function App({ onTheme }: { onTheme: () => void }) {
|
|||
const mod = e.metaKey || e.ctrlKey;
|
||||
if (mod && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') { e.preventDefault(); setPalette(p => !p); return; }
|
||||
if (palette || welcome) return;
|
||||
if (e.altKey && !mod && !e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown') && !isEditable(e.target as HTMLInputElement)) { e.preventDefault(); stepApp(e.key === 'ArrowDown' ? 1 : -1); return; }
|
||||
if (mod && !e.shiftKey && !e.altKey && /^[1-6]$/.test(e.key)) {
|
||||
e.preventDefault();
|
||||
const n = Number(e.key);
|
||||
|
|
@ -103,7 +114,7 @@ export function App({ onTheme }: { onTheme: () => void }) {
|
|||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [palette, welcome, route, go, tabs]);
|
||||
}, [palette, welcome, route, go, tabs, stepApp]);
|
||||
|
||||
const closeWelcome = useCallback(() => {
|
||||
setWelcome(false);
|
||||
|
|
@ -118,10 +129,24 @@ export function App({ onTheme }: { onTheme: () => void }) {
|
|||
const studioName = (a: UnifiedApp) => d.studios.find(s => s.id === a.studio)?.name;
|
||||
for (const a of visible) c.push({ id: `app-${a.key}`, group: G.apps, title: a.name, hint: studioName(a) ?? t('palette.openApp'), keywords: `${a.ios?.app.bundleId ?? ''} ${a.android?.app.id ?? ''} ${a.project?.name ?? ''}`, app: { name: a.name, icon: d.iconFor(a) },
|
||||
run: () => go({ screen: 'app', appId: a.key, tab: route.screen === 'app' && tabsFor(!!a.project).includes(route.tab) ? route.tab : 'releases' }) });
|
||||
c.push({ id: 'go-next-app', group: G.go, title: t('palette.nextApp'), icon: 'arrowRight', hint: 'Alt ↓', keywords: 'next app down j', run: () => stepApp(1) });
|
||||
c.push({ id: 'go-prev-app', group: G.go, title: t('palette.prevApp'), icon: 'arrowLeft', hint: 'Alt ↑', keywords: 'previous app up k', run: () => stepApp(-1) });
|
||||
c.push({ id: 'go-portfolio', group: G.go, title: t('nav.portfolio'), icon: 'portfolio', hint: `${MOD} 1`, run: () => go({ screen: 'portfolio' }) });
|
||||
WORKSPACE.forEach((s, i) => c.push({ id: `go-${s}`, group: G.go, title: t(WS_KEY[s]), icon: WS_ICON[s], hint: `${MOD} ${i + 2}`, run: () => go({ screen: s }) }));
|
||||
for (const s of d.studios) c.push({ id: `studio-${s.id}`, group: G.go, title: s.name, icon: 'building', keywords: 'studio', hint: t('palette.studio'), run: () => go({ screen: 'accounts', section: s.id }) });
|
||||
if (app) for (const tab of tabs) c.push({ id: `tab-${tab}`, group: G.tabs, title: `${app.name} · ${t(TAB_KEY[tab])}`, icon: TAB_ICON[tab], run: () => go({ screen: 'app', appId: app.key, tab }) });
|
||||
// the app's own menu (consoles, folder, copy ids, reads, ignore, unpair) as commands: every right-click action is
|
||||
// also one Ctrl+K away; opening a tab is already above
|
||||
if (app) {
|
||||
const skip = new Set([t('cm.open'), t('cm.openTab')]);
|
||||
const add = (items: MenuInput, prefix: string) => items.forEach((e, i) => {
|
||||
if (!e || !isItem(e) || skip.has(e.label) || e.disabled) return;
|
||||
const title = prefix ? `${prefix} · ${e.label}` : e.label;
|
||||
if (e.submenu) { add(e.submenu, title); return; }
|
||||
if (e.onSelect) c.push({ id: `appcmd-${prefix}-${i}-${e.label}`, group: t('palette.gApp', { name: app.name }), title, icon: e.icon, hint: e.shortcut, keywords: app.name, run: e.onSelect });
|
||||
});
|
||||
add(appMenu(app, d, { route, go, openPalette: () => setPalette(true), openWelcome: () => setWelcome(true) }, { showIgnored }), '');
|
||||
}
|
||||
for (const x of exports) c.push({ id: `export-${x.id}`, group: G.exp, title: t('palette.export', { what: x.label }), icon: x.icon ?? 'download', keywords: `export save download ${x.kind}`, hint: x.disabled || `.${x.kind}`, run: () => { runExport(x); } });
|
||||
if (app?.project) {
|
||||
const p = app.project.id;
|
||||
|
|
@ -137,7 +162,7 @@ export function App({ onTheme }: { onTheme: () => void }) {
|
|||
c.push({ id: 'act-theme', group: G.act, title: t('action.toggleTheme'), icon: 'moon', keywords: 'dark light', run: onTheme });
|
||||
c.push({ id: 'act-welcome', group: G.act, title: t('action.welcome'), icon: 'sparkle', keywords: 'onboarding start guide', run: () => setWelcome(true) });
|
||||
return c;
|
||||
}, [d, visible, app, tabs, route, go, onTheme, exports, runExport]);
|
||||
}, [d, visible, app, tabs, route, go, onTheme, exports, runExport, showIgnored, stepApp]);
|
||||
|
||||
const crumbs: { label: string; to?: Route }[] = route.screen === 'portfolio' ? [{ label: t('nav.portfolio') }]
|
||||
: route.screen === 'app' ? [{ label: t('nav.portfolio'), to: { screen: 'portfolio' } }, { label: app?.name ?? '', to: app ? { screen: 'app', appId: app.key, tab: 'releases' } : undefined }, { label: t(TAB_KEY[route.tab]) }]
|
||||
|
|
@ -148,7 +173,6 @@ export function App({ onTheme }: { onTheme: () => void }) {
|
|||
useEffect(() => { setFocus(app?.key ?? null); }, [app?.key, setFocus]);
|
||||
const nav = useMemo(() => ({ route, go, openPalette: () => setPalette(true), openWelcome: () => setWelcome(true) }), [route, go]);
|
||||
const current = (r: Route) => (sameRoute(r, route) || (r.screen === 'app' && route.screen === 'app' && r.appId === route.appId) || (r.screen !== 'app' && r.screen !== 'portfolio' && r.screen === route.screen) ? 'page' : undefined);
|
||||
const groups = d.studios.length ? groupByStudio(listed.map(a => ({ app: a })), d.studios) : [{ studio: null as string | null, rows: listed.map(a => ({ app: a })) }];
|
||||
const ignoredCount = d.apps.filter(a => a.ignored).length + d.overrides.ignored.filter(i => i.startsWith('account:')).length;
|
||||
|
||||
const menu = useContextMenu();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue