@seed-ship/mcp-ui-solid 6.1.0 → 6.2.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 +65 -0
- package/dist/components/CarouselRenderer.cjs +41 -30
- package/dist/components/CarouselRenderer.cjs.map +1 -1
- package/dist/components/CarouselRenderer.d.ts.map +1 -1
- package/dist/components/CarouselRenderer.js +42 -31
- package/dist/components/CarouselRenderer.js.map +1 -1
- package/dist/components/CodeBlockRenderer.cjs +88 -25
- package/dist/components/CodeBlockRenderer.cjs.map +1 -1
- package/dist/components/CodeBlockRenderer.d.ts.map +1 -1
- package/dist/components/CodeBlockRenderer.js +89 -26
- package/dist/components/CodeBlockRenderer.js.map +1 -1
- package/dist/components/ImageGalleryRenderer.cjs +101 -77
- package/dist/components/ImageGalleryRenderer.cjs.map +1 -1
- package/dist/components/ImageGalleryRenderer.d.ts.map +1 -1
- package/dist/components/ImageGalleryRenderer.js +102 -78
- package/dist/components/ImageGalleryRenderer.js.map +1 -1
- package/dist/components/MapRenderer.cjs +94 -34
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +107 -47
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/VideoRenderer.cjs +95 -74
- package/dist/components/VideoRenderer.cjs.map +1 -1
- package/dist/components/VideoRenderer.d.ts.map +1 -1
- package/dist/components/VideoRenderer.js +96 -75
- package/dist/components/VideoRenderer.js.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/CarouselRenderer.tsx +9 -1
- package/src/components/CodeBlockRenderer.tsx +65 -5
- package/src/components/ImageGalleryRenderer.test.tsx +18 -7
- package/src/components/ImageGalleryRenderer.tsx +22 -3
- package/src/components/MapRenderer.tsx +68 -14
- package/src/components/VideoRenderer.tsx +14 -4
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -7,7 +7,18 @@
|
|
|
7
7
|
import { Component, createEffect, onCleanup, createSignal, Show, For } from 'solid-js'
|
|
8
8
|
import { isServer } from 'solid-js/web'
|
|
9
9
|
import type { UIComponent, CodeComponentParams } from '../types'
|
|
10
|
-
import { ExpandableWrapper } from './ExpandableWrapper'
|
|
10
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
11
|
+
import { highlightQuery } from './UIResourceRenderer'
|
|
12
|
+
|
|
13
|
+
/** Map of `params.language` → file extension for the v6.2.0 download button. */
|
|
14
|
+
const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|
15
|
+
typescript: 'ts', tsx: 'tsx', javascript: 'js', jsx: 'jsx',
|
|
16
|
+
python: 'py', ruby: 'rb', go: 'go', rust: 'rs', java: 'java',
|
|
17
|
+
kotlin: 'kt', swift: 'swift', php: 'php', csharp: 'cs', cpp: 'cpp',
|
|
18
|
+
c: 'c', sql: 'sql', json: 'json', yaml: 'yml', toml: 'toml',
|
|
19
|
+
bash: 'sh', shell: 'sh', html: 'html', css: 'css', scss: 'scss',
|
|
20
|
+
markdown: 'md', xml: 'xml', graphql: 'graphql',
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
// Lazy load highlight.js
|
|
13
24
|
let hljs: any = null
|
|
@@ -34,6 +45,35 @@ export const CodeBlockRenderer: Component<CodeBlockRendererProps> = (props) => {
|
|
|
34
45
|
const [wordWrap, setWordWrap] = createSignal(false)
|
|
35
46
|
|
|
36
47
|
const params = () => props.params || (props.component?.params as CodeComponentParams)
|
|
48
|
+
const isExpanded = useExpanded()
|
|
49
|
+
const [searchQuery, setSearchQuery] = createSignal('')
|
|
50
|
+
|
|
51
|
+
// v6.2.0 — search highlight: re-wraps `<mark>` around matches in the
|
|
52
|
+
// already-highlighted (hljs) HTML output. `highlightQuery` is the same
|
|
53
|
+
// helper TableRenderer uses (only wraps text outside of HTML tags so
|
|
54
|
+
// syntax span colors stay intact).
|
|
55
|
+
const displayedHTML = () => {
|
|
56
|
+
const q = searchQuery().trim()
|
|
57
|
+
return q ? highlightQuery(highlightedCode(), q) : highlightedCode()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleDownload = () => {
|
|
61
|
+
const code = params()?.code
|
|
62
|
+
if (!code) return
|
|
63
|
+
const lang = (params()?.language || '').toLowerCase()
|
|
64
|
+
const ext = LANGUAGE_EXTENSIONS[lang] || 'txt'
|
|
65
|
+
const stem = (params()?.filename || `code-${Date.now()}`).replace(/\.[^.]+$/, '')
|
|
66
|
+
const filename = stem.endsWith(`.${ext}`) ? stem : `${stem}.${ext}`
|
|
67
|
+
const blob = new Blob([code], { type: 'text/plain' })
|
|
68
|
+
const url = URL.createObjectURL(blob)
|
|
69
|
+
const a = document.createElement('a')
|
|
70
|
+
a.href = url
|
|
71
|
+
a.download = filename
|
|
72
|
+
document.body.appendChild(a)
|
|
73
|
+
a.click()
|
|
74
|
+
document.body.removeChild(a)
|
|
75
|
+
URL.revokeObjectURL(url)
|
|
76
|
+
}
|
|
37
77
|
|
|
38
78
|
// Load highlight.js on mount
|
|
39
79
|
createEffect(async () => {
|
|
@@ -152,13 +192,33 @@ export const CodeBlockRenderer: Component<CodeBlockRendererProps> = (props) => {
|
|
|
152
192
|
|
|
153
193
|
return (
|
|
154
194
|
<ExpandableWrapper title={params()?.filename || params()?.language || 'Code'} copyData={params()?.code} copyLabel="Copy code">
|
|
155
|
-
<div class=
|
|
195
|
+
<div class={`w-full bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden text-sm flex flex-col ${isExpanded() ? 'flex-1 min-h-0' : ''}`}>
|
|
156
196
|
{/* Header */}
|
|
157
197
|
<div class="flex items-center justify-between px-4 py-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shrink-0">
|
|
158
198
|
<div class="font-mono text-xs text-gray-600 dark:text-gray-400">
|
|
159
199
|
{params()?.filename || params()?.language || 'Code'}
|
|
160
200
|
</div>
|
|
161
201
|
<div class="flex items-center gap-2">
|
|
202
|
+
{/* Search input (v6.2.0) */}
|
|
203
|
+
<input
|
|
204
|
+
type="text"
|
|
205
|
+
value={searchQuery()}
|
|
206
|
+
onInput={(e) => setSearchQuery(e.currentTarget.value)}
|
|
207
|
+
placeholder="Search…"
|
|
208
|
+
class="px-2 py-0.5 text-xs border border-gray-200 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none w-32"
|
|
209
|
+
aria-label="Search in code"
|
|
210
|
+
/>
|
|
211
|
+
{/* Download button (v6.2.0) */}
|
|
212
|
+
<button
|
|
213
|
+
onClick={handleDownload}
|
|
214
|
+
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 focus:outline-none transition-colors"
|
|
215
|
+
aria-label="Download code as file"
|
|
216
|
+
title="Download code"
|
|
217
|
+
>
|
|
218
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
219
|
+
<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" />
|
|
220
|
+
</svg>
|
|
221
|
+
</button>
|
|
162
222
|
{/* Word wrap toggle */}
|
|
163
223
|
<button
|
|
164
224
|
onClick={() => setWordWrap(!wordWrap())}
|
|
@@ -192,8 +252,8 @@ export const CodeBlockRenderer: Component<CodeBlockRendererProps> = (props) => {
|
|
|
192
252
|
|
|
193
253
|
{/* Code Area */}
|
|
194
254
|
<div
|
|
195
|
-
class=
|
|
196
|
-
style={params()?.maxHeight ? { 'max-height': params()?.maxHeight } : {}}
|
|
255
|
+
class={`relative overflow-auto flex ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
256
|
+
style={!isExpanded() && params()?.maxHeight ? { 'max-height': params()?.maxHeight } : {}}
|
|
197
257
|
>
|
|
198
258
|
{/* Line Numbers */}
|
|
199
259
|
<Show when={params()?.showLineNumbers !== false}>
|
|
@@ -212,7 +272,7 @@ export const CodeBlockRenderer: Component<CodeBlockRendererProps> = (props) => {
|
|
|
212
272
|
>
|
|
213
273
|
<code
|
|
214
274
|
class={`hljs ${params()?.language ? `language-${params()?.language}` : ''}`}
|
|
215
|
-
innerHTML={
|
|
275
|
+
innerHTML={displayedHTML()}
|
|
216
276
|
/>
|
|
217
277
|
</pre>
|
|
218
278
|
</div>
|
|
@@ -120,10 +120,16 @@ describe('ImageGalleryRenderer', () => {
|
|
|
120
120
|
})
|
|
121
121
|
|
|
122
122
|
it('renders buttons for images when lightbox is enabled', () => {
|
|
123
|
-
render(() => <ImageGalleryRenderer params={defaultParams} />)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
123
|
+
const { container } = render(() => <ImageGalleryRenderer params={defaultParams} />)
|
|
124
|
+
|
|
125
|
+
// Filter to image-trigger buttons (have aria-label "View image …"). The
|
|
126
|
+
// ExpandableWrapper expand button (added in v6.2.0) is excluded.
|
|
127
|
+
// Image-trigger buttons have a tailwind `relative overflow-hidden` group
|
|
128
|
+
// class. The ExpandableWrapper expand button (added v6.2.0) has a
|
|
129
|
+
// different `absolute top-2 right-2` class — this filter excludes it.
|
|
130
|
+
const buttons = Array.from(container.querySelectorAll('button')).filter(
|
|
131
|
+
(b) => b.className.includes('relative overflow-hidden')
|
|
132
|
+
)
|
|
127
133
|
expect(buttons.length).toBe(3)
|
|
128
134
|
})
|
|
129
135
|
|
|
@@ -133,10 +139,15 @@ describe('ImageGalleryRenderer', () => {
|
|
|
133
139
|
lightbox: false,
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
render(() => <ImageGalleryRenderer params={paramsNoLightbox} />)
|
|
142
|
+
const { container } = render(() => <ImageGalleryRenderer params={paramsNoLightbox} />)
|
|
137
143
|
|
|
138
|
-
//
|
|
139
|
-
|
|
144
|
+
// Same filter as above — ignore the ExpandableWrapper expand button (v6.2.0).
|
|
145
|
+
// Image-trigger buttons have a tailwind `relative overflow-hidden` group
|
|
146
|
+
// class. The ExpandableWrapper expand button (added v6.2.0) has a
|
|
147
|
+
// different `absolute top-2 right-2` class — this filter excludes it.
|
|
148
|
+
const buttons = Array.from(container.querySelectorAll('button')).filter(
|
|
149
|
+
(b) => b.className.includes('relative overflow-hidden')
|
|
150
|
+
)
|
|
140
151
|
expect(buttons.length).toBe(3)
|
|
141
152
|
})
|
|
142
153
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Component, createSignal, For, Show } from 'solid-js'
|
|
7
7
|
import type { UIComponent, ImageGalleryParams } from '../types'
|
|
8
8
|
import { LightboxOverlay } from './LightboxOverlay'
|
|
9
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
9
10
|
|
|
10
11
|
export interface ImageGalleryRendererProps {
|
|
11
12
|
/**
|
|
@@ -19,8 +20,18 @@ export interface ImageGalleryRendererProps {
|
|
|
19
20
|
params?: ImageGalleryParams
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** Build a newline-separated list of image URLs (with captions when present)
|
|
24
|
+
* for the ExpandableWrapper copy button. v6.2.0. */
|
|
25
|
+
function imagesToTextList(p: ImageGalleryParams | undefined): string {
|
|
26
|
+
if (!p) return ''
|
|
27
|
+
return (p.images ?? [])
|
|
28
|
+
.map((img) => (img.caption ? `${img.url}\t${img.caption}` : img.url))
|
|
29
|
+
.join('\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
export const ImageGalleryRenderer: Component<ImageGalleryRendererProps> = (props) => {
|
|
23
33
|
const [selectedIndex, setSelectedIndex] = createSignal<number | null>(null)
|
|
34
|
+
const isExpanded = useExpanded()
|
|
24
35
|
|
|
25
36
|
const params = () => props.params || (props.component?.params as ImageGalleryParams)
|
|
26
37
|
|
|
@@ -72,16 +83,23 @@ export const ImageGalleryRenderer: Component<ImageGalleryRendererProps> = (props
|
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
return (
|
|
75
|
-
<
|
|
86
|
+
<ExpandableWrapper
|
|
87
|
+
title={params()?.title || 'Gallery'}
|
|
88
|
+
copyData={imagesToTextList(params())}
|
|
89
|
+
copyLabel="Copy image URLs"
|
|
90
|
+
>
|
|
91
|
+
<div class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
|
|
92
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
93
|
+
}`}>
|
|
76
94
|
{/* Title */}
|
|
77
95
|
<Show when={params()?.title}>
|
|
78
|
-
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
96
|
+
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
79
97
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{params()!.title}</h3>
|
|
80
98
|
</div>
|
|
81
99
|
</Show>
|
|
82
100
|
|
|
83
101
|
{/* Gallery Grid */}
|
|
84
|
-
<div class={`grid ${columnsClass()} ${gapClass()} p-4`}>
|
|
102
|
+
<div class={`grid ${columnsClass()} ${gapClass()} p-4 ${isExpanded() ? 'flex-1 min-h-0 overflow-auto' : ''}`}>
|
|
85
103
|
<For each={params()?.images}>
|
|
86
104
|
{(image, index) => (
|
|
87
105
|
<button
|
|
@@ -142,5 +160,6 @@ export const ImageGalleryRenderer: Component<ImageGalleryRendererProps> = (props
|
|
|
142
160
|
onNavigate={setSelectedIndex}
|
|
143
161
|
/>
|
|
144
162
|
</div>
|
|
163
|
+
</ExpandableWrapper>
|
|
145
164
|
)
|
|
146
165
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js'
|
|
8
8
|
import { isServer } from 'solid-js/web'
|
|
9
9
|
import type { UIComponent, MapComponentParams, MapClusterOptions, MapGeoJSONStyle, MapPopupConfig, MapLayer, MapPMTilesConfig } from '../types'
|
|
10
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
10
11
|
|
|
11
12
|
// Lazy load leaflet (it doesn't support SSR well)
|
|
12
13
|
let L: any = null
|
|
@@ -165,14 +166,55 @@ function addGeoJSONLayer(
|
|
|
165
166
|
|
|
166
167
|
// ─── Component ──────────────────────────────────────────────
|
|
167
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Build a GeoJSON FeatureCollection from the map's `markers` (and any
|
|
171
|
+
* inlined GeoJSON layers, when present). Used by the "Copy data" button
|
|
172
|
+
* shipped via `<ExpandableWrapper>` (v6.2.0). Best-effort — clusters,
|
|
173
|
+
* tile layers, and choropleth-only data don't get round-tripped.
|
|
174
|
+
*/
|
|
175
|
+
function mapToGeoJSON(p: MapComponentParams | undefined): string {
|
|
176
|
+
if (!p) return '{"type":"FeatureCollection","features":[]}'
|
|
177
|
+
const features: any[] = []
|
|
178
|
+
for (const marker of p.markers ?? []) {
|
|
179
|
+
const pos: any = marker.position as any
|
|
180
|
+
// Accept both [lat, lng] tuple and {lat, lng} object shapes (v5.0.2 spec)
|
|
181
|
+
const lat = Array.isArray(pos) ? pos[0] : pos?.lat
|
|
182
|
+
const lng = Array.isArray(pos) ? pos[1] : pos?.lng
|
|
183
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') continue
|
|
184
|
+
features.push({
|
|
185
|
+
type: 'Feature',
|
|
186
|
+
geometry: { type: 'Point', coordinates: [lng, lat] },
|
|
187
|
+
properties: {
|
|
188
|
+
...(marker.tooltip ? { tooltip: marker.tooltip } : {}),
|
|
189
|
+
...(marker.popup ? { popup: marker.popup } : {}),
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
return JSON.stringify({ type: 'FeatureCollection', features }, null, 2)
|
|
194
|
+
}
|
|
195
|
+
|
|
168
196
|
export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
169
197
|
let mapContainer: HTMLDivElement | undefined
|
|
170
198
|
let mapInstance: any = null
|
|
171
199
|
const [isLeafletLoaded, setIsLeafletLoaded] = createSignal(false)
|
|
172
200
|
const [error, setError] = createSignal<string | null>(null)
|
|
201
|
+
const isExpanded = useExpanded()
|
|
173
202
|
|
|
174
203
|
const params = () => props.params || (props.component?.params as MapComponentParams)
|
|
175
204
|
|
|
205
|
+
// v6.2.0 — Leaflet has to be told to re-measure when its container
|
|
206
|
+
// resizes (e.g. transitioning to fullscreen via ExpandableWrapper).
|
|
207
|
+
// We give the DOM a tick to settle the new dimensions, then ask
|
|
208
|
+
// Leaflet to reflow tiles.
|
|
209
|
+
createEffect(() => {
|
|
210
|
+
const expanded = isExpanded()
|
|
211
|
+
if (!mapInstance) return
|
|
212
|
+
// Read the signal so the effect re-runs on toggle ; the value is
|
|
213
|
+
// observed for its side effects on layout.
|
|
214
|
+
void expanded
|
|
215
|
+
setTimeout(() => mapInstance?.invalidateSize?.(), 100)
|
|
216
|
+
})
|
|
217
|
+
|
|
176
218
|
// Initialize Map
|
|
177
219
|
createEffect(async () => {
|
|
178
220
|
if (isServer) return // Don't run on server
|
|
@@ -384,19 +426,31 @@ export const MapRenderer: Component<MapRendererProps> = (props) => {
|
|
|
384
426
|
})
|
|
385
427
|
|
|
386
428
|
return (
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
429
|
+
<ExpandableWrapper
|
|
430
|
+
title={'Map'}
|
|
431
|
+
copyData={mapToGeoJSON(params())}
|
|
432
|
+
copyLabel="Copy markers as GeoJSON"
|
|
433
|
+
>
|
|
434
|
+
<div class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${params()?.className || ''} ${
|
|
435
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
436
|
+
}`}>
|
|
437
|
+
<Show when={error()}>
|
|
438
|
+
<div class="p-4 text-red-500 bg-red-50 dark:bg-red-900/20 text-center">
|
|
439
|
+
{error()}
|
|
440
|
+
</div>
|
|
441
|
+
</Show>
|
|
442
|
+
<Show when={!error()}>
|
|
443
|
+
<div
|
|
444
|
+
ref={mapContainer}
|
|
445
|
+
style={
|
|
446
|
+
isExpanded()
|
|
447
|
+
? { height: '100%', width: '100%', 'z-index': 0 }
|
|
448
|
+
: { height: params()?.height || '400px', width: '100%', 'z-index': 0 }
|
|
449
|
+
}
|
|
450
|
+
class={`relative z-0 ${isExpanded() ? 'flex-1 min-h-0' : ''}`}
|
|
451
|
+
/>
|
|
452
|
+
</Show>
|
|
453
|
+
</div>
|
|
454
|
+
</ExpandableWrapper>
|
|
401
455
|
)
|
|
402
456
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Component, createMemo, Show } from 'solid-js'
|
|
9
9
|
import type { UIComponent, VideoComponentParams } from '../types'
|
|
10
|
+
import { ExpandableWrapper, useExpanded } from './ExpandableWrapper'
|
|
10
11
|
|
|
11
12
|
export interface VideoRendererProps {
|
|
12
13
|
/**
|
|
@@ -68,6 +69,7 @@ function parseVideoUrl(url: string): VideoInfo {
|
|
|
68
69
|
|
|
69
70
|
export const VideoRenderer: Component<VideoRendererProps> = (props) => {
|
|
70
71
|
const params = () => props.params || (props.component?.params as VideoComponentParams)
|
|
72
|
+
const isExpanded = useExpanded()
|
|
71
73
|
|
|
72
74
|
const videoInfo = createMemo(() => parseVideoUrl(params()?.url || ''))
|
|
73
75
|
|
|
@@ -126,7 +128,14 @@ export const VideoRenderer: Component<VideoRendererProps> = (props) => {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
return (
|
|
129
|
-
<
|
|
131
|
+
<ExpandableWrapper
|
|
132
|
+
title={params()?.title || 'Video'}
|
|
133
|
+
copyData={params()?.url || ''}
|
|
134
|
+
copyLabel="Copy video URL"
|
|
135
|
+
>
|
|
136
|
+
<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
|
+
isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''
|
|
138
|
+
}`}>
|
|
130
139
|
{/* Title */}
|
|
131
140
|
<Show when={params()?.title}>
|
|
132
141
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
@@ -134,8 +143,8 @@ export const VideoRenderer: Component<VideoRendererProps> = (props) => {
|
|
|
134
143
|
</div>
|
|
135
144
|
</Show>
|
|
136
145
|
|
|
137
|
-
{/* Video Container */}
|
|
138
|
-
<div class={`relative ${aspectClass()}
|
|
146
|
+
{/* Video Container — when expanded, fill remaining space (override aspect ratio) */}
|
|
147
|
+
<div class={`relative bg-black ${isExpanded() ? 'flex-1 min-h-0' : aspectClass()}`}>
|
|
139
148
|
<Show
|
|
140
149
|
when={embedUrl()}
|
|
141
150
|
fallback={
|
|
@@ -170,11 +179,12 @@ export const VideoRenderer: Component<VideoRendererProps> = (props) => {
|
|
|
170
179
|
|
|
171
180
|
{/* Caption */}
|
|
172
181
|
<Show when={params()?.caption}>
|
|
173
|
-
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
|
|
182
|
+
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
174
183
|
<p class="text-sm text-gray-600 dark:text-gray-400">{params()!.caption}</p>
|
|
175
184
|
</div>
|
|
176
185
|
</Show>
|
|
177
186
|
</div>
|
|
187
|
+
</ExpandableWrapper>
|
|
178
188
|
)
|
|
179
189
|
}
|
|
180
190
|
|