@goodfellow-cms/admin 0.3.0 → 0.5.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 +1 -1
- package/dist/editor.d.ts +18 -0
- package/dist/editor.js +1780 -0
- package/dist/entry-values-ocbalc9U.js +3952 -0
- package/dist/index.d.ts +470 -23
- package/dist/index.js +6485 -6036
- package/package.json +12 -7
- package/styles.css +510 -7
package/dist/editor.js
ADDED
|
@@ -0,0 +1,1780 @@
|
|
|
1
|
+
import { A as MediaChoiceContext, Ft as editedMarkdown, It as formattedHtml, Mt as storedEntryFields, O as createMediaPreviews, Q as StringsProvider, U as createChannel, V as READY_MESSAGE, a as AdminContext, at as mediaUrl, f as useDarkMode, h as ScheduleFields, i as usePreviewStyles, j as MediaChooser, jt as storedData, k as showMediaInPreview, l as useSiteContent, rt as findPage, s as useAdmin, t as SCHEDULE_PROP, tt as useStrings, u as setThemeChoice, y as SETTINGS_SECTIONS } from "./entry-values-ocbalc9U.js";
|
|
2
|
+
import { COLOR_SCHEME_ATTRIBUTE, MEDIA_DIR, WEEKDAYS, allPages, canFormatMarkdown, cleanEventValue, contentInLanguage, datePart, escapeStyleText, findEntry, formatDate, formatOccurrence, getPageHead, googleFontsUrl, hasLanguages, isAllDay, isEventLike, languageHome, languageLinks, mainLanguage, neverIndexed, pageView, pathLanguage, renderedProblems, shownOccurrence, sitePages, themeToCss, todayIn, wallClock, weekdayName, weekdayOf } from "@goodfellow-cms/core";
|
|
3
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { PageBody, SiteProvider, adminSite, applyEntry, createPuckConfig, createPuckConfigs, cx, mediaFieldKind, preparePage, siteMetadata } from "@goodfellow-cms/react";
|
|
7
|
+
import { AutoField, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
|
|
8
|
+
import { createPortal, flushSync } from "react-dom";
|
|
9
|
+
const CANVAS = `.gf-canvas`;
|
|
10
|
+
const CANVAS_CONTAINER = "gf-canvas";
|
|
11
|
+
/** At-rules whose blocks hold rules like the top level's, which are kept to the canvas too. */
|
|
12
|
+
const GROUPS = /* @__PURE__ */ new Set([
|
|
13
|
+
"media",
|
|
14
|
+
"supports",
|
|
15
|
+
"layer",
|
|
16
|
+
"container",
|
|
17
|
+
"document",
|
|
18
|
+
"starting-style"
|
|
19
|
+
]);
|
|
20
|
+
/** Where `text` has a comment, string or escape starting at `index`, the index just after it. */
|
|
21
|
+
function skipAtom(text, index) {
|
|
22
|
+
const char = text[index];
|
|
23
|
+
if (char === "/" && text[index + 1] === "*") {
|
|
24
|
+
const end = text.indexOf("*/", index + 2);
|
|
25
|
+
return end === -1 ? text.length : end + 2;
|
|
26
|
+
}
|
|
27
|
+
if (char === "\"" || char === "'") {
|
|
28
|
+
let at = index + 1;
|
|
29
|
+
while (at < text.length && text[at] !== char) at += text[at] === "\\" ? 2 : 1;
|
|
30
|
+
return at + 1;
|
|
31
|
+
}
|
|
32
|
+
if (char === "\\") return index + 2;
|
|
33
|
+
return index;
|
|
34
|
+
}
|
|
35
|
+
/** Splits a selector list at its top-level commas. */
|
|
36
|
+
function splitList(list) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
let depth = 0;
|
|
39
|
+
let start = 0;
|
|
40
|
+
for (let at = 0; at < list.length; at++) {
|
|
41
|
+
const skipped = skipAtom(list, at);
|
|
42
|
+
if (skipped !== at) {
|
|
43
|
+
at = skipped - 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const char = list[at];
|
|
47
|
+
if (char === "(" || char === "[") depth++;
|
|
48
|
+
else if (char === ")" || char === "]") depth--;
|
|
49
|
+
else if (char === "," && depth === 0) {
|
|
50
|
+
parts.push(list.slice(start, at));
|
|
51
|
+
start = at + 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
parts.push(list.slice(start));
|
|
55
|
+
return parts;
|
|
56
|
+
}
|
|
57
|
+
/** The page itself, as selectors name it, which becomes the canvas: `:root`, `html`, `html > body` or `body`. */
|
|
58
|
+
const ROOT = /^(?:(?:html|:root|:host)(?![\w-])(?:\s*>?\s*body(?![\w-]))?|body(?![\w-]))/i;
|
|
59
|
+
/** Attributes, classes and pseudo-classes on the page itself, such as `[data-gf-color-scheme="dark"]`. */
|
|
60
|
+
const QUALIFIERS = /^(?:\[[^\]]*\]|[.#][\w-]+|:[\w-]+(?:\((?:[^()]|\([^()]*\))*\))?)+/;
|
|
61
|
+
/** A pseudo-element at the end of a selector, which `:is()` can't hold. */
|
|
62
|
+
const PSEUDO_ELEMENT = /(::?(?:before|after|first-line|first-letter)|::[\w-]+(?:\([^)]*\))?)+$/i;
|
|
63
|
+
/** One selector, kept to the canvas. */
|
|
64
|
+
function scopeSelector(selector) {
|
|
65
|
+
const trimmed = selector.trim();
|
|
66
|
+
if (!trimmed) return selector;
|
|
67
|
+
const root = trimmed.match(ROOT);
|
|
68
|
+
if (root) {
|
|
69
|
+
const rest = trimmed.slice(root[0].length);
|
|
70
|
+
const qualifiers = rest.match(QUALIFIERS)?.[0] ?? "";
|
|
71
|
+
return qualifiers ? `:root${qualifiers} ${CANVAS}${rest.slice(qualifiers.length)}` : `${CANVAS}${rest}`;
|
|
72
|
+
}
|
|
73
|
+
const pseudo = trimmed.match(PSEUDO_ELEMENT);
|
|
74
|
+
const base = pseudo ? trimmed.slice(0, pseudo.index).trim() : trimmed;
|
|
75
|
+
const after = pseudo ? pseudo[0] : "";
|
|
76
|
+
return base ? `${CANVAS} :is(${base})${after}` : `${CANVAS} ${after}`;
|
|
77
|
+
}
|
|
78
|
+
/** A media query on the page's width alone, as one on the canvas's, or `undefined` for any other. */
|
|
79
|
+
function widthQuery(prelude) {
|
|
80
|
+
const query = prelude.replace(/^@media\s+/i, "").replace(/^(?:only\s+)?screen\s+and\s+/i, "").trim();
|
|
81
|
+
if (query.includes(",")) return void 0;
|
|
82
|
+
const features = query.split(/\s+and\s+/i);
|
|
83
|
+
return features.every((feature) => /^\(\s*(?:(?:min-|max-)?width\s*:[^()]+|[^()]*\bwidth\b[^()]*)\)$/i.test(feature.trim())) ? `@container ${CANVAS_CONTAINER} ${features.map((feature) => feature.trim()).join(" and ")}` : void 0;
|
|
84
|
+
}
|
|
85
|
+
/** The site's CSS, kept to the canvas. */
|
|
86
|
+
function scopeCss(css) {
|
|
87
|
+
let out = "";
|
|
88
|
+
const stack = ["group"];
|
|
89
|
+
let start = 0;
|
|
90
|
+
let at = 0;
|
|
91
|
+
while (at < css.length) {
|
|
92
|
+
const skipped = skipAtom(css, at);
|
|
93
|
+
if (skipped !== at) {
|
|
94
|
+
at = skipped;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const char = css[at];
|
|
98
|
+
if (char === "{") {
|
|
99
|
+
const raw = css.slice(start, at);
|
|
100
|
+
const lead = raw.match(/^\s*/)?.[0] ?? "";
|
|
101
|
+
const prelude = raw.replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
102
|
+
if (stack[stack.length - 1] === "group") {
|
|
103
|
+
if (prelude.startsWith("@")) {
|
|
104
|
+
const name = prelude.slice(1).match(/^[\w-]+/)?.[0]?.toLowerCase() ?? "";
|
|
105
|
+
const container = name === "media" ? widthQuery(prelude) : void 0;
|
|
106
|
+
out += `${lead}${container ?? prelude}{`;
|
|
107
|
+
stack.push(GROUPS.has(name) ? "group" : "other");
|
|
108
|
+
} else {
|
|
109
|
+
out += `${lead}${splitList(prelude).map(scopeSelector).join(", ")}{`;
|
|
110
|
+
stack.push("other");
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
out += `${raw}{`;
|
|
114
|
+
stack.push("other");
|
|
115
|
+
}
|
|
116
|
+
at += 1;
|
|
117
|
+
start = at;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "}" || char === ";") {
|
|
121
|
+
if (char === "}" && stack.length > 1) stack.pop();
|
|
122
|
+
out += css.slice(start, at + 1);
|
|
123
|
+
at += 1;
|
|
124
|
+
start = at;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
at += 1;
|
|
128
|
+
}
|
|
129
|
+
out += css.slice(start);
|
|
130
|
+
return `${CANVAS}{container:${CANVAS_CONTAINER}/inline-size}\n${out}`;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/frame/canvas-styles.ts
|
|
134
|
+
/**
|
|
135
|
+
* The site's styles in the editor's frame, kept to its canvas: the site's
|
|
136
|
+
* compiled CSS, the theme, and what Tailwind makes in the browser for classes
|
|
137
|
+
* typed in the editor and for custom CSS, so none of it restyles Puck.
|
|
138
|
+
*/
|
|
139
|
+
function styleElement(key) {
|
|
140
|
+
let element = document.head.querySelector(`style[data-gf="${key}"]`);
|
|
141
|
+
if (!element) {
|
|
142
|
+
element = document.createElement("style");
|
|
143
|
+
element.dataset.gf = key;
|
|
144
|
+
document.head.append(element);
|
|
145
|
+
}
|
|
146
|
+
return element;
|
|
147
|
+
}
|
|
148
|
+
/** What `@tailwindcss/browser` writes, which starts with its licence. */
|
|
149
|
+
function isTailwindOutput(node) {
|
|
150
|
+
return node instanceof HTMLStyleElement && !node.dataset.gf && node.getAttribute("type") === null && (node.textContent ?? "").startsWith("/*! tailwindcss");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Keeps Tailwind's own output from applying, since it styles the whole page,
|
|
154
|
+
* and puts a copy of it kept to the canvas in its place, as it changes.
|
|
155
|
+
*/
|
|
156
|
+
function scopeTailwindOutput() {
|
|
157
|
+
const watched = /* @__PURE__ */ new WeakSet();
|
|
158
|
+
const copy = styleElement("tailwind-scoped");
|
|
159
|
+
const follow = (output) => {
|
|
160
|
+
if (watched.has(output)) return;
|
|
161
|
+
watched.add(output);
|
|
162
|
+
output.media = "not all";
|
|
163
|
+
const update = () => {
|
|
164
|
+
copy.textContent = scopeCss(output.textContent ?? "");
|
|
165
|
+
};
|
|
166
|
+
update();
|
|
167
|
+
new MutationObserver(update).observe(output, {
|
|
168
|
+
childList: true,
|
|
169
|
+
characterData: true,
|
|
170
|
+
subtree: true
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const find = () => {
|
|
174
|
+
for (const element of document.head.querySelectorAll("style")) if (isTailwindOutput(element)) follow(element);
|
|
175
|
+
};
|
|
176
|
+
const observer = new MutationObserver(find);
|
|
177
|
+
observer.observe(document.head, {
|
|
178
|
+
childList: true,
|
|
179
|
+
subtree: true,
|
|
180
|
+
characterData: true
|
|
181
|
+
});
|
|
182
|
+
find();
|
|
183
|
+
return () => observer.disconnect();
|
|
184
|
+
}
|
|
185
|
+
/** The site's compiled CSS, kept to the canvas, once. */
|
|
186
|
+
function installSiteCss(css) {
|
|
187
|
+
styleElement("site").textContent = escapeStyleText(scopeCss(css.join("\n")));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The theme, fonts, custom CSS and Tailwind for the canvas, as the site
|
|
191
|
+
* shows them, light or dark as `scheme` says.
|
|
192
|
+
*/
|
|
193
|
+
function useCanvasStyles(preview, styles) {
|
|
194
|
+
const { theme, customCss, scheme } = styles;
|
|
195
|
+
useEffect(() => scopeTailwindOutput(), []);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
document.documentElement.setAttribute(COLOR_SCHEME_ATTRIBUTE, theme.darkMode ? scheme : "light");
|
|
198
|
+
styleElement("theme").textContent = escapeStyleText(scopeCss(themeToCss(theme)));
|
|
199
|
+
const fonts = googleFontsUrl(theme);
|
|
200
|
+
const link = document.head.querySelector("link[data-gf=\"fonts\"]");
|
|
201
|
+
if (fonts) {
|
|
202
|
+
const element = link ?? document.createElement("link");
|
|
203
|
+
element.rel = "stylesheet";
|
|
204
|
+
element.dataset.gf = "fonts";
|
|
205
|
+
element.href = fonts;
|
|
206
|
+
if (!link) document.head.append(element);
|
|
207
|
+
} else link?.remove();
|
|
208
|
+
}, [theme, scheme]);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const text = escapeStyleText(`${preview.themeCss}\n${customCss}`);
|
|
211
|
+
const current = document.head.querySelector("style[data-gf=\"tailwind\"]");
|
|
212
|
+
if (current?.textContent === text) return;
|
|
213
|
+
current?.remove();
|
|
214
|
+
const element = document.createElement("style");
|
|
215
|
+
element.type = "text/tailwindcss";
|
|
216
|
+
element.dataset.gf = "tailwind";
|
|
217
|
+
element.textContent = text;
|
|
218
|
+
document.head.append(element);
|
|
219
|
+
}, [preview.themeCss, customCss]);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const url = preview.tailwindBrowserUrl;
|
|
222
|
+
if (!url || document.head.querySelector("script[data-gf=\"tailwind-browser\"]")) return;
|
|
223
|
+
const script = document.createElement("script");
|
|
224
|
+
script.dataset.gf = "tailwind-browser";
|
|
225
|
+
script.src = url;
|
|
226
|
+
document.head.append(script);
|
|
227
|
+
}, [preview.tailwindBrowserUrl]);
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/share-preview.tsx
|
|
231
|
+
const usePuck$2 = createUsePuck();
|
|
232
|
+
/**
|
|
233
|
+
* How the page looks when a link to it is shared, such as in a message or on
|
|
234
|
+
* social media: its title, description and image as link previews show them,
|
|
235
|
+
* from the page's settings or else Site Settings', updated as they're edited.
|
|
236
|
+
*/
|
|
237
|
+
function SharePreview({ path }) {
|
|
238
|
+
const t = useStrings();
|
|
239
|
+
const { mediaPreviews } = useAdmin();
|
|
240
|
+
const { content } = useSiteContent();
|
|
241
|
+
const page = {
|
|
242
|
+
path,
|
|
243
|
+
file: "",
|
|
244
|
+
content: {
|
|
245
|
+
version: 1,
|
|
246
|
+
data: {
|
|
247
|
+
root: usePuck$2((state) => state.appState.data.root),
|
|
248
|
+
content: []
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const head = getPageHead(content.settings, page);
|
|
253
|
+
const { url } = content.settings;
|
|
254
|
+
const host = url ? new URL(url).hostname : void 0;
|
|
255
|
+
const image = head.socialImage ? mediaPreviews.resolve(head.socialImage) : void 0;
|
|
256
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
257
|
+
className: "gfa-share-preview",
|
|
258
|
+
"aria-labelledby": "gfa-share-preview-title",
|
|
259
|
+
children: [
|
|
260
|
+
/* @__PURE__ */ jsx("h3", {
|
|
261
|
+
id: "gfa-share-preview-title",
|
|
262
|
+
className: "gfa-label",
|
|
263
|
+
children: t("sharePreview.title")
|
|
264
|
+
}),
|
|
265
|
+
/* @__PURE__ */ jsxs("div", {
|
|
266
|
+
className: "gfa-share-card",
|
|
267
|
+
children: [image ? /* @__PURE__ */ jsx("img", {
|
|
268
|
+
src: image,
|
|
269
|
+
alt: "",
|
|
270
|
+
className: "gfa-share-image"
|
|
271
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
272
|
+
className: "gfa-share-image gfa-share-no-image",
|
|
273
|
+
children: t("sharePreview.noImage")
|
|
274
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
275
|
+
className: "gfa-share-text",
|
|
276
|
+
children: [
|
|
277
|
+
host && /* @__PURE__ */ jsx("span", {
|
|
278
|
+
className: "gfa-share-host",
|
|
279
|
+
children: host
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ jsx("strong", { children: head.title }),
|
|
282
|
+
head.description && /* @__PURE__ */ jsx("span", {
|
|
283
|
+
className: "gfa-share-description",
|
|
284
|
+
children: head.description
|
|
285
|
+
})
|
|
286
|
+
]
|
|
287
|
+
})]
|
|
288
|
+
}),
|
|
289
|
+
!url && /* @__PURE__ */ jsx("p", {
|
|
290
|
+
className: "gfa-hint",
|
|
291
|
+
children: t("sharePreview.noAddress")
|
|
292
|
+
}),
|
|
293
|
+
head.noIndex && /* @__PURE__ */ jsx("p", {
|
|
294
|
+
className: "gfa-hint",
|
|
295
|
+
children: t("sharePreview.hidden")
|
|
296
|
+
})
|
|
297
|
+
]
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/event-field.tsx
|
|
302
|
+
const EVERY_LABELS = {
|
|
303
|
+
none: "event.repeat.none",
|
|
304
|
+
day: "event.repeat.day",
|
|
305
|
+
week: "event.repeat.week",
|
|
306
|
+
month: "event.repeat.month",
|
|
307
|
+
year: "event.repeat.year"
|
|
308
|
+
};
|
|
309
|
+
const INTERVAL_LABELS = {
|
|
310
|
+
day: "event.interval.days",
|
|
311
|
+
week: "event.interval.weeks",
|
|
312
|
+
month: "event.interval.months",
|
|
313
|
+
year: "event.interval.years"
|
|
314
|
+
};
|
|
315
|
+
const NTH = {
|
|
316
|
+
1: "event.nth.1",
|
|
317
|
+
2: "event.nth.2",
|
|
318
|
+
3: "event.nth.3",
|
|
319
|
+
4: "event.nth.4",
|
|
320
|
+
5: "event.nth.5"
|
|
321
|
+
};
|
|
322
|
+
const DEFAULT_TIME = "09:00";
|
|
323
|
+
function timeOf(value) {
|
|
324
|
+
return value && !isAllDay(value) ? value.slice(11, 16) : "";
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* An event field: when it starts and ends, whether it's all day, and how it
|
|
328
|
+
* repeats, with dates it skips. Times are the site's own, in its time zone.
|
|
329
|
+
*/
|
|
330
|
+
function EventField({ id, label, hint, value, onChange, language, timeZone }) {
|
|
331
|
+
const t = useStrings();
|
|
332
|
+
const event = isEventLike(value) ? value : void 0;
|
|
333
|
+
const [skipDate, setSkipDate] = useState("");
|
|
334
|
+
const start = event?.start ?? "";
|
|
335
|
+
const allDay = start ? isAllDay(start) : false;
|
|
336
|
+
const startDate = datePart(start);
|
|
337
|
+
const repeat = event?.repeat;
|
|
338
|
+
const update = (changes) => {
|
|
339
|
+
const next = {
|
|
340
|
+
...event ?? { start: "" },
|
|
341
|
+
...changes
|
|
342
|
+
};
|
|
343
|
+
if (!next.start) return onChange(void 0);
|
|
344
|
+
onChange(cleanEventValue(next));
|
|
345
|
+
};
|
|
346
|
+
const updateRepeat = (changes) => {
|
|
347
|
+
if (changes === void 0) return update({ repeat: void 0 });
|
|
348
|
+
update({ repeat: {
|
|
349
|
+
every: repeat?.every ?? "week",
|
|
350
|
+
...repeat,
|
|
351
|
+
...changes
|
|
352
|
+
} });
|
|
353
|
+
};
|
|
354
|
+
const setStartDate = (date) => {
|
|
355
|
+
if (!date) return update({
|
|
356
|
+
start: "",
|
|
357
|
+
end: void 0
|
|
358
|
+
});
|
|
359
|
+
const time = allDay ? "" : `T${timeOf(start) || DEFAULT_TIME}`;
|
|
360
|
+
let end = event?.end;
|
|
361
|
+
if (end && startDate) {
|
|
362
|
+
const shift = (wallClock(date) ?? 0) - (wallClock(startDate) ?? 0);
|
|
363
|
+
const moved = new Date((wallClock(end) ?? 0) + shift).toISOString();
|
|
364
|
+
end = allDay ? moved.slice(0, 10) : moved.slice(0, 16);
|
|
365
|
+
}
|
|
366
|
+
update({
|
|
367
|
+
start: `${date}${time}`,
|
|
368
|
+
end
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
const setAllDay = (checked) => {
|
|
372
|
+
if (!startDate) return;
|
|
373
|
+
const endDate = event?.end ? datePart(event.end) : void 0;
|
|
374
|
+
if (checked) update({
|
|
375
|
+
start: startDate,
|
|
376
|
+
end: endDate && endDate !== startDate ? endDate : void 0
|
|
377
|
+
});
|
|
378
|
+
else update({
|
|
379
|
+
start: `${startDate}T${DEFAULT_TIME}`,
|
|
380
|
+
end: endDate ? `${endDate}T10:00` : void 0
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
const setEnd = (date, time) => {
|
|
384
|
+
if (!date && !time) return update({ end: void 0 });
|
|
385
|
+
const day = date || startDate;
|
|
386
|
+
update({ end: allDay ? day : `${day}T${time || timeOf(start) || DEFAULT_TIME}` });
|
|
387
|
+
};
|
|
388
|
+
const weekday = startDate ? weekdayOf(startDate) : "mo";
|
|
389
|
+
const nth = startDate ? Math.ceil(Number(startDate.slice(8)) / 7) : 1;
|
|
390
|
+
const shown = event?.start ? shownOccurrence(event, todayIn(timeZone)) : void 0;
|
|
391
|
+
const days = repeat?.days?.length ? repeat.days : [weekday];
|
|
392
|
+
return /* @__PURE__ */ jsx(FieldLabel, {
|
|
393
|
+
label,
|
|
394
|
+
el: "div",
|
|
395
|
+
children: /* @__PURE__ */ jsxs("fieldset", {
|
|
396
|
+
className: "gfa-event-field",
|
|
397
|
+
id,
|
|
398
|
+
"aria-label": label,
|
|
399
|
+
children: [
|
|
400
|
+
/* @__PURE__ */ jsxs("div", {
|
|
401
|
+
className: "gfa-event-row",
|
|
402
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
403
|
+
className: "gfa-event-part",
|
|
404
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.starts") }), /* @__PURE__ */ jsx("input", {
|
|
405
|
+
type: "date",
|
|
406
|
+
className: "gfa-puck-input",
|
|
407
|
+
value: startDate,
|
|
408
|
+
onChange: (change) => setStartDate(change.target.value)
|
|
409
|
+
})]
|
|
410
|
+
}), !allDay && startDate && /* @__PURE__ */ jsxs("label", {
|
|
411
|
+
className: "gfa-event-part",
|
|
412
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.time") }), /* @__PURE__ */ jsx("input", {
|
|
413
|
+
type: "time",
|
|
414
|
+
className: "gfa-puck-input",
|
|
415
|
+
value: timeOf(start),
|
|
416
|
+
onChange: (change) => change.target.value && update({ start: `${startDate}T${change.target.value}` })
|
|
417
|
+
})]
|
|
418
|
+
})]
|
|
419
|
+
}),
|
|
420
|
+
startDate && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
421
|
+
/* @__PURE__ */ jsxs("label", {
|
|
422
|
+
className: "gfa-checkbox",
|
|
423
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
424
|
+
type: "checkbox",
|
|
425
|
+
checked: allDay,
|
|
426
|
+
onChange: (change) => setAllDay(change.target.checked)
|
|
427
|
+
}), t("event.allDay")]
|
|
428
|
+
}),
|
|
429
|
+
/* @__PURE__ */ jsxs("div", {
|
|
430
|
+
className: "gfa-event-row",
|
|
431
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
432
|
+
className: "gfa-event-part",
|
|
433
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.ends") }), /* @__PURE__ */ jsx("input", {
|
|
434
|
+
type: "date",
|
|
435
|
+
className: "gfa-puck-input",
|
|
436
|
+
value: event?.end ? datePart(event.end) : "",
|
|
437
|
+
min: startDate,
|
|
438
|
+
onChange: (change) => setEnd(change.target.value, timeOf(event?.end))
|
|
439
|
+
})]
|
|
440
|
+
}), !allDay && /* @__PURE__ */ jsxs("label", {
|
|
441
|
+
className: "gfa-event-part",
|
|
442
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.time") }), /* @__PURE__ */ jsx("input", {
|
|
443
|
+
type: "time",
|
|
444
|
+
className: "gfa-puck-input",
|
|
445
|
+
value: timeOf(event?.end),
|
|
446
|
+
onChange: (change) => setEnd(event?.end ? datePart(event.end) : "", change.target.value)
|
|
447
|
+
})]
|
|
448
|
+
})]
|
|
449
|
+
}),
|
|
450
|
+
/* @__PURE__ */ jsxs("label", {
|
|
451
|
+
className: "gfa-event-part",
|
|
452
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.repeats") }), /* @__PURE__ */ jsx("select", {
|
|
453
|
+
className: "gfa-puck-input",
|
|
454
|
+
value: repeat?.every ?? "none",
|
|
455
|
+
onChange: (change) => change.target.value === "none" ? updateRepeat(void 0) : updateRepeat({ every: change.target.value }),
|
|
456
|
+
children: [
|
|
457
|
+
"none",
|
|
458
|
+
"day",
|
|
459
|
+
"week",
|
|
460
|
+
"month",
|
|
461
|
+
"year"
|
|
462
|
+
].map((every) => /* @__PURE__ */ jsx("option", {
|
|
463
|
+
value: every,
|
|
464
|
+
children: t(EVERY_LABELS[every])
|
|
465
|
+
}, every))
|
|
466
|
+
})]
|
|
467
|
+
}),
|
|
468
|
+
repeat && /* @__PURE__ */ jsxs("div", {
|
|
469
|
+
className: "gfa-event-repeat",
|
|
470
|
+
children: [
|
|
471
|
+
/* @__PURE__ */ jsxs("label", {
|
|
472
|
+
className: "gfa-event-part gfa-event-interval",
|
|
473
|
+
children: [/* @__PURE__ */ jsx("span", { children: t(INTERVAL_LABELS[repeat.every]) }), /* @__PURE__ */ jsx("input", {
|
|
474
|
+
type: "number",
|
|
475
|
+
min: 1,
|
|
476
|
+
className: "gfa-puck-input",
|
|
477
|
+
value: repeat.interval ?? 1,
|
|
478
|
+
onChange: (change) => {
|
|
479
|
+
const interval = Math.max(1, Math.round(Number(change.target.value) || 1));
|
|
480
|
+
updateRepeat({ interval });
|
|
481
|
+
}
|
|
482
|
+
})]
|
|
483
|
+
}),
|
|
484
|
+
repeat.every === "week" && /* @__PURE__ */ jsxs("fieldset", {
|
|
485
|
+
className: "gfa-event-days",
|
|
486
|
+
children: [/* @__PURE__ */ jsx("legend", { children: t("event.onDays") }), WEEKDAYS.map((day) => /* @__PURE__ */ jsxs("label", {
|
|
487
|
+
className: "gfa-checkbox",
|
|
488
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
489
|
+
type: "checkbox",
|
|
490
|
+
checked: days.includes(day),
|
|
491
|
+
onChange: (change) => {
|
|
492
|
+
const chosen = change.target.checked ? [...days, day] : days.filter((d) => d !== day);
|
|
493
|
+
if (chosen.length > 0) updateRepeat({ days: chosen });
|
|
494
|
+
}
|
|
495
|
+
}), weekdayName(day, language)]
|
|
496
|
+
}, day))]
|
|
497
|
+
}),
|
|
498
|
+
repeat.every === "month" && /* @__PURE__ */ jsxs("label", {
|
|
499
|
+
className: "gfa-event-part",
|
|
500
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.monthlyOn") }), /* @__PURE__ */ jsxs("select", {
|
|
501
|
+
className: "gfa-puck-input",
|
|
502
|
+
value: repeat.on ?? "date",
|
|
503
|
+
onChange: (change) => updateRepeat({ on: change.target.value }),
|
|
504
|
+
children: [
|
|
505
|
+
/* @__PURE__ */ jsx("option", {
|
|
506
|
+
value: "date",
|
|
507
|
+
children: t("event.monthly.date", { day: String(Number(startDate.slice(8))) })
|
|
508
|
+
}),
|
|
509
|
+
nth <= 4 && /* @__PURE__ */ jsx("option", {
|
|
510
|
+
value: "weekday",
|
|
511
|
+
children: t("event.monthly.weekday", {
|
|
512
|
+
nth: t(NTH[nth] ?? "event.nth.1"),
|
|
513
|
+
weekday: weekdayName(weekday, language, "long")
|
|
514
|
+
})
|
|
515
|
+
}),
|
|
516
|
+
/* @__PURE__ */ jsx("option", {
|
|
517
|
+
value: "last",
|
|
518
|
+
children: t("event.monthly.last", { weekday: weekdayName(weekday, language, "long") })
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
})]
|
|
522
|
+
}),
|
|
523
|
+
/* @__PURE__ */ jsxs("label", {
|
|
524
|
+
className: "gfa-event-part",
|
|
525
|
+
children: [/* @__PURE__ */ jsx("span", { children: t("event.until") }), /* @__PURE__ */ jsx("input", {
|
|
526
|
+
type: "date",
|
|
527
|
+
className: "gfa-puck-input",
|
|
528
|
+
value: repeat.until ?? "",
|
|
529
|
+
min: startDate,
|
|
530
|
+
onChange: (change) => updateRepeat({ until: change.target.value || void 0 })
|
|
531
|
+
})]
|
|
532
|
+
}),
|
|
533
|
+
/* @__PURE__ */ jsxs("div", {
|
|
534
|
+
className: "gfa-event-part",
|
|
535
|
+
children: [
|
|
536
|
+
/* @__PURE__ */ jsx("span", { children: t("event.skip") }),
|
|
537
|
+
(repeat.skip ?? []).length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
538
|
+
className: "gfa-event-skips",
|
|
539
|
+
children: (repeat.skip ?? []).map((date) => /* @__PURE__ */ jsxs("li", {
|
|
540
|
+
className: "gfa-event-skip",
|
|
541
|
+
children: [/* @__PURE__ */ jsx("span", { children: formatDate(date, language, "medium") }), /* @__PURE__ */ jsx("button", {
|
|
542
|
+
type: "button",
|
|
543
|
+
className: "gfa-button gfa-button-ghost",
|
|
544
|
+
onClick: () => updateRepeat({ skip: (repeat.skip ?? []).filter((d) => d !== date) }),
|
|
545
|
+
children: t("menus.remove")
|
|
546
|
+
})]
|
|
547
|
+
}, date))
|
|
548
|
+
}),
|
|
549
|
+
/* @__PURE__ */ jsxs("div", {
|
|
550
|
+
className: "gfa-event-row",
|
|
551
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
552
|
+
type: "date",
|
|
553
|
+
"aria-label": t("event.skipDate"),
|
|
554
|
+
className: "gfa-puck-input",
|
|
555
|
+
value: skipDate,
|
|
556
|
+
min: startDate,
|
|
557
|
+
onChange: (change) => setSkipDate(change.target.value)
|
|
558
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
559
|
+
type: "button",
|
|
560
|
+
className: "gfa-button",
|
|
561
|
+
disabled: !skipDate,
|
|
562
|
+
onClick: () => {
|
|
563
|
+
updateRepeat({ skip: [...repeat.skip ?? [], skipDate] });
|
|
564
|
+
setSkipDate("");
|
|
565
|
+
},
|
|
566
|
+
children: t("event.addSkip")
|
|
567
|
+
})]
|
|
568
|
+
})
|
|
569
|
+
]
|
|
570
|
+
})
|
|
571
|
+
]
|
|
572
|
+
}),
|
|
573
|
+
shown && /* @__PURE__ */ jsx("p", {
|
|
574
|
+
className: "gfa-puck-hint",
|
|
575
|
+
role: "status",
|
|
576
|
+
children: t(repeat ? "event.next" : "event.when", { when: formatOccurrence(shown, language) })
|
|
577
|
+
})
|
|
578
|
+
] }),
|
|
579
|
+
hint && /* @__PURE__ */ jsx("p", {
|
|
580
|
+
className: "gfa-puck-hint",
|
|
581
|
+
children: hint
|
|
582
|
+
})
|
|
583
|
+
]
|
|
584
|
+
})
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/markdown-field.tsx
|
|
589
|
+
/**
|
|
590
|
+
* Puck's formatted editor, without what Markdown can't store: underlining and
|
|
591
|
+
* alignment. Links, pictures and tables are edited in the Markdown tab.
|
|
592
|
+
*/
|
|
593
|
+
const formattedField = {
|
|
594
|
+
type: "richtext",
|
|
595
|
+
options: { underline: false },
|
|
596
|
+
renderMenu: () => /* @__PURE__ */ jsxs(RichTextMenu, { children: [
|
|
597
|
+
/* @__PURE__ */ jsxs(RichTextMenu.Group, { children: [/* @__PURE__ */ jsx(RichTextMenu.HeadingSelect, {}), /* @__PURE__ */ jsx(RichTextMenu.ListSelect, {})] }),
|
|
598
|
+
/* @__PURE__ */ jsxs(RichTextMenu.Group, { children: [
|
|
599
|
+
/* @__PURE__ */ jsx(RichTextMenu.Bold, {}),
|
|
600
|
+
/* @__PURE__ */ jsx(RichTextMenu.Italic, {}),
|
|
601
|
+
/* @__PURE__ */ jsx(RichTextMenu.Strikethrough, {}),
|
|
602
|
+
/* @__PURE__ */ jsx(RichTextMenu.InlineCode, {})
|
|
603
|
+
] }),
|
|
604
|
+
/* @__PURE__ */ jsxs(RichTextMenu.Group, { children: [
|
|
605
|
+
/* @__PURE__ */ jsx(RichTextMenu.Blockquote, {}),
|
|
606
|
+
/* @__PURE__ */ jsx(RichTextMenu.CodeBlock, {}),
|
|
607
|
+
/* @__PURE__ */ jsx(RichTextMenu.HorizontalRule, {})
|
|
608
|
+
] })
|
|
609
|
+
] })
|
|
610
|
+
};
|
|
611
|
+
/**
|
|
612
|
+
* The body of an entry stored as Markdown: edited formatted, or as Markdown.
|
|
613
|
+
* Markdown the formatted editor can't show without losing something, such as
|
|
614
|
+
* a table, is only offered as Markdown.
|
|
615
|
+
*/
|
|
616
|
+
function MarkdownField({ id, label, hint, value, onChange }) {
|
|
617
|
+
const t = useStrings();
|
|
618
|
+
const formattable = canFormatMarkdown(value);
|
|
619
|
+
const [tab, setTab] = useState(formattable ? "formatted" : "markdown");
|
|
620
|
+
const start = useRef(value);
|
|
621
|
+
const [html, setHtml] = useState(() => formattedHtml(value));
|
|
622
|
+
const choose = (next) => {
|
|
623
|
+
if (next === "formatted") {
|
|
624
|
+
start.current = value;
|
|
625
|
+
setHtml(formattedHtml(value));
|
|
626
|
+
}
|
|
627
|
+
setTab(next);
|
|
628
|
+
};
|
|
629
|
+
const current = formattable ? tab : "markdown";
|
|
630
|
+
return /* @__PURE__ */ jsxs(FieldLabel, {
|
|
631
|
+
label,
|
|
632
|
+
el: "div",
|
|
633
|
+
children: [
|
|
634
|
+
/* @__PURE__ */ jsx("div", {
|
|
635
|
+
className: "gfa-markdown-tabs",
|
|
636
|
+
role: "tablist",
|
|
637
|
+
"aria-label": label,
|
|
638
|
+
children: ["formatted", "markdown"].map((name) => /* @__PURE__ */ jsx("button", {
|
|
639
|
+
type: "button",
|
|
640
|
+
role: "tab",
|
|
641
|
+
className: "gfa-tab",
|
|
642
|
+
"aria-selected": current === name,
|
|
643
|
+
disabled: name === "formatted" && !formattable,
|
|
644
|
+
onClick: () => choose(name),
|
|
645
|
+
children: t(name === "formatted" ? "markdown.formatted" : "markdown.source")
|
|
646
|
+
}, name))
|
|
647
|
+
}),
|
|
648
|
+
current === "formatted" ? /* @__PURE__ */ jsx("div", {
|
|
649
|
+
className: "gfa-markdown-formatted",
|
|
650
|
+
children: /* @__PURE__ */ jsx(AutoField, {
|
|
651
|
+
id,
|
|
652
|
+
field: formattedField,
|
|
653
|
+
value: html,
|
|
654
|
+
onChange: (next) => {
|
|
655
|
+
const nextHtml = typeof next === "string" ? next : "";
|
|
656
|
+
setHtml(nextHtml);
|
|
657
|
+
onChange(editedMarkdown(start.current, nextHtml));
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("textarea", {
|
|
661
|
+
id,
|
|
662
|
+
"aria-label": label,
|
|
663
|
+
className: "gfa-puck-input gfa-markdown-source",
|
|
664
|
+
value,
|
|
665
|
+
spellCheck: true,
|
|
666
|
+
onChange: (event) => onChange(event.target.value)
|
|
667
|
+
}), !formattable && /* @__PURE__ */ jsx("p", {
|
|
668
|
+
className: "gfa-puck-hint",
|
|
669
|
+
children: t("markdown.sourceOnly")
|
|
670
|
+
})] }),
|
|
671
|
+
hint && /* @__PURE__ */ jsx("p", {
|
|
672
|
+
className: "gfa-puck-hint",
|
|
673
|
+
children: hint
|
|
674
|
+
})
|
|
675
|
+
]
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/frame/site-frame.tsx
|
|
680
|
+
/** The site's header and footer, read-only, around a page's content, the way the live site shows them. */
|
|
681
|
+
function SiteFrame({ layoutConfig, header, footer, site, className, children }) {
|
|
682
|
+
const metadata = siteMetadata(site);
|
|
683
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
684
|
+
header.content.length > 0 && /* @__PURE__ */ jsx("header", {
|
|
685
|
+
className: "gf-header",
|
|
686
|
+
inert: true,
|
|
687
|
+
children: /* @__PURE__ */ jsx(Render, {
|
|
688
|
+
config: layoutConfig,
|
|
689
|
+
data: header,
|
|
690
|
+
metadata
|
|
691
|
+
})
|
|
692
|
+
}),
|
|
693
|
+
/* @__PURE__ */ jsx("main", {
|
|
694
|
+
className: cx("gf-main", className),
|
|
695
|
+
children
|
|
696
|
+
}),
|
|
697
|
+
footer.content.length > 0 && /* @__PURE__ */ jsx("footer", {
|
|
698
|
+
className: "gf-footer",
|
|
699
|
+
inert: true,
|
|
700
|
+
children: /* @__PURE__ */ jsx(Render, {
|
|
701
|
+
config: layoutConfig,
|
|
702
|
+
data: footer,
|
|
703
|
+
metadata
|
|
704
|
+
})
|
|
705
|
+
})
|
|
706
|
+
] });
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/frame/entry-fields.tsx
|
|
710
|
+
const usePuck$1 = createUsePuck();
|
|
711
|
+
function Hint({ text }) {
|
|
712
|
+
return text ? /* @__PURE__ */ jsx("p", {
|
|
713
|
+
className: "gfa-puck-hint",
|
|
714
|
+
children: text
|
|
715
|
+
}) : null;
|
|
716
|
+
}
|
|
717
|
+
/** What the AI assistant is told a field holds, since it can't tell from a custom field. */
|
|
718
|
+
function aiHint(field, markdown) {
|
|
719
|
+
const description = field.hint;
|
|
720
|
+
if (markdown) return {
|
|
721
|
+
type: "string",
|
|
722
|
+
description: [description, "Formatted text, written in Markdown"].filter(Boolean).join(". ")
|
|
723
|
+
};
|
|
724
|
+
switch (field.type) {
|
|
725
|
+
case "number": return {
|
|
726
|
+
type: "number",
|
|
727
|
+
description
|
|
728
|
+
};
|
|
729
|
+
case "date": return {
|
|
730
|
+
type: "date",
|
|
731
|
+
description
|
|
732
|
+
};
|
|
733
|
+
case "select": return {
|
|
734
|
+
type: "choice",
|
|
735
|
+
options: field.options ?? [],
|
|
736
|
+
description
|
|
737
|
+
};
|
|
738
|
+
case "tags": return {
|
|
739
|
+
type: "choices",
|
|
740
|
+
options: field.options ?? [],
|
|
741
|
+
description
|
|
742
|
+
};
|
|
743
|
+
case "link": return {
|
|
744
|
+
type: "string",
|
|
745
|
+
description: [description, "A web address or a page address such as /about"].filter(Boolean).join(". ")
|
|
746
|
+
};
|
|
747
|
+
case "image": return {
|
|
748
|
+
type: "string",
|
|
749
|
+
description: [description, "An image address. Leave empty unless one is given"].filter(Boolean).join(". ")
|
|
750
|
+
};
|
|
751
|
+
default: return {
|
|
752
|
+
type: "string",
|
|
753
|
+
description
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** The Puck field for one of a collection's fields, shown in the editor's sidebar. */
|
|
758
|
+
function puckField(field, collection, settings) {
|
|
759
|
+
const markdown = collection.settings.markdown?.body === field.name;
|
|
760
|
+
const control = fieldControl(field, markdown, settings);
|
|
761
|
+
return field.type === "event" ? control : {
|
|
762
|
+
...control,
|
|
763
|
+
metadata: { ai: aiHint(field, markdown) }
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function fieldControl(field, markdown, settings) {
|
|
767
|
+
const label = field.required ? `${field.label} *` : field.label;
|
|
768
|
+
if (markdown) return {
|
|
769
|
+
type: "custom",
|
|
770
|
+
label,
|
|
771
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(MarkdownField, {
|
|
772
|
+
id,
|
|
773
|
+
label,
|
|
774
|
+
hint: field.hint,
|
|
775
|
+
value: typeof value === "string" ? value : "",
|
|
776
|
+
onChange
|
|
777
|
+
})
|
|
778
|
+
};
|
|
779
|
+
switch (field.type) {
|
|
780
|
+
case "richtext": return {
|
|
781
|
+
type: "richtext",
|
|
782
|
+
label
|
|
783
|
+
};
|
|
784
|
+
case "select": return {
|
|
785
|
+
type: "custom",
|
|
786
|
+
label,
|
|
787
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
788
|
+
label,
|
|
789
|
+
children: [/* @__PURE__ */ jsx(AutoField, {
|
|
790
|
+
id,
|
|
791
|
+
field: {
|
|
792
|
+
type: "select",
|
|
793
|
+
options: [{
|
|
794
|
+
label: "",
|
|
795
|
+
value: ""
|
|
796
|
+
}, ...field.options ?? []]
|
|
797
|
+
},
|
|
798
|
+
value: value ?? "",
|
|
799
|
+
onChange
|
|
800
|
+
}), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
|
|
801
|
+
})
|
|
802
|
+
};
|
|
803
|
+
case "number": return {
|
|
804
|
+
type: "custom",
|
|
805
|
+
label,
|
|
806
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
807
|
+
label,
|
|
808
|
+
children: [/* @__PURE__ */ jsx(AutoField, {
|
|
809
|
+
id,
|
|
810
|
+
field: { type: "number" },
|
|
811
|
+
value,
|
|
812
|
+
onChange: (next) => onChange(typeof next === "number" && Number.isFinite(next) ? next : void 0)
|
|
813
|
+
}), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
|
|
814
|
+
})
|
|
815
|
+
};
|
|
816
|
+
case "tags": return {
|
|
817
|
+
type: "custom",
|
|
818
|
+
label,
|
|
819
|
+
render: ({ value, onChange, id }) => {
|
|
820
|
+
const chosen = Array.isArray(value) ? value : [];
|
|
821
|
+
return /* @__PURE__ */ jsxs(FieldLabel, {
|
|
822
|
+
label,
|
|
823
|
+
el: "div",
|
|
824
|
+
children: [/* @__PURE__ */ jsx("fieldset", {
|
|
825
|
+
id,
|
|
826
|
+
className: "gfa-tags-field",
|
|
827
|
+
"aria-label": label,
|
|
828
|
+
children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs("label", {
|
|
829
|
+
className: "gfa-checkbox",
|
|
830
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
831
|
+
type: "checkbox",
|
|
832
|
+
checked: chosen.includes(option.value),
|
|
833
|
+
onChange: (event) => {
|
|
834
|
+
const next = event.target.checked ? [...chosen, option.value] : chosen.filter((tag) => tag !== option.value);
|
|
835
|
+
const ordered = (field.options ?? []).map((candidate) => candidate.value).filter((tag) => next.includes(tag));
|
|
836
|
+
onChange(ordered.length > 0 ? ordered : void 0);
|
|
837
|
+
}
|
|
838
|
+
}), option.label]
|
|
839
|
+
}, option.value))
|
|
840
|
+
}), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
case "event": return {
|
|
845
|
+
type: "custom",
|
|
846
|
+
label,
|
|
847
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(EventField, {
|
|
848
|
+
id,
|
|
849
|
+
label,
|
|
850
|
+
hint: field.hint,
|
|
851
|
+
value,
|
|
852
|
+
onChange,
|
|
853
|
+
language: settings.language,
|
|
854
|
+
timeZone: settings.timeZone
|
|
855
|
+
})
|
|
856
|
+
};
|
|
857
|
+
case "date": return {
|
|
858
|
+
type: "custom",
|
|
859
|
+
label,
|
|
860
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
861
|
+
label,
|
|
862
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
863
|
+
id,
|
|
864
|
+
type: "date",
|
|
865
|
+
className: "gfa-puck-input",
|
|
866
|
+
value: typeof value === "string" ? value : "",
|
|
867
|
+
onChange: (event) => onChange(event.target.value)
|
|
868
|
+
}), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
|
|
869
|
+
})
|
|
870
|
+
};
|
|
871
|
+
default: return {
|
|
872
|
+
type: "custom",
|
|
873
|
+
label,
|
|
874
|
+
render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
|
|
875
|
+
label,
|
|
876
|
+
children: [
|
|
877
|
+
/* @__PURE__ */ jsx(AutoField, {
|
|
878
|
+
id,
|
|
879
|
+
field: { type: field.type === "textarea" ? "textarea" : "text" },
|
|
880
|
+
value: typeof value === "string" ? value : "",
|
|
881
|
+
onChange
|
|
882
|
+
}),
|
|
883
|
+
(field.type === "image" || field.type === "link") && /* @__PURE__ */ jsx(MediaChooser, {
|
|
884
|
+
value: typeof value === "string" ? value : "",
|
|
885
|
+
kind: field.type === "image" ? "image" : "file",
|
|
886
|
+
onChange
|
|
887
|
+
}),
|
|
888
|
+
/* @__PURE__ */ jsx(Hint, { text: field.hint })
|
|
889
|
+
]
|
|
890
|
+
})
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
/** The entry's page as it will look, with the values being edited filled into the collection's template. */
|
|
895
|
+
function EntryPreview({ collection, entry, content, templateConfig, layoutConfig }) {
|
|
896
|
+
const t = useStrings();
|
|
897
|
+
const values = usePuck$1((state) => state.appState.data.root.props);
|
|
898
|
+
const current = useMemo(() => ({
|
|
899
|
+
...entry,
|
|
900
|
+
content: {
|
|
901
|
+
version: 1,
|
|
902
|
+
fields: storedEntryFields(values ?? {})
|
|
903
|
+
}
|
|
904
|
+
}), [entry, values]);
|
|
905
|
+
const site = useMemo(() => ({
|
|
906
|
+
settings: content.settings,
|
|
907
|
+
menus: content.menus,
|
|
908
|
+
path: entry.path ?? "/",
|
|
909
|
+
collections: content.collections,
|
|
910
|
+
collection,
|
|
911
|
+
entry: current
|
|
912
|
+
}), [
|
|
913
|
+
content,
|
|
914
|
+
entry.path,
|
|
915
|
+
collection,
|
|
916
|
+
current
|
|
917
|
+
]);
|
|
918
|
+
const metadata = useMemo(() => siteMetadata(site), [site]);
|
|
919
|
+
const applied = useMemo(() => applyEntry(migrate(collection.settings.template), templateConfig, collection, current, content.settings.language, content.settings.timeZone), [
|
|
920
|
+
collection,
|
|
921
|
+
templateConfig,
|
|
922
|
+
current,
|
|
923
|
+
content.settings.language,
|
|
924
|
+
content.settings.timeZone
|
|
925
|
+
]);
|
|
926
|
+
const [resolved, setResolved] = useState(applied);
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
let cancelled = false;
|
|
929
|
+
resolveAllData(applied, templateConfig, metadata).then((data) => {
|
|
930
|
+
if (!cancelled) setResolved(data);
|
|
931
|
+
});
|
|
932
|
+
return () => {
|
|
933
|
+
cancelled = true;
|
|
934
|
+
};
|
|
935
|
+
}, [
|
|
936
|
+
applied,
|
|
937
|
+
templateConfig,
|
|
938
|
+
metadata
|
|
939
|
+
]);
|
|
940
|
+
if (!collection.settings.path) return /* @__PURE__ */ jsx("p", {
|
|
941
|
+
style: {
|
|
942
|
+
maxWidth: "36rem",
|
|
943
|
+
margin: "4rem auto",
|
|
944
|
+
padding: "0 1rem",
|
|
945
|
+
textAlign: "center"
|
|
946
|
+
},
|
|
947
|
+
children: t("editEntry.noPage", { name: collection.settings.name })
|
|
948
|
+
});
|
|
949
|
+
const className = resolved.root.props?.className;
|
|
950
|
+
return /* @__PURE__ */ jsx(SiteProvider, {
|
|
951
|
+
value: site,
|
|
952
|
+
children: /* @__PURE__ */ jsx(SiteFrame, {
|
|
953
|
+
layoutConfig,
|
|
954
|
+
header: content.header.data,
|
|
955
|
+
footer: content.footer.data,
|
|
956
|
+
site,
|
|
957
|
+
className: typeof className === "string" ? className : void 0,
|
|
958
|
+
children: /* @__PURE__ */ jsx(Render, {
|
|
959
|
+
config: templateConfig,
|
|
960
|
+
data: resolved,
|
|
961
|
+
metadata
|
|
962
|
+
})
|
|
963
|
+
})
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/** The item's schedule, last in its fields, which the AI assistant leaves alone. */
|
|
967
|
+
function scheduleField(content) {
|
|
968
|
+
return {
|
|
969
|
+
type: "custom",
|
|
970
|
+
render: ({ value, onChange }) => /* @__PURE__ */ jsx(ScheduleFields, {
|
|
971
|
+
value: value ?? {},
|
|
972
|
+
onChange,
|
|
973
|
+
content
|
|
974
|
+
})
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/** The item editor's Puck config: no blocks, only the item's values, with its page as the canvas. */
|
|
978
|
+
function entryConfig(collection, entry, content, configs) {
|
|
979
|
+
return {
|
|
980
|
+
components: {},
|
|
981
|
+
root: {
|
|
982
|
+
fields: Object.fromEntries([...collection.settings.fields.map((field) => [field.name, puckField(field, collection, content.settings)]), [SCHEDULE_PROP, scheduleField(content)]]),
|
|
983
|
+
render: () => /* @__PURE__ */ jsx("div", {
|
|
984
|
+
className: "gf-canvas",
|
|
985
|
+
children: /* @__PURE__ */ jsx(EntryPreview, {
|
|
986
|
+
collection,
|
|
987
|
+
entry,
|
|
988
|
+
content,
|
|
989
|
+
templateConfig: configs.templateConfig,
|
|
990
|
+
layoutConfig: configs.layoutConfig
|
|
991
|
+
})
|
|
992
|
+
})
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/frame/local-code.ts
|
|
998
|
+
/**
|
|
999
|
+
* The site's code, run in the page that has loaded it: the frame that runs
|
|
1000
|
+
* it for the admin panel, or tests. Never imported by the admin panel's own page.
|
|
1001
|
+
*/
|
|
1002
|
+
/** The site's code run here, in the page that has loaded it: the frame, or tests. */
|
|
1003
|
+
function localSiteCode(config) {
|
|
1004
|
+
const configs = createPuckConfigs(config);
|
|
1005
|
+
return {
|
|
1006
|
+
async sitePages(content, today) {
|
|
1007
|
+
return sitePages(content, today, config.blocks);
|
|
1008
|
+
},
|
|
1009
|
+
async pageView(page, content, today) {
|
|
1010
|
+
return pageView(page, content, today, config.blocks);
|
|
1011
|
+
},
|
|
1012
|
+
async renderedProblems(content, targets, today) {
|
|
1013
|
+
if (targets.length === 0 || typeof document === "undefined") return [];
|
|
1014
|
+
const container = document.createElement("div");
|
|
1015
|
+
const root = createRoot(container);
|
|
1016
|
+
const problems = [];
|
|
1017
|
+
try {
|
|
1018
|
+
for (const { page, file } of targets.slice(0, 10)) try {
|
|
1019
|
+
const view = pageView(page, content, today, config.blocks);
|
|
1020
|
+
const prepared = await preparePage(configs, content, view ? {
|
|
1021
|
+
...page,
|
|
1022
|
+
view
|
|
1023
|
+
} : page);
|
|
1024
|
+
flushSync(() => root.render(createElement(PageBody, {
|
|
1025
|
+
site: prepared.site,
|
|
1026
|
+
pageConfig: prepared.pageConfig,
|
|
1027
|
+
layoutConfig: prepared.layoutConfig,
|
|
1028
|
+
page: prepared.data,
|
|
1029
|
+
header: prepared.header,
|
|
1030
|
+
footer: prepared.footer
|
|
1031
|
+
})));
|
|
1032
|
+
problems.push(...renderedProblems({
|
|
1033
|
+
html: container.innerHTML,
|
|
1034
|
+
path: page.path,
|
|
1035
|
+
file
|
|
1036
|
+
}));
|
|
1037
|
+
} catch {}
|
|
1038
|
+
} finally {
|
|
1039
|
+
root.unmount();
|
|
1040
|
+
}
|
|
1041
|
+
return problems;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region src/frame/frame-site.tsx
|
|
1047
|
+
/**
|
|
1048
|
+
* What components in the site's frames share: the site's code, the
|
|
1049
|
+
* connection to the admin panel's page, and a stand-in for the admin panel's
|
|
1050
|
+
* context with what its components use there, which can't publish or read the
|
|
1051
|
+
* repository.
|
|
1052
|
+
*/
|
|
1053
|
+
const FrameCodeContext = createContext(null);
|
|
1054
|
+
function useFrameCode() {
|
|
1055
|
+
const value = useContext(FrameCodeContext);
|
|
1056
|
+
if (!value) throw new Error("useFrameCode() must be used in the site's frame.");
|
|
1057
|
+
return value;
|
|
1058
|
+
}
|
|
1059
|
+
function adminClient(channel) {
|
|
1060
|
+
return {
|
|
1061
|
+
call: (method, ...params) => channel.call(method, params[0]),
|
|
1062
|
+
emit: (name, payload) => channel.emit(name, payload),
|
|
1063
|
+
channel
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const AdminClientContext = createContext(null);
|
|
1067
|
+
function useAdminClient() {
|
|
1068
|
+
const value = useContext(AdminClientContext);
|
|
1069
|
+
if (!value) throw new Error("useAdminClient() must be used in the site's frame.");
|
|
1070
|
+
return value;
|
|
1071
|
+
}
|
|
1072
|
+
/** Media from the admin panel's page, which reads files the live site doesn't have yet from the repository. */
|
|
1073
|
+
function frameMediaPreviews(client, siteUrl) {
|
|
1074
|
+
return createMediaPreviews({ readBytes: async (path) => {
|
|
1075
|
+
if (!path.startsWith(`${MEDIA_DIR}/`)) return void 0;
|
|
1076
|
+
return await client.call("media.bytes", mediaUrl(path)) ?? void 0;
|
|
1077
|
+
} }, siteUrl);
|
|
1078
|
+
}
|
|
1079
|
+
function refused() {
|
|
1080
|
+
throw new Error("The site's frame can't do this.");
|
|
1081
|
+
}
|
|
1082
|
+
/** Everything the frame's components need, around them. */
|
|
1083
|
+
function FrameProviders({ config, client, init, site, children }) {
|
|
1084
|
+
const code = useMemo(() => ({
|
|
1085
|
+
config,
|
|
1086
|
+
pageConfig: createPuckConfig(config, "page"),
|
|
1087
|
+
layoutConfig: createPuckConfig(config, "layout"),
|
|
1088
|
+
templateConfig: createPuckConfig(config, "template"),
|
|
1089
|
+
code: localSiteCode(config)
|
|
1090
|
+
}), [config]);
|
|
1091
|
+
const described = useMemo(() => adminSite(config), [config]);
|
|
1092
|
+
const mediaPreviews = useMemo(() => frameMediaPreviews(client, init.siteUrl), [client, init.siteUrl]);
|
|
1093
|
+
useEffect(() => client.channel.handle("media.remember", (file) => {
|
|
1094
|
+
if (typeof file?.url === "string" && file.bytes instanceof Uint8Array) mediaPreviews.remember(file.url, file.bytes);
|
|
1095
|
+
}), [client, mediaPreviews]);
|
|
1096
|
+
const { timing, visibility, syncProblem } = site.schedule;
|
|
1097
|
+
const admin = useMemo(() => ({
|
|
1098
|
+
site: described,
|
|
1099
|
+
blocks: code.templateConfig.components,
|
|
1100
|
+
code: code.code,
|
|
1101
|
+
preview: init.preview,
|
|
1102
|
+
siteUrl: init.siteUrl,
|
|
1103
|
+
state: {
|
|
1104
|
+
status: "ready",
|
|
1105
|
+
content: site.content,
|
|
1106
|
+
revision: "",
|
|
1107
|
+
media: site.media
|
|
1108
|
+
},
|
|
1109
|
+
account: {
|
|
1110
|
+
user: { login: "" },
|
|
1111
|
+
hostName: init.hostName,
|
|
1112
|
+
redirectAddressesListed: false
|
|
1113
|
+
},
|
|
1114
|
+
mediaPreviews,
|
|
1115
|
+
reload: async () => refused(),
|
|
1116
|
+
publish: async () => refused(),
|
|
1117
|
+
drafts: {
|
|
1118
|
+
kept: true,
|
|
1119
|
+
names: [],
|
|
1120
|
+
open: async () => refused(),
|
|
1121
|
+
publishAll: async () => refused(),
|
|
1122
|
+
submit: async () => refused(),
|
|
1123
|
+
discard: async () => refused()
|
|
1124
|
+
},
|
|
1125
|
+
readFile: async () => refused(),
|
|
1126
|
+
listFiles: async () => refused(),
|
|
1127
|
+
registries: {},
|
|
1128
|
+
outOfReach: false,
|
|
1129
|
+
contentOnly: false,
|
|
1130
|
+
...timing && { scheduledBuilds: {
|
|
1131
|
+
timing: async () => timing,
|
|
1132
|
+
sync: void 0
|
|
1133
|
+
} },
|
|
1134
|
+
...visibility && { repositoryVisibility: async () => visibility },
|
|
1135
|
+
...syncProblem && { scheduleSyncProblem: syncProblem }
|
|
1136
|
+
}), [
|
|
1137
|
+
described,
|
|
1138
|
+
code,
|
|
1139
|
+
init,
|
|
1140
|
+
site,
|
|
1141
|
+
mediaPreviews,
|
|
1142
|
+
timing,
|
|
1143
|
+
visibility,
|
|
1144
|
+
syncProblem
|
|
1145
|
+
]);
|
|
1146
|
+
const chooseMedia = useMemo(() => (kind, value) => client.call("media.choose", {
|
|
1147
|
+
kind,
|
|
1148
|
+
value
|
|
1149
|
+
}), [client]);
|
|
1150
|
+
return /* @__PURE__ */ jsx(StringsProvider, {
|
|
1151
|
+
strings: init.strings,
|
|
1152
|
+
children: /* @__PURE__ */ jsx(AdminClientContext.Provider, {
|
|
1153
|
+
value: client,
|
|
1154
|
+
children: /* @__PURE__ */ jsx(FrameCodeContext.Provider, {
|
|
1155
|
+
value: code,
|
|
1156
|
+
children: /* @__PURE__ */ jsx(AdminContext.Provider, {
|
|
1157
|
+
value: admin,
|
|
1158
|
+
children: /* @__PURE__ */ jsx(MediaChoiceContext.Provider, {
|
|
1159
|
+
value: chooseMedia,
|
|
1160
|
+
children
|
|
1161
|
+
})
|
|
1162
|
+
})
|
|
1163
|
+
})
|
|
1164
|
+
})
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
//#endregion
|
|
1168
|
+
//#region src/frame/page-settings.ts
|
|
1169
|
+
/** For pages and templates, whose settings include `noIndex`; `path` is the page's, for pages. */
|
|
1170
|
+
function shownSettings(original, path) {
|
|
1171
|
+
const fixed = path !== void 0 && neverIndexed(path);
|
|
1172
|
+
const originalRoot = original.root;
|
|
1173
|
+
const originalValue = originalRoot.props?.noIndex;
|
|
1174
|
+
return {
|
|
1175
|
+
show(data) {
|
|
1176
|
+
const root = data.root;
|
|
1177
|
+
if (fixed) return {
|
|
1178
|
+
...data,
|
|
1179
|
+
root: {
|
|
1180
|
+
...root,
|
|
1181
|
+
props: {
|
|
1182
|
+
...root.props,
|
|
1183
|
+
noIndex: true
|
|
1184
|
+
},
|
|
1185
|
+
readOnly: {
|
|
1186
|
+
...root.readOnly,
|
|
1187
|
+
noIndex: true
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
if (root.props?.noIndex !== void 0) return data;
|
|
1192
|
+
return {
|
|
1193
|
+
...data,
|
|
1194
|
+
root: {
|
|
1195
|
+
...root,
|
|
1196
|
+
props: {
|
|
1197
|
+
...root.props,
|
|
1198
|
+
noIndex: false
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
},
|
|
1203
|
+
keep(data) {
|
|
1204
|
+
const root = data.root;
|
|
1205
|
+
const { noIndex, ...props } = root.props ?? {};
|
|
1206
|
+
const kept = fixed || originalValue === void 0 && noIndex === false ? originalValue : noIndex;
|
|
1207
|
+
const { readOnly: _shown, ...rest } = root;
|
|
1208
|
+
const readOnly = fixed ? originalRoot.readOnly : root.readOnly;
|
|
1209
|
+
return {
|
|
1210
|
+
...data,
|
|
1211
|
+
root: {
|
|
1212
|
+
...rest,
|
|
1213
|
+
props: kept === void 0 ? props : {
|
|
1214
|
+
...props,
|
|
1215
|
+
noIndex: kept
|
|
1216
|
+
},
|
|
1217
|
+
...readOnly && { readOnly }
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
//#endregion
|
|
1224
|
+
//#region src/frame/editor.tsx
|
|
1225
|
+
/**
|
|
1226
|
+
* Puck in the site's frame, for one page, header, footer, template or item.
|
|
1227
|
+
* Its own header isn't shown: the admin panel's page draws the title, Undo,
|
|
1228
|
+
* Redo and Publish outside the frame, and asks this one for what to show.
|
|
1229
|
+
*/
|
|
1230
|
+
const usePuck = createUsePuck();
|
|
1231
|
+
/** Where in the admin panel blocks' notes link to. */
|
|
1232
|
+
const ADMIN_PLACES = {
|
|
1233
|
+
settings: "#/settings/general",
|
|
1234
|
+
"contact-settings": `#/settings/general?section=${SETTINGS_SECTIONS.contact}`,
|
|
1235
|
+
"forms-settings": `#/settings/general?section=${SETTINGS_SECTIONS.forms}`,
|
|
1236
|
+
"theme-settings": "#/settings/theme",
|
|
1237
|
+
"language-settings": "#/settings/languages"
|
|
1238
|
+
};
|
|
1239
|
+
/** Puck's text field, with the media library's chooser below it for fields that hold a media address. */
|
|
1240
|
+
function TextFieldWithMedia({ field, value, onChange, readOnly, children }) {
|
|
1241
|
+
const kind = mediaFieldKind(field);
|
|
1242
|
+
if (!kind || readOnly) return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
1243
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1244
|
+
className: "gfa-media-puck-field",
|
|
1245
|
+
children: [children, /* @__PURE__ */ jsx(MediaChooser, {
|
|
1246
|
+
value: typeof value === "string" ? value : "",
|
|
1247
|
+
kind,
|
|
1248
|
+
onChange: (url) => onChange(url)
|
|
1249
|
+
})]
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Wraps the editor's canvas the way the live site wraps it, so custom CSS
|
|
1254
|
+
* targeting `.gf-header`, `.gf-main` or `.gf-footer` previews correctly, in
|
|
1255
|
+
* `.gf-canvas`, which the site's CSS is scoped to. Pages also show the site's
|
|
1256
|
+
* header and footer, read-only, for context.
|
|
1257
|
+
*/
|
|
1258
|
+
function canvasConfig(kind, config, layoutConfig, header, footer, site) {
|
|
1259
|
+
const render = kind === "page" || kind === "template" ? ({ children, className }) => /* @__PURE__ */ jsx("div", {
|
|
1260
|
+
className: "gf-canvas",
|
|
1261
|
+
children: /* @__PURE__ */ jsx(SiteFrame, {
|
|
1262
|
+
layoutConfig,
|
|
1263
|
+
header,
|
|
1264
|
+
footer,
|
|
1265
|
+
site,
|
|
1266
|
+
className,
|
|
1267
|
+
children
|
|
1268
|
+
})
|
|
1269
|
+
}) : ({ children }) => /* @__PURE__ */ jsx("div", {
|
|
1270
|
+
className: "gf-canvas",
|
|
1271
|
+
children: kind === "header" ? /* @__PURE__ */ jsx("header", {
|
|
1272
|
+
className: "gf-header",
|
|
1273
|
+
children
|
|
1274
|
+
}) : /* @__PURE__ */ jsx("footer", {
|
|
1275
|
+
className: "gf-footer",
|
|
1276
|
+
children
|
|
1277
|
+
})
|
|
1278
|
+
});
|
|
1279
|
+
return {
|
|
1280
|
+
...config,
|
|
1281
|
+
root: {
|
|
1282
|
+
...config.root,
|
|
1283
|
+
render
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/** Puck's fields and blocks as JSON, for the AI assistant in the admin panel's page. */
|
|
1288
|
+
function describeConfig(config) {
|
|
1289
|
+
return JSON.parse(JSON.stringify({
|
|
1290
|
+
components: Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, {
|
|
1291
|
+
label: component.label,
|
|
1292
|
+
fields: component.fields ?? {},
|
|
1293
|
+
defaultProps: component.defaultProps
|
|
1294
|
+
}])),
|
|
1295
|
+
root: { fields: config.root?.fields ?? {} }
|
|
1296
|
+
}));
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Tells the admin panel's page what Puck shows, as it changes, and does what it
|
|
1300
|
+
* asks: Undo, Redo, the sidebars, and the AI assistant's changes. Rendered in
|
|
1301
|
+
* place of Puck's header, so it's inside Puck.
|
|
1302
|
+
*/
|
|
1303
|
+
function Bridge({ channel, settings }) {
|
|
1304
|
+
const getPuck = useGetPuck();
|
|
1305
|
+
const data = usePuck((state) => state.appState.data);
|
|
1306
|
+
const selected = usePuck((state) => state.selectedItem);
|
|
1307
|
+
const past = usePuck((state) => state.history.hasPast);
|
|
1308
|
+
const future = usePuck((state) => state.history.hasFuture);
|
|
1309
|
+
const ui = usePuck((state) => state.appState.ui);
|
|
1310
|
+
useEffect(() => {
|
|
1311
|
+
const state = {
|
|
1312
|
+
data: settings.keep(data),
|
|
1313
|
+
selected: selected ?? null,
|
|
1314
|
+
past,
|
|
1315
|
+
future,
|
|
1316
|
+
leftSideBarVisible: ui.leftSideBarVisible,
|
|
1317
|
+
rightSideBarVisible: ui.rightSideBarVisible,
|
|
1318
|
+
leftSideBarWidth: ui.leftSideBarWidth,
|
|
1319
|
+
rightSideBarWidth: ui.rightSideBarWidth
|
|
1320
|
+
};
|
|
1321
|
+
channel.emit("editor.state", state);
|
|
1322
|
+
}, [
|
|
1323
|
+
channel,
|
|
1324
|
+
settings,
|
|
1325
|
+
data,
|
|
1326
|
+
selected,
|
|
1327
|
+
past,
|
|
1328
|
+
future,
|
|
1329
|
+
ui
|
|
1330
|
+
]);
|
|
1331
|
+
useEffect(() => {
|
|
1332
|
+
const stops = [
|
|
1333
|
+
channel.handle("editor.data", () => settings.keep(getPuck().appState.data)),
|
|
1334
|
+
channel.handle("editor.undo", () => getPuck().history.back()),
|
|
1335
|
+
channel.handle("editor.redo", () => getPuck().history.forward()),
|
|
1336
|
+
channel.handle("editor.ui", (changes) => {
|
|
1337
|
+
const next = {};
|
|
1338
|
+
if (typeof changes?.leftSideBarVisible === "boolean") next.leftSideBarVisible = changes.leftSideBarVisible;
|
|
1339
|
+
if (typeof changes?.rightSideBarVisible === "boolean") next.rightSideBarVisible = changes.rightSideBarVisible;
|
|
1340
|
+
getPuck().dispatch({
|
|
1341
|
+
type: "setUi",
|
|
1342
|
+
ui: next
|
|
1343
|
+
});
|
|
1344
|
+
}),
|
|
1345
|
+
channel.handle("editor.apply", (change) => {
|
|
1346
|
+
const { appState, dispatch, getSelectorForId } = getPuck();
|
|
1347
|
+
if (change.type === "root") {
|
|
1348
|
+
const { root } = settings.show({
|
|
1349
|
+
...appState.data,
|
|
1350
|
+
root: {
|
|
1351
|
+
...appState.data.root,
|
|
1352
|
+
props: change.props
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
dispatch({
|
|
1356
|
+
type: "replaceRoot",
|
|
1357
|
+
root
|
|
1358
|
+
});
|
|
1359
|
+
} else if (change.type === "translate") dispatch({
|
|
1360
|
+
type: "setData",
|
|
1361
|
+
data: (previous) => settings.show({
|
|
1362
|
+
...previous,
|
|
1363
|
+
root: {
|
|
1364
|
+
...previous.root,
|
|
1365
|
+
props: {
|
|
1366
|
+
...previous.root.props,
|
|
1367
|
+
...change.props
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
content: change.content
|
|
1371
|
+
})
|
|
1372
|
+
});
|
|
1373
|
+
else if (change.type === "replace") {
|
|
1374
|
+
const selector = getSelectorForId(change.id);
|
|
1375
|
+
if (!selector) throw new Error("The block being changed is no longer on the page.");
|
|
1376
|
+
dispatch({
|
|
1377
|
+
type: "replace",
|
|
1378
|
+
destinationIndex: selector.index,
|
|
1379
|
+
destinationZone: selector.zone,
|
|
1380
|
+
data: {
|
|
1381
|
+
...change.block,
|
|
1382
|
+
props: {
|
|
1383
|
+
...change.block.props,
|
|
1384
|
+
id: change.id
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
ui: { itemSelector: selector }
|
|
1388
|
+
});
|
|
1389
|
+
} else if (change.type === "append") dispatch({
|
|
1390
|
+
type: "setData",
|
|
1391
|
+
data: (previous) => ({
|
|
1392
|
+
...previous,
|
|
1393
|
+
content: [...previous.content, ...change.blocks]
|
|
1394
|
+
})
|
|
1395
|
+
});
|
|
1396
|
+
})
|
|
1397
|
+
];
|
|
1398
|
+
return () => {
|
|
1399
|
+
for (const stop of stops) stop();
|
|
1400
|
+
};
|
|
1401
|
+
}, [
|
|
1402
|
+
channel,
|
|
1403
|
+
settings,
|
|
1404
|
+
getPuck
|
|
1405
|
+
]);
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
/** The item editor's sidebars: no blocks, so the left one starts closed; the fields' one starts wider. */
|
|
1409
|
+
const ENTRY_UI = {
|
|
1410
|
+
leftSideBarVisible: false,
|
|
1411
|
+
rightSideBarWidth: 520
|
|
1412
|
+
};
|
|
1413
|
+
const entryPlugins = [legacySideBarPlugin()];
|
|
1414
|
+
/** Puck, set up for one page, header, footer, template or item: site context, previews and the bridge. */
|
|
1415
|
+
function FrameEditor({ open, onOpened }) {
|
|
1416
|
+
const client = useAdminClient();
|
|
1417
|
+
const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
|
|
1418
|
+
const { preview, mediaPreviews } = useAdmin();
|
|
1419
|
+
const { content: loaded } = useSiteContent();
|
|
1420
|
+
const { kind, path, title, data } = open;
|
|
1421
|
+
const collection = open.collection ? loaded.collections.find((candidate) => candidate.id === open.collection) : void 0;
|
|
1422
|
+
const language = open.language ?? (kind === "page" ? pathLanguage(loaded.settings, path) : mainLanguage(loaded.settings));
|
|
1423
|
+
const content = useMemo(() => contentInLanguage(loaded, language), [loaded, language]);
|
|
1424
|
+
const languages = useMemo(() => {
|
|
1425
|
+
if (!hasLanguages(loaded.settings)) return void 0;
|
|
1426
|
+
const page = kind === "page" ? allPages(loaded).find((candidate) => candidate.path === path) : void 0;
|
|
1427
|
+
const home = loaded.pages.find((candidate) => candidate.path === "/");
|
|
1428
|
+
const shown = page ?? home;
|
|
1429
|
+
return shown ? languageLinks(loaded, shown) : void 0;
|
|
1430
|
+
}, [
|
|
1431
|
+
loaded,
|
|
1432
|
+
kind,
|
|
1433
|
+
path
|
|
1434
|
+
]);
|
|
1435
|
+
const dark = useDarkMode();
|
|
1436
|
+
useCanvasStyles(preview, useMemo(() => ({
|
|
1437
|
+
theme: loaded.settings.theme,
|
|
1438
|
+
customCss: loaded.customCss,
|
|
1439
|
+
scheme: dark ? "dark" : "light"
|
|
1440
|
+
}), [
|
|
1441
|
+
loaded.settings.theme,
|
|
1442
|
+
loaded.customCss,
|
|
1443
|
+
dark
|
|
1444
|
+
]));
|
|
1445
|
+
useEffect(() => showMediaInPreview(document, mediaPreviews), [mediaPreviews]);
|
|
1446
|
+
useEffect(() => {
|
|
1447
|
+
const onClick = (event) => {
|
|
1448
|
+
if (event.target?.closest?.(".gf-canvas a[href]")) event.preventDefault();
|
|
1449
|
+
};
|
|
1450
|
+
document.addEventListener("click", onClick);
|
|
1451
|
+
return () => document.removeEventListener("click", onClick);
|
|
1452
|
+
}, []);
|
|
1453
|
+
const migrated = useMemo(() => migrate(data), [data]);
|
|
1454
|
+
const settings = useMemo(() => kind === "page" || kind === "template" ? shownSettings(migrated, kind === "page" ? path : void 0) : {
|
|
1455
|
+
show: (shown) => shown,
|
|
1456
|
+
keep: (kept) => kept
|
|
1457
|
+
}, [
|
|
1458
|
+
migrated,
|
|
1459
|
+
kind,
|
|
1460
|
+
path
|
|
1461
|
+
]);
|
|
1462
|
+
const initialData = useMemo(() => settings.show(migrated), [settings, migrated]);
|
|
1463
|
+
const viewOf = useCallback((next) => kind === "page" && path ? pageView({
|
|
1464
|
+
path,
|
|
1465
|
+
file: "",
|
|
1466
|
+
content: {
|
|
1467
|
+
version: 1,
|
|
1468
|
+
data: storedData(next)
|
|
1469
|
+
}
|
|
1470
|
+
}, content, todayIn(content.settings.timeZone), siteConfig.blocks) : void 0, [
|
|
1471
|
+
kind,
|
|
1472
|
+
path,
|
|
1473
|
+
content,
|
|
1474
|
+
siteConfig.blocks
|
|
1475
|
+
]);
|
|
1476
|
+
const [view, setView] = useState(() => viewOf(initialData));
|
|
1477
|
+
const updateView = (next) => {
|
|
1478
|
+
const found = viewOf(next);
|
|
1479
|
+
setView((current) => current?.block === found?.block ? current : found);
|
|
1480
|
+
};
|
|
1481
|
+
const adminLink = useCallback((place) => {
|
|
1482
|
+
const href = ADMIN_PLACES[place];
|
|
1483
|
+
return {
|
|
1484
|
+
href,
|
|
1485
|
+
ref: (element) => registerOverlayPortal(element, { disableDrag: true }),
|
|
1486
|
+
onClick: (event) => {
|
|
1487
|
+
event.preventDefault();
|
|
1488
|
+
client.call("navigate", href);
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
}, [client]);
|
|
1492
|
+
const site = useMemo(() => ({
|
|
1493
|
+
settings: content.settings,
|
|
1494
|
+
menus: content.menus,
|
|
1495
|
+
path,
|
|
1496
|
+
collections: content.collections,
|
|
1497
|
+
collection,
|
|
1498
|
+
...view && { view },
|
|
1499
|
+
...languages && {
|
|
1500
|
+
languages,
|
|
1501
|
+
home: languageHome(loaded, language)
|
|
1502
|
+
},
|
|
1503
|
+
adminLink
|
|
1504
|
+
}), [
|
|
1505
|
+
content,
|
|
1506
|
+
loaded,
|
|
1507
|
+
language,
|
|
1508
|
+
path,
|
|
1509
|
+
collection,
|
|
1510
|
+
view,
|
|
1511
|
+
languages,
|
|
1512
|
+
adminLink
|
|
1513
|
+
]);
|
|
1514
|
+
const entry = kind === "entry" && collection && open.entry ? findEntry(loaded, collection.id, open.entry) : void 0;
|
|
1515
|
+
const config = useMemo(() => {
|
|
1516
|
+
if (kind === "entry") {
|
|
1517
|
+
if (!collection || !entry) throw new Error("The item isn't in the site.");
|
|
1518
|
+
return entryConfig(collection, entry.entry, content, {
|
|
1519
|
+
templateConfig,
|
|
1520
|
+
layoutConfig
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site);
|
|
1524
|
+
}, [
|
|
1525
|
+
kind,
|
|
1526
|
+
collection,
|
|
1527
|
+
entry,
|
|
1528
|
+
pageConfig,
|
|
1529
|
+
templateConfig,
|
|
1530
|
+
layoutConfig,
|
|
1531
|
+
content,
|
|
1532
|
+
site
|
|
1533
|
+
]);
|
|
1534
|
+
const described = useMemo(() => describeConfig(config), [config]);
|
|
1535
|
+
const told = useRef(void 0);
|
|
1536
|
+
useEffect(() => {
|
|
1537
|
+
if (told.current === described) return;
|
|
1538
|
+
told.current = described;
|
|
1539
|
+
onOpened({
|
|
1540
|
+
config: described,
|
|
1541
|
+
data: migrated
|
|
1542
|
+
});
|
|
1543
|
+
}, [
|
|
1544
|
+
described,
|
|
1545
|
+
migrated,
|
|
1546
|
+
onOpened
|
|
1547
|
+
]);
|
|
1548
|
+
const ui = useMemo(() => ({
|
|
1549
|
+
...kind === "entry" ? ENTRY_UI : {},
|
|
1550
|
+
...open.ui
|
|
1551
|
+
}), [kind, open.ui]);
|
|
1552
|
+
const overrides = useMemo(() => ({
|
|
1553
|
+
header: () => /* @__PURE__ */ jsx(Bridge, {
|
|
1554
|
+
channel: client.channel,
|
|
1555
|
+
settings
|
|
1556
|
+
}),
|
|
1557
|
+
fieldTypes: { text: TextFieldWithMedia },
|
|
1558
|
+
...kind === "page" && path && { fields: ({ children, itemSelector }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [children, !itemSelector && /* @__PURE__ */ jsx(SharePreview, { path })] }) }
|
|
1559
|
+
}), [
|
|
1560
|
+
client,
|
|
1561
|
+
settings,
|
|
1562
|
+
kind,
|
|
1563
|
+
path
|
|
1564
|
+
]);
|
|
1565
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1566
|
+
className: `gfa-editor-puck gfa-editor-for-${kind}`,
|
|
1567
|
+
children: /* @__PURE__ */ jsx(SiteProvider, {
|
|
1568
|
+
value: site,
|
|
1569
|
+
children: /* @__PURE__ */ jsx(Puck, {
|
|
1570
|
+
config,
|
|
1571
|
+
data: initialData,
|
|
1572
|
+
metadata: siteMetadata(site),
|
|
1573
|
+
headerTitle: title,
|
|
1574
|
+
height: "100%",
|
|
1575
|
+
iframe: { enabled: false },
|
|
1576
|
+
ui,
|
|
1577
|
+
...kind === "entry" && { plugins: entryPlugins },
|
|
1578
|
+
overrides,
|
|
1579
|
+
onChange: updateView
|
|
1580
|
+
})
|
|
1581
|
+
})
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
//#endregion
|
|
1585
|
+
//#region src/frame/render.tsx
|
|
1586
|
+
/**
|
|
1587
|
+
* A page of the site shown in a frame, as the live site would show it: for
|
|
1588
|
+
* Site Settings' and Version history's previews, an item's preview and the
|
|
1589
|
+
* preview page tools take screenshots with.
|
|
1590
|
+
*/
|
|
1591
|
+
/** Styles for before a page is shown, which aren't put in the page. */
|
|
1592
|
+
const NOTHING_SHOWN = {
|
|
1593
|
+
theme: {
|
|
1594
|
+
colors: {},
|
|
1595
|
+
fonts: {}
|
|
1596
|
+
},
|
|
1597
|
+
customCss: ""
|
|
1598
|
+
};
|
|
1599
|
+
function FrameRender({ channel }) {
|
|
1600
|
+
const { code, pageConfig, layoutConfig, templateConfig } = useFrameCode();
|
|
1601
|
+
const [shown, setShown] = useState();
|
|
1602
|
+
const rendered = useRef(void 0);
|
|
1603
|
+
useEffect(() => channel.handle("render.show", async (show) => {
|
|
1604
|
+
const { content } = show;
|
|
1605
|
+
const today = todayIn(content.settings.timeZone);
|
|
1606
|
+
const match = await findPage(code, content, today, show.path);
|
|
1607
|
+
if (!match) return {
|
|
1608
|
+
ok: false,
|
|
1609
|
+
problem: `The site has no page at ${show.path}.`
|
|
1610
|
+
};
|
|
1611
|
+
const prepared = await preparePage({
|
|
1612
|
+
page: pageConfig,
|
|
1613
|
+
layout: layoutConfig,
|
|
1614
|
+
template: templateConfig
|
|
1615
|
+
}, content, match.page);
|
|
1616
|
+
await new Promise((done) => {
|
|
1617
|
+
rendered.current = done;
|
|
1618
|
+
setShown({
|
|
1619
|
+
show,
|
|
1620
|
+
prepared
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
return {
|
|
1624
|
+
ok: true,
|
|
1625
|
+
status: match.status,
|
|
1626
|
+
height: document.documentElement.scrollHeight
|
|
1627
|
+
};
|
|
1628
|
+
}), [
|
|
1629
|
+
channel,
|
|
1630
|
+
code,
|
|
1631
|
+
pageConfig,
|
|
1632
|
+
layoutConfig,
|
|
1633
|
+
templateConfig
|
|
1634
|
+
]);
|
|
1635
|
+
const styles = useMemo(() => shown && {
|
|
1636
|
+
theme: shown.show.content.settings.theme,
|
|
1637
|
+
customCss: shown.show.content.customCss,
|
|
1638
|
+
...shown.show.scheme && { scheme: shown.show.scheme }
|
|
1639
|
+
}, [shown]);
|
|
1640
|
+
usePreviewStyles(styles ? document : void 0, styles ?? NOTHING_SHOWN);
|
|
1641
|
+
useEffect(() => {
|
|
1642
|
+
if (!shown) return;
|
|
1643
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
1644
|
+
rendered.current?.();
|
|
1645
|
+
rendered.current = void 0;
|
|
1646
|
+
}));
|
|
1647
|
+
}, [shown]);
|
|
1648
|
+
useEffect(() => {
|
|
1649
|
+
let last = 0;
|
|
1650
|
+
const observer = new ResizeObserver(() => {
|
|
1651
|
+
const height = document.documentElement.scrollHeight;
|
|
1652
|
+
if (height === last) return;
|
|
1653
|
+
last = height;
|
|
1654
|
+
channel.emit("render.height", height);
|
|
1655
|
+
});
|
|
1656
|
+
observer.observe(document.body);
|
|
1657
|
+
return () => observer.disconnect();
|
|
1658
|
+
}, [channel]);
|
|
1659
|
+
if (!shown) return null;
|
|
1660
|
+
const { prepared } = shown;
|
|
1661
|
+
return createPortal(/* @__PURE__ */ jsx(PageBody, {
|
|
1662
|
+
site: prepared.site,
|
|
1663
|
+
pageConfig: prepared.pageConfig,
|
|
1664
|
+
layoutConfig: prepared.layoutConfig,
|
|
1665
|
+
page: prepared.data,
|
|
1666
|
+
header: prepared.header,
|
|
1667
|
+
footer: prepared.footer
|
|
1668
|
+
}), document.body);
|
|
1669
|
+
}
|
|
1670
|
+
//#endregion
|
|
1671
|
+
//#region src/frame/app.tsx
|
|
1672
|
+
/** Answers for the site's code, for the admin panel's screens. */
|
|
1673
|
+
function CodeService({ channel }) {
|
|
1674
|
+
const { code } = useFrameCode();
|
|
1675
|
+
useEffect(() => {
|
|
1676
|
+
const stops = [
|
|
1677
|
+
channel.handle("code.sitePages", ({ content, today }) => code.sitePages(content, today)),
|
|
1678
|
+
channel.handle("code.pageView", async ({ page, content, today }) => await code.pageView(page, content, today) ?? null),
|
|
1679
|
+
channel.handle("code.renderedProblems", ({ content, targets, today }) => code.renderedProblems(content, targets, today))
|
|
1680
|
+
];
|
|
1681
|
+
return () => {
|
|
1682
|
+
for (const stop of stops) stop();
|
|
1683
|
+
};
|
|
1684
|
+
}, [channel, code]);
|
|
1685
|
+
return null;
|
|
1686
|
+
}
|
|
1687
|
+
/** Which editor is open: another one starts Puck afresh. */
|
|
1688
|
+
function editorKey(open) {
|
|
1689
|
+
return JSON.stringify([
|
|
1690
|
+
open.kind,
|
|
1691
|
+
open.path,
|
|
1692
|
+
open.collection,
|
|
1693
|
+
open.entry,
|
|
1694
|
+
open.language
|
|
1695
|
+
]);
|
|
1696
|
+
}
|
|
1697
|
+
/** Opens an editor when the admin panel's page asks, and tells it Puck's config once it's open. */
|
|
1698
|
+
function EditorService({ channel }) {
|
|
1699
|
+
const [opened, setOpened] = useState();
|
|
1700
|
+
useEffect(() => channel.handle("editor.open", (open) => new Promise((resolve) => {
|
|
1701
|
+
setOpened({
|
|
1702
|
+
open,
|
|
1703
|
+
told: resolve
|
|
1704
|
+
});
|
|
1705
|
+
})), [channel]);
|
|
1706
|
+
const onOpened = useCallback((editor) => opened?.told(editor), [opened]);
|
|
1707
|
+
useEffect(() => channel.handle("editor.viewport", (width) => {
|
|
1708
|
+
const style = document.documentElement.style;
|
|
1709
|
+
if (typeof width === "number" && width > 0) style.setProperty("--gf-canvas-width", `${Math.round(width)}px`);
|
|
1710
|
+
else style.removeProperty("--gf-canvas-width");
|
|
1711
|
+
}), [channel]);
|
|
1712
|
+
return opened ? /* @__PURE__ */ jsx(FrameEditor, {
|
|
1713
|
+
open: opened.open,
|
|
1714
|
+
onOpened
|
|
1715
|
+
}, editorKey(opened.open)) : null;
|
|
1716
|
+
}
|
|
1717
|
+
function FrameApp({ config, channel, init, onReady }) {
|
|
1718
|
+
const [site, setSite] = useState(init);
|
|
1719
|
+
const client = useState(() => adminClient(channel))[0];
|
|
1720
|
+
useEffect(() => {
|
|
1721
|
+
const stops = [channel.handle("site", (next) => setSite(next)), channel.handle("theme", (theme) => setThemeChoice(theme))];
|
|
1722
|
+
return () => {
|
|
1723
|
+
for (const stop of stops) stop();
|
|
1724
|
+
};
|
|
1725
|
+
}, [channel]);
|
|
1726
|
+
const ready = useRef(onReady);
|
|
1727
|
+
useEffect(() => ready.current(), []);
|
|
1728
|
+
return /* @__PURE__ */ jsxs(FrameProviders, {
|
|
1729
|
+
config,
|
|
1730
|
+
client,
|
|
1731
|
+
init,
|
|
1732
|
+
site,
|
|
1733
|
+
children: [
|
|
1734
|
+
init.mode === "editor" && /* @__PURE__ */ jsx(EditorService, { channel }),
|
|
1735
|
+
init.mode === "render" && /* @__PURE__ */ jsx(FrameRender, { channel }),
|
|
1736
|
+
init.mode === "code" && /* @__PURE__ */ jsx(CodeService, { channel })
|
|
1737
|
+
]
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
function mountEditorFrame(config, element, options = {}) {
|
|
1741
|
+
document.documentElement.classList.add("gfa-frame");
|
|
1742
|
+
let container = element;
|
|
1743
|
+
if (!container) {
|
|
1744
|
+
container = document.createElement("div");
|
|
1745
|
+
container.id = "goodfellow-frame";
|
|
1746
|
+
document.body.append(container);
|
|
1747
|
+
}
|
|
1748
|
+
const root = createRoot(container);
|
|
1749
|
+
let connected = false;
|
|
1750
|
+
const onMessage = (event) => {
|
|
1751
|
+
const port = event.ports[0];
|
|
1752
|
+
if (connected || event.source !== window.parent || !port) return;
|
|
1753
|
+
if (event.data?.type !== "goodfellow:connect") return;
|
|
1754
|
+
connected = true;
|
|
1755
|
+
window.removeEventListener("message", onMessage);
|
|
1756
|
+
const channel = createChannel(port);
|
|
1757
|
+
channel.handle("init", (given) => new Promise((resolve) => {
|
|
1758
|
+
const init = options.tailwindBrowserUrl ? {
|
|
1759
|
+
...given,
|
|
1760
|
+
preview: {
|
|
1761
|
+
...given.preview,
|
|
1762
|
+
tailwindBrowserUrl: options.tailwindBrowserUrl
|
|
1763
|
+
}
|
|
1764
|
+
} : given;
|
|
1765
|
+
setThemeChoice(init.theme);
|
|
1766
|
+
document.documentElement.classList.add(`gfa-frame-${init.mode}`);
|
|
1767
|
+
if (init.siteCss) installSiteCss(init.siteCss);
|
|
1768
|
+
root.render(/* @__PURE__ */ jsx(FrameApp, {
|
|
1769
|
+
config,
|
|
1770
|
+
channel,
|
|
1771
|
+
init,
|
|
1772
|
+
onReady: resolve
|
|
1773
|
+
}));
|
|
1774
|
+
}));
|
|
1775
|
+
};
|
|
1776
|
+
window.addEventListener("message", onMessage);
|
|
1777
|
+
window.parent.postMessage(READY_MESSAGE, "*");
|
|
1778
|
+
}
|
|
1779
|
+
//#endregion
|
|
1780
|
+
export { mountEditorFrame };
|