diff --git a/apps/manacore/apps/web/src/lib/modules/calc/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/calc/AppView.svelte new file mode 100644 index 000000000..834e3b1fb --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/AppView.svelte @@ -0,0 +1,95 @@ + + + +
+ +
+

{expression || ' '}

+

{result || '0'}

+
+ + + + + +
+ {#each ['7', '8', '9', '/', '4', '5', '6', '*', '1', '2', '3', '-', '0', '.', '=', '+'] as key} + + {/each} +
+ + + {#if recent.length > 0} +
+

Verlauf

+ {#each recent as calc (calc.id)} +
+ {calc.expression} + = {calc.result} +
+ {/each} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/modules/calendar/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/calendar/AppView.svelte new file mode 100644 index 000000000..8c7f42993 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calendar/AppView.svelte @@ -0,0 +1,92 @@ + + + +
+ +
+ {#each weekDays() as day, i} + {@const isToday = day.toISOString().split('T')[0] === todayStr} +
+ {dayNames[i]} + + {day.getDate()} + +
+ {/each} +
+ + +
+

Heute

+ {#each todayEvents as event (event.id)} +
+

{event.title}

+

+ {#if event.allDay} + Ganztägig + {:else} + {formatTime(event.startDate)} — {formatTime(event.endDate)} + {/if} +

+ {#if event.location} +

{event.location}

+ {/if} +
+ {/each} + + {#if todayEvents.length === 0} +

Keine Termine heute

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/cards/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/cards/AppView.svelte new file mode 100644 index 000000000..e07ec4dd7 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/cards/AppView.svelte @@ -0,0 +1,73 @@ + + + +
+
+

{decks.length} Decks

+

{dueForReview()} fällig

+
+ +
+ {#each decks as deck (deck.id)} +
+
+
+

{deck.name}

+ {cardsInDeck(deck.id)} +
+ {#if deck.description} +

{deck.description}

+ {/if} +
+ {/each} + + {#if decks.length === 0} +

Keine Decks

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/chat/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/chat/AppView.svelte new file mode 100644 index 000000000..8b36b28ef --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/chat/AppView.svelte @@ -0,0 +1,77 @@ + + + +
+

{conversations.length} Unterhaltungen

+ +
+ {#each sorted as conv (conv.id)} + {@const lastMsg = lastMessages.get(conv.id)} +
+
+

+ {conv.title || 'Neue Unterhaltung'} +

+ {#if conv.isPinned} + 📌 + {/if} +
+ {#if lastMsg} +

+ {lastMsg.sender === 'user' ? 'Du: ' : ''}{truncate(lastMsg.messageText)} +

+ {/if} +
+ {/each} + + {#if sorted.length === 0} +

Keine Unterhaltungen

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/citycorners/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/citycorners/AppView.svelte new file mode 100644 index 000000000..39c741d99 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/citycorners/AppView.svelte @@ -0,0 +1,86 @@ + + + +
+
+ {locations.length} Orte + {favorites.length} Favoriten +
+ +
+ {#each locations as location (location.id)} +
+
+
+
+

{location.name}

+ {#if favoriteIds.has(location.id)} + + {/if} +
+

+ {categoryLabels[location.category] ?? location.category} +

+
+
+ {/each} + + {#if locations.length === 0} +

Keine Orte

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/clock/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/clock/AppView.svelte new file mode 100644 index 000000000..b36c2397d --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/clock/AppView.svelte @@ -0,0 +1,126 @@ + + + +
+ +
+

+ {now.toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit', second: '2-digit' })} +

+

+ {now.toLocaleDateString('de', { weekday: 'long', day: 'numeric', month: 'long' })} +

+
+ + + {#if worldClocks.length > 0} +
+

Weltuhr

+ {#each worldClocks as wc (wc.id)} +
+ {wc.cityName} + {timeInZone(wc.timezone)} +
+ {/each} +
+ {/if} + + + {#if activeTimers.length > 0} +
+

Timer

+ {#each activeTimers as timer (timer.id)} +
+ {timer.label ?? 'Timer'} + + {formatDuration(timer.remainingSeconds ?? timer.durationSeconds)} + +
+ {/each} +
+ {/if} + + + {#if enabledAlarms.length > 0} +
+

Wecker ({enabledAlarms.length})

+ {#each enabledAlarms.slice(0, 3) as alarm (alarm.id)} +
+ {alarm.label ?? 'Wecker'} + {alarm.time} +
+ {/each} +
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/modules/contacts/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/contacts/AppView.svelte new file mode 100644 index 000000000..ef78c42a2 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/contacts/AppView.svelte @@ -0,0 +1,82 @@ + + + +
+ + +

{filtered().length} Kontakte

+ +
+ {#each filtered() as contact (contact.id)} +
+
+ {initials(contact)} +
+
+

{displayName(contact)}

+ {#if contact.company} +

{contact.company}

+ {/if} +
+ {#if contact.isFavorite} + + {/if} +
+ {/each} + + {#if filtered().length === 0} +

Keine Kontakte gefunden

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/context/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/context/AppView.svelte new file mode 100644 index 000000000..3539b4341 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/context/AppView.svelte @@ -0,0 +1,88 @@ + + + +
+
+ {spaces.length} Spaces + {documents.length} Dokumente +
+ +
+ + {#if spaces.filter((s) => s.pinned).length > 0} +

Angepinnte Spaces

+ {#each spaces.filter((s) => s.pinned) as space (space.id)} +
+

{space.name}

+ {#if space.description} +

{space.description}

+ {/if} +
+ {/each} + {/if} + + +

Zuletzt bearbeitet

+ {#each recentDocs as doc (doc.id)} +
+ {@html typeIcons[doc.type] ?? '📄'} +
+

{doc.title || 'Unbenannt'}

+
+ {#if doc.pinned} + 📌 + {/if} +
+ {/each} + + {#if recentDocs.length === 0} +

Keine Dokumente

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/inventar/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/inventar/AppView.svelte new file mode 100644 index 000000000..39bf5321e --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/inventar/AppView.svelte @@ -0,0 +1,77 @@ + + + +
+
+ {items.length} Gegenstände + {#if totalValue() > 0} + ~{totalValue().toFixed(0)} EUR + {/if} +
+ +
+ {#each collections as collection (collection.id)} +
+
+ {#if collection.icon} + {collection.icon} + {/if} +

{collection.name}

+ {itemsInCollection(collection.id)} +
+ {#if collection.description} +

{collection.description}

+ {/if} +
+ {/each} + + {#if collections.length === 0} +

Keine Sammlungen

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/memoro/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/memoro/AppView.svelte new file mode 100644 index 000000000..a8a30d091 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/memoro/AppView.svelte @@ -0,0 +1,88 @@ + + + +
+
+ {memos.length} Memos + {pinned.length} angepinnt +
+ +
+ {#each sorted as memo (memo.id)} +
+
+
+
+ {#if memo.isPinned} + 📌 + {/if} +

+ {memo.title || 'Unbenanntes Memo'} +

+
+ {#if memo.intro} +

{memo.intro}

+ {/if} +
+ + {memo.processingStatus === 'completed' + ? formatDuration(memo.audioDurationMs) + : memo.processingStatus} + +
+
+ {/each} + + {#if sorted.length === 0} +

Keine Memos

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/moodlit/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/moodlit/AppView.svelte new file mode 100644 index 000000000..0ba9a6003 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/moodlit/AppView.svelte @@ -0,0 +1,68 @@ + + + +
+ + {#if activeMood} +
+

{activeMood.name}

+
+ {:else} +
+

Kein Mood aktiv

+
+ {/if} + + +
+
+ {#each moods as mood (mood.id)} + + {/each} +
+ + {#if moods.length === 0} +

Keine Moods

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/mukke/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/mukke/AppView.svelte new file mode 100644 index 000000000..fd64b790f --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/mukke/AppView.svelte @@ -0,0 +1,84 @@ + + + +
+
+ {songs.length} Songs + {playlists.length} Playlists + {favorites.length} Favoriten +
+ +
+

Zuletzt gehört

+ {#each recentlyPlayed as song (song.id)} +
+
+ ♫ +
+
+

{song.title}

+

{song.artist ?? 'Unbekannt'}

+
+ {formatDuration(song.duration)} +
+ {/each} + + {#if recentlyPlayed.length === 0} +

Noch nichts gehört

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/nutriphi/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/nutriphi/AppView.svelte new file mode 100644 index 000000000..7c9ab6d81 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/nutriphi/AppView.svelte @@ -0,0 +1,109 @@ + + + +
+ +
+

{Math.round(totalCalories)}

+

+ {#if goal} + von {goal.dailyCalories} kcal + {:else} + kcal heute + {/if} +

+ {#if goal} +
+
+
+ {/if} +
+ + +
+ {Math.round(totalProtein)}g Protein + {todayMeals.length} Mahlzeiten +
+ + +
+ {#each todayMeals as meal (meal.id)} +
+
+ {mealTypeLabels[meal.mealType] ?? meal.mealType} + {#if meal.nutrition} + {Math.round(meal.nutrition.calories)} kcal + {/if} +
+

{meal.description}

+
+ {/each} + + {#if todayMeals.length === 0} +

Noch keine Mahlzeiten heute

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/photos/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/photos/AppView.svelte new file mode 100644 index 000000000..f469dee84 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/photos/AppView.svelte @@ -0,0 +1,64 @@ + + + +
+
+ {albums.length} Alben + {favorites.length} Favoriten +
+ +
+

Alben

+ {#each albums as album (album.id)} +
+

{album.name}

+ {#if album.description} +

{album.description}

+ {/if} +

+ {album.isAutoGenerated ? 'Auto-generiert' : 'Manuell'} +

+
+ {/each} + + {#if albums.length === 0} +

Keine Alben

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/picture/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/picture/AppView.svelte new file mode 100644 index 000000000..050429e28 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/picture/AppView.svelte @@ -0,0 +1,62 @@ + + + +
+
+

{images.length} Bilder

+

{favoriteCount} Favoriten

+
+ +
+
+ {#each sorted as image (image.id)} +
+ {#if image.publicUrl} + {image.prompt} + {:else} +
+ {image.format ?? 'img'} +
+ {/if} + {#if image.isFavorite} + + {/if} +
+ {/each} +
+ + {#if sorted.length === 0} +

Keine Bilder

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/planta/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/planta/AppView.svelte new file mode 100644 index 000000000..b1db32674 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/planta/AppView.svelte @@ -0,0 +1,102 @@ + + + +
+
+ {plants.length} Pflanzen + {#if dueForWatering.length > 0} + {dueForWatering.length} giessen + {/if} + {#if needsAttention.length > 0} + {needsAttention.length} brauchen Pflege + {/if} +
+ +
+ {#each plants as plant (plant.id)} + {@const schedule = getSchedule(plant.id)} + {@const waterDue = needsWater(schedule)} +
+
+ {@html healthIcons[plant.healthStatus ?? 'healthy'] ?? '🌱'} +
+

{plant.name}

+ {#if plant.scientificName} +

{plant.scientificName}

+ {/if} +
+ {#if waterDue} + 💧 + {/if} +
+ {#if schedule} +

+ Alle {schedule.frequencyDays} Tage giessen +

+ {/if} +
+ {/each} + + {#if plants.length === 0} +

Keine Pflanzen

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/playground/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/playground/AppView.svelte new file mode 100644 index 000000000..d432ef7c0 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/playground/AppView.svelte @@ -0,0 +1,82 @@ + + + +
+ + + + +
+ {#each messages as msg, i} +
+

{msg.role === 'user' ? 'Du' : modelLabel}

+

{msg.content}

+
+ {/each} + + {#if messages.length === 0} +
+

Schreib einen Prompt...

+
+ {/if} +
+ + +
{ + e.preventDefault(); + send(); + }} + class="flex gap-2" + > + + +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/presi/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/presi/AppView.svelte new file mode 100644 index 000000000..2e1a30dae --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/presi/AppView.svelte @@ -0,0 +1,67 @@ + + + +
+

{decks.length} Präsentationen

+ +
+ {#each decks as deck (deck.id)} +
+

{deck.title}

+
+ {slideCount(deck.id)} Folien + {#if deck.isPublic} + Öffentlich + {/if} +
+ {#if deck.description} +

{deck.description}

+ {/if} +
+ {/each} + + {#if decks.length === 0} +

Keine Präsentationen

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/questions/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/questions/AppView.svelte new file mode 100644 index 000000000..ad87ff28f --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/questions/AppView.svelte @@ -0,0 +1,94 @@ + + + +
+
+ {questions.length} Fragen + {collections.length} Sammlungen +
+ +
+ {#each sorted as question (question.id)} +
+
+

{question.title}

+ + {statusLabels[question.status] ?? question.status} + +
+ {#if question.description} +

{question.description}

+ {/if} + {#if question.tags.length > 0} +
+ {#each question.tags.slice(0, 3) as tag} + {tag} + {/each} +
+ {/if} +
+ {/each} + + {#if sorted.length === 0} +

Keine offenen Fragen

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/skilltree/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/skilltree/AppView.svelte new file mode 100644 index 000000000..282cb9326 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/skilltree/AppView.svelte @@ -0,0 +1,81 @@ + + + +
+ +
+ {totalXp} XP + Level {highestLevel} + {skills.length} Skills +
+ + +
+ {#each skills as skill (skill.id)} + {@const branch = BRANCH_INFO[skill.branch as SkillBranch]} + {@const progress = xpProgress(skill.currentXp, skill.level)} +
+
+
+ {skill.icon} +

{skill.name}

+
+ Lv. {skill.level} +
+
+
+
+
+ {skill.currentXp} XP +
+

+ {branch?.name ?? skill.branch} — {LEVEL_NAMES[skill.level] ?? 'Unbekannt'} +

+
+ {/each} + + {#if skills.length === 0} +

Keine Skills angelegt

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/storage/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/storage/AppView.svelte new file mode 100644 index 000000000..3ebce74ce --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/storage/AppView.svelte @@ -0,0 +1,92 @@ + + + +
+
+ {folders.length} Ordner + {files.length} Dateien +
+ +
+ + {#if folders.filter((f) => !f.parentFolderId).length > 0} +

Ordner

+ {#each folders.filter((f) => !f.parentFolderId) as folder (folder.id)} +
+ 📁 + {folder.name} +
+ {/each} + {/if} + + +

Zuletzt

+ {#each recentFiles as file (file.id)} +
+ {@html fileIcon(file.mimeType)} + {file.name} + {formatSize(file.size)} +
+ {/each} + + {#if recentFiles.length === 0} +

Keine Dateien

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/times/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/times/AppView.svelte new file mode 100644 index 000000000..0be427377 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/times/AppView.svelte @@ -0,0 +1,95 @@ + + + +
+ + {#if running} +
+
+
+

{running.description || 'Timer läuft'}

+
+

{projectName(running.projectId)}

+
+ {/if} + + +
+ Heute: {todayEntries.length} Einträge + {formatDuration(totalToday)} +
+ + +
+ {#each todayEntries as entry (entry.id)} +
+
+

{entry.description || 'Ohne Beschreibung'}

+ {formatDuration(entry.duration)} +
+

{projectName(entry.projectId)}

+
+ {/each} + + {#if todayEntries.length === 0 && !running} +

Noch keine Zeiteinträge heute

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/todo/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/todo/AppView.svelte new file mode 100644 index 000000000..cfba65f35 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/todo/AppView.svelte @@ -0,0 +1,127 @@ + + + +
+ +
+ {stats.total} gesamt + {stats.today} heute + 0}>{stats.overdue} überfällig +
+ + +
+ {#each ['inbox', 'today', 'overdue'] as f} + + {/each} +
+ + +
{ + e.preventDefault(); + addTask(); + }} + class="flex gap-2" + > + + +
+ + +
+ {#each filtered() as task (task.id)} + + {/each} + + {#if filtered().length === 0} +

Keine Aufgaben

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte new file mode 100644 index 000000000..7d2663780 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/uload/AppView.svelte @@ -0,0 +1,79 @@ + + + +
+
+ {links.length} Links + {totalClicks} Klicks + {folders.length} Ordner +
+ +
+ {#each sorted as link (link.id)} +
+
+

+ {link.title || link.shortCode} +

+ {link.clickCount} +
+

{hostname(link.originalUrl)}

+ {#if link.customCode} +

/{link.customCode}

+ {/if} +
+ {/each} + + {#if sorted.length === 0} +

Keine Links

+ {/if} +
+
diff --git a/apps/manacore/apps/web/src/lib/modules/zitare/AppView.svelte b/apps/manacore/apps/web/src/lib/modules/zitare/AppView.svelte new file mode 100644 index 000000000..393f109de --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/zitare/AppView.svelte @@ -0,0 +1,58 @@ + + + +
+
+
+ «{todayQuote.text}» +
+

— {todayQuote.author}

+
+ +
+ {favorites.length} gespeicherte Zitate +
+
diff --git a/apps/manacore/apps/web/src/lib/splitscreen/PanelHeader.svelte b/apps/manacore/apps/web/src/lib/splitscreen/PanelHeader.svelte new file mode 100644 index 000000000..030610999 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/splitscreen/PanelHeader.svelte @@ -0,0 +1,65 @@ + + + +
+ {label} + +
+ {#if onSwap} + + {/if} + + +
+
diff --git a/apps/manacore/apps/web/src/lib/splitscreen/ResizeHandle.svelte b/apps/manacore/apps/web/src/lib/splitscreen/ResizeHandle.svelte new file mode 100644 index 000000000..ce52f48e5 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/splitscreen/ResizeHandle.svelte @@ -0,0 +1,69 @@ + + + + + + +{#if isDragging} + +
+{/if} diff --git a/apps/manacore/apps/web/src/lib/splitscreen/SplitPaneLayout.svelte b/apps/manacore/apps/web/src/lib/splitscreen/SplitPaneLayout.svelte new file mode 100644 index 000000000..34460ea3a --- /dev/null +++ b/apps/manacore/apps/web/src/lib/splitscreen/SplitPaneLayout.svelte @@ -0,0 +1,49 @@ + + + +
+ +
+ {@render children()} +
+ + {#if splitStore.isActive && splitStore.SplitComponent} + splitStore.setDividerPosition(pos)} /> + + +
+ splitStore.closeSplit()} /> +
+ +
+
+ {/if} + + {#if splitStore.isLoading} +
+
+
+ {/if} +
diff --git a/apps/manacore/apps/web/src/lib/splitscreen/index.ts b/apps/manacore/apps/web/src/lib/splitscreen/index.ts new file mode 100644 index 000000000..f8a6d214d --- /dev/null +++ b/apps/manacore/apps/web/src/lib/splitscreen/index.ts @@ -0,0 +1,10 @@ +/** + * Split-Screen — barrel exports. + */ + +export { splitStore } from './store.svelte'; +export { loadAppComponent, SPLIT_APP_IDS, SPLIT_APP_LABELS } from './registry'; +export type { SplitAppId } from './registry'; +export { default as SplitPaneLayout } from './SplitPaneLayout.svelte'; +export { default as ResizeHandle } from './ResizeHandle.svelte'; +export { default as PanelHeader } from './PanelHeader.svelte'; diff --git a/apps/manacore/apps/web/src/lib/splitscreen/registry.ts b/apps/manacore/apps/web/src/lib/splitscreen/registry.ts new file mode 100644 index 000000000..069815430 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/splitscreen/registry.ts @@ -0,0 +1,74 @@ +/** + * Split-Screen App Registry + * + * Lazy-import registry for all app modules. + * Each app has an AppView.svelte component that renders in split-screen. + */ + +const APP_COMPONENTS = { + todo: () => import('$lib/modules/todo/AppView.svelte'), + calendar: () => import('$lib/modules/calendar/AppView.svelte'), + contacts: () => import('$lib/modules/contacts/AppView.svelte'), + chat: () => import('$lib/modules/chat/AppView.svelte'), + picture: () => import('$lib/modules/picture/AppView.svelte'), + cards: () => import('$lib/modules/cards/AppView.svelte'), + zitare: () => import('$lib/modules/zitare/AppView.svelte'), + clock: () => import('$lib/modules/clock/AppView.svelte'), + mukke: () => import('$lib/modules/mukke/AppView.svelte'), + storage: () => import('$lib/modules/storage/AppView.svelte'), + presi: () => import('$lib/modules/presi/AppView.svelte'), + inventar: () => import('$lib/modules/inventar/AppView.svelte'), + photos: () => import('$lib/modules/photos/AppView.svelte'), + skilltree: () => import('$lib/modules/skilltree/AppView.svelte'), + citycorners: () => import('$lib/modules/citycorners/AppView.svelte'), + times: () => import('$lib/modules/times/AppView.svelte'), + context: () => import('$lib/modules/context/AppView.svelte'), + questions: () => import('$lib/modules/questions/AppView.svelte'), + nutriphi: () => import('$lib/modules/nutriphi/AppView.svelte'), + planta: () => import('$lib/modules/planta/AppView.svelte'), + uload: () => import('$lib/modules/uload/AppView.svelte'), + calc: () => import('$lib/modules/calc/AppView.svelte'), + moodlit: () => import('$lib/modules/moodlit/AppView.svelte'), + memoro: () => import('$lib/modules/memoro/AppView.svelte'), + playground: () => import('$lib/modules/playground/AppView.svelte'), +}; + +export type SplitAppId = keyof typeof APP_COMPONENTS; + +export const SPLIT_APP_IDS = Object.keys(APP_COMPONENTS) as SplitAppId[]; + +/** Display names for each app (German UI). */ +export const SPLIT_APP_LABELS: Record = { + todo: 'Todo', + calendar: 'Kalender', + contacts: 'Kontakte', + chat: 'Chat', + picture: 'Picture', + cards: 'Cards', + zitare: 'Zitare', + clock: 'Uhr', + mukke: 'Mukke', + storage: 'Storage', + presi: 'Presi', + inventar: 'Inventar', + photos: 'Fotos', + skilltree: 'SkillTree', + citycorners: 'CityCorners', + times: 'Times', + context: 'Context', + questions: 'Questions', + nutriphi: 'NutriPhi', + planta: 'Planta', + uload: 'uLoad', + calc: 'Calc', + moodlit: 'Moodlit', + memoro: 'Memoro', + playground: 'Playground', +}; + +export async function loadAppComponent(appId: string) { + const loader = APP_COMPONENTS[appId as SplitAppId]; + if (!loader) return null; + const module = await loader(); + return module.default; +} diff --git a/apps/manacore/apps/web/src/lib/splitscreen/store.svelte.ts b/apps/manacore/apps/web/src/lib/splitscreen/store.svelte.ts new file mode 100644 index 000000000..a9ccb4db2 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/splitscreen/store.svelte.ts @@ -0,0 +1,133 @@ +/** + * Split-Screen Store — Svelte 5 runes store + * + * Manages split-screen panel state: which app is shown, divider position, + * component loading, and localStorage persistence. + */ + +import type { Component } from 'svelte'; +import { loadAppComponent, type SplitAppId } from './registry'; + +const STORAGE_KEY = 'manacore:split-screen'; +const MIN_POSITION = 20; +const MAX_POSITION = 80; +const DEFAULT_POSITION = 50; +const MIN_SCREEN_WIDTH = 1024; + +interface PersistedState { + splitApp: SplitAppId | null; + dividerPosition: number; +} + +function loadPersisted(): PersistedState { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { splitApp: null, dividerPosition: DEFAULT_POSITION }; + const parsed = JSON.parse(raw) as PersistedState; + return { + splitApp: parsed.splitApp ?? null, + dividerPosition: clamp(parsed.dividerPosition ?? DEFAULT_POSITION), + }; + } catch { + return { splitApp: null, dividerPosition: DEFAULT_POSITION }; + } +} + +function savePersisted(state: PersistedState) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch { + // Storage full or unavailable — ignore + } +} + +function clamp(pos: number): number { + return Math.min(MAX_POSITION, Math.max(MIN_POSITION, pos)); +} + +function createSplitStore() { + const persisted = + typeof window !== 'undefined' + ? loadPersisted() + : { splitApp: null, dividerPosition: DEFAULT_POSITION }; + + let splitApp = $state(null); + let SplitComponent = $state(null); + let dividerPosition = $state(persisted.dividerPosition); + let isLoading = $state(false); + let isMobile = $state( + typeof window !== 'undefined' ? window.innerWidth < MIN_SCREEN_WIDTH : false + ); + + const isActive = $derived(!isMobile && splitApp !== null && SplitComponent !== null); + + // Listen for resize + if (typeof window !== 'undefined') { + window.addEventListener('resize', () => { + isMobile = window.innerWidth < MIN_SCREEN_WIDTH; + }); + + // Restore persisted app on load + if (persisted.splitApp && !isMobile) { + isLoading = true; + loadAppComponent(persisted.splitApp).then((component) => { + if (component) { + SplitComponent = component; + splitApp = persisted.splitApp; + } + isLoading = false; + }); + } + } + + return { + get splitApp() { + return splitApp; + }, + get SplitComponent() { + return SplitComponent; + }, + get dividerPosition() { + return dividerPosition; + }, + get isLoading() { + return isLoading; + }, + get isActive() { + return isActive; + }, + get isMobile() { + return isMobile; + }, + + async openSplit(appId: SplitAppId) { + if (isMobile) return; + if (splitApp === appId) return; + + isLoading = true; + const component = await loadAppComponent(appId); + if (component) { + SplitComponent = component; + splitApp = appId; + savePersisted({ splitApp: appId, dividerPosition }); + } + isLoading = false; + }, + + closeSplit() { + splitApp = null; + SplitComponent = null; + isLoading = false; + savePersisted({ splitApp: null, dividerPosition }); + }, + + setDividerPosition(pos: number) { + dividerPosition = clamp(pos); + if (splitApp) { + savePersisted({ splitApp, dividerPosition }); + } + }, + }; +} + +export const splitStore = createSplitStore();