@kitn.ai/ui 0.15.0 → 0.16.0

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 (366) hide show
  1. package/README.md +9 -0
  2. package/bin/mcp.js +22 -0
  3. package/dist/bash-InADTalH.js +6 -1
  4. package/dist/components/artifact.d.ts +58 -0
  5. package/dist/components/attachments.d.ts +67 -0
  6. package/dist/components/card-fallback.d.ts +10 -0
  7. package/dist/components/card-renderer.d.ts +13 -0
  8. package/dist/components/card.d.ts +28 -0
  9. package/dist/components/chain-of-thought.d.ts +29 -0
  10. package/dist/components/chat-container.d.ts +19 -0
  11. package/dist/components/chat-scope-picker.d.ts +9 -0
  12. package/dist/components/chat-thread.d.ts +93 -0
  13. package/dist/components/checkpoint.d.ts +20 -0
  14. package/dist/components/choice-card.d.ts +77 -0
  15. package/dist/components/code-block.d.ts +16 -0
  16. package/dist/components/confirm-card.d.ts +57 -0
  17. package/dist/components/context.d.ts +65 -0
  18. package/dist/components/conversation-item.d.ts +8 -0
  19. package/dist/components/conversation-list.d.ts +11 -0
  20. package/dist/components/embed.d.ts +21 -0
  21. package/dist/components/empty.d.ts +43 -0
  22. package/dist/components/feedback-bar.d.ts +45 -0
  23. package/dist/components/file-tree.d.ts +57 -0
  24. package/dist/components/file-upload.d.ts +16 -0
  25. package/dist/components/form-widgets.d.ts +31 -0
  26. package/dist/components/form.d.ts +103 -0
  27. package/dist/components/image.d.ts +12 -0
  28. package/dist/components/link-preview.d.ts +23 -0
  29. package/dist/components/loader.d.ts +60 -0
  30. package/dist/components/markdown.d.ts +8 -0
  31. package/dist/components/message-skills.d.ts +15 -0
  32. package/dist/components/message.d.ts +54 -0
  33. package/dist/components/model-switcher.d.ts +8 -0
  34. package/dist/components/prompt-input.d.ts +38 -0
  35. package/dist/components/prompt-suggestion.d.ts +13 -0
  36. package/dist/components/reasoning.d.ts +20 -0
  37. package/dist/components/response-stream.d.ts +14 -0
  38. package/dist/components/scroll-button.d.ts +7 -0
  39. package/dist/components/slash-command.d.ts +15 -0
  40. package/dist/components/source.d.ts +23 -0
  41. package/dist/components/tasks-card.d.ts +71 -0
  42. package/dist/components/text-shimmer.d.ts +9 -0
  43. package/dist/components/thinking-bar.d.ts +10 -0
  44. package/dist/components/tool.d.ts +16 -0
  45. package/dist/components/use-card-resolution.d.ts +12 -0
  46. package/dist/components/voice-input.d.ts +7 -0
  47. package/dist/core-AYMC6_lb.js +5874 -12
  48. package/dist/css-M7EaDHN_.js +6 -1
  49. package/dist/elements/artifact.d.ts +1 -0
  50. package/dist/elements/attachments.d.ts +1 -0
  51. package/dist/elements/card.d.ts +1 -0
  52. package/dist/elements/cards.d.ts +0 -0
  53. package/dist/elements/chain-of-thought.d.ts +13 -0
  54. package/dist/elements/chat-scope-picker.d.ts +1 -0
  55. package/dist/elements/chat-types.d.ts +41 -0
  56. package/dist/elements/chat-workspace.d.ts +1 -0
  57. package/dist/elements/chat.d.ts +1 -0
  58. package/dist/elements/checkpoint.d.ts +1 -0
  59. package/dist/elements/choice.d.ts +1 -0
  60. package/dist/elements/code-block.d.ts +1 -0
  61. package/dist/elements/confirm-card.d.ts +1 -0
  62. package/dist/elements/context-meter.d.ts +1 -0
  63. package/dist/elements/conversation-list.d.ts +11 -0
  64. package/dist/elements/css.d.ts +1 -0
  65. package/dist/elements/default-input.d.ts +40 -0
  66. package/dist/elements/define.d.ts +42 -0
  67. package/dist/elements/embed.d.ts +1 -0
  68. package/dist/elements/empty.d.ts +1 -0
  69. package/dist/elements/feedback-bar.d.ts +1 -0
  70. package/dist/elements/file-tree.d.ts +1 -0
  71. package/dist/elements/file-upload.d.ts +1 -0
  72. package/dist/elements/form.d.ts +1 -0
  73. package/dist/elements/image.d.ts +1 -0
  74. package/dist/elements/link-preview.d.ts +1 -0
  75. package/dist/elements/loader.d.ts +1 -0
  76. package/dist/elements/markdown.d.ts +1 -0
  77. package/dist/elements/message-skills.d.ts +17 -0
  78. package/dist/elements/message.d.ts +1 -0
  79. package/dist/elements/model-switcher.d.ts +11 -0
  80. package/dist/elements/popover.d.ts +1 -0
  81. package/dist/elements/prompt-input.d.ts +9 -0
  82. package/dist/elements/prompt-suggestions.d.ts +7 -0
  83. package/dist/elements/reasoning.d.ts +1 -0
  84. package/dist/elements/register-impl.d.ts +0 -0
  85. package/dist/elements/register.d.ts +3 -0
  86. package/dist/elements/remote.d.ts +1 -0
  87. package/dist/elements/resizable.d.ts +17 -0
  88. package/dist/elements/response-stream.d.ts +1 -0
  89. package/dist/elements/scroll-button.d.ts +1 -0
  90. package/dist/elements/source.d.ts +18 -0
  91. package/dist/elements/switch.d.ts +1 -0
  92. package/dist/elements/tasks.d.ts +1 -0
  93. package/dist/elements/text-shimmer.d.ts +1 -0
  94. package/dist/elements/thinking-bar.d.ts +1 -0
  95. package/dist/elements/tool.d.ts +1 -0
  96. package/dist/elements/voice-input.d.ts +1 -0
  97. package/dist/elements.d.ts +732 -0
  98. package/dist/engine-javascript-vq0WuIJl.js +2516 -14
  99. package/dist/github-dark-dimmed-DUshB20C.js +4 -1
  100. package/dist/github-light-JYsPkUQd.js +4 -1
  101. package/dist/html-CPZ3oZQ7.js +10 -1
  102. package/dist/index.d.ts +112 -2
  103. package/dist/index.js +13551 -0
  104. package/dist/javascript-C25yR2R2.js +6 -1
  105. package/dist/json-DxJze_jm.js +6 -1
  106. package/dist/kitn-chat.es.js +1 -77
  107. package/dist/llms/llms-full.txt +1 -1
  108. package/dist/llms/llms.txt +1 -1
  109. package/dist/mcp.es.js +2140 -0
  110. package/dist/primitives/card-contract.d.ts +103 -0
  111. package/dist/primitives/card-host.d.ts +12 -0
  112. package/dist/primitives/card-registry.d.ts +17 -0
  113. package/dist/primitives/card-resolution.d.ts +8 -0
  114. package/dist/primitives/card-routing.d.ts +12 -0
  115. package/dist/primitives/card-validate.d.ts +25 -0
  116. package/dist/primitives/chat-config.d.ts +31 -0
  117. package/dist/primitives/controllable.d.ts +13 -0
  118. package/dist/primitives/embed-providers.d.ts +65 -0
  119. package/dist/primitives/highlighter.d.ts +23 -0
  120. package/dist/primitives/link-preview.d.ts +47 -0
  121. package/dist/primitives/pdf-preview.d.ts +50 -0
  122. package/dist/primitives/use-auto-resize.d.ts +8 -0
  123. package/dist/primitives/use-resize-observer.d.ts +1 -0
  124. package/dist/primitives/use-stick-to-bottom.d.ts +5 -0
  125. package/dist/primitives/use-text-stream.d.ts +19 -0
  126. package/dist/primitives/use-voice-recorder.d.ts +9 -0
  127. package/dist/react/index.d.ts +1118 -0
  128. package/dist/react/runtime.d.ts +15 -0
  129. package/dist/react.js +265 -0
  130. package/dist/register-impl-wCsFPoh9.js +77 -0
  131. package/dist/register-ujhr1nBS.js +1 -0
  132. package/dist/remote/host-embed.d.ts +33 -0
  133. package/dist/remote/index.d.ts +2 -0
  134. package/dist/remote/origin.d.ts +5 -0
  135. package/dist/remote/provider-runtime.d.ts +27 -0
  136. package/dist/remote/provider.d.ts +2 -0
  137. package/dist/remote/validate.d.ts +4 -0
  138. package/dist/remote/version.d.ts +4 -0
  139. package/dist/remote/wire.d.ts +53 -0
  140. package/dist/svelte--5p79yCD.js +15 -1
  141. package/dist/tsx-B8rCNbgL.js +6 -1
  142. package/dist/types.d.ts +42 -0
  143. package/dist/typescript-RycA9KXf.js +6 -1
  144. package/dist/ui/action-icons.d.ts +11 -0
  145. package/dist/ui/avatar.d.ts +8 -0
  146. package/dist/ui/badge.d.ts +9 -0
  147. package/dist/ui/button.d.ts +10 -0
  148. package/dist/ui/collapsible.d.ts +27 -0
  149. package/dist/ui/dropdown.d.ts +20 -0
  150. package/dist/ui/hover-card.d.ts +28 -0
  151. package/dist/ui/overlay.d.ts +67 -0
  152. package/dist/ui/popover.d.ts +32 -0
  153. package/dist/ui/resizable.d.ts +76 -0
  154. package/dist/ui/scroll-area.d.ts +5 -0
  155. package/dist/ui/separator.d.ts +5 -0
  156. package/dist/ui/skeleton.d.ts +5 -0
  157. package/dist/ui/switch.d.ts +19 -0
  158. package/dist/ui/textarea.d.ts +6 -0
  159. package/dist/ui/tooltip.d.ts +8 -0
  160. package/dist/utils/cn.d.ts +2 -0
  161. package/dist/vue-BmIZj4XD.js +33 -1
  162. package/frameworks/react/index.tsx +4 -0
  163. package/frameworks/react/runtime.tsx +16 -2
  164. package/llms-full.txt +1 -1
  165. package/llms.txt +1 -1
  166. package/package.json +29 -11
  167. package/src/agent-tooling/archetypes.ts +46 -0
  168. package/src/agent-tooling/integrations/cloudflare.ts +88 -0
  169. package/src/agent-tooling/integrations/langgraph.ts +60 -0
  170. package/src/agent-tooling/integrations/mastra.ts +27 -0
  171. package/src/agent-tooling/integrations/mock.ts +34 -0
  172. package/src/agent-tooling/integrations/ollama.ts +47 -0
  173. package/src/agent-tooling/integrations/openrouter.ts +26 -0
  174. package/src/agent-tooling/integrations/pi.ts +40 -0
  175. package/src/agent-tooling/integrations/pydantic-ai.ts +52 -0
  176. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +44 -0
  177. package/src/agent-tooling/mcp/manifest.ts +128 -0
  178. package/src/agent-tooling/mcp/server.ts +59 -0
  179. package/src/agent-tooling/mcp/stdio.ts +18 -0
  180. package/src/agent-tooling/mcp/tools/debug.ts +305 -0
  181. package/src/agent-tooling/mcp/tools/reference.ts +132 -0
  182. package/src/agent-tooling/mcp/tools/scaffold.ts +1627 -0
  183. package/src/agent-tooling/mcp/tools/theme.ts +340 -0
  184. package/src/agent-tooling/mcp/tools/types.ts +14 -0
  185. package/src/agent-tooling/registry.ts +41 -0
  186. package/src/agent-tooling/types.ts +30 -0
  187. package/src/elements/compiled.css +1 -1
  188. package/src/elements/define.tsx +7 -1
  189. package/src/elements/element-types.d.ts +2 -1
  190. package/src/elements/register-impl.ts +55 -0
  191. package/src/elements/register.ts +17 -51
  192. package/theme.css +4 -2
  193. package/src/components/artifact.stories.tsx +0 -138
  194. package/src/components/attachments.stories.tsx +0 -303
  195. package/src/components/attachments.test.tsx +0 -64
  196. package/src/components/chain-of-thought.stories.tsx +0 -211
  197. package/src/components/chat-container.stories.tsx +0 -187
  198. package/src/components/chat-thread.test.tsx +0 -58
  199. package/src/components/checkpoint.stories.tsx +0 -102
  200. package/src/components/code-block.stories.tsx +0 -150
  201. package/src/components/confirm-card.stories.tsx +0 -74
  202. package/src/components/context.stories.tsx +0 -229
  203. package/src/components/context.test.tsx +0 -133
  204. package/src/components/conversation-item.stories.tsx +0 -125
  205. package/src/components/conversation-list.stories.tsx +0 -133
  206. package/src/components/default-input.test.tsx +0 -111
  207. package/src/components/empty.stories.tsx +0 -434
  208. package/src/components/feedback-bar.stories.tsx +0 -138
  209. package/src/components/feedback-bar.test.tsx +0 -103
  210. package/src/components/file-tree.stories.tsx +0 -73
  211. package/src/components/file-upload.stories.tsx +0 -156
  212. package/src/components/image.stories.tsx +0 -89
  213. package/src/components/loader.stories.tsx +0 -181
  214. package/src/components/markdown.stories.tsx +0 -180
  215. package/src/components/message-action-bar.test.tsx +0 -128
  216. package/src/components/message-narrow.stories.tsx +0 -329
  217. package/src/components/message-skills.stories.tsx +0 -211
  218. package/src/components/message.stories.tsx +0 -281
  219. package/src/components/model-switcher.stories.tsx +0 -123
  220. package/src/components/model-switcher.test.tsx +0 -65
  221. package/src/components/prompt-input.stories.tsx +0 -222
  222. package/src/components/prompt-suggestion.stories.tsx +0 -142
  223. package/src/components/reasoning.stories.tsx +0 -140
  224. package/src/components/resizable.test.tsx +0 -226
  225. package/src/components/scroll-button.stories.tsx +0 -100
  226. package/src/components/slash-command.stories.tsx +0 -163
  227. package/src/components/source-list.test.tsx +0 -152
  228. package/src/components/source.stories.tsx +0 -124
  229. package/src/components/tasks-card.stories.tsx +0 -78
  230. package/src/components/text-shimmer.stories.tsx +0 -87
  231. package/src/components/thinking-bar.stories.tsx +0 -87
  232. package/src/components/tool.stories.tsx +0 -153
  233. package/src/components/voice-input.stories.tsx +0 -83
  234. package/src/elements/artifact.stories.tsx +0 -506
  235. package/src/elements/attachments.stories.tsx +0 -190
  236. package/src/elements/card.stories.tsx +0 -113
  237. package/src/elements/cards.stories.tsx +0 -59
  238. package/src/elements/catalog.stories.tsx +0 -491
  239. package/src/elements/chain-of-thought.declarative.test.tsx +0 -153
  240. package/src/elements/chain-of-thought.stories.tsx +0 -113
  241. package/src/elements/chat-scope-picker.stories.tsx +0 -73
  242. package/src/elements/chat-workspace.stories.tsx +0 -288
  243. package/src/elements/chat.stories.tsx +0 -151
  244. package/src/elements/checkpoint.stories.tsx +0 -72
  245. package/src/elements/choice.stories.tsx +0 -240
  246. package/src/elements/code-block.stories.tsx +0 -83
  247. package/src/elements/composed-shell.stories.tsx +0 -316
  248. package/src/elements/confirm-card.stories.tsx +0 -227
  249. package/src/elements/context-meter.stories.tsx +0 -151
  250. package/src/elements/conversation-list.declarative.test.tsx +0 -137
  251. package/src/elements/conversation-list.stories.tsx +0 -243
  252. package/src/elements/embed.stories.tsx +0 -213
  253. package/src/elements/empty.stories.tsx +0 -111
  254. package/src/elements/feedback-bar.stories.tsx +0 -114
  255. package/src/elements/file-tree.stories.tsx +0 -134
  256. package/src/elements/file-upload.stories.tsx +0 -82
  257. package/src/elements/form.stories.tsx +0 -248
  258. package/src/elements/image.stories.tsx +0 -71
  259. package/src/elements/link-preview.stories.tsx +0 -209
  260. package/src/elements/loader.stories.tsx +0 -88
  261. package/src/elements/markdown.stories.tsx +0 -76
  262. package/src/elements/message-skills.declarative.test.tsx +0 -104
  263. package/src/elements/message-skills.stories.tsx +0 -114
  264. package/src/elements/message.stories.tsx +0 -218
  265. package/src/elements/model-switcher.declarative.test.tsx +0 -130
  266. package/src/elements/model-switcher.stories.tsx +0 -198
  267. package/src/elements/popover.stories.tsx +0 -80
  268. package/src/elements/prompt-input-slash-command.test.tsx +0 -128
  269. package/src/elements/prompt-input-stoppable.test.tsx +0 -88
  270. package/src/elements/prompt-input-toolbar-actions.test.tsx +0 -120
  271. package/src/elements/prompt-input.stories.tsx +0 -331
  272. package/src/elements/prompt-suggestions.declarative.test.tsx +0 -164
  273. package/src/elements/prompt-suggestions.stories.tsx +0 -213
  274. package/src/elements/reasoning.stories.tsx +0 -77
  275. package/src/elements/remote.stories.tsx +0 -314
  276. package/src/elements/resizable.stories.tsx +0 -426
  277. package/src/elements/response-stream.stories.tsx +0 -79
  278. package/src/elements/scroll-button.stories.tsx +0 -202
  279. package/src/elements/scroll-button.test.tsx +0 -148
  280. package/src/elements/source-list.stories.tsx +0 -143
  281. package/src/elements/source.stories.tsx +0 -88
  282. package/src/elements/switch.stories.tsx +0 -56
  283. package/src/elements/tasks.stories.tsx +0 -237
  284. package/src/elements/text-shimmer.stories.tsx +0 -64
  285. package/src/elements/thinking-bar.stories.tsx +0 -72
  286. package/src/elements/tool.stories.tsx +0 -89
  287. package/src/elements/voice-input.stories.tsx +0 -88
  288. package/src/primitives/controllable.test.ts +0 -47
  289. package/src/stories/chat-panel-layout.stories.tsx +0 -145
  290. package/src/stories/chat-scene.tsx +0 -571
  291. package/src/stories/checkpoint-restore.stories.tsx +0 -256
  292. package/src/stories/context-usage.stories.tsx +0 -212
  293. package/src/stories/conversation-with-reasoning.stories.tsx +0 -182
  294. package/src/stories/conversation-with-sources.stories.tsx +0 -471
  295. package/src/stories/docs/Accessibility.mdx +0 -119
  296. package/src/stories/docs/ChildElements.mdx +0 -269
  297. package/src/stories/docs/ForAIAgents.mdx +0 -162
  298. package/src/stories/docs/GettingStarted.mdx +0 -65
  299. package/src/stories/docs/Installation.mdx +0 -83
  300. package/src/stories/docs/Introduction.mdx +0 -50
  301. package/src/stories/docs/SolidJsAdvanced.mdx +0 -17
  302. package/src/stories/docs/Theming.mdx +0 -85
  303. package/src/stories/docs/element-controls.ts +0 -121
  304. package/src/stories/docs/frameworks/Angular.mdx +0 -245
  305. package/src/stories/docs/frameworks/Html.mdx +0 -227
  306. package/src/stories/docs/frameworks/Overview.mdx +0 -47
  307. package/src/stories/docs/frameworks/React.mdx +0 -227
  308. package/src/stories/docs/frameworks/Solid.mdx +0 -173
  309. package/src/stories/docs/frameworks/Svelte.mdx +0 -194
  310. package/src/stories/docs/frameworks/Vue.mdx +0 -238
  311. package/src/stories/docs/generative-ui-overview.mdx +0 -186
  312. package/src/stories/docs/recipes/SpeechToText.mdx +0 -7
  313. package/src/stories/docs/recipes/Streaming.mdx +0 -68
  314. package/src/stories/docs/recipes/TextToSpeech.mdx +0 -35
  315. package/src/stories/docs/theme-editor/canvas.tsx +0 -32
  316. package/src/stories/docs/theme-editor/inspector.tsx +0 -66
  317. package/src/stories/docs/theme-editor/presets.test.ts +0 -32
  318. package/src/stories/docs/theme-editor/presets.ts +0 -64
  319. package/src/stories/docs/theme-editor/theme-css.test.ts +0 -19
  320. package/src/stories/docs/theme-editor/theme-css.ts +0 -15
  321. package/src/stories/docs/theme-editor/theme-editor.tsx +0 -146
  322. package/src/stories/docs/theme-tokens.tsx +0 -174
  323. package/src/stories/examples/ChoosingComponents.mdx +0 -106
  324. package/src/stories/examples/sample-data.ts +0 -79
  325. package/src/stories/examples/usage/checkpoint-restore.ts +0 -156
  326. package/src/stories/examples/usage/context-usage.ts +0 -591
  327. package/src/stories/examples/usage/conversation-with-reasoning.ts +0 -224
  328. package/src/stories/examples/usage/conversation-with-sources.ts +0 -589
  329. package/src/stories/examples/usage/empty-state.ts +0 -144
  330. package/src/stories/examples/usage/full-chat-app.ts +0 -277
  331. package/src/stories/examples/usage/index.ts +0 -63
  332. package/src/stories/examples/usage/message-actions.ts +0 -704
  333. package/src/stories/examples/usage/pattern-centered-conversation.ts +0 -446
  334. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +0 -442
  335. package/src/stories/examples/usage/pattern-docked-widget.ts +0 -479
  336. package/src/stories/examples/usage/prompt-input-variants.ts +0 -1356
  337. package/src/stories/examples/usage/streaming-response.ts +0 -687
  338. package/src/stories/examples/usage/types.ts +0 -33
  339. package/src/stories/full-chat.stories.tsx +0 -18
  340. package/src/stories/message-actions.stories.tsx +0 -230
  341. package/src/stories/pattern-centered-conversation.stories.tsx +0 -93
  342. package/src/stories/pattern-docked-widget.stories.tsx +0 -93
  343. package/src/stories/pattern-empty-state.stories.tsx +0 -76
  344. package/src/stories/prompt-input-variants.stories.tsx +0 -456
  345. package/src/stories/streaming-response.stories.tsx +0 -438
  346. package/src/stories/theme-editor.stories.tsx +0 -16
  347. package/src/stories/token-reference.stories.tsx +0 -18
  348. package/src/stories/typography.stories.tsx +0 -78
  349. package/src/ui/avatar.stories.tsx +0 -103
  350. package/src/ui/badge.stories.tsx +0 -86
  351. package/src/ui/button.stories.tsx +0 -145
  352. package/src/ui/collapsible.stories.tsx +0 -69
  353. package/src/ui/dropdown.stories.tsx +0 -59
  354. package/src/ui/hover-card.stories.tsx +0 -77
  355. package/src/ui/hover-card.test.tsx +0 -30
  356. package/src/ui/overlay.stories.tsx +0 -115
  357. package/src/ui/popover.stories.tsx +0 -81
  358. package/src/ui/popover.test.tsx +0 -99
  359. package/src/ui/resizable.stories.tsx +0 -236
  360. package/src/ui/scroll-area.stories.tsx +0 -50
  361. package/src/ui/separator.stories.tsx +0 -81
  362. package/src/ui/skeleton.stories.tsx +0 -337
  363. package/src/ui/switch.stories.tsx +0 -51
  364. package/src/ui/switch.test.tsx +0 -59
  365. package/src/ui/textarea.stories.tsx +0 -76
  366. package/src/ui/tooltip.stories.tsx +0 -74
@@ -1,226 +0,0 @@
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
- });
@@ -1,100 +0,0 @@
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
- };
@@ -1,163 +0,0 @@
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
- };
@@ -1,152 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import '@testing-library/jest-dom/vitest';
3
- import { render, cleanup } from '@solidjs/testing-library';
4
- import { For } from 'solid-js';
5
- import { Source, SourceTrigger, SourceContent, SourceList } from './source';
6
- import { parseKaiSourceElement } from '../elements/source';
7
-
8
- afterEach(cleanup);
9
-
10
- // ---------------------------------------------------------------------------
11
- // parseKaiSourceElement — unit-tests for the attribute→SourceItem mapping
12
- // ---------------------------------------------------------------------------
13
-
14
- describe('parseKaiSourceElement', () => {
15
- function makeEl(attrs: Record<string, string | null>): Element {
16
- const el = document.createElement('kai-source');
17
- for (const [k, v] of Object.entries(attrs)) {
18
- if (v !== null) el.setAttribute(k, v);
19
- }
20
- return el;
21
- }
22
-
23
- it('maps href, label, headline→title, description', () => {
24
- const el = makeEl({
25
- href: 'https://kitn.dev',
26
- label: 'kitn',
27
- headline: 'kitn — the kit',
28
- description: 'Composable SolidJS chat UI.',
29
- });
30
- expect(parseKaiSourceElement(el)).toEqual({
31
- href: 'https://kitn.dev',
32
- label: 'kitn',
33
- title: 'kitn — the kit',
34
- description: 'Composable SolidJS chat UI.',
35
- showFavicon: false,
36
- });
37
- });
38
-
39
- it('sets showFavicon=true for a bare show-favicon attribute', () => {
40
- const el = makeEl({ href: 'https://kitn.dev', 'show-favicon': '' });
41
- expect(parseKaiSourceElement(el).showFavicon).toBe(true);
42
- });
43
-
44
- it('sets showFavicon=false when show-favicon="false"', () => {
45
- const el = makeEl({ href: 'https://kitn.dev', 'show-favicon': 'false' });
46
- expect(parseKaiSourceElement(el).showFavicon).toBe(false);
47
- });
48
-
49
- it('returns undefined for optional attrs when absent', () => {
50
- const el = makeEl({ href: 'https://kitn.dev' });
51
- const item = parseKaiSourceElement(el);
52
- expect(item.label).toBeUndefined();
53
- expect(item.title).toBeUndefined();
54
- expect(item.description).toBeUndefined();
55
- });
56
-
57
- it('falls back to empty string for missing href', () => {
58
- const el = makeEl({});
59
- expect(parseKaiSourceElement(el).href).toBe('');
60
- });
61
- });
62
-
63
- // ---------------------------------------------------------------------------
64
- // SourceList component — verify citations render as anchor links
65
- // ---------------------------------------------------------------------------
66
-
67
- describe('SourceList with Source children', () => {
68
- it('renders citation links for each source', () => {
69
- const sources = [
70
- { href: 'https://kitn.dev', title: 'kitn', description: 'Composable chat UI.' },
71
- { href: 'https://solidjs.com', title: 'SolidJS', description: 'A reactive UI library.' },
72
- ];
73
-
74
- const { container } = render(() => (
75
- <SourceList>
76
- {sources.map((s) => (
77
- <Source href={s.href}>
78
- <SourceTrigger />
79
- <SourceContent title={s.title} description={s.description} />
80
- </Source>
81
- ))}
82
- </SourceList>
83
- ));
84
-
85
- const links = container.querySelectorAll('a[href]');
86
- // SourceTrigger and SourceContent each render an <a>; 2 sources × 2 = 4
87
- expect(links.length).toBeGreaterThanOrEqual(2);
88
- const hrefs = [...links].map((a) => a.getAttribute('href'));
89
- expect(hrefs).toContain('https://kitn.dev');
90
- expect(hrefs).toContain('https://solidjs.com');
91
- });
92
- });
93
-
94
- // ---------------------------------------------------------------------------
95
- // numbered prop — kai-sources web component behavior reproduced at Solid level
96
- // ---------------------------------------------------------------------------
97
-
98
- describe('numbered citations', () => {
99
- /** Helper: render N sources with explicit numeric labels (mirroring numbered mode). */
100
- function renderNumbered(sources: { href: string; label?: string | number }[]) {
101
- return render(() => (
102
- <SourceList>
103
- <For each={sources}>
104
- {(s, i) => (
105
- <Source href={s.href}>
106
- {/* When numbered is active the element replaces label with i()+1. */}
107
- <SourceTrigger label={i() + 1} />
108
- <SourceContent title="" description="" />
109
- </Source>
110
- )}
111
- </For>
112
- </SourceList>
113
- ));
114
- }
115
-
116
- it('labels N sources 1..N in order', () => {
117
- const sources = [
118
- { href: 'https://kitn.dev' },
119
- { href: 'https://solidjs.com' },
120
- { href: 'https://vitejs.dev' },
121
- ];
122
-
123
- const { container } = renderNumbered(sources);
124
-
125
- // SourceTrigger renders a <span> with the label inside the trigger <a>.
126
- // Grab all trigger anchors — they have target="_blank".
127
- const triggerLinks = [...container.querySelectorAll('a[target="_blank"]')];
128
- // One trigger per source (SourceContent also renders an <a target="_blank">,
129
- // so we narrow to ones whose text content is a number 1–N).
130
- const labels = triggerLinks
131
- .map((a) => a.querySelector('span')?.textContent?.trim())
132
- .filter(Boolean);
133
-
134
- expect(labels).toEqual(['1', '2', '3']);
135
- });
136
-
137
- it('label sequence resets from 1 for each independent list', () => {
138
- const firstSources = [{ href: 'https://kitn.dev' }, { href: 'https://solidjs.com' }];
139
- const secondSources = [{ href: 'https://vitejs.dev' }, { href: 'https://vitest.dev' }];
140
-
141
- const { container: c1 } = renderNumbered(firstSources);
142
- const { container: c2 } = renderNumbered(secondSources);
143
-
144
- const getLabels = (c: HTMLElement) =>
145
- [...c.querySelectorAll('a[target="_blank"]')]
146
- .map((a) => a.querySelector('span')?.textContent?.trim())
147
- .filter(Boolean);
148
-
149
- expect(getLabels(c1)).toEqual(['1', '2']);
150
- expect(getLabels(c2)).toEqual(['1', '2']);
151
- });
152
- });