fix(site-builder): final whole-branch review fixes

C1: HtmlBlock's PURIFY_CONFIG omitted 'style' from ALLOWED_ATTR, so the
toolbar colour picker added in this branch was silently deleted by
DOMPurify -- issue #2 was regressed, not fixed. Adds style/id plus table
tags, with tests pinning the markup path in both render and toHtml.

I3: PagesPanel's three confirmation states were not mutually exclusive;
cancelling delete revealed an unbidden reset prompt on a destructive action.

I5: orphan repair logged at console.warn, which the new console buffer
cannot see -- the reporter would never capture the most diagnostic signal
for the still-unreproduced drop bug. Also aligns useWhpApi's initial-load
failure handling with loadState's fallback.

I7: corrects comments (and the design spec) that asserted an orphan
"renders somewhere on the canvas", which a mid-plan audit disproved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 12:47:23 -07:00
co-authored by Claude Opus 5
parent 3dd6b54a35
commit 69e61ab4b2
12 changed files with 317 additions and 32 deletions
@@ -122,7 +122,12 @@ describe('loadState (via switchPage) repairs an orphaned node before handing it
),
);
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// I5 (review): the orphan-repair log must be console.error, not
// console.warn -- console-buffer.ts (feeding the in-builder issue
// reporter) only patches console.error, and this reattach signal is the
// single most diagnostic clue for the still-unreproduced "elements drop
// off the canvas" report.
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
deserializeMock.mockClear();
// The real switch-into-a-stored-page path.
@@ -137,11 +142,12 @@ describe('loadState (via switchPage) repairs an orphaned node before handing it
expect(parsed.stray.parent).toBe('ROOT');
// The observable signal that repair actually ran, not just that the
// orphan happened to be absent for some unrelated reason.
expect(warnSpy).toHaveBeenCalled();
expect(warnSpy.mock.calls[0][0]).toContain('reattached');
// orphan happened to be absent for some unrelated reason. (React's own
// act()-environment warnings also go through console.error in this
// harness, so search all calls rather than assuming index 0.)
expect(errorSpy.mock.calls.some((call) => String(call[0]).includes('reattached'))).toBe(true);
warnSpy.mockRestore();
errorSpy.mockRestore();
unmount();
});
});
+18 -7
View File
@@ -371,19 +371,30 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
/** Load a craft state into the Frame.
*
* Every state goes through `repairOrphanNodes` first: a node that no
* parent lists is invisible to Layers and unselectable on the canvas, so
* it can neither be moved nor deleted. Reattaching it to the end of ROOT
* makes it an ordinary child the user can select and delete. Cheap
* (single JSON round-trip) and a no-op -- returning the identical string
* -- for the overwhelmingly common healthy case. */
* Every state goes through `repairOrphanNodes` first: a node present in
* the serialized state but not reachable from ROOT via `nodes`/
* `linkedNodes` is never instantiated by Craft.js's `<Frame>` at all --
* it doesn't render, so it isn't merely unselectable, it's invisible and
* otherwise unrecoverable. Reattaching it to the end of ROOT makes it an
* ordinary child the user can see, select and delete. Cheap (single JSON
* round-trip) and a no-op -- returning the identical string -- for the
* overwhelmingly common healthy case.
*
* Note this orphan-node repair is a different mechanism from the
* originally-reported symptom (a *visible* element on the canvas that
* can't be selected or deleted) -- that report is still unreproduced;
* see `orphan-repair.ts` for detail. */
const loadState = useCallback(
(craftState: string | null, fallback: string) => {
setTimeout(() => {
const source = craftState || fallback;
const { state, repaired } = repairOrphanNodes(source);
if (repaired.length > 0) {
console.warn(
// I5: console-buffer.ts only patches console.error, and this
// reattach signal is the single most diagnostic clue for the
// still-unreproduced "elements drop off the canvas" report -- it
// must reach the in-builder issue reporter's console buffer.
console.error(
`[site-builder] reattached ${repaired.length} unreachable node(s) to the page root:`,
repaired.join(', '),
);