@kitnai/chat 0.8.0 → 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.
@@ -53,6 +53,96 @@
53
53
  "scalar": true,
54
54
  "description": "Accessible title for the preview iframe.",
55
55
  "displayType": "undefined | string"
56
+ },
57
+ {
58
+ "name": "maximized",
59
+ "type": "undefined | false | true",
60
+ "optional": true,
61
+ "scalar": true,
62
+ "description": "Reflects the artifact's own maximized view-state (usually driven by the protocol).",
63
+ "default": "false",
64
+ "displayType": "undefined | false | true"
65
+ },
66
+ {
67
+ "name": "expandable",
68
+ "type": "undefined | false | true",
69
+ "optional": true,
70
+ "scalar": true,
71
+ "description": "Show the expand-to-fill button (OPT-IN).",
72
+ "default": "false",
73
+ "displayType": "undefined | false | true"
74
+ },
75
+ {
76
+ "name": "openInTab",
77
+ "type": "undefined | false | true",
78
+ "optional": true,
79
+ "scalar": true,
80
+ "description": "Show the open-in-new-tab button (OPT-IN).",
81
+ "default": "false",
82
+ "displayType": "undefined | false | true"
83
+ },
84
+ {
85
+ "name": "noNav",
86
+ "type": "undefined | false | true",
87
+ "optional": true,
88
+ "scalar": true,
89
+ "description": "Hide back/forward.",
90
+ "default": "false",
91
+ "displayType": "undefined | false | true"
92
+ },
93
+ {
94
+ "name": "noReload",
95
+ "type": "undefined | false | true",
96
+ "optional": true,
97
+ "scalar": true,
98
+ "description": "Hide reload.",
99
+ "default": "false",
100
+ "displayType": "undefined | false | true"
101
+ },
102
+ {
103
+ "name": "noHome",
104
+ "type": "undefined | false | true",
105
+ "optional": true,
106
+ "scalar": true,
107
+ "description": "Hide home.",
108
+ "default": "false",
109
+ "displayType": "undefined | false | true"
110
+ },
111
+ {
112
+ "name": "noPathField",
113
+ "type": "undefined | false | true",
114
+ "optional": true,
115
+ "scalar": true,
116
+ "description": "Hide the address field.",
117
+ "default": "false",
118
+ "displayType": "undefined | false | true"
119
+ },
120
+ {
121
+ "name": "noTabs",
122
+ "type": "undefined | false | true",
123
+ "optional": true,
124
+ "scalar": true,
125
+ "description": "Hide the Preview|Code toggle.",
126
+ "default": "false",
127
+ "displayType": "undefined | false | true"
128
+ },
129
+ {
130
+ "name": "standalone",
131
+ "type": "undefined | false | true",
132
+ "optional": true,
133
+ "scalar": true,
134
+ "description": "Standalone chrome: rounded corners + border (else square, borderless in-panel).",
135
+ "default": "false",
136
+ "displayType": "undefined | false | true"
137
+ },
138
+ {
139
+ "name": "readonlyPath",
140
+ "type": "undefined | false | true",
141
+ "optional": true,
142
+ "scalar": true,
143
+ "description": "Show the address but make it read-only (visible, nav-tracking, non-editable).",
144
+ "default": "false",
145
+ "displayType": "undefined | false | true"
56
146
  }
57
147
  ],
58
148
  "events": [
@@ -62,6 +152,12 @@
62
152
  "description": "Fired when a file is selected. `detail.path`.",
63
153
  "displayDetail": "{ path: string }"
64
154
  },
155
+ {
156
+ "name": "maximizechange",
157
+ "detail": "{ maximized: false | true }",
158
+ "description": "Artifact's own maximize button toggled (consumer-observable; non-bubbling).",
159
+ "displayDetail": "{ maximized: false | true }"
160
+ },
65
161
  {
66
162
  "name": "navigate",
67
163
  "detail": "{ url: string }",
@@ -247,6 +343,45 @@
247
343
  ],
248
344
  "tokens": []
249
345
  },
346
+ {
347
+ "tag": "kc-cards",
348
+ "className": "KcCardsElement",
349
+ "props": [
350
+ {
351
+ "name": "cards",
352
+ "type": "undefined | { type: string; id: string; data: unknown; title?: undefined | string }[]",
353
+ "optional": true,
354
+ "scalar": false,
355
+ "description": "The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`.",
356
+ "displayType": "undefined | { type: string; id: string; data: unknown; title?: undefined | string }[]"
357
+ },
358
+ {
359
+ "name": "types",
360
+ "type": "undefined | Record<string, string>",
361
+ "optional": true,
362
+ "scalar": false,
363
+ "description": "Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type.",
364
+ "displayType": "undefined | Record<string, string>"
365
+ },
366
+ {
367
+ "name": "policy",
368
+ "type": "undefined | { onSubmitData?: undefined | (cardId: string, data: unknown) => void; onAction?: undefined | (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined | (text: string, opts: { mode: \"compose\" | \"send\"; context?: unknown; }) => void; onOpen?: undefined | (url: string, target: \"tab\" | \"artifact\") => void; onState?: undefined | (cardId: string, patch: unknown) => void; onDismiss?: undefined | (cardId: string) => void; onError?: undefined | (cardId: string, message: string) => void; maxSendPromptMode?: undefined | \"compose\" | \"send\" }",
369
+ "optional": true,
370
+ "scalar": false,
371
+ "description": "Optional CardPolicy handling child events. Property: `el.policy`.",
372
+ "displayType": "undefined | { onSubmitData?: undefined | (cardId: string, data: unknown) => void; onAction?: undefined | (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined | (text: string, opts: { mode: \"compose\" | \"send\"; context?: unknown; }) => void; onOpen?: undefined | (url: string, target: \"tab\" | \"artifact\") => void; onState?: undefined | (cardId: string, patch: unknown) => void; onDismiss?: undefined | (cardId: string) => void; onError?: undefined | (cardId: string, message: string) => void; maxSendPromptMode?: undefined | \"compose\" | \"send\" }"
373
+ }
374
+ ],
375
+ "events": [],
376
+ "composedFrom": [
377
+ {
378
+ "name": "CardFallback",
379
+ "group": "Components",
380
+ "storyId": "components-cardfallback--docs"
381
+ }
382
+ ],
383
+ "tokens": []
384
+ },
250
385
  {
251
386
  "tag": "kc-chain-of-thought",
252
387
  "className": "KcChainOfThoughtElement",
@@ -1693,6 +1828,15 @@
1693
1828
  "description": "Layout axis: `horizontal` (row, default) or `vertical` (column).",
1694
1829
  "default": "'horizontal'",
1695
1830
  "displayType": "undefined | \"horizontal\" | \"vertical\""
1831
+ },
1832
+ {
1833
+ "name": "maximizedIndex",
1834
+ "type": "undefined | null | number",
1835
+ "optional": true,
1836
+ "scalar": false,
1837
+ "description": "Which item index is maximized (null = none). Declarative source of truth.",
1838
+ "default": "null",
1839
+ "displayType": "undefined | null | number"
1696
1840
  }
1697
1841
  ],
1698
1842
  "events": [
@@ -1701,6 +1845,12 @@
1701
1845
  "detail": "{ sizes: number[] }",
1702
1846
  "description": "Fired on drag-end / keyboard resize / visibility change. `detail.sizes` = panel sizes in percent.",
1703
1847
  "displayDetail": "{ sizes: number[] }"
1848
+ },
1849
+ {
1850
+ "name": "maximizechange",
1851
+ "detail": "{ maximized: false | true; index: null | number }",
1852
+ "description": "Observe layout maximize state.",
1853
+ "displayDetail": "{ maximized: false | true; index: null | number }"
1704
1854
  }
1705
1855
  ],
1706
1856
  "composedFrom": [
@@ -1765,6 +1915,12 @@
1765
1915
  "detail": "unknown",
1766
1916
  "description": "",
1767
1917
  "displayDetail": "unknown"
1918
+ },
1919
+ {
1920
+ "name": "maximizechange",
1921
+ "detail": "unknown",
1922
+ "description": "",
1923
+ "displayDetail": "unknown"
1768
1924
  }
1769
1925
  ],
1770
1926
  "composedFrom": [
@@ -23,6 +23,26 @@ export interface KcArtifactElement extends HTMLElement {
23
23
  sandbox?: string;
24
24
  /** Accessible title for the preview iframe. */
25
25
  iframeTitle?: string;
26
+ /** Reflects the artifact's own maximized view-state (usually driven by the protocol). */
27
+ maximized?: boolean;
28
+ /** Show the expand-to-fill button (OPT-IN). */
29
+ expandable?: boolean;
30
+ /** Show the open-in-new-tab button (OPT-IN). */
31
+ openInTab?: boolean;
32
+ /** Hide back/forward. */
33
+ noNav?: boolean;
34
+ /** Hide reload. */
35
+ noReload?: boolean;
36
+ /** Hide home. */
37
+ noHome?: boolean;
38
+ /** Hide the address field. */
39
+ noPathField?: boolean;
40
+ /** Hide the Preview|Code toggle. */
41
+ noTabs?: boolean;
42
+ /** Standalone chrome: rounded corners + border (else square, borderless in-panel). */
43
+ standalone?: boolean;
44
+ /** Show the address but make it read-only (visible, nav-tracking, non-editable). */
45
+ readonlyPath?: boolean;
26
46
  }
27
47
 
28
48
  export interface KcAttachmentsElement extends HTMLElement {
@@ -55,6 +75,17 @@ export interface KcCardElement extends HTMLElement {
55
75
  dense?: boolean;
56
76
  }
57
77
 
78
+ export interface KcCardsElement extends HTMLElement {
79
+ /** Color mode (`auto` follows prefers-color-scheme). */
80
+ theme?: 'light' | 'dark' | 'auto';
81
+ /** The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. */
82
+ cards?: { type: string; id: string; data: unknown; title?: string }[];
83
+ /** Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. */
84
+ types?: Record<string, string>;
85
+ /** Optional CardPolicy handling child events. Property: `el.policy`. */
86
+ policy?: { onSubmitData?: (cardId: string, data: unknown) => void; onAction?: (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: (text: string, opts: { mode: "compose" | "send"; context?: unknown; }) => void; onOpen?: (url: string, target: "tab" | "artifact") => void; onState?: (cardId: string, patch: unknown) => void; onDismiss?: (cardId: string) => void; onError?: (cardId: string, message: string) => void; maxSendPromptMode?: "compose" | "send" };
87
+ }
88
+
58
89
  export interface KcChainOfThoughtElement extends HTMLElement {
59
90
  /** Color mode (`auto` follows prefers-color-scheme). */
60
91
  theme?: 'light' | 'dark' | 'auto';
@@ -347,6 +378,8 @@ export interface KcResizableElement extends HTMLElement {
347
378
  theme?: 'light' | 'dark' | 'auto';
348
379
  /** Layout axis: `horizontal` (row, default) or `vertical` (column). */
349
380
  orientation?: "horizontal" | "vertical";
381
+ /** Which item index is maximized (null = none). Declarative source of truth. */
382
+ maximizedIndex?: null | number;
350
383
  }
351
384
 
352
385
  export interface KcResizableItemElement extends HTMLElement {
@@ -531,6 +564,7 @@ declare global {
531
564
  'kc-artifact': KcArtifactElement;
532
565
  'kc-attachments': KcAttachmentsElement;
533
566
  'kc-card': KcCardElement;
567
+ 'kc-cards': KcCardsElement;
534
568
  'kc-chain-of-thought': KcChainOfThoughtElement;
535
569
  'kc-chat': KcChatElement;
536
570
  'kc-checkpoint': KcCheckpointElement;
@@ -45,6 +45,7 @@ import './link-card';
45
45
  import './embed';
46
46
  import './confirm-card';
47
47
  import './task-list-card';
48
+ import './cards';
48
49
 
49
50
  export type { ChatMessage, ChatMessageAction } from './chat-types';
50
51
  export { configureCodeHighlighting, isCodeHighlightingEnabled } from '../primitives/highlighter';
@@ -0,0 +1,27 @@
1
+ // src/elements/resizable.d.ts
2
+ // Hand-authored ambient types for <kc-resizable>'s maximize capability. The
3
+ // generated element-types.d.ts emits prop/event interfaces only (not methods),
4
+ // so the imperative host API + the cross-element protocol events live here.
5
+ // NOT regenerated by `npm run build`.
6
+ import type { KcMaximizeIntentDetail, KcMaximizeStateDetail } from './resizable';
7
+
8
+ declare global {
9
+ interface KcResizableElement extends HTMLElement {
10
+ /** Which item index is maximized (null = none). Setting it maximizes that
11
+ * item (or restores when set to null) — the declarative source of truth. */
12
+ maximizedIndex: number | null;
13
+ /** Imperatively maximize the item at `index` (thin wrapper over maximizedIndex). */
14
+ maximize(index: number): void;
15
+ /** Imperatively restore from the maximized layout. */
16
+ restore(): void;
17
+ }
18
+ interface HTMLElementTagNameMap {
19
+ 'kc-resizable': KcResizableElement;
20
+ }
21
+ interface HTMLElementEventMap {
22
+ 'kc-maximize-intent': CustomEvent<KcMaximizeIntentDetail>;
23
+ 'kc-maximize-state': CustomEvent<KcMaximizeStateDetail>;
24
+ }
25
+ }
26
+
27
+ export {};
@@ -1,7 +1,10 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
- import { createSignal, type JSX } from 'solid-js';
2
+ import { createSignal, onMount, type JSX } from 'solid-js';
3
3
  import './register'; // side effect: registers the custom elements
4
4
  import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
+ import { Resizable, ResizablePanel } from '../ui/resizable';
6
+ import type { ArtifactFile } from '../components/artifact';
7
+ import { Artifact } from '../components/artifact';
5
8
 
6
9
  // The web components are custom DOM elements, so declare the tags for JSX.
7
10
  declare module 'solid-js' {
@@ -12,10 +15,23 @@ declare module 'solid-js' {
12
15
  'kc-resizable-item': JSX.HTMLAttributes<HTMLElement> & {
13
16
  size?: string; min?: string; max?: string; locked?: boolean | string; hidden?: boolean | string;
14
17
  };
18
+ // `kc-artifact` JSX type is augmented (with the full attr set) in
19
+ // artifact.stories.tsx — declaring it again here with a different shape
20
+ // would trip TS2717 (module-augmentation merges must match). Reuse that one.
15
21
  }
16
22
  }
17
23
  }
18
24
 
25
+ // Fixture base URL (served by Storybook staticDirs from examples/artifact-fixtures).
26
+ const BASE = '/artifact-fixtures';
27
+
28
+ const ARTIFACT_FILES: ArtifactFile[] = [
29
+ { path: 'index.html', url: `${BASE}/index.html`, type: 'html', language: 'html',
30
+ code: `<!DOCTYPE html><html lang="en"><head><title>Preview</title></head><body><h1>Starboard</h1></body></html>` },
31
+ { path: 'about.html', url: `${BASE}/about.html`, type: 'html', language: 'html',
32
+ code: `<!DOCTYPE html><html lang="en"><head><title>About</title></head><body><h1>About</h1></body></html>` },
33
+ ];
34
+
19
35
  /** A labelled placeholder pane so the layout is visible in stories. */
20
36
  function Pane(props: { label: string; tone?: 'muted' | 'plain' }) {
21
37
  return (
@@ -198,3 +214,212 @@ export const HiddenToggle: Story = {
198
214
  );
199
215
  },
200
216
  };
217
+
218
+ const EXPAND_TO_FILL_SNIPPET = `<!-- The artifact's "Expand" button (opt-in: expandable) fires a bubbling
219
+ kc-maximize-intent event. The nearest enclosing <kc-resizable> catches it
220
+ automatically and hides siblings so the preview panel fills the container.
221
+ No wiring needed between the two elements — the protocol is zero-config. -->
222
+
223
+ <kc-resizable orientation="horizontal" style="display:block;height:480px">
224
+ <kc-resizable-item size="20%" min="120px"> …list… </kc-resizable-item>
225
+ <kc-resizable-item> …chat… </kc-resizable-item>
226
+ <kc-resizable-item size="35%" min="200px">
227
+ <!-- expandable opt-in: adds the Expand/Collapse button -->
228
+ <kc-artifact
229
+ src="…/index.html"
230
+ expandable
231
+ ></kc-artifact>
232
+ </kc-resizable-item>
233
+ </kc-resizable>
234
+
235
+ <script type="module">
236
+ import '@kitnai/chat/elements';
237
+
238
+ // Optional: observe the layout events.
239
+ document.querySelector('kc-resizable')
240
+ .addEventListener('change', (e) => console.log('change', e.detail.sizes));
241
+ document.querySelector('kc-resizable')
242
+ .addEventListener('maximizechange', (e) => console.log('maximizechange', e.detail));
243
+ document.querySelector('kc-artifact')
244
+ .addEventListener('maximizechange', (e) => console.log('artifact maximizechange', e.detail));
245
+ </script>
246
+
247
+ <!-- Cross-element protocol (hand-authored; not generated):
248
+ • kc-maximize-intent — CustomEvent, bubbles:true, composed:true
249
+ Fired by <kc-artifact> when the Expand/Collapse button is clicked.
250
+ detail: { requested: boolean } (true = maximize, false = restore)
251
+ • kc-maximize-state — CustomEvent, bubbles:false, composed:true
252
+ Dispatched by <kc-resizable> back DOWN onto the maximized <kc-resizable-item>
253
+ (on maximize) or the group host + the formerly-maximized item (on restore)
254
+ so the artifact can reconcile its button label.
255
+ detail: { maximized: boolean }
256
+ These protocol events are NOT listed in the generated web-components.md because
257
+ they are cross-element — the generator only documents per-element dispatch events. -->`;
258
+
259
+ /**
260
+ * **Expand to fill** — the headline integration: the artifact's expand button
261
+ * fills the preview panel to the full container width. No wiring between the
262
+ * two elements is needed — clicking **Expand** fires a `kc-maximize-intent` event
263
+ * that bubbles up to the nearest enclosing `<kc-resizable>`, which hides siblings
264
+ * and lets the preview panel fill. **Collapse** (or **Escape**) restores the
265
+ * original layout.
266
+ *
267
+ * **Cross-element protocol (hand-authored docs):**
268
+ * - `kc-maximize-intent` — `bubbles:true, composed:true`; fired by `<kc-artifact>`
269
+ * when Expand/Collapse is toggled. `detail: { requested: boolean }`.
270
+ * - `kc-maximize-state` — `bubbles:false, composed:true`; dispatched by
271
+ * `<kc-resizable>` back down to the affected `<kc-resizable-item>` so the
272
+ * artifact can reconcile its button. `detail: { maximized: boolean }`.
273
+ *
274
+ * These protocol events are NOT in the generated `web-components.md`
275
+ * (the generator only documents per-element `dispatch` events — resolved decision #1).
276
+ */
277
+ export const ExpandToFill: Story = {
278
+ name: 'Expand to fill',
279
+ render: () => {
280
+ const [log, setLog] = createSignal<string[]>([]);
281
+ let artifactEl: HTMLElement & { files?: ArtifactFile[] };
282
+ let resizableEl: HTMLElement;
283
+ onMount(() => {
284
+ if (artifactEl) artifactEl.files = ARTIFACT_FILES;
285
+ if (resizableEl) {
286
+ resizableEl.addEventListener('change', (e: Event) =>
287
+ setLog((l) => [`change → ${JSON.stringify((e as CustomEvent).detail.sizes)}`, ...l].slice(0, 6)),
288
+ );
289
+ resizableEl.addEventListener('maximizechange', (e: Event) =>
290
+ setLog((l) => [`maximizechange → ${JSON.stringify((e as CustomEvent).detail)}`, ...l].slice(0, 6)),
291
+ );
292
+ }
293
+ if (artifactEl) {
294
+ artifactEl.addEventListener('maximizechange', (e: Event) =>
295
+ setLog((l) => [`artifact maximizechange → ${JSON.stringify((e as CustomEvent).detail)}`, ...l].slice(0, 6)),
296
+ );
297
+ }
298
+ });
299
+ return (
300
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
301
+ <div
302
+ style={{
303
+ height: '480px',
304
+ width: '100%',
305
+ 'max-width': '900px',
306
+ border: '1px solid var(--color-border, #e4e4e7)',
307
+ 'border-radius': '8px',
308
+ overflow: 'hidden',
309
+ }}
310
+ >
311
+ <kc-resizable
312
+ ref={(e) => (resizableEl = e as HTMLElement)}
313
+ orientation="horizontal"
314
+ >
315
+ <kc-resizable-item size="20%" min="120px">
316
+ <Pane label="List" />
317
+ </kc-resizable-item>
318
+ <kc-resizable-item min="140px">
319
+ <Pane label="Chat" tone="plain" />
320
+ </kc-resizable-item>
321
+ <kc-resizable-item size="35%" min="200px">
322
+ <kc-artifact
323
+ ref={(e) => (artifactEl = e as HTMLElement & { files?: ArtifactFile[] })}
324
+ src={`${BASE}/index.html`}
325
+ iframe-title="Starboard artifact preview"
326
+ expandable
327
+ />
328
+ </kc-resizable-item>
329
+ </kc-resizable>
330
+ </div>
331
+ <pre
332
+ style={{
333
+ 'font-size': '12px',
334
+ 'max-width': '900px',
335
+ padding: '10px 12px',
336
+ 'border-radius': '8px',
337
+ background: 'var(--color-muted, #f4f4f5)',
338
+ color: 'var(--color-muted-foreground, #71717a)',
339
+ 'min-height': '72px',
340
+ margin: '0',
341
+ }}
342
+ >
343
+ {log().length ? log().join('\n') : '(click the ⤢ Expand button in the preview toolbar…)'}
344
+ </pre>
345
+ </div>
346
+ );
347
+ },
348
+ parameters: { docs: { source: { code: EXPAND_TO_FILL_SNIPPET, language: 'html' } } },
349
+ };
350
+
351
+ const SOLID_PARITY_SNIPPET = `// SolidJS — Artifact inside Resizable with maximizedIndex/onMaximizeChange.
352
+ // No web components needed; works in a pure-Solid app.
353
+ import { createSignal } from 'solid-js';
354
+ import { Artifact } from '@kitnai/chat/components';
355
+ import { Resizable, ResizablePanel } from '@kitnai/chat/ui';
356
+
357
+ function App() {
358
+ const [maximizedIndex, setMaximizedIndex] = createSignal<number | null>(null);
359
+ return (
360
+ <Resizable
361
+ maximizedIndex={maximizedIndex()}
362
+ onMaximizeChange={setMaximizedIndex}
363
+ style="height:480px"
364
+ >
365
+ <ResizablePanel defaultSize="20%" minSize="120px">…list…</ResizablePanel>
366
+ <ResizablePanel minSize="140px">…chat…</ResizablePanel>
367
+ <ResizablePanel defaultSize="35%" minSize="200px">
368
+ <Artifact
369
+ src="…"
370
+ expandable
371
+ maximized={maximizedIndex() === 2}
372
+ onMaximizeChange={(m) => setMaximizedIndex(m ? 2 : null)}
373
+ />
374
+ </ResizablePanel>
375
+ </Resizable>
376
+ );
377
+ }`;
378
+
379
+ /**
380
+ * **SolidJS parity** — the same `list | chat | artifact` layout using the Solid
381
+ * `Resizable` convenience and the `Artifact` component directly (no web components).
382
+ * `maximizedIndex` / `onMaximizeChange` on `Resizable` mirror the web-component
383
+ * protocol at the Solid level. Wire `Artifact`'s `onMaximizeChange` to set the
384
+ * index and pass `maximized` back down to drive the button.
385
+ */
386
+ export const SolidParity: Story = {
387
+ name: 'SolidJS parity (Resizable + Artifact)',
388
+ render: () => {
389
+ const [maximizedIndex, setMaximizedIndex] = createSignal<number | null>(null);
390
+ return (
391
+ <div
392
+ style={{
393
+ height: '480px',
394
+ width: '100%',
395
+ 'max-width': '900px',
396
+ border: '1px solid var(--color-border, #e4e4e7)',
397
+ 'border-radius': '8px',
398
+ overflow: 'hidden',
399
+ }}
400
+ >
401
+ <Resizable
402
+ maximizedIndex={maximizedIndex()}
403
+ onMaximizeChange={setMaximizedIndex}
404
+ >
405
+ <ResizablePanel defaultSize="20%" minSize="120px">
406
+ <Pane label="List" />
407
+ </ResizablePanel>
408
+ <ResizablePanel minSize="140px">
409
+ <Pane label="Chat" tone="plain" />
410
+ </ResizablePanel>
411
+ <ResizablePanel defaultSize="35%" minSize="200px">
412
+ <Artifact
413
+ src={`${BASE}/index.html`}
414
+ iframeTitle="Starboard artifact preview"
415
+ expandable
416
+ maximized={maximizedIndex() === 2}
417
+ onMaximizeChange={(m) => setMaximizedIndex(m ? 2 : null)}
418
+ />
419
+ </ResizablePanel>
420
+ </Resizable>
421
+ </div>
422
+ );
423
+ },
424
+ parameters: { docs: { source: { code: SOLID_PARITY_SNIPPET, language: 'tsx' } } },
425
+ };