diff --git a/web/sections/LayoutPanel.tsx b/web/sections/LayoutPanel.tsx index 069c3f8..ac8f55a 100644 --- a/web/sections/LayoutPanel.tsx +++ b/web/sections/LayoutPanel.tsx @@ -63,6 +63,38 @@ const ROLES = [ { key: 'font_display' as const, label: 'Display', hint: 'The largest statements' }, ]; +/** + * The draft, as the CSS custom properties the page reads. + * + * ── Why this can be done at all ── + * + * The frame is SAME ORIGIN — it comes through the platform's dev proxy on Officer's own host — so the + * panel can reach into its document and set properties on ``. That is the whole trick: no save, + * no request, no reload, and the page restyles itself because these are the variables it already reads. + * + * ── The duplication, stated plainly ── + * + * These names and this exact font syntax mirror `themeStyle()` in the site's `src/apps/web/layout.tsx`. + * The two live in different repositories and cannot share code, so this is a real copy that can drift — + * and if it does, the symptom is a preview that lies, which is worse than one that does not work. + * + * The site is the source of truth. A property renamed there must be renamed here, and the way to check + * is to save: the saved page is rendered by the site's own function, so if the live preview and the + * page after a save disagree, this copy is the one that is wrong. + * + * A chosen face keeps `sans-serif` behind it, exactly as the site writes it — dropping the fallback + * would leave a face that failed to load with nothing to substitute. + */ +const themeProperties = (draft: Layout): Record => ({ + '--site-bg': draft.background, + '--site-fg': draft.foreground, + '--site-accent': draft.accent, + '--font-body': draft.font_body && `"${draft.font_body}", sans-serif`, + '--font-sub': draft.font_sub && `"${draft.font_sub}", sans-serif`, + '--font-title': draft.font_title && `"${draft.font_title}", sans-serif`, + '--font-display': draft.font_display && `"${draft.font_display}", sans-serif`, +}); + const COLOURS = [ { key: 'background' as const, label: 'Background', fallback: '#ffffff' }, { key: 'foreground' as const, label: 'Text', fallback: '#000000' }, @@ -182,7 +214,7 @@ export const LayoutPanel = () => { that sits in the content column. Worse, it was a second implementation of the layout, so it would have gone on being wrong differently every time either side changed. */} - + {/* The controls, and only in Edit. @@ -282,7 +314,7 @@ export const LayoutPanel = () => { * was the other option and it is a guess about where the browser kept it; the platform tried that and * says so in its own comments. */ -const LayoutPreview = ({ port }: { port: number | null }) => { +const LayoutPreview = ({ port, theme }: { port: number | null; theme: Record }) => { const client = useClient(); /* Through a ref, and this is the whole reason the preview hung. @@ -341,6 +373,34 @@ const LayoutPreview = ({ port }: { port: number | null }) => { }; }, [port]); + /* + The draft, pushed into the frame's own document. + + `setProperty` where there is a value and `removeProperty` where there is not — an empty string would + set the variable to nothing rather than unsetting it, and the page's `var(--site-bg, #fff)` fallback + would never come back. Choosing "default" has to actually restore the default. + + Keyed on the serialised theme rather than the object, which is a new identity every render. That is + the same trap that hung this component on "Connecting…", one level down. + */ + const frame = useRef(null); + const serialised = JSON.stringify(theme); + + const applyTheme = useCallback(() => { + const root = frame.current?.contentDocument?.documentElement; + if (!root) return; + for (const [property, value] of Object.entries(JSON.parse(serialised) as Record)) { + if (value) root.style.setProperty(property, value); + else root.style.removeProperty(property); + } + }, [serialised]); + + // On every change, and again whenever the frame is (re)mounted — switching device remounts nothing, + // but a reconnect or a section change does, and the draft has to survive that. + useEffect(() => { + applyTheme(); + }, [applyTheme, ready]); + const message = !port ? 'No development server is configured — set its port under Administration.' : failed ? `Could not authorise the preview: ${failed}` @@ -386,6 +446,10 @@ const LayoutPreview = ({ port }: { port: number | null }) => { remove the attribute. This is the owner's own development server on this machine. */