mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-25 03:42:06 +00:00
53dc0b7db8
* fix(web): delete confirmation becomes an in-menu sub-view (TASK-2327)
Moves the inline `.delete-confirm` band out of `.meta-actions` and into
the pane `⋯` overflow as a third drill-down view alongside `move`
(PLAN-2326 DR-6). The band was a ~180px text-plus-two-buttons control
that could not survive the 360px pane the strip refactor (TASK-2328)
targets; as a menu sub-view it is width-independent by construction and
`sheetOnMobile` gives mobile a bottom sheet for free.
Ships first so main never carries a broken intermediate: the strip
refactor deletes `.meta-actions`, and until the confirmation moves, a
`Delete…` click would arm state with no confirmation UI rendered.
- `paneMenuView` widened to `'root' | 'move' | 'delete'`; the `{:else}`
branch that rendered the move-target list for EVERY non-root view is
split into explicit `move` / `delete` branches.
- `Delete…` drills down instead of closing the menu; `confirmDelete`
state is gone. Cancel returns to root, the view resets on close (the
existing `onclose`), and the item-switch / peek-freeze resets already
covered `paneMenuView`, so the armed-confirmation-survives-a-switch
hazard is unchanged. `handleDelete`'s failure path disarms, dismisses
the menu and returns focus to the trigger.
- Cancel is listed FIRST so the focus handoff lands on the
non-destructive row — Enter on arrival can never delete. The prompt is
a presentational div, so Menu's `[role^="menuitem"]` arrow-key walk
sees exactly the two actionable rows; MenuItem gains an optional
`describedBy` so the destructive row carries the prompt as its
aria-describedby (it would otherwise never be announced — Codex P2).
Also fixes the focus-handoff defect that the `move` sub-view already had
(DR-8, folded in per the fold-in-by-default rule): the focus $effect only
ran when `open` changed, so an in-place view swap stranded keyboard focus
on the unmounted MenuItem. `Menu` gains an optional `focusKey` prop that
the effect reads purely for dependency tracking, and forwards it to
`BottomSheet`, which owns focus in `sheetOnMobile` mode and had the same
gap (Codex P1). Both effects still only perform DOM focus/placement, so
neither can self-trigger (CONVE-1688). `ItemDetail` passes
`focusKey={paneMenuView}`, fixing move and delete together on both
surfaces.
Gates: `npm run check` 0 errors; `make check` exit 0; full Playwright
e2e suite green at CI worker count (77 passed). Verified by hand against
`make install` (40 scripted browser checks): in-place swap, cancel,
Escape-closes-and-returns-focus, reset-on-close, arrow-key walk inside
the sub-view, keyboard-only path, focus handoff on BOTH move and delete,
aria-describedby wiring, and an end-to-end delete (`deleted_at` set) —
across full-page, docked pane, mobile bottom sheet, and dark theme.
Claude-Session: https://claude.ai/code/session_01E2fRi12n8rARczvdEa2LYT
* test(web): FreezeProbe mirrors the ⋯-menu route to delete/move (TASK-2327)
`FreezeProbe.svelte` is a hand-written mirror of ItemDetail's freeze /
permission gate expressions (BUG-2263). Its `delete-btn` and `move-btn`
rendered bar buttons, which no longer exist: #1029 moved Move into the ⋯
overflow and TASK-2327 moved Delete's confirmation there as a drill-down.
The probe stayed green while mirroring markup that was gone — `move-btn`
had been stale that way since #1029.
The row gate (`{#if canEdit}`) was in fact still correct; what was
missing was the REACHABILITY half. Both surfaces are now reached through
one trigger, so the probe mirrors it: `pane-more-btn`, with no canEdit
and no peeking gate (it renders on the peeking side and a click activates
that side first) and `disabled={moving}`. Without it, gating the trigger
on `!peeking` would take delete AND move off the passive side with every
existing assertion still passing.
Delete's confirm row gets its own model and test, because its gate is
genuinely different in two ways:
- It is NOT canEdit-gated. It renders whenever the 'delete' sub-view is
active and refuses via `disabled={deleting || !canEdit}`, so a
mid-confirm permission loss leaves it present but inert. (A first draft
wrapped it in `{#if canEdit}` — caught by Codex, since that would have
claimed the row vanishes when the real one does not.)
- It IS the one delete-related surface the freeze touches, and in the
opposite direction to everything else in the file: peek-begin
force-disarms it (ItemDetail's peek handler resets paneMenuOpen /
paneMenuView), so an armed confirmation can never survive into a peek.
The affordance itself stays live on the peeking side as before.
Mutation-tested — all four bite, each failing exactly one test:
peek-no-longer-disarms, confirm-drops-the-permission-guard,
canEdit-gate-the-confirm (the Codex finding), trigger-drops-its-in-flight
guard.
`make check` exit 0 (490 vitest tests, was 488); `npm run check` 0 errors.
Claude-Session: https://claude.ai/code/session_01E2fRi12n8rARczvdEa2LYT
* fix(web): drop the probe's invented peek gate; mark the menu prompt presentational (TASK-2327)
Two review findings on PR #1032.
1. FreezeProbe gated the delete-confirm row on `deleteViewArmed && !peeking`.
That reintroduced the drift it was meant to fix, in a subtler form: the
real row renders on `paneMenuView === 'delete'` ALONE. Peek safety is an
EMERGENT effect of ItemDetail's peek-begin handler resetting paneMenuOpen /
paneMenuView — it is not a gate on the row. Encoding it as one is worse
than asserting nothing: delete the reset from ItemDetail and the probe
stays green off its own hard-coded `!peeking`, mirroring nothing. The
earlier mutation testing didn't catch this because mutating the PROBE only
proves the test is sensitive to the probe.
The gate is dropped and the render condition mirrored exactly. The
peek-disarm property is now explicitly NOT claimed, with the reasoning in
the file: a static prop-driven mirror can't express a transition, and e2e
can't discriminate it either — every click that causes a peek is also an
outside-click that closes the menu on its own, so a passing assertion would
prove nothing. Filed TASK-2337 for real coverage of that reset (it guards
five other surfaces too — editingTitle / shareDialogOpen /
editCollectionOpen / showAddLink — and nothing asserts any of them today).
Re-ran mutation testing on what remains; all four still bite, one test
each: confirm-drops-the-permission-guard, canEdit-gate-the-confirm,
trigger-drops-its-in-flight-guard, move-row-drops-its-in-flight-guard.
2. The prompt div inside `role="menu"` was undeclared. It now carries
`role="presentation"`. Verified against the rendered a11y tree rather than
assumed: the destructive row reports name "Delete item" / description
"Delete this item?", Cancel reports no description, and the menu's direct
children are [presentation, menuitem, separator, menuitem]. A second Codex
note corrected two overstatements in the comment — role=presentation is not
what excludes the prompt from the `[role^="menuitem"]` walk (a bare div was
already excluded), and a menu owns separator/group children too, not only
menuitems.
Gates: `npm run check` 0 errors; `make check` exit 0; delete flow re-verified
end-to-end (29 desktop + 11 pane/mobile + 7 a11y checks) against `make install`.
Claude-Session: https://claude.ai/code/session_01E2fRi12n8rARczvdEa2LYT
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