@goodfellow-cms/admin 0.4.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/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 };