Local folders are picked read-only, so create/edit/rename/delete were
either disabled or would fail. Now offline mode supports the same CRUD as
server mode, bounded only by what the filesystem grants:
- upload.js: ensureWritable() escalates the picked root to readwrite via
the FS-Access permission prompt on the first mutation (one prompt, then
granted for the session; requires the user gesture every caller has).
makeDir/makeFile gain FS-API branches (getDirectoryHandle/getFileHandle
{create:true} + createWritable) resolved through handleForDir; removeNode
and renameNode (already FS-API) now escalate first.
- preview-markdown.js: the markdown editor's save escalates before
createWritable, so editing a local .md persists.
- events.js: New folder / New markdown file menu items are enabled whenever
there's a writable target (server, or a picked local folder) via
canCreateHere(); rename/delete were already gated by canMutate (FS-API).
The aggregator party-picker stays server-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two layers shipped together since the second builds on the first.
LAYER 1 — reviewing/ + Plan Review scaffolding
- reviewing/ is now a real folder under each project, populated by the
Plan Review composite endpoint. The old reviewing/ virtual aggregator
handler is retired.
- POST /<project>/archive/<party>/received/<tracking>/ with X-ZDDC-Op:
plan-review scaffolds physical workflow folders under reviewing_root
and staging_root, each carrying .zddc.received_path pointing back at
the canonical submittal. Idempotent re-runs match by received_path
and re-converge the ACL.
- Virtual received window: when listing or writing under
<workflow>/received/, the server resolves through the canonical
archive/<party>/received/<tracking>/ via the workflow's
.zddc.received_path. Writes get rewritten to
<workflow>/<base>+C<n><suffix> so review comments land in the
workflow folder and never touch the WORM archive.
- Cascade defaults declare on_plan_review per project so the
reviewing_root and staging_root are configurable.
LAYER 2 — browse context-menu workflows
- Accept Transmittal: right-click a transmittal folder in
archive/<party>/incoming/ → validates ZDDC folder + filename
conformance, atomic-renames the folder to
archive/<party>/received/<tracking>/ (WORM zone), and optionally
chains into Plan Review in the same composite request. Re-acceptance
with a different revision merges file-by-file; WORM forbids
overwrite of an existing filename.
- Stage / Unstage: right-click files in working/<…>/ → "Stage to…"
with picker of existing staging transmittal folders + inline
"New transmittal folder…" create; right-click files in
staging/<…>/ → "Unstage to working/" defaulting to the user's
working/<email>/ home. Reuses the file-API move primitive.
- Create Transmittal folder: right-click the staging/ pane → prompts
for a ZDDC-conforming folder name with live validation; mkdir,
then navigate to the new folder URL where the transmittal tool
serves the editor.
- Supporting infrastructure: new CanonicalFolderAt cascade lookup +
X-ZDDC-Canonical-Folder response header so the browse SPA can
scope-gate menu items without re-implementing the cascade
client-side.
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>
The last hardcoded client-side knowledge of the canonical convention
was the upload-zone regex in browse:
var UPLOAD_SCOPES = /\/(working|staging|incoming)(\/|$)/i;
Now declared in the cascade:
Schema:
drop_target: true|false leaf-only; describes THIS dir
(not propagated to descendants)
Lookup:
zddc.DropTargetAt(root, dir) bool
Surfaced to clients:
Directory listings carry an X-ZDDC-Drop-Target: true response
header when the cascade declares this leaf as an upload zone.
No header = no drop target.
Defaults populated:
working / working/* / staging / archive/<party>/incoming
all carry drop_target: true. Operators can extend (e.g. drop
files on archive/<party>/received via override) or disable
(e.g. drop_target: false at a specific staging subtree) without
touching code.
Browse migration:
loader.fetchServerChildren reads the response header and stamps
state.scopeDropTarget on every listing fetch. upload.js's
currentScopeAllows now reads that flag instead of regex-
matching the URL. Initial value is false in init.js so a
listing failure (offline / server doesn't emit the header)
safely defaults to "no drop zone".
Phase 4a closes the most visible asymmetry between server-side and
client-side cascade knowledge. The remaining client hardcodes
(browse grid-mode regex, archive source heuristics, shared/nav
stage strip) follow the same pattern when needed — Phase 4b/c/d.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop files anywhere on a browse page; if the current scope is inside
a working/, staging/, or incoming/ subtree the files are PUT to the
current directory via the existing file API. Per-file ACL is enforced
server-side (authorizeAction); a 403 surfaces as a per-file error
toast and the rest of the batch proceeds.
UX:
- dragenter → semi-transparent overlay with a dashed-border panel
showing the destination path. Hides immediately on dragleave or
drop.
- drop → "Uploading N files…" toast, then per-file failure toasts
inline, then a summary toast (success / partial / all-failed).
- listing auto-refreshes after the batch so new files appear in
the tree without a manual reload.
Scope:
- upload-eligible paths are matched by /\/(working|staging|incoming)
(\/|$)/i — same convention as the new grid-mode URL token.
- 256 MiB per-file cap (UPLOAD_MAX_BYTES) since browse's single-
body PUT loads the file as a Blob in the tab; larger uploads
should use a dedicated client.
- Outside the upload-eligible set the overlay never appears; drops
are silently ignored (drag effect = none).
Sequential uploads keep progress predictable; parallel batching can
land later if needed. The module hooks document-level dragenter/leave
/over/drop so it works regardless of which pane the user drags over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>