@kitn.ai/ui 0.15.0 → 0.15.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 (177) hide show
  1. package/dist/kitn-chat.es.js +1 -1
  2. package/package.json +6 -1
  3. package/src/elements/compiled.css +1 -1
  4. package/src/components/artifact.stories.tsx +0 -138
  5. package/src/components/attachments.stories.tsx +0 -303
  6. package/src/components/attachments.test.tsx +0 -64
  7. package/src/components/chain-of-thought.stories.tsx +0 -211
  8. package/src/components/chat-container.stories.tsx +0 -187
  9. package/src/components/chat-thread.test.tsx +0 -58
  10. package/src/components/checkpoint.stories.tsx +0 -102
  11. package/src/components/code-block.stories.tsx +0 -150
  12. package/src/components/confirm-card.stories.tsx +0 -74
  13. package/src/components/context.stories.tsx +0 -229
  14. package/src/components/context.test.tsx +0 -133
  15. package/src/components/conversation-item.stories.tsx +0 -125
  16. package/src/components/conversation-list.stories.tsx +0 -133
  17. package/src/components/default-input.test.tsx +0 -111
  18. package/src/components/empty.stories.tsx +0 -434
  19. package/src/components/feedback-bar.stories.tsx +0 -138
  20. package/src/components/feedback-bar.test.tsx +0 -103
  21. package/src/components/file-tree.stories.tsx +0 -73
  22. package/src/components/file-upload.stories.tsx +0 -156
  23. package/src/components/image.stories.tsx +0 -89
  24. package/src/components/loader.stories.tsx +0 -181
  25. package/src/components/markdown.stories.tsx +0 -180
  26. package/src/components/message-action-bar.test.tsx +0 -128
  27. package/src/components/message-narrow.stories.tsx +0 -329
  28. package/src/components/message-skills.stories.tsx +0 -211
  29. package/src/components/message.stories.tsx +0 -281
  30. package/src/components/model-switcher.stories.tsx +0 -123
  31. package/src/components/model-switcher.test.tsx +0 -65
  32. package/src/components/prompt-input.stories.tsx +0 -222
  33. package/src/components/prompt-suggestion.stories.tsx +0 -142
  34. package/src/components/reasoning.stories.tsx +0 -140
  35. package/src/components/resizable.test.tsx +0 -226
  36. package/src/components/scroll-button.stories.tsx +0 -100
  37. package/src/components/slash-command.stories.tsx +0 -163
  38. package/src/components/source-list.test.tsx +0 -152
  39. package/src/components/source.stories.tsx +0 -124
  40. package/src/components/tasks-card.stories.tsx +0 -78
  41. package/src/components/text-shimmer.stories.tsx +0 -87
  42. package/src/components/thinking-bar.stories.tsx +0 -87
  43. package/src/components/tool.stories.tsx +0 -153
  44. package/src/components/voice-input.stories.tsx +0 -83
  45. package/src/elements/artifact.stories.tsx +0 -506
  46. package/src/elements/attachments.stories.tsx +0 -190
  47. package/src/elements/card.stories.tsx +0 -113
  48. package/src/elements/cards.stories.tsx +0 -59
  49. package/src/elements/catalog.stories.tsx +0 -491
  50. package/src/elements/chain-of-thought.declarative.test.tsx +0 -153
  51. package/src/elements/chain-of-thought.stories.tsx +0 -113
  52. package/src/elements/chat-scope-picker.stories.tsx +0 -73
  53. package/src/elements/chat-workspace.stories.tsx +0 -288
  54. package/src/elements/chat.stories.tsx +0 -151
  55. package/src/elements/checkpoint.stories.tsx +0 -72
  56. package/src/elements/choice.stories.tsx +0 -240
  57. package/src/elements/code-block.stories.tsx +0 -83
  58. package/src/elements/composed-shell.stories.tsx +0 -316
  59. package/src/elements/confirm-card.stories.tsx +0 -227
  60. package/src/elements/context-meter.stories.tsx +0 -151
  61. package/src/elements/conversation-list.declarative.test.tsx +0 -137
  62. package/src/elements/conversation-list.stories.tsx +0 -243
  63. package/src/elements/embed.stories.tsx +0 -213
  64. package/src/elements/empty.stories.tsx +0 -111
  65. package/src/elements/feedback-bar.stories.tsx +0 -114
  66. package/src/elements/file-tree.stories.tsx +0 -134
  67. package/src/elements/file-upload.stories.tsx +0 -82
  68. package/src/elements/form.stories.tsx +0 -248
  69. package/src/elements/image.stories.tsx +0 -71
  70. package/src/elements/link-preview.stories.tsx +0 -209
  71. package/src/elements/loader.stories.tsx +0 -88
  72. package/src/elements/markdown.stories.tsx +0 -76
  73. package/src/elements/message-skills.declarative.test.tsx +0 -104
  74. package/src/elements/message-skills.stories.tsx +0 -114
  75. package/src/elements/message.stories.tsx +0 -218
  76. package/src/elements/model-switcher.declarative.test.tsx +0 -130
  77. package/src/elements/model-switcher.stories.tsx +0 -198
  78. package/src/elements/popover.stories.tsx +0 -80
  79. package/src/elements/prompt-input-slash-command.test.tsx +0 -128
  80. package/src/elements/prompt-input-stoppable.test.tsx +0 -88
  81. package/src/elements/prompt-input-toolbar-actions.test.tsx +0 -120
  82. package/src/elements/prompt-input.stories.tsx +0 -331
  83. package/src/elements/prompt-suggestions.declarative.test.tsx +0 -164
  84. package/src/elements/prompt-suggestions.stories.tsx +0 -213
  85. package/src/elements/reasoning.stories.tsx +0 -77
  86. package/src/elements/remote.stories.tsx +0 -314
  87. package/src/elements/resizable.stories.tsx +0 -426
  88. package/src/elements/response-stream.stories.tsx +0 -79
  89. package/src/elements/scroll-button.stories.tsx +0 -202
  90. package/src/elements/scroll-button.test.tsx +0 -148
  91. package/src/elements/source-list.stories.tsx +0 -143
  92. package/src/elements/source.stories.tsx +0 -88
  93. package/src/elements/switch.stories.tsx +0 -56
  94. package/src/elements/tasks.stories.tsx +0 -237
  95. package/src/elements/text-shimmer.stories.tsx +0 -64
  96. package/src/elements/thinking-bar.stories.tsx +0 -72
  97. package/src/elements/tool.stories.tsx +0 -89
  98. package/src/elements/voice-input.stories.tsx +0 -88
  99. package/src/primitives/controllable.test.ts +0 -47
  100. package/src/stories/chat-panel-layout.stories.tsx +0 -145
  101. package/src/stories/chat-scene.tsx +0 -571
  102. package/src/stories/checkpoint-restore.stories.tsx +0 -256
  103. package/src/stories/context-usage.stories.tsx +0 -212
  104. package/src/stories/conversation-with-reasoning.stories.tsx +0 -182
  105. package/src/stories/conversation-with-sources.stories.tsx +0 -471
  106. package/src/stories/docs/Accessibility.mdx +0 -119
  107. package/src/stories/docs/ChildElements.mdx +0 -269
  108. package/src/stories/docs/ForAIAgents.mdx +0 -162
  109. package/src/stories/docs/GettingStarted.mdx +0 -65
  110. package/src/stories/docs/Installation.mdx +0 -83
  111. package/src/stories/docs/Introduction.mdx +0 -50
  112. package/src/stories/docs/SolidJsAdvanced.mdx +0 -17
  113. package/src/stories/docs/Theming.mdx +0 -85
  114. package/src/stories/docs/element-controls.ts +0 -121
  115. package/src/stories/docs/frameworks/Angular.mdx +0 -245
  116. package/src/stories/docs/frameworks/Html.mdx +0 -227
  117. package/src/stories/docs/frameworks/Overview.mdx +0 -47
  118. package/src/stories/docs/frameworks/React.mdx +0 -227
  119. package/src/stories/docs/frameworks/Solid.mdx +0 -173
  120. package/src/stories/docs/frameworks/Svelte.mdx +0 -194
  121. package/src/stories/docs/frameworks/Vue.mdx +0 -238
  122. package/src/stories/docs/generative-ui-overview.mdx +0 -186
  123. package/src/stories/docs/recipes/SpeechToText.mdx +0 -7
  124. package/src/stories/docs/recipes/Streaming.mdx +0 -68
  125. package/src/stories/docs/recipes/TextToSpeech.mdx +0 -35
  126. package/src/stories/docs/theme-editor/canvas.tsx +0 -32
  127. package/src/stories/docs/theme-editor/inspector.tsx +0 -66
  128. package/src/stories/docs/theme-editor/presets.test.ts +0 -32
  129. package/src/stories/docs/theme-editor/presets.ts +0 -64
  130. package/src/stories/docs/theme-editor/theme-css.test.ts +0 -19
  131. package/src/stories/docs/theme-editor/theme-css.ts +0 -15
  132. package/src/stories/docs/theme-editor/theme-editor.tsx +0 -146
  133. package/src/stories/docs/theme-tokens.tsx +0 -174
  134. package/src/stories/examples/ChoosingComponents.mdx +0 -106
  135. package/src/stories/examples/sample-data.ts +0 -79
  136. package/src/stories/examples/usage/checkpoint-restore.ts +0 -156
  137. package/src/stories/examples/usage/context-usage.ts +0 -591
  138. package/src/stories/examples/usage/conversation-with-reasoning.ts +0 -224
  139. package/src/stories/examples/usage/conversation-with-sources.ts +0 -589
  140. package/src/stories/examples/usage/empty-state.ts +0 -144
  141. package/src/stories/examples/usage/full-chat-app.ts +0 -277
  142. package/src/stories/examples/usage/index.ts +0 -63
  143. package/src/stories/examples/usage/message-actions.ts +0 -704
  144. package/src/stories/examples/usage/pattern-centered-conversation.ts +0 -446
  145. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +0 -442
  146. package/src/stories/examples/usage/pattern-docked-widget.ts +0 -479
  147. package/src/stories/examples/usage/prompt-input-variants.ts +0 -1356
  148. package/src/stories/examples/usage/streaming-response.ts +0 -687
  149. package/src/stories/examples/usage/types.ts +0 -33
  150. package/src/stories/full-chat.stories.tsx +0 -18
  151. package/src/stories/message-actions.stories.tsx +0 -230
  152. package/src/stories/pattern-centered-conversation.stories.tsx +0 -93
  153. package/src/stories/pattern-docked-widget.stories.tsx +0 -93
  154. package/src/stories/pattern-empty-state.stories.tsx +0 -76
  155. package/src/stories/prompt-input-variants.stories.tsx +0 -456
  156. package/src/stories/streaming-response.stories.tsx +0 -438
  157. package/src/stories/theme-editor.stories.tsx +0 -16
  158. package/src/stories/token-reference.stories.tsx +0 -18
  159. package/src/stories/typography.stories.tsx +0 -78
  160. package/src/ui/avatar.stories.tsx +0 -103
  161. package/src/ui/badge.stories.tsx +0 -86
  162. package/src/ui/button.stories.tsx +0 -145
  163. package/src/ui/collapsible.stories.tsx +0 -69
  164. package/src/ui/dropdown.stories.tsx +0 -59
  165. package/src/ui/hover-card.stories.tsx +0 -77
  166. package/src/ui/hover-card.test.tsx +0 -30
  167. package/src/ui/overlay.stories.tsx +0 -115
  168. package/src/ui/popover.stories.tsx +0 -81
  169. package/src/ui/popover.test.tsx +0 -99
  170. package/src/ui/resizable.stories.tsx +0 -236
  171. package/src/ui/scroll-area.stories.tsx +0 -50
  172. package/src/ui/separator.stories.tsx +0 -81
  173. package/src/ui/skeleton.stories.tsx +0 -337
  174. package/src/ui/switch.stories.tsx +0 -51
  175. package/src/ui/switch.test.tsx +0 -59
  176. package/src/ui/textarea.stories.tsx +0 -76
  177. package/src/ui/tooltip.stories.tsx +0 -74
@@ -1,238 +0,0 @@
1
- import { Meta } from '@storybook/addon-docs/blocks';
2
-
3
- <Meta title="Docs/Frameworks/Vue" />
4
-
5
- # Vue
6
-
7
- Use the `kai-*` custom elements directly in Vue templates. Pass arrays and objects via the **`.prop`
8
- modifier** so Vue sets them as live DOM properties (not stringified attributes). Events use the
9
- standard `@event` syntax.
10
-
11
- There are **two ways to build with the kit**, and you can mix them:
12
-
13
- 1. **`<kai-chat>`** — the batteries-included shell: a whole chat experience in one tag. Fastest start.
14
- 2. **Compose the individual elements** (`<kai-conversations>`, `<kai-markdown>`, `<kai-artifact>`, …)
15
- into your own layout when you want full control.
16
-
17
- Both are shown below.
18
-
19
- ## Install & setup
20
-
21
- ```bash
22
- npm i @kitn.ai/ui
23
- ```
24
-
25
- Register the custom elements **once**, as a static side-effect import in `main.ts` **before**
26
- `createApp().mount()`. Vue stamps the tags at mount time — if the elements aren't registered yet,
27
- array/object props set by Vue are clobbered when the elements upgrade later and the UI renders blank.
28
-
29
- ```ts
30
- // src/main.ts
31
- import '@kitn.ai/ui/elements'; // MUST come before createApp
32
- import { createApp } from 'vue';
33
- import App from './App.vue';
34
-
35
- createApp(App).mount('#app');
36
- ```
37
-
38
- Tell Vue's template compiler that `kai-*` tags are native custom elements, not Vue components —
39
- this prevents "Unknown custom element" warnings and ensures `.prop` bindings work correctly:
40
-
41
- ```ts
42
- // vite.config.ts
43
- import { defineConfig } from 'vite';
44
- import vue from '@vitejs/plugin-vue';
45
-
46
- export default defineConfig({
47
- plugins: [
48
- vue({
49
- template: {
50
- compilerOptions: {
51
- isCustomElement: (tag) => tag.startsWith('kai-'),
52
- },
53
- },
54
- }),
55
- ],
56
- });
57
- ```
58
-
59
- No CSS to import: each element is styled inside its own Shadow DOM. Only pull in
60
- `@kitn.ai/ui/theme.css` if you want to override design tokens (see **Theming**).
61
-
62
- ### TypeScript / Volar augmentation
63
-
64
- Add a reference to the kit's type declarations once so Vue's template compiler (and Volar) knows
65
- the element's attributes and properties:
66
-
67
- ```ts
68
- // env.d.ts (or vite-env.d.ts)
69
- /// <reference types="@kitn.ai/ui/elements" />
70
- ```
71
-
72
- ## Quick start — the all-in-one shell
73
-
74
- `<kai-chat>` is **transport-agnostic**: give it a `messages` array, handle the `submit` event, and
75
- stream your model's reply back into state. You own the request; the element owns the UI.
76
-
77
- ```html
78
- <script setup lang="ts">
79
- import '@kitn.ai/ui/elements';
80
- import { ref } from 'vue';
81
-
82
- type Message = { id: string; role: 'user' | 'assistant'; content: string };
83
-
84
- const messages = ref<Message[]>([
85
- { id: '1', role: 'assistant', content: 'Hello! How can I help?' },
86
- ]);
87
-
88
- const handleSubmit = async (e: CustomEvent<{ value: string }>) => {
89
- const history = [...messages.value, { id: crypto.randomUUID(), role: 'user' as const, content: e.detail.value }];
90
- messages.value = history;
91
-
92
- const aid = crypto.randomUUID();
93
- messages.value = [...history, { id: aid, role: 'assistant', content: '' }];
94
-
95
- let answer = '';
96
- for await (const token of streamFromYourAPI(history)) {
97
- answer += token;
98
- messages.value = messages.value.map((m) => (m.id === aid ? { ...m, content: answer } : m));
99
- }
100
- };
101
- </script>
102
-
103
- <template>
104
- <!-- The elements fill their container — use flex and let the element grow with
105
- flex: 1 rather than hard-coding a height. -->
106
- <div style="display: flex; flex-direction: column; height: 100dvh;">
107
- <kai-chat
108
- :messages.prop="messages"
109
- :suggestions.prop="['Summarize the chat', 'Start fresh']"
110
- style="flex: 1; min-height: 0;"
111
- @kai-submit="handleSubmit"
112
- />
113
- </div>
114
- </template>
115
- ```
116
-
117
- ## Go further — compose the pieces
118
-
119
- `<kai-chat>` is one option, not the only one. Every element can be composed in your own layout.
120
- Here's a multi-conversation shell — a `<kai-conversations>` sidebar next to the `<kai-chat>` thread:
121
-
122
- ```html
123
- <script setup lang="ts">
124
- import '@kitn.ai/ui/elements';
125
- import { ref } from 'vue';
126
-
127
- const conversations = ref([
128
- {
129
- id: 'c1',
130
- title: 'First chat',
131
- scope: { type: 'document' },
132
- messageCount: 3,
133
- lastMessageAt: '2026-06-01T12:00:00Z',
134
- updatedAt: '2026-06-01T12:00:00Z',
135
- },
136
- ]);
137
- const activeId = ref('c1');
138
- const messages = ref([{ id: '1', role: 'assistant', content: 'Hi!' }]);
139
-
140
- const onSelect = (e: CustomEvent<{ id: string }>) => {
141
- activeId.value = e.detail.id;
142
- // load messages for the selected conversation
143
- };
144
-
145
- const onSubmit = (e: CustomEvent<{ value: string }>) => {
146
- // send message and stream reply
147
- };
148
- </script>
149
-
150
- <template>
151
- <!-- Lay panels out with flex: the sidebar is fixed-width, the thread takes the rest
152
- with flex: 1. The elements fill whatever box you give them. -->
153
- <div style="display: flex; height: 100dvh;">
154
- <kai-conversations
155
- :conversations.prop="conversations"
156
- :active-id="activeId"
157
- style="width: 300px; flex-shrink: 0;"
158
- @kai-conversation-select="onSelect"
159
- @kai-new-chat="startNewConversation"
160
- />
161
- <kai-chat
162
- :messages.prop="messages"
163
- style="flex: 1; min-width: 0;"
164
- @kai-submit="onSubmit"
165
- />
166
- </div>
167
- </template>
168
- ```
169
-
170
- ### Make the panels resizable
171
-
172
- Want a draggable divider between the sidebar and the thread? Wrap the panels in `<kai-resizable>`
173
- with one `<kai-resizable-item>` each — the handles are inserted for you (up to 3 panels). Each item
174
- takes a `size` (px or `%`) plus optional `min`/`max`; listen for `@change` (`detail.sizes`) to
175
- persist the layout.
176
-
177
- ```html
178
- <template>
179
- <div style="display: flex; flex-direction: column; height: 100dvh;">
180
- <kai-resizable orientation="horizontal" style="flex: 1; min-height: 0;">
181
- <kai-resizable-item size="25%" min="200px">
182
- <kai-conversations
183
- :conversations.prop="conversations"
184
- :active-id="activeId"
185
- @kai-conversation-select="onSelect"
186
- />
187
- </kai-resizable-item>
188
- <kai-resizable-item>
189
- <kai-chat :messages.prop="messages" @kai-submit="onSubmit" />
190
- </kai-resizable-item>
191
- </kai-resizable>
192
- </div>
193
- </template>
194
- ```
195
-
196
- You can also drop **standalone display elements** anywhere in your own UI — `<kai-markdown>`,
197
- `<kai-code-block>`, `<kai-artifact>`, `<kai-reasoning>`, `<kai-tool>` — to render rich AI content
198
- without adopting the whole chat. Each fills its container and is controlled via props and events.
199
-
200
- > **See it all assembled:** **[Examples → Full Chat App](?path=/story/examples-full-chat-app--default)**
201
- > wires a sidebar, threaded markdown, reasoning, a tool call, a model switcher, a context meter, and
202
- > a rich prompt input into one screen — a working reference to crib from.
203
-
204
- > **Find every element:** browse the **Components** section in the sidebar. Each element's **API**
205
- > tab lists its props, events, and copy-paste usage for Vue (and every other framework).
206
-
207
- ## Props & events
208
-
209
- The rule for all elements: **rich data (arrays, objects) goes in as DOM properties; interactions
210
- come out as CustomEvents.**
211
-
212
- In Vue templates, use the **`.prop` modifier** to bind rich data as DOM properties rather than
213
- stringified HTML attributes. Scalar values (strings, booleans, numbers) can go through normal
214
- attribute bindings. Events use `@event` and carry their payload in `event.detail`.
215
-
216
- | Binding type | Vue syntax | When to use |
217
- |---|---|---|
218
- | Rich data (array/object) | `:messages.prop="messages"` | Always for arrays and objects |
219
- | Scalar attribute | `:active-id="activeId"` or `active-id="c1"` | Strings, booleans, numbers |
220
- | Event | `@kai-conversation-select="onSelect"` | All CustomEvents |
221
-
222
- Common events and their `detail` payloads:
223
-
224
- | Element | DOM event | `detail` |
225
- |---|---|---|
226
- | `kai-chat` | `submit` | `{ value: string }` |
227
- | `kai-conversations` | `conversationselect` | `{ id: string }` |
228
- | `kai-conversations` | `newchat` | — |
229
- | `kai-conversations` | `togglesidebar` | — |
230
- | `kai-resizable` | `change` | `{ sizes: string[] }` |
231
-
232
- Example event handler pattern:
233
-
234
- ```ts
235
- const onSelect = (e: CustomEvent<{ id: string }>) => {
236
- activeId.value = e.detail.id;
237
- };
238
- ```
@@ -1,186 +0,0 @@
1
- {/* src/stories/docs/generative-ui-overview.mdx */}
2
- import { Meta } from '@storybook/addon-docs/blocks';
3
-
4
- <Meta title="Generative UI/Overview" />
5
-
6
- # Generative UI
7
-
8
- Agents don't just reply with text — they ask the chat to **render typed, interactive
9
- cards**: a form to fill, an action to approve, a plan to pick from, a link to preview.
10
- `@kitn.ai/ui` does this with a small, typed **Card Contract** and a turnkey dispatcher.
11
-
12
- ## The loop
13
-
14
- ```
15
- agent/server ──CardEnvelope(s)──▶ host sets <kai-cards>.cards
16
- ▲ │
17
- │ dispatcher renders the right kai-* by `type`
18
- │ │
19
- └── result / next envelopes ◀─ CardPolicy ◀─ kai-card event ◀─ user interacts
20
- ```
21
-
22
- ## Envelopes vs. cards
23
-
24
- Two things are easy to conflate — keeping them straight makes the rest click:
25
-
26
- - An **envelope** is what the agent *sends*: an addressed data wrapper, not UI. Like a
27
- postal envelope, it *carries* a card spec but isn't the card itself.
28
- - A **card** is what the user *sees*: the rendered, interactive UI the dispatcher mounts
29
- for that envelope (a `kai-form`, `kai-confirm`, … inside the shared card chrome).
30
-
31
- The dispatcher's whole job is **envelope in → card out** — one envelope renders one card.
32
-
33
- Everything an agent sends is a `CardEnvelope`:
34
-
35
- ```ts
36
- interface CardEnvelope {
37
- type: string; // selects the card: form | confirm | choice | tasks | link | embed
38
- id: string; // correlates every event back to this card
39
- data: unknown; // follows that type's JSON Schema (shipped in dist/schemas)
40
- title?: string; // optional chrome heading
41
- resolution?: CardResolution; // set once the user acts → renders the read-only view
42
- }
43
- ```
44
-
45
- `data` follows the `type`'s **JSON Schema**, so an agent/server can validate before
46
- sending; `id` correlates every event back to this card. (For **remote** cards, a transport
47
- `WireFrame` wraps this *same* envelope over `postMessage` — the envelope inside is identical;
48
- see Remote cards.)
49
-
50
- ## Turnkey: drop `<kai-cards>`
51
-
52
- ```html
53
- <kai-cards></kai-cards>
54
- <script type="module">
55
- import '@kitn.ai/ui/elements';
56
- const cards = document.querySelector('kai-cards');
57
- cards.cards = [{ type: 'confirm', id: 'deploy', title: 'Deploy?',
58
- data: { body: 'Ship it?', actions: [{ id: 'go', label: 'Deploy', default: true }] } }];
59
- cards.policy = { onAction: (id, action) => console.log(id, action) };
60
- </script>
61
- ```
62
-
63
- `<kai-cards>` renders one card per envelope and routes every interaction through `policy`
64
- (or listen for the raw bubbling `kai-card` event). In SolidJS, use `renderCard(envelope)` /
65
- `<CardRenderer>` inside a `<CardProvider>`.
66
-
67
- ## Streaming & follow-ups
68
-
69
- Swap the `cards` array to add, replace, or remove cards — e.g. replace a `confirm` with a
70
- result card once the user acts. For **live streaming into a single card**, the SolidJS
71
- `<CardRenderer>` re-renders reactively as its envelope's `data` changes, so an agent can
72
- stream into it as work progresses. (Keyed-by-`id` in-place updates within a `<kai-cards>`
73
- list are a planned refinement.)
74
-
75
- ## Resolved cards
76
-
77
- Interactive cards (`kai-confirm`, `kai-choice`, `kai-tasks`, `kai-form`) flip to a **chromed
78
- read-only** view the moment the user acts — optimistically, before any server round-trip.
79
-
80
- To make that survive a **reload or history re-hydration**, the card reads an optional
81
- `resolution` field on its `CardEnvelope`. Persist it by feeding the card's terminal event
82
- back into the array with the `applyResolution` helper:
83
-
84
- ```ts
85
- import { applyResolution } from '@kitn.ai/ui';
86
-
87
- // <kai-cards> is controlled — persist the resolution so it survives reload:
88
- el.addEventListener('kai-card', (e) => {
89
- el.cards = applyResolution(el.cards, e.detail); // resolved cards re-hydrate read-only
90
- // …then save el.cards to your store/server.
91
- });
92
- ```
93
-
94
- `applyResolution` is pure and safe to call on every `kai-card` event — non-terminal events
95
- (`ready` / `error` / …) and unknown card ids return the same array unchanged. The terminal
96
- verbs are `action` (confirm / choice) and `submit` (tasks / form).
97
-
98
- ## Remote cards
99
-
100
- Everything above renders cards you **bundle yourself** — the dispatcher mounts a native
101
- `kai-*` element for each envelope. That native `types` seam stays the easy path: register a
102
- card type, ship its element, done.
103
-
104
- **Remote cards** are the power feature for **provider-owned, cross-origin UI** — a card
105
- whose code lives on a *different* origin (a partner, a plugin, your own card service) and
106
- that you don't want to bundle or trust in your DOM. `<kai-remote>` renders it inside a
107
- **sandboxed cross-origin `<iframe>`** and bridges it to the host over `postMessage`, carrying
108
- the **exact same** `CardEnvelope` / `CardContext` / `CardEvent` shapes and routing every
109
- event through the **same `CardPolicy`** as a native card. The envelope inside is identical —
110
- a transport `WireFrame` just wraps it on the wire.
111
-
112
- ```html
113
- <kai-remote
114
- provider-origin="https://cards.provider.example"
115
- src="https://cards.provider.example/card"
116
- ></kai-remote>
117
- <script type="module">
118
- import '@kitn.ai/ui/elements'; // registers <kai-remote>
119
-
120
- const el = document.querySelector('kai-remote');
121
- // The CardEnvelope is set as a JS property — it travels down the wire unchanged.
122
- el.envelope = { type: 'form', id: 'signup', title: 'Join the beta',
123
- data: { type: 'object', required: ['email'],
124
- properties: { email: { type: 'string', title: 'Email', format: 'email' } } } };
125
-
126
- // Every routed event is re-emitted as a bubbling kai-card CustomEvent — persist the
127
- // resolution exactly like a native card:
128
- el.addEventListener('kai-card', (e) => {
129
- if (e.detail.kind === 'submit') save(e.detail.data);
130
- });
131
- // …or pass a CardPolicy directly: el.policy = { onSubmit: (id, data) => save(data) };
132
- </script>
133
- ```
134
-
135
- Prefer the **imperative** SDK when you manage the lifecycle yourself (theme refresh, swapping
136
- cards in one warm frame):
137
-
138
- ```ts
139
- import { mountRemoteCard } from '@kitn.ai/ui';
140
-
141
- const handle = mountRemoteCard({
142
- container: document.querySelector('#slot'),
143
- providerOrigin: 'https://cards.provider.example',
144
- src: 'https://cards.provider.example/card',
145
- envelope,
146
- context: { theme: { mode: 'dark' }, locale: 'en', authToken },
147
- policy: { onSubmit: (cardId, data) => { /* persist via applyResolution, advance the chat */ } },
148
- });
149
-
150
- handle.updateContext({ theme: { mode: 'light' } }); // re-themes the framed card live
151
- handle.update(nextEnvelope); // swap cards in the same iframe
152
- handle.destroy(); // teardown + remove the frame
153
- ```
154
-
155
- **Inputs.** `providerOrigin` is the exact origin (`https://…`, or `http://localhost` for
156
- dev) the iframe is pinned to. `src` is the provider page URL (must be on `providerOrigin`).
157
- `envelope` is the `CardEnvelope` to render. `policy` is the optional `CardPolicy`. `context`
158
- carries theme/locale/`a11y` and a short-lived `authToken`.
159
-
160
- **Theme changes remount; token/locale refreshes are silent (v1).** A host **theme change
161
- re-renders (remounts) the remote card**, so any in-progress remote-card state (e.g. a
162
- half-filled form) **resets on a theme toggle** — renderers apply theme imperatively at mount,
163
- and v1 has no in-place renderer theme hook yet. **Token / locale / `a11y` refreshes are
164
- silent** (no remount — renderers read `host.context()` on demand). A future refinement is a
165
- renderer context subscription for live re-theme without remount.
166
-
167
- **Security model.** The host mints a per-instance **nonce** and pins every inbound frame on
168
- **origin + source window + nonce + negotiated protocol version** — a frame failing any check
169
- is dropped, so a wrong-origin or forged `postMessage` can't drive your policy. The iframe is
170
- sandboxed (`allow-scripts allow-forms allow-same-origin`, **no** `allow-popups`) and gets a
171
- `no-referrer` policy. Any `authToken` you pass should be **short-lived** (the provider sees
172
- it). The **host page must set `Content-Security-Policy: frame-ancestors 'self'`** (or
173
- `'none'`) so it can't itself be reframed; the **provider** locks who may embed it with its
174
- own `frame-ancestors` listing your exact host origin (no wildcards).
175
-
176
- **Reference runtime.** `examples/remote-provider/` is the runnable provider iframe a real
177
- provider copies — it calls `createCardBridge` (from `@kitn.ai/ui/provider`, a SolidJS-free
178
- bundle) and registers one renderer per card `type`. `examples/remote-host/` is the matching
179
- host page. The real cross-origin behavior is verified by the standalone Playwright suite
180
- (`tests/e2e/`).
181
-
182
- ## Card elements are `kai-*` components too
183
-
184
- Each card type (`kai-confirm`, `kai-choice`, `kai-tasks`, `kai-form`, `kai-link`, `kai-embed`, `kai-remote`) is a registered `kai-*` custom element with its own prop/event API — the same model as everything else under **Components**. They live here under **Generative UI / Cards** because they only make sense inside the dispatcher/envelope system, but they follow the same component contract: attributes, JS properties, a `kai-card` event, CSS parts. Browse **Generative UI → Cards** for the per-element API references and live demos.
185
-
186
- See **Cards** for each card type, and **SDK** for the dispatcher in action.
@@ -1,7 +0,0 @@
1
- import { Meta } from '@storybook/addon-docs/blocks';
2
-
3
- <Meta title="Docs/Recipes/Speech-to-Text" />
4
-
5
- # Speech-to-text
6
-
7
- The reverse direction is built in — the kit ships `<kai-voice-input>` (and a `VoiceInput` SolidJS component). Find it in the sidebar under the component stories.
@@ -1,68 +0,0 @@
1
- import { Meta } from '@storybook/addon-docs/blocks';
2
-
3
- <Meta title="Docs/Recipes/Streaming (OpenRouter)" />
4
-
5
- # Streaming from OpenRouter
6
-
7
- [OpenRouter](https://openrouter.ai) exposes an OpenAI-compatible streaming API (Server-Sent Events). Wire it into the `submit` event:
8
-
9
- > **Security:** never ship an API key to the browser. In production, point `fetch` at your own backend that proxies to OpenRouter and injects the key.
10
-
11
- ```js
12
- chat.addEventListener('kai-submit', async (e) => {
13
- const text = e.detail.value.trim();
14
- if (!text) return;
15
-
16
- // 1. Show the user message
17
- const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
18
- chat.messages = history;
19
- chat.loading = true;
20
-
21
- // 2. Empty assistant placeholder to stream into
22
- const assistantId = crypto.randomUUID();
23
- chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];
24
-
25
- // In production, replace this with your own proxy endpoint.
26
- const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
27
- method: 'POST',
28
- headers: {
29
- 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
30
- 'Content-Type': 'application/json',
31
- },
32
- body: JSON.stringify({
33
- model: 'anthropic/claude-sonnet-4',
34
- stream: true,
35
- messages: history.map((m) => ({ role: m.role, content: m.content })),
36
- }),
37
- });
38
-
39
- const reader = res.body.getReader();
40
- const decoder = new TextDecoder();
41
- let buffer = '';
42
- let answer = '';
43
-
44
- while (true) {
45
- const { value, done } = await reader.read();
46
- if (done) break;
47
- buffer += decoder.decode(value, { stream: true });
48
-
49
- const lines = buffer.split('\n');
50
- buffer = lines.pop();
51
- for (const line of lines) {
52
- const s = line.trim();
53
- if (!s.startsWith('data:')) continue;
54
- const payload = s.slice(5).trim();
55
- if (payload === '[DONE]') continue;
56
- try {
57
- const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
58
- if (!delta) continue;
59
- answer += delta;
60
- chat.messages = chat.messages.map((m) =>
61
- m.id === assistantId ? { ...m, content: answer } : m
62
- );
63
- } catch { /* ignore non-JSON keep-alive lines */ }
64
- }
65
- }
66
- chat.loading = false;
67
- });
68
- ```
@@ -1,35 +0,0 @@
1
- import { Meta } from '@storybook/addon-docs/blocks';
2
-
3
- <Meta title="Docs/Recipes/Text-to-Speech" />
4
-
5
- # Text-to-speech (TTS)
6
-
7
- ## Browser-native (zero dependencies)
8
-
9
- Speak each reply once it finishes streaming — call `speak(answer)` right before `chat.loading = false`:
10
-
11
- ```js
12
- function speak(text) {
13
- if (!('speechSynthesis' in window)) return;
14
- const utter = new SpeechSynthesisUtterance(text);
15
- utter.lang = 'en-US';
16
- speechSynthesis.cancel();
17
- speechSynthesis.speak(utter);
18
- }
19
- ```
20
-
21
- ## Cloud TTS (OpenAI, ElevenLabs, …)
22
-
23
- For higher-quality voices, have your backend call a TTS API and return audio (keep the provider key server-side):
24
-
25
- ```js
26
- async function speakCloud(text) {
27
- const res = await fetch('/api/tts', {
28
- method: 'POST',
29
- headers: { 'Content-Type': 'application/json' },
30
- body: JSON.stringify({ text, voice: 'alloy' }),
31
- });
32
- const audio = new Audio(URL.createObjectURL(await res.blob()));
33
- audio.play();
34
- }
35
- ```
@@ -1,32 +0,0 @@
1
- import { ChatScene } from '../../chat-scene';
2
-
3
- /** Class the editor targets to write the active mode's token values directly onto
4
- * this wrapper — so the preview reflects the editor's mode independently of any
5
- * ancestor `.dark` (e.g. Storybook's own dark theme), and only the canvas reskins. */
6
- export const CANVAS_CLASS = 'kitn-editor-canvas';
7
-
8
- /** Live preview: the real chat app scene + a small rail for tokens the chat
9
- * doesn't naturally surface. The editor sets the active palette on CANVAS_CLASS;
10
- * the `.dark` class is also applied for any `dark:`-keyed component styling. */
11
- export function Canvas(props: { mode: 'light' | 'dark' }) {
12
- return (
13
- <div classList={{ [CANVAS_CLASS]: true, dark: props.mode === 'dark' }} class="h-full">
14
- <div class="h-full flex flex-col rounded-xl border border-border overflow-hidden bg-background">
15
- {/* The real product UI — same component the Full Chat App example uses */}
16
- <div class="flex-1 min-h-0">
17
- <ChatScene class="h-full" />
18
- </div>
19
-
20
- {/* Coverage rail: tokens the chat scene doesn't surface at rest */}
21
- <div class="shrink-0 border-t border-border px-4 py-2.5 flex flex-wrap items-center gap-2 bg-background text-foreground">
22
- <span class="text-[11px] text-muted-foreground mr-1">Other tokens:</span>
23
- <button class="bg-destructive text-destructive-foreground rounded-md px-3 h-8 text-xs font-medium">Destructive</button>
24
- <button class="bg-secondary text-secondary-foreground rounded-md px-3 h-8 text-xs font-medium">Secondary</button>
25
- <span class="bg-accent text-accent-foreground rounded-md px-2.5 py-1.5 text-xs">Accent</span>
26
- <span class="bg-popover text-popover-foreground border border-border shadow rounded-md px-2.5 py-1.5 text-xs">Popover</span>
27
- <input class="bg-input border border-border rounded-md px-2 h-8 text-xs ring-2 ring-ring" placeholder="Focus ring" />
28
- </div>
29
- </div>
30
- </div>
31
- );
32
- }
@@ -1,66 +0,0 @@
1
- // src/stories/docs/theme-editor/inspector.tsx
2
- import { For, type JSX } from 'solid-js';
3
- import { toHex, PURPOSE } from '../theme-tokens';
4
- import type { Palette } from './theme-css';
5
-
6
- const RADIUS_MAX = 1.4; // rem
7
-
8
- export function Inspector(props: {
9
- tokens: string[]; // ordered '--color-*' names
10
- values: Palette; // active-mode palette (drives swatch colors)
11
- radius: string; // e.g. '0.6rem'
12
- onColorChange: (token: string, hex: string) => void;
13
- onRadiusChange: (rem: string) => void;
14
- }) {
15
- const radiusRem = () => parseFloat(props.radius) || 0;
16
- const swatch: JSX.CSSProperties = {
17
- width: '1.75rem', height: '1.75rem', padding: '0', border: '1px solid var(--color-border)',
18
- 'border-radius': '6px', background: 'none', cursor: 'pointer', 'flex-shrink': '0',
19
- };
20
- return (
21
- <div class="p-3 text-foreground">
22
- <div class="text-xs font-semibold mb-2.5 text-muted-foreground uppercase tracking-wide">Tokens</div>
23
- <div class="flex flex-col gap-2.5">
24
- <For each={props.tokens}>
25
- {(name) => {
26
- const hex = () => {
27
- try {
28
- return toHex(props.values[name]);
29
- } catch {
30
- return '#888888';
31
- }
32
- };
33
- return (
34
- <label class="flex items-center gap-2.5">
35
- <input
36
- type="color"
37
- value={hex()}
38
- onInput={(e) => props.onColorChange(name, e.currentTarget.value)}
39
- style={swatch}
40
- />
41
- <span class="flex min-w-0 flex-col leading-tight">
42
- <span class="text-sm font-medium truncate">{name.replace('--color-', '')}</span>
43
- <span class="text-xs text-muted-foreground truncate">{PURPOSE[name] ?? ''}</span>
44
- </span>
45
- </label>
46
- );
47
- }}
48
- </For>
49
- </div>
50
-
51
- <div class="text-xs font-semibold mt-5 mb-2.5 text-muted-foreground uppercase tracking-wide">Radius</div>
52
- <label class="flex items-center gap-2.5 text-sm">
53
- <input
54
- type="range"
55
- min="0"
56
- max={RADIUS_MAX}
57
- step="0.05"
58
- value={radiusRem()}
59
- onInput={(e) => props.onRadiusChange(`${e.currentTarget.value}rem`)}
60
- class="flex-1"
61
- />
62
- <span class="text-sm tabular-nums w-16 text-right">{props.radius}</span>
63
- </label>
64
- </div>
65
- );
66
- }
@@ -1,32 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { buildPresets, BRAND_OVERRIDES } from './presets';
3
-
4
- const base = {
5
- light: { '--color-primary': 'hsl(240 5.9% 10%)', '--color-border': 'hsl(240 5.9% 90%)', '--radius': '0.6rem' },
6
- dark: { '--color-primary': 'hsl(0 0% 98%)', '--color-border': 'hsl(240 3.7% 15.9%)' },
7
- };
8
-
9
- describe('buildPresets', () => {
10
- it('orders Default first, then the brand presets', () => {
11
- expect(buildPresets(base).map((p) => p.name)).toEqual(['Default', 'Violet', 'Emerald', 'Mono']);
12
- });
13
-
14
- it('Default preset equals the base palette (no drift)', () => {
15
- const def = buildPresets(base).find((p) => p.name === 'Default')!;
16
- expect(def.light).toEqual(base.light);
17
- expect(def.dark).toEqual(base.dark);
18
- });
19
-
20
- it('brand presets override primary but preserve base neutrals', () => {
21
- const violet = buildPresets(base).find((p) => p.name === 'Violet')!;
22
- expect(violet.light['--color-primary']).toBe(BRAND_OVERRIDES.Violet.light['--color-primary']);
23
- expect(violet.light['--color-border']).toBe(base.light['--color-border']);
24
- expect(violet.light['--radius']).toBe(base.light['--radius']);
25
- });
26
-
27
- it('does not mutate the base palette', () => {
28
- const snapshot = JSON.stringify(base);
29
- buildPresets(base);
30
- expect(JSON.stringify(base)).toBe(snapshot);
31
- });
32
- });