@byscott-io/pagelet 0.0.0-stage → 0.1.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/editor.js ADDED
@@ -0,0 +1,57 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { Puck } from '@puckeditor/core';
3
+ import { migrate } from '@puckeditor/core/rsc';
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
+
39
+ function PageletEditor({ document, palette, theme, onPublish, onChange, headerTitle, ...rest }) {
40
+ if (!palette) throw new Error("pagelet: <PageletEditor> needs a palette (see createPalette)");
41
+ const { data } = upgradeDocument(document, palette);
42
+ return /* @__PURE__ */ jsx(
43
+ Puck,
44
+ {
45
+ ...rest,
46
+ config: palette.config,
47
+ data,
48
+ metadata: { pageletTheme: theme },
49
+ headerTitle,
50
+ onPublish: onPublish ? (next) => onPublish(createDocument(next)) : void 0,
51
+ onChange: onChange ? (next) => onChange(createDocument(next)) : void 0
52
+ }
53
+ );
54
+ }
55
+
56
+ export { PageletEditor };
57
+ //# sourceMappingURL=editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.js","sources":["../src/document.js","../src/PageletEditor.jsx"],"sourcesContent":["import { migrate } from '@puckeditor/core/rsc';\n\n// What an app stores for a page: Puck's data inside an envelope that carries\n// pagelet's own format version. The version lets a stored page be upgraded when\n// pagelet or Puck changes shape, so apps never hold raw Puck data whose format\n// they do not control.\nexport const DOCUMENT_VERSION = 1;\n\nexport function emptyData() {\n return { root: { props: {} }, content: [] };\n}\n\nexport function createDocument(data = emptyData()) {\n return { pagelet: DOCUMENT_VERSION, data };\n}\n\n// Bring a stored document up to the current format.\n//\n// Accepts bare Puck data too, since an app adopting pagelet may already hold\n// some. Passing the palette also runs Puck's own migrate(), which converts\n// older Puck structures (DropZone zones) into the current one (slots).\nexport function upgradeDocument(document, palette) {\n if (document == null) return createDocument();\n if (typeof document !== 'object') {\n throw new TypeError('pagelet: a page document must be an object');\n }\n\n let doc = document;\n if (!('pagelet' in doc)) {\n if (!('content' in doc) && !('root' in doc)) {\n throw new TypeError('pagelet: not a page document (no \"pagelet\" version and no Puck data)');\n }\n doc = createDocument(doc);\n }\n\n if (doc.pagelet > DOCUMENT_VERSION) {\n throw new Error(\n `pagelet: this page was saved by a newer pagelet (format ${doc.pagelet}); ` +\n `this version reads up to format ${DOCUMENT_VERSION}. Upgrade @byscott-io/pagelet.`,\n );\n }\n\n // Format migrations go here, one step per version:\n // if (doc.pagelet === 1) doc = migrateV1toV2(doc);\n\n const data = { ...emptyData(), ...doc.data };\n return createDocument(palette && needsPuckMigration(data) ? migrate(data, palette.config) : data);\n}\n\n// Puck's migrate() logs to the console every time it is given a config, even\n// when there is nothing to convert. On a public page that would be a log line\n// per page view, so only call it for data in an older Puck shape: DropZone\n// zones holding content, or a root whose fields sit outside `props`.\nexport function needsPuckMigration(data) {\n const zones = data.zones ?? {};\n if (Object.values(zones).some((content) => Array.isArray(content) && content.length > 0)) return true;\n const root = data.root ?? {};\n return Object.keys(root).some((key) => key !== 'props' && key !== 'readOnly');\n}\n","import { Puck } from '@puckeditor/core';\nimport { createDocument, upgradeDocument } from './document.js';\n\n// The visual editor. Import from \"@byscott-io/pagelet/editor\" (and its\n// stylesheet from \"@byscott-io/pagelet/editor.css\") in admin screens only.\n//\n// onPublish and onChange receive a pagelet document, ready to store as-is.\nexport function PageletEditor({ document, palette, theme, onPublish, onChange, headerTitle, ...rest }) {\n if (!palette) throw new Error('pagelet: <PageletEditor> needs a palette (see createPalette)');\n const { data } = upgradeDocument(document, palette);\n return (\n <Puck\n {...rest}\n config={palette.config}\n data={data}\n metadata={{ pageletTheme: theme }}\n headerTitle={headerTitle}\n onPublish={onPublish ? (next) => onPublish(createDocument(next)) : undefined}\n onChange={onChange ? (next) => onChange(createDocument(next)) : undefined}\n />\n );\n}\n"],"names":[],"mappings":";;;;AAMO,MAAM,gBAAA,GAAmB,CAAA;AAEzB,SAAS,SAAA,GAAY;AAC1B,EAAA,OAAO,EAAE,MAAM,EAAE,KAAA,EAAO,EAAC,EAAE,EAAG,OAAA,EAAS,EAAC,EAAE;AAC5C;AAEO,SAAS,cAAA,CAAe,IAAA,GAAO,SAAA,EAAU,EAAG;AACjD,EAAA,OAAO,EAAE,OAAA,EAAS,gBAAA,EAAkB,IAAA,EAAK;AAC3C;AAOO,SAAS,eAAA,CAAgB,UAAU,OAAA,EAAS;AACjD,EAAA,IAAI,QAAA,IAAY,IAAA,EAAM,OAAO,cAAA,EAAe;AAC5C,EAAA,IAAI,OAAO,aAAa,QAAA,EAAU;AAChC,IAAA,MAAM,IAAI,UAAU,4CAA4C,CAAA;AAAA,EAClE;AAEA,EAAA,IAAI,GAAA,GAAM,QAAA;AACV,EAAA,IAAI,EAAE,aAAa,GAAA,CAAA,EAAM;AACvB,IAAA,IAAI,EAAE,SAAA,IAAa,GAAA,CAAA,IAAQ,EAAE,UAAU,GAAA,CAAA,EAAM;AAC3C,MAAA,MAAM,IAAI,UAAU,sEAAsE,CAAA;AAAA,IAC5F;AACA,IAAA,GAAA,GAAM,eAAe,GAAG,CAAA;AAAA,EAC1B;AAEA,EAAA,IAAI,GAAA,CAAI,UAAU,gBAAA,EAAkB;AAClC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,wDAAA,EAA2D,GAAA,CAAI,OAAO,CAAA,mCAAA,EACjC,gBAAgB,CAAA,8BAAA;AAAA,KACvD;AAAA,EACF;AAKA,EAAA,MAAM,OAAO,EAAE,GAAG,WAAU,EAAG,GAAG,IAAI,IAAA,EAAK;AAC3C,EAAA,OAAO,cAAA,CAAe,OAAA,IAAW,kBAAA,CAAmB,IAAI,CAAA,GAAI,QAAQ,IAAA,EAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,IAAI,CAAA;AAClG;AAMO,SAAS,mBAAmB,IAAA,EAAM;AACvC,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,EAAC;AAC7B,EAAA,IAAI,MAAA,CAAO,MAAA,CAAO,KAAK,CAAA,CAAE,KAAK,CAAC,OAAA,KAAY,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,IAAK,OAAA,CAAQ,MAAA,GAAS,CAAC,GAAG,OAAO,IAAA;AACjG,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,IAAA,IAAQ,EAAC;AAC3B,EAAA,OAAO,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,CAAE,IAAA,CAAK,CAAC,GAAA,KAAQ,GAAA,KAAQ,OAAA,IAAW,GAAA,KAAQ,UAAU,CAAA;AAC9E;;ACnDO,SAAS,aAAA,CAAc,EAAE,QAAA,EAAU,OAAA,EAAS,KAAA,EAAO,WAAW,QAAA,EAAU,WAAA,EAAa,GAAG,IAAA,EAAK,EAAG;AACrG,EAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,MAAM,8DAA8D,CAAA;AAC5F,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,eAAA,CAAgB,UAAU,OAAO,CAAA;AAClD,EAAA,uBACE,GAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MACE,GAAG,IAAA;AAAA,MACJ,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,IAAA;AAAA,MACA,QAAA,EAAU,EAAE,YAAA,EAAc,KAAA,EAAM;AAAA,MAChC,WAAA;AAAA,MACA,SAAA,EAAW,YAAY,CAAC,IAAA,KAAS,UAAU,cAAA,CAAe,IAAI,CAAC,CAAA,GAAI,MAAA;AAAA,MACnE,QAAA,EAAU,WAAW,CAAC,IAAA,KAAS,SAAS,cAAA,CAAe,IAAI,CAAC,CAAA,GAAI;AAAA;AAAA,GAClE;AAEJ;;"}
package/dist/index.cjs ADDED
@@ -0,0 +1,599 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var rsc = require('@puckeditor/core/rsc');
5
+
6
+ const DOCUMENT_VERSION = 1;
7
+ function emptyData() {
8
+ return { root: { props: {} }, content: [] };
9
+ }
10
+ function createDocument(data = emptyData()) {
11
+ return { pagelet: DOCUMENT_VERSION, data };
12
+ }
13
+ function upgradeDocument(document, palette) {
14
+ if (document == null) return createDocument();
15
+ if (typeof document !== "object") {
16
+ throw new TypeError("pagelet: a page document must be an object");
17
+ }
18
+ let doc = document;
19
+ if (!("pagelet" in doc)) {
20
+ if (!("content" in doc) && !("root" in doc)) {
21
+ throw new TypeError('pagelet: not a page document (no "pagelet" version and no Puck data)');
22
+ }
23
+ doc = createDocument(doc);
24
+ }
25
+ if (doc.pagelet > DOCUMENT_VERSION) {
26
+ throw new Error(
27
+ `pagelet: this page was saved by a newer pagelet (format ${doc.pagelet}); this version reads up to format ${DOCUMENT_VERSION}. Upgrade @byscott-io/pagelet.`
28
+ );
29
+ }
30
+ const data = { ...emptyData(), ...doc.data };
31
+ return createDocument(palette && needsPuckMigration(data) ? rsc.migrate(data, palette.config) : data);
32
+ }
33
+ function needsPuckMigration(data) {
34
+ const zones = data.zones ?? {};
35
+ if (Object.values(zones).some((content) => Array.isArray(content) && content.length > 0)) return true;
36
+ const root = data.root ?? {};
37
+ return Object.keys(root).some((key) => key !== "props" && key !== "readOnly");
38
+ }
39
+
40
+ function PageletPage({ document, palette, theme }) {
41
+ if (!palette) throw new Error("pagelet: <PageletPage> needs a palette (see createPalette)");
42
+ const { data } = upgradeDocument(document, palette);
43
+ return /* @__PURE__ */ jsxRuntime.jsx(rsc.Render, { config: palette.config, data, metadata: { pageletTheme: theme } });
44
+ }
45
+
46
+ const SAFE_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
47
+ function safeUrl(value) {
48
+ if (typeof value !== "string") return void 0;
49
+ const url = value.trim();
50
+ if (url === "") return void 0;
51
+ if (url.startsWith("/") && !url.startsWith("//")) return url;
52
+ if (url.startsWith("#") || url.startsWith("?")) return url;
53
+ const normalized = url.replace(/[\u0000- ]/g, "").toLowerCase();
54
+ const scheme = normalized.match(/^([a-z][a-z0-9+.-]*):/);
55
+ if (!scheme) return void 0;
56
+ return SAFE_SCHEMES.includes(`${scheme[1]}:`) ? url : void 0;
57
+ }
58
+ function safeImageUrl(value) {
59
+ const url = safeUrl(value);
60
+ if (!url) return void 0;
61
+ return /^(mailto|tel):/i.test(url) ? void 0 : url;
62
+ }
63
+
64
+ const defaultTheme = {
65
+ colorText: "#1f2933",
66
+ colorBackground: "#ffffff",
67
+ colorMuted: "#f3f4f6",
68
+ colorAccent: "#2457a6",
69
+ colorAccentText: "#ffffff",
70
+ fontBody: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
71
+ fontHeading: "inherit",
72
+ radius: "8px",
73
+ maxWidth: "1100px",
74
+ space: "24px"
75
+ };
76
+ const TOKEN_NAMES = Object.keys(defaultTheme);
77
+ function cssVariableName(token) {
78
+ return `--pagelet-${token.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
79
+ }
80
+ function themeToStyle(theme = {}) {
81
+ const merged = { ...defaultTheme, ...theme };
82
+ const style = {};
83
+ for (const token of TOKEN_NAMES) {
84
+ style[cssVariableName(token)] = String(merged[token]);
85
+ }
86
+ style.color = "var(--pagelet-color-text)";
87
+ style.background = "var(--pagelet-color-background)";
88
+ style.fontFamily = "var(--pagelet-font-body)";
89
+ return style;
90
+ }
91
+ function tokenVar(token) {
92
+ return `var(${cssVariableName(token)})`;
93
+ }
94
+
95
+ const alignOptions = [
96
+ { label: "Left", value: "left" },
97
+ { label: "Center", value: "center" }
98
+ ];
99
+ function paragraphs(text) {
100
+ return String(text ?? "").split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
101
+ }
102
+ function Paragraphs({ text, style }) {
103
+ return paragraphs(text).map((paragraph, index) => /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 1em", whiteSpace: "pre-line", ...style }, children: paragraph }, index));
104
+ }
105
+ function ButtonLink({ label, href, variant = "primary" }) {
106
+ const url = safeUrl(href);
107
+ if (!label) return null;
108
+ const primary = variant !== "outline";
109
+ return /* @__PURE__ */ jsxRuntime.jsx(
110
+ "a",
111
+ {
112
+ href: url,
113
+ style: {
114
+ display: "inline-block",
115
+ padding: "10px 20px",
116
+ borderRadius: tokenVar("radius"),
117
+ border: `2px solid ${tokenVar("colorAccent")}`,
118
+ background: primary ? tokenVar("colorAccent") : "transparent",
119
+ color: primary ? tokenVar("colorAccentText") : tokenVar("colorAccent"),
120
+ fontWeight: 600,
121
+ textDecoration: "none"
122
+ },
123
+ children: label
124
+ }
125
+ );
126
+ }
127
+ const buttonFields = {
128
+ buttonLabel: { type: "text", label: "Button label" },
129
+ buttonHref: { type: "text", label: "Button link" }
130
+ };
131
+ const Heading = {
132
+ label: "Heading",
133
+ category: "content",
134
+ fields: {
135
+ text: { type: "text" },
136
+ level: {
137
+ type: "select",
138
+ options: [
139
+ { label: "Large (H1)", value: "h1" },
140
+ { label: "Medium (H2)", value: "h2" },
141
+ { label: "Small (H3)", value: "h3" }
142
+ ]
143
+ },
144
+ align: { type: "radio", options: alignOptions }
145
+ },
146
+ defaultProps: { text: "Heading", level: "h2", align: "left" },
147
+ render: ({ text, level, align }) => {
148
+ const Tag = ["h1", "h2", "h3"].includes(level) ? level : "h2";
149
+ return /* @__PURE__ */ jsxRuntime.jsx(Tag, { style: { fontFamily: tokenVar("fontHeading"), textAlign: align, margin: "0 0 0.5em", textWrap: "balance" }, children: text });
150
+ }
151
+ };
152
+ const Text = {
153
+ label: "Text",
154
+ category: "content",
155
+ fields: {
156
+ text: { type: "textarea" },
157
+ align: { type: "radio", options: alignOptions }
158
+ },
159
+ defaultProps: { text: "Write something here.", align: "left" },
160
+ render: ({ text, align }) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: align, maxWidth: "68ch", marginInline: align === "center" ? "auto" : void 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text }) })
161
+ };
162
+ const Button = {
163
+ label: "Button",
164
+ category: "content",
165
+ fields: {
166
+ label: { type: "text" },
167
+ href: { type: "text", label: "Link" },
168
+ variant: {
169
+ type: "radio",
170
+ options: [
171
+ { label: "Filled", value: "primary" },
172
+ { label: "Outline", value: "outline" }
173
+ ]
174
+ },
175
+ align: { type: "radio", options: alignOptions }
176
+ },
177
+ defaultProps: { label: "Learn more", href: "#", variant: "primary", align: "left" },
178
+ render: ({ label, href, variant, align }) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: align, margin: "0 0 1em" }, children: /* @__PURE__ */ jsxRuntime.jsx(ButtonLink, { label, href, variant }) })
179
+ };
180
+ const Hero = {
181
+ label: "Hero",
182
+ category: "content",
183
+ fields: {
184
+ heading: { type: "text" },
185
+ subheading: { type: "textarea" },
186
+ imageUrl: { type: "text", label: "Background image URL" },
187
+ ...buttonFields
188
+ },
189
+ defaultProps: { heading: "Welcome", subheading: "", imageUrl: "", buttonLabel: "", buttonHref: "" },
190
+ render: ({ heading, subheading, imageUrl, buttonLabel, buttonHref }) => {
191
+ const image = safeImageUrl(imageUrl);
192
+ return /* @__PURE__ */ jsxRuntime.jsx(
193
+ "header",
194
+ {
195
+ style: {
196
+ padding: "96px 16px",
197
+ textAlign: "center",
198
+ color: image ? "#ffffff" : tokenVar("colorAccentText"),
199
+ background: image ? `linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.45)), url("${encodeURI(image)}") center / cover` : tokenVar("colorAccent")
200
+ },
201
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { maxWidth: "760px", margin: "0 auto" }, children: [
202
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontFamily: tokenVar("fontHeading"), fontSize: "clamp(2rem, 5vw, 3.25rem)", margin: "0 0 0.4em", textWrap: "balance" }, children: heading }),
203
+ /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text: subheading, style: { fontSize: "1.15rem" } }),
204
+ /* @__PURE__ */ jsxRuntime.jsx(ButtonLink, { label: buttonLabel, href: buttonHref, variant: "outline" })
205
+ ] })
206
+ }
207
+ );
208
+ }
209
+ };
210
+ const CallToAction = {
211
+ label: "Call to action",
212
+ category: "content",
213
+ fields: {
214
+ heading: { type: "text" },
215
+ text: { type: "textarea" },
216
+ ...buttonFields
217
+ },
218
+ defaultProps: { heading: "Join us", text: "", buttonLabel: "Get in touch", buttonHref: "#" },
219
+ render: ({ heading, text, buttonLabel, buttonHref }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { background: tokenVar("colorMuted"), borderRadius: tokenVar("radius"), padding: "40px 24px", textAlign: "center" }, children: [
220
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontFamily: tokenVar("fontHeading"), margin: "0 0 0.5em" }, children: heading }),
221
+ /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text }),
222
+ /* @__PURE__ */ jsxRuntime.jsx(ButtonLink, { label: buttonLabel, href: buttonHref })
223
+ ] })
224
+ };
225
+ const Cards = {
226
+ label: "Cards",
227
+ category: "content",
228
+ fields: {
229
+ items: {
230
+ type: "array",
231
+ getItemSummary: (item) => item.title || "Card",
232
+ arrayFields: {
233
+ title: { type: "text" },
234
+ text: { type: "textarea" },
235
+ imageUrl: { type: "text", label: "Image URL" },
236
+ href: { type: "text", label: "Link (optional)" }
237
+ },
238
+ defaultItemProps: { title: "Card title", text: "", imageUrl: "", href: "" }
239
+ }
240
+ },
241
+ defaultProps: { items: [{ title: "Card title", text: "A short description.", imageUrl: "", href: "" }] },
242
+ render: ({ items = [] }) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 240px), 1fr))", gap: tokenVar("space") }, children: items.map((item, index) => {
243
+ const image = safeImageUrl(item.imageUrl);
244
+ const href = safeUrl(item.href);
245
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
246
+ image && /* @__PURE__ */ jsxRuntime.jsx("img", { src: image, alt: "", style: { width: "100%", aspectRatio: "16 / 9", objectFit: "cover", display: "block" } }),
247
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "16px" }, children: [
248
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontFamily: tokenVar("fontHeading"), margin: "0 0 0.4em" }, children: item.title }),
249
+ /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text: item.text })
250
+ ] })
251
+ ] });
252
+ const cardStyle = { display: "block", background: tokenVar("colorMuted"), borderRadius: tokenVar("radius"), overflow: "hidden", color: "inherit", textDecoration: "none" };
253
+ return href ? /* @__PURE__ */ jsxRuntime.jsx("a", { href, style: cardStyle, children: body }, index) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: cardStyle, children: body }, index);
254
+ }) })
255
+ };
256
+ const Faq = {
257
+ label: "FAQ",
258
+ category: "content",
259
+ fields: {
260
+ items: {
261
+ type: "array",
262
+ getItemSummary: (item) => item.question || "Question",
263
+ arrayFields: { question: { type: "text" }, answer: { type: "textarea" } },
264
+ defaultItemProps: { question: "Question?", answer: "" }
265
+ }
266
+ },
267
+ defaultProps: { items: [{ question: "Question?", answer: "Answer." }] },
268
+ render: ({ items = [] }) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: items.map((item, index) => /* @__PURE__ */ jsxRuntime.jsxs("details", { style: { borderBottom: `1px solid ${tokenVar("colorMuted")}`, padding: "12px 0" }, children: [
269
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { style: { fontWeight: 600, cursor: "pointer" }, children: item.question }),
270
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingTop: "8px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Paragraphs, { text: item.answer }) })
271
+ ] }, index)) })
272
+ };
273
+
274
+ const SPACING = { none: "0", small: "16px", medium: "40px", large: "80px" };
275
+ const spacingOptions = Object.keys(SPACING).map((value) => ({ label: value, value }));
276
+ const BACKGROUNDS = {
277
+ none: "transparent",
278
+ muted: tokenVar("colorMuted"),
279
+ accent: tokenVar("colorAccent")
280
+ };
281
+ const Section = {
282
+ label: "Section",
283
+ category: "layout",
284
+ fields: {
285
+ background: {
286
+ type: "select",
287
+ options: [
288
+ { label: "None", value: "none" },
289
+ { label: "Muted", value: "muted" },
290
+ { label: "Accent", value: "accent" }
291
+ ]
292
+ },
293
+ padding: { type: "select", options: spacingOptions },
294
+ content: { type: "slot" }
295
+ },
296
+ defaultProps: { background: "none", padding: "medium" },
297
+ render: ({ background, padding, content: Content }) => /* @__PURE__ */ jsxRuntime.jsx(
298
+ "section",
299
+ {
300
+ style: {
301
+ background: BACKGROUNDS[background] ?? BACKGROUNDS.none,
302
+ color: background === "accent" ? tokenVar("colorAccentText") : void 0,
303
+ paddingBlock: SPACING[padding] ?? SPACING.medium
304
+ },
305
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxWidth: tokenVar("maxWidth"), margin: "0 auto", paddingInline: "16px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Content, {}) })
306
+ }
307
+ )
308
+ };
309
+ const Columns = {
310
+ label: "Columns",
311
+ category: "layout",
312
+ fields: {
313
+ count: {
314
+ type: "radio",
315
+ options: [
316
+ { label: "2", value: 2 },
317
+ { label: "3", value: 3 }
318
+ ]
319
+ },
320
+ first: { type: "slot" },
321
+ second: { type: "slot" },
322
+ third: { type: "slot" }
323
+ },
324
+ defaultProps: { count: 2 },
325
+ render: ({ count, first: First, second: Second, third: Third }) => {
326
+ const columns = count === 3 ? [First, Second, Third] : [First, Second];
327
+ return /* @__PURE__ */ jsxRuntime.jsx(
328
+ "div",
329
+ {
330
+ style: {
331
+ display: "grid",
332
+ gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${count === 3 ? 240 : 320}px), 1fr))`,
333
+ gap: tokenVar("space")
334
+ },
335
+ children: columns.map((Column, index) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { minWidth: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(Column, {}) }, index))
336
+ }
337
+ );
338
+ }
339
+ };
340
+ const Spacer = {
341
+ label: "Spacer",
342
+ category: "layout",
343
+ fields: { size: { type: "select", options: spacingOptions.slice(1) } },
344
+ defaultProps: { size: "medium" },
345
+ render: ({ size }) => /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: { height: SPACING[size] ?? SPACING.medium } })
346
+ };
347
+ const Divider = {
348
+ label: "Divider",
349
+ category: "layout",
350
+ fields: {},
351
+ render: () => /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: 0, borderTop: `1px solid ${tokenVar("colorMuted")}`, margin: `${tokenVar("space")} 0` } })
352
+ };
353
+
354
+ const Image = {
355
+ label: "Image",
356
+ category: "media",
357
+ fields: {
358
+ url: { type: "text", label: "Image URL" },
359
+ alt: { type: "text", label: "Description (for screen readers)" },
360
+ caption: { type: "text" }
361
+ },
362
+ defaultProps: { url: "", alt: "", caption: "" },
363
+ render: ({ url, alt, caption }) => {
364
+ const src = safeImageUrl(url);
365
+ if (!src) return null;
366
+ return /* @__PURE__ */ jsxRuntime.jsxs("figure", { style: { margin: "0 0 1em" }, children: [
367
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", style: { maxWidth: "100%", height: "auto", display: "block", borderRadius: tokenVar("radius") } }),
368
+ caption && /* @__PURE__ */ jsxRuntime.jsx("figcaption", { style: { fontSize: "0.9em", opacity: 0.75, marginTop: "6px" }, children: caption })
369
+ ] });
370
+ }
371
+ };
372
+ const ImageText = {
373
+ label: "Image with text",
374
+ category: "media",
375
+ fields: {
376
+ url: { type: "text", label: "Image URL" },
377
+ alt: { type: "text", label: "Image description" },
378
+ heading: { type: "text" },
379
+ text: { type: "textarea" },
380
+ imageSide: {
381
+ type: "radio",
382
+ options: [
383
+ { label: "Left", value: "left" },
384
+ { label: "Right", value: "right" }
385
+ ]
386
+ }
387
+ },
388
+ defaultProps: { url: "", alt: "", heading: "Heading", text: "", imageSide: "left" },
389
+ render: ({ url, alt, heading, text, imageSide }) => {
390
+ const src = safeImageUrl(url);
391
+ const image = src ? /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", style: { width: "100%", height: "auto", display: "block", borderRadius: tokenVar("radius"), order: imageSide === "right" ? 2 : 0 } }) : null;
392
+ const words = /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: 0, order: 1 }, children: [
393
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontFamily: tokenVar("fontHeading"), margin: "0 0 0.5em" }, children: heading }),
394
+ String(text ?? "").split(/\n\s*\n/).filter((paragraph) => paragraph.trim()).map((paragraph, index) => /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 1em" }, children: paragraph.trim() }, index))
395
+ ] });
396
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 300px), 1fr))", gap: tokenVar("space"), alignItems: "center" }, children: [
397
+ image,
398
+ words
399
+ ] });
400
+ }
401
+ };
402
+ function embedUrl(value) {
403
+ let url;
404
+ try {
405
+ url = new URL(String(value ?? "").trim());
406
+ } catch {
407
+ return void 0;
408
+ }
409
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
410
+ const host = url.hostname.replace(/^www\./, "").replace(/^m\./, "");
411
+ let id;
412
+ if (host === "youtube.com") {
413
+ id = url.pathname === "/watch" ? url.searchParams.get("v") : url.pathname.match(/^\/(?:embed|shorts|live)\/([^/]+)/)?.[1];
414
+ return id && /^[\w-]{6,}$/.test(id) ? `https://www.youtube-nocookie.com/embed/${id}` : void 0;
415
+ }
416
+ if (host === "youtu.be") {
417
+ id = url.pathname.slice(1);
418
+ return /^[\w-]{6,}$/.test(id) ? `https://www.youtube-nocookie.com/embed/${id}` : void 0;
419
+ }
420
+ if (host === "vimeo.com") {
421
+ id = url.pathname.match(/^\/(\d+)/)?.[1];
422
+ return id ? `https://player.vimeo.com/video/${id}` : void 0;
423
+ }
424
+ return void 0;
425
+ }
426
+ const VideoEmbed = {
427
+ label: "Video (YouTube or Vimeo)",
428
+ category: "media",
429
+ fields: {
430
+ url: { type: "text", label: "Video link" },
431
+ title: { type: "text", label: "Title (for screen readers)" }
432
+ },
433
+ defaultProps: { url: "", title: "Video" },
434
+ render: ({ url, title }) => {
435
+ const src = embedUrl(url);
436
+ if (!src) return null;
437
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", aspectRatio: "16 / 9", maxWidth: "100%", margin: "0 0 1em" }, children: /* @__PURE__ */ jsxRuntime.jsx(
438
+ "iframe",
439
+ {
440
+ src,
441
+ title: title || "Video",
442
+ loading: "lazy",
443
+ allow: "accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen",
444
+ allowFullScreen: true,
445
+ style: { position: "absolute", inset: 0, width: "100%", height: "100%", border: 0, borderRadius: tokenVar("radius") }
446
+ }
447
+ ) });
448
+ }
449
+ };
450
+
451
+ const builtInBlocks = {
452
+ Section,
453
+ Columns,
454
+ Spacer,
455
+ Divider,
456
+ Hero,
457
+ Heading,
458
+ Text,
459
+ Button,
460
+ CallToAction,
461
+ Cards,
462
+ Faq,
463
+ Image,
464
+ ImageText,
465
+ VideoEmbed
466
+ };
467
+ const builtInCategories = {
468
+ layout: { title: "Layout" },
469
+ content: { title: "Content" },
470
+ media: { title: "Media" }
471
+ };
472
+
473
+ function ThemeRoot({ theme, children }) {
474
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pagelet", style: themeToStyle(theme), children });
475
+ }
476
+
477
+ function createPalette({ blocks = {}, hide = [], categories = {}, root = {} } = {}) {
478
+ const hidden = new Set(hide);
479
+ for (const name of hidden) {
480
+ if (!(name in builtInBlocks) && !(name in blocks)) {
481
+ throw new Error(`pagelet: cannot hide unknown block "${name}"`);
482
+ }
483
+ }
484
+ const all = { ...builtInBlocks, ...blocks };
485
+ const components = {};
486
+ const membership = {};
487
+ for (const [name, block] of Object.entries(all)) {
488
+ if (hidden.has(name)) continue;
489
+ if (typeof block?.render !== "function") {
490
+ throw new Error(`pagelet: block "${name}" needs a render function`);
491
+ }
492
+ const { category = "other", ...component } = block;
493
+ components[name] = component;
494
+ (membership[category] ?? (membership[category] = [])).push(name);
495
+ }
496
+ const categoryInfo = { ...builtInCategories, ...categories };
497
+ const puckCategories = {};
498
+ const order = [.../* @__PURE__ */ new Set([...Object.keys(categories), ...Object.keys(builtInCategories), ...Object.keys(membership)])];
499
+ for (const category of order) {
500
+ if (!membership[category]) continue;
501
+ puckCategories[category] = { title: categoryInfo[category]?.title ?? category, components: membership[category] };
502
+ }
503
+ return {
504
+ blocks: Object.keys(components),
505
+ config: {
506
+ components,
507
+ categories: puckCategories,
508
+ root: {
509
+ ...root,
510
+ // The page's root render applies the theme. It is the root of the
511
+ // editor's preview iframe as well as of a rendered page, so the theme
512
+ // reaches both. Pages receive it through Puck's metadata.
513
+ render: ({ children, puck }) => /* @__PURE__ */ jsxRuntime.jsx(ThemeRoot, { theme: puck?.metadata?.pageletTheme, children })
514
+ }
515
+ }
516
+ };
517
+ }
518
+
519
+ class PageletApiError extends Error {
520
+ constructor(message, { status, errors } = {}) {
521
+ super(message);
522
+ this.name = "PageletApiError";
523
+ this.status = status;
524
+ this.errors = errors ?? {};
525
+ }
526
+ }
527
+ function csrfFromPage() {
528
+ if (typeof document === "undefined") return void 0;
529
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ?? void 0;
530
+ }
531
+ function createPageletClient({ baseUrl = "/pagelet", csrfToken, headers = {}, fetch: fetchImpl } = {}) {
532
+ const doFetch = fetchImpl ?? globalThis.fetch;
533
+ if (typeof doFetch !== "function") throw new Error("pagelet: no fetch available; pass one to createPageletClient");
534
+ let root = String(baseUrl);
535
+ while (root.endsWith("/")) root = root.slice(0, -1);
536
+ async function request(method, path, body) {
537
+ const token = typeof csrfToken === "function" ? csrfToken() : csrfToken ?? csrfFromPage();
538
+ const response = await doFetch(`${root}${path}`, {
539
+ method,
540
+ credentials: "same-origin",
541
+ headers: {
542
+ Accept: "application/json",
543
+ ...body === void 0 ? {} : { "Content-Type": "application/json" },
544
+ ...token && method !== "GET" ? { "X-CSRF-Token": token } : {},
545
+ ...headers
546
+ },
547
+ body: body === void 0 ? void 0 : JSON.stringify(body)
548
+ });
549
+ if (response.status === 204) return null;
550
+ const payload = await response.json().catch(() => ({}));
551
+ if (response.ok) return payload;
552
+ const messages = Object.values(payload.errors ?? {}).flat();
553
+ const detail = messages.length ? messages.join("; ") : payload.error ?? response.statusText;
554
+ throw new PageletApiError(`pagelet: ${method} ${path} failed (${response.status}): ${detail}`, {
555
+ status: response.status,
556
+ errors: payload.errors
557
+ });
558
+ }
559
+ const pagePath = (id) => `/pages/${encodeURIComponent(id)}`;
560
+ return {
561
+ listPages: () => request("GET", "/pages"),
562
+ getPage: (id) => request("GET", pagePath(id)),
563
+ createPage: ({ path, title }) => request("POST", "/pages", { page: { path, title } }),
564
+ updatePage: (id, { path, title }) => request("PATCH", pagePath(id), { page: { path, title } }),
565
+ saveDraft: (id, document2) => request("PATCH", pagePath(id), { page: { draft_document: document2 } }),
566
+ deletePage: (id) => request("DELETE", pagePath(id)),
567
+ publish: (id, { publishedBy } = {}) => request("POST", `${pagePath(id)}/publish`, publishedBy ? { published_by: publishedBy } : {}),
568
+ listVersions: (id) => request("GET", `${pagePath(id)}/versions`),
569
+ restoreVersion: (id, number) => request("POST", `${pagePath(id)}/versions/${encodeURIComponent(number)}/restore`, {}),
570
+ // Public: the published document at a path, or null when there is none.
571
+ getPublished: async (path) => {
572
+ try {
573
+ const { document: document2 } = await request("GET", `/published?path=${encodeURIComponent(path)}`);
574
+ return document2;
575
+ } catch (error) {
576
+ if (error instanceof PageletApiError && error.status === 404) return null;
577
+ throw error;
578
+ }
579
+ }
580
+ };
581
+ }
582
+
583
+ exports.DOCUMENT_VERSION = DOCUMENT_VERSION;
584
+ exports.PageletApiError = PageletApiError;
585
+ exports.PageletPage = PageletPage;
586
+ exports.builtInBlocks = builtInBlocks;
587
+ exports.builtInCategories = builtInCategories;
588
+ exports.createDocument = createDocument;
589
+ exports.createPageletClient = createPageletClient;
590
+ exports.createPalette = createPalette;
591
+ exports.defaultTheme = defaultTheme;
592
+ exports.embedUrl = embedUrl;
593
+ exports.emptyData = emptyData;
594
+ exports.safeImageUrl = safeImageUrl;
595
+ exports.safeUrl = safeUrl;
596
+ exports.themeToStyle = themeToStyle;
597
+ exports.tokenVar = tokenVar;
598
+ exports.upgradeDocument = upgradeDocument;
599
+ //# sourceMappingURL=index.cjs.map