@seed-ship/mcp-ui-solid 6.21.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 +79 -0
- 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/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/context/MCPUIConfigContext.cjs +2 -1
- package/dist/context/MCPUIConfigContext.cjs.map +1 -1
- package/dist/context/MCPUIConfigContext.d.ts +60 -11
- package/dist/context/MCPUIConfigContext.d.ts.map +1 -1
- package/dist/context/MCPUIConfigContext.js +2 -1
- package/dist/context/MCPUIConfigContext.js.map +1 -1
- package/dist/context/MCPUIStringsContext.cjs +6 -1
- package/dist/context/MCPUIStringsContext.cjs.map +1 -1
- package/dist/context/MCPUIStringsContext.d.ts +22 -0
- package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
- package/dist/context/MCPUIStringsContext.js +6 -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 +6 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/iframe-coep.cjs.map +1 -1
- package/dist/utils/iframe-coep.d.ts +9 -2
- package/dist/utils/iframe-coep.d.ts.map +1 -1
- package/dist/utils/iframe-coep.js.map +1 -1
- 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/package.json +5 -1
- package/src/components/ChartJSRenderer.tsx +396 -1
- package/src/components/ExpandableWrapper.tsx +26 -0
- package/src/components/LightboxOverlay.tsx +280 -4
- package/src/context/MCPUIConfigContext.tsx +62 -11
- package/src/context/MCPUIStringsContext.tsx +30 -0
- package/src/hooks/index.ts +13 -0
- package/src/index.ts +16 -0
- package/src/utils/iframe-coep.ts +9 -2
- package/src/utils/pan-zoom.ts +779 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LightboxOverlay.js","sources":["../../src/components/LightboxOverlay.tsx"],"sourcesContent":["/**\n * LightboxOverlay - Fullscreen image viewer\n * Sprint 5: Media Components\n */\n\nimport { Component, Show, createEffect, onCleanup } from 'solid-js'\nimport { Portal } from 'solid-js/web'\nimport type { GalleryImage } from '../types'\nimport { safeUrl } from '../utils/safe-url'\nimport { useMCPUIStrings } from '../context/MCPUIStringsContext'\n\nexport interface LightboxOverlayProps {\n /**\n * Array of images to navigate through\n */\n images: GalleryImage[]\n\n /**\n * Currently selected image index (null when closed)\n */\n selectedIndex: number | null\n\n /**\n * Callback when lightbox should close\n */\n onClose: () => void\n\n /**\n * Callback when navigating to a different image\n */\n onNavigate: (index: number) => void\n}\n\nexport const LightboxOverlay: Component<LightboxOverlayProps> = (props) => {\n const strings = useMCPUIStrings()\n const isOpen = () => props.selectedIndex !== null\n const currentImage = () =>\n props.selectedIndex !== null ? props.images[props.selectedIndex] : null\n const canGoPrev = () => props.selectedIndex !== null && props.selectedIndex > 0\n const canGoNext = () =>\n props.selectedIndex !== null && props.selectedIndex < props.images.length - 1\n\n const handlePrev = () => {\n if (canGoPrev()) {\n props.onNavigate(props.selectedIndex! - 1)\n }\n }\n\n const handleNext = () => {\n if (canGoNext()) {\n props.onNavigate(props.selectedIndex! + 1)\n }\n }\n\n // Keyboard navigation\n createEffect(() => {\n if (!isOpen()) return\n\n const handleKeydown = (e: KeyboardEvent) => {\n switch (e.key) {\n case 'Escape':\n props.onClose()\n break\n case 'ArrowLeft':\n handlePrev()\n break\n case 'ArrowRight':\n handleNext()\n break\n }\n }\n\n document.addEventListener('keydown', handleKeydown)\n document.body.style.overflow = 'hidden'\n\n onCleanup(() => {\n document.removeEventListener('keydown', handleKeydown)\n document.body.style.overflow = ''\n })\n })\n\n return (\n <Show when={isOpen()}>\n <Portal>\n <div\n class=\"fixed inset-0 z-50 bg-black/90 flex items-center justify-center\"\n onClick={props.onClose}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={strings.lightboxLabel}\n >\n {/* Close button */}\n <button\n class=\"absolute top-4 right-4 text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={props.onClose}\n aria-label={strings.lightboxClose}\n >\n <svg class=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n\n {/* Previous button */}\n <Show when={canGoPrev()}>\n <button\n class=\"absolute left-4 top-1/2 -translate-y-1/2 text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={(e) => {\n e.stopPropagation()\n handlePrev()\n }}\n aria-label={strings.lightboxPrevious}\n >\n <svg class=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n </Show>\n\n {/* Next button */}\n <Show when={canGoNext()}>\n <button\n class=\"absolute right-4 top-1/2 -translate-y-1/2 text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={(e) => {\n e.stopPropagation()\n handleNext()\n }}\n aria-label={strings.lightboxNext}\n >\n <svg class=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </Show>\n\n {/* Image */}\n <img\n src={safeUrl(currentImage()?.url ?? '', { allowDataImage: true })}\n alt={currentImage()?.alt || ''}\n srcset={currentImage()?.srcset}\n sizes={currentImage()?.sizes}\n class=\"max-w-[90vw] max-h-[90vh] object-contain\"\n onClick={(e) => e.stopPropagation()}\n />\n\n {/* Caption */}\n <Show when={currentImage()?.caption}>\n <div class=\"absolute bottom-16 left-1/2 -translate-x-1/2 bg-black/60 text-white px-4 py-2 rounded-lg max-w-[80vw] text-center\">\n {currentImage()!.caption}\n </div>\n </Show>\n\n {/* Counter */}\n <div class=\"absolute top-4 left-4 text-white/80 bg-black/40 px-3 py-1 rounded-full text-sm\">\n {(props.selectedIndex ?? 0) + 1} / {props.images.length}\n </div>\n </div>\n </Portal>\n </Show>\n )\n}\n"],"names":["LightboxOverlay","props","strings","useMCPUIStrings","isOpen","selectedIndex","currentImage","images","canGoPrev","canGoNext","length","handlePrev","onNavigate","handleNext","createEffect","handleKeydown","e","key","onClose","document","addEventListener","body","style","overflow","onCleanup","removeEventListener","_$createComponent","Show","when","children","Portal","_el$","_$getNextElement","_tmpl$4","_el$2","firstChild","_el$11","nextSibling","_el$12","_co$3","_$getNextMarker","_el$13","_el$14","_co$4","_el$5","_el$15","_el$16","_co$5","_el$7","_el$9","_el$0","_co$","_el$8","_el$1","_el$10","_co$2","_$addEventListener","_$insert","_el$3","_tmpl$","$$click","stopPropagation","_$effect","_$setAttribute","lightboxPrevious","_$runHydrationEvents","_el$4","_tmpl$2","lightboxNext","caption","_el$6","_tmpl$3","_p$","_v$","lightboxLabel","_v$2","lightboxClose","_v$3","safeUrl","url","allowDataImage","_v$4","alt","_v$5","srcset","_v$6","sizes","t","a","o","i","n","undefined","_$delegateEvents"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LightboxOverlay.js","sources":["../../src/components/LightboxOverlay.tsx"],"sourcesContent":["/**\n * LightboxOverlay - Fullscreen image viewer\n * Sprint 5: Media Components\n *\n * v6.22.0 — zoom & pan. The overlay is a fullscreen `<Portal>`: it OWNS the\n * viewport, nothing scrolls behind it (the effect below locks `body`), so\n * hijacking the wheel here is legitimate in a way it would not be for a\n * component sitting inline in a chat feed. Zoom is therefore always on in the\n * lightbox and needs no host opt-in.\n */\n\nimport { Component, Show, createEffect, createMemo, onCleanup } from 'solid-js'\nimport { Portal } from 'solid-js/web'\nimport type { GalleryImage } from '../types'\nimport { safeUrl } from '../utils/safe-url'\nimport { createPanZoom, type PanZoomPoint } from '../utils/pan-zoom'\nimport { formatMCPUIString, useMCPUIStrings } from '../context/MCPUIStringsContext'\n\n/** Highest magnification of the lightbox — 8× the fitted size. */\nconst MAX_ZOOM = 8\n\n/** Ratio one press of a zoom button (or of `+` / `-`) applies. */\nconst ZOOM_BUTTON_FACTOR = 1.5\n\n/**\n * Scale a double-click jumps to. Deliberately modest: a double-click is a\n * coarse gesture, and the wheel is right there for the rest.\n */\nconst DOUBLE_CLICK_ZOOM = 2\n\nexport interface LightboxOverlayProps {\n /**\n * Array of images to navigate through\n */\n images: GalleryImage[]\n\n /**\n * Currently selected image index (null when closed)\n */\n selectedIndex: number | null\n\n /**\n * Callback when lightbox should close\n */\n onClose: () => void\n\n /**\n * Callback when navigating to a different image\n */\n onNavigate: (index: number) => void\n}\n\nexport const LightboxOverlay: Component<LightboxOverlayProps> = (props) => {\n const strings = useMCPUIStrings()\n let imageRef: HTMLImageElement | undefined\n\n /**\n * The handlers are bound on the BACKDROP, not on the `<img>`: a pan that\n * drags the image out from under the cursor must keep receiving the pointer\n * stream, and a wheel event over the black margin around a portrait image\n * must still zoom. `target` therefore points the maths back at the image,\n * which is the element that actually carries the transform.\n */\n const panZoom = createPanZoom({ min: 1, max: MAX_ZOOM, target: () => imageRef })\n\n const isOpen = () => props.selectedIndex !== null\n const currentImage = () =>\n props.selectedIndex !== null ? props.images[props.selectedIndex] : null\n const canGoPrev = () => props.selectedIndex !== null && props.selectedIndex > 0\n const canGoNext = () =>\n props.selectedIndex !== null && props.selectedIndex < props.images.length - 1\n\n const handlePrev = () => {\n if (canGoPrev()) {\n props.onNavigate(props.selectedIndex! - 1)\n }\n }\n\n const handleNext = () => {\n if (canGoNext()) {\n props.onNavigate(props.selectedIndex! + 1)\n }\n }\n\n /**\n * Identity of the picture on screen, as a VALUE rather than as an object\n * reference — `null` while the overlay is closed.\n *\n * The index alone does not name a picture. A host that replaces `images`\n * under a held `selectedIndex` — a gallery whose list streams in, a refilter,\n * a re-render that rebuilds the array — puts a different photo on screen\n * without the index ever moving. `url` and `srcset` are what the `<img>`\n * below actually resolves, so together with the index they are what \"the\n * displayed image changed\" has to mean. (`sizes` is left out on purpose: it\n * re-picks among the candidates of the SAME picture, so it changes the\n * resolution fetched, never the subject the zoom is framing.)\n *\n * A memo, and a string, so the comparison is by value: `ImageGalleryRenderer`\n * passes `params()?.images || []`, a fresh array on every parent update, and\n * re-reading that must not by itself count as a new picture — it would throw\n * away a zoom the user is in the middle of. The index stays in the key so\n * that navigating between two entries sharing a URL is still the clean slate\n * the arrows promise.\n */\n const displayedImage = createMemo(() => {\n if (props.selectedIndex === null) return null\n const image = props.images[props.selectedIndex]\n return JSON.stringify([props.selectedIndex, image?.url ?? null, image?.srcset ?? null])\n })\n\n /**\n * Every image opens at its fitted size. Without this the transform left over\n * from the previous picture would be applied to the next one — a 6× zoom on\n * a corner of a portrait photo, carried onto a landscape one, shows black.\n * It also settles the \"arrows vs. panning\" question: navigating away is\n * always a clean slate rather than a frame half-belonging to two images.\n *\n * The dependency is `displayedImage()` and not `selectedIndex`, for the same\n * reason: a swapped-out `images` array reaches the user as exactly that black\n * frame — the old picture's scale and offset applied to a new photo — with no\n * navigation to blame it on.\n *\n * `reset()` reads its state through `untrack`, so this effect subscribes to\n * that one memo and never re-runs on its own write. The scale and offset it\n * writes are absent from the memo for the same reason: panning must not\n * reset the pan.\n *\n * `cancelGesture()` comes first, and matters most on the CLOSE (`null`): the\n * pan handlers live on the backdrop, inside the `<Show>` below, but the\n * controller belongs to this component — which `ImageGalleryRenderer` mounts\n * once and never unmounts. Close the overlay with a button still held\n * (Escape mid-drag) and the backdrop is detached before its `pointerup`,\n * which is then delivered to whatever is underneath and never to these\n * handlers. Nothing else would end that gesture: `reset()` only writes the\n * transform, and `onCleanup` belongs to an owner that is still alive. The\n * overlay would reopen stuck at `cursor: grabbing`, with `isPanning()` true\n * and the arrow keys dead behind their pan guards.\n */\n createEffect(() => {\n displayedImage()\n panZoom.cancelGesture()\n panZoom.reset()\n })\n\n /**\n * Is the keystroke going into something the user is typing in?\n *\n * The overlay binds its shortcuts on `document`, so it hears every key in\n * the page — and it does not trap focus, so the host's own composer can keep\n * it while the lightbox is open (`Tab` from the thumbnail that opened the\n * overlay walks on into the host, since the `<Portal>` content is appended\n * after it). The zoom keys are `-`, `+`, `=` and `0`: characters. Calling\n * `preventDefault()` on them without this check silently eats them out of\n * the host's text field — typing a date would zoom the photo instead.\n *\n * `Escape` stays global, as it was before the zoom keys existed: it produces\n * no character, and dismissing the modal is what it is for.\n */\n const isEditableTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false\n if (target.isContentEditable) return true\n return ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)\n }\n\n // Keyboard navigation\n createEffect(() => {\n if (!isOpen()) return\n\n const handleKeydown = (e: KeyboardEvent) => {\n // Ctrl/Cmd + `+` / `-` / `0` is the browser's own page zoom; the overlay\n // must not swallow it.\n const isZoomChord = e.ctrlKey || e.metaKey\n\n // A field the user is typing in owns its keys — all of them but the one\n // that dismisses a modal.\n if (e.key !== 'Escape' && isEditableTarget(e.target)) return\n\n switch (e.key) {\n case 'Escape':\n props.onClose()\n break\n case 'ArrowLeft':\n // Mid-drag the arrows would move the picture the user is holding.\n if (panZoom.isPanning()) break\n handlePrev()\n break\n case 'ArrowRight':\n if (panZoom.isPanning()) break\n handleNext()\n break\n case '+':\n case '=':\n // `=` is `+` unshifted — the key people actually press.\n if (isZoomChord) break\n e.preventDefault()\n panZoom.zoomBy(ZOOM_BUTTON_FACTOR)\n break\n case '-':\n if (isZoomChord) break\n e.preventDefault()\n panZoom.zoomBy(1 / ZOOM_BUTTON_FACTOR)\n break\n case '0':\n if (isZoomChord) break\n e.preventDefault()\n panZoom.reset()\n break\n }\n }\n\n document.addEventListener('keydown', handleKeydown)\n document.body.style.overflow = 'hidden'\n\n onCleanup(() => {\n document.removeEventListener('keydown', handleKeydown)\n document.body.style.overflow = ''\n })\n })\n\n /**\n * Closing on a backdrop click is the behaviour users expect — but a pan\n * releases the pointer over the backdrop as often as not, and the browser\n * then fires a `click` on the common ancestor of the press and the release.\n * `wasDragged()` is exactly that distinction: it stays true from the moment\n * the pointer travelled more than a few pixels until the next `pointerdown`,\n * so the click that closes the overlay is only ever a click that did not move.\n */\n const handleBackdropClick = () => {\n if (panZoom.wasDragged()) return\n props.onClose()\n }\n\n /**\n * The chrome (close, prev/next, the zoom toolbar) sits on top of the pan\n * surface. Pressing a button must not open a gesture: while zoomed\n * `onPointerDown` calls `preventDefault()`, which costs the button its focus,\n * and a hand that shakes on the way to \"next\" would pan the image instead.\n */\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target\n if (target instanceof Element && target.closest('button')) return\n panZoom.onPointerDown(event)\n }\n\n /**\n * Viewport point of a mouse event, in the space `zoomTo` anchors on: client\n * pixels measured from the image's UNTRANSFORMED top-left corner. That corner\n * is not directly observable — `getBoundingClientRect()` already includes the\n * transform — but with `transform-origin: 0 0` the rendered left edge is\n * exactly `corner + offset`, so subtracting the current offset recovers it\n * whatever the flex centring did.\n *\n * `createPanZoom` computes the same point for the wheel but keeps it private;\n * this is the double-click's copy of it.\n */\n const anchorOf = (event: MouseEvent): PanZoomPoint | undefined => {\n if (!imageRef) return undefined\n const rect = imageRef.getBoundingClientRect()\n const offset = panZoom.offset()\n return {\n x: event.clientX - (rect.left - offset.x),\n y: event.clientY - (rect.top - offset.y),\n }\n }\n\n /** Double-click toggles: in at the spot that was double-clicked, or back out. */\n const handleDoubleClick = (event: MouseEvent) => {\n event.stopPropagation()\n if (panZoom.isZoomed()) {\n panZoom.reset()\n } else {\n panZoom.zoomTo(DOUBLE_CLICK_ZOOM, anchorOf(event))\n }\n }\n\n return (\n <Show when={isOpen()}>\n <Portal>\n <div\n class=\"fixed inset-0 z-50 bg-black/90 flex items-center justify-center\"\n style={panZoom.surfaceStyle()}\n onClick={handleBackdropClick}\n onWheel={panZoom.onWheel}\n onPointerDown={handlePointerDown}\n onPointerMove={panZoom.onPointerMove}\n onPointerUp={panZoom.onPointerUp}\n onPointerCancel={panZoom.onPointerCancel}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={strings.lightboxLabel}\n >\n {/* Close button */}\n <button\n class=\"absolute top-4 right-4 text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={props.onClose}\n aria-label={strings.lightboxClose}\n >\n <svg class=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n\n {/* Previous button */}\n <Show when={canGoPrev()}>\n <button\n class=\"absolute left-4 top-1/2 -translate-y-1/2 text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={(e) => {\n e.stopPropagation()\n handlePrev()\n }}\n aria-label={strings.lightboxPrevious}\n >\n <svg class=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n </Show>\n\n {/* Next button */}\n <Show when={canGoNext()}>\n <button\n class=\"absolute right-4 top-1/2 -translate-y-1/2 text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={(e) => {\n e.stopPropagation()\n handleNext()\n }}\n aria-label={strings.lightboxNext}\n >\n <svg class=\"w-8 h-8\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </Show>\n\n {/* Image — `max-w/max-h` fit it, the transform zooms it from there */}\n <img\n ref={imageRef}\n src={safeUrl(currentImage()?.url ?? '', { allowDataImage: true })}\n alt={currentImage()?.alt || ''}\n srcset={currentImage()?.srcset}\n sizes={currentImage()?.sizes}\n class=\"max-w-[90vw] max-h-[90vh] object-contain\"\n style={panZoom.contentStyle()}\n draggable={false}\n onClick={(e) => e.stopPropagation()}\n onDblClick={handleDoubleClick}\n />\n\n {/* Caption */}\n <Show when={currentImage()?.caption}>\n <div class=\"absolute bottom-16 left-1/2 -translate-x-1/2 bg-black/60 text-white px-4 py-2 rounded-lg max-w-[80vw] text-center\">\n {currentImage()!.caption}\n </div>\n </Show>\n\n {/* Counter */}\n <div class=\"absolute top-4 left-4 text-white/80 bg-black/40 px-3 py-1 rounded-full text-sm\">\n {(props.selectedIndex ?? 0) + 1} / {props.images.length}\n </div>\n\n {/* Zoom controls — the pointer-only gestures made reachable by\n keyboard and touch. The wrapper swallows the click so the\n backdrop's close handler never sees it. */}\n <div\n class=\"absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-1 bg-black/50 rounded-full px-2 py-1\"\n onClick={(e) => e.stopPropagation()}\n >\n <button\n class=\"text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors disabled:opacity-40 disabled:hover:bg-transparent\"\n onClick={() => panZoom.zoomBy(1 / ZOOM_BUTTON_FACTOR)}\n disabled={panZoom.scale() <= panZoom.min}\n aria-label={strings.zoomOut}\n title={strings.zoomOut}\n type=\"button\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6\"\n />\n </svg>\n </button>\n\n <span\n class=\"text-white/80 text-xs tabular-nums text-center min-w-[4rem]\"\n aria-live=\"polite\"\n >\n {formatMCPUIString(strings.zoomLevel, { percent: panZoom.percent() })}\n </span>\n\n <button\n class=\"text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors disabled:opacity-40 disabled:hover:bg-transparent\"\n onClick={() => panZoom.zoomBy(ZOOM_BUTTON_FACTOR)}\n disabled={panZoom.scale() >= panZoom.max}\n aria-label={strings.zoomIn}\n title={strings.zoomIn}\n type=\"button\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6\"\n />\n </svg>\n </button>\n\n {/* Only while there is something to reset — at 100% it would be a\n button that does nothing. */}\n <Show when={panZoom.isZoomed()}>\n <button\n class=\"text-white/80 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors\"\n onClick={() => panZoom.reset()}\n aria-label={strings.zoomReset}\n title={strings.zoomReset}\n type=\"button\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"2\"\n d=\"M16.023 9.348h4.992V4.356m0 4.992l-3.181-3.183a8.25 8.25 0 00-13.803 3.7M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7\"\n />\n </svg>\n </button>\n </Show>\n </div>\n </div>\n </Portal>\n </Show>\n )\n}\n"],"names":["MAX_ZOOM","ZOOM_BUTTON_FACTOR","DOUBLE_CLICK_ZOOM","LightboxOverlay","props","strings","useMCPUIStrings","imageRef","panZoom","createPanZoom","min","max","target","isOpen","selectedIndex","currentImage","images","canGoPrev","canGoNext","length","handlePrev","onNavigate","handleNext","displayedImage","createMemo","image","JSON","stringify","url","srcset","createEffect","cancelGesture","reset","isEditableTarget","HTMLElement","isContentEditable","includes","tagName","handleKeydown","e","isZoomChord","ctrlKey","metaKey","key","onClose","isPanning","preventDefault","zoomBy","document","addEventListener","body","style","overflow","onCleanup","removeEventListener","handleBackdropClick","wasDragged","handlePointerDown","event","Element","closest","onPointerDown","anchorOf","undefined","rect","getBoundingClientRect","offset","x","clientX","left","y","clientY","top","handleDoubleClick","stopPropagation","isZoomed","zoomTo","_$createComponent","Show","when","children","Portal","_el$","_$getNextElement","_tmpl$5","_el$2","firstChild","_el$18","nextSibling","_el$19","_co$4","_$getNextMarker","_el$20","_el$21","_co$5","_el$5","_el$22","_el$23","_co$6","_el$7","_el$9","_el$0","_co$","_el$8","_el$1","_el$10","_co$2","_el$11","_el$12","_el$13","_el$14","_el$16","_el$17","_co$3","_$addEventListener","onPointerCancel","onPointerUp","onPointerMove","$$pointerdown","onWheel","$$click","_$insert","_el$3","_tmpl$","_$effect","_$setAttribute","lightboxPrevious","_$runHydrationEvents","_el$4","_tmpl$2","lightboxNext","$$dblclick","_ref$","_$use","caption","_el$6","_tmpl$3","formatMCPUIString","zoomLevel","percent","_el$15","_tmpl$4","_p$","_v$","zoomReset","_v$2","t","_v$3","surfaceStyle","_v$4","lightboxLabel","_v$5","lightboxClose","_v$6","safeUrl","allowDataImage","_v$7","alt","_v$8","_v$9","sizes","_v$0","contentStyle","_v$1","scale","_v$10","zoomOut","_v$11","_v$12","_v$13","zoomIn","_v$14","_$style","a","o","i","n","s","h","r","_$setProperty","d","l","u","c","w","_$delegateEvents"],"mappings":"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"}
|
|
@@ -7,7 +7,8 @@ const DEFAULT_MCPUI_CONFIG = {
|
|
|
7
7
|
customTrustedIframeDomains: [],
|
|
8
8
|
iframePolicy: "strict",
|
|
9
9
|
customIframeDomains: [],
|
|
10
|
-
iframeFallbackLink: "auto"
|
|
10
|
+
iframeFallbackLink: "auto",
|
|
11
|
+
chartZoom: "expanded"
|
|
11
12
|
};
|
|
12
13
|
const MCPUIConfigContext = solidJs.createContext(DEFAULT_MCPUI_CONFIG);
|
|
13
14
|
function useMCPUIConfig() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MCPUIConfigContext.cjs","sources":["../../src/context/MCPUIConfigContext.tsx"],"sourcesContent":["/**\n * MCPUIConfigContext — host-level rendering policy
|
|
1
|
+
{"version":3,"file":"MCPUIConfigContext.cjs","sources":["../../src/context/MCPUIConfigContext.tsx"],"sourcesContent":["/**\n * MCPUIConfigContext — host-level rendering policy: the library's iframes\n * (v6.21.0) and, since v6.22.0, where a chart may capture the wheel.\n *\n * @since v6.21.0\n *\n * ## Why\n *\n * A host that serves `Cross-Origin-Embedder-Policy: credentialless` makes the\n * browser refuse every cross-origin iframe whose own document does not send a\n * COEP header — which is YouTube, Vimeo, Google Docs, Notion and nearly all of\n * `DEFAULT_IFRAME_DOMAINS`. The escape hatch is the boolean HTML attribute\n * `credentialless` on the `<iframe>` itself: the document then loads, but\n * without access to the cookies and storage of its own origin.\n *\n * That trade-off is free for a public video or chart and fatal for an\n * authenticated embed, so the library keys it on the distinction it already\n * makes: `TRUSTED_IFRAME_DOMAINS` (the hosts that receive `allow-same-origin`\n * because they need their own cookies) do NOT get the attribute; everything\n * else does. This context lets a host override that, add its own trusted\n * hosts, and control the \"open in a new tab\" fallback link.\n *\n * ## Scope — policy only, NOT strings\n *\n * Text lives in `MCPUIStrings` (`MCPUIStringsContext`). This context carries\n * only behavioural switches, and like the strings context every renderer works\n * standalone: with no provider mounted, `DEFAULT_MCPUI_CONFIG` applies.\n *\n * @example\n * ```tsx\n * import { MCPUIConfigProvider } from '@seed-ship/mcp-ui-solid'\n *\n * <MCPUIConfigProvider config={{ customTrustedIframeDomains: ['embed.acme.com'] }}>\n * <App />\n * </MCPUIConfigProvider>\n * ```\n */\n\nimport { createContext, mergeProps, useContext, type JSX } from 'solid-js'\nimport type { IframePolicy } from '../types'\n\n/**\n * Host-level rendering policy. Every field has a default — a provider passes\n * a partial override.\n *\n * ## Why every field is optional\n *\n * This interface grows a key every time the library gains a policy switch,\n * and those releases are MINOR. Were the fields required, adding one would\n * break the build of any consumer holding a COMPLETE `MCPUIConfig` value —\n * a typed constant, a helper that returns one, or a direct\n * `<MCPUIConfigContext.Provider value={…}>` — because their object would\n * suddenly miss a key. Nothing about their behaviour changed; only `tsc`\n * would fail. Optional fields make each new key additive.\n *\n * The completeness guarantee moves from the type to the two values that\n * actually need it, exactly as `MCPUIStrings` does it:\n * `DEFAULT_MCPUI_CONFIG` is `Required<MCPUIConfig>`, so forgetting a default\n * is a compile error HERE, and {@link useMCPUIConfig} returns\n * `Required<MCPUIConfig>`, so every renderer still reads a fully resolved\n * value and never has to null-check a policy.\n */\nexport interface MCPUIConfig {\n /**\n * When the library puts the boolean `credentialless` attribute on an\n * `<iframe>` it renders.\n *\n * - `'auto'` (default) — on every iframe whose host is NOT in\n * `TRUSTED_IFRAME_DOMAINS` (plus `customTrustedIframeDomains`). Those\n * hosts already ran without cookies, since their sandbox has no\n * `allow-same-origin`; the attribute is what unblocks them under COEP.\n * - `'always'` — on every iframe, trusted hosts included. An authenticated\n * embed will then render its login screen.\n * - `'never'` — never. The right value for a host that sends no COEP header\n * and does not want its embeds to lose their cookies.\n *\n * Chrome/Edge 110+ honour the attribute; Firefox ignores it (iframes stay\n * blocked while the host sends COEP); Safari ignores the COEP value itself,\n * so nothing is blocked there in the first place.\n */\n iframeCredentialless?: 'auto' | 'always' | 'never'\n\n /**\n * Extra hosts treated as trusted, on top of `TRUSTED_IFRAME_DOMAINS`:\n * no `credentialless` attribute, and `allow-same-origin` in the sandbox.\n * A subdomain of a listed host matches too.\n *\n * This only RECLASSIFIES a host the allow-list already accepts. To render an\n * iframe whose host is outside `DEFAULT_IFRAME_DOMAINS`, add it to\n * {@link MCPUIConfig.customIframeDomains} with `iframePolicy: 'extend'`.\n */\n customTrustedIframeDomains?: string[]\n\n /**\n * How `UIResourceRenderer` validates an `iframe` component's host:\n * `'strict'` (default) accepts `DEFAULT_IFRAME_DOMAINS` only, `'extend'`\n * also accepts {@link MCPUIConfig.customIframeDomains}.\n */\n iframePolicy?: IframePolicy\n\n /**\n * Extra hosts the allow-list accepts when `iframePolicy` is `'extend'`.\n * Without this, a component pointing at an unlisted host is replaced by the\n * validation card before any renderer runs.\n */\n customIframeDomains?: string[]\n\n /**\n * When the library renders an \"open in a new tab\" link under an embed\n * (`strings.iframeOpenInNewTab`).\n *\n * A COEP-blocked iframe fires no event the parent page can observe, so\n * without this the user sees an unexplained blank frame.\n *\n * - `'auto'` (default) — only when the page is cross-origin isolated\n * (`window.crossOriginIsolated`). That flag needs COOP `same-origin` AND\n * COEP, so a host that sends COEP alone still blocks embeds while the\n * flag is false: set `'always'` there. Read after mount, never during\n * render, so SSR markup and hydration agree.\n * - `'always'` / `'never'` — unconditional.\n */\n iframeFallbackLink?: 'auto' | 'always' | 'never'\n\n /**\n * Where `ChartJSRenderer` enables wheel-zoom / drag-pan / pinch on a chart\n * (through the optional `chartjs-plugin-zoom` peer dependency — absent, the\n * chart renders exactly as it does without this setting).\n *\n * Hijacking the mouse wheel on a component sitting inline in a scrolling\n * chat feed is hostile: the user scrolls the conversation and the chart eats\n * the gesture. So zoom belongs to a surface that OWNS its viewport, which\n * here is `ExpandableWrapper`'s fullscreen modal.\n *\n * - `'expanded'` (default) — only inside the fullscreen modal.\n * - `'always'` — inline as well. For a host that lays charts out in a\n * dashboard rather than in a feed, where the wheel has nothing to steal.\n * - `'never'` — off everywhere; the plugin is never registered.\n *\n * @since 6.22.0\n */\n chartZoom?: 'expanded' | 'always' | 'never'\n}\n\n/**\n * Defaults. Chosen so a host that sends no COEP header sees no change from\n * 6.20.0 apart from the fallback link, which `'auto'` keeps hidden there —\n * and, since 6.22.0, so no inline component ever captures the wheel\n * (`chartZoom: 'expanded'`).\n *\n * Typed `Required<MCPUIConfig>`: the interface's fields are optional so that\n * adding one stays backward compatible, and this annotation is what keeps a\n * new key from shipping without a default.\n */\nexport const DEFAULT_MCPUI_CONFIG: Required<MCPUIConfig> = {\n iframeCredentialless: 'auto',\n customTrustedIframeDomains: [],\n iframePolicy: 'strict',\n customIframeDomains: [],\n iframeFallbackLink: 'auto',\n chartZoom: 'expanded',\n}\n\n/**\n * Deliberately typed with the PARTIAL `MCPUIConfig`, not `Required<…>`: a\n * host that bypasses `MCPUIConfigProvider` and feeds this provider directly\n * may pass whichever keys it cares about, and `useMCPUIConfig` fills the\n * rest. Widening it this way only ever accepts more than before.\n */\nexport const MCPUIConfigContext = createContext<MCPUIConfig>(DEFAULT_MCPUI_CONFIG)\n\n/**\n * Reads the active host config, resolved: every key is present, whether it\n * came from the provider or from `DEFAULT_MCPUI_CONFIG`. Returns the defaults\n * outright when no `<MCPUIConfigProvider>` is mounted above.\n */\nexport function useMCPUIConfig(): Required<MCPUIConfig> {\n return mergeProps(DEFAULT_MCPUI_CONFIG, useContext(MCPUIConfigContext))\n}\n\nexport interface MCPUIConfigProviderProps {\n /**\n * Partial override of the host config. Any key left unset falls back to\n * `DEFAULT_MCPUI_CONFIG`.\n */\n config?: Partial<MCPUIConfig>\n children: JSX.Element\n}\n\n/**\n * Provides the host config to every MCP-UI renderer below it.\n * Merges the partial `config` override over the defaults.\n */\nexport function MCPUIConfigProvider(props: MCPUIConfigProviderProps): JSX.Element {\n // The function source keeps replacement `config` objects reactive while\n // mergeProps fills any omitted key without taking a one-time snapshot.\n const value = mergeProps(DEFAULT_MCPUI_CONFIG, () => props.config)\n return (\n <MCPUIConfigContext.Provider value={value}>\n {props.children}\n </MCPUIConfigContext.Provider>\n )\n}\n"],"names":["DEFAULT_MCPUI_CONFIG","iframeCredentialless","customTrustedIframeDomains","iframePolicy","customIframeDomains","iframeFallbackLink","chartZoom","MCPUIConfigContext","createContext","useMCPUIConfig","mergeProps","useContext","MCPUIConfigProvider","props","value","config","_$createComponent","Provider","children"],"mappings":";;;;AAyJO,MAAMA,uBAA8C;AAAA,EACzDC,sBAAsB;AAAA,EACtBC,4BAA4B,CAAA;AAAA,EAC5BC,cAAc;AAAA,EACdC,qBAAqB,CAAA;AAAA,EACrBC,oBAAoB;AAAA,EACpBC,WAAW;AACb;AAQO,MAAMC,qBAAqBC,QAAAA,cAA2BR,oBAAoB;AAO1E,SAASS,iBAAwC;AACtD,SAAOC,mBAAWV,sBAAsBW,QAAAA,WAAWJ,kBAAkB,CAAC;AACxE;AAeO,SAASK,oBAAoBC,OAA8C;AAGhF,QAAMC,QAAQJ,QAAAA,WAAWV,sBAAsB,MAAMa,MAAME,MAAM;AACjE,SAAAC,IAAAA,gBACGT,mBAAmBU,UAAQ;AAAA,IAACH;AAAAA,IAAY,IAAAI,WAAA;AAAA,aACtCL,MAAMK;AAAAA,IAAQ;AAAA,EAAA,CAAA;AAGrB;;;;;"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCPUIConfigContext — host-level rendering policy
|
|
2
|
+
* MCPUIConfigContext — host-level rendering policy: the library's iframes
|
|
3
|
+
* (v6.21.0) and, since v6.22.0, where a chart may capture the wheel.
|
|
3
4
|
*
|
|
4
5
|
* @since v6.21.0
|
|
5
6
|
*
|
|
@@ -39,6 +40,23 @@ import type { IframePolicy } from '../types';
|
|
|
39
40
|
/**
|
|
40
41
|
* Host-level rendering policy. Every field has a default — a provider passes
|
|
41
42
|
* a partial override.
|
|
43
|
+
*
|
|
44
|
+
* ## Why every field is optional
|
|
45
|
+
*
|
|
46
|
+
* This interface grows a key every time the library gains a policy switch,
|
|
47
|
+
* and those releases are MINOR. Were the fields required, adding one would
|
|
48
|
+
* break the build of any consumer holding a COMPLETE `MCPUIConfig` value —
|
|
49
|
+
* a typed constant, a helper that returns one, or a direct
|
|
50
|
+
* `<MCPUIConfigContext.Provider value={…}>` — because their object would
|
|
51
|
+
* suddenly miss a key. Nothing about their behaviour changed; only `tsc`
|
|
52
|
+
* would fail. Optional fields make each new key additive.
|
|
53
|
+
*
|
|
54
|
+
* The completeness guarantee moves from the type to the two values that
|
|
55
|
+
* actually need it, exactly as `MCPUIStrings` does it:
|
|
56
|
+
* `DEFAULT_MCPUI_CONFIG` is `Required<MCPUIConfig>`, so forgetting a default
|
|
57
|
+
* is a compile error HERE, and {@link useMCPUIConfig} returns
|
|
58
|
+
* `Required<MCPUIConfig>`, so every renderer still reads a fully resolved
|
|
59
|
+
* value and never has to null-check a policy.
|
|
42
60
|
*/
|
|
43
61
|
export interface MCPUIConfig {
|
|
44
62
|
/**
|
|
@@ -58,7 +76,7 @@ export interface MCPUIConfig {
|
|
|
58
76
|
* blocked while the host sends COEP); Safari ignores the COEP value itself,
|
|
59
77
|
* so nothing is blocked there in the first place.
|
|
60
78
|
*/
|
|
61
|
-
iframeCredentialless
|
|
79
|
+
iframeCredentialless?: 'auto' | 'always' | 'never';
|
|
62
80
|
/**
|
|
63
81
|
* Extra hosts treated as trusted, on top of `TRUSTED_IFRAME_DOMAINS`:
|
|
64
82
|
* no `credentialless` attribute, and `allow-same-origin` in the sandbox.
|
|
@@ -68,19 +86,19 @@ export interface MCPUIConfig {
|
|
|
68
86
|
* iframe whose host is outside `DEFAULT_IFRAME_DOMAINS`, add it to
|
|
69
87
|
* {@link MCPUIConfig.customIframeDomains} with `iframePolicy: 'extend'`.
|
|
70
88
|
*/
|
|
71
|
-
customTrustedIframeDomains
|
|
89
|
+
customTrustedIframeDomains?: string[];
|
|
72
90
|
/**
|
|
73
91
|
* How `UIResourceRenderer` validates an `iframe` component's host:
|
|
74
92
|
* `'strict'` (default) accepts `DEFAULT_IFRAME_DOMAINS` only, `'extend'`
|
|
75
93
|
* also accepts {@link MCPUIConfig.customIframeDomains}.
|
|
76
94
|
*/
|
|
77
|
-
iframePolicy
|
|
95
|
+
iframePolicy?: IframePolicy;
|
|
78
96
|
/**
|
|
79
97
|
* Extra hosts the allow-list accepts when `iframePolicy` is `'extend'`.
|
|
80
98
|
* Without this, a component pointing at an unlisted host is replaced by the
|
|
81
99
|
* validation card before any renderer runs.
|
|
82
100
|
*/
|
|
83
|
-
customIframeDomains
|
|
101
|
+
customIframeDomains?: string[];
|
|
84
102
|
/**
|
|
85
103
|
* When the library renders an "open in a new tab" link under an embed
|
|
86
104
|
* (`strings.iframeOpenInNewTab`).
|
|
@@ -95,19 +113,50 @@ export interface MCPUIConfig {
|
|
|
95
113
|
* render, so SSR markup and hydration agree.
|
|
96
114
|
* - `'always'` / `'never'` — unconditional.
|
|
97
115
|
*/
|
|
98
|
-
iframeFallbackLink
|
|
116
|
+
iframeFallbackLink?: 'auto' | 'always' | 'never';
|
|
117
|
+
/**
|
|
118
|
+
* Where `ChartJSRenderer` enables wheel-zoom / drag-pan / pinch on a chart
|
|
119
|
+
* (through the optional `chartjs-plugin-zoom` peer dependency — absent, the
|
|
120
|
+
* chart renders exactly as it does without this setting).
|
|
121
|
+
*
|
|
122
|
+
* Hijacking the mouse wheel on a component sitting inline in a scrolling
|
|
123
|
+
* chat feed is hostile: the user scrolls the conversation and the chart eats
|
|
124
|
+
* the gesture. So zoom belongs to a surface that OWNS its viewport, which
|
|
125
|
+
* here is `ExpandableWrapper`'s fullscreen modal.
|
|
126
|
+
*
|
|
127
|
+
* - `'expanded'` (default) — only inside the fullscreen modal.
|
|
128
|
+
* - `'always'` — inline as well. For a host that lays charts out in a
|
|
129
|
+
* dashboard rather than in a feed, where the wheel has nothing to steal.
|
|
130
|
+
* - `'never'` — off everywhere; the plugin is never registered.
|
|
131
|
+
*
|
|
132
|
+
* @since 6.22.0
|
|
133
|
+
*/
|
|
134
|
+
chartZoom?: 'expanded' | 'always' | 'never';
|
|
99
135
|
}
|
|
100
136
|
/**
|
|
101
137
|
* Defaults. Chosen so a host that sends no COEP header sees no change from
|
|
102
|
-
* 6.20.0 apart from the fallback link, which `'auto'` keeps hidden there
|
|
138
|
+
* 6.20.0 apart from the fallback link, which `'auto'` keeps hidden there —
|
|
139
|
+
* and, since 6.22.0, so no inline component ever captures the wheel
|
|
140
|
+
* (`chartZoom: 'expanded'`).
|
|
141
|
+
*
|
|
142
|
+
* Typed `Required<MCPUIConfig>`: the interface's fields are optional so that
|
|
143
|
+
* adding one stays backward compatible, and this annotation is what keeps a
|
|
144
|
+
* new key from shipping without a default.
|
|
145
|
+
*/
|
|
146
|
+
export declare const DEFAULT_MCPUI_CONFIG: Required<MCPUIConfig>;
|
|
147
|
+
/**
|
|
148
|
+
* Deliberately typed with the PARTIAL `MCPUIConfig`, not `Required<…>`: a
|
|
149
|
+
* host that bypasses `MCPUIConfigProvider` and feeds this provider directly
|
|
150
|
+
* may pass whichever keys it cares about, and `useMCPUIConfig` fills the
|
|
151
|
+
* rest. Widening it this way only ever accepts more than before.
|
|
103
152
|
*/
|
|
104
|
-
export declare const DEFAULT_MCPUI_CONFIG: MCPUIConfig;
|
|
105
153
|
export declare const MCPUIConfigContext: import("solid-js").Context<MCPUIConfig>;
|
|
106
154
|
/**
|
|
107
|
-
* Reads the active host config
|
|
108
|
-
*
|
|
155
|
+
* Reads the active host config, resolved: every key is present, whether it
|
|
156
|
+
* came from the provider or from `DEFAULT_MCPUI_CONFIG`. Returns the defaults
|
|
157
|
+
* outright when no `<MCPUIConfigProvider>` is mounted above.
|
|
109
158
|
*/
|
|
110
|
-
export declare function useMCPUIConfig(): MCPUIConfig
|
|
159
|
+
export declare function useMCPUIConfig(): Required<MCPUIConfig>;
|
|
111
160
|
export interface MCPUIConfigProviderProps {
|
|
112
161
|
/**
|
|
113
162
|
* Partial override of the host config. Any key left unset falls back to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MCPUIConfigContext.d.ts","sourceRoot":"","sources":["../../src/context/MCPUIConfigContext.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"MCPUIConfigContext.d.ts","sourceRoot":"","sources":["../../src/context/MCPUIConfigContext.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAyC,KAAK,GAAG,EAAE,MAAM,UAAU,CAAA;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;;;;;;;;OAgBG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;IAElD;;;;;;;;OAQG;IACH,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAA;IAErC;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;IAE3B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAA;IAE9B;;;;;;;;;;;;;OAaG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;IAEhD;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;CAC5C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAOtD,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,yCAAmD,CAAA;AAElF;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,QAAQ,CAAC,WAAW,CAAC,CAEtD;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAA;CACtB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,GAAG,CAAC,OAAO,CAShF"}
|
|
@@ -5,7 +5,8 @@ const DEFAULT_MCPUI_CONFIG = {
|
|
|
5
5
|
customTrustedIframeDomains: [],
|
|
6
6
|
iframePolicy: "strict",
|
|
7
7
|
customIframeDomains: [],
|
|
8
|
-
iframeFallbackLink: "auto"
|
|
8
|
+
iframeFallbackLink: "auto",
|
|
9
|
+
chartZoom: "expanded"
|
|
9
10
|
};
|
|
10
11
|
const MCPUIConfigContext = createContext(DEFAULT_MCPUI_CONFIG);
|
|
11
12
|
function useMCPUIConfig() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MCPUIConfigContext.js","sources":["../../src/context/MCPUIConfigContext.tsx"],"sourcesContent":["/**\n * MCPUIConfigContext — host-level rendering policy
|
|
1
|
+
{"version":3,"file":"MCPUIConfigContext.js","sources":["../../src/context/MCPUIConfigContext.tsx"],"sourcesContent":["/**\n * MCPUIConfigContext — host-level rendering policy: the library's iframes\n * (v6.21.0) and, since v6.22.0, where a chart may capture the wheel.\n *\n * @since v6.21.0\n *\n * ## Why\n *\n * A host that serves `Cross-Origin-Embedder-Policy: credentialless` makes the\n * browser refuse every cross-origin iframe whose own document does not send a\n * COEP header — which is YouTube, Vimeo, Google Docs, Notion and nearly all of\n * `DEFAULT_IFRAME_DOMAINS`. The escape hatch is the boolean HTML attribute\n * `credentialless` on the `<iframe>` itself: the document then loads, but\n * without access to the cookies and storage of its own origin.\n *\n * That trade-off is free for a public video or chart and fatal for an\n * authenticated embed, so the library keys it on the distinction it already\n * makes: `TRUSTED_IFRAME_DOMAINS` (the hosts that receive `allow-same-origin`\n * because they need their own cookies) do NOT get the attribute; everything\n * else does. This context lets a host override that, add its own trusted\n * hosts, and control the \"open in a new tab\" fallback link.\n *\n * ## Scope — policy only, NOT strings\n *\n * Text lives in `MCPUIStrings` (`MCPUIStringsContext`). This context carries\n * only behavioural switches, and like the strings context every renderer works\n * standalone: with no provider mounted, `DEFAULT_MCPUI_CONFIG` applies.\n *\n * @example\n * ```tsx\n * import { MCPUIConfigProvider } from '@seed-ship/mcp-ui-solid'\n *\n * <MCPUIConfigProvider config={{ customTrustedIframeDomains: ['embed.acme.com'] }}>\n * <App />\n * </MCPUIConfigProvider>\n * ```\n */\n\nimport { createContext, mergeProps, useContext, type JSX } from 'solid-js'\nimport type { IframePolicy } from '../types'\n\n/**\n * Host-level rendering policy. Every field has a default — a provider passes\n * a partial override.\n *\n * ## Why every field is optional\n *\n * This interface grows a key every time the library gains a policy switch,\n * and those releases are MINOR. Were the fields required, adding one would\n * break the build of any consumer holding a COMPLETE `MCPUIConfig` value —\n * a typed constant, a helper that returns one, or a direct\n * `<MCPUIConfigContext.Provider value={…}>` — because their object would\n * suddenly miss a key. Nothing about their behaviour changed; only `tsc`\n * would fail. Optional fields make each new key additive.\n *\n * The completeness guarantee moves from the type to the two values that\n * actually need it, exactly as `MCPUIStrings` does it:\n * `DEFAULT_MCPUI_CONFIG` is `Required<MCPUIConfig>`, so forgetting a default\n * is a compile error HERE, and {@link useMCPUIConfig} returns\n * `Required<MCPUIConfig>`, so every renderer still reads a fully resolved\n * value and never has to null-check a policy.\n */\nexport interface MCPUIConfig {\n /**\n * When the library puts the boolean `credentialless` attribute on an\n * `<iframe>` it renders.\n *\n * - `'auto'` (default) — on every iframe whose host is NOT in\n * `TRUSTED_IFRAME_DOMAINS` (plus `customTrustedIframeDomains`). Those\n * hosts already ran without cookies, since their sandbox has no\n * `allow-same-origin`; the attribute is what unblocks them under COEP.\n * - `'always'` — on every iframe, trusted hosts included. An authenticated\n * embed will then render its login screen.\n * - `'never'` — never. The right value for a host that sends no COEP header\n * and does not want its embeds to lose their cookies.\n *\n * Chrome/Edge 110+ honour the attribute; Firefox ignores it (iframes stay\n * blocked while the host sends COEP); Safari ignores the COEP value itself,\n * so nothing is blocked there in the first place.\n */\n iframeCredentialless?: 'auto' | 'always' | 'never'\n\n /**\n * Extra hosts treated as trusted, on top of `TRUSTED_IFRAME_DOMAINS`:\n * no `credentialless` attribute, and `allow-same-origin` in the sandbox.\n * A subdomain of a listed host matches too.\n *\n * This only RECLASSIFIES a host the allow-list already accepts. To render an\n * iframe whose host is outside `DEFAULT_IFRAME_DOMAINS`, add it to\n * {@link MCPUIConfig.customIframeDomains} with `iframePolicy: 'extend'`.\n */\n customTrustedIframeDomains?: string[]\n\n /**\n * How `UIResourceRenderer` validates an `iframe` component's host:\n * `'strict'` (default) accepts `DEFAULT_IFRAME_DOMAINS` only, `'extend'`\n * also accepts {@link MCPUIConfig.customIframeDomains}.\n */\n iframePolicy?: IframePolicy\n\n /**\n * Extra hosts the allow-list accepts when `iframePolicy` is `'extend'`.\n * Without this, a component pointing at an unlisted host is replaced by the\n * validation card before any renderer runs.\n */\n customIframeDomains?: string[]\n\n /**\n * When the library renders an \"open in a new tab\" link under an embed\n * (`strings.iframeOpenInNewTab`).\n *\n * A COEP-blocked iframe fires no event the parent page can observe, so\n * without this the user sees an unexplained blank frame.\n *\n * - `'auto'` (default) — only when the page is cross-origin isolated\n * (`window.crossOriginIsolated`). That flag needs COOP `same-origin` AND\n * COEP, so a host that sends COEP alone still blocks embeds while the\n * flag is false: set `'always'` there. Read after mount, never during\n * render, so SSR markup and hydration agree.\n * - `'always'` / `'never'` — unconditional.\n */\n iframeFallbackLink?: 'auto' | 'always' | 'never'\n\n /**\n * Where `ChartJSRenderer` enables wheel-zoom / drag-pan / pinch on a chart\n * (through the optional `chartjs-plugin-zoom` peer dependency — absent, the\n * chart renders exactly as it does without this setting).\n *\n * Hijacking the mouse wheel on a component sitting inline in a scrolling\n * chat feed is hostile: the user scrolls the conversation and the chart eats\n * the gesture. So zoom belongs to a surface that OWNS its viewport, which\n * here is `ExpandableWrapper`'s fullscreen modal.\n *\n * - `'expanded'` (default) — only inside the fullscreen modal.\n * - `'always'` — inline as well. For a host that lays charts out in a\n * dashboard rather than in a feed, where the wheel has nothing to steal.\n * - `'never'` — off everywhere; the plugin is never registered.\n *\n * @since 6.22.0\n */\n chartZoom?: 'expanded' | 'always' | 'never'\n}\n\n/**\n * Defaults. Chosen so a host that sends no COEP header sees no change from\n * 6.20.0 apart from the fallback link, which `'auto'` keeps hidden there —\n * and, since 6.22.0, so no inline component ever captures the wheel\n * (`chartZoom: 'expanded'`).\n *\n * Typed `Required<MCPUIConfig>`: the interface's fields are optional so that\n * adding one stays backward compatible, and this annotation is what keeps a\n * new key from shipping without a default.\n */\nexport const DEFAULT_MCPUI_CONFIG: Required<MCPUIConfig> = {\n iframeCredentialless: 'auto',\n customTrustedIframeDomains: [],\n iframePolicy: 'strict',\n customIframeDomains: [],\n iframeFallbackLink: 'auto',\n chartZoom: 'expanded',\n}\n\n/**\n * Deliberately typed with the PARTIAL `MCPUIConfig`, not `Required<…>`: a\n * host that bypasses `MCPUIConfigProvider` and feeds this provider directly\n * may pass whichever keys it cares about, and `useMCPUIConfig` fills the\n * rest. Widening it this way only ever accepts more than before.\n */\nexport const MCPUIConfigContext = createContext<MCPUIConfig>(DEFAULT_MCPUI_CONFIG)\n\n/**\n * Reads the active host config, resolved: every key is present, whether it\n * came from the provider or from `DEFAULT_MCPUI_CONFIG`. Returns the defaults\n * outright when no `<MCPUIConfigProvider>` is mounted above.\n */\nexport function useMCPUIConfig(): Required<MCPUIConfig> {\n return mergeProps(DEFAULT_MCPUI_CONFIG, useContext(MCPUIConfigContext))\n}\n\nexport interface MCPUIConfigProviderProps {\n /**\n * Partial override of the host config. Any key left unset falls back to\n * `DEFAULT_MCPUI_CONFIG`.\n */\n config?: Partial<MCPUIConfig>\n children: JSX.Element\n}\n\n/**\n * Provides the host config to every MCP-UI renderer below it.\n * Merges the partial `config` override over the defaults.\n */\nexport function MCPUIConfigProvider(props: MCPUIConfigProviderProps): JSX.Element {\n // The function source keeps replacement `config` objects reactive while\n // mergeProps fills any omitted key without taking a one-time snapshot.\n const value = mergeProps(DEFAULT_MCPUI_CONFIG, () => props.config)\n return (\n <MCPUIConfigContext.Provider value={value}>\n {props.children}\n </MCPUIConfigContext.Provider>\n )\n}\n"],"names":["DEFAULT_MCPUI_CONFIG","iframeCredentialless","customTrustedIframeDomains","iframePolicy","customIframeDomains","iframeFallbackLink","chartZoom","MCPUIConfigContext","createContext","useMCPUIConfig","mergeProps","useContext","MCPUIConfigProvider","props","value","config","_$createComponent","Provider","children"],"mappings":";;AAyJO,MAAMA,uBAA8C;AAAA,EACzDC,sBAAsB;AAAA,EACtBC,4BAA4B,CAAA;AAAA,EAC5BC,cAAc;AAAA,EACdC,qBAAqB,CAAA;AAAA,EACrBC,oBAAoB;AAAA,EACpBC,WAAW;AACb;AAQO,MAAMC,qBAAqBC,cAA2BR,oBAAoB;AAO1E,SAASS,iBAAwC;AACtD,SAAOC,WAAWV,sBAAsBW,WAAWJ,kBAAkB,CAAC;AACxE;AAeO,SAASK,oBAAoBC,OAA8C;AAGhF,QAAMC,QAAQJ,WAAWV,sBAAsB,MAAMa,MAAME,MAAM;AACjE,SAAAC,gBACGT,mBAAmBU,UAAQ;AAAA,IAACH;AAAAA,IAAY,IAAAI,WAAA;AAAA,aACtCL,MAAMK;AAAAA,IAAQ;AAAA,EAAA,CAAA;AAGrB;"}
|
|
@@ -284,7 +284,12 @@ const DEFAULT_MCPUI_STRINGS = {
|
|
|
284
284
|
sizeKilobytes: "{size} KB",
|
|
285
285
|
sizeMegabytes: "{size} MB",
|
|
286
286
|
// ── 6.21.0 — iframe COEP fallback ──────────────────────────
|
|
287
|
-
iframeOpenInNewTab: "Open in a new tab"
|
|
287
|
+
iframeOpenInNewTab: "Open in a new tab",
|
|
288
|
+
// ── 6.22.0 — zoom / pan controls ───────────────────────────
|
|
289
|
+
zoomIn: "Zoom in",
|
|
290
|
+
zoomOut: "Zoom out",
|
|
291
|
+
zoomReset: "Reset zoom",
|
|
292
|
+
zoomLevel: "Zoom {percent}%"
|
|
288
293
|
};
|
|
289
294
|
const MCPUIStringsContext = solidJs.createContext(DEFAULT_MCPUI_STRINGS);
|
|
290
295
|
const resolvedLocales = /* @__PURE__ */ new Map();
|