@goodfellow-cms/admin 0.6.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/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";
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
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";
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";
5
5
  import { createRoot } from "react-dom/client";
6
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
- import { PageBody, PageContent, SiteProvider, adminSite, applyEntry, createPuckConfig, createPuckConfigs, cx, isTableField, mediaFieldKind, preparePage, siteMetadata, tableCells } from "@goodfellow-cms/react";
8
- import { AutoField, FieldLabel, Puck, Render, RichTextMenu, createUsePuck, legacySideBarPlugin, migrate, registerOverlayPortal, resolveAllData, useGetPuck } from "@puckeditor/core";
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";
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(),
@@ -1304,6 +1428,384 @@ function shownSettings(original, path) {
1304
1428
  };
1305
1429
  }
1306
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
1307
1809
  //#region src/frame/table-field.tsx
1308
1810
  /**
1309
1811
  * The grid of cells a table's field is edited in (`tableField()` in
@@ -1608,6 +2110,14 @@ const ENTRY_UI = {
1608
2110
  };
1609
2111
  const entryPlugins = [legacySideBarPlugin()];
1610
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
+ };
1611
2121
  function FrameEditor({ open, onOpened }) {
1612
2122
  const client = useAdminClient();
1613
2123
  const { config: siteConfig, pageConfig, layoutConfig, templateConfig } = useFrameCode();
@@ -1736,6 +2246,11 @@ function FrameEditor({ open, onOpened }) {
1736
2246
  site
1737
2247
  ]);
1738
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
+ ]);
1739
2254
  const told = useRef(void 0);
1740
2255
  useEffect(() => {
1741
2256
  if (told.current === described) return;
@@ -1753,25 +2268,42 @@ function FrameEditor({ open, onOpened }) {
1753
2268
  ...kind === "entry" ? ENTRY_UI : {},
1754
2269
  ...open.ui
1755
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]);
1756
2280
  const overrides = useMemo(() => ({
1757
2281
  header: () => /* @__PURE__ */ jsx(Bridge, {
1758
2282
  channel: client.channel,
1759
2283
  settings
1760
2284
  }),
2285
+ drawer: ({ children }) => /* @__PURE__ */ jsx(BlockSearch, {
2286
+ blocks: searchable,
2287
+ item: drawerItem,
2288
+ children
2289
+ }),
2290
+ drawerItem,
1761
2291
  fieldTypes: { text: TextFieldWithMedia },
1762
2292
  ...kind === "page" && path && { fields: ({ children, itemSelector }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [children, !itemSelector && /* @__PURE__ */ jsx(SharePreview, { path })] }) }
1763
2293
  }), [
1764
2294
  client,
1765
2295
  settings,
1766
2296
  kind,
1767
- path
2297
+ path,
2298
+ searchable,
2299
+ drawerItem
1768
2300
  ]);
1769
2301
  return /* @__PURE__ */ jsx("div", {
1770
2302
  className: `gfa-editor-puck gfa-editor-for-${kind}`,
1771
2303
  children: /* @__PURE__ */ jsx(SiteProvider, {
1772
2304
  value: site,
1773
2305
  children: /* @__PURE__ */ jsx(Puck, {
1774
- config,
2306
+ config: puckConfig,
1775
2307
  data: initialData,
1776
2308
  metadata: siteMetadata(site),
1777
2309
  headerTitle: title,
@@ -1779,6 +2311,7 @@ function FrameEditor({ open, onOpened }) {
1779
2311
  iframe: { enabled: false },
1780
2312
  ui,
1781
2313
  ...kind === "entry" && { plugins: entryPlugins },
2314
+ ...open.readOnly && { permissions: READ_ONLY },
1782
2315
  overrides,
1783
2316
  onChange: updateView
1784
2317
  })