Files
pad/web
xarmian c7cd363173 fix(web): a renamed item's links follow the new title in collab-edited docs (BUG-3315 U2) (#1688)
* fix(web): a renamed item's links follow the new title in collab-edited docs (BUG-3315 U2)

A tab loads [[TASK-1]] as a link whose TEXT is the title at that moment,
and the text lives in the collaborative document from then on. After a
rename, the next save compared it with the current title only and wrote
[[TASK-1|Old Title]], pinning the old title for good.

The load now marks a link that follows the title with the link mark's
EXISTING `title` attribute, set to the text as loaded, so no schema change.
It is set only for a link with no explicit override whose text is the
title. The save treats text still equal to the marker as following the
title ([[REF]]). An edited text, or a deliberate [[REF|X]] (which loads
without the marker), is kept as an override.

The marker never reaches stored content: markdownToWikiLinks consumes it,
and cleanBrokenLinks, the last step of every save pipeline, strips any that
is left, because those pipelines skip markdownToWikiLinks on an empty
index. SafeLink keeps a same-origin link's `title` out of the DOM, so the
stale title is never shown as a tooltip.

Documents already in an op-log carry no marker and keep today's behaviour
until they are next loaded. The characterization test from #1684 is
flipped to the fixed answer.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): keep the save's link match as wide as before the marker (BUG-3315 U2, codex r1)

The marker regex narrowed the href to exclude whitespace, so a link whose
last segment had a space stopped converting. The path is matched lazily
instead, ahead of the optional title, and the cleanBrokenLinks strip the
same way. A guard leg pins it.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): the follows-title marker is prefixed, so a user's link title is never read or stripped (BUG-3315 U2, codex r2)

cleanBrokenLinks stripped EVERY same-origin link title, so a user-authored
[x](/path "tip") lost its tooltip on save. markdownToWikiLinks would also
read that title as a marker. The marker is now "pad-follows-title:<text>".
Only a title with that prefix is read, stripped, or hidden from the DOM. A
titled internal link a user wrote is left exactly as before U2, which never
converted it. The prefix lives in its own module (utils/followsTitle.ts),
so safeLink, which the materializer bundles, imports nothing DOM-dependent.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): a follows-title marker straight from the load still reads as the title (BUG-3315 U2, codex r3)

Through the editor the marker comes back as the serializer writes a title
(only " escaped), so it is compared raw. Straight from wikiLinksToMarkdown
it still carries the load's \\, \" and \& escapes, so it is also compared
with those undone. The ')' findings of the same round did not reproduce;
A)B, f(x) = y and q"uote) convert and strip correctly on both paths.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* docs(web): the follows-title prefix is reserved (BUG-3315 U2, codex r4)

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): the follows-title marker is stripped from every link at save, whatever its href (BUG-3315 U2)

The lead's gate before the GO: the marker never reaches stored markdown on
any path where a marked link does not convert back to [[...]]. A leg now
covers ten paths through real save pipelines: target deleted, empty index,
restricted viewer, hard break in the text, partial bold, markdown table,
HTML-serialized table, and a link whose URL the user edited to an external
address (target present, and empty index). It asserts no
'pad-follows-title' substring, with a guard that each document really
carried the marker.

The URL-edit path LEAKED: the mark keeps its attributes, and both strips
matched only same-origin hrefs. The final strip in cleanBrokenLinks now
removes the reserved-prefix title from any link. Red variants: that strip
off alone leaks 3 paths, all strips off leaks 9.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): the marker strip touches only a real link's title, never code or prose (BUG-3315 U2, codex)

The broadened strip removed marker-shaped text wherever it appeared, so a
doc that mentions the marker (a code span, a fenced block, escaped-bracket
prose) lost text on save. Marker handling in markdownToWikiLinks and the
cleanBrokenLinks strip now skip fenced code blocks and inline code spans
(codeRanges), and the strip needs an unescaped '](' with a space-free href.
Four legs pin it: each goes red when its guard is removed.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): marker strip counts backslash parity; code scan is linear (BUG-3315 U2, codex)

A link text ending in a backslash serializes as [a\\](...): its ']' follows
a backslash, but an even number of them, so it is a real link. The strip's
lookbehind now requires even parity, where it used to keep the marker. The
inline-span scan rescanned per opening run (267ms for 40,000 alternating
runs, 18x for 10x the input). It is now one pass with per-length pointers
and binary-searched ranges (9.6ms, 98ms at 400,000). The fence finding of
the same round did not reproduce: a closing fence with trailing spaces is
recognized. Legs pin both fixes.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* fix(web): an inline code span never pairs across a fenced block (BUG-3315 U2, codex r3)

Backtick runs are paired only inside their own gap between fences, which
also keeps every code range disjoint, as the binary search requires. A
leg pins it; it goes red when the gap is ignored.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* test(web): pin that a run inside a code span opens no second span (BUG-3315 U2, codex r4)

Codex r4 read the span pairing as able to overlap spans of different
lengths. It cannot, because the scan resumes after the closing run, so the
finding did not reproduce. This leg pins it, and goes red when the resume
is removed.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
2026-09-30 08:52:35 -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

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
    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