@techrox/page-studio 1.0.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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/index.cjs +548 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +543 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
- package/src/styles.css +193 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 techrox
|
|
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
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @techrox/page-studio
|
|
2
|
+
|
|
3
|
+
The editor shell for Page Studio — a drop-in `<PageStudio />` component that gives you a visual page builder backed by [Puck](https://puckeditor.com).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @techrox/page-studio @techrox/page-studio-blocks
|
|
7
|
+
pnpm add @puckeditor/core antd @ant-design/icons # peers
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```jsx
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import { PageStudio } from '@techrox/page-studio';
|
|
16
|
+
import '@techrox/page-studio/styles.css';
|
|
17
|
+
import '@techrox/page-studio-blocks/styles.css';
|
|
18
|
+
|
|
19
|
+
export default function BuilderRoute({ pageKey, initialData }) {
|
|
20
|
+
return (
|
|
21
|
+
<PageStudio
|
|
22
|
+
pageKey={pageKey}
|
|
23
|
+
initialData={initialData}
|
|
24
|
+
pageTitle="About us"
|
|
25
|
+
adapter={{
|
|
26
|
+
savePage: async (key, data) => api.savePage(key, data),
|
|
27
|
+
loadPage: async (key) => api.loadPage(key), // optional if initialData is provided
|
|
28
|
+
onCreatePage: () => router.push('/admin/pages/new'), // optional — shows "New page" button
|
|
29
|
+
}}
|
|
30
|
+
branding={{
|
|
31
|
+
name: 'Acme CMS',
|
|
32
|
+
logo: <SvgLogo />,
|
|
33
|
+
primaryColor: '#0F766E',
|
|
34
|
+
}}
|
|
35
|
+
studio={{ Link, services, site, submitLead, track }}
|
|
36
|
+
account={{ name: 'Jane', email: 'jane@acme.com' }}
|
|
37
|
+
onSignOut={() => signOut()}
|
|
38
|
+
homeHref="/admin/pages"
|
|
39
|
+
livePath="/about"
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Props
|
|
46
|
+
|
|
47
|
+
| Prop | Type | Notes |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `pageKey` | `string` | **Required.** Logical key for the page; passed to adapter functions. |
|
|
50
|
+
| `initialData` | `PuckData` | Optional. If supplied, `loadPage` is skipped — best for SSR. |
|
|
51
|
+
| `pageTitle` | `string` | Human-readable label shown in the top bar crumb. |
|
|
52
|
+
| `adapter` | `{ loadPage?, savePage?, onCreatePage? }` | Async functions the editor calls. `savePage` is required for publish. If `onCreatePage` is set, a "New page" button appears in the top bar. |
|
|
53
|
+
| `studio` | `StudioValue` | Forwarded to `<PageStudioProvider>` so blocks see `Link`, `services`, `site`, `submitLead`, `subscribeNewsletter`, `track`. See `@techrox/page-studio-blocks`. |
|
|
54
|
+
| `branding` | `{ name?, logo?, primaryColor?, accentColor?, inkColor? }` | Drives the top-bar identity + CSS variables (`--psd-primary`, `--psd-accent`, `--psd-ink`). |
|
|
55
|
+
| `header` | `ReactNode \| (props) => ReactNode` | Fully replace the default top bar. If a function, receives `{ pageKey, pageTitle, account, livePath, savedAt, pending, onPublish, onSignOut, onCreatePage, branding, extraActions, LinkComponent }`. |
|
|
56
|
+
| `headerActions` | `ReactNode` | Extra buttons appended to the default top bar (between View live + Publish). |
|
|
57
|
+
| `account` | `{ name, email }` | If set, an avatar/dropdown appears at the top right. |
|
|
58
|
+
| `onSignOut` | `() => void` | Optional sign-out handler for the account dropdown. |
|
|
59
|
+
| `homeHref` | `string` | If set, the brand mark + "Admin home" menu entry link here. |
|
|
60
|
+
| `livePath` | `string` | If set, a "View live" button opens this path in a new tab. |
|
|
61
|
+
| `config` | `PuckConfig` | Override the Puck config (use `createPuckConfig` from blocks). |
|
|
62
|
+
| `overrides` | `PuckOverrides` | Override Puck overrides (defaults to the block-card drawer item). |
|
|
63
|
+
| `sidebarLabels` | `{ blocks?, layers? }` | Labels for the injected sidebar tabs. Default: "Blocks" / "Layers". |
|
|
64
|
+
| `LinkComponent` | `Component` | Component used for top-bar links (defaults to `<a>`). Pass `next/link` or React Router's `Link` for client-side navigation. |
|
|
65
|
+
|
|
66
|
+
## What the editor renders
|
|
67
|
+
|
|
68
|
+
The default top bar shows:
|
|
69
|
+
|
|
70
|
+
- Brand mark + name on the left (links to `homeHref` if set)
|
|
71
|
+
- Page title crumb in the centre
|
|
72
|
+
- Right side: optional `headerActions`, optional "New page" (if `onCreatePage` set), optional "View live" (if `livePath` set), the **Publish** button, optional account avatar
|
|
73
|
+
|
|
74
|
+
The sidebar is Puck's, enhanced with:
|
|
75
|
+
|
|
76
|
+
- A **Blocks / Layers** tab bar at the top
|
|
77
|
+
- A count badge on each component category header
|
|
78
|
+
|
|
79
|
+
These enhancements live in `BuilderEnhancements.jsx` and are pure post-mount DOM mutations against Puck's emitted classnames — kept resilient to minor Puck version changes by tagging sections via content rather than DOM order.
|
|
80
|
+
|
|
81
|
+
## Adapter contract
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
type Adapter = {
|
|
85
|
+
loadPage?: (pageKey: string) => Promise<PuckData>;
|
|
86
|
+
savePage?: (pageKey: string, data: PuckData) => Promise<void>;
|
|
87
|
+
onCreatePage?: () => void;
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- The editor **does not** know how you persist data. JWT, session cookies, signed URLs, anything — `savePage` is your line of integration.
|
|
92
|
+
- Both `loadPage` and `savePage` should throw on failure. The editor turns errors into AntD message toasts.
|
|
93
|
+
- `onCreatePage` is a *callback*, not an adapter function — typically just `router.push('/new')`. The button stays hidden unless the prop is set.
|
|
94
|
+
|
|
95
|
+
## Customising the top bar
|
|
96
|
+
|
|
97
|
+
For small additions, use `headerActions` to slot extra buttons next to Publish.
|
|
98
|
+
|
|
99
|
+
For full control, pass `header` as a render function:
|
|
100
|
+
|
|
101
|
+
```jsx
|
|
102
|
+
<PageStudio
|
|
103
|
+
header={({ onPublish, pending, pageTitle }) => (
|
|
104
|
+
<MyCustomBar title={pageTitle} onPublish={onPublish} saving={pending} />
|
|
105
|
+
)}
|
|
106
|
+
// ...
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Your function receives all the same props the default top bar uses, so you can pick and choose what to render.
|
|
111
|
+
|
|
112
|
+
## Required host CSS
|
|
113
|
+
|
|
114
|
+
The editor ships:
|
|
115
|
+
|
|
116
|
+
- `@techrox/page-studio/styles.css` — editor chrome (top bar, sidebar tabs, loading state)
|
|
117
|
+
- `@techrox/page-studio-blocks/styles.css` — block-card picker UI + reveal animations
|
|
118
|
+
|
|
119
|
+
Block typography classes (`.tps-h1`, `.tps-section`, `.tps-container`, `.tps-lede`, etc.) are **not** in the package — they live in your host stylesheet. The blocks reference these class names but expect the host to define them. See `@techrox/page-studio-blocks` README for the full list.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/index.js
|
|
20
|
+
var index_exports = {};
|
|
21
|
+
__export(index_exports, {
|
|
22
|
+
BuilderEnhancements: () => BuilderEnhancements,
|
|
23
|
+
BuilderTopBar: () => BuilderTopBar,
|
|
24
|
+
PageStudio: () => PageStudio,
|
|
25
|
+
PageStudioProvider: () => import_page_studio_blocks2.PageStudioProvider,
|
|
26
|
+
createPuckConfig: () => import_page_studio_blocks2.createPuckConfig,
|
|
27
|
+
defaultBlocks: () => import_page_studio_blocks2.defaultBlocks,
|
|
28
|
+
defaultCategories: () => import_page_studio_blocks2.defaultCategories,
|
|
29
|
+
defaultOverrides: () => import_page_studio_blocks2.defaultOverrides,
|
|
30
|
+
emptyPuckData: () => import_page_studio_blocks2.emptyPuckData,
|
|
31
|
+
useStudio: () => import_page_studio_blocks2.useStudio
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/PageStudio.jsx
|
|
36
|
+
var import_react2 = require("react");
|
|
37
|
+
var import_core = require("@puckeditor/core");
|
|
38
|
+
var import_antd2 = require("antd");
|
|
39
|
+
var import_page_studio_blocks = require("@techrox/page-studio-blocks");
|
|
40
|
+
|
|
41
|
+
// src/BuilderEnhancements.jsx
|
|
42
|
+
var import_react = require("react");
|
|
43
|
+
var STORAGE_KEY = "psd.builderTab";
|
|
44
|
+
function BuilderEnhancements({
|
|
45
|
+
blocksLabel = "Blocks",
|
|
46
|
+
layersLabel = "Layers",
|
|
47
|
+
searchPlaceholder = "Search blocks"
|
|
48
|
+
} = {}) {
|
|
49
|
+
(0, import_react.useEffect)(() => {
|
|
50
|
+
if (typeof document === "undefined") return;
|
|
51
|
+
let observer = null;
|
|
52
|
+
let query = "";
|
|
53
|
+
const matchesQuery = (card, q) => {
|
|
54
|
+
if (!q) return true;
|
|
55
|
+
const name = (card.querySelector(".tps-block-card__name")?.textContent || "").toLowerCase();
|
|
56
|
+
const desc = (card.querySelector(".tps-block-card__desc")?.textContent || "").toLowerCase();
|
|
57
|
+
return name.includes(q) || desc.includes(q);
|
|
58
|
+
};
|
|
59
|
+
const setAttrIfChanged = (el, name, value) => {
|
|
60
|
+
const current = el.getAttribute(name);
|
|
61
|
+
if (value == null) {
|
|
62
|
+
if (current !== null) el.removeAttribute(name);
|
|
63
|
+
} else if (current !== value) {
|
|
64
|
+
el.setAttribute(name, value);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const updateCounts = (sidebar) => {
|
|
68
|
+
const headers = sidebar.querySelectorAll('[class*="_ComponentList-title_"]');
|
|
69
|
+
headers.forEach((header) => {
|
|
70
|
+
const parent = header.closest('[class*="_ComponentList_"]');
|
|
71
|
+
if (!parent) return;
|
|
72
|
+
const list = parent.querySelector('[class*="_ComponentList-content_"]');
|
|
73
|
+
if (!list) return;
|
|
74
|
+
const cards = list.querySelectorAll(".tps-block-card");
|
|
75
|
+
const visible = Array.from(cards).filter(
|
|
76
|
+
(c) => c.getAttribute("data-psd-hidden") !== "true"
|
|
77
|
+
).length;
|
|
78
|
+
const count = String(visible);
|
|
79
|
+
let badge = header.querySelector(".psd-cat-count");
|
|
80
|
+
if (!badge) {
|
|
81
|
+
badge = document.createElement("span");
|
|
82
|
+
badge.className = "psd-cat-count";
|
|
83
|
+
badge.textContent = count;
|
|
84
|
+
const chevron = header.querySelector('[class*="_ComponentList-titleIcon_"]');
|
|
85
|
+
if (chevron) {
|
|
86
|
+
header.insertBefore(badge, chevron);
|
|
87
|
+
} else {
|
|
88
|
+
header.appendChild(badge);
|
|
89
|
+
}
|
|
90
|
+
} else if (badge.textContent !== count) {
|
|
91
|
+
badge.textContent = count;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const applyFilter = (sidebar) => {
|
|
96
|
+
const q = query.trim().toLowerCase();
|
|
97
|
+
const cards = sidebar.querySelectorAll(".tps-block-card");
|
|
98
|
+
cards.forEach((card) => {
|
|
99
|
+
const hidden = !matchesQuery(card, q);
|
|
100
|
+
setAttrIfChanged(card, "data-psd-hidden", hidden ? "true" : null);
|
|
101
|
+
});
|
|
102
|
+
const drawers = sidebar.querySelectorAll("[data-puck-drawer]");
|
|
103
|
+
drawers.forEach((drawer) => {
|
|
104
|
+
Array.from(drawer.children).forEach((cell) => {
|
|
105
|
+
const card = cell.querySelector(".tps-block-card");
|
|
106
|
+
if (!card) {
|
|
107
|
+
setAttrIfChanged(cell, "data-psd-cell-hidden", null);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const hidden = !matchesQuery(card, q);
|
|
111
|
+
setAttrIfChanged(cell, "data-psd-cell-hidden", hidden ? "true" : null);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
const groups = sidebar.querySelectorAll('[class*="_ComponentList_"]');
|
|
115
|
+
groups.forEach((g) => {
|
|
116
|
+
const items = g.querySelectorAll(".tps-block-card");
|
|
117
|
+
if (!items.length) {
|
|
118
|
+
setAttrIfChanged(g, "data-psd-empty", null);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const allHidden = Array.from(items).every(
|
|
122
|
+
(c) => c.getAttribute("data-psd-hidden") === "true"
|
|
123
|
+
);
|
|
124
|
+
setAttrIfChanged(g, "data-psd-empty", q && allHidden ? "true" : null);
|
|
125
|
+
});
|
|
126
|
+
updateCounts(sidebar);
|
|
127
|
+
};
|
|
128
|
+
const apply = () => {
|
|
129
|
+
const sidebar = document.querySelector('[class*="_Sidebar--left"]');
|
|
130
|
+
if (!sidebar) return;
|
|
131
|
+
const sections = sidebar.querySelectorAll('[class*="_SidebarSection_"]');
|
|
132
|
+
if (sections.length < 2) return;
|
|
133
|
+
sections.forEach((s) => {
|
|
134
|
+
const isComponents = !!s.querySelector('[class*="_ComponentList_"]');
|
|
135
|
+
const want = isComponents ? "components" : "outline";
|
|
136
|
+
if (s.getAttribute("data-psd-section") !== want) {
|
|
137
|
+
s.setAttribute("data-psd-section", want);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
sections.forEach((s) => {
|
|
141
|
+
const title = s.querySelector('[class*="_SidebarSection-title_"]');
|
|
142
|
+
if (title && !title.hasAttribute("data-psd-hidden-title")) {
|
|
143
|
+
title.setAttribute("data-psd-hidden-title", "true");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
let tabs = sidebar.querySelector(".psd-sidebar-tabs");
|
|
147
|
+
if (!tabs) {
|
|
148
|
+
tabs = document.createElement("div");
|
|
149
|
+
tabs.className = "psd-sidebar-tabs";
|
|
150
|
+
tabs.innerHTML = `
|
|
151
|
+
<button type="button" class="psd-sidebar-tab" data-tab="components">${blocksLabel}</button>
|
|
152
|
+
<button type="button" class="psd-sidebar-tab" data-tab="outline">${layersLabel}</button>
|
|
153
|
+
`;
|
|
154
|
+
sidebar.insertBefore(tabs, sidebar.firstChild);
|
|
155
|
+
const setActive = (which) => {
|
|
156
|
+
if (sidebar.getAttribute("data-psd-active-tab") === which) return;
|
|
157
|
+
sidebar.setAttribute("data-psd-active-tab", which);
|
|
158
|
+
try {
|
|
159
|
+
sessionStorage.setItem(STORAGE_KEY, which);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
tabs.querySelectorAll("button").forEach((b) => {
|
|
163
|
+
const active = b.dataset.tab === which;
|
|
164
|
+
if (b.classList.contains("is-active") !== active) {
|
|
165
|
+
b.classList.toggle("is-active", active);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
tabs.addEventListener("click", (e) => {
|
|
170
|
+
const btn = e.target.closest("button[data-tab]");
|
|
171
|
+
if (btn) setActive(btn.dataset.tab);
|
|
172
|
+
});
|
|
173
|
+
let initial = "components";
|
|
174
|
+
try {
|
|
175
|
+
initial = sessionStorage.getItem(STORAGE_KEY) || "components";
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
setActive(initial);
|
|
179
|
+
}
|
|
180
|
+
let search = sidebar.querySelector(".psd-sidebar-search");
|
|
181
|
+
if (!search) {
|
|
182
|
+
search = document.createElement("div");
|
|
183
|
+
search.className = "psd-sidebar-search";
|
|
184
|
+
const input = document.createElement("input");
|
|
185
|
+
input.type = "search";
|
|
186
|
+
input.className = "psd-sidebar-search__input";
|
|
187
|
+
input.placeholder = searchPlaceholder;
|
|
188
|
+
input.setAttribute("aria-label", searchPlaceholder);
|
|
189
|
+
search.appendChild(input);
|
|
190
|
+
if (tabs.nextSibling) sidebar.insertBefore(search, tabs.nextSibling);
|
|
191
|
+
else sidebar.appendChild(search);
|
|
192
|
+
input.addEventListener("input", () => {
|
|
193
|
+
query = input.value || "";
|
|
194
|
+
applyFilter(sidebar);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const searchInput = search.querySelector("input");
|
|
198
|
+
if (searchInput && searchInput.value !== query) searchInput.value = query;
|
|
199
|
+
applyFilter(sidebar);
|
|
200
|
+
};
|
|
201
|
+
const safeApply = () => {
|
|
202
|
+
if (observer) observer.disconnect();
|
|
203
|
+
try {
|
|
204
|
+
apply();
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
if (observer) observer.observe(document.body, { childList: true, subtree: true });
|
|
208
|
+
};
|
|
209
|
+
safeApply();
|
|
210
|
+
let queued = false;
|
|
211
|
+
observer = new MutationObserver(() => {
|
|
212
|
+
if (queued) return;
|
|
213
|
+
queued = true;
|
|
214
|
+
requestAnimationFrame(() => {
|
|
215
|
+
queued = false;
|
|
216
|
+
safeApply();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
220
|
+
return () => {
|
|
221
|
+
if (observer) observer.disconnect();
|
|
222
|
+
};
|
|
223
|
+
}, [blocksLabel, layersLabel, searchPlaceholder]);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/BuilderTopBar.jsx
|
|
228
|
+
var import_antd = require("antd");
|
|
229
|
+
var import_icons = require("@ant-design/icons");
|
|
230
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
231
|
+
function DefaultLogo() {
|
|
232
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: 20, height: 20, viewBox: "0 0 64 64", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: "64", height: "64", rx: "14", fill: "currentColor" }) });
|
|
233
|
+
}
|
|
234
|
+
function BuilderTopBar({
|
|
235
|
+
pageKey,
|
|
236
|
+
pageTitle,
|
|
237
|
+
account,
|
|
238
|
+
livePath,
|
|
239
|
+
savedAt,
|
|
240
|
+
pending,
|
|
241
|
+
onPublish,
|
|
242
|
+
onSignOut,
|
|
243
|
+
onCreatePage,
|
|
244
|
+
homeHref,
|
|
245
|
+
branding = {},
|
|
246
|
+
extraActions,
|
|
247
|
+
LinkComponent = "a"
|
|
248
|
+
}) {
|
|
249
|
+
const brand = {
|
|
250
|
+
name: "Page Studio",
|
|
251
|
+
logo: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultLogo, {}),
|
|
252
|
+
primaryColor: "#0F766E",
|
|
253
|
+
...branding
|
|
254
|
+
};
|
|
255
|
+
const Link = LinkComponent;
|
|
256
|
+
const accountMenu = account ? {
|
|
257
|
+
items: [
|
|
258
|
+
{
|
|
259
|
+
key: "who",
|
|
260
|
+
disabled: true,
|
|
261
|
+
label: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "4px 0", minWidth: 200 }, children: [
|
|
262
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 600, color: "#0f172a" }, children: account.name }),
|
|
263
|
+
account.email && account.email !== account.name && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, color: "#94a3b8", marginTop: 2 }, children: account.email })
|
|
264
|
+
] })
|
|
265
|
+
},
|
|
266
|
+
{ type: "divider" },
|
|
267
|
+
homeHref && {
|
|
268
|
+
key: "home",
|
|
269
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.HomeOutlined, {}),
|
|
270
|
+
label: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link, { href: homeHref, children: "Admin home" })
|
|
271
|
+
},
|
|
272
|
+
onSignOut && {
|
|
273
|
+
key: "signout",
|
|
274
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.LogoutOutlined, {}),
|
|
275
|
+
label: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
276
|
+
"button",
|
|
277
|
+
{
|
|
278
|
+
type: "button",
|
|
279
|
+
onClick: onSignOut,
|
|
280
|
+
style: { all: "unset", cursor: "pointer", width: "100%", display: "block" },
|
|
281
|
+
children: "Sign out"
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
].filter(Boolean)
|
|
286
|
+
} : null;
|
|
287
|
+
const brandTheme = {
|
|
288
|
+
algorithm: import_antd.theme.defaultAlgorithm,
|
|
289
|
+
token: {
|
|
290
|
+
colorPrimary: brand.primaryColor,
|
|
291
|
+
colorInfo: brand.primaryColor
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.ConfigProvider, { theme: brandTheme, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "psd-builder-bar", style: { color: "#fff" }, children: [
|
|
295
|
+
homeHref ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
296
|
+
Link,
|
|
297
|
+
{
|
|
298
|
+
href: homeHref,
|
|
299
|
+
className: "psd-builder-bar__brand",
|
|
300
|
+
title: "Back to admin",
|
|
301
|
+
style: { color: "inherit" },
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.ArrowLeftOutlined, { style: { fontSize: 12 } }),
|
|
304
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": true, style: { color: brand.primaryColor, display: "inline-flex" }, children: brand.logo }),
|
|
305
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: brand.name })
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "psd-builder-bar__brand", children: [
|
|
309
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": true, style: { color: brand.primaryColor, display: "inline-flex" }, children: brand.logo }),
|
|
310
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: brand.name })
|
|
311
|
+
] }),
|
|
312
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "psd-builder-bar__crumbs", children: [
|
|
313
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "psd-builder-bar__current", children: pageTitle || pageKey }),
|
|
314
|
+
pageKey && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { marginLeft: 8, opacity: 0.5, fontSize: 11 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 10 }, children: pageKey }) }),
|
|
315
|
+
savedAt && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: 12, fontSize: 11, opacity: 0.55 }, children: [
|
|
316
|
+
"\xB7 Saved ",
|
|
317
|
+
new Date(savedAt).toLocaleTimeString()
|
|
318
|
+
] }),
|
|
319
|
+
pending && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { marginLeft: 12, fontSize: 11, opacity: 0.55 }, children: "\xB7 Saving\u2026" })
|
|
320
|
+
] }),
|
|
321
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Space, { size: 6, className: "psd-builder-bar__actions", children: [
|
|
322
|
+
extraActions,
|
|
323
|
+
onCreatePage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.PlusOutlined, {}), onClick: onCreatePage, children: "New page" }),
|
|
324
|
+
livePath && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Link, { href: livePath, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.EyeOutlined, {}), children: "View live" }) }),
|
|
325
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
326
|
+
import_antd.Button,
|
|
327
|
+
{
|
|
328
|
+
type: "primary",
|
|
329
|
+
size: "small",
|
|
330
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.RocketOutlined, {}),
|
|
331
|
+
onClick: onPublish,
|
|
332
|
+
loading: pending,
|
|
333
|
+
children: "Publish"
|
|
334
|
+
}
|
|
335
|
+
),
|
|
336
|
+
account && accountMenu && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Dropdown, { menu: accountMenu, placement: "bottomRight", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { type: "text", size: "small", style: { color: "#fff" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
337
|
+
import_antd.Avatar,
|
|
338
|
+
{
|
|
339
|
+
size: 22,
|
|
340
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.UserOutlined, {}),
|
|
341
|
+
style: { background: brand.primaryColor }
|
|
342
|
+
}
|
|
343
|
+
) }) })
|
|
344
|
+
] })
|
|
345
|
+
] }) });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/PageStudio.jsx
|
|
349
|
+
var import_puck = require("@puckeditor/core/puck.css");
|
|
350
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
351
|
+
var PSD_LEGACY_SIDEBAR = (0, import_core.legacySideBarPlugin)();
|
|
352
|
+
var HeaderPropsContext = (0, import_react2.createContext)(null);
|
|
353
|
+
function StableHeaderOverride() {
|
|
354
|
+
const getPuck = (0, import_core.useGetPuck)();
|
|
355
|
+
const live = (0, import_react2.useContext)(HeaderPropsContext);
|
|
356
|
+
if (!live) return null;
|
|
357
|
+
const props = {
|
|
358
|
+
pageKey: live.pageKey,
|
|
359
|
+
pageTitle: live.pageTitle,
|
|
360
|
+
account: live.account,
|
|
361
|
+
livePath: live.livePath,
|
|
362
|
+
homeHref: live.homeHref,
|
|
363
|
+
savedAt: live.savedAt,
|
|
364
|
+
pending: live.pending,
|
|
365
|
+
onPublish: () => live.handlePublish(getPuck().appState.data),
|
|
366
|
+
onSignOut: live.onSignOut,
|
|
367
|
+
onCreatePage: live.onCreatePage,
|
|
368
|
+
branding: live.branding,
|
|
369
|
+
extraActions: live.headerActions,
|
|
370
|
+
LinkComponent: live.LinkComponent
|
|
371
|
+
};
|
|
372
|
+
if (typeof live.header === "function") return live.header(props);
|
|
373
|
+
if (live.header) return live.header;
|
|
374
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BuilderTopBar, { ...props });
|
|
375
|
+
}
|
|
376
|
+
var NO_OP_ACTIONS = () => null;
|
|
377
|
+
function buildStableOverrides(hostOverrides) {
|
|
378
|
+
return { ...hostOverrides, header: StableHeaderOverride, headerActions: NO_OP_ACTIONS };
|
|
379
|
+
}
|
|
380
|
+
function setCssVars(branding) {
|
|
381
|
+
if (typeof document === "undefined" || !branding) return;
|
|
382
|
+
const root = document.documentElement.style;
|
|
383
|
+
if (branding.primaryColor) root.setProperty("--tps-primary", branding.primaryColor);
|
|
384
|
+
if (branding.accentColor) root.setProperty("--tps-accent", branding.accentColor);
|
|
385
|
+
if (branding.inkColor) root.setProperty("--tps-ink", branding.inkColor);
|
|
386
|
+
const lines = [];
|
|
387
|
+
if (branding.primaryColor) lines.push(`--tps-primary: ${branding.primaryColor};`);
|
|
388
|
+
if (branding.accentColor) lines.push(`--tps-accent: ${branding.accentColor};`);
|
|
389
|
+
if (branding.inkColor) lines.push(`--tps-ink: ${branding.inkColor};`);
|
|
390
|
+
if (!lines.length) return;
|
|
391
|
+
let tag = document.getElementById("tps-brand-vars");
|
|
392
|
+
if (!tag) {
|
|
393
|
+
tag = document.createElement("style");
|
|
394
|
+
tag.id = "tps-brand-vars";
|
|
395
|
+
document.head.appendChild(tag);
|
|
396
|
+
}
|
|
397
|
+
const next = `:root { ${lines.join(" ")} }`;
|
|
398
|
+
if (tag.textContent !== next) tag.textContent = next;
|
|
399
|
+
}
|
|
400
|
+
function PageStudio({
|
|
401
|
+
pageKey,
|
|
402
|
+
initialData,
|
|
403
|
+
pageTitle,
|
|
404
|
+
account,
|
|
405
|
+
livePath,
|
|
406
|
+
homeHref,
|
|
407
|
+
branding,
|
|
408
|
+
studio,
|
|
409
|
+
adapter = {},
|
|
410
|
+
config,
|
|
411
|
+
blockDefaults,
|
|
412
|
+
overrides = import_page_studio_blocks.defaultOverrides,
|
|
413
|
+
header,
|
|
414
|
+
headerActions,
|
|
415
|
+
onSignOut,
|
|
416
|
+
sidebarLabels,
|
|
417
|
+
LinkComponent,
|
|
418
|
+
// Forwarded to Puck. Defaults to iframe enabled — Puck v0.20 mounts the
|
|
419
|
+
// @dnd-kit context inside the canvas iframe; with the iframe disabled,
|
|
420
|
+
// strict-mode double-mount and Vite HMR can leave the canvas with no live
|
|
421
|
+
// drop targets, so dropped blocks vanish into an empty content array.
|
|
422
|
+
// Hosts that need the canvas to share the host document (e.g. to inherit
|
|
423
|
+
// global CSS without copying it across) can pass { enabled: false }.
|
|
424
|
+
iframe = { enabled: true }
|
|
425
|
+
}) {
|
|
426
|
+
const { message } = import_antd2.App.useApp();
|
|
427
|
+
const [pending, startTransition] = (0, import_react2.useTransition)();
|
|
428
|
+
const [savedAt, setSavedAt] = (0, import_react2.useState)(null);
|
|
429
|
+
const [resolvedConfig] = (0, import_react2.useState)(
|
|
430
|
+
() => config || (0, import_page_studio_blocks.createPuckConfig)({ defaults: blockDefaults })
|
|
431
|
+
);
|
|
432
|
+
const [data, setData] = (0, import_react2.useState)(
|
|
433
|
+
() => initialData && Array.isArray(initialData.content) ? (0, import_page_studio_blocks.applyConfigDefaults)((0, import_page_studio_blocks.normalizePuckData)(initialData), resolvedConfig) : null
|
|
434
|
+
);
|
|
435
|
+
const [loading, setLoading] = (0, import_react2.useState)(!data && !!adapter.loadPage);
|
|
436
|
+
setCssVars(branding);
|
|
437
|
+
(0, import_react2.useEffect)(() => {
|
|
438
|
+
if (data || !adapter.loadPage) return;
|
|
439
|
+
let cancelled = false;
|
|
440
|
+
setLoading(true);
|
|
441
|
+
adapter.loadPage(pageKey).then((loaded) => {
|
|
442
|
+
if (cancelled) return;
|
|
443
|
+
setData(
|
|
444
|
+
loaded && Array.isArray(loaded.content) ? (0, import_page_studio_blocks.applyConfigDefaults)((0, import_page_studio_blocks.normalizePuckData)(loaded), resolvedConfig) : import_page_studio_blocks.emptyPuckData
|
|
445
|
+
);
|
|
446
|
+
}).catch((err) => {
|
|
447
|
+
if (cancelled) return;
|
|
448
|
+
message.error(err?.message || "Could not load page.");
|
|
449
|
+
setData(import_page_studio_blocks.emptyPuckData);
|
|
450
|
+
}).finally(() => {
|
|
451
|
+
if (!cancelled) setLoading(false);
|
|
452
|
+
});
|
|
453
|
+
return () => {
|
|
454
|
+
cancelled = true;
|
|
455
|
+
};
|
|
456
|
+
}, [pageKey, adapter, data, message]);
|
|
457
|
+
const handlePublish = (nextData) => {
|
|
458
|
+
if (!adapter.savePage) {
|
|
459
|
+
message.error("No savePage adapter configured.");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
startTransition(async () => {
|
|
463
|
+
try {
|
|
464
|
+
await adapter.savePage(pageKey, nextData);
|
|
465
|
+
setSavedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
466
|
+
message.success("Published.");
|
|
467
|
+
} catch (err) {
|
|
468
|
+
message.error(err?.message || "Save failed.");
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
const headerLive = (0, import_react2.useMemo)(() => ({
|
|
473
|
+
pageKey,
|
|
474
|
+
pageTitle,
|
|
475
|
+
account,
|
|
476
|
+
livePath,
|
|
477
|
+
homeHref,
|
|
478
|
+
savedAt,
|
|
479
|
+
pending,
|
|
480
|
+
handlePublish,
|
|
481
|
+
onSignOut,
|
|
482
|
+
onCreatePage: adapter.onCreatePage,
|
|
483
|
+
branding,
|
|
484
|
+
headerActions,
|
|
485
|
+
LinkComponent,
|
|
486
|
+
header
|
|
487
|
+
}), [
|
|
488
|
+
pageKey,
|
|
489
|
+
pageTitle,
|
|
490
|
+
account,
|
|
491
|
+
livePath,
|
|
492
|
+
homeHref,
|
|
493
|
+
savedAt,
|
|
494
|
+
pending,
|
|
495
|
+
handlePublish,
|
|
496
|
+
onSignOut,
|
|
497
|
+
adapter.onCreatePage,
|
|
498
|
+
branding,
|
|
499
|
+
headerActions,
|
|
500
|
+
LinkComponent,
|
|
501
|
+
header
|
|
502
|
+
]);
|
|
503
|
+
const mergedOverrides = (0, import_react2.useMemo)(
|
|
504
|
+
() => buildStableOverrides(overrides),
|
|
505
|
+
[overrides]
|
|
506
|
+
);
|
|
507
|
+
if (loading || !data) {
|
|
508
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "psd-builder-page psd-builder-page--loading", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "psd-builder-loading", children: "Loading editor\u2026" }) });
|
|
509
|
+
}
|
|
510
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_page_studio_blocks.PageStudioProvider, { value: studio, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HeaderPropsContext.Provider, { value: headerLive, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "psd-builder-page", children: [
|
|
511
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
512
|
+
BuilderEnhancements,
|
|
513
|
+
{
|
|
514
|
+
blocksLabel: sidebarLabels?.blocks,
|
|
515
|
+
layersLabel: sidebarLabels?.layers,
|
|
516
|
+
searchPlaceholder: sidebarLabels?.search
|
|
517
|
+
}
|
|
518
|
+
),
|
|
519
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
520
|
+
import_core.Puck,
|
|
521
|
+
{
|
|
522
|
+
config: resolvedConfig,
|
|
523
|
+
data,
|
|
524
|
+
overrides: mergedOverrides,
|
|
525
|
+
onPublish: handlePublish,
|
|
526
|
+
iframe,
|
|
527
|
+
plugins: [PSD_LEGACY_SIDEBAR]
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
] }) }) });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/index.js
|
|
534
|
+
var import_page_studio_blocks2 = require("@techrox/page-studio-blocks");
|
|
535
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
536
|
+
0 && (module.exports = {
|
|
537
|
+
BuilderEnhancements,
|
|
538
|
+
BuilderTopBar,
|
|
539
|
+
PageStudio,
|
|
540
|
+
PageStudioProvider,
|
|
541
|
+
createPuckConfig,
|
|
542
|
+
defaultBlocks,
|
|
543
|
+
defaultCategories,
|
|
544
|
+
defaultOverrides,
|
|
545
|
+
emptyPuckData,
|
|
546
|
+
useStudio
|
|
547
|
+
});
|
|
548
|
+
//# sourceMappingURL=index.cjs.map
|