@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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-13)
4
+
5
+ First release.
6
+
7
+ ### npm `@byscott-io/pagelet`
8
+ - `createPalette`: the built-in blocks plus an app's own, with hiding and grouping.
9
+ - `PageletPage`: renders a saved page. No editor code; works in server-side rendering.
10
+ - `PageletEditor` (from `@byscott-io/pagelet/editor`): the visual editor, built on Puck 0.24.
11
+ - Built-in blocks: Section, Columns, Spacer, Divider, Hero, Heading, Text, Button,
12
+ Call to action, Cards, FAQ, Image, Image with text, Video (YouTube/Vimeo).
13
+ - Theming through CSS variables.
14
+ - Versioned page documents (`{ pagelet: 1, data }`), upgraded when loaded.
15
+ - `safeUrl` / `safeImageUrl`: user-entered links and images refuse unsafe schemes.
16
+ - `createPageletClient`: the engine's API from the browser, with CSRF and
17
+ `PageletApiError` carrying the server's field errors.
18
+
19
+ ### gem `pagelet` (Rails engine)
20
+ - Pages scoped to an owner the app supplies; another owner's page is a 404.
21
+ - Drafts, numbered published versions (never edited after publishing), and
22
+ rollback by restoring an old version into the draft.
23
+ - JSON API for the editor, behind the app's own `authorize` hook (`:edit`, `:publish`).
24
+ - `Page.published_document(owner, path)` for server-side rendering, and
25
+ `Page.sitemap_entries(owner)`. Drafts never reach either.
26
+ - Document checks: format version, shape, and a size limit.
27
+ - `bin/rails generate pagelet:install`: initializer, migrations, route.
28
+
29
+ ## 0.0.1 (gem only)
30
+ - Placeholder that reserves the name on RubyGems. No functionality.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scott Gibson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,141 @@
1
- # Temporary Holding Version
1
+ # pagelet
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A page builder for React apps. Your users build pages by dragging blocks onto a
4
+ canvas, and your site draws the saved pages with the same React components.
5
+
6
+ - **A visual editor** for admin screens: drag, reorder, edit, undo, and preview
7
+ at phone and desktop widths.
8
+ - **A small renderer** for public pages. It has no editor code in it, and it
9
+ works in server-side rendering, so search engines see real HTML.
10
+ - **A standard set of blocks**: sections, columns, hero, headings, text,
11
+ buttons, calls to action, cards, FAQ, images, image with text, and
12
+ YouTube/Vimeo video.
13
+ - **Your own blocks**: register any React component and it appears in the
14
+ palette beside the built-in ones.
15
+ - **Theming** through CSS variables: colors, fonts, corner radius, page width.
16
+
17
+ pagelet is built on [Puck](https://puckeditor.com), an MIT-licensed visual
18
+ editor for React. Your app talks only to pagelet, which keeps Puck's upgrades in
19
+ one place.
20
+
21
+ > Not related to the older `pagelet` package on npm from the BigPipe project.
22
+
23
+ **Status:** early (0.1.x); the API may still change. Two packages:
24
+
25
+ - `@byscott-io/pagelet` (npm): the editor, renderer and blocks.
26
+ - `pagelet` (gem): a Rails engine that stores pages, keeps every published
27
+ version, and serves the editor's API. See [docs/INTEGRATION.md](docs/INTEGRATION.md).
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ yarn add @byscott-io/pagelet react react-dom
33
+ ```
34
+
35
+ ## Use
36
+
37
+ **1. Define your palette once**, with any blocks of your own:
38
+
39
+ ```jsx
40
+ import { createPalette } from '@byscott-io/pagelet';
41
+
42
+ const Quote = {
43
+ label: 'Quote',
44
+ category: 'extras',
45
+ fields: { text: { type: 'textarea' }, author: { type: 'text' } },
46
+ render: ({ text, author }) => (
47
+ <blockquote>
48
+ {text}
49
+ <cite>{author}</cite>
50
+ </blockquote>
51
+ ),
52
+ };
53
+
54
+ export const palette = createPalette({
55
+ blocks: { Quote }, // added beside the built-in blocks
56
+ categories: { extras: { title: 'Extras' } },
57
+ hide: ['Faq'], // built-in blocks you don't want
58
+ });
59
+ ```
60
+
61
+ **2. Edit pages** in your admin screens:
62
+
63
+ ```jsx
64
+ import { PageletEditor } from '@byscott-io/pagelet/editor';
65
+ import '@byscott-io/pagelet/editor.css';
66
+
67
+ <PageletEditor
68
+ document={savedDocument}
69
+ palette={palette}
70
+ theme={siteTheme}
71
+ headerTitle="Home page"
72
+ onPublish={(document) => save(document)}
73
+ />;
74
+ ```
75
+
76
+ **3. Render pages** on your site:
77
+
78
+ ```jsx
79
+ import { PageletPage } from '@byscott-io/pagelet';
80
+
81
+ <PageletPage document={savedDocument} palette={palette} theme={siteTheme} />;
82
+ ```
83
+
84
+ `document` is plain JSON (`{ pagelet: 1, data: {...} }`). Store it as-is; pass
85
+ it back to either component. Pages saved by an older pagelet are upgraded when
86
+ they load.
87
+
88
+ **4. With the Rails engine**, the client does the storing:
89
+
90
+ ```jsx
91
+ import { createPageletClient } from '@byscott-io/pagelet';
92
+
93
+ const pages = createPageletClient(); // same origin, mounted at /pagelet
94
+
95
+ const { page } = await pages.getPage(id);
96
+ <PageletEditor
97
+ document={page.draft_document}
98
+ palette={palette}
99
+ onChange={(document) => pages.saveDraft(id, document)}
100
+ onPublish={async (document) => {
101
+ await pages.saveDraft(id, document);
102
+ await pages.publish(id);
103
+ }}
104
+ />;
105
+ ```
106
+
107
+ It sends the app's session cookie and the CSRF token from the page's
108
+ `csrf-token` meta tag, and throws `PageletApiError` (with `status` and field
109
+ `errors`) when the server refuses.
110
+
111
+ ## Theme
112
+
113
+ Any subset of these tokens; the rest use the defaults:
114
+
115
+ ```js
116
+ {
117
+ colorText, colorBackground, colorMuted,
118
+ colorAccent, colorAccentText,
119
+ fontBody, fontHeading,
120
+ radius, maxWidth, space,
121
+ }
122
+ ```
123
+
124
+ They become CSS variables (`--pagelet-color-accent`, …) on the page wrapper, so
125
+ your own blocks can use them too: `color: var(--pagelet-color-accent)`.
126
+
127
+ ## Writing your own blocks
128
+
129
+ A block is a [Puck component config](https://puckeditor.com/docs/api-reference/configuration/component-config)
130
+ (`fields`, `defaultProps`, `render`) plus an optional `category`.
131
+
132
+ - **Render on the server.** `render` must not touch `window` or `document`
133
+ while rendering. Add browser-only behavior in an effect.
134
+ - **Treat every URL as untrusted.** Pass user-entered links through `safeUrl`
135
+ and images through `safeImageUrl` (both exported); they refuse `javascript:`
136
+ and other unsafe schemes.
137
+ - A block with a built-in block's name replaces that block.
138
+
139
+ ## License
140
+
141
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var core = require('@puckeditor/core');
5
+ var rsc = require('@puckeditor/core/rsc');
6
+
7
+ const DOCUMENT_VERSION = 1;
8
+ function emptyData() {
9
+ return { root: { props: {} }, content: [] };
10
+ }
11
+ function createDocument(data = emptyData()) {
12
+ return { pagelet: DOCUMENT_VERSION, data };
13
+ }
14
+ function upgradeDocument(document, palette) {
15
+ if (document == null) return createDocument();
16
+ if (typeof document !== "object") {
17
+ throw new TypeError("pagelet: a page document must be an object");
18
+ }
19
+ let doc = document;
20
+ if (!("pagelet" in doc)) {
21
+ if (!("content" in doc) && !("root" in doc)) {
22
+ throw new TypeError('pagelet: not a page document (no "pagelet" version and no Puck data)');
23
+ }
24
+ doc = createDocument(doc);
25
+ }
26
+ if (doc.pagelet > DOCUMENT_VERSION) {
27
+ throw new Error(
28
+ `pagelet: this page was saved by a newer pagelet (format ${doc.pagelet}); this version reads up to format ${DOCUMENT_VERSION}. Upgrade @byscott-io/pagelet.`
29
+ );
30
+ }
31
+ const data = { ...emptyData(), ...doc.data };
32
+ return createDocument(palette && needsPuckMigration(data) ? rsc.migrate(data, palette.config) : data);
33
+ }
34
+ function needsPuckMigration(data) {
35
+ const zones = data.zones ?? {};
36
+ if (Object.values(zones).some((content) => Array.isArray(content) && content.length > 0)) return true;
37
+ const root = data.root ?? {};
38
+ return Object.keys(root).some((key) => key !== "props" && key !== "readOnly");
39
+ }
40
+
41
+ function PageletEditor({ document, palette, theme, onPublish, onChange, headerTitle, ...rest }) {
42
+ if (!palette) throw new Error("pagelet: <PageletEditor> needs a palette (see createPalette)");
43
+ const { data } = upgradeDocument(document, palette);
44
+ return /* @__PURE__ */ jsxRuntime.jsx(
45
+ core.Puck,
46
+ {
47
+ ...rest,
48
+ config: palette.config,
49
+ data,
50
+ metadata: { pageletTheme: theme },
51
+ headerTitle,
52
+ onPublish: onPublish ? (next) => onPublish(createDocument(next)) : void 0,
53
+ onChange: onChange ? (next) => onChange(createDocument(next)) : void 0
54
+ }
55
+ );
56
+ }
57
+
58
+ exports.PageletEditor = PageletEditor;
59
+ //# sourceMappingURL=editor.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.cjs","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":["migrate","jsx","Puck"],"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,GAAIA,YAAQ,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,uBACEC,cAAA;AAAA,IAACC,SAAA;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;;"}