@byscott-io/pagelet 0.1.0 → 0.2.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/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var rsc = require('@puckeditor/core/rsc');
5
+ var react = require('react');
5
6
 
6
7
  const DOCUMENT_VERSION = 1;
7
8
  function emptyData() {
@@ -36,6 +37,10 @@ function needsPuckMigration(data) {
36
37
  const root = data.root ?? {};
37
38
  return Object.keys(root).some((key) => key !== "props" && key !== "readOnly");
38
39
  }
40
+ function pageSettings(document) {
41
+ const props = upgradeDocument(document)?.data?.root?.props ?? {};
42
+ return { title: props.title || "", background: props.background ?? "site" };
43
+ }
39
44
 
40
45
  function PageletPage({ document, palette, theme }) {
41
46
  if (!palette) throw new Error("pagelet: <PageletPage> needs a palette (see createPalette)");
@@ -43,6 +48,63 @@ function PageletPage({ document, palette, theme }) {
43
48
  return /* @__PURE__ */ jsxRuntime.jsx(rsc.Render, { config: palette.config, data, metadata: { pageletTheme: theme } });
44
49
  }
45
50
 
51
+ const outline = { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" };
52
+ const PATHS = {
53
+ location: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
54
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 22s7-6.2 7-12a7 7 0 1 0-14 0c0 5.8 7 12 7 12z" }),
55
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "10", r: "2.5" })
56
+ ] }),
57
+ 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" }),
58
+ email: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
59
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
60
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 7l9 6 9-6" })
61
+ ] }),
62
+ video: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
63
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2.5", y: "6", width: "13", height: "12", rx: "2" }),
64
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15.5 10.5 21.5 7v10l-6-3.5z" })
65
+ ] }),
66
+ calendar: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
67
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }),
68
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 10h18M8 3v4M16 3v4" })
69
+ ] }),
70
+ clock: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
71
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
72
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 7v5l3 2" })
73
+ ] }),
74
+ link: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
75
+ /* @__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" }),
76
+ /* @__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" })
77
+ ] }),
78
+ facebook: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
79
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "3" }),
80
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15.5 8H14a2 2 0 0 0-2 2v11M9.5 13h5" })
81
+ ] }),
82
+ youtube: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
83
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2.5", y: "5.5", width: "19", height: "13", rx: "4" }),
84
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.5 9.5v5l4-2.5z" })
85
+ ] })
86
+ };
87
+ const iconNames = Object.keys(PATHS);
88
+ const iconOptions = [{ label: "None", value: "" }].concat(
89
+ iconNames.map((name) => ({ label: name[0].toUpperCase() + name.slice(1), value: name }))
90
+ );
91
+ function Icon({ name, size = "1.1em", style }) {
92
+ const glyph = PATHS[name];
93
+ if (!glyph) return null;
94
+ return /* @__PURE__ */ jsxRuntime.jsx(
95
+ "svg",
96
+ {
97
+ viewBox: "0 0 24 24",
98
+ width: size,
99
+ height: size,
100
+ "aria-hidden": "true",
101
+ focusable: "false",
102
+ style: { ...outline, verticalAlign: "-0.15em", flexShrink: 0, ...style },
103
+ children: glyph
104
+ }
105
+ );
106
+ }
107
+
46
108
  const SAFE_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
47
109
  function safeUrl(value) {
48
110
  if (typeof value !== "string") return void 0;
@@ -94,8 +156,10 @@ function tokenVar(token) {
94
156
 
95
157
  const alignOptions = [
96
158
  { label: "Left", value: "left" },
97
- { label: "Center", value: "center" }
159
+ { label: "Center", value: "center" },
160
+ { label: "Right", value: "right" }
98
161
  ];
162
+ const blockMargin = (align) => align === "center" ? { marginInline: "auto" } : align === "right" ? { marginLeft: "auto" } : {};
99
163
  function paragraphs(text) {
100
164
  return String(text ?? "").split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
101
165
  }
@@ -157,7 +221,7 @@ const Text = {
157
221
  align: { type: "radio", options: alignOptions }
158
222
  },
159
223
  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 }) })
224
+ render: ({ text, align }) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: align, maxWidth: "68ch", ...blockMargin(align) }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text }) })
161
225
  };
162
226
  const Button = {
163
227
  label: "Button",
@@ -270,6 +334,79 @@ const Faq = {
270
334
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingTop: "8px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text: item.answer }) })
271
335
  ] }, index)) })
272
336
  };
337
+ const Quote = {
338
+ label: "Quote",
339
+ category: "content",
340
+ fields: {
341
+ text: { type: "textarea" },
342
+ citation: { type: "text", label: "Citation (e.g. Matthew 28:19-20 ESV)" },
343
+ citationAlign: { type: "radio", label: "Citation alignment", options: alignOptions }
344
+ },
345
+ defaultProps: { text: "Quotation", citation: "", citationAlign: "right" },
346
+ render: ({ text, citation, citationAlign }) => /* @__PURE__ */ jsxRuntime.jsxs("figure", { style: { margin: "0 0 1em", maxWidth: "68ch" }, children: [
347
+ /* @__PURE__ */ jsxRuntime.jsx("blockquote", { style: { margin: 0, fontStyle: "italic" }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text, style: { margin: "0 0 0.5em" } }) }),
348
+ citation && /* @__PURE__ */ jsxRuntime.jsx("figcaption", { style: { textAlign: citationAlign ?? "right" }, children: citation })
349
+ ] })
350
+ };
351
+ const IconList = {
352
+ label: "Icon list",
353
+ category: "content",
354
+ fields: {
355
+ items: {
356
+ type: "array",
357
+ getItemSummary: (item) => item.text?.split("\n")[0] || "Item",
358
+ arrayFields: {
359
+ icon: { type: "select", options: iconOptions },
360
+ text: { type: "textarea", label: "Text (line breaks are kept)" },
361
+ href: { type: "text", label: "Link (optional): https://, tel:, mailto:" }
362
+ },
363
+ defaultItemProps: { icon: "link", text: "", href: "" }
364
+ },
365
+ align: { type: "radio", options: alignOptions }
366
+ },
367
+ defaultProps: { items: [{ icon: "phone", text: "(555) 555-0100", href: "tel:+15555550100" }], align: "left" },
368
+ render: ({ items = [], align }) => /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { listStyle: "none", padding: 0, margin: "0 0 1em", textAlign: align }, children: items.map((item, index) => {
369
+ const href = safeUrl(item.href);
370
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
371
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: item.icon, style: { marginRight: "0.4em" } }),
372
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { whiteSpace: "pre-line" }, children: item.text })
373
+ ] });
374
+ 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);
375
+ }) })
376
+ };
377
+ const Schedule = {
378
+ label: "Schedule",
379
+ category: "content",
380
+ fields: {
381
+ rows: {
382
+ type: "array",
383
+ getItemSummary: (row) => [row.day, row.time, row.what].filter(Boolean).join(" ") || "Row",
384
+ arrayFields: { day: { type: "text" }, time: { type: "text" }, what: { type: "text" } },
385
+ defaultItemProps: { day: "Sunday", time: "10:00", what: "Worship" }
386
+ },
387
+ align: { type: "radio", options: alignOptions }
388
+ },
389
+ defaultProps: { rows: [{ day: "Sunday", time: "10:00", what: "Worship" }], align: "center" },
390
+ 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: [
391
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "4px 12px", textAlign: "right", fontWeight: 600 }, children: row.day }),
392
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "4px 12px", textAlign: "right", fontVariantNumeric: "tabular-nums" }, children: row.time }),
393
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { padding: "4px 12px" }, children: row.what })
394
+ ] }, index)) }) })
395
+ };
396
+
397
+ function overlayColor(hex, opacityPercent) {
398
+ let value = String(hex ?? "").trim().replace(/^#/, "");
399
+ if (/^[0-9a-f]{3}$/i.test(value)) value = value.split("").map((c) => c + c).join("");
400
+ if (!/^[0-9a-f]{6}$/i.test(value)) value = "ffffff";
401
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(value.slice(i, i + 2), 16));
402
+ const opacity = Math.min(100, Math.max(0, Number(opacityPercent) || 0)) / 100;
403
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
404
+ }
405
+ function cssImage(url) {
406
+ const safe = safeImageUrl(url);
407
+ return safe ? `url("${encodeURI(safe)}")` : void 0;
408
+ }
409
+ const percentField = (label) => ({ type: "number", label, min: 0, max: 100 });
273
410
 
274
411
  const SPACING = { none: "0", small: "16px", medium: "40px", large: "80px" };
275
412
  const spacingOptions = Object.keys(SPACING).map((value) => ({ label: value, value }));
@@ -290,21 +427,27 @@ const Section = {
290
427
  { label: "Accent", value: "accent" }
291
428
  ]
292
429
  },
430
+ backgroundImage: { type: "text", label: "Background image URL (optional)" },
431
+ overlayColor: { type: "text", label: "Overlay color over the image (hex, e.g. #ffffff)" },
432
+ overlayOpacity: percentField("Overlay opacity %"),
293
433
  padding: { type: "select", options: spacingOptions },
294
434
  content: { type: "slot" }
295
435
  },
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
- )
436
+ defaultProps: { background: "none", backgroundImage: "", overlayColor: "#ffffff", overlayOpacity: 85, padding: "medium" },
437
+ render: ({ background, backgroundImage, overlayColor: color, overlayOpacity, padding, content: Content }) => {
438
+ const image = cssImage(backgroundImage);
439
+ const inner = /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxWidth: tokenVar("maxWidth"), margin: "0 auto", paddingInline: "16px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Content, {}) });
440
+ return /* @__PURE__ */ jsxRuntime.jsx(
441
+ "section",
442
+ {
443
+ style: {
444
+ background: image ? `${image} center / cover` : BACKGROUNDS[background] ?? BACKGROUNDS.none,
445
+ color: !image && background === "accent" ? tokenVar("colorAccentText") : void 0
446
+ },
447
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBlock: SPACING[padding] ?? SPACING.medium, background: image ? overlayColor(color, overlayOpacity ?? 85) : void 0 }, children: inner })
448
+ }
449
+ );
450
+ }
308
451
  };
309
452
  const Columns = {
310
453
  label: "Columns",
@@ -357,15 +500,27 @@ const Image = {
357
500
  fields: {
358
501
  url: { type: "text", label: "Image URL" },
359
502
  alt: { type: "text", label: "Description (for screen readers)" },
360
- caption: { type: "text" }
503
+ caption: { type: "text" },
504
+ align: {
505
+ type: "radio",
506
+ options: [
507
+ { label: "Left", value: "left" },
508
+ { label: "Center", value: "center" },
509
+ { label: "Right", value: "right" }
510
+ ]
511
+ },
512
+ width: { ...percentField("Width (% of the column)"), min: 10 }
361
513
  },
362
- defaultProps: { url: "", alt: "", caption: "" },
363
- render: ({ url, alt, caption }) => {
514
+ defaultProps: { url: "", alt: "", caption: "", align: "left", width: 100 },
515
+ render: ({ url, alt, caption, align, width }) => {
364
516
  const src = safeImageUrl(url);
365
517
  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 })
518
+ const requested = Number.isFinite(Number(width)) && width !== "" && width !== null ? Number(width) : 100;
519
+ const percent = Math.min(100, Math.max(10, requested));
520
+ const side = align === "center" ? { marginInline: "auto" } : align === "right" ? { marginLeft: "auto" } : {};
521
+ return /* @__PURE__ */ jsxRuntime.jsxs("figure", { style: { margin: "0 0 1em", width: `${percent}%`, maxWidth: "100%", ...side }, children: [
522
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", style: { width: "100%", height: "auto", display: "block", borderRadius: tokenVar("radius") } }),
523
+ caption && /* @__PURE__ */ jsxRuntime.jsx("figcaption", { style: { fontSize: "0.9em", opacity: 0.75, marginTop: "6px", textAlign: align }, children: caption })
369
524
  ] });
370
525
  }
371
526
  };
@@ -447,6 +602,48 @@ const VideoEmbed = {
447
602
  ) });
448
603
  }
449
604
  };
605
+ function embedAllowed(value, hosts) {
606
+ let url;
607
+ try {
608
+ url = new URL(String(value ?? "").trim());
609
+ } catch {
610
+ return void 0;
611
+ }
612
+ if (url.protocol !== "https:") return void 0;
613
+ const host = url.hostname.toLowerCase();
614
+ const ok = hosts.some((allowed) => {
615
+ const name = String(allowed).toLowerCase();
616
+ return host === name || host.endsWith(`.${name}`);
617
+ });
618
+ return ok ? url.href : void 0;
619
+ }
620
+ function makeEmbed(hosts) {
621
+ return {
622
+ label: "Embedded page",
623
+ category: "media",
624
+ fields: {
625
+ url: { type: "text", label: `Page address (https, from: ${hosts.join(", ")})` },
626
+ title: { type: "text", label: "Title (for screen readers)" },
627
+ height: { type: "number", label: "Height (pixels)", min: 200, max: 4e3 }
628
+ },
629
+ defaultProps: { url: "", title: "Embedded page", height: 800 },
630
+ render: ({ url, title, height }) => {
631
+ const src = embedAllowed(url, hosts);
632
+ if (!src) return null;
633
+ return /* @__PURE__ */ jsxRuntime.jsx(
634
+ "iframe",
635
+ {
636
+ src,
637
+ title: title || "Embedded page",
638
+ loading: "lazy",
639
+ sandbox: "allow-scripts allow-same-origin allow-popups",
640
+ referrerPolicy: "no-referrer",
641
+ style: { width: "100%", height: `${Math.min(4e3, Math.max(200, Number(height) || 800))}px`, border: 0, borderRadius: tokenVar("radius"), background: "#fff" }
642
+ }
643
+ );
644
+ }
645
+ };
646
+ }
450
647
 
451
648
  const builtInBlocks = {
452
649
  Section,
@@ -456,6 +653,9 @@ const builtInBlocks = {
456
653
  Hero,
457
654
  Heading,
458
655
  Text,
656
+ Quote,
657
+ IconList,
658
+ Schedule,
459
659
  Button,
460
660
  CallToAction,
461
661
  Cards,
@@ -470,18 +670,64 @@ const builtInCategories = {
470
670
  media: { title: "Media" }
471
671
  };
472
672
 
473
- function ThemeRoot({ theme, children }) {
474
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet", style: themeToStyle(theme), children });
673
+ const SITE_BACKGROUND_DEFAULTS = { image: "", overlayColor: "#ffffff", overlayOpacity: 85, rounded: true, fit: "cover", frame: "none" };
674
+ function resolveBackground(theme = {}, page = {}) {
675
+ const site = { ...SITE_BACKGROUND_DEFAULTS, ...theme.background ?? {} };
676
+ const mode = page.background ?? "site";
677
+ if (mode === "none") return null;
678
+ const chosen = mode === "custom" ? {
679
+ image: page.backgroundImage,
680
+ overlayColor: page.overlayColor ?? site.overlayColor,
681
+ overlayOpacity: page.overlayOpacity ?? site.overlayOpacity,
682
+ rounded: site.rounded,
683
+ fit: site.fit,
684
+ frame: site.frame
685
+ } : {
686
+ ...site,
687
+ // A page may keep the site's photo but change how much shows through.
688
+ overlayOpacity: page.overlayOpacity ?? site.overlayOpacity
689
+ };
690
+ const image = cssImage(chosen.image);
691
+ return image ? { ...chosen, image, imageUrl: safeImageUrl(chosen.image) } : null;
692
+ }
693
+ function ThemeRoot({ theme, page, children }) {
694
+ const background = resolveBackground(theme ?? {}, page ?? {});
695
+ if (!background) {
696
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet", style: themeToStyle(theme), children });
697
+ }
698
+ const radius = background.rounded ? "var(--pagelet-radius)" : 0;
699
+ const panel = { background: overlayColor(background.overlayColor, background.overlayOpacity), padding: "calc(var(--pagelet-space) * 2)" };
700
+ const area = background.fit === "photo" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pagelet-background", style: { display: "grid", borderRadius: radius, overflow: "hidden" }, children: [
701
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: background.imageUrl, alt: "", "aria-hidden": "true", style: { gridArea: "1 / 1", width: "100%", height: "100%", objectFit: "cover", display: "block" } }),
702
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-panel", style: { gridArea: "1 / 1", ...panel }, children })
703
+ ] }) : /* @__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 }) });
704
+ 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
705
  }
476
706
 
477
- function createPalette({ blocks = {}, hide = [], categories = {}, root = {} } = {}) {
707
+ const pageFields = {
708
+ title: { type: "text", label: "Page title (browser tab)" },
709
+ background: {
710
+ type: "radio",
711
+ label: "Background",
712
+ options: [
713
+ { label: "Site's", value: "site" },
714
+ { label: "Custom", value: "custom" },
715
+ { label: "None", value: "none" }
716
+ ]
717
+ },
718
+ backgroundImage: { type: "text", label: "Custom background image URL" },
719
+ overlayColor: { type: "text", label: "Custom overlay color (hex)" },
720
+ overlayOpacity: percentField("Overlay opacity % (blank uses the site setting)")
721
+ };
722
+ function createPalette({ blocks = {}, hide = [], categories = {}, root = {}, embedHosts = [] } = {}) {
723
+ const builtIns = embedHosts.length ? { ...builtInBlocks, Embed: makeEmbed(embedHosts) } : builtInBlocks;
478
724
  const hidden = new Set(hide);
479
725
  for (const name of hidden) {
480
- if (!(name in builtInBlocks) && !(name in blocks)) {
726
+ if (!(name in builtIns) && !(name in blocks)) {
481
727
  throw new Error(`pagelet: cannot hide unknown block "${name}"`);
482
728
  }
483
729
  }
484
- const all = { ...builtInBlocks, ...blocks };
730
+ const all = { ...builtIns, ...blocks };
485
731
  const components = {};
486
732
  const membership = {};
487
733
  for (const [name, block] of Object.entries(all)) {
@@ -506,15 +752,28 @@ function createPalette({ blocks = {}, hide = [], categories = {}, root = {} } =
506
752
  components,
507
753
  categories: puckCategories,
508
754
  root: {
755
+ defaultProps: { title: "", background: "site", backgroundImage: "", overlayColor: "#ffffff" },
509
756
  ...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 })
757
+ fields: { ...pageFields, ...root.fields ?? {} },
758
+ // The page's root render applies the theme and the background. It is
759
+ // the root of the editor's preview iframe as well as of a rendered
760
+ // page, so both reach the editor. The site theme arrives through
761
+ // Puck's metadata; the page's own settings are this render's props.
762
+ render: ({ children, puck, title, background, backgroundImage, overlayColor, overlayOpacity }) => /* @__PURE__ */ jsxRuntime.jsx(
763
+ ThemeRoot,
764
+ {
765
+ theme: puck?.metadata?.pageletTheme,
766
+ page: { title, background, backgroundImage, overlayColor, overlayOpacity: blankToUndefined(overlayOpacity) },
767
+ children
768
+ }
769
+ )
514
770
  }
515
771
  }
516
772
  };
517
773
  }
774
+ function blankToUndefined(value) {
775
+ return value === "" || value === null || value === void 0 ? void 0 : value;
776
+ }
518
777
 
519
778
  class PageletApiError extends Error {
520
779
  constructor(message, { status, errors } = {}) {
@@ -557,31 +816,115 @@ function createPageletClient({ baseUrl = "/pagelet", csrfToken, headers = {}, fe
557
816
  });
558
817
  }
559
818
  const pagePath = (id) => `/pages/${encodeURIComponent(id)}`;
819
+ async function getPublishedPage(path) {
820
+ try {
821
+ const data = await request("GET", `/published?path=${encodeURIComponent(path)}`);
822
+ return { document: data.document, pagePath: data.page_path, rest: data.rest ?? "" };
823
+ } catch (error) {
824
+ if (error instanceof PageletApiError && error.status === 404) return null;
825
+ throw error;
826
+ }
827
+ }
560
828
  return {
561
829
  listPages: () => request("GET", "/pages"),
562
830
  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 } }),
831
+ createPage: ({ path, title, matchesSubpaths }) => request("POST", "/pages", { page: { path, title, matches_subpaths: matchesSubpaths } }),
832
+ updatePage: (id, { path, title, matchesSubpaths }) => request("PATCH", pagePath(id), { page: { path, title, matches_subpaths: matchesSubpaths } }),
565
833
  saveDraft: (id, document2) => request("PATCH", pagePath(id), { page: { draft_document: document2 } }),
566
834
  deletePage: (id) => request("DELETE", pagePath(id)),
567
835
  publish: (id, { publishedBy } = {}) => request("POST", `${pagePath(id)}/publish`, publishedBy ? { published_by: publishedBy } : {}),
568
836
  listVersions: (id) => request("GET", `${pagePath(id)}/versions`),
569
837
  restoreVersion: (id, number) => request("POST", `${pagePath(id)}/versions/${encodeURIComponent(number)}/restore`, {}),
570
838
  // 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
- }
839
+ getPublished: async (path) => (await getPublishedPage(path))?.document ?? null,
840
+ // Public: { document, pagePath, rest } for a path, or null. rest is what
841
+ // follows a page that answers for sub-paths: /messages/43 -> "43".
842
+ getPublishedPage,
843
+ // Public: the site's theme and menu.
844
+ getSite: async () => (await request("GET", "/site")).settings,
845
+ // Needs publish permission: changes are live at once.
846
+ updateSite: async (settings) => (await request("PATCH", "/site", { settings })).settings
580
847
  };
581
848
  }
582
849
 
850
+ const CSS = `
851
+ .pagelet-menu{display:flex;align-items:center;flex-wrap:wrap;gap:4px 16px;padding:8px 16px;background:var(--pagelet-menu-background,#f8f9fa)}
852
+ .pagelet-menu--centered{flex-direction:column}
853
+ .pagelet-menu__groups{display:flex;flex:1;align-items:center;gap:4px 16px;flex-wrap:wrap}
854
+ .pagelet-menu--centered .pagelet-menu__groups{justify-content:center;flex:none}
855
+ .pagelet-menu__group{display:flex;gap:4px 16px;flex-wrap:wrap;align-items:center}
856
+ .pagelet-menu__group--right{margin-left:auto}
857
+ .pagelet-menu--centered .pagelet-menu__group--right{margin-left:0}
858
+ .pagelet-menu__link{text-decoration:none;color:rgba(0,0,0,.65);padding:6px 0;display:inline-flex;align-items:center;gap:6px}
859
+ .pagelet-menu__link:hover{color:rgba(0,0,0,.9)}
860
+ .pagelet-menu__link--current{color:rgba(0,0,0,.9)}
861
+ .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}
862
+ @media (max-width: 767px){
863
+ .pagelet-menu__toggle{display:inline-block}
864
+ .pagelet-menu__groups{display:none;flex-basis:100%;flex-direction:column;align-items:flex-start}
865
+ .pagelet-menu__groups--open{display:flex}
866
+ .pagelet-menu__group{flex-direction:column;align-items:flex-start}
867
+ .pagelet-menu__group--right{margin-left:0}
868
+ }`;
869
+ function isExternal(href) {
870
+ return /^https?:\/\//i.test(href);
871
+ }
872
+ function needsPlainAnchor(href, LinkComponent) {
873
+ return !LinkComponent || isExternal(href) || /^(mailto|tel):/i.test(href);
874
+ }
875
+ function MenuLink({ item, linkComponent: LinkComponent, currentPath, onNavigate }) {
876
+ const href = safeUrl(item.href);
877
+ if (!href) return null;
878
+ const current = !isExternal(href) && currentPath === href;
879
+ const className = `pagelet-menu__link${current ? " pagelet-menu__link--current" : ""}`;
880
+ const style = item.highlight ? { color: tokenVar("colorAccent") } : void 0;
881
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
882
+ item.label,
883
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: item.icon })
884
+ ] });
885
+ if (needsPlainAnchor(href, LinkComponent)) {
886
+ const external = isExternal(href) ? { target: "_blank", rel: "noopener noreferrer" } : {};
887
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { className, style, href, ...external, children: body });
888
+ }
889
+ return /* @__PURE__ */ jsxRuntime.jsx(LinkComponent, { className, style, to: href, onClick: onNavigate, "aria-current": current ? "page" : void 0, children: body });
890
+ }
891
+ function PageletMenu({ menu = {}, linkComponent, currentPath }) {
892
+ const [open, setOpen] = react.useState(false);
893
+ const items = Array.isArray(menu.items) ? menu.items : [];
894
+ const brand = menu.brand ?? {};
895
+ const logo = safeImageUrl(brand.logo);
896
+ const brandHref = safeUrl(brand.href) ?? "/";
897
+ const close = () => setOpen(false);
898
+ const group = (name) => items.filter((item) => (item.group ?? "left") === name);
899
+ const linkProps = { linkComponent, currentPath, onNavigate: close };
900
+ const BrandLink = needsPlainAnchor(brandHref, linkComponent) ? "a" : linkComponent;
901
+ const brandTarget = BrandLink === "a" ? { href: brandHref } : { to: brandHref, onClick: close };
902
+ return /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: `pagelet-menu${menu.style === "centered" ? " pagelet-menu--centered" : ""}`, "aria-label": "Site", children: [
903
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: CSS }),
904
+ (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: [
905
+ logo && /* @__PURE__ */ jsxRuntime.jsx("img", { src: logo, alt: brand.label || "Home", style: { height: "40px", width: "auto", display: "block" } }),
906
+ !logo && brand.label
907
+ ] }),
908
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "pagelet-menu__toggle", "aria-expanded": open, onClick: () => setOpen((value) => !value), children: "Menu" }),
909
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `pagelet-menu__groups${open ? " pagelet-menu__groups--open" : ""}`, children: [
910
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet-menu__group", children: group("left").map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(MenuLink, { item, ...linkProps }, i)) }),
911
+ /* @__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)) })
912
+ ] })
913
+ ] });
914
+ }
915
+
916
+ function PageletLayout({ site = {}, linkComponent, currentPath, children }) {
917
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pagelet-layout", style: { ...themeToStyle(site.theme), maxWidth: tokenVar("maxWidth"), margin: "0 auto" }, children: [
918
+ /* @__PURE__ */ jsxRuntime.jsx(PageletMenu, { menu: site.menu, linkComponent, currentPath }),
919
+ /* @__PURE__ */ jsxRuntime.jsx("main", { style: { marginTop: "var(--pagelet-space)" }, children })
920
+ ] });
921
+ }
922
+
583
923
  exports.DOCUMENT_VERSION = DOCUMENT_VERSION;
924
+ exports.Icon = Icon;
584
925
  exports.PageletApiError = PageletApiError;
926
+ exports.PageletLayout = PageletLayout;
927
+ exports.PageletMenu = PageletMenu;
585
928
  exports.PageletPage = PageletPage;
586
929
  exports.builtInBlocks = builtInBlocks;
587
930
  exports.builtInCategories = builtInCategories;
@@ -589,8 +932,12 @@ exports.createDocument = createDocument;
589
932
  exports.createPageletClient = createPageletClient;
590
933
  exports.createPalette = createPalette;
591
934
  exports.defaultTheme = defaultTheme;
935
+ exports.embedAllowed = embedAllowed;
592
936
  exports.embedUrl = embedUrl;
593
937
  exports.emptyData = emptyData;
938
+ exports.iconNames = iconNames;
939
+ exports.overlayColor = overlayColor;
940
+ exports.pageSettings = pageSettings;
594
941
  exports.safeImageUrl = safeImageUrl;
595
942
  exports.safeUrl = safeUrl;
596
943
  exports.themeToStyle = themeToStyle;