Mobile guard: HtmlBlock wrapper never gets a box; phone net is media + per-table/pre scroll only

Review of bb4fb75: overflow-x:auto on [data-wsb-html] forced overflow-y:auto (clipped dropdowns/shadows),
broke position:sticky inside customer HTML, and display:block!important merged multi-root blocks that are
direct flex/grid items. Now the wrapper stays display:contents on every viewport and no rule targets it;
at <=767px only its descendants are touched: media max-width:100% (img/video height:auto), and a table or
<pre> becomes its own sideways scroller (not when already inside an inline overflow wrapper). No
overflow-wrap:anywhere (it shrank min-content); body{overflow-wrap:break-word} inherits through the wrapper.
Deliberately wide customer markup (fixed widths, px grids) is clipped as before; the publish warning is
its guard, and its wording now says the excess is cut off. width= attributes are no longer flagged (they
size only media and tables, which the net handles). stripMediaBlocks lowercases once.

Browser suite: htmlblocks probes at every width (wrapper display:contents, sticky sticks, absolutely
positioned dropdown visible on top, multi-root block = separate flex items / grid cells, bare table
scrolls on its own); new htmlwide doc asserts wide blocks stay clipped and the page never scrolls sideways.
All probes fail on bb4fb75's net and pass now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-11-01 15:09:22 -08:00
1 parent bb4fb7548b
commit cfeeef37ce
10 files changed
+175 -89

No files matched your search

+8 -5
View File
@@ -465,16 +465,19 @@ Every component has a static `toHtml(props, childrenHtml)` method. The `html-exp
- **Mobile guard (real browsers):** `npm run test:browser:mobile-guard` (Chromium + Firefox, headless; jsdom has no
layout). Exports representative pages (navbar, columns, card grids, images, long words, wide customer HtmlBlocks)
and asserts at 360/390/768/1280: no horizontal overflow (body.scrollWidth AND the right edge of every visible box
and text line -- the head's `html,body{overflow-x:clip}` hides overflow from documentElement.scrollWidth), columns
and card grids stack on phones and keep their columns on tablet, navbar links wrap, an intentional
`overflow-x:auto` table still scrolls, and desktop is box- and pixel-identical with the mobile hooks removed.
and text line -- the head's `html,body{overflow-x:clip}` hides overflow from documentElement.scrollWidth) and no
sideways page scroll, columns and card grids stack on phones and keep their columns on tablet, navbar links wrap,
tables scroll on their own, sticky/dropdowns/multi-root flex+grid items inside HtmlBlocks still work, deliberately
wide customer HTML stays clipped (not reflowed), and desktop is box- and pixel-identical with the hooks removed.
Set `CHROMIUM_PATH` / `FIREFOX_PATH` if the Playwright-pinned browsers are not installed; record/compare a desktop
baseline across commits with `MOBILE_GUARD_RECORD=<file>` (on the base commit) / `MOBILE_GUARD_BASELINE=<file>`.
Run it whenever a component's `toHtml`, the exporter, or the head CSS changes.
- **Mobile hooks:** the head's `<=767px` block (RESPONSIVE_CSS in `utils/html-export.ts`, mirrored byte-for-byte by
WHP `generateCompiledHTML`) targets `[data-wsb-grid]` (component card grids -> one column), `body` (long words
wrap) and `[data-wsb-html]` (the inline `display:contents` wrapper the exporter puts around HtmlBlock output; on
phones a sideways-scrolling box). A new component with an inline multi-column grid should carry `data-wsb-grid`.
wrap) and descendants of `[data-wsb-html]` (the inline `display:contents` wrapper the exporter puts around
HtmlBlock output: media shrink, tables/`<pre>` scroll on their own). Never give that wrapper a box or overflow: it
clipped dropdowns, broke sticky and merged flex/grid items. A new component with an inline multi-column grid
should carry `data-wsb-grid`.
- **WHP integration:** Deploy to staging, verify save/load, verify PHP wrapper injection
## Development Notes
+37 -24
View File
@@ -60,39 +60,51 @@ export function componentsState(): string {
return JSON.stringify(nodes);
}
/** Customer raw HTML that is wider than a phone. Keyed by node id so the runner can name offenders. */
export const HTML_BLOCKS: Record<string, string> = {
/** Customer raw HTML the phone net makes fit: media shrink, tables and <pre> scroll on their own, long words wrap. */
export const HTML_FITS: Record<string, string> = {
hbIframe: '<iframe width="800" height="450" src="about:blank" title="video"></iframe>',
hbImg: '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="1200" height="400" alt="">',
hbTableBare: '<table id="bare-table"><tr>' + Array.from({ length: 10 }, (_, i) => `<td>Column_heading_${i}</td>`).join('') + '</tr></table>',
hbPre: '<pre>const reallyLongLine = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";</pre>',
hbUrl: '<p>See https://example.com/a/really/long/path/that/never/breaks/because/it/has/no/spaces/at/all/ok</p>',
// Deliberate horizontal scroller: must STILL scroll (the net must not flatten or clip it).
hbScrollTable: '<div id="intentional-scroll" style="overflow-x:auto"><table style="width:1000px"><tr><td>a</td><td>b</td><td>c</td></tr></table></div>',
// Phone probes: none of these may be clipped, unstuck or merged by the net.
hbSticky: '<div id="sticky-box" style="height:1200px;background:#f5f5f5"><div id="sticky-probe" style="position:sticky;top:0;background:#ffeb3b;padding:8px">Sticky bar</div></div>',
hbDropdown: '<div style="position:relative"><button type="button">Menu</button><ul id="dropdown-probe" style="position:absolute;top:100%;left:0;z-index:10;list-style:none;margin:0;padding:8px;background:#fff;box-shadow:0 4px 12px #0004"><li>One</li><li>Two</li><li>Three</li><li>Four</li><li id="dropdown-last">Last item</li></ul></div>',
};
/** Customer raw HTML wider than a phone by design (fixed widths / px grids): clipped as before, never a sideways page. */
export const HTML_WIDE: Record<string, string> = {
hbFixed: '<div style="width:1100px;background:#eee;padding:20px">A fixed 1100px banner</div>',
hbGridPx: '<div style="display:grid;grid-template-columns:repeat(3,300px);gap:20px"><div style="padding:16px;border:1px solid #ccc">One</div><div style="padding:16px;border:1px solid #ccc">Two</div><div style="padding:16px;border:1px solid #ccc">Three</div></div>',
hbGridFr: '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:32px"><div style="padding:24px"><h3>Multi-Brand Portfolio</h3></div><div style="padding:24px"><h3>Hosting</h3></div><div style="padding:24px"><h3>StreamerTools</h3></div><div style="padding:24px"><h3>Consultancy</h3></div></div>',
hbStyleFlex: '<style>.cards{display:flex;gap:16px}.card{flex:0 0 360px;padding:16px;border:1px solid #ccc}</style><div class="cards"><div class="card">A</div><div class="card">B</div><div class="card">C</div></div>',
hbMinWidth: '<div style="min-width:900px">min-width 900px content</div>',
hbIframe: '<iframe width="800" height="450" src="about:blank" title="video"></iframe>',
hbImg: '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="1200" height="400" alt="">',
hbTableBare: '<table><tr>' + Array.from({ length: 10 }, (_, i) => `<td>Column_heading_${i}</td>`).join('') + '</tr></table>',
hbPre: '<pre>const reallyLongLine = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";</pre>',
hbUrl: '<p>See https://example.com/a/really/long/path/that/never/breaks/because/it/has/no/spaces/at/all/ok</p>',
// Deliberate horizontal scroller: must STILL scroll (the net must not flatten or clip it).
hbScrollTable: '<div id="intentional-scroll" style="overflow-x:auto"><table style="width:1000px"><tr><td>a</td><td>b</td><td>c</td></tr></table></div>',
};
export function htmlBlocksState(): string {
function htmlState(blocks: Record<string, string>, probes: boolean): string {
const nodes: N = {};
const kids: string[] = [];
nodes.intro = node('TextBlock', { text: 'Before the custom HTML' }, [], { parent: 'ROOT' }); kids.push('intro');
for (const [id, code] of Object.entries(HTML_BLOCKS)) { nodes[id] = node('HtmlBlock', { code }, [], { parent: 'ROOT' }); kids.push(id); }
// Desktop-identity probes: a boxed wrapper would merge these roots into ONE flex item, stop the root
// margins collapsing with their neighbours', and break the inline run. display:contents must not.
nodes.flexRow = node('Container', { tag: 'div', style: { display: 'flex', flexDirection: 'row', gap: '12px' } }, ['hbRoots', 'rowText'], { parent: 'ROOT' });
nodes.hbRoots = node('HtmlBlock', { code: '<div style="flex:1;background:#eef">root one</div><div style="flex:2;background:#fee">root two</div>' }, [], { parent: 'flexRow' });
nodes.rowText = node('TextBlock', { text: 'flex sibling' }, [], { parent: 'flexRow' });
kids.push('flexRow');
nodes.marginText = node('TextBlock', { text: 'margin neighbour', style: { marginBottom: '30px' } }, [], { parent: 'ROOT' }); kids.push('marginText');
nodes.hbMargins = node('HtmlBlock', { code: '<h2 style="margin:40px 0">Collapsing margins</h2>' }, [], { parent: 'ROOT' }); kids.push('hbMargins');
nodes.inlineRun = node('Container', { tag: 'div', style: {} }, ['hbInline', 'inlineBtn'], { parent: 'ROOT' });
nodes.hbInline = node('HtmlBlock', { code: '<span>inline custom</span>' }, [], { parent: 'inlineRun' });
nodes.inlineBtn = node('ButtonLink', { text: 'inline button' }, [], { parent: 'inlineRun' });
kids.push('inlineRun');
for (const [id, code] of Object.entries(blocks)) { nodes[id] = node('HtmlBlock', { code }, [], { parent: 'ROOT' }); kids.push(id); }
if (probes) {
// A multi-root HtmlBlock that is a direct flex / grid item: its roots must stay separate items/cells, at every
// width (and a boxed wrapper would also move the margin and inline probes at desktop).
nodes.flexRow = node('Container', { tag: 'div', style: { display: 'flex', flexDirection: 'row', gap: '12px' } }, ['hbRoots', 'rowText'], { parent: 'ROOT' });
nodes.hbRoots = node('HtmlBlock', { code: '<div class="root-probe" style="flex:1;background:#eef">root one</div><div class="root-probe" style="flex:2;background:#fee">root two</div>' }, [], { parent: 'flexRow' });
nodes.rowText = node('TextBlock', { text: 'flex sibling' }, [], { parent: 'flexRow' });
kids.push('flexRow');
nodes.gridBox = node('Container', { tag: 'div', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' } }, ['hbCells'], { parent: 'ROOT' });
nodes.hbCells = node('HtmlBlock', { code: '<div class="cell-probe" style="background:#efe">cell one</div><div class="cell-probe" style="background:#fef">cell two</div>' }, [], { parent: 'gridBox' });
kids.push('gridBox');
nodes.marginText = node('TextBlock', { text: 'margin neighbour', style: { marginBottom: '30px' } }, [], { parent: 'ROOT' }); kids.push('marginText');
nodes.hbMargins = node('HtmlBlock', { code: '<h2 style="margin:40px 0">Collapsing margins</h2>' }, [], { parent: 'ROOT' }); kids.push('hbMargins');
nodes.inlineRun = node('Container', { tag: 'div', style: {} }, ['hbInline', 'inlineBtn'], { parent: 'ROOT' });
nodes.hbInline = node('HtmlBlock', { code: '<span>inline custom</span>' }, [], { parent: 'inlineRun' });
nodes.inlineBtn = node('ButtonLink', { text: 'inline button' }, [], { parent: 'inlineRun' });
kids.push('inlineRun');
}
nodes.outro = node('TextBlock', { text: 'After the custom HTML' }, [], { parent: 'ROOT' }); kids.push('outro');
nodes.ROOT = node('Container', { tag: 'div', style: { padding: '20px' } }, kids);
return JSON.stringify(nodes);
@@ -106,5 +118,6 @@ function compose(state: string, title: string, showMobileMenu: boolean): string
(window as any).wsbMobileGuardDocs = (): Record<string, string> => ({
components: compose(componentsState(), 'Components', false),
htmlblocks: compose(htmlBlocksState(), 'Custom HTML', true),
htmlblocks: compose(htmlState(HTML_FITS, true), 'Custom HTML', true),
htmlwide: compose(htmlState(HTML_WIDE, false), 'Wide custom HTML', true),
});
+83 -15
View File
@@ -5,26 +5,30 @@
*
* npm run test:browser:mobile-guard
*
* Builds two representative documents with the REAL exporter (scripts/e2e-mobile-guard-entry.ts):
* Builds three representative documents with the REAL exporter (scripts/e2e-mobile-guard-entry.ts):
* components - Navbar with 8 links, hero, ColumnLayout 2/3/4/30-70, FeaturesGrid, Testimonials,
* NumberCounter, Gallery, PricingTable, ReviewsStrip, a 1600px image, long word + URL
* htmlblocks - customer HtmlBlocks wider than a phone (fixed width, px grid, 1fr grid, <style> flex
* cards, min-width, iframe, img, bare table, pre, URL) + one intentional overflow-x:auto table
* htmlblocks - customer HtmlBlocks the phone net makes fit (iframe, img, bare wide table, pre, URL), an
* intentional overflow-x:auto table, and probes: sticky bar, absolutely positioned dropdown,
* multi-root blocks as direct flex items / grid cells, collapsing margins, an inline run
* htmlwide - customer HtmlBlocks wider than a phone BY DESIGN (fixed width, px grid, 4x1fr grid, <style>
* flex cards, min-width): clipped as before, the publish warning is their guard
* and measures them at 360 / 390 / 768 / 1280.
*
* Asserted:
* - documentElement.scrollWidth AND body.scrollWidth <= viewport (the head sets html,body{overflow-x:clip},
* so these alone stay green while content is CLIPPED -- hence also:)
* - the right edge of every visible element box and text line <= viewport, unless it sits inside a
* scroll/clip container that itself fits (that content is reachable / intentionally clipped).
* components: at every width. htmlblocks: at <=767px (the HtmlBlock net is mobile-only).
* - documentElement.scrollWidth <= viewport and the page cannot be scrolled sideways (every doc, every width)
* - body.scrollWidth <= viewport and the right edge of every visible element box and text line <= viewport,
* unless it sits inside a scroll/clip container that itself fits (reachable / intentionally clipped):
* components at every width, htmlblocks at <=767px (the HtmlBlock net is phone-only). htmlwide: the wide
* content is still clipped (not reflowed) at <=767px.
* - ColumnLayout rows stack (one column per row) at <=767px; component card grids are one column at
* <=767px and keep more than one column at 768 (tablet).
* - the Navbar fits the viewport and wraps its links onto more than one row at 360/390.
* - the intentional overflow-x:auto table still scrolls at every width.
* - the intentional overflow-x:auto table still scrolls; the bare wide table scrolls on its own on phones.
* - htmlblocks probes, every width: the HtmlBlock wrapper is display:contents, sticky still sticks, the
* dropdown is visible on top, multi-root blocks stay separate flex items / grid cells.
* - desktop (1280) is unchanged by the mobile hooks: every element box AND the full-page screenshot are
* identical after removing them in the page (unwrap [data-wsb-html], drop data-wsb-grid). The htmlblocks
* page carries probes a boxed wrapper would move (multi-root flex item, collapsing margins, inline run).
* identical after removing them in the page (unwrap [data-wsb-html], drop data-wsb-grid).
* Optional cross-commit proof of the same thing:
* MOBILE_GUARD_RECORD=<file> write the 1280 boxes (run this on the base commit)
* MOBILE_GUARD_BASELINE=<file> assert 1280 boxes equal that recording
@@ -116,6 +120,54 @@ function measure() {
};
}
/** Runs in the page: can the page be scrolled sideways? (scrollX after asking for 5000px.) */
function sidewaysScroll() {
const y = window.scrollY;
window.scrollTo(5000, y);
const x = window.scrollX;
window.scrollTo(0, y);
return x;
}
/** Runs in the page (htmlblocks doc): the net must not box, clip, unstick or merge customer HTML. */
function probes() {
const out = {};
out.wrapperDisplays = [...new Set([...document.querySelectorAll('[data-wsb-html]')].map((w) => getComputedStyle(w).display))];
// Multi-root block as direct grid items: two separate cells side by side.
const cells = [...document.querySelectorAll('.cell-probe')].map((c) => c.getBoundingClientRect());
out.cellsSeparate = cells.length === 2 && Math.abs(cells[0].top - cells[1].top) < 1 && cells[1].left > cells[0].right;
// Multi-root block as direct flex items: no box between the roots and the flex container.
const roots = [...document.querySelectorAll('.root-probe')];
const flexBox = roots[0]?.closest('[style*="display:flex"]');
out.rootsAreFlexItems = roots.length === 2 && roots.every((r) => {
let p = r.parentElement;
while (p && getComputedStyle(p).display === 'contents') p = p.parentElement;
return p === flexBox;
});
// Sticky: scroll 300px into its tall container; the bar must stay pinned at the top of the viewport.
const box = document.getElementById('sticky-box');
const bar = document.getElementById('sticky-probe');
if (box && bar) {
window.scrollTo(0, window.scrollY + box.getBoundingClientRect().top + 300);
out.stickyTop = Math.round(bar.getBoundingClientRect().top * 10) / 10;
window.scrollTo(0, 0);
}
// Absolutely positioned dropdown hanging below its block: its last item must be visible and on top.
const last = document.getElementById('dropdown-last');
if (last) {
// window.scrollTo, not scrollIntoView: scrollIntoView would also scroll a clipping ancestor to reveal it.
window.scrollTo(0, window.scrollY + last.getBoundingClientRect().top - 200);
const r = last.getBoundingClientRect();
const hit = document.elementFromPoint(r.left + Math.min(10, r.width / 2), r.top + r.height / 2);
out.dropdownVisible = !!hit && (hit === last || last.contains(hit));
window.scrollTo(0, 0);
}
// Wide bare table: it scrolls by itself, so its last cell is reachable.
const t = document.getElementById('bare-table');
if (t) { t.scrollLeft = 10000; out.tableScrolls = t.scrollWidth > t.clientWidth && t.scrollLeft > 0; t.scrollLeft = 0; }
return out;
}
/** Runs in the page. Element boxes, ignoring display:contents hook wrappers (they have no box). */
function boxes() {
return [...document.body.querySelectorAll('*')]
@@ -152,12 +204,18 @@ async function run(name, launcher, exe) {
const m = await page.evaluate(measure);
const tag = `[${name} ${w}] ${docName}`;
const mobile = w <= MOBILE_MAX;
summary.push(`${name.padEnd(8)} ${String(w).padEnd(4)} ${docName.padEnd(10)} docSW=${m.docSW} bodySW=${m.bodySW} maxRight=${m.maxRight}${m.maxRight > w + 0.5 ? ' (' + m.worst + ')' : ''} cols=${m.cols.map((c) => c.rows + '/' + c.n).join(',') || '-'} grids/row=${m.grids.map((g) => g.perRow + '/' + g.n + (g.hook ? 'h' : '')).join(',') || '-'} nav=${m.navRight}/${m.linkRows}rows${m.scrolls === null ? '' : ' scroller=' + m.scrolls}`);
summary.push(`${name.padEnd(8)} ${String(w).padEnd(4)} ${docName.padEnd(11)} docSW=${m.docSW} bodySW=${m.bodySW} maxRight=${m.maxRight}${m.maxRight > w + 0.5 ? ' (' + m.worst + ')' : ''} cols=${m.cols.map((c) => c.rows + '/' + c.n).join(',') || '-'} grids/row=${m.grids.map((g) => g.perRow + '/' + g.n + (g.hook ? 'h' : '')).join(',') || '-'} nav=${m.navRight}/${m.linkRows}rows${m.scrolls === null ? '' : ' scroller=' + m.scrolls}`);
ok(m.docSW <= w, `${tag}: documentElement.scrollWidth ${m.docSW} > ${w}`);
// Customer HTML is held to the viewport only where the HtmlBlock net applies (<=767px): wider than
// that, a block the author made wider than the screen renders exactly as it always has.
if (docName === 'components' || mobile) {
const sx = await page.evaluate(sidewaysScroll);
ok(sx === 0, `${tag}: the page scrolls sideways (scrollX=${sx})`);
if (docName === 'htmlwide') {
// Wider than a phone by design (fixed widths, px grids): the net does not reflow or box it, so it stays
// clipped by html,body{overflow-x:clip} exactly as before (the publish warning is the guard for these),
// and the page never scrolls sideways (asserted above).
if (mobile) ok(m.maxRight > w + 0.5, `${tag}: the wide fixed block was expected to stay clipped (content ends at x=${m.maxRight})`);
} else if (docName === 'components' || mobile) {
// Customer HTML the net makes fit is held to the viewport only where the net applies (<=767px).
ok(m.bodySW <= w, `${tag}: body.scrollWidth ${m.bodySW} > ${w}`);
ok(m.maxRight <= w + 0.5, `${tag}: content reaches x=${m.maxRight} (clipped on a real device): ${m.worst}`);
}
@@ -166,6 +224,16 @@ async function run(name, launcher, exe) {
if (docName === 'htmlblocks' && mobile) ok(m.linkRows === null, `${tag}: hamburger navbar shows its links on a phone`);
// The 1000px table only needs to scroll where it is wider than its box.
if (m.scrolls !== null && w <= 768) ok(m.scrolls, `${tag}: the intentional overflow-x:auto table no longer scrolls`);
if (docName === 'htmlblocks') {
const pr = await page.evaluate(probes);
summary.push(`${' '.repeat(26)}probes ${JSON.stringify(pr)}`);
ok(pr.wrapperDisplays.length === 1 && pr.wrapperDisplays[0] === 'contents', `${tag}: HtmlBlock wrapper has a box (display ${pr.wrapperDisplays})`);
ok(pr.cellsSeparate, `${tag}: a multi-root HtmlBlock in a grid no longer fills separate cells`);
ok(pr.rootsAreFlexItems, `${tag}: a multi-root HtmlBlock in a flex row is no longer separate flex items`);
ok(pr.stickyTop !== undefined && Math.abs(pr.stickyTop) <= 1, `${tag}: position:sticky inside an HtmlBlock stopped sticking (top=${pr.stickyTop})`);
ok(pr.dropdownVisible, `${tag}: an absolutely positioned dropdown inside an HtmlBlock is clipped or covered`);
if (mobile) ok(pr.tableScrolls, `${tag}: the wide bare table does not scroll on its own`);
}
if (docName === 'components') {
for (const c of m.cols) ok(mobile ? c.rows === c.n : c.rows === 1, `${tag}: ColumnLayout ${mobile ? 'not stacked' : 'stacked'} (${c.rows} rows / ${c.n} cols)`);
for (const g of m.grids) {
+4 -5
View File
@@ -403,12 +403,11 @@ HtmlBlock.craft = {
/* ---------- HTML export ---------- */
/**
* Mobile safety net: the exporter wraps the block's output in this element
* Mobile safety net hook: the exporter wraps the block's output in this element
* (after injecting the node's data attrs, so `html` itself stays byte-identical
* to every earlier release). Inline `display:contents` = no box: desktop and
* tablet layout, and pages whose head predates the net, are unchanged. The head's
* <=767px rule turns it into a sideways-scrolling box no wider than its parent
* (RESPONSIVE_CSS in utils/html-export.ts).
* to every earlier release). Inline `display:contents` = no box, on every
* viewport: layout is unchanged everywhere. It only scopes the head's <=767px
* media/table rules (RESPONSIVE_CSS in utils/html-export.ts).
*/
export const HTML_BLOCK_WRAP: [string, string] = ['<div data-wsb-html style="display:contents">', '</div>'];
+6 -7
View File
@@ -2374,25 +2374,24 @@ body {
}
/* Mirrors of the published <=767px head rules (RESPONSIVE_CSS in utils/html-export.ts): component card
grids go to one column, long words wrap, customer HTML (HtmlBlock) scrolls sideways inside its own box. */
grids go to one column, long words wrap, media in customer HTML shrink and its tables/<pre> scroll on their own. */
.canvas-device-frame[data-device="mobile"] [data-wsb-grid] {
grid-template-columns: minmax(0, 1fr) !important;
}
.canvas-device-frame[data-device="mobile"] {
overflow-wrap: break-word;
}
.canvas-device-frame[data-device="mobile"] [data-wsb-html] {
max-width: 100%;
min-width: 0;
overflow-x: auto;
overflow-wrap: anywhere;
}
.canvas-device-frame[data-device="mobile"] [data-wsb-html] :is(img, video, iframe, embed, object, svg, canvas) {
max-width: 100%;
}
.canvas-device-frame[data-device="mobile"] [data-wsb-html] :is(img, video) {
height: auto;
}
.canvas-device-frame[data-device="mobile"] [data-wsb-html] :is(table, pre):not([style*="overflow"] *) {
display: block;
max-width: 100%;
overflow-x: auto;
}
.canvas-device-frame:not([data-device="desktop"]) [data-fullwidth] {
width: 100% !important;
@@ -14,14 +14,14 @@ import { exportToHtml, exportBodyHtml, buildFontsLink, type ExportOptions } from
import { DEFAULT_SITE_DESIGN, type SiteDesign } from '../state/SiteDesignContext';
const PINNED_HEAD: Record<string, string> = {
A_defaults: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Joe's Pizza &amp; Grill</title>\n <meta property=\"og:title\" content=\"Joe&#39;s Pizza &amp; Grill\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
B_full: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta name=\"robots\" content=\"noindex, nofollow\">\n <title>About &quot;Us&quot;</title>\n <meta name=\"description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:title\" content=\"About Joe&#39;s\">\n <meta property=\"og:description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:image\" content=\"https://cdn.example.com/og.png\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"icon\" href=\"https://cdn.example.com/favicon.ico\">\n <link rel=\"canonical\" href=\"https://example.com/about\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Roboto:wght@300;400;500;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#ff0066;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:12px;--wsb-heading-font:Playfair Display, serif;--wsb-body-font:Roboto, sans-serif;--wsb-button-font:Roboto, sans-serif;--wsb-button-radius:999px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-X\"></script>\n<meta name=\"google-site-verification\" content=\"abc123\">\n</head>",
C_unknown_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Contact</title>\n <meta name=\"description\" content=\"Get in touch\">\n <meta property=\"og:title\" content=\"Contact\">\n <meta property=\"og:description\" content=\"Get in touch\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"canonical\" href=\"https://example.com/contact\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;600;700&family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;600;700&family=Merriweather:wght@300;400;700&family=Source+Code+Pro:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Comic Sans MS, cursive;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
D_body_font_and_blank_tokens: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Services</title>\n <meta property=\"og:title\" content=\"Services\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/services\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#111111;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
E_icons: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Icons</title>\n <meta property=\"og:title\" content=\"Icons\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/icons\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.min.css\" integrity=\"sha384-qrALq7+6jBOZIQsNnT6xGkMDru64qD6uTlDra39xrt2SoXl4pO3FX6Roz/RpR/BS\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/v4-shims.min.css\" integrity=\"sha384-BOBgwI2GEcZ4l/UA/+WOVnLChe2S4c7UhUCtdmX1m0v7Wdyk0S/tDoJOBci2ywvl\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
A_defaults: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Joe's Pizza &amp; Grill</title>\n <meta property=\"og:title\" content=\"Joe&#39;s Pizza &amp; Grill\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
B_full: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta name=\"robots\" content=\"noindex, nofollow\">\n <title>About &quot;Us&quot;</title>\n <meta name=\"description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:title\" content=\"About Joe&#39;s\">\n <meta property=\"og:description\" content=\"We&#39;re &lt;the&gt; best &amp; &quot;only&quot; bakery\">\n <meta property=\"og:image\" content=\"https://cdn.example.com/og.png\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"icon\" href=\"https://cdn.example.com/favicon.ico\">\n <link rel=\"canonical\" href=\"https://example.com/about\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Roboto:wght@300;400;500;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#ff0066;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:12px;--wsb-heading-font:Playfair Display, serif;--wsb-body-font:Roboto, sans-serif;--wsb-button-font:Roboto, sans-serif;--wsb-button-radius:999px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-X\"></script>\n<meta name=\"google-site-verification\" content=\"abc123\">\n</head>",
C_unknown_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Contact</title>\n <meta name=\"description\" content=\"Get in touch\">\n <meta property=\"og:title\" content=\"Contact\">\n <meta property=\"og:description\" content=\"Get in touch\">\n <meta property=\"og:type\" content=\"website\">\n <meta name=\"twitter:card\" content=\"summary\">\n <link rel=\"canonical\" href=\"https://example.com/contact\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto:wght@300;400;500;700&family=Open+Sans:wght@300;400;600;700&family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;600;700&family=Merriweather:wght@300;400;700&family=Source+Code+Pro:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Comic Sans MS, cursive;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
D_body_font_and_blank_tokens: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Services</title>\n <meta property=\"og:title\" content=\"Services\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/services\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#111111;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
E_icons: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Icons</title>\n <meta property=\"og:title\" content=\"Icons\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/icons\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.min.css\" integrity=\"sha384-qrALq7+6jBOZIQsNnT6xGkMDru64qD6uTlDra39xrt2SoXl4pO3FX6Roz/RpR/BS\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/v4-shims.min.css\" integrity=\"sha384-BOBgwI2GEcZ4l/UA/+WOVnLChe2S4c7UhUCtdmX1m0v7Wdyk0S/tDoJOBci2ywvl\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>",
};
PINNED_HEAD.E_brand_swatches = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Brand</title>\n <meta property=\"og:title\" content=\"Brand\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px;--wsb-brand-1:#FFcc00;--wsb-brand-3:#0A7}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>";
PINNED_HEAD.E_brand_swatches = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Brand</title>\n <meta property=\"og:title\" content=\"Brand\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px;--wsb-brand-1:#FFcc00;--wsb-brand-3:#0A7}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n</head>";
PINNED_HEAD.F_swatches_no_brand = PINNED_HEAD.A_defaults;
const PINNED_BODY: Record<string, string> = {
@@ -196,8 +196,8 @@ describe('composed preview: animation head gate covers header+body+footer', () =
* only in the header), default design, canonical https://example.com/
*/
const BACKEND_PINNED_HEAD: Record<string, string> = {
E_headcode_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Notes</title>\n <meta property=\"og:title\" content=\"Notes\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/notes\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <style>.x{font-family:Merriweather,serif}</style>\n</head>",
F_header_animation: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Home</title>\n <meta property=\"og:title\" content=\"Home\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n</head>",
E_headcode_font: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Notes</title>\n <meta property=\"og:title\" content=\"Notes\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/notes\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <style>.x{font-family:Merriweather,serif}</style>\n</head>",
F_header_animation: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Home</title>\n <meta property=\"og:title\" content=\"Home\">\n <meta property=\"og:type\" content=\"website\">\n <link rel=\"canonical\" href=\"https://example.com/\">\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n <style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body{overflow-x:clip}body{font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:#1f2937;-webkit-font-smoothing:antialiased}img{max-width:100%;height:auto;display:block}a{color:inherit}@media(max-width:767px){[style*=\"display: flex\"][style*=\"flex-direction: row\"],[style*=\"display:flex\"][style*=\"flex-direction:row\"]{flex-direction:column!important}[class^=\"cols_\"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*=\"overflow\"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^=\"nav_\"],nav[class^=\"nav_\"] .navbar-links{flex-wrap:wrap}}@media(min-width:992px){[data-hide-desktop]{display:none!important}}@media(min-width:768px) and (max-width:991px){[data-hide-tablet]{display:none!important}}@media(max-width:767px){[data-hide-mobile]{display:none!important}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation=\"fade-in\"].animated{animation-name:fadeIn}[data-animation=\"slide-up\"].animated{animation-name:slideUp}[data-animation=\"slide-left\"].animated{animation-name:slideLeft}[data-animation=\"slide-right\"].animated{animation-name:slideRight}[data-animation=\"zoom-in\"].animated{animation-name:zoomIn}[data-animation=\"bounce\"].animated{animation-name:bounce}:root{--wsb-primary:#3b82f6;--wsb-secondary:#8b5cf6;--wsb-accent:#10b981;--wsb-link:#3b82f6;--wsb-success:#10b981;--wsb-warning:#f59e0b;--wsb-error:#ef4444;--wsb-bg:#ffffff;--wsb-text:#1f2937;--wsb-muted:#6b7280;--wsb-border:#e5e7eb;--wsb-radius:8px;--wsb-heading-font:Inter, sans-serif;--wsb-body-font:Inter, sans-serif;--wsb-button-font:Inter, sans-serif;--wsb-button-radius:8px}body{font-family:var(--wsb-body-font);color:var(--wsb-text);background:var(--wsb-bg)}h1,h2,h3,h4,h5,h6{font-family:var(--wsb-heading-font)}a{color:var(--wsb-link)}a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--wsb-accent);outline-offset:2px}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}</style>\n <noscript><style>[data-animation]{opacity:1}</style></noscript>\n</head>",
};
describe('Preview head === backend head for the head-code font scan and header/footer animation gate', () => {
@@ -277,7 +277,10 @@ describe('mobile stacking + navbar wrap head rules', () => {
const mobile = /^@media\(max-width:767px\)\{(.*)\}@media\(max-width:991px\)/.exec(RESPONSIVE_CSS_MINIFIED)![1];
expect(mobile).toContain('[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}');
expect(mobile).toContain('body{overflow-wrap:break-word}');
expect(mobile).toContain('[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}');
expect(mobile).toContain('[data-wsb-html] :is(table,pre):not([style*="overflow"] *){display:block;max-width:100%;overflow-x:auto}');
// The wrapper itself never gets a box or a scroll container (that clipped dropdowns and broke sticky).
expect(RESPONSIVE_CSS_MINIFIED).not.toMatch(/\[data-wsb-html\]\{/);
expect(RESPONSIVE_CSS_MINIFIED).not.toContain('anywhere');
expect(RESPONSIVE_CSS_MINIFIED.split('data-wsb-').length - 1).toBe(mobile.split('data-wsb-').length - 1);
});
});
+1 -1
View File
@@ -578,7 +578,7 @@ describe('export head unchanged by the TOKEN_VARS move (W0-tokens-core)', () =>
const undo767 = (html: string) => {
// mobile-guard: grid/word/HtmlBlock rules joined the 767 block and the Navbar wrap moved to its own
// <=991px block. Fold that back to the mobile-parity form first.
const guard = '[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:991px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}';
const guard = '[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*="overflow"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:991px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}';
expect(html.split(guard).length).toBe(2);
html = html.replace(guard, 'nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}');
const from = '@media(max-width:767px){[style*=';
+15 -13
View File
@@ -407,11 +407,15 @@ const { mobileMax, tabletMin, tabletMax, desktopMin } = BREAKPOINTS;
// NumberCounter) go to one column on a phone; tablet and desktop keep their columns. minmax(0,1fr), not 1fr,
// so a long word cannot push the single track wider than the screen.
// `body{overflow-wrap:break-word}`: an unbreakable word/URL wraps instead of running off a phone screen; text
// that already fits is untouched.
// `[data-wsb-html]`: the wrapper HtmlBlock exports around customer HTML (inline display:contents, so it has no
// box and changes nothing above 767px). On a phone it becomes a box no wider than its parent that scrolls
// sideways, so customer markup wider than the screen (fixed widths, px grids, wide tables) stays reachable
// instead of being clipped by html,body{overflow-x:clip}; media shrink to fit and long words break.
// that already fits is untouched. break-word, not anywhere: anywhere would also shrink min-content sizes and so
// squeeze table columns and flex items. It inherits into HtmlBlock content through the display:contents wrapper.
// `[data-wsb-html]`: the wrapper HtmlBlock exports around customer HTML. It is inline display:contents on EVERY
// viewport and no rule here gives it a box: a box would clip dropdowns/shadows (overflow-x:auto forces
// overflow-y:auto), unstick position:sticky inside it and merge a multi-root block that is a direct flex/grid
// item into one item (all measured, scripts/e2e-mobile-guard.mjs). It only scopes these non-layout phone
// protections: media shrink to fit, and a table or <pre> scrolls sideways on its own (unless it already sits
// in an inline-styled overflow wrapper the author made). Customer markup made wider than a phone with fixed
// widths/grids stays clipped by html,body{overflow-x:clip} as before; publish-checks warns about it.
const RESPONSIVE_CSS = `
@media (max-width: ${mobileMax}px) {
[style*="display: flex"][style*="flex-direction: row"],
@@ -428,19 +432,17 @@ const RESPONSIVE_CSS = `
body {
overflow-wrap: break-word;
}
[data-wsb-html] {
display: block !important;
max-width: 100%;
min-width: 0;
overflow-x: auto;
overflow-wrap: anywhere;
}
[data-wsb-html] :is(img, video, iframe, embed, object, svg, canvas) {
max-width: 100%;
}
[data-wsb-html] :is(img, video) {
height: auto;
}
[data-wsb-html] :is(table, pre):not([style*="overflow"] *) {
display: block;
max-width: 100%;
overflow-x: auto;
}
}
@media (max-width: ${tabletMax}px) {
nav[class^="nav_"], nav[class^="nav_"] .navbar-links {
@@ -448,7 +450,7 @@ const RESPONSIVE_CSS = `
}
}`;
export const RESPONSIVE_CSS_MINIFIED = `@media(max-width:${mobileMax}px){[style*="display: flex"][style*="flex-direction: row"],[style*="display:flex"][style*="flex-direction:row"]{flex-direction:column!important}[class^="cols_"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html]{display:block!important;max-width:100%;min-width:0;overflow-x:auto;overflow-wrap:anywhere}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}}@media(max-width:${tabletMax}px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}}`;
export const RESPONSIVE_CSS_MINIFIED = `@media(max-width:${mobileMax}px){[style*="display: flex"][style*="flex-direction: row"],[style*="display:flex"][style*="flex-direction:row"]{flex-direction:column!important}[class^="cols_"]{flex-direction:column!important;align-items:stretch!important}[data-wsb-grid]{grid-template-columns:minmax(0,1fr)!important}body{overflow-wrap:break-word}[data-wsb-html] :is(img,video,iframe,embed,object,svg,canvas){max-width:100%}[data-wsb-html] :is(img,video){height:auto}[data-wsb-html] :is(table,pre):not([style*="overflow"] *){display:block;max-width:100%;overflow-x:auto}}@media(max-width:${tabletMax}px){nav[class^="nav_"],nav[class^="nav_"] .navbar-links{flex-wrap:wrap}}`;
const VISIBILITY_CSS = `
@media (min-width: ${desktopMin}px) { [data-hide-desktop] { display: none !important; } }
+3 -3
View File
@@ -219,12 +219,11 @@ describe('custom HTML wider than a phone (mobile guard)', () => {
const hb = (code: string): N => ({ type: 'HtmlBlock', props: { code, aiName: 'Cards' } });
const wide = (code: string) => run([page({ nodes: { h1, x: hb(code) } })]).filter((c) => c.id.startsWith('html-wide:'));
test('flags fixed widths, min-widths, flex-basis and HTML width attributes over 390px', () => {
test('flags fixed widths, min-widths and flex sizes over 390px', () => {
expect(htmlBlockOverflowReasons('<div style="width:1100px">x</div>')).toEqual(['a fixed width of 1100px']);
expect(htmlBlockOverflowReasons('<div style="min-width: 900px !important">x</div>')).toEqual(['a minimum width of 900px']);
expect(htmlBlockOverflowReasons('<style>.c{flex:0 0 420px}</style><div class="c"></div>')).toEqual(['a fixed flex size of 420px']);
expect(htmlBlockOverflowReasons('<style>.c{flex-basis:500px}</style>')).toEqual(['a fixed flex size of 500px']);
expect(htmlBlockOverflowReasons('<table width="800"><tr><td>x</td></tr></table>')).toEqual(['a table/box width="800"']);
});
test('flags grids whose fixed tracks add up to more than 390px, and 3+ column grids with no phone rule', () => {
@@ -241,6 +240,7 @@ describe('custom HTML wider than a phone (mobile guard)', () => {
'<div style="width:390px">x</div>', // exactly a phone
'<img src="a.jpg" width="1200" height="400">', // media shrink to fit (net + reset)
'<iframe width="800" height="450" src="https://example.com"></iframe>',
'<table width="800"><tr><td>x</td></tr></table>', // tables scroll on their own on phones
'<div style="grid-template-columns:repeat(auto-fit,minmax(240px,1fr))">', // reflows on its own
'<div style="grid-template-columns:1fr 1fr">', // two columns: cramped at worst
'<style>.g{display:grid;grid-template-columns:repeat(3,1fr)}@media (max-width:767px){.g{grid-template-columns:1fr}}</style>',
@@ -256,7 +256,7 @@ describe('custom HTML wider than a phone (mobile guard)', () => {
expect(cs[0]).toMatchObject({ id: 'html-wide:p1:x', severity: 'warn', pageId: 'p1', nodeId: 'x', fix: 'select' });
expect(cs[0].message).toContain('Home: custom HTML "Cards" may be wider than a phone screen (390px)');
expect(cs[0].message).toContain('a fixed width of 1100px and a grid at least 900px wide');
expect(cs[0].message).toContain('scrolls sideways inside the block');
expect(cs[0].message).toContain('the part past the screen edge is cut off');
expect(cs[0].message).toContain('Not checked: head code, scripts, external stylesheets, em/rem/% sizes and content widths');
});
+6 -7
View File
@@ -225,10 +225,11 @@ const NOT_CHECKED = 'Not checked: head code, scripts, external stylesheets, em/r
/** `css` with every @media block removed (balanced braces): those rules are the author's own breakpoints. */
function stripMediaBlocks(css: string): string {
const lower = css.toLowerCase();
let out = '';
let i = 0;
for (;;) {
const at = css.toLowerCase().indexOf('@media', i);
const at = lower.indexOf('@media', i);
if (at === -1) return out + css.slice(i);
out += css.slice(i, at);
const open = css.indexOf('{', at);
@@ -280,8 +281,9 @@ function gridMin(value: string): { min: number; cols: number } {
/**
* Why a Custom HTML block's code is likely wider than a {@link PHONE_WIDTH}px phone, as short phrases
* (empty = nothing found). A heuristic over the block's own inline `style` attributes and `<style>`
* elements (rules inside `@media` are the author's breakpoints and are skipped), plus `width=` on
* non-media elements. Media (img/video/iframe...) are not flagged: the published mobile net shrinks them.
* elements (rules inside `@media` are the author's breakpoints and are skipped). `width=` attributes are
* not flagged: they size only media and tables, and on phones the published net shrinks media and lets
* tables scroll on their own.
*/
export function htmlBlockOverflowReasons(code: string): string[] {
if (!code) return [];
@@ -307,9 +309,6 @@ export function htmlBlockOverflowReasons(code: string): string[] {
if (min > PHONE_WIDTH) add(`a grid at least ${min}px wide`);
else if (cols >= 3 && !authorHasPhoneRule) add(`a ${cols}-column grid with no phone layout`);
}
for (const m of code.matchAll(/<(table|div|td|th|section|article|p|form)\b[^>]*?\swidth\s*=\s*["']?(\d+)(?!\d|%)/gi)) {
if (parseInt(m[2], 10) > PHONE_WIDTH) add(`a table/box width="${m[2]}"`);
}
return reasons;
}
@@ -324,7 +323,7 @@ function htmlBlockChecks(nodes: SNodes, ids: string[], ownerId: string, where: s
out.push({
id: `html-wide:${ownerId}:${id}`, severity: 'warn',
message: `${where}: custom HTML${name} may be wider than a phone screen (${PHONE_WIDTH}px): it sets ${found}. `
+ `On phones it scrolls sideways inside the block; use max-width or a phone @media rule to make it fit. ${NOT_CHECKED}`,
+ `On phones the part past the screen edge is cut off; use max-width or a phone @media rule to make it fit. ${NOT_CHECKED}`,
pageId: ownerId, nodeId: id, fix: 'select',
});
}