@seed-ship/mcp-ui-solid 6.3.0 → 6.4.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 +87 -0
- package/dist/components/CarouselRenderer.cjs +3 -0
- package/dist/components/CarouselRenderer.cjs.map +1 -1
- package/dist/components/CarouselRenderer.d.ts +5 -0
- package/dist/components/CarouselRenderer.d.ts.map +1 -1
- package/dist/components/CarouselRenderer.js +3 -0
- package/dist/components/CarouselRenderer.js.map +1 -1
- package/dist/components/ChartJSRenderer.cjs +3 -0
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts +5 -0
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +3 -0
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/CodeBlockRenderer.cjs +3 -0
- package/dist/components/CodeBlockRenderer.cjs.map +1 -1
- package/dist/components/CodeBlockRenderer.d.ts +5 -0
- package/dist/components/CodeBlockRenderer.d.ts.map +1 -1
- package/dist/components/CodeBlockRenderer.js +3 -0
- package/dist/components/CodeBlockRenderer.js.map +1 -1
- package/dist/components/GraphRenderer.cjs +33 -24
- package/dist/components/GraphRenderer.cjs.map +1 -1
- package/dist/components/GraphRenderer.d.ts +8 -2
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +33 -24
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/ImageGalleryRenderer.cjs +3 -0
- package/dist/components/ImageGalleryRenderer.cjs.map +1 -1
- package/dist/components/ImageGalleryRenderer.d.ts +5 -0
- package/dist/components/ImageGalleryRenderer.d.ts.map +1 -1
- package/dist/components/ImageGalleryRenderer.js +3 -0
- package/dist/components/ImageGalleryRenderer.js.map +1 -1
- package/dist/components/MapRenderer.cjs +3 -0
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts +5 -0
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +3 -0
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/PortalDropdownMenu.cjs +82 -0
- package/dist/components/PortalDropdownMenu.cjs.map +1 -0
- package/dist/components/PortalDropdownMenu.d.ts +56 -0
- package/dist/components/PortalDropdownMenu.d.ts.map +1 -0
- package/dist/components/PortalDropdownMenu.js +82 -0
- package/dist/components/PortalDropdownMenu.js.map +1 -0
- package/dist/components/UIResourceRenderer.cjs +296 -256
- package/dist/components/UIResourceRenderer.cjs.map +1 -1
- package/dist/components/UIResourceRenderer.d.ts +12 -0
- package/dist/components/UIResourceRenderer.d.ts.map +1 -1
- package/dist/components/UIResourceRenderer.js +296 -256
- package/dist/components/UIResourceRenderer.js.map +1 -1
- package/dist/components/VideoRenderer.cjs +3 -0
- package/dist/components/VideoRenderer.cjs.map +1 -1
- package/dist/components/VideoRenderer.d.ts +5 -0
- package/dist/components/VideoRenderer.d.ts.map +1 -1
- package/dist/components/VideoRenderer.js +3 -0
- package/dist/components/VideoRenderer.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +2 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +2 -0
- package/dist/components.d.ts +2 -0
- package/dist/components.js +2 -0
- package/dist/components.js.map +1 -1
- package/dist/index.cjs +2 -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 +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CarouselRenderer.tsx +6 -0
- package/src/components/ChartJSRenderer.tsx +7 -0
- package/src/components/CodeBlockRenderer.tsx +7 -1
- package/src/components/GraphRenderer.tsx +40 -21
- package/src/components/ImageGalleryRenderer.tsx +7 -0
- package/src/components/MapRenderer.tsx +7 -0
- package/src/components/PortalDropdownMenu.test.tsx +113 -0
- package/src/components/PortalDropdownMenu.tsx +130 -0
- package/src/components/UIResourceRenderer.fluidity.test.tsx +51 -0
- package/src/components/UIResourceRenderer.tsx +50 -24
- package/src/components/VideoRenderer.tsx +7 -0
- package/src/components/index.ts +4 -0
- package/src/index.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v6.4.0 — Portal-mounted dropdown for table + graph Export menus.
|
|
3
|
+
*
|
|
4
|
+
* Coverage targets :
|
|
5
|
+
* 1. Menu mounts via <Portal> on document.body (not in trigger's parent)
|
|
6
|
+
* 2. Menu position is computed from the trigger's getBoundingClientRect
|
|
7
|
+
* 3. Click outside the menu closes it
|
|
8
|
+
* 4. Escape key closes it
|
|
9
|
+
* 5. Click on the trigger itself does not close (parent owns toggle)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
13
|
+
import { render, fireEvent, cleanup } from '@solidjs/testing-library'
|
|
14
|
+
import { createSignal } from 'solid-js'
|
|
15
|
+
import { PortalDropdownMenu } from './PortalDropdownMenu'
|
|
16
|
+
|
|
17
|
+
describe('PortalDropdownMenu (v6.4.0)', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
cleanup()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function harness(initiallyOpen = true) {
|
|
23
|
+
const [open, setOpen] = createSignal(initiallyOpen)
|
|
24
|
+
let triggerRef: HTMLButtonElement | undefined
|
|
25
|
+
const result = render(() => (
|
|
26
|
+
<div id="harness-root">
|
|
27
|
+
<button ref={triggerRef} onClick={() => setOpen((v) => !v)}>
|
|
28
|
+
Open
|
|
29
|
+
</button>
|
|
30
|
+
<PortalDropdownMenu
|
|
31
|
+
open={open()}
|
|
32
|
+
onClose={() => setOpen(false)}
|
|
33
|
+
trigger={triggerRef}
|
|
34
|
+
width={144}
|
|
35
|
+
>
|
|
36
|
+
<button data-testid="menu-item">Item 1</button>
|
|
37
|
+
</PortalDropdownMenu>
|
|
38
|
+
</div>
|
|
39
|
+
))
|
|
40
|
+
return { ...result, getOpen: open, setOpen, getTrigger: () => triggerRef }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('mounts the menu on document.body, NOT inside the trigger parent', () => {
|
|
44
|
+
const { container } = harness(true)
|
|
45
|
+
// Menu should NOT be inside the harness root (it's portaled to body)
|
|
46
|
+
const harnessRoot = container.querySelector('#harness-root')
|
|
47
|
+
expect(harnessRoot).toBeTruthy()
|
|
48
|
+
expect(harnessRoot!.querySelector('[data-testid="menu-item"]')).toBeNull()
|
|
49
|
+
// But it IS in document.body
|
|
50
|
+
const menuItem = document.body.querySelector('[data-testid="menu-item"]')
|
|
51
|
+
expect(menuItem).toBeTruthy()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('does not render the menu when open=false', () => {
|
|
55
|
+
harness(false)
|
|
56
|
+
expect(document.body.querySelector('[data-testid="menu-item"]')).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('positions the menu using the trigger getBoundingClientRect (right-aligned, 4px below)', () => {
|
|
60
|
+
// Stub the trigger rect so we get deterministic coords in jsdom
|
|
61
|
+
const fakeRect = {
|
|
62
|
+
top: 100, bottom: 130, left: 200, right: 350,
|
|
63
|
+
width: 150, height: 30, x: 200, y: 100, toJSON: () => ({}),
|
|
64
|
+
} as DOMRect
|
|
65
|
+
// Patch getBoundingClientRect on HTMLElement.prototype just for this test
|
|
66
|
+
const orig = HTMLElement.prototype.getBoundingClientRect
|
|
67
|
+
HTMLElement.prototype.getBoundingClientRect = vi.fn(() => fakeRect)
|
|
68
|
+
try {
|
|
69
|
+
harness(true)
|
|
70
|
+
const menu = document.body.querySelector('[role="menu"]') as HTMLElement
|
|
71
|
+
expect(menu).toBeTruthy()
|
|
72
|
+
// Position: top = bottom + 4 = 134, left = right - width = 350 - 144 = 206
|
|
73
|
+
expect(menu.style.top).toBe('134px')
|
|
74
|
+
expect(menu.style.left).toBe('206px')
|
|
75
|
+
expect(menu.style.position).toBe('fixed')
|
|
76
|
+
} finally {
|
|
77
|
+
HTMLElement.prototype.getBoundingClientRect = orig
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('closes on outside click (mousedown on document.body outside menu)', () => {
|
|
82
|
+
const { getOpen } = harness(true)
|
|
83
|
+
expect(getOpen()).toBe(true)
|
|
84
|
+
// Click on a brand-new element appended to body, outside the menu
|
|
85
|
+
const stranger = document.createElement('div')
|
|
86
|
+
document.body.appendChild(stranger)
|
|
87
|
+
fireEvent.mouseDown(stranger)
|
|
88
|
+
expect(getOpen()).toBe(false)
|
|
89
|
+
document.body.removeChild(stranger)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('closes on Escape', () => {
|
|
93
|
+
const { getOpen } = harness(true)
|
|
94
|
+
expect(getOpen()).toBe(true)
|
|
95
|
+
fireEvent.keyDown(document, { key: 'Escape' })
|
|
96
|
+
expect(getOpen()).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('does not close when mousedown lands on the trigger (parent owns toggle)', () => {
|
|
100
|
+
const { getOpen, getTrigger } = harness(true)
|
|
101
|
+
expect(getOpen()).toBe(true)
|
|
102
|
+
fireEvent.mouseDown(getTrigger()!)
|
|
103
|
+
// Menu should still be open — trigger toggle is parent's responsibility
|
|
104
|
+
expect(getOpen()).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('does not close when mousedown lands inside the menu', () => {
|
|
108
|
+
const { getOpen } = harness(true)
|
|
109
|
+
const item = document.body.querySelector('[data-testid="menu-item"]') as HTMLElement
|
|
110
|
+
fireEvent.mouseDown(item)
|
|
111
|
+
expect(getOpen()).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PortalDropdownMenu (v6.4.0) — generic dropdown that mounts via
|
|
3
|
+
* `<Portal>` on `document.body` instead of an in-tree `position: absolute`
|
|
4
|
+
* sibling. Eliminates two pain points around the legacy in-tree pattern :
|
|
5
|
+
*
|
|
6
|
+
* 1. **`overflow: hidden` clipping** — when the trigger lives inside a
|
|
7
|
+
* chat bubble or a card with `overflow: hidden`, an absolutely
|
|
8
|
+
* positioned menu sibling gets clipped at the ancestor's boundary.
|
|
9
|
+
* Mounting on `document.body` escapes the clip stack entirely.
|
|
10
|
+
* 2. **`z-index` wars** — chat surfaces stack composer / message rails
|
|
11
|
+
* above the message list, and ancestor `z-index` creates a new
|
|
12
|
+
* stacking context that captures the in-tree menu. A portal is a
|
|
13
|
+
* sibling of the document, so a single `z-index: 9999` wins.
|
|
14
|
+
*
|
|
15
|
+
* The menu is positioned with `position: fixed` from the trigger's
|
|
16
|
+
* `getBoundingClientRect()`. We re-measure on `scroll` (capture phase, so
|
|
17
|
+
* nested scrollables also fire) and `resize` to keep the menu pinned
|
|
18
|
+
* while the user interacts with surrounding chrome.
|
|
19
|
+
*
|
|
20
|
+
* Close affordances : click outside, Escape, programmatic via `onClose`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Component, JSX, Show, createSignal, createEffect, onCleanup } from 'solid-js'
|
|
24
|
+
import { Portal } from 'solid-js/web'
|
|
25
|
+
|
|
26
|
+
export interface PortalDropdownMenuProps {
|
|
27
|
+
/**
|
|
28
|
+
* Whether the menu is currently open. Controlled by the parent.
|
|
29
|
+
*/
|
|
30
|
+
open: boolean
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Called when the menu wants to close (outside click / Escape / item
|
|
34
|
+
* click — it's the parent's job to actually flip `open` to false).
|
|
35
|
+
*/
|
|
36
|
+
onClose: () => void
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Trigger element used as the positioning anchor. The menu's right
|
|
40
|
+
* edge is aligned to the trigger's right edge, top to its bottom + 4px.
|
|
41
|
+
*/
|
|
42
|
+
trigger: HTMLElement | undefined
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Menu width in pixels. Used to compute the left coordinate so the menu's
|
|
46
|
+
* right edge aligns with the trigger's right edge. Default : `144` (the
|
|
47
|
+
* legacy table menu width — `w-36`).
|
|
48
|
+
*/
|
|
49
|
+
width?: number
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Menu content. Wrapped in the rounded / shadowed container — keep
|
|
53
|
+
* children minimal (just the items).
|
|
54
|
+
*/
|
|
55
|
+
children: JSX.Element
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Optional additional class names for the menu container, appended after
|
|
59
|
+
* the default Tailwind classes. Useful to override width or padding.
|
|
60
|
+
*/
|
|
61
|
+
class?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const PortalDropdownMenu: Component<PortalDropdownMenuProps> = (props) => {
|
|
65
|
+
const [position, setPosition] = createSignal({ top: 0, left: 0 })
|
|
66
|
+
let menuRef: HTMLDivElement | undefined
|
|
67
|
+
|
|
68
|
+
const updatePosition = () => {
|
|
69
|
+
const t = props.trigger
|
|
70
|
+
if (!t) return
|
|
71
|
+
const rect = t.getBoundingClientRect()
|
|
72
|
+
const w = props.width ?? 144
|
|
73
|
+
setPosition({
|
|
74
|
+
top: rect.bottom + 4,
|
|
75
|
+
left: rect.right - w,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Measure once when the menu opens, then react to viewport changes.
|
|
80
|
+
createEffect(() => {
|
|
81
|
+
if (!props.open) return
|
|
82
|
+
updatePosition()
|
|
83
|
+
|
|
84
|
+
const onDown = (e: MouseEvent) => {
|
|
85
|
+
const target = e.target as Node
|
|
86
|
+
if (menuRef?.contains(target) || props.trigger?.contains(target)) return
|
|
87
|
+
props.onClose()
|
|
88
|
+
}
|
|
89
|
+
const onKey = (e: KeyboardEvent) => {
|
|
90
|
+
if (e.key === 'Escape') props.onClose()
|
|
91
|
+
}
|
|
92
|
+
const onScrollOrResize = () => updatePosition()
|
|
93
|
+
|
|
94
|
+
document.addEventListener('mousedown', onDown)
|
|
95
|
+
document.addEventListener('keydown', onKey)
|
|
96
|
+
// Capture phase so scrolls inside nested containers (e.g. chat virtual
|
|
97
|
+
// list) also re-position the menu. `passive: true` because we never
|
|
98
|
+
// preventDefault here.
|
|
99
|
+
window.addEventListener('scroll', onScrollOrResize, { capture: true, passive: true })
|
|
100
|
+
window.addEventListener('resize', onScrollOrResize)
|
|
101
|
+
|
|
102
|
+
onCleanup(() => {
|
|
103
|
+
document.removeEventListener('mousedown', onDown)
|
|
104
|
+
document.removeEventListener('keydown', onKey)
|
|
105
|
+
window.removeEventListener('scroll', onScrollOrResize, { capture: true })
|
|
106
|
+
window.removeEventListener('resize', onScrollOrResize)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Show when={props.open}>
|
|
112
|
+
<Portal>
|
|
113
|
+
<div
|
|
114
|
+
ref={menuRef}
|
|
115
|
+
role="menu"
|
|
116
|
+
style={{
|
|
117
|
+
position: 'fixed',
|
|
118
|
+
top: `${position().top}px`,
|
|
119
|
+
left: `${position().left}px`,
|
|
120
|
+
'z-index': 9999,
|
|
121
|
+
width: `${props.width ?? 144}px`,
|
|
122
|
+
}}
|
|
123
|
+
class={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1 text-sm ${props.class ?? ''}`}
|
|
124
|
+
>
|
|
125
|
+
{props.children}
|
|
126
|
+
</div>
|
|
127
|
+
</Portal>
|
|
128
|
+
</Show>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -99,3 +99,54 @@ describe('Chart (iframe path) — exportable default (v6.1.0)', () => {
|
|
|
99
99
|
).not.toThrow()
|
|
100
100
|
})
|
|
101
101
|
})
|
|
102
|
+
|
|
103
|
+
describe('UIResourceRenderer.toolbarVariant — forwarding to ExpandableWrapper (v6.3.1)', () => {
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
cleanup()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
function tableComponent(): UIComponent {
|
|
109
|
+
return {
|
|
110
|
+
id: 'tbl',
|
|
111
|
+
type: 'table',
|
|
112
|
+
position: { colStart: 1, colSpan: 12 },
|
|
113
|
+
params: {
|
|
114
|
+
columns: [
|
|
115
|
+
{ key: 'name', label: 'Name' },
|
|
116
|
+
{ key: 'value', label: 'Value' },
|
|
117
|
+
],
|
|
118
|
+
rows: [{ name: 'a', value: 1 }],
|
|
119
|
+
} as any,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
it('default (toolbarVariant undefined) → expand button uses opacity-0 hover-only classes', () => {
|
|
124
|
+
const { container } = render(() => <UIResourceRenderer content={tableComponent()} />)
|
|
125
|
+
const btn = container.querySelector('button[aria-label="Expand to fullscreen"]')
|
|
126
|
+
expect(btn).toBeTruthy()
|
|
127
|
+
expect(btn!.className).toContain('opacity-0')
|
|
128
|
+
expect(btn!.className).toContain('group-hover:opacity-70')
|
|
129
|
+
expect(btn!.className).not.toContain('opacity-60')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('toolbarVariant="always-visible" → expand button uses opacity-60 (no group-hover gate)', () => {
|
|
133
|
+
const { container } = render(() => (
|
|
134
|
+
<UIResourceRenderer content={tableComponent()} toolbarVariant="always-visible" />
|
|
135
|
+
))
|
|
136
|
+
const btn = container.querySelector('button[aria-label="Expand to fullscreen"]')
|
|
137
|
+
expect(btn).toBeTruthy()
|
|
138
|
+
expect(btn!.className).toContain('opacity-60')
|
|
139
|
+
expect(btn!.className).not.toContain('opacity-0')
|
|
140
|
+
expect(btn!.className).not.toContain('group-hover:opacity-70')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('toolbarVariant="hover" (explicit) → matches default behavior', () => {
|
|
144
|
+
const { container } = render(() => (
|
|
145
|
+
<UIResourceRenderer content={tableComponent()} toolbarVariant="hover" />
|
|
146
|
+
))
|
|
147
|
+
const btn = container.querySelector('button[aria-label="Expand to fullscreen"]')
|
|
148
|
+
expect(btn).toBeTruthy()
|
|
149
|
+
expect(btn!.className).toContain('opacity-0')
|
|
150
|
+
expect(btn!.className).toContain('group-hover:opacity-70')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -41,6 +41,7 @@ import { CodeBlockRenderer } from './CodeBlockRenderer'
|
|
|
41
41
|
import { MapRenderer } from './MapRenderer'
|
|
42
42
|
import { GraphRenderer } from './GraphRenderer'
|
|
43
43
|
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
44
|
+
import { PortalDropdownMenu } from './PortalDropdownMenu'
|
|
44
45
|
import { RenderProvider } from './RenderContext'
|
|
45
46
|
import { useAction } from '../hooks/useAction'
|
|
46
47
|
import { marked } from 'marked'
|
|
@@ -121,6 +122,19 @@ export interface UIResourceRendererProps {
|
|
|
121
122
|
* @see ValidationErrorMode
|
|
122
123
|
*/
|
|
123
124
|
errorMode?: ValidationErrorMode
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Visibility behavior of the inline expand button shipped by every
|
|
128
|
+
* renderer wrapped in `<ExpandableWrapper>` (v6.3.1 — closes axe 4 of
|
|
129
|
+
* the deposium handoff).
|
|
130
|
+
* - `'hover'` (default) : button fades in on hover. Backwards-compatible.
|
|
131
|
+
* - `'always-visible'` : button visible at rest. Use this on chat
|
|
132
|
+
* surfaces / touch surfaces where the hover affordance is hidden.
|
|
133
|
+
*
|
|
134
|
+
* Forwarded to all internal renderers : table, chart (Chart.js path),
|
|
135
|
+
* graph, map, video, carousel, image-gallery, code.
|
|
136
|
+
*/
|
|
137
|
+
toolbarVariant?: 'hover' | 'always-visible'
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
/**
|
|
@@ -133,6 +147,7 @@ export interface UIResourceRendererProps {
|
|
|
133
147
|
function ChartRenderer(props: {
|
|
134
148
|
component: UIComponent
|
|
135
149
|
onError?: (error: RendererError) => void
|
|
150
|
+
toolbarVariant?: 'hover' | 'always-visible'
|
|
136
151
|
}) {
|
|
137
152
|
const [useNative, setUseNative] = createSignal(false)
|
|
138
153
|
const [iframeUrl, setIframeUrl] = createSignal<string>()
|
|
@@ -256,6 +271,7 @@ function ChartRenderer(props: {
|
|
|
256
271
|
>
|
|
257
272
|
<ChartJSRenderer
|
|
258
273
|
component={props.component}
|
|
274
|
+
toolbarVariant={props.toolbarVariant}
|
|
259
275
|
onError={(err) => props.onError?.({
|
|
260
276
|
type: 'render',
|
|
261
277
|
message: err.message,
|
|
@@ -486,6 +502,7 @@ export function renderCellValue(value: any, citationCtx?: CitationCtx): string {
|
|
|
486
502
|
function TableRenderer(props: {
|
|
487
503
|
component: UIComponent
|
|
488
504
|
onError?: (error: RendererError) => void
|
|
505
|
+
toolbarVariant?: 'hover' | 'always-visible'
|
|
489
506
|
}) {
|
|
490
507
|
const tableParams = props.component.params as any
|
|
491
508
|
let scrollContainerRef: HTMLDivElement | undefined
|
|
@@ -739,6 +756,8 @@ function TableRenderer(props: {
|
|
|
739
756
|
|
|
740
757
|
// Export dropdown state
|
|
741
758
|
const [showExportMenu, setShowExportMenu] = createSignal(false)
|
|
759
|
+
// v6.4.0 — trigger ref consumed by <PortalDropdownMenu> for positioning
|
|
760
|
+
let exportTriggerRef: HTMLButtonElement | undefined
|
|
742
761
|
|
|
743
762
|
const handleExport = (format: string) => {
|
|
744
763
|
setShowExportMenu(false)
|
|
@@ -818,35 +837,41 @@ function TableRenderer(props: {
|
|
|
818
837
|
}
|
|
819
838
|
|
|
820
839
|
return (
|
|
821
|
-
<ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)">
|
|
840
|
+
<ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)" toolbarVariant={props.toolbarVariant}>
|
|
822
841
|
<div class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden group ${
|
|
823
842
|
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : 'h-full'
|
|
824
843
|
}`}>
|
|
825
844
|
<Show when={exportable} fallback={<CopyButton getText={getTableCSV} title="Copy table (CSV)" position="top-right" />}>
|
|
826
845
|
<div class="absolute right-10 top-2 z-10">
|
|
827
846
|
<button
|
|
847
|
+
ref={exportTriggerRef}
|
|
828
848
|
onClick={() => setShowExportMenu(!showExportMenu())}
|
|
829
849
|
class="opacity-60 hover:opacity-100 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm"
|
|
830
850
|
title="Export table"
|
|
831
851
|
aria-label="Export table"
|
|
852
|
+
aria-haspopup="menu"
|
|
853
|
+
aria-expanded={showExportMenu()}
|
|
832
854
|
>
|
|
833
855
|
<svg class="w-3 h-3 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
834
856
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
835
857
|
</svg>
|
|
836
858
|
</button>
|
|
837
|
-
<
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
</
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
</
|
|
848
|
-
</
|
|
849
|
-
|
|
859
|
+
<PortalDropdownMenu
|
|
860
|
+
open={showExportMenu()}
|
|
861
|
+
onClose={() => setShowExportMenu(false)}
|
|
862
|
+
trigger={exportTriggerRef}
|
|
863
|
+
width={144}
|
|
864
|
+
>
|
|
865
|
+
<Show when={(exportFormats as string[]).includes('tsv')}>
|
|
866
|
+
<button onClick={() => handleExport('tsv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Copy TSV</button>
|
|
867
|
+
</Show>
|
|
868
|
+
<Show when={(exportFormats as string[]).includes('csv')}>
|
|
869
|
+
<button onClick={() => handleExport('csv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Download CSV</button>
|
|
870
|
+
</Show>
|
|
871
|
+
<Show when={(exportFormats as string[]).includes('json')}>
|
|
872
|
+
<button onClick={() => handleExport('json')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Download JSON</button>
|
|
873
|
+
</Show>
|
|
874
|
+
</PortalDropdownMenu>
|
|
850
875
|
</div>
|
|
851
876
|
</Show>
|
|
852
877
|
<div class={`p-4 ${isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''}`}>
|
|
@@ -1275,6 +1300,7 @@ function ComponentRenderer(props: {
|
|
|
1275
1300
|
component: UIComponent
|
|
1276
1301
|
onError?: (error: RendererError) => void
|
|
1277
1302
|
errorMode?: ValidationErrorMode
|
|
1303
|
+
toolbarVariant?: 'hover' | 'always-visible'
|
|
1278
1304
|
}) {
|
|
1279
1305
|
// Performance marks — visible in Chrome DevTools "Performance" panel under
|
|
1280
1306
|
// user timings. Always-on, SSR-safe (see utils/perf.ts).
|
|
@@ -1403,10 +1429,10 @@ function ComponentRenderer(props: {
|
|
|
1403
1429
|
allowRetry={true}
|
|
1404
1430
|
>
|
|
1405
1431
|
<Show when={props.component.type === 'chart'}>
|
|
1406
|
-
<ChartRenderer component={props.component} onError={props.onError} />
|
|
1432
|
+
<ChartRenderer component={props.component} onError={props.onError} toolbarVariant={props.toolbarVariant} />
|
|
1407
1433
|
</Show>
|
|
1408
1434
|
<Show when={props.component.type === 'table'}>
|
|
1409
|
-
<TableRenderer component={props.component} onError={props.onError} />
|
|
1435
|
+
<TableRenderer component={props.component} onError={props.onError} toolbarVariant={props.toolbarVariant} />
|
|
1410
1436
|
</Show>
|
|
1411
1437
|
<Show when={props.component.type === 'metric'}>
|
|
1412
1438
|
<MetricRenderer component={props.component} />
|
|
@@ -1430,7 +1456,7 @@ function ComponentRenderer(props: {
|
|
|
1430
1456
|
<GridRenderer component={props.component} onError={props.onError} />
|
|
1431
1457
|
</Show>
|
|
1432
1458
|
<Show when={props.component.type === 'carousel'}>
|
|
1433
|
-
<CarouselRenderer items={(props.component.params as any)?.items || []} height={(props.component.params as any)?.height} />
|
|
1459
|
+
<CarouselRenderer items={(props.component.params as any)?.items || []} height={(props.component.params as any)?.height} toolbarVariant={props.toolbarVariant} />
|
|
1434
1460
|
</Show>
|
|
1435
1461
|
<Show when={props.component.type === 'artifact'}>
|
|
1436
1462
|
<ArtifactRenderer params={props.component.params as any} />
|
|
@@ -1445,19 +1471,19 @@ function ComponentRenderer(props: {
|
|
|
1445
1471
|
<ActionGroupRenderer component={props.component} />
|
|
1446
1472
|
</Show>
|
|
1447
1473
|
<Show when={props.component.type === 'image-gallery'}>
|
|
1448
|
-
<ImageGalleryRenderer component={props.component} />
|
|
1474
|
+
<ImageGalleryRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
|
|
1449
1475
|
</Show>
|
|
1450
1476
|
<Show when={props.component.type === 'video'}>
|
|
1451
|
-
<VideoRenderer component={props.component} />
|
|
1477
|
+
<VideoRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
|
|
1452
1478
|
</Show>
|
|
1453
1479
|
<Show when={props.component.type === 'code'}>
|
|
1454
|
-
<CodeBlockRenderer component={props.component} />
|
|
1480
|
+
<CodeBlockRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
|
|
1455
1481
|
</Show>
|
|
1456
1482
|
<Show when={props.component.type === 'map'}>
|
|
1457
|
-
<MapRenderer component={props.component} />
|
|
1483
|
+
<MapRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
|
|
1458
1484
|
</Show>
|
|
1459
1485
|
<Show when={props.component.type === 'graph'}>
|
|
1460
|
-
<GraphRenderer component={props.component} />
|
|
1486
|
+
<GraphRenderer component={props.component} toolbarVariant={props.toolbarVariant} />
|
|
1461
1487
|
</Show>
|
|
1462
1488
|
</GenerativeUIErrorBoundary>
|
|
1463
1489
|
)
|
|
@@ -1742,7 +1768,7 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1742
1768
|
|
|
1743
1769
|
// Wrapper function for RenderContext (breaks circular dependency)
|
|
1744
1770
|
const renderComponent = (component: UIComponent, onError?: (error: RendererError) => void) => (
|
|
1745
|
-
<ComponentRenderer component={component} onError={onError} errorMode={props.errorMode} />
|
|
1771
|
+
<ComponentRenderer component={component} onError={onError} errorMode={props.errorMode} toolbarVariant={props.toolbarVariant} />
|
|
1746
1772
|
)
|
|
1747
1773
|
|
|
1748
1774
|
return (
|
|
@@ -1752,7 +1778,7 @@ export const UIResourceRenderer: Component<UIResourceRendererProps> = (props) =>
|
|
|
1752
1778
|
<For each={layoutData.components}>
|
|
1753
1779
|
{(component) => (
|
|
1754
1780
|
<div style={getGridStyleString(component)}>
|
|
1755
|
-
<ComponentRenderer component={component} onError={props.onError} errorMode={props.errorMode} />
|
|
1781
|
+
<ComponentRenderer component={component} onError={props.onError} errorMode={props.errorMode} toolbarVariant={props.toolbarVariant} />
|
|
1756
1782
|
</div>
|
|
1757
1783
|
)}
|
|
1758
1784
|
</For>
|
|
@@ -24,6 +24,12 @@ export interface VideoRendererProps {
|
|
|
24
24
|
* Error callback
|
|
25
25
|
*/
|
|
26
26
|
onError?: (error: Error) => void
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).
|
|
30
|
+
* @see ExpandableWrapperProps.toolbarVariant
|
|
31
|
+
*/
|
|
32
|
+
toolbarVariant?: 'hover' | 'always-visible'
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -132,6 +138,7 @@ export const VideoRenderer: Component<VideoRendererProps> = (props) => {
|
|
|
132
138
|
title={params()?.title || 'Video'}
|
|
133
139
|
copyData={params()?.url || ''}
|
|
134
140
|
copyLabel="Copy video URL"
|
|
141
|
+
toolbarVariant={props.toolbarVariant}
|
|
135
142
|
>
|
|
136
143
|
<div class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
|
|
137
144
|
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
package/src/components/index.ts
CHANGED
|
@@ -92,5 +92,9 @@ export type { RenderContextValue, RenderComponentFn } from './RenderContext'
|
|
|
92
92
|
export { ElicitationForm } from './ElicitationForm'
|
|
93
93
|
export type { ElicitationFormProps } from './ElicitationForm'
|
|
94
94
|
|
|
95
|
+
// v6.4.0 — Portal-mounted dropdown (used by table + graph Export menus)
|
|
96
|
+
export { PortalDropdownMenu } from './PortalDropdownMenu'
|
|
97
|
+
export type { PortalDropdownMenuProps } from './PortalDropdownMenu'
|
|
98
|
+
|
|
95
99
|
// Default exports for lazy loading compatibility
|
|
96
100
|
export { UIResourceRenderer as default } from './UIResourceRenderer'
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type { FeedbackInlineProps, FeedbackInlineContext } from './components/Fe
|
|
|
44
44
|
export { ChatBusProvider, useChatBus } from './hooks/useChatBus'
|
|
45
45
|
export { ChatPrompt } from './components/ChatPrompt'
|
|
46
46
|
export { ElicitationForm } from './components/ElicitationForm'
|
|
47
|
+
export { PortalDropdownMenu } from './components/PortalDropdownMenu'
|
|
47
48
|
export { ScratchpadPanel } from './components/ScratchpadPanel'
|
|
48
49
|
export {
|
|
49
50
|
dispatchScratchpad,
|
|
@@ -126,6 +127,7 @@ export type { ExpandableWrapperProps } from './components/ExpandableWrapper'
|
|
|
126
127
|
export type { ComponentToolbarProps, ToolbarAction, ToolbarIcon } from './components/ComponentToolbar'
|
|
127
128
|
export type { ChatPromptProps } from './components/ChatPrompt'
|
|
128
129
|
export type { ElicitationFormProps } from './components/ElicitationForm'
|
|
130
|
+
export type { PortalDropdownMenuProps } from './components/PortalDropdownMenu'
|
|
129
131
|
export type { ScratchpadPanelProps } from './components/ScratchpadPanel'
|
|
130
132
|
export type { VerifiedTextProps } from './components/VerifiedText'
|
|
131
133
|
export type { DataPreviewSectionProps } from './components/DataPreviewSection'
|