@astro-live-cms/core 0.2.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 (163) hide show
  1. package/dist/index.d.ts +15 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +122 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/runtime/auth/session.d.ts +14 -0
  6. package/dist/runtime/auth/session.d.ts.map +1 -0
  7. package/dist/runtime/auth/session.js +77 -0
  8. package/dist/runtime/auth/session.js.map +1 -0
  9. package/dist/runtime/bind.d.ts +14 -0
  10. package/dist/runtime/bind.d.ts.map +1 -0
  11. package/dist/runtime/bind.js +11 -0
  12. package/dist/runtime/bind.js.map +1 -0
  13. package/dist/runtime/config.d.ts +6 -0
  14. package/dist/runtime/config.d.ts.map +1 -0
  15. package/dist/runtime/config.js +18 -0
  16. package/dist/runtime/config.js.map +1 -0
  17. package/dist/runtime/content.d.ts +30 -0
  18. package/dist/runtime/content.d.ts.map +1 -0
  19. package/dist/runtime/content.js +48 -0
  20. package/dist/runtime/content.js.map +1 -0
  21. package/dist/runtime/index.d.ts +10 -0
  22. package/dist/runtime/index.d.ts.map +1 -0
  23. package/dist/runtime/index.js +12 -0
  24. package/dist/runtime/index.js.map +1 -0
  25. package/dist/runtime/markers.d.ts +17 -0
  26. package/dist/runtime/markers.d.ts.map +1 -0
  27. package/dist/runtime/markers.js +17 -0
  28. package/dist/runtime/markers.js.map +1 -0
  29. package/dist/runtime/middleware.d.ts +12 -0
  30. package/dist/runtime/middleware.d.ts.map +1 -0
  31. package/dist/runtime/middleware.js +37 -0
  32. package/dist/runtime/middleware.js.map +1 -0
  33. package/dist/runtime/mutations/contracts.d.ts +57 -0
  34. package/dist/runtime/mutations/contracts.d.ts.map +1 -0
  35. package/dist/runtime/mutations/contracts.js +242 -0
  36. package/dist/runtime/mutations/contracts.js.map +1 -0
  37. package/dist/runtime/mutations/engine.d.ts +23 -0
  38. package/dist/runtime/mutations/engine.d.ts.map +1 -0
  39. package/dist/runtime/mutations/engine.js +161 -0
  40. package/dist/runtime/mutations/engine.js.map +1 -0
  41. package/dist/runtime/routes/_helpers.d.ts +6 -0
  42. package/dist/runtime/routes/_helpers.d.ts.map +1 -0
  43. package/dist/runtime/routes/_helpers.js +23 -0
  44. package/dist/runtime/routes/_helpers.js.map +1 -0
  45. package/dist/runtime/routes/admin.d.ts +3 -0
  46. package/dist/runtime/routes/admin.d.ts.map +1 -0
  47. package/dist/runtime/routes/admin.js +110 -0
  48. package/dist/runtime/routes/admin.js.map +1 -0
  49. package/dist/runtime/routes/auth-login.d.ts +4 -0
  50. package/dist/runtime/routes/auth-login.d.ts.map +1 -0
  51. package/dist/runtime/routes/auth-login.js +66 -0
  52. package/dist/runtime/routes/auth-login.js.map +1 -0
  53. package/dist/runtime/routes/auth-logout.d.ts +4 -0
  54. package/dist/runtime/routes/auth-logout.d.ts.map +1 -0
  55. package/dist/runtime/routes/auth-logout.js +51 -0
  56. package/dist/runtime/routes/auth-logout.js.map +1 -0
  57. package/dist/runtime/routes/editor-assets.d.ts +3 -0
  58. package/dist/runtime/routes/editor-assets.d.ts.map +1 -0
  59. package/dist/runtime/routes/editor-assets.js +47 -0
  60. package/dist/runtime/routes/editor-assets.js.map +1 -0
  61. package/dist/runtime/routes/entries.d.ts +3 -0
  62. package/dist/runtime/routes/entries.d.ts.map +1 -0
  63. package/dist/runtime/routes/entries.js +89 -0
  64. package/dist/runtime/routes/entries.js.map +1 -0
  65. package/dist/runtime/routes/history.d.ts +4 -0
  66. package/dist/runtime/routes/history.d.ts.map +1 -0
  67. package/dist/runtime/routes/history.js +56 -0
  68. package/dist/runtime/routes/history.js.map +1 -0
  69. package/dist/runtime/routes/mutate.d.ts +4 -0
  70. package/dist/runtime/routes/mutate.d.ts.map +1 -0
  71. package/dist/runtime/routes/mutate.js +35 -0
  72. package/dist/runtime/routes/mutate.js.map +1 -0
  73. package/dist/runtime/routes/schema.d.ts +3 -0
  74. package/dist/runtime/routes/schema.d.ts.map +1 -0
  75. package/dist/runtime/routes/schema.js +27 -0
  76. package/dist/runtime/routes/schema.js.map +1 -0
  77. package/dist/runtime/routes/studio-home.d.ts +3 -0
  78. package/dist/runtime/routes/studio-home.d.ts.map +1 -0
  79. package/dist/runtime/routes/studio-home.js +174 -0
  80. package/dist/runtime/routes/studio-home.js.map +1 -0
  81. package/dist/runtime/routes/theme-bootstrap.d.ts +4 -0
  82. package/dist/runtime/routes/theme-bootstrap.d.ts.map +1 -0
  83. package/dist/runtime/routes/theme-bootstrap.js +65 -0
  84. package/dist/runtime/routes/theme-bootstrap.js.map +1 -0
  85. package/dist/runtime/routes/theme-factory.d.ts +3 -0
  86. package/dist/runtime/routes/theme-factory.d.ts.map +1 -0
  87. package/dist/runtime/routes/theme-factory.js +142 -0
  88. package/dist/runtime/routes/theme-factory.js.map +1 -0
  89. package/dist/runtime/routes/upload.d.ts +3 -0
  90. package/dist/runtime/routes/upload.d.ts.map +1 -0
  91. package/dist/runtime/routes/upload.js +29 -0
  92. package/dist/runtime/routes/upload.js.map +1 -0
  93. package/dist/runtime/schema/infer-json-schema.d.ts +12 -0
  94. package/dist/runtime/schema/infer-json-schema.d.ts.map +1 -0
  95. package/dist/runtime/schema/infer-json-schema.js +75 -0
  96. package/dist/runtime/schema/infer-json-schema.js.map +1 -0
  97. package/dist/runtime/storage/filesystem-adapter.d.ts +29 -0
  98. package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -0
  99. package/dist/runtime/storage/filesystem-adapter.js +182 -0
  100. package/dist/runtime/storage/filesystem-adapter.js.map +1 -0
  101. package/dist/runtime/storage/filesystem-upload-handler.d.ts +11 -0
  102. package/dist/runtime/storage/filesystem-upload-handler.d.ts.map +1 -0
  103. package/dist/runtime/storage/filesystem-upload-handler.js +37 -0
  104. package/dist/runtime/storage/filesystem-upload-handler.js.map +1 -0
  105. package/dist/runtime/storage/in-memory-adapter.d.ts +24 -0
  106. package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -0
  107. package/dist/runtime/storage/in-memory-adapter.js +78 -0
  108. package/dist/runtime/storage/in-memory-adapter.js.map +1 -0
  109. package/dist/runtime/themes/preset-catalog.d.ts +9 -0
  110. package/dist/runtime/themes/preset-catalog.d.ts.map +1 -0
  111. package/dist/runtime/themes/preset-catalog.js +47 -0
  112. package/dist/runtime/themes/preset-catalog.js.map +1 -0
  113. package/dist/runtime/utils.d.ts +6 -0
  114. package/dist/runtime/utils.d.ts.map +1 -0
  115. package/dist/runtime/utils.js +29 -0
  116. package/dist/runtime/utils.js.map +1 -0
  117. package/dist/types.d.ts +28 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/dist/vite/virtual-config-plugin.d.ts +7 -0
  122. package/dist/vite/virtual-config-plugin.d.ts.map +1 -0
  123. package/dist/vite/virtual-config-plugin.js +25 -0
  124. package/dist/vite/virtual-config-plugin.js.map +1 -0
  125. package/package.json +35 -0
  126. package/static/cms/editor/config.js +6 -0
  127. package/static/cms/editor/guards.js +68 -0
  128. package/static/cms/editor/helpers.js +16 -0
  129. package/static/cms/editor/image-edit.js +148 -0
  130. package/static/cms/editor/image-utils.js +84 -0
  131. package/static/cms/editor/image.css +133 -0
  132. package/static/cms/editor/link-ui.css +143 -0
  133. package/static/cms/editor/linkify.js +55 -0
  134. package/static/cms/editor/panel.css +91 -0
  135. package/static/cms/editor/panel.js +64 -0
  136. package/static/cms/editor/save-queue.js +167 -0
  137. package/static/cms/editor/section-controls/activation.js +10 -0
  138. package/static/cms/editor/section-controls/api.js +88 -0
  139. package/static/cms/editor/section-controls/constants.js +24 -0
  140. package/static/cms/editor/section-controls/index.js +622 -0
  141. package/static/cms/editor/section-controls/model.js +76 -0
  142. package/static/cms/editor/section-controls/mutations.js +112 -0
  143. package/static/cms/editor/section-controls/page-context.js +34 -0
  144. package/static/cms/editor/section-controls/pickers.js +196 -0
  145. package/static/cms/editor/section-controls/reorder.js +92 -0
  146. package/static/cms/editor/section-controls/selection.js +54 -0
  147. package/static/cms/editor/section-controls/spacing-drag.js +83 -0
  148. package/static/cms/editor/section-controls/ui-elements.js +54 -0
  149. package/static/cms/editor/section-controls/utils.js +35 -0
  150. package/static/cms/editor/section-controls.css +349 -0
  151. package/static/cms/editor/section-controls.js +1 -0
  152. package/static/cms/editor/security.js +23 -0
  153. package/static/cms/editor/sync.js +64 -0
  154. package/static/cms/editor/text-edit.js +129 -0
  155. package/static/cms/editor/text-link-interactions.js +191 -0
  156. package/static/cms/editor/toast.css +50 -0
  157. package/static/cms/editor/toast.js +29 -0
  158. package/static/cms/editor/tokens-and-text.css +94 -0
  159. package/static/cms/editor/toolbar.css +261 -0
  160. package/static/cms/editor/toolbar.js +110 -0
  161. package/static/cms/editor.css +10 -0
  162. package/static/cms/editor.js +101 -0
  163. package/static/cms/studio.css +312 -0
@@ -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(parseCmsAttr, sectionNodes) {
2
+ for (const sectionNode of sectionNodes) {
3
+ if (!(sectionNode instanceof Element)) continue;
4
+ const collection = sectionNode.getAttribute('data-cms-collection');
5
+ const id = sectionNode.getAttribute('data-cms-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-cms], [data-cms-img]');
14
+ if (!marker) continue;
15
+
16
+ const attr = marker.getAttribute('data-cms') || marker.getAttribute('data-cms-img');
17
+ if (!attr) continue;
18
+
19
+ const parsed = parseCmsAttr(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-cms-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
+ }
@@ -0,0 +1,35 @@
1
+ import { SPACING_Y_VALUES } from './constants.js';
2
+
3
+ export function humanizeSectionKey(key) {
4
+ const short = key.startsWith('home.') ? key.slice('home.'.length) : key;
5
+ return short.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
6
+ }
7
+
8
+ export function normalizeSpacingY(value) {
9
+ if (typeof value !== 'string') return undefined;
10
+ return SPACING_Y_VALUES.includes(value) && value !== 'default' ? value : undefined;
11
+ }
12
+
13
+ export function spacingToken(value) {
14
+ return normalizeSpacingY(value) || 'default';
15
+ }
16
+
17
+ export function spacingIndex(value) {
18
+ const idx = SPACING_Y_VALUES.indexOf(spacingToken(value));
19
+ return idx === -1 ? 0 : idx;
20
+ }
21
+
22
+ export function spacingButtonLabel(value) {
23
+ const token = spacingToken(value);
24
+ return token === 'default' ? 'Space' : `Space:${token}`;
25
+ }
26
+
27
+ export function isRecord(value) {
28
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
29
+ }
30
+
31
+ export function generateSectionId(key) {
32
+ return `sec-${key.replace(/\./g, '-')}-${Date.now().toString(36)}-${Math.random()
33
+ .toString(36)
34
+ .slice(2, 6)}`;
35
+ }