@kitn.ai/ui 0.29.0 → 0.31.0

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 (280) hide show
  1. package/README.md +11 -7
  2. package/dist/{Icon-DObhsRtm.js → Icon-DJDuE_-x.js} +1 -1
  3. package/dist/action-icons-xkBBVKkF.js +1 -0
  4. package/dist/agent-tooling/construct/schema-url.d.ts +9 -0
  5. package/dist/agent-tooling/construct/schema.d.ts +76 -1
  6. package/dist/agent-tooling/construct/templates.d.ts +45 -0
  7. package/dist/arrow-down-DU6wOl-h.js +1 -0
  8. package/dist/arrow-left-DD46kN17.js +1 -0
  9. package/dist/artifact-DoUAr1Fd.js +1 -0
  10. package/dist/assets/dev-DwzHsqYD.js +553 -0
  11. package/dist/{attachments-Qr4kg-2b.js → attachments-BLWPDiLH.js} +1 -1
  12. package/dist/{audio-visualizer-DToYdfSc.js → audio-visualizer-DhHicYvV.js} +1 -1
  13. package/dist/{badge-CdEu_jn2.js → badge-BGm7pcwi.js} +1 -1
  14. package/dist/{bell-DQAAOpDz.js → bell-CMEdSo7A.js} +1 -1
  15. package/dist/builder-app/App.d.ts +1 -0
  16. package/dist/builder-app/edit-guard.d.ts +8 -0
  17. package/dist/builder-app/main.d.ts +0 -0
  18. package/dist/builder-page/assets/index-C10OF-ur.js +72 -0
  19. package/dist/builder-page/assets/index-CFyKLBlu.css +1 -0
  20. package/dist/builder-page/index.html +20 -0
  21. package/dist/button-BM0YTTDQ.js +1 -0
  22. package/dist/{card-renderer-Jx70m6_l.js → card-renderer-Ds-wXc6_.js} +1 -1
  23. package/dist/check-BG1VfYjm.js +1 -0
  24. package/dist/{checkbox-group-B2_rpoPM.js → checkbox-group-CqXj8ehz.js} +1 -1
  25. package/dist/{checkbox-D7iRB3nL.js → checkbox-np_5mKxM.js} +1 -1
  26. package/dist/chevron-down-CZ4W0hy6.js +1 -0
  27. package/dist/chevron-right-5gdxkCTF.js +1 -0
  28. package/dist/{choice-card-m0BYqqOK.js → choice-card-BYBmPAT5.js} +1 -1
  29. package/dist/circle-C0lwjD2A.js +1 -0
  30. package/dist/circle-check-BfT1cxQU.js +1 -0
  31. package/dist/{circle-x-BPG0iPst.js → circle-x-DzUfvRqr.js} +1 -1
  32. package/dist/{code-block-ClMLKb7C.js → code-block-H6E5Eurc.js} +1 -1
  33. package/dist/{collapsible-B4DlWicL.js → collapsible-CBq1Tj6y.js} +1 -1
  34. package/dist/components/builder-composer-triggers.d.ts +62 -0
  35. package/dist/components/builder-layout.d.ts +54 -0
  36. package/dist/components/builder-message-actions.d.ts +63 -0
  37. package/dist/components/builder-panel-derived.d.ts +18 -0
  38. package/dist/components/builder-panel.d.ts +240 -0
  39. package/dist/components/builder-preview.d.ts +11 -0
  40. package/dist/components/builder-shell-controls.d.ts +69 -0
  41. package/dist/components/builder-skeleton.d.ts +58 -0
  42. package/dist/components/builder-start.d.ts +79 -0
  43. package/dist/components/builder-workspace-variants.d.ts +23 -0
  44. package/dist/components/captions.d.ts +66 -0
  45. package/dist/components/chat-thread.d.ts +27 -1
  46. package/dist/components/construct-form-paths.d.ts +54 -0
  47. package/dist/components/message.d.ts +5 -0
  48. package/dist/{composer-sQ0HcvyG.js → composer-B5IvgxAF.js} +1 -1
  49. package/dist/{confirm-card-rQWDQsnC.js → confirm-card-B63pZIfG.js} +1 -1
  50. package/dist/construct-cli.es.js +469 -125
  51. package/dist/construct-templates.d.ts +5 -0
  52. package/dist/construct-templates.js +266 -0
  53. package/dist/construct.js +269 -86
  54. package/dist/{context-0kg4ZX1J.js → context-Byqor78j.js} +1 -1
  55. package/dist/controllable-C4BSt73s.js +1 -0
  56. package/dist/{conversation-item-BxwQLaFJ.js → conversation-item-piXZFRWa.js} +1 -1
  57. package/dist/{conversation-list-BCfGN1JU.js → conversation-list-B5yu9cPg.js} +1 -1
  58. package/dist/{copy-n9u9rqpi.js → copy-DfI2NYEX.js} +1 -1
  59. package/dist/{create-tween-uPkArjIG.js → create-tween-C4j96_Aj.js} +1 -1
  60. package/dist/{create-tween-C-q-JCM-.js → create-tween-CHleHI8m.js} +1 -1
  61. package/dist/{create-tween-CRrJXxBd.js → create-tween-DQ6lOhdl.js} +1 -1
  62. package/dist/{create-tween-DZ3RRDHD.js → create-tween-DyjTgkJi.js} +1 -1
  63. package/dist/custom-elements.json +40 -5
  64. package/dist/{default-input-CaMrJkgH.js → default-input-Bwmt8Lai.js} +1 -1
  65. package/dist/define-B3OpsIDE.js +1 -0
  66. package/dist/define.js +41 -41
  67. package/dist/define.server.js +46 -46
  68. package/dist/{disclosure-WUImBbDv.js → disclosure-ZSAlD4wg.js} +1 -1
  69. package/dist/{dropdown-wZVeJOMd.js → dropdown-CSRhiMN2.js} +1 -1
  70. package/dist/elements/agent-card.js +1 -1
  71. package/dist/elements/artifact.js +1 -1
  72. package/dist/elements/attachments.js +1 -1
  73. package/dist/elements/audio-visualizer.js +1 -1
  74. package/dist/elements/avatar.js +1 -1
  75. package/dist/elements/badge.js +1 -1
  76. package/dist/elements/button.js +1 -1
  77. package/dist/elements/card.js +1 -1
  78. package/dist/elements/cards.js +1 -1
  79. package/dist/elements/chain-of-thought.js +1 -1
  80. package/dist/elements/chat-actions.d.ts +12 -0
  81. package/dist/elements/chat-scope-picker.js +1 -1
  82. package/dist/elements/chat-types.d.ts +7 -2
  83. package/dist/elements/chat-workspace.js +1 -1
  84. package/dist/elements/chat.js +1 -1
  85. package/dist/elements/checkbox-group.js +1 -1
  86. package/dist/elements/checkbox.js +1 -1
  87. package/dist/elements/checkpoint.js +1 -1
  88. package/dist/elements/choice.js +1 -1
  89. package/dist/elements/coachmark.js +1 -1
  90. package/dist/elements/code-block.js +1 -1
  91. package/dist/elements/command.js +1 -1
  92. package/dist/elements/compare.js +1 -1
  93. package/dist/elements/composer.js +1 -1
  94. package/dist/elements/confirm-card.js +1 -1
  95. package/dist/elements/context-meter.js +1 -1
  96. package/dist/elements/conversation-item.js +1 -1
  97. package/dist/elements/conversation-list.js +1 -1
  98. package/dist/elements/dialog.js +1 -1
  99. package/dist/elements/dock.js +1 -1
  100. package/dist/elements/dropdown.js +1 -1
  101. package/dist/elements/editable-label.js +1 -1
  102. package/dist/elements/embed.js +1 -1
  103. package/dist/elements/empty.js +1 -1
  104. package/dist/elements/feedback-bar.js +1 -1
  105. package/dist/elements/file-tree.js +1 -1
  106. package/dist/elements/file-upload.js +1 -1
  107. package/dist/elements/form.js +1 -1
  108. package/dist/elements/hover-card.js +1 -1
  109. package/dist/elements/icon.js +1 -1
  110. package/dist/elements/image.js +1 -1
  111. package/dist/elements/input.js +1 -1
  112. package/dist/elements/kbd.js +1 -1
  113. package/dist/elements/link-preview.js +1 -1
  114. package/dist/elements/loader.js +1 -1
  115. package/dist/elements/markdown.js +1 -1
  116. package/dist/elements/menu.js +1 -1
  117. package/dist/elements/message-skills.js +1 -1
  118. package/dist/elements/message.js +1 -1
  119. package/dist/elements/model-switcher.js +1 -1
  120. package/dist/elements/nav.js +1 -1
  121. package/dist/elements/notice.js +1 -1
  122. package/dist/elements/pane-grid.js +1 -1
  123. package/dist/elements/pane-group.js +1 -1
  124. package/dist/elements/pane.js +1 -1
  125. package/dist/elements/popover.js +1 -1
  126. package/dist/elements/progress-bar.js +1 -1
  127. package/dist/elements/prompt-dock.js +1 -1
  128. package/dist/elements/prompt-input.js +1 -1
  129. package/dist/elements/prompt-suggestions.js +1 -1
  130. package/dist/elements/radio-group.js +1 -1
  131. package/dist/elements/reasoning.js +1 -1
  132. package/dist/elements/remote.js +1 -1
  133. package/dist/elements/resizable.js +1 -1
  134. package/dist/elements/response-stream.js +1 -1
  135. package/dist/elements/screen.js +1 -1
  136. package/dist/elements/scroll-area.js +1 -1
  137. package/dist/elements/scroll-button.js +1 -1
  138. package/dist/elements/search.js +1 -1
  139. package/dist/elements/segmented.js +1 -1
  140. package/dist/elements/select.js +1 -1
  141. package/dist/elements/separator.js +1 -1
  142. package/dist/elements/setting-item.js +1 -1
  143. package/dist/elements/settings-group.js +1 -1
  144. package/dist/elements/skeleton.js +1 -1
  145. package/dist/elements/slider.js +1 -1
  146. package/dist/elements/source.js +1 -1
  147. package/dist/elements/status.js +1 -1
  148. package/dist/elements/switch.js +1 -1
  149. package/dist/elements/tabs.js +1 -1
  150. package/dist/elements/tasks.js +1 -1
  151. package/dist/elements/text-shimmer.js +1 -1
  152. package/dist/elements/thinking-bar.js +1 -1
  153. package/dist/elements/thread.js +1 -1
  154. package/dist/elements/toast.js +1 -1
  155. package/dist/elements/tool.js +1 -1
  156. package/dist/elements/tooltip.js +1 -1
  157. package/dist/elements/voice-input.js +1 -1
  158. package/dist/elements/voice-output.js +1 -1
  159. package/dist/elements.d.ts +22 -10
  160. package/dist/{ellipsis-CbOpJnU3.js → ellipsis-CJ5QIjWo.js} +1 -1
  161. package/dist/{embed-CrfjkbUG.js → embed-6Vji0PfS.js} +1 -1
  162. package/dist/{external-link-ByXAwBBQ.js → external-link-BPTtTdet.js} +1 -1
  163. package/dist/{file-text-DMib2qwt.js → file-text-eVjcFDrK.js} +1 -1
  164. package/dist/{file-tree-CJqM2aP7.js → file-tree-a9tfWYmg.js} +1 -1
  165. package/dist/{folder-_BAb929C.js → folder-DN6Ilq6d.js} +1 -1
  166. package/dist/{form-DSZgZQZk.js → form-U1cwhcQx.js} +1 -1
  167. package/dist/{hover-card-BQbul9eq.js → hover-card-C5ptzZOZ.js} +1 -1
  168. package/dist/{icon-CdcfRhYI.js → icon-BOMrFp0D.js} +1 -1
  169. package/dist/index.js +2297 -2279
  170. package/dist/index.server.js +2102 -2084
  171. package/dist/{info-DsFjGuI_.js → info-DxzKsIHn.js} +1 -1
  172. package/dist/{input-DKbmkMos.js → input-aW72PACH.js} +1 -1
  173. package/dist/kai.es.js +1 -1
  174. package/dist/{kbd-PvdGhX4s.js → kbd-BP-6MwUs.js} +1 -1
  175. package/dist/{link-preview-DbM1866N.js → link-preview-CZaDilMG.js} +1 -1
  176. package/dist/{link-zF8PzWfF.js → link-rlN11N0B.js} +1 -1
  177. package/dist/loader-BdFK70fF.js +1 -0
  178. package/dist/{markdown-LMvmAyBk.js → markdown-DjEaoNz-.js} +1 -1
  179. package/dist/mcp.es.js +587 -111
  180. package/dist/message--Nlbemtp.js +1 -0
  181. package/dist/message-C88yTu2A.js +1 -0
  182. package/dist/message-circle-CZeVDN1f.js +1 -0
  183. package/dist/message-square-BXNBKC6i.js +1 -0
  184. package/dist/{minimize-2-BQRoBV1V.js → minimize-2-BxLOOSEf.js} +1 -1
  185. package/dist/{model-switcher-CMpCOYcB.js → model-switcher-BxRvub2A.js} +1 -1
  186. package/dist/{overlay-Cb-MkyDa.js → overlay-CDwKJtHN.js} +1 -1
  187. package/dist/{panel-right-i8warFxT.js → panel-right-BovedCwW.js} +1 -1
  188. package/dist/{paperclip-ByY3UVO6.js → paperclip-D0Xyjja2.js} +1 -1
  189. package/dist/play-DGJ4MgB2.js +1 -0
  190. package/dist/primitives/speech.d.ts +35 -0
  191. package/dist/primitives/use-auto-resize.d.ts +11 -0
  192. package/dist/{progress-bar-jVAHvMUf.js → progress-bar-D-aQfy5h.js} +1 -1
  193. package/dist/{prompt-suggestion-CD-qtNgC.js → prompt-suggestion-C6qLjMEQ.js} +1 -1
  194. package/dist/{radio-BsoBMhkF.js → radio-D1CkXISc.js} +1 -1
  195. package/dist/react/index.d.ts +22 -6
  196. package/dist/react.js +13 -13
  197. package/dist/{reasoning-4kBPtlTx.js → reasoning-DARmbgmE.js} +1 -1
  198. package/dist/register-impl-WcrdIihK.js +424 -0
  199. package/dist/{resizable-uPmY9VY-.js → resizable-Da6oSO2z.js} +1 -1
  200. package/dist/{rotate-ccw-B2xn93py.js → rotate-ccw-31ed1xK_.js} +1 -1
  201. package/dist/{rotate-cw-CMVLoCyH.js → rotate-cw-qkl39dw3.js} +1 -1
  202. package/dist/{scroll-area-BqLUo8ZZ.js → scroll-area-RrKnLYlA.js} +1 -1
  203. package/dist/{scroll-button-DslXiJyL.js → scroll-button--QeNjobK.js} +1 -1
  204. package/dist/{select-2cLRtPGT.js → select-CS_0mWW3.js} +1 -1
  205. package/dist/{separator-BWQtNJcM.js → separator-fT-zBypW.js} +1 -1
  206. package/dist/{settings-BBR63jLg.js → settings-dfkV_WDm.js} +1 -1
  207. package/dist/{settings-group-B2ciy-Iw.js → settings-group-CX694npG.js} +1 -1
  208. package/dist/{skeleton-CkHI-HgC.js → skeleton-BsqKD9TV.js} +1 -1
  209. package/dist/{slider-DDR9U-i0.js → slider-_EWfTH9H.js} +1 -1
  210. package/dist/{solid-DruVOXnf.js → solid-BITWG_VE.js} +4214 -3996
  211. package/dist/{solid-BzCFdL25.js → solid-CRFYWlBw.js} +5647 -5373
  212. package/dist/solid.d.ts +6 -0
  213. package/dist/solid.js +259 -254
  214. package/dist/solid.server.js +259 -254
  215. package/dist/{source-GEnFF528.js → source-Da1TFiuj.js} +1 -1
  216. package/dist/speech-CjbWuqG2.js +1 -0
  217. package/dist/{star-CilHq8GP.js → star-ZvSbbXS6.js} +1 -1
  218. package/dist/{store-Ciyk6eZG.js → store-DE5lcr9q.js} +1 -1
  219. package/dist/{switch-KkeQIbuH.js → switch-P_fsHqv3.js} +1 -1
  220. package/dist/{tasks-card-BrN4bggJ.js → tasks-card-B86qqyZm.js} +1 -1
  221. package/dist/text-shimmer-BAkw_65G.js +1 -0
  222. package/dist/textarea-Bkinatpi.js +1 -0
  223. package/dist/theme.tokens.css +22 -21
  224. package/dist/{thumbs-up-CsT9Wzb9.js → thumbs-up-CDGW7a4o.js} +1 -1
  225. package/dist/{toast-store-tgK6MuMv.js → toast-store-B94I50AO.js} +1 -1
  226. package/dist/{tool-DzUjIBT8.js → tool-CUiCeyi1.js} +1 -1
  227. package/dist/{tooltip-Bkkg_4t0.js → tooltip-B3wlQSPh.js} +1 -1
  228. package/dist/{trash-2-jBKxjGGQ.js → trash-2-Cu8yZmFQ.js} +1 -1
  229. package/dist/{triangle-alert-DlQ68ppf.js → triangle-alert-CCtDUUmn.js} +1 -1
  230. package/dist/ui/button-variant-names.d.ts +12 -0
  231. package/dist/ui/button.d.ts +6 -0
  232. package/dist/ui/color-field.d.ts +48 -0
  233. package/dist/ui/textarea.d.ts +4 -0
  234. package/dist/ui/toggle-chip.d.ts +40 -0
  235. package/dist/{upload-TcAHXv9W.js → upload-CxotJlLv.js} +1 -1
  236. package/dist/{use-card-resolution-DaZqAdWf.js → use-card-resolution-DILUvNyj.js} +1 -1
  237. package/dist/{variant-aurora-DbW_OWL2.js → variant-aurora-BGaXBBGf.js} +1 -1
  238. package/dist/{variant-aurora-BhLuYxJR.js → variant-aurora-BLcQmvkd.js} +1 -1
  239. package/dist/{variant-aurora-CNmfHhcx.js → variant-aurora-DZIfAtx1.js} +2 -2
  240. package/dist/{variant-aurora-D6GsdTPA.js → variant-aurora-DpoIW4ZX.js} +2 -2
  241. package/dist/{variant-custom-BRvaNHSi.js → variant-custom-BhtWdAxE.js} +1 -1
  242. package/dist/{variant-custom-DMTrEO5w.js → variant-custom-BtApuSyr.js} +2 -2
  243. package/dist/{variant-custom-DV2x254s.js → variant-custom-C-zS4ZtE.js} +2 -2
  244. package/dist/{variant-custom-BvYHR62b.js → variant-custom-swi9PXxU.js} +1 -1
  245. package/dist/{variant-wave-sp-qwsdI.js → variant-wave-7JCIwhpb.js} +1 -1
  246. package/dist/{variant-wave-BpDd8t3Z.js → variant-wave-BG4nGr6x.js} +1 -1
  247. package/dist/{variant-wave-BNMGrDfj.js → variant-wave-C32lM9G2.js} +2 -2
  248. package/dist/{variant-wave-CyBueXj9.js → variant-wave-Dl6jOiK3.js} +2 -2
  249. package/dist/x-C4niw05N.js +1 -0
  250. package/frameworks/react/index.tsx +12 -6
  251. package/kit-base.css +311 -0
  252. package/llms-full.txt +9 -6
  253. package/llms.txt +1 -1
  254. package/package.json +23 -4
  255. package/solid.css +48 -0
  256. package/src/elements/element-meta.json +39 -14
  257. package/theme.css +39 -23
  258. package/dist/action-icons-CcgNN2oT.js +0 -1
  259. package/dist/arrow-down-CjRU3JgV.js +0 -1
  260. package/dist/arrow-left-Bnpb1UEW.js +0 -1
  261. package/dist/artifact-Bwt7OgcR.js +0 -1
  262. package/dist/assets/dev-5XzXYR21.js +0 -91
  263. package/dist/button-DkuOnGLy.js +0 -1
  264. package/dist/check-njkVdZu2.js +0 -1
  265. package/dist/chevron-down-B72s6PXR.js +0 -1
  266. package/dist/chevron-right-BatuHzUm.js +0 -1
  267. package/dist/circle-check-B62pODB3.js +0 -1
  268. package/dist/circle-zx0ByzY9.js +0 -1
  269. package/dist/controllable-CnGpMF6r.js +0 -1
  270. package/dist/define-By-uL588.js +0 -1
  271. package/dist/loader-BM4CEz4G.js +0 -1
  272. package/dist/message-BSsgvHvf.js +0 -1
  273. package/dist/message-BYZECyek.js +0 -1
  274. package/dist/message-circle-DruwObfX.js +0 -1
  275. package/dist/message-square-BG6q6mlm.js +0 -1
  276. package/dist/play-BvKKq3fX.js +0 -1
  277. package/dist/register-impl-B1hLkrI8.js +0 -424
  278. package/dist/text-shimmer-Dcj1MHuq.js +0 -1
  279. package/dist/textarea-Dpvbngbg.js +0 -1
  280. package/dist/x-Z5UntkIJ.js +0 -1
package/dist/mcp.es.js CHANGED
@@ -7794,14 +7794,18 @@ function renderSolid(components, ctx) {
7794
7794
  `//`,
7795
7795
  `// SETUP, once. These components are Tailwind-v4 SOURCE (not shadow-encapsulated),`,
7796
7796
  `// so their class names have to reach YOUR stylesheet:`,
7797
- `// npm i -D tailwindcss @tailwindcss/vite vite-plugin-solid`,
7797
+ `// npm i -D tailwindcss @tailwindcss/vite vite-plugin-solid tw-animate-css @tailwindcss/typography`,
7798
7798
  `// vite.config.ts -> plugins: [solid(), tailwindcss()]`,
7799
7799
  `// src/styles.css -> @import "tailwindcss";`,
7800
- `// @import "@kitn.ai/ui/theme.css"; /* --color-* tokens */`,
7800
+ `// @import "@kitn.ai/ui/solid.css"; /* tokens + base rules + plugins */`,
7801
7801
  `// @source "../node_modules/@kitn.ai/ui"; /* scan the kit for classes */`,
7802
7802
  `// The @source line is NOT optional: without it Tailwind scans only src/, strips`,
7803
7803
  `// every kit utility class as unused, and the whole UI renders unstyled.`,
7804
- `// (theme.css here, not theme.tokens.css: this app compiles Tailwind itself.)`,
7804
+ `// solid.css, not theme.css: theme.css is the tokens only. solid.css imports it and`,
7805
+ `// adds what the kai-* elements carry in their shadow roots and this app has to`,
7806
+ `// compile itself: the form-control and focus-ring rules, tw-animate-css (every`,
7807
+ `// overlay animation) and the typography plugin (prose-sm/prose-lg). The two`,
7808
+ `// plugins are optional peers of the kit, hence the extra installs above.`,
7805
7809
  `import { For, Index, Match, Show, Switch, ${block ? "createEffect, " : ""}createMemo, createSignal${block ? ", onCleanup" : ""} } from 'solid-js';`,
7806
7810
  // '@kitn.ai/ui/solid', NOT the root '@kitn.ai/ui', and the difference is
7807
7811
  // invisible to every compiler on this repo's critical path: src/solid.ts is
@@ -8997,7 +9001,7 @@ const scaffold = {
8997
9001
  );
8998
9002
  }
8999
9003
  };
9000
- const themeCss = "/* @kitn.ai/ui design system — import after `@import \"tailwindcss\"`.\n Defines ONLY namespaced (--color-*) tokens, component keyframes, animation\n utilities, and the .chat-markdown styles. Contains NO global base/reset rules,\n so importing it restyles nothing on its own — a host opts in via the kit's\n components + classes. Rebrand by overriding the --color-* values (optionally\n scoped to a subtree).\n NOTE: This file is Tailwind-v4 source (requires tailwindcss as a peer). For\n bundler-based apps that don't use Tailwind, import @kitn.ai/ui/theme.tokens.css\n instead — it is plain compiled CSS with no toolchain requirements. */\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: var(--kai-color-background, hsl(0 0% 100%));\n --color-foreground: var(--kai-color-foreground, hsl(240 10% 3.9%));\n --color-card: var(--kai-color-card, hsl(0 0% 100%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(240 10% 3.9%));\n --color-popover: var(--kai-color-popover, hsl(0 0% 100%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(240 10% 3.9%));\n --color-primary: var(--kai-color-primary, hsl(240 5.9% 10%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(0 0% 98%));\n --color-secondary: var(--kai-color-secondary, hsl(240 4.8% 95.9%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(240 5.9% 10%));\n --color-muted: var(--kai-color-muted, hsl(240 4.8% 95.9%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(240 3.8% 43%));\n /* Subtle SURFACE — an opaque \"muted panel\" for things that sit OVER content\n (the prompt input, message bubbles, artifact/card headers, chips). CALCULATED\n from existing tokens — `--color-muted` blended 40% onto `--color-background`\n — so it tracks the theme (incl. dark) automatically and needs no hand-picked\n value. SOLID (no alpha), so nothing behind it bleeds through. This replaces\n ad-hoc `bg-muted/40` surfaces. Retint via the --kai-color-surface knob (or a\n bare value); --color-surface-strong is the stronger step (hover/raised) and\n --color-surface-sunken is recessed BELOW the base surface (wells, or a banner\n that sits behind a card). Sunken is theme-specific: \"darker than the card\"\n means a LOWER muted% in dark but a HIGHER one in light, so the two scopes\n can't share a number — see the .dark block. */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 85%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(240 4.8% 95.9%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(240 5.9% 10%));\n --color-destructive: var(--kai-color-destructive, hsl(0 72% 45%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - solid fills with a legible foreground, matched to\n the kit's tool-green / -amber / -blue hues so they read on-brand. Use\n bg-success / bg-warning / bg-info with the matching -foreground for badges,\n banners, and buttons; or text-success / -warning / -info on a soft tint\n (below). destructive (above) is the danger/error member of the same family.\n Light uses darker hues so the color carries a light foreground AND stays\n legible as text on a pale tint; dark (see .dark) flips to brighter hues over\n a near-black foreground. Override via --kai-color-success / -warning / -info\n (+ -foreground). */\n --color-success: var(--kai-color-success, hsl(142 71% 30%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(0 0% 98%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 30%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(0 0% 98%));\n --color-info: var(--kai-color-info, hsl(217 91% 42%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(0 0% 98%));\n /* Soft tinted status surfaces - OPAQUE, theme-tracking fills derived as ~14% of\n the status hue blended onto the background. Use these for subtle callouts /\n banners instead of an eyeballed bg-success/10; pair with text-success /\n -warning / -info / -destructive. Derived, so declared in BOTH light and dark\n (a color-mix resolves where it's declared - see the surface tokens above). A\n bare bg- utility for a color-mix token is added at the bottom (see bg-surface). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n /* Interaction-state backgrounds - opaque, theme-tracking fills for row/control\n states so consumers stop eyeballing /10 opacities. --color-hover is a subtle\n neutral (a little --color-muted on the background); --color-selected is a\n faint --color-primary tint so a selected row tracks the brand. Use bg-hover /\n bg-selected. Derived, so declared in BOTH light and dark; bare bg- utilities\n added at the bottom. */\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n /* UNREAD indicator — a dedicated token (owner ruling, 2026-08-26), not a\n reuse of --color-destructive: unread is notification semantics, not\n error, so a consumer re-theming \"danger\" red must not also move every\n unread dot. Used identically by all three places an unread conversation\n surfaces: the list-row trailing dot (ConversationPanel), the header\n toggle's badge (ChatThread), and Dock's own closed-launcher badge.\n A vivid \"messenger red\" (Tailwind red-500 territory, the Intercom/iOS\n badge convention) rather than the darker --color-destructive hue: the\n first pass at this token picked a color that read as legible-on-paper\n but visually flat/dark next to the launcher's near-black ring. This one\n value is used in BOTH light and dark (no .dark override below) because\n it independently clears 3:1 (WCAG 1.4.11, non-text) against every real\n surface an unread dot sits on in EITHER mode: light row/card background\n (white) 3.78:1, light Dock-launcher ring (--color-primary, near-black)\n 4.68:1, dark row/card background (near-black) 4.73–4.35:1, dark\n Dock-launcher ring (--color-primary, near-white) 3.62:1. (Measured via\n the WCAG relative-luminance formula against this file's own light/dark\n --color-background/-card/-primary values — see\n .superpowers/sdd/2026-08-26-conversations/rework-report.md Round 4 for\n the worked numbers.) Override via --kai-color-unread; a construct author\n can also set it per-widget through `theme.unreadColor` (construct\n vocabulary — see agent-tooling/construct/schema.ts). */\n --color-unread: var(--kai-color-unread, hsl(0 84% 60%));\n /* MARKED text — the fill behind a keyword the app has flagged (the composer's\n `highlights` rules, decorated through the CSS Custom Highlight API). This is\n `<mark>`, not a selection and not a status: a warm yellow, the convention\n every browser already ships for <mark> and every reader already reads as\n \"someone marked this\". Deliberately NOT derived from --color-primary — that\n token is a NEUTRAL near-black in light and near-white in dark, so any tint of\n it is grey, which is why the highlight used to be invisible. Also not blue\n (the browser's own ::selection is blue — a highlight must not read as a\n selection), not red (error) and not the skill/agent pill hues (blue/violet).\n OPAQUE, so it composites predictably over whichever field it lands on.\n Override via --kai-color-highlight. Light: near-black text over it is 15.6:1;\n dark: near-white text over it is 6.7:1 — both well clear of WCAG AA. */\n --color-highlight: var(--kai-color-highlight, hsl(45 96% 78%));\n /* SELECTED text — the kit's own ::selection fill, applied inside every kai-*\n shadow root (see src/elements/styles.css for the scoping rationale). The kit\n owns this because leaving it to the UA meant the browser's blue outranked the\n kit's own deliberate emphasis: a marked keyword was LESS prominent than an\n incidental drag-select. Stays in the cool/blue family the UA trained everyone\n to read as \"selected\", so it can never be confused with the warm --color-highlight\n mark above; OPAQUE, so a selection over a mark composites cleanly (selection\n paints on top of a custom highlight) instead of muddying into a third colour.\n The FOREGROUND is set too, and that is not cosmetic: without it the selected\n glyph keeps its own colour, and the kit's coloured text — muted text (3.8:1),\n the skill/agent pill hues (3.6:1 / 4.0:1), inline code (4.7:1) — lands at or\n under WCAG AA on this fill. Forcing the foreground token makes the guarantee\n hold for every glyph regardless of what colour it had. Light 13.99:1,\n dark 8.87:1. Override via --kai-color-selection(-foreground). */\n --color-selection: var(--kai-color-selection, hsl(213 94% 87%));\n --color-selection-foreground: var(--kai-color-selection-foreground, hsl(240 10% 3.9%));\n --color-border: var(--kai-color-border, hsl(240 5.9% 90%));\n /* CONTROL boundary — the visible edge of an interactive form control (text\n input, textarea, select, the radio/checkbox ring). WCAG 2.1 SC 1.4.11\n Non-text Contrast requires >=3:1 against the adjacent surface for exactly\n this: a control whose boundary you cannot see is not perceivable. That is\n why this is NOT --color-border: a divider or a card outline is decorative\n and may sit below 3:1, a control's edge may not. Do NOT use border-input\n for list wrappers or panel outlines — use border-border there.\n Contrast is the requirement; a thicker border does not satisfy 1.4.11. */\n --color-input: var(--kai-color-input, hsl(240 5% 53%));\n /* Focus ring — a deliberate blue (not a neutral) so keyboard focus is always\n obvious and on-brand in both modes. Light uses a strong blue; dark uses a\n brighter one for contrast on dark surfaces. Both clear WCAG 2.1 non-text\n contrast (≥3:1) against the kit's backgrounds. Override via --kai-color-ring. */\n --color-ring: var(--kai-color-ring, hsl(217 91% 53%));\n /* Custom scrollbars — thin, rounded, subtle; thumb strengthens on hover. A\n consistent cross-platform look (the kit styles its OWN scroll regions inside\n the shadow root; the track stays transparent so nothing is forced where no\n scrollbar is needed). Override via --kai-color-scrollbar-thumb(-hover). */\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(240 5% 80%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(240 4% 64%));\n --color-sidebar: var(--kai-color-sidebar, hsl(0 0% 100%));\n\n /* Inline `code` accent. Blue text on a translucent blue chip. */\n --color-code-foreground: var(--kai-color-code-foreground, hsl(224.3 76.3% 48%));\n\n /* Tool/status chip hues. Each chip is hue text over a 15% translucent fill of\n the SAME hue (set in tool.tsx). The bare hue is too light to reach WCAG AA\n (4.5:1) on the faint fill in LIGHT mode, so the light values are darkened;\n dark mode keeps brighter hues for AA on the dark fill. Override via\n --kai-color-tool-* to retheme. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 38%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 28%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 26%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 72% 42%));\n\n --radius: var(--kai-radius, 0.6rem);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n\n /* Typography scale — ONE scale for the whole kit. Each semantic name\n (text-micro / text-caption / text-meta / text-compact / text-body /\n text-title / text-lg)\n resolves through a namespaced --kai-text-* token, so overriding that token\n on :root restyles the kit globally — it pierces the Shadow DOM via the\n var() fallback, exactly like the --kai-color-* tokens. The bare --text-*\n names stay internal so a host's own --text-* can't collide.\n\n Tailwind's OWN scale is re-pointed at the same tokens: in Tailwind v4 the\n `text-xs` / `text-sm` / `text-base` / `text-lg` utilities are generated FROM\n --text-xs/sm/base/lg in this very @theme block, so aliasing them makes\n text-xs ≡ text-meta, text-sm ≡ text-body, text-base ≡ text-title and\n text-lg ≡ text-lg. That matters because the vast majority of the kit's call\n sites spell the Tailwind name; without the alias the --kai-text-* knobs\n reached ~12% of them and the \"globally\" promise in the theming guide was\n false. Defaults are byte-identical to stock Tailwind, so nothing moves\n until a consumer actually sets a --kai-text-* token — including in a\n consumer's own Tailwind build that imports this file, which is the point:\n one scale everywhere, kit and app moving together.\n\n Line-heights are UNITLESS RATIOS (Tailwind's own), never fixed rem. A fixed\n leading does not scale: --kai-text-body: 1.25rem under the old 1.45rem\n leading gave 20px text on 23.2px leading — near solid-set — which made the\n themable token unsafe to actually theme.\n (Message/markdown reading size also scales with the `proseSize` prop; these\n tokens cover the fixed chrome & controls.) */\n --text-micro: var(--kai-text-micro, 0.625rem); --text-micro--line-height: calc(0.875 / 0.625); /* 10px — badges, pills, uppercase eyebrows */\n --text-caption: var(--kai-text-caption, 0.6875rem); --text-caption--line-height: calc(1 / 0.6875); /* 11px — micro labels, sub-counts, xs code */\n --text-meta: var(--kai-text-meta, 0.75rem); --text-meta--line-height: calc(1 / 0.75); /* 12px — controls, toggles, switchers, captions */\n --text-xs: var(--kai-text-meta, 0.75rem); --text-xs--line-height: calc(1 / 0.75); /* ≡ text-meta */\n --text-compact: var(--kai-text-compact, 0.8125rem); --text-compact--line-height: calc(1.21875 / 0.8125); /* 13px — dense chrome & code: a notch below body */\n --text-body: var(--kai-text-body, 0.875rem); --text-body--line-height: calc(1.25 / 0.875); /* 14px — primary reading text */\n --text-sm: var(--kai-text-body, 0.875rem); --text-sm--line-height: calc(1.25 / 0.875); /* ≡ text-body */\n --text-title: var(--kai-text-title, 1rem); --text-title--line-height: calc(1.5 / 1); /* 16px — emphasis / headers */\n --text-base: var(--kai-text-title, 1rem); --text-base--line-height: calc(1.5 / 1); /* ≡ text-title */\n --text-lg: var(--kai-text-lg, 1.125rem); --text-lg--line-height: calc(1.75 / 1.125); /* 18px — section headings, lg prose */\n\n @keyframes typing { 0%,100% { transform: translateY(0); opacity: .5 } 50% { transform: translateY(-2px); opacity: 1 } }\n @keyframes loading-dots { 0%,100% { opacity: 0 } 50% { opacity: 1 } }\n @keyframes wave { 0%,100% { transform: scaleY(1) } 50% { transform: scaleY(.6) } }\n @keyframes blink { 0%,100% { opacity: 1 } 50% { opacity: 0 } }\n @keyframes text-blink { 0%,100% { color: var(--color-primary) } 50% { color: var(--color-muted-foreground) } }\n @keyframes bounce-dots { 0%,100% { transform: scale(.8); opacity: .5 } 50% { transform: scale(1.2); opacity: 1 } }\n @keyframes thin-pulse { 0%,100% { transform: scale(.95); opacity: .8 } 50% { transform: scale(1.05); opacity: .4 } }\n @keyframes pulse-dot { 0%,100% { transform: scale(1); opacity: .8 } 50% { transform: scale(1.5); opacity: 1 } }\n @keyframes shimmer-text { 0% { background-position: 150% center } 100% { background-position: -150% center } }\n @keyframes wave-bars { 0%,100% { transform: scaleY(1); opacity: .5 } 50% { transform: scaleY(.6); opacity: 1 } }\n @keyframes shimmer { 0% { background-position: 200% 50% } 100% { background-position: -200% 50% } }\n @keyframes spinner-fade { 0% { opacity: 0 } 100% { opacity: 1 } }\n @keyframes collapsible-down { from { height: 0; opacity: 0 } to { height: var(--kb-collapsible-content-height); opacity: 1 } }\n @keyframes collapsible-up { from { height: var(--kb-collapsible-content-height); opacity: 1 } to { height: 0; opacity: 0 } }\n}\n\n.dark {\n --color-background: var(--kai-color-background, hsl(50 2% 9%));\n --color-foreground: var(--kai-color-foreground, hsl(0 0% 98%));\n --color-card: var(--kai-color-card, hsl(45 4% 12%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(0 0% 98%));\n --color-popover: var(--kai-color-popover, hsl(45 4% 12%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(0 0% 98%));\n --color-primary: var(--kai-color-primary, hsl(0 0% 98%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(45 4% 11%));\n --color-secondary: var(--kai-color-secondary, hsl(45 4% 17%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(0 0% 98%));\n --color-muted: var(--kai-color-muted, hsl(45 4% 17%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(45 4% 64%));\n /* Re-declared in the dark scope so the color-mix recomputes with the DARK\n muted/background (a derived token is computed where it's declared, not where\n it's used — see the light --color-surface for the full note). */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n /* Sunken: in dark a LOWER muted% is darker (blends toward the dark background). */\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 18%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(45 4% 17%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(0 0% 98%));\n --color-destructive: var(--kai-color-destructive, hsl(0 62.8% 30.6%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - dark mode flips to the brighter tool-* dark hues\n (matching the chip values) carried over a near-black foreground, so the color\n both reads as text on a dark soft tint AND backs a legible foreground. See\n the light @theme note. */\n --color-success: var(--kai-color-success, hsl(142 71% 45%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(45 4% 11%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 50%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(45 4% 11%));\n --color-info: var(--kai-color-info, hsl(217 91% 70%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(45 4% 11%));\n /* Soft status surfaces + interaction states - re-declared so each color-mix\n recomputes with the DARK hues/background (a derived token resolves where it's\n declared; see the light block for the full note). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n /* Unread indicator — dark. Same value as light (see the light block for the\n full contrast note): one vivid red independently clears 3:1 against both\n modes' real surfaces, so there is nothing to flip here. Still redeclared\n for override symmetry with every other semantic color in this file. */\n --color-unread: var(--kai-color-unread, hsl(0 84% 60%));\n /* Marked text — dark. A deep warm amber rather than the light yellow: a pale\n highlighter on a near-black field would blow out and force the text to flip\n colour. See the light block for the full note. */\n --color-highlight: var(--kai-color-highlight, hsl(40 75% 26%));\n /* Selected text — dark. A deep blue rather than the light one, for the same\n reason the mark darkens. See the light block for the full note. */\n --color-selection: var(--kai-color-selection, hsl(217 60% 32%));\n --color-selection-foreground: var(--kai-color-selection-foreground, hsl(0 0% 98%));\n --color-border: var(--kai-color-border, hsl(45 4% 17%));\n /* Control boundary — see the light block. Lifted to clear 3:1 (SC 1.4.11)\n against the dark background and card surfaces. */\n --color-input: var(--kai-color-input, hsl(45 4% 48%));\n --color-ring: var(--kai-color-ring, hsl(217 91% 68%));\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(45 3% 30%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(45 3% 42%));\n --color-sidebar: var(--kai-color-sidebar, hsl(50 2% 7%));\n --color-code-foreground: var(--kai-color-code-foreground, hsl(213 94% 78%));\n\n /* Tool/status chip hues — dark mode. Brighter hues reach AA on the dark fill. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 70%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 50%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 45%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 84% 70%));\n}\n\n/* Self-contained markdown styling — replaces the typography plugin's `prose`.\n Keyed on --color-* tokens so it themes with the rest of the kit. */\n.chat-markdown { color: var(--color-foreground); line-height: 1.6; }\n.chat-markdown > div:first-child > :first-child { margin-top: 0; }\n.chat-markdown > div:last-child > :last-child { margin-bottom: 0; }\n.chat-markdown p { margin: 0 0 0.75em; }\n.chat-markdown p:last-child { margin-bottom: 0; }\n.chat-markdown h1, .chat-markdown h2, .chat-markdown h3, .chat-markdown h4 {\n margin: 1.2em 0 0.5em; font-weight: 600; line-height: 1.3; color: var(--color-foreground);\n}\n.chat-markdown h1 { font-size: 1.4em; } .chat-markdown h2 { font-size: 1.25em; }\n.chat-markdown h3 { font-size: 1.1em; } .chat-markdown h4 { font-size: 1em; }\n.chat-markdown ul, .chat-markdown ol { margin: 0 0 0.75em; padding-left: 1.4em; }\n.chat-markdown li { margin: 0.2em 0; }\n.chat-markdown ul { list-style: disc; } .chat-markdown ol { list-style: decimal; }\n.chat-markdown a { color: var(--color-primary); text-decoration: underline; text-underline-offset: 2px; }\n.chat-markdown strong { font-weight: 600; color: var(--color-foreground); }\n.chat-markdown em { font-style: italic; }\n.chat-markdown blockquote {\n margin: 0 0 0.75em; padding-left: 0.9em; border-left: 2px solid var(--color-border);\n color: var(--color-muted-foreground);\n}\n.chat-markdown hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border); }\n.chat-markdown code:not(pre code) {\n background: color-mix(in oklab, var(--color-code-foreground) 15%, transparent);\n color: var(--color-code-foreground);\n border-radius: 4px; padding: 0.1em 0.35em; font-size: 0.875em;\n font-family: var(--kai-font-code, ui-monospace, \"SF Mono\", Menlo, monospace);\n}\n.chat-markdown table { width: 100%; border-collapse: collapse; margin: 0 0 0.75em; font-size: 0.9em; }\n.chat-markdown th, .chat-markdown td { border: 1px solid var(--color-border); padding: 0.4em 0.6em; text-align: left; }\n.chat-markdown th { font-weight: 600; background: color-mix(in oklab, var(--color-muted-foreground) 8%, transparent); }\n\n/* Themeable elevation. The kit is mostly flat (borders), but a few surfaces lift\n off the page — popovers, dropdowns, hover cards, cards. The shadow COLOR is a\n token (--kai-shadow-color, default near-black); set it transparent for a fully\n flat look, or tint it to match a theme. Defined here (not as a Tailwind shadow\n utility) so the color stays overridable and works in both the web-component and\n SolidJS layers. */\n.kai-elevation {\n box-shadow:\n 0 4px 12px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 14%, transparent),\n 0 2px 6px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 10%, transparent);\n}\n.kai-elevation-sm {\n box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 8%, transparent);\n}\n\n/* Cross-platform thin scrollbar utility (Firefox + WebKit). Used by ScrollArea\n and any scroll region in the SolidJS-component build (web components also get\n this globally inside their shadow root — see src/elements/styles.css). */\n.scrollbar-thin {\n scrollbar-width: thin;\n scrollbar-color: var(--color-scrollbar-thumb) transparent;\n}\n.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }\n.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }\n.scrollbar-thin::-webkit-scrollbar-thumb {\n background-color: var(--color-scrollbar-thumb);\n border-radius: 9999px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n.scrollbar-thin::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }\n.scrollbar-thin::-webkit-scrollbar-corner { background: transparent; }\n\n/* Surface utilities — `bg-surface` / `bg-surface-strong` paint the opaque,\n theme-derived surface tokens (see --color-surface above). Defined explicitly\n because a color-mix-valued @theme token does NOT emit a bare `bg-` utility on\n its own (only opacity variants). Use these for panels that sit OVER content\n instead of `bg-muted/NN`. */\n@utility bg-surface {\n background-color: var(--color-surface);\n}\n@utility bg-surface-strong {\n background-color: var(--color-surface-strong);\n}\n@utility bg-surface-sunken {\n background-color: var(--color-surface-sunken);\n}\n\n/* Soft-status + interaction-state utilities. Same reason as bg-surface above: a\n color-mix-valued @theme token does NOT emit a bare `bg-` utility on its own, so\n bg-success-soft / bg-warning-soft / bg-info-soft / bg-destructive-soft and\n bg-hover / bg-selected are declared here. (The SOLID tokens - success / warning\n / info + their -foreground - are plain hsl values, so bg-success / text-success\n etc. ARE auto-generated by @theme and need no explicit utility.) */\n@utility bg-success-soft {\n background-color: var(--color-success-soft);\n}\n@utility bg-warning-soft {\n background-color: var(--color-warning-soft);\n}\n@utility bg-info-soft {\n background-color: var(--color-info-soft);\n}\n@utility bg-destructive-soft {\n background-color: var(--color-destructive-soft);\n}\n@utility bg-hover {\n background-color: var(--color-hover);\n}\n@utility bg-selected {\n background-color: var(--color-selected);\n}\n";
9004
+ const themeCss = "/* @kitn.ai/ui design system — import after `@import \"tailwindcss\"`.\n Defines the shadcn-style (--color-*) tokens, component keyframes, animation\n utilities, and the .chat-markdown styles. Contains NO global base/reset rules,\n so importing it restyles nothing on its own — a host opts in via the kit's\n components + classes. Rebrand by overriding the --color-* values (optionally\n scoped to a subtree).\n NOTE: This file is Tailwind-v4 source (requires tailwindcss as a peer). For\n bundler-based apps that don't use Tailwind, import @kitn.ai/ui/theme.tokens.css\n instead — it is plain compiled CSS with no toolchain requirements.\n\n THIS FILE IS MERGED INTO THE CONSUMER'S TAILWIND BUILD when they @import it,\n so everything at global scope here lands in THEIR namespace. What it changes\n for them (documented in apps/docs/.../guides/theming.mdx, keep the two in sync):\n - `@custom-variant dark` below switches their `dark:` utilities from\n prefers-color-scheme to the `.dark` class.\n - the `--color-*` names merge into their @theme (last import wins).\n - `--radius-sm/md/lg/xl` re-point Tailwind's stock radius scale at --radius.\n - `--text-xs/sm/base/lg` are aliased onto the kit's type scale (defaults\n equal stock Tailwind, so nothing moves until a --kai-text-* token is set).\n Everything else global carries a `kai-` prefix so it cannot clash by name:\n every @keyframes is `kai-<name>`, the scrollbar utility is `.kai-scrollbar-thin`.\n The one unprefixed global class is `.chat-markdown` (public; renaming it is a\n breaking change). tests/elements/theme-globals.test.ts pins the prefix rule\n and that every animation reference in src/ resolves to a keyframe here. */\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: var(--kai-color-background, hsl(0 0% 100%));\n --color-foreground: var(--kai-color-foreground, hsl(240 10% 3.9%));\n --color-card: var(--kai-color-card, hsl(0 0% 100%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(240 10% 3.9%));\n --color-popover: var(--kai-color-popover, hsl(0 0% 100%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(240 10% 3.9%));\n --color-primary: var(--kai-color-primary, hsl(240 5.9% 10%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(0 0% 98%));\n --color-secondary: var(--kai-color-secondary, hsl(240 4.8% 95.9%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(240 5.9% 10%));\n --color-muted: var(--kai-color-muted, hsl(240 4.8% 95.9%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(240 3.8% 43%));\n /* Subtle SURFACE — an opaque \"muted panel\" for things that sit OVER content\n (the prompt input, message bubbles, artifact/card headers, chips). CALCULATED\n from existing tokens — `--color-muted` blended 40% onto `--color-background`\n — so it tracks the theme (incl. dark) automatically and needs no hand-picked\n value. SOLID (no alpha), so nothing behind it bleeds through. This replaces\n ad-hoc `bg-muted/40` surfaces. Retint via the --kai-color-surface knob (or a\n bare value); --color-surface-strong is the stronger step (hover/raised) and\n --color-surface-sunken is recessed BELOW the base surface (wells, or a banner\n that sits behind a card). Sunken is theme-specific: \"darker than the card\"\n means a LOWER muted% in dark but a HIGHER one in light, so the two scopes\n can't share a number — see the .dark block. */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 85%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(240 4.8% 95.9%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(240 5.9% 10%));\n --color-destructive: var(--kai-color-destructive, hsl(0 72% 45%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - solid fills with a legible foreground, matched to\n the kit's tool-green / -amber / -blue hues so they read on-brand. Use\n bg-success / bg-warning / bg-info with the matching -foreground for badges,\n banners, and buttons; or text-success / -warning / -info on a soft tint\n (below). destructive (above) is the danger/error member of the same family.\n Light uses darker hues so the color carries a light foreground AND stays\n legible as text on a pale tint; dark (see .dark) flips to brighter hues over\n a near-black foreground. Override via --kai-color-success / -warning / -info\n (+ -foreground). */\n --color-success: var(--kai-color-success, hsl(142 71% 30%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(0 0% 98%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 30%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(0 0% 98%));\n --color-info: var(--kai-color-info, hsl(217 91% 42%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(0 0% 98%));\n /* Soft tinted status surfaces - OPAQUE, theme-tracking fills derived as ~14% of\n the status hue blended onto the background. Use these for subtle callouts /\n banners instead of an eyeballed bg-success/10; pair with text-success /\n -warning / -info / -destructive. Derived, so declared in BOTH light and dark\n (a color-mix resolves where it's declared - see the surface tokens above). A\n bare bg- utility for a color-mix token is added at the bottom (see bg-surface). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n /* Interaction-state backgrounds - opaque, theme-tracking fills for row/control\n states so consumers stop eyeballing /10 opacities. --color-hover is a subtle\n neutral (a little --color-muted on the background); --color-selected is a\n faint --color-primary tint so a selected row tracks the brand. Use bg-hover /\n bg-selected. Derived, so declared in BOTH light and dark; bare bg- utilities\n added at the bottom. */\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n /* UNREAD indicator — a dedicated token (owner ruling, 2026-08-26), not a\n reuse of --color-destructive: unread is notification semantics, not\n error, so a consumer re-theming \"danger\" red must not also move every\n unread dot. Used identically by all three places an unread conversation\n surfaces: the list-row trailing dot (ConversationPanel), the header\n toggle's badge (ChatThread), and Dock's own closed-launcher badge.\n A vivid \"messenger red\" (Tailwind red-500 territory, the Intercom/iOS\n badge convention) rather than the darker --color-destructive hue: the\n first pass at this token picked a color that read as legible-on-paper\n but visually flat/dark next to the launcher's near-black ring. This one\n value is used in BOTH light and dark (no .dark override below) because\n it independently clears 3:1 (WCAG 1.4.11, non-text) against every real\n surface an unread dot sits on in EITHER mode: light row/card background\n (white) 3.78:1, light Dock-launcher ring (--color-primary, near-black)\n 4.68:1, dark row/card background (near-black) 4.73–4.35:1, dark\n Dock-launcher ring (--color-primary, near-white) 3.62:1. (Measured via\n the WCAG relative-luminance formula against this file's own light/dark\n --color-background/-card/-primary values — see\n .superpowers/sdd/2026-08-26-conversations/rework-report.md Round 4 for\n the worked numbers.) Override via --kai-color-unread; a construct author\n can also set it per-widget through `theme.unreadColor` (construct\n vocabulary — see agent-tooling/construct/schema.ts). */\n --color-unread: var(--kai-color-unread, hsl(0 84% 60%));\n /* MARKED text — the fill behind a keyword the app has flagged (the composer's\n `highlights` rules, decorated through the CSS Custom Highlight API). This is\n `<mark>`, not a selection and not a status: a warm yellow, the convention\n every browser already ships for <mark> and every reader already reads as\n \"someone marked this\". Deliberately NOT derived from --color-primary — that\n token is a NEUTRAL near-black in light and near-white in dark, so any tint of\n it is grey, which is why the highlight used to be invisible. Also not blue\n (the browser's own ::selection is blue — a highlight must not read as a\n selection), not red (error) and not the skill/agent pill hues (blue/violet).\n OPAQUE, so it composites predictably over whichever field it lands on.\n Override via --kai-color-highlight. Light: near-black text over it is 15.6:1;\n dark: near-white text over it is 6.7:1 — both well clear of WCAG AA. */\n --color-highlight: var(--kai-color-highlight, hsl(45 96% 78%));\n /* SELECTED text — the kit's own ::selection fill, applied inside every kai-*\n shadow root (see src/elements/styles.css for the scoping rationale). The kit\n owns this because leaving it to the UA meant the browser's blue outranked the\n kit's own deliberate emphasis: a marked keyword was LESS prominent than an\n incidental drag-select. Stays in the cool/blue family the UA trained everyone\n to read as \"selected\", so it can never be confused with the warm --color-highlight\n mark above; OPAQUE, so a selection over a mark composites cleanly (selection\n paints on top of a custom highlight) instead of muddying into a third colour.\n The FOREGROUND is set too, and that is not cosmetic: without it the selected\n glyph keeps its own colour, and the kit's coloured text — muted text (3.8:1),\n the skill/agent pill hues (3.6:1 / 4.0:1), inline code (4.7:1) — lands at or\n under WCAG AA on this fill. Forcing the foreground token makes the guarantee\n hold for every glyph regardless of what colour it had. Light 13.99:1,\n dark 8.87:1. Override via --kai-color-selection(-foreground). */\n --color-selection: var(--kai-color-selection, hsl(213 94% 87%));\n --color-selection-foreground: var(--kai-color-selection-foreground, hsl(240 10% 3.9%));\n --color-border: var(--kai-color-border, hsl(240 5.9% 90%));\n /* CONTROL boundary — the visible edge of an interactive form control (text\n input, textarea, select, the radio/checkbox ring). WCAG 2.1 SC 1.4.11\n Non-text Contrast requires >=3:1 against the adjacent surface for exactly\n this: a control whose boundary you cannot see is not perceivable. That is\n why this is NOT --color-border: a divider or a card outline is decorative\n and may sit below 3:1, a control's edge may not. Do NOT use border-input\n for list wrappers or panel outlines — use border-border there.\n Contrast is the requirement; a thicker border does not satisfy 1.4.11. */\n --color-input: var(--kai-color-input, hsl(240 5% 53%));\n /* Focus ring — a deliberate blue (not a neutral) so keyboard focus is always\n obvious and on-brand in both modes. Light uses a strong blue; dark uses a\n brighter one for contrast on dark surfaces. Both clear WCAG 2.1 non-text\n contrast (≥3:1) against the kit's backgrounds. Override via --kai-color-ring. */\n --color-ring: var(--kai-color-ring, hsl(217 91% 53%));\n /* Custom scrollbars — thin, rounded, subtle; thumb strengthens on hover. A\n consistent cross-platform look (the kit styles its OWN scroll regions inside\n the shadow root; the track stays transparent so nothing is forced where no\n scrollbar is needed). Override via --kai-color-scrollbar-thumb(-hover). */\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(240 5% 80%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(240 4% 64%));\n --color-sidebar: var(--kai-color-sidebar, hsl(0 0% 100%));\n\n /* Inline `code` accent. Blue text on a translucent blue chip. */\n --color-code-foreground: var(--kai-color-code-foreground, hsl(224.3 76.3% 48%));\n\n /* Tool/status chip hues. Each chip is hue text over a 15% translucent fill of\n the SAME hue (set in tool.tsx). The bare hue is too light to reach WCAG AA\n (4.5:1) on the faint fill in LIGHT mode, so the light values are darkened;\n dark mode keeps brighter hues for AA on the dark fill. Override via\n --kai-color-tool-* to retheme. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 38%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 28%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 26%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 72% 42%));\n\n --radius: var(--kai-radius, 0.6rem);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n\n /* Typography scale — ONE scale for the whole kit. Each semantic name\n (text-micro / text-caption / text-meta / text-compact / text-body /\n text-title / text-lg)\n resolves through a namespaced --kai-text-* token, so overriding that token\n on :root restyles the kit globally — it pierces the Shadow DOM via the\n var() fallback, exactly like the --kai-color-* tokens. The bare --text-*\n names stay internal so a host's own --text-* can't collide.\n\n Tailwind's OWN scale is re-pointed at the same tokens: in Tailwind v4 the\n `text-xs` / `text-sm` / `text-base` / `text-lg` utilities are generated FROM\n --text-xs/sm/base/lg in this very @theme block, so aliasing them makes\n text-xs ≡ text-meta, text-sm ≡ text-body, text-base ≡ text-title and\n text-lg ≡ text-lg. That matters because the vast majority of the kit's call\n sites spell the Tailwind name; without the alias the --kai-text-* knobs\n reached ~12% of them and the \"globally\" promise in the theming guide was\n false. Defaults are byte-identical to stock Tailwind, so nothing moves\n until a consumer actually sets a --kai-text-* token — including in a\n consumer's own Tailwind build that imports this file, which is the point:\n one scale everywhere, kit and app moving together.\n\n Line-heights are UNITLESS RATIOS (Tailwind's own), never fixed rem. A fixed\n leading does not scale: --kai-text-body: 1.25rem under the old 1.45rem\n leading gave 20px text on 23.2px leading — near solid-set — which made the\n themable token unsafe to actually theme.\n (Message/markdown reading size also scales with the `proseSize` prop; these\n tokens cover the fixed chrome & controls.) */\n --text-micro: var(--kai-text-micro, 0.625rem); --text-micro--line-height: calc(0.875 / 0.625); /* 10px — badges, pills, uppercase eyebrows */\n --text-caption: var(--kai-text-caption, 0.6875rem); --text-caption--line-height: calc(1 / 0.6875); /* 11px — micro labels, sub-counts, xs code */\n --text-meta: var(--kai-text-meta, 0.75rem); --text-meta--line-height: calc(1 / 0.75); /* 12px — controls, toggles, switchers, captions */\n --text-xs: var(--kai-text-meta, 0.75rem); --text-xs--line-height: calc(1 / 0.75); /* ≡ text-meta */\n --text-compact: var(--kai-text-compact, 0.8125rem); --text-compact--line-height: calc(1.21875 / 0.8125); /* 13px — dense chrome & code: a notch below body */\n --text-body: var(--kai-text-body, 0.875rem); --text-body--line-height: calc(1.25 / 0.875); /* 14px — primary reading text */\n --text-sm: var(--kai-text-body, 0.875rem); --text-sm--line-height: calc(1.25 / 0.875); /* ≡ text-body */\n --text-title: var(--kai-text-title, 1rem); --text-title--line-height: calc(1.5 / 1); /* 16px — emphasis / headers */\n --text-base: var(--kai-text-title, 1rem); --text-base--line-height: calc(1.5 / 1); /* ≡ text-title */\n --text-lg: var(--kai-text-lg, 1.125rem); --text-lg--line-height: calc(1.75 / 1.125); /* 18px — section headings, lg prose */\n\n @keyframes kai-typing { 0%,100% { transform: translateY(0); opacity: .5 } 50% { transform: translateY(-2px); opacity: 1 } }\n @keyframes kai-loading-dots { 0%,100% { opacity: 0 } 50% { opacity: 1 } }\n @keyframes kai-wave { 0%,100% { transform: scaleY(1) } 50% { transform: scaleY(.6) } }\n @keyframes kai-blink { 0%,100% { opacity: 1 } 50% { opacity: 0 } }\n @keyframes kai-text-blink { 0%,100% { color: var(--color-primary) } 50% { color: var(--color-muted-foreground) } }\n @keyframes kai-bounce-dots { 0%,100% { transform: scale(.8); opacity: .5 } 50% { transform: scale(1.2); opacity: 1 } }\n @keyframes kai-thin-pulse { 0%,100% { transform: scale(.95); opacity: .8 } 50% { transform: scale(1.05); opacity: .4 } }\n @keyframes kai-pulse-dot { 0%,100% { transform: scale(1); opacity: .8 } 50% { transform: scale(1.5); opacity: 1 } }\n @keyframes kai-shimmer-text { 0% { background-position: 150% center } 100% { background-position: -150% center } }\n @keyframes kai-wave-bars { 0%,100% { transform: scaleY(1); opacity: .5 } 50% { transform: scaleY(.6); opacity: 1 } }\n @keyframes kai-shimmer { 0% { background-position: 200% 50% } 100% { background-position: -200% 50% } }\n @keyframes kai-spinner-fade { 0% { opacity: 0 } 100% { opacity: 1 } }\n}\n\n.dark {\n --color-background: var(--kai-color-background, hsl(50 2% 9%));\n --color-foreground: var(--kai-color-foreground, hsl(0 0% 98%));\n --color-card: var(--kai-color-card, hsl(45 4% 12%));\n --color-card-foreground: var(--kai-color-card-foreground, hsl(0 0% 98%));\n --color-popover: var(--kai-color-popover, hsl(45 4% 12%));\n --color-popover-foreground: var(--kai-color-popover-foreground, hsl(0 0% 98%));\n --color-primary: var(--kai-color-primary, hsl(0 0% 98%));\n --color-primary-foreground: var(--kai-color-primary-foreground, hsl(45 4% 11%));\n --color-secondary: var(--kai-color-secondary, hsl(45 4% 17%));\n --color-secondary-foreground: var(--kai-color-secondary-foreground, hsl(0 0% 98%));\n --color-muted: var(--kai-color-muted, hsl(45 4% 17%));\n --color-muted-foreground: var(--kai-color-muted-foreground, hsl(45 4% 64%));\n /* Re-declared in the dark scope so the color-mix recomputes with the DARK\n muted/background (a derived token is computed where it's declared, not where\n it's used — see the light --color-surface for the full note). */\n --color-surface: var(--kai-color-surface, color-mix(in srgb, var(--color-muted) 40%, var(--color-background)));\n --color-surface-strong: var(--kai-color-surface-strong, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)));\n /* Sunken: in dark a LOWER muted% is darker (blends toward the dark background). */\n --color-surface-sunken: var(--kai-color-surface-sunken, color-mix(in srgb, var(--color-muted) 18%, var(--color-background)));\n --color-accent: var(--kai-color-accent, hsl(45 4% 17%));\n --color-accent-foreground: var(--kai-color-accent-foreground, hsl(0 0% 98%));\n --color-destructive: var(--kai-color-destructive, hsl(0 62.8% 30.6%));\n --color-destructive-foreground: var(--kai-color-destructive-foreground, hsl(0 0% 98%));\n /* Semantic status colors - dark mode flips to the brighter tool-* dark hues\n (matching the chip values) carried over a near-black foreground, so the color\n both reads as text on a dark soft tint AND backs a legible foreground. See\n the light @theme note. */\n --color-success: var(--kai-color-success, hsl(142 71% 45%));\n --color-success-foreground: var(--kai-color-success-foreground, hsl(45 4% 11%));\n --color-warning: var(--kai-color-warning, hsl(38 92% 50%));\n --color-warning-foreground: var(--kai-color-warning-foreground, hsl(45 4% 11%));\n --color-info: var(--kai-color-info, hsl(217 91% 70%));\n --color-info-foreground: var(--kai-color-info-foreground, hsl(45 4% 11%));\n /* Soft status surfaces + interaction states - re-declared so each color-mix\n recomputes with the DARK hues/background (a derived token resolves where it's\n declared; see the light block for the full note). */\n --color-success-soft: var(--kai-color-success-soft, color-mix(in oklab, var(--color-success) 14%, var(--color-background)));\n --color-warning-soft: var(--kai-color-warning-soft, color-mix(in oklab, var(--color-warning) 14%, var(--color-background)));\n --color-info-soft: var(--kai-color-info-soft, color-mix(in oklab, var(--color-info) 14%, var(--color-background)));\n --color-destructive-soft: var(--kai-color-destructive-soft, color-mix(in oklab, var(--color-destructive) 14%, var(--color-background)));\n --color-hover: var(--kai-color-hover, color-mix(in oklab, var(--color-muted) 60%, var(--color-background)));\n --color-selected: var(--kai-color-selected, color-mix(in oklab, var(--color-primary) 8%, var(--color-background)));\n /* Unread indicator — dark. Same value as light (see the light block for the\n full contrast note): one vivid red independently clears 3:1 against both\n modes' real surfaces, so there is nothing to flip here. Still redeclared\n for override symmetry with every other semantic color in this file. */\n --color-unread: var(--kai-color-unread, hsl(0 84% 60%));\n /* Marked text — dark. A deep warm amber rather than the light yellow: a pale\n highlighter on a near-black field would blow out and force the text to flip\n colour. See the light block for the full note. */\n --color-highlight: var(--kai-color-highlight, hsl(40 75% 26%));\n /* Selected text — dark. A deep blue rather than the light one, for the same\n reason the mark darkens. See the light block for the full note. */\n --color-selection: var(--kai-color-selection, hsl(217 60% 32%));\n --color-selection-foreground: var(--kai-color-selection-foreground, hsl(0 0% 98%));\n --color-border: var(--kai-color-border, hsl(45 4% 17%));\n /* Control boundary — see the light block. Lifted to clear 3:1 (SC 1.4.11)\n against the dark background and card surfaces. */\n --color-input: var(--kai-color-input, hsl(45 4% 48%));\n --color-ring: var(--kai-color-ring, hsl(217 91% 68%));\n --color-scrollbar-thumb: var(--kai-color-scrollbar-thumb, hsl(45 3% 30%));\n --color-scrollbar-thumb-hover: var(--kai-color-scrollbar-thumb-hover, hsl(45 3% 42%));\n --color-sidebar: var(--kai-color-sidebar, hsl(50 2% 7%));\n --color-code-foreground: var(--kai-color-code-foreground, hsl(213 94% 78%));\n\n /* Tool/status chip hues — dark mode. Brighter hues reach AA on the dark fill. */\n --color-tool-blue: var(--kai-color-tool-blue, hsl(217 91% 70%));\n --color-tool-amber: var(--kai-color-tool-amber, hsl(38 92% 50%));\n --color-tool-green: var(--kai-color-tool-green, hsl(142 71% 45%));\n --color-tool-red: var(--kai-color-tool-red, hsl(0 84% 70%));\n}\n\n/* Self-contained markdown styling — replaces the typography plugin's `prose`.\n Keyed on --color-* tokens so it themes with the rest of the kit. */\n.chat-markdown { color: var(--color-foreground); line-height: 1.6; }\n.chat-markdown > div:first-child > :first-child { margin-top: 0; }\n.chat-markdown > div:last-child > :last-child { margin-bottom: 0; }\n.chat-markdown p { margin: 0 0 0.75em; }\n.chat-markdown p:last-child { margin-bottom: 0; }\n.chat-markdown h1, .chat-markdown h2, .chat-markdown h3, .chat-markdown h4 {\n margin: 1.2em 0 0.5em; font-weight: 600; line-height: 1.3; color: var(--color-foreground);\n}\n.chat-markdown h1 { font-size: 1.4em; } .chat-markdown h2 { font-size: 1.25em; }\n.chat-markdown h3 { font-size: 1.1em; } .chat-markdown h4 { font-size: 1em; }\n.chat-markdown ul, .chat-markdown ol { margin: 0 0 0.75em; padding-left: 1.4em; }\n.chat-markdown li { margin: 0.2em 0; }\n.chat-markdown ul { list-style: disc; } .chat-markdown ol { list-style: decimal; }\n.chat-markdown a { color: var(--color-primary); text-decoration: underline; text-underline-offset: 2px; }\n.chat-markdown strong { font-weight: 600; color: var(--color-foreground); }\n.chat-markdown em { font-style: italic; }\n.chat-markdown blockquote {\n margin: 0 0 0.75em; padding-left: 0.9em; border-left: 2px solid var(--color-border);\n color: var(--color-muted-foreground);\n}\n.chat-markdown hr { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border); }\n.chat-markdown code:not(pre code) {\n background: color-mix(in oklab, var(--color-code-foreground) 15%, transparent);\n color: var(--color-code-foreground);\n border-radius: 4px; padding: 0.1em 0.35em; font-size: 0.875em;\n font-family: var(--kai-font-code, ui-monospace, \"SF Mono\", Menlo, monospace);\n}\n.chat-markdown table { width: 100%; border-collapse: collapse; margin: 0 0 0.75em; font-size: 0.9em; }\n.chat-markdown th, .chat-markdown td { border: 1px solid var(--color-border); padding: 0.4em 0.6em; text-align: left; }\n.chat-markdown th { font-weight: 600; background: color-mix(in oklab, var(--color-muted-foreground) 8%, transparent); }\n\n/* Themeable elevation. The kit is mostly flat (borders), but a few surfaces lift\n off the page — popovers, dropdowns, hover cards, cards. The shadow COLOR is a\n token (--kai-shadow-color, default near-black); set it transparent for a fully\n flat look, or tint it to match a theme. Defined here (not as a Tailwind shadow\n utility) so the color stays overridable and works in both the web-component and\n SolidJS layers. */\n.kai-elevation {\n box-shadow:\n 0 4px 12px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 14%, transparent),\n 0 2px 6px -2px color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 10%, transparent);\n}\n.kai-elevation-sm {\n box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--kai-shadow-color, oklch(0 0 0)) 8%, transparent);\n}\n\n/* Cross-platform thin scrollbar utility (Firefox + WebKit). Used by ScrollArea\n and any scroll region in the SolidJS-component build (web components also get\n this globally inside their shadow root — see src/elements/styles.css).\n `kai-` prefixed because this file is merged into a consumer's Tailwind build\n and `.scrollbar-thin` is a name they may well own (no consumer-facing doc ever\n named the old unprefixed class, so no alias is kept). */\n.kai-scrollbar-thin {\n scrollbar-width: thin;\n scrollbar-color: var(--color-scrollbar-thumb) transparent;\n}\n.kai-scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }\n.kai-scrollbar-thin::-webkit-scrollbar-track { background: transparent; }\n.kai-scrollbar-thin::-webkit-scrollbar-thumb {\n background-color: var(--color-scrollbar-thumb);\n border-radius: 9999px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n.kai-scrollbar-thin::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }\n.kai-scrollbar-thin::-webkit-scrollbar-corner { background: transparent; }\n\n/* Surface utilities — `bg-surface` / `bg-surface-strong` paint the opaque,\n theme-derived surface tokens (see --color-surface above). Defined explicitly\n because a color-mix-valued @theme token does NOT emit a bare `bg-` utility on\n its own (only opacity variants). Use these for panels that sit OVER content\n instead of `bg-muted/NN`. */\n@utility bg-surface {\n background-color: var(--color-surface);\n}\n@utility bg-surface-strong {\n background-color: var(--color-surface-strong);\n}\n@utility bg-surface-sunken {\n background-color: var(--color-surface-sunken);\n}\n\n/* Soft-status + interaction-state utilities. Same reason as bg-surface above: a\n color-mix-valued @theme token does NOT emit a bare `bg-` utility on its own, so\n bg-success-soft / bg-warning-soft / bg-info-soft / bg-destructive-soft and\n bg-hover / bg-selected are declared here. (The SOLID tokens - success / warning\n / info + their -foreground - are plain hsl values, so bg-success / text-success\n etc. ARE auto-generated by @theme and need no explicit utility.) */\n@utility bg-success-soft {\n background-color: var(--color-success-soft);\n}\n@utility bg-warning-soft {\n background-color: var(--color-warning-soft);\n}\n@utility bg-info-soft {\n background-color: var(--color-info-soft);\n}\n@utility bg-destructive-soft {\n background-color: var(--color-destructive-soft);\n}\n@utility bg-hover {\n background-color: var(--color-hover);\n}\n@utility bg-selected {\n background-color: var(--color-selected);\n}\n";
9001
9005
  function declaredKaiTokens(css) {
9002
9006
  const names = /* @__PURE__ */ new Set();
9003
9007
  for (const m of css.matchAll(/var\(\s*(--kai-[a-zA-Z0-9-]+)\s*[,)]/g)) names.add(m[1]);
@@ -9501,8 +9505,15 @@ function isSafeUrl(url) {
9501
9505
  const scheme = schemeOf(url);
9502
9506
  return scheme !== void 0 && SAFE_SCHEMES.includes(scheme);
9503
9507
  }
9508
+ const CHAT_MESSAGE_ACTIONS = ["copy", "like", "dislike", "regenerate", "edit", "speak"];
9509
+ const BUTTON_VARIANT_NAMES = ["default", "ghost", "subtle", "outline", "destructive"];
9504
9510
  const CONSTRUCT_SCHEMA_URL = "https://ui.kitn.ai/schemas/construct/v1.json";
9505
9511
  const TAG_RE = /^[a-z][a-z0-9]*-[a-z0-9-]+$/;
9512
+ const TriggerEntrySchema = z$1.object({
9513
+ id: z$1.string().min(1),
9514
+ label: z$1.string().min(1),
9515
+ description: z$1.string().min(1).optional()
9516
+ }).strict();
9506
9517
  const ProviderSchema = z$1.discriminatedUnion("mode", [
9507
9518
  z$1.object({ mode: z$1.literal("mock") }).strict(),
9508
9519
  z$1.object({
@@ -9571,7 +9582,26 @@ const ConstructSchema = z$1.object({
9571
9582
  /** Rendered in ChatThread's built-in header bar (left side). Construct-
9572
9583
  * authored/untrusted text, like theme.accent/provider.url — JSON.stringify'd
9573
9584
  * at its one emit site, never a raw JSX attribute string. */
9574
- title: z$1.string().min(1).optional()
9585
+ title: z$1.string().min(1).optional(),
9586
+ /** Renders a theme-toggle Button in ChatThread's header-end region,
9587
+ * flipping the host's `theme` attribute (the attribute
9588
+ * defineWebComponent already owns) via the facade's ctx.element —
9589
+ * codegen work, no new kit surface (B-10). */
9590
+ themeToggle: z$1.boolean().optional(),
9591
+ /** Header action buttons (label + kit Button variant), rendered in
9592
+ * the header-end region. Vocabulary-never-logic: the construct
9593
+ * cannot say what an action DOES, so each click dispatches a
9594
+ * non-bubbling `kai-header-action` CustomEvent on the host with
9595
+ * `detail: { label }` — the consumer's listening seam (B-10).
9596
+ * `variant` derives from the kit Button's own list
9597
+ * (BUTTON_VARIANT_NAMES — B-6a), never restated. `label` is
9598
+ * construct-authored untrusted text, JSON.stringify'd at emit. */
9599
+ actions: z$1.array(
9600
+ z$1.object({
9601
+ label: z$1.string().min(1),
9602
+ variant: z$1.enum(BUTTON_VARIANT_NAMES).optional()
9603
+ }).strict()
9604
+ ).min(1).optional()
9575
9605
  }).strict().optional(),
9576
9606
  /** Greeting shown while the thread is empty (no messages yet) — the
9577
9607
  * proven "welcome screen" pattern (Intercom-class): title + optional
@@ -9680,7 +9710,28 @@ const ConstructSchema = z$1.object({
9680
9710
  * vocabulary; HOW it persists (the ConversationStore adapter,
9681
9711
  * localStorage vs. a fetch endpoint) is codegen's call, never
9682
9712
  * vocabulary here (C-3 — no transport-layer vocabulary). */
9683
- conversations: z$1.literal(true).optional()
9713
+ conversations: z$1.literal(true).optional(),
9714
+ /** Role-scoped default action bars, threaded onto ChatThread's
9715
+ * `userActions`/`assistantActions` props (B-3/B-7b). Ordered
9716
+ * arrays; enum ids ONLY, read off the ONE const
9717
+ * (CHAT_MESSAGE_ACTIONS — B-6): a CustomAction is an id the APP
9718
+ * must handle, a construct has no app code, so emitting one is a
9719
+ * dead affordance. Duplicates within one array rejected below
9720
+ * (superRefine, the slots pattern). min(1): an empty list IS the
9721
+ * absent key. */
9722
+ messageActions: z$1.object({
9723
+ user: z$1.array(z$1.enum(CHAT_MESSAGE_ACTIONS)).min(1).optional(),
9724
+ assistant: z$1.array(z$1.enum(CHAT_MESSAGE_ACTIONS)).min(1).optional()
9725
+ }).strict().optional(),
9726
+ /** The citations STRIP (the `part="citations"` row consecutive
9727
+ * `source` parts already collapse into — message.tsx). `strip:
9728
+ * false` hides it (emits ChatThread's `hideSources`); `strip:
9729
+ * true` or the key absent emits NOTHING — the row already renders,
9730
+ * the kit default IS the on state, the same anchored-on-the-
9731
+ * default convention as `reasoning: 'full'` (B-4). */
9732
+ sources: z$1.object({
9733
+ strip: z$1.boolean().optional()
9734
+ }).strict().optional()
9684
9735
  }).strict().optional(),
9685
9736
  /** Named generative-UI card definitions the model can emit as tool calls,
9686
9737
  * rendered in the thread. Top-level (not a capability): a card is
@@ -9727,109 +9778,245 @@ const ConstructSchema = z$1.object({
9727
9778
  /** Seed Dock's open state at mount. Uncontrolled — never steals focus (Dock's
9728
9779
  * own focus contract, dock.tsx). Omitted keeps Dock's own default (closed). */
9729
9780
  defaultOpen: z$1.boolean().optional()
9781
+ }).strict().optional(),
9782
+ /** Layout-scoped aside geometry, `layout: 'aside'` only (superRefine
9783
+ * below, mirroring `widget`'s scoping exactly). `position` picks the
9784
+ * docked inline edge (default 'end', today's hardcoded behavior);
9785
+ * `width` overrides codegen's 380px default. `width` is construct-
9786
+ * authored untrusted text: it lands as a JSON.stringify'd VALUE inside
9787
+ * the emitted Solid `style={{ }}` object — property assignment, the
9788
+ * same no-CSS-text-interpolation guarantee as setProperty — never
9789
+ * concatenated into a CSS string (B-2). */
9790
+ aside: z$1.object({
9791
+ position: z$1.enum(["start", "end"]).optional(),
9792
+ width: z$1.string().min(1).optional()
9793
+ }).strict().optional(),
9794
+ /** Construct-wide shell chrome (10a): both members reuse REAL kit
9795
+ * pieces — command.tsx's CommandList behind a codegen-emitted overlay
9796
+ * (opened on Mod+K; entries DERIVE from what this construct enables —
9797
+ * menu-honesty against dead entries), and the documented Dropdown+
9798
+ * Avatar user-menu recipe. `name`/`plan` are construct-authored
9799
+ * untrusted text, JSON.stringify'd at emit like every sibling.
9800
+ * `commandPalette` is presence-only `z.literal(true)`, matching
9801
+ * `conversations`' pattern. */
9802
+ shell: z$1.object({
9803
+ commandPalette: z$1.literal(true).optional(),
9804
+ userMenu: z$1.object({
9805
+ name: z$1.string().min(1),
9806
+ plan: z$1.string().min(1).optional()
9807
+ }).strict().optional()
9808
+ }).strict().optional(),
9809
+ /** Composer chrome — NOT a capability: chrome on the medium, like
9810
+ * `header` (B-5). `triggers` maps onto ChatThread's real, shipped
9811
+ * `triggers` prop: `slash` → `{ char: '/', kind: 'command', items }`,
9812
+ * `mention` → `{ char: '@', kind: 'mention', items }` at emit. */
9813
+ composer: z$1.object({
9814
+ triggers: z$1.object({
9815
+ slash: z$1.array(TriggerEntrySchema).min(1).optional(),
9816
+ mention: z$1.array(TriggerEntrySchema).min(1).optional()
9817
+ }).strict().optional()
9730
9818
  }).strict().optional()
9731
9819
  }).strict().superRefine((construct, ctx) => {
9732
- if (construct.slots) {
9733
- const seen = /* @__PURE__ */ new Set();
9734
- construct.slots.forEach((name, i) => {
9735
- if (seen.has(name)) {
9820
+ for (const rule of CROSS_FIELD_RULES) rule.check(construct, ctx);
9821
+ });
9822
+ const CROSS_FIELD_RULES = [
9823
+ {
9824
+ id: "slots-unique",
9825
+ paths: ["slots"],
9826
+ check: (construct, ctx) => {
9827
+ if (!construct.slots) return;
9828
+ const seen = /* @__PURE__ */ new Set();
9829
+ construct.slots.forEach((name, i) => {
9830
+ if (seen.has(name)) {
9831
+ ctx.addIssue({
9832
+ code: z$1.ZodIssueCode.custom,
9833
+ path: ["slots", i],
9834
+ message: `duplicate slot name "${name}"`
9835
+ });
9836
+ }
9837
+ seen.add(name);
9838
+ });
9839
+ }
9840
+ },
9841
+ {
9842
+ id: "custom-layout-needs-slots",
9843
+ paths: ["layout", "slots"],
9844
+ check: (construct, ctx) => {
9845
+ if (construct.layout === "custom" && (!construct.slots || construct.slots.length === 0)) {
9736
9846
  ctx.addIssue({
9737
9847
  code: z$1.ZodIssueCode.custom,
9738
- path: ["slots", i],
9739
- message: `duplicate slot name "${name}"`
9848
+ path: ["slots"],
9849
+ message: '"custom" layout requires at least one declared slot custom IS the slots grain'
9740
9850
  });
9741
9851
  }
9742
- seen.add(name);
9743
- });
9744
- }
9745
- if (construct.layout === "custom" && (!construct.slots || construct.slots.length === 0)) {
9746
- ctx.addIssue({
9747
- code: z$1.ZodIssueCode.custom,
9748
- path: ["slots"],
9749
- message: '"custom" layout requires at least one declared slot — custom IS the slots grain'
9750
- });
9751
- }
9752
- if (construct.layout === "split" && construct.slots) {
9753
- const i = construct.slots.indexOf("pane");
9754
- if (i !== -1) {
9755
- ctx.addIssue({
9756
- code: z$1.ZodIssueCode.custom,
9757
- path: ["slots", i],
9758
- message: `"pane" collides with the "split" layout's own fixed <slot name="pane"> — choose a different slot name`
9759
- });
9760
9852
  }
9761
- }
9762
- if (construct.widget && construct.layout !== "widget") {
9763
- ctx.addIssue({
9764
- code: z$1.ZodIssueCode.custom,
9765
- path: ["widget"],
9766
- message: '"widget" is only valid on layout: "widget"'
9767
- });
9768
- }
9769
- if (construct.widget?.launcherIcon && !isSafeUrl(construct.widget.launcherIcon)) {
9770
- ctx.addIssue({
9771
- code: z$1.ZodIssueCode.custom,
9772
- path: ["widget", "launcherIcon"],
9773
- message: "launcherIcon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
9774
- });
9775
- }
9776
- if (construct.empty?.icon && !isSafeUrl(construct.empty.icon)) {
9777
- ctx.addIssue({
9778
- code: z$1.ZodIssueCode.custom,
9779
- path: ["empty", "icon"],
9780
- message: "icon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
9781
- });
9782
- }
9783
- const reasoning = construct.capabilities?.reasoning;
9784
- const reasoningOpen = construct.capabilities?.reasoningOpen;
9785
- if (reasoningOpen !== void 0 && (reasoning === "compact" || reasoning === "off")) {
9786
- ctx.addIssue({
9787
- code: z$1.ZodIssueCode.custom,
9788
- path: ["capabilities", "reasoningOpen"],
9789
- message: '"reasoningOpen" only applies when reasoning is "full" or omitted — "compact"/"off" have no disclosure to open'
9790
- });
9791
- }
9792
- const history = construct.capabilities?.history;
9793
- if (construct.capabilities?.conversations && (!history || history.persistence === "none")) {
9794
- ctx.addIssue({
9795
- code: z$1.ZodIssueCode.custom,
9796
- path: ["capabilities", "conversations"],
9797
- message: '"conversations" requires capabilities.history.persistence to be "local" or "endpoint" — a conversation list needs somewhere to persist conversations'
9798
- });
9799
- }
9800
- const URL_SHAPED = /^[a-zA-Z][a-zA-Z0-9+.-]*:|^\/\//;
9801
- for (const [i, link] of (construct.home?.links ?? []).entries()) {
9802
- if (link.href && !isSafeUrl(link.href)) {
9803
- ctx.addIssue({
9804
- code: z$1.ZodIssueCode.custom,
9805
- path: ["home", "links", i, "href"],
9806
- message: "href must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
9807
- });
9853
+ },
9854
+ {
9855
+ id: "split-pane-slot-collision",
9856
+ paths: ["layout", "slots"],
9857
+ check: (construct, ctx) => {
9858
+ if (construct.layout === "split" && construct.slots) {
9859
+ const i = construct.slots.indexOf("pane");
9860
+ if (i !== -1) {
9861
+ ctx.addIssue({
9862
+ code: z$1.ZodIssueCode.custom,
9863
+ path: ["slots", i],
9864
+ message: `"pane" collides with the "split" layout's own fixed <slot name="pane"> — choose a different slot name`
9865
+ });
9866
+ }
9867
+ }
9808
9868
  }
9809
- if (link.icon && URL_SHAPED.test(link.icon) && !isSafeUrl(link.icon)) {
9810
- ctx.addIssue({
9811
- code: z$1.ZodIssueCode.custom,
9812
- path: ["home", "links", i, "icon"],
9813
- message: "icon must be a kit icon name or an http(s)/relative URL — no javascript:/data: schemes"
9814
- });
9869
+ },
9870
+ {
9871
+ id: "widget-layout-scope",
9872
+ paths: ["layout", "widget"],
9873
+ check: (construct, ctx) => {
9874
+ if (construct.widget && construct.layout !== "widget") {
9875
+ ctx.addIssue({
9876
+ code: z$1.ZodIssueCode.custom,
9877
+ path: ["widget"],
9878
+ message: '"widget" is only valid on layout: "widget"'
9879
+ });
9880
+ }
9881
+ }
9882
+ },
9883
+ {
9884
+ id: "aside-layout-scope",
9885
+ paths: ["layout", "aside"],
9886
+ check: (construct, ctx) => {
9887
+ if (construct.aside && construct.layout !== "aside") {
9888
+ ctx.addIssue({
9889
+ code: z$1.ZodIssueCode.custom,
9890
+ path: ["aside"],
9891
+ message: '"aside" is only valid on layout: "aside"'
9892
+ });
9893
+ }
9894
+ }
9895
+ },
9896
+ {
9897
+ id: "message-actions-unique",
9898
+ paths: ["capabilities.messageActions.user", "capabilities.messageActions.assistant"],
9899
+ check: (construct, ctx) => {
9900
+ const messageActions = construct.capabilities?.messageActions;
9901
+ if (!messageActions) return;
9902
+ for (const role of ["user", "assistant"]) {
9903
+ const list = messageActions[role];
9904
+ if (!list) continue;
9905
+ const seen = /* @__PURE__ */ new Set();
9906
+ list.forEach((id, i) => {
9907
+ if (seen.has(id)) {
9908
+ ctx.addIssue({
9909
+ code: z$1.ZodIssueCode.custom,
9910
+ path: ["capabilities", "messageActions", role, i],
9911
+ message: `duplicate action id "${id}"`
9912
+ });
9913
+ }
9914
+ seen.add(id);
9915
+ });
9916
+ }
9917
+ }
9918
+ },
9919
+ {
9920
+ id: "launcher-icon-url",
9921
+ paths: ["widget.launcherIcon"],
9922
+ check: (construct, ctx) => {
9923
+ if (construct.widget?.launcherIcon && !isSafeUrl(construct.widget.launcherIcon)) {
9924
+ ctx.addIssue({
9925
+ code: z$1.ZodIssueCode.custom,
9926
+ path: ["widget", "launcherIcon"],
9927
+ message: "launcherIcon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
9928
+ });
9929
+ }
9930
+ }
9931
+ },
9932
+ {
9933
+ id: "empty-icon-url",
9934
+ paths: ["empty.icon"],
9935
+ check: (construct, ctx) => {
9936
+ if (construct.empty?.icon && !isSafeUrl(construct.empty.icon)) {
9937
+ ctx.addIssue({
9938
+ code: z$1.ZodIssueCode.custom,
9939
+ path: ["empty", "icon"],
9940
+ message: "icon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
9941
+ });
9942
+ }
9943
+ }
9944
+ },
9945
+ {
9946
+ id: "reasoning-open-scope",
9947
+ paths: ["capabilities.reasoning", "capabilities.reasoningOpen"],
9948
+ check: (construct, ctx) => {
9949
+ const reasoning = construct.capabilities?.reasoning;
9950
+ const reasoningOpen = construct.capabilities?.reasoningOpen;
9951
+ if (reasoningOpen !== void 0 && (reasoning === "compact" || reasoning === "off")) {
9952
+ ctx.addIssue({
9953
+ code: z$1.ZodIssueCode.custom,
9954
+ path: ["capabilities", "reasoningOpen"],
9955
+ message: '"reasoningOpen" only applies when reasoning is "full" or omitted — "compact"/"off" have no disclosure to open'
9956
+ });
9957
+ }
9958
+ }
9959
+ },
9960
+ {
9961
+ id: "conversations-need-history",
9962
+ paths: ["capabilities.conversations", "capabilities.history"],
9963
+ check: (construct, ctx) => {
9964
+ const history = construct.capabilities?.history;
9965
+ if (construct.capabilities?.conversations && (!history || history.persistence === "none")) {
9966
+ ctx.addIssue({
9967
+ code: z$1.ZodIssueCode.custom,
9968
+ path: ["capabilities", "conversations"],
9969
+ message: '"conversations" requires capabilities.history.persistence to be "local" or "endpoint" — a conversation list needs somewhere to persist conversations'
9970
+ });
9971
+ }
9972
+ }
9973
+ },
9974
+ {
9975
+ id: "home-link-urls",
9976
+ paths: ["home.links"],
9977
+ check: (construct, ctx) => {
9978
+ const URL_SHAPED = /^[a-zA-Z][a-zA-Z0-9+.-]*:|^\/\//;
9979
+ for (const [i, link] of (construct.home?.links ?? []).entries()) {
9980
+ if (link.href && !isSafeUrl(link.href)) {
9981
+ ctx.addIssue({
9982
+ code: z$1.ZodIssueCode.custom,
9983
+ path: ["home", "links", i, "href"],
9984
+ message: "href must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
9985
+ });
9986
+ }
9987
+ if (link.icon && URL_SHAPED.test(link.icon) && !isSafeUrl(link.icon)) {
9988
+ ctx.addIssue({
9989
+ code: z$1.ZodIssueCode.custom,
9990
+ path: ["home", "links", i, "icon"],
9991
+ message: "icon must be a kit icon name or an http(s)/relative URL — no javascript:/data: schemes"
9992
+ });
9993
+ }
9994
+ }
9995
+ }
9996
+ },
9997
+ {
9998
+ id: "history-endpoint-url",
9999
+ paths: ["capabilities.history.persistence", "capabilities.history.url"],
10000
+ check: (construct, ctx) => {
10001
+ const history = construct.capabilities?.history;
10002
+ if (!history) return;
10003
+ if (history.persistence === "endpoint" && !history.url) {
10004
+ ctx.addIssue({
10005
+ code: z$1.ZodIssueCode.custom,
10006
+ path: ["capabilities", "history", "url"],
10007
+ message: '"endpoint" persistence requires a url'
10008
+ });
10009
+ }
10010
+ if (history.persistence !== "endpoint" && history.url !== void 0) {
10011
+ ctx.addIssue({
10012
+ code: z$1.ZodIssueCode.custom,
10013
+ path: ["capabilities", "history", "url"],
10014
+ message: 'url is only valid with "endpoint" persistence'
10015
+ });
10016
+ }
9815
10017
  }
9816
10018
  }
9817
- if (!history) return;
9818
- if (history.persistence === "endpoint" && !history.url) {
9819
- ctx.addIssue({
9820
- code: z$1.ZodIssueCode.custom,
9821
- path: ["capabilities", "history", "url"],
9822
- message: '"endpoint" persistence requires a url'
9823
- });
9824
- }
9825
- if (history.persistence !== "endpoint" && history.url !== void 0) {
9826
- ctx.addIssue({
9827
- code: z$1.ZodIssueCode.custom,
9828
- path: ["capabilities", "history", "url"],
9829
- message: 'url is only valid with "endpoint" persistence'
9830
- });
9831
- }
9832
- });
10019
+ ];
9833
10020
  function validateConstruct(input) {
9834
10021
  const parsed = ConstructSchema.safeParse(input);
9835
10022
  if (parsed.success) return { ok: true, construct: parsed.data };
@@ -9851,25 +10038,314 @@ function validateConstruct(input) {
9851
10038
  })
9852
10039
  };
9853
10040
  }
10041
+ const IDENTITY = { id: "identity", paths: ["name"] };
10042
+ const THEME = { id: "theme", paths: ["theme.accent", "theme.mode", "theme.unreadColor"] };
10043
+ const HEADER = { id: "header", paths: ["header.title"] };
10044
+ const HEADER_CHROME = {
10045
+ id: "header",
10046
+ paths: ["header.title", "header.themeToggle", "header.actions"]
10047
+ };
10048
+ const EMPTY = { id: "empty", paths: ["empty.title", "empty.description", "empty.icon"] };
10049
+ const HOME = { id: "home", paths: ["home"] };
10050
+ const CAPABILITIES = {
10051
+ id: "capabilities",
10052
+ paths: [
10053
+ "capabilities.starters",
10054
+ "capabilities.attachments",
10055
+ "capabilities.history",
10056
+ "capabilities.conversations",
10057
+ "capabilities.reasoning",
10058
+ "capabilities.reasoningOpen"
10059
+ ]
10060
+ };
10061
+ const MESSAGE_ACTIONS = {
10062
+ id: "messageActions",
10063
+ paths: ["capabilities.messageActions.user", "capabilities.messageActions.assistant"]
10064
+ };
10065
+ const SOURCES = { id: "sources", paths: ["capabilities.sources.strip"] };
10066
+ const WIDGET_CHROME = {
10067
+ id: "widget",
10068
+ paths: ["widget.position", "widget.launcherIcon", "widget.defaultOpen"]
10069
+ };
10070
+ const ASIDE = { id: "aside", paths: ["aside.position", "aside.width"] };
10071
+ const COMPOSER_TRIGGERS = {
10072
+ id: "composerTriggers",
10073
+ paths: ["composer.triggers.slash", "composer.triggers.mention"]
10074
+ };
10075
+ const SHELL = { id: "shell", paths: ["shell.commandPalette", "shell.userMenu"] };
10076
+ const CARDS = { id: "cards", paths: ["cards"] };
10077
+ const PROVIDER = { id: "provider", paths: ["provider"] };
10078
+ const widgetStarter = {
10079
+ $schema: CONSTRUCT_SCHEMA_URL,
10080
+ name: "support-widget",
10081
+ layout: "widget",
10082
+ provider: { mode: "mock" },
10083
+ header: { title: "Support" },
10084
+ theme: { unreadColor: "#38BDF8", mode: "system" },
10085
+ empty: {
10086
+ title: "Hi, we're here to help",
10087
+ description: "Ask us about orders, refunds, and more."
10088
+ },
10089
+ home: {
10090
+ greeting: { title: "How can we help? 👋", subtitle: "Orders, refunds, anything." },
10091
+ recentConversation: true,
10092
+ links: [
10093
+ { label: "Help center", href: "https://ui.kitn.ai", description: "Guides and FAQs", icon: "book-open" }
10094
+ ]
10095
+ },
10096
+ // States the kit default loudly (the anchored-on-the-default convention,
10097
+ // same as research's sources.strip) so the template's chrome fact is
10098
+ // visible/editable in its own JSON.
10099
+ widget: { position: "bottom-end" },
10100
+ capabilities: {
10101
+ starters: ["Where's my order?", "Request a refund"],
10102
+ attachments: { accept: ["image/*", "application/pdf"] },
10103
+ history: { persistence: "local" },
10104
+ conversations: true
10105
+ }
10106
+ };
10107
+ const inAppAssistantStarter = {
10108
+ $schema: CONSTRUCT_SCHEMA_URL,
10109
+ name: "in-app-assistant",
10110
+ layout: "aside",
10111
+ provider: { mode: "mock" },
10112
+ header: { title: "Assistant" },
10113
+ // Dark-by-default (owner ruling, dark round): every buildable starter
10114
+ // EXCEPT widget ships mode: 'dark' — widget is embedded in a host site and
10115
+ // follows IT, not its own preference (T-3: this is registry data, not a
10116
+ // schema default; 'system' stays the schema's own default for anyone
10117
+ // hand-authoring a construct).
10118
+ theme: { accent: "#0ea5e9", mode: "dark" },
10119
+ // codegen's own defaults, stated so the geometry is visible/editable.
10120
+ aside: { position: "end", width: "380px" },
10121
+ capabilities: {
10122
+ starters: ["Deploy payments to production", "Check the canary status"],
10123
+ attachments: { accept: ["image/*", "application/pdf"] },
10124
+ history: { persistence: "local" }
10125
+ }
10126
+ };
10127
+ const assistantStarter = {
10128
+ $schema: CONSTRUCT_SCHEMA_URL,
10129
+ name: "daily-assistant",
10130
+ layout: "fullscreen",
10131
+ provider: { mode: "mock" },
10132
+ header: { title: "Assistant" },
10133
+ // Dark-by-default (owner ruling, dark round) — see inAppAssistantStarter's
10134
+ // note above.
10135
+ theme: { accent: "#7c3aed", mode: "dark" },
10136
+ empty: {
10137
+ title: "What can I help with?",
10138
+ description: "Ask anything, or start from a suggestion below."
10139
+ },
10140
+ capabilities: {
10141
+ starters: ["Draft the Q3 board update", "Summarize a document", "Compare two options"],
10142
+ attachments: { accept: ["image/*", "application/pdf"] },
10143
+ history: { persistence: "local" },
10144
+ conversations: true
10145
+ }
10146
+ };
10147
+ const researchStarter = {
10148
+ $schema: CONSTRUCT_SCHEMA_URL,
10149
+ name: "research-assistant",
10150
+ layout: "fullscreen",
10151
+ provider: { mode: "mock" },
10152
+ header: { title: "Research" },
10153
+ // Dark-by-default (owner ruling, dark round) — see inAppAssistantStarter's
10154
+ // note above.
10155
+ theme: { accent: "#0f766e", mode: "dark" },
10156
+ capabilities: {
10157
+ starters: ["How does the wire adapter work?", "What are message parts?"],
10158
+ attachments: { accept: ["application/pdf"] },
10159
+ history: { persistence: "local" },
10160
+ // The template's defining fact, stated even though it matches the emit
10161
+ // default (B-4): the row already renders; strip: true is the visible
10162
+ // switch this template exists around.
10163
+ sources: { strip: true },
10164
+ // "assistant-style actions" (B-13) — the owner's A3 default matrix
10165
+ // (builder-message-actions.tsx: user Edit on; assistant
10166
+ // Copy/Like/Dislike on).
10167
+ messageActions: { user: ["edit"], assistant: ["copy", "like", "dislike"] }
10168
+ }
10169
+ };
10170
+ const workspaceTriggers = {
10171
+ triggers: {
10172
+ slash: [
10173
+ { id: "summarize", label: "summarize", description: "Summarize the thread so far" },
10174
+ { id: "translate", label: "translate", description: "Translate the last message" }
10175
+ ],
10176
+ mention: [
10177
+ { id: "researcher", label: "researcher", description: "Hands off to the research agent" },
10178
+ { id: "coder", label: "coder", description: "Hands off to the coding agent" }
10179
+ ]
10180
+ }
10181
+ };
10182
+ const workspaceBase = {
10183
+ $schema: CONSTRUCT_SCHEMA_URL,
10184
+ name: "build-workspace",
10185
+ layout: "split",
10186
+ provider: { mode: "mock" },
10187
+ header: {
10188
+ title: "Workspace",
10189
+ themeToggle: true,
10190
+ // The story's Share/Deploy rows, mapped onto the kit Button's real
10191
+ // variant names (B-6a's enum): 'secondary' → outline, 'primary' → default.
10192
+ actions: [
10193
+ { label: "Share", variant: "outline" },
10194
+ { label: "Deploy", variant: "default" }
10195
+ ]
10196
+ },
10197
+ // Dark-by-default (owner ruling, dark round) — see inAppAssistantStarter's
10198
+ // note above. Both variants below spread ...workspaceBase without
10199
+ // overriding theme, so this covers artifactPreview and appPreview too.
10200
+ theme: { accent: "#ea580c", mode: "dark" },
10201
+ shell: { commandPalette: true, userMenu: { name: "Ada", plan: "Pro" } },
10202
+ composer: workspaceTriggers,
10203
+ capabilities: {
10204
+ starters: ["Build a pricing table", "Add a dark mode toggle"],
10205
+ attachments: { accept: ["image/*"] },
10206
+ history: { persistence: "local" }
10207
+ }
10208
+ };
10209
+ const workspaceArtifactPreview = {
10210
+ ...workspaceBase,
10211
+ name: "artifact-workspace"
10212
+ };
10213
+ const workspaceAppPreview = {
10214
+ ...workspaceBase,
10215
+ name: "app-workspace",
10216
+ capabilities: {
10217
+ ...workspaceBase.capabilities,
10218
+ starters: ["Build a landing page for a coffee shop", "Make the hero work on mobile"]
10219
+ }
10220
+ };
10221
+ const TEMPLATES = [
10222
+ {
10223
+ id: "widget",
10224
+ name: "Support widget",
10225
+ description: "A floating chat that lives in the corner of your site.",
10226
+ availability: "buildable",
10227
+ starter: widgetStarter,
10228
+ controls: [IDENTITY, THEME, HEADER, HOME, CAPABILITIES, WIDGET_CHROME, PROVIDER]
10229
+ },
10230
+ {
10231
+ id: "inAppAssistant",
10232
+ name: "In-app assistant",
10233
+ description: "An assistant docked inside your existing app.",
10234
+ availability: "buildable",
10235
+ starter: inAppAssistantStarter,
10236
+ // Design-parity fix wave (2026-08-29 audit): the story's In-app
10237
+ // assistant panel shows Composer triggers, Message actions and a
10238
+ // read-only Cards list, none of which were wired here even though the
10239
+ // vocabulary and the panel machinery both already exist (proven working
10240
+ // on Workspace/Assistant/Research). Voice/Reveal-mode/Rail-placement
10241
+ // stay absent — those are the story's own labeled preview-only,
10242
+ // T-5-deferred fields with no construct vocabulary at all.
10243
+ controls: [IDENTITY, THEME, HEADER, ASIDE, COMPOSER_TRIGGERS, CAPABILITIES, MESSAGE_ACTIONS, CARDS, PROVIDER]
10244
+ },
10245
+ {
10246
+ id: "assistant",
10247
+ name: "Assistant",
10248
+ description: "A full-page assistant with a history of past conversations.",
10249
+ availability: "buildable",
10250
+ starter: assistantStarter,
10251
+ // Design-parity fix wave: the story's Assistant panel shows an App
10252
+ // chrome / Shell section (Command palette toggle, User menu) that
10253
+ // wasn't wired here — same already-working vocabulary as Workspace's
10254
+ // SHELL section.
10255
+ controls: [IDENTITY, THEME, HEADER, SHELL, EMPTY, CAPABILITIES, MESSAGE_ACTIONS, PROVIDER]
10256
+ },
10257
+ {
10258
+ id: "research",
10259
+ name: "Research",
10260
+ description: "Search-first answers with cited sources.",
10261
+ availability: "buildable",
10262
+ starter: researchStarter,
10263
+ controls: [IDENTITY, THEME, HEADER, CAPABILITIES, SOURCES, MESSAGE_ACTIONS, PROVIDER]
10264
+ },
10265
+ {
10266
+ id: "workspace",
10267
+ name: "Workspace",
10268
+ description: "Chat drives a live work surface: previews, code, and artifacts build beside the conversation.",
10269
+ availability: "buildable",
10270
+ starter: workspaceBase,
10271
+ variants: [
10272
+ {
10273
+ id: "artifactPreview",
10274
+ name: "Artifact preview beside chat",
10275
+ description: "A code or rendered-output pane grows beside the conversation as you build.",
10276
+ starter: workspaceArtifactPreview
10277
+ },
10278
+ {
10279
+ id: "appPreview",
10280
+ name: "App preview with device toggles",
10281
+ description: "A full browser-chrome preview of the running app, with desktop, tablet, and mobile views.",
10282
+ starter: workspaceAppPreview
10283
+ }
10284
+ ],
10285
+ controls: [
10286
+ IDENTITY,
10287
+ THEME,
10288
+ HEADER_CHROME,
10289
+ SHELL,
10290
+ COMPOSER_TRIGGERS,
10291
+ CAPABILITIES,
10292
+ MESSAGE_ACTIONS,
10293
+ PROVIDER
10294
+ ]
10295
+ },
10296
+ {
10297
+ id: "voice",
10298
+ name: "Voice",
10299
+ description: "A voice-first assistant you talk to, push-to-talk and all.",
10300
+ availability: "story-only"
10301
+ }
10302
+ ];
10303
+ function buildableTemplates() {
10304
+ return TEMPLATES.filter((t) => t.availability === "buildable");
10305
+ }
9854
10306
  const inputSchema = z$1.object({
9855
10307
  intent: z$1.string().min(1).optional().describe("What the author wants, in their words. Alone: returns a starter construct."),
9856
10308
  construct: z$1.record(z$1.string(), z$1.unknown()).optional().describe("The FULL construct JSON for this turn. The harness owns the file; send all of it every turn.")
9857
10309
  }).strict();
10310
+ const INTENT_PATTERNS = [
10311
+ { id: "widget", re: /\b(?:widget|embed|bubble|corner|launcher)\b/i },
10312
+ { id: "research", re: /\b(?:research|search|cite|citation|sources?)\b/i },
10313
+ { id: "workspace", re: /\b(?:workspace|split|pane|artifact|side.?by.?side|preview)\b/i },
10314
+ { id: "inAppAssistant", re: /\b(?:aside|dock|in.?app|copilot|console|sidebar)\b/i },
10315
+ { id: "assistant", re: /\b(?:assistant|chat\s*(?:app|bot)|full.?screen)\b/i }
10316
+ ];
9858
10317
  function starterFor(intent) {
9859
- const impliesWidget = /widget|embed|bubble|corner|launcher/i.test(intent);
10318
+ const templates = buildableTemplates();
9860
10319
  const name = "my-chat";
10320
+ const tagQuestion = `What should the element tag be? (kebab-case, e.g. "acme-support"; using "${name}" until you say)`;
10321
+ const match = INTENT_PATTERNS.find((p) => p.re.test(intent));
10322
+ const template = match ? templates.find((t) => t.id === match.id) : void 0;
10323
+ if (!template) {
10324
+ return {
10325
+ construct: {
10326
+ $schema: CONSTRUCT_SCHEMA_URL,
10327
+ name,
10328
+ layout: "fullscreen",
10329
+ provider: { mode: "mock" }
10330
+ },
10331
+ stated: [
10332
+ "no template implied — starting from a bare fullscreen construct. Templates available:",
10333
+ ...templates.map((t) => ` · ${t.id} — ${t.name}: ${t.description}`)
10334
+ ],
10335
+ questions: [
10336
+ tagQuestion,
10337
+ "Which template fits? (name one of the ids above, or keep the bare construct)"
10338
+ ]
10339
+ };
10340
+ }
10341
+ const construct = structuredClone(template.starter);
10342
+ construct.name = name;
9861
10343
  return {
9862
- construct: {
9863
- $schema: CONSTRUCT_SCHEMA_URL,
9864
- name,
9865
- layout: "widget",
9866
- provider: { mode: "mock" }
9867
- },
9868
- stated: impliesWidget ? ["layout: widget (implied by your request)"] : [],
9869
- questions: [
9870
- `What should the element tag be? (kebab-case, e.g. "acme-support"; using "${name}" until you say)`,
9871
- ...impliesWidget ? [] : ["Layout: widget, fullscreen, aside, split, or custom?"]
9872
- ]
10344
+ construct,
10345
+ stated: [
10346
+ `template: ${template.id} (${template.name}) — implied by your request; every field below is yours to edit`
10347
+ ],
10348
+ questions: [tagQuestion]
9873
10349
  };
9874
10350
  }
9875
10351
  const constructTool = {