@paulpaulstudio/strapi-render 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/README.md +95 -0
- package/dist/index.cjs +179 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +154 -0
- package/dist/index.d.ts +154 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @paulpaulstudio/strapi-render
|
|
2
|
+
|
|
3
|
+
React SDK für Strapi-Frontends mit Onpage-Editing über [cms.paulpaul.studio](https://cms.paulpaul.studio).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @paulpaulstudio/strapi-render
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer-Deps: `react@>=18`, `react-dom@>=18`.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// app/layout.tsx (Next.js App Router) oder _app.tsx (Pages Router)
|
|
17
|
+
import { StrapiEditModeProvider } from "@paulpaulstudio/strapi-render";
|
|
18
|
+
|
|
19
|
+
export default function Layout({ children }) {
|
|
20
|
+
return (
|
|
21
|
+
<html>
|
|
22
|
+
<body>
|
|
23
|
+
<StrapiEditModeProvider>
|
|
24
|
+
{children}
|
|
25
|
+
</StrapiEditModeProvider>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Der Provider liest `?__pp_edit=1&__pp_token=<jwt>` aus der URL und aktiviert
|
|
33
|
+
den Edit-Mode automatisch. Im normalen Mode rendert das SDK keinerlei
|
|
34
|
+
zusätzliches DOM.
|
|
35
|
+
|
|
36
|
+
## Komponenten
|
|
37
|
+
|
|
38
|
+
### `<StrapiText>`
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<StrapiText path="hero.title" value={page.hero.title} as="h1" />
|
|
42
|
+
<StrapiText path="hero.body" value={page.hero.body} fieldType="richText" as="div" />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `<StrapiImage>`
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<StrapiImage
|
|
49
|
+
path="hero.bg"
|
|
50
|
+
value={page.hero.bg}
|
|
51
|
+
baseUrl="https://cms.paulpaul.studio"
|
|
52
|
+
className="hero-bg"
|
|
53
|
+
/>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `<StrapiList>`
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<StrapiList path="features" value={page.features} renderItem={(f, fp) => (
|
|
60
|
+
<div>
|
|
61
|
+
<StrapiText path={`${fp}.title`} value={f.title} as="h3" />
|
|
62
|
+
<StrapiImage path={`${fp}.icon`} value={f.icon} />
|
|
63
|
+
</div>
|
|
64
|
+
)} />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `<StrapiField>` (Low-Level)
|
|
68
|
+
|
|
69
|
+
Für Custom-Layouts wo die High-Level-Komponenten nicht passen:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<StrapiField path="hero.gallery" type="media" value={page.hero.gallery}>
|
|
73
|
+
<MyCarousel images={page.hero.gallery} />
|
|
74
|
+
</StrapiField>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Edit-Mode
|
|
78
|
+
|
|
79
|
+
Im Edit-Mode (`?__pp_edit=1`):
|
|
80
|
+
- Jedes Strapi-Feld bekommt einen `data-pp-edit="<path>"`-Attribut
|
|
81
|
+
- Hover zeigt orangen Outline (P&P-Brand-Accent)
|
|
82
|
+
- Click sendet `pp:edit:click` postMessage an den Parent (cms.paulpaul.studio)
|
|
83
|
+
- Parent zeigt Editor-Popover mit dem passenden Field-Input
|
|
84
|
+
|
|
85
|
+
Im Normal-Mode: identisches HTML wie ohne SDK — kein Overhead.
|
|
86
|
+
|
|
87
|
+
## Hosting & Onpage-Editor
|
|
88
|
+
|
|
89
|
+
Onpage-Editing ist Feature des **P&P-Hostings**. Self-hosted Sites bekommen
|
|
90
|
+
den CMS-Editor, aber kein Onpage-Mode. Webstudio-Sites haben Onpage nativ
|
|
91
|
+
(eigener Webstudio-Renderer-Patch).
|
|
92
|
+
|
|
93
|
+
## Lizenz
|
|
94
|
+
|
|
95
|
+
UNLICENSED — internes Paul & Paul Studio Tooling.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/EditModeContext.tsx
|
|
7
|
+
var EditModeContext = react.createContext({ enabled: false, token: null });
|
|
8
|
+
function useStrapiEditMode() {
|
|
9
|
+
return react.useContext(EditModeContext);
|
|
10
|
+
}
|
|
11
|
+
function StrapiEditModeProvider({ children, enabled: enabledOverride }) {
|
|
12
|
+
const [urlState, setUrlState] = react.useState({ enabled: false, token: null });
|
|
13
|
+
react.useEffect(() => {
|
|
14
|
+
if (typeof window === "undefined") return;
|
|
15
|
+
if (enabledOverride !== void 0) {
|
|
16
|
+
setUrlState({ enabled: enabledOverride, token: null });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const params = new URLSearchParams(window.location.search);
|
|
21
|
+
const edit = params.get("__pp_edit") === "1";
|
|
22
|
+
const token = params.get("__pp_token");
|
|
23
|
+
setUrlState({ enabled: edit, token });
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}, [enabledOverride]);
|
|
27
|
+
react.useEffect(() => {
|
|
28
|
+
if (typeof document === "undefined") return;
|
|
29
|
+
document.documentElement.classList.toggle("pp-edit-active", urlState.enabled);
|
|
30
|
+
}, [urlState.enabled]);
|
|
31
|
+
react.useEffect(() => {
|
|
32
|
+
if (typeof window === "undefined") return;
|
|
33
|
+
if (!urlState.enabled) return;
|
|
34
|
+
if (window.parent === window) return;
|
|
35
|
+
try {
|
|
36
|
+
window.parent.postMessage(
|
|
37
|
+
{ type: "pp:edit:ready", href: window.location.href },
|
|
38
|
+
"*"
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}, [urlState.enabled]);
|
|
43
|
+
react.useEffect(() => {
|
|
44
|
+
if (typeof window === "undefined") return;
|
|
45
|
+
if (!urlState.enabled) return;
|
|
46
|
+
function onMsg(e) {
|
|
47
|
+
const d = e.data;
|
|
48
|
+
if (d && typeof d === "object" && d.type === "pp:edit:reload") {
|
|
49
|
+
window.location.reload();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
window.addEventListener("message", onMsg);
|
|
53
|
+
return () => window.removeEventListener("message", onMsg);
|
|
54
|
+
}, [urlState.enabled]);
|
|
55
|
+
const value = react.useMemo(() => urlState, [urlState]);
|
|
56
|
+
return /* @__PURE__ */ jsxRuntime.jsx(EditModeContext.Provider, { value, children });
|
|
57
|
+
}
|
|
58
|
+
var EDIT_ATTR_STYLE = {
|
|
59
|
+
outline: "1px dashed transparent",
|
|
60
|
+
outlineOffset: "2px",
|
|
61
|
+
cursor: "pointer",
|
|
62
|
+
transition: "outline-color 0.15s ease"
|
|
63
|
+
};
|
|
64
|
+
var EDIT_HOVER_STYLE = {
|
|
65
|
+
outlineColor: "#FA501E"
|
|
66
|
+
};
|
|
67
|
+
function StrapiField({ path, type, value, children, className }) {
|
|
68
|
+
const { enabled } = useStrapiEditMode();
|
|
69
|
+
const onClick = react.useCallback(
|
|
70
|
+
(e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
if (typeof window === "undefined" || window.parent === window) return;
|
|
74
|
+
const target = e.currentTarget;
|
|
75
|
+
const rect = target.getBoundingClientRect();
|
|
76
|
+
try {
|
|
77
|
+
window.parent.postMessage(
|
|
78
|
+
{
|
|
79
|
+
type: "pp:edit:click",
|
|
80
|
+
path,
|
|
81
|
+
fieldType: type,
|
|
82
|
+
rect: {
|
|
83
|
+
top: rect.top,
|
|
84
|
+
left: rect.left,
|
|
85
|
+
width: rect.width,
|
|
86
|
+
height: rect.height
|
|
87
|
+
},
|
|
88
|
+
currentValue: value
|
|
89
|
+
},
|
|
90
|
+
"*"
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[path, type, value]
|
|
96
|
+
);
|
|
97
|
+
if (!enabled) {
|
|
98
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
99
|
+
}
|
|
100
|
+
const arr = react.Children.toArray(children);
|
|
101
|
+
if (arr.length === 1 && react.isValidElement(arr[0])) {
|
|
102
|
+
const el = arr[0];
|
|
103
|
+
const existing = el.props.style ?? {};
|
|
104
|
+
const existingClass = el.props.className ?? "";
|
|
105
|
+
return react.cloneElement(el, {
|
|
106
|
+
"data-pp-edit": path,
|
|
107
|
+
"data-pp-type": type,
|
|
108
|
+
onClick,
|
|
109
|
+
onMouseEnter: (e) => {
|
|
110
|
+
e.currentTarget.style.outlineColor = EDIT_HOVER_STYLE.outlineColor;
|
|
111
|
+
},
|
|
112
|
+
onMouseLeave: (e) => {
|
|
113
|
+
e.currentTarget.style.outlineColor = "transparent";
|
|
114
|
+
},
|
|
115
|
+
style: { ...EDIT_ATTR_STYLE, ...existing },
|
|
116
|
+
className: [existingClass, className, "pp-edit-target"].filter(Boolean).join(" ")
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
120
|
+
"span",
|
|
121
|
+
{
|
|
122
|
+
"data-pp-edit": path,
|
|
123
|
+
"data-pp-type": type,
|
|
124
|
+
onClick,
|
|
125
|
+
onMouseEnter: (e) => {
|
|
126
|
+
e.currentTarget.style.outlineColor = EDIT_HOVER_STYLE.outlineColor;
|
|
127
|
+
},
|
|
128
|
+
onMouseLeave: (e) => {
|
|
129
|
+
e.currentTarget.style.outlineColor = "transparent";
|
|
130
|
+
},
|
|
131
|
+
style: EDIT_ATTR_STYLE,
|
|
132
|
+
className: ["pp-edit-target", className].filter(Boolean).join(" "),
|
|
133
|
+
children
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
function StrapiText({ path, value, fieldType = "text", as = "span", className, children }) {
|
|
138
|
+
const content = children ?? (value === null || value === void 0 ? "" : String(value));
|
|
139
|
+
const el = react.createElement(as, { className }, content);
|
|
140
|
+
return /* @__PURE__ */ jsxRuntime.jsx(StrapiField, { path, type: fieldType, value, children: el });
|
|
141
|
+
}
|
|
142
|
+
function resolveUrl(value, baseUrl) {
|
|
143
|
+
const u = value.url;
|
|
144
|
+
if (u.startsWith("http")) return u;
|
|
145
|
+
if (baseUrl) return `${baseUrl.replace(/\/$/, "")}${u.startsWith("/") ? "" : "/"}${u}`;
|
|
146
|
+
return u;
|
|
147
|
+
}
|
|
148
|
+
function StrapiImage({ path, value, baseUrl, alt, className, style, loading = "lazy", fallback }) {
|
|
149
|
+
if (!value) {
|
|
150
|
+
return /* @__PURE__ */ jsxRuntime.jsx(StrapiField, { path, type: "media", value: null, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className, style, children: fallback ?? "" }) });
|
|
151
|
+
}
|
|
152
|
+
return /* @__PURE__ */ jsxRuntime.jsx(StrapiField, { path, type: "media", value, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
153
|
+
"img",
|
|
154
|
+
{
|
|
155
|
+
src: resolveUrl(value, baseUrl),
|
|
156
|
+
alt: alt ?? value.alternativeText ?? value.name ?? "",
|
|
157
|
+
width: value.width,
|
|
158
|
+
height: value.height,
|
|
159
|
+
loading,
|
|
160
|
+
className,
|
|
161
|
+
style
|
|
162
|
+
}
|
|
163
|
+
) });
|
|
164
|
+
}
|
|
165
|
+
function StrapiList({ path, value, renderItem, fallback }) {
|
|
166
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
167
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback ?? null });
|
|
168
|
+
}
|
|
169
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: value.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderItem(item, `${path}.${i}`, i) }, i)) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
exports.StrapiEditModeProvider = StrapiEditModeProvider;
|
|
173
|
+
exports.StrapiField = StrapiField;
|
|
174
|
+
exports.StrapiImage = StrapiImage;
|
|
175
|
+
exports.StrapiList = StrapiList;
|
|
176
|
+
exports.StrapiText = StrapiText;
|
|
177
|
+
exports.useStrapiEditMode = useStrapiEditMode;
|
|
178
|
+
//# sourceMappingURL=index.cjs.map
|
|
179
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/EditModeContext.tsx","../src/StrapiField.tsx","../src/StrapiText.tsx","../src/StrapiImage.tsx","../src/StrapiList.tsx"],"names":["createContext","useContext","useState","useEffect","useMemo","jsx","useCallback","Fragment","Children","isValidElement","cloneElement","createElement"],"mappings":"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","file":"index.cjs","sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState } from \"react\";\n\ninterface EditModeState {\n enabled: boolean;\n token: string | null;\n}\n\nconst EditModeContext = createContext<EditModeState>({ enabled: false, token: null });\n\nexport function useStrapiEditMode(): EditModeState {\n return useContext(EditModeContext);\n}\n\ninterface ProviderProps {\n children: React.ReactNode;\n /**\n * Override automatic URL-based detection. If `true`/`false` ist gesetzt,\n * wird der URL-Parameter ignoriert.\n */\n enabled?: boolean;\n}\n\n/**\n * Provider, der den Edit-Mode aus `?__pp_edit=1&__pp_token=<jwt>` der URL\n * liest und an Kinder via Context weitergibt. Token wird sicherheitshalber\n * NICHT ans CSR exponiert über Props oder DOM — er bleibt nur im Context.\n *\n * Im Edit-Mode wird ausserdem eine kleine globale CSS-Klasse `pp-edit-active`\n * am `<html>` gesetzt, sodass Customer-Sites optional CSS-Hooks setzen können.\n */\nexport function StrapiEditModeProvider({ children, enabled: enabledOverride }: ProviderProps) {\n const [urlState, setUrlState] = useState<EditModeState>({ enabled: false, token: null });\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (enabledOverride !== undefined) {\n setUrlState({ enabled: enabledOverride, token: null });\n return;\n }\n try {\n const params = new URLSearchParams(window.location.search);\n const edit = params.get(\"__pp_edit\") === \"1\";\n const token = params.get(\"__pp_token\");\n setUrlState({ enabled: edit, token });\n } catch {\n // ignore\n }\n }, [enabledOverride]);\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n document.documentElement.classList.toggle(\"pp-edit-active\", urlState.enabled);\n }, [urlState.enabled]);\n\n // postMessage \"ready\" wenn Edit-Mode aktiv ist und Iframe geladen\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (!urlState.enabled) return;\n if (window.parent === window) return; // nicht im Iframe\n try {\n window.parent.postMessage(\n { type: \"pp:edit:ready\", href: window.location.href },\n \"*\",\n );\n } catch {\n // ignore\n }\n }, [urlState.enabled]);\n\n // Reload-Listener vom Parent (nach Save)\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (!urlState.enabled) return;\n function onMsg(e: MessageEvent) {\n const d = e.data;\n if (d && typeof d === \"object\" && d.type === \"pp:edit:reload\") {\n // Soft-Reload — re-loaded mit gleichem Suchparameter\n window.location.reload();\n }\n }\n window.addEventListener(\"message\", onMsg);\n return () => window.removeEventListener(\"message\", onMsg);\n }, [urlState.enabled]);\n\n const value = useMemo(() => urlState, [urlState]);\n return <EditModeContext.Provider value={value}>{children}</EditModeContext.Provider>;\n}\n","\"use client\";\n\nimport { Children, cloneElement, isValidElement, useCallback, type ReactElement, type ReactNode } from \"react\";\nimport { useStrapiEditMode } from \"./EditModeContext\";\nimport type { StrapiFieldType } from \"./types\";\n\ninterface StrapiFieldProps {\n /** Dot-notation path im Strapi-Document, z.B. \"hero.title\" oder \"features.0.image\" */\n path: string;\n /** Strapi field type — bestimmt welche Edit-UI im Parent geöffnet wird */\n type: StrapiFieldType;\n /** Aktueller Wert. Wird beim Click-Event mitgesendet, damit der Editor initial befüllt ist */\n value?: unknown;\n /** Inhalt (das echte gerenderte HTML). Wird im Edit-Mode mit data-attrs + Hover/Click annotiert */\n children: ReactNode;\n /** Optional: zusätzliche CSS-Klassen die im Edit-Mode angehängt werden */\n className?: string;\n}\n\nconst EDIT_ATTR_STYLE = {\n outline: \"1px dashed transparent\",\n outlineOffset: \"2px\",\n cursor: \"pointer\",\n transition: \"outline-color 0.15s ease\",\n} as const;\n\nconst EDIT_HOVER_STYLE = {\n outlineColor: \"#FA501E\",\n};\n\n/**\n * Universeller Wrapper für editierbare Strapi-Felder.\n *\n * Im normalen Mode rendert er nur die `children` — keine zusätzlichen DOM-\n * Nodes, kein Overhead. Im Edit-Mode wird das einzige Top-Level-Child mit\n * `data-pp-edit=\"<path>\"` annotiert + Click-Handler bekommt.\n *\n * Falls children mehrere Top-Level-Knoten haben, wird automatisch ein\n * `<span>` als Wrapper eingefügt.\n */\nexport function StrapiField({ path, type, value, children, className }: StrapiFieldProps) {\n const { enabled } = useStrapiEditMode();\n\n const onClick = useCallback(\n (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (typeof window === \"undefined\" || window.parent === window) return;\n const target = e.currentTarget as HTMLElement;\n const rect = target.getBoundingClientRect();\n try {\n window.parent.postMessage(\n {\n type: \"pp:edit:click\",\n path,\n fieldType: type,\n rect: {\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n },\n currentValue: value,\n },\n \"*\",\n );\n } catch {\n // ignore\n }\n },\n [path, type, value],\n );\n\n if (!enabled) {\n return <>{children}</>;\n }\n\n // Wenn genau ein React-Element drin ist, klonen wir es und reichen die Edit-Attrs durch.\n const arr = Children.toArray(children);\n if (arr.length === 1 && isValidElement(arr[0])) {\n const el = arr[0] as ReactElement<Record<string, unknown>>;\n const existing = (el.props.style as React.CSSProperties) ?? {};\n const existingClass = (el.props.className as string) ?? \"\";\n return cloneElement(el, {\n \"data-pp-edit\": path,\n \"data-pp-type\": type,\n onClick,\n onMouseEnter: (e: React.MouseEvent) => {\n (e.currentTarget as HTMLElement).style.outlineColor = EDIT_HOVER_STYLE.outlineColor;\n },\n onMouseLeave: (e: React.MouseEvent) => {\n (e.currentTarget as HTMLElement).style.outlineColor = \"transparent\";\n },\n style: { ...EDIT_ATTR_STYLE, ...existing },\n className: [existingClass, className, \"pp-edit-target\"].filter(Boolean).join(\" \"),\n });\n }\n\n // Mehrere Kinder: in einen span wrappen\n return (\n <span\n data-pp-edit={path}\n data-pp-type={type}\n onClick={onClick}\n onMouseEnter={(e) => { (e.currentTarget).style.outlineColor = EDIT_HOVER_STYLE.outlineColor; }}\n onMouseLeave={(e) => { (e.currentTarget).style.outlineColor = \"transparent\"; }}\n style={EDIT_ATTR_STYLE}\n className={[\"pp-edit-target\", className].filter(Boolean).join(\" \")}\n >\n {children}\n </span>\n );\n}\n","\"use client\";\n\nimport { createElement, type ElementType, type ReactNode } from \"react\";\nimport { StrapiField } from \"./StrapiField\";\nimport type { StrapiFieldType } from \"./types\";\n\ninterface StrapiTextProps {\n path: string;\n value: string | number | null | undefined;\n /** Welcher Strapi-Feldtyp ist das? Default \"text\". Für mehrzeilige Felder \"textarea\", für Markdown \"richText\". */\n fieldType?: Extract<StrapiFieldType, \"text\" | \"textarea\" | \"richText\" | \"email\" | \"number\">;\n /** HTML-Tag um den Wert zu rendern. Default \"span\". */\n as?: ElementType;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * Inline-Text wrapper. Im normalen Mode rendert es einfach `value` im\n * gewählten Tag — identisch zu einem rohen `<h1>{value}</h1>`. Im Edit-Mode\n * wird ein Click-Handler aktiviert.\n *\n * Wenn `children` mitgegeben sind, werden diese statt `value` als Inhalt\n * verwendet — nützlich für custom Formatierung. Der Click-Wert bleibt\n * `value`.\n */\nexport function StrapiText({ path, value, fieldType = \"text\", as = \"span\", className, children }: StrapiTextProps) {\n const content = children ?? (value === null || value === undefined ? \"\" : String(value));\n const el = createElement(as, { className }, content);\n return (\n <StrapiField path={path} type={fieldType} value={value}>\n {el}\n </StrapiField>\n );\n}\n","\"use client\";\n\nimport { StrapiField } from \"./StrapiField\";\nimport type { StrapiMediaValue } from \"./types\";\n\ninterface StrapiImageProps {\n path: string;\n value: StrapiMediaValue | null | undefined;\n /** Optional: Strapi-Basis-URL für relative URLs (z.B. \"https://cms.paulpaul.studio\") */\n baseUrl?: string;\n /** Override alt-Text (default: alternativeText vom Strapi-Objekt, dann name) */\n alt?: string;\n className?: string;\n style?: React.CSSProperties;\n loading?: \"lazy\" | \"eager\";\n /** Fallback wenn value null/undefined ist */\n fallback?: React.ReactNode;\n}\n\nfunction resolveUrl(value: StrapiMediaValue, baseUrl?: string): string {\n const u = value.url;\n if (u.startsWith(\"http\")) return u;\n if (baseUrl) return `${baseUrl.replace(/\\/$/, \"\")}${u.startsWith(\"/\") ? \"\" : \"/\"}${u}`;\n return u;\n}\n\n/**\n * Wrapper für ein einzelnes Strapi-Media-Feld. Rendert `<img>` mit der\n * abgeleiteten URL. Im Edit-Mode klickbar — der Editor öffnet einen\n * MediaPicker.\n *\n * Wenn `value` null ist, wird der `fallback` gerendert (oder ein leeres\n * `<span>`).\n */\nexport function StrapiImage({ path, value, baseUrl, alt, className, style, loading = \"lazy\", fallback }: StrapiImageProps) {\n if (!value) {\n return (\n <StrapiField path={path} type=\"media\" value={null}>\n <span className={className} style={style}>{fallback ?? \"\"}</span>\n </StrapiField>\n );\n }\n return (\n <StrapiField path={path} type=\"media\" value={value}>\n <img\n src={resolveUrl(value, baseUrl)}\n alt={alt ?? value.alternativeText ?? value.name ?? \"\"}\n width={value.width}\n height={value.height}\n loading={loading}\n className={className}\n style={style}\n />\n </StrapiField>\n );\n}\n","\"use client\";\n\nimport { Fragment, type ReactNode } from \"react\";\n\ninterface StrapiListProps<T> {\n /** Pfad zum Array-Feld, z.B. \"features\" */\n path: string;\n value: T[] | null | undefined;\n /**\n * Render-Funktion pro Item. Bekommt einen `itemPath` als zweites Argument,\n * der für nested-StrapiText/Image-Calls als path-Prefix dient.\n */\n renderItem: (item: T, itemPath: string, index: number) => ReactNode;\n /** Optional: Fallback wenn value null/leer ist */\n fallback?: ReactNode;\n}\n\n/**\n * Iteriert über ein Strapi-Repeatable-Field oder eine Relation-Liste. Generiert\n * pro Item den vollständigen Edit-Pfad (z.B. \"features.0\", \"features.1\"), den\n * der Caller in nested StrapiText/Image weitergibt.\n *\n * Beispiel:\n * <StrapiList path=\"features\" value={page.features} renderItem={(f, fp) => (\n * <div>\n * <StrapiText path={`${fp}.title`} value={f.title} as=\"h3\" />\n * <StrapiImage path={`${fp}.icon`} value={f.icon} />\n * </div>\n * )} />\n */\nexport function StrapiList<T>({ path, value, renderItem, fallback }: StrapiListProps<T>) {\n if (!Array.isArray(value) || value.length === 0) {\n return <>{fallback ?? null}</>;\n }\n return (\n <>\n {value.map((item, i) => (\n <Fragment key={i}>{renderItem(item, `${path}.${i}`, i)}</Fragment>\n ))}\n </>\n );\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode, ElementType } from 'react';
|
|
3
|
+
|
|
4
|
+
interface EditModeState {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
token: string | null;
|
|
7
|
+
}
|
|
8
|
+
declare function useStrapiEditMode(): EditModeState;
|
|
9
|
+
interface ProviderProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Override automatic URL-based detection. If `true`/`false` ist gesetzt,
|
|
13
|
+
* wird der URL-Parameter ignoriert.
|
|
14
|
+
*/
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Provider, der den Edit-Mode aus `?__pp_edit=1&__pp_token=<jwt>` der URL
|
|
19
|
+
* liest und an Kinder via Context weitergibt. Token wird sicherheitshalber
|
|
20
|
+
* NICHT ans CSR exponiert über Props oder DOM — er bleibt nur im Context.
|
|
21
|
+
*
|
|
22
|
+
* Im Edit-Mode wird ausserdem eine kleine globale CSS-Klasse `pp-edit-active`
|
|
23
|
+
* am `<html>` gesetzt, sodass Customer-Sites optional CSS-Hooks setzen können.
|
|
24
|
+
*/
|
|
25
|
+
declare function StrapiEditModeProvider({ children, enabled: enabledOverride }: ProviderProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
|
|
27
|
+
type StrapiFieldType = "text" | "textarea" | "richText" | "number" | "checkbox" | "date" | "email" | "media" | "select" | "relation" | "group" | "array" | "blocks" | "json" | "unknown";
|
|
28
|
+
interface StrapiMediaValue {
|
|
29
|
+
id?: number;
|
|
30
|
+
documentId?: string;
|
|
31
|
+
url: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
alternativeText?: string | null;
|
|
34
|
+
mime?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
}
|
|
38
|
+
interface StrapiEditReadyMessage {
|
|
39
|
+
type: "pp:edit:ready";
|
|
40
|
+
href: string;
|
|
41
|
+
}
|
|
42
|
+
interface StrapiEditClickMessage {
|
|
43
|
+
type: "pp:edit:click";
|
|
44
|
+
path: string;
|
|
45
|
+
fieldType: StrapiFieldType;
|
|
46
|
+
rect: {
|
|
47
|
+
top: number;
|
|
48
|
+
left: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
};
|
|
52
|
+
currentValue: unknown;
|
|
53
|
+
}
|
|
54
|
+
interface StrapiEditReloadMessage {
|
|
55
|
+
type: "pp:edit:reload";
|
|
56
|
+
}
|
|
57
|
+
type StrapiEditMessage = StrapiEditReadyMessage | StrapiEditClickMessage | StrapiEditReloadMessage;
|
|
58
|
+
|
|
59
|
+
interface StrapiFieldProps {
|
|
60
|
+
/** Dot-notation path im Strapi-Document, z.B. "hero.title" oder "features.0.image" */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Strapi field type — bestimmt welche Edit-UI im Parent geöffnet wird */
|
|
63
|
+
type: StrapiFieldType;
|
|
64
|
+
/** Aktueller Wert. Wird beim Click-Event mitgesendet, damit der Editor initial befüllt ist */
|
|
65
|
+
value?: unknown;
|
|
66
|
+
/** Inhalt (das echte gerenderte HTML). Wird im Edit-Mode mit data-attrs + Hover/Click annotiert */
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
/** Optional: zusätzliche CSS-Klassen die im Edit-Mode angehängt werden */
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Universeller Wrapper für editierbare Strapi-Felder.
|
|
73
|
+
*
|
|
74
|
+
* Im normalen Mode rendert er nur die `children` — keine zusätzlichen DOM-
|
|
75
|
+
* Nodes, kein Overhead. Im Edit-Mode wird das einzige Top-Level-Child mit
|
|
76
|
+
* `data-pp-edit="<path>"` annotiert + Click-Handler bekommt.
|
|
77
|
+
*
|
|
78
|
+
* Falls children mehrere Top-Level-Knoten haben, wird automatisch ein
|
|
79
|
+
* `<span>` als Wrapper eingefügt.
|
|
80
|
+
*/
|
|
81
|
+
declare function StrapiField({ path, type, value, children, className }: StrapiFieldProps): react_jsx_runtime.JSX.Element;
|
|
82
|
+
|
|
83
|
+
interface StrapiTextProps {
|
|
84
|
+
path: string;
|
|
85
|
+
value: string | number | null | undefined;
|
|
86
|
+
/** Welcher Strapi-Feldtyp ist das? Default "text". Für mehrzeilige Felder "textarea", für Markdown "richText". */
|
|
87
|
+
fieldType?: Extract<StrapiFieldType, "text" | "textarea" | "richText" | "email" | "number">;
|
|
88
|
+
/** HTML-Tag um den Wert zu rendern. Default "span". */
|
|
89
|
+
as?: ElementType;
|
|
90
|
+
className?: string;
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Inline-Text wrapper. Im normalen Mode rendert es einfach `value` im
|
|
95
|
+
* gewählten Tag — identisch zu einem rohen `<h1>{value}</h1>`. Im Edit-Mode
|
|
96
|
+
* wird ein Click-Handler aktiviert.
|
|
97
|
+
*
|
|
98
|
+
* Wenn `children` mitgegeben sind, werden diese statt `value` als Inhalt
|
|
99
|
+
* verwendet — nützlich für custom Formatierung. Der Click-Wert bleibt
|
|
100
|
+
* `value`.
|
|
101
|
+
*/
|
|
102
|
+
declare function StrapiText({ path, value, fieldType, as, className, children }: StrapiTextProps): react_jsx_runtime.JSX.Element;
|
|
103
|
+
|
|
104
|
+
interface StrapiImageProps {
|
|
105
|
+
path: string;
|
|
106
|
+
value: StrapiMediaValue | null | undefined;
|
|
107
|
+
/** Optional: Strapi-Basis-URL für relative URLs (z.B. "https://cms.paulpaul.studio") */
|
|
108
|
+
baseUrl?: string;
|
|
109
|
+
/** Override alt-Text (default: alternativeText vom Strapi-Objekt, dann name) */
|
|
110
|
+
alt?: string;
|
|
111
|
+
className?: string;
|
|
112
|
+
style?: React.CSSProperties;
|
|
113
|
+
loading?: "lazy" | "eager";
|
|
114
|
+
/** Fallback wenn value null/undefined ist */
|
|
115
|
+
fallback?: React.ReactNode;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Wrapper für ein einzelnes Strapi-Media-Feld. Rendert `<img>` mit der
|
|
119
|
+
* abgeleiteten URL. Im Edit-Mode klickbar — der Editor öffnet einen
|
|
120
|
+
* MediaPicker.
|
|
121
|
+
*
|
|
122
|
+
* Wenn `value` null ist, wird der `fallback` gerendert (oder ein leeres
|
|
123
|
+
* `<span>`).
|
|
124
|
+
*/
|
|
125
|
+
declare function StrapiImage({ path, value, baseUrl, alt, className, style, loading, fallback }: StrapiImageProps): react_jsx_runtime.JSX.Element;
|
|
126
|
+
|
|
127
|
+
interface StrapiListProps<T> {
|
|
128
|
+
/** Pfad zum Array-Feld, z.B. "features" */
|
|
129
|
+
path: string;
|
|
130
|
+
value: T[] | null | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Render-Funktion pro Item. Bekommt einen `itemPath` als zweites Argument,
|
|
133
|
+
* der für nested-StrapiText/Image-Calls als path-Prefix dient.
|
|
134
|
+
*/
|
|
135
|
+
renderItem: (item: T, itemPath: string, index: number) => ReactNode;
|
|
136
|
+
/** Optional: Fallback wenn value null/leer ist */
|
|
137
|
+
fallback?: ReactNode;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Iteriert über ein Strapi-Repeatable-Field oder eine Relation-Liste. Generiert
|
|
141
|
+
* pro Item den vollständigen Edit-Pfad (z.B. "features.0", "features.1"), den
|
|
142
|
+
* der Caller in nested StrapiText/Image weitergibt.
|
|
143
|
+
*
|
|
144
|
+
* Beispiel:
|
|
145
|
+
* <StrapiList path="features" value={page.features} renderItem={(f, fp) => (
|
|
146
|
+
* <div>
|
|
147
|
+
* <StrapiText path={`${fp}.title`} value={f.title} as="h3" />
|
|
148
|
+
* <StrapiImage path={`${fp}.icon`} value={f.icon} />
|
|
149
|
+
* </div>
|
|
150
|
+
* )} />
|
|
151
|
+
*/
|
|
152
|
+
declare function StrapiList<T>({ path, value, renderItem, fallback }: StrapiListProps<T>): react_jsx_runtime.JSX.Element;
|
|
153
|
+
|
|
154
|
+
export { type StrapiEditClickMessage, type StrapiEditMessage, StrapiEditModeProvider, StrapiField, type StrapiFieldType, StrapiImage, StrapiList, type StrapiMediaValue, StrapiText, useStrapiEditMode };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode, ElementType } from 'react';
|
|
3
|
+
|
|
4
|
+
interface EditModeState {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
token: string | null;
|
|
7
|
+
}
|
|
8
|
+
declare function useStrapiEditMode(): EditModeState;
|
|
9
|
+
interface ProviderProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Override automatic URL-based detection. If `true`/`false` ist gesetzt,
|
|
13
|
+
* wird der URL-Parameter ignoriert.
|
|
14
|
+
*/
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Provider, der den Edit-Mode aus `?__pp_edit=1&__pp_token=<jwt>` der URL
|
|
19
|
+
* liest und an Kinder via Context weitergibt. Token wird sicherheitshalber
|
|
20
|
+
* NICHT ans CSR exponiert über Props oder DOM — er bleibt nur im Context.
|
|
21
|
+
*
|
|
22
|
+
* Im Edit-Mode wird ausserdem eine kleine globale CSS-Klasse `pp-edit-active`
|
|
23
|
+
* am `<html>` gesetzt, sodass Customer-Sites optional CSS-Hooks setzen können.
|
|
24
|
+
*/
|
|
25
|
+
declare function StrapiEditModeProvider({ children, enabled: enabledOverride }: ProviderProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
|
|
27
|
+
type StrapiFieldType = "text" | "textarea" | "richText" | "number" | "checkbox" | "date" | "email" | "media" | "select" | "relation" | "group" | "array" | "blocks" | "json" | "unknown";
|
|
28
|
+
interface StrapiMediaValue {
|
|
29
|
+
id?: number;
|
|
30
|
+
documentId?: string;
|
|
31
|
+
url: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
alternativeText?: string | null;
|
|
34
|
+
mime?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
}
|
|
38
|
+
interface StrapiEditReadyMessage {
|
|
39
|
+
type: "pp:edit:ready";
|
|
40
|
+
href: string;
|
|
41
|
+
}
|
|
42
|
+
interface StrapiEditClickMessage {
|
|
43
|
+
type: "pp:edit:click";
|
|
44
|
+
path: string;
|
|
45
|
+
fieldType: StrapiFieldType;
|
|
46
|
+
rect: {
|
|
47
|
+
top: number;
|
|
48
|
+
left: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
};
|
|
52
|
+
currentValue: unknown;
|
|
53
|
+
}
|
|
54
|
+
interface StrapiEditReloadMessage {
|
|
55
|
+
type: "pp:edit:reload";
|
|
56
|
+
}
|
|
57
|
+
type StrapiEditMessage = StrapiEditReadyMessage | StrapiEditClickMessage | StrapiEditReloadMessage;
|
|
58
|
+
|
|
59
|
+
interface StrapiFieldProps {
|
|
60
|
+
/** Dot-notation path im Strapi-Document, z.B. "hero.title" oder "features.0.image" */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Strapi field type — bestimmt welche Edit-UI im Parent geöffnet wird */
|
|
63
|
+
type: StrapiFieldType;
|
|
64
|
+
/** Aktueller Wert. Wird beim Click-Event mitgesendet, damit der Editor initial befüllt ist */
|
|
65
|
+
value?: unknown;
|
|
66
|
+
/** Inhalt (das echte gerenderte HTML). Wird im Edit-Mode mit data-attrs + Hover/Click annotiert */
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
/** Optional: zusätzliche CSS-Klassen die im Edit-Mode angehängt werden */
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Universeller Wrapper für editierbare Strapi-Felder.
|
|
73
|
+
*
|
|
74
|
+
* Im normalen Mode rendert er nur die `children` — keine zusätzlichen DOM-
|
|
75
|
+
* Nodes, kein Overhead. Im Edit-Mode wird das einzige Top-Level-Child mit
|
|
76
|
+
* `data-pp-edit="<path>"` annotiert + Click-Handler bekommt.
|
|
77
|
+
*
|
|
78
|
+
* Falls children mehrere Top-Level-Knoten haben, wird automatisch ein
|
|
79
|
+
* `<span>` als Wrapper eingefügt.
|
|
80
|
+
*/
|
|
81
|
+
declare function StrapiField({ path, type, value, children, className }: StrapiFieldProps): react_jsx_runtime.JSX.Element;
|
|
82
|
+
|
|
83
|
+
interface StrapiTextProps {
|
|
84
|
+
path: string;
|
|
85
|
+
value: string | number | null | undefined;
|
|
86
|
+
/** Welcher Strapi-Feldtyp ist das? Default "text". Für mehrzeilige Felder "textarea", für Markdown "richText". */
|
|
87
|
+
fieldType?: Extract<StrapiFieldType, "text" | "textarea" | "richText" | "email" | "number">;
|
|
88
|
+
/** HTML-Tag um den Wert zu rendern. Default "span". */
|
|
89
|
+
as?: ElementType;
|
|
90
|
+
className?: string;
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Inline-Text wrapper. Im normalen Mode rendert es einfach `value` im
|
|
95
|
+
* gewählten Tag — identisch zu einem rohen `<h1>{value}</h1>`. Im Edit-Mode
|
|
96
|
+
* wird ein Click-Handler aktiviert.
|
|
97
|
+
*
|
|
98
|
+
* Wenn `children` mitgegeben sind, werden diese statt `value` als Inhalt
|
|
99
|
+
* verwendet — nützlich für custom Formatierung. Der Click-Wert bleibt
|
|
100
|
+
* `value`.
|
|
101
|
+
*/
|
|
102
|
+
declare function StrapiText({ path, value, fieldType, as, className, children }: StrapiTextProps): react_jsx_runtime.JSX.Element;
|
|
103
|
+
|
|
104
|
+
interface StrapiImageProps {
|
|
105
|
+
path: string;
|
|
106
|
+
value: StrapiMediaValue | null | undefined;
|
|
107
|
+
/** Optional: Strapi-Basis-URL für relative URLs (z.B. "https://cms.paulpaul.studio") */
|
|
108
|
+
baseUrl?: string;
|
|
109
|
+
/** Override alt-Text (default: alternativeText vom Strapi-Objekt, dann name) */
|
|
110
|
+
alt?: string;
|
|
111
|
+
className?: string;
|
|
112
|
+
style?: React.CSSProperties;
|
|
113
|
+
loading?: "lazy" | "eager";
|
|
114
|
+
/** Fallback wenn value null/undefined ist */
|
|
115
|
+
fallback?: React.ReactNode;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Wrapper für ein einzelnes Strapi-Media-Feld. Rendert `<img>` mit der
|
|
119
|
+
* abgeleiteten URL. Im Edit-Mode klickbar — der Editor öffnet einen
|
|
120
|
+
* MediaPicker.
|
|
121
|
+
*
|
|
122
|
+
* Wenn `value` null ist, wird der `fallback` gerendert (oder ein leeres
|
|
123
|
+
* `<span>`).
|
|
124
|
+
*/
|
|
125
|
+
declare function StrapiImage({ path, value, baseUrl, alt, className, style, loading, fallback }: StrapiImageProps): react_jsx_runtime.JSX.Element;
|
|
126
|
+
|
|
127
|
+
interface StrapiListProps<T> {
|
|
128
|
+
/** Pfad zum Array-Feld, z.B. "features" */
|
|
129
|
+
path: string;
|
|
130
|
+
value: T[] | null | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Render-Funktion pro Item. Bekommt einen `itemPath` als zweites Argument,
|
|
133
|
+
* der für nested-StrapiText/Image-Calls als path-Prefix dient.
|
|
134
|
+
*/
|
|
135
|
+
renderItem: (item: T, itemPath: string, index: number) => ReactNode;
|
|
136
|
+
/** Optional: Fallback wenn value null/leer ist */
|
|
137
|
+
fallback?: ReactNode;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Iteriert über ein Strapi-Repeatable-Field oder eine Relation-Liste. Generiert
|
|
141
|
+
* pro Item den vollständigen Edit-Pfad (z.B. "features.0", "features.1"), den
|
|
142
|
+
* der Caller in nested StrapiText/Image weitergibt.
|
|
143
|
+
*
|
|
144
|
+
* Beispiel:
|
|
145
|
+
* <StrapiList path="features" value={page.features} renderItem={(f, fp) => (
|
|
146
|
+
* <div>
|
|
147
|
+
* <StrapiText path={`${fp}.title`} value={f.title} as="h3" />
|
|
148
|
+
* <StrapiImage path={`${fp}.icon`} value={f.icon} />
|
|
149
|
+
* </div>
|
|
150
|
+
* )} />
|
|
151
|
+
*/
|
|
152
|
+
declare function StrapiList<T>({ path, value, renderItem, fallback }: StrapiListProps<T>): react_jsx_runtime.JSX.Element;
|
|
153
|
+
|
|
154
|
+
export { type StrapiEditClickMessage, type StrapiEditMessage, StrapiEditModeProvider, StrapiField, type StrapiFieldType, StrapiImage, StrapiList, type StrapiMediaValue, StrapiText, useStrapiEditMode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, useMemo, useCallback, Children, isValidElement, cloneElement, createElement, Fragment as Fragment$1 } from 'react';
|
|
2
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
var EditModeContext = createContext({ enabled: false, token: null });
|
|
5
|
+
function useStrapiEditMode() {
|
|
6
|
+
return useContext(EditModeContext);
|
|
7
|
+
}
|
|
8
|
+
function StrapiEditModeProvider({ children, enabled: enabledOverride }) {
|
|
9
|
+
const [urlState, setUrlState] = useState({ enabled: false, token: null });
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof window === "undefined") return;
|
|
12
|
+
if (enabledOverride !== void 0) {
|
|
13
|
+
setUrlState({ enabled: enabledOverride, token: null });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const params = new URLSearchParams(window.location.search);
|
|
18
|
+
const edit = params.get("__pp_edit") === "1";
|
|
19
|
+
const token = params.get("__pp_token");
|
|
20
|
+
setUrlState({ enabled: edit, token });
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}, [enabledOverride]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (typeof document === "undefined") return;
|
|
26
|
+
document.documentElement.classList.toggle("pp-edit-active", urlState.enabled);
|
|
27
|
+
}, [urlState.enabled]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (typeof window === "undefined") return;
|
|
30
|
+
if (!urlState.enabled) return;
|
|
31
|
+
if (window.parent === window) return;
|
|
32
|
+
try {
|
|
33
|
+
window.parent.postMessage(
|
|
34
|
+
{ type: "pp:edit:ready", href: window.location.href },
|
|
35
|
+
"*"
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}, [urlState.enabled]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (typeof window === "undefined") return;
|
|
42
|
+
if (!urlState.enabled) return;
|
|
43
|
+
function onMsg(e) {
|
|
44
|
+
const d = e.data;
|
|
45
|
+
if (d && typeof d === "object" && d.type === "pp:edit:reload") {
|
|
46
|
+
window.location.reload();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
window.addEventListener("message", onMsg);
|
|
50
|
+
return () => window.removeEventListener("message", onMsg);
|
|
51
|
+
}, [urlState.enabled]);
|
|
52
|
+
const value = useMemo(() => urlState, [urlState]);
|
|
53
|
+
return /* @__PURE__ */ jsx(EditModeContext.Provider, { value, children });
|
|
54
|
+
}
|
|
55
|
+
var EDIT_ATTR_STYLE = {
|
|
56
|
+
outline: "1px dashed transparent",
|
|
57
|
+
outlineOffset: "2px",
|
|
58
|
+
cursor: "pointer",
|
|
59
|
+
transition: "outline-color 0.15s ease"
|
|
60
|
+
};
|
|
61
|
+
var EDIT_HOVER_STYLE = {
|
|
62
|
+
outlineColor: "#FA501E"
|
|
63
|
+
};
|
|
64
|
+
function StrapiField({ path, type, value, children, className }) {
|
|
65
|
+
const { enabled } = useStrapiEditMode();
|
|
66
|
+
const onClick = useCallback(
|
|
67
|
+
(e) => {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
if (typeof window === "undefined" || window.parent === window) return;
|
|
71
|
+
const target = e.currentTarget;
|
|
72
|
+
const rect = target.getBoundingClientRect();
|
|
73
|
+
try {
|
|
74
|
+
window.parent.postMessage(
|
|
75
|
+
{
|
|
76
|
+
type: "pp:edit:click",
|
|
77
|
+
path,
|
|
78
|
+
fieldType: type,
|
|
79
|
+
rect: {
|
|
80
|
+
top: rect.top,
|
|
81
|
+
left: rect.left,
|
|
82
|
+
width: rect.width,
|
|
83
|
+
height: rect.height
|
|
84
|
+
},
|
|
85
|
+
currentValue: value
|
|
86
|
+
},
|
|
87
|
+
"*"
|
|
88
|
+
);
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
[path, type, value]
|
|
93
|
+
);
|
|
94
|
+
if (!enabled) {
|
|
95
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
96
|
+
}
|
|
97
|
+
const arr = Children.toArray(children);
|
|
98
|
+
if (arr.length === 1 && isValidElement(arr[0])) {
|
|
99
|
+
const el = arr[0];
|
|
100
|
+
const existing = el.props.style ?? {};
|
|
101
|
+
const existingClass = el.props.className ?? "";
|
|
102
|
+
return cloneElement(el, {
|
|
103
|
+
"data-pp-edit": path,
|
|
104
|
+
"data-pp-type": type,
|
|
105
|
+
onClick,
|
|
106
|
+
onMouseEnter: (e) => {
|
|
107
|
+
e.currentTarget.style.outlineColor = EDIT_HOVER_STYLE.outlineColor;
|
|
108
|
+
},
|
|
109
|
+
onMouseLeave: (e) => {
|
|
110
|
+
e.currentTarget.style.outlineColor = "transparent";
|
|
111
|
+
},
|
|
112
|
+
style: { ...EDIT_ATTR_STYLE, ...existing },
|
|
113
|
+
className: [existingClass, className, "pp-edit-target"].filter(Boolean).join(" ")
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return /* @__PURE__ */ jsx(
|
|
117
|
+
"span",
|
|
118
|
+
{
|
|
119
|
+
"data-pp-edit": path,
|
|
120
|
+
"data-pp-type": type,
|
|
121
|
+
onClick,
|
|
122
|
+
onMouseEnter: (e) => {
|
|
123
|
+
e.currentTarget.style.outlineColor = EDIT_HOVER_STYLE.outlineColor;
|
|
124
|
+
},
|
|
125
|
+
onMouseLeave: (e) => {
|
|
126
|
+
e.currentTarget.style.outlineColor = "transparent";
|
|
127
|
+
},
|
|
128
|
+
style: EDIT_ATTR_STYLE,
|
|
129
|
+
className: ["pp-edit-target", className].filter(Boolean).join(" "),
|
|
130
|
+
children
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
function StrapiText({ path, value, fieldType = "text", as = "span", className, children }) {
|
|
135
|
+
const content = children ?? (value === null || value === void 0 ? "" : String(value));
|
|
136
|
+
const el = createElement(as, { className }, content);
|
|
137
|
+
return /* @__PURE__ */ jsx(StrapiField, { path, type: fieldType, value, children: el });
|
|
138
|
+
}
|
|
139
|
+
function resolveUrl(value, baseUrl) {
|
|
140
|
+
const u = value.url;
|
|
141
|
+
if (u.startsWith("http")) return u;
|
|
142
|
+
if (baseUrl) return `${baseUrl.replace(/\/$/, "")}${u.startsWith("/") ? "" : "/"}${u}`;
|
|
143
|
+
return u;
|
|
144
|
+
}
|
|
145
|
+
function StrapiImage({ path, value, baseUrl, alt, className, style, loading = "lazy", fallback }) {
|
|
146
|
+
if (!value) {
|
|
147
|
+
return /* @__PURE__ */ jsx(StrapiField, { path, type: "media", value: null, children: /* @__PURE__ */ jsx("span", { className, style, children: fallback ?? "" }) });
|
|
148
|
+
}
|
|
149
|
+
return /* @__PURE__ */ jsx(StrapiField, { path, type: "media", value, children: /* @__PURE__ */ jsx(
|
|
150
|
+
"img",
|
|
151
|
+
{
|
|
152
|
+
src: resolveUrl(value, baseUrl),
|
|
153
|
+
alt: alt ?? value.alternativeText ?? value.name ?? "",
|
|
154
|
+
width: value.width,
|
|
155
|
+
height: value.height,
|
|
156
|
+
loading,
|
|
157
|
+
className,
|
|
158
|
+
style
|
|
159
|
+
}
|
|
160
|
+
) });
|
|
161
|
+
}
|
|
162
|
+
function StrapiList({ path, value, renderItem, fallback }) {
|
|
163
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
164
|
+
return /* @__PURE__ */ jsx(Fragment, { children: fallback ?? null });
|
|
165
|
+
}
|
|
166
|
+
return /* @__PURE__ */ jsx(Fragment, { children: value.map((item, i) => /* @__PURE__ */ jsx(Fragment$1, { children: renderItem(item, `${path}.${i}`, i) }, i)) });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { StrapiEditModeProvider, StrapiField, StrapiImage, StrapiList, StrapiText, useStrapiEditMode };
|
|
170
|
+
//# sourceMappingURL=index.js.map
|
|
171
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/EditModeContext.tsx","../src/StrapiField.tsx","../src/StrapiText.tsx","../src/StrapiImage.tsx","../src/StrapiList.tsx"],"names":["jsx","Fragment"],"mappings":";;;AASA,IAAM,kBAAkB,aAAA,CAA6B,EAAE,SAAS,KAAA,EAAO,KAAA,EAAO,MAAM,CAAA;AAE7E,SAAS,iBAAA,GAAmC;AACjD,EAAA,OAAO,WAAW,eAAe,CAAA;AACnC;AAmBO,SAAS,sBAAA,CAAuB,EAAE,QAAA,EAAU,OAAA,EAAS,iBAAgB,EAAkB;AAC5F,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,QAAA,CAAwB,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,CAAA;AAEvF,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,IAAA,IAAI,oBAAoB,MAAA,EAAW;AACjC,MAAA,WAAA,CAAY,EAAE,OAAA,EAAS,eAAA,EAAiB,KAAA,EAAO,MAAM,CAAA;AACrD,MAAA;AAAA,IACF;AACA,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAA,CAAO,SAAS,MAAM,CAAA;AACzD,MAAA,MAAM,IAAA,GAAO,MAAA,CAAO,GAAA,CAAI,WAAW,CAAA,KAAM,GAAA;AACzC,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA;AACrC,MAAA,WAAA,CAAY,EAAE,OAAA,EAAS,IAAA,EAAM,KAAA,EAAO,CAAA;AAAA,IACtC,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA,EAAG,CAAC,eAAe,CAAC,CAAA;AAEpB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,IAAA,QAAA,CAAS,eAAA,CAAgB,SAAA,CAAU,MAAA,CAAO,gBAAA,EAAkB,SAAS,OAAO,CAAA;AAAA,EAC9E,CAAA,EAAG,CAAC,QAAA,CAAS,OAAO,CAAC,CAAA;AAGrB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,IAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACvB,IAAA,IAAI,MAAA,CAAO,WAAW,MAAA,EAAQ;AAC9B,IAAA,IAAI;AACF,MAAA,MAAA,CAAO,MAAA,CAAO,WAAA;AAAA,QACZ,EAAE,IAAA,EAAM,eAAA,EAAiB,IAAA,EAAM,MAAA,CAAO,SAAS,IAAA,EAAK;AAAA,QACpD;AAAA,OACF;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,CAAA,EAAG,CAAC,QAAA,CAAS,OAAO,CAAC,CAAA;AAGrB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,IAAA,IAAI,CAAC,SAAS,OAAA,EAAS;AACvB,IAAA,SAAS,MAAM,CAAA,EAAiB;AAC9B,MAAA,MAAM,IAAI,CAAA,CAAE,IAAA;AACZ,MAAA,IAAI,KAAK,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,SAAS,gBAAA,EAAkB;AAE7D,QAAA,MAAA,CAAO,SAAS,MAAA,EAAO;AAAA,MACzB;AAAA,IACF;AACA,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,KAAK,CAAA;AACxC,IAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,KAAK,CAAA;AAAA,EAC1D,CAAA,EAAG,CAAC,QAAA,CAAS,OAAO,CAAC,CAAA;AAErB,EAAA,MAAM,QAAQ,OAAA,CAAQ,MAAM,QAAA,EAAU,CAAC,QAAQ,CAAC,CAAA;AAChD,EAAA,uBAAO,GAAA,CAAC,eAAA,CAAgB,QAAA,EAAhB,EAAyB,OAAe,QAAA,EAAS,CAAA;AAC3D;ACrEA,IAAM,eAAA,GAAkB;AAAA,EACtB,OAAA,EAAS,wBAAA;AAAA,EACT,aAAA,EAAe,KAAA;AAAA,EACf,MAAA,EAAQ,SAAA;AAAA,EACR,UAAA,EAAY;AACd,CAAA;AAEA,IAAM,gBAAA,GAAmB;AAAA,EACvB,YAAA,EAAc;AAChB,CAAA;AAYO,SAAS,YAAY,EAAE,IAAA,EAAM,MAAM,KAAA,EAAO,QAAA,EAAU,WAAU,EAAqB;AACxF,EAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,iBAAA,EAAkB;AAEtC,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,CAAA,KAAwB;AACvB,MAAA,CAAA,CAAE,cAAA,EAAe;AACjB,MAAA,CAAA,CAAE,eAAA,EAAgB;AAClB,MAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,MAAA,CAAO,WAAW,MAAA,EAAQ;AAC/D,MAAA,MAAM,SAAS,CAAA,CAAE,aAAA;AACjB,MAAA,MAAM,IAAA,GAAO,OAAO,qBAAA,EAAsB;AAC1C,MAAA,IAAI;AACF,QAAA,MAAA,CAAO,MAAA,CAAO,WAAA;AAAA,UACZ;AAAA,YACE,IAAA,EAAM,eAAA;AAAA,YACN,IAAA;AAAA,YACA,SAAA,EAAW,IAAA;AAAA,YACX,IAAA,EAAM;AAAA,cACJ,KAAK,IAAA,CAAK,GAAA;AAAA,cACV,MAAM,IAAA,CAAK,IAAA;AAAA,cACX,OAAO,IAAA,CAAK,KAAA;AAAA,cACZ,QAAQ,IAAA,CAAK;AAAA,aACf;AAAA,YACA,YAAA,EAAc;AAAA,WAChB;AAAA,UACA;AAAA,SACF;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF,CAAA;AAAA,IACA,CAAC,IAAA,EAAM,IAAA,EAAM,KAAK;AAAA,GACpB;AAEA,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,uBAAOA,GAAAA,CAAA,QAAA,EAAA,EAAG,QAAA,EAAS,CAAA;AAAA,EACrB;AAGA,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,OAAA,CAAQ,QAAQ,CAAA;AACrC,EAAA,IAAI,IAAI,MAAA,KAAW,CAAA,IAAK,eAAe,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG;AAC9C,IAAA,MAAM,EAAA,GAAK,IAAI,CAAC,CAAA;AAChB,IAAA,MAAM,QAAA,GAAY,EAAA,CAAG,KAAA,CAAM,KAAA,IAAiC,EAAC;AAC7D,IAAA,MAAM,aAAA,GAAiB,EAAA,CAAG,KAAA,CAAM,SAAA,IAAwB,EAAA;AACxD,IAAA,OAAO,aAAa,EAAA,EAAI;AAAA,MACtB,cAAA,EAAgB,IAAA;AAAA,MAChB,cAAA,EAAgB,IAAA;AAAA,MAChB,OAAA;AAAA,MACA,YAAA,EAAc,CAAC,CAAA,KAAwB;AACrC,QAAC,CAAA,CAAE,aAAA,CAA8B,KAAA,CAAM,YAAA,GAAe,gBAAA,CAAiB,YAAA;AAAA,MACzE,CAAA;AAAA,MACA,YAAA,EAAc,CAAC,CAAA,KAAwB;AACrC,QAAC,CAAA,CAAE,aAAA,CAA8B,KAAA,CAAM,YAAA,GAAe,aAAA;AAAA,MACxD,CAAA;AAAA,MACA,KAAA,EAAO,EAAE,GAAG,eAAA,EAAiB,GAAG,QAAA,EAAS;AAAA,MACzC,SAAA,EAAW,CAAC,aAAA,EAAe,SAAA,EAAW,gBAAgB,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG;AAAA,KACjF,CAAA;AAAA,EACH;AAGA,EAAA,uBACEA,GAAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,cAAA,EAAc,IAAA;AAAA,MACd,cAAA,EAAc,IAAA;AAAA,MACd,OAAA;AAAA,MACA,YAAA,EAAc,CAAC,CAAA,KAAM;AAAE,QAAC,CAAA,CAAE,aAAA,CAAe,KAAA,CAAM,YAAA,GAAe,gBAAA,CAAiB,YAAA;AAAA,MAAc,CAAA;AAAA,MAC7F,YAAA,EAAc,CAAC,CAAA,KAAM;AAAE,QAAC,CAAA,CAAE,aAAA,CAAe,KAAA,CAAM,YAAA,GAAe,aAAA;AAAA,MAAe,CAAA;AAAA,MAC7E,KAAA,EAAO,eAAA;AAAA,MACP,SAAA,EAAW,CAAC,gBAAA,EAAkB,SAAS,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,MAEhE;AAAA;AAAA,GACH;AAEJ;ACtFO,SAAS,UAAA,CAAW,EAAE,IAAA,EAAM,KAAA,EAAO,SAAA,GAAY,QAAQ,EAAA,GAAK,MAAA,EAAQ,SAAA,EAAW,QAAA,EAAS,EAAoB;AACjH,EAAA,MAAM,OAAA,GAAU,aAAa,KAAA,KAAU,IAAA,IAAQ,UAAU,MAAA,GAAY,EAAA,GAAK,OAAO,KAAK,CAAA,CAAA;AACtF,EAAA,MAAM,KAAK,aAAA,CAAc,EAAA,EAAI,EAAE,SAAA,IAAa,OAAO,CAAA;AACnD,EAAA,uBACEA,GAAAA,CAAC,WAAA,EAAA,EAAY,MAAY,IAAA,EAAM,SAAA,EAAW,OACvC,QAAA,EAAA,EAAA,EACH,CAAA;AAEJ;ACfA,SAAS,UAAA,CAAW,OAAyB,OAAA,EAA0B;AACrE,EAAA,MAAM,IAAI,KAAA,CAAM,GAAA;AAChB,EAAA,IAAI,CAAA,CAAE,UAAA,CAAW,MAAM,CAAA,EAAG,OAAO,CAAA;AACjC,EAAA,IAAI,SAAS,OAAO,CAAA,EAAG,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAC,CAAA,EAAG,CAAA,CAAE,WAAW,GAAG,CAAA,GAAI,EAAA,GAAK,GAAG,GAAG,CAAC,CAAA,CAAA;AACpF,EAAA,OAAO,CAAA;AACT;AAUO,SAAS,WAAA,CAAY,EAAE,IAAA,EAAM,KAAA,EAAO,OAAA,EAAS,GAAA,EAAK,SAAA,EAAW,KAAA,EAAO,OAAA,GAAU,MAAA,EAAQ,QAAA,EAAS,EAAqB;AACzH,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,uBACEA,GAAAA,CAAC,WAAA,EAAA,EAAY,IAAA,EAAY,MAAK,OAAA,EAAQ,KAAA,EAAO,IAAA,EAC3C,QAAA,kBAAAA,IAAC,MAAA,EAAA,EAAK,SAAA,EAAsB,KAAA,EAAe,QAAA,EAAA,QAAA,IAAY,IAAG,CAAA,EAC5D,CAAA;AAAA,EAEJ;AACA,EAAA,uBACEA,GAAAA,CAAC,WAAA,EAAA,EAAY,MAAY,IAAA,EAAK,OAAA,EAAQ,OACpC,QAAA,kBAAAA,GAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,GAAA,EAAK,UAAA,CAAW,KAAA,EAAO,OAAO,CAAA;AAAA,MAC9B,GAAA,EAAK,GAAA,IAAO,KAAA,CAAM,eAAA,IAAmB,MAAM,IAAA,IAAQ,EAAA;AAAA,MACnD,OAAO,KAAA,CAAM,KAAA;AAAA,MACb,QAAQ,KAAA,CAAM,MAAA;AAAA,MACd,OAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA;AAAA,GACF,EACF,CAAA;AAEJ;ACzBO,SAAS,WAAc,EAAE,IAAA,EAAM,KAAA,EAAO,UAAA,EAAY,UAAS,EAAuB;AACvF,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,WAAW,CAAA,EAAG;AAC/C,IAAA,uBAAOA,GAAAA,CAAAC,QAAAA,EAAA,EAAG,sBAAY,IAAA,EAAK,CAAA;AAAA,EAC7B;AACA,EAAA,uBACED,GAAAA,CAAAC,QAAAA,EAAA,EACG,QAAA,EAAA,KAAA,CAAM,IAAI,CAAC,IAAA,EAAM,CAAA,qBAChBD,GAAAA,CAACC,UAAAA,EAAA,EAAkB,QAAA,EAAA,UAAA,CAAW,IAAA,EAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,CAAC,IAAI,CAAC,CAAA,EAAA,EAAtC,CAAwC,CACxD,CAAA,EACH,CAAA;AAEJ","file":"index.js","sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useEffect, useMemo, useState } from \"react\";\n\ninterface EditModeState {\n enabled: boolean;\n token: string | null;\n}\n\nconst EditModeContext = createContext<EditModeState>({ enabled: false, token: null });\n\nexport function useStrapiEditMode(): EditModeState {\n return useContext(EditModeContext);\n}\n\ninterface ProviderProps {\n children: React.ReactNode;\n /**\n * Override automatic URL-based detection. If `true`/`false` ist gesetzt,\n * wird der URL-Parameter ignoriert.\n */\n enabled?: boolean;\n}\n\n/**\n * Provider, der den Edit-Mode aus `?__pp_edit=1&__pp_token=<jwt>` der URL\n * liest und an Kinder via Context weitergibt. Token wird sicherheitshalber\n * NICHT ans CSR exponiert über Props oder DOM — er bleibt nur im Context.\n *\n * Im Edit-Mode wird ausserdem eine kleine globale CSS-Klasse `pp-edit-active`\n * am `<html>` gesetzt, sodass Customer-Sites optional CSS-Hooks setzen können.\n */\nexport function StrapiEditModeProvider({ children, enabled: enabledOverride }: ProviderProps) {\n const [urlState, setUrlState] = useState<EditModeState>({ enabled: false, token: null });\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (enabledOverride !== undefined) {\n setUrlState({ enabled: enabledOverride, token: null });\n return;\n }\n try {\n const params = new URLSearchParams(window.location.search);\n const edit = params.get(\"__pp_edit\") === \"1\";\n const token = params.get(\"__pp_token\");\n setUrlState({ enabled: edit, token });\n } catch {\n // ignore\n }\n }, [enabledOverride]);\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n document.documentElement.classList.toggle(\"pp-edit-active\", urlState.enabled);\n }, [urlState.enabled]);\n\n // postMessage \"ready\" wenn Edit-Mode aktiv ist und Iframe geladen\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (!urlState.enabled) return;\n if (window.parent === window) return; // nicht im Iframe\n try {\n window.parent.postMessage(\n { type: \"pp:edit:ready\", href: window.location.href },\n \"*\",\n );\n } catch {\n // ignore\n }\n }, [urlState.enabled]);\n\n // Reload-Listener vom Parent (nach Save)\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (!urlState.enabled) return;\n function onMsg(e: MessageEvent) {\n const d = e.data;\n if (d && typeof d === \"object\" && d.type === \"pp:edit:reload\") {\n // Soft-Reload — re-loaded mit gleichem Suchparameter\n window.location.reload();\n }\n }\n window.addEventListener(\"message\", onMsg);\n return () => window.removeEventListener(\"message\", onMsg);\n }, [urlState.enabled]);\n\n const value = useMemo(() => urlState, [urlState]);\n return <EditModeContext.Provider value={value}>{children}</EditModeContext.Provider>;\n}\n","\"use client\";\n\nimport { Children, cloneElement, isValidElement, useCallback, type ReactElement, type ReactNode } from \"react\";\nimport { useStrapiEditMode } from \"./EditModeContext\";\nimport type { StrapiFieldType } from \"./types\";\n\ninterface StrapiFieldProps {\n /** Dot-notation path im Strapi-Document, z.B. \"hero.title\" oder \"features.0.image\" */\n path: string;\n /** Strapi field type — bestimmt welche Edit-UI im Parent geöffnet wird */\n type: StrapiFieldType;\n /** Aktueller Wert. Wird beim Click-Event mitgesendet, damit der Editor initial befüllt ist */\n value?: unknown;\n /** Inhalt (das echte gerenderte HTML). Wird im Edit-Mode mit data-attrs + Hover/Click annotiert */\n children: ReactNode;\n /** Optional: zusätzliche CSS-Klassen die im Edit-Mode angehängt werden */\n className?: string;\n}\n\nconst EDIT_ATTR_STYLE = {\n outline: \"1px dashed transparent\",\n outlineOffset: \"2px\",\n cursor: \"pointer\",\n transition: \"outline-color 0.15s ease\",\n} as const;\n\nconst EDIT_HOVER_STYLE = {\n outlineColor: \"#FA501E\",\n};\n\n/**\n * Universeller Wrapper für editierbare Strapi-Felder.\n *\n * Im normalen Mode rendert er nur die `children` — keine zusätzlichen DOM-\n * Nodes, kein Overhead. Im Edit-Mode wird das einzige Top-Level-Child mit\n * `data-pp-edit=\"<path>\"` annotiert + Click-Handler bekommt.\n *\n * Falls children mehrere Top-Level-Knoten haben, wird automatisch ein\n * `<span>` als Wrapper eingefügt.\n */\nexport function StrapiField({ path, type, value, children, className }: StrapiFieldProps) {\n const { enabled } = useStrapiEditMode();\n\n const onClick = useCallback(\n (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (typeof window === \"undefined\" || window.parent === window) return;\n const target = e.currentTarget as HTMLElement;\n const rect = target.getBoundingClientRect();\n try {\n window.parent.postMessage(\n {\n type: \"pp:edit:click\",\n path,\n fieldType: type,\n rect: {\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n },\n currentValue: value,\n },\n \"*\",\n );\n } catch {\n // ignore\n }\n },\n [path, type, value],\n );\n\n if (!enabled) {\n return <>{children}</>;\n }\n\n // Wenn genau ein React-Element drin ist, klonen wir es und reichen die Edit-Attrs durch.\n const arr = Children.toArray(children);\n if (arr.length === 1 && isValidElement(arr[0])) {\n const el = arr[0] as ReactElement<Record<string, unknown>>;\n const existing = (el.props.style as React.CSSProperties) ?? {};\n const existingClass = (el.props.className as string) ?? \"\";\n return cloneElement(el, {\n \"data-pp-edit\": path,\n \"data-pp-type\": type,\n onClick,\n onMouseEnter: (e: React.MouseEvent) => {\n (e.currentTarget as HTMLElement).style.outlineColor = EDIT_HOVER_STYLE.outlineColor;\n },\n onMouseLeave: (e: React.MouseEvent) => {\n (e.currentTarget as HTMLElement).style.outlineColor = \"transparent\";\n },\n style: { ...EDIT_ATTR_STYLE, ...existing },\n className: [existingClass, className, \"pp-edit-target\"].filter(Boolean).join(\" \"),\n });\n }\n\n // Mehrere Kinder: in einen span wrappen\n return (\n <span\n data-pp-edit={path}\n data-pp-type={type}\n onClick={onClick}\n onMouseEnter={(e) => { (e.currentTarget).style.outlineColor = EDIT_HOVER_STYLE.outlineColor; }}\n onMouseLeave={(e) => { (e.currentTarget).style.outlineColor = \"transparent\"; }}\n style={EDIT_ATTR_STYLE}\n className={[\"pp-edit-target\", className].filter(Boolean).join(\" \")}\n >\n {children}\n </span>\n );\n}\n","\"use client\";\n\nimport { createElement, type ElementType, type ReactNode } from \"react\";\nimport { StrapiField } from \"./StrapiField\";\nimport type { StrapiFieldType } from \"./types\";\n\ninterface StrapiTextProps {\n path: string;\n value: string | number | null | undefined;\n /** Welcher Strapi-Feldtyp ist das? Default \"text\". Für mehrzeilige Felder \"textarea\", für Markdown \"richText\". */\n fieldType?: Extract<StrapiFieldType, \"text\" | \"textarea\" | \"richText\" | \"email\" | \"number\">;\n /** HTML-Tag um den Wert zu rendern. Default \"span\". */\n as?: ElementType;\n className?: string;\n children?: ReactNode;\n}\n\n/**\n * Inline-Text wrapper. Im normalen Mode rendert es einfach `value` im\n * gewählten Tag — identisch zu einem rohen `<h1>{value}</h1>`. Im Edit-Mode\n * wird ein Click-Handler aktiviert.\n *\n * Wenn `children` mitgegeben sind, werden diese statt `value` als Inhalt\n * verwendet — nützlich für custom Formatierung. Der Click-Wert bleibt\n * `value`.\n */\nexport function StrapiText({ path, value, fieldType = \"text\", as = \"span\", className, children }: StrapiTextProps) {\n const content = children ?? (value === null || value === undefined ? \"\" : String(value));\n const el = createElement(as, { className }, content);\n return (\n <StrapiField path={path} type={fieldType} value={value}>\n {el}\n </StrapiField>\n );\n}\n","\"use client\";\n\nimport { StrapiField } from \"./StrapiField\";\nimport type { StrapiMediaValue } from \"./types\";\n\ninterface StrapiImageProps {\n path: string;\n value: StrapiMediaValue | null | undefined;\n /** Optional: Strapi-Basis-URL für relative URLs (z.B. \"https://cms.paulpaul.studio\") */\n baseUrl?: string;\n /** Override alt-Text (default: alternativeText vom Strapi-Objekt, dann name) */\n alt?: string;\n className?: string;\n style?: React.CSSProperties;\n loading?: \"lazy\" | \"eager\";\n /** Fallback wenn value null/undefined ist */\n fallback?: React.ReactNode;\n}\n\nfunction resolveUrl(value: StrapiMediaValue, baseUrl?: string): string {\n const u = value.url;\n if (u.startsWith(\"http\")) return u;\n if (baseUrl) return `${baseUrl.replace(/\\/$/, \"\")}${u.startsWith(\"/\") ? \"\" : \"/\"}${u}`;\n return u;\n}\n\n/**\n * Wrapper für ein einzelnes Strapi-Media-Feld. Rendert `<img>` mit der\n * abgeleiteten URL. Im Edit-Mode klickbar — der Editor öffnet einen\n * MediaPicker.\n *\n * Wenn `value` null ist, wird der `fallback` gerendert (oder ein leeres\n * `<span>`).\n */\nexport function StrapiImage({ path, value, baseUrl, alt, className, style, loading = \"lazy\", fallback }: StrapiImageProps) {\n if (!value) {\n return (\n <StrapiField path={path} type=\"media\" value={null}>\n <span className={className} style={style}>{fallback ?? \"\"}</span>\n </StrapiField>\n );\n }\n return (\n <StrapiField path={path} type=\"media\" value={value}>\n <img\n src={resolveUrl(value, baseUrl)}\n alt={alt ?? value.alternativeText ?? value.name ?? \"\"}\n width={value.width}\n height={value.height}\n loading={loading}\n className={className}\n style={style}\n />\n </StrapiField>\n );\n}\n","\"use client\";\n\nimport { Fragment, type ReactNode } from \"react\";\n\ninterface StrapiListProps<T> {\n /** Pfad zum Array-Feld, z.B. \"features\" */\n path: string;\n value: T[] | null | undefined;\n /**\n * Render-Funktion pro Item. Bekommt einen `itemPath` als zweites Argument,\n * der für nested-StrapiText/Image-Calls als path-Prefix dient.\n */\n renderItem: (item: T, itemPath: string, index: number) => ReactNode;\n /** Optional: Fallback wenn value null/leer ist */\n fallback?: ReactNode;\n}\n\n/**\n * Iteriert über ein Strapi-Repeatable-Field oder eine Relation-Liste. Generiert\n * pro Item den vollständigen Edit-Pfad (z.B. \"features.0\", \"features.1\"), den\n * der Caller in nested StrapiText/Image weitergibt.\n *\n * Beispiel:\n * <StrapiList path=\"features\" value={page.features} renderItem={(f, fp) => (\n * <div>\n * <StrapiText path={`${fp}.title`} value={f.title} as=\"h3\" />\n * <StrapiImage path={`${fp}.icon`} value={f.icon} />\n * </div>\n * )} />\n */\nexport function StrapiList<T>({ path, value, renderItem, fallback }: StrapiListProps<T>) {\n if (!Array.isArray(value) || value.length === 0) {\n return <>{fallback ?? null}</>;\n }\n return (\n <>\n {value.map((item, i) => (\n <Fragment key={i}>{renderItem(item, `${path}.${i}`, i)}</Fragment>\n ))}\n </>\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paulpaulstudio/strapi-render",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React SDK für Strapi-Frontends mit onpage-edit-Support über cms.paulpaul.studio.",
|
|
5
|
+
"keywords": ["strapi", "cms", "onpage", "edit", "react"],
|
|
6
|
+
"author": "Paul & Paul Studio <info@paulpaul.studio>",
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"private": false,
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://gitlab.paulpaul.studio/intern/strapi-render-sdk.git"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"prepare": "tsup",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.0.0",
|
|
42
|
+
"react-dom": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19.0.0",
|
|
46
|
+
"@types/react-dom": "^19.0.0",
|
|
47
|
+
"react": "^19.0.0",
|
|
48
|
+
"react-dom": "^19.0.0",
|
|
49
|
+
"tsup": "^8.3.0",
|
|
50
|
+
"typescript": "^5.5.0"
|
|
51
|
+
}
|
|
52
|
+
}
|