@caretcms/core 0.1.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 (249) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/dist/browser-runtime.d.ts +12 -0
  4. package/dist/browser-runtime.d.ts.map +1 -0
  5. package/dist/browser-runtime.js +298 -0
  6. package/dist/browser-runtime.js.map +1 -0
  7. package/dist/index.d.ts +81 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +343 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/loader.d.ts +61 -0
  12. package/dist/loader.d.ts.map +1 -0
  13. package/dist/loader.js +80 -0
  14. package/dist/loader.js.map +1 -0
  15. package/dist/providers/storage/filesystem.d.ts +7 -0
  16. package/dist/providers/storage/filesystem.d.ts.map +1 -0
  17. package/dist/providers/storage/filesystem.js +6 -0
  18. package/dist/providers/storage/filesystem.js.map +1 -0
  19. package/dist/providers/uploads/local.d.ts +6 -0
  20. package/dist/providers/uploads/local.d.ts.map +1 -0
  21. package/dist/providers/uploads/local.js +6 -0
  22. package/dist/providers/uploads/local.js.map +1 -0
  23. package/dist/runtime/auth/cookie-utils.d.ts +3 -0
  24. package/dist/runtime/auth/cookie-utils.d.ts.map +1 -0
  25. package/dist/runtime/auth/cookie-utils.js +33 -0
  26. package/dist/runtime/auth/cookie-utils.js.map +1 -0
  27. package/dist/runtime/auth/demo-session.d.ts +24 -0
  28. package/dist/runtime/auth/demo-session.d.ts.map +1 -0
  29. package/dist/runtime/auth/demo-session.js +42 -0
  30. package/dist/runtime/auth/demo-session.js.map +1 -0
  31. package/dist/runtime/auth/rate-limiter.d.ts +26 -0
  32. package/dist/runtime/auth/rate-limiter.d.ts.map +1 -0
  33. package/dist/runtime/auth/rate-limiter.js +65 -0
  34. package/dist/runtime/auth/rate-limiter.js.map +1 -0
  35. package/dist/runtime/auth/session.d.ts +14 -0
  36. package/dist/runtime/auth/session.d.ts.map +1 -0
  37. package/dist/runtime/auth/session.js +128 -0
  38. package/dist/runtime/auth/session.js.map +1 -0
  39. package/dist/runtime/bind.d.ts +14 -0
  40. package/dist/runtime/bind.d.ts.map +1 -0
  41. package/dist/runtime/bind.js +19 -0
  42. package/dist/runtime/bind.js.map +1 -0
  43. package/dist/runtime/config.d.ts +20 -0
  44. package/dist/runtime/config.d.ts.map +1 -0
  45. package/dist/runtime/config.js +40 -0
  46. package/dist/runtime/config.js.map +1 -0
  47. package/dist/runtime/content.d.ts +28 -0
  48. package/dist/runtime/content.d.ts.map +1 -0
  49. package/dist/runtime/content.js +37 -0
  50. package/dist/runtime/content.js.map +1 -0
  51. package/dist/runtime/index.d.ts +14 -0
  52. package/dist/runtime/index.d.ts.map +1 -0
  53. package/dist/runtime/index.js +16 -0
  54. package/dist/runtime/index.js.map +1 -0
  55. package/dist/runtime/middleware.d.ts +14 -0
  56. package/dist/runtime/middleware.d.ts.map +1 -0
  57. package/dist/runtime/middleware.js +80 -0
  58. package/dist/runtime/middleware.js.map +1 -0
  59. package/dist/runtime/mutations/contracts.d.ts +91 -0
  60. package/dist/runtime/mutations/contracts.d.ts.map +1 -0
  61. package/dist/runtime/mutations/contracts.js +320 -0
  62. package/dist/runtime/mutations/contracts.js.map +1 -0
  63. package/dist/runtime/mutations/engine.d.ts +23 -0
  64. package/dist/runtime/mutations/engine.d.ts.map +1 -0
  65. package/dist/runtime/mutations/engine.js +255 -0
  66. package/dist/runtime/mutations/engine.js.map +1 -0
  67. package/dist/runtime/providers.d.ts +9 -0
  68. package/dist/runtime/providers.d.ts.map +1 -0
  69. package/dist/runtime/providers.js +44 -0
  70. package/dist/runtime/providers.js.map +1 -0
  71. package/dist/runtime/request-context.d.ts +11 -0
  72. package/dist/runtime/request-context.d.ts.map +1 -0
  73. package/dist/runtime/request-context.js +16 -0
  74. package/dist/runtime/request-context.js.map +1 -0
  75. package/dist/runtime/rewrite.d.ts +13 -0
  76. package/dist/runtime/rewrite.d.ts.map +1 -0
  77. package/dist/runtime/rewrite.js +253 -0
  78. package/dist/runtime/rewrite.js.map +1 -0
  79. package/dist/runtime/routes/_helpers.d.ts +17 -0
  80. package/dist/runtime/routes/_helpers.d.ts.map +1 -0
  81. package/dist/runtime/routes/_helpers.js +38 -0
  82. package/dist/runtime/routes/_helpers.js.map +1 -0
  83. package/dist/runtime/routes/admin-entry.d.ts +3 -0
  84. package/dist/runtime/routes/admin-entry.d.ts.map +1 -0
  85. package/dist/runtime/routes/admin-entry.js +170 -0
  86. package/dist/runtime/routes/admin-entry.js.map +1 -0
  87. package/dist/runtime/routes/admin.d.ts +3 -0
  88. package/dist/runtime/routes/admin.d.ts.map +1 -0
  89. package/dist/runtime/routes/admin.js +189 -0
  90. package/dist/runtime/routes/admin.js.map +1 -0
  91. package/dist/runtime/routes/auth-login.d.ts +4 -0
  92. package/dist/runtime/routes/auth-login.d.ts.map +1 -0
  93. package/dist/runtime/routes/auth-login.js +71 -0
  94. package/dist/runtime/routes/auth-login.js.map +1 -0
  95. package/dist/runtime/routes/auth-logout.d.ts +4 -0
  96. package/dist/runtime/routes/auth-logout.d.ts.map +1 -0
  97. package/dist/runtime/routes/auth-logout.js +47 -0
  98. package/dist/runtime/routes/auth-logout.js.map +1 -0
  99. package/dist/runtime/routes/auth-session.d.ts +3 -0
  100. package/dist/runtime/routes/auth-session.d.ts.map +1 -0
  101. package/dist/runtime/routes/auth-session.js +8 -0
  102. package/dist/runtime/routes/auth-session.js.map +1 -0
  103. package/dist/runtime/routes/collection-list.d.ts +3 -0
  104. package/dist/runtime/routes/collection-list.d.ts.map +1 -0
  105. package/dist/runtime/routes/collection-list.js +420 -0
  106. package/dist/runtime/routes/collection-list.js.map +1 -0
  107. package/dist/runtime/routes/collections-metadata.d.ts +3 -0
  108. package/dist/runtime/routes/collections-metadata.d.ts.map +1 -0
  109. package/dist/runtime/routes/collections-metadata.js +17 -0
  110. package/dist/runtime/routes/collections-metadata.js.map +1 -0
  111. package/dist/runtime/routes/editor-assets.d.ts +3 -0
  112. package/dist/runtime/routes/editor-assets.d.ts.map +1 -0
  113. package/dist/runtime/routes/editor-assets.js +73 -0
  114. package/dist/runtime/routes/editor-assets.js.map +1 -0
  115. package/dist/runtime/routes/entries.d.ts +3 -0
  116. package/dist/runtime/routes/entries.d.ts.map +1 -0
  117. package/dist/runtime/routes/entries.js +96 -0
  118. package/dist/runtime/routes/entries.js.map +1 -0
  119. package/dist/runtime/routes/history.d.ts +4 -0
  120. package/dist/runtime/routes/history.d.ts.map +1 -0
  121. package/dist/runtime/routes/history.js +54 -0
  122. package/dist/runtime/routes/history.js.map +1 -0
  123. package/dist/runtime/routes/mutate.d.ts +4 -0
  124. package/dist/runtime/routes/mutate.d.ts.map +1 -0
  125. package/dist/runtime/routes/mutate.js +38 -0
  126. package/dist/runtime/routes/mutate.js.map +1 -0
  127. package/dist/runtime/routes/schema.d.ts +3 -0
  128. package/dist/runtime/routes/schema.d.ts.map +1 -0
  129. package/dist/runtime/routes/schema.js +58 -0
  130. package/dist/runtime/routes/schema.js.map +1 -0
  131. package/dist/runtime/routes/studio-home.d.ts +3 -0
  132. package/dist/runtime/routes/studio-home.d.ts.map +1 -0
  133. package/dist/runtime/routes/studio-home.js +99 -0
  134. package/dist/runtime/routes/studio-home.js.map +1 -0
  135. package/dist/runtime/routes/theme-stylesheet.d.ts +3 -0
  136. package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -0
  137. package/dist/runtime/routes/theme-stylesheet.js +32 -0
  138. package/dist/runtime/routes/theme-stylesheet.js.map +1 -0
  139. package/dist/runtime/routes/upload.d.ts +3 -0
  140. package/dist/runtime/routes/upload.d.ts.map +1 -0
  141. package/dist/runtime/routes/upload.js +33 -0
  142. package/dist/runtime/routes/upload.js.map +1 -0
  143. package/dist/runtime/sanitize-html.d.ts +8 -0
  144. package/dist/runtime/sanitize-html.d.ts.map +1 -0
  145. package/dist/runtime/sanitize-html.js +86 -0
  146. package/dist/runtime/sanitize-html.js.map +1 -0
  147. package/dist/runtime/schema/template.d.ts +15 -0
  148. package/dist/runtime/schema/template.d.ts.map +1 -0
  149. package/dist/runtime/schema/template.js +38 -0
  150. package/dist/runtime/schema/template.js.map +1 -0
  151. package/dist/runtime/schema-registry.d.ts +17 -0
  152. package/dist/runtime/schema-registry.d.ts.map +1 -0
  153. package/dist/runtime/schema-registry.js +16 -0
  154. package/dist/runtime/schema-registry.js.map +1 -0
  155. package/dist/runtime/storage/filesystem-adapter.d.ts +36 -0
  156. package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -0
  157. package/dist/runtime/storage/filesystem-adapter.js +291 -0
  158. package/dist/runtime/storage/filesystem-adapter.js.map +1 -0
  159. package/dist/runtime/storage/filesystem-upload-handler.d.ts +11 -0
  160. package/dist/runtime/storage/filesystem-upload-handler.d.ts.map +1 -0
  161. package/dist/runtime/storage/filesystem-upload-handler.js +20 -0
  162. package/dist/runtime/storage/filesystem-upload-handler.js.map +1 -0
  163. package/dist/runtime/storage/image-validation.d.ts +28 -0
  164. package/dist/runtime/storage/image-validation.d.ts.map +1 -0
  165. package/dist/runtime/storage/image-validation.js +94 -0
  166. package/dist/runtime/storage/image-validation.js.map +1 -0
  167. package/dist/runtime/storage/in-memory-adapter.d.ts +29 -0
  168. package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -0
  169. package/dist/runtime/storage/in-memory-adapter.js +110 -0
  170. package/dist/runtime/storage/in-memory-adapter.js.map +1 -0
  171. package/dist/runtime/storage/quota-upload-handler.d.ts +27 -0
  172. package/dist/runtime/storage/quota-upload-handler.d.ts.map +1 -0
  173. package/dist/runtime/storage/quota-upload-handler.js +34 -0
  174. package/dist/runtime/storage/quota-upload-handler.js.map +1 -0
  175. package/dist/runtime/storage/session-overlay-adapter.d.ts +33 -0
  176. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -0
  177. package/dist/runtime/storage/session-overlay-adapter.js +121 -0
  178. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -0
  179. package/dist/runtime/themes/preset-tokens.d.ts +17 -0
  180. package/dist/runtime/themes/preset-tokens.d.ts.map +1 -0
  181. package/dist/runtime/themes/preset-tokens.js +47 -0
  182. package/dist/runtime/themes/preset-tokens.js.map +1 -0
  183. package/dist/runtime/utils.d.ts +6 -0
  184. package/dist/runtime/utils.d.ts.map +1 -0
  185. package/dist/runtime/utils.js +29 -0
  186. package/dist/runtime/utils.js.map +1 -0
  187. package/dist/runtime/views/studio-layout.d.ts +25 -0
  188. package/dist/runtime/views/studio-layout.d.ts.map +1 -0
  189. package/dist/runtime/views/studio-layout.js +108 -0
  190. package/dist/runtime/views/studio-layout.js.map +1 -0
  191. package/dist/schema-utils.d.ts +16 -0
  192. package/dist/schema-utils.d.ts.map +1 -0
  193. package/dist/schema-utils.js +62 -0
  194. package/dist/schema-utils.js.map +1 -0
  195. package/dist/types.d.ts +76 -0
  196. package/dist/types.d.ts.map +1 -0
  197. package/dist/types.js +2 -0
  198. package/dist/types.js.map +1 -0
  199. package/editor-runtime.d.ts +6 -0
  200. package/editor-runtime.js +2 -0
  201. package/package.json +88 -0
  202. package/static/cms/admin-entry.css +207 -0
  203. package/static/cms/admin-entry.js +1083 -0
  204. package/static/cms/editor/config.js +118 -0
  205. package/static/cms/editor/content-map.css +141 -0
  206. package/static/cms/editor/content-map.js +172 -0
  207. package/static/cms/editor/guards.js +72 -0
  208. package/static/cms/editor/helpers.js +51 -0
  209. package/static/cms/editor/image-edit.js +151 -0
  210. package/static/cms/editor/image-utils.js +84 -0
  211. package/static/cms/editor/image.css +133 -0
  212. package/static/cms/editor/link-follow.js +144 -0
  213. package/static/cms/editor/link-ui.css +199 -0
  214. package/static/cms/editor/linkify.js +55 -0
  215. package/static/cms/editor/panel.css +91 -0
  216. package/static/cms/editor/panel.js +70 -0
  217. package/static/cms/editor/rich-toolbar.css +67 -0
  218. package/static/cms/editor/rich-toolbar.js +200 -0
  219. package/static/cms/editor/sanitize.js +89 -0
  220. package/static/cms/editor/save-queue.js +166 -0
  221. package/static/cms/editor/section-controls/activation.js +10 -0
  222. package/static/cms/editor/section-controls/api.js +87 -0
  223. package/static/cms/editor/section-controls/constants.js +24 -0
  224. package/static/cms/editor/section-controls/index.js +622 -0
  225. package/static/cms/editor/section-controls/model.js +76 -0
  226. package/static/cms/editor/section-controls/mutations.js +112 -0
  227. package/static/cms/editor/section-controls/page-context.js +34 -0
  228. package/static/cms/editor/section-controls/pickers.js +196 -0
  229. package/static/cms/editor/section-controls/reorder.js +92 -0
  230. package/static/cms/editor/section-controls/selection.js +54 -0
  231. package/static/cms/editor/section-controls/spacing-drag.js +83 -0
  232. package/static/cms/editor/section-controls/ui-elements.js +54 -0
  233. package/static/cms/editor/section-controls/utils.js +35 -0
  234. package/static/cms/editor/section-controls.css +349 -0
  235. package/static/cms/editor/section-controls.js +1 -0
  236. package/static/cms/editor/security.js +48 -0
  237. package/static/cms/editor/sync.js +72 -0
  238. package/static/cms/editor/text-edit.js +151 -0
  239. package/static/cms/editor/text-link-interactions.js +254 -0
  240. package/static/cms/editor/toast.css +50 -0
  241. package/static/cms/editor/toast.js +29 -0
  242. package/static/cms/editor/tokens-and-text.css +93 -0
  243. package/static/cms/editor/toolbar.css +249 -0
  244. package/static/cms/editor/toolbar.js +117 -0
  245. package/static/cms/editor.css +12 -0
  246. package/static/cms/editor.js +116 -0
  247. package/static/cms/studio-login.css +300 -0
  248. package/static/cms/studio-tokens.css +85 -0
  249. package/static/cms/studio.css +437 -0
@@ -0,0 +1,1083 @@
1
+ /**
2
+ * Studio entry editor — runtime.
3
+ *
4
+ * Loaded by the /admin/cms/[collection]/[id] route's HTML shell. Reads its
5
+ * config from <script id="caret-entry-config" type="application/json"> and
6
+ * binds to the page's #cms-entry-page root.
7
+ *
8
+ * Field types supported (schema-driven, with typeof fallback):
9
+ * text, email, url, textarea, number, boolean, select(enum),
10
+ * image, image-gallery, tag-list, nested object, array of objects.
11
+ *
12
+ * API surface used:
13
+ * GET ${api}/entries?collection=&id=
14
+ * GET ${api}/schema?collection=
15
+ * POST ${api}/mutate { type: "put_entry" | "delete_entry", ... }
16
+ * POST ${api}/upload FormData(file)
17
+ * GET ${api}/history?collection=&id=
18
+ * POST ${api}/history { collection, id, ts } // restore snapshot
19
+ */
20
+ (function () {
21
+ "use strict";
22
+
23
+ var configEl = document.getElementById("caret-entry-config");
24
+ var pageRoot = document.getElementById("cms-entry-page");
25
+ if (!configEl || !pageRoot) return;
26
+
27
+ var CFG;
28
+ try {
29
+ CFG = JSON.parse(configEl.textContent || "{}");
30
+ } catch (e) {
31
+ console.error("[caret] invalid entry config", e);
32
+ return;
33
+ }
34
+
35
+ var API = CFG.apiBasePath;
36
+ var MOUNT = CFG.mountPath;
37
+ var COLLECTION = CFG.collection;
38
+ var ID = CFG.id;
39
+ if (!API || !COLLECTION || !ID) return;
40
+
41
+ /* ─── State ─────────────────────────────────────────────────────── */
42
+ var entryData = null;
43
+ var entrySchema = null;
44
+ var entryRevision = undefined;
45
+ var originalJson = "";
46
+ var saving = false;
47
+ var dirtyTimer = 0;
48
+
49
+ /* ─── DOM refs ──────────────────────────────────────────────────── */
50
+ var loadingEl = document.getElementById("loading");
51
+ var notFoundEl = document.getElementById("not-found");
52
+ var editorEl = document.getElementById("editor");
53
+ var fieldsEl = document.getElementById("fields");
54
+ var titleEl = document.getElementById("entry-title");
55
+ var statusEl = document.getElementById("status-msg");
56
+ var saveBtn = document.getElementById("btn-save");
57
+ var deleteBtn = document.getElementById("btn-delete");
58
+ var historyBtn = document.getElementById("btn-history");
59
+ var historyPanel = document.getElementById("history-panel");
60
+ var historyList = document.getElementById("history-list");
61
+ var historyCloseBtn = document.getElementById("btn-history-close");
62
+ var deleteDialog = document.getElementById("delete-dialog");
63
+ var deleteNameEl = document.getElementById("delete-entry-name");
64
+ var deleteCancelBtn = document.getElementById("btn-delete-cancel");
65
+ var deleteConfirmBtn = document.getElementById("btn-delete-confirm");
66
+
67
+ /* ─── Helpers ───────────────────────────────────────────────────── */
68
+ function deepClone(v) { return JSON.parse(JSON.stringify(v)); }
69
+
70
+ function getTitle(data) {
71
+ if (!data) return "Untitled";
72
+ var keys = ["name", "title", "question", "company_name", "headline", "label"];
73
+ for (var i = 0; i < keys.length; i++) {
74
+ if (typeof data[keys[i]] === "string" && data[keys[i]]) return data[keys[i]];
75
+ }
76
+ return "Untitled";
77
+ }
78
+
79
+ function humanizeKey(key) {
80
+ return key.replace(/_/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); });
81
+ }
82
+
83
+ function isDirty() {
84
+ return entryData !== null && JSON.stringify(entryData) !== originalJson;
85
+ }
86
+
87
+ function updateSaveButton() {
88
+ var dirty = isDirty();
89
+ saveBtn.disabled = !dirty || saving;
90
+ saveBtn.classList.remove("studio-save-dirty", "studio-save-clean", "studio-save-saving");
91
+ saveBtn.classList.add(saving ? "studio-save-saving" : (dirty ? "studio-save-dirty" : "studio-save-clean"));
92
+ saveBtn.textContent = saving ? "Saving…" : "Save";
93
+ titleEl.textContent = getTitle(entryData);
94
+ }
95
+
96
+ function showStatus(type, msg) {
97
+ statusEl.textContent = msg;
98
+ statusEl.hidden = false;
99
+ statusEl.style.color = type === "saved" ? "var(--studio-green)" : "var(--studio-red)";
100
+ if (type === "saved") {
101
+ setTimeout(function () { statusEl.hidden = true; }, 2000);
102
+ }
103
+ }
104
+
105
+ function setNestedValue(obj, path, value) {
106
+ var keys = path.split(".");
107
+ var cur = obj;
108
+ for (var i = 0; i < keys.length - 1; i++) {
109
+ var k = keys[i];
110
+ if (typeof cur[k] !== "object" || cur[k] === null) {
111
+ cur[k] = /^\d+$/.test(keys[i + 1]) ? [] : {};
112
+ }
113
+ cur = cur[k];
114
+ }
115
+ cur[keys[keys.length - 1]] = value;
116
+ }
117
+
118
+ function getNestedValue(obj, path) {
119
+ return path.split(".").reduce(function (o, k) { return o == null ? undefined : o[k]; }, obj);
120
+ }
121
+
122
+ function updateField(path, value) {
123
+ setNestedValue(entryData, path, value);
124
+ if (!saving) {
125
+ saveBtn.disabled = false;
126
+ saveBtn.classList.remove("studio-save-clean", "studio-save-saving");
127
+ saveBtn.classList.add("studio-save-dirty");
128
+ }
129
+ clearTimeout(dirtyTimer);
130
+ dirtyTimer = setTimeout(updateSaveButton, 300);
131
+ }
132
+
133
+ function loginRedirect() {
134
+ window.location.href = MOUNT + "?redirect=" + encodeURIComponent(window.location.pathname);
135
+ }
136
+
137
+ function handleAuth(res) {
138
+ if (res.status === 401) { loginRedirect(); return true; }
139
+ return false;
140
+ }
141
+
142
+ /* ─── Image compression + upload ────────────────────────────────── */
143
+ function compressImage(file, maxWidth, quality) {
144
+ maxWidth = maxWidth || 1600;
145
+ quality = quality || 0.82;
146
+ return Promise.resolve().then(function () {
147
+ if (file.size < 200000 && file.type === "image/webp") return file;
148
+ return createImageBitmap(file).then(function (bitmap) {
149
+ var width = bitmap.width;
150
+ var height = bitmap.height;
151
+ var needsResize = width > maxWidth;
152
+ if (file.type === "image/png" && !needsResize) {
153
+ bitmap.close();
154
+ return file;
155
+ }
156
+ if (needsResize) {
157
+ height = Math.round((height * maxWidth) / width);
158
+ width = maxWidth;
159
+ }
160
+ var canvas = document.createElement("canvas");
161
+ canvas.width = width;
162
+ canvas.height = height;
163
+ canvas.getContext("2d").drawImage(bitmap, 0, 0, width, height);
164
+ bitmap.close();
165
+ var isPng = file.type === "image/png";
166
+ var outType = isPng ? "image/png" : "image/webp";
167
+ return new Promise(function (resolve) {
168
+ canvas.toBlob(function (blob) {
169
+ if (!blob || blob.size >= file.size) return resolve(file);
170
+ var ext = isPng ? ".png" : ".webp";
171
+ resolve(new File([blob], file.name.replace(/\.[^.]+$/, ext), { type: outType }));
172
+ }, outType, isPng ? undefined : quality);
173
+ });
174
+ });
175
+ });
176
+ }
177
+
178
+ function uploadFile(file) {
179
+ return compressImage(file).then(function (compressed) {
180
+ var fd = new FormData();
181
+ fd.append("file", compressed);
182
+ return fetch(API + "/upload", {
183
+ method: "POST",
184
+ body: fd,
185
+ headers: { "x-caret-request": "1" },
186
+ credentials: "same-origin",
187
+ });
188
+ }).then(function (res) {
189
+ if (handleAuth(res)) throw new Error("Unauthorized");
190
+ if (!res.ok) throw new Error("Upload failed");
191
+ return res.json();
192
+ }).then(function (json) { return json.url; });
193
+ }
194
+
195
+ /* ─── Save / Delete ─────────────────────────────────────────────── */
196
+ function save() {
197
+ if (!entryData || saving) return;
198
+ saving = true;
199
+ updateSaveButton();
200
+ var payload = {
201
+ type: "put_entry",
202
+ collection: COLLECTION,
203
+ id: ID,
204
+ data: entryData,
205
+ };
206
+ if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
207
+
208
+ fetch(API + "/mutate", {
209
+ method: "POST",
210
+ headers: { "Content-Type": "application/json", "x-caret-request": "1" },
211
+ credentials: "same-origin",
212
+ body: JSON.stringify(payload),
213
+ }).then(function (res) {
214
+ if (handleAuth(res)) return null;
215
+ return res.json().catch(function () { return null; }).then(function (body) {
216
+ return { ok: res.ok, status: res.status, body: body };
217
+ });
218
+ }).then(function (result) {
219
+ if (!result) return;
220
+ if (!result.ok) {
221
+ if (result.body && result.body.issues) {
222
+ showValidationErrors(result.body.issues);
223
+ showStatus("error", "Validation failed (" + result.body.issues.length + ")");
224
+ var firstErr = fieldsEl.querySelector(".studio-field-error");
225
+ if (firstErr) firstErr.scrollIntoView({ behavior: "smooth", block: "center" });
226
+ return;
227
+ }
228
+ if (result.status === 409 && result.body && typeof result.body.currentRevision === "number") {
229
+ entryRevision = result.body.currentRevision;
230
+ showStatus("error", "Conflict — reload to resolve");
231
+ return;
232
+ }
233
+ throw new Error("save failed");
234
+ }
235
+ clearFieldErrors();
236
+ if (result.body && typeof result.body.revision === "number") entryRevision = result.body.revision;
237
+ originalJson = JSON.stringify(entryData);
238
+ if (window.parent !== window) {
239
+ try {
240
+ window.parent.postMessage(
241
+ { type: "cms:saved", collection: COLLECTION, id: ID, data: deepClone(entryData) },
242
+ window.location.origin,
243
+ );
244
+ } catch (e) { /* noop */ }
245
+ }
246
+ showStatus("saved", "Saved");
247
+ }).catch(function () {
248
+ showStatus("error", "Error");
249
+ }).then(function () {
250
+ saving = false;
251
+ updateSaveButton();
252
+ });
253
+ }
254
+
255
+ function deleteEntry() {
256
+ deleteNameEl.textContent = ID;
257
+ deleteDialog.hidden = false;
258
+ }
259
+
260
+ function confirmDelete() {
261
+ deleteConfirmBtn.disabled = true;
262
+ deleteConfirmBtn.textContent = "Deleting…";
263
+ var payload = { type: "delete_entry", collection: COLLECTION, id: ID };
264
+ if (typeof entryRevision === "number") payload.expectedRevision = entryRevision;
265
+ fetch(API + "/mutate", {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/json", "x-caret-request": "1" },
268
+ credentials: "same-origin",
269
+ body: JSON.stringify(payload),
270
+ }).then(function (res) {
271
+ if (handleAuth(res)) return;
272
+ if (!res.ok) throw new Error("delete failed");
273
+ window.location.href = MOUNT + "/cms/" + encodeURIComponent(COLLECTION);
274
+ }).catch(function () {
275
+ deleteDialog.hidden = true;
276
+ deleteConfirmBtn.disabled = false;
277
+ deleteConfirmBtn.textContent = "Delete";
278
+ showStatus("error", "Delete failed");
279
+ });
280
+ }
281
+
282
+ /* ─── Validation rendering ──────────────────────────────────────── */
283
+ function showFieldError(group, message) {
284
+ var errEl = group.querySelector(".studio-error-text");
285
+ if (message) {
286
+ if (!errEl) {
287
+ errEl = document.createElement("p");
288
+ errEl.className = "studio-error-text";
289
+ group.appendChild(errEl);
290
+ }
291
+ errEl.textContent = message;
292
+ errEl.hidden = false;
293
+ group.classList.add("studio-field-error");
294
+ } else if (errEl) {
295
+ errEl.remove();
296
+ group.classList.remove("studio-field-error");
297
+ }
298
+ }
299
+
300
+ function clearFieldErrors() {
301
+ fieldsEl.querySelectorAll(".studio-error-text").forEach(function (el) { el.remove(); });
302
+ fieldsEl.querySelectorAll(".caret-field-group").forEach(function (el) { el.classList.remove("studio-field-error"); });
303
+ }
304
+
305
+ function showValidationErrors(issues) {
306
+ clearFieldErrors();
307
+ issues.forEach(function (issue) {
308
+ var group = fieldsEl.querySelector('[data-field-path="' + cssEscape(issue.path) + '"]');
309
+ if (group) showFieldError(group, issue.message);
310
+ });
311
+ }
312
+
313
+ function cssEscape(value) {
314
+ if (window.CSS && CSS.escape) return CSS.escape(value);
315
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
316
+ }
317
+
318
+ /* ─── Field group ───────────────────────────────────────────────── */
319
+ function fieldIdFromPath(path) {
320
+ return "caret-field-" + String(path || "").replace(/[^a-zA-Z0-9_-]/g, "-");
321
+ }
322
+
323
+ function createFieldGroup(label, isRequired, inline) {
324
+ var wrapper = document.createElement("div");
325
+ wrapper.className = "caret-field-group" + (inline ? " caret-field-inline" : "");
326
+
327
+ var labelEl = document.createElement("label");
328
+ labelEl.className = "studio-label";
329
+ labelEl.textContent = label;
330
+ if (isRequired === false) {
331
+ var opt = document.createElement("span");
332
+ opt.textContent = " (optional)";
333
+ opt.style.cssText = "color:var(--studio-text-dim);font-weight:400;text-transform:none;letter-spacing:normal;";
334
+ labelEl.appendChild(opt);
335
+ }
336
+ wrapper.appendChild(labelEl);
337
+
338
+ // Once children are populated, link the label to the first form control
339
+ // for screen-reader accessibility. Path is read from data-fieldPath
340
+ // (set by the caller after createFieldGroup returns).
341
+ queueMicrotask(function () {
342
+ var path = wrapper.dataset.fieldPath;
343
+ if (!path) return;
344
+ var control = wrapper.querySelector("input, select, textarea");
345
+ if (!control) return;
346
+ var fid = fieldIdFromPath(path);
347
+ if (!control.id) control.id = fid;
348
+ if (!control.name) control.name = path;
349
+ labelEl.htmlFor = control.id;
350
+ });
351
+
352
+ return wrapper;
353
+ }
354
+
355
+ /* ─── Schema-driven rendering ───────────────────────────────────── */
356
+ function renderFields(container, data, schema, basePath) {
357
+ if (!schema || !schema.properties) {
358
+ renderFieldsFallback(container, data, basePath);
359
+ return;
360
+ }
361
+
362
+ var required = new Set(schema.required || []);
363
+
364
+ Object.keys(schema.properties).forEach(function (key) {
365
+ var prop = schema.properties[key];
366
+ var path = basePath ? basePath + "." + key : key;
367
+ var label = prop.title || humanizeKey(key);
368
+ var isRequired = required.has(key);
369
+ var value = data ? data[key] : undefined;
370
+
371
+ // Backfill missing keys with sensible defaults
372
+ if (value === undefined && data) {
373
+ if (prop.default !== undefined) data[key] = deepClone(prop.default);
374
+ else if (prop.type === "object") data[key] = {};
375
+ else if (prop.type === "array") data[key] = [];
376
+ else if (prop.type === "boolean") data[key] = false;
377
+ else if (prop.type === "number") data[key] = 0;
378
+ else if (prop.type === "string" && prop.enum) data[key] = prop.enum[0];
379
+ else if (prop.type === "string") data[key] = "";
380
+ }
381
+ var current = data ? data[key] : undefined;
382
+
383
+ var group;
384
+
385
+ if (prop.type === "string" && prop.enum) {
386
+ group = createFieldGroup(label, isRequired);
387
+ group.dataset.fieldPath = path;
388
+ var select = document.createElement("select");
389
+ select.className = "studio-input";
390
+ prop.enum.forEach(function (opt) {
391
+ var option = document.createElement("option");
392
+ option.value = opt;
393
+ option.textContent = String(opt).charAt(0).toUpperCase() + String(opt).slice(1);
394
+ if (opt === current) option.selected = true;
395
+ select.appendChild(option);
396
+ });
397
+ select.addEventListener("change", function () { updateField(path, select.value); });
398
+ group.appendChild(select);
399
+ } else if (prop.type === "string" && prop.format === "image") {
400
+ group = createFieldGroup(label, isRequired);
401
+ group.dataset.fieldPath = path;
402
+ group.appendChild(buildSingleImage(current || "", path));
403
+ } else if (prop.type === "string" && prop.format === "textarea") {
404
+ group = createFieldGroup(label, isRequired);
405
+ group.dataset.fieldPath = path;
406
+ var ta = document.createElement("textarea");
407
+ ta.className = "studio-input";
408
+ ta.rows = 4;
409
+ ta.value = current || "";
410
+ ta.addEventListener("input", function () { updateField(path, ta.value); });
411
+ group.appendChild(ta);
412
+ } else if (prop.type === "string" && (prop.format === "email" || prop.format === "uri")) {
413
+ group = createFieldGroup(label, isRequired);
414
+ group.dataset.fieldPath = path;
415
+ var emailInput = document.createElement("input");
416
+ emailInput.type = prop.format === "email" ? "email" : "url";
417
+ emailInput.className = "studio-input";
418
+ emailInput.value = current || "";
419
+ emailInput.addEventListener("input", function () { updateField(path, emailInput.value); });
420
+ group.appendChild(emailInput);
421
+ } else if (prop.type === "string") {
422
+ group = createFieldGroup(label, isRequired);
423
+ group.dataset.fieldPath = path;
424
+ var input;
425
+ if (typeof current === "string" && current.length > 100) {
426
+ input = document.createElement("textarea");
427
+ input.rows = 4;
428
+ } else {
429
+ input = document.createElement("input");
430
+ input.type = "text";
431
+ }
432
+ input.className = "studio-input";
433
+ input.value = current || "";
434
+ input.addEventListener("input", function () { updateField(path, input.value); });
435
+ group.appendChild(input);
436
+ } else if (prop.type === "number" || prop.type === "integer") {
437
+ group = createFieldGroup(label, isRequired);
438
+ group.dataset.fieldPath = path;
439
+ var num = document.createElement("input");
440
+ num.type = "number";
441
+ num.className = "studio-input";
442
+ num.value = (current == null ? 0 : current);
443
+ if (prop.minimum !== undefined) num.min = prop.minimum;
444
+ if (prop.maximum !== undefined) num.max = prop.maximum;
445
+ num.addEventListener("input", function () { updateField(path, Number(num.value) || 0); });
446
+ group.appendChild(num);
447
+ } else if (prop.type === "boolean") {
448
+ group = createFieldGroup(label, isRequired, true);
449
+ group.dataset.fieldPath = path;
450
+ var toggle = document.createElement("button");
451
+ toggle.type = "button";
452
+ toggle.className = "studio-toggle" + (current ? " active" : "");
453
+ toggle.addEventListener("click", function () {
454
+ var next = !toggle.classList.contains("active");
455
+ toggle.classList.toggle("active");
456
+ updateField(path, next);
457
+ });
458
+ group.appendChild(toggle);
459
+ } else if (prop.type === "array" && prop.format === "image-gallery") {
460
+ group = createFieldGroup(label, isRequired);
461
+ group.dataset.fieldPath = path;
462
+ group.appendChild(buildImageGallery(current || [], path));
463
+ } else if (prop.type === "array" && prop.items && prop.items.type === "object") {
464
+ group = createFieldGroup(label, isRequired);
465
+ group.dataset.fieldPath = path;
466
+ group.appendChild(buildObjectArray(current || [], path, prop.items));
467
+ } else if (prop.type === "array" && prop.items && prop.items.type === "string") {
468
+ group = createFieldGroup(label, isRequired);
469
+ group.dataset.fieldPath = path;
470
+ group.appendChild(buildTagList(current || [], path));
471
+ } else if (prop.type === "object" && prop.properties) {
472
+ group = createFieldGroup(label, isRequired);
473
+ group.dataset.fieldPath = path;
474
+ var nested = document.createElement("div");
475
+ nested.className = "caret-nested";
476
+ renderFields(nested, current || {}, prop, path);
477
+ group.appendChild(nested);
478
+ } else {
479
+ group = createFieldGroup(label, isRequired);
480
+ group.dataset.fieldPath = path;
481
+ var jsonTa = document.createElement("textarea");
482
+ jsonTa.className = "studio-input";
483
+ jsonTa.style.fontFamily = "var(--studio-font-mono)";
484
+ jsonTa.style.fontSize = "11px";
485
+ jsonTa.rows = 4;
486
+ jsonTa.value = JSON.stringify(current, null, 2);
487
+ jsonTa.addEventListener("input", function () {
488
+ try { updateField(path, JSON.parse(jsonTa.value)); } catch (e) { /* ignore until valid */ }
489
+ });
490
+ group.appendChild(jsonTa);
491
+ }
492
+
493
+ container.appendChild(group);
494
+ });
495
+ }
496
+
497
+ function renderFieldsFallback(container, data, basePath) {
498
+ if (!data || typeof data !== "object") return;
499
+ Object.keys(data).forEach(function (key) {
500
+ var value = data[key];
501
+ var path = basePath ? basePath + "." + key : key;
502
+ var label = humanizeKey(key);
503
+
504
+ if (key === "images" && Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
505
+ var g = createFieldGroup(label, true);
506
+ g.dataset.fieldPath = path;
507
+ g.appendChild(buildImageGallery(value, path));
508
+ container.appendChild(g);
509
+ return;
510
+ }
511
+ if ((key === "image" || key === "thumbnail" || key === "bg_image") && typeof value === "string") {
512
+ var g2 = createFieldGroup(label, true);
513
+ g2.dataset.fieldPath = path;
514
+ g2.appendChild(buildSingleImage(value, path));
515
+ container.appendChild(g2);
516
+ return;
517
+ }
518
+ if (Array.isArray(value) && value.every(function (v) { return typeof v === "string"; })) {
519
+ var g3 = createFieldGroup(label, true);
520
+ g3.dataset.fieldPath = path;
521
+ g3.appendChild(buildTagList(value, path));
522
+ container.appendChild(g3);
523
+ return;
524
+ }
525
+ if (Array.isArray(value) && value.every(function (v) { return typeof v === "object" && v !== null && !Array.isArray(v); })) {
526
+ var g4 = createFieldGroup(label, true);
527
+ g4.dataset.fieldPath = path;
528
+ g4.appendChild(buildObjectArray(value, path, null));
529
+ container.appendChild(g4);
530
+ return;
531
+ }
532
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
533
+ var g5 = createFieldGroup(label, true);
534
+ g5.dataset.fieldPath = path;
535
+ var nested = document.createElement("div");
536
+ nested.className = "caret-nested";
537
+ renderFieldsFallback(nested, value, path);
538
+ g5.appendChild(nested);
539
+ container.appendChild(g5);
540
+ return;
541
+ }
542
+ if (typeof value === "boolean") {
543
+ var g6 = createFieldGroup(label, true, true);
544
+ g6.dataset.fieldPath = path;
545
+ var t = document.createElement("button");
546
+ t.type = "button";
547
+ t.className = "studio-toggle" + (value ? " active" : "");
548
+ t.addEventListener("click", function () {
549
+ var next = !t.classList.contains("active");
550
+ t.classList.toggle("active");
551
+ updateField(path, next);
552
+ });
553
+ g6.appendChild(t);
554
+ container.appendChild(g6);
555
+ return;
556
+ }
557
+ if (typeof value === "number") {
558
+ var g7 = createFieldGroup(label, true);
559
+ g7.dataset.fieldPath = path;
560
+ var n = document.createElement("input");
561
+ n.type = "number";
562
+ n.className = "studio-input";
563
+ n.value = value;
564
+ n.addEventListener("input", function () { updateField(path, Number(n.value) || 0); });
565
+ g7.appendChild(n);
566
+ container.appendChild(g7);
567
+ return;
568
+ }
569
+ if (typeof value === "string") {
570
+ var g8 = createFieldGroup(label, true);
571
+ g8.dataset.fieldPath = path;
572
+ var i;
573
+ if (value.length > 100) { i = document.createElement("textarea"); i.rows = 4; }
574
+ else { i = document.createElement("input"); i.type = "text"; }
575
+ i.className = "studio-input";
576
+ i.value = value;
577
+ i.addEventListener("input", function () { updateField(path, i.value); });
578
+ g8.appendChild(i);
579
+ container.appendChild(g8);
580
+ return;
581
+ }
582
+ // Fallback to JSON
583
+ var g9 = createFieldGroup(label, true);
584
+ g9.dataset.fieldPath = path;
585
+ var jt = document.createElement("textarea");
586
+ jt.className = "studio-input";
587
+ jt.style.fontFamily = "var(--studio-font-mono)";
588
+ jt.style.fontSize = "11px";
589
+ jt.rows = 4;
590
+ jt.value = JSON.stringify(value, null, 2);
591
+ jt.addEventListener("input", function () {
592
+ try { updateField(path, JSON.parse(jt.value)); } catch (e) { /* ignore */ }
593
+ });
594
+ g9.appendChild(jt);
595
+ container.appendChild(g9);
596
+ });
597
+ }
598
+
599
+ /* ─── Image gallery ─────────────────────────────────────────────── */
600
+ function buildImageGallery(images, path) {
601
+ var wrapper = document.createElement("div");
602
+
603
+ function rerender() {
604
+ wrapper.innerHTML = "";
605
+ var current = getNestedValue(entryData, path) || [];
606
+
607
+ if (current.length > 0) {
608
+ var grid = document.createElement("div");
609
+ grid.className = "caret-gallery-grid";
610
+ var dragIdx = null;
611
+
612
+ current.forEach(function (url, idx) {
613
+ var item = document.createElement("div");
614
+ item.className = "studio-gallery-item caret-gallery-cell";
615
+ item.draggable = true;
616
+ item.innerHTML =
617
+ '<img src="' + url + '" alt="Image ' + (idx + 1) + '" loading="lazy" />' +
618
+ '<button class="caret-gallery-remove" type="button" title="Remove">×</button>' +
619
+ '<span class="caret-gallery-index">' + (idx + 1) + "</span>";
620
+ item.querySelector(".caret-gallery-remove").addEventListener("click", function (e) {
621
+ e.stopPropagation();
622
+ var imgs = current.slice();
623
+ imgs.splice(idx, 1);
624
+ updateField(path, imgs);
625
+ rerender();
626
+ });
627
+ item.addEventListener("dragstart", function () { dragIdx = idx; item.classList.add("dragging"); });
628
+ item.addEventListener("dragend", function () { dragIdx = null; item.classList.remove("dragging"); });
629
+ item.addEventListener("dragover", function (e) {
630
+ e.preventDefault();
631
+ if (dragIdx !== null && dragIdx !== idx) item.classList.add("drag-over-item");
632
+ });
633
+ item.addEventListener("dragleave", function () { item.classList.remove("drag-over-item"); });
634
+ item.addEventListener("drop", function (e) {
635
+ e.preventDefault();
636
+ item.classList.remove("drag-over-item");
637
+ if (dragIdx !== null && dragIdx !== idx) {
638
+ var imgs = current.slice();
639
+ var moved = imgs.splice(dragIdx, 1)[0];
640
+ imgs.splice(idx, 0, moved);
641
+ updateField(path, imgs);
642
+ rerender();
643
+ }
644
+ });
645
+ grid.appendChild(item);
646
+ });
647
+ wrapper.appendChild(grid);
648
+ }
649
+
650
+ var dropzone = document.createElement("div");
651
+ dropzone.className = "studio-dropzone caret-dropzone";
652
+ dropzone.innerHTML =
653
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:32px;height:32px;color:var(--studio-text-dim);margin-bottom:0.75rem;">' +
654
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"/>' +
655
+ "</svg>" +
656
+ '<p style="margin:0 0 0.25rem;font-size:0.75rem;color:var(--studio-text-muted);">Drop images here or click to browse</p>' +
657
+ '<p style="margin:0;font-size:0.7rem;color:var(--studio-text-dim);">JPEG, PNG, WebP, AVIF — max 10 MB each</p>';
658
+
659
+ var fileInput = document.createElement("input");
660
+ fileInput.type = "file";
661
+ fileInput.multiple = true;
662
+ fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
663
+ fileInput.style.display = "none";
664
+
665
+ function handleFiles(files) {
666
+ var valid = Array.prototype.filter.call(files, function (f) {
667
+ return ["image/jpeg", "image/png", "image/webp", "image/avif"].indexOf(f.type) >= 0;
668
+ });
669
+ if (!valid.length) return;
670
+ dropzone.innerHTML =
671
+ '<div class="studio-spinner" style="margin-bottom:0.5rem;"></div>' +
672
+ '<p style="margin:0;font-size:0.75rem;color:var(--studio-text-muted);">Uploading ' + valid.length + " image" + (valid.length > 1 ? "s" : "") + "…</p>";
673
+ dropzone.style.pointerEvents = "none";
674
+
675
+ var imgs = (getNestedValue(entryData, path) || []).slice();
676
+ Promise.allSettled(valid.map(function (f) { return uploadFile(f); })).then(function (results) {
677
+ results.forEach(function (r) { if (r.status === "fulfilled") imgs.push(r.value); });
678
+ updateField(path, imgs);
679
+ rerender();
680
+ });
681
+ }
682
+
683
+ dropzone.addEventListener("click", function () { fileInput.click(); });
684
+ dropzone.addEventListener("dragover", function (e) { e.preventDefault(); dropzone.classList.add("drag-over"); });
685
+ dropzone.addEventListener("dragleave", function () { dropzone.classList.remove("drag-over"); });
686
+ dropzone.addEventListener("drop", function (e) {
687
+ e.preventDefault();
688
+ dropzone.classList.remove("drag-over");
689
+ if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files);
690
+ });
691
+ fileInput.addEventListener("change", function () {
692
+ if (fileInput.files.length) handleFiles(fileInput.files);
693
+ fileInput.value = "";
694
+ });
695
+
696
+ wrapper.appendChild(dropzone);
697
+ wrapper.appendChild(fileInput);
698
+ }
699
+
700
+ rerender();
701
+ return wrapper;
702
+ }
703
+
704
+ /* ─── Single image ──────────────────────────────────────────────── */
705
+ function buildSingleImage(url, path) {
706
+ var wrapper = document.createElement("div");
707
+
708
+ function rerender() {
709
+ wrapper.innerHTML = "";
710
+ var current = getNestedValue(entryData, path) || "";
711
+
712
+ if (current) {
713
+ var preview = document.createElement("div");
714
+ preview.className = "caret-single-image-preview";
715
+ preview.innerHTML = '<img src="' + current + '" alt="" />';
716
+ wrapper.appendChild(preview);
717
+ }
718
+
719
+ var row = document.createElement("div");
720
+ row.className = "caret-single-image-row";
721
+
722
+ var input = document.createElement("input");
723
+ input.type = "text";
724
+ input.className = "studio-input";
725
+ input.style.flex = "1";
726
+ input.value = current;
727
+ input.placeholder = "Image URL";
728
+ input.addEventListener("input", function () { updateField(path, input.value); });
729
+ input.addEventListener("blur", rerender);
730
+
731
+ var uploadBtn = document.createElement("button");
732
+ uploadBtn.type = "button";
733
+ uploadBtn.className = "studio-btn-ghost";
734
+ uploadBtn.textContent = "Upload";
735
+
736
+ var fileInput = document.createElement("input");
737
+ fileInput.type = "file";
738
+ fileInput.accept = "image/jpeg,image/png,image/webp,image/avif";
739
+ fileInput.style.display = "none";
740
+
741
+ uploadBtn.addEventListener("click", function () { fileInput.click(); });
742
+ fileInput.addEventListener("change", function () {
743
+ var file = fileInput.files && fileInput.files[0];
744
+ if (!file) return;
745
+ uploadBtn.textContent = "Uploading…";
746
+ uploadBtn.disabled = true;
747
+ uploadFile(file).then(function (newUrl) {
748
+ updateField(path, newUrl);
749
+ rerender();
750
+ }).catch(function () {
751
+ uploadBtn.textContent = "Upload";
752
+ uploadBtn.disabled = false;
753
+ });
754
+ fileInput.value = "";
755
+ });
756
+
757
+ row.appendChild(input);
758
+ row.appendChild(uploadBtn);
759
+ row.appendChild(fileInput);
760
+ wrapper.appendChild(row);
761
+ }
762
+
763
+ rerender();
764
+ return wrapper;
765
+ }
766
+
767
+ /* ─── Tag list ──────────────────────────────────────────────────── */
768
+ function buildTagList(items, path) {
769
+ var wrapper = document.createElement("div");
770
+
771
+ function rerender() {
772
+ wrapper.innerHTML = "";
773
+ var current = getNestedValue(entryData, path) || [];
774
+
775
+ if (current.length > 0) {
776
+ var tagsEl = document.createElement("div");
777
+ tagsEl.className = "caret-tags";
778
+ current.forEach(function (item, idx) {
779
+ var tag = document.createElement("span");
780
+ tag.className = "studio-tag";
781
+ tag.textContent = item;
782
+ var btn = document.createElement("button");
783
+ btn.type = "button";
784
+ btn.title = "Remove";
785
+ btn.textContent = "×";
786
+ btn.addEventListener("click", function () {
787
+ var next = current.slice();
788
+ next.splice(idx, 1);
789
+ updateField(path, next);
790
+ rerender();
791
+ });
792
+ tag.appendChild(btn);
793
+ tagsEl.appendChild(tag);
794
+ });
795
+ wrapper.appendChild(tagsEl);
796
+ }
797
+
798
+ var row = document.createElement("div");
799
+ row.className = "caret-add-row";
800
+ var input = document.createElement("input");
801
+ input.type = "text";
802
+ input.className = "studio-input";
803
+ input.style.flex = "1";
804
+ input.placeholder = "Add item and press Enter";
805
+
806
+ var addBtn = document.createElement("button");
807
+ addBtn.type = "button";
808
+ addBtn.className = "studio-btn-ghost";
809
+ addBtn.textContent = "Add";
810
+
811
+ function addTag() {
812
+ var v = input.value.trim();
813
+ if (v && current.indexOf(v) < 0) {
814
+ updateField(path, current.concat([v]));
815
+ rerender();
816
+ }
817
+ }
818
+
819
+ input.addEventListener("keydown", function (e) {
820
+ if (e.key === "Enter") { e.preventDefault(); addTag(); }
821
+ });
822
+ addBtn.addEventListener("click", addTag);
823
+ row.appendChild(input);
824
+ row.appendChild(addBtn);
825
+ wrapper.appendChild(row);
826
+ }
827
+
828
+ rerender();
829
+ return wrapper;
830
+ }
831
+
832
+ /* ─── Object array ──────────────────────────────────────────────── */
833
+ function buildObjectArray(items, path, itemSchema) {
834
+ var wrapper = document.createElement("div");
835
+ wrapper.className = "caret-object-array";
836
+
837
+ function rerender() {
838
+ wrapper.innerHTML = "";
839
+ var current = getNestedValue(entryData, path) || [];
840
+
841
+ current.forEach(function (item, idx) {
842
+ var card = document.createElement("div");
843
+ card.className = "caret-object-array-item";
844
+
845
+ var removeBtn = document.createElement("button");
846
+ removeBtn.type = "button";
847
+ removeBtn.className = "caret-object-array-remove";
848
+ removeBtn.title = "Remove item";
849
+ removeBtn.textContent = "×";
850
+ removeBtn.addEventListener("click", function () {
851
+ var next = current.slice();
852
+ next.splice(idx, 1);
853
+ updateField(path, next);
854
+ rerender();
855
+ });
856
+ card.appendChild(removeBtn);
857
+
858
+ var idxLabel = document.createElement("span");
859
+ idxLabel.className = "caret-object-array-index";
860
+ idxLabel.textContent = "[" + idx + "]";
861
+ card.appendChild(idxLabel);
862
+
863
+ var fieldsCont = document.createElement("div");
864
+ fieldsCont.className = "caret-object-array-fields";
865
+
866
+ var keys = (itemSchema && itemSchema.properties) ? Object.keys(itemSchema.properties) : Object.keys(item);
867
+ keys.forEach(function (key) {
868
+ var itemPath = path + "." + idx + "." + key;
869
+ var val = item[key];
870
+ var propSchema = itemSchema && itemSchema.properties && itemSchema.properties[key];
871
+ var fieldLabel = (propSchema && propSchema.title) || humanizeKey(key);
872
+
873
+ var row = document.createElement("div");
874
+ row.className = "caret-object-array-row";
875
+
876
+ var keyLabel = document.createElement("span");
877
+ keyLabel.className = "caret-object-array-key";
878
+ keyLabel.textContent = fieldLabel;
879
+
880
+ var input = document.createElement("input");
881
+ input.type = "text";
882
+ input.className = "studio-input";
883
+ input.value = typeof val === "string" ? val : (val == null ? "" : JSON.stringify(val));
884
+ input.addEventListener("input", function () { updateField(itemPath, input.value); });
885
+
886
+ row.appendChild(keyLabel);
887
+ row.appendChild(input);
888
+ fieldsCont.appendChild(row);
889
+ });
890
+
891
+ card.appendChild(fieldsCont);
892
+ wrapper.appendChild(card);
893
+ });
894
+
895
+ var addBtn = document.createElement("button");
896
+ addBtn.type = "button";
897
+ addBtn.className = "caret-object-array-add";
898
+ addBtn.textContent = "+ Add item";
899
+ addBtn.addEventListener("click", function () {
900
+ var template;
901
+ if (itemSchema && itemSchema.properties) {
902
+ template = {};
903
+ Object.keys(itemSchema.properties).forEach(function (k) {
904
+ var s = itemSchema.properties[k];
905
+ template[k] = s.type === "number" || s.type === "integer" ? 0 : (s.type === "boolean" ? false : "");
906
+ });
907
+ } else {
908
+ template = current.length > 0
909
+ ? Object.fromEntries(Object.keys(current[0]).map(function (k) { return [k, ""]; }))
910
+ : {};
911
+ }
912
+ updateField(path, current.concat([template]));
913
+ rerender();
914
+ });
915
+ wrapper.appendChild(addBtn);
916
+ }
917
+
918
+ rerender();
919
+ return wrapper;
920
+ }
921
+
922
+ /* ─── History ───────────────────────────────────────────────────── */
923
+ function loadHistory() {
924
+ historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-text-dim);margin:0;">Loading…</p>';
925
+ historyPanel.hidden = false;
926
+
927
+ fetch(API + "/history?collection=" + encodeURIComponent(COLLECTION) + "&id=" + encodeURIComponent(ID), {
928
+ credentials: "same-origin",
929
+ }).then(function (res) {
930
+ if (handleAuth(res)) return null;
931
+ if (!res.ok) throw new Error("history failed");
932
+ return res.json();
933
+ }).then(function (json) {
934
+ if (!json) return;
935
+ var items = json.history || [];
936
+ if (items.length === 0) {
937
+ historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-text-dim);margin:0;">No history yet. Edits create snapshots automatically.</p>';
938
+ return;
939
+ }
940
+ historyList.innerHTML = "";
941
+ items.forEach(function (item) {
942
+ var row = document.createElement("div");
943
+ row.className = "caret-history-row";
944
+ var date = new Date(item.ts);
945
+ var timeStr = date.toLocaleString();
946
+
947
+ var info = document.createElement("div");
948
+ info.style.cssText = "display:flex;align-items:center;gap:0.75rem;";
949
+
950
+ var actionSpan = document.createElement("span");
951
+ actionSpan.className = "caret-history-action";
952
+ actionSpan.dataset.action = item.action || "save";
953
+ actionSpan.textContent = item.action || "save";
954
+
955
+ var timeSpan = document.createElement("span");
956
+ timeSpan.style.cssText = "font-size:0.75rem;color:var(--studio-text-muted);";
957
+ timeSpan.textContent = timeStr;
958
+
959
+ info.appendChild(actionSpan);
960
+ info.appendChild(timeSpan);
961
+ row.appendChild(info);
962
+
963
+ var restoreBtn = document.createElement("button");
964
+ restoreBtn.type = "button";
965
+ restoreBtn.className = "studio-btn-ghost";
966
+ restoreBtn.textContent = "Restore";
967
+ restoreBtn.addEventListener("click", function () { restoreSnapshot(item.ts); });
968
+ row.appendChild(restoreBtn);
969
+
970
+ historyList.appendChild(row);
971
+ });
972
+ }).catch(function () {
973
+ historyList.innerHTML = '<p style="font-size:0.75rem;color:var(--studio-red);margin:0;">Failed to load history</p>';
974
+ });
975
+ }
976
+
977
+ function restoreSnapshot(ts) {
978
+ if (!window.confirm("Restore this version? Current changes will be saved to history first.")) return;
979
+ fetch(API + "/history", {
980
+ method: "POST",
981
+ headers: { "Content-Type": "application/json", "x-caret-request": "1" },
982
+ credentials: "same-origin",
983
+ body: JSON.stringify({ collection: COLLECTION, id: ID, ts: ts }),
984
+ }).then(function (res) {
985
+ if (handleAuth(res)) return null;
986
+ if (!res.ok) throw new Error("restore failed");
987
+ return res.json();
988
+ }).then(function (json) {
989
+ if (!json) return;
990
+ var prevData = entryData;
991
+ var prevJson = originalJson;
992
+ try {
993
+ entryData = deepClone(json.data);
994
+ originalJson = JSON.stringify(json.data);
995
+ if (typeof json.revision === "number") entryRevision = json.revision;
996
+ fieldsEl.innerHTML = "";
997
+ renderFields(fieldsEl, entryData, entrySchema, "");
998
+ updateSaveButton();
999
+ } catch (e) {
1000
+ entryData = prevData;
1001
+ originalJson = prevJson;
1002
+ fieldsEl.innerHTML = "";
1003
+ renderFields(fieldsEl, entryData, entrySchema, "");
1004
+ updateSaveButton();
1005
+ showStatus("error", "Restore failed — invalid data");
1006
+ return;
1007
+ }
1008
+ historyPanel.hidden = true;
1009
+ showStatus("saved", "Restored");
1010
+ }).catch(function () {
1011
+ showStatus("error", "Restore failed");
1012
+ });
1013
+ }
1014
+
1015
+ /* ─── Wire events ───────────────────────────────────────────────── */
1016
+ saveBtn.addEventListener("click", save);
1017
+ deleteBtn.addEventListener("click", deleteEntry);
1018
+ historyBtn.addEventListener("click", loadHistory);
1019
+ historyCloseBtn.addEventListener("click", function () {
1020
+ historyPanel.hidden = true;
1021
+ historyList.innerHTML = "";
1022
+ });
1023
+ deleteCancelBtn.addEventListener("click", function () { deleteDialog.hidden = true; });
1024
+ deleteConfirmBtn.addEventListener("click", confirmDelete);
1025
+ deleteDialog.addEventListener("click", function (e) {
1026
+ if (e.target === deleteDialog) deleteDialog.hidden = true;
1027
+ });
1028
+
1029
+ window.addEventListener("keydown", function (e) {
1030
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
1031
+ e.preventDefault();
1032
+ if (isDirty()) save();
1033
+ }
1034
+ if (e.key === "Escape") {
1035
+ if (!deleteDialog.hidden) deleteDialog.hidden = true;
1036
+ if (!historyPanel.hidden) {
1037
+ historyPanel.hidden = true;
1038
+ historyList.innerHTML = "";
1039
+ }
1040
+ }
1041
+ });
1042
+
1043
+ window.addEventListener("beforeunload", function (e) {
1044
+ if (isDirty()) { e.preventDefault(); e.returnValue = ""; }
1045
+ });
1046
+
1047
+ /* ─── Load entry + schema ───────────────────────────────────────── */
1048
+ Promise.all([
1049
+ fetch(API + "/entries?collection=" + encodeURIComponent(COLLECTION) + "&id=" + encodeURIComponent(ID), { credentials: "same-origin" }),
1050
+ fetch(API + "/schema?collection=" + encodeURIComponent(COLLECTION), { credentials: "same-origin" }),
1051
+ ]).then(function (results) {
1052
+ var entryRes = results[0];
1053
+ var schemaRes = results[1];
1054
+
1055
+ if (handleAuth(entryRes)) return;
1056
+ if (!entryRes.ok) throw new Error("load failed");
1057
+
1058
+ return entryRes.json().then(function (entryJson) {
1059
+ var entry = (entryJson && entryJson.entries) ? entryJson.entries[0] : null;
1060
+
1061
+ var schemaPromise = schemaRes.ok ? schemaRes.json() : Promise.resolve(null);
1062
+ return schemaPromise.then(function (schemaJson) {
1063
+ if (schemaJson && schemaJson.schema) entrySchema = schemaJson.schema;
1064
+
1065
+ loadingEl.hidden = true;
1066
+ if (!entry) {
1067
+ notFoundEl.hidden = false;
1068
+ return;
1069
+ }
1070
+ entryData = deepClone(entry.data);
1071
+ entryRevision = typeof entry.revision === "number" ? entry.revision : undefined;
1072
+ originalJson = JSON.stringify(entry.data);
1073
+ titleEl.textContent = getTitle(entryData);
1074
+ renderFields(fieldsEl, entryData, entrySchema, "");
1075
+ updateSaveButton();
1076
+ editorEl.hidden = false;
1077
+ });
1078
+ });
1079
+ }).catch(function () {
1080
+ loadingEl.hidden = true;
1081
+ notFoundEl.hidden = false;
1082
+ });
1083
+ })();