The Export context-menu offered only md↔docx↔html (a symmetric set), so PDF
— which the server supports only as md→pdf — was missing. The markdown
editor's DOCX/HTML/PDF buttons hardcoded their own list, so the two could
drift.
Introduce a single source of truth in download.js: EXPORT_MATRIX mirrors
zddc/internal/convert.Convert() exactly (md→docx|html|pdf, docx→md|html,
html→md|docx), exposed as download.exportTargets(ext) + download.convertUrl().
The Export submenu and the editor's buttons both consume it, so a .md file now
offers PDF in the menu and the two surfaces can never disagree. PDF stays
markdown-only (no docx→pdf / html→pdf path exists server-side).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an "Export" item to the row context menu with a submenu:
- a folder offers ".zip" (reuses download.downloadFolder; works offline + server)
- an md/docx/html file offers the OTHER two formats, each triggering a
server-side conversion download via the new download.exportFile (builds the
sibling-extension URL and lets the browser pull the converted bytes). File
conversion is server-only, so it's hidden in offline (FS) mode; a zip is
already an archive and gets no Export.
menu-model's toMenuItem now passes a descriptor's `items` through as a submenu
(resolved against the captured browse ctx) instead of only emitting action rows.
Verified: 11/11 browse Playwright specs pass (incl. menu/context + Download ZIP);
a logic harness confirms the per-type submenu contents and that clicks route to
download.exportFile / downloadFolder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
downloadFsSubtree pre-read every file's arrayBuffer() and handed the raw
ArrayBuffer to JSZip, so the entire subtree's bytes sat in the JS heap at
once before zipping — the likely OOM on a large local folder despite the
size warning. Hand JSZip the File (a Blob backed by disk) instead; it reads
each lazily during generateAsync, dropping peak memory to roughly the zip
output plus JSZip's working set.
Also document, on downloadUrl, why server-side download errors aren't
surfaced as toasts: the <a download> click is fire-and-forget, and the
folder path targets zddc-server's streamed virtual "<dir>.zip" endpoint —
routing it through fetch() to make errors catchable would defeat the
streaming for arbitrarily large archives. Left as a known, documented
limitation rather than a buffering regression.
All 6 browse Playwright specs pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace `?zip=1` / `?convert=docx|html|pdf` query forms with path-suffix
URLs that look like ordinary files. `<dir>.zip` and `<file>.docx` /
`.html` / `.pdf` are virtual files served by the dispatcher when stat
fails at the requested path AND the corresponding base resource exists:
GET /Project-1/archive.zip ← if archive/ is a real directory
GET /Project-1/notes.docx ← if notes.md exists
Real on-disk files always win — a genuine archive.zip in the tree
serves its bytes normally. The virtual forms only fire when nothing
real is there.
Why: the URL form lets clients emit plain <a href> without query-
string handling; `curl -O` writes a sensible filename; mirror tools
pick up the path through normal recursion; the protocol surface
becomes "every URL is a file". Bash + filesystem mental model.
Server:
- New helpers handler.RecognizeVirtualSubtreeZip /
RecognizeVirtualConvert (in subtreezip.go and converthandler.go).
- Dispatcher's stat-fails branch checks them between IsDefaultMdlSpec
and MatchAppHTML. ACL is enforced on the base resource (the source
directory for zip, the .md source for convert).
- Three legacy query-form branches removed from main.go.
Client:
- browse/js/download.js: `dir + '.zip'` instead of `dir + '/?zip=1'`.
- browse/js/preview-markdown.js: convert anchor hrefs become
`<mdUrl-minus-.md>.<fmt>` instead of `<mdUrl>?convert=<fmt>`.
- shared/zddc-source.js downloadConverted: same transform.
Tests: subtreezip_test.go test URLs cosmetically updated to the new
shape (the handler is exercised directly, so the URL is metadata only,
but the test reads better).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major upgrade to the browse tool's UX, plus a few shared modules other
tools can adopt.
User-facing:
- Right-click context menu on tree rows AND empty pane space. Traditional
file-manager grouping (Open / Download / New / Rename-Delete / Copy /
Tree ops / View). Items stay visible but disabled when not applicable
so muscle memory carries. Generic shared/context-menu.js framework
supports normal items, toggles, submenus, separators, danger styling.
- YAML editor for .yaml / .yml / .zddc files (CodeMirror 5 vendored at
shared/vendor/codemirror-yaml.min.*). js-yaml lint on every change
for parse errors. For .zddc cascade files, an additional schema-aware
lint pass flags unknown keys, bad enum values, and wrong types.
- Per-row drag-drop upload using webkitGetAsEntry (folder uploads work
recursively). Per-row drop indicator; doc-level overlay still fires
for blank-space drops at drop_target scopes.
- New folder / New markdown file context-menu items (server mode).
Rename + Delete with native confirm() dialog. File-API helpers
removeNode / renameNode use the existing PUT/POST/DELETE endpoints.
- Hover info card with the row's full metadata (ZDDC fields + filesystem
info + path/URL). Interactive — mouse into it, drag-select text,
Ctrl/Cmd-C or right-click → Copy. 200ms grace before dismiss.
- Autofilter input at the top of the tree pane. Same grammar as
archive's column filters (zddc.filter.parse / matches). Filters
files; folders without matches collapse out. Non-matching folders
force-open visually when descendants match, without mutating the
user's actual expand state.
- Two-line ZDDC label: title-first, tracking/rev/status as monospace
meta below. Icon column anchors to the title line. Chevron is a
Lucide outline `chevron-right` SVG, rotated 90° on `.expanded`.
- File-type Lucide icon sprite (shared/icons.js — 16 outline glyphs,
~5 KB). PDF / Word / Spreadsheet / Slides / Image / Video / Audio /
CAD / Web / Config / Code / Archive get distinct icons; folders
tinted with --primary.
- Header wraps gracefully at narrow viewports (shared/base.css
flex-wrap + title min-width:0 ellipsis). Body becomes flex column
in browse so a wrapping header doesn't break #appMain height.
- Markdown editor opens in WYSIWYG mode by default. YAML front-matter
+ TOC sidebar reworked: flexbox layout (single visible resizer
between FM and TOC), both bodies overflow:auto for X+Y scrollbars.
- `?file=<path>` deep links open browse pre-positioned at a specific
file. Multi-segment paths walk into subdirectories on the way.
Auto-flips Show hidden when a segment is dot/underscore-prefixed.
- Refresh + show-hidden toggle preserve expansion / selection /
preview pinning. Path-keyed snapshot survives a re-fetched listing.
- "Add Local Directory" → "Use Local Directory" across the four tools
that have it (browse, archive, classifier, +transmittal comment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A "⤓ Download (zip)" button in the browse toolbar (shown once a
directory is loaded) downloads the directory you're currently
viewing — and everything under it you're allowed to see — as a single
.zip. Navigate into a subfolder first to grab just that subtree.
- Server mode: an <a download> at "<currentPath>?zip=1" — zddc-server
streams the ACL-filtered zip (see the previous commit), nothing held
in the browser.
- Offline (file://) mode: new browse/js/download.js walks the picked
folder with the FS-Access API in two passes — metadata first (so it
can confirm() before loading >~2000 files / ~500 MB into memory),
then bytes — bundles with the already-vendored JSZip, and triggers a
blob download. Hidden entries (".":/"_"-prefixed) are skipped, the
zip's top level is "<folderName>/…" so it unpacks tidily, and the
status bar shows progress.
Wired in browse/js/events.js (button click + show/hide alongside the
refresh button); concatenated into browse/build.sh; ARCHITECTURE.md +
AGENTS.md note the ?zip=1 endpoint and the button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>