the preview restyles itself as you change things
No save, no request, no reload. The panel sets the CSS custom properties on the frame's own <html> and the page restyles itself, because those are the variables it already reads. This is only possible because the frame is SAME ORIGIN — it comes through the platform's dev proxy on Officer's own host. It is the payoff from dropping this plugin's hand-rolled cross-origin proxy, and the reason that swap was worth making beyond the bugs it fixed. `removeProperty` where a value is null, not an empty string. Setting the variable to nothing is not the same as unsetting it: the page's `var(--site-bg, #fff)` fallback would never come back, so choosing "default" would fail to restore the default — the one case somebody would test. Applied on load as well as on change, because a reload replaces the document and the properties go with it. Keyed on the serialised theme rather than the object, which is a new identity every render — the same trap that hung this component on "Connecting…", one level down. The property names mirror themeStyle() in the site's layout.tsx and cannot share code across the two repositories, so this is a copy that can drift. Said so in the file, with the check: the saved page is rendered by the site's own function, so if live and post-save disagree, this copy is the wrong one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<html>`. 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<string, string | null> => ({
|
||||
'--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.
|
||||
*/}
|
||||
<LayoutPreview port={devPort} />
|
||||
<LayoutPreview port={devPort} theme={themeProperties(draft)} />
|
||||
|
||||
{/*
|
||||
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<string, string | null> }) => {
|
||||
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<HTMLIFrameElement | null>(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<string, string | null>)) {
|
||||
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.
|
||||
*/
|
||||
<iframe
|
||||
ref={frame}
|
||||
// Applied on load as well as on change: the document is replaced on a reload, and the
|
||||
// properties go with it.
|
||||
onLoad={applyTheme}
|
||||
src={`/api/dev-proxy/${port}/layout`}
|
||||
title="The site's layout"
|
||||
className="border-0"
|
||||
|
||||
Reference in New Issue
Block a user