@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 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