@caretcms/core 0.2.0 → 0.4.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 +195 -12
- package/dist/index.d.ts +26 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -12
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +35 -3
- package/dist/loader.js.map +1 -1
- package/dist/markdown/paragraphs.d.ts +14 -0
- package/dist/markdown/paragraphs.d.ts.map +1 -0
- package/dist/markdown/paragraphs.js +61 -0
- package/dist/markdown/paragraphs.js.map +1 -0
- package/dist/markdown/remark.d.ts.map +1 -1
- package/dist/markdown/remark.js +1 -0
- package/dist/markdown/remark.js.map +1 -1
- package/dist/markdown/satteri.d.ts.map +1 -1
- package/dist/markdown/satteri.js +35 -9
- package/dist/markdown/satteri.js.map +1 -1
- package/dist/markdown/stamp.d.ts +0 -9
- package/dist/markdown/stamp.d.ts.map +1 -1
- package/dist/markdown/stamp.js +6 -0
- package/dist/markdown/stamp.js.map +1 -1
- package/dist/providers/deployment/github.d.ts +27 -0
- package/dist/providers/deployment/github.d.ts.map +1 -0
- package/dist/providers/deployment/github.js +180 -0
- package/dist/providers/deployment/github.js.map +1 -0
- package/dist/providers/deployment/simulated.d.ts +10 -0
- package/dist/providers/deployment/simulated.d.ts.map +1 -0
- package/dist/providers/deployment/simulated.js +29 -0
- package/dist/providers/deployment/simulated.js.map +1 -0
- 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/authorization.d.ts +11 -0
- package/dist/runtime/authorization.d.ts.map +1 -0
- package/dist/runtime/authorization.js +29 -0
- package/dist/runtime/authorization.js.map +1 -0
- package/dist/runtime/collection-policy.d.ts +11 -0
- package/dist/runtime/collection-policy.d.ts.map +1 -0
- package/dist/runtime/collection-policy.js +44 -0
- package/dist/runtime/collection-policy.js.map +1 -0
- 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-errors.d.ts +9 -0
- package/dist/runtime/content-errors.d.ts.map +1 -0
- package/dist/runtime/content-errors.js +17 -0
- package/dist/runtime/content-errors.js.map +1 -0
- package/dist/runtime/content.d.ts.map +1 -1
- package/dist/runtime/content.js +13 -4
- package/dist/runtime/content.js.map +1 -1
- package/dist/runtime/deployment-status.d.ts +14 -0
- package/dist/runtime/deployment-status.d.ts.map +1 -0
- package/dist/runtime/deployment-status.js +57 -0
- package/dist/runtime/deployment-status.js.map +1 -0
- package/dist/runtime/draft-state.d.ts +16 -0
- package/dist/runtime/draft-state.d.ts.map +1 -0
- package/dist/runtime/draft-state.js +29 -0
- package/dist/runtime/draft-state.js.map +1 -0
- package/dist/runtime/i18n.d.ts +97 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +190 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/middleware.js +61 -4
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +12 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +54 -4
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts +2 -0
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +220 -57
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +4 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +18 -1
- package/dist/runtime/providers.js.map +1 -1
- package/dist/runtime/publish-recovery.d.ts +32 -0
- package/dist/runtime/publish-recovery.d.ts.map +1 -0
- package/dist/runtime/publish-recovery.js +98 -0
- package/dist/runtime/publish-recovery.js.map +1 -0
- package/dist/runtime/publish.d.ts +10 -4
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +105 -63
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/rebuild-webhook.d.ts +4 -0
- package/dist/runtime/rebuild-webhook.d.ts.map +1 -1
- package/dist/runtime/rebuild-webhook.js +12 -2
- package/dist/runtime/rebuild-webhook.js.map +1 -1
- package/dist/runtime/request-context.d.ts +7 -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/rewrite.d.ts.map +1 -1
- package/dist/runtime/rewrite.js +28 -2
- package/dist/runtime/rewrite.js.map +1 -1
- package/dist/runtime/rich-allowlist.js +1 -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 +129 -21
- 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 +10 -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 +213 -66
- package/dist/runtime/routes/collection-list.js.map +1 -1
- package/dist/runtime/routes/deployment.d.ts +5 -0
- package/dist/runtime/routes/deployment.d.ts.map +1 -0
- package/dist/runtime/routes/deployment.js +38 -0
- package/dist/runtime/routes/deployment.js.map +1 -0
- package/dist/runtime/routes/draft.d.ts.map +1 -1
- package/dist/runtime/routes/draft.js +10 -3
- package/dist/runtime/routes/draft.js.map +1 -1
- package/dist/runtime/routes/entries.d.ts.map +1 -1
- package/dist/runtime/routes/entries.js +87 -43
- 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 +40 -21
- 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 +4 -1
- 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 +127 -33
- 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 -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 +41 -20
- package/dist/runtime/routes/studio-home.js.map +1 -1
- package/dist/runtime/routes/upload.d.ts.map +1 -1
- package/dist/runtime/routes/upload.js +3 -0
- package/dist/runtime/routes/upload.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 +49 -3
- package/dist/runtime/schema-registry.js.map +1 -1
- package/dist/runtime/storage/entry-commit.d.ts +8 -0
- package/dist/runtime/storage/entry-commit.d.ts.map +1 -0
- package/dist/runtime/storage/entry-commit.js +45 -0
- package/dist/runtime/storage/entry-commit.js.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts +5 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/filesystem-adapter.js +12 -3
- package/dist/runtime/storage/filesystem-adapter.js.map +1 -1
- package/dist/runtime/storage/frontmatter-codec.d.ts +5 -23
- package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
- package/dist/runtime/storage/frontmatter-codec.js +156 -13
- package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
- package/dist/runtime/storage/in-memory-adapter.d.ts +7 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/in-memory-adapter.js +6 -0
- package/dist/runtime/storage/in-memory-adapter.js.map +1 -1
- package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/markdown-adapter.js +18 -20
- package/dist/runtime/storage/markdown-adapter.js.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.d.ts +12 -4
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.js +100 -13
- package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
- package/dist/runtime/storage/sidecar-meta-store.d.ts +5 -0
- package/dist/runtime/storage/sidecar-meta-store.d.ts.map +1 -1
- package/dist/runtime/storage/sidecar-meta-store.js +48 -5
- package/dist/runtime/storage/sidecar-meta-store.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.map +1 -1
- package/dist/runtime/utils.js +3 -4
- 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 +165 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -3
- package/static/cms/admin-entry.css +144 -13
- package/static/cms/admin-entry.js +539 -586
- package/static/cms/dev-toolbar/app.js +78 -19
- package/static/cms/editor/config.js +35 -15
- package/static/cms/editor/content-map.css +8 -1
- package/static/cms/editor/content-map.js +62 -23
- package/static/cms/editor/deployment-status.js +66 -0
- package/static/cms/editor/draft-client.js +87 -0
- package/static/cms/editor/guards.js +20 -0
- package/static/cms/editor/helpers.js +30 -1
- package/static/cms/editor/highlight.js +1 -0
- package/static/cms/editor/image-edit.js +44 -1
- package/static/cms/editor/image-utils.js +23 -7
- package/static/cms/editor/link-follow.js +122 -41
- package/static/cms/editor/link-ui.css +10 -1
- package/static/cms/editor/linkify.js +2 -0
- package/static/cms/editor/md-block-edit.js +140 -37
- package/static/cms/editor/md-paragraphs.js +98 -0
- package/static/cms/editor/md-serialize.js +1 -1
- package/static/cms/editor/panel.js +53 -7
- package/static/cms/editor/preview-mode.js +44 -0
- package/static/cms/editor/publish-result.js +100 -0
- package/static/cms/editor/rich-toolbar.js +58 -15
- package/static/cms/editor/sanitize.js +6 -2
- package/static/cms/editor/save-queue.js +75 -17
- package/static/cms/editor/section-controls/activation.js +3 -0
- package/static/cms/editor/section-controls/api.js +25 -7
- package/static/cms/editor/section-controls/index.js +60 -8
- package/static/cms/editor/section-controls/model.js +39 -4
- package/static/cms/editor/section-controls/mutations.js +15 -0
- package/static/cms/editor/section-controls/page-context.js +9 -0
- package/static/cms/editor/section-controls/pickers.js +53 -2
- package/static/cms/editor/section-controls/reorder.js +23 -0
- package/static/cms/editor/section-controls/selection.js +6 -0
- package/static/cms/editor/section-controls/spacing-drag.js +16 -0
- package/static/cms/editor/section-controls/ui-elements.js +7 -0
- package/static/cms/editor/section-controls/utils.js +7 -0
- package/static/cms/editor/section-controls.css +18 -3
- package/static/cms/editor/security.js +13 -2
- package/static/cms/editor/stega-hydrate.js +17 -4
- package/static/cms/editor/sync.js +475 -56
- package/static/cms/editor/text-edit.js +37 -3
- package/static/cms/editor/text-link-interactions.js +59 -17
- package/static/cms/editor/toast.js +23 -9
- package/static/cms/editor/tokens-and-text.css +9 -0
- package/static/cms/editor/toolbar.css +5 -0
- package/static/cms/editor/toolbar.js +91 -101
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +1 -0
- package/static/cms/studio/entry-loader.js +102 -0
- package/static/cms/studio/field-model.js +110 -0
- package/static/cms/studio/fields.js +319 -0
- package/static/cms/studio/history-client.js +74 -0
- package/static/cms/studio/mutation-client.js +74 -0
- package/static/cms/studio/sync-client.js +107 -0
- package/static/cms/studio/upload-client.js +109 -0
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -10,6 +10,19 @@ import { defineToolbarApp } from "astro/toolbar";
|
|
|
10
10
|
// keeps working in `output: 'static'` projects where /__caret/ is not served.
|
|
11
11
|
// If helpers.js changes, keep these two in sync.
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{ collection: string | null, id: string | null, field: string }} ParsedBinding
|
|
15
|
+
* @typedef {{ collection: string, id: string, field: string }} ResolvedBinding
|
|
16
|
+
* @typedef {"text" | "image" | "rich"} BindingKind
|
|
17
|
+
* @typedef {{ el: Element, index: number, collection: string, id: string, field: string, key: string, kind: BindingKind }} PageBinding
|
|
18
|
+
* @typedef {{ el: Element, attr: string, kind: BindingKind, index: number }} OrphanBinding
|
|
19
|
+
* @typedef {{ bindings: PageBinding[], orphans: OrphanBinding[], duplicates: PageBinding[], stega: boolean, total: number }} BindingScan
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} attr
|
|
24
|
+
* @returns {ParsedBinding | null}
|
|
25
|
+
*/
|
|
13
26
|
function parseCaretAttr(attr) {
|
|
14
27
|
const parts = attr.split("::");
|
|
15
28
|
if (parts.length === 3) return { collection: parts[0], id: parts[1], field: parts[2] };
|
|
@@ -17,11 +30,18 @@ function parseCaretAttr(attr) {
|
|
|
17
30
|
return null;
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @param {Element} el
|
|
35
|
+
* @param {ParsedBinding | null} parsed
|
|
36
|
+
* @returns {ResolvedBinding | null}
|
|
37
|
+
*/
|
|
20
38
|
function resolveBinding(el, parsed) {
|
|
21
39
|
if (!parsed) return null;
|
|
22
40
|
// Explicit null check, not truthiness — mirrors helpers.js; see the parity
|
|
23
41
|
// test (tests/unit/caret-parser-parity.test.ts).
|
|
24
|
-
if (parsed.collection !== null && parsed.id !== null)
|
|
42
|
+
if (parsed.collection !== null && parsed.id !== null) {
|
|
43
|
+
return { collection: parsed.collection, id: parsed.id, field: parsed.field };
|
|
44
|
+
}
|
|
25
45
|
let node = el.parentElement;
|
|
26
46
|
while (node) {
|
|
27
47
|
const scope = node.getAttribute("data-caret-scope");
|
|
@@ -35,13 +55,21 @@ function resolveBinding(el, parsed) {
|
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
function devConfig() {
|
|
38
|
-
const c =
|
|
58
|
+
const c =
|
|
59
|
+
(typeof window !== "undefined" &&
|
|
60
|
+
/** @type {Window & { __CARET_DEV__?: { mountPath?: unknown, apiBasePath?: unknown } }} */ (window)
|
|
61
|
+
.__CARET_DEV__) ||
|
|
62
|
+
{};
|
|
39
63
|
return {
|
|
40
64
|
mountPath: typeof c.mountPath === "string" ? c.mountPath : "/admin",
|
|
41
65
|
apiBasePath: typeof c.apiBasePath === "string" ? c.apiBasePath : "/api/cms",
|
|
42
66
|
};
|
|
43
67
|
}
|
|
44
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @param {Element} el
|
|
71
|
+
* @returns {BindingKind}
|
|
72
|
+
*/
|
|
45
73
|
function classify(el) {
|
|
46
74
|
if (el.tagName.toLowerCase() === "img") return "image";
|
|
47
75
|
if (el.hasAttribute("data-caret-rich")) return "rich";
|
|
@@ -50,9 +78,12 @@ function classify(el) {
|
|
|
50
78
|
|
|
51
79
|
// Walk the page and resolve every data-caret element to a full key, splitting
|
|
52
80
|
// out orphans (field-only attrs with no resolvable scope) and duplicate keys.
|
|
81
|
+
/** @returns {BindingScan} */
|
|
53
82
|
function scanBindings() {
|
|
54
83
|
const els = Array.from(document.querySelectorAll("[data-caret]"));
|
|
84
|
+
/** @type {PageBinding[]} */
|
|
55
85
|
const bindings = [];
|
|
86
|
+
/** @type {OrphanBinding[]} */
|
|
56
87
|
const orphans = [];
|
|
57
88
|
|
|
58
89
|
els.forEach((el, i) => {
|
|
@@ -75,9 +106,10 @@ function scanBindings() {
|
|
|
75
106
|
});
|
|
76
107
|
});
|
|
77
108
|
|
|
109
|
+
/** @type {Map<string, number>} */
|
|
78
110
|
const counts = new Map();
|
|
79
111
|
bindings.forEach((b) => counts.set(b.key, (counts.get(b.key) || 0) + 1));
|
|
80
|
-
const duplicates = bindings.filter((b) => counts.get(b.key) > 1);
|
|
112
|
+
const duplicates = bindings.filter((b) => (counts.get(b.key) ?? 0) > 1);
|
|
81
113
|
|
|
82
114
|
// Stega-encoded bindings (live-loader content) only become real data-caret
|
|
83
115
|
// attributes after the editor's stega-hydrate.js runs, which needs auth — so
|
|
@@ -93,6 +125,7 @@ function scanBindings() {
|
|
|
93
125
|
// be told apart when both are active at once.
|
|
94
126
|
const HIGHLIGHT_STYLE_ID = "__caret-devtoolbar-highlight__";
|
|
95
127
|
|
|
128
|
+
/** @param {boolean} active */
|
|
96
129
|
function applyHighlight(active) {
|
|
97
130
|
const existing = document.getElementById(HIGHLIGHT_STYLE_ID);
|
|
98
131
|
if (existing) existing.remove();
|
|
@@ -107,22 +140,43 @@ function applyHighlight(active) {
|
|
|
107
140
|
document.head.appendChild(style);
|
|
108
141
|
}
|
|
109
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Repeated jumps share the first inline style snapshot so overlapping timers
|
|
145
|
+
* cannot restore a temporary outline and leave it stuck on the page.
|
|
146
|
+
* @type {WeakMap<HTMLElement | SVGElement, { outline: string, outlineOffset: string, timer: number }>}
|
|
147
|
+
*/
|
|
148
|
+
const scrollFlashes = new WeakMap();
|
|
149
|
+
|
|
150
|
+
/** @param {Element} el */
|
|
110
151
|
function scrollTo(el) {
|
|
111
152
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
112
|
-
|
|
113
|
-
|
|
153
|
+
if (!(el instanceof HTMLElement || el instanceof SVGElement)) return;
|
|
154
|
+
|
|
155
|
+
const active = scrollFlashes.get(el);
|
|
156
|
+
if (active) window.clearTimeout(active.timer);
|
|
157
|
+
const outline = active?.outline ?? el.style.outline;
|
|
158
|
+
const outlineOffset = active?.outlineOffset ?? el.style.outlineOffset;
|
|
114
159
|
el.style.outline = "3px solid #6366f1";
|
|
115
160
|
el.style.outlineOffset = "3px";
|
|
116
|
-
setTimeout(() => {
|
|
117
|
-
|
|
118
|
-
|
|
161
|
+
const timer = window.setTimeout(() => {
|
|
162
|
+
const current = scrollFlashes.get(el);
|
|
163
|
+
if (!current || current.timer !== timer) return;
|
|
164
|
+
el.style.outline = current.outline;
|
|
165
|
+
el.style.outlineOffset = current.outlineOffset;
|
|
166
|
+
scrollFlashes.delete(el);
|
|
119
167
|
}, 1500);
|
|
168
|
+
scrollFlashes.set(el, { outline, outlineOffset, timer });
|
|
120
169
|
}
|
|
121
170
|
|
|
171
|
+
/** @type {Record<BindingKind, string>} */
|
|
122
172
|
const KIND_LABEL = { text: "text", image: "image", rich: "rich" };
|
|
123
173
|
|
|
174
|
+
/** @type {Record<string, string>} */
|
|
175
|
+
const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
176
|
+
|
|
177
|
+
/** @param {string} s */
|
|
124
178
|
function esc(s) {
|
|
125
|
-
return
|
|
179
|
+
return s.replace(/[&<>"]/g, (c) => HTML_ESCAPES[c] ?? c);
|
|
126
180
|
}
|
|
127
181
|
|
|
128
182
|
const PANEL_CSS = `
|
|
@@ -169,19 +223,22 @@ export default defineToolbarApp({
|
|
|
169
223
|
canvas.appendChild(win);
|
|
170
224
|
|
|
171
225
|
let highlightOn = false;
|
|
226
|
+
/** @type {BindingScan} */
|
|
172
227
|
let scan = { bindings: [], orphans: [], duplicates: [], stega: false, total: 0 };
|
|
173
228
|
|
|
174
229
|
function render() {
|
|
175
230
|
scan = scanBindings();
|
|
176
231
|
const { mountPath } = devConfig();
|
|
177
|
-
const { bindings, orphans, duplicates, stega
|
|
232
|
+
const { bindings, orphans, duplicates, stega } = scan;
|
|
178
233
|
|
|
179
234
|
// Group bindings by collection::id, preserving first-seen order.
|
|
235
|
+
/** @type {Map<string, PageBinding[]>} */
|
|
180
236
|
const groups = new Map();
|
|
181
237
|
for (const b of bindings) {
|
|
182
238
|
const gk = `${b.collection}::${b.id}`;
|
|
183
|
-
|
|
184
|
-
|
|
239
|
+
const rows = groups.get(gk);
|
|
240
|
+
if (rows) rows.push(b);
|
|
241
|
+
else groups.set(gk, [b]);
|
|
185
242
|
}
|
|
186
243
|
|
|
187
244
|
const dupKeys = new Set(duplicates.map((d) => d.key));
|
|
@@ -192,8 +249,8 @@ export default defineToolbarApp({
|
|
|
192
249
|
<span class="count">${bindings.length} on page${orphans.length ? ` · ${orphans.length} orphan${orphans.length > 1 ? "s" : ""}` : ""}</span>
|
|
193
250
|
</div>
|
|
194
251
|
<div class="actions">
|
|
195
|
-
<button class="btn ${highlightOn ? "on" : ""}" data-action="highlight">${highlightOn ? "Hide" : "Highlight"} all</button>
|
|
196
|
-
<button class="btn" data-action="rescan">Rescan</button>
|
|
252
|
+
<button type="button" class="btn ${highlightOn ? "on" : ""}" data-action="highlight" aria-pressed="${highlightOn}">${highlightOn ? "Hide" : "Highlight"} all</button>
|
|
253
|
+
<button type="button" class="btn" data-action="rescan">Rescan</button>
|
|
197
254
|
</div>
|
|
198
255
|
<div class="scroll">`;
|
|
199
256
|
|
|
@@ -209,7 +266,7 @@ export default defineToolbarApp({
|
|
|
209
266
|
}
|
|
210
267
|
|
|
211
268
|
if (!bindings.length && !orphans.length) {
|
|
212
|
-
html += `<div class="empty">No <code>data-caret</code> bindings found on this page
|
|
269
|
+
html += `<div class="empty">No <code>data-caret</code> bindings found on this page.</div>`;
|
|
213
270
|
}
|
|
214
271
|
|
|
215
272
|
for (const [gk, rows] of groups) {
|
|
@@ -221,7 +278,7 @@ export default defineToolbarApp({
|
|
|
221
278
|
html += `<div class="row">
|
|
222
279
|
<span class="field ${dupKeys.has(b.key) ? "dup" : ""}">${esc(b.field)}</span>
|
|
223
280
|
<span class="kind ${b.kind}">${KIND_LABEL[b.kind]}</span>
|
|
224
|
-
<button class="jump" data-action="scroll" data-index="${b.index}">scroll to</button>
|
|
281
|
+
<button type="button" class="jump" data-action="scroll" data-index="${b.index}">scroll to</button>
|
|
225
282
|
</div>`;
|
|
226
283
|
}
|
|
227
284
|
html += `</div>`;
|
|
@@ -229,23 +286,27 @@ export default defineToolbarApp({
|
|
|
229
286
|
|
|
230
287
|
html += `</div>`;
|
|
231
288
|
wrap.innerHTML = html;
|
|
289
|
+
const probs = scan.orphans.length + scan.duplicates.length;
|
|
290
|
+
app.toggleNotification({ state: probs > 0, level: "warning" });
|
|
232
291
|
}
|
|
233
292
|
|
|
293
|
+
/** @param {string | null} index */
|
|
234
294
|
function elementByIndex(index) {
|
|
235
295
|
const b = scan.bindings.find((x) => x.index === Number(index));
|
|
236
296
|
return b ? b.el : null;
|
|
237
297
|
}
|
|
238
298
|
|
|
239
299
|
wrap.addEventListener("click", (event) => {
|
|
300
|
+
if (!(event.target instanceof Element)) return;
|
|
240
301
|
const target = event.target.closest("[data-action]");
|
|
241
302
|
if (!target) return;
|
|
242
303
|
const action = target.getAttribute("data-action");
|
|
243
304
|
if (action === "highlight") {
|
|
244
305
|
highlightOn = !highlightOn;
|
|
245
306
|
applyHighlight(highlightOn);
|
|
246
|
-
app.toggleState({ state: highlightOn });
|
|
247
307
|
target.textContent = `${highlightOn ? "Hide" : "Highlight"} all`;
|
|
248
308
|
target.classList.toggle("on", highlightOn);
|
|
309
|
+
target.setAttribute("aria-pressed", String(highlightOn));
|
|
249
310
|
} else if (action === "rescan") {
|
|
250
311
|
render();
|
|
251
312
|
} else if (action === "scroll") {
|
|
@@ -258,8 +319,6 @@ export default defineToolbarApp({
|
|
|
258
319
|
app.onToggled(({ state }) => {
|
|
259
320
|
if (!state) return;
|
|
260
321
|
render();
|
|
261
|
-
const probs = scan.orphans.length + scan.duplicates.length;
|
|
262
|
-
app.toggleNotification({ state: probs > 0, level: "warning" });
|
|
263
322
|
});
|
|
264
323
|
},
|
|
265
324
|
});
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/** Runtime config injected by the integration bootstrap script. */
|
|
2
|
+
/** @typedef {{ endpoint?: unknown, projectId?: unknown, contentPath?: unknown, publicToken?: unknown, environment?: unknown }} RawCloudConfig */
|
|
3
|
+
/** @typedef {{ endpoint: string, projectId: string, contentPath: string, publicToken: string | null, environment: string | null }} CloudConfig */
|
|
4
|
+
/** @typedef {{ mode?: unknown, mountPath?: unknown, apiBasePath?: unknown, cloud?: RawCloudConfig | null, delivery?: { mode?: unknown } | null, draftMode?: unknown }} RuntimeConfig */
|
|
5
|
+
|
|
6
|
+
/** @type {RuntimeConfig} */
|
|
2
7
|
const defaults = {
|
|
3
8
|
mode: 'embedded',
|
|
4
9
|
mountPath: '/admin',
|
|
@@ -6,8 +11,13 @@ const defaults = {
|
|
|
6
11
|
cloud: null,
|
|
7
12
|
};
|
|
8
13
|
|
|
9
|
-
const
|
|
14
|
+
const injected = typeof window !== 'undefined'
|
|
15
|
+
? /** @type {Window & { __CARET__?: RuntimeConfig }} */ (window).__CARET__
|
|
16
|
+
: undefined;
|
|
17
|
+
/** @type {RuntimeConfig} */
|
|
18
|
+
const cfg = injected || defaults;
|
|
10
19
|
|
|
20
|
+
/** @param {unknown} value @param {string} fallback */
|
|
11
21
|
function normalizePath(value, fallback) {
|
|
12
22
|
if (!value || typeof value !== 'string') return fallback;
|
|
13
23
|
const trimmed = value.trim();
|
|
@@ -18,11 +28,13 @@ function normalizePath(value, fallback) {
|
|
|
18
28
|
: withLeadingSlash;
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
/** @param {unknown} value */
|
|
21
32
|
function normalizeEndpoint(value) {
|
|
22
33
|
if (!value || typeof value !== 'string') return '';
|
|
23
34
|
return value.trim().replace(/\/$/, '');
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/** @returns {CloudConfig | null} */
|
|
26
38
|
function getCloudConfig() {
|
|
27
39
|
if (!cfg || cfg.mode !== 'cloud' || !cfg.cloud || typeof cfg.cloud !== 'object') {
|
|
28
40
|
return null;
|
|
@@ -48,6 +60,7 @@ function getCloudConfig() {
|
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
/** @param {CloudConfig} cloud */
|
|
51
64
|
function getStorageKey(cloud) {
|
|
52
65
|
return `caretcms:cloud-session:${cloud.endpoint}:${cloud.projectId}`;
|
|
53
66
|
}
|
|
@@ -57,6 +70,8 @@ export const CMS_MODE = cfg.mode === 'cloud' || cfg.mode === 'hybrid' ? cfg.mode
|
|
|
57
70
|
export const DELIVERY_MODE =
|
|
58
71
|
cfg.delivery && cfg.delivery.mode === 'static' ? 'static' : 'server';
|
|
59
72
|
|
|
73
|
+
export function isPolicyDraftMode() { return cfg.draftMode === true; }
|
|
74
|
+
|
|
60
75
|
export function isStaticDelivery() {
|
|
61
76
|
return DELIVERY_MODE === 'static';
|
|
62
77
|
}
|
|
@@ -64,8 +79,8 @@ export function isStaticDelivery() {
|
|
|
64
79
|
export function isServerDelivery() {
|
|
65
80
|
return !isStaticDelivery();
|
|
66
81
|
}
|
|
67
|
-
export const MOUNT_PATH = normalizePath(cfg.mountPath,
|
|
68
|
-
export const API_BASE = normalizePath(cfg.apiBasePath,
|
|
82
|
+
export const MOUNT_PATH = normalizePath(cfg.mountPath, '/admin');
|
|
83
|
+
export const API_BASE = normalizePath(cfg.apiBasePath, '/api/cms');
|
|
69
84
|
export const CLOUD = getCloudConfig();
|
|
70
85
|
export const STUDIO_PATH = CLOUD ? null : `${MOUNT_PATH}/cms`;
|
|
71
86
|
|
|
@@ -74,9 +89,10 @@ export function isCloudMode() {
|
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
export function getCloudSessionToken() {
|
|
77
|
-
|
|
92
|
+
const cloud = CLOUD;
|
|
93
|
+
if (!cloud || CMS_MODE !== 'cloud' || typeof window === 'undefined') return null;
|
|
78
94
|
try {
|
|
79
|
-
const value = window.localStorage.getItem(getStorageKey(
|
|
95
|
+
const value = window.localStorage.getItem(getStorageKey(cloud));
|
|
80
96
|
return value && value.trim() ? value.trim() : null;
|
|
81
97
|
} catch {
|
|
82
98
|
return null;
|
|
@@ -84,23 +100,26 @@ export function getCloudSessionToken() {
|
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
export function clearCloudSessionToken() {
|
|
87
|
-
|
|
103
|
+
const cloud = CLOUD;
|
|
104
|
+
if (!cloud || CMS_MODE !== 'cloud' || typeof window === 'undefined') return;
|
|
88
105
|
try {
|
|
89
|
-
window.localStorage.removeItem(getStorageKey(
|
|
106
|
+
window.localStorage.removeItem(getStorageKey(cloud));
|
|
90
107
|
} catch {
|
|
91
108
|
// Ignore localStorage failures.
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
|
|
112
|
+
/** @param {string} path @param {URLSearchParams | Record<string, string> | undefined} [search] */
|
|
95
113
|
export function buildCmsUrl(path, search) {
|
|
96
114
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
97
115
|
let url;
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
url.
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
const cloud = CLOUD;
|
|
118
|
+
if (CMS_MODE === 'cloud' && cloud) {
|
|
119
|
+
url = new URL(`${cloud.contentPath}${normalizedPath}`, `${cloud.endpoint}/`);
|
|
120
|
+
url.searchParams.set('projectId', cloud.projectId);
|
|
121
|
+
if (cloud.environment) {
|
|
122
|
+
url.searchParams.set('environment', cloud.environment);
|
|
104
123
|
}
|
|
105
124
|
} else {
|
|
106
125
|
url = new URL(`${API_BASE}${normalizedPath}`, window.location.origin);
|
|
@@ -117,9 +136,10 @@ export function buildCmsUrl(path, search) {
|
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
export function getEditorLoginUrl(redirectTo = window.location.href) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
url.
|
|
139
|
+
const cloud = CLOUD;
|
|
140
|
+
if (CMS_MODE === 'cloud' && cloud) {
|
|
141
|
+
const url = new URL(`${cloud.contentPath}/admin`, `${cloud.endpoint}/`);
|
|
142
|
+
url.searchParams.set('projectId', cloud.projectId);
|
|
123
143
|
url.searchParams.set('redirect', redirectTo);
|
|
124
144
|
return url.toString();
|
|
125
145
|
}
|
|
@@ -78,12 +78,19 @@
|
|
|
78
78
|
display: flex;
|
|
79
79
|
align-items: center;
|
|
80
80
|
justify-content: space-between;
|
|
81
|
+
width: 100%;
|
|
82
|
+
border: 0;
|
|
83
|
+
background: transparent;
|
|
84
|
+
color: inherit;
|
|
85
|
+
font: inherit;
|
|
86
|
+
text-align: left;
|
|
81
87
|
padding: 5px 14px;
|
|
82
88
|
cursor: pointer;
|
|
83
89
|
transition: background 0.15s;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
.cms-content-map-item:hover
|
|
92
|
+
.cms-content-map-item:hover,
|
|
93
|
+
.cms-content-map-item:focus-visible {
|
|
87
94
|
background: rgba(255, 255, 255, 0.05);
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import { buildCmsUrl } from './config.js';
|
|
2
2
|
import { readHeaders } from './security.js';
|
|
3
|
-
import { parseCaretAttr, resolveBinding
|
|
3
|
+
import { parseCaretAttr, resolveBinding } from './helpers.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Content Map dev panel.
|
|
7
7
|
* Lists all data-caret bindings on the current page with override status.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/** @typedef {{ collection: string, id: string, field: string }} ResolvedBinding */
|
|
11
|
+
/** @typedef {{ el: Element, resolved: ResolvedBinding, isImg: boolean, isScoped: boolean, index: number }} MapBinding */
|
|
12
|
+
|
|
13
|
+
/** @type {HTMLElement | null} */
|
|
10
14
|
let panel = null;
|
|
11
15
|
let isOpen = false;
|
|
16
|
+
let refreshGeneration = 0;
|
|
17
|
+
/** @type {WeakSet<Element>} */
|
|
18
|
+
const mountedButtons = new WeakSet();
|
|
12
19
|
|
|
20
|
+
/** @returns {HTMLElement} */
|
|
13
21
|
function createPanel() {
|
|
14
22
|
const el = document.createElement('div');
|
|
15
23
|
el.className = 'cms-content-map';
|
|
24
|
+
el.setAttribute('role', 'region');
|
|
25
|
+
el.setAttribute('aria-label', 'Content map');
|
|
26
|
+
el.setAttribute('aria-hidden', 'true');
|
|
16
27
|
el.innerHTML = `
|
|
17
28
|
<div class="cms-content-map-header">
|
|
18
29
|
<span class="cms-content-map-title">Content Map</span>
|
|
19
|
-
<button type="button" class="cms-content-map-close">×</button>
|
|
30
|
+
<button type="button" class="cms-content-map-close" aria-label="Close content map">×</button>
|
|
20
31
|
</div>
|
|
21
32
|
<div class="cms-content-map-body"></div>
|
|
22
33
|
`;
|
|
@@ -29,19 +40,32 @@ function createPanel() {
|
|
|
29
40
|
return el;
|
|
30
41
|
}
|
|
31
42
|
|
|
43
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
44
|
+
function isRecord(value) {
|
|
45
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} payload @returns {Record<string, unknown> | null} */
|
|
49
|
+
export function readOverrideData(payload) {
|
|
50
|
+
if (!isRecord(payload) || !Array.isArray(payload.entries)) return null;
|
|
51
|
+
const entry = payload.entries[0];
|
|
52
|
+
return isRecord(entry) && isRecord(entry.data) ? entry.data : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {Iterable<string>} entryKeys @returns {Promise<Map<string, Record<string, unknown>>>} */
|
|
32
56
|
async function loadOverrides(entryKeys) {
|
|
57
|
+
/** @type {Map<string, Record<string, unknown>>} */
|
|
33
58
|
const overrides = new Map();
|
|
34
59
|
for (const key of entryKeys) {
|
|
35
60
|
const [collection, id] = key.split('::');
|
|
61
|
+
if (!collection || !id) continue;
|
|
36
62
|
try {
|
|
37
63
|
const res = await fetch(buildCmsUrl('/entries', { collection, id }), {
|
|
38
64
|
headers: readHeaders(),
|
|
39
65
|
});
|
|
40
66
|
if (res.ok) {
|
|
41
|
-
const data = await res.json();
|
|
42
|
-
if (data.
|
|
43
|
-
overrides.set(key, data.entries[0].data);
|
|
44
|
-
}
|
|
67
|
+
const data = readOverrideData(await res.json());
|
|
68
|
+
if (data) overrides.set(key, data);
|
|
45
69
|
}
|
|
46
70
|
} catch {
|
|
47
71
|
// Skip failed loads
|
|
@@ -50,24 +74,33 @@ async function loadOverrides(entryKeys) {
|
|
|
50
74
|
return overrides;
|
|
51
75
|
}
|
|
52
76
|
|
|
77
|
+
/** @param {unknown} data @param {string} path @returns {unknown} */
|
|
53
78
|
function getNestedValue(data, path) {
|
|
54
79
|
const keys = path.split('.');
|
|
80
|
+
/** @type {unknown} */
|
|
55
81
|
let current = data;
|
|
56
82
|
for (const key of keys) {
|
|
57
|
-
if (current
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
if (Array.isArray(current)) {
|
|
84
|
+
if (!/^\d+$/.test(key)) return undefined;
|
|
85
|
+
current = current[Number(key)];
|
|
86
|
+
} else if (isRecord(current)) {
|
|
87
|
+
if (!Object.hasOwn(current, key)) return undefined;
|
|
88
|
+
current = current[key];
|
|
89
|
+
} else return undefined;
|
|
60
90
|
}
|
|
61
91
|
return current;
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
async function refresh() {
|
|
65
95
|
if (!panel) return;
|
|
96
|
+
const generation = ++refreshGeneration;
|
|
66
97
|
const body = panel.querySelector('.cms-content-map-body');
|
|
67
98
|
if (!body) return;
|
|
68
99
|
|
|
69
100
|
const elements = document.querySelectorAll('[data-caret]');
|
|
101
|
+
/** @type {MapBinding[]} */
|
|
70
102
|
const bindings = [];
|
|
103
|
+
/** @type {Set<string>} */
|
|
71
104
|
const entryKeys = new Set();
|
|
72
105
|
|
|
73
106
|
elements.forEach((el) => {
|
|
@@ -79,19 +112,20 @@ async function refresh() {
|
|
|
79
112
|
const key = `${resolved.collection}::${resolved.id}`;
|
|
80
113
|
entryKeys.add(key);
|
|
81
114
|
|
|
82
|
-
const isScoped = parsed
|
|
115
|
+
const isScoped = parsed?.collection === null;
|
|
83
116
|
bindings.push({
|
|
84
117
|
el,
|
|
85
118
|
resolved,
|
|
86
|
-
fullKey: `${resolved.collection}::${resolved.id}::${resolved.field}`,
|
|
87
119
|
isImg: el instanceof HTMLImageElement,
|
|
88
120
|
isScoped,
|
|
121
|
+
index: bindings.length,
|
|
89
122
|
});
|
|
90
123
|
});
|
|
91
124
|
|
|
92
125
|
body.innerHTML = `<div class="cms-content-map-loading">Loading...</div>`;
|
|
93
126
|
|
|
94
127
|
const overrides = await loadOverrides(entryKeys);
|
|
128
|
+
if (!isOpen || generation !== refreshGeneration) return;
|
|
95
129
|
|
|
96
130
|
if (bindings.length === 0) {
|
|
97
131
|
body.innerHTML = `<div class="cms-content-map-empty">No data-caret bindings found on this page.</div>`;
|
|
@@ -99,11 +133,13 @@ async function refresh() {
|
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
// Group by collection::id
|
|
136
|
+
/** @type {Map<string, MapBinding[]>} */
|
|
102
137
|
const groups = new Map();
|
|
103
138
|
for (const b of bindings) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
const groupKey = `${b.resolved.collection}::${b.resolved.id}`;
|
|
140
|
+
const group = groups.get(groupKey) ?? [];
|
|
141
|
+
group.push(b);
|
|
142
|
+
groups.set(groupKey, group);
|
|
107
143
|
}
|
|
108
144
|
|
|
109
145
|
let html = '';
|
|
@@ -123,13 +159,13 @@ async function refresh() {
|
|
|
123
159
|
const typeTag = f.isImg ? '<span class="cms-map-type">img</span>' : '';
|
|
124
160
|
|
|
125
161
|
html += `
|
|
126
|
-
<
|
|
162
|
+
<button type="button" class="cms-content-map-item" data-caret-map-index="${f.index}">
|
|
127
163
|
<span class="cms-content-map-field">${escapeHtml(f.resolved.field)}</span>
|
|
128
164
|
<span class="cms-content-map-tags">
|
|
129
165
|
${typeTag}${scopeTag}
|
|
130
166
|
<span class="cms-map-status ${statusClass}">${statusLabel}</span>
|
|
131
167
|
</span>
|
|
132
|
-
</
|
|
168
|
+
</button>
|
|
133
169
|
`;
|
|
134
170
|
}
|
|
135
171
|
|
|
@@ -141,8 +177,9 @@ async function refresh() {
|
|
|
141
177
|
// Click to scroll to element
|
|
142
178
|
body.querySelectorAll('.cms-content-map-item').forEach((item) => {
|
|
143
179
|
item.addEventListener('click', () => {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
180
|
+
const rawIndex = item.getAttribute('data-caret-map-index');
|
|
181
|
+
const index = rawIndex !== null && /^\d+$/.test(rawIndex) ? Number(rawIndex) : -1;
|
|
182
|
+
const target = bindings[index];
|
|
146
183
|
if (target?.el) {
|
|
147
184
|
target.el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
148
185
|
target.el.classList.add('cms-map-highlight');
|
|
@@ -156,17 +193,19 @@ function toggle() {
|
|
|
156
193
|
if (!panel) panel = createPanel();
|
|
157
194
|
isOpen = !isOpen;
|
|
158
195
|
panel.classList.toggle('cms-content-map-open', isOpen);
|
|
196
|
+
panel.setAttribute('aria-hidden', String(!isOpen));
|
|
159
197
|
if (isOpen) refresh();
|
|
198
|
+
else refreshGeneration++;
|
|
160
199
|
}
|
|
161
200
|
|
|
201
|
+
/** @param {string} str @returns {string} */
|
|
162
202
|
function escapeHtml(str) {
|
|
163
203
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
164
204
|
}
|
|
165
205
|
|
|
166
|
-
|
|
167
|
-
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
168
|
-
}
|
|
169
|
-
|
|
206
|
+
/** @param {{ mapButton: Element | null }} options */
|
|
170
207
|
export function mountContentMap({ mapButton }) {
|
|
171
|
-
mapButton
|
|
208
|
+
if (!mapButton || mountedButtons.has(mapButton)) return;
|
|
209
|
+
mountedButtons.add(mapButton);
|
|
210
|
+
mapButton.addEventListener('click', toggle);
|
|
172
211
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** @typedef {'saving' | 'idle' | 'error'} ToolbarStatusState */
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} DeploymentStatusResponse
|
|
4
|
+
* @property {boolean} configured
|
|
5
|
+
* @property {'deploying' | 'live' | 'failed' | 'unknown'} state
|
|
6
|
+
* @property {unknown} [target]
|
|
7
|
+
* @property {unknown} [buildId]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Poll the authenticated deployment route and translate provider states into
|
|
12
|
+
* the toolbar's small status surface.
|
|
13
|
+
*
|
|
14
|
+
* @param {{
|
|
15
|
+
* fetchStatus: () => Promise<Response>,
|
|
16
|
+
* setStatus: (state: ToolbarStatusState, text: string) => void,
|
|
17
|
+
* setBuildId: (buildId: string | null) => void,
|
|
18
|
+
* pollDelayMs?: number,
|
|
19
|
+
* setTimer?: (callback: () => void, delay: number) => number,
|
|
20
|
+
* clearTimer?: (timer: number) => void,
|
|
21
|
+
* }} options
|
|
22
|
+
*/
|
|
23
|
+
export function createDeploymentStatusPoller(options) {
|
|
24
|
+
const pollDelayMs = options.pollDelayMs ?? 1000;
|
|
25
|
+
const setTimer = options.setTimer ?? ((callback, delay) => window.setTimeout(callback, delay));
|
|
26
|
+
const clearTimer = options.clearTimer ?? ((timer) => window.clearTimeout(timer));
|
|
27
|
+
let timer = 0;
|
|
28
|
+
|
|
29
|
+
function stop() {
|
|
30
|
+
if (timer) clearTimer(timer);
|
|
31
|
+
timer = 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schedule() {
|
|
35
|
+
stop();
|
|
36
|
+
timer = setTimer(() => { void refresh(); }, pollDelayMs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function refresh() {
|
|
40
|
+
try {
|
|
41
|
+
const response = await options.fetchStatus();
|
|
42
|
+
if (!response.ok) return;
|
|
43
|
+
const data = /** @type {DeploymentStatusResponse} */ (await response.json());
|
|
44
|
+
if (!data.configured || !data.target) return;
|
|
45
|
+
options.setBuildId(typeof data.buildId === 'string' ? data.buildId : null);
|
|
46
|
+
if (data.state === 'deploying') {
|
|
47
|
+
options.setStatus('saving', 'Deploying…');
|
|
48
|
+
schedule();
|
|
49
|
+
} else if (data.state === 'live') {
|
|
50
|
+
stop();
|
|
51
|
+
options.setStatus('idle', 'Live');
|
|
52
|
+
} else if (data.state === 'failed') {
|
|
53
|
+
stop();
|
|
54
|
+
options.setStatus('error', 'Deploy failed');
|
|
55
|
+
} else {
|
|
56
|
+
stop();
|
|
57
|
+
options.setStatus('error', 'Deploy status unknown');
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// A transient status lookup must not turn a successful content save into
|
|
61
|
+
// an error. A reload or a scheduled in-flight check can retry later.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { refresh, schedule, stop };
|
|
66
|
+
}
|