From fb68fa648569cf368af876aaeba5f5f940bcca13 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sat, 8 Aug 2026 10:39:00 -0700 Subject: [PATCH] docs: design spec for five user-reported site-builder issues Covers off-canvas drops (reachability invariant: prevent, repair on load, recover via Layers), the HTML block's dead style props (render/export mismatch plus a code-editor toolbar), page/site reset, Layers virtual rows for array-prop composites, and in-builder issue reporting backed by a new whp.site_builder_reports table and a root-only admin page. Co-Authored-By: Claude Opus 5 (1M context) --- ...ite-builder-user-reported-issues-design.md | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-08-site-builder-user-reported-issues-design.md diff --git a/docs/superpowers/specs/2026-08-08-site-builder-user-reported-issues-design.md b/docs/superpowers/specs/2026-08-08-site-builder-user-reported-issues-design.md new file mode 100644 index 0000000..ab01213 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-site-builder-user-reported-issues-design.md @@ -0,0 +1,294 @@ +# Site Builder — Five User-Reported Issues + +**Date:** 2026-08-08 +**Status:** Approved design, ready for implementation planning +**Repos touched:** `cloud-hosting-platform/site-builder` (`craft/`) and `cloud-hosting-platform/whp` (`web-files/`, `sql/`, `scripts/`) + +## Background + +Five issues came in from people using the WHP site builder: + +1. Elements can be dropped outside the main page; once there they cannot be selected or deleted. +2. The HTML element exposes colour options that show in the builder but do not reach the live page. +3. Users want a way to reset a page to blank. +4. The Layers tab sometimes does not show everything. +5. There is no way to report a builder problem from inside the builder. + +Two of these are already explained by the source: + +- **#2 is a render/export mismatch, not just a stray control.** `HtmlBlock`'s React render applies its `style` prop, but `HtmlBlock.toHtml()` returns only the DOMPurify-cleaned `code` and ignores `style` entirely. Every style control the panel offers is therefore dead on export. The controls themselves come from `GenericPropsEditor`, the catch-all panel that `GuidedStyles` routes HTML to via its `isUtility` branch (shared with Divider and Spacer). +- **#4 is a data-model consequence.** `FeaturesGrid`, `Tabs`, `Accordion`, `PricingTable`, `Testimonials`, `Gallery`, `ContentSlider`, `NumberCounter`, `Menu`, `SocialLinks`, `Navbar` and `ContactForm` all hold their content in **array props**, not Craft child nodes, so they are genuinely leaf nodes in the Craft tree and `LayersPanel` has nothing to nest under them. `ColumnLayout` is the exception — it uses real `` children, which is why columns nest correctly and nothing else does. + +#1 is **not** diagnosed. Reproducing it is the first task of implementation, and the prevention fix follows the evidence rather than this document. + +## Scope + +In scope: the five items above, across the Craft.js editor, the WHP site-builder API, one new database table, and one new root-only admin page. + +Out of scope: converting composite components from array props to real Craft children; any change to publish/export output beyond `HtmlBlock` no longer applying `style` in the editor; CSRF enforcement anywhere other than the one new endpoint. + +--- + +## Item 1 — Elements dropped outside the page + +### Invariant + +**Every node in editor state is reachable from `ROOT`.** Enforced at three independent points so no single failure strands a block. + +### 1a. Prevention (drop time) + +A drop whose resolved target is not a `ROOT` descendant is rejected: a new block from `BlocksPanel` is never created, and a moved block returns to its origin. + +The mechanism is deliberately left open pending a live reproduction. Two structural candidates, both to be tested against a real editor session before anything is written: + +- Craft.js's `DefaultEventHandlers` retains the **last valid drop indicator** when the pointer leaves every droppable region. The gutter beside `.canvas-device-frame` (visible at tablet/mobile device widths) and the `pointer-events: none` header/footer `ZonePreview` bands are both places where the pointer is over the canvas area but over no Craft node. +- `Canvas.tsx` wraps `` in a plain positioned `
`; drops landing on that wrapper resolve to no node. + +**The plan's first task is reproduction, and its acceptance criterion is a written description of the actual mechanism.** No fix is written before that. + +### 1b. Repair (load time) + +`PageContext`'s deserialization path runs an orphan sweep before handing state to Craft: + +- Parse the serialized craft state, walk every node's parent chain. +- Any node whose chain does not terminate at `ROOT` is reattached to the end of `ROOT`'s children. +- The repair is logged to the console with node id and type. + +This is a **pure function** over serialized state — `repairOrphanNodes(serialized: string): { state: string; repaired: string[] }` — unit-testable with no browser and no Craft instance, following the precedent of `PageContext.treeToState.test.ts`. + +### 1c. Recovery (always available) + +`LayersPanel` gains an **Unplaced** group, rendered after the `ROOT` tree, listing any node in `state.nodes` not reachable from `ROOT`. Rows behave like normal layer rows: click to select, and the existing delete paths (context menu, Delete key) then work on them. + +The group renders only when the set is non-empty, so ordinary sessions see no new UI. + +### Error handling + +If `repairOrphanNodes` throws on malformed state, it returns the input unchanged and logs — a page must never fail to load because the repair pass could not parse it. + +--- + +## Item 2 — HTML block + +### 2a. Only the Edit HTML control + +- `GuidedStyles` gains an `isHtml` branch (`/^html$/i`) evaluated **before** `isUtility`, and `html` is removed from the `isUtility` regex so the two cannot both match. +- New `panels/right/styles/HtmlStylePanel.tsx` renders the Edit HTML button and nothing else. +- `HtmlCodeField` moves out of `GenericPropsEditor.tsx` into `panels/right/styles/HtmlCodeField.tsx` and is imported by `HtmlStylePanel`. `GenericPropsEditor` keeps `code` in `SKIP_PROPS` (harmless, and guards any future component carrying a `code` prop). + +### 2b. Render must match export + +`HtmlBlock`'s render stops applying the `style` prop. It keeps the selection outline and `minHeight: 40px` — both are editor affordances, not content. + +This is the fix for the actual complaint. Removing the controls alone would leave already-styled blocks still rendering their dead styling in the editor and still not on the live page. + +Existing stored `style` props are left in place: they are already ignored by `toHtml()`, so they have no output effect, and stripping them would mean touching every saved site. + +### 2c. Smarter HTML editor + +`ui/CodeEditor.tsx` already provides CodeMirror 6 with autocompletion, auto-closing tags and brackets, bracket matching, a fold gutter, `indentWithTab` and the one-dark theme. Nothing new is needed for tab-complete — but the component **lazy-loads CodeMirror and silently falls back to a plain `