@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,133 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { fn } from 'storybook/test';
3
+ import { ConversationList, type ConversationListProps } from './conversation-list';
4
+ import type { ConversationSummary, ConversationGroup } from '../types';
5
+ import { componentDescription } from '../stories/docs/element-controls';
6
+
7
+ const scope = { type: 'document' as const };
8
+
9
+ const groups: ConversationGroup[] = [
10
+ { id: 'today', name: 'Today', sortOrder: 0, createdAt: '2026-04-10' },
11
+ { id: 'yesterday', name: 'Yesterday', sortOrder: 1, createdAt: '2026-04-09' },
12
+ { id: 'week', name: 'This Week', sortOrder: 2, createdAt: '2026-04-07' },
13
+ ];
14
+
15
+ const conversations: ConversationSummary[] = [
16
+ { id: '1', title: 'SolidJS signals explained', groupId: 'today', scope, messageCount: 5, lastMessageAt: '2026-04-10T14:00:00Z', updatedAt: '2026-04-10T14:00:00Z' },
17
+ { id: '2', title: 'TypeScript generics deep dive', groupId: 'today', scope, messageCount: 12, lastMessageAt: '2026-04-10T10:00:00Z', updatedAt: '2026-04-10T10:00:00Z' },
18
+ { id: '3', title: 'CSS Grid vs Flexbox', groupId: 'yesterday', scope, messageCount: 8, lastMessageAt: '2026-04-09T16:00:00Z', updatedAt: '2026-04-09T16:00:00Z' },
19
+ { id: '4', title: 'Setting up Storybook', groupId: 'yesterday', scope, messageCount: 3, lastMessageAt: '2026-04-09T11:00:00Z', updatedAt: '2026-04-09T11:00:00Z' },
20
+ { id: '5', title: 'Vite configuration tips', groupId: 'week', scope, messageCount: 7, lastMessageAt: '2026-04-08T09:00:00Z', updatedAt: '2026-04-08T09:00:00Z' },
21
+ { id: '6', title: 'Chrome extension manifest v3', groupId: 'week', scope, messageCount: 15, lastMessageAt: '2026-04-07T14:00:00Z', updatedAt: '2026-04-07T14:00:00Z' },
22
+ ];
23
+
24
+ /**
25
+ * The conversation/chat history sidebar: a header with new-chat action, a search
26
+ * box, and grouped, collapsible lists of `ConversationItem`s.
27
+ */
28
+ const meta = {
29
+ title: 'Solid (Advanced)/Elements/ConversationList',
30
+ component: ConversationList,
31
+ tags: ['autodocs'],
32
+ parameters: {
33
+ layout: 'padded',
34
+ docs: {
35
+ description: componentDescription([
36
+ 'A full chat-history sidebar: header (sidebar toggle + new chat), a built-in search box that filters by title, and conversations bucketed into collapsible, count-badged groups.',
37
+ '**When to use:** as the left-hand navigation for a chat app — browsing, searching, and switching between past conversations.',
38
+ '**How to use:** pass `groups` and `conversations` arrays, the `activeId`, and handlers `onSelect(id)` / `onNewChat()` (plus optional `onToggleSidebar()`). Give it a sized, overflow-hidden container.',
39
+ '**Placement:** the persistent left sidebar of a chat layout.',
40
+ ]),
41
+ controls: { exclude: ['use:eventListener'] },
42
+ },
43
+ },
44
+ argTypes: {
45
+ groups: {
46
+ control: 'object',
47
+ description: 'Conversation groups (buckets) to render as collapsible sections.',
48
+ },
49
+ conversations: {
50
+ control: 'object',
51
+ description: 'Conversation summaries; placed into groups by their `groupId`.',
52
+ },
53
+ activeId: {
54
+ control: 'text',
55
+ description: 'Id of the currently selected conversation.',
56
+ },
57
+ onSelect: {
58
+ action: 'select',
59
+ description: 'Fired with the conversation id when a row is clicked.',
60
+ table: { category: 'Events' },
61
+ },
62
+ onNewChat: {
63
+ action: 'newChat',
64
+ description: 'Fired when the new-chat (+) button is clicked.',
65
+ table: { category: 'Events' },
66
+ },
67
+ onToggleSidebar: {
68
+ action: 'toggleSidebar',
69
+ description: 'Fired when the sidebar-toggle (menu) button is clicked.',
70
+ table: { category: 'Events' },
71
+ },
72
+ },
73
+ args: {
74
+ groups,
75
+ conversations,
76
+ activeId: '1',
77
+ onSelect: fn(),
78
+ onNewChat: fn(),
79
+ onToggleSidebar: fn(),
80
+ },
81
+ render: (args) => (
82
+ <div class="h-[500px] w-72 border border-border rounded-lg overflow-hidden">
83
+ <ConversationList {...args} />
84
+ </div>
85
+ ),
86
+ } satisfies Meta<typeof ConversationList>;
87
+
88
+ export default meta;
89
+ type Story = StoryObj<typeof meta>;
90
+
91
+ const IMPORT = `import { ConversationList } from '@kitn.ai/ui';`;
92
+ const src = (code: string) => ({
93
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
94
+ });
95
+
96
+ const usage = `<ConversationList
97
+ groups={groups}
98
+ conversations={conversations}
99
+ activeId={activeId()}
100
+ onSelect={setActiveId}
101
+ onNewChat={() => {}}
102
+ />`;
103
+
104
+ /** Interactive playground — edit the groups/conversations arrays and active id via controls. */
105
+ export const Playground: Story = {
106
+ ...src(usage),
107
+ };
108
+
109
+ /** Conversations bucketed into Today / Yesterday / This Week. */
110
+ export const WithGroups: Story = {
111
+ ...src(usage),
112
+ };
113
+
114
+ /** No conversations — the list renders just its header and search box. */
115
+ export const EmptyState: Story = {
116
+ args: { groups: [], conversations: [], activeId: undefined },
117
+ render: (args: ConversationListProps) => (
118
+ <div class="h-[400px] w-72 border border-border rounded-lg overflow-hidden">
119
+ <ConversationList {...args} />
120
+ </div>
121
+ ),
122
+ ...src(`<ConversationList
123
+ groups={[]}
124
+ conversations={[]}
125
+ onSelect={() => {}}
126
+ onNewChat={() => {}}
127
+ />`),
128
+ };
129
+
130
+ /** Type in the built-in search box to filter conversations by title. */
131
+ export const WithSearch: Story = {
132
+ ...src(usage),
133
+ };
@@ -0,0 +1,101 @@
1
+ import { splitProps, For, Show, createSignal, createMemo } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../ui/collapsible';
4
+ import { Button } from '../ui/button';
5
+ import { Badge } from '../ui/badge';
6
+ import { ScrollArea } from '../ui/scroll-area';
7
+ import { ConversationItem } from './conversation-item';
8
+ import type { ConversationSummary, ConversationGroup } from '../types';
9
+
10
+ export interface ConversationListProps {
11
+ groups: ConversationGroup[];
12
+ conversations: ConversationSummary[];
13
+ activeId?: string;
14
+ onSelect: (id: string) => void;
15
+ onNewChat: () => void;
16
+ onToggleSidebar?: () => void;
17
+ class?: string;
18
+ }
19
+
20
+ export function ConversationList(props: ConversationListProps) {
21
+ const [local] = splitProps(props, ['groups', 'conversations', 'activeId', 'onSelect', 'onNewChat', 'onToggleSidebar', 'class']);
22
+ const [searchQuery, setSearchQuery] = createSignal('');
23
+
24
+ const filteredConversations = createMemo(() => {
25
+ const q = searchQuery().toLowerCase();
26
+ if (!q) return local.conversations;
27
+ return local.conversations.filter((c) => c.title.toLowerCase().includes(q));
28
+ });
29
+
30
+ const groupedConversations = createMemo(() => {
31
+ const grouped = new Map<string | undefined, ConversationSummary[]>();
32
+ for (const conv of filteredConversations()) {
33
+ const key = conv.groupId;
34
+ if (!grouped.has(key)) grouped.set(key, []);
35
+ grouped.get(key)!.push(conv);
36
+ }
37
+ return grouped;
38
+ });
39
+
40
+ const ungrouped = createMemo(() => groupedConversations().get(undefined) ?? []);
41
+
42
+ return (
43
+ <div class={cn('flex flex-col h-full bg-sidebar', local.class)}>
44
+ <div class="flex items-center justify-between p-3 pb-2">
45
+ <div class="flex items-center gap-2">
46
+ <Button variant="ghost" size="icon-sm" aria-label="Toggle sidebar" onClick={local.onToggleSidebar}>
47
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
48
+ </Button>
49
+ <span class="text-sm font-semibold text-foreground">Chats</span>
50
+ </div>
51
+ <Button variant="ghost" size="icon-sm" aria-label="New chat" onClick={local.onNewChat}>
52
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
53
+ </Button>
54
+ </div>
55
+ <div class="px-3 pb-2">
56
+ <div class="flex items-center gap-2 rounded-md bg-muted/40 px-2.5 py-1.5">
57
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted-foreground"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
58
+ <input type="text" value={searchQuery()} onInput={(e) => setSearchQuery(e.currentTarget.value)} placeholder="Search chats..."
59
+ aria-label="Search chats"
60
+ class="bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring w-full" />
61
+ </div>
62
+ </div>
63
+ <ScrollArea class="flex-1 px-2">
64
+ <For each={local.groups}>
65
+ {(group) => {
66
+ const convs = createMemo(() => groupedConversations().get(group.id) ?? []);
67
+ return (
68
+ <Show when={convs().length > 0}>
69
+ <GroupSection name={group.name} count={convs().length} conversations={convs()} activeId={local.activeId} onSelect={local.onSelect} />
70
+ </Show>
71
+ );
72
+ }}
73
+ </For>
74
+ <Show when={ungrouped().length > 0}>
75
+ <GroupSection name="Ungrouped" count={ungrouped().length} conversations={ungrouped()} activeId={local.activeId} onSelect={local.onSelect} />
76
+ </Show>
77
+ </ScrollArea>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ function GroupSection(props: { name: string; count: number; conversations: ConversationSummary[]; activeId?: string; onSelect: (id: string) => void }) {
83
+ const [open, setOpen] = createSignal(true);
84
+ return (
85
+ <Collapsible open={open()} onOpenChange={setOpen}>
86
+ <CollapsibleTrigger class="flex items-center gap-1.5 w-full px-1.5 py-1 rounded-md text-[13px] text-muted-foreground font-medium hover:bg-muted/30 transition-colors cursor-pointer mt-1.5">
87
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
88
+ class={cn('transition-transform', !open() && '-rotate-90')}><polyline points="6 9 12 15 18 9"/></svg>
89
+ <span>{props.name}</span>
90
+ <Badge variant="count" class="ml-auto text-[11px]">{props.count}</Badge>
91
+ </CollapsibleTrigger>
92
+ <CollapsibleContent>
93
+ <div class="pl-2 mt-0.5 space-y-0.5">
94
+ <For each={props.conversations}>
95
+ {(conv) => <ConversationItem conversation={conv} isActive={conv.id === props.activeId} onSelect={props.onSelect} />}
96
+ </For>
97
+ </div>
98
+ </CollapsibleContent>
99
+ </Collapsible>
100
+ );
101
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import '@testing-library/jest-dom/vitest';
3
+ import { render, cleanup, fireEvent } from '@solidjs/testing-library';
4
+ import { createSignal } from 'solid-js';
5
+ import { DefaultPromptInput } from '../elements/default-input';
6
+
7
+ // jsdom doesn't implement Element.scrollTo; some inner controls may call it.
8
+ if (!Element.prototype.scrollTo) (Element.prototype as unknown as { scrollTo: () => void }).scrollTo = () => {};
9
+
10
+ afterEach(cleanup);
11
+
12
+ const noop = () => {};
13
+
14
+ describe('DefaultPromptInput disabled state', () => {
15
+ it('renders the textarea and send button as disabled', () => {
16
+ const { container } = render(() => (
17
+ <DefaultPromptInput
18
+ value="hello"
19
+ disabled
20
+ onValueChange={noop}
21
+ onSubmit={noop}
22
+ onSuggestionClick={noop}
23
+ />
24
+ ));
25
+ const textarea = container.querySelector('textarea');
26
+ const send = container.querySelector('[data-testid="send"]');
27
+ expect(textarea).toBeDisabled();
28
+ expect(send).toBeDisabled();
29
+ });
30
+
31
+ it('does not call onSubmit when the send button is clicked while disabled', () => {
32
+ const onSubmit = vi.fn();
33
+ const { container } = render(() => (
34
+ <DefaultPromptInput
35
+ value="hello"
36
+ disabled
37
+ onValueChange={noop}
38
+ onSubmit={onSubmit}
39
+ onSuggestionClick={noop}
40
+ />
41
+ ));
42
+ const send = container.querySelector('[data-testid="send"]') as HTMLButtonElement;
43
+ fireEvent.click(send);
44
+ expect(onSubmit).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it('does not call onSubmit when Enter is pressed while disabled', () => {
48
+ const onSubmit = vi.fn();
49
+ const { container } = render(() => (
50
+ <DefaultPromptInput
51
+ value="hello"
52
+ disabled
53
+ onValueChange={noop}
54
+ onSubmit={onSubmit}
55
+ onSuggestionClick={noop}
56
+ />
57
+ ));
58
+ const textarea = container.querySelector('textarea') as HTMLTextAreaElement;
59
+ fireEvent.keyDown(textarea, { key: 'Enter' });
60
+ expect(onSubmit).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('reactively disables/enables the textarea when `disabled` changes after mount', () => {
64
+ const [disabled, setDisabled] = createSignal(false);
65
+ const { container } = render(() => (
66
+ <DefaultPromptInput
67
+ value="hi"
68
+ disabled={disabled()}
69
+ onValueChange={noop}
70
+ onSubmit={noop}
71
+ onSuggestionClick={noop}
72
+ />
73
+ ));
74
+ const textarea = container.querySelector('textarea') as HTMLTextAreaElement;
75
+ expect(textarea).not.toBeDisabled();
76
+ setDisabled(true);
77
+ expect(textarea).toBeDisabled(); // fails if PromptInput context captures `disabled` statically
78
+ setDisabled(false);
79
+ expect(textarea).not.toBeDisabled();
80
+ });
81
+
82
+ it('still submits via Enter when not disabled (sanity)', () => {
83
+ const onSubmit = vi.fn();
84
+ const { container } = render(() => (
85
+ <DefaultPromptInput
86
+ value="hello"
87
+ onValueChange={noop}
88
+ onSubmit={onSubmit}
89
+ onSuggestionClick={noop}
90
+ />
91
+ ));
92
+ const textarea = container.querySelector('textarea') as HTMLTextAreaElement;
93
+ fireEvent.keyDown(textarea, { key: 'Enter' });
94
+ expect(onSubmit).toHaveBeenCalledTimes(1);
95
+ });
96
+ });
97
+
98
+ describe('DefaultPromptInput leading/trailing slots', () => {
99
+ it('renders a leading and trailing named slot', () => {
100
+ const { container } = render(() => (
101
+ <DefaultPromptInput
102
+ value=""
103
+ onValueChange={noop}
104
+ onSubmit={noop}
105
+ onSuggestionClick={noop}
106
+ />
107
+ ));
108
+ expect(container.querySelector('slot[name="leading"]')).toBeTruthy();
109
+ expect(container.querySelector('slot[name="trailing"]')).toBeTruthy();
110
+ });
111
+ });
@@ -0,0 +1,196 @@
1
+ import {
2
+ type JSX,
3
+ splitProps,
4
+ createSignal,
5
+ createEffect,
6
+ createMemo,
7
+ on,
8
+ onMount,
9
+ Show,
10
+ } from 'solid-js';
11
+ import { cn } from '../utils/cn';
12
+ import { Play, Video, ExternalLink, TriangleAlert } from 'lucide-solid';
13
+ import type { CardEvent } from '../primitives/card-contract';
14
+ import {
15
+ type EmbedCardData,
16
+ type ResolvedEmbed,
17
+ resolveEmbed,
18
+ watchUrl,
19
+ providerLabel,
20
+ aspectRatioValue,
21
+ } from '../primitives/embed-providers';
22
+
23
+ export interface EmbedProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onError'> {
24
+ /** The card id correlating every emitted event. */
25
+ cardId: string;
26
+ /** The embed payload (data-down). */
27
+ data: EmbedCardData;
28
+ /** Emit a contract CardEvent up (host routes it). */
29
+ onEmit?: (event: CardEvent) => void;
30
+ }
31
+
32
+ /**
33
+ * `Embed` — a privacy-first lazy media facade. Initial render is a poster + a play
34
+ * button: NO provider iframe, NO provider JS, NO cookies until the user opts in.
35
+ * On play it swaps in the provider `<iframe>` (youtube-nocookie / vimeo dnt /
36
+ * allowlisted generic). A persistent "Open on {provider}" affordance routes the
37
+ * contract `open` verb so a blocked embed is never a dead end. The only verbs it
38
+ * emits are lifecycle `ready`, `open`, and failure `error`.
39
+ */
40
+ export function Embed(props: EmbedProps): JSX.Element {
41
+ const [local, rest] = splitProps(props, ['cardId', 'data', 'onEmit', 'class']);
42
+
43
+ const emit = (event: CardEvent) => local.onEmit?.(event);
44
+
45
+ const [playing, setPlaying] = createSignal(false);
46
+ const [posterBroken, setPosterBroken] = createSignal(false);
47
+ let playerRegion: HTMLDivElement | undefined;
48
+
49
+ // Resolve the provider URL/poster/sandbox; capture a resolution error to render inline.
50
+ const resolved = createMemo<{ ok: true; value: ResolvedEmbed } | { ok: false; error: string }>(
51
+ () => {
52
+ try {
53
+ return { ok: true, value: resolveEmbed(local.data) };
54
+ } catch (e) {
55
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
56
+ }
57
+ },
58
+ );
59
+
60
+ const ratio = createMemo(() => aspectRatioValue(local.data.aspectRatio));
61
+ const titleText = () => local.data.title ?? 'Embedded video';
62
+ const poster = createMemo(() =>
63
+ resolved().ok && !posterBroken() ? (resolved() as { value: ResolvedEmbed }).value.posterUrl : undefined,
64
+ );
65
+ const watch = createMemo(() => watchUrl(local.data));
66
+ const label = () => providerLabel(local.data.provider);
67
+
68
+ onMount(() => emit({ kind: 'ready', cardId: local.cardId }));
69
+
70
+ // Resolution failure → a single `error` event (defense in depth; the schema allOf
71
+ // and the origin allowlist should catch most of these earlier).
72
+ createEffect(
73
+ on(resolved, (r) => {
74
+ if (!r.ok) emit({ kind: 'error', cardId: local.cardId, message: r.error });
75
+ }),
76
+ );
77
+
78
+ // Reset facade state when the payload changes.
79
+ createEffect(
80
+ on(
81
+ () => local.data,
82
+ () => {
83
+ setPlaying(false);
84
+ setPosterBroken(false);
85
+ },
86
+ { defer: true },
87
+ ),
88
+ );
89
+
90
+ const play = () => {
91
+ if (!resolved().ok) return;
92
+ setPlaying(true);
93
+ // Move focus into the player region for SR/keyboard continuity.
94
+ queueMicrotask(() => playerRegion?.focus());
95
+ };
96
+ const onPlayKeyDown = (e: KeyboardEvent) => {
97
+ if (e.key === 'Enter' || e.key === ' ') {
98
+ e.preventDefault();
99
+ play();
100
+ }
101
+ };
102
+ const openOnProvider = () => {
103
+ const w = watch();
104
+ if (w) emit({ kind: 'open', cardId: local.cardId, url: w, target: 'tab' });
105
+ };
106
+
107
+ return (
108
+ <div
109
+ class={cn('overflow-hidden rounded-xl border border-border bg-card text-card-foreground', local.class)}
110
+ {...rest}
111
+ >
112
+ <div class="relative w-full bg-black" style={{ 'aspect-ratio': ratio() }}>
113
+ <Show
114
+ when={resolved().ok}
115
+ fallback={
116
+ <div
117
+ role="img"
118
+ aria-label="Can't load this video"
119
+ class="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-muted p-4 text-center text-sm text-muted-foreground"
120
+ >
121
+ <TriangleAlert size={28} aria-hidden="true" />
122
+ <span>Can't load this video</span>
123
+ </div>
124
+ }
125
+ >
126
+ <Show
127
+ when={playing()}
128
+ fallback={
129
+ <button
130
+ type="button"
131
+ aria-label={`Play ${local.data.title ?? 'video'}`}
132
+ class="group absolute inset-0 flex h-full w-full items-center justify-center outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
133
+ onClick={() => play()}
134
+ onKeyDown={onPlayKeyDown}
135
+ >
136
+ {/* Poster (or a neutral placeholder when absent/broken). */}
137
+ <Show
138
+ when={poster()}
139
+ fallback={
140
+ <div
141
+ class="absolute inset-0 bg-gradient-to-br from-zinc-800 to-zinc-950"
142
+ aria-hidden="true"
143
+ />
144
+ }
145
+ >
146
+ <img
147
+ src={poster()}
148
+ alt={local.data.title ?? ''}
149
+ class="absolute inset-0 h-full w-full object-cover"
150
+ onError={() => setPosterBroken(true)}
151
+ />
152
+ </Show>
153
+ <span
154
+ class="relative flex h-14 w-14 items-center justify-center rounded-full bg-black/60 text-white shadow-lg transition-transform motion-safe:group-hover:scale-110"
155
+ aria-hidden="true"
156
+ >
157
+ <Play size={26} fill="currentColor" />
158
+ </span>
159
+ </button>
160
+ }
161
+ >
162
+ <div ref={(el) => (playerRegion = el)} tabindex="-1" class="absolute inset-0 outline-none">
163
+ <iframe
164
+ src={(resolved() as { value: ResolvedEmbed }).value.embedUrl}
165
+ title={titleText()}
166
+ sandbox={(resolved() as { value: ResolvedEmbed }).value.sandbox}
167
+ allow={(resolved() as { value: ResolvedEmbed }).value.allow}
168
+ allowfullscreen
169
+ class="absolute inset-0 h-full w-full border-0"
170
+ />
171
+ </div>
172
+ </Show>
173
+ </Show>
174
+ </div>
175
+ {/* Footer: title + the always-available "Open on {provider}" fallback affordance. */}
176
+ <div class="flex items-center justify-between gap-2 px-3 py-2">
177
+ <span class="flex min-w-0 items-center gap-1.5 text-sm text-foreground">
178
+ <Video size={14} class="shrink-0 opacity-70" aria-hidden="true" />
179
+ <span class="truncate">{local.data.title ?? label()}</span>
180
+ </span>
181
+ <Show when={watch()}>
182
+ <button
183
+ type="button"
184
+ class="inline-flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
185
+ onClick={() => openOnProvider()}
186
+ >
187
+ Open on {label()}
188
+ <ExternalLink size={12} aria-hidden="true" />
189
+ </button>
190
+ </Show>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ export type { EmbedCardData } from '../primitives/embed-providers';