@ggui-ai/iframe-runtime 0.15.0 → 0.17.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/dist/channel-transport.d.ts +2 -2
- package/dist/connection.d.ts +34 -12
- package/dist/connection.js +40 -12
- package/dist/events-polling.js +12 -0
- package/dist/host-context-emitter.d.ts +22 -1
- package/dist/host-context-emitter.js +37 -4
- package/dist/host-fonts.d.ts +10 -0
- package/dist/host-fonts.js +113 -0
- package/dist/host-palette-bridge.js +20 -15
- package/dist/iframe-runtime.js +69 -78
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/mcp-app-iframe-host.d.ts +15 -0
- package/dist/mcp-app-iframe-host.js +5 -1
- package/dist/meta-parse.d.ts +17 -0
- package/dist/meta-parse.js +30 -2
- package/dist/observability.d.ts +73 -1
- package/dist/react-renderer.d.ts +20 -36
- package/dist/react-renderer.js +80 -96
- package/dist/render-item.d.ts +12 -13
- package/dist/render-item.js +5 -0
- package/dist/runtime.d.ts +32 -39
- package/dist/runtime.js +116 -68
- package/dist/status-dom.js +7 -0
- package/dist/system-cards/NoCredentialsCard.js +13 -13
- package/dist/system-cards/ProtocolProbeCard.js +11 -11
- package/dist/system-cards/QrCode.js +1 -1
- package/dist/system-cards/index.js +2 -2
- package/dist/wire-config.d.ts +2 -2
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export { parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './m
|
|
|
37
37
|
export type { RendererBootFailureReason, RendererBootFailedMessage, } from './runtime.js';
|
|
38
38
|
export type { ProtocolError, ProtocolErrorEmitter, BootstrapFailureReason, } from './protocol-error.js';
|
|
39
39
|
export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
|
|
40
|
-
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
|
|
40
|
+
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, FontFaceBlockedEvent, AppThemeInvalidEvent, AppThemeMemberStrippedEvent, ThemeReinjectFailedEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
|
|
41
41
|
export { isObservabilityMessage, isRelayDeadTapEvent, isRelayIncapabilityEvent, postObservabilityToParent, } from './observability.js';
|
|
42
42
|
export type { LifecycleEmitter } from './lifecycle.js';
|
|
43
43
|
export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
|
|
@@ -50,6 +50,21 @@ export interface McpAppIframeMountOptions {
|
|
|
50
50
|
/** Optional locale; defaults to `navigator.language` when
|
|
51
51
|
* available. */
|
|
52
52
|
readonly locale?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The color mode this host paints around the card (ggui#987 §4: the
|
|
55
|
+
* embedding host owns runtime mode). Announced as
|
|
56
|
+
* `hostContext.theme`; the card's ladder, overlay and `color-scheme`
|
|
57
|
+
* follow it. Omit to announce nothing — the card falls back to the
|
|
58
|
+
* slice's stamped mode.
|
|
59
|
+
*/
|
|
60
|
+
readonly theme?: 'light' | 'dark';
|
|
61
|
+
/**
|
|
62
|
+
* `@font-face` rules the card may install (ggui#987 §5), announced
|
|
63
|
+
* as `hostContext.styles.css.fonts`. The card's CSP decides whether
|
|
64
|
+
* each `src` loads; a refused face is reported back as
|
|
65
|
+
* `font-face-blocked`.
|
|
66
|
+
*/
|
|
67
|
+
readonly fonts?: string;
|
|
53
68
|
}
|
|
54
69
|
export interface McpAppIframeMount {
|
|
55
70
|
/** Tear down — posts `ui/resource-teardown` then removes the iframe. */
|
|
@@ -113,7 +113,9 @@ export function mountMcpAppIframe(container, opts) {
|
|
|
113
113
|
// `hostCapabilities` + `hostContext`; the pre-App draft shape
|
|
114
114
|
// fails that gate and kills the embedded mount. Adapter
|
|
115
115
|
// boundary — `hostContext` carries `{locale,
|
|
116
|
-
// containerDimensions}`
|
|
116
|
+
// containerDimensions}` plus the host's OWN `theme` / fonts
|
|
117
|
+
// when the embedder passed them (ggui#987 §4/§5); never any
|
|
118
|
+
// outer ggui render state.
|
|
117
119
|
const requestedRaw = req.params?.['protocolVersion'];
|
|
118
120
|
const requested = typeof requestedRaw === 'string' && requestedRaw.length > 0
|
|
119
121
|
? requestedRaw
|
|
@@ -133,6 +135,8 @@ export function mountMcpAppIframe(container, opts) {
|
|
|
133
135
|
locale: opts.locale ??
|
|
134
136
|
(typeof navigator !== 'undefined' ? navigator.language : 'en-US'),
|
|
135
137
|
containerDimensions: dims,
|
|
138
|
+
...(opts.theme !== undefined ? { theme: opts.theme } : {}),
|
|
139
|
+
...(opts.fonts !== undefined ? { styles: { css: { fonts: opts.fonts } } } : {}),
|
|
136
140
|
},
|
|
137
141
|
};
|
|
138
142
|
response = { jsonrpc: '2.0', id, result };
|
package/dist/meta-parse.d.ts
CHANGED
|
@@ -16,6 +16,23 @@ import type { McpAppAiGguiMetaParseResult } from './types.js';
|
|
|
16
16
|
* has nothing to mount.
|
|
17
17
|
*/
|
|
18
18
|
export declare function validateMeta(meta: McpAppAiGguiRenderMeta): McpAppAiGguiMetaParseResult;
|
|
19
|
+
/**
|
|
20
|
+
* The wire parser refused the slice's `theme` (ggui#987 §3.4): report it
|
|
21
|
+
* to the embedding host as `app-theme-invalid` and mount without it. A
|
|
22
|
+
* write door should have refused this upstream — the event is the
|
|
23
|
+
* operator's pointer to the door that did not. Passed as
|
|
24
|
+
* `onInvalidTheme` on every `parseMcpAppAiGguiRenderMeta` call in this
|
|
25
|
+
* package.
|
|
26
|
+
*/
|
|
27
|
+
export declare function reportInvalidAppTheme(issues: readonly string[]): void;
|
|
28
|
+
/**
|
|
29
|
+
* The read door KEPT the slice's theme but stripped top-level members
|
|
30
|
+
* this release does not name (ggui#1093 belt, VERSION-POLICY §3.6):
|
|
31
|
+
* report the member names to the embedding host so a newer writer's
|
|
32
|
+
* member is visible, not swallowed. Passed as `onStrippedThemeMembers`
|
|
33
|
+
* on every `parseMcpAppAiGguiRenderMeta` call in this package.
|
|
34
|
+
*/
|
|
35
|
+
export declare function reportStrippedThemeMembers(keys: readonly string[]): void;
|
|
19
36
|
/**
|
|
20
37
|
* Parse the render slice from `globalThis.__GGUI_META__` — the
|
|
21
38
|
* synchronous self-contained shell delivery channel.
|
package/dist/meta-parse.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
*/
|
|
53
53
|
import { PUBLIC_ENV_APP_KEY_RE } from '@ggui-ai/protocol/wire';
|
|
54
54
|
import { parseMcpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
55
|
+
import { postObservabilityToParent } from './observability.js';
|
|
55
56
|
/**
|
|
56
57
|
* Type guard — true iff `value` is a non-null, non-array plain object.
|
|
57
58
|
*/
|
|
@@ -323,6 +324,27 @@ export function validateMeta(meta) {
|
|
|
323
324
|
return result;
|
|
324
325
|
return { ok: true, meta: result.meta };
|
|
325
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* The wire parser refused the slice's `theme` (ggui#987 §3.4): report it
|
|
329
|
+
* to the embedding host as `app-theme-invalid` and mount without it. A
|
|
330
|
+
* write door should have refused this upstream — the event is the
|
|
331
|
+
* operator's pointer to the door that did not. Passed as
|
|
332
|
+
* `onInvalidTheme` on every `parseMcpAppAiGguiRenderMeta` call in this
|
|
333
|
+
* package.
|
|
334
|
+
*/
|
|
335
|
+
export function reportInvalidAppTheme(issues) {
|
|
336
|
+
postObservabilityToParent({ kind: 'app-theme-invalid', issues: [...issues] });
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The read door KEPT the slice's theme but stripped top-level members
|
|
340
|
+
* this release does not name (ggui#1093 belt, VERSION-POLICY §3.6):
|
|
341
|
+
* report the member names to the embedding host so a newer writer's
|
|
342
|
+
* member is visible, not swallowed. Passed as `onStrippedThemeMembers`
|
|
343
|
+
* on every `parseMcpAppAiGguiRenderMeta` call in this package.
|
|
344
|
+
*/
|
|
345
|
+
export function reportStrippedThemeMembers(keys) {
|
|
346
|
+
postObservabilityToParent({ kind: 'app-theme-member-stripped', keys: [...keys] });
|
|
347
|
+
}
|
|
326
348
|
/**
|
|
327
349
|
* Parse the render slice from `globalThis.__GGUI_META__` — the
|
|
328
350
|
* synchronous self-contained shell delivery channel.
|
|
@@ -345,7 +367,10 @@ export function parseMetaFromGlobal() {
|
|
|
345
367
|
if (!isPlainObject(raw)) {
|
|
346
368
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
347
369
|
}
|
|
348
|
-
const parsed = parseMcpAppAiGguiRenderMeta(raw
|
|
370
|
+
const parsed = parseMcpAppAiGguiRenderMeta(raw, {
|
|
371
|
+
onInvalidTheme: reportInvalidAppTheme,
|
|
372
|
+
onStrippedThemeMembers: reportStrippedThemeMembers,
|
|
373
|
+
});
|
|
349
374
|
if (!parsed.ok) {
|
|
350
375
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
351
376
|
}
|
|
@@ -396,7 +421,10 @@ export function parseMetaFromToolResult(params) {
|
|
|
396
421
|
return { ok: false, reason: 'MISSING_META_GGUI_BOOTSTRAP' };
|
|
397
422
|
}
|
|
398
423
|
const meta = topMeta;
|
|
399
|
-
const parsed = parseMcpAppAiGguiRenderMeta(meta
|
|
424
|
+
const parsed = parseMcpAppAiGguiRenderMeta(meta, {
|
|
425
|
+
onInvalidTheme: reportInvalidAppTheme,
|
|
426
|
+
onStrippedThemeMembers: reportStrippedThemeMembers,
|
|
427
|
+
});
|
|
400
428
|
if (!parsed.ok) {
|
|
401
429
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
402
430
|
}
|
package/dist/observability.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { MCP_APP_OBSERVE_TYPE } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
|
33
33
|
*
|
|
34
34
|
* @public
|
|
35
35
|
*/
|
|
36
|
-
export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | UnknownObservabilityEvent;
|
|
36
|
+
export type ObservabilityEvent = SchemaVersionMismatchEvent | FontFaceBlockedEvent | AppThemeInvalidEvent | AppThemeMemberStrippedEvent | ThemeReinjectFailedEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | ComponentEmptyEvent | UnknownObservabilityEvent;
|
|
37
37
|
/**
|
|
38
38
|
* Fired when the protocol-version handshake rejects the connection.
|
|
39
39
|
* Parallel to `UpgradeRequiredError` — the host can choose to render
|
|
@@ -295,6 +295,25 @@ export interface RelayDeadTapEvent {
|
|
|
295
295
|
readonly sessionId?: string;
|
|
296
296
|
readonly appId?: string;
|
|
297
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* The mount painted NOTHING (ggui#1103): the renderer held no component,
|
|
300
|
+
* so the scope element carries only its stylesheet — a blank card, with no
|
|
301
|
+
* error boundary to catch anything (the boundary wraps a null child). The
|
|
302
|
+
* host is told which mount moment it was and why, because a card that
|
|
303
|
+
* renders nothing is invisible on every other channel: `reason: 'no-code'`
|
|
304
|
+
* is a payload that carried no component code, `'eval-failed'` a module
|
|
305
|
+
* that threw while evaluating. Names the moment only — never the code, the
|
|
306
|
+
* props or the error text, which stay in the operator-visible console line.
|
|
307
|
+
*
|
|
308
|
+
* @public
|
|
309
|
+
*/
|
|
310
|
+
export interface ComponentEmptyEvent {
|
|
311
|
+
readonly kind: 'component-empty';
|
|
312
|
+
/** Which lifecycle moment painted nothing. */
|
|
313
|
+
readonly where: 'mount' | 'update';
|
|
314
|
+
/** Why the renderer held no component. */
|
|
315
|
+
readonly reason: 'no-code' | 'eval-failed';
|
|
316
|
+
}
|
|
298
317
|
/**
|
|
299
318
|
* Catch-all branch for event kinds the host's typings don't recognize.
|
|
300
319
|
* Lets the union stay extensible without forcing protocol-version
|
|
@@ -303,6 +322,59 @@ export interface RelayDeadTapEvent {
|
|
|
303
322
|
*
|
|
304
323
|
* @public
|
|
305
324
|
*/
|
|
325
|
+
/**
|
|
326
|
+
* A host-delivered `@font-face` src the document's CSP refused
|
|
327
|
+
* (ggui#987 §5): the card installed the host's `styles.css.fonts`, the
|
|
328
|
+
* browser blocked the fetch under `font-src`, and the family will fall
|
|
329
|
+
* back to its stack. Reported ONCE per (family, host) per document —
|
|
330
|
+
* the operator's signal that the shell's `font-src` allowlist and the
|
|
331
|
+
* host's font origin disagree.
|
|
332
|
+
*
|
|
333
|
+
* @public
|
|
334
|
+
*/
|
|
335
|
+
export interface FontFaceBlockedEvent {
|
|
336
|
+
readonly kind: 'font-face-blocked';
|
|
337
|
+
/** The `font-family` the refused `@font-face` block declared. */
|
|
338
|
+
readonly family: string;
|
|
339
|
+
/** Host of the blocked `src` URL (`blockedURI`, origin-stripped by CSP). */
|
|
340
|
+
readonly host: string;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The render slice carried a `theme` the wire schema refused (ggui#987
|
|
344
|
+
* §3.4): the parser dropped it and the card painted the compiled ladder
|
|
345
|
+
* alone. `issues` are the schema's own messages — the operator's
|
|
346
|
+
* pointer to what the write door should have refused.
|
|
347
|
+
*
|
|
348
|
+
* @public
|
|
349
|
+
*/
|
|
350
|
+
export interface AppThemeInvalidEvent {
|
|
351
|
+
readonly kind: 'app-theme-invalid';
|
|
352
|
+
readonly issues: readonly string[];
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The render slice's `theme` carried top-level members THIS release does
|
|
356
|
+
* not name (ggui#1093 belt, VERSION-POLICY §3.6): the read door kept the
|
|
357
|
+
* theme and stripped them, and the card says which — a newer writer's
|
|
358
|
+
* member is never silently swallowed. `keys` are the member names only,
|
|
359
|
+
* never their content.
|
|
360
|
+
*
|
|
361
|
+
* @public
|
|
362
|
+
*/
|
|
363
|
+
export interface AppThemeMemberStrippedEvent {
|
|
364
|
+
readonly kind: 'app-theme-member-stripped';
|
|
365
|
+
readonly keys: readonly string[];
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The live re-injection on a host theme flip (ggui#987 §4) failed: the
|
|
369
|
+
* renderer's update rejected, so the card kept its previous overlay /
|
|
370
|
+
* ladder / `color-scheme`. `message` is the rejection's.
|
|
371
|
+
*
|
|
372
|
+
* @public
|
|
373
|
+
*/
|
|
374
|
+
export interface ThemeReinjectFailedEvent {
|
|
375
|
+
readonly kind: 'theme-reinject-failed';
|
|
376
|
+
readonly message: string;
|
|
377
|
+
}
|
|
306
378
|
export interface UnknownObservabilityEvent {
|
|
307
379
|
readonly kind: string & {};
|
|
308
380
|
readonly [field: string]: unknown;
|
package/dist/react-renderer.d.ts
CHANGED
|
@@ -63,44 +63,26 @@ export interface ReactRootMountOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
readonly themeMode?: 'light' | 'dark';
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
66
|
+
* The per-app theme (protocol's `AppTheme`, ggui#987 v2): BOTH modes'
|
|
67
|
+
* derived `--ggui-*` projections (`overlays.light` / `overlays.dark`),
|
|
68
|
+
* optional mode-agnostic `cssVariables`, per-mode `keyframes`, and
|
|
69
|
+
* the `frameless` flag. The renderer injects the overlay for the
|
|
70
|
+
* EFFECTIVE mode (`themeMode`, host-owned per §4; absent ⇒ light) —
|
|
71
|
+
* a mode switch re-paints from the retained other set with no
|
|
72
|
+
* network activity. Merged INTO the scoped in-body token block, the
|
|
73
|
+
* only placement that can recolor tree content (a `:root`-only
|
|
74
|
+
* append is cascade-dead for components — the scoped block sits
|
|
75
|
+
* later in document order and re-specifies every token; browser-
|
|
76
|
+
* verified, see the scoped-merge comment in `renderTree`). The
|
|
77
|
+
* `:root` append still happens, for body chrome only.
|
|
77
78
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
79
|
+
* Injection order (§3.3), one document-order cascade:
|
|
80
|
+
* compiled ladder (`themeId` ?? default) < `hostPalette` <
|
|
81
|
+
* `overlays[mode]` < `cssVariables` < `cssOverrides`, then
|
|
82
|
+
* `keyframes[mode]` and the frameless rule. Values are injection-
|
|
83
|
+
* safe by the wire schema — never re-validated here.
|
|
81
84
|
*/
|
|
82
|
-
readonly appTheme?:
|
|
83
|
-
readonly mode: 'light' | 'dark';
|
|
84
|
-
readonly cssVariables: Record<string, string>;
|
|
85
|
-
/**
|
|
86
|
-
* The REGISTERED base ladder, delivered over the wire (ggui#598-C
|
|
87
|
-
* runtime theme registration — see protocol's `appThemeSchema`).
|
|
88
|
-
* BOTH modes' resolved variable sets ride the envelope; when
|
|
89
|
-
* present, the mode-selected set REPLACES the compiled ladder
|
|
90
|
-
* block (`getScopedThemeCss`/`getScopedCssTokens`) at the base
|
|
91
|
-
* position of the #573 precedence above — delivery changes WHERE
|
|
92
|
-
* the ladder's bytes come from, never WHICH layer wins. Mode
|
|
93
|
-
* selection reuses the mount's already-resolved `themeMode` (one
|
|
94
|
-
* projection — no second mode resolution here), and a mode switch
|
|
95
|
-
* re-paints from the retained other set with no network activity.
|
|
96
|
-
* Values are injection-safe by the wire schema — never
|
|
97
|
-
* re-validated here; serialization is key-sorted for byte-stable
|
|
98
|
-
* output. `documentHash` is the registration's identity (joins a
|
|
99
|
-
* painted ladder to its registration record); the renderer
|
|
100
|
-
* carries it but exposes no observability surface for it today.
|
|
101
|
-
*/
|
|
102
|
-
readonly base?: NonNullable<AppTheme['base']>;
|
|
103
|
-
};
|
|
85
|
+
readonly appTheme?: AppTheme;
|
|
104
86
|
/**
|
|
105
87
|
* Host-announced palette, already translated onto `--ggui-*` tokens
|
|
106
88
|
* (see `host-palette-bridge.ts` — the spec `--color-*` vocabulary
|
|
@@ -113,6 +95,8 @@ export interface ReactRootMountOptions {
|
|
|
113
95
|
*/
|
|
114
96
|
readonly hostPalette?: Readonly<Record<string, string>>;
|
|
115
97
|
readonly cssOverrides?: string;
|
|
98
|
+
/** `fill` — the root is the whole canvas: no silhouette, fills the page (ggui#1041; from the host's `displayMode`). */
|
|
99
|
+
readonly fit?: 'fill';
|
|
116
100
|
readonly onError?: (error: Error) => void;
|
|
117
101
|
/**
|
|
118
102
|
* Children injected BETWEEN the mount DOM (scope + CSS) and the
|
package/dist/react-renderer.js
CHANGED
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import React, { Component, createElement, Fragment, } from 'react';
|
|
33
33
|
import { createRoot } from 'react-dom/client';
|
|
34
|
-
import {
|
|
34
|
+
import { postObservabilityToParent } from './observability.js';
|
|
35
|
+
import { stripMarkers, rewriteImports, composeThemeCss, } from '@ggui-ai/design/rendering';
|
|
35
36
|
import { hoistImports, loadModule, loadModuleInline, probeUrlModuleLoad, } from '@ggui-ai/design/module-loader';
|
|
36
37
|
/**
|
|
37
38
|
* Document-lifetime verdict: URL-scheme module loading (`blob:` main
|
|
@@ -66,11 +67,6 @@ function makeScopeClass() {
|
|
|
66
67
|
* bridge's sanitizer), so the string-join is safe — do NOT
|
|
67
68
|
* re-sanitize here.
|
|
68
69
|
*/
|
|
69
|
-
function toCssDecls(vars) {
|
|
70
|
-
return Object.entries(vars)
|
|
71
|
-
.map(([k, v]) => `${k}: ${v};`)
|
|
72
|
-
.join('');
|
|
73
|
-
}
|
|
74
70
|
// =============================================================================
|
|
75
71
|
// Error boundary — port of RCR's internal ErrorBoundary (L68–203).
|
|
76
72
|
// =============================================================================
|
|
@@ -109,7 +105,7 @@ class RcrErrorBoundary extends Component {
|
|
|
109
105
|
// The two fallback UIs match the host-SDK error boundary
|
|
110
106
|
// verbatim (same inline styles so operator-visible DOM is
|
|
111
107
|
// identical between the host-SDK and iframe-renderer paths).
|
|
112
|
-
// Colors inherit the themed body (`--ggui-color-
|
|
108
|
+
// Colors inherit the themed body (`--ggui-color-onContainer`) with
|
|
113
109
|
// opacity for hierarchy — hardcoded white-alpha text was
|
|
114
110
|
// illegible on light shells (#481); the boundary must stay
|
|
115
111
|
// readable on WHATEVER background the theme painted, including
|
|
@@ -126,7 +122,7 @@ class RcrErrorBoundary extends Component {
|
|
|
126
122
|
gap: 8,
|
|
127
123
|
fontFamily: FONT,
|
|
128
124
|
fontSize: 13,
|
|
129
|
-
color: 'var(--ggui-color-
|
|
125
|
+
color: 'var(--ggui-color-onContainer, inherit)',
|
|
130
126
|
opacity: 0.6,
|
|
131
127
|
},
|
|
132
128
|
}, createElement('div', {
|
|
@@ -151,7 +147,7 @@ class RcrErrorBoundary extends Component {
|
|
|
151
147
|
gap: 12,
|
|
152
148
|
textAlign: 'center',
|
|
153
149
|
fontFamily: FONT,
|
|
154
|
-
color: 'var(--ggui-color-
|
|
150
|
+
color: 'var(--ggui-color-onContainer, inherit)',
|
|
155
151
|
},
|
|
156
152
|
}, createElement('div', {
|
|
157
153
|
style: {
|
|
@@ -314,57 +310,29 @@ export async function mountReactRoot(container, opts) {
|
|
|
314
310
|
}
|
|
315
311
|
}
|
|
316
312
|
function renderTree(opts) {
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
})
|
|
341
|
-
: opts.themeId
|
|
342
|
-
? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
|
|
343
|
-
: getScopedCssTokens(scopeClass, opts.themeMode);
|
|
344
|
-
if (opts.hostPalette) {
|
|
345
|
-
// Host palette = the fallback layer (ggui#572, #573 ruling):
|
|
346
|
-
// after the base scoped block so it can repaint at all (the base
|
|
347
|
-
// re-specifies every token), BEFORE the appTheme overlay so the
|
|
348
|
-
// slice's stamped theme wins by document order. Keys + values
|
|
349
|
-
// arrive pre-sanitized from the host-palette bridge.
|
|
350
|
-
themeCss += `.${scopeClass}{${toCssDecls(opts.hostPalette)}}`;
|
|
351
|
-
}
|
|
352
|
-
if (opts.appTheme) {
|
|
353
|
-
const decls = toCssDecls(opts.appTheme.cssVariables);
|
|
354
|
-
// The overlay must be merged HERE, into the scoped style that
|
|
355
|
-
// mounts INSIDE the scope div — the `:root` append below is
|
|
356
|
-
// cascade-dead for tree content: the in-scope block sits later
|
|
357
|
-
// in document order than anything in `<head>`, so at equal
|
|
358
|
-
// specificity the base scoped tokens always win over a head-level
|
|
359
|
-
// override (browser-verified in rnd/gen-ui/beauty/experiments/001,
|
|
360
|
-
// the probe sequence that found this). Appended AFTER the base
|
|
361
|
-
// block + host palette and BEFORE `cssOverrides`, so: base <
|
|
362
|
-
// host palette < operator overlay < explicit overrides
|
|
363
|
-
// (ggui#573: slice wins, host fallback). Values validated
|
|
364
|
-
// injection-safe upstream (same guarantee the `:root` append
|
|
365
|
-
// cites).
|
|
366
|
-
themeCss += `.${scopeClass}{${decls}}`;
|
|
367
|
-
}
|
|
313
|
+
// The effective mode (ggui#987 §4: the host owns it; the runtime
|
|
314
|
+
// resolves it ONCE into `themeMode`). Absent means no opinion
|
|
315
|
+
// anywhere — the light ladder paints, and every mode-keyed layer
|
|
316
|
+
// below selects with the same `m`, so the ladder, the overlay, the
|
|
317
|
+
// keyframes and `color-scheme` can never disagree.
|
|
318
|
+
const m = opts.themeMode ?? 'light';
|
|
319
|
+
// ONE composition (`@ggui-ai/design/rendering` `composeThemeCss`): the
|
|
320
|
+
// scoped tree block — compiled ladder < host palette < overlays[m] <
|
|
321
|
+
// cssVariables < cssOverrides, then the mode's keyframes and the
|
|
322
|
+
// frameless rule. The `:root` chrome block below and an evaluator's
|
|
323
|
+
// page are the same function's other layers, so no two paints can
|
|
324
|
+
// disagree on a token (ggui#1020: a judge that composed its own
|
|
325
|
+
// scored an ink-on-ink hero readable).
|
|
326
|
+
const themeCss = composeThemeCss({
|
|
327
|
+
layer: 'tree',
|
|
328
|
+
scopeClass,
|
|
329
|
+
mode: m,
|
|
330
|
+
...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
|
|
331
|
+
...(opts.hostPalette !== undefined ? { hostPalette: opts.hostPalette } : {}),
|
|
332
|
+
...(opts.appTheme !== undefined ? { appTheme: opts.appTheme } : {}),
|
|
333
|
+
...(opts.cssOverrides !== undefined ? { cssOverrides: opts.cssOverrides } : {}),
|
|
334
|
+
...(opts.fit !== undefined ? { fit: opts.fit } : {}),
|
|
335
|
+
});
|
|
368
336
|
// Also inject theme CSS at `:root` on `document.head`. The scoped
|
|
369
337
|
// injection above isolates token resolution to the React tree, but
|
|
370
338
|
// the embedding shell's body styles (font-family, color, background)
|
|
@@ -375,37 +343,20 @@ export async function mountReactRoot(container, opts) {
|
|
|
375
343
|
// `ThemeProvider` so a Studio/Portal context with both runtime + RP
|
|
376
344
|
// active doesn't double-stack.
|
|
377
345
|
if (typeof document !== 'undefined') {
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// slice wins, host fallback). Keeps the embedding shell's
|
|
393
|
-
// body font/color/background coherent with the tree.
|
|
394
|
-
rootCss += `:root{${toCssDecls(opts.hostPalette)}}`;
|
|
395
|
-
}
|
|
396
|
-
if (opts.appTheme) {
|
|
397
|
-
const decls = toCssDecls(opts.appTheme.cssVariables);
|
|
398
|
-
// BODY-CHROME ONLY. This `:root` append themes the embedding
|
|
399
|
-
// shell's body styles (font/color/background outside the scope
|
|
400
|
-
// div). It does NOT reach tree content — the scoped in-body
|
|
401
|
-
// token block wins by document order (see the scoped merge in
|
|
402
|
-
// `renderTree`, and rnd/gen-ui/beauty/experiments/001 for the
|
|
403
|
-
// probe that falsified the old "inherits into the tree" claim).
|
|
404
|
-
// Values were validated injection-safe upstream (write-side
|
|
405
|
-
// `appThemeSchema` + the wire parser re-validates), so the
|
|
406
|
-
// string-join here is safe — do NOT re-sanitize.
|
|
407
|
-
rootCss += `:root{color-scheme:${opts.appTheme.mode};${decls}}`;
|
|
408
|
-
}
|
|
346
|
+
// Same ladder, same mode as the scoped block above — the
|
|
347
|
+
// standalone viewer's body chrome and its tree content resolve
|
|
348
|
+
// the same values.
|
|
349
|
+
// BODY-CHROME ONLY — the same composer's `chrome` layer: the ladder,
|
|
350
|
+
// the host palette, `color-scheme` + the app theme's variable layers
|
|
351
|
+
// (no keyframes, no overrides); tree content resolves the scoped block
|
|
352
|
+
// above, which wins by document order.
|
|
353
|
+
const rootCss = composeThemeCss({
|
|
354
|
+
layer: 'chrome',
|
|
355
|
+
mode: m,
|
|
356
|
+
...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
|
|
357
|
+
...(opts.hostPalette !== undefined ? { hostPalette: opts.hostPalette } : {}),
|
|
358
|
+
...(opts.appTheme !== undefined ? { appTheme: opts.appTheme } : {}),
|
|
359
|
+
});
|
|
409
360
|
const styleId = 'ggui-theme-vars';
|
|
410
361
|
let styleEl = document.getElementById(styleId);
|
|
411
362
|
if (!styleEl) {
|
|
@@ -426,17 +377,51 @@ export async function mountReactRoot(container, opts) {
|
|
|
426
377
|
// The scope `<div>` + inline `<style>` + error-boundary-wrapped
|
|
427
378
|
// children match RCR L376–383. `key={currentCode?.length}` mirrors
|
|
428
379
|
// RCR's "reset boundary when code changes" heuristic.
|
|
429
|
-
root.render(createElement('div', { className: scopeClass }, createElement('style', null,
|
|
380
|
+
root.render(createElement('div', { className: scopeClass }, createElement('style', null, themeCss), createElement(RcrErrorBoundary, {
|
|
430
381
|
key: currentCode?.length ?? 0,
|
|
431
382
|
...(opts.onError ? { onError: opts.onError } : {}),
|
|
432
383
|
}, createElement(Fragment, null, wrapped))));
|
|
433
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* The mount painted NOTHING (ggui#1103). `wrapped` is `null` whenever the
|
|
387
|
+
* renderer holds no component, so the scope element carries only its
|
|
388
|
+
* `<style>`: height 0, no text, and the error boundary never engages —
|
|
389
|
+
* it wraps a null child. Every such moment is reported on all three
|
|
390
|
+
* channels (an operator-visible console line, the caller's `onError`, the
|
|
391
|
+
* host's observability seam), because until now the two empty-code paths
|
|
392
|
+
* were silent on ALL of them and an empty card could not be told apart
|
|
393
|
+
* from a card whose module threw. Never carries the code or the props.
|
|
394
|
+
*/
|
|
395
|
+
function reportNoComponent(where, reason, error, onError) {
|
|
396
|
+
if (reason === 'eval-failed') {
|
|
397
|
+
// eslint-disable-next-line no-console -- operator-visible failure signal
|
|
398
|
+
console.error(`[ggui] mountReactRoot: the ${where} painted nothing (eval-failed) —`, error);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// A payload that carried no code is not an exception — nothing threw,
|
|
402
|
+
// and `onError` stays the caller's "the component failed" channel, so a
|
|
403
|
+
// host that paints an error panel does not paint one here. It is still
|
|
404
|
+
// operator-visible, and the host still hears it on the seam below.
|
|
405
|
+
// eslint-disable-next-line no-console -- operator-visible failure signal
|
|
406
|
+
console.warn(`[ggui] mountReactRoot: the ${where} painted nothing (no-code) — ${error.message}`);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
postObservabilityToParent({ kind: 'component-empty', where, reason });
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// The host seam is best-effort: a blocked postMessage must not turn a
|
|
413
|
+
// blank card into a thrown mount.
|
|
414
|
+
}
|
|
415
|
+
if (reason === 'eval-failed')
|
|
416
|
+
onError?.(error);
|
|
417
|
+
}
|
|
434
418
|
async function initialEvaluate() {
|
|
435
419
|
const code = currentOpts.render.componentCode ?? '';
|
|
436
420
|
if (code.trim().length === 0) {
|
|
437
421
|
currentComponent = null;
|
|
438
422
|
currentCode = null;
|
|
439
423
|
renderTree(currentOpts);
|
|
424
|
+
reportNoComponent('mount', 'no-code', new Error('mount carried no component code'), currentOpts.onError);
|
|
440
425
|
return;
|
|
441
426
|
}
|
|
442
427
|
try {
|
|
@@ -450,12 +435,10 @@ export async function mountReactRoot(container, opts) {
|
|
|
450
435
|
// not enough on its own — not every mount path wires one, so
|
|
451
436
|
// without this log a mount with a broken component renders a
|
|
452
437
|
// blank iframe and emits nothing.
|
|
453
|
-
// eslint-disable-next-line no-console -- operator-visible failure signal
|
|
454
|
-
console.error('[ggui] mountReactRoot: component evaluation failed —', e);
|
|
455
438
|
currentComponent = null;
|
|
456
439
|
currentCode = null;
|
|
457
440
|
renderTree(currentOpts);
|
|
458
|
-
currentOpts.onError
|
|
441
|
+
reportNoComponent('mount', 'eval-failed', e, currentOpts.onError);
|
|
459
442
|
}
|
|
460
443
|
}
|
|
461
444
|
await initialEvaluate();
|
|
@@ -476,6 +459,7 @@ export async function mountReactRoot(container, opts) {
|
|
|
476
459
|
currentComponent = null;
|
|
477
460
|
currentCode = null;
|
|
478
461
|
renderTree(currentOpts);
|
|
462
|
+
reportNoComponent('update', 'no-code', new Error('update carried no component code'), next.onError);
|
|
479
463
|
return;
|
|
480
464
|
}
|
|
481
465
|
try {
|
|
@@ -488,7 +472,7 @@ export async function mountReactRoot(container, opts) {
|
|
|
488
472
|
currentComponent = null;
|
|
489
473
|
currentCode = null;
|
|
490
474
|
renderTree(currentOpts);
|
|
491
|
-
next.onError
|
|
475
|
+
reportNoComponent('update', 'eval-failed', e, next.onError);
|
|
492
476
|
}
|
|
493
477
|
},
|
|
494
478
|
unmount() {
|
package/dist/render-item.d.ts
CHANGED
|
@@ -47,20 +47,19 @@ export interface RenderItemOptions {
|
|
|
47
47
|
*/
|
|
48
48
|
readonly themeMode?: 'light' | 'dark';
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* A structural subset of protocol's `AppTheme` (`name` is display-only).
|
|
54
|
-
* `base` is the delivered registered ladder (ggui#598-C) — forwarded
|
|
55
|
-
* verbatim; the renderer injects the mode-selected set in place of
|
|
56
|
-
* the compiled ladder block (see
|
|
57
|
-
* {@link ReactRootMountOptions.appTheme}).
|
|
50
|
+
* The mounted root fills its surface without a silhouette of its own (ggui#1041):
|
|
51
|
+
* set when the host's context says `displayMode: 'fullscreen'` — a canvas panel
|
|
52
|
+
* is the chrome. Absent for inline mounts.
|
|
58
53
|
*/
|
|
59
|
-
readonly
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
readonly fit?: 'fill';
|
|
55
|
+
/**
|
|
56
|
+
* The per-app theme (protocol's `AppTheme`, ggui#987 v2) forwarded
|
|
57
|
+
* verbatim to {@link ReactRootMountOptions.appTheme}: the renderer
|
|
58
|
+
* injects the overlay for the effective `themeMode` into the scoped
|
|
59
|
+
* block and at `:root`. Sourced from the bootstrap's
|
|
60
|
+
* `_meta["ai.ggui/render"].theme`.
|
|
61
|
+
*/
|
|
62
|
+
readonly appTheme?: AppTheme;
|
|
64
63
|
/**
|
|
65
64
|
* Host-announced palette, pre-mapped onto `--ggui-*` tokens by the
|
|
66
65
|
* host-palette bridge. Forwarded to
|
package/dist/render-item.js
CHANGED
|
@@ -183,6 +183,10 @@ export async function mountRender(container, opts) {
|
|
|
183
183
|
...(currentOpts.cssOverrides !== undefined
|
|
184
184
|
? { cssOverrides: currentOpts.cssOverrides }
|
|
185
185
|
: {}),
|
|
186
|
+
// The host's canvas is the chrome (ggui#1041): `fit` MUST cross this
|
|
187
|
+
// boundary like every other compose option — ggui#1073 shipped
|
|
188
|
+
// without it, and every served hello composed as an inline card.
|
|
189
|
+
...(currentOpts.fit !== undefined ? { fit: currentOpts.fit } : {}),
|
|
186
190
|
...(currentOpts.onError !== undefined ? { onError: currentOpts.onError } : {}),
|
|
187
191
|
renderWrapper: wrapInScopedProvider,
|
|
188
192
|
});
|
|
@@ -249,6 +253,7 @@ export async function mountRender(container, opts) {
|
|
|
249
253
|
...(next.appTheme !== undefined ? { appTheme: next.appTheme } : {}),
|
|
250
254
|
...(next.hostPalette !== undefined ? { hostPalette: next.hostPalette } : {}),
|
|
251
255
|
...(next.cssOverrides !== undefined ? { cssOverrides: next.cssOverrides } : {}),
|
|
256
|
+
...(next.fit !== undefined ? { fit: next.fit } : {}),
|
|
252
257
|
...(next.onError !== undefined ? { onError: next.onError } : {}),
|
|
253
258
|
renderWrapper: wrapInScopedProvider,
|
|
254
259
|
});
|