@broberg/cms-inline-edit 0.4.21 → 0.5.1

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/README.md CHANGED
@@ -36,3 +36,56 @@ import { saveInlineEditField, verifyEditSession } from "@broberg/cms-inline-edit
36
36
  ```
37
37
 
38
38
  See `docs/features/F157-inline-editing.md` in the [@webhouse/cms](https://github.com/webhousecode/cms) repo for the full design.
39
+
40
+ ## Links to a page (live references)
41
+
42
+ The toolbar's link button offers a **free URL** or **a page on the site**. A page
43
+ link stores a reference next to a real, working href:
44
+
45
+ ```html
46
+ <a href="/da/om-sanne" data-cms-ref="sider:om-sanne" data-cms-ref-label="auto">Om Sanne</a>
47
+ ```
48
+
49
+ Render it through `resolveCmsLinks()` and the link re-points itself when the page
50
+ moves or is renamed — nothing rewrites stored content:
51
+
52
+ ```ts
53
+ import { resolveCmsLinks } from "@broberg/cms-inline-edit/server";
54
+
55
+ const html = resolveCmsLinks(renderMarkdown(doc.body), (collection, slug) => {
56
+ const page = findPage(collection, slug);
57
+ return page ? { url: page.path, title: page.title } : null;
58
+ });
59
+ ```
60
+
61
+ `data-cms-ref-label="auto"` (set when the editor left the link text empty) also
62
+ follows the page's current title. An unknown reference keeps the href it has, so
63
+ a deleted page degrades to the last known link rather than a dead one — and a
64
+ site that never calls the resolver still ships working links.
65
+
66
+ The picker's page list comes from `GET /api/inline-edit/pages?site=<id>`
67
+ (published documents only).
68
+
69
+ ## Markup in a PLAIN field — use `data-cms-token`
70
+
71
+ **Which save path a fix covers matters.** The serializer fixes in 0.4.21
72
+ (orphaned `<li>`) and 0.4.23 (top-level inline formatting, dropped images) live
73
+ in the **rich** path — a field marked `data-cms-richtext="true"`, serialised
74
+ through `htmlToMarkdown`.
75
+
76
+ A **plain** field is different: it saves `el.textContent`, and `textContent` has
77
+ no asterisks. So Markdown markup inside a plain field is lost on save, and
78
+ **0.4.23 does not change that.** If you are carrying a `data-cms-token`
79
+ workaround for markup in a plain field, KEEP IT after upgrading — it is not dead
80
+ code (reported by the sanneandersen session, 2026-08-17).
81
+
82
+ The supported way to keep markup in a plain field is `data-cms-token` on the
83
+ formatted segment, carrying the raw source:
84
+
85
+ ```html
86
+ <span data-cms-token="**Alt fra Blad**"><strong>Alt fra Blad</strong></span>
87
+ ```
88
+
89
+ `serializeTokenSafe()` round-trips those segments verbatim, so the stored value
90
+ keeps its `**…**` while the page shows the rendered form. Use the rich path
91
+ (`data-cms-richtext="true"`) when a field should support formatting generally.
package/dist/index.cjs CHANGED
@@ -82,7 +82,27 @@ var DEFAULT_LABELS = {
82
82
  done: "F\xE6rdig",
83
83
  saving: "Gemmer\u2026",
84
84
  saved: "Gemt \u2713",
85
- error: "Fejl \u2014 pr\xF8v igen"
85
+ error: "Fejl \u2014 pr\xF8v igen",
86
+ link: "Link",
87
+ linkTabPage: "Side p\xE5 sitet",
88
+ linkTabUrl: "Fri adresse",
89
+ linkSearch: "S\xF8g efter en side\u2026",
90
+ linkText: "Linktekst",
91
+ linkTextAuto: "F\xF8lger sidens titel",
92
+ linkTextAutoHint: "<b>Tom = f\xF8lger sidens titel.</b> Omd\xF8bes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den st\xE5ende.",
93
+ linkTextOwnHint: "Linket viser <b>din egen tekst</b>. Den bliver st\xE5ende, ogs\xE5 hvis siden omd\xF8bes.",
94
+ linkLiveHint: "Linket peger p\xE5 <b>siden</b> \u2014 ikke p\xE5 en adresse. Flyttes siden, eller f\xE5r den et nyt navn, retter linket sig selv.",
95
+ linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
96
+ linkUrl: "Adresse",
97
+ linkInsert: "Inds\xE6t link",
98
+ linkSave: "Gem \xE6ndring",
99
+ linkCancel: "Annull\xE9r",
100
+ linkRemove: "Fjern link",
101
+ linkRemoveConfirm: "Fjern?",
102
+ linkYes: "Ja",
103
+ linkNo: "Nej",
104
+ linkEmpty: "Ingen sider fundet",
105
+ linkLoading: "Henter sider\u2026"
86
106
  };
87
107
  var uiLabels = DEFAULT_LABELS;
88
108
  function resolveOptions(options) {
@@ -143,12 +163,18 @@ function getConnectedToken(options) {
143
163
  captureTokenFromUrl(resolved);
144
164
  const token = localStorage.getItem(resolved.storageKey);
145
165
  if (!token) return null;
146
- if (isExpired(token)) {
166
+ if (isExpired(token) || !isForThisSite(token, resolved.siteId)) {
147
167
  localStorage.removeItem(resolved.storageKey);
148
168
  return null;
149
169
  }
150
170
  return token;
151
171
  }
172
+ function isForThisSite(token, siteId) {
173
+ const claims = decodeJwtPayload(token);
174
+ const site = claims?.site;
175
+ if (typeof site !== "string" || !site) return true;
176
+ return site === siteId;
177
+ }
152
178
  function buildConnectUrl(options, returnUrl) {
153
179
  const resolved = resolveOptions(options);
154
180
  return `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}&return=${encodeURIComponent(returnUrl)}`;
@@ -184,6 +210,7 @@ function isExpired(token) {
184
210
  var SQUARE_PEN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><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"/></svg>';
185
211
  var UL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>';
186
212
  var OL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>';
213
+ var LINK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg>';
187
214
  function makeIcon() {
188
215
  const icon = document.createElement("span");
189
216
  icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
@@ -342,6 +369,7 @@ function deactivateRich() {
342
369
  el.removeAttribute("contenteditable");
343
370
  el.classList.remove("cms-rich-editing");
344
371
  hideRichToolbar();
372
+ hideLinkDialog();
345
373
  if (el.innerHTML !== originalHtml) {
346
374
  const value = mode === "html" ? el.innerHTML : htmlToMarkdown(el.innerHTML);
347
375
  void saveField(el, value, token, options);
@@ -387,6 +415,10 @@ function buildRichToolbar() {
387
415
  t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
388
416
  t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
389
417
  t.appendChild(sep());
418
+ const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));
419
+ linkBtn.setAttribute("data-testid", "inline-toolbar-link");
420
+ t.appendChild(linkBtn);
421
+ t.appendChild(sep());
390
422
  const clrLabel = document.createElement("label");
391
423
  clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
392
424
  clrLabel.textContent = uiLabels.color;
@@ -420,6 +452,231 @@ var EMOJIS = "\u{1F600} \u{1F603} \u{1F604} \u{1F601} \u{1F606} \u{1F609} \u{1F6
420
452
  );
421
453
  var emojiPicker = null;
422
454
  var savedRange = null;
455
+ var linkDialog = null;
456
+ var linkPages = null;
457
+ var linkPicked = null;
458
+ var linkEditing = null;
459
+ function hideLinkDialog() {
460
+ if (linkDialog) linkDialog.style.display = "none";
461
+ linkPicked = null;
462
+ linkEditing = null;
463
+ }
464
+ function anchorAtCaret() {
465
+ const sel = window.getSelection();
466
+ if (!sel || sel.rangeCount === 0) return null;
467
+ let node = sel.getRangeAt(0).startContainer;
468
+ while (node && node !== document.body) {
469
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "A") {
470
+ return node;
471
+ }
472
+ node = node.parentNode;
473
+ }
474
+ return null;
475
+ }
476
+ async function fetchLinkablePages() {
477
+ if (linkPages) return linkPages;
478
+ if (!richCtx) return [];
479
+ const { options, token } = richCtx;
480
+ const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;
481
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
482
+ if (!res.ok) throw new Error(`pages ${res.status}`);
483
+ const body = await res.json();
484
+ linkPages = body.pages ?? [];
485
+ return linkPages;
486
+ }
487
+ function toggleLinkDialog(anchor) {
488
+ if (linkDialog && linkDialog.style.display === "block") {
489
+ hideLinkDialog();
490
+ return;
491
+ }
492
+ const sel = window.getSelection();
493
+ if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
494
+ linkEditing = anchorAtCaret();
495
+ if (!linkDialog) linkDialog = buildLinkDialog();
496
+ renderLinkDialog();
497
+ const rect = anchor.getBoundingClientRect();
498
+ linkDialog.style.top = `${rect.bottom + 8}px`;
499
+ linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;
500
+ linkDialog.style.display = "block";
501
+ }
502
+ function buildLinkDialog() {
503
+ const d = document.createElement("div");
504
+ d.setAttribute("data-cms-inline-edit-toolbar", "");
505
+ d.setAttribute("data-testid", "inline-link-dialog");
506
+ d.style.cssText = "position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:system-ui,sans-serif;color:#fff;overflow:hidden;";
507
+ d.addEventListener("mousedown", (e) => {
508
+ const t = e.target;
509
+ if (t.tagName !== "INPUT") e.preventDefault();
510
+ e.stopPropagation();
511
+ });
512
+ document.body.appendChild(d);
513
+ return d;
514
+ }
515
+ function renderLinkDialog() {
516
+ const d = linkDialog;
517
+ if (!d) return;
518
+ const L = uiLabels;
519
+ const editing = !!linkEditing;
520
+ const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
521
+ const onPageTab = !editing || !!existingRef;
522
+ d.innerHTML = `<div style="display:flex;gap:4px;padding:8px 8px 0"><button type="button" data-testid="inline-link-tab-page" data-tab="page" style="${tabCss(onPageTab)}">${L.linkTabPage}</button><button type="button" data-testid="inline-link-tab-url" data-tab="url" style="${tabCss(!onPageTab)}">${L.linkTabUrl}</button></div><div style="padding:10px 12px 12px"><div data-pane="page" style="display:${onPageTab ? "block" : "none"}"><input data-testid="inline-link-search" data-role="search" placeholder="${L.linkSearch}" style="${fieldCss()}"><div data-role="list" data-testid="inline-link-list" style="margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821"></div></div><div data-pane="url" style="display:${onPageTab ? "none" : "block"}"><label style="${labelCss()}">${L.linkUrl}</label><input data-testid="inline-link-url" data-role="url" placeholder="https://\u2026" style="${fieldCss()}"></div><label style="${labelCss()}">${L.linkText}</label><input data-testid="inline-link-text" data-role="text" placeholder="${L.linkTextAuto}" style="${fieldCss()}"><p data-role="hint" style="font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5"></p><div data-role="live" style="display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px"><p style="margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55">${L.linkLiveHint}</p></div><div style="display:flex;gap:8px;align-items:center;margin-top:14px"><div data-role="remove"></div><div style="flex:1"></div><button type="button" data-testid="inline-link-cancel" data-role="cancel" style="${btnCss(false)}">${L.linkCancel}</button><button type="button" data-testid="inline-link-submit" data-role="submit" style="${btnCss(true)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
523
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
524
+ const text = q("text");
525
+ const urlIn = q("url");
526
+ const search = q("search");
527
+ if (editing) {
528
+ text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
529
+ if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
530
+ q("remove").appendChild(buildRemoveLink());
531
+ }
532
+ d.querySelectorAll("[data-tab]").forEach((btn) => {
533
+ btn.onclick = () => {
534
+ const page = btn.dataset.tab === "page";
535
+ d.querySelectorAll("[data-tab]").forEach((b) => {
536
+ b.setAttribute("style", tabCss(b.dataset.tab === "page" ? page : !page));
537
+ });
538
+ d.querySelector('[data-pane="page"]').style.display = page ? "block" : "none";
539
+ d.querySelector('[data-pane="url"]').style.display = page ? "none" : "block";
540
+ q("live").style.display = page ? "flex" : "none";
541
+ syncLinkDialog();
542
+ };
543
+ });
544
+ search.oninput = () => renderLinkList(search.value);
545
+ text.oninput = syncLinkDialog;
546
+ urlIn.oninput = syncLinkDialog;
547
+ q("cancel").onclick = hideLinkDialog;
548
+ q("submit").onclick = applyLink;
549
+ renderLinkList("");
550
+ syncLinkDialog();
551
+ }
552
+ function buildRemoveLink() {
553
+ const wrap = document.createElement("div");
554
+ const btn = document.createElement("button");
555
+ btn.type = "button";
556
+ btn.setAttribute("data-testid", "inline-link-remove");
557
+ btn.textContent = uiLabels.linkRemove;
558
+ btn.style.cssText = "background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;";
559
+ btn.onclick = () => {
560
+ wrap.innerHTML = `<span style="font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px">${uiLabels.linkRemoveConfirm}</span><button type="button" data-testid="inline-link-remove-yes" style="font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px">${uiLabels.linkYes}</button><button type="button" data-testid="inline-link-remove-no" style="font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px">${uiLabels.linkNo}</button>`;
561
+ wrap.querySelector('[data-testid="inline-link-remove-no"]').onclick = () => {
562
+ wrap.innerHTML = "";
563
+ wrap.appendChild(btn);
564
+ };
565
+ wrap.querySelector('[data-testid="inline-link-remove-yes"]').onclick = () => {
566
+ if (linkEditing) {
567
+ const parent = linkEditing.parentNode;
568
+ while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);
569
+ parent?.removeChild(linkEditing);
570
+ }
571
+ hideLinkDialog();
572
+ };
573
+ };
574
+ wrap.appendChild(btn);
575
+ return wrap;
576
+ }
577
+ function renderLinkList(filter) {
578
+ const d = linkDialog;
579
+ if (!d) return;
580
+ const list = d.querySelector('[data-role="list"]');
581
+ if (!list) return;
582
+ const paint = (pages) => {
583
+ const f = filter.trim().toLowerCase();
584
+ const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));
585
+ if (!shown.length) {
586
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkEmpty}</div>`;
587
+ return;
588
+ }
589
+ list.innerHTML = "";
590
+ shown.forEach((p) => {
591
+ const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;
592
+ const row = document.createElement("div");
593
+ row.setAttribute("data-testid", `inline-link-page-${p.collection}-${p.slug}`);
594
+ row.style.cssText = "display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;border-bottom:1px solid #232833;" + (on ? "background:rgba(0,178,255,.14);" : "");
595
+ row.innerHTML = `<div style="min-width:0;flex:1"><div style="font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(p.title)}</div><div style="font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(p.path)}</div></div><span style="flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap">${escapeHtml(p.label)}</span>`;
596
+ row.onclick = () => {
597
+ linkPicked = p;
598
+ renderLinkList(filter);
599
+ syncLinkDialog();
600
+ };
601
+ list.appendChild(row);
602
+ });
603
+ };
604
+ if (linkPages) {
605
+ paint(linkPages);
606
+ return;
607
+ }
608
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
609
+ fetchLinkablePages().then((pages) => {
610
+ const ref = linkEditing?.getAttribute("data-cms-ref");
611
+ if (ref && !linkPicked) {
612
+ const [c, ...rest] = ref.split(":");
613
+ linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(":")) ?? null;
614
+ }
615
+ paint(pages);
616
+ syncLinkDialog();
617
+ }).catch(() => {
618
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#ff8a8a">${uiLabels.error}</div>`;
619
+ });
620
+ }
621
+ function syncLinkDialog() {
622
+ const d = linkDialog;
623
+ if (!d) return;
624
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
625
+ const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
626
+ const own = q("text").value.trim();
627
+ q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
628
+ q("live").style.display = onPage ? "flex" : "none";
629
+ q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
630
+ }
631
+ function applyLink() {
632
+ const d = linkDialog;
633
+ if (!d || !richCtx) return;
634
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
635
+ const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
636
+ const own = q("text").value.trim();
637
+ const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
638
+ if (!href) return;
639
+ const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
640
+ const label = own || (onPage ? linkPicked?.title ?? href : href);
641
+ if (linkEditing) {
642
+ linkEditing.setAttribute("href", href);
643
+ if (ref) linkEditing.setAttribute("data-cms-ref", ref);
644
+ else linkEditing.removeAttribute("data-cms-ref");
645
+ if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
646
+ else linkEditing.removeAttribute("data-cms-ref-label");
647
+ linkEditing.textContent = label;
648
+ hideLinkDialog();
649
+ return;
650
+ }
651
+ const a = document.createElement("a");
652
+ a.setAttribute("href", href);
653
+ if (ref) {
654
+ a.setAttribute("data-cms-ref", ref);
655
+ if (!own) a.setAttribute("data-cms-ref-label", "auto");
656
+ }
657
+ const sel = window.getSelection();
658
+ if (savedRange && sel) {
659
+ sel.removeAllRanges();
660
+ sel.addRange(savedRange);
661
+ const range = sel.getRangeAt(0);
662
+ a.textContent = own || range.toString() || label;
663
+ range.deleteContents();
664
+ range.insertNode(a);
665
+ sel.removeAllRanges();
666
+ } else {
667
+ a.textContent = label;
668
+ richCtx.el.appendChild(a);
669
+ }
670
+ savedRange = null;
671
+ hideLinkDialog();
672
+ }
673
+ function escapeHtml(v) {
674
+ return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
675
+ }
676
+ var tabCss = (on) => "flex:1;background:" + (on ? "#2a2f38" : "none") + ";border:1px solid " + (on ? "#3a3f4a" : "transparent") + ";color:" + (on ? "#fff" : "#9aa3b2") + ";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;";
677
+ var fieldCss = () => "width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;";
678
+ var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
679
+ var btnCss = (primary) => "height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;" + (primary ? "background:#00b2ff;color:#04121b;border:none;" : "background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;");
423
680
  function toggleEmojiPicker(anchor) {
424
681
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
425
682
  if (emojiPicker.style.display === "block") {
@@ -529,12 +786,44 @@ function reattachOrphanListItems(container) {
529
786
  }
530
787
  }
531
788
  }
789
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
790
+ "h1",
791
+ "h2",
792
+ "h3",
793
+ "h4",
794
+ "h5",
795
+ "h6",
796
+ "p",
797
+ "div",
798
+ "ul",
799
+ "ol",
800
+ "li",
801
+ "blockquote",
802
+ "pre",
803
+ "hr",
804
+ "table"
805
+ ]);
806
+ function isBlockNode(node) {
807
+ return node.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
808
+ }
532
809
  function serializeBlockChildren(parent) {
533
810
  const blocks = [];
534
- parent.childNodes.forEach((node) => {
535
- const s = serializeBlock(node).trim();
811
+ let inlineRun = "";
812
+ const flushInline = () => {
813
+ const s = inlineRun.trim();
536
814
  if (s) blocks.push(s);
815
+ inlineRun = "";
816
+ };
817
+ parent.childNodes.forEach((node) => {
818
+ if (isBlockNode(node)) {
819
+ flushInline();
820
+ const s = serializeBlock(node).trim();
821
+ if (s) blocks.push(s);
822
+ } else {
823
+ inlineRun += serializeInlineNode(node);
824
+ }
537
825
  });
826
+ flushInline();
538
827
  return blocks.join("\n\n");
539
828
  }
540
829
  function serializeBlock(node) {
@@ -608,14 +897,23 @@ function serializeList(listEl, ordered) {
608
897
  });
609
898
  return items.join("\n");
610
899
  }
900
+ function escapeAttr(value) {
901
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
902
+ }
611
903
  function serializeInline(node) {
612
904
  let out = "";
613
905
  node.childNodes.forEach((child) => {
614
- if (child.nodeType === Node.TEXT_NODE) {
615
- out += (child.textContent ?? "").replace(/\s+/g, " ");
616
- return;
617
- }
618
- if (child.nodeType !== Node.ELEMENT_NODE) return;
906
+ out += serializeInlineNode(child);
907
+ });
908
+ return out;
909
+ }
910
+ function serializeInlineNode(child) {
911
+ if (child.nodeType === Node.TEXT_NODE) {
912
+ return (child.textContent ?? "").replace(/\s+/g, " ");
913
+ }
914
+ if (child.nodeType !== Node.ELEMENT_NODE) return "";
915
+ {
916
+ let out = "";
619
917
  const el = child;
620
918
  const tag = el.tagName.toLowerCase();
621
919
  const inner = serializeInline(el);
@@ -636,7 +934,15 @@ function serializeInline(node) {
636
934
  break;
637
935
  case "a": {
638
936
  const href = el.getAttribute("href") || "";
639
- out += href ? `[${inner}](${href})` : inner;
937
+ const ref = el.getAttribute("data-cms-ref");
938
+ if (ref) {
939
+ const attrs = [`href="${escapeAttr(href)}"`, `data-cms-ref="${escapeAttr(ref)}"`];
940
+ const label = el.getAttribute("data-cms-ref-label");
941
+ if (label) attrs.push(`data-cms-ref-label="${escapeAttr(label)}"`);
942
+ out += `<a ${attrs.join(" ")}>${inner}</a>`;
943
+ } else {
944
+ out += href ? `[${inner}](${href})` : inner;
945
+ }
640
946
  break;
641
947
  }
642
948
  case "img": {
@@ -657,8 +963,8 @@ function serializeInline(node) {
657
963
  default:
658
964
  out += inner;
659
965
  }
660
- });
661
- return out;
966
+ return out;
967
+ }
662
968
  }
663
969
  async function saveField(el, value, token, options) {
664
970
  const collection = el.dataset.cmsCollection;