@persona/layout-editor-remote-module 0.7.0 → 0.8.1
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 +14 -6
- package/dist/bridge-frame.d.mts +1 -1
- package/dist/bridge-frame.mjs +15 -7
- package/dist/bridge-host.d.mts +1 -1
- package/dist/bridge-host.mjs +13 -5
- package/dist/{protocol-Dr_M54Kt.mjs → protocol-D_pdVZjs.mjs} +2 -1
- package/dist/{protocol-BljV-Zhi.d.mts → protocol-Dqnl8JTs.d.mts} +1 -0
- 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';
|
|
@@ -115,6 +116,13 @@ cannot derive itself:
|
|
|
115
116
|
- `isReadOnly` — the host object is locked for edits
|
|
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
|
|
119
|
+
- `dynamicHeight` — whether the host sizes the iframe to the height the bridge reports
|
|
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.
|
|
118
126
|
|
|
119
127
|
`context` is sent with `init` and pushed again whenever it changes, just like `data`; inside the
|
|
120
128
|
iframe it arrives through `ready()`, `getContext()`, and the `context` render prop (all
|
|
@@ -135,10 +143,10 @@ The bridge's security model is intentionally narrow:
|
|
|
135
143
|
- The iframe derives the ready target from `ancestorOrigins` or the referrer, and the host retries
|
|
136
144
|
`init` until the frame answers.
|
|
137
145
|
- Messages use a versioned protocol that stays compatible with frames built against older releases
|
|
138
|
-
(from
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
142
150
|
- `extraParams` rejects reserved names and credential-like keys.
|
|
143
151
|
|
|
144
152
|
### Consuming a manifest
|
package/dist/bridge-frame.d.mts
CHANGED
package/dist/bridge-frame.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-
|
|
1
|
+
import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-D_pdVZjs.mjs";
|
|
2
2
|
import { Component, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -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
package/dist/bridge-host.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-
|
|
1
|
+
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-D_pdVZjs.mjs";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -139,16 +139,23 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
139
139
|
|
|
140
140
|
//#endregion
|
|
141
141
|
//#region src/bridge/host/RemoteModuleFrame.tsx
|
|
142
|
+
function withDynamicHeight(context, dynamicHeight) {
|
|
143
|
+
return {
|
|
144
|
+
...context,
|
|
145
|
+
dynamicHeight
|
|
146
|
+
};
|
|
147
|
+
}
|
|
142
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 }) {
|
|
143
149
|
const iframeRef = useRef(null);
|
|
144
150
|
const dataRef = useRef(data);
|
|
145
|
-
const contextRef = useRef(context);
|
|
151
|
+
const contextRef = useRef(withDynamicHeight(context, dynamicHeight));
|
|
146
152
|
const onRenderErrorRef = useRef(onRenderError);
|
|
147
153
|
const resolveDataPathRef = useRef(resolveDataPath);
|
|
148
154
|
const bridgeRef = useRef(void 0);
|
|
149
155
|
const [height, setHeight] = useState();
|
|
156
|
+
const hostContext = useMemo(() => withDynamicHeight(context, dynamicHeight), [context, dynamicHeight]);
|
|
150
157
|
dataRef.current = data;
|
|
151
|
-
contextRef.current =
|
|
158
|
+
contextRef.current = hostContext;
|
|
152
159
|
onRenderErrorRef.current = onRenderError;
|
|
153
160
|
resolveDataPathRef.current = resolveDataPath;
|
|
154
161
|
const src = useMemo(() => buildModuleSrc(documentUrl, {
|
|
@@ -194,8 +201,8 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
194
201
|
bridgeRef.current?.pushData(data);
|
|
195
202
|
}, [data]);
|
|
196
203
|
useEffect(() => {
|
|
197
|
-
bridgeRef.current?.pushContext(
|
|
198
|
-
}, [
|
|
204
|
+
bridgeRef.current?.pushContext(hostContext);
|
|
205
|
+
}, [hostContext]);
|
|
199
206
|
const computedHeight = dynamicHeight && height !== void 0 ? `${Math.max(height + heightOffset, minHeight ?? 0)}px` : minHeight === void 0 ? "100%" : `${minHeight}px`;
|
|
200
207
|
return /* @__PURE__ */ jsx("iframe", {
|
|
201
208
|
ref: iframeRef,
|
|
@@ -207,6 +214,7 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
207
214
|
style: {
|
|
208
215
|
...style,
|
|
209
216
|
height: computedHeight,
|
|
217
|
+
display: style?.display ?? "block",
|
|
210
218
|
width: "100%",
|
|
211
219
|
border: "none"
|
|
212
220
|
}
|
|
@@ -156,7 +156,8 @@ const remoteModuleContextSchema = z.object({
|
|
|
156
156
|
layoutObjectId: z.string().nullable(),
|
|
157
157
|
isReadOnly: z.boolean(),
|
|
158
158
|
isInEditor: z.boolean(),
|
|
159
|
-
isUsingStubData: z.boolean()
|
|
159
|
+
isUsingStubData: z.boolean(),
|
|
160
|
+
dynamicHeight: z.boolean().optional()
|
|
160
161
|
});
|
|
161
162
|
const moduleToHostMessageSchema = z.discriminatedUnion("type", [
|
|
162
163
|
readySchema,
|
|
@@ -20,6 +20,7 @@ declare const remoteModuleContextSchema: z.ZodObject<{
|
|
|
20
20
|
isReadOnly: z.ZodBoolean;
|
|
21
21
|
isInEditor: z.ZodBoolean;
|
|
22
22
|
isUsingStubData: z.ZodBoolean;
|
|
23
|
+
dynamicHeight: z.ZodOptional<z.ZodBoolean>;
|
|
23
24
|
}, z.core.$strip>;
|
|
24
25
|
type RemoteModuleContext = z.infer<typeof remoteModuleContextSchema>;
|
|
25
26
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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`.
|