From 69fd34bd7c328b048366bc3bda3f37d71bbb2adf Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 1 Nov 2026 10:10:15 -0800 Subject: [PATCH] Preview/Published head: stack ColumnLayout on phones, wrap Navbar links, emit Font Awesome - <=767px: [class^=cols_] wrappers (every ColumnLayout split) stack; a mobile flexDirection:row override still opts out. - <=767px: navbars without a hamburger wrap instead of overflowing. - wrapInDocument emits the same Font Awesome links as the backend sb_font_awesome_link (Preview is a sandboxed srcdoc iframe). - head-parity pins updated (+ E_icons case). Co-Authored-By: Claude Sonnet 5.5 --- .../src/utils/html-export.head-parity.test.ts | 61 +++++++++++++++++-- craft/src/utils/html-export.test.ts | 5 +- craft/src/utils/html-export.ts | 40 +++++++++++- 3 files changed, 97 insertions(+), 9 deletions(-) diff --git a/craft/src/utils/html-export.head-parity.test.ts b/craft/src/utils/html-export.head-parity.test.ts index 1e2ec4a..c17baa0 100644 --- a/craft/src/utils/html-export.head-parity.test.ts +++ b/craft/src/utils/html-export.head-parity.test.ts @@ -14,10 +14,11 @@ import { exportToHtml, exportBodyHtml, buildFontsLink, type ExportOptions } from import { DEFAULT_SITE_DESIGN, type SiteDesign } from '../state/SiteDesignContext'; const PINNED_HEAD: Record = { - A_defaults: "\n\n\n \n \n Joe's Pizza & Grill\n \n \n \n \n \n \n \n", - B_full: "\n\n\n \n \n \n About "Us"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - C_unknown_font: "\n\n\n \n \n Contact\n \n \n \n \n \n \n \n \n \n \n", - D_body_font_and_blank_tokens: "\n\n\n \n \n Services\n \n \n \n \n \n \n \n", + A_defaults: "\n\n\n \n \n Joe's Pizza & Grill\n \n \n \n \n \n \n \n", + B_full: "\n\n\n \n \n \n About "Us"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + C_unknown_font: "\n\n\n \n \n Contact\n \n \n \n \n \n \n \n \n \n \n", + D_body_font_and_blank_tokens: "\n\n\n \n \n Services\n \n \n \n \n \n \n \n", + E_icons: "\n\n\n \n \n Icons\n \n \n \n \n \n \n \n \n \n", }; const PINNED_BODY: Record = { @@ -25,6 +26,7 @@ const PINNED_BODY: Record = { B_full: "
", C_unknown_font: "
", D_body_font_and_blank_tokens: "
", + E_icons: "", }; @@ -77,6 +79,10 @@ const CASES: Record = { design: merged({ accentColor: '', textColor: ' #111111 ' }), }, }, + E_icons: { + state: root({}), + opts: { title: 'Icons', includeFonts: true, canonicalUrl: U + 'icons', fontScanHtml: '' }, + }, }; const headOf = (doc: string) => doc.slice(0, doc.indexOf('') + ''.length); @@ -84,6 +90,8 @@ const headOf = (doc: string) => doc.slice(0, doc.indexOf('') + ''. describe('Preview head === pinned published head (WHP test-site-builder-head-parity.php)', () => { for (const [name, { state, opts }] of Object.entries(CASES)) { test(`${name}: body input matches the backend case`, () => { + // E_icons feeds the icon markup through fontScanHtml (the composed page), as the PHP case passes it as the body. + if (name === 'E_icons') return expect(opts.fontScanHtml).toBe(PINNED_BODY[name]); expect(exportBodyHtml(state).html).toBe(PINNED_BODY[name]); }); test(`${name}: COMPLETE head is byte-identical`, () => { @@ -159,8 +167,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 = { - E_headcode_font: "\n\n\n \n \n Notes\n \n \n \n \n \n \n \n \n", - F_header_animation: "\n\n\n \n \n Home\n \n \n \n \n \n \n \n \n", + E_headcode_font: "\n\n\n \n \n Notes\n \n \n \n \n \n \n \n \n", + F_header_animation: "\n\n\n \n \n Home\n \n \n \n \n \n \n \n \n", }; describe('Preview head === backend head for the head-code font scan and header/footer animation gate', () => { @@ -194,3 +202,44 @@ describe('Preview head === backend head for the head-code font scan and header/f expect(headOf(html)).toBe(BACKEND_PINNED_HEAD.F_header_animation); }); }); + +// Font Awesome links: same decisions as the backend's sb_font_awesome_link() (scripts/test-site-builder-head-parity.php "FA:" block). +describe('Font Awesome head links (Preview === Published)', () => { + const FA = 'font-awesome/7.3.1/css/all.min.css'; + const SHIM = 'font-awesome/7.3.1/css/v4-shims.min.css'; + const count = (s: string, needle: string) => s.split(needle).length - 1; + const head = (body: string, headCode = '') => headOf(exportToHtml(root({}), { title: 'T', includeFonts: true, fontScanHtml: body, headCode }).html); + + test('no icon => no link', () => { expect(head('

x

')).not.toContain('font-awesome'); }); + test('v7 markup => all.min.css once, no shims', () => { + const h = head(''); + expect([count(h, FA), count(h, SHIM)]).toEqual([1, 0]); + }); + test('legacy `fa fa-star` => all.min.css + v4-shims once each', () => { + const h = head(''); + expect([count(h, FA), count(h, SHIM)]).toEqual([1, 1]); + }); + test('order: fonts link, then Font Awesome, then ${animationNoscript}${headCode}