diff --git a/docs/superpowers/plans/2026-08-08-site-builder-user-reported-issues.md b/docs/superpowers/plans/2026-08-08-site-builder-user-reported-issues.md new file mode 100644 index 0000000..9f300a6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-08-site-builder-user-reported-issues.md @@ -0,0 +1,4244 @@ +# Site Builder — Five User-Reported Issues Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix five reported site-builder problems — elements stranded outside the page, dead colour controls on the HTML block, no way to blank a page, an incomplete Layers tree, and no in-builder way to report a bug. + +**Architecture:** Editor work lands in the Craft.js app at `/workspace/site-builder/craft/`. Every non-trivial behaviour is first extracted as a **pure function** in `src/utils/` or a small registry module, unit-tested with no React and no Craft instance, then wired into a panel. Reporting adds one endpoint to the existing WHP site-builder API, one MySQL table, and one root-only admin page in `/workspace/whp/`. + +**Tech Stack:** Vite 6, React 18, TypeScript 5 (strict), @craftjs/core 0.2.x, CodeMirror 6 (lazy-loaded), vitest + jsdom, PHP 8 + PDO/MySQL, Bootstrap 5 (admin page). + +## Global Constraints + +- **Design spec:** `docs/superpowers/specs/2026-08-08-site-builder-user-reported-issues-design.md`. Read it before starting. +- **Active source is `/workspace/site-builder/craft/`.** Never edit `/workspace/site-builder/` top level — that is the dead GrapesJS builder. +- **No `@/` alias in tests.** `vitest.config.ts` defines no `resolve.alias`. Use relative imports in all source and test files. +- **No `@testing-library/react`.** This repo renders with `createRoot` + `act` from `react-dom/test-utils`. For a component needing only `useNode`, mock `@craftjs/core` (pattern: `src/components/media/ImageBlock.render.test.tsx`). For anything needing a real editor, use `renderEditorHarness()` from `src/test-utils/editorHarness.tsx`. +- **Run tests with:** `cd /workspace/site-builder/craft && npx vitest run `. +- **New dependencies: none.** The HTML formatter is hand-written; do not add `js-beautify`, `prettier`, `html2canvas`, or an Emmet package. +- **Every component prop change must honour both sides:** the React `render` *and* the static `.toHtml(props, childrenHtml)`. `HtmlBlock` is the exception this plan creates deliberately (Task 1). +- **PHP shell/CLI rule:** verify anything path-sensitive through the web path, not bare `php -r` — CLI has an empty `open_basedir`. +- **SQL migrations are idempotent** (`CREATE TABLE IF NOT EXISTS`), go in `sql/migrations/staging/`, and never hand-create versioned directories. +- **Commit after every task.** Co-author trailer: `Co-Authored-By: Claude Opus 5 (1M context) `. + +--- + +## File Structure + +**Created in `/workspace/site-builder/craft/`:** + +| File | Responsibility | +|---|---| +| `src/utils/orphan-repair.ts` | Pure: find nodes unreachable from `ROOT` in serialized craft state; reattach them. | +| `src/utils/orphan-repair.test.ts` | Tests for the above. | +| `src/utils/format-html.ts` | Pure: indent-only HTML prettifier. | +| `src/utils/format-html.test.ts` | Tests for the above. | +| `src/utils/console-buffer.ts` | Global ring buffer of the last 20 console/window errors. | +| `src/utils/console-buffer.test.ts` | Tests for the above. | +| `src/utils/build-stamp.ts` | Safe accessor for the `__EDITOR_BUILD__` compile-time define. | +| `src/utils/report-payload.ts` | Pure: assemble + size-cap an issue-report payload. | +| `src/utils/report-payload.test.ts` | Tests for the above. | +| `src/panels/left/layers-virtual-rows.ts` | Registry + pure derivation of virtual child rows for array-prop composites. | +| `src/panels/left/layers-virtual-rows.test.ts` | Tests for the above. | +| `src/panels/left/LayerFocusContext.tsx` | Context carrying "scroll array item N of node X into view". | +| `src/panels/right/styles/HtmlCodeField.tsx` | The Edit HTML button + modal (moved out of `GenericPropsEditor`). | +| `src/panels/right/styles/HtmlToolbar.tsx` | Insert/colour/format toolbar for the Edit HTML modal. | +| `src/panels/right/styles/HtmlStylePanel.tsx` | HTML block's only style panel — the Edit HTML control alone. | +| `src/panels/right/styles/HtmlStylePanel.test.tsx` | Asserts no colour/style controls render. | +| `src/panels/topbar/ReportIssueModal.tsx` | Report-an-issue form + submit. | +| `src/panels/topbar/ReportIssueModal.test.tsx` | Tests for the above. | + +**Modified in `craft/`:** `src/components/basic/HtmlBlock.tsx`, `src/panels/right/GuidedStyles.tsx`, `src/panels/right/styles/GenericPropsEditor.tsx`, `src/panels/right/styles/index.ts`, `src/ui/CodeEditor.tsx`, `src/state/PageContext.tsx`, `src/panels/left/LayersPanel.tsx`, `src/panels/left/PagesPanel.tsx`, `src/panels/right/SiteDesignPanel.tsx`, `src/panels/topbar/TopBar.tsx`, `src/panels/topbar/TopBarOverflowMenu.tsx`, `src/main.tsx`, `vite.config.ts`, `src/styles/editor.css`. + +**Created in `/workspace/whp/`:** `sql/migrations/staging/create-site-builder-reports.sql`, `scripts/test-site-builder-report-validation.php`, `web-files/pages/site-builder-reports.php`. +**Modified in `/workspace/whp/`:** `web-files/api/site-builder.php`, `web-files/includes/` sidebar, `web-files/libs/permission_manager.php`, `web-files/index.php` (page registration), `DOCS_FOR_AGENTS/DATABASE_SCHEMA.md`. + +--- + +## Phase 1 — HTML block (spec item 2) + +### Task 1: HtmlBlock stops applying `style` in the editor + +The reported symptom is that colours show in the builder and never reach the live page. `toHtml()` already ignores `style`; the render does not. Fix the render so both sides agree. + +**Files:** +- Modify: `src/components/basic/HtmlBlock.tsx:65-78` +- Test: `src/components/basic/HtmlBlock.render.test.tsx` (create) +- Test: `src/components/basic/HtmlBlock.toHtml.test.ts` (extend) + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: `HtmlBlock` render no longer reads `props.style`. `HtmlBlockProps` keeps its `style?: CSSProperties` field so stored state stays loadable. + +- [ ] **Step 1: Write the failing render test** + +Create `src/components/basic/HtmlBlock.render.test.tsx`: + +```tsx +import { describe, test, expect, vi } from 'vitest'; +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; +import { act } from 'react-dom/test-utils'; + +vi.mock('@craftjs/core', () => ({ + useNode: (collect?: (node: any) => any) => { + const node = { events: { selected: false } }; + return { + connectors: { connect: (el: any) => el, drag: (el: any) => el }, + actions: { setProp: vi.fn() }, + ...(collect ? collect(node) : {}), + }; + }, +})); + +import { HtmlBlock } from './HtmlBlock'; + +let container: HTMLDivElement; +let root: Root; + +function render(ui: React.ReactElement) { + container = document.createElement('div'); + document.body.appendChild(container); + act(() => { + root = createRoot(container); + root.render(ui); + }); +} + +describe('HtmlBlock render ignores the style prop (matches toHtml)', () => { + test('a stored backgroundColor/color/padding is NOT applied to the wrapper', () => { + render( + React.createElement(HtmlBlock as any, { + code: '

hello

', + style: { backgroundColor: 'rgb(255, 0, 0)', color: 'rgb(0, 0, 255)', padding: '40px' }, + }), + ); + const wrapper = container.firstElementChild as HTMLElement; + expect(wrapper.style.backgroundColor).toBe(''); + expect(wrapper.style.color).toBe(''); + expect(wrapper.style.padding).toBe(''); + }); + + test('the editor affordances survive: minHeight is kept, content still renders', () => { + render(React.createElement(HtmlBlock as any, { code: '

hello

', style: {} })); + const wrapper = container.firstElementChild as HTMLElement; + expect(wrapper.style.minHeight).toBe('40px'); + expect(wrapper.innerHTML).toContain('hello'); + }); +}); +``` + +- [ ] **Step 2: Run it and verify it fails** + +Run: `npx vitest run src/components/basic/HtmlBlock.render.test.tsx` +Expected: FAIL — the first test reports `backgroundColor` as `rgb(255, 0, 0)` because the current render spreads `...style`. + +- [ ] **Step 3: Make the render ignore `style`** + +In `src/components/basic/HtmlBlock.tsx`, replace the component body's `React.createElement` call: + +```tsx +export const HtmlBlock: UserComponent = ({ code = '' }) => { + const { connectors: { connect, drag }, selected } = useNode((node) => ({ selected: node.events.selected })); + const clean = useMemo(() => purifyHtml(code), [code]); + const setRef = (ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }; + // The `style` prop is deliberately NOT applied. `toHtml()` emits only the + // purified `code`, so anything styled here would show in the editor and + // vanish on the live page -- the exact bug this block was reported for. + // Wrapper styling belongs in the user's own markup (see the Edit HTML + // toolbar's colour control). `style` stays on the props interface so + // already-saved sites keep deserializing cleanly. + return React.createElement('div', { + ref: setRef, + style: { + minHeight: '40px', + outline: selected ? '2px solid #3b82f6' : 'none', + }, + dangerouslySetInnerHTML: { __html: clean }, + }); +}; +``` + +- [ ] **Step 4: Add the matching export assertion** + +Append to `src/components/basic/HtmlBlock.toHtml.test.ts`: + +```ts +test('toHtml never emits the style prop (the other half of the render/export contract)', () => { + const out = (HtmlBlock as any).toHtml( + { code: '

hi

', style: { backgroundColor: '#ff0000', padding: '40px' } }, + '', + ); + expect(out.html).toBe('

hi

'); + expect(out.html).not.toContain('background'); + expect(out.html).not.toContain('40px'); +}); +``` + +- [ ] **Step 5: Run both files and verify they pass** + +Run: `npx vitest run src/components/basic/HtmlBlock` +Expected: PASS, all tests in `HtmlBlock.test.ts`, `HtmlBlock.toHtml.test.ts`, `HtmlBlock.render.test.tsx`. + +- [ ] **Step 6: Commit** + +```bash +cd /workspace/site-builder +git add craft/src/components/basic/HtmlBlock.tsx craft/src/components/basic/HtmlBlock.render.test.tsx craft/src/components/basic/HtmlBlock.toHtml.test.ts +git commit -m "fix(site-builder): HtmlBlock render stops applying style so editor matches published output + +Co-Authored-By: Claude Opus 5 (1M context) " +``` + +--- + +### Task 2: Move `HtmlCodeField` into its own file + +Pure extraction, no behaviour change. Isolating it first keeps Task 3 (the panel) and Task 6 (the toolbar) from both editing `GenericPropsEditor.tsx`. + +**Files:** +- Create: `src/panels/right/styles/HtmlCodeField.tsx` +- Modify: `src/panels/right/styles/GenericPropsEditor.tsx:1-118` + +**Interfaces:** +- Produces: `export const HtmlCodeField: React.FC<{ value: string; onChange: (v: string) => void }>` — renders a `CollapsibleSection` titled "HTML Code" containing an "Edit HTML" button that opens a `Modal` with a `CodeEditor`. + +- [ ] **Step 1: Create the new file** + +Create `src/panels/right/styles/HtmlCodeField.tsx` with the exact `HtmlCodeField` component currently living at `GenericPropsEditor.tsx:28-92`, plus its imports: + +```tsx +import React, { useState } from 'react'; +import { CollapsibleSection, sectionGap } from './shared'; +import { Modal } from '../../../ui/Modal'; +import { CodeEditor } from '../../../ui/CodeEditor'; + +/* "Edit HTML" modal for the HtmlBlock `code` prop. `code` is raw HTML + (potentially many lines, embedded