@goodfellow-cms/admin 0.6.0 → 0.8.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 { Ft as storedEntryFields, G as READY_MESSAGE, Pt as storedData, R as createMediaPreviews, Rt as editedMarkdown, a as AdminContext, at as findPage, b as MediaChooser, ct as mediaUrl, et as StringsProvider, f as useDarkMode, h as ScheduleFields, i as usePreviewStyles, l as useSiteContent, rt as useStrings, s as useAdmin, t as SCHEDULE_PROP, u as setThemeChoice, w as SETTINGS_SECTIONS, y as MediaChoiceContext, z as showMediaInPreview, zt as formattedHtml } from "./entry-values-BrY3L0Ai.js";
2
- import { r as createChannel } from "./plugin-protocol-DkvBDqsF.js";
3
- import { COLOR_SCHEME_ATTRIBUTE, DRAWN_SLOT_ELEMENT, MEDIA_DIR, WEEKDAYS, allPages, canFormatMarkdown, cleanEventValue, contentInLanguage, datePart, escapeStyleText, findEntry, formatDate, formatOccurrence, getPageHead, googleFontsUrl, hasLanguages, isAllDay, isEventLike, languageHome, languageLinks, mainLanguage, neverIndexed, pageLinks, pageView, pathLanguage, renderedProblems, shownOccurrence, sitePages, themeToCss, todayIn, wallClock, weekdayName, weekdayOf } from "@goodfellow-cms/core";
4
- import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
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";
4
+ import { Box } from "lucide-react";
5
+ import { createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
5
6
  import { createRoot } from "react-dom/client";
6
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
- import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, createPuckConfig, createPuckConfigs, cx, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
8
- import { AutoField, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
8
+ import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, blockIcon, blockKeywords, createPuckConfig, createPuckConfigs, cx, isStyleField, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
9
+ import { AutoField, Drawer, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
9
10
  import { createPortal, flushSync } from "react-dom";
10
11
  const CANVAS = `.gf-canvas`;
11
12
  const CANVAS_CONTAINER = "gf-canvas";
@@ -299,6 +300,128 @@ function SharePreview({ path }) {
299
300
  });
300
301
  }
301
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
302
425
  //#region src/event-field.tsx
303
426
  const EVERY_LABELS = {
304
427
  none: "event.repeat.none",
@@ -1199,6 +1322,7 @@ function FrameProviders({ config, client, init, site, children }) {
1199
1322
  drafts: {
1200
1323
  kept: true,
1201
1324
  names: [],
1325
+ reviews: [],
1202
1326
  open: async () => refused(),
1203
1327
  publishAll: async () => refused(),
1204
1328
  submit: async () => refused(),
@@ -1230,17 +1354,24 @@ function FrameProviders({ config, client, init, site, children }) {
1230
1354
  kind,
1231
1355
  value
1232
1356
  }), [client]);
1357
+ const text = useMemo(() => ({
1358
+ puck: init.puck ?? {},
1359
+ labels: init.labels ?? {}
1360
+ }), [init.puck, init.labels]);
1233
1361
  return /* @__PURE__ */ jsx(StringsProvider, {
1234
1362
  strings: init.strings,
1235
- children: /* @__PURE__ */ jsx(AdminClientContext.Provider, {
1236
- value: client,
1237
- children: /* @__PURE__ */ jsx(FrameCodeContext.Provider, {
1238
- value: code,
1239
- children: /* @__PURE__ */ jsx(AdminContext.Provider, {
1240
- value: admin,
1241
- children: /* @__PURE__ */ jsx(MediaChoiceContext.Provider, {
1242
- value: chooseMedia,
1243
- children
1363
+ children: /* @__PURE__ */ jsx(EditorTextContext.Provider, {
1364
+ value: text,
1365
+ children: /* @__PURE__ */ jsx(AdminClientContext.Provider, {
1366
+ value: client,
1367
+ children: /* @__PURE__ */ jsx(FrameCodeContext.Provider, {
1368
+ value: code,
1369
+ children: /* @__PURE__ */ jsx(AdminContext.Provider, {
1370
+ value: admin,
1371
+ children: /* @__PURE__ */ jsx(MediaChoiceContext.Provider, {
1372
+ value: chooseMedia,
1373
+ children
1374
+ })
1244
1375
  })
1245
1376
  })
1246
1377
  })
@@ -1248,6 +1379,121 @@ function FrameProviders({ config, client, init, site, children }) {
1248
1379
  });
1249
1380
  }
1250
1381
  //#endregion
1382
+ //#region src/frame/labels.ts
1383
+ /** A label in the admin panel's language, or as it is where the table has none. */
1384
+ function translated(labels, text) {
1385
+ return text === void 0 ? text : labels[text] ?? text;
1386
+ }
1387
+ function withFieldLabels(field, labels) {
1388
+ return {
1389
+ ...field,
1390
+ ...field.label !== void 0 && { label: translated(labels, field.label) },
1391
+ ...typeof field.placeholder === "string" && { placeholder: translated(labels, field.placeholder) },
1392
+ ...Array.isArray(field.options) && { options: field.options.map((option) => ({
1393
+ ...option,
1394
+ label: translated(labels, option.label) ?? option.label
1395
+ })) },
1396
+ ...field.arrayFields && { arrayFields: withFieldsLabels(field.arrayFields, labels) },
1397
+ ...field.objectFields && { objectFields: withFieldsLabels(field.objectFields, labels) }
1398
+ };
1399
+ }
1400
+ function withFieldsLabels(fields, labels) {
1401
+ return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, withFieldLabels(field, labels)]));
1402
+ }
1403
+ function withComponentLabels(component, labels) {
1404
+ if (!component) return component;
1405
+ const { resolveFields, label } = component;
1406
+ return {
1407
+ ...component,
1408
+ ...label !== void 0 && { label: translated(labels, label) },
1409
+ ...component.fields && { fields: withFieldsLabels(component.fields, labels) },
1410
+ ...resolveFields && { resolveFields: async (...args) => withFieldsLabels(await resolveFields(...args), labels) }
1411
+ };
1412
+ }
1413
+ /**
1414
+ * The config with blocks' names, groups and settings' labels, choices and
1415
+ * placeholders in the admin panel's language, looked up by their English
1416
+ * wording, so a site's own blocks that word theirs the same way are
1417
+ * translated too. Only what the editor shows changes: props, choices' values
1418
+ * and what blocks render stay as they are.
1419
+ */
1420
+ function withLabels(config, labels) {
1421
+ if (Object.keys(labels).length === 0) return config;
1422
+ const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, withComponentLabels(component, labels)]));
1423
+ const categories = config.categories && Object.fromEntries(Object.entries(config.categories).map(([name, category]) => [name, {
1424
+ ...category,
1425
+ ...category.title !== void 0 && { title: translated(labels, category.title) }
1426
+ }]));
1427
+ return {
1428
+ ...config,
1429
+ components,
1430
+ ...categories && { categories },
1431
+ ...config.root && { root: withComponentLabels(config.root, labels) }
1432
+ };
1433
+ }
1434
+ //#endregion
1435
+ //#region src/frame/owners-only.tsx
1436
+ /**
1437
+ * Block settings only owners may change (`metadata: { owners: true }`, such as
1438
+ * Custom HTML used exactly as written), shown to editors on full-stack sites
1439
+ * as they are, without a way to change them. Their server refuses such a
1440
+ * change anyway (`ownersOnlyChanges()`); this only says so up front.
1441
+ */
1442
+ function valueLabel(field, value) {
1443
+ const option = ("options" in field && Array.isArray(field.options) ? field.options : []).find((choice) => JSON.stringify(choice.value) === JSON.stringify(value));
1444
+ if (option) return String(option.label);
1445
+ return value === void 0 || value === null ? "" : String(value);
1446
+ }
1447
+ function LockedField({ field, value }) {
1448
+ const t = useStrings();
1449
+ return /* @__PURE__ */ jsxs("div", {
1450
+ className: "gfa-locked-field",
1451
+ children: [
1452
+ field.label && /* @__PURE__ */ jsx("div", {
1453
+ className: "gfa-locked-field-label",
1454
+ children: field.label
1455
+ }),
1456
+ /* @__PURE__ */ jsx("div", {
1457
+ className: "gfa-locked-field-value",
1458
+ children: valueLabel(field, value)
1459
+ }),
1460
+ /* @__PURE__ */ jsx("p", {
1461
+ className: "gfa-locked-field-note",
1462
+ children: t("owners.only")
1463
+ })
1464
+ ]
1465
+ });
1466
+ }
1467
+ function locked(fields) {
1468
+ return Object.fromEntries(Object.entries(fields).map(([name, field]) => {
1469
+ if (!isOwnersOnlyField(field)) return [name, field];
1470
+ return [name, {
1471
+ type: "custom",
1472
+ ...field.label !== void 0 && { label: field.label },
1473
+ ...field.metadata !== void 0 && { metadata: field.metadata },
1474
+ render: ({ value }) => /* @__PURE__ */ jsx(LockedField, {
1475
+ field,
1476
+ value
1477
+ })
1478
+ }];
1479
+ }));
1480
+ }
1481
+ /** The config with owners-only settings shown but not changeable, for editors who aren't owners. */
1482
+ function withOwnersOnlyLocked(config) {
1483
+ const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => {
1484
+ const fields = component.fields ?? {};
1485
+ if (!Object.values(fields).some(isOwnersOnlyField)) return [name, component];
1486
+ return [name, {
1487
+ ...component,
1488
+ fields: locked(fields)
1489
+ }];
1490
+ }));
1491
+ return {
1492
+ ...config,
1493
+ components
1494
+ };
1495
+ }
1496
+ //#endregion
1251
1497
  //#region src/frame/page-settings.ts
1252
1498
  /** For pages and templates, whose settings include `noIndex`; `path` is the page's, for pages. */
1253
1499
  function shownSettings(original, path) {
@@ -1304,6 +1550,420 @@ function shownSettings(original, path) {
1304
1550
  };
1305
1551
  }
1306
1552
  //#endregion
1553
+ //#region src/frame/style-field.tsx
1554
+ /**
1555
+ * A block's Style field (`classNameField` in `@goodfellow-cms/react`), which
1556
+ * Puck's editor shows as controls for its look in place of a box of classes:
1557
+ * text alignment, size and color, background, space inside and around it,
1558
+ * rounded corners and the sizes of screen it's hidden on, written into its `className` as Tailwind classes
1559
+ * (`withBlockStyle()` in `@goodfellow-cms/core`), with any other classes kept.
1560
+ */
1561
+ /** A color's value to show and compare: a theme color's, or the editor's own. */
1562
+ function shownColor(value, colors) {
1563
+ if (!value) return void 0;
1564
+ const theme = themeColorOf(value);
1565
+ return theme ? colors[theme] ?? DEFAULT_THEME_COLORS[theme] : value;
1566
+ }
1567
+ /**
1568
+ * A choice from a scale, or a size of the editor's own: the scale's steps,
1569
+ * "As the block has it" and "Your own…", which shows a box for a length.
1570
+ */
1571
+ function ScaleChoice({ label, value, steps, stepLabel, readOnly, onChange }) {
1572
+ const t = useStrings();
1573
+ const id = useId();
1574
+ const custom = value !== void 0 && !steps.includes(value);
1575
+ const [ownChosen, setOwnChosen] = useState(custom);
1576
+ const [typed, setTyped] = useState(custom ? value : "");
1577
+ useEffect(() => {
1578
+ if (value !== void 0 && !steps.includes(value)) {
1579
+ setOwnChosen(true);
1580
+ setTyped(value);
1581
+ }
1582
+ }, [value, steps]);
1583
+ const own = custom || ownChosen;
1584
+ const invalid = own && typed.trim() !== "" && customLength(typed) === void 0;
1585
+ return /* @__PURE__ */ jsxs("div", {
1586
+ className: "gfa-style-choice",
1587
+ children: [
1588
+ /* @__PURE__ */ jsx("label", {
1589
+ className: "gfa-style-label",
1590
+ htmlFor: id,
1591
+ children: label
1592
+ }),
1593
+ /* @__PURE__ */ jsxs("select", {
1594
+ id,
1595
+ className: "gfa-input",
1596
+ disabled: readOnly,
1597
+ value: own ? "custom" : value ?? "",
1598
+ onChange: (event) => {
1599
+ const chosen = event.target.value;
1600
+ if (chosen === "custom") {
1601
+ setOwnChosen(true);
1602
+ const length = customLength(typed);
1603
+ if (length) onChange(length);
1604
+ return;
1605
+ }
1606
+ setOwnChosen(false);
1607
+ onChange(chosen === "" ? void 0 : chosen);
1608
+ },
1609
+ children: [
1610
+ /* @__PURE__ */ jsx("option", {
1611
+ value: "",
1612
+ children: t("style.default")
1613
+ }),
1614
+ steps.map((step) => /* @__PURE__ */ jsx("option", {
1615
+ value: step,
1616
+ children: stepLabel(step)
1617
+ }, step)),
1618
+ /* @__PURE__ */ jsx("option", {
1619
+ value: "custom",
1620
+ children: t("style.custom")
1621
+ })
1622
+ ]
1623
+ }),
1624
+ own && /* @__PURE__ */ jsx("input", {
1625
+ className: "gfa-input",
1626
+ value: typed,
1627
+ readOnly,
1628
+ "aria-label": `${label}: ${t("style.customLength")}`,
1629
+ placeholder: t("style.customLength"),
1630
+ "aria-invalid": invalid,
1631
+ onChange: (event) => {
1632
+ setTyped(event.target.value);
1633
+ const length = customLength(event.target.value);
1634
+ if (length) onChange(length);
1635
+ }
1636
+ }),
1637
+ invalid && /* @__PURE__ */ jsx("p", {
1638
+ className: "gfa-error-text",
1639
+ children: t("style.customLengthInvalid")
1640
+ })
1641
+ ]
1642
+ });
1643
+ }
1644
+ /** Spacing steps as people read them: `4` is 16px. */
1645
+ function spacingLabel(t, step) {
1646
+ return step === "0" ? t("style.none") : `${Number(step) * 4}px`;
1647
+ }
1648
+ /** Space on every side at once, or on each side. */
1649
+ function SidesChoice({ label, sides, readOnly, onChange }) {
1650
+ const t = useStrings();
1651
+ const values = SIDES.map((side) => sides?.[side]);
1652
+ const same = values.every((value) => value === values[0]);
1653
+ const [each, setEach] = useState(!same);
1654
+ useEffect(() => {
1655
+ if (!same) setEach(true);
1656
+ }, [same]);
1657
+ const set = (next) => {
1658
+ const kept = Object.fromEntries(Object.entries(next).filter(([, value]) => value !== void 0));
1659
+ onChange(Object.keys(kept).length > 0 ? kept : void 0);
1660
+ };
1661
+ const stepLabel = (step) => spacingLabel(t, step);
1662
+ return /* @__PURE__ */ jsxs("fieldset", {
1663
+ className: "gfa-style-group",
1664
+ children: [
1665
+ /* @__PURE__ */ jsx("legend", {
1666
+ className: "gfa-style-label",
1667
+ children: label
1668
+ }),
1669
+ each ? /* @__PURE__ */ jsx("div", {
1670
+ className: "gfa-style-sides",
1671
+ children: SIDES.map((side) => /* @__PURE__ */ jsx(ScaleChoice, {
1672
+ label: t(`style.side.${side}`),
1673
+ value: sides?.[side],
1674
+ steps: SPACING_STEPS,
1675
+ stepLabel,
1676
+ readOnly,
1677
+ onChange: (value) => set({
1678
+ ...sides,
1679
+ [side]: value
1680
+ })
1681
+ }, side))
1682
+ }) : /* @__PURE__ */ jsx(ScaleChoice, {
1683
+ label: t("style.allSides"),
1684
+ value: values[0],
1685
+ steps: SPACING_STEPS,
1686
+ stepLabel,
1687
+ readOnly,
1688
+ onChange: (value) => set(Object.fromEntries(SIDES.map((side) => [side, value])))
1689
+ }),
1690
+ /* @__PURE__ */ jsxs("label", {
1691
+ className: "gfa-style-check",
1692
+ children: [/* @__PURE__ */ jsx("input", {
1693
+ type: "checkbox",
1694
+ checked: each,
1695
+ disabled: readOnly || each && !same,
1696
+ onChange: (event) => setEach(event.target.checked)
1697
+ }), t("style.eachSide")]
1698
+ })
1699
+ ]
1700
+ });
1701
+ }
1702
+ /** The theme's colors as swatches, a color of the editor's own, and back to the block's. */
1703
+ function ColorChoice({ label, value, colors, readOnly, onChange }) {
1704
+ const t = useStrings();
1705
+ const own = value !== void 0 && isCustomColor(value);
1706
+ return /* @__PURE__ */ jsxs("fieldset", {
1707
+ className: "gfa-style-group",
1708
+ children: [/* @__PURE__ */ jsx("legend", {
1709
+ className: "gfa-style-label",
1710
+ children: label
1711
+ }), /* @__PURE__ */ jsxs("div", {
1712
+ className: "gfa-style-swatches",
1713
+ children: [
1714
+ /* @__PURE__ */ jsx("button", {
1715
+ type: "button",
1716
+ className: "gfa-style-swatch gfa-style-swatch-none",
1717
+ "aria-pressed": value === void 0,
1718
+ disabled: readOnly,
1719
+ title: t("style.colorDefault"),
1720
+ "aria-label": t("style.colorDefault"),
1721
+ onClick: () => onChange(void 0)
1722
+ }),
1723
+ THEME_COLORS.map((color) => {
1724
+ const slug = colorSlug(color);
1725
+ const name = t(`theme.color.${color}`);
1726
+ return /* @__PURE__ */ jsx("button", {
1727
+ type: "button",
1728
+ className: "gfa-style-swatch",
1729
+ style: { background: colors[color] ?? DEFAULT_THEME_COLORS[color] },
1730
+ "aria-pressed": value === slug,
1731
+ disabled: readOnly,
1732
+ title: name,
1733
+ "aria-label": name,
1734
+ onClick: () => onChange(slug)
1735
+ }, color);
1736
+ }),
1737
+ /* @__PURE__ */ jsx("label", {
1738
+ className: "gfa-style-swatch gfa-style-swatch-own",
1739
+ title: t("style.colorCustom"),
1740
+ children: /* @__PURE__ */ jsx("input", {
1741
+ type: "color",
1742
+ value: own ? value : "#000000",
1743
+ disabled: readOnly,
1744
+ "aria-label": `${label}: ${t("style.colorCustom")}`,
1745
+ onChange: (event) => onChange(event.target.value.toLowerCase())
1746
+ })
1747
+ })
1748
+ ]
1749
+ })]
1750
+ });
1751
+ }
1752
+ /** The sizes of screen a block is hidden on, a box for each. */
1753
+ function HiddenOnChoice({ hiddenOn, readOnly, onChange }) {
1754
+ const t = useStrings();
1755
+ const hidden = new Set(hiddenOn);
1756
+ return /* @__PURE__ */ jsxs("fieldset", {
1757
+ className: "gfa-style-group",
1758
+ children: [
1759
+ /* @__PURE__ */ jsx("legend", {
1760
+ className: "gfa-style-label",
1761
+ children: t("style.hiddenOn")
1762
+ }),
1763
+ SCREEN_SIZES.map((size) => /* @__PURE__ */ jsxs("label", {
1764
+ className: "gfa-style-check",
1765
+ children: [/* @__PURE__ */ jsx("input", {
1766
+ type: "checkbox",
1767
+ checked: hidden.has(size),
1768
+ disabled: readOnly,
1769
+ onChange: (event) => {
1770
+ const next = SCREEN_SIZES.filter((candidate) => candidate === size ? event.target.checked : hidden.has(candidate));
1771
+ onChange(next.length > 0 ? next : void 0);
1772
+ }
1773
+ }), t(`style.hiddenOn.${size}`)]
1774
+ }, size)),
1775
+ hidden.size > 0 && /* @__PURE__ */ jsx("p", {
1776
+ className: "gfa-hint",
1777
+ children: t(hidden.size === SCREEN_SIZES.length ? "style.hiddenOnAll" : "style.hiddenOnHint")
1778
+ })
1779
+ ]
1780
+ });
1781
+ }
1782
+ /** Other classes, kept as typed while the box has focus, so a class isn't taken into the controls halfway. */
1783
+ function OtherClasses({ other, readOnly, onChange }) {
1784
+ const t = useStrings();
1785
+ const id = useId();
1786
+ const joined = other.join(" ");
1787
+ const [typed, setTyped] = useState(joined);
1788
+ const [focused, setFocused] = useState(false);
1789
+ useEffect(() => {
1790
+ if (!focused) setTyped(joined);
1791
+ }, [joined, focused]);
1792
+ return /* @__PURE__ */ jsxs("div", {
1793
+ className: "gfa-style-choice",
1794
+ children: [
1795
+ /* @__PURE__ */ jsx("label", {
1796
+ className: "gfa-style-label",
1797
+ htmlFor: id,
1798
+ children: t("style.other")
1799
+ }),
1800
+ /* @__PURE__ */ jsx("input", {
1801
+ id,
1802
+ className: "gfa-input gfa-code",
1803
+ value: typed,
1804
+ readOnly,
1805
+ spellCheck: false,
1806
+ onFocus: () => setFocused(true),
1807
+ onBlur: () => setFocused(false),
1808
+ onChange: (event) => {
1809
+ setTyped(event.target.value);
1810
+ onChange(event.target.value.split(/\s+/).filter(Boolean));
1811
+ }
1812
+ }),
1813
+ /* @__PURE__ */ jsx("p", {
1814
+ className: "gfa-hint",
1815
+ children: t("style.otherHint")
1816
+ })
1817
+ ]
1818
+ });
1819
+ }
1820
+ function StyleControls({ value, colors, readOnly, label, ownAlign, block, onChange }) {
1821
+ const t = useStrings();
1822
+ const { style, other } = readBlockStyle(value);
1823
+ const set = (next) => onChange(withBlockStyle(other, next));
1824
+ const change = (key, next) => {
1825
+ const { [key]: _old, ...rest } = style;
1826
+ set(next === void 0 ? rest : {
1827
+ ...rest,
1828
+ [key]: next
1829
+ });
1830
+ };
1831
+ const text = shownColor(style.color, colors);
1832
+ const background = shownColor(style.background, colors);
1833
+ const ratio = text && background ? contrastRatio(text, background) : void 0;
1834
+ return /* @__PURE__ */ jsxs("div", {
1835
+ className: "gfa-style-field",
1836
+ children: [
1837
+ label && /* @__PURE__ */ jsx("div", {
1838
+ className: "gfa-style-title",
1839
+ children: label
1840
+ }),
1841
+ !ownAlign && /* @__PURE__ */ jsxs("fieldset", {
1842
+ className: "gfa-style-group",
1843
+ children: [/* @__PURE__ */ jsx("legend", {
1844
+ className: "gfa-style-label",
1845
+ children: t("style.align")
1846
+ }), /* @__PURE__ */ jsx("div", {
1847
+ className: "gfa-style-segments",
1848
+ children: [void 0, ...TEXT_ALIGNS].map((align) => /* @__PURE__ */ jsx("button", {
1849
+ type: "button",
1850
+ className: "gfa-style-segment",
1851
+ "aria-pressed": style.align === align,
1852
+ disabled: readOnly,
1853
+ onClick: () => change("align", align),
1854
+ children: t(`style.align.${align ?? "default"}`)
1855
+ }, align ?? "default"))
1856
+ })]
1857
+ }),
1858
+ /* @__PURE__ */ jsx(ScaleChoice, {
1859
+ label: t("style.fontSize"),
1860
+ value: style.fontSize,
1861
+ steps: FONT_SIZES,
1862
+ stepLabel: (step) => t(`style.fontSize.${step}`),
1863
+ readOnly,
1864
+ onChange: (size) => change("fontSize", size)
1865
+ }),
1866
+ /* @__PURE__ */ jsx(ColorChoice, {
1867
+ label: t("style.color"),
1868
+ value: style.color,
1869
+ colors,
1870
+ readOnly,
1871
+ onChange: (color) => change("color", color)
1872
+ }),
1873
+ /* @__PURE__ */ jsx(ColorChoice, {
1874
+ label: t("style.background"),
1875
+ value: style.background,
1876
+ colors,
1877
+ readOnly,
1878
+ onChange: (color) => change("background", color)
1879
+ }),
1880
+ ratio !== void 0 && ratio < MINIMUM_CONTRAST && /* @__PURE__ */ jsx("p", {
1881
+ className: "gfa-hint gfa-style-warning",
1882
+ role: "status",
1883
+ children: t("style.contrast")
1884
+ }),
1885
+ /* @__PURE__ */ jsx(SidesChoice, {
1886
+ label: t("style.padding"),
1887
+ sides: style.padding,
1888
+ readOnly,
1889
+ onChange: (sides) => change("padding", sides)
1890
+ }),
1891
+ /* @__PURE__ */ jsx(SidesChoice, {
1892
+ label: t("style.margin"),
1893
+ sides: style.margin,
1894
+ readOnly,
1895
+ onChange: (sides) => change("margin", sides)
1896
+ }),
1897
+ /* @__PURE__ */ jsx(ScaleChoice, {
1898
+ label: t("style.radius"),
1899
+ value: style.radius,
1900
+ steps: RADII,
1901
+ stepLabel: (step) => t(`style.radius.${step}`),
1902
+ readOnly,
1903
+ onChange: (radius) => change("radius", radius)
1904
+ }),
1905
+ (block || style.hiddenOn) && /* @__PURE__ */ jsx(HiddenOnChoice, {
1906
+ hiddenOn: style.hiddenOn,
1907
+ readOnly,
1908
+ onChange: (hiddenOn) => change("hiddenOn", hiddenOn)
1909
+ }),
1910
+ /* @__PURE__ */ jsx(OtherClasses, {
1911
+ other,
1912
+ readOnly,
1913
+ onChange: (next) => onChange(withBlockStyle(next, style))
1914
+ }),
1915
+ /* @__PURE__ */ jsx("button", {
1916
+ type: "button",
1917
+ className: "gfa-button gfa-button-secondary",
1918
+ disabled: readOnly || Object.keys(style).length === 0 && other.length === 0,
1919
+ onClick: () => onChange(""),
1920
+ children: t("style.reset")
1921
+ })
1922
+ ]
1923
+ });
1924
+ }
1925
+ function withStyleField(field, colors, ownAlign, block) {
1926
+ if (field.type !== "text" || !isStyleField(field)) return field;
1927
+ const render = ({ value, onChange, readOnly }) => /* @__PURE__ */ jsx(StyleControls, {
1928
+ value,
1929
+ colors,
1930
+ readOnly,
1931
+ label: field.label,
1932
+ ownAlign,
1933
+ block,
1934
+ onChange
1935
+ });
1936
+ return {
1937
+ type: "custom",
1938
+ label: field.label,
1939
+ metadata: field.metadata,
1940
+ render
1941
+ };
1942
+ }
1943
+ function withStyleFieldsIn(fields, colors, block) {
1944
+ if (!fields) return fields;
1945
+ const ownAlign = "align" in fields;
1946
+ return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, withStyleField(field, colors, ownAlign, block)]));
1947
+ }
1948
+ function withStyleComponent(component, colors, block) {
1949
+ if (!component) return component;
1950
+ const { resolveFields } = component;
1951
+ return {
1952
+ ...component,
1953
+ ...component.fields && { fields: withStyleFieldsIn(component.fields, colors, block) },
1954
+ ...resolveFields && { resolveFields: async (...args) => withStyleFieldsIn(await resolveFields(...args), colors, block) }
1955
+ };
1956
+ }
1957
+ /** The config with every block's Style field, and the page's, drawn as a style's controls. */
1958
+ function withStyleFields(config, colors) {
1959
+ const components = Object.fromEntries(Object.entries(config.components).map(([name, component]) => [name, withStyleComponent(component, colors, true)]));
1960
+ return {
1961
+ ...config,
1962
+ components,
1963
+ ...config.root && { root: withStyleComponent(config.root, colors, false) }
1964
+ };
1965
+ }
1966
+ //#endregion
1307
1967
  //#region src/frame/table-field.tsx
1308
1968
  /**
1309
1969
  * The grid of cells a table's field is edited in (`tableField()` in
@@ -1430,7 +2090,8 @@ const ADMIN_PLACES = {
1430
2090
  "contact-settings": `#/settings/general?section=${SETTINGS_SECTIONS.contact}`,
1431
2091
  "forms-settings": `#/settings/general?section=${SETTINGS_SECTIONS.forms}`,
1432
2092
  "theme-settings": "#/settings/theme",
1433
- "language-settings": "#/settings/languages"
2093
+ "language-settings": "#/settings/languages",
2094
+ "comments-settings": "#/settings/comments"
1434
2095
  };
1435
2096
  /** Puck's text field, with the media library's chooser below it for fields that hold a media address. */
1436
2097
  function TextFieldWithMedia({ field, value, onChange, readOnly, children }) {
@@ -1608,6 +2269,14 @@ const ENTRY_UI = {
1608
2269
  };
1609
2270
  const entryPlugins = [legacySideBarPlugin()];
1610
2271
  /** Puck, set up for one page, header, footer, template or item: site context, previews and the bridge. */
2272
+ /** Puck's permissions for something shown without editing: nothing can be changed, added, moved or removed. */
2273
+ const READ_ONLY = {
2274
+ drag: false,
2275
+ duplicate: false,
2276
+ delete: false,
2277
+ edit: false,
2278
+ insert: false
2279
+ };
1611
2280
  function FrameEditor({ open, onOpened }) {
1612
2281
  const client = useAdminClient();
1613
2282
  const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
@@ -1716,15 +2385,16 @@ function FrameEditor({ open, onOpened }) {
1716
2385
  callPlugin
1717
2386
  ]);
1718
2387
  const entry = kind === "entry" && collection && open.entry ? findEntry(loaded, collection.id, open.entry) : void 0;
2388
+ const { puck: dictionary, labels } = useEditorText();
1719
2389
  const config = useMemo(() => {
1720
2390
  if (kind === "entry") {
1721
2391
  if (!collection || !entry) throw new Error("The item isn't in the site.");
1722
- return entryConfig(collection, entry.entry, content, {
2392
+ return withLabels(entryConfig(collection, entry.entry, content, {
1723
2393
  templateConfig,
1724
2394
  layoutConfig
1725
- });
2395
+ }), labels);
1726
2396
  }
1727
- return withTableFields(canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site));
2397
+ return withLabels(withTableFields(canvasConfig(kind, kind === "page" ? pageConfig : kind === "template" ? templateConfig : layoutConfig, layoutConfig, content.header.data, content.footer.data, site)), labels);
1728
2398
  }, [
1729
2399
  kind,
1730
2400
  collection,
@@ -1733,9 +2403,19 @@ function FrameEditor({ open, onOpened }) {
1733
2403
  templateConfig,
1734
2404
  layoutConfig,
1735
2405
  content,
1736
- site
2406
+ site,
2407
+ labels
1737
2408
  ]);
1738
2409
  const described = useMemo(() => describeConfig(config), [config]);
2410
+ const puckConfig = useMemo(() => {
2411
+ const styled = kind === "entry" ? config : withStyleFields(config, content.settings.theme.colors);
2412
+ return open.ownersOnlyLocked ? withOwnersOnlyLocked(styled) : styled;
2413
+ }, [
2414
+ kind,
2415
+ config,
2416
+ content.settings.theme.colors,
2417
+ open.ownersOnlyLocked
2418
+ ]);
1739
2419
  const told = useRef(void 0);
1740
2420
  useEffect(() => {
1741
2421
  if (told.current === described) return;
@@ -1753,32 +2433,51 @@ function FrameEditor({ open, onOpened }) {
1753
2433
  ...kind === "entry" ? ENTRY_UI : {},
1754
2434
  ...open.ui
1755
2435
  }), [kind, open.ui]);
2436
+ const searchable = useMemo(() => searchableBlocks(config, siteConfig), [config, siteConfig]);
2437
+ const drawerItem = useMemo(() => {
2438
+ const icons = /* @__PURE__ */ new Map();
2439
+ for (const [name, block] of Object.entries(siteConfig.blocks)) {
2440
+ const icon = blockIcon(block);
2441
+ if (icon) icons.set(name, icon);
2442
+ }
2443
+ return drawerItemWithIcon(icons);
2444
+ }, [siteConfig]);
1756
2445
  const overrides = useMemo(() => ({
1757
2446
  header: () => /* @__PURE__ */ jsx(Bridge, {
1758
2447
  channel: client.channel,
1759
2448
  settings
1760
2449
  }),
2450
+ drawer: ({ children }) => /* @__PURE__ */ jsx(BlockSearch, {
2451
+ blocks: searchable,
2452
+ item: drawerItem,
2453
+ children
2454
+ }),
2455
+ drawerItem,
1761
2456
  fieldTypes: { text: TextFieldWithMedia },
1762
2457
  ...kind === "page" && path && { fields: ({ children, itemSelector }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [children, !itemSelector && /* @__PURE__ */ jsx(SharePreview, { path })] }) }
1763
2458
  }), [
1764
2459
  client,
1765
2460
  settings,
1766
2461
  kind,
1767
- path
2462
+ path,
2463
+ searchable,
2464
+ drawerItem
1768
2465
  ]);
1769
2466
  return /* @__PURE__ */ jsx("div", {
1770
2467
  className: `gfa-editor-puck gfa-editor-for-${kind}`,
1771
2468
  children: /* @__PURE__ */ jsx(SiteProvider, {
1772
2469
  value: site,
1773
2470
  children: /* @__PURE__ */ jsx(Puck, {
1774
- config,
2471
+ config: puckConfig,
1775
2472
  data: initialData,
1776
2473
  metadata: siteMetadata(site),
1777
2474
  headerTitle: title,
2475
+ dictionary,
1778
2476
  height: "100%",
1779
2477
  iframe: { enabled: false },
1780
2478
  ui,
1781
2479
  ...kind === "entry" && { plugins: entryPlugins },
2480
+ ...open.readOnly && { permissions: READ_ONLY },
1782
2481
  overrides,
1783
2482
  onChange: updateView
1784
2483
  })
@@ -1896,7 +2595,8 @@ function editorKey(open) {
1896
2595
  open.path,
1897
2596
  open.collection,
1898
2597
  open.entry,
1899
- open.language
2598
+ open.language,
2599
+ open.reopened
1900
2600
  ]);
1901
2601
  }
1902
2602
  /** Opens an editor when the admin panel's page asks, and tells it Puck's config once it's open. */