Web frame preview ignores canvas layer ordering and always paints on top #207

Closed
opened 2026-05-20 12:24:42 +00:00 by AhmedHanafy725 · 3 comments

Summary

The web frame object renders as a DOM iframe wrapped in an absolutely-positioned overlay (crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js, the wrapper/iframe created around the overlay's position:absolute markup). Because HTML elements always paint above a canvas, the iframe sits visually on top of every Konva object regardless of z_index, bring-to-front / send-to-back actions, frame containment, or whatever other object the user expects to be in front. The web frame's own preview/screenshot card is also part of that DOM overlay, so it has the same problem before the iframe has loaded.

This is not a stacking bug in object.update; it's a fundamental DOM-over-canvas limitation. The web frame cannot honour the canvas's layer order while it is rendered as a live iframe overlay.

Reproduction

  1. Place a web frame on the board and let it load a page.
  2. Drop a sticky note (or any object) on top of the web frame area.
  3. Right-click the sticky → Bring to Front. The sticky stays visually behind the iframe.
  4. Right-click the web frame → Send to Back. The web frame still paints over the sticky and every other object.

Same effect when a web frame is inside (or behind) a frame: the iframe overlay still covers items the user expects to be in front of it.

Scope

Make the web frame respect canvas layer ordering. The recommended approach is to stop rendering a live iframe in-canvas by default and instead:

  • Default state: render the web frame as a normal Konva object — a card with a title, the URL, and (when available) a static preview image of the page. Z-order, frames, send-to-back/front, and selection all work normally because it is now part of the object layer.
  • Open state: clicking the card opens the page in an iframe overlay (modal / focused view) or in a new browser tab, depending on what fits the rest of the app. The overlay is shown explicitly by the user, exists only while open, and is closed via a button or Escape. While open, the overlay being above the canvas is correct and expected.
  • Preview image source: prefer something the client can render itself (favicon + page metadata via the existing pre-flight, or a captured thumbnail when available). Do not add server-side scraping or any new network endpoint to fetch external images.

This matches how other in-canvas embeddings handle the same constraint (Miro and similar tools render a preview card by default and open the live page on demand).

Requirements

  • A web frame on the canvas honours z_index, send-to-back / bring-to-front, and frame containment exactly like any other object — its preview is part of the Konva object layer.
  • The live iframe is shown only on explicit user action (e.g. double-click or an Open action on the card / selection toolbar); when shown it can sit above the canvas.
  • Closing the iframe returns to the static preview; the preview keeps its position, size, and z-order.
  • No new server endpoints or external network calls are introduced; preview metadata uses what is already pre-flighted today.
  • Existing web frames in saved boards continue to work; opening one for the first time after the change shows the preview card, not a live iframe.
  • Read-only and presentation views also use the preview card by default; opening the live iframe still works the same way.

Acceptance Criteria

  • Adding a web frame shows a Konva card with title / URL / placeholder (or thumbnail) on the canvas; no DOM iframe is visible by default.
  • Bringing other objects to front / sending the web frame to back changes the visible stacking on screen; the web frame no longer covers things that should be in front of it.
  • A web frame placed inside a frame stays visually inside that frame.
  • Double-clicking (or activating Open) on the preview opens the live page in an overlay or new tab and closing the overlay restores the preview without moving the object.
  • No new server endpoints or external image fetches are added.

Notes

The whiteboard frontend is vanilla JS modules under crates/hero_whiteboard_admin/static/web/js/whiteboard/, embedded via rust-embed. The current overlay machinery is in webframe.js (overlay creation, hide-on-drag, hide-on-pan/zoom logic). The replacement preview should reuse the existing objects.js factory pattern so undo/redo, selection toolbar, sync, and persistence keep working unchanged.

## Summary The web frame object renders as a DOM iframe wrapped in an absolutely-positioned overlay (`crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js`, the `wrapper`/`iframe` created around the overlay's `position:absolute` markup). Because HTML elements always paint above a canvas, the iframe sits visually on top of every Konva object regardless of `z_index`, bring-to-front / send-to-back actions, frame containment, or whatever other object the user expects to be in front. The web frame's own preview/screenshot card is also part of that DOM overlay, so it has the same problem before the iframe has loaded. This is not a stacking bug in `object.update`; it's a fundamental DOM-over-canvas limitation. The web frame cannot honour the canvas's layer order while it is rendered as a live iframe overlay. ## Reproduction 1. Place a web frame on the board and let it load a page. 2. Drop a sticky note (or any object) on top of the web frame area. 3. Right-click the sticky → Bring to Front. The sticky stays visually behind the iframe. 4. Right-click the web frame → Send to Back. The web frame still paints over the sticky and every other object. Same effect when a web frame is inside (or behind) a frame: the iframe overlay still covers items the user expects to be in front of it. ## Scope Make the web frame respect canvas layer ordering. The recommended approach is to stop rendering a live iframe in-canvas by default and instead: - Default state: render the web frame as a normal Konva object — a card with a title, the URL, and (when available) a static preview image of the page. Z-order, frames, send-to-back/front, and selection all work normally because it is now part of the object layer. - Open state: clicking the card opens the page in an iframe overlay (modal / focused view) or in a new browser tab, depending on what fits the rest of the app. The overlay is shown explicitly by the user, exists only while open, and is closed via a button or Escape. While open, the overlay being above the canvas is correct and expected. - Preview image source: prefer something the client can render itself (favicon + page metadata via the existing pre-flight, or a captured thumbnail when available). Do not add server-side scraping or any new network endpoint to fetch external images. This matches how other in-canvas embeddings handle the same constraint (Miro and similar tools render a preview card by default and open the live page on demand). ## Requirements - A web frame on the canvas honours `z_index`, send-to-back / bring-to-front, and frame containment exactly like any other object — its preview is part of the Konva object layer. - The live iframe is shown only on explicit user action (e.g. double-click or an Open action on the card / selection toolbar); when shown it can sit above the canvas. - Closing the iframe returns to the static preview; the preview keeps its position, size, and z-order. - No new server endpoints or external network calls are introduced; preview metadata uses what is already pre-flighted today. - Existing web frames in saved boards continue to work; opening one for the first time after the change shows the preview card, not a live iframe. - Read-only and presentation views also use the preview card by default; opening the live iframe still works the same way. ## Acceptance Criteria - Adding a web frame shows a Konva card with title / URL / placeholder (or thumbnail) on the canvas; no DOM iframe is visible by default. - Bringing other objects to front / sending the web frame to back changes the visible stacking on screen; the web frame no longer covers things that should be in front of it. - A web frame placed inside a frame stays visually inside that frame. - Double-clicking (or activating Open) on the preview opens the live page in an overlay or new tab and closing the overlay restores the preview without moving the object. - No new server endpoints or external image fetches are added. ## Notes The whiteboard frontend is vanilla JS modules under `crates/hero_whiteboard_admin/static/web/js/whiteboard/`, embedded via rust-embed. The current overlay machinery is in `webframe.js` (overlay creation, hide-on-drag, hide-on-pan/zoom logic). The replacement preview should reuse the existing `objects.js` factory pattern so undo/redo, selection toolbar, sync, and persistence keep working unchanged.
Author
Owner

Implementation Spec for Issue #207

Objective

Eliminate the on-canvas HTML <iframe> overlay used for web-frame previews so web-frame objects honor Konva layer ordering (z_index, bringToFront/sendToBack, frame clipping). Render every web-frame as a pure Konva card by default; expose the live <iframe> only inside a transient, centered modal that the user opens explicitly (double-click on the card or an Open button in the selection toolbar) and dismisses (X / Escape / backdrop click).

Requirements

  1. Web-frame objects paint inside the Konva object layer with no HTML overlay attached to canvas coordinates. Stacking, bringToFront/sendToBack, frame containment, and presentation mode apply uniformly.
  2. The default Konva card shows: rounded background, header strip with a globe icon and truncated URL, body with a Double-click to open hint. The server's existing /api/url-check (web_url_check in routes.rs:437) returns only { embeddable, reason? } — no favicon/title — so the card uses URL + icon only. No new server endpoint.
  3. The persisted server payload stays { url } inside data (sync.js:365-366) — no schema migration; pre-existing webframes load as the new card.
  4. Opening a web-frame:
    • Displays a single shared modal overlay (one at a time; opening a new one closes the previous).
    • Header: URL, "Open in new tab" link, close (X).
    • Body: the iframe; if /api/url-check says non-embeddable, the existing "site doesn't allow embedding" card is rendered into the body instead.
    • Closes on X, Escape, or backdrop click; closing clears iframe.src.
    • Auto-hides while body.wb-presenting is set; auto-closes on entering presentation.
  5. Read-only board_view.html renders the new card and allows opening the modal (read-only is cosmetic; opening is a view-only action).
  6. Selection, drag, resize, rotation, transformer behavior follow the same patterns as the document card factory (objects.js:1657) — no webframe-specific overlay reposition code remains.
  7. Stage wheel, stage dragstart/dragend, group dragstart/dragend overlay-reposition logic is deleted.
  8. Backwards-compatible public surface: keep createWebframe, updateUrl, applyNewUrl, submitUrlModal, cancelUrlModal; add openOverlay, closeOverlay. The dead overlay APIs (destroyOverlay, remapOverlay, refreshOverlay, setAllInteractive, hideOverlay, showOverlay, hideAllOverlays, showAllOverlays, refreshAllOverlays) become no-ops so the call sites in objects.js, sync.js, app.js, canvas.js, history.js, tools.js, frames.js keep working without per-file edits. (Latent fix: applyNewUrl is currently called by selection_toolbar.js:1727 but missing from the public return — added explicitly.)

Files to Modify/Create

  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js — rewrite factory to Konva-only; move iframe code into a transient shared modal.
  • Modify: crates/hero_whiteboard_admin/templates/web/board.html — add #wb-webframe-modal markup (near line 403).
  • Modify: crates/hero_whiteboard_admin/templates/web/board_view.html — mirror the same modal markup.
  • Modify: crates/hero_whiteboard_admin/static/web/css/whiteboard.css — add .wb-webframe-modal* styles using the existing --wb-* theme vars; include body.wb-presenting .wb-webframe-modal { display:none !important; }.
  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js_renderWebframe (line 1713) appends an Open icon button (_buildIconBtn 'bi bi-box-arrow-up-right') calling WhiteboardWebframe.openOverlay(node).
  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js — drop the webframe-specific show/hide loops (lines 268-269 and 384-407); on startPresentation call WhiteboardWebframe.closeOverlay().

No new JS file; no new server endpoint.

Implementation Plan

Step 1: Modal markup

Files: templates/web/board.html, templates/web/board_view.html

  • Insert <div id="wb-webframe-modal" class="wb-webframe-modal" role="dialog" aria-modal="true" style="display:none;"> containing __dialog/__header(url, newtab link, close X)/__body(iframe + hidden non-embeddable card slot).
    Dependencies: none (parallelizable with Step 2)

Step 2: Modal CSS

Files: static/web/css/whiteboard.css

  • .wb-webframe-modal (fixed inset:0, backdrop, z-index above chrome), __dialog (max-width:90vw, max-height:85vh), __header (flex, themed), __url (mono, truncate, --wb-text-muted), __close, __body (flex:1, position:relative). Add body.wb-presenting .wb-webframe-modal { display:none !important; }.
    Dependencies: none (parallelizable with Step 1)

Step 3: Rewrite webframe.js

Files: static/web/js/whiteboard/webframe.js

  • createWebframe: keep the Konva.Group with bg/header/label/placeholder; remove createIframeOverlay(id, ...) call and the stage dragstart.wf_* / dragend.wf_* / wheel.wf_* listeners and setTimeout(updateOverlayPosition, 50). Placeholder label reads Double-click to open\n<url>.
  • dblclick/dbltap: open the modal via openOverlay(group) (not promptUrl); URL editing remains in the selection toolbar popover.
  • Drag/end handlers: keep history snapshot + recomputeParentFrame; drop hideOverlay/showOverlay.
  • Delete overlays map and the helpers: createIframeOverlay, updateOverlayPosition, destroyOverlay, remapOverlay, refreshOverlay, setAllInteractive, hideOverlay, showOverlay, hideAllOverlays, showAllOverlays, refreshAllOverlays. Replace each with a no-op return-undefined function exported from the module.
  • New openOverlay(group): read _wfState.url (or group.findOne('.label').text()), set the modal's URL text + iframe src (after the existing _checkEmbeddable; non-embeddable → render the existing not-embeddable card markup into #wb-webframe-modal-card and hide the iframe). Wire Escape + backdrop + X. If overlay already open, call closeOverlay() first.
  • New closeOverlay(): hide the modal, clear iframe.src, detach listeners.
  • applyNewUrl(id, url) / updateUrl(id, url): drop the overlays[id] branches; update the .label and .placeholder Konva text.
  • Public return: { createWebframe, openOverlay, closeOverlay, applyNewUrl, updateUrl, submitUrlModal, cancelUrlModal, destroyOverlay: noop, remapOverlay: noop, refreshOverlay: noop, setAllInteractive: noop, hideOverlay: noop, showOverlay: noop, hideAllOverlays: noop, showAllOverlays: noop, refreshAllOverlays: noop }.
    Dependencies: Steps 1 and 2

Step 4: Selection-toolbar Open button

Files: static/web/js/whiteboard/selection_toolbar.js

  • In _renderWebframe (line 1713), append after the URL popover: propsEl.appendChild(_buildIconBtn('bi bi-box-arrow-up-right', 'Open web frame', function(){ if (WhiteboardWebframe && WhiteboardWebframe.openOverlay) WhiteboardWebframe.openOverlay(node); }));.
    Dependencies: Step 3

Step 5: Presentation mode close

Files: static/web/js/whiteboard/frames.js

  • Remove the per-id webframe show/hide loop (lines 384-407) and the show/hide calls at lines 268-269; on startPresentation call WhiteboardWebframe.closeOverlay() once.
    Dependencies: Step 3

Step 6: Build + verify

  • touch crates/hero_whiteboard_admin/src/assets.rs (CSS/template + JS changed), cargo build --release -p hero_whiteboard_admin. Verify the served assets show the new card markup and the modal HTML. Manual check: stack a sticky in front of a webframe; rubber-band select crosses; open via double-click and via toolbar; Escape closes; presentation mode closes the modal.

Parallelization: Steps 1, 2 in parallel. Step 3 after both. Steps 4, 5 in parallel after Step 3.

Acceptance Criteria

  • A web-frame renders as a Konva card; no <div id="wf-overlay-..."> exists in the DOM.
  • bringToFront / sendToBack on a web-frame stacks correctly relative to other objects.
  • Placing a web-frame inside a frame respects frame containment/clipping like other objects.
  • Double-clicking the card opens the shared modal.
  • Selection-toolbar Open icon opens the modal.
  • Modal closes on X, Escape, backdrop click; closing clears iframe.src so the page unloads.
  • Opening a second web-frame closes the first.
  • Saved webframes (data.url only) load as the card; no migration.
  • Entering presentation mode closes the modal; the modal cannot appear while body.wb-presenting is set.
  • Pan, zoom, drag, resize, rotate of a web-frame card no longer triggers HTML overlay reposition (no related listeners in the code).
  • Read-only board_view.html renders the card and opens correctly.
  • Non-embeddable URLs render the existing fallback card inside the modal body, with an "Open in new tab" link.
  • No console errors from the noop overlay APIs (destroyOverlay, refreshOverlay, etc.).

Notes

  • The issue body's hypothesis that pre-flight returns favicon + page title is wrong for this codebase; web_url_check returns { embeddable, reason? } only. Card stays URL + globe icon.
  • Persistence wire shape is unchanged ({ url } inside data); only client rendering changes.
  • Latent fix: applyNewUrl was missing from the public return today; the rewrite adds it back. selection_toolbar.js:1727 will resolve it correctly post-change.
  • The themed WhiteboardPromptModal (#205) and the existing #webframe-url-modal markup stay as-is; URL editing path is unchanged.
  • Embedding caveat (test/deploy): JS/CSS/HTML changes need cargo build --release -p hero_whiteboard_admin; no new file is added, so assets.rs strictly does NOT need touching (but touching is harmless).
## Implementation Spec for Issue #207 ### Objective Eliminate the on-canvas HTML `<iframe>` overlay used for web-frame previews so web-frame objects honor Konva layer ordering (`z_index`, `bringToFront`/`sendToBack`, frame clipping). Render every web-frame as a pure Konva card by default; expose the live `<iframe>` only inside a transient, centered modal that the user opens explicitly (double-click on the card or an Open button in the selection toolbar) and dismisses (X / Escape / backdrop click). ### Requirements 1. Web-frame objects paint inside the Konva object layer with no HTML overlay attached to canvas coordinates. Stacking, `bringToFront`/`sendToBack`, frame containment, and presentation mode apply uniformly. 2. The default Konva card shows: rounded background, header strip with a globe icon and truncated URL, body with a `Double-click to open` hint. The server's existing `/api/url-check` (`web_url_check` in `routes.rs:437`) returns only `{ embeddable, reason? }` — no favicon/title — so the card uses URL + icon only. No new server endpoint. 3. The persisted server payload stays `{ url }` inside `data` (`sync.js:365-366`) — no schema migration; pre-existing webframes load as the new card. 4. Opening a web-frame: - Displays a single shared modal overlay (one at a time; opening a new one closes the previous). - Header: URL, "Open in new tab" link, close (X). - Body: the iframe; if `/api/url-check` says non-embeddable, the existing "site doesn't allow embedding" card is rendered into the body instead. - Closes on X, Escape, or backdrop click; closing clears `iframe.src`. - Auto-hides while `body.wb-presenting` is set; auto-closes on entering presentation. 5. Read-only `board_view.html` renders the new card and allows opening the modal (read-only is cosmetic; opening is a view-only action). 6. Selection, drag, resize, rotation, transformer behavior follow the same patterns as the document card factory (`objects.js:1657`) — no webframe-specific overlay reposition code remains. 7. Stage `wheel`, stage `dragstart`/`dragend`, group `dragstart`/`dragend` overlay-reposition logic is deleted. 8. Backwards-compatible public surface: keep `createWebframe`, `updateUrl`, `applyNewUrl`, `submitUrlModal`, `cancelUrlModal`; add `openOverlay`, `closeOverlay`. The dead overlay APIs (`destroyOverlay`, `remapOverlay`, `refreshOverlay`, `setAllInteractive`, `hideOverlay`, `showOverlay`, `hideAllOverlays`, `showAllOverlays`, `refreshAllOverlays`) become no-ops so the call sites in `objects.js`, `sync.js`, `app.js`, `canvas.js`, `history.js`, `tools.js`, `frames.js` keep working without per-file edits. (Latent fix: `applyNewUrl` is currently called by `selection_toolbar.js:1727` but missing from the public return — added explicitly.) ### Files to Modify/Create - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js` — rewrite factory to Konva-only; move iframe code into a transient shared modal. - Modify: `crates/hero_whiteboard_admin/templates/web/board.html` — add `#wb-webframe-modal` markup (near line 403). - Modify: `crates/hero_whiteboard_admin/templates/web/board_view.html` — mirror the same modal markup. - Modify: `crates/hero_whiteboard_admin/static/web/css/whiteboard.css` — add `.wb-webframe-modal*` styles using the existing `--wb-*` theme vars; include `body.wb-presenting .wb-webframe-modal { display:none !important; }`. - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` — `_renderWebframe` (line 1713) appends an Open icon button (`_buildIconBtn 'bi bi-box-arrow-up-right'`) calling `WhiteboardWebframe.openOverlay(node)`. - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js` — drop the webframe-specific show/hide loops (lines 268-269 and 384-407); on `startPresentation` call `WhiteboardWebframe.closeOverlay()`. No new JS file; no new server endpoint. ### Implementation Plan #### Step 1: Modal markup Files: `templates/web/board.html`, `templates/web/board_view.html` - Insert `<div id="wb-webframe-modal" class="wb-webframe-modal" role="dialog" aria-modal="true" style="display:none;">` containing `__dialog/__header(url, newtab link, close X)/__body(iframe + hidden non-embeddable card slot)`. Dependencies: none (parallelizable with Step 2) #### Step 2: Modal CSS Files: `static/web/css/whiteboard.css` - `.wb-webframe-modal` (fixed inset:0, backdrop, z-index above chrome), `__dialog` (max-width:90vw, max-height:85vh), `__header` (flex, themed), `__url` (mono, truncate, `--wb-text-muted`), `__close`, `__body` (flex:1, position:relative). Add `body.wb-presenting .wb-webframe-modal { display:none !important; }`. Dependencies: none (parallelizable with Step 1) #### Step 3: Rewrite webframe.js Files: `static/web/js/whiteboard/webframe.js` - `createWebframe`: keep the `Konva.Group` with bg/header/label/placeholder; **remove** `createIframeOverlay(id, ...)` call and the stage `dragstart.wf_*` / `dragend.wf_*` / `wheel.wf_*` listeners and `setTimeout(updateOverlayPosition, 50)`. Placeholder label reads `Double-click to open\n<url>`. - `dblclick`/`dbltap`: open the modal via `openOverlay(group)` (not `promptUrl`); URL editing remains in the selection toolbar popover. - Drag/end handlers: keep history snapshot + `recomputeParentFrame`; drop `hideOverlay/showOverlay`. - Delete `overlays` map and the helpers: `createIframeOverlay`, `updateOverlayPosition`, `destroyOverlay`, `remapOverlay`, `refreshOverlay`, `setAllInteractive`, `hideOverlay`, `showOverlay`, `hideAllOverlays`, `showAllOverlays`, `refreshAllOverlays`. Replace each with a no-op return-`undefined` function exported from the module. - New `openOverlay(group)`: read `_wfState.url` (or `group.findOne('.label').text()`), set the modal's URL text + iframe `src` (after the existing `_checkEmbeddable`; non-embeddable → render the existing not-embeddable card markup into `#wb-webframe-modal-card` and hide the iframe). Wire Escape + backdrop + X. If overlay already open, call `closeOverlay()` first. - New `closeOverlay()`: hide the modal, clear `iframe.src`, detach listeners. - `applyNewUrl(id, url)` / `updateUrl(id, url)`: drop the `overlays[id]` branches; update the `.label` and `.placeholder` Konva text. - Public return: `{ createWebframe, openOverlay, closeOverlay, applyNewUrl, updateUrl, submitUrlModal, cancelUrlModal, destroyOverlay: noop, remapOverlay: noop, refreshOverlay: noop, setAllInteractive: noop, hideOverlay: noop, showOverlay: noop, hideAllOverlays: noop, showAllOverlays: noop, refreshAllOverlays: noop }`. Dependencies: Steps 1 and 2 #### Step 4: Selection-toolbar Open button Files: `static/web/js/whiteboard/selection_toolbar.js` - In `_renderWebframe` (line 1713), append after the URL popover: `propsEl.appendChild(_buildIconBtn('bi bi-box-arrow-up-right', 'Open web frame', function(){ if (WhiteboardWebframe && WhiteboardWebframe.openOverlay) WhiteboardWebframe.openOverlay(node); }));`. Dependencies: Step 3 #### Step 5: Presentation mode close Files: `static/web/js/whiteboard/frames.js` - Remove the per-id webframe show/hide loop (lines 384-407) and the show/hide calls at lines 268-269; on `startPresentation` call `WhiteboardWebframe.closeOverlay()` once. Dependencies: Step 3 #### Step 6: Build + verify - `touch crates/hero_whiteboard_admin/src/assets.rs` (CSS/template + JS changed), `cargo build --release -p hero_whiteboard_admin`. Verify the served assets show the new card markup and the modal HTML. Manual check: stack a sticky in front of a webframe; rubber-band select crosses; open via double-click and via toolbar; Escape closes; presentation mode closes the modal. Parallelization: Steps 1, 2 in parallel. Step 3 after both. Steps 4, 5 in parallel after Step 3. ### Acceptance Criteria - [ ] A web-frame renders as a Konva card; no `<div id="wf-overlay-...">` exists in the DOM. - [ ] `bringToFront` / `sendToBack` on a web-frame stacks correctly relative to other objects. - [ ] Placing a web-frame inside a frame respects frame containment/clipping like other objects. - [ ] Double-clicking the card opens the shared modal. - [ ] Selection-toolbar Open icon opens the modal. - [ ] Modal closes on X, Escape, backdrop click; closing clears `iframe.src` so the page unloads. - [ ] Opening a second web-frame closes the first. - [ ] Saved webframes (`data.url` only) load as the card; no migration. - [ ] Entering presentation mode closes the modal; the modal cannot appear while `body.wb-presenting` is set. - [ ] Pan, zoom, drag, resize, rotate of a web-frame card no longer triggers HTML overlay reposition (no related listeners in the code). - [ ] Read-only `board_view.html` renders the card and opens correctly. - [ ] Non-embeddable URLs render the existing fallback card inside the modal body, with an "Open in new tab" link. - [ ] No console errors from the noop overlay APIs (`destroyOverlay`, `refreshOverlay`, etc.). ### Notes - The issue body's hypothesis that pre-flight returns favicon + page title is wrong for this codebase; `web_url_check` returns `{ embeddable, reason? }` only. Card stays URL + globe icon. - Persistence wire shape is unchanged (`{ url }` inside `data`); only client rendering changes. - Latent fix: `applyNewUrl` was missing from the public return today; the rewrite adds it back. `selection_toolbar.js:1727` will resolve it correctly post-change. - The themed `WhiteboardPromptModal` (#205) and the existing `#webframe-url-modal` markup stay as-is; URL editing path is unchanged. - Embedding caveat (test/deploy): JS/CSS/HTML changes need `cargo build --release -p hero_whiteboard_admin`; no new file is added, so `assets.rs` strictly does NOT need touching (but touching is harmless).
Author
Owner

Implementation Summary

Web frames no longer use an on-canvas DOM iframe overlay. They render as a normal Konva card and honour canvas layer ordering; the live iframe is shown in a transient shared modal opened on demand.

Changes

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js:
    • Rewritten to be Konva-only on the canvas (rounded bg, header strip with globe icon + truncated URL, placeholder body with Double-click to open).
    • Removed: overlays map, createIframeOverlay, updateOverlayPosition, stage dragstart.wf_* / dragend.wf_* / wheel.wf_* listeners, and all hide/show/refresh overlay helpers (along with their per-webframe HTML wrappers).
    • Added openOverlay(group) / closeOverlay() that drive a single shared modal (#wb-webframe-modal): pre-flights via the existing /api/url-check, shows the iframe when embeddable or renders the existing fallback card when not, closes on X / Escape / backdrop click and clears iframe.src on close.
    • applyNewUrl / updateUrl now also refresh the Konva placeholder text; iframe-overlay branches dropped.
    • Public surface keeps backwards-compatible no-op stubs for the removed APIs (destroyOverlay, remapOverlay, refreshOverlay, setAllInteractive, hideOverlay, showOverlay, hideAllOverlays, showAllOverlays, refreshAllOverlays) so existing call sites in objects.js, sync.js, app.js, canvas.js, history.js, tools.js, frames.js continue to work without per-file edits.
  • crates/hero_whiteboard_admin/templates/web/board.html and crates/hero_whiteboard_admin/templates/web/board_view.html: added the shared #wb-webframe-modal markup (header with URL, "open in new tab" link, close X; body with iframe + a fallback card slot).
  • crates/hero_whiteboard_admin/static/web/css/whiteboard.css: added .wb-webframe-modal* rules using the existing --wb-* theme variables; body.wb-presenting .wb-webframe-modal { display:none !important; } so the modal cannot appear during presentation.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js: _renderWebframe appends an Open icon button (bi-box-arrow-up-right) that calls WhiteboardWebframe.openOverlay(node).
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js: replaced the per-webframe overlay show/hide loops with a single WhiteboardWebframe.closeOverlay() on startPresentation (the overlay is now a transient modal and cannot leak into presentation).

Behavior

  • A web-frame on the canvas now stacks with z_index / bringToFront / sendToBack like any other object, and frame containment applies.
  • The persisted payload ({ url } inside data) is unchanged; existing boards load as the new card with no migration.
  • Double-click on the card opens the shared modal; the toolbar's Open button does the same. Opening a second web-frame closes the first.
  • Closing the modal clears iframe.src so the inner page is unloaded.
  • Presentation mode auto-closes the overlay and hides it via CSS for the duration of presentation.
  • Non-embeddable URLs render the existing fallback card inside the modal body with an "Open in new tab" link.

Test results

  • cargo test --workspace --lib: compiled cleanly, no failures (change is JS/HTML/CSS-only; regression guard).
  • node --check passed for webframe.js, selection_toolbar.js, frames.js. File encodings clean (UTF-8, zero control bytes).
  • Rebuilt and redeployed; served board page contains the modal markup, served webframe.js contains openOverlay/closeOverlay/no-op stubs, served CSS contains the new .wb-webframe-modal* rules.

Notes

  • The pre-flight /api/url-check (web_url_check) returns only { embeddable, reason? } — there is no favicon/title available without a new server endpoint, so the on-canvas card uses URL + globe icon only. Out of scope to add a thumbnailing endpoint here.
  • The themed URL-edit modal (#webframe-url-modal) and the selection-toolbar URL popover are unchanged.
  • A latent bug surfaced and fixed in passing: applyNewUrl was being called by the toolbar but was missing from the previous public return — it is now exported.
## Implementation Summary Web frames no longer use an on-canvas DOM iframe overlay. They render as a normal Konva card and honour canvas layer ordering; the live iframe is shown in a transient shared modal opened on demand. ### Changes - `crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js`: - Rewritten to be Konva-only on the canvas (rounded bg, header strip with globe icon + truncated URL, placeholder body with `Double-click to open`). - Removed: `overlays` map, `createIframeOverlay`, `updateOverlayPosition`, stage `dragstart.wf_*` / `dragend.wf_*` / `wheel.wf_*` listeners, and all hide/show/refresh overlay helpers (along with their per-webframe HTML wrappers). - Added `openOverlay(group)` / `closeOverlay()` that drive a single shared modal (`#wb-webframe-modal`): pre-flights via the existing `/api/url-check`, shows the iframe when embeddable or renders the existing fallback card when not, closes on X / Escape / backdrop click and clears `iframe.src` on close. - `applyNewUrl` / `updateUrl` now also refresh the Konva placeholder text; iframe-overlay branches dropped. - Public surface keeps backwards-compatible no-op stubs for the removed APIs (`destroyOverlay`, `remapOverlay`, `refreshOverlay`, `setAllInteractive`, `hideOverlay`, `showOverlay`, `hideAllOverlays`, `showAllOverlays`, `refreshAllOverlays`) so existing call sites in `objects.js`, `sync.js`, `app.js`, `canvas.js`, `history.js`, `tools.js`, `frames.js` continue to work without per-file edits. - `crates/hero_whiteboard_admin/templates/web/board.html` and `crates/hero_whiteboard_admin/templates/web/board_view.html`: added the shared `#wb-webframe-modal` markup (header with URL, "open in new tab" link, close X; body with iframe + a fallback card slot). - `crates/hero_whiteboard_admin/static/web/css/whiteboard.css`: added `.wb-webframe-modal*` rules using the existing `--wb-*` theme variables; `body.wb-presenting .wb-webframe-modal { display:none !important; }` so the modal cannot appear during presentation. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js`: `_renderWebframe` appends an Open icon button (`bi-box-arrow-up-right`) that calls `WhiteboardWebframe.openOverlay(node)`. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js`: replaced the per-webframe overlay show/hide loops with a single `WhiteboardWebframe.closeOverlay()` on `startPresentation` (the overlay is now a transient modal and cannot leak into presentation). ### Behavior - A web-frame on the canvas now stacks with `z_index` / `bringToFront` / `sendToBack` like any other object, and frame containment applies. - The persisted payload (`{ url }` inside `data`) is unchanged; existing boards load as the new card with no migration. - Double-click on the card opens the shared modal; the toolbar's Open button does the same. Opening a second web-frame closes the first. - Closing the modal clears `iframe.src` so the inner page is unloaded. - Presentation mode auto-closes the overlay and hides it via CSS for the duration of presentation. - Non-embeddable URLs render the existing fallback card inside the modal body with an "Open in new tab" link. ### Test results - `cargo test --workspace --lib`: compiled cleanly, no failures (change is JS/HTML/CSS-only; regression guard). - `node --check` passed for `webframe.js`, `selection_toolbar.js`, `frames.js`. File encodings clean (UTF-8, zero control bytes). - Rebuilt and redeployed; served board page contains the modal markup, served `webframe.js` contains `openOverlay`/`closeOverlay`/no-op stubs, served CSS contains the new `.wb-webframe-modal*` rules. ### Notes - The pre-flight `/api/url-check` (`web_url_check`) returns only `{ embeddable, reason? }` — there is no favicon/title available without a new server endpoint, so the on-canvas card uses URL + globe icon only. Out of scope to add a thumbnailing endpoint here. - The themed URL-edit modal (`#webframe-url-modal`) and the selection-toolbar URL popover are unchanged. - A latent bug surfaced and fixed in passing: `applyNewUrl` was being called by the toolbar but was missing from the previous public return — it is now exported.
Author
Owner

Implementation Spec for Issue #207 (v2 — revised approach)

The previous v1 approach (replace the live iframe with a Konva card + a transient open-modal) removed the live preview the user wants to keep. This v2 keeps the existing in-canvas iframe overlay and makes it respect canvas layer order through CSS clipping and occlusion-based show/hide.

Objective

The existing live <iframe> overlay stays as the on-canvas preview, but it now:

  1. Is clipped to its parent frame's screen rectangle (frame containment).
  2. Is hidden (the user sees the Konva placeholder underneath) whenever another object with a higher z_index overlaps it — so bringToFront, sendToBack, frame containment, and object stacking all behave as expected.
  3. Reappears automatically when the webframe is brought to front or the occluding object is moved/sent to back.

Requirements

  • The live <iframe> overlay machinery in webframe.js (overlays[id], the wrapper element, updateOverlayPosition, pan/zoom/drag listeners) is preserved — no rewrite to a Konva-only card.
  • A new updateOverlayLayering(id) step runs whenever an event could change the visibility result and:
    • Computes the webframe's screen rect (existing updateOverlayPosition already does this).
    • If the webframe has a parent_frame_id, computes the parent frame group's screen rect and sets the wrapper's clip-path: inset(top right bottom left) so the iframe is clipped to the frame. If the webframe is fully outside the frame's visible area, hide the wrapper.
    • Scans the object layer's siblings: for each Konva node with zIndex() > webframe.zIndex() whose getClientRect() intersects the webframe's getClientRect(), mark the webframe as occluded. If occluded, hide the wrapper (the user sees the Konva placeholder underneath the now-hidden iframe). Otherwise show.
  • bringToFront / sendToBack (Konva moveToTop / moveToBottom) on a webframe or its overlappers must immediately reflect in the iframe visibility.
  • Re-evaluation triggers: stage wheel (pan/zoom), stage drag, any Konva dragmove/dragend on the object layer, and an explicit WhiteboardWebframe.refreshAllLayering() call after object create / delete / z-index change (called by objects.js/tools.js where stacking changes happen).
  • Performance: occlusion checks are O(W·N) per re-evaluation (W webframes, N siblings). Evaluation is debounced to once per requestAnimationFrame.
  • No new server endpoint, no schema change, no Bootstrap/CDN additions.
  • The existing pan/zoom hide-during-drag listeners stay; on dragend we run the new layering check rather than always-restore.

Files to Modify/Create

  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js — add updateOverlayLayering(id), refreshAllLayering(), and _computeOcclusion(group); hook into stage dragend / wheel-end and into the existing updateOverlayPosition path so layering updates whenever position updates. Add new exports.
  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js — after the stacking helpers (bringToFront/sendToBack/moveUp/moveDown callers in contextmenu.js/tools.js) call WhiteboardWebframe.refreshAllLayering() once. Also call it from the create/delete object code paths.
  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js — at the end of object dragend on the object layer, call WhiteboardWebframe.refreshAllLayering() (cheap, rAF-debounced).
  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js — when a parent frame moves or resizes, call WhiteboardWebframe.refreshAllLayering() so the frame-containment clip updates.

No new files, no template/CSS changes (the clip is set inline via JS).

Implementation Plan

Step 1: Add layering helpers in webframe.js

Files: webframe.js

  • function _getClientRectScreen(node) — returns the node's bounding box in screen pixels (stage.container() coords). Use Konva's getClientRect({ relativeTo: stage }) then map by stage.scaleX()/Y() and stage.x()/y(). (Mirror what updateOverlayPosition already does.)
  • function _findParentFrameGroup(group) — read obj.parent_frame_id from the objects registry; return the frame's Konva group or null.
  • function _computeOcclusion(group) — get webframe rect = _getClientRectScreen(group); get object layer children; for each n with n.zIndex() > group.zIndex() AND n !== group, compute nRect; if intersects(rect, nRect) return true; return false.
  • function _computeFrameClipInset(group, wrapper) — if _findParentFrameGroup(group) returns a frame f, compute fRect = _getClientRectScreen(f) and the wrapper's screen rect; return top/right/bottom/left inset values in CSS pixels relative to the wrapper origin so inset(top right bottom left) clips the wrapper to the frame; return null if no parent frame.
  • function updateOverlayLayering(id) — combines the above: applies clip-path: inset(...)/clears it, and toggles wrapper display: none/block based on occlusion + outside-frame.
  • function refreshAllLayering() — debounced via rAF; iterates overlays map and calls updateOverlayLayering for each.
  • Export updateOverlayLayering and refreshAllLayering from the module.
    Dependencies: none

Step 2: Wire layering updates into existing overlay updates

Files: webframe.js

  • In updateOverlayPosition(id) (existing), call updateOverlayLayering(id) at the end so layering is recomputed every time the position is recomputed.
  • The existing stage wheel/dragstart/dragend listeners that update position will automatically pick up layering.
    Dependencies: Step 1

Step 3: Call refreshAllLayering on layer/z mutations

Files: objects.js, tools.js, frames.js

  • In objects.js bringToFront/sendToBack/moveUp/moveDown helpers (find them; if the code uses node.moveToTop() etc. inline, wrap or add a hook): after the Konva stacking call, WhiteboardWebframe.refreshAllLayering().
  • In tools.js global dragend handler on the object layer (find it; the one that fires after any object drag ends), call WhiteboardWebframe.refreshAllLayering().
  • In frames.js frame dragend/transform-end handlers, call WhiteboardWebframe.refreshAllLayering().
  • In objects.js/sync.js object create and delete paths, call WhiteboardWebframe.refreshAllLayering() once after the registry mutation settles.
    Dependencies: Step 2

Step 4: Build + verify

  • touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin.
  • Manual: create a webframe; drop a sticky on top of it → iframe hides, placeholder is visible; bring webframe to front → iframe reappears; put webframe inside a frame and pan the frame → iframe is clipped to the frame; move webframe outside frame visible area → iframe hidden.

Acceptance Criteria

  • Live <iframe> preview still appears on the canvas by default (no Konva-only card replacement).
  • Dropping any other object on top of a webframe (with higher z) hides the iframe; the Konva placeholder underneath shows instead.
  • Bringing the webframe to front while occluded restores the iframe.
  • Sending the webframe to back while another object overlaps it hides the iframe.
  • A webframe inside a frame is visually clipped to the frame; moving the parent frame moves the clip; webframe outside the frame's visible area is hidden.
  • Pan / zoom updates the clip and the occlusion state correctly.
  • Multiple webframes on a board each track their own occlusion/clip state.
  • No regression to URL editing, drag, resize, persistence, or the existing hide-on-drag/hide-on-pan-zoom behaviour.
  • No new console errors; layering recompute is debounced to rAF.

Notes

  • The hide-when-occluded rule is deliberately binary (show or hide, no per-pixel cut-outs). Partial occlusion shows the placeholder; bring-to-front restores. This avoids fragile polygon clip-path math and re-renders the user can't predict.
  • Konva's node.zIndex() returns the runtime layer order, which is what bring-to-front / send-to-back manipulate. The unfixed z_index persistence gap (V2 audit High) is orthogonal — Konva's runtime order is what matters for visibility.
  • This works without a new server endpoint and without external screenshot infra.
## Implementation Spec for Issue #207 (v2 — revised approach) The previous v1 approach (replace the live iframe with a Konva card + a transient open-modal) removed the live preview the user wants to keep. This v2 keeps the existing in-canvas iframe overlay and makes it **respect canvas layer order** through CSS clipping and occlusion-based show/hide. ### Objective The existing live `<iframe>` overlay stays as the on-canvas preview, but it now: 1. Is clipped to its parent frame's screen rectangle (frame containment). 2. Is hidden (the user sees the Konva placeholder underneath) whenever another object with a higher `z_index` overlaps it — so `bringToFront`, `sendToBack`, frame containment, and object stacking all behave as expected. 3. Reappears automatically when the webframe is brought to front or the occluding object is moved/sent to back. ### Requirements - The live `<iframe>` overlay machinery in `webframe.js` (`overlays[id]`, the wrapper element, `updateOverlayPosition`, pan/zoom/drag listeners) is preserved — no rewrite to a Konva-only card. - A new `updateOverlayLayering(id)` step runs whenever an event could change the visibility result and: - Computes the webframe's screen rect (existing `updateOverlayPosition` already does this). - If the webframe has a `parent_frame_id`, computes the parent frame group's screen rect and sets the wrapper's `clip-path: inset(top right bottom left)` so the iframe is clipped to the frame. If the webframe is fully outside the frame's visible area, hide the wrapper. - Scans the object layer's siblings: for each Konva node with `zIndex() > webframe.zIndex()` whose `getClientRect()` intersects the webframe's `getClientRect()`, mark the webframe as occluded. If occluded, hide the wrapper (the user sees the Konva placeholder underneath the now-hidden iframe). Otherwise show. - `bringToFront` / `sendToBack` (Konva `moveToTop` / `moveToBottom`) on a webframe or its overlappers must immediately reflect in the iframe visibility. - Re-evaluation triggers: stage `wheel` (pan/zoom), stage drag, any Konva `dragmove`/`dragend` on the object layer, and an explicit `WhiteboardWebframe.refreshAllLayering()` call after object create / delete / z-index change (called by `objects.js`/`tools.js` where stacking changes happen). - Performance: occlusion checks are O(W·N) per re-evaluation (W webframes, N siblings). Evaluation is debounced to once per `requestAnimationFrame`. - No new server endpoint, no schema change, no Bootstrap/CDN additions. - The existing pan/zoom hide-during-drag listeners stay; on `dragend` we run the new layering check rather than always-restore. ### Files to Modify/Create - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/webframe.js` — add `updateOverlayLayering(id)`, `refreshAllLayering()`, and `_computeOcclusion(group)`; hook into stage `dragend` / `wheel`-end and into the existing `updateOverlayPosition` path so layering updates whenever position updates. Add new exports. - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js` — after the stacking helpers (`bringToFront`/`sendToBack`/`moveUp`/`moveDown` callers in `contextmenu.js`/`tools.js`) call `WhiteboardWebframe.refreshAllLayering()` once. Also call it from the create/delete object code paths. - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` — at the end of object `dragend` on the object layer, call `WhiteboardWebframe.refreshAllLayering()` (cheap, rAF-debounced). - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/frames.js` — when a parent frame moves or resizes, call `WhiteboardWebframe.refreshAllLayering()` so the frame-containment clip updates. No new files, no template/CSS changes (the clip is set inline via JS). ### Implementation Plan #### Step 1: Add layering helpers in webframe.js Files: `webframe.js` - `function _getClientRectScreen(node)` — returns the node's bounding box in screen pixels (stage.container() coords). Use Konva's `getClientRect({ relativeTo: stage })` then map by `stage.scaleX()/Y()` and `stage.x()/y()`. (Mirror what `updateOverlayPosition` already does.) - `function _findParentFrameGroup(group)` — read `obj.parent_frame_id` from the objects registry; return the frame's Konva group or null. - `function _computeOcclusion(group)` — get webframe `rect = _getClientRectScreen(group)`; get object layer children; for each `n` with `n.zIndex() > group.zIndex()` AND `n !== group`, compute `nRect`; if `intersects(rect, nRect)` return true; return false. - `function _computeFrameClipInset(group, wrapper)` — if `_findParentFrameGroup(group)` returns a frame `f`, compute `fRect = _getClientRectScreen(f)` and the wrapper's screen rect; return `top/right/bottom/left` inset values in CSS pixels relative to the wrapper origin so `inset(top right bottom left)` clips the wrapper to the frame; return null if no parent frame. - `function updateOverlayLayering(id)` — combines the above: applies `clip-path: inset(...)`/clears it, and toggles wrapper `display: none/block` based on occlusion + outside-frame. - `function refreshAllLayering()` — debounced via rAF; iterates `overlays` map and calls `updateOverlayLayering` for each. - Export `updateOverlayLayering` and `refreshAllLayering` from the module. Dependencies: none #### Step 2: Wire layering updates into existing overlay updates Files: `webframe.js` - In `updateOverlayPosition(id)` (existing), call `updateOverlayLayering(id)` at the end so layering is recomputed every time the position is recomputed. - The existing stage `wheel`/`dragstart`/`dragend` listeners that update position will automatically pick up layering. Dependencies: Step 1 #### Step 3: Call refreshAllLayering on layer/z mutations Files: `objects.js`, `tools.js`, `frames.js` - In `objects.js` `bringToFront`/`sendToBack`/`moveUp`/`moveDown` helpers (find them; if the code uses `node.moveToTop()` etc. inline, wrap or add a hook): after the Konva stacking call, `WhiteboardWebframe.refreshAllLayering()`. - In `tools.js` global `dragend` handler on the object layer (find it; the one that fires after any object drag ends), call `WhiteboardWebframe.refreshAllLayering()`. - In `frames.js` frame `dragend`/transform-end handlers, call `WhiteboardWebframe.refreshAllLayering()`. - In `objects.js`/`sync.js` object create and delete paths, call `WhiteboardWebframe.refreshAllLayering()` once after the registry mutation settles. Dependencies: Step 2 #### Step 4: Build + verify - `touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin`. - Manual: create a webframe; drop a sticky on top of it → iframe hides, placeholder is visible; bring webframe to front → iframe reappears; put webframe inside a frame and pan the frame → iframe is clipped to the frame; move webframe outside frame visible area → iframe hidden. ### Acceptance Criteria - [ ] Live `<iframe>` preview still appears on the canvas by default (no Konva-only card replacement). - [ ] Dropping any other object on top of a webframe (with higher z) hides the iframe; the Konva placeholder underneath shows instead. - [ ] Bringing the webframe to front while occluded restores the iframe. - [ ] Sending the webframe to back while another object overlaps it hides the iframe. - [ ] A webframe inside a frame is visually clipped to the frame; moving the parent frame moves the clip; webframe outside the frame's visible area is hidden. - [ ] Pan / zoom updates the clip and the occlusion state correctly. - [ ] Multiple webframes on a board each track their own occlusion/clip state. - [ ] No regression to URL editing, drag, resize, persistence, or the existing hide-on-drag/hide-on-pan-zoom behaviour. - [ ] No new console errors; layering recompute is debounced to rAF. ### Notes - The hide-when-occluded rule is deliberately binary (show or hide, no per-pixel cut-outs). Partial occlusion shows the placeholder; bring-to-front restores. This avoids fragile polygon clip-path math and re-renders the user can't predict. - Konva's `node.zIndex()` returns the runtime layer order, which is what bring-to-front / send-to-back manipulate. The unfixed `z_index` persistence gap (V2 audit High) is orthogonal — Konva's runtime order is what matters for visibility. - This works without a new server endpoint and without external screenshot infra.
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#207
No description provided.