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