@persona/layout-editor-remote-module 0.8.1 → 0.10.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 +12 -0
- package/dist/bridge-frame.d.mts +2 -0
- package/dist/bridge-frame.mjs +58 -14
- package/dist/bridge-host.d.mts +8 -2
- package/dist/bridge-host.mjs +80 -12
- package/dist/{protocol-D_pdVZjs.mjs → protocol-Br3C6FM7.mjs} +7 -1
- package/package.json +1 -1
- package/skills/getting-started/SKILL.md +19 -15
package/README.md
CHANGED
|
@@ -71,12 +71,24 @@ import { RemoteModuleFrame } from '@persona/layout-editor-remote-module/bridge-h
|
|
|
71
71
|
data={moduleData}
|
|
72
72
|
context={moduleContext}
|
|
73
73
|
dynamicHeight
|
|
74
|
+
loadingFallback={<div>Loading module…</div>}
|
|
74
75
|
resolveDataPath={resolveDataPath}
|
|
75
76
|
/>;
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
The required `sandbox` value is chosen by the host.
|
|
79
80
|
|
|
81
|
+
`loadingFallback` is drawn over the iframe until the module has rendered. `LayoutModuleBridge`
|
|
82
|
+
advertises the `loaded` message automatically after its first render and waits for the first height
|
|
83
|
+
measurement before sending it when height observation is enabled. A module that needs a loading
|
|
84
|
+
state should render it on its first render; `LayoutModuleBridge`'s `fallback` is hidden under the
|
|
85
|
+
host overlay until then and remains available for render errors.
|
|
86
|
+
|
|
87
|
+
- `createModuleBridge` and `useRemoteModuleBridge` keep legacy behavior by default; direct bridge
|
|
88
|
+
users opt in with `signalsLoaded: true` and call `bridge.signalLoaded()` after rendering.
|
|
89
|
+
- Frames without the `loaded` capability are treated as legacy: `ready`, another bridge message, or
|
|
90
|
+
a silent-frame timeout after iframe load dismisses the overlay.
|
|
91
|
+
|
|
80
92
|
By default, the bridge accepts host origins matching `*.withpersona.com`,
|
|
81
93
|
`*.withpersona-staging.com`, `localhost`, and `127.0.0.1`. Add development or preview origins with
|
|
82
94
|
`allowedHostOrigins`, optionally composing the exported `personaDashboardHostOrigins` helper:
|
package/dist/bridge-frame.d.mts
CHANGED
|
@@ -12,6 +12,7 @@ type ModuleBridgeInitPayload = {
|
|
|
12
12
|
type CreateModuleBridgeOptions = {
|
|
13
13
|
allowedHostOrigins?: (string | RegExp)[] | ((origin: string) => boolean);
|
|
14
14
|
resolveTimeoutMs?: number;
|
|
15
|
+
signalsLoaded?: boolean;
|
|
15
16
|
};
|
|
16
17
|
type ModuleBridge = {
|
|
17
18
|
ready: () => Promise<ModuleBridgeInitPayload>;
|
|
@@ -21,6 +22,7 @@ type ModuleBridge = {
|
|
|
21
22
|
reportRenderError: (message: string) => void;
|
|
22
23
|
resolveDataPath: (dataPath: string, variableMap?: Record<string, string | number>) => Promise<Json>;
|
|
23
24
|
observeHeight: (element: Element) => () => void;
|
|
25
|
+
signalLoaded: () => void;
|
|
24
26
|
destroy: () => void;
|
|
25
27
|
};
|
|
26
28
|
//#endregion
|
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-Br3C6FM7.mjs";
|
|
2
2
|
import { Component, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -70,7 +70,7 @@ const measureDocumentHeight = (element) => {
|
|
|
70
70
|
}
|
|
71
71
|
return Math.ceil(rect.top + window.scrollY + rect.height + trailing);
|
|
72
72
|
};
|
|
73
|
-
function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins, resolveTimeoutMs = 3e4 } = {}) {
|
|
73
|
+
function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins, resolveTimeoutMs = 3e4, signalsLoaded = false } = {}) {
|
|
74
74
|
const isEmbedded = typeof window !== "undefined" && window.parent !== window;
|
|
75
75
|
const allowedOrigin = resolveAllowedHostOrigins(allowedHostOrigins);
|
|
76
76
|
if (!isEmbedded) {
|
|
@@ -88,6 +88,7 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
88
88
|
throw notEmbeddedError;
|
|
89
89
|
},
|
|
90
90
|
observeHeight: () => () => {},
|
|
91
|
+
signalLoaded: () => {},
|
|
91
92
|
destroy: () => {}
|
|
92
93
|
};
|
|
93
94
|
}
|
|
@@ -96,8 +97,12 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
96
97
|
let data = null;
|
|
97
98
|
let context = null;
|
|
98
99
|
let readyResolved = false;
|
|
100
|
+
let initialReadyPosted = false;
|
|
101
|
+
let loadedRequested = false;
|
|
102
|
+
let loadedPosted = false;
|
|
99
103
|
const listeners = /* @__PURE__ */ new Set();
|
|
100
104
|
const observers = /* @__PURE__ */ new Set();
|
|
105
|
+
const pendingHeightObservers = /* @__PURE__ */ new Set();
|
|
101
106
|
const pendingRequests = createPendingRequests();
|
|
102
107
|
const queuedRequests = /* @__PURE__ */ new Map();
|
|
103
108
|
let resolveReady;
|
|
@@ -107,7 +112,18 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
107
112
|
rejectReady = reject;
|
|
108
113
|
});
|
|
109
114
|
const post = (message) => {
|
|
110
|
-
if (
|
|
115
|
+
if (!destroyed && hostOrigin) {
|
|
116
|
+
window.parent.postMessage(message, hostOrigin);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
const flushLoaded = () => {
|
|
122
|
+
if (!destroyed && loadedRequested && !loadedPosted && hostOrigin && pendingHeightObservers.size === 0) loadedPosted = post({
|
|
123
|
+
source: SOURCE,
|
|
124
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
125
|
+
type: "loaded"
|
|
126
|
+
});
|
|
111
127
|
};
|
|
112
128
|
const flushQueuedRequests = () => {
|
|
113
129
|
if (!hostOrigin) return;
|
|
@@ -127,7 +143,6 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
127
143
|
if (!allowedOrigin(origin)) return false;
|
|
128
144
|
if (hostOrigin !== void 0 && hostOrigin !== origin) return false;
|
|
129
145
|
hostOrigin = origin;
|
|
130
|
-
flushQueuedRequests();
|
|
131
146
|
return true;
|
|
132
147
|
};
|
|
133
148
|
const notify = () => {
|
|
@@ -137,6 +152,17 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
137
152
|
if (destroyed || event.source !== window.parent) return;
|
|
138
153
|
const message = parseHostToModuleMessage(event.data);
|
|
139
154
|
if (!message || !pinHostOrigin(event.origin)) return;
|
|
155
|
+
if (message.type === "init" && signalsLoaded && !initialReadyPosted) {
|
|
156
|
+
initialReadyPosted = true;
|
|
157
|
+
post({
|
|
158
|
+
source: SOURCE,
|
|
159
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
160
|
+
type: "ready",
|
|
161
|
+
capabilities: { loaded: true }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
flushQueuedRequests();
|
|
165
|
+
flushLoaded();
|
|
140
166
|
switch (message.type) {
|
|
141
167
|
case "init":
|
|
142
168
|
data = message.data;
|
|
@@ -166,11 +192,15 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
166
192
|
};
|
|
167
193
|
window.addEventListener("message", onMessage);
|
|
168
194
|
const parentOrigin = candidateParentOrigin();
|
|
169
|
-
if (parentOrigin && allowedOrigin(parentOrigin))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
if (parentOrigin && allowedOrigin(parentOrigin)) {
|
|
196
|
+
window.parent.postMessage({
|
|
197
|
+
source: SOURCE,
|
|
198
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
199
|
+
type: "ready",
|
|
200
|
+
...signalsLoaded ? { capabilities: { loaded: true } } : {}
|
|
201
|
+
}, parentOrigin);
|
|
202
|
+
initialReadyPosted = true;
|
|
203
|
+
}
|
|
174
204
|
return {
|
|
175
205
|
ready: () => readyPromise,
|
|
176
206
|
getData: () => data,
|
|
@@ -202,17 +232,19 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
202
232
|
let lastHeight;
|
|
203
233
|
let frame;
|
|
204
234
|
let disconnected = false;
|
|
235
|
+
let observer;
|
|
205
236
|
const postHeight = (height) => {
|
|
206
237
|
if (disconnected || height === lastHeight) return;
|
|
207
|
-
|
|
208
|
-
post({
|
|
238
|
+
if (!post({
|
|
209
239
|
source: SOURCE,
|
|
210
240
|
protocolVersion: PROTOCOL_VERSION,
|
|
211
241
|
type: "set-height",
|
|
212
242
|
height
|
|
213
|
-
});
|
|
243
|
+
})) return;
|
|
244
|
+
lastHeight = height;
|
|
245
|
+
if (pendingHeightObservers.delete(observer)) flushLoaded();
|
|
214
246
|
};
|
|
215
|
-
|
|
247
|
+
observer = new ResizeObserver(() => {
|
|
216
248
|
if (frame !== void 0) return;
|
|
217
249
|
const callback = () => {
|
|
218
250
|
frame = void 0;
|
|
@@ -221,6 +253,7 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
221
253
|
frame = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(callback) : window.setTimeout(callback, 0);
|
|
222
254
|
});
|
|
223
255
|
observers.add(observer);
|
|
256
|
+
pendingHeightObservers.add(observer);
|
|
224
257
|
observer.observe(element);
|
|
225
258
|
const body = element.ownerDocument?.body;
|
|
226
259
|
if (body && body !== element) observer.observe(body);
|
|
@@ -232,14 +265,21 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
232
265
|
}
|
|
233
266
|
observer.disconnect();
|
|
234
267
|
observers.delete(observer);
|
|
268
|
+
if (pendingHeightObservers.delete(observer)) flushLoaded();
|
|
235
269
|
};
|
|
236
270
|
},
|
|
271
|
+
signalLoaded() {
|
|
272
|
+
if (destroyed || loadedRequested) return;
|
|
273
|
+
loadedRequested = true;
|
|
274
|
+
flushLoaded();
|
|
275
|
+
},
|
|
237
276
|
destroy() {
|
|
238
277
|
if (destroyed) return;
|
|
239
278
|
destroyed = true;
|
|
240
279
|
window.removeEventListener("message", onMessage);
|
|
241
280
|
for (const observer of observers) observer.disconnect();
|
|
242
281
|
observers.clear();
|
|
282
|
+
pendingHeightObservers.clear();
|
|
243
283
|
queuedRequests.clear();
|
|
244
284
|
const error = new BridgeDestroyedError();
|
|
245
285
|
pendingRequests.rejectAll(error);
|
|
@@ -320,7 +360,8 @@ var BridgeErrorBoundary = class extends Component {
|
|
|
320
360
|
function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeight = true, fallback = null, children }) {
|
|
321
361
|
const { bridge, context, data, ready, resolveDataPath } = useRemoteModuleBridge({
|
|
322
362
|
allowedHostOrigins,
|
|
323
|
-
resolveTimeoutMs
|
|
363
|
+
resolveTimeoutMs,
|
|
364
|
+
signalsLoaded: true
|
|
324
365
|
});
|
|
325
366
|
const rootRef = useRef(null);
|
|
326
367
|
useEffect(() => {
|
|
@@ -338,6 +379,9 @@ function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeigh
|
|
|
338
379
|
observeHeight,
|
|
339
380
|
ready
|
|
340
381
|
]);
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (ready && bridge) bridge.signalLoaded();
|
|
384
|
+
}, [bridge, ready]);
|
|
341
385
|
if (!ready || !bridge) return fallback;
|
|
342
386
|
return /* @__PURE__ */ jsx(BridgeErrorBoundary, {
|
|
343
387
|
bridge,
|
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,10 +30,14 @@ 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;
|
|
36
38
|
onRenderError?: (message: string) => void;
|
|
39
|
+
/** Rendered over the iframe until the module reports that it has loaded. */
|
|
40
|
+
loadingFallback?: ReactNode;
|
|
37
41
|
};
|
|
38
42
|
declare function RemoteModuleFrame({
|
|
39
43
|
documentUrl,
|
|
@@ -52,10 +56,12 @@ declare function RemoteModuleFrame({
|
|
|
52
56
|
minHeight,
|
|
53
57
|
className,
|
|
54
58
|
style,
|
|
59
|
+
iframeRef: externalIframeRef,
|
|
55
60
|
data,
|
|
56
61
|
context,
|
|
57
62
|
resolveDataPath,
|
|
58
|
-
onRenderError
|
|
63
|
+
onRenderError,
|
|
64
|
+
loadingFallback
|
|
59
65
|
}: RemoteModuleFrameProps): ReactNode;
|
|
60
66
|
//#endregion
|
|
61
67
|
export { type Json, type RemoteModuleContext, RemoteModuleFrame, type RemoteModuleFrameProps, type ResolveDataPath };
|
package/dist/bridge-host.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
1
|
+
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-Br3C6FM7.mjs";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/bridge/host/buildModuleSrc.ts
|
|
6
6
|
const reservedParams = new Set([
|
|
@@ -24,11 +24,15 @@ function buildModuleSrc(documentUrl, options = {}) {
|
|
|
24
24
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/bridge/host/createHostBridge.ts
|
|
27
|
-
function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDataPath, onHeight, onRenderError, initRetryMs = 250, initRetryTimeoutMs = 1e4 }) {
|
|
27
|
+
function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDataPath, onHeight, onRenderError, onLoaded, initRetryMs = 250, initRetryTimeoutMs = 1e4 }) {
|
|
28
28
|
assertNotSelfOrigin(moduleOrigin);
|
|
29
29
|
if (moduleOrigin === "*") throw new Error("Remote module origin must be explicit");
|
|
30
30
|
let destroyed = false;
|
|
31
31
|
let initialized = false;
|
|
32
|
+
let capable;
|
|
33
|
+
let loaded = false;
|
|
34
|
+
let fallbackTimer;
|
|
35
|
+
let fallbackTimerStarted = false;
|
|
32
36
|
let initRetryTimer;
|
|
33
37
|
const clearInitRetry = () => {
|
|
34
38
|
if (initRetryTimer !== void 0) {
|
|
@@ -36,6 +40,18 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
36
40
|
initRetryTimer = void 0;
|
|
37
41
|
}
|
|
38
42
|
};
|
|
43
|
+
const clearFallbackTimer = () => {
|
|
44
|
+
if (fallbackTimer !== void 0) {
|
|
45
|
+
clearTimeout(fallbackTimer);
|
|
46
|
+
fallbackTimer = void 0;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const markLoaded = () => {
|
|
50
|
+
if (loaded) return;
|
|
51
|
+
loaded = true;
|
|
52
|
+
clearFallbackTimer();
|
|
53
|
+
onLoaded?.();
|
|
54
|
+
};
|
|
39
55
|
const post = (message) => {
|
|
40
56
|
if (!destroyed) iframe.contentWindow?.postMessage(message, moduleOrigin);
|
|
41
57
|
};
|
|
@@ -72,17 +88,38 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
72
88
|
});
|
|
73
89
|
};
|
|
74
90
|
const onLoad = () => {
|
|
75
|
-
if (!destroyed)
|
|
91
|
+
if (!destroyed) {
|
|
92
|
+
if (!fallbackTimerStarted) {
|
|
93
|
+
fallbackTimerStarted = true;
|
|
94
|
+
fallbackTimer = setTimeout(() => {
|
|
95
|
+
fallbackTimer = void 0;
|
|
96
|
+
if (capable === void 0) markLoaded();
|
|
97
|
+
}, Math.max(initRetryTimeoutMs, 0) + 500);
|
|
98
|
+
}
|
|
99
|
+
startInitRetry();
|
|
100
|
+
}
|
|
76
101
|
};
|
|
77
102
|
const onMessage = (event) => {
|
|
78
103
|
if (destroyed || event.origin !== moduleOrigin || event.source !== iframe.contentWindow) return;
|
|
79
104
|
const message = parseModuleToHostMessage(event.data);
|
|
80
105
|
if (!message) return;
|
|
81
106
|
clearInitRetry();
|
|
107
|
+
if (message.type === "ready") {
|
|
108
|
+
if (capable === void 0) {
|
|
109
|
+
capable = message.capabilities?.loaded === true;
|
|
110
|
+
if (!capable) markLoaded();
|
|
111
|
+
}
|
|
112
|
+
} else if (message.type !== "loaded" && message.type !== "render-error" && capable === void 0) {
|
|
113
|
+
capable = false;
|
|
114
|
+
markLoaded();
|
|
115
|
+
}
|
|
82
116
|
switch (message.type) {
|
|
83
117
|
case "ready":
|
|
84
118
|
sendInit();
|
|
85
119
|
break;
|
|
120
|
+
case "loaded":
|
|
121
|
+
markLoaded();
|
|
122
|
+
break;
|
|
86
123
|
case "request-data":
|
|
87
124
|
sendData(getData(), message.requestId);
|
|
88
125
|
break;
|
|
@@ -90,6 +127,7 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
90
127
|
onHeight?.(message.height);
|
|
91
128
|
break;
|
|
92
129
|
case "render-error":
|
|
130
|
+
markLoaded();
|
|
93
131
|
onRenderError?.(message.message);
|
|
94
132
|
break;
|
|
95
133
|
case "resolve-data-path":
|
|
@@ -131,6 +169,7 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
131
169
|
if (destroyed) return;
|
|
132
170
|
destroyed = true;
|
|
133
171
|
clearInitRetry();
|
|
172
|
+
clearFallbackTimer();
|
|
134
173
|
window.removeEventListener("message", onMessage);
|
|
135
174
|
iframe.removeEventListener("load", onLoad);
|
|
136
175
|
}
|
|
@@ -145,14 +184,28 @@ function withDynamicHeight(context, dynamicHeight) {
|
|
|
145
184
|
dynamicHeight
|
|
146
185
|
};
|
|
147
186
|
}
|
|
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 }) {
|
|
187
|
+
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, loadingFallback }) {
|
|
149
188
|
const iframeRef = useRef(null);
|
|
150
189
|
const dataRef = useRef(data);
|
|
151
190
|
const contextRef = useRef(withDynamicHeight(context, dynamicHeight));
|
|
152
191
|
const onRenderErrorRef = useRef(onRenderError);
|
|
153
192
|
const resolveDataPathRef = useRef(resolveDataPath);
|
|
154
193
|
const bridgeRef = useRef(void 0);
|
|
194
|
+
const externalCleanupRef = useRef(void 0);
|
|
155
195
|
const [height, setHeight] = useState();
|
|
196
|
+
const [loadedSrc, setLoadedSrc] = useState();
|
|
197
|
+
const setRefs = useCallback((node) => {
|
|
198
|
+
iframeRef.current = node;
|
|
199
|
+
if (typeof externalIframeRef === "function") if (node) {
|
|
200
|
+
const cleanup = externalIframeRef(node);
|
|
201
|
+
externalCleanupRef.current = typeof cleanup === "function" ? cleanup : void 0;
|
|
202
|
+
} else if (externalCleanupRef.current) {
|
|
203
|
+
const cleanup = externalCleanupRef.current;
|
|
204
|
+
externalCleanupRef.current = void 0;
|
|
205
|
+
cleanup();
|
|
206
|
+
} else externalIframeRef(null);
|
|
207
|
+
else if (externalIframeRef) externalIframeRef.current = node;
|
|
208
|
+
}, [externalIframeRef]);
|
|
156
209
|
const hostContext = useMemo(() => withDynamicHeight(context, dynamicHeight), [context, dynamicHeight]);
|
|
157
210
|
dataRef.current = data;
|
|
158
211
|
contextRef.current = hostContext;
|
|
@@ -171,10 +224,10 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
171
224
|
locale
|
|
172
225
|
]);
|
|
173
226
|
useEffect(() => {
|
|
174
|
-
const iframe = iframeRef.current;
|
|
175
|
-
if (!iframe) return;
|
|
227
|
+
const iframe$1 = iframeRef.current;
|
|
228
|
+
if (!iframe$1) return;
|
|
176
229
|
const bridge = createHostBridge({
|
|
177
|
-
iframe,
|
|
230
|
+
iframe: iframe$1,
|
|
178
231
|
moduleOrigin,
|
|
179
232
|
getData: () => dataRef.current,
|
|
180
233
|
getContext: () => contextRef.current,
|
|
@@ -184,7 +237,8 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
184
237
|
onHeight: (nextHeight) => {
|
|
185
238
|
setHeight(nextHeight);
|
|
186
239
|
},
|
|
187
|
-
onRenderError: (message) => onRenderErrorRef.current?.(message)
|
|
240
|
+
onRenderError: (message) => onRenderErrorRef.current?.(message),
|
|
241
|
+
onLoaded: () => setLoadedSrc(src)
|
|
188
242
|
});
|
|
189
243
|
bridgeRef.current = bridge;
|
|
190
244
|
return () => {
|
|
@@ -204,8 +258,8 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
204
258
|
bridgeRef.current?.pushContext(hostContext);
|
|
205
259
|
}, [hostContext]);
|
|
206
260
|
const computedHeight = dynamicHeight && height !== void 0 ? `${Math.max(height + heightOffset, minHeight ?? 0)}px` : minHeight === void 0 ? "100%" : `${minHeight}px`;
|
|
207
|
-
|
|
208
|
-
ref:
|
|
261
|
+
const iframe = /* @__PURE__ */ jsx("iframe", {
|
|
262
|
+
ref: setRefs,
|
|
209
263
|
src,
|
|
210
264
|
title,
|
|
211
265
|
sandbox,
|
|
@@ -219,6 +273,20 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
219
273
|
border: "none"
|
|
220
274
|
}
|
|
221
275
|
}, src);
|
|
276
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
277
|
+
style: {
|
|
278
|
+
position: "relative",
|
|
279
|
+
width: "100%",
|
|
280
|
+
height: computedHeight === "100%" ? "100%" : void 0
|
|
281
|
+
},
|
|
282
|
+
children: [iframe, loadingFallback !== void 0 && !(loadedSrc === src) && /* @__PURE__ */ jsx("div", {
|
|
283
|
+
style: {
|
|
284
|
+
position: "absolute",
|
|
285
|
+
inset: 0
|
|
286
|
+
},
|
|
287
|
+
children: loadingFallback
|
|
288
|
+
})]
|
|
289
|
+
});
|
|
222
290
|
}
|
|
223
291
|
|
|
224
292
|
//#endregion
|
|
@@ -117,7 +117,12 @@ const requestIdSchema = z.string().min(1);
|
|
|
117
117
|
const variableMapSchema = z.record(z.string(), z.union([z.string(), z.number()]));
|
|
118
118
|
const readySchema = z.object({
|
|
119
119
|
...envelopeSchema,
|
|
120
|
-
type: z.literal("ready")
|
|
120
|
+
type: z.literal("ready"),
|
|
121
|
+
capabilities: z.object({ loaded: z.boolean().optional() }).optional()
|
|
122
|
+
});
|
|
123
|
+
const loadedSchema = z.object({
|
|
124
|
+
...envelopeSchema,
|
|
125
|
+
type: z.literal("loaded")
|
|
121
126
|
});
|
|
122
127
|
const requestDataSchema = z.object({
|
|
123
128
|
...envelopeSchema,
|
|
@@ -161,6 +166,7 @@ const remoteModuleContextSchema = z.object({
|
|
|
161
166
|
});
|
|
162
167
|
const moduleToHostMessageSchema = z.discriminatedUnion("type", [
|
|
163
168
|
readySchema,
|
|
169
|
+
loadedSchema,
|
|
164
170
|
requestDataSchema,
|
|
165
171
|
setHeightSchema,
|
|
166
172
|
renderErrorSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -6,11 +6,11 @@ description: >
|
|
|
6
6
|
document entry, add a zod configSchema and an RJSF uiConfigSchema (labels, field order,
|
|
7
7
|
DataSourceField pickers), and talk to the host from the iframe with LayoutModuleBridge (layout
|
|
8
8
|
data, host context such as layoutType / layoutObjectId / isReadOnly / dynamicHeight,
|
|
9
|
-
resolveDataPath, height). Activate when adding a layout module, integrating this
|
|
10
|
-
first time, wiring a module document to the dashboard host, or debugging a
|
|
11
|
-
build or validate (duplicate key, missing defaultSize entry, render path
|
|
12
|
-
never receives data from the host,
|
|
13
|
-
dynamic height.
|
|
9
|
+
resolveDataPath, height, loaded signal). Activate when adding a layout module, integrating this
|
|
10
|
+
package for the first time, wiring a module document to the dashboard host, or debugging a
|
|
11
|
+
manifest that fails to build or validate (duplicate key, missing defaultSize entry, render path
|
|
12
|
+
not found), a module that never receives data from the host, a host loading card that never
|
|
13
|
+
clears, or an iframe whose height collapses or keeps growing under dynamic height.
|
|
14
14
|
metadata:
|
|
15
15
|
type: core
|
|
16
16
|
library: '@persona/layout-editor-remote-module'
|
|
@@ -266,7 +266,7 @@ import { LayoutModuleBridge } from '@persona/layout-editor-remote-module/bridge-
|
|
|
266
266
|
|
|
267
267
|
export function App() {
|
|
268
268
|
return (
|
|
269
|
-
<LayoutModuleBridge
|
|
269
|
+
<LayoutModuleBridge>
|
|
270
270
|
{({ data, context, resolveDataPath }) => (
|
|
271
271
|
<RiskSummary data={data} context={context} resolveDataPath={resolveDataPath} />
|
|
272
272
|
)}
|
|
@@ -276,7 +276,9 @@ export function App() {
|
|
|
276
276
|
```
|
|
277
277
|
|
|
278
278
|
- `children` is only invoked once the host has sent the initial layout data — it never sees
|
|
279
|
-
`data: undefined`. Until then `fallback` (default `null`) is rendered
|
|
279
|
+
`data: undefined`. Until then `fallback` (default `null`) is rendered, hidden under the host's
|
|
280
|
+
loading card. After the first render the bridge posts `loaded` and the host removes the card, so
|
|
281
|
+
render any data-loading state from `children`.
|
|
280
282
|
- `data` is the module's persisted config as JSON, and re-renders whenever the host pushes an
|
|
281
283
|
update.
|
|
282
284
|
- `context` (`RemoteModuleContext`) is where the module is rendered — host-owned facts the module
|
|
@@ -291,7 +293,8 @@ export function App() {
|
|
|
291
293
|
borders, so the host can size the iframe (`observeHeight={false}` to opt out). The host uses the
|
|
292
294
|
reported height only when [`context.dynamicHeight`](#host-context) is true; otherwise, the iframe
|
|
293
295
|
keeps its grid size.
|
|
294
|
-
- A render error thrown inside `children` is caught, reported to the host, and `fallback` is shown
|
|
296
|
+
- A render error thrown inside `children` is caught, reported to the host, and `fallback` is shown
|
|
297
|
+
after the host loading overlay is dismissed.
|
|
295
298
|
- Mount exactly one `LayoutModuleBridge` per document.
|
|
296
299
|
|
|
297
300
|
Props: `fallback?: ReactNode`, `observeHeight?: boolean` (default `true`),
|
|
@@ -373,17 +376,18 @@ from there.
|
|
|
373
376
|
|
|
374
377
|
Source: `src/bridge/iframe/LayoutModuleBridge.tsx`.
|
|
375
378
|
|
|
376
|
-
### [MEDIUM]
|
|
379
|
+
### [MEDIUM] Host loading card stays visible forever when embedded
|
|
377
380
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
+
The host removes its loading card once `LayoutModuleBridge` has rendered `children`, so a card that
|
|
382
|
+
never clears usually means the handshake didn't complete. Check, in order: the document is really
|
|
383
|
+
cross-origin-embedded by a Persona host (the default `allowedHostOrigins` predicate rejects unknown
|
|
384
|
+
parents — pass your own only for a non-Persona dev host); the frame's
|
|
381
385
|
`@persona/layout-editor-remote-module` is 0.5.3 or later (a frame on 0.5.2 or older rejects an
|
|
382
386
|
`init` whose context has keys it doesn't know, e.g. `dynamicHeight`, sent by hosts on 0.8.0+, so
|
|
383
387
|
`children` never runs); and the host is rendering it through `RemoteModuleFrame` (bare `<iframe>`s
|
|
384
|
-
never send `init`).
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
never send `init`). For a direct `createModuleBridge` integration, also confirm that a bridge
|
|
389
|
+
created with `signalsLoaded: true` calls `signalLoaded()`. Opening the document directly in a tab is
|
|
390
|
+
_not_ embedded — `data` and `context` resolve to `null` and `children` renders.
|
|
387
391
|
|
|
388
392
|
Source: `src/bridge/iframe/createModuleBridge.ts`, `src/bridge/origin.ts`, `src/bridge/protocol.ts`.
|
|
389
393
|
|