Support exporting a board to multiple formats (PNG, PDF, JSON) #203

Open
opened 2026-05-19 10:12:23 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

There is currently no way to export a board. Users should be able to export the current board to common formats so work can be shared, archived, or used outside the app.

Scope

Add an Export action (toolbar/menu in the board view) offering at least:

  • PNG image — raster export of the board. Should export the whole board content (all objects' bounding box) at a reasonable resolution, with a transparent or white background option, not just the current viewport.
  • PDF — the board rendered to a single-page PDF sized to the content.
  • JSON — the board's structured data (workspace/board/objects/connectors) as a downloadable .json file, suitable for backup and for a future import feature.

All exports are client-driven where possible (the canvas is Konva-based, so PNG can be produced from the stage; PDF can wrap that image). JSON export should reflect the same data the server returns for the board so it round-trips cleanly later.

Requirements

  • An Export control is reachable from the board UI (e.g. a button or menu entry) with a small format chooser (PNG / PDF / JSON).
  • PNG/PDF capture the full board content extents (compute the bounding box of all objects, including frames/connectors), not only what is on screen, and temporarily restore the original stage transform afterward so the user's view is unchanged.
  • Exported files download with a sensible filename including the board name/id (sanitized) and the format extension.
  • JSON export contains enough to reconstruct the board (board metadata + objects + connectors), using the existing SDK/RPC data shapes.
  • No new network listeners or server ports; if any server endpoint is needed it must follow the existing Unix-socket JSON-RPC pattern. Prefer a fully client-side implementation for PNG/PDF.
  • Export must work in the normal board view and not break the read-only/presentation views.
  • Large boards must not freeze the UI indefinitely; provide basic progress/disabled-button feedback during export.

Acceptance Criteria

  • Exporting a board with several objects to PNG produces an image containing all objects with correct relative positions.
  • Exporting to PDF produces a valid PDF whose page shows the same content as the PNG.
  • Exporting to JSON produces a file that contains the board's objects and connectors and can be re-read programmatically.
  • The user's current pan/zoom is unchanged after an export.
  • Filenames are sanitized and include the board identifier and correct extension.
  • Exports function in the standard board view; presentation/read-only views are unaffected.

Notes

The whiteboard frontend is vanilla JS modules under crates/hero_whiteboard_admin/static/web/js/whiteboard/ with a Konva stage set up in canvas.js. Any third-party library for PDF generation should be vendored/served as a static asset consistent with how existing frontend libraries are delivered (no external CDN at runtime). Implementation should reuse existing object/connector data access already present in the frontend rather than introducing a parallel data model.

## Summary There is currently no way to export a board. Users should be able to export the current board to common formats so work can be shared, archived, or used outside the app. ## Scope Add an Export action (toolbar/menu in the board view) offering at least: - **PNG image** — raster export of the board. Should export the whole board content (all objects' bounding box) at a reasonable resolution, with a transparent or white background option, not just the current viewport. - **PDF** — the board rendered to a single-page PDF sized to the content. - **JSON** — the board's structured data (workspace/board/objects/connectors) as a downloadable `.json` file, suitable for backup and for a future import feature. All exports are client-driven where possible (the canvas is Konva-based, so PNG can be produced from the stage; PDF can wrap that image). JSON export should reflect the same data the server returns for the board so it round-trips cleanly later. ## Requirements - An Export control is reachable from the board UI (e.g. a button or menu entry) with a small format chooser (PNG / PDF / JSON). - PNG/PDF capture the full board content extents (compute the bounding box of all objects, including frames/connectors), not only what is on screen, and temporarily restore the original stage transform afterward so the user's view is unchanged. - Exported files download with a sensible filename including the board name/id (sanitized) and the format extension. - JSON export contains enough to reconstruct the board (board metadata + objects + connectors), using the existing SDK/RPC data shapes. - No new network listeners or server ports; if any server endpoint is needed it must follow the existing Unix-socket JSON-RPC pattern. Prefer a fully client-side implementation for PNG/PDF. - Export must work in the normal board view and not break the read-only/presentation views. - Large boards must not freeze the UI indefinitely; provide basic progress/disabled-button feedback during export. ## Acceptance Criteria - Exporting a board with several objects to PNG produces an image containing all objects with correct relative positions. - Exporting to PDF produces a valid PDF whose page shows the same content as the PNG. - Exporting to JSON produces a file that contains the board's objects and connectors and can be re-read programmatically. - The user's current pan/zoom is unchanged after an export. - Filenames are sanitized and include the board identifier and correct extension. - Exports function in the standard board view; presentation/read-only views are unaffected. ## Notes The whiteboard frontend is vanilla JS modules under `crates/hero_whiteboard_admin/static/web/js/whiteboard/` with a Konva stage set up in `canvas.js`. Any third-party library for PDF generation should be vendored/served as a static asset consistent with how existing frontend libraries are delivered (no external CDN at runtime). Implementation should reuse existing object/connector data access already present in the frontend rather than introducing a parallel data model.
Author
Member

Implementation Spec for Issue #203

Objective

Add an in-board Export control that lets a user export the current board to PNG, PDF, or JSON. PNG/PDF capture the full content extents (bounding box of all objects + connectors, not just the viewport) at reasonable resolution, restore the user's original view afterward, and are produced entirely client-side. JSON serializes the same board metadata + objects + connectors the server returns, suitable for a future import. No new network listeners or server endpoints are added.

Requirements

  • Export control reachable from the normal board UI with a small format chooser (PNG / PDF / JSON).
  • PNG: raster of the whole board content bounding box (including frames/connectors), white background, reasonable resolution.
  • PDF: single page sized to content, wrapping the PNG; no vendored third-party library or CDN.
  • JSON: structured board data (board metadata + objects + connectors) mirroring server data so it can round-trip into a future import.
  • PNG/PDF must temporarily reset and then restore the stage transform (scale + position) so the user's view is unchanged.
  • Downloads use a sanitized filename including board name/id + correct extension.
  • Fully client-side for PNG/PDF; no server endpoints.
  • Must work in the normal board view and not break the read-only/presentation view.
  • Large boards: show basic progress / disable the control during export; never freeze indefinitely.

Files to Modify/Create

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/export.js — replace the existing single exportPng stub with full PNG/PDF/JSON logic (full-content capture, transform save/restore, filename sanitization, embedded minimal PDF writer).
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js — add getBoardId/getBoardName, replace exportPng with exportBoard(format) delegating to WhiteboardExport, keep exportPng alias.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/canvas.js — add a minimal setExportMode(bool) accessor (hides grid/cursor/ui layers during capture).
  • crates/hero_whiteboard_admin/templates/web/board.html — replace the single "Export PNG" navbar button with an Export button + a small PNG/PDF/JSON menu. No script-tag change (export.js already loaded before app.js).
  • No new vendored library; PDF is hand-rolled inline.

Implementation Plan

Step 1: Add export data accessors in app.js

Files: app.js
Dependencies: none

  • Add getBoardId() returning the module-private boardId.
  • Add getBoardName() reading #board-name textContent (present in board.html and board_view.html).
  • Replace exportPng to delegate; add exportBoard(fmt) -> WhiteboardExport.exportBoard(fmt); keep exportPng alias. Export all from WhiteboardApp.

Step 2: Add canvas export-mode accessor

Files: canvas.js
Dependencies: none (parallelizable with Step 1)

  • Add setExportMode(on) toggling gridLayer/cursorLayer/uiLayer visibility; expose in WhiteboardCanvas return object (mirrors existing layer getters).

Step 3: Full-content PNG capture with transform save/restore

Files: export.js
Dependencies: Steps 1, 2

  • computeContentRect(): after neutralizing stage transform, union objectLayer.getClientRect() and connectorLayer.getClientRect(), add ~24px padding; empty board -> toast "Nothing to export" and abort.
  • withFullContentCapture(fn): save {x,y,scale}; set scale {1,1} + position {0,0}; WhiteboardCanvas.setExportMode(true); run fn; in finally restore scale/position, call WhiteboardCanvas.syncScaleFromStage(), WhiteboardCanvas.drawGrid(), setExportMode(false).
  • buildPng(): adaptive pixelRatio (2 normally; 1 when area > ~4e6 px²; reduce further so each dimension*pr ≤ 8000); stage.toDataURL({x,y,width,height,pixelRatio,mimeType:'image/png'}); composite onto a white-filled offscreen canvas; return {dataURL,width,height}.

Step 4: JSON export reusing server data shape

Files: export.js
Dependencies: Step 1

  • buildJson(): board = {id: getBoardId(), name: getBoardName()}; objects via WhiteboardSync.serializeForServer(entry.group) over WhiteboardObjects.getAllObjects() (exact server RPC payload shape); connectors from WhiteboardConnectors.getConnectors() mapped to {id, from_id, to_id, line_style, stroke, stroke_width}; wrap {board, objects, connectors, exported_at, version:1}; download as application/json Blob.

Step 5: Minimal hand-rolled single-image PDF writer

Files: export.js
Dependencies: Step 3

  • Re-encode the captured canvas as JPEG (toDataURL('image/jpeg',0.92)), embed with /DCTDecode (no zlib needed). Build a 5-object one-page PDF (Catalog, Pages, Page with MediaBox = image px at 1px=1pt, Image XObject /DeviceRGB, content stream q W 0 0 H 0 0 cm /Im0 Do Q) with a byte-accurate xref; assemble as Uint8Array; download as application/pdf. Justification: a one-page single-image PDF is ~40 lines and avoids vendoring jsPDF/pdf-lib (~350KB) — honors the no-CDN/vendored constraint with zero added asset weight.

Step 6: Filename sanitization, download helper, progress/disable

Files: export.js
Dependencies: Steps 3, 4, 5

  • safeName(ext): board name (fallback board) + -id, lowercased, [^a-z0-9._-]+-, collapse/trim, cap ~80, add extension.
  • Shared download(blobOrUrl, filename) anchor helper (revokeObjectURL after click).
  • exportBoard(fmt): re-entrancy flag _busy; disable #export-btn + spinner; WhiteboardApp.showToast for "Exporting…"/done/error; defer heavy raster via rAF+setTimeout so disabled state paints; try/finally always re-enables.

Step 7: Wire the Export UI into board chrome

Files: board.html
Dependencies: Step 6

  • Replace the single Export PNG button (~line 136) with a btn btn-sm trigger #export-btn (bi bi-download) + a small menu: PNG/PDF/JSON calling WhiteboardApp.exportBoard('png'|'pdf'|'json'), matching adjacent navbar button/menu patterns. The control lives in .wb-navbar, which the existing body.wb-presenting CSS rule already hides during presentation — no extra CSS. Not added to read-only board_view.html.

Acceptance Criteria

  • Export control visible in the normal board navbar with PNG/PDF/JSON chooser.
  • PNG contains full board content by bounding box (not just viewport), white background, pixelRatio 2 (auto-reduced for huge boards).
  • PDF is a single content-sized page showing the same raster, opens in standard viewers, adds no vendored library.
  • JSON downloads with board metadata, objects via serializeForServer, and connectors, matching server data shape.
  • Stage scale/position exactly restored after export, even on mid-export error.
  • Filenames sanitized, include board name + id + correct extension.
  • Export control disabled with progress feedback during export; always re-enabled (finally).
  • No new ports/listeners/server endpoints; PNG/PDF/JSON fully client-side.
  • Read-only/presentation views not broken; control hidden during presentation via existing wb-presenting rule.
  • Empty board shows a "nothing to export" message instead of a broken/zero-size file.

Notes

  • Deploy caveat (test/deploy phase, not implementing agents): only JS/HTML under static/web/ + templates/web/ change; these are embedded by rust_embed in src/assets.rs, so cargo build --release -p hero_whiteboard_admin must be re-run (no edit to assets.rs needed since no files added/renamed). Verify via the admin Unix socket.
  • JSON export must reuse WhiteboardSync.serializeForServer + WhiteboardConnectors.getConnectors() — no parallel serializer.
  • Transform restore must be in finally, including syncScaleFromStage() + drawGrid() so the zoom indicator and grid match the restored view.
  • Export control intentionally only in board.html .wb-navbar; presentation auto-hides it via existing CSS.
## Implementation Spec for Issue #203 ### Objective Add an in-board Export control that lets a user export the current board to PNG, PDF, or JSON. PNG/PDF capture the full content extents (bounding box of all objects + connectors, not just the viewport) at reasonable resolution, restore the user's original view afterward, and are produced entirely client-side. JSON serializes the same board metadata + objects + connectors the server returns, suitable for a future import. No new network listeners or server endpoints are added. ### Requirements - Export control reachable from the normal board UI with a small format chooser (PNG / PDF / JSON). - PNG: raster of the whole board content bounding box (including frames/connectors), white background, reasonable resolution. - PDF: single page sized to content, wrapping the PNG; no vendored third-party library or CDN. - JSON: structured board data (board metadata + objects + connectors) mirroring server data so it can round-trip into a future import. - PNG/PDF must temporarily reset and then restore the stage transform (scale + position) so the user's view is unchanged. - Downloads use a sanitized filename including board name/id + correct extension. - Fully client-side for PNG/PDF; no server endpoints. - Must work in the normal board view and not break the read-only/presentation view. - Large boards: show basic progress / disable the control during export; never freeze indefinitely. ### Files to Modify/Create - `crates/hero_whiteboard_admin/static/web/js/whiteboard/export.js` — replace the existing single `exportPng` stub with full PNG/PDF/JSON logic (full-content capture, transform save/restore, filename sanitization, embedded minimal PDF writer). - `crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js` — add `getBoardId`/`getBoardName`, replace `exportPng` with `exportBoard(format)` delegating to `WhiteboardExport`, keep `exportPng` alias. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/canvas.js` — add a minimal `setExportMode(bool)` accessor (hides grid/cursor/ui layers during capture). - `crates/hero_whiteboard_admin/templates/web/board.html` — replace the single "Export PNG" navbar button with an Export button + a small PNG/PDF/JSON menu. No script-tag change (export.js already loaded before app.js). - No new vendored library; PDF is hand-rolled inline. ### Implementation Plan #### Step 1: Add export data accessors in app.js Files: `app.js` Dependencies: none - Add `getBoardId()` returning the module-private `boardId`. - Add `getBoardName()` reading `#board-name` textContent (present in board.html and board_view.html). - Replace `exportPng` to delegate; add `exportBoard(fmt)` -> `WhiteboardExport.exportBoard(fmt)`; keep `exportPng` alias. Export all from `WhiteboardApp`. #### Step 2: Add canvas export-mode accessor Files: `canvas.js` Dependencies: none (parallelizable with Step 1) - Add `setExportMode(on)` toggling `gridLayer/cursorLayer/uiLayer` visibility; expose in `WhiteboardCanvas` return object (mirrors existing layer getters). #### Step 3: Full-content PNG capture with transform save/restore Files: `export.js` Dependencies: Steps 1, 2 - `computeContentRect()`: after neutralizing stage transform, union `objectLayer.getClientRect()` and `connectorLayer.getClientRect()`, add ~24px padding; empty board -> toast "Nothing to export" and abort. - `withFullContentCapture(fn)`: save `{x,y,scale}`; set scale `{1,1}` + position `{0,0}`; `WhiteboardCanvas.setExportMode(true)`; run `fn`; in `finally` restore scale/position, call `WhiteboardCanvas.syncScaleFromStage()`, `WhiteboardCanvas.drawGrid()`, `setExportMode(false)`. - `buildPng()`: adaptive `pixelRatio` (2 normally; 1 when area > ~4e6 px²; reduce further so each dimension*pr ≤ 8000); `stage.toDataURL({x,y,width,height,pixelRatio,mimeType:'image/png'})`; composite onto a white-filled offscreen canvas; return `{dataURL,width,height}`. #### Step 4: JSON export reusing server data shape Files: `export.js` Dependencies: Step 1 - `buildJson()`: `board` = `{id: getBoardId(), name: getBoardName()}`; `objects` via `WhiteboardSync.serializeForServer(entry.group)` over `WhiteboardObjects.getAllObjects()` (exact server RPC payload shape); `connectors` from `WhiteboardConnectors.getConnectors()` mapped to `{id, from_id, to_id, line_style, stroke, stroke_width}`; wrap `{board, objects, connectors, exported_at, version:1}`; download as `application/json` Blob. #### Step 5: Minimal hand-rolled single-image PDF writer Files: `export.js` Dependencies: Step 3 - Re-encode the captured canvas as JPEG (`toDataURL('image/jpeg',0.92)`), embed with `/DCTDecode` (no zlib needed). Build a 5-object one-page PDF (Catalog, Pages, Page with MediaBox = image px at 1px=1pt, Image XObject `/DeviceRGB`, content stream `q W 0 0 H 0 0 cm /Im0 Do Q`) with a byte-accurate xref; assemble as `Uint8Array`; download as `application/pdf`. Justification: a one-page single-image PDF is ~40 lines and avoids vendoring jsPDF/pdf-lib (~350KB) — honors the no-CDN/vendored constraint with zero added asset weight. #### Step 6: Filename sanitization, download helper, progress/disable Files: `export.js` Dependencies: Steps 3, 4, 5 - `safeName(ext)`: board name (fallback `board`) + `-id`, lowercased, `[^a-z0-9._-]+`→`-`, collapse/trim, cap ~80, add extension. - Shared `download(blobOrUrl, filename)` anchor helper (revokeObjectURL after click). - `exportBoard(fmt)`: re-entrancy flag `_busy`; disable `#export-btn` + spinner; `WhiteboardApp.showToast` for "Exporting…"/done/error; defer heavy raster via rAF+setTimeout so disabled state paints; `try/finally` always re-enables. #### Step 7: Wire the Export UI into board chrome Files: `board.html` Dependencies: Step 6 - Replace the single Export PNG button (~line 136) with a `btn btn-sm` trigger `#export-btn` (`bi bi-download`) + a small menu: PNG/PDF/JSON calling `WhiteboardApp.exportBoard('png'|'pdf'|'json')`, matching adjacent navbar button/menu patterns. The control lives in `.wb-navbar`, which the existing `body.wb-presenting` CSS rule already hides during presentation — no extra CSS. Not added to read-only `board_view.html`. ### Acceptance Criteria - [ ] Export control visible in the normal board navbar with PNG/PDF/JSON chooser. - [ ] PNG contains full board content by bounding box (not just viewport), white background, pixelRatio 2 (auto-reduced for huge boards). - [ ] PDF is a single content-sized page showing the same raster, opens in standard viewers, adds no vendored library. - [ ] JSON downloads with `board` metadata, `objects` via `serializeForServer`, and `connectors`, matching server data shape. - [ ] Stage scale/position exactly restored after export, even on mid-export error. - [ ] Filenames sanitized, include board name + id + correct extension. - [ ] Export control disabled with progress feedback during export; always re-enabled (finally). - [ ] No new ports/listeners/server endpoints; PNG/PDF/JSON fully client-side. - [ ] Read-only/presentation views not broken; control hidden during presentation via existing `wb-presenting` rule. - [ ] Empty board shows a "nothing to export" message instead of a broken/zero-size file. ### Notes - Deploy caveat (test/deploy phase, not implementing agents): only JS/HTML under `static/web/` + `templates/web/` change; these are embedded by `rust_embed` in `src/assets.rs`, so `cargo build --release -p hero_whiteboard_admin` must be re-run (no edit to assets.rs needed since no files added/renamed). Verify via the admin Unix socket. - JSON export must reuse `WhiteboardSync.serializeForServer` + `WhiteboardConnectors.getConnectors()` — no parallel serializer. - Transform restore must be in `finally`, including `syncScaleFromStage()` + `drawGrid()` so the zoom indicator and grid match the restored view. - Export control intentionally only in `board.html` `.wb-navbar`; presentation auto-hides it via existing CSS.
Author
Member

Test Results

  • Workspace lib tests (cargo test --workspace --lib): compiled cleanly, no failures (0 passed; 0 failed; 0 ignored across all workspace crates)
  • JS syntax check (node --check): app.js OK, canvas.js OK, export.js OK
  • board.html: HTML template (not unit-tested); change is markup only

Note: this feature is JS/HTML-only (no Rust source changed); the workspace lib tests are run as a regression guard.

## Test Results - Workspace lib tests (cargo test --workspace --lib): compiled cleanly, no failures (0 passed; 0 failed; 0 ignored across all workspace crates) - JS syntax check (node --check): app.js OK, canvas.js OK, export.js OK - board.html: HTML template (not unit-tested); change is markup only Note: this feature is JS/HTML-only (no Rust source changed); the workspace lib tests are run as a regression guard.
Author
Member

Implementation Summary

Board export to PNG, PDF, and JSON, fully client-side.

Changes

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/export.js — replaced the stub with the full export module: full-content bounding-box capture (computeContentRect), stage transform neutralize/restore in a finally (withFullContentCapture), white-composited PNG with adaptive pixelRatio (2, reduced for very large boards, dimension-capped at 8000px), a hand-rolled single-image PDF writer (JPEG via /DCTDecode, byte-accurate xref — no third-party library), JSON export reusing WhiteboardSync.serializeForServer + WhiteboardConnectors.getConnectors(), sanitized filenames (board name + id), re-entrancy guard, and progress/disable feedback.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js — added getBoardId(), getBoardName(), and exportBoard(fmt) delegating to the export module; kept exportPng() as a backward-compatible alias.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/canvas.js — added setExportMode(on) to hide the grid/cursor/UI layers during capture (object + connector layers stay visible).
  • crates/hero_whiteboard_admin/templates/web/board.html — replaced the single Export button with an Export control (#export-btn) and a PNG/PDF/JSON menu, matching the navbar's existing inline-style button pattern (no Bootstrap JS is loaded in this template). The control lives in .wb-navbar, which existing CSS already hides during presentation.

Behavior

  • PNG/PDF capture the whole board content extents (not just the viewport); the user's pan/zoom is restored afterward, even on error.
  • PDF is a single content-sized page wrapping the raster; no vendored library added.
  • JSON contains { board, objects, connectors, exported_at, version } using the server's data shapes for round-tripping into a future import.
  • Empty board shows a "Nothing to export" message instead of a broken file.
  • Export control is absent from the read-only board_view.html and auto-hidden during presentation; no new network ports/listeners or server endpoints.

Test results

  • cargo test --workspace --lib (matches CI): compiled cleanly, no failures (feature is JS/HTML-only; run as a regression guard).
  • node --check passed for app.js, canvas.js, export.js.
  • Rebuilt and redeployed the admin binary; served export.js, app.js, canvas.js, and the board page verified to contain the new code and the Export control.

Notes

  • PDF is intentionally hand-rolled (a ~5-object single-image PDF) to avoid vendoring a large library and to honor the no-CDN constraint.
  • The data model is reused (no parallel serializer), so JSON export stays consistent with the server contract.
## Implementation Summary Board export to PNG, PDF, and JSON, fully client-side. ### Changes - `crates/hero_whiteboard_admin/static/web/js/whiteboard/export.js` — replaced the stub with the full export module: full-content bounding-box capture (`computeContentRect`), stage transform neutralize/restore in a `finally` (`withFullContentCapture`), white-composited PNG with adaptive `pixelRatio` (2, reduced for very large boards, dimension-capped at 8000px), a hand-rolled single-image PDF writer (JPEG via `/DCTDecode`, byte-accurate xref — no third-party library), JSON export reusing `WhiteboardSync.serializeForServer` + `WhiteboardConnectors.getConnectors()`, sanitized filenames (board name + id), re-entrancy guard, and progress/disable feedback. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js` — added `getBoardId()`, `getBoardName()`, and `exportBoard(fmt)` delegating to the export module; kept `exportPng()` as a backward-compatible alias. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/canvas.js` — added `setExportMode(on)` to hide the grid/cursor/UI layers during capture (object + connector layers stay visible). - `crates/hero_whiteboard_admin/templates/web/board.html` — replaced the single Export button with an Export control (`#export-btn`) and a PNG/PDF/JSON menu, matching the navbar's existing inline-style button pattern (no Bootstrap JS is loaded in this template). The control lives in `.wb-navbar`, which existing CSS already hides during presentation. ### Behavior - PNG/PDF capture the whole board content extents (not just the viewport); the user's pan/zoom is restored afterward, even on error. - PDF is a single content-sized page wrapping the raster; no vendored library added. - JSON contains `{ board, objects, connectors, exported_at, version }` using the server's data shapes for round-tripping into a future import. - Empty board shows a "Nothing to export" message instead of a broken file. - Export control is absent from the read-only `board_view.html` and auto-hidden during presentation; no new network ports/listeners or server endpoints. ### Test results - `cargo test --workspace --lib` (matches CI): compiled cleanly, no failures (feature is JS/HTML-only; run as a regression guard). - `node --check` passed for `app.js`, `canvas.js`, `export.js`. - Rebuilt and redeployed the admin binary; served `export.js`, `app.js`, `canvas.js`, and the board page verified to contain the new code and the Export control. ### Notes - PDF is intentionally hand-rolled (a ~5-object single-image PDF) to avoid vendoring a large library and to honor the no-CDN constraint. - The data model is reused (no parallel serializer), so JSON export stays consistent with the server contract.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#203
No description provided.