@byscott-io/pagelet 0.0.0-stage → 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/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +158 -2
- package/dist/editor.cjs +59 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.css +3214 -0
- package/dist/editor.js +57 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +946 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +922 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { migrate, Render } from '@puckeditor/core/rsc';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
const DOCUMENT_VERSION = 1;
|
|
6
|
+
function emptyData() {
|
|
7
|
+
return { root: { props: {} }, content: [] };
|
|
8
|
+
}
|
|
9
|
+
function createDocument(data = emptyData()) {
|
|
10
|
+
return { pagelet: DOCUMENT_VERSION, data };
|
|
11
|
+
}
|
|
12
|
+
function upgradeDocument(document, palette) {
|
|
13
|
+
if (document == null) return createDocument();
|
|
14
|
+
if (typeof document !== "object") {
|
|
15
|
+
throw new TypeError("pagelet: a page document must be an object");
|
|
16
|
+
}
|
|
17
|
+
let doc = document;
|
|
18
|
+
if (!("pagelet" in doc)) {
|
|
19
|
+
if (!("content" in doc) && !("root" in doc)) {
|
|
20
|
+
throw new TypeError('pagelet: not a page document (no "pagelet" version and no Puck data)');
|
|
21
|
+
}
|
|
22
|
+
doc = createDocument(doc);
|
|
23
|
+
}
|
|
24
|
+
if (doc.pagelet > DOCUMENT_VERSION) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`pagelet: this page was saved by a newer pagelet (format ${doc.pagelet}); this version reads up to format ${DOCUMENT_VERSION}. Upgrade @byscott-io/pagelet.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const data = { ...emptyData(), ...doc.data };
|
|
30
|
+
return createDocument(palette && needsPuckMigration(data) ? migrate(data, palette.config) : data);
|
|
31
|
+
}
|
|
32
|
+
function needsPuckMigration(data) {
|
|
33
|
+
const zones = data.zones ?? {};
|
|
34
|
+
if (Object.values(zones).some((content) => Array.isArray(content) && content.length > 0)) return true;
|
|
35
|
+
const root = data.root ?? {};
|
|
36
|
+
return Object.keys(root).some((key) => key !== "props" && key !== "readOnly");
|
|
37
|
+
}
|
|
38
|
+
function pageSettings(document) {
|
|
39
|
+
const props = upgradeDocument(document)?.data?.root?.props ?? {};
|
|
40
|
+
return { title: props.title || "", background: props.background ?? "site" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function PageletPage({ document, palette, theme }) {
|
|
44
|
+
if (!palette) throw new Error("pagelet: <PageletPage> needs a palette (see createPalette)");
|
|
45
|
+
const { data } = upgradeDocument(document, palette);
|
|
46
|
+
return /* @__PURE__ */ jsx(Render, { config: palette.config, data, metadata: { pageletTheme: theme } });
|
|
47
|
+
}
|
|
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
|
+
|
|
106
|
+
const SAFE_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
|
|
107
|
+
function safeUrl(value) {
|
|
108
|
+
if (typeof value !== "string") return void 0;
|
|
109
|
+
const url = value.trim();
|
|
110
|
+
if (url === "") return void 0;
|
|
111
|
+
if (url.startsWith("/") && !url.startsWith("//")) return url;
|
|
112
|
+
if (url.startsWith("#") || url.startsWith("?")) return url;
|
|
113
|
+
const normalized = url.replace(/[\u0000- ]/g, "").toLowerCase();
|
|
114
|
+
const scheme = normalized.match(/^([a-z][a-z0-9+.-]*):/);
|
|
115
|
+
if (!scheme) return void 0;
|
|
116
|
+
return SAFE_SCHEMES.includes(`${scheme[1]}:`) ? url : void 0;
|
|
117
|
+
}
|
|
118
|
+
function safeImageUrl(value) {
|
|
119
|
+
const url = safeUrl(value);
|
|
120
|
+
if (!url) return void 0;
|
|
121
|
+
return /^(mailto|tel):/i.test(url) ? void 0 : url;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const defaultTheme = {
|
|
125
|
+
colorText: "#1f2933",
|
|
126
|
+
colorBackground: "#ffffff",
|
|
127
|
+
colorMuted: "#f3f4f6",
|
|
128
|
+
colorAccent: "#2457a6",
|
|
129
|
+
colorAccentText: "#ffffff",
|
|
130
|
+
fontBody: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
131
|
+
fontHeading: "inherit",
|
|
132
|
+
radius: "8px",
|
|
133
|
+
maxWidth: "1100px",
|
|
134
|
+
space: "24px"
|
|
135
|
+
};
|
|
136
|
+
const TOKEN_NAMES = Object.keys(defaultTheme);
|
|
137
|
+
function cssVariableName(token) {
|
|
138
|
+
return `--pagelet-${token.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
139
|
+
}
|
|
140
|
+
function themeToStyle(theme = {}) {
|
|
141
|
+
const merged = { ...defaultTheme, ...theme };
|
|
142
|
+
const style = {};
|
|
143
|
+
for (const token of TOKEN_NAMES) {
|
|
144
|
+
style[cssVariableName(token)] = String(merged[token]);
|
|
145
|
+
}
|
|
146
|
+
style.color = "var(--pagelet-color-text)";
|
|
147
|
+
style.background = "var(--pagelet-color-background)";
|
|
148
|
+
style.fontFamily = "var(--pagelet-font-body)";
|
|
149
|
+
return style;
|
|
150
|
+
}
|
|
151
|
+
function tokenVar(token) {
|
|
152
|
+
return `var(${cssVariableName(token)})`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const alignOptions = [
|
|
156
|
+
{ label: "Left", value: "left" },
|
|
157
|
+
{ label: "Center", value: "center" },
|
|
158
|
+
{ label: "Right", value: "right" }
|
|
159
|
+
];
|
|
160
|
+
const blockMargin = (align) => align === "center" ? { marginInline: "auto" } : align === "right" ? { marginLeft: "auto" } : {};
|
|
161
|
+
function paragraphs(text) {
|
|
162
|
+
return String(text ?? "").split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
function Paragraphs({ text, style }) {
|
|
165
|
+
return paragraphs(text).map((paragraph, index) => /* @__PURE__ */ jsx("p", { style: { margin: "0 0 1em", whiteSpace: "pre-line", ...style }, children: paragraph }, index));
|
|
166
|
+
}
|
|
167
|
+
function ButtonLink({ label, href, variant = "primary" }) {
|
|
168
|
+
const url = safeUrl(href);
|
|
169
|
+
if (!label) return null;
|
|
170
|
+
const primary = variant !== "outline";
|
|
171
|
+
return /* @__PURE__ */ jsx(
|
|
172
|
+
"a",
|
|
173
|
+
{
|
|
174
|
+
href: url,
|
|
175
|
+
style: {
|
|
176
|
+
display: "inline-block",
|
|
177
|
+
padding: "10px 20px",
|
|
178
|
+
borderRadius: tokenVar("radius"),
|
|
179
|
+
border: `2px solid ${tokenVar("colorAccent")}`,
|
|
180
|
+
background: primary ? tokenVar("colorAccent") : "transparent",
|
|
181
|
+
color: primary ? tokenVar("colorAccentText") : tokenVar("colorAccent"),
|
|
182
|
+
fontWeight: 600,
|
|
183
|
+
textDecoration: "none"
|
|
184
|
+
},
|
|
185
|
+
children: label
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const buttonFields = {
|
|
190
|
+
buttonLabel: { type: "text", label: "Button label" },
|
|
191
|
+
buttonHref: { type: "text", label: "Button link" }
|
|
192
|
+
};
|
|
193
|
+
const Heading = {
|
|
194
|
+
label: "Heading",
|
|
195
|
+
category: "content",
|
|
196
|
+
fields: {
|
|
197
|
+
text: { type: "text" },
|
|
198
|
+
level: {
|
|
199
|
+
type: "select",
|
|
200
|
+
options: [
|
|
201
|
+
{ label: "Large (H1)", value: "h1" },
|
|
202
|
+
{ label: "Medium (H2)", value: "h2" },
|
|
203
|
+
{ label: "Small (H3)", value: "h3" }
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
align: { type: "radio", options: alignOptions }
|
|
207
|
+
},
|
|
208
|
+
defaultProps: { text: "Heading", level: "h2", align: "left" },
|
|
209
|
+
render: ({ text, level, align }) => {
|
|
210
|
+
const Tag = ["h1", "h2", "h3"].includes(level) ? level : "h2";
|
|
211
|
+
return /* @__PURE__ */ jsx(Tag, { style: { fontFamily: tokenVar("fontHeading"), textAlign: align, margin: "0 0 0.5em", textWrap: "balance" }, children: text });
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const Text = {
|
|
215
|
+
label: "Text",
|
|
216
|
+
category: "content",
|
|
217
|
+
fields: {
|
|
218
|
+
text: { type: "textarea" },
|
|
219
|
+
align: { type: "radio", options: alignOptions }
|
|
220
|
+
},
|
|
221
|
+
defaultProps: { text: "Write something here.", align: "left" },
|
|
222
|
+
render: ({ text, align }) => /* @__PURE__ */ jsx("div", { style: { textAlign: align, maxWidth: "68ch", ...blockMargin(align) }, children: /* @__PURE__ */ jsx(Paragraphs, { text }) })
|
|
223
|
+
};
|
|
224
|
+
const Button = {
|
|
225
|
+
label: "Button",
|
|
226
|
+
category: "content",
|
|
227
|
+
fields: {
|
|
228
|
+
label: { type: "text" },
|
|
229
|
+
href: { type: "text", label: "Link" },
|
|
230
|
+
variant: {
|
|
231
|
+
type: "radio",
|
|
232
|
+
options: [
|
|
233
|
+
{ label: "Filled", value: "primary" },
|
|
234
|
+
{ label: "Outline", value: "outline" }
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
align: { type: "radio", options: alignOptions }
|
|
238
|
+
},
|
|
239
|
+
defaultProps: { label: "Learn more", href: "#", variant: "primary", align: "left" },
|
|
240
|
+
render: ({ label, href, variant, align }) => /* @__PURE__ */ jsx("div", { style: { textAlign: align, margin: "0 0 1em" }, children: /* @__PURE__ */ jsx(ButtonLink, { label, href, variant }) })
|
|
241
|
+
};
|
|
242
|
+
const Hero = {
|
|
243
|
+
label: "Hero",
|
|
244
|
+
category: "content",
|
|
245
|
+
fields: {
|
|
246
|
+
heading: { type: "text" },
|
|
247
|
+
subheading: { type: "textarea" },
|
|
248
|
+
imageUrl: { type: "text", label: "Background image URL" },
|
|
249
|
+
...buttonFields
|
|
250
|
+
},
|
|
251
|
+
defaultProps: { heading: "Welcome", subheading: "", imageUrl: "", buttonLabel: "", buttonHref: "" },
|
|
252
|
+
render: ({ heading, subheading, imageUrl, buttonLabel, buttonHref }) => {
|
|
253
|
+
const image = safeImageUrl(imageUrl);
|
|
254
|
+
return /* @__PURE__ */ jsx(
|
|
255
|
+
"header",
|
|
256
|
+
{
|
|
257
|
+
style: {
|
|
258
|
+
padding: "96px 16px",
|
|
259
|
+
textAlign: "center",
|
|
260
|
+
color: image ? "#ffffff" : tokenVar("colorAccentText"),
|
|
261
|
+
background: image ? `linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.45)), url("${encodeURI(image)}") center / cover` : tokenVar("colorAccent")
|
|
262
|
+
},
|
|
263
|
+
children: /* @__PURE__ */ jsxs("div", { style: { maxWidth: "760px", margin: "0 auto" }, children: [
|
|
264
|
+
/* @__PURE__ */ jsx("h1", { style: { fontFamily: tokenVar("fontHeading"), fontSize: "clamp(2rem, 5vw, 3.25rem)", margin: "0 0 0.4em", textWrap: "balance" }, children: heading }),
|
|
265
|
+
/* @__PURE__ */ jsx(Paragraphs, { text: subheading, style: { fontSize: "1.15rem" } }),
|
|
266
|
+
/* @__PURE__ */ jsx(ButtonLink, { label: buttonLabel, href: buttonHref, variant: "outline" })
|
|
267
|
+
] })
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const CallToAction = {
|
|
273
|
+
label: "Call to action",
|
|
274
|
+
category: "content",
|
|
275
|
+
fields: {
|
|
276
|
+
heading: { type: "text" },
|
|
277
|
+
text: { type: "textarea" },
|
|
278
|
+
...buttonFields
|
|
279
|
+
},
|
|
280
|
+
defaultProps: { heading: "Join us", text: "", buttonLabel: "Get in touch", buttonHref: "#" },
|
|
281
|
+
render: ({ heading, text, buttonLabel, buttonHref }) => /* @__PURE__ */ jsxs("div", { style: { background: tokenVar("colorMuted"), borderRadius: tokenVar("radius"), padding: "40px 24px", textAlign: "center" }, children: [
|
|
282
|
+
/* @__PURE__ */ jsx("h2", { style: { fontFamily: tokenVar("fontHeading"), margin: "0 0 0.5em" }, children: heading }),
|
|
283
|
+
/* @__PURE__ */ jsx(Paragraphs, { text }),
|
|
284
|
+
/* @__PURE__ */ jsx(ButtonLink, { label: buttonLabel, href: buttonHref })
|
|
285
|
+
] })
|
|
286
|
+
};
|
|
287
|
+
const Cards = {
|
|
288
|
+
label: "Cards",
|
|
289
|
+
category: "content",
|
|
290
|
+
fields: {
|
|
291
|
+
items: {
|
|
292
|
+
type: "array",
|
|
293
|
+
getItemSummary: (item) => item.title || "Card",
|
|
294
|
+
arrayFields: {
|
|
295
|
+
title: { type: "text" },
|
|
296
|
+
text: { type: "textarea" },
|
|
297
|
+
imageUrl: { type: "text", label: "Image URL" },
|
|
298
|
+
href: { type: "text", label: "Link (optional)" }
|
|
299
|
+
},
|
|
300
|
+
defaultItemProps: { title: "Card title", text: "", imageUrl: "", href: "" }
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
defaultProps: { items: [{ title: "Card title", text: "A short description.", imageUrl: "", href: "" }] },
|
|
304
|
+
render: ({ items = [] }) => /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 240px), 1fr))", gap: tokenVar("space") }, children: items.map((item, index) => {
|
|
305
|
+
const image = safeImageUrl(item.imageUrl);
|
|
306
|
+
const href = safeUrl(item.href);
|
|
307
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
308
|
+
image && /* @__PURE__ */ jsx("img", { src: image, alt: "", style: { width: "100%", aspectRatio: "16 / 9", objectFit: "cover", display: "block" } }),
|
|
309
|
+
/* @__PURE__ */ jsxs("div", { style: { padding: "16px" }, children: [
|
|
310
|
+
/* @__PURE__ */ jsx("h3", { style: { fontFamily: tokenVar("fontHeading"), margin: "0 0 0.4em" }, children: item.title }),
|
|
311
|
+
/* @__PURE__ */ jsx(Paragraphs, { text: item.text })
|
|
312
|
+
] })
|
|
313
|
+
] });
|
|
314
|
+
const cardStyle = { display: "block", background: tokenVar("colorMuted"), borderRadius: tokenVar("radius"), overflow: "hidden", color: "inherit", textDecoration: "none" };
|
|
315
|
+
return href ? /* @__PURE__ */ jsx("a", { href, style: cardStyle, children: body }, index) : /* @__PURE__ */ jsx("div", { style: cardStyle, children: body }, index);
|
|
316
|
+
}) })
|
|
317
|
+
};
|
|
318
|
+
const Faq = {
|
|
319
|
+
label: "FAQ",
|
|
320
|
+
category: "content",
|
|
321
|
+
fields: {
|
|
322
|
+
items: {
|
|
323
|
+
type: "array",
|
|
324
|
+
getItemSummary: (item) => item.question || "Question",
|
|
325
|
+
arrayFields: { question: { type: "text" }, answer: { type: "textarea" } },
|
|
326
|
+
defaultItemProps: { question: "Question?", answer: "" }
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
defaultProps: { items: [{ question: "Question?", answer: "Answer." }] },
|
|
330
|
+
render: ({ items = [] }) => /* @__PURE__ */ jsx("div", { children: items.map((item, index) => /* @__PURE__ */ jsxs("details", { style: { borderBottom: `1px solid ${tokenVar("colorMuted")}`, padding: "12px 0" }, children: [
|
|
331
|
+
/* @__PURE__ */ jsx("summary", { style: { fontWeight: 600, cursor: "pointer" }, children: item.question }),
|
|
332
|
+
/* @__PURE__ */ jsx("div", { style: { paddingTop: "8px" }, children: /* @__PURE__ */ jsx(Paragraphs, { text: item.answer }) })
|
|
333
|
+
] }, index)) })
|
|
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 });
|
|
408
|
+
|
|
409
|
+
const SPACING = { none: "0", small: "16px", medium: "40px", large: "80px" };
|
|
410
|
+
const spacingOptions = Object.keys(SPACING).map((value) => ({ label: value, value }));
|
|
411
|
+
const BACKGROUNDS = {
|
|
412
|
+
none: "transparent",
|
|
413
|
+
muted: tokenVar("colorMuted"),
|
|
414
|
+
accent: tokenVar("colorAccent")
|
|
415
|
+
};
|
|
416
|
+
const Section = {
|
|
417
|
+
label: "Section",
|
|
418
|
+
category: "layout",
|
|
419
|
+
fields: {
|
|
420
|
+
background: {
|
|
421
|
+
type: "select",
|
|
422
|
+
options: [
|
|
423
|
+
{ label: "None", value: "none" },
|
|
424
|
+
{ label: "Muted", value: "muted" },
|
|
425
|
+
{ label: "Accent", value: "accent" }
|
|
426
|
+
]
|
|
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 %"),
|
|
431
|
+
padding: { type: "select", options: spacingOptions },
|
|
432
|
+
content: { type: "slot" }
|
|
433
|
+
},
|
|
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
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const Columns = {
|
|
451
|
+
label: "Columns",
|
|
452
|
+
category: "layout",
|
|
453
|
+
fields: {
|
|
454
|
+
count: {
|
|
455
|
+
type: "radio",
|
|
456
|
+
options: [
|
|
457
|
+
{ label: "2", value: 2 },
|
|
458
|
+
{ label: "3", value: 3 }
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
first: { type: "slot" },
|
|
462
|
+
second: { type: "slot" },
|
|
463
|
+
third: { type: "slot" }
|
|
464
|
+
},
|
|
465
|
+
defaultProps: { count: 2 },
|
|
466
|
+
render: ({ count, first: First, second: Second, third: Third }) => {
|
|
467
|
+
const columns = count === 3 ? [First, Second, Third] : [First, Second];
|
|
468
|
+
return /* @__PURE__ */ jsx(
|
|
469
|
+
"div",
|
|
470
|
+
{
|
|
471
|
+
style: {
|
|
472
|
+
display: "grid",
|
|
473
|
+
gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${count === 3 ? 240 : 320}px), 1fr))`,
|
|
474
|
+
gap: tokenVar("space")
|
|
475
|
+
},
|
|
476
|
+
children: columns.map((Column, index) => /* @__PURE__ */ jsx("div", { style: { minWidth: 0 }, children: /* @__PURE__ */ jsx(Column, {}) }, index))
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
const Spacer = {
|
|
482
|
+
label: "Spacer",
|
|
483
|
+
category: "layout",
|
|
484
|
+
fields: { size: { type: "select", options: spacingOptions.slice(1) } },
|
|
485
|
+
defaultProps: { size: "medium" },
|
|
486
|
+
render: ({ size }) => /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: SPACING[size] ?? SPACING.medium } })
|
|
487
|
+
};
|
|
488
|
+
const Divider = {
|
|
489
|
+
label: "Divider",
|
|
490
|
+
category: "layout",
|
|
491
|
+
fields: {},
|
|
492
|
+
render: () => /* @__PURE__ */ jsx("hr", { style: { border: 0, borderTop: `1px solid ${tokenVar("colorMuted")}`, margin: `${tokenVar("space")} 0` } })
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const Image = {
|
|
496
|
+
label: "Image",
|
|
497
|
+
category: "media",
|
|
498
|
+
fields: {
|
|
499
|
+
url: { type: "text", label: "Image URL" },
|
|
500
|
+
alt: { type: "text", label: "Description (for screen readers)" },
|
|
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 }
|
|
511
|
+
},
|
|
512
|
+
defaultProps: { url: "", alt: "", caption: "", align: "left", width: 100 },
|
|
513
|
+
render: ({ url, alt, caption, align, width }) => {
|
|
514
|
+
const src = safeImageUrl(url);
|
|
515
|
+
if (!src) return null;
|
|
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 })
|
|
522
|
+
] });
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const ImageText = {
|
|
526
|
+
label: "Image with text",
|
|
527
|
+
category: "media",
|
|
528
|
+
fields: {
|
|
529
|
+
url: { type: "text", label: "Image URL" },
|
|
530
|
+
alt: { type: "text", label: "Image description" },
|
|
531
|
+
heading: { type: "text" },
|
|
532
|
+
text: { type: "textarea" },
|
|
533
|
+
imageSide: {
|
|
534
|
+
type: "radio",
|
|
535
|
+
options: [
|
|
536
|
+
{ label: "Left", value: "left" },
|
|
537
|
+
{ label: "Right", value: "right" }
|
|
538
|
+
]
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
defaultProps: { url: "", alt: "", heading: "Heading", text: "", imageSide: "left" },
|
|
542
|
+
render: ({ url, alt, heading, text, imageSide }) => {
|
|
543
|
+
const src = safeImageUrl(url);
|
|
544
|
+
const image = src ? /* @__PURE__ */ jsx("img", { src, alt: alt ?? "", style: { width: "100%", height: "auto", display: "block", borderRadius: tokenVar("radius"), order: imageSide === "right" ? 2 : 0 } }) : null;
|
|
545
|
+
const words = /* @__PURE__ */ jsxs("div", { style: { minWidth: 0, order: 1 }, children: [
|
|
546
|
+
/* @__PURE__ */ jsx("h2", { style: { fontFamily: tokenVar("fontHeading"), margin: "0 0 0.5em" }, children: heading }),
|
|
547
|
+
String(text ?? "").split(/\n\s*\n/).filter((paragraph) => paragraph.trim()).map((paragraph, index) => /* @__PURE__ */ jsx("p", { style: { margin: "0 0 1em" }, children: paragraph.trim() }, index))
|
|
548
|
+
] });
|
|
549
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 300px), 1fr))", gap: tokenVar("space"), alignItems: "center" }, children: [
|
|
550
|
+
image,
|
|
551
|
+
words
|
|
552
|
+
] });
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
function embedUrl(value) {
|
|
556
|
+
let url;
|
|
557
|
+
try {
|
|
558
|
+
url = new URL(String(value ?? "").trim());
|
|
559
|
+
} catch {
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
|
|
563
|
+
const host = url.hostname.replace(/^www\./, "").replace(/^m\./, "");
|
|
564
|
+
let id;
|
|
565
|
+
if (host === "youtube.com") {
|
|
566
|
+
id = url.pathname === "/watch" ? url.searchParams.get("v") : url.pathname.match(/^\/(?:embed|shorts|live)\/([^/]+)/)?.[1];
|
|
567
|
+
return id && /^[\w-]{6,}$/.test(id) ? `https://www.youtube-nocookie.com/embed/${id}` : void 0;
|
|
568
|
+
}
|
|
569
|
+
if (host === "youtu.be") {
|
|
570
|
+
id = url.pathname.slice(1);
|
|
571
|
+
return /^[\w-]{6,}$/.test(id) ? `https://www.youtube-nocookie.com/embed/${id}` : void 0;
|
|
572
|
+
}
|
|
573
|
+
if (host === "vimeo.com") {
|
|
574
|
+
id = url.pathname.match(/^\/(\d+)/)?.[1];
|
|
575
|
+
return id ? `https://player.vimeo.com/video/${id}` : void 0;
|
|
576
|
+
}
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
const VideoEmbed = {
|
|
580
|
+
label: "Video (YouTube or Vimeo)",
|
|
581
|
+
category: "media",
|
|
582
|
+
fields: {
|
|
583
|
+
url: { type: "text", label: "Video link" },
|
|
584
|
+
title: { type: "text", label: "Title (for screen readers)" }
|
|
585
|
+
},
|
|
586
|
+
defaultProps: { url: "", title: "Video" },
|
|
587
|
+
render: ({ url, title }) => {
|
|
588
|
+
const src = embedUrl(url);
|
|
589
|
+
if (!src) return null;
|
|
590
|
+
return /* @__PURE__ */ jsx("div", { style: { position: "relative", aspectRatio: "16 / 9", maxWidth: "100%", margin: "0 0 1em" }, children: /* @__PURE__ */ jsx(
|
|
591
|
+
"iframe",
|
|
592
|
+
{
|
|
593
|
+
src,
|
|
594
|
+
title: title || "Video",
|
|
595
|
+
loading: "lazy",
|
|
596
|
+
allow: "accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen",
|
|
597
|
+
allowFullScreen: true,
|
|
598
|
+
style: { position: "absolute", inset: 0, width: "100%", height: "100%", border: 0, borderRadius: tokenVar("radius") }
|
|
599
|
+
}
|
|
600
|
+
) });
|
|
601
|
+
}
|
|
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
|
+
}
|
|
645
|
+
|
|
646
|
+
const builtInBlocks = {
|
|
647
|
+
Section,
|
|
648
|
+
Columns,
|
|
649
|
+
Spacer,
|
|
650
|
+
Divider,
|
|
651
|
+
Hero,
|
|
652
|
+
Heading,
|
|
653
|
+
Text,
|
|
654
|
+
Quote,
|
|
655
|
+
IconList,
|
|
656
|
+
Schedule,
|
|
657
|
+
Button,
|
|
658
|
+
CallToAction,
|
|
659
|
+
Cards,
|
|
660
|
+
Faq,
|
|
661
|
+
Image,
|
|
662
|
+
ImageText,
|
|
663
|
+
VideoEmbed
|
|
664
|
+
};
|
|
665
|
+
const builtInCategories = {
|
|
666
|
+
layout: { title: "Layout" },
|
|
667
|
+
content: { title: "Content" },
|
|
668
|
+
media: { title: "Media" }
|
|
669
|
+
};
|
|
670
|
+
|
|
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 });
|
|
703
|
+
}
|
|
704
|
+
|
|
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;
|
|
722
|
+
const hidden = new Set(hide);
|
|
723
|
+
for (const name of hidden) {
|
|
724
|
+
if (!(name in builtIns) && !(name in blocks)) {
|
|
725
|
+
throw new Error(`pagelet: cannot hide unknown block "${name}"`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const all = { ...builtIns, ...blocks };
|
|
729
|
+
const components = {};
|
|
730
|
+
const membership = {};
|
|
731
|
+
for (const [name, block] of Object.entries(all)) {
|
|
732
|
+
if (hidden.has(name)) continue;
|
|
733
|
+
if (typeof block?.render !== "function") {
|
|
734
|
+
throw new Error(`pagelet: block "${name}" needs a render function`);
|
|
735
|
+
}
|
|
736
|
+
const { category = "other", ...component } = block;
|
|
737
|
+
components[name] = component;
|
|
738
|
+
(membership[category] ?? (membership[category] = [])).push(name);
|
|
739
|
+
}
|
|
740
|
+
const categoryInfo = { ...builtInCategories, ...categories };
|
|
741
|
+
const puckCategories = {};
|
|
742
|
+
const order = [.../* @__PURE__ */ new Set([...Object.keys(categories), ...Object.keys(builtInCategories), ...Object.keys(membership)])];
|
|
743
|
+
for (const category of order) {
|
|
744
|
+
if (!membership[category]) continue;
|
|
745
|
+
puckCategories[category] = { title: categoryInfo[category]?.title ?? category, components: membership[category] };
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
blocks: Object.keys(components),
|
|
749
|
+
config: {
|
|
750
|
+
components,
|
|
751
|
+
categories: puckCategories,
|
|
752
|
+
root: {
|
|
753
|
+
defaultProps: { title: "", background: "site", backgroundImage: "", overlayColor: "#ffffff" },
|
|
754
|
+
...root,
|
|
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
|
+
)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function blankToUndefined(value) {
|
|
773
|
+
return value === "" || value === null || value === void 0 ? void 0 : value;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
class PageletApiError extends Error {
|
|
777
|
+
constructor(message, { status, errors } = {}) {
|
|
778
|
+
super(message);
|
|
779
|
+
this.name = "PageletApiError";
|
|
780
|
+
this.status = status;
|
|
781
|
+
this.errors = errors ?? {};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function csrfFromPage() {
|
|
785
|
+
if (typeof document === "undefined") return void 0;
|
|
786
|
+
return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ?? void 0;
|
|
787
|
+
}
|
|
788
|
+
function createPageletClient({ baseUrl = "/pagelet", csrfToken, headers = {}, fetch: fetchImpl } = {}) {
|
|
789
|
+
const doFetch = fetchImpl ?? globalThis.fetch;
|
|
790
|
+
if (typeof doFetch !== "function") throw new Error("pagelet: no fetch available; pass one to createPageletClient");
|
|
791
|
+
let root = String(baseUrl);
|
|
792
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
793
|
+
async function request(method, path, body) {
|
|
794
|
+
const token = typeof csrfToken === "function" ? csrfToken() : csrfToken ?? csrfFromPage();
|
|
795
|
+
const response = await doFetch(`${root}${path}`, {
|
|
796
|
+
method,
|
|
797
|
+
credentials: "same-origin",
|
|
798
|
+
headers: {
|
|
799
|
+
Accept: "application/json",
|
|
800
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
801
|
+
...token && method !== "GET" ? { "X-CSRF-Token": token } : {},
|
|
802
|
+
...headers
|
|
803
|
+
},
|
|
804
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
805
|
+
});
|
|
806
|
+
if (response.status === 204) return null;
|
|
807
|
+
const payload = await response.json().catch(() => ({}));
|
|
808
|
+
if (response.ok) return payload;
|
|
809
|
+
const messages = Object.values(payload.errors ?? {}).flat();
|
|
810
|
+
const detail = messages.length ? messages.join("; ") : payload.error ?? response.statusText;
|
|
811
|
+
throw new PageletApiError(`pagelet: ${method} ${path} failed (${response.status}): ${detail}`, {
|
|
812
|
+
status: response.status,
|
|
813
|
+
errors: payload.errors
|
|
814
|
+
});
|
|
815
|
+
}
|
|
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
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
listPages: () => request("GET", "/pages"),
|
|
828
|
+
getPage: (id) => request("GET", pagePath(id)),
|
|
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 } }),
|
|
831
|
+
saveDraft: (id, document2) => request("PATCH", pagePath(id), { page: { draft_document: document2 } }),
|
|
832
|
+
deletePage: (id) => request("DELETE", pagePath(id)),
|
|
833
|
+
publish: (id, { publishedBy } = {}) => request("POST", `${pagePath(id)}/publish`, publishedBy ? { published_by: publishedBy } : {}),
|
|
834
|
+
listVersions: (id) => request("GET", `${pagePath(id)}/versions`),
|
|
835
|
+
restoreVersion: (id, number) => request("POST", `${pagePath(id)}/versions/${encodeURIComponent(number)}/restore`, {}),
|
|
836
|
+
// Public: the published document at a path, or null when there is none.
|
|
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
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
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 };
|
|
922
|
+
//# sourceMappingURL=index.js.map
|