@kitnai/chat 0.7.0 → 0.8.1

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.
Files changed (214) hide show
  1. package/README.md +9 -9
  2. package/dist/custom-elements.json +1626 -883
  3. package/dist/kitn-chat.es.js +36 -36
  4. package/dist/llms/llms-full.txt +303 -142
  5. package/dist/llms/llms.txt +18 -18
  6. package/dist/schemas/card-envelope.schema.json +14 -0
  7. package/dist/schemas/card-event.schema.json +12 -0
  8. package/dist/schemas/confirm.schema.json +65 -0
  9. package/dist/schemas/embed.schema.json +65 -0
  10. package/dist/schemas/form.result.schema.json +7 -0
  11. package/dist/schemas/form.schema.json +33 -0
  12. package/dist/schemas/link.schema.json +56 -0
  13. package/dist/schemas/task-list.result.schema.json +16 -0
  14. package/dist/schemas/task-list.schema.json +78 -0
  15. package/dist/theme.tokens.css +65 -65
  16. package/dist/tsx-B8rCNbgL.js +1 -0
  17. package/dist/typescript-RycA9KXf.js +1 -0
  18. package/frameworks/react/index.tsx +356 -189
  19. package/frameworks/react/runtime.tsx +2 -2
  20. package/llms-full.txt +303 -142
  21. package/llms.txt +18 -18
  22. package/package.json +5 -2
  23. package/src/components/artifact.stories.tsx +138 -0
  24. package/src/components/artifact.tsx +581 -0
  25. package/src/components/attachments.stories.tsx +7 -8
  26. package/src/components/attachments.tsx +2 -2
  27. package/src/components/card.tsx +110 -0
  28. package/src/components/chain-of-thought.stories.tsx +7 -8
  29. package/src/components/chat-container.stories.tsx +7 -8
  30. package/src/components/chat-container.tsx +4 -0
  31. package/src/components/checkpoint.stories.tsx +7 -8
  32. package/src/components/checkpoint.tsx +3 -0
  33. package/src/components/code-block.stories.tsx +8 -9
  34. package/src/components/code-block.tsx +5 -2
  35. package/src/components/component-meta.json +3419 -0
  36. package/src/components/confirm-card.stories.tsx +74 -0
  37. package/src/components/confirm-card.tsx +299 -0
  38. package/src/components/context.stories.tsx +7 -8
  39. package/src/components/conversation-item.stories.tsx +7 -8
  40. package/src/components/conversation-item.tsx +2 -2
  41. package/src/components/conversation-list.stories.tsx +7 -8
  42. package/src/components/conversation-list.tsx +1 -1
  43. package/src/components/embed.tsx +196 -0
  44. package/src/components/empty.stories.tsx +8 -9
  45. package/src/components/feedback-bar.stories.tsx +7 -8
  46. package/src/components/file-tree.stories.tsx +73 -0
  47. package/src/components/file-tree.tsx +383 -0
  48. package/src/components/file-upload.stories.tsx +7 -8
  49. package/src/components/form-widgets.tsx +461 -0
  50. package/src/components/form.tsx +796 -0
  51. package/src/components/image.stories.tsx +7 -8
  52. package/src/components/link-card.tsx +194 -0
  53. package/src/components/loader.stories.tsx +7 -8
  54. package/src/components/markdown.stories.tsx +7 -8
  55. package/src/components/message-narrow.stories.tsx +12 -13
  56. package/src/components/message-skills.stories.tsx +16 -17
  57. package/src/components/message.stories.tsx +17 -18
  58. package/src/components/model-switcher.stories.tsx +7 -8
  59. package/src/components/prompt-input.stories.tsx +8 -9
  60. package/src/components/prompt-suggestion.stories.tsx +7 -8
  61. package/src/components/prompt-suggestion.tsx +3 -3
  62. package/src/components/reasoning.stories.tsx +7 -8
  63. package/src/components/scroll-button.stories.tsx +7 -8
  64. package/src/components/slash-command.stories.tsx +8 -9
  65. package/src/components/slash-command.tsx +2 -2
  66. package/src/components/source.stories.tsx +7 -8
  67. package/src/components/source.tsx +1 -1
  68. package/src/components/task-list-card.stories.tsx +78 -0
  69. package/src/components/task-list-card.tsx +388 -0
  70. package/src/components/text-shimmer.stories.tsx +7 -8
  71. package/src/components/thinking-bar.stories.tsx +7 -8
  72. package/src/components/tool.stories.tsx +7 -8
  73. package/src/components/tool.tsx +2 -2
  74. package/src/components/voice-input.stories.tsx +7 -8
  75. package/src/elements/artifact.stories.tsx +291 -0
  76. package/src/elements/artifact.tsx +72 -0
  77. package/src/elements/{kitn-attachments.stories.tsx → attachments.stories.tsx} +11 -20
  78. package/src/elements/attachments.tsx +4 -4
  79. package/src/elements/card.stories.tsx +118 -0
  80. package/src/elements/card.tsx +40 -0
  81. package/src/elements/catalog.stories.tsx +491 -0
  82. package/src/elements/{kitn-chain-of-thought.stories.tsx → chain-of-thought.stories.tsx} +13 -22
  83. package/src/elements/chain-of-thought.tsx +3 -3
  84. package/src/elements/{kitn-chat-scope-picker.stories.tsx → chat-scope-picker.stories.tsx} +10 -19
  85. package/src/elements/chat-scope-picker.tsx +4 -4
  86. package/src/elements/{kitn-chat-workspace.stories.tsx → chat-workspace.stories.tsx} +15 -23
  87. package/src/elements/chat-workspace.tsx +2 -2
  88. package/src/elements/{kitn-chat.stories.tsx → chat.stories.tsx} +12 -20
  89. package/src/elements/chat.tsx +2 -2
  90. package/src/elements/{kitn-checkpoint.stories.tsx → checkpoint.stories.tsx} +11 -20
  91. package/src/elements/checkpoint.tsx +8 -4
  92. package/src/elements/{kitn-code-block.stories.tsx → code-block.stories.tsx} +10 -19
  93. package/src/elements/code-block.tsx +3 -3
  94. package/src/elements/compiled.css +1 -1
  95. package/src/elements/composed-shell.stories.tsx +316 -0
  96. package/src/elements/confirm-card.stories.tsx +186 -0
  97. package/src/elements/confirm-card.tsx +45 -0
  98. package/src/elements/{kitn-context-meter.stories.tsx → context-meter.stories.tsx} +10 -19
  99. package/src/elements/context-meter.tsx +3 -3
  100. package/src/elements/{kitn-conversation-list.stories.tsx → conversation-list.stories.tsx} +12 -20
  101. package/src/elements/conversation-list.tsx +2 -2
  102. package/src/elements/css.ts +1 -1
  103. package/src/elements/define.tsx +10 -10
  104. package/src/elements/element-meta.json +1379 -733
  105. package/src/elements/element-types.d.ts +251 -125
  106. package/src/elements/embed.stories.tsx +197 -0
  107. package/src/elements/embed.tsx +35 -0
  108. package/src/elements/{kitn-empty.stories.tsx → empty.stories.tsx} +12 -21
  109. package/src/elements/empty.tsx +3 -3
  110. package/src/elements/{kitn-feedback-bar.stories.tsx → feedback-bar.stories.tsx} +11 -20
  111. package/src/elements/feedback-bar.tsx +4 -4
  112. package/src/elements/file-tree.stories.tsx +133 -0
  113. package/src/elements/file-tree.tsx +52 -0
  114. package/src/elements/{kitn-file-upload.stories.tsx → file-upload.stories.tsx} +12 -21
  115. package/src/elements/file-upload.tsx +4 -4
  116. package/src/elements/form.stories.tsx +204 -0
  117. package/src/elements/form.tsx +37 -0
  118. package/src/elements/{kitn-image.stories.tsx → image.stories.tsx} +10 -19
  119. package/src/elements/image.tsx +3 -3
  120. package/src/elements/link-card.stories.tsx +193 -0
  121. package/src/elements/link-card.tsx +34 -0
  122. package/src/elements/{kitn-loader.stories.tsx → loader.stories.tsx} +11 -20
  123. package/src/elements/loader.tsx +3 -3
  124. package/src/elements/{kitn-markdown.stories.tsx → markdown.stories.tsx} +10 -19
  125. package/src/elements/markdown.tsx +3 -3
  126. package/src/elements/{kitn-message-skills.stories.tsx → message-skills.stories.tsx} +10 -19
  127. package/src/elements/message-skills.tsx +3 -3
  128. package/src/elements/{kitn-message.stories.tsx → message.stories.tsx} +12 -21
  129. package/src/elements/message.tsx +5 -5
  130. package/src/elements/{kitn-model-switcher.stories.tsx → model-switcher.stories.tsx} +10 -19
  131. package/src/elements/model-switcher.tsx +5 -5
  132. package/src/elements/{kitn-prompt-input.stories.tsx → prompt-input.stories.tsx} +14 -22
  133. package/src/elements/prompt-input.tsx +3 -3
  134. package/src/elements/{kitn-prompt-suggestions.stories.tsx → prompt-suggestions.stories.tsx} +13 -22
  135. package/src/elements/prompt-suggestions.tsx +4 -4
  136. package/src/elements/{kitn-reasoning.stories.tsx → reasoning.stories.tsx} +10 -19
  137. package/src/elements/reasoning.tsx +4 -4
  138. package/src/elements/register.ts +11 -1
  139. package/src/elements/resizable.stories.tsx +200 -0
  140. package/src/elements/resizable.tsx +264 -0
  141. package/src/elements/{kitn-response-stream.stories.tsx → response-stream.stories.tsx} +10 -19
  142. package/src/elements/response-stream.tsx +4 -4
  143. package/src/elements/{kitn-source-list.stories.tsx → source-list.stories.tsx} +11 -20
  144. package/src/elements/{kitn-source.stories.tsx → source.stories.tsx} +12 -21
  145. package/src/elements/source.tsx +5 -5
  146. package/src/elements/styles.css +140 -1
  147. package/src/elements/task-list-card.stories.tsx +194 -0
  148. package/src/elements/task-list-card.tsx +40 -0
  149. package/src/elements/{kitn-text-shimmer.stories.tsx → text-shimmer.stories.tsx} +10 -19
  150. package/src/elements/text-shimmer.tsx +3 -3
  151. package/src/elements/{kitn-thinking-bar.stories.tsx → thinking-bar.stories.tsx} +11 -20
  152. package/src/elements/thinking-bar.tsx +5 -5
  153. package/src/elements/{kitn-tool.stories.tsx → tool.stories.tsx} +10 -19
  154. package/src/elements/tool.tsx +3 -3
  155. package/src/elements/{kitn-voice-input.stories.tsx → voice-input.stories.tsx} +10 -19
  156. package/src/elements/voice-input.tsx +4 -4
  157. package/src/index.ts +94 -2
  158. package/src/primitives/card-contract.ts +60 -0
  159. package/src/primitives/card-host.tsx +35 -0
  160. package/src/primitives/card-routing.ts +79 -0
  161. package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
  162. package/src/primitives/card-schemas/card-event.schema.json +12 -0
  163. package/src/primitives/card-schemas/confirm.schema.json +65 -0
  164. package/src/primitives/card-schemas/embed.schema.json +65 -0
  165. package/src/primitives/card-schemas/form.result.schema.json +7 -0
  166. package/src/primitives/card-schemas/form.schema.json +33 -0
  167. package/src/primitives/card-schemas/link.schema.json +56 -0
  168. package/src/primitives/card-schemas/task-list.result.schema.json +16 -0
  169. package/src/primitives/card-schemas/task-list.schema.json +78 -0
  170. package/src/primitives/card-validate.ts +95 -0
  171. package/src/primitives/embed-providers.ts +254 -0
  172. package/src/primitives/highlighter.ts +4 -0
  173. package/src/primitives/link-preview.ts +87 -0
  174. package/src/primitives/pdf-preview.ts +121 -0
  175. package/src/stories/chat-panel-layout.stories.tsx +2 -1
  176. package/src/stories/chat-scene.tsx +22 -21
  177. package/src/stories/checkpoint-restore.stories.tsx +10 -10
  178. package/src/stories/conversation-with-reasoning.stories.tsx +4 -4
  179. package/src/stories/conversation-with-sources.stories.tsx +7 -7
  180. package/src/stories/docs/Accessibility.mdx +2 -2
  181. package/src/stories/docs/ForAIAgents.mdx +3 -3
  182. package/src/stories/docs/GettingStarted.mdx +2 -2
  183. package/src/stories/docs/Installation.mdx +2 -2
  184. package/src/stories/docs/Integrations.mdx +29 -29
  185. package/src/stories/docs/Introduction.mdx +3 -3
  186. package/src/stories/docs/Theming.mdx +2 -2
  187. package/src/stories/docs/element-controls.ts +32 -0
  188. package/src/stories/docs/theme-editor/theme-editor.tsx +1 -0
  189. package/src/stories/examples/ChoosingComponents.mdx +94 -0
  190. package/src/stories/examples/sample-data.ts +79 -0
  191. package/src/stories/message-actions.stories.tsx +13 -13
  192. package/src/stories/pattern-centered-conversation.stories.tsx +3 -3
  193. package/src/stories/pattern-docked-widget.stories.tsx +1 -1
  194. package/src/stories/pattern-empty-state.stories.tsx +3 -3
  195. package/src/stories/prompt-input-variants.stories.tsx +13 -13
  196. package/src/stories/streaming-response.stories.tsx +3 -3
  197. package/src/stories/typography.stories.tsx +4 -4
  198. package/src/ui/avatar.stories.tsx +7 -8
  199. package/src/ui/badge.stories.tsx +7 -8
  200. package/src/ui/button.stories.tsx +8 -9
  201. package/src/ui/button.tsx +1 -0
  202. package/src/ui/collapsible.stories.tsx +6 -7
  203. package/src/ui/dropdown.stories.tsx +6 -7
  204. package/src/ui/hover-card.stories.tsx +6 -7
  205. package/src/ui/resizable.stories.tsx +74 -9
  206. package/src/ui/resizable.tsx +351 -71
  207. package/src/ui/scroll-area.stories.tsx +6 -7
  208. package/src/ui/scroll-area.tsx +3 -1
  209. package/src/ui/separator.stories.tsx +7 -8
  210. package/src/ui/skeleton.stories.tsx +7 -8
  211. package/src/ui/textarea.stories.tsx +6 -7
  212. package/src/ui/tooltip.stories.tsx +8 -9
  213. package/theme.css +65 -65
  214. package/src/stories/docs/element-spec.tsx +0 -86
@@ -1,5 +1,6 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { TextShimmer } from './text-shimmer';
3
+ import { componentDescription } from '../stories/docs/element-controls';
3
4
 
4
5
  const meta = {
5
6
  title: 'Components/TextShimmer',
@@ -9,14 +10,12 @@ const meta = {
9
10
  layout: 'padded',
10
11
  docs: {
11
12
  controls: { exclude: ['use:eventListener'] },
12
- description: {
13
- component: [
14
- 'An animated text effect that sweeps a light gradient across its content, rendered into any element tag via `as`.',
15
- '**When to use:** to signal an in-progress / loading state with a label e.g. "Thinking", "Generating", "Searching" or to draw subtle attention to a transient status line.',
16
- '**How to use:** wrap text in `<TextShimmer>`; tune `duration` (seconds per sweep) and `spread` (gradient width, clamped 5–45). Use `as` to change the wrapping tag (e.g. `"h2"`) and pass any standard HTML attributes.',
17
- '**Placement:** loading indicators, streaming status lines, thinking bars, and placeholder labels.',
18
- ].join('\n\n'),
19
- },
13
+ description: componentDescription([
14
+ 'An animated text effect that sweeps a light gradient across its content, rendered into any element tag via `as`.',
15
+ '**When to use:** to signal an in-progress / loading state with a label e.g. "Thinking", "Generating", "Searching" or to draw subtle attention to a transient status line.',
16
+ '**How to use:** wrap text in `<TextShimmer>`; tune `duration` (seconds per sweep) and `spread` (gradient width, clamped 5–45). Use `as` to change the wrapping tag (e.g. `"h2"`) and pass any standard HTML attributes.',
17
+ '**Placement:** loading indicators, streaming status lines, thinking bars, and placeholder labels.',
18
+ ]),
20
19
  },
21
20
  },
22
21
  argTypes: {
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { fn } from 'storybook/test';
3
3
  import { ThinkingBar } from './thinking-bar';
4
+ import { componentDescription } from '../stories/docs/element-controls';
4
5
 
5
6
  const meta = {
6
7
  title: 'Components/ThinkingBar',
@@ -10,14 +11,12 @@ const meta = {
10
11
  layout: 'padded',
11
12
  docs: {
12
13
  controls: { exclude: ['use:eventListener'] },
13
- description: {
14
- component: [
15
- 'A status row that shows a shimmering "thinking" label, optionally clickable to expand reasoning, with an optional stop/cancel action.',
16
- '**When to use:** while the assistant is generating or reasoning, to show live activity and give the user a way to interrupt or to open the reasoning trace.',
17
- '**How to use:** set `text` for the label. Pass `onClick` to make the label a button (adds a chevron) typically to toggle a reasoning panel. Pass `onStop` (with optional `stopLabel`) to render the interrupt action.',
18
- '**Placement:** above or in place of a streaming message, or at the top of a reasoning/chain-of-thought block.',
19
- ].join('\n\n'),
20
- },
14
+ description: componentDescription([
15
+ 'A status row that shows a shimmering "thinking" label, optionally clickable to expand reasoning, with an optional stop/cancel action.',
16
+ '**When to use:** while the assistant is generating or reasoning, to show live activity and give the user a way to interrupt or to open the reasoning trace.',
17
+ '**How to use:** set `text` for the label. Pass `onClick` to make the label a button (adds a chevron) — typically to toggle a reasoning panel. Pass `onStop` (with optional `stopLabel`) to render the interrupt action.',
18
+ '**Placement:** above or in place of a streaming message, or at the top of a reasoning/chain-of-thought block.',
19
+ ]),
21
20
  },
22
21
  },
23
22
  argTypes: {
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { Tool } from './tool';
3
3
  import type { ToolPart } from './tool';
4
+ import { componentDescription } from '../stories/docs/element-controls';
4
5
 
5
6
  const streamingPart: ToolPart = {
6
7
  type: 'search_documents',
@@ -40,14 +41,12 @@ const meta = {
40
41
  layout: 'padded',
41
42
  docs: {
42
43
  controls: { exclude: ['use:eventListener'] },
43
- description: {
44
- component: [
45
- 'A collapsible panel that visualizes a single tool call its name, state (processing / ready / completed / error), input, output, error, and call ID.',
46
- '**When to use:** to surface assistant tool/function calls in the conversation, so users can inspect what was run and what came back.',
47
- '**How to use:** pass a `toolPart` describing the call (`type`, `state`, optional `input`, `output`, `errorText`, `toolCallId`). State drives the icon and badge automatically. Set `defaultOpen` to start expanded.',
48
- '**Placement:** inline within an assistant message, typically between text segments where the tool was invoked.',
49
- ].join('\n\n'),
50
- },
44
+ description: componentDescription([
45
+ 'A collapsible panel that visualizes a single tool call — its name, state (processing / ready / completed / error), input, output, error, and call ID.',
46
+ '**When to use:** to surface assistant tool/function calls in the conversation, so users can inspect what was run and what came back.',
47
+ '**How to use:** pass a `toolPart` describing the call (`type`, `state`, optional `input`, `output`, `errorText`, `toolCallId`). State drives the icon and badge automatically. Set `defaultOpen` to start expanded.',
48
+ '**Placement:** inline within an assistant message, typically between text segments where the tool was invoked.',
49
+ ]),
51
50
  },
52
51
  },
53
52
  argTypes: {
@@ -150,7 +150,7 @@ function Tool(props: ToolProps) {
150
150
  <Show when={output()}>
151
151
  <div>
152
152
  <h4 class="text-muted-foreground mb-2 text-sm font-medium">Output</h4>
153
- <div class="max-h-60 overflow-auto rounded bg-muted/50 p-2 font-mono text-sm">
153
+ <div tabindex={0} class="max-h-60 overflow-auto rounded bg-muted/50 p-2 font-mono text-sm">
154
154
  <pre class="whitespace-pre-wrap">{formatValue(output())}</pre>
155
155
  </div>
156
156
  </div>
@@ -158,7 +158,7 @@ function Tool(props: ToolProps) {
158
158
 
159
159
  <Show when={state() === 'output-error' && local.toolPart.errorText}>
160
160
  <div>
161
- <h4 class="mb-2 text-sm font-medium text-red-500">Error</h4>
161
+ <h4 class="mb-2 text-sm font-medium text-red-600 dark:text-red-400">Error</h4>
162
162
  <div class="rounded bg-red-500/10 p-2 text-sm">
163
163
  {local.toolPart.errorText}
164
164
  </div>
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { fn } from 'storybook/test';
3
3
  import { VoiceInput } from './voice-input';
4
+ import { componentDescription } from '../stories/docs/element-controls';
4
5
 
5
6
  /** Sample transcription handler — resolves the recorded audio to text. */
6
7
  const transcribe = async (_audio: Blob): Promise<string> => {
@@ -16,14 +17,12 @@ const meta = {
16
17
  layout: 'padded',
17
18
  docs: {
18
19
  controls: { exclude: ['use:eventListener'] },
19
- description: {
20
- component: [
21
- 'A microphone button that records audio, shows recording (pulse rings) and processing (spinner) states, then hands the audio off for transcription.',
22
- '**When to use:** to let users dictate input by voice instead of typing — speech-to-text for the prompt field.',
23
- '**How to use:** provide `onTranscribe(audio)` returning a `Promise<string>` (your STT call) and `onTranscription(text)` to receive the result. Click toggles recording; transcription runs automatically on stop. Set `disabled` to block input.',
24
- '**Placement:** inside the prompt input action bar, next to send and other input actions.',
25
- ].join('\n\n'),
26
- },
20
+ description: componentDescription([
21
+ 'A microphone button that records audio, shows recording (pulse rings) and processing (spinner) states, then hands the audio off for transcription.',
22
+ '**When to use:** to let users dictate input by voice instead of typing speech-to-text for the prompt field.',
23
+ '**How to use:** provide `onTranscribe(audio)` returning a `Promise<string>` (your STT call) and `onTranscription(text)` to receive the result. Click toggles recording; transcription runs automatically on stop. Set `disabled` to block input.',
24
+ '**Placement:** inside the prompt input action bar, next to send and other input actions.',
25
+ ]),
27
26
  },
28
27
  },
29
28
  argTypes: {
@@ -0,0 +1,291 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal, onMount, type JSX } from 'solid-js';
3
+ import './register'; // side effect: registers the custom elements
4
+ import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
+ import type { ArtifactFile } from '../components/artifact';
6
+
7
+ // Custom DOM elements — declare the tags for JSX.
8
+ declare module 'solid-js' {
9
+ // eslint-disable-next-line @typescript-eslint/no-namespace
10
+ namespace JSX {
11
+ interface IntrinsicElements {
12
+ 'kc-artifact': JSX.HTMLAttributes<HTMLElement> & {
13
+ src?: string;
14
+ tab?: string;
15
+ 'active-file'?: string;
16
+ sandbox?: string;
17
+ 'iframe-title'?: string;
18
+ ref?: (el: HTMLElement) => void;
19
+ };
20
+ }
21
+ }
22
+ }
23
+
24
+ // Storybook serves examples/artifact-fixtures at /artifact-fixtures (see
25
+ // .storybook/main.ts staticDirs). These are real, cross-linked pages so
26
+ // relative-link nav, back/forward and multi-format all work in the iframe.
27
+ const BASE = '/artifact-fixtures';
28
+
29
+ const FILES: ArtifactFile[] = [
30
+ {
31
+ path: 'index.html',
32
+ url: `${BASE}/index.html`,
33
+ type: 'html',
34
+ language: 'html',
35
+ code: `<!DOCTYPE html>
36
+ <html lang="en">
37
+ <head>
38
+ <link rel="stylesheet" href="css/site.css" />
39
+ <title>Starboard — Home</title>
40
+ </head>
41
+ <body>
42
+ <h1>Starboard</h1>
43
+ <a href="about.html">About</a>
44
+ </body>
45
+ </html>`,
46
+ },
47
+ {
48
+ path: 'about.html',
49
+ url: `${BASE}/about.html`,
50
+ type: 'html',
51
+ language: 'html',
52
+ code: `<!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <link rel="stylesheet" href="css/site.css" />
56
+ <title>Starboard — About</title>
57
+ </head>
58
+ <body data-page="about">
59
+ <h1>About Starboard</h1>
60
+ <a href="index.html">← Home</a>
61
+ </body>
62
+ </html>`,
63
+ },
64
+ {
65
+ path: 'css/site.css',
66
+ url: `${BASE}/css/site.css`,
67
+ type: 'other',
68
+ language: 'css',
69
+ code: `:root { --accent: #6ea8fe; }
70
+ body { font-family: system-ui, sans-serif; }
71
+ .card { border: 1px solid var(--accent); border-radius: 14px; }`,
72
+ },
73
+ {
74
+ path: 'assets/logo.svg',
75
+ url: `${BASE}/assets/logo.svg`,
76
+ type: 'image',
77
+ },
78
+ {
79
+ path: 'assets/report.pdf',
80
+ url: `${BASE}/assets/report.pdf`,
81
+ type: 'pdf',
82
+ },
83
+ ];
84
+
85
+ /** A bordered, sized box the artifact fills. */
86
+ function Frame(props: { children: JSX.Element; height?: string }) {
87
+ return (
88
+ <div
89
+ style={{
90
+ height: props.height ?? '520px',
91
+ width: '100%',
92
+ 'max-width': '900px',
93
+ border: '1px solid var(--color-border, #e4e4e7)',
94
+ 'border-radius': '12px',
95
+ overflow: 'hidden',
96
+ }}
97
+ >
98
+ {props.children}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
104
+ <kc-artifact
105
+ src="https://your-backend.example/artifacts/abc/index.html"
106
+ style="display:block;height:520px"
107
+ ></kc-artifact>
108
+
109
+ <script type="module">
110
+ import '@kitnai/chat/elements'; // registers the custom elements
111
+
112
+ const el = document.querySelector('kc-artifact');
113
+ // \`files\` is a JS property (array): tree labels (folders from \`/\`), a preview
114
+ // \`url\` per file, and \`code\` for the Code tab.
115
+ el.files = [
116
+ { path: 'index.html', url: '…/index.html', type: 'html', code: '<!DOCTYPE …' },
117
+ { path: 'about.html', url: '…/about.html', type: 'html', code: '<!DOCTYPE …' },
118
+ { path: 'assets/logo.svg', url: '…/logo.svg', type: 'image' },
119
+ ];
120
+
121
+ el.addEventListener('navigate', (e) => console.log('navigate', e.detail.url));
122
+ el.addEventListener('tabchange', (e) => console.log('tab', e.detail.tab));
123
+ el.addEventListener('fileselect', (e) => console.log('file', e.detail.path));
124
+ </script>`;
125
+
126
+ const meta = {
127
+ title: 'Web Components/kc-artifact',
128
+ tags: ['autodocs'],
129
+ argTypes: argTypesFor('kc-artifact'),
130
+ parameters: {
131
+ layout: 'padded',
132
+ docs: {
133
+ description: specDescription('kc-artifact', [
134
+ '`<kc-artifact>` is the framework-agnostic **web component** for a framed, switchable **generated-artifact viewer** — the "canvas / artifacts" pattern (Claude Artifacts, ChatGPT Canvas, V0, bolt). It frames a consumer-served URL in a **sandboxed `<iframe>`** with a functional nav toolbar, and a **Preview | Code** toggle whose Code tab shows a file tree (`<kc-file-tree>`) + the active file source (`<kc-code-block>`). Isolated in **Shadow DOM**.',
135
+ "**When to use:** to show an AI-generated artifact (a web page, doc, image, or PDF your backend hosts) beside the conversation — typically inside a `<kc-resizable>` panel. The component **frames** content; your backend serves it, so relative links, back/forward, reload and multi-format work natively.",
136
+ "**How to use:** register once with `import '@kitnai/chat/elements'`, set `src` to the hosted URL, and set the `files` **property** (a JS array of `{ path, url?, code?, language?, type? }`) for the Code tab tree. The iframe `sandbox` defaults to `allow-scripts allow-forms` (**not** `allow-same-origin`); widen it via the `sandbox` attribute only if you trust the artifact. Listen for **`navigate`** (`detail.url`), **`tabchange`** (`detail.tab`), and **`fileselect`** (`detail.path`).",
137
+ '**Placement:** the preview/canvas panel of a compose-your-own-chat shell — e.g. `list | chat | artifact`. It **fills** its container, so give the parent (or the element) a height.',
138
+ 'See the **Code** tab for HTML usage.',
139
+ ]),
140
+ },
141
+ },
142
+ args: { tab: 'preview', sandbox: 'allow-scripts allow-forms' },
143
+ } satisfies Meta;
144
+
145
+ export default meta;
146
+ type Story = StoryObj;
147
+
148
+ /** Interactive playground — frames a live, cross-linked fixture. Flip Preview/Code. */
149
+ export const Playground: Story = {
150
+ render: (args: { src?: string; tab?: string; sandbox?: string }) => {
151
+ let el: HTMLElement & { files?: ArtifactFile[] };
152
+ onMount(() => {
153
+ if (el) el.files = FILES;
154
+ });
155
+ return (
156
+ <Frame>
157
+ <kc-artifact
158
+ ref={(e) => (el = e as HTMLElement & { files?: ArtifactFile[] })}
159
+ src={args.src ?? `${BASE}/index.html`}
160
+ tab={args.tab ?? 'preview'}
161
+ sandbox={args.sandbox ?? 'allow-scripts allow-forms'}
162
+ iframe-title="Starboard artifact preview"
163
+ />
164
+ </Frame>
165
+ );
166
+ },
167
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
168
+ };
169
+
170
+ /** Opens on the Code tab — file tree + syntax-highlighted source. */
171
+ export const CodeTab: Story = {
172
+ name: 'Code tab',
173
+ render: () => {
174
+ let el: HTMLElement & { files?: ArtifactFile[] };
175
+ onMount(() => {
176
+ if (el) el.files = FILES;
177
+ });
178
+ return (
179
+ <Frame>
180
+ <kc-artifact
181
+ ref={(e) => (el = e as HTMLElement & { files?: ArtifactFile[] })}
182
+ src={`${BASE}/index.html`}
183
+ tab="code"
184
+ active-file="index.html"
185
+ iframe-title="Starboard artifact preview"
186
+ />
187
+ </Frame>
188
+ );
189
+ },
190
+ };
191
+
192
+ /** Multi-format: the preview frames an image fixture natively. */
193
+ export const ImagePreview: Story = {
194
+ name: 'Image (multi-format)',
195
+ render: () => {
196
+ let el: HTMLElement & { files?: ArtifactFile[] };
197
+ onMount(() => {
198
+ if (el) el.files = FILES;
199
+ });
200
+ return (
201
+ <Frame>
202
+ <kc-artifact
203
+ ref={(e) => (el = e as HTMLElement & { files?: ArtifactFile[] })}
204
+ src={`${BASE}/assets/logo.svg`}
205
+ iframe-title="Logo image"
206
+ />
207
+ </Frame>
208
+ );
209
+ },
210
+ };
211
+
212
+ /** Multi-format: PDFs render inline via pdf.js (loaded on demand from a CDN). */
213
+ export const PdfPreview: Story = {
214
+ name: 'PDF (multi-format)',
215
+ render: () => {
216
+ let el: HTMLElement & { files?: ArtifactFile[] };
217
+ onMount(() => {
218
+ if (el) el.files = FILES;
219
+ });
220
+ return (
221
+ <Frame>
222
+ <kc-artifact
223
+ ref={(e) => (el = e as HTMLElement & { files?: ArtifactFile[] })}
224
+ src={`${BASE}/assets/report.pdf`}
225
+ iframe-title="Report PDF"
226
+ />
227
+ </Frame>
228
+ );
229
+ },
230
+ };
231
+
232
+ /** When inline rendering can't work (CORS / 404 / blocked CDN) the viewer shows
233
+ * an "Open in new tab / Download" card. Here the src 404s to force that path. */
234
+ export const PdfFallback: Story = {
235
+ name: 'PDF (fallback card)',
236
+ render: () => (
237
+ <Frame>
238
+ <kc-artifact
239
+ src={`${BASE}/assets/does-not-exist.pdf`}
240
+ iframe-title="Missing PDF"
241
+ />
242
+ </Frame>
243
+ ),
244
+ };
245
+
246
+ /** Logs the emitted events so you can see the controlled nav model in action. */
247
+ export const WithEvents: Story = {
248
+ name: 'Events',
249
+ render: () => {
250
+ const [log, setLog] = createSignal<string[]>([]);
251
+ let el: HTMLElement & { files?: ArtifactFile[] };
252
+ onMount(() => {
253
+ if (!el) return;
254
+ el.files = FILES;
255
+ el.addEventListener('navigate', (e) =>
256
+ setLog((l) => [`navigate → ${(e as CustomEvent).detail.url}`, ...l].slice(0, 6)),
257
+ );
258
+ el.addEventListener('tabchange', (e) =>
259
+ setLog((l) => [`tabchange → ${(e as CustomEvent).detail.tab}`, ...l].slice(0, 6)),
260
+ );
261
+ el.addEventListener('fileselect', (e) =>
262
+ setLog((l) => [`fileselect → ${(e as CustomEvent).detail.path}`, ...l].slice(0, 6)),
263
+ );
264
+ });
265
+ return (
266
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
267
+ <Frame height="420px">
268
+ <kc-artifact
269
+ ref={(e) => (el = e as HTMLElement & { files?: ArtifactFile[] })}
270
+ src={`${BASE}/index.html`}
271
+ iframe-title="Starboard artifact preview"
272
+ />
273
+ </Frame>
274
+ <pre
275
+ style={{
276
+ 'font-size': '12px',
277
+ 'max-width': '900px',
278
+ padding: '10px 12px',
279
+ 'border-radius': '8px',
280
+ background: 'var(--color-muted, #f4f4f5)',
281
+ color: 'var(--color-muted-foreground, #71717a)',
282
+ 'min-height': '96px',
283
+ margin: '0',
284
+ }}
285
+ >
286
+ {log().length ? log().join('\n') : '(navigate the preview / switch tabs / pick a file…)'}
287
+ </pre>
288
+ </div>
289
+ );
290
+ },
291
+ };
@@ -0,0 +1,72 @@
1
+ import { defineWebComponent } from './define';
2
+ import { Artifact, type ArtifactFile, type ArtifactTab } from '../components/artifact';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /** URL the preview iframe frames. Consumer-controlled. */
6
+ src?: string;
7
+ /** Files for the Code tab tree + each file's preview `url`. Set as a JS property (array). */
8
+ files: ArtifactFile[];
9
+ /** Active tab: `preview` (default) or `code`. */
10
+ tab?: ArtifactTab;
11
+ /** Selected file path — syncs the tree highlight, Code source, and preview. */
12
+ activeFile?: string;
13
+ /** iframe `sandbox` override. Secure default `allow-scripts allow-forms` (NOT `allow-same-origin`). */
14
+ sandbox?: string;
15
+ /** Accessible title for the preview iframe. */
16
+ iframeTitle?: string;
17
+ }
18
+
19
+ interface Events extends Record<string, unknown> {
20
+ /** Fired when the preview navigates. `detail.url` = the new location. */
21
+ navigate: { url: string };
22
+ /** Fired when the Preview|Code tab changes. `detail.tab`. */
23
+ tabchange: { tab: ArtifactTab };
24
+ /** Fired when a file is selected. `detail.path`. */
25
+ fileselect: { path: string };
26
+ }
27
+
28
+ /**
29
+ * `<kc-artifact>` — a framed, switchable generated-artifact viewer: a sandboxed
30
+ * preview iframe with a functional nav toolbar (back · forward · reload · home +
31
+ * editable path field) and a Preview|Code toggle; the Code tab shows a file tree
32
+ * (`<kc-file-tree>`) + the active file's source via `<kc-code-block>`. The
33
+ * component self-navigates the iframe and emits `navigate` / `tabchange` /
34
+ * `fileselect`. Designed to FILL its container (e.g. a `<kc-resizable>` panel).
35
+ */
36
+ defineWebComponent<Props, Events>('kc-artifact', {
37
+ src: undefined,
38
+ files: [],
39
+ tab: 'preview',
40
+ activeFile: undefined,
41
+ sandbox: 'allow-scripts allow-forms',
42
+ iframeTitle: undefined,
43
+ }, (props, { dispatch }) => (
44
+ <>
45
+ {/* The artifact fills its container; the internal column flex (toolbar
46
+ flex-shrink:0, body flex:1/min-height:0) is in the Solid component. Wrap
47
+ in a definite `1fr` grid cell (NOT :host) so the facade's sibling
48
+ portal-mount div can't steal a grid track — see resizable.tsx. */}
49
+ <style>{':host{display:block;height:100%;min-height:0}'}</style>
50
+ <div
51
+ style={{
52
+ display: 'grid',
53
+ 'grid-template-rows': 'minmax(0, 1fr)',
54
+ 'grid-template-columns': 'minmax(0, 1fr)',
55
+ height: '100%',
56
+ 'min-height': '0',
57
+ }}
58
+ >
59
+ <Artifact
60
+ src={props.src}
61
+ files={props.files}
62
+ tab={props.tab}
63
+ activeFile={props.activeFile}
64
+ sandbox={props.sandbox}
65
+ iframeTitle={props.iframeTitle}
66
+ onNavigate={(url) => dispatch('navigate', { url })}
67
+ onTabChange={(tab) => dispatch('tabchange', { tab })}
68
+ onFileSelect={(path) => dispatch('fileselect', { path })}
69
+ />
70
+ </div>
71
+ </>
72
+ ));
@@ -2,15 +2,14 @@ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
2
  import { onMount } from 'solid-js';
3
3
  import './register'; // side effect: registers the custom elements
4
4
  import type { AttachmentData } from '../components/attachments';
5
- import { ElementSpec } from '../stories/docs/element-spec';
6
- import { argTypesFor } from '../stories/docs/element-controls';
5
+ import { argTypesFor, specDescription } from '../stories/docs/element-controls';
7
6
 
8
7
  // The web components are custom DOM elements, so declare the tags for JSX.
9
8
  declare module 'solid-js' {
10
9
  // eslint-disable-next-line @typescript-eslint/no-namespace
11
10
  namespace JSX {
12
11
  interface IntrinsicElements {
13
- 'kitn-attachments': JSX.HTMLAttributes<HTMLElement> & {
12
+ 'kc-attachments': JSX.HTMLAttributes<HTMLElement> & {
14
13
  variant?: string;
15
14
  'hover-card'?: boolean | string;
16
15
  removable?: boolean | string;
@@ -60,7 +59,7 @@ const imageItems: AttachmentData[] = [
60
59
  },
61
60
  ];
62
61
 
63
- /** Render `<kitn-attachments>` with an `items` property and the given flags. */
62
+ /** Render `<kc-attachments>` with an `items` property and the given flags. */
64
63
  function AttachmentsElement(props: {
65
64
  items: AttachmentData[];
66
65
  variant?: string;
@@ -83,7 +82,7 @@ function AttachmentsElement(props: {
83
82
  }
84
83
  });
85
84
  return (
86
- <kitn-attachments
85
+ <kc-attachments
87
86
  ref={(e) => (el = e as HTMLElement)}
88
87
  style={{ display: 'block', padding: '24px', 'max-width': '720px' }}
89
88
  />
@@ -91,7 +90,7 @@ function AttachmentsElement(props: {
91
90
  }
92
91
 
93
92
  const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
94
- <kitn-attachments id="att" variant="grid" removable></kitn-attachments>
93
+ <kc-attachments id="att" variant="grid" removable></kc-attachments>
95
94
 
96
95
  <script type="module">
97
96
  import '@kitnai/chat/elements'; // registers the custom elements
@@ -110,7 +109,7 @@ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
110
109
  </script>`;
111
110
 
112
111
  const HOVER_SNIPPET = `<!-- inline/list chips with a hover-card image preview -->
113
- <kitn-attachments id="att" variant="inline" hover-card></kitn-attachments>
112
+ <kc-attachments id="att" variant="inline" hover-card></kc-attachments>
114
113
 
115
114
  <script type="module">
116
115
  import '@kitnai/chat/elements';
@@ -125,20 +124,18 @@ const HOVER_SNIPPET = `<!-- inline/list chips with a hover-card image preview --
125
124
  </script>`;
126
125
 
127
126
  const meta = {
128
- title: 'Web Components/kitn-attachments',
127
+ title: 'Web Components/kc-attachments',
129
128
  tags: ['autodocs'],
130
- argTypes: argTypesFor('kitn-attachments'),
129
+ argTypes: argTypesFor('kc-attachments'),
131
130
  parameters: {
132
131
  layout: 'fullscreen',
133
132
  docs: {
134
- description: {
135
- component: [
136
- '`<kitn-attachments>` is the framework-agnostic **web component** for a set of file/source attachments, and the exemplar for the "collapse a compound primitive to ONE configurable element" pattern: the sub-parts the SolidJS layer composes become attributes here. Isolated in **Shadow DOM**.',
133
+ description: specDescription('kc-attachments', [
134
+ '`<kc-attachments>` is the framework-agnostic **web component** for a set of file/source attachments, and the exemplar for the "collapse a compound primitive to ONE configurable element" pattern: the sub-parts the SolidJS layer composes become attributes here. Isolated in **Shadow DOM**.',
137
135
  '**When to use:** rendering attachment chips/tiles in a non-Solid app. In SolidJS, compose the `Attachment*` primitives for fully custom layouts.',
138
136
  "**How to use:** register once with `import '@kitnai/chat/elements'`, set the data via the `items` **property**, pick a layout with `variant` (`grid` | `inline` | `list`), add `removable` to get per-item remove buttons (emits a `remove` **CustomEvent** with `{ id }`), and `hover-card` for inline/list previews (image attachments preview their thumbnail).",
139
137
  'See the **Code** tab for HTML usage.',
140
- ].join('\n\n'),
141
- },
138
+ ]),
142
139
  },
143
140
  },
144
141
  } satisfies Meta;
@@ -146,12 +143,6 @@ const meta = {
146
143
  export default meta;
147
144
  type Story = StoryObj;
148
145
 
149
- /** Full generated API reference — properties, events, tokens, and composed-from. */
150
- export const API: Story = {
151
- render: () => <ElementSpec tag="kitn-attachments" />,
152
- parameters: { layout: 'padded' },
153
- };
154
-
155
146
  /** Visual tiles (the default `grid` variant) — image attachments render as real
156
147
  * `<img>` thumbnails; non-image files fall back to a media-type icon. */
157
148
  export const Grid: Story = {
@@ -1,5 +1,5 @@
1
1
  import { For, Show } from 'solid-js';
2
- import { defineKitnElement } from './define';
2
+ import { defineWebComponent } from './define';
3
3
  import {
4
4
  Attachments,
5
5
  Attachment,
@@ -31,14 +31,14 @@ interface Props extends Record<string, unknown> {
31
31
  emptyText?: string;
32
32
  }
33
33
 
34
- /** Events fired by `<kitn-attachments>`. */
34
+ /** Events fired by `<kc-attachments>`. */
35
35
  interface Events {
36
36
  /** A remove button was clicked. */
37
37
  remove: { id: string };
38
38
  }
39
39
 
40
40
  /**
41
- * `<kitn-attachments>` — the exemplar for the "collapse a compound primitive to
41
+ * `<kc-attachments>` — the exemplar for the "collapse a compound primitive to
42
42
  * ONE configurable element" pattern (Route 1). The presentation knobs that the
43
43
  * SolidJS layer expresses by composing sub-parts (`<AttachmentPreview>`,
44
44
  * `<AttachmentInfo>`, `<AttachmentHoverCard>`, `<AttachmentRemove>`) become
@@ -52,7 +52,7 @@ interface Events {
52
52
  * as an event. For fully-custom hover content, the SolidJS primitives remain the
53
53
  * escape hatch (a templated slot — "Route 2" — is a deliberate future add).
54
54
  */
55
- defineKitnElement<Props, Events>('kitn-attachments', {
55
+ defineWebComponent<Props, Events>('kc-attachments', {
56
56
  items: [],
57
57
  variant: 'grid',
58
58
  hoverCard: false,