@caretcms/core 0.2.0 → 0.3.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.
- package/README.md +81 -3
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -9
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +27 -3
- package/dist/loader.js.map +1 -1
- package/dist/markdown/satteri.d.ts.map +1 -1
- package/dist/markdown/satteri.js +32 -8
- package/dist/markdown/satteri.js.map +1 -1
- package/dist/runtime/auth/session.d.ts +1 -0
- package/dist/runtime/auth/session.d.ts.map +1 -1
- package/dist/runtime/auth/session.js +16 -0
- package/dist/runtime/auth/session.js.map +1 -1
- package/dist/runtime/config.d.ts +3 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +4 -1
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/i18n.d.ts +86 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +168 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/middleware.js +40 -2
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +3 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +9 -0
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +121 -10
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +2 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +7 -1
- package/dist/runtime/providers.js.map +1 -1
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +5 -0
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/request-context.d.ts +5 -1
- package/dist/runtime/request-context.d.ts.map +1 -1
- package/dist/runtime/request-context.js +7 -1
- package/dist/runtime/request-context.js.map +1 -1
- package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
- package/dist/runtime/routes/admin-entry.js +114 -18
- package/dist/runtime/routes/admin-entry.js.map +1 -1
- package/dist/runtime/routes/admin.d.ts.map +1 -1
- package/dist/runtime/routes/admin.js +28 -14
- package/dist/runtime/routes/admin.js.map +1 -1
- package/dist/runtime/routes/auth-login.d.ts.map +1 -1
- package/dist/runtime/routes/auth-login.js +4 -0
- package/dist/runtime/routes/auth-login.js.map +1 -1
- package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
- package/dist/runtime/routes/auth-logout.js +7 -2
- package/dist/runtime/routes/auth-logout.js.map +1 -1
- package/dist/runtime/routes/auth-session.d.ts.map +1 -1
- package/dist/runtime/routes/auth-session.js +4 -2
- package/dist/runtime/routes/auth-session.js.map +1 -1
- package/dist/runtime/routes/collection-list.d.ts.map +1 -1
- package/dist/runtime/routes/collection-list.js +64 -41
- package/dist/runtime/routes/collection-list.js.map +1 -1
- package/dist/runtime/routes/entries.d.ts.map +1 -1
- package/dist/runtime/routes/entries.js +15 -6
- package/dist/runtime/routes/entries.js.map +1 -1
- package/dist/runtime/routes/history.d.ts.map +1 -1
- package/dist/runtime/routes/history.js +4 -1
- package/dist/runtime/routes/history.js.map +1 -1
- package/dist/runtime/routes/schema.d.ts.map +1 -1
- package/dist/runtime/routes/schema.js +7 -2
- package/dist/runtime/routes/schema.js.map +1 -1
- package/dist/runtime/routes/studio-home.d.ts.map +1 -1
- package/dist/runtime/routes/studio-home.js +38 -19
- package/dist/runtime/routes/studio-home.js.map +1 -1
- package/dist/runtime/schema-registry.d.ts +8 -1
- package/dist/runtime/schema-registry.d.ts.map +1 -1
- package/dist/runtime/schema-registry.js +47 -3
- package/dist/runtime/schema-registry.js.map +1 -1
- package/dist/runtime/themes/preset-tokens.js +4 -4
- package/dist/runtime/themes/preset-tokens.js.map +1 -1
- package/dist/runtime/views/studio-layout.d.ts +3 -0
- package/dist/runtime/views/studio-layout.d.ts.map +1 -1
- package/dist/runtime/views/studio-layout.js +36 -13
- package/dist/runtime/views/studio-layout.js.map +1 -1
- package/dist/schema-utils.d.ts +7 -0
- package/dist/schema-utils.d.ts.map +1 -1
- package/dist/schema-utils.js +143 -0
- package/dist/schema-utils.js.map +1 -1
- package/dist/types.d.ts +43 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/static/cms/admin-entry.css +135 -13
- package/static/cms/admin-entry.js +506 -117
- package/static/cms/editor/image-edit.js +1 -0
- package/static/cms/editor/panel.js +19 -1
- package/static/cms/editor/sync.js +277 -56
- package/static/cms/editor/tokens-and-text.css +9 -0
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +1 -1
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -72,6 +72,7 @@ export function mountImageEditors({
|
|
|
72
72
|
const input = document.createElement('input');
|
|
73
73
|
input.type = 'file';
|
|
74
74
|
input.accept = 'image/jpeg,image/png,image/webp,image/avif';
|
|
75
|
+
input.setAttribute('aria-label', 'Upload replacement image');
|
|
75
76
|
input.style.display = 'none';
|
|
76
77
|
wrapper.appendChild(input);
|
|
77
78
|
|
|
@@ -24,10 +24,28 @@ export function mountStudioPanel({ studioButton }) {
|
|
|
24
24
|
panel.appendChild(loading);
|
|
25
25
|
|
|
26
26
|
const iframe = document.createElement('iframe');
|
|
27
|
-
|
|
27
|
+
let rememberedPath = '';
|
|
28
|
+
try {
|
|
29
|
+
const storedPath = sessionStorage.getItem('cms-panel-path') || '';
|
|
30
|
+
if (storedPath === STUDIO_PATH || storedPath.startsWith(`${STUDIO_PATH}/`)) {
|
|
31
|
+
rememberedPath = storedPath;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Session storage is optional; the Studio root remains a safe fallback.
|
|
35
|
+
}
|
|
36
|
+
iframe.src = rememberedPath || STUDIO_PATH;
|
|
28
37
|
iframe.title = 'Content Studio';
|
|
29
38
|
iframe.addEventListener('load', () => {
|
|
30
39
|
loading.setAttribute('hidden', 'hidden');
|
|
40
|
+
try {
|
|
41
|
+
const currentPath = `${iframe.contentWindow.location.pathname}${iframe.contentWindow.location.search}`;
|
|
42
|
+
if (currentPath === STUDIO_PATH || currentPath.startsWith(`${STUDIO_PATH}/`)) {
|
|
43
|
+
sessionStorage.setItem('cms-panel-path', currentPath);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// The configured Studio may be cross-origin, in which case its route is
|
|
47
|
+
// intentionally unavailable to the parent page.
|
|
48
|
+
}
|
|
31
49
|
});
|
|
32
50
|
panel.appendChild(iframe);
|
|
33
51
|
document.body.appendChild(panel);
|
|
@@ -1,72 +1,293 @@
|
|
|
1
1
|
import { sanitizeHtml } from './sanitize.js';
|
|
2
|
+
import { parseCaretAttr, resolveBinding } from './helpers.js';
|
|
3
|
+
import { STUDIO_PATH } from './config.js';
|
|
4
|
+
|
|
5
|
+
const SYNC_CHANNEL = 'caretcms:content';
|
|
2
6
|
|
|
3
7
|
export function mountStudioSync({ studioIframe, state, showToast, clientLinkify }) {
|
|
4
|
-
|
|
8
|
+
let refreshTimer = 0;
|
|
9
|
+
let selectedInlineEl = null;
|
|
10
|
+
let linkedSelectionTimer = 0;
|
|
11
|
+
let pendingStudioSelection = null;
|
|
12
|
+
let activeStudioSelection = null;
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
function matchingElements(collection, id, field) {
|
|
15
|
+
const fullKey = `${collection}::${id}::${field}`;
|
|
16
|
+
const matches = Array.from(document.querySelectorAll(`[data-caret="${CSS.escape(fullKey)}"]`));
|
|
17
|
+
document.querySelectorAll(`[data-caret="${CSS.escape(field)}"]`).forEach((el) => {
|
|
18
|
+
const scope = el.closest('[data-caret-scope]')?.getAttribute('data-caret-scope');
|
|
19
|
+
if (scope === `${collection}::${id}` && !matches.includes(el)) matches.push(el);
|
|
20
|
+
});
|
|
21
|
+
return matches;
|
|
22
|
+
}
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
function selectInlineField(collection, id, field, shouldScroll) {
|
|
25
|
+
if (typeof collection !== 'string' || typeof id !== 'string' || typeof field !== 'string') return;
|
|
26
|
+
const target = matchingElements(collection, id, field)[0];
|
|
27
|
+
if (!target) return;
|
|
28
|
+
|
|
29
|
+
window.clearTimeout(linkedSelectionTimer);
|
|
30
|
+
if (selectedInlineEl && selectedInlineEl !== target) {
|
|
31
|
+
selectedInlineEl.classList.remove('cms-linked-selection');
|
|
32
|
+
}
|
|
33
|
+
selectedInlineEl = target;
|
|
34
|
+
target.classList.add('cms-linked-selection');
|
|
35
|
+
if (shouldScroll) {
|
|
36
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Blue confirms that the paired field was located. Green remains the
|
|
40
|
+
// distinct confirmation that an edit was actually saved.
|
|
41
|
+
linkedSelectionTimer = window.setTimeout(() => {
|
|
42
|
+
if (selectedInlineEl !== target) return;
|
|
43
|
+
target.classList.remove('cms-linked-selection');
|
|
44
|
+
selectedInlineEl = null;
|
|
45
|
+
}, 1500);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function publishSelection(message) {
|
|
49
|
+
if (!('BroadcastChannel' in window)) return;
|
|
50
|
+
try {
|
|
51
|
+
const channel = new BroadcastChannel(SYNC_CHANNEL);
|
|
52
|
+
channel.postMessage(message);
|
|
53
|
+
window.setTimeout(() => channel.close(), 0);
|
|
54
|
+
} catch {
|
|
55
|
+
// Cross-tab selection linking is a progressive enhancement.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function studioEntryPath(message) {
|
|
60
|
+
return `${STUDIO_PATH}/${encodeURIComponent(message.collection)}/${encodeURIComponent(message.id)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function postEmbeddedSelection(message) {
|
|
64
|
+
if (!studioIframe?.contentWindow) return;
|
|
65
|
+
try {
|
|
66
|
+
studioIframe.contentWindow.postMessage({ ...message, embedded: true }, window.location.origin);
|
|
67
|
+
} catch {
|
|
68
|
+
// Embedded field linking is a progressive enhancement.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function deliverEmbeddedSelection(message) {
|
|
73
|
+
if (!studioIframe?.contentWindow) return;
|
|
74
|
+
const targetPath = studioEntryPath(message);
|
|
75
|
+
|
|
76
|
+
// A normal mouse interaction can emit pointerdown, focusin, and click.
|
|
77
|
+
// Keep the latest field without restarting the same iframe navigation for
|
|
78
|
+
// each event in that sequence.
|
|
79
|
+
if (pendingStudioSelection && studioIframe.getAttribute('src') === targetPath) {
|
|
80
|
+
pendingStudioSelection = message;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
if (studioIframe.contentWindow.location.pathname !== targetPath) {
|
|
86
|
+
pendingStudioSelection = message;
|
|
87
|
+
studioIframe.src = targetPath;
|
|
88
|
+
try {
|
|
89
|
+
sessionStorage.setItem('cms-panel-path', targetPath);
|
|
90
|
+
} catch {
|
|
91
|
+
// Remembering the route is optional; the current navigation is not.
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Cross-origin Studio panels cannot be inspected or navigated safely;
|
|
97
|
+
// preserve the existing postMessage-only behavior for those setups.
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
postEmbeddedSelection(message);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
studioIframe?.addEventListener('load', () => {
|
|
104
|
+
if (!pendingStudioSelection) return;
|
|
105
|
+
const message = pendingStudioSelection;
|
|
106
|
+
pendingStudioSelection = null;
|
|
107
|
+
window.requestAnimationFrame(() => postEmbeddedSelection(message));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function selectStudioField(el) {
|
|
111
|
+
const resolved = resolveBinding(el, parseCaretAttr(el.getAttribute('data-caret') || ''));
|
|
112
|
+
if (!resolved) return;
|
|
113
|
+
|
|
114
|
+
selectInlineField(resolved.collection, resolved.id, resolved.field, false);
|
|
115
|
+
const message = {
|
|
116
|
+
type: 'cms:field-selected',
|
|
117
|
+
collection: resolved.collection,
|
|
118
|
+
id: resolved.id,
|
|
119
|
+
field: resolved.field,
|
|
120
|
+
source: 'inline',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
activeStudioSelection = message;
|
|
124
|
+
deliverEmbeddedSelection(message);
|
|
125
|
+
publishSelection(message);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Pointerdown covers image-like bindings; focusin covers keyboard navigation
|
|
129
|
+
// and text editing. Click catches non-focusable bound elements.
|
|
130
|
+
document.addEventListener('pointerdown', (event) => {
|
|
131
|
+
const el = event.target instanceof Element ? event.target.closest('[data-caret]') : null;
|
|
132
|
+
if (el) selectStudioField(el);
|
|
133
|
+
});
|
|
134
|
+
document.addEventListener('click', (event) => {
|
|
135
|
+
const el = event.target instanceof Element ? event.target.closest('[data-caret]') : null;
|
|
136
|
+
if (el) selectStudioField(el);
|
|
137
|
+
});
|
|
138
|
+
document.addEventListener('focusin', (event) => {
|
|
139
|
+
const el = event.target instanceof Element ? event.target.closest('[data-caret]') : null;
|
|
140
|
+
if (el) selectStudioField(el);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
function rememberStudioPath(msg) {
|
|
144
|
+
if (typeof msg.studioPath !== 'string') return;
|
|
145
|
+
try {
|
|
146
|
+
sessionStorage.setItem('cms-panel-path', msg.studioPath);
|
|
147
|
+
} catch {
|
|
148
|
+
// Remembering the current Studio route is a progressive enhancement.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function scheduleEmbeddedRefresh(msg) {
|
|
153
|
+
rememberStudioPath(msg);
|
|
154
|
+
|
|
155
|
+
if (state.dirtyEls.size > 0) {
|
|
156
|
+
showToast('Saved in Studio — finish your inline edit before refreshing', 'success');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
showToast('Saved — refreshing visual preview…', 'success');
|
|
161
|
+
window.clearTimeout(refreshTimer);
|
|
162
|
+
refreshTimer = window.setTimeout(() => window.location.reload(), 280);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applySavedMessage(msg) {
|
|
166
|
+
const { collection, id, data } = msg;
|
|
167
|
+
if (!data || typeof data !== 'object') return 0;
|
|
168
|
+
|
|
169
|
+
let updated = 0;
|
|
170
|
+
|
|
171
|
+
function boundElements(selector, path) {
|
|
172
|
+
const matches = Array.from(document.querySelectorAll(`[data-caret="${CSS.escape(selector)}"]`));
|
|
173
|
+
document.querySelectorAll(`[data-caret="${CSS.escape(path)}"]`).forEach((el) => {
|
|
174
|
+
const scope = el.closest('[data-caret-scope]')?.getAttribute('data-caret-scope');
|
|
175
|
+
if (scope === `${collection}::${id}` && !matches.includes(el)) matches.push(el);
|
|
176
|
+
});
|
|
177
|
+
return matches;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function walkData(obj, prefix) {
|
|
181
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
182
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
183
|
+
const selector = `${collection}::${id}::${path}`;
|
|
184
|
+
|
|
185
|
+
if (typeof value === 'string') {
|
|
186
|
+
const els = boundElements(selector, path);
|
|
187
|
+
els.forEach((el) => {
|
|
188
|
+
// Skip elements being actively edited to prevent overwriting unsaved work
|
|
189
|
+
if (state.dirtyEls.has(el) || el === document.activeElement) return;
|
|
190
|
+
|
|
191
|
+
if (el instanceof HTMLImageElement) {
|
|
192
|
+
if (el.src !== value && el.getAttribute('src') !== value) {
|
|
193
|
+
el.src = value;
|
|
194
|
+
updated++;
|
|
195
|
+
}
|
|
196
|
+
} else if (el.hasAttribute('data-caret-rich')) {
|
|
197
|
+
const sanitized = sanitizeHtml(value);
|
|
198
|
+
if (el.innerHTML !== sanitized) {
|
|
199
|
+
el.innerHTML = sanitized;
|
|
200
|
+
updated++;
|
|
201
|
+
}
|
|
202
|
+
} else if (el.hasAttribute('data-caret-raw')) {
|
|
203
|
+
if (el.getAttribute('data-caret-raw') !== value) {
|
|
204
|
+
el.setAttribute('data-caret-raw', value);
|
|
205
|
+
el.innerHTML = clientLinkify(value);
|
|
50
206
|
updated++;
|
|
51
207
|
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
208
|
+
} else if (el.textContent !== value) {
|
|
209
|
+
el.textContent = value;
|
|
210
|
+
updated++;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
214
|
+
walkData(value, path);
|
|
56
215
|
}
|
|
57
216
|
}
|
|
217
|
+
}
|
|
58
218
|
|
|
59
|
-
|
|
219
|
+
walkData(data, '');
|
|
220
|
+
return updated;
|
|
221
|
+
}
|
|
60
222
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
223
|
+
window.addEventListener('message', (e) => {
|
|
224
|
+
// Only accept messages from our iframe
|
|
225
|
+
if (!studioIframe || e.source !== studioIframe.contentWindow) return;
|
|
226
|
+
const msg = e.data;
|
|
227
|
+
if (!msg || typeof msg.type !== 'string') return;
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
msg.type === 'cms:entry-ready'
|
|
231
|
+
&& activeStudioSelection
|
|
232
|
+
&& msg.collection === activeStudioSelection.collection
|
|
233
|
+
&& msg.id === activeStudioSelection.id
|
|
234
|
+
) {
|
|
235
|
+
postEmbeddedSelection(activeStudioSelection);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (msg.type === 'cms:preview') {
|
|
240
|
+
applySavedMessage(msg);
|
|
241
|
+
return;
|
|
66
242
|
}
|
|
67
243
|
|
|
68
|
-
if (msg.type === 'cms:
|
|
69
|
-
|
|
244
|
+
if (msg.type === 'cms:field-selected') {
|
|
245
|
+
selectInlineField(msg.collection, msg.id, msg.field, true);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (msg.type === 'cms:saved') {
|
|
250
|
+
applySavedMessage(msg);
|
|
251
|
+
scheduleEmbeddedRefresh(msg);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (msg.type === 'cms:created' || msg.type === 'cms:deleted') {
|
|
255
|
+
scheduleEmbeddedRefresh(msg);
|
|
70
256
|
}
|
|
71
257
|
});
|
|
258
|
+
|
|
259
|
+
// Standalone Studio pages live in a separate tab, so postMessage cannot
|
|
260
|
+
// reach the site preview. BroadcastChannel provides a same-origin bridge.
|
|
261
|
+
// Reload only after a confirmed mutation. The server remains the source of
|
|
262
|
+
// truth for lists, routing, and derived UI; sites can make the refresh feel
|
|
263
|
+
// continuous with their own cross-document view transitions.
|
|
264
|
+
if ('BroadcastChannel' in window) {
|
|
265
|
+
try {
|
|
266
|
+
const channel = new BroadcastChannel(SYNC_CHANNEL);
|
|
267
|
+
channel.addEventListener('message', (event) => {
|
|
268
|
+
const msg = event.data;
|
|
269
|
+
if (!msg || typeof msg.type !== 'string' || msg.embedded === true) return;
|
|
270
|
+
|
|
271
|
+
if (msg.type === 'cms:field-selected') {
|
|
272
|
+
selectInlineField(msg.collection, msg.id, msg.field, true);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!['cms:saved', 'cms:created', 'cms:deleted'].includes(msg.type)) return;
|
|
276
|
+
|
|
277
|
+
if (state.dirtyEls.size > 0) {
|
|
278
|
+
showToast('Saved in Studio — finish your inline edit before refreshing', 'success');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (msg.type === 'cms:saved') applySavedMessage(msg);
|
|
283
|
+
showToast('Saved — refreshing preview…', 'success');
|
|
284
|
+
window.clearTimeout(refreshTimer);
|
|
285
|
+
refreshTimer = window.setTimeout(() => window.location.reload(), 280);
|
|
286
|
+
});
|
|
287
|
+
window.addEventListener('pagehide', () => channel.close(), { once: true });
|
|
288
|
+
} catch {
|
|
289
|
+
// Cross-tab preview sync is progressive enhancement; iframe sync above
|
|
290
|
+
// remains available in browsers without BroadcastChannel.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
72
293
|
}
|
|
@@ -72,6 +72,15 @@
|
|
|
72
72
|
box-shadow: 0 0 0 4px var(--cms-blue-dim);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* Briefly confirms that the paired Studio field was located. Save feedback is
|
|
76
|
+
green, so linked selection keeps the editor's blue navigation language. */
|
|
77
|
+
.cms-linked-selection {
|
|
78
|
+
outline: 2px solid var(--cms-blue) !important;
|
|
79
|
+
outline-offset: 4px;
|
|
80
|
+
box-shadow: 0 0 0 4px var(--cms-blue-dim);
|
|
81
|
+
transition: outline-color 0.35s ease, box-shadow 0.35s ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
/* ---------------------------------------------------------------------------
|
|
76
85
|
Unpublished body draft
|
|
77
86
|
The server rewrite tags blocks whose prose comes from an unpublished draft
|
package/static/cms/editor.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* ========================================================================
|
|
2
2
|
CMS Editor Styles — only loaded when isEditor = true
|
|
3
3
|
======================================================================== */
|
|
4
|
-
@import "./editor/tokens-and-text.css";
|
|
4
|
+
@import "./editor/tokens-and-text.css?v=studio-upstream-20260819";
|
|
5
5
|
@import "./editor/image.css";
|
|
6
6
|
@import "./editor/section-controls.css";
|
|
7
7
|
@import "./editor/toolbar.css";
|
package/static/cms/editor.js
CHANGED
|
@@ -18,7 +18,7 @@ import { createToast } from './editor/toast.js';
|
|
|
18
18
|
import { mountEditorGuards } from './editor/guards.js';
|
|
19
19
|
import { mountToolbar, normalizePreviewForDelivery } from './editor/toolbar.js';
|
|
20
20
|
import { mountStudioPanel } from './editor/panel.js';
|
|
21
|
-
import { mountStudioSync } from './editor/sync.js';
|
|
21
|
+
import { mountStudioSync } from './editor/sync.js?v=studio-upstream-20260819';
|
|
22
22
|
import { mountTextEditors } from './editor/text-edit.js';
|
|
23
23
|
import { mountImageEditors } from './editor/image-edit.js';
|
|
24
24
|
import { mountSectionControls } from './editor/section-controls.js';
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
--color-theme-border: rgba(255, 255, 255, 0.10);
|
|
31
31
|
|
|
32
32
|
/* ─── Semantic: Accent ────────────────────────────────────────────── */
|
|
33
|
-
--color-theme-accent: #
|
|
33
|
+
--color-theme-accent: #60a5fa;
|
|
34
34
|
--color-theme-accent-soft: rgba(59, 130, 246, 0.12);
|
|
35
35
|
--color-theme-accent-strong: rgba(59, 130, 246, 0.24);
|
|
36
|
-
--color-theme-on-accent: #
|
|
36
|
+
--color-theme-on-accent: #07111f;
|
|
37
37
|
|
|
38
38
|
/* ─── Semantic: Text ──────────────────────────────────────────────── */
|
|
39
39
|
--color-theme-text: rgba(255, 255, 255, 0.92);
|
|
40
|
-
--color-theme-text-muted:
|
|
41
|
-
--color-theme-text-dim:
|
|
40
|
+
--color-theme-text-muted: #b6bac3;
|
|
41
|
+
--color-theme-text-dim: #9298a3;
|
|
42
42
|
|
|
43
43
|
/* ─── Semantic: State ─────────────────────────────────────────────── */
|
|
44
44
|
--color-theme-success: #22c55e;
|
package/static/cms/studio.css
CHANGED
|
@@ -29,6 +29,18 @@ html, body {
|
|
|
29
29
|
box-sizing: border-box;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
.studio-sr-only {
|
|
33
|
+
position: absolute;
|
|
34
|
+
width: 1px;
|
|
35
|
+
height: 1px;
|
|
36
|
+
padding: 0;
|
|
37
|
+
margin: -1px;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
clip: rect(0, 0, 0, 0);
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
border: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
a {
|
|
33
45
|
color: inherit;
|
|
34
46
|
text-decoration: none;
|
|
@@ -47,9 +59,11 @@ a {
|
|
|
47
59
|
/* ─── Layout: Shell + Header ────────────────────────────────────────── */
|
|
48
60
|
|
|
49
61
|
.studio-shell {
|
|
62
|
+
height: 100dvh;
|
|
50
63
|
min-height: 100vh;
|
|
51
64
|
display: flex;
|
|
52
65
|
flex-direction: column;
|
|
66
|
+
overflow: hidden;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
.studio-header {
|
|
@@ -87,6 +101,15 @@ a {
|
|
|
87
101
|
flex-shrink: 0;
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
.studio-editor-identity {
|
|
105
|
+
max-width: 14rem;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
text-overflow: ellipsis;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
color: var(--studio-text-muted);
|
|
110
|
+
font-size: 0.72rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
90
113
|
.studio-brand {
|
|
91
114
|
display: flex;
|
|
92
115
|
align-items: center;
|
|
@@ -113,6 +136,7 @@ a {
|
|
|
113
136
|
|
|
114
137
|
.studio-main {
|
|
115
138
|
flex: 1;
|
|
139
|
+
min-height: 0;
|
|
116
140
|
padding: var(--space-theme-xl) var(--space-theme-lg);
|
|
117
141
|
overflow-y: auto;
|
|
118
142
|
}
|