@broberg/cms-inline-edit 0.4.13 → 0.4.14

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
@@ -26,6 +26,18 @@ __export(index_exports, {
26
26
  initInlineEdit: () => initInlineEdit
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
+ var DEFAULT_LABELS = {
30
+ bold: "Fed",
31
+ italic: "Kursiv",
32
+ underline: "Understreget",
33
+ color: "Farve",
34
+ emoji: "Inds\xE6t emoji",
35
+ done: "F\xE6rdig",
36
+ saving: "Gemmer\u2026",
37
+ saved: "Gemt \u2713",
38
+ error: "Fejl \u2014 pr\xF8v igen"
39
+ };
40
+ var uiLabels = DEFAULT_LABELS;
29
41
  function resolveOptions(options) {
30
42
  return {
31
43
  tokenParam: "cms_edit",
@@ -33,12 +45,14 @@ function resolveOptions(options) {
33
45
  // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.
34
46
  connectLabel: "Log ind for at redigere",
35
47
  disconnectLabel: "Afbryd",
36
- ...options
48
+ ...options,
49
+ labels: { ...DEFAULT_LABELS, ...options.labels ?? {} }
37
50
  };
38
51
  }
39
52
  async function initInlineEdit(options) {
40
53
  if (typeof window === "undefined") return;
41
54
  const resolved = resolveOptions(options);
55
+ uiLabels = resolved.labels;
42
56
  captureTokenFromUrl(resolved);
43
57
  const enabled = await checkEnabled(resolved);
44
58
  if (!enabled) return;
@@ -240,9 +254,9 @@ function buildRichToolbar() {
240
254
  const t = document.createElement("div");
241
255
  t.setAttribute("data-cms-inline-edit-toolbar", "");
242
256
  t.style.cssText = "position:fixed;top:14px;left:50%;transform:translateX(-50%);background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;padding:6px 10px;display:flex;gap:6px;align-items:center;z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:system-ui,sans-serif;";
243
- t.appendChild(toolbarButton("<b>B</b>", "Fed", () => document.execCommand("bold")));
244
- t.appendChild(toolbarButton("<i>I</i>", "Kursiv", () => document.execCommand("italic")));
245
- t.appendChild(toolbarButton("<u>U</u>", "Understreget", () => document.execCommand("underline")));
257
+ t.appendChild(toolbarButton("<b>B</b>", uiLabels.bold, () => document.execCommand("bold")));
258
+ t.appendChild(toolbarButton("<i>I</i>", uiLabels.italic, () => document.execCommand("italic")));
259
+ t.appendChild(toolbarButton("<u>U</u>", uiLabels.underline, () => document.execCommand("underline")));
246
260
  const sep = () => {
247
261
  const s = document.createElement("div");
248
262
  s.style.cssText = "width:1px;height:20px;background:#3a3f4a;";
@@ -251,7 +265,7 @@ function buildRichToolbar() {
251
265
  t.appendChild(sep());
252
266
  const clrLabel = document.createElement("label");
253
267
  clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
254
- clrLabel.textContent = "Farve";
268
+ clrLabel.textContent = uiLabels.color;
255
269
  const clr = document.createElement("input");
256
270
  clr.type = "color";
257
271
  clr.style.cssText = "width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;";
@@ -260,13 +274,13 @@ function buildRichToolbar() {
260
274
  clrLabel.prepend(clr);
261
275
  t.appendChild(clrLabel);
262
276
  t.appendChild(sep());
263
- const emojiBtn = toolbarButton("\u{1F600}", "Inds\xE6t emoji", () => toggleEmojiPicker(emojiBtn));
277
+ const emojiBtn = toolbarButton("\u{1F600}", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));
264
278
  emojiBtn.style.fontSize = "16px";
265
279
  t.appendChild(emojiBtn);
266
280
  t.appendChild(sep());
267
281
  const done = document.createElement("button");
268
282
  done.type = "button";
269
- done.textContent = "F\xE6rdig";
283
+ done.textContent = uiLabels.done;
270
284
  done.style.cssText = "background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;";
271
285
  done.addEventListener("mousedown", (e) => {
272
286
  e.preventDefault();
@@ -538,11 +552,11 @@ function showPill(el, state) {
538
552
  const rect = el.getBoundingClientRect();
539
553
  pill.style.cssText = `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,.25);`;
540
554
  if (state === "saving") {
541
- pill.textContent = "Gemmer\u2026";
555
+ pill.textContent = uiLabels.saving;
542
556
  pill.style.background = "#1c2027";
543
557
  pill.style.color = "#fff";
544
558
  } else if (state === "saved") {
545
- pill.textContent = "Gemt \u2713";
559
+ pill.textContent = uiLabels.saved;
546
560
  pill.style.background = "#16a34a";
547
561
  pill.style.color = "#fff";
548
562
  setTimeout(() => {
@@ -550,7 +564,7 @@ function showPill(el, state) {
550
564
  pills.delete(el);
551
565
  }, 1500);
552
566
  } else {
553
- pill.textContent = "Fejl \u2014 pr\xF8v igen";
567
+ pill.textContent = uiLabels.error;
554
568
  pill.style.background = "#dc2626";
555
569
  pill.style.color = "#fff";
556
570
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.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 */\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\" badge — the whole pill is the exit-edit action (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n}\n\ninterface ResolvedOptions extends Required<InlineEditOptions> {}\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 ...options,\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n\n captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (token) {\n activateEditMode(token, resolved);\n } else {\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}\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 activateEditMode(data.token, options);\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)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\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 the URL. */\nfunction captureTokenFromUrl(options: ResolvedOptions): void {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\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\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\nfunction activateEditMode(token: string, options: ResolvedOptions): void {\n injectStyles();\n showActiveBadge(options);\n\n const fields = document.querySelectorAll<HTMLElement>(\"[data-cms-field]\");\n fields.forEach((el) => wireField(el, token, options));\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\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 (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 el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = 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 (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 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>\", \"Fed\", () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", \"Kursiv\", () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", \"Understreget\", () => 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 // 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 = \"Farve\";\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(\"😀\", \"Indsæt 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 = \"Færdig\";\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\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 */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\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 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\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\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 out += href ? `[${inner}](${href})` : inner;\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 });\n return out;\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\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 (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 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 = \"Gemmer…\";\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = \"Gemt ✓\";\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 = \"Fejl — prøv igen\";\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 badge.addEventListener(\"click\", () => {\n disconnect(options);\n window.location.reload();\n });\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 style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [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 [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\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/index.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 */\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 color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: 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\" badge — the whole pill is the exit-edit action (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\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 color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\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 ...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 captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (token) {\n activateEditMode(token, resolved);\n } else {\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}\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 activateEditMode(data.token, options);\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)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\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 the URL. */\nfunction captureTokenFromUrl(options: ResolvedOptions): void {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\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\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\nfunction activateEditMode(token: string, options: ResolvedOptions): void {\n injectStyles();\n showActiveBadge(options);\n\n const fields = document.querySelectorAll<HTMLElement>(\"[data-cms-field]\");\n fields.forEach((el) => wireField(el, token, options));\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\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 (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 el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = 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 (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 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 // 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\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 */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\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 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\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\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 out += href ? `[${inner}](${href})` : inner;\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 });\n return out;\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\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 (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 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 badge.addEventListener(\"click\", () => {\n disconnect(options);\n window.location.reload();\n });\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 style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [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 [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\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/dist/index.d.cts CHANGED
@@ -5,6 +5,24 @@
5
5
  * element carrying data-cms-collection/data-cms-slug/data-cms-field
6
6
  * (F129's attribute convention) — no per-document step.
7
7
  */
8
+ /**
9
+ * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
10
+ * tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
11
+ * save-status pill. Every field is optional; unset fields keep the Danish
12
+ * default. Pass an English (or any-locale) set on a non-Danish site so the
13
+ * toolbar and status text match the page language.
14
+ */
15
+ interface InlineEditLabels {
16
+ bold?: string;
17
+ italic?: string;
18
+ underline?: string;
19
+ color?: string;
20
+ emoji?: string;
21
+ done?: string;
22
+ saving?: string;
23
+ saved?: string;
24
+ error?: string;
25
+ }
8
26
  interface InlineEditOptions {
9
27
  /** Base URL of the CMS API, e.g. "https://webhouse.app". */
10
28
  cmsBaseUrl: string;
@@ -18,6 +36,8 @@ interface InlineEditOptions {
18
36
  connectLabel?: string;
19
37
  /** Text for the "connected" badge — the whole pill is the exit-edit action (icon prepended). Default "Afbryd". */
20
38
  disconnectLabel?: string;
39
+ /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */
40
+ labels?: InlineEditLabels;
21
41
  }
22
42
  declare function initInlineEdit(options: InlineEditOptions): Promise<void>;
23
43
  /**
@@ -34,4 +54,4 @@ declare function buildConnectUrl(options: InlineEditOptions, returnUrl: string):
34
54
  /** Clears the connected token in this browser (e.g. a "log out" action in a site's own /admin panel). */
35
55
  declare function disconnect(options: InlineEditOptions): void;
36
56
 
37
- export { type InlineEditOptions, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
57
+ export { type InlineEditLabels, type InlineEditOptions, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,24 @@
5
5
  * element carrying data-cms-collection/data-cms-slug/data-cms-field
6
6
  * (F129's attribute convention) — no per-document step.
7
7
  */
8
+ /**
9
+ * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
10
+ * tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
11
+ * save-status pill. Every field is optional; unset fields keep the Danish
12
+ * default. Pass an English (or any-locale) set on a non-Danish site so the
13
+ * toolbar and status text match the page language.
14
+ */
15
+ interface InlineEditLabels {
16
+ bold?: string;
17
+ italic?: string;
18
+ underline?: string;
19
+ color?: string;
20
+ emoji?: string;
21
+ done?: string;
22
+ saving?: string;
23
+ saved?: string;
24
+ error?: string;
25
+ }
8
26
  interface InlineEditOptions {
9
27
  /** Base URL of the CMS API, e.g. "https://webhouse.app". */
10
28
  cmsBaseUrl: string;
@@ -18,6 +36,8 @@ interface InlineEditOptions {
18
36
  connectLabel?: string;
19
37
  /** Text for the "connected" badge — the whole pill is the exit-edit action (icon prepended). Default "Afbryd". */
20
38
  disconnectLabel?: string;
39
+ /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */
40
+ labels?: InlineEditLabels;
21
41
  }
22
42
  declare function initInlineEdit(options: InlineEditOptions): Promise<void>;
23
43
  /**
@@ -34,4 +54,4 @@ declare function buildConnectUrl(options: InlineEditOptions, returnUrl: string):
34
54
  /** Clears the connected token in this browser (e.g. a "log out" action in a site's own /admin panel). */
35
55
  declare function disconnect(options: InlineEditOptions): void;
36
56
 
37
- export { type InlineEditOptions, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
57
+ export { type InlineEditLabels, type InlineEditOptions, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
package/dist/index.js CHANGED
@@ -1,4 +1,16 @@
1
1
  // src/index.ts
2
+ var DEFAULT_LABELS = {
3
+ bold: "Fed",
4
+ italic: "Kursiv",
5
+ underline: "Understreget",
6
+ color: "Farve",
7
+ emoji: "Inds\xE6t emoji",
8
+ done: "F\xE6rdig",
9
+ saving: "Gemmer\u2026",
10
+ saved: "Gemt \u2713",
11
+ error: "Fejl \u2014 pr\xF8v igen"
12
+ };
13
+ var uiLabels = DEFAULT_LABELS;
2
14
  function resolveOptions(options) {
3
15
  return {
4
16
  tokenParam: "cms_edit",
@@ -6,12 +18,14 @@ function resolveOptions(options) {
6
18
  // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.
7
19
  connectLabel: "Log ind for at redigere",
8
20
  disconnectLabel: "Afbryd",
9
- ...options
21
+ ...options,
22
+ labels: { ...DEFAULT_LABELS, ...options.labels ?? {} }
10
23
  };
11
24
  }
12
25
  async function initInlineEdit(options) {
13
26
  if (typeof window === "undefined") return;
14
27
  const resolved = resolveOptions(options);
28
+ uiLabels = resolved.labels;
15
29
  captureTokenFromUrl(resolved);
16
30
  const enabled = await checkEnabled(resolved);
17
31
  if (!enabled) return;
@@ -213,9 +227,9 @@ function buildRichToolbar() {
213
227
  const t = document.createElement("div");
214
228
  t.setAttribute("data-cms-inline-edit-toolbar", "");
215
229
  t.style.cssText = "position:fixed;top:14px;left:50%;transform:translateX(-50%);background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;padding:6px 10px;display:flex;gap:6px;align-items:center;z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:system-ui,sans-serif;";
216
- t.appendChild(toolbarButton("<b>B</b>", "Fed", () => document.execCommand("bold")));
217
- t.appendChild(toolbarButton("<i>I</i>", "Kursiv", () => document.execCommand("italic")));
218
- t.appendChild(toolbarButton("<u>U</u>", "Understreget", () => document.execCommand("underline")));
230
+ t.appendChild(toolbarButton("<b>B</b>", uiLabels.bold, () => document.execCommand("bold")));
231
+ t.appendChild(toolbarButton("<i>I</i>", uiLabels.italic, () => document.execCommand("italic")));
232
+ t.appendChild(toolbarButton("<u>U</u>", uiLabels.underline, () => document.execCommand("underline")));
219
233
  const sep = () => {
220
234
  const s = document.createElement("div");
221
235
  s.style.cssText = "width:1px;height:20px;background:#3a3f4a;";
@@ -224,7 +238,7 @@ function buildRichToolbar() {
224
238
  t.appendChild(sep());
225
239
  const clrLabel = document.createElement("label");
226
240
  clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
227
- clrLabel.textContent = "Farve";
241
+ clrLabel.textContent = uiLabels.color;
228
242
  const clr = document.createElement("input");
229
243
  clr.type = "color";
230
244
  clr.style.cssText = "width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;";
@@ -233,13 +247,13 @@ function buildRichToolbar() {
233
247
  clrLabel.prepend(clr);
234
248
  t.appendChild(clrLabel);
235
249
  t.appendChild(sep());
236
- const emojiBtn = toolbarButton("\u{1F600}", "Inds\xE6t emoji", () => toggleEmojiPicker(emojiBtn));
250
+ const emojiBtn = toolbarButton("\u{1F600}", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));
237
251
  emojiBtn.style.fontSize = "16px";
238
252
  t.appendChild(emojiBtn);
239
253
  t.appendChild(sep());
240
254
  const done = document.createElement("button");
241
255
  done.type = "button";
242
- done.textContent = "F\xE6rdig";
256
+ done.textContent = uiLabels.done;
243
257
  done.style.cssText = "background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;";
244
258
  done.addEventListener("mousedown", (e) => {
245
259
  e.preventDefault();
@@ -511,11 +525,11 @@ function showPill(el, state) {
511
525
  const rect = el.getBoundingClientRect();
512
526
  pill.style.cssText = `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,.25);`;
513
527
  if (state === "saving") {
514
- pill.textContent = "Gemmer\u2026";
528
+ pill.textContent = uiLabels.saving;
515
529
  pill.style.background = "#1c2027";
516
530
  pill.style.color = "#fff";
517
531
  } else if (state === "saved") {
518
- pill.textContent = "Gemt \u2713";
532
+ pill.textContent = uiLabels.saved;
519
533
  pill.style.background = "#16a34a";
520
534
  pill.style.color = "#fff";
521
535
  setTimeout(() => {
@@ -523,7 +537,7 @@ function showPill(el, state) {
523
537
  pills.delete(el);
524
538
  }, 1500);
525
539
  } else {
526
- pill.textContent = "Fejl \u2014 pr\xF8v igen";
540
+ pill.textContent = uiLabels.error;
527
541
  pill.style.background = "#dc2626";
528
542
  pill.style.color = "#fff";
529
543
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.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 */\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\" badge — the whole pill is the exit-edit action (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n}\n\ninterface ResolvedOptions extends Required<InlineEditOptions> {}\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 ...options,\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n\n captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (token) {\n activateEditMode(token, resolved);\n } else {\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}\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 activateEditMode(data.token, options);\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)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\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 the URL. */\nfunction captureTokenFromUrl(options: ResolvedOptions): void {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\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\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\nfunction activateEditMode(token: string, options: ResolvedOptions): void {\n injectStyles();\n showActiveBadge(options);\n\n const fields = document.querySelectorAll<HTMLElement>(\"[data-cms-field]\");\n fields.forEach((el) => wireField(el, token, options));\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\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 (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 el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = 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 (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 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>\", \"Fed\", () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", \"Kursiv\", () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", \"Understreget\", () => 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 // 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 = \"Farve\";\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(\"😀\", \"Indsæt 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 = \"Færdig\";\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\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 */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\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 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\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\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 out += href ? `[${inner}](${href})` : inner;\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 });\n return out;\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\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 (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 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 = \"Gemmer…\";\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = \"Gemt ✓\";\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 = \"Fejl — prøv igen\";\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 badge.addEventListener(\"click\", () => {\n disconnect(options);\n window.location.reload();\n });\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 style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [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 [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\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/index.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 */\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 color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: 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\" badge — the whole pill is the exit-edit action (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\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 color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\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 ...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 captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (token) {\n activateEditMode(token, resolved);\n } else {\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}\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 activateEditMode(data.token, options);\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)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\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 the URL. */\nfunction captureTokenFromUrl(options: ResolvedOptions): void {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\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\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\nfunction activateEditMode(token: string, options: ResolvedOptions): void {\n injectStyles();\n showActiveBadge(options);\n\n const fields = document.querySelectorAll<HTMLElement>(\"[data-cms-field]\");\n fields.forEach((el) => wireField(el, token, options));\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\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 (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 el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = 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 (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 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 // 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\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 */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\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 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\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\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 out += href ? `[${inner}](${href})` : inner;\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 });\n return out;\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\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 (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 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 badge.addEventListener(\"click\", () => {\n disconnect(options);\n window.location.reload();\n });\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 style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [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 [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\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,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/cms-inline-edit",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "Click-to-edit inline editing for live @webhouse/cms-powered sites — copy-owned, framework-agnostic",
5
5
  "type": "module",
6
6
  "license": "MIT",