@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,91 @@
1
+ /* ---------------------------------------------------------------------------
2
+ Studio Panel — sliding left sidebar with iframe
3
+ --------------------------------------------------------------------------- */
4
+
5
+ .cms-studio-panel {
6
+ position: fixed;
7
+ top: 0;
8
+ left: 0;
9
+ bottom: 0;
10
+ width: 380px;
11
+ z-index: 100000;
12
+ background: #0a1128;
13
+ border-right: 1px solid rgba(212, 175, 55, 0.12);
14
+ box-shadow: 4px 0 30px rgba(0, 0, 0, 0.3);
15
+ transform: translateX(-100%);
16
+ transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
17
+ }
18
+
19
+ .cms-studio-panel.open {
20
+ transform: translateX(0);
21
+ }
22
+
23
+ .cms-studio-panel.no-transition {
24
+ transition: none;
25
+ }
26
+
27
+ .cms-studio-panel iframe {
28
+ width: 100%;
29
+ height: 100%;
30
+ border: none;
31
+ display: block;
32
+ }
33
+
34
+ /* Push body content when panel is open */
35
+ body.cms-panel-open {
36
+ padding-left: 380px;
37
+ transition: padding-left 0.5s cubic-bezier(0.16, 1, 0.3, 1);
38
+ }
39
+
40
+ body.cms-panel-open.no-transition {
41
+ transition: none;
42
+ }
43
+
44
+ /* Shift fixed header */
45
+ body.cms-panel-open #main-header {
46
+ left: 380px;
47
+ transition: left 0.5s cubic-bezier(0.16, 1, 0.3, 1);
48
+ }
49
+
50
+ body.cms-panel-open.no-transition #main-header {
51
+ transition: none;
52
+ }
53
+
54
+ /* Shift fixed toolbar */
55
+ body.cms-panel-open .cms-toolbar {
56
+ left: 380px;
57
+ transition: left 0.5s cubic-bezier(0.16, 1, 0.3, 1);
58
+ }
59
+
60
+ body.cms-panel-open.no-transition .cms-toolbar {
61
+ transition: none;
62
+ }
63
+
64
+ /* Studio button active state */
65
+ .cms-studio-btn.active {
66
+ background: rgba(212, 175, 55, 0.3);
67
+ border-color: rgba(212, 175, 55, 0.5);
68
+ color: var(--cms-gold);
69
+ box-shadow: 0 0 8px rgba(212, 175, 55, 0.15);
70
+ }
71
+
72
+
73
+ /* Mobile: full-width overlay, no content push */
74
+ @media (max-width: 768px) {
75
+ .cms-studio-panel {
76
+ width: 100%;
77
+ box-shadow: none;
78
+ }
79
+
80
+ body.cms-panel-open {
81
+ padding-left: 0;
82
+ }
83
+
84
+ body.cms-panel-open #main-header {
85
+ left: 0;
86
+ }
87
+
88
+ body.cms-panel-open .cms-toolbar {
89
+ left: 0;
90
+ }
91
+ }
@@ -0,0 +1,64 @@
1
+ import { MOUNT_PATH } from './config.js';
2
+
3
+ export function mountStudioPanel({ studioButton }) {
4
+ const panel = document.createElement('div');
5
+ panel.className = 'cms-studio-panel';
6
+
7
+ const iframe = document.createElement('iframe');
8
+ iframe.src = `${MOUNT_PATH}/cms`;
9
+ iframe.title = 'Content Studio';
10
+ panel.appendChild(iframe);
11
+ document.body.appendChild(panel);
12
+
13
+ function openPanel(animate = true) {
14
+ if (!animate) {
15
+ panel.classList.add('no-transition');
16
+ document.body.classList.add('no-transition');
17
+ }
18
+ panel.classList.add('open');
19
+ document.body.classList.add('cms-panel-open');
20
+ studioButton?.classList.add('active');
21
+ sessionStorage.setItem('cms-panel-open', '1');
22
+ if (!animate) {
23
+ requestAnimationFrame(() => {
24
+ requestAnimationFrame(() => {
25
+ panel.classList.remove('no-transition');
26
+ document.body.classList.remove('no-transition');
27
+ });
28
+ });
29
+ }
30
+ }
31
+
32
+ function closePanel() {
33
+ panel.classList.remove('open');
34
+ document.body.classList.remove('cms-panel-open');
35
+ studioButton?.classList.remove('active');
36
+ sessionStorage.setItem('cms-panel-open', '0');
37
+ }
38
+
39
+ function togglePanel() {
40
+ if (panel.classList.contains('open')) {
41
+ closePanel();
42
+ } else {
43
+ openPanel();
44
+ }
45
+ }
46
+
47
+ studioButton?.addEventListener('click', togglePanel);
48
+
49
+ // Restore panel state from sessionStorage (without animation)
50
+ if (sessionStorage.getItem('cms-panel-open') === '1') {
51
+ openPanel(false);
52
+ }
53
+
54
+ // Escape key closes panel (only when no CMS element is focused)
55
+ document.addEventListener('keydown', (e) => {
56
+ if (e.key !== 'Escape' || !panel.classList.contains('open')) return;
57
+ const focused = document.activeElement;
58
+ if (!focused || !focused.matches('[data-cms]')) {
59
+ closePanel();
60
+ }
61
+ });
62
+
63
+ return { iframe, closePanel };
64
+ }
@@ -0,0 +1,167 @@
1
+ import { API_BASE } from './config.js';
2
+ import { mutateHeaders } from './security.js';
3
+
4
+ function entryKey(collection, id) {
5
+ return `${collection}::${id}`;
6
+ }
7
+
8
+ function isRecord(value) {
9
+ return typeof value === 'object' && value !== null;
10
+ }
11
+
12
+ function getNestedStringValue(data, path) {
13
+ if (!isRecord(data)) return undefined;
14
+
15
+ let current = data;
16
+ for (const segment of path.split('.')) {
17
+ if (!isRecord(current) && !Array.isArray(current)) return undefined;
18
+ current = current[segment];
19
+ }
20
+
21
+ return typeof current === 'string' ? current : undefined;
22
+ }
23
+
24
+ async function fetchEntrySnapshot(collection, id, onUnauthorized) {
25
+ const url = `${API_BASE}/entries?collection=${encodeURIComponent(collection)}&id=${encodeURIComponent(
26
+ id,
27
+ )}`;
28
+ const res = await fetch(url);
29
+ if (res.status === 401) {
30
+ onUnauthorized();
31
+ return null;
32
+ }
33
+ if (!res.ok) throw new Error('Failed to load entry');
34
+
35
+ const json = await res.json();
36
+ const entry = Array.isArray(json.entries) ? json.entries[0] : null;
37
+ if (!entry || !isRecord(entry.data)) {
38
+ return { revision: 0, data: {} };
39
+ }
40
+
41
+ return {
42
+ revision: Number.isInteger(entry.revision) ? entry.revision : 0,
43
+ data: entry.data,
44
+ };
45
+ }
46
+
47
+ export function createSaveField({ setStatus, onUnauthorized }) {
48
+ let saveQueue = Promise.resolve();
49
+ const revisionByEntry = new Map();
50
+ const revisionLoadByEntry = new Map();
51
+
52
+ async function ensureRevision(collection, id) {
53
+ const key = entryKey(collection, id);
54
+ if (revisionByEntry.has(key)) {
55
+ return revisionByEntry.get(key);
56
+ }
57
+
58
+ const inFlight = revisionLoadByEntry.get(key);
59
+ if (inFlight) return inFlight;
60
+
61
+ const task = (async () => {
62
+ const snapshot = await fetchEntrySnapshot(collection, id, onUnauthorized);
63
+ if (!snapshot) return null;
64
+ revisionByEntry.set(key, snapshot.revision);
65
+ return snapshot.revision;
66
+ })().finally(() => {
67
+ revisionLoadByEntry.delete(key);
68
+ });
69
+
70
+ revisionLoadByEntry.set(key, task);
71
+ return task;
72
+ }
73
+
74
+ async function readLatestFieldValue(collection, id, field) {
75
+ const snapshot = await fetchEntrySnapshot(collection, id, onUnauthorized);
76
+ if (!snapshot) return { currentRevision: undefined, latestValue: undefined };
77
+
78
+ revisionByEntry.set(entryKey(collection, id), snapshot.revision);
79
+ return {
80
+ currentRevision: snapshot.revision,
81
+ latestValue: getNestedStringValue(snapshot.data, field),
82
+ };
83
+ }
84
+
85
+ async function execSave(collection, id, field, value) {
86
+ setStatus('saving', 'Saving...');
87
+ const key = entryKey(collection, id);
88
+
89
+ try {
90
+ const expectedRevision = await ensureRevision(collection, id);
91
+ if (expectedRevision === null) {
92
+ return { ok: false, reason: 'unauthorized' };
93
+ }
94
+
95
+ const body = { type: 'save_field', collection, id, field, value };
96
+ if (typeof expectedRevision === 'number') {
97
+ body.expectedRevision = expectedRevision;
98
+ }
99
+
100
+ const res = await fetch(`${API_BASE}/mutate`, {
101
+ method: 'POST',
102
+ headers: await mutateHeaders(),
103
+ body: JSON.stringify(body),
104
+ });
105
+ if (res.status === 401) {
106
+ onUnauthorized();
107
+ return { ok: false, reason: 'unauthorized' };
108
+ }
109
+
110
+ const json = await res.json().catch(() => ({}));
111
+ if (!res.ok) {
112
+ if (res.status === 409) {
113
+ const currentRevision = Number.isInteger(json.currentRevision)
114
+ ? json.currentRevision
115
+ : undefined;
116
+ if (typeof currentRevision === 'number') {
117
+ revisionByEntry.set(key, currentRevision);
118
+ }
119
+
120
+ let latest = { currentRevision: undefined, latestValue: undefined };
121
+ try {
122
+ latest = await readLatestFieldValue(collection, id, field);
123
+ } catch {
124
+ // Keep conflict semantics even if a follow-up refresh fails.
125
+ }
126
+ setStatus('error', 'Revision conflict');
127
+ return {
128
+ ok: false,
129
+ reason: 'conflict',
130
+ currentRevision:
131
+ typeof latest.currentRevision === 'number'
132
+ ? latest.currentRevision
133
+ : currentRevision,
134
+ latestValue: latest.latestValue,
135
+ };
136
+ }
137
+
138
+ setStatus('error', 'Save failed');
139
+ return { ok: false, reason: 'error' };
140
+ }
141
+
142
+ const nextRevision = Number.isInteger(json.revision)
143
+ ? json.revision
144
+ : typeof expectedRevision === 'number'
145
+ ? expectedRevision + 1
146
+ : undefined;
147
+ if (typeof nextRevision === 'number') {
148
+ revisionByEntry.set(key, nextRevision);
149
+ }
150
+
151
+ setStatus('idle', 'Saved');
152
+ return { ok: true, revision: nextRevision };
153
+ } catch {
154
+ setStatus('error', 'Save failed');
155
+ return { ok: false, reason: 'error' };
156
+ }
157
+ }
158
+
159
+ return function saveField(collection, id, field, value) {
160
+ const task = saveQueue.then(() => execSave(collection, id, field, value));
161
+ saveQueue = task.then(
162
+ () => undefined,
163
+ () => undefined,
164
+ );
165
+ return task;
166
+ };
167
+ }
@@ -0,0 +1,10 @@
1
+ export function bindSectionActivation({ sectionNode, sectionId, setActiveSection }) {
2
+ sectionNode.addEventListener('mouseenter', () => setActiveSection(sectionId));
3
+ sectionNode.addEventListener('focusin', () => setActiveSection(sectionId));
4
+ sectionNode.addEventListener('pointerdown', (event) => {
5
+ const target = event.target;
6
+ if (!(target instanceof Element)) return;
7
+ if (target.closest('.cms-section-add-picker')) return;
8
+ setActiveSection(sectionId);
9
+ });
10
+ }
@@ -0,0 +1,88 @@
1
+ import { API_BASE } from '../config.js';
2
+ import { isRecord } from './utils.js';
3
+ import { mutateHeaders } from '../security.js';
4
+
5
+ export async function fetchPageEntry({ collection, id, onUnauthorized }) {
6
+ const url = `${API_BASE}/entries?collection=${encodeURIComponent(collection)}&id=${encodeURIComponent(
7
+ id,
8
+ )}`;
9
+ const res = await fetch(url);
10
+ if (res.status === 401) {
11
+ onUnauthorized();
12
+ return null;
13
+ }
14
+ if (!res.ok) throw new Error('Failed to load page data');
15
+
16
+ const json = await res.json();
17
+ const entry = Array.isArray(json.entries) ? json.entries[0] : null;
18
+ if (!entry || !isRecord(entry.data)) {
19
+ throw new Error('Page entry not found');
20
+ }
21
+
22
+ return {
23
+ data: entry.data,
24
+ revision: Number.isInteger(entry.revision) ? entry.revision : 0,
25
+ };
26
+ }
27
+
28
+ export async function savePageLayout({
29
+ id,
30
+ sections,
31
+ expectedRevision,
32
+ onUnauthorized,
33
+ }) {
34
+ const res = await fetch(`${API_BASE}/mutate`, {
35
+ method: 'POST',
36
+ headers: await mutateHeaders(),
37
+ body: JSON.stringify({
38
+ type: 'update_page_layout',
39
+ id,
40
+ sections,
41
+ expectedRevision,
42
+ }),
43
+ });
44
+
45
+ if (res.status === 401) {
46
+ onUnauthorized();
47
+ return { ok: false, reason: 'unauthorized' };
48
+ }
49
+
50
+ const json = await res.json().catch(() => ({}));
51
+ if (!res.ok) {
52
+ if (res.status === 409) {
53
+ return {
54
+ ok: false,
55
+ reason: 'conflict',
56
+ currentRevision: Number.isInteger(json.currentRevision) ? json.currentRevision : null,
57
+ };
58
+ }
59
+ const issue = Array.isArray(json.issues) ? json.issues[0] : null;
60
+ const issueDetail =
61
+ issue &&
62
+ typeof issue === 'object' &&
63
+ typeof issue.path === 'string' &&
64
+ typeof issue.message === 'string'
65
+ ? `${issue.path}: ${issue.message}`
66
+ : null;
67
+ return {
68
+ ok: false,
69
+ reason: 'error',
70
+ error:
71
+ typeof json.error === 'string'
72
+ ? issueDetail
73
+ ? `${json.error} (${issueDetail})`
74
+ : json.error
75
+ : 'Failed to save layout',
76
+ };
77
+ }
78
+
79
+ return {
80
+ ok: true,
81
+ revision:
82
+ Number.isInteger(json.revision)
83
+ ? json.revision
84
+ : Number.isInteger(expectedRevision)
85
+ ? expectedRevision + 1
86
+ : 1,
87
+ };
88
+ }
@@ -0,0 +1,24 @@
1
+ export const HOME_SECTION_KEYS = [
2
+ 'home.hero',
3
+ 'home.logo_bar',
4
+ 'home.features',
5
+ 'home.theme_showcase',
6
+ 'home.how_it_works',
7
+ 'home.use_cases',
8
+ 'home.composer',
9
+ 'home.spacer',
10
+ 'home.testimonials',
11
+ 'home.pricing',
12
+ 'home.faq',
13
+ 'home.cta',
14
+ ];
15
+
16
+ export const SPACING_Y_OPTIONS = [
17
+ { value: 'default', label: 'Default' },
18
+ { value: 'compact', label: 'Compact' },
19
+ { value: 'comfortable', label: 'Comfortable' },
20
+ { value: 'spacious', label: 'Spacious' },
21
+ { value: 'cinematic', label: 'Cinematic' },
22
+ ];
23
+
24
+ export const SPACING_Y_VALUES = SPACING_Y_OPTIONS.map((item) => item.value);