feat(mana-web): keyboard shortcuts for workbench + nav bars

- 1–9 scroll to the Nth open app on the workbench homepage; 0 opens the
  app picker.
- q/w/e toggle the bottom bars (workbench tabs / search / tags); r opens
  the user-menu PillDropdownBar (expanding the PillNav first if needed);
  t toggles the PillNav visibility.

Adds a `data-user-menu-trigger` hook on the user pill so the layout can
drive the menu bar programmatically without duplicating its config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 00:53:42 +02:00
parent 4be5e29bd3
commit 4d6e6e61b4
3 changed files with 174 additions and 37 deletions

View file

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { Component, Snippet } from 'svelte';
import { onDestroy, setContext } from 'svelte';
import { onDestroy, setContext, tick } from 'svelte';
import { createReminderScheduler } from '@mana/shared-stores';
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
import { startEventStore, stopEventStore } from '$lib/data/events/event-store';
@ -261,7 +261,7 @@
const bottomChromeHeight = $derived(
isFullscreen
? 0
: (isCollapsed ? 0 : 80) +
: (isCollapsed ? 0 : 56) +
(activeBar ? 64 : 0) +
(isTagStripVisible ? 64 : 0) +
(isQuickInputVisible ? 64 : 0) +
@ -311,7 +311,7 @@
{
href: '/',
label: 'Workbench-Tabs',
icon: 'columns',
icon: 'tabs',
iconOnly: true,
onClick: handleBottomBarToggle,
active: isBottomBarVisible,
@ -372,6 +372,40 @@
const route = navRoutes[num - 1];
if (route) goto(route);
}
return;
}
if (event.ctrlKey || event.metaKey || event.altKey) return;
switch (event.key) {
case 'q':
case 'Q':
event.preventDefault();
handleBottomBarToggle();
return;
case 'w':
case 'W':
event.preventDefault();
handleQuickInputToggle();
return;
case 'e':
case 'E':
event.preventDefault();
handleTagStripToggle();
return;
case 'r':
case 'R':
event.preventDefault();
(async () => {
if (isCollapsed) handleCollapsedChange(false);
await tick();
document.querySelector<HTMLButtonElement>('[data-user-menu-trigger]')?.click();
})();
return;
case 't':
case 'T':
event.preventDefault();
if (!isCollapsed) closeAllBars();
handleCollapsedChange(!isCollapsed);
return;
}
}
@ -872,15 +906,6 @@
{/if}
</button>
{/snippet}
{#snippet rightAction()}
<button
class="pill-nav-toggle"
onclick={() => handleCollapsedChange(!isCollapsed)}
title={isCollapsed ? 'Navigation einblenden' : 'Navigation ausblenden'}
>
<span class="pill-nav-toggle-icon" class:collapsed={isCollapsed}>▼</span>
</button>
{/snippet}
</QuickInputBar>
{/if}

View file

@ -120,6 +120,35 @@
if (el) el.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
// ── Keyboard shortcuts 1-9 / 0 ─────────────────────────
// 1-9 scroll to the Nth open app in the active scene.
// 0 opens the new-app picker (which scrolls itself into view).
onMount(() => {
function onKeydown(e: KeyboardEvent) {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const target = e.target as HTMLElement | null;
if (target) {
const tag = target.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable)
return;
}
if (e.key === '0') {
e.preventDefault();
showPicker = true;
return;
}
if (e.key >= '1' && e.key <= '9') {
const idx = Number(e.key) - 1;
const page = carouselPages[idx];
if (!page) return;
e.preventDefault();
scrollToPage(page.id);
}
}
window.addEventListener('keydown', onKeydown);
return () => window.removeEventListener('keydown', onKeydown);
});
// ── Register SceneAppBar in the layout's bottom-stack ───
// Split into two effects so prop churn (carouselPages re-deriving on
// every openApps change) doesn't re-write barComponent. The first