@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,399 @@
1
+ import { type JSX, createContext, useContext, splitProps, Show } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { Button } from '../ui/button';
4
+ import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card';
5
+ import {
6
+ FileText,
7
+ Globe,
8
+ Image as ImageIcon,
9
+ Music2,
10
+ Paperclip,
11
+ Video,
12
+ X,
13
+ } from 'lucide-solid';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export interface AttachmentData {
20
+ id: string;
21
+ type: 'file' | 'source-document';
22
+ filename?: string;
23
+ mediaType?: string;
24
+ url?: string;
25
+ title?: string;
26
+ }
27
+
28
+ export type AttachmentMediaCategory = 'image' | 'video' | 'audio' | 'document' | 'source' | 'unknown';
29
+ export type AttachmentVariant = 'grid' | 'inline' | 'list';
30
+
31
+ const mediaCategoryIcons: Record<AttachmentMediaCategory, typeof ImageIcon> = {
32
+ audio: Music2,
33
+ document: FileText,
34
+ image: ImageIcon,
35
+ source: Globe,
36
+ unknown: Paperclip,
37
+ video: Video,
38
+ };
39
+
40
+ // ============================================================================
41
+ // Utility Functions
42
+ // ============================================================================
43
+
44
+ export const getMediaCategory = (data: AttachmentData): AttachmentMediaCategory => {
45
+ if (data.type === 'source-document') {
46
+ return 'source';
47
+ }
48
+
49
+ const mediaType = data.mediaType ?? '';
50
+
51
+ if (mediaType.startsWith('image/')) return 'image';
52
+ if (mediaType.startsWith('video/')) return 'video';
53
+ if (mediaType.startsWith('audio/')) return 'audio';
54
+ if (mediaType.startsWith('application/') || mediaType.startsWith('text/')) return 'document';
55
+
56
+ return 'unknown';
57
+ };
58
+
59
+ export const getAttachmentLabel = (data: AttachmentData): string => {
60
+ if (data.type === 'source-document') {
61
+ return data.title || data.filename || 'Source';
62
+ }
63
+
64
+ const category = getMediaCategory(data);
65
+ return data.filename || (category === 'image' ? 'Image' : 'Attachment');
66
+ };
67
+
68
+ // ============================================================================
69
+ // Contexts
70
+ // ============================================================================
71
+
72
+ interface AttachmentsContextValue {
73
+ variant: AttachmentVariant;
74
+ }
75
+
76
+ const AttachmentsContext = createContext<AttachmentsContextValue>();
77
+
78
+ interface AttachmentContextValue {
79
+ data: AttachmentData;
80
+ mediaCategory: AttachmentMediaCategory;
81
+ onRemove?: () => void;
82
+ variant: AttachmentVariant;
83
+ }
84
+
85
+ const AttachmentContext = createContext<AttachmentContextValue>();
86
+
87
+ // ============================================================================
88
+ // Hooks
89
+ // ============================================================================
90
+
91
+ export const useAttachmentsContext = () =>
92
+ useContext(AttachmentsContext) ?? { variant: 'grid' as const };
93
+
94
+ export const useAttachmentContext = () => {
95
+ const ctx = useContext(AttachmentContext);
96
+ if (!ctx) {
97
+ throw new Error('Attachment components must be used within <Attachment>');
98
+ }
99
+ return ctx;
100
+ };
101
+
102
+ // ============================================================================
103
+ // Attachments - Container
104
+ // ============================================================================
105
+
106
+ export interface AttachmentsProps extends JSX.HTMLAttributes<HTMLDivElement> {
107
+ variant?: AttachmentVariant;
108
+ }
109
+
110
+ function Attachments(props: AttachmentsProps) {
111
+ const [local, rest] = splitProps(props, ['variant', 'class', 'children']);
112
+ const variant = () => local.variant ?? 'grid';
113
+
114
+ return (
115
+ <AttachmentsContext.Provider value={{ get variant() { return variant(); } }}>
116
+ <div
117
+ class={cn(
118
+ 'flex items-start',
119
+ variant() === 'list' ? 'flex-col gap-2' : 'flex-wrap gap-2',
120
+ variant() === 'grid' && 'w-fit',
121
+ local.class,
122
+ )}
123
+ {...rest}
124
+ >
125
+ {local.children}
126
+ </div>
127
+ </AttachmentsContext.Provider>
128
+ );
129
+ }
130
+
131
+ // ============================================================================
132
+ // Attachment - Item
133
+ // ============================================================================
134
+
135
+ export interface AttachmentProps extends JSX.HTMLAttributes<HTMLDivElement> {
136
+ data: AttachmentData;
137
+ onRemove?: () => void;
138
+ }
139
+
140
+ function Attachment(props: AttachmentProps) {
141
+ const [local, rest] = splitProps(props, ['data', 'onRemove', 'class', 'children']);
142
+ // Read the getter reactively — DON'T destructure, or the variant is captured
143
+ // once and the item never re-lays-out when the container variant changes.
144
+ const ctx = useAttachmentsContext();
145
+ const mediaCategory = () => getMediaCategory(local.data);
146
+
147
+ return (
148
+ <AttachmentContext.Provider
149
+ value={{
150
+ get data() { return local.data; },
151
+ get mediaCategory() { return mediaCategory(); },
152
+ get onRemove() { return local.onRemove; },
153
+ get variant() { return ctx.variant; },
154
+ }}
155
+ >
156
+ <div
157
+ class={cn(
158
+ 'group relative',
159
+ ctx.variant === 'grid' && 'size-24 overflow-hidden rounded-lg',
160
+ ctx.variant === 'inline' && [
161
+ 'flex h-8 cursor-pointer select-none items-center gap-1.5',
162
+ 'rounded-md bg-muted/50 px-1.5 text-foreground',
163
+ 'font-medium text-sm transition-all',
164
+ 'hover:bg-muted',
165
+ ],
166
+ ctx.variant === 'list' && [
167
+ 'flex w-full items-center gap-3 rounded-lg bg-muted/30 p-3 text-foreground',
168
+ 'hover:bg-muted/50',
169
+ ],
170
+ local.class,
171
+ )}
172
+ {...rest}
173
+ >
174
+ {local.children}
175
+ </div>
176
+ </AttachmentContext.Provider>
177
+ );
178
+ }
179
+
180
+ // ============================================================================
181
+ // AttachmentPreview - Media preview
182
+ // ============================================================================
183
+
184
+ export interface AttachmentPreviewProps extends JSX.HTMLAttributes<HTMLDivElement> {
185
+ fallbackIcon?: JSX.Element;
186
+ }
187
+
188
+ function AttachmentPreview(props: AttachmentPreviewProps) {
189
+ const [local, rest] = splitProps(props, ['fallbackIcon', 'class']);
190
+ const ctx = useAttachmentContext();
191
+
192
+ const iconSize = () => ctx.variant === 'inline' ? 'size-3' : 'size-4';
193
+
194
+ const renderIcon = (Icon: typeof ImageIcon) => (
195
+ <Icon class={cn(iconSize(), 'text-muted-foreground')} />
196
+ );
197
+
198
+ const renderContent = () => {
199
+ if (ctx.mediaCategory === 'image' && ctx.data.type === 'file' && ctx.data.url) {
200
+ return ctx.variant === 'grid' ? (
201
+ <img
202
+ alt={ctx.data.filename || 'Image'}
203
+ class="size-full object-cover"
204
+ height={96}
205
+ src={ctx.data.url}
206
+ width={96}
207
+ />
208
+ ) : (
209
+ <img
210
+ alt={ctx.data.filename || 'Image'}
211
+ class="size-full rounded object-cover"
212
+ height={20}
213
+ src={ctx.data.url}
214
+ width={20}
215
+ />
216
+ );
217
+ }
218
+
219
+ if (ctx.mediaCategory === 'video' && ctx.data.type === 'file' && ctx.data.url) {
220
+ return <video class="size-full object-cover" muted src={ctx.data.url} />;
221
+ }
222
+
223
+ const Icon = mediaCategoryIcons[ctx.mediaCategory];
224
+ return local.fallbackIcon ?? renderIcon(Icon);
225
+ };
226
+
227
+ return (
228
+ <div
229
+ class={cn(
230
+ 'flex shrink-0 items-center justify-center overflow-hidden',
231
+ ctx.variant === 'grid' && 'size-full bg-muted',
232
+ ctx.variant === 'inline' && 'size-5 rounded bg-background',
233
+ ctx.variant === 'list' && 'size-12 rounded bg-muted',
234
+ local.class,
235
+ )}
236
+ {...rest}
237
+ >
238
+ {renderContent()}
239
+ </div>
240
+ );
241
+ }
242
+
243
+ // ============================================================================
244
+ // AttachmentInfo - Name and type display
245
+ // ============================================================================
246
+
247
+ export interface AttachmentInfoProps extends JSX.HTMLAttributes<HTMLDivElement> {
248
+ showMediaType?: boolean;
249
+ }
250
+
251
+ function AttachmentInfo(props: AttachmentInfoProps) {
252
+ const [local, rest] = splitProps(props, ['showMediaType', 'class']);
253
+ const ctx = useAttachmentContext();
254
+ const label = () => getAttachmentLabel(ctx.data);
255
+
256
+ return (
257
+ <Show when={ctx.variant !== 'grid'}>
258
+ <div class={cn('min-w-0 flex-1', local.class)} {...rest}>
259
+ <span class="block truncate">{label()}</span>
260
+ {/* The media-type subtitle is a two-line affordance — only the `list`
261
+ variant has room for it; `inline` chips are a fixed single-line height. */}
262
+ <Show when={local.showMediaType && ctx.variant === 'list' && ctx.data.mediaType}>
263
+ <span class="text-muted-foreground text-caption block truncate">
264
+ {ctx.data.mediaType}
265
+ </span>
266
+ </Show>
267
+ </div>
268
+ </Show>
269
+ );
270
+ }
271
+
272
+ // ============================================================================
273
+ // AttachmentRemove - Remove button
274
+ // ============================================================================
275
+
276
+ export interface AttachmentRemoveProps {
277
+ label?: string;
278
+ class?: string;
279
+ children?: JSX.Element;
280
+ }
281
+
282
+ function AttachmentRemove(props: AttachmentRemoveProps) {
283
+ const ctx = useAttachmentContext();
284
+
285
+ const handleClick = (e: MouseEvent) => {
286
+ e.stopPropagation();
287
+ ctx.onRemove?.();
288
+ };
289
+
290
+ return (
291
+ <Show when={ctx.onRemove}>
292
+ <Button
293
+ aria-label={props.label ?? 'Remove'}
294
+ class={cn(
295
+ ctx.variant === 'grid' && [
296
+ 'absolute top-2 right-2 size-6 rounded-full p-0',
297
+ 'bg-background/80 backdrop-blur-sm',
298
+ 'opacity-0 transition-opacity group-hover:opacity-100',
299
+ 'hover:bg-background',
300
+ '[&>svg]:size-3',
301
+ ],
302
+ ctx.variant === 'inline' && [
303
+ 'size-5 rounded p-0',
304
+ 'opacity-0 transition-opacity group-hover:opacity-100',
305
+ '[&>svg]:size-2.5',
306
+ ],
307
+ ctx.variant === 'list' && ['size-8 shrink-0 rounded p-0', '[&>svg]:size-4'],
308
+ props.class,
309
+ )}
310
+ onClick={handleClick}
311
+ type="button"
312
+ variant="ghost"
313
+ >
314
+ {props.children ?? <X />}
315
+ <span class="sr-only">{props.label ?? 'Remove'}</span>
316
+ </Button>
317
+ </Show>
318
+ );
319
+ }
320
+
321
+ // ============================================================================
322
+ // AttachmentHoverCard - Hover preview
323
+ // ============================================================================
324
+
325
+ export interface AttachmentHoverCardProps {
326
+ children: JSX.Element;
327
+ openDelay?: number;
328
+ closeDelay?: number;
329
+ }
330
+
331
+ function AttachmentHoverCard(props: AttachmentHoverCardProps) {
332
+ return (
333
+ <HoverCardRoot
334
+ openDelay={props.openDelay ?? 0}
335
+ closeDelay={props.closeDelay ?? 0}
336
+ >
337
+ {props.children}
338
+ </HoverCardRoot>
339
+ );
340
+ }
341
+
342
+ export interface AttachmentHoverCardTriggerProps {
343
+ children: JSX.Element;
344
+ class?: string;
345
+ }
346
+
347
+ function AttachmentHoverCardTrigger(props: AttachmentHoverCardTriggerProps) {
348
+ return <HoverCardTrigger class={props.class}>{props.children}</HoverCardTrigger>;
349
+ }
350
+
351
+ export interface AttachmentHoverCardContentProps {
352
+ children: JSX.Element;
353
+ class?: string;
354
+ }
355
+
356
+ function AttachmentHoverCardContent(props: AttachmentHoverCardContentProps) {
357
+ return (
358
+ <HoverCardContent class={cn('w-auto p-2', props.class)}>
359
+ {props.children}
360
+ </HoverCardContent>
361
+ );
362
+ }
363
+
364
+ // ============================================================================
365
+ // AttachmentEmpty - Empty state
366
+ // ============================================================================
367
+
368
+ export interface AttachmentEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {}
369
+
370
+ function AttachmentEmpty(props: AttachmentEmptyProps) {
371
+ const [local, rest] = splitProps(props, ['class', 'children']);
372
+ return (
373
+ <div
374
+ class={cn(
375
+ 'flex items-center justify-center p-4 text-muted-foreground text-sm',
376
+ local.class,
377
+ )}
378
+ {...rest}
379
+ >
380
+ {local.children ?? 'No attachments'}
381
+ </div>
382
+ );
383
+ }
384
+
385
+ // ============================================================================
386
+ // Exports
387
+ // ============================================================================
388
+
389
+ export {
390
+ Attachments,
391
+ Attachment,
392
+ AttachmentPreview,
393
+ AttachmentInfo,
394
+ AttachmentRemove,
395
+ AttachmentHoverCard,
396
+ AttachmentHoverCardTrigger,
397
+ AttachmentHoverCardContent,
398
+ AttachmentEmpty,
399
+ };
@@ -0,0 +1,28 @@
1
+ // src/components/card-fallback.tsx
2
+ // Rendered by the dispatcher when an envelope's `type` has no registered card.
3
+ // Visual: reuses the Card chrome so it sits naturally in a card stream.
4
+ import type { JSX } from 'solid-js';
5
+ import { Card } from './card';
6
+ import { AlertTriangle } from 'lucide-solid';
7
+
8
+ export interface CardFallbackProps {
9
+ /** The unrecognized envelope type, shown to aid debugging. */
10
+ type: string;
11
+ /** The envelope id (for parity with cards; not displayed). */
12
+ cardId?: string;
13
+ }
14
+
15
+ /** Inert, themed fallback for an unsupported card type. Emits nothing itself —
16
+ * the dispatcher emits the contract `error` event alongside rendering this. */
17
+ export function CardFallback(props: CardFallbackProps): JSX.Element {
18
+ return (
19
+ <Card>
20
+ <div role="alert" class="flex items-center gap-2 p-4 text-sm text-muted-foreground">
21
+ <AlertTriangle class="size-4 shrink-0 text-destructive dark:text-red-400" aria-hidden="true" />
22
+ <span>
23
+ Unsupported card type: <code class="font-mono">{props.type}</code>
24
+ </span>
25
+ </div>
26
+ </Card>
27
+ );
28
+ }
@@ -0,0 +1,52 @@
1
+ // src/components/card-renderer.tsx
2
+ // The Solid single-envelope dispatcher: pick the card for envelope.type and render it
3
+ // with the envelope spread onto its props. Routing uses the ambient CardProvider
4
+ // (useCardHost). Unknown type → CardFallback + a one-shot contract `error` emit.
5
+ import { createMemo, untrack, Show, type JSX } from 'solid-js';
6
+ import { Dynamic } from 'solid-js/web';
7
+ import type { CardEnvelope } from '../primitives/card-contract';
8
+ import { useCardHost } from '../primitives/card-host';
9
+ import { mergeCardComponents, type CardComponentMap } from '../primitives/card-registry';
10
+ import { CardFallback } from './card-fallback';
11
+
12
+ export interface CardRendererProps {
13
+ envelope: CardEnvelope;
14
+ /** Add/override type→component entries (merged over the built-ins). */
15
+ types?: CardComponentMap;
16
+ }
17
+
18
+ export function CardRenderer(props: CardRendererProps): JSX.Element {
19
+ const host = useCardHost();
20
+ const map = createMemo(() => mergeCardComponents(props.types));
21
+ const entry = createMemo(() => map()[props.envelope.type]);
22
+
23
+ return (
24
+ <Show
25
+ when={entry()}
26
+ fallback={<UnknownCard envelope={props.envelope} />}
27
+ >
28
+ {(comp) => <Dynamic component={comp()} envelope={props.envelope} host={host} />}
29
+ </Show>
30
+ );
31
+ }
32
+
33
+ /** Renders the fallback and emits exactly one `error` (untracked, on first render). */
34
+ function UnknownCard(props: { envelope: CardEnvelope }): JSX.Element {
35
+ const host = useCardHost();
36
+ untrack(() =>
37
+ host?.emit({
38
+ kind: 'error',
39
+ cardId: props.envelope.id,
40
+ message: `Unsupported card type: ${props.envelope.type}`,
41
+ }),
42
+ );
43
+ return <CardFallback type={props.envelope.type} cardId={props.envelope.id} />;
44
+ }
45
+
46
+ /** Function sugar: renderCard(env) ≡ <CardRenderer envelope={env} />. */
47
+ export function renderCard(
48
+ envelope: CardEnvelope,
49
+ opts?: { types?: CardComponentMap },
50
+ ): JSX.Element {
51
+ return <CardRenderer envelope={envelope} types={opts?.types} />;
52
+ }
@@ -0,0 +1,110 @@
1
+ import { type JSX, Show, splitProps, mergeProps, createUniqueId } from 'solid-js';
2
+ import { cn } from '../utils/cn';
3
+ import { AlertTriangle } from 'lucide-solid';
4
+
5
+ export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement> {
6
+ /** Heading rendered in the card chrome (the contract's CardEnvelope.title).
7
+ * NB: NOT `title` — `title` is a reserved IDL attr (see define.tsx RESERVED). */
8
+ heading?: string;
9
+ /** Optional supporting text under the heading. */
10
+ description?: string;
11
+ /** Media region (image/icon) rendered above the heading. */
12
+ media?: JSX.Element;
13
+ /** Footer action region (buttons). */
14
+ actions?: JSX.Element;
15
+ /** When set, the card renders its standard inline error state INSTEAD of body. */
16
+ errorMessage?: string;
17
+ /** Compact spacing for dense lists of cards. */
18
+ dense?: boolean;
19
+ /** Stable id for the heading (so composing cards can `aria-labelledby` it).
20
+ * Auto-generated when omitted. */
21
+ headingId?: string;
22
+ }
23
+
24
+ /**
25
+ * `Card` — the shared presentational chrome every native card composes from:
26
+ * an optional media region, a heading + description, a body (default slot), an
27
+ * actions footer, and one consistent inline **error** state (the contract's
28
+ * "never a broken/partial card" rule). It is intentionally chrome-only: it reads
29
+ * no `CardContext` and emits no `CardEvent` — the cards that compose it (e.g.
30
+ * `kai-form`) own the contract interaction.
31
+ */
32
+ export function Card(props: CardProps): JSX.Element {
33
+ const merged = mergeProps({ dense: false }, props);
34
+ const [local, rest] = splitProps(merged, [
35
+ 'heading',
36
+ 'description',
37
+ 'media',
38
+ 'actions',
39
+ 'errorMessage',
40
+ 'dense',
41
+ 'headingId',
42
+ 'class',
43
+ 'children',
44
+ ]);
45
+
46
+ const autoId = createUniqueId();
47
+ const headingId = () => local.headingId ?? `kai-card-heading-${autoId}`;
48
+ const hasError = () => local.errorMessage !== undefined && local.errorMessage !== '';
49
+ const hasHeader = () => Boolean(local.heading) || Boolean(local.description);
50
+
51
+ return (
52
+ <div
53
+ class={cn(
54
+ 'flex flex-col rounded-xl border border-border bg-card text-card-foreground kai-elevation-sm',
55
+ local.dense ? 'gap-2.5 p-3.5' : 'gap-4 p-5',
56
+ local.class,
57
+ )}
58
+ {...rest}
59
+ >
60
+ <Show when={local.media}>
61
+ <div class="overflow-hidden rounded-lg">{local.media}</div>
62
+ </Show>
63
+
64
+ <Show when={hasHeader()}>
65
+ <div
66
+ class={cn(
67
+ 'flex flex-col border-b border-border',
68
+ local.dense ? 'gap-0.5 pb-2.5' : 'gap-1 pb-4',
69
+ )}
70
+ >
71
+ <Show when={local.heading}>
72
+ <h3
73
+ id={headingId()}
74
+ class="text-[1.0625rem] font-semibold leading-snug tracking-tight text-foreground"
75
+ >
76
+ {local.heading}
77
+ </h3>
78
+ </Show>
79
+ <Show when={local.description}>
80
+ <p class="text-[0.8125rem] leading-relaxed text-muted-foreground">{local.description}</p>
81
+ </Show>
82
+ </div>
83
+ </Show>
84
+
85
+ <Show
86
+ when={hasError()}
87
+ fallback={
88
+ <>
89
+ <Show when={local.children}>
90
+ <div class="text-sm text-foreground">{local.children}</div>
91
+ </Show>
92
+ <Show when={local.actions}>
93
+ <div class="flex flex-wrap items-center justify-end gap-2 pt-1">
94
+ {local.actions}
95
+ </div>
96
+ </Show>
97
+ </>
98
+ }
99
+ >
100
+ <div
101
+ role="alert"
102
+ class="flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive dark:bg-destructive/15 dark:text-red-400"
103
+ >
104
+ <AlertTriangle size={16} class="mt-0.5 shrink-0" aria-hidden="true" />
105
+ <span>{local.errorMessage}</span>
106
+ </div>
107
+ </Show>
108
+ </div>
109
+ );
110
+ }