@persona/layout-editor-remote-module 0.8.0 → 0.9.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 +13 -10
- package/dist/bridge-frame.mjs +14 -6
- package/dist/bridge-host.d.mts +4 -1
- package/dist/bridge-host.mjs +17 -3
- package/package.json +1 -1
- package/skills/getting-started/SKILL.md +81 -85
package/README.md
CHANGED
|
@@ -56,8 +56,9 @@ contract, including the optional `path` field for a client-routed SPA.
|
|
|
56
56
|
|
|
57
57
|
## Communicating with the host
|
|
58
58
|
|
|
59
|
-
Document-delivered modules can use the browser bridge to receive layout data, report
|
|
60
|
-
|
|
59
|
+
Document-delivered modules can use the browser bridge to receive layout data, report the height
|
|
60
|
+
their document needs (including `body` and ancestor margins and padding), and resolve data paths
|
|
61
|
+
through the host. The host renders the document with `RemoteModuleFrame`:
|
|
61
62
|
|
|
62
63
|
```tsx
|
|
63
64
|
import { RemoteModuleFrame } from '@persona/layout-editor-remote-module/bridge-host';
|
|
@@ -116,10 +117,12 @@ cannot derive itself:
|
|
|
116
117
|
- `isInEditor` — the module is rendered inside the layout editor rather than on a live record
|
|
117
118
|
- `isUsingStubData` — the host is feeding placeholder/stub data instead of a real record
|
|
118
119
|
- `dynamicHeight` — whether the host sizes the iframe to the height the bridge reports
|
|
119
|
-
(`RemoteModuleFrame`'s `dynamicHeight`).
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
(`RemoteModuleFrame`'s `dynamicHeight`). The reported height covers the whole frame document,
|
|
121
|
+
including `body` and ancestor margins and padding. A module that fills a fixed-height iframe with
|
|
122
|
+
viewport units (`100vh`) should size to its content when it's set — combined with any `body`
|
|
123
|
+
margin or padding the reported height exceeds the iframe, so it grows on every report.
|
|
124
|
+
`RemoteModuleFrame` sets it from its `dynamicHeight` prop, overriding any value in the `context`
|
|
125
|
+
prop.
|
|
123
126
|
|
|
124
127
|
`context` is sent with `init` and pushed again whenever it changes, just like `data`; inside the
|
|
125
128
|
iframe it arrives through `ready()`, `getContext()`, and the `context` render prop (all
|
|
@@ -140,10 +143,10 @@ The bridge's security model is intentionally narrow:
|
|
|
140
143
|
- The iframe derives the ready target from `ancestorOrigins` or the referrer, and the host retries
|
|
141
144
|
`init` until the frame answers.
|
|
142
145
|
- Messages use a versioned protocol that stays compatible with frames built against older releases
|
|
143
|
-
(from
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
(from 0.5.3): receivers ignore message types they don't know and strip keys they don't know, and
|
|
147
|
+
fields are never removed, renamed or narrowed. `context.layoutType` can be a layout type added
|
|
148
|
+
later, so handle values you don't know. Credentials remain in the iframe URL and are never handled
|
|
149
|
+
through bridge messages.
|
|
147
150
|
- `extraParams` rejects reserved names and credential-like keys.
|
|
148
151
|
|
|
149
152
|
### Consuming a manifest
|
package/dist/bridge-frame.mjs
CHANGED
|
@@ -61,6 +61,15 @@ function createPendingRequests() {
|
|
|
61
61
|
|
|
62
62
|
//#endregion
|
|
63
63
|
//#region src/bridge/iframe/createModuleBridge.ts
|
|
64
|
+
const measureDocumentHeight = (element) => {
|
|
65
|
+
const rect = element.getBoundingClientRect();
|
|
66
|
+
let trailing = 0;
|
|
67
|
+
for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
|
|
68
|
+
const style = window.getComputedStyle(ancestor);
|
|
69
|
+
trailing += parseFloat(style.paddingBottom) + parseFloat(style.borderBottomWidth) + parseFloat(style.marginBottom);
|
|
70
|
+
}
|
|
71
|
+
return Math.ceil(rect.top + window.scrollY + rect.height + trailing);
|
|
72
|
+
};
|
|
64
73
|
function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins, resolveTimeoutMs = 3e4 } = {}) {
|
|
65
74
|
const isEmbedded = typeof window !== "undefined" && window.parent !== window;
|
|
66
75
|
const allowedOrigin = resolveAllowedHostOrigins(allowedHostOrigins);
|
|
@@ -203,20 +212,18 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
203
212
|
height
|
|
204
213
|
});
|
|
205
214
|
};
|
|
206
|
-
const observer = new ResizeObserver((
|
|
207
|
-
const entry = entries[0];
|
|
208
|
-
if (!entry) return;
|
|
209
|
-
const borderBoxSize = entry.borderBoxSize;
|
|
210
|
-
const height = (Array.isArray(borderBoxSize) ? borderBoxSize[0]?.blockSize : void 0) ?? entry.contentRect.height;
|
|
215
|
+
const observer = new ResizeObserver(() => {
|
|
211
216
|
if (frame !== void 0) return;
|
|
212
217
|
const callback = () => {
|
|
213
218
|
frame = void 0;
|
|
214
|
-
postHeight(
|
|
219
|
+
postHeight(measureDocumentHeight(element));
|
|
215
220
|
};
|
|
216
221
|
frame = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(callback) : window.setTimeout(callback, 0);
|
|
217
222
|
});
|
|
218
223
|
observers.add(observer);
|
|
219
224
|
observer.observe(element);
|
|
225
|
+
const body = element.ownerDocument?.body;
|
|
226
|
+
if (body && body !== element) observer.observe(body);
|
|
220
227
|
return () => {
|
|
221
228
|
disconnected = true;
|
|
222
229
|
if (frame !== void 0) {
|
|
@@ -337,6 +344,7 @@ function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeigh
|
|
|
337
344
|
fallback,
|
|
338
345
|
render: () => /* @__PURE__ */ jsx("div", {
|
|
339
346
|
ref: rootRef,
|
|
347
|
+
style: { display: "flow-root" },
|
|
340
348
|
children: children({
|
|
341
349
|
context,
|
|
342
350
|
data,
|
package/dist/bridge-host.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as Json, t as RemoteModuleContext } from "./protocol-Dqnl8JTs.mjs";
|
|
2
|
-
import { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { CSSProperties, ReactNode, Ref } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/bridge/host/buildModuleSrc.d.ts
|
|
5
5
|
type BuildModuleSrcOptions = {
|
|
@@ -30,6 +30,8 @@ type RemoteModuleFrameProps = {
|
|
|
30
30
|
minHeight?: number;
|
|
31
31
|
className?: string;
|
|
32
32
|
style?: CSSProperties;
|
|
33
|
+
/** Receives the underlying <iframe> so a host can attach extra postMessage bridges. */
|
|
34
|
+
iframeRef?: Ref<HTMLIFrameElement>;
|
|
33
35
|
data: Json;
|
|
34
36
|
context: RemoteModuleContext;
|
|
35
37
|
resolveDataPath: ResolveDataPath;
|
|
@@ -52,6 +54,7 @@ declare function RemoteModuleFrame({
|
|
|
52
54
|
minHeight,
|
|
53
55
|
className,
|
|
54
56
|
style,
|
|
57
|
+
iframeRef: externalIframeRef,
|
|
55
58
|
data,
|
|
56
59
|
context,
|
|
57
60
|
resolveDataPath,
|
package/dist/bridge-host.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-D_pdVZjs.mjs";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/bridge/host/buildModuleSrc.ts
|
|
@@ -145,14 +145,27 @@ function withDynamicHeight(context, dynamicHeight) {
|
|
|
145
145
|
dynamicHeight
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
-
function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environmentId, locale, extraParams, title, sandbox, referrerPolicy = "no-referrer", initRetryMs, initRetryTimeoutMs, dynamicHeight = false, heightOffset = 0, minHeight, className, style, data, context, resolveDataPath, onRenderError }) {
|
|
148
|
+
function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environmentId, locale, extraParams, title, sandbox, referrerPolicy = "no-referrer", initRetryMs, initRetryTimeoutMs, dynamicHeight = false, heightOffset = 0, minHeight, className, style, iframeRef: externalIframeRef, data, context, resolveDataPath, onRenderError }) {
|
|
149
149
|
const iframeRef = useRef(null);
|
|
150
150
|
const dataRef = useRef(data);
|
|
151
151
|
const contextRef = useRef(withDynamicHeight(context, dynamicHeight));
|
|
152
152
|
const onRenderErrorRef = useRef(onRenderError);
|
|
153
153
|
const resolveDataPathRef = useRef(resolveDataPath);
|
|
154
154
|
const bridgeRef = useRef(void 0);
|
|
155
|
+
const externalCleanupRef = useRef(void 0);
|
|
155
156
|
const [height, setHeight] = useState();
|
|
157
|
+
const setRefs = useCallback((node) => {
|
|
158
|
+
iframeRef.current = node;
|
|
159
|
+
if (typeof externalIframeRef === "function") if (node) {
|
|
160
|
+
const cleanup = externalIframeRef(node);
|
|
161
|
+
externalCleanupRef.current = typeof cleanup === "function" ? cleanup : void 0;
|
|
162
|
+
} else if (externalCleanupRef.current) {
|
|
163
|
+
const cleanup = externalCleanupRef.current;
|
|
164
|
+
externalCleanupRef.current = void 0;
|
|
165
|
+
cleanup();
|
|
166
|
+
} else externalIframeRef(null);
|
|
167
|
+
else if (externalIframeRef) externalIframeRef.current = node;
|
|
168
|
+
}, [externalIframeRef]);
|
|
156
169
|
const hostContext = useMemo(() => withDynamicHeight(context, dynamicHeight), [context, dynamicHeight]);
|
|
157
170
|
dataRef.current = data;
|
|
158
171
|
contextRef.current = hostContext;
|
|
@@ -205,7 +218,7 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
205
218
|
}, [hostContext]);
|
|
206
219
|
const computedHeight = dynamicHeight && height !== void 0 ? `${Math.max(height + heightOffset, minHeight ?? 0)}px` : minHeight === void 0 ? "100%" : `${minHeight}px`;
|
|
207
220
|
return /* @__PURE__ */ jsx("iframe", {
|
|
208
|
-
ref:
|
|
221
|
+
ref: setRefs,
|
|
209
222
|
src,
|
|
210
223
|
title,
|
|
211
224
|
sandbox,
|
|
@@ -214,6 +227,7 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
214
227
|
style: {
|
|
215
228
|
...style,
|
|
216
229
|
height: computedHeight,
|
|
230
|
+
display: style?.display ?? "block",
|
|
217
231
|
width: "100%",
|
|
218
232
|
border: "none"
|
|
219
233
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules in Persona dashboard apps.",
|
|
5
5
|
"license": "SEE LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
name: getting-started
|
|
3
3
|
description: >
|
|
4
4
|
Wire up @persona/layout-editor-remote-module in a Persona dashboard app: register
|
|
5
|
-
personaLayoutModules in
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
personaLayoutModules in vite.config.ts, author modules with defineLayoutModule using an .html
|
|
6
|
+
document entry, add a zod configSchema and an RJSF uiConfigSchema (labels, field order,
|
|
7
|
+
DataSourceField pickers), and talk to the host from the iframe with LayoutModuleBridge (layout
|
|
8
|
+
data, host context such as layoutType / layoutObjectId / isReadOnly / dynamicHeight,
|
|
9
|
+
resolveDataPath, height). Activate when adding a layout module, integrating this package for the
|
|
10
|
+
first time, wiring a module document to the dashboard host, or debugging a manifest that fails to
|
|
11
|
+
build or validate (duplicate key, missing defaultSize entry, render path not found), a module that
|
|
12
|
+
never receives data from the host, or an iframe whose height collapses or keeps growing under
|
|
13
|
+
dynamic height.
|
|
14
14
|
metadata:
|
|
15
15
|
type: core
|
|
16
16
|
library: '@persona/layout-editor-remote-module'
|
|
@@ -31,8 +31,13 @@ This package is publicly available from the npm registry.
|
|
|
31
31
|
yarn add @persona/layout-editor-remote-module
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
`vite` and `zod` are peer dependencies
|
|
35
|
-
|
|
34
|
+
`vite` (`^6 || ^7 || ^8`) and `zod` are peer dependencies. Vite 5 is unsupported because the plugin
|
|
35
|
+
relies on the Vite 6 Environment API; `react` / `react-dom` (`^17 || ^18 || ^19`) are optional peers
|
|
36
|
+
needed only by the `bridge-frame` entry.
|
|
37
|
+
|
|
38
|
+
Build the iframed document against `@persona/layout-editor-remote-module` 0.5.3 or later. Older
|
|
39
|
+
frames reject any host message that carries a key they don't know, so they stop working once the
|
|
40
|
+
host sends new context fields such as `dynamicHeight`.
|
|
36
41
|
|
|
37
42
|
## Setup (vite.config.ts)
|
|
38
43
|
|
|
@@ -65,8 +70,10 @@ export default defineConfig({
|
|
|
65
70
|
An empty or omitted `layouts` makes the module available in every layout. When `layouts` lists
|
|
66
71
|
specific layouts, `defaultSize` must provide an entry for each one.
|
|
67
72
|
|
|
68
|
-
Optional fields: `allowDynamicHeight` (
|
|
69
|
-
|
|
73
|
+
Optional fields: `allowDynamicHeight` (exposes the option only; the host sizes the iframe to the
|
|
74
|
+
module's reported height only when the user turns "dynamic height" on for that placement, and the
|
|
75
|
+
module learns this through [`context.dynamicHeight`](#host-context)), `badge`
|
|
76
|
+
(`'beta' | 'deprecated' | 'improved' | 'new'`), `category`
|
|
70
77
|
(`'build_your_own' | 'components' | 'management' | 'related_records'`; the host defaults to
|
|
71
78
|
`build_your_own`), `categoryCustom` (a free-form module-library category label, up to 80 characters;
|
|
72
79
|
the host renders it verbatim — it is not translated — and groups modules that share the same
|
|
@@ -81,6 +88,12 @@ narrow visibility, never widen it), and `configSchema` / `uiConfigSchema` (below
|
|
|
81
88
|
`entry` is resolved relative to the Vite **root** (`config.root`, or `process.cwd()` when unset) —
|
|
82
89
|
not relative to `vite.config.ts`'s own directory when `root` is overridden elsewhere in the config.
|
|
83
90
|
|
|
91
|
+
In a multi-environment build (e.g. with `@cloudflare/vite-plugin`), the plugin adds module documents
|
|
92
|
+
only to the `client` environment's build inputs and writes the manifest only into the client
|
|
93
|
+
environment's `build.outDir`. Non-client environments (workers/SSR) never see the `.html` entries.
|
|
94
|
+
|
|
95
|
+
Source: `src/vite-plugin.ts` (`configEnvironment`, `closeBundle`).
|
|
96
|
+
|
|
84
97
|
## Core Patterns
|
|
85
98
|
|
|
86
99
|
### Document delivery — iframed page
|
|
@@ -273,8 +286,11 @@ export function App() {
|
|
|
273
286
|
implementation. It rejects if the host reports an error or does not answer within
|
|
274
287
|
`resolveTimeoutMs` (default 30s). Outside an iframe it rejects immediately, so a module opened
|
|
275
288
|
directly in a tab still renders.
|
|
276
|
-
- Height is reported automatically: the bridge wraps `children` in a `
|
|
277
|
-
|
|
289
|
+
- Height is reported automatically: the bridge wraps `children` in a `display: flow-root` div and
|
|
290
|
+
reports the height the frame document needs, including `body` and ancestor margins, padding, and
|
|
291
|
+
borders, so the host can size the iframe (`observeHeight={false}` to opt out). The host uses the
|
|
292
|
+
reported height only when [`context.dynamicHeight`](#host-context) is true; otherwise, the iframe
|
|
293
|
+
keeps its grid size.
|
|
278
294
|
- A render error thrown inside `children` is caught, reported to the host, and `fallback` is shown.
|
|
279
295
|
- Mount exactly one `LayoutModuleBridge` per document.
|
|
280
296
|
|
|
@@ -301,15 +317,24 @@ import {
|
|
|
301
317
|
|
|
302
318
|
`RemoteModuleContext` (exported as a type from `bridge-frame`) has:
|
|
303
319
|
|
|
304
|
-
- `layoutType` — `'account' | 'case' | 'transaction'`,
|
|
320
|
+
- `layoutType` — known values are `'account' | 'case' | 'transaction'`, but its type is
|
|
321
|
+
`LayoutType | (string & {})` and the bridge accepts layout types added after the module's release.
|
|
322
|
+
Handle unknown values with a default branch rather than a switch that tries to cover every case.
|
|
305
323
|
- `layoutObjectId` — the ID of the object the layout is rendered for (e.g. the viewed case's ID on a
|
|
306
324
|
`case` layout), or `null` when there is none.
|
|
307
325
|
- `isReadOnly` — the host object is locked for edits; hide or disable mutating actions.
|
|
308
326
|
- `isInEditor` — rendered inside the layout editor rather than on a live record.
|
|
309
327
|
- `isUsingStubData` — the host is feeding placeholder data instead of a real record; skip network
|
|
310
328
|
calls or side effects keyed off the record.
|
|
329
|
+
- `dynamicHeight` — `boolean`, optional in the type; `RemoteModuleFrame` always sets it. It is true
|
|
330
|
+
when the host sizes the iframe to the height the bridge reports. When true, size to content
|
|
331
|
+
(`height: auto`) instead of filling the viewport. When false or absent, the iframe has a fixed
|
|
332
|
+
grid height, so filling it (`100vh`) is fine. Treat a `null` context (not embedded) like false;
|
|
333
|
+
see
|
|
334
|
+
[Viewport-relative height under dynamic height](#high-viewport-relative-height-under-dynamic-height).
|
|
311
335
|
|
|
312
|
-
`context` is `null` only when the document is not embedded (opened directly in a tab)
|
|
336
|
+
`context` is `null` only when the document is not embedded (opened directly in a tab); inside
|
|
337
|
+
`children` it is otherwise always set, because `children` runs only after `init`.
|
|
313
338
|
|
|
314
339
|
```tsx
|
|
315
340
|
import {
|
|
@@ -325,6 +350,11 @@ function RiskSummary({ context }: { context: RemoteModuleContext | null }) {
|
|
|
325
350
|
}
|
|
326
351
|
```
|
|
327
352
|
|
|
353
|
+
The protocol only ever adds fields. Receivers built on 0.5.3+ ignore unknown message types and strip
|
|
354
|
+
unknown keys, so new context fields may appear in later host releases without a frame upgrade.
|
|
355
|
+
|
|
356
|
+
Source: `src/bridge/protocol.ts`.
|
|
357
|
+
|
|
328
358
|
#### Access token and environment id
|
|
329
359
|
|
|
330
360
|
The host still appends `access_token`, `environment_id`, and `locale` to the iframe `src`. The app
|
|
@@ -337,35 +367,9 @@ parameters cannot leak through subsequent requests.
|
|
|
337
367
|
|
|
338
368
|
### [HIGH] Reading layout data outside `LayoutModuleBridge`
|
|
339
369
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const data = await requestDataSomehow(); // no such API
|
|
344
|
-
const resolveDataPath = window.resolveDataPath; // no such API
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
Correct:
|
|
348
|
-
|
|
349
|
-
```tsx
|
|
350
|
-
function Module({
|
|
351
|
-
data,
|
|
352
|
-
resolveDataPath,
|
|
353
|
-
}: {
|
|
354
|
-
data: Json;
|
|
355
|
-
resolveDataPath: (
|
|
356
|
-
dataPath: string,
|
|
357
|
-
variableMap?: Record<string, string | number>,
|
|
358
|
-
) => Promise<Json>;
|
|
359
|
-
}) {
|
|
360
|
-
// ...
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
<LayoutModuleBridge>
|
|
364
|
-
{({ data, resolveDataPath }) => <Module data={data} resolveDataPath={resolveDataPath} />}
|
|
365
|
-
</LayoutModuleBridge>;
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
`data` and `resolveDataPath` only exist inside the render prop.
|
|
370
|
+
Do not invent APIs such as `requestDataSomehow()` or `window.resolveDataPath`. `data` and
|
|
371
|
+
`resolveDataPath` exist only in the `LayoutModuleBridge` render prop; pass them to nested components
|
|
372
|
+
from there.
|
|
369
373
|
|
|
370
374
|
Source: `src/bridge/iframe/LayoutModuleBridge.tsx`.
|
|
371
375
|
|
|
@@ -373,12 +377,34 @@ Source: `src/bridge/iframe/LayoutModuleBridge.tsx`.
|
|
|
373
377
|
|
|
374
378
|
`children` never running means the handshake didn't complete. Check, in order: the document is
|
|
375
379
|
really cross-origin-embedded by a Persona host (the default `allowedHostOrigins` predicate rejects
|
|
376
|
-
unknown parents — pass your own only for a non-Persona dev host); the
|
|
377
|
-
|
|
380
|
+
unknown parents — pass your own only for a non-Persona dev host); the frame's
|
|
381
|
+
`@persona/layout-editor-remote-module` is 0.5.3 or later (a frame on 0.5.2 or older rejects an
|
|
382
|
+
`init` whose context has keys it doesn't know, e.g. `dynamicHeight`, sent by hosts on 0.8.0+, so
|
|
383
|
+
`children` never runs); and the host is rendering it through `RemoteModuleFrame` (bare `<iframe>`s
|
|
378
384
|
never send `init`). Opening the document directly in a tab is _not_ embedded — `data` and `context`
|
|
379
|
-
resolve to `null` and `children` renders, so a stuck fallback is always a host
|
|
385
|
+
resolve to `null` and `children` renders, so a stuck fallback is always a host, origin, or frame
|
|
386
|
+
version problem.
|
|
387
|
+
|
|
388
|
+
Source: `src/bridge/iframe/createModuleBridge.ts`, `src/bridge/origin.ts`, `src/bridge/protocol.ts`.
|
|
389
|
+
|
|
390
|
+
### [HIGH] Viewport-relative height under dynamic height
|
|
391
|
+
|
|
392
|
+
Wrong: the module root uses `height: 100vh` or `min-height: 100vh` (or `height: 100%` on `html` /
|
|
393
|
+
`body` / root) regardless of context. With dynamic height on, the bridge reports the iframe's own
|
|
394
|
+
height, so the module can never shrink. And because the reported height includes the frame
|
|
395
|
+
document's `body`/ancestor margin and padding, `100vh` or `min-height: 100vh` combined with any body
|
|
396
|
+
margin or padding reports a height larger than the iframe, so the iframe grows on every report.
|
|
397
|
+
|
|
398
|
+
Correct: switch to `auto` when `context?.dynamicHeight` is true:
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
const sizing = context?.dynamicHeight ? 'content' : 'viewport';
|
|
402
|
+
// root: height: sizing === 'content' ? 'auto' : '100vh'
|
|
403
|
+
// min-height: sizing === 'content' ? 'auto' : '100vh'
|
|
404
|
+
```
|
|
380
405
|
|
|
381
|
-
Source: `src/bridge/
|
|
406
|
+
Source: `src/bridge/protocol.ts` (`remoteModuleContextSchema.dynamicHeight`),
|
|
407
|
+
`src/bridge/host/RemoteModuleFrame.tsx`.
|
|
382
408
|
|
|
383
409
|
### [HIGH] `DataSourceField` on a non-string property, or read as the resolved value
|
|
384
410
|
|
|
@@ -397,19 +423,6 @@ Spell `'DataSourceField'` exactly; module-supplied field components are not poss
|
|
|
397
423
|
|
|
398
424
|
### [HIGH] `key` uses the wrong charset or collides with another module
|
|
399
425
|
|
|
400
|
-
Wrong:
|
|
401
|
-
|
|
402
|
-
```ts
|
|
403
|
-
defineLayoutModule({ key: 'risk-summary' /* ... */ }); // hyphens
|
|
404
|
-
defineLayoutModule({ key: 'Risk_Summary' /* ... */ }); // uppercase
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
Correct:
|
|
408
|
-
|
|
409
|
-
```ts
|
|
410
|
-
defineLayoutModule({ key: 'risk_summary' /* ... */ });
|
|
411
|
-
```
|
|
412
|
-
|
|
413
426
|
`key` must match `^[a-z0-9_]+$` and be unique within the app — it's embedded verbatim into a
|
|
414
427
|
persisted module identity string and DOM ids. A bad charset fails zod validation at build time
|
|
415
428
|
(`closeBundle`); a duplicate throws
|
|
@@ -470,28 +483,11 @@ Source: `src/vite-plugin.ts` (`checkDeliveryExists`).
|
|
|
470
483
|
|
|
471
484
|
### [MEDIUM] `manifestPath` set to a directory instead of a file
|
|
472
485
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
Correct:
|
|
480
|
-
|
|
481
|
-
```ts
|
|
482
|
-
personaLayoutModules({ manifestPath: '/manifests/my-app.json', modules: [] });
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
`manifestPath` must include the full JSON file name — it defaults to `DEFAULT_MANIFEST_PATH`
|
|
486
|
-
(`/_layout_modules_manifest.json`). A leading `/` is optional and stripped either way.
|
|
487
|
-
|
|
488
|
-
Code outside `vite.config.ts` that needs the default path (e.g. a subapp server adding CORS on the
|
|
489
|
-
manifest route) should import the constant from the runtime-safe `.` entry rather than hardcoding it
|
|
490
|
-
— this avoids pulling in Vite:
|
|
491
|
-
|
|
492
|
-
```ts
|
|
493
|
-
import { DEFAULT_MANIFEST_PATH } from '@persona/layout-editor-remote-module';
|
|
494
|
-
```
|
|
486
|
+
Use a file path such as `/manifests/my-app.json`, not a directory such as `/manifests/`.
|
|
487
|
+
`manifestPath` defaults to `DEFAULT_MANIFEST_PATH` (`/_layout_modules_manifest.json`); a leading `/`
|
|
488
|
+
is optional and stripped either way. Code outside `vite.config.ts` that needs the default (e.g. a
|
|
489
|
+
subapp server adding CORS on the manifest route) should import `DEFAULT_MANIFEST_PATH` from the
|
|
490
|
+
runtime-safe `.` entry to avoid pulling in Vite.
|
|
495
491
|
|
|
496
492
|
Source: `src/vite-plugin.ts` (`PersonaLayoutModulesOptions.manifestPath` doc comment),
|
|
497
493
|
`src/manifest-path.ts`.
|