@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,76 @@
1
+ import { HOME_SECTION_KEYS } from './constants.js';
2
+ import { normalizeSpacingY, isRecord } from './utils.js';
3
+
4
+ const LEGACY_SECTION_KEY_MAP = {
5
+ 'home.trust_badges': 'home.logo_bar',
6
+ 'home.featured': 'home.features',
7
+ };
8
+
9
+ function normalizeSectionKey(rawKey) {
10
+ if (typeof rawKey !== 'string') return null;
11
+ const mapped = LEGACY_SECTION_KEY_MAP[rawKey] || rawKey;
12
+ return HOME_SECTION_KEYS.includes(mapped) ? mapped : null;
13
+ }
14
+
15
+ export function normalizeSectionsFromData(entryData, fallbackFromDom) {
16
+ const layout = isRecord(entryData.layout) ? entryData.layout : {};
17
+ const raw = Array.isArray(layout.sections) ? layout.sections : null;
18
+
19
+ if (!raw) return fallbackFromDom;
20
+
21
+ const normalized = raw
22
+ .map((item, index) => {
23
+ if (!isRecord(item)) return null;
24
+ const key = normalizeSectionKey(item.key);
25
+ if (!key) return null;
26
+
27
+ const id =
28
+ typeof item.id === 'string' && item.id.trim()
29
+ ? item.id.trim()
30
+ : `sec-${key.replace(/\./g, '-')}-${index + 1}`;
31
+
32
+ return {
33
+ id,
34
+ key,
35
+ enabled: item.enabled !== false,
36
+ spacing_y: normalizeSpacingY(item.spacing_y),
37
+ };
38
+ })
39
+ .filter(Boolean);
40
+
41
+ if (!normalized.length) return fallbackFromDom;
42
+ return normalized;
43
+ }
44
+
45
+ export function sectionsFromDom(sectionNodes) {
46
+ return sectionNodes
47
+ .map((el, index) => {
48
+ const rawKey = el.getAttribute('data-caret-section') || 'home.hero';
49
+ const key = normalizeSectionKey(rawKey);
50
+ if (!key) return null;
51
+
52
+ const fallbackId = `sec-${key.replace(/\./g, '-')}-${index + 1}`;
53
+ return {
54
+ id: el.getAttribute('data-caret-section-id') || fallbackId,
55
+ key,
56
+ enabled: true,
57
+ spacing_y: normalizeSpacingY(el.getAttribute('data-caret-spacing-y')),
58
+ };
59
+ })
60
+ .filter(Boolean);
61
+ }
62
+
63
+ export function reorderByDrag({ sections, sourceId, targetId, placeAfter }) {
64
+ if (sourceId === targetId) return sections;
65
+
66
+ const sourceIndex = sections.findIndex((section) => section.id === sourceId);
67
+ const targetIndex = sections.findIndex((section) => section.id === targetId);
68
+ if (sourceIndex === -1 || targetIndex === -1) return sections;
69
+
70
+ const next = [...sections];
71
+ const [source] = next.splice(sourceIndex, 1);
72
+ const currentTargetIndex = next.findIndex((section) => section.id === targetId);
73
+ const insertIndex = currentTargetIndex + (placeAfter ? 1 : 0);
74
+ next.splice(insertIndex, 0, source);
75
+ return next;
76
+ }
@@ -0,0 +1,112 @@
1
+ import { HOME_SECTION_KEYS } from './constants.js';
2
+ import {
3
+ generateSectionId,
4
+ humanizeSectionKey,
5
+ normalizeSpacingY,
6
+ } from './utils.js';
7
+
8
+ export function mutateSections({
9
+ sections,
10
+ action,
11
+ sectionId,
12
+ payload,
13
+ applySpacing,
14
+ }) {
15
+ const index = sections.findIndex((section) => section.id === sectionId);
16
+ if (index === -1) return { ok: false, message: 'Section not found' };
17
+
18
+ if (action === 'up') {
19
+ if (index === 0) return { ok: false, message: 'Section is already first' };
20
+ const next = [...sections];
21
+ [next[index - 1], next[index]] = [next[index], next[index - 1]];
22
+ return { ok: true, message: 'Section moved up', sections: next };
23
+ }
24
+
25
+ if (action === 'down') {
26
+ if (index === sections.length - 1) {
27
+ return { ok: false, message: 'Section is already last' };
28
+ }
29
+ const next = [...sections];
30
+ [next[index], next[index + 1]] = [next[index + 1], next[index]];
31
+ return { ok: true, message: 'Section moved down', sections: next };
32
+ }
33
+
34
+ if (action === 'duplicate') {
35
+ const source = sections[index];
36
+ const duplicate = {
37
+ ...source,
38
+ id: `${source.id}-copy-${Math.random().toString(36).slice(2, 5)}`,
39
+ enabled: true,
40
+ };
41
+ const next = [...sections.slice(0, index + 1), duplicate, ...sections.slice(index + 1)];
42
+ return { ok: true, message: 'Section duplicated', sections: next };
43
+ }
44
+
45
+ if (action === 'delete') {
46
+ if (sections.length <= 1) {
47
+ return { ok: false, message: 'At least one section is required' };
48
+ }
49
+ return {
50
+ ok: true,
51
+ message: 'Section deleted',
52
+ sections: sections.filter((section) => section.id !== sectionId),
53
+ };
54
+ }
55
+
56
+ if (action === 'toggle') {
57
+ const section = sections[index];
58
+ const enabledCount = sections.filter((item) => item.enabled).length;
59
+ if (section.enabled && enabledCount <= 1) {
60
+ return { ok: false, message: 'At least one section must remain visible' };
61
+ }
62
+
63
+ const next = [...sections];
64
+ next[index] = {
65
+ ...section,
66
+ enabled: !section.enabled,
67
+ };
68
+
69
+ return {
70
+ ok: true,
71
+ message: next[index].enabled ? 'Section shown' : 'Section hidden',
72
+ sections: next,
73
+ };
74
+ }
75
+
76
+ if (action === 'add') {
77
+ const key = payload?.sectionKey;
78
+ if (typeof key !== 'string' || !HOME_SECTION_KEYS.includes(key)) {
79
+ return { ok: false, message: 'Invalid section key' };
80
+ }
81
+ const inserted = {
82
+ id: generateSectionId(key),
83
+ key,
84
+ enabled: true,
85
+ };
86
+ const next = [...sections.slice(0, index + 1), inserted, ...sections.slice(index + 1)];
87
+ return {
88
+ ok: true,
89
+ message: `Inserted ${humanizeSectionKey(key)}`,
90
+ sections: next,
91
+ };
92
+ }
93
+
94
+ if (action === 'spacing') {
95
+ const spacingY = normalizeSpacingY(payload?.spacingY);
96
+ const applied = applySpacing(sectionId, spacingY);
97
+ if (!applied) return { ok: false, message: 'Section not found' };
98
+
99
+ const next = [...sections];
100
+ next[index] = {
101
+ ...next[index],
102
+ spacing_y: spacingY,
103
+ };
104
+ return {
105
+ ok: true,
106
+ message: `Spacing set to ${spacingY ? spacingY : 'default'}`,
107
+ sections: next,
108
+ };
109
+ }
110
+
111
+ return { ok: false, message: 'Unknown action' };
112
+ }
@@ -0,0 +1,34 @@
1
+ export function getPageContext(parseCaretAttr, sectionNodes) {
2
+ for (const sectionNode of sectionNodes) {
3
+ if (!(sectionNode instanceof Element)) continue;
4
+ const collection = sectionNode.getAttribute('data-caret-collection');
5
+ const id = sectionNode.getAttribute('data-caret-entry-id');
6
+ if (collection && id) {
7
+ return { collection, id };
8
+ }
9
+ }
10
+
11
+ for (const sectionNode of sectionNodes) {
12
+ if (!(sectionNode instanceof Element)) continue;
13
+ const marker = sectionNode.querySelector('[data-caret]');
14
+ if (!marker) continue;
15
+
16
+ const attr = marker.getAttribute('data-caret');
17
+ if (!attr) continue;
18
+
19
+ const parsed = parseCaretAttr(attr);
20
+ if (!parsed) continue;
21
+ if (parsed.collection !== 'pages') continue;
22
+
23
+ return {
24
+ collection: parsed.collection,
25
+ id: parsed.id,
26
+ };
27
+ }
28
+
29
+ if (window.location.pathname === '/') {
30
+ return { collection: 'pages', id: 'home' };
31
+ }
32
+
33
+ return null;
34
+ }
@@ -0,0 +1,196 @@
1
+ import { HOME_SECTION_KEYS, SPACING_Y_OPTIONS } from './constants.js';
2
+ import { humanizeSectionKey } from './utils.js';
3
+
4
+ function attachGlobalPositioning({ picker, getAnchor }) {
5
+ function position() {
6
+ const anchorButton = getAnchor();
7
+ if (!anchorButton || picker.hidden) return;
8
+
9
+ const rect = anchorButton.getBoundingClientRect();
10
+ const pickerRect = picker.getBoundingClientRect();
11
+
12
+ let left = rect.left;
13
+ if (left + pickerRect.width > window.innerWidth - 12) {
14
+ left = window.innerWidth - pickerRect.width - 12;
15
+ }
16
+ if (left < 12) left = 12;
17
+
18
+ const top = rect.bottom + 8;
19
+ picker.style.left = `${Math.round(left)}px`;
20
+ picker.style.top = `${Math.round(top)}px`;
21
+ }
22
+
23
+ window.addEventListener('resize', position);
24
+ window.addEventListener('scroll', position, true);
25
+ return position;
26
+ }
27
+
28
+ function attachOutsideClose({ picker, getAnchor, close }) {
29
+ document.addEventListener('click', (event) => {
30
+ if (picker.hidden) return;
31
+ const target = event.target;
32
+ if (!(target instanceof Node)) return;
33
+
34
+ const anchorButton = getAnchor();
35
+ if (picker.contains(target)) return;
36
+ if (anchorButton && anchorButton.contains(target)) return;
37
+ close();
38
+ });
39
+ }
40
+
41
+ export function createAddPicker({ onPick }) {
42
+ const picker = document.createElement('div');
43
+ picker.className = 'cms-section-add-picker';
44
+ picker.hidden = true;
45
+ picker.innerHTML = `
46
+ <div class="cms-section-add-picker-head">Insert Section</div>
47
+ <div class="cms-section-add-picker-list"></div>
48
+ `;
49
+
50
+ const list = picker.querySelector('.cms-section-add-picker-list');
51
+ HOME_SECTION_KEYS.forEach((key) => {
52
+ const btn = document.createElement('button');
53
+ btn.type = 'button';
54
+ btn.className = 'cms-section-add-option';
55
+ btn.dataset.key = key;
56
+ btn.textContent = humanizeSectionKey(key);
57
+ list?.appendChild(btn);
58
+ });
59
+
60
+ document.body.appendChild(picker);
61
+
62
+ let openForSectionId = null;
63
+ let anchorButton = null;
64
+
65
+ function close() {
66
+ picker.hidden = true;
67
+ picker.classList.remove('open');
68
+ openForSectionId = null;
69
+ anchorButton = null;
70
+ }
71
+
72
+ const position = attachGlobalPositioning({
73
+ picker,
74
+ getAnchor: () => anchorButton,
75
+ });
76
+
77
+ function open(button, sectionId) {
78
+ anchorButton = button;
79
+ openForSectionId = sectionId;
80
+ picker.hidden = false;
81
+ picker.classList.add('open');
82
+ position();
83
+ }
84
+
85
+ picker.addEventListener('click', (event) => {
86
+ const target = event.target;
87
+ if (!(target instanceof Element)) return;
88
+
89
+ const option = target.closest('.cms-section-add-option');
90
+ if (!(option instanceof HTMLButtonElement)) return;
91
+
92
+ const key = option.dataset.key;
93
+ if (!openForSectionId || !key) return;
94
+ onPick(openForSectionId, key);
95
+ close();
96
+ });
97
+
98
+ attachOutsideClose({
99
+ picker,
100
+ getAnchor: () => anchorButton,
101
+ close,
102
+ });
103
+
104
+ return {
105
+ open,
106
+ close,
107
+ setDisabled(disabled) {
108
+ picker.querySelectorAll('.cms-section-add-option').forEach((button) => {
109
+ button.disabled = disabled;
110
+ });
111
+ },
112
+ };
113
+ }
114
+
115
+ export function createSpacingPicker({ onPick }) {
116
+ const picker = document.createElement('div');
117
+ picker.className = 'cms-section-add-picker cms-section-spacing-picker';
118
+ picker.hidden = true;
119
+ picker.innerHTML = `
120
+ <div class="cms-section-add-picker-head">Vertical Spacing</div>
121
+ <div class="cms-section-add-picker-list"></div>
122
+ `;
123
+
124
+ const list = picker.querySelector('.cms-section-add-picker-list');
125
+ SPACING_Y_OPTIONS.forEach((option) => {
126
+ const btn = document.createElement('button');
127
+ btn.type = 'button';
128
+ btn.className = 'cms-section-add-option';
129
+ btn.dataset.value = option.value;
130
+ btn.textContent = option.label;
131
+ list?.appendChild(btn);
132
+ });
133
+
134
+ document.body.appendChild(picker);
135
+
136
+ let openForSectionId = null;
137
+ let anchorButton = null;
138
+
139
+ function close() {
140
+ picker.hidden = true;
141
+ picker.classList.remove('open');
142
+ openForSectionId = null;
143
+ anchorButton = null;
144
+ }
145
+
146
+ const position = attachGlobalPositioning({
147
+ picker,
148
+ getAnchor: () => anchorButton,
149
+ });
150
+
151
+ function open(button, sectionId, currentValue) {
152
+ anchorButton = button;
153
+ openForSectionId = sectionId;
154
+ picker.hidden = false;
155
+ picker.classList.add('open');
156
+
157
+ picker.querySelectorAll('.cms-section-add-option').forEach((item) => {
158
+ item.classList.toggle(
159
+ 'active',
160
+ (item instanceof HTMLButtonElement ? item.dataset.value : null) ===
161
+ (currentValue || 'default'),
162
+ );
163
+ });
164
+
165
+ position();
166
+ }
167
+
168
+ picker.addEventListener('click', (event) => {
169
+ const target = event.target;
170
+ if (!(target instanceof Element)) return;
171
+
172
+ const option = target.closest('.cms-section-add-option');
173
+ if (!(option instanceof HTMLButtonElement)) return;
174
+
175
+ const value = option.dataset.value;
176
+ if (!openForSectionId || !value) return;
177
+ onPick(openForSectionId, value);
178
+ close();
179
+ });
180
+
181
+ attachOutsideClose({
182
+ picker,
183
+ getAnchor: () => anchorButton,
184
+ close,
185
+ });
186
+
187
+ return {
188
+ open,
189
+ close,
190
+ setDisabled(disabled) {
191
+ picker.querySelectorAll('.cms-section-add-option').forEach((button) => {
192
+ button.disabled = disabled;
193
+ });
194
+ },
195
+ };
196
+ }
@@ -0,0 +1,92 @@
1
+ import { reorderByDrag } from './model.js';
2
+
3
+ export function bindDragSourceHandlers({
4
+ controls,
5
+ sectionId,
6
+ sectionNode,
7
+ setDragSourceId,
8
+ clearDropIndicators,
9
+ }) {
10
+ controls.addEventListener('dragstart', (event) => {
11
+ const target = event.target;
12
+ if (!(target instanceof Element)) return;
13
+ const dragHandle = target.closest('.cms-section-btn-drag');
14
+ if (!(dragHandle instanceof HTMLButtonElement)) return;
15
+
16
+ setDragSourceId(sectionId);
17
+ sectionNode.classList.add('cms-section-drag-source');
18
+ document.body.classList.add('cms-section-dragging');
19
+
20
+ if (event.dataTransfer) {
21
+ event.dataTransfer.effectAllowed = 'move';
22
+ event.dataTransfer.setData('text/plain', sectionId);
23
+ }
24
+ });
25
+
26
+ controls.addEventListener('dragend', () => {
27
+ setDragSourceId(null);
28
+ clearDropIndicators();
29
+ });
30
+ }
31
+
32
+ export function bindDropTargetHandlers({
33
+ sectionNode,
34
+ sectionId,
35
+ isBusy,
36
+ getDragSourceId,
37
+ getSections,
38
+ setSections,
39
+ clearDropIndicators,
40
+ onReordered,
41
+ }) {
42
+ sectionNode.addEventListener('dragover', (event) => {
43
+ const dragSourceId = getDragSourceId();
44
+ if (!dragSourceId || isBusy()) return;
45
+ if (dragSourceId === sectionId) return;
46
+
47
+ event.preventDefault();
48
+ const rect = sectionNode.getBoundingClientRect();
49
+ const placeAfter = event.clientY > rect.top + rect.height / 2;
50
+
51
+ sectionNode.classList.add('cms-section-drop-target');
52
+ sectionNode.classList.toggle('cms-section-drop-before', !placeAfter);
53
+ sectionNode.classList.toggle('cms-section-drop-after', placeAfter);
54
+ sectionNode.dataset.dropPosition = placeAfter ? 'after' : 'before';
55
+
56
+ if (event.dataTransfer) {
57
+ event.dataTransfer.dropEffect = 'move';
58
+ }
59
+ });
60
+
61
+ sectionNode.addEventListener('dragleave', (event) => {
62
+ const related = event.relatedTarget;
63
+ if (related instanceof Node && sectionNode.contains(related)) return;
64
+ sectionNode.classList.remove(
65
+ 'cms-section-drop-target',
66
+ 'cms-section-drop-before',
67
+ 'cms-section-drop-after',
68
+ );
69
+ delete sectionNode.dataset.dropPosition;
70
+ });
71
+
72
+ sectionNode.addEventListener('drop', (event) => {
73
+ const dragSourceId = getDragSourceId();
74
+ if (!dragSourceId || isBusy()) return;
75
+ if (dragSourceId === sectionId) return;
76
+
77
+ event.preventDefault();
78
+ const placeAfter = sectionNode.dataset.dropPosition === 'after';
79
+ const currentSections = getSections();
80
+ const nextSections = reorderByDrag({
81
+ sections: currentSections,
82
+ sourceId: dragSourceId,
83
+ targetId: sectionId,
84
+ placeAfter,
85
+ });
86
+
87
+ clearDropIndicators();
88
+ if (nextSections === currentSections) return;
89
+ setSections(nextSections);
90
+ onReordered();
91
+ });
92
+ }
@@ -0,0 +1,54 @@
1
+ export function createSectionSelectionController({
2
+ sectionNodeById,
3
+ isSelectionLocked,
4
+ }) {
5
+ let activeSectionId = null;
6
+ let observer = null;
7
+
8
+ function setActiveSection(sectionId) {
9
+ if (!sectionId || activeSectionId === sectionId) return;
10
+ activeSectionId = sectionId;
11
+
12
+ sectionNodeById.forEach((node, id) => {
13
+ if (!(node instanceof HTMLElement)) return;
14
+ node.classList.toggle('cms-section-active', id === sectionId);
15
+ });
16
+ }
17
+
18
+ function startObserver() {
19
+ if (typeof IntersectionObserver === 'undefined') return;
20
+ if (observer) {
21
+ observer.disconnect();
22
+ }
23
+
24
+ observer = new IntersectionObserver(
25
+ (entries) => {
26
+ if (isSelectionLocked()) return;
27
+ const visible = entries
28
+ .filter((entry) => entry.isIntersecting)
29
+ .sort((a, b) => b.intersectionRatio - a.intersectionRatio);
30
+ const top = visible[0];
31
+ if (!(top?.target instanceof HTMLElement)) return;
32
+
33
+ const sectionId = top.target.getAttribute('data-caret-section-id');
34
+ if (!sectionId) return;
35
+ setActiveSection(sectionId);
36
+ },
37
+ {
38
+ root: null,
39
+ rootMargin: '-24% 0px -48% 0px',
40
+ threshold: [0.2, 0.35, 0.5, 0.7],
41
+ },
42
+ );
43
+
44
+ sectionNodeById.forEach((node) => {
45
+ if (!(node instanceof HTMLElement)) return;
46
+ observer?.observe(node);
47
+ });
48
+ }
49
+
50
+ return {
51
+ setActiveSection,
52
+ startObserver,
53
+ };
54
+ }
@@ -0,0 +1,83 @@
1
+ import { SPACING_Y_VALUES } from './constants.js';
2
+ import { spacingIndex } from './utils.js';
3
+
4
+ export function createSpacingDragController({
5
+ isBusy,
6
+ isReordering,
7
+ closePickers,
8
+ getSectionSpacing,
9
+ updateSpacingByToken,
10
+ setActiveSection,
11
+ onCommitted,
12
+ }) {
13
+ let dragState = null;
14
+
15
+ function stop() {
16
+ if (!dragState) return;
17
+ window.removeEventListener('pointermove', onMove);
18
+ window.removeEventListener('pointerup', onEnd);
19
+ window.removeEventListener('pointercancel', onEnd);
20
+ document.body.classList.remove('cms-spacing-dragging');
21
+ dragState.handle.classList.remove('is-dragging');
22
+ dragState = null;
23
+ }
24
+
25
+ function start(event, sectionId, handle) {
26
+ if (isBusy()) return;
27
+
28
+ event.preventDefault();
29
+ event.stopPropagation();
30
+ setActiveSection(sectionId);
31
+ closePickers();
32
+
33
+ const currentSpacing = getSectionSpacing(sectionId);
34
+ dragState = {
35
+ sectionId,
36
+ startY: event.clientY,
37
+ startIndex: spacingIndex(currentSpacing),
38
+ currentIndex: spacingIndex(currentSpacing),
39
+ changed: false,
40
+ handle,
41
+ };
42
+
43
+ handle.classList.add('is-dragging');
44
+ document.body.classList.add('cms-spacing-dragging');
45
+ window.addEventListener('pointermove', onMove);
46
+ window.addEventListener('pointerup', onEnd, { once: true });
47
+ window.addEventListener('pointercancel', onEnd, { once: true });
48
+ }
49
+
50
+ function onMove(event) {
51
+ if (!dragState || isBusy() || isReordering()) return;
52
+
53
+ const delta = event.clientY - dragState.startY;
54
+ const step = Math.round(delta / 56);
55
+ const nextIndex = Math.max(
56
+ 0,
57
+ Math.min(SPACING_Y_VALUES.length - 1, dragState.startIndex + step),
58
+ );
59
+
60
+ if (nextIndex === dragState.currentIndex) return;
61
+
62
+ dragState.currentIndex = nextIndex;
63
+ dragState.changed = true;
64
+ const nextToken = SPACING_Y_VALUES[nextIndex];
65
+ updateSpacingByToken(dragState.sectionId, nextToken);
66
+ }
67
+
68
+ function onEnd() {
69
+ if (!dragState) return;
70
+ const changed = dragState.changed;
71
+ const sectionId = dragState.sectionId;
72
+ stop();
73
+ if (changed) {
74
+ onCommitted(sectionId);
75
+ }
76
+ }
77
+
78
+ return {
79
+ start,
80
+ stop,
81
+ isDragging: () => dragState !== null,
82
+ };
83
+ }
@@ -0,0 +1,54 @@
1
+ import { spacingToken, humanizeSectionKey } from './utils.js';
2
+
3
+ export function createSectionControlsElement() {
4
+ const controls = document.createElement('div');
5
+ controls.className = 'cms-section-controls';
6
+ controls.innerHTML = `
7
+ <div class="cms-section-controls-actions">
8
+ <button type="button" class="cms-section-btn cms-section-btn-drag" data-action="drag" title="Drag to reorder" draggable="true">Drag</button>
9
+ <button type="button" class="cms-section-btn" data-action="up" title="Move up">Up</button>
10
+ <button type="button" class="cms-section-btn" data-action="down" title="Move down">Down</button>
11
+ <button type="button" class="cms-section-btn" data-action="duplicate" title="Duplicate">Duplicate</button>
12
+ <button type="button" class="cms-section-btn" data-action="delete" title="Delete">Delete</button>
13
+ <button type="button" class="cms-section-btn" data-action="toggle" title="Toggle visibility">Hide</button>
14
+ <button type="button" class="cms-section-btn" data-action="spacing" title="Set vertical spacing">Space</button>
15
+ </div>
16
+ `;
17
+ return controls;
18
+ }
19
+
20
+ export function createSectionBadge(section, order) {
21
+ const badge = document.createElement('div');
22
+ badge.className = 'cms-section-chip';
23
+ badge.textContent = `${order}. ${humanizeSectionKey(section.key)}`;
24
+ return badge;
25
+ }
26
+
27
+ export function createSpacingDragHandle(section) {
28
+ const handle = document.createElement('button');
29
+ handle.type = 'button';
30
+ handle.className = 'cms-section-gap-handle';
31
+ handle.title = 'Drag vertically to resize section spacing';
32
+ handle.dataset.sectionId = section.id;
33
+ handle.innerHTML = `
34
+ <span class="cms-section-gap-grip"></span>
35
+ <span class="cms-section-gap-label">${
36
+ spacingToken(section.spacing_y) === 'default'
37
+ ? 'Y'
38
+ : `Y:${spacingToken(section.spacing_y)}`
39
+ }</span>
40
+ `;
41
+ return handle;
42
+ }
43
+
44
+ export function createInsertHandle() {
45
+ const handle = document.createElement('button');
46
+ handle.type = 'button';
47
+ handle.className = 'cms-section-insert-handle';
48
+ handle.title = 'Insert section below';
49
+ handle.setAttribute('aria-label', 'Insert section below');
50
+ handle.innerHTML = `
51
+ <span class="cms-section-insert-plus">+</span>
52
+ `;
53
+ return handle;
54
+ }