@goodfellow-cms/admin 0.4.0 → 0.6.0

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