@byscott-io/pagelet 0.1.0 → 0.2.1

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/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
- var rsc = require('@puckeditor/core/rsc');
4
+ var react = require('react');
5
5
 
6
6
  const DOCUMENT_VERSION = 1;
7
7
  function emptyData() {
@@ -10,7 +10,7 @@ function emptyData() {
10
10
  function createDocument(data = emptyData()) {
11
11
  return { pagelet: DOCUMENT_VERSION, data };
12
12
  }
13
- function upgradeDocument(document, palette) {
13
+ function upgradeDocument(document) {
14
14
  if (document == null) return createDocument();
15
15
  if (typeof document !== "object") {
16
16
  throw new TypeError("pagelet: a page document must be an object");
@@ -28,19 +28,121 @@ function upgradeDocument(document, palette) {
28
28
  );
29
29
  }
30
30
  const data = { ...emptyData(), ...doc.data };
31
- return createDocument(palette && needsPuckMigration(data) ? rsc.migrate(data, palette.config) : data);
31
+ return createDocument({ ...data, root: upgradeRoot(data.root) });
32
32
  }
33
- function needsPuckMigration(data) {
34
- const zones = data.zones ?? {};
35
- if (Object.values(zones).some((content) => Array.isArray(content) && content.length > 0)) return true;
36
- const root = data.root ?? {};
37
- return Object.keys(root).some((key) => key !== "props" && key !== "readOnly");
33
+ function upgradeRoot(root = {}) {
34
+ const { props = {}, readOnly, ...legacy } = root;
35
+ if (Object.keys(legacy).length === 0) return root;
36
+ return { ...readOnly ? { readOnly } : {}, props: { ...legacy, ...props } };
37
+ }
38
+ function pageSettings(document) {
39
+ const props = upgradeDocument(document)?.data?.root?.props ?? {};
40
+ return { title: props.title || "", background: props.background ?? "site" };
41
+ }
42
+
43
+ const ROOT_ZONE = "root:default-zone";
44
+ function slotNames(component) {
45
+ return Object.entries(component.fields ?? {}).filter(([, field]) => field?.type === "slot").map(([name]) => name);
46
+ }
47
+ function renderItems(items, context) {
48
+ return (Array.isArray(items) ? items : []).map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderItem(item, context) }, item?.props?.id ?? index));
49
+ }
50
+ function puckFor(areaId, context) {
51
+ return {
52
+ isEditing: false,
53
+ metadata: context.metadata,
54
+ dragRef: null,
55
+ renderDropZone: ({ zone }) => {
56
+ const compound = `${areaId}:${zone}`;
57
+ const items = compound === ROOT_ZONE ? context.data.content : context.data.zones?.[compound];
58
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderItems(items, context) });
59
+ }
60
+ };
61
+ }
62
+ function renderItem(item, context) {
63
+ const component = item && context.config.components[item.type];
64
+ if (!component) return null;
65
+ const props = { ...item.props, puck: puckFor(item.props?.id, context) };
66
+ for (const name of slotNames(component)) {
67
+ const children = item.props?.[name];
68
+ props[name] = function Slot({ className, style }) {
69
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, children: renderItems(children, context) });
70
+ };
71
+ }
72
+ const Render = component.render;
73
+ return /* @__PURE__ */ jsxRuntime.jsx(Render, { ...props });
74
+ }
75
+ function renderPage({ config, data, metadata = {} }) {
76
+ const context = { config, data: data ?? {}, metadata };
77
+ const puck = puckFor("root", context);
78
+ const rootProps = data?.root?.props ?? {};
79
+ const content = renderItems(data?.content, context);
80
+ const RootRender = config.root?.render;
81
+ if (!RootRender) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: content });
82
+ return /* @__PURE__ */ jsxRuntime.jsx(RootRender, { ...rootProps, title: rootProps.title || "", editMode: false, id: "puck-root", puck, children: content });
38
83
  }
39
84
 
40
85
  function PageletPage({ document, palette, theme }) {
41
86
  if (!palette) throw new Error("pagelet: <PageletPage> needs a palette (see createPalette)");
42
- const { data } = upgradeDocument(document, palette);
43
- return /* @__PURE__ */ jsxRuntime.jsx(rsc.Render, { config: palette.config, data, metadata: { pageletTheme: theme } });
87
+ const { data } = upgradeDocument(document);
88
+ return renderPage({ config: palette.config, data, metadata: { pageletTheme: theme } });
89
+ }
90
+
91
+ const outline = { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" };
92
+ const PATHS = {
93
+ location: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 22s7-6.2 7-12a7 7 0 1 0-14 0c0 5.8 7 12 7 12z" }),
95
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "10", r: "2.5" })
96
+ ] }),
97
+ phone: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.5 3h3l1.5 4.5-2 1.5a12 12 0 0 0 6 6l1.5-2L21 14.5v3a2 2 0 0 1-2 2A16 16 0 0 1 4.5 5a2 2 0 0 1 2-2z" }),
98
+ email: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
99
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
100
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 7l9 6 9-6" })
101
+ ] }),
102
+ video: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
103
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2.5", y: "6", width: "13", height: "12", rx: "2" }),
104
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15.5 10.5 21.5 7v10l-6-3.5z" })
105
+ ] }),
106
+ calendar: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
107
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }),
108
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 10h18M8 3v4M16 3v4" })
109
+ ] }),
110
+ clock: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
111
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
112
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 7v5l3 2" })
113
+ ] }),
114
+ link: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
115
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1" }),
116
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" })
117
+ ] }),
118
+ facebook: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
119
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "3" }),
120
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15.5 8H14a2 2 0 0 0-2 2v11M9.5 13h5" })
121
+ ] }),
122
+ youtube: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
123
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2.5", y: "5.5", width: "19", height: "13", rx: "4" }),
124
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.5 9.5v5l4-2.5z" })
125
+ ] })
126
+ };
127
+ const iconNames = Object.keys(PATHS);
128
+ const iconOptions = [{ label: "None", value: "" }].concat(
129
+ iconNames.map((name) => ({ label: name[0].toUpperCase() + name.slice(1), value: name }))
130
+ );
131
+ function Icon({ name, size = "1.1em", style }) {
132
+ const glyph = PATHS[name];
133
+ if (!glyph) return null;
134
+ return /* @__PURE__ */ jsxRuntime.jsx(
135
+ "svg",
136
+ {
137
+ viewBox: "0 0 24 24",
138
+ width: size,
139
+ height: size,
140
+ "aria-hidden": "true",
141
+ focusable: "false",
142
+ style: { ...outline, verticalAlign: "-0.15em", flexShrink: 0, ...style },
143
+ children: glyph
144
+ }
145
+ );
44
146
  }
45
147
 
46
148
  const SAFE_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
@@ -94,8 +196,10 @@ function tokenVar(token) {
94
196
 
95
197
  const alignOptions = [
96
198
  { label: "Left", value: "left" },
97
- { label: "Center", value: "center" }
199
+ { label: "Center", value: "center" },
200
+ { label: "Right", value: "right" }
98
201
  ];
202
+ const blockMargin = (align) => align === "center" ? { marginInline: "auto" } : align === "right" ? { marginLeft: "auto" } : {};
99
203
  function paragraphs(text) {
100
204
  return String(text ?? "").split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
101
205
  }
@@ -157,7 +261,7 @@ const Text = {
157
261
  align: { type: "radio", options: alignOptions }
158
262
  },
159
263
  defaultProps: { text: "Write something here.", align: "left" },
160
- render: ({ text, align }) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: align, maxWidth: "68ch", marginInline: align === "center" ? "auto" : void 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text }) })
264
+ render: ({ text, align }) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: align, maxWidth: "68ch", ...blockMargin(align) }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text }) })
161
265
  };
162
266
  const Button = {
163
267
  label: "Button",
@@ -270,6 +374,79 @@ const Faq = {
270
374
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingTop: "8px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text: item.answer }) })
271
375
  ] }, index)) })
272
376
  };
377
+ const Quote = {
378
+ label: "Quote",
379
+ category: "content",
380
+ fields: {
381
+ text: { type: "textarea" },
382
+ citation: { type: "text", label: "Citation (e.g. Matthew 28:19-20 ESV)" },
383
+ citationAlign: { type: "radio", label: "Citation alignment", options: alignOptions }
384
+ },
385
+ defaultProps: { text: "Quotation", citation: "", citationAlign: "right" },
386
+ render: ({ text, citation, citationAlign }) => /* @__PURE__ */ jsxRuntime.jsxs("figure", { style: { margin: "0 0 1em", maxWidth: "68ch" }, children: [
387
+ /* @__PURE__ */ jsxRuntime.jsx("blockquote", { style: { margin: 0, fontStyle: "italic" }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text, style: { margin: "0 0 0.5em" } }) }),
388
+ citation && /* @__PURE__ */ jsxRuntime.jsx("figcaption", { style: { textAlign: citationAlign ?? "right" }, children: citation })
389
+ ] })
390
+ };
391
+ const IconList = {
392
+ label: "Icon list",
393
+ category: "content",
394
+ fields: {
395
+ items: {
396
+ type: "array",
397
+ getItemSummary: (item) => item.text?.split("\n")[0] || "Item",
398
+ arrayFields: {
399
+ icon: { type: "select", options: iconOptions },
400
+ text: { type: "textarea", label: "Text (line breaks are kept)" },
401
+ href: { type: "text", label: "Link (optional): https://, tel:, mailto:" }
402
+ },
403
+ defaultItemProps: { icon: "link", text: "", href: "" }
404
+ },
405
+ align: { type: "radio", options: alignOptions }
406
+ },
407
+ defaultProps: { items: [{ icon: "phone", text: "(555) 555-0100", href: "tel:+15555550100" }], align: "left" },
408
+ render: ({ items = [], align }) => /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { listStyle: "none", padding: 0, margin: "0 0 1em", textAlign: align }, children: items.map((item, index) => {
409
+ const href = safeUrl(item.href);
410
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
411
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: item.icon, style: { marginRight: "0.4em" } }),
412
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { whiteSpace: "pre-line" }, children: item.text })
413
+ ] });
414
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { style: { margin: "0.35em 0" }, children: href ? /* @__PURE__ */ jsxRuntime.jsx("a", { href, style: { color: tokenVar("colorAccent") }, children: content }) : content }, index);
415
+ }) })
416
+ };
417
+ const Schedule = {
418
+ label: "Schedule",
419
+ category: "content",
420
+ fields: {
421
+ rows: {
422
+ type: "array",
423
+ getItemSummary: (row) => [row.day, row.time, row.what].filter(Boolean).join(" ") || "Row",
424
+ arrayFields: { day: { type: "text" }, time: { type: "text" }, what: { type: "text" } },
425
+ defaultItemProps: { day: "Sunday", time: "10:00", what: "Worship" }
426
+ },
427
+ align: { type: "radio", options: alignOptions }
428
+ },
429
+ defaultProps: { rows: [{ day: "Sunday", time: "10:00", what: "Worship" }], align: "center" },
430
+ render: ({ rows = [], align }) => /* @__PURE__ */ jsxRuntime.jsx("table", { style: { borderCollapse: "collapse", margin: "0 0 1em", ...blockMargin(align ?? "center") }, children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: rows.map((row, index) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
431
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "4px 12px", textAlign: "right", fontWeight: 600 }, children: row.day }),
432
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "4px 12px", textAlign: "right", fontVariantNumeric: "tabular-nums" }, children: row.time }),
433
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "4px 12px" }, children: row.what })
434
+ ] }, index)) }) })
435
+ };
436
+
437
+ function overlayColor(hex, opacityPercent) {
438
+ let value = String(hex ?? "").trim().replace(/^#/, "");
439
+ if (/^[0-9a-f]{3}$/i.test(value)) value = value.split("").map((c) => c + c).join("");
440
+ if (!/^[0-9a-f]{6}$/i.test(value)) value = "ffffff";
441
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(value.slice(i, i + 2), 16));
442
+ const opacity = Math.min(100, Math.max(0, Number(opacityPercent) || 0)) / 100;
443
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
444
+ }
445
+ function cssImage(url) {
446
+ const safe = safeImageUrl(url);
447
+ return safe ? `url("${encodeURI(safe)}")` : void 0;
448
+ }
449
+ const percentField = (label) => ({ type: "number", label, min: 0, max: 100 });
273
450
 
274
451
  const SPACING = { none: "0", small: "16px", medium: "40px", large: "80px" };
275
452
  const spacingOptions = Object.keys(SPACING).map((value) => ({ label: value, value }));
@@ -290,21 +467,27 @@ const Section = {
290
467
  { label: "Accent", value: "accent" }
291
468
  ]
292
469
  },
470
+ backgroundImage: { type: "text", label: "Background image URL (optional)" },
471
+ overlayColor: { type: "text", label: "Overlay color over the image (hex, e.g. #ffffff)" },
472
+ overlayOpacity: percentField("Overlay opacity %"),
293
473
  padding: { type: "select", options: spacingOptions },
294
474
  content: { type: "slot" }
295
475
  },
296
- defaultProps: { background: "none", padding: "medium" },
297
- render: ({ background, padding, content: Content }) => /* @__PURE__ */ jsxRuntime.jsx(
298
- "section",
299
- {
300
- style: {
301
- background: BACKGROUNDS[background] ?? BACKGROUNDS.none,
302
- color: background === "accent" ? tokenVar("colorAccentText") : void 0,
303
- paddingBlock: SPACING[padding] ?? SPACING.medium
304
- },
305
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxWidth: tokenVar("maxWidth"), margin: "0 auto", paddingInline: "16px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Content, {}) })
306
- }
307
- )
476
+ defaultProps: { background: "none", backgroundImage: "", overlayColor: "#ffffff", overlayOpacity: 85, padding: "medium" },
477
+ render: ({ background, backgroundImage, overlayColor: color, overlayOpacity, padding, content: Content }) => {
478
+ const image = cssImage(backgroundImage);
479
+ const inner = /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxWidth: tokenVar("maxWidth"), margin: "0 auto", paddingInline: "16px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Content, {}) });
480
+ return /* @__PURE__ */ jsxRuntime.jsx(
481
+ "section",
482
+ {
483
+ style: {
484
+ background: image ? `${image} center / cover` : BACKGROUNDS[background] ?? BACKGROUNDS.none,
485
+ color: !image && background === "accent" ? tokenVar("colorAccentText") : void 0
486
+ },
487
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBlock: SPACING[padding] ?? SPACING.medium, background: image ? overlayColor(color, overlayOpacity ?? 85) : void 0 }, children: inner })
488
+ }
489
+ );
490
+ }
308
491
  };
309
492
  const Columns = {
310
493
  label: "Columns",
@@ -357,15 +540,27 @@ const Image = {
357
540
  fields: {
358
541
  url: { type: "text", label: "Image URL" },
359
542
  alt: { type: "text", label: "Description (for screen readers)" },
360
- caption: { type: "text" }
543
+ caption: { type: "text" },
544
+ align: {
545
+ type: "radio",
546
+ options: [
547
+ { label: "Left", value: "left" },
548
+ { label: "Center", value: "center" },
549
+ { label: "Right", value: "right" }
550
+ ]
551
+ },
552
+ width: { ...percentField("Width (% of the column)"), min: 10 }
361
553
  },
362
- defaultProps: { url: "", alt: "", caption: "" },
363
- render: ({ url, alt, caption }) => {
554
+ defaultProps: { url: "", alt: "", caption: "", align: "left", width: 100 },
555
+ render: ({ url, alt, caption, align, width }) => {
364
556
  const src = safeImageUrl(url);
365
557
  if (!src) return null;
366
- return /* @__PURE__ */ jsxRuntime.jsxs("figure", { style: { margin: "0 0 1em" }, children: [
367
- /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", style: { maxWidth: "100%", height: "auto", display: "block", borderRadius: tokenVar("radius") } }),
368
- caption && /* @__PURE__ */ jsxRuntime.jsx("figcaption", { style: { fontSize: "0.9em", opacity: 0.75, marginTop: "6px" }, children: caption })
558
+ const requested = Number.isFinite(Number(width)) && width !== "" && width !== null ? Number(width) : 100;
559
+ const percent = Math.min(100, Math.max(10, requested));
560
+ const side = align === "center" ? { marginInline: "auto" } : align === "right" ? { marginLeft: "auto" } : {};
561
+ return /* @__PURE__ */ jsxRuntime.jsxs("figure", { style: { margin: "0 0 1em", width: `${percent}%`, maxWidth: "100%", ...side }, children: [
562
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", style: { width: "100%", height: "auto", display: "block", borderRadius: tokenVar("radius") } }),
563
+ caption && /* @__PURE__ */ jsxRuntime.jsx("figcaption", { style: { fontSize: "0.9em", opacity: 0.75, marginTop: "6px", textAlign: align }, children: caption })
369
564
  ] });
370
565
  }
371
566
  };
@@ -447,6 +642,48 @@ const VideoEmbed = {
447
642
  ) });
448
643
  }
449
644
  };
645
+ function embedAllowed(value, hosts) {
646
+ let url;
647
+ try {
648
+ url = new URL(String(value ?? "").trim());
649
+ } catch {
650
+ return void 0;
651
+ }
652
+ if (url.protocol !== "https:") return void 0;
653
+ const host = url.hostname.toLowerCase();
654
+ const ok = hosts.some((allowed) => {
655
+ const name = String(allowed).toLowerCase();
656
+ return host === name || host.endsWith(`.${name}`);
657
+ });
658
+ return ok ? url.href : void 0;
659
+ }
660
+ function makeEmbed(hosts) {
661
+ return {
662
+ label: "Embedded page",
663
+ category: "media",
664
+ fields: {
665
+ url: { type: "text", label: `Page address (https, from: ${hosts.join(", ")})` },
666
+ title: { type: "text", label: "Title (for screen readers)" },
667
+ height: { type: "number", label: "Height (pixels)", min: 200, max: 4e3 }
668
+ },
669
+ defaultProps: { url: "", title: "Embedded page", height: 800 },
670
+ render: ({ url, title, height }) => {
671
+ const src = embedAllowed(url, hosts);
672
+ if (!src) return null;
673
+ return /* @__PURE__ */ jsxRuntime.jsx(
674
+ "iframe",
675
+ {
676
+ src,
677
+ title: title || "Embedded page",
678
+ loading: "lazy",
679
+ sandbox: "allow-scripts allow-same-origin allow-popups",
680
+ referrerPolicy: "no-referrer",
681
+ style: { width: "100%", height: `${Math.min(4e3, Math.max(200, Number(height) || 800))}px`, border: 0, borderRadius: tokenVar("radius"), background: "#fff" }
682
+ }
683
+ );
684
+ }
685
+ };
686
+ }
450
687
 
451
688
  const builtInBlocks = {
452
689
  Section,
@@ -456,6 +693,9 @@ const builtInBlocks = {
456
693
  Hero,
457
694
  Heading,
458
695
  Text,
696
+ Quote,
697
+ IconList,
698
+ Schedule,
459
699
  Button,
460
700
  CallToAction,
461
701
  Cards,
@@ -470,18 +710,64 @@ const builtInCategories = {
470
710
  media: { title: "Media" }
471
711
  };
472
712
 
473
- function ThemeRoot({ theme, children }) {
474
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet", style: themeToStyle(theme), children });
713
+ const SITE_BACKGROUND_DEFAULTS = { image: "", overlayColor: "#ffffff", overlayOpacity: 85, rounded: true, fit: "cover", frame: "none" };
714
+ function resolveBackground(theme = {}, page = {}) {
715
+ const site = { ...SITE_BACKGROUND_DEFAULTS, ...theme.background ?? {} };
716
+ const mode = page.background ?? "site";
717
+ if (mode === "none") return null;
718
+ const chosen = mode === "custom" ? {
719
+ image: page.backgroundImage,
720
+ overlayColor: page.overlayColor ?? site.overlayColor,
721
+ overlayOpacity: page.overlayOpacity ?? site.overlayOpacity,
722
+ rounded: site.rounded,
723
+ fit: site.fit,
724
+ frame: site.frame
725
+ } : {
726
+ ...site,
727
+ // A page may keep the site's photo but change how much shows through.
728
+ overlayOpacity: page.overlayOpacity ?? site.overlayOpacity
729
+ };
730
+ const image = cssImage(chosen.image);
731
+ return image ? { ...chosen, image, imageUrl: safeImageUrl(chosen.image) } : null;
732
+ }
733
+ function ThemeRoot({ theme, page, children }) {
734
+ const background = resolveBackground(theme ?? {}, page ?? {});
735
+ if (!background) {
736
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet", style: themeToStyle(theme), children });
737
+ }
738
+ const radius = background.rounded ? "var(--pagelet-radius)" : 0;
739
+ const panel = { background: overlayColor(background.overlayColor, background.overlayOpacity), padding: "calc(var(--pagelet-space) * 2)" };
740
+ const area = background.fit === "photo" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pagelet-background", style: { display: "grid", borderRadius: radius, overflow: "hidden" }, children: [
741
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: background.imageUrl, alt: "", "aria-hidden": "true", style: { gridArea: "1 / 1", width: "100%", height: "100%", objectFit: "cover", display: "block" } }),
742
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-panel", style: { gridArea: "1 / 1", ...panel }, children })
743
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-background", style: { background: `${background.image} center / cover`, borderRadius: radius, overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-panel", style: { ...panel, minHeight: "60vh" }, children }) });
744
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet", style: themeToStyle(theme), children: background.frame === "card" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-frame", style: { border: "1px solid rgba(0, 0, 0, 0.15)", borderRadius: radius, padding: "var(--pagelet-space)", background: "var(--pagelet-color-background)" }, children: area }) : area });
475
745
  }
476
746
 
477
- function createPalette({ blocks = {}, hide = [], categories = {}, root = {} } = {}) {
747
+ const pageFields = {
748
+ title: { type: "text", label: "Page title (browser tab)" },
749
+ background: {
750
+ type: "radio",
751
+ label: "Background",
752
+ options: [
753
+ { label: "Site's", value: "site" },
754
+ { label: "Custom", value: "custom" },
755
+ { label: "None", value: "none" }
756
+ ]
757
+ },
758
+ backgroundImage: { type: "text", label: "Custom background image URL" },
759
+ overlayColor: { type: "text", label: "Custom overlay color (hex)" },
760
+ overlayOpacity: percentField("Overlay opacity % (blank uses the site setting)")
761
+ };
762
+ function createPalette({ blocks = {}, hide = [], categories = {}, root = {}, embedHosts = [] } = {}) {
763
+ const builtIns = embedHosts.length ? { ...builtInBlocks, Embed: makeEmbed(embedHosts) } : builtInBlocks;
478
764
  const hidden = new Set(hide);
479
765
  for (const name of hidden) {
480
- if (!(name in builtInBlocks) && !(name in blocks)) {
766
+ if (!(name in builtIns) && !(name in blocks)) {
481
767
  throw new Error(`pagelet: cannot hide unknown block "${name}"`);
482
768
  }
483
769
  }
484
- const all = { ...builtInBlocks, ...blocks };
770
+ const all = { ...builtIns, ...blocks };
485
771
  const components = {};
486
772
  const membership = {};
487
773
  for (const [name, block] of Object.entries(all)) {
@@ -506,15 +792,28 @@ function createPalette({ blocks = {}, hide = [], categories = {}, root = {} } =
506
792
  components,
507
793
  categories: puckCategories,
508
794
  root: {
795
+ defaultProps: { title: "", background: "site", backgroundImage: "", overlayColor: "#ffffff" },
509
796
  ...root,
510
- // The page's root render applies the theme. It is the root of the
511
- // editor's preview iframe as well as of a rendered page, so the theme
512
- // reaches both. Pages receive it through Puck's metadata.
513
- render: ({ children, puck }) => /* @__PURE__ */ jsxRuntime.jsx(ThemeRoot, { theme: puck?.metadata?.pageletTheme, children })
797
+ fields: { ...pageFields, ...root.fields ?? {} },
798
+ // The page's root render applies the theme and the background. It is
799
+ // the root of the editor's preview iframe as well as of a rendered
800
+ // page, so both reach the editor. The site theme arrives through
801
+ // Puck's metadata; the page's own settings are this render's props.
802
+ render: ({ children, puck, title, background, backgroundImage, overlayColor, overlayOpacity }) => /* @__PURE__ */ jsxRuntime.jsx(
803
+ ThemeRoot,
804
+ {
805
+ theme: puck?.metadata?.pageletTheme,
806
+ page: { title, background, backgroundImage, overlayColor, overlayOpacity: blankToUndefined(overlayOpacity) },
807
+ children
808
+ }
809
+ )
514
810
  }
515
811
  }
516
812
  };
517
813
  }
814
+ function blankToUndefined(value) {
815
+ return value === "" || value === null || value === void 0 ? void 0 : value;
816
+ }
518
817
 
519
818
  class PageletApiError extends Error {
520
819
  constructor(message, { status, errors } = {}) {
@@ -557,31 +856,115 @@ function createPageletClient({ baseUrl = "/pagelet", csrfToken, headers = {}, fe
557
856
  });
558
857
  }
559
858
  const pagePath = (id) => `/pages/${encodeURIComponent(id)}`;
859
+ async function getPublishedPage(path) {
860
+ try {
861
+ const data = await request("GET", `/published?path=${encodeURIComponent(path)}`);
862
+ return { document: data.document, pagePath: data.page_path, rest: data.rest ?? "" };
863
+ } catch (error) {
864
+ if (error instanceof PageletApiError && error.status === 404) return null;
865
+ throw error;
866
+ }
867
+ }
560
868
  return {
561
869
  listPages: () => request("GET", "/pages"),
562
870
  getPage: (id) => request("GET", pagePath(id)),
563
- createPage: ({ path, title }) => request("POST", "/pages", { page: { path, title } }),
564
- updatePage: (id, { path, title }) => request("PATCH", pagePath(id), { page: { path, title } }),
871
+ createPage: ({ path, title, matchesSubpaths }) => request("POST", "/pages", { page: { path, title, matches_subpaths: matchesSubpaths } }),
872
+ updatePage: (id, { path, title, matchesSubpaths }) => request("PATCH", pagePath(id), { page: { path, title, matches_subpaths: matchesSubpaths } }),
565
873
  saveDraft: (id, document2) => request("PATCH", pagePath(id), { page: { draft_document: document2 } }),
566
874
  deletePage: (id) => request("DELETE", pagePath(id)),
567
875
  publish: (id, { publishedBy } = {}) => request("POST", `${pagePath(id)}/publish`, publishedBy ? { published_by: publishedBy } : {}),
568
876
  listVersions: (id) => request("GET", `${pagePath(id)}/versions`),
569
877
  restoreVersion: (id, number) => request("POST", `${pagePath(id)}/versions/${encodeURIComponent(number)}/restore`, {}),
570
878
  // Public: the published document at a path, or null when there is none.
571
- getPublished: async (path) => {
572
- try {
573
- const { document: document2 } = await request("GET", `/published?path=${encodeURIComponent(path)}`);
574
- return document2;
575
- } catch (error) {
576
- if (error instanceof PageletApiError && error.status === 404) return null;
577
- throw error;
578
- }
579
- }
879
+ getPublished: async (path) => (await getPublishedPage(path))?.document ?? null,
880
+ // Public: { document, pagePath, rest } for a path, or null. rest is what
881
+ // follows a page that answers for sub-paths: /messages/43 -> "43".
882
+ getPublishedPage,
883
+ // Public: the site's theme and menu.
884
+ getSite: async () => (await request("GET", "/site")).settings,
885
+ // Needs publish permission: changes are live at once.
886
+ updateSite: async (settings) => (await request("PATCH", "/site", { settings })).settings
580
887
  };
581
888
  }
582
889
 
890
+ const CSS = `
891
+ .pagelet-menu{display:flex;align-items:center;flex-wrap:wrap;gap:4px 16px;padding:8px 16px;background:var(--pagelet-menu-background,#f8f9fa)}
892
+ .pagelet-menu--centered{flex-direction:column}
893
+ .pagelet-menu__groups{display:flex;flex:1;align-items:center;gap:4px 16px;flex-wrap:wrap}
894
+ .pagelet-menu--centered .pagelet-menu__groups{justify-content:center;flex:none}
895
+ .pagelet-menu__group{display:flex;gap:4px 16px;flex-wrap:wrap;align-items:center}
896
+ .pagelet-menu__group--right{margin-left:auto}
897
+ .pagelet-menu--centered .pagelet-menu__group--right{margin-left:0}
898
+ .pagelet-menu__link{text-decoration:none;color:rgba(0,0,0,.65);padding:6px 0;display:inline-flex;align-items:center;gap:6px}
899
+ .pagelet-menu__link:hover{color:rgba(0,0,0,.9)}
900
+ .pagelet-menu__link--current{color:rgba(0,0,0,.9)}
901
+ .pagelet-menu__toggle{display:none;margin-left:auto;background:none;border:1px solid rgba(0,0,0,.2);border-radius:6px;padding:6px 10px;cursor:pointer;font:inherit}
902
+ @media (max-width: 767px){
903
+ .pagelet-menu__toggle{display:inline-block}
904
+ .pagelet-menu__groups{display:none;flex-basis:100%;flex-direction:column;align-items:flex-start}
905
+ .pagelet-menu__groups--open{display:flex}
906
+ .pagelet-menu__group{flex-direction:column;align-items:flex-start}
907
+ .pagelet-menu__group--right{margin-left:0}
908
+ }`;
909
+ function isExternal(href) {
910
+ return /^https?:\/\//i.test(href);
911
+ }
912
+ function needsPlainAnchor(href, LinkComponent) {
913
+ return !LinkComponent || isExternal(href) || /^(mailto|tel):/i.test(href);
914
+ }
915
+ function MenuLink({ item, linkComponent: LinkComponent, currentPath, onNavigate }) {
916
+ const href = safeUrl(item.href);
917
+ if (!href) return null;
918
+ const current = !isExternal(href) && currentPath === href;
919
+ const className = `pagelet-menu__link${current ? " pagelet-menu__link--current" : ""}`;
920
+ const style = item.highlight ? { color: tokenVar("colorAccent") } : void 0;
921
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
922
+ item.label,
923
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: item.icon })
924
+ ] });
925
+ if (needsPlainAnchor(href, LinkComponent)) {
926
+ const external = isExternal(href) ? { target: "_blank", rel: "noopener noreferrer" } : {};
927
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { className, style, href, ...external, children: body });
928
+ }
929
+ return /* @__PURE__ */ jsxRuntime.jsx(LinkComponent, { className, style, to: href, onClick: onNavigate, "aria-current": current ? "page" : void 0, children: body });
930
+ }
931
+ function PageletMenu({ menu = {}, linkComponent, currentPath }) {
932
+ const [open, setOpen] = react.useState(false);
933
+ const items = Array.isArray(menu.items) ? menu.items : [];
934
+ const brand = menu.brand ?? {};
935
+ const logo = safeImageUrl(brand.logo);
936
+ const brandHref = safeUrl(brand.href) ?? "/";
937
+ const close = () => setOpen(false);
938
+ const group = (name) => items.filter((item) => (item.group ?? "left") === name);
939
+ const linkProps = { linkComponent, currentPath, onNavigate: close };
940
+ const BrandLink = needsPlainAnchor(brandHref, linkComponent) ? "a" : linkComponent;
941
+ const brandTarget = BrandLink === "a" ? { href: brandHref } : { to: brandHref, onClick: close };
942
+ return /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: `pagelet-menu${menu.style === "centered" ? " pagelet-menu--centered" : ""}`, "aria-label": "Site", children: [
943
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: CSS }),
944
+ (logo || brand.label) && /* @__PURE__ */ jsxRuntime.jsxs(BrandLink, { className: "pagelet-menu__brand", ...brandTarget, style: { display: "inline-flex", alignItems: "center", gap: "8px", textDecoration: "none", color: "inherit" }, children: [
945
+ logo && /* @__PURE__ */ jsxRuntime.jsx("img", { src: logo, alt: brand.label || "Home", style: { height: "40px", width: "auto", display: "block" } }),
946
+ !logo && brand.label
947
+ ] }),
948
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pagelet-menu__toggle", "aria-expanded": open, onClick: () => setOpen((value) => !value), children: "Menu" }),
949
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `pagelet-menu__groups${open ? " pagelet-menu__groups--open" : ""}`, children: [
950
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-menu__group", children: group("left").map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(MenuLink, { item, ...linkProps }, i)) }),
951
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-menu__group pagelet-menu__group--right", children: group("right").map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(MenuLink, { item, ...linkProps }, i)) })
952
+ ] })
953
+ ] });
954
+ }
955
+
956
+ function PageletLayout({ site = {}, linkComponent, currentPath, children }) {
957
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pagelet-layout", style: { ...themeToStyle(site.theme), maxWidth: tokenVar("maxWidth"), margin: "0 auto" }, children: [
958
+ /* @__PURE__ */ jsxRuntime.jsx(PageletMenu, { menu: site.menu, linkComponent, currentPath }),
959
+ /* @__PURE__ */ jsxRuntime.jsx("main", { style: { marginTop: "var(--pagelet-space)" }, children })
960
+ ] });
961
+ }
962
+
583
963
  exports.DOCUMENT_VERSION = DOCUMENT_VERSION;
964
+ exports.Icon = Icon;
584
965
  exports.PageletApiError = PageletApiError;
966
+ exports.PageletLayout = PageletLayout;
967
+ exports.PageletMenu = PageletMenu;
585
968
  exports.PageletPage = PageletPage;
586
969
  exports.builtInBlocks = builtInBlocks;
587
970
  exports.builtInCategories = builtInCategories;
@@ -589,8 +972,12 @@ exports.createDocument = createDocument;
589
972
  exports.createPageletClient = createPageletClient;
590
973
  exports.createPalette = createPalette;
591
974
  exports.defaultTheme = defaultTheme;
975
+ exports.embedAllowed = embedAllowed;
592
976
  exports.embedUrl = embedUrl;
593
977
  exports.emptyData = emptyData;
978
+ exports.iconNames = iconNames;
979
+ exports.overlayColor = overlayColor;
980
+ exports.pageSettings = pageSettings;
594
981
  exports.safeImageUrl = safeImageUrl;
595
982
  exports.safeUrl = safeUrl;
596
983
  exports.themeToStyle = themeToStyle;