This document describes the architecture for the Svelte 5 implementation of Zander — the LCARS Bookmark System. It is the canonical reference for the Svelte SPA.
Legacy reference: the previous single-file implementation is preserved as index.legacy.html (not described here beyond reference links).
The architecture is defined by these artifacts:
- Types and data contracts:
src/lib/state/model.ts(orsrc/lib/state/stateTypes.tsif not yet renamed) - App state controller:
src/lib/state/index.svelte.ts(rune-based shared module) - Theme controller:
src/lib/state/theme/index.svelte.ts(rune-based shared module) - Persistence port:
src/lib/persistence/PersistenceBackend.ts - Persistence implementations:
src/lib/persistence/LocalStorageBackend.ts(v1 guest mode)FirestoreBackend(v2 user mode)
- UI structure:
src/App.svelteandsrc/lib/components/** - Accessibility requirements:
ACCESSIBILITY.md - Visual/design primitives:
DESIGN.md - Behavioral contracts: tests (Vitest + component tests) and Playwright (E2E)
Specs:
- Authoritative specs:
spec/(YAML/Markdown requirements + scenarios) - Any
src/specor otherspec/folders are considered legacy/internal notes unless explicitly migrated intospec/.
- Provide a maintainable Svelte 5 SPA that replicates LCARS look-and-feel and core flows.
- Use Svelte 5 idioms (runes) and TypeScript for state/domain logic.
- Keep persistence behind a stable port (
PersistenceBackend) supporting:- Guest mode (localStorage)
- User mode (Firestore)
- Maintain keyboard-first interactions and meet WCAG 2.1 AA where practical.
Non-goals:
- A router-based multi-page architecture (the app is a view-switching SPA).
- Extracting LCARS primitives into a standalone package before v1 is complete.
Entry points:
index.html— SPA entry (bootssrc/main.ts)src/main.ts— mountssrc/App.svelte(avoid top-levelawaitto preserve older browser targets)src/App.svelte— root LCARS shell and view switcher
UI composition:
src/lib/components/lcars/— LCARS layout primitives (header/sidebar/footer/status)src/lib/components/views/— view-level components:BookmarksView.svelteSettingsView.svelteAboutView.svelte
src/lib/components/dialogs/— modal dialogs:BookmarkDialog.svelteConfirmDialog.svelte- (Color picker dialog if present)
State and domain (no svelte/store):
src/lib/state/— domain model, selectors, and rune-based shared state modulesindex.svelte.ts— app state controller (uses runes; must be.svelte.ts)index.ts— plain TypeScript barrel re-export surface (no runes)app/— app-state modules: persistence queue + mutation actionstheme/— theme controller module(s)domain/— pure domain functions (no side effects; unit-testable)selectors/— pure derived reads (selectors; no side effects)model.ts(orstateTypes.ts) — canonical domain typesdefaults.ts(orstateDefaults.ts) — default state factory
Persistence:
src/lib/persistence/— persistence port + backends- Port:
PersistenceBackend.ts - v1 backend:
LocalStorageBackend.ts - v2 backend:
FirestoreBackend.ts
- Port:
Auth and telemetry:
src/lib/auth/— authentication abstractions/implementations (Firestore Auth or Logto)src/lib/telemetry/— optional telemetry (no-op by default unless enabled)
Canonical types are defined in src/lib/state/model.ts (or stateTypes.ts). Architectural invariants:
- Category is a tree
Category.children: Category[]is the canonical representation.- Category identity is stable via
Category.id.
- Bookmarks belong to a category
Bookmark.categoryId: stringis required.- Root selection is represented by
State.currentCategoryId = null(not nullable bookmark categories).
- Creation timestamps are immutable
Bookmark.createdAtandCategory.createdAtare creation timestamps and should not be modified during edits.
State-driven navigation:
State.currentViewcontrols which view is active ("bookmarks" | "settings" | "about").State.currentSettingsPagecontrols the active settings sub-page.State.landingCategoryIdrepresents the user’s configured “home” category (used when the app goes “Home”).
Zander does not use svelte/store (writable, derived, $store) for app state.
Instead, app state and theme state are managed via Svelte 5 shared modules (.svelte.ts) using runes:
$statefor shared mutable reactive state$derivedfor derived values (if/when needed)$effectfor side effects when appropriate
- Any file that uses runes must be a
.svelte.ts(or.svelte.js) module. - Plain
.tsmodules must not call runes. - To avoid accidental
rune_outside_svelteat runtime, Zander uses a two-file entrypoint pattern:src/lib/state/index.svelte.tscontains the rune-based implementationsrc/lib/state/index.tsre-exports from.svelte.tsand exports types (no runes)
This keeps imports stable ($lib/state) while ensuring rune usage is always inside Svelte-compiled modules.
main.tsmountsApp.svelte.App.sveltecreates controllers:createAppState(backend)from$lib/statecreateThemeState(storage?)from$lib/state
App.sveltecalls:app.loadInitialState()to load persisted app statetheme.loadInitialTheme()to apply persisted theme + setdata-theme
- UI renders based on
app.model.stateand other controller state.
- All mutations go through controller APIs (not direct component writes).
- Mutations persist via
PersistenceBackend.saveState(state)as full-state snapshots. - Writes are serialized through a single
persistAndSetqueue to prevent lost updates when multiple async mutations occur.
- Header “Home” and footer buttons update app state (
currentView, etc.). - Keyboard shortcuts provide fast navigation between views and dialogs.
- When switching views, focus is moved to a predictable target (e.g.,
<main tabindex="-1">) to keep keyboard users oriented.
Persistence is abstracted behind src/lib/persistence/PersistenceBackend.ts:
export interface PersistenceBackend {
loadState(): Promise<State | null>;
saveState(state: State): Promise<void>;
exportData(): Promise<ExportBundle>;
importData(bundle: ExportBundle): Pro