Files
pad/web
xarmian 1af63e3a47 feat(web): wire ContentSkeleton into item detail loading + collab-sync (TASK-1375) (#515)
* feat(web): wire ContentSkeleton into item detail loading + collab-sync (TASK-1375)

Two gaps where the item detail page would show a blank body:

1. HTTP-load gap — `{#if loading}` rendered the literal string
   "Loading..." while loadData()'s GET was in flight. Now renders
   <ContentSkeleton variant="page" />.

2. Collab-sync gap — for editable items, the Editor mounted on
   `ydoc !== null` but content lives in the Y.Doc which starts
   empty until the WS replays the op-log. On slow/broken
   connections this looked indistinguishable from a genuinely
   empty item. The {:else if ydoc} branch now renders
   <ContentSkeleton variant="inline" /> while
   `collabProvider.state === 'connecting' && !hasEverSynced`.

The `hasEverSynced` latch (split into two $effects per CONVE-606
— route-change reset on item.id vs reactive-state-sync on
collabProvider.synced) ensures mid-session `reconnecting` does
NOT re-show the skeleton over already-rendered content. The
reset on item navigation handles SvelteKit's reuse of
+page.svelte across [slug] changes.

Error UI and retry (offline state, stuck-connecting timeout) is
TASK-1376's scope — this PR is skeleton-only.

Parent: PLAN-1373. Resolves part of BUG-1372.

* fix(web): reset hasEverSynced on provider change, not just item.id (TASK-1375 round 1)

Per Codex review: the original `void item?.id` reset dependency
missed two cases where the same item gets a fresh unsynced Y.Doc:

  1. Raw -> Rich mode toggle (collabKey derives from rawMode, so
     the collab $effect tears down + rebuilds the provider but
     item.id is unchanged).
  2. forceRefreshNonce bump (server force_refresh or a future
     TASK-1376 retryCollabSync), which rebuilds the provider
     against the same item.

In both cases hasEverSynced=true survived the rebuild, the
skeleton gate bypassed, and the editor mounted on the new
unsynced Y.Doc showing blank — re-opening the very pre-sync
empty window the skeleton was meant to close.

Fix: depend on `collabProvider` (the reactive variable, not its
fields). Any provider instance change — navigation, mode toggle,
nonce bump, cleanup-to-null — fires the reset. Strictly more
correct than item.id since it also covers same-item rebuilds.

Codex round 1.

* fix(web): null editorInstance on provider change (TASK-1375 round 2)

Per Codex review round 2: `editorInstance` is set by the Editor's
`onEditor` callback on mount but is NOT re-nulled on unmount. During
the connecting-skeleton window for a same-item provider rebuild
(rawMode toggle, force_refresh), the old <Editor> unmounts but
`editorInstance` still points at the previous (now-destroyed)
instance.

If an `applier_request` frame arrives on the new provider during
that window, `onApplierRequest` would call `setContent` on the
WRONG editor (or a destroyed one) instead of returning false and
letting the server fall back to a direct items.content write.

Fix: null `editorInstance` in the same reset $effect that resets
`hasEverSynced`. The new editor's onEditor callback re-populates
the reference once it mounts after the skeleton phase.

Codex round 2.
2026-05-12 11:58:33 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
      activity/              ActivityFeed
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens