@seamlet/astro 0.5.0 → 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/README.md +18 -1
- package/dist/bridge.js +46 -1
- package/dist/chunk-3BS5CE26.js +20 -0
- package/dist/index.js +8 -0
- package/dist/protocol.d.ts +8 -1
- package/dist/protocol.js +8 -0
- package/dist/routes/own-photos.d.ts +5 -0
- package/dist/routes/own-photos.js +19 -0
- package/dist/site.d.ts +8 -0
- package/dist/site.js +2 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -125,6 +125,22 @@ A photo may carry the point that must stay in view when the site crops it, chose
|
|
|
125
125
|
/>
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
A photo may also be one of the site's own files, named by an address of the site (`/menu/01.jpg`) that the site turns into its image when it renders. Give `defineSeamletSite` a `photos` function that finds that image, and the CMS's editor shows the photo in its field too:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const MENU_FILES = import.meta.glob<{ default: ImageMetadata }>(
|
|
132
|
+
"./assets/menu/*.jpg",
|
|
133
|
+
{ eager: true },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export default defineSeamletSite({
|
|
137
|
+
id: "zerno",
|
|
138
|
+
schema,
|
|
139
|
+
pages: { "/": HomePage },
|
|
140
|
+
photos: (src) => MENU_FILES[src.replace("/menu/", "./assets/menu/")]?.default,
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
128
144
|
## Presets
|
|
129
145
|
|
|
130
146
|
Sets of fields almost every site has, edited the same way everywhere; they are plain groups and collections, rendered as the site likes:
|
|
@@ -164,7 +180,7 @@ A reference to a record removed since is kept, with the issue `missing-record`:
|
|
|
164
180
|
|
|
165
181
|
## What the page hides
|
|
166
182
|
|
|
167
|
-
A field is edited where it is seen, so the editor opens what hides it when a person chooses it in the list of fields. A native `<details>` needs nothing: a click on its summary opens it in the editor too. A part the site hides and shows by itself (its own accordion, a step of a quiz, a dialog, a tab) gets the attribute `data-cms-reveal` (`REVEAL_ATTRIBUTE`). When a field inside it is chosen, the bridge sets `data-cms-revealed` on it and sends it the event `cms:reveal`, which bubbles; the site shows the part with CSS or with its own script:
|
|
183
|
+
A field is edited where it is seen, so the editor opens what hides it when a person chooses it in the list of fields. A native `<details>` needs nothing: a click on its summary opens it in the editor too. Neither do tabs, filters and toggles made of radio buttons and checkboxes and shown with CSS: a click on one or its label flips it in the editor, and the page keeps it flipped after every edit. A part the site hides and shows by itself (its own accordion, a step of a quiz, a dialog, a tab) gets the attribute `data-cms-reveal` (`REVEAL_ATTRIBUTE`). When a field inside it is chosen, the bridge sets `data-cms-revealed` on it and sends it the event `cms:reveal`, which bubbles; the site shows the part with CSS or with its own script:
|
|
168
184
|
|
|
169
185
|
```css
|
|
170
186
|
[data-cms-reveal][data-cms-revealed] {
|
|
@@ -178,6 +194,7 @@ What was opened stays open while the page is rendered anew after every edit.
|
|
|
178
194
|
|
|
179
195
|
- `/preview` and `/preview/<page>`: the preview the CMS's editor frames. `GET` shows the published content; `POST` with the content as JSON shows that content without saving it, which is how the editor shows every edit at once. Only the CMS may frame it (`Content-Security-Policy: frame-ancestors`), and it is never indexed or cached.
|
|
180
196
|
- The bridge on the preview: it tells the editor where the marked elements are and what a person does on the page, and types texts in place.
|
|
197
|
+
- `/seamlet-photos/<address>`: the site's own photo at an address its content gives it, found by `photos` of `defineSeamletSite`, for the CMS's editor.
|
|
181
198
|
- `/media/<site>/<file>`: the photos uploaded in the CMS, taken from it on request; a static build copies every uploaded photo its built pages refer to next to them, so a static host serves them too.
|
|
182
199
|
|
|
183
200
|
A site never renders content as raw HTML (`set:html`): the editor acts on what its preview says.
|
package/dist/bridge.js
CHANGED
|
@@ -62,6 +62,7 @@ function asElement(value) {
|
|
|
62
62
|
// src/bridge/page-reveals.ts
|
|
63
63
|
var DETAILS_SELECTOR = "details";
|
|
64
64
|
var REVEAL_SELECTOR = `[${REVEAL_ATTRIBUTE}]`;
|
|
65
|
+
var SWITCH_SELECTOR = 'input[type="radio"], input[type="checkbox"]';
|
|
65
66
|
function revealAround(element) {
|
|
66
67
|
for (let part = element; part !== null; ) {
|
|
67
68
|
if (isDetails(part)) part.open = true;
|
|
@@ -78,14 +79,28 @@ function toggleDetailsAt(target, isOnField) {
|
|
|
78
79
|
}
|
|
79
80
|
if (!isOnField) details.open = false;
|
|
80
81
|
}
|
|
82
|
+
function flipSwitchAt(target, isOnField) {
|
|
83
|
+
const control = switchAround(target);
|
|
84
|
+
if (control === void 0) return;
|
|
85
|
+
const isRadio = control.type === "radio";
|
|
86
|
+
const isFlipped = isRadio ? !control.checked : !isOnField;
|
|
87
|
+
if (!isFlipped) return;
|
|
88
|
+
control.checked = isRadio || !control.checked;
|
|
89
|
+
announceSwitch(control);
|
|
90
|
+
}
|
|
81
91
|
function revealedPartsOf(document2) {
|
|
82
92
|
const details = [...document2.querySelectorAll(DETAILS_SELECTOR)];
|
|
83
93
|
const parts = [...document2.querySelectorAll(REVEAL_SELECTOR)];
|
|
94
|
+
const switches = [...document2.querySelectorAll(SWITCH_SELECTOR)];
|
|
84
95
|
return {
|
|
85
96
|
openDetails: indexesWhere(details, (part) => isDetails(part) && part.open),
|
|
86
97
|
revealed: indexesWhere(
|
|
87
98
|
parts,
|
|
88
99
|
(part) => part.hasAttribute(REVEALED_ATTRIBUTE)
|
|
100
|
+
),
|
|
101
|
+
flippedSwitches: indexesWhere(
|
|
102
|
+
switches,
|
|
103
|
+
(control) => isSwitch(control) && control.checked !== control.defaultChecked
|
|
89
104
|
)
|
|
90
105
|
};
|
|
91
106
|
}
|
|
@@ -100,6 +115,26 @@ function restoreRevealedParts(document2, revealed) {
|
|
|
100
115
|
const part = parts[index];
|
|
101
116
|
if (part !== void 0) reveal(part);
|
|
102
117
|
}
|
|
118
|
+
restoreSwitches(document2, revealed.flippedSwitches);
|
|
119
|
+
}
|
|
120
|
+
function restoreSwitches(document2, flipped) {
|
|
121
|
+
const switches = [...document2.querySelectorAll(SWITCH_SELECTOR)];
|
|
122
|
+
for (const index of flipped) {
|
|
123
|
+
const control = switches[index];
|
|
124
|
+
if (control === void 0 || !isSwitch(control)) continue;
|
|
125
|
+
control.checked = !control.defaultChecked;
|
|
126
|
+
announceSwitch(control);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function announceSwitch(control) {
|
|
130
|
+
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
131
|
+
control.dispatchEvent(new Event("change", { bubbles: true }));
|
|
132
|
+
}
|
|
133
|
+
function switchAround(target) {
|
|
134
|
+
if (!isElement(target)) return void 0;
|
|
135
|
+
const label = target.closest("label");
|
|
136
|
+
const control = isLabel(label) ? label.control : target;
|
|
137
|
+
return control !== null && isSwitch(control) ? control : void 0;
|
|
103
138
|
}
|
|
104
139
|
function reveal(part) {
|
|
105
140
|
if (part.hasAttribute(REVEALED_ATTRIBUTE)) return;
|
|
@@ -120,6 +155,12 @@ function indexesWhere(elements, isPicked) {
|
|
|
120
155
|
function isDetails(element) {
|
|
121
156
|
return element.localName === "details";
|
|
122
157
|
}
|
|
158
|
+
function isLabel(element) {
|
|
159
|
+
return element?.localName === "label";
|
|
160
|
+
}
|
|
161
|
+
function isSwitch(element) {
|
|
162
|
+
return element.matches(SWITCH_SELECTOR);
|
|
163
|
+
}
|
|
123
164
|
function isElement(value) {
|
|
124
165
|
return value !== null && "nodeType" in value && value.nodeType === Node.ELEMENT_NODE;
|
|
125
166
|
}
|
|
@@ -147,7 +188,9 @@ function listenToPage(document2, listeners) {
|
|
|
147
188
|
return;
|
|
148
189
|
}
|
|
149
190
|
const marked = markedElementAround(event.target);
|
|
150
|
-
|
|
191
|
+
const isOnField = marked !== void 0;
|
|
192
|
+
toggleDetailsAt(event.target, isOnField);
|
|
193
|
+
flipSwitchAt(event.target, isOnField);
|
|
151
194
|
listeners.onClick(
|
|
152
195
|
marked === void 0 ? void 0 : { ...marked, point: { x: event.clientX, y: event.clientY } }
|
|
153
196
|
);
|
|
@@ -194,6 +237,8 @@ function listenToPage(document2, listeners) {
|
|
|
194
237
|
["scroll", listeners.onLayoutChange],
|
|
195
238
|
// An opened or closed `<details>` moves what is below it; toggle does not bubble.
|
|
196
239
|
["toggle", listeners.onLayoutChange],
|
|
240
|
+
// A flipped switch shows and hides parts of the page.
|
|
241
|
+
["change", listeners.onLayoutChange],
|
|
197
242
|
// Images load after the page is shown and move what is below them; load does not bubble.
|
|
198
243
|
["load", listeners.onLayoutChange]
|
|
199
244
|
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/own-photos.ts
|
|
2
|
+
var OWN_PHOTOS_PATH = "/seamlet-photos";
|
|
3
|
+
var UPLOADS_PATH = "/media/";
|
|
4
|
+
var PARENT_SEGMENT = "..";
|
|
5
|
+
function isOwnPhotoSrc(src) {
|
|
6
|
+
const isOnSite = src.startsWith("/") && !src.startsWith("//");
|
|
7
|
+
const isUpload = src.startsWith(UPLOADS_PATH);
|
|
8
|
+
const isPlainPath = !/[?#\\]/u.test(src);
|
|
9
|
+
const climbsUp = src.split("/").includes(PARENT_SEGMENT);
|
|
10
|
+
return isOnSite && !isUpload && isPlainPath && !climbsUp;
|
|
11
|
+
}
|
|
12
|
+
function ownPhotoPath(src) {
|
|
13
|
+
return isOwnPhotoSrc(src) ? `${OWN_PHOTOS_PATH}${src}` : void 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
OWN_PHOTOS_PATH,
|
|
18
|
+
isOwnPhotoSrc,
|
|
19
|
+
ownPhotoPath
|
|
20
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OWN_PHOTOS_PATH
|
|
3
|
+
} from "./chunk-3BS5CE26.js";
|
|
1
4
|
import {
|
|
2
5
|
isUploadedPhoto
|
|
3
6
|
} from "./chunk-P2OQOZBC.js";
|
|
@@ -89,6 +92,11 @@ function seamlet(options) {
|
|
|
89
92
|
entrypoint: "@seamlet/astro/routes/media",
|
|
90
93
|
prerender: false
|
|
91
94
|
});
|
|
95
|
+
injectRoute({
|
|
96
|
+
pattern: `${OWN_PHOTOS_PATH}/[...path]`,
|
|
97
|
+
entrypoint: "@seamlet/astro/routes/own-photos",
|
|
98
|
+
prerender: false
|
|
99
|
+
});
|
|
92
100
|
},
|
|
93
101
|
// A static host has no route to take the uploaded photos from the CMS on request: the photos
|
|
94
102
|
// the built pages refer to are copied next to them. Pages rendered on request take them
|
package/dist/protocol.d.ts
CHANGED
|
@@ -16,4 +16,11 @@ declare function imageFieldAt(schema: Fields, path: string | undefined): {
|
|
|
16
16
|
*/
|
|
17
17
|
declare function closestMarkedPath(path: FieldPath, isMarked: (path: string) => boolean): string | undefined;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** Where a site hands out its own photos: `/seamlet-photos/menu/cafe/01.jpg`. */
|
|
20
|
+
declare const OWN_PHOTOS_PATH = "/seamlet-photos";
|
|
21
|
+
/** An address of the site itself that is not an upload, and that cannot climb out of the site. */
|
|
22
|
+
declare function isOwnPhotoSrc(src: string): boolean;
|
|
23
|
+
/** Where the site hands out its own photo; undefined for an upload or another site's address. */
|
|
24
|
+
declare function ownPhotoPath(src: string): string | undefined;
|
|
25
|
+
|
|
26
|
+
export { OWN_PHOTOS_PATH, carriesFiles, closestMarkedPath, imageFieldAt, isOwnPhotoSrc, ownPhotoPath };
|
package/dist/protocol.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OWN_PHOTOS_PATH,
|
|
3
|
+
isOwnPhotoSrc,
|
|
4
|
+
ownPhotoPath
|
|
5
|
+
} from "./chunk-3BS5CE26.js";
|
|
1
6
|
import {
|
|
2
7
|
PROTOCOL_VERSION,
|
|
3
8
|
carriesFiles,
|
|
@@ -9,10 +14,13 @@ import {
|
|
|
9
14
|
} from "./chunk-XCBYYKWR.js";
|
|
10
15
|
import "./chunk-GLXC5OLC.js";
|
|
11
16
|
export {
|
|
17
|
+
OWN_PHOTOS_PATH,
|
|
12
18
|
PROTOCOL_VERSION,
|
|
13
19
|
carriesFiles,
|
|
14
20
|
closestMarkedPath,
|
|
15
21
|
imageFieldAt,
|
|
22
|
+
isOwnPhotoSrc,
|
|
23
|
+
ownPhotoPath,
|
|
16
24
|
readEditorMessage,
|
|
17
25
|
readPageMessage,
|
|
18
26
|
shortcutOf
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isOwnPhotoSrc
|
|
3
|
+
} from "../chunk-3BS5CE26.js";
|
|
4
|
+
|
|
5
|
+
// src/routes/own-photos.ts
|
|
6
|
+
import site from "virtual:seamlet/site";
|
|
7
|
+
var NOT_FOUND = 404;
|
|
8
|
+
var FOUND = 302;
|
|
9
|
+
var GET = ({ params, redirect }) => {
|
|
10
|
+
const src = `/${params["path"] ?? ""}`;
|
|
11
|
+
const photo = isOwnPhotoSrc(src) ? site.ownPhotoAt(src) : void 0;
|
|
12
|
+
if (photo === void 0) {
|
|
13
|
+
return new Response(null, { status: NOT_FOUND });
|
|
14
|
+
}
|
|
15
|
+
return redirect(photo.src, FOUND);
|
|
16
|
+
};
|
|
17
|
+
export {
|
|
18
|
+
GET
|
|
19
|
+
};
|
package/dist/site.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ImageMetadata } from 'astro';
|
|
1
2
|
import { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
2
3
|
import { F as Fields, C as ContentOf } from './content-C_WpiWBm.js';
|
|
3
4
|
|
|
@@ -10,6 +11,11 @@ interface SeamletSiteDefinition<F extends Fields> {
|
|
|
10
11
|
* as their `content` prop. The CMS lists the same addresses for its editor.
|
|
11
12
|
*/
|
|
12
13
|
readonly pages: Readonly<Record<string, AstroComponentFactory>>;
|
|
14
|
+
/**
|
|
15
|
+
* The site's own photo at the address its content gives it (`/menu/cafe/01.jpg`), so the CMS's
|
|
16
|
+
* editor can show it in the photo's field. A site without own photos leaves it out.
|
|
17
|
+
*/
|
|
18
|
+
readonly photos?: (src: string) => ImageMetadata | undefined;
|
|
13
19
|
}
|
|
14
20
|
interface SeamletSite<F extends Fields = Fields> {
|
|
15
21
|
readonly id: string;
|
|
@@ -18,6 +24,8 @@ interface SeamletSite<F extends Fields = Fields> {
|
|
|
18
24
|
readonly pageAt: (rest: string | undefined) => AstroComponentFactory | undefined;
|
|
19
25
|
/** What the CMS published: the site's initial content until the first publication. */
|
|
20
26
|
readonly loadPublishedContent: () => Promise<ContentOf<F>>;
|
|
27
|
+
/** The site's own photo at an address its content gives it, if it has one there. */
|
|
28
|
+
readonly ownPhotoAt: (src: string) => ImageMetadata | undefined;
|
|
21
29
|
}
|
|
22
30
|
/** Describes the site for the integration: `src/seamlet.ts` exports it as its default. */
|
|
23
31
|
declare function defineSeamletSite<F extends Fields>(definition: SeamletSiteDefinition<F>): SeamletSite<F>;
|
package/dist/site.js
CHANGED
|
@@ -14,7 +14,8 @@ function defineSeamletSite(definition) {
|
|
|
14
14
|
loadPublishedContent: async () => {
|
|
15
15
|
const published = await fetchPublished(definition.id);
|
|
16
16
|
return parseContent(definition.schema, published).content;
|
|
17
|
-
}
|
|
17
|
+
},
|
|
18
|
+
ownPhotoAt: (src) => definition.photos?.(src)
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
async function fetchPublished(siteId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seamlet/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Connects an Astro site to the Seamlet CMS: the schema of its content, the preview the editor frames, and its published content.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"./routes/media": {
|
|
43
43
|
"types": "./dist/routes/media.d.ts",
|
|
44
44
|
"default": "./dist/routes/media.js"
|
|
45
|
+
},
|
|
46
|
+
"./routes/own-photos": {
|
|
47
|
+
"types": "./dist/routes/own-photos.d.ts",
|
|
48
|
+
"default": "./dist/routes/own-photos.js"
|
|
45
49
|
}
|
|
46
50
|
},
|
|
47
51
|
"files": [
|