Canvas renders plain text via textToInlineHtml (newline -> <br>); toHtml used escapeHtml,
so line breaks typed in the editor vanished on publish (TTM about page: ~100px shorter).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
React assigns '' for a blank style value and a blank longhand after a shorthand
({padding:'20px', paddingTop:''}) wipes the shorthand in the canvas; the exporter
drops blanks. ResolvedRender is the one place node style reaches React, so
normalise there.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Published pages/Preview ship a head reset (*{margin:0;padding:0}, img block,
a inherit, body line-height 1.6 / #1f2937 / 16px); the editor canvas had none,
so h1-h6/p margins, line-height and inherited size/colour differed.
- editor.css: one commented block scoping the reset to .canvas-device-frame,
every selector zero-specificity (:where) so inline styles / class rules win
exactly as when published; editor chrome untouched (snapshot identical).
- tokens.ts: canvas heading/link token rules made zero-specificity too.
- utils/css-reset.ts: single source for the reset declarations (html-export
imports it; test asserts byte-identity with the backend head and that the
editor.css block carries the same declarations).
- scripts/e2e-canvas-parity.mjs (npm run test:browser:canvas): real editor vs
published HTML in Firefox + Chromium, boxes within 1px + computed styles.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
HeroSimple emitted 'background' together with backgroundImage/Size/Position
that were undefined. React assigns '' for those on first render and Chromium
and Firefox then wipe the shorthand's gradient (background-image: none), so
the canvas hero rendered white. Build the style with only the relevant keys
(shared by render and toHtml); exported HTML is byte-identical.
Adds a real-browser (Chromium+Firefox) regression test: npm run test:browser:bg,
and toHtml byte-identity assertions.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
relayFormWiring always emits captcha="1" for relay-wired forms (legacy captcha prop ignored,
old projects with captcha:false load fine). CaptchaMock always shown for relay-wired forms;
FormStylePanel toggle replaced by a read-only note.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXgKV8v9BUrQWXSUW4P
The Navbar renders logoImage only when logoType === 'image', but the
Logo/Text toggle lived in the settings UI removed in 65a10a1 (2026-07-12);
NavStylePanel kept the image picker and never gained the toggle. A picked
logo was stored and silently ignored (editor and published site showed the
text fallback "MySite"; seen on olsdigital.com).
- Logo Type Text/Image toggle in the Navbar Logo section
- picking an image sets logoImage + logoType:'image' in one undo step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
client_id per editor load (memory only), already_saved, save warnings via notices,
upload_max_bytes enforced client-side with the server error text on 413,
project_max_bytes warning, headCode in the Preview font scan, pinned backend heads.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The section library now calls closeSheet() after inserting a section (or a
global section). It mounts in CanvasOverlays, beside EditorShell, so it could
not reach the MobileChromeProvider inside the shell; the provider moves up to
App around the shell and the overlays so both share one sheet state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Esc was only claimed by select.parent for a single node, so a multi-selection
could not be dropped from the keyboard. Add select.clear (Esc, >1 selected)
through the existing dispatcher, which already skips text fields and open
dialogs. The context menu's Duplicate/Delete now run the multi-selection core
commands (edit.duplicate / edit.delete) when the clicked node is part of a
multi-selection, instead of acting on the first node only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The business-profile wizard was forced on an empty canvas even when
Sitesmith was locked (paid addon), capped or switched off. The decision now
waits for the entitlement and skips the wizard unless the user can chat
(OK_MONTHLY / OK_BONUS); a failed entitlement falls back to the modal.
useSitesmith reports an ok:false entitlement as an error so it cannot hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Title-length, missing meta description and noindex-landing checks carry
fix: 'page-settings', but Fix only closed the list. It now shows that page's
PageSettingsModal in place of the checklist; closing it finishes the Fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On mobile the collapsed topbar had no route to Version history, Export ZIP,
Business profile or the command palette. Add a 'Version history' item (only
when the site.history command is registered, run through the registry) and a
'More commands…' item that opens the command palette.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zone previews render the exported HTML, whose <style data-wsb-responsive>
@media rules evaluate against the browser viewport, so tablet/mobile modes
showed desktop styling for header/footer. ZonePreview now applies the active
device's override rules unconditionally (applyResponsiveCssForDevice) and
drops the rest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Published mobile stacking is the global RESPONSIVE_CSS rule matching the
DESKTOP inline style, so un-stacking Columns on mobile under a stacked tablet
(styleMobile.flexDirection 'row') publishes un-stacked. The editor's
device-emulation copy matched the resolved inline style and showed it stacked.
RenderNode now marks nodes with a flexDirection/display override with
data-wsb-base-stack (baseStacksOnMobile) and editor.css keys off it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export/published :root token CSS replaced a blank/whitespace token with its
default; the editor canvas emitted an empty custom property, invalidating
every var() that read it. Share one buildTokenVarDecls between the canvas
and the export so both apply the same fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preview (TopBar handlePreview + history snapshot preview) gated the
animation CSS and no-JS fallback on the page body only, while the published
head (generateCompiledHTML) gates on the composed header+body+footer. Add
exportComposedPreviewHtml (head scanned over composed content for fonts and
animation; body replaced with composed HTML + one reveal script) and use it
from both Preview paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PageContext.loadState deserializes on setTimeout(0); a save landing in that
window serialized the previous page's still-mounted canvas into the new active
page's slot. loadState now records the pending state and sendSave uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
syncInstances rebuilds an instance's subtree from the master, so a node
dropped, pasted, duplicated or moved into an instance vanished from every page
on the next master edit. Chosen approach: BLOCK (not preserve-on-sync):
- every resolver component's Craft canMoveIn/canMoveOut refuses a canvas inside
an instance (canvas DnD, Layers DnD, actions.move); synthetic shell-inner
nodes now carry the component's rules instead of hardcoded allow-all
- insertBlock (BlocksPanel / EmptyContainerAdd / palette), paste (menu +
shortcut), duplicate (menu + shortcut) and Sitesmith patch/section replace
redirect to after the instance or refuse; no empty-container '+' inside one
The instance root still moves/duplicates as a whole; content overrides are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each replaced site content under an open master zone: Templates cleared the zone
and committed the template's first section as the new master (rewriting every
instance); replaceAllPages left the zone without committing; history Restore
loaded page content around it. Templates button/command/overflow item and the
history Restore buttons are disabled with a 'Click Done first' notice, and the
handlers (TemplateModal.handleLoad, replaceAllPages -> false, Sitesmith site,
Reset site, restoreSite/restorePage) refuse as a hard guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zone's live master was neither in extras nor in the stored pages until
Done, so a save from inside the zone succeeded without it and cleared dirty;
closing the tab lost the edit (publish shipped stale instances). The save
payload now carries the master committed into the library and every stored
page/header/footer synced, computed exactly as Done would without touching the
editor (PageContext.globalZoneSaveView).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Legacy Navbar nodes store backgroundColor '#ffffff' + textColor '#3f3f46'; the
editor took the logo colour from the Site Design text (light on a dark design)
while the export already uses logoColor || textCol. Same dark-palette class as
finding 8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
snapshotToPages mapped only id/name/slug/craftState/seo and
planPageRestore built/replaced pages without nav, so 'Restore site' and
'Restore one page' dropped every page's nav (show/parentId/label). The
NavSync effect then rewrote the follow-pages header: hidden pages
reappeared, dropdown nesting flattened, custom labels reverted.
SnapshotPage now carries `nav?: PageNav`, accepted only when it is a
plain non-null, non-array object (shallow-copied), else omitted.
planPageRestore restores it in id/slug replace mode (like seo) and on
append. HistoryModal already passes these rows straight to
setPagesCraftState, which keeps nav, so no wiring change was needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tablet/mobile Alignment and Vertical-alignment overrides published only the
raw text-align/justify-content declarations, while the editor (which renders
from the device-resolved style) also applied the derived flex layout
(display:flex;flex-direction:column;align-items). Desktop-centred containers
stayed centred on mobile, mobile-only centring/vertical-align did nothing, and
in boxed mode the flex sits on the inner div which had no scope at all.
- Container/Section: single containerLayout()/sectionLayout() helper used by
render, toHtml and a new static responsiveLayout hook.
- responsiveCssForNode takes an optional DerivedLayoutFn: per device it diffs
the layout derived from the resolved style against what already applies
(desktop for tablet, tablet for mobile), emits changes (and resets for
dropped keys), and scopes boxed-mode inner rules as [data-wsb-r=x]>div.
- html-export passes the component's responsiveLayout hook.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nodes saved before the component-tokens work (9ab0102) store light surface
hexes -- ContactForm inputBg '#ffffff', SubscribeForm style.backgroundColor
'#f8fafc', Testimonials cardBg '#f8fafc' -- while the text on those surfaces
became var(--wsb-text)/var(--wsb-muted). On a dark Site Design palette (light
text token, e.g. Agency #fafafa) that rendered white-on-white.
Text on a STORED light surface (hex #rgb/#rrggbb or rgb()) now keeps the
pre-token dark hex (input #1f2937, subscribe heading #1f2937, testimonial
quote #374151 / name #18181b / title #64748b); absent, token or dark surfaces
keep the token text. Stored props are never rewritten. Applied in both the
editor render and toHtml export.
Generalises Navbar's isLightNavBg into utils/color.ts (isLightColor /
textOnSurface, plus parseColor moved out of publish-checks and re-exported);
Navbar and autoTextColor now share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
injectAttrs() skipped leading <style> blocks and then inserted the
responsive/visibility/animation attrs before the first '>'. Relay forms
(ContactForm, SubscribeForm, FormContainer with a recipient) start their
toHtml with `<!--WHP-FORM ...-->`, so the attrs landed inside the marker
(`thankyou=""-- data-wsb-r="...">`): the comment was left unterminated,
Preview swallowed the form, and the publish step refused the whole site.
injectAttrs now skips any leading run of whitespace, <style> blocks and
<!--...--> comments (any order, repeated) and targets the first real
element; a string that is only such a run is returned unchanged. The
marker wire format is untouched.
Tests port the backend's strict marker regex from FormRelayRewrite.php
and export each relay form with a recipient + styleMobile/styleTablet +
hideOnMobile + animation through exportBodyHtml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save/load keep 30s; publish of a large site legitimately runs longer and was
reported as failed at 30s while the server kept going.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preview and history Preview opened the composed page (raw site head code
included) as a panel-origin blob: tab, so a customer's <script> ran with the
viewer's panel session and CSRF token (root included). Both now open an empty
shell tab inside the click and render the page in an <iframe srcdoc> with
sandbox="allow-scripts allow-popups allow-forms" (no allow-same-origin):
an opaque origin, no opener. Tests assert the sandbox attribute and scan the
source for any remaining text/html blob / document.write / blob tab path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preset buttons in BoxModelSection / BorderEffectsSection now write through
the discrete setters (setPropStyles / setPropStyle), so a keyboard-activated
preset (no pointerup or focusout to close the burst) never merges into the
previous edit of the same key. SpacingControl gains an optional onPreset
for this; margin/padding presets are also one undo step for all four sides
in the media panels.
Refs #7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- save, publish and load run under a 30s AbortController timeout (raced,
so a stalled body read can't hang either); save/publish resolve to
{ success:false, error:'timeout', message } and load rejects with a
readable Error, so a hung request no longer blocks the save queue
- readApiResult falls back to obj.error when obj.message is absent, so
PHP texts like 'Another save of this site is still in progress...' are
shown instead of 'server returned HTTP 503'
Refs #4
Refs #47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Single-key shortcuts (1/2/3 device switch) are skipped while any editor
dialog or modal is open (role=dialog/alertdialog, aria-modal, dialog[open],
.modal-backdrop) or focus is inside one. Dialog-like page content inside
the canvas frame does not count.
Refs #39
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>