@seed-ship/mcp-ui-solid 6.20.0 → 6.22.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/CHANGELOG.md +223 -0
- package/README.md +120 -3
- package/dist/components/ChartJSRenderer.cjs +244 -68
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts +9 -0
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +246 -70
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/ExpandableWrapper.cjs +9 -1
- package/dist/components/ExpandableWrapper.cjs.map +1 -1
- package/dist/components/ExpandableWrapper.d.ts.map +1 -1
- package/dist/components/ExpandableWrapper.js +9 -1
- package/dist/components/ExpandableWrapper.js.map +1 -1
- package/dist/components/IframeFallbackLink.cjs +37 -0
- package/dist/components/IframeFallbackLink.cjs.map +1 -0
- package/dist/components/IframeFallbackLink.d.ts +23 -0
- package/dist/components/IframeFallbackLink.d.ts.map +1 -0
- package/dist/components/IframeFallbackLink.js +37 -0
- package/dist/components/IframeFallbackLink.js.map +1 -0
- package/dist/components/LightboxOverlay.cjs +139 -16
- package/dist/components/LightboxOverlay.cjs.map +1 -1
- package/dist/components/LightboxOverlay.d.ts +6 -0
- package/dist/components/LightboxOverlay.d.ts.map +1 -1
- package/dist/components/LightboxOverlay.js +141 -18
- package/dist/components/LightboxOverlay.js.map +1 -1
- package/dist/components/UIResourceRenderer.cjs +150 -127
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +151 -128
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/VideoRenderer.cjs +45 -19
- package/dist/components/VideoRenderer.cjs.map +1 -1
- package/dist/components/VideoRenderer.d.ts.map +1 -1
- package/dist/components/VideoRenderer.js +45 -19
- package/dist/components/VideoRenderer.js.map +1 -1
- package/dist/context/MCPUIConfigContext.cjs +30 -0
- package/dist/context/MCPUIConfigContext.cjs.map +1 -0
- package/dist/context/MCPUIConfigContext.d.ts +173 -0
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -0
- package/dist/context/MCPUIConfigContext.js +30 -0
- package/dist/context/MCPUIConfigContext.js.map +1 -0
- package/dist/context/MCPUIStringsContext.cjs +8 -1
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +29 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +8 -1
- package/dist/context/MCPUIStringsContext.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/validation.cjs +11 -10
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts +20 -0
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +11 -10
- package/dist/services/validation.js.map +1 -1
- package/dist/utils/iframe-coep.cjs +30 -0
- package/dist/utils/iframe-coep.cjs.map +1 -0
- package/dist/utils/iframe-coep.d.ts +53 -0
- package/dist/utils/iframe-coep.d.ts.map +1 -0
- package/dist/utils/iframe-coep.js +30 -0
- package/dist/utils/iframe-coep.js.map +1 -0
- package/dist/utils/pan-zoom.cjs +312 -0
- package/dist/utils/pan-zoom.cjs.map +1 -0
- package/dist/utils/pan-zoom.d.ts +235 -0
- package/dist/utils/pan-zoom.d.ts.map +1 -0
- package/dist/utils/pan-zoom.js +312 -0
- package/dist/utils/pan-zoom.js.map +1 -0
- package/dist/validation.cjs +2 -0
- package/dist/validation.cjs.map +1 -1
- package/dist/validation.d.cts +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +3 -1
- package/package.json +5 -1
- package/src/components/ChartJSRenderer.tsx +396 -1
- package/src/components/ExpandableWrapper.tsx +26 -0
- package/src/components/IframeFallbackLink.tsx +57 -0
- package/src/components/LightboxOverlay.tsx +280 -4
- package/src/components/UIResourceRenderer.tsx +31 -4
- package/src/components/VideoRenderer.tsx +20 -0
- package/src/context/MCPUIConfigContext.tsx +202 -0
- package/src/context/MCPUIStringsContext.tsx +40 -0
- package/src/hooks/index.ts +13 -0
- package/src/index.ts +31 -0
- package/src/services/index.ts +1 -0
- package/src/services/validation.ts +38 -16
- package/src/solid-jsx-attrs.d.ts +24 -0
- package/src/utils/iframe-coep.ts +81 -0
- package/src/utils/pan-zoom.ts +779 -0
- package/src/validation.ts +4 -0
|
@@ -274,6 +274,30 @@ export interface MCPUIStrings {
|
|
|
274
274
|
/** `aria-label` of the lightbox "next image" button. */
|
|
275
275
|
lightboxNext?: string
|
|
276
276
|
|
|
277
|
+
// ── Zoom controls (lightbox, chart) ────────────────────────
|
|
278
|
+
/**
|
|
279
|
+
* `aria-label` / `title` of the "zoom in" button.
|
|
280
|
+
* @since 6.22.0
|
|
281
|
+
*/
|
|
282
|
+
zoomIn?: string
|
|
283
|
+
/**
|
|
284
|
+
* `aria-label` / `title` of the "zoom out" button.
|
|
285
|
+
* @since 6.22.0
|
|
286
|
+
*/
|
|
287
|
+
zoomOut?: string
|
|
288
|
+
/**
|
|
289
|
+
* `aria-label` / `title` of the "reset zoom" button — the one the chart
|
|
290
|
+
* toolbar only shows while the chart is actually zoomed.
|
|
291
|
+
* @since 6.22.0
|
|
292
|
+
*/
|
|
293
|
+
zoomReset?: string
|
|
294
|
+
/**
|
|
295
|
+
* Zoom-level readout next to the zoom buttons.
|
|
296
|
+
* Template — `{percent}` = the scale as a whole percentage (`100` at rest).
|
|
297
|
+
* @since 6.22.0
|
|
298
|
+
*/
|
|
299
|
+
zoomLevel?: string
|
|
300
|
+
|
|
277
301
|
// ── ModalRenderer ──────────────────────────────────────────
|
|
278
302
|
/** `aria-label` of the modal close button. */
|
|
279
303
|
modalClose?: string
|
|
@@ -323,6 +347,13 @@ export interface MCPUIStrings {
|
|
|
323
347
|
imageViewFullSize?: string
|
|
324
348
|
/** Fallback `title` of an iframe without one. */
|
|
325
349
|
iframeTitle?: string
|
|
350
|
+
/**
|
|
351
|
+
* Label of the "open in a new tab" link rendered under an embed
|
|
352
|
+
* (`IframeRenderer`, `VideoRenderer`). A COEP-blocked iframe fires no
|
|
353
|
+
* observable event, so this link is the only way out of a blank frame.
|
|
354
|
+
* @since 6.21.0
|
|
355
|
+
*/
|
|
356
|
+
iframeOpenInNewTab?: string
|
|
326
357
|
/** Fallback label of a link component without one. */
|
|
327
358
|
linkLabel?: string
|
|
328
359
|
/**
|
|
@@ -1085,6 +1116,15 @@ export const DEFAULT_MCPUI_STRINGS: Required<MCPUIStrings> = {
|
|
|
1085
1116
|
sizeBytes: '{size} B',
|
|
1086
1117
|
sizeKilobytes: '{size} KB',
|
|
1087
1118
|
sizeMegabytes: '{size} MB',
|
|
1119
|
+
|
|
1120
|
+
// ── 6.21.0 — iframe COEP fallback ──────────────────────────
|
|
1121
|
+
iframeOpenInNewTab: 'Open in a new tab',
|
|
1122
|
+
|
|
1123
|
+
// ── 6.22.0 — zoom / pan controls ───────────────────────────
|
|
1124
|
+
zoomIn: 'Zoom in',
|
|
1125
|
+
zoomOut: 'Zoom out',
|
|
1126
|
+
zoomReset: 'Reset zoom',
|
|
1127
|
+
zoomLevel: 'Zoom {percent}%',
|
|
1088
1128
|
}
|
|
1089
1129
|
|
|
1090
1130
|
/**
|
package/src/hooks/index.ts
CHANGED
|
@@ -59,6 +59,19 @@ export type {
|
|
|
59
59
|
UseAutocompleteReturn,
|
|
60
60
|
} from './useAutocomplete'
|
|
61
61
|
|
|
62
|
+
// Pan/zoom primitive (v6.22.0). It lives under `src/utils` because it is pure
|
|
63
|
+
// arithmetic plus two signals, but it is reactive and registers an onCleanup,
|
|
64
|
+
// so it is used exactly like a hook — hence this re-export on the `./hooks`
|
|
65
|
+
// subpath as well as on the root barrel.
|
|
66
|
+
export { createPanZoom } from '../utils/pan-zoom'
|
|
67
|
+
export type {
|
|
68
|
+
PanZoomOptions,
|
|
69
|
+
PanZoomController,
|
|
70
|
+
PanZoomState,
|
|
71
|
+
PanZoomPoint,
|
|
72
|
+
PanZoomCursor,
|
|
73
|
+
} from '../utils/pan-zoom'
|
|
74
|
+
|
|
62
75
|
// Data Validator hooks (v3.1.0 — anti-hallucination)
|
|
63
76
|
export { useDataValidator } from './useDataValidator'
|
|
64
77
|
export type {
|
package/src/index.ts
CHANGED
|
@@ -128,6 +128,22 @@ export type {
|
|
|
128
128
|
DuplicateMountReporter,
|
|
129
129
|
} from './utils/duplicate-mount-registry'
|
|
130
130
|
|
|
131
|
+
// Cursor-anchored zoom + drag-pan primitive (v6.22.0 — lightbox, expanded views)
|
|
132
|
+
export {
|
|
133
|
+
createPanZoom,
|
|
134
|
+
clampScale,
|
|
135
|
+
contentPointAt,
|
|
136
|
+
viewportPointOf,
|
|
137
|
+
zoomAtAnchor,
|
|
138
|
+
} from './utils/pan-zoom'
|
|
139
|
+
export type {
|
|
140
|
+
PanZoomOptions,
|
|
141
|
+
PanZoomController,
|
|
142
|
+
PanZoomState,
|
|
143
|
+
PanZoomPoint,
|
|
144
|
+
PanZoomCursor,
|
|
145
|
+
} from './utils/pan-zoom'
|
|
146
|
+
|
|
131
147
|
// Chrome i18n — library's own strings, EN defaults (v6.6.0 — D2/R4)
|
|
132
148
|
export {
|
|
133
149
|
MCPUIStringsProvider,
|
|
@@ -142,6 +158,18 @@ export type {
|
|
|
142
158
|
MCPUIStringsProviderProps,
|
|
143
159
|
} from './context/MCPUIStringsContext'
|
|
144
160
|
|
|
161
|
+
// Host rendering policy — iframe `credentialless` / COEP (v6.21.0)
|
|
162
|
+
export {
|
|
163
|
+
MCPUIConfigProvider,
|
|
164
|
+
MCPUIConfigContext,
|
|
165
|
+
useMCPUIConfig,
|
|
166
|
+
DEFAULT_MCPUI_CONFIG,
|
|
167
|
+
} from './context/MCPUIConfigContext'
|
|
168
|
+
export type {
|
|
169
|
+
MCPUIConfig,
|
|
170
|
+
MCPUIConfigProviderProps,
|
|
171
|
+
} from './context/MCPUIConfigContext'
|
|
172
|
+
|
|
145
173
|
// Telemetry sink (B.5 — v5.6.0)
|
|
146
174
|
export {
|
|
147
175
|
MCPUITelemetryProvider,
|
|
@@ -331,6 +359,9 @@ export {
|
|
|
331
359
|
validateLayout,
|
|
332
360
|
validateIframeDomain,
|
|
333
361
|
getIframeSandbox,
|
|
362
|
+
// v6.21.0 — the trusted-host test behind `getIframeSandbox` and the
|
|
363
|
+
// `credentialless` iframe attribute.
|
|
364
|
+
isTrustedIframeDomain,
|
|
334
365
|
DEFAULT_RESOURCE_LIMITS,
|
|
335
366
|
DEFAULT_IFRAME_DOMAINS,
|
|
336
367
|
TRUSTED_IFRAME_DOMAINS,
|
package/src/services/index.ts
CHANGED
|
@@ -689,6 +689,42 @@ export function validateIframeDomain(
|
|
|
689
689
|
}
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Is this URL's host a trusted iframe host?
|
|
694
|
+
*
|
|
695
|
+
* A host matches when it equals an entry of `TRUSTED_IFRAME_DOMAINS` (plus
|
|
696
|
+
* `options.customTrustedDomains`) or is a subdomain of one. Trusted hosts
|
|
697
|
+
* need their own cookies/storage to authenticate, which is why they receive
|
|
698
|
+
* `allow-same-origin` in `getIframeSandbox` and deliberately do NOT receive
|
|
699
|
+
* the `credentialless` iframe attribute (an authenticated embed loaded
|
|
700
|
+
* without its cookies only shows a login screen).
|
|
701
|
+
*
|
|
702
|
+
* An unparsable URL is never trusted.
|
|
703
|
+
*
|
|
704
|
+
* @param url - The iframe URL
|
|
705
|
+
* @param options - Optional custom trusted domains, merged over the defaults
|
|
706
|
+
* @returns `true` when the URL's host is trusted
|
|
707
|
+
* @since v6.21.0 — extracted from `getIframeSandbox`, identical matching.
|
|
708
|
+
*/
|
|
709
|
+
export function isTrustedIframeDomain(
|
|
710
|
+
url: string,
|
|
711
|
+
options?: { customTrustedDomains?: string[] }
|
|
712
|
+
): boolean {
|
|
713
|
+
try {
|
|
714
|
+
const domain = new URL(url).hostname;
|
|
715
|
+
const trustedList = options?.customTrustedDomains
|
|
716
|
+
? [...TRUSTED_IFRAME_DOMAINS, ...options.customTrustedDomains]
|
|
717
|
+
: TRUSTED_IFRAME_DOMAINS;
|
|
718
|
+
|
|
719
|
+
return trustedList.some(
|
|
720
|
+
(trusted) => domain === trusted || domain.endsWith(`.${trusted}`)
|
|
721
|
+
);
|
|
722
|
+
} catch {
|
|
723
|
+
// Invalid URL — never trusted.
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
692
728
|
/**
|
|
693
729
|
* Get the appropriate sandbox attribute for an iframe URL.
|
|
694
730
|
*
|
|
@@ -707,22 +743,8 @@ export function getIframeSandbox(
|
|
|
707
743
|
): string {
|
|
708
744
|
const baseSandbox = 'allow-scripts allow-popups';
|
|
709
745
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
let trustedList = TRUSTED_IFRAME_DOMAINS;
|
|
713
|
-
if (options?.customTrustedDomains) {
|
|
714
|
-
trustedList = [...TRUSTED_IFRAME_DOMAINS, ...options.customTrustedDomains];
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const isTrusted = trustedList.some(
|
|
718
|
-
(trusted) => domain === trusted || domain.endsWith(`.${trusted}`)
|
|
719
|
-
);
|
|
720
|
-
|
|
721
|
-
if (isTrusted) {
|
|
722
|
-
return `${baseSandbox} allow-same-origin allow-forms`;
|
|
723
|
-
}
|
|
724
|
-
} catch {
|
|
725
|
-
// Invalid URL — use restrictive sandbox
|
|
746
|
+
if (isTrustedIframeDomain(url, options)) {
|
|
747
|
+
return `${baseSandbox} allow-same-origin allow-forms`;
|
|
726
748
|
}
|
|
727
749
|
|
|
728
750
|
return baseSandbox;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solid JSX augmentation — `attr:credentialless` (v6.21.0).
|
|
3
|
+
*
|
|
4
|
+
* `credentialless` is listed in dom-expressions' `Properties` set, so the
|
|
5
|
+
* plain `credentialless={…}` form compiles to a JS property assignment
|
|
6
|
+
* (`el.credentialless = v`) and never reaches the DOM as an attribute in an
|
|
7
|
+
* engine that does not reflect it (jsdom, and any pre-110 browser). The
|
|
8
|
+
* `attr:` namespace forces `setAttribute` / `ssrAttribute` instead.
|
|
9
|
+
*
|
|
10
|
+
* Solid types `attr:*` only for keys declared in `JSX.ExplicitAttributes`,
|
|
11
|
+
* which ships empty — hence this augmentation. The value type is
|
|
12
|
+
* `true | undefined` on purpose: `undefined` removes the attribute, so the
|
|
13
|
+
* boolean attribute is never emitted as `credentialless="false"`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import 'solid-js'
|
|
17
|
+
|
|
18
|
+
declare module 'solid-js' {
|
|
19
|
+
namespace JSX {
|
|
20
|
+
interface ExplicitAttributes {
|
|
21
|
+
credentialless: true | undefined
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COEP helpers for the two iframes the library renders
|
|
3
|
+
* (`IframeRenderer` in `UIResourceRenderer.tsx`, and the YouTube / Vimeo
|
|
4
|
+
* embed in `VideoRenderer.tsx`).
|
|
5
|
+
*
|
|
6
|
+
* @since v6.21.0
|
|
7
|
+
*
|
|
8
|
+
* A host that serves `Cross-Origin-Embedder-Policy: credentialless` makes the
|
|
9
|
+
* browser refuse any cross-origin iframe whose own document sends no COEP
|
|
10
|
+
* header. The HTML attribute `credentialless` on the `<iframe>` lifts that,
|
|
11
|
+
* at the price of the embedded document losing access to the cookies and
|
|
12
|
+
* storage of its own origin — see `MCPUIConfigContext` for the policy.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createSignal, onMount, type Accessor } from 'solid-js'
|
|
16
|
+
import { isTrustedIframeDomain } from '../services/validation'
|
|
17
|
+
import type { MCPUIConfig } from '../context/MCPUIConfigContext'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Does this iframe get the boolean `credentialless` attribute?
|
|
21
|
+
*
|
|
22
|
+
* `'auto'` (the default) says yes only when the embed has nothing to lose:
|
|
23
|
+
* the host is not trusted AND the iframe already runs without cookies because
|
|
24
|
+
* its `sandbox` has no `allow-same-origin` (`sandboxedWithoutSameOrigin`).
|
|
25
|
+
* The video embed carries no sandbox, so in `'auto'` it keeps its provider
|
|
26
|
+
* cookies and a COEP host must opt in with `'always'`.
|
|
27
|
+
*
|
|
28
|
+
* An unparsable URL is not trusted, so it gets the attribute.
|
|
29
|
+
*
|
|
30
|
+
* Takes a RESOLVED config (`Required<MCPUIConfig>` — what `useMCPUIConfig()`
|
|
31
|
+
* and `DEFAULT_MCPUI_CONFIG` hand out). `MCPUIConfig`'s own fields are
|
|
32
|
+
* optional so new keys stay backward compatible, and reading an unresolved
|
|
33
|
+
* one here would silently re-implement the defaults next to their source.
|
|
34
|
+
*/
|
|
35
|
+
export function shouldSetCredentialless(
|
|
36
|
+
url: string,
|
|
37
|
+
config: Required<MCPUIConfig>,
|
|
38
|
+
options: { sandboxedWithoutSameOrigin: boolean }
|
|
39
|
+
): boolean {
|
|
40
|
+
const mode = config.iframeCredentialless
|
|
41
|
+
if (mode === 'always') return true
|
|
42
|
+
if (mode === 'never') return false
|
|
43
|
+
if (!options.sandboxedWithoutSameOrigin) return false
|
|
44
|
+
return !isTrustedIframeDomain(url, {
|
|
45
|
+
customTrustedDomains: config.customTrustedIframeDomains,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `window.crossOriginIsolated`, read **after mount** into a signal.
|
|
51
|
+
*
|
|
52
|
+
* Reading it during render would make the server emit markup the client
|
|
53
|
+
* cannot reproduce; the signal starts at `false` (the SSR value) and flips
|
|
54
|
+
* once mounted, so hydration always matches.
|
|
55
|
+
*/
|
|
56
|
+
export function createCrossOriginIsolated(): Accessor<boolean> {
|
|
57
|
+
const [isolated, setIsolated] = createSignal(false)
|
|
58
|
+
onMount(() => {
|
|
59
|
+
setIsolated(typeof window !== 'undefined' && window.crossOriginIsolated === true)
|
|
60
|
+
})
|
|
61
|
+
return isolated
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Does the "open in a new tab" link show under an embed?
|
|
66
|
+
*
|
|
67
|
+
* `'auto'` shows it only on a cross-origin-isolated page — exactly where COEP
|
|
68
|
+
* can block the frame, and where the blocking is silent (a refused
|
|
69
|
+
* cross-origin document fires no `error` event the parent can observe).
|
|
70
|
+
*
|
|
71
|
+
* Takes a resolved config, for the reason given on `shouldSetCredentialless`.
|
|
72
|
+
*/
|
|
73
|
+
export function shouldShowIframeFallbackLink(
|
|
74
|
+
config: Required<MCPUIConfig>,
|
|
75
|
+
crossOriginIsolated: boolean
|
|
76
|
+
): boolean {
|
|
77
|
+
const mode = config.iframeFallbackLink
|
|
78
|
+
if (mode === 'always') return true
|
|
79
|
+
if (mode === 'never') return false
|
|
80
|
+
return crossOriginIsolated
|
|
81
|
+
}
|