refactor(manacore/web): merge entity + app registries into unified AppDescriptor

Replace the dual registry system (app-registry.ts + entities/) with a
single AppDescriptor that contains identity, views, and entity fields.

- Create $lib/app-registry/ with types.ts, registry.ts, apps.ts, index.ts
- Merge all 27 app entries + 3 entity descriptors into one registry
- Move ViewProps from nav-stack.ts into app-registry/types.ts
- Update all 39 consumer files (ListViews, DetailViews, AppPage, etc.)
- Delete old files: entities/, app-registry.ts, nav-stack.ts, entity.ts

Single source of truth: one AppDescriptor per module, one registry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 14:01:27 +02:00
parent 9abbf9c70d
commit c01eccb852
51 changed files with 617 additions and 699 deletions

View file

@ -0,0 +1,432 @@
/**
* Unified App Registrations All app descriptors in one file.
*
* Apps with entity capabilities (todo, calendar, contacts) include
* collection, paramKey, dragType, etc. for cross-module DnD and linking.
* All other apps only declare identity + views.
*/
import { registerApp } from './registry';
// ── Apps with entity capabilities ───────────────────────────
registerApp({
id: 'todo',
name: 'Todo',
color: '#8B5CF6',
views: {
list: { load: () => import('$lib/modules/todo/ListView.svelte') },
detail: { load: () => import('$lib/modules/todo/views/DetailView.svelte') },
},
collection: 'tasks',
paramKey: 'taskId',
dragType: 'task',
acceptsDropFrom: ['event', 'contact'],
transformIncoming: {
event: (source) => ({
title: source.title as string,
dueDate: source.startDate as string,
description: source.description as string | undefined,
}),
contact: (source) => ({
title: `Kontaktieren: ${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
}),
},
getDisplayData: (item) => ({
title: (item.title as string) || 'Aufgabe',
subtitle: item.dueDate ? new Date(item.dueDate as string).toLocaleDateString('de') : undefined,
}),
createItem: async (data) => {
const { tasksStore } = await import('$lib/modules/todo/stores/tasks.svelte');
const task = await tasksStore.createTask(
data as { title: string; dueDate?: string; description?: string }
);
return task.id;
},
});
registerApp({
id: 'calendar',
name: 'Kalender',
color: '#3B82F6',
views: {
list: { load: () => import('$lib/modules/calendar/ListView.svelte') },
detail: { load: () => import('$lib/modules/calendar/views/DetailView.svelte') },
},
collection: 'events',
paramKey: 'eventId',
dragType: 'event',
acceptsDropFrom: ['task', 'contact'],
transformIncoming: {
task: (source) => {
const dueDate = (source.dueDate as string) || new Date().toISOString();
const start = new Date(dueDate);
const end = new Date(start.getTime() + 60 * 60 * 1000);
return {
title: source.title as string,
startTime: start.toISOString(),
endTime: end.toISOString(),
description: source.description as string | undefined,
};
},
contact: (source) => {
const name = [source.firstName, source.lastName].filter(Boolean).join(' ');
const now = new Date();
const end = new Date(now.getTime() + 60 * 60 * 1000);
return {
title: `Treffen mit ${name}`,
startTime: now.toISOString(),
endTime: end.toISOString(),
};
},
},
getDisplayData: (item) => ({
title: (item.title as string) || 'Termin',
subtitle: item.startDate
? new Date(item.startDate as string).toLocaleDateString('de', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
: undefined,
}),
createItem: async (data) => {
const { db } = await import('$lib/data/database');
const { eventsStore } = await import('$lib/modules/calendar/stores/events.svelte');
const calendars = await db.table('calendars').toArray();
const defaultCal = calendars.find((c: Record<string, unknown>) => !c.deletedAt);
const calendarId = (defaultCal?.id as string) ?? 'default';
const result = await eventsStore.createEvent({
calendarId,
title: data.title as string,
startTime: data.startTime as string,
endTime: data.endTime as string,
description: (data.description as string) ?? undefined,
});
if (!result.success || !result.data) throw new Error(result.error || 'Failed to create event');
return result.data.id;
},
});
registerApp({
id: 'contacts',
name: 'Kontakte',
color: '#22C55E',
views: {
list: { load: () => import('$lib/modules/contacts/ListView.svelte') },
detail: { load: () => import('$lib/modules/contacts/views/DetailView.svelte') },
},
collection: 'contacts',
paramKey: 'contactId',
dragType: 'contact',
getDisplayData: (item) => {
const name = [item.firstName, item.lastName].filter(Boolean).join(' ');
return {
title: name || (item.email as string) || 'Kontakt',
subtitle: (item.company as string) ?? undefined,
};
},
// Contacts are drag sources only -- dropping onto contacts doesn't create a new contact
});
// ── Apps without entity capabilities ────────────────────────
registerApp({
id: 'habits',
name: 'Habits',
color: '#8B5CF6',
views: {
list: { load: () => import('$lib/modules/habits/ListView.svelte') },
},
collection: 'habits',
paramKey: 'habitId',
dragType: 'habit',
acceptsDropFrom: ['task'],
transformIncoming: {
task: (source) => ({
title: source.title as string,
emoji: '\u{1F4AA}',
color: '#6366f1',
}),
},
getDisplayData: (item) => ({
title: `${item.emoji as string} ${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}',
color: (data.color as string) ?? '#6366f1',
});
return habit.id;
},
});
registerApp({
id: 'notes',
name: 'Notes',
color: '#F59E0B',
views: {
list: { load: () => import('$lib/modules/notes/ListView.svelte') },
},
collection: 'notes',
paramKey: 'noteId',
dragType: 'note',
acceptsDropFrom: ['task', 'contact'],
transformIncoming: {
task: (source) => ({
title: source.title as string,
content: (source.description as string) ?? '',
}),
contact: (source) => ({
title: `${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
content: `Kontakt: ${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
}),
},
getDisplayData: (item) => ({
title: (item.title as string) || 'Notiz',
subtitle: undefined,
}),
createItem: async (data) => {
const { notesStore } = await import('$lib/modules/notes/stores/notes.svelte');
const note = await notesStore.createNote({
title: data.title as string,
content: (data.content as string) ?? '',
});
return note.id;
},
});
registerApp({
id: 'finance',
name: 'Finance',
color: '#22C55E',
views: {
list: { load: () => import('$lib/modules/finance/ListView.svelte') },
},
collection: 'transactions',
paramKey: 'transactionId',
dragType: 'transaction',
acceptsDropFrom: [],
getDisplayData: (item) => ({
title: (item.description as string) || 'Transaktion',
subtitle: item.amount ? `${item.type === 'income' ? '+' : '-'}${item.amount}` : undefined,
}),
createItem: async (data) => {
const { financeStore } = await import('$lib/modules/finance/stores/finance.svelte');
const tx = await financeStore.addTransaction({
type: 'expense',
amount: (data.amount as number) ?? 0,
description: (data.title as string) ?? (data.description as string) ?? '',
});
return tx.id;
},
});
registerApp({
id: 'chat',
name: 'Chat',
color: '#6366F1',
views: {
list: { load: () => import('$lib/modules/chat/ListView.svelte') },
},
});
registerApp({
id: 'context',
name: 'Context',
color: '#7C3AED',
views: {
list: { load: () => import('$lib/modules/context/ListView.svelte') },
},
});
registerApp({
id: 'times',
name: 'Times',
color: '#F59E0B',
views: {
list: { load: () => import('$lib/modules/times/ListView.svelte') },
detail: { load: () => import('$lib/modules/times/views/DetailView.svelte') },
},
});
registerApp({
id: 'zitare',
name: 'Zitare',
color: '#EC4899',
views: {
list: { load: () => import('$lib/modules/zitare/ListView.svelte') },
detail: { load: () => import('$lib/modules/zitare/views/DetailView.svelte') },
},
});
registerApp({
id: 'cards',
name: 'Cards',
color: '#EF4444',
views: {
list: { load: () => import('$lib/modules/cards/ListView.svelte') },
detail: { load: () => import('$lib/modules/cards/views/DetailView.svelte') },
},
});
registerApp({
id: 'picture',
name: 'Picture',
color: '#8B5CF6',
views: {
list: { load: () => import('$lib/modules/picture/ListView.svelte') },
},
});
registerApp({
id: 'mukke',
name: 'Mukke',
color: '#F97316',
views: {
list: { load: () => import('$lib/modules/mukke/ListView.svelte') },
detail: { load: () => import('$lib/modules/mukke/views/DetailView.svelte') },
},
});
registerApp({
id: 'photos',
name: 'Photos',
color: '#06B6D4',
views: {
list: { load: () => import('$lib/modules/photos/ListView.svelte') },
},
});
registerApp({
id: 'storage',
name: 'Storage',
color: '#6B7280',
views: {
list: { load: () => import('$lib/modules/storage/ListView.svelte') },
detail: { load: () => import('$lib/modules/storage/views/DetailView.svelte') },
},
});
registerApp({
id: 'nutriphi',
name: 'Nutriphi',
color: '#22C55E',
views: {
list: { load: () => import('$lib/modules/nutriphi/ListView.svelte') },
},
});
registerApp({
id: 'planta',
name: 'Planta',
color: '#16A34A',
views: {
list: { load: () => import('$lib/modules/planta/ListView.svelte') },
detail: { load: () => import('$lib/modules/planta/views/DetailView.svelte') },
},
});
registerApp({
id: 'presi',
name: 'Presi',
color: '#A855F7',
views: {
list: { load: () => import('$lib/modules/presi/ListView.svelte') },
detail: { load: () => import('$lib/modules/presi/views/DetailView.svelte') },
},
});
registerApp({
id: 'inventar',
name: 'Inventar',
color: '#78716C',
views: {
list: { load: () => import('$lib/modules/inventar/ListView.svelte') },
detail: { load: () => import('$lib/modules/inventar/views/DetailView.svelte') },
},
});
registerApp({
id: 'memoro',
name: 'Memoro',
color: '#F59E0B',
views: {
list: { load: () => import('$lib/modules/memoro/ListView.svelte') },
detail: { load: () => import('$lib/modules/memoro/views/DetailView.svelte') },
},
});
registerApp({
id: 'questions',
name: 'Questions',
color: '#2563EB',
views: {
list: { load: () => import('$lib/modules/questions/ListView.svelte') },
detail: { load: () => import('$lib/modules/questions/views/DetailView.svelte') },
},
});
registerApp({
id: 'skilltree',
name: 'SkillTree',
color: '#D946EF',
views: {
list: { load: () => import('$lib/modules/skilltree/ListView.svelte') },
detail: { load: () => import('$lib/modules/skilltree/views/DetailView.svelte') },
},
});
registerApp({
id: 'moodlit',
name: 'Moodlit',
color: '#F97316',
views: {
list: { load: () => import('$lib/modules/moodlit/ListView.svelte') },
},
});
registerApp({
id: 'citycorners',
name: 'CityCorners',
color: '#14B8A6',
views: {
list: { load: () => import('$lib/modules/citycorners/ListView.svelte') },
detail: { load: () => import('$lib/modules/citycorners/views/DetailView.svelte') },
},
});
registerApp({
id: 'uload',
name: 'uLoad',
color: '#0EA5E9',
views: {
list: { load: () => import('$lib/modules/uload/ListView.svelte') },
detail: { load: () => import('$lib/modules/uload/views/DetailView.svelte') },
},
});
registerApp({
id: 'calc',
name: 'Calc',
color: '#6B7280',
views: {
list: { load: () => import('$lib/modules/calc/ListView.svelte') },
},
});
registerApp({
id: 'playground',
name: 'Playground',
color: '#9CA3AF',
views: {
list: { load: () => import('$lib/modules/playground/ListView.svelte') },
},
});

View file

@ -0,0 +1,15 @@
// Types
export type { AppDescriptor, ViewLoader, EntityDisplayData, DropResult, ViewProps } from './types';
// Registry
export {
registerApp,
getApp,
getAppByDragType,
canDrop,
executeDrop,
getAllApps,
} from './registry';
// Register all apps eagerly — descriptors are lightweight with lazy imports
import './apps';

View file

@ -1,30 +1,30 @@
/**
* Entity Registry Collects module descriptors and orchestrates cross-module drops.
* Unified App Registry Collects app descriptors and orchestrates cross-module drops.
*/
import type { DragType } from '@manacore/shared-ui/dnd';
import { linkMutations, buildCachedData } from '@manacore/shared-links';
import type { EntityDescriptor, DropResult } from './types';
import type { AppDescriptor, DropResult } from './types';
const entities = new Map<string, EntityDescriptor>();
const apps = new Map<string, AppDescriptor>();
export function registerEntity(descriptor: EntityDescriptor): void {
entities.set(descriptor.appId, descriptor);
export function registerApp(descriptor: AppDescriptor): void {
apps.set(descriptor.id, descriptor);
}
export function getEntity(appId: string): EntityDescriptor | undefined {
return entities.get(appId);
export function getApp(appId: string): AppDescriptor | undefined {
return apps.get(appId);
}
export function getEntityByDragType(type: DragType): EntityDescriptor | undefined {
for (const e of entities.values()) {
if (e.dragType === type) return e;
export function getAppByDragType(type: DragType): AppDescriptor | undefined {
for (const a of apps.values()) {
if (a.dragType === type) return a;
}
return undefined;
}
export function canDrop(sourceType: DragType, targetAppId: string): boolean {
const target = entities.get(targetAppId);
const target = apps.get(targetAppId);
if (!target?.acceptsDropFrom?.includes(sourceType)) return false;
if (!target.createItem) return false;
if (!target.transformIncoming?.[sourceType]) return false;
@ -36,14 +36,18 @@ export async function executeDrop(
sourceAppId: string,
targetAppId: string
): Promise<DropResult> {
const source = entities.get(sourceAppId);
const target = entities.get(targetAppId);
if (!source || !target)
throw new Error(`Entity not registered: ${sourceAppId} or ${targetAppId}`);
const source = apps.get(sourceAppId);
const target = apps.get(targetAppId);
if (!source || !target) throw new Error(`App not registered: ${sourceAppId} or ${targetAppId}`);
if (!target.createItem) throw new Error(`Target ${targetAppId} has no createItem`);
if (!source.dragType) throw new Error(`Source ${sourceAppId} has no dragType`);
if (!source.collection) throw new Error(`Source ${sourceAppId} has no collection`);
if (!target.collection) throw new Error(`Target ${targetAppId} has no collection`);
if (!source.getDisplayData) throw new Error(`Source ${sourceAppId} has no getDisplayData`);
if (!target.getDisplayData) throw new Error(`Target ${targetAppId} has no getDisplayData`);
const transform = target.transformIncoming?.[source.dragType];
if (!transform) throw new Error(`No transform for ${source.dragType}${targetAppId}`);
if (!transform) throw new Error(`No transform for ${source.dragType} -> ${targetAppId}`);
// 1. Transform source data into target shape
const transformedData = transform(sourceItem);
@ -74,6 +78,6 @@ export async function executeDrop(
return { newItemId, linkPairId: forward.pairId };
}
export function getAllEntities(): EntityDescriptor[] {
return Array.from(entities.values());
export function getAllApps(): AppDescriptor[] {
return Array.from(apps.values());
}

View file

@ -0,0 +1,55 @@
/**
* Unified App Registry Types
*
* Each app declares an AppDescriptor that describes identity, views,
* and optional entity capabilities (DnD, linking, cross-module drops).
*/
import type { DragType } from '@manacore/shared-ui/dnd';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyComponent = import('svelte').Component<any, any>;
export interface ViewLoader {
load: () => Promise<{ default: AnyComponent }>;
}
export interface EntityDisplayData {
title: string;
subtitle?: string;
}
export interface AppDescriptor {
// -- Identity --
id: string;
name: string;
color: string;
// -- Views --
views: {
list: ViewLoader;
detail?: ViewLoader;
};
// -- Entity (optional -- for DnD + linking) --
collection?: string;
paramKey?: string;
dragType?: DragType;
acceptsDropFrom?: DragType[];
transformIncoming?: Partial<
Record<DragType, (sourceItem: Record<string, unknown>) => Record<string, unknown>>
>;
createItem?: (data: Record<string, unknown>) => Promise<string>;
getDisplayData?: (item: Record<string, unknown>) => EntityDisplayData;
}
export interface DropResult {
newItemId: string;
linkPairId: string;
}
export interface ViewProps {
navigate: (viewName: string, params?: Record<string, unknown>) => void;
goBack: () => void;
params: Record<string, unknown>;
}

View file

@ -8,8 +8,8 @@
type ManaRecordRef,
type LocalManaLink,
} from '@manacore/shared-links';
import { getAppEntry } from '$lib/components/workbench/app-registry';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import { getApp } from '$lib/app-registry';
import type { ViewProps } from '$lib/app-registry';
interface Props {
recordRef: ManaRecordRef;
@ -34,7 +34,7 @@
<span class="section-label">Verknüpfungen</span>
<div class="links-list">
{#each links as link (link.id)}
{@const appEntry = getAppEntry(link.targetApp)}
{@const appEntry = getApp(link.targetApp)}
{@const color = link.cachedTarget?.color ?? appEntry?.color ?? '#6B7280'}
<button class="link-item" onclick={() => openLink(link)}>
<span class="link-dot" style="background: {color}"></span>

View file

@ -6,20 +6,11 @@
<script lang="ts">
import { X, CaretUp, CaretDown, ArrowLeft, SpinnerGap } from '@manacore/shared-icons';
import { PageShell } from '$lib/components/page-carousel';
import { getAppEntry } from './app-registry';
import { getApp, getAppByDragType, canDrop, executeDrop } from '$lib/app-registry';
import type { Component } from 'svelte';
import { dropTarget } from '@manacore/shared-ui/dnd';
import {
getEntity,
getEntityByDragType,
canDrop,
executeDrop,
ensureEntitiesRegistered,
} from '$lib/entities';
import type { DragPayload } from '@manacore/shared-ui/dnd';
ensureEntitiesRegistered();
interface Props {
appId: string;
widthPx: number;
@ -46,18 +37,17 @@
onMoveRight,
}: Props = $props();
let appEntry = $derived(getAppEntry(appId));
let appName = $derived(appEntry?.name ?? appId);
let appColor = $derived(appEntry?.color ?? '#6B7280');
let app = $derived(getApp(appId));
let appName = $derived(app?.name ?? appId);
let appColor = $derived(app?.color ?? '#6B7280');
// ── Cross-module drop target ────────────────────────────
let targetEntity = $derived(getEntity(appId));
let acceptedDropTypes = $derived(targetEntity?.acceptsDropFrom ?? []);
let acceptedDropTypes = $derived(app?.acceptsDropFrom ?? []);
function handleCrossModuleDrop(payload: DragPayload) {
const sourceEntity = getEntityByDragType(payload.type);
if (!sourceEntity) return;
executeDrop(payload.data as Record<string, unknown>, sourceEntity.appId, appId);
const sourceApp = getAppByDragType(payload.type);
if (!sourceApp) return;
executeDrop(payload.data as Record<string, unknown>, sourceApp.id, appId);
}
// ── List View (always loaded) ───────────────────────────
@ -67,8 +57,8 @@
$effect(() => {
ListComponent = null;
loadError = false;
if (appEntry) {
const loader = appEntry.views?.list?.load ?? appEntry.load;
if (app) {
const loader = app.views.list.load;
loader().then(
(mod) => (ListComponent = mod.default),
() => (loadError = true)
@ -120,10 +110,9 @@
const targetId = params._targetId as string;
if (!targetApp || !targetId) return;
const targetEntity = getEntity(targetApp);
const targetAppEntry = getAppEntry(targetApp);
const targetViewEntry = targetAppEntry?.views?.detail;
if (!targetViewEntry || !targetEntity) {
const targetAppDesc = getApp(targetApp);
const targetViewEntry = targetAppDesc?.views?.detail;
if (!targetViewEntry || !targetAppDesc?.paramKey) {
console.warn(`No detail view registered for app "${targetApp}"`);
return;
}
@ -133,10 +122,10 @@
...overlayStack,
{
viewName: 'cross-detail',
params: { [targetEntity.paramKey]: targetId },
params: { [targetAppDesc.paramKey!]: targetId },
component: mod.default,
overlayColor: targetAppEntry?.color,
overlayTitle: targetAppEntry?.name,
overlayColor: targetAppDesc.color,
overlayTitle: targetAppDesc.name,
},
];
});
@ -144,7 +133,7 @@
}
// Normal detail view within the same app
const viewEntry = appEntry?.views?.[viewName];
const viewEntry = viewName === 'detail' ? app?.views?.detail : undefined;
if (!viewEntry) {
console.warn(`View "${viewName}" not registered for app "${appId}"`);
return;

View file

@ -4,7 +4,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { X } from '@manacore/shared-icons';
import { APP_REGISTRY } from './app-registry';
import { getAllApps } from '$lib/app-registry';
interface Props {
onSelect: (appId: string) => void;
@ -14,7 +14,7 @@
let { onSelect, onClose, activeAppIds = [] }: Props = $props();
let availableApps = $derived(APP_REGISTRY.filter((app) => !activeAppIds.includes(app.id)));
let availableApps = $derived(getAllApps().filter((app) => !activeAppIds.includes(app.id)));
</script>
<div class="app-picker">

View file

@ -1,238 +0,0 @@
/**
* App Component Registry Maps app IDs to lazy-loaded views.
*
* Each entry provides a default `load` (list view) and optional named `views`
* for in-panel navigation (detail, create, edit, etc.).
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyComponent = import('svelte').Component<any, any>;
export interface ViewEntry {
load: () => Promise<{ default: AnyComponent }>;
}
export interface AppEntry {
id: string;
name: string;
color: string;
/** Default view loader (list/main view). */
load: () => Promise<{ default: AnyComponent }>;
/** Named views for in-panel navigation. Fallback: { list: load }. */
views?: Record<string, ViewEntry>;
}
export const APP_REGISTRY: AppEntry[] = [
{
id: 'todo',
name: 'Todo',
color: '#8B5CF6',
load: () => import('$lib/modules/todo/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/todo/ListView.svelte') },
detail: { load: () => import('$lib/modules/todo/views/DetailView.svelte') },
},
},
{
id: 'calendar',
name: 'Kalender',
color: '#3B82F6',
load: () => import('$lib/modules/calendar/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/calendar/ListView.svelte') },
detail: { load: () => import('$lib/modules/calendar/views/DetailView.svelte') },
},
},
{
id: 'contacts',
name: 'Kontakte',
color: '#22C55E',
load: () => import('$lib/modules/contacts/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/contacts/ListView.svelte') },
detail: { load: () => import('$lib/modules/contacts/views/DetailView.svelte') },
},
},
{
id: 'habits',
name: 'Habits',
color: '#8B5CF6',
load: () => import('$lib/modules/habits/ListView.svelte'),
},
{
id: 'chat',
name: 'Chat',
color: '#6366F1',
load: () => import('$lib/modules/chat/ListView.svelte'),
},
{
id: 'times',
name: 'Times',
color: '#F59E0B',
load: () => import('$lib/modules/times/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/times/ListView.svelte') },
detail: { load: () => import('$lib/modules/times/views/DetailView.svelte') },
},
},
{
id: 'zitare',
name: 'Zitare',
color: '#EC4899',
load: () => import('$lib/modules/zitare/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/zitare/ListView.svelte') },
detail: { load: () => import('$lib/modules/zitare/views/DetailView.svelte') },
},
},
{
id: 'cards',
name: 'Cards',
color: '#EF4444',
load: () => import('$lib/modules/cards/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/cards/ListView.svelte') },
detail: { load: () => import('$lib/modules/cards/views/DetailView.svelte') },
},
},
{
id: 'picture',
name: 'Picture',
color: '#8B5CF6',
load: () => import('$lib/modules/picture/ListView.svelte'),
},
{
id: 'mukke',
name: 'Mukke',
color: '#F97316',
load: () => import('$lib/modules/mukke/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/mukke/ListView.svelte') },
detail: { load: () => import('$lib/modules/mukke/views/DetailView.svelte') },
},
},
{
id: 'photos',
name: 'Photos',
color: '#06B6D4',
load: () => import('$lib/modules/photos/ListView.svelte'),
},
{
id: 'storage',
name: 'Storage',
color: '#6B7280',
load: () => import('$lib/modules/storage/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/storage/ListView.svelte') },
detail: { load: () => import('$lib/modules/storage/views/DetailView.svelte') },
},
},
{
id: 'nutriphi',
name: 'Nutriphi',
color: '#22C55E',
load: () => import('$lib/modules/nutriphi/ListView.svelte'),
},
{
id: 'planta',
name: 'Planta',
color: '#16A34A',
load: () => import('$lib/modules/planta/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/planta/ListView.svelte') },
detail: { load: () => import('$lib/modules/planta/views/DetailView.svelte') },
},
},
{
id: 'presi',
name: 'Presi',
color: '#A855F7',
load: () => import('$lib/modules/presi/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/presi/ListView.svelte') },
detail: { load: () => import('$lib/modules/presi/views/DetailView.svelte') },
},
},
{
id: 'inventar',
name: 'Inventar',
color: '#78716C',
load: () => import('$lib/modules/inventar/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/inventar/ListView.svelte') },
detail: { load: () => import('$lib/modules/inventar/views/DetailView.svelte') },
},
},
{
id: 'memoro',
name: 'Memoro',
color: '#F59E0B',
load: () => import('$lib/modules/memoro/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/memoro/ListView.svelte') },
detail: { load: () => import('$lib/modules/memoro/views/DetailView.svelte') },
},
},
{
id: 'questions',
name: 'Questions',
color: '#2563EB',
load: () => import('$lib/modules/questions/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/questions/ListView.svelte') },
detail: { load: () => import('$lib/modules/questions/views/DetailView.svelte') },
},
},
{
id: 'skilltree',
name: 'SkillTree',
color: '#D946EF',
load: () => import('$lib/modules/skilltree/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/skilltree/ListView.svelte') },
detail: { load: () => import('$lib/modules/skilltree/views/DetailView.svelte') },
},
},
{
id: 'moodlit',
name: 'Moodlit',
color: '#F97316',
load: () => import('$lib/modules/moodlit/ListView.svelte'),
},
{
id: 'citycorners',
name: 'CityCorners',
color: '#14B8A6',
load: () => import('$lib/modules/citycorners/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/citycorners/ListView.svelte') },
detail: { load: () => import('$lib/modules/citycorners/views/DetailView.svelte') },
},
},
{
id: 'uload',
name: 'uLoad',
color: '#0EA5E9',
load: () => import('$lib/modules/uload/ListView.svelte'),
views: {
list: { load: () => import('$lib/modules/uload/ListView.svelte') },
detail: { load: () => import('$lib/modules/uload/views/DetailView.svelte') },
},
},
{
id: 'calc',
name: 'Calc',
color: '#6B7280',
load: () => import('$lib/modules/calc/ListView.svelte'),
},
{
id: 'playground',
name: 'Playground',
color: '#9CA3AF',
load: () => import('$lib/modules/playground/ListView.svelte'),
},
];
export function getAppEntry(appId: string): AppEntry | undefined {
return APP_REGISTRY.find((a) => a.id === appId);
}

View file

@ -1,20 +0,0 @@
/**
* Panel Navigation Stack Types for in-panel view navigation.
*
* Each workbench panel manages its own navigation stack.
* Views are pushed/popped within the panel (list detail edit).
*/
import type { Component } from 'svelte';
export interface NavFrame {
viewName: string;
params: Record<string, unknown>;
component: Component | null;
}
export interface ViewProps {
navigate: (viewName: string, params?: Record<string, unknown>) => void;
goBack: () => void;
params: Record<string, unknown>;
}

View file

@ -1,23 +0,0 @@
// Types
export type { EntityDescriptor, EntityDisplayData, DropResult } from './types';
// Registry
export {
registerEntity,
getEntity,
getEntityByDragType,
canDrop,
executeDrop,
getAllEntities,
} from './registry';
// Register module entities eagerly — these are lightweight descriptor files
// with no heavy dependencies (stores are only called at drop time, not import time).
import '$lib/modules/todo/entity';
import '$lib/modules/calendar/entity';
import '$lib/modules/contacts/entity';
// Re-export for consumers that previously used lazy registration
export function ensureEntitiesRegistered(): void {
// No-op — entities are now registered at import time
}

View file

@ -1,43 +0,0 @@
/**
* Entity Descriptor System Types
*
* Each module declares an EntityDescriptor that describes how its items
* can be displayed, dragged, dropped, and created from other modules.
*/
import type { DragType } from '@manacore/shared-ui/dnd';
export interface EntityDisplayData {
title: string;
subtitle?: string;
}
export interface EntityDescriptor {
appId: string;
collection: string;
/** Param key name used in DetailView (e.g. 'taskId', 'eventId', 'contactId'). */
paramKey: string;
/** How to display an item in link previews and drag ghosts. */
getDisplayData: (item: Record<string, unknown>) => EntityDisplayData;
/** DragType used when dragging items from this module. */
dragType: DragType;
/** Which DragTypes this module accepts as drop targets. */
acceptsDropFrom?: DragType[];
/** Transform incoming items from other modules into this module's create shape. */
transformIncoming?: Partial<
Record<DragType, (sourceItem: Record<string, unknown>) => Record<string, unknown>>
>;
/** Create a new item in this module. Returns the new item's ID. */
createItem?: (data: Record<string, unknown>) => Promise<string>;
}
export interface DropResult {
newItemId: string;
linkPairId: string;
}

View file

@ -9,7 +9,7 @@
import type { LocalEvent } from './types';
import { eventsStore } from './stores/events.svelte';
import { Plus } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import { dropTarget, dragSource } from '@manacore/shared-ui/dnd';
import type { TagDragData } from '@manacore/shared-ui/dnd';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';

View file

@ -1,70 +0,0 @@
import { registerEntity } from '$lib/entities/registry';
import type { EntityDescriptor } from '$lib/entities/types';
const calendarEntity: EntityDescriptor = {
appId: 'calendar',
collection: 'events',
paramKey: 'eventId',
getDisplayData: (item) => ({
title: (item.title as string) || 'Termin',
subtitle: item.startDate
? new Date(item.startDate as string).toLocaleDateString('de', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
: undefined,
}),
dragType: 'event',
acceptsDropFrom: ['task', 'contact'],
transformIncoming: {
task: (source) => {
const dueDate = (source.dueDate as string) || new Date().toISOString();
const start = new Date(dueDate);
const end = new Date(start.getTime() + 60 * 60 * 1000);
return {
title: source.title as string,
startTime: start.toISOString(),
endTime: end.toISOString(),
description: source.description as string | undefined,
};
},
contact: (source) => {
const name = [source.firstName, source.lastName].filter(Boolean).join(' ');
const now = new Date();
const end = new Date(now.getTime() + 60 * 60 * 1000);
return {
title: `Treffen mit ${name}`,
startTime: now.toISOString(),
endTime: end.toISOString(),
};
},
},
createItem: async (data) => {
// Lazy imports to avoid circular dependency at registration time
const { db } = await import('$lib/data/database');
const { eventsStore } = await import('./stores/events.svelte');
const calendars = await db.table('calendars').toArray();
const defaultCal = calendars.find((c: Record<string, unknown>) => !c.deletedAt);
const calendarId = (defaultCal?.id as string) ?? 'default';
const result = await eventsStore.createEvent({
calendarId,
title: data.title as string,
startTime: data.startTime as string,
endTime: data.endTime as string,
description: (data.description as string) ?? undefined,
});
if (!result.success || !result.data) throw new Error(result.error || 'Failed to create event');
return result.data.id;
},
};
registerEntity(calendarEntity);

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { eventsStore } from '../stores/events.svelte';
import { Trash, MapPin, Clock, X } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalEvent } from '../types';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
import LinkedItems from '$lib/components/links/LinkedItems.svelte';

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalDeck, LocalCard } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { deckStore } from '../stores/decks.svelte';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalDeck, LocalCard } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import type { LocalLocation, LocalFavorite } from './types';
import { CATEGORY_COLORS } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { favoritesStore } from '../stores/favorites.svelte';
import { Star, Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalLocation, LocalFavorite } from '../types';
import { CATEGORY_COLORS } from '../types';

View file

@ -9,7 +9,7 @@
import type { LocalContact } from './types';
import { contactsStore } from './stores/contacts.svelte';
import { Plus, Star } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import { dropTarget, dragSource } from '@manacore/shared-ui/dnd';
import type { TagDragData } from '@manacore/shared-ui/dnd';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';

View file

@ -1,21 +0,0 @@
import { registerEntity } from '$lib/entities/registry';
import type { EntityDescriptor } from '$lib/entities/types';
const contactsEntity: EntityDescriptor = {
appId: 'contacts',
collection: 'contacts',
paramKey: 'contactId',
getDisplayData: (item) => {
const name = [item.firstName, item.lastName].filter(Boolean).join(' ');
return {
title: name || (item.email as string) || 'Kontakt',
subtitle: (item.company as string) ?? undefined,
};
},
dragType: 'contact',
// Contacts are drag sources only — dropping onto contacts doesn't create a new contact
};
registerEntity(contactsEntity);

View file

@ -16,7 +16,7 @@
Globe,
X,
} from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalContact } from '../types';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
import LinkedItems from '$lib/components/links/LinkedItems.svelte';

View file

@ -1,28 +0,0 @@
import { registerEntity } from '$lib/entities/registry';
import { financeStore } from './stores/finance.svelte';
import type { EntityDescriptor } from '$lib/entities/types';
const financeEntity: EntityDescriptor = {
appId: 'finance',
collection: 'transactions',
paramKey: 'transactionId',
getDisplayData: (item) => ({
title: (item.description as string) || 'Transaktion',
subtitle: item.amount ? `${item.type === 'income' ? '+' : '-'}${item.amount}` : undefined,
}),
dragType: 'transaction',
acceptsDropFrom: [],
createItem: async (data) => {
const tx = await financeStore.addTransaction({
type: 'expense',
amount: (data.amount as number) ?? 0,
description: (data.title as string) ?? (data.description as string) ?? '',
});
return tx.id;
},
};
registerEntity(financeEntity);

View file

@ -13,7 +13,7 @@
} from './queries';
import { habitsStore } from './stores/habits.svelte';
import type { Habit, HabitLog } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -1,36 +0,0 @@
import { registerEntity } from '$lib/entities/registry';
import { habitsStore } from './stores/habits.svelte';
import type { EntityDescriptor } from '$lib/entities/types';
const habitsEntity: EntityDescriptor = {
appId: 'habits',
collection: 'habits',
paramKey: 'habitId',
getDisplayData: (item) => ({
title: `${item.emoji as string} ${item.title as string}`,
subtitle: undefined,
}),
dragType: 'habit',
acceptsDropFrom: ['task'],
transformIncoming: {
task: (source) => ({
title: source.title as string,
emoji: '\ud83d\udcaa',
color: '#6366f1',
}),
},
createItem: async (data) => {
const habit = await habitsStore.createHabit({
title: data.title as string,
emoji: (data.emoji as string) ?? '\u2b50',
color: (data.color as string) ?? '#6366f1',
});
return habit.id;
},
};
registerEntity(habitsEntity);

View file

@ -5,7 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalCollection, LocalItem } from './types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { collectionsStore } from '../stores/collections.svelte';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalCollection, LocalItem } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -5,7 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalMemo } from './types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { memosStore } from '../stores/memos.svelte';
import { Trash, PushPin } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalMemo, ProcessingStatus } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalSong, LocalPlaylist } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { libraryStore } from '../stores/library.svelte';
import { Heart, Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalSong } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -1,38 +0,0 @@
import { registerEntity } from '$lib/entities/registry';
import { notesStore } from './stores/notes.svelte';
import type { EntityDescriptor } from '$lib/entities/types';
const notesEntity: EntityDescriptor = {
appId: 'notes',
collection: 'notes',
paramKey: 'noteId',
getDisplayData: (item) => ({
title: (item.title as string) || 'Notiz',
subtitle: undefined,
}),
dragType: 'note',
acceptsDropFrom: ['task', 'contact'],
transformIncoming: {
task: (source) => ({
title: source.title as string,
content: (source.description as string) ?? '',
}),
contact: (source) => ({
title: `${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
content: `Kontakt: ${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
}),
},
createItem: async (data) => {
const note = await notesStore.createNote({
title: data.title as string,
content: (data.content as string) ?? '',
});
return note.id;
},
};
registerEntity(notesEntity);

View file

@ -5,7 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalPlant, LocalWateringSchedule } from './types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalPlant, HealthStatus, LightLevel } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalDeck, LocalSlide } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { decksStore } from '../stores/decks.svelte';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalDeck, LocalSlide } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalQuestion, LocalCollection } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalQuestion, QuestionStatus, QuestionPriority, ResearchDepth } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -5,7 +5,7 @@
<script lang="ts">
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalSkill, LocalActivity } from './types';
import { LEVEL_NAMES, BRANCH_INFO, xpProgress, type SkillBranch } from './types';

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { skillStore } from '../stores/skills.svelte';
import { Trash, Lightning } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalSkill, SkillBranch } from '../types';
import { BRANCH_INFO, LEVEL_NAMES, xpProgress, xpForNextLevel } from '../types';

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalFile, LocalFolder } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { filesStore } from '../stores/files.svelte';
import { Heart, Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalFile } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -8,7 +8,7 @@
import { timerStore } from '$lib/modules/times/stores/timer.svelte';
import { formatDuration } from '$lib/modules/times/queries';
import { Play, Stop } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalTimeEntry, LocalProject } from './types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalTimeEntry, LocalProject, LocalClient } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -14,7 +14,7 @@
} from './queries';
import { tasksStore } from './stores/tasks.svelte';
import { Circle, Check } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import { dropTarget, dragSource } from '@manacore/shared-ui/dnd';
import type { TagDragData } from '@manacore/shared-ui/dnd';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';

View file

@ -1,38 +0,0 @@
import { registerEntity } from '$lib/entities/registry';
import type { EntityDescriptor } from '$lib/entities/types';
const todoEntity: EntityDescriptor = {
appId: 'todo',
collection: 'tasks',
paramKey: 'taskId',
getDisplayData: (item) => ({
title: (item.title as string) || 'Aufgabe',
subtitle: item.dueDate ? new Date(item.dueDate as string).toLocaleDateString('de') : undefined,
}),
dragType: 'task',
acceptsDropFrom: ['event', 'contact'],
transformIncoming: {
event: (source) => ({
title: source.title as string,
dueDate: source.startDate as string,
description: source.description as string | undefined,
}),
contact: (source) => ({
title: `Kontaktieren: ${[source.firstName, source.lastName].filter(Boolean).join(' ')}`,
}),
},
createItem: async (data) => {
// Lazy import to avoid circular dependency at registration time
const { tasksStore } = await import('./stores/tasks.svelte');
const task = await tasksStore.createTask(
data as { title: string; dueDate?: string; description?: string }
);
return task.id;
},
};
registerEntity(todoEntity);

View file

@ -7,7 +7,7 @@
import { db } from '$lib/data/database';
import { tasksStore } from '../stores/tasks.svelte';
import { Check, Trash, X } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalTask, TaskPriority } from '../types';
import { useAllTags, getTagsByIds } from '$lib/stores/tags.svelte';
import LinkedItems from '$lib/components/links/LinkedItems.svelte';

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import type { LocalLink, LocalFolder } from './types';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -6,7 +6,7 @@
import { liveQuery } from 'dexie';
import { db } from '$lib/data/database';
import { Trash } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalLink } from '../types';
let { navigate, goBack, params }: ViewProps = $props();

View file

@ -13,7 +13,7 @@
import { dropTarget } from '@manacore/shared-ui/dnd';
import type { TagDragData } from '@manacore/shared-ui/dnd';
import { getTagsByIds } from '$lib/stores/tags.svelte';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalFavorite } from './types';
import type { Quote } from '@zitare/content';

View file

@ -9,7 +9,7 @@
import { favoritesStore } from '$lib/modules/zitare/stores/favorites.svelte';
import { isFavorite as checkIsFavorite, type Favorite } from '$lib/modules/zitare/queries';
import { Heart, ShareNetwork, Info } from '@manacore/shared-icons';
import type { ViewProps } from '$lib/components/workbench/nav-stack';
import type { ViewProps } from '$lib/app-registry';
import type { LocalFavorite } from '../types';
import { QUOTES, type Quote, type Category } from '@zitare/content';

View file

@ -1,72 +1,50 @@
/**
* Split-Screen App Registry
*
* Lazy-import registry for all app modules.
* Each app has an ListView.svelte component that renders in split-screen.
* Delegates to the unified app registry for component loading and labels.
*/
const APP_COMPONENTS = {
todo: () => import('$lib/modules/todo/ListView.svelte'),
calendar: () => import('$lib/modules/calendar/ListView.svelte'),
contacts: () => import('$lib/modules/contacts/ListView.svelte'),
chat: () => import('$lib/modules/chat/ListView.svelte'),
picture: () => import('$lib/modules/picture/ListView.svelte'),
cards: () => import('$lib/modules/cards/ListView.svelte'),
zitare: () => import('$lib/modules/zitare/ListView.svelte'),
mukke: () => import('$lib/modules/mukke/ListView.svelte'),
storage: () => import('$lib/modules/storage/ListView.svelte'),
presi: () => import('$lib/modules/presi/ListView.svelte'),
inventar: () => import('$lib/modules/inventar/ListView.svelte'),
photos: () => import('$lib/modules/photos/ListView.svelte'),
skilltree: () => import('$lib/modules/skilltree/ListView.svelte'),
citycorners: () => import('$lib/modules/citycorners/ListView.svelte'),
times: () => import('$lib/modules/times/ListView.svelte'),
context: () => import('$lib/modules/context/ListView.svelte'),
questions: () => import('$lib/modules/questions/ListView.svelte'),
nutriphi: () => import('$lib/modules/nutriphi/ListView.svelte'),
planta: () => import('$lib/modules/planta/ListView.svelte'),
uload: () => import('$lib/modules/uload/ListView.svelte'),
calc: () => import('$lib/modules/calc/ListView.svelte'),
moodlit: () => import('$lib/modules/moodlit/ListView.svelte'),
memoro: () => import('$lib/modules/memoro/ListView.svelte'),
playground: () => import('$lib/modules/playground/ListView.svelte'),
};
import { getApp, getAllApps } from '$lib/app-registry';
export type SplitAppId = keyof typeof APP_COMPONENTS;
const SPLIT_APP_ID_LIST = [
'todo',
'calendar',
'contacts',
'chat',
'picture',
'cards',
'zitare',
'mukke',
'storage',
'presi',
'inventar',
'photos',
'skilltree',
'citycorners',
'times',
'context',
'questions',
'nutriphi',
'planta',
'uload',
'calc',
'moodlit',
'memoro',
'playground',
] as const;
export const SPLIT_APP_IDS = Object.keys(APP_COMPONENTS) as SplitAppId[];
export type SplitAppId = (typeof SPLIT_APP_ID_LIST)[number];
/** Display names for each app (German UI). */
export const SPLIT_APP_LABELS: Record<SplitAppId, string> = {
todo: 'Todo',
calendar: 'Kalender',
contacts: 'Kontakte',
chat: 'Chat',
picture: 'Picture',
cards: 'Cards',
zitare: 'Zitare',
mukke: 'Mukke',
storage: 'Storage',
presi: 'Presi',
inventar: 'Inventar',
photos: 'Fotos',
skilltree: 'SkillTree',
citycorners: 'CityCorners',
times: 'Times & Clock',
context: 'Context',
questions: 'Questions',
nutriphi: 'NutriPhi',
planta: 'Planta',
uload: 'uLoad',
calc: 'Calc',
moodlit: 'Moodlit',
memoro: 'Memoro',
playground: 'Playground',
};
export const SPLIT_APP_IDS = SPLIT_APP_ID_LIST as readonly SplitAppId[];
/** Display names for each app (from unified registry). */
export const SPLIT_APP_LABELS: Record<SplitAppId, string> = Object.fromEntries(
SPLIT_APP_ID_LIST.map((id) => [id, getApp(id)?.name ?? id])
) as Record<SplitAppId, string>;
export async function loadAppComponent(appId: string) {
const loader = APP_COMPONENTS[appId as SplitAppId];
if (!loader) return null;
const module = await loader();
const app = getApp(appId);
if (!app) return null;
const module = await app.views.list.load();
return module.default;
}