From 2f87cf9d9ad1a1ad808e9b2e4336858a82e136b5 Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 3 Apr 2026 21:18:05 +0200 Subject: [PATCH] feat(manacore/web): add unified context menu system for workbench and app pages Adds right-click context menus to workbench cards, minimized tabs, PillNavigation, and item-level context menus for todo, calendar, contacts, habits, notes, places, and moodlit modules. Uses a shared builder pattern with app-specific actions registered via AppDescriptor.contextMenuActions. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/app-registry/apps.ts | 62 +++++- .../apps/web/src/lib/app-registry/types.ts | 15 ++ .../page-carousel/PageCarousel.svelte | 5 +- .../components/page-carousel/PageShell.svelte | 5 +- .../lib/components/workbench/AppPage.svelte | 3 + .../lib/context-menu/build-context-menu.ts | 141 ++++++++++++++ .../apps/web/src/lib/context-menu/index.ts | 5 + .../use-workbench-context-menu.svelte.ts | 36 ++++ .../src/lib/modules/calendar/ListView.svelte | 54 +++++- .../src/lib/modules/contacts/ListView.svelte | 64 +++++- .../src/lib/modules/habits/ListView.svelte | 182 ++++++++++-------- .../src/lib/modules/moodlit/ListView.svelte | 54 ++++++ .../web/src/lib/modules/notes/ListView.svelte | 62 +++++- .../src/lib/modules/places/ListView.svelte | 58 +++++- .../web/src/lib/modules/todo/ListView.svelte | 69 ++++++- .../modules/todo/components/TaskList.svelte | 155 +++++++-------- .../apps/web/src/routes/(app)/+layout.svelte | 64 +++++- .../apps/web/src/routes/(app)/+page.svelte | 46 +++++ .../src/navigation/PillNavigation.svelte | 14 +- packages/shared-ui/src/navigation/types.ts | 2 + 20 files changed, 919 insertions(+), 177 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/context-menu/build-context-menu.ts create mode 100644 apps/manacore/apps/web/src/lib/context-menu/index.ts create mode 100644 apps/manacore/apps/web/src/lib/context-menu/use-workbench-context-menu.svelte.ts diff --git a/apps/manacore/apps/web/src/lib/app-registry/apps.ts b/apps/manacore/apps/web/src/lib/app-registry/apps.ts index a697d7ecf..e89e2004b 100644 --- a/apps/manacore/apps/web/src/lib/app-registry/apps.ts +++ b/apps/manacore/apps/web/src/lib/app-registry/apps.ts @@ -7,6 +7,7 @@ */ import { registerApp } from './registry'; +import { Plus } from '@manacore/shared-icons'; // ── Apps with entity capabilities ─────────────────────────── @@ -18,6 +19,17 @@ registerApp({ list: { load: () => import('$lib/modules/todo/ListView.svelte') }, detail: { load: () => import('$lib/modules/todo/views/DetailView.svelte') }, }, + contextMenuActions: [ + { + id: 'new-task', + label: 'Neue Aufgabe', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'todo', action: 'new' } }) + ), + }, + ], collection: 'tasks', paramKey: 'taskId', dragType: 'task', @@ -53,6 +65,17 @@ registerApp({ list: { load: () => import('$lib/modules/calendar/ListView.svelte') }, detail: { load: () => import('$lib/modules/calendar/views/DetailView.svelte') }, }, + contextMenuActions: [ + { + id: 'new-event', + label: 'Neuer Termin', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'calendar', action: 'new' } }) + ), + }, + ], collection: 'events', paramKey: 'eventId', dragType: 'event', @@ -120,6 +143,17 @@ registerApp({ list: { load: () => import('$lib/modules/contacts/ListView.svelte') }, detail: { load: () => import('$lib/modules/contacts/views/DetailView.svelte') }, }, + contextMenuActions: [ + { + id: 'new-contact', + label: 'Neuer Kontakt', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'contacts', action: 'new' } }) + ), + }, + ], collection: 'contacts', paramKey: 'contactId', dragType: 'contact', @@ -142,6 +176,17 @@ registerApp({ views: { list: { load: () => import('$lib/modules/habits/ListView.svelte') }, }, + contextMenuActions: [ + { + id: 'new-habit', + label: 'Neues Habit', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'habits', action: 'new' } }) + ), + }, + ], collection: 'habits', paramKey: 'habitId', dragType: 'habit', @@ -149,19 +194,19 @@ registerApp({ transformIncoming: { task: (source) => ({ title: source.title as string, - emoji: '\u{1F4AA}', + icon: 'barbell', color: '#6366f1', }), }, getDisplayData: (item) => ({ - title: `${item.emoji as string} ${item.title as string}`, + title: item.title as string, subtitle: undefined, }), createItem: async (data) => { const { habitsStore } = await import('$lib/modules/habits/stores/habits.svelte'); const habit = await habitsStore.createHabit({ title: data.title as string, - emoji: (data.emoji as string) ?? '\u{2B50}', + icon: (data.icon as string) ?? 'star', color: (data.color as string) ?? '#6366f1', }); return habit.id; @@ -175,6 +220,17 @@ registerApp({ views: { list: { load: () => import('$lib/modules/notes/ListView.svelte') }, }, + contextMenuActions: [ + { + id: 'new-note', + label: 'Neue Notiz', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'notes', action: 'new' } }) + ), + }, + ], collection: 'notes', paramKey: 'noteId', dragType: 'note', diff --git a/apps/manacore/apps/web/src/lib/app-registry/types.ts b/apps/manacore/apps/web/src/lib/app-registry/types.ts index 17e5ace0e..622825be1 100644 --- a/apps/manacore/apps/web/src/lib/app-registry/types.ts +++ b/apps/manacore/apps/web/src/lib/app-registry/types.ts @@ -10,6 +10,18 @@ import type { DragType } from '@manacore/shared-ui/dnd'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyComponent = import('svelte').Component; +export type ContextMenuLocation = 'card' | 'tab' | 'nav' | 'page'; + +export interface AppContextMenuAction { + id: string; + label: string; + icon?: AnyComponent; + shortcut?: string; + /** Only show in certain contexts (default: all) */ + showIn?: ContextMenuLocation[]; + action: () => void; +} + export interface ViewLoader { load: () => Promise<{ default: AnyComponent }>; } @@ -41,6 +53,9 @@ export interface AppDescriptor { >; createItem?: (data: Record) => Promise; getDisplayData?: (item: Record) => EntityDisplayData; + + // -- Context Menu (optional) -- + contextMenuActions?: AppContextMenuAction[]; } export interface DropResult { diff --git a/apps/manacore/apps/web/src/lib/components/page-carousel/PageCarousel.svelte b/apps/manacore/apps/web/src/lib/components/page-carousel/PageCarousel.svelte index f81788c20..f4c0775ae 100644 --- a/apps/manacore/apps/web/src/lib/components/page-carousel/PageCarousel.svelte +++ b/apps/manacore/apps/web/src/lib/components/page-carousel/PageCarousel.svelte @@ -26,6 +26,7 @@ onMaximize: (id: string) => void; onRemove: (id: string) => void; onTogglePicker: () => void; + onTabContextMenu?: (e: MouseEvent, pageId: string) => void; addLabel?: string; page: Snippet<[CarouselPage, number]>; picker?: Snippet; @@ -40,6 +41,7 @@ onMaximize, onRemove, onTogglePicker, + onTabContextMenu, addLabel = 'Hinzufügen', page: pageSnippet, picker, @@ -136,7 +138,8 @@ {#if minimizedPages.length > 0}
{#each minimizedPages as p (p.id)} -
+ +
onTabContextMenu?.(e, p.id)}>