@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,553 @@
1
+ import { createSignal, createEffect, on, onMount, onCleanup, For, Show, type JSX } from 'solid-js';
2
+ import { defineWebComponent } from './define';
3
+ import { ResizableHandle, normalizeSize } from '../ui/resizable';
4
+
5
+ type Orientation = 'horizontal' | 'vertical';
6
+
7
+ /** Bubbling, composed intent: a descendant asks the nearest enclosing
8
+ * <kai-resizable> to maximize the item containing it (filling, hiding siblings)
9
+ * or to restore. Any panel content may emit it — the protocol is zero-config. */
10
+ export interface KaiMaximizeIntentDetail {
11
+ /** true = maximize the item containing me; false = restore. */
12
+ requested: boolean;
13
+ }
14
+
15
+ /** Composed, non-bubbling notification the group dispatches DOWN onto the
16
+ * affected <kai-resizable-item> (on maximize) or the group host + the formerly
17
+ * maximized item (on restore) so descendant content can sync its affordance. */
18
+ export interface KaiMaximizeStateDetail {
19
+ /** Whether THIS subtree's item is the maximized one. */
20
+ maximized: boolean;
21
+ }
22
+
23
+ /** Event type names for the cross-element maximize protocol. */
24
+ export const KAI_MAXIMIZE_INTENT = 'kai-maximize-intent' as const;
25
+ export const KAI_MAXIMIZE_STATE = 'kai-maximize-state' as const;
26
+
27
+ /** Parsed view of a `<kai-resizable-item>` light child. */
28
+ interface ItemInfo {
29
+ el: HTMLElement;
30
+ size?: string;
31
+ /** Original configured size, captured once and stable across drags (for dblclick-reset). */
32
+ defaultSize?: string;
33
+ min?: string;
34
+ max?: string;
35
+ locked: boolean;
36
+ hidden: boolean;
37
+ }
38
+
39
+ /** Max number of panels a single group lays out (nest for more). */
40
+ const MAX_ITEMS = 3;
41
+
42
+ /** Convert a px-or-% bound to `data-*` attribute values the handle understands. */
43
+ function boundAttrs(value: string | undefined): { pxAttr?: string; pctAttr?: string } {
44
+ if (value === undefined || value === '') return {};
45
+ const t = value.trim();
46
+ if (t.endsWith('px')) return { pxAttr: String(parseFloat(t)) };
47
+ if (t.endsWith('%')) return { pctAttr: String(parseFloat(t)) };
48
+ if (Number.isFinite(Number(t))) return { pctAttr: t };
49
+ return {};
50
+ }
51
+
52
+ interface GroupProps extends Record<string, unknown> {
53
+ /** Layout axis: `horizontal` (row, default) or `vertical` (column). */
54
+ orientation?: Orientation;
55
+ /** Which item index is maximized (null = none). Declarative source of truth. */
56
+ maximizedIndex?: number | null;
57
+ }
58
+
59
+ interface GroupEvents extends Record<string, unknown> {
60
+ /** Fired on drag-end / keyboard resize / visibility change. `detail.sizes` = panel sizes in percent. */
61
+ 'kai-change': { sizes: number[] };
62
+ /** Observe layout maximize state. */
63
+ 'kai-maximize-change': { maximized: boolean; index: number | null };
64
+ }
65
+
66
+ /**
67
+ * `<kai-resizable>` — a composable, resizable multi-panel layout (up to 3 panels)
68
+ * with auto-inserted draggable dividers. It lays out its `<kai-resizable-item>`
69
+ * light children along an axis (`orientation`), reading each item's `size`,
70
+ * `min`, `max`, `locked` and `hidden` attributes via a `MutationObserver`. A
71
+ * divider is interactive only between two unlocked, visible panels. Emits a
72
+ * `change` event (`detail.sizes`, percent) on resize / visibility change.
73
+ */
74
+ defineWebComponent<GroupProps, GroupEvents>('kai-resizable', {
75
+ orientation: 'horizontal',
76
+ maximizedIndex: null,
77
+ }, (props, { element, dispatch }) => {
78
+ const [items, setItems] = createSignal<ItemInfo[]>([]);
79
+ const orientation = (): Orientation => (props.orientation === 'vertical' ? 'vertical' : 'horizontal');
80
+
81
+ /** Read the current `<kai-resizable-item>` light children + their attributes. */
82
+ function readItems() {
83
+ const all = Array.from(element.children).filter(
84
+ (c): c is HTMLElement => c.tagName.toLowerCase() === 'kai-resizable-item',
85
+ );
86
+ if (all.length > MAX_ITEMS) {
87
+ // eslint-disable-next-line no-console
88
+ console.warn(
89
+ `<kai-resizable>: only the first ${MAX_ITEMS} <kai-resizable-item> children are laid out ` +
90
+ `(got ${all.length}). Nest <kai-resizable> for more.`,
91
+ );
92
+ }
93
+ const capped = all.slice(0, MAX_ITEMS);
94
+ const parsed: ItemInfo[] = capped.map((el) => {
95
+ // Capture the ORIGINAL configured size ONCE per item, before any drag (or
96
+ // `persistSizes`) overwrites the live `size` attribute. Stashed on a non-
97
+ // observed data-* attr so dblclick-reset always snaps to the true default,
98
+ // not to wherever the user last dragged. Empty string = no explicit default.
99
+ if (!el.hasAttribute('data-kai-default-size')) {
100
+ el.setAttribute('data-kai-default-size', el.getAttribute('size') ?? '');
101
+ }
102
+ const defaultSize = el.getAttribute('data-kai-default-size') || undefined;
103
+ return {
104
+ el,
105
+ size: el.getAttribute('size') ?? undefined,
106
+ defaultSize,
107
+ min: el.getAttribute('min') ?? undefined,
108
+ max: el.getAttribute('max') ?? undefined,
109
+ locked: el.hasAttribute('locked') && el.getAttribute('locked') !== 'false',
110
+ // Honour both the `hidden` boolean attribute and the IDL property — Solid
111
+ // (and direct `el.hidden = true`) set the property, which doesn't reflect
112
+ // to the attribute on a custom element.
113
+ hidden: el.hidden || (el.hasAttribute('hidden') && el.getAttribute('hidden') !== 'false'),
114
+ };
115
+ });
116
+
117
+ // Assign each visible item to its panel slot by visible order; clear the
118
+ // rest so hidden/extra items don't leak into a slot.
119
+ let visIdx = 0;
120
+ for (const info of parsed) {
121
+ if (info.hidden) {
122
+ info.el.removeAttribute('slot');
123
+ } else {
124
+ info.el.setAttribute('slot', `p${visIdx}`);
125
+ visIdx++;
126
+ }
127
+ }
128
+ for (const el of all.slice(MAX_ITEMS)) el.removeAttribute('slot');
129
+
130
+ // BUG FIX (U10b): the MutationObserver fires for ANY subtree change, including
131
+ // a slotted child re-rendering its CONTENT (e.g. a new chat message). If we
132
+ // unconditionally `setItems(parsed)`, the <For> re-keys and each panel's
133
+ // flex-basis is re-derived from the item's `size` ATTRIBUTE — wiping out any
134
+ // size the user dragged (which lives only as inline flex-basis on the panel
135
+ // div, set by ResizableHandle.settleToPercent). The composite <kai-workspace>
136
+ // never hits this because it renders Solid panels directly with no re-read.
137
+ //
138
+ // So: only commit a new items() array when the panel LAYOUT actually changed —
139
+ // i.e. an item was added/removed, or a config attribute (size/min/max/locked/
140
+ // hidden) changed. A pure content mutation is structurally identical and is
141
+ // skipped, leaving the dragged inline basis intact.
142
+ if (!itemsChanged(items(), parsed)) return;
143
+ setItems(parsed);
144
+ }
145
+
146
+ /** True when the parsed item list differs structurally (identity, order, or any
147
+ * layout-affecting config attribute) from the current one. Content-only
148
+ * mutations leave every field equal → returns false → no re-render. */
149
+ function itemsChanged(prev: ItemInfo[], next: ItemInfo[]): boolean {
150
+ if (prev.length !== next.length) return true;
151
+ for (let i = 0; i < prev.length; i++) {
152
+ const a = prev[i];
153
+ const b = next[i];
154
+ if (a.el !== b.el) return true;
155
+ if (a.size !== b.size || a.min !== b.min || a.max !== b.max) return true;
156
+ if (a.locked !== b.locked || a.hidden !== b.hidden) return true;
157
+ }
158
+ return false;
159
+ }
160
+
161
+ const visible = () => items().filter((i) => !i.hidden);
162
+
163
+ /** Compute current panel sizes (percent of container) from the rendered DOM. */
164
+ function currentSizes(): number[] {
165
+ return visible().map((info) => {
166
+ const panel = info.el.assignedSlot?.closest('[data-panel]') as HTMLElement | null;
167
+ const target = panel ?? info.el;
168
+ const parent = target.parentElement;
169
+ const dim = orientation() === 'horizontal' ? 'width' : 'height';
170
+ const total = parent ? parent.getBoundingClientRect()[dim] : 0;
171
+ const val = target.getBoundingClientRect()[dim];
172
+ return total > 0 ? Math.round((val / total) * 100) : 0;
173
+ });
174
+ }
175
+
176
+ function emitChange() {
177
+ dispatch('kai-change', { sizes: currentSizes() });
178
+ }
179
+
180
+ /** Guard: while writing `size` attributes back from a live layout, the
181
+ * MutationObserver must NOT treat our own writes as an external structural
182
+ * change (which would re-render and could fight the in-flight drag). */
183
+ let persistingSizes = false;
184
+
185
+ /**
186
+ * BUG FIX (U10b) — durability: a drag/keyboard resize writes inline flex-basis
187
+ * onto the panel divs only; the item `size` ATTRIBUTE (the source of truth that
188
+ * `readItems` re-derives basis from) is left at its initial value. Persist the
189
+ * live percent back to each visible item's `size` attribute so the dragged
190
+ * layout survives ANY later re-read (including a genuine add/remove relayout),
191
+ * not just content-only mutations. Guarded so it doesn't trip the observer.
192
+ */
193
+ function persistSizes() {
194
+ const vis = visible();
195
+ const live = currentSizes();
196
+ if (vis.length !== live.length) return;
197
+ persistingSizes = true;
198
+ vis.forEach((info, i) => {
199
+ const pct = live[i];
200
+ if (Number.isFinite(pct) && pct > 0) info.el.setAttribute('size', `${pct}%`);
201
+ });
202
+ // Hold the guard across the MutationObserver microtask (it fires AFTER this
203
+ // synchronous code), then clear it — otherwise the observer sees the flag
204
+ // already false and re-reads our own writes. Same pattern as applyingMaximize.
205
+ queueMicrotask(() => { persistingSizes = false; });
206
+ }
207
+
208
+ // --- Task 2: maximize/restore core ---
209
+
210
+ interface SavedItemState {
211
+ el: HTMLElement;
212
+ size: string | null;
213
+ hidden: boolean;
214
+ locked: boolean;
215
+ }
216
+ interface MaximizeStash {
217
+ /** The element that was maximized (identity anchor for re-target + events). */
218
+ item: HTMLElement;
219
+ /** Per-element stash keyed by element identity, not position. */
220
+ saved: SavedItemState[];
221
+ }
222
+ const [maximized, setMaximized] = createSignal<MaximizeStash | null>(null);
223
+ /** Re-entrancy guard: while we (un)apply maximize attributes, the observer must
224
+ * NOT emit a mid-flight `change`. The final relayout emits the real one. */
225
+ let applyingMaximize = false;
226
+
227
+ /** Find the capped <kai-resizable-item> ancestor of an event target, if any. */
228
+ function findContainingItem(node: Node | null): HTMLElement | null {
229
+ let el = node instanceof Element ? node : node?.parentElement ?? null;
230
+ // The intent is composed; its target may be inside the artifact's shadow.
231
+ // Resolve to a direct light child <kai-resizable-item> of THIS group.
232
+ const capped = items().map((i) => i.el);
233
+ while (el) {
234
+ if (el instanceof HTMLElement && capped.includes(el)) return el;
235
+ el = el.parentElement ?? (el.getRootNode() as ShadowRoot).host ?? null;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ function readAttrState(el: HTMLElement) {
241
+ return {
242
+ size: el.getAttribute('size'),
243
+ hidden: el.hidden || (el.hasAttribute('hidden') && el.getAttribute('hidden') !== 'false'),
244
+ locked: el.hasAttribute('locked') && el.getAttribute('locked') !== 'false',
245
+ };
246
+ }
247
+
248
+ function setBoolAttr(el: HTMLElement, name: string, on: boolean) {
249
+ if (on) el.setAttribute(name, '');
250
+ else el.removeAttribute(name);
251
+ }
252
+
253
+ function maximizeItem(item: HTMLElement) {
254
+ const list = items();
255
+ const index = list.findIndex((i) => i.el === item);
256
+ if (index < 0) return;
257
+ const current = maximized();
258
+ if (current) {
259
+ if (current.item === item) return; // same item → no-op
260
+ restore(); // different item → re-target
261
+ }
262
+ applyingMaximize = true;
263
+ // Capture the EFFECTIVE current % so a post-drag layout restores faithfully.
264
+ const live = currentSizes(); // visible-order percents (Playwright-verified)
265
+ let visIdx = 0;
266
+ const saved: SavedItemState[] = list.map((info) => {
267
+ const prev = readAttrState(info.el);
268
+ if (!prev.hidden) {
269
+ // Write the live % back as the stashed size baseline.
270
+ const pct = live[visIdx++];
271
+ if (Number.isFinite(pct) && pct > 0) info.el.setAttribute('size', `${pct}%`);
272
+ }
273
+ return { el: info.el, size: info.el.getAttribute('size'), hidden: prev.hidden, locked: prev.locked };
274
+ });
275
+ // Hide every other item; free the maximized one to fill.
276
+ list.forEach((info, i) => {
277
+ if (i === index) {
278
+ info.el.removeAttribute('size');
279
+ info.el.removeAttribute('locked');
280
+ setBoolAttr(info.el, 'hidden', false);
281
+ } else {
282
+ setBoolAttr(info.el, 'hidden', true);
283
+ }
284
+ });
285
+ setMaximized({ item, saved });
286
+ element.setAttribute('data-maximized', '');
287
+ item.setAttribute('data-maximized-panel', '');
288
+ readItems();
289
+ emitChange();
290
+ // Keep applyingMaximize = true until AFTER the MutationObserver microtask fires
291
+ // so its queueMicrotask(emitChange) sees the guard and skips (storm guard).
292
+ queueMicrotask(() => { applyingMaximize = false; });
293
+ // Tell the maximized subtree (and only it) it is now maximized.
294
+ item.dispatchEvent(new CustomEvent('kai-maximize-state', { detail: { maximized: true }, bubbles: false, composed: true }));
295
+ dispatch('kai-maximize-change', { maximized: true, index });
296
+ }
297
+
298
+ function restore() {
299
+ const stash = maximized();
300
+ if (!stash) return;
301
+ applyingMaximize = true;
302
+ // Re-apply saved state keyed by element identity; items no longer in the DOM
303
+ // are simply skipped. This is safe regardless of sibling additions/removals
304
+ // that happened while maximized (no index drift).
305
+ for (const s of stash.saved) {
306
+ if (!s.el.isConnected) continue;
307
+ if (s.size === null) s.el.removeAttribute('size');
308
+ else s.el.setAttribute('size', s.size);
309
+ setBoolAttr(s.el, 'hidden', s.hidden);
310
+ setBoolAttr(s.el, 'locked', s.locked);
311
+ s.el.removeAttribute('data-maximized-panel');
312
+ }
313
+ const prevItem = stash.item;
314
+ setMaximized(null);
315
+ element.removeAttribute('data-maximized');
316
+ readItems();
317
+ emitChange();
318
+ // Keep applyingMaximize = true until AFTER the MutationObserver microtask fires
319
+ // so its queueMicrotask(emitChange) sees the guard and skips (storm guard).
320
+ queueMicrotask(() => { applyingMaximize = false; });
321
+ // Broadcast restore on the host AND directly on the formerly-maximized item.
322
+ element.dispatchEvent(new CustomEvent('kai-maximize-state', { detail: { maximized: false }, bubbles: false, composed: true }));
323
+ prevItem?.dispatchEvent(new CustomEvent('kai-maximize-state', { detail: { maximized: false }, bubbles: false, composed: true }));
324
+ dispatch('kai-maximize-change', { maximized: false, index: null });
325
+ }
326
+
327
+ onMount(() => {
328
+ readItems();
329
+ const onIntent = (e: Event) => {
330
+ const ce = e as CustomEvent<KaiMaximizeIntentDetail>;
331
+ e.stopPropagation(); // nearest group wins (nesting)
332
+ const item = findContainingItem(ce.target as Node);
333
+ if (!item) return; // outside any item → ignore
334
+ if (ce.detail.requested) maximizeItem(item);
335
+ else restore();
336
+ };
337
+ element.addEventListener('kai-maximize-intent', onIntent);
338
+
339
+ const onKeydown = (e: KeyboardEvent) => {
340
+ // Only act while maximized — the capture listener on the host already
341
+ // ensures the event originates within the group.
342
+ if (e.key !== 'Escape' || !maximized()) return;
343
+ e.stopPropagation();
344
+ restore();
345
+ };
346
+ element.addEventListener('keydown', onKeydown, true);
347
+
348
+ const mo = new MutationObserver(() => {
349
+ // Skip our OWN per-drag size writes (persistSizes). Re-reading here would
350
+ // setItems() → re-render the <For>, replacing the ResizableHandle mid-drag
351
+ // and dropping its pointer capture — the drag stalls after the first move.
352
+ // (The flag is held across this observer microtask; see persistSizes.)
353
+ if (persistingSizes) return;
354
+ readItems();
355
+ const stash = maximized();
356
+ if (stash) {
357
+ // Use element identity, not a positional index, to detect whether the
358
+ // maximized item is still present and visible.
359
+ const maximizedEl = stash.item;
360
+ const isConnected = maximizedEl.isConnected && element.contains(maximizedEl);
361
+ const isVisible = isConnected && !(maximizedEl.hidden || maximizedEl.hasAttribute('hidden'));
362
+ if (!isConnected || !isVisible) {
363
+ // The maximized item was removed or hidden out from under us → restore.
364
+ restore();
365
+ return;
366
+ }
367
+ }
368
+ if (applyingMaximize) return; // our own maximize writes — skip the emit
369
+ queueMicrotask(emitChange);
370
+ });
371
+ mo.observe(element, {
372
+ childList: true,
373
+ // Attribute changes happen on the <kai-resizable-item> *children*, so we
374
+ // must observe the subtree (filtered to the config attributes) — not just
375
+ // the host's own attributes.
376
+ subtree: true,
377
+ attributes: true,
378
+ attributeFilter: ['size', 'locked', 'min', 'max', 'hidden'],
379
+ });
380
+ onCleanup(() => {
381
+ mo.disconnect();
382
+ element.removeEventListener('kai-maximize-intent', onIntent);
383
+ element.removeEventListener('keydown', onKeydown, true);
384
+ });
385
+
386
+ // --- Task 3: imperative host methods + declarative maximizedIndex prop ---
387
+
388
+ // Imperative host API (assigned onto the element; typed by resizable.d.ts).
389
+ const host = element as unknown as { maximize(i: number): void; restore(): void };
390
+ host.maximize = (i: number) => {
391
+ const it = items()[i]?.el;
392
+ if (it) maximizeItem(it);
393
+ };
394
+ host.restore = () => restore();
395
+
396
+ // Declarative maximizedIndex → maximize/restore. Skip the initial null run.
397
+ createEffect(
398
+ on(
399
+ () => props.maximizedIndex,
400
+ (idx) => {
401
+ if (idx == null) restore();
402
+ else {
403
+ const it = items()[idx]?.el;
404
+ if (it) maximizeItem(it);
405
+ }
406
+ },
407
+ { defer: true },
408
+ ),
409
+ );
410
+ });
411
+
412
+ const isHoriz = () => orientation() === 'horizontal';
413
+
414
+ return (
415
+ <>
416
+ {/* A resizable layout fills its container — default the host to a block
417
+ that stretches, so consumers only need to give a parent (or the element)
418
+ a height. Override with an explicit height on the element if needed. */}
419
+ <style>{':host{display:block;height:100%}'}</style>
420
+ <div
421
+ data-resizable-root
422
+ data-orientation={orientation()}
423
+ style={{
424
+ display: 'flex',
425
+ 'flex-direction': isHoriz() ? 'row' : 'column',
426
+ width: '100%',
427
+ height: '100%',
428
+ }}
429
+ >
430
+ <For each={visible()}>
431
+ {(info, i) => {
432
+ const min = boundAttrs(info.min);
433
+ const max = boundAttrs(info.max);
434
+ const def = boundAttrs(info.defaultSize);
435
+ const basis = normalizeSize(info.size);
436
+ const prev = () => visible()[i() - 1];
437
+ const isStatic = () => !!(info.locked || prev()?.locked);
438
+ return (
439
+ <>
440
+ <Show when={i() > 0}>
441
+ <ResizableHandle
442
+ withHandle
443
+ orientation={orientation()}
444
+ static={isStatic()}
445
+ onPanelResize={() => {
446
+ // Persist the dragged/dblclick-reset layout back to the item
447
+ // `size` attributes so it survives later re-reads, then emit.
448
+ persistSizes();
449
+ queueMicrotask(emitChange);
450
+ }}
451
+ />
452
+ </Show>
453
+ <div
454
+ data-panel
455
+ data-locked={info.locked ? '' : undefined}
456
+ data-min-size={min.pxAttr}
457
+ data-min-size-pct={min.pctAttr}
458
+ data-max-size={max.pxAttr}
459
+ data-max-size-pct={max.pctAttr}
460
+ // Reflect the configured default size so a dblclick on the
461
+ // adjacent <ResizableHandle> can snap this panel back to it.
462
+ data-default-size={def.pxAttr}
463
+ data-default-size-pct={def.pctAttr}
464
+ style={{
465
+ // The panel is a flex item of the root: `flex-basis` sizes its
466
+ // MAIN axis (width when horizontal, height when vertical) and the
467
+ // drag/keyboard handlers rewrite that basis — so the layout spine
468
+ // stays a plain flex row/column (drag math unchanged).
469
+ //
470
+ // For the FILL, the panel is itself a `display:grid` with a single
471
+ // `minmax(0,1fr)` cell on BOTH axes. A grid item (the slotted
472
+ // `<kai-resizable-item>`) stretches to fill its cell on both axes by
473
+ // default (`place-items:stretch`), and a `1fr` track inside a
474
+ // definite-sized grid is a *definite* length — so content fills
475
+ // height AND width whether or not it sets `height:100%`, in both
476
+ // orientations. A flex panel only stretches the CROSS axis (which is
477
+ // why the previous `display:flex` panel collapsed the main axis to
478
+ // content height). This mirrors how Shoelace/Web Awesome
479
+ // `<sl-split-panel>` lay panels out with grid for guaranteed fill,
480
+ // adapted here to a per-panel grid cell (our double slot boundary
481
+ // makes a slot-as-grid-item unusable — a slot with assigned nodes
482
+ // has a zero box). min:0 on both axes enables shrink-to-scroll;
483
+ // overflow clips (the item owns the scroll).
484
+ ...(basis !== undefined
485
+ ? { 'flex-basis': basis, 'flex-grow': '0', 'flex-shrink': '0' }
486
+ : { flex: '1 1 0%' }),
487
+ display: 'grid',
488
+ 'grid-template-rows': 'minmax(0, 1fr)',
489
+ 'grid-template-columns': 'minmax(0, 1fr)',
490
+ 'min-width': '0',
491
+ 'min-height': '0',
492
+ overflow: 'hidden',
493
+ }}
494
+ >
495
+ <slot name={`p${i()}`} />
496
+ </div>
497
+ </>
498
+ );
499
+ }}
500
+ </For>
501
+ </div>
502
+ </>
503
+ );
504
+ });
505
+
506
+ interface ItemProps extends Record<string, unknown> {
507
+ /** Initial main-axis size: `"280px"` (fixed) or `"25%"`/`25` (percent). Omitted → flexible. */
508
+ size?: string;
509
+ /** Minimum size during resize (px or %). */
510
+ min?: string;
511
+ /** Maximum size during resize (px or %). */
512
+ max?: string;
513
+ /** Fix this panel's size; adjacent dividers become non-draggable. */
514
+ locked?: boolean;
515
+ /** Hide this panel; its divider is dropped and the rest reflow. */
516
+ hidden?: boolean;
517
+ }
518
+
519
+ /**
520
+ * `<kai-resizable-item>` — a passive config-carrier inside `<kai-resizable>`. It
521
+ * renders its own slotted light content (`<slot/>`); the parent `<kai-resizable>`
522
+ * reads its `size`/`min`/`max`/`locked`/`hidden` attributes to lay it out.
523
+ */
524
+ defineWebComponent<ItemProps>('kai-resizable-item', {
525
+ size: undefined,
526
+ min: undefined,
527
+ max: undefined,
528
+ locked: false,
529
+ hidden: false,
530
+ }, () => (
531
+ <>
532
+ {/* The item host fills the panel's single grid cell (the panel stretches it on
533
+ both axes) and OWNS the scroll: `display:block; width/height:100%;
534
+ overflow:auto`. min:0 enables shrink-to-scroll.
535
+
536
+ The FILL of the *slotted* content happens one level in: a `display:grid`
537
+ wrapper with a single `minmax(0,1fr)` cell whose ONLY child is the `<slot>`.
538
+ A grid item stretches to fill its cell on BOTH axes by default
539
+ (`place-items:stretch`) into a *definite* `1fr` track, so slotted content
540
+ fills width AND height whether or not it sets `height:100%`. The grid MUST
541
+ be its own element wrapping the slot — NOT the host — because the element
542
+ facade renders an empty portal-mount `<div>` (for overlays) as a sibling of
543
+ the facade output; if the host itself were the grid, that portal div would
544
+ become a second grid item and steal a track (collapsing content to the auto
545
+ row). Scoping the grid to a wrapper whose sole child is the slot keeps
546
+ exactly one grid item and is robust to the facade's portal sibling.
547
+ Mirrors how Shoelace/Web Awesome `<sl-split-panel>` use grid for fill. */}
548
+ <style>{':host{display:block;width:100%;height:100%;min-width:0;min-height:0;overflow:auto}'}</style>
549
+ <div style={{ display: 'grid', 'grid-template-rows': 'minmax(0, 1fr)', 'grid-template-columns': 'minmax(0, 1fr)', width: '100%', height: '100%', 'min-width': '0', 'min-height': '0' }}>
550
+ <slot />
551
+ </div>
552
+ </>
553
+ ) as unknown as JSX.Element);
@@ -0,0 +1,79 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { onMount } from 'solid-js';
3
+ import './register'; // side effect: registers the custom elements
4
+ import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
+
6
+ // The web components are custom DOM elements, so declare the tags for JSX.
7
+ declare module 'solid-js' {
8
+ // eslint-disable-next-line @typescript-eslint/no-namespace
9
+ namespace JSX {
10
+ interface IntrinsicElements {
11
+ 'kai-response-stream': JSX.HTMLAttributes<HTMLElement> & {
12
+ mode?: string;
13
+ speed?: number;
14
+ };
15
+ }
16
+ }
17
+ }
18
+
19
+ const STREAM_TEXT =
20
+ "This text reveals with a typewriter animation, streamed character by character — exactly how you'd render a live assistant reply.";
21
+
22
+ /** Render `<kai-response-stream>` with the `text` set as a JS property. */
23
+ function StreamElement(props: { text: string; mode?: string; speed?: number }) {
24
+ let el: (HTMLElement & { text?: string }) | undefined;
25
+ onMount(() => {
26
+ if (el) el.text = props.text;
27
+ });
28
+ return (
29
+ <kai-response-stream
30
+ ref={(e) => (el = e as HTMLElement)}
31
+ mode={props.mode}
32
+ speed={props.speed}
33
+ style={{ display: 'block', padding: '24px', 'max-width': '640px', 'line-height': 1.6 }}
34
+ />
35
+ );
36
+ }
37
+
38
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
39
+ <kai-response-stream id="stream" mode="typewriter" speed="20"></kai-response-stream>
40
+
41
+ <script type="module">
42
+ import '@kitn.ai/ui/elements'; // registers the custom elements
43
+
44
+ const stream = document.getElementById('stream');
45
+ // text can be a string, or an AsyncIterable<string> for live streaming
46
+ stream.text = "Hello, this reveals one character at a time…";
47
+ stream.addEventListener('kai-complete', () => console.log('done'));
48
+ </script>`;
49
+
50
+ const meta = {
51
+ title: 'Components/ResponseStream',
52
+ tags: ['autodocs'],
53
+ argTypes: argTypesFor('kai-response-stream'),
54
+ parameters: {
55
+ layout: 'fullscreen',
56
+ docs: {
57
+ description: specDescription('kai-response-stream', [
58
+ '`<kai-response-stream>` is the framework-agnostic **web component** that reveals text with a typewriter or fade animation — the building block for streamed assistant replies, isolated in **Shadow DOM**.',
59
+ '**When to use:** animating a response as it arrives. Pass a finished string to replay an animation, or an `AsyncIterable<string>` to drive it from a live stream. In SolidJS, use the `ResponseStream` primitive.',
60
+ "**How to use:** register once with `import '@kitn.ai/ui/elements'`, set the `text` **property** (string or async iterable), tune `mode` (`typewriter` / `fade`) and `speed`, and listen for the `kai-complete` **CustomEvent**.",
61
+ 'See the **Code** tab for HTML usage.',
62
+ ]),
63
+ },
64
+ },
65
+ } satisfies Meta;
66
+
67
+ export default meta;
68
+ type Story = StoryObj;
69
+
70
+ /** Typewriter reveal (the default). */
71
+ export const Typewriter: Story = {
72
+ render: () => <StreamElement text={STREAM_TEXT} mode="typewriter" speed={20} />,
73
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
74
+ };
75
+
76
+ /** Fade-in reveal, segment by segment. */
77
+ export const Fade: Story = {
78
+ render: () => <StreamElement text={STREAM_TEXT} mode="fade" speed={10} />,
79
+ };
@@ -0,0 +1,40 @@
1
+ import { defineWebComponent } from './define';
2
+ import { ResponseStream, type Mode } from '../components/response-stream';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /** Text to stream. A string, or an `AsyncIterable<string>` (set as a JS
6
+ * property — async iterables can't be HTML attributes). */
7
+ text?: string | AsyncIterable<string>;
8
+ /** Reveal animation. */
9
+ mode?: Mode;
10
+ /** Characters/segments per tick. */
11
+ speed?: number;
12
+ /** Element tag to render as. */
13
+ as?: string;
14
+ }
15
+
16
+ /** Events fired by `<kai-response-stream>`. */
17
+ interface Events {
18
+ /** Streaming finished. */
19
+ 'kai-complete': void;
20
+ }
21
+
22
+ /**
23
+ * `<kai-response-stream>` — reveals text with a typewriter or fade animation.
24
+ * Text via the `text` property; `mode`/`speed` attributes; emits `kai-complete`.
25
+ */
26
+ defineWebComponent<Props, Events>('kai-response-stream', {
27
+ text: '',
28
+ mode: 'typewriter',
29
+ speed: 20,
30
+ as: undefined,
31
+ }, (props, { dispatch }) => (
32
+ <ResponseStream
33
+ textStream={props.text ?? ''}
34
+ mode={props.mode}
35
+ speed={props.speed}
36
+ as={props.as}
37
+ class="text-body"
38
+ onComplete={() => dispatch('kai-complete')}
39
+ />
40
+ ));