From ae19dc58b1afe5631e33d46a1c4ae2b0dc39cdd1 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 17 Aug 2026 20:33:09 +0000 Subject: [PATCH] Preserve CSS ordering on hot theme changes. --- frontend/src/SiteEditor.vue | 41 +++++++++++++++++++++++-------------- pagerite/views.py | 11 ++++++++-- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/frontend/src/SiteEditor.vue b/frontend/src/SiteEditor.vue index 42e4528..1a114f8 100644 --- a/frontend/src/SiteEditor.vue +++ b/frontend/src/SiteEditor.vue @@ -142,22 +142,33 @@ function swapRegions(doc) { } else if (curUserStyle) { curUserStyle.remove() } - // Theme and other public stylesheets live in and must be kept in - // sync; editor-only stylesheets are preserved. Diff-based: unchanged - // sheets keep their elements, so their @keyframes are never torn down - // (re-creating keyframes would replay the editor's slide-in animation). - const curLinks = [...document.head.querySelectorAll('link[rel="stylesheet"]')] - .filter((l) => !l.dataset.pagerite) - const freshHrefs = [...doc.head.querySelectorAll('link[rel="stylesheet"]')] - .map((l) => l.href) - for (const link of curLinks) { - if (!freshHrefs.includes(link.href)) link.remove() + // Theme and other public stylesheets live in , rendered with stable + // ids by the backend; sync them positionally so the custom CSS (rendered + // last) always keeps winning by order. Diff-based: unchanged sheets keep + // their elements, so their @keyframes are never torn down (re-creating + // keyframes would replay the editor's slide-in animation). + const freshLinks = [...doc.head.querySelectorAll('link[rel="stylesheet"]')] + const freshIds = new Set(freshLinks.map((l) => l.id)) + for (const link of [...document.head.querySelectorAll('link[rel="stylesheet"]')]) { + if (!link.dataset.pagerite && !freshIds.has(link.id)) link.remove() } - const have = new Set( - [...document.head.querySelectorAll('link[rel="stylesheet"]')].map((l) => l.href), - ) - for (const link of doc.head.querySelectorAll('link[rel="stylesheet"]')) { - if (!have.has(link.href)) document.head.appendChild(document.importNode(link, true)) + // Insert missing sheets in the fresh document's order, each right after + // its predecessor's element. The first sheet rendered is always the base + // CSS, so its link doubles as the fallback anchor when nothing matched yet + // (e.g. no theme was selected before and the position is otherwise lost). + let anchor = null + for (const link of freshLinks) { + const cur = link.id && document.getElementById(link.id) + if (cur && cur.href === link.href) { + anchor = cur + continue + } + const el = document.importNode(link, true) + // Same id, new URL (theme switch): replace in place, keeping position. + if (cur) cur.replaceWith(el) + else if (anchor) anchor.after(el) + else document.getElementById('pagerite-base')?.after(el) ?? document.head.append(el) + anchor = el } document.title = doc.title } diff --git a/pagerite/views.py b/pagerite/views.py index 15eaa4d..9e4a14d 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -106,8 +106,15 @@ def _layout( doc.meta(name="pagerite:editor-src", content=script[-1]) if editor_css: doc.meta(name="pagerite:editor-css", content=editor_css) - for url in urls: - doc.link(rel="stylesheet", href=url, blocking="render") + # Stylesheet links carry stable ids so the site editor's hot swap can + # keep each sheet at its rendered position (see swapRegions). + for i, url in enumerate(urls): + doc.link( + rel="stylesheet", + href=url, + blocking="render", + id="pagerite-base" if i == 0 else "pagerite-theme", + ) for src in modules: doc.script(src=src, type="module") if custom_css.strip():