@caretcms/core 0.1.2 → 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 +109 -13
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +31 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -30
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +17 -6
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +57 -14
- package/dist/loader.js.map +1 -1
- package/dist/markdown/block-context.d.ts +21 -0
- package/dist/markdown/block-context.d.ts.map +1 -0
- package/dist/markdown/block-context.js +27 -0
- package/dist/markdown/block-context.js.map +1 -0
- package/dist/markdown/canonical-body.d.ts +30 -0
- package/dist/markdown/canonical-body.d.ts.map +1 -0
- package/dist/markdown/canonical-body.js +54 -0
- package/dist/markdown/canonical-body.js.map +1 -0
- package/dist/markdown/contracts.d.ts +66 -0
- package/dist/markdown/contracts.d.ts.map +1 -0
- package/dist/markdown/contracts.js +102 -0
- package/dist/markdown/contracts.js.map +1 -0
- package/dist/markdown/html-to-snodes.d.ts +26 -0
- package/dist/markdown/html-to-snodes.d.ts.map +1 -0
- package/dist/markdown/html-to-snodes.js +118 -0
- package/dist/markdown/html-to-snodes.js.map +1 -0
- package/dist/markdown/remark.d.ts +35 -0
- package/dist/markdown/remark.d.ts.map +1 -0
- package/dist/markdown/remark.js +66 -0
- package/dist/markdown/remark.js.map +1 -0
- package/dist/markdown/satteri.d.ts +19 -0
- package/dist/markdown/satteri.d.ts.map +1 -0
- package/dist/markdown/satteri.js +144 -0
- package/dist/markdown/satteri.js.map +1 -0
- package/dist/markdown/serialize.d.ts +60 -0
- package/dist/markdown/serialize.d.ts.map +1 -0
- package/dist/markdown/serialize.js +264 -0
- package/dist/markdown/serialize.js.map +1 -0
- package/dist/markdown/splice.d.ts +32 -0
- package/dist/markdown/splice.d.ts.map +1 -0
- package/dist/markdown/splice.js +44 -0
- package/dist/markdown/splice.js.map +1 -0
- package/dist/markdown/stamp.d.ts +68 -0
- package/dist/markdown/stamp.d.ts.map +1 -0
- package/dist/markdown/stamp.js +131 -0
- package/dist/markdown/stamp.js.map +1 -0
- package/dist/runtime/auth/rate-limiter.d.ts.map +1 -1
- package/dist/runtime/auth/rate-limiter.js +5 -0
- package/dist/runtime/auth/rate-limiter.js.map +1 -1
- package/dist/runtime/auth/session.d.ts +3 -3
- package/dist/runtime/auth/session.d.ts.map +1 -1
- package/dist/runtime/auth/session.js +68 -21
- 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/content.d.ts.map +1 -1
- package/dist/runtime/content.js +4 -2
- package/dist/runtime/content.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 +61 -6
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +28 -1
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +76 -0
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts +6 -1
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +271 -51
- 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 +13 -1
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +80 -4
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/request-context.d.ts +14 -2
- 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/rewrite.d.ts.map +1 -1
- package/dist/runtime/rewrite.js +109 -3
- package/dist/runtime/rewrite.js.map +1 -1
- package/dist/runtime/rich-allowlist.d.ts.map +1 -1
- package/dist/runtime/rich-allowlist.js +5 -1
- package/dist/runtime/rich-allowlist.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 +33 -15
- 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 +16 -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 +50 -22
- package/dist/runtime/routes/history.js.map +1 -1
- package/dist/runtime/routes/mutate.d.ts.map +1 -1
- package/dist/runtime/routes/mutate.js +40 -6
- package/dist/runtime/routes/mutate.js.map +1 -1
- package/dist/runtime/routes/publish.d.ts.map +1 -1
- package/dist/runtime/routes/publish.js +5 -2
- package/dist/runtime/routes/publish.js.map +1 -1
- package/dist/runtime/routes/schema.d.ts.map +1 -1
- package/dist/runtime/routes/schema.js +11 -3
- 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/routes/theme-stylesheet.d.ts.map +1 -1
- package/dist/runtime/routes/theme-stylesheet.js +11 -1
- package/dist/runtime/routes/theme-stylesheet.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/storage/atomic-write.d.ts.map +1 -1
- package/dist/runtime/storage/atomic-write.js +8 -3
- package/dist/runtime/storage/atomic-write.js.map +1 -1
- package/dist/runtime/storage/markdown-adapter.d.ts +19 -0
- package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/markdown-adapter.js +41 -0
- package/dist/runtime/storage/markdown-adapter.js.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.d.ts +8 -0
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.js +24 -2
- package/dist/runtime/storage/session-overlay-adapter.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/utils.d.ts +10 -0
- package/dist/runtime/utils.d.ts.map +1 -1
- package/dist/runtime/utils.js +16 -0
- package/dist/runtime/utils.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 +83 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/cms/admin-entry.css +135 -13
- package/static/cms/admin-entry.js +509 -86
- package/static/cms/editor/guards.js +1 -1
- package/static/cms/editor/image-edit.js +42 -11
- package/static/cms/editor/image.css +7 -1
- package/static/cms/editor/md-block-edit.js +176 -0
- package/static/cms/editor/md-serialize.js +301 -0
- package/static/cms/editor/panel.css +69 -25
- package/static/cms/editor/panel.js +58 -1
- package/static/cms/editor/rich-toolbar.js +4 -3
- package/static/cms/editor/sanitize.js +2 -1
- package/static/cms/editor/sync.js +277 -56
- package/static/cms/editor/text-edit.js +68 -17
- package/static/cms/editor/text-link-interactions.js +4 -4
- package/static/cms/editor/toast.css +114 -2
- package/static/cms/editor/toast.js +121 -22
- package/static/cms/editor/tokens-and-text.css +73 -3
- package/static/cms/editor/toolbar.css +19 -6
- package/static/cms/editor/toolbar.js +64 -23
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +14 -3
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -5,6 +5,20 @@ import { sanitizeHtml } from './sanitize.js';
|
|
|
5
5
|
const snapshots = new WeakMap();
|
|
6
6
|
let keydownBound = false;
|
|
7
7
|
|
|
8
|
+
// `contenteditable="plaintext-only"` isn't supported in older Firefox (pre-136);
|
|
9
|
+
// setting it there leaves the element non-editable. Feature-detect once and fall
|
|
10
|
+
// back to "true" (rich contenteditable) — the blur handler saves via textContent
|
|
11
|
+
// for non-rich fields, so the stored value stays plain-text regardless.
|
|
12
|
+
const PLAINTEXT_ONLY_SUPPORTED = (() => {
|
|
13
|
+
try {
|
|
14
|
+
const probe = document.createElement('div');
|
|
15
|
+
probe.setAttribute('contenteditable', 'plaintext-only');
|
|
16
|
+
return probe.contentEditable === 'plaintext-only';
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
|
|
8
22
|
export function mountTextEditors({
|
|
9
23
|
state,
|
|
10
24
|
parseCaretAttr,
|
|
@@ -52,7 +66,10 @@ export function mountTextEditors({
|
|
|
52
66
|
const isRich = el.hasAttribute('data-caret-rich');
|
|
53
67
|
const isLinkable = !isRich && el.hasAttribute('data-caret-raw');
|
|
54
68
|
|
|
55
|
-
el.setAttribute(
|
|
69
|
+
el.setAttribute(
|
|
70
|
+
'contenteditable',
|
|
71
|
+
isRich || !PLAINTEXT_ONLY_SUPPORTED ? 'true' : 'plaintext-only',
|
|
72
|
+
);
|
|
56
73
|
el.classList.add('cms-editable');
|
|
57
74
|
if (isRich) el.classList.add('cms-rich');
|
|
58
75
|
if (isLinkable) el.classList.add('cms-linkable');
|
|
@@ -95,9 +112,48 @@ export function mountTextEditors({
|
|
|
95
112
|
el.addEventListener('input', () => {
|
|
96
113
|
const snapshot = snapshots.get(el);
|
|
97
114
|
const current = isRich ? el.innerHTML : (el.textContent || '');
|
|
98
|
-
|
|
115
|
+
if (current !== snapshot) state.dirtyEls.add(el);
|
|
116
|
+
else state.dirtyEls.delete(el);
|
|
99
117
|
});
|
|
100
118
|
|
|
119
|
+
// Save-conflict resolution: never silently discard the user's edit. Keep
|
|
120
|
+
// their text in the field and offer an explicit choice. "Keep mine" re-saves
|
|
121
|
+
// (the save queue refreshed the cached revision on the 409, so the retry
|
|
122
|
+
// targets the current revision and overwrites); "Load latest" replaces the
|
|
123
|
+
// field with the server value. The element stays dirty until resolved.
|
|
124
|
+
const runConflict = (resolved, mine, latestValue) => {
|
|
125
|
+
state.dirtyEls.add(el);
|
|
126
|
+
el.classList.add('cms-conflict');
|
|
127
|
+
showToast.conflict({
|
|
128
|
+
message: 'This content changed elsewhere while you were editing.',
|
|
129
|
+
onKeepMine: async () => {
|
|
130
|
+
el.classList.remove('cms-conflict');
|
|
131
|
+
el.classList.add('cms-saving');
|
|
132
|
+
const retry = await saveField(resolved.collection, resolved.id, resolved.field, mine);
|
|
133
|
+
el.classList.remove('cms-saving');
|
|
134
|
+
flash(el, retry.ok);
|
|
135
|
+
if (retry.ok) {
|
|
136
|
+
state.dirtyEls.delete(el);
|
|
137
|
+
syncBoundText(attr, mine);
|
|
138
|
+
showToast('Your version saved', 'success');
|
|
139
|
+
} else if (retry.reason === 'conflict') {
|
|
140
|
+
runConflict(resolved, mine, retry.latestValue);
|
|
141
|
+
} else if (retry.reason !== 'unauthorized') {
|
|
142
|
+
showToast('Failed to save. Try again.', 'error');
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
onLoadLatest: () => {
|
|
146
|
+
el.classList.remove('cms-conflict');
|
|
147
|
+
state.dirtyEls.delete(el);
|
|
148
|
+
const value = typeof latestValue === 'string' ? latestValue : mine;
|
|
149
|
+
applyTextValue(el, value);
|
|
150
|
+
snapshots.set(el, isRich ? sanitizeHtml(value) : value);
|
|
151
|
+
syncBoundText(attr, value);
|
|
152
|
+
showToast('Loaded latest version', 'success');
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
101
157
|
el.addEventListener('blur', async () => {
|
|
102
158
|
removeLinkHint();
|
|
103
159
|
|
|
@@ -107,7 +163,7 @@ export function mountTextEditors({
|
|
|
107
163
|
// Skip save if Escape was pressed
|
|
108
164
|
if (el._caretSkipSave) {
|
|
109
165
|
el._caretSkipSave = false;
|
|
110
|
-
state.
|
|
166
|
+
state.dirtyEls.delete(el);
|
|
111
167
|
if (isRich) {
|
|
112
168
|
el.innerHTML = snapshots.get(el) || '';
|
|
113
169
|
} else if (isLinkable) {
|
|
@@ -147,24 +203,19 @@ export function mountTextEditors({
|
|
|
147
203
|
const result = await saveField(resolved.collection, resolved.id, resolved.field, current);
|
|
148
204
|
el.classList.remove('cms-saving');
|
|
149
205
|
flash(el, result.ok);
|
|
150
|
-
state.dirtyEl = null;
|
|
151
206
|
|
|
152
|
-
// Sync all duplicated text bindings
|
|
153
207
|
if (result.ok) {
|
|
208
|
+
state.dirtyEls.delete(el);
|
|
154
209
|
syncBoundText(attr, current);
|
|
155
210
|
showToast('Content saved', 'success');
|
|
156
|
-
} else {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
showToast('Content changed elsewhere. Loaded latest value.', 'error');
|
|
165
|
-
} else if (result.reason !== 'unauthorized') {
|
|
166
|
-
showToast('Failed to save. Changes reverted.', 'error');
|
|
167
|
-
}
|
|
211
|
+
} else if (result.reason === 'conflict') {
|
|
212
|
+
// Preserve the user's edit and let them choose — don't overwrite it.
|
|
213
|
+
runConflict(resolved, current, result.latestValue);
|
|
214
|
+
} else if (result.reason !== 'unauthorized') {
|
|
215
|
+
// Genuine failure (not an auth redirect): revert to the pre-edit snapshot.
|
|
216
|
+
state.dirtyEls.delete(el);
|
|
217
|
+
syncBoundText(attr, snapshot);
|
|
218
|
+
showToast('Failed to save. Changes reverted.', 'error');
|
|
168
219
|
}
|
|
169
220
|
});
|
|
170
221
|
});
|
|
@@ -177,10 +177,10 @@ export function createTextLinkInteractions({
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
state.
|
|
180
|
+
state.dirtyEls.add(focused);
|
|
181
181
|
} else if (url === '' && existingLink) {
|
|
182
182
|
document.execCommand('unlink', false, null);
|
|
183
|
-
state.
|
|
183
|
+
state.dirtyEls.add(focused);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
state.linkPopupEl = null;
|
|
@@ -208,7 +208,7 @@ export function createTextLinkInteractions({
|
|
|
208
208
|
newSel.removeAllRanges();
|
|
209
209
|
newSel.addRange(range);
|
|
210
210
|
document.execCommand('insertText', false, `[${selectedText}](${url})`);
|
|
211
|
-
state.
|
|
211
|
+
state.dirtyEls.add(focused);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
state.linkPopupEl = null;
|
|
@@ -235,7 +235,7 @@ export function createTextLinkInteractions({
|
|
|
235
235
|
} else {
|
|
236
236
|
focused.textContent = snapshot;
|
|
237
237
|
}
|
|
238
|
-
state.
|
|
238
|
+
state.dirtyEls.delete(focused);
|
|
239
239
|
}
|
|
240
240
|
focused._caretSkipSave = true;
|
|
241
241
|
focused.blur();
|
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
Toast notifications
|
|
3
3
|
--------------------------------------------------------------------------- */
|
|
4
4
|
|
|
5
|
-
.cms-toast {
|
|
5
|
+
.cms-toast-stack {
|
|
6
6
|
position: fixed;
|
|
7
7
|
bottom: 62px;
|
|
8
8
|
right: 24px;
|
|
9
9
|
z-index: 100000;
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column-reverse; /* newest nearest the toolbar */
|
|
12
|
+
align-items: flex-end;
|
|
13
|
+
gap: 8px;
|
|
14
|
+
max-width: min(360px, calc(100vw - 48px));
|
|
15
|
+
pointer-events: none; /* the region is inert; interactive toasts opt back in */
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cms-toast {
|
|
10
19
|
display: flex;
|
|
11
20
|
align-items: center;
|
|
12
21
|
gap: 10px;
|
|
@@ -16,7 +25,7 @@
|
|
|
16
25
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
17
26
|
font-family: var(--cms-font);
|
|
18
27
|
font-size: 12px;
|
|
19
|
-
color: rgba(255, 255, 255, 0.
|
|
28
|
+
color: rgba(255, 255, 255, 0.86);
|
|
20
29
|
border-radius: 2px;
|
|
21
30
|
opacity: 0;
|
|
22
31
|
transform: translateY(8px) scale(0.98);
|
|
@@ -24,22 +33,114 @@
|
|
|
24
33
|
pointer-events: none;
|
|
25
34
|
}
|
|
26
35
|
|
|
36
|
+
/* Sticky / actionable toasts must receive clicks (close + conflict buttons). */
|
|
37
|
+
.cms-toast.cms-toast-interactive {
|
|
38
|
+
pointer-events: auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
.cms-toast.cms-toast-out {
|
|
28
42
|
animation: cms-toast-out 0.3s ease forwards;
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
.cms-toast-msg {
|
|
46
|
+
flex: 1 1 auto;
|
|
47
|
+
line-height: 1.4;
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
.cms-toast-icon {
|
|
32
51
|
width: 16px;
|
|
33
52
|
height: 16px;
|
|
34
53
|
flex-shrink: 0;
|
|
35
54
|
}
|
|
36
55
|
|
|
56
|
+
.cms-toast-icon-wrap {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
.cms-toast-success { border-left: 3px solid var(--cms-green); }
|
|
38
62
|
.cms-toast-success .cms-toast-icon { color: var(--cms-green); }
|
|
39
63
|
|
|
40
64
|
.cms-toast-error { border-left: 3px solid var(--cms-red); }
|
|
41
65
|
.cms-toast-error .cms-toast-icon { color: var(--cms-red); }
|
|
42
66
|
|
|
67
|
+
.cms-toast-warning { border-left: 3px solid var(--cms-amber, #eab308); }
|
|
68
|
+
.cms-toast-warning .cms-toast-icon { color: var(--cms-amber, #eab308); }
|
|
69
|
+
|
|
70
|
+
/* Conflict prompt lays the action buttons below the message. */
|
|
71
|
+
.cms-toast-warning {
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cms-toast-actions {
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
width: 100%;
|
|
79
|
+
margin-top: 4px;
|
|
80
|
+
justify-content: flex-end;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.cms-toast-btn {
|
|
84
|
+
font-family: var(--cms-font);
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
padding: 5px 12px;
|
|
88
|
+
border-radius: 2px;
|
|
89
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
90
|
+
background: transparent;
|
|
91
|
+
color: rgba(255, 255, 255, 0.78);
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.cms-toast-btn:hover {
|
|
97
|
+
background: rgba(255, 255, 255, 0.06);
|
|
98
|
+
color: rgba(255, 255, 255, 0.95);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.cms-toast-btn-primary {
|
|
102
|
+
border-color: var(--cms-blue, #3b82f6);
|
|
103
|
+
color: #fff;
|
|
104
|
+
background: var(--cms-blue, #3b82f6);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.cms-toast-btn-primary:hover {
|
|
108
|
+
filter: brightness(1.1);
|
|
109
|
+
background: var(--cms-blue, #3b82f6);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.cms-toast-btn:focus-visible,
|
|
113
|
+
.cms-toast-close:focus-visible {
|
|
114
|
+
outline: 2px solid var(--cms-blue, #3b82f6);
|
|
115
|
+
outline-offset: 2px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.cms-toast-close {
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
width: 20px;
|
|
121
|
+
height: 20px;
|
|
122
|
+
margin-left: 2px;
|
|
123
|
+
padding: 0;
|
|
124
|
+
border: none;
|
|
125
|
+
background: transparent;
|
|
126
|
+
color: rgba(255, 255, 255, 0.5);
|
|
127
|
+
font-size: 18px;
|
|
128
|
+
line-height: 1;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
border-radius: 2px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.cms-toast-close:hover {
|
|
134
|
+
color: rgba(255, 255, 255, 0.9);
|
|
135
|
+
background: rgba(255, 255, 255, 0.06);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* An editable region awaiting a conflict decision. */
|
|
139
|
+
.cms-editable.cms-conflict {
|
|
140
|
+
outline: 2px solid var(--cms-amber, #eab308) !important;
|
|
141
|
+
outline-offset: 4px;
|
|
142
|
+
}
|
|
143
|
+
|
|
43
144
|
@keyframes cms-toast-in {
|
|
44
145
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
45
146
|
}
|
|
@@ -48,3 +149,14 @@
|
|
|
48
149
|
to { opacity: 0; transform: translateY(12px) scale(0.98); }
|
|
49
150
|
}
|
|
50
151
|
|
|
152
|
+
@media (prefers-reduced-motion: reduce) {
|
|
153
|
+
.cms-toast {
|
|
154
|
+
animation: none;
|
|
155
|
+
opacity: 1;
|
|
156
|
+
transform: none;
|
|
157
|
+
}
|
|
158
|
+
.cms-toast.cms-toast-out {
|
|
159
|
+
animation: none;
|
|
160
|
+
opacity: 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -1,29 +1,128 @@
|
|
|
1
|
+
const SUCCESS_SVG =
|
|
2
|
+
'<svg class="cms-toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>';
|
|
3
|
+
const ERROR_SVG =
|
|
4
|
+
'<svg class="cms-toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
|
|
5
|
+
const WARNING_SVG =
|
|
6
|
+
'<svg class="cms-toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
|
|
7
|
+
|
|
8
|
+
function iconFor(type) {
|
|
9
|
+
if (type === 'success') return SUCCESS_SVG;
|
|
10
|
+
if (type === 'warning') return WARNING_SVG;
|
|
11
|
+
return ERROR_SVG;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Toast controller. Returns a `showToast(message, type)` function with a
|
|
16
|
+
* `.conflict()` method for the two-choice save-conflict prompt.
|
|
17
|
+
*
|
|
18
|
+
* Design notes:
|
|
19
|
+
* - Toasts stack (a container region) instead of one replacing the next, so a
|
|
20
|
+
* burst of saves or an error + a later success don't clobber each other.
|
|
21
|
+
* - Errors and conflict prompts are STICKY and dismissible — a 2.5s auto-hide
|
|
22
|
+
* would let a failure or a data-loss choice vanish before it's read.
|
|
23
|
+
* - The message is always set via textContent, never innerHTML, so a
|
|
24
|
+
* server-provided error string can't inject markup into the editor chrome.
|
|
25
|
+
* - The container is an aria-live region; errors/conflicts are role="alert".
|
|
26
|
+
*/
|
|
1
27
|
export function createToast() {
|
|
2
|
-
let
|
|
28
|
+
let container = null;
|
|
3
29
|
|
|
4
|
-
|
|
5
|
-
if (
|
|
6
|
-
|
|
7
|
-
|
|
30
|
+
function ensureContainer() {
|
|
31
|
+
if (container && document.body.contains(container)) return container;
|
|
32
|
+
container = document.createElement('div');
|
|
33
|
+
container.className = 'cms-toast-stack';
|
|
34
|
+
container.setAttribute('aria-live', 'polite');
|
|
35
|
+
document.body.appendChild(container);
|
|
36
|
+
return container;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dismiss(toast) {
|
|
40
|
+
if (toast._caretDismissed) return;
|
|
41
|
+
toast._caretDismissed = true;
|
|
42
|
+
toast.classList.add('cms-toast-out');
|
|
43
|
+
setTimeout(() => toast.remove(), 300);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function render(type, { message, sticky, actions, role }) {
|
|
47
|
+
const stack = ensureContainer();
|
|
8
48
|
|
|
9
49
|
const toast = document.createElement('div');
|
|
10
50
|
toast.className = `cms-toast cms-toast-${type}`;
|
|
51
|
+
if (role) toast.setAttribute('role', role);
|
|
52
|
+
if (sticky || (actions && actions.length)) toast.classList.add('cms-toast-interactive');
|
|
53
|
+
|
|
54
|
+
const iconWrap = document.createElement('span');
|
|
55
|
+
iconWrap.className = 'cms-toast-icon-wrap';
|
|
56
|
+
iconWrap.setAttribute('aria-hidden', 'true');
|
|
57
|
+
iconWrap.innerHTML = iconFor(type); // trusted constant SVG only
|
|
58
|
+
toast.appendChild(iconWrap);
|
|
59
|
+
|
|
60
|
+
const msg = document.createElement('span');
|
|
61
|
+
msg.className = 'cms-toast-msg';
|
|
62
|
+
msg.textContent = message; // never innerHTML — message may carry server text
|
|
63
|
+
toast.appendChild(msg);
|
|
64
|
+
|
|
65
|
+
if (actions && actions.length) {
|
|
66
|
+
const group = document.createElement('div');
|
|
67
|
+
group.className = 'cms-toast-actions';
|
|
68
|
+
for (const action of actions) {
|
|
69
|
+
const btn = document.createElement('button');
|
|
70
|
+
btn.type = 'button';
|
|
71
|
+
btn.className = `cms-toast-btn${action.primary ? ' cms-toast-btn-primary' : ''}`;
|
|
72
|
+
btn.textContent = action.label;
|
|
73
|
+
btn.addEventListener('click', () => {
|
|
74
|
+
dismiss(toast);
|
|
75
|
+
try {
|
|
76
|
+
action.onClick();
|
|
77
|
+
} catch (e) {
|
|
78
|
+
/* action errors shouldn't break the toast */
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
group.appendChild(btn);
|
|
82
|
+
}
|
|
83
|
+
toast.appendChild(group);
|
|
84
|
+
} else if (sticky) {
|
|
85
|
+
const close = document.createElement('button');
|
|
86
|
+
close.type = 'button';
|
|
87
|
+
close.className = 'cms-toast-close';
|
|
88
|
+
close.setAttribute('aria-label', 'Dismiss');
|
|
89
|
+
close.textContent = '×';
|
|
90
|
+
close.addEventListener('click', () => dismiss(toast));
|
|
91
|
+
toast.appendChild(close);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stack.appendChild(toast);
|
|
95
|
+
if (!sticky) setTimeout(() => dismiss(toast), 2500);
|
|
96
|
+
return toast;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function showToast(message, type) {
|
|
100
|
+
const t = type === 'error' ? 'error' : type || 'success';
|
|
101
|
+
return render(t, {
|
|
102
|
+
message,
|
|
103
|
+
sticky: t === 'error',
|
|
104
|
+
role: t === 'error' ? 'alert' : undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Two-choice save-conflict prompt. Sticky so the user's edit is never silently
|
|
109
|
+
// discarded — they explicitly choose to keep theirs or load the latest.
|
|
110
|
+
showToast.conflict = ({
|
|
111
|
+
message,
|
|
112
|
+
onKeepMine,
|
|
113
|
+
onLoadLatest,
|
|
114
|
+
keepLabel = 'Keep mine',
|
|
115
|
+
latestLabel = 'Load latest',
|
|
116
|
+
}) =>
|
|
117
|
+
render('warning', {
|
|
118
|
+
message,
|
|
119
|
+
sticky: true,
|
|
120
|
+
role: 'alert',
|
|
121
|
+
actions: [
|
|
122
|
+
{ label: keepLabel, primary: true, onClick: onKeepMine },
|
|
123
|
+
{ label: latestLabel, onClick: onLoadLatest },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
11
126
|
|
|
12
|
-
|
|
13
|
-
type === 'success'
|
|
14
|
-
? '<svg class="cms-toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>'
|
|
15
|
-
: '<svg class="cms-toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
|
|
16
|
-
|
|
17
|
-
toast.innerHTML = iconSvg + `<span>${message}</span>`;
|
|
18
|
-
document.body.appendChild(toast);
|
|
19
|
-
activeToast = toast;
|
|
20
|
-
|
|
21
|
-
setTimeout(() => {
|
|
22
|
-
toast.classList.add('cms-toast-out');
|
|
23
|
-
setTimeout(() => {
|
|
24
|
-
toast.remove();
|
|
25
|
-
if (activeToast === toast) activeToast = null;
|
|
26
|
-
}, 300);
|
|
27
|
-
}, 2500);
|
|
28
|
-
};
|
|
127
|
+
return showToast;
|
|
29
128
|
}
|
|
@@ -16,14 +16,43 @@
|
|
|
16
16
|
--cms-green: #22c55e;
|
|
17
17
|
--cms-red: #ef4444;
|
|
18
18
|
--cms-yellow: #eab308;
|
|
19
|
+
--cms-amber: #eab308;
|
|
19
20
|
--cms-font: Inter, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/* ---------------------------------------------------------------------------
|
|
24
|
+
Host-style insulation
|
|
25
|
+
|
|
26
|
+
The editor chrome is injected as direct <body> children on the HOST site, so
|
|
27
|
+
an aggressive host reset (`* {}`, `body {}`, tag selectors) can bleed into it
|
|
28
|
+
— distorting fonts, line-height, letter-spacing, or box model. Pin the chrome
|
|
29
|
+
roots to sane defaults so they render consistently regardless of the host's
|
|
30
|
+
CSS. (Individual controls set their own type where needed.)
|
|
31
|
+
--------------------------------------------------------------------------- */
|
|
32
|
+
.cms-toolbar,
|
|
33
|
+
.cms-toast-stack,
|
|
34
|
+
.cms-toast,
|
|
35
|
+
.cms-link-popover,
|
|
36
|
+
.cms-content-map,
|
|
37
|
+
.cms-link-follow,
|
|
38
|
+
.cms-rich-toolbar,
|
|
39
|
+
.cms-studio-panel {
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
font-family: var(--cms-font);
|
|
42
|
+
line-height: normal;
|
|
43
|
+
letter-spacing: normal;
|
|
44
|
+
text-transform: none;
|
|
45
|
+
text-align: left;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
/* ---------------------------------------------------------------------------
|
|
23
49
|
Text editing affordances — simple blue outline
|
|
24
50
|
--------------------------------------------------------------------------- */
|
|
25
51
|
|
|
26
|
-
|
|
52
|
+
/* Both field bindings (data-caret) and markdown body blocks (data-caret-md)
|
|
53
|
+
carry .cms-editable, so the hover/focus affordances apply to both. */
|
|
54
|
+
[data-caret].cms-editable,
|
|
55
|
+
[data-caret-md].cms-editable {
|
|
27
56
|
cursor: text;
|
|
28
57
|
transition: outline 0.2s, box-shadow 0.2s, background 0.4s;
|
|
29
58
|
outline: 2px dashed transparent;
|
|
@@ -31,16 +60,57 @@
|
|
|
31
60
|
border-radius: 2px;
|
|
32
61
|
}
|
|
33
62
|
|
|
34
|
-
[data-caret].cms-editable:hover
|
|
63
|
+
[data-caret].cms-editable:hover,
|
|
64
|
+
[data-caret-md].cms-editable:hover {
|
|
35
65
|
outline-color: var(--cms-blue-hover);
|
|
36
66
|
}
|
|
37
67
|
|
|
38
|
-
[data-caret].cms-editable:focus
|
|
68
|
+
[data-caret].cms-editable:focus,
|
|
69
|
+
[data-caret-md].cms-editable:focus {
|
|
39
70
|
outline: 2px solid var(--cms-blue);
|
|
40
71
|
outline-offset: 4px;
|
|
41
72
|
box-shadow: 0 0 0 4px var(--cms-blue-dim);
|
|
42
73
|
}
|
|
43
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
|
+
|
|
84
|
+
/* ---------------------------------------------------------------------------
|
|
85
|
+
Unpublished body draft
|
|
86
|
+
The server rewrite tags blocks whose prose comes from an unpublished draft
|
|
87
|
+
(editor preview only — the public reads the base and never receives this).
|
|
88
|
+
Flag them so a reloaded preview is visibly distinct from live, published
|
|
89
|
+
prose and the editor knows a Publish is still pending.
|
|
90
|
+
--------------------------------------------------------------------------- */
|
|
91
|
+
|
|
92
|
+
[data-caret-md-draft] {
|
|
93
|
+
position: relative;
|
|
94
|
+
background: var(--cms-blue-dim);
|
|
95
|
+
box-shadow: -0.6em 0 0 0 var(--cms-blue-dim);
|
|
96
|
+
border-radius: 2px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[data-caret-md-draft]::after {
|
|
100
|
+
content: 'Draft';
|
|
101
|
+
position: absolute;
|
|
102
|
+
top: -0.6em;
|
|
103
|
+
right: 0;
|
|
104
|
+
font: 600 10px / 1 var(--cms-font);
|
|
105
|
+
letter-spacing: 0.08em;
|
|
106
|
+
text-transform: uppercase;
|
|
107
|
+
color: #fff;
|
|
108
|
+
background: var(--cms-yellow);
|
|
109
|
+
padding: 3px 6px;
|
|
110
|
+
border-radius: 3px;
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
44
114
|
/* ---------------------------------------------------------------------------
|
|
45
115
|
Link/button click prevention in editor mode
|
|
46
116
|
--------------------------------------------------------------------------- */
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
letter-spacing: 0.08em;
|
|
134
134
|
text-transform: uppercase;
|
|
135
135
|
background: transparent;
|
|
136
|
-
color: rgba(255, 255, 255, 0.
|
|
136
|
+
color: rgba(255, 255, 255, 0.72);
|
|
137
137
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
138
138
|
border-radius: 2px;
|
|
139
139
|
cursor: pointer;
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
.cms-status-text {
|
|
192
|
-
color: rgba(255, 255, 255, 0.
|
|
192
|
+
color: rgba(255, 255, 255, 0.72);
|
|
193
193
|
font-size: 11px;
|
|
194
194
|
font-weight: 400;
|
|
195
195
|
}
|
|
@@ -220,6 +220,17 @@
|
|
|
220
220
|
display: none;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/* Draft controls wrapper is transparent to the toolbar's flex layout so its
|
|
224
|
+
buttons keep the row's gap; [hidden] (server delivery, no pending draft)
|
|
225
|
+
still collapses the whole group. */
|
|
226
|
+
.cms-draft-controls {
|
|
227
|
+
display: contents;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.cms-draft-controls[hidden] {
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
223
234
|
/* Draft preview active — amber, signalling "unpublished" */
|
|
224
235
|
.cms-toolbar button.cms-preview-btn-active {
|
|
225
236
|
background: rgba(234, 179, 8, 0.12);
|
|
@@ -227,7 +238,7 @@
|
|
|
227
238
|
color: var(--cms-yellow);
|
|
228
239
|
}
|
|
229
240
|
|
|
230
|
-
/*
|
|
241
|
+
/* Publish — primary action on static delivery */
|
|
231
242
|
.cms-toolbar button.cms-go-live-btn {
|
|
232
243
|
background: rgba(34, 197, 94, 0.12);
|
|
233
244
|
border-color: rgba(34, 197, 94, 0.3);
|
|
@@ -240,7 +251,7 @@
|
|
|
240
251
|
color: var(--cms-green);
|
|
241
252
|
}
|
|
242
253
|
|
|
243
|
-
/* Publish — green
|
|
254
|
+
/* Publish — green */
|
|
244
255
|
.cms-toolbar button.cms-publish-btn {
|
|
245
256
|
background: rgba(34, 197, 94, 0.12);
|
|
246
257
|
border-color: rgba(34, 197, 94, 0.3);
|
|
@@ -260,8 +271,9 @@
|
|
|
260
271
|
color: var(--cms-red);
|
|
261
272
|
}
|
|
262
273
|
|
|
263
|
-
/* Highlight all mode — show every editable region */
|
|
264
|
-
.cms-highlight-all [data-caret].cms-editable
|
|
274
|
+
/* Highlight all mode — show every editable region (fields AND body blocks) */
|
|
275
|
+
.cms-highlight-all [data-caret].cms-editable,
|
|
276
|
+
.cms-highlight-all [data-caret-md].cms-editable {
|
|
265
277
|
outline-color: var(--cms-blue-hover);
|
|
266
278
|
background: rgba(59, 130, 246, 0.04);
|
|
267
279
|
}
|
|
@@ -275,6 +287,7 @@
|
|
|
275
287
|
transition — motion is suppressed, the information is not. */
|
|
276
288
|
@media (prefers-reduced-motion: reduce) {
|
|
277
289
|
.cms-highlight-all [data-caret].cms-editable,
|
|
290
|
+
.cms-highlight-all [data-caret-md].cms-editable,
|
|
278
291
|
.cms-highlight-all img[data-caret] {
|
|
279
292
|
transition: none;
|
|
280
293
|
}
|