@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,238 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Docs/Frameworks/Vue" />
4
+
5
+ # Vue
6
+
7
+ Use the `kai-*` custom elements directly in Vue templates. Pass arrays and objects via the **`.prop`
8
+ modifier** so Vue sets them as live DOM properties (not stringified attributes). Events use the
9
+ standard `@event` syntax.
10
+
11
+ There are **two ways to build with the kit**, and you can mix them:
12
+
13
+ 1. **`<kai-chat>`** — the batteries-included shell: a whole chat experience in one tag. Fastest start.
14
+ 2. **Compose the individual elements** (`<kai-conversations>`, `<kai-markdown>`, `<kai-artifact>`, …)
15
+ into your own layout when you want full control.
16
+
17
+ Both are shown below.
18
+
19
+ ## Install & setup
20
+
21
+ ```bash
22
+ npm i @kitn.ai/ui
23
+ ```
24
+
25
+ Register the custom elements **once**, as a static side-effect import in `main.ts` **before**
26
+ `createApp().mount()`. Vue stamps the tags at mount time — if the elements aren't registered yet,
27
+ array/object props set by Vue are clobbered when the elements upgrade later and the UI renders blank.
28
+
29
+ ```ts
30
+ // src/main.ts
31
+ import '@kitn.ai/ui/elements'; // MUST come before createApp
32
+ import { createApp } from 'vue';
33
+ import App from './App.vue';
34
+
35
+ createApp(App).mount('#app');
36
+ ```
37
+
38
+ Tell Vue's template compiler that `kai-*` tags are native custom elements, not Vue components —
39
+ this prevents "Unknown custom element" warnings and ensures `.prop` bindings work correctly:
40
+
41
+ ```ts
42
+ // vite.config.ts
43
+ import { defineConfig } from 'vite';
44
+ import vue from '@vitejs/plugin-vue';
45
+
46
+ export default defineConfig({
47
+ plugins: [
48
+ vue({
49
+ template: {
50
+ compilerOptions: {
51
+ isCustomElement: (tag) => tag.startsWith('kai-'),
52
+ },
53
+ },
54
+ }),
55
+ ],
56
+ });
57
+ ```
58
+
59
+ No CSS to import: each element is styled inside its own Shadow DOM. Only pull in
60
+ `@kitn.ai/ui/theme.css` if you want to override design tokens (see **Theming**).
61
+
62
+ ### TypeScript / Volar augmentation
63
+
64
+ Add a reference to the kit's type declarations once so Vue's template compiler (and Volar) knows
65
+ the element's attributes and properties:
66
+
67
+ ```ts
68
+ // env.d.ts (or vite-env.d.ts)
69
+ /// <reference types="@kitn.ai/ui/elements" />
70
+ ```
71
+
72
+ ## Quick start — the all-in-one shell
73
+
74
+ `<kai-chat>` is **transport-agnostic**: give it a `messages` array, handle the `submit` event, and
75
+ stream your model's reply back into state. You own the request; the element owns the UI.
76
+
77
+ ```html
78
+ <script setup lang="ts">
79
+ import '@kitn.ai/ui/elements';
80
+ import { ref } from 'vue';
81
+
82
+ type Message = { id: string; role: 'user' | 'assistant'; content: string };
83
+
84
+ const messages = ref<Message[]>([
85
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?' },
86
+ ]);
87
+
88
+ const handleSubmit = async (e: CustomEvent<{ value: string }>) => {
89
+ const history = [...messages.value, { id: crypto.randomUUID(), role: 'user' as const, content: e.detail.value }];
90
+ messages.value = history;
91
+
92
+ const aid = crypto.randomUUID();
93
+ messages.value = [...history, { id: aid, role: 'assistant', content: '' }];
94
+
95
+ let answer = '';
96
+ for await (const token of streamFromYourAPI(history)) {
97
+ answer += token;
98
+ messages.value = messages.value.map((m) => (m.id === aid ? { ...m, content: answer } : m));
99
+ }
100
+ };
101
+ </script>
102
+
103
+ <template>
104
+ <!-- The elements fill their container — use flex and let the element grow with
105
+ flex: 1 rather than hard-coding a height. -->
106
+ <div style="display: flex; flex-direction: column; height: 100dvh;">
107
+ <kai-chat
108
+ :messages.prop="messages"
109
+ :suggestions.prop="['Summarize the chat', 'Start fresh']"
110
+ style="flex: 1; min-height: 0;"
111
+ @kai-submit="handleSubmit"
112
+ />
113
+ </div>
114
+ </template>
115
+ ```
116
+
117
+ ## Go further — compose the pieces
118
+
119
+ `<kai-chat>` is one option, not the only one. Every element can be composed in your own layout.
120
+ Here's a multi-conversation shell — a `<kai-conversations>` sidebar next to the `<kai-chat>` thread:
121
+
122
+ ```html
123
+ <script setup lang="ts">
124
+ import '@kitn.ai/ui/elements';
125
+ import { ref } from 'vue';
126
+
127
+ const conversations = ref([
128
+ {
129
+ id: 'c1',
130
+ title: 'First chat',
131
+ scope: { type: 'document' },
132
+ messageCount: 3,
133
+ lastMessageAt: '2026-06-01T12:00:00Z',
134
+ updatedAt: '2026-06-01T12:00:00Z',
135
+ },
136
+ ]);
137
+ const activeId = ref('c1');
138
+ const messages = ref([{ id: '1', role: 'assistant', content: 'Hi!' }]);
139
+
140
+ const onSelect = (e: CustomEvent<{ id: string }>) => {
141
+ activeId.value = e.detail.id;
142
+ // load messages for the selected conversation
143
+ };
144
+
145
+ const onSubmit = (e: CustomEvent<{ value: string }>) => {
146
+ // send message and stream reply
147
+ };
148
+ </script>
149
+
150
+ <template>
151
+ <!-- Lay panels out with flex: the sidebar is fixed-width, the thread takes the rest
152
+ with flex: 1. The elements fill whatever box you give them. -->
153
+ <div style="display: flex; height: 100dvh;">
154
+ <kai-conversations
155
+ :conversations.prop="conversations"
156
+ :active-id="activeId"
157
+ style="width: 300px; flex-shrink: 0;"
158
+ @kai-conversation-select="onSelect"
159
+ @kai-new-chat="startNewConversation"
160
+ />
161
+ <kai-chat
162
+ :messages.prop="messages"
163
+ style="flex: 1; min-width: 0;"
164
+ @kai-submit="onSubmit"
165
+ />
166
+ </div>
167
+ </template>
168
+ ```
169
+
170
+ ### Make the panels resizable
171
+
172
+ Want a draggable divider between the sidebar and the thread? Wrap the panels in `<kai-resizable>`
173
+ with one `<kai-resizable-item>` each — the handles are inserted for you (up to 3 panels). Each item
174
+ takes a `size` (px or `%`) plus optional `min`/`max`; listen for `@change` (`detail.sizes`) to
175
+ persist the layout.
176
+
177
+ ```html
178
+ <template>
179
+ <div style="display: flex; flex-direction: column; height: 100dvh;">
180
+ <kai-resizable orientation="horizontal" style="flex: 1; min-height: 0;">
181
+ <kai-resizable-item size="25%" min="200px">
182
+ <kai-conversations
183
+ :conversations.prop="conversations"
184
+ :active-id="activeId"
185
+ @kai-conversation-select="onSelect"
186
+ />
187
+ </kai-resizable-item>
188
+ <kai-resizable-item>
189
+ <kai-chat :messages.prop="messages" @kai-submit="onSubmit" />
190
+ </kai-resizable-item>
191
+ </kai-resizable>
192
+ </div>
193
+ </template>
194
+ ```
195
+
196
+ You can also drop **standalone display elements** anywhere in your own UI — `<kai-markdown>`,
197
+ `<kai-code-block>`, `<kai-artifact>`, `<kai-reasoning>`, `<kai-tool>` — to render rich AI content
198
+ without adopting the whole chat. Each fills its container and is controlled via props and events.
199
+
200
+ > **See it all assembled:** **[Examples → Full Chat App](?path=/story/examples-full-chat-app--default)**
201
+ > wires a sidebar, threaded markdown, reasoning, a tool call, a model switcher, a context meter, and
202
+ > a rich prompt input into one screen — a working reference to crib from.
203
+
204
+ > **Find every element:** browse the **Components** section in the sidebar. Each element's **API**
205
+ > tab lists its props, events, and copy-paste usage for Vue (and every other framework).
206
+
207
+ ## Props & events
208
+
209
+ The rule for all elements: **rich data (arrays, objects) goes in as DOM properties; interactions
210
+ come out as CustomEvents.**
211
+
212
+ In Vue templates, use the **`.prop` modifier** to bind rich data as DOM properties rather than
213
+ stringified HTML attributes. Scalar values (strings, booleans, numbers) can go through normal
214
+ attribute bindings. Events use `@event` and carry their payload in `event.detail`.
215
+
216
+ | Binding type | Vue syntax | When to use |
217
+ |---|---|---|
218
+ | Rich data (array/object) | `:messages.prop="messages"` | Always for arrays and objects |
219
+ | Scalar attribute | `:active-id="activeId"` or `active-id="c1"` | Strings, booleans, numbers |
220
+ | Event | `@kai-conversation-select="onSelect"` | All CustomEvents |
221
+
222
+ Common events and their `detail` payloads:
223
+
224
+ | Element | DOM event | `detail` |
225
+ |---|---|---|
226
+ | `kai-chat` | `submit` | `{ value: string }` |
227
+ | `kai-conversations` | `conversationselect` | `{ id: string }` |
228
+ | `kai-conversations` | `newchat` | — |
229
+ | `kai-conversations` | `togglesidebar` | — |
230
+ | `kai-resizable` | `change` | `{ sizes: string[] }` |
231
+
232
+ Example event handler pattern:
233
+
234
+ ```ts
235
+ const onSelect = (e: CustomEvent<{ id: string }>) => {
236
+ activeId.value = e.detail.id;
237
+ };
238
+ ```
@@ -0,0 +1,186 @@
1
+ {/* src/stories/docs/generative-ui-overview.mdx */}
2
+ import { Meta } from '@storybook/addon-docs/blocks';
3
+
4
+ <Meta title="Generative UI/Overview" />
5
+
6
+ # Generative UI
7
+
8
+ Agents don't just reply with text — they ask the chat to **render typed, interactive
9
+ cards**: a form to fill, an action to approve, a plan to pick from, a link to preview.
10
+ `@kitn.ai/ui` does this with a small, typed **Card Contract** and a turnkey dispatcher.
11
+
12
+ ## The loop
13
+
14
+ ```
15
+ agent/server ──CardEnvelope(s)──▶ host sets <kai-cards>.cards
16
+ ▲ │
17
+ │ dispatcher renders the right kai-* by `type`
18
+ │ │
19
+ └── result / next envelopes ◀─ CardPolicy ◀─ kai-card event ◀─ user interacts
20
+ ```
21
+
22
+ ## Envelopes vs. cards
23
+
24
+ Two things are easy to conflate — keeping them straight makes the rest click:
25
+
26
+ - An **envelope** is what the agent *sends*: an addressed data wrapper, not UI. Like a
27
+ postal envelope, it *carries* a card spec but isn't the card itself.
28
+ - A **card** is what the user *sees*: the rendered, interactive UI the dispatcher mounts
29
+ for that envelope (a `kai-form`, `kai-confirm`, … inside the shared card chrome).
30
+
31
+ The dispatcher's whole job is **envelope in → card out** — one envelope renders one card.
32
+
33
+ Everything an agent sends is a `CardEnvelope`:
34
+
35
+ ```ts
36
+ interface CardEnvelope {
37
+ type: string; // selects the card: form | confirm | choice | tasks | link | embed
38
+ id: string; // correlates every event back to this card
39
+ data: unknown; // follows that type's JSON Schema (shipped in dist/schemas)
40
+ title?: string; // optional chrome heading
41
+ resolution?: CardResolution; // set once the user acts → renders the read-only view
42
+ }
43
+ ```
44
+
45
+ `data` follows the `type`'s **JSON Schema**, so an agent/server can validate before
46
+ sending; `id` correlates every event back to this card. (For **remote** cards, a transport
47
+ `WireFrame` wraps this *same* envelope over `postMessage` — the envelope inside is identical;
48
+ see Remote cards.)
49
+
50
+ ## Turnkey: drop `<kai-cards>`
51
+
52
+ ```html
53
+ <kai-cards></kai-cards>
54
+ <script type="module">
55
+ import '@kitn.ai/ui/elements';
56
+ const cards = document.querySelector('kai-cards');
57
+ cards.cards = [{ type: 'confirm', id: 'deploy', title: 'Deploy?',
58
+ data: { body: 'Ship it?', actions: [{ id: 'go', label: 'Deploy', default: true }] } }];
59
+ cards.policy = { onAction: (id, action) => console.log(id, action) };
60
+ </script>
61
+ ```
62
+
63
+ `<kai-cards>` renders one card per envelope and routes every interaction through `policy`
64
+ (or listen for the raw bubbling `kai-card` event). In SolidJS, use `renderCard(envelope)` /
65
+ `<CardRenderer>` inside a `<CardProvider>`.
66
+
67
+ ## Streaming & follow-ups
68
+
69
+ Swap the `cards` array to add, replace, or remove cards — e.g. replace a `confirm` with a
70
+ result card once the user acts. For **live streaming into a single card**, the SolidJS
71
+ `<CardRenderer>` re-renders reactively as its envelope's `data` changes, so an agent can
72
+ stream into it as work progresses. (Keyed-by-`id` in-place updates within a `<kai-cards>`
73
+ list are a planned refinement.)
74
+
75
+ ## Resolved cards
76
+
77
+ Interactive cards (`kai-confirm`, `kai-choice`, `kai-tasks`, `kai-form`) flip to a **chromed
78
+ read-only** view the moment the user acts — optimistically, before any server round-trip.
79
+
80
+ To make that survive a **reload or history re-hydration**, the card reads an optional
81
+ `resolution` field on its `CardEnvelope`. Persist it by feeding the card's terminal event
82
+ back into the array with the `applyResolution` helper:
83
+
84
+ ```ts
85
+ import { applyResolution } from '@kitn.ai/ui';
86
+
87
+ // <kai-cards> is controlled — persist the resolution so it survives reload:
88
+ el.addEventListener('kai-card', (e) => {
89
+ el.cards = applyResolution(el.cards, e.detail); // resolved cards re-hydrate read-only
90
+ // …then save el.cards to your store/server.
91
+ });
92
+ ```
93
+
94
+ `applyResolution` is pure and safe to call on every `kai-card` event — non-terminal events
95
+ (`ready` / `error` / …) and unknown card ids return the same array unchanged. The terminal
96
+ verbs are `action` (confirm / choice) and `submit` (tasks / form).
97
+
98
+ ## Remote cards
99
+
100
+ Everything above renders cards you **bundle yourself** — the dispatcher mounts a native
101
+ `kai-*` element for each envelope. That native `types` seam stays the easy path: register a
102
+ card type, ship its element, done.
103
+
104
+ **Remote cards** are the power feature for **provider-owned, cross-origin UI** — a card
105
+ whose code lives on a *different* origin (a partner, a plugin, your own card service) and
106
+ that you don't want to bundle or trust in your DOM. `<kai-remote>` renders it inside a
107
+ **sandboxed cross-origin `<iframe>`** and bridges it to the host over `postMessage`, carrying
108
+ the **exact same** `CardEnvelope` / `CardContext` / `CardEvent` shapes and routing every
109
+ event through the **same `CardPolicy`** as a native card. The envelope inside is identical —
110
+ a transport `WireFrame` just wraps it on the wire.
111
+
112
+ ```html
113
+ <kai-remote
114
+ provider-origin="https://cards.provider.example"
115
+ src="https://cards.provider.example/card"
116
+ ></kai-remote>
117
+ <script type="module">
118
+ import '@kitn.ai/ui/elements'; // registers <kai-remote>
119
+
120
+ const el = document.querySelector('kai-remote');
121
+ // The CardEnvelope is set as a JS property — it travels down the wire unchanged.
122
+ el.envelope = { type: 'form', id: 'signup', title: 'Join the beta',
123
+ data: { type: 'object', required: ['email'],
124
+ properties: { email: { type: 'string', title: 'Email', format: 'email' } } } };
125
+
126
+ // Every routed event is re-emitted as a bubbling kai-card CustomEvent — persist the
127
+ // resolution exactly like a native card:
128
+ el.addEventListener('kai-card', (e) => {
129
+ if (e.detail.kind === 'submit') save(e.detail.data);
130
+ });
131
+ // …or pass a CardPolicy directly: el.policy = { onSubmit: (id, data) => save(data) };
132
+ </script>
133
+ ```
134
+
135
+ Prefer the **imperative** SDK when you manage the lifecycle yourself (theme refresh, swapping
136
+ cards in one warm frame):
137
+
138
+ ```ts
139
+ import { mountRemoteCard } from '@kitn.ai/ui';
140
+
141
+ const handle = mountRemoteCard({
142
+ container: document.querySelector('#slot'),
143
+ providerOrigin: 'https://cards.provider.example',
144
+ src: 'https://cards.provider.example/card',
145
+ envelope,
146
+ context: { theme: { mode: 'dark' }, locale: 'en', authToken },
147
+ policy: { onSubmit: (cardId, data) => { /* persist via applyResolution, advance the chat */ } },
148
+ });
149
+
150
+ handle.updateContext({ theme: { mode: 'light' } }); // re-themes the framed card live
151
+ handle.update(nextEnvelope); // swap cards in the same iframe
152
+ handle.destroy(); // teardown + remove the frame
153
+ ```
154
+
155
+ **Inputs.** `providerOrigin` is the exact origin (`https://…`, or `http://localhost` for
156
+ dev) the iframe is pinned to. `src` is the provider page URL (must be on `providerOrigin`).
157
+ `envelope` is the `CardEnvelope` to render. `policy` is the optional `CardPolicy`. `context`
158
+ carries theme/locale/`a11y` and a short-lived `authToken`.
159
+
160
+ **Theme changes remount; token/locale refreshes are silent (v1).** A host **theme change
161
+ re-renders (remounts) the remote card**, so any in-progress remote-card state (e.g. a
162
+ half-filled form) **resets on a theme toggle** — renderers apply theme imperatively at mount,
163
+ and v1 has no in-place renderer theme hook yet. **Token / locale / `a11y` refreshes are
164
+ silent** (no remount — renderers read `host.context()` on demand). A future refinement is a
165
+ renderer context subscription for live re-theme without remount.
166
+
167
+ **Security model.** The host mints a per-instance **nonce** and pins every inbound frame on
168
+ **origin + source window + nonce + negotiated protocol version** — a frame failing any check
169
+ is dropped, so a wrong-origin or forged `postMessage` can't drive your policy. The iframe is
170
+ sandboxed (`allow-scripts allow-forms allow-same-origin`, **no** `allow-popups`) and gets a
171
+ `no-referrer` policy. Any `authToken` you pass should be **short-lived** (the provider sees
172
+ it). The **host page must set `Content-Security-Policy: frame-ancestors 'self'`** (or
173
+ `'none'`) so it can't itself be reframed; the **provider** locks who may embed it with its
174
+ own `frame-ancestors` listing your exact host origin (no wildcards).
175
+
176
+ **Reference runtime.** `examples/remote-provider/` is the runnable provider iframe a real
177
+ provider copies — it calls `createCardBridge` (from `@kitn.ai/ui/provider`, a SolidJS-free
178
+ bundle) and registers one renderer per card `type`. `examples/remote-host/` is the matching
179
+ host page. The real cross-origin behavior is verified by the standalone Playwright suite
180
+ (`tests/e2e/`).
181
+
182
+ ## Card elements are `kai-*` components too
183
+
184
+ Each card type (`kai-confirm`, `kai-choice`, `kai-tasks`, `kai-form`, `kai-link`, `kai-embed`, `kai-remote`) is a registered `kai-*` custom element with its own prop/event API — the same model as everything else under **Components**. They live here under **Generative UI / Cards** because they only make sense inside the dispatcher/envelope system, but they follow the same component contract: attributes, JS properties, a `kai-card` event, CSS parts. Browse **Generative UI → Cards** for the per-element API references and live demos.
185
+
186
+ See **Cards** for each card type, and **SDK** for the dispatcher in action.
@@ -0,0 +1,7 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Docs/Recipes/Speech-to-Text" />
4
+
5
+ # Speech-to-text
6
+
7
+ The reverse direction is built in — the kit ships `<kai-voice-input>` (and a `VoiceInput` SolidJS component). Find it in the sidebar under the component stories.
@@ -0,0 +1,68 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Docs/Recipes/Streaming (OpenRouter)" />
4
+
5
+ # Streaming from OpenRouter
6
+
7
+ [OpenRouter](https://openrouter.ai) exposes an OpenAI-compatible streaming API (Server-Sent Events). Wire it into the `submit` event:
8
+
9
+ > **Security:** never ship an API key to the browser. In production, point `fetch` at your own backend that proxies to OpenRouter and injects the key.
10
+
11
+ ```js
12
+ chat.addEventListener('kai-submit', async (e) => {
13
+ const text = e.detail.value.trim();
14
+ if (!text) return;
15
+
16
+ // 1. Show the user message
17
+ const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
18
+ chat.messages = history;
19
+ chat.loading = true;
20
+
21
+ // 2. Empty assistant placeholder to stream into
22
+ const assistantId = crypto.randomUUID();
23
+ chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];
24
+
25
+ // In production, replace this with your own proxy endpoint.
26
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify({
33
+ model: 'anthropic/claude-sonnet-4',
34
+ stream: true,
35
+ messages: history.map((m) => ({ role: m.role, content: m.content })),
36
+ }),
37
+ });
38
+
39
+ const reader = res.body.getReader();
40
+ const decoder = new TextDecoder();
41
+ let buffer = '';
42
+ let answer = '';
43
+
44
+ while (true) {
45
+ const { value, done } = await reader.read();
46
+ if (done) break;
47
+ buffer += decoder.decode(value, { stream: true });
48
+
49
+ const lines = buffer.split('\n');
50
+ buffer = lines.pop();
51
+ for (const line of lines) {
52
+ const s = line.trim();
53
+ if (!s.startsWith('data:')) continue;
54
+ const payload = s.slice(5).trim();
55
+ if (payload === '[DONE]') continue;
56
+ try {
57
+ const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
58
+ if (!delta) continue;
59
+ answer += delta;
60
+ chat.messages = chat.messages.map((m) =>
61
+ m.id === assistantId ? { ...m, content: answer } : m
62
+ );
63
+ } catch { /* ignore non-JSON keep-alive lines */ }
64
+ }
65
+ }
66
+ chat.loading = false;
67
+ });
68
+ ```
@@ -0,0 +1,35 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Docs/Recipes/Text-to-Speech" />
4
+
5
+ # Text-to-speech (TTS)
6
+
7
+ ## Browser-native (zero dependencies)
8
+
9
+ Speak each reply once it finishes streaming — call `speak(answer)` right before `chat.loading = false`:
10
+
11
+ ```js
12
+ function speak(text) {
13
+ if (!('speechSynthesis' in window)) return;
14
+ const utter = new SpeechSynthesisUtterance(text);
15
+ utter.lang = 'en-US';
16
+ speechSynthesis.cancel();
17
+ speechSynthesis.speak(utter);
18
+ }
19
+ ```
20
+
21
+ ## Cloud TTS (OpenAI, ElevenLabs, …)
22
+
23
+ For higher-quality voices, have your backend call a TTS API and return audio (keep the provider key server-side):
24
+
25
+ ```js
26
+ async function speakCloud(text) {
27
+ const res = await fetch('/api/tts', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ text, voice: 'alloy' }),
31
+ });
32
+ const audio = new Audio(URL.createObjectURL(await res.blob()));
33
+ audio.play();
34
+ }
35
+ ```
@@ -0,0 +1,32 @@
1
+ import { ChatScene } from '../../chat-scene';
2
+
3
+ /** Class the editor targets to write the active mode's token values directly onto
4
+ * this wrapper — so the preview reflects the editor's mode independently of any
5
+ * ancestor `.dark` (e.g. Storybook's own dark theme), and only the canvas reskins. */
6
+ export const CANVAS_CLASS = 'kitn-editor-canvas';
7
+
8
+ /** Live preview: the real chat app scene + a small rail for tokens the chat
9
+ * doesn't naturally surface. The editor sets the active palette on CANVAS_CLASS;
10
+ * the `.dark` class is also applied for any `dark:`-keyed component styling. */
11
+ export function Canvas(props: { mode: 'light' | 'dark' }) {
12
+ return (
13
+ <div classList={{ [CANVAS_CLASS]: true, dark: props.mode === 'dark' }} class="h-full">
14
+ <div class="h-full flex flex-col rounded-xl border border-border overflow-hidden bg-background">
15
+ {/* The real product UI — same component the Full Chat App example uses */}
16
+ <div class="flex-1 min-h-0">
17
+ <ChatScene class="h-full" />
18
+ </div>
19
+
20
+ {/* Coverage rail: tokens the chat scene doesn't surface at rest */}
21
+ <div class="shrink-0 border-t border-border px-4 py-2.5 flex flex-wrap items-center gap-2 bg-background text-foreground">
22
+ <span class="text-[11px] text-muted-foreground mr-1">Other tokens:</span>
23
+ <button class="bg-destructive text-destructive-foreground rounded-md px-3 h-8 text-xs font-medium">Destructive</button>
24
+ <button class="bg-secondary text-secondary-foreground rounded-md px-3 h-8 text-xs font-medium">Secondary</button>
25
+ <span class="bg-accent text-accent-foreground rounded-md px-2.5 py-1.5 text-xs">Accent</span>
26
+ <span class="bg-popover text-popover-foreground border border-border shadow rounded-md px-2.5 py-1.5 text-xs">Popover</span>
27
+ <input class="bg-input border border-border rounded-md px-2 h-8 text-xs ring-2 ring-ring" placeholder="Focus ring" />
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,66 @@
1
+ // src/stories/docs/theme-editor/inspector.tsx
2
+ import { For, type JSX } from 'solid-js';
3
+ import { toHex, PURPOSE } from '../theme-tokens';
4
+ import type { Palette } from './theme-css';
5
+
6
+ const RADIUS_MAX = 1.4; // rem
7
+
8
+ export function Inspector(props: {
9
+ tokens: string[]; // ordered '--color-*' names
10
+ values: Palette; // active-mode palette (drives swatch colors)
11
+ radius: string; // e.g. '0.6rem'
12
+ onColorChange: (token: string, hex: string) => void;
13
+ onRadiusChange: (rem: string) => void;
14
+ }) {
15
+ const radiusRem = () => parseFloat(props.radius) || 0;
16
+ const swatch: JSX.CSSProperties = {
17
+ width: '1.75rem', height: '1.75rem', padding: '0', border: '1px solid var(--color-border)',
18
+ 'border-radius': '6px', background: 'none', cursor: 'pointer', 'flex-shrink': '0',
19
+ };
20
+ return (
21
+ <div class="p-3 text-foreground">
22
+ <div class="text-xs font-semibold mb-2.5 text-muted-foreground uppercase tracking-wide">Tokens</div>
23
+ <div class="flex flex-col gap-2.5">
24
+ <For each={props.tokens}>
25
+ {(name) => {
26
+ const hex = () => {
27
+ try {
28
+ return toHex(props.values[name]);
29
+ } catch {
30
+ return '#888888';
31
+ }
32
+ };
33
+ return (
34
+ <label class="flex items-center gap-2.5">
35
+ <input
36
+ type="color"
37
+ value={hex()}
38
+ onInput={(e) => props.onColorChange(name, e.currentTarget.value)}
39
+ style={swatch}
40
+ />
41
+ <span class="flex min-w-0 flex-col leading-tight">
42
+ <span class="text-sm font-medium truncate">{name.replace('--color-', '')}</span>
43
+ <span class="text-xs text-muted-foreground truncate">{PURPOSE[name] ?? ''}</span>
44
+ </span>
45
+ </label>
46
+ );
47
+ }}
48
+ </For>
49
+ </div>
50
+
51
+ <div class="text-xs font-semibold mt-5 mb-2.5 text-muted-foreground uppercase tracking-wide">Radius</div>
52
+ <label class="flex items-center gap-2.5 text-sm">
53
+ <input
54
+ type="range"
55
+ min="0"
56
+ max={RADIUS_MAX}
57
+ step="0.05"
58
+ value={radiusRem()}
59
+ onInput={(e) => props.onRadiusChange(`${e.currentTarget.value}rem`)}
60
+ class="flex-1"
61
+ />
62
+ <span class="text-sm tabular-nums w-16 text-right">{props.radius}</span>
63
+ </label>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildPresets, BRAND_OVERRIDES } from './presets';
3
+
4
+ const base = {
5
+ light: { '--color-primary': 'hsl(240 5.9% 10%)', '--color-border': 'hsl(240 5.9% 90%)', '--radius': '0.6rem' },
6
+ dark: { '--color-primary': 'hsl(0 0% 98%)', '--color-border': 'hsl(240 3.7% 15.9%)' },
7
+ };
8
+
9
+ describe('buildPresets', () => {
10
+ it('orders Default first, then the brand presets', () => {
11
+ expect(buildPresets(base).map((p) => p.name)).toEqual(['Default', 'Violet', 'Emerald', 'Mono']);
12
+ });
13
+
14
+ it('Default preset equals the base palette (no drift)', () => {
15
+ const def = buildPresets(base).find((p) => p.name === 'Default')!;
16
+ expect(def.light).toEqual(base.light);
17
+ expect(def.dark).toEqual(base.dark);
18
+ });
19
+
20
+ it('brand presets override primary but preserve base neutrals', () => {
21
+ const violet = buildPresets(base).find((p) => p.name === 'Violet')!;
22
+ expect(violet.light['--color-primary']).toBe(BRAND_OVERRIDES.Violet.light['--color-primary']);
23
+ expect(violet.light['--color-border']).toBe(base.light['--color-border']);
24
+ expect(violet.light['--radius']).toBe(base.light['--radius']);
25
+ });
26
+
27
+ it('does not mutate the base palette', () => {
28
+ const snapshot = JSON.stringify(base);
29
+ buildPresets(base);
30
+ expect(JSON.stringify(base)).toBe(snapshot);
31
+ });
32
+ });