@kitnai/chat 0.8.1 → 0.9.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/dist/custom-elements.json +247 -0
- package/dist/kitn-chat.es.js +29 -29
- package/dist/llms/llms-full.txt +32 -4
- package/dist/llms/llms.txt +3 -3
- package/frameworks/react/index.tsx +47 -5
- package/llms-full.txt +32 -4
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/artifact.tsx +241 -82
- package/src/components/card-fallback.tsx +28 -0
- package/src/components/card-renderer.tsx +52 -0
- package/src/components/component-meta.json +169 -12
- package/src/elements/artifact.stories.tsx +214 -0
- package/src/elements/artifact.tsx +95 -30
- package/src/elements/cards.stories.tsx +54 -0
- package/src/elements/cards.tsx +91 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/element-meta.json +156 -0
- package/src/elements/element-types.d.ts +34 -0
- package/src/elements/register.ts +1 -0
- package/src/elements/resizable.d.ts +27 -0
- package/src/elements/resizable.stories.tsx +226 -1
- package/src/elements/resizable.tsx +208 -3
- package/src/index.ts +10 -0
- package/src/primitives/card-registry.tsx +58 -0
- package/src/stories/docs/generative-ui-overview.mdx +59 -0
- package/src/ui/resizable.tsx +33 -4
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
FileText,
|
|
27
27
|
ExternalLink,
|
|
28
28
|
Download,
|
|
29
|
+
Maximize2,
|
|
30
|
+
Minimize2,
|
|
29
31
|
} from 'lucide-solid';
|
|
30
32
|
|
|
31
33
|
export type ArtifactTab = 'preview' | 'code';
|
|
@@ -52,6 +54,32 @@ export interface ArtifactProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>,
|
|
|
52
54
|
onTabChange?: (tab: ArtifactTab) => void;
|
|
53
55
|
/** Fired when a file is selected in the tree. */
|
|
54
56
|
onFileSelect?: (path: string) => void;
|
|
57
|
+
// view-state
|
|
58
|
+
/** Controlled maximize view-state (drives the expand/restore button). */
|
|
59
|
+
maximized?: boolean;
|
|
60
|
+
/** Fired when the expand/restore button toggles the maximize view-state. */
|
|
61
|
+
onMaximizeChange?: (maximized: boolean) => void;
|
|
62
|
+
// toolbar composition — existing five default SHOWN (no-* flags invert in the facade)
|
|
63
|
+
/** Show the back/forward nav buttons. Default `true`. */
|
|
64
|
+
showNav?: boolean;
|
|
65
|
+
/** Show the reload button. Default `true`. */
|
|
66
|
+
showReload?: boolean;
|
|
67
|
+
/** Show the home button. Default `true`. */
|
|
68
|
+
showHome?: boolean;
|
|
69
|
+
/** Show the editable path/address field. Default `true`. */
|
|
70
|
+
showPathField?: boolean;
|
|
71
|
+
/** Show the Preview|Code tab toggle. Default `true`. */
|
|
72
|
+
showTabs?: boolean;
|
|
73
|
+
// new affordances — OPT-IN (default hidden; see resolved decision #2)
|
|
74
|
+
/** Show the expand-to-fill button. Default `false` (opt-in). */
|
|
75
|
+
expandable?: boolean;
|
|
76
|
+
/** Show the open-in-new-tab button. Default `false` (opt-in). */
|
|
77
|
+
openInTab?: boolean;
|
|
78
|
+
// chrome
|
|
79
|
+
/** Standalone chrome: rounded + bordered (default in-panel = square, borderless). */
|
|
80
|
+
standalone?: boolean;
|
|
81
|
+
/** Make the path field read-only (visible, nav-tracking, non-editable). */
|
|
82
|
+
readonlyPath?: boolean;
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
const DEFAULT_SANDBOX = 'allow-scripts allow-forms';
|
|
@@ -86,7 +114,21 @@ export function isPdfUrl(url: string, files: ArtifactFile[]): boolean {
|
|
|
86
114
|
*/
|
|
87
115
|
export function Artifact(props: ArtifactProps): JSX.Element {
|
|
88
116
|
const merged = mergeProps(
|
|
89
|
-
{
|
|
117
|
+
{
|
|
118
|
+
files: [] as ArtifactFile[],
|
|
119
|
+
tab: 'preview' as ArtifactTab,
|
|
120
|
+
sandbox: DEFAULT_SANDBOX,
|
|
121
|
+
showNav: true,
|
|
122
|
+
showReload: true,
|
|
123
|
+
showHome: true,
|
|
124
|
+
showPathField: true,
|
|
125
|
+
showTabs: true,
|
|
126
|
+
expandable: false,
|
|
127
|
+
openInTab: false,
|
|
128
|
+
standalone: false,
|
|
129
|
+
readonlyPath: false,
|
|
130
|
+
maximized: false,
|
|
131
|
+
},
|
|
90
132
|
props,
|
|
91
133
|
);
|
|
92
134
|
const [local, rest] = splitProps(merged, [
|
|
@@ -99,6 +141,17 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
99
141
|
'onNavigate',
|
|
100
142
|
'onTabChange',
|
|
101
143
|
'onFileSelect',
|
|
144
|
+
'maximized',
|
|
145
|
+
'onMaximizeChange',
|
|
146
|
+
'showNav',
|
|
147
|
+
'showReload',
|
|
148
|
+
'showHome',
|
|
149
|
+
'showPathField',
|
|
150
|
+
'showTabs',
|
|
151
|
+
'expandable',
|
|
152
|
+
'openInTab',
|
|
153
|
+
'standalone',
|
|
154
|
+
'readonlyPath',
|
|
102
155
|
'class',
|
|
103
156
|
]);
|
|
104
157
|
|
|
@@ -120,6 +173,39 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
120
173
|
const [activeFile, setActiveFile] = createSignal<string | undefined>(local.activeFile);
|
|
121
174
|
const [reloadKey, setReloadKey] = createSignal(0);
|
|
122
175
|
|
|
176
|
+
// Maximize view-state — controlled by `local.maximized`, toggled by the button.
|
|
177
|
+
const [maximized, setMaximized] = createSignal<boolean>(local.maximized ?? false);
|
|
178
|
+
createEffect(() => setMaximized(local.maximized ?? false));
|
|
179
|
+
const toggleMaximize = () => {
|
|
180
|
+
const next = !maximized();
|
|
181
|
+
setMaximized(next);
|
|
182
|
+
local.onMaximizeChange?.(next);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Open-in-tab — only enabled when there's a concrete url to open.
|
|
186
|
+
const canOpenInTab = createMemo(() => {
|
|
187
|
+
const u = currentUrl();
|
|
188
|
+
return !!u && u !== 'about:blank';
|
|
189
|
+
});
|
|
190
|
+
const openInNewTab = () => {
|
|
191
|
+
if (!canOpenInTab()) return;
|
|
192
|
+
window.open(currentUrl(), '_blank', 'noopener,noreferrer');
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// The expand button is suppressed in standalone (no enclosing resizable).
|
|
196
|
+
const showExpand = createMemo(() => local.expandable && !local.standalone);
|
|
197
|
+
// Omit the whole toolbar when nothing is shown.
|
|
198
|
+
const showAnyToolbar = createMemo(
|
|
199
|
+
() =>
|
|
200
|
+
local.showNav ||
|
|
201
|
+
local.showReload ||
|
|
202
|
+
local.showHome ||
|
|
203
|
+
local.showPathField ||
|
|
204
|
+
local.showTabs ||
|
|
205
|
+
showExpand() ||
|
|
206
|
+
local.openInTab,
|
|
207
|
+
);
|
|
208
|
+
|
|
123
209
|
let iframeEl: HTMLIFrameElement | undefined;
|
|
124
210
|
|
|
125
211
|
// Controlled syncing: when the consumer changes the props, follow them.
|
|
@@ -225,30 +311,50 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
225
311
|
const input = (e.currentTarget as HTMLFormElement).elements.namedItem(
|
|
226
312
|
'kc-artifact-path',
|
|
227
313
|
) as HTMLInputElement | null;
|
|
314
|
+
if (local.readonlyPath) {
|
|
315
|
+
// Submit is a no-op while read-only; keep the field reflecting currentUrl.
|
|
316
|
+
if (input) input.value = currentUrl();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
228
319
|
if (input && input.value) navigate(input.value);
|
|
229
320
|
};
|
|
230
321
|
|
|
231
322
|
return (
|
|
232
323
|
<div
|
|
233
324
|
class={cn(
|
|
234
|
-
'flex h-full w-full flex-col overflow-hidden
|
|
325
|
+
'flex h-full w-full flex-col overflow-hidden bg-card text-card-foreground',
|
|
326
|
+
local.standalone && 'rounded-xl border border-border',
|
|
235
327
|
local.class,
|
|
236
328
|
)}
|
|
237
329
|
{...rest}
|
|
238
330
|
>
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
331
|
+
<Show when={showAnyToolbar()}>
|
|
332
|
+
<ArtifactToolbar
|
|
333
|
+
url={currentUrl}
|
|
334
|
+
tab={tab}
|
|
335
|
+
canBack={canBack}
|
|
336
|
+
canForward={canForward}
|
|
337
|
+
canHome={() => !!local.src}
|
|
338
|
+
onBack={goBack}
|
|
339
|
+
onForward={goForward}
|
|
340
|
+
onReload={reload}
|
|
341
|
+
onHome={goHome}
|
|
342
|
+
onSubmitPath={submitPath}
|
|
343
|
+
onTab={selectTab}
|
|
344
|
+
showNav={() => local.showNav}
|
|
345
|
+
showReload={() => local.showReload}
|
|
346
|
+
showHome={() => local.showHome}
|
|
347
|
+
showPathField={() => local.showPathField}
|
|
348
|
+
showTabs={() => local.showTabs}
|
|
349
|
+
showExpand={showExpand}
|
|
350
|
+
showOpenInTab={() => local.openInTab}
|
|
351
|
+
maximized={maximized}
|
|
352
|
+
onToggleMaximize={toggleMaximize}
|
|
353
|
+
canOpenInTab={canOpenInTab}
|
|
354
|
+
onOpenInTab={openInNewTab}
|
|
355
|
+
readonlyPath={() => local.readonlyPath}
|
|
356
|
+
/>
|
|
357
|
+
</Show>
|
|
252
358
|
<div class="relative min-h-0 flex-1">
|
|
253
359
|
<Show
|
|
254
360
|
when={tab() === 'preview'}
|
|
@@ -296,77 +402,130 @@ interface ToolbarProps {
|
|
|
296
402
|
onHome: () => void;
|
|
297
403
|
onSubmitPath: (e: Event) => void;
|
|
298
404
|
onTab: (tab: ArtifactTab) => void;
|
|
405
|
+
showNav: () => boolean;
|
|
406
|
+
showReload: () => boolean;
|
|
407
|
+
showHome: () => boolean;
|
|
408
|
+
showPathField: () => boolean;
|
|
409
|
+
showTabs: () => boolean;
|
|
410
|
+
showExpand: () => boolean;
|
|
411
|
+
showOpenInTab: () => boolean;
|
|
412
|
+
maximized: () => boolean;
|
|
413
|
+
onToggleMaximize: () => void;
|
|
414
|
+
canOpenInTab: () => boolean;
|
|
415
|
+
onOpenInTab: () => void;
|
|
416
|
+
readonlyPath: () => boolean;
|
|
299
417
|
}
|
|
300
418
|
|
|
301
419
|
function ArtifactToolbar(props: ToolbarProps): JSX.Element {
|
|
302
420
|
return (
|
|
303
|
-
<div
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
aria-label="
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
class=
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
421
|
+
<div
|
|
422
|
+
data-artifact-toolbar
|
|
423
|
+
class="flex shrink-0 items-center gap-1.5 border-b border-border bg-muted/40 px-2 py-1.5"
|
|
424
|
+
>
|
|
425
|
+
<Show when={props.showNav()}>
|
|
426
|
+
<Button
|
|
427
|
+
variant="ghost"
|
|
428
|
+
size="icon-sm"
|
|
429
|
+
aria-label="Back"
|
|
430
|
+
disabled={!props.canBack()}
|
|
431
|
+
onClick={() => props.onBack()}
|
|
432
|
+
>
|
|
433
|
+
<ArrowLeft size={16} aria-hidden="true" />
|
|
434
|
+
</Button>
|
|
435
|
+
<Button
|
|
436
|
+
variant="ghost"
|
|
437
|
+
size="icon-sm"
|
|
438
|
+
aria-label="Forward"
|
|
439
|
+
disabled={!props.canForward()}
|
|
440
|
+
onClick={() => props.onForward()}
|
|
441
|
+
>
|
|
442
|
+
<ArrowRight size={16} aria-hidden="true" />
|
|
443
|
+
</Button>
|
|
444
|
+
</Show>
|
|
445
|
+
<Show when={props.showReload()}>
|
|
446
|
+
<Button variant="ghost" size="icon-sm" aria-label="Reload" onClick={() => props.onReload()}>
|
|
447
|
+
<RotateCw size={15} aria-hidden="true" />
|
|
448
|
+
</Button>
|
|
449
|
+
</Show>
|
|
450
|
+
<Show when={props.showHome()}>
|
|
451
|
+
<Button
|
|
452
|
+
variant="ghost"
|
|
453
|
+
size="icon-sm"
|
|
454
|
+
aria-label="Home"
|
|
455
|
+
disabled={!props.canHome()}
|
|
456
|
+
onClick={() => props.onHome()}
|
|
457
|
+
>
|
|
458
|
+
<House size={15} aria-hidden="true" />
|
|
459
|
+
</Button>
|
|
460
|
+
</Show>
|
|
461
|
+
<Show when={props.showPathField()}>
|
|
462
|
+
<form class="min-w-0 flex-1" onSubmit={(e) => props.onSubmitPath(e)}>
|
|
463
|
+
<label class="sr-only" for="kc-artifact-path">
|
|
464
|
+
Address
|
|
465
|
+
</label>
|
|
466
|
+
<input
|
|
467
|
+
id="kc-artifact-path"
|
|
468
|
+
name="kc-artifact-path"
|
|
469
|
+
type="text"
|
|
470
|
+
spellcheck={false}
|
|
471
|
+
autocomplete="off"
|
|
472
|
+
readonly={props.readonlyPath() || undefined}
|
|
473
|
+
aria-readonly={props.readonlyPath() ? 'true' : undefined}
|
|
474
|
+
value={props.url()}
|
|
475
|
+
class={cn(
|
|
476
|
+
'h-7 w-full rounded-md border border-border px-2.5 text-xs text-foreground font-mono outline-none',
|
|
477
|
+
props.readonlyPath()
|
|
478
|
+
? 'bg-muted/40 cursor-default'
|
|
479
|
+
: 'bg-background focus-visible:ring-2 focus-visible:ring-ring',
|
|
480
|
+
)}
|
|
481
|
+
placeholder="Enter a path or URL…"
|
|
482
|
+
/>
|
|
483
|
+
</form>
|
|
484
|
+
</Show>
|
|
485
|
+
<Show when={props.showExpand()}>
|
|
486
|
+
<Button
|
|
487
|
+
variant="ghost"
|
|
488
|
+
size="icon-sm"
|
|
489
|
+
aria-label={props.maximized() ? 'Collapse' : 'Expand'}
|
|
490
|
+
aria-expanded={props.maximized()}
|
|
491
|
+
onClick={() => props.onToggleMaximize()}
|
|
492
|
+
>
|
|
493
|
+
<Show when={props.maximized()} fallback={<Maximize2 size={15} aria-hidden="true" />}>
|
|
494
|
+
<Minimize2 size={15} aria-hidden="true" />
|
|
495
|
+
</Show>
|
|
496
|
+
</Button>
|
|
497
|
+
</Show>
|
|
498
|
+
<Show when={props.showOpenInTab()}>
|
|
499
|
+
<Button
|
|
500
|
+
variant="ghost"
|
|
501
|
+
size="icon-sm"
|
|
502
|
+
aria-label="Open in new tab"
|
|
503
|
+
disabled={!props.canOpenInTab()}
|
|
504
|
+
onClick={() => props.onOpenInTab()}
|
|
505
|
+
>
|
|
506
|
+
<ExternalLink size={15} aria-hidden="true" />
|
|
507
|
+
</Button>
|
|
508
|
+
</Show>
|
|
509
|
+
<Show when={props.showTabs()}>
|
|
510
|
+
<div
|
|
511
|
+
role="tablist"
|
|
512
|
+
aria-label="View"
|
|
513
|
+
class="flex shrink-0 items-center gap-0.5 rounded-md bg-muted p-0.5"
|
|
514
|
+
>
|
|
515
|
+
<SegmentButton
|
|
516
|
+
label="Preview"
|
|
517
|
+
icon={<Eye size={14} aria-hidden="true" />}
|
|
518
|
+
selected={props.tab() === 'preview'}
|
|
519
|
+
onClick={() => props.onTab('preview')}
|
|
520
|
+
/>
|
|
521
|
+
<SegmentButton
|
|
522
|
+
label="Code"
|
|
523
|
+
icon={<CodeIcon size={14} aria-hidden="true" />}
|
|
524
|
+
selected={props.tab() === 'code'}
|
|
525
|
+
onClick={() => props.onTab('code')}
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
</Show>
|
|
370
529
|
</div>
|
|
371
530
|
);
|
|
372
531
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/components/card-fallback.tsx
|
|
2
|
+
// Rendered by the dispatcher when an envelope's `type` has no registered card.
|
|
3
|
+
// Visual: reuses the Card chrome so it sits naturally in a card stream.
|
|
4
|
+
import type { JSX } from 'solid-js';
|
|
5
|
+
import { Card } from './card';
|
|
6
|
+
import { AlertTriangle } from 'lucide-solid';
|
|
7
|
+
|
|
8
|
+
export interface CardFallbackProps {
|
|
9
|
+
/** The unrecognized envelope type, shown to aid debugging. */
|
|
10
|
+
type: string;
|
|
11
|
+
/** The envelope id (for parity with cards; not displayed). */
|
|
12
|
+
cardId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Inert, themed fallback for an unsupported card type. Emits nothing itself —
|
|
16
|
+
* the dispatcher emits the contract `error` event alongside rendering this. */
|
|
17
|
+
export function CardFallback(props: CardFallbackProps): JSX.Element {
|
|
18
|
+
return (
|
|
19
|
+
<Card>
|
|
20
|
+
<div role="alert" class="flex items-center gap-2 p-4 text-sm text-muted-foreground">
|
|
21
|
+
<AlertTriangle class="size-4 shrink-0 text-destructive dark:text-red-400" aria-hidden="true" />
|
|
22
|
+
<span>
|
|
23
|
+
Unsupported card type: <code class="font-mono">{props.type}</code>
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
</Card>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/components/card-renderer.tsx
|
|
2
|
+
// The Solid single-envelope dispatcher: pick the card for envelope.type and render it
|
|
3
|
+
// with the envelope spread onto its props. Routing uses the ambient CardProvider
|
|
4
|
+
// (useCardHost). Unknown type → CardFallback + a one-shot contract `error` emit.
|
|
5
|
+
import { createMemo, untrack, Show, type JSX } from 'solid-js';
|
|
6
|
+
import { Dynamic } from 'solid-js/web';
|
|
7
|
+
import type { CardEnvelope } from '../primitives/card-contract';
|
|
8
|
+
import { useCardHost } from '../primitives/card-host';
|
|
9
|
+
import { mergeCardComponents, type CardComponentMap } from '../primitives/card-registry';
|
|
10
|
+
import { CardFallback } from './card-fallback';
|
|
11
|
+
|
|
12
|
+
export interface CardRendererProps {
|
|
13
|
+
envelope: CardEnvelope;
|
|
14
|
+
/** Add/override type→component entries (merged over the built-ins). */
|
|
15
|
+
types?: CardComponentMap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function CardRenderer(props: CardRendererProps): JSX.Element {
|
|
19
|
+
const host = useCardHost();
|
|
20
|
+
const map = createMemo(() => mergeCardComponents(props.types));
|
|
21
|
+
const entry = createMemo(() => map()[props.envelope.type]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Show
|
|
25
|
+
when={entry()}
|
|
26
|
+
fallback={<UnknownCard envelope={props.envelope} />}
|
|
27
|
+
>
|
|
28
|
+
{(comp) => <Dynamic component={comp()} envelope={props.envelope} host={host} />}
|
|
29
|
+
</Show>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Renders the fallback and emits exactly one `error` (untracked, on first render). */
|
|
34
|
+
function UnknownCard(props: { envelope: CardEnvelope }): JSX.Element {
|
|
35
|
+
const host = useCardHost();
|
|
36
|
+
untrack(() =>
|
|
37
|
+
host?.emit({
|
|
38
|
+
kind: 'error',
|
|
39
|
+
cardId: props.envelope.id,
|
|
40
|
+
message: `Unsupported card type: ${props.envelope.type}`,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return <CardFallback type={props.envelope.type} cardId={props.envelope.id} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Function sugar: renderCard(env) ≡ <CardRenderer envelope={env} />. */
|
|
47
|
+
export function renderCard(
|
|
48
|
+
envelope: CardEnvelope,
|
|
49
|
+
opts?: { types?: CardComponentMap },
|
|
50
|
+
): JSX.Element {
|
|
51
|
+
return <CardRenderer envelope={envelope} types={opts?.types} />;
|
|
52
|
+
}
|