@caretcms/core 0.2.0 → 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 (101) hide show
  1. package/README.md +81 -3
  2. package/dist/index.d.ts +13 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +37 -9
  5. package/dist/index.js.map +1 -1
  6. package/dist/loader.d.ts.map +1 -1
  7. package/dist/loader.js +27 -3
  8. package/dist/loader.js.map +1 -1
  9. package/dist/markdown/satteri.d.ts.map +1 -1
  10. package/dist/markdown/satteri.js +32 -8
  11. package/dist/markdown/satteri.js.map +1 -1
  12. package/dist/runtime/auth/session.d.ts +1 -0
  13. package/dist/runtime/auth/session.d.ts.map +1 -1
  14. package/dist/runtime/auth/session.js +16 -0
  15. package/dist/runtime/auth/session.js.map +1 -1
  16. package/dist/runtime/config.d.ts +3 -0
  17. package/dist/runtime/config.d.ts.map +1 -1
  18. package/dist/runtime/config.js +4 -1
  19. package/dist/runtime/config.js.map +1 -1
  20. package/dist/runtime/i18n.d.ts +86 -0
  21. package/dist/runtime/i18n.d.ts.map +1 -0
  22. package/dist/runtime/i18n.js +168 -0
  23. package/dist/runtime/i18n.js.map +1 -0
  24. package/dist/runtime/middleware.d.ts.map +1 -1
  25. package/dist/runtime/middleware.js +40 -2
  26. package/dist/runtime/middleware.js.map +1 -1
  27. package/dist/runtime/mutations/contracts.d.ts +3 -0
  28. package/dist/runtime/mutations/contracts.d.ts.map +1 -1
  29. package/dist/runtime/mutations/contracts.js +9 -0
  30. package/dist/runtime/mutations/contracts.js.map +1 -1
  31. package/dist/runtime/mutations/engine.d.ts.map +1 -1
  32. package/dist/runtime/mutations/engine.js +121 -10
  33. package/dist/runtime/mutations/engine.js.map +1 -1
  34. package/dist/runtime/providers.d.ts +2 -1
  35. package/dist/runtime/providers.d.ts.map +1 -1
  36. package/dist/runtime/providers.js +7 -1
  37. package/dist/runtime/providers.js.map +1 -1
  38. package/dist/runtime/publish.d.ts.map +1 -1
  39. package/dist/runtime/publish.js +5 -0
  40. package/dist/runtime/publish.js.map +1 -1
  41. package/dist/runtime/request-context.d.ts +5 -1
  42. package/dist/runtime/request-context.d.ts.map +1 -1
  43. package/dist/runtime/request-context.js +7 -1
  44. package/dist/runtime/request-context.js.map +1 -1
  45. package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
  46. package/dist/runtime/routes/admin-entry.js +114 -18
  47. package/dist/runtime/routes/admin-entry.js.map +1 -1
  48. package/dist/runtime/routes/admin.d.ts.map +1 -1
  49. package/dist/runtime/routes/admin.js +28 -14
  50. package/dist/runtime/routes/admin.js.map +1 -1
  51. package/dist/runtime/routes/auth-login.d.ts.map +1 -1
  52. package/dist/runtime/routes/auth-login.js +4 -0
  53. package/dist/runtime/routes/auth-login.js.map +1 -1
  54. package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
  55. package/dist/runtime/routes/auth-logout.js +7 -2
  56. package/dist/runtime/routes/auth-logout.js.map +1 -1
  57. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  58. package/dist/runtime/routes/auth-session.js +4 -2
  59. package/dist/runtime/routes/auth-session.js.map +1 -1
  60. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  61. package/dist/runtime/routes/collection-list.js +64 -41
  62. package/dist/runtime/routes/collection-list.js.map +1 -1
  63. package/dist/runtime/routes/entries.d.ts.map +1 -1
  64. package/dist/runtime/routes/entries.js +15 -6
  65. package/dist/runtime/routes/entries.js.map +1 -1
  66. package/dist/runtime/routes/history.d.ts.map +1 -1
  67. package/dist/runtime/routes/history.js +4 -1
  68. package/dist/runtime/routes/history.js.map +1 -1
  69. package/dist/runtime/routes/schema.d.ts.map +1 -1
  70. package/dist/runtime/routes/schema.js +7 -2
  71. package/dist/runtime/routes/schema.js.map +1 -1
  72. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  73. package/dist/runtime/routes/studio-home.js +38 -19
  74. package/dist/runtime/routes/studio-home.js.map +1 -1
  75. package/dist/runtime/schema-registry.d.ts +8 -1
  76. package/dist/runtime/schema-registry.d.ts.map +1 -1
  77. package/dist/runtime/schema-registry.js +47 -3
  78. package/dist/runtime/schema-registry.js.map +1 -1
  79. package/dist/runtime/themes/preset-tokens.js +4 -4
  80. package/dist/runtime/themes/preset-tokens.js.map +1 -1
  81. package/dist/runtime/views/studio-layout.d.ts +3 -0
  82. package/dist/runtime/views/studio-layout.d.ts.map +1 -1
  83. package/dist/runtime/views/studio-layout.js +36 -13
  84. package/dist/runtime/views/studio-layout.js.map +1 -1
  85. package/dist/schema-utils.d.ts +7 -0
  86. package/dist/schema-utils.d.ts.map +1 -1
  87. package/dist/schema-utils.js +143 -0
  88. package/dist/schema-utils.js.map +1 -1
  89. package/dist/types.d.ts +43 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +1 -1
  92. package/static/cms/admin-entry.css +135 -13
  93. package/static/cms/admin-entry.js +506 -117
  94. package/static/cms/editor/image-edit.js +1 -0
  95. package/static/cms/editor/panel.js +19 -1
  96. package/static/cms/editor/sync.js +277 -56
  97. package/static/cms/editor/tokens-and-text.css +9 -0
  98. package/static/cms/editor.css +1 -1
  99. package/static/cms/editor.js +1 -1
  100. package/static/cms/studio-tokens.css +4 -4
  101. 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"];
@@ -80,31 +151,6 @@
80
151
  return key.replace(/_/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
81
152
  }
82
153
 
83
- // Coerce a text-input value back to the field's declared (or original) type so
84
- // editing an object-array's numeric/boolean/nested field doesn't turn it into a
85
- // string. Unparseable input returns the raw string (server validation reports it).
86
- function coerceScalarValue(raw, propSchema, originalVal) {
87
- var type = (propSchema && propSchema.type) ||
88
- (typeof originalVal === "number" ? "number" :
89
- typeof originalVal === "boolean" ? "boolean" :
90
- (originalVal && typeof originalVal === "object" ? "object" : "string"));
91
- if (type === "number" || type === "integer") {
92
- if (raw.trim() === "") return null;
93
- var n = Number(raw);
94
- return Number.isFinite(n) ? n : raw;
95
- }
96
- if (type === "boolean") {
97
- var t = raw.trim().toLowerCase();
98
- if (t === "true") return true;
99
- if (t === "false") return false;
100
- return raw;
101
- }
102
- if (type === "object" || type === "array") {
103
- try { return JSON.parse(raw); } catch (e) { return raw; }
104
- }
105
- return raw;
106
- }
107
-
108
154
  function isDirty() {
109
155
  return entryData !== null && JSON.stringify(entryData) !== originalJson;
110
156
  }
@@ -114,17 +160,22 @@
114
160
  saveBtn.disabled = !dirty || saving;
115
161
  saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
116
162
  saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
117
- 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"));
118
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
+ }
119
170
  }
120
171
 
121
172
  function showStatus(type, msg) {
122
173
  statusEl.textContent = msg;
123
174
  statusEl.hidden = false;
124
- statusEl.style.color = type === "saved" ? "var(--studio-green)" : "var(--studio-red)";
125
- if (type === "saved") {
126
- setTimeout(function () { statusEl.hidden = true; }, 2000);
127
- }
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)");
128
179
  }
129
180
 
130
181
  function setNestedValue(obj, path, value) {
@@ -153,6 +204,7 @@
153
204
  }
154
205
  clearTimeout(dirtyTimer);
155
206
  dirtyTimer = setTimeout(updateSaveButton, 300);
207
+ queueEmbeddedPreview();
156
208
  }
157
209
 
158
210
  function loginRedirect() {
@@ -164,6 +216,47 @@
164
216
  return false;
165
217
  }
166
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
+
167
260
  /* ─── Image compression + upload ────────────────────────────────── */
168
261
  function compressImage(file, maxWidth, quality) {
169
262
  maxWidth = maxWidth || 1600;
@@ -217,9 +310,54 @@
217
310
  }).then(function (json) { return json.url; });
218
311
  }
219
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
+
220
351
  /* ─── Save / Delete ─────────────────────────────────────────────── */
221
352
  function save() {
222
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
+ }
223
361
  saving = true;
224
362
  updateSaveButton();
225
363
  var payload = {
@@ -245,7 +383,7 @@
245
383
  if (!result.ok) {
246
384
  if (result.body && result.body.issues) {
247
385
  showValidationErrors(result.body.issues);
248
- showStatus("error", "Validation failed (" + result.body.issues.length + ")");
386
+ showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.body.issues.length + ")");
249
387
  var firstErr = fieldsEl.querySelector(".studio-field-error");
250
388
  if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
251
389
  return;
@@ -255,7 +393,7 @@
255
393
  // revision, so saving again overwrites. Don't tell the user to reload
256
394
  // (that would throw their edits away).
257
395
  entryRevision = result.body.currentRevision;
258
- showStatus("error", "Changed elsewhere — Save again to overwrite");
396
+ showStatus("error", msg("entry.changedElsewhere", "Changed elsewhere — Save again to overwrite"));
259
397
  return;
260
398
  }
261
399
  throw new Error("save failed");
@@ -263,17 +401,10 @@
263
401
  clearFieldErrors();
264
402
  if (result.body && typeof result.body.revision === "number") entryRevision = result.body.revision;
265
403
  originalJson = JSON.stringify(entryData);
266
- if (window.parent !== window) {
267
- try {
268
- window.parent.postMessage(
269
- { type: "cms:saved", collection: COLLECTION, id: ID, data: deepClone(entryData) },
270
- window.location.origin,
271
- );
272
- } catch (e) { /* noop */ }
273
- }
274
- showStatus("saved", "Saved");
404
+ announceChange("cms:saved", entryData);
405
+ showStatus("saved", msg("entry.saved", "Saved"));
275
406
  }).catch(function () {
276
- showStatus("error", "Error");
407
+ showStatus("error", msg("common.error", "Error"));
277
408
  }).then(function () {
278
409
  saving = false;
279
410
  updateSaveButton();
@@ -287,7 +418,7 @@
287
418
 
288
419
  function confirmDelete() {
289
420
  deleteConfirmBtn.disabled = true;
290
- deleteConfirmBtn.textContent = "Deleting…";
421
+ deleteConfirmBtn.textContent = msg("entry.deleting", "Deleting…");
291
422
  var payload = { type: "delete_entry", collection: COLLECTION, id: ID };
292
423
  if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
293
424
  fetch(API + "/mutate", {
@@ -298,12 +429,13 @@
298
429
  }).then(function (res) {
299
430
  if (handleAuth(res)) return;
300
431
  if (!res.ok) throw new Error("delete failed");
432
+ announceChange("cms:deleted", null);
301
433
  window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
302
434
  }).catch(function () {
303
435
  deleteDialog.hidden = true;
304
436
  deleteConfirmBtn.disabled = false;
305
- deleteConfirmBtn.textContent = "Delete";
306
- showStatus("error", "Delete failed");
437
+ deleteConfirmBtn.textContent = msg("entry.delete", "Delete");
438
+ showStatus("error", msg("entry.deleteFailed", "Delete failed"));
307
439
  });
308
440
  }
309
441
 
@@ -343,6 +475,62 @@
343
475
  return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
344
476
  }
345
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
+
346
534
  /* ─── Field group ───────────────────────────────────────────────── */
347
535
  function fieldIdFromPath(path) {
348
536
  return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -369,11 +557,16 @@
369
557
  queueMicrotask(function () {
370
558
  var path = wrapper.dataset.fieldPath;
371
559
  if (!path) return;
372
- 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
+ });
373
566
  if (!control) return;
374
567
  var fid = fieldIdFromPath(path);
375
- if (!control.id) control.id = fid;
376
- if (!control.name) control.name = path;
568
+ control.id = fid;
569
+ control.name = path;
377
570
  labelEl.htmlFor = control.id;
378
571
  });
379
572
 
@@ -398,13 +591,7 @@
398
591
 
399
592
  // Backfill missing keys with sensible defaults
400
593
  if (value === undefined && data) {
401
- if (prop.default !== undefined) data[key] = deepClone(prop.default);
402
- else if (prop.type === "object") data[key] = {};
403
- else if (prop.type === "array") data[key] = [];
404
- else if (prop.type === "boolean") data[key] = false;
405
- else if (prop.type === "number") data[key] = 0;
406
- else if (prop.type === "string" && prop.enum) data[key] = prop.enum[0];
407
- else if (prop.type === "string") data[key] = "";
594
+ data[key] = templateFromSchema(prop);
408
595
  }
409
596
  var current = data ? data[key] : undefined;
410
597
 
@@ -428,7 +615,7 @@
428
615
  group = createFieldGroup(label, isRequired);
429
616
  group.dataset.fieldPath = path;
430
617
  group.appendChild(buildSingleImage(current || "", path));
431
- } else if (prop.type === "string" && prop.format === "textarea") {
618
+ } else if (prop.type === "string" && (prop.format === "textarea" || prop.format === "html")) {
432
619
  group = createFieldGroup(label, isRequired);
433
620
  group.dataset.fieldPath = path;
434
621
  var ta = document.createElement("textarea");
@@ -437,7 +624,7 @@
437
624
  ta.value = current || "";
438
625
  ta.addEventListener("input", function () { updateField(path, ta.value); });
439
626
  group.appendChild(ta);
440
- } 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")) {
441
628
  group = createFieldGroup(label, isRequired);
442
629
  group.dataset.fieldPath = path;
443
630
  var emailInput = document.createElement("input");
@@ -483,7 +670,20 @@
483
670
  toggle.classList.toggle("active");
484
671
  updateField(path, next);
485
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
+ });
486
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
+ }
487
687
  } else if (prop.type === "array" && prop.format === "image-gallery") {
488
688
  group = createFieldGroup(label, isRequired);
489
689
  group.dataset.fieldPath = path;
@@ -643,7 +843,7 @@
643
843
  item.draggable = true;
644
844
  item.innerHTML =
645
845
  '<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
646
- '<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>' +
647
847
  '<span class="caret-gallery-index">' + (idx + 1) + "</span>";
648
848
  item.querySelector(".caret-gallery-remove").addEventListener("click", function (e) {
649
849
  e.stopPropagation();
@@ -688,6 +888,7 @@
688
888
  fileInput.type = "file";
689
889
  fileInput.multiple = true;
690
890
  fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
891
+ fileInput.setAttribute("aria-label", "Upload images for " + humanizeKey(path.split(".").pop() || "images"));
691
892
  fileInput.style.display = "none";
692
893
 
693
894
  function handleFiles(files) {
@@ -732,16 +933,41 @@
732
933
  /* ─── Single image ──────────────────────────────────────────────── */
733
934
  function buildSingleImage(url, path) {
734
935
  var wrapper = document.createElement("div");
936
+ wrapper.className = "caret-single-image";
735
937
 
736
938
  function rerender() {
737
939
  wrapper.innerHTML = "";
738
940
  var current = getNestedValue(entryData, path) || "";
739
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
+
740
948
  if (current) {
741
- var preview = document.createElement("div");
949
+ var preview = document.createElement("button");
950
+ preview.type = "button";
742
951
  preview.className = "caret-single-image-preview";
743
- 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(); });
744
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);
745
971
  }
746
972
 
747
973
  var row = document.createElement("div");
@@ -750,6 +976,8 @@
750
976
  var input = document.createElement("input");
751
977
  input.type = "text";
752
978
  input.className = "studio-input";
979
+ input.id = fieldIdFromPath(path);
980
+ input.name = path;
753
981
  input.style.flex = "1";
754
982
  input.value = current;
755
983
  input.placeholder = "Image URL";
@@ -759,12 +987,13 @@
759
987
  var uploadBtn = document.createElement("button");
760
988
  uploadBtn.type = "button";
761
989
  uploadBtn.className = "studio-btn-ghost";
762
- uploadBtn.textContent = "Upload";
990
+ uploadBtn.textContent = current ? "Replace" : "Upload";
763
991
 
764
- var fileInput = document.createElement("input");
765
- fileInput.type = "file";
766
- fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
767
- 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.";
768
997
 
769
998
  uploadBtn.addEventListener("click", function () { fileInput.click(); });
770
999
  fileInput.addEventListener("change", function () {
@@ -772,11 +1001,14 @@
772
1001
  if (!file) return;
773
1002
  uploadBtn.textContent = "Uploading…";
774
1003
  uploadBtn.disabled = true;
775
- uploadFile(file).then(function (newUrl) {
776
- updateField(path, newUrl);
1004
+ Promise.all([uploadFile(file), readImageDimensions(file)]).then(function (results) {
1005
+ updateField(path, results[0]);
1006
+ populateImageObject(path, file, results[1]);
777
1007
  rerender();
778
1008
  }).catch(function () {
779
- 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";
780
1012
  uploadBtn.disabled = false;
781
1013
  });
782
1014
  fileInput.value = "";
@@ -786,6 +1018,7 @@
786
1018
  row.appendChild(uploadBtn);
787
1019
  row.appendChild(fileInput);
788
1020
  wrapper.appendChild(row);
1021
+ wrapper.appendChild(help);
789
1022
  }
790
1023
 
791
1024
  rerender();
@@ -809,7 +1042,7 @@
809
1042
  tag.textContent = item;
810
1043
  var btn = document.createElement("button");
811
1044
  btn.type = "button";
812
- btn.title = "Remove";
1045
+ btn.title = msg("field.remove", "Remove");
813
1046
  btn.textContent = "×";
814
1047
  btn.addEventListener("click", function () {
815
1048
  var next = current.slice();
@@ -861,6 +1094,31 @@
861
1094
  function buildObjectArray(items, path, itemSchema) {
862
1095
  var wrapper = document.createElement("div");
863
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
+ }
864
1122
 
865
1123
  function rerender() {
866
1124
  wrapper.innerHTML = "";
@@ -870,10 +1128,81 @@
870
1128
  var card = document.createElement("div");
871
1129
  card.className = "caret-object-array-item";
872
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
+
873
1201
  var removeBtn = document.createElement("button");
874
1202
  removeBtn.type = "button";
875
1203
  removeBtn.className = "caret-object-array-remove";
876
- 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));
877
1206
  removeBtn.textContent = "×";
878
1207
  removeBtn.addEventListener("click", function () {
879
1208
  var next = current.slice();
@@ -881,46 +1210,21 @@
881
1210
  updateField(path, next);
882
1211
  rerender();
883
1212
  });
884
- card.appendChild(removeBtn);
885
1213
 
886
- var idxLabel = document.createElement("span");
887
- idxLabel.className = "caret-object-array-index";
888
- idxLabel.textContent = "[" + idx + "]";
889
- card.appendChild(idxLabel);
1214
+ actions.appendChild(upBtn);
1215
+ actions.appendChild(downBtn);
1216
+ actions.appendChild(removeBtn);
1217
+ header.appendChild(actions);
1218
+ card.appendChild(header);
890
1219
 
891
1220
  var fieldsCont = document.createElement("div");
892
1221
  fieldsCont.className = "caret-object-array-fields";
893
1222
 
894
- var keys = (itemSchema && itemSchema.properties) ? Object.keys(itemSchema.properties) : Object.keys(item);
895
- keys.forEach(function (key) {
896
- var itemPath = path + "." + idx + "." + key;
897
- var val = item[key];
898
- var propSchema = itemSchema && itemSchema.properties && itemSchema.properties[key];
899
- var fieldLabel = (propSchema && propSchema.title) || humanizeKey(key);
900
-
901
- var row = document.createElement("div");
902
- row.className = "caret-object-array-row";
903
-
904
- var keyLabel = document.createElement("span");
905
- keyLabel.className = "caret-object-array-key";
906
- keyLabel.textContent = fieldLabel;
907
-
908
- var input = document.createElement("input");
909
- input.type = "text";
910
- input.className = "studio-input";
911
- input.value = typeof val === "string" ? val : (val == null ? "" : JSON.stringify(val));
912
- // Write back in the field's declared (or original) type, not always a
913
- // string — otherwise editing a number/boolean/nested field silently
914
- // rewrites it as text. Unparseable input falls back to the raw string
915
- // so server-side validation can report it.
916
- input.addEventListener("input", function () {
917
- updateField(itemPath, coerceScalarValue(input.value, propSchema, val));
918
- });
919
-
920
- row.appendChild(keyLabel);
921
- row.appendChild(input);
922
- fieldsCont.appendChild(row);
923
- });
1223
+ if (itemSchema && itemSchema.properties) {
1224
+ renderFields(fieldsCont, item, itemSchema, path + "." + idx);
1225
+ } else {
1226
+ renderFieldsFallback(fieldsCont, item, path + "." + idx);
1227
+ }
924
1228
 
925
1229
  card.appendChild(fieldsCont);
926
1230
  wrapper.appendChild(card);
@@ -929,22 +1233,34 @@
929
1233
  var addBtn = document.createElement("button");
930
1234
  addBtn.type = "button";
931
1235
  addBtn.className = "caret-object-array-add";
932
- addBtn.textContent = "+ Add item";
1236
+ addBtn.textContent = "+ " + msg("field.add", "Add") + " " + singularItemLabel(path);
933
1237
  addBtn.addEventListener("click", function () {
934
1238
  var template;
935
1239
  if (itemSchema && itemSchema.properties) {
936
- template = {};
937
- Object.keys(itemSchema.properties).forEach(function (k) {
938
- var s = itemSchema.properties[k];
939
- template[k] = s.type === "number" || s.type === "integer" ? 0 : (s.type === "boolean" ? false : "");
940
- });
1240
+ template = templateFromSchema(itemSchema);
1241
+ if (Object.prototype.hasOwnProperty.call(template, "id") && !template.id) {
1242
+ template.id = generatedId(path, current.length);
1243
+ }
941
1244
  } else {
942
1245
  template = current.length > 0
943
- ? 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
+ }))
944
1250
  : {};
945
1251
  }
946
1252
  updateField(path, current.concat([template]));
947
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
+ });
948
1264
  });
949
1265
  wrapper.appendChild(addBtn);
950
1266
  }
@@ -992,6 +1308,12 @@
992
1308
 
993
1309
  info.appendChild(actionSpan);
994
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
+ }
995
1317
  row.appendChild(info);
996
1318
 
997
1319
  var restoreBtn = document.createElement("button");
@@ -1040,6 +1362,7 @@
1040
1362
  return;
1041
1363
  }
1042
1364
  historyPanel.hidden = true;
1365
+ announceChange("cms:saved", entryData);
1043
1366
  showStatus("saved", "Restored");
1044
1367
  }).catch(function () {
1045
1368
  showStatus("error", "Restore failed");
@@ -1048,7 +1371,7 @@
1048
1371
 
1049
1372
  /* ─── Wire events ───────────────────────────────────────────────── */
1050
1373
  saveBtn.addEventListener("click", save);
1051
- deleteBtn.addEventListener("click", deleteEntry);
1374
+ if (deleteBtn) deleteBtn.addEventListener("click", deleteEntry);
1052
1375
  historyBtn.addEventListener("click", loadHistory);
1053
1376
  historyCloseBtn.addEventListener("click", function () {
1054
1377
  historyPanel.hidden = true;
@@ -1060,6 +1383,41 @@
1060
1383
  if (e.target === deleteDialog) deleteDialog.hidden = true;
1061
1384
  });
1062
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
+
1063
1421
  window.addEventListener("keydown", function (e) {
1064
1422
  if ((e.metaKey || e.ctrlKey) && e.key === "s") {
1065
1423
  e.preventDefault();
@@ -1097,17 +1455,48 @@
1097
1455
  if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
1098
1456
 
1099
1457
  loadingEl.hidden = true;
1100
- if (!entry) {
1458
+ var initializedMissingEntry = !entry && (IS_NEW || INITIALIZE_IF_MISSING);
1459
+ if (!entry && !initializedMissingEntry) {
1101
1460
  notFoundEl.hidden = false;
1102
1461
  return;
1103
1462
  }
1104
- entryData = deepClone(entry.data);
1105
- entryRevision = typeof entry.revision === "number" ? entry.revision : undefined;
1106
- 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);
1107
1469
  titleEl.textContent = getTitle(entryData);
1108
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
+ }
1109
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
+ }
1110
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
+ }
1111
1500
  });
1112
1501
  });
1113
1502
  }).catch(function () {