@seamlet/astro 0.1.1 → 0.3.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 +78 -0
- package/dist/bridge.d.ts +3 -3
- package/dist/bridge.js +85 -9
- package/dist/{chunk-EC42AIQC.js → chunk-AFTVM755.js} +285 -5
- package/dist/{chunk-HRAL4X6W.js → chunk-N2UE3EQU.js} +1 -1
- package/dist/content-GL5k9Mch.d.ts +163 -0
- package/dist/{messages-DIMwt3Kn.d.ts → messages-CJ8vKPs9.d.ts} +2 -2
- package/dist/{parse-content-DF2dL2FG.d.ts → parse-content-CVU-pHrH.d.ts} +17 -2
- package/dist/protocol.d.ts +3 -3
- package/dist/protocol.js +2 -2
- package/dist/schema.d.ts +37 -6
- package/dist/schema.js +35 -3
- package/dist/site.d.ts +1 -1
- package/dist/site.js +1 -1
- package/package.json +1 -1
- package/dist/content-DKVM1CUQ.d.ts +0 -90
package/README.md
CHANGED
|
@@ -76,6 +76,84 @@ Connects an [Astro](https://astro.build) site to the Seamlet CMS ([source](https
|
|
|
76
76
|
|
|
77
77
|
The site's pages take their published content with `site.loadPublishedContent()`: at build time for a static site, on request otherwise.
|
|
78
78
|
|
|
79
|
+
## Fields
|
|
80
|
+
|
|
81
|
+
| Builder | Holds | Empty |
|
|
82
|
+
| ----------------------------------------- | ----------------------------------------------------------------------- | ---------------- |
|
|
83
|
+
| `text({ label, multiline?, maxLength? })` | a string | `""` |
|
|
84
|
+
| `number({ label, min?, max? })` | a number | `null` |
|
|
85
|
+
| `date({ label })` | `YYYY-MM-DD` | `null` |
|
|
86
|
+
| `time({ label })` | `HH:MM`, 00:00 to 23:59 | `null` |
|
|
87
|
+
| `image({ label })` | `{ src, alt }` | `null` |
|
|
88
|
+
| `link({ label })` | `{ text, href }`: a page of the site, another site, a phone or an email | `null` |
|
|
89
|
+
| `url({ label })` | a web address without a text: a booking widget, a map | `null` |
|
|
90
|
+
| `toggle({ label })` | `true` or `false` | `false` |
|
|
91
|
+
| `choice({ label, options })` | the `value` of one option | `null` |
|
|
92
|
+
| `choices({ label, options })` | the `value`s of any number of options | `[]` |
|
|
93
|
+
| `reference({ label, collection })` | the `id` of a record of a collection of the schema | `null` |
|
|
94
|
+
| `references({ label, collection })` | the `id`s of any number of such records | `[]` |
|
|
95
|
+
| `group({ label, fields })` | named fields shown together | each field empty |
|
|
96
|
+
| `collection({ label, item, itemTitle? })` | a list of records of the same fields | `[]` |
|
|
97
|
+
|
|
98
|
+
Options are `{ value, label }`: the site reads `value`, the editor shows `label`, so a label can change without touching the site. The content has their exact type:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const level = choice({
|
|
102
|
+
label: "Нагрузка",
|
|
103
|
+
options: [
|
|
104
|
+
{ value: "soft", label: "Мягко" },
|
|
105
|
+
{ value: "active", label: "Активно" },
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
// ContentOf gives "soft" | "active" | null
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
To let the owner hide a record without removing it, give the record a toggle and leave hidden records out:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
item: { name: text({ label: "Название" }), isHidden: toggle({ label: "Скрыть с сайта" }) }
|
|
115
|
+
// content.practices.filter((practice) => !practice.isHidden)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Records and references
|
|
119
|
+
|
|
120
|
+
Every record of a collection has an `id` of its own, which never changes when records move, so a record has no field named `id`. The CMS gives ids to records that have none (`withRecordIds`), so a site's initial content may leave them out, unless something refers to those records.
|
|
121
|
+
|
|
122
|
+
A reference points at a record of another collection by its id: the teacher of a practice is chosen from the teachers, so renaming the teacher changes every practice at once. `collection` is where the collection is, its name or a path through groups:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const schema = defineSchema({
|
|
126
|
+
teachers: collection({
|
|
127
|
+
label: "Преподаватели",
|
|
128
|
+
itemTitle: "name",
|
|
129
|
+
item: { name: text({ label: "Имя" }) },
|
|
130
|
+
}),
|
|
131
|
+
practices: collection({
|
|
132
|
+
label: "Практики",
|
|
133
|
+
itemTitle: "name",
|
|
134
|
+
item: {
|
|
135
|
+
name: text({ label: "Название" }),
|
|
136
|
+
teacher: reference({ label: "Ведёт", collection: "teachers" }),
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
// findRecord(content.teachers, practice.teacher)?.name
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A reference to a record removed since is kept, with the issue `missing-record`: the content is not saved until the person picks another one or clears it.
|
|
144
|
+
|
|
145
|
+
## What the page hides
|
|
146
|
+
|
|
147
|
+
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:
|
|
148
|
+
|
|
149
|
+
```css
|
|
150
|
+
[data-cms-reveal][data-cms-revealed] {
|
|
151
|
+
display: block;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
What was opened stays open while the page is rendered anew after every edit.
|
|
156
|
+
|
|
79
157
|
## What the integration adds
|
|
80
158
|
|
|
81
159
|
- `/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.
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { F as Fields } from './content-
|
|
2
|
-
import { P as PageMessage, E as EditorMessage } from './messages-
|
|
3
|
-
import './parse-content-
|
|
1
|
+
import { F as Fields } from './content-GL5k9Mch.js';
|
|
2
|
+
import { P as PageMessage, E as EditorMessage } from './messages-CJ8vKPs9.js';
|
|
3
|
+
import './parse-content-CVU-pHrH.js';
|
|
4
4
|
|
|
5
5
|
interface BridgeOptions {
|
|
6
6
|
/** The origin of the CMS whose editor may frame the page, such as `https://cms.example.com`. */
|
package/dist/bridge.js
CHANGED
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
imageFieldAt,
|
|
6
6
|
readEditorMessage,
|
|
7
7
|
shortcutOf
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-N2UE3EQU.js";
|
|
9
9
|
import {
|
|
10
|
-
FIELD_PATH_ATTRIBUTE
|
|
11
|
-
|
|
10
|
+
FIELD_PATH_ATTRIBUTE,
|
|
11
|
+
REVEALED_ATTRIBUTE,
|
|
12
|
+
REVEAL_ATTRIBUTE,
|
|
13
|
+
REVEAL_EVENT
|
|
14
|
+
} from "./chunk-AFTVM755.js";
|
|
12
15
|
|
|
13
16
|
// src/bridge/page-markers.ts
|
|
14
17
|
var MARKER_SELECTOR = `[${FIELD_PATH_ATTRIBUTE}]`;
|
|
@@ -52,8 +55,73 @@ function isHtmlElement(value) {
|
|
|
52
55
|
return asElement(value)?.namespaceURI === HTML_NAMESPACE;
|
|
53
56
|
}
|
|
54
57
|
function asElement(value) {
|
|
55
|
-
const
|
|
56
|
-
return
|
|
58
|
+
const isElement2 = typeof value === "object" && value !== null && "nodeType" in value && value.nodeType === Node.ELEMENT_NODE;
|
|
59
|
+
return isElement2 ? value : void 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/bridge/page-reveals.ts
|
|
63
|
+
var DETAILS_SELECTOR = "details";
|
|
64
|
+
var REVEAL_SELECTOR = `[${REVEAL_ATTRIBUTE}]`;
|
|
65
|
+
function revealAround(element) {
|
|
66
|
+
for (let part = element; part !== null; ) {
|
|
67
|
+
if (isDetails(part)) part.open = true;
|
|
68
|
+
if (part.hasAttribute(REVEAL_ATTRIBUTE)) reveal(part);
|
|
69
|
+
part = part.parentElement;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function toggleDetailsAt(target, isOnField) {
|
|
73
|
+
const details = detailsOfSummaryAround(target);
|
|
74
|
+
if (details === void 0) return;
|
|
75
|
+
if (!details.open) {
|
|
76
|
+
details.open = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!isOnField) details.open = false;
|
|
80
|
+
}
|
|
81
|
+
function revealedPartsOf(document2) {
|
|
82
|
+
const details = [...document2.querySelectorAll(DETAILS_SELECTOR)];
|
|
83
|
+
const parts = [...document2.querySelectorAll(REVEAL_SELECTOR)];
|
|
84
|
+
return {
|
|
85
|
+
openDetails: indexesWhere(details, (part) => isDetails(part) && part.open),
|
|
86
|
+
revealed: indexesWhere(
|
|
87
|
+
parts,
|
|
88
|
+
(part) => part.hasAttribute(REVEALED_ATTRIBUTE)
|
|
89
|
+
)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function restoreRevealedParts(document2, revealed) {
|
|
93
|
+
const details = [...document2.querySelectorAll(DETAILS_SELECTOR)];
|
|
94
|
+
const parts = [...document2.querySelectorAll(REVEAL_SELECTOR)];
|
|
95
|
+
for (const index of revealed.openDetails) {
|
|
96
|
+
const part = details[index];
|
|
97
|
+
if (part !== void 0 && isDetails(part)) part.open = true;
|
|
98
|
+
}
|
|
99
|
+
for (const index of revealed.revealed) {
|
|
100
|
+
const part = parts[index];
|
|
101
|
+
if (part !== void 0) reveal(part);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function reveal(part) {
|
|
105
|
+
if (part.hasAttribute(REVEALED_ATTRIBUTE)) return;
|
|
106
|
+
part.setAttribute(REVEALED_ATTRIBUTE, "");
|
|
107
|
+
part.dispatchEvent(new CustomEvent(REVEAL_EVENT, { bubbles: true }));
|
|
108
|
+
}
|
|
109
|
+
function detailsOfSummaryAround(target) {
|
|
110
|
+
const summary = isElement(target) ? target.closest("summary") : null;
|
|
111
|
+
const details = summary?.parentElement;
|
|
112
|
+
const isSummaryOfDetails = details !== null && details !== void 0 && isDetails(details);
|
|
113
|
+
return isSummaryOfDetails ? details : void 0;
|
|
114
|
+
}
|
|
115
|
+
function indexesWhere(elements, isPicked) {
|
|
116
|
+
return elements.flatMap(
|
|
117
|
+
(element, index) => isPicked(element) ? [index] : []
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function isDetails(element) {
|
|
121
|
+
return element.localName === "details";
|
|
122
|
+
}
|
|
123
|
+
function isElement(value) {
|
|
124
|
+
return value !== null && "nodeType" in value && value.nodeType === Node.ELEMENT_NODE;
|
|
57
125
|
}
|
|
58
126
|
|
|
59
127
|
// src/bridge/page-events.ts
|
|
@@ -79,6 +147,7 @@ function listenToPage(document2, listeners) {
|
|
|
79
147
|
return;
|
|
80
148
|
}
|
|
81
149
|
const marked = markedElementAround(event.target);
|
|
150
|
+
toggleDetailsAt(event.target, marked !== void 0);
|
|
82
151
|
listeners.onClick(
|
|
83
152
|
marked === void 0 ? void 0 : { ...marked, point: { x: event.clientX, y: event.clientY } }
|
|
84
153
|
);
|
|
@@ -123,6 +192,8 @@ function listenToPage(document2, listeners) {
|
|
|
123
192
|
["drop", dropFile],
|
|
124
193
|
["dragleave", leaveWithFile],
|
|
125
194
|
["scroll", listeners.onLayoutChange],
|
|
195
|
+
// An opened or closed `<details>` moves what is below it; toggle does not bubble.
|
|
196
|
+
["toggle", listeners.onLayoutChange],
|
|
126
197
|
// Images load after the page is shown and move what is below them; load does not bubble.
|
|
127
198
|
["load", listeners.onLayoutChange]
|
|
128
199
|
];
|
|
@@ -255,8 +326,10 @@ async function fetchRenderedPage(url, content, signal) {
|
|
|
255
326
|
}
|
|
256
327
|
function replacePageBody(document2, html) {
|
|
257
328
|
const rendered = new DOMParser().parseFromString(html, "text/html");
|
|
329
|
+
const revealed = revealedPartsOf(document2);
|
|
258
330
|
document2.title = rendered.title;
|
|
259
331
|
document2.body.replaceWith(document2.importNode(rendered.body, true));
|
|
332
|
+
restoreRevealedParts(document2, revealed);
|
|
260
333
|
}
|
|
261
334
|
|
|
262
335
|
// src/bridge/element-text.ts
|
|
@@ -510,10 +583,7 @@ function connectPage(document2, schema, send) {
|
|
|
510
583
|
typing.stop();
|
|
511
584
|
break;
|
|
512
585
|
case "scroll-to":
|
|
513
|
-
|
|
514
|
-
block: "nearest",
|
|
515
|
-
behavior: "smooth"
|
|
516
|
-
});
|
|
586
|
+
scrollToField(document2, message.path);
|
|
517
587
|
break;
|
|
518
588
|
}
|
|
519
589
|
};
|
|
@@ -527,6 +597,12 @@ function connectPage(document2, schema, send) {
|
|
|
527
597
|
}
|
|
528
598
|
};
|
|
529
599
|
}
|
|
600
|
+
function scrollToField(document2, path) {
|
|
601
|
+
const element = findClosestMarkedElement(document2, path);
|
|
602
|
+
if (element === void 0) return;
|
|
603
|
+
revealAround(element);
|
|
604
|
+
element.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
605
|
+
}
|
|
530
606
|
function editorKeyOf(event) {
|
|
531
607
|
if (event.key === "Escape") return "escape";
|
|
532
608
|
return shortcutOf(event);
|
|
@@ -17,6 +17,27 @@ function image(options) {
|
|
|
17
17
|
function link(options) {
|
|
18
18
|
return { kind: "link", ...options };
|
|
19
19
|
}
|
|
20
|
+
function toggle(options) {
|
|
21
|
+
return { kind: "toggle", ...options };
|
|
22
|
+
}
|
|
23
|
+
function choice(options) {
|
|
24
|
+
return { kind: "choice", ...options };
|
|
25
|
+
}
|
|
26
|
+
function choices(options) {
|
|
27
|
+
return { kind: "choices", ...options };
|
|
28
|
+
}
|
|
29
|
+
function url(options) {
|
|
30
|
+
return { kind: "url", ...options };
|
|
31
|
+
}
|
|
32
|
+
function time(options) {
|
|
33
|
+
return { kind: "time", ...options };
|
|
34
|
+
}
|
|
35
|
+
function reference(options) {
|
|
36
|
+
return { kind: "reference", ...options };
|
|
37
|
+
}
|
|
38
|
+
function references(options) {
|
|
39
|
+
return { kind: "references", ...options };
|
|
40
|
+
}
|
|
20
41
|
function group(options) {
|
|
21
42
|
return { kind: "group", ...options };
|
|
22
43
|
}
|
|
@@ -29,10 +50,19 @@ function defaultValue(field) {
|
|
|
29
50
|
switch (field.kind) {
|
|
30
51
|
case "text":
|
|
31
52
|
return "";
|
|
53
|
+
case "toggle":
|
|
54
|
+
return false;
|
|
55
|
+
case "choices":
|
|
56
|
+
case "references":
|
|
57
|
+
return [];
|
|
32
58
|
case "number":
|
|
33
59
|
case "date":
|
|
34
60
|
case "image":
|
|
35
61
|
case "link":
|
|
62
|
+
case "choice":
|
|
63
|
+
case "url":
|
|
64
|
+
case "time":
|
|
65
|
+
case "reference":
|
|
36
66
|
return null;
|
|
37
67
|
case "group":
|
|
38
68
|
return defaultContent(field.fields);
|
|
@@ -117,8 +147,135 @@ function withoutPrefix(address, prefix) {
|
|
|
117
147
|
return hasPrefix ? address.slice(prefix.length) : void 0;
|
|
118
148
|
}
|
|
119
149
|
|
|
150
|
+
// src/schema/record-ids.ts
|
|
151
|
+
var RECORD_ID_KEY = "id";
|
|
152
|
+
var RECORD_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/u;
|
|
153
|
+
function isRecordId(value) {
|
|
154
|
+
return typeof value === "string" && RECORD_ID_PATTERN.test(value);
|
|
155
|
+
}
|
|
156
|
+
function newRecord(collection2, id) {
|
|
157
|
+
return { ...defaultContent(collection2.item), [RECORD_ID_KEY]: id };
|
|
158
|
+
}
|
|
159
|
+
function withRecordIds(schema, content, newId) {
|
|
160
|
+
return withIdsInFields(schema, content, newId);
|
|
161
|
+
}
|
|
162
|
+
var COLLECTION_PATH_SEPARATOR = ".";
|
|
163
|
+
function collectionAt(schema, content, path) {
|
|
164
|
+
return collectionIn(schema, content, path.split(COLLECTION_PATH_SEPARATOR));
|
|
165
|
+
}
|
|
166
|
+
function collectionIn(fields, content, names) {
|
|
167
|
+
const [name, ...rest] = names;
|
|
168
|
+
const field = name !== void 0 && Object.hasOwn(fields, name) ? fields[name] : void 0;
|
|
169
|
+
if (name === void 0 || field === void 0) return void 0;
|
|
170
|
+
const value = content[name];
|
|
171
|
+
if (rest.length === 0 && field.kind === "collection") {
|
|
172
|
+
const records = Array.isArray(value) ? value.filter(isContent) : [];
|
|
173
|
+
return { collection: field, records };
|
|
174
|
+
}
|
|
175
|
+
if (rest.length > 0 && field.kind === "group" && isContent(value)) {
|
|
176
|
+
return collectionIn(field.fields, value, rest);
|
|
177
|
+
}
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
function findRecord(records, id) {
|
|
181
|
+
if (id === null) return void 0;
|
|
182
|
+
return records.find((record) => record.id === id);
|
|
183
|
+
}
|
|
184
|
+
function withIdsInFields(fields, content, newId) {
|
|
185
|
+
const entries = Object.entries(content).map(([name, value]) => {
|
|
186
|
+
const field = Object.hasOwn(fields, name) ? fields[name] : void 0;
|
|
187
|
+
return [name, field === void 0 ? value : withIdsIn(field, value, newId)];
|
|
188
|
+
});
|
|
189
|
+
return Object.fromEntries(entries);
|
|
190
|
+
}
|
|
191
|
+
function withIdsIn(field, value, newId) {
|
|
192
|
+
if (field.kind === "group" && isContent(value)) {
|
|
193
|
+
return withIdsInFields(field.fields, value, newId);
|
|
194
|
+
}
|
|
195
|
+
if (field.kind === "collection" && Array.isArray(value)) {
|
|
196
|
+
return withIdsInRecords(field, value.filter(isContent), newId);
|
|
197
|
+
}
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
function withIdsInRecords(collection2, records, newId) {
|
|
201
|
+
const taken = /* @__PURE__ */ new Set();
|
|
202
|
+
return records.map((record) => {
|
|
203
|
+
const own = record[RECORD_ID_KEY];
|
|
204
|
+
const isOwnFree = isRecordId(own) && !taken.has(own);
|
|
205
|
+
const id = isOwnFree ? own : newId();
|
|
206
|
+
taken.add(id);
|
|
207
|
+
const fields = withIdsInFields(collection2.item, record, newId);
|
|
208
|
+
return { ...fields, [RECORD_ID_KEY]: id };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function isContent(value) {
|
|
212
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/schema/reference-checks.ts
|
|
216
|
+
function missingRecordIssues(schema, content) {
|
|
217
|
+
const idsOf = (collection2) => recordIdsAt(schema, content, collection2);
|
|
218
|
+
return issuesInFields(schema, content, [], idsOf);
|
|
219
|
+
}
|
|
220
|
+
function issuesInFields(fields, content, path, idsOf) {
|
|
221
|
+
return Object.entries(fields).flatMap(
|
|
222
|
+
([name, field]) => issuesInField(field, content[name], [...path, name], idsOf)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
function issuesInField(field, value, path, idsOf) {
|
|
226
|
+
switch (field.kind) {
|
|
227
|
+
case "reference":
|
|
228
|
+
return missingIds(
|
|
229
|
+
typeof value === "string" ? [value] : [],
|
|
230
|
+
field,
|
|
231
|
+
path,
|
|
232
|
+
idsOf
|
|
233
|
+
);
|
|
234
|
+
case "references":
|
|
235
|
+
return missingIds(stringsOf(value), field, path, idsOf);
|
|
236
|
+
case "group":
|
|
237
|
+
return isContent2(value) ? issuesInFields(field.fields, value, path, idsOf) : [];
|
|
238
|
+
case "collection":
|
|
239
|
+
return recordsOf(value).flatMap(
|
|
240
|
+
(record, index) => issuesInFields(field.item, record, [...path, index], idsOf)
|
|
241
|
+
);
|
|
242
|
+
case "text":
|
|
243
|
+
case "number":
|
|
244
|
+
case "date":
|
|
245
|
+
case "image":
|
|
246
|
+
case "link":
|
|
247
|
+
case "toggle":
|
|
248
|
+
case "choice":
|
|
249
|
+
case "choices":
|
|
250
|
+
case "url":
|
|
251
|
+
case "time":
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function missingIds(ids, field, path, idsOf) {
|
|
256
|
+
const known = idsOf(field.collection);
|
|
257
|
+
const missing = ids.filter((id) => !known.has(id));
|
|
258
|
+
return missing.map((id) => ({ path, code: "missing-record", id }));
|
|
259
|
+
}
|
|
260
|
+
function recordIdsAt(schema, content, path) {
|
|
261
|
+
const records = collectionAt(schema, content, path)?.records ?? [];
|
|
262
|
+
const ids = records.map((record) => record[RECORD_ID_KEY]);
|
|
263
|
+
return new Set(ids.filter((id) => typeof id === "string"));
|
|
264
|
+
}
|
|
265
|
+
function recordsOf(value) {
|
|
266
|
+
return Array.isArray(value) ? value.filter(isContent2) : [];
|
|
267
|
+
}
|
|
268
|
+
function stringsOf(value) {
|
|
269
|
+
const list = Array.isArray(value) ? value : [];
|
|
270
|
+
return list.filter((id) => typeof id === "string");
|
|
271
|
+
}
|
|
272
|
+
function isContent2(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
|
|
120
276
|
// src/schema/parse-content.ts
|
|
121
277
|
var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
|
|
278
|
+
var TIME_PATTERN = /^(?:[01]\d|2[0-3]):[0-5]\d$/u;
|
|
122
279
|
var GRAPHEMES = new Intl.Segmenter("ru-RU", { granularity: "grapheme" });
|
|
123
280
|
function formatFieldPath(path) {
|
|
124
281
|
return path.join(".");
|
|
@@ -128,7 +285,11 @@ function countCharacters(text2) {
|
|
|
128
285
|
}
|
|
129
286
|
function parseContent(schema, input) {
|
|
130
287
|
const parsed = parseFields(schema, input, []);
|
|
131
|
-
|
|
288
|
+
const missingRecords = missingRecordIssues(schema, parsed.value);
|
|
289
|
+
return {
|
|
290
|
+
content: parsed.value,
|
|
291
|
+
issues: [...parsed.issues, ...missingRecords]
|
|
292
|
+
};
|
|
132
293
|
}
|
|
133
294
|
function parseFields(fields, input, path) {
|
|
134
295
|
if (!isRecord(input)) {
|
|
@@ -159,6 +320,20 @@ function parseField(field, input, path) {
|
|
|
159
320
|
return parseImage(field, input, path);
|
|
160
321
|
case "link":
|
|
161
322
|
return parseLink(field, input, path);
|
|
323
|
+
case "toggle":
|
|
324
|
+
return parseToggle(field, input, path);
|
|
325
|
+
case "choice":
|
|
326
|
+
return parseChoice(field, input, path);
|
|
327
|
+
case "choices":
|
|
328
|
+
return parseChoices(field, input, path);
|
|
329
|
+
case "url":
|
|
330
|
+
return parseUrl(field, input, path);
|
|
331
|
+
case "time":
|
|
332
|
+
return parseTime(field, input, path);
|
|
333
|
+
case "reference":
|
|
334
|
+
return parseReference(field, input, path);
|
|
335
|
+
case "references":
|
|
336
|
+
return parseReferences(field, input, path);
|
|
162
337
|
case "group":
|
|
163
338
|
return parseFields(field.fields, input, path);
|
|
164
339
|
case "collection":
|
|
@@ -249,25 +424,109 @@ function parseLink(field, input, path) {
|
|
|
249
424
|
}
|
|
250
425
|
return valid(link2);
|
|
251
426
|
}
|
|
427
|
+
function parseToggle(field, input, path) {
|
|
428
|
+
if (typeof input !== "boolean") {
|
|
429
|
+
return invalidType(defaultValue(field), path);
|
|
430
|
+
}
|
|
431
|
+
return valid(input);
|
|
432
|
+
}
|
|
433
|
+
function parseChoice(field, input, path) {
|
|
434
|
+
if (input === null) {
|
|
435
|
+
return valid(null);
|
|
436
|
+
}
|
|
437
|
+
if (typeof input !== "string") {
|
|
438
|
+
return invalidType(defaultValue(field), path);
|
|
439
|
+
}
|
|
440
|
+
const issues = unknownChoiceIssues(field, [input], path);
|
|
441
|
+
return { value: input, issues };
|
|
442
|
+
}
|
|
443
|
+
function parseChoices(field, input, path) {
|
|
444
|
+
const isListOfStrings = Array.isArray(input) && input.every((value) => typeof value === "string");
|
|
445
|
+
if (!isListOfStrings) {
|
|
446
|
+
return invalidType(defaultValue(field), path);
|
|
447
|
+
}
|
|
448
|
+
const picked = [...new Set(input)];
|
|
449
|
+
const issues = unknownChoiceIssues(field, picked, path);
|
|
450
|
+
return { value: picked, issues };
|
|
451
|
+
}
|
|
452
|
+
function unknownChoiceIssues(field, picked, path) {
|
|
453
|
+
const known = new Set(field.options.map((option) => option.value));
|
|
454
|
+
const unknown = picked.filter((value) => !known.has(value));
|
|
455
|
+
return unknown.map((value) => ({ path, code: "unknown-choice", value }));
|
|
456
|
+
}
|
|
457
|
+
function parseUrl(field, input, path) {
|
|
458
|
+
if (input === null) {
|
|
459
|
+
return valid(null);
|
|
460
|
+
}
|
|
461
|
+
if (typeof input !== "string") {
|
|
462
|
+
return invalidType(defaultValue(field), path);
|
|
463
|
+
}
|
|
464
|
+
const isWebAddress = linkTargetOf(input)?.kind === "web";
|
|
465
|
+
if (!isWebAddress) {
|
|
466
|
+
return { value: input, issues: [{ path, code: "unsupported-url" }] };
|
|
467
|
+
}
|
|
468
|
+
return valid(input);
|
|
469
|
+
}
|
|
470
|
+
function parseTime(field, input, path) {
|
|
471
|
+
if (input === null) {
|
|
472
|
+
return valid(null);
|
|
473
|
+
}
|
|
474
|
+
if (typeof input !== "string") {
|
|
475
|
+
return invalidType(defaultValue(field), path);
|
|
476
|
+
}
|
|
477
|
+
if (!TIME_PATTERN.test(input)) {
|
|
478
|
+
return {
|
|
479
|
+
value: defaultValue(field),
|
|
480
|
+
issues: [{ path, code: "invalid-time" }]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return valid(input);
|
|
484
|
+
}
|
|
252
485
|
function parseCollection(field, input, path) {
|
|
253
486
|
if (!Array.isArray(input)) {
|
|
254
487
|
return invalidType(defaultValue(field), path);
|
|
255
488
|
}
|
|
256
489
|
const records = input;
|
|
257
490
|
const parsedRecords = records.map(
|
|
258
|
-
(record, index) =>
|
|
491
|
+
(record, index) => parseRecord(field, record, [...path, index])
|
|
259
492
|
);
|
|
260
493
|
return {
|
|
261
494
|
value: parsedRecords.map((parsed) => parsed.value),
|
|
262
495
|
issues: parsedRecords.flatMap((parsed) => parsed.issues)
|
|
263
496
|
};
|
|
264
497
|
}
|
|
498
|
+
function parseRecord(field, input, path) {
|
|
499
|
+
const parsed = parseFields(field.item, input, path);
|
|
500
|
+
const id = isRecord(input) ? input[RECORD_ID_KEY] : void 0;
|
|
501
|
+
if (!isRecordId(id)) return parsed;
|
|
502
|
+
return {
|
|
503
|
+
value: { ...parsed.value, [RECORD_ID_KEY]: id },
|
|
504
|
+
issues: parsed.issues
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function parseReference(field, input, path) {
|
|
508
|
+
if (input === null) {
|
|
509
|
+
return valid(null);
|
|
510
|
+
}
|
|
511
|
+
if (!isRecordId(input)) {
|
|
512
|
+
return invalidType(defaultValue(field), path);
|
|
513
|
+
}
|
|
514
|
+
return valid(input);
|
|
515
|
+
}
|
|
516
|
+
function parseReferences(field, input, path) {
|
|
517
|
+
const isListOfIds = Array.isArray(input) && input.every((id) => isRecordId(id));
|
|
518
|
+
if (!isListOfIds) {
|
|
519
|
+
return invalidType(defaultValue(field), path);
|
|
520
|
+
}
|
|
521
|
+
const ids = [...new Set(input)];
|
|
522
|
+
return valid(ids);
|
|
523
|
+
}
|
|
265
524
|
function isCalendarDate(value) {
|
|
266
525
|
if (!DATE_PATTERN.test(value)) {
|
|
267
526
|
return false;
|
|
268
527
|
}
|
|
269
|
-
const
|
|
270
|
-
return !Number.isNaN(
|
|
528
|
+
const time2 = Date.parse(`${value}T00:00:00Z`);
|
|
529
|
+
return !Number.isNaN(time2) && new Date(time2).toISOString().startsWith(value);
|
|
271
530
|
}
|
|
272
531
|
function isRecord(value) {
|
|
273
532
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -324,6 +583,11 @@ function resolveInCollection(collection2, segments, collectionPath) {
|
|
|
324
583
|
return resolveInFields(collection2.item, rest, recordPath);
|
|
325
584
|
}
|
|
326
585
|
|
|
586
|
+
// src/schema/reveal-markers.ts
|
|
587
|
+
var REVEAL_ATTRIBUTE = "data-cms-reveal";
|
|
588
|
+
var REVEALED_ATTRIBUTE = "data-cms-revealed";
|
|
589
|
+
var REVEAL_EVENT = "cms:reveal";
|
|
590
|
+
|
|
327
591
|
export {
|
|
328
592
|
defineSchema,
|
|
329
593
|
text,
|
|
@@ -331,16 +595,32 @@ export {
|
|
|
331
595
|
date,
|
|
332
596
|
image,
|
|
333
597
|
link,
|
|
598
|
+
toggle,
|
|
599
|
+
choice,
|
|
600
|
+
choices,
|
|
601
|
+
url,
|
|
602
|
+
time,
|
|
603
|
+
reference,
|
|
604
|
+
references,
|
|
334
605
|
group,
|
|
335
606
|
collection,
|
|
336
607
|
defaultValue,
|
|
337
608
|
defaultContent,
|
|
338
609
|
linkTargetOf,
|
|
339
610
|
hrefOf,
|
|
611
|
+
RECORD_ID_KEY,
|
|
612
|
+
isRecordId,
|
|
613
|
+
newRecord,
|
|
614
|
+
withRecordIds,
|
|
615
|
+
collectionAt,
|
|
616
|
+
findRecord,
|
|
340
617
|
formatFieldPath,
|
|
341
618
|
countCharacters,
|
|
342
619
|
parseContent,
|
|
343
620
|
FIELD_PATH_ATTRIBUTE,
|
|
344
621
|
editable,
|
|
345
|
-
resolveFieldPath
|
|
622
|
+
resolveFieldPath,
|
|
623
|
+
REVEAL_ATTRIBUTE,
|
|
624
|
+
REVEALED_ATTRIBUTE,
|
|
625
|
+
REVEAL_EVENT
|
|
346
626
|
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
interface TextField {
|
|
2
|
+
readonly kind: "text";
|
|
3
|
+
readonly label: string;
|
|
4
|
+
/** A textarea instead of a single line. */
|
|
5
|
+
readonly multiline?: boolean;
|
|
6
|
+
/** In characters as people see them (see countCharacters), not UTF-16 units. */
|
|
7
|
+
readonly maxLength?: number;
|
|
8
|
+
}
|
|
9
|
+
interface NumberField {
|
|
10
|
+
readonly kind: "number";
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly min?: number;
|
|
13
|
+
readonly max?: number;
|
|
14
|
+
}
|
|
15
|
+
/** A calendar date without time, stored as `YYYY-MM-DD`. */
|
|
16
|
+
interface DateField {
|
|
17
|
+
readonly kind: "date";
|
|
18
|
+
readonly label: string;
|
|
19
|
+
}
|
|
20
|
+
/** An image address with its alternative text. */
|
|
21
|
+
interface ImageField {
|
|
22
|
+
readonly kind: "image";
|
|
23
|
+
readonly label: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A link: its text and where it leads (a page of the site, another site, a phone or an email).
|
|
27
|
+
* The site decides how it looks; a button «Записаться» stays a button.
|
|
28
|
+
*/
|
|
29
|
+
interface LinkField {
|
|
30
|
+
readonly kind: "link";
|
|
31
|
+
readonly label: string;
|
|
32
|
+
}
|
|
33
|
+
/** Yes or no, for example «Можно новичкам» or «Скрыть с сайта». */
|
|
34
|
+
interface ToggleField {
|
|
35
|
+
readonly kind: "toggle";
|
|
36
|
+
readonly label: string;
|
|
37
|
+
}
|
|
38
|
+
/** One of the options a person picks from, as the site names it and as the editor shows it. */
|
|
39
|
+
interface ChoiceOption {
|
|
40
|
+
/** What the content stores and the site reads; never shown to people. */
|
|
41
|
+
readonly value: string;
|
|
42
|
+
/** What the editor shows. */
|
|
43
|
+
readonly label: string;
|
|
44
|
+
}
|
|
45
|
+
/** One option of a fixed list, or none: a level, a category, a badge. */
|
|
46
|
+
interface ChoiceField<O extends readonly ChoiceOption[] = readonly ChoiceOption[]> {
|
|
47
|
+
readonly kind: "choice";
|
|
48
|
+
readonly label: string;
|
|
49
|
+
readonly options: O;
|
|
50
|
+
}
|
|
51
|
+
/** Any number of options of a fixed list: tags, the days a class runs. */
|
|
52
|
+
interface ChoicesField<O extends readonly ChoiceOption[] = readonly ChoiceOption[]> {
|
|
53
|
+
readonly kind: "choices";
|
|
54
|
+
readonly label: string;
|
|
55
|
+
readonly options: O;
|
|
56
|
+
}
|
|
57
|
+
/** A web address without a text: a booking widget, a map. Only `http://` and `https://`. */
|
|
58
|
+
interface UrlField {
|
|
59
|
+
readonly kind: "url";
|
|
60
|
+
readonly label: string;
|
|
61
|
+
}
|
|
62
|
+
/** A time of day without a date, stored as `HH:MM` (24 hours). */
|
|
63
|
+
interface TimeField {
|
|
64
|
+
readonly kind: "time";
|
|
65
|
+
readonly label: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A record of a collection of the schema, by its id: the teacher of a practice. `collection` is
|
|
69
|
+
* where that collection is, its name or a path through groups: "teachers", "studio.teachers".
|
|
70
|
+
*/
|
|
71
|
+
interface ReferenceField {
|
|
72
|
+
readonly kind: "reference";
|
|
73
|
+
readonly label: string;
|
|
74
|
+
readonly collection: string;
|
|
75
|
+
}
|
|
76
|
+
/** Any number of records of a collection of the schema, by their ids: the practices a teacher leads. */
|
|
77
|
+
interface ReferencesField {
|
|
78
|
+
readonly kind: "references";
|
|
79
|
+
readonly label: string;
|
|
80
|
+
readonly collection: string;
|
|
81
|
+
}
|
|
82
|
+
/** Named fields shown and stored together, for example the first screen of a page. */
|
|
83
|
+
interface GroupField<F extends Fields = Fields> {
|
|
84
|
+
readonly kind: "group";
|
|
85
|
+
readonly label: string;
|
|
86
|
+
readonly fields: F;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A list of records of the same shape, for example the items of a menu. Every record also has an
|
|
90
|
+
* `id` of its own, which never changes (see withRecordIds), so a record has no field named `id`.
|
|
91
|
+
*/
|
|
92
|
+
interface CollectionField<F extends RecordFields = RecordFields> {
|
|
93
|
+
readonly kind: "collection";
|
|
94
|
+
readonly label: string;
|
|
95
|
+
readonly item: F;
|
|
96
|
+
/** The field of a record that names it in the editor: a text field, or a link by its text. */
|
|
97
|
+
readonly itemTitle?: keyof F & string;
|
|
98
|
+
}
|
|
99
|
+
type Field = TextField | NumberField | DateField | ImageField | LinkField | ToggleField | ChoiceField | ChoicesField | UrlField | TimeField | ReferenceField | ReferencesField | GroupField | CollectionField;
|
|
100
|
+
type FieldKind = Field["kind"];
|
|
101
|
+
/** Field names to fields; the order of the keys is the order of the form. */
|
|
102
|
+
interface Fields {
|
|
103
|
+
readonly [name: string]: Field;
|
|
104
|
+
}
|
|
105
|
+
/** The fields of a record of a collection: any fields but `id`, which is the record's own. */
|
|
106
|
+
type RecordFields = Fields & {
|
|
107
|
+
readonly id?: never;
|
|
108
|
+
};
|
|
109
|
+
type FieldOptions<F extends Field> = Omit<F, "kind">;
|
|
110
|
+
/** The root of a site's schema. Only checks the shape and keeps the literal types. */
|
|
111
|
+
declare function defineSchema<F extends Fields>(fields: F): F;
|
|
112
|
+
declare function text(options: FieldOptions<TextField>): TextField;
|
|
113
|
+
declare function number(options: FieldOptions<NumberField>): NumberField;
|
|
114
|
+
declare function date(options: FieldOptions<DateField>): DateField;
|
|
115
|
+
declare function image(options: FieldOptions<ImageField>): ImageField;
|
|
116
|
+
declare function link(options: FieldOptions<LinkField>): LinkField;
|
|
117
|
+
declare function toggle(options: FieldOptions<ToggleField>): ToggleField;
|
|
118
|
+
/** `const` keeps the values of the options literal, so the content has their exact type. */
|
|
119
|
+
declare function choice<const O extends readonly ChoiceOption[]>(options: FieldOptions<ChoiceField<O>>): ChoiceField<O>;
|
|
120
|
+
declare function choices<const O extends readonly ChoiceOption[]>(options: FieldOptions<ChoicesField<O>>): ChoicesField<O>;
|
|
121
|
+
declare function url(options: FieldOptions<UrlField>): UrlField;
|
|
122
|
+
declare function time(options: FieldOptions<TimeField>): TimeField;
|
|
123
|
+
declare function reference(options: FieldOptions<ReferenceField>): ReferenceField;
|
|
124
|
+
declare function references(options: FieldOptions<ReferencesField>): ReferencesField;
|
|
125
|
+
declare function group<F extends Fields>(options: FieldOptions<GroupField<F>>): GroupField<F>;
|
|
126
|
+
declare function collection<F extends RecordFields>(options: FieldOptions<CollectionField<F>>): CollectionField<F>;
|
|
127
|
+
|
|
128
|
+
interface ImageValue {
|
|
129
|
+
readonly src: string;
|
|
130
|
+
readonly alt: string;
|
|
131
|
+
}
|
|
132
|
+
interface LinkValue {
|
|
133
|
+
readonly text: string;
|
|
134
|
+
/** A path of the site (`/menu`), a web address, `tel:` or `mailto:`; see linkTargetOf. */
|
|
135
|
+
readonly href: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The value of each field. A text can be empty (""), the other scalar fields can be unset (null):
|
|
139
|
+
* content is filled in by people, so a site must be ready for a missing price or date.
|
|
140
|
+
*/
|
|
141
|
+
type ValueOf<F extends Field> = F extends TextField ? string : F extends NumberField ? number | null : F extends DateField ? string | null : F extends ImageField ? ImageValue | null : F extends LinkField ? LinkValue | null : F extends ToggleField ? boolean : F extends ChoiceField<infer Options> ? OptionValue<Options> | null : F extends ChoicesField<infer Options> ? readonly OptionValue<Options>[] : F extends UrlField ? string | null : F extends TimeField ? string | null : F extends ReferenceField ? string | null : F extends ReferencesField ? readonly string[] : F extends GroupField<infer Group> ? ContentOf<Group> : F extends CollectionField<infer Item> ? readonly RecordOf<Item>[] : never;
|
|
142
|
+
/**
|
|
143
|
+
* A record of a collection: its fields and its id. The id is missing only in content no editor has
|
|
144
|
+
* saved yet, such as a site's initial content (see withRecordIds).
|
|
145
|
+
*/
|
|
146
|
+
type RecordOf<F extends RecordFields> = ContentOf<F> & {
|
|
147
|
+
readonly id?: string;
|
|
148
|
+
};
|
|
149
|
+
/** The values a list of options stores: `"soft" | "active"` for options with those values. */
|
|
150
|
+
type OptionValue<O extends readonly ChoiceOption[]> = O[number]["value"];
|
|
151
|
+
/** The content a schema describes: `ContentOf<typeof schema>` is the type a site renders. */
|
|
152
|
+
type ContentOf<F extends Fields> = {
|
|
153
|
+
readonly [Name in keyof F]: ValueOf<F[Name]>;
|
|
154
|
+
};
|
|
155
|
+
/** Content of any schema, for code that works with every site, such as the editor and the store. */
|
|
156
|
+
interface Content {
|
|
157
|
+
readonly [name: string]: FieldValue;
|
|
158
|
+
}
|
|
159
|
+
type FieldValue = string | number | boolean | null | readonly string[] | ImageValue | LinkValue | Content | readonly Content[];
|
|
160
|
+
declare function defaultValue(field: Field): FieldValue;
|
|
161
|
+
declare function defaultContent(fields: Fields): Content;
|
|
162
|
+
|
|
163
|
+
export { reference as A, references as B, type ContentOf as C, type DateField as D, text as E, type Fields as F, type GroupField as G, time as H, type ImageField as I, toggle as J, url as K, type LinkValue as L, type NumberField as N, type OptionValue as O, type RecordFields as R, type TextField as T, type UrlField as U, type ValueOf as V, type Content as a, type ImageValue as b, type Field as c, type CollectionField as d, type ChoiceField as e, type ChoiceOption as f, type ChoicesField as g, type FieldKind as h, type FieldValue as i, type LinkField as j, type RecordOf as k, type ReferenceField as l, type ReferencesField as m, type TimeField as n, type ToggleField as o, choice as p, choices as q, collection as r, date as s, defaultContent as t, defaultValue as u, defineSchema as v, group as w, image as x, link as y, number as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as Fields,
|
|
2
|
-
import { F as FieldPath } from './parse-content-
|
|
1
|
+
import { F as Fields, a as Content } from './content-GL5k9Mch.js';
|
|
2
|
+
import { F as FieldPath } from './parse-content-CVU-pHrH.js';
|
|
3
3
|
|
|
4
4
|
interface Box {
|
|
5
5
|
readonly top: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as Fields, C as ContentOf } from './content-
|
|
1
|
+
import { F as Fields, C as ContentOf } from './content-GL5k9Mch.js';
|
|
2
2
|
|
|
3
3
|
/** Where a value lives in the content: field names and, inside a collection, record indexes. */
|
|
4
4
|
type FieldPath = readonly (string | number)[];
|
|
@@ -8,6 +8,9 @@ type ContentIssue = {
|
|
|
8
8
|
} | {
|
|
9
9
|
readonly path: FieldPath;
|
|
10
10
|
readonly code: "invalid-date";
|
|
11
|
+
} | {
|
|
12
|
+
readonly path: FieldPath;
|
|
13
|
+
readonly code: "invalid-time";
|
|
11
14
|
} | {
|
|
12
15
|
readonly path: FieldPath;
|
|
13
16
|
readonly code: "too-long";
|
|
@@ -29,6 +32,17 @@ type ContentIssue = {
|
|
|
29
32
|
} | {
|
|
30
33
|
readonly path: FieldPath;
|
|
31
34
|
readonly code: "unsupported-link-href";
|
|
35
|
+
} | {
|
|
36
|
+
readonly path: FieldPath;
|
|
37
|
+
readonly code: "unsupported-url";
|
|
38
|
+
} | {
|
|
39
|
+
readonly path: FieldPath;
|
|
40
|
+
readonly code: "unknown-choice";
|
|
41
|
+
readonly value: string;
|
|
42
|
+
} | {
|
|
43
|
+
readonly path: FieldPath;
|
|
44
|
+
readonly code: "missing-record";
|
|
45
|
+
readonly id: string;
|
|
32
46
|
};
|
|
33
47
|
interface ParsedContent<C> {
|
|
34
48
|
readonly content: C;
|
|
@@ -46,7 +60,8 @@ declare function countCharacters(text: string): number;
|
|
|
46
60
|
* - a value of the wrong type or format gets the default too, and an issue;
|
|
47
61
|
* - a value of the right type that breaks a rule of its field (too long, below the minimum) is
|
|
48
62
|
* kept, with an issue: nothing a person typed is lost;
|
|
49
|
-
* - keys the schema does not know are dropped
|
|
63
|
+
* - keys the schema does not know are dropped; a record keeps its id (see withRecordIds);
|
|
64
|
+
* - a reference to a record its collection does not have (removed since) is kept, with an issue.
|
|
50
65
|
* Content with issues is valid to show and edit, but not to save.
|
|
51
66
|
*/
|
|
52
67
|
declare function parseContent<F extends Fields>(schema: F, input: unknown): ParsedContent<ContentOf<F>>;
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as MarkerBox, b as PROTOCOL_VERSION, c as PageLayout, P as PageMessage, d as Point, S as Shortcut, V as Viewport, r as readEditorMessage, e as readPageMessage, s as shortcutOf } from './messages-
|
|
2
|
-
import { F as Fields, I as ImageField } from './content-
|
|
3
|
-
import { F as FieldPath } from './parse-content-
|
|
1
|
+
export { B as Box, a as EditorKey, E as EditorMessage, I as InlineEditingOutcome, L as LinkBox, M as MarkerBox, b as PROTOCOL_VERSION, c as PageLayout, P as PageMessage, d as Point, S as Shortcut, V as Viewport, r as readEditorMessage, e as readPageMessage, s as shortcutOf } from './messages-CJ8vKPs9.js';
|
|
2
|
+
import { F as Fields, I as ImageField } from './content-GL5k9Mch.js';
|
|
3
|
+
import { F as FieldPath } from './parse-content-CVU-pHrH.js';
|
|
4
4
|
|
|
5
5
|
declare function carriesFiles(event: DragEvent): boolean;
|
|
6
6
|
|
package/dist/protocol.js
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
import { F as FieldPath } from './parse-content-
|
|
4
|
-
export { C as ContentIssue, P as ParsedContent, c as countCharacters, f as formatFieldPath, p as parseContent } from './parse-content-
|
|
1
|
+
import { b as ImageValue, L as LinkValue, c as Field, d as CollectionField, F as Fields, a as Content, R as RecordFields, C as ContentOf } from './content-GL5k9Mch.js';
|
|
2
|
+
export { e as ChoiceField, f as ChoiceOption, g as ChoicesField, D as DateField, h as FieldKind, i as FieldValue, G as GroupField, I as ImageField, j as LinkField, N as NumberField, O as OptionValue, k as RecordOf, l as ReferenceField, m as ReferencesField, T as TextField, n as TimeField, o as ToggleField, U as UrlField, V as ValueOf, p as choice, q as choices, r as collection, s as date, t as defaultContent, u as defaultValue, v as defineSchema, w as group, x as image, y as link, z as number, A as reference, B as references, E as text, H as time, J as toggle, K as url } from './content-GL5k9Mch.js';
|
|
3
|
+
import { F as FieldPath } from './parse-content-CVU-pHrH.js';
|
|
4
|
+
export { C as ContentIssue, P as ParsedContent, c as countCharacters, f as formatFieldPath, p as parseContent } from './parse-content-CVU-pHrH.js';
|
|
5
5
|
|
|
6
6
|
declare const FIELD_PATH_ATTRIBUTE = "data-cms-path";
|
|
7
7
|
interface FieldMarker {
|
|
8
8
|
readonly [FIELD_PATH_ATTRIBUTE]: string;
|
|
9
9
|
}
|
|
10
|
+
type Leaf = string | number | boolean | null | ImageValue | LinkValue | readonly string[];
|
|
10
11
|
/**
|
|
11
12
|
* Every path in content of type C, as a tuple: ["hero"], ["hero", "title"], ["menu", 2],
|
|
12
13
|
* ["menu", 2, "price"]. Scalars, images and links are leaves: an image is one field, not two.
|
|
13
14
|
*/
|
|
14
|
-
type ContentPath<C> = C extends readonly (infer Record)[] ? [number] | [number, ...ContentPath<Record>] :
|
|
15
|
+
type ContentPath<C> = C extends Leaf ? never : C extends readonly (infer Record)[] ? [number] | [number, ...ContentPath<Record>] : {
|
|
15
16
|
[Name in keyof C & string]: [Name] | [Name, ...ContentPath<C[Name]>];
|
|
16
17
|
}[keyof C & string];
|
|
17
18
|
/**
|
|
@@ -39,6 +40,36 @@ type FieldTarget = {
|
|
|
39
40
|
*/
|
|
40
41
|
declare function resolveFieldPath(schema: Fields, text: string): FieldTarget | undefined;
|
|
41
42
|
|
|
43
|
+
declare const RECORD_ID_KEY = "id";
|
|
44
|
+
declare function isRecordId(value: unknown): value is string;
|
|
45
|
+
/** A new record of a collection: its fields empty, its id the given one. */
|
|
46
|
+
declare function newRecord<F extends RecordFields>(collection: CollectionField<F>, id: string): Content;
|
|
47
|
+
/**
|
|
48
|
+
* The content with an id given to every record that has none, and a new one to a record whose id
|
|
49
|
+
* another record of its collection already has (a copy). Records with ids of their own keep them.
|
|
50
|
+
*/
|
|
51
|
+
declare function withRecordIds<F extends Fields>(schema: F, content: ContentOf<F>, newId: () => string): ContentOf<F>;
|
|
52
|
+
interface CollectionRecords {
|
|
53
|
+
readonly collection: CollectionField;
|
|
54
|
+
readonly records: readonly Content[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The collection a reference points at, `"teachers"` or `"studio.teachers"` through groups, with
|
|
58
|
+
* its records in the content; undefined when the schema has no collection there.
|
|
59
|
+
*/
|
|
60
|
+
declare function collectionAt(schema: Fields, content: Content, path: string): CollectionRecords | undefined;
|
|
61
|
+
/** The record of the list with the id, if it is there. */
|
|
62
|
+
declare function findRecord<R extends {
|
|
63
|
+
readonly id?: string;
|
|
64
|
+
}>(records: readonly R[], id: string | null): R | undefined;
|
|
65
|
+
|
|
66
|
+
/** On the element the site hides and shows itself: `<div data-cms-reveal hidden>`. */
|
|
67
|
+
declare const REVEAL_ATTRIBUTE = "data-cms-reveal";
|
|
68
|
+
/** Set by the bridge on a part it revealed; a site can show it with CSS alone. */
|
|
69
|
+
declare const REVEALED_ATTRIBUTE = "data-cms-revealed";
|
|
70
|
+
/** Sent to a revealed part, and bubbling up from it, for a site that shows it with a script. */
|
|
71
|
+
declare const REVEAL_EVENT = "cms:reveal";
|
|
72
|
+
|
|
42
73
|
/** Where a link leads, as the editor offers it. */
|
|
43
74
|
type LinkTarget = {
|
|
44
75
|
readonly kind: "page";
|
|
@@ -65,4 +96,4 @@ declare function linkTargetOf(href: string): LinkTarget | undefined;
|
|
|
65
96
|
*/
|
|
66
97
|
declare function hrefOf(target: LinkTarget): string;
|
|
67
98
|
|
|
68
|
-
export { CollectionField, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, ImageValue, type LinkKind, type LinkTarget, LinkValue, editable, hrefOf, linkTargetOf, resolveFieldPath };
|
|
99
|
+
export { CollectionField, type CollectionRecords, Content, ContentOf, type ContentPath, FIELD_PATH_ATTRIBUTE, Field, type FieldMarker, FieldPath, type FieldTarget, Fields, ImageValue, type LinkKind, type LinkTarget, LinkValue, RECORD_ID_KEY, REVEALED_ATTRIBUTE, REVEAL_ATTRIBUTE, REVEAL_EVENT, RecordFields, collectionAt, editable, findRecord, hrefOf, isRecordId, linkTargetOf, newRecord, resolveFieldPath, withRecordIds };
|
package/dist/schema.js
CHANGED
|
@@ -1,40 +1,72 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FIELD_PATH_ATTRIBUTE,
|
|
3
|
+
RECORD_ID_KEY,
|
|
4
|
+
REVEALED_ATTRIBUTE,
|
|
5
|
+
REVEAL_ATTRIBUTE,
|
|
6
|
+
REVEAL_EVENT,
|
|
7
|
+
choice,
|
|
8
|
+
choices,
|
|
3
9
|
collection,
|
|
10
|
+
collectionAt,
|
|
4
11
|
countCharacters,
|
|
5
12
|
date,
|
|
6
13
|
defaultContent,
|
|
7
14
|
defaultValue,
|
|
8
15
|
defineSchema,
|
|
9
16
|
editable,
|
|
17
|
+
findRecord,
|
|
10
18
|
formatFieldPath,
|
|
11
19
|
group,
|
|
12
20
|
hrefOf,
|
|
13
21
|
image,
|
|
22
|
+
isRecordId,
|
|
14
23
|
link,
|
|
15
24
|
linkTargetOf,
|
|
25
|
+
newRecord,
|
|
16
26
|
number,
|
|
17
27
|
parseContent,
|
|
28
|
+
reference,
|
|
29
|
+
references,
|
|
18
30
|
resolveFieldPath,
|
|
19
|
-
text
|
|
20
|
-
|
|
31
|
+
text,
|
|
32
|
+
time,
|
|
33
|
+
toggle,
|
|
34
|
+
url,
|
|
35
|
+
withRecordIds
|
|
36
|
+
} from "./chunk-AFTVM755.js";
|
|
21
37
|
export {
|
|
22
38
|
FIELD_PATH_ATTRIBUTE,
|
|
39
|
+
RECORD_ID_KEY,
|
|
40
|
+
REVEALED_ATTRIBUTE,
|
|
41
|
+
REVEAL_ATTRIBUTE,
|
|
42
|
+
REVEAL_EVENT,
|
|
43
|
+
choice,
|
|
44
|
+
choices,
|
|
23
45
|
collection,
|
|
46
|
+
collectionAt,
|
|
24
47
|
countCharacters,
|
|
25
48
|
date,
|
|
26
49
|
defaultContent,
|
|
27
50
|
defaultValue,
|
|
28
51
|
defineSchema,
|
|
29
52
|
editable,
|
|
53
|
+
findRecord,
|
|
30
54
|
formatFieldPath,
|
|
31
55
|
group,
|
|
32
56
|
hrefOf,
|
|
33
57
|
image,
|
|
58
|
+
isRecordId,
|
|
34
59
|
link,
|
|
35
60
|
linkTargetOf,
|
|
61
|
+
newRecord,
|
|
36
62
|
number,
|
|
37
63
|
parseContent,
|
|
64
|
+
reference,
|
|
65
|
+
references,
|
|
38
66
|
resolveFieldPath,
|
|
39
|
-
text
|
|
67
|
+
text,
|
|
68
|
+
time,
|
|
69
|
+
toggle,
|
|
70
|
+
url,
|
|
71
|
+
withRecordIds
|
|
40
72
|
};
|
package/dist/site.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
2
|
-
import { F as Fields, C as ContentOf } from './content-
|
|
2
|
+
import { F as Fields, C as ContentOf } from './content-GL5k9Mch.js';
|
|
3
3
|
|
|
4
4
|
interface SeamletSiteDefinition<F extends Fields> {
|
|
5
5
|
/** The site's address in the CMS: `/sites/<id>`. */
|
package/dist/site.js
CHANGED
package/package.json
CHANGED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
interface TextField {
|
|
2
|
-
readonly kind: "text";
|
|
3
|
-
readonly label: string;
|
|
4
|
-
/** A textarea instead of a single line. */
|
|
5
|
-
readonly multiline?: boolean;
|
|
6
|
-
/** In characters as people see them (see countCharacters), not UTF-16 units. */
|
|
7
|
-
readonly maxLength?: number;
|
|
8
|
-
}
|
|
9
|
-
interface NumberField {
|
|
10
|
-
readonly kind: "number";
|
|
11
|
-
readonly label: string;
|
|
12
|
-
readonly min?: number;
|
|
13
|
-
readonly max?: number;
|
|
14
|
-
}
|
|
15
|
-
/** A calendar date without time, stored as `YYYY-MM-DD`. */
|
|
16
|
-
interface DateField {
|
|
17
|
-
readonly kind: "date";
|
|
18
|
-
readonly label: string;
|
|
19
|
-
}
|
|
20
|
-
/** An image address with its alternative text. */
|
|
21
|
-
interface ImageField {
|
|
22
|
-
readonly kind: "image";
|
|
23
|
-
readonly label: string;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* A link: its text and where it leads (a page of the site, another site, a phone or an email).
|
|
27
|
-
* The site decides how it looks; a button «Записаться» stays a button.
|
|
28
|
-
*/
|
|
29
|
-
interface LinkField {
|
|
30
|
-
readonly kind: "link";
|
|
31
|
-
readonly label: string;
|
|
32
|
-
}
|
|
33
|
-
/** Named fields shown and stored together, for example the first screen of a page. */
|
|
34
|
-
interface GroupField<F extends Fields = Fields> {
|
|
35
|
-
readonly kind: "group";
|
|
36
|
-
readonly label: string;
|
|
37
|
-
readonly fields: F;
|
|
38
|
-
}
|
|
39
|
-
/** A list of records of the same shape, for example the items of a menu. */
|
|
40
|
-
interface CollectionField<F extends Fields = Fields> {
|
|
41
|
-
readonly kind: "collection";
|
|
42
|
-
readonly label: string;
|
|
43
|
-
readonly item: F;
|
|
44
|
-
/** The field of a record that names it in the editor: a text field, or a link by its text. */
|
|
45
|
-
readonly itemTitle?: keyof F & string;
|
|
46
|
-
}
|
|
47
|
-
type Field = TextField | NumberField | DateField | ImageField | LinkField | GroupField | CollectionField;
|
|
48
|
-
type FieldKind = Field["kind"];
|
|
49
|
-
/** Field names to fields; the order of the keys is the order of the form. */
|
|
50
|
-
interface Fields {
|
|
51
|
-
readonly [name: string]: Field;
|
|
52
|
-
}
|
|
53
|
-
type FieldOptions<F extends Field> = Omit<F, "kind">;
|
|
54
|
-
/** The root of a site's schema. Only checks the shape and keeps the literal types. */
|
|
55
|
-
declare function defineSchema<F extends Fields>(fields: F): F;
|
|
56
|
-
declare function text(options: FieldOptions<TextField>): TextField;
|
|
57
|
-
declare function number(options: FieldOptions<NumberField>): NumberField;
|
|
58
|
-
declare function date(options: FieldOptions<DateField>): DateField;
|
|
59
|
-
declare function image(options: FieldOptions<ImageField>): ImageField;
|
|
60
|
-
declare function link(options: FieldOptions<LinkField>): LinkField;
|
|
61
|
-
declare function group<F extends Fields>(options: FieldOptions<GroupField<F>>): GroupField<F>;
|
|
62
|
-
declare function collection<F extends Fields>(options: FieldOptions<CollectionField<F>>): CollectionField<F>;
|
|
63
|
-
|
|
64
|
-
interface ImageValue {
|
|
65
|
-
readonly src: string;
|
|
66
|
-
readonly alt: string;
|
|
67
|
-
}
|
|
68
|
-
interface LinkValue {
|
|
69
|
-
readonly text: string;
|
|
70
|
-
/** A path of the site (`/menu`), a web address, `tel:` or `mailto:`; see linkTargetOf. */
|
|
71
|
-
readonly href: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* The value of each field. A text can be empty (""), the other scalar fields can be unset (null):
|
|
75
|
-
* content is filled in by people, so a site must be ready for a missing price or date.
|
|
76
|
-
*/
|
|
77
|
-
type ValueOf<F extends Field> = F extends TextField ? string : F extends NumberField ? number | null : F extends DateField ? string | null : F extends ImageField ? ImageValue | null : F extends LinkField ? LinkValue | null : F extends GroupField<infer Group> ? ContentOf<Group> : F extends CollectionField<infer Item> ? readonly ContentOf<Item>[] : never;
|
|
78
|
-
/** The content a schema describes: `ContentOf<typeof schema>` is the type a site renders. */
|
|
79
|
-
type ContentOf<F extends Fields> = {
|
|
80
|
-
readonly [Name in keyof F]: ValueOf<F[Name]>;
|
|
81
|
-
};
|
|
82
|
-
/** Content of any schema, for code that works with every site, such as the editor and the store. */
|
|
83
|
-
interface Content {
|
|
84
|
-
readonly [name: string]: FieldValue;
|
|
85
|
-
}
|
|
86
|
-
type FieldValue = string | number | null | ImageValue | LinkValue | Content | readonly Content[];
|
|
87
|
-
declare function defaultValue(field: Field): FieldValue;
|
|
88
|
-
declare function defaultContent(fields: Fields): Content;
|
|
89
|
-
|
|
90
|
-
export { type ContentOf as C, type DateField as D, type Fields as F, type GroupField as G, type ImageField as I, type LinkValue as L, type NumberField as N, type TextField as T, type ValueOf as V, type ImageValue as a, type Field as b, type CollectionField as c, type Content as d, type FieldKind as e, type FieldValue as f, type LinkField as g, collection as h, date as i, defaultContent as j, defaultValue as k, defineSchema as l, group as m, image as n, link as o, number as p, text as t };
|