From f2af192172cb4d8e4a5ec75f4779da1faa447802 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 14:13:28 +0200 Subject: [PATCH] feat(manacore/web): workbench with app pages carousel on home Replace the static app-grid home page with a workbench carousel where each app renders as a paper-sheet page that can be minimized, maximized, reordered, and resized: - AppPage.svelte: paper-sheet shell with lazy-loaded app modules - AppPagePicker.svelte: picker showing all 23 available apps - App component registry: maps appId to dynamic AppView imports - Horizontal fokus-track carousel with snap-scroll - Edit FAB with width pills (S/M/L/XL) - Minimized tabs bar for collapsed apps - Drag-and-drop reordering + arrow buttons in edit mode - Workbench state persisted to IndexedDB via shared-stores - Default layout: Todo + Calendar + Contacts Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/components/workbench/AppPage.svelte | 357 +++++++ .../components/workbench/AppPagePicker.svelte | 167 +++ .../lib/components/workbench/app-registry.ts | 159 +++ .../web/src/routes/(app)/home/+page.svelte | 994 ++++++++++-------- 4 files changed, 1249 insertions(+), 428 deletions(-) create mode 100644 apps/manacore/apps/web/src/lib/components/workbench/AppPage.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/workbench/AppPagePicker.svelte create mode 100644 apps/manacore/apps/web/src/lib/components/workbench/app-registry.ts diff --git a/apps/manacore/apps/web/src/lib/components/workbench/AppPage.svelte b/apps/manacore/apps/web/src/lib/components/workbench/AppPage.svelte new file mode 100644 index 000000000..9fdc5aaf7 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/workbench/AppPage.svelte @@ -0,0 +1,357 @@ + + + +
+
+ +
+ + + {#if editMode} +
+
+ {#if !isFirst && onMoveLeft} + + {/if} + {#if !isLast && onMoveRight} + + {/if} +
+ +
+ {/if} + + + + + +
+ {#if loadError} +
+

App konnte nicht geladen werden

+
+ {:else if AppComponent} + + {:else} +
+ +
+ {/if} +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/components/workbench/AppPagePicker.svelte b/apps/manacore/apps/web/src/lib/components/workbench/AppPagePicker.svelte new file mode 100644 index 000000000..8cbbc9f0e --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/workbench/AppPagePicker.svelte @@ -0,0 +1,167 @@ + + + +
+
+

App hinzufügen

+ +
+
+ {#each availableApps as app, i (app.id)} + {#if i > 0}
{/if} + + {/each} + + {#if availableApps.length === 0} +

Alle Apps sind bereits geöffnet

+ {/if} +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/components/workbench/app-registry.ts b/apps/manacore/apps/web/src/lib/components/workbench/app-registry.ts new file mode 100644 index 000000000..64bb418af --- /dev/null +++ b/apps/manacore/apps/web/src/lib/components/workbench/app-registry.ts @@ -0,0 +1,159 @@ +/** + * App Component Registry — Maps app IDs to lazy-loaded AppView components. + * + * Each entry provides the dynamic import for embedding in the workbench carousel. + */ + +import type { Component } from 'svelte'; + +export interface AppEntry { + id: string; + name: string; + color: string; + load: () => Promise<{ default: Component }>; +} + +export const APP_REGISTRY: AppEntry[] = [ + { + id: 'todo', + name: 'Todo', + color: '#8B5CF6', + load: () => import('$lib/modules/todo/AppView.svelte'), + }, + { + id: 'calendar', + name: 'Kalender', + color: '#3B82F6', + load: () => import('$lib/modules/calendar/AppView.svelte'), + }, + { + id: 'contacts', + name: 'Kontakte', + color: '#22C55E', + load: () => import('$lib/modules/contacts/AppView.svelte'), + }, + { + id: 'chat', + name: 'Chat', + color: '#6366F1', + load: () => import('$lib/modules/chat/AppView.svelte'), + }, + { + id: 'times', + name: 'Times', + color: '#F59E0B', + load: () => import('$lib/modules/times/AppView.svelte'), + }, + { + id: 'zitare', + name: 'Zitare', + color: '#EC4899', + load: () => import('$lib/modules/zitare/AppView.svelte'), + }, + { + id: 'cards', + name: 'Cards', + color: '#EF4444', + load: () => import('$lib/modules/cards/AppView.svelte'), + }, + { + id: 'picture', + name: 'Picture', + color: '#8B5CF6', + load: () => import('$lib/modules/picture/AppView.svelte'), + }, + { + id: 'mukke', + name: 'Mukke', + color: '#F97316', + load: () => import('$lib/modules/mukke/AppView.svelte'), + }, + { + id: 'photos', + name: 'Photos', + color: '#06B6D4', + load: () => import('$lib/modules/photos/AppView.svelte'), + }, + { + id: 'storage', + name: 'Storage', + color: '#6B7280', + load: () => import('$lib/modules/storage/AppView.svelte'), + }, + { + id: 'nutriphi', + name: 'Nutriphi', + color: '#22C55E', + load: () => import('$lib/modules/nutriphi/AppView.svelte'), + }, + { + id: 'planta', + name: 'Planta', + color: '#16A34A', + load: () => import('$lib/modules/planta/AppView.svelte'), + }, + { + id: 'presi', + name: 'Presi', + color: '#A855F7', + load: () => import('$lib/modules/presi/AppView.svelte'), + }, + { + id: 'inventar', + name: 'Inventar', + color: '#78716C', + load: () => import('$lib/modules/inventar/AppView.svelte'), + }, + { + id: 'memoro', + name: 'Memoro', + color: '#F59E0B', + load: () => import('$lib/modules/memoro/AppView.svelte'), + }, + { + id: 'questions', + name: 'Questions', + color: '#2563EB', + load: () => import('$lib/modules/questions/AppView.svelte'), + }, + { + id: 'skilltree', + name: 'SkillTree', + color: '#D946EF', + load: () => import('$lib/modules/skilltree/AppView.svelte'), + }, + { + id: 'moodlit', + name: 'Moodlit', + color: '#F97316', + load: () => import('$lib/modules/moodlit/AppView.svelte'), + }, + { + id: 'citycorners', + name: 'CityCorners', + color: '#14B8A6', + load: () => import('$lib/modules/citycorners/AppView.svelte'), + }, + { + id: 'uload', + name: 'uLoad', + color: '#0EA5E9', + load: () => import('$lib/modules/uload/AppView.svelte'), + }, + { + id: 'calc', + name: 'Calc', + color: '#6B7280', + load: () => import('$lib/modules/calc/AppView.svelte'), + }, + { + id: 'playground', + name: 'Playground', + color: '#9CA3AF', + load: () => import('$lib/modules/playground/AppView.svelte'), + }, +]; + +export function getAppEntry(appId: string): AppEntry | undefined { + return APP_REGISTRY.find((a) => a.id === appId); +} diff --git a/apps/manacore/apps/web/src/routes/(app)/home/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/home/+page.svelte index b541b0521..9ab7876e2 100644 --- a/apps/manacore/apps/web/src/routes/(app)/home/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/home/+page.svelte @@ -1,486 +1,624 @@ -
- -
-
-

- {greeting}, {userName} -

-

- {#if currentLocale === 'en'} - Your Mana ecosystem — {totalApps} apps, {liveApps} live - {:else} - Dein Mana-Ökosystem — {totalApps} Apps, {liveApps} live - {/if} -

+ + Home - ManaCore + + +
+ + {#if editMode} +
+
+ {#each WIDTH_OPTIONS as opt (opt.id)} + + {/each} +
- -
+ {/if} - - store.toggleFavorite(id)} - pinnedIds={store.favorites} - /> - - - - - - - - -
-

- {currentLocale === 'en' ? 'All Apps' : 'Alle Apps'} -

- -
- {#each sortedApps as app (app.id)} - - {/each} -
-
- - -
- {currentLocale === 'en' ? 'Status' : 'Status'}: - {#each Object.entries(statusLabels) as [status, label]} - - - {label} - + +
+ {#each expandedApps as app, idx (app.appId)} + +
handleDragStart(e, app.appId)} + ondragover={handleDragOver} + ondrop={(e) => handleDrop(e, app.appId)} + ondragend={handleDragEnd} + > + handleRemoveApp(app.appId)} + onMinimize={() => handleMinimizeApp(app.appId)} + onMaximize={() => handleMaximizeApp(app.appId)} + onMoveLeft={editMode ? () => handleMoveLeft(app.appId) : undefined} + onMoveRight={editMode ? () => handleMoveRight(app.appId) : undefined} + /> +
{/each} + + + {#if expandedApps.length === 0} +
+ {#if showPicker} + (showPicker = false)} + activeAppIds={openApps.map((a) => a.appId)} + /> + {:else} + + {/if} +
+ {:else if showPicker} +
+ (showPicker = false)} + activeAppIds={openApps.map((a) => a.appId)} + /> +
+ {:else} + + {/if}
+ + + {#if minimizedApps.length > 0} +
+ {#each minimizedApps as app (app.appId)} +
+ + + + +
+ {/each} + +
+ {/if} + + +