Select text inside a selected Heading or Text block and a floating
toolbar offers B / I / U / Link / Unlink (execCommand; mousedown is
prevented so the selection survives; mod+b/i/u handled on the editable).
Storage: new `html` prop holding sanitised inline HTML, with `text` kept
as the plain mirror for AI summaries, search and legacy content.
Unmarked edits store html '' so legacy nodes keep their shape. The
allowlist (b strong i em u a br span; style=color only, hex/rgb/
var(--wsb-*); href via DOMPurify URI check AND safeUrl, unsafe links
unwrapped; target only _blank with forced rel=noopener) runs on commit,
on canvas render and again at export. If `text` was changed without
`html` (Sitesmith update_props), the stale html is ignored.
Each edit commits once (blur, or deselect if no blur came first) as one
setProp = one undo step; the old blur+deselect double commit is gone.
Heading colour default now tokenRef('text'). Sitesmith canvas summary
skips `html`.
Refs #46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame gets data-device; RenderNode mirrors hide flags and selection onto the
dom; editor.css emulates the exported breakpoints inside the narrowed frame.
Also: data-wsb-overridden counts only set values, responsive scope ids are
zone-prefixed (h-/b-/f-) so ROOT no longer collides, and comment delimiters
are stripped from responsive declarations.
Refs #37
Restyle Craft's insertion bar via the Editor indicator prop and mirror the
live placement onto the DOM (data-drop-target/-error/-empty, is-dragging).
Refs #43
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Colour controls offer the site palette first, show the resolved colour for
var(--wsb-*) values with a token chip, and keep the stored reference.
autoTextColor resolves tokens. Site Design gains six one-click palettes.
Refs #44
Text/number/hex inputs keep local state and commit on 150ms idle, blur or
Enter through the continuous setters; native colour pickers get an
onContinuous path; ButtonStylePanel colour routes through the shared
setters. LayersPanel collectors return structural signatures/primitives
so hover and unrelated prop edits no longer rebuild the tree.
Refs #40
- utils/breakpoints.ts: single source for 767/768/991/992; head
VISIBILITY/RESPONSIVE CSS built from it (flex-stacking rule moved from
768 to the 767 mobile boundary).
- utils/responsive-style.ts: styleTablet/styleMobile partial overrides,
cascade resolution, pure setters, scoped @media CSS per node.
- state/DeviceContext.tsx: real DeviceProvider (owns the one-time mobile
forcing from EditorShell); TopBar, overflow menu, ReportIssueModal and
Canvas read useDevice() instead of props.
- editor/RenderNode.tsx: resolves the effective style for the current
device in one place and marks overridden nodes with data-wsb-overridden.
Craft passes an opaque <DefaultRender/> to onRender, so the component
element is rebuilt with the resolved style (cloneElement cannot work).
- utils/html-export.ts: hidden nodes skipped; overrides emitted as one
body-level <style data-wsb-responsive> block + data-wsb-r scope attr;
injectAttrs skips leading <style> elements.
- test-utils/editorHarness.tsx: optional onRender/wrap options.
Refs #45
Refs #37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CollapsibleSection persists open/closed per section title via local-prefs.
Container/Section/Columns drop the shorthand padding preset grid; the
per-side Padding control expands a legacy shorthand and writes all four
longhands in one undo step (new SpacingControl onChangeAll).
Refs #35
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Make var(--wsb-*) a first-class style value in the editor:
- utils/tokens.ts: TokenKey, TOKEN_VARS (moved from html-export.ts, same
order/content as the backend), COLOR_TOKENS palette order, tokenRef,
isTokenRef, tokenKeyOf, resolveTokenValue and buildCanvasTokenCss, which
defines the 16 vars + token base CSS scoped to .canvas-device-frame
(never :root) with every value through cssValue().
- editor/CanvasTokenStyle.tsx: <style data-wsb-canvas-tokens> driven by
useSiteDesign(), mounted in App inside SiteDesignProvider, so design
edits recolour token-backed values on the canvas instantly.
- panels/right/styles/TokenSwatchRow.tsx: palette-first swatch row that
emits token references; .token-swatch styles appended to editor.css.
- html-export.ts imports TOKEN_VARS; export output pinned byte-identical
to the pre-move baseline by hash in html-export.test.ts.
Refs #44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add an exported block registry (keywords, search, insertBlock) consumed by
BlocksPanel, a NoticeProvider/NoticeHost toast stack, a guarded localStorage
prefs helper, a ProjectExtras store persisted as project.json 'extras', and a
CanvasOverlays mount point.
Refs #38
Refs #41
Refs #49
Refs #48
Refs #57
Refs #58
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A manual Save during Publish sent a second save with the same base_rev; the
loser 409'd against its own tab (and could abort the publish).
- useWhpApi: every save runs through one in-flight promise (SaveGuard
saveInFlight). A save requested while one is out waits, then sends only
if still dirty, with the rev the first returned; payload is read from the
editor at send time. Forced (empty/overwrite) saves always send.
- Save buttons are disabled while publishing.
- Pill: a conflict reads 'Unsaved changes' (modal open) from Save and from
Publish alike; a never-saved site (load rev null) reads 'Not saved yet'.
- Autosave interval reads runSave through a ref, so editing can't keep
restarting it and starve autosave.
- Fix stale 'Throws like exportBodyHtml' comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft records actions.deserialize as an undoable step and nothing cleared
history, so Undo right after load returned the canvas to the blank pre-load
Frame (autosave would then save it), and Undo after a page switch put the
previous page's content into the current slot.
- clear history after the load-time deserialize and after every page/
header/footer switch (in-place AI replacement of the same slot keeps it)
- continuousSetProp opens a burst only when its first call actually
changed something, so it never merges into an entry it did not create
- the empty-save banner's Undo is disabled when there is nothing to undo
Real-editor tests: load, page/header switch, no-op-first burst, banner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Track dirty from Craft node changes and page/design/header/footer state, cleared
only by a successful save. Persistent Unsaved/Saving/Saved/Published/Save failed
pill, beforeunload prompt while dirty or saving, one persistent error notice,
resp.ok/non-JSON handling for save and publish, and publish refuses to run
after a failed save. Draft badge no longer flips on a no-op autosave.
Refs #30
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
I-1: WebhookSecretField now mounts with key={selectedId}. GuidedStyles renders
FormStylePanel with no key, so a selection change re-rendered rather than
remounted it and React kept `draft`/`status`. Since a failed store deliberately
retains the draft, clicking a second contact form showed node A's raw secret in
node B's field, and the next blur assigned the returned secret_id to the wrong
form (burning a slot against the per-site cap). The status banner leaked the
same way. Both pinned by the reviewer's repro sequence.
I-2: the button is "Clear", not "Remove", and says so -- nothing deletes a
stored key file, so the 50-per-site cap counts stores-ever. Labelling it Remove
told the customer they had reclaimed a slot right up until the 429 that said
otherwise. Actually deleting the file is Task 8's territory.
M-1: inline warning when the webhook URL is blank or not absolute https. The
publish step does refuse these, but into an error_log the customer never reads.
Warning only -- isHttpsWebhookUrl never edits the value or blocks the publish,
since either would trade a loud server-side refusal for a silently inert form.
M-2: the BYTE-IDENTITY test now compares against a literal marker captured by
executing the emitter at 071f3447, not against another head-revision output. The
self-comparison could only catch a drift affecting one side; a uniform one
passed it. Verified: a uniform `<!-- WHP-FORM` drift now fails this assertion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ContactForm gains four craft props (destinationType, webhookUrl,
webhookSecretId, webhookAuthMode), all present in craft.props defaults so
FormStylePanel's `nodeProps.X !== undefined` gates actually render their
controls. The controls live in FormStylePanel (RightPanel renders only
GuidedStyles, so related.settings would be dead UI).
relayFormWiring widens the marker to optionally carry type/url/secret/
authmode BETWEEN `id` and `recipient`, which is where FormRelayRewrite.php's
parser looks. A marker with no type is byte-identical to what shipped before
-- pinned by a test that diffs an explicit-email form against one with no
destination props at all, since every already-published site depends on that
shape continuing to provision an email endpoint.
Every optional attribute value goes through one escaping site (markerAttr ->
escapeAttr); type and authmode are additionally allowlisted, so a case-drifted
"Bearer" reaches the relay as the exact literal it compares against instead of
being silently downgraded to unsigned.
The raw shared secret is never a prop: it is held in WebhookSecretField's
local state, POSTed to /api/form-webhook-secret.php on blur, and only the
returned opaque id is persisted. The field is write-only (set / replace /
remove, never view) because the endpoint has no read route, and the endpoint's
429 cap message is surfaced verbatim so a customer can act on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Custom HTML block renders arbitrary markup on the published site.
Scripts and event handlers are stripped, but forms, iframes, and
images survive and can send data elsewhere. Add a plain, always-shown
advisory (no content detection) alongside the existing wrapper-styling
note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>