@caretcms/core 0.3.0 → 0.4.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 (223) hide show
  1. package/README.md +116 -11
  2. package/dist/index.d.ts +15 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +78 -5
  5. package/dist/index.js.map +1 -1
  6. package/dist/loader.d.ts.map +1 -1
  7. package/dist/loader.js +8 -0
  8. package/dist/loader.js.map +1 -1
  9. package/dist/markdown/paragraphs.d.ts +14 -0
  10. package/dist/markdown/paragraphs.d.ts.map +1 -0
  11. package/dist/markdown/paragraphs.js +61 -0
  12. package/dist/markdown/paragraphs.js.map +1 -0
  13. package/dist/markdown/remark.d.ts.map +1 -1
  14. package/dist/markdown/remark.js +1 -0
  15. package/dist/markdown/remark.js.map +1 -1
  16. package/dist/markdown/satteri.d.ts.map +1 -1
  17. package/dist/markdown/satteri.js +3 -1
  18. package/dist/markdown/satteri.js.map +1 -1
  19. package/dist/markdown/stamp.d.ts +0 -9
  20. package/dist/markdown/stamp.d.ts.map +1 -1
  21. package/dist/markdown/stamp.js +6 -0
  22. package/dist/markdown/stamp.js.map +1 -1
  23. package/dist/providers/deployment/github.d.ts +27 -0
  24. package/dist/providers/deployment/github.d.ts.map +1 -0
  25. package/dist/providers/deployment/github.js +180 -0
  26. package/dist/providers/deployment/github.js.map +1 -0
  27. package/dist/providers/deployment/simulated.d.ts +10 -0
  28. package/dist/providers/deployment/simulated.d.ts.map +1 -0
  29. package/dist/providers/deployment/simulated.js +29 -0
  30. package/dist/providers/deployment/simulated.js.map +1 -0
  31. package/dist/runtime/authorization.d.ts +11 -0
  32. package/dist/runtime/authorization.d.ts.map +1 -0
  33. package/dist/runtime/authorization.js +29 -0
  34. package/dist/runtime/authorization.js.map +1 -0
  35. package/dist/runtime/collection-policy.d.ts +11 -0
  36. package/dist/runtime/collection-policy.d.ts.map +1 -0
  37. package/dist/runtime/collection-policy.js +44 -0
  38. package/dist/runtime/collection-policy.js.map +1 -0
  39. package/dist/runtime/content-errors.d.ts +9 -0
  40. package/dist/runtime/content-errors.d.ts.map +1 -0
  41. package/dist/runtime/content-errors.js +17 -0
  42. package/dist/runtime/content-errors.js.map +1 -0
  43. package/dist/runtime/content.d.ts.map +1 -1
  44. package/dist/runtime/content.js +13 -4
  45. package/dist/runtime/content.js.map +1 -1
  46. package/dist/runtime/deployment-status.d.ts +14 -0
  47. package/dist/runtime/deployment-status.d.ts.map +1 -0
  48. package/dist/runtime/deployment-status.js +57 -0
  49. package/dist/runtime/deployment-status.js.map +1 -0
  50. package/dist/runtime/draft-state.d.ts +16 -0
  51. package/dist/runtime/draft-state.d.ts.map +1 -0
  52. package/dist/runtime/draft-state.js +29 -0
  53. package/dist/runtime/draft-state.js.map +1 -0
  54. package/dist/runtime/i18n.d.ts +13 -2
  55. package/dist/runtime/i18n.d.ts.map +1 -1
  56. package/dist/runtime/i18n.js +26 -4
  57. package/dist/runtime/i18n.js.map +1 -1
  58. package/dist/runtime/index.d.ts +1 -0
  59. package/dist/runtime/index.d.ts.map +1 -1
  60. package/dist/runtime/index.js +1 -0
  61. package/dist/runtime/index.js.map +1 -1
  62. package/dist/runtime/middleware.d.ts.map +1 -1
  63. package/dist/runtime/middleware.js +21 -2
  64. package/dist/runtime/middleware.js.map +1 -1
  65. package/dist/runtime/mutations/contracts.d.ts +9 -0
  66. package/dist/runtime/mutations/contracts.d.ts.map +1 -1
  67. package/dist/runtime/mutations/contracts.js +45 -4
  68. package/dist/runtime/mutations/contracts.js.map +1 -1
  69. package/dist/runtime/mutations/engine.d.ts +2 -0
  70. package/dist/runtime/mutations/engine.d.ts.map +1 -1
  71. package/dist/runtime/mutations/engine.js +115 -63
  72. package/dist/runtime/mutations/engine.js.map +1 -1
  73. package/dist/runtime/providers.d.ts +3 -1
  74. package/dist/runtime/providers.d.ts.map +1 -1
  75. package/dist/runtime/providers.js +12 -1
  76. package/dist/runtime/providers.js.map +1 -1
  77. package/dist/runtime/publish-recovery.d.ts +32 -0
  78. package/dist/runtime/publish-recovery.d.ts.map +1 -0
  79. package/dist/runtime/publish-recovery.js +98 -0
  80. package/dist/runtime/publish-recovery.js.map +1 -0
  81. package/dist/runtime/publish.d.ts +10 -4
  82. package/dist/runtime/publish.d.ts.map +1 -1
  83. package/dist/runtime/publish.js +104 -67
  84. package/dist/runtime/publish.js.map +1 -1
  85. package/dist/runtime/rebuild-webhook.d.ts +4 -0
  86. package/dist/runtime/rebuild-webhook.d.ts.map +1 -1
  87. package/dist/runtime/rebuild-webhook.js +12 -2
  88. package/dist/runtime/rebuild-webhook.js.map +1 -1
  89. package/dist/runtime/request-context.d.ts +3 -1
  90. package/dist/runtime/request-context.d.ts.map +1 -1
  91. package/dist/runtime/request-context.js.map +1 -1
  92. package/dist/runtime/rewrite.d.ts.map +1 -1
  93. package/dist/runtime/rewrite.js +28 -2
  94. package/dist/runtime/rewrite.js.map +1 -1
  95. package/dist/runtime/rich-allowlist.js +1 -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 +24 -12
  99. package/dist/runtime/routes/admin-entry.js.map +1 -1
  100. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  101. package/dist/runtime/routes/auth-session.js +6 -0
  102. package/dist/runtime/routes/auth-session.js.map +1 -1
  103. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  104. package/dist/runtime/routes/collection-list.js +164 -40
  105. package/dist/runtime/routes/collection-list.js.map +1 -1
  106. package/dist/runtime/routes/deployment.d.ts +5 -0
  107. package/dist/runtime/routes/deployment.d.ts.map +1 -0
  108. package/dist/runtime/routes/deployment.js +38 -0
  109. package/dist/runtime/routes/deployment.js.map +1 -0
  110. package/dist/runtime/routes/draft.d.ts.map +1 -1
  111. package/dist/runtime/routes/draft.js +10 -3
  112. package/dist/runtime/routes/draft.js.map +1 -1
  113. package/dist/runtime/routes/entries.d.ts.map +1 -1
  114. package/dist/runtime/routes/entries.js +74 -39
  115. package/dist/runtime/routes/entries.js.map +1 -1
  116. package/dist/runtime/routes/history.d.ts.map +1 -1
  117. package/dist/runtime/routes/history.js +37 -21
  118. package/dist/runtime/routes/history.js.map +1 -1
  119. package/dist/runtime/routes/mutate.d.ts.map +1 -1
  120. package/dist/runtime/routes/mutate.js +4 -1
  121. package/dist/runtime/routes/mutate.js.map +1 -1
  122. package/dist/runtime/routes/publish.d.ts.map +1 -1
  123. package/dist/runtime/routes/publish.js +127 -33
  124. package/dist/runtime/routes/publish.js.map +1 -1
  125. package/dist/runtime/routes/schema.d.ts.map +1 -1
  126. package/dist/runtime/routes/schema.js +4 -0
  127. package/dist/runtime/routes/schema.js.map +1 -1
  128. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  129. package/dist/runtime/routes/studio-home.js +3 -1
  130. package/dist/runtime/routes/studio-home.js.map +1 -1
  131. package/dist/runtime/routes/upload.d.ts.map +1 -1
  132. package/dist/runtime/routes/upload.js +3 -0
  133. package/dist/runtime/routes/upload.js.map +1 -1
  134. package/dist/runtime/schema-registry.d.ts.map +1 -1
  135. package/dist/runtime/schema-registry.js +2 -0
  136. package/dist/runtime/schema-registry.js.map +1 -1
  137. package/dist/runtime/storage/entry-commit.d.ts +8 -0
  138. package/dist/runtime/storage/entry-commit.d.ts.map +1 -0
  139. package/dist/runtime/storage/entry-commit.js +45 -0
  140. package/dist/runtime/storage/entry-commit.js.map +1 -0
  141. package/dist/runtime/storage/filesystem-adapter.d.ts +5 -0
  142. package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -1
  143. package/dist/runtime/storage/filesystem-adapter.js +12 -3
  144. package/dist/runtime/storage/filesystem-adapter.js.map +1 -1
  145. package/dist/runtime/storage/frontmatter-codec.d.ts +5 -23
  146. package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
  147. package/dist/runtime/storage/frontmatter-codec.js +156 -13
  148. package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
  149. package/dist/runtime/storage/in-memory-adapter.d.ts +7 -0
  150. package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -1
  151. package/dist/runtime/storage/in-memory-adapter.js +6 -0
  152. package/dist/runtime/storage/in-memory-adapter.js.map +1 -1
  153. package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
  154. package/dist/runtime/storage/markdown-adapter.js +18 -20
  155. package/dist/runtime/storage/markdown-adapter.js.map +1 -1
  156. package/dist/runtime/storage/session-overlay-adapter.d.ts +12 -4
  157. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
  158. package/dist/runtime/storage/session-overlay-adapter.js +100 -13
  159. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
  160. package/dist/runtime/storage/sidecar-meta-store.d.ts +5 -0
  161. package/dist/runtime/storage/sidecar-meta-store.d.ts.map +1 -1
  162. package/dist/runtime/storage/sidecar-meta-store.js +48 -5
  163. package/dist/runtime/storage/sidecar-meta-store.js.map +1 -1
  164. package/dist/runtime/utils.d.ts.map +1 -1
  165. package/dist/runtime/utils.js +3 -4
  166. package/dist/runtime/utils.js.map +1 -1
  167. package/dist/types.d.ts +123 -1
  168. package/dist/types.d.ts.map +1 -1
  169. package/package.json +12 -3
  170. package/static/cms/admin-entry.css +9 -0
  171. package/static/cms/admin-entry.js +251 -687
  172. package/static/cms/dev-toolbar/app.js +78 -19
  173. package/static/cms/editor/config.js +35 -15
  174. package/static/cms/editor/content-map.css +8 -1
  175. package/static/cms/editor/content-map.js +62 -23
  176. package/static/cms/editor/deployment-status.js +66 -0
  177. package/static/cms/editor/draft-client.js +87 -0
  178. package/static/cms/editor/guards.js +20 -0
  179. package/static/cms/editor/helpers.js +30 -1
  180. package/static/cms/editor/highlight.js +1 -0
  181. package/static/cms/editor/image-edit.js +43 -1
  182. package/static/cms/editor/image-utils.js +23 -7
  183. package/static/cms/editor/link-follow.js +122 -41
  184. package/static/cms/editor/link-ui.css +10 -1
  185. package/static/cms/editor/linkify.js +2 -0
  186. package/static/cms/editor/md-block-edit.js +140 -37
  187. package/static/cms/editor/md-paragraphs.js +98 -0
  188. package/static/cms/editor/md-serialize.js +1 -1
  189. package/static/cms/editor/panel.js +43 -15
  190. package/static/cms/editor/preview-mode.js +44 -0
  191. package/static/cms/editor/publish-result.js +100 -0
  192. package/static/cms/editor/rich-toolbar.js +58 -15
  193. package/static/cms/editor/sanitize.js +6 -2
  194. package/static/cms/editor/save-queue.js +75 -17
  195. package/static/cms/editor/section-controls/activation.js +3 -0
  196. package/static/cms/editor/section-controls/api.js +25 -7
  197. package/static/cms/editor/section-controls/index.js +60 -8
  198. package/static/cms/editor/section-controls/model.js +39 -4
  199. package/static/cms/editor/section-controls/mutations.js +15 -0
  200. package/static/cms/editor/section-controls/page-context.js +9 -0
  201. package/static/cms/editor/section-controls/pickers.js +53 -2
  202. package/static/cms/editor/section-controls/reorder.js +23 -0
  203. package/static/cms/editor/section-controls/selection.js +6 -0
  204. package/static/cms/editor/section-controls/spacing-drag.js +16 -0
  205. package/static/cms/editor/section-controls/ui-elements.js +7 -0
  206. package/static/cms/editor/section-controls/utils.js +7 -0
  207. package/static/cms/editor/section-controls.css +18 -3
  208. package/static/cms/editor/security.js +13 -2
  209. package/static/cms/editor/stega-hydrate.js +17 -4
  210. package/static/cms/editor/sync.js +208 -10
  211. package/static/cms/editor/text-edit.js +37 -3
  212. package/static/cms/editor/text-link-interactions.js +59 -17
  213. package/static/cms/editor/toast.js +23 -9
  214. package/static/cms/editor/toolbar.css +5 -0
  215. package/static/cms/editor/toolbar.js +91 -101
  216. package/static/cms/editor.js +2 -1
  217. package/static/cms/studio/entry-loader.js +102 -0
  218. package/static/cms/studio/field-model.js +110 -0
  219. package/static/cms/studio/fields.js +319 -0
  220. package/static/cms/studio/history-client.js +74 -0
  221. package/static/cms/studio/mutation-client.js +74 -0
  222. package/static/cms/studio/sync-client.js +107 -0
  223. package/static/cms/studio/upload-client.js +109 -0
@@ -1,3 +1,15 @@
1
+ import { deepClone, templateFromSchema, generatedId, friendlyFileTitle, singularItemLabel, getTitle, humanizeKey, fieldIdFromPath, getNestedValue, setNestedValue } from './studio/field-model.js';
2
+ import { createFieldGroup, createFieldRenderer } from './studio/fields.js';
3
+ import { createStudioMutationClient } from './studio/mutation-client.js';
4
+ import { createStudioHistoryClient } from './studio/history-client.js';
5
+ import { createStudioUploadClient, isSupportedImage } from './studio/upload-client.js';
6
+ import { createStudioEntryLoader } from './studio/entry-loader.js';
7
+ import { createStudioSync } from './studio/sync-client.js';
8
+
9
+ /** @typedef {import('../../src/schema-utils.js').JsonSchemaNode} Schema */
10
+ /** @typedef {{ path: string, message: string }} ValidationIssue */
11
+ /** @typedef {{ apiBasePath?: string, mountPath?: string, collection?: string, id?: string, isNew?: boolean, initializeIfMissing?: boolean, saveTarget?: string, previewPath?: string, editable?: boolean, messages?: Record<string, string> }} EntryConfig */
12
+
1
13
  /**
2
14
  * Studio entry editor — runtime.
3
15
  *
@@ -29,148 +41,115 @@
29
41
  var pageRoot = document.getElementById("cms-entry-page");
30
42
  if (!configEl || !pageRoot) return;
31
43
 
44
+ /** @type {EntryConfig} */
32
45
  var CFG;
33
46
  try {
34
- CFG = JSON.parse(configEl.textContent || "{}");
47
+ CFG = /** @type {EntryConfig} */ (JSON.parse(configEl.textContent || "{}"));
35
48
  } catch (e) {
36
49
  console.error("[caret] invalid entry config", e);
37
50
  return;
38
51
  }
39
52
 
40
- var API = CFG.apiBasePath;
41
- var MOUNT = CFG.mountPath;
42
- var COLLECTION = CFG.collection;
43
- var ID = CFG.id;
53
+ var API = typeof CFG.apiBasePath === "string" ? CFG.apiBasePath : "";
54
+ var MOUNT = typeof CFG.mountPath === "string" ? CFG.mountPath : "";
55
+ var COLLECTION = typeof CFG.collection === "string" ? CFG.collection : "";
56
+ var ID = typeof CFG.id === "string" ? CFG.id : "";
44
57
  var IS_NEW = CFG.isNew === true;
45
58
  var INITIALIZE_IF_MISSING = CFG.initializeIfMissing === true;
46
59
  var SAVE_TARGET = CFG.saveTarget === "preview" ? "preview" : "live";
60
+ var PREVIEW_PATH = typeof CFG.previewPath === "string" ? CFG.previewPath : null;
61
+ /** @type {Record<string, string>} */
47
62
  var MSG = CFG.messages || {};
63
+ /** @param {string} key @param {string} fallback */
48
64
  function msg(key, fallback) { return typeof MSG[key] === "string" ? MSG[key] : fallback; }
65
+ /** @param {unknown} value */
49
66
  function htmlEscape(value) {
67
+ /** @type {Record<string, string>} */
68
+ var entities = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
50
69
  return String(value).replace(/[&<>"']/g, function (char) {
51
- return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char];
70
+ return entities[char] || char;
52
71
  });
53
72
  }
54
73
  if (!API || !COLLECTION || !ID) return;
74
+ var mutationClient = createStudioMutationClient({ apiBasePath: API });
75
+ var historyClient = createStudioHistoryClient({ apiBasePath: API });
76
+ var uploadClient = createStudioUploadClient({ apiBasePath: API, onUnauthorized: loginRedirect });
77
+ var entryLoader = createStudioEntryLoader({ apiBasePath: API });
55
78
 
56
79
  /* ─── State ─────────────────────────────────────────────────────── */
80
+ /** @type {Record<string, unknown> | null} */
57
81
  var entryData = null;
82
+ /** @type {Schema | null} */
58
83
  var entrySchema = null;
84
+ /** @type {number | undefined} */
59
85
  var entryRevision = undefined;
60
86
  var originalJson = "";
61
87
  var saving = false;
62
88
  var dirtyTimer = 0;
63
- var previewTimer = 0;
64
89
  var linkedSelectionTimer = 0;
90
+ /** @type {Record<string, unknown> | null} */
65
91
  var pendingRemoteSelection = null;
66
- var SYNC_CHANNEL = "caretcms:content";
67
- var syncChannel = null;
92
+ /** @type {string | null} */
93
+ var publicationFieldName = null;
94
+ var studioSync = createStudioSync({
95
+ collection: COLLECTION,
96
+ id: ID,
97
+ previewPath: PREVIEW_PATH,
98
+ origin: window.location.origin,
99
+ isEmbedded: window.parent !== window,
100
+ studioPath: function () { return window.location.pathname + window.location.search; },
101
+ postToParent: function (message) { window.parent.postMessage(message, window.location.origin); },
102
+ onSelection: applyRemoteSelection,
103
+ addWindowMessageListener: function (listener) { window.addEventListener("message", listener); },
104
+ addPagehideListener: function (listener) { window.addEventListener("pagehide", listener, { once: true }); },
105
+ openChannel: "BroadcastChannel" in window ? function (name) { return new BroadcastChannel(name); } : undefined,
106
+ });
68
107
 
69
108
  /* ─── DOM refs ──────────────────────────────────────────────────── */
70
- var loadingEl = document.getElementById("loading");
71
- var notFoundEl = document.getElementById("not-found");
72
- var editorEl = document.getElementById("editor");
73
- var fieldsEl = document.getElementById("fields");
74
- var titleEl = document.getElementById("entry-title");
75
- var statusEl = document.getElementById("status-msg");
76
- var validationWarningEl = document.getElementById("validation-warning");
77
- var saveBtn = document.getElementById("btn-save");
78
- var deleteBtn = document.getElementById("btn-delete");
79
- var historyBtn = document.getElementById("btn-history");
80
- var historyPanel = document.getElementById("history-panel");
81
- var historyList = document.getElementById("history-list");
82
- var historyCloseBtn = document.getElementById("btn-history-close");
83
- var deleteDialog = document.getElementById("delete-dialog");
84
- var deleteNameEl = document.getElementById("delete-entry-name");
85
- var deleteCancelBtn = document.getElementById("btn-delete-cancel");
86
- var deleteConfirmBtn = document.getElementById("btn-delete-confirm");
109
+ var loadingEl = /** @type {HTMLElement} */ (document.getElementById("loading"));
110
+ var notFoundEl = /** @type {HTMLElement} */ (document.getElementById("not-found"));
111
+ var editorEl = /** @type {HTMLElement} */ (document.getElementById("editor"));
112
+ var fieldsEl = /** @type {HTMLElement} */ (document.getElementById("fields"));
113
+ var titleEl = /** @type {HTMLElement} */ (document.getElementById("entry-title"));
114
+ var statusEl = /** @type {HTMLElement} */ (document.getElementById("status-msg"));
115
+ var validationWarningEl = /** @type {HTMLElement | null} */ (document.getElementById("validation-warning"));
116
+ var saveBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-save"));
117
+ var deleteBtn = /** @type {HTMLButtonElement | null} */ (document.getElementById("btn-delete"));
118
+ var historyBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-history"));
119
+ var historyPanel = /** @type {HTMLElement} */ (document.getElementById("history-panel"));
120
+ var historyList = /** @type {HTMLElement} */ (document.getElementById("history-list"));
121
+ var historyCloseBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-history-close"));
122
+ var deleteDialog = /** @type {HTMLElement} */ (document.getElementById("delete-dialog"));
123
+ var deleteNameEl = /** @type {HTMLElement} */ (document.getElementById("delete-entry-name"));
124
+ var deleteCancelBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-delete-cancel"));
125
+ var deleteConfirmBtn = /** @type {HTMLButtonElement} */ (document.getElementById("btn-delete-confirm"));
126
+ /** @type {HTMLElement | null} */
127
+ var deleteRestoreFocus = null;
87
128
 
88
129
  /* ─── Helpers ───────────────────────────────────────────────────── */
89
- function deepClone(v) { return JSON.parse(JSON.stringify(v)); }
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
-
141
- function getTitle(data) {
142
- if (!data) return "Untitled";
143
- var keys = ["name", "title", "question", "company_name", "headline", "label"];
144
- for (var i = 0; i < keys.length; i++) {
145
- if (typeof data[keys[i]] === "string" && data[keys[i]]) return data[keys[i]];
146
- }
147
- return "Untitled";
148
- }
149
-
150
- function humanizeKey(key) {
151
- return key.replace(/_/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
152
- }
153
130
 
154
131
  function isDirty() {
155
132
  return entryData !== null && JSON.stringify(entryData) !== originalJson;
156
133
  }
157
134
 
158
- function updateSaveButton() {
135
+ /** @param {boolean} [preserveStatus] */
136
+ function updateSaveButton(preserveStatus) {
159
137
  var dirty = isDirty();
160
- saveBtn.disabled = !dirty || saving;
138
+ saveBtn.disabled = CFG.editable === false || !dirty || saving;
161
139
  saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
162
140
  saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
163
141
  saveBtn.textContent = saving ? msg("common.saving", "Saving…") : (SAVE_TARGET === "preview" ? msg("entry.saveDraft", "Save draft") : msg("entry.saveLive", "Save live"));
164
142
  titleEl.textContent = getTitle(entryData);
165
- if (entryData !== null) {
143
+ if (entryData !== null && !preserveStatus) {
166
144
  if (saving) showStatus("saving", msg("entry.savingChanges", "Saving changes…"));
167
145
  else if (dirty) showStatus("dirty", msg("entry.unsaved", "Unsaved changes"));
168
146
  else showStatus("saved", msg("entry.allSaved", "All changes saved"));
169
147
  }
170
148
  }
171
149
 
172
- function showStatus(type, msg) {
173
- statusEl.textContent = msg;
150
+ /** @param {string} type @param {string} message */
151
+ function showStatus(type, message) {
152
+ statusEl.textContent = message;
174
153
  statusEl.hidden = false;
175
154
  statusEl.dataset.status = type;
176
155
  statusEl.style.color = type === "saved"
@@ -178,151 +157,31 @@
178
157
  : (type === "dirty" || type === "saving" ? "var(--studio-text-muted)" : "var(--studio-red)");
179
158
  }
180
159
 
181
- function setNestedValue(obj, path, value) {
182
- var keys = path.split(".");
183
- var cur = obj;
184
- for (var i = 0; i < keys.length - 1; i++) {
185
- var k = keys[i];
186
- if (typeof cur[k] !== "object" || cur[k] === null) {
187
- cur[k] = /^\d+$/.test(keys[i + 1]) ? [] : {};
188
- }
189
- cur = cur[k];
190
- }
191
- cur[keys[keys.length - 1]] = value;
192
- }
193
-
194
- function getNestedValue(obj, path) {
195
- return path.split(".").reduce(function (o, k) { return o == null ? undefined : o[k]; }, obj);
196
- }
197
-
160
+ /** @param {string} path @param {unknown} value */
198
161
  function updateField(path, value) {
162
+ if (!entryData) return;
199
163
  setNestedValue(entryData, path, value);
200
164
  if (!saving) {
201
- saveBtn.disabled = false;
165
+ saveBtn.disabled = CFG.editable === false;
202
166
  saveBtn.classList.remove("studio-save-clean", "studio-save-saving");
203
167
  saveBtn.classList.add("studio-save-dirty");
204
168
  }
205
169
  clearTimeout(dirtyTimer);
206
170
  dirtyTimer = setTimeout(updateSaveButton, 300);
207
- queueEmbeddedPreview();
171
+ studioSync.queuePreview(entryData);
208
172
  }
209
173
 
210
174
  function loginRedirect() {
211
175
  window.location.href = MOUNT + "?redirect=" + encodeURIComponent(window.location.pathname);
212
176
  }
213
177
 
178
+ /** @param {Response} res */
214
179
  function handleAuth(res) {
215
180
  if (res.status === 401) { loginRedirect(); return true; }
216
181
  return false;
217
182
  }
218
183
 
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
-
260
- /* ─── Image compression + upload ────────────────────────────────── */
261
- function compressImage(file, maxWidth, quality) {
262
- maxWidth = maxWidth || 1600;
263
- quality = quality || 0.82;
264
- return Promise.resolve().then(function () {
265
- if (file.size < 200000 && file.type === "image/webp") return file;
266
- return createImageBitmap(file).then(function (bitmap) {
267
- var width = bitmap.width;
268
- var height = bitmap.height;
269
- var needsResize = width > maxWidth;
270
- if (file.type === "image/png" && !needsResize) {
271
- bitmap.close();
272
- return file;
273
- }
274
- if (needsResize) {
275
- height = Math.round((height * maxWidth) / width);
276
- width = maxWidth;
277
- }
278
- var canvas = document.createElement("canvas");
279
- canvas.width = width;
280
- canvas.height = height;
281
- canvas.getContext("2d").drawImage(bitmap, 0, 0, width, height);
282
- bitmap.close();
283
- var isPng = file.type === "image/png";
284
- var outType = isPng ? "image/png" : "image/webp";
285
- return new Promise(function (resolve) {
286
- canvas.toBlob(function (blob) {
287
- if (!blob || blob.size >= file.size) return resolve(file);
288
- var ext = isPng ? ".png" : ".webp";
289
- resolve(new File([blob], file.name.replace(/\.[^.]+$/, ext), { type: outType }));
290
- }, outType, isPng ? undefined : quality);
291
- });
292
- });
293
- });
294
- }
295
-
296
- function uploadFile(file) {
297
- return compressImage(file).then(function (compressed) {
298
- var fd = new FormData();
299
- fd.append("file", compressed);
300
- return fetch(API + "/upload", {
301
- method: "POST",
302
- body: fd,
303
- headers: { "x-caret-request": "1" },
304
- credentials: "same-origin",
305
- });
306
- }).then(function (res) {
307
- if (handleAuth(res)) throw new Error("Unauthorized");
308
- if (!res.ok) throw new Error("Upload failed");
309
- return res.json();
310
- }).then(function (json) { return json.url; });
311
- }
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
-
184
+ /** @param {string} path @param {File} file @param {{ width: number, height: number } | null} dimensions */
326
185
  function populateImageObject(path, file, dimensions) {
327
186
  var parts = path.split(".");
328
187
  if (parts.length < 2 || parts[parts.length - 1] !== "src") return;
@@ -330,7 +189,7 @@
330
189
  var current = getNestedValue(entryData, parentPath);
331
190
  if (!current || typeof current !== "object" || Array.isArray(current)) return;
332
191
 
333
- var next = deepClone(current);
192
+ var next = /** @type {Record<string, unknown>} */ (deepClone(current));
334
193
  var title = friendlyFileTitle(file.name);
335
194
  if (Object.prototype.hasOwnProperty.call(next, "id") && !next.id) {
336
195
  next.id = generatedId(parentPath, Number(parts[parts.length - 2]) || 0);
@@ -360,79 +219,76 @@
360
219
  }
361
220
  saving = true;
362
221
  updateSaveButton();
222
+ var preserveSaveStatus = false;
223
+ /** @type {{ collection: string, id: string, data: Record<string, unknown>, expectedRevision?: number }} */
363
224
  var payload = {
364
- type: "put_entry",
365
225
  collection: COLLECTION,
366
226
  id: ID,
367
227
  data: entryData,
368
228
  };
369
229
  if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
370
230
 
371
- fetch(API + "/mutate", {
372
- method: "POST",
373
- headers: { "Content-Type": "application/json", "x-caret-request": "1" },
374
- credentials: "same-origin",
375
- body: JSON.stringify(payload),
376
- }).then(function (res) {
377
- if (handleAuth(res)) return null;
378
- return res.json().catch(function () { return null; }).then(function (body) {
379
- return { ok: res.ok, status: res.status, body: body };
380
- });
381
- }).then(function (result) {
382
- if (!result) return;
383
- if (!result.ok) {
384
- if (result.body && result.body.issues) {
385
- showValidationErrors(result.body.issues);
386
- showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.body.issues.length + ")");
231
+ mutationClient.save(payload).then(function (result) {
232
+ if (result.kind === "unauthorized") { loginRedirect(); return; }
233
+ if (result.kind === "validation") {
234
+ preserveSaveStatus = true;
235
+ showValidationErrors(result.issues);
236
+ showStatus("error", msg("entry.validationFailed", "Validation failed") + " (" + result.issues.length + ")");
387
237
  var firstErr = fieldsEl.querySelector(".studio-field-error");
388
238
  if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
389
239
  return;
390
- }
391
- if (result.status === 409 && result.body && typeof result.body.currentRevision === "number") {
240
+ }
241
+ if (result.kind === "conflict") {
242
+ preserveSaveStatus = true;
392
243
  // Your edits are still in the form; we've refreshed to the server's
393
244
  // revision, so saving again overwrites. Don't tell the user to reload
394
245
  // (that would throw their edits away).
395
- entryRevision = result.body.currentRevision;
246
+ entryRevision = result.currentRevision;
396
247
  showStatus("error", msg("entry.changedElsewhere", "Changed elsewhere — Save again to overwrite"));
397
248
  return;
398
- }
399
- throw new Error("save failed");
400
249
  }
250
+ if (result.kind === "error") throw new Error("save failed");
401
251
  clearFieldErrors();
402
- if (result.body && typeof result.body.revision === "number") entryRevision = result.body.revision;
252
+ if (typeof result.revision === "number") entryRevision = result.revision;
403
253
  originalJson = JSON.stringify(entryData);
404
- announceChange("cms:saved", entryData);
254
+ studioSync.announceChange("cms:saved", entryData);
405
255
  showStatus("saved", msg("entry.saved", "Saved"));
406
256
  }).catch(function () {
257
+ preserveSaveStatus = true;
407
258
  showStatus("error", msg("common.error", "Error"));
408
259
  }).then(function () {
409
260
  saving = false;
410
- updateSaveButton();
261
+ updateSaveButton(preserveSaveStatus);
411
262
  });
412
263
  }
413
264
 
414
265
  function deleteEntry() {
415
266
  deleteNameEl.textContent = ID;
267
+ deleteRestoreFocus = document.activeElement instanceof HTMLElement ? document.activeElement : deleteBtn;
416
268
  deleteDialog.hidden = false;
269
+ deleteCancelBtn.focus();
270
+ }
271
+
272
+ function closeDeleteDialog() {
273
+ deleteDialog.hidden = true;
274
+ var restore = deleteRestoreFocus || deleteBtn;
275
+ deleteRestoreFocus = null;
276
+ if (restore instanceof HTMLElement && restore.isConnected) restore.focus();
417
277
  }
418
278
 
419
279
  function confirmDelete() {
420
280
  deleteConfirmBtn.disabled = true;
421
281
  deleteConfirmBtn.textContent = msg("entry.deleting", "Deleting…");
422
- var payload = { type: "delete_entry", collection: COLLECTION, id: ID };
282
+ /** @type {{ collection: string, id: string, expectedRevision?: number }} */
283
+ var payload = { collection: COLLECTION, id: ID };
423
284
  if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
424
- fetch(API + "/mutate", {
425
- method: "POST",
426
- headers: { "Content-Type": "application/json", "x-caret-request": "1" },
427
- credentials: "same-origin",
428
- body: JSON.stringify(payload),
429
- }).then(function (res) {
430
- if (handleAuth(res)) return;
431
- if (!res.ok) throw new Error("delete failed");
432
- announceChange("cms:deleted", null);
285
+ mutationClient.remove(payload).then(function (result) {
286
+ if (result === "unauthorized") { loginRedirect(); return; }
287
+ if (result !== "deleted") throw new Error("delete failed");
288
+ studioSync.announceChange("cms:deleted", null);
433
289
  window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
434
290
  }).catch(function () {
435
- deleteDialog.hidden = true;
291
+ closeDeleteDialog();
436
292
  deleteConfirmBtn.disabled = false;
437
293
  deleteConfirmBtn.textContent = msg("entry.delete", "Delete");
438
294
  showStatus("error", msg("entry.deleteFailed", "Delete failed"));
@@ -440,6 +296,7 @@
440
296
  }
441
297
 
442
298
  /* ─── Validation rendering ──────────────────────────────────────── */
299
+ /** @param {Element} group @param {string} message */
443
300
  function showFieldError(group, message) {
444
301
  var errEl = group.querySelector(".studio-error-text");
445
302
  if (message) {
@@ -449,7 +306,7 @@
449
306
  group.appendChild(errEl);
450
307
  }
451
308
  errEl.textContent = message;
452
- errEl.hidden = false;
309
+ /** @type {HTMLElement} */ (errEl).hidden = false;
453
310
  group.classList.add("studio-field-error");
454
311
  } else if (errEl) {
455
312
  errEl.remove();
@@ -462,6 +319,7 @@
462
319
  fieldsEl.querySelectorAll(".caret-field-group").forEach(function (el) { el.classList.remove("studio-field-error"); });
463
320
  }
464
321
 
322
+ /** @param {ValidationIssue[]} issues */
465
323
  function showValidationErrors(issues) {
466
324
  clearFieldErrors();
467
325
  issues.forEach(function (issue) {
@@ -470,15 +328,18 @@
470
328
  });
471
329
  }
472
330
 
331
+ /** @param {string} value */
473
332
  function cssEscape(value) {
474
333
  if (window.CSS && CSS.escape) return CSS.escape(value);
475
334
  return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
476
335
  }
477
336
 
337
+ /** @param {string} path */
478
338
  function fieldControl(path) {
479
339
  return fieldsEl.querySelector("#" + cssEscape(fieldIdFromPath(path)));
480
340
  }
481
341
 
342
+ /** @param {Element} group */
482
343
  function showLinkedSelection(group) {
483
344
  window.clearTimeout(linkedSelectionTimer);
484
345
  fieldsEl.querySelectorAll(".caret-field-selected").forEach(function (selected) {
@@ -490,6 +351,7 @@
490
351
  }, 1500);
491
352
  }
492
353
 
354
+ /** @param {Record<string, unknown>} message */
493
355
  function applyRemoteSelection(message) {
494
356
  if (
495
357
  !message
@@ -510,331 +372,33 @@
510
372
  group.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
511
373
  }
512
374
 
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
-
534
- /* ─── Field group ───────────────────────────────────────────────── */
535
- function fieldIdFromPath(path) {
536
- return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
537
- }
538
-
539
- function createFieldGroup(label, isRequired, inline) {
540
- var wrapper = document.createElement("div");
541
- wrapper.className = "caret-field-group" + (inline ? " caret-field-inline" : "");
542
-
543
- var labelEl = document.createElement("label");
544
- labelEl.className = "studio-label";
545
- labelEl.textContent = label;
546
- if (isRequired === false) {
547
- var opt = document.createElement("span");
548
- opt.textContent = " (optional)";
549
- opt.style.cssText = "color:var(--studio-text-dim);font-weight:400;text-transform:none;letter-spacing:normal;";
550
- labelEl.appendChild(opt);
551
- }
552
- wrapper.appendChild(labelEl);
553
-
554
- // Once children are populated, link the label to the first form control
555
- // for screen-reader accessibility. Path is read from data-fieldPath
556
- // (set by the caller after createFieldGroup returns).
557
- queueMicrotask(function () {
558
- var path = wrapper.dataset.fieldPath;
559
- if (!path) return;
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
- });
566
- if (!control) return;
567
- var fid = fieldIdFromPath(path);
568
- control.id = fid;
569
- control.name = path;
570
- labelEl.htmlFor = control.id;
571
- });
572
-
573
- return wrapper;
574
- }
575
-
576
- /* ─── Schema-driven rendering ───────────────────────────────────── */
577
- function renderFields(container, data, schema, basePath) {
578
- if (!schema || !schema.properties) {
579
- renderFieldsFallback(container, data, basePath);
580
- return;
581
- }
582
-
583
- var required = new Set(schema.required || []);
584
-
585
- Object.keys(schema.properties).forEach(function (key) {
586
- var prop = schema.properties[key];
587
- var path = basePath ? basePath + "." + key : key;
588
- var label = prop.title || humanizeKey(key);
589
- var isRequired = required.has(key);
590
- var value = data ? data[key] : undefined;
375
+ /* ─── Field rendering ───────────────────────────────────────────── */
591
376
 
592
- // Backfill missing keys with sensible defaults
593
- if (value === undefined && data) {
594
- data[key] = templateFromSchema(prop);
595
- }
596
- var current = data ? data[key] : undefined;
597
-
598
- var group;
599
-
600
- if (prop.type === "string" && prop.enum) {
601
- group = createFieldGroup(label, isRequired);
602
- group.dataset.fieldPath = path;
603
- var select = document.createElement("select");
604
- select.className = "studio-input";
605
- prop.enum.forEach(function (opt) {
606
- var option = document.createElement("option");
607
- option.value = opt;
608
- option.textContent = String(opt).charAt(0).toUpperCase() + String(opt).slice(1);
609
- if (opt === current) option.selected = true;
610
- select.appendChild(option);
611
- });
612
- select.addEventListener("change", function () { updateField(path, select.value); });
613
- group.appendChild(select);
614
- } else if (prop.type === "string" && prop.format === "image") {
615
- group = createFieldGroup(label, isRequired);
616
- group.dataset.fieldPath = path;
617
- group.appendChild(buildSingleImage(current || "", path));
618
- } else if (prop.type === "string" && (prop.format === "textarea" || prop.format === "html")) {
619
- group = createFieldGroup(label, isRequired);
620
- group.dataset.fieldPath = path;
621
- var ta = document.createElement("textarea");
622
- ta.className = "studio-input";
623
- ta.rows = 4;
624
- ta.value = current || "";
625
- ta.addEventListener("input", function () { updateField(path, ta.value); });
626
- group.appendChild(ta);
627
- } else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri" || prop.format === "url")) {
628
- group = createFieldGroup(label, isRequired);
629
- group.dataset.fieldPath = path;
630
- var emailInput = document.createElement("input");
631
- emailInput.type = prop.format === "email" ? "email" : "url";
632
- emailInput.className = "studio-input";
633
- emailInput.value = current || "";
634
- emailInput.addEventListener("input", function () { updateField(path, emailInput.value); });
635
- group.appendChild(emailInput);
636
- } else if (prop.type === "string") {
637
- group = createFieldGroup(label, isRequired);
638
- group.dataset.fieldPath = path;
639
- var input;
640
- if (typeof current === "string" && current.length > 100) {
641
- input = document.createElement("textarea");
642
- input.rows = 4;
643
- } else {
644
- input = document.createElement("input");
645
- input.type = "text";
646
- }
647
- input.className = "studio-input";
648
- input.value = current || "";
649
- input.addEventListener("input", function () { updateField(path, input.value); });
650
- group.appendChild(input);
651
- } else if (prop.type === "number" || prop.type === "integer") {
652
- group = createFieldGroup(label, isRequired);
653
- group.dataset.fieldPath = path;
654
- var num = document.createElement("input");
655
- num.type = "number";
656
- num.className = "studio-input";
657
- num.value = (current == null ? 0 : current);
658
- if (prop.minimum !== undefined) num.min = prop.minimum;
659
- if (prop.maximum !== undefined) num.max = prop.maximum;
660
- num.addEventListener("input", function () { updateField(path, Number(num.value) || 0); });
661
- group.appendChild(num);
662
- } else if (prop.type === "boolean") {
663
- group = createFieldGroup(label, isRequired, true);
664
- group.dataset.fieldPath = path;
665
- var toggle = document.createElement("button");
666
- toggle.type = "button";
667
- toggle.className = "studio-toggle" + (current ? " active" : "");
668
- toggle.addEventListener("click", function () {
669
- var next = !toggle.classList.contains("active");
670
- toggle.classList.toggle("active");
671
- updateField(path, next);
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
- });
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
- }
687
- } else if (prop.type === "array" && prop.format === "image-gallery") {
688
- group = createFieldGroup(label, isRequired);
689
- group.dataset.fieldPath = path;
690
- group.appendChild(buildImageGallery(current || [], path));
691
- } else if (prop.type === "array" && prop.items && prop.items.type === "object") {
692
- group = createFieldGroup(label, isRequired);
693
- group.dataset.fieldPath = path;
694
- group.appendChild(buildObjectArray(current || [], path, prop.items));
695
- } else if (prop.type === "array" && prop.items && prop.items.type === "string") {
696
- group = createFieldGroup(label, isRequired);
697
- group.dataset.fieldPath = path;
698
- group.appendChild(buildTagList(current || [], path));
699
- } else if (prop.type === "object" && prop.properties) {
700
- group = createFieldGroup(label, isRequired);
701
- group.dataset.fieldPath = path;
702
- var nested = document.createElement("div");
703
- nested.className = "caret-nested";
704
- renderFields(nested, current || {}, prop, path);
705
- group.appendChild(nested);
706
- } else {
707
- group = createFieldGroup(label, isRequired);
708
- group.dataset.fieldPath = path;
709
- var jsonTa = document.createElement("textarea");
710
- jsonTa.className = "studio-input";
711
- jsonTa.style.fontFamily = "var(--studio-font-mono)";
712
- jsonTa.style.fontSize = "11px";
713
- jsonTa.rows = 4;
714
- jsonTa.value = JSON.stringify(current, null, 2);
715
- jsonTa.addEventListener("input", function () {
716
- try { updateField(path, JSON.parse(jsonTa.value)); } catch (e) { /* ignore until valid */ }
717
- });
718
- group.appendChild(jsonTa);
719
- }
720
-
721
- container.appendChild(group);
722
- });
723
- }
724
-
725
- function renderFieldsFallback(container, data, basePath) {
726
- if (!data || typeof data !== "object") return;
727
- Object.keys(data).forEach(function (key) {
728
- var value = data[key];
729
- var path = basePath ? basePath + "." + key : key;
730
- var label = humanizeKey(key);
731
-
732
- if (key === "images" && Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
733
- var g = createFieldGroup(label, true);
734
- g.dataset.fieldPath = path;
735
- g.appendChild(buildImageGallery(value, path));
736
- container.appendChild(g);
737
- return;
738
- }
739
- if ((key === "image" || key === "thumbnail" || key === "bg_image") && typeof value === "string") {
740
- var g2 = createFieldGroup(label, true);
741
- g2.dataset.fieldPath = path;
742
- g2.appendChild(buildSingleImage(value, path));
743
- container.appendChild(g2);
744
- return;
745
- }
746
- if (Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
747
- var g3 = createFieldGroup(label, true);
748
- g3.dataset.fieldPath = path;
749
- g3.appendChild(buildTagList(value, path));
750
- container.appendChild(g3);
751
- return;
752
- }
753
- if (Array.isArray(value) && value.every(function (v) { return typeof v === "object" && v !== null && !Array.isArray(v); })) {
754
- var g4 = createFieldGroup(label, true);
755
- g4.dataset.fieldPath = path;
756
- g4.appendChild(buildObjectArray(value, path, null));
757
- container.appendChild(g4);
758
- return;
759
- }
760
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
761
- var g5 = createFieldGroup(label, true);
762
- g5.dataset.fieldPath = path;
763
- var nested = document.createElement("div");
764
- nested.className = "caret-nested";
765
- renderFieldsFallback(nested, value, path);
766
- g5.appendChild(nested);
767
- container.appendChild(g5);
768
- return;
769
- }
770
- if (typeof value === "boolean") {
771
- var g6 = createFieldGroup(label, true, true);
772
- g6.dataset.fieldPath = path;
773
- var t = document.createElement("button");
774
- t.type = "button";
775
- t.className = "studio-toggle" + (value ? " active" : "");
776
- t.addEventListener("click", function () {
777
- var next = !t.classList.contains("active");
778
- t.classList.toggle("active");
779
- updateField(path, next);
780
- });
781
- g6.appendChild(t);
782
- container.appendChild(g6);
783
- return;
784
- }
785
- if (typeof value === "number") {
786
- var g7 = createFieldGroup(label, true);
787
- g7.dataset.fieldPath = path;
788
- var n = document.createElement("input");
789
- n.type = "number";
790
- n.className = "studio-input";
791
- n.value = value;
792
- n.addEventListener("input", function () { updateField(path, Number(n.value) || 0); });
793
- g7.appendChild(n);
794
- container.appendChild(g7);
795
- return;
796
- }
797
- if (typeof value === "string") {
798
- var g8 = createFieldGroup(label, true);
799
- g8.dataset.fieldPath = path;
800
- var i;
801
- if (value.length > 100) { i = document.createElement("textarea"); i.rows = 4; }
802
- else { i = document.createElement("input"); i.type = "text"; }
803
- i.className = "studio-input";
804
- i.value = value;
805
- i.addEventListener("input", function () { updateField(path, i.value); });
806
- g8.appendChild(i);
807
- container.appendChild(g8);
808
- return;
809
- }
810
- // Fallback to JSON
811
- var g9 = createFieldGroup(label, true);
812
- g9.dataset.fieldPath = path;
813
- var jt = document.createElement("textarea");
814
- jt.className = "studio-input";
815
- jt.style.fontFamily = "var(--studio-font-mono)";
816
- jt.style.fontSize = "11px";
817
- jt.rows = 4;
818
- jt.value = JSON.stringify(value, null, 2);
819
- jt.addEventListener("input", function () {
820
- try { updateField(path, JSON.parse(jt.value)); } catch (e) { /* ignore */ }
821
- });
822
- g9.appendChild(jt);
823
- container.appendChild(g9);
824
- });
825
- }
377
+ const { renderFields, renderFieldsFallback } = createFieldRenderer({
378
+ updateField,
379
+ buildSingleImage,
380
+ buildImageGallery,
381
+ buildObjectArray,
382
+ buildTagList,
383
+ getPublicationField: () => publicationFieldName,
384
+ });
826
385
 
827
386
  /* ─── Image gallery ─────────────────────────────────────────────── */
387
+ /** @param {unknown} images @param {string} path */
828
388
  function buildImageGallery(images, path) {
829
389
  var wrapper = document.createElement("div");
830
390
 
831
391
  function rerender() {
832
392
  wrapper.innerHTML = "";
833
- var current = getNestedValue(entryData, path) || [];
393
+ var galleryValue = getNestedValue(entryData, path);
394
+ var current = Array.isArray(galleryValue)
395
+ ? galleryValue.filter(function (value) { return typeof value === "string"; })
396
+ : [];
834
397
 
835
398
  if (current.length > 0) {
836
399
  var grid = document.createElement("div");
837
400
  grid.className = "caret-gallery-grid";
401
+ /** @type {number | null} */
838
402
  var dragIdx = null;
839
403
 
840
404
  current.forEach(function (url, idx) {
@@ -845,7 +409,7 @@
845
409
  '<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
846
410
  '<button class="caret-gallery-remove" type="button" title="' + htmlEscape(msg("field.remove", "Remove")) + '">×</button>' +
847
411
  '<span class="caret-gallery-index">' + (idx + 1) + "</span>";
848
- item.querySelector(".caret-gallery-remove").addEventListener("click", function (e) {
412
+ item.querySelector(".caret-gallery-remove")?.addEventListener("click", function (e) {
849
413
  e.stopPropagation();
850
414
  var imgs = current.slice();
851
415
  imgs.splice(idx, 1);
@@ -891,9 +455,10 @@
891
455
  fileInput.setAttribute("aria-label", "Upload images for " + humanizeKey(path.split(".").pop() || "images"));
892
456
  fileInput.style.display = "none";
893
457
 
458
+ /** @param {ArrayLike<File>} files */
894
459
  function handleFiles(files) {
895
- var valid = Array.prototype.filter.call(files, function (f) {
896
- return ["image/jpeg", "image/png", "image/webp", "image/avif"].indexOf(f.type) >= 0;
460
+ var valid = Array.from(files).filter(function (f) {
461
+ return isSupportedImage(f);
897
462
  });
898
463
  if (!valid.length) return;
899
464
  dropzone.innerHTML =
@@ -901,8 +466,11 @@
901
466
  '<p style="margin:0;font-size:0.75rem;color:var(--studio-text-muted);">Uploading ' + valid.length + " image" + (valid.length > 1 ? "s" : "") + "…</p>";
902
467
  dropzone.style.pointerEvents = "none";
903
468
 
904
- var imgs = (getNestedValue(entryData, path) || []).slice();
905
- Promise.allSettled(valid.map(function (f) { return uploadFile(f); })).then(function (results) {
469
+ var existingImages = getNestedValue(entryData, path);
470
+ var imgs = Array.isArray(existingImages)
471
+ ? existingImages.filter(function (value) { return typeof value === "string"; })
472
+ : [];
473
+ Promise.allSettled(valid.map(function (f) { return uploadClient.upload(f); })).then(function (results) {
906
474
  results.forEach(function (r) { if (r.status === "fulfilled") imgs.push(r.value); });
907
475
  updateField(path, imgs);
908
476
  rerender();
@@ -915,10 +483,11 @@
915
483
  dropzone.addEventListener("drop", function (e) {
916
484
  e.preventDefault();
917
485
  dropzone.classList.remove("drag-over");
918
- if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files);
486
+ var files = e.dataTransfer && e.dataTransfer.files;
487
+ if (files && files.length) handleFiles(files);
919
488
  });
920
489
  fileInput.addEventListener("change", function () {
921
- if (fileInput.files.length) handleFiles(fileInput.files);
490
+ if (fileInput.files && fileInput.files.length) handleFiles(fileInput.files);
922
491
  fileInput.value = "";
923
492
  });
924
493
 
@@ -931,13 +500,15 @@
931
500
  }
932
501
 
933
502
  /* ─── Single image ──────────────────────────────────────────────── */
503
+ /** @param {unknown} url @param {string} path */
934
504
  function buildSingleImage(url, path) {
935
505
  var wrapper = document.createElement("div");
936
506
  wrapper.className = "caret-single-image";
937
507
 
938
508
  function rerender() {
939
509
  wrapper.innerHTML = "";
940
- var current = getNestedValue(entryData, path) || "";
510
+ var imageValue = getNestedValue(entryData, path);
511
+ var current = typeof imageValue === "string" ? imageValue : "";
941
512
 
942
513
  var fileInput = document.createElement("input");
943
514
  fileInput.type = "file";
@@ -999,11 +570,12 @@
999
570
  fileInput.addEventListener("change", function () {
1000
571
  var file = fileInput.files && fileInput.files[0];
1001
572
  if (!file) return;
573
+ var selectedFile = file;
1002
574
  uploadBtn.textContent = "Uploading…";
1003
575
  uploadBtn.disabled = true;
1004
- Promise.all([uploadFile(file), readImageDimensions(file)]).then(function (results) {
576
+ Promise.all([uploadClient.upload(selectedFile), uploadClient.readDimensions(selectedFile)]).then(function (results) {
1005
577
  updateField(path, results[0]);
1006
- populateImageObject(path, file, results[1]);
578
+ populateImageObject(path, selectedFile, results[1]);
1007
579
  rerender();
1008
580
  }).catch(function () {
1009
581
  help.textContent = "Upload failed. Check the image type and try again.";
@@ -1026,12 +598,16 @@
1026
598
  }
1027
599
 
1028
600
  /* ─── Tag list ──────────────────────────────────────────────────── */
601
+ /** @param {unknown} items @param {string} path */
1029
602
  function buildTagList(items, path) {
1030
603
  var wrapper = document.createElement("div");
1031
604
 
1032
605
  function rerender() {
1033
606
  wrapper.innerHTML = "";
1034
- var current = getNestedValue(entryData, path) || [];
607
+ var tagValue = getNestedValue(entryData, path);
608
+ var current = Array.isArray(tagValue)
609
+ ? tagValue.filter(function (value) { return typeof value === "string"; })
610
+ : [];
1035
611
 
1036
612
  if (current.length > 0) {
1037
613
  var tagsEl = document.createElement("div");
@@ -1091,11 +667,14 @@
1091
667
  }
1092
668
 
1093
669
  /* ─── Object array ──────────────────────────────────────────────── */
670
+ /** @param {unknown} items @param {string} path @param {Schema | null | undefined} itemSchema */
1094
671
  function buildObjectArray(items, path, itemSchema) {
1095
672
  var wrapper = document.createElement("div");
1096
673
  wrapper.className = "caret-object-array";
674
+ /** @type {number | null} */
1097
675
  var draggedIndex = null;
1098
676
 
677
+ /** @param {Record<string, unknown>} item @param {number} idx */
1099
678
  function itemSummary(item, idx) {
1100
679
  var summaryKeys = ["title", "label", "name", "alt", "src"];
1101
680
  for (var i = 0; i < summaryKeys.length; i++) {
@@ -1111,6 +690,7 @@
1111
690
  return singularItemLabel(path).replace(/^./, function (c) { return c.toUpperCase(); }) + " " + (idx + 1);
1112
691
  }
1113
692
 
693
+ /** @param {Record<string, unknown>[]} current @param {number} from @param {number} to */
1114
694
  function moveItem(current, from, to) {
1115
695
  if (to < 0 || to >= current.length || from === to) return;
1116
696
  var next = current.slice();
@@ -1122,7 +702,10 @@
1122
702
 
1123
703
  function rerender() {
1124
704
  wrapper.innerHTML = "";
1125
- var current = getNestedValue(entryData, path) || [];
705
+ var arrayValue = getNestedValue(entryData, path);
706
+ var current = Array.isArray(arrayValue)
707
+ ? arrayValue.filter(function (value) { return value !== null && typeof value === "object" && !Array.isArray(value); })
708
+ : [];
1126
709
 
1127
710
  current.forEach(function (item, idx) {
1128
711
  var card = document.createElement("div");
@@ -1235,9 +818,13 @@
1235
818
  addBtn.className = "caret-object-array-add";
1236
819
  addBtn.textContent = "+ " + msg("field.add", "Add") + " " + singularItemLabel(path);
1237
820
  addBtn.addEventListener("click", function () {
821
+ /** @type {Record<string, unknown>} */
1238
822
  var template;
1239
823
  if (itemSchema && itemSchema.properties) {
1240
- template = templateFromSchema(itemSchema);
824
+ var templateValue = templateFromSchema(itemSchema);
825
+ template = templateValue && typeof templateValue === "object" && !Array.isArray(templateValue)
826
+ ? /** @type {Record<string, unknown>} */ (templateValue)
827
+ : {};
1241
828
  if (Object.prototype.hasOwnProperty.call(template, "id") && !template.id) {
1242
829
  template.id = generatedId(path, current.length);
1243
830
  }
@@ -1257,7 +844,7 @@
1257
844
  // Focus immediately. Deferring focus to the next animation frame can
1258
845
  // steal it back after a fast user (or assistive automation) has already
1259
846
  // moved to another control in the new row.
1260
- if (firstControl) firstControl.focus();
847
+ if (firstControl instanceof HTMLElement) firstControl.focus();
1261
848
  requestAnimationFrame(function () {
1262
849
  if (newest) newest.scrollIntoView({ behavior: "smooth", block: "nearest" });
1263
850
  });
@@ -1273,16 +860,13 @@
1273
860
  function loadHistory() {
1274
861
  historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-text-dim);margin:0;">Loading…</p>';
1275
862
  historyPanel.hidden = false;
863
+ historyBtn.setAttribute("aria-expanded", "true");
864
+ historyCloseBtn.focus();
1276
865
 
1277
- fetch(API + "/history?collection=" + encodeURIComponent(COLLECTION) + "&id=" + encodeURIComponent(ID), {
1278
- credentials: "same-origin",
1279
- }).then(function (res) {
1280
- if (handleAuth(res)) return null;
1281
- if (!res.ok) throw new Error("history failed");
1282
- return res.json();
1283
- }).then(function (json) {
1284
- if (!json) return;
1285
- var items = json.history || [];
866
+ historyClient.list({ collection: COLLECTION, id: ID }).then(function (result) {
867
+ if (result.kind === "unauthorized") { loginRedirect(); return; }
868
+ if (result.kind === "error") throw new Error("history failed");
869
+ var items = result.items;
1286
870
  if (items.length === 0) {
1287
871
  historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-text-dim);margin:0;">No history yet. Edits create snapshots automatically.</p>';
1288
872
  return;
@@ -1330,25 +914,28 @@
1330
914
  });
1331
915
  }
1332
916
 
917
+ function closeHistoryPanel() {
918
+ historyPanel.hidden = true;
919
+ historyBtn.setAttribute("aria-expanded", "false");
920
+ historyList.innerHTML = "";
921
+ if (historyBtn.isConnected) historyBtn.focus();
922
+ }
923
+
924
+ /** @param {number} ts */
1333
925
  function restoreSnapshot(ts) {
1334
926
  if (!window.confirm("Restore this version? Current changes will be saved to history first.")) return;
1335
- fetch(API + "/history", {
1336
- method: "POST",
1337
- headers: { "Content-Type": "application/json", "x-caret-request": "1" },
1338
- credentials: "same-origin",
1339
- body: JSON.stringify({ collection: COLLECTION, id: ID, ts: ts }),
1340
- }).then(function (res) {
1341
- if (handleAuth(res)) return null;
1342
- if (!res.ok) throw new Error("restore failed");
1343
- return res.json();
1344
- }).then(function (json) {
1345
- if (!json) return;
927
+ historyClient.restore({ collection: COLLECTION, id: ID, ts: ts }).then(function (result) {
928
+ if (result.kind === "unauthorized") { loginRedirect(); return; }
929
+ if (result.kind === "error") throw new Error("restore failed");
1346
930
  var prevData = entryData;
1347
931
  var prevJson = originalJson;
1348
932
  try {
1349
- entryData = deepClone(json.data);
1350
- originalJson = JSON.stringify(json.data);
1351
- if (typeof json.revision === "number") entryRevision = json.revision;
933
+ if (!result.data || typeof result.data !== "object" || Array.isArray(result.data)) {
934
+ throw new Error("invalid restored entry data");
935
+ }
936
+ entryData = deepClone(/** @type {Record<string, unknown>} */ (result.data));
937
+ originalJson = JSON.stringify(result.data);
938
+ if (typeof result.revision === "number") entryRevision = result.revision;
1352
939
  fieldsEl.innerHTML = "";
1353
940
  renderFields(fieldsEl, entryData, entrySchema, "");
1354
941
  updateSaveButton();
@@ -1361,8 +948,8 @@
1361
948
  showStatus("error", "Restore failed — invalid data");
1362
949
  return;
1363
950
  }
1364
- historyPanel.hidden = true;
1365
- announceChange("cms:saved", entryData);
951
+ closeHistoryPanel();
952
+ studioSync.announceChange("cms:saved", entryData);
1366
953
  showStatus("saved", "Restored");
1367
954
  }).catch(function () {
1368
955
  showStatus("error", "Restore failed");
@@ -1373,20 +960,26 @@
1373
960
  saveBtn.addEventListener("click", save);
1374
961
  if (deleteBtn) deleteBtn.addEventListener("click", deleteEntry);
1375
962
  historyBtn.addEventListener("click", loadHistory);
1376
- historyCloseBtn.addEventListener("click", function () {
1377
- historyPanel.hidden = true;
1378
- historyList.innerHTML = "";
1379
- });
1380
- deleteCancelBtn.addEventListener("click", function () { deleteDialog.hidden = true; });
963
+ historyCloseBtn.addEventListener("click", closeHistoryPanel);
964
+ deleteCancelBtn.addEventListener("click", closeDeleteDialog);
1381
965
  deleteConfirmBtn.addEventListener("click", confirmDelete);
1382
966
  deleteDialog.addEventListener("click", function (e) {
1383
- if (e.target === deleteDialog) deleteDialog.hidden = true;
967
+ if (e.target === deleteDialog) closeDeleteDialog();
1384
968
  });
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);
969
+ deleteDialog.addEventListener("keydown", function (e) {
970
+ if (deleteDialog.hidden) return;
971
+ if (e.key !== "Tab") return;
972
+ var controls = Array.from(deleteDialog.querySelectorAll("button:not([disabled])"));
973
+ var first = controls[0];
974
+ var last = controls[controls.length - 1];
975
+ if (!(first instanceof HTMLElement) || !(last instanceof HTMLElement)) return;
976
+ var active = document.activeElement;
977
+ if (e.shiftKey && (active === first || !deleteDialog.contains(active))) {
978
+ e.preventDefault();
979
+ last.focus();
980
+ } else if (!e.shiftKey && active === last) {
981
+ e.preventDefault();
982
+ first.focus();
1390
983
  }
1391
984
  });
1392
985
 
@@ -1398,109 +991,80 @@
1398
991
  ? event.target.closest("input, select, textarea, button")
1399
992
  : null;
1400
993
  var group = control && control.closest("[data-field-path]");
1401
- var path = group && group.dataset.fieldPath;
994
+ var path = group instanceof HTMLElement ? group.dataset.fieldPath : undefined;
1402
995
  if (!path) return;
996
+ if (!group) return;
1403
997
  showLinkedSelection(group);
1404
- announceFieldSelection(path);
998
+ studioSync.announceFieldSelection(path);
1405
999
  });
1406
1000
 
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
-
1421
1001
  window.addEventListener("keydown", function (e) {
1002
+ if (e.key === "Escape" && (!deleteDialog.hidden || !historyPanel.hidden)) {
1003
+ e.preventDefault();
1004
+ e.stopPropagation();
1005
+ if (!deleteDialog.hidden) closeDeleteDialog();
1006
+ else closeHistoryPanel();
1007
+ return;
1008
+ }
1422
1009
  if ((e.metaKey || e.ctrlKey) && e.key === "s") {
1423
1010
  e.preventDefault();
1424
1011
  if (isDirty()) save();
1425
1012
  }
1426
- if (e.key === "Escape") {
1427
- if (!deleteDialog.hidden) deleteDialog.hidden = true;
1428
- if (!historyPanel.hidden) {
1429
- historyPanel.hidden = true;
1430
- historyList.innerHTML = "";
1431
- }
1432
- }
1433
- });
1013
+ }, true);
1434
1014
 
1435
1015
  window.addEventListener("beforeunload", function (e) {
1436
1016
  if (isDirty()) { e.preventDefault(); e.returnValue = ""; }
1437
1017
  });
1438
1018
 
1439
1019
  /* ─── Load entry + schema ───────────────────────────────────────── */
1440
- Promise.all([
1441
- fetch(API + "/entries?collection=" + encodeURIComponent(COLLECTION) + "&id=" + encodeURIComponent(ID), { credentials: "same-origin" }),
1442
- fetch(API + "/schema?collection=" + encodeURIComponent(COLLECTION), { credentials: "same-origin" }),
1443
- ]).then(function (results) {
1444
- var entryRes = results[0];
1445
- var schemaRes = results[1];
1446
-
1447
- if (handleAuth(entryRes)) return;
1448
- if (!entryRes.ok) throw new Error("load failed");
1449
-
1450
- return entryRes.json().then(function (entryJson) {
1451
- var entry = (entryJson && entryJson.entries) ? entryJson.entries[0] : null;
1452
-
1453
- var schemaPromise = schemaRes.ok ? schemaRes.json() : Promise.resolve(null);
1454
- return schemaPromise.then(function (schemaJson) {
1455
- if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
1456
-
1020
+ entryLoader.load({
1021
+ collection: COLLECTION,
1022
+ id: ID,
1023
+ isNew: IS_NEW,
1024
+ initializeIfMissing: INITIALIZE_IF_MISSING,
1025
+ }).then(function (result) {
1026
+ if (result.kind === "unauthorized") { loginRedirect(); return; }
1027
+ if (result.kind === "error") {
1028
+ throw new Error(msg("entry." + result.code, msg("entry.loadFailed", "Could not load this entry. Retry after checking its source or storage.")));
1029
+ }
1030
+ entrySchema = result.schema;
1031
+ publicationFieldName = result.publicationFieldName;
1457
1032
  loadingEl.hidden = true;
1458
- var initializedMissingEntry = !entry && (IS_NEW || INITIALIZE_IF_MISSING);
1459
- if (!entry && !initializedMissingEntry) {
1033
+ if (result.kind === "missing") {
1460
1034
  notFoundEl.hidden = false;
1461
1035
  return;
1462
1036
  }
1463
- var initialData = entry
1464
- ? entry.data
1465
- : (schemaJson && schemaJson.template) || templateFromSchema(entrySchema);
1037
+ var initialData = result.data;
1466
1038
  entryData = deepClone(initialData);
1467
- entryRevision = entry && typeof entry.revision === "number" ? entry.revision : 0;
1039
+ entryRevision = result.revision;
1468
1040
  originalJson = JSON.stringify(initialData);
1469
1041
  titleEl.textContent = getTitle(entryData);
1470
1042
  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) {
1043
+ if (validationWarningEl && result.validationIssues.length > 0) {
1044
+ validationWarningEl.textContent = "This stored entry has invalid fields: " + result.validationIssues.map(function (issue) {
1473
1045
  return (issue.path || "entry") + " — " + issue.message;
1474
1046
  }).join("; ") + ". Correct them and Save to restore public delivery.";
1475
1047
  validationWarningEl.hidden = false;
1476
1048
  }
1477
1049
  updateSaveButton();
1478
- if (IS_NEW || initializedMissingEntry) {
1050
+ if (IS_NEW || result.initialized) {
1479
1051
  var guideTitle = document.getElementById("editor-guide-title");
1480
1052
  var guideCopy = document.getElementById("editor-guide-copy");
1481
1053
  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.");
1054
+ 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. When publication is enabled, new entries stay private until Published is turned on.");
1483
1055
  }
1484
1056
  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
- }
1057
+ studioSync.announceReady();
1495
1058
  if (pendingRemoteSelection) {
1496
1059
  var pending = pendingRemoteSelection;
1497
1060
  pendingRemoteSelection = null;
1498
1061
  applyRemoteSelection(pending);
1499
1062
  }
1500
- });
1501
- });
1502
- }).catch(function () {
1063
+ }).catch(function (error) {
1503
1064
  loadingEl.hidden = true;
1504
- notFoundEl.hidden = false;
1065
+ notFoundEl.hidden = true;
1066
+ /** @type {HTMLElement} */ (document.getElementById("load-error")).hidden = false;
1067
+ /** @type {HTMLElement} */ (document.getElementById("load-error-message")).textContent = error instanceof Error ? error.message : msg("entry.loadFailed", "Could not load this entry.");
1505
1068
  });
1069
+ /** @type {HTMLElement} */ (document.getElementById("load-error-retry")).addEventListener("click", function () { window.location.reload(); });
1506
1070
  })();