@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,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/card-envelope.schema.json",
4
+ "title": "CardEnvelope",
5
+ "description": "A card the agent/server asks the chat to render. `data` validates against the per-type schema.",
6
+ "type": "object",
7
+ "required": ["type", "id", "data"],
8
+ "properties": {
9
+ "type": { "type": "string", "minLength": 1 },
10
+ "id": { "type": "string", "minLength": 1 },
11
+ "data": {},
12
+ "title": { "type": "string" }
13
+ }
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/card-event.schema.json",
4
+ "title": "CardEvent",
5
+ "description": "An event a card emits up to the host. `kind` discriminates the payload.",
6
+ "type": "object",
7
+ "required": ["kind"],
8
+ "properties": {
9
+ "kind": { "enum": ["ready", "submit", "action", "send-prompt", "open", "resize", "state", "dismiss", "error"] },
10
+ "cardId": { "type": "string" }
11
+ }
12
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/choice.schema.json",
4
+ "title": "ChoiceCardData",
5
+ "description": "Data payload for a `choice` card (CardEnvelope.data when type='choice'). A single-select 'pick one of N rich options' card; activating an option emits the `action` verb.",
6
+ "type": "object",
7
+ "required": ["options"],
8
+ "properties": {
9
+ "prompt": {
10
+ "type": "string",
11
+ "description": "Optional question/body text rendered above the options. Distinct from CardEnvelope.title (the card chrome heading)."
12
+ },
13
+ "submitLabel": {
14
+ "type": "string",
15
+ "description": "Label for the Submit button; default 'Submit'."
16
+ },
17
+ "options": {
18
+ "type": "array",
19
+ "minItems": 1,
20
+ "description": "The selectable options, rendered in order. Exactly one can be chosen.",
21
+ "items": {
22
+ "type": "object",
23
+ "required": ["id", "label"],
24
+ "properties": {
25
+ "id": {
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "description": "Emitted as CardEvent.action. Must be unique within `options`. (The reserved id '__other__' is used by the allowOther escape.)",
29
+ "x-kai-unique": true
30
+ },
31
+ "label": { "type": "string", "minLength": 1, "description": "Visible option label." },
32
+ "description": {
33
+ "type": "string",
34
+ "description": "Optional secondary line under the label."
35
+ },
36
+ "media": {
37
+ "type": "object",
38
+ "description": "Optional leading media: an image URL or a named icon.",
39
+ "properties": {
40
+ "image": { "type": "string", "description": "Image URL rendered as a leading thumbnail in the row." },
41
+ "imageAlt": { "type": "string", "description": "Accessible alt text for `image` (decorative if omitted)." },
42
+ "icon": { "type": "string", "description": "Named icon rendered as a leading glyph." }
43
+ }
44
+ },
45
+ "meta": {
46
+ "type": "string",
47
+ "description": "Trailing freeform text (e.g. a price or badge), right-aligned in the row."
48
+ },
49
+ "recommended": {
50
+ "type": "boolean",
51
+ "default": false,
52
+ "description": "Renders a small 'Recommended' pill near the label."
53
+ },
54
+ "disabled": {
55
+ "type": "boolean",
56
+ "default": false,
57
+ "description": "Option is shown but not selectable, and skipped in keyboard nav."
58
+ },
59
+ "payload": {
60
+ "description": "Optional opaque payload echoed back in CardEvent.payload (any JSON)."
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "allowOther": {
66
+ "description": "Optional free-text escape ('none of these / specify'). `true` appends a final 'Other…' option; an object customizes its `label`/`placeholder`. Selecting it reveals a text input; the shared Submit button then emits `{ kind:'action', action:'__other__', payload:{ text } }`.",
67
+ "x-kai-control": "allow-other"
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/confirm.schema.json",
4
+ "title": "ConfirmCardData",
5
+ "description": "Data payload for a `confirm` card (CardEnvelope.data when type='confirm').",
6
+ "type": "object",
7
+ "required": ["actions"],
8
+ "properties": {
9
+ "heading": {
10
+ "type": "string",
11
+ "description": "Optional in-body heading. Distinct from CardEnvelope.title (the card chrome title)."
12
+ },
13
+ "body": {
14
+ "type": "string",
15
+ "description": "Body text. Plain text in v1 (rendered safely; no HTML injection)."
16
+ },
17
+ "tone": {
18
+ "type": "string",
19
+ "enum": ["default", "warning", "danger"],
20
+ "default": "default",
21
+ "description": "Overall card tone; 'danger' adds a warning icon + accent for destructive approvals.",
22
+ "x-kai-control": "tone"
23
+ },
24
+ "actions": {
25
+ "type": "array",
26
+ "minItems": 1,
27
+ "maxItems": 4,
28
+ "description": "The choice set. Rendered as buttons in order; max 4 keeps it a decision, not a menu.",
29
+ "items": {
30
+ "type": "object",
31
+ "required": ["id", "label"],
32
+ "properties": {
33
+ "id": {
34
+ "type": "string",
35
+ "minLength": 1,
36
+ "description": "Emitted as CardEvent.action. Must be unique within `actions`.",
37
+ "x-kai-unique": true
38
+ },
39
+ "label": { "type": "string", "minLength": 1, "description": "Visible button label." },
40
+ "style": {
41
+ "type": "string",
42
+ "enum": ["primary", "default", "destructive"],
43
+ "default": "default",
44
+ "description": "Button emphasis. 'destructive' = red/danger; 'primary' = filled accent.",
45
+ "x-kai-control": "button-style"
46
+ },
47
+ "payload": {
48
+ "description": "Optional opaque payload echoed back in CardEvent.payload (any JSON)."
49
+ },
50
+ "default": {
51
+ "type": "boolean",
52
+ "default": false,
53
+ "description": "If true, this action is the keyboard default (Enter) and gets initial focus. At most one should be true; the card uses the first if several are.",
54
+ "x-kai-default-action": true
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "dismissible": {
60
+ "type": "boolean",
61
+ "default": false,
62
+ "description": "Show a close affordance that emits the `dismiss` verb."
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/embed.schema.json",
4
+ "title": "EmbedCardData",
5
+ "description": "Lazy media-embed payload (YouTube / Vimeo / generic player URL).",
6
+ "type": "object",
7
+ "x-kai-card-type": "embed",
8
+ "x-kai-contract-version": "1",
9
+ "required": ["provider"],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "provider": {
13
+ "type": "string",
14
+ "enum": ["youtube", "vimeo", "generic"],
15
+ "description": "Media provider. 'generic' frames `url` directly (must be an https embeddable player URL whose origin is app-allowlisted).",
16
+ "x-kai-control": "select"
17
+ },
18
+ "id": {
19
+ "type": "string",
20
+ "description": "Provider video id (required for youtube/vimeo when `url` is absent). e.g. 'dQw4w9WgXcQ'.",
21
+ "maxLength": 64,
22
+ "pattern": "^[A-Za-z0-9_-]+$"
23
+ },
24
+ "url": {
25
+ "type": "string",
26
+ "format": "uri",
27
+ "description": "Full media/watch URL. For youtube/vimeo it is parsed to an id; for 'generic' it is the embeddable player src (https only).",
28
+ "x-kai-format": "url"
29
+ },
30
+ "title": {
31
+ "type": "string",
32
+ "description": "Accessible title for the player iframe + the poster label. Strongly recommended for a11y.",
33
+ "maxLength": 300
34
+ },
35
+ "poster": {
36
+ "type": "string",
37
+ "format": "uri",
38
+ "description": "Thumbnail shown before play. When omitted, youtube/vimeo derive a default thumbnail; 'generic' shows a neutral play placeholder.",
39
+ "x-kai-format": "url"
40
+ },
41
+ "start": {
42
+ "type": "integer",
43
+ "minimum": 0,
44
+ "description": "Optional start offset in seconds.",
45
+ "x-kai-unit": "seconds"
46
+ },
47
+ "aspectRatio": {
48
+ "type": "string",
49
+ "enum": ["16:9", "4:3", "1:1", "9:16"],
50
+ "default": "16:9",
51
+ "description": "Player box aspect ratio (CSS aspect-ratio).",
52
+ "x-kai-control": "select"
53
+ }
54
+ },
55
+ "allOf": [
56
+ {
57
+ "if": { "properties": { "provider": { "const": "generic" } } },
58
+ "then": { "required": ["url"] }
59
+ },
60
+ {
61
+ "if": { "properties": { "provider": { "enum": ["youtube", "vimeo"] } } },
62
+ "then": { "anyOf": [{ "required": ["id"] }, { "required": ["url"] }] }
63
+ }
64
+ ]
65
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/form.result.schema.json",
4
+ "title": "kai-form submit payload",
5
+ "description": "The CardEvent {kind:'submit'} `data` for a form card: an object that validates against the form-definition schema that was sent down in CardEnvelope.data. Standard JSON Schema validation (type/enum/required/min-max/length/pattern) holds. Optional empty strings are omitted.",
6
+ "type": "object"
7
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/form.schema.json",
4
+ "title": "kai-form data (a form definition)",
5
+ "description": "The CardEnvelope.data for a card of type 'form': a JSON Schema (type:'object') describing the fields kai-form renders, plus optional x-kai-* UI hints. The same schema validates the submission.",
6
+ "type": "object",
7
+ "required": ["type", "properties"],
8
+ "properties": {
9
+ "type": { "const": "object" },
10
+ "title": { "type": "string" },
11
+ "description": { "type": "string" },
12
+ "required": { "type": "array", "items": { "type": "string" } },
13
+ "properties": {
14
+ "type": "object"
15
+ },
16
+ "x-kai-order": { "type": "array", "items": { "type": "string" } },
17
+ "x-kai-inlineMax": { "type": "integer", "minimum": 1 },
18
+ "x-kai-submitLabel": { "type": "string" },
19
+ "x-kai-dismissible": { "type": "boolean" },
20
+ "x-kai-actions": {
21
+ "type": "array",
22
+ "items": {
23
+ "type": "object",
24
+ "required": ["id", "label"],
25
+ "properties": {
26
+ "id": { "type": "string" },
27
+ "label": { "type": "string" },
28
+ "variant": { "enum": ["default", "ghost", "outline"] }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/link.schema.json",
4
+ "title": "LinkPreviewData",
5
+ "description": "Rich link / Open-Graph preview payload. The card renders from this; it never fetches.",
6
+ "type": "object",
7
+ "x-kai-card-type": "link",
8
+ "x-kai-contract-version": "1",
9
+ "required": ["url"],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "url": {
13
+ "type": "string",
14
+ "format": "uri",
15
+ "description": "Canonical destination. Opened via the contract `open` verb (target 'tab').",
16
+ "x-kai-format": "url"
17
+ },
18
+ "title": {
19
+ "type": "string",
20
+ "description": "OG title (og:title). Falls back to the domain when absent.",
21
+ "maxLength": 300
22
+ },
23
+ "description": {
24
+ "type": "string",
25
+ "description": "OG description (og:description). Clamped to 3 lines in the UI.",
26
+ "maxLength": 1000
27
+ },
28
+ "image": {
29
+ "type": "string",
30
+ "format": "uri",
31
+ "description": "Preview image (og:image). Optional; the card degrades gracefully when missing or it fails to load.",
32
+ "x-kai-format": "url"
33
+ },
34
+ "imageAlt": {
35
+ "type": "string",
36
+ "description": "Alt text for the preview image. Defaults to the title (or empty = decorative) when omitted.",
37
+ "maxLength": 300
38
+ },
39
+ "favicon": {
40
+ "type": "string",
41
+ "format": "uri",
42
+ "description": "Site favicon shown next to the domain.",
43
+ "x-kai-format": "url"
44
+ },
45
+ "domain": {
46
+ "type": "string",
47
+ "description": "Display domain (e.g. 'example.com'). Derived from `url` when omitted.",
48
+ "maxLength": 253
49
+ },
50
+ "siteName": {
51
+ "type": "string",
52
+ "description": "OG site name (og:site_name). Shown in place of the domain when present.",
53
+ "maxLength": 200
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/tasks.result.schema.json",
4
+ "title": "TasksCardResult",
5
+ "description": "Payload of CardEvent { kind: 'submit' } from a tasks card.",
6
+ "type": "object",
7
+ "required": ["selected"],
8
+ "properties": {
9
+ "selected": {
10
+ "type": "array",
11
+ "description": "Ids of the checked tasks, in the order they appear in `tasks`. Subset of the input task ids.",
12
+ "items": { "type": "string", "minLength": 1 },
13
+ "uniqueItems": true
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kitn.ai/schemas/card/tasks.schema.json",
4
+ "title": "TasksCardData",
5
+ "description": "Data payload for a `tasks` card (CardEnvelope.data when type='tasks').",
6
+ "type": "object",
7
+ "required": ["tasks"],
8
+ "properties": {
9
+ "mode": {
10
+ "type": "string",
11
+ "enum": ["select"],
12
+ "default": "select",
13
+ "description": "v1 supports only 'select' (toggle + confirm). 'progress' (live AG-UI status) is a future enum value; restricting it now keeps the wire forward-compatible.",
14
+ "x-kai-mode": true
15
+ },
16
+ "heading": {
17
+ "type": "string",
18
+ "description": "Optional in-body heading; distinct from CardEnvelope.title."
19
+ },
20
+ "tasks": {
21
+ "type": "array",
22
+ "minItems": 1,
23
+ "description": "The selectable rows, rendered in order.",
24
+ "items": {
25
+ "type": "object",
26
+ "required": ["id", "label"],
27
+ "properties": {
28
+ "id": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "Stable id; the selected ids are returned in the result. Unique within `tasks`.",
32
+ "x-kai-unique": true
33
+ },
34
+ "label": { "type": "string", "minLength": 1, "description": "Row label." },
35
+ "description": { "type": "string", "description": "Optional secondary line under the label." },
36
+ "checked": {
37
+ "type": "boolean",
38
+ "default": false,
39
+ "description": "Initial checked state of the row."
40
+ },
41
+ "disabled": {
42
+ "type": "boolean",
43
+ "default": false,
44
+ "description": "Row is shown but not toggleable (and excluded from select-all)."
45
+ }
46
+ }
47
+ }
48
+ },
49
+ "selectAll": {
50
+ "type": "boolean",
51
+ "default": false,
52
+ "description": "Render a master select-all checkbox above the list.",
53
+ "x-kai-control": "select-all"
54
+ },
55
+ "confirmLabel": {
56
+ "type": "string",
57
+ "default": "Confirm",
58
+ "description": "Label for the confirm button."
59
+ },
60
+ "allowEmpty": {
61
+ "type": "boolean",
62
+ "default": false,
63
+ "description": "If true, confirm is enabled with zero selected (emits { selected: [] }). If false, confirm is disabled until >=1 selected."
64
+ },
65
+ "min": {
66
+ "type": "integer",
67
+ "minimum": 0,
68
+ "description": "Optional minimum number that must be selected to confirm.",
69
+ "x-kai-select-min": true
70
+ },
71
+ "max": {
72
+ "type": "integer",
73
+ "minimum": 1,
74
+ "description": "Optional maximum selectable; further toggles are blocked once reached.",
75
+ "x-kai-select-max": true
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,95 @@
1
+ // src/primitives/card-validate.ts
2
+ // The single shared lean JSON-Schema validator the contract mandates. Covers the
3
+ // subset cards use; `x-*` keywords (incl. x-kai-*) are ignored. No ajv. Used at
4
+ // every boundary (incoming card data, outgoing payloads) by cards + both transports.
5
+
6
+ export interface JsonSchema {
7
+ type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
8
+ const?: unknown;
9
+ enum?: unknown[];
10
+ required?: string[];
11
+ properties?: Record<string, JsonSchema>;
12
+ items?: JsonSchema;
13
+ minimum?: number; maximum?: number;
14
+ exclusiveMinimum?: number; exclusiveMaximum?: number;
15
+ minLength?: number; maxLength?: number;
16
+ pattern?: string;
17
+ minItems?: number; maxItems?: number;
18
+ uniqueItems?: boolean;
19
+ // x-* keywords (e.g. x-kai-widget) are allowed and ignored.
20
+ [key: `x-${string}`]: unknown;
21
+ }
22
+
23
+ export interface ValidationResult {
24
+ valid: boolean;
25
+ errors: string[];
26
+ }
27
+
28
+ function typeOf(v: unknown): string {
29
+ if (v === null) return 'null';
30
+ if (Array.isArray(v)) return 'array';
31
+ return typeof v;
32
+ }
33
+
34
+ function matchesType(v: unknown, t: NonNullable<JsonSchema['type']>): boolean {
35
+ switch (t) {
36
+ case 'integer': return typeof v === 'number' && Number.isInteger(v);
37
+ case 'number': return typeof v === 'number' && Number.isFinite(v);
38
+ case 'array': return Array.isArray(v);
39
+ case 'null': return v === null;
40
+ case 'object': return typeOf(v) === 'object';
41
+ default: return typeof v === t;
42
+ }
43
+ }
44
+
45
+ function walk(schema: JsonSchema, value: unknown, path: string, errors: string[]): void {
46
+ const at = path || '(root)';
47
+ if (schema.type && !matchesType(value, schema.type)) {
48
+ errors.push(`${at}: expected ${schema.type}, got ${typeOf(value)}`);
49
+ return; // type wrong → downstream checks are meaningless
50
+ }
51
+ if ('const' in schema && JSON.stringify(value) !== JSON.stringify(schema.const)) {
52
+ errors.push(`${at}: must equal const ${JSON.stringify(schema.const)}`);
53
+ }
54
+ if (schema.enum && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
55
+ errors.push(`${at}: must be one of ${JSON.stringify(schema.enum)}`);
56
+ }
57
+ if (typeof value === 'number') {
58
+ if (schema.minimum !== undefined && value < schema.minimum) errors.push(`${at}: < minimum ${schema.minimum}`);
59
+ if (schema.maximum !== undefined && value > schema.maximum) errors.push(`${at}: > maximum ${schema.maximum}`);
60
+ if (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) errors.push(`${at}: <= exclusiveMinimum`);
61
+ if (schema.exclusiveMaximum !== undefined && value >= schema.exclusiveMaximum) errors.push(`${at}: >= exclusiveMaximum`);
62
+ }
63
+ if (typeof value === 'string') {
64
+ if (schema.minLength !== undefined && value.length < schema.minLength) errors.push(`${at}: shorter than minLength ${schema.minLength}`);
65
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) errors.push(`${at}: longer than maxLength ${schema.maxLength}`);
66
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) errors.push(`${at}: does not match pattern`);
67
+ }
68
+ if (Array.isArray(value)) {
69
+ if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${at}: fewer than minItems ${schema.minItems}`);
70
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) errors.push(`${at}: more than maxItems ${schema.maxItems}`);
71
+ if (schema.uniqueItems) {
72
+ const seen = new Set(value.map((v) => JSON.stringify(v)));
73
+ if (seen.size !== value.length) errors.push(`${at}: items not unique`);
74
+ }
75
+ if (schema.items) value.forEach((v, i) => walk(schema.items!, v, `${at}[${i}]`, errors));
76
+ }
77
+ if (typeOf(value) === 'object') {
78
+ const obj = value as Record<string, unknown>;
79
+ for (const key of schema.required ?? []) {
80
+ if (!(key in obj) || obj[key] === undefined) errors.push(`${at}.${key}: required`);
81
+ }
82
+ if (schema.properties) {
83
+ for (const [key, sub] of Object.entries(schema.properties)) {
84
+ if (key in obj && obj[key] !== undefined) walk(sub, obj[key], `${at}.${key}`, errors);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /** Validate `value` against the lean JSON-Schema subset. */
91
+ export function validateAgainstSchema(schema: JsonSchema, value: unknown): ValidationResult {
92
+ const errors: string[] = [];
93
+ walk(schema, value, '', errors);
94
+ return { valid: errors.length === 0, errors };
95
+ }
@@ -0,0 +1,76 @@
1
+ import { createContext, useContext, type Accessor, type JSX } from 'solid-js';
2
+
3
+ export type ProseSize = 'xs' | 'sm' | 'base' | 'lg';
4
+
5
+ export interface ChatConfigValue {
6
+ /** Prose/text size for messages, markdown, and UI elements */
7
+ proseSize: Accessor<ProseSize>;
8
+ /** Shiki theme for code blocks */
9
+ codeTheme: Accessor<string>;
10
+ /** Node the kit's overlays portal into; undefined → document.body */
11
+ portalMount: Accessor<HTMLElement | undefined>;
12
+ /** Whether code blocks are syntax-highlighted; false → plain text, no Shiki loaded */
13
+ codeHighlight: Accessor<boolean>;
14
+ }
15
+
16
+ const defaultConfig: ChatConfigValue = {
17
+ proseSize: () => 'sm' as ProseSize,
18
+ codeTheme: () => 'github-dark-dimmed',
19
+ portalMount: () => undefined,
20
+ codeHighlight: () => true,
21
+ };
22
+
23
+ const ChatConfigContext = createContext<ChatConfigValue>(defaultConfig);
24
+
25
+ export interface ChatConfigProps {
26
+ proseSize?: ProseSize;
27
+ codeTheme?: string;
28
+ portalMount?: HTMLElement;
29
+ codeHighlight?: boolean;
30
+ children: JSX.Element;
31
+ }
32
+
33
+ /**
34
+ * Provides chat-wide appearance settings to all child components.
35
+ * Set once at the top level — MessageContent, Markdown, CodeBlock,
36
+ * ConversationList, and PromptInput all read from this.
37
+ */
38
+ export function ChatConfig(props: ChatConfigProps) {
39
+ const value: ChatConfigValue = {
40
+ proseSize: () => props.proseSize ?? 'sm',
41
+ codeTheme: () => props.codeTheme ?? 'github-dark-dimmed',
42
+ portalMount: () => props.portalMount,
43
+ codeHighlight: () => props.codeHighlight ?? true,
44
+ };
45
+
46
+ return (
47
+ <ChatConfigContext.Provider value={value}>
48
+ {props.children}
49
+ </ChatConfigContext.Provider>
50
+ );
51
+ }
52
+
53
+ /** Read the current chat config. Returns defaults if no provider is present. */
54
+ export function useChatConfig(): ChatConfigValue {
55
+ return useContext(ChatConfigContext)!;
56
+ }
57
+
58
+ /** Maps prose size to Tailwind prose class */
59
+ export function proseClass(size: ProseSize): string {
60
+ switch (size) {
61
+ case 'xs': return 'prose-sm text-xs';
62
+ case 'sm': return 'prose-sm';
63
+ case 'base': return '';
64
+ case 'lg': return 'prose-lg';
65
+ }
66
+ }
67
+
68
+ /** Maps prose size to a text class for non-prose elements (sidebar, input) */
69
+ export function textClass(size: ProseSize): string {
70
+ switch (size) {
71
+ case 'xs': return 'text-xs';
72
+ case 'sm': return 'text-sm';
73
+ case 'base': return 'text-base';
74
+ case 'lg': return 'text-lg';
75
+ }
76
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createRoot, createSignal } from 'solid-js';
3
+ import { createControllableSignal } from './controllable';
4
+
5
+ describe('createControllableSignal', () => {
6
+ it('uses the initial value when uncontrolled (controlled accessor is undefined)', () => {
7
+ createRoot((dispose) => {
8
+ const [value] = createControllableSignal(() => undefined, true);
9
+ expect(value()).toBe(true);
10
+ dispose();
11
+ });
12
+ });
13
+
14
+ it('updates via the setter when uncontrolled', () => {
15
+ createRoot((dispose) => {
16
+ const [value, setValue] = createControllableSignal(() => undefined, false);
17
+ setValue(true);
18
+ expect(value()).toBe(true);
19
+ dispose();
20
+ });
21
+ });
22
+
23
+ it('reflects the controlled value and ignores the internal setter while controlled', () => {
24
+ createRoot((dispose) => {
25
+ const [controlled, setControlled] = createSignal<boolean | undefined>(false);
26
+ const [value, setValue] = createControllableSignal(controlled, false);
27
+ expect(value()).toBe(false);
28
+ setValue(true); // masked while controlled
29
+ expect(value()).toBe(false);
30
+ setControlled(true); // the controlling value wins
31
+ expect(value()).toBe(true);
32
+ dispose();
33
+ });
34
+ });
35
+
36
+ it('falls back to the internal value when control is released (becomes undefined)', () => {
37
+ createRoot((dispose) => {
38
+ const [controlled, setControlled] = createSignal<boolean | undefined>(true);
39
+ const [value, setValue] = createControllableSignal(controlled, false);
40
+ expect(value()).toBe(true);
41
+ setValue(true); // sets internal (masked now)
42
+ setControlled(undefined); // uncontrolled → internal shows
43
+ expect(value()).toBe(true);
44
+ dispose();
45
+ });
46
+ });
47
+ });