@charlescms/astro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/SECURITY.md +77 -0
  4. package/THIRD_PARTY_NOTICES.md +56 -0
  5. package/connector/worker.js +505 -0
  6. package/connector/wrangler.toml +15 -0
  7. package/package.json +92 -0
  8. package/scripts/check-licenses.js +45 -0
  9. package/scripts/check-package.js +62 -0
  10. package/scripts/setup.js +719 -0
  11. package/scripts/update-vendored-site.js +71 -0
  12. package/src/admin.astro +314 -0
  13. package/src/analyzer.js +639 -0
  14. package/src/asset-images.js +130 -0
  15. package/src/astro-frontmatter.js +17 -0
  16. package/src/boot.js +35 -0
  17. package/src/client.js +347 -0
  18. package/src/connector-client.js +185 -0
  19. package/src/content-bridge.js +162 -0
  20. package/src/content-panel.js +440 -0
  21. package/src/data-analyzer.js +304 -0
  22. package/src/edit-affordance.js +463 -0
  23. package/src/editor-styles.js +243 -0
  24. package/src/element-editor.js +355 -0
  25. package/src/fields.js +6 -0
  26. package/src/frontmatter.js +153 -0
  27. package/src/ids.js +20 -0
  28. package/src/index.js +681 -0
  29. package/src/js-ast.js +140 -0
  30. package/src/markdown-analyzer.js +95 -0
  31. package/src/media-preview.js +58 -0
  32. package/src/panel-manager.js +133 -0
  33. package/src/publishing.js +457 -0
  34. package/src/rich-text-editor.js +209 -0
  35. package/src/routes.js +21 -0
  36. package/src/runtime-controller.js +206 -0
  37. package/src/sanitize.js +150 -0
  38. package/src/section-editor.js +437 -0
  39. package/src/source-edit.js +310 -0
  40. package/src/source-map-runtime.js +184 -0
  41. package/src/staged-panel.js +145 -0
  42. package/src/toolbar.js +128 -0
  43. package/src/versions-panel.js +112 -0
package/src/toolbar.js ADDED
@@ -0,0 +1,128 @@
1
+ // Renders the persistent editor toolbar and exposes its small status API.
2
+ //
3
+ // The toolbar does not publish or open panels itself. Those operations are
4
+ // injected, which keeps this module focused on visibility, button state, busy
5
+ // state, and reserving enough page space that the fixed toolbar covers nothing.
6
+ export function createToolbar({
7
+ state,
8
+ currentPageHasFrontmatter,
9
+ openPageSettings,
10
+ openVersionsPanel,
11
+ openStagedPanel,
12
+ publishPending,
13
+ discardPending,
14
+ logout,
15
+ scanEditableElements
16
+ }) {
17
+ function installToolbar() {
18
+ const toolbar = document.createElement("div");
19
+ toolbar.dataset.charlescmsUi = "true";
20
+ toolbar.className = "charlescms-toolbar";
21
+ toolbar.innerHTML = `
22
+ <div class="charlescms-brand"><strong>CharlesCMS</strong></div>
23
+ <div class="charlescms-info" data-info></div>
24
+ <div class="charlescms-toolbar-actions">
25
+ <button type="button" data-charlescms-review hidden title="See every staged change and which page it's on">Review</button>
26
+ <button type="button" data-charlescms-discard hidden>Discard</button>
27
+ <button type="button" data-charlescms-publish hidden>Publish</button>
28
+ <button type="button" data-charlescms-page-settings hidden title="Fields that don't show on the page itself, like the Google title and description">Page info</button>
29
+ <button type="button" data-charlescms-highlight title="Outline everything you can edit on this page">Show editable</button>
30
+ <button type="button" data-charlescms-versions ${state.previewOnly ? 'disabled title="Disabled in this public demo"' : ""}>Versions</button>
31
+ ${state.previewOnly ? "" : '<button type="button" data-charlescms-logout>Logout</button>'}
32
+ </div>
33
+ `;
34
+ toolbar.querySelector("[data-charlescms-logout]")?.addEventListener("click", logout);
35
+
36
+ // Editors start with all editable regions outlined; the toggle provides an
37
+ // unobstructed preview without leaving edit mode.
38
+ const highlight = toolbar.querySelector("[data-charlescms-highlight]");
39
+ const setHighlight = (on) => {
40
+ document.documentElement.classList.toggle("charlescms-show-all", on);
41
+ highlight.classList.toggle("charlescms-toggle-on", on);
42
+ highlight.textContent = on ? "Hide outlines" : "Show editable";
43
+ };
44
+ setHighlight(true);
45
+ highlight.addEventListener("click", () => setHighlight(!document.documentElement.classList.contains("charlescms-show-all")));
46
+ toolbar.querySelector("[data-charlescms-publish]")?.addEventListener("click", state.previewOnly ? explainPreviewOnly : publishPending);
47
+ toolbar.querySelector("[data-charlescms-discard]").addEventListener("click", discardPending);
48
+ toolbar.querySelector("[data-charlescms-review]")?.addEventListener("click", openStagedPanel);
49
+
50
+ // "Page info" is the only panel button: it edits the page's fields that
51
+ // render nowhere visible (Google title, meta description, frontmatter).
52
+ // Everything visible is edited by clicking it on the page itself.
53
+ const pageSettings = toolbar.querySelector("[data-charlescms-page-settings]");
54
+ pageSettings.hidden = !currentPageHasFrontmatter();
55
+ pageSettings.addEventListener("click", openPageSettings);
56
+ toolbar.querySelector("[data-charlescms-versions]")?.addEventListener("click", state.previewOnly ? explainPreviewOnly : openVersionsPanel);
57
+
58
+ document.body.append(toolbar);
59
+ state.toolbar = toolbar;
60
+ installToolbarReserve(toolbar);
61
+ renderPendingTray();
62
+ }
63
+
64
+ function installToolbarReserve(toolbar) {
65
+ const body = document.body;
66
+ body.style.setProperty("--charlescms-page-padding-bottom", getComputedStyle(body).paddingBottom);
67
+ const update = () => {
68
+ const reserve = Math.ceil(toolbar.getBoundingClientRect().height + 32);
69
+ document.documentElement.style.setProperty("--charlescms-toolbar-reserve", `${reserve}px`);
70
+ };
71
+ state.toolbarObserver?.disconnect();
72
+ state.toolbarObserver = new ResizeObserver(update);
73
+ state.toolbarObserver.observe(toolbar);
74
+ update();
75
+ }
76
+
77
+ function renderPendingTray() {
78
+ if (!state.toolbar) return;
79
+ const publish = state.toolbar.querySelector("[data-charlescms-publish]");
80
+ const discard = state.toolbar.querySelector("[data-charlescms-discard]");
81
+ const count = state.pending.size;
82
+ if (publish) {
83
+ publish.hidden = false;
84
+ publish.disabled = state.previewOnly || state.busy;
85
+ if (state.previewOnly) publish.title = "Disabled in this public demo";
86
+ else publish.title = count === 0 ? "No changes to publish" : "";
87
+ }
88
+ discard.hidden = count === 0;
89
+ const review = state.toolbar.querySelector("[data-charlescms-review]");
90
+ if (review) review.hidden = count === 0;
91
+ // One compact status slot serves both as the idle editable count and the
92
+ // staged-change indicator. Transient messages reuse the same slot.
93
+ setToolbarInfo(count > 0
94
+ ? `${count} change${count === 1 ? "" : "s"} staged`
95
+ : state.previewOnly ? "Preview only" : `${scanEditableElements().length} editable`);
96
+ }
97
+
98
+ function setToolbarInfo(text) {
99
+ const info = state.toolbar?.querySelector("[data-info]");
100
+ if (!info) return;
101
+ info.textContent = text;
102
+ info.classList.toggle("charlescms-info-pending", state.pending.size > 0);
103
+ }
104
+
105
+ function explainPreviewOnly() {
106
+ setToolbarStatus("Disabled in this public demo. Real installations can publish through your connector.");
107
+ }
108
+
109
+ function setBusy(value, message = "") {
110
+ state.busy = value;
111
+ if (message) setToolbarStatus(message);
112
+ if (!state.toolbar) return;
113
+ for (const button of state.toolbar.querySelectorAll("button")) {
114
+ button.disabled = value;
115
+ }
116
+ }
117
+
118
+ function setToolbarStatus(message) {
119
+ setToolbarInfo(message);
120
+ }
121
+
122
+ return {
123
+ installToolbar,
124
+ renderPendingTray,
125
+ setBusy,
126
+ setToolbarStatus
127
+ };
128
+ }
@@ -0,0 +1,112 @@
1
+ // Presents Git commit history as editor-friendly versions.
2
+ //
3
+ // Restoring a version never rewrites Git history. The connector creates a new
4
+ // revert commit, then this panel waits for the deployed page to become reachable
5
+ // again before reporting success.
6
+ export function createVersionsPanel({
7
+ state,
8
+ closeEditor,
9
+ mountPanel,
10
+ setBusy,
11
+ setToolbarStatus,
12
+ waitForLivePage,
13
+ showPanelError,
14
+ branchHint,
15
+ escapeHtml,
16
+ escapeAttribute
17
+ }) {
18
+ async function openVersionsPanel() {
19
+ // Panels are mutually exclusive. Closing first also cleans listeners owned
20
+ // by the previously mounted panel.
21
+ closeEditor();
22
+ const dialog = document.createElement("div");
23
+ dialog.dataset.charlescmsUi = "true";
24
+ dialog.className = "charlescms-panel charlescms-versions";
25
+ dialog.innerHTML = `
26
+ <div class="charlescms-panel-header">
27
+ <div>
28
+ <div class="charlescms-kicker">Versions</div>
29
+ <div class="charlescms-title">Published changes</div>
30
+ </div>
31
+ <button class="charlescms-icon-button" data-close aria-label="Close">×</button>
32
+ </div>
33
+ <div data-versions-list class="charlescms-versions-list">Loading...</div>
34
+ `;
35
+ mountPanel(dialog);
36
+ dialog.querySelector("[data-close]").addEventListener("click", closeEditor);
37
+ await loadVersions(dialog);
38
+ }
39
+
40
+ // Only commits the CMS itself published. Raw repository history (developer
41
+ // commits, merges, refactors) is the developer's domain — showing it here
42
+ // would invite an editor to "restore" a code change they can't assess.
43
+ // CMS commits are recognisable by the fixed message prefixes the editor
44
+ // writes (publishing.js) and the revert commits this panel creates.
45
+ function isCmsPublish(version) {
46
+ return /^(?:Update content|Update section|Add section|Remove section|Remove media|Upload media|Replace download|Revert)\b/.test(String(version.message || ""));
47
+ }
48
+
49
+ async function loadVersions(dialog) {
50
+ const list = dialog.querySelector("[data-versions-list]");
51
+ try {
52
+ const versions = (await state.connector.listCommits()).filter(isCmsPublish);
53
+ if (versions.length === 0) {
54
+ list.textContent = "No published versions yet. Changes you publish will appear here, each with an undo.";
55
+ return;
56
+ }
57
+ list.innerHTML = versions.map((version, index) => `
58
+ <div class="charlescms-version">
59
+ <div>
60
+ <strong>${escapeHtml(firstLine(version.message) || version.sha.slice(0, 7))}</strong>
61
+ <span>${escapeHtml(formatVersionDate(version.committedAt || version.authoredAt))}</span>
62
+ </div>
63
+ <button type="button" data-revert="${escapeAttribute(version.sha)}">${index === 0 ? "Undo last publish" : "Restore"}</button>
64
+ </div>
65
+ `).join("");
66
+ // Event delegation keeps one listener regardless of history length.
67
+ list.addEventListener("click", async (event) => {
68
+ const button = event.target.closest("[data-revert]");
69
+ if (!button) return;
70
+ await revertVersion(button.dataset.revert, dialog);
71
+ });
72
+ } catch (error) {
73
+ list.textContent = error?.status === 404
74
+ ? `History couldn't load: ${branchHint()}.`
75
+ : (error.message || "Versions could not be loaded.");
76
+ }
77
+ }
78
+
79
+ async function revertVersion(sha, dialog) {
80
+ if (state.busy) return;
81
+ // Reverting creates a new commit. Existing history is never rewritten, so a
82
+ // later restore remains possible and the repository keeps a complete audit.
83
+ if (!window.confirm("Undo this change? This adds a new commit that reverses it — your history stays intact.")) return;
84
+ setBusy(true, "Reverting...");
85
+ try {
86
+ await state.connector.revertCommit(sha);
87
+ setToolbarStatus("Reverted. Waiting for the deployed page...");
88
+ await waitForLivePage();
89
+ setToolbarStatus("Reverted.");
90
+ dialog.remove();
91
+ state.active = null;
92
+ } catch (error) {
93
+ showPanelError(dialog, error.message || "Revert failed.");
94
+ setToolbarStatus(error.message || "Revert failed.");
95
+ } finally {
96
+ setBusy(false);
97
+ }
98
+ }
99
+
100
+ return { openVersionsPanel };
101
+ }
102
+
103
+ function firstLine(value) {
104
+ return String(value || "").split("\n")[0];
105
+ }
106
+
107
+ function formatVersionDate(value) {
108
+ if (!value) return "";
109
+ const date = new Date(value);
110
+ if (Number.isNaN(date.getTime())) return value;
111
+ return date.toLocaleString();
112
+ }