@caretcms/core 0.1.2 → 0.3.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.
Files changed (190) hide show
  1. package/README.md +109 -13
  2. package/dist/contracts.d.ts +1 -0
  3. package/dist/contracts.d.ts.map +1 -1
  4. package/dist/contracts.js +1 -0
  5. package/dist/contracts.js.map +1 -1
  6. package/dist/index.d.ts +31 -9
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +146 -30
  9. package/dist/index.js.map +1 -1
  10. package/dist/loader.d.ts +17 -6
  11. package/dist/loader.d.ts.map +1 -1
  12. package/dist/loader.js +57 -14
  13. package/dist/loader.js.map +1 -1
  14. package/dist/markdown/block-context.d.ts +21 -0
  15. package/dist/markdown/block-context.d.ts.map +1 -0
  16. package/dist/markdown/block-context.js +27 -0
  17. package/dist/markdown/block-context.js.map +1 -0
  18. package/dist/markdown/canonical-body.d.ts +30 -0
  19. package/dist/markdown/canonical-body.d.ts.map +1 -0
  20. package/dist/markdown/canonical-body.js +54 -0
  21. package/dist/markdown/canonical-body.js.map +1 -0
  22. package/dist/markdown/contracts.d.ts +66 -0
  23. package/dist/markdown/contracts.d.ts.map +1 -0
  24. package/dist/markdown/contracts.js +102 -0
  25. package/dist/markdown/contracts.js.map +1 -0
  26. package/dist/markdown/html-to-snodes.d.ts +26 -0
  27. package/dist/markdown/html-to-snodes.d.ts.map +1 -0
  28. package/dist/markdown/html-to-snodes.js +118 -0
  29. package/dist/markdown/html-to-snodes.js.map +1 -0
  30. package/dist/markdown/remark.d.ts +35 -0
  31. package/dist/markdown/remark.d.ts.map +1 -0
  32. package/dist/markdown/remark.js +66 -0
  33. package/dist/markdown/remark.js.map +1 -0
  34. package/dist/markdown/satteri.d.ts +19 -0
  35. package/dist/markdown/satteri.d.ts.map +1 -0
  36. package/dist/markdown/satteri.js +144 -0
  37. package/dist/markdown/satteri.js.map +1 -0
  38. package/dist/markdown/serialize.d.ts +60 -0
  39. package/dist/markdown/serialize.d.ts.map +1 -0
  40. package/dist/markdown/serialize.js +264 -0
  41. package/dist/markdown/serialize.js.map +1 -0
  42. package/dist/markdown/splice.d.ts +32 -0
  43. package/dist/markdown/splice.d.ts.map +1 -0
  44. package/dist/markdown/splice.js +44 -0
  45. package/dist/markdown/splice.js.map +1 -0
  46. package/dist/markdown/stamp.d.ts +68 -0
  47. package/dist/markdown/stamp.d.ts.map +1 -0
  48. package/dist/markdown/stamp.js +131 -0
  49. package/dist/markdown/stamp.js.map +1 -0
  50. package/dist/runtime/auth/rate-limiter.d.ts.map +1 -1
  51. package/dist/runtime/auth/rate-limiter.js +5 -0
  52. package/dist/runtime/auth/rate-limiter.js.map +1 -1
  53. package/dist/runtime/auth/session.d.ts +3 -3
  54. package/dist/runtime/auth/session.d.ts.map +1 -1
  55. package/dist/runtime/auth/session.js +68 -21
  56. package/dist/runtime/auth/session.js.map +1 -1
  57. package/dist/runtime/config.d.ts +3 -0
  58. package/dist/runtime/config.d.ts.map +1 -1
  59. package/dist/runtime/config.js +4 -1
  60. package/dist/runtime/config.js.map +1 -1
  61. package/dist/runtime/content.d.ts.map +1 -1
  62. package/dist/runtime/content.js +4 -2
  63. package/dist/runtime/content.js.map +1 -1
  64. package/dist/runtime/i18n.d.ts +86 -0
  65. package/dist/runtime/i18n.d.ts.map +1 -0
  66. package/dist/runtime/i18n.js +168 -0
  67. package/dist/runtime/i18n.js.map +1 -0
  68. package/dist/runtime/middleware.d.ts.map +1 -1
  69. package/dist/runtime/middleware.js +61 -6
  70. package/dist/runtime/middleware.js.map +1 -1
  71. package/dist/runtime/mutations/contracts.d.ts +28 -1
  72. package/dist/runtime/mutations/contracts.d.ts.map +1 -1
  73. package/dist/runtime/mutations/contracts.js +76 -0
  74. package/dist/runtime/mutations/contracts.js.map +1 -1
  75. package/dist/runtime/mutations/engine.d.ts +6 -1
  76. package/dist/runtime/mutations/engine.d.ts.map +1 -1
  77. package/dist/runtime/mutations/engine.js +271 -51
  78. package/dist/runtime/mutations/engine.js.map +1 -1
  79. package/dist/runtime/providers.d.ts +2 -1
  80. package/dist/runtime/providers.d.ts.map +1 -1
  81. package/dist/runtime/providers.js +7 -1
  82. package/dist/runtime/providers.js.map +1 -1
  83. package/dist/runtime/publish.d.ts +13 -1
  84. package/dist/runtime/publish.d.ts.map +1 -1
  85. package/dist/runtime/publish.js +80 -4
  86. package/dist/runtime/publish.js.map +1 -1
  87. package/dist/runtime/request-context.d.ts +14 -2
  88. package/dist/runtime/request-context.d.ts.map +1 -1
  89. package/dist/runtime/request-context.js +7 -1
  90. package/dist/runtime/request-context.js.map +1 -1
  91. package/dist/runtime/rewrite.d.ts.map +1 -1
  92. package/dist/runtime/rewrite.js +109 -3
  93. package/dist/runtime/rewrite.js.map +1 -1
  94. package/dist/runtime/rich-allowlist.d.ts.map +1 -1
  95. package/dist/runtime/rich-allowlist.js +5 -1
  96. package/dist/runtime/rich-allowlist.js.map +1 -1
  97. package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
  98. package/dist/runtime/routes/admin-entry.js +114 -18
  99. package/dist/runtime/routes/admin-entry.js.map +1 -1
  100. package/dist/runtime/routes/admin.d.ts.map +1 -1
  101. package/dist/runtime/routes/admin.js +33 -15
  102. package/dist/runtime/routes/admin.js.map +1 -1
  103. package/dist/runtime/routes/auth-login.d.ts.map +1 -1
  104. package/dist/runtime/routes/auth-login.js +4 -0
  105. package/dist/runtime/routes/auth-login.js.map +1 -1
  106. package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
  107. package/dist/runtime/routes/auth-logout.js +7 -2
  108. package/dist/runtime/routes/auth-logout.js.map +1 -1
  109. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  110. package/dist/runtime/routes/auth-session.js +4 -2
  111. package/dist/runtime/routes/auth-session.js.map +1 -1
  112. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  113. package/dist/runtime/routes/collection-list.js +64 -41
  114. package/dist/runtime/routes/collection-list.js.map +1 -1
  115. package/dist/runtime/routes/entries.d.ts.map +1 -1
  116. package/dist/runtime/routes/entries.js +16 -6
  117. package/dist/runtime/routes/entries.js.map +1 -1
  118. package/dist/runtime/routes/history.d.ts.map +1 -1
  119. package/dist/runtime/routes/history.js +50 -22
  120. package/dist/runtime/routes/history.js.map +1 -1
  121. package/dist/runtime/routes/mutate.d.ts.map +1 -1
  122. package/dist/runtime/routes/mutate.js +40 -6
  123. package/dist/runtime/routes/mutate.js.map +1 -1
  124. package/dist/runtime/routes/publish.d.ts.map +1 -1
  125. package/dist/runtime/routes/publish.js +5 -2
  126. package/dist/runtime/routes/publish.js.map +1 -1
  127. package/dist/runtime/routes/schema.d.ts.map +1 -1
  128. package/dist/runtime/routes/schema.js +11 -3
  129. package/dist/runtime/routes/schema.js.map +1 -1
  130. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  131. package/dist/runtime/routes/studio-home.js +38 -19
  132. package/dist/runtime/routes/studio-home.js.map +1 -1
  133. package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -1
  134. package/dist/runtime/routes/theme-stylesheet.js +11 -1
  135. package/dist/runtime/routes/theme-stylesheet.js.map +1 -1
  136. package/dist/runtime/schema-registry.d.ts +8 -1
  137. package/dist/runtime/schema-registry.d.ts.map +1 -1
  138. package/dist/runtime/schema-registry.js +47 -3
  139. package/dist/runtime/schema-registry.js.map +1 -1
  140. package/dist/runtime/storage/atomic-write.d.ts.map +1 -1
  141. package/dist/runtime/storage/atomic-write.js +8 -3
  142. package/dist/runtime/storage/atomic-write.js.map +1 -1
  143. package/dist/runtime/storage/markdown-adapter.d.ts +19 -0
  144. package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
  145. package/dist/runtime/storage/markdown-adapter.js +41 -0
  146. package/dist/runtime/storage/markdown-adapter.js.map +1 -1
  147. package/dist/runtime/storage/session-overlay-adapter.d.ts +8 -0
  148. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
  149. package/dist/runtime/storage/session-overlay-adapter.js +24 -2
  150. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
  151. package/dist/runtime/themes/preset-tokens.js +4 -4
  152. package/dist/runtime/themes/preset-tokens.js.map +1 -1
  153. package/dist/runtime/utils.d.ts +10 -0
  154. package/dist/runtime/utils.d.ts.map +1 -1
  155. package/dist/runtime/utils.js +16 -0
  156. package/dist/runtime/utils.js.map +1 -1
  157. package/dist/runtime/views/studio-layout.d.ts +3 -0
  158. package/dist/runtime/views/studio-layout.d.ts.map +1 -1
  159. package/dist/runtime/views/studio-layout.js +36 -13
  160. package/dist/runtime/views/studio-layout.js.map +1 -1
  161. package/dist/schema-utils.d.ts +7 -0
  162. package/dist/schema-utils.d.ts.map +1 -1
  163. package/dist/schema-utils.js +143 -0
  164. package/dist/schema-utils.js.map +1 -1
  165. package/dist/types.d.ts +83 -1
  166. package/dist/types.d.ts.map +1 -1
  167. package/package.json +2 -2
  168. package/static/cms/admin-entry.css +135 -13
  169. package/static/cms/admin-entry.js +509 -86
  170. package/static/cms/editor/guards.js +1 -1
  171. package/static/cms/editor/image-edit.js +42 -11
  172. package/static/cms/editor/image.css +7 -1
  173. package/static/cms/editor/md-block-edit.js +176 -0
  174. package/static/cms/editor/md-serialize.js +301 -0
  175. package/static/cms/editor/panel.css +69 -25
  176. package/static/cms/editor/panel.js +58 -1
  177. package/static/cms/editor/rich-toolbar.js +4 -3
  178. package/static/cms/editor/sanitize.js +2 -1
  179. package/static/cms/editor/sync.js +277 -56
  180. package/static/cms/editor/text-edit.js +68 -17
  181. package/static/cms/editor/text-link-interactions.js +4 -4
  182. package/static/cms/editor/toast.css +114 -2
  183. package/static/cms/editor/toast.js +121 -22
  184. package/static/cms/editor/tokens-and-text.css +73 -3
  185. package/static/cms/editor/toolbar.css +19 -6
  186. package/static/cms/editor/toolbar.js +64 -23
  187. package/static/cms/editor.css +1 -1
  188. package/static/cms/editor.js +14 -3
  189. package/static/cms/studio-tokens.css +4 -4
  190. package/static/cms/studio.css +24 -0
@@ -16,6 +16,11 @@
16
16
  * POST ${api}/upload FormData(file)
17
17
  * GET ${api}/history?collection=&id=
18
18
  * POST ${api}/history { collection, id, ts } // restore snapshot
19
+ *
20
+ * Successful mutations are announced on the same-origin
21
+ * `caretcms:content` BroadcastChannel so an open site preview can refresh
22
+ * automatically. Embedded Studio panels keep using postMessage for their
23
+ * faster in-page field patching.
19
24
  */
20
25
  (function () {
21
26
  "use strict";
@@ -36,6 +41,16 @@
36
41
  var MOUNT = CFG.mountPath;
37
42
  var COLLECTION = CFG.collection;
38
43
  var ID = CFG.id;
44
+ var IS_NEW = CFG.isNew === true;
45
+ var INITIALIZE_IF_MISSING = CFG.initializeIfMissing === true;
46
+ var SAVE_TARGET = CFG.saveTarget === "preview" ? "preview" : "live";
47
+ var MSG = CFG.messages || {};
48
+ function msg(key, fallback) { return typeof MSG[key] === "string" ? MSG[key] : fallback; }
49
+ function htmlEscape(value) {
50
+ return String(value).replace(/[&<>"']/g, function (char) {
51
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char];
52
+ });
53
+ }
39
54
  if (!API || !COLLECTION || !ID) return;
40
55
 
41
56
  /* ─── State ─────────────────────────────────────────────────────── */
@@ -45,6 +60,11 @@
45
60
  var originalJson = "";
46
61
  var saving = false;
47
62
  var dirtyTimer = 0;
63
+ var previewTimer = 0;
64
+ var linkedSelectionTimer = 0;
65
+ var pendingRemoteSelection = null;
66
+ var SYNC_CHANNEL = "caretcms:content";
67
+ var syncChannel = null;
48
68
 
49
69
  /* ─── DOM refs ──────────────────────────────────────────────────── */
50
70
  var loadingEl = document.getElementById("loading");
@@ -53,6 +73,7 @@
53
73
  var fieldsEl = document.getElementById("fields");
54
74
  var titleEl = document.getElementById("entry-title");
55
75
  var statusEl = document.getElementById("status-msg");
76
+ var validationWarningEl = document.getElementById("validation-warning");
56
77
  var saveBtn = document.getElementById("btn-save");
57
78
  var deleteBtn = document.getElementById("btn-delete");
58
79
  var historyBtn = document.getElementById("btn-history");
@@ -67,6 +88,56 @@
67
88
  /* ─── Helpers ───────────────────────────────────────────────────── */
68
89
  function deepClone(v) { return JSON.parse(JSON.stringify(v)); }
69
90
 
91
+ function templateFromSchema(schema) {
92
+ if (!schema || typeof schema !== "object") return "";
93
+ if (schema.default !== undefined) return deepClone(schema.default);
94
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return deepClone(schema.enum[0]);
95
+ if (schema.type === "object") {
96
+ var objectTemplate = {};
97
+ Object.keys(schema.properties || {}).forEach(function (key) {
98
+ objectTemplate[key] = templateFromSchema(schema.properties[key]);
99
+ });
100
+ return objectTemplate;
101
+ }
102
+ if (schema.type === "array") return [];
103
+ if (schema.type === "boolean") return false;
104
+ if (schema.type === "number" || schema.type === "integer") {
105
+ return typeof schema.minimum === "number" ? schema.minimum : 0;
106
+ }
107
+ return "";
108
+ }
109
+
110
+ function generatedId(path, index) {
111
+ var parts = path.split(".").filter(Boolean);
112
+ var key = parts.pop() || "item";
113
+ if (/^\d+$/.test(key)) key = parts.pop() || "item";
114
+ key = key.replace(/ies$/, "y").replace(/s$/, "");
115
+ return (key + "-" + (index + 1) + "-" + Date.now().toString(36))
116
+ .toLowerCase()
117
+ .replace(/[^a-z0-9]+/g, "-")
118
+ .replace(/^-|-$/g, "");
119
+ }
120
+
121
+ function friendlyFileTitle(filename) {
122
+ return String(filename || "Image")
123
+ .replace(/\.[^.]+$/, "")
124
+ .replace(/[-_]+/g, " ")
125
+ .replace(/\s+/g, " ")
126
+ .trim()
127
+ .replace(/\b\w/g, function (c) { return c.toUpperCase(); }) || "Image";
128
+ }
129
+
130
+ function singularItemLabel(path) {
131
+ var key = (path.split(".").filter(Boolean).pop() || "item").toLowerCase();
132
+ if (key === "images") return "image";
133
+ if (key === "details") return "detail";
134
+ if (key === "documents") return "document";
135
+ if (key === "videos") return "video";
136
+ if (key === "links") return "link";
137
+ if (key === "resume") return "résumé item";
138
+ return key.replace(/ies$/, "y").replace(/s$/, "") || "item";
139
+ }
140
+
70
141
  function getTitle(data) {
71
142
  if (!data) return "Untitled";
72
143
  var keys = ["name", "title", "question", "company_name", "headline", "label"];
@@ -89,17 +160,22 @@
89
160
  saveBtn.disabled = !dirty || saving;
90
161
  saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
91
162
  saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
92
- saveBtn.textContent = saving ? "Saving…" : "Save";
163
+ saveBtn.textContent = saving ? msg("common.saving", "Saving…") : (SAVE_TARGET === "preview" ? msg("entry.saveDraft", "Save draft") : msg("entry.saveLive", "Save live"));
93
164
  titleEl.textContent = getTitle(entryData);
165
+ if (entryData !== null) {
166
+ if (saving) showStatus("saving", msg("entry.savingChanges", "Saving changes…"));
167
+ else if (dirty) showStatus("dirty", msg("entry.unsaved", "Unsaved changes"));
168
+ else showStatus("saved", msg("entry.allSaved", "All changes saved"));
169
+ }
94
170
  }
95
171
 
96
172
  function showStatus(type, msg) {
97
173
  statusEl.textContent = msg;
98
174
  statusEl.hidden = false;
99
- statusEl.style.color = type === "saved" ? "var(--studio-green)" : "var(--studio-red)";
100
- if (type === "saved") {
101
- setTimeout(function () { statusEl.hidden = true; }, 2000);
102
- }
175
+ statusEl.dataset.status = type;
176
+ statusEl.style.color = type === "saved"
177
+ ? "var(--studio-green)"
178
+ : (type === "dirty" || type === "saving" ? "var(--studio-text-muted)" : "var(--studio-red)");
103
179
  }
104
180
 
105
181
  function setNestedValue(obj, path, value) {
@@ -128,6 +204,7 @@
128
204
  }
129
205
  clearTimeout(dirtyTimer);
130
206
  dirtyTimer = setTimeout(updateSaveButton, 300);
207
+ queueEmbeddedPreview();
131
208
  }
132
209
 
133
210
  function loginRedirect() {
@@ -139,6 +216,47 @@
139
216
  return false;
140
217
  }
141
218
 
219
+ function queueEmbeddedPreview() {
220
+ if (window.parent === window || !entryData) return;
221
+ clearTimeout(previewTimer);
222
+ previewTimer = setTimeout(function () {
223
+ try {
224
+ window.parent.postMessage({
225
+ type: "cms:preview",
226
+ collection: COLLECTION,
227
+ id: ID,
228
+ data: deepClone(entryData),
229
+ embedded: true,
230
+ studioPath: window.location.pathname + window.location.search,
231
+ }, window.location.origin);
232
+ } catch (e) { /* visual preview is a progressive enhancement */ }
233
+ }, 80);
234
+ }
235
+
236
+ function announceChange(type, data) {
237
+ var message = {
238
+ type: type,
239
+ collection: COLLECTION,
240
+ id: ID,
241
+ data: data ? deepClone(data) : null,
242
+ embedded: window.parent !== window,
243
+ studioPath: window.location.pathname + window.location.search,
244
+ savedAt: Date.now(),
245
+ };
246
+
247
+ if (window.parent !== window) {
248
+ try { window.parent.postMessage(message, window.location.origin); } catch (e) { /* noop */ }
249
+ }
250
+
251
+ if ("BroadcastChannel" in window) {
252
+ try {
253
+ var channel = new BroadcastChannel(SYNC_CHANNEL);
254
+ channel.postMessage(message);
255
+ setTimeout(function () { channel.close(); }, 0);
256
+ } catch (e) { /* automatic preview sync is a progressive enhancement */ }
257
+ }
258
+ }
259
+
142
260
  /* ─── Image compression + upload ────────────────────────────────── */
143
261
  function compressImage(file, maxWidth, quality) {
144
262
  maxWidth = maxWidth || 1600;
@@ -192,9 +310,54 @@
192
310
  }).then(function (json) { return json.url; });
193
311
  }
194
312
 
313
+ function readImageDimensions(file) {
314
+ return createImageBitmap(file).then(function (bitmap) {
315
+ var width = bitmap.width;
316
+ var height = bitmap.height;
317
+ bitmap.close();
318
+ if (width > 1600) {
319
+ height = Math.round((height * 1600) / width);
320
+ width = 1600;
321
+ }
322
+ return { width: width, height: height };
323
+ }).catch(function () { return null; });
324
+ }
325
+
326
+ function populateImageObject(path, file, dimensions) {
327
+ var parts = path.split(".");
328
+ if (parts.length < 2 || parts[parts.length - 1] !== "src") return;
329
+ var parentPath = parts.slice(0, -1).join(".");
330
+ var current = getNestedValue(entryData, parentPath);
331
+ if (!current || typeof current !== "object" || Array.isArray(current)) return;
332
+
333
+ var next = deepClone(current);
334
+ var title = friendlyFileTitle(file.name);
335
+ if (Object.prototype.hasOwnProperty.call(next, "id") && !next.id) {
336
+ next.id = generatedId(parentPath, Number(parts[parts.length - 2]) || 0);
337
+ }
338
+ if (Object.prototype.hasOwnProperty.call(next, "title") && !next.title) next.title = title;
339
+ if (Object.prototype.hasOwnProperty.call(next, "alt") && !next.alt) next.alt = title;
340
+ if (dimensions && Object.prototype.hasOwnProperty.call(next, "width")) next.width = dimensions.width;
341
+ if (dimensions && Object.prototype.hasOwnProperty.call(next, "height")) next.height = dimensions.height;
342
+ updateField(parentPath, next);
343
+
344
+ ["id", "title", "alt", "width", "height"].forEach(function (key) {
345
+ if (!Object.prototype.hasOwnProperty.call(next, key)) return;
346
+ var control = document.getElementById(fieldIdFromPath(parentPath + "." + key));
347
+ if (control && "value" in control) control.value = String(next[key]);
348
+ });
349
+ }
350
+
195
351
  /* ─── Save / Delete ─────────────────────────────────────────────── */
196
352
  function save() {
197
353
  if (!entryData || saving) return;
354
+ if (SAVE_TARGET === "live") {
355
+ var liveConfirmationKey = "caretcms:confirmed-live-save";
356
+ var confirmed = false;
357
+ try { confirmed = sessionStorage.getItem(liveConfirmationKey) === "1"; } catch (e) { /* storage may be disabled */ }
358
+ if (!confirmed && !window.confirm(msg("entry.liveConfirm", "Save these changes directly to the live site?"))) return;
359
+ try { sessionStorage.setItem(liveConfirmationKey, "1"); } catch (e) { /* confirmation still applies to this save */ }
360
+ }
198
361
  saving = true;
199
362
  updateSaveButton();
200
363
  var payload = {
@@ -220,14 +383,17 @@
220
383
  if (!result.ok) {
221
384
  if (result.body && result.body.issues) {
222
385
  showValidationErrors(result.body.issues);
223
- showStatus("error", "Validation failed (" + result.body.issues.length + ")");
386
+ showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.body.issues.length + ")");
224
387
  var firstErr = fieldsEl.querySelector(".studio-field-error");
225
388
  if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
226
389
  return;
227
390
  }
228
391
  if (result.status === 409 && result.body && typeof result.body.currentRevision === "number") {
392
+ // Your edits are still in the form; we've refreshed to the server's
393
+ // revision, so saving again overwrites. Don't tell the user to reload
394
+ // (that would throw their edits away).
229
395
  entryRevision = result.body.currentRevision;
230
- showStatus("error", "Conflictreload to resolve");
396
+ showStatus("error", msg("entry.changedElsewhere", "Changed elsewhere Save again to overwrite"));
231
397
  return;
232
398
  }
233
399
  throw new Error("save failed");
@@ -235,17 +401,10 @@
235
401
  clearFieldErrors();
236
402
  if (result.body && typeof result.body.revision === "number") entryRevision = result.body.revision;
237
403
  originalJson = JSON.stringify(entryData);
238
- if (window.parent !== window) {
239
- try {
240
- window.parent.postMessage(
241
- { type: "cms:saved", collection: COLLECTION, id: ID, data: deepClone(entryData) },
242
- window.location.origin,
243
- );
244
- } catch (e) { /* noop */ }
245
- }
246
- showStatus("saved", "Saved");
404
+ announceChange("cms:saved", entryData);
405
+ showStatus("saved", msg("entry.saved", "Saved"));
247
406
  }).catch(function () {
248
- showStatus("error", "Error");
407
+ showStatus("error", msg("common.error", "Error"));
249
408
  }).then(function () {
250
409
  saving = false;
251
410
  updateSaveButton();
@@ -259,7 +418,7 @@
259
418
 
260
419
  function confirmDelete() {
261
420
  deleteConfirmBtn.disabled = true;
262
- deleteConfirmBtn.textContent = "Deleting…";
421
+ deleteConfirmBtn.textContent = msg("entry.deleting", "Deleting…");
263
422
  var payload = { type: "delete_entry", collection: COLLECTION, id: ID };
264
423
  if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
265
424
  fetch(API + "/mutate", {
@@ -270,12 +429,13 @@
270
429
  }).then(function (res) {
271
430
  if (handleAuth(res)) return;
272
431
  if (!res.ok) throw new Error("delete failed");
432
+ announceChange("cms:deleted", null);
273
433
  window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
274
434
  }).catch(function () {
275
435
  deleteDialog.hidden = true;
276
436
  deleteConfirmBtn.disabled = false;
277
- deleteConfirmBtn.textContent = "Delete";
278
- showStatus("error", "Delete failed");
437
+ deleteConfirmBtn.textContent = msg("entry.delete", "Delete");
438
+ showStatus("error", msg("entry.deleteFailed", "Delete failed"));
279
439
  });
280
440
  }
281
441
 
@@ -315,6 +475,62 @@
315
475
  return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
316
476
  }
317
477
 
478
+ function fieldControl(path) {
479
+ return fieldsEl.querySelector("#" + cssEscape(fieldIdFromPath(path)));
480
+ }
481
+
482
+ function showLinkedSelection(group) {
483
+ window.clearTimeout(linkedSelectionTimer);
484
+ fieldsEl.querySelectorAll(".caret-field-selected").forEach(function (selected) {
485
+ selected.classList.remove("caret-field-selected");
486
+ });
487
+ group.classList.add("caret-field-selected");
488
+ linkedSelectionTimer = window.setTimeout(function () {
489
+ group.classList.remove("caret-field-selected");
490
+ }, 1500);
491
+ }
492
+
493
+ function applyRemoteSelection(message) {
494
+ if (
495
+ !message
496
+ || message.collection !== COLLECTION
497
+ || message.id !== ID
498
+ || typeof message.field !== "string"
499
+ ) return;
500
+
501
+ if (!entryData) {
502
+ pendingRemoteSelection = message;
503
+ return;
504
+ }
505
+
506
+ var control = fieldControl(message.field);
507
+ var group = control && control.closest(".caret-field-group");
508
+ if (!group) return;
509
+ showLinkedSelection(group);
510
+ group.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
511
+ }
512
+
513
+ function announceFieldSelection(path) {
514
+ var message = {
515
+ type: "cms:field-selected",
516
+ collection: COLLECTION,
517
+ id: ID,
518
+ field: path,
519
+ source: "studio",
520
+ };
521
+
522
+ if (window.parent !== window) {
523
+ try {
524
+ window.parent.postMessage(Object.assign({ embedded: true }, message), window.location.origin);
525
+ } catch (e) { /* embedded selection linking is a progressive enhancement */ }
526
+ return;
527
+ }
528
+
529
+ try {
530
+ if (syncChannel) syncChannel.postMessage(message);
531
+ } catch (e) { /* cross-tab selection linking is a progressive enhancement */ }
532
+ }
533
+
318
534
  /* ─── Field group ───────────────────────────────────────────────── */
319
535
  function fieldIdFromPath(path) {
320
536
  return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -341,11 +557,16 @@
341
557
  queueMicrotask(function () {
342
558
  var path = wrapper.dataset.fieldPath;
343
559
  if (!path) return;
344
- var control = wrapper.querySelector("input, select, textarea");
560
+ // Nested object/array groups contain other field groups. Only claim a
561
+ // control whose nearest field-group owner is this wrapper; otherwise an
562
+ // outer `hero` group can overwrite `hero.headline`'s stable identity.
563
+ var control = Array.from(wrapper.querySelectorAll("input, select, textarea")).find(function (candidate) {
564
+ return candidate.closest(".caret-field-group") === wrapper;
565
+ });
345
566
  if (!control) return;
346
567
  var fid = fieldIdFromPath(path);
347
- if (!control.id) control.id = fid;
348
- if (!control.name) control.name = path;
568
+ control.id = fid;
569
+ control.name = path;
349
570
  labelEl.htmlFor = control.id;
350
571
  });
351
572
 
@@ -370,13 +591,7 @@
370
591
 
371
592
  // Backfill missing keys with sensible defaults
372
593
  if (value === undefined && data) {
373
- if (prop.default !== undefined) data[key] = deepClone(prop.default);
374
- else if (prop.type === "object") data[key] = {};
375
- else if (prop.type === "array") data[key] = [];
376
- else if (prop.type === "boolean") data[key] = false;
377
- else if (prop.type === "number") data[key] = 0;
378
- else if (prop.type === "string" && prop.enum) data[key] = prop.enum[0];
379
- else if (prop.type === "string") data[key] = "";
594
+ data[key] = templateFromSchema(prop);
380
595
  }
381
596
  var current = data ? data[key] : undefined;
382
597
 
@@ -400,7 +615,7 @@
400
615
  group = createFieldGroup(label, isRequired);
401
616
  group.dataset.fieldPath = path;
402
617
  group.appendChild(buildSingleImage(current || "", path));
403
- } else if (prop.type === "string" && prop.format === "textarea") {
618
+ } else if (prop.type === "string" && (prop.format === "textarea" || prop.format === "html")) {
404
619
  group = createFieldGroup(label, isRequired);
405
620
  group.dataset.fieldPath = path;
406
621
  var ta = document.createElement("textarea");
@@ -409,7 +624,7 @@
409
624
  ta.value = current || "";
410
625
  ta.addEventListener("input", function () { updateField(path, ta.value); });
411
626
  group.appendChild(ta);
412
- } else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri")) {
627
+ } else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri" || prop.format === "url")) {
413
628
  group = createFieldGroup(label, isRequired);
414
629
  group.dataset.fieldPath = path;
415
630
  var emailInput = document.createElement("input");
@@ -455,7 +670,20 @@
455
670
  toggle.classList.toggle("active");
456
671
  updateField(path, next);
457
672
  });
673
+ toggle.setAttribute("role", "switch");
674
+ toggle.setAttribute("aria-label", label);
675
+ toggle.setAttribute("aria-checked", current ? "true" : "false");
676
+ toggle.addEventListener("click", function () {
677
+ toggle.setAttribute("aria-checked", toggle.classList.contains("active") ? "true" : "false");
678
+ });
458
679
  group.appendChild(toggle);
680
+ if (key === "published") {
681
+ group.classList.add("caret-field-with-help");
682
+ var publishHelp = document.createElement("p");
683
+ publishHelp.className = "caret-field-help";
684
+ publishHelp.textContent = "Turn this on and Save when the entry is ready to appear on the live site.";
685
+ group.appendChild(publishHelp);
686
+ }
459
687
  } else if (prop.type === "array" && prop.format === "image-gallery") {
460
688
  group = createFieldGroup(label, isRequired);
461
689
  group.dataset.fieldPath = path;
@@ -615,7 +843,7 @@
615
843
  item.draggable = true;
616
844
  item.innerHTML =
617
845
  '<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
618
- '<button class="caret-gallery-remove" type="button" title="Remove">×</button>' +
846
+ '<button class="caret-gallery-remove" type="button" title="' + htmlEscape(msg("field.remove", "Remove")) + '">×</button>' +
619
847
  '<span class="caret-gallery-index">' + (idx + 1) + "</span>";
620
848
  item.querySelector(".caret-gallery-remove").addEventListener("click", function (e) {
621
849
  e.stopPropagation();
@@ -660,6 +888,7 @@
660
888
  fileInput.type = "file";
661
889
  fileInput.multiple = true;
662
890
  fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
891
+ fileInput.setAttribute("aria-label", "Upload images for " + humanizeKey(path.split(".").pop() || "images"));
663
892
  fileInput.style.display = "none";
664
893
 
665
894
  function handleFiles(files) {
@@ -704,16 +933,41 @@
704
933
  /* ─── Single image ──────────────────────────────────────────────── */
705
934
  function buildSingleImage(url, path) {
706
935
  var wrapper = document.createElement("div");
936
+ wrapper.className = "caret-single-image";
707
937
 
708
938
  function rerender() {
709
939
  wrapper.innerHTML = "";
710
940
  var current = getNestedValue(entryData, path) || "";
711
941
 
942
+ var fileInput = document.createElement("input");
943
+ fileInput.type = "file";
944
+ fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
945
+ fileInput.setAttribute("aria-label", msg("field.uploadOrReplaceImage", "Upload or replace image"));
946
+ fileInput.style.display = "none";
947
+
712
948
  if (current) {
713
- var preview = document.createElement("div");
949
+ var preview = document.createElement("button");
950
+ preview.type = "button";
714
951
  preview.className = "caret-single-image-preview";
715
- preview.innerHTML = '<img src="' + current + '" alt="" />';
952
+ preview.title = msg("field.replaceImage", "Replace image");
953
+ preview.setAttribute("aria-label", msg("field.replaceImage", "Replace image"));
954
+ var image = document.createElement("img");
955
+ image.src = current;
956
+ image.alt = "Current image preview";
957
+ var overlay = document.createElement("span");
958
+ overlay.className = "caret-single-image-overlay";
959
+ overlay.textContent = msg("field.replaceImage", "Replace image");
960
+ preview.appendChild(image);
961
+ preview.appendChild(overlay);
962
+ preview.addEventListener("click", function () { fileInput.click(); });
716
963
  wrapper.appendChild(preview);
964
+ } else {
965
+ var emptyPreview = document.createElement("button");
966
+ emptyPreview.type = "button";
967
+ emptyPreview.className = "caret-single-image-empty";
968
+ emptyPreview.textContent = "+ Choose an image";
969
+ emptyPreview.addEventListener("click", function () { fileInput.click(); });
970
+ wrapper.appendChild(emptyPreview);
717
971
  }
718
972
 
719
973
  var row = document.createElement("div");
@@ -722,6 +976,8 @@
722
976
  var input = document.createElement("input");
723
977
  input.type = "text";
724
978
  input.className = "studio-input";
979
+ input.id = fieldIdFromPath(path);
980
+ input.name = path;
725
981
  input.style.flex = "1";
726
982
  input.value = current;
727
983
  input.placeholder = "Image URL";
@@ -731,12 +987,13 @@
731
987
  var uploadBtn = document.createElement("button");
732
988
  uploadBtn.type = "button";
733
989
  uploadBtn.className = "studio-btn-ghost";
734
- uploadBtn.textContent = "Upload";
990
+ uploadBtn.textContent = current ? "Replace" : "Upload";
735
991
 
736
- var fileInput = document.createElement("input");
737
- fileInput.type = "file";
738
- fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
739
- fileInput.style.display = "none";
992
+ var help = document.createElement("p");
993
+ help.className = "caret-image-help";
994
+ help.textContent = current
995
+ ? "Click the preview or Replace to upload a new image. You can also paste a URL. For gallery images, blank title and alt fields are filled from the filename—review them before saving."
996
+ : "Upload an image or paste its URL.";
740
997
 
741
998
  uploadBtn.addEventListener("click", function () { fileInput.click(); });
742
999
  fileInput.addEventListener("change", function () {
@@ -744,11 +1001,14 @@
744
1001
  if (!file) return;
745
1002
  uploadBtn.textContent = "Uploading…";
746
1003
  uploadBtn.disabled = true;
747
- uploadFile(file).then(function (newUrl) {
748
- updateField(path, newUrl);
1004
+ Promise.all([uploadFile(file), readImageDimensions(file)]).then(function (results) {
1005
+ updateField(path, results[0]);
1006
+ populateImageObject(path, file, results[1]);
749
1007
  rerender();
750
1008
  }).catch(function () {
751
- uploadBtn.textContent = "Upload";
1009
+ help.textContent = "Upload failed. Check the image type and try again.";
1010
+ help.classList.add("is-error");
1011
+ uploadBtn.textContent = current ? "Replace" : "Upload";
752
1012
  uploadBtn.disabled = false;
753
1013
  });
754
1014
  fileInput.value = "";
@@ -758,6 +1018,7 @@
758
1018
  row.appendChild(uploadBtn);
759
1019
  row.appendChild(fileInput);
760
1020
  wrapper.appendChild(row);
1021
+ wrapper.appendChild(help);
761
1022
  }
762
1023
 
763
1024
  rerender();
@@ -781,7 +1042,7 @@
781
1042
  tag.textContent = item;
782
1043
  var btn = document.createElement("button");
783
1044
  btn.type = "button";
784
- btn.title = "Remove";
1045
+ btn.title = msg("field.remove", "Remove");
785
1046
  btn.textContent = "×";
786
1047
  btn.addEventListener("click", function () {
787
1048
  var next = current.slice();
@@ -833,6 +1094,31 @@
833
1094
  function buildObjectArray(items, path, itemSchema) {
834
1095
  var wrapper = document.createElement("div");
835
1096
  wrapper.className = "caret-object-array";
1097
+ var draggedIndex = null;
1098
+
1099
+ function itemSummary(item, idx) {
1100
+ var summaryKeys = ["title", "label", "name", "alt", "src"];
1101
+ for (var i = 0; i < summaryKeys.length; i++) {
1102
+ var value = item && item[summaryKeys[i]];
1103
+ if (typeof value === "string" && value.trim()) {
1104
+ if (summaryKeys[i] === "src") {
1105
+ var clean = value.split("?")[0].split("/").pop();
1106
+ return clean || (singularItemLabel(path) + " " + (idx + 1));
1107
+ }
1108
+ return value;
1109
+ }
1110
+ }
1111
+ return singularItemLabel(path).replace(/^./, function (c) { return c.toUpperCase(); }) + " " + (idx + 1);
1112
+ }
1113
+
1114
+ function moveItem(current, from, to) {
1115
+ if (to < 0 || to >= current.length || from === to) return;
1116
+ var next = current.slice();
1117
+ var moved = next.splice(from, 1)[0];
1118
+ next.splice(to, 0, moved);
1119
+ updateField(path, next);
1120
+ rerender();
1121
+ }
836
1122
 
837
1123
  function rerender() {
838
1124
  wrapper.innerHTML = "";
@@ -842,10 +1128,81 @@
842
1128
  var card = document.createElement("div");
843
1129
  card.className = "caret-object-array-item";
844
1130
 
1131
+ var header = document.createElement("div");
1132
+ header.className = "caret-object-array-header";
1133
+
1134
+ var dragHandle = document.createElement("button");
1135
+ dragHandle.type = "button";
1136
+ dragHandle.className = "caret-object-array-drag";
1137
+ dragHandle.draggable = true;
1138
+ dragHandle.title = msg("field.drag", "Drag to reorder");
1139
+ dragHandle.setAttribute("aria-label", msg("field.drag", "Drag to reorder") + " " + itemSummary(item, idx));
1140
+ dragHandle.textContent = "⠿";
1141
+ dragHandle.addEventListener("dragstart", function (event) {
1142
+ draggedIndex = idx;
1143
+ card.classList.add("caret-object-array-dragging");
1144
+ if (event.dataTransfer) {
1145
+ event.dataTransfer.effectAllowed = "move";
1146
+ event.dataTransfer.setData("text/plain", String(idx));
1147
+ }
1148
+ });
1149
+ dragHandle.addEventListener("dragend", function () {
1150
+ draggedIndex = null;
1151
+ wrapper.querySelectorAll(".caret-object-array-item").forEach(function (row) {
1152
+ row.classList.remove("caret-object-array-dragging", "caret-object-array-drag-over");
1153
+ });
1154
+ });
1155
+ header.appendChild(dragHandle);
1156
+
1157
+ var idxLabel = document.createElement("span");
1158
+ idxLabel.className = "caret-object-array-index";
1159
+ idxLabel.textContent = itemSummary(item, idx);
1160
+ header.appendChild(idxLabel);
1161
+
1162
+ card.addEventListener("dragover", function (event) {
1163
+ if (draggedIndex === null || draggedIndex === idx) return;
1164
+ event.preventDefault();
1165
+ card.classList.add("caret-object-array-drag-over");
1166
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
1167
+ });
1168
+ card.addEventListener("dragleave", function () {
1169
+ card.classList.remove("caret-object-array-drag-over");
1170
+ });
1171
+ card.addEventListener("drop", function (event) {
1172
+ event.preventDefault();
1173
+ card.classList.remove("caret-object-array-drag-over");
1174
+ if (draggedIndex === null) return;
1175
+ var from = draggedIndex;
1176
+ draggedIndex = null;
1177
+ moveItem(current, from, idx);
1178
+ });
1179
+
1180
+ var actions = document.createElement("div");
1181
+ actions.className = "caret-object-array-actions";
1182
+
1183
+ var upBtn = document.createElement("button");
1184
+ upBtn.type = "button";
1185
+ upBtn.className = "caret-object-array-move";
1186
+ upBtn.title = msg("field.moveUp", "Move up");
1187
+ upBtn.setAttribute("aria-label", msg("field.moveUp", "Move up") + " " + itemSummary(item, idx));
1188
+ upBtn.textContent = "↑";
1189
+ upBtn.disabled = idx === 0;
1190
+ upBtn.addEventListener("click", function () { moveItem(current, idx, idx - 1); });
1191
+
1192
+ var downBtn = document.createElement("button");
1193
+ downBtn.type = "button";
1194
+ downBtn.className = "caret-object-array-move";
1195
+ downBtn.title = msg("field.moveDown", "Move down");
1196
+ downBtn.setAttribute("aria-label", msg("field.moveDown", "Move down") + " " + itemSummary(item, idx));
1197
+ downBtn.textContent = "↓";
1198
+ downBtn.disabled = idx === current.length - 1;
1199
+ downBtn.addEventListener("click", function () { moveItem(current, idx, idx + 1); });
1200
+
845
1201
  var removeBtn = document.createElement("button");
846
1202
  removeBtn.type = "button";
847
1203
  removeBtn.className = "caret-object-array-remove";
848
- removeBtn.title = "Remove item";
1204
+ removeBtn.title = msg("field.remove", "Remove") + " " + singularItemLabel(path);
1205
+ removeBtn.setAttribute("aria-label", msg("field.remove", "Remove") + " " + itemSummary(item, idx));
849
1206
  removeBtn.textContent = "×";
850
1207
  removeBtn.addEventListener("click", function () {
851
1208
  var next = current.slice();
@@ -853,40 +1210,21 @@
853
1210
  updateField(path, next);
854
1211
  rerender();
855
1212
  });
856
- card.appendChild(removeBtn);
857
1213
 
858
- var idxLabel = document.createElement("span");
859
- idxLabel.className = "caret-object-array-index";
860
- idxLabel.textContent = "[" + idx + "]";
861
- card.appendChild(idxLabel);
1214
+ actions.appendChild(upBtn);
1215
+ actions.appendChild(downBtn);
1216
+ actions.appendChild(removeBtn);
1217
+ header.appendChild(actions);
1218
+ card.appendChild(header);
862
1219
 
863
1220
  var fieldsCont = document.createElement("div");
864
1221
  fieldsCont.className = "caret-object-array-fields";
865
1222
 
866
- var keys = (itemSchema && itemSchema.properties) ? Object.keys(itemSchema.properties) : Object.keys(item);
867
- keys.forEach(function (key) {
868
- var itemPath = path + "." + idx + "." + key;
869
- var val = item[key];
870
- var propSchema = itemSchema && itemSchema.properties && itemSchema.properties[key];
871
- var fieldLabel = (propSchema && propSchema.title) || humanizeKey(key);
872
-
873
- var row = document.createElement("div");
874
- row.className = "caret-object-array-row";
875
-
876
- var keyLabel = document.createElement("span");
877
- keyLabel.className = "caret-object-array-key";
878
- keyLabel.textContent = fieldLabel;
879
-
880
- var input = document.createElement("input");
881
- input.type = "text";
882
- input.className = "studio-input";
883
- input.value = typeof val === "string" ? val : (val == null ? "" : JSON.stringify(val));
884
- input.addEventListener("input", function () { updateField(itemPath, input.value); });
885
-
886
- row.appendChild(keyLabel);
887
- row.appendChild(input);
888
- fieldsCont.appendChild(row);
889
- });
1223
+ if (itemSchema && itemSchema.properties) {
1224
+ renderFields(fieldsCont, item, itemSchema, path + "." + idx);
1225
+ } else {
1226
+ renderFieldsFallback(fieldsCont, item, path + "." + idx);
1227
+ }
890
1228
 
891
1229
  card.appendChild(fieldsCont);
892
1230
  wrapper.appendChild(card);
@@ -895,22 +1233,34 @@
895
1233
  var addBtn = document.createElement("button");
896
1234
  addBtn.type = "button";
897
1235
  addBtn.className = "caret-object-array-add";
898
- addBtn.textContent = "+ Add item";
1236
+ addBtn.textContent = "+ " + msg("field.add", "Add") + " " + singularItemLabel(path);
899
1237
  addBtn.addEventListener("click", function () {
900
1238
  var template;
901
1239
  if (itemSchema && itemSchema.properties) {
902
- template = {};
903
- Object.keys(itemSchema.properties).forEach(function (k) {
904
- var s = itemSchema.properties[k];
905
- template[k] = s.type === "number" || s.type === "integer" ? 0 : (s.type === "boolean" ? false : "");
906
- });
1240
+ template = templateFromSchema(itemSchema);
1241
+ if (Object.prototype.hasOwnProperty.call(template, "id") && !template.id) {
1242
+ template.id = generatedId(path, current.length);
1243
+ }
907
1244
  } else {
908
1245
  template = current.length > 0
909
- ? Object.fromEntries(Object.keys(current[0]).map(function (k) { return [k, ""]; }))
1246
+ ? Object.fromEntries(Object.keys(current[0]).map(function (k) {
1247
+ var sample = current[0][k];
1248
+ return [k, typeof sample === "number" ? 0 : (typeof sample === "boolean" ? false : (Array.isArray(sample) ? [] : (sample && typeof sample === "object" ? {} : "")))];
1249
+ }))
910
1250
  : {};
911
1251
  }
912
1252
  updateField(path, current.concat([template]));
913
1253
  rerender();
1254
+ var cards = wrapper.querySelectorAll(".caret-object-array-item");
1255
+ var newest = cards[cards.length - 1];
1256
+ var firstControl = newest && newest.querySelector("input, textarea, select, button.caret-single-image-empty");
1257
+ // Focus immediately. Deferring focus to the next animation frame can
1258
+ // steal it back after a fast user (or assistive automation) has already
1259
+ // moved to another control in the new row.
1260
+ if (firstControl) firstControl.focus();
1261
+ requestAnimationFrame(function () {
1262
+ if (newest) newest.scrollIntoView({ behavior: "smooth", block: "nearest" });
1263
+ });
914
1264
  });
915
1265
  wrapper.appendChild(addBtn);
916
1266
  }
@@ -958,6 +1308,12 @@
958
1308
 
959
1309
  info.appendChild(actionSpan);
960
1310
  info.appendChild(timeSpan);
1311
+ if (item.editor && typeof item.editor.id === "string") {
1312
+ var editorSpan = document.createElement("span");
1313
+ editorSpan.style.cssText = "font-size:0.7rem;color:var(--studio-text-dim);";
1314
+ editorSpan.textContent = item.editor.name || item.editor.email || item.editor.id;
1315
+ info.appendChild(editorSpan);
1316
+ }
961
1317
  row.appendChild(info);
962
1318
 
963
1319
  var restoreBtn = document.createElement("button");
@@ -1006,6 +1362,7 @@
1006
1362
  return;
1007
1363
  }
1008
1364
  historyPanel.hidden = true;
1365
+ announceChange("cms:saved", entryData);
1009
1366
  showStatus("saved", "Restored");
1010
1367
  }).catch(function () {
1011
1368
  showStatus("error", "Restore failed");
@@ -1014,7 +1371,7 @@
1014
1371
 
1015
1372
  /* ─── Wire events ───────────────────────────────────────────────── */
1016
1373
  saveBtn.addEventListener("click", save);
1017
- deleteBtn.addEventListener("click", deleteEntry);
1374
+ if (deleteBtn) deleteBtn.addEventListener("click", deleteEntry);
1018
1375
  historyBtn.addEventListener("click", loadHistory);
1019
1376
  historyCloseBtn.addEventListener("click", function () {
1020
1377
  historyPanel.hidden = true;
@@ -1026,6 +1383,41 @@
1026
1383
  if (e.target === deleteDialog) deleteDialog.hidden = true;
1027
1384
  });
1028
1385
 
1386
+ window.addEventListener("message", function (event) {
1387
+ if (event.origin !== window.location.origin) return;
1388
+ if (event.data && event.data.type === "cms:field-selected") {
1389
+ applyRemoteSelection(event.data);
1390
+ }
1391
+ });
1392
+
1393
+ // Focusing a Studio control locates its live-page counterpart without
1394
+ // stealing focus from the editor. The temporary blue cue is distinct from
1395
+ // green save confirmation.
1396
+ fieldsEl.addEventListener("focusin", function (event) {
1397
+ var control = event.target instanceof Element
1398
+ ? event.target.closest("input, select, textarea, button")
1399
+ : null;
1400
+ var group = control && control.closest("[data-field-path]");
1401
+ var path = group && group.dataset.fieldPath;
1402
+ if (!path) return;
1403
+ showLinkedSelection(group);
1404
+ announceFieldSelection(path);
1405
+ });
1406
+
1407
+ if ("BroadcastChannel" in window) {
1408
+ try {
1409
+ syncChannel = new BroadcastChannel(SYNC_CHANNEL);
1410
+ syncChannel.addEventListener("message", function (event) {
1411
+ if (event.data && event.data.type === "cms:field-selected") {
1412
+ applyRemoteSelection(event.data);
1413
+ }
1414
+ });
1415
+ window.addEventListener("pagehide", function () { syncChannel.close(); }, { once: true });
1416
+ } catch (e) {
1417
+ syncChannel = null;
1418
+ }
1419
+ }
1420
+
1029
1421
  window.addEventListener("keydown", function (e) {
1030
1422
  if ((e.metaKey || e.ctrlKey) && e.key === "s") {
1031
1423
  e.preventDefault();
@@ -1063,17 +1455,48 @@
1063
1455
  if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
1064
1456
 
1065
1457
  loadingEl.hidden = true;
1066
- if (!entry) {
1458
+ var initializedMissingEntry = !entry && (IS_NEW || INITIALIZE_IF_MISSING);
1459
+ if (!entry && !initializedMissingEntry) {
1067
1460
  notFoundEl.hidden = false;
1068
1461
  return;
1069
1462
  }
1070
- entryData = deepClone(entry.data);
1071
- entryRevision = typeof entry.revision === "number" ? entry.revision : undefined;
1072
- originalJson = JSON.stringify(entry.data);
1463
+ var initialData = entry
1464
+ ? entry.data
1465
+ : (schemaJson && schemaJson.template) || templateFromSchema(entrySchema);
1466
+ entryData = deepClone(initialData);
1467
+ entryRevision = entry && typeof entry.revision === "number" ? entry.revision : 0;
1468
+ originalJson = JSON.stringify(initialData);
1073
1469
  titleEl.textContent = getTitle(entryData);
1074
1470
  renderFields(fieldsEl, entryData, entrySchema, "");
1471
+ if (validationWarningEl && entry && Array.isArray(entry.validationIssues) && entry.validationIssues.length > 0) {
1472
+ validationWarningEl.textContent = "This stored entry has invalid fields: " + entry.validationIssues.map(function (issue) {
1473
+ return (issue.path || "entry") + " — " + issue.message;
1474
+ }).join("; ") + ". Correct them and Save to restore public delivery.";
1475
+ validationWarningEl.hidden = false;
1476
+ }
1075
1477
  updateSaveButton();
1478
+ if (IS_NEW || initializedMissingEntry) {
1479
+ var guideTitle = document.getElementById("editor-guide-title");
1480
+ var guideCopy = document.getElementById("editor-guide-copy");
1481
+ if (guideTitle) guideTitle.textContent = msg("entry.newGuideTitle", "Entry created with safe defaults");
1482
+ if (guideCopy) guideCopy.textContent = msg("entry.newGuideCopy", "Fill in the fields below. Save becomes available after your first change. In the sidebar Studio, text and image edits preview on the page as you work; Save confirms them and refreshes the visual preview for structural changes. The Published switch controls whether signed-out visitors can see this entry.");
1483
+ }
1076
1484
  editorEl.hidden = false;
1485
+ if (window.parent !== window) {
1486
+ try {
1487
+ window.parent.postMessage({
1488
+ type: "cms:entry-ready",
1489
+ collection: COLLECTION,
1490
+ id: ID,
1491
+ embedded: true,
1492
+ }, window.location.origin);
1493
+ } catch (e) { /* embedded field linking is a progressive enhancement */ }
1494
+ }
1495
+ if (pendingRemoteSelection) {
1496
+ var pending = pendingRemoteSelection;
1497
+ pendingRemoteSelection = null;
1498
+ applyRemoteSelection(pending);
1499
+ }
1077
1500
  });
1078
1501
  });
1079
1502
  }).catch(function () {