feat(web): app-registry architecture — OS + Apps, lazy loading, installable apps
Problem: the frontend had an implicit OS+Apps metaphor (desktop, floating windows, an app registry) but the contract was informal — the mascot was hardcoded into the shell, all apps were statically imported into one 800KB bundle, and there was no install/uninstall path. Change: three phases landed. - Phase 1 (contract + docked kind): AppDef extended with docked/noIcon and optional geometry; the mascot registered as a docked app via a generic DockedLayer that replaces the hardcoded <MascotLayer />; openAppWindow branches on docked → toggleDocked; persisted docked visibility store (absent key = visible, no APPS import to avoid a static cycle). - Phase 2 (lazy loading): AppDef.component is now a dynamic-import loader; LazyApp renders with a loading skeleton; Vite code-splits each app (main bundle 800KB→485KB); the LazyMascot wrapper is gone since the lazy loader breaks the import cycle directly. - Phase 3 (installable apps, local bundles): AppManifest + catalog + installApp/uninstallApp + localStorage persistence; reactive apps store (built-in + installed) and derived appById; App Store page; Notes demo app; icons.ts and WindowLayer's orphan-close react to registration so installs appear without a reload. - Structure: data-table casing unified to PascalCase; the mislabeled DataTable.svelte.ts (pure types, not runes) renamed to types.ts; LazyApp colocated with its desktop-shell consumers; app-store moved under lib/ so the dependency direction is consistent. Risk: the app registry is now a reactive store, not a static array, so every consumer (Desktop, DockedLayer, Taskbar, icons, windows) reads from derived stores. Two static-cycle traps are documented in docs/mbse/components.md §9: docked.ts must not import APPS (it would fire a TDZ at init via the apps.ts→pages→windows.ts→here path), and apps.ts must not statically import the mascot (the lazy loader defers its module graph). Remote bundle loading, the /api/v1/apps endpoint, and permission enforcement are deliberately NOT in this commit — they are security-critical and deferred to Phase 4 with an ADR. Verification: vitest 38/38; svelte-check + tsc clean for changed files; eslint clean; vite build green; runtime smoke confirmed (install Notes → icon appears → open → uninstall → icon + window gone; survives reload). docs/mbse/components.md Component 9 and the plan updated. Plan: plans/2026-07-21-frontend-os-apps-architecture.md
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
// opened from Knowledge Base, chat, or anywhere else land in the same
|
||||
// floating window layer, with several windows open side by side, rather than
|
||||
// each page owning its own single-entity sidebar/sheet.
|
||||
import { derived, type Readable } from 'svelte/store'
|
||||
import { derived, get, type Readable } from 'svelte/store'
|
||||
import { createManager, createDesktop, wmStore } from '@surdeddd/wmkit/svelte'
|
||||
import { persist } from '@surdeddd/wmkit/persist'
|
||||
import { appById, appWindowId } from '$lib/apps'
|
||||
import { toggleDocked } from '$lib/stores/docked'
|
||||
import { sessions } from '$lib/stores/chat'
|
||||
import { heading } from '$lib/tasks'
|
||||
|
||||
@@ -75,9 +76,18 @@ export function toggleShowDesktop(): void {
|
||||
// Opens (or focuses/restores) a registry app's window. Apps are
|
||||
// single-instance — double-clicking an already-open app's icon should never
|
||||
// stack a second window, same dedupe pattern as openEntityWindow below.
|
||||
// Docked apps (e.g. the mascot) have no wmkit window at all — clicking their
|
||||
// icon toggles visibility on the Docked Layer instead, so this branches on
|
||||
// kind before touching the window manager. All callers (the desktop icon,
|
||||
// the taskbar settings button, legacy hash resolution) go through here, so
|
||||
// none of them need a kind-specific branch.
|
||||
export function openAppWindow(appId: string): void {
|
||||
const app = appById.get(appId)
|
||||
const app = get(appById).get(appId)
|
||||
if (!app) return
|
||||
if (app.docked) {
|
||||
toggleDocked(appId)
|
||||
return
|
||||
}
|
||||
const id = appWindowId(appId)
|
||||
if (wm.get(id)) {
|
||||
wm.restore(id)
|
||||
|
||||
Reference in New Issue
Block a user