assets.gf.cxshelf

dropzone

Drag-and-drop file capture, pure client-side.

primitive · dropzone · v0.1.0

Drop-zone primitive for gf.cx portfolio surfaces. No server, no upload pipeline — files are base64'd into localStorage and exported as JSON for a companion CLI helper to persist to disk. Depth-1 by design: lift to R2 direct-upload or GitHub-API commit when MVP outgrows the ~5 MB per-file localStorage cap.

Embed

<link rel="stylesheet" href="https://assets.gf.cx/dropzone/dropzone.css">
<script src="https://assets.gf.cx/dropzone/dropzone.js" defer></script>

<div data-dropzone
     data-field="40-payouts"
     data-context="claim-046414618"
     data-accept="image/*,application/pdf"
     data-multiple="true"
     data-max-mb="5"></div>

Live demo

Drop a file (image or PDF up to 5 MB) onto the zone below. It persists in your browser's localStorage under dropzone:demo:specimen — clear it by reloading or via the trash button rendered after a successful drop.

↓ a working dropzone, isolated for inspection:

Attributes

AttributeDefaultPurpose
data-fielduntitledForm-field name · written into export payload + becomes localStorage key suffix
data-contextdefaultNamespace (claim ID, contractor slug, etc.) so two pages can have a replacement_receipt field without colliding
data-accept(any)Comma-separated MIME patterns: image/*,application/pdf,.csv
data-multiplefalseAccept multiple files
data-max-mb5Per-file size cap; over → reject

Storage shape

Each dropzone writes to localStorage["dropzone:<context>:<field>"] as a JSON array of file records:

[
  {
    "name": "receipt-2026-05-21.pdf",
    "type": "application/pdf",
    "size": 1240892,
    "dataUrl": "data:application/pdf;base64,JVBERi0xLjQK...",
    "ts": "2026-05-21T14:30:00.000Z"
  }
]

Export payload

The 📋 Copy JSON for sync button (visible once at least one file has been captured) writes this object to the clipboard:

{
  "schema": "dropzone-export.v1",
  "context": "claim-046414618",
  "field": "40-payouts",
  "captured_at": "2026-05-21T14:30:00.000Z",
  "files": [
    {
      "name": "...",
      "type": "...",
      "size": 1234,
      "dataUrl": "data:...;base64,..."
    }
  ]
}

The companion CLI helper at ~/bin/pa_dropzone_apply.py reads this JSON (clipboard or stdin) and writes files to canonical paths.

Events

Each dropzone fires a bubbling dropzone:change event when files are added/removed. The detail payload contains field, context, and files (sans the heavy dataUrl). Host pages can listen to react to drops:

document.addEventListener('dropzone:change', (e) => {
  const { field, context, files } = e.detail;
  console.log(`${context}/${field}: ${files.length} files`);
});

Styling tokens

The CSS inherits via custom properties. Host pages can override:

TokenRole
--accentPrimary border + active state
--accent-alertReject / over-size border
--line-strongIdle border
--ink-softHelper-text colour

Depth

This is Depth 1 — pure client-side, base64 data URLs persisted in localStorage. The practical per-file cap is ~5 MB (image-heavy claims fit fine; high-res scans of larger documents do not).

Upgrade paths when MVP outgrows the cap:

Both sketched in devreports-content/parked_sketch_drag_drop_file_field_pattern_2026-05-21.md.

Cross-references

dropzone · v0.1.0 · first shipped 2026-05-21 · specimen page 2026-05-30