@caretcms/core 0.2.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 (256) hide show
  1. package/README.md +195 -12
  2. package/dist/index.d.ts +26 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +113 -12
  5. package/dist/index.js.map +1 -1
  6. package/dist/loader.d.ts.map +1 -1
  7. package/dist/loader.js +35 -3
  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 +35 -9
  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/auth/session.d.ts +1 -0
  32. package/dist/runtime/auth/session.d.ts.map +1 -1
  33. package/dist/runtime/auth/session.js +16 -0
  34. package/dist/runtime/auth/session.js.map +1 -1
  35. package/dist/runtime/authorization.d.ts +11 -0
  36. package/dist/runtime/authorization.d.ts.map +1 -0
  37. package/dist/runtime/authorization.js +29 -0
  38. package/dist/runtime/authorization.js.map +1 -0
  39. package/dist/runtime/collection-policy.d.ts +11 -0
  40. package/dist/runtime/collection-policy.d.ts.map +1 -0
  41. package/dist/runtime/collection-policy.js +44 -0
  42. package/dist/runtime/collection-policy.js.map +1 -0
  43. package/dist/runtime/config.d.ts +3 -0
  44. package/dist/runtime/config.d.ts.map +1 -1
  45. package/dist/runtime/config.js +4 -1
  46. package/dist/runtime/config.js.map +1 -1
  47. package/dist/runtime/content-errors.d.ts +9 -0
  48. package/dist/runtime/content-errors.d.ts.map +1 -0
  49. package/dist/runtime/content-errors.js +17 -0
  50. package/dist/runtime/content-errors.js.map +1 -0
  51. package/dist/runtime/content.d.ts.map +1 -1
  52. package/dist/runtime/content.js +13 -4
  53. package/dist/runtime/content.js.map +1 -1
  54. package/dist/runtime/deployment-status.d.ts +14 -0
  55. package/dist/runtime/deployment-status.d.ts.map +1 -0
  56. package/dist/runtime/deployment-status.js +57 -0
  57. package/dist/runtime/deployment-status.js.map +1 -0
  58. package/dist/runtime/draft-state.d.ts +16 -0
  59. package/dist/runtime/draft-state.d.ts.map +1 -0
  60. package/dist/runtime/draft-state.js +29 -0
  61. package/dist/runtime/draft-state.js.map +1 -0
  62. package/dist/runtime/i18n.d.ts +97 -0
  63. package/dist/runtime/i18n.d.ts.map +1 -0
  64. package/dist/runtime/i18n.js +190 -0
  65. package/dist/runtime/i18n.js.map +1 -0
  66. package/dist/runtime/index.d.ts +1 -0
  67. package/dist/runtime/index.d.ts.map +1 -1
  68. package/dist/runtime/index.js +1 -0
  69. package/dist/runtime/index.js.map +1 -1
  70. package/dist/runtime/middleware.d.ts.map +1 -1
  71. package/dist/runtime/middleware.js +61 -4
  72. package/dist/runtime/middleware.js.map +1 -1
  73. package/dist/runtime/mutations/contracts.d.ts +12 -0
  74. package/dist/runtime/mutations/contracts.d.ts.map +1 -1
  75. package/dist/runtime/mutations/contracts.js +54 -4
  76. package/dist/runtime/mutations/contracts.js.map +1 -1
  77. package/dist/runtime/mutations/engine.d.ts +2 -0
  78. package/dist/runtime/mutations/engine.d.ts.map +1 -1
  79. package/dist/runtime/mutations/engine.js +220 -57
  80. package/dist/runtime/mutations/engine.js.map +1 -1
  81. package/dist/runtime/providers.d.ts +4 -1
  82. package/dist/runtime/providers.d.ts.map +1 -1
  83. package/dist/runtime/providers.js +18 -1
  84. package/dist/runtime/providers.js.map +1 -1
  85. package/dist/runtime/publish-recovery.d.ts +32 -0
  86. package/dist/runtime/publish-recovery.d.ts.map +1 -0
  87. package/dist/runtime/publish-recovery.js +98 -0
  88. package/dist/runtime/publish-recovery.js.map +1 -0
  89. package/dist/runtime/publish.d.ts +10 -4
  90. package/dist/runtime/publish.d.ts.map +1 -1
  91. package/dist/runtime/publish.js +105 -63
  92. package/dist/runtime/publish.js.map +1 -1
  93. package/dist/runtime/rebuild-webhook.d.ts +4 -0
  94. package/dist/runtime/rebuild-webhook.d.ts.map +1 -1
  95. package/dist/runtime/rebuild-webhook.js +12 -2
  96. package/dist/runtime/rebuild-webhook.js.map +1 -1
  97. package/dist/runtime/request-context.d.ts +7 -1
  98. package/dist/runtime/request-context.d.ts.map +1 -1
  99. package/dist/runtime/request-context.js +7 -1
  100. package/dist/runtime/request-context.js.map +1 -1
  101. package/dist/runtime/rewrite.d.ts.map +1 -1
  102. package/dist/runtime/rewrite.js +28 -2
  103. package/dist/runtime/rewrite.js.map +1 -1
  104. package/dist/runtime/rich-allowlist.js +1 -1
  105. package/dist/runtime/rich-allowlist.js.map +1 -1
  106. package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
  107. package/dist/runtime/routes/admin-entry.js +129 -21
  108. package/dist/runtime/routes/admin-entry.js.map +1 -1
  109. package/dist/runtime/routes/admin.d.ts.map +1 -1
  110. package/dist/runtime/routes/admin.js +28 -14
  111. package/dist/runtime/routes/admin.js.map +1 -1
  112. package/dist/runtime/routes/auth-login.d.ts.map +1 -1
  113. package/dist/runtime/routes/auth-login.js +4 -0
  114. package/dist/runtime/routes/auth-login.js.map +1 -1
  115. package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
  116. package/dist/runtime/routes/auth-logout.js +7 -2
  117. package/dist/runtime/routes/auth-logout.js.map +1 -1
  118. package/dist/runtime/routes/auth-session.d.ts.map +1 -1
  119. package/dist/runtime/routes/auth-session.js +10 -2
  120. package/dist/runtime/routes/auth-session.js.map +1 -1
  121. package/dist/runtime/routes/collection-list.d.ts.map +1 -1
  122. package/dist/runtime/routes/collection-list.js +213 -66
  123. package/dist/runtime/routes/collection-list.js.map +1 -1
  124. package/dist/runtime/routes/deployment.d.ts +5 -0
  125. package/dist/runtime/routes/deployment.d.ts.map +1 -0
  126. package/dist/runtime/routes/deployment.js +38 -0
  127. package/dist/runtime/routes/deployment.js.map +1 -0
  128. package/dist/runtime/routes/draft.d.ts.map +1 -1
  129. package/dist/runtime/routes/draft.js +10 -3
  130. package/dist/runtime/routes/draft.js.map +1 -1
  131. package/dist/runtime/routes/entries.d.ts.map +1 -1
  132. package/dist/runtime/routes/entries.js +87 -43
  133. package/dist/runtime/routes/entries.js.map +1 -1
  134. package/dist/runtime/routes/history.d.ts.map +1 -1
  135. package/dist/runtime/routes/history.js +40 -21
  136. package/dist/runtime/routes/history.js.map +1 -1
  137. package/dist/runtime/routes/mutate.d.ts.map +1 -1
  138. package/dist/runtime/routes/mutate.js +4 -1
  139. package/dist/runtime/routes/mutate.js.map +1 -1
  140. package/dist/runtime/routes/publish.d.ts.map +1 -1
  141. package/dist/runtime/routes/publish.js +127 -33
  142. package/dist/runtime/routes/publish.js.map +1 -1
  143. package/dist/runtime/routes/schema.d.ts.map +1 -1
  144. package/dist/runtime/routes/schema.js +11 -2
  145. package/dist/runtime/routes/schema.js.map +1 -1
  146. package/dist/runtime/routes/studio-home.d.ts.map +1 -1
  147. package/dist/runtime/routes/studio-home.js +41 -20
  148. package/dist/runtime/routes/studio-home.js.map +1 -1
  149. package/dist/runtime/routes/upload.d.ts.map +1 -1
  150. package/dist/runtime/routes/upload.js +3 -0
  151. package/dist/runtime/routes/upload.js.map +1 -1
  152. package/dist/runtime/schema-registry.d.ts +8 -1
  153. package/dist/runtime/schema-registry.d.ts.map +1 -1
  154. package/dist/runtime/schema-registry.js +49 -3
  155. package/dist/runtime/schema-registry.js.map +1 -1
  156. package/dist/runtime/storage/entry-commit.d.ts +8 -0
  157. package/dist/runtime/storage/entry-commit.d.ts.map +1 -0
  158. package/dist/runtime/storage/entry-commit.js +45 -0
  159. package/dist/runtime/storage/entry-commit.js.map +1 -0
  160. package/dist/runtime/storage/filesystem-adapter.d.ts +5 -0
  161. package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -1
  162. package/dist/runtime/storage/filesystem-adapter.js +12 -3
  163. package/dist/runtime/storage/filesystem-adapter.js.map +1 -1
  164. package/dist/runtime/storage/frontmatter-codec.d.ts +5 -23
  165. package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
  166. package/dist/runtime/storage/frontmatter-codec.js +156 -13
  167. package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
  168. package/dist/runtime/storage/in-memory-adapter.d.ts +7 -0
  169. package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -1
  170. package/dist/runtime/storage/in-memory-adapter.js +6 -0
  171. package/dist/runtime/storage/in-memory-adapter.js.map +1 -1
  172. package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
  173. package/dist/runtime/storage/markdown-adapter.js +18 -20
  174. package/dist/runtime/storage/markdown-adapter.js.map +1 -1
  175. package/dist/runtime/storage/session-overlay-adapter.d.ts +12 -4
  176. package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
  177. package/dist/runtime/storage/session-overlay-adapter.js +100 -13
  178. package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
  179. package/dist/runtime/storage/sidecar-meta-store.d.ts +5 -0
  180. package/dist/runtime/storage/sidecar-meta-store.d.ts.map +1 -1
  181. package/dist/runtime/storage/sidecar-meta-store.js +48 -5
  182. package/dist/runtime/storage/sidecar-meta-store.js.map +1 -1
  183. package/dist/runtime/themes/preset-tokens.js +4 -4
  184. package/dist/runtime/themes/preset-tokens.js.map +1 -1
  185. package/dist/runtime/utils.d.ts.map +1 -1
  186. package/dist/runtime/utils.js +3 -4
  187. package/dist/runtime/utils.js.map +1 -1
  188. package/dist/runtime/views/studio-layout.d.ts +3 -0
  189. package/dist/runtime/views/studio-layout.d.ts.map +1 -1
  190. package/dist/runtime/views/studio-layout.js +36 -13
  191. package/dist/runtime/views/studio-layout.js.map +1 -1
  192. package/dist/schema-utils.d.ts +7 -0
  193. package/dist/schema-utils.d.ts.map +1 -1
  194. package/dist/schema-utils.js +143 -0
  195. package/dist/schema-utils.js.map +1 -1
  196. package/dist/types.d.ts +165 -1
  197. package/dist/types.d.ts.map +1 -1
  198. package/package.json +12 -3
  199. package/static/cms/admin-entry.css +144 -13
  200. package/static/cms/admin-entry.js +539 -586
  201. package/static/cms/dev-toolbar/app.js +78 -19
  202. package/static/cms/editor/config.js +35 -15
  203. package/static/cms/editor/content-map.css +8 -1
  204. package/static/cms/editor/content-map.js +62 -23
  205. package/static/cms/editor/deployment-status.js +66 -0
  206. package/static/cms/editor/draft-client.js +87 -0
  207. package/static/cms/editor/guards.js +20 -0
  208. package/static/cms/editor/helpers.js +30 -1
  209. package/static/cms/editor/highlight.js +1 -0
  210. package/static/cms/editor/image-edit.js +44 -1
  211. package/static/cms/editor/image-utils.js +23 -7
  212. package/static/cms/editor/link-follow.js +122 -41
  213. package/static/cms/editor/link-ui.css +10 -1
  214. package/static/cms/editor/linkify.js +2 -0
  215. package/static/cms/editor/md-block-edit.js +140 -37
  216. package/static/cms/editor/md-paragraphs.js +98 -0
  217. package/static/cms/editor/md-serialize.js +1 -1
  218. package/static/cms/editor/panel.js +53 -7
  219. package/static/cms/editor/preview-mode.js +44 -0
  220. package/static/cms/editor/publish-result.js +100 -0
  221. package/static/cms/editor/rich-toolbar.js +58 -15
  222. package/static/cms/editor/sanitize.js +6 -2
  223. package/static/cms/editor/save-queue.js +75 -17
  224. package/static/cms/editor/section-controls/activation.js +3 -0
  225. package/static/cms/editor/section-controls/api.js +25 -7
  226. package/static/cms/editor/section-controls/index.js +60 -8
  227. package/static/cms/editor/section-controls/model.js +39 -4
  228. package/static/cms/editor/section-controls/mutations.js +15 -0
  229. package/static/cms/editor/section-controls/page-context.js +9 -0
  230. package/static/cms/editor/section-controls/pickers.js +53 -2
  231. package/static/cms/editor/section-controls/reorder.js +23 -0
  232. package/static/cms/editor/section-controls/selection.js +6 -0
  233. package/static/cms/editor/section-controls/spacing-drag.js +16 -0
  234. package/static/cms/editor/section-controls/ui-elements.js +7 -0
  235. package/static/cms/editor/section-controls/utils.js +7 -0
  236. package/static/cms/editor/section-controls.css +18 -3
  237. package/static/cms/editor/security.js +13 -2
  238. package/static/cms/editor/stega-hydrate.js +17 -4
  239. package/static/cms/editor/sync.js +475 -56
  240. package/static/cms/editor/text-edit.js +37 -3
  241. package/static/cms/editor/text-link-interactions.js +59 -17
  242. package/static/cms/editor/toast.js +23 -9
  243. package/static/cms/editor/tokens-and-text.css +9 -0
  244. package/static/cms/editor/toolbar.css +5 -0
  245. package/static/cms/editor/toolbar.js +91 -101
  246. package/static/cms/editor.css +1 -1
  247. package/static/cms/editor.js +1 -0
  248. package/static/cms/studio/entry-loader.js +102 -0
  249. package/static/cms/studio/field-model.js +110 -0
  250. package/static/cms/studio/fields.js +319 -0
  251. package/static/cms/studio/history-client.js +74 -0
  252. package/static/cms/studio/mutation-client.js +74 -0
  253. package/static/cms/studio/sync-client.js +107 -0
  254. package/static/cms/studio/upload-client.js +109 -0
  255. package/static/cms/studio-tokens.css +4 -4
  256. package/static/cms/studio.css +24 -0
@@ -10,6 +10,19 @@ import { defineToolbarApp } from "astro/toolbar";
10
10
  // keeps working in `output: 'static'` projects where /__caret/ is not served.
11
11
  // If helpers.js changes, keep these two in sync.
12
12
 
13
+ /**
14
+ * @typedef {{ collection: string | null, id: string | null, field: string }} ParsedBinding
15
+ * @typedef {{ collection: string, id: string, field: string }} ResolvedBinding
16
+ * @typedef {"text" | "image" | "rich"} BindingKind
17
+ * @typedef {{ el: Element, index: number, collection: string, id: string, field: string, key: string, kind: BindingKind }} PageBinding
18
+ * @typedef {{ el: Element, attr: string, kind: BindingKind, index: number }} OrphanBinding
19
+ * @typedef {{ bindings: PageBinding[], orphans: OrphanBinding[], duplicates: PageBinding[], stega: boolean, total: number }} BindingScan
20
+ */
21
+
22
+ /**
23
+ * @param {string} attr
24
+ * @returns {ParsedBinding | null}
25
+ */
13
26
  function parseCaretAttr(attr) {
14
27
  const parts = attr.split("::");
15
28
  if (parts.length === 3) return { collection: parts[0], id: parts[1], field: parts[2] };
@@ -17,11 +30,18 @@ function parseCaretAttr(attr) {
17
30
  return null;
18
31
  }
19
32
 
33
+ /**
34
+ * @param {Element} el
35
+ * @param {ParsedBinding | null} parsed
36
+ * @returns {ResolvedBinding | null}
37
+ */
20
38
  function resolveBinding(el, parsed) {
21
39
  if (!parsed) return null;
22
40
  // Explicit null check, not truthiness — mirrors helpers.js; see the parity
23
41
  // test (tests/unit/caret-parser-parity.test.ts).
24
- if (parsed.collection !== null && parsed.id !== null) return parsed;
42
+ if (parsed.collection !== null && parsed.id !== null) {
43
+ return { collection: parsed.collection, id: parsed.id, field: parsed.field };
44
+ }
25
45
  let node = el.parentElement;
26
46
  while (node) {
27
47
  const scope = node.getAttribute("data-caret-scope");
@@ -35,13 +55,21 @@ function resolveBinding(el, parsed) {
35
55
  }
36
56
 
37
57
  function devConfig() {
38
- const c = (typeof window !== "undefined" && window.__CARET_DEV__) || {};
58
+ const c =
59
+ (typeof window !== "undefined" &&
60
+ /** @type {Window & { __CARET_DEV__?: { mountPath?: unknown, apiBasePath?: unknown } }} */ (window)
61
+ .__CARET_DEV__) ||
62
+ {};
39
63
  return {
40
64
  mountPath: typeof c.mountPath === "string" ? c.mountPath : "/admin",
41
65
  apiBasePath: typeof c.apiBasePath === "string" ? c.apiBasePath : "/api/cms",
42
66
  };
43
67
  }
44
68
 
69
+ /**
70
+ * @param {Element} el
71
+ * @returns {BindingKind}
72
+ */
45
73
  function classify(el) {
46
74
  if (el.tagName.toLowerCase() === "img") return "image";
47
75
  if (el.hasAttribute("data-caret-rich")) return "rich";
@@ -50,9 +78,12 @@ function classify(el) {
50
78
 
51
79
  // Walk the page and resolve every data-caret element to a full key, splitting
52
80
  // out orphans (field-only attrs with no resolvable scope) and duplicate keys.
81
+ /** @returns {BindingScan} */
53
82
  function scanBindings() {
54
83
  const els = Array.from(document.querySelectorAll("[data-caret]"));
84
+ /** @type {PageBinding[]} */
55
85
  const bindings = [];
86
+ /** @type {OrphanBinding[]} */
56
87
  const orphans = [];
57
88
 
58
89
  els.forEach((el, i) => {
@@ -75,9 +106,10 @@ function scanBindings() {
75
106
  });
76
107
  });
77
108
 
109
+ /** @type {Map<string, number>} */
78
110
  const counts = new Map();
79
111
  bindings.forEach((b) => counts.set(b.key, (counts.get(b.key) || 0) + 1));
80
- const duplicates = bindings.filter((b) => counts.get(b.key) > 1);
112
+ const duplicates = bindings.filter((b) => (counts.get(b.key) ?? 0) > 1);
81
113
 
82
114
  // Stega-encoded bindings (live-loader content) only become real data-caret
83
115
  // attributes after the editor's stega-hydrate.js runs, which needs auth — so
@@ -93,6 +125,7 @@ function scanBindings() {
93
125
  // be told apart when both are active at once.
94
126
  const HIGHLIGHT_STYLE_ID = "__caret-devtoolbar-highlight__";
95
127
 
128
+ /** @param {boolean} active */
96
129
  function applyHighlight(active) {
97
130
  const existing = document.getElementById(HIGHLIGHT_STYLE_ID);
98
131
  if (existing) existing.remove();
@@ -107,22 +140,43 @@ function applyHighlight(active) {
107
140
  document.head.appendChild(style);
108
141
  }
109
142
 
143
+ /**
144
+ * Repeated jumps share the first inline style snapshot so overlapping timers
145
+ * cannot restore a temporary outline and leave it stuck on the page.
146
+ * @type {WeakMap<HTMLElement | SVGElement, { outline: string, outlineOffset: string, timer: number }>}
147
+ */
148
+ const scrollFlashes = new WeakMap();
149
+
150
+ /** @param {Element} el */
110
151
  function scrollTo(el) {
111
152
  el.scrollIntoView({ behavior: "smooth", block: "center" });
112
- const prev = el.style.outline;
113
- const prevOffset = el.style.outlineOffset;
153
+ if (!(el instanceof HTMLElement || el instanceof SVGElement)) return;
154
+
155
+ const active = scrollFlashes.get(el);
156
+ if (active) window.clearTimeout(active.timer);
157
+ const outline = active?.outline ?? el.style.outline;
158
+ const outlineOffset = active?.outlineOffset ?? el.style.outlineOffset;
114
159
  el.style.outline = "3px solid #6366f1";
115
160
  el.style.outlineOffset = "3px";
116
- setTimeout(() => {
117
- el.style.outline = prev;
118
- el.style.outlineOffset = prevOffset;
161
+ const timer = window.setTimeout(() => {
162
+ const current = scrollFlashes.get(el);
163
+ if (!current || current.timer !== timer) return;
164
+ el.style.outline = current.outline;
165
+ el.style.outlineOffset = current.outlineOffset;
166
+ scrollFlashes.delete(el);
119
167
  }, 1500);
168
+ scrollFlashes.set(el, { outline, outlineOffset, timer });
120
169
  }
121
170
 
171
+ /** @type {Record<BindingKind, string>} */
122
172
  const KIND_LABEL = { text: "text", image: "image", rich: "rich" };
123
173
 
174
+ /** @type {Record<string, string>} */
175
+ const HTML_ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
176
+
177
+ /** @param {string} s */
124
178
  function esc(s) {
125
- return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
179
+ return s.replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] ?? c);
126
180
  }
127
181
 
128
182
  const PANEL_CSS = `
@@ -169,19 +223,22 @@ export default defineToolbarApp({
169
223
  canvas.appendChild(win);
170
224
 
171
225
  let highlightOn = false;
226
+ /** @type {BindingScan} */
172
227
  let scan = { bindings: [], orphans: [], duplicates: [], stega: false, total: 0 };
173
228
 
174
229
  function render() {
175
230
  scan = scanBindings();
176
231
  const { mountPath } = devConfig();
177
- const { bindings, orphans, duplicates, stega, total } = scan;
232
+ const { bindings, orphans, duplicates, stega } = scan;
178
233
 
179
234
  // Group bindings by collection::id, preserving first-seen order.
235
+ /** @type {Map<string, PageBinding[]>} */
180
236
  const groups = new Map();
181
237
  for (const b of bindings) {
182
238
  const gk = `${b.collection}::${b.id}`;
183
- if (!groups.has(gk)) groups.set(gk, []);
184
- groups.get(gk).push(b);
239
+ const rows = groups.get(gk);
240
+ if (rows) rows.push(b);
241
+ else groups.set(gk, [b]);
185
242
  }
186
243
 
187
244
  const dupKeys = new Set(duplicates.map((d) => d.key));
@@ -192,8 +249,8 @@ export default defineToolbarApp({
192
249
  <span class="count">${bindings.length} on page${orphans.length ? ` · ${orphans.length} orphan${orphans.length > 1 ? "s" : ""}` : ""}</span>
193
250
  </div>
194
251
  <div class="actions">
195
- <button class="btn ${highlightOn ? "on" : ""}" data-action="highlight">${highlightOn ? "Hide" : "Highlight"} all</button>
196
- <button class="btn" data-action="rescan">Rescan</button>
252
+ <button type="button" class="btn ${highlightOn ? "on" : ""}" data-action="highlight" aria-pressed="${highlightOn}">${highlightOn ? "Hide" : "Highlight"} all</button>
253
+ <button type="button" class="btn" data-action="rescan">Rescan</button>
197
254
  </div>
198
255
  <div class="scroll">`;
199
256
 
@@ -209,7 +266,7 @@ export default defineToolbarApp({
209
266
  }
210
267
 
211
268
  if (!bindings.length && !orphans.length) {
212
- html += `<div class="empty">No <code>data-caret</code> bindings found on this page.${total ? "" : ""}</div>`;
269
+ html += `<div class="empty">No <code>data-caret</code> bindings found on this page.</div>`;
213
270
  }
214
271
 
215
272
  for (const [gk, rows] of groups) {
@@ -221,7 +278,7 @@ export default defineToolbarApp({
221
278
  html += `<div class="row">
222
279
  <span class="field ${dupKeys.has(b.key) ? "dup" : ""}">${esc(b.field)}</span>
223
280
  <span class="kind ${b.kind}">${KIND_LABEL[b.kind]}</span>
224
- <button class="jump" data-action="scroll" data-index="${b.index}">scroll to</button>
281
+ <button type="button" class="jump" data-action="scroll" data-index="${b.index}">scroll to</button>
225
282
  </div>`;
226
283
  }
227
284
  html += `</div>`;
@@ -229,23 +286,27 @@ export default defineToolbarApp({
229
286
 
230
287
  html += `</div>`;
231
288
  wrap.innerHTML = html;
289
+ const probs = scan.orphans.length + scan.duplicates.length;
290
+ app.toggleNotification({ state: probs > 0, level: "warning" });
232
291
  }
233
292
 
293
+ /** @param {string | null} index */
234
294
  function elementByIndex(index) {
235
295
  const b = scan.bindings.find((x) => x.index === Number(index));
236
296
  return b ? b.el : null;
237
297
  }
238
298
 
239
299
  wrap.addEventListener("click", (event) => {
300
+ if (!(event.target instanceof Element)) return;
240
301
  const target = event.target.closest("[data-action]");
241
302
  if (!target) return;
242
303
  const action = target.getAttribute("data-action");
243
304
  if (action === "highlight") {
244
305
  highlightOn = !highlightOn;
245
306
  applyHighlight(highlightOn);
246
- app.toggleState({ state: highlightOn });
247
307
  target.textContent = `${highlightOn ? "Hide" : "Highlight"} all`;
248
308
  target.classList.toggle("on", highlightOn);
309
+ target.setAttribute("aria-pressed", String(highlightOn));
249
310
  } else if (action === "rescan") {
250
311
  render();
251
312
  } else if (action === "scroll") {
@@ -258,8 +319,6 @@ export default defineToolbarApp({
258
319
  app.onToggled(({ state }) => {
259
320
  if (!state) return;
260
321
  render();
261
- const probs = scan.orphans.length + scan.duplicates.length;
262
- app.toggleNotification({ state: probs > 0, level: "warning" });
263
322
  });
264
323
  },
265
324
  });
@@ -1,4 +1,9 @@
1
1
  /** Runtime config injected by the integration bootstrap script. */
2
+ /** @typedef {{ endpoint?: unknown, projectId?: unknown, contentPath?: unknown, publicToken?: unknown, environment?: unknown }} RawCloudConfig */
3
+ /** @typedef {{ endpoint: string, projectId: string, contentPath: string, publicToken: string | null, environment: string | null }} CloudConfig */
4
+ /** @typedef {{ mode?: unknown, mountPath?: unknown, apiBasePath?: unknown, cloud?: RawCloudConfig | null, delivery?: { mode?: unknown } | null, draftMode?: unknown }} RuntimeConfig */
5
+
6
+ /** @type {RuntimeConfig} */
2
7
  const defaults = {
3
8
  mode: 'embedded',
4
9
  mountPath: '/admin',
@@ -6,8 +11,13 @@ const defaults = {
6
11
  cloud: null,
7
12
  };
8
13
 
9
- const cfg = (typeof window !== 'undefined' && window.__CARET__) || defaults;
14
+ const injected = typeof window !== 'undefined'
15
+ ? /** @type {Window & { __CARET__?: RuntimeConfig }} */ (window).__CARET__
16
+ : undefined;
17
+ /** @type {RuntimeConfig} */
18
+ const cfg = injected || defaults;
10
19
 
20
+ /** @param {unknown} value @param {string} fallback */
11
21
  function normalizePath(value, fallback) {
12
22
  if (!value || typeof value !== 'string') return fallback;
13
23
  const trimmed = value.trim();
@@ -18,11 +28,13 @@ function normalizePath(value, fallback) {
18
28
  : withLeadingSlash;
19
29
  }
20
30
 
31
+ /** @param {unknown} value */
21
32
  function normalizeEndpoint(value) {
22
33
  if (!value || typeof value !== 'string') return '';
23
34
  return value.trim().replace(/\/$/, '');
24
35
  }
25
36
 
37
+ /** @returns {CloudConfig | null} */
26
38
  function getCloudConfig() {
27
39
  if (!cfg || cfg.mode !== 'cloud' || !cfg.cloud || typeof cfg.cloud !== 'object') {
28
40
  return null;
@@ -48,6 +60,7 @@ function getCloudConfig() {
48
60
  };
49
61
  }
50
62
 
63
+ /** @param {CloudConfig} cloud */
51
64
  function getStorageKey(cloud) {
52
65
  return `caretcms:cloud-session:${cloud.endpoint}:${cloud.projectId}`;
53
66
  }
@@ -57,6 +70,8 @@ export const CMS_MODE = cfg.mode === 'cloud' || cfg.mode === 'hybrid' ? cfg.mode
57
70
  export const DELIVERY_MODE =
58
71
  cfg.delivery && cfg.delivery.mode === 'static' ? 'static' : 'server';
59
72
 
73
+ export function isPolicyDraftMode() { return cfg.draftMode === true; }
74
+
60
75
  export function isStaticDelivery() {
61
76
  return DELIVERY_MODE === 'static';
62
77
  }
@@ -64,8 +79,8 @@ export function isStaticDelivery() {
64
79
  export function isServerDelivery() {
65
80
  return !isStaticDelivery();
66
81
  }
67
- export const MOUNT_PATH = normalizePath(cfg.mountPath, defaults.mountPath);
68
- export const API_BASE = normalizePath(cfg.apiBasePath, defaults.apiBasePath);
82
+ export const MOUNT_PATH = normalizePath(cfg.mountPath, '/admin');
83
+ export const API_BASE = normalizePath(cfg.apiBasePath, '/api/cms');
69
84
  export const CLOUD = getCloudConfig();
70
85
  export const STUDIO_PATH = CLOUD ? null : `${MOUNT_PATH}/cms`;
71
86
 
@@ -74,9 +89,10 @@ export function isCloudMode() {
74
89
  }
75
90
 
76
91
  export function getCloudSessionToken() {
77
- if (!isCloudMode() || typeof window === 'undefined') return null;
92
+ const cloud = CLOUD;
93
+ if (!cloud || CMS_MODE !== 'cloud' || typeof window === 'undefined') return null;
78
94
  try {
79
- const value = window.localStorage.getItem(getStorageKey(CLOUD));
95
+ const value = window.localStorage.getItem(getStorageKey(cloud));
80
96
  return value && value.trim() ? value.trim() : null;
81
97
  } catch {
82
98
  return null;
@@ -84,23 +100,26 @@ export function getCloudSessionToken() {
84
100
  }
85
101
 
86
102
  export function clearCloudSessionToken() {
87
- if (!isCloudMode() || typeof window === 'undefined') return;
103
+ const cloud = CLOUD;
104
+ if (!cloud || CMS_MODE !== 'cloud' || typeof window === 'undefined') return;
88
105
  try {
89
- window.localStorage.removeItem(getStorageKey(CLOUD));
106
+ window.localStorage.removeItem(getStorageKey(cloud));
90
107
  } catch {
91
108
  // Ignore localStorage failures.
92
109
  }
93
110
  }
94
111
 
112
+ /** @param {string} path @param {URLSearchParams | Record<string, string> | undefined} [search] */
95
113
  export function buildCmsUrl(path, search) {
96
114
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
97
115
  let url;
98
116
 
99
- if (isCloudMode()) {
100
- url = new URL(`${CLOUD.contentPath}${normalizedPath}`, `${CLOUD.endpoint}/`);
101
- url.searchParams.set('projectId', CLOUD.projectId);
102
- if (CLOUD.environment) {
103
- url.searchParams.set('environment', CLOUD.environment);
117
+ const cloud = CLOUD;
118
+ if (CMS_MODE === 'cloud' && cloud) {
119
+ url = new URL(`${cloud.contentPath}${normalizedPath}`, `${cloud.endpoint}/`);
120
+ url.searchParams.set('projectId', cloud.projectId);
121
+ if (cloud.environment) {
122
+ url.searchParams.set('environment', cloud.environment);
104
123
  }
105
124
  } else {
106
125
  url = new URL(`${API_BASE}${normalizedPath}`, window.location.origin);
@@ -117,9 +136,10 @@ export function buildCmsUrl(path, search) {
117
136
  }
118
137
 
119
138
  export function getEditorLoginUrl(redirectTo = window.location.href) {
120
- if (isCloudMode()) {
121
- const url = new URL(`${CLOUD.contentPath}/admin`, `${CLOUD.endpoint}/`);
122
- url.searchParams.set('projectId', CLOUD.projectId);
139
+ const cloud = CLOUD;
140
+ if (CMS_MODE === 'cloud' && cloud) {
141
+ const url = new URL(`${cloud.contentPath}/admin`, `${cloud.endpoint}/`);
142
+ url.searchParams.set('projectId', cloud.projectId);
123
143
  url.searchParams.set('redirect', redirectTo);
124
144
  return url.toString();
125
145
  }
@@ -78,12 +78,19 @@
78
78
  display: flex;
79
79
  align-items: center;
80
80
  justify-content: space-between;
81
+ width: 100%;
82
+ border: 0;
83
+ background: transparent;
84
+ color: inherit;
85
+ font: inherit;
86
+ text-align: left;
81
87
  padding: 5px 14px;
82
88
  cursor: pointer;
83
89
  transition: background 0.15s;
84
90
  }
85
91
 
86
- .cms-content-map-item:hover {
92
+ .cms-content-map-item:hover,
93
+ .cms-content-map-item:focus-visible {
87
94
  background: rgba(255, 255, 255, 0.05);
88
95
  }
89
96
 
@@ -1,22 +1,33 @@
1
1
  import { buildCmsUrl } from './config.js';
2
2
  import { readHeaders } from './security.js';
3
- import { parseCaretAttr, resolveBinding, getResolvedKey } from './helpers.js';
3
+ import { parseCaretAttr, resolveBinding } from './helpers.js';
4
4
 
5
5
  /**
6
6
  * Content Map dev panel.
7
7
  * Lists all data-caret bindings on the current page with override status.
8
8
  */
9
9
 
10
+ /** @typedef {{ collection: string, id: string, field: string }} ResolvedBinding */
11
+ /** @typedef {{ el: Element, resolved: ResolvedBinding, isImg: boolean, isScoped: boolean, index: number }} MapBinding */
12
+
13
+ /** @type {HTMLElement | null} */
10
14
  let panel = null;
11
15
  let isOpen = false;
16
+ let refreshGeneration = 0;
17
+ /** @type {WeakSet<Element>} */
18
+ const mountedButtons = new WeakSet();
12
19
 
20
+ /** @returns {HTMLElement} */
13
21
  function createPanel() {
14
22
  const el = document.createElement('div');
15
23
  el.className = 'cms-content-map';
24
+ el.setAttribute('role', 'region');
25
+ el.setAttribute('aria-label', 'Content map');
26
+ el.setAttribute('aria-hidden', 'true');
16
27
  el.innerHTML = `
17
28
  <div class="cms-content-map-header">
18
29
  <span class="cms-content-map-title">Content Map</span>
19
- <button type="button" class="cms-content-map-close">&times;</button>
30
+ <button type="button" class="cms-content-map-close" aria-label="Close content map">&times;</button>
20
31
  </div>
21
32
  <div class="cms-content-map-body"></div>
22
33
  `;
@@ -29,19 +40,32 @@ function createPanel() {
29
40
  return el;
30
41
  }
31
42
 
43
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
44
+ function isRecord(value) {
45
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
46
+ }
47
+
48
+ /** @param {unknown} payload @returns {Record<string, unknown> | null} */
49
+ export function readOverrideData(payload) {
50
+ if (!isRecord(payload) || !Array.isArray(payload.entries)) return null;
51
+ const entry = payload.entries[0];
52
+ return isRecord(entry) && isRecord(entry.data) ? entry.data : null;
53
+ }
54
+
55
+ /** @param {Iterable<string>} entryKeys @returns {Promise<Map<string, Record<string, unknown>>>} */
32
56
  async function loadOverrides(entryKeys) {
57
+ /** @type {Map<string, Record<string, unknown>>} */
33
58
  const overrides = new Map();
34
59
  for (const key of entryKeys) {
35
60
  const [collection, id] = key.split('::');
61
+ if (!collection || !id) continue;
36
62
  try {
37
63
  const res = await fetch(buildCmsUrl('/entries', { collection, id }), {
38
64
  headers: readHeaders(),
39
65
  });
40
66
  if (res.ok) {
41
- const data = await res.json();
42
- if (data.entries && data.entries.length > 0) {
43
- overrides.set(key, data.entries[0].data);
44
- }
67
+ const data = readOverrideData(await res.json());
68
+ if (data) overrides.set(key, data);
45
69
  }
46
70
  } catch {
47
71
  // Skip failed loads
@@ -50,24 +74,33 @@ async function loadOverrides(entryKeys) {
50
74
  return overrides;
51
75
  }
52
76
 
77
+ /** @param {unknown} data @param {string} path @returns {unknown} */
53
78
  function getNestedValue(data, path) {
54
79
  const keys = path.split('.');
80
+ /** @type {unknown} */
55
81
  let current = data;
56
82
  for (const key of keys) {
57
- if (current === null || current === undefined) return undefined;
58
- if (typeof current !== 'object') return undefined;
59
- current = current[key];
83
+ if (Array.isArray(current)) {
84
+ if (!/^\d+$/.test(key)) return undefined;
85
+ current = current[Number(key)];
86
+ } else if (isRecord(current)) {
87
+ if (!Object.hasOwn(current, key)) return undefined;
88
+ current = current[key];
89
+ } else return undefined;
60
90
  }
61
91
  return current;
62
92
  }
63
93
 
64
94
  async function refresh() {
65
95
  if (!panel) return;
96
+ const generation = ++refreshGeneration;
66
97
  const body = panel.querySelector('.cms-content-map-body');
67
98
  if (!body) return;
68
99
 
69
100
  const elements = document.querySelectorAll('[data-caret]');
101
+ /** @type {MapBinding[]} */
70
102
  const bindings = [];
103
+ /** @type {Set<string>} */
71
104
  const entryKeys = new Set();
72
105
 
73
106
  elements.forEach((el) => {
@@ -79,19 +112,20 @@ async function refresh() {
79
112
  const key = `${resolved.collection}::${resolved.id}`;
80
113
  entryKeys.add(key);
81
114
 
82
- const isScoped = parsed && !parsed.collection;
115
+ const isScoped = parsed?.collection === null;
83
116
  bindings.push({
84
117
  el,
85
118
  resolved,
86
- fullKey: `${resolved.collection}::${resolved.id}::${resolved.field}`,
87
119
  isImg: el instanceof HTMLImageElement,
88
120
  isScoped,
121
+ index: bindings.length,
89
122
  });
90
123
  });
91
124
 
92
125
  body.innerHTML = `<div class="cms-content-map-loading">Loading...</div>`;
93
126
 
94
127
  const overrides = await loadOverrides(entryKeys);
128
+ if (!isOpen || generation !== refreshGeneration) return;
95
129
 
96
130
  if (bindings.length === 0) {
97
131
  body.innerHTML = `<div class="cms-content-map-empty">No data-caret bindings found on this page.</div>`;
@@ -99,11 +133,13 @@ async function refresh() {
99
133
  }
100
134
 
101
135
  // Group by collection::id
136
+ /** @type {Map<string, MapBinding[]>} */
102
137
  const groups = new Map();
103
138
  for (const b of bindings) {
104
- const key = `${b.resolved.collection}::${b.resolved.id}`;
105
- if (!groups.has(key)) groups.set(key, []);
106
- groups.get(key).push(b);
139
+ const groupKey = `${b.resolved.collection}::${b.resolved.id}`;
140
+ const group = groups.get(groupKey) ?? [];
141
+ group.push(b);
142
+ groups.set(groupKey, group);
107
143
  }
108
144
 
109
145
  let html = '';
@@ -123,13 +159,13 @@ async function refresh() {
123
159
  const typeTag = f.isImg ? '<span class="cms-map-type">img</span>' : '';
124
160
 
125
161
  html += `
126
- <div class="cms-content-map-item" data-caret-map-key="${escapeAttr(f.fullKey)}">
162
+ <button type="button" class="cms-content-map-item" data-caret-map-index="${f.index}">
127
163
  <span class="cms-content-map-field">${escapeHtml(f.resolved.field)}</span>
128
164
  <span class="cms-content-map-tags">
129
165
  ${typeTag}${scopeTag}
130
166
  <span class="cms-map-status ${statusClass}">${statusLabel}</span>
131
167
  </span>
132
- </div>
168
+ </button>
133
169
  `;
134
170
  }
135
171
 
@@ -141,8 +177,9 @@ async function refresh() {
141
177
  // Click to scroll to element
142
178
  body.querySelectorAll('.cms-content-map-item').forEach((item) => {
143
179
  item.addEventListener('click', () => {
144
- const key = item.getAttribute('data-caret-map-key');
145
- const target = bindings.find((b) => b.fullKey === key);
180
+ const rawIndex = item.getAttribute('data-caret-map-index');
181
+ const index = rawIndex !== null && /^\d+$/.test(rawIndex) ? Number(rawIndex) : -1;
182
+ const target = bindings[index];
146
183
  if (target?.el) {
147
184
  target.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
148
185
  target.el.classList.add('cms-map-highlight');
@@ -156,17 +193,19 @@ function toggle() {
156
193
  if (!panel) panel = createPanel();
157
194
  isOpen = !isOpen;
158
195
  panel.classList.toggle('cms-content-map-open', isOpen);
196
+ panel.setAttribute('aria-hidden', String(!isOpen));
159
197
  if (isOpen) refresh();
198
+ else refreshGeneration++;
160
199
  }
161
200
 
201
+ /** @param {string} str @returns {string} */
162
202
  function escapeHtml(str) {
163
203
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
164
204
  }
165
205
 
166
- function escapeAttr(str) {
167
- return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
168
- }
169
-
206
+ /** @param {{ mapButton: Element | null }} options */
170
207
  export function mountContentMap({ mapButton }) {
171
- mapButton?.addEventListener('click', toggle);
208
+ if (!mapButton || mountedButtons.has(mapButton)) return;
209
+ mountedButtons.add(mapButton);
210
+ mapButton.addEventListener('click', toggle);
172
211
  }
@@ -0,0 +1,66 @@
1
+ /** @typedef {'saving' | 'idle' | 'error'} ToolbarStatusState */
2
+ /**
3
+ * @typedef {object} DeploymentStatusResponse
4
+ * @property {boolean} configured
5
+ * @property {'deploying' | 'live' | 'failed' | 'unknown'} state
6
+ * @property {unknown} [target]
7
+ * @property {unknown} [buildId]
8
+ */
9
+
10
+ /**
11
+ * Poll the authenticated deployment route and translate provider states into
12
+ * the toolbar's small status surface.
13
+ *
14
+ * @param {{
15
+ * fetchStatus: () => Promise<Response>,
16
+ * setStatus: (state: ToolbarStatusState, text: string) => void,
17
+ * setBuildId: (buildId: string | null) => void,
18
+ * pollDelayMs?: number,
19
+ * setTimer?: (callback: () => void, delay: number) => number,
20
+ * clearTimer?: (timer: number) => void,
21
+ * }} options
22
+ */
23
+ export function createDeploymentStatusPoller(options) {
24
+ const pollDelayMs = options.pollDelayMs ?? 1000;
25
+ const setTimer = options.setTimer ?? ((callback, delay) => window.setTimeout(callback, delay));
26
+ const clearTimer = options.clearTimer ?? ((timer) => window.clearTimeout(timer));
27
+ let timer = 0;
28
+
29
+ function stop() {
30
+ if (timer) clearTimer(timer);
31
+ timer = 0;
32
+ }
33
+
34
+ function schedule() {
35
+ stop();
36
+ timer = setTimer(() => { void refresh(); }, pollDelayMs);
37
+ }
38
+
39
+ async function refresh() {
40
+ try {
41
+ const response = await options.fetchStatus();
42
+ if (!response.ok) return;
43
+ const data = /** @type {DeploymentStatusResponse} */ (await response.json());
44
+ if (!data.configured || !data.target) return;
45
+ options.setBuildId(typeof data.buildId === 'string' ? data.buildId : null);
46
+ if (data.state === 'deploying') {
47
+ options.setStatus('saving', 'Deploying…');
48
+ schedule();
49
+ } else if (data.state === 'live') {
50
+ stop();
51
+ options.setStatus('idle', 'Live');
52
+ } else if (data.state === 'failed') {
53
+ stop();
54
+ options.setStatus('error', 'Deploy failed');
55
+ } else {
56
+ stop();
57
+ options.setStatus('error', 'Deploy status unknown');
58
+ }
59
+ } catch {
60
+ // A transient status lookup must not turn a successful content save into
61
+ // an error. A reload or a scheduled in-flight check can retry later.
62
+ }
63
+ }
64
+
65
+ return { refresh, schedule, stop };
66
+ }