@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,65 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import '@testing-library/jest-dom/vitest';
3
+ import { render, cleanup, fireEvent, screen } from '@solidjs/testing-library';
4
+ import { ModelSwitcher } from './model-switcher';
5
+ import type { ModelOption } from '../types';
6
+
7
+ afterEach(cleanup);
8
+
9
+ const MODELS: ModelOption[] = [
10
+ { id: 'gpt-5.5', name: 'GPT-5.5', description: 'Flagship model' },
11
+ { id: 'gpt-4o', name: 'GPT-4o', group: 'Legacy models' },
12
+ { id: 'gpt-4.1', name: 'GPT-4.1', group: 'Legacy models' },
13
+ ];
14
+
15
+ // Dropdown content renders through a Portal (outside the render container), so
16
+ // query via `screen` (document.body). The trigger button is the only button
17
+ // before any group is expanded.
18
+ const open = () => fireEvent.click(screen.getByRole('button'));
19
+
20
+ describe('ModelSwitcher grouping & descriptions', () => {
21
+ it('renders a model description under the model name', () => {
22
+ render(() => <ModelSwitcher models={MODELS} currentModelId="gpt-5.5" onModelChange={() => {}} />);
23
+ open();
24
+ expect(screen.getByText('Flagship model')).toBeInTheDocument();
25
+ });
26
+
27
+ it('hides grouped models behind a collapsed group header until expanded', () => {
28
+ render(() => <ModelSwitcher models={MODELS} currentModelId="gpt-5.5" onModelChange={() => {}} />);
29
+ open();
30
+ expect(screen.getByText('Legacy models')).toBeInTheDocument();
31
+ expect(screen.queryByText('GPT-4o')).not.toBeInTheDocument();
32
+ fireEvent.click(screen.getByText('Legacy models'));
33
+ expect(screen.getByText('GPT-4o')).toBeInTheDocument();
34
+ expect(screen.getByText('GPT-4.1')).toBeInTheDocument();
35
+ });
36
+
37
+ it('selects an ungrouped model', () => {
38
+ const onModelChange = vi.fn();
39
+ render(() => <ModelSwitcher models={MODELS} currentModelId="gpt-4o" onModelChange={onModelChange} />);
40
+ open();
41
+ fireEvent.click(screen.getByText('Flagship model')); // the GPT-5.5 row (description is unique)
42
+ expect(onModelChange).toHaveBeenCalledWith('gpt-5.5');
43
+ });
44
+
45
+ it('selects a grouped model after expanding its group', () => {
46
+ const onModelChange = vi.fn();
47
+ render(() => <ModelSwitcher models={MODELS} currentModelId="gpt-5.5" onModelChange={onModelChange} />);
48
+ open();
49
+ fireEvent.click(screen.getByText('Legacy models'));
50
+ fireEvent.click(screen.getByText('GPT-4o'));
51
+ expect(onModelChange).toHaveBeenCalledWith('gpt-4o');
52
+ });
53
+
54
+ it('still renders provider when no description is given (back-compat)', () => {
55
+ render(() => (
56
+ <ModelSwitcher
57
+ models={[{ id: 'a', name: 'Model A', provider: 'Acme' }, { id: 'b', name: 'Model B' }]}
58
+ currentModelId="a"
59
+ onModelChange={() => {}}
60
+ />
61
+ ));
62
+ open();
63
+ expect(screen.getByText('Acme')).toBeInTheDocument();
64
+ });
65
+ });
@@ -0,0 +1,87 @@
1
+ import { splitProps, For, Show, createSignal } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '../ui/dropdown';
4
+ import { Button } from '../ui/button';
5
+ import type { ModelOption } from '../types';
6
+
7
+ export interface ModelSwitcherProps { models: ModelOption[]; currentModelId: string; onModelChange: (modelId: string) => void; class?: string; }
8
+
9
+ const Chevron = (props: { class?: string }) => (
10
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class={props.class}>
11
+ <polyline points="6 9 12 15 18 9" />
12
+ </svg>
13
+ );
14
+
15
+ function ModelRow(props: { model: ModelOption; currentModelId: string; onModelChange: (id: string) => void }) {
16
+ const subtitle = () => props.model.description ?? props.model.provider;
17
+ return (
18
+ <DropdownItem onSelect={() => props.onModelChange(props.model.id)}>
19
+ <div class="flex flex-col">
20
+ <span class={cn('text-body', props.model.id === props.currentModelId && 'font-medium text-foreground')}>{props.model.name}</span>
21
+ <Show when={subtitle()}><span class="text-caption text-muted-foreground">{subtitle()}</span></Show>
22
+ </div>
23
+ </DropdownItem>
24
+ );
25
+ }
26
+
27
+ export function ModelSwitcher(props: ModelSwitcherProps) {
28
+ const [local] = splitProps(props, ['models', 'currentModelId', 'onModelChange', 'class']);
29
+ const currentModel = () => local.models.find((m) => m.id === local.currentModelId);
30
+
31
+ // Ungrouped models list first; grouped models collect under their group name,
32
+ // preserving first-seen group order.
33
+ const ungrouped = () => local.models.filter((m) => !m.group);
34
+ const groups = () => {
35
+ const order: string[] = [];
36
+ const byGroup = new Map<string, ModelOption[]>();
37
+ for (const m of local.models) {
38
+ if (!m.group) continue;
39
+ if (!byGroup.has(m.group)) { byGroup.set(m.group, []); order.push(m.group); }
40
+ byGroup.get(m.group)!.push(m);
41
+ }
42
+ return order.map((name) => ({ name, models: byGroup.get(name)! }));
43
+ };
44
+
45
+ return (
46
+ <Show when={local.models.length > 1}>
47
+ <Dropdown>
48
+ <DropdownTrigger as={(triggerProps: any) => (
49
+ <Button variant="ghost" size="sm" class={cn('gap-1 text-meta text-muted-foreground', local.class)} {...triggerProps}>
50
+ {currentModel()?.name ?? local.currentModelId}
51
+ <Chevron />
52
+ </Button>
53
+ )} />
54
+ <DropdownContent>
55
+ <For each={ungrouped()}>
56
+ {(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} />}
57
+ </For>
58
+ <For each={groups()}>
59
+ {(group) => {
60
+ const [open, setOpen] = createSignal(false);
61
+ return (
62
+ <div>
63
+ <button
64
+ type="button"
65
+ aria-expanded={open()}
66
+ onClick={() => setOpen(!open())}
67
+ class="flex w-full items-center justify-between rounded-md px-2 py-1.5 text-body outline-none transition-colors hover:bg-muted focus:bg-muted"
68
+ >
69
+ {group.name}
70
+ <Chevron class={cn('transition-transform', open() && 'rotate-180')} />
71
+ </button>
72
+ <Show when={open()}>
73
+ <div class="pl-2">
74
+ <For each={group.models}>
75
+ {(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} />}
76
+ </For>
77
+ </div>
78
+ </Show>
79
+ </div>
80
+ );
81
+ }}
82
+ </For>
83
+ </DropdownContent>
84
+ </Dropdown>
85
+ </Show>
86
+ );
87
+ }
@@ -0,0 +1,222 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { fn } from 'storybook/test';
3
+ import { createSignal } from 'solid-js';
4
+ import { PromptInput, PromptInputTextarea, PromptInputActions } from './prompt-input';
5
+ import { Button } from '../ui/button';
6
+ import { componentDescription } from '../stories/docs/element-controls';
7
+
8
+ const meta = {
9
+ title: 'Solid (Advanced)/Elements/PromptInput',
10
+ component: PromptInput,
11
+ tags: ['autodocs'],
12
+ parameters: {
13
+ layout: 'padded',
14
+ docs: {
15
+ controls: { exclude: ['use:eventListener'] },
16
+ description: componentDescription([
17
+ 'A composer shell that hosts an auto-resizing `PromptInputTextarea` and a `PromptInputActions` toolbar, with controlled or uncontrolled value, loading, and disabled states.',
18
+ '**When to use:** as the message input at the bottom of any chat surface, wherever the user types and submits a prompt.',
19
+ '**How to use:** control text via `value` + `onValueChange` (or leave uncontrolled), wire `onSubmit` (also fired on Enter without Shift), and place your send/stop controls inside `PromptInputActions`. Toggle `isLoading` / `disabled` for in-flight and read-only states.',
20
+ '**Placement:** pinned at the bottom of the chat column, below the message transcript.',
21
+ ]),
22
+ },
23
+ },
24
+ argTypes: {
25
+ value: {
26
+ control: 'text',
27
+ description: 'Controlled text value of the textarea.',
28
+ },
29
+ isLoading: {
30
+ control: 'boolean',
31
+ description: 'Marks a response as in-flight (e.g. to show a Stop action).',
32
+ table: { defaultValue: { summary: 'false' } },
33
+ },
34
+ disabled: {
35
+ control: 'boolean',
36
+ description: 'Disables the textarea and dims the composer.',
37
+ table: { defaultValue: { summary: 'false' } },
38
+ },
39
+ maxHeight: {
40
+ control: 'number',
41
+ description: 'Max auto-resize height in px (or a CSS length string) before the textarea scrolls.',
42
+ table: { defaultValue: { summary: '240' } },
43
+ },
44
+ onValueChange: {
45
+ action: 'valueChange',
46
+ description: 'Fired with the new text whenever the textarea value changes.',
47
+ table: { category: 'Events' },
48
+ },
49
+ onSubmit: {
50
+ action: 'submit',
51
+ description: 'Fired on Enter (without Shift), and wherever you call it from an action.',
52
+ table: { category: 'Events' },
53
+ },
54
+ children: {
55
+ control: false,
56
+ description: 'Composer contents — usually a textarea plus an actions row.',
57
+ },
58
+ class: {
59
+ control: 'text',
60
+ description: 'Extra classes for the composer shell.',
61
+ },
62
+ },
63
+ args: {
64
+ value: '',
65
+ isLoading: false,
66
+ disabled: false,
67
+ maxHeight: 240,
68
+ onValueChange: fn(),
69
+ onSubmit: fn(),
70
+ },
71
+ render: (args) => (
72
+ <div class="max-w-xl">
73
+ <PromptInput {...args}>
74
+ <PromptInputTextarea placeholder="Ask anything..." />
75
+ <PromptInputActions>
76
+ <Button variant="default" size="sm">Send</Button>
77
+ </PromptInputActions>
78
+ </PromptInput>
79
+ </div>
80
+ ),
81
+ } satisfies Meta<typeof PromptInput>;
82
+
83
+ export default meta;
84
+ type Story = StoryObj<typeof meta>;
85
+
86
+ const IMPORT = `import { PromptInput, PromptInputTextarea, PromptInputActions, Button } from '@kitn.ai/ui';`;
87
+ const src = (code: string) => ({
88
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
89
+ });
90
+
91
+ /** Interactive playground — toggle loading/disabled and edit the value via controls. */
92
+ export const Playground: Story = {
93
+ ...src(`<PromptInput value={value()} onValueChange={setValue} onSubmit={send}>
94
+ <PromptInputTextarea placeholder="Ask anything..." />
95
+ <PromptInputActions>
96
+ <Button variant="default" size="sm">Send</Button>
97
+ </PromptInputActions>
98
+ </PromptInput>`),
99
+ };
100
+
101
+ /** Empty composer with a Send button disabled until there is text. */
102
+ export const Default: Story = {
103
+ render: () => {
104
+ const [value, setValue] = createSignal('');
105
+ return (
106
+ <div class="max-w-xl">
107
+ <PromptInput value={value()} onValueChange={setValue}>
108
+ <PromptInputTextarea placeholder="Ask anything..." />
109
+ <PromptInputActions>
110
+ <Button variant="default" size="sm" disabled={!value()}>
111
+ Send
112
+ </Button>
113
+ </PromptInputActions>
114
+ </PromptInput>
115
+ </div>
116
+ );
117
+ },
118
+ ...src(`<PromptInput value={value()} onValueChange={setValue}>
119
+ <PromptInputTextarea placeholder="Ask anything..." />
120
+ <PromptInputActions>
121
+ <Button variant="default" size="sm" disabled={!value()}>Send</Button>
122
+ </PromptInputActions>
123
+ </PromptInput>`),
124
+ };
125
+
126
+ /** Pre-filled with a prompt. */
127
+ export const WithContent: Story = {
128
+ render: () => {
129
+ const [value, setValue] = createSignal('Tell me about SolidJS reactive primitives');
130
+ return (
131
+ <div class="max-w-xl">
132
+ <PromptInput value={value()} onValueChange={setValue}>
133
+ <PromptInputTextarea placeholder="Ask anything..." />
134
+ <PromptInputActions>
135
+ <Button variant="default" size="sm">Send</Button>
136
+ </PromptInputActions>
137
+ </PromptInput>
138
+ </div>
139
+ );
140
+ },
141
+ ...src(`<PromptInput value={value()} onValueChange={setValue}>
142
+ <PromptInputTextarea placeholder="Ask anything..." />
143
+ <PromptInputActions>
144
+ <Button variant="default" size="sm">Send</Button>
145
+ </PromptInputActions>
146
+ </PromptInput>`),
147
+ };
148
+
149
+ /** Read-only composer — `disabled` dims it and blocks input. */
150
+ export const Disabled: Story = {
151
+ render: () => (
152
+ <div class="max-w-xl">
153
+ <PromptInput disabled value="" onValueChange={() => {}}>
154
+ <PromptInputTextarea placeholder="Chat is disabled..." />
155
+ <PromptInputActions>
156
+ <Button variant="default" size="sm" disabled>Send</Button>
157
+ </PromptInputActions>
158
+ </PromptInput>
159
+ </div>
160
+ ),
161
+ ...src(`<PromptInput disabled value="" onValueChange={setValue}>
162
+ <PromptInputTextarea placeholder="Chat is disabled..." />
163
+ <PromptInputActions>
164
+ <Button variant="default" size="sm" disabled>Send</Button>
165
+ </PromptInputActions>
166
+ </PromptInput>`),
167
+ };
168
+
169
+ /** In-flight — `isLoading` typically pairs with a Stop action. */
170
+ export const Loading: Story = {
171
+ render: () => (
172
+ <div class="max-w-xl">
173
+ <PromptInput isLoading value="" onValueChange={() => {}}>
174
+ <PromptInputTextarea placeholder="Generating response..." />
175
+ <PromptInputActions>
176
+ <Button variant="outline" size="sm">
177
+ Stop
178
+ </Button>
179
+ </PromptInputActions>
180
+ </PromptInput>
181
+ </div>
182
+ ),
183
+ ...src(`<PromptInput isLoading value={value()} onValueChange={setValue}>
184
+ <PromptInputTextarea placeholder="Generating response..." />
185
+ <PromptInputActions>
186
+ <Button variant="outline" size="sm">Stop</Button>
187
+ </PromptInputActions>
188
+ </PromptInput>`),
189
+ };
190
+
191
+ /** A split actions row — a leading icon control and a trailing Send (showcase). */
192
+ export const WithMultipleActions: Story = {
193
+ render: () => {
194
+ const [value, setValue] = createSignal('');
195
+ return (
196
+ <div class="max-w-xl">
197
+ <PromptInput value={value()} onValueChange={setValue}>
198
+ <PromptInputTextarea placeholder="Ask anything..." />
199
+ <PromptInputActions class="justify-between w-full px-2 pb-1">
200
+ <div class="flex items-center gap-1">
201
+ <Button variant="ghost" size="icon-sm" aria-label="Attach file">
202
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
203
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
204
+ </svg>
205
+ </Button>
206
+ </div>
207
+ <Button variant="default" size="sm" disabled={!value()}>
208
+ Send
209
+ </Button>
210
+ </PromptInputActions>
211
+ </PromptInput>
212
+ </div>
213
+ );
214
+ },
215
+ ...src(`<PromptInput value={value()} onValueChange={setValue}>
216
+ <PromptInputTextarea placeholder="Ask anything..." />
217
+ <PromptInputActions class="justify-between w-full px-2 pb-1">
218
+ <Button variant="ghost" size="icon-sm"><AttachIcon /></Button>
219
+ <Button variant="default" size="sm" disabled={!value()}>Send</Button>
220
+ </PromptInputActions>
221
+ </PromptInput>`),
222
+ };
@@ -0,0 +1,211 @@
1
+ import { type JSX, splitProps, createSignal, createContext, useContext, createEffect, on } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { useChatConfig, textClass } from '../primitives/chat-config';
4
+
5
+ // --- Context ---
6
+
7
+ interface PromptInputContextType {
8
+ isLoading: boolean;
9
+ value: () => string;
10
+ setValue: (value: string) => void;
11
+ maxHeight: number | string;
12
+ onSubmit?: () => void;
13
+ disabled?: boolean;
14
+ textareaRef: HTMLTextAreaElement | undefined;
15
+ setTextareaRef: (el: HTMLTextAreaElement) => void;
16
+ }
17
+
18
+ const PromptInputContext = createContext<PromptInputContextType>();
19
+
20
+ function usePromptInput() {
21
+ const ctx = useContext(PromptInputContext);
22
+ if (!ctx) throw new Error('PromptInput subcomponents must be used within PromptInput');
23
+ return ctx;
24
+ }
25
+
26
+ // --- PromptInput (Root) ---
27
+
28
+ export interface PromptInputProps extends JSX.HTMLAttributes<HTMLDivElement> {
29
+ isLoading?: boolean;
30
+ value?: string;
31
+ onValueChange?: (value: string) => void;
32
+ maxHeight?: number | string;
33
+ onSubmit?: () => void;
34
+ children: JSX.Element;
35
+ disabled?: boolean;
36
+ }
37
+
38
+ function PromptInput(props: PromptInputProps) {
39
+ const [local, rest] = splitProps(props, [
40
+ 'isLoading', 'value', 'onValueChange', 'maxHeight', 'onSubmit',
41
+ 'children', 'disabled', 'class', 'onClick',
42
+ ]);
43
+
44
+ const [internalValue, setInternalValue] = createSignal(local.value ?? '');
45
+ let textareaRef: HTMLTextAreaElement | undefined;
46
+
47
+ const handleChange = (newValue: string) => {
48
+ setInternalValue(newValue);
49
+ local.onValueChange?.(newValue);
50
+ };
51
+
52
+ const handleClick: JSX.EventHandler<HTMLDivElement, MouseEvent> = (e) => {
53
+ if (!local.disabled) textareaRef?.focus();
54
+ if (typeof local.onClick === 'function') {
55
+ (local.onClick as (e: MouseEvent & { currentTarget: HTMLDivElement }) => void)(e);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <PromptInputContext.Provider
61
+ value={{
62
+ isLoading: local.isLoading ?? false,
63
+ value: () => local.value ?? internalValue(),
64
+ setValue: local.onValueChange ?? handleChange,
65
+ maxHeight: local.maxHeight ?? 240,
66
+ onSubmit: local.onSubmit,
67
+ get disabled() { return local.disabled; },
68
+ get textareaRef() { return textareaRef; },
69
+ setTextareaRef: (el) => { textareaRef = el; },
70
+ }}
71
+ >
72
+ <div
73
+ onClick={handleClick}
74
+ class={cn(
75
+ // The inner textarea neutralizes its own ring (focus-visible:ring-0),
76
+ // so the FRAME owns the focus affordance: a blue ring whenever a
77
+ // control inside it is focused. Without this the composer had no
78
+ // visible keyboard-focus state.
79
+ 'bg-muted/40 cursor-text rounded-xl p-2 shadow-xs',
80
+ 'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-0',
81
+ local.disabled && 'cursor-not-allowed opacity-60',
82
+ local.class
83
+ )}
84
+ {...rest}
85
+ >
86
+ {local.children}
87
+ </div>
88
+ </PromptInputContext.Provider>
89
+ );
90
+ }
91
+
92
+ // --- PromptInputTextarea ---
93
+
94
+ export interface PromptInputTextareaProps extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement> {
95
+ disableAutosize?: boolean;
96
+ }
97
+
98
+ function PromptInputTextarea(props: PromptInputTextareaProps) {
99
+ const [local, rest] = splitProps(props, ['class', 'onKeyDown', 'disableAutosize']);
100
+ const ctx = usePromptInput();
101
+ const config = useChatConfig();
102
+
103
+ function adjustHeight(el: HTMLTextAreaElement | undefined) {
104
+ if (!el || local.disableAutosize) return;
105
+ el.style.height = 'auto';
106
+ const maxH = ctx.maxHeight;
107
+ if (typeof maxH === 'number') {
108
+ el.style.height = `${Math.min(el.scrollHeight, maxH)}px`;
109
+ } else {
110
+ el.style.height = `min(${el.scrollHeight}px, ${maxH})`;
111
+ }
112
+ }
113
+
114
+ function handleRef(el: HTMLTextAreaElement) {
115
+ ctx.setTextareaRef(el);
116
+ adjustHeight(el);
117
+ }
118
+
119
+ createEffect(on(
120
+ () => [ctx.value(), ctx.maxHeight, local.disableAutosize],
121
+ () => {
122
+ if (ctx.textareaRef && !local.disableAutosize) {
123
+ adjustHeight(ctx.textareaRef);
124
+ }
125
+ }
126
+ ));
127
+
128
+ function handleInput(e: InputEvent & { currentTarget: HTMLTextAreaElement }) {
129
+ const el = e.currentTarget;
130
+ let value = el.value;
131
+ // Disallow leading whitespace — a prompt can't start with a space or blank
132
+ // line. Strip it (covers typing a space at the start AND pasting) and keep
133
+ // the caret in the right place.
134
+ if (/^\s/.test(value)) {
135
+ const stripped = value.replace(/^\s+/, '');
136
+ const removed = value.length - stripped.length;
137
+ const caret = Math.max(0, (el.selectionStart ?? 0) - removed);
138
+ el.value = stripped;
139
+ el.setSelectionRange(caret, caret);
140
+ value = stripped;
141
+ }
142
+ adjustHeight(el);
143
+ ctx.setValue(value);
144
+ }
145
+
146
+ function handleKeyDown(e: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) {
147
+ if (e.key === 'Enter' && !e.shiftKey) {
148
+ e.preventDefault();
149
+ // A disabled composer is non-interactive: Enter must not submit.
150
+ if (!ctx.disabled) ctx.onSubmit?.();
151
+ }
152
+ if (typeof local.onKeyDown === 'function') {
153
+ (local.onKeyDown as (e: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) => void)(e);
154
+ }
155
+ }
156
+
157
+ return (
158
+ <textarea
159
+ ref={handleRef}
160
+ value={ctx.value()}
161
+ onInput={handleInput}
162
+ onKeyDown={handleKeyDown}
163
+ class={cn(
164
+ // Typed text uses the normal foreground (light/dark), NOT the brand/accent
165
+ // color — the composer should read as plain text, not tinted.
166
+ 'text-foreground min-h-[44px] w-full resize-none border-none bg-transparent shadow-none outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
167
+ textClass(config.proseSize()),
168
+ local.class
169
+ )}
170
+ rows={1}
171
+ disabled={ctx.disabled}
172
+ {...rest}
173
+ />
174
+ );
175
+ }
176
+
177
+ // --- PromptInputActions ---
178
+
179
+ export interface PromptInputActionsProps extends JSX.HTMLAttributes<HTMLDivElement> {
180
+ children: JSX.Element;
181
+ }
182
+
183
+ function PromptInputActions(props: PromptInputActionsProps) {
184
+ const [local, rest] = splitProps(props, ['children', 'class']);
185
+ return (
186
+ <div class={cn('flex items-center gap-2', local.class)} {...rest}>
187
+ {local.children}
188
+ </div>
189
+ );
190
+ }
191
+
192
+ // --- PromptInputAction ---
193
+
194
+ export interface PromptInputActionProps {
195
+ tooltip?: string;
196
+ children: JSX.Element;
197
+ side?: 'top' | 'bottom' | 'left' | 'right';
198
+ class?: string;
199
+ }
200
+
201
+ function PromptInputAction(props: PromptInputActionProps) {
202
+ return <>{props.children}</>;
203
+ }
204
+
205
+ export {
206
+ PromptInput,
207
+ PromptInputTextarea,
208
+ PromptInputActions,
209
+ PromptInputAction,
210
+ usePromptInput,
211
+ };