- 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>
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>
data-animation-delay is stored as a plain seconds string (e.g. '2'); the reveal
script assigned it raw to el.style.animationDelay, which is invalid CSS and no-ops.
Suffix 's' onto bare numbers (leaving '2s'/'200ms' alone) so entrance-animation
delays actually apply. Backend generateCompiledHTML gets the byte-identical change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial re-review found the C1 fix incomplete plus an adjacent
same-class XSS, both reachable via the AI update_props path and
deserialized saved state:
- cssPropsToString only ran sanitizeCssValue on typeof-string values, so a
non-string style value (array/object) with a valid key skipped
sanitization entirely and was template-coerced raw into style="...",
e.g. { color: ['red', '"><img src=x onerror=alert(1)>'] }. Now every
non-number value is coerced with String() and sanitized; numbers stay
raw. sanitizeBreakoutChars also now escapes < and > (previously only ;
and ") as defense-in-depth, since values can reach it from non-string
sources.
- props.tag (Container) and props.level (Heading) were interpolated raw
into the tag position of exported HTML (`<${tag}`, `<${level}`) with no
runtime validation, letting a malicious value break out of the tag
entirely. Both are now allowlisted/clamped against their known-safe sets
(div/section/article/header/footer/main; h1-h6), falling back to
div/h2. Applied in Container's live render + toHtml, Heading's live
render + toHtml, and the typeName==='div' fallback branch in
html-export.ts's renderNode (hit for unresolved/legacy node types).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C1: cssPropsToString emitted the camelToKebab'd style-object KEY
unsanitized while only sanitizing the VALUE. A malicious style key
containing a quote (reachable via AI update_props or deserialized
saved state) could close the style="..." attribute and inject a live
element. Now validates each key against a CSS property/custom-prop
allowlist and drops anything that doesn't match.
C2: buildDataAttrs (html-export.ts) interpolated props.animation and
props.animationDelay directly into data-animation="..."/
data-animation-delay="..." with no escaping, for every exported node.
Now routes both through escapeAttr.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>