@cancia/toolbar 0.13.0 → 0.15.0

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/cancia.js CHANGED
@@ -50,9 +50,9 @@ function renderRichToDom(target, blocks) {
50
50
  }
51
51
  listWrap = null;
52
52
  listKind = null;
53
- const el = document.createElement(STYLE_TAG[block.style] ?? "p");
54
- appendSpans(el, block);
55
- target.appendChild(el);
53
+ const el2 = document.createElement(STYLE_TAG[block.style] ?? "p");
54
+ appendSpans(el2, block);
55
+ target.appendChild(el2);
56
56
  }
57
57
  }
58
58
  function appendSpans(parent, block) {
@@ -87,8 +87,8 @@ var STYLE_OF_TAG = {
87
87
  };
88
88
  function domToRows(region) {
89
89
  const rows = [];
90
- const pushBlock = (el, style, listItem) => {
91
- const text = inlineToShorthand(el);
90
+ const pushBlock = (el2, style, listItem) => {
91
+ const text = inlineToShorthand(el2);
92
92
  if (text.trim() === "") return;
93
93
  rows.push(listItem ? { text, style, listItem } : { text, style });
94
94
  };
@@ -113,9 +113,9 @@ function domToRows(region) {
113
113
  }
114
114
  return rows;
115
115
  }
116
- function inlineToShorthand(el) {
116
+ function inlineToShorthand(el2) {
117
117
  let out = "";
118
- for (const node of el.childNodes) {
118
+ for (const node of el2.childNodes) {
119
119
  if (node.nodeType === Node.TEXT_NODE) {
120
120
  out += (node.textContent ?? "").replace(/\s+/g, " ");
121
121
  continue;
@@ -180,36 +180,36 @@ function setPending(key, lang, value) {
180
180
  state.pending.set(full, { key, lang, value });
181
181
  }
182
182
  function applyOverlay() {
183
- document.querySelectorAll("[data-cms]").forEach((el) => {
184
- if (el.dataset.cmsList !== void 0) return;
185
- const key = el.dataset.cms;
183
+ document.querySelectorAll("[data-cms]").forEach((el2) => {
184
+ if (el2.dataset.cmsList !== void 0) return;
185
+ const key = el2.dataset.cms;
186
186
  if (!key) return;
187
187
  const savedValue = state.cmsData[`${key}.${state.activeLang}`];
188
188
  if (savedValue === void 0) return;
189
- if (el.tagName === "IMG") {
190
- el.src = savedValue;
189
+ if (el2.tagName === "IMG") {
190
+ el2.src = savedValue;
191
191
  return;
192
192
  }
193
- if (el.dataset.cmsType === "richtext") {
193
+ if (el2.dataset.cmsType === "richtext") {
194
194
  const blocks = parseRichValue(savedValue);
195
- if (blocks) renderRichToDom(el, blocks);
195
+ if (blocks) renderRichToDom(el2, blocks);
196
196
  return;
197
197
  }
198
- if (el.dataset.cmsType === "link") {
198
+ if (el2.dataset.cmsType === "link") {
199
199
  const link = parseLinkValue(savedValue);
200
- if (link.href && el.tagName === "A") el.setAttribute("href", link.href);
201
- const labelEls = el.querySelectorAll("[data-cms-label]");
200
+ if (link.href && el2.tagName === "A") el2.setAttribute("href", link.href);
201
+ const labelEls = el2.querySelectorAll("[data-cms-label]");
202
202
  if (labelEls.length > 0) labelEls.forEach((n) => n.textContent = link.label);
203
- else if (el.childElementCount === 0) el.textContent = link.label;
203
+ else if (el2.childElementCount === 0) el2.textContent = link.label;
204
204
  return;
205
205
  }
206
- if (el.childElementCount > 0) {
206
+ if (el2.childElementCount > 0) {
207
207
  console.warn(
208
208
  `Cancia: skipping overlay for "${key}" \u2014 element has child elements (textContent would destroy them).`
209
209
  );
210
210
  return;
211
211
  }
212
- el.textContent = savedValue;
212
+ el2.textContent = savedValue;
213
213
  });
214
214
  }
215
215
  function revertPending() {
@@ -220,17 +220,17 @@ function revertPending() {
220
220
  return;
221
221
  }
222
222
  const savedValue = state.cmsData[fullKey];
223
- document.querySelectorAll(`[data-cms="${key}"]`).forEach((el) => {
224
- if (el.tagName === "IMG") {
225
- el.src = savedValue;
223
+ document.querySelectorAll(`[data-cms="${key}"]`).forEach((el2) => {
224
+ if (el2.tagName === "IMG") {
225
+ el2.src = savedValue;
226
226
  return;
227
227
  }
228
- if (el.dataset.cmsType === "richtext") {
228
+ if (el2.dataset.cmsType === "richtext") {
229
229
  const blocks = parseRichValue(savedValue);
230
- if (blocks) renderRichToDom(el, blocks);
230
+ if (blocks) renderRichToDom(el2, blocks);
231
231
  return;
232
232
  }
233
- el.textContent = savedValue;
233
+ el2.textContent = savedValue;
234
234
  });
235
235
  }
236
236
  state.pending.clear();
@@ -242,6 +242,7 @@ function headers() {
242
242
  if (state.sessionToken) h["Authorization"] = `Bearer ${state.sessionToken}`;
243
243
  return h;
244
244
  }
245
+ var CREDS = "include";
245
246
  function currentRoute() {
246
247
  return typeof location !== "undefined" ? location.pathname : "";
247
248
  }
@@ -252,6 +253,7 @@ function routeParam() {
252
253
  async function fetchContent() {
253
254
  const { apiUrl, site } = state.config;
254
255
  const res = await fetch(`${apiUrl}/api/cancia/content?site=${encodeURIComponent(site)}`, {
256
+ credentials: CREDS,
255
257
  headers: headers()
256
258
  });
257
259
  if (!res.ok) throw new Error(`Cancia: failed to fetch content (${res.status})`);
@@ -260,6 +262,7 @@ async function fetchContent() {
260
262
  async function saveEntry(key, lang, value) {
261
263
  const { apiUrl, site } = state.config;
262
264
  const res = await fetch(`${apiUrl}/api/cancia/save`, {
265
+ credentials: CREDS,
263
266
  method: "POST",
264
267
  headers: headers(),
265
268
  // `route` lets the server invalidate this page's cache tag (034, invalidate mode).
@@ -270,6 +273,7 @@ async function saveEntry(key, lang, value) {
270
273
  async function deleteContent(key, lang) {
271
274
  const { apiUrl, site } = state.config;
272
275
  const res = await fetch(`${apiUrl}/api/cancia/content`, {
276
+ credentials: CREDS,
273
277
  method: "DELETE",
274
278
  headers: headers(),
275
279
  body: JSON.stringify({ site, key, lang, route: currentRoute() })
@@ -294,6 +298,7 @@ function uploadImage(file, onProgress) {
294
298
  form.append("site", site);
295
299
  const xhr = new XMLHttpRequest();
296
300
  xhr.open("POST", `${apiUrl}/api/cancia/upload`);
301
+ xhr.withCredentials = true;
297
302
  if (state.sessionToken) xhr.setRequestHeader("Authorization", `Bearer ${state.sessionToken}`);
298
303
  xhr.upload.addEventListener("progress", (e) => {
299
304
  if (e.lengthComputable) onProgress?.(Math.round(e.loaded / e.total * 100));
@@ -316,6 +321,7 @@ function uploadImage(file, onProgress) {
316
321
  async function triggerPublish() {
317
322
  const { apiUrl, site } = state.config;
318
323
  const res = await fetch(`${apiUrl}/api/cancia/publish`, {
324
+ credentials: CREDS,
319
325
  method: "POST",
320
326
  headers: headers(),
321
327
  body: JSON.stringify({ site })
@@ -324,7 +330,7 @@ async function triggerPublish() {
324
330
  }
325
331
  async function fetchSchemas() {
326
332
  const { apiUrl } = state.config;
327
- const res = await fetch(`${apiUrl}/api/cancia/schemas`, { headers: headers() });
333
+ const res = await fetch(`${apiUrl}/api/cancia/schemas`, { headers: headers(), credentials: CREDS });
328
334
  if (!res.ok) throw new Error(`Cancia: failed to fetch schemas (${res.status})`);
329
335
  const body = await res.json();
330
336
  return body.schemas;
@@ -336,7 +342,7 @@ async function fetchList(listName, locale) {
336
342
  const { apiUrl, site } = state.config;
337
343
  const res = await fetch(
338
344
  `${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
339
- { headers: headers() }
345
+ { headers: headers(), credentials: CREDS }
340
346
  );
341
347
  if (!res.ok) throw new Error(`Cancia: failed to fetch list "${listName}" (${res.status})`);
342
348
  const body = await res.json();
@@ -346,7 +352,7 @@ async function fetchTranslations(listName) {
346
352
  const { apiUrl, site } = state.config;
347
353
  const res = await fetch(
348
354
  `${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/_translations?site=${encodeURIComponent(site)}`,
349
- { headers: headers() }
355
+ { headers: headers(), credentials: CREDS }
350
356
  );
351
357
  if (!res.ok) throw new Error(`Cancia: failed to fetch translations (${res.status})`);
352
358
  const body = await res.json();
@@ -356,7 +362,7 @@ async function createListEntry(listName, data, locale, id) {
356
362
  const { apiUrl, site } = state.config;
357
363
  const res = await fetch(
358
364
  `${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
359
- { method: "POST", headers: headers(), body: JSON.stringify({ data, id }) }
365
+ { method: "POST", headers: headers(), credentials: CREDS, body: JSON.stringify({ data, id }) }
360
366
  );
361
367
  if (!res.ok) {
362
368
  throw await toApiError(res, "Could not create the entry. Check your connection and try again.");
@@ -368,7 +374,7 @@ async function updateListEntry(listName, id, data, rev, locale) {
368
374
  const { apiUrl, site } = state.config;
369
375
  const res = await fetch(
370
376
  `${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
371
- { method: "PATCH", headers: headers(), body: JSON.stringify({ data, _rev: rev }) }
377
+ { method: "PATCH", headers: headers(), credentials: CREDS, body: JSON.stringify({ data, _rev: rev }) }
372
378
  );
373
379
  if (!res.ok) {
374
380
  throw await toApiError(res, "Could not save your changes. Check your connection and try again.");
@@ -380,7 +386,7 @@ async function deleteListEntry(listName, id, locale) {
380
386
  const { apiUrl, site } = state.config;
381
387
  const res = await fetch(
382
388
  `${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
383
- { method: "DELETE", headers: headers() }
389
+ { method: "DELETE", headers: headers(), credentials: CREDS }
384
390
  );
385
391
  if (!res.ok) throw new Error(`Cancia: failed to delete entry (${res.status})`);
386
392
  }
@@ -388,9 +394,9 @@ async function reorderList(listName, ids) {
388
394
  const { apiUrl, site } = state.config;
389
395
  const res = await fetch(
390
396
  `${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/reorder?site=${encodeURIComponent(site)}${routeParam()}`,
391
- { method: "POST", headers: headers(), body: JSON.stringify({ ids }) }
397
+ { method: "POST", headers: headers(), credentials: CREDS, body: JSON.stringify({ ids, knownCount: ids.length }) }
392
398
  );
393
- if (!res.ok) throw new Error(`Cancia: failed to reorder list (${res.status})`);
399
+ if (!res.ok) throw await toApiError(res, "Could not save the new order. Check your connection and try again.");
394
400
  }
395
401
  async function flushEdits(edits, save) {
396
402
  const results = await Promise.allSettled(edits.map((edit) => save(edit)));
@@ -659,9 +665,9 @@ ${tokenCss(accent4, useAccent)}
659
665
  `;
660
666
  document.head.appendChild(style);
661
667
  }
662
- function markUi(el) {
663
- el.dataset.canciaUi = "";
664
- return el;
668
+ function markUi(el2) {
669
+ el2.dataset.canciaUi = "";
670
+ return el2;
665
671
  }
666
672
  var surface = (level = 2) => `
667
673
  background: ${v(`surface-${level}`)};
@@ -768,41 +774,41 @@ var groupRow = () => `
768
774
  align-items: flex-start;
769
775
  gap: ${v("space-2")};
770
776
  `;
771
- function attachHover(el, opts = {}) {
777
+ function attachHover(el2, opts = {}) {
772
778
  const bg = opts.bg ?? v("surface-hover");
773
779
  const color = opts.color;
774
- const priorBg = el.style.background;
775
- const priorColor = el.style.color;
776
- el.addEventListener("mouseenter", () => {
777
- el.style.background = bg;
778
- if (color) el.style.color = color;
780
+ const priorBg = el2.style.background;
781
+ const priorColor = el2.style.color;
782
+ el2.addEventListener("mouseenter", () => {
783
+ el2.style.background = bg;
784
+ if (color) el2.style.color = color;
779
785
  });
780
- el.addEventListener("mouseleave", () => {
781
- el.style.background = priorBg;
782
- if (color) el.style.color = priorColor;
786
+ el2.addEventListener("mouseleave", () => {
787
+ el2.style.background = priorBg;
788
+ if (color) el2.style.color = priorColor;
783
789
  });
784
790
  }
785
- function attachPress(el, scale = 0.96) {
791
+ function attachPress(el2, scale = 0.96) {
786
792
  const down = (e) => {
787
- if (el.disabled) return;
788
- el.setPointerCapture?.(e.pointerId);
789
- el.style.transform = `scale(${scale})`;
793
+ if (el2.disabled) return;
794
+ el2.setPointerCapture?.(e.pointerId);
795
+ el2.style.transform = `scale(${scale})`;
790
796
  };
791
797
  const up = () => {
792
- el.style.transform = "";
798
+ el2.style.transform = "";
793
799
  };
794
- el.addEventListener("pointerdown", down);
795
- el.addEventListener("pointerup", up);
796
- el.addEventListener("pointercancel", up);
797
- }
798
- function attachInputFocus(el) {
799
- el.addEventListener("focus", () => {
800
- el.style.borderColor = v("accent-ring");
801
- el.style.background = v("surface-hover");
800
+ el2.addEventListener("pointerdown", down);
801
+ el2.addEventListener("pointerup", up);
802
+ el2.addEventListener("pointercancel", up);
803
+ }
804
+ function attachInputFocus(el2) {
805
+ el2.addEventListener("focus", () => {
806
+ el2.style.borderColor = v("accent-ring");
807
+ el2.style.background = v("surface-hover");
802
808
  });
803
- el.addEventListener("blur", () => {
804
- el.style.borderColor = v("border");
805
- el.style.background = v("surface-raised");
809
+ el2.addEventListener("blur", () => {
810
+ el2.style.borderColor = v("border");
811
+ el2.style.background = v("surface-raised");
806
812
  });
807
813
  }
808
814
 
@@ -832,15 +838,15 @@ function withAlpha(color, alpha) {
832
838
  }
833
839
  return `rgba(24, 24, 27, ${alpha})`;
834
840
  }
835
- function fieldType(el) {
836
- if (el.dataset.cmsType === "image") return "image";
837
- if (el.dataset.cmsType === "link") return "link";
838
- if (el.dataset.cmsType === "richtext") return "richtext";
839
- if (el.tagName === "IMG") return "image";
841
+ function fieldType(el2) {
842
+ if (el2.dataset.cmsType === "image") return "image";
843
+ if (el2.dataset.cmsType === "link") return "link";
844
+ if (el2.dataset.cmsType === "richtext") return "richtext";
845
+ if (el2.tagName === "IMG") return "image";
840
846
  return "text";
841
847
  }
842
- function elementMode(el) {
843
- return el.dataset.cmsList ? "list" : "field";
848
+ function elementMode(el2) {
849
+ return el2.dataset.cmsList ? "list" : "field";
844
850
  }
845
851
  function injectStyles() {
846
852
  if (styleInjected) return;
@@ -948,9 +954,9 @@ var KIND_LABEL = {
948
954
  link: "Link",
949
955
  richtext: "Rich text"
950
956
  };
951
- function positionOverlay(el, animate = false) {
952
- const rect = el.getBoundingClientRect();
953
- const mode = elementMode(el);
957
+ function positionOverlay(el2, animate = false) {
958
+ const rect = el2.getBoundingClientRect();
959
+ const mode = elementMode(el2);
954
960
  const isList = mode === "list";
955
961
  const a = isList ? listAccent(accent()) : accent();
956
962
  const overlay = getOrCreateOverlay();
@@ -959,8 +965,8 @@ function positionOverlay(el, animate = false) {
959
965
  overlay.style.transform = `translate(${rect.left - padding}px, ${rect.top - padding}px)`;
960
966
  overlay.style.width = `${rect.width + padding * 2}px`;
961
967
  overlay.style.height = `${rect.height + padding * 2}px`;
962
- if (lastOverlayEl !== el) {
963
- lastOverlayEl = el;
968
+ if (lastOverlayEl !== el2) {
969
+ lastOverlayEl = el2;
964
970
  if (isList) {
965
971
  overlay.style.border = `1px dashed ${a}`;
966
972
  overlay.style.borderRadius = "6px";
@@ -974,12 +980,12 @@ function positionOverlay(el, animate = false) {
974
980
  if (isList) {
975
981
  tagIcon = LIST_ICON;
976
982
  tagLabel = "List";
977
- tooltipText = `list: ${el.dataset.cmsList}`;
983
+ tooltipText = `list: ${el2.dataset.cmsList}`;
978
984
  } else {
979
- const type = fieldType(el);
985
+ const type = fieldType(el2);
980
986
  tagIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
981
987
  tagLabel = KIND_LABEL[type];
982
- tooltipText = el.dataset.cms ?? "";
988
+ tooltipText = el2.dataset.cms ?? "";
983
989
  }
984
990
  overlay.style.border = `2px solid ${withAlpha(a, isList ? 0.55 : 0.45)}`;
985
991
  overlay.style.background = withAlpha(a, 0.05);
@@ -1062,18 +1068,18 @@ function handleClick(e) {
1062
1068
  }
1063
1069
  function warnUnhoverableRegions() {
1064
1070
  const els = document.querySelectorAll(CMS_SELECTOR);
1065
- els.forEach((el) => {
1066
- const rect = el.getBoundingClientRect();
1071
+ els.forEach((el2) => {
1072
+ const rect = el2.getBoundingClientRect();
1067
1073
  if (rect.width > 0 && rect.height > 0) return;
1068
- const cs = getComputedStyle(el);
1074
+ const cs = getComputedStyle(el2);
1069
1075
  const selfInflicted = cs.display === "contents" || cs.position === "absolute" || cs.clipPath !== "none";
1070
1076
  if (!selfInflicted) return;
1071
- const name = el.dataset.cmsList ? `list "${el.dataset.cmsList}"` : `field "${el.dataset.cms}"`;
1077
+ const name = el2.dataset.cmsList ? `list "${el2.dataset.cmsList}"` : `field "${el2.dataset.cms}"`;
1072
1078
  console.warn(
1073
1079
  `[cancia] ${name} is annotated but has no layout box (${Math.round(rect.width)}\xD7${Math.round(
1074
1080
  rect.height
1075
1081
  )}) \u2014 it cannot be hovered or clicked in the editor. Avoid sr-only / display:contents on CMS regions; give the container a real box.`,
1076
- el
1082
+ el2
1077
1083
  );
1078
1084
  });
1079
1085
  }
@@ -1699,9 +1705,9 @@ function makePrimaryButton(label2, color) {
1699
1705
  attachPress(btn);
1700
1706
  return btn;
1701
1707
  }
1702
- function applyPopupStyles(el) {
1703
- markUi(el);
1704
- el.style.cssText = `
1708
+ function applyPopupStyles(el2) {
1709
+ markUi(el2);
1710
+ el2.style.cssText = `
1705
1711
  ${surface(2)}
1706
1712
  position: absolute;
1707
1713
  z-index: ${v("z-panel")};
@@ -1944,12 +1950,12 @@ function closePopup() {
1944
1950
  keyListener = null;
1945
1951
  }
1946
1952
  if (popupEl) {
1947
- const el = popupEl;
1953
+ const el2 = popupEl;
1948
1954
  popupEl = null;
1949
- el.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}, transform ${v("duration-exit")} ${v("ease-out")}`;
1950
- el.style.opacity = "0";
1951
- el.style.transform = "scale(0.93)";
1952
- setTimeout(() => el.remove(), 200);
1955
+ el2.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}, transform ${v("duration-exit")} ${v("ease-out")}`;
1956
+ el2.style.opacity = "0";
1957
+ el2.style.transform = "scale(0.93)";
1958
+ setTimeout(() => el2.remove(), 200);
1953
1959
  }
1954
1960
  }
1955
1961
 
@@ -2436,15 +2442,15 @@ async function refreshListPanel() {
2436
2442
  function closeListPanel() {
2437
2443
  if (panelEl) {
2438
2444
  panelEl.style.animation = `cancia-panel-out ${v("duration-exit")} ${v("ease-out")} forwards`;
2439
- const el = panelEl;
2440
- setTimeout(() => el.remove(), 180);
2445
+ const el2 = panelEl;
2446
+ setTimeout(() => el2.remove(), 180);
2441
2447
  panelEl = null;
2442
2448
  }
2443
2449
  if (backdropEl) {
2444
- const el = backdropEl;
2445
- el.style.opacity = "0";
2446
- el.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}`;
2447
- setTimeout(() => el.remove(), 180);
2450
+ const el2 = backdropEl;
2451
+ el2.style.opacity = "0";
2452
+ el2.style.transition = `opacity ${v("duration-exit")} ${v("ease-out")}`;
2453
+ setTimeout(() => el2.remove(), 180);
2448
2454
  backdropEl = null;
2449
2455
  }
2450
2456
  currentSchema = null;
@@ -2740,6 +2746,99 @@ function renderField(field, initial, depth = 0) {
2740
2746
  getValue2 = () => sel.value === "" ? void 0 : sel.value;
2741
2747
  break;
2742
2748
  }
2749
+ case "file": {
2750
+ let currentUrl = typeof initial === "string" ? initial : "";
2751
+ const box = document.createElement("div");
2752
+ box.style.cssText = `
2753
+ display: flex; flex-direction: column; gap: 8px;
2754
+ background: ${v("surface-raised")};
2755
+ border: 1px dashed ${v("border-strong")};
2756
+ border-radius: ${v("radius")};
2757
+ padding: 10px;
2758
+ `;
2759
+ const nameRow = document.createElement("div");
2760
+ nameRow.style.cssText = `display:flex; align-items:center; gap:8px; min-width:0;`;
2761
+ const nameLink = document.createElement("a");
2762
+ nameLink.target = "_blank";
2763
+ nameLink.rel = "noopener";
2764
+ nameLink.style.cssText = `
2765
+ font-size: ${v("text-sm")}; color: ${v("fg")}; text-decoration: underline;
2766
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
2767
+ `;
2768
+ const renderName = (url) => {
2769
+ if (!url) {
2770
+ nameLink.removeAttribute("href");
2771
+ nameLink.textContent = "No file attached";
2772
+ nameLink.style.color = v("fg-faint");
2773
+ nameLink.style.textDecoration = "none";
2774
+ return;
2775
+ }
2776
+ let label2 = url;
2777
+ try {
2778
+ label2 = decodeURIComponent(new URL(url, location.href).pathname.split("/").pop() || url);
2779
+ } catch {
2780
+ }
2781
+ nameLink.href = url;
2782
+ nameLink.textContent = label2;
2783
+ nameLink.style.color = v("fg");
2784
+ nameLink.style.textDecoration = "underline";
2785
+ };
2786
+ renderName(currentUrl);
2787
+ nameRow.appendChild(nameLink);
2788
+ box.appendChild(nameRow);
2789
+ const btnRow = document.createElement("div");
2790
+ btnRow.style.cssText = `display:flex; gap:8px; align-items:center;`;
2791
+ const fileInput = document.createElement("input");
2792
+ fileInput.type = "file";
2793
+ fileInput.accept = "application/pdf";
2794
+ fileInput.style.display = "none";
2795
+ const uploadBtn = document.createElement("button");
2796
+ uploadBtn.type = "button";
2797
+ uploadBtn.textContent = "Upload\u2026";
2798
+ uploadBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
2799
+ attachHover(uploadBtn, { bg: v("surface-active") });
2800
+ uploadBtn.addEventListener("click", () => fileInput.click());
2801
+ const clearBtn = document.createElement("button");
2802
+ clearBtn.type = "button";
2803
+ clearBtn.textContent = "Clear";
2804
+ clearBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
2805
+ attachHover(clearBtn, { bg: v("surface-active") });
2806
+ clearBtn.addEventListener("click", () => {
2807
+ currentUrl = "";
2808
+ renderName("");
2809
+ progressEl.textContent = "";
2810
+ });
2811
+ const progressEl = document.createElement("span");
2812
+ progressEl.style.cssText = `font-size: ${v("text-xs")}; color: ${v("fg-muted")};`;
2813
+ fileInput.addEventListener("change", async () => {
2814
+ const f = fileInput.files?.[0];
2815
+ if (!f) return;
2816
+ uploadBtn.disabled = true;
2817
+ try {
2818
+ const url = await uploadImage(f, (pct) => {
2819
+ progressEl.textContent = `Uploading\u2026 ${pct}%`;
2820
+ });
2821
+ currentUrl = url;
2822
+ renderName(url);
2823
+ progressEl.textContent = "Uploaded";
2824
+ } catch (err) {
2825
+ const msg = err instanceof Error && err.message.includes("(413)") ? "That file is too large." : "Upload failed \u2014 try again.";
2826
+ progressEl.textContent = msg;
2827
+ progressEl.style.color = v("danger");
2828
+ } finally {
2829
+ uploadBtn.disabled = false;
2830
+ fileInput.value = "";
2831
+ }
2832
+ });
2833
+ btnRow.appendChild(uploadBtn);
2834
+ btnRow.appendChild(clearBtn);
2835
+ btnRow.appendChild(progressEl);
2836
+ box.appendChild(btnRow);
2837
+ box.appendChild(fileInput);
2838
+ wrapper.appendChild(box);
2839
+ getValue2 = () => currentUrl === "" ? void 0 : currentUrl;
2840
+ break;
2841
+ }
2743
2842
  case "image": {
2744
2843
  const initialUrl = typeof initial === "string" ? initial : "";
2745
2844
  let currentUrl = initialUrl;
@@ -3804,22 +3903,22 @@ function openEntryModal(opts) {
3804
3903
  }
3805
3904
  function closeEntryModal() {
3806
3905
  if (modalEl) {
3807
- const el = modalEl;
3906
+ const el2 = modalEl;
3808
3907
  modalEl = null;
3809
3908
  if (mountedInPanel) {
3810
- popPanelView(el);
3909
+ popPanelView(el2);
3811
3910
  } else {
3812
- el.style.transition = `opacity 0.16s ${v("ease-out")}, transform 0.16s ${v("ease-out")}`;
3813
- el.style.opacity = "0";
3814
- el.style.transform = "translate(-50%, calc(-50% + 8px)) scale(0.985)";
3815
- setTimeout(() => el.remove(), 180);
3911
+ el2.style.transition = `opacity 0.16s ${v("ease-out")}, transform 0.16s ${v("ease-out")}`;
3912
+ el2.style.opacity = "0";
3913
+ el2.style.transform = "translate(-50%, calc(-50% + 8px)) scale(0.985)";
3914
+ setTimeout(() => el2.remove(), 180);
3816
3915
  }
3817
3916
  }
3818
3917
  mountedInPanel = false;
3819
3918
  if (backdropEl2) {
3820
- const el = backdropEl2;
3821
- el.style.opacity = "0";
3822
- setTimeout(() => el.remove(), 180);
3919
+ const el2 = backdropEl2;
3920
+ el2.style.opacity = "0";
3921
+ setTimeout(() => el2.remove(), 180);
3823
3922
  backdropEl2 = null;
3824
3923
  }
3825
3924
  if (escListener) {
@@ -4551,6 +4650,141 @@ function unmountToolbar() {
4551
4650
  }
4552
4651
  }
4553
4652
 
4653
+ // src/login.ts
4654
+ var PANEL_ID = "cancia-login";
4655
+ function el(tag, css, text) {
4656
+ const node = document.createElement(tag);
4657
+ node.style.cssText = css;
4658
+ if (text) node.textContent = text;
4659
+ return node;
4660
+ }
4661
+ function closeLoginPrompt() {
4662
+ document.getElementById(PANEL_ID)?.remove();
4663
+ }
4664
+ function openLoginPrompt() {
4665
+ return new Promise((resolve) => {
4666
+ closeLoginPrompt();
4667
+ const card = el(
4668
+ "div",
4669
+ `position:fixed;bottom:24px;right:24px;z-index:2147483646;width:320px;
4670
+ background:#fff;border:1px solid #e5e5e5;border-radius:12px;
4671
+ box-shadow:0 10px 40px rgba(0,0,0,.15);padding:20px;
4672
+ font:14px/1.5 system-ui,-apple-system,sans-serif;color:#111;`
4673
+ );
4674
+ card.id = PANEL_ID;
4675
+ const title = el("div", "font-weight:600;font-size:15px;margin-bottom:4px;", "Sign in to edit");
4676
+ const blurb = el(
4677
+ "div",
4678
+ "color:#666;font-size:13px;margin-bottom:14px;",
4679
+ "We'll email you a link. No password needed."
4680
+ );
4681
+ const input2 = el(
4682
+ "input",
4683
+ `width:100%;box-sizing:border-box;padding:9px 11px;border:1px solid #d4d4d4;
4684
+ border-radius:8px;font:inherit;margin-bottom:10px;`
4685
+ );
4686
+ input2.type = "email";
4687
+ input2.placeholder = "you@example.com";
4688
+ input2.autocomplete = "email";
4689
+ const button2 = el(
4690
+ "button",
4691
+ `width:100%;padding:9px 12px;border:0;border-radius:8px;background:#111;
4692
+ color:#fff;font:inherit;font-weight:500;cursor:pointer;`,
4693
+ "Email me a link"
4694
+ );
4695
+ const status = el("div", "font-size:13px;margin-top:10px;color:#666;");
4696
+ status.setAttribute("aria-live", "polite");
4697
+ const dismiss = el(
4698
+ "button",
4699
+ `position:absolute;top:10px;right:12px;border:0;background:none;
4700
+ font-size:18px;line-height:1;color:#999;cursor:pointer;padding:2px 6px;`,
4701
+ "\xD7"
4702
+ );
4703
+ dismiss.setAttribute("aria-label", "Close");
4704
+ dismiss.onclick = () => closeLoginPrompt();
4705
+ card.style.position = "fixed";
4706
+ card.append(dismiss, title, blurb, input2, button2, status);
4707
+ document.body.appendChild(card);
4708
+ input2.focus();
4709
+ async function submit() {
4710
+ const email = input2.value.trim();
4711
+ if (!email) {
4712
+ status.textContent = "Enter your email address.";
4713
+ return;
4714
+ }
4715
+ button2.disabled = true;
4716
+ button2.textContent = "Sending\u2026";
4717
+ status.textContent = "";
4718
+ try {
4719
+ const res = await fetch("/api/cancia/auth/request", {
4720
+ method: "POST",
4721
+ headers: { "Content-Type": "application/json" },
4722
+ body: JSON.stringify({ email })
4723
+ });
4724
+ if (res.status === 404) {
4725
+ status.textContent = "Email login is not set up for this site.";
4726
+ button2.disabled = false;
4727
+ button2.textContent = "Email me a link";
4728
+ return;
4729
+ }
4730
+ if (res.status === 429) {
4731
+ status.textContent = "Too many attempts. Wait a minute and try again.";
4732
+ button2.disabled = false;
4733
+ button2.textContent = "Email me a link";
4734
+ return;
4735
+ }
4736
+ input2.remove();
4737
+ button2.remove();
4738
+ blurb.remove();
4739
+ title.textContent = "Check your email";
4740
+ status.textContent = "If that address can edit this site, a link is on its way. It expires in 15 minutes.";
4741
+ status.style.color = "#111";
4742
+ } catch {
4743
+ status.textContent = "Could not reach the server. Check your connection.";
4744
+ button2.disabled = false;
4745
+ button2.textContent = "Email me a link";
4746
+ }
4747
+ }
4748
+ button2.onclick = submit;
4749
+ input2.onkeydown = (e) => {
4750
+ if (e.key === "Enter") submit();
4751
+ };
4752
+ void resolve;
4753
+ });
4754
+ }
4755
+ async function redeemLoginToken() {
4756
+ const params = new URLSearchParams(window.location.search);
4757
+ const token = params.get("cancia_login");
4758
+ if (!token) return false;
4759
+ params.delete("cancia_login");
4760
+ const search = params.toString();
4761
+ window.history.replaceState(
4762
+ null,
4763
+ "",
4764
+ window.location.pathname + (search ? `?${search}` : "") + window.location.hash
4765
+ );
4766
+ try {
4767
+ const res = await fetch("/api/cancia/auth/callback", {
4768
+ method: "POST",
4769
+ headers: { "Content-Type": "application/json" },
4770
+ body: JSON.stringify({ token })
4771
+ });
4772
+ return res.ok;
4773
+ } catch {
4774
+ return false;
4775
+ }
4776
+ }
4777
+ async function hasSession() {
4778
+ try {
4779
+ const res = await fetch("/api/cancia/auth/session", { credentials: "same-origin" });
4780
+ if (!res.ok) return false;
4781
+ const body = await res.json();
4782
+ return body.authenticated === true;
4783
+ } catch {
4784
+ return false;
4785
+ }
4786
+ }
4787
+
4554
4788
  // src/index.ts
4555
4789
  var SESSION_KEY = "cancia_session";
4556
4790
  function getSessionToken() {
@@ -4626,6 +4860,27 @@ async function tryAutoInit() {
4626
4860
  init(window.__CANCIA__);
4627
4861
  return;
4628
4862
  }
4863
+ if (window.__CANCIA__.magicLink) {
4864
+ const justSignedIn = await redeemLoginToken();
4865
+ if (justSignedIn || await hasSession()) {
4866
+ closeLoginPrompt();
4867
+ state.onLogout = async () => {
4868
+ try {
4869
+ await fetch("/api/cancia/auth/logout", {
4870
+ method: "POST",
4871
+ credentials: "include"
4872
+ });
4873
+ } catch {
4874
+ }
4875
+ unmountToolbar();
4876
+ };
4877
+ init(window.__CANCIA__);
4878
+ return;
4879
+ }
4880
+ const wantsLogin = new URLSearchParams(window.location.search).has("cancia_login") || window.location.hash === "#cancia-login";
4881
+ if (wantsLogin) void openLoginPrompt();
4882
+ return;
4883
+ }
4629
4884
  const token = await handleMagicUrl() ?? getSessionToken();
4630
4885
  if (!token) return;
4631
4886
  state.sessionToken = token;