@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,226 @@
1
+ import { describe, it, expect, afterEach, beforeAll } from 'vitest';
2
+ import '@testing-library/jest-dom/vitest';
3
+ import { render, cleanup } from '@solidjs/testing-library';
4
+ import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '../ui/resizable';
5
+
6
+ // NOTE on file location: the task brief names `src/components/resizable.tsx`, but
7
+ // in this tree the resizable UI primitives live in `src/ui/resizable.tsx` and the
8
+ // custom element in `src/elements/resizable.tsx`. This test (kept at the requested
9
+ // `src/components/` path) exercises both the UI primitive (`ResizableHandle`
10
+ // dblclick-reset + default-size reflection) and the element (`<kai-resizable>`
11
+ // size-preservation across content-only re-renders).
12
+
13
+ // jsdom doesn't implement layout: getBoundingClientRect returns all-zeros, so the
14
+ // pixel-driven DRAG math (pointer move → flex-basis) and the live-percent settle
15
+ // are NOT meaningfully testable here. We test the *state logic* that is layout-
16
+ // independent: (a) the panel reflects its default size to a data-* attribute, and
17
+ // (b) dblclick on a handle restores adjacent panels' flex-basis from those
18
+ // defaults, even after their inline basis has been mutated (simulating a drag).
19
+ // The element test asserts that a content-only mutation does NOT clobber a panel's
20
+ // dragged inline flex-basis.
21
+
22
+ afterEach(cleanup);
23
+
24
+ describe('ResizablePanel default-size reflection', () => {
25
+ it('reflects defaultSize to data-default-size-pct (percent) so a handle can reset to it', () => {
26
+ const { container } = render(() => (
27
+ <ResizablePanelGroup orientation="horizontal">
28
+ <ResizablePanel defaultSize={30}>a</ResizablePanel>
29
+ <ResizableHandle />
30
+ <ResizablePanel>b</ResizablePanel>
31
+ </ResizablePanelGroup>
32
+ ));
33
+ const panels = container.querySelectorAll('[style*="flex-basis"]');
34
+ const first = panels[0] as HTMLElement;
35
+ expect(first.dataset.defaultSizePct).toBe('30');
36
+ });
37
+
38
+ it('reflects a px defaultSize to data-default-size (pixels)', () => {
39
+ const { container } = render(() => (
40
+ <ResizablePanelGroup orientation="horizontal">
41
+ <ResizablePanel defaultSize="280px">a</ResizablePanel>
42
+ <ResizableHandle />
43
+ <ResizablePanel>b</ResizablePanel>
44
+ </ResizablePanelGroup>
45
+ ));
46
+ const first = container.querySelector('[data-default-size]') as HTMLElement;
47
+ expect(first?.dataset.defaultSize).toBe('280');
48
+ });
49
+ });
50
+
51
+ describe('ResizableHandle dblclick resets adjacent panels to defaults', () => {
52
+ it('restores both adjacent panels flex-basis from their default sizes on dblclick', () => {
53
+ const { container } = render(() => (
54
+ <ResizablePanelGroup orientation="horizontal">
55
+ <ResizablePanel defaultSize={30}>a</ResizablePanel>
56
+ <ResizableHandle />
57
+ <ResizablePanel defaultSize={70}>b</ResizablePanel>
58
+ </ResizablePanelGroup>
59
+ ));
60
+ const handle = container.querySelector('[role="separator"]') as HTMLElement;
61
+ const prev = handle.previousElementSibling as HTMLElement;
62
+ const next = handle.nextElementSibling as HTMLElement;
63
+
64
+ // Simulate the result of a drag: panels carry pixel flex-basis inline.
65
+ prev.style.flexBasis = '120px';
66
+ next.style.flexBasis = '480px';
67
+
68
+ handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
69
+
70
+ expect(prev.style.flexBasis).toBe('30%');
71
+ expect(next.style.flexBasis).toBe('70%');
72
+ });
73
+
74
+ it('resets a panel that has no explicit default back to flexible (clears inline basis)', () => {
75
+ const { container } = render(() => (
76
+ <ResizablePanelGroup orientation="horizontal">
77
+ <ResizablePanel defaultSize={30}>a</ResizablePanel>
78
+ <ResizableHandle />
79
+ <ResizablePanel>b</ResizablePanel>
80
+ </ResizablePanelGroup>
81
+ ));
82
+ const handle = container.querySelector('[role="separator"]') as HTMLElement;
83
+ const prev = handle.previousElementSibling as HTMLElement;
84
+ const next = handle.nextElementSibling as HTMLElement;
85
+
86
+ prev.style.flexBasis = '120px';
87
+ next.style.flexBasis = '480px';
88
+ next.style.flexGrow = '0';
89
+ next.style.flexShrink = '0';
90
+
91
+ handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
92
+
93
+ expect(prev.style.flexBasis).toBe('30%');
94
+ // The flexible panel (no default) returns to growing: basis cleared, grow:1.
95
+ expect(next.style.flexBasis).toBe('');
96
+ expect(next.style.flexGrow).toBe('1');
97
+ });
98
+
99
+ it('does nothing on a static (locked) handle', () => {
100
+ const { container } = render(() => (
101
+ <ResizablePanelGroup orientation="horizontal">
102
+ <ResizablePanel defaultSize={30}>a</ResizablePanel>
103
+ <ResizableHandle static />
104
+ <ResizablePanel defaultSize={70}>b</ResizablePanel>
105
+ </ResizablePanelGroup>
106
+ ));
107
+ const handle = container.querySelector('[role="separator"]') as HTMLElement;
108
+ const prev = handle.previousElementSibling as HTMLElement;
109
+ prev.style.flexBasis = '120px';
110
+
111
+ handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
112
+
113
+ expect(prev.style.flexBasis).toBe('120px');
114
+ });
115
+ });
116
+
117
+ describe('<kai-resizable> size preservation across content-only re-renders', () => {
118
+ beforeAll(async () => {
119
+ // Importing registers the custom element. jsdom supports customElements.
120
+ await import('../elements/resizable');
121
+ });
122
+
123
+ function mount(html: string): HTMLElement {
124
+ const host = document.createElement('div');
125
+ host.innerHTML = html;
126
+ document.body.appendChild(host);
127
+ return host;
128
+ }
129
+
130
+ afterEach(() => {
131
+ document.body.innerHTML = '';
132
+ });
133
+
134
+ it('does not clobber a dragged panel flex-basis when an item child re-renders content', async () => {
135
+ const host = mount(`
136
+ <kai-resizable orientation="horizontal" style="height:300px;width:600px">
137
+ <kai-resizable-item size="30%"><p id="content">hi</p></kai-resizable-item>
138
+ <kai-resizable-item><p>right</p></kai-resizable-item>
139
+ </kai-resizable>
140
+ `);
141
+ const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
142
+ // Let solid-element upgrade + onMount run.
143
+ await Promise.resolve();
144
+ await new Promise((r) => setTimeout(r, 0));
145
+
146
+ const panel = el.shadowRoot.querySelector('[data-panel]') as HTMLElement;
147
+ expect(panel).toBeTruthy();
148
+
149
+ // Simulate a settled drag: the handle writes inline flex-basis on the panel.
150
+ panel.style.flexBasis = '55%';
151
+
152
+ // Now mutate ONLY the slotted content of an existing item (no add/remove).
153
+ const content = host.querySelector('#content') as HTMLElement;
154
+ content.textContent = 'a new chat message arrived';
155
+
156
+ // Allow the MutationObserver microtask + any re-render to flush.
157
+ await Promise.resolve();
158
+ await new Promise((r) => setTimeout(r, 0));
159
+
160
+ const panelAfter = el.shadowRoot.querySelector('[data-panel]') as HTMLElement;
161
+ // The dragged basis must survive (the bug reset it to "30%").
162
+ expect(panelAfter.style.flexBasis).toBe('55%');
163
+ });
164
+
165
+ it('reflects each item default size onto its panel for handle dblclick-reset', async () => {
166
+ const host = mount(`
167
+ <kai-resizable orientation="horizontal" style="height:300px;width:600px">
168
+ <kai-resizable-item size="30%"><p>a</p></kai-resizable-item>
169
+ <kai-resizable-item><p>b</p></kai-resizable-item>
170
+ </kai-resizable>
171
+ `);
172
+ const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
173
+ await Promise.resolve();
174
+ await new Promise((r) => setTimeout(r, 0));
175
+
176
+ const panels = el.shadowRoot.querySelectorAll('[data-panel]');
177
+ expect((panels[0] as HTMLElement).dataset.defaultSizePct).toBe('30');
178
+ // Second panel has no explicit default → no reflected default size.
179
+ expect((panels[1] as HTMLElement).dataset.defaultSizePct).toBeUndefined();
180
+ });
181
+
182
+ it('keeps the ORIGINAL default for dblclick-reset even after sizes are persisted', async () => {
183
+ const host = mount(`
184
+ <kai-resizable orientation="horizontal" style="height:300px;width:600px">
185
+ <kai-resizable-item size="30%"><p>a</p></kai-resizable-item>
186
+ <kai-resizable-item size="70%"><p>b</p></kai-resizable-item>
187
+ </kai-resizable>
188
+ `);
189
+ const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
190
+ await Promise.resolve();
191
+ await new Promise((r) => setTimeout(r, 0));
192
+
193
+ // Simulate persistence overwriting the live `size` attribute after a drag.
194
+ const item = host.querySelector('kai-resizable-item') as HTMLElement;
195
+ item.setAttribute('size', '55%');
196
+ await Promise.resolve();
197
+ await new Promise((r) => setTimeout(r, 0));
198
+
199
+ const panel = el.shadowRoot.querySelector('[data-panel]') as HTMLElement;
200
+ // The reflected DEFAULT (for dblclick-reset) stays the original 30, not 55.
201
+ expect(panel.dataset.defaultSizePct).toBe('30');
202
+ });
203
+
204
+ it('still re-initializes when an item is actually added', async () => {
205
+ const host = mount(`
206
+ <kai-resizable orientation="horizontal" style="height:300px;width:600px">
207
+ <kai-resizable-item size="30%"><p>a</p></kai-resizable-item>
208
+ <kai-resizable-item><p>b</p></kai-resizable-item>
209
+ </kai-resizable>
210
+ `);
211
+ const el = host.querySelector('kai-resizable') as HTMLElement & { shadowRoot: ShadowRoot };
212
+ await Promise.resolve();
213
+ await new Promise((r) => setTimeout(r, 0));
214
+
215
+ expect(el.shadowRoot.querySelectorAll('[data-panel]').length).toBe(2);
216
+
217
+ const item = document.createElement('kai-resizable-item');
218
+ item.innerHTML = '<p>c</p>';
219
+ el.appendChild(item);
220
+
221
+ await Promise.resolve();
222
+ await new Promise((r) => setTimeout(r, 0));
223
+
224
+ expect(el.shadowRoot.querySelectorAll('[data-panel]').length).toBe(3);
225
+ });
226
+ });
@@ -0,0 +1,103 @@
1
+ import { splitProps, Show, For, createEffect, on } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { useTextStream } from '../primitives/use-text-stream';
4
+
5
+ export type Mode = 'typewriter' | 'fade';
6
+
7
+ export interface ResponseStreamProps {
8
+ textStream: string | AsyncIterable<string>;
9
+ mode?: Mode;
10
+ speed?: number;
11
+ class?: string;
12
+ onComplete?: () => void;
13
+ as?: string;
14
+ fadeDuration?: number;
15
+ segmentDelay?: number;
16
+ characterChunkSize?: number;
17
+ }
18
+
19
+ function ResponseStream(props: ResponseStreamProps) {
20
+ const [local] = splitProps(props, [
21
+ 'textStream', 'mode', 'speed', 'class', 'onComplete',
22
+ 'as', 'fadeDuration', 'segmentDelay', 'characterChunkSize',
23
+ ]);
24
+
25
+ const mode = () => local.mode ?? 'typewriter';
26
+ const speed = () => local.speed ?? 20;
27
+
28
+ const stream = useTextStream({
29
+ mode: mode(),
30
+ speed: speed(),
31
+ characterChunkSize: local.characterChunkSize,
32
+ fadeDuration: local.fadeDuration,
33
+ });
34
+
35
+ createEffect(on(
36
+ () => local.textStream,
37
+ (source) => {
38
+ if (source) stream.startStreaming(source);
39
+ }
40
+ ));
41
+
42
+ createEffect(on(
43
+ () => stream.isComplete(),
44
+ (complete) => {
45
+ if (complete) local.onComplete?.();
46
+ }
47
+ ));
48
+
49
+ const fadeStyle = () => {
50
+ const dur = local.fadeDuration ?? Math.round(1000 / Math.sqrt(Math.min(100, Math.max(1, speed()))));
51
+ return `
52
+ @keyframes fadeIn {
53
+ from { opacity: 0; }
54
+ to { opacity: 1; }
55
+ }
56
+ .fade-segment {
57
+ display: inline-block;
58
+ opacity: 0;
59
+ animation: fadeIn ${dur}ms ease-out forwards;
60
+ }
61
+ .fade-segment-space {
62
+ white-space: pre;
63
+ }
64
+ `;
65
+ };
66
+
67
+ const segDelay = () => {
68
+ if (typeof local.segmentDelay === 'number') return Math.max(0, local.segmentDelay);
69
+ const normalizedSpeed = Math.min(100, Math.max(1, speed()));
70
+ return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed)));
71
+ };
72
+
73
+ return (
74
+ <div class={local.class}>
75
+ <Show
76
+ when={mode() === 'fade'}
77
+ fallback={<>{stream.displayedText()}</>}
78
+ >
79
+ <style>{fadeStyle()}</style>
80
+ <div class="relative">
81
+ <For each={stream.segments()}>
82
+ {(segment, idx) => {
83
+ const isWhitespace = () => /^\s+$/.test(segment.text);
84
+ return (
85
+ <span
86
+ class={cn(
87
+ 'fade-segment',
88
+ isWhitespace() && 'fade-segment-space'
89
+ )}
90
+ style={{ 'animation-delay': `${idx() * segDelay()}ms` }}
91
+ >
92
+ {segment.text}
93
+ </span>
94
+ );
95
+ }}
96
+ </For>
97
+ </div>
98
+ </Show>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export { ResponseStream };
@@ -0,0 +1,100 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { For } from 'solid-js';
3
+ import { ScrollButton } from './scroll-button';
4
+ import { ChatContainerRoot, ChatContainerContent } from './chat-container';
5
+ import { componentDescription } from '../stories/docs/element-controls';
6
+
7
+ /**
8
+ * `ScrollButton` reads scroll state from the surrounding `ChatContainerRoot`
9
+ * context, so every story wraps it in a scrollable container. It is hidden
10
+ * (faded out) while pinned to the bottom and appears once you scroll up.
11
+ */
12
+ function ScrollDemo(props: { variant?: 'outline' | 'ghost' | 'default'; size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm'; class?: string }) {
13
+ return (
14
+ <div class="relative h-64 w-80 overflow-hidden rounded-lg border">
15
+ <ChatContainerRoot class="h-full p-4">
16
+ <ChatContainerContent class="gap-2">
17
+ <For each={Array.from({ length: 20 })}>
18
+ {(_, i) => (
19
+ <div class="rounded-md bg-muted/40 px-3 py-2 text-sm">Message {i() + 1}</div>
20
+ )}
21
+ </For>
22
+ </ChatContainerContent>
23
+ {/* ScrollButton must live INSIDE ChatContainerRoot (it reads that
24
+ context); it's absolutely positioned relative to the outer .relative
25
+ box, so it stays pinned and doesn't scroll with the content. */}
26
+ <div class="absolute inset-x-0 bottom-3 flex justify-center">
27
+ <ScrollButton variant={props.variant} size={props.size} class={props.class} />
28
+ </div>
29
+ </ChatContainerRoot>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ const meta = {
35
+ title: 'Solid (Advanced)/Elements/ScrollButton',
36
+ component: ScrollButton,
37
+ tags: ['autodocs'],
38
+ parameters: {
39
+ layout: 'padded',
40
+ docs: {
41
+ description: componentDescription([
42
+ 'A floating "scroll to bottom" button wired to the enclosing `ChatContainerRoot`. It calls `scrollToBottom()` on click and animates in/out based on `isAtBottom`.',
43
+ '**When to use:** in a scrollable message log, to let the user jump back to the latest message after scrolling up. It hides itself automatically while pinned to the bottom.',
44
+ '**How to use:** render it inside a `ChatContainerRoot` (it consumes that context). Position it with absolute layout and optionally restyle via `variant`, `size`, and `class`.',
45
+ '**Placement:** overlaid near the bottom-center of the chat message area.',
46
+ ]),
47
+ controls: { exclude: ['use:eventListener'] },
48
+ },
49
+ },
50
+ argTypes: {
51
+ variant: {
52
+ control: 'select',
53
+ options: ['default', 'ghost', 'outline'],
54
+ description: 'Underlying button visual emphasis.',
55
+ table: { defaultValue: { summary: 'outline' } },
56
+ },
57
+ size: {
58
+ control: 'select',
59
+ options: ['sm', 'md', 'lg', 'icon', 'icon-sm'],
60
+ description: 'Underlying button size preset.',
61
+ table: { defaultValue: { summary: 'sm' } },
62
+ },
63
+ class: {
64
+ control: 'text',
65
+ description: 'Additional classes merged onto the button.',
66
+ },
67
+ },
68
+ args: {
69
+ variant: 'outline',
70
+ size: 'sm',
71
+ },
72
+ render: (args) => <ScrollDemo variant={args.variant} size={args.size} class={args.class} />,
73
+ } satisfies Meta<typeof ScrollButton>;
74
+
75
+ export default meta;
76
+ type Story = StoryObj<typeof meta>;
77
+
78
+ const IMPORT = `import { ScrollButton, ChatContainerRoot, ChatContainerContent } from '@kitn.ai/ui';`;
79
+ const src = (code: string) => ({
80
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
81
+ });
82
+
83
+ /** Interactive playground — scroll up to reveal the button; tweak `variant`/`size`. */
84
+ export const Playground: Story = {
85
+ ...src(`<div class="relative">
86
+ <ChatContainerRoot class="h-full overflow-y-auto">
87
+ <ChatContainerContent>{/* messages */}</ChatContainerContent>
88
+ {/* inside the Root (reads its context); absolutely positioned to stay pinned */}
89
+ <div class="absolute inset-x-0 bottom-3 flex justify-center">
90
+ <ScrollButton />
91
+ </div>
92
+ </ChatContainerRoot>
93
+ </div>`),
94
+ };
95
+
96
+ /** Ghost variant overlaid in a chat area (showcase). */
97
+ export const Ghost: Story = {
98
+ args: { variant: 'ghost' },
99
+ ...src(`<ScrollButton variant="ghost" />`),
100
+ };
@@ -0,0 +1,34 @@
1
+ import { cn } from '../utils/cn';
2
+ import { Button } from '../ui/button';
3
+ import { useChatContainer } from './chat-container';
4
+ import { ChevronDown } from 'lucide-solid';
5
+
6
+ export interface ScrollButtonProps {
7
+ class?: string;
8
+ variant?: 'outline' | 'ghost' | 'default';
9
+ size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm';
10
+ }
11
+
12
+ function ScrollButton(props: ScrollButtonProps) {
13
+ const { isAtBottom, scrollToBottom } = useChatContainer();
14
+
15
+ return (
16
+ <Button
17
+ variant={props.variant ?? 'outline'}
18
+ size={props.size ?? 'sm'}
19
+ aria-label="Scroll to bottom"
20
+ class={cn(
21
+ 'h-10 w-10 rounded-full transition-all duration-150 ease-out',
22
+ !isAtBottom()
23
+ ? 'translate-y-0 scale-100 opacity-100'
24
+ : 'pointer-events-none translate-y-4 scale-95 opacity-0',
25
+ props.class
26
+ )}
27
+ onClick={() => scrollToBottom()}
28
+ >
29
+ <ChevronDown class="h-5 w-5" />
30
+ </Button>
31
+ );
32
+ }
33
+
34
+ export { ScrollButton };
@@ -0,0 +1,163 @@
1
+ import type { Meta, StoryObj } from "storybook-solidjs-vite";
2
+ import { fn } from "storybook/test";
3
+ import { createSignal } from "solid-js";
4
+ import { SlashCommand, type SlashCommandItem } from "./slash-command";
5
+ import { PromptInput, PromptInputTextarea } from "./prompt-input";
6
+ import { ChatConfig } from "../primitives/chat-config";
7
+ import { componentDescription } from '../stories/docs/element-controls';
8
+
9
+ const skillCommands: SlashCommandItem[] = [
10
+ { id: "caveman", label: "Caveman", description: "Ultra-compressed terse responses", category: "Skills" },
11
+ { id: "detailed", label: "Detailed", description: "Thorough, comprehensive responses", category: "Skills" },
12
+ { id: "eli5", label: "ELI5", description: "Explain simply, avoid jargon", category: "Skills" },
13
+ { id: "socratic", label: "Socratic", description: "Ask guiding questions", category: "Skills" },
14
+ { id: "concise", label: "Concise", description: "Short, direct answers", category: "Skills" },
15
+ ];
16
+
17
+ const mixedCommands: SlashCommandItem[] = [
18
+ ...skillCommands,
19
+ { id: "clear", label: "Clear", description: "Clear conversation history", category: "Actions" },
20
+ { id: "export", label: "Export", description: "Export conversation as markdown", category: "Actions" },
21
+ ];
22
+
23
+ /**
24
+ * `SlashCommand` reads the input value from the enclosing `PromptInput`
25
+ * context, so every story mounts it inside a `PromptInput`. The popup opens
26
+ * when the input starts with `/`.
27
+ */
28
+ function SlashDemo(props: { commands: SlashCommandItem[]; activeIds?: string[]; compact?: boolean; onSelect: (cmd: SlashCommandItem) => void }) {
29
+ const [value, setValue] = createSignal("/");
30
+ return (
31
+ <ChatConfig proseSize="sm">
32
+ <div style={{ width: "420px" }} class="bg-card rounded-lg p-4">
33
+ <div class="relative">
34
+ <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue("")}>
35
+ <PromptInputTextarea placeholder="Type / for commands..." class="min-h-[36px] pt-2 pl-3" />
36
+ <SlashCommand
37
+ commands={props.commands}
38
+ activeIds={props.activeIds}
39
+ compact={props.compact}
40
+ onSelect={(cmd) => {
41
+ props.onSelect(cmd);
42
+ setValue("");
43
+ }}
44
+ />
45
+ </PromptInput>
46
+ </div>
47
+ <p class="text-xs text-muted-foreground mt-2 text-center">
48
+ Type <code class="bg-muted px-1 rounded">/</code> to see commands. Arrow keys + Tab/Enter to select.
49
+ </p>
50
+ </div>
51
+ </ChatConfig>
52
+ );
53
+ }
54
+
55
+ const meta = {
56
+ title: "Solid (Advanced)/Elements/SlashCommand",
57
+ component: SlashCommand,
58
+ tags: ["autodocs"],
59
+ parameters: {
60
+ layout: "centered",
61
+ docs: {
62
+ description: componentDescription([
63
+ "A keyboard-navigable command palette that pops above a `PromptInput` when the user types `/`. Filters and groups commands by category, with arrow-key navigation and Tab/Enter to select.",
64
+ "**When to use:** to offer slash commands or toggleable skills/modes directly from the prompt input as the user types `/name`.",
65
+ "**How to use:** render it as a child of `PromptInput` (it consumes that context). Pass `commands`, handle `onSelect`, and optionally pass `activeIds` to mark active toggles. Set `compact={false}` for two-line rows.",
66
+ "**Placement:** inside the relative-positioned `PromptInput`, absolutely anchored to the top of the textarea.",
67
+ ]),
68
+ controls: { exclude: ["use:eventListener"] },
69
+ },
70
+ },
71
+ argTypes: {
72
+ commands: {
73
+ control: "object",
74
+ description: "List of selectable commands ({ id, label, description?, category? }).",
75
+ },
76
+ activeIds: {
77
+ control: "object",
78
+ description: "IDs currently marked active — selecting an active command toggles it off.",
79
+ },
80
+ compact: {
81
+ control: "boolean",
82
+ description: "Single-line rows (label + description side by side). When false, two-line rows.",
83
+ table: { defaultValue: { summary: "true" } },
84
+ },
85
+ onSelect: {
86
+ action: "select",
87
+ description: "Fired with the chosen command when an item is selected.",
88
+ table: { category: "Events" },
89
+ },
90
+ class: { control: "text", description: "Additional classes on the popup container." },
91
+ },
92
+ args: {
93
+ commands: skillCommands,
94
+ compact: true,
95
+ activeIds: [],
96
+ onSelect: fn(),
97
+ },
98
+ render: (args) => (
99
+ <SlashDemo commands={args.commands} activeIds={args.activeIds} compact={args.compact} onSelect={args.onSelect} />
100
+ ),
101
+ } satisfies Meta<typeof SlashCommand>;
102
+
103
+ export default meta;
104
+ type Story = StoryObj<typeof meta>;
105
+
106
+ const IMPORT = `import { SlashCommand, PromptInput, PromptInputTextarea } from '@kitn.ai/ui';`;
107
+ const src = (code: string) => ({
108
+ parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
109
+ });
110
+
111
+ /** Interactive playground — type `/` in the input; tweak `commands`/`compact`/`activeIds`. */
112
+ export const Playground: Story = {
113
+ ...src(`<PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}>
114
+ <PromptInputTextarea placeholder="Type / for commands..." />
115
+ <SlashCommand commands={commands} onSelect={(cmd) => { /* apply */ }} />
116
+ </PromptInput>`),
117
+ };
118
+
119
+ /** Compact single-line rows (default). */
120
+ export const Compact: Story = {
121
+ args: { compact: true },
122
+ ...src(`<SlashCommand commands={commands} onSelect={handleSelect} />`),
123
+ };
124
+
125
+ /** Two-line rows with label above description. */
126
+ export const Expanded: Story = {
127
+ args: { compact: false },
128
+ ...src(`<SlashCommand commands={commands} compact={false} onSelect={handleSelect} />`),
129
+ };
130
+
131
+ /** Commands grouped by category (Skills + Actions). */
132
+ export const WithCategories: Story = {
133
+ args: { commands: mixedCommands },
134
+ ...src(`const commands = [
135
+ { id: 'eli5', label: 'ELI5', description: 'Explain simply', category: 'Skills' },
136
+ { id: 'clear', label: 'Clear', description: 'Clear conversation', category: 'Actions' },
137
+ ];
138
+
139
+ <SlashCommand commands={commands} onSelect={handleSelect} />`),
140
+ };
141
+
142
+ /** Toggleable skills — active IDs are marked and toggle off when reselected (showcase). */
143
+ export const ActiveToggles: Story = {
144
+ render: () => {
145
+ const [selected, setSelected] = createSignal<string[]>(["eli5"]);
146
+ return (
147
+ <SlashDemo
148
+ commands={skillCommands}
149
+ activeIds={selected()}
150
+ onSelect={(cmd) =>
151
+ setSelected((prev) =>
152
+ prev.includes(cmd.id) ? prev.filter((id) => id !== cmd.id) : [...prev, cmd.id],
153
+ )
154
+ }
155
+ />
156
+ );
157
+ },
158
+ ...src(`<SlashCommand
159
+ commands={skillCommands}
160
+ activeIds={selected()}
161
+ onSelect={(cmd) => toggle(cmd.id)}
162
+ />`),
163
+ };