@tideworthy/tidysites-platform 0.5.1 → 0.6.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/CHANGELOG.md +5 -0
- package/README.md +4 -0
- package/package.json +2 -1
- package/src/editor/TidyEditor.astro +36 -15
- package/src/editor/runtime.ts +5 -1
- package/src/index.ts +1 -0
- package/src/routes/edit.ts +26 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- `GET /_tidy/edit?on=1|0&to=/path`: switches on-site editing on or off for the signed-in editor (the `emdash-edit-mode` cookie) and returns to the page. No bearer; a visitor without a session is sent back unchanged.
|
|
6
|
+
- Editor: mounts itself. The layout renders `<TidyEditor>` on every page; it shows nothing to a visitor, an "Edit" pill to a signed-in editor, and the editor once editing is on. A page without blocks gets the pill only (EmDash's inline editing handles its prose). The pill gains "Done". Every prop is optional now; EmDash's toolbar is hidden only while this editor runs (`html.tidy-editing`).
|
|
7
|
+
|
|
3
8
|
## 0.5.1
|
|
4
9
|
|
|
5
10
|
- Editor: a record region nested inside a section (a service card in the services grid) gets its own "Edit details" chip; clicking it edits the record instead of following the card's link, and leaving it hands the chip back to the section.
|
package/README.md
CHANGED
|
@@ -96,6 +96,10 @@ Mints a one-time sign-in link for a person the platform has already authenticate
|
|
|
96
96
|
|
|
97
97
|
The browser lands here. Signs the user in (an Astro session, exactly as EmDash's own login does), sets or clears the visual-editing cookie, and redirects with 303 to the requested path. A link can be used once. Expired, reused, or unknown links get a 410 page.
|
|
98
98
|
|
|
99
|
+
### `GET /_tidy/edit?on=1|0&to=/path`
|
|
100
|
+
|
|
101
|
+
No bearer. For the browser session the claim signed in: sets (`on=1`) or clears the `emdash-edit-mode` cookie and answers 303 to `to` (a local path; anything else becomes `/`). Without a signed-in session it answers the same 303 and changes nothing. The editor's pill links here.
|
|
102
|
+
|
|
99
103
|
### `GET /_tidy/health`
|
|
100
104
|
|
|
101
105
|
`{ ok, plugin, setupComplete, siteTitle, siteUrl, migrations: { applied, pending }, time }`. `ok` is false and the status is 503 when migrations are pending.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tideworthy/tidysites-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"./routes/bootstrap": "./src/routes/bootstrap.ts",
|
|
26
26
|
"./routes/session": "./src/routes/session.ts",
|
|
27
27
|
"./routes/session-claim": "./src/routes/session-claim.ts",
|
|
28
|
+
"./routes/edit": "./src/routes/edit.ts",
|
|
28
29
|
"./routes/health": "./src/routes/health.ts",
|
|
29
30
|
"./routes/rotate-token": "./src/routes/rotate-token.ts",
|
|
30
31
|
"./routes/fill": "./src/routes/fill.ts",
|
|
@@ -1,31 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* The Tideworthy on-site editor
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* The Tideworthy on-site editor. Mount it from the layout on every page; it
|
|
4
|
+
* renders nothing for a visitor, an "Edit" pill for a signed-in editor, and
|
|
5
|
+
* the editor itself once editing is on (the `emdash-edit-mode` cookie, set
|
|
6
|
+
* and cleared by /_tidy/edit from the pill). Block pages hand it the entry
|
|
7
|
+
* they show, the block schemas the template declares, and the block list as
|
|
8
|
+
* rendered; the template frames each custom block with `data-tidy-block`
|
|
9
|
+
* (its key) and `data-tidy-type`, and wraps the list in `data-tidy-blocks`.
|
|
10
|
+
* A record region (`data-tidy-record`, `data-tidy-record-id`,
|
|
11
|
+
* `data-tidy-record-label`) gets an "Edit details" chip whose form comes
|
|
12
|
+
* from EmDash's manifest. A page without blocks (an article) gets the pill
|
|
13
|
+
* only; EmDash's inline editing handles its prose in edit mode.
|
|
10
14
|
*/
|
|
11
15
|
import type { Block, BlockSchema } from "./ops";
|
|
12
16
|
|
|
13
17
|
interface Props {
|
|
14
|
-
collection
|
|
15
|
-
id
|
|
18
|
+
collection?: string;
|
|
19
|
+
id?: string;
|
|
16
20
|
field?: string;
|
|
17
|
-
blocks
|
|
18
|
-
value
|
|
21
|
+
blocks?: BlockSchema[];
|
|
22
|
+
value?: Block[] | null;
|
|
19
23
|
/** Records reachable from the pill (the business) or from `[data-tidy-record]` regions on the page. */
|
|
20
24
|
records?: Array<{ collection: string; id: string; label: string }>;
|
|
21
25
|
}
|
|
22
26
|
const { collection, id, field = "content", blocks, value, records = [] } = Astro.props;
|
|
23
|
-
const
|
|
27
|
+
const signedIn = Boolean(Astro.session ? await Astro.session.get("user") : null);
|
|
28
|
+
const active = Astro.cookies.get("emdash-edit-mode")?.value === "true";
|
|
29
|
+
const here = Astro.url.pathname + Astro.url.search;
|
|
30
|
+
const toggle = (on: boolean) => `/_tidy/edit?on=${on ? 1 : 0}&to=${encodeURIComponent(here)}`;
|
|
31
|
+
const editable = active && Boolean(collection && id && blocks && value);
|
|
32
|
+
const config = editable ? JSON.stringify({ collection, id, field, blocks, value, records, done: toggle(false) }) : null;
|
|
24
33
|
---
|
|
25
|
-
|
|
26
|
-
<
|
|
34
|
+
{signedIn && !active && (
|
|
35
|
+
<div class="tidy-status tidy-status-idle"><a class="tidy-btn tidy-btn-primary" href={toggle(true)}>Edit</a></div>
|
|
36
|
+
)}
|
|
37
|
+
{signedIn && active && !editable && (
|
|
38
|
+
<div class="tidy-status"><span>Editing</span><a class="tidy-btn" href={toggle(false)}>Done</a></div>
|
|
39
|
+
)}
|
|
40
|
+
{config && (
|
|
41
|
+
<>
|
|
42
|
+
<script type="application/json" id="tidy-editor-config" set:html={config.replace(/</g, "\\u003c")} />
|
|
43
|
+
<script src="./runtime.ts"></script>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
27
46
|
<style is:global>
|
|
28
|
-
#emdash-toolbar { display: none !important; }
|
|
47
|
+
.tidy-editing #emdash-toolbar { display: none !important; }
|
|
29
48
|
[data-tidy-record] { outline: 2px dashed transparent; outline-offset: 4px; transition: outline-color 120ms; cursor: pointer; }
|
|
30
49
|
[data-tidy-record]:hover, [data-tidy-record].tidy-active { outline-color: #2b7fd1; }
|
|
31
50
|
.tidy-image-preview { display: grid; place-items: center; min-height: 96px; border: 1px solid #d3dcd6; border-radius: 6px; background: #f6f7f5; overflow: hidden; }
|
|
@@ -71,4 +90,6 @@ const config = JSON.stringify({ collection, id, field, blocks, value, records })
|
|
|
71
90
|
.tidy-add-item:hover { border-color: #2b7fd1; background: rgba(43,127,209,.06); }
|
|
72
91
|
.tidy-add-item span { color: #4a5851; font-size: 13px; }
|
|
73
92
|
.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); }
|
|
93
|
+
.tidy-status a { text-decoration: none; }
|
|
94
|
+
.tidy-status-idle { padding: 4px; }
|
|
74
95
|
</style>
|
package/src/editor/runtime.ts
CHANGED
|
@@ -32,6 +32,8 @@ interface Config {
|
|
|
32
32
|
blocks: BlockSchema[];
|
|
33
33
|
value: Block[];
|
|
34
34
|
records?: RecordRef[];
|
|
35
|
+
/** Where "Done" goes: /_tidy/edit switching editing off and returning here. */
|
|
36
|
+
done?: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
interface ManifestField {
|
|
@@ -88,6 +90,7 @@ export function start(config: Config): void {
|
|
|
88
90
|
const frames = () => Array.from(document.querySelectorAll<HTMLElement>("[data-tidy-block]"));
|
|
89
91
|
|
|
90
92
|
// EmDash's inline editor is for prose pages; here the sections and records own their fields.
|
|
93
|
+
document.documentElement.classList.add("tidy-editing");
|
|
91
94
|
document.querySelectorAll("[data-emdash-ref]").forEach((n) => n.removeAttribute("data-emdash-ref"));
|
|
92
95
|
|
|
93
96
|
// --- pending publishes ---------------------------------------------------
|
|
@@ -99,7 +102,8 @@ export function start(config: Config): void {
|
|
|
99
102
|
const statusText = el("span");
|
|
100
103
|
const bizBtn = el("button", { type: "button", class: "tidy-btn", text: "Business details" });
|
|
101
104
|
const publishBtn = el("button", { type: "button", class: "tidy-btn tidy-btn-primary", text: "Publish" });
|
|
102
|
-
|
|
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.append(statusText, bizBtn, publishBtn, doneBtn);
|
|
103
107
|
document.body.append(status);
|
|
104
108
|
const business = (config.records ?? []).find((r) => r.collection === "business");
|
|
105
109
|
bizBtn.hidden = !business;
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export const PLATFORM_ROUTES = [
|
|
|
25
25
|
{ pattern: "/_tidy/bootstrap", entrypoint: `${PACKAGE}/routes/bootstrap` },
|
|
26
26
|
{ pattern: "/_tidy/session", entrypoint: `${PACKAGE}/routes/session` },
|
|
27
27
|
{ pattern: "/_tidy/session/claim", entrypoint: `${PACKAGE}/routes/session-claim` },
|
|
28
|
+
{ pattern: "/_tidy/edit", entrypoint: `${PACKAGE}/routes/edit` },
|
|
28
29
|
{ pattern: "/_tidy/health", entrypoint: `${PACKAGE}/routes/health` },
|
|
29
30
|
{ pattern: "/_tidy/rotate-token", entrypoint: `${PACKAGE}/routes/rotate-token` },
|
|
30
31
|
{ pattern: "/_tidy/fill", entrypoint: `${PACKAGE}/routes/fill` },
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_tidy/edit?on=1|0&to=/path: switch on-site editing on or off for the
|
|
3
|
+
* signed-in editor and go back to the page. No bearer: the browser calls it
|
|
4
|
+
* from the editor's pill. Anyone without a session is sent back unchanged.
|
|
5
|
+
*/
|
|
6
|
+
import type { APIRoute } from "astro";
|
|
7
|
+
import { isSafeLocalPath } from "../lib/http";
|
|
8
|
+
import { methodNotAllowed } from "./_shared";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
const EDIT_COOKIE = "emdash-edit-mode";
|
|
13
|
+
|
|
14
|
+
export const GET: APIRoute = async (context) => {
|
|
15
|
+
const to = context.url.searchParams.get("to");
|
|
16
|
+
const back = isSafeLocalPath(to) ? to : "/";
|
|
17
|
+
const headers = { location: back, "cache-control": "no-store" };
|
|
18
|
+
const user = context.session ? await context.session.get("user") : null;
|
|
19
|
+
if (!user) return new Response(null, { status: 303, headers });
|
|
20
|
+
const secure = context.url.protocol === "https:";
|
|
21
|
+
if (context.url.searchParams.get("on") === "1") context.cookies.set(EDIT_COOKIE, "true", { path: "/", sameSite: "lax", secure });
|
|
22
|
+
else context.cookies.delete(EDIT_COOKIE, { path: "/" });
|
|
23
|
+
return new Response(null, { status: 303, headers });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ALL = methodNotAllowed("GET");
|
package/src/version.ts
CHANGED