Document the user's preference (cards over subroutes), the migration pattern (module ListView + registerApp + thin route wrapper), what's already shipped in batches 1 + 2, and the remaining backlog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.7 KiB
Workbench cards over subroutes — migration plan
Started 2026-04-21.
Why
User preference (2026-04-21):
"wir wollen möglichst alles als cards darstellen statt unterrouten"
The unified Mana app has a workbench that lets users compose scenes
from draggable cards. Every module already registers itself as a card
via registerApp() in apps/mana/apps/web/src/lib/app-registry/apps.ts
so the list/detail view shows up when the user drops the app into a
scene.
Secondary surfaces — admin panels, per-module settings, filtered
lists (archive/favorites), preference editors — were historically
built as dedicated subroutes (/admin/users, /broadcasts/settings,
/news/preferences, …). That forces a URL round-trip and fragments
the UX: you can't have "members" and "broadcast settings" side-by-side
in the same scene without flipping tabs.
Convention going forward: secondary surfaces ship as workbench cards first. If a subroute already exists and should stay navigable by URL, we keep the route as a thin wrapper that renders the same ListView the workbench uses.
Pattern
For every surface being migrated:
apps/mana/apps/web/src/
├── lib/modules/{id}/
│ └── ListView.svelte # self-contained pane
├── lib/app-registry/apps.ts # registerApp({ id, views: { list } })
└── routes/(app)/{path}/+page.svelte # thin wrapper: `<ListView />`
Rules of thumb:
- Pane chrome: ListView wraps its own content in a
.panecontainer withmax-width: 720px(or similar) and theme tokens (hsl(var(--color-foreground)),hsl(var(--color-card)),hsl(var(--color-border))). No<PageHeader>— the workbench provides scene chrome, and the route wrapper works without one. - No MANA_APPS entry for power-user cards (admin/settings).
MANA_APPS is the app-drawer; admin panels shouldn't appear there.
Only add a MANA_APPS entry when the surface is user-facing
(e.g.
spaces). - Admin-role guard inline for admin cards: the workbench doesn't
run the
/admin/+layout.svelteguard, so each admin ListView checksauthStore.user?.role === 'admin'and renders a fallbackAdmin-onlygate-screen for non-admins. - Wrap existing form components where the subroute was already
thin (e.g.
/broadcasts/settingsdelegated toSettingsForm); the ListView just wraps that component + a small bar heading. - Route wrapper is 10 lines:
<script lang="ts"> import ListView from '$lib/modules/{id}/ListView.svelte'; </script> <ListView />
Out of scope (stay as routes)
- Detail pages with
[id]parameters — the URL is the entity identifier (notes/[id],contacts/[id],events/[id],admin/user-data/[userId],broadcasts/[id]/edit, …). - Deeply nested flows —
citycorners/cities/[slug]/locations/[id]etc. Every level as a card would explode the registry and break deep-linking. - Auth-critical pages needing SSR redirects — login/register/recovery.
Shipped batches
Batch 1 — Spaces (commit 88eca8a75, 2026-04-21)
| Card id | Route | Module folder |
|---|---|---|
spaces |
/spaces (new canonical) + /spaces/members (legacy alias) |
lib/modules/spaces/ |
Also: APP_ICONS.spaces + MANA_APPS entry + SpaceSwitcher link
updated to /spaces.
Batch 2 — Admin + Module Settings (commit 92fe23d46, 2026-04-21)
Admin cards (role-gated inline):
| Card id | Route | Source |
|---|---|---|
admin-users |
/admin/users |
extracted from route |
admin-system |
/admin/system |
extracted from route |
admin-user-data |
/admin/user-data |
extracted from route |
| (existing) | /admin/complexity |
route now wraps complexity module |
Module settings cards:
| Card id | Route | Source |
|---|---|---|
broadcast-settings |
/broadcasts/settings |
wraps broadcast/components/SettingsForm |
invoices-settings |
/invoices/settings |
wraps invoices/components/SenderProfileForm |
uload-settings |
/uload/settings |
extracted from route |
news-preferences |
/news/preferences |
extracted from route |
Backlog
High-value next batch (archive/filtered-list surfaces)
All of these are single-view pages inside existing modules — the content is self-contained and benefits from being scene-composable next to the module's main ListView.
chat/archive,chat/templatesmemoro/archive,memoro/tagspicture/archive,picture/boardstorage/favorites,storage/trash,storage/searchphotos/favorites,photos/albums,photos/uploadquotes/categories,quotes/favorites,quotes/listsnews/saved,news/sources,news/addmeditate/history,food/history,food/goals,food/addplants/tags,plants/addmoodlit/moods,moodlit/sequencescards/decks,cards/explore,cards/progressmusic/library,music/playlists,music/projectsinventory/categories,inventory/locations,inventory/search,inventory/collectionsskilltree/achievements,skilltree/treecalendar/calendarsuload/tags,uload/links,uload/analytics/[id](detail — skip)times/clients,times/projects,times/reports,times/templates,times/entries,times/clockagents/templatestimeline/analyticsresearch-lab/keys
Second-tier (settings-style, likely worth migrating)
todo/settings(367 lines — non-trivial, worth a separate batch)
Admin-adjacent (not yet migrated)
organizations,teams— technically post-Spaces these routes are dead ends (users are told to use Spaces instead). Either migrate to cards or delete.gifts,gifts/redeem— probably keep as routes (shareable)feedback,observatory,llm-test,tags— already registered as cards or thin enough to leave.
Open questions
- Should the
/admin/+layout.sveltenav tabs be removed once every sub-page is a card? Cards make the sidebar nav redundant, but the admin role guard still lives in the layout so removing it means moving that guard somewhere else. Decision deferred until the full admin batch is shipped. - Do we want a convention for registering sibling views of the
same module (e.g.
chat-archivealongsidechat)? Current pattern uses separate ids. Works, but clutters the registry.