Round-Trip
The editor's Download panel config JSON button (packages/) is the only export path in the current stage-1 app — there is no in-app Open File/import UI yet. "Round-trip" here means: the downloaded JSON, fed back through parsePanelConfig(), reproduces the on-screen document. That guarantee is what makes the file trustworthy as an order artifact and as an interchange format for future tooling.
The download step
downloadPanelConfig(doc) calls serializePanelConfig(doc), pretty-prints it (JSON.stringify(config, null, 2)), and triggers a browser download named zpd-panel-<hp>hp.json. The pure serialization step — panelConfigJson(doc) — is factored out separately from the DOM/Blob mechanics so the exact JSON string is unit-testable without a real download.
What is preserved exactly
packages/ and packages/ both assert full round-trip fidelity for a document covering all 5 layer types:
doc → serializePanelConfig → JSON.stringify → JSON.parse → parsePanelConfig → docparsePanelConfig(JSON.parse(JSON.stringify(serializePanelConfig(doc)))) deep-equals the original doc for a fixture exercising:
every layer type (
shape,pattern,path,text,image)a hidden layer (
hidden: true)a
pathlayer with bezier handles (hin/hout) on some points but not othersa
pathlayer'sextraSubpaths(multiple closed subpaths from image tracing)rotationpresent on some layers and absent on others
All of the above survives the round trip unchanged.
What is intentionally NOT preserved
Unknown/extra fields — anything not in the
PanelConfigshape (stray top-level keys, extra per-layer properties) is dropped on import. See Defensive Parsing.panel.widthMm/panel.heightMm— always recomputed fromhpon the way back in, even if the file was hand-edited to disagree. These are advisory output, not authoritative input. See PanelConfig Format.Invalid values — an out-of-range
color, a non-finite coordinate, a malformedhp, etc. get clamped or defaulted rather than reproduced verbatim (by definition — the input was invalid). See Defensive Parsing.patternTypevalidity — an unrecognizedpatternTypestring round-trips as opaque data (it is preserved verbatim), but nothing about the pattern's rendering is guaranteed — core has no dependency on the patterns registry, so only the app/patterns layer knows whether a givenpatternTyperenders to anything.
Verified at the browser level too
The Playwright @smoke suite exercises the real download in a browser, not just the pure functions: it clicks the download button, reads the produced file off disk, and asserts parsePanelConfig(downloaded) equals parsePanelConfig(onScreen), where onScreen comes from the live document via the window.__zpdTest test bridge. See Testing for how that suite is structured.