Fix back-button DOM restoration: process OOB swaps on popstate, disable editor font overrides
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m43s

- Process sx-swap-oob and hx-swap-oob elements in the popstate handler
  so sidebar, filter, menu, and headers are restored on back navigation
- Disable the 62.5% base font-size hack that leaked globally and caused
  all fonts to shrink when navigating to/from the editor
- Cache-bust sx.js to v=20260301d

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 23:14:32 +00:00
parent 3bc5de126d
commit 1a5969202e
4 changed files with 232 additions and 21 deletions

View File

@@ -8,6 +8,7 @@
(form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]"
(input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :id "lexical-json-input" :name "lexical" :value "")
(input :type "hidden" :id "sx-content-input" :name "sx_content" :value "")
(input :type "hidden" :id "feature-image-input" :name "feature_image" :value "")
(input :type "hidden" :id "feature-image-caption-input" :name "feature_image_caption" :value "")
(div :id "feature-image-container" :class "relative mt-[16px] mb-[24px] group"
@@ -34,7 +35,18 @@
:class "w-full text-[36px] font-bold bg-transparent border-none outline-none placeholder:text-stone-300 mb-[8px] leading-tight")
(textarea :name "custom_excerpt" :rows "1" :placeholder "Add an excerpt..."
:class "w-full text-[18px] text-stone-500 bg-transparent border-none outline-none placeholder:text-stone-300 resize-none mb-[24px] leading-relaxed")
(div :id "lexical-editor" :class "relative w-full bg-transparent")
;; Editor tabs: SX (primary) and Koenig (legacy)
(div :class "flex gap-[4px] mb-[8px] border-b border-stone-200"
(button :type "button" :id "editor-tab-sx"
:class "px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent"
:onclick "document.getElementById('sx-editor').style.display='block';document.getElementById('lexical-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-koenig').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'"
"SX Editor")
(button :type "button" :id "editor-tab-koenig"
:class "px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600"
:onclick "document.getElementById('lexical-editor').style.display='block';document.getElementById('sx-editor').style.display='none';this.className='px-[12px] py-[6px] text-[13px] font-medium text-stone-700 border-b-2 border-stone-700 cursor-pointer bg-transparent';document.getElementById('editor-tab-sx').className='px-[12px] py-[6px] text-[13px] font-medium text-stone-400 border-b-2 border-transparent cursor-pointer bg-transparent hover:text-stone-600'"
"Koenig (Legacy)"))
(div :id "sx-editor" :class "relative w-full bg-transparent")
(div :id "lexical-editor" :class "relative w-full bg-transparent" :style "display:none")
(div :class "flex items-center gap-[16px] mt-[32px] pt-[16px] border-t border-stone-200"
(select :name "status"
:class "text-[14px] rounded-[4px] border border-stone-200 px-[8px] py-[6px] bg-white text-stone-600"
@@ -50,5 +62,146 @@
"#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }"
"#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }")))
(defcomp ~blog-editor-scripts (&key js-src init-js)
(<> (script :src js-src) (script init-js)))
(defcomp ~blog-editor-scripts (&key js-src sx-editor-js-src init-js)
(<> (script :src js-src)
(when sx-editor-js-src (script :src sx-editor-js-src))
(script init-js)))
;; SX editor styles — comprehensive CSS for the Koenig-style block editor
(defcomp ~sx-editor-styles ()
(style
;; Editor container
".sx-editor { position: relative; font-size: 18px; line-height: 1.6; font-family: Georgia, 'Times New Roman', serif; color: #1c1917; }"
".sx-blocks-container { display: flex; flex-direction: column; min-height: 300px; cursor: text; padding-left: 48px; }"
;; Block base
".sx-block { position: relative; margin: 1px 0; }"
".sx-block-content { padding: 2px 0; }"
".sx-editable { outline: none; min-height: 1.6em; }"
".sx-editable:empty:before { content: attr(data-placeholder); color: #d6d3d1; pointer-events: none; }"
;; Text block styles
".sx-heading { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-weight: 700; }"
".sx-block[data-sx-tag='h2'] .sx-heading { font-size: 2em; line-height: 1.25; margin: 0.5em 0 0.25em; }"
".sx-block[data-sx-tag='h3'] .sx-heading { font-size: 1.5em; line-height: 1.3; margin: 0.4em 0 0.2em; }"
".sx-quote { border-left: 3px solid #d6d3d1; padding-left: 16px; font-style: italic; color: #57534e; }"
;; HR
".sx-block-hr { padding: 12px 0; }"
".sx-hr { border: none; border-top: 1px solid #e7e5e4; }"
;; Code blocks
".sx-block-code { background: #1c1917; border-radius: 6px; padding: 16px; margin: 8px 0; }"
".sx-code-header { margin-bottom: 8px; }"
".sx-code-lang { background: transparent; border: none; color: #a8a29e; font-size: 12px; outline: none; width: 120px; font-family: monospace; }"
".sx-code-textarea { width: 100%; background: transparent; border: none; color: #fafaf9; font-family: 'SF Mono', 'Fira Code', 'Fira Mono', monospace; font-size: 14px; line-height: 1.5; resize: none; outline: none; min-height: 60px; tab-size: 2; }"
;; List blocks
".sx-list-content { padding-left: 0; }"
".sx-list-item { padding: 2px 0 2px 24px; position: relative; outline: none; min-height: 1.6em; }"
".sx-list-item:empty:before { content: attr(data-placeholder); color: #d6d3d1; pointer-events: none; }"
".sx-list-item:before { content: '\\2022'; position: absolute; left: 6px; color: #78716c; }"
".sx-block[data-sx-tag='ol'] { counter-reset: sx-list; }"
".sx-block[data-sx-tag='ol'] .sx-list-item { counter-increment: sx-list; }"
".sx-block[data-sx-tag='ol'] .sx-list-item:before { content: counter(sx-list) '.'; font-size: 14px; }"
;; Card blocks
".sx-block-card { margin: 12px 0; border-radius: 4px; position: relative; transition: box-shadow 0.15s; }"
".sx-block-card:hover { box-shadow: 0 0 0 1px #d6d3d1; }"
".sx-block-card.sx-card-editing { box-shadow: 0 0 0 2px #3b82f6; }"
".sx-card-preview { cursor: pointer; }"
".sx-card-preview img { max-width: 100%; }"
".sx-card-fallback { padding: 24px; text-align: center; color: #a8a29e; font-style: italic; background: #fafaf9; border-radius: 4px; }"
".sx-card-caption { padding: 8px 0; font-size: 14px; color: #78716c; text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-card-caption:empty:before { content: attr(data-placeholder); color: #d6d3d1; }"
;; Card toolbar
".sx-card-toolbar { position: absolute; top: -36px; right: 0; z-index: 10; display: none; gap: 4px; }"
".sx-block-card:hover .sx-card-toolbar { display: flex; }"
".sx-card-editing .sx-card-toolbar { display: flex; }"
".sx-card-tool-btn { width: 28px; height: 28px; border-radius: 4px; border: 1px solid #d6d3d1; background: white; color: #78716c; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; transition: all 0.1s; }"
".sx-card-tool-btn:hover { background: #fafaf9; color: #dc2626; border-color: #dc2626; }"
;; Card edit panel
".sx-card-edit { padding: 16px; background: #fafaf9; border-radius: 4px; }"
;; Plus button (Koenig-style floating)
".sx-plus-container { position: absolute; z-index: 40; display: flex; align-items: center; }"
".sx-plus-btn { width: 28px; height: 28px; border-radius: 50%; border: 1px solid #d6d3d1; background: white; color: #a8a29e; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; padding: 0; }"
".sx-plus-btn:hover { border-color: #1c1917; color: #1c1917; }"
".sx-plus-btn-open { transform: rotate(45deg); border-color: #1c1917; color: #1c1917; }"
".sx-plus-btn svg { width: 14px; height: 14px; }"
;; Plus menu
".sx-plus-menu { position: absolute; top: 36px; left: -8px; z-index: 50; background: white; border: 1px solid #e7e5e4; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.12); padding: 8px; width: 320px; max-height: 420px; overflow-y: auto; }"
".sx-plus-menu-section { margin-bottom: 4px; }"
".sx-plus-menu-heading { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #a8a29e; padding: 6px 8px 2px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-plus-menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 6px 8px; border: none; background: none; cursor: pointer; font-size: 14px; color: #1c1917; border-radius: 4px; text-align: left; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-plus-menu-item:hover { background: #f5f5f4; }"
".sx-plus-menu-icon { width: 24px; text-align: center; font-size: 14px; color: #78716c; flex-shrink: 0; }"
".sx-plus-menu-label { font-weight: 500; white-space: nowrap; }"
".sx-plus-menu-desc { color: #a8a29e; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }"
;; Slash command menu
".sx-slash-menu { position: absolute; z-index: 50; background: white; border: 1px solid #e7e5e4; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.12); padding: 4px; width: 320px; max-height: 300px; overflow-y: auto; }"
".sx-slash-menu-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: none; background: none; cursor: pointer; font-size: 14px; color: #1c1917; border-radius: 4px; text-align: left; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-slash-menu-item:hover { background: #f5f5f4; }"
".sx-slash-menu-icon { width: 24px; text-align: center; font-size: 14px; color: #78716c; flex-shrink: 0; }"
".sx-slash-menu-label { font-weight: 500; white-space: nowrap; }"
".sx-slash-menu-desc { color: #a8a29e; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }"
;; Format toolbar
".sx-format-bar { position: absolute; z-index: 100; display: flex; gap: 1px; background: #1c1917; border-radius: 6px; padding: 2px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-format-btn { border: none; background: none; color: #e7e5e4; cursor: pointer; padding: 6px 10px; border-radius: 4px; font-size: 14px; line-height: 1; }"
".sx-format-btn:hover { background: #44403c; color: white; }"
".sx-fmt-bold { font-weight: 700; }"
".sx-fmt-italic { font-style: italic; }"
;; Card edit UI elements
".sx-edit-controls { display: flex; flex-direction: column; gap: 8px; }"
".sx-edit-row { display: flex; align-items: center; gap: 8px; }"
".sx-edit-label { font-size: 12px; font-weight: 600; color: #78716c; min-width: 80px; text-transform: uppercase; letter-spacing: 0.03em; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-edit-input { flex: 1; padding: 6px 10px; border: 1px solid #d6d3d1; border-radius: 4px; font-size: 14px; outline: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-edit-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.15); }"
".sx-edit-select { padding: 6px 10px; border: 1px solid #d6d3d1; border-radius: 4px; font-size: 14px; outline: none; background: white; }"
".sx-edit-btn { padding: 8px 16px; border: 1px solid #d6d3d1; border-radius: 6px; font-size: 14px; cursor: pointer; background: white; color: #1c1917; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; transition: all 0.1s; }"
".sx-edit-btn:hover { background: #f5f5f4; border-color: #a8a29e; }"
".sx-edit-btn-sm { padding: 4px 12px; font-size: 12px; }"
".sx-edit-info { font-size: 12px; color: #a8a29e; }"
".sx-edit-status { font-size: 13px; color: #78716c; margin-top: 4px; }"
".sx-edit-url-input { font-family: monospace; }"
".sx-edit-url-display { font-size: 12px; color: #78716c; font-family: monospace; margin: 4px 0; word-break: break-all; }"
".sx-edit-checkbox-label { display: flex; align-items: center; gap: 6px; font-size: 14px; color: #44403c; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-edit-img-preview { max-width: 100%; max-height: 300px; object-fit: contain; border-radius: 4px; margin-bottom: 12px; }"
".sx-edit-embed-preview { margin-bottom: 8px; }"
".sx-edit-embed-preview iframe { max-width: 100%; }"
".sx-edit-audio-player { width: 100%; margin-bottom: 12px; }"
".sx-edit-video-player { width: 100%; max-height: 300px; margin-bottom: 12px; border-radius: 4px; }"
".sx-edit-html-textarea { width: 100%; min-height: 120px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; padding: 12px; border: 1px solid #d6d3d1; border-radius: 4px; resize: vertical; outline: none; background: #fafaf9; line-height: 1.5; tab-size: 2; }"
".sx-edit-html-textarea:focus { border-color: #3b82f6; }"
".sx-edit-callout-content { padding: 8px 12px; background: white; border: 1px solid #d6d3d1; border-radius: 4px; min-height: 40px; }"
".sx-edit-toggle-content { padding: 8px 12px; background: white; border: 1px solid #d6d3d1; border-radius: 4px; min-height: 40px; margin-top: 4px; }"
".sx-edit-emoji-input { width: 60px; text-align: center; font-size: 20px; padding: 4px; margin-bottom: 8px; }"
;; Color picker
".sx-edit-color-row { display: flex; gap: 6px; margin-bottom: 8px; }"
".sx-edit-color-swatch { width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: all 0.1s; }"
".sx-edit-color-swatch:hover { transform: scale(1.15); }"
".sx-edit-color-swatch.active { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.3); }"
;; Gallery grid
".sx-edit-gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }"
".sx-edit-gallery-thumb { position: relative; aspect-ratio: 1; overflow: hidden; border-radius: 4px; }"
".sx-edit-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; }"
".sx-edit-gallery-remove { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border-radius: 50%; background: rgba(0,0,0,0.6); color: white; border: none; cursor: pointer; font-size: 14px; line-height: 1; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.1s; }"
".sx-edit-gallery-thumb:hover .sx-edit-gallery-remove { opacity: 1; }"
;; Upload area
".sx-upload-area { padding: 40px 24px; border: 2px dashed #d6d3d1; border-radius: 8px; text-align: center; cursor: pointer; transition: all 0.15s; }"
".sx-upload-area:hover, .sx-upload-dragover { border-color: #3b82f6; background: rgba(59,130,246,0.03); }"
".sx-upload-icon { font-size: 32px; color: #a8a29e; margin-bottom: 8px; }"
".sx-upload-msg { font-size: 14px; color: #78716c; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }"
".sx-upload-progress { font-size: 13px; color: #3b82f6; margin-top: 8px; }"
;; Drag over editor
".sx-drag-over { outline: 2px dashed #3b82f6; outline-offset: -2px; border-radius: 4px; }"))

View File

@@ -1115,6 +1115,7 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) ->
asset_url_fn = current_app.jinja_env.globals.get("asset_url", lambda p: "")
editor_css = asset_url_fn("scripts/editor.css")
editor_js = asset_url_fn("scripts/editor.js")
sx_editor_js = asset_url_fn("scripts/sx-editor.js")
upload_image_url = qurl("blog.editor_api.upload_image")
upload_media_url = qurl("blog.editor_api.upload_media")
@@ -1139,29 +1140,21 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) ->
)
parts.append(form_html)
# Editor CSS + inline styles
# Editor CSS + inline styles + sx editor styles
parts.append(sx_call("blog-editor-styles", css_href=editor_css))
parts.append(sx_call("sx-editor-styles"))
# Editor JS + init script
init_js = (
"console.log('[EDITOR-DEBUG] init script running');\n"
"(function() {\n"
" console.log('[EDITOR-DEBUG] IIFE entered, mountEditor=', typeof window.mountEditor);\n"
" function applyEditorFontSize() {\n"
" document.documentElement.style.fontSize = '62.5%';\n"
" document.body.style.fontSize = '1.6rem';\n"
" }\n"
" function restoreDefaultFontSize() {\n"
" document.documentElement.style.fontSize = '';\n"
" document.body.style.fontSize = '';\n"
" }\n"
" applyEditorFontSize();\n"
" document.body.addEventListener('htmx:beforeSwap', function cleanup(e) {\n"
" if (e.detail.target && e.detail.target.id === 'main-panel') {\n"
" restoreDefaultFontSize();\n"
" document.body.removeEventListener('htmx:beforeSwap', cleanup);\n"
" }\n"
" });\n"
" // Font size overrides disabled — caused global font shrinking\n"
" // function applyEditorFontSize() {\n"
" // document.documentElement.style.fontSize = '62.5%';\n"
" // document.body.style.fontSize = '1.6rem';\n"
" // }\n"
" // applyEditorFontSize();\n"
"\n"
" function init() {\n"
" var csrfToken = document.querySelector('input[name=\"csrf_token\"]').value;\n"
@@ -1259,6 +1252,18 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) ->
f" snippetsUrl: '{snippets_url}',\n"
" });\n"
"\n"
" if (typeof SxEditor !== 'undefined') {\n"
" SxEditor.mount('sx-editor', {\n"
" initialSx: window.__SX_INITIAL__ || null,\n"
" csrfToken: csrfToken,\n"
" uploadUrls: uploadUrls,\n"
f" oembedUrl: '{oembed_url}',\n"
" onChange: function(sx) {\n"
" document.getElementById('sx-content-input').value = sx;\n"
" }\n"
" });\n"
" }\n"
"\n"
" document.addEventListener('keydown', function(e) {\n"
" if ((e.ctrlKey || e.metaKey) && e.key === 's') {\n"
" e.preventDefault();\n"
@@ -1276,7 +1281,10 @@ def render_editor_panel(save_error: str | None = None, is_page: bool = False) ->
" }\n"
"})();\n"
)
parts.append(sx_call("blog-editor-scripts", js_src=editor_js, init_js=init_js))
parts.append(sx_call("blog-editor-scripts",
js_src=editor_js,
sx_editor_js_src=sx_editor_js,
init_js=init_js))
return "(<> " + " ".join(parts) + ")" if parts else ""

View File

@@ -2188,6 +2188,33 @@
var popDom = Sx.render(text);
var popContainer = document.createElement("div");
popContainer.appendChild(popDom);
// Process OOB swaps (sidebar, filter, menu, headers)
var oobs = popContainer.querySelectorAll("[sx-swap-oob]");
oobs.forEach(function (oob) {
var oobSwap = oob.getAttribute("sx-swap-oob") || "outerHTML";
var oobTarget = document.getElementById(oob.id);
oob.removeAttribute("sx-swap-oob");
oob.parentNode.removeChild(oob);
if (oobTarget) {
_swapDOM(oobTarget, oob, oobSwap);
Sx.hydrate(oobTarget);
SxEngine.process(oobTarget);
}
});
var hxOobs = popContainer.querySelectorAll("[hx-swap-oob]");
hxOobs.forEach(function (oob) {
var oobSwap = oob.getAttribute("hx-swap-oob") || "outerHTML";
var oobTarget = document.getElementById(oob.id);
oob.removeAttribute("hx-swap-oob");
oob.parentNode.removeChild(oob);
if (oobTarget) {
_swapDOM(oobTarget, oob, oobSwap);
Sx.hydrate(oobTarget);
SxEngine.process(oobTarget);
}
});
var newMain = popContainer.querySelector("#main-panel");
_morphChildren(main, newMain || popContainer);
_activateScripts(main);
@@ -2204,6 +2231,29 @@
// HTML response — parse and morph
var parser = new DOMParser();
var doc = parser.parseFromString(text, "text/html");
// Process OOB swaps from HTML response
var hOobs = doc.querySelectorAll("[sx-swap-oob]");
hOobs.forEach(function (oob) {
var oobSwap = oob.getAttribute("sx-swap-oob") || "outerHTML";
var oobTarget = document.getElementById(oob.id);
oob.removeAttribute("sx-swap-oob");
if (oobTarget) {
_swapContent(oobTarget, oob.outerHTML, oobSwap);
}
oob.parentNode.removeChild(oob);
});
var hhOobs = doc.querySelectorAll("[hx-swap-oob]");
hhOobs.forEach(function (oob) {
var oobSwap = oob.getAttribute("hx-swap-oob") || "outerHTML";
var oobTarget = document.getElementById(oob.id);
oob.removeAttribute("hx-swap-oob");
if (oobTarget) {
_swapContent(oobTarget, oob.outerHTML, oobSwap);
}
oob.parentNode.removeChild(oob);
});
var newMain = doc.getElementById("main-panel");
if (newMain) {
_morphChildren(main, newMain);

View File

@@ -482,7 +482,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
<body class="bg-stone-50 text-stone-900">
<script type="text/sx" data-components>{component_defs}</script>
<script type="text/sx" data-mount="body">{page_sx}</script>
<script src="{asset_url}/scripts/sx.js?v=20260301c"></script>
<script src="{asset_url}/scripts/sx.js?v=20260301d"></script>
<script src="{asset_url}/scripts/body.js"></script>
</body>
</html>"""