@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,221 @@
1
+ // src/elements/remote.tsx
2
+ // <kai-remote> — Shadow-DOM facade that mounts a sandboxed cross-origin iframe
3
+ // card via mountRemoteCard(), re-emits every CardEvent as a bubbling+composed
4
+ // kai-card CustomEvent, and validates the provider-origin before mounting.
5
+ import { createEffect, createSignal, onCleanup, onMount } from 'solid-js';
6
+ import { defineWebComponent } from './define';
7
+ import { mountRemoteCard } from '../remote/host-embed';
8
+ import type { RemoteCardHandle } from '../remote/host-embed';
9
+ import type { CardEnvelope, CardContext, CardPolicy, CardEvent } from '../primitives/card-contract';
10
+ import { emitCardEvent } from '../primitives/card-routing';
11
+
12
+ interface Props extends Record<string, unknown> {
13
+ /** The remote card URL. Attribute: `src`. */
14
+ src?: string;
15
+ /** Exact provider origin (https: or http://localhost for dev). Attribute: `provider-origin`. */
16
+ providerOrigin?: string;
17
+ /** The card envelope to render. JS property only. */
18
+ envelope?: Record<string, unknown>;
19
+ /** Optional routing policy. JS property only. */
20
+ policy?: Record<string, unknown>;
21
+ }
22
+
23
+ /**
24
+ * Validate that `origin` is a single absolute origin that is either:
25
+ * - an https: origin (e.g. "https://provider.example")
26
+ * - http://localhost or http://127.0.0.1 (local dev only)
27
+ *
28
+ * Rejects "*", comma-lists, bare paths, and any other http: origin.
29
+ */
30
+ function isValidProviderOrigin(origin: string | undefined): boolean {
31
+ if (!origin) return false;
32
+ // Reject wildcard and comma-lists immediately.
33
+ if (origin === '*' || origin.includes(',')) return false;
34
+ let parsed: URL;
35
+ try {
36
+ parsed = new URL(origin);
37
+ } catch {
38
+ return false;
39
+ }
40
+ // Must be an exact origin: no pathname beyond '/', no search, no hash.
41
+ if (parsed.pathname !== '/' || parsed.search !== '' || parsed.hash !== '') return false;
42
+ if (parsed.protocol === 'https:') return true;
43
+ if (parsed.protocol === 'http:') {
44
+ return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
45
+ }
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Build a CardPolicy that re-emits each routed CardEvent as a bubbling+composed
51
+ * `kai-card` CustomEvent on `element`, AND delegates to the caller-supplied `policy`.
52
+ */
53
+ function buildWrappedPolicy(element: HTMLElement, userPolicy: CardPolicy | undefined): CardPolicy {
54
+ function emit(ev: CardEvent): void {
55
+ emitCardEvent(element, ev);
56
+ }
57
+ return {
58
+ onSubmit(cardId, data) {
59
+ emit({ kind: 'submit', cardId, data });
60
+ userPolicy?.onSubmit?.(cardId, data);
61
+ },
62
+ onAction(cardId, action, payload) {
63
+ emit({ kind: 'action', cardId, action, payload });
64
+ userPolicy?.onAction?.(cardId, action, payload);
65
+ },
66
+ onSendPrompt(text, opts) {
67
+ // send-prompt doesn't carry cardId in the policy signature; we use a placeholder.
68
+ emit({ kind: 'send-prompt', cardId: '', text, mode: opts.mode, context: opts.context });
69
+ userPolicy?.onSendPrompt?.(text, opts);
70
+ },
71
+ onOpen(url, target) {
72
+ // open doesn't carry cardId in the policy signature.
73
+ emit({ kind: 'open', cardId: '', url, target });
74
+ userPolicy?.onOpen?.(url, target);
75
+ },
76
+ onState(cardId, patch) {
77
+ emit({ kind: 'state', cardId, patch });
78
+ userPolicy?.onState?.(cardId, patch);
79
+ },
80
+ onDismiss(cardId) {
81
+ emit({ kind: 'dismiss', cardId });
82
+ userPolicy?.onDismiss?.(cardId);
83
+ },
84
+ onError(cardId, message) {
85
+ emit({ kind: 'error', cardId, message });
86
+ userPolicy?.onError?.(cardId, message);
87
+ },
88
+ maxSendPromptMode: userPolicy?.maxSendPromptMode,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * `<kai-remote>` — mounts a sandboxed cross-origin iframe card.
94
+ *
95
+ * Required props:
96
+ * - `provider-origin` attribute: the exact HTTPS origin of the card provider
97
+ * (or http://localhost for local dev).
98
+ * - `src` attribute: the URL of the card page (must share the provider origin).
99
+ * - `envelope` JS property: the CardEnvelope to render.
100
+ *
101
+ * Optional:
102
+ * - `policy` JS property: a CardPolicy for routing card events.
103
+ * - `theme` attribute (inherited from defineWebComponent): 'light' | 'dark' | 'auto'.
104
+ *
105
+ * Every routed CardEvent is also dispatched as a bubbling+composed `kai-card`
106
+ * CustomEvent off the host element.
107
+ */
108
+ defineWebComponent<Props>(
109
+ 'kai-remote',
110
+ {
111
+ src: undefined,
112
+ providerOrigin: undefined,
113
+ envelope: undefined,
114
+ policy: undefined,
115
+ },
116
+ (props, { element }) => {
117
+ // Lifted out of onMount so the theme effect (below) can re-push context to the
118
+ // live bridge after mount. `undefined` until the iframe is successfully mounted.
119
+ let handle: RemoteCardHandle | undefined;
120
+
121
+ // Resolve dark mode the SAME way native elements do (see createDarkMode in
122
+ // define.tsx): the `theme` prop is 'light' | 'dark' | 'auto' (default 'auto',
123
+ // which follows the OS `prefers-color-scheme`). Tracked reactively so a host /
124
+ // Storybook theme toggle flows through the effect below.
125
+ const [systemDark, setSystemDark] = createSignal(false);
126
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
127
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
128
+ setSystemDark(mq.matches);
129
+ const onChange = (e: MediaQueryListEvent) => setSystemDark(e.matches);
130
+ mq.addEventListener('change', onChange);
131
+ onCleanup(() => mq.removeEventListener('change', onChange));
132
+ }
133
+ const isDark = () => {
134
+ const theme = (props.theme as string | undefined) ?? 'auto';
135
+ return theme === 'dark' || (theme === 'auto' && systemDark());
136
+ };
137
+
138
+ onMount(() => {
139
+ const shadow = element.shadowRoot;
140
+ if (!shadow) return;
141
+
142
+ function renderError(message: string): void {
143
+ const alert = document.createElement('div');
144
+ alert.setAttribute('role', 'alert');
145
+ alert.textContent = message;
146
+ shadow!.appendChild(alert);
147
+ }
148
+
149
+ // Validate provider-origin before attempting any mount.
150
+ const providerOrigin = props.providerOrigin;
151
+ if (!isValidProviderOrigin(providerOrigin)) {
152
+ renderError(
153
+ `[kai-remote] Invalid provider-origin "${providerOrigin ?? ''}". ` +
154
+ `Must be an absolute https: origin, or http://localhost / http://127.0.0.1 for local dev.`,
155
+ );
156
+ return;
157
+ }
158
+
159
+ const src = props.src;
160
+ if (!src) {
161
+ renderError('[kai-remote] Missing required "src" attribute.');
162
+ return;
163
+ }
164
+
165
+ const envelope = props.envelope as CardEnvelope | undefined;
166
+ if (!envelope) {
167
+ renderError('[kai-remote] Missing required "envelope" property.');
168
+ return;
169
+ }
170
+
171
+ // Derive a minimal CardContext. Theme resolved the same way native elements
172
+ // do (isDark(), above); read untracked here so this initial mount doesn't
173
+ // re-run on theme change — the createEffect below pushes later changes.
174
+ const context: CardContext = {
175
+ theme: { mode: isDark() ? 'dark' : 'light' },
176
+ locale: (typeof navigator !== 'undefined' && navigator.language) || 'en',
177
+ };
178
+
179
+ const userPolicy = props.policy as CardPolicy | undefined;
180
+ const wrappedPolicy = buildWrappedPolicy(element, userPolicy);
181
+
182
+ // Create a container inside the shadow root for mountRemoteCard.
183
+ const container = document.createElement('div');
184
+ shadow.appendChild(container);
185
+
186
+ try {
187
+ handle = mountRemoteCard({
188
+ container,
189
+ providerOrigin: providerOrigin as string,
190
+ src,
191
+ envelope,
192
+ context,
193
+ policy: wrappedPolicy,
194
+ });
195
+ } catch (err) {
196
+ const message = err instanceof Error ? err.message : String(err);
197
+ renderError(`[kai-remote] Mount failed: ${message}`);
198
+ container.remove();
199
+ return;
200
+ }
201
+
202
+ onCleanup(() => {
203
+ handle?.destroy();
204
+ handle = undefined;
205
+ });
206
+ });
207
+
208
+ // React to later host/Storybook theme toggles: re-push a fresh resolved theme
209
+ // to the live bridge so the framed card re-themes (consistent with native
210
+ // elements). No-op until the iframe has mounted (handle is set). The bridge
211
+ // does a dispose+remount on a genuine theme change — see provider-runtime.ts;
212
+ // token/locale refreshes stay silent.
213
+ createEffect(() => {
214
+ const mode = isDark() ? 'dark' : 'light';
215
+ handle?.updateContext({ theme: { mode } });
216
+ });
217
+
218
+ // This element renders purely into the shadow root via onMount — no JSX return needed.
219
+ return <></>;
220
+ },
221
+ );
@@ -0,0 +1,27 @@
1
+ // src/elements/resizable.d.ts
2
+ // Hand-authored ambient types for <kai-resizable>'s maximize capability. The
3
+ // generated element-types.d.ts emits prop/event interfaces only (not methods),
4
+ // so the imperative host API + the cross-element protocol events live here.
5
+ // NOT regenerated by `npm run build`.
6
+ import type { KaiMaximizeIntentDetail, KaiMaximizeStateDetail } from './resizable';
7
+
8
+ declare global {
9
+ interface KaiResizableElement extends HTMLElement {
10
+ /** Which item index is maximized (null = none). Setting it maximizes that
11
+ * item (or restores when set to null) — the declarative source of truth. */
12
+ maximizedIndex: number | null;
13
+ /** Imperatively maximize the item at `index` (thin wrapper over maximizedIndex). */
14
+ maximize(index: number): void;
15
+ /** Imperatively restore from the maximized layout. */
16
+ restore(): void;
17
+ }
18
+ interface HTMLElementTagNameMap {
19
+ 'kai-resizable': KaiResizableElement;
20
+ }
21
+ interface HTMLElementEventMap {
22
+ 'kai-maximize-intent': CustomEvent<KaiMaximizeIntentDetail>;
23
+ 'kai-maximize-state': CustomEvent<KaiMaximizeStateDetail>;
24
+ }
25
+ }
26
+
27
+ export {};
@@ -0,0 +1,426 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal, onMount, type JSX } from 'solid-js';
3
+ import './register'; // side effect: registers the custom elements
4
+ import { argTypesFor, specDescription } from '../stories/docs/element-controls';
5
+ import { Resizable, ResizablePanel } from '../ui/resizable';
6
+ import type { ArtifactFile } from '../components/artifact';
7
+ import { Artifact } from '../components/artifact';
8
+
9
+ // The web components are custom DOM elements, so declare the tags for JSX.
10
+ declare module 'solid-js' {
11
+ // eslint-disable-next-line @typescript-eslint/no-namespace
12
+ namespace JSX {
13
+ interface IntrinsicElements {
14
+ 'kai-resizable': JSX.HTMLAttributes<HTMLElement> & { orientation?: string };
15
+ 'kai-resizable-item': JSX.HTMLAttributes<HTMLElement> & {
16
+ size?: string; min?: string; max?: string; locked?: boolean | string; hidden?: boolean | string;
17
+ };
18
+ // `kai-artifact` JSX type is augmented (with the full attr set) in
19
+ // artifact.stories.tsx — declaring it again here with a different shape
20
+ // would trip TS2717 (module-augmentation merges must match). Reuse that one.
21
+ }
22
+ }
23
+ }
24
+
25
+ // Fixture base URL (served by Storybook staticDirs from examples/artifact-fixtures).
26
+ const BASE = new URL('artifact-fixtures', document.baseURI).href;
27
+
28
+ const ARTIFACT_FILES: ArtifactFile[] = [
29
+ { path: 'index.html', url: `${BASE}/index.html`, type: 'html', language: 'html',
30
+ code: `<!DOCTYPE html><html lang="en"><head><title>Preview</title></head><body><h1>Starboard</h1></body></html>` },
31
+ { path: 'about.html', url: `${BASE}/about.html`, type: 'html', language: 'html',
32
+ code: `<!DOCTYPE html><html lang="en"><head><title>About</title></head><body><h1>About</h1></body></html>` },
33
+ ];
34
+
35
+ /** A labelled placeholder pane so the layout is visible in stories. */
36
+ function Pane(props: { label: string; tone?: 'muted' | 'plain' }) {
37
+ return (
38
+ <div
39
+ style={{
40
+ height: '100%',
41
+ display: 'flex',
42
+ 'align-items': 'center',
43
+ 'justify-content': 'center',
44
+ padding: '16px',
45
+ background: props.tone === 'plain' ? 'transparent' : 'var(--color-muted, #f4f4f5)',
46
+ color: 'var(--color-muted-foreground, #71717a)',
47
+ 'font-size': '13px',
48
+ }}
49
+ >
50
+ {props.label}
51
+ </div>
52
+ );
53
+ }
54
+
55
+ /** A bordered, sized frame the group fills. */
56
+ function Frame(props: { children: JSX.Element; tall?: boolean }) {
57
+ return (
58
+ <div
59
+ style={{
60
+ height: props.tall ? '384px' : '256px',
61
+ width: '100%',
62
+ 'max-width': '768px',
63
+ border: '1px solid var(--color-border, #e4e4e7)',
64
+ 'border-radius': '8px',
65
+ overflow: 'hidden',
66
+ }}
67
+ >
68
+ {props.children}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
74
+ <kai-resizable orientation="horizontal" style="display:block;height:400px">
75
+ <kai-resizable-item size="25%" min="160px"> ...list... </kai-resizable-item>
76
+ <kai-resizable-item> ...chat... </kai-resizable-item>
77
+ <kai-resizable-item size="30%"> ...preview... </kai-resizable-item>
78
+ </kai-resizable>
79
+
80
+ <script type="module">
81
+ import '@kitn.ai/ui/elements'; // registers the custom elements
82
+ document.querySelector('kai-resizable')
83
+ .addEventListener('kai-change', (e) => console.log(e.detail.sizes));
84
+ </script>`;
85
+
86
+ const meta = {
87
+ title: 'Components/Resizable',
88
+ tags: ['autodocs'],
89
+ argTypes: argTypesFor('kai-resizable'),
90
+ parameters: {
91
+ layout: 'padded',
92
+ docs: {
93
+ description: specDescription('kai-resizable', [
94
+ '`<kai-resizable>` is the framework-agnostic **web component** for a composable, resizable multi-panel layout (up to **3** `<kai-resizable-item>` panels) with **auto-inserted draggable dividers** — isolated in **Shadow DOM**.',
95
+ '**When to use:** to compose an app shell out of slotted regions without hand-wiring panels and handles — e.g. `list | chat | preview`. In SolidJS, use the `Resizable` convenience (UI/Resizable) directly.',
96
+ "**How to use:** register once with `import '@kitn.ai/ui/elements'`, set `orientation` (`horizontal` row / `vertical` column), and put a `<kai-resizable-item>` per panel. Each item carries `size` (px or %, e.g. `\"280px\"` or `\"25%\"`), `min`/`max`, `locked` (fixed size + non-draggable neighbour), and `hidden` (drops the panel + its divider). Listen for the **`change`** event (`detail.sizes`, percent).",
97
+ '**Anatomy:** one or more **`<kai-resizable-item>`** light children (each a config-carrier: `size`, `min`, `max`, `locked`, `hidden` attributes; renders its slotted content) with auto-inserted **draggable divider handles** between each visible, unlocked pair. A panel is omitted from layout when `hidden`; its adjacent divider is dropped. Nesting `<kai-resizable>` inside an item allows more than 3 panels.',
98
+ '**Placement:** the layout spine for compose-your-own-chat shells — sidebar + conversation, conversation + inspector, or a three-up list/chat/preview.',
99
+ 'See the **Code** tab for HTML usage.',
100
+ ]),
101
+ },
102
+ },
103
+ args: { orientation: 'horizontal' },
104
+ } satisfies Meta;
105
+
106
+ export default meta;
107
+ type Story = StoryObj;
108
+
109
+ /** Interactive playground — flip orientation, then drag the dividers. */
110
+ export const Playground: Story = {
111
+ render: (args: { orientation?: string }) => (
112
+ <Frame>
113
+ <kai-resizable orientation={args.orientation ?? 'horizontal'}>
114
+ <kai-resizable-item size="25%" min="120px"><Pane label="List" /></kai-resizable-item>
115
+ <kai-resizable-item><Pane label="Chat" tone="plain" /></kai-resizable-item>
116
+ </kai-resizable>
117
+ </Frame>
118
+ ),
119
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
120
+ };
121
+
122
+ /** Two panels: a sized list beside a flexible chat. */
123
+ export const ListChat: Story = {
124
+ name: 'Sidebar + chat',
125
+ render: () => (
126
+ <Frame>
127
+ <kai-resizable orientation="horizontal">
128
+ <kai-resizable-item size="28%" min="140px" max="50%"><Pane label="List" /></kai-resizable-item>
129
+ <kai-resizable-item><Pane label="Chat" tone="plain" /></kai-resizable-item>
130
+ </kai-resizable>
131
+ </Frame>
132
+ ),
133
+ };
134
+
135
+ /** Three panels, two draggable dividers. */
136
+ export const ListChatPreview: Story = {
137
+ name: 'List + chat + preview',
138
+ render: () => (
139
+ <Frame>
140
+ <kai-resizable orientation="horizontal">
141
+ <kai-resizable-item size="22%" min="120px"><Pane label="List" /></kai-resizable-item>
142
+ <kai-resizable-item><Pane label="Chat" tone="plain" /></kai-resizable-item>
143
+ <kai-resizable-item size="30%" min="160px"><Pane label="Preview" /></kai-resizable-item>
144
+ </kai-resizable>
145
+ </Frame>
146
+ ),
147
+ };
148
+
149
+ /** A locked, fixed-px sidebar — its divider is a static (non-draggable) separator. */
150
+ export const LockedSidebar: Story = {
151
+ name: 'Locked sidebar',
152
+ render: () => (
153
+ <Frame>
154
+ <kai-resizable orientation="horizontal">
155
+ <kai-resizable-item size="240px" locked><Pane label="Locked sidebar (240px)" /></kai-resizable-item>
156
+ <kai-resizable-item><Pane label="Chat" tone="plain" /></kai-resizable-item>
157
+ </kai-resizable>
158
+ </Frame>
159
+ ),
160
+ };
161
+
162
+ /** Stacked top/bottom split. */
163
+ export const Vertical: Story = {
164
+ name: 'Vertical split',
165
+ render: () => (
166
+ <Frame tall>
167
+ <kai-resizable orientation="vertical">
168
+ <kai-resizable-item size="40%" min="80px"><Pane label="Top" /></kai-resizable-item>
169
+ <kai-resizable-item><Pane label="Bottom" tone="plain" /></kai-resizable-item>
170
+ </kai-resizable>
171
+ </Frame>
172
+ ),
173
+ };
174
+
175
+ /** Toggle the preview panel — its divider drops and the rest reflow. */
176
+ export const HiddenToggle: Story = {
177
+ name: 'Show / hide a panel',
178
+ render: () => {
179
+ const [showPreview, setShowPreview] = createSignal(true);
180
+ let previewItem: HTMLElement | undefined;
181
+ const toggle = () => {
182
+ setShowPreview((v) => !v);
183
+ // Drive the boolean attribute directly so the group's MutationObserver
184
+ // re-lays out (Solid sets the `hidden` IDL property, which doesn't reflect
185
+ // to the attribute on a custom element).
186
+ if (previewItem) {
187
+ if (showPreview()) previewItem.removeAttribute('hidden');
188
+ else previewItem.setAttribute('hidden', '');
189
+ }
190
+ };
191
+ return (
192
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
193
+ <button
194
+ type="button"
195
+ onClick={toggle}
196
+ style={{
197
+ 'align-self': 'flex-start',
198
+ padding: '4px 12px',
199
+ 'font-size': '13px',
200
+ border: '1px solid var(--color-border, #e4e4e7)',
201
+ 'border-radius': '6px',
202
+ cursor: 'pointer',
203
+ }}
204
+ >
205
+ {showPreview() ? 'Hide preview' : 'Show preview'}
206
+ </button>
207
+ <Frame>
208
+ <kai-resizable orientation="horizontal">
209
+ <kai-resizable-item size="24%" min="120px"><Pane label="List" /></kai-resizable-item>
210
+ <kai-resizable-item><Pane label="Chat" tone="plain" /></kai-resizable-item>
211
+ <kai-resizable-item ref={(e) => (previewItem = e as HTMLElement)} size="30%"><Pane label="Preview" /></kai-resizable-item>
212
+ </kai-resizable>
213
+ </Frame>
214
+ </div>
215
+ );
216
+ },
217
+ };
218
+
219
+ const EXPAND_TO_FILL_SNIPPET = `<!-- The artifact's "Expand" button (opt-in: expandable) fires a bubbling
220
+ kai-maximize-intent event. The nearest enclosing <kai-resizable> catches it
221
+ automatically and hides siblings so the preview panel fills the container.
222
+ No wiring needed between the two elements — the protocol is zero-config. -->
223
+
224
+ <kai-resizable orientation="horizontal" style="display:block;height:480px">
225
+ <kai-resizable-item size="20%" min="120px"> …list… </kai-resizable-item>
226
+ <kai-resizable-item> …chat… </kai-resizable-item>
227
+ <kai-resizable-item size="35%" min="200px">
228
+ <!-- expandable opt-in: adds the Expand/Collapse button -->
229
+ <kai-artifact
230
+ src="…/index.html"
231
+ expandable
232
+ ></kai-artifact>
233
+ </kai-resizable-item>
234
+ </kai-resizable>
235
+
236
+ <script type="module">
237
+ import '@kitn.ai/ui/elements';
238
+
239
+ // Optional: observe the layout events.
240
+ document.querySelector('kai-resizable')
241
+ .addEventListener('kai-change', (e) => console.log('change', e.detail.sizes));
242
+ document.querySelector('kai-resizable')
243
+ .addEventListener('kai-maximize-change', (e) => console.log('maximizechange', e.detail));
244
+ document.querySelector('kai-artifact')
245
+ .addEventListener('kai-maximize-change', (e) => console.log('artifact maximizechange', e.detail));
246
+ </script>
247
+
248
+ <!-- Cross-element protocol (hand-authored; not generated):
249
+ • kai-maximize-intent — CustomEvent, bubbles:true, composed:true
250
+ Fired by <kai-artifact> when the Expand/Collapse button is clicked.
251
+ detail: { requested: boolean } (true = maximize, false = restore)
252
+ • kai-maximize-state — CustomEvent, bubbles:false, composed:true
253
+ Dispatched by <kai-resizable> back DOWN onto the maximized <kai-resizable-item>
254
+ (on maximize) or the group host + the formerly-maximized item (on restore)
255
+ so the artifact can reconcile its button label.
256
+ detail: { maximized: boolean }
257
+ These protocol events are NOT listed in the generated web-components.md because
258
+ they are cross-element — the generator only documents per-element dispatch events. -->`;
259
+
260
+ /**
261
+ * **Expand to fill** — the headline integration: the artifact's expand button
262
+ * fills the preview panel to the full container width. No wiring between the
263
+ * two elements is needed — clicking **Expand** fires a `kai-maximize-intent` event
264
+ * that bubbles up to the nearest enclosing `<kai-resizable>`, which hides siblings
265
+ * and lets the preview panel fill. **Collapse** (or **Escape**) restores the
266
+ * original layout.
267
+ *
268
+ * **Cross-element protocol (hand-authored docs):**
269
+ * - `kai-maximize-intent` — `bubbles:true, composed:true`; fired by `<kai-artifact>`
270
+ * when Expand/Collapse is toggled. `detail: { requested: boolean }`.
271
+ * - `kai-maximize-state` — `bubbles:false, composed:true`; dispatched by
272
+ * `<kai-resizable>` back down to the affected `<kai-resizable-item>` so the
273
+ * artifact can reconcile its button. `detail: { maximized: boolean }`.
274
+ *
275
+ * These protocol events are NOT in the generated `web-components.md`
276
+ * (the generator only documents per-element `dispatch` events — resolved decision #1).
277
+ */
278
+ export const ExpandToFill: Story = {
279
+ name: 'Expand to fill',
280
+ render: () => {
281
+ const [log, setLog] = createSignal<string[]>([]);
282
+ let artifactEl: HTMLElement & { files?: ArtifactFile[] };
283
+ let resizableEl: HTMLElement;
284
+ onMount(() => {
285
+ if (artifactEl) artifactEl.files = ARTIFACT_FILES;
286
+ if (resizableEl) {
287
+ resizableEl.addEventListener('kai-change', (e: Event) =>
288
+ setLog((l) => [`change → ${JSON.stringify((e as CustomEvent).detail.sizes)}`, ...l].slice(0, 6)),
289
+ );
290
+ resizableEl.addEventListener('kai-maximize-change', (e: Event) =>
291
+ setLog((l) => [`kai-maximize-change → ${JSON.stringify((e as CustomEvent).detail)}`, ...l].slice(0, 6)),
292
+ );
293
+ }
294
+ if (artifactEl) {
295
+ artifactEl.addEventListener('kai-maximize-change', (e: Event) =>
296
+ setLog((l) => [`artifact kai-maximize-change → ${JSON.stringify((e as CustomEvent).detail)}`, ...l].slice(0, 6)),
297
+ );
298
+ }
299
+ });
300
+ return (
301
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}>
302
+ <div
303
+ style={{
304
+ height: '480px',
305
+ width: '100%',
306
+ 'max-width': '900px',
307
+ border: '1px solid var(--color-border, #e4e4e7)',
308
+ 'border-radius': '8px',
309
+ overflow: 'hidden',
310
+ }}
311
+ >
312
+ <kai-resizable
313
+ ref={(e) => (resizableEl = e as HTMLElement)}
314
+ orientation="horizontal"
315
+ >
316
+ <kai-resizable-item size="20%" min="120px">
317
+ <Pane label="List" />
318
+ </kai-resizable-item>
319
+ <kai-resizable-item min="140px">
320
+ <Pane label="Chat" tone="plain" />
321
+ </kai-resizable-item>
322
+ <kai-resizable-item size="35%" min="200px">
323
+ <kai-artifact
324
+ ref={(e) => (artifactEl = e as HTMLElement & { files?: ArtifactFile[] })}
325
+ src={`${BASE}/index.html`}
326
+ iframe-title="Starboard artifact preview"
327
+ expandable
328
+ />
329
+ </kai-resizable-item>
330
+ </kai-resizable>
331
+ </div>
332
+ <pre
333
+ style={{
334
+ 'font-size': '12px',
335
+ 'max-width': '900px',
336
+ padding: '10px 12px',
337
+ 'border-radius': '8px',
338
+ background: 'var(--color-muted, #f4f4f5)',
339
+ color: 'var(--color-muted-foreground, #71717a)',
340
+ 'min-height': '72px',
341
+ margin: '0',
342
+ }}
343
+ >
344
+ {log().length ? log().join('\n') : '(click the ⤢ Expand button in the preview toolbar…)'}
345
+ </pre>
346
+ </div>
347
+ );
348
+ },
349
+ parameters: { docs: { source: { code: EXPAND_TO_FILL_SNIPPET, language: 'html' } } },
350
+ };
351
+
352
+ const SOLID_PARITY_SNIPPET = `// SolidJS — Artifact inside Resizable with maximizedIndex/onMaximizeChange.
353
+ // No web components needed; works in a pure-Solid app.
354
+ import { createSignal } from 'solid-js';
355
+ import { Artifact } from '@kitn.ai/ui/components';
356
+ import { Resizable, ResizablePanel } from '@kitn.ai/ui/ui';
357
+
358
+ function App() {
359
+ const [maximizedIndex, setMaximizedIndex] = createSignal<number | null>(null);
360
+ return (
361
+ <Resizable
362
+ maximizedIndex={maximizedIndex()}
363
+ onMaximizeChange={setMaximizedIndex}
364
+ style="height:480px"
365
+ >
366
+ <ResizablePanel defaultSize="20%" minSize="120px">…list…</ResizablePanel>
367
+ <ResizablePanel minSize="140px">…chat…</ResizablePanel>
368
+ <ResizablePanel defaultSize="35%" minSize="200px">
369
+ <Artifact
370
+ src="…"
371
+ expandable
372
+ maximized={maximizedIndex() === 2}
373
+ onMaximizeChange={(m) => setMaximizedIndex(m ? 2 : null)}
374
+ />
375
+ </ResizablePanel>
376
+ </Resizable>
377
+ );
378
+ }`;
379
+
380
+ /**
381
+ * **SolidJS parity** — the same `list | chat | artifact` layout using the Solid
382
+ * `Resizable` convenience and the `Artifact` component directly (no web components).
383
+ * `maximizedIndex` / `onMaximizeChange` on `Resizable` mirror the web-component
384
+ * protocol at the Solid level. Wire `Artifact`'s `onMaximizeChange` to set the
385
+ * index and pass `maximized` back down to drive the button.
386
+ */
387
+ export const SolidParity: Story = {
388
+ name: 'SolidJS parity (Resizable + Artifact)',
389
+ render: () => {
390
+ const [maximizedIndex, setMaximizedIndex] = createSignal<number | null>(null);
391
+ return (
392
+ <div
393
+ style={{
394
+ height: '480px',
395
+ width: '100%',
396
+ 'max-width': '900px',
397
+ border: '1px solid var(--color-border, #e4e4e7)',
398
+ 'border-radius': '8px',
399
+ overflow: 'hidden',
400
+ }}
401
+ >
402
+ <Resizable
403
+ maximizedIndex={maximizedIndex()}
404
+ onMaximizeChange={setMaximizedIndex}
405
+ >
406
+ <ResizablePanel defaultSize="20%" minSize="120px">
407
+ <Pane label="List" />
408
+ </ResizablePanel>
409
+ <ResizablePanel minSize="140px">
410
+ <Pane label="Chat" tone="plain" />
411
+ </ResizablePanel>
412
+ <ResizablePanel defaultSize="35%" minSize="200px">
413
+ <Artifact
414
+ src={`${BASE}/index.html`}
415
+ iframeTitle="Starboard artifact preview"
416
+ expandable
417
+ maximized={maximizedIndex() === 2}
418
+ onMaximizeChange={(m) => setMaximizedIndex(m ? 2 : null)}
419
+ />
420
+ </ResizablePanel>
421
+ </Resizable>
422
+ </div>
423
+ );
424
+ },
425
+ parameters: { docs: { source: { code: SOLID_PARITY_SNIPPET, language: 'tsx' } } },
426
+ };