mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
cd2bf7f977
Adds the third preview predicate GitHub #1169 needs, and widens the surface-renderer union onto the 'text' slot PLAN-2393 reserved. `canPreviewAsText` is deliberately NOT an edit to either existing predicate. `canOpenInViewer` (DR-16) asks what the in-app IMAGE viewer may decode; `canBrowserPreview` (DR-5) asks what the BROWSER may be handed in a new tab, where for `text/markdown` the honest answer stays no because the browser downloads it. Widening DR-5 to reach #1169 would route markdown to the Open-in-new-tab action and reproduce the exact download-instead-of-render behaviour the issue reports. The new question — do WE fetch the bytes and render them ourselves — is the only one #1169 asks, and it gets its own predicate. `canBrowserPreview` and its tests are untouched, which is the signal that nothing widened. MIME-exact, never by category: the server's `CategoryText` CONTAINS the force-download bucket (`text/html`, `text/javascript`, `application/javascript` — `internal/attachments/mime.go`), so a category test would admit exactly the types PLAN-2393 DR-6 forbids inlining. An allowlist excludes them by construction. Deliberately outside the server mirror: `inlineSafe` in mime.go declares itself the mirror of `VIEWER_MIMES` + `BROWSER_PREVIEW_MIMES`, the set the server may send `Content-Disposition: inline`. This set must never join it — we never ask the browser to inline these bytes, we `fetch()` them (which a download disposition does not impede) and render sanitized HTML ourselves. That is what lets `text/markdown` preview in-app while still being served as an attachment. `isMarkdownAttachment` consults the FILENAME, and that is the path that actually fires rather than belt-and-braces: an uploaded `.md` is stored as `text/plain`. `ValidateUpload` sniffs the bytes with `http.DetectContentType`, which answers `text/plain` for prose, and returns the SNIFFED entry; the extension is used only to REJECT a mismatch, and `.md` → `text/markdown` shares `CategoryText`, so nothing rejects. Measured: `ValidateUpload([]byte("# Heading\n..."), "preview.md")` returns `mime="text/plain"`. The MIME check stays first (an explicitly-typed row is honoured whatever it is named); the extension fallback is GATED on the MIME already being in the set, so a filename can never widen what previews. Server side filed as BUG-2841. Set is smaller than what we could render: csv/tsv/json/xml/yaml/toml are allowlisted uploads and all left out, because each has an obviously better rendering this unit does not build, and shipping them raw now would make that rendering a regression later. Tests pin the exclusions with that reason. `TEXT_PREVIEW_MAX_BYTES` carries its receipt: measured over 25 repo `*.md` files (p50 3.6 KB, max 82 KB) and 63 workspace doc bodies (p50 5.0 KB, max 23 KB); 1 MiB is ~12x the largest observed, sits under the 25 MiB upload bound so it has a live range, and errs toward rendering. The comment states the limit honestly — the cap always bounds what is RENDERED, but it saves the TRANSFER only when the size is known before the load. One existing assertion changed meaning rather than breaking: the renderer test asserted `text/plain` has no renderer. It now asserts 'text'. That is the contract change, made explicitly and annotated. Two force-download assertions are labelled as regression guards rather than evidence — they pass against origin/main too. Refs IDEA-2712
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