@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 ADDED
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (2026-11-13)
4
+
5
+ ### npm `@byscott-io/pagelet`
6
+ - **Page settings** (the page's own fields): a browser title, and a background
7
+ that follows the site's, is custom, or is none, with an overlay color and
8
+ **opacity %**. `pageSettings(document)` returns the title.
9
+ - **Site background**, set in the theme: an image, overlay color and opacity,
10
+ `fit` (`cover`, or `photo` to show the whole photo), and `frame` (`none`, or
11
+ `card` for a bordered, rounded, padded frame).
12
+ - **New blocks:** Quote (italic text plus a citation), Icon list (icon, text, an
13
+ optional link), Schedule (rows of day / time / what), and Embedded page.
14
+ - **Embedded page** is offered only when the app names trusted hosts:
15
+ `createPalette({ embedHosts: ['example.org'] })`. https only, sandboxed.
16
+ - **Right alignment** for Text, Heading, Button and the new blocks; Image gains
17
+ alignment and a width %; Section gains a background image with an overlay
18
+ color and opacity.
19
+ - A built-in **icon set** (inline SVG): location, phone, email, video, calendar,
20
+ clock, link, Facebook, YouTube. `<Icon name>` is exported.
21
+ - **`<PageletMenu>` and `<PageletLayout>`:** a site's menu from its settings
22
+ (a brand logo, items with optional icon and highlight, left and right groups,
23
+ external links in a new tab, a menu button on narrow screens) and the frame
24
+ around a page. Pass your router's link component as `linkComponent`.
25
+ - The menu's brand link is a plain anchor for `tel:`/`mailto:` (like menu
26
+ items), never passed to the app's router.
27
+ - **Client:** `getSite`, `updateSite`, and `getPublishedPage(path)` returning
28
+ `{ document, pagePath, rest }`; `createPage`/`updatePage` take `matchesSubpaths`.
29
+
30
+ ### gem `pagelet`
31
+ - **Site settings** per owner (`Pagelet::Site`): the theme and the menu.
32
+ `GET /site` is public; `PATCH /site` needs `:publish`, since changes are live
33
+ at once. Shapes are checked (menu style, labels, links, groups, size).
34
+ - **Pages that answer for sub-paths:** `matches_subpaths` on a page makes
35
+ `/messages` serve `/messages/43`. An exact page wins, then the longest such
36
+ prefix. `Page.published_match(owner, path)` returns the document, the page's
37
+ path and the remainder; `GET /published` returns them too.
38
+ - **New migration:** run `bin/rails pagelet:install:migrations` and migrate
39
+ (see docs/UPGRADING.md).
40
+
41
+ ## 0.1.0 (2026-11-13)
42
+
43
+ First release.
44
+
45
+ ### npm `@byscott-io/pagelet`
46
+ - `createPalette`: the built-in blocks plus an app's own, with hiding and grouping.
47
+ - `PageletPage`: renders a saved page. No editor code; works in server-side rendering.
48
+ - `PageletEditor` (from `@byscott-io/pagelet/editor`): the visual editor, built on Puck 0.24.
49
+ - Built-in blocks: Section, Columns, Spacer, Divider, Hero, Heading, Text, Button,
50
+ Call to action, Cards, FAQ, Image, Image with text, Video (YouTube/Vimeo).
51
+ - Theming through CSS variables.
52
+ - Versioned page documents (`{ pagelet: 1, data }`), upgraded when loaded.
53
+ - `safeUrl` / `safeImageUrl`: user-entered links and images refuse unsafe schemes.
54
+ - `createPageletClient`: the engine's API from the browser, with CSRF and
55
+ `PageletApiError` carrying the server's field errors.
56
+
57
+ ### gem `pagelet` (Rails engine)
58
+ - Pages scoped to an owner the app supplies; another owner's page is a 404.
59
+ - Drafts, numbered published versions (never edited after publishing), and
60
+ rollback by restoring an old version into the draft.
61
+ - JSON API for the editor, behind the app's own `authorize` hook (`:edit`, `:publish`).
62
+ - `Page.published_document(owner, path)` for server-side rendering, and
63
+ `Page.sitemap_entries(owner)`. Drafts never reach either.
64
+ - Document checks: format version, shape, and a size limit.
65
+ - `bin/rails generate pagelet:install`: initializer, migrations, route.
66
+
67
+ ## 0.0.1 (gem only)
68
+ - 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,159 @@
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
+ quotes, icon lists, schedules, buttons, calls to action, cards, FAQ, images,
12
+ image with text, YouTube/Vimeo video, and (for hosts you allow) embedded pages.
13
+ - **Page backgrounds**: a site-wide photo under a panel of any color and
14
+ opacity, which each page can follow, replace or turn off.
15
+ - **Your own blocks**: register any React component and it appears in the
16
+ palette beside the built-in ones.
17
+ - **Theming** through CSS variables: colors, fonts, corner radius, page width.
18
+ - **A site menu and layout**: logo, items with icons and highlights, left and
19
+ right groups, and a menu button on phones, all from site settings.
20
+
21
+ pagelet is built on [Puck](https://puckeditor.com), an MIT-licensed visual
22
+ editor for React. Your app talks only to pagelet, which keeps Puck's upgrades in
23
+ one place.
24
+
25
+ > Not related to the older `pagelet` package on npm from the BigPipe project.
26
+
27
+ **Status:** early (0.1.x); the API may still change. Two packages:
28
+
29
+ - `@byscott-io/pagelet` (npm): the editor, renderer and blocks.
30
+ - `pagelet` (gem): a Rails engine that stores pages, keeps every published
31
+ version, and serves the editor's API. See [docs/INTEGRATION.md](docs/INTEGRATION.md).
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ yarn add @byscott-io/pagelet react react-dom
37
+ ```
38
+
39
+ ## Use
40
+
41
+ **1. Define your palette once**, with any blocks of your own:
42
+
43
+ ```jsx
44
+ import { createPalette } from '@byscott-io/pagelet';
45
+
46
+ const Quote = {
47
+ label: 'Quote',
48
+ category: 'extras',
49
+ fields: { text: { type: 'textarea' }, author: { type: 'text' } },
50
+ render: ({ text, author }) => (
51
+ <blockquote>
52
+ {text}
53
+ <cite>{author}</cite>
54
+ </blockquote>
55
+ ),
56
+ };
57
+
58
+ export const palette = createPalette({
59
+ blocks: { Quote }, // added beside the built-in blocks
60
+ categories: { extras: { title: 'Extras' } },
61
+ hide: ['Faq'], // built-in blocks you don't want
62
+ });
63
+ ```
64
+
65
+ **2. Edit pages** in your admin screens:
66
+
67
+ ```jsx
68
+ import { PageletEditor } from '@byscott-io/pagelet/editor';
69
+ import '@byscott-io/pagelet/editor.css';
70
+
71
+ <PageletEditor
72
+ document={savedDocument}
73
+ palette={palette}
74
+ theme={siteTheme}
75
+ headerTitle="Home page"
76
+ onPublish={(document) => save(document)}
77
+ />;
78
+ ```
79
+
80
+ **3. Render pages** on your site:
81
+
82
+ ```jsx
83
+ import { PageletPage } from '@byscott-io/pagelet';
84
+
85
+ <PageletPage document={savedDocument} palette={palette} theme={siteTheme} />;
86
+ ```
87
+
88
+ `document` is plain JSON (`{ pagelet: 1, data: {...} }`). Store it as-is; pass
89
+ it back to either component. Pages saved by an older pagelet are upgraded when
90
+ they load.
91
+
92
+ **4. With the Rails engine**, the client does the storing:
93
+
94
+ ```jsx
95
+ import { createPageletClient } from '@byscott-io/pagelet';
96
+
97
+ const pages = createPageletClient(); // same origin, mounted at /pagelet
98
+
99
+ const { page } = await pages.getPage(id);
100
+ <PageletEditor
101
+ document={page.draft_document}
102
+ palette={palette}
103
+ onChange={(document) => pages.saveDraft(id, document)}
104
+ onPublish={async (document) => {
105
+ await pages.saveDraft(id, document);
106
+ await pages.publish(id);
107
+ }}
108
+ />;
109
+ ```
110
+
111
+ It sends the app's session cookie and the CSRF token from the page's
112
+ `csrf-token` meta tag, and throws `PageletApiError` (with `status` and field
113
+ `errors`) when the server refuses.
114
+
115
+ ## Theme
116
+
117
+ Any subset of these tokens; the rest use the defaults:
118
+
119
+ ```js
120
+ {
121
+ colorText, colorBackground, colorMuted,
122
+ colorAccent, colorAccentText,
123
+ fontBody, fontHeading,
124
+ radius, maxWidth, space,
125
+ }
126
+ ```
127
+
128
+ They become CSS variables (`--pagelet-color-accent`, …) on the page wrapper, so
129
+ your own blocks can use them too: `color: var(--pagelet-color-accent)`.
130
+
131
+ A site background goes in the theme too, and each page can follow it, use its
132
+ own, or have none (the page's Background setting):
133
+
134
+ ```js
135
+ background: {
136
+ image: '/images/building.jpg',
137
+ overlayColor: '#ffffff',
138
+ overlayOpacity: 85, // percent
139
+ fit: 'photo', // or 'cover'
140
+ frame: 'card', // or 'none'
141
+ rounded: true,
142
+ }
143
+ ```
144
+
145
+ ## Writing your own blocks
146
+
147
+ A block is a [Puck component config](https://puckeditor.com/docs/api-reference/configuration/component-config)
148
+ (`fields`, `defaultProps`, `render`) plus an optional `category`.
149
+
150
+ - **Render on the server.** `render` must not touch `window` or `document`
151
+ while rendering. Add browser-only behavior in an effect.
152
+ - **Treat every URL as untrusted.** Pass user-entered links through `safeUrl`
153
+ and images through `safeImageUrl` (both exported); they refuse `javascript:`
154
+ and other unsafe schemes.
155
+ - A block with a built-in block's name replaces that block.
156
+
157
+ ## License
158
+
159
+ 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\n// A page's own settings (title, background), for the app to use outside the\n// page body, e.g. the browser title.\nexport function pageSettings(document) {\n const props = upgradeDocument(document)?.data?.root?.props ?? {};\n return { title: props.title || '', background: props.background ?? 'site' };\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;;"}