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