@broberg/cms-inline-edit 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -246,8 +246,23 @@ function setupFields(token, options) {
246
246
  if (fieldsWired) return;
247
247
  injectStyles();
248
248
  document.querySelectorAll("[data-cms-field]").forEach((el) => wireField(el, token, options));
249
+ observeNewFields();
249
250
  fieldsWired = true;
250
251
  }
252
+ var fieldObserver = null;
253
+ function observeNewFields() {
254
+ if (fieldObserver || typeof MutationObserver === "undefined") return;
255
+ let queued = false;
256
+ fieldObserver = new MutationObserver(() => {
257
+ if (queued) return;
258
+ queued = true;
259
+ requestAnimationFrame(() => {
260
+ queued = false;
261
+ rescanFields();
262
+ });
263
+ });
264
+ fieldObserver.observe(document.body, { childList: true, subtree: true });
265
+ }
251
266
  function rescanFields() {
252
267
  if (typeof window === "undefined" || !stateOptions) return;
253
268
  const token = localStorage.getItem(stateOptions.storageKey);
@@ -761,7 +776,8 @@ function htmlToMarkdown(html) {
761
776
  const container = document.createElement("div");
762
777
  container.innerHTML = html;
763
778
  reattachOrphanListItems(container);
764
- return serializeBlockChildren(container).replace(/\n{3,}/g, "\n\n").trim() + "\n";
779
+ const md = serializeBlockChildren(container).replace(/\n{3,}/g, "\n\n").trim();
780
+ return md.includes("\n") ? md + "\n" : md;
765
781
  }
766
782
  function isList(el) {
767
783
  if (!el) return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/field-slice.ts","../src/token-safe.ts"],"sourcesContent":["/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\nimport { applyFieldSlice } from \"./field-slice\";\nexport { applyFieldSlice } from \"./field-slice\";\nimport { serializeTokenSafe, hasTokenChips, lockTokenChips } from \"./token-safe\";\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n orderedList?: string;\n unorderedList?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n /** F164 — link dialog. */\n link?: string;\n linkTabPage?: string;\n linkTabUrl?: string;\n linkSearch?: string;\n linkText?: string;\n linkTextAuto?: string;\n linkTextAutoHint?: string;\n linkTextOwnHint?: string;\n linkLiveHint?: string;\n linkUrlHint?: string;\n linkUrl?: string;\n linkInsert?: string;\n linkSave?: string;\n linkCancel?: string;\n linkRemove?: string;\n linkRemoveConfirm?: string;\n linkYes?: string;\n linkNo?: string;\n linkEmpty?: string;\n linkLoading?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected + editing\" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Text for the explicit \"log out\" action on the idle pill — clicking it clears the token (back to the logged-out state). Default \"Log ud\". */\n logoutLabel?: string;\n /** Show an on-site \"connect\" pill to LOGGED-OUT visitors (click → connect flow).\n * Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only\n * enable for a site with no real customers (e.g. an internal/first-party site)\n * where a self-contained on-site login entry is wanted. The safe entry for a\n * customer site is always the \"Redigér live\" button in webhouse.app CMS-admin. */\n connectPrompt?: boolean;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n orderedList: \"Nummereret liste\",\n unorderedList: \"Punktliste\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n link: \"Link\",\n linkTabPage: \"Side på sitet\",\n linkTabUrl: \"Fri adresse\",\n linkSearch: \"Søg efter en side…\",\n linkText: \"Linktekst\",\n linkTextAuto: \"Følger sidens titel\",\n linkTextAutoHint:\n \"<b>Tom = følger sidens titel.</b> Omdøbes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den stående.\",\n linkTextOwnHint:\n \"Linket viser <b>din egen tekst</b>. Den bliver stående, også hvis siden omdøbes.\",\n linkLiveHint:\n \"Linket peger på <b>siden</b> — ikke på en adresse. Flyttes siden, eller får den et nyt navn, retter linket sig selv.\",\n linkUrlHint:\n \"En fri adresse peger præcis dér, du skriver. Den følger <b>ikke</b> med, hvis målet flytter sig.\",\n linkUrl: \"Adresse\",\n linkInsert: \"Indsæt link\",\n linkSave: \"Gem ændring\",\n linkCancel: \"Annullér\",\n linkRemove: \"Fjern link\",\n linkRemoveConfirm: \"Fjern?\",\n linkYes: \"Ja\",\n linkNo: \"Nej\",\n linkEmpty: \"Ingen sider fundet\",\n linkLoading: \"Henter sider…\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n logoutLabel: \"Log ud\",\n connectPrompt: false,\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n // Capture BEFORE checkEnabled so we know whether the token JUST arrived from\n // the connect redirect (→ drop straight into edit mode) vs. a normal load\n // with a stored token (→ idle \"Rediger\" pill, don't force edit mode).\n const freshFromUrl = captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (!token) {\n // Logged-out visitor / customer. DEFAULT: show NOTHING (customer-safe) —\n // the consumer can now call initInlineEdit() unconditionally without leaking\n // an edit affordance. Only sites that opt in (connectPrompt: true) get the\n // on-site connect pill + its popup token receiver.\n if (resolved.connectPrompt) {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n return;\n }\n\n // Connected editor. Wire fields once, then pick the entry state: arrived from\n // connect → edit now; normal load with a stored token → idle \"Rediger\" pill\n // (one click enters edit mode; the token persists so \"Rediger\" stays visible\n // on every page while logged in — no trip back to admin).\n setupFields(token, resolved);\n if (freshFromUrl) enterEditMode();\n else showIdlePill();\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n // Fresh connect via popup → wire fields + drop straight into edit mode.\n setupFields(data.token, options);\n enterEditMode();\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token) || !isForThisSite(token, resolved.siteId)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/**\n * Is this token scoped to the site this page belongs to?\n *\n * NOT an authorization check — the server is, and it holds: proxy.ts refuses a\n * token whose `site` claim differs from the request's site, measured 2026-08-17\n * at 403 on both GET and PATCH. This is a UX guard. Without it the editor\n * entered edit mode with a foreign-site token, made the page look editable, and\n * then failed EVERY save — and the natural reaction is to hunt for the bug in\n * your own site rather than suspect the token (reported by the sanneandersen\n * session, who predicted exactly that dead end). Better to not offer editing at\n * all than to offer it and lose the work.\n *\n * A token that cannot be read at all is let through: the server decides, and an\n * unreadable token simply fails there instead of being silently discarded here.\n */\nfunction isForThisSite(token: string, siteId: string): boolean {\n const claims = decodeJwtPayload(token);\n const site = claims?.site;\n if (typeof site !== \"string\" || !site) return true; // not ours to judge\n return site === siteId;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from\n * the URL. Returns true when a FRESH token was just captured from the URL\n * (i.e. this is the connect-redirect landing) so the caller can auto-enter\n * edit mode; false on a normal load where the token comes from localStorage. */\nfunction captureTokenFromUrl(options: ResolvedOptions): boolean {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return false;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n return true;\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\n// Toolbar list-button faces (lucide \"list\" / \"list-ordered\"), inline so no\n// icon-font/runtime dep; stroke inherits the button's white `currentColor`.\nconst UL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/>' +\n '<line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>' +\n \"</svg>\";\n\nconst OL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/>' +\n '<path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/>' +\n \"</svg>\";\n\nconst LINK_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7\"/>' +\n '<path d=\"M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\n// ─── Connected-editor state machine ────────────────────────────────────────\n// A connected editor (valid token) is ALWAYS in one of two visible states:\n// idle → a \"Rediger\" pill (options.connectLabel); one click enters editing.\n// editing → fields are contenteditable + an \"Afslut redigering\" pill.\n// Leaving edit mode returns to idle and KEEPS the token — the editor stays\n// logged in and can re-enter with one click, on every page, for the token's\n// whole life. Only an explicit \"Log ud\" clears the token (→ logged-out state:\n// no pill at all, same as a customer). A field is wired at most ONCE (idempotent\n// via wiredFields); their click handlers no-op while editingActive is false, so\n// idle is truly inert. On an SPA (soft-navigation, no full reload) new pages add\n// fresh [data-cms-field] nodes to the DOM AFTER init — the consumer calls\n// rescanFields() (e.g. on route change) to wire those late arrivals.\nlet editingActive = false;\nlet fieldsWired = false;\nlet stateOptions: ResolvedOptions | null = null;\n// Elements whose listeners are already attached — makes wireField idempotent so\n// setupFields (once) and rescanFields (N times) can both run without double-wiring.\nconst wiredFields = new WeakSet<HTMLElement>();\n\nfunction setupFields(token: string, options: ResolvedOptions): void {\n stateOptions = options;\n if (fieldsWired) return;\n injectStyles();\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, options));\n fieldsWired = true;\n}\n\n/**\n * Wire any [data-cms-field] elements added to the DOM since the last scan.\n * Needed on single-page apps (Next.js et al.): the layout mounts once → init +\n * setupFields run once → only the first page's fields get listeners. Soft\n * navigation swaps in new pages' fields WITHOUT re-running init, so their\n * click-to-edit never activates. A consumer calls this on every route change.\n *\n * Safe + idempotent by design:\n * - No-ops unless a connected, non-expired token exists (same guard as init) —\n * a consumer may call it unconditionally without leaking an edit affordance.\n * - wireField skips already-wired elements (wiredFields WeakSet), so re-scanning\n * the whole document costs nothing for existing fields.\n * - Does NOT touch edit-mode state: editingActive (module-level) and\n * body[data-cms-editing] persist across a soft-nav, so freshly-wired fields\n * are immediately live when the editor is mid-session — no re-enter needed.\n * MPA consumers (broberg: full reload per navigation re-runs init) never need it.\n */\nexport function rescanFields(): void {\n if (typeof window === \"undefined\" || !stateOptions) return;\n const token = localStorage.getItem(stateOptions.storageKey);\n if (!token || isExpired(token)) return;\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, stateOptions!));\n}\n\nfunction enterEditMode(): void {\n if (!stateOptions || editingActive) return;\n editingActive = true;\n // Marks the page as actively editing → unlocks the field hover-outline +\n // text-cursor affordance (see injectStyles). In idle the fields look/behave\n // like normal text; the edit affordance appears only after \"Rediger\".\n document.body.setAttribute(\"data-cms-editing\", \"true\");\n removeIdlePill();\n showActiveBadge(stateOptions);\n}\n\nfunction exitEditMode(): void {\n if (!stateOptions || !editingActive) return;\n deactivateRich(); // commit any active rich region first\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n document.querySelector(\"[data-cms-inline-edit-badge]\")?.remove();\n showIdlePill();\n}\n\nfunction removeIdlePill(): void {\n document.querySelector(\"[data-cms-inline-edit-idle]\")?.remove();\n}\n\nfunction showIdlePill(): void {\n if (!stateOptions) return;\n // Invariant: the idle pill is showing ⟹ we are NOT editing. Force the flag\n // false here so \"Rediger\" (enterEditMode) can never become a no-op if the\n // state ever diverged from the visible pill (defensive — enterEditMode bails\n // when editingActive is already true).\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n const options = stateOptions;\n removeIdlePill();\n const wrap = document.createElement(\"div\");\n wrap.setAttribute(\"data-cms-inline-edit-idle\", \"\");\n wrap.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;\" +\n \"z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;\";\n // Main action: \"Rediger\" → enter edit mode (no server round-trip; the token\n // already exists locally).\n const edit = document.createElement(\"button\");\n edit.type = \"button\";\n edit.setAttribute(\"data-cms-inline-edit-enter\", \"\");\n edit.style.cssText =\n \"display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;\";\n const editText = document.createElement(\"span\");\n editText.textContent = options.connectLabel;\n edit.append(makeIcon(), editText);\n edit.addEventListener(\"click\", enterEditMode);\n // Secondary: \"Log ud\" → clear the token (back to the logged-out state).\n const out = document.createElement(\"button\");\n out.type = \"button\";\n out.setAttribute(\"data-cms-inline-edit-logout\", \"\");\n out.textContent = options.logoutLabel;\n out.style.cssText =\n \"background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;\" +\n \"border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;\";\n out.addEventListener(\"click\", () => {\n disconnect(options);\n removeIdlePill();\n });\n wrap.append(edit, out);\n document.body.appendChild(wrap);\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n if (wiredFields.has(el)) return; // idempotent — safe to re-scan (rescanFields)\n wiredFields.add(el);\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n // Token-safe fields render {år}-style auto-values as atomic chips. Read the\n // TEMPLATE form (chips → their tokens) so the saved value keeps its tokens,\n // and lock the chips so each edits as one unbreakable unit.\n const tokenSafe = hasTokenChips(el);\n el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n el.setAttribute(\"contenteditable\", \"true\");\n if (tokenSafe) lockTokenChips(el);\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = hasTokenChips(el) ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n hideLinkDialog();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Bullet + numbered lists — native execCommand, and the Markdown serializer\n // already round-trips <ul>/<ol> to \"- \" / \"1. \" so no save-path change needed.\n t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand(\"insertUnorderedList\")));\n t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand(\"insertOrderedList\")));\n\n t.appendChild(sep());\n\n // F164 — link: a free URL, or a live reference to a page on the site.\n const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));\n linkBtn.setAttribute(\"data-testid\", \"inline-toolbar-link\");\n t.appendChild(linkBtn);\n\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\n/* ------------------------------------------------------------------ F164 --\n * Link dialog: a free URL, or a LIVE reference to a page on the site.\n *\n * A page link stores data-cms-ref=\"collection:slug\" next to a real working\n * href, so resolveCmsLinks() (see ./server) can re-point it when the page moves\n * or is renamed. Leaving the label empty marks it data-cms-ref-label=\"auto\" so\n * the link shows the page's current title.\n * -------------------------------------------------------------------------- */\n\ninterface LinkablePage {\n collection: string;\n slug: string;\n title: string;\n path: string;\n label: string;\n}\n\nlet linkDialog: HTMLElement | null = null;\nlet linkPages: LinkablePage[] | null = null;\nlet linkPicked: LinkablePage | null = null;\nlet linkEditing: HTMLAnchorElement | null = null;\n\nfunction hideLinkDialog(): void {\n if (linkDialog) linkDialog.style.display = \"none\";\n linkPicked = null;\n linkEditing = null;\n}\n\n/** The <a> the caret sits inside, if any — so clicking Link EDITS it. */\nfunction anchorAtCaret(): HTMLAnchorElement | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n let node: Node | null = sel.getRangeAt(0).startContainer;\n while (node && node !== document.body) {\n if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === \"A\") {\n return node as HTMLAnchorElement;\n }\n node = node.parentNode;\n }\n return null;\n}\n\nasync function fetchLinkablePages(): Promise<LinkablePage[]> {\n if (linkPages) return linkPages;\n if (!richCtx) return [];\n const { options, token } = richCtx;\n const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;\n const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });\n if (!res.ok) throw new Error(`pages ${res.status}`);\n const body = (await res.json()) as { pages?: LinkablePage[] };\n linkPages = body.pages ?? [];\n return linkPages;\n}\n\nfunction toggleLinkDialog(anchor: HTMLElement): void {\n if (linkDialog && linkDialog.style.display === \"block\") {\n hideLinkDialog();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n linkEditing = anchorAtCaret();\n if (!linkDialog) linkDialog = buildLinkDialog();\n renderLinkDialog();\n const rect = anchor.getBoundingClientRect();\n linkDialog.style.top = `${rect.bottom + 8}px`;\n linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;\n linkDialog.style.display = \"block\";\n}\n\nfunction buildLinkDialog(): HTMLElement {\n const d = document.createElement(\"div\");\n d.setAttribute(\"data-cms-inline-edit-toolbar\", \"\"); // shares the \"don't commit on click\" guard\n d.setAttribute(\"data-testid\", \"inline-link-dialog\");\n d.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;color:#fff;overflow:hidden;\";\n // Keep the page selection alive while interacting with the dialog.\n d.addEventListener(\"mousedown\", (e) => {\n const t = e.target as HTMLElement;\n if (t.tagName !== \"INPUT\") e.preventDefault();\n e.stopPropagation();\n });\n document.body.appendChild(d);\n return d;\n}\n\nfunction renderLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const L = uiLabels;\n const editing = !!linkEditing;\n const existingRef = linkEditing?.getAttribute(\"data-cms-ref\") ?? \"\";\n const onPageTab = !editing || !!existingRef;\n\n d.innerHTML =\n '<div style=\"display:flex;gap:4px;padding:8px 8px 0\">' +\n `<button type=\"button\" data-testid=\"inline-link-tab-page\" data-tab=\"page\" style=\"${tabCss(onPageTab)}\">${L.linkTabPage}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-tab-url\" data-tab=\"url\" style=\"${tabCss(!onPageTab)}\">${L.linkTabUrl}</button>` +\n \"</div>\" +\n '<div style=\"padding:10px 12px 12px\">' +\n `<div data-pane=\"page\" style=\"display:${onPageTab ? \"block\" : \"none\"}\">` +\n `<input data-testid=\"inline-link-search\" data-role=\"search\" placeholder=\"${L.linkSearch}\" style=\"${fieldCss()}\">` +\n `<div data-role=\"list\" data-testid=\"inline-link-list\" style=\"margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821\"></div>` +\n \"</div>\" +\n `<div data-pane=\"url\" style=\"display:${onPageTab ? \"none\" : \"block\"}\">` +\n `<label style=\"${labelCss()}\">${L.linkUrl}</label>` +\n `<input data-testid=\"inline-link-url\" data-role=\"url\" placeholder=\"https://…\" style=\"${fieldCss()}\">` +\n \"</div>\" +\n `<label style=\"${labelCss()}\">${L.linkText}</label>` +\n `<input data-testid=\"inline-link-text\" data-role=\"text\" placeholder=\"${L.linkTextAuto}\" style=\"${fieldCss()}\">` +\n `<p data-role=\"hint\" style=\"font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5\"></p>` +\n `<div data-role=\"live\" style=\"display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px\">` +\n `<p style=\"margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55\">${L.linkLiveHint}</p></div>` +\n '<div style=\"display:flex;gap:8px;align-items:center;margin-top:14px\">' +\n '<div data-role=\"remove\"></div><div style=\"flex:1\"></div>' +\n `<button type=\"button\" data-testid=\"inline-link-cancel\" data-role=\"cancel\" style=\"${btnCss(false)}\">${L.linkCancel}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-submit\" data-role=\"submit\" style=\"${btnCss(true)}\" disabled>${editing ? L.linkSave : L.linkInsert}</button>` +\n \"</div></div>\";\n\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const text = q<HTMLInputElement>(\"text\");\n const urlIn = q<HTMLInputElement>(\"url\");\n const search = q<HTMLInputElement>(\"search\");\n\n if (editing) {\n text.value = linkEditing?.getAttribute(\"data-cms-ref-label\") === \"auto\" ? \"\" : (linkEditing?.textContent ?? \"\");\n if (!existingRef) urlIn.value = linkEditing?.getAttribute(\"href\") ?? \"\";\n q<HTMLElement>(\"remove\").appendChild(buildRemoveLink());\n }\n\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((btn) => {\n btn.onclick = () => {\n const page = btn.dataset.tab === \"page\";\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((b) => {\n b.setAttribute(\"style\", tabCss(b.dataset.tab === \"page\" ? page : !page));\n });\n (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display = page ? \"block\" : \"none\";\n (d.querySelector('[data-pane=\"url\"]') as HTMLElement).style.display = page ? \"none\" : \"block\";\n q<HTMLElement>(\"live\").style.display = page ? \"flex\" : \"none\";\n syncLinkDialog();\n };\n });\n\n search.oninput = () => renderLinkList(search.value);\n text.oninput = syncLinkDialog;\n urlIn.oninput = syncLinkDialog;\n q<HTMLElement>(\"cancel\").onclick = hideLinkDialog;\n q<HTMLElement>(\"submit\").onclick = applyLink;\n\n renderLinkList(\"\");\n syncLinkDialog();\n}\n\nfunction buildRemoveLink(): HTMLElement {\n const wrap = document.createElement(\"div\");\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.setAttribute(\"data-testid\", \"inline-link-remove\");\n btn.textContent = uiLabels.linkRemove;\n btn.style.cssText =\n \"background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;\";\n btn.onclick = () => {\n wrap.innerHTML =\n `<span style=\"font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px\">${uiLabels.linkRemoveConfirm}</span>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-yes\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkYes}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-no\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkNo}</button>`;\n (wrap.querySelector('[data-testid=\"inline-link-remove-no\"]') as HTMLElement).onclick = () => {\n wrap.innerHTML = \"\";\n wrap.appendChild(btn);\n };\n (wrap.querySelector('[data-testid=\"inline-link-remove-yes\"]') as HTMLElement).onclick = () => {\n if (linkEditing) {\n const parent = linkEditing.parentNode;\n while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);\n parent?.removeChild(linkEditing);\n }\n hideLinkDialog();\n };\n };\n wrap.appendChild(btn);\n return wrap;\n}\n\nfunction renderLinkList(filter: string): void {\n const d = linkDialog;\n if (!d) return;\n const list = d.querySelector('[data-role=\"list\"]') as HTMLElement | null;\n if (!list) return;\n\n const paint = (pages: LinkablePage[]) => {\n const f = filter.trim().toLowerCase();\n const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));\n if (!shown.length) {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkEmpty}</div>`;\n return;\n }\n list.innerHTML = \"\";\n shown.forEach((p) => {\n const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;\n const row = document.createElement(\"div\");\n row.setAttribute(\"data-testid\", `inline-link-page-${p.collection}-${p.slug}`);\n row.style.cssText =\n \"display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;\" +\n \"border-bottom:1px solid #232833;\" +\n (on ? \"background:rgba(0,178,255,.14);\" : \"\");\n row.innerHTML =\n `<div style=\"min-width:0;flex:1\"><div style=\"font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.title)}</div>` +\n `<div style=\"font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.path)}</div></div>` +\n `<span style=\"flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap\">${escapeHtml(p.label)}</span>`;\n row.onclick = () => {\n linkPicked = p;\n renderLinkList(filter);\n syncLinkDialog();\n };\n list.appendChild(row);\n });\n };\n\n if (linkPages) {\n paint(linkPages);\n return;\n }\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkLoading}</div>`;\n fetchLinkablePages()\n .then((pages) => {\n // Pre-select the page an existing reference already points at.\n const ref = linkEditing?.getAttribute(\"data-cms-ref\");\n if (ref && !linkPicked) {\n const [c, ...rest] = ref.split(\":\");\n linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(\":\")) ?? null;\n }\n paint(pages);\n syncLinkDialog();\n })\n .catch(() => {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#ff8a8a\">${uiLabels.error}</div>`;\n });\n}\n\nfunction syncLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n q<HTMLElement>(\"hint\").innerHTML = own\n ? (uiLabels.linkTextOwnHint as string)\n : onPage\n ? (uiLabels.linkTextAutoHint as string)\n : (uiLabels.linkUrlHint as string);\n q<HTMLElement>(\"live\").style.display = onPage ? \"flex\" : \"none\";\n (q<HTMLButtonElement>(\"submit\")).disabled = onPage\n ? !linkPicked\n : !q<HTMLInputElement>(\"url\").value.trim();\n}\n\nfunction applyLink(): void {\n const d = linkDialog;\n if (!d || !richCtx) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n const href = onPage ? (linkPicked?.path ?? \"\") : q<HTMLInputElement>(\"url\").value.trim();\n if (!href) return;\n\n const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : \"\";\n const label = own || (onPage ? (linkPicked?.title ?? href) : href);\n\n if (linkEditing) {\n linkEditing.setAttribute(\"href\", href);\n if (ref) linkEditing.setAttribute(\"data-cms-ref\", ref);\n else linkEditing.removeAttribute(\"data-cms-ref\");\n if (ref && !own) linkEditing.setAttribute(\"data-cms-ref-label\", \"auto\");\n else linkEditing.removeAttribute(\"data-cms-ref-label\");\n linkEditing.textContent = label;\n hideLinkDialog();\n return;\n }\n\n const a = document.createElement(\"a\");\n a.setAttribute(\"href\", href);\n if (ref) {\n a.setAttribute(\"data-cms-ref\", ref);\n if (!own) a.setAttribute(\"data-cms-ref-label\", \"auto\");\n }\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n const range = sel.getRangeAt(0);\n a.textContent = own || range.toString() || label;\n range.deleteContents();\n range.insertNode(a);\n sel.removeAllRanges();\n } else {\n a.textContent = label;\n richCtx.el.appendChild(a);\n }\n savedRange = null;\n hideLinkDialog();\n}\n\nfunction escapeHtml(v: string): string {\n return v.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nconst tabCss = (on: boolean) =>\n \"flex:1;background:\" +\n (on ? \"#2a2f38\" : \"none\") +\n \";border:1px solid \" +\n (on ? \"#3a3f4a\" : \"transparent\") +\n \";color:\" +\n (on ? \"#fff\" : \"#9aa3b2\") +\n \";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;\";\n\nconst fieldCss = () =>\n \"width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;\" +\n \"border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;\";\n\nconst labelCss = () =>\n \"font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;\";\n\nconst btnCss = (primary: boolean) =>\n \"height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;\" +\n (primary\n ? \"background:#00b2ff;color:#04121b;border:none;\"\n : \"background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;\");\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nexport function htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n reattachOrphanListItems(container);\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction isList(el: Element | null): el is HTMLElement {\n if (!el) return false;\n const tag = el.tagName.toLowerCase();\n return tag === \"ul\" || tag === \"ol\";\n}\n\n/**\n * A contenteditable region can end up with an <li> as a DIRECT child of the\n * field container — browsers routinely split a list and orphan an item when the\n * caret sits in it, and `execCommand(\"insertUnorderedList\")` does the same.\n * Serializing an orphan through the inline default silently drops its marker,\n * so a list item degrades into a paragraph and the stored Markdown is corrupted\n * (sanneandersen, 2026-08-16: a 13-item CV list saved back as 1 paragraph + 12\n * bullets on a public page, on every single save).\n *\n * Put each orphan back in the adjacent list — before it if the list follows,\n * after it if the list precedes — so author order survives. An orphan with no\n * list next to it gets its own.\n */\nfunction reattachOrphanListItems(container: HTMLElement): void {\n const orphans = Array.from(container.children).filter(\n (c) => c.tagName.toLowerCase() === \"li\",\n );\n for (const li of orphans) {\n const prev = li.previousElementSibling;\n const next = li.nextElementSibling;\n if (isList(prev)) {\n prev.appendChild(li);\n } else if (isList(next)) {\n next.insertBefore(li, next.firstChild);\n } else {\n const list = container.ownerDocument.createElement(\"ul\");\n li.replaceWith(list);\n list.appendChild(li);\n }\n }\n}\n\n/** Tags serializeBlock treats as their own block; everything else is inline. */\nconst BLOCK_TAGS = new Set([\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\",\n \"p\", \"div\", \"ul\", \"ol\", \"li\", \"blockquote\", \"pre\", \"hr\", \"table\",\n]);\n\nfunction isBlockNode(node: Node): boolean {\n return (\n node.nodeType === Node.ELEMENT_NODE &&\n BLOCK_TAGS.has((node as HTMLElement).tagName.toLowerCase())\n );\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n // Text and inline elements sitting directly at block level form ONE implicit\n // paragraph. Serialising them one-by-one and joining with \"\\n\\n\" tore a\n // single sentence into separate blocks (\"Noget <b>fedt</b> mere\" became three\n // paragraphs) and dropped each element's own formatting. Gather the run, then\n // serialise it as the one paragraph the author actually wrote.\n let inlineRun = \"\";\n const flushInline = () => {\n const s = inlineRun.trim();\n if (s) blocks.push(s);\n inlineRun = \"\";\n };\n\n parent.childNodes.forEach((node) => {\n if (isBlockNode(node)) {\n flushInline();\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n } else {\n inlineRun += serializeInlineNode(node);\n }\n });\n flushInline();\n\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n // Backstop for an orphaned <li> that reattachOrphanListItems could not\n // place (e.g. nested inside another block). Without this it falls through\n // to the inline default and loses its marker — silent content loss.\n case \"li\":\n return \"- \" + serializeInline(el).trim();\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\n/** Escape a value for use inside a double-quoted HTML attribute (F164). */\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n out += serializeInlineNode(child);\n });\n return out;\n}\n\n/**\n * Serialize ONE inline node, applying ITS OWN tag's formatting.\n *\n * Split out of serializeInline because that function only ever looked at a\n * node's CHILDREN: handed an element it returned the element's text with the\n * element's own markup dropped. serializeBlock's default arm did exactly that,\n * so a <strong> (or <em>, <code>, <a>, <img>) sitting at the top level of an\n * edited field lost its formatting — or, for an image, vanished entirely — on\n * save. Same failure family as the orphaned <li>, one tag deeper.\n */\nfunction serializeInlineNode(child: Node): string {\n if (child.nodeType === Node.TEXT_NODE) {\n return (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return \"\";\n {\n let out = \"\";\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n const ref = el.getAttribute(\"data-cms-ref\");\n if (ref) {\n // F164 — a link to a PAGE on the site carries a reference so the page\n // can re-resolve href (and optionally the label) when that page moves\n // or is renamed. Markdown link syntax has nowhere to put that, so\n // emit inline HTML, which `marked` passes through untouched.\n // Collapsing to [text](href) here would strip the reference on the\n // editor's very FIRST save and silently kill the whole feature.\n const attrs = [`href=\"${escapeAttr(href)}\"`, `data-cms-ref=\"${escapeAttr(ref)}\"`];\n const label = el.getAttribute(\"data-cms-ref-label\");\n if (label) attrs.push(`data-cms-ref-label=\"${escapeAttr(label)}\"`);\n out += `<a ${attrs.join(\" \")}>${inner}</a>`;\n } else {\n out += href ? `[${inner}](${href})` : inner;\n }\n break;\n }\n case \"img\": {\n // Content images embedded in the body (![alt](src)) — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += `![${alt}](${src})`;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n return out;\n }\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n const slice = el.dataset.cmsSlice;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (slice !== undefined) {\n // Field-slice save: replace just this segment inside the full field value,\n // preserving every other segment + embed. Aborts on a non-unique match.\n const currentVal = typeof mergedData[field] === \"string\" ? (mergedData[field] as string) : \"\";\n mergedData[field] = applyFieldSlice(currentVal, slice, value);\n } else if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n // The slice we just wrote is now the current text — track it so a second\n // edit of the same segment (no reload) matches against the new value.\n if (slice !== undefined) el.dataset.cmsSlice = value;\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n // Clicking the badge LEAVES edit mode but KEEPS the login → back to the idle\n // \"Rediger\" pill (not a logout). Explicit logout lives on the idle pill.\n badge.addEventListener(\"click\", exitEditMode);\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n // The hover-outline + text-cursor affordance is gated on body[data-cms-editing]\n // so it appears ONLY while actively editing (after \"Rediger\"). In idle a\n // connected editor's fields look + behave like normal page text — no dashed\n // outline, no text cursor — so the page never looks editable before you opt in.\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; transition: outline-color .15s; }\n body[data-cms-editing=\"true\"] [data-cms-field] { cursor: text; }\n body[data-cms-editing=\"true\"] [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n","/**\n * Replace ONE slice of a larger text field.\n *\n * Used when an element renders a single SEGMENT of a bigger field — e.g. one\n * prose paragraph of a `content` body that is interleaved with `[block:]`\n * embeds — so editing that segment must write back into the full field without\n * clobbering the other segments or the embeds.\n *\n * Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous\n * or stale slice ABORTS the save (surfaced as the error pill) instead of\n * corrupting the whole field. Pure + framework-agnostic; unit-tested.\n */\nexport function applyFieldSlice(current: string, original: string, next: string): string {\n const first = current.indexOf(original);\n if (first === -1) throw new Error(\"field-slice: original not found\");\n if (current.indexOf(original, first + original.length) !== -1) {\n throw new Error(\"field-slice: original found more than once (ambiguous)\");\n }\n return current.slice(0, first) + next + current.slice(first + original.length);\n}\n","/**\n * F162.3 — token-safe serialization.\n *\n * Some CMS fields store auto-resolve TOKENS that expand at render time, e.g.\n * `\"{år} års erfaring\"` → `\"26 års erfaring\"` ({år} = years-since-2000). Plain\n * inline-edit saves the rendered `textContent`, which overwrites the token with\n * its expanded value (\"26\") and freezes the number forever.\n *\n * Fix without fragile re-alignment: the site renders each token as an ATOMIC\n * inline chip inside the editable field —\n * `{år}` → `<span data-cms-token=\"{år}\">26</span>`\n * — where the chip's TEXT is the rendered value and its `data-cms-token`\n * attribute holds the original token. While editing, the chip is made\n * contenteditable=false (an unbreakable unit: the user edits the words AROUND\n * it, never inside it). On save we serialise by walking child nodes: text nodes\n * contribute their text verbatim, a chip contributes its TOKEN. The stored\n * template's tokens therefore survive the edit — no diffing, no guessing.\n */\n\nconst TEXT_NODE = 3;\nconst ELEMENT_NODE = 1;\n\n/** True when the element contains at least one token chip → its save path must\n * use serializeTokenSafe instead of textContent. */\nexport function hasTokenChips(el: Element): boolean {\n return el.querySelector('[data-cms-token]') !== null;\n}\n\n/**\n * Re-tokenise an edited token-safe element back to its stored template form:\n * text nodes verbatim; each `[data-cms-token]` chip → its token value (NOT the\n * rendered value). Recurses so a chip wrapped in other markup still resolves.\n * A deleted chip simply drops its token — an intentional edit, not a bug.\n */\nexport function serializeTokenSafe(el: Node): string {\n let out = '';\n el.childNodes.forEach((node) => {\n if (node.nodeType === TEXT_NODE) {\n out += node.textContent ?? '';\n return;\n }\n if (node.nodeType !== ELEMENT_NODE) return;\n const element = node as Element;\n const token = element.getAttribute('data-cms-token');\n if (token) {\n out += token; // atomic chip → its token, NEVER the rendered value\n } else {\n out += serializeTokenSafe(element); // wrapper markup → recurse into it\n }\n });\n return out;\n}\n\n/** Lock every token chip so it edits as one unit (called on edit-enter). */\nexport function lockTokenChips(el: Element): void {\n el.querySelectorAll('[data-cms-token]').forEach((chip) => {\n chip.setAttribute('contenteditable', 'false');\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/field-slice.ts","../src/token-safe.ts"],"sourcesContent":["/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\nimport { applyFieldSlice } from \"./field-slice\";\nexport { applyFieldSlice } from \"./field-slice\";\nimport { serializeTokenSafe, hasTokenChips, lockTokenChips } from \"./token-safe\";\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n orderedList?: string;\n unorderedList?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n /** F164 — link dialog. */\n link?: string;\n linkTabPage?: string;\n linkTabUrl?: string;\n linkSearch?: string;\n linkText?: string;\n linkTextAuto?: string;\n linkTextAutoHint?: string;\n linkTextOwnHint?: string;\n linkLiveHint?: string;\n linkUrlHint?: string;\n linkUrl?: string;\n linkInsert?: string;\n linkSave?: string;\n linkCancel?: string;\n linkRemove?: string;\n linkRemoveConfirm?: string;\n linkYes?: string;\n linkNo?: string;\n linkEmpty?: string;\n linkLoading?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected + editing\" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Text for the explicit \"log out\" action on the idle pill — clicking it clears the token (back to the logged-out state). Default \"Log ud\". */\n logoutLabel?: string;\n /** Show an on-site \"connect\" pill to LOGGED-OUT visitors (click → connect flow).\n * Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only\n * enable for a site with no real customers (e.g. an internal/first-party site)\n * where a self-contained on-site login entry is wanted. The safe entry for a\n * customer site is always the \"Redigér live\" button in webhouse.app CMS-admin. */\n connectPrompt?: boolean;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n orderedList: \"Nummereret liste\",\n unorderedList: \"Punktliste\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n link: \"Link\",\n linkTabPage: \"Side på sitet\",\n linkTabUrl: \"Fri adresse\",\n linkSearch: \"Søg efter en side…\",\n linkText: \"Linktekst\",\n linkTextAuto: \"Følger sidens titel\",\n linkTextAutoHint:\n \"<b>Tom = følger sidens titel.</b> Omdøbes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den stående.\",\n linkTextOwnHint:\n \"Linket viser <b>din egen tekst</b>. Den bliver stående, også hvis siden omdøbes.\",\n linkLiveHint:\n \"Linket peger på <b>siden</b> — ikke på en adresse. Flyttes siden, eller får den et nyt navn, retter linket sig selv.\",\n linkUrlHint:\n \"En fri adresse peger præcis dér, du skriver. Den følger <b>ikke</b> med, hvis målet flytter sig.\",\n linkUrl: \"Adresse\",\n linkInsert: \"Indsæt link\",\n linkSave: \"Gem ændring\",\n linkCancel: \"Annullér\",\n linkRemove: \"Fjern link\",\n linkRemoveConfirm: \"Fjern?\",\n linkYes: \"Ja\",\n linkNo: \"Nej\",\n linkEmpty: \"Ingen sider fundet\",\n linkLoading: \"Henter sider…\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n logoutLabel: \"Log ud\",\n connectPrompt: false,\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n // Capture BEFORE checkEnabled so we know whether the token JUST arrived from\n // the connect redirect (→ drop straight into edit mode) vs. a normal load\n // with a stored token (→ idle \"Rediger\" pill, don't force edit mode).\n const freshFromUrl = captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (!token) {\n // Logged-out visitor / customer. DEFAULT: show NOTHING (customer-safe) —\n // the consumer can now call initInlineEdit() unconditionally without leaking\n // an edit affordance. Only sites that opt in (connectPrompt: true) get the\n // on-site connect pill + its popup token receiver.\n if (resolved.connectPrompt) {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n return;\n }\n\n // Connected editor. Wire fields once, then pick the entry state: arrived from\n // connect → edit now; normal load with a stored token → idle \"Rediger\" pill\n // (one click enters edit mode; the token persists so \"Rediger\" stays visible\n // on every page while logged in — no trip back to admin).\n setupFields(token, resolved);\n if (freshFromUrl) enterEditMode();\n else showIdlePill();\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n // Fresh connect via popup → wire fields + drop straight into edit mode.\n setupFields(data.token, options);\n enterEditMode();\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token) || !isForThisSite(token, resolved.siteId)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/**\n * Is this token scoped to the site this page belongs to?\n *\n * NOT an authorization check — the server is, and it holds: proxy.ts refuses a\n * token whose `site` claim differs from the request's site, measured 2026-08-17\n * at 403 on both GET and PATCH. This is a UX guard. Without it the editor\n * entered edit mode with a foreign-site token, made the page look editable, and\n * then failed EVERY save — and the natural reaction is to hunt for the bug in\n * your own site rather than suspect the token (reported by the sanneandersen\n * session, who predicted exactly that dead end). Better to not offer editing at\n * all than to offer it and lose the work.\n *\n * A token that cannot be read at all is let through: the server decides, and an\n * unreadable token simply fails there instead of being silently discarded here.\n */\nfunction isForThisSite(token: string, siteId: string): boolean {\n const claims = decodeJwtPayload(token);\n const site = claims?.site;\n if (typeof site !== \"string\" || !site) return true; // not ours to judge\n return site === siteId;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from\n * the URL. Returns true when a FRESH token was just captured from the URL\n * (i.e. this is the connect-redirect landing) so the caller can auto-enter\n * edit mode; false on a normal load where the token comes from localStorage. */\nfunction captureTokenFromUrl(options: ResolvedOptions): boolean {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return false;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n return true;\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\n// Toolbar list-button faces (lucide \"list\" / \"list-ordered\"), inline so no\n// icon-font/runtime dep; stroke inherits the button's white `currentColor`.\nconst UL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/>' +\n '<line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>' +\n \"</svg>\";\n\nconst OL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/>' +\n '<path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/>' +\n \"</svg>\";\n\nconst LINK_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7\"/>' +\n '<path d=\"M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\n// ─── Connected-editor state machine ────────────────────────────────────────\n// A connected editor (valid token) is ALWAYS in one of two visible states:\n// idle → a \"Rediger\" pill (options.connectLabel); one click enters editing.\n// editing → fields are contenteditable + an \"Afslut redigering\" pill.\n// Leaving edit mode returns to idle and KEEPS the token — the editor stays\n// logged in and can re-enter with one click, on every page, for the token's\n// whole life. Only an explicit \"Log ud\" clears the token (→ logged-out state:\n// no pill at all, same as a customer). A field is wired at most ONCE (idempotent\n// via wiredFields); their click handlers no-op while editingActive is false, so\n// idle is truly inert. On an SPA (soft-navigation, no full reload) new pages add\n// fresh [data-cms-field] nodes to the DOM AFTER init — the consumer calls\n// rescanFields() (e.g. on route change) to wire those late arrivals.\nlet editingActive = false;\nlet fieldsWired = false;\nlet stateOptions: ResolvedOptions | null = null;\n// Elements whose listeners are already attached — makes wireField idempotent so\n// setupFields (once) and rescanFields (N times) can both run without double-wiring.\nconst wiredFields = new WeakSet<HTMLElement>();\n\nfunction setupFields(token: string, options: ResolvedOptions): void {\n stateOptions = options;\n if (fieldsWired) return;\n injectStyles();\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, options));\n observeNewFields();\n fieldsWired = true;\n}\n\nlet fieldObserver: MutationObserver | null = null;\n\n/**\n * Re-wire fields that appear after the first scan, without the consumer having\n * to ask.\n *\n * rescanFields() below has existed for this since day one, and its own comment\n * says \"a consumer calls this on every route change\". webhouse.dk did not, and\n * the result was the worst shape a failure can take: the badge kept saying\n * AFSLUT REDIGERING while every field on every page reached by clicking a link\n * was dead. Nothing errored. Reported by the site owner, reproduced in a\n * browser — after a soft navigation the clicked field's contenteditable stayed\n * null and no toolbar appeared.\n *\n * A contract that depends on every consumer remembering a call is a contract\n * that will be broken silently, so the package now keeps its own promise.\n * rescanFields() stays exported and still works — a consumer that already calls\n * it is unaffected, and wireField is idempotent, so the two cannot conflict.\n *\n * Cheap by construction: the observer only fires on childList changes and the\n * rescan is coalesced into one animation frame, so a page inserting many nodes\n * costs one scan, not one per node.\n */\nfunction observeNewFields(): void {\n if (fieldObserver || typeof MutationObserver === \"undefined\") return;\n let queued = false;\n fieldObserver = new MutationObserver(() => {\n if (queued) return;\n queued = true;\n requestAnimationFrame(() => {\n queued = false;\n rescanFields();\n });\n });\n fieldObserver.observe(document.body, { childList: true, subtree: true });\n}\n\n/**\n * Wire any [data-cms-field] elements added to the DOM since the last scan.\n * Needed on single-page apps (Next.js et al.): the layout mounts once → init +\n * setupFields run once → only the first page's fields get listeners. Soft\n * navigation swaps in new pages' fields WITHOUT re-running init, so their\n * click-to-edit never activates. A consumer calls this on every route change.\n *\n * Safe + idempotent by design:\n * - No-ops unless a connected, non-expired token exists (same guard as init) —\n * a consumer may call it unconditionally without leaking an edit affordance.\n * - wireField skips already-wired elements (wiredFields WeakSet), so re-scanning\n * the whole document costs nothing for existing fields.\n * - Does NOT touch edit-mode state: editingActive (module-level) and\n * body[data-cms-editing] persist across a soft-nav, so freshly-wired fields\n * are immediately live when the editor is mid-session — no re-enter needed.\n * MPA consumers (broberg: full reload per navigation re-runs init) never need it.\n */\nexport function rescanFields(): void {\n if (typeof window === \"undefined\" || !stateOptions) return;\n const token = localStorage.getItem(stateOptions.storageKey);\n if (!token || isExpired(token)) return;\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, stateOptions!));\n}\n\nfunction enterEditMode(): void {\n if (!stateOptions || editingActive) return;\n editingActive = true;\n // Marks the page as actively editing → unlocks the field hover-outline +\n // text-cursor affordance (see injectStyles). In idle the fields look/behave\n // like normal text; the edit affordance appears only after \"Rediger\".\n document.body.setAttribute(\"data-cms-editing\", \"true\");\n removeIdlePill();\n showActiveBadge(stateOptions);\n}\n\nfunction exitEditMode(): void {\n if (!stateOptions || !editingActive) return;\n deactivateRich(); // commit any active rich region first\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n document.querySelector(\"[data-cms-inline-edit-badge]\")?.remove();\n showIdlePill();\n}\n\nfunction removeIdlePill(): void {\n document.querySelector(\"[data-cms-inline-edit-idle]\")?.remove();\n}\n\nfunction showIdlePill(): void {\n if (!stateOptions) return;\n // Invariant: the idle pill is showing ⟹ we are NOT editing. Force the flag\n // false here so \"Rediger\" (enterEditMode) can never become a no-op if the\n // state ever diverged from the visible pill (defensive — enterEditMode bails\n // when editingActive is already true).\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n const options = stateOptions;\n removeIdlePill();\n const wrap = document.createElement(\"div\");\n wrap.setAttribute(\"data-cms-inline-edit-idle\", \"\");\n wrap.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;\" +\n \"z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;\";\n // Main action: \"Rediger\" → enter edit mode (no server round-trip; the token\n // already exists locally).\n const edit = document.createElement(\"button\");\n edit.type = \"button\";\n edit.setAttribute(\"data-cms-inline-edit-enter\", \"\");\n edit.style.cssText =\n \"display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;\";\n const editText = document.createElement(\"span\");\n editText.textContent = options.connectLabel;\n edit.append(makeIcon(), editText);\n edit.addEventListener(\"click\", enterEditMode);\n // Secondary: \"Log ud\" → clear the token (back to the logged-out state).\n const out = document.createElement(\"button\");\n out.type = \"button\";\n out.setAttribute(\"data-cms-inline-edit-logout\", \"\");\n out.textContent = options.logoutLabel;\n out.style.cssText =\n \"background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;\" +\n \"border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;\";\n out.addEventListener(\"click\", () => {\n disconnect(options);\n removeIdlePill();\n });\n wrap.append(edit, out);\n document.body.appendChild(wrap);\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n if (wiredFields.has(el)) return; // idempotent — safe to re-scan (rescanFields)\n wiredFields.add(el);\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n // Token-safe fields render {år}-style auto-values as atomic chips. Read the\n // TEMPLATE form (chips → their tokens) so the saved value keeps its tokens,\n // and lock the chips so each edits as one unbreakable unit.\n const tokenSafe = hasTokenChips(el);\n el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n el.setAttribute(\"contenteditable\", \"true\");\n if (tokenSafe) lockTokenChips(el);\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = hasTokenChips(el) ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n hideLinkDialog();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Bullet + numbered lists — native execCommand, and the Markdown serializer\n // already round-trips <ul>/<ol> to \"- \" / \"1. \" so no save-path change needed.\n t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand(\"insertUnorderedList\")));\n t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand(\"insertOrderedList\")));\n\n t.appendChild(sep());\n\n // F164 — link: a free URL, or a live reference to a page on the site.\n const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));\n linkBtn.setAttribute(\"data-testid\", \"inline-toolbar-link\");\n t.appendChild(linkBtn);\n\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\n/* ------------------------------------------------------------------ F164 --\n * Link dialog: a free URL, or a LIVE reference to a page on the site.\n *\n * A page link stores data-cms-ref=\"collection:slug\" next to a real working\n * href, so resolveCmsLinks() (see ./server) can re-point it when the page moves\n * or is renamed. Leaving the label empty marks it data-cms-ref-label=\"auto\" so\n * the link shows the page's current title.\n * -------------------------------------------------------------------------- */\n\ninterface LinkablePage {\n collection: string;\n slug: string;\n title: string;\n path: string;\n label: string;\n}\n\nlet linkDialog: HTMLElement | null = null;\nlet linkPages: LinkablePage[] | null = null;\nlet linkPicked: LinkablePage | null = null;\nlet linkEditing: HTMLAnchorElement | null = null;\n\nfunction hideLinkDialog(): void {\n if (linkDialog) linkDialog.style.display = \"none\";\n linkPicked = null;\n linkEditing = null;\n}\n\n/** The <a> the caret sits inside, if any — so clicking Link EDITS it. */\nfunction anchorAtCaret(): HTMLAnchorElement | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n let node: Node | null = sel.getRangeAt(0).startContainer;\n while (node && node !== document.body) {\n if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === \"A\") {\n return node as HTMLAnchorElement;\n }\n node = node.parentNode;\n }\n return null;\n}\n\nasync function fetchLinkablePages(): Promise<LinkablePage[]> {\n if (linkPages) return linkPages;\n if (!richCtx) return [];\n const { options, token } = richCtx;\n const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;\n const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });\n if (!res.ok) throw new Error(`pages ${res.status}`);\n const body = (await res.json()) as { pages?: LinkablePage[] };\n linkPages = body.pages ?? [];\n return linkPages;\n}\n\nfunction toggleLinkDialog(anchor: HTMLElement): void {\n if (linkDialog && linkDialog.style.display === \"block\") {\n hideLinkDialog();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n linkEditing = anchorAtCaret();\n if (!linkDialog) linkDialog = buildLinkDialog();\n renderLinkDialog();\n const rect = anchor.getBoundingClientRect();\n linkDialog.style.top = `${rect.bottom + 8}px`;\n linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;\n linkDialog.style.display = \"block\";\n}\n\nfunction buildLinkDialog(): HTMLElement {\n const d = document.createElement(\"div\");\n d.setAttribute(\"data-cms-inline-edit-toolbar\", \"\"); // shares the \"don't commit on click\" guard\n d.setAttribute(\"data-testid\", \"inline-link-dialog\");\n d.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;color:#fff;overflow:hidden;\";\n // Keep the page selection alive while interacting with the dialog.\n d.addEventListener(\"mousedown\", (e) => {\n const t = e.target as HTMLElement;\n if (t.tagName !== \"INPUT\") e.preventDefault();\n e.stopPropagation();\n });\n document.body.appendChild(d);\n return d;\n}\n\nfunction renderLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const L = uiLabels;\n const editing = !!linkEditing;\n const existingRef = linkEditing?.getAttribute(\"data-cms-ref\") ?? \"\";\n const onPageTab = !editing || !!existingRef;\n\n d.innerHTML =\n '<div style=\"display:flex;gap:4px;padding:8px 8px 0\">' +\n `<button type=\"button\" data-testid=\"inline-link-tab-page\" data-tab=\"page\" style=\"${tabCss(onPageTab)}\">${L.linkTabPage}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-tab-url\" data-tab=\"url\" style=\"${tabCss(!onPageTab)}\">${L.linkTabUrl}</button>` +\n \"</div>\" +\n '<div style=\"padding:10px 12px 12px\">' +\n `<div data-pane=\"page\" style=\"display:${onPageTab ? \"block\" : \"none\"}\">` +\n `<input data-testid=\"inline-link-search\" data-role=\"search\" placeholder=\"${L.linkSearch}\" style=\"${fieldCss()}\">` +\n `<div data-role=\"list\" data-testid=\"inline-link-list\" style=\"margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821\"></div>` +\n \"</div>\" +\n `<div data-pane=\"url\" style=\"display:${onPageTab ? \"none\" : \"block\"}\">` +\n `<label style=\"${labelCss()}\">${L.linkUrl}</label>` +\n `<input data-testid=\"inline-link-url\" data-role=\"url\" placeholder=\"https://…\" style=\"${fieldCss()}\">` +\n \"</div>\" +\n `<label style=\"${labelCss()}\">${L.linkText}</label>` +\n `<input data-testid=\"inline-link-text\" data-role=\"text\" placeholder=\"${L.linkTextAuto}\" style=\"${fieldCss()}\">` +\n `<p data-role=\"hint\" style=\"font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5\"></p>` +\n `<div data-role=\"live\" style=\"display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px\">` +\n `<p style=\"margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55\">${L.linkLiveHint}</p></div>` +\n '<div style=\"display:flex;gap:8px;align-items:center;margin-top:14px\">' +\n '<div data-role=\"remove\"></div><div style=\"flex:1\"></div>' +\n `<button type=\"button\" data-testid=\"inline-link-cancel\" data-role=\"cancel\" style=\"${btnCss(false)}\">${L.linkCancel}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-submit\" data-role=\"submit\" style=\"${btnCss(true)}\" disabled>${editing ? L.linkSave : L.linkInsert}</button>` +\n \"</div></div>\";\n\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const text = q<HTMLInputElement>(\"text\");\n const urlIn = q<HTMLInputElement>(\"url\");\n const search = q<HTMLInputElement>(\"search\");\n\n if (editing) {\n text.value = linkEditing?.getAttribute(\"data-cms-ref-label\") === \"auto\" ? \"\" : (linkEditing?.textContent ?? \"\");\n if (!existingRef) urlIn.value = linkEditing?.getAttribute(\"href\") ?? \"\";\n q<HTMLElement>(\"remove\").appendChild(buildRemoveLink());\n }\n\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((btn) => {\n btn.onclick = () => {\n const page = btn.dataset.tab === \"page\";\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((b) => {\n b.setAttribute(\"style\", tabCss(b.dataset.tab === \"page\" ? page : !page));\n });\n (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display = page ? \"block\" : \"none\";\n (d.querySelector('[data-pane=\"url\"]') as HTMLElement).style.display = page ? \"none\" : \"block\";\n q<HTMLElement>(\"live\").style.display = page ? \"flex\" : \"none\";\n syncLinkDialog();\n };\n });\n\n search.oninput = () => renderLinkList(search.value);\n text.oninput = syncLinkDialog;\n urlIn.oninput = syncLinkDialog;\n q<HTMLElement>(\"cancel\").onclick = hideLinkDialog;\n q<HTMLElement>(\"submit\").onclick = applyLink;\n\n renderLinkList(\"\");\n syncLinkDialog();\n}\n\nfunction buildRemoveLink(): HTMLElement {\n const wrap = document.createElement(\"div\");\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.setAttribute(\"data-testid\", \"inline-link-remove\");\n btn.textContent = uiLabels.linkRemove;\n btn.style.cssText =\n \"background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;\";\n btn.onclick = () => {\n wrap.innerHTML =\n `<span style=\"font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px\">${uiLabels.linkRemoveConfirm}</span>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-yes\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkYes}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-no\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkNo}</button>`;\n (wrap.querySelector('[data-testid=\"inline-link-remove-no\"]') as HTMLElement).onclick = () => {\n wrap.innerHTML = \"\";\n wrap.appendChild(btn);\n };\n (wrap.querySelector('[data-testid=\"inline-link-remove-yes\"]') as HTMLElement).onclick = () => {\n if (linkEditing) {\n const parent = linkEditing.parentNode;\n while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);\n parent?.removeChild(linkEditing);\n }\n hideLinkDialog();\n };\n };\n wrap.appendChild(btn);\n return wrap;\n}\n\nfunction renderLinkList(filter: string): void {\n const d = linkDialog;\n if (!d) return;\n const list = d.querySelector('[data-role=\"list\"]') as HTMLElement | null;\n if (!list) return;\n\n const paint = (pages: LinkablePage[]) => {\n const f = filter.trim().toLowerCase();\n const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));\n if (!shown.length) {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkEmpty}</div>`;\n return;\n }\n list.innerHTML = \"\";\n shown.forEach((p) => {\n const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;\n const row = document.createElement(\"div\");\n row.setAttribute(\"data-testid\", `inline-link-page-${p.collection}-${p.slug}`);\n row.style.cssText =\n \"display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;\" +\n \"border-bottom:1px solid #232833;\" +\n (on ? \"background:rgba(0,178,255,.14);\" : \"\");\n row.innerHTML =\n `<div style=\"min-width:0;flex:1\"><div style=\"font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.title)}</div>` +\n `<div style=\"font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.path)}</div></div>` +\n `<span style=\"flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap\">${escapeHtml(p.label)}</span>`;\n row.onclick = () => {\n linkPicked = p;\n renderLinkList(filter);\n syncLinkDialog();\n };\n list.appendChild(row);\n });\n };\n\n if (linkPages) {\n paint(linkPages);\n return;\n }\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkLoading}</div>`;\n fetchLinkablePages()\n .then((pages) => {\n // Pre-select the page an existing reference already points at.\n const ref = linkEditing?.getAttribute(\"data-cms-ref\");\n if (ref && !linkPicked) {\n const [c, ...rest] = ref.split(\":\");\n linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(\":\")) ?? null;\n }\n paint(pages);\n syncLinkDialog();\n })\n .catch(() => {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#ff8a8a\">${uiLabels.error}</div>`;\n });\n}\n\nfunction syncLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n q<HTMLElement>(\"hint\").innerHTML = own\n ? (uiLabels.linkTextOwnHint as string)\n : onPage\n ? (uiLabels.linkTextAutoHint as string)\n : (uiLabels.linkUrlHint as string);\n q<HTMLElement>(\"live\").style.display = onPage ? \"flex\" : \"none\";\n (q<HTMLButtonElement>(\"submit\")).disabled = onPage\n ? !linkPicked\n : !q<HTMLInputElement>(\"url\").value.trim();\n}\n\nfunction applyLink(): void {\n const d = linkDialog;\n if (!d || !richCtx) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n const href = onPage ? (linkPicked?.path ?? \"\") : q<HTMLInputElement>(\"url\").value.trim();\n if (!href) return;\n\n const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : \"\";\n const label = own || (onPage ? (linkPicked?.title ?? href) : href);\n\n if (linkEditing) {\n linkEditing.setAttribute(\"href\", href);\n if (ref) linkEditing.setAttribute(\"data-cms-ref\", ref);\n else linkEditing.removeAttribute(\"data-cms-ref\");\n if (ref && !own) linkEditing.setAttribute(\"data-cms-ref-label\", \"auto\");\n else linkEditing.removeAttribute(\"data-cms-ref-label\");\n linkEditing.textContent = label;\n hideLinkDialog();\n return;\n }\n\n const a = document.createElement(\"a\");\n a.setAttribute(\"href\", href);\n if (ref) {\n a.setAttribute(\"data-cms-ref\", ref);\n if (!own) a.setAttribute(\"data-cms-ref-label\", \"auto\");\n }\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n const range = sel.getRangeAt(0);\n a.textContent = own || range.toString() || label;\n range.deleteContents();\n range.insertNode(a);\n sel.removeAllRanges();\n } else {\n a.textContent = label;\n richCtx.el.appendChild(a);\n }\n savedRange = null;\n hideLinkDialog();\n}\n\nfunction escapeHtml(v: string): string {\n return v.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nconst tabCss = (on: boolean) =>\n \"flex:1;background:\" +\n (on ? \"#2a2f38\" : \"none\") +\n \";border:1px solid \" +\n (on ? \"#3a3f4a\" : \"transparent\") +\n \";color:\" +\n (on ? \"#fff\" : \"#9aa3b2\") +\n \";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;\";\n\nconst fieldCss = () =>\n \"width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;\" +\n \"border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;\";\n\nconst labelCss = () =>\n \"font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;\";\n\nconst btnCss = (primary: boolean) =>\n \"height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;\" +\n (primary\n ? \"background:#00b2ff;color:#04121b;border:none;\"\n : \"background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;\");\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nexport function htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n reattachOrphanListItems(container);\n const md = serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim();\n // A whole document keeps its trailing newline — that is the file convention\n // a `richtext` body is stored with. A SINGLE-LINE inline field must NOT gain\n // one: a heading is not a document, and the newline is stored, shows up in\n // the CMS editor, and appears out of nowhere the first time anyone edits the\n // field. Measured on webhouse.dk 2026-08-24, where every heading became a\n // rich field: \"Drift & Infrastruktur\" came back as \"Drift & Infrastruktur\\n\".\n return md.includes(\"\\n\") ? md + \"\\n\" : md;\n}\n\nfunction isList(el: Element | null): el is HTMLElement {\n if (!el) return false;\n const tag = el.tagName.toLowerCase();\n return tag === \"ul\" || tag === \"ol\";\n}\n\n/**\n * A contenteditable region can end up with an <li> as a DIRECT child of the\n * field container — browsers routinely split a list and orphan an item when the\n * caret sits in it, and `execCommand(\"insertUnorderedList\")` does the same.\n * Serializing an orphan through the inline default silently drops its marker,\n * so a list item degrades into a paragraph and the stored Markdown is corrupted\n * (sanneandersen, 2026-08-16: a 13-item CV list saved back as 1 paragraph + 12\n * bullets on a public page, on every single save).\n *\n * Put each orphan back in the adjacent list — before it if the list follows,\n * after it if the list precedes — so author order survives. An orphan with no\n * list next to it gets its own.\n */\nfunction reattachOrphanListItems(container: HTMLElement): void {\n const orphans = Array.from(container.children).filter(\n (c) => c.tagName.toLowerCase() === \"li\",\n );\n for (const li of orphans) {\n const prev = li.previousElementSibling;\n const next = li.nextElementSibling;\n if (isList(prev)) {\n prev.appendChild(li);\n } else if (isList(next)) {\n next.insertBefore(li, next.firstChild);\n } else {\n const list = container.ownerDocument.createElement(\"ul\");\n li.replaceWith(list);\n list.appendChild(li);\n }\n }\n}\n\n/** Tags serializeBlock treats as their own block; everything else is inline. */\nconst BLOCK_TAGS = new Set([\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\",\n \"p\", \"div\", \"ul\", \"ol\", \"li\", \"blockquote\", \"pre\", \"hr\", \"table\",\n]);\n\nfunction isBlockNode(node: Node): boolean {\n return (\n node.nodeType === Node.ELEMENT_NODE &&\n BLOCK_TAGS.has((node as HTMLElement).tagName.toLowerCase())\n );\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n // Text and inline elements sitting directly at block level form ONE implicit\n // paragraph. Serialising them one-by-one and joining with \"\\n\\n\" tore a\n // single sentence into separate blocks (\"Noget <b>fedt</b> mere\" became three\n // paragraphs) and dropped each element's own formatting. Gather the run, then\n // serialise it as the one paragraph the author actually wrote.\n let inlineRun = \"\";\n const flushInline = () => {\n const s = inlineRun.trim();\n if (s) blocks.push(s);\n inlineRun = \"\";\n };\n\n parent.childNodes.forEach((node) => {\n if (isBlockNode(node)) {\n flushInline();\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n } else {\n inlineRun += serializeInlineNode(node);\n }\n });\n flushInline();\n\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n // Backstop for an orphaned <li> that reattachOrphanListItems could not\n // place (e.g. nested inside another block). Without this it falls through\n // to the inline default and loses its marker — silent content loss.\n case \"li\":\n return \"- \" + serializeInline(el).trim();\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\n/** Escape a value for use inside a double-quoted HTML attribute (F164). */\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n out += serializeInlineNode(child);\n });\n return out;\n}\n\n/**\n * Serialize ONE inline node, applying ITS OWN tag's formatting.\n *\n * Split out of serializeInline because that function only ever looked at a\n * node's CHILDREN: handed an element it returned the element's text with the\n * element's own markup dropped. serializeBlock's default arm did exactly that,\n * so a <strong> (or <em>, <code>, <a>, <img>) sitting at the top level of an\n * edited field lost its formatting — or, for an image, vanished entirely — on\n * save. Same failure family as the orphaned <li>, one tag deeper.\n */\nfunction serializeInlineNode(child: Node): string {\n if (child.nodeType === Node.TEXT_NODE) {\n return (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return \"\";\n {\n let out = \"\";\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n const ref = el.getAttribute(\"data-cms-ref\");\n if (ref) {\n // F164 — a link to a PAGE on the site carries a reference so the page\n // can re-resolve href (and optionally the label) when that page moves\n // or is renamed. Markdown link syntax has nowhere to put that, so\n // emit inline HTML, which `marked` passes through untouched.\n // Collapsing to [text](href) here would strip the reference on the\n // editor's very FIRST save and silently kill the whole feature.\n const attrs = [`href=\"${escapeAttr(href)}\"`, `data-cms-ref=\"${escapeAttr(ref)}\"`];\n const label = el.getAttribute(\"data-cms-ref-label\");\n if (label) attrs.push(`data-cms-ref-label=\"${escapeAttr(label)}\"`);\n out += `<a ${attrs.join(\" \")}>${inner}</a>`;\n } else {\n out += href ? `[${inner}](${href})` : inner;\n }\n break;\n }\n case \"img\": {\n // Content images embedded in the body (![alt](src)) — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += `![${alt}](${src})`;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n return out;\n }\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n const slice = el.dataset.cmsSlice;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (slice !== undefined) {\n // Field-slice save: replace just this segment inside the full field value,\n // preserving every other segment + embed. Aborts on a non-unique match.\n const currentVal = typeof mergedData[field] === \"string\" ? (mergedData[field] as string) : \"\";\n mergedData[field] = applyFieldSlice(currentVal, slice, value);\n } else if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n // The slice we just wrote is now the current text — track it so a second\n // edit of the same segment (no reload) matches against the new value.\n if (slice !== undefined) el.dataset.cmsSlice = value;\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n // Clicking the badge LEAVES edit mode but KEEPS the login → back to the idle\n // \"Rediger\" pill (not a logout). Explicit logout lives on the idle pill.\n badge.addEventListener(\"click\", exitEditMode);\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n // The hover-outline + text-cursor affordance is gated on body[data-cms-editing]\n // so it appears ONLY while actively editing (after \"Rediger\"). In idle a\n // connected editor's fields look + behave like normal page text — no dashed\n // outline, no text cursor — so the page never looks editable before you opt in.\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; transition: outline-color .15s; }\n body[data-cms-editing=\"true\"] [data-cms-field] { cursor: text; }\n body[data-cms-editing=\"true\"] [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n","/**\n * Replace ONE slice of a larger text field.\n *\n * Used when an element renders a single SEGMENT of a bigger field — e.g. one\n * prose paragraph of a `content` body that is interleaved with `[block:]`\n * embeds — so editing that segment must write back into the full field without\n * clobbering the other segments or the embeds.\n *\n * Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous\n * or stale slice ABORTS the save (surfaced as the error pill) instead of\n * corrupting the whole field. Pure + framework-agnostic; unit-tested.\n */\nexport function applyFieldSlice(current: string, original: string, next: string): string {\n const first = current.indexOf(original);\n if (first === -1) throw new Error(\"field-slice: original not found\");\n if (current.indexOf(original, first + original.length) !== -1) {\n throw new Error(\"field-slice: original found more than once (ambiguous)\");\n }\n return current.slice(0, first) + next + current.slice(first + original.length);\n}\n","/**\n * F162.3 — token-safe serialization.\n *\n * Some CMS fields store auto-resolve TOKENS that expand at render time, e.g.\n * `\"{år} års erfaring\"` → `\"26 års erfaring\"` ({år} = years-since-2000). Plain\n * inline-edit saves the rendered `textContent`, which overwrites the token with\n * its expanded value (\"26\") and freezes the number forever.\n *\n * Fix without fragile re-alignment: the site renders each token as an ATOMIC\n * inline chip inside the editable field —\n * `{år}` → `<span data-cms-token=\"{år}\">26</span>`\n * — where the chip's TEXT is the rendered value and its `data-cms-token`\n * attribute holds the original token. While editing, the chip is made\n * contenteditable=false (an unbreakable unit: the user edits the words AROUND\n * it, never inside it). On save we serialise by walking child nodes: text nodes\n * contribute their text verbatim, a chip contributes its TOKEN. The stored\n * template's tokens therefore survive the edit — no diffing, no guessing.\n */\n\nconst TEXT_NODE = 3;\nconst ELEMENT_NODE = 1;\n\n/** True when the element contains at least one token chip → its save path must\n * use serializeTokenSafe instead of textContent. */\nexport function hasTokenChips(el: Element): boolean {\n return el.querySelector('[data-cms-token]') !== null;\n}\n\n/**\n * Re-tokenise an edited token-safe element back to its stored template form:\n * text nodes verbatim; each `[data-cms-token]` chip → its token value (NOT the\n * rendered value). Recurses so a chip wrapped in other markup still resolves.\n * A deleted chip simply drops its token — an intentional edit, not a bug.\n */\nexport function serializeTokenSafe(el: Node): string {\n let out = '';\n el.childNodes.forEach((node) => {\n if (node.nodeType === TEXT_NODE) {\n out += node.textContent ?? '';\n return;\n }\n if (node.nodeType !== ELEMENT_NODE) return;\n const element = node as Element;\n const token = element.getAttribute('data-cms-token');\n if (token) {\n out += token; // atomic chip → its token, NEVER the rendered value\n } else {\n out += serializeTokenSafe(element); // wrapper markup → recurse into it\n }\n });\n return out;\n}\n\n/** Lock every token chip so it edits as one unit (called on edit-enter). */\nexport function lockTokenChips(el: Element): void {\n el.querySelectorAll('[data-cms-token]').forEach((chip) => {\n chip.setAttribute('contenteditable', 'false');\n });\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -214,8 +214,23 @@ function setupFields(token, options) {
214
214
  if (fieldsWired) return;
215
215
  injectStyles();
216
216
  document.querySelectorAll("[data-cms-field]").forEach((el) => wireField(el, token, options));
217
+ observeNewFields();
217
218
  fieldsWired = true;
218
219
  }
220
+ var fieldObserver = null;
221
+ function observeNewFields() {
222
+ if (fieldObserver || typeof MutationObserver === "undefined") return;
223
+ let queued = false;
224
+ fieldObserver = new MutationObserver(() => {
225
+ if (queued) return;
226
+ queued = true;
227
+ requestAnimationFrame(() => {
228
+ queued = false;
229
+ rescanFields();
230
+ });
231
+ });
232
+ fieldObserver.observe(document.body, { childList: true, subtree: true });
233
+ }
219
234
  function rescanFields() {
220
235
  if (typeof window === "undefined" || !stateOptions) return;
221
236
  const token = localStorage.getItem(stateOptions.storageKey);
@@ -729,7 +744,8 @@ function htmlToMarkdown(html) {
729
744
  const container = document.createElement("div");
730
745
  container.innerHTML = html;
731
746
  reattachOrphanListItems(container);
732
- return serializeBlockChildren(container).replace(/\n{3,}/g, "\n\n").trim() + "\n";
747
+ const md = serializeBlockChildren(container).replace(/\n{3,}/g, "\n\n").trim();
748
+ return md.includes("\n") ? md + "\n" : md;
733
749
  }
734
750
  function isList(el) {
735
751
  if (!el) return false;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/field-slice.ts","../src/token-safe.ts","../src/index.ts"],"sourcesContent":["/**\n * Replace ONE slice of a larger text field.\n *\n * Used when an element renders a single SEGMENT of a bigger field — e.g. one\n * prose paragraph of a `content` body that is interleaved with `[block:]`\n * embeds — so editing that segment must write back into the full field without\n * clobbering the other segments or the embeds.\n *\n * Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous\n * or stale slice ABORTS the save (surfaced as the error pill) instead of\n * corrupting the whole field. Pure + framework-agnostic; unit-tested.\n */\nexport function applyFieldSlice(current: string, original: string, next: string): string {\n const first = current.indexOf(original);\n if (first === -1) throw new Error(\"field-slice: original not found\");\n if (current.indexOf(original, first + original.length) !== -1) {\n throw new Error(\"field-slice: original found more than once (ambiguous)\");\n }\n return current.slice(0, first) + next + current.slice(first + original.length);\n}\n","/**\n * F162.3 — token-safe serialization.\n *\n * Some CMS fields store auto-resolve TOKENS that expand at render time, e.g.\n * `\"{år} års erfaring\"` → `\"26 års erfaring\"` ({år} = years-since-2000). Plain\n * inline-edit saves the rendered `textContent`, which overwrites the token with\n * its expanded value (\"26\") and freezes the number forever.\n *\n * Fix without fragile re-alignment: the site renders each token as an ATOMIC\n * inline chip inside the editable field —\n * `{år}` → `<span data-cms-token=\"{år}\">26</span>`\n * — where the chip's TEXT is the rendered value and its `data-cms-token`\n * attribute holds the original token. While editing, the chip is made\n * contenteditable=false (an unbreakable unit: the user edits the words AROUND\n * it, never inside it). On save we serialise by walking child nodes: text nodes\n * contribute their text verbatim, a chip contributes its TOKEN. The stored\n * template's tokens therefore survive the edit — no diffing, no guessing.\n */\n\nconst TEXT_NODE = 3;\nconst ELEMENT_NODE = 1;\n\n/** True when the element contains at least one token chip → its save path must\n * use serializeTokenSafe instead of textContent. */\nexport function hasTokenChips(el: Element): boolean {\n return el.querySelector('[data-cms-token]') !== null;\n}\n\n/**\n * Re-tokenise an edited token-safe element back to its stored template form:\n * text nodes verbatim; each `[data-cms-token]` chip → its token value (NOT the\n * rendered value). Recurses so a chip wrapped in other markup still resolves.\n * A deleted chip simply drops its token — an intentional edit, not a bug.\n */\nexport function serializeTokenSafe(el: Node): string {\n let out = '';\n el.childNodes.forEach((node) => {\n if (node.nodeType === TEXT_NODE) {\n out += node.textContent ?? '';\n return;\n }\n if (node.nodeType !== ELEMENT_NODE) return;\n const element = node as Element;\n const token = element.getAttribute('data-cms-token');\n if (token) {\n out += token; // atomic chip → its token, NEVER the rendered value\n } else {\n out += serializeTokenSafe(element); // wrapper markup → recurse into it\n }\n });\n return out;\n}\n\n/** Lock every token chip so it edits as one unit (called on edit-enter). */\nexport function lockTokenChips(el: Element): void {\n el.querySelectorAll('[data-cms-token]').forEach((chip) => {\n chip.setAttribute('contenteditable', 'false');\n });\n}\n","/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\nimport { applyFieldSlice } from \"./field-slice\";\nexport { applyFieldSlice } from \"./field-slice\";\nimport { serializeTokenSafe, hasTokenChips, lockTokenChips } from \"./token-safe\";\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n orderedList?: string;\n unorderedList?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n /** F164 — link dialog. */\n link?: string;\n linkTabPage?: string;\n linkTabUrl?: string;\n linkSearch?: string;\n linkText?: string;\n linkTextAuto?: string;\n linkTextAutoHint?: string;\n linkTextOwnHint?: string;\n linkLiveHint?: string;\n linkUrlHint?: string;\n linkUrl?: string;\n linkInsert?: string;\n linkSave?: string;\n linkCancel?: string;\n linkRemove?: string;\n linkRemoveConfirm?: string;\n linkYes?: string;\n linkNo?: string;\n linkEmpty?: string;\n linkLoading?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected + editing\" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Text for the explicit \"log out\" action on the idle pill — clicking it clears the token (back to the logged-out state). Default \"Log ud\". */\n logoutLabel?: string;\n /** Show an on-site \"connect\" pill to LOGGED-OUT visitors (click → connect flow).\n * Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only\n * enable for a site with no real customers (e.g. an internal/first-party site)\n * where a self-contained on-site login entry is wanted. The safe entry for a\n * customer site is always the \"Redigér live\" button in webhouse.app CMS-admin. */\n connectPrompt?: boolean;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n orderedList: \"Nummereret liste\",\n unorderedList: \"Punktliste\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n link: \"Link\",\n linkTabPage: \"Side på sitet\",\n linkTabUrl: \"Fri adresse\",\n linkSearch: \"Søg efter en side…\",\n linkText: \"Linktekst\",\n linkTextAuto: \"Følger sidens titel\",\n linkTextAutoHint:\n \"<b>Tom = følger sidens titel.</b> Omdøbes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den stående.\",\n linkTextOwnHint:\n \"Linket viser <b>din egen tekst</b>. Den bliver stående, også hvis siden omdøbes.\",\n linkLiveHint:\n \"Linket peger på <b>siden</b> — ikke på en adresse. Flyttes siden, eller får den et nyt navn, retter linket sig selv.\",\n linkUrlHint:\n \"En fri adresse peger præcis dér, du skriver. Den følger <b>ikke</b> med, hvis målet flytter sig.\",\n linkUrl: \"Adresse\",\n linkInsert: \"Indsæt link\",\n linkSave: \"Gem ændring\",\n linkCancel: \"Annullér\",\n linkRemove: \"Fjern link\",\n linkRemoveConfirm: \"Fjern?\",\n linkYes: \"Ja\",\n linkNo: \"Nej\",\n linkEmpty: \"Ingen sider fundet\",\n linkLoading: \"Henter sider…\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n logoutLabel: \"Log ud\",\n connectPrompt: false,\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n // Capture BEFORE checkEnabled so we know whether the token JUST arrived from\n // the connect redirect (→ drop straight into edit mode) vs. a normal load\n // with a stored token (→ idle \"Rediger\" pill, don't force edit mode).\n const freshFromUrl = captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (!token) {\n // Logged-out visitor / customer. DEFAULT: show NOTHING (customer-safe) —\n // the consumer can now call initInlineEdit() unconditionally without leaking\n // an edit affordance. Only sites that opt in (connectPrompt: true) get the\n // on-site connect pill + its popup token receiver.\n if (resolved.connectPrompt) {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n return;\n }\n\n // Connected editor. Wire fields once, then pick the entry state: arrived from\n // connect → edit now; normal load with a stored token → idle \"Rediger\" pill\n // (one click enters edit mode; the token persists so \"Rediger\" stays visible\n // on every page while logged in — no trip back to admin).\n setupFields(token, resolved);\n if (freshFromUrl) enterEditMode();\n else showIdlePill();\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n // Fresh connect via popup → wire fields + drop straight into edit mode.\n setupFields(data.token, options);\n enterEditMode();\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token) || !isForThisSite(token, resolved.siteId)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/**\n * Is this token scoped to the site this page belongs to?\n *\n * NOT an authorization check — the server is, and it holds: proxy.ts refuses a\n * token whose `site` claim differs from the request's site, measured 2026-08-17\n * at 403 on both GET and PATCH. This is a UX guard. Without it the editor\n * entered edit mode with a foreign-site token, made the page look editable, and\n * then failed EVERY save — and the natural reaction is to hunt for the bug in\n * your own site rather than suspect the token (reported by the sanneandersen\n * session, who predicted exactly that dead end). Better to not offer editing at\n * all than to offer it and lose the work.\n *\n * A token that cannot be read at all is let through: the server decides, and an\n * unreadable token simply fails there instead of being silently discarded here.\n */\nfunction isForThisSite(token: string, siteId: string): boolean {\n const claims = decodeJwtPayload(token);\n const site = claims?.site;\n if (typeof site !== \"string\" || !site) return true; // not ours to judge\n return site === siteId;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from\n * the URL. Returns true when a FRESH token was just captured from the URL\n * (i.e. this is the connect-redirect landing) so the caller can auto-enter\n * edit mode; false on a normal load where the token comes from localStorage. */\nfunction captureTokenFromUrl(options: ResolvedOptions): boolean {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return false;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n return true;\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\n// Toolbar list-button faces (lucide \"list\" / \"list-ordered\"), inline so no\n// icon-font/runtime dep; stroke inherits the button's white `currentColor`.\nconst UL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/>' +\n '<line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>' +\n \"</svg>\";\n\nconst OL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/>' +\n '<path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/>' +\n \"</svg>\";\n\nconst LINK_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7\"/>' +\n '<path d=\"M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\n// ─── Connected-editor state machine ────────────────────────────────────────\n// A connected editor (valid token) is ALWAYS in one of two visible states:\n// idle → a \"Rediger\" pill (options.connectLabel); one click enters editing.\n// editing → fields are contenteditable + an \"Afslut redigering\" pill.\n// Leaving edit mode returns to idle and KEEPS the token — the editor stays\n// logged in and can re-enter with one click, on every page, for the token's\n// whole life. Only an explicit \"Log ud\" clears the token (→ logged-out state:\n// no pill at all, same as a customer). A field is wired at most ONCE (idempotent\n// via wiredFields); their click handlers no-op while editingActive is false, so\n// idle is truly inert. On an SPA (soft-navigation, no full reload) new pages add\n// fresh [data-cms-field] nodes to the DOM AFTER init — the consumer calls\n// rescanFields() (e.g. on route change) to wire those late arrivals.\nlet editingActive = false;\nlet fieldsWired = false;\nlet stateOptions: ResolvedOptions | null = null;\n// Elements whose listeners are already attached — makes wireField idempotent so\n// setupFields (once) and rescanFields (N times) can both run without double-wiring.\nconst wiredFields = new WeakSet<HTMLElement>();\n\nfunction setupFields(token: string, options: ResolvedOptions): void {\n stateOptions = options;\n if (fieldsWired) return;\n injectStyles();\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, options));\n fieldsWired = true;\n}\n\n/**\n * Wire any [data-cms-field] elements added to the DOM since the last scan.\n * Needed on single-page apps (Next.js et al.): the layout mounts once → init +\n * setupFields run once → only the first page's fields get listeners. Soft\n * navigation swaps in new pages' fields WITHOUT re-running init, so their\n * click-to-edit never activates. A consumer calls this on every route change.\n *\n * Safe + idempotent by design:\n * - No-ops unless a connected, non-expired token exists (same guard as init) —\n * a consumer may call it unconditionally without leaking an edit affordance.\n * - wireField skips already-wired elements (wiredFields WeakSet), so re-scanning\n * the whole document costs nothing for existing fields.\n * - Does NOT touch edit-mode state: editingActive (module-level) and\n * body[data-cms-editing] persist across a soft-nav, so freshly-wired fields\n * are immediately live when the editor is mid-session — no re-enter needed.\n * MPA consumers (broberg: full reload per navigation re-runs init) never need it.\n */\nexport function rescanFields(): void {\n if (typeof window === \"undefined\" || !stateOptions) return;\n const token = localStorage.getItem(stateOptions.storageKey);\n if (!token || isExpired(token)) return;\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, stateOptions!));\n}\n\nfunction enterEditMode(): void {\n if (!stateOptions || editingActive) return;\n editingActive = true;\n // Marks the page as actively editing → unlocks the field hover-outline +\n // text-cursor affordance (see injectStyles). In idle the fields look/behave\n // like normal text; the edit affordance appears only after \"Rediger\".\n document.body.setAttribute(\"data-cms-editing\", \"true\");\n removeIdlePill();\n showActiveBadge(stateOptions);\n}\n\nfunction exitEditMode(): void {\n if (!stateOptions || !editingActive) return;\n deactivateRich(); // commit any active rich region first\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n document.querySelector(\"[data-cms-inline-edit-badge]\")?.remove();\n showIdlePill();\n}\n\nfunction removeIdlePill(): void {\n document.querySelector(\"[data-cms-inline-edit-idle]\")?.remove();\n}\n\nfunction showIdlePill(): void {\n if (!stateOptions) return;\n // Invariant: the idle pill is showing ⟹ we are NOT editing. Force the flag\n // false here so \"Rediger\" (enterEditMode) can never become a no-op if the\n // state ever diverged from the visible pill (defensive — enterEditMode bails\n // when editingActive is already true).\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n const options = stateOptions;\n removeIdlePill();\n const wrap = document.createElement(\"div\");\n wrap.setAttribute(\"data-cms-inline-edit-idle\", \"\");\n wrap.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;\" +\n \"z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;\";\n // Main action: \"Rediger\" → enter edit mode (no server round-trip; the token\n // already exists locally).\n const edit = document.createElement(\"button\");\n edit.type = \"button\";\n edit.setAttribute(\"data-cms-inline-edit-enter\", \"\");\n edit.style.cssText =\n \"display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;\";\n const editText = document.createElement(\"span\");\n editText.textContent = options.connectLabel;\n edit.append(makeIcon(), editText);\n edit.addEventListener(\"click\", enterEditMode);\n // Secondary: \"Log ud\" → clear the token (back to the logged-out state).\n const out = document.createElement(\"button\");\n out.type = \"button\";\n out.setAttribute(\"data-cms-inline-edit-logout\", \"\");\n out.textContent = options.logoutLabel;\n out.style.cssText =\n \"background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;\" +\n \"border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;\";\n out.addEventListener(\"click\", () => {\n disconnect(options);\n removeIdlePill();\n });\n wrap.append(edit, out);\n document.body.appendChild(wrap);\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n if (wiredFields.has(el)) return; // idempotent — safe to re-scan (rescanFields)\n wiredFields.add(el);\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n // Token-safe fields render {år}-style auto-values as atomic chips. Read the\n // TEMPLATE form (chips → their tokens) so the saved value keeps its tokens,\n // and lock the chips so each edits as one unbreakable unit.\n const tokenSafe = hasTokenChips(el);\n el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n el.setAttribute(\"contenteditable\", \"true\");\n if (tokenSafe) lockTokenChips(el);\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = hasTokenChips(el) ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n hideLinkDialog();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Bullet + numbered lists — native execCommand, and the Markdown serializer\n // already round-trips <ul>/<ol> to \"- \" / \"1. \" so no save-path change needed.\n t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand(\"insertUnorderedList\")));\n t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand(\"insertOrderedList\")));\n\n t.appendChild(sep());\n\n // F164 — link: a free URL, or a live reference to a page on the site.\n const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));\n linkBtn.setAttribute(\"data-testid\", \"inline-toolbar-link\");\n t.appendChild(linkBtn);\n\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\n/* ------------------------------------------------------------------ F164 --\n * Link dialog: a free URL, or a LIVE reference to a page on the site.\n *\n * A page link stores data-cms-ref=\"collection:slug\" next to a real working\n * href, so resolveCmsLinks() (see ./server) can re-point it when the page moves\n * or is renamed. Leaving the label empty marks it data-cms-ref-label=\"auto\" so\n * the link shows the page's current title.\n * -------------------------------------------------------------------------- */\n\ninterface LinkablePage {\n collection: string;\n slug: string;\n title: string;\n path: string;\n label: string;\n}\n\nlet linkDialog: HTMLElement | null = null;\nlet linkPages: LinkablePage[] | null = null;\nlet linkPicked: LinkablePage | null = null;\nlet linkEditing: HTMLAnchorElement | null = null;\n\nfunction hideLinkDialog(): void {\n if (linkDialog) linkDialog.style.display = \"none\";\n linkPicked = null;\n linkEditing = null;\n}\n\n/** The <a> the caret sits inside, if any — so clicking Link EDITS it. */\nfunction anchorAtCaret(): HTMLAnchorElement | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n let node: Node | null = sel.getRangeAt(0).startContainer;\n while (node && node !== document.body) {\n if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === \"A\") {\n return node as HTMLAnchorElement;\n }\n node = node.parentNode;\n }\n return null;\n}\n\nasync function fetchLinkablePages(): Promise<LinkablePage[]> {\n if (linkPages) return linkPages;\n if (!richCtx) return [];\n const { options, token } = richCtx;\n const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;\n const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });\n if (!res.ok) throw new Error(`pages ${res.status}`);\n const body = (await res.json()) as { pages?: LinkablePage[] };\n linkPages = body.pages ?? [];\n return linkPages;\n}\n\nfunction toggleLinkDialog(anchor: HTMLElement): void {\n if (linkDialog && linkDialog.style.display === \"block\") {\n hideLinkDialog();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n linkEditing = anchorAtCaret();\n if (!linkDialog) linkDialog = buildLinkDialog();\n renderLinkDialog();\n const rect = anchor.getBoundingClientRect();\n linkDialog.style.top = `${rect.bottom + 8}px`;\n linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;\n linkDialog.style.display = \"block\";\n}\n\nfunction buildLinkDialog(): HTMLElement {\n const d = document.createElement(\"div\");\n d.setAttribute(\"data-cms-inline-edit-toolbar\", \"\"); // shares the \"don't commit on click\" guard\n d.setAttribute(\"data-testid\", \"inline-link-dialog\");\n d.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;color:#fff;overflow:hidden;\";\n // Keep the page selection alive while interacting with the dialog.\n d.addEventListener(\"mousedown\", (e) => {\n const t = e.target as HTMLElement;\n if (t.tagName !== \"INPUT\") e.preventDefault();\n e.stopPropagation();\n });\n document.body.appendChild(d);\n return d;\n}\n\nfunction renderLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const L = uiLabels;\n const editing = !!linkEditing;\n const existingRef = linkEditing?.getAttribute(\"data-cms-ref\") ?? \"\";\n const onPageTab = !editing || !!existingRef;\n\n d.innerHTML =\n '<div style=\"display:flex;gap:4px;padding:8px 8px 0\">' +\n `<button type=\"button\" data-testid=\"inline-link-tab-page\" data-tab=\"page\" style=\"${tabCss(onPageTab)}\">${L.linkTabPage}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-tab-url\" data-tab=\"url\" style=\"${tabCss(!onPageTab)}\">${L.linkTabUrl}</button>` +\n \"</div>\" +\n '<div style=\"padding:10px 12px 12px\">' +\n `<div data-pane=\"page\" style=\"display:${onPageTab ? \"block\" : \"none\"}\">` +\n `<input data-testid=\"inline-link-search\" data-role=\"search\" placeholder=\"${L.linkSearch}\" style=\"${fieldCss()}\">` +\n `<div data-role=\"list\" data-testid=\"inline-link-list\" style=\"margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821\"></div>` +\n \"</div>\" +\n `<div data-pane=\"url\" style=\"display:${onPageTab ? \"none\" : \"block\"}\">` +\n `<label style=\"${labelCss()}\">${L.linkUrl}</label>` +\n `<input data-testid=\"inline-link-url\" data-role=\"url\" placeholder=\"https://…\" style=\"${fieldCss()}\">` +\n \"</div>\" +\n `<label style=\"${labelCss()}\">${L.linkText}</label>` +\n `<input data-testid=\"inline-link-text\" data-role=\"text\" placeholder=\"${L.linkTextAuto}\" style=\"${fieldCss()}\">` +\n `<p data-role=\"hint\" style=\"font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5\"></p>` +\n `<div data-role=\"live\" style=\"display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px\">` +\n `<p style=\"margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55\">${L.linkLiveHint}</p></div>` +\n '<div style=\"display:flex;gap:8px;align-items:center;margin-top:14px\">' +\n '<div data-role=\"remove\"></div><div style=\"flex:1\"></div>' +\n `<button type=\"button\" data-testid=\"inline-link-cancel\" data-role=\"cancel\" style=\"${btnCss(false)}\">${L.linkCancel}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-submit\" data-role=\"submit\" style=\"${btnCss(true)}\" disabled>${editing ? L.linkSave : L.linkInsert}</button>` +\n \"</div></div>\";\n\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const text = q<HTMLInputElement>(\"text\");\n const urlIn = q<HTMLInputElement>(\"url\");\n const search = q<HTMLInputElement>(\"search\");\n\n if (editing) {\n text.value = linkEditing?.getAttribute(\"data-cms-ref-label\") === \"auto\" ? \"\" : (linkEditing?.textContent ?? \"\");\n if (!existingRef) urlIn.value = linkEditing?.getAttribute(\"href\") ?? \"\";\n q<HTMLElement>(\"remove\").appendChild(buildRemoveLink());\n }\n\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((btn) => {\n btn.onclick = () => {\n const page = btn.dataset.tab === \"page\";\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((b) => {\n b.setAttribute(\"style\", tabCss(b.dataset.tab === \"page\" ? page : !page));\n });\n (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display = page ? \"block\" : \"none\";\n (d.querySelector('[data-pane=\"url\"]') as HTMLElement).style.display = page ? \"none\" : \"block\";\n q<HTMLElement>(\"live\").style.display = page ? \"flex\" : \"none\";\n syncLinkDialog();\n };\n });\n\n search.oninput = () => renderLinkList(search.value);\n text.oninput = syncLinkDialog;\n urlIn.oninput = syncLinkDialog;\n q<HTMLElement>(\"cancel\").onclick = hideLinkDialog;\n q<HTMLElement>(\"submit\").onclick = applyLink;\n\n renderLinkList(\"\");\n syncLinkDialog();\n}\n\nfunction buildRemoveLink(): HTMLElement {\n const wrap = document.createElement(\"div\");\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.setAttribute(\"data-testid\", \"inline-link-remove\");\n btn.textContent = uiLabels.linkRemove;\n btn.style.cssText =\n \"background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;\";\n btn.onclick = () => {\n wrap.innerHTML =\n `<span style=\"font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px\">${uiLabels.linkRemoveConfirm}</span>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-yes\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkYes}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-no\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkNo}</button>`;\n (wrap.querySelector('[data-testid=\"inline-link-remove-no\"]') as HTMLElement).onclick = () => {\n wrap.innerHTML = \"\";\n wrap.appendChild(btn);\n };\n (wrap.querySelector('[data-testid=\"inline-link-remove-yes\"]') as HTMLElement).onclick = () => {\n if (linkEditing) {\n const parent = linkEditing.parentNode;\n while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);\n parent?.removeChild(linkEditing);\n }\n hideLinkDialog();\n };\n };\n wrap.appendChild(btn);\n return wrap;\n}\n\nfunction renderLinkList(filter: string): void {\n const d = linkDialog;\n if (!d) return;\n const list = d.querySelector('[data-role=\"list\"]') as HTMLElement | null;\n if (!list) return;\n\n const paint = (pages: LinkablePage[]) => {\n const f = filter.trim().toLowerCase();\n const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));\n if (!shown.length) {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkEmpty}</div>`;\n return;\n }\n list.innerHTML = \"\";\n shown.forEach((p) => {\n const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;\n const row = document.createElement(\"div\");\n row.setAttribute(\"data-testid\", `inline-link-page-${p.collection}-${p.slug}`);\n row.style.cssText =\n \"display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;\" +\n \"border-bottom:1px solid #232833;\" +\n (on ? \"background:rgba(0,178,255,.14);\" : \"\");\n row.innerHTML =\n `<div style=\"min-width:0;flex:1\"><div style=\"font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.title)}</div>` +\n `<div style=\"font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.path)}</div></div>` +\n `<span style=\"flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap\">${escapeHtml(p.label)}</span>`;\n row.onclick = () => {\n linkPicked = p;\n renderLinkList(filter);\n syncLinkDialog();\n };\n list.appendChild(row);\n });\n };\n\n if (linkPages) {\n paint(linkPages);\n return;\n }\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkLoading}</div>`;\n fetchLinkablePages()\n .then((pages) => {\n // Pre-select the page an existing reference already points at.\n const ref = linkEditing?.getAttribute(\"data-cms-ref\");\n if (ref && !linkPicked) {\n const [c, ...rest] = ref.split(\":\");\n linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(\":\")) ?? null;\n }\n paint(pages);\n syncLinkDialog();\n })\n .catch(() => {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#ff8a8a\">${uiLabels.error}</div>`;\n });\n}\n\nfunction syncLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n q<HTMLElement>(\"hint\").innerHTML = own\n ? (uiLabels.linkTextOwnHint as string)\n : onPage\n ? (uiLabels.linkTextAutoHint as string)\n : (uiLabels.linkUrlHint as string);\n q<HTMLElement>(\"live\").style.display = onPage ? \"flex\" : \"none\";\n (q<HTMLButtonElement>(\"submit\")).disabled = onPage\n ? !linkPicked\n : !q<HTMLInputElement>(\"url\").value.trim();\n}\n\nfunction applyLink(): void {\n const d = linkDialog;\n if (!d || !richCtx) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n const href = onPage ? (linkPicked?.path ?? \"\") : q<HTMLInputElement>(\"url\").value.trim();\n if (!href) return;\n\n const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : \"\";\n const label = own || (onPage ? (linkPicked?.title ?? href) : href);\n\n if (linkEditing) {\n linkEditing.setAttribute(\"href\", href);\n if (ref) linkEditing.setAttribute(\"data-cms-ref\", ref);\n else linkEditing.removeAttribute(\"data-cms-ref\");\n if (ref && !own) linkEditing.setAttribute(\"data-cms-ref-label\", \"auto\");\n else linkEditing.removeAttribute(\"data-cms-ref-label\");\n linkEditing.textContent = label;\n hideLinkDialog();\n return;\n }\n\n const a = document.createElement(\"a\");\n a.setAttribute(\"href\", href);\n if (ref) {\n a.setAttribute(\"data-cms-ref\", ref);\n if (!own) a.setAttribute(\"data-cms-ref-label\", \"auto\");\n }\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n const range = sel.getRangeAt(0);\n a.textContent = own || range.toString() || label;\n range.deleteContents();\n range.insertNode(a);\n sel.removeAllRanges();\n } else {\n a.textContent = label;\n richCtx.el.appendChild(a);\n }\n savedRange = null;\n hideLinkDialog();\n}\n\nfunction escapeHtml(v: string): string {\n return v.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nconst tabCss = (on: boolean) =>\n \"flex:1;background:\" +\n (on ? \"#2a2f38\" : \"none\") +\n \";border:1px solid \" +\n (on ? \"#3a3f4a\" : \"transparent\") +\n \";color:\" +\n (on ? \"#fff\" : \"#9aa3b2\") +\n \";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;\";\n\nconst fieldCss = () =>\n \"width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;\" +\n \"border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;\";\n\nconst labelCss = () =>\n \"font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;\";\n\nconst btnCss = (primary: boolean) =>\n \"height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;\" +\n (primary\n ? \"background:#00b2ff;color:#04121b;border:none;\"\n : \"background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;\");\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nexport function htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n reattachOrphanListItems(container);\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction isList(el: Element | null): el is HTMLElement {\n if (!el) return false;\n const tag = el.tagName.toLowerCase();\n return tag === \"ul\" || tag === \"ol\";\n}\n\n/**\n * A contenteditable region can end up with an <li> as a DIRECT child of the\n * field container — browsers routinely split a list and orphan an item when the\n * caret sits in it, and `execCommand(\"insertUnorderedList\")` does the same.\n * Serializing an orphan through the inline default silently drops its marker,\n * so a list item degrades into a paragraph and the stored Markdown is corrupted\n * (sanneandersen, 2026-08-16: a 13-item CV list saved back as 1 paragraph + 12\n * bullets on a public page, on every single save).\n *\n * Put each orphan back in the adjacent list — before it if the list follows,\n * after it if the list precedes — so author order survives. An orphan with no\n * list next to it gets its own.\n */\nfunction reattachOrphanListItems(container: HTMLElement): void {\n const orphans = Array.from(container.children).filter(\n (c) => c.tagName.toLowerCase() === \"li\",\n );\n for (const li of orphans) {\n const prev = li.previousElementSibling;\n const next = li.nextElementSibling;\n if (isList(prev)) {\n prev.appendChild(li);\n } else if (isList(next)) {\n next.insertBefore(li, next.firstChild);\n } else {\n const list = container.ownerDocument.createElement(\"ul\");\n li.replaceWith(list);\n list.appendChild(li);\n }\n }\n}\n\n/** Tags serializeBlock treats as their own block; everything else is inline. */\nconst BLOCK_TAGS = new Set([\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\",\n \"p\", \"div\", \"ul\", \"ol\", \"li\", \"blockquote\", \"pre\", \"hr\", \"table\",\n]);\n\nfunction isBlockNode(node: Node): boolean {\n return (\n node.nodeType === Node.ELEMENT_NODE &&\n BLOCK_TAGS.has((node as HTMLElement).tagName.toLowerCase())\n );\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n // Text and inline elements sitting directly at block level form ONE implicit\n // paragraph. Serialising them one-by-one and joining with \"\\n\\n\" tore a\n // single sentence into separate blocks (\"Noget <b>fedt</b> mere\" became three\n // paragraphs) and dropped each element's own formatting. Gather the run, then\n // serialise it as the one paragraph the author actually wrote.\n let inlineRun = \"\";\n const flushInline = () => {\n const s = inlineRun.trim();\n if (s) blocks.push(s);\n inlineRun = \"\";\n };\n\n parent.childNodes.forEach((node) => {\n if (isBlockNode(node)) {\n flushInline();\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n } else {\n inlineRun += serializeInlineNode(node);\n }\n });\n flushInline();\n\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n // Backstop for an orphaned <li> that reattachOrphanListItems could not\n // place (e.g. nested inside another block). Without this it falls through\n // to the inline default and loses its marker — silent content loss.\n case \"li\":\n return \"- \" + serializeInline(el).trim();\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\n/** Escape a value for use inside a double-quoted HTML attribute (F164). */\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n out += serializeInlineNode(child);\n });\n return out;\n}\n\n/**\n * Serialize ONE inline node, applying ITS OWN tag's formatting.\n *\n * Split out of serializeInline because that function only ever looked at a\n * node's CHILDREN: handed an element it returned the element's text with the\n * element's own markup dropped. serializeBlock's default arm did exactly that,\n * so a <strong> (or <em>, <code>, <a>, <img>) sitting at the top level of an\n * edited field lost its formatting — or, for an image, vanished entirely — on\n * save. Same failure family as the orphaned <li>, one tag deeper.\n */\nfunction serializeInlineNode(child: Node): string {\n if (child.nodeType === Node.TEXT_NODE) {\n return (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return \"\";\n {\n let out = \"\";\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n const ref = el.getAttribute(\"data-cms-ref\");\n if (ref) {\n // F164 — a link to a PAGE on the site carries a reference so the page\n // can re-resolve href (and optionally the label) when that page moves\n // or is renamed. Markdown link syntax has nowhere to put that, so\n // emit inline HTML, which `marked` passes through untouched.\n // Collapsing to [text](href) here would strip the reference on the\n // editor's very FIRST save and silently kill the whole feature.\n const attrs = [`href=\"${escapeAttr(href)}\"`, `data-cms-ref=\"${escapeAttr(ref)}\"`];\n const label = el.getAttribute(\"data-cms-ref-label\");\n if (label) attrs.push(`data-cms-ref-label=\"${escapeAttr(label)}\"`);\n out += `<a ${attrs.join(\" \")}>${inner}</a>`;\n } else {\n out += href ? `[${inner}](${href})` : inner;\n }\n break;\n }\n case \"img\": {\n // Content images embedded in the body (![alt](src)) — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += `![${alt}](${src})`;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n return out;\n }\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n const slice = el.dataset.cmsSlice;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (slice !== undefined) {\n // Field-slice save: replace just this segment inside the full field value,\n // preserving every other segment + embed. Aborts on a non-unique match.\n const currentVal = typeof mergedData[field] === \"string\" ? (mergedData[field] as string) : \"\";\n mergedData[field] = applyFieldSlice(currentVal, slice, value);\n } else if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n // The slice we just wrote is now the current text — track it so a second\n // edit of the same segment (no reload) matches against the new value.\n if (slice !== undefined) el.dataset.cmsSlice = value;\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n // Clicking the badge LEAVES edit mode but KEEPS the login → back to the idle\n // \"Rediger\" pill (not a logout). Explicit logout lives on the idle pill.\n badge.addEventListener(\"click\", exitEditMode);\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n // The hover-outline + text-cursor affordance is gated on body[data-cms-editing]\n // so it appears ONLY while actively editing (after \"Rediger\"). In idle a\n // connected editor's fields look + behave like normal page text — no dashed\n // outline, no text cursor — so the page never looks editable before you opt in.\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; transition: outline-color .15s; }\n body[data-cms-editing=\"true\"] [data-cms-field] { cursor: text; }\n body[data-cms-editing=\"true\"] [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/field-slice.ts","../src/token-safe.ts","../src/index.ts"],"sourcesContent":["/**\n * Replace ONE slice of a larger text field.\n *\n * Used when an element renders a single SEGMENT of a bigger field — e.g. one\n * prose paragraph of a `content` body that is interleaved with `[block:]`\n * embeds — so editing that segment must write back into the full field without\n * clobbering the other segments or the embeds.\n *\n * Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous\n * or stale slice ABORTS the save (surfaced as the error pill) instead of\n * corrupting the whole field. Pure + framework-agnostic; unit-tested.\n */\nexport function applyFieldSlice(current: string, original: string, next: string): string {\n const first = current.indexOf(original);\n if (first === -1) throw new Error(\"field-slice: original not found\");\n if (current.indexOf(original, first + original.length) !== -1) {\n throw new Error(\"field-slice: original found more than once (ambiguous)\");\n }\n return current.slice(0, first) + next + current.slice(first + original.length);\n}\n","/**\n * F162.3 — token-safe serialization.\n *\n * Some CMS fields store auto-resolve TOKENS that expand at render time, e.g.\n * `\"{år} års erfaring\"` → `\"26 års erfaring\"` ({år} = years-since-2000). Plain\n * inline-edit saves the rendered `textContent`, which overwrites the token with\n * its expanded value (\"26\") and freezes the number forever.\n *\n * Fix without fragile re-alignment: the site renders each token as an ATOMIC\n * inline chip inside the editable field —\n * `{år}` → `<span data-cms-token=\"{år}\">26</span>`\n * — where the chip's TEXT is the rendered value and its `data-cms-token`\n * attribute holds the original token. While editing, the chip is made\n * contenteditable=false (an unbreakable unit: the user edits the words AROUND\n * it, never inside it). On save we serialise by walking child nodes: text nodes\n * contribute their text verbatim, a chip contributes its TOKEN. The stored\n * template's tokens therefore survive the edit — no diffing, no guessing.\n */\n\nconst TEXT_NODE = 3;\nconst ELEMENT_NODE = 1;\n\n/** True when the element contains at least one token chip → its save path must\n * use serializeTokenSafe instead of textContent. */\nexport function hasTokenChips(el: Element): boolean {\n return el.querySelector('[data-cms-token]') !== null;\n}\n\n/**\n * Re-tokenise an edited token-safe element back to its stored template form:\n * text nodes verbatim; each `[data-cms-token]` chip → its token value (NOT the\n * rendered value). Recurses so a chip wrapped in other markup still resolves.\n * A deleted chip simply drops its token — an intentional edit, not a bug.\n */\nexport function serializeTokenSafe(el: Node): string {\n let out = '';\n el.childNodes.forEach((node) => {\n if (node.nodeType === TEXT_NODE) {\n out += node.textContent ?? '';\n return;\n }\n if (node.nodeType !== ELEMENT_NODE) return;\n const element = node as Element;\n const token = element.getAttribute('data-cms-token');\n if (token) {\n out += token; // atomic chip → its token, NEVER the rendered value\n } else {\n out += serializeTokenSafe(element); // wrapper markup → recurse into it\n }\n });\n return out;\n}\n\n/** Lock every token chip so it edits as one unit (called on edit-enter). */\nexport function lockTokenChips(el: Element): void {\n el.querySelectorAll('[data-cms-token]').forEach((chip) => {\n chip.setAttribute('contenteditable', 'false');\n });\n}\n","/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\nimport { applyFieldSlice } from \"./field-slice\";\nexport { applyFieldSlice } from \"./field-slice\";\nimport { serializeTokenSafe, hasTokenChips, lockTokenChips } from \"./token-safe\";\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n orderedList?: string;\n unorderedList?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n /** F164 — link dialog. */\n link?: string;\n linkTabPage?: string;\n linkTabUrl?: string;\n linkSearch?: string;\n linkText?: string;\n linkTextAuto?: string;\n linkTextAutoHint?: string;\n linkTextOwnHint?: string;\n linkLiveHint?: string;\n linkUrlHint?: string;\n linkUrl?: string;\n linkInsert?: string;\n linkSave?: string;\n linkCancel?: string;\n linkRemove?: string;\n linkRemoveConfirm?: string;\n linkYes?: string;\n linkNo?: string;\n linkEmpty?: string;\n linkLoading?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected + editing\" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Text for the explicit \"log out\" action on the idle pill — clicking it clears the token (back to the logged-out state). Default \"Log ud\". */\n logoutLabel?: string;\n /** Show an on-site \"connect\" pill to LOGGED-OUT visitors (click → connect flow).\n * Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only\n * enable for a site with no real customers (e.g. an internal/first-party site)\n * where a self-contained on-site login entry is wanted. The safe entry for a\n * customer site is always the \"Redigér live\" button in webhouse.app CMS-admin. */\n connectPrompt?: boolean;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n orderedList: \"Nummereret liste\",\n unorderedList: \"Punktliste\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n link: \"Link\",\n linkTabPage: \"Side på sitet\",\n linkTabUrl: \"Fri adresse\",\n linkSearch: \"Søg efter en side…\",\n linkText: \"Linktekst\",\n linkTextAuto: \"Følger sidens titel\",\n linkTextAutoHint:\n \"<b>Tom = følger sidens titel.</b> Omdøbes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den stående.\",\n linkTextOwnHint:\n \"Linket viser <b>din egen tekst</b>. Den bliver stående, også hvis siden omdøbes.\",\n linkLiveHint:\n \"Linket peger på <b>siden</b> — ikke på en adresse. Flyttes siden, eller får den et nyt navn, retter linket sig selv.\",\n linkUrlHint:\n \"En fri adresse peger præcis dér, du skriver. Den følger <b>ikke</b> med, hvis målet flytter sig.\",\n linkUrl: \"Adresse\",\n linkInsert: \"Indsæt link\",\n linkSave: \"Gem ændring\",\n linkCancel: \"Annullér\",\n linkRemove: \"Fjern link\",\n linkRemoveConfirm: \"Fjern?\",\n linkYes: \"Ja\",\n linkNo: \"Nej\",\n linkEmpty: \"Ingen sider fundet\",\n linkLoading: \"Henter sider…\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n logoutLabel: \"Log ud\",\n connectPrompt: false,\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n // Capture BEFORE checkEnabled so we know whether the token JUST arrived from\n // the connect redirect (→ drop straight into edit mode) vs. a normal load\n // with a stored token (→ idle \"Rediger\" pill, don't force edit mode).\n const freshFromUrl = captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (!token) {\n // Logged-out visitor / customer. DEFAULT: show NOTHING (customer-safe) —\n // the consumer can now call initInlineEdit() unconditionally without leaking\n // an edit affordance. Only sites that opt in (connectPrompt: true) get the\n // on-site connect pill + its popup token receiver.\n if (resolved.connectPrompt) {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n return;\n }\n\n // Connected editor. Wire fields once, then pick the entry state: arrived from\n // connect → edit now; normal load with a stored token → idle \"Rediger\" pill\n // (one click enters edit mode; the token persists so \"Rediger\" stays visible\n // on every page while logged in — no trip back to admin).\n setupFields(token, resolved);\n if (freshFromUrl) enterEditMode();\n else showIdlePill();\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n // Fresh connect via popup → wire fields + drop straight into edit mode.\n setupFields(data.token, options);\n enterEditMode();\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token) || !isForThisSite(token, resolved.siteId)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/**\n * Is this token scoped to the site this page belongs to?\n *\n * NOT an authorization check — the server is, and it holds: proxy.ts refuses a\n * token whose `site` claim differs from the request's site, measured 2026-08-17\n * at 403 on both GET and PATCH. This is a UX guard. Without it the editor\n * entered edit mode with a foreign-site token, made the page look editable, and\n * then failed EVERY save — and the natural reaction is to hunt for the bug in\n * your own site rather than suspect the token (reported by the sanneandersen\n * session, who predicted exactly that dead end). Better to not offer editing at\n * all than to offer it and lose the work.\n *\n * A token that cannot be read at all is let through: the server decides, and an\n * unreadable token simply fails there instead of being silently discarded here.\n */\nfunction isForThisSite(token: string, siteId: string): boolean {\n const claims = decodeJwtPayload(token);\n const site = claims?.site;\n if (typeof site !== \"string\" || !site) return true; // not ours to judge\n return site === siteId;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from\n * the URL. Returns true when a FRESH token was just captured from the URL\n * (i.e. this is the connect-redirect landing) so the caller can auto-enter\n * edit mode; false on a normal load where the token comes from localStorage. */\nfunction captureTokenFromUrl(options: ResolvedOptions): boolean {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return false;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n return true;\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\n// Toolbar list-button faces (lucide \"list\" / \"list-ordered\"), inline so no\n// icon-font/runtime dep; stroke inherits the button's white `currentColor`.\nconst UL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/>' +\n '<line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>' +\n \"</svg>\";\n\nconst OL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/>' +\n '<path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/>' +\n \"</svg>\";\n\nconst LINK_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7\"/>' +\n '<path d=\"M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\n// ─── Connected-editor state machine ────────────────────────────────────────\n// A connected editor (valid token) is ALWAYS in one of two visible states:\n// idle → a \"Rediger\" pill (options.connectLabel); one click enters editing.\n// editing → fields are contenteditable + an \"Afslut redigering\" pill.\n// Leaving edit mode returns to idle and KEEPS the token — the editor stays\n// logged in and can re-enter with one click, on every page, for the token's\n// whole life. Only an explicit \"Log ud\" clears the token (→ logged-out state:\n// no pill at all, same as a customer). A field is wired at most ONCE (idempotent\n// via wiredFields); their click handlers no-op while editingActive is false, so\n// idle is truly inert. On an SPA (soft-navigation, no full reload) new pages add\n// fresh [data-cms-field] nodes to the DOM AFTER init — the consumer calls\n// rescanFields() (e.g. on route change) to wire those late arrivals.\nlet editingActive = false;\nlet fieldsWired = false;\nlet stateOptions: ResolvedOptions | null = null;\n// Elements whose listeners are already attached — makes wireField idempotent so\n// setupFields (once) and rescanFields (N times) can both run without double-wiring.\nconst wiredFields = new WeakSet<HTMLElement>();\n\nfunction setupFields(token: string, options: ResolvedOptions): void {\n stateOptions = options;\n if (fieldsWired) return;\n injectStyles();\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, options));\n observeNewFields();\n fieldsWired = true;\n}\n\nlet fieldObserver: MutationObserver | null = null;\n\n/**\n * Re-wire fields that appear after the first scan, without the consumer having\n * to ask.\n *\n * rescanFields() below has existed for this since day one, and its own comment\n * says \"a consumer calls this on every route change\". webhouse.dk did not, and\n * the result was the worst shape a failure can take: the badge kept saying\n * AFSLUT REDIGERING while every field on every page reached by clicking a link\n * was dead. Nothing errored. Reported by the site owner, reproduced in a\n * browser — after a soft navigation the clicked field's contenteditable stayed\n * null and no toolbar appeared.\n *\n * A contract that depends on every consumer remembering a call is a contract\n * that will be broken silently, so the package now keeps its own promise.\n * rescanFields() stays exported and still works — a consumer that already calls\n * it is unaffected, and wireField is idempotent, so the two cannot conflict.\n *\n * Cheap by construction: the observer only fires on childList changes and the\n * rescan is coalesced into one animation frame, so a page inserting many nodes\n * costs one scan, not one per node.\n */\nfunction observeNewFields(): void {\n if (fieldObserver || typeof MutationObserver === \"undefined\") return;\n let queued = false;\n fieldObserver = new MutationObserver(() => {\n if (queued) return;\n queued = true;\n requestAnimationFrame(() => {\n queued = false;\n rescanFields();\n });\n });\n fieldObserver.observe(document.body, { childList: true, subtree: true });\n}\n\n/**\n * Wire any [data-cms-field] elements added to the DOM since the last scan.\n * Needed on single-page apps (Next.js et al.): the layout mounts once → init +\n * setupFields run once → only the first page's fields get listeners. Soft\n * navigation swaps in new pages' fields WITHOUT re-running init, so their\n * click-to-edit never activates. A consumer calls this on every route change.\n *\n * Safe + idempotent by design:\n * - No-ops unless a connected, non-expired token exists (same guard as init) —\n * a consumer may call it unconditionally without leaking an edit affordance.\n * - wireField skips already-wired elements (wiredFields WeakSet), so re-scanning\n * the whole document costs nothing for existing fields.\n * - Does NOT touch edit-mode state: editingActive (module-level) and\n * body[data-cms-editing] persist across a soft-nav, so freshly-wired fields\n * are immediately live when the editor is mid-session — no re-enter needed.\n * MPA consumers (broberg: full reload per navigation re-runs init) never need it.\n */\nexport function rescanFields(): void {\n if (typeof window === \"undefined\" || !stateOptions) return;\n const token = localStorage.getItem(stateOptions.storageKey);\n if (!token || isExpired(token)) return;\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, stateOptions!));\n}\n\nfunction enterEditMode(): void {\n if (!stateOptions || editingActive) return;\n editingActive = true;\n // Marks the page as actively editing → unlocks the field hover-outline +\n // text-cursor affordance (see injectStyles). In idle the fields look/behave\n // like normal text; the edit affordance appears only after \"Rediger\".\n document.body.setAttribute(\"data-cms-editing\", \"true\");\n removeIdlePill();\n showActiveBadge(stateOptions);\n}\n\nfunction exitEditMode(): void {\n if (!stateOptions || !editingActive) return;\n deactivateRich(); // commit any active rich region first\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n document.querySelector(\"[data-cms-inline-edit-badge]\")?.remove();\n showIdlePill();\n}\n\nfunction removeIdlePill(): void {\n document.querySelector(\"[data-cms-inline-edit-idle]\")?.remove();\n}\n\nfunction showIdlePill(): void {\n if (!stateOptions) return;\n // Invariant: the idle pill is showing ⟹ we are NOT editing. Force the flag\n // false here so \"Rediger\" (enterEditMode) can never become a no-op if the\n // state ever diverged from the visible pill (defensive — enterEditMode bails\n // when editingActive is already true).\n editingActive = false;\n document.body.removeAttribute(\"data-cms-editing\");\n const options = stateOptions;\n removeIdlePill();\n const wrap = document.createElement(\"div\");\n wrap.setAttribute(\"data-cms-inline-edit-idle\", \"\");\n wrap.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;\" +\n \"z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;\";\n // Main action: \"Rediger\" → enter edit mode (no server round-trip; the token\n // already exists locally).\n const edit = document.createElement(\"button\");\n edit.type = \"button\";\n edit.setAttribute(\"data-cms-inline-edit-enter\", \"\");\n edit.style.cssText =\n \"display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;\";\n const editText = document.createElement(\"span\");\n editText.textContent = options.connectLabel;\n edit.append(makeIcon(), editText);\n edit.addEventListener(\"click\", enterEditMode);\n // Secondary: \"Log ud\" → clear the token (back to the logged-out state).\n const out = document.createElement(\"button\");\n out.type = \"button\";\n out.setAttribute(\"data-cms-inline-edit-logout\", \"\");\n out.textContent = options.logoutLabel;\n out.style.cssText =\n \"background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;\" +\n \"border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;\";\n out.addEventListener(\"click\", () => {\n disconnect(options);\n removeIdlePill();\n });\n wrap.append(edit, out);\n document.body.appendChild(wrap);\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n if (wiredFields.has(el)) return; // idempotent — safe to re-scan (rescanFields)\n wiredFields.add(el);\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n // Token-safe fields render {år}-style auto-values as atomic chips. Read the\n // TEMPLATE form (chips → their tokens) so the saved value keeps its tokens,\n // and lock the chips so each edits as one unbreakable unit.\n const tokenSafe = hasTokenChips(el);\n el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n el.setAttribute(\"contenteditable\", \"true\");\n if (tokenSafe) lockTokenChips(el);\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = hasTokenChips(el) ? serializeTokenSafe(el) : (el.textContent ?? \"\");\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n hideLinkDialog();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Bullet + numbered lists — native execCommand, and the Markdown serializer\n // already round-trips <ul>/<ol> to \"- \" / \"1. \" so no save-path change needed.\n t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand(\"insertUnorderedList\")));\n t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand(\"insertOrderedList\")));\n\n t.appendChild(sep());\n\n // F164 — link: a free URL, or a live reference to a page on the site.\n const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));\n linkBtn.setAttribute(\"data-testid\", \"inline-toolbar-link\");\n t.appendChild(linkBtn);\n\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\n/* ------------------------------------------------------------------ F164 --\n * Link dialog: a free URL, or a LIVE reference to a page on the site.\n *\n * A page link stores data-cms-ref=\"collection:slug\" next to a real working\n * href, so resolveCmsLinks() (see ./server) can re-point it when the page moves\n * or is renamed. Leaving the label empty marks it data-cms-ref-label=\"auto\" so\n * the link shows the page's current title.\n * -------------------------------------------------------------------------- */\n\ninterface LinkablePage {\n collection: string;\n slug: string;\n title: string;\n path: string;\n label: string;\n}\n\nlet linkDialog: HTMLElement | null = null;\nlet linkPages: LinkablePage[] | null = null;\nlet linkPicked: LinkablePage | null = null;\nlet linkEditing: HTMLAnchorElement | null = null;\n\nfunction hideLinkDialog(): void {\n if (linkDialog) linkDialog.style.display = \"none\";\n linkPicked = null;\n linkEditing = null;\n}\n\n/** The <a> the caret sits inside, if any — so clicking Link EDITS it. */\nfunction anchorAtCaret(): HTMLAnchorElement | null {\n const sel = window.getSelection();\n if (!sel || sel.rangeCount === 0) return null;\n let node: Node | null = sel.getRangeAt(0).startContainer;\n while (node && node !== document.body) {\n if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === \"A\") {\n return node as HTMLAnchorElement;\n }\n node = node.parentNode;\n }\n return null;\n}\n\nasync function fetchLinkablePages(): Promise<LinkablePage[]> {\n if (linkPages) return linkPages;\n if (!richCtx) return [];\n const { options, token } = richCtx;\n const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;\n const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });\n if (!res.ok) throw new Error(`pages ${res.status}`);\n const body = (await res.json()) as { pages?: LinkablePage[] };\n linkPages = body.pages ?? [];\n return linkPages;\n}\n\nfunction toggleLinkDialog(anchor: HTMLElement): void {\n if (linkDialog && linkDialog.style.display === \"block\") {\n hideLinkDialog();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n linkEditing = anchorAtCaret();\n if (!linkDialog) linkDialog = buildLinkDialog();\n renderLinkDialog();\n const rect = anchor.getBoundingClientRect();\n linkDialog.style.top = `${rect.bottom + 8}px`;\n linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;\n linkDialog.style.display = \"block\";\n}\n\nfunction buildLinkDialog(): HTMLElement {\n const d = document.createElement(\"div\");\n d.setAttribute(\"data-cms-inline-edit-toolbar\", \"\"); // shares the \"don't commit on click\" guard\n d.setAttribute(\"data-testid\", \"inline-link-dialog\");\n d.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;color:#fff;overflow:hidden;\";\n // Keep the page selection alive while interacting with the dialog.\n d.addEventListener(\"mousedown\", (e) => {\n const t = e.target as HTMLElement;\n if (t.tagName !== \"INPUT\") e.preventDefault();\n e.stopPropagation();\n });\n document.body.appendChild(d);\n return d;\n}\n\nfunction renderLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const L = uiLabels;\n const editing = !!linkEditing;\n const existingRef = linkEditing?.getAttribute(\"data-cms-ref\") ?? \"\";\n const onPageTab = !editing || !!existingRef;\n\n d.innerHTML =\n '<div style=\"display:flex;gap:4px;padding:8px 8px 0\">' +\n `<button type=\"button\" data-testid=\"inline-link-tab-page\" data-tab=\"page\" style=\"${tabCss(onPageTab)}\">${L.linkTabPage}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-tab-url\" data-tab=\"url\" style=\"${tabCss(!onPageTab)}\">${L.linkTabUrl}</button>` +\n \"</div>\" +\n '<div style=\"padding:10px 12px 12px\">' +\n `<div data-pane=\"page\" style=\"display:${onPageTab ? \"block\" : \"none\"}\">` +\n `<input data-testid=\"inline-link-search\" data-role=\"search\" placeholder=\"${L.linkSearch}\" style=\"${fieldCss()}\">` +\n `<div data-role=\"list\" data-testid=\"inline-link-list\" style=\"margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821\"></div>` +\n \"</div>\" +\n `<div data-pane=\"url\" style=\"display:${onPageTab ? \"none\" : \"block\"}\">` +\n `<label style=\"${labelCss()}\">${L.linkUrl}</label>` +\n `<input data-testid=\"inline-link-url\" data-role=\"url\" placeholder=\"https://…\" style=\"${fieldCss()}\">` +\n \"</div>\" +\n `<label style=\"${labelCss()}\">${L.linkText}</label>` +\n `<input data-testid=\"inline-link-text\" data-role=\"text\" placeholder=\"${L.linkTextAuto}\" style=\"${fieldCss()}\">` +\n `<p data-role=\"hint\" style=\"font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5\"></p>` +\n `<div data-role=\"live\" style=\"display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px\">` +\n `<p style=\"margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55\">${L.linkLiveHint}</p></div>` +\n '<div style=\"display:flex;gap:8px;align-items:center;margin-top:14px\">' +\n '<div data-role=\"remove\"></div><div style=\"flex:1\"></div>' +\n `<button type=\"button\" data-testid=\"inline-link-cancel\" data-role=\"cancel\" style=\"${btnCss(false)}\">${L.linkCancel}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-submit\" data-role=\"submit\" style=\"${btnCss(true)}\" disabled>${editing ? L.linkSave : L.linkInsert}</button>` +\n \"</div></div>\";\n\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const text = q<HTMLInputElement>(\"text\");\n const urlIn = q<HTMLInputElement>(\"url\");\n const search = q<HTMLInputElement>(\"search\");\n\n if (editing) {\n text.value = linkEditing?.getAttribute(\"data-cms-ref-label\") === \"auto\" ? \"\" : (linkEditing?.textContent ?? \"\");\n if (!existingRef) urlIn.value = linkEditing?.getAttribute(\"href\") ?? \"\";\n q<HTMLElement>(\"remove\").appendChild(buildRemoveLink());\n }\n\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((btn) => {\n btn.onclick = () => {\n const page = btn.dataset.tab === \"page\";\n d.querySelectorAll<HTMLElement>(\"[data-tab]\").forEach((b) => {\n b.setAttribute(\"style\", tabCss(b.dataset.tab === \"page\" ? page : !page));\n });\n (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display = page ? \"block\" : \"none\";\n (d.querySelector('[data-pane=\"url\"]') as HTMLElement).style.display = page ? \"none\" : \"block\";\n q<HTMLElement>(\"live\").style.display = page ? \"flex\" : \"none\";\n syncLinkDialog();\n };\n });\n\n search.oninput = () => renderLinkList(search.value);\n text.oninput = syncLinkDialog;\n urlIn.oninput = syncLinkDialog;\n q<HTMLElement>(\"cancel\").onclick = hideLinkDialog;\n q<HTMLElement>(\"submit\").onclick = applyLink;\n\n renderLinkList(\"\");\n syncLinkDialog();\n}\n\nfunction buildRemoveLink(): HTMLElement {\n const wrap = document.createElement(\"div\");\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.setAttribute(\"data-testid\", \"inline-link-remove\");\n btn.textContent = uiLabels.linkRemove;\n btn.style.cssText =\n \"background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;\";\n btn.onclick = () => {\n wrap.innerHTML =\n `<span style=\"font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px\">${uiLabels.linkRemoveConfirm}</span>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-yes\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkYes}</button>` +\n `<button type=\"button\" data-testid=\"inline-link-remove-no\" style=\"font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px\">${uiLabels.linkNo}</button>`;\n (wrap.querySelector('[data-testid=\"inline-link-remove-no\"]') as HTMLElement).onclick = () => {\n wrap.innerHTML = \"\";\n wrap.appendChild(btn);\n };\n (wrap.querySelector('[data-testid=\"inline-link-remove-yes\"]') as HTMLElement).onclick = () => {\n if (linkEditing) {\n const parent = linkEditing.parentNode;\n while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);\n parent?.removeChild(linkEditing);\n }\n hideLinkDialog();\n };\n };\n wrap.appendChild(btn);\n return wrap;\n}\n\nfunction renderLinkList(filter: string): void {\n const d = linkDialog;\n if (!d) return;\n const list = d.querySelector('[data-role=\"list\"]') as HTMLElement | null;\n if (!list) return;\n\n const paint = (pages: LinkablePage[]) => {\n const f = filter.trim().toLowerCase();\n const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));\n if (!shown.length) {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkEmpty}</div>`;\n return;\n }\n list.innerHTML = \"\";\n shown.forEach((p) => {\n const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;\n const row = document.createElement(\"div\");\n row.setAttribute(\"data-testid\", `inline-link-page-${p.collection}-${p.slug}`);\n row.style.cssText =\n \"display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;\" +\n \"border-bottom:1px solid #232833;\" +\n (on ? \"background:rgba(0,178,255,.14);\" : \"\");\n row.innerHTML =\n `<div style=\"min-width:0;flex:1\"><div style=\"font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.title)}</div>` +\n `<div style=\"font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">${escapeHtml(p.path)}</div></div>` +\n `<span style=\"flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap\">${escapeHtml(p.label)}</span>`;\n row.onclick = () => {\n linkPicked = p;\n renderLinkList(filter);\n syncLinkDialog();\n };\n list.appendChild(row);\n });\n };\n\n if (linkPages) {\n paint(linkPages);\n return;\n }\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#9aa3b2\">${uiLabels.linkLoading}</div>`;\n fetchLinkablePages()\n .then((pages) => {\n // Pre-select the page an existing reference already points at.\n const ref = linkEditing?.getAttribute(\"data-cms-ref\");\n if (ref && !linkPicked) {\n const [c, ...rest] = ref.split(\":\");\n linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(\":\")) ?? null;\n }\n paint(pages);\n syncLinkDialog();\n })\n .catch(() => {\n list.innerHTML = `<div style=\"padding:10px;font-size:12.5px;color:#ff8a8a\">${uiLabels.error}</div>`;\n });\n}\n\nfunction syncLinkDialog(): void {\n const d = linkDialog;\n if (!d) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n q<HTMLElement>(\"hint\").innerHTML = own\n ? (uiLabels.linkTextOwnHint as string)\n : onPage\n ? (uiLabels.linkTextAutoHint as string)\n : (uiLabels.linkUrlHint as string);\n q<HTMLElement>(\"live\").style.display = onPage ? \"flex\" : \"none\";\n (q<HTMLButtonElement>(\"submit\")).disabled = onPage\n ? !linkPicked\n : !q<HTMLInputElement>(\"url\").value.trim();\n}\n\nfunction applyLink(): void {\n const d = linkDialog;\n if (!d || !richCtx) return;\n const q = <T extends HTMLElement>(role: string) => d.querySelector(`[data-role=\"${role}\"]`) as T;\n const onPage = (d.querySelector('[data-pane=\"page\"]') as HTMLElement).style.display !== \"none\";\n const own = q<HTMLInputElement>(\"text\").value.trim();\n const href = onPage ? (linkPicked?.path ?? \"\") : q<HTMLInputElement>(\"url\").value.trim();\n if (!href) return;\n\n const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : \"\";\n const label = own || (onPage ? (linkPicked?.title ?? href) : href);\n\n if (linkEditing) {\n linkEditing.setAttribute(\"href\", href);\n if (ref) linkEditing.setAttribute(\"data-cms-ref\", ref);\n else linkEditing.removeAttribute(\"data-cms-ref\");\n if (ref && !own) linkEditing.setAttribute(\"data-cms-ref-label\", \"auto\");\n else linkEditing.removeAttribute(\"data-cms-ref-label\");\n linkEditing.textContent = label;\n hideLinkDialog();\n return;\n }\n\n const a = document.createElement(\"a\");\n a.setAttribute(\"href\", href);\n if (ref) {\n a.setAttribute(\"data-cms-ref\", ref);\n if (!own) a.setAttribute(\"data-cms-ref-label\", \"auto\");\n }\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n const range = sel.getRangeAt(0);\n a.textContent = own || range.toString() || label;\n range.deleteContents();\n range.insertNode(a);\n sel.removeAllRanges();\n } else {\n a.textContent = label;\n richCtx.el.appendChild(a);\n }\n savedRange = null;\n hideLinkDialog();\n}\n\nfunction escapeHtml(v: string): string {\n return v.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nconst tabCss = (on: boolean) =>\n \"flex:1;background:\" +\n (on ? \"#2a2f38\" : \"none\") +\n \";border:1px solid \" +\n (on ? \"#3a3f4a\" : \"transparent\") +\n \";color:\" +\n (on ? \"#fff\" : \"#9aa3b2\") +\n \";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;\";\n\nconst fieldCss = () =>\n \"width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;\" +\n \"border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;\";\n\nconst labelCss = () =>\n \"font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;\";\n\nconst btnCss = (primary: boolean) =>\n \"height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;\" +\n (primary\n ? \"background:#00b2ff;color:#04121b;border:none;\"\n : \"background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;\");\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nexport function htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n reattachOrphanListItems(container);\n const md = serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim();\n // A whole document keeps its trailing newline — that is the file convention\n // a `richtext` body is stored with. A SINGLE-LINE inline field must NOT gain\n // one: a heading is not a document, and the newline is stored, shows up in\n // the CMS editor, and appears out of nowhere the first time anyone edits the\n // field. Measured on webhouse.dk 2026-08-24, where every heading became a\n // rich field: \"Drift & Infrastruktur\" came back as \"Drift & Infrastruktur\\n\".\n return md.includes(\"\\n\") ? md + \"\\n\" : md;\n}\n\nfunction isList(el: Element | null): el is HTMLElement {\n if (!el) return false;\n const tag = el.tagName.toLowerCase();\n return tag === \"ul\" || tag === \"ol\";\n}\n\n/**\n * A contenteditable region can end up with an <li> as a DIRECT child of the\n * field container — browsers routinely split a list and orphan an item when the\n * caret sits in it, and `execCommand(\"insertUnorderedList\")` does the same.\n * Serializing an orphan through the inline default silently drops its marker,\n * so a list item degrades into a paragraph and the stored Markdown is corrupted\n * (sanneandersen, 2026-08-16: a 13-item CV list saved back as 1 paragraph + 12\n * bullets on a public page, on every single save).\n *\n * Put each orphan back in the adjacent list — before it if the list follows,\n * after it if the list precedes — so author order survives. An orphan with no\n * list next to it gets its own.\n */\nfunction reattachOrphanListItems(container: HTMLElement): void {\n const orphans = Array.from(container.children).filter(\n (c) => c.tagName.toLowerCase() === \"li\",\n );\n for (const li of orphans) {\n const prev = li.previousElementSibling;\n const next = li.nextElementSibling;\n if (isList(prev)) {\n prev.appendChild(li);\n } else if (isList(next)) {\n next.insertBefore(li, next.firstChild);\n } else {\n const list = container.ownerDocument.createElement(\"ul\");\n li.replaceWith(list);\n list.appendChild(li);\n }\n }\n}\n\n/** Tags serializeBlock treats as their own block; everything else is inline. */\nconst BLOCK_TAGS = new Set([\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\",\n \"p\", \"div\", \"ul\", \"ol\", \"li\", \"blockquote\", \"pre\", \"hr\", \"table\",\n]);\n\nfunction isBlockNode(node: Node): boolean {\n return (\n node.nodeType === Node.ELEMENT_NODE &&\n BLOCK_TAGS.has((node as HTMLElement).tagName.toLowerCase())\n );\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n // Text and inline elements sitting directly at block level form ONE implicit\n // paragraph. Serialising them one-by-one and joining with \"\\n\\n\" tore a\n // single sentence into separate blocks (\"Noget <b>fedt</b> mere\" became three\n // paragraphs) and dropped each element's own formatting. Gather the run, then\n // serialise it as the one paragraph the author actually wrote.\n let inlineRun = \"\";\n const flushInline = () => {\n const s = inlineRun.trim();\n if (s) blocks.push(s);\n inlineRun = \"\";\n };\n\n parent.childNodes.forEach((node) => {\n if (isBlockNode(node)) {\n flushInline();\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n } else {\n inlineRun += serializeInlineNode(node);\n }\n });\n flushInline();\n\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n // Backstop for an orphaned <li> that reattachOrphanListItems could not\n // place (e.g. nested inside another block). Without this it falls through\n // to the inline default and loses its marker — silent content loss.\n case \"li\":\n return \"- \" + serializeInline(el).trim();\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\n/** Escape a value for use inside a double-quoted HTML attribute (F164). */\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n out += serializeInlineNode(child);\n });\n return out;\n}\n\n/**\n * Serialize ONE inline node, applying ITS OWN tag's formatting.\n *\n * Split out of serializeInline because that function only ever looked at a\n * node's CHILDREN: handed an element it returned the element's text with the\n * element's own markup dropped. serializeBlock's default arm did exactly that,\n * so a <strong> (or <em>, <code>, <a>, <img>) sitting at the top level of an\n * edited field lost its formatting — or, for an image, vanished entirely — on\n * save. Same failure family as the orphaned <li>, one tag deeper.\n */\nfunction serializeInlineNode(child: Node): string {\n if (child.nodeType === Node.TEXT_NODE) {\n return (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return \"\";\n {\n let out = \"\";\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n const ref = el.getAttribute(\"data-cms-ref\");\n if (ref) {\n // F164 — a link to a PAGE on the site carries a reference so the page\n // can re-resolve href (and optionally the label) when that page moves\n // or is renamed. Markdown link syntax has nowhere to put that, so\n // emit inline HTML, which `marked` passes through untouched.\n // Collapsing to [text](href) here would strip the reference on the\n // editor's very FIRST save and silently kill the whole feature.\n const attrs = [`href=\"${escapeAttr(href)}\"`, `data-cms-ref=\"${escapeAttr(ref)}\"`];\n const label = el.getAttribute(\"data-cms-ref-label\");\n if (label) attrs.push(`data-cms-ref-label=\"${escapeAttr(label)}\"`);\n out += `<a ${attrs.join(\" \")}>${inner}</a>`;\n } else {\n out += href ? `[${inner}](${href})` : inner;\n }\n break;\n }\n case \"img\": {\n // Content images embedded in the body (![alt](src)) — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += `![${alt}](${src})`;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n return out;\n }\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n const slice = el.dataset.cmsSlice;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (slice !== undefined) {\n // Field-slice save: replace just this segment inside the full field value,\n // preserving every other segment + embed. Aborts on a non-unique match.\n const currentVal = typeof mergedData[field] === \"string\" ? (mergedData[field] as string) : \"\";\n mergedData[field] = applyFieldSlice(currentVal, slice, value);\n } else if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n // The slice we just wrote is now the current text — track it so a second\n // edit of the same segment (no reload) matches against the new value.\n if (slice !== undefined) el.dataset.cmsSlice = value;\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n // Clicking the badge LEAVES edit mode but KEEPS the login → back to the idle\n // \"Rediger\" pill (not a logout). Explicit logout lives on the idle pill.\n badge.addEventListener(\"click\", exitEditMode);\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n // The hover-outline + text-cursor affordance is gated on body[data-cms-editing]\n // so it appears ONLY while actively editing (after \"Rediger\"). In idle a\n // connected editor's fields look + behave like normal page text — no dashed\n // outline, no text cursor — so the page never looks editable before you opt in.\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; transition: outline-color .15s; }\n body[data-cms-editing=\"true\"] [data-cms-field] { cursor: text; }\n body[data-cms-editing=\"true\"] [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@broberg/cms-inline-edit",
3
- "version": "0.5.1",
4
- "description": "Click-to-edit inline editing for live @webhouse/cms-powered sites copy-owned, framework-agnostic",
3
+ "version": "0.5.3",
4
+ "description": "Click-to-edit inline editing for live @webhouse/cms-powered sites \u2014 copy-owned, framework-agnostic",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./dist/index.cjs",