@goodfellow-cms/admin 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dev.d.ts +4 -2
- package/dist/dev.js +21 -1
- package/dist/editor.js +752 -14
- package/dist/{entry-values-ocbalc9U.js → entry-values-DO01xZ9L.js} +1975 -513
- package/dist/index.d.ts +340 -11
- package/dist/index.js +5377 -3058
- package/dist/plugin-protocol-DkvBDqsF.js +119 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +153 -0
- package/package.json +12 -6
- package/plugin.css +219 -0
- package/styles.css +385 -6
package/dist/editor.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Bt as editedMarkdown, It as storedData, Lt as storedEntryFields, R as createMediaPreviews, Vt as formattedHtml, a as AdminContext, at as useStrings, b as MediaChooser, f as useDarkMode, h as ScheduleFields, i as usePreviewStyles, l as useSiteContent, nt as StringsProvider, q as READY_MESSAGE, s as useAdmin, st as findPage, t as SCHEDULE_PROP, u as setThemeChoice, ut as mediaUrl, w as SETTINGS_SECTIONS, y as MediaChoiceContext, z as showMediaInPreview } from "./entry-values-DO01xZ9L.js";
|
|
2
|
+
import { r as createChannel } from "./plugin-protocol-DkvBDqsF.js";
|
|
3
|
+
import { COLOR_SCHEME_ATTRIBUTE, DEFAULT_THEME_COLORS, DRAWN_SLOT_ELEMENT, FONT_SIZES, MEDIA_DIR, MINIMUM_CONTRAST, RADII, 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, languageHome, languageLinks, mainLanguage, neverIndexed, pageLinks, pageView, pathLanguage, readBlockStyle, renderedProblems, shownOccurrence, sitePages, themeColorOf, themeToCss, todayIn, wallClock, weekdayName, weekdayOf, withBlockStyle } from "@goodfellow-cms/core";
|
|
4
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
5
|
import { createRoot } from "react-dom/client";
|
|
5
6
|
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 {
|
|
7
|
+
import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
|
|
8
|
+
import { Box } from "lucide-react";
|
|
9
|
+
import { AutoField, Drawer, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
|
|
8
10
|
import { createPortal, flushSync } from "react-dom";
|
|
9
11
|
const CANVAS = `.gf-canvas`;
|
|
10
12
|
const CANVAS_CONTAINER = "gf-canvas";
|
|
@@ -298,6 +300,128 @@ function SharePreview({ path }) {
|
|
|
298
300
|
});
|
|
299
301
|
}
|
|
300
302
|
//#endregion
|
|
303
|
+
//#region src/frame/block-icon.tsx
|
|
304
|
+
/** A block's icon, at the block list's size, in the text's color. */
|
|
305
|
+
function BlockIconSvg({ icon }) {
|
|
306
|
+
if (!icon) return /* @__PURE__ */ jsx(Box, {
|
|
307
|
+
className: "gfa-block-icon",
|
|
308
|
+
size: 16,
|
|
309
|
+
strokeWidth: 2,
|
|
310
|
+
"aria-hidden": "true"
|
|
311
|
+
});
|
|
312
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
313
|
+
className: "gfa-block-icon",
|
|
314
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
315
|
+
width: "16",
|
|
316
|
+
height: "16",
|
|
317
|
+
viewBox: "0 0 24 24",
|
|
318
|
+
fill: "none",
|
|
319
|
+
stroke: "currentColor",
|
|
320
|
+
strokeWidth: "2",
|
|
321
|
+
strokeLinecap: "round",
|
|
322
|
+
strokeLinejoin: "round",
|
|
323
|
+
"aria-hidden": "true",
|
|
324
|
+
children: icon.map(([shape, attributes], index) => createElement(shape, {
|
|
325
|
+
key: index,
|
|
326
|
+
...attributes
|
|
327
|
+
}))
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Puck's block list item, with the block's icon before its name. */
|
|
331
|
+
function drawerItemWithIcon(icons) {
|
|
332
|
+
return function DrawerItemWithIcon({ children, name }) {
|
|
333
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
334
|
+
className: "gfa-drawer-item",
|
|
335
|
+
children: [/* @__PURE__ */ jsx(BlockIconSvg, { icon: icons.get(name) }), children]
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/frame/block-search.tsx
|
|
341
|
+
/** The blocks of a Puck config, with their groups and the site's keywords for them, in the list's order. */
|
|
342
|
+
function searchableBlocks(config, siteConfig) {
|
|
343
|
+
const groups = /* @__PURE__ */ new Map();
|
|
344
|
+
const order = [];
|
|
345
|
+
for (const [key, category] of Object.entries(config.categories ?? {})) for (const name of category?.components ?? []) {
|
|
346
|
+
const block = String(name);
|
|
347
|
+
if (!config.components[block] || groups.has(block)) continue;
|
|
348
|
+
groups.set(block, category?.title ?? key);
|
|
349
|
+
order.push(block);
|
|
350
|
+
}
|
|
351
|
+
for (const name of Object.keys(config.components)) if (!groups.has(name)) order.push(name);
|
|
352
|
+
return order.map((name) => {
|
|
353
|
+
const block = siteConfig?.blocks[name];
|
|
354
|
+
return {
|
|
355
|
+
name,
|
|
356
|
+
label: config.components[name]?.label ?? name,
|
|
357
|
+
...groups.has(name) && { group: groups.get(name) },
|
|
358
|
+
keywords: (block && blockKeywords(block)) ?? []
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** A block's name as words: `MediaText` is "media text". */
|
|
363
|
+
function nameWords(name) {
|
|
364
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ");
|
|
365
|
+
}
|
|
366
|
+
/** Whether every word typed starts a word of `text`. */
|
|
367
|
+
function startsWords(text, words) {
|
|
368
|
+
const lower = ` ${text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ")}`;
|
|
369
|
+
return words.every((word) => lower.includes(` ${word}`));
|
|
370
|
+
}
|
|
371
|
+
/** The blocks `query` finds, best first: by label, then by name or keyword, then by group. */
|
|
372
|
+
function searchBlocks(query, blocks) {
|
|
373
|
+
const words = query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
374
|
+
if (words.length === 0) return [...blocks];
|
|
375
|
+
const rank = (block) => {
|
|
376
|
+
if (block.label.toLowerCase().startsWith(query.trim().toLowerCase())) return 0;
|
|
377
|
+
if (startsWords(block.label, words)) return 1;
|
|
378
|
+
if (startsWords(nameWords(block.name), words) || block.keywords.some((keyword) => startsWords(keyword, words))) return 2;
|
|
379
|
+
if (block.group && startsWords(`${block.group} ${block.label}`, words)) return 3;
|
|
380
|
+
};
|
|
381
|
+
return blocks.map((block, index) => ({
|
|
382
|
+
block,
|
|
383
|
+
index,
|
|
384
|
+
rank: rank(block)
|
|
385
|
+
})).filter((found) => found.rank !== void 0).sort((a, b) => a.rank - b.rank || a.index - b.index).map(({ block }) => block);
|
|
386
|
+
}
|
|
387
|
+
/** Puck's `drawer` override: the search box, then Puck's own list or what's found. */
|
|
388
|
+
function BlockSearch({ blocks, item, children }) {
|
|
389
|
+
const t = useStrings();
|
|
390
|
+
const [query, setQuery] = useState("");
|
|
391
|
+
const id = useId();
|
|
392
|
+
const found = query.trim() ? searchBlocks(query, blocks) : void 0;
|
|
393
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
394
|
+
className: "gfa-block-search",
|
|
395
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
396
|
+
className: "gfa-block-search-box",
|
|
397
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
398
|
+
id,
|
|
399
|
+
type: "search",
|
|
400
|
+
className: "gfa-block-search-input",
|
|
401
|
+
"aria-label": t("editor.blockSearch"),
|
|
402
|
+
placeholder: t("editor.blockSearch"),
|
|
403
|
+
value: query,
|
|
404
|
+
onChange: (event) => setQuery(event.target.value),
|
|
405
|
+
onKeyDown: (event) => {
|
|
406
|
+
if (event.key === "Escape") setQuery("");
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
}), found === void 0 ? children : found.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
410
|
+
className: "gfa-block-search-none",
|
|
411
|
+
role: "status",
|
|
412
|
+
children: t("editor.blockSearchNone")
|
|
413
|
+
}) : /* @__PURE__ */ jsx("section", {
|
|
414
|
+
className: "gfa-block-search-results",
|
|
415
|
+
"aria-label": t("editor.blockSearchFound"),
|
|
416
|
+
children: /* @__PURE__ */ jsx(Drawer, { children: found.map((block) => /* @__PURE__ */ jsx(Drawer.Item, {
|
|
417
|
+
name: block.name,
|
|
418
|
+
label: block.label,
|
|
419
|
+
children: item
|
|
420
|
+
}, block.name)) })
|
|
421
|
+
})]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
301
425
|
//#region src/event-field.tsx
|
|
302
426
|
const EVERY_LABELS = {
|
|
303
427
|
none: "event.repeat.none",
|
|
@@ -907,6 +1031,7 @@ function EntryPreview({ collection, entry, content, templateConfig, layoutConfig
|
|
|
907
1031
|
menus: content.menus,
|
|
908
1032
|
path: entry.path ?? "/",
|
|
909
1033
|
collections: content.collections,
|
|
1034
|
+
pages: pageLinks(content),
|
|
910
1035
|
collection,
|
|
911
1036
|
entry: current
|
|
912
1037
|
}), [
|
|
@@ -999,9 +1124,76 @@ function entryConfig(collection, entry, content, configs) {
|
|
|
999
1124
|
* The site's code, run in the page that has loaded it: the frame that runs
|
|
1000
1125
|
* it for the admin panel, or tests. Never imported by the admin panel's own page.
|
|
1001
1126
|
*/
|
|
1127
|
+
/** The block drawn in each slot's place, which only drawing has: its name can't be one of a site's. */
|
|
1128
|
+
const DRAWN_SLOT_BLOCK = "__goodfellow_drawn_slot__";
|
|
1129
|
+
/** A block that draws nothing but an empty `DRAWN_SLOT_ELEMENT` with its slot's name. */
|
|
1130
|
+
const drawnSlot = {
|
|
1131
|
+
fields: { slot: { type: "text" } },
|
|
1132
|
+
render: ({ slot }) => createElement(DRAWN_SLOT_ELEMENT, { "data-slot": String(slot ?? "") })
|
|
1133
|
+
};
|
|
1134
|
+
/** The block as it's drawn: each of its slots holding only the block that marks its place. */
|
|
1135
|
+
function withSlotPlaces(block, config) {
|
|
1136
|
+
const fields = config?.fields ?? {};
|
|
1137
|
+
const places = Object.entries(fields).filter(([, field]) => field.type === "slot").map(([slot]) => [slot, [{
|
|
1138
|
+
type: DRAWN_SLOT_BLOCK,
|
|
1139
|
+
props: {
|
|
1140
|
+
id: `${String(block.props.id)}:${slot}`,
|
|
1141
|
+
slot
|
|
1142
|
+
}
|
|
1143
|
+
}]]);
|
|
1144
|
+
return {
|
|
1145
|
+
...block,
|
|
1146
|
+
props: {
|
|
1147
|
+
...block.props,
|
|
1148
|
+
...Object.fromEntries(places)
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
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 }) {
|
|
1154
|
+
const layout = kind === "header" || kind === "footer";
|
|
1155
|
+
const config = layout ? configs.layout : kind === "template" ? configs.template : configs.page;
|
|
1156
|
+
if (!config.components[block.type]) throw new Error(`The site has no block called ${block.type} here.`);
|
|
1157
|
+
const found = allPages(content).find((page) => page.path === path);
|
|
1158
|
+
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])]
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const prepared = await preparePage({
|
|
1172
|
+
...configs,
|
|
1173
|
+
page: config
|
|
1174
|
+
}, content, page);
|
|
1175
|
+
const container = document.createElement("div");
|
|
1176
|
+
const reactRoot = createRoot(container);
|
|
1177
|
+
try {
|
|
1178
|
+
flushSync(() => reactRoot.render(createElement(PageContent, {
|
|
1179
|
+
site: {
|
|
1180
|
+
...prepared.site,
|
|
1181
|
+
credits: []
|
|
1182
|
+
},
|
|
1183
|
+
pageConfig: prepared.pageConfig,
|
|
1184
|
+
page: prepared.data
|
|
1185
|
+
})));
|
|
1186
|
+
const main = container.querySelector("main");
|
|
1187
|
+
const zone = main?.children.length === 1 ? main.firstElementChild : null;
|
|
1188
|
+
return (zone?.tagName === "DIV" && zone.attributes.length === 0 ? zone : main)?.innerHTML ?? "";
|
|
1189
|
+
} finally {
|
|
1190
|
+
reactRoot.unmount();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1002
1193
|
/** The site's code run here, in the page that has loaded it: the frame, or tests. */
|
|
1003
1194
|
function localSiteCode(config) {
|
|
1004
1195
|
const configs = createPuckConfigs(config);
|
|
1196
|
+
let drawing;
|
|
1005
1197
|
return {
|
|
1006
1198
|
async sitePages(content, today) {
|
|
1007
1199
|
return sitePages(content, today, config.blocks);
|
|
@@ -1039,6 +1231,17 @@ function localSiteCode(config) {
|
|
|
1039
1231
|
root.unmount();
|
|
1040
1232
|
}
|
|
1041
1233
|
return problems;
|
|
1234
|
+
},
|
|
1235
|
+
async drawBlock(content, target) {
|
|
1236
|
+
if (typeof document === "undefined") throw new Error("Blocks are drawn only in a browser.");
|
|
1237
|
+
drawing ??= createPuckConfigs({
|
|
1238
|
+
...config,
|
|
1239
|
+
blocks: {
|
|
1240
|
+
...config.blocks,
|
|
1241
|
+
[DRAWN_SLOT_BLOCK]: drawnSlot
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
return drawBlock(drawing, content, target);
|
|
1042
1245
|
}
|
|
1043
1246
|
};
|
|
1044
1247
|
}
|
|
@@ -1070,11 +1273,11 @@ function useAdminClient() {
|
|
|
1070
1273
|
return value;
|
|
1071
1274
|
}
|
|
1072
1275
|
/** 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) {
|
|
1276
|
+
function frameMediaPreviews(client, siteUrl, storage = () => void 0) {
|
|
1074
1277
|
return createMediaPreviews({ readBytes: async (path) => {
|
|
1075
1278
|
if (!path.startsWith(`${MEDIA_DIR}/`)) return void 0;
|
|
1076
1279
|
return await client.call("media.bytes", mediaUrl(path)) ?? void 0;
|
|
1077
|
-
} }, siteUrl);
|
|
1280
|
+
} }, siteUrl, storage);
|
|
1078
1281
|
}
|
|
1079
1282
|
function refused() {
|
|
1080
1283
|
throw new Error("The site's frame can't do this.");
|
|
@@ -1089,7 +1292,9 @@ function FrameProviders({ config, client, init, site, children }) {
|
|
|
1089
1292
|
code: localSiteCode(config)
|
|
1090
1293
|
}), [config]);
|
|
1091
1294
|
const described = useMemo(() => adminSite(config), [config]);
|
|
1092
|
-
const
|
|
1295
|
+
const storage = useRef(site.content.mediaStorage);
|
|
1296
|
+
storage.current = site.content.mediaStorage;
|
|
1297
|
+
const mediaPreviews = useMemo(() => frameMediaPreviews(client, init.siteUrl, () => storage.current), [client, init.siteUrl]);
|
|
1093
1298
|
useEffect(() => client.channel.handle("media.remember", (file) => {
|
|
1094
1299
|
if (typeof file?.url === "string" && file.bytes instanceof Uint8Array) mediaPreviews.remember(file.url, file.bytes);
|
|
1095
1300
|
}), [client, mediaPreviews]);
|
|
@@ -1117,6 +1322,7 @@ function FrameProviders({ config, client, init, site, children }) {
|
|
|
1117
1322
|
drafts: {
|
|
1118
1323
|
kept: true,
|
|
1119
1324
|
names: [],
|
|
1325
|
+
reviews: [],
|
|
1120
1326
|
open: async () => refused(),
|
|
1121
1327
|
publishAll: async () => refused(),
|
|
1122
1328
|
submit: async () => refused(),
|
|
@@ -1124,6 +1330,7 @@ function FrameProviders({ config, client, init, site, children }) {
|
|
|
1124
1330
|
},
|
|
1125
1331
|
readFile: async () => refused(),
|
|
1126
1332
|
listFiles: async () => refused(),
|
|
1333
|
+
readBytes: async () => refused(),
|
|
1127
1334
|
registries: {},
|
|
1128
1335
|
outOfReach: false,
|
|
1129
1336
|
contentOnly: false,
|
|
@@ -1221,6 +1428,497 @@ function shownSettings(original, path) {
|
|
|
1221
1428
|
};
|
|
1222
1429
|
}
|
|
1223
1430
|
//#endregion
|
|
1431
|
+
//#region src/frame/style-field.tsx
|
|
1432
|
+
/**
|
|
1433
|
+
* A block's Style field (`classNameField` in `@goodfellow-cms/react`), which
|
|
1434
|
+
* Puck's editor shows as controls for its look in place of a box of classes:
|
|
1435
|
+
* text alignment, size and color, background, space inside and around it and
|
|
1436
|
+
* rounded corners, written into its `className` as Tailwind classes
|
|
1437
|
+
* (`withBlockStyle()` in `@goodfellow-cms/core`), with any other classes kept.
|
|
1438
|
+
*/
|
|
1439
|
+
/** A color's value to show and compare: a theme color's, or the editor's own. */
|
|
1440
|
+
function shownColor(value, colors) {
|
|
1441
|
+
if (!value) return void 0;
|
|
1442
|
+
const theme = themeColorOf(value);
|
|
1443
|
+
return theme ? colors[theme] ?? DEFAULT_THEME_COLORS[theme] : value;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* A choice from a scale, or a size of the editor's own: the scale's steps,
|
|
1447
|
+
* "As the block has it" and "Your own…", which shows a box for a length.
|
|
1448
|
+
*/
|
|
1449
|
+
function ScaleChoice({ label, value, steps, stepLabel, readOnly, onChange }) {
|
|
1450
|
+
const t = useStrings();
|
|
1451
|
+
const id = useId();
|
|
1452
|
+
const custom = value !== void 0 && !steps.includes(value);
|
|
1453
|
+
const [ownChosen, setOwnChosen] = useState(custom);
|
|
1454
|
+
const [typed, setTyped] = useState(custom ? value : "");
|
|
1455
|
+
useEffect(() => {
|
|
1456
|
+
if (value !== void 0 && !steps.includes(value)) {
|
|
1457
|
+
setOwnChosen(true);
|
|
1458
|
+
setTyped(value);
|
|
1459
|
+
}
|
|
1460
|
+
}, [value, steps]);
|
|
1461
|
+
const own = custom || ownChosen;
|
|
1462
|
+
const invalid = own && typed.trim() !== "" && customLength(typed) === void 0;
|
|
1463
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1464
|
+
className: "gfa-style-choice",
|
|
1465
|
+
children: [
|
|
1466
|
+
/* @__PURE__ */ jsx("label", {
|
|
1467
|
+
className: "gfa-style-label",
|
|
1468
|
+
htmlFor: id,
|
|
1469
|
+
children: label
|
|
1470
|
+
}),
|
|
1471
|
+
/* @__PURE__ */ jsxs("select", {
|
|
1472
|
+
id,
|
|
1473
|
+
className: "gfa-input",
|
|
1474
|
+
disabled: readOnly,
|
|
1475
|
+
value: own ? "custom" : value ?? "",
|
|
1476
|
+
onChange: (event) => {
|
|
1477
|
+
const chosen = event.target.value;
|
|
1478
|
+
if (chosen === "custom") {
|
|
1479
|
+
setOwnChosen(true);
|
|
1480
|
+
const length = customLength(typed);
|
|
1481
|
+
if (length) onChange(length);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
setOwnChosen(false);
|
|
1485
|
+
onChange(chosen === "" ? void 0 : chosen);
|
|
1486
|
+
},
|
|
1487
|
+
children: [
|
|
1488
|
+
/* @__PURE__ */ jsx("option", {
|
|
1489
|
+
value: "",
|
|
1490
|
+
children: t("style.default")
|
|
1491
|
+
}),
|
|
1492
|
+
steps.map((step) => /* @__PURE__ */ jsx("option", {
|
|
1493
|
+
value: step,
|
|
1494
|
+
children: stepLabel(step)
|
|
1495
|
+
}, step)),
|
|
1496
|
+
/* @__PURE__ */ jsx("option", {
|
|
1497
|
+
value: "custom",
|
|
1498
|
+
children: t("style.custom")
|
|
1499
|
+
})
|
|
1500
|
+
]
|
|
1501
|
+
}),
|
|
1502
|
+
own && /* @__PURE__ */ jsx("input", {
|
|
1503
|
+
className: "gfa-input",
|
|
1504
|
+
value: typed,
|
|
1505
|
+
readOnly,
|
|
1506
|
+
"aria-label": `${label}: ${t("style.customLength")}`,
|
|
1507
|
+
placeholder: t("style.customLength"),
|
|
1508
|
+
"aria-invalid": invalid,
|
|
1509
|
+
onChange: (event) => {
|
|
1510
|
+
setTyped(event.target.value);
|
|
1511
|
+
const length = customLength(event.target.value);
|
|
1512
|
+
if (length) onChange(length);
|
|
1513
|
+
}
|
|
1514
|
+
}),
|
|
1515
|
+
invalid && /* @__PURE__ */ jsx("p", {
|
|
1516
|
+
className: "gfa-error-text",
|
|
1517
|
+
children: t("style.customLengthInvalid")
|
|
1518
|
+
})
|
|
1519
|
+
]
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
/** Spacing steps as people read them: `4` is 16px. */
|
|
1523
|
+
function spacingLabel(t, step) {
|
|
1524
|
+
return step === "0" ? t("style.none") : `${Number(step) * 4}px`;
|
|
1525
|
+
}
|
|
1526
|
+
/** Space on every side at once, or on each side. */
|
|
1527
|
+
function SidesChoice({ label, sides, readOnly, onChange }) {
|
|
1528
|
+
const t = useStrings();
|
|
1529
|
+
const values = SIDES.map((side) => sides?.[side]);
|
|
1530
|
+
const same = values.every((value) => value === values[0]);
|
|
1531
|
+
const [each, setEach] = useState(!same);
|
|
1532
|
+
useEffect(() => {
|
|
1533
|
+
if (!same) setEach(true);
|
|
1534
|
+
}, [same]);
|
|
1535
|
+
const set = (next) => {
|
|
1536
|
+
const kept = Object.fromEntries(Object.entries(next).filter(([, value]) => value !== void 0));
|
|
1537
|
+
onChange(Object.keys(kept).length > 0 ? kept : void 0);
|
|
1538
|
+
};
|
|
1539
|
+
const stepLabel = (step) => spacingLabel(t, step);
|
|
1540
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
1541
|
+
className: "gfa-style-group",
|
|
1542
|
+
children: [
|
|
1543
|
+
/* @__PURE__ */ jsx("legend", {
|
|
1544
|
+
className: "gfa-style-label",
|
|
1545
|
+
children: label
|
|
1546
|
+
}),
|
|
1547
|
+
each ? /* @__PURE__ */ jsx("div", {
|
|
1548
|
+
className: "gfa-style-sides",
|
|
1549
|
+
children: SIDES.map((side) => /* @__PURE__ */ jsx(ScaleChoice, {
|
|
1550
|
+
label: t(`style.side.${side}`),
|
|
1551
|
+
value: sides?.[side],
|
|
1552
|
+
steps: SPACING_STEPS,
|
|
1553
|
+
stepLabel,
|
|
1554
|
+
readOnly,
|
|
1555
|
+
onChange: (value) => set({
|
|
1556
|
+
...sides,
|
|
1557
|
+
[side]: value
|
|
1558
|
+
})
|
|
1559
|
+
}, side))
|
|
1560
|
+
}) : /* @__PURE__ */ jsx(ScaleChoice, {
|
|
1561
|
+
label: t("style.allSides"),
|
|
1562
|
+
value: values[0],
|
|
1563
|
+
steps: SPACING_STEPS,
|
|
1564
|
+
stepLabel,
|
|
1565
|
+
readOnly,
|
|
1566
|
+
onChange: (value) => set(Object.fromEntries(SIDES.map((side) => [side, value])))
|
|
1567
|
+
}),
|
|
1568
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1569
|
+
className: "gfa-style-check",
|
|
1570
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1571
|
+
type: "checkbox",
|
|
1572
|
+
checked: each,
|
|
1573
|
+
disabled: readOnly || each && !same,
|
|
1574
|
+
onChange: (event) => setEach(event.target.checked)
|
|
1575
|
+
}), t("style.eachSide")]
|
|
1576
|
+
})
|
|
1577
|
+
]
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
/** The theme's colors as swatches, a color of the editor's own, and back to the block's. */
|
|
1581
|
+
function ColorChoice({ label, value, colors, readOnly, onChange }) {
|
|
1582
|
+
const t = useStrings();
|
|
1583
|
+
const own = value !== void 0 && isCustomColor(value);
|
|
1584
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
1585
|
+
className: "gfa-style-group",
|
|
1586
|
+
children: [/* @__PURE__ */ jsx("legend", {
|
|
1587
|
+
className: "gfa-style-label",
|
|
1588
|
+
children: label
|
|
1589
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1590
|
+
className: "gfa-style-swatches",
|
|
1591
|
+
children: [
|
|
1592
|
+
/* @__PURE__ */ jsx("button", {
|
|
1593
|
+
type: "button",
|
|
1594
|
+
className: "gfa-style-swatch gfa-style-swatch-none",
|
|
1595
|
+
"aria-pressed": value === void 0,
|
|
1596
|
+
disabled: readOnly,
|
|
1597
|
+
title: t("style.colorDefault"),
|
|
1598
|
+
"aria-label": t("style.colorDefault"),
|
|
1599
|
+
onClick: () => onChange(void 0)
|
|
1600
|
+
}),
|
|
1601
|
+
THEME_COLORS.map((color) => {
|
|
1602
|
+
const slug = colorSlug(color);
|
|
1603
|
+
const name = t(`theme.color.${color}`);
|
|
1604
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1605
|
+
type: "button",
|
|
1606
|
+
className: "gfa-style-swatch",
|
|
1607
|
+
style: { background: colors[color] ?? DEFAULT_THEME_COLORS[color] },
|
|
1608
|
+
"aria-pressed": value === slug,
|
|
1609
|
+
disabled: readOnly,
|
|
1610
|
+
title: name,
|
|
1611
|
+
"aria-label": name,
|
|
1612
|
+
onClick: () => onChange(slug)
|
|
1613
|
+
}, color);
|
|
1614
|
+
}),
|
|
1615
|
+
/* @__PURE__ */ jsx("label", {
|
|
1616
|
+
className: "gfa-style-swatch gfa-style-swatch-own",
|
|
1617
|
+
title: t("style.colorCustom"),
|
|
1618
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
1619
|
+
type: "color",
|
|
1620
|
+
value: own ? value : "#000000",
|
|
1621
|
+
disabled: readOnly,
|
|
1622
|
+
"aria-label": `${label}: ${t("style.colorCustom")}`,
|
|
1623
|
+
onChange: (event) => onChange(event.target.value.toLowerCase())
|
|
1624
|
+
})
|
|
1625
|
+
})
|
|
1626
|
+
]
|
|
1627
|
+
})]
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
/** Other classes, kept as typed while the box has focus, so a class isn't taken into the controls halfway. */
|
|
1631
|
+
function OtherClasses({ other, readOnly, onChange }) {
|
|
1632
|
+
const t = useStrings();
|
|
1633
|
+
const id = useId();
|
|
1634
|
+
const joined = other.join(" ");
|
|
1635
|
+
const [typed, setTyped] = useState(joined);
|
|
1636
|
+
const [focused, setFocused] = useState(false);
|
|
1637
|
+
useEffect(() => {
|
|
1638
|
+
if (!focused) setTyped(joined);
|
|
1639
|
+
}, [joined, focused]);
|
|
1640
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1641
|
+
className: "gfa-style-choice",
|
|
1642
|
+
children: [
|
|
1643
|
+
/* @__PURE__ */ jsx("label", {
|
|
1644
|
+
className: "gfa-style-label",
|
|
1645
|
+
htmlFor: id,
|
|
1646
|
+
children: t("style.other")
|
|
1647
|
+
}),
|
|
1648
|
+
/* @__PURE__ */ jsx("input", {
|
|
1649
|
+
id,
|
|
1650
|
+
className: "gfa-input gfa-code",
|
|
1651
|
+
value: typed,
|
|
1652
|
+
readOnly,
|
|
1653
|
+
spellCheck: false,
|
|
1654
|
+
onFocus: () => setFocused(true),
|
|
1655
|
+
onBlur: () => setFocused(false),
|
|
1656
|
+
onChange: (event) => {
|
|
1657
|
+
setTyped(event.target.value);
|
|
1658
|
+
onChange(event.target.value.split(/\s+/).filter(Boolean));
|
|
1659
|
+
}
|
|
1660
|
+
}),
|
|
1661
|
+
/* @__PURE__ */ jsx("p", {
|
|
1662
|
+
className: "gfa-hint",
|
|
1663
|
+
children: t("style.otherHint")
|
|
1664
|
+
})
|
|
1665
|
+
]
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
function StyleControls({ value, colors, readOnly, label, ownAlign, onChange }) {
|
|
1669
|
+
const t = useStrings();
|
|
1670
|
+
const { style, other } = readBlockStyle(value);
|
|
1671
|
+
const set = (next) => onChange(withBlockStyle(other, next));
|
|
1672
|
+
const change = (key, next) => {
|
|
1673
|
+
const { [key]: _old, ...rest } = style;
|
|
1674
|
+
set(next === void 0 ? rest : {
|
|
1675
|
+
...rest,
|
|
1676
|
+
[key]: next
|
|
1677
|
+
});
|
|
1678
|
+
};
|
|
1679
|
+
const text = shownColor(style.color, colors);
|
|
1680
|
+
const background = shownColor(style.background, colors);
|
|
1681
|
+
const ratio = text && background ? contrastRatio(text, background) : void 0;
|
|
1682
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1683
|
+
className: "gfa-style-field",
|
|
1684
|
+
children: [
|
|
1685
|
+
label && /* @__PURE__ */ jsx("div", {
|
|
1686
|
+
className: "gfa-style-title",
|
|
1687
|
+
children: label
|
|
1688
|
+
}),
|
|
1689
|
+
!ownAlign && /* @__PURE__ */ jsxs("fieldset", {
|
|
1690
|
+
className: "gfa-style-group",
|
|
1691
|
+
children: [/* @__PURE__ */ jsx("legend", {
|
|
1692
|
+
className: "gfa-style-label",
|
|
1693
|
+
children: t("style.align")
|
|
1694
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1695
|
+
className: "gfa-style-segments",
|
|
1696
|
+
children: [void 0, ...TEXT_ALIGNS].map((align) => /* @__PURE__ */ jsx("button", {
|
|
1697
|
+
type: "button",
|
|
1698
|
+
className: "gfa-style-segment",
|
|
1699
|
+
"aria-pressed": style.align === align,
|
|
1700
|
+
disabled: readOnly,
|
|
1701
|
+
onClick: () => change("align", align),
|
|
1702
|
+
children: t(`style.align.${align ?? "default"}`)
|
|
1703
|
+
}, align ?? "default"))
|
|
1704
|
+
})]
|
|
1705
|
+
}),
|
|
1706
|
+
/* @__PURE__ */ jsx(ScaleChoice, {
|
|
1707
|
+
label: t("style.fontSize"),
|
|
1708
|
+
value: style.fontSize,
|
|
1709
|
+
steps: FONT_SIZES,
|
|
1710
|
+
stepLabel: (step) => t(`style.fontSize.${step}`),
|
|
1711
|
+
readOnly,
|
|
1712
|
+
onChange: (size) => change("fontSize", size)
|
|
1713
|
+
}),
|
|
1714
|
+
/* @__PURE__ */ jsx(ColorChoice, {
|
|
1715
|
+
label: t("style.color"),
|
|
1716
|
+
value: style.color,
|
|
1717
|
+
colors,
|
|
1718
|
+
readOnly,
|
|
1719
|
+
onChange: (color) => change("color", color)
|
|
1720
|
+
}),
|
|
1721
|
+
/* @__PURE__ */ jsx(ColorChoice, {
|
|
1722
|
+
label: t("style.background"),
|
|
1723
|
+
value: style.background,
|
|
1724
|
+
colors,
|
|
1725
|
+
readOnly,
|
|
1726
|
+
onChange: (color) => change("background", color)
|
|
1727
|
+
}),
|
|
1728
|
+
ratio !== void 0 && ratio < MINIMUM_CONTRAST && /* @__PURE__ */ jsx("p", {
|
|
1729
|
+
className: "gfa-hint gfa-style-warning",
|
|
1730
|
+
role: "status",
|
|
1731
|
+
children: t("style.contrast")
|
|
1732
|
+
}),
|
|
1733
|
+
/* @__PURE__ */ jsx(SidesChoice, {
|
|
1734
|
+
label: t("style.padding"),
|
|
1735
|
+
sides: style.padding,
|
|
1736
|
+
readOnly,
|
|
1737
|
+
onChange: (sides) => change("padding", sides)
|
|
1738
|
+
}),
|
|
1739
|
+
/* @__PURE__ */ jsx(SidesChoice, {
|
|
1740
|
+
label: t("style.margin"),
|
|
1741
|
+
sides: style.margin,
|
|
1742
|
+
readOnly,
|
|
1743
|
+
onChange: (sides) => change("margin", sides)
|
|
1744
|
+
}),
|
|
1745
|
+
/* @__PURE__ */ jsx(ScaleChoice, {
|
|
1746
|
+
label: t("style.radius"),
|
|
1747
|
+
value: style.radius,
|
|
1748
|
+
steps: RADII,
|
|
1749
|
+
stepLabel: (step) => t(`style.radius.${step}`),
|
|
1750
|
+
readOnly,
|
|
1751
|
+
onChange: (radius) => change("radius", radius)
|
|
1752
|
+
}),
|
|
1753
|
+
/* @__PURE__ */ jsx(OtherClasses, {
|
|
1754
|
+
other,
|
|
1755
|
+
readOnly,
|
|
1756
|
+
onChange: (next) => onChange(withBlockStyle(next, style))
|
|
1757
|
+
}),
|
|
1758
|
+
/* @__PURE__ */ jsx("button", {
|
|
1759
|
+
type: "button",
|
|
1760
|
+
className: "gfa-button gfa-button-secondary",
|
|
1761
|
+
disabled: readOnly || Object.keys(style).length === 0 && other.length === 0,
|
|
1762
|
+
onClick: () => onChange(""),
|
|
1763
|
+
children: t("style.reset")
|
|
1764
|
+
})
|
|
1765
|
+
]
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
function withStyleField(field, colors, ownAlign) {
|
|
1769
|
+
if (field.type !== "text" || !isStyleField(field)) return field;
|
|
1770
|
+
const render = ({ value, onChange, readOnly }) => /* @__PURE__ */ jsx(StyleControls, {
|
|
1771
|
+
value,
|
|
1772
|
+
colors,
|
|
1773
|
+
readOnly,
|
|
1774
|
+
label: field.label,
|
|
1775
|
+
ownAlign,
|
|
1776
|
+
onChange
|
|
1777
|
+
});
|
|
1778
|
+
return {
|
|
1779
|
+
type: "custom",
|
|
1780
|
+
label: field.label,
|
|
1781
|
+
metadata: field.metadata,
|
|
1782
|
+
render
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function withStyleFieldsIn(fields, colors) {
|
|
1786
|
+
if (!fields) return fields;
|
|
1787
|
+
const ownAlign = "align" in fields;
|
|
1788
|
+
return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, withStyleField(field, colors, ownAlign)]));
|
|
1789
|
+
}
|
|
1790
|
+
function withStyleComponent(component, colors) {
|
|
1791
|
+
if (!component) return component;
|
|
1792
|
+
const { resolveFields } = component;
|
|
1793
|
+
return {
|
|
1794
|
+
...component,
|
|
1795
|
+
...component.fields && { fields: withStyleFieldsIn(component.fields, colors) },
|
|
1796
|
+
...resolveFields && { resolveFields: async (...args) => withStyleFieldsIn(await resolveFields(...args), colors) }
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
/** The config with every block's Style field, and the page's, drawn as a style's controls. */
|
|
1800
|
+
function withStyleFields(config, colors) {
|
|
1801
|
+
const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, withStyleComponent(component, colors)]));
|
|
1802
|
+
return {
|
|
1803
|
+
...config,
|
|
1804
|
+
components,
|
|
1805
|
+
...config.root && { root: withStyleComponent(config.root, colors) }
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
//#endregion
|
|
1809
|
+
//#region src/frame/table-field.tsx
|
|
1810
|
+
/**
|
|
1811
|
+
* The grid of cells a table's field is edited in (`tableField()` in
|
|
1812
|
+
* `@goodfellow-cms/react`), which Puck's editor shows in place of the field's
|
|
1813
|
+
* own render, since blocks can't bring their own editing code.
|
|
1814
|
+
*/
|
|
1815
|
+
const MAX_ROWS = 100;
|
|
1816
|
+
const MAX_COLUMNS = 12;
|
|
1817
|
+
function TableCellsField({ value, onChange, readOnly, label }) {
|
|
1818
|
+
const t = useStrings();
|
|
1819
|
+
const rows = tableCells(value);
|
|
1820
|
+
const width = rows[0]?.length ?? 1;
|
|
1821
|
+
const set = (row, column, text) => onChange(rows.map((cells, r) => r === row ? cells.map((cell, c) => c === column ? text : cell) : cells));
|
|
1822
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1823
|
+
className: "gfa-table-field",
|
|
1824
|
+
children: [
|
|
1825
|
+
label && /* @__PURE__ */ jsx("div", {
|
|
1826
|
+
className: "gfa-table-field-label",
|
|
1827
|
+
children: label
|
|
1828
|
+
}),
|
|
1829
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1830
|
+
className: "gfa-table-field-grid",
|
|
1831
|
+
style: { gridTemplateColumns: `repeat(${width}, minmax(6rem, 1fr)) auto` },
|
|
1832
|
+
children: [rows.map((cells, row) => /* @__PURE__ */ jsxs("div", {
|
|
1833
|
+
className: "gfa-table-field-row",
|
|
1834
|
+
children: [cells.map((cell, column) => /* @__PURE__ */ jsx("input", {
|
|
1835
|
+
className: "gfa-input gfa-table-field-cell",
|
|
1836
|
+
value: cell,
|
|
1837
|
+
readOnly,
|
|
1838
|
+
"aria-label": t("table.cell", {
|
|
1839
|
+
row: String(row + 1),
|
|
1840
|
+
column: String(column + 1)
|
|
1841
|
+
}),
|
|
1842
|
+
onChange: (event) => set(row, column, event.target.value)
|
|
1843
|
+
}, column)), /* @__PURE__ */ jsx("button", {
|
|
1844
|
+
type: "button",
|
|
1845
|
+
className: "gfa-table-field-remove",
|
|
1846
|
+
disabled: readOnly || rows.length <= 1,
|
|
1847
|
+
title: t("table.removeRow"),
|
|
1848
|
+
"aria-label": t("table.removeRowNumber", { row: String(row + 1) }),
|
|
1849
|
+
onClick: () => onChange(rows.filter((_, r) => r !== row)),
|
|
1850
|
+
children: "×"
|
|
1851
|
+
})]
|
|
1852
|
+
}, row)), /* @__PURE__ */ jsx("div", {
|
|
1853
|
+
className: "gfa-table-field-row",
|
|
1854
|
+
children: Array.from({ length: width }, (_, column) => /* @__PURE__ */ jsx("button", {
|
|
1855
|
+
type: "button",
|
|
1856
|
+
className: "gfa-table-field-remove",
|
|
1857
|
+
disabled: readOnly || width <= 1,
|
|
1858
|
+
title: t("table.removeColumn"),
|
|
1859
|
+
"aria-label": t("table.removeColumnNumber", { column: String(column + 1) }),
|
|
1860
|
+
onClick: () => onChange(rows.map((cells) => cells.filter((_, c) => c !== column))),
|
|
1861
|
+
children: "×"
|
|
1862
|
+
}, column))
|
|
1863
|
+
})]
|
|
1864
|
+
}),
|
|
1865
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1866
|
+
className: "gfa-table-field-actions",
|
|
1867
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1868
|
+
type: "button",
|
|
1869
|
+
className: "gfa-button gfa-button-secondary",
|
|
1870
|
+
disabled: readOnly || rows.length >= MAX_ROWS,
|
|
1871
|
+
onClick: () => onChange([...rows, Array(width).fill("")]),
|
|
1872
|
+
children: t("table.addRow")
|
|
1873
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1874
|
+
type: "button",
|
|
1875
|
+
className: "gfa-button gfa-button-secondary",
|
|
1876
|
+
disabled: readOnly || width >= MAX_COLUMNS,
|
|
1877
|
+
onClick: () => onChange(rows.map((cells) => [...cells, ""])),
|
|
1878
|
+
children: t("table.addColumn")
|
|
1879
|
+
})]
|
|
1880
|
+
})
|
|
1881
|
+
]
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
function withTableField(field) {
|
|
1885
|
+
if (field.type === "custom" && isTableField(field)) {
|
|
1886
|
+
const render = ({ value, onChange, readOnly, field: own }) => /* @__PURE__ */ jsx(TableCellsField, {
|
|
1887
|
+
value,
|
|
1888
|
+
onChange,
|
|
1889
|
+
readOnly,
|
|
1890
|
+
label: own.label
|
|
1891
|
+
});
|
|
1892
|
+
return {
|
|
1893
|
+
...field,
|
|
1894
|
+
render
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
if (field.type === "array") return {
|
|
1898
|
+
...field,
|
|
1899
|
+
arrayFields: withTableFieldsIn(field.arrayFields)
|
|
1900
|
+
};
|
|
1901
|
+
if (field.type === "object") return {
|
|
1902
|
+
...field,
|
|
1903
|
+
objectFields: withTableFieldsIn(field.objectFields)
|
|
1904
|
+
};
|
|
1905
|
+
return field;
|
|
1906
|
+
}
|
|
1907
|
+
function withTableFieldsIn(fields) {
|
|
1908
|
+
return Object.fromEntries(Object.entries(fields ?? {}).map(([name, field]) => [name, withTableField(field)]));
|
|
1909
|
+
}
|
|
1910
|
+
/** The config with every table's field edited as a grid of cells. */
|
|
1911
|
+
function withTableFields(config) {
|
|
1912
|
+
const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, component.fields ? {
|
|
1913
|
+
...component,
|
|
1914
|
+
fields: withTableFieldsIn(component.fields)
|
|
1915
|
+
} : component]));
|
|
1916
|
+
return {
|
|
1917
|
+
...config,
|
|
1918
|
+
components
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
//#endregion
|
|
1224
1922
|
//#region src/frame/editor.tsx
|
|
1225
1923
|
/**
|
|
1226
1924
|
* Puck in the site's frame, for one page, header, footer, template or item.
|
|
@@ -1412,6 +2110,14 @@ const ENTRY_UI = {
|
|
|
1412
2110
|
};
|
|
1413
2111
|
const entryPlugins = [legacySideBarPlugin()];
|
|
1414
2112
|
/** Puck, set up for one page, header, footer, template or item: site context, previews and the bridge. */
|
|
2113
|
+
/** Puck's permissions for something shown without editing: nothing can be changed, added, moved or removed. */
|
|
2114
|
+
const READ_ONLY = {
|
|
2115
|
+
drag: false,
|
|
2116
|
+
duplicate: false,
|
|
2117
|
+
delete: false,
|
|
2118
|
+
edit: false,
|
|
2119
|
+
insert: false
|
|
2120
|
+
};
|
|
1415
2121
|
function FrameEditor({ open, onOpened }) {
|
|
1416
2122
|
const client = useAdminClient();
|
|
1417
2123
|
const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
|
|
@@ -1489,18 +2195,25 @@ function FrameEditor({ open, onOpened }) {
|
|
|
1489
2195
|
}
|
|
1490
2196
|
};
|
|
1491
2197
|
}, [client]);
|
|
2198
|
+
const callPlugin = useCallback((plugin, method, ...args) => client.call("plugin.call", {
|
|
2199
|
+
plugin,
|
|
2200
|
+
method,
|
|
2201
|
+
args
|
|
2202
|
+
}), [client]);
|
|
1492
2203
|
const site = useMemo(() => ({
|
|
1493
2204
|
settings: content.settings,
|
|
1494
2205
|
menus: content.menus,
|
|
1495
2206
|
path,
|
|
1496
2207
|
collections: content.collections,
|
|
2208
|
+
pages: pageLinks(content),
|
|
1497
2209
|
collection,
|
|
1498
2210
|
...view && { view },
|
|
1499
2211
|
...languages && {
|
|
1500
2212
|
languages,
|
|
1501
2213
|
home: languageHome(loaded, language)
|
|
1502
2214
|
},
|
|
1503
|
-
adminLink
|
|
2215
|
+
adminLink,
|
|
2216
|
+
callPlugin
|
|
1504
2217
|
}), [
|
|
1505
2218
|
content,
|
|
1506
2219
|
loaded,
|
|
@@ -1509,7 +2222,8 @@ function FrameEditor({ open, onOpened }) {
|
|
|
1509
2222
|
collection,
|
|
1510
2223
|
view,
|
|
1511
2224
|
languages,
|
|
1512
|
-
adminLink
|
|
2225
|
+
adminLink,
|
|
2226
|
+
callPlugin
|
|
1513
2227
|
]);
|
|
1514
2228
|
const entry = kind === "entry" && collection && open.entry ? findEntry(loaded, collection.id, open.entry) : void 0;
|
|
1515
2229
|
const config = useMemo(() => {
|
|
@@ -1520,7 +2234,7 @@ function FrameEditor({ open, onOpened }) {
|
|
|
1520
2234
|
layoutConfig
|
|
1521
2235
|
});
|
|
1522
2236
|
}
|
|
1523
|
-
return canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site);
|
|
2237
|
+
return withTableFields(canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site));
|
|
1524
2238
|
}, [
|
|
1525
2239
|
kind,
|
|
1526
2240
|
collection,
|
|
@@ -1532,6 +2246,11 @@ function FrameEditor({ open, onOpened }) {
|
|
|
1532
2246
|
site
|
|
1533
2247
|
]);
|
|
1534
2248
|
const described = useMemo(() => describeConfig(config), [config]);
|
|
2249
|
+
const puckConfig = useMemo(() => kind === "entry" ? config : withStyleFields(config, content.settings.theme.colors), [
|
|
2250
|
+
kind,
|
|
2251
|
+
config,
|
|
2252
|
+
content.settings.theme.colors
|
|
2253
|
+
]);
|
|
1535
2254
|
const told = useRef(void 0);
|
|
1536
2255
|
useEffect(() => {
|
|
1537
2256
|
if (told.current === described) return;
|
|
@@ -1549,25 +2268,42 @@ function FrameEditor({ open, onOpened }) {
|
|
|
1549
2268
|
...kind === "entry" ? ENTRY_UI : {},
|
|
1550
2269
|
...open.ui
|
|
1551
2270
|
}), [kind, open.ui]);
|
|
2271
|
+
const searchable = useMemo(() => searchableBlocks(config, siteConfig), [config, siteConfig]);
|
|
2272
|
+
const drawerItem = useMemo(() => {
|
|
2273
|
+
const icons = /* @__PURE__ */ new Map();
|
|
2274
|
+
for (const [name, block] of Object.entries(siteConfig.blocks)) {
|
|
2275
|
+
const icon = blockIcon(block);
|
|
2276
|
+
if (icon) icons.set(name, icon);
|
|
2277
|
+
}
|
|
2278
|
+
return drawerItemWithIcon(icons);
|
|
2279
|
+
}, [siteConfig]);
|
|
1552
2280
|
const overrides = useMemo(() => ({
|
|
1553
2281
|
header: () => /* @__PURE__ */ jsx(Bridge, {
|
|
1554
2282
|
channel: client.channel,
|
|
1555
2283
|
settings
|
|
1556
2284
|
}),
|
|
2285
|
+
drawer: ({ children }) => /* @__PURE__ */ jsx(BlockSearch, {
|
|
2286
|
+
blocks: searchable,
|
|
2287
|
+
item: drawerItem,
|
|
2288
|
+
children
|
|
2289
|
+
}),
|
|
2290
|
+
drawerItem,
|
|
1557
2291
|
fieldTypes: { text: TextFieldWithMedia },
|
|
1558
2292
|
...kind === "page" && path && { fields: ({ children, itemSelector }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [children, !itemSelector && /* @__PURE__ */ jsx(SharePreview, { path })] }) }
|
|
1559
2293
|
}), [
|
|
1560
2294
|
client,
|
|
1561
2295
|
settings,
|
|
1562
2296
|
kind,
|
|
1563
|
-
path
|
|
2297
|
+
path,
|
|
2298
|
+
searchable,
|
|
2299
|
+
drawerItem
|
|
1564
2300
|
]);
|
|
1565
2301
|
return /* @__PURE__ */ jsx("div", {
|
|
1566
2302
|
className: `gfa-editor-puck gfa-editor-for-${kind}`,
|
|
1567
2303
|
children: /* @__PURE__ */ jsx(SiteProvider, {
|
|
1568
2304
|
value: site,
|
|
1569
2305
|
children: /* @__PURE__ */ jsx(Puck, {
|
|
1570
|
-
config,
|
|
2306
|
+
config: puckConfig,
|
|
1571
2307
|
data: initialData,
|
|
1572
2308
|
metadata: siteMetadata(site),
|
|
1573
2309
|
headerTitle: title,
|
|
@@ -1575,6 +2311,7 @@ function FrameEditor({ open, onOpened }) {
|
|
|
1575
2311
|
iframe: { enabled: false },
|
|
1576
2312
|
ui,
|
|
1577
2313
|
...kind === "entry" && { plugins: entryPlugins },
|
|
2314
|
+
...open.readOnly && { permissions: READ_ONLY },
|
|
1578
2315
|
overrides,
|
|
1579
2316
|
onChange: updateView
|
|
1580
2317
|
})
|
|
@@ -1676,7 +2413,8 @@ function CodeService({ channel }) {
|
|
|
1676
2413
|
const stops = [
|
|
1677
2414
|
channel.handle("code.sitePages", ({ content, today }) => code.sitePages(content, today)),
|
|
1678
2415
|
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))
|
|
2416
|
+
channel.handle("code.renderedProblems", ({ content, targets, today }) => code.renderedProblems(content, targets, today)),
|
|
2417
|
+
channel.handle("code.drawBlock", ({ content, target }) => code.drawBlock(content, target))
|
|
1680
2418
|
];
|
|
1681
2419
|
return () => {
|
|
1682
2420
|
for (const stop of stops) stop();
|