@tideworthy/tidysites-platform 0.6.1 → 0.7.1
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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/editor/TidyEditor.astro +41 -27
- package/src/editor/runtime.ts +37 -6
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
- Editor: the class the bar puts on `<html>` is `tidy-has-bar`. 0.7.0 used `tidy-bar`, the bar's own class, so the bar's fixed styles applied to the document and pushed the top of the page off screen for a signed-in editor.
|
|
6
|
+
|
|
7
|
+
## 0.7.0
|
|
8
|
+
|
|
9
|
+
- Editor: the corner pill is a bar across the top of the page. "Staging" (the `label` prop) on the left; Edit on the right while editing is off; Business details, Publish (with the count, only while something is unpublished) and Done while it is on. The bar sets `--tidy-bar` on `<html>` so a template's sticky header can sit below it.
|
|
10
|
+
- Editor: editing is a page edit. Inside the sections a click edits the block or record it lands on and never follows a link; a click on the header or footer (outside the menu) opens Business details.
|
|
11
|
+
- Editor: section and record outlines show only while editing. They used to show on hover for every visitor, live site included.
|
|
12
|
+
|
|
3
13
|
## 0.6.1
|
|
4
14
|
|
|
5
15
|
- Editor: EmDash's toolbar is hidden while editing is off, so the Edit pill is the one control on the page. It still shows on article pages in edit mode, where EmDash's inline editing needs it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tideworthy/tidysites-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "The Tidysites platform integration for EmDash sites: bootstrap, sessions, maintenance, health, and publish hooks under /_tidy/*.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* The Tideworthy on-site editor. Mount it from the layout on every page; it
|
|
4
|
-
* renders nothing for a visitor,
|
|
5
|
-
* the
|
|
6
|
-
* and cleared by /_tidy/edit
|
|
7
|
-
* they show, the block schemas the template
|
|
8
|
-
* rendered; the template frames each custom
|
|
9
|
-
* (its key) and `data-tidy-type`, and wraps the
|
|
10
|
-
*
|
|
11
|
-
* `data-tidy-record-label`) gets an "Edit details"
|
|
12
|
-
* from EmDash's manifest. A page without blocks (an
|
|
13
|
-
* only; EmDash's inline editing handles its prose
|
|
14
|
-
* toolbar shows
|
|
4
|
+
* renders nothing for a visitor and, for a signed-in editor, a bar across the
|
|
5
|
+
* top of the page: "Edit" while editing is off, the editor's controls while
|
|
6
|
+
* it is on (the `emdash-edit-mode` cookie, set and cleared by /_tidy/edit).
|
|
7
|
+
* Block pages hand it the entry they show, the block schemas the template
|
|
8
|
+
* declares, and the block list as rendered; the template frames each custom
|
|
9
|
+
* block with `data-tidy-block` (its key) and `data-tidy-type`, and wraps the
|
|
10
|
+
* list in `data-tidy-blocks`. A record region (`data-tidy-record`,
|
|
11
|
+
* `data-tidy-record-id`, `data-tidy-record-label`) gets an "Edit details"
|
|
12
|
+
* chip whose form comes from EmDash's manifest. A page without blocks (an
|
|
13
|
+
* article) gets the bar only; EmDash's inline editing handles its prose and
|
|
14
|
+
* its toolbar shows there. While editing is off, EmDash's toolbar is hidden:
|
|
15
|
+
* the bar is the one control. The bar sets `--tidy-bar` on <html> (its
|
|
16
|
+
* height) so a template's sticky header can sit below it.
|
|
15
17
|
*/
|
|
16
18
|
import type { Block, BlockSchema } from "./ops";
|
|
17
19
|
|
|
@@ -21,26 +23,32 @@ interface Props {
|
|
|
21
23
|
field?: string;
|
|
22
24
|
blocks?: BlockSchema[];
|
|
23
25
|
value?: Block[] | null;
|
|
24
|
-
/** Records reachable from the
|
|
26
|
+
/** Records reachable from the bar (the business) or from `[data-tidy-record]` regions on the page. */
|
|
25
27
|
records?: Array<{ collection: string; id: string; label: string }>;
|
|
28
|
+
/** What the bar calls this site; "Staging" by default. */
|
|
29
|
+
label?: string;
|
|
26
30
|
}
|
|
27
|
-
const { collection, id, field = "content", blocks, value, records = [] } = Astro.props;
|
|
31
|
+
const { collection, id, field = "content", blocks, value, records = [], label = "Staging" } = Astro.props;
|
|
28
32
|
const signedIn = Boolean(Astro.session ? await Astro.session.get("user") : null);
|
|
29
33
|
const active = Astro.cookies.get("emdash-edit-mode")?.value === "true";
|
|
30
34
|
const here = Astro.url.pathname + Astro.url.search;
|
|
31
35
|
const toggle = (on: boolean) => `/_tidy/edit?on=${on ? 1 : 0}&to=${encodeURIComponent(here)}`;
|
|
32
36
|
const editable = active && Boolean(collection && id && blocks && value);
|
|
33
37
|
const config = editable ? JSON.stringify({ collection, id, field, blocks, value, records, done: toggle(false) }) : null;
|
|
38
|
+
const htmlClass = !signedIn ? "" : !active ? "tidy-has-bar tidy-idle" : editable ? "tidy-has-bar" : "tidy-has-bar tidy-inline";
|
|
34
39
|
---
|
|
35
|
-
{signedIn &&
|
|
40
|
+
{signedIn && (
|
|
36
41
|
<>
|
|
37
|
-
<div class="tidy-
|
|
38
|
-
|
|
42
|
+
<div class="tidy-bar" id="tidy-bar" role="region" aria-label="Site editing">
|
|
43
|
+
<span class="tidy-bar-label">{label}</span>
|
|
44
|
+
<div class="tidy-bar-actions" id="tidy-bar-actions">
|
|
45
|
+
{!active && <a class="tidy-btn tidy-btn-primary" href={toggle(true)}>Edit</a>}
|
|
46
|
+
{active && !editable && <a class="tidy-btn" href={toggle(false)}>Done</a>}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<script is:inline define:vars={{ htmlClass }}>document.documentElement.className += " " + htmlClass;</script>
|
|
39
50
|
</>
|
|
40
51
|
)}
|
|
41
|
-
{signedIn && active && !editable && (
|
|
42
|
-
<div class="tidy-status"><span>Editing</span><a class="tidy-btn" href={toggle(false)}>Done</a></div>
|
|
43
|
-
)}
|
|
44
52
|
{config && (
|
|
45
53
|
<>
|
|
46
54
|
<script type="application/json" id="tidy-editor-config" set:html={config.replace(/</g, "\\u003c")} />
|
|
@@ -49,8 +57,17 @@ const config = editable ? JSON.stringify({ collection, id, field, blocks, value,
|
|
|
49
57
|
)}
|
|
50
58
|
<style is:global>
|
|
51
59
|
.tidy-editing #emdash-toolbar, .tidy-idle #emdash-toolbar { display: none !important; }
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
html.tidy-has-bar { --tidy-bar: 44px; }
|
|
61
|
+
html.tidy-has-bar body { padding-top: var(--tidy-bar); }
|
|
62
|
+
.tidy-bar { position: fixed; top: 0; left: 0; right: 0; height: var(--tidy-bar, 44px); z-index: 2147483000; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 16px; box-sizing: border-box; background: #17211c; color: #fff; font: 500 13px/1 system-ui, sans-serif; box-shadow: 0 1px 0 rgba(255,255,255,.08), 0 6px 20px rgba(0,0,0,.18); }
|
|
63
|
+
.tidy-bar-label { opacity: .7; letter-spacing: .04em; text-transform: uppercase; font-size: 11px; }
|
|
64
|
+
.tidy-bar-actions { display: flex; align-items: center; gap: 8px; }
|
|
65
|
+
.tidy-bar-actions a { text-decoration: none; }
|
|
66
|
+
.tidy-bar .tidy-btn { padding: 6px 12px; }
|
|
67
|
+
.tidy-bar-count { opacity: .85; margin-right: 4px; }
|
|
68
|
+
.tidy-bar-count:empty { display: none; }
|
|
69
|
+
.tidy-editing [data-tidy-record] { outline: 2px dashed transparent; outline-offset: 4px; transition: outline-color 120ms; cursor: pointer; }
|
|
70
|
+
.tidy-editing [data-tidy-record]:hover, .tidy-editing [data-tidy-record].tidy-active { outline-color: #2b7fd1; }
|
|
54
71
|
.tidy-image-preview { display: grid; place-items: center; min-height: 96px; border: 1px solid #d3dcd6; border-radius: 6px; background: #f6f7f5; overflow: hidden; }
|
|
55
72
|
.tidy-image-preview img { max-width: 100%; max-height: 180px; display: block; }
|
|
56
73
|
.tidy-image-empty { color: #7b877f; font-size: 13px; padding: 8px; }
|
|
@@ -60,10 +77,10 @@ const config = editable ? JSON.stringify({ collection, id, field, blocks, value,
|
|
|
60
77
|
.tidy-image-thumb { padding: 0; border: 2px solid transparent; border-radius: 6px; overflow: hidden; background: #fff; cursor: pointer; aspect-ratio: 1; }
|
|
61
78
|
.tidy-image-thumb:hover { border-color: #2b7fd1; }
|
|
62
79
|
.tidy-image-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
63
|
-
[data-tidy-block] { position: relative; outline: 2px dashed transparent; outline-offset: -2px; transition: outline-color 120ms; cursor: pointer; }
|
|
64
|
-
[data-tidy-block]:hover, [data-tidy-block].tidy-active { outline-color: #2b7fd1; }
|
|
80
|
+
.tidy-editing [data-tidy-block] { position: relative; outline: 2px dashed transparent; outline-offset: -2px; transition: outline-color 120ms; cursor: pointer; }
|
|
81
|
+
.tidy-editing [data-tidy-block]:hover, .tidy-editing [data-tidy-block].tidy-active { outline-color: #2b7fd1; }
|
|
65
82
|
.tidy-chip { position: absolute; z-index: 2147483000; display: flex; gap: 4px; align-items: center; padding: 4px 6px; background: #17211c; color: #fff; border-radius: 8px; font: 500 13px/1.2 system-ui, sans-serif; box-shadow: 0 6px 20px rgba(0,0,0,.25); }
|
|
66
|
-
.tidy-chip[hidden], .tidy-panel[hidden]
|
|
83
|
+
.tidy-chip[hidden], .tidy-panel[hidden] { display: none; }
|
|
67
84
|
.tidy-chip-label { padding: 0 6px 0 4px; opacity: .85; }
|
|
68
85
|
.tidy-btn { appearance: none; border: 1px solid rgba(255,255,255,.25); background: rgba(255,255,255,.08); color: inherit; border-radius: 6px; padding: 4px 8px; font: inherit; cursor: pointer; }
|
|
69
86
|
.tidy-btn:hover { background: rgba(255,255,255,.18); }
|
|
@@ -93,7 +110,4 @@ const config = editable ? JSON.stringify({ collection, id, field, blocks, value,
|
|
|
93
110
|
.tidy-add-item { display: grid; gap: 2px; text-align: left; padding: 10px 12px; border: 1px solid #d3dcd6; border-radius: 8px; background: #fff; cursor: pointer; font: inherit; }
|
|
94
111
|
.tidy-add-item:hover { border-color: #2b7fd1; background: rgba(43,127,209,.06); }
|
|
95
112
|
.tidy-add-item span { color: #4a5851; font-size: 13px; }
|
|
96
|
-
.tidy-status { position: fixed; left: 16px; bottom: 16px; z-index: 2147483000; display: flex; gap: 10px; align-items: center; padding: 8px 12px; background: #17211c; color: #fff; border-radius: 999px; font: 500 13px system-ui, sans-serif; box-shadow: 0 6px 20px rgba(0,0,0,.25); }
|
|
97
|
-
.tidy-status a { text-decoration: none; }
|
|
98
|
-
.tidy-status-idle { padding: 4px; }
|
|
99
113
|
</style>
|
package/src/editor/runtime.ts
CHANGED
|
@@ -97,20 +97,19 @@ export function start(config: Config): void {
|
|
|
97
97
|
const pending = new Set<string>(JSON.parse(sessionStorage.getItem(PENDING_KEY) ?? "[]") as string[]);
|
|
98
98
|
const persistPending = () => sessionStorage.setItem(PENDING_KEY, JSON.stringify([...pending]));
|
|
99
99
|
|
|
100
|
-
// ---
|
|
101
|
-
const status = el("div", { class: "tidy-
|
|
102
|
-
const statusText = el("span");
|
|
100
|
+
// --- the bar's controls --------------------------------------------------
|
|
101
|
+
const status = document.getElementById("tidy-bar-actions") ?? document.body.appendChild(el("div", { class: "tidy-bar-actions" }));
|
|
102
|
+
const statusText = el("span", { class: "tidy-bar-count" });
|
|
103
103
|
const bizBtn = el("button", { type: "button", class: "tidy-btn", text: "Business details" });
|
|
104
104
|
const publishBtn = el("button", { type: "button", class: "tidy-btn tidy-btn-primary", text: "Publish" });
|
|
105
105
|
const doneBtn = el("a", { class: "tidy-btn", href: config.done ?? `/_tidy/edit?on=0&to=${encodeURIComponent(location.pathname + location.search)}`, text: "Done" });
|
|
106
|
-
status.
|
|
107
|
-
document.body.append(status);
|
|
106
|
+
status.replaceChildren(statusText, bizBtn, publishBtn, doneBtn);
|
|
108
107
|
const business = (config.records ?? []).find((r) => r.collection === "business");
|
|
109
108
|
bizBtn.hidden = !business;
|
|
110
109
|
bizBtn.addEventListener("click", () => business && void openRecord(business));
|
|
111
110
|
function refreshStatus(text?: string) {
|
|
112
111
|
const n = pending.size;
|
|
113
|
-
statusText.textContent = text ?? (n === 0 ? "
|
|
112
|
+
statusText.textContent = text ?? (n === 0 ? "" : `${n} unpublished change${n === 1 ? "" : "s"}`);
|
|
114
113
|
publishBtn.hidden = n === 0 && !text?.startsWith("Publish failed");
|
|
115
114
|
}
|
|
116
115
|
refreshStatus();
|
|
@@ -228,6 +227,38 @@ export function start(config: Config): void {
|
|
|
228
227
|
void openRecord({ collection: r.dataset.tidyRecord ?? "", id: r.dataset.tidyRecordId ?? "", label: r.dataset.tidyRecordLabel ?? "" });
|
|
229
228
|
});
|
|
230
229
|
}
|
|
230
|
+
// Editing is a page edit: inside the sections a click edits what it lands
|
|
231
|
+
// on and never follows a link; outside them the header and footer show the
|
|
232
|
+
// business record's facts, so a click there opens Business details. The
|
|
233
|
+
// menu keeps walking the site, and the editor's own controls are untouched.
|
|
234
|
+
document.addEventListener(
|
|
235
|
+
"click",
|
|
236
|
+
(e) => {
|
|
237
|
+
const t = e.target as HTMLElement | null;
|
|
238
|
+
if (!t || t.closest(".tidy-bar,.tidy-chip,.tidy-panel,.tidy-add-end,#emdash-toolbar")) return;
|
|
239
|
+
if (t.closest("[data-tidy-blocks]")) {
|
|
240
|
+
if (!t.closest("a,button")) return;
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
e.stopPropagation();
|
|
243
|
+
const record = t.closest<HTMLElement>("[data-tidy-record]");
|
|
244
|
+
const frame = t.closest<HTMLElement>("[data-tidy-block]");
|
|
245
|
+
if (record) {
|
|
246
|
+
showRecordChip(record);
|
|
247
|
+
void openRecord({ collection: record.dataset.tidyRecord ?? "", id: record.dataset.tidyRecordId ?? "", label: record.dataset.tidyRecordLabel ?? "" });
|
|
248
|
+
} else if (frame) {
|
|
249
|
+
showBlockChip(frame);
|
|
250
|
+
openBlock(frame.dataset.tidyBlock ?? "");
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (t.closest("main") || t.closest("nav a, header button, input, select, textarea, label")) return;
|
|
255
|
+
if (!business) return;
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
e.stopPropagation();
|
|
258
|
+
void openRecord(business);
|
|
259
|
+
},
|
|
260
|
+
true
|
|
261
|
+
);
|
|
231
262
|
// The chip sits outside every frame; hovering it must not count as leaving.
|
|
232
263
|
chip.addEventListener("mouseenter", () => current?.classList.add("tidy-active"));
|
|
233
264
|
bEdit.addEventListener("click", () => {
|
package/src/version.ts
CHANGED