@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
@@ -23,6 +23,22 @@ import { createSpacingDragController } from './spacing-drag.js';
23
23
  import { bindDragSourceHandlers, bindDropTargetHandlers } from './reorder.js';
24
24
  import { bindSectionActivation } from './activation.js';
25
25
 
26
+ /** @typedef {import('./model.js').Section} Section */
27
+ /** @typedef {import('./page-context.js').PageContext} PageContext */
28
+ /** @typedef {{ collection: string | null, id: string | null, field: string }} ParsedCaretBinding */
29
+ /** @typedef {ReturnType<typeof createSpacingDragController>} SpacingDragController */
30
+ /** @typedef {'saving' | 'idle' | 'error'} ToolbarStatusState */
31
+ /** @typedef {'success' | 'error'} ToastType */
32
+
33
+ /**
34
+ * @param {{
35
+ * parseCaretAttr: (value: string) => ParsedCaretBinding | null,
36
+ * setStatus: (type: ToolbarStatusState, message: string) => void,
37
+ * showToast: (message: string, type?: ToastType) => void,
38
+ * onUnauthorized: () => void,
39
+ * onCanvasStructureChanged?: () => void,
40
+ * }} options
41
+ */
26
42
  export function mountSectionControls({
27
43
  parseCaretAttr,
28
44
  setStatus,
@@ -30,33 +46,46 @@ export function mountSectionControls({
30
46
  onUnauthorized,
31
47
  onCanvasStructureChanged,
32
48
  }) {
49
+ /** @type {HTMLElement[]} */
33
50
  let sectionNodes = Array.from(
34
51
  document.querySelectorAll('[data-caret-section][data-caret-section-id]'),
35
- );
52
+ ).filter((node) => node instanceof HTMLElement);
36
53
  if (!sectionNodes.length) return;
37
54
 
38
- const pageContext = getPageContext(parseCaretAttr, sectionNodes);
39
- if (!pageContext) return;
55
+ const detectedPageContext = getPageContext(parseCaretAttr, sectionNodes);
56
+ if (!detectedPageContext) return;
57
+ /** @type {PageContext} */
58
+ const pageContext = detectedPageContext;
40
59
 
60
+ /** @type {Section[]} */
41
61
  let sections = sectionsFromDom(sectionNodes);
62
+ /** @type {Record<string, unknown>} */
42
63
  let entryData = {};
43
64
  let revision = 0;
44
65
  let busy = false;
66
+ /** @type {string | null} */
45
67
  let dragSourceId = null;
46
68
  let refreshRequired = false;
47
69
 
70
+ /** @type {Map<string, HTMLElement>} */
48
71
  const sectionNodeById = new Map();
72
+ /** @type {Map<string, HTMLButtonElement>} */
49
73
  const toggleButtonsById = new Map();
74
+ /** @type {Map<string, HTMLButtonElement>} */
50
75
  const spacingButtonsById = new Map();
76
+ /** @type {Map<string, HTMLButtonElement>} */
51
77
  const gapHandlesById = new Map();
78
+ /** @type {Map<string, HTMLElement>} */
52
79
  const sectionTemplateByKey = new Map();
80
+ /** @type {SpacingDragController | null} */
53
81
  let spacingDragController = null;
54
82
 
55
83
  sectionNodes.forEach((sectionNode) => {
56
84
  if (!(sectionNode instanceof HTMLElement)) return;
57
85
  const sectionKey = sectionNode.getAttribute('data-caret-section') || 'home.hero';
58
86
  if (sectionTemplateByKey.has(sectionKey)) return;
59
- sectionTemplateByKey.set(sectionKey, sectionNode.cloneNode(true));
87
+ const cloned = sectionNode.cloneNode(true);
88
+ if (cloned instanceof HTMLElement) sectionTemplateByKey.set(sectionKey, cloned);
60
89
  });
61
90
 
62
91
  document
@@ -70,7 +99,8 @@ export function mountSectionControls({
70
99
  '[data-caret-section][data-caret-section-id]',
71
100
  );
72
101
  if (!(contentNode instanceof HTMLElement)) return;
73
- sectionTemplateByKey.set(sectionKey, contentNode.cloneNode(true));
102
+ const cloned = contentNode.cloneNode(true);
103
+ if (cloned instanceof HTMLElement) sectionTemplateByKey.set(sectionKey, cloned);
74
104
  });
75
105
 
76
106
  const addPicker = createAddPicker({
@@ -97,6 +127,7 @@ export function mountSectionControls({
97
127
  },
98
128
  });
99
129
 
130
+ /** @param {Section[]} sectionList @returns {Section[]} */
100
131
  function serializeSectionsForLayout(sectionList) {
101
132
  return sectionList.map((section) => ({
102
133
  id: section.id,
@@ -130,16 +161,17 @@ export function mountSectionControls({
130
161
  document.body.classList.remove('cms-spacing-dragging');
131
162
  }
132
163
 
164
+ /** @param {boolean} disabled */
133
165
  function setControlsDisabled(disabled) {
134
166
  const shouldDisable = disabled || refreshRequired;
135
167
  document.querySelectorAll('.cms-section-btn').forEach((button) => {
136
- button.disabled = shouldDisable;
168
+ if (button instanceof HTMLButtonElement) button.disabled = shouldDisable;
137
169
  });
138
170
  document.querySelectorAll('.cms-section-insert-handle').forEach((button) => {
139
- button.disabled = shouldDisable;
171
+ if (button instanceof HTMLButtonElement) button.disabled = shouldDisable;
140
172
  });
141
173
  document.querySelectorAll('.cms-section-gap-handle').forEach((button) => {
142
- button.disabled = shouldDisable;
174
+ if (button instanceof HTMLButtonElement) button.disabled = shouldDisable;
143
175
  });
144
176
  addPicker.setDisabled(shouldDisable);
145
177
  spacingPicker.setDisabled(shouldDisable);
@@ -162,6 +194,7 @@ export function mountSectionControls({
162
194
  busy || refreshRequired || Boolean(dragSourceId) || Boolean(spacingDragController?.isDragging()),
163
195
  });
164
196
 
197
+ /** @param {string} sectionId @param {string | undefined} spacingY */
165
198
  function setNodeSpacingPreview(sectionId, spacingY) {
166
199
  const node = sectionNodeById.get(sectionId);
167
200
  if (!(node instanceof HTMLElement)) return;
@@ -172,6 +205,7 @@ export function mountSectionControls({
172
205
  }
173
206
  }
174
207
 
208
+ /** @param {string} sectionId */
175
209
  function updateSpacingUi(sectionId) {
176
210
  const section = sections.find((item) => item.id === sectionId);
177
211
  if (!section) return;
@@ -191,6 +225,7 @@ export function mountSectionControls({
191
225
  }
192
226
  }
193
227
 
228
+ /** @param {string} sectionId */
194
229
  function updateToggleUi(sectionId) {
195
230
  const section = sections.find((item) => item.id === sectionId);
196
231
  if (!section) return;
@@ -200,6 +235,7 @@ export function mountSectionControls({
200
235
  }
201
236
  }
202
237
 
238
+ /** @param {string} sectionId @param {boolean} enabled */
203
239
  function setSectionEnabledPreview(sectionId, enabled) {
204
240
  const node = sectionNodeById.get(sectionId);
205
241
  if (!(node instanceof HTMLElement)) return;
@@ -207,6 +243,7 @@ export function mountSectionControls({
207
243
  node.setAttribute('data-caret-section-enabled', enabled === false ? 'false' : 'true');
208
244
  }
209
245
 
246
+ /** @param {string} sectionId @param {string} sectionKey @returns {Section} */
210
247
  function getSectionSnapshot(sectionId, sectionKey) {
211
248
  return sections.find((item) => item.id === sectionId) || {
212
249
  id: sectionId,
@@ -215,6 +252,7 @@ export function mountSectionControls({
215
252
  };
216
253
  }
217
254
 
255
+ /** @param {string} sectionId @param {number} order @param {string} sectionKey */
218
256
  function updateSectionChip(sectionId, order, sectionKey) {
219
257
  const node = sectionNodeById.get(sectionId);
220
258
  if (!(node instanceof HTMLElement)) return;
@@ -226,6 +264,7 @@ export function mountSectionControls({
226
264
  }
227
265
  }
228
266
 
267
+ /** @param {HTMLElement} sectionNode @param {number} sectionIndex */
229
268
  function mountSectionNode(sectionNode, sectionIndex) {
230
269
  if (!(sectionNode instanceof HTMLElement)) return;
231
270
 
@@ -362,6 +401,7 @@ export function mountSectionControls({
362
401
  updateSectionChip(sectionId, sectionIndex + 1, sectionKey);
363
402
  }
364
403
 
404
+ /** @param {Section[]} nextSections @returns {boolean} */
365
405
  function applySectionsToCanvas(nextSections) {
366
406
  if (!Array.isArray(nextSections) || !nextSections.length) return false;
367
407
 
@@ -376,6 +416,7 @@ export function mountSectionControls({
376
416
  existingById.set(id, node);
377
417
  });
378
418
 
419
+ /** @type {HTMLElement[]} */
379
420
  const nextNodes = [];
380
421
  let structureChanged = false;
381
422
 
@@ -441,6 +482,9 @@ export function mountSectionControls({
441
482
  return true;
442
483
  }
443
484
 
485
+ /**
486
+ * @param {{ statusMessage: string, toastMessage: string, statusType?: ToolbarStatusState, toastType?: ToastType }} options
487
+ */
444
488
  function markRefreshRequired({
445
489
  statusMessage,
446
490
  toastMessage,
@@ -480,6 +524,7 @@ export function mountSectionControls({
480
524
  showToast('Layout conflict resolved with latest content.', 'success');
481
525
  }
482
526
 
527
+ /** @param {string} sectionId @param {unknown} spacingValue @returns {boolean} */
483
528
  function applySectionSpacing(sectionId, spacingValue) {
484
529
  const index = sections.findIndex((section) => section.id === sectionId);
485
530
  if (index === -1) return false;
@@ -493,6 +538,11 @@ export function mountSectionControls({
493
538
  return true;
494
539
  }
495
540
 
541
+ /**
542
+ * @param {string} action
543
+ * @param {string} sectionId
544
+ * @param {{ sectionKey?: unknown, spacingY?: unknown }} [payload]
545
+ */
496
546
  function mutateSections(action, sectionId, payload) {
497
547
  const result = mutateSectionsModel({
498
548
  sections,
@@ -523,6 +573,7 @@ export function mountSectionControls({
523
573
  },
524
574
  });
525
575
 
576
+ /** @param {string} successMessage */
526
577
  async function persistWithReload(successMessage) {
527
578
  if (!canEditLayout()) return;
528
579
  if (busy) return;
@@ -535,6 +586,7 @@ export function mountSectionControls({
535
586
 
536
587
  try {
537
588
  const result = await savePageLayout({
589
+ collection: pageContext.collection,
538
590
  id: pageContext.id,
539
591
  sections,
540
592
  expectedRevision: revision,
@@ -1,33 +1,60 @@
1
1
  import { HOME_SECTION_KEYS } from './constants.js';
2
2
  import { normalizeSpacingY, isRecord } from './utils.js';
3
3
 
4
+ /** @typedef {{ id: string, key: string, enabled: boolean, spacing_y?: string }} Section */
5
+
6
+ /** @type {Record<string, string>} */
4
7
  const LEGACY_SECTION_KEY_MAP = {
5
8
  'home.trust_badges': 'home.logo_bar',
6
9
  'home.featured': 'home.features',
7
10
  };
8
11
 
12
+ /** @param {unknown} rawKey @returns {string | null} */
9
13
  function normalizeSectionKey(rawKey) {
10
14
  if (typeof rawKey !== 'string') return null;
11
15
  const mapped = LEGACY_SECTION_KEY_MAP[rawKey] || rawKey;
12
16
  return HOME_SECTION_KEYS.includes(mapped) ? mapped : null;
13
17
  }
14
18
 
19
+ /**
20
+ * @param {string} preferred
21
+ * @param {string} key
22
+ * @param {number} index
23
+ * @param {Set<string>} usedIds
24
+ * @returns {string}
25
+ */
26
+ function uniqueSectionId(preferred, key, index, usedIds) {
27
+ const base = preferred.trim() || `sec-${key.replace(/\./g, '-')}-${index + 1}`;
28
+ let id = base;
29
+ let suffix = 2;
30
+ while (usedIds.has(id)) {
31
+ id = `${base}-${suffix}`;
32
+ suffix++;
33
+ }
34
+ usedIds.add(id);
35
+ return id;
36
+ }
37
+
38
+ /** @param {unknown} entryData @param {Section[]} fallbackFromDom @returns {Section[]} */
15
39
  export function normalizeSectionsFromData(entryData, fallbackFromDom) {
40
+ if (!isRecord(entryData)) return fallbackFromDom;
16
41
  const layout = isRecord(entryData.layout) ? entryData.layout : {};
17
42
  const raw = Array.isArray(layout.sections) ? layout.sections : null;
18
43
 
19
44
  if (!raw) return fallbackFromDom;
20
45
 
46
+ const usedIds = new Set();
21
47
  const normalized = raw
22
48
  .map((item, index) => {
23
49
  if (!isRecord(item)) return null;
24
50
  const key = normalizeSectionKey(item.key);
25
51
  if (!key) return null;
26
52
 
27
- const id =
53
+ const preferredId =
28
54
  typeof item.id === 'string' && item.id.trim()
29
55
  ? item.id.trim()
30
56
  : `sec-${key.replace(/\./g, '-')}-${index + 1}`;
57
+ const id = uniqueSectionId(preferredId, key, index, usedIds);
31
58
 
32
59
  return {
33
60
  id,
@@ -36,13 +63,15 @@ export function normalizeSectionsFromData(entryData, fallbackFromDom) {
36
63
  spacing_y: normalizeSpacingY(item.spacing_y),
37
64
  };
38
65
  })
39
- .filter(Boolean);
66
+ .filter((section) => section !== null);
40
67
 
41
68
  if (!normalized.length) return fallbackFromDom;
42
69
  return normalized;
43
70
  }
44
71
 
72
+ /** @param {Element[]} sectionNodes @returns {Section[]} */
45
73
  export function sectionsFromDom(sectionNodes) {
74
+ const usedIds = new Set();
46
75
  return sectionNodes
47
76
  .map((el, index) => {
48
77
  const rawKey = el.getAttribute('data-caret-section') || 'home.hero';
@@ -50,16 +79,21 @@ export function sectionsFromDom(sectionNodes) {
50
79
  if (!key) return null;
51
80
 
52
81
  const fallbackId = `sec-${key.replace(/\./g, '-')}-${index + 1}`;
82
+ const rawId = el.getAttribute('data-caret-section-id') || fallbackId;
53
83
  return {
54
- id: el.getAttribute('data-caret-section-id') || fallbackId,
84
+ id: uniqueSectionId(rawId, key, index, usedIds),
55
85
  key,
56
86
  enabled: true,
57
87
  spacing_y: normalizeSpacingY(el.getAttribute('data-caret-spacing-y')),
58
88
  };
59
89
  })
60
- .filter(Boolean);
90
+ .filter((section) => section !== null);
61
91
  }
62
92
 
93
+ /**
94
+ * @param {{ sections: Section[], sourceId: string, targetId: string, placeAfter: boolean }} options
95
+ * @returns {Section[]}
96
+ */
63
97
  export function reorderByDrag({ sections, sourceId, targetId, placeAfter }) {
64
98
  if (sourceId === targetId) return sections;
65
99
 
@@ -69,6 +103,7 @@ export function reorderByDrag({ sections, sourceId, targetId, placeAfter }) {
69
103
 
70
104
  const next = [...sections];
71
105
  const [source] = next.splice(sourceIndex, 1);
106
+ if (!source) return sections;
72
107
  const currentTargetIndex = next.findIndex((section) => section.id === targetId);
73
108
  const insertIndex = currentTargetIndex + (placeAfter ? 1 : 0);
74
109
  next.splice(insertIndex, 0, source);
@@ -5,6 +5,19 @@ import {
5
5
  normalizeSpacingY,
6
6
  } from './utils.js';
7
7
 
8
+ /** @typedef {import('./model.js').Section} Section */
9
+ /** @typedef {{ sectionKey?: unknown, spacingY?: unknown }} MutationPayload */
10
+
11
+ /**
12
+ * @param {{
13
+ * sections: Section[],
14
+ * action: string,
15
+ * sectionId: string,
16
+ * payload?: MutationPayload,
17
+ * applySpacing: (sectionId: string, spacingY: string | undefined) => boolean,
18
+ * }} options
19
+ * @returns {{ ok: true, message: string, sections: Section[] } | { ok: false, message: string }}
20
+ */
8
21
  export function mutateSections({
9
22
  sections,
10
23
  action,
@@ -33,6 +46,7 @@ export function mutateSections({
33
46
 
34
47
  if (action === 'duplicate') {
35
48
  const source = sections[index];
49
+ if (!source) return { ok: false, message: 'Section not found' };
36
50
  const duplicate = {
37
51
  ...source,
38
52
  id: `${source.id}-copy-${Math.random().toString(36).slice(2, 5)}`,
@@ -55,6 +69,7 @@ export function mutateSections({
55
69
 
56
70
  if (action === 'toggle') {
57
71
  const section = sections[index];
72
+ if (!section) return { ok: false, message: 'Section not found' };
58
73
  const enabledCount = sections.filter((item) => item.enabled).length;
59
74
  if (section.enabled && enabledCount <= 1) {
60
75
  return { ok: false, message: 'At least one section must remain visible' };
@@ -1,3 +1,11 @@
1
+ /** @typedef {{ collection: string, id: string }} PageContext */
2
+ /** @typedef {{ collection: string | null, id: string | null, field: string }} ParsedCaretBinding */
3
+
4
+ /**
5
+ * @param {(value: string) => ParsedCaretBinding | null} parseCaretAttr
6
+ * @param {Element[]} sectionNodes
7
+ * @returns {PageContext | null}
8
+ */
1
9
  export function getPageContext(parseCaretAttr, sectionNodes) {
2
10
  for (const sectionNode of sectionNodes) {
3
11
  if (!(sectionNode instanceof Element)) continue;
@@ -19,6 +27,7 @@ export function getPageContext(parseCaretAttr, sectionNodes) {
19
27
  const parsed = parseCaretAttr(attr);
20
28
  if (!parsed) continue;
21
29
  if (parsed.collection !== 'pages') continue;
30
+ if (!parsed.id) continue;
22
31
 
23
32
  return {
24
33
  collection: parsed.collection,
@@ -1,6 +1,10 @@
1
1
  import { HOME_SECTION_KEYS, SPACING_Y_OPTIONS } from './constants.js';
2
2
  import { humanizeSectionKey } from './utils.js';
3
3
 
4
+ /**
5
+ * @param {{ picker: HTMLElement, getAnchor: () => HTMLButtonElement | null }} options
6
+ * @returns {() => void}
7
+ */
4
8
  function attachGlobalPositioning({ picker, getAnchor }) {
5
9
  function position() {
6
10
  const anchorButton = getAnchor();
@@ -25,6 +29,9 @@ function attachGlobalPositioning({ picker, getAnchor }) {
25
29
  return position;
26
30
  }
27
31
 
32
+ /**
33
+ * @param {{ picker: HTMLElement, getAnchor: () => HTMLButtonElement | null, close: () => void }} options
34
+ */
28
35
  function attachOutsideClose({ picker, getAnchor, close }) {
29
36
  document.addEventListener('click', (event) => {
30
37
  if (picker.hidden) return;
@@ -38,9 +45,13 @@ function attachOutsideClose({ picker, getAnchor, close }) {
38
45
  });
39
46
  }
40
47
 
48
+ /** @param {{ onPick: (sectionId: string, key: string) => void }} options */
41
49
  export function createAddPicker({ onPick }) {
42
50
  const picker = document.createElement('div');
51
+ picker.id = 'cms-section-add-picker';
43
52
  picker.className = 'cms-section-add-picker';
53
+ picker.setAttribute('role', 'dialog');
54
+ picker.setAttribute('aria-label', 'Insert section');
44
55
  picker.hidden = true;
45
56
  picker.innerHTML = `
46
57
  <div class="cms-section-add-picker-head">Insert Section</div>
@@ -59,10 +70,13 @@ export function createAddPicker({ onPick }) {
59
70
 
60
71
  document.body.appendChild(picker);
61
72
 
73
+ /** @type {string | null} */
62
74
  let openForSectionId = null;
75
+ /** @type {HTMLButtonElement | null} */
63
76
  let anchorButton = null;
64
77
 
65
78
  function close() {
79
+ anchorButton?.setAttribute('aria-expanded', 'false');
66
80
  picker.hidden = true;
67
81
  picker.classList.remove('open');
68
82
  openForSectionId = null;
@@ -74,14 +88,28 @@ export function createAddPicker({ onPick }) {
74
88
  getAnchor: () => anchorButton,
75
89
  });
76
90
 
91
+ /** @param {HTMLButtonElement} button @param {string} sectionId */
77
92
  function open(button, sectionId) {
78
93
  anchorButton = button;
79
94
  openForSectionId = sectionId;
95
+ button.setAttribute('aria-haspopup', 'dialog');
96
+ button.setAttribute('aria-controls', picker.id);
97
+ button.setAttribute('aria-expanded', 'true');
80
98
  picker.hidden = false;
81
99
  picker.classList.add('open');
82
100
  position();
101
+ const firstButton = picker.querySelector('button');
102
+ if (firstButton instanceof HTMLButtonElement) firstButton.focus();
83
103
  }
84
104
 
105
+ picker.addEventListener('keydown', (event) => {
106
+ if (event.key !== 'Escape') return;
107
+ event.preventDefault();
108
+ const previousAnchor = anchorButton;
109
+ close();
110
+ previousAnchor?.focus();
111
+ });
112
+
85
113
  picker.addEventListener('click', (event) => {
86
114
  const target = event.target;
87
115
  if (!(target instanceof Element)) return;
@@ -104,17 +132,22 @@ export function createAddPicker({ onPick }) {
104
132
  return {
105
133
  open,
106
134
  close,
135
+ /** @param {boolean} disabled */
107
136
  setDisabled(disabled) {
108
137
  picker.querySelectorAll('.cms-section-add-option').forEach((button) => {
109
- button.disabled = disabled;
138
+ if (button instanceof HTMLButtonElement) button.disabled = disabled;
110
139
  });
111
140
  },
112
141
  };
113
142
  }
114
143
 
144
+ /** @param {{ onPick: (sectionId: string, value: string) => void }} options */
115
145
  export function createSpacingPicker({ onPick }) {
116
146
  const picker = document.createElement('div');
147
+ picker.id = 'cms-section-spacing-picker';
117
148
  picker.className = 'cms-section-add-picker cms-section-spacing-picker';
149
+ picker.setAttribute('role', 'dialog');
150
+ picker.setAttribute('aria-label', 'Vertical spacing');
118
151
  picker.hidden = true;
119
152
  picker.innerHTML = `
120
153
  <div class="cms-section-add-picker-head">Vertical Spacing</div>
@@ -133,10 +166,13 @@ export function createSpacingPicker({ onPick }) {
133
166
 
134
167
  document.body.appendChild(picker);
135
168
 
169
+ /** @type {string | null} */
136
170
  let openForSectionId = null;
171
+ /** @type {HTMLButtonElement | null} */
137
172
  let anchorButton = null;
138
173
 
139
174
  function close() {
175
+ anchorButton?.setAttribute('aria-expanded', 'false');
140
176
  picker.hidden = true;
141
177
  picker.classList.remove('open');
142
178
  openForSectionId = null;
@@ -148,9 +184,13 @@ export function createSpacingPicker({ onPick }) {
148
184
  getAnchor: () => anchorButton,
149
185
  });
150
186
 
187
+ /** @param {HTMLButtonElement} button @param {string} sectionId @param {unknown} currentValue */
151
188
  function open(button, sectionId, currentValue) {
152
189
  anchorButton = button;
153
190
  openForSectionId = sectionId;
191
+ button.setAttribute('aria-haspopup', 'dialog');
192
+ button.setAttribute('aria-controls', picker.id);
193
+ button.setAttribute('aria-expanded', 'true');
154
194
  picker.hidden = false;
155
195
  picker.classList.add('open');
156
196
 
@@ -163,8 +203,18 @@ export function createSpacingPicker({ onPick }) {
163
203
  });
164
204
 
165
205
  position();
206
+ const selectedButton = picker.querySelector('button.active, button');
207
+ if (selectedButton instanceof HTMLButtonElement) selectedButton.focus();
166
208
  }
167
209
 
210
+ picker.addEventListener('keydown', (event) => {
211
+ if (event.key !== 'Escape') return;
212
+ event.preventDefault();
213
+ const previousAnchor = anchorButton;
214
+ close();
215
+ previousAnchor?.focus();
216
+ });
217
+
168
218
  picker.addEventListener('click', (event) => {
169
219
  const target = event.target;
170
220
  if (!(target instanceof Element)) return;
@@ -187,9 +237,10 @@ export function createSpacingPicker({ onPick }) {
187
237
  return {
188
238
  open,
189
239
  close,
240
+ /** @param {boolean} disabled */
190
241
  setDisabled(disabled) {
191
242
  picker.querySelectorAll('.cms-section-add-option').forEach((button) => {
192
- button.disabled = disabled;
243
+ if (button instanceof HTMLButtonElement) button.disabled = disabled;
193
244
  });
194
245
  },
195
246
  };
@@ -1,5 +1,16 @@
1
1
  import { reorderByDrag } from './model.js';
2
2
 
3
+ /** @typedef {import('./model.js').Section} Section */
4
+
5
+ /**
6
+ * @param {{
7
+ * controls: HTMLElement,
8
+ * sectionId: string,
9
+ * sectionNode: HTMLElement,
10
+ * setDragSourceId: (sectionId: string | null) => void,
11
+ * clearDropIndicators: () => void,
12
+ * }} options
13
+ */
3
14
  export function bindDragSourceHandlers({
4
15
  controls,
5
16
  sectionId,
@@ -29,6 +40,18 @@ export function bindDragSourceHandlers({
29
40
  });
30
41
  }
31
42
 
43
+ /**
44
+ * @param {{
45
+ * sectionNode: HTMLElement,
46
+ * sectionId: string,
47
+ * isBusy: () => boolean,
48
+ * getDragSourceId: () => string | null,
49
+ * getSections: () => Section[],
50
+ * setSections: (sections: Section[]) => void,
51
+ * clearDropIndicators: () => void,
52
+ * onReordered: () => void,
53
+ * }} options
54
+ */
32
55
  export function bindDropTargetHandlers({
33
56
  sectionNode,
34
57
  sectionId,
@@ -1,10 +1,16 @@
1
+ /**
2
+ * @param {{ sectionNodeById: Map<string, HTMLElement>, isSelectionLocked: () => boolean }} options
3
+ */
1
4
  export function createSectionSelectionController({
2
5
  sectionNodeById,
3
6
  isSelectionLocked,
4
7
  }) {
8
+ /** @type {string | null} */
5
9
  let activeSectionId = null;
10
+ /** @type {IntersectionObserver | null} */
6
11
  let observer = null;
7
12
 
13
+ /** @param {string} sectionId */
8
14
  function setActiveSection(sectionId) {
9
15
  if (!sectionId || activeSectionId === sectionId) return;
10
16
  activeSectionId = sectionId;
@@ -1,6 +1,19 @@
1
1
  import { SPACING_Y_VALUES } from './constants.js';
2
2
  import { spacingIndex } from './utils.js';
3
3
 
4
+ /** @typedef {{ sectionId: string, startY: number, startIndex: number, currentIndex: number, changed: boolean, handle: HTMLButtonElement }} SpacingDragState */
5
+
6
+ /**
7
+ * @param {{
8
+ * isBusy: () => boolean,
9
+ * isReordering: () => boolean,
10
+ * closePickers: () => void,
11
+ * getSectionSpacing: (sectionId: string) => string | undefined,
12
+ * updateSpacingByToken: (sectionId: string, token: string) => boolean,
13
+ * setActiveSection: (sectionId: string) => void,
14
+ * onCommitted: (sectionId: string) => void,
15
+ * }} options
16
+ */
4
17
  export function createSpacingDragController({
5
18
  isBusy,
6
19
  isReordering,
@@ -10,6 +23,7 @@ export function createSpacingDragController({
10
23
  setActiveSection,
11
24
  onCommitted,
12
25
  }) {
26
+ /** @type {SpacingDragState | null} */
13
27
  let dragState = null;
14
28
 
15
29
  function stop() {
@@ -22,6 +36,7 @@ export function createSpacingDragController({
22
36
  dragState = null;
23
37
  }
24
38
 
39
+ /** @param {PointerEvent} event @param {string} sectionId @param {HTMLButtonElement} handle */
25
40
  function start(event, sectionId, handle) {
26
41
  if (isBusy()) return;
27
42
 
@@ -47,6 +62,7 @@ export function createSpacingDragController({
47
62
  window.addEventListener('pointercancel', onEnd, { once: true });
48
63
  }
49
64
 
65
+ /** @param {PointerEvent} event */
50
66
  function onMove(event) {
51
67
  if (!dragState || isBusy() || isReordering()) return;
52
68
 
@@ -1,5 +1,8 @@
1
1
  import { spacingToken, humanizeSectionKey } from './utils.js';
2
2
 
3
+ /** @typedef {import('./model.js').Section} Section */
4
+
5
+ /** @returns {HTMLDivElement} */
3
6
  export function createSectionControlsElement() {
4
7
  const controls = document.createElement('div');
5
8
  controls.className = 'cms-section-controls';
@@ -17,6 +20,7 @@ export function createSectionControlsElement() {
17
20
  return controls;
18
21
  }
19
22
 
23
+ /** @param {Section} section @param {number} order @returns {HTMLDivElement} */
20
24
  export function createSectionBadge(section, order) {
21
25
  const badge = document.createElement('div');
22
26
  badge.className = 'cms-section-chip';
@@ -24,11 +28,13 @@ export function createSectionBadge(section, order) {
24
28
  return badge;
25
29
  }
26
30
 
31
+ /** @param {Section} section @returns {HTMLButtonElement} */
27
32
  export function createSpacingDragHandle(section) {
28
33
  const handle = document.createElement('button');
29
34
  handle.type = 'button';
30
35
  handle.className = 'cms-section-gap-handle';
31
36
  handle.title = 'Drag vertically to resize section spacing';
37
+ handle.setAttribute('aria-label', 'Resize vertical section spacing');
32
38
  handle.dataset.sectionId = section.id;
33
39
  handle.innerHTML = `
34
40
  <span class="cms-section-gap-grip"></span>
@@ -41,6 +47,7 @@ export function createSpacingDragHandle(section) {
41
47
  return handle;
42
48
  }
43
49
 
50
+ /** @returns {HTMLButtonElement} */
44
51
  export function createInsertHandle() {
45
52
  const handle = document.createElement('button');
46
53
  handle.type = 'button';