@goodfellow-cms/admin 0.8.0 → 0.10.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 CHANGED
@@ -1,11 +1,12 @@
1
- import { J as editedMarkdown, U as storedData, W as storedEntryFields, Y as formattedHtml, g as mediaUrl, i as showMediaInPreview, n as createMediaPreviews, o as createChannel } from "./plugin-protocol-BgYL9WSm.js";
2
- import { J as EditorTextContext, T as SETTINGS_SECTIONS, Y as useEditorText, a as AdminContext, b as MediaChoiceContext, d as setThemeChoice, g as ScheduleFields, i as usePreviewStyles, it as StringsProvider, l as useSiteContent, lt as findPage, p as useDarkMode, q as READY_MESSAGE, s as useAdmin, st as useStrings, t as SCHEDULE_PROP, x as MediaChooser } from "./entry-values-Bu8ljnoX.js";
3
- import { COLOR_SCHEME_ATTRIBUTE, DEFAULT_THEME_COLORS, DRAWN_SLOT_ELEMENT, FONT_SIZES, MEDIA_DIR, MINIMUM_CONTRAST, RADII, SCREEN_SIZES, SIDES, SPACING_STEPS, TEXT_ALIGNS, THEME_COLORS, WEEKDAYS, allPages, canFormatMarkdown, cleanEventValue, colorSlug, contentInLanguage, contrastRatio, customLength, datePart, escapeStyleText, findEntry, formatDate, formatOccurrence, getPageHead, googleFontsUrl, hasLanguages, isAllDay, isCustomColor, isEventLike, isOwnersOnlyField, languageHome, languageLinks, mainLanguage, neverIndexed, pageLinks, pageView, pathLanguage, readBlockStyle, renderedProblems, shownOccurrence, sitePages, themeColorOf, themeToCss, todayIn, wallClock, weekdayName, weekdayOf, withBlockStyle } from "@goodfellow-cms/core";
1
+ import { $ as formattedHtml, K as storedData, Q as editedMarkdown, _ as mediaUrl, a as showMediaInPreview, q as storedEntryFields, r as createMediaPreviews, s as createChannel, t as installCustomFonts } from "./custom-font-faces-DCan2jgW.js";
2
+ import { C as MediaChoiceContext, D as MediaThumb, G as isForwardedShortcut, H as READY_MESSAGE, J as shortcutFor, K as isTyping, X as SETTINGS_SECTIONS, c as AdminContext, f as useSiteContent, g as useDarkMode, i as usePreviewStyles, lt as Button, m as setThemeChoice, ot as EditorTextContext, rt as syncedBlockEditorHref, s as withBlocksInPlace, st as useEditorText, t as SCHEDULE_PROP, u as useAdmin, w as MediaChooser, y as ScheduleFields, yt as findPage } from "./entry-values-CAPB0igb.js";
3
+ import { i as useStrings, t as StringsProvider } from "./strings-CgC6pd6k.js";
4
+ import { COLOR_SCHEME_ATTRIBUTE, DEFAULT_THEME_COLORS, DRAWN_SLOT_ELEMENT, FONT_SIZES, MEDIA_DIR, MINIMUM_CONTRAST, RADII, SCREEN_SIZES, SIDES, SPACING_STEPS, SYNCED_BLOCK, SYNCED_BLOCK_PROP, TEXT_ALIGNS, THEME_COLORS, WEEKDAYS, allPages, canFormatMarkdown, cleanEventValue, colorSlug, contentInLanguage, contrastRatio, customFonts, customLength, datePart, describedIn, entryTitle, escapeCellText, escapeStyleText, expandSyncedBlocks, findEntry, findSyncedBlock, focusPosition, formatDate, formatOccurrence, getPageHead, googleFontsUrl, hasLanguages, isAllDay, isCustomColor, isEventLike, isOwnersOnlyField, isSafeUrl, languageHome, languageLinks, mainLanguage, neverIndexed, pageLinks, pageView, pathLanguage, readBlockStyle, renderedProblems, shownOccurrence, sitePages, syncedBlockIn, tableCellHtml, themeColorOf, themeToCss, todayIn, wallClock, weekdayName, weekdayOf, withBlockStyle } from "@goodfellow-cms/core";
4
5
  import { Box } from "lucide-react";
5
- import { createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
6
+ import { createContext, createElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
7
  import { createRoot } from "react-dom/client";
7
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
- import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
9
+ import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, pageHeadings, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
9
10
  import { AutoField, Drawer, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
10
11
  import { createPortal, flushSync } from "react-dom";
11
12
  const CANVAS = `.gf-canvas`;
@@ -190,14 +191,16 @@ function installSiteCss(css) {
190
191
  }
191
192
  /**
192
193
  * The theme, fonts, custom CSS and Tailwind for the canvas, as the site
193
- * shows them, light or dark as `scheme` says.
194
+ * shows them, light or dark as `scheme` says. The site's own fonts come from
195
+ * their bytes, through `media`, since the frame can't load the site's files.
194
196
  */
195
- function useCanvasStyles(preview, styles) {
197
+ function useCanvasStyles(preview, styles, media) {
196
198
  const { theme, customCss, scheme } = styles;
197
199
  useEffect(() => scopeTailwindOutput(), []);
198
200
  useEffect(() => {
199
201
  document.documentElement.setAttribute(COLOR_SCHEME_ATTRIBUTE, theme.darkMode ? scheme : "light");
200
- styleElement("theme").textContent = escapeStyleText(scopeCss(themeToCss(theme)));
202
+ styleElement("theme").textContent = escapeStyleText(scopeCss(themeToCss(theme, void 0, { fontFaces: false })));
203
+ installCustomFonts(document, customFonts(theme), (url) => media.bytes(url));
201
204
  const fonts = googleFontsUrl(theme);
202
205
  const link = document.head.querySelector("link[data-gf=\"fonts\"]");
203
206
  if (fonts) {
@@ -207,7 +210,11 @@ function useCanvasStyles(preview, styles) {
207
210
  element.href = fonts;
208
211
  if (!link) document.head.append(element);
209
212
  } else link?.remove();
210
- }, [theme, scheme]);
213
+ }, [
214
+ theme,
215
+ scheme,
216
+ media
217
+ ]);
211
218
  useEffect(() => {
212
219
  const text = escapeStyleText(`${preview.themeCss}\n${customCss}`);
213
220
  const current = document.head.querySelector("style[data-gf=\"tailwind\"]");
@@ -234,7 +241,8 @@ const usePuck$2 = createUsePuck();
234
241
  /**
235
242
  * How the page looks when a link to it is shared, such as in a message or on
236
243
  * social media: its title, description and image as link previews show them,
237
- * from the page's settings or else Site Settings', updated as they're edited.
244
+ * from the page's settings or else Site Settings', updated as they're edited,
245
+ * with the picture large over the title or small beside it, as its settings say.
238
246
  */
239
247
  function SharePreview({ path }) {
240
248
  const t = useStrings();
@@ -265,7 +273,7 @@ function SharePreview({ path }) {
265
273
  children: t("sharePreview.title")
266
274
  }),
267
275
  /* @__PURE__ */ jsxs("div", {
268
- className: "gfa-share-card",
276
+ className: head.card === "summary" ? "gfa-share-card gfa-share-card-small" : "gfa-share-card",
269
277
  children: [image ? /* @__PURE__ */ jsx("img", {
270
278
  src: image,
271
279
  alt: "",
@@ -709,6 +717,135 @@ function EventField({ id, label, hint, value, onChange, language, timeZone }) {
709
717
  });
710
718
  }
711
719
  //#endregion
720
+ //#region src/item-fields.tsx
721
+ /**
722
+ * The item editor's controls for links to items and for several images: a
723
+ * searchable list of another collection's items, and a list of images to add
724
+ * to, order and take out.
725
+ */
726
+ /** How many matches the item picker lists at once; searching narrows the rest. */
727
+ const SHOWN_MATCHES = 50;
728
+ /** The items a link can name: the collection's own, never translations, which links reach through the item they translate. */
729
+ function linkableItems(collection) {
730
+ return (collection?.entries ?? []).filter((entry) => entry.content.translationOf === void 0).map((entry) => ({
731
+ slug: entry.slug,
732
+ title: entryTitle(entry)
733
+ })).sort((a, b) => a.title.localeCompare(b.title));
734
+ }
735
+ /** Chooses one item, or several, of a collection, by searching their titles. */
736
+ function ItemPicker({ id, collection, multiple, value, onChange }) {
737
+ const t = useStrings();
738
+ const listId = useId();
739
+ const [search, setSearch] = useState("");
740
+ const items = linkableItems(collection);
741
+ const chosen = typeof value === "string" ? value ? [value] : [] : Array.isArray(value) ? value.map(String) : [];
742
+ const name = collection?.settings.name ?? "";
743
+ const titleOf = (slug) => items.find((item) => item.slug === slug)?.title ?? slug;
744
+ const words = search.trim().toLowerCase();
745
+ const matches = items.filter((item) => !chosen.includes(item.slug) && (!words || item.title.toLowerCase().includes(words)));
746
+ const set = (next) => onChange(multiple ? next.length > 0 ? next : void 0 : next[0]);
747
+ if (!collection) return /* @__PURE__ */ jsx("p", {
748
+ className: "gfa-puck-hint",
749
+ children: t("editEntry.items.missing")
750
+ });
751
+ if (items.length === 0) return /* @__PURE__ */ jsx("p", {
752
+ className: "gfa-puck-hint",
753
+ children: t("editEntry.items.empty", { name })
754
+ });
755
+ return /* @__PURE__ */ jsxs("div", {
756
+ className: "gfa-item-picker",
757
+ children: [chosen.length > 0 && /* @__PURE__ */ jsx("ul", {
758
+ className: "gfa-item-picker-chosen",
759
+ children: chosen.map((slug) => /* @__PURE__ */ jsxs("li", {
760
+ className: "gfa-chosen-row",
761
+ children: [/* @__PURE__ */ jsx("span", {
762
+ className: "gfa-chosen-row-name",
763
+ children: titleOf(slug)
764
+ }), /* @__PURE__ */ jsx(Button, {
765
+ variant: "ghost",
766
+ "aria-label": t("editEntry.items.remove", { title: titleOf(slug) }),
767
+ onClick: () => set(chosen.filter((other) => other !== slug)),
768
+ children: "×"
769
+ })]
770
+ }, slug))
771
+ }), (multiple || chosen.length === 0) && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("input", {
772
+ id,
773
+ type: "search",
774
+ className: "gfa-puck-input",
775
+ placeholder: t("editEntry.items.search", { name }),
776
+ "aria-controls": listId,
777
+ value: search,
778
+ onChange: (event) => setSearch(event.target.value)
779
+ }), /* @__PURE__ */ jsxs("ul", {
780
+ id: listId,
781
+ className: "gfa-item-picker-matches",
782
+ children: [matches.slice(0, SHOWN_MATCHES).map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
783
+ type: "button",
784
+ onClick: () => {
785
+ set([...chosen, item.slug]);
786
+ setSearch("");
787
+ },
788
+ children: item.title
789
+ }) }, item.slug)), matches.length === 0 && /* @__PURE__ */ jsx("li", {
790
+ className: "gfa-puck-hint",
791
+ children: t("editEntry.items.none", { name })
792
+ })]
793
+ })] })]
794
+ });
795
+ }
796
+ /** Several images, in order: each can move up or down or be taken out, and the media chooser adds more. */
797
+ function ImagesField({ value, onChange }) {
798
+ const t = useStrings();
799
+ const images = Array.isArray(value) ? value.filter((item) => typeof item === "string" && !!item) : [];
800
+ const set = (next) => onChange(next.length > 0 ? next : void 0);
801
+ const move = (index, by) => {
802
+ const next = [...images];
803
+ const [moved] = next.splice(index, 1);
804
+ next.splice(index + by, 0, moved);
805
+ set(next);
806
+ };
807
+ return /* @__PURE__ */ jsxs("div", {
808
+ className: "gfa-images-field",
809
+ children: [images.length > 0 && /* @__PURE__ */ jsx("ul", { children: images.map((src, index) => /* @__PURE__ */ jsxs("li", {
810
+ className: "gfa-chosen-row",
811
+ children: [
812
+ /* @__PURE__ */ jsx(MediaThumb, {
813
+ url: src,
814
+ className: "gfa-images-field-thumb"
815
+ }),
816
+ /* @__PURE__ */ jsx("span", {
817
+ className: "gfa-chosen-row-name",
818
+ children: src.slice(src.lastIndexOf("/") + 1)
819
+ }),
820
+ /* @__PURE__ */ jsx(Button, {
821
+ variant: "ghost",
822
+ disabled: index === 0,
823
+ "aria-label": t("editEntry.images.up"),
824
+ onClick: () => move(index, -1),
825
+ children: "↑"
826
+ }),
827
+ /* @__PURE__ */ jsx(Button, {
828
+ variant: "ghost",
829
+ disabled: index === images.length - 1,
830
+ "aria-label": t("editEntry.images.down"),
831
+ onClick: () => move(index, 1),
832
+ children: "↓"
833
+ }),
834
+ /* @__PURE__ */ jsx(Button, {
835
+ variant: "ghost",
836
+ "aria-label": t("editEntry.images.remove"),
837
+ onClick: () => set(images.filter((_, other) => other !== index)),
838
+ children: "×"
839
+ })
840
+ ]
841
+ }, src)) }), /* @__PURE__ */ jsx(MediaChooser, {
842
+ value: "",
843
+ kind: "image",
844
+ onChange: (src) => src && !images.includes(src) && set([...images, src])
845
+ })]
846
+ });
847
+ }
848
+ //#endregion
712
849
  //#region src/markdown-field.tsx
713
850
  /**
714
851
  * Puck's formatted editor, without what Markdown can't store: underlining and
@@ -839,7 +976,7 @@ function Hint({ text }) {
839
976
  }) : null;
840
977
  }
841
978
  /** What the AI assistant is told a field holds, since it can't tell from a custom field. */
842
- function aiHint(field, markdown) {
979
+ function aiHint(field, markdown, collections) {
843
980
  const description = field.hint;
844
981
  if (markdown) return {
845
982
  type: "string",
@@ -864,6 +1001,25 @@ function aiHint(field, markdown) {
864
1001
  options: field.options ?? [],
865
1002
  description
866
1003
  };
1004
+ case "yesno": return {
1005
+ type: "boolean",
1006
+ description
1007
+ };
1008
+ case "item": {
1009
+ const options = linkableItems(collections.find((candidate) => candidate.id === field.collection)).slice(0, MAX_AI_ITEMS).map((item) => ({
1010
+ value: item.slug,
1011
+ label: item.title
1012
+ }));
1013
+ if (options.length === 0) return void 0;
1014
+ return {
1015
+ type: field.multiple ? "choices" : "choice",
1016
+ options,
1017
+ description
1018
+ };
1019
+ }
1020
+ case "images":
1021
+ case "file":
1022
+ case "group": return;
867
1023
  case "link": return {
868
1024
  type: "string",
869
1025
  description: [description, "A web address or a page address such as /about"].filter(Boolean).join(". ")
@@ -878,16 +1034,102 @@ function aiHint(field, markdown) {
878
1034
  };
879
1035
  }
880
1036
  }
1037
+ /** The most items the assistant is offered to link to, so a large collection doesn't fill its request. */
1038
+ const MAX_AI_ITEMS = 200;
881
1039
  /** The Puck field for one of a collection's fields, shown in the editor's sidebar. */
882
- function puckField(field, collection, settings) {
1040
+ function puckField(field, collection, content) {
883
1041
  const markdown = collection.settings.markdown?.body === field.name;
884
- const control = fieldControl(field, markdown, settings);
885
- return field.type === "event" ? control : {
1042
+ const control = fieldControl(field, markdown, content);
1043
+ const ai = field.type === "event" ? void 0 : aiHint(field, markdown, content.collections);
1044
+ return ai ? {
886
1045
  ...control,
887
- metadata: { ai: aiHint(field, markdown) }
888
- };
1046
+ metadata: { ai }
1047
+ } : control;
1048
+ }
1049
+ /** A text field for an address, with the media chooser below it. */
1050
+ function addressControl(label, hint, kind) {
1051
+ return { render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
1052
+ label,
1053
+ children: [
1054
+ /* @__PURE__ */ jsx(AutoField, {
1055
+ id,
1056
+ field: { type: "text" },
1057
+ value: typeof value === "string" ? value : "",
1058
+ onChange
1059
+ }),
1060
+ /* @__PURE__ */ jsx(MediaChooser, {
1061
+ value: typeof value === "string" ? value : "",
1062
+ kind,
1063
+ onChange
1064
+ }),
1065
+ /* @__PURE__ */ jsx(Hint, { text: hint })
1066
+ ]
1067
+ }) };
1068
+ }
1069
+ /** A checkbox for a yes or no field, which stores nothing when it's off. */
1070
+ function YesNo({ id, label, hint, value, onChange }) {
1071
+ return /* @__PURE__ */ jsxs("div", {
1072
+ className: "gfa-puck-field",
1073
+ children: [/* @__PURE__ */ jsxs("label", {
1074
+ className: "gfa-checkbox",
1075
+ children: [/* @__PURE__ */ jsx("input", {
1076
+ id,
1077
+ type: "checkbox",
1078
+ checked: value === true,
1079
+ onChange: (event) => onChange(event.target.checked ? true : void 0)
1080
+ }), label]
1081
+ }), /* @__PURE__ */ jsx(Hint, { text: hint })]
1082
+ });
1083
+ }
1084
+ /** One of a repeating group's fields, as the group's rows show it. */
1085
+ function groupFieldControl(field) {
1086
+ switch (field.type) {
1087
+ case "textarea": return {
1088
+ type: "textarea",
1089
+ label: field.label
1090
+ };
1091
+ case "number": return {
1092
+ type: "number",
1093
+ label: field.label
1094
+ };
1095
+ case "date": return {
1096
+ type: "custom",
1097
+ label: field.label,
1098
+ render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(FieldLabel, {
1099
+ label: field.label,
1100
+ children: /* @__PURE__ */ jsx("input", {
1101
+ id,
1102
+ type: "date",
1103
+ className: "gfa-puck-input",
1104
+ value: typeof value === "string" ? value : "",
1105
+ onChange: (event) => onChange(event.target.value)
1106
+ })
1107
+ })
1108
+ };
1109
+ case "yesno": return {
1110
+ type: "custom",
1111
+ label: field.label,
1112
+ render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(YesNo, {
1113
+ id,
1114
+ label: field.label,
1115
+ value,
1116
+ onChange
1117
+ })
1118
+ };
1119
+ case "image":
1120
+ case "link": return {
1121
+ type: "custom",
1122
+ label: field.label,
1123
+ ...addressControl(field.label, void 0, field.type === "image" ? "image" : "file")
1124
+ };
1125
+ default: return {
1126
+ type: "text",
1127
+ label: field.label
1128
+ };
1129
+ }
889
1130
  }
890
- function fieldControl(field, markdown, settings) {
1131
+ function fieldControl(field, markdown, content) {
1132
+ const { settings } = content;
891
1133
  const label = field.required ? `${field.label} *` : field.label;
892
1134
  if (markdown) return {
893
1135
  type: "custom",
@@ -965,6 +1207,59 @@ function fieldControl(field, markdown, settings) {
965
1207
  });
966
1208
  }
967
1209
  };
1210
+ case "yesno": return {
1211
+ type: "custom",
1212
+ label,
1213
+ render: ({ value, onChange, id }) => /* @__PURE__ */ jsx(YesNo, {
1214
+ id,
1215
+ label,
1216
+ hint: field.hint,
1217
+ value,
1218
+ onChange
1219
+ })
1220
+ };
1221
+ case "item": return {
1222
+ type: "custom",
1223
+ label,
1224
+ render: ({ value, onChange, id }) => /* @__PURE__ */ jsxs(FieldLabel, {
1225
+ label,
1226
+ el: "div",
1227
+ children: [/* @__PURE__ */ jsx(ItemPicker, {
1228
+ id,
1229
+ collection: content.collections.find((candidate) => candidate.id === field.collection),
1230
+ multiple: field.multiple === true,
1231
+ value,
1232
+ onChange
1233
+ }), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
1234
+ })
1235
+ };
1236
+ case "images": return {
1237
+ type: "custom",
1238
+ label,
1239
+ render: ({ value, onChange }) => /* @__PURE__ */ jsxs(FieldLabel, {
1240
+ label,
1241
+ el: "div",
1242
+ children: [/* @__PURE__ */ jsx(ImagesField, {
1243
+ value,
1244
+ onChange
1245
+ }), /* @__PURE__ */ jsx(Hint, { text: field.hint })]
1246
+ })
1247
+ };
1248
+ case "file": return {
1249
+ type: "custom",
1250
+ label,
1251
+ ...addressControl(label, field.hint, "file")
1252
+ };
1253
+ case "group": {
1254
+ const subFields = field.fields ?? [];
1255
+ const first = subFields[0]?.name;
1256
+ return {
1257
+ type: "array",
1258
+ label,
1259
+ arrayFields: Object.fromEntries(subFields.map((sub) => [sub.name, groupFieldControl(sub)])),
1260
+ getItemSummary: (row, index) => first && typeof row?.[first] === "string" && row[first] || `${(index ?? 0) + 1}`
1261
+ };
1262
+ }
968
1263
  case "event": return {
969
1264
  type: "custom",
970
1265
  label,
@@ -1041,12 +1336,11 @@ function EntryPreview({ collection, entry, content, templateConfig, layoutConfig
1041
1336
  current
1042
1337
  ]);
1043
1338
  const metadata = useMemo(() => siteMetadata(site), [site]);
1044
- const applied = useMemo(() => applyEntry(migrate(collection.settings.template), templateConfig, collection, current, content.settings.language, content.settings.timeZone), [
1339
+ const applied = useMemo(() => applyEntry(expandSyncedBlocks(migrate(collection.settings.template), content, content.settings.language), templateConfig, collection, current, content.settings.language, content.settings.timeZone, content.collections), [
1045
1340
  collection,
1046
1341
  templateConfig,
1047
1342
  current,
1048
- content.settings.language,
1049
- content.settings.timeZone
1343
+ content
1050
1344
  ]);
1051
1345
  const [resolved, setResolved] = useState(applied);
1052
1346
  useEffect(() => {
@@ -1076,8 +1370,8 @@ function EntryPreview({ collection, entry, content, templateConfig, layoutConfig
1076
1370
  value: site,
1077
1371
  children: /* @__PURE__ */ jsx(SiteFrame, {
1078
1372
  layoutConfig,
1079
- header: content.header.data,
1080
- footer: content.footer.data,
1373
+ header: expandSyncedBlocks(content.header.data, content, content.settings.language),
1374
+ footer: expandSyncedBlocks(content.footer.data, content, content.settings.language),
1081
1375
  site,
1082
1376
  className: typeof className === "string" ? className : void 0,
1083
1377
  children: /* @__PURE__ */ jsx(Render, {
@@ -1104,7 +1398,7 @@ function entryConfig(collection, entry, content, configs) {
1104
1398
  return {
1105
1399
  components: {},
1106
1400
  root: {
1107
- fields: Object.fromEntries([...collection.settings.fields.map((field) => [field.name, puckField(field, collection, content.settings)]), [SCHEDULE_PROP, scheduleField(content)]]),
1401
+ fields: Object.fromEntries([...collection.settings.fields.map((field) => [field.name, puckField(field, collection, content)]), [SCHEDULE_PROP, scheduleField(content)]]),
1108
1402
  render: () => /* @__PURE__ */ jsx("div", {
1109
1403
  className: "gf-canvas",
1110
1404
  children: /* @__PURE__ */ jsx(EntryPreview, {
@@ -1149,25 +1443,56 @@ function withSlotPlaces(block, config) {
1149
1443
  }
1150
1444
  };
1151
1445
  }
1152
- /** One block drawn as the site draws it, in the page at its address, as `drawBlock()` describes. */
1153
- async function drawBlock(configs, content, { kind, path, block }) {
1446
+ /** The config a target's block is in, and the page it's on, with that block alone. */
1447
+ function targetPage(configs, content, { kind, path, block }) {
1154
1448
  const layout = kind === "header" || kind === "footer";
1155
1449
  const config = layout ? configs.layout : kind === "template" ? configs.template : configs.page;
1156
1450
  if (!config.components[block.type]) throw new Error(`The site has no block called ${block.type} here.`);
1157
1451
  const found = allPages(content).find((page) => page.path === path);
1158
1452
  const root = layout ? (kind === "header" ? content.header : content.footer).data.root : found?.content.data.root;
1159
- const page = {
1160
- path,
1161
- file: found?.file ?? "",
1162
- content: {
1163
- ...found?.content,
1164
- version: found?.content.version ?? 1,
1165
- data: {
1166
- root: root ?? { props: {} },
1167
- content: [withSlotPlaces(block, config.components[block.type])]
1453
+ return {
1454
+ config,
1455
+ page: {
1456
+ path,
1457
+ file: found?.file ?? "",
1458
+ content: {
1459
+ ...found?.content,
1460
+ version: found?.content.version ?? 1,
1461
+ data: {
1462
+ root: root ?? { props: {} },
1463
+ content: [withSlotPlaces(block, config.components[block.type])]
1464
+ }
1168
1465
  }
1169
1466
  }
1170
1467
  };
1468
+ }
1469
+ /** A copy of fields as JSON, as the site's description has them: what they say, without the code in them. */
1470
+ function fieldsAsJson(fields) {
1471
+ return JSON.parse(JSON.stringify(fields ?? {}));
1472
+ }
1473
+ /** A block's fields as its props make them, as `blockFields()` describes. */
1474
+ async function blockFields(configs, content, target) {
1475
+ const { config, page } = targetPage(configs, content, target);
1476
+ const component = config.components[target.block.type];
1477
+ const fields = component.fields ?? {};
1478
+ if (!component.resolveFields) return fieldsAsJson(fields);
1479
+ const prepared = await preparePage({
1480
+ ...configs,
1481
+ page: config
1482
+ }, content, page);
1483
+ return fieldsAsJson(await component.resolveFields(target.block, {
1484
+ changed: {},
1485
+ fields,
1486
+ lastFields: fields,
1487
+ lastData: null,
1488
+ metadata: siteMetadata(prepared.site),
1489
+ appState: {},
1490
+ parent: null
1491
+ }));
1492
+ }
1493
+ /** One block drawn as the site draws it, in the page at its address, as `drawBlock()` describes. */
1494
+ async function drawBlock(configs, content, target) {
1495
+ const { config, page } = targetPage(configs, content, target);
1171
1496
  const prepared = await preparePage({
1172
1497
  ...configs,
1173
1498
  page: config
@@ -1242,6 +1567,9 @@ function localSiteCode(config) {
1242
1567
  }
1243
1568
  });
1244
1569
  return drawBlock(drawing, content, target);
1570
+ },
1571
+ async blockFields(content, target) {
1572
+ return blockFields(configs, content, target);
1245
1573
  }
1246
1574
  };
1247
1575
  }
@@ -1289,6 +1617,7 @@ function FrameProviders({ config, client, init, site, children }) {
1289
1617
  pageConfig: createPuckConfig(config, "page"),
1290
1618
  layoutConfig: createPuckConfig(config, "layout"),
1291
1619
  templateConfig: createPuckConfig(config, "template"),
1620
+ syncedConfig: createPuckConfig(config, "synced"),
1292
1621
  code: localSiteCode(config)
1293
1622
  }), [config]);
1294
1623
  const described = useMemo(() => adminSite(config), [config]);
@@ -1495,20 +1824,33 @@ function withOwnersOnlyLocked(config) {
1495
1824
  }
1496
1825
  //#endregion
1497
1826
  //#region src/frame/page-settings.ts
1827
+ /** What each setting is where a page leaves it out, which the editor shows as chosen. */
1828
+ const SHOWN_DEFAULTS = {
1829
+ noIndex: false,
1830
+ noFollow: false,
1831
+ noSnippet: false,
1832
+ card: "auto"
1833
+ };
1498
1834
  /** For pages and templates, whose settings include `noIndex`; `path` is the page's, for pages. */
1499
1835
  function shownSettings(original, path) {
1500
1836
  const fixed = path !== void 0 && neverIndexed(path);
1501
1837
  const originalRoot = original.root;
1502
- const originalValue = originalRoot.props?.noIndex;
1838
+ const originalProps = originalRoot.props ?? {};
1503
1839
  return {
1504
1840
  show(data) {
1505
1841
  const root = data.root;
1842
+ const missing = Object.entries(SHOWN_DEFAULTS).filter(([name]) => root.props?.[name] === void 0);
1843
+ if (missing.length === 0 && !fixed) return data;
1844
+ const props = {
1845
+ ...root.props,
1846
+ ...Object.fromEntries(missing)
1847
+ };
1506
1848
  if (fixed) return {
1507
1849
  ...data,
1508
1850
  root: {
1509
1851
  ...root,
1510
1852
  props: {
1511
- ...root.props,
1853
+ ...props,
1512
1854
  noIndex: true
1513
1855
  },
1514
1856
  readOnly: {
@@ -1517,32 +1859,29 @@ function shownSettings(original, path) {
1517
1859
  }
1518
1860
  }
1519
1861
  };
1520
- if (root.props?.noIndex !== void 0) return data;
1521
1862
  return {
1522
1863
  ...data,
1523
1864
  root: {
1524
1865
  ...root,
1525
- props: {
1526
- ...root.props,
1527
- noIndex: false
1528
- }
1866
+ props
1529
1867
  }
1530
1868
  };
1531
1869
  },
1532
1870
  keep(data) {
1533
1871
  const root = data.root;
1534
- const { noIndex, ...props } = root.props ?? {};
1535
- const kept = fixed || originalValue === void 0 && noIndex === false ? originalValue : noIndex;
1872
+ const props = { ...root.props };
1873
+ for (const [name, value] of Object.entries(SHOWN_DEFAULTS)) {
1874
+ const kept = fixed && name === "noIndex" || originalProps[name] === void 0 && props[name] === value ? originalProps[name] : props[name];
1875
+ if (kept === void 0) delete props[name];
1876
+ else props[name] = kept;
1877
+ }
1536
1878
  const { readOnly: _shown, ...rest } = root;
1537
1879
  const readOnly = fixed ? originalRoot.readOnly : root.readOnly;
1538
1880
  return {
1539
1881
  ...data,
1540
1882
  root: {
1541
1883
  ...rest,
1542
- props: kept === void 0 ? props : {
1543
- ...props,
1544
- noIndex: kept
1545
- },
1884
+ props,
1546
1885
  ...readOnly && { readOnly }
1547
1886
  }
1548
1887
  };
@@ -1964,16 +2303,230 @@ function withStyleFields(config, colors) {
1964
2303
  };
1965
2304
  }
1966
2305
  //#endregion
2306
+ //#region src/frame/synced-block.tsx
2307
+ /**
2308
+ * Synced blocks on the editor's canvas: a placed Synced block draws the
2309
+ * synced block's own blocks in its place, read-only, with a link to edit it,
2310
+ * since they're kept once for every page that places them. Builds put them
2311
+ * in place before rendering (`expandSyncedBlocks()`); the editor keeps the
2312
+ * placement, which is what the page saves.
2313
+ */
2314
+ function SyncedBlockDrawing({ name, config, drawing }) {
2315
+ const t = useStrings();
2316
+ const block = name ? findSyncedBlock(drawing.content, name) : void 0;
2317
+ if (!block) return /* @__PURE__ */ jsx("div", {
2318
+ className: "gfa-synced-block gfa-synced-block-missing",
2319
+ children: /* @__PURE__ */ jsx("p", { children: name ? t("synced.missing", { name }) : t("synced.choose") })
2320
+ });
2321
+ const shown = syncedBlockIn(block, drawing.language);
2322
+ const href = syncedBlockEditorHref(block.name, drawing.main ? void 0 : drawing.language);
2323
+ const data = {
2324
+ root: { props: {} },
2325
+ content: shown.data.content
2326
+ };
2327
+ return /* @__PURE__ */ jsxs("div", {
2328
+ className: "gfa-synced-block",
2329
+ "data-gf-synced-block": block.name,
2330
+ children: [/* @__PURE__ */ jsxs("div", {
2331
+ className: "gfa-synced-block-bar",
2332
+ children: [/* @__PURE__ */ jsx("span", { children: t("synced.label", { title: shown.title }) }), /* @__PURE__ */ jsx("a", {
2333
+ href,
2334
+ ref: (element) => registerOverlayPortal(element, { disableDrag: true }),
2335
+ onClick: (event) => {
2336
+ event.preventDefault();
2337
+ drawing.navigate(href);
2338
+ },
2339
+ children: t("synced.edit")
2340
+ })]
2341
+ }), /* @__PURE__ */ jsx("div", {
2342
+ inert: true,
2343
+ children: /* @__PURE__ */ jsx(Render, {
2344
+ config,
2345
+ data,
2346
+ metadata: drawing.metadata
2347
+ })
2348
+ })]
2349
+ });
2350
+ }
2351
+ /**
2352
+ * The config with its Synced block drawing the synced block it places, from
2353
+ * the blocks of the same config, whose own Synced block draws nothing, since
2354
+ * a synced block can't place another.
2355
+ */
2356
+ function withSyncedBlockDrawing(config, drawing) {
2357
+ const component = config.components[SYNCED_BLOCK];
2358
+ if (!component) return config;
2359
+ return {
2360
+ ...config,
2361
+ components: {
2362
+ ...config.components,
2363
+ [SYNCED_BLOCK]: {
2364
+ ...component,
2365
+ render: (props) => /* @__PURE__ */ jsx(SyncedBlockDrawing, {
2366
+ name: typeof props[SYNCED_BLOCK_PROP] === "string" ? props[SYNCED_BLOCK_PROP] : "",
2367
+ config,
2368
+ drawing
2369
+ })
2370
+ }
2371
+ }
2372
+ };
2373
+ }
2374
+ //#endregion
1967
2375
  //#region src/frame/table-field.tsx
1968
2376
  /**
1969
2377
  * The grid of cells a table's field is edited in (`tableField()` in
1970
2378
  * `@goodfellow-cms/react`), which Puck's editor shows in place of the field's
1971
- * own render, since blocks can't bring their own editing code.
2379
+ * own render, since blocks can't bring their own editing code. Each cell is
2380
+ * a small box of formatted text: bold, italics, links and line breaks, with
2381
+ * what's pasted kept to those (`tableCellHtml()`).
1972
2382
  */
1973
2383
  const MAX_ROWS = 100;
1974
2384
  const MAX_COLUMNS = 12;
2385
+ /** One cell, as formatted text. What it holds comes from the cell's value, except while it's being typed in. */
2386
+ function RichCell({ value, label, readOnly, onChange, onFocus }) {
2387
+ const ref = useRef(null);
2388
+ useLayoutEffect(() => {
2389
+ const cell = ref.current;
2390
+ if (!cell || document.activeElement === cell) return;
2391
+ const html = tableCellHtml(value);
2392
+ if (cell.innerHTML !== html) cell.innerHTML = html;
2393
+ }, [value]);
2394
+ const changed = () => {
2395
+ if (ref.current) onChange(tableCellHtml(ref.current.innerHTML));
2396
+ };
2397
+ return /* @__PURE__ */ jsx("div", {
2398
+ ref,
2399
+ role: "textbox",
2400
+ tabIndex: 0,
2401
+ "aria-label": label,
2402
+ "aria-multiline": "true",
2403
+ "aria-readonly": readOnly || void 0,
2404
+ contentEditable: !readOnly,
2405
+ suppressContentEditableWarning: true,
2406
+ className: "gfa-input gfa-table-field-cell gfa-table-field-rich",
2407
+ onInput: changed,
2408
+ onFocus: () => ref.current && onFocus(ref.current),
2409
+ onKeyDown: (event) => {
2410
+ if (event.key === "Enter") {
2411
+ event.preventDefault();
2412
+ document.execCommand("insertLineBreak");
2413
+ } else if ((event.ctrlKey || event.metaKey) && (event.key === "b" || event.key === "i")) {
2414
+ event.preventDefault();
2415
+ document.execCommand(event.key === "b" ? "bold" : "italic");
2416
+ }
2417
+ },
2418
+ onPaste: (event) => {
2419
+ event.preventDefault();
2420
+ const html = event.clipboardData.getData("text/html");
2421
+ const text = event.clipboardData.getData("text/plain");
2422
+ document.execCommand("insertHTML", false, html ? tableCellHtml(html) : escapeCellText(text));
2423
+ }
2424
+ });
2425
+ }
2426
+ /** Bold, italics and links for the cell last typed in. */
2427
+ function CellToolbar({ cell, readOnly }) {
2428
+ const t = useStrings();
2429
+ const [linking, setLinking] = useState();
2430
+ const disabled = readOnly || !cell;
2431
+ const command = (name, argument) => {
2432
+ cell?.focus();
2433
+ document.execCommand(name, false, argument);
2434
+ };
2435
+ const startLink = () => {
2436
+ const selection = document.getSelection();
2437
+ const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : void 0;
2438
+ setLinking({
2439
+ range: range && cell?.contains(range.commonAncestorContainer) ? range : void 0,
2440
+ address: ""
2441
+ });
2442
+ };
2443
+ const applyLink = (event) => {
2444
+ event.preventDefault();
2445
+ const address = linking?.address.trim() ?? "";
2446
+ if (cell && linking?.range && address && isSafeUrl(address)) {
2447
+ cell.focus();
2448
+ const selection = document.getSelection();
2449
+ selection?.removeAllRanges();
2450
+ selection?.addRange(linking.range);
2451
+ document.execCommand("createLink", false, address);
2452
+ }
2453
+ setLinking(void 0);
2454
+ };
2455
+ const keep = (event) => event.preventDefault();
2456
+ return /* @__PURE__ */ jsxs("div", {
2457
+ className: "gfa-table-field-toolbar",
2458
+ children: [
2459
+ /* @__PURE__ */ jsx("button", {
2460
+ type: "button",
2461
+ className: "gfa-table-field-tool",
2462
+ disabled,
2463
+ onMouseDown: keep,
2464
+ onClick: () => command("bold"),
2465
+ children: /* @__PURE__ */ jsx("strong", { children: t("table.bold") })
2466
+ }),
2467
+ /* @__PURE__ */ jsx("button", {
2468
+ type: "button",
2469
+ className: "gfa-table-field-tool",
2470
+ disabled,
2471
+ onMouseDown: keep,
2472
+ onClick: () => command("italic"),
2473
+ children: /* @__PURE__ */ jsx("em", { children: t("table.italic") })
2474
+ }),
2475
+ /* @__PURE__ */ jsx("button", {
2476
+ type: "button",
2477
+ className: "gfa-table-field-tool",
2478
+ disabled,
2479
+ onMouseDown: keep,
2480
+ onClick: startLink,
2481
+ children: t("table.link")
2482
+ }),
2483
+ /* @__PURE__ */ jsx("button", {
2484
+ type: "button",
2485
+ className: "gfa-table-field-tool",
2486
+ disabled,
2487
+ onMouseDown: keep,
2488
+ onClick: () => command("unlink"),
2489
+ children: t("table.unlink")
2490
+ }),
2491
+ linking && /* @__PURE__ */ jsxs("form", {
2492
+ className: "gfa-table-field-link",
2493
+ onSubmit: applyLink,
2494
+ children: [
2495
+ /* @__PURE__ */ jsx("input", {
2496
+ className: "gfa-input",
2497
+ "aria-label": t("table.linkAddress"),
2498
+ placeholder: "/about",
2499
+ value: linking.address,
2500
+ autoFocus: true,
2501
+ onChange: (event) => setLinking({
2502
+ ...linking,
2503
+ address: event.target.value
2504
+ })
2505
+ }),
2506
+ /* @__PURE__ */ jsx("button", {
2507
+ type: "submit",
2508
+ className: "gfa-button gfa-button-secondary",
2509
+ disabled: !linking.range,
2510
+ children: t("table.linkApply")
2511
+ }),
2512
+ /* @__PURE__ */ jsx("button", {
2513
+ type: "button",
2514
+ className: "gfa-button gfa-button-secondary",
2515
+ onClick: () => setLinking(void 0),
2516
+ children: t("action.cancel")
2517
+ }),
2518
+ !linking.range && /* @__PURE__ */ jsx("p", {
2519
+ className: "gfa-hint",
2520
+ children: t("table.linkSelect")
2521
+ })
2522
+ ]
2523
+ })
2524
+ ]
2525
+ });
2526
+ }
1975
2527
  function TableCellsField({ value, onChange, readOnly, label }) {
1976
2528
  const t = useStrings();
2529
+ const [active, setActive] = useState();
1977
2530
  const rows = tableCells(value);
1978
2531
  const width = rows[0]?.length ?? 1;
1979
2532
  const set = (row, column, text) => onChange(rows.map((cells, r) => r === row ? cells.map((cell, c) => c === column ? text : cell) : cells));
@@ -1984,20 +2537,24 @@ function TableCellsField({ value, onChange, readOnly, label }) {
1984
2537
  className: "gfa-table-field-label",
1985
2538
  children: label
1986
2539
  }),
2540
+ /* @__PURE__ */ jsx(CellToolbar, {
2541
+ cell: active?.isConnected ? active : void 0,
2542
+ readOnly
2543
+ }),
1987
2544
  /* @__PURE__ */ jsxs("div", {
1988
2545
  className: "gfa-table-field-grid",
1989
2546
  style: { gridTemplateColumns: `repeat(${width}, minmax(6rem, 1fr)) auto` },
1990
2547
  children: [rows.map((cells, row) => /* @__PURE__ */ jsxs("div", {
1991
2548
  className: "gfa-table-field-row",
1992
- children: [cells.map((cell, column) => /* @__PURE__ */ jsx("input", {
1993
- className: "gfa-input gfa-table-field-cell",
2549
+ children: [cells.map((cell, column) => /* @__PURE__ */ jsx(RichCell, {
1994
2550
  value: cell,
1995
2551
  readOnly,
1996
- "aria-label": t("table.cell", {
2552
+ label: t("table.cell", {
1997
2553
  row: String(row + 1),
1998
2554
  column: String(column + 1)
1999
2555
  }),
2000
- onChange: (event) => set(row, column, event.target.value)
2556
+ onChange: (html) => set(row, column, html),
2557
+ onFocus: setActive
2001
2558
  }, column)), /* @__PURE__ */ jsx("button", {
2002
2559
  type: "button",
2003
2560
  className: "gfa-table-field-remove",
@@ -2128,9 +2685,12 @@ function canvasConfig(kind, config, layoutConfig, header, footer, site) {
2128
2685
  children: kind === "header" ? /* @__PURE__ */ jsx("header", {
2129
2686
  className: "gf-header",
2130
2687
  children
2131
- }) : /* @__PURE__ */ jsx("footer", {
2688
+ }) : kind === "footer" ? /* @__PURE__ */ jsx("footer", {
2132
2689
  className: "gf-footer",
2133
2690
  children
2691
+ }) : /* @__PURE__ */ jsx("main", {
2692
+ className: "gf-main",
2693
+ children
2134
2694
  })
2135
2695
  });
2136
2696
  return {
@@ -2250,6 +2810,13 @@ function Bridge({ channel, settings }) {
2250
2810
  content: [...previous.content, ...change.blocks]
2251
2811
  })
2252
2812
  });
2813
+ else if (change.type === "detach") {
2814
+ if (!getSelectorForId(change.id)) throw new Error("The block being changed is no longer on the page.");
2815
+ dispatch({
2816
+ type: "setData",
2817
+ data: (previous) => withBlocksInPlace(previous, change.id, change.blocks)
2818
+ });
2819
+ }
2253
2820
  })
2254
2821
  ];
2255
2822
  return () => {
@@ -2279,7 +2846,7 @@ const READ_ONLY = {
2279
2846
  };
2280
2847
  function FrameEditor({ open, onOpened }) {
2281
2848
  const client = useAdminClient();
2282
- const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
2849
+ const { config: siteConfig, pageConfig, layoutConfig, templateConfig, syncedConfig } = useFrameCode();
2283
2850
  const { preview, mediaPreviews } = useAdmin();
2284
2851
  const { content: loaded } = useSiteContent();
2285
2852
  const { kind, path, title, data } = open;
@@ -2306,7 +2873,7 @@ function FrameEditor({ open, onOpened }) {
2306
2873
  loaded.settings.theme,
2307
2874
  loaded.customCss,
2308
2875
  dark
2309
- ]));
2876
+ ]), mediaPreviews);
2310
2877
  useEffect(() => showMediaInPreview(document, mediaPreviews), [mediaPreviews]);
2311
2878
  useEffect(() => {
2312
2879
  const onClick = (event) => {
@@ -2339,9 +2906,13 @@ function FrameEditor({ open, onOpened }) {
2339
2906
  siteConfig.blocks
2340
2907
  ]);
2341
2908
  const [view, setView] = useState(() => viewOf(initialData));
2909
+ const headingsOf = useCallback((next) => kind === "entry" || kind === "header" || kind === "footer" ? [] : pageHeadings(next, { components: siteConfig.blocks }), [kind, siteConfig.blocks]);
2910
+ const [headings, setHeadings] = useState(() => headingsOf(initialData));
2342
2911
  const updateView = (next) => {
2343
2912
  const found = viewOf(next);
2344
2913
  setView((current) => current?.block === found?.block ? current : found);
2914
+ const listed = headingsOf(next);
2915
+ setHeadings((current) => JSON.stringify(current) === JSON.stringify(listed) ? current : listed);
2345
2916
  };
2346
2917
  const adminLink = useCallback((place) => {
2347
2918
  const href = ADMIN_PLACES[place];
@@ -2354,6 +2925,16 @@ function FrameEditor({ open, onOpened }) {
2354
2925
  }
2355
2926
  };
2356
2927
  }, [client]);
2928
+ useEffect(() => {
2929
+ const onKeyDown = (event) => {
2930
+ const shortcut = shortcutFor(event, isTyping(event.target));
2931
+ if (!isForwardedShortcut(shortcut)) return;
2932
+ event.preventDefault();
2933
+ client.call("shortcut", shortcut).catch(() => {});
2934
+ };
2935
+ document.addEventListener("keydown", onKeyDown, true);
2936
+ return () => document.removeEventListener("keydown", onKeyDown, true);
2937
+ }, [client]);
2357
2938
  const callPlugin = useCallback((plugin, method, ...args) => client.call("plugin.call", {
2358
2939
  plugin,
2359
2940
  method,
@@ -2367,10 +2948,14 @@ function FrameEditor({ open, onOpened }) {
2367
2948
  pages: pageLinks(content),
2368
2949
  collection,
2369
2950
  ...view && { view },
2951
+ ...headings.length > 0 && { headings },
2370
2952
  ...languages && {
2371
2953
  languages,
2372
2954
  home: languageHome(loaded, language)
2373
2955
  },
2956
+ ...content.descriptions && { descriptions: Object.fromEntries(Object.entries(content.descriptions).map(([url, description]) => [url, describedIn(description, content.settings.language)])) },
2957
+ ...content.focus && { focus: Object.fromEntries(Object.entries(content.focus).map(([url, point]) => [url, focusPosition(point)])) },
2958
+ ...loaded.syncedBlocks && { syncedBlocks: loaded.syncedBlocks },
2374
2959
  adminLink,
2375
2960
  callPlugin
2376
2961
  }), [
@@ -2380,12 +2965,26 @@ function FrameEditor({ open, onOpened }) {
2380
2965
  path,
2381
2966
  collection,
2382
2967
  view,
2968
+ headings,
2383
2969
  languages,
2384
2970
  adminLink,
2385
2971
  callPlugin
2386
2972
  ]);
2387
2973
  const entry = kind === "entry" && collection && open.entry ? findEntry(loaded, collection.id, open.entry) : void 0;
2388
2974
  const { puck: dictionary, labels } = useEditorText();
2975
+ const metadata = useMemo(() => siteMetadata(site), [site]);
2976
+ const drawn = useCallback((config) => withSyncedBlockDrawing(config, {
2977
+ content: loaded,
2978
+ language,
2979
+ main: language === mainLanguage(loaded.settings),
2980
+ metadata,
2981
+ navigate: (href) => void client.call("navigate", href)
2982
+ }), [
2983
+ loaded,
2984
+ language,
2985
+ metadata,
2986
+ client
2987
+ ]);
2389
2988
  const config = useMemo(() => {
2390
2989
  if (kind === "entry") {
2391
2990
  if (!collection || !entry) throw new Error("The item isn't in the site.");
@@ -2394,17 +2993,21 @@ function FrameEditor({ open, onOpened }) {
2394
2993
  layoutConfig
2395
2994
  }), labels);
2396
2995
  }
2397
- return withLabels(withTableFields(canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site)), labels);
2996
+ return withLabels(withTableFields(canvasConfig(kind, drawn(kind === "page" ? pageConfig : kind === "template" ? templateConfig : kind === "synced" ? syncedConfig : layoutConfig), layoutConfig, expandSyncedBlocks(content.header.data, loaded, language), expandSyncedBlocks(content.footer.data, loaded, language), site)), labels);
2398
2997
  }, [
2399
2998
  kind,
2400
2999
  collection,
2401
3000
  entry,
2402
3001
  pageConfig,
2403
3002
  templateConfig,
3003
+ syncedConfig,
2404
3004
  layoutConfig,
2405
3005
  content,
3006
+ loaded,
3007
+ language,
2406
3008
  site,
2407
- labels
3009
+ labels,
3010
+ drawn
2408
3011
  ]);
2409
3012
  const described = useMemo(() => describeConfig(config), [config]);
2410
3013
  const puckConfig = useMemo(() => {
@@ -2470,7 +3073,7 @@ function FrameEditor({ open, onOpened }) {
2470
3073
  children: /* @__PURE__ */ jsx(Puck, {
2471
3074
  config: puckConfig,
2472
3075
  data: initialData,
2473
- metadata: siteMetadata(site),
3076
+ metadata,
2474
3077
  headerTitle: title,
2475
3078
  dictionary,
2476
3079
  height: "100%",
@@ -2580,7 +3183,8 @@ function CodeService({ channel }) {
2580
3183
  channel.handle("code.sitePages", ({ content, today }) => code.sitePages(content, today)),
2581
3184
  channel.handle("code.pageView", async ({ page, content, today }) => await code.pageView(page, content, today) ?? null),
2582
3185
  channel.handle("code.renderedProblems", ({ content, targets, today }) => code.renderedProblems(content, targets, today)),
2583
- channel.handle("code.drawBlock", ({ content, target }) => code.drawBlock(content, target))
3186
+ channel.handle("code.drawBlock", ({ content, target }) => code.drawBlock(content, target)),
3187
+ channel.handle("code.blockFields", ({ content, target }) => code.blockFields(content, target))
2584
3188
  ];
2585
3189
  return () => {
2586
3190
  for (const stop of stops) stop();