Menu
Public repository / 3af23b0e937520000e1aa43c7e2eb9de2675abd1
AkurAI-Mail / docs/framework-migration.md
Latest change 3af23b0e937520000e1aa43c7e2eb9de2675abd1 - OLI-248: webmail list and reader as framework islands on a signals store by Ólafur Búi Ólafsson
# Webmail UI → AkurAI-Framework migration plan (OLI-241) Framework client runtime (AkurAI-Framework `site/frontend`): native ES modules, **no build step, no npm deps**. `signals.js` (signal/effect + `data-*` directives), `islands.js` (`registerIsland`, hydrate server-rendered markup), `spa.js` (router), `akurai-client.js` (typed auto-API client), `ai/components` (chat/tool UI), server-side templates. Source of truth checked: AkurAI-Framework@d506ed5. ## Inventory and mapping | Current (SvelteKit) | Size | Framework equivalent | Migration step | |---|---|---|---| | `src/lib/webmail/shortcuts.ts` + toggle in `+page.svelte` | 36 | `signals.js` + plain ESM | **Done in OLI-241** → `src/framework/shortcuts.js` | | `filters.ts`, `folders.ts`, `threads.ts`, `virtual.ts`, `recipients.ts`, `compose.ts`, `session.ts` (pure logic + tests) | ~380 | Plain ESM + JSDoc, tests stay on `bun test` | Rename `.ts`→`.js` with JSDoc types; no framework API needed. Wave 1. | | `sync.ts`, `sync.svelte.ts` (rune store) | 91 | `signals.js` `signal`/`effect` | Wave 1 (replace runes with signals) | | `SignalBadge`, `SyncBanner`, `SettingsDrawer` | 231 | Islands (`registerIsland`) over server-rendered markup | Wave 2 | | `RecipientCombobox`, `RichTextEditor`, `ComposeDialog` | 212 | Islands; editor needs a framework rich-text component (gap G4) | Wave 3 | | `MessageList.svelte` (virtual list, threads, chips) | 605 | Island + `data-for`; virtualization needs gap G3 | Wave 3 | | `MessageReader.svelte` | 381 | Island; sanitized-HTML render | Wave 3 | | `src/routes/webmail/+page.svelte` (state, actions, layout, CSS) | 1727 | Server template + `spa.js` route + one top-level island holding a signal store | Wave 4, last (decompose first) | | `src/routes/webmail/+page.ts` (auth gate + load) | 18 | Framework auth/sessions (`/docs/auth`) + `akurai-client.js` | Wave 4 | | `src/lib/api.ts` | 23 | `akurai-client.js` | Wave 4; keep `api.test.ts` green by porting 1:1 | Order rationale: leaf logic first (no UI risk), then components bottom-up, page last. Each wave ships behind green `bun test ./src` and `scripts/webmail-shortcuts-e2e.ts`-style browser checks. ## Wave 0 (this issue): shortcuts slice - `src/framework/shortcuts.js`: `resolveKey` keymap, `createShortcuts` (signal-driven listener + persisted toggle), `scrollSelectedIntoView`. - `src/framework/vendor/signals.js`: vendored framework runtime (see G1/G2). - `+page.svelte` now only maps actions to its own state; the keymap, toggle state and scroll helper live in the framework module. - Tests: `src/framework/shortcuts.test.js` (ported from OLI-239, written red first), plus `scripts/webmail-shortcuts-e2e.ts` (headless chromium against a mock API: Enter opens, `#` prompts delete, `j` scrolls selection into view, toggle persists and silences keys). - Behaviour note: the OLI-239 `shortcuts.ts` was only partially wired (page had its own handler with extra keys R/f/s/u/!/Delete/arrows). The framework module is now the single keymap and covers all of them; behaviour is unchanged. ## Framework gaps (to file as AkurAI-Framework issues) | # | Gap | Why mail needs it | Proposed owner | |---|---|---|---| | G1 | `signals.js` touches `document` at import time → cannot be imported under `bun test`/SSR | Unit-testing any framework module; vendored copy carries a one-line guard | Framework dev (Dev Lead to file) | | G2 | No installable/versioned client package (files are served from the site only) | We vendor-copy `signals.js`, which can drift; need a pinned import path (npm tarball or `/vendor/akurai@x.y.z/`) | Platform Engineer + Framework | | G3 | No virtualized list / windowing primitive for `data-for` | Inbox lists reach thousands of rows (`virtual.ts` today) | Framework dev | | G4 | No rich-text editor component | Compose (`RichTextEditor.svelte`) | Framework dev | | G5 | No keyboard-shortcut/hotkey helper or focus-scope utility | Generic version of today's `src/framework/shortcuts.js`; upstream it | Mail Dev (extract) → Framework | | G6 | `data-*` directives use `new Function`/`with`; mail renders untrusted content so only island JS (not directives) may touch message data | CSP and XSS posture for message bodies | SecOps Engineer review | | G7 | No typed (`.d.ts`) surface for signals/islands/client; we use JSDoc + `checkJs` | TypeScript-checked consumers | Framework dev | | G8 | Framework SPA router vs. IdP return_to redirect flow (`loginUrl`) undocumented | Page auth gate migration (Wave 4) | Framework dev + IDP owner | ## Risks - Tooling: `package.json` still pins SvelteKit/Vite; the Svelte page imports the framework module through Vite until Wave 4 removes Svelte. - `bun` is resolved via mise `latest` — pin before wave 1 (Dev Lead). - Do not deploy mid-migration waves without the browser check passing. ## Wave 2 progress (OLI-247) - `src/framework/signal-badge.js`: SignalBadge view model (label/color/icon/age); `SignalBadge.svelte` now uses it. - `src/framework/sync-state.js`: signals-based sync store with injectable clock/timers (tested); not yet wired — `sync.svelte.ts` stays until the page moves off Svelte (Wave 4 needs a signal→rune bridge). - Not done: SyncBanner/SettingsDrawer islands (need server-rendered markup + island host from Wave 4), `svelte-check` (no node_modules in worktree). - Run tests with the mise bun directly: `~/.local/share/mise/installs/bun/1.4.2/bin/bun test ./src` (shim fails in sandbox). ## Wave 3 progress (OLI-248): list and reader as islands The webmail page now exists twice: the SvelteKit page (still what `bun run build` ships) and the framework page under `app/webmail/` + `src/framework/`, assembled by `bun run build:framework` into `build/` with no bundler (the browser loads the same ES modules `bun test` runs). - Wave 1 finished on the way: `src/lib/webmail/*.ts` are `.js` with JSDoc types (checkJs clean). - `src/framework/store.js`: all page state and actions on `signals.js`; fetch, storage, navigation and timers are injected, so `store.test.js` covers loading, cache fallback, filters, paging, reading, reply/forward, send + undo, send later, draft recovery and settings without a DOM. - `src/framework/dom.js` (`h()`), `api.js`, `icons.js` (Lucide path data inlined, no runtime dependency; kebab or PascalCase names), vendored `islands.js` and `rich-text.js` (G2 still open). - Islands (`src/framework/webmail.js` registers them, `app/webmail/index.html` marks them): `webmail-rail`, `sync-banner`, `webmail-topbar`, `mail-canvas`, `message-list` (`list-island.js`: folder tree with remembered collapse state, chips, smart mailboxes, windowed thread rows via `virtual.js`, pager), `message-reader` (`reader-island.js`: toolbar, move menu, signals panel, body, attachments, empty states) and `webmail-overlays` (toasts, delete confirmation, shortcut reference). `compose-dialog` and `settings-drawer` are registered as empty mount points for wave 4. - Gotchas: `islands.js` hydrates on import, so `webmail.js` calls `hydrate()` again after registering; islands mount once the `ready` store promise resolves. Browser imports need explicit `.js` extensions. A new selection scrolls into view from inside the list render, not a separate effect (effects re-subscribe in a different order after they re-run). - `scripts/webmail-e2e.ts` (`bun run e2e:framework`): mock API + headless chromium, 19 checks (windowing, folders, Enter/j/k, archive, delete dialog, search, folder switch, chips, help). - Not in wave 3: compose, settings drawer, admin pages, login, SvelteKit removal (wave 4, OLI-249).