@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.
Files changed (103) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/README.md +120 -3
  3. package/dist/components/ChartJSRenderer.cjs +244 -68
  4. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  5. package/dist/components/ChartJSRenderer.d.ts +9 -0
  6. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  7. package/dist/components/ChartJSRenderer.js +246 -70
  8. package/dist/components/ChartJSRenderer.js.map +1 -1
  9. package/dist/components/ExpandableWrapper.cjs +9 -1
  10. package/dist/components/ExpandableWrapper.cjs.map +1 -1
  11. package/dist/components/ExpandableWrapper.d.ts.map +1 -1
  12. package/dist/components/ExpandableWrapper.js +9 -1
  13. package/dist/components/ExpandableWrapper.js.map +1 -1
  14. package/dist/components/IframeFallbackLink.cjs +37 -0
  15. package/dist/components/IframeFallbackLink.cjs.map +1 -0
  16. package/dist/components/IframeFallbackLink.d.ts +23 -0
  17. package/dist/components/IframeFallbackLink.d.ts.map +1 -0
  18. package/dist/components/IframeFallbackLink.js +37 -0
  19. package/dist/components/IframeFallbackLink.js.map +1 -0
  20. package/dist/components/LightboxOverlay.cjs +139 -16
  21. package/dist/components/LightboxOverlay.cjs.map +1 -1
  22. package/dist/components/LightboxOverlay.d.ts +6 -0
  23. package/dist/components/LightboxOverlay.d.ts.map +1 -1
  24. package/dist/components/LightboxOverlay.js +141 -18
  25. package/dist/components/LightboxOverlay.js.map +1 -1
  26. package/dist/components/UIResourceRenderer.cjs +150 -127
  27. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  28. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  29. package/dist/components/UIResourceRenderer.js +151 -128
  30. package/dist/components/UIResourceRenderer.js.map +1 -1
  31. package/dist/components/VideoRenderer.cjs +45 -19
  32. package/dist/components/VideoRenderer.cjs.map +1 -1
  33. package/dist/components/VideoRenderer.d.ts.map +1 -1
  34. package/dist/components/VideoRenderer.js +45 -19
  35. package/dist/components/VideoRenderer.js.map +1 -1
  36. package/dist/context/MCPUIConfigContext.cjs +30 -0
  37. package/dist/context/MCPUIConfigContext.cjs.map +1 -0
  38. package/dist/context/MCPUIConfigContext.d.ts +173 -0
  39. package/dist/context/MCPUIConfigContext.d.ts.map +1 -0
  40. package/dist/context/MCPUIConfigContext.js +30 -0
  41. package/dist/context/MCPUIConfigContext.js.map +1 -0
  42. package/dist/context/MCPUIStringsContext.cjs +8 -1
  43. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  44. package/dist/context/MCPUIStringsContext.d.ts +29 -0
  45. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  46. package/dist/context/MCPUIStringsContext.js +8 -1
  47. package/dist/context/MCPUIStringsContext.js.map +1 -1
  48. package/dist/hooks/index.d.ts +2 -0
  49. package/dist/hooks/index.d.ts.map +1 -1
  50. package/dist/hooks.cjs +2 -0
  51. package/dist/hooks.cjs.map +1 -1
  52. package/dist/hooks.js +2 -0
  53. package/dist/hooks.js.map +1 -1
  54. package/dist/index.cjs +12 -0
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/index.d.cts +5 -1
  57. package/dist/index.d.ts +5 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +14 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/services/index.d.ts +1 -1
  62. package/dist/services/index.d.ts.map +1 -1
  63. package/dist/services/validation.cjs +11 -10
  64. package/dist/services/validation.cjs.map +1 -1
  65. package/dist/services/validation.d.ts +20 -0
  66. package/dist/services/validation.d.ts.map +1 -1
  67. package/dist/services/validation.js +11 -10
  68. package/dist/services/validation.js.map +1 -1
  69. package/dist/utils/iframe-coep.cjs +30 -0
  70. package/dist/utils/iframe-coep.cjs.map +1 -0
  71. package/dist/utils/iframe-coep.d.ts +53 -0
  72. package/dist/utils/iframe-coep.d.ts.map +1 -0
  73. package/dist/utils/iframe-coep.js +30 -0
  74. package/dist/utils/iframe-coep.js.map +1 -0
  75. package/dist/utils/pan-zoom.cjs +312 -0
  76. package/dist/utils/pan-zoom.cjs.map +1 -0
  77. package/dist/utils/pan-zoom.d.ts +235 -0
  78. package/dist/utils/pan-zoom.d.ts.map +1 -0
  79. package/dist/utils/pan-zoom.js +312 -0
  80. package/dist/utils/pan-zoom.js.map +1 -0
  81. package/dist/validation.cjs +2 -0
  82. package/dist/validation.cjs.map +1 -1
  83. package/dist/validation.d.cts +1 -1
  84. package/dist/validation.d.ts +1 -1
  85. package/dist/validation.d.ts.map +1 -1
  86. package/dist/validation.js +3 -1
  87. package/package.json +5 -1
  88. package/src/components/ChartJSRenderer.tsx +396 -1
  89. package/src/components/ExpandableWrapper.tsx +26 -0
  90. package/src/components/IframeFallbackLink.tsx +57 -0
  91. package/src/components/LightboxOverlay.tsx +280 -4
  92. package/src/components/UIResourceRenderer.tsx +31 -4
  93. package/src/components/VideoRenderer.tsx +20 -0
  94. package/src/context/MCPUIConfigContext.tsx +202 -0
  95. package/src/context/MCPUIStringsContext.tsx +40 -0
  96. package/src/hooks/index.ts +13 -0
  97. package/src/index.ts +31 -0
  98. package/src/services/index.ts +1 -0
  99. package/src/services/validation.ts +38 -16
  100. package/src/solid-jsx-attrs.d.ts +24 -0
  101. package/src/utils/iframe-coep.ts +81 -0
  102. package/src/utils/pan-zoom.ts +779 -0
  103. 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
  /**
@@ -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,
@@ -9,6 +9,7 @@ export {
9
9
  validateLayout,
10
10
  validateIframeDomain,
11
11
  getIframeSandbox,
12
+ isTrustedIframeDomain,
12
13
  DEFAULT_RESOURCE_LIMITS,
13
14
  DEFAULT_IFRAME_DOMAINS,
14
15
  TRUSTED_IFRAME_DOMAINS,
@@ -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
- try {
711
- const domain = new URL(url).hostname;
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
+ }