feat(editor): WYSIWYG HTML slide editing — click-to-edit, drag, resize #96

Closed
opened 2026-05-29 06:57:12 +00:00 by casper-stevens · 2 comments
Member

Summary

Now that slides are rendered as HTML (not static images), the preview iframe gives us full DOM access. This opens up direct visual editing that was previously impossible.

What to implement

Phase 1 — Core editing (highest leverage)

Inject editing script into iframe on ?edit=true
When serving the slide HTML in edit mode, append a <script> block that runs inside the iframe DOM. This avoids coordinate translation and gives full element access. Strip the script before saving HTML back to disk.

Click-to-select
Click any element in the preview → highlight with a selection box overlay. Clicking away deselects.

Double-click to edit text
Double-click any text element → set contentEditable = true, user types inline. Blur → save.

slide.saveHtml RPC
New RPC method that writes the modified iframe.contentDocument.documentElement.outerHTML back to deck/output/<slide>.html. Once a slide has manual HTML edits, mark it as "manually edited" — skip AI regeneration unless explicitly requested. This avoids the hard markdown↔HTML sync problem entirely.

Undo/redo stack
Before each mutation, push outerHTML snapshot. Ctrl+Z pops and re-injects. Keep last 20 states. ~20 lines once save path exists.

Phase 2 — Move & resize

Drag to move (abs-positioned elements only)
The AI generates most elements with position:absolute + explicit left/top. Mousedown → track delta → mutate style.left/style.top. Skip flow-layout elements — dragging those breaks layout unpredictably.

Resize handles
8 handles (corners + edges) as overlay divs on the selection box. Drag → mutate width/height (and left/top for top/left handles).

Phase 3 — Properties panel

When an element is selected, show a floating or sidebar panel:

  • Text: font-size, color, font-weight, text-align
  • Position: x, y, width, height (numeric inputs, synced with drag)
  • Visibility: hide/show

Direct DOM style mutation, same save path.

What NOT to do

  • No markdown↔HTML sync — treat HTML as the final output layer. Markdown is AI input only.
  • No drag for flow-layout elements — only position:absolute elements.
  • No Figma scope creep — layers panel, z-index, shape insertion are out of scope.

Feasibility

Feature Effort Notes
Click-to-select + text edit Low ~200 lines JS + 1 RPC
slide.saveHtml RPC Very low New file write path
Drag (abs-positioned) Low-medium Clean CSS mutation
Undo/redo Low 20 lines once save works
Resize handles Medium 300 lines on top of selection
Properties panel Medium Most UI work

Phase 1 alone eliminates the "regenerate the whole slide to fix a typo" problem.

Technical approach

Same-origin iframe → iframe.contentDocument is fully accessible from parent JS. Inject edit overlay via ?edit=true query param handled in the slide HTML route. No cross-origin, no CSP issues since we control the server.

## Summary Now that slides are rendered as HTML (not static images), the preview iframe gives us full DOM access. This opens up direct visual editing that was previously impossible. ## What to implement ### Phase 1 — Core editing (highest leverage) **Inject editing script into iframe on `?edit=true`** When serving the slide HTML in edit mode, append a `<script>` block that runs inside the iframe DOM. This avoids coordinate translation and gives full element access. Strip the script before saving HTML back to disk. **Click-to-select** Click any element in the preview → highlight with a selection box overlay. Clicking away deselects. **Double-click to edit text** Double-click any text element → set `contentEditable = true`, user types inline. Blur → save. **`slide.saveHtml` RPC** New RPC method that writes the modified `iframe.contentDocument.documentElement.outerHTML` back to `deck/output/<slide>.html`. Once a slide has manual HTML edits, mark it as "manually edited" — skip AI regeneration unless explicitly requested. This avoids the hard markdown↔HTML sync problem entirely. **Undo/redo stack** Before each mutation, push `outerHTML` snapshot. Ctrl+Z pops and re-injects. Keep last 20 states. ~20 lines once save path exists. ### Phase 2 — Move & resize **Drag to move (abs-positioned elements only)** The AI generates most elements with `position:absolute` + explicit `left/top`. Mousedown → track delta → mutate `style.left/style.top`. Skip flow-layout elements — dragging those breaks layout unpredictably. **Resize handles** 8 handles (corners + edges) as overlay divs on the selection box. Drag → mutate `width`/`height` (and `left`/`top` for top/left handles). ### Phase 3 — Properties panel When an element is selected, show a floating or sidebar panel: - Text: font-size, color, font-weight, text-align - Position: x, y, width, height (numeric inputs, synced with drag) - Visibility: hide/show Direct DOM `style` mutation, same save path. ## What NOT to do - **No markdown↔HTML sync** — treat HTML as the final output layer. Markdown is AI input only. - **No drag for flow-layout elements** — only `position:absolute` elements. - **No Figma scope creep** — layers panel, z-index, shape insertion are out of scope. ## Feasibility | Feature | Effort | Notes | |---|---|---| | Click-to-select + text edit | Low | ~200 lines JS + 1 RPC | | `slide.saveHtml` RPC | Very low | New file write path | | Drag (abs-positioned) | Low-medium | Clean CSS mutation | | Undo/redo | Low | 20 lines once save works | | Resize handles | Medium | 300 lines on top of selection | | Properties panel | Medium | Most UI work | Phase 1 alone eliminates the "regenerate the whole slide to fix a typo" problem. ## Technical approach Same-origin iframe → `iframe.contentDocument` is fully accessible from parent JS. Inject edit overlay via `?edit=true` query param handled in the slide HTML route. No cross-origin, no CSP issues since we control the server.
Author
Member

Related branch: development_pptx_mockup

This work builds on the HTML-based PPTX export work done on that branch (the switch from image to HTML rendering is what makes this possible).

Related branch: `development_pptx_mockup` This work builds on the HTML-based PPTX export work done on that branch (the switch from image to HTML rendering is what makes this possible).
Author
Member

Implementation Spec for Issue #96

Objective

Add WYSIWYG (click-to-edit, drag, resize, properties-panel) editing of slides directly inside the existing same-origin preview iframe on the slide editor page (crates/hero_slides_admin). Edits mutate the live iframe.contentDocument DOM in JS, then persist the cleaned documentElement.outerHTML back to deck/output/<slide>.html via a new slide.saveHtml RPC. Once a slide has manual HTML edits it is marked "manually edited" so AI HTML regeneration skips it unless forced.

Requirements

  • Serving the slide HTML in edit mode (?edit=true) appends an editor <script> (+overlay CSS) that runs inside the iframe DOM. The script is stripped before the HTML is written to disk.
  • Click-to-select with a selection-box overlay; click-away deselects.
  • Double-click a text element → contentEditable=true, inline typing; blur saves the snapshot.
  • slide.saveHtml RPC writes the edited outerHTML to output/<slide>.html and sets a per-slide manually_edited marker in metadata.toml.
  • Undo/redo stack (last 20 outerHTML snapshots; Ctrl+Z / Ctrl+Shift+Z).
  • Drag to move only position:absolute elements (mutate style.left/style.top).
  • 8 resize handles (corners + edges) on the selection box; mutate width/height (+left/top for top/left handles).
  • Properties panel for a selected element: Text (font-size, color, font-weight, text-align), Position (x/y/w/h numeric inputs), Visibility (hide/show). Direct style mutation, same save path.
  • Manual-edit marker makes deck_generate_html / slide_generate_html skip the slide unless force=true.
  • Out of scope (do NOT do): markdown↔HTML sync, dragging flow-layout elements, layers panel, z-index, shape insertion.

Files to Modify / Create

Backend — Rust

  • crates/hero_slides_admin/src/routes.rs — In serve_slide_html, read the ?edit=true query param; when set, inject the editor script + CSS before </body>. The route already resolves the deck path and serves the file.
  • crates/hero_slides_lib/src/hashing.rs — Add pub manually_edited: bool (with #[serde(default)]) to SlideMetaEntry. Add slide_mark_manually_edited(deck_dir, slide_name) and slide_is_manually_edited(deck_dir, slide_name) -> bool (via existing load_metadata/save_metadata).
  • crates/hero_slides_lib/src/deck.rs — Add slide_save_html(deck_path, slide_name, html) -> Result<PathBuf> (writes output/<slide>.html, then marks manually edited). In deck_generate_html and slide_generate_html, skip when manually_edited is true and !force (add a force param to slide_generate_html).
  • crates/hero_slides_lib/src/lib.rs — Re-export slide_save_html, slide_mark_manually_edited, slide_is_manually_edited.
  • crates/hero_slides_server/src/rpc/mod.rs — Register "slide.saveHtml" => handle_slide_save_html(...) in the dispatch match (by slide.saveContent). Implement handle_slide_save_html (modeled on handle_slide_save_content). Add "slide.saveHtml" to is_deck_mutation so the deck-changed SSE fires.
  • crates/hero_slides_server/openrpc.json — Add a slide.saveHtml method entry (params: collection, deck, slide, html; result { saved: bool }).

Frontend — JS/HTML/CSS

  • crates/hero_slides_admin/static/js/slide_edit.js — Parent-side controller: load preview with ?edit=true, relay save to rpc('slide.saveHtml', …). Integration points: slideHtmlUrl(), loadPreview(), rpc() helper.
  • crates/hero_slides_admin/static/css/slide_edit.css — Properties-panel + toolbar styling on the parent page.
  • crates/hero_slides_admin/templates/slide_edit.html — "Edit layout" toggle button + properties-panel markup.
  • New crates/hero_slides_admin/static/js/slide_wysiwyg_inject.js — The script injected into the iframe (selection box + 8 handles, click/dblclick/drag/resize, undo stack, postMessage to parent). Embedded into the served HTML by serve_slide_html.

Implementation Plan

Phase 1 — Core editing

Step 1.1 — manually_edited marker in the data model
Files: hero_slides_lib/src/hashing.rs, hero_slides_lib/src/lib.rs.
Add #[serde(default)] pub manually_edited: bool to SlideMetaEntry; add slide_mark_manually_edited / slide_is_manually_edited; re-export both.
Dependencies: none.

Step 1.2 — slide_save_html lib function + skip-on-generate
Files: hero_slides_lib/src/deck.rs, hero_slides_lib/src/lib.rs.
Add slide_save_html (write output/<slide>.html, then mark); in deck_generate_html skip manually-edited slides unless force; in slide_generate_html add a force param and early-return when manually edited and not forced; re-export slide_save_html.
Dependencies: 1.1.

Step 1.3 — slide.saveHtml RPC + openrpc.json
Files: hero_slides_server/src/rpc/mod.rs, hero_slides_server/openrpc.json.
Implement handle_slide_save_html (copy handle_slide_save_content shape, call slide_save_html in spawn_blocking); add match arm; add to is_deck_mutation; add method to openrpc.json.
Dependencies: 1.2.

Step 1.4 — ?edit=true injection in the HTML route
Files: hero_slides_admin/src/routes.rs.
serve_slide_html accepts a Query<HashMap>; when edit=true, splice the injected block before </body>; non-edit requests unchanged.
Dependencies: pairs with 1.5.

Step 1.5 — In-iframe editor script: select + edit text + undo
Files: new hero_slides_admin/static/js/slide_wysiwyg_inject.js, hero_slides_admin/src/routes.rs.
Selection box overlay, click-to-select, click-away deselect, double-click contentEditable, blur→snapshot, undo/redo (cap 20), Ctrl+Z/Ctrl+Shift+Z. On save, postMessage {type:'hero:wysiwyg-save', html: cleanedOuterHTML} with injected nodes (tagged data-hero-wysiwyg) + contentEditable stripped.
Dependencies: 1.4.

Step 1.6 — Parent-side controller wiring
Files: hero_slides_admin/static/js/slide_edit.js, templates/slide_edit.html, static/css/slide_edit.css.
"Edit layout" toggle, load iframe with &edit=true, listen for hero:wysiwyg-saverpc('slide.saveHtml', …) → reload staleness.
Dependencies: 1.3, 1.5.

Phase 2 — Move & resize

Step 2.1 — Drag-to-move (abs-positioned only)
Files: slide_wysiwyg_inject.js.
On mousedown over a selected position:absolute/fixed element, track delta, mutate style.left/style.top; skip flow-layout; undo snapshot on drag start.
Dependencies: 1.5.

Step 2.2 — 8 resize handles
Files: slide_wysiwyg_inject.js.
8 handle divs on the selection box; per-handle resize math mutating width/height (+left/top for top/left); undo snapshot on resize start.
Dependencies: 1.5, 2.1.

Phase 3 — Properties panel

Step 3.1 — Properties panel UI + binding
Files: templates/slide_edit.html, static/js/slide_edit.js, static/css/slide_edit.css, slide_wysiwyg_inject.js.
On selection, inject script posts the element's computed style to the parent; panel inputs (font-size, color, font-weight, text-align, x, y, w, h, hidden) post changes back; inject script applies to element.style + undo snapshot; persist via the hero:wysiwyg-save path.
Dependencies: 1.6, 2.2.

Acceptance Criteria

  • .../slides/{s}/html?edit=true returns HTML with the editor script injected; without ?edit=true it returns the raw file unchanged.
  • Clicking an element shows a selection box; clicking empty space deselects.
  • Double-clicking a text element makes it editable; typing then blurring updates the DOM.
  • slide.saveHtml writes the edited outerHTML to output/<slide>.html with injected script/overlay/contentEditable stripped.
  • After a manual save, the slide's metadata.toml entry has manually_edited = true.
  • deck_generate_html / slide_generate_html skip a manually-edited slide unless force=true.
  • Ctrl+Z restores the previous outerHTML; undo stack capped at 20.
  • position:absolute elements drag; flow-layout elements do not.
  • 8 resize handles appear and resize the element (top/left handles also move it).
  • Properties panel reads font-size/color/font-weight/text-align/x/y/w/h/visibility and writing a value mutates + persists.
  • slide.saveHtml appears in openrpc.json.
  • cargo build succeeds for hero_slides_lib, hero_slides_server, hero_slides_admin.

Notes

  • Architecture correction vs. issue text: there is no web.rs / templates/web.html. The slide route is serve_slide_html in crates/hero_slides_admin/src/routes.rs; the preview iframe (preview-iframe) is in templates/slide_edit.html, driven by static/js/slide_edit.js (loadPreview). RPC dispatch is a manual match in hero_slides_server/src/rpc/mod.rs (handle_request) — no auto-registry; the public schema lives in openrpc.json. The browser calls /rpc on the admin server, which proxies over a Unix socket to the backend.
  • Marker design: reuse metadata.toml + SlideMetaEntry (mirrors the existing hidden flag / slide_set_hidden). #[serde(default)] keeps old files parseable. Saving markdown (slide.saveContent) does NOT clear the flag (out of scope here).
  • Force bypass already wired: the "Regenerate anyway (force)" control (btn-regenerate-forceregenerate(force)slide.generateHtmlAsync--force) already exists; the lib-level skip just needs to honor force.
  • Script stripping on save: done inside the iframe script (it knows which nodes it added; tag them data-hero-wysiwyg) rather than server-side regex.
  • Undo stack lives inside the iframe script; re-running setup after restore is required because innerHTML replacement drops listeners.
  • postMessage namespace: slide_edit.html already uses a message listener with hero:theme-ready; use a distinct hero:wysiwyg-* namespace.
  • Uncommitted browser_pptx.rs (the HTML→PPTX exporter) is unrelated to this feature and will not be touched.
## Implementation Spec for Issue #96 ### Objective Add WYSIWYG (click-to-edit, drag, resize, properties-panel) editing of slides directly inside the existing same-origin preview iframe on the slide editor page (`crates/hero_slides_admin`). Edits mutate the live `iframe.contentDocument` DOM in JS, then persist the cleaned `documentElement.outerHTML` back to `deck/output/<slide>.html` via a new `slide.saveHtml` RPC. Once a slide has manual HTML edits it is marked "manually edited" so AI HTML regeneration skips it unless forced. ### Requirements - Serving the slide HTML in edit mode (`?edit=true`) appends an editor `<script>` (+overlay CSS) that runs inside the iframe DOM. The script is stripped before the HTML is written to disk. - Click-to-select with a selection-box overlay; click-away deselects. - Double-click a text element → `contentEditable=true`, inline typing; blur saves the snapshot. - `slide.saveHtml` RPC writes the edited `outerHTML` to `output/<slide>.html` and sets a per-slide `manually_edited` marker in `metadata.toml`. - Undo/redo stack (last 20 `outerHTML` snapshots; Ctrl+Z / Ctrl+Shift+Z). - Drag to move only `position:absolute` elements (mutate `style.left`/`style.top`). - 8 resize handles (corners + edges) on the selection box; mutate `width`/`height` (+`left`/`top` for top/left handles). - Properties panel for a selected element: Text (font-size, color, font-weight, text-align), Position (x/y/w/h numeric inputs), Visibility (hide/show). Direct `style` mutation, same save path. - Manual-edit marker makes `deck_generate_html` / `slide_generate_html` skip the slide unless `force=true`. - Out of scope (do NOT do): markdown↔HTML sync, dragging flow-layout elements, layers panel, z-index, shape insertion. ### Files to Modify / Create **Backend — Rust** - `crates/hero_slides_admin/src/routes.rs` — In `serve_slide_html`, read the `?edit=true` query param; when set, inject the editor script + CSS before `</body>`. The route already resolves the deck path and serves the file. - `crates/hero_slides_lib/src/hashing.rs` — Add `pub manually_edited: bool` (with `#[serde(default)]`) to `SlideMetaEntry`. Add `slide_mark_manually_edited(deck_dir, slide_name)` and `slide_is_manually_edited(deck_dir, slide_name) -> bool` (via existing `load_metadata`/`save_metadata`). - `crates/hero_slides_lib/src/deck.rs` — Add `slide_save_html(deck_path, slide_name, html) -> Result<PathBuf>` (writes `output/<slide>.html`, then marks manually edited). In `deck_generate_html` and `slide_generate_html`, skip when `manually_edited` is true and `!force` (add a `force` param to `slide_generate_html`). - `crates/hero_slides_lib/src/lib.rs` — Re-export `slide_save_html`, `slide_mark_manually_edited`, `slide_is_manually_edited`. - `crates/hero_slides_server/src/rpc/mod.rs` — Register `"slide.saveHtml" => handle_slide_save_html(...)` in the dispatch match (by `slide.saveContent`). Implement `handle_slide_save_html` (modeled on `handle_slide_save_content`). Add `"slide.saveHtml"` to `is_deck_mutation` so the deck-changed SSE fires. - `crates/hero_slides_server/openrpc.json` — Add a `slide.saveHtml` method entry (params: `collection`, `deck`, `slide`, `html`; result `{ saved: bool }`). **Frontend — JS/HTML/CSS** - `crates/hero_slides_admin/static/js/slide_edit.js` — Parent-side controller: load preview with `?edit=true`, relay save to `rpc('slide.saveHtml', …)`. Integration points: `slideHtmlUrl()`, `loadPreview()`, `rpc()` helper. - `crates/hero_slides_admin/static/css/slide_edit.css` — Properties-panel + toolbar styling on the parent page. - `crates/hero_slides_admin/templates/slide_edit.html` — "Edit layout" toggle button + properties-panel markup. - **New** `crates/hero_slides_admin/static/js/slide_wysiwyg_inject.js` — The script injected into the iframe (selection box + 8 handles, click/dblclick/drag/resize, undo stack, `postMessage` to parent). Embedded into the served HTML by `serve_slide_html`. ### Implementation Plan **Phase 1 — Core editing** **Step 1.1 — `manually_edited` marker in the data model** Files: `hero_slides_lib/src/hashing.rs`, `hero_slides_lib/src/lib.rs`. Add `#[serde(default)] pub manually_edited: bool` to `SlideMetaEntry`; add `slide_mark_manually_edited` / `slide_is_manually_edited`; re-export both. Dependencies: none. **Step 1.2 — `slide_save_html` lib function + skip-on-generate** Files: `hero_slides_lib/src/deck.rs`, `hero_slides_lib/src/lib.rs`. Add `slide_save_html` (write `output/<slide>.html`, then mark); in `deck_generate_html` skip manually-edited slides unless `force`; in `slide_generate_html` add a `force` param and early-return when manually edited and not forced; re-export `slide_save_html`. Dependencies: 1.1. **Step 1.3 — `slide.saveHtml` RPC + openrpc.json** Files: `hero_slides_server/src/rpc/mod.rs`, `hero_slides_server/openrpc.json`. Implement `handle_slide_save_html` (copy `handle_slide_save_content` shape, call `slide_save_html` in `spawn_blocking`); add match arm; add to `is_deck_mutation`; add method to `openrpc.json`. Dependencies: 1.2. **Step 1.4 — `?edit=true` injection in the HTML route** Files: `hero_slides_admin/src/routes.rs`. `serve_slide_html` accepts a `Query<HashMap>`; when `edit=true`, splice the injected block before `</body>`; non-edit requests unchanged. Dependencies: pairs with 1.5. **Step 1.5 — In-iframe editor script: select + edit text + undo** Files: new `hero_slides_admin/static/js/slide_wysiwyg_inject.js`, `hero_slides_admin/src/routes.rs`. Selection box overlay, click-to-select, click-away deselect, double-click `contentEditable`, blur→snapshot, undo/redo (cap 20), Ctrl+Z/Ctrl+Shift+Z. On save, `postMessage` `{type:'hero:wysiwyg-save', html: cleanedOuterHTML}` with injected nodes (tagged `data-hero-wysiwyg`) + `contentEditable` stripped. Dependencies: 1.4. **Step 1.6 — Parent-side controller wiring** Files: `hero_slides_admin/static/js/slide_edit.js`, `templates/slide_edit.html`, `static/css/slide_edit.css`. "Edit layout" toggle, load iframe with `&edit=true`, listen for `hero:wysiwyg-save` → `rpc('slide.saveHtml', …)` → reload staleness. Dependencies: 1.3, 1.5. **Phase 2 — Move & resize** **Step 2.1 — Drag-to-move (abs-positioned only)** Files: `slide_wysiwyg_inject.js`. On mousedown over a selected `position:absolute`/`fixed` element, track delta, mutate `style.left`/`style.top`; skip flow-layout; undo snapshot on drag start. Dependencies: 1.5. **Step 2.2 — 8 resize handles** Files: `slide_wysiwyg_inject.js`. 8 handle divs on the selection box; per-handle resize math mutating `width`/`height` (+`left`/`top` for top/left); undo snapshot on resize start. Dependencies: 1.5, 2.1. **Phase 3 — Properties panel** **Step 3.1 — Properties panel UI + binding** Files: `templates/slide_edit.html`, `static/js/slide_edit.js`, `static/css/slide_edit.css`, `slide_wysiwyg_inject.js`. On selection, inject script posts the element's computed style to the parent; panel inputs (font-size, color, font-weight, text-align, x, y, w, h, hidden) post changes back; inject script applies to `element.style` + undo snapshot; persist via the `hero:wysiwyg-save` path. Dependencies: 1.6, 2.2. ### Acceptance Criteria - [ ] `.../slides/{s}/html?edit=true` returns HTML with the editor script injected; without `?edit=true` it returns the raw file unchanged. - [ ] Clicking an element shows a selection box; clicking empty space deselects. - [ ] Double-clicking a text element makes it editable; typing then blurring updates the DOM. - [ ] `slide.saveHtml` writes the edited `outerHTML` to `output/<slide>.html` with injected script/overlay/`contentEditable` stripped. - [ ] After a manual save, the slide's `metadata.toml` entry has `manually_edited = true`. - [ ] `deck_generate_html` / `slide_generate_html` skip a manually-edited slide unless `force=true`. - [ ] Ctrl+Z restores the previous `outerHTML`; undo stack capped at 20. - [ ] `position:absolute` elements drag; flow-layout elements do not. - [ ] 8 resize handles appear and resize the element (top/left handles also move it). - [ ] Properties panel reads font-size/color/font-weight/text-align/x/y/w/h/visibility and writing a value mutates + persists. - [ ] `slide.saveHtml` appears in `openrpc.json`. - [ ] `cargo build` succeeds for `hero_slides_lib`, `hero_slides_server`, `hero_slides_admin`. ### Notes - **Architecture correction vs. issue text:** there is no `web.rs` / `templates/web.html`. The slide route is `serve_slide_html` in `crates/hero_slides_admin/src/routes.rs`; the preview iframe (`preview-iframe`) is in `templates/slide_edit.html`, driven by `static/js/slide_edit.js` (`loadPreview`). RPC dispatch is a manual `match` in `hero_slides_server/src/rpc/mod.rs` (`handle_request`) — no auto-registry; the public schema lives in `openrpc.json`. The browser calls `/rpc` on the admin server, which proxies over a Unix socket to the backend. - **Marker design:** reuse `metadata.toml` + `SlideMetaEntry` (mirrors the existing `hidden` flag / `slide_set_hidden`). `#[serde(default)]` keeps old files parseable. Saving markdown (`slide.saveContent`) does NOT clear the flag (out of scope here). - **Force bypass already wired:** the "Regenerate anyway (force)" control (`btn-regenerate-force` → `regenerate(force)` → `slide.generateHtmlAsync` → `--force`) already exists; the lib-level skip just needs to honor `force`. - **Script stripping on save:** done inside the iframe script (it knows which nodes it added; tag them `data-hero-wysiwyg`) rather than server-side regex. - **Undo stack** lives inside the iframe script; re-running setup after restore is required because innerHTML replacement drops listeners. - **postMessage namespace:** `slide_edit.html` already uses a `message` listener with `hero:theme-ready`; use a distinct `hero:wysiwyg-*` namespace. - **Uncommitted `browser_pptx.rs`** (the HTML→PPTX exporter) is unrelated to this feature and will not be touched.
Sign in to join this conversation.
No labels
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_slides#96
No description provided.