@kitn.ai/ui 0.14.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 (370) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +394 -0
  3. package/dist/bash-InADTalH.js +1 -0
  4. package/dist/core-AYMC6_lb.js +12 -0
  5. package/dist/css-M7EaDHN_.js +1 -0
  6. package/dist/custom-elements.json +4739 -0
  7. package/dist/engine-javascript-vq0WuIJl.js +141 -0
  8. package/dist/github-dark-dimmed-DUshB20C.js +1 -0
  9. package/dist/github-light-JYsPkUQd.js +1 -0
  10. package/dist/host-embed.d.ts +33 -0
  11. package/dist/html-CPZ3oZQ7.js +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/javascript-C25yR2R2.js +1 -0
  14. package/dist/json-DxJze_jm.js +1 -0
  15. package/dist/kitn-chat-provider.d.ts +2 -0
  16. package/dist/kitn-chat-provider.es.js +240 -0
  17. package/dist/kitn-chat.es.js +77 -0
  18. package/dist/llms/llms-full.txt +1018 -0
  19. package/dist/llms/llms.txt +104 -0
  20. package/dist/origin.d.ts +5 -0
  21. package/dist/provider-runtime.d.ts +27 -0
  22. package/dist/schemas/card-envelope.schema.json +14 -0
  23. package/dist/schemas/card-event.schema.json +12 -0
  24. package/dist/schemas/choice.schema.json +70 -0
  25. package/dist/schemas/confirm.schema.json +65 -0
  26. package/dist/schemas/embed.schema.json +65 -0
  27. package/dist/schemas/form.result.schema.json +7 -0
  28. package/dist/schemas/form.schema.json +33 -0
  29. package/dist/schemas/link.schema.json +56 -0
  30. package/dist/schemas/tasks.result.schema.json +16 -0
  31. package/dist/schemas/tasks.schema.json +78 -0
  32. package/dist/svelte--5p79yCD.js +1 -0
  33. package/dist/theme.tokens.css +176 -0
  34. package/dist/tsx-B8rCNbgL.js +1 -0
  35. package/dist/typescript-RycA9KXf.js +1 -0
  36. package/dist/validate.d.ts +4 -0
  37. package/dist/version.d.ts +4 -0
  38. package/dist/vue-BmIZj4XD.js +1 -0
  39. package/dist/wire.d.ts +53 -0
  40. package/frameworks/react/index.tsx +948 -0
  41. package/frameworks/react/runtime.tsx +94 -0
  42. package/llms-full.txt +1018 -0
  43. package/llms.txt +104 -0
  44. package/package.json +141 -0
  45. package/src/components/artifact.stories.tsx +138 -0
  46. package/src/components/artifact.tsx +743 -0
  47. package/src/components/attachments.stories.tsx +303 -0
  48. package/src/components/attachments.test.tsx +64 -0
  49. package/src/components/attachments.tsx +399 -0
  50. package/src/components/card-fallback.tsx +28 -0
  51. package/src/components/card-renderer.tsx +52 -0
  52. package/src/components/card.tsx +110 -0
  53. package/src/components/chain-of-thought.stories.tsx +211 -0
  54. package/src/components/chain-of-thought.tsx +139 -0
  55. package/src/components/chat-container.stories.tsx +187 -0
  56. package/src/components/chat-container.tsx +82 -0
  57. package/src/components/chat-scope-picker.tsx +47 -0
  58. package/src/components/chat-thread.test.tsx +58 -0
  59. package/src/components/chat-thread.tsx +249 -0
  60. package/src/components/checkpoint.stories.tsx +102 -0
  61. package/src/components/checkpoint.tsx +88 -0
  62. package/src/components/choice-card.tsx +569 -0
  63. package/src/components/code-block.stories.tsx +150 -0
  64. package/src/components/code-block.tsx +105 -0
  65. package/src/components/component-meta.json +3735 -0
  66. package/src/components/confirm-card.stories.tsx +74 -0
  67. package/src/components/confirm-card.tsx +315 -0
  68. package/src/components/context.stories.tsx +229 -0
  69. package/src/components/context.test.tsx +133 -0
  70. package/src/components/context.tsx +363 -0
  71. package/src/components/conversation-item.stories.tsx +125 -0
  72. package/src/components/conversation-item.tsx +16 -0
  73. package/src/components/conversation-list.stories.tsx +133 -0
  74. package/src/components/conversation-list.tsx +101 -0
  75. package/src/components/default-input.test.tsx +111 -0
  76. package/src/components/embed.tsx +196 -0
  77. package/src/components/empty.stories.tsx +434 -0
  78. package/src/components/empty.tsx +166 -0
  79. package/src/components/feedback-bar.stories.tsx +138 -0
  80. package/src/components/feedback-bar.test.tsx +103 -0
  81. package/src/components/feedback-bar.tsx +166 -0
  82. package/src/components/file-tree.stories.tsx +73 -0
  83. package/src/components/file-tree.tsx +383 -0
  84. package/src/components/file-upload.stories.tsx +156 -0
  85. package/src/components/file-upload.tsx +161 -0
  86. package/src/components/form-widgets.tsx +461 -0
  87. package/src/components/form.tsx +873 -0
  88. package/src/components/image.stories.tsx +89 -0
  89. package/src/components/image.tsx +67 -0
  90. package/src/components/link-preview.tsx +194 -0
  91. package/src/components/loader.stories.tsx +181 -0
  92. package/src/components/loader.tsx +333 -0
  93. package/src/components/markdown.stories.tsx +180 -0
  94. package/src/components/markdown.tsx +81 -0
  95. package/src/components/message-action-bar.test.tsx +128 -0
  96. package/src/components/message-narrow.stories.tsx +329 -0
  97. package/src/components/message-skills.stories.tsx +211 -0
  98. package/src/components/message-skills.tsx +36 -0
  99. package/src/components/message.stories.tsx +281 -0
  100. package/src/components/message.tsx +227 -0
  101. package/src/components/model-switcher.stories.tsx +123 -0
  102. package/src/components/model-switcher.test.tsx +65 -0
  103. package/src/components/model-switcher.tsx +87 -0
  104. package/src/components/prompt-input.stories.tsx +222 -0
  105. package/src/components/prompt-input.tsx +211 -0
  106. package/src/components/prompt-suggestion.stories.tsx +142 -0
  107. package/src/components/prompt-suggestion.tsx +115 -0
  108. package/src/components/reasoning.stories.tsx +140 -0
  109. package/src/components/reasoning.tsx +156 -0
  110. package/src/components/resizable.test.tsx +226 -0
  111. package/src/components/response-stream.tsx +103 -0
  112. package/src/components/scroll-button.stories.tsx +100 -0
  113. package/src/components/scroll-button.tsx +34 -0
  114. package/src/components/slash-command.stories.tsx +163 -0
  115. package/src/components/slash-command.tsx +232 -0
  116. package/src/components/source-list.test.tsx +152 -0
  117. package/src/components/source.stories.tsx +124 -0
  118. package/src/components/source.tsx +129 -0
  119. package/src/components/tasks-card.stories.tsx +78 -0
  120. package/src/components/tasks-card.tsx +432 -0
  121. package/src/components/text-shimmer.stories.tsx +87 -0
  122. package/src/components/text-shimmer.tsx +37 -0
  123. package/src/components/thinking-bar.stories.tsx +87 -0
  124. package/src/components/thinking-bar.tsx +50 -0
  125. package/src/components/tool.stories.tsx +153 -0
  126. package/src/components/tool.tsx +184 -0
  127. package/src/components/use-card-resolution.ts +28 -0
  128. package/src/components/voice-input.stories.tsx +83 -0
  129. package/src/components/voice-input.tsx +107 -0
  130. package/src/elements/artifact.stories.tsx +506 -0
  131. package/src/elements/artifact.tsx +137 -0
  132. package/src/elements/attachments.stories.tsx +190 -0
  133. package/src/elements/attachments.tsx +136 -0
  134. package/src/elements/card.stories.tsx +113 -0
  135. package/src/elements/card.tsx +40 -0
  136. package/src/elements/cards.stories.tsx +59 -0
  137. package/src/elements/cards.tsx +97 -0
  138. package/src/elements/catalog.stories.tsx +491 -0
  139. package/src/elements/chain-of-thought.declarative.test.tsx +153 -0
  140. package/src/elements/chain-of-thought.stories.tsx +113 -0
  141. package/src/elements/chain-of-thought.tsx +92 -0
  142. package/src/elements/chat-scope-picker.stories.tsx +73 -0
  143. package/src/elements/chat-scope-picker.tsx +36 -0
  144. package/src/elements/chat-types.ts +42 -0
  145. package/src/elements/chat-workspace.stories.tsx +288 -0
  146. package/src/elements/chat-workspace.tsx +161 -0
  147. package/src/elements/chat.stories.tsx +151 -0
  148. package/src/elements/chat.tsx +80 -0
  149. package/src/elements/checkpoint.stories.tsx +72 -0
  150. package/src/elements/checkpoint.tsx +47 -0
  151. package/src/elements/choice.stories.tsx +240 -0
  152. package/src/elements/choice.tsx +48 -0
  153. package/src/elements/code-block.stories.tsx +83 -0
  154. package/src/elements/code-block.tsx +42 -0
  155. package/src/elements/compiled.css +2 -0
  156. package/src/elements/composed-shell.stories.tsx +316 -0
  157. package/src/elements/confirm-card.stories.tsx +227 -0
  158. package/src/elements/confirm-card.tsx +51 -0
  159. package/src/elements/context-meter.stories.tsx +151 -0
  160. package/src/elements/context-meter.tsx +121 -0
  161. package/src/elements/conversation-list.declarative.test.tsx +137 -0
  162. package/src/elements/conversation-list.stories.tsx +243 -0
  163. package/src/elements/conversation-list.tsx +80 -0
  164. package/src/elements/css.ts +5 -0
  165. package/src/elements/default-input.tsx +247 -0
  166. package/src/elements/define.tsx +171 -0
  167. package/src/elements/element-meta.json +3314 -0
  168. package/src/elements/element-types.d.ts +710 -0
  169. package/src/elements/embed.stories.tsx +213 -0
  170. package/src/elements/embed.tsx +35 -0
  171. package/src/elements/empty.stories.tsx +111 -0
  172. package/src/elements/empty.tsx +29 -0
  173. package/src/elements/feedback-bar.stories.tsx +114 -0
  174. package/src/elements/feedback-bar.tsx +61 -0
  175. package/src/elements/file-tree.stories.tsx +134 -0
  176. package/src/elements/file-tree.tsx +52 -0
  177. package/src/elements/file-upload.stories.tsx +82 -0
  178. package/src/elements/file-upload.tsx +44 -0
  179. package/src/elements/form.stories.tsx +248 -0
  180. package/src/elements/form.tsx +43 -0
  181. package/src/elements/framework-usage.json +563 -0
  182. package/src/elements/image.stories.tsx +71 -0
  183. package/src/elements/image.tsx +32 -0
  184. package/src/elements/link-preview.stories.tsx +209 -0
  185. package/src/elements/link-preview.tsx +34 -0
  186. package/src/elements/loader.stories.tsx +88 -0
  187. package/src/elements/loader.tsx +25 -0
  188. package/src/elements/markdown.stories.tsx +76 -0
  189. package/src/elements/markdown.tsx +38 -0
  190. package/src/elements/message-skills.declarative.test.tsx +104 -0
  191. package/src/elements/message-skills.stories.tsx +114 -0
  192. package/src/elements/message-skills.tsx +85 -0
  193. package/src/elements/message.stories.tsx +218 -0
  194. package/src/elements/message.tsx +162 -0
  195. package/src/elements/model-switcher.declarative.test.tsx +130 -0
  196. package/src/elements/model-switcher.stories.tsx +198 -0
  197. package/src/elements/model-switcher.tsx +93 -0
  198. package/src/elements/popover.stories.tsx +80 -0
  199. package/src/elements/popover.tsx +59 -0
  200. package/src/elements/prompt-input-slash-command.test.tsx +128 -0
  201. package/src/elements/prompt-input-stoppable.test.tsx +88 -0
  202. package/src/elements/prompt-input-toolbar-actions.test.tsx +120 -0
  203. package/src/elements/prompt-input.stories.tsx +331 -0
  204. package/src/elements/prompt-input.tsx +185 -0
  205. package/src/elements/prompt-suggestions.declarative.test.tsx +164 -0
  206. package/src/elements/prompt-suggestions.stories.tsx +213 -0
  207. package/src/elements/prompt-suggestions.tsx +94 -0
  208. package/src/elements/reasoning.stories.tsx +77 -0
  209. package/src/elements/reasoning.tsx +50 -0
  210. package/src/elements/register.ts +56 -0
  211. package/src/elements/remote.stories.tsx +314 -0
  212. package/src/elements/remote.tsx +221 -0
  213. package/src/elements/resizable.d.ts +27 -0
  214. package/src/elements/resizable.stories.tsx +426 -0
  215. package/src/elements/resizable.tsx +553 -0
  216. package/src/elements/response-stream.stories.tsx +79 -0
  217. package/src/elements/response-stream.tsx +40 -0
  218. package/src/elements/scroll-button.stories.tsx +202 -0
  219. package/src/elements/scroll-button.test.tsx +148 -0
  220. package/src/elements/scroll-button.tsx +126 -0
  221. package/src/elements/source-list.stories.tsx +143 -0
  222. package/src/elements/source.stories.tsx +88 -0
  223. package/src/elements/source.tsx +120 -0
  224. package/src/elements/styles.css +199 -0
  225. package/src/elements/switch.stories.tsx +56 -0
  226. package/src/elements/switch.tsx +43 -0
  227. package/src/elements/tasks.stories.tsx +237 -0
  228. package/src/elements/tasks.tsx +46 -0
  229. package/src/elements/text-shimmer.stories.tsx +64 -0
  230. package/src/elements/text-shimmer.tsx +28 -0
  231. package/src/elements/thinking-bar.stories.tsx +72 -0
  232. package/src/elements/thinking-bar.tsx +34 -0
  233. package/src/elements/tool.stories.tsx +89 -0
  234. package/src/elements/tool.tsx +23 -0
  235. package/src/elements/voice-input.stories.tsx +88 -0
  236. package/src/elements/voice-input.tsx +41 -0
  237. package/src/index.ts +253 -0
  238. package/src/primitives/card-contract.ts +71 -0
  239. package/src/primitives/card-host.tsx +35 -0
  240. package/src/primitives/card-registry.tsx +67 -0
  241. package/src/primitives/card-resolution.ts +36 -0
  242. package/src/primitives/card-routing.ts +79 -0
  243. package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
  244. package/src/primitives/card-schemas/card-event.schema.json +12 -0
  245. package/src/primitives/card-schemas/choice.schema.json +70 -0
  246. package/src/primitives/card-schemas/confirm.schema.json +65 -0
  247. package/src/primitives/card-schemas/embed.schema.json +65 -0
  248. package/src/primitives/card-schemas/form.result.schema.json +7 -0
  249. package/src/primitives/card-schemas/form.schema.json +33 -0
  250. package/src/primitives/card-schemas/link.schema.json +56 -0
  251. package/src/primitives/card-schemas/tasks.result.schema.json +16 -0
  252. package/src/primitives/card-schemas/tasks.schema.json +78 -0
  253. package/src/primitives/card-validate.ts +95 -0
  254. package/src/primitives/chat-config.tsx +76 -0
  255. package/src/primitives/controllable.test.ts +47 -0
  256. package/src/primitives/controllable.ts +21 -0
  257. package/src/primitives/embed-providers.ts +254 -0
  258. package/src/primitives/highlighter.ts +157 -0
  259. package/src/primitives/link-preview.ts +87 -0
  260. package/src/primitives/pdf-preview.ts +121 -0
  261. package/src/primitives/use-auto-resize.ts +31 -0
  262. package/src/primitives/use-resize-observer.ts +12 -0
  263. package/src/primitives/use-stick-to-bottom.ts +43 -0
  264. package/src/primitives/use-text-stream.ts +112 -0
  265. package/src/primitives/use-voice-recorder.ts +50 -0
  266. package/src/remote/host-embed.ts +345 -0
  267. package/src/remote/index.ts +2 -0
  268. package/src/remote/origin.ts +30 -0
  269. package/src/remote/provider-runtime.ts +259 -0
  270. package/src/remote/provider.ts +2 -0
  271. package/src/remote/validate.ts +22 -0
  272. package/src/remote/version.ts +12 -0
  273. package/src/remote/wire.ts +48 -0
  274. package/src/stories/chat-panel-layout.stories.tsx +145 -0
  275. package/src/stories/chat-scene.tsx +571 -0
  276. package/src/stories/checkpoint-restore.stories.tsx +256 -0
  277. package/src/stories/context-usage.stories.tsx +212 -0
  278. package/src/stories/conversation-with-reasoning.stories.tsx +182 -0
  279. package/src/stories/conversation-with-sources.stories.tsx +471 -0
  280. package/src/stories/docs/Accessibility.mdx +119 -0
  281. package/src/stories/docs/ChildElements.mdx +269 -0
  282. package/src/stories/docs/ForAIAgents.mdx +162 -0
  283. package/src/stories/docs/GettingStarted.mdx +65 -0
  284. package/src/stories/docs/Installation.mdx +83 -0
  285. package/src/stories/docs/Introduction.mdx +50 -0
  286. package/src/stories/docs/SolidJsAdvanced.mdx +17 -0
  287. package/src/stories/docs/Theming.mdx +85 -0
  288. package/src/stories/docs/element-controls.ts +121 -0
  289. package/src/stories/docs/frameworks/Angular.mdx +245 -0
  290. package/src/stories/docs/frameworks/Html.mdx +227 -0
  291. package/src/stories/docs/frameworks/Overview.mdx +47 -0
  292. package/src/stories/docs/frameworks/React.mdx +227 -0
  293. package/src/stories/docs/frameworks/Solid.mdx +173 -0
  294. package/src/stories/docs/frameworks/Svelte.mdx +194 -0
  295. package/src/stories/docs/frameworks/Vue.mdx +238 -0
  296. package/src/stories/docs/generative-ui-overview.mdx +186 -0
  297. package/src/stories/docs/recipes/SpeechToText.mdx +7 -0
  298. package/src/stories/docs/recipes/Streaming.mdx +68 -0
  299. package/src/stories/docs/recipes/TextToSpeech.mdx +35 -0
  300. package/src/stories/docs/theme-editor/canvas.tsx +32 -0
  301. package/src/stories/docs/theme-editor/inspector.tsx +66 -0
  302. package/src/stories/docs/theme-editor/presets.test.ts +32 -0
  303. package/src/stories/docs/theme-editor/presets.ts +64 -0
  304. package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
  305. package/src/stories/docs/theme-editor/theme-css.ts +15 -0
  306. package/src/stories/docs/theme-editor/theme-editor.tsx +146 -0
  307. package/src/stories/docs/theme-tokens.tsx +174 -0
  308. package/src/stories/examples/ChoosingComponents.mdx +106 -0
  309. package/src/stories/examples/sample-data.ts +79 -0
  310. package/src/stories/examples/usage/checkpoint-restore.ts +156 -0
  311. package/src/stories/examples/usage/context-usage.ts +591 -0
  312. package/src/stories/examples/usage/conversation-with-reasoning.ts +224 -0
  313. package/src/stories/examples/usage/conversation-with-sources.ts +589 -0
  314. package/src/stories/examples/usage/empty-state.ts +144 -0
  315. package/src/stories/examples/usage/full-chat-app.ts +277 -0
  316. package/src/stories/examples/usage/index.ts +63 -0
  317. package/src/stories/examples/usage/message-actions.ts +704 -0
  318. package/src/stories/examples/usage/pattern-centered-conversation.ts +446 -0
  319. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +442 -0
  320. package/src/stories/examples/usage/pattern-docked-widget.ts +479 -0
  321. package/src/stories/examples/usage/prompt-input-variants.ts +1356 -0
  322. package/src/stories/examples/usage/streaming-response.ts +687 -0
  323. package/src/stories/examples/usage/types.ts +33 -0
  324. package/src/stories/full-chat.stories.tsx +18 -0
  325. package/src/stories/message-actions.stories.tsx +230 -0
  326. package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
  327. package/src/stories/pattern-docked-widget.stories.tsx +93 -0
  328. package/src/stories/pattern-empty-state.stories.tsx +76 -0
  329. package/src/stories/prompt-input-variants.stories.tsx +456 -0
  330. package/src/stories/streaming-response.stories.tsx +438 -0
  331. package/src/stories/theme-editor.stories.tsx +16 -0
  332. package/src/stories/token-reference.stories.tsx +18 -0
  333. package/src/stories/typography.stories.tsx +78 -0
  334. package/src/types.ts +47 -0
  335. package/src/ui/action-icons.ts +52 -0
  336. package/src/ui/avatar.stories.tsx +103 -0
  337. package/src/ui/avatar.tsx +23 -0
  338. package/src/ui/badge.stories.tsx +86 -0
  339. package/src/ui/badge.tsx +21 -0
  340. package/src/ui/button.stories.tsx +145 -0
  341. package/src/ui/button.tsx +38 -0
  342. package/src/ui/collapsible.stories.tsx +69 -0
  343. package/src/ui/collapsible.tsx +125 -0
  344. package/src/ui/dropdown.stories.tsx +59 -0
  345. package/src/ui/dropdown.tsx +196 -0
  346. package/src/ui/hover-card.stories.tsx +77 -0
  347. package/src/ui/hover-card.test.tsx +30 -0
  348. package/src/ui/hover-card.tsx +172 -0
  349. package/src/ui/overlay.stories.tsx +115 -0
  350. package/src/ui/overlay.tsx +158 -0
  351. package/src/ui/popover.stories.tsx +81 -0
  352. package/src/ui/popover.test.tsx +99 -0
  353. package/src/ui/popover.tsx +92 -0
  354. package/src/ui/resizable.stories.tsx +236 -0
  355. package/src/ui/resizable.tsx +576 -0
  356. package/src/ui/scroll-area.stories.tsx +50 -0
  357. package/src/ui/scroll-area.tsx +15 -0
  358. package/src/ui/separator.stories.tsx +81 -0
  359. package/src/ui/separator.tsx +10 -0
  360. package/src/ui/skeleton.stories.tsx +337 -0
  361. package/src/ui/skeleton.tsx +16 -0
  362. package/src/ui/switch.stories.tsx +51 -0
  363. package/src/ui/switch.test.tsx +59 -0
  364. package/src/ui/switch.tsx +62 -0
  365. package/src/ui/textarea.stories.tsx +76 -0
  366. package/src/ui/textarea.tsx +21 -0
  367. package/src/ui/tooltip.stories.tsx +74 -0
  368. package/src/ui/tooltip.tsx +68 -0
  369. package/src/utils/cn.ts +24 -0
  370. package/theme.css +187 -0
@@ -0,0 +1,227 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal, onMount, type JSX } from 'solid-js';
3
+ import './confirm-card';
4
+ import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
+ import type { ConfirmCardData } from '../components/confirm-card';
6
+ import type { CardEvent } from '../primitives/card-contract';
7
+
8
+ declare module 'solid-js' {
9
+ // eslint-disable-next-line @typescript-eslint/no-namespace
10
+ namespace JSX {
11
+ interface IntrinsicElements {
12
+ 'kai-confirm': JSX.HTMLAttributes<HTMLElement> & {
13
+ heading?: string;
14
+ 'card-id'?: string;
15
+ ref?: (el: HTMLElement) => void;
16
+ };
17
+ }
18
+ }
19
+ }
20
+
21
+ type ConfirmEl = HTMLElement & { data?: ConfirmCardData; resolution?: Record<string, unknown> };
22
+
23
+ function Frame(props: { children: JSX.Element }) {
24
+ return <div style={{ 'max-width': '460px' }}>{props.children}</div>;
25
+ }
26
+
27
+ /** Mounts a <kai-confirm>, sets `.data`, logs the emitted CardEvent under the render. */
28
+ function ConfirmDemo(props: { def: ConfirmCardData; cardId: string; heading?: string }) {
29
+ const [log, setLog] = createSignal<CardEvent[]>([]);
30
+ let el: ConfirmEl | undefined;
31
+ onMount(() => {
32
+ if (!el) return;
33
+ el.data = props.def;
34
+ el.addEventListener('kai-card', (e) => {
35
+ const detail = (e as CustomEvent<CardEvent>).detail;
36
+ setLog((prev) => [...prev, detail]);
37
+ });
38
+ });
39
+ return (
40
+ <Frame>
41
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '12px' }}>
42
+ <kai-confirm ref={(e) => (el = e as ConfirmEl)} card-id={props.cardId} heading={props.heading} />
43
+ <pre
44
+ style={{
45
+ margin: 0,
46
+ 'max-height': '180px',
47
+ overflow: 'auto',
48
+ background: 'var(--color-muted, #f4f4f5)',
49
+ 'border-radius': '8px',
50
+ padding: '8px',
51
+ 'font-size': '12px',
52
+ }}
53
+ >
54
+ {log().length === 0 ? '// emitted CardEvents appear here' : JSON.stringify(log(), null, 2)}
55
+ </pre>
56
+ </div>
57
+ </Frame>
58
+ );
59
+ }
60
+
61
+ const APPROVE: ConfirmCardData = {
62
+ body: 'This will apply 3 pending migrations to production. This cannot be undone.',
63
+ tone: 'warning',
64
+ actions: [
65
+ { id: 'approve', label: 'Run migration', style: 'primary', default: true },
66
+ { id: 'reject', label: 'Cancel' },
67
+ ],
68
+ };
69
+
70
+ const DESTRUCTIVE: ConfirmCardData = {
71
+ body: 'Permanently delete 12 files from the workspace? This cannot be undone.',
72
+ tone: 'danger',
73
+ actions: [
74
+ { id: 'delete', label: 'Delete files', style: 'destructive', default: true },
75
+ { id: 'cancel', label: 'Keep them' },
76
+ ],
77
+ };
78
+
79
+ const CHOICES: ConfirmCardData = {
80
+ heading: 'Where should I deploy?',
81
+ body: 'Pick a target environment for this build.',
82
+ actions: [
83
+ { id: 'staging', label: 'Staging', default: true },
84
+ { id: 'preview', label: 'Preview' },
85
+ { id: 'prod', label: 'Production', style: 'primary' },
86
+ ],
87
+ };
88
+
89
+ const DISMISSIBLE: ConfirmCardData = {
90
+ body: 'Send the drafted email to the customer?',
91
+ dismissible: true,
92
+ actions: [
93
+ { id: 'send', label: 'Send', style: 'primary', default: true },
94
+ { id: 'edit', label: 'Edit first' },
95
+ ],
96
+ };
97
+
98
+ const HEADING_MAP: Record<string, string | undefined> = {
99
+ 'card-approve': 'Run database migration?',
100
+ 'card-delete': 'Delete files?',
101
+ 'card-deploy': undefined,
102
+ 'card-send': 'Send email?',
103
+ };
104
+
105
+ const HTML_SNIPPET = (def: ConfirmCardData, cardId: string) => {
106
+ const heading = HEADING_MAP[cardId];
107
+ return `<kai-confirm${heading ? ` heading="${heading}"` : ''}></kai-confirm>
108
+ <script type="module">
109
+ import '@kitn.ai/ui/elements'; // registers the custom elements
110
+
111
+ const el = document.querySelector('kai-confirm');
112
+ // \`data\` is the CardEnvelope.data (set as a property).
113
+ el.data = ${JSON.stringify(def, null, 2)};
114
+
115
+ // Cards bubble ONE \`kai-card\` CustomEvent carrying a typed CardEvent.
116
+ el.addEventListener('kai-card', (e) => {
117
+ const ev = e.detail; // { kind:'action', cardId, action, payload? } | { kind:'dismiss', ... } | ...
118
+ if (ev.kind === 'action') console.log('chose', ev.action);
119
+ });
120
+ </script>`;
121
+ };
122
+
123
+ const meta = {
124
+ title: 'Generative UI/Cards/kai-confirm',
125
+ tags: ['autodocs'],
126
+ argTypes: argTypesFor('kai-confirm'),
127
+ parameters: {
128
+ layout: 'padded',
129
+ docs: {
130
+ description: specDescription('kai-confirm', [
131
+ "`<kai-confirm>` is a **named-intent approval** card (set via the `data` **property**): a title + body + a small set of action buttons. Activating an action emits the Card contract's **`action`** verb up a bubbling **`kai-card`** CustomEvent of `{ kind: 'action', cardId, action, payload? }`, then **resolves** the card (other actions disabled, the chosen one marked) so the same approval can't double-fire.",
132
+ '**Action styles:** `primary` (filled accent), `default` (outline), `destructive` (red/danger). A `tone:\'danger\'` and any destructive action add a warning icon + danger accent — never color alone. At most one action can be `default:true` (the keyboard default; gets focus only when `autofocus` is set, which is **off** by default to avoid focus-stealing mid-stream).',
133
+ '**Events** (all frozen Card-contract verbs): `ready` on mount, `action` on choice, `dismiss` for the optional close affordance (`dismissible:true`), `error` for a malformed definition (renders the inline `kai-card` error). It **never invents events**. The same shapes flow over the remote iframe transport unchanged.',
134
+ ]),
135
+ },
136
+ },
137
+ } satisfies Meta;
138
+
139
+ export default meta;
140
+ type Story = StoryObj;
141
+
142
+ /** The canonical two-action approval (Approve / Reject). */
143
+ export const ApproveReject: Story = {
144
+ render: () => <ConfirmDemo def={APPROVE} cardId="card-approve" heading="Run database migration?" />,
145
+ parameters: { docs: { source: { code: HTML_SNIPPET(APPROVE, 'card-approve'), language: 'html' } } },
146
+ };
147
+
148
+ /** A destructive action (`tone:'danger'` + `style:'destructive'`) — danger styling, no color-only cue. */
149
+ export const Destructive: Story = {
150
+ render: () => <ConfirmDemo def={DESTRUCTIVE} cardId="card-delete" heading="Delete files?" />,
151
+ parameters: { docs: { source: { code: HTML_SNIPPET(DESTRUCTIVE, 'card-delete'), language: 'html' } } },
152
+ };
153
+
154
+ /** A small choice set (3 actions, one `default:true`). */
155
+ export const ChoiceSet: Story = {
156
+ render: () => <ConfirmDemo def={CHOICES} cardId="card-deploy" />,
157
+ parameters: { docs: { source: { code: HTML_SNIPPET(CHOICES, 'card-deploy'), language: 'html' } } },
158
+ };
159
+
160
+ /** A dismissible approval (a close affordance emits the `dismiss` verb). */
161
+ export const Dismissible: Story = {
162
+ render: () => <ConfirmDemo def={DISMISSIBLE} cardId="card-send" heading="Send email?" />,
163
+ parameters: { docs: { source: { code: HTML_SNIPPET(DISMISSIBLE, 'card-send'), language: 'html' } } },
164
+ };
165
+
166
+ const RESOLVED_CONFIRM: ConfirmCardData = {
167
+ body: 'Apply 3 migrations?',
168
+ actions: [
169
+ { id: 'approve', label: 'Run migration', style: 'primary' },
170
+ { id: 'reject', label: 'Cancel' },
171
+ ],
172
+ };
173
+
174
+ /** The card after the user chose **Run migration** — chromed read-only, no buttons. */
175
+ export const Resolved: Story = {
176
+ name: 'Resolved (read-only)',
177
+ render: () => {
178
+ let el: ConfirmEl | undefined;
179
+ onMount(() => {
180
+ if (!el) return;
181
+ el.data = RESOLVED_CONFIRM;
182
+ el.resolution = { kind: 'action', action: 'approve' };
183
+ });
184
+ return (
185
+ <Frame>
186
+ <kai-confirm ref={(e) => (el = e as ConfirmEl)} card-id="card-resolved-confirm" heading="Run database migration?" />
187
+ </Frame>
188
+ );
189
+ },
190
+ parameters: {
191
+ docs: {
192
+ source: {
193
+ code: `<kai-confirm heading="Run database migration?"></kai-confirm>
194
+ <script type="module">
195
+ import '@kitn.ai/ui/elements';
196
+ const el = document.querySelector('kai-confirm');
197
+ el.data = ${JSON.stringify(RESOLVED_CONFIRM, null, 2)};
198
+ // Setting .resolution renders the chromed read-only view — no interactive controls.
199
+ el.resolution = { kind: 'action', action: 'approve' };
200
+ </script>`,
201
+ language: 'html',
202
+ },
203
+ },
204
+ },
205
+ };
206
+
207
+ /** A malformed `data` (empty `actions`) → the inline error state + an `error` event. */
208
+ export const ErrorState: Story = {
209
+ render: () => <ConfirmDemo def={{ actions: [] } as unknown as ConfirmCardData} cardId="card-bad" />,
210
+ parameters: {
211
+ docs: {
212
+ source: {
213
+ code: `<kai-confirm></kai-confirm>
214
+ <script type="module">
215
+ import '@kitn.ai/ui/elements';
216
+ const el = document.querySelector('kai-confirm');
217
+ // No actions → inline error state + an \`error\` event.
218
+ el.data = { actions: [] };
219
+ el.addEventListener('kai-card', (e) => {
220
+ if (e.detail.kind === 'error') console.warn('confirm error:', e.detail.message);
221
+ });
222
+ </script>`,
223
+ language: 'html',
224
+ },
225
+ },
226
+ },
227
+ };
@@ -0,0 +1,51 @@
1
+ import { defineWebComponent } from './define';
2
+ import { ConfirmCard, type ConfirmCardData } from '../components/confirm-card';
3
+ import type { CardResolution } from '../primitives/card-contract';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The confirm definition (the CardEnvelope.data). Set as a JS PROPERTY:
7
+ * `el.data = { body, tone, actions:[…] }`. Import `ConfirmCardData` from
8
+ * `@kitn.ai/ui` for the full shape. */
9
+ data?: Record<string, unknown>;
10
+ /** Stable card id correlating every emitted CardEvent. Attribute: `card-id`. */
11
+ cardId?: string;
12
+ /** Heading rendered in the card chrome (= CardEnvelope.title). Attribute: `heading`. */
13
+ heading?: string;
14
+ /** Focus the default action on mount (off by default — no focus-stealing).
15
+ * Attribute: `autofocus`. */
16
+ autofocus?: boolean;
17
+ /** Set when the user resolved this card; renders the read-only view. Property:
18
+ * `el.resolution = { kind:'action', action:'…' }`. */
19
+ resolution?: Record<string, unknown>;
20
+ }
21
+
22
+ /**
23
+ * `<kai-confirm>` — a named-intent **approval** card (set via the `data` property):
24
+ * a title + body + a small set of action buttons. Activating an action emits the
25
+ * Card contract's **`action`** verb up a bubbling **`kai-card`** CustomEvent
26
+ * (`{ kind:'action', cardId, action, payload }`) and resolves the card so the same
27
+ * approval can't double-fire. Also emits `ready` on mount, `dismiss` for the
28
+ * optional close affordance, and `error` for a malformed definition (inline error).
29
+ * Routes through a `CardProvider` when present, else the bubbling `kai-card` event.
30
+ * Isolated in Shadow DOM; theme-aware via the shared kit tokens.
31
+ */
32
+ defineWebComponent<Props>(
33
+ 'kai-confirm',
34
+ {
35
+ data: undefined,
36
+ cardId: undefined,
37
+ heading: undefined,
38
+ autofocus: false,
39
+ resolution: undefined,
40
+ },
41
+ (props, { element, flag }) => (
42
+ <ConfirmCard
43
+ data={props.data as ConfirmCardData | undefined}
44
+ cardId={props.cardId ?? (element.id || 'kai-confirm')}
45
+ heading={props.heading}
46
+ autofocus={flag('autofocus')}
47
+ resolution={props.resolution as CardResolution | undefined}
48
+ hostElement={element}
49
+ />
50
+ ),
51
+ );
@@ -0,0 +1,151 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { onMount } from 'solid-js';
3
+ import './register'; // side effect: registers the custom elements
4
+ import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
+
6
+ interface ContextUsage {
7
+ usedTokens: number;
8
+ maxTokens: number;
9
+ inputTokens?: number;
10
+ outputTokens?: number;
11
+ reasoningTokens?: number;
12
+ cacheTokens?: number;
13
+ estimatedCost?: number;
14
+ }
15
+
16
+ // The web components are custom DOM elements, so declare the tags for JSX.
17
+ declare module 'solid-js' {
18
+ // eslint-disable-next-line @typescript-eslint/no-namespace
19
+ namespace JSX {
20
+ interface IntrinsicElements {
21
+ 'kai-context': JSX.HTMLAttributes<HTMLElement>;
22
+ }
23
+ }
24
+ }
25
+
26
+ const usage: ContextUsage = {
27
+ usedTokens: 48200,
28
+ maxTokens: 200000,
29
+ inputTokens: 31000,
30
+ outputTokens: 17200,
31
+ reasoningTokens: 4200,
32
+ cacheTokens: 8000,
33
+ estimatedCost: 0.42,
34
+ };
35
+
36
+ interface MeterElementProps {
37
+ context: ContextUsage;
38
+ warnThreshold?: number;
39
+ dangerThreshold?: number;
40
+ onThresholdChange?: (level: string) => void;
41
+ }
42
+
43
+ /** Render `<kai-context>` with the `context` set as a JS property. */
44
+ function MeterElement(props: MeterElementProps) {
45
+ let el: (HTMLElement & { context?: ContextUsage; warnThreshold?: number; dangerThreshold?: number }) | undefined;
46
+ onMount(() => {
47
+ if (!el) return;
48
+ el.context = props.context;
49
+ if (props.warnThreshold !== undefined) el.warnThreshold = props.warnThreshold;
50
+ if (props.dangerThreshold !== undefined) el.dangerThreshold = props.dangerThreshold;
51
+ if (props.onThresholdChange) {
52
+ el.addEventListener('kai-threshold-change', (e) => {
53
+ props.onThresholdChange!((e as CustomEvent<{ level: string }>).detail.level);
54
+ });
55
+ }
56
+ });
57
+ return (
58
+ <kai-context ref={(e) => (el = e as HTMLElement)} style={{ display: 'inline-block', padding: '40px' }} />
59
+ );
60
+ }
61
+
62
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
63
+ <kai-context id="ctx"></kai-context>
64
+
65
+ <script type="module">
66
+ import '@kitn.ai/ui/elements'; // registers the custom elements
67
+
68
+ document.getElementById('ctx').context = {
69
+ usedTokens: 48200, maxTokens: 200000,
70
+ inputTokens: 31000, outputTokens: 17200,
71
+ reasoningTokens: 4200, cacheTokens: 8000,
72
+ estimatedCost: 0.42,
73
+ };
74
+ </script>`;
75
+
76
+ const CUSTOM_THRESHOLDS_SNIPPET = `<!-- Custom thresholds: warn at 50%, danger at 75% -->
77
+ <kai-context id="ctx"></kai-context>
78
+
79
+ <script type="module">
80
+ import '@kitn.ai/ui/elements';
81
+
82
+ const el = document.getElementById('ctx');
83
+ el.context = {
84
+ usedTokens: 110000, // 55% of 200 000 -> warn (> 50%)
85
+ maxTokens: 200000,
86
+ inputTokens: 70000,
87
+ outputTokens: 40000,
88
+ estimatedCost: 0.65,
89
+ };
90
+
91
+ // Lower the thresholds so the indicator warns earlier.
92
+ el.warnThreshold = 0.5; // default 0.7
93
+ el.dangerThreshold = 0.75; // default 0.9
94
+
95
+ // Listen for severity changes.
96
+ el.addEventListener('kai-threshold-change', (e) => {
97
+ console.log('severity changed to', e.detail.level); // 'ok' | 'warn' | 'danger'
98
+ });
99
+ </script>`;
100
+
101
+ const meta = {
102
+ title: 'Components/Context',
103
+ tags: ['autodocs'],
104
+ argTypes: argTypesFor('kai-context'),
105
+ parameters: {
106
+ layout: 'fullscreen',
107
+ docs: {
108
+ description: specDescription('kai-context', [
109
+ '`<kai-context>` is the framework-agnostic **web component** for a token/context-window usage meter — a compact gauge with a hover-card breakdown (input / output / reasoning / cache + estimated cost) — isolated in **Shadow DOM**.',
110
+ '**When to use:** showing how much of the context window a conversation is using, typically in a chat header. In SolidJS, compose the `Context` primitives.',
111
+ '**Placement:** inline in the chat header or toolbar, beside the model switcher or other header controls; it is a compact `inline-block` element and does not need a dedicated row.',
112
+ "**How to use:** register once with `import '@kitn.ai/ui/elements'`, then set the `context` **property** with the usage object. Hover the meter to reveal the breakdown.",
113
+ '**Color thresholds** are configurable via `warnThreshold` (default `0.7`) and `dangerThreshold` (default `0.9`) number properties. When the computed severity level changes, the element fires a **`kai-threshold-change`** event with `detail.level` set to `\'ok\'`, `\'warn\'`, or `\'danger\'`.',
114
+ 'See the **Code** tab for HTML usage.',
115
+ ]),
116
+ },
117
+ },
118
+ } satisfies Meta;
119
+
120
+ export default meta;
121
+ type Story = StoryObj;
122
+
123
+ /** A meter at ~24% usage; hover to reveal the full breakdown. */
124
+ export const Default: Story = {
125
+ render: () => <MeterElement context={usage} />,
126
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
127
+ };
128
+
129
+ /**
130
+ * Custom thresholds — `warnThreshold=0.5` and `dangerThreshold=0.75` so the
131
+ * meter turns yellow at 50% and red at 75%. At ~55% (110 000 / 200 000) the
132
+ * meter renders yellow. Open the browser console to see `kai-threshold-change`
133
+ * events as the property changes.
134
+ */
135
+ export const CustomThresholds: Story = {
136
+ render: () => (
137
+ <MeterElement
138
+ context={{
139
+ usedTokens: 110000,
140
+ maxTokens: 200000,
141
+ inputTokens: 70000,
142
+ outputTokens: 40000,
143
+ estimatedCost: 0.65,
144
+ }}
145
+ warnThreshold={0.5}
146
+ dangerThreshold={0.75}
147
+ onThresholdChange={(level) => console.log('kai-threshold-change', { level })}
148
+ />
149
+ ),
150
+ parameters: { docs: { source: { code: CUSTOM_THRESHOLDS_SNIPPET, language: 'html' } } },
151
+ };
@@ -0,0 +1,121 @@
1
+ import { Show, createEffect, createMemo, on } from 'solid-js';
2
+ import { defineWebComponent } from './define';
3
+ import {
4
+ Context,
5
+ ContextTrigger,
6
+ ContextContent,
7
+ ContextContentHeader,
8
+ ContextContentBody,
9
+ ContextContentFooter,
10
+ ContextInputUsage,
11
+ ContextOutputUsage,
12
+ ContextReasoningUsage,
13
+ ContextCacheUsage,
14
+ computeSeverity,
15
+ DEFAULT_WARN_THRESHOLD,
16
+ DEFAULT_DANGER_THRESHOLD,
17
+ type ContextSeverity,
18
+ } from '../components/context';
19
+
20
+ interface ContextUsage {
21
+ /** Tokens consumed so far in the context window (drives the meter fill). */
22
+ usedTokens: number;
23
+ /** The model's total context-window size in tokens. */
24
+ maxTokens: number;
25
+ /** Tokens attributed to the prompt/input, shown in the breakdown. */
26
+ inputTokens?: number;
27
+ /** Tokens attributed to the generated output, shown in the breakdown. */
28
+ outputTokens?: number;
29
+ /** Tokens attributed to reasoning/thinking, shown when present. */
30
+ reasoningTokens?: number;
31
+ /** Tokens served from prompt cache, shown when present. */
32
+ cacheTokens?: number;
33
+ /** Estimated cost in dollars for this usage, shown in the footer. */
34
+ estimatedCost?: number;
35
+ }
36
+
37
+ interface Props extends Record<string, unknown> {
38
+ /** Token-usage data. Set as a JS property. */
39
+ context?: ContextUsage;
40
+ /**
41
+ * Fraction (0–1) above which the meter turns yellow.
42
+ * Defaults to `0.7` (70%).
43
+ */
44
+ warnThreshold?: number;
45
+ /**
46
+ * Fraction (0–1) above which the meter turns red.
47
+ * Defaults to `0.9` (90%).
48
+ */
49
+ dangerThreshold?: number;
50
+ }
51
+
52
+ /** Events fired by `<kai-context>`. */
53
+ interface Events {
54
+ /**
55
+ * Fires when the computed severity level changes (ok → warn → danger or back).
56
+ * `detail.level` is `'ok'`, `'warn'`, or `'danger'`.
57
+ */
58
+ 'kai-threshold-change': { level: ContextSeverity };
59
+ }
60
+
61
+ /**
62
+ * `<kai-context>` — a token/context-window usage meter with a hover-card
63
+ * breakdown (input/output/reasoning/cache + estimated cost). Data via the
64
+ * `context` property.
65
+ *
66
+ * **Color thresholds** are configurable via `warnThreshold` (default `0.7`)
67
+ * and `dangerThreshold` (default `0.9`). When the computed severity level
68
+ * changes, a `kai-threshold-change` event fires with `detail.level` set to
69
+ * `'ok'`, `'warn'`, or `'danger'`.
70
+ */
71
+ defineWebComponent<Props, Events>('kai-context', {
72
+ context: undefined,
73
+ warnThreshold: undefined,
74
+ dangerThreshold: undefined,
75
+ }, (props, { dispatch }) => {
76
+ const warn = () => (props.warnThreshold as number | undefined) ?? DEFAULT_WARN_THRESHOLD;
77
+ const danger = () => (props.dangerThreshold as number | undefined) ?? DEFAULT_DANGER_THRESHOLD;
78
+
79
+ const severity = createMemo<ContextSeverity>(() => {
80
+ const ctx = props.context;
81
+ if (!ctx) return 'ok';
82
+ return computeSeverity(ctx.usedTokens / ctx.maxTokens, warn(), danger());
83
+ });
84
+
85
+ // Fire kai-threshold-change only when the level actually changes.
86
+ createEffect(on(severity, (level, prev) => {
87
+ if (prev !== undefined && level !== prev) {
88
+ dispatch('kai-threshold-change', { level });
89
+ }
90
+ }));
91
+
92
+ return (
93
+ <Show when={props.context}>
94
+ <Context
95
+ usedTokens={props.context!.usedTokens}
96
+ maxTokens={props.context!.maxTokens}
97
+ inputTokens={props.context!.inputTokens}
98
+ outputTokens={props.context!.outputTokens}
99
+ reasoningTokens={props.context!.reasoningTokens}
100
+ cacheTokens={props.context!.cacheTokens}
101
+ estimatedCost={props.context!.estimatedCost}
102
+ warnThreshold={warn()}
103
+ dangerThreshold={danger()}
104
+ >
105
+ <ContextTrigger />
106
+ <ContextContent>
107
+ <ContextContentHeader />
108
+ <ContextContentBody>
109
+ <div class="space-y-1.5">
110
+ <ContextInputUsage />
111
+ <ContextOutputUsage />
112
+ <Show when={props.context!.reasoningTokens}><ContextReasoningUsage /></Show>
113
+ <Show when={props.context!.cacheTokens}><ContextCacheUsage /></Show>
114
+ </div>
115
+ </ContextContentBody>
116
+ <ContextContentFooter />
117
+ </ContextContent>
118
+ </Context>
119
+ </Show>
120
+ );
121
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Unit tests for the declarative `<kai-conversation>` light-DOM API of
3
+ * `<kai-conversations>`.
4
+ *
5
+ * Strategy: `defineWebComponent` registers a real Shadow-DOM custom element
6
+ * and is not suitable for jsdom unit tests. Instead:
7
+ * 1. Test the exported `parseKaiConversationElement` helper in isolation.
8
+ * 2. Test that the merged list of prop + slotted items renders items correctly
9
+ * via a minimal ConvList harness, mirroring the pattern in
10
+ * `source-list.test.tsx`.
11
+ */
12
+ import { describe, it, expect, vi, afterEach } from 'vitest';
13
+ import '@testing-library/jest-dom/vitest';
14
+ import { render, cleanup, fireEvent } from '@solidjs/testing-library';
15
+ import { For } from 'solid-js';
16
+ import { parseKaiConversationElement } from './conversation-list';
17
+ import type { ConversationSummary } from '../types';
18
+
19
+ afterEach(cleanup);
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // parseKaiConversationElement — pure helper
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('parseKaiConversationElement', () => {
26
+ function makeEl(attrs: Record<string, string | null>, textContent = ''): Element {
27
+ const el = document.createElement('kai-conversation');
28
+ for (const [k, v] of Object.entries(attrs)) {
29
+ if (v !== null) el.setAttribute(k, v);
30
+ }
31
+ el.textContent = textContent;
32
+ return el;
33
+ }
34
+
35
+ it('maps id attribute to ConversationSummary.id', () => {
36
+ const el = makeEl({ id: 'c-1' }, 'Q2 plan');
37
+ expect(parseKaiConversationElement(el).id).toBe('c-1');
38
+ });
39
+
40
+ it('uses trimmed textContent as title', () => {
41
+ const el = makeEl({ id: 'c-1' }, ' Q2 plan ');
42
+ expect(parseKaiConversationElement(el).title).toBe('Q2 plan');
43
+ });
44
+
45
+ it('reads group-id attribute as groupId when present', () => {
46
+ const el = makeEl({ id: 'c-1', 'group-id': 'g-work' }, 'Q2 plan');
47
+ expect(parseKaiConversationElement(el).groupId).toBe('g-work');
48
+ });
49
+
50
+ it('sets groupId to undefined when group-id attribute is absent', () => {
51
+ const el = makeEl({ id: 'c-1' }, 'Q2 plan');
52
+ expect(parseKaiConversationElement(el).groupId).toBeUndefined();
53
+ });
54
+
55
+ it('falls back to empty string for missing id attribute', () => {
56
+ const el = makeEl({}, 'Untitled');
57
+ expect(parseKaiConversationElement(el).id).toBe('');
58
+ });
59
+
60
+ it('provides safe defaults for scope, messageCount, lastMessageAt, updatedAt', () => {
61
+ const el = makeEl({ id: 'c-1' }, 'Q2 plan');
62
+ const item = parseKaiConversationElement(el);
63
+ expect(item.scope).toEqual({ type: 'collection' });
64
+ expect(item.messageCount).toBe(0);
65
+ expect(typeof item.lastMessageAt).toBe('string');
66
+ expect(typeof item.updatedAt).toBe('string');
67
+ });
68
+ });
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Merge: prop conversations + slotted conversations combine correctly
72
+ // ---------------------------------------------------------------------------
73
+
74
+ describe('conversation merge order', () => {
75
+ /** Minimal render harness that outputs a button per conversation. */
76
+ function ConvList(props: { items: ConversationSummary[]; onSelect: (id: string) => void }) {
77
+ return (
78
+ <div>
79
+ <For each={props.items}>
80
+ {(c) => (
81
+ <button type="button" onClick={() => props.onSelect(c.id)}>
82
+ {c.title}
83
+ </button>
84
+ )}
85
+ </For>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ it('renders one item per conversation', () => {
91
+ const items: ConversationSummary[] = [
92
+ { id: 'c-1', title: 'First', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: '', updatedAt: '' },
93
+ { id: 'c-2', title: 'Second', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: '', updatedAt: '' },
94
+ ];
95
+ const { getByText } = render(() => <ConvList items={items} onSelect={() => {}} />);
96
+ expect(getByText('First')).toBeInTheDocument();
97
+ expect(getByText('Second')).toBeInTheDocument();
98
+ });
99
+
100
+ it('fires onSelect with the conversation id when an item is clicked', () => {
101
+ const onSelect = vi.fn();
102
+ const items: ConversationSummary[] = [
103
+ { id: 'c-42', title: 'API plan', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: '', updatedAt: '' },
104
+ ];
105
+ const { getByText } = render(() => <ConvList items={items} onSelect={onSelect} />);
106
+ fireEvent.click(getByText('API plan'));
107
+ expect(onSelect).toHaveBeenCalledWith('c-42');
108
+ });
109
+
110
+ it('renders prop items before declarative (slotted) items', () => {
111
+ const propItems: ConversationSummary[] = [
112
+ { id: 'p-1', title: 'Prop conv', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: '', updatedAt: '' },
113
+ ];
114
+ const slottedItems: ConversationSummary[] = [
115
+ { id: 's-1', title: 'Slotted conv', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: '', updatedAt: '' },
116
+ ];
117
+ const merged = [...propItems, ...slottedItems];
118
+
119
+ const { getAllByRole } = render(() => <ConvList items={merged} onSelect={() => {}} />);
120
+ const buttons = getAllByRole('button');
121
+ expect(buttons).toHaveLength(2);
122
+ expect(buttons[0]).toHaveTextContent('Prop conv');
123
+ expect(buttons[1]).toHaveTextContent('Slotted conv');
124
+ });
125
+
126
+ it('parseKaiConversationElement produces items that render and fire correct id', () => {
127
+ const onSelect = vi.fn();
128
+ const el = document.createElement('kai-conversation');
129
+ el.setAttribute('id', 'c-99');
130
+ el.textContent = 'My conversation';
131
+
132
+ const parsed = parseKaiConversationElement(el);
133
+ const { getByText } = render(() => <ConvList items={[parsed]} onSelect={onSelect} />);
134
+ fireEvent.click(getByText('My conversation'));
135
+ expect(onSelect).toHaveBeenCalledWith('c-99');
136
+ });
137
+ });