@theokit/ui 0.13.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 (715) hide show
  1. package/CHANGELOG.md +1325 -0
  2. package/DESIGN.md +456 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +38 -0
  5. package/README.md +467 -0
  6. package/dist/chunk-27ENTTY7.js +146 -0
  7. package/dist/chunk-27ENTTY7.js.map +1 -0
  8. package/dist/chunk-2H6TQELG.js +33 -0
  9. package/dist/chunk-2H6TQELG.js.map +1 -0
  10. package/dist/chunk-2L6MRJD4.js +120 -0
  11. package/dist/chunk-2L6MRJD4.js.map +1 -0
  12. package/dist/chunk-2Y5V2PAL.js +80 -0
  13. package/dist/chunk-2Y5V2PAL.js.map +1 -0
  14. package/dist/chunk-34NAFDVL.js +46 -0
  15. package/dist/chunk-34NAFDVL.js.map +1 -0
  16. package/dist/chunk-36KJGXEK.js +112 -0
  17. package/dist/chunk-36KJGXEK.js.map +1 -0
  18. package/dist/chunk-3BMYYNN6.js +124 -0
  19. package/dist/chunk-3BMYYNN6.js.map +1 -0
  20. package/dist/chunk-3OHV7EEI.js +34 -0
  21. package/dist/chunk-3OHV7EEI.js.map +1 -0
  22. package/dist/chunk-3QKTS6F5.js +88 -0
  23. package/dist/chunk-3QKTS6F5.js.map +1 -0
  24. package/dist/chunk-3TBXLYNM.js +42 -0
  25. package/dist/chunk-3TBXLYNM.js.map +1 -0
  26. package/dist/chunk-4AM2HSXU.js +67 -0
  27. package/dist/chunk-4AM2HSXU.js.map +1 -0
  28. package/dist/chunk-4BCGKM65.js +44 -0
  29. package/dist/chunk-4BCGKM65.js.map +1 -0
  30. package/dist/chunk-4D3JILQX.js +145 -0
  31. package/dist/chunk-4D3JILQX.js.map +1 -0
  32. package/dist/chunk-4EJU2GBG.js +48 -0
  33. package/dist/chunk-4EJU2GBG.js.map +1 -0
  34. package/dist/chunk-4WKO3G5C.js +110 -0
  35. package/dist/chunk-4WKO3G5C.js.map +1 -0
  36. package/dist/chunk-53XPKI7Q.js +97 -0
  37. package/dist/chunk-53XPKI7Q.js.map +1 -0
  38. package/dist/chunk-55TDVDPG.js +58 -0
  39. package/dist/chunk-55TDVDPG.js.map +1 -0
  40. package/dist/chunk-56BJLFW7.js +26 -0
  41. package/dist/chunk-56BJLFW7.js.map +1 -0
  42. package/dist/chunk-5HOQLE6Y.js +35 -0
  43. package/dist/chunk-5HOQLE6Y.js.map +1 -0
  44. package/dist/chunk-5TY3NYF5.js +144 -0
  45. package/dist/chunk-5TY3NYF5.js.map +1 -0
  46. package/dist/chunk-5VOSCJKQ.js +92 -0
  47. package/dist/chunk-5VOSCJKQ.js.map +1 -0
  48. package/dist/chunk-65NVO6TK.js +171 -0
  49. package/dist/chunk-65NVO6TK.js.map +1 -0
  50. package/dist/chunk-6A5TPCKP.js +64 -0
  51. package/dist/chunk-6A5TPCKP.js.map +1 -0
  52. package/dist/chunk-6CO4LEXZ.js +41 -0
  53. package/dist/chunk-6CO4LEXZ.js.map +1 -0
  54. package/dist/chunk-6FVUPNPG.js +56 -0
  55. package/dist/chunk-6FVUPNPG.js.map +1 -0
  56. package/dist/chunk-76YWTIWK.js +106 -0
  57. package/dist/chunk-76YWTIWK.js.map +1 -0
  58. package/dist/chunk-7EI7424P.js +78 -0
  59. package/dist/chunk-7EI7424P.js.map +1 -0
  60. package/dist/chunk-AHTVYOPQ.js +26 -0
  61. package/dist/chunk-AHTVYOPQ.js.map +1 -0
  62. package/dist/chunk-AJTJNHKK.js +85 -0
  63. package/dist/chunk-AJTJNHKK.js.map +1 -0
  64. package/dist/chunk-AMT3CPMC.js +155 -0
  65. package/dist/chunk-AMT3CPMC.js.map +1 -0
  66. package/dist/chunk-AX5EH73R.js +59 -0
  67. package/dist/chunk-AX5EH73R.js.map +1 -0
  68. package/dist/chunk-B3VAJSZ2.js +35 -0
  69. package/dist/chunk-B3VAJSZ2.js.map +1 -0
  70. package/dist/chunk-B4CQMQ64.js +25 -0
  71. package/dist/chunk-B4CQMQ64.js.map +1 -0
  72. package/dist/chunk-BMRZXT5T.js +115 -0
  73. package/dist/chunk-BMRZXT5T.js.map +1 -0
  74. package/dist/chunk-BYZ6OFH4.js +73 -0
  75. package/dist/chunk-BYZ6OFH4.js.map +1 -0
  76. package/dist/chunk-C55VUQ7N.js +156 -0
  77. package/dist/chunk-C55VUQ7N.js.map +1 -0
  78. package/dist/chunk-D4GEAV4C.js +91 -0
  79. package/dist/chunk-D4GEAV4C.js.map +1 -0
  80. package/dist/chunk-DC43CHAM.js +152 -0
  81. package/dist/chunk-DC43CHAM.js.map +1 -0
  82. package/dist/chunk-DKCRLN35.js +92 -0
  83. package/dist/chunk-DKCRLN35.js.map +1 -0
  84. package/dist/chunk-DN5BUDBI.js +86 -0
  85. package/dist/chunk-DN5BUDBI.js.map +1 -0
  86. package/dist/chunk-DOLKDYMS.js +88 -0
  87. package/dist/chunk-DOLKDYMS.js.map +1 -0
  88. package/dist/chunk-DW34WXCG.js +28 -0
  89. package/dist/chunk-DW34WXCG.js.map +1 -0
  90. package/dist/chunk-DZAAKHGZ.js +135 -0
  91. package/dist/chunk-DZAAKHGZ.js.map +1 -0
  92. package/dist/chunk-E4IRSSHO.js +116 -0
  93. package/dist/chunk-E4IRSSHO.js.map +1 -0
  94. package/dist/chunk-E67WQXBV.js +104 -0
  95. package/dist/chunk-E67WQXBV.js.map +1 -0
  96. package/dist/chunk-EG6IHP3H.js +128 -0
  97. package/dist/chunk-EG6IHP3H.js.map +1 -0
  98. package/dist/chunk-EO7LOXG2.js +82 -0
  99. package/dist/chunk-EO7LOXG2.js.map +1 -0
  100. package/dist/chunk-EWDN56AS.js +24 -0
  101. package/dist/chunk-EWDN56AS.js.map +1 -0
  102. package/dist/chunk-F5P5P2SC.js +141 -0
  103. package/dist/chunk-F5P5P2SC.js.map +1 -0
  104. package/dist/chunk-FAWPRZTM.js +79 -0
  105. package/dist/chunk-FAWPRZTM.js.map +1 -0
  106. package/dist/chunk-FGYJ2WPX.js +36 -0
  107. package/dist/chunk-FGYJ2WPX.js.map +1 -0
  108. package/dist/chunk-GBG3I5I5.js +46 -0
  109. package/dist/chunk-GBG3I5I5.js.map +1 -0
  110. package/dist/chunk-GDMCDW66.js +19 -0
  111. package/dist/chunk-GDMCDW66.js.map +1 -0
  112. package/dist/chunk-H6HSQCOW.js +80 -0
  113. package/dist/chunk-H6HSQCOW.js.map +1 -0
  114. package/dist/chunk-HDM4RCIF.js +111 -0
  115. package/dist/chunk-HDM4RCIF.js.map +1 -0
  116. package/dist/chunk-HNTOGGVD.js +77 -0
  117. package/dist/chunk-HNTOGGVD.js.map +1 -0
  118. package/dist/chunk-HQW2ABO4.js +28 -0
  119. package/dist/chunk-HQW2ABO4.js.map +1 -0
  120. package/dist/chunk-HRDRGZ2Y.js +76 -0
  121. package/dist/chunk-HRDRGZ2Y.js.map +1 -0
  122. package/dist/chunk-HUOVA7SF.js +83 -0
  123. package/dist/chunk-HUOVA7SF.js.map +1 -0
  124. package/dist/chunk-ITA3SNOR.js +133 -0
  125. package/dist/chunk-ITA3SNOR.js.map +1 -0
  126. package/dist/chunk-IYNUPG2G.js +61 -0
  127. package/dist/chunk-IYNUPG2G.js.map +1 -0
  128. package/dist/chunk-JJ65ZI4P.js +199 -0
  129. package/dist/chunk-JJ65ZI4P.js.map +1 -0
  130. package/dist/chunk-JRBGZ6NI.js +106 -0
  131. package/dist/chunk-JRBGZ6NI.js.map +1 -0
  132. package/dist/chunk-K45OO62F.js +108 -0
  133. package/dist/chunk-K45OO62F.js.map +1 -0
  134. package/dist/chunk-KDTKA667.js +67 -0
  135. package/dist/chunk-KDTKA667.js.map +1 -0
  136. package/dist/chunk-KI7KZBSN.js +142 -0
  137. package/dist/chunk-KI7KZBSN.js.map +1 -0
  138. package/dist/chunk-KOJ7XOPZ.js +87 -0
  139. package/dist/chunk-KOJ7XOPZ.js.map +1 -0
  140. package/dist/chunk-KQTHJ22B.js +82 -0
  141. package/dist/chunk-KQTHJ22B.js.map +1 -0
  142. package/dist/chunk-KRC43RZR.js +77 -0
  143. package/dist/chunk-KRC43RZR.js.map +1 -0
  144. package/dist/chunk-LJQOEGQ2.js +116 -0
  145. package/dist/chunk-LJQOEGQ2.js.map +1 -0
  146. package/dist/chunk-LKRNUSKZ.js +149 -0
  147. package/dist/chunk-LKRNUSKZ.js.map +1 -0
  148. package/dist/chunk-LLL7QQ52.js +76 -0
  149. package/dist/chunk-LLL7QQ52.js.map +1 -0
  150. package/dist/chunk-LQ4B5X4Y.js +56 -0
  151. package/dist/chunk-LQ4B5X4Y.js.map +1 -0
  152. package/dist/chunk-M3FSLEHQ.js +76 -0
  153. package/dist/chunk-M3FSLEHQ.js.map +1 -0
  154. package/dist/chunk-M5G3O6H6.js +57 -0
  155. package/dist/chunk-M5G3O6H6.js.map +1 -0
  156. package/dist/chunk-M6JIC5PU.js +81 -0
  157. package/dist/chunk-M6JIC5PU.js.map +1 -0
  158. package/dist/chunk-N2HJ3SLS.js +186 -0
  159. package/dist/chunk-N2HJ3SLS.js.map +1 -0
  160. package/dist/chunk-NGZWBFTP.js +45 -0
  161. package/dist/chunk-NGZWBFTP.js.map +1 -0
  162. package/dist/chunk-OAKCXT35.js +34 -0
  163. package/dist/chunk-OAKCXT35.js.map +1 -0
  164. package/dist/chunk-OSD3U3HT.js +54 -0
  165. package/dist/chunk-OSD3U3HT.js.map +1 -0
  166. package/dist/chunk-OUXESQ2R.js +42 -0
  167. package/dist/chunk-OUXESQ2R.js.map +1 -0
  168. package/dist/chunk-OY2LJHMJ.js +43 -0
  169. package/dist/chunk-OY2LJHMJ.js.map +1 -0
  170. package/dist/chunk-OYEZR4CN.js +221 -0
  171. package/dist/chunk-OYEZR4CN.js.map +1 -0
  172. package/dist/chunk-P57HUMAE.js +66 -0
  173. package/dist/chunk-P57HUMAE.js.map +1 -0
  174. package/dist/chunk-P6Y2PI6L.js +82 -0
  175. package/dist/chunk-P6Y2PI6L.js.map +1 -0
  176. package/dist/chunk-PA7TDXUQ.js +51 -0
  177. package/dist/chunk-PA7TDXUQ.js.map +1 -0
  178. package/dist/chunk-PPBGGNPV.js +112 -0
  179. package/dist/chunk-PPBGGNPV.js.map +1 -0
  180. package/dist/chunk-PRH4HKND.js +48 -0
  181. package/dist/chunk-PRH4HKND.js.map +1 -0
  182. package/dist/chunk-PSPAZJUQ.js +32 -0
  183. package/dist/chunk-PSPAZJUQ.js.map +1 -0
  184. package/dist/chunk-Q5G5CGZ2.js +170 -0
  185. package/dist/chunk-Q5G5CGZ2.js.map +1 -0
  186. package/dist/chunk-QDAF3LP7.js +89 -0
  187. package/dist/chunk-QDAF3LP7.js.map +1 -0
  188. package/dist/chunk-QGVIGNJ3.js +37 -0
  189. package/dist/chunk-QGVIGNJ3.js.map +1 -0
  190. package/dist/chunk-QNUITYSY.js +68 -0
  191. package/dist/chunk-QNUITYSY.js.map +1 -0
  192. package/dist/chunk-QSWVN3RT.js +116 -0
  193. package/dist/chunk-QSWVN3RT.js.map +1 -0
  194. package/dist/chunk-QTLQZ7OJ.js +110 -0
  195. package/dist/chunk-QTLQZ7OJ.js.map +1 -0
  196. package/dist/chunk-QYAMLIG2.js +84 -0
  197. package/dist/chunk-QYAMLIG2.js.map +1 -0
  198. package/dist/chunk-REILH4XF.js +128 -0
  199. package/dist/chunk-REILH4XF.js.map +1 -0
  200. package/dist/chunk-S6SSK6QX.js +80 -0
  201. package/dist/chunk-S6SSK6QX.js.map +1 -0
  202. package/dist/chunk-SA7ED3PN.js +68 -0
  203. package/dist/chunk-SA7ED3PN.js.map +1 -0
  204. package/dist/chunk-SIJOEM4N.js +55 -0
  205. package/dist/chunk-SIJOEM4N.js.map +1 -0
  206. package/dist/chunk-SLOKAAH2.js +70 -0
  207. package/dist/chunk-SLOKAAH2.js.map +1 -0
  208. package/dist/chunk-TR6NPSMX.js +85 -0
  209. package/dist/chunk-TR6NPSMX.js.map +1 -0
  210. package/dist/chunk-TSZ5DEAT.js +106 -0
  211. package/dist/chunk-TSZ5DEAT.js.map +1 -0
  212. package/dist/chunk-TUNVF45W.js +127 -0
  213. package/dist/chunk-TUNVF45W.js.map +1 -0
  214. package/dist/chunk-TXOBNSQ5.js +63 -0
  215. package/dist/chunk-TXOBNSQ5.js.map +1 -0
  216. package/dist/chunk-U44DRLMM.js +88 -0
  217. package/dist/chunk-U44DRLMM.js.map +1 -0
  218. package/dist/chunk-U4THNRV5.js +114 -0
  219. package/dist/chunk-U4THNRV5.js.map +1 -0
  220. package/dist/chunk-UAZOFC4W.js +72 -0
  221. package/dist/chunk-UAZOFC4W.js.map +1 -0
  222. package/dist/chunk-UGKI466V.js +12 -0
  223. package/dist/chunk-UGKI466V.js.map +1 -0
  224. package/dist/chunk-VM4RMQQN.js +11 -0
  225. package/dist/chunk-VM4RMQQN.js.map +1 -0
  226. package/dist/chunk-VQ37VLAS.js +54 -0
  227. package/dist/chunk-VQ37VLAS.js.map +1 -0
  228. package/dist/chunk-VT7VSYH5.js +73 -0
  229. package/dist/chunk-VT7VSYH5.js.map +1 -0
  230. package/dist/chunk-VTIRUCLZ.js +57 -0
  231. package/dist/chunk-VTIRUCLZ.js.map +1 -0
  232. package/dist/chunk-VVBAEYKI.js +202 -0
  233. package/dist/chunk-VVBAEYKI.js.map +1 -0
  234. package/dist/chunk-WHFIQUCC.js +120 -0
  235. package/dist/chunk-WHFIQUCC.js.map +1 -0
  236. package/dist/chunk-WPSESV5Z.js +74 -0
  237. package/dist/chunk-WPSESV5Z.js.map +1 -0
  238. package/dist/chunk-WXEXCHEN.js +51 -0
  239. package/dist/chunk-WXEXCHEN.js.map +1 -0
  240. package/dist/chunk-X2DDPD3D.js +113 -0
  241. package/dist/chunk-X2DDPD3D.js.map +1 -0
  242. package/dist/chunk-X7VIMKLD.js +127 -0
  243. package/dist/chunk-X7VIMKLD.js.map +1 -0
  244. package/dist/chunk-XJ3EG6XY.js +30 -0
  245. package/dist/chunk-XJ3EG6XY.js.map +1 -0
  246. package/dist/chunk-XOT5HWSF.js +23 -0
  247. package/dist/chunk-XOT5HWSF.js.map +1 -0
  248. package/dist/chunk-Y72IP43U.js +117 -0
  249. package/dist/chunk-Y72IP43U.js.map +1 -0
  250. package/dist/chunk-YD6FLXBV.js +61 -0
  251. package/dist/chunk-YD6FLXBV.js.map +1 -0
  252. package/dist/chunk-YEQQGYYO.js +1022 -0
  253. package/dist/chunk-YEQQGYYO.js.map +1 -0
  254. package/dist/chunk-YYW6AEIT.js +46 -0
  255. package/dist/chunk-YYW6AEIT.js.map +1 -0
  256. package/dist/chunk-ZEVGXKRU.js +104 -0
  257. package/dist/chunk-ZEVGXKRU.js.map +1 -0
  258. package/dist/chunk-ZKSMMLDP.js +74 -0
  259. package/dist/chunk-ZKSMMLDP.js.map +1 -0
  260. package/dist/chunk-ZU6IM6PK.js +101 -0
  261. package/dist/chunk-ZU6IM6PK.js.map +1 -0
  262. package/dist/chunk-ZUS5KZGO.js +714 -0
  263. package/dist/chunk-ZUS5KZGO.js.map +1 -0
  264. package/dist/chunk-ZVS2GOT2.js +58 -0
  265. package/dist/chunk-ZVS2GOT2.js.map +1 -0
  266. package/dist/chunk-ZXPDS6DH.js +3 -0
  267. package/dist/chunk-ZXPDS6DH.js.map +1 -0
  268. package/dist/chunk-ZZQQJX5Z.js +173 -0
  269. package/dist/chunk-ZZQQJX5Z.js.map +1 -0
  270. package/dist/components.css +2 -0
  271. package/dist/composites/account-menu/index.js +6 -0
  272. package/dist/composites/account-menu/index.js.map +1 -0
  273. package/dist/composites/agent-composer/index.js +7 -0
  274. package/dist/composites/agent-composer/index.js.map +1 -0
  275. package/dist/composites/agent-editor/index.js +10 -0
  276. package/dist/composites/agent-editor/index.js.map +1 -0
  277. package/dist/composites/agent-stream/index.js +12 -0
  278. package/dist/composites/agent-stream/index.js.map +1 -0
  279. package/dist/composites/agent-timeline/index.js +5 -0
  280. package/dist/composites/agent-timeline/index.js.map +1 -0
  281. package/dist/composites/approval-card/index.js +5 -0
  282. package/dist/composites/approval-card/index.js.map +1 -0
  283. package/dist/composites/chat-composer/index.js +6 -0
  284. package/dist/composites/chat-composer/index.js.map +1 -0
  285. package/dist/composites/chat-message/index.js +6 -0
  286. package/dist/composites/chat-message/index.js.map +1 -0
  287. package/dist/composites/code-block/index.js +5 -0
  288. package/dist/composites/code-block/index.js.map +1 -0
  289. package/dist/composites/command-palette/index.js +5 -0
  290. package/dist/composites/command-palette/index.js.map +1 -0
  291. package/dist/composites/confirm-dialog/index.js +7 -0
  292. package/dist/composites/confirm-dialog/index.js.map +1 -0
  293. package/dist/composites/cron-jobs-list/index.js +5 -0
  294. package/dist/composites/cron-jobs-list/index.js.map +1 -0
  295. package/dist/composites/data-table/index.js +10 -0
  296. package/dist/composites/data-table/index.js.map +1 -0
  297. package/dist/composites/deployment-row/index.js +5 -0
  298. package/dist/composites/deployment-row/index.js.map +1 -0
  299. package/dist/composites/domain-config/index.js +7 -0
  300. package/dist/composites/domain-config/index.js.map +1 -0
  301. package/dist/composites/env-var-editor/index.js +7 -0
  302. package/dist/composites/env-var-editor/index.js.map +1 -0
  303. package/dist/composites/mcp-server-list/index.js +5 -0
  304. package/dist/composites/mcp-server-list/index.js.map +1 -0
  305. package/dist/composites/page-shell/index.js +7 -0
  306. package/dist/composites/page-shell/index.js.map +1 -0
  307. package/dist/composites/permission-modal/index.js +6 -0
  308. package/dist/composites/permission-modal/index.js.map +1 -0
  309. package/dist/composites/preview-env-card/index.js +6 -0
  310. package/dist/composites/preview-env-card/index.js.map +1 -0
  311. package/dist/composites/preview-panel/index.js +5 -0
  312. package/dist/composites/preview-panel/index.js.map +1 -0
  313. package/dist/composites/project-card/index.js +6 -0
  314. package/dist/composites/project-card/index.js.map +1 -0
  315. package/dist/composites/rollback-ui/index.js +6 -0
  316. package/dist/composites/rollback-ui/index.js.map +1 -0
  317. package/dist/composites/rule-editor/index.js +11 -0
  318. package/dist/composites/rule-editor/index.js.map +1 -0
  319. package/dist/composites/skill-editor/index.js +11 -0
  320. package/dist/composites/skill-editor/index.js.map +1 -0
  321. package/dist/composites/skills-list/index.js +5 -0
  322. package/dist/composites/skills-list/index.js.map +1 -0
  323. package/dist/composites/stability-bundle-viewer/index.js +4 -0
  324. package/dist/composites/stability-bundle-viewer/index.js.map +1 -0
  325. package/dist/composites/task-header/index.js +5 -0
  326. package/dist/composites/task-header/index.js.map +1 -0
  327. package/dist/composites/usage-meter/index.js +5 -0
  328. package/dist/composites/usage-meter/index.js.map +1 -0
  329. package/dist/fonts/LICENSE-GEIST.txt +92 -0
  330. package/dist/fonts/geist-400.woff2 +0 -0
  331. package/dist/fonts/geist-500.woff2 +0 -0
  332. package/dist/fonts/geist-600.woff2 +0 -0
  333. package/dist/fonts/geist-mono-400.woff2 +0 -0
  334. package/dist/fonts/geist-mono-500.woff2 +0 -0
  335. package/dist/fonts/geist-mono-600.woff2 +0 -0
  336. package/dist/fonts-cdn.css +28 -0
  337. package/dist/fonts.css +75 -0
  338. package/dist/index.d.ts +4621 -0
  339. package/dist/index.js +1338 -0
  340. package/dist/index.js.map +1 -0
  341. package/dist/plugin-D5xmXqYb.d.ts +172 -0
  342. package/dist/preset-v3-legacy.d.ts +35 -0
  343. package/dist/preset-v3-legacy.js +159 -0
  344. package/dist/preset-v3-legacy.js.map +1 -0
  345. package/dist/preset.css +27 -0
  346. package/dist/primitives/action-bar/index.js +4 -0
  347. package/dist/primitives/action-bar/index.js.map +1 -0
  348. package/dist/primitives/agent-error-card/index.js +5 -0
  349. package/dist/primitives/agent-error-card/index.js.map +1 -0
  350. package/dist/primitives/agent-event/index.js +4 -0
  351. package/dist/primitives/agent-event/index.js.map +1 -0
  352. package/dist/primitives/agent-handoff/index.js +4 -0
  353. package/dist/primitives/agent-handoff/index.js.map +1 -0
  354. package/dist/primitives/agent-profile/index.js +4 -0
  355. package/dist/primitives/agent-profile/index.js.map +1 -0
  356. package/dist/primitives/agent-starting-state/index.js +5 -0
  357. package/dist/primitives/agent-starting-state/index.js.map +1 -0
  358. package/dist/primitives/agent-streaming/index.js +5 -0
  359. package/dist/primitives/agent-streaming/index.js.map +1 -0
  360. package/dist/primitives/alert/index.js +4 -0
  361. package/dist/primitives/alert/index.js.map +1 -0
  362. package/dist/primitives/artifact-preview/index.js +4 -0
  363. package/dist/primitives/artifact-preview/index.js.map +1 -0
  364. package/dist/primitives/attachment-chip/index.js +4 -0
  365. package/dist/primitives/attachment-chip/index.js.map +1 -0
  366. package/dist/primitives/audit-log-entry/index.js +4 -0
  367. package/dist/primitives/audit-log-entry/index.js.map +1 -0
  368. package/dist/primitives/auto-compact-notice/index.js +5 -0
  369. package/dist/primitives/auto-compact-notice/index.js.map +1 -0
  370. package/dist/primitives/avatar/index.js +4 -0
  371. package/dist/primitives/avatar/index.js.map +1 -0
  372. package/dist/primitives/badge/index.js +4 -0
  373. package/dist/primitives/badge/index.js.map +1 -0
  374. package/dist/primitives/branch-indicator/index.js +4 -0
  375. package/dist/primitives/branch-indicator/index.js.map +1 -0
  376. package/dist/primitives/browser-controls/index.js +4 -0
  377. package/dist/primitives/browser-controls/index.js.map +1 -0
  378. package/dist/primitives/build-log-stream/index.js +5 -0
  379. package/dist/primitives/build-log-stream/index.js.map +1 -0
  380. package/dist/primitives/button/index.js +4 -0
  381. package/dist/primitives/button/index.js.map +1 -0
  382. package/dist/primitives/capability-indicator/index.js +4 -0
  383. package/dist/primitives/capability-indicator/index.js.map +1 -0
  384. package/dist/primitives/card/index.js +4 -0
  385. package/dist/primitives/card/index.js.map +1 -0
  386. package/dist/primitives/channel-card/index.js +4 -0
  387. package/dist/primitives/channel-card/index.js.map +1 -0
  388. package/dist/primitives/chat-thread/index.js +5 -0
  389. package/dist/primitives/chat-thread/index.js.map +1 -0
  390. package/dist/primitives/checkbox/index.js +4 -0
  391. package/dist/primitives/checkbox/index.js.map +1 -0
  392. package/dist/primitives/context-card/index.js +4 -0
  393. package/dist/primitives/context-card/index.js.map +1 -0
  394. package/dist/primitives/context-window-bar/index.js +4 -0
  395. package/dist/primitives/context-window-bar/index.js.map +1 -0
  396. package/dist/primitives/copy-button/index.js +4 -0
  397. package/dist/primitives/copy-button/index.js.map +1 -0
  398. package/dist/primitives/cost-meter/index.js +4 -0
  399. package/dist/primitives/cost-meter/index.js.map +1 -0
  400. package/dist/primitives/created-files-card/index.js +4 -0
  401. package/dist/primitives/created-files-card/index.js.map +1 -0
  402. package/dist/primitives/cron-job-card/index.js +4 -0
  403. package/dist/primitives/cron-job-card/index.js.map +1 -0
  404. package/dist/primitives/danger-zone/index.js +4 -0
  405. package/dist/primitives/danger-zone/index.js.map +1 -0
  406. package/dist/primitives/dialog/index.js +4 -0
  407. package/dist/primitives/dialog/index.js.map +1 -0
  408. package/dist/primitives/diff-viewer/index.js +4 -0
  409. package/dist/primitives/diff-viewer/index.js.map +1 -0
  410. package/dist/primitives/dropdown-menu/index.js +4 -0
  411. package/dist/primitives/dropdown-menu/index.js.map +1 -0
  412. package/dist/primitives/empty-state/index.js +4 -0
  413. package/dist/primitives/empty-state/index.js.map +1 -0
  414. package/dist/primitives/export-chat-dialog/index.js +4 -0
  415. package/dist/primitives/export-chat-dialog/index.js.map +1 -0
  416. package/dist/primitives/folder-context-card/index.js +4 -0
  417. package/dist/primitives/folder-context-card/index.js.map +1 -0
  418. package/dist/primitives/folder-selector/index.js +4 -0
  419. package/dist/primitives/folder-selector/index.js.map +1 -0
  420. package/dist/primitives/form-field/index.js +4 -0
  421. package/dist/primitives/form-field/index.js.map +1 -0
  422. package/dist/primitives/gateway-status-indicator/index.js +4 -0
  423. package/dist/primitives/gateway-status-indicator/index.js.map +1 -0
  424. package/dist/primitives/hook-config/index.js +4 -0
  425. package/dist/primitives/hook-config/index.js.map +1 -0
  426. package/dist/primitives/hook-event-log/index.js +4 -0
  427. package/dist/primitives/hook-event-log/index.js.map +1 -0
  428. package/dist/primitives/input/index.js +4 -0
  429. package/dist/primitives/input/index.js.map +1 -0
  430. package/dist/primitives/intent-selector/index.js +4 -0
  431. package/dist/primitives/intent-selector/index.js.map +1 -0
  432. package/dist/primitives/label/index.js +4 -0
  433. package/dist/primitives/label/index.js.map +1 -0
  434. package/dist/primitives/lane-board/index.js +4 -0
  435. package/dist/primitives/lane-board/index.js.map +1 -0
  436. package/dist/primitives/login-split/index.js +4 -0
  437. package/dist/primitives/login-split/index.js.map +1 -0
  438. package/dist/primitives/mcp-server-card/index.js +4 -0
  439. package/dist/primitives/mcp-server-card/index.js.map +1 -0
  440. package/dist/primitives/memory-editor/index.js +4 -0
  441. package/dist/primitives/memory-editor/index.js.map +1 -0
  442. package/dist/primitives/mention-menu/index.js +4 -0
  443. package/dist/primitives/mention-menu/index.js.map +1 -0
  444. package/dist/primitives/metrics-panel/index.js +4 -0
  445. package/dist/primitives/metrics-panel/index.js.map +1 -0
  446. package/dist/primitives/model-card/index.js +4 -0
  447. package/dist/primitives/model-card/index.js.map +1 -0
  448. package/dist/primitives/model-selector/index.js +4 -0
  449. package/dist/primitives/model-selector/index.js.map +1 -0
  450. package/dist/primitives/pagination/index.js +4 -0
  451. package/dist/primitives/pagination/index.js.map +1 -0
  452. package/dist/primitives/permission-matrix/index.js +4 -0
  453. package/dist/primitives/permission-matrix/index.js.map +1 -0
  454. package/dist/primitives/pin-input/index.js +4 -0
  455. package/dist/primitives/pin-input/index.js.map +1 -0
  456. package/dist/primitives/plan-badge/index.js +4 -0
  457. package/dist/primitives/plan-badge/index.js.map +1 -0
  458. package/dist/primitives/progress/index.js +4 -0
  459. package/dist/primitives/progress/index.js.map +1 -0
  460. package/dist/primitives/progress-checklist/index.js +4 -0
  461. package/dist/primitives/progress-checklist/index.js.map +1 -0
  462. package/dist/primitives/project-switcher/index.js +4 -0
  463. package/dist/primitives/project-switcher/index.js.map +1 -0
  464. package/dist/primitives/quick-action-chips/index.js +4 -0
  465. package/dist/primitives/quick-action-chips/index.js.map +1 -0
  466. package/dist/primitives/radio-group/index.js +4 -0
  467. package/dist/primitives/radio-group/index.js.map +1 -0
  468. package/dist/primitives/recent-folders-list/index.js +4 -0
  469. package/dist/primitives/recent-folders-list/index.js.map +1 -0
  470. package/dist/primitives/rule-card/index.js +4 -0
  471. package/dist/primitives/rule-card/index.js.map +1 -0
  472. package/dist/primitives/run-stats/index.js +4 -0
  473. package/dist/primitives/run-stats/index.js.map +1 -0
  474. package/dist/primitives/run-status-pill/index.js +4 -0
  475. package/dist/primitives/run-status-pill/index.js.map +1 -0
  476. package/dist/primitives/running-tasks-panel/index.js +4 -0
  477. package/dist/primitives/running-tasks-panel/index.js.map +1 -0
  478. package/dist/primitives/scroll-area/index.js +4 -0
  479. package/dist/primitives/scroll-area/index.js.map +1 -0
  480. package/dist/primitives/select/index.js +4 -0
  481. package/dist/primitives/select/index.js.map +1 -0
  482. package/dist/primitives/session-list-item/index.js +4 -0
  483. package/dist/primitives/session-list-item/index.js.map +1 -0
  484. package/dist/primitives/session-timeline/index.js +4 -0
  485. package/dist/primitives/session-timeline/index.js.map +1 -0
  486. package/dist/primitives/sheet/index.js +4 -0
  487. package/dist/primitives/sheet/index.js.map +1 -0
  488. package/dist/primitives/sidebar/index.js +4 -0
  489. package/dist/primitives/sidebar/index.js.map +1 -0
  490. package/dist/primitives/skeleton/index.js +5 -0
  491. package/dist/primitives/skeleton/index.js.map +1 -0
  492. package/dist/primitives/skill-card/index.js +4 -0
  493. package/dist/primitives/skill-card/index.js.map +1 -0
  494. package/dist/primitives/social-auth-row/index.js +4 -0
  495. package/dist/primitives/social-auth-row/index.js.map +1 -0
  496. package/dist/primitives/stat-tile/index.js +4 -0
  497. package/dist/primitives/stat-tile/index.js.map +1 -0
  498. package/dist/primitives/status-dot/index.js +4 -0
  499. package/dist/primitives/status-dot/index.js.map +1 -0
  500. package/dist/primitives/steps-rail/index.js +4 -0
  501. package/dist/primitives/steps-rail/index.js.map +1 -0
  502. package/dist/primitives/sub-agent-dispatch/index.js +4 -0
  503. package/dist/primitives/sub-agent-dispatch/index.js.map +1 -0
  504. package/dist/primitives/switch/index.js +4 -0
  505. package/dist/primitives/switch/index.js.map +1 -0
  506. package/dist/primitives/system-prompt-editor/index.js +4 -0
  507. package/dist/primitives/system-prompt-editor/index.js.map +1 -0
  508. package/dist/primitives/table/index.js +4 -0
  509. package/dist/primitives/table/index.js.map +1 -0
  510. package/dist/primitives/tabs/index.js +4 -0
  511. package/dist/primitives/tabs/index.js.map +1 -0
  512. package/dist/primitives/task-plan/index.js +4 -0
  513. package/dist/primitives/task-plan/index.js.map +1 -0
  514. package/dist/primitives/terminal-panel/index.js +5 -0
  515. package/dist/primitives/terminal-panel/index.js.map +1 -0
  516. package/dist/primitives/textarea/index.js +4 -0
  517. package/dist/primitives/textarea/index.js.map +1 -0
  518. package/dist/primitives/thinking-level-selector/index.js +4 -0
  519. package/dist/primitives/thinking-level-selector/index.js.map +1 -0
  520. package/dist/primitives/timestamp/index.js +4 -0
  521. package/dist/primitives/timestamp/index.js.map +1 -0
  522. package/dist/primitives/toast/index.js +4 -0
  523. package/dist/primitives/toast/index.js.map +1 -0
  524. package/dist/primitives/token-usage-chart/index.js +4 -0
  525. package/dist/primitives/token-usage-chart/index.js.map +1 -0
  526. package/dist/primitives/tool-call/index.js +4 -0
  527. package/dist/primitives/tool-call/index.js.map +1 -0
  528. package/dist/primitives/tool-call-card/index.js +4 -0
  529. package/dist/primitives/tool-call-card/index.js.map +1 -0
  530. package/dist/primitives/tool-result/index.js +4 -0
  531. package/dist/primitives/tool-result/index.js.map +1 -0
  532. package/dist/primitives/tools-list/index.js +4 -0
  533. package/dist/primitives/tools-list/index.js.map +1 -0
  534. package/dist/primitives/tooltip/index.js +4 -0
  535. package/dist/primitives/tooltip/index.js.map +1 -0
  536. package/dist/primitives/topnav/index.js +4 -0
  537. package/dist/primitives/topnav/index.js.map +1 -0
  538. package/dist/primitives/update-banner/index.js +4 -0
  539. package/dist/primitives/update-banner/index.js.map +1 -0
  540. package/dist/slide/index.d.ts +212 -0
  541. package/dist/slide/index.js +3 -0
  542. package/dist/slide/index.js.map +1 -0
  543. package/dist/slide/plugins/emoji/index.d.ts +29 -0
  544. package/dist/slide/plugins/emoji/index.js +157 -0
  545. package/dist/slide/plugins/emoji/index.js.map +1 -0
  546. package/dist/slide/plugins/math/index.d.ts +13 -0
  547. package/dist/slide/plugins/math/index.js +145 -0
  548. package/dist/slide/plugins/math/index.js.map +1 -0
  549. package/dist/slide/plugins/mermaid/index.d.ts +55 -0
  550. package/dist/slide/plugins/mermaid/index.js +218 -0
  551. package/dist/slide/plugins/mermaid/index.js.map +1 -0
  552. package/dist/slide/plugins/shiki/index.d.ts +18 -0
  553. package/dist/slide/plugins/shiki/index.js +87 -0
  554. package/dist/slide/plugins/shiki/index.js.map +1 -0
  555. package/dist/slide/themes/default.css +256 -0
  556. package/dist/slide/themes/layouts.css +143 -0
  557. package/dist/slide/themes/violet-forge.css +256 -0
  558. package/dist/slide-deck/index.css +52 -0
  559. package/dist/slide-deck/index.css.map +1 -0
  560. package/dist/slide-deck/index.d.ts +377 -0
  561. package/dist/slide-deck/index.js +1111 -0
  562. package/dist/slide-deck/index.js.map +1 -0
  563. package/dist/styles-v3-legacy.css +88 -0
  564. package/dist/styles.css +137 -0
  565. package/dist/tokens-v4.css +187 -0
  566. package/dist/tokens.css +230 -0
  567. package/dist/vite-plugin.d.ts +29 -0
  568. package/dist/vite-plugin.js +76 -0
  569. package/dist/vite-plugin.js.map +1 -0
  570. package/dist/whiteboard/index.d.ts +258 -0
  571. package/dist/whiteboard/index.js +738 -0
  572. package/dist/whiteboard/index.js.map +1 -0
  573. package/llms.txt +273 -0
  574. package/package.json +800 -0
  575. package/registry/index.json +856 -0
  576. package/registry/r/account-menu.json +24 -0
  577. package/registry/r/action-bar.json +22 -0
  578. package/registry/r/agent-composer.json +22 -0
  579. package/registry/r/agent-editor.json +27 -0
  580. package/registry/r/agent-error-card.json +22 -0
  581. package/registry/r/agent-event.json +24 -0
  582. package/registry/r/agent-handoff.json +22 -0
  583. package/registry/r/agent-profile.json +23 -0
  584. package/registry/r/agent-starting-state.json +22 -0
  585. package/registry/r/agent-stream.json +27 -0
  586. package/registry/r/agent-streaming.json +22 -0
  587. package/registry/r/agent-timeline.json +22 -0
  588. package/registry/r/agent-types.json +15 -0
  589. package/registry/r/alert.json +22 -0
  590. package/registry/r/approval-card.json +25 -0
  591. package/registry/r/artifact-preview.json +22 -0
  592. package/registry/r/attachment-chip.json +24 -0
  593. package/registry/r/audit-log-entry.json +23 -0
  594. package/registry/r/auto-compact-notice.json +22 -0
  595. package/registry/r/avatar.json +23 -0
  596. package/registry/r/badge.json +22 -0
  597. package/registry/r/browser-controls.json +22 -0
  598. package/registry/r/build-log-stream.json +19 -0
  599. package/registry/r/button.json +23 -0
  600. package/registry/r/capability-indicator.json +23 -0
  601. package/registry/r/card.json +22 -0
  602. package/registry/r/chat-composer.json +23 -0
  603. package/registry/r/chat-message.json +129 -0
  604. package/registry/r/chat-thread.json +20 -0
  605. package/registry/r/chat-types.json +15 -0
  606. package/registry/r/checkbox.json +24 -0
  607. package/registry/r/cn.json +19 -0
  608. package/registry/r/code-block.json +21 -0
  609. package/registry/r/command-palette.json +25 -0
  610. package/registry/r/confirm-dialog.json +25 -0
  611. package/registry/r/context-card.json +23 -0
  612. package/registry/r/context-window-bar.json +20 -0
  613. package/registry/r/copy-button.json +22 -0
  614. package/registry/r/cost-meter.json +22 -0
  615. package/registry/r/created-files-card.json +23 -0
  616. package/registry/r/cron-job-card.json +22 -0
  617. package/registry/r/cron-jobs-list.json +23 -0
  618. package/registry/r/danger-zone.json +20 -0
  619. package/registry/r/data-table.json +27 -0
  620. package/registry/r/deployment-row.json +23 -0
  621. package/registry/r/dialog.json +23 -0
  622. package/registry/r/diff-viewer.json +20 -0
  623. package/registry/r/domain-config.json +25 -0
  624. package/registry/r/dropdown-menu.json +23 -0
  625. package/registry/r/empty-state.json +20 -0
  626. package/registry/r/env-var-editor.json +25 -0
  627. package/registry/r/folder-context-card.json +23 -0
  628. package/registry/r/folder-selector.json +22 -0
  629. package/registry/r/form-field.json +23 -0
  630. package/registry/r/hook-config.json +22 -0
  631. package/registry/r/hook-event-log.json +22 -0
  632. package/registry/r/input.json +22 -0
  633. package/registry/r/intent-selector.json +24 -0
  634. package/registry/r/label.json +22 -0
  635. package/registry/r/lane-board.json +20 -0
  636. package/registry/r/live-region-context.json +16 -0
  637. package/registry/r/login-split.json +20 -0
  638. package/registry/r/mcp-server-card.json +22 -0
  639. package/registry/r/mcp-server-list.json +23 -0
  640. package/registry/r/memory-editor.json +23 -0
  641. package/registry/r/mention-menu.json +23 -0
  642. package/registry/r/metrics-panel.json +22 -0
  643. package/registry/r/mode-types.json +15 -0
  644. package/registry/r/model-card.json +23 -0
  645. package/registry/r/model-selector.json +23 -0
  646. package/registry/r/page-shell.json +25 -0
  647. package/registry/r/pagination.json +22 -0
  648. package/registry/r/permission-matrix.json +22 -0
  649. package/registry/r/permission-modal.json +24 -0
  650. package/registry/r/permission-types.json +15 -0
  651. package/registry/r/pin-input.json +20 -0
  652. package/registry/r/plan-badge.json +20 -0
  653. package/registry/r/preview-env-card.json +25 -0
  654. package/registry/r/preview-panel.json +21 -0
  655. package/registry/r/progress-checklist.json +23 -0
  656. package/registry/r/progress.json +20 -0
  657. package/registry/r/project-card.json +25 -0
  658. package/registry/r/project-switcher.json +22 -0
  659. package/registry/r/quick-action-chips.json +21 -0
  660. package/registry/r/radio-group.json +23 -0
  661. package/registry/r/recent-folders-list.json +22 -0
  662. package/registry/r/rollback-ui.json +24 -0
  663. package/registry/r/rule-card.json +23 -0
  664. package/registry/r/rule-editor.json +28 -0
  665. package/registry/r/rule-types.json +18 -0
  666. package/registry/r/run-stats.json +22 -0
  667. package/registry/r/running-tasks-panel.json +22 -0
  668. package/registry/r/safe-href.json +16 -0
  669. package/registry/r/scroll-area.json +22 -0
  670. package/registry/r/select.json +24 -0
  671. package/registry/r/session-list-item.json +20 -0
  672. package/registry/r/session-timeline.json +22 -0
  673. package/registry/r/sheet.json +24 -0
  674. package/registry/r/sidebar.json +19 -0
  675. package/registry/r/skeleton.json +19 -0
  676. package/registry/r/skill-card.json +24 -0
  677. package/registry/r/skill-editor.json +28 -0
  678. package/registry/r/skills-list.json +23 -0
  679. package/registry/r/slide-deck.json +130 -0
  680. package/registry/r/slide-plugin-emoji.json +28 -0
  681. package/registry/r/slide-plugin-math.json +24 -0
  682. package/registry/r/slide-plugin-mermaid.json +23 -0
  683. package/registry/r/slide-plugin-shiki.json +23 -0
  684. package/registry/r/slide.json +123 -0
  685. package/registry/r/social-auth-row.json +21 -0
  686. package/registry/r/stat-tile.json +22 -0
  687. package/registry/r/status-dot.json +20 -0
  688. package/registry/r/steps-rail.json +20 -0
  689. package/registry/r/sub-agent-dispatch.json +22 -0
  690. package/registry/r/switch.json +23 -0
  691. package/registry/r/system-prompt-editor.json +22 -0
  692. package/registry/r/table.json +22 -0
  693. package/registry/r/tabs.json +22 -0
  694. package/registry/r/tailwind-preset.json +19 -0
  695. package/registry/r/task-header.json +24 -0
  696. package/registry/r/task-plan.json +22 -0
  697. package/registry/r/task-types.json +15 -0
  698. package/registry/r/terminal-panel.json +22 -0
  699. package/registry/r/textarea.json +22 -0
  700. package/registry/r/theme-provider.json +59 -0
  701. package/registry/r/theme-script.json +18 -0
  702. package/registry/r/theo-ui-provider.json +20 -0
  703. package/registry/r/timestamp.json +20 -0
  704. package/registry/r/toast.json +30 -0
  705. package/registry/r/token-usage-chart.json +20 -0
  706. package/registry/r/tokens.json +21 -0
  707. package/registry/r/tool-call-card.json +23 -0
  708. package/registry/r/tool-call.json +22 -0
  709. package/registry/r/tool-result.json +20 -0
  710. package/registry/r/tools-list.json +23 -0
  711. package/registry/r/tooltip.json +22 -0
  712. package/registry/r/topnav.json +22 -0
  713. package/registry/r/types.json +15 -0
  714. package/registry/r/usage-meter.json +21 -0
  715. package/registry/r/whiteboard.json +101 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tool-call-card",
4
+ "type": "registry:ui",
5
+ "title": "ToolCallCard",
6
+ "description": "Single agent tool invocation rendered inside the stream.",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json",
13
+ "https://usetheodev.github.io/theo-ui/r/types.json"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/primitives/tool-call-card/tool-call-card.tsx",
18
+ "type": "registry:ui",
19
+ "target": "components/ui/tool-call-card.tsx",
20
+ "content": "import { Check, ChevronRight, Loader2, X } from \"lucide-react\";\nimport { useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport type { IconComponent } from \"@/lib/types\";\n\n/**\n * ToolCallCard — single agent tool invocation rendered inside the stream.\n *\n * Visual: row with tool icon + tool name + target/command (mono) + status +\n * optional chevron. Expandable: when `output` is provided the row becomes a\n * `<details>` whose body renders the stdout/stderr/result block.\n *\n * Distinct from `AgentEvent` in the existing AgentTimeline by being a\n * stand-alone card that lives inside an AgentStream alongside chat messages.\n */\n\nexport type ToolCallStatus = \"running\" | \"success\" | \"failed\" | \"queued\" | \"skipped\";\n\nconst STATUS_ICON: Record<ToolCallStatus, ReactNode> = {\n running: <Loader2 className=\"size-3.5 animate-spin text-primary\" aria-hidden=\"true\" />,\n success: <Check className=\"size-3.5 text-success\" aria-hidden=\"true\" />,\n failed: <X className=\"size-3.5 text-destructive\" aria-hidden=\"true\" />,\n queued: <span className=\"size-2 rounded-full bg-warning\" aria-hidden=\"true\" />,\n skipped: <span className=\"size-2 rounded-full bg-muted-foreground\" aria-hidden=\"true\" />,\n};\n\nconst STATUS_LABEL: Record<ToolCallStatus, string> = {\n running: \"Running\",\n success: \"Completed\",\n failed: \"Failed\",\n queued: \"Queued\",\n skipped: \"Skipped\",\n};\n\ninterface ToolCallCardProps extends HTMLAttributes<HTMLElement> {\n /** Tool name (matches Theo Code / Claude Code tool registry: Bash, Edit, Read, …). */\n tool: ReactNode;\n /** Optional icon for the tool. */\n icon?: IconComponent;\n /** Target / command shown in monospace next to the tool name. */\n target?: ReactNode;\n status: ToolCallStatus;\n /** Optional stdout/stderr/result body. When present, the card becomes expandable. */\n output?: ReactNode;\n /** Default expanded state. Default: false (collapsed). */\n defaultExpanded?: boolean;\n /** Timestamp shown on the right. */\n timestamp?: ReactNode;\n}\n\nexport function ToolCallCard({\n className,\n tool,\n icon: Icon,\n target,\n status,\n output,\n defaultExpanded = false,\n timestamp,\n ...props\n}: ToolCallCardProps) {\n const [open, setOpen] = useState(defaultExpanded);\n const expandable = !!output;\n\n return (\n <article\n className={cn(\n \"overflow-hidden rounded-lg border border-border/40 bg-card/40 text-card-foreground\",\n className,\n )}\n {...props}\n >\n {/* T5.4: <header role=\"button\"> previously failed axe's\n * aria-prohibited-attr + semantic-landmark guidance. Replaced by\n * <div> for the layout container and a separate <button> for the\n * expand affordance (when expandable). Status icon span now carries\n * role=\"img\" to make aria-label valid. */}\n <div className={cn(\"flex items-center gap-2 px-3 py-2\")}>\n {expandable ? (\n <button\n type=\"button\"\n onClick={() => setOpen((v) => !v)}\n aria-expanded={open}\n aria-label={open ? `Collapse ${tool} details` : `Expand ${tool} details`}\n className=\"-m-1 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-1 text-muted-foreground hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <ChevronRight\n className={cn(\"size-3.5 transition-transform duration-base\", open && \"rotate-90\")}\n aria-hidden=\"true\"\n />\n </button>\n ) : null}\n {Icon ? (\n <Icon className=\"size-4 shrink-0 text-muted-foreground\" aria-hidden=\"true\" />\n ) : null}\n <span className=\"shrink-0 font-medium font-mono text-code-sm text-foreground\">{tool}</span>\n {target ? (\n <span className=\"truncate font-mono text-code-sm text-muted-foreground\">{target}</span>\n ) : null}\n <span\n role=\"img\"\n aria-label={STATUS_LABEL[status]}\n className=\"ml-auto inline-flex shrink-0 items-center gap-1.5\"\n >\n {STATUS_ICON[status]}\n </span>\n {timestamp ? (\n <span className=\"shrink-0 font-mono text-label text-muted-foreground tabular-nums\">\n {timestamp}\n </span>\n ) : null}\n </div>\n {expandable && open ? (\n <div className=\"border-border/40 border-t bg-muted/20 px-3 py-2 font-mono text-code-sm\">\n {output}\n </div>\n ) : null}\n </article>\n );\n}\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tool-call",
4
+ "type": "registry:ui",
5
+ "title": "ToolCall",
6
+ "description": "Collapsible row representing an agent tool invocation.",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/primitives/tool-call/tool-call.tsx",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/tool-call.tsx",
19
+ "content": "import { ChevronRight, Wrench } from \"lucide-react\";\nimport { forwardRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ninterface ToolCallProps extends HTMLAttributes<HTMLDivElement> {\n /** Tool name e.g. \"bash\", \"read_file\", \"edit_file\". */\n name?: string;\n /**\n * Summary label e.g. \"Ran 2 commands\", \"Read 18 files\".\n */\n summary: ReactNode;\n /**\n * Collapsible payload (e.g. command, stdout, file list).\n */\n detail?: ReactNode;\n defaultOpen?: boolean;\n /**\n * If true, hides the wrench icon (useful when grouping by name elsewhere).\n */\n hideIcon?: boolean;\n}\n\n/**\n * ToolCall — collapsible row representing an agent tool invocation.\n *\n * Visual: subtle muted container, wrench icon, summary in body text,\n * chevron rotates on expand. Pairs with `<ToolResult>` for the rendered output.\n *\n * Typical usage inside a ChatMessage assistant body:\n *\n * <ToolCall summary=\"Read 18 files\" detail={<ToolResult>{output}</ToolResult>} />\n */\nconst ToolCall = forwardRef<HTMLDivElement, ToolCallProps>(\n ({ className, name, summary, detail, defaultOpen, hideIcon, ...props }, ref) => {\n const [open, setOpen] = useState(defaultOpen ?? false);\n const expandable = detail !== undefined;\n return (\n <div\n ref={ref}\n className={cn(\"rounded-md border border-border/40 bg-muted/30\", className)}\n {...props}\n >\n <button\n type=\"button\"\n onClick={() => expandable && setOpen((v) => !v)}\n aria-expanded={expandable ? open : undefined}\n disabled={!expandable}\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2 text-left\",\n \"font-sans text-body-sm text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n expandable && \"cursor-pointer hover:bg-muted/60\",\n )}\n >\n {!hideIcon ? (\n <Wrench className=\"size-3.5 shrink-0 text-primary\" aria-hidden=\"true\" />\n ) : null}\n {name ? (\n <span className=\"font-mono text-code-sm text-muted-foreground\">{name}</span>\n ) : null}\n <span className=\"flex-1 truncate\">{summary}</span>\n {expandable ? (\n <ChevronRight\n className={cn(\n \"size-3.5 text-muted-foreground transition-transform\",\n open && \"rotate-90\",\n )}\n aria-hidden=\"true\"\n />\n ) : null}\n </button>\n {expandable && open ? (\n <div className=\"border-border/40 border-t bg-card px-3 py-2\">{detail}</div>\n ) : null}\n </div>\n );\n },\n);\nToolCall.displayName = \"ToolCall\";\n\nexport { ToolCall };\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tool-result",
4
+ "type": "registry:ui",
5
+ "title": "ToolResult",
6
+ "description": "Formatted output of a tool invocation.",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
10
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/primitives/tool-result/tool-result.tsx",
15
+ "type": "registry:ui",
16
+ "target": "components/ui/tool-result.tsx",
17
+ "content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Variant = \"text\" | \"code\" | \"json\";\n\ninterface ToolResultProps extends HTMLAttributes<HTMLDivElement> {\n variant?: Variant;\n /**\n * Pre-formatted content. For `code`/`json`, the component uses mono font\n * and preserves whitespace. For `text`, normal body font.\n */\n children: ReactNode;\n}\n\n/**\n * ToolResult — formatted output of a tool invocation.\n *\n * Three quick variants: plain text, code (monospace), json (monospace, tinted).\n * Always rendered as a `<div>` for predictable prop typing; code/json variants\n * wrap children in `<pre>` internally.\n */\nconst ToolResult = forwardRef<HTMLDivElement, ToolResultProps>(\n ({ className, variant = \"text\", children, ...props }, ref) => {\n if (variant === \"text\") {\n return (\n <div ref={ref} className={cn(\"text-body-sm text-muted-foreground\", className)} {...props}>\n {children}\n </div>\n );\n }\n return (\n <div ref={ref} className={className} {...props}>\n <pre\n className={cn(\n \"overflow-x-auto whitespace-pre-wrap font-mono text-code-sm\",\n variant === \"json\" ? \"text-primary-glow\" : \"text-foreground\",\n )}\n >\n {children}\n </pre>\n </div>\n );\n },\n);\nToolResult.displayName = \"ToolResult\";\n\nexport { ToolResult };\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tools-list",
4
+ "type": "registry:ui",
5
+ "title": "ToolsList",
6
+ "description": "Surface every tool the agent could call, with its enablement",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json",
13
+ "https://usetheodev.github.io/theo-ui/r/types.json"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/primitives/tools-list/tools-list.tsx",
18
+ "type": "registry:ui",
19
+ "target": "components/ui/tools-list.tsx",
20
+ "content": "import { Eye, Lock, Settings2 } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport type { IconComponent } from \"@/lib/types\";\n\nexport type ToolEnablement = \"enabled\" | \"ask\" | \"denied\";\n\nexport interface ToolEntry {\n id: string;\n name: string;\n description?: ReactNode;\n icon?: IconComponent;\n enablement?: ToolEnablement;\n /**\n * Source of the tool: built-in, plugin/skill, or MCP server name.\n */\n source?: string;\n /** Optional badge text (e.g. \"destructive\", \"experimental\"). */\n badge?: ReactNode;\n}\n\ninterface ToolsListProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\" | \"onChange\"> {\n tools: ToolEntry[];\n /** Title above the list. */\n title?: ReactNode;\n /**\n * Fires when the consumer toggles a tool's enablement state.\n * Cycle: enabled → ask → denied → enabled.\n */\n onEnablementChange?: (id: string, next: ToolEnablement) => void;\n}\n\nconst ENABLEMENT_LABEL: Record<ToolEnablement, string> = {\n enabled: \"Allowed\",\n ask: \"Ask before use\",\n denied: \"Denied\",\n};\n\nconst ENABLEMENT_CLASS: Record<ToolEnablement, string> = {\n enabled: \"bg-success/15 text-success border-success/40\",\n ask: \"bg-warning/15 text-warning border-warning/40\",\n denied: \"bg-destructive/15 text-destructive border-destructive/40\",\n};\n\nconst cycle = (cur: ToolEnablement): ToolEnablement =>\n cur === \"enabled\" ? \"ask\" : cur === \"ask\" ? \"denied\" : \"enabled\";\n\n/**\n * ToolsList — surface every tool the agent could call, with its enablement\n * state. Click the chip to cycle: Allowed → Ask → Denied.\n */\nconst ToolsList = forwardRef<HTMLDivElement, ToolsListProps>(\n ({ className, tools, title = \"Tools\", onEnablementChange, ...props }, ref) => (\n <section ref={ref} className={cn(\"rounded-xl border bg-card\", className)} {...props}>\n {title ? (\n <header className=\"flex items-center justify-between border-border/40 border-b px-4 py-3\">\n <h3 className=\"font-display text-title-md tracking-tight\">{title}</h3>\n <span className=\"font-mono text-label text-muted-foreground\">\n {tools.length} {tools.length === 1 ? \"tool\" : \"tools\"}\n </span>\n </header>\n ) : null}\n <ul className=\"divide-y divide-border/30\">\n {tools.map((tool) => {\n const Icon = tool.icon ?? Settings2;\n const state = tool.enablement ?? \"enabled\";\n return (\n <li\n key={tool.id}\n className=\"grid grid-cols-[auto_1fr_auto] items-start gap-3 px-4 py-3\"\n >\n <span className=\"mt-0.5 grid size-8 place-items-center rounded-md bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium font-mono text-body-sm text-foreground\">\n {tool.name}\n </span>\n {tool.source ? (\n <span className=\"font-mono text-label text-muted-foreground uppercase tracking-wider\">\n {tool.source}\n </span>\n ) : null}\n {tool.badge ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent/15 px-2 py-0.5 font-mono text-accent text-label uppercase\">\n {tool.badge}\n </span>\n ) : null}\n </div>\n {tool.description ? (\n <p className=\"mt-0.5 text-body-sm text-muted-foreground\">{tool.description}</p>\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={() => onEnablementChange?.(tool.id, cycle(state))}\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1\",\n \"font-mono text-label uppercase tracking-wider transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n ENABLEMENT_CLASS[state],\n !onEnablementChange && \"pointer-events-none\",\n )}\n aria-label={`Cycle enablement for ${tool.name}`}\n >\n {state === \"enabled\" ? (\n <Eye className=\"size-3\" aria-hidden=\"true\" />\n ) : state === \"ask\" ? (\n <Settings2 className=\"size-3\" aria-hidden=\"true\" />\n ) : (\n <Lock className=\"size-3\" aria-hidden=\"true\" />\n )}\n {ENABLEMENT_LABEL[state]}\n </button>\n </li>\n );\n })}\n </ul>\n </section>\n ),\n);\nToolsList.displayName = \"ToolsList\";\n\nexport { ToolsList };\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tooltip",
4
+ "type": "registry:ui",
5
+ "title": "Tooltip",
6
+ "description": "Built on Radix Tooltip — accessible hover / focus tooltip with delay and side / align controls.",
7
+ "dependencies": [
8
+ "@radix-ui/react-tooltip"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/primitives/tooltip/tooltip.tsx",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/tooltip.tsx",
19
+ "content": "import * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { forwardRef } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Tooltip — built on Radix Tooltip.\n *\n * Visual: dark surface in light mode (and inverse in dark) with rounded-md,\n * shadow-md, text-body-sm. 8px delay-show default.\n *\n * Wrap your app in <Tooltip.Provider> once (or use the default delayDuration here).\n */\n\nconst Provider = TooltipPrimitive.Provider;\nconst Root = TooltipPrimitive.Root;\nconst Trigger = TooltipPrimitive.Trigger;\n\nconst Content = forwardRef<\n ElementRef<typeof TooltipPrimitive.Content>,\n ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 6, ...props }, ref) => (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 overflow-hidden rounded-md border border-border/40 bg-foreground px-2.5 py-1.5\",\n \"text-background text-body-sm shadow-md\",\n \"data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-[state=delayed-open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out\",\n \"data-[side=top]:slide-in-from-bottom-1 data-[side=bottom]:slide-in-from-top-1\",\n \"data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1\",\n className,\n )}\n {...props}\n />\n </TooltipPrimitive.Portal>\n));\nContent.displayName = \"Tooltip.Content\";\n\ninterface TooltipProps extends ComponentPropsWithoutRef<typeof TooltipPrimitive.Root> {\n label: ReactNode;\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n align?: \"start\" | \"center\" | \"end\";\n children: ReactNode;\n}\n\n/**\n * Shorthand: <Tooltip label=\"…\"><Button>…</Button></Tooltip>\n * Wraps Provider + Root + Trigger asChild + Content for the common case.\n * For advanced usage (controlled state, custom content), use Tooltip.Root etc. directly.\n */\nconst Tooltip = ({\n label,\n side = \"top\",\n align = \"center\",\n delayDuration = 200,\n children,\n ...rootProps\n}: TooltipProps) =>\n (\n <Provider delayDuration={delayDuration}>\n <Root {...rootProps}>\n <Trigger asChild>{children}</Trigger>\n <Content side={side} align={align}>\n {label}\n </Content>\n </Root>\n </Provider>\n ) as ReturnType<typeof Provider>;\n\nconst TooltipWithStatics = Tooltip as typeof Tooltip & {\n Provider: typeof Provider;\n Root: typeof Root;\n Trigger: typeof Trigger;\n Content: typeof Content;\n};\nTooltipWithStatics.Provider = Provider;\nTooltipWithStatics.Root = Root;\nTooltipWithStatics.Trigger = Trigger;\nTooltipWithStatics.Content = Content;\n\nexport { TooltipWithStatics as Tooltip };\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "topnav",
4
+ "type": "registry:ui",
5
+ "title": "TopNav",
6
+ "description": "Horizontal app bar (64px) with breadcrumbs, mode switcher (radiogroup), and action slots.",
7
+ "dependencies": [
8
+ "lucide-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
12
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/primitives/topnav/topnav.tsx",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/topnav.tsx",
19
+ "content": "import { ChevronRight } from \"lucide-react\";\nimport { Fragment, forwardRef } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * TopNav — horizontal app bar (64px).\n *\n * Composition:\n * <TopNav>\n * <TopNav.Left>\n * <TopNav.Breadcrumbs items={[{label: \"acme\"}, {label: \"api\"}]} />\n * </TopNav.Left>\n * <TopNav.Center>…segmented switcher…</TopNav.Center>\n * <TopNav.Right>…actions…</TopNav.Right>\n * </TopNav>\n *\n * Variant — hairline bottom border. No glass/blur (anti-glass guideline).\n */\n\nconst Root = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(\n ({ className, ...props }, ref) => (\n <header\n ref={ref}\n className={cn(\n \"flex h-16 items-center justify-between gap-4 border-border/40 border-b bg-card px-6\",\n className,\n )}\n {...props}\n />\n ),\n);\nRoot.displayName = \"TopNav\";\n\nconst Left = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex flex-1 items-center gap-3\", className)} {...props} />\n ),\n);\nLeft.displayName = \"TopNav.Left\";\n\nconst Center = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"hidden flex-1 justify-center md:flex\", className)} {...props} />\n ),\n);\nCenter.displayName = \"TopNav.Center\";\n\nconst Right = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex flex-1 items-center justify-end gap-2\", className)}\n {...props}\n />\n ),\n);\nRight.displayName = \"TopNav.Right\";\n\ninterface BreadcrumbItem {\n label: ReactNode;\n href?: string;\n}\n\ninterface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {\n items: BreadcrumbItem[];\n}\n\nconst Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(\n ({ className, items, ...props }, ref) => (\n <nav\n ref={ref}\n aria-label=\"Breadcrumb\"\n className={cn(\"flex items-center gap-1.5 text-body-sm\", className)}\n {...props}\n >\n {items.map((item, idx) => {\n const isLast = idx === items.length - 1;\n const key = typeof item.label === \"string\" ? item.label : idx;\n return (\n <Fragment key={key}>\n {item.href && !isLast ? (\n <a\n href={item.href}\n className=\"font-sans text-muted-foreground transition-colors hover:text-foreground\"\n >\n {item.label}\n </a>\n ) : (\n <span\n className={cn(\n \"font-sans\",\n isLast ? \"font-medium text-foreground\" : \"text-muted-foreground\",\n )}\n aria-current={isLast ? \"page\" : undefined}\n >\n {item.label}\n </span>\n )}\n {!isLast ? (\n <ChevronRight className=\"size-3.5 text-muted-foreground\" aria-hidden=\"true\" />\n ) : null}\n </Fragment>\n );\n })}\n </nav>\n ),\n);\nBreadcrumbs.displayName = \"TopNav.Breadcrumbs\";\n\ninterface ModeSwitcherOption {\n value: string;\n label: ReactNode;\n}\n\ninterface ModeSwitcherProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n value: string;\n options: ModeSwitcherOption[];\n onChange?: (value: string) => void;\n /**\n * Accessible label for the radiogroup. Defaults to \"Mode\".\n */\n ariaLabel?: string;\n}\n\n/**\n * TopNav.ModeSwitcher — segmented control (Chat / Code / Infra).\n *\n * ARIA semantics: `role=\"radiogroup\"` + `role=\"radio\"` per option, with roving\n * tabindex and full keyboard navigation (Arrow keys + Home/End). Per WAI-ARIA\n * radiogroup pattern, exactly one option has `tabIndex=0` (the active one) and\n * the rest have `tabIndex=-1`, so Tab moves in and Tab moves out.\n *\n * Stateless: pass `value` + `onChange`.\n */\nconst ModeSwitcher = forwardRef<HTMLDivElement, ModeSwitcherProps>(\n ({ className, value, options, onChange, ariaLabel = \"Mode\", ...props }, ref) => {\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (!onChange || options.length === 0) return;\n const idx = options.findIndex((o) => o.value === value);\n const current = idx >= 0 ? idx : 0;\n let nextIdx: number | null = null;\n if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n nextIdx = (current + 1) % options.length;\n } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n nextIdx = (current - 1 + options.length) % options.length;\n } else if (e.key === \"Home\") {\n nextIdx = 0;\n } else if (e.key === \"End\") {\n nextIdx = options.length - 1;\n }\n if (nextIdx === null) return;\n e.preventDefault();\n const target = options[nextIdx];\n if (target) onChange(target.value);\n };\n\n return (\n <div\n ref={ref}\n role=\"radiogroup\"\n aria-label={ariaLabel}\n onKeyDown={handleKeyDown}\n className={cn(\n \"inline-flex items-center rounded-lg border border-border/60 bg-muted p-1\",\n className,\n )}\n {...props}\n >\n {options.map((opt) => {\n const isActive = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n // biome-ignore lint/a11y/useSemanticElements: WAI-ARIA radiogroup pattern requires role=\"radio\" on buttons for segmented controls\n role=\"radio\"\n aria-checked={isActive}\n tabIndex={isActive ? 0 : -1}\n onClick={() => onChange?.(opt.value)}\n className={cn(\n \"rounded-md px-3 py-1.5 font-medium font-sans text-body-sm\",\n \"transition-all duration-base ease-out-soft\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isActive\n ? \"bg-card text-foreground shadow-sm\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n },\n);\nModeSwitcher.displayName = \"TopNav.ModeSwitcher\";\n\nconst TopNav = /*#__PURE__*/ Object.assign(Root, {\n Left,\n Center,\n Right,\n Breadcrumbs,\n ModeSwitcher,\n});\n\nexport { TopNav };\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "types",
4
+ "type": "registry:lib",
5
+ "title": "Theo UI shared types",
6
+ "description": "Shared TypeScript helper types (IconComponent, etc.) used across Theo UI.",
7
+ "files": [
8
+ {
9
+ "path": "lib/types.ts",
10
+ "type": "registry:lib",
11
+ "target": "lib/types.ts",
12
+ "content": "import type { ComponentType, SVGProps } from \"react\";\n\n/**\n * IconComponent — shape compatible with both lucide-react icons and custom React SVG components.\n *\n * Centralizing this here avoids TS2322 noise from `exactOptionalPropertyTypes` when mapping\n * icons through Record<...> structures.\n */\nexport type IconComponent = ComponentType<SVGProps<SVGSVGElement> & { className?: string }>;\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "usage-meter",
4
+ "type": "registry:ui",
5
+ "title": "UsageMeter",
6
+ "description": "Multi-metric stacked usage card for PaaS dashboards. Renders N metrics (data transfer, requests, build minutes, seats, …) each with label + value/max + Progress bar. Supports custom per-metric formatter, over-quota warning, and a compact bars-only mode. PaaS-shape sibling of CostMeter.",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://usetheodev.github.io/theo-ui/r/cn.json",
10
+ "https://usetheodev.github.io/theo-ui/r/progress.json",
11
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/composites/usage-meter/usage-meter.tsx",
16
+ "type": "registry:ui",
17
+ "target": "components/ui/usage-meter.tsx",
18
+ "content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Progress } from \"@/components/ui/progress\";\n\n/**\n * UsageMeter — multi-metric stacked usage card.\n *\n * PaaS-shape sibling of `<CostMeter>` (which is mono-USD). Displays N\n * metrics with arbitrary units (GB, requests, build-minutes, seats) per\n * the reference TheoCloud dashboard mockup. Each row = label + value/max\n * + `<Progress>` fill bar.\n *\n * Composition:\n *\n * <UsageMeter\n * title=\"Last 30 days\"\n * action={<Badge variant=\"outline\">Upgrade</Badge>}\n * metrics={[\n * { label: \"Fast Data Transfer\", value: 0, max: 100, unit: \"GB\" },\n * { label: \"Edge Requests\", value: 0, max: 1_000_000, unit: \"req\",\n * formatter: (v, m) => `${v} / ${m >= 1e6 ? `${m / 1e6}M` : m}` },\n * ]}\n * />\n *\n * Over-quota detection: when `value > max`, the value text gets the\n * `text-warning` class and the underlying `<Progress>` uses\n * `intent=\"warning\"` (bar fills 100% via Progress's own clamping).\n *\n * `compact` mode renders only the bars (no label/value text) — useful in\n * narrow nav-bar slots.\n *\n * Imports the sibling `<Progress>` primitive via relative path (per RFC\n * dashboard-paas-primitives D3) — primitives must not depend on the\n * `@theokit/ui` barrel.\n */\n\nexport interface UsageMetric {\n /** Display label (e.g. \"Fast Data Transfer\"). */\n label: ReactNode;\n /** Current consumption. */\n value: number;\n /** Maximum allowed in the current period. */\n max: number;\n /** Unit string (e.g. \"GB\", \"req\", \"min\"). Rendered after value/max. */\n unit?: string;\n /** Optional custom formatter — overrides default `${value} / ${max} ${unit}`. */\n formatter?: (value: number, max: number, unit?: string) => string;\n}\n\nexport interface UsageMeterProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Card title (e.g. \"Last 30 days\", \"This billing period\"). */\n title?: ReactNode;\n /** Optional right-aligned action slot (e.g. an Upgrade Badge or Button). */\n action?: ReactNode;\n /** Array of metrics to display. Order preserved. */\n metrics: UsageMetric[];\n /** When true, show only the bars without label/value text (sparkline mode). */\n compact?: boolean;\n}\n\nfunction defaultFormat(value: number, max: number, unit?: string): string {\n return `${value} / ${max}${unit ? ` ${unit}` : \"\"}`;\n}\n\nfunction metricAriaLabel(metric: UsageMetric, formatted: string): string {\n // Label may be a ReactNode; coerce to string for aria-label.\n const label = typeof metric.label === \"string\" ? metric.label : \"metric\";\n return `${label}: ${formatted}`;\n}\n\nconst UsageMeter = forwardRef<HTMLDivElement, UsageMeterProps>(\n ({ className, title, action, metrics, compact = false, ...props }, ref) => {\n const hasHeader = Boolean(title) || Boolean(action);\n return (\n <div\n ref={ref}\n className={cn(\n \"grid gap-3 rounded-xl border border-border bg-card p-4\",\n compact && \"gap-2\",\n className,\n )}\n data-theo-usage-meter=\"\"\n {...props}\n >\n {hasHeader ? (\n <header className=\"flex items-baseline justify-between gap-3\">\n {title ? (\n <span\n className={cn(\n \"font-mono text-label-caps text-muted-foreground uppercase tracking-wider\",\n )}\n >\n {title}\n </span>\n ) : (\n <span />\n )}\n {action ? <div className=\"shrink-0\">{action}</div> : null}\n </header>\n ) : null}\n\n {metrics.map((metric, idx) => {\n const overQuota = metric.value > metric.max;\n const fmt = metric.formatter ?? defaultFormat;\n const formatted = fmt(metric.value, metric.max, metric.unit);\n const intent = overQuota ? \"warning\" : \"default\";\n // Each metric needs a stable key. Without a unique id field we\n // synthesize one from label + idx — duplicates resolve via the\n // index suffix.\n const key = `${typeof metric.label === \"string\" ? metric.label : \"metric\"}-${idx}`;\n\n if (compact) {\n return (\n <Progress\n key={key}\n value={metric.value}\n max={metric.max}\n intent={intent}\n aria-label={metricAriaLabel(metric, formatted)}\n />\n );\n }\n\n return (\n <div key={key} className=\"grid gap-1.5\">\n <div className=\"flex items-baseline justify-between gap-3 text-body-sm\">\n <span className=\"truncate text-muted-foreground\">{metric.label}</span>\n <span\n className={cn(\n \"shrink-0 font-mono tabular-nums\",\n overQuota ? \"text-warning\" : \"text-foreground\",\n )}\n >\n {formatted}\n </span>\n </div>\n <Progress\n value={metric.value}\n max={metric.max}\n intent={intent}\n aria-label={metricAriaLabel(metric, formatted)}\n />\n </div>\n );\n })}\n </div>\n );\n },\n);\nUsageMeter.displayName = \"UsageMeter\";\n\nexport { UsageMeter };\n"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "whiteboard",
4
+ "type": "registry:ui",
5
+ "title": "Whiteboard",
6
+ "description": "View-only primitive that renders a declarative JSON scene as SVG with a hand-drawn aesthetic. Pan + zoom built-in; no editor surface. Subpath-isolated bundle in @theokit/ui.",
7
+ "dependencies": [
8
+ "perfect-freehand",
9
+ "roughjs",
10
+ "zod"
11
+ ],
12
+ "registryDependencies": [
13
+ "https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/primitives/whiteboard/whiteboard.tsx",
18
+ "type": "registry:ui",
19
+ "target": "components/ui/whiteboard/whiteboard.tsx",
20
+ "content": "import { useEffect, useMemo, useRef } from \"react\";\n/**\n * `<Whiteboard>` — view-only primitive that turns a JSON scene into a\n * hand-drawn SVG. Lives in the isolated subpath `@theokit/ui/whiteboard`.\n *\n * See RFC 0001 (`docs/rfcs/0001-whiteboard.md`) and the plan in\n * `.claude/knowledge-base/plans/whiteboard-view-primitive-plan.md`.\n */\nimport rough from \"roughjs\";\nimport type { RoughGenerator } from \"roughjs/bin/generator\";\nimport { renderScene } from \"@/components/ui/whiteboard/render/scene\";\nimport type { WhiteboardScene, WhiteboardSceneInput } from \"@/components/ui/whiteboard/schema\";\nimport { type ValidationError, validateScene } from \"@/components/ui/whiteboard/validate\";\nimport { usePointerPan } from \"@/components/ui/whiteboard/viewport/use-pointer-pan\";\nimport { useViewport } from \"@/components/ui/whiteboard/viewport/use-viewport\";\n\n/** Input type accepted by the component (defaults like `headEnd` are optional). */\nexport type WhiteboardData = WhiteboardSceneInput;\nexport type { ValidationError } from \"@/components/ui/whiteboard/validate\";\n\nexport interface WhiteboardProps {\n /** The scene to render. Validated with Zod on every change. */\n data: WhiteboardData | unknown;\n className?: string;\n /** Starting zoom level (clamped 0.1–8). Default 1. */\n initialZoom?: number;\n /** World coordinate centered in the viewport on first render. */\n initialCenter?: [number, number];\n /** When true, computes a bounding box of all elements and zooms to fit. */\n fitOnLoad?: boolean;\n /** Called once per validation failure with the list of errors (EC-6: in useEffect, not render). */\n onValidationError?: (errors: ValidationError[]) => void;\n /** Accessible label for the SVG (defaults to \"Whiteboard diagram\"). */\n \"aria-label\"?: string;\n}\n\nfunction computeBounds(scene: WhiteboardScene): {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n} | null {\n if (scene.elements.length === 0) return null;\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const el of scene.elements) {\n const x1 = el.x;\n const y1 = el.y;\n let x2 = el.x;\n let y2 = el.y;\n if (el.type === \"rect\" || el.type === \"ellipse\" || el.type === \"diamond\") {\n x2 = el.x + el.w;\n y2 = el.y + el.h;\n } else if (el.type === \"line\" || el.type === \"arrow\") {\n x2 = el.to[0];\n y2 = el.to[1];\n } else if (el.type === \"freedraw\") {\n for (const [px, py] of el.points) {\n const tx = el.x + px;\n const ty = el.y + py;\n if (tx < minX) minX = tx;\n if (ty < minY) minY = ty;\n if (tx > maxX) maxX = tx;\n if (ty > maxY) maxY = ty;\n }\n continue;\n }\n if (x1 < minX) minX = x1;\n if (y1 < minY) minY = y1;\n if (x2 < minX) minX = x2;\n if (y2 < minY) minY = y2;\n if (x1 > maxX) maxX = x1;\n if (y1 > maxY) maxY = y1;\n if (x2 > maxX) maxX = x2;\n if (y2 > maxY) maxY = y2;\n }\n if (!Number.isFinite(minX)) return null;\n return { minX, minY, maxX, maxY };\n}\n\nexport function Whiteboard({\n data,\n className,\n initialZoom,\n initialCenter,\n fitOnLoad,\n onValidationError,\n \"aria-label\": ariaLabel,\n}: WhiteboardProps) {\n const validation = useMemo(() => validateScene(data), [data]);\n const scene = validation.ok ? validation.scene : null;\n\n // EC-6: side effects only inside useEffect — never call user callbacks in render.\n useEffect(() => {\n if (!validation.ok && onValidationError) {\n onValidationError(validation.errors);\n }\n }, [validation, onValidationError]);\n\n // Stable rough.js generator across renders.\n const generator: RoughGenerator | null = useMemo(() => {\n if (typeof globalThis === \"undefined\") return null;\n try {\n return rough.generator();\n } catch {\n return null;\n }\n }, []);\n\n // Fallback dimensions for invalid scenes (still render a valid svg).\n const sceneWidth = scene?.width ?? 400;\n const sceneHeight = scene?.height ?? 300;\n const sceneBackground = scene?.background;\n\n const viewport = useViewport({\n width: sceneWidth,\n height: sceneHeight,\n initialZoom,\n initialCenter,\n });\n\n const svgRef = useRef<SVGSVGElement | null>(null);\n\n const fitTo = viewport.fitTo;\n // EC-14: fitOnLoad must run in useEffect (after mount), not during render.\n useEffect(() => {\n if (!fitOnLoad || !scene) return;\n const bounds = computeBounds(scene);\n if (!bounds) return;\n fitTo(bounds, { width: sceneWidth, height: sceneHeight });\n }, [fitOnLoad, scene, sceneWidth, sceneHeight, fitTo]);\n\n const handlers = usePointerPan(svgRef, viewport, {\n width: sceneWidth,\n height: sceneHeight,\n });\n\n const label = ariaLabel ?? \"Whiteboard diagram\";\n\n return (\n <svg\n ref={svgRef}\n viewBox={viewport.viewBox({ width: sceneWidth, height: sceneHeight })}\n width={sceneWidth}\n height={sceneHeight}\n className={className}\n role=\"img\"\n aria-label={label}\n xmlns=\"http://www.w3.org/2000/svg\"\n data-whiteboard-state={scene ? \"ok\" : \"invalid\"}\n style={{ touchAction: \"none\", userSelect: \"none\" }}\n {...handlers}\n >\n <title>{label}</title>\n {sceneBackground ? (\n <rect\n x={0}\n y={0}\n width={sceneWidth}\n height={sceneHeight}\n fill={sceneBackground}\n data-layer=\"background\"\n />\n ) : null}\n {scene && generator ? renderScene(scene, generator) : null}\n </svg>\n );\n}\n"
21
+ },
22
+ {
23
+ "path": "components/primitives/whiteboard/schema.ts",
24
+ "type": "registry:ui",
25
+ "target": "components/ui/whiteboard/schema.ts",
26
+ "content": "/**\n * Zod schema for the Whiteboard JSON v1 (`WhiteboardScene`).\n *\n * Design goals (see RFC 0001 §4 and ADR D4 in the plan):\n * - LLM-friendly: minimal fields per element.\n * - Discriminated union by `type` for exhaustive typing in TS.\n * - Defensive against LLM mistakes:\n * - EC-3: `.finite()` everywhere — rejects NaN / Infinity.\n * - EC-4: `.max()` on dimensions to cap absurd values from the model.\n */\nimport { z } from \"zod\";\n\nconst finiteNumber = z.number().finite();\nconst finitePositive = finiteNumber.positive();\n\nconst strokeStyle = z.enum([\"solid\", \"dashed\", \"dotted\"]);\nconst fillStyle = z.enum([\"hachure\", \"solid\", \"cross-hatch\", \"zigzag\"]);\nconst align = z.enum([\"left\", \"center\", \"right\"]);\nconst fontFamily = z.enum([\"sans\", \"serif\", \"mono\", \"hand\"]);\nconst roundness = z.enum([\"sharp\", \"round\"]);\n\nconst baseElement = z.object({\n id: z.string().max(120).optional(),\n x: finiteNumber,\n y: finiteNumber,\n stroke: z.string().max(64).optional(),\n strokeWidth: finitePositive.max(50).optional(),\n strokeStyle: strokeStyle.optional(),\n fill: z.string().max(64).optional(),\n fillStyle: fillStyle.optional(),\n opacity: finiteNumber.min(0).max(1).optional(),\n roughness: finiteNumber.min(0).max(3).optional(),\n seed: z.number().int().finite().optional(),\n});\n\nconst dim = finitePositive.max(20000);\nconst labelText = z.string().max(500);\n\nconst rectElement = baseElement.extend({\n type: z.literal(\"rect\"),\n w: dim,\n h: dim,\n label: labelText.optional(),\n roundness: roundness.optional(),\n});\n\nconst ellipseElement = baseElement.extend({\n type: z.literal(\"ellipse\"),\n w: dim,\n h: dim,\n label: labelText.optional(),\n});\n\nconst diamondElement = baseElement.extend({\n type: z.literal(\"diamond\"),\n w: dim,\n h: dim,\n label: labelText.optional(),\n});\n\nconst point2 = z.tuple([finiteNumber, finiteNumber]);\n\nconst lineElement = baseElement.extend({\n type: z.literal(\"line\"),\n to: point2,\n});\n\nconst arrowElement = baseElement.extend({\n type: z.literal(\"arrow\"),\n to: point2,\n label: labelText.optional(),\n headStart: z.boolean().optional(),\n headEnd: z.boolean().default(true),\n});\n\nconst textElement = baseElement.extend({\n type: z.literal(\"text\"),\n text: z.string().max(5000),\n fontSize: finitePositive.max(500).optional(),\n align: align.optional(),\n fontFamily: fontFamily.optional(),\n});\n\nconst freedrawPoint = z.tuple([finiteNumber, finiteNumber, finiteNumber.optional()]);\n\nconst freedrawElement = baseElement.extend({\n type: z.literal(\"freedraw\"),\n points: z.array(freedrawPoint).min(2).max(5000),\n});\n\nexport const whiteboardElement = z.discriminatedUnion(\"type\", [\n rectElement,\n ellipseElement,\n diamondElement,\n lineElement,\n arrowElement,\n textElement,\n freedrawElement,\n]);\n\nexport const whiteboardScene = z.object({\n version: z.literal(1),\n width: dim, // EC-4: clamped 1..20000\n height: dim, // EC-4: clamped 1..20000\n background: z.string().max(64).optional(),\n elements: z.array(whiteboardElement).max(5000),\n});\n\n// Internal \"parsed\" types (defaults applied) — used inside the renderer.\nexport type WhiteboardElement = z.output<typeof whiteboardElement>;\nexport type WhiteboardScene = z.output<typeof whiteboardScene>;\nexport type RectElement = z.output<typeof rectElement>;\nexport type EllipseElement = z.output<typeof ellipseElement>;\nexport type DiamondElement = z.output<typeof diamondElement>;\nexport type LineElement = z.output<typeof lineElement>;\nexport type ArrowElement = z.output<typeof arrowElement>;\nexport type TextElement = z.output<typeof textElement>;\nexport type FreedrawElement = z.output<typeof freedrawElement>;\n\n// Public \"input\" types (defaults optional) — used by consumers passing data.\nexport type WhiteboardElementInput = z.input<typeof whiteboardElement>;\nexport type WhiteboardSceneInput = z.input<typeof whiteboardScene>;\n"
27
+ },
28
+ {
29
+ "path": "components/primitives/whiteboard/validate.ts",
30
+ "type": "registry:ui",
31
+ "target": "components/ui/whiteboard/validate.ts",
32
+ "content": "/**\n * Public validator for `WhiteboardScene` JSON. Wraps Zod and converts errors\n * into a structured shape that an LLM can consume to self-correct.\n *\n * Returns `{ ok: true, scene }` on success or `{ ok: false, errors }` with\n * each error carrying `{ path, message, code, got? }`. See RFC 0001 §4.\n */\nimport { type WhiteboardScene, whiteboardScene } from \"@/components/ui/whiteboard/schema\";\n\nexport interface ValidationError {\n /** Dot-joined Zod path, e.g. \"elements.2.type\" or \"width\". */\n path: string;\n /** Human-readable explanation (passes through Zod's message). */\n message: string;\n /** Zod issue code: `invalid_type`, `too_small`, `invalid_literal`, ... */\n code: string;\n /** Actual value received at `path`. Populated for type mismatches, discriminator mismatches, and any issue where Zod's `received` is absent but the value can be recovered by walking the original input along `path`. */\n got?: unknown;\n}\n\nexport type ValidationResult =\n | { ok: true; scene: WhiteboardScene }\n | { ok: false; errors: ValidationError[] };\n\ninterface ZodLikeIssue {\n path: ReadonlyArray<string | number>;\n message: string;\n code: string;\n received?: unknown;\n}\n\n/** Walk `input` following the issue path and return the offending value. */\nfunction valueAtPath(input: unknown, path: ReadonlyArray<string | number>): unknown {\n let cursor: unknown = input;\n for (const segment of path) {\n if (cursor === null || typeof cursor !== \"object\") return undefined;\n cursor = (cursor as Record<string | number, unknown>)[segment];\n }\n return cursor;\n}\n\nfunction formatIssue(issue: ZodLikeIssue, input: unknown): ValidationError {\n const error: ValidationError = {\n path: issue.path.join(\".\"),\n message: issue.message,\n code: issue.code,\n };\n // Populate `got` from the issue when Zod provides it (invalid_type), or by\n // walking the original input along the path (invalid_union / invalid_value).\n // LLM auto-correction benefits from seeing what was actually emitted.\n if (\"received\" in issue && issue.received !== undefined) {\n error.got = issue.received;\n } else if (issue.path.length > 0) {\n const value = valueAtPath(input, issue.path);\n if (value !== undefined) error.got = value;\n }\n return error;\n}\n\nexport function validateScene(input: unknown): ValidationResult {\n const result = whiteboardScene.safeParse(input);\n if (result.success) {\n return { ok: true, scene: result.data };\n }\n const errors = result.error.issues.map((issue) =>\n formatIssue(issue as unknown as ZodLikeIssue, input),\n );\n return { ok: false, errors };\n}\n"
33
+ },
34
+ {
35
+ "path": "components/primitives/whiteboard/seed.ts",
36
+ "type": "registry:ui",
37
+ "target": "components/ui/whiteboard/seed.ts",
38
+ "content": "/**\n * Deterministic seed derivation for rough.js — see ADR D9.\n *\n * rough.js uses pseudo-randomness to produce the hand-drawn look. Without a\n * stable seed every render shifts the strokes slightly, breaking snapshot\n * tests, causing SSR hydration mismatches, and visible jitter when the parent\n * re-renders. FNV-1a 32-bit gives us a fast, dependency-free hash.\n */\n\nconst FNV_OFFSET_BASIS_32 = 0x811c9dc5;\nconst FNV_PRIME_32 = 0x01000193;\n\n/** FNV-1a 32-bit hash. Returns a signed 32-bit integer. */\nexport function fnv1a32(input: string): number {\n let hash = FNV_OFFSET_BASIS_32;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n // Multiply by the FNV prime, but keep within 32-bit unsigned range.\n hash = Math.imul(hash, FNV_PRIME_32);\n }\n // Cast to signed 32-bit via `| 0`.\n return hash | 0;\n}\n\ninterface SeedableShape {\n type: string;\n x: number;\n y: number;\n w?: number;\n h?: number;\n label?: string;\n seed?: number;\n}\n\n/** Return the element's explicit `seed` or derive a stable one from its props. */\nexport function deriveSeed(el: SeedableShape): number {\n if (typeof el.seed === \"number\" && Number.isFinite(el.seed)) {\n return el.seed | 0;\n }\n // Compose a key from the dimensions that visually define the shape. Other\n // fields (colors, opacity) don't change the underlying rough.js geometry.\n const key = `${el.type}|${el.x}|${el.y}|${el.w ?? \"\"}|${el.h ?? \"\"}|${el.label ?? \"\"}`;\n return fnv1a32(key);\n}\n"
39
+ },
40
+ {
41
+ "path": "components/primitives/whiteboard/index.ts",
42
+ "type": "registry:ui",
43
+ "target": "components/ui/whiteboard/index.ts",
44
+ "content": "export { Whiteboard, type WhiteboardData, type WhiteboardProps } from \"@/components/ui/whiteboard/whiteboard\";\nexport { validateScene, type ValidationError, type ValidationResult } from \"@/components/ui/whiteboard/validate\";\n"
45
+ },
46
+ {
47
+ "path": "components/primitives/whiteboard/render/scene.tsx",
48
+ "type": "registry:ui",
49
+ "target": "components/ui/whiteboard/render/scene.tsx",
50
+ "content": "import type { RoughGenerator } from \"roughjs/bin/generator\";\nimport type { WhiteboardElement, WhiteboardScene } from \"@/components/ui/whiteboard/schema\";\nimport { deriveSeed } from \"@/components/ui/whiteboard/seed\";\nimport { renderFreedraw } from \"@/components/ui/whiteboard/render/freedraw\";\nimport { renderArrow, renderLine } from \"@/components/ui/whiteboard/render/line\";\nimport { renderDiamond, renderEllipse, renderRect } from \"@/components/ui/whiteboard/render/shape\";\nimport { renderText } from \"@/components/ui/whiteboard/render/text\";\n\nfunction elementWithSeed<T extends WhiteboardElement>(el: T): T {\n if (typeof el.seed === \"number\") return el;\n return { ...el, seed: deriveSeed(el) } as T;\n}\n\nfunction renderElement(el: WhiteboardElement, gen: RoughGenerator): React.ReactNode {\n switch (el.type) {\n case \"rect\":\n return renderRect(gen, elementWithSeed(el));\n case \"ellipse\":\n return renderEllipse(gen, elementWithSeed(el));\n case \"diamond\":\n return renderDiamond(gen, elementWithSeed(el));\n case \"line\":\n return renderLine(gen, elementWithSeed(el));\n case \"arrow\":\n return renderArrow(gen, elementWithSeed(el));\n case \"text\":\n return renderText(elementWithSeed(el));\n case \"freedraw\":\n return renderFreedraw(elementWithSeed(el));\n }\n}\n\nexport function renderScene(scene: WhiteboardScene, gen: RoughGenerator): React.ReactNode {\n return (\n <>\n {scene.elements.map((el, i) => (\n <g\n key={el.id ?? `__idx-${i}`}\n data-element-id={el.id ?? String(i)}\n data-element-type={el.type}\n >\n {renderElement(el, gen)}\n </g>\n ))}\n </>\n );\n}\n"
51
+ },
52
+ {
53
+ "path": "components/primitives/whiteboard/render/shape.tsx",
54
+ "type": "registry:ui",
55
+ "target": "components/ui/whiteboard/render/shape.tsx",
56
+ "content": "import type { RoughGenerator } from \"roughjs/bin/generator\";\nimport type { DiamondElement, EllipseElement, RectElement } from \"@/components/ui/whiteboard/schema\";\nimport { toRoughPaths } from \"@/components/ui/whiteboard/render/rough-paths\";\nimport { buildOptions, strokeDashArray } from \"@/components/ui/whiteboard/render/style\";\n\ninterface LabelProps {\n cx: number;\n cy: number;\n label: string;\n stroke?: string;\n}\n\nfunction Label({ cx, cy, label, stroke }: LabelProps) {\n return (\n <text\n x={cx}\n y={cy}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n fontSize={16}\n fontFamily=\"ui-sans-serif, system-ui, sans-serif\"\n fill={stroke ?? \"currentColor\"}\n style={{ pointerEvents: \"none\" }}\n >\n {label}\n </text>\n );\n}\n\nfunction pathsToReactNodes(\n paths: ReturnType<typeof toRoughPaths>,\n keyPrefix: string,\n outlineStroke: string,\n dashArray?: string,\n): React.ReactNode[] {\n return paths.map((p, i) => {\n // Apply stroke-dasharray only to paths whose stroke matches the outline\n // color — this avoids dashing hachure/cross-hatch fill lines (which are\n // stroked in the fill color, not the outline color) when a shape mixes\n // fillStyle with strokeStyle.\n const isOutline = p.stroke === outlineStroke;\n return (\n <path\n // biome-ignore lint/suspicious/noArrayIndexKey: rough.js path order is stable for a given (geometry, seed) tuple — index is the most precise key.\n key={`${keyPrefix}-${i}`}\n d={p.d}\n stroke={p.stroke}\n strokeWidth={p.strokeWidth}\n fill={p.fill ?? \"none\"}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeDasharray={isOutline ? dashArray : undefined}\n />\n );\n });\n}\n\nexport function renderRect(gen: RoughGenerator, el: RectElement): React.ReactNode {\n const opts = buildOptions(el, el.seed ?? 0);\n const drawable = gen.rectangle(el.x, el.y, el.w, el.h, opts);\n const paths = toRoughPaths(gen, drawable);\n const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);\n return (\n <g opacity={el.opacity ?? 1}>\n {pathsToReactNodes(paths, \"rect\", opts.stroke as string, dash)}\n {el.label ? (\n <Label cx={el.x + el.w / 2} cy={el.y + el.h / 2} label={el.label} stroke={el.stroke} />\n ) : null}\n </g>\n );\n}\n\nexport function renderEllipse(gen: RoughGenerator, el: EllipseElement): React.ReactNode {\n const opts = buildOptions(el, el.seed ?? 0);\n // rough.js ellipse takes center + width + height.\n const drawable = gen.ellipse(el.x + el.w / 2, el.y + el.h / 2, el.w, el.h, opts);\n const paths = toRoughPaths(gen, drawable);\n const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);\n return (\n <g opacity={el.opacity ?? 1}>\n {pathsToReactNodes(paths, \"ellipse\", opts.stroke as string, dash)}\n {el.label ? (\n <Label cx={el.x + el.w / 2} cy={el.y + el.h / 2} label={el.label} stroke={el.stroke} />\n ) : null}\n </g>\n );\n}\n\nexport function renderDiamond(gen: RoughGenerator, el: DiamondElement): React.ReactNode {\n const opts = buildOptions(el, el.seed ?? 0);\n const cx = el.x + el.w / 2;\n const cy = el.y + el.h / 2;\n const points: [number, number][] = [\n [cx, el.y], // top\n [el.x + el.w, cy], // right\n [cx, el.y + el.h], // bottom\n [el.x, cy], // left\n ];\n const drawable = gen.polygon(points, opts);\n const paths = toRoughPaths(gen, drawable);\n const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);\n return (\n <g opacity={el.opacity ?? 1}>\n {pathsToReactNodes(paths, \"diamond\", opts.stroke as string, dash)}\n {el.label ? <Label cx={cx} cy={cy} label={el.label} stroke={el.stroke} /> : null}\n </g>\n );\n}\n"
57
+ },
58
+ {
59
+ "path": "components/primitives/whiteboard/render/line.tsx",
60
+ "type": "registry:ui",
61
+ "target": "components/ui/whiteboard/render/line.tsx",
62
+ "content": "import type { RoughGenerator } from \"roughjs/bin/generator\";\nimport type { ArrowElement, LineElement } from \"@/components/ui/whiteboard/schema\";\nimport { toRoughPaths } from \"@/components/ui/whiteboard/render/rough-paths\";\nimport { buildOptions, strokeDashArray } from \"@/components/ui/whiteboard/render/style\";\n\nconst HEAD_BASE_PX = 12;\nconst HEAD_ANGLE_RAD = Math.PI / 7; // ~25°\n\ninterface HeadGeom {\n apexX: number;\n apexY: number;\n leftX: number;\n leftY: number;\n rightX: number;\n rightY: number;\n}\n\nfunction arrowHeadGeometry(\n fromX: number,\n fromY: number,\n toX: number,\n toY: number,\n strokeWidth: number,\n): HeadGeom | null {\n const dx = toX - fromX;\n const dy = toY - fromY;\n const dist = Math.sqrt(dx * dx + dy * dy);\n if (dist === 0) return null;\n const angle = Math.atan2(dy, dx);\n // EC-7: clamp head length so it never exceeds 40% of the segment.\n const headLen = Math.min(HEAD_BASE_PX + strokeWidth * 2, dist * 0.4);\n const leftX = toX - headLen * Math.cos(angle - HEAD_ANGLE_RAD);\n const leftY = toY - headLen * Math.sin(angle - HEAD_ANGLE_RAD);\n const rightX = toX - headLen * Math.cos(angle + HEAD_ANGLE_RAD);\n const rightY = toY - headLen * Math.sin(angle + HEAD_ANGLE_RAD);\n return { apexX: toX, apexY: toY, leftX, leftY, rightX, rightY };\n}\n\nfunction lineBody(\n gen: RoughGenerator,\n el: LineElement | ArrowElement,\n seed: number,\n): React.ReactNode[] {\n const opts = buildOptions(el, seed);\n const drawable = gen.line(el.x, el.y, el.to[0], el.to[1], opts);\n const dash = strokeDashArray(el.strokeStyle, el.strokeWidth ?? 1.5);\n return toRoughPaths(gen, drawable).map((p, i) => (\n <path\n // biome-ignore lint/suspicious/noArrayIndexKey: stable rough.js path order\n key={`body-${i}`}\n d={p.d}\n stroke={p.stroke}\n strokeWidth={p.strokeWidth}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeDasharray={dash}\n />\n ));\n}\n\nfunction arrowHead(\n gen: RoughGenerator,\n fromX: number,\n fromY: number,\n toX: number,\n toY: number,\n seed: number,\n el: ArrowElement,\n side: \"start\" | \"end\",\n): React.ReactNode[] {\n const geom = arrowHeadGeometry(fromX, fromY, toX, toY, el.strokeWidth ?? 1.5);\n if (!geom) return [];\n const opts = buildOptions(el, seed);\n // Two short rough-drawn segments forming the V.\n const leftDrawable = gen.line(geom.apexX, geom.apexY, geom.leftX, geom.leftY, opts);\n const rightDrawable = gen.line(geom.apexX, geom.apexY, geom.rightX, geom.rightY, opts);\n const leftPaths = toRoughPaths(gen, leftDrawable);\n const rightPaths = toRoughPaths(gen, rightDrawable);\n const nodes: React.ReactNode[] = [];\n for (const [i, p] of leftPaths.entries()) {\n nodes.push(\n <path\n key={`${side}-l-${i}`}\n d={p.d}\n stroke={p.stroke}\n strokeWidth={p.strokeWidth}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-line-part=\"arrowhead\"\n />,\n );\n }\n for (const [i, p] of rightPaths.entries()) {\n nodes.push(\n <path\n key={`${side}-r-${i}`}\n d={p.d}\n stroke={p.stroke}\n strokeWidth={p.strokeWidth}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n data-line-part=\"arrowhead\"\n />,\n );\n }\n return nodes;\n}\n\nexport function renderLine(gen: RoughGenerator, el: LineElement): React.ReactNode {\n return <g opacity={el.opacity ?? 1}>{lineBody(gen, el, el.seed ?? 0)}</g>;\n}\n\nexport function renderArrow(gen: RoughGenerator, el: ArrowElement): React.ReactNode {\n const seed = el.seed ?? 0;\n const nodes: React.ReactNode[] = [...lineBody(gen, el, seed)];\n if (el.headEnd !== false) {\n nodes.push(...arrowHead(gen, el.x, el.y, el.to[0], el.to[1], seed + 1, el, \"end\"));\n }\n if (el.headStart) {\n nodes.push(...arrowHead(gen, el.to[0], el.to[1], el.x, el.y, seed + 2, el, \"start\"));\n }\n let label: React.ReactNode = null;\n if (el.label) {\n const midX = (el.x + el.to[0]) / 2;\n const midY = (el.y + el.to[1]) / 2;\n // Offset perpendicular to the line so label doesn't sit on the stroke.\n const dx = el.to[0] - el.x;\n const dy = el.to[1] - el.y;\n const dist = Math.sqrt(dx * dx + dy * dy) || 1;\n const nx = -dy / dist;\n const ny = dx / dist;\n const offset = 12;\n label = (\n <text\n x={midX + nx * offset}\n y={midY + ny * offset}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n fontSize={14}\n fontFamily=\"ui-sans-serif, system-ui, sans-serif\"\n fill={el.stroke ?? \"currentColor\"}\n style={{ pointerEvents: \"none\" }}\n >\n {el.label}\n </text>\n );\n }\n return (\n <g opacity={el.opacity ?? 1}>\n {nodes}\n {label}\n </g>\n );\n}\n"
63
+ },
64
+ {
65
+ "path": "components/primitives/whiteboard/render/freedraw.tsx",
66
+ "type": "registry:ui",
67
+ "target": "components/ui/whiteboard/render/freedraw.tsx",
68
+ "content": "import { getStroke } from \"perfect-freehand\";\nimport type { FreedrawElement } from \"@/components/ui/whiteboard/schema\";\n\nconst DEFAULT_STROKE_OPTIONS = {\n size: 8,\n thinning: 0.5,\n smoothing: 0.5,\n streamline: 0.5,\n};\n\nfunction fmt(n: number | undefined): string {\n return typeof n === \"number\" && Number.isFinite(n) ? n.toFixed(2) : \"0\";\n}\n\nfunction svgPathFromStroke(points: number[][]): string {\n if (points.length === 0) return \"\";\n const first = points[0] ?? [];\n let d = `M ${fmt(first[0])} ${fmt(first[1])}`;\n for (let i = 1; i < points.length; i++) {\n const p = points[i] ?? [];\n d += ` L ${fmt(p[0])} ${fmt(p[1])}`;\n }\n d += \" Z\";\n return d;\n}\n\nexport function renderFreedraw(el: FreedrawElement): React.ReactNode {\n const size = (el.strokeWidth ?? 1.5) * 5;\n const inputPoints = el.points.map(([x, y, pressure]) => {\n const tx = el.x + x;\n const ty = el.y + y;\n return pressure === undefined ? [tx, ty] : [tx, ty, pressure];\n });\n const stroke = getStroke(inputPoints, {\n ...DEFAULT_STROKE_OPTIONS,\n size,\n });\n const d = svgPathFromStroke(stroke as number[][]);\n return (\n <g opacity={el.opacity ?? 1}>\n <path d={d} fill={el.stroke ?? \"currentColor\"} stroke=\"none\" />\n </g>\n );\n}\n"
69
+ },
70
+ {
71
+ "path": "components/primitives/whiteboard/render/text.tsx",
72
+ "type": "registry:ui",
73
+ "target": "components/ui/whiteboard/render/text.tsx",
74
+ "content": "import type { TextElement } from \"@/components/ui/whiteboard/schema\";\n\nconst FONT_STACKS: Record<NonNullable<TextElement[\"fontFamily\"]>, string> = {\n sans: \"ui-sans-serif, system-ui, sans-serif\",\n serif: \"ui-serif, Georgia, serif\",\n mono: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n hand: '\"Virgil\", \"Caveat\", \"Comic Sans MS\", cursive',\n};\n\nfunction textAnchor(align: TextElement[\"align\"]): \"start\" | \"middle\" | \"end\" {\n if (align === \"center\") return \"middle\";\n if (align === \"right\") return \"end\";\n return \"start\";\n}\n\nexport function renderText(el: TextElement): React.ReactNode {\n const fontSize = el.fontSize ?? 18;\n const fontFamily = FONT_STACKS[el.fontFamily ?? \"hand\"];\n const anchor = textAnchor(el.align);\n const lines = el.text.split(\"\\n\");\n // dy: first tspan at 0, subsequent at fontSize * 1.2 line-height.\n return (\n <g opacity={el.opacity ?? 1}>\n <text\n x={el.x}\n y={el.y}\n textAnchor={anchor}\n dominantBaseline=\"hanging\"\n fontSize={fontSize}\n fontFamily={fontFamily}\n fill={el.stroke ?? \"currentColor\"}\n >\n {lines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: text is split deterministically on '\\n' so the line index is the most stable key.\n <tspan key={i} x={el.x} dy={i === 0 ? 0 : fontSize * 1.2}>\n {line}\n </tspan>\n ))}\n </text>\n </g>\n );\n}\n"
75
+ },
76
+ {
77
+ "path": "components/primitives/whiteboard/render/rough-paths.ts",
78
+ "type": "registry:ui",
79
+ "target": "components/ui/whiteboard/render/rough-paths.ts",
80
+ "content": "/**\n * Thin adapter over `roughjs` — converts a `Drawable` into the array of\n * `<path>` descriptors we render in SVG. The actual `rough.generator()`\n * instance is created once per scene and reused across elements.\n */\nimport type { Drawable, Options } from \"roughjs/bin/core\";\nimport type { RoughGenerator } from \"roughjs/bin/generator\";\n\nexport interface RoughPath {\n d: string;\n stroke: string;\n strokeWidth: number;\n fill?: string;\n}\n\nexport type RoughOptions = Options;\n\n/** Convert a rough.js Drawable into renderable SVG path descriptors. */\nexport function toRoughPaths(generator: RoughGenerator, drawable: Drawable): RoughPath[] {\n const paths = generator.toPaths(drawable);\n return paths.map((p) => ({\n d: p.d,\n stroke: p.stroke,\n strokeWidth: p.strokeWidth,\n fill: p.fill,\n }));\n}\n"
81
+ },
82
+ {
83
+ "path": "components/primitives/whiteboard/render/style.ts",
84
+ "type": "registry:ui",
85
+ "target": "components/ui/whiteboard/render/style.ts",
86
+ "content": "/**\n * Maps a `WhiteboardElement` style props onto rough.js `Options`.\n */\nimport type { Options } from \"roughjs/bin/core\";\n\ninterface StyleSource {\n stroke?: string;\n strokeWidth?: number;\n strokeStyle?: \"solid\" | \"dashed\" | \"dotted\";\n fill?: string;\n fillStyle?: \"hachure\" | \"solid\" | \"cross-hatch\" | \"zigzag\";\n opacity?: number;\n roughness?: number;\n}\n\nconst DEFAULT_STROKE = \"currentColor\";\n\nexport function buildOptions(src: StyleSource, seed: number): Options {\n const o: Options = {\n seed,\n stroke: src.stroke ?? DEFAULT_STROKE,\n strokeWidth: src.strokeWidth ?? 1.5,\n roughness: src.roughness ?? 1.2,\n };\n if (src.strokeStyle === \"dashed\") o.strokeLineDash = [10, 6];\n else if (src.strokeStyle === \"dotted\") o.strokeLineDash = [2, 4];\n if (src.fill) o.fill = src.fill;\n if (src.fillStyle) o.fillStyle = src.fillStyle;\n return o;\n}\n\n/**\n * SVG `stroke-dasharray` value for a logical strokeStyle. rough.js's\n * `RoughGenerator.toPaths()` does NOT propagate `strokeLineDash` into the\n * returned `PathInfo` — only the canvas / direct-SVG backends apply it. We\n * must set the attribute ourselves on the rendered `<path>`. Returns\n * `undefined` for `solid` and any unknown value.\n */\nexport function strokeDashArray(\n strokeStyle: StyleSource[\"strokeStyle\"],\n strokeWidth: number,\n): string | undefined {\n if (strokeStyle === \"dashed\") {\n const dash = Math.max(8, strokeWidth * 6);\n const gap = Math.max(5, strokeWidth * 4);\n return `${dash} ${gap}`;\n }\n if (strokeStyle === \"dotted\") {\n // 1×strokeWidth dot, ~2×strokeWidth gap. Round end caps make these look\n // like dots instead of squares.\n return `${strokeWidth} ${strokeWidth * 2.5}`;\n }\n return undefined;\n}\n\nexport { DEFAULT_STROKE };\n"
87
+ },
88
+ {
89
+ "path": "components/primitives/whiteboard/viewport/use-pointer-pan.ts",
90
+ "type": "registry:ui",
91
+ "target": "components/ui/whiteboard/viewport/use-pointer-pan.ts",
92
+ "content": "import { useCallback, useEffect, useRef } from \"react\";\nimport type { ViewportControls } from \"@/components/ui/whiteboard/viewport/use-viewport\";\n\ninterface DragState {\n pointerId: number;\n lastX: number;\n lastY: number;\n}\n\n/** Attaches pan + zoom listeners to an SVG element. */\nexport function usePointerPan(\n ref: React.RefObject<SVGSVGElement | null>,\n viewport: ViewportControls,\n size: { width: number; height: number },\n): {\n onPointerDown: (e: React.PointerEvent<SVGSVGElement>) => void;\n onPointerMove: (e: React.PointerEvent<SVGSVGElement>) => void;\n onPointerUp: (e: React.PointerEvent<SVGSVGElement>) => void;\n onPointerCancel: (e: React.PointerEvent<SVGSVGElement>) => void;\n} {\n const dragRef = useRef<DragState | null>(null);\n const spaceHeldRef = useRef(false);\n\n // EC-2: wheel events on React's JSX onWheel are passive — `preventDefault`\n // does nothing, which causes the page to scroll while the user tries to\n // zoom. We must attach the wheel handler imperatively with `{passive:false}`.\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const handler = (e: WheelEvent) => {\n e.preventDefault();\n const rect = el.getBoundingClientRect();\n const localX = e.clientX - rect.left;\n const localY = e.clientY - rect.top;\n // Map screen-px → viewBox coords by ratio of rect to size.\n const scaleX = size.width / rect.width;\n const scaleY = size.height / rect.height;\n viewport.zoomAt(localX * scaleX, localY * scaleY, -e.deltaY * 0.001, size);\n };\n el.addEventListener(\"wheel\", handler, { passive: false });\n return () => el.removeEventListener(\"wheel\", handler);\n }, [ref, viewport, size]);\n\n // Track Space key for pan-with-any-button (Excalidraw \"hand\" mode).\n useEffect(() => {\n const down = (e: KeyboardEvent) => {\n if (e.code === \"Space\") spaceHeldRef.current = true;\n };\n const up = (e: KeyboardEvent) => {\n if (e.code === \"Space\") spaceHeldRef.current = false;\n };\n window.addEventListener(\"keydown\", down);\n window.addEventListener(\"keyup\", up);\n return () => {\n window.removeEventListener(\"keydown\", down);\n window.removeEventListener(\"keyup\", up);\n };\n }, []);\n\n const onPointerDown = useCallback((e: React.PointerEvent<SVGSVGElement>) => {\n // EC-10: only start drag when the down event is on our SVG, not on a\n // child rendered via portal/etc. event.currentTarget guarantees this.\n if (e.target instanceof Element && !e.currentTarget.contains(e.target)) return;\n // Left button (0) or middle (1) or Space-held pan.\n const isMouseButton = e.button === 0 || e.button === 1;\n if (!isMouseButton && e.pointerType === \"mouse\") return;\n if (e.pointerType === \"mouse\" && e.button !== 1 && !spaceHeldRef.current && e.button !== 0) {\n return;\n }\n dragRef.current = { pointerId: e.pointerId, lastX: e.clientX, lastY: e.clientY };\n e.currentTarget.setPointerCapture(e.pointerId);\n }, []);\n\n const onPointerMove = useCallback(\n (e: React.PointerEvent<SVGSVGElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n const dx = e.clientX - drag.lastX;\n const dy = e.clientY - drag.lastY;\n drag.lastX = e.clientX;\n drag.lastY = e.clientY;\n const el = ref.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n const scaleX = size.width / rect.width;\n const scaleY = size.height / rect.height;\n viewport.pan(dx * scaleX, dy * scaleY);\n },\n [ref, viewport, size],\n );\n\n const releaseDrag = useCallback((e: React.PointerEvent<SVGSVGElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n try {\n e.currentTarget.releasePointerCapture(e.pointerId);\n } catch {\n // Already released — ignore.\n }\n dragRef.current = null;\n }, []);\n\n return {\n onPointerDown,\n onPointerMove,\n onPointerUp: releaseDrag,\n onPointerCancel: releaseDrag,\n };\n}\n"
93
+ },
94
+ {
95
+ "path": "components/primitives/whiteboard/viewport/use-viewport.ts",
96
+ "type": "registry:ui",
97
+ "target": "components/ui/whiteboard/viewport/use-viewport.ts",
98
+ "content": "/**\n * Viewport state and helpers — pan via (x, y) and zoom via a scalar.\n *\n * Coordinate system:\n * - World space: where elements are defined (the JSON `x`, `y` etc.).\n * - Screen space: the rendered SVG bounding box on the page.\n * - viewBox: `${x} ${y} ${width/zoom} ${height/zoom}`. Increasing zoom\n * SHRINKS the viewBox dimensions, magnifying the rendered content.\n *\n * `zoomAt(screenX, screenY, delta, viewportSize)` keeps the world point under\n * the cursor stable while zooming (see test for invariant).\n */\nimport { useCallback, useMemo, useState } from \"react\";\n\nexport const MIN_ZOOM = 0.1;\nexport const MAX_ZOOM = 8;\n\nexport interface ViewportState {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport interface ViewportSize {\n width: number;\n height: number;\n}\n\nexport interface ViewportBounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\nexport interface UseViewportOptions {\n width: number;\n height: number;\n initialZoom?: number;\n initialCenter?: [number, number];\n}\n\nexport interface ViewportControls {\n state: ViewportState;\n pan: (dx: number, dy: number) => void;\n setZoom: (zoom: number) => void;\n zoomAt: (screenX: number, screenY: number, delta: number, size: ViewportSize) => void;\n reset: () => void;\n fitTo: (bounds: ViewportBounds, size: ViewportSize) => void;\n viewBox: (size: ViewportSize) => string;\n}\n\nfunction clampZoom(z: number): number {\n if (!Number.isFinite(z)) return 1;\n return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z));\n}\n\nexport function useViewport(opts: UseViewportOptions): ViewportControls {\n const initial: ViewportState = useMemo(() => {\n const zoom = clampZoom(opts.initialZoom ?? 1);\n if (opts.initialCenter) {\n // Position viewBox so initialCenter sits at the visual center.\n const [cx, cy] = opts.initialCenter;\n return {\n x: cx - opts.width / (2 * zoom),\n y: cy - opts.height / (2 * zoom),\n zoom,\n };\n }\n return { x: 0, y: 0, zoom };\n }, [opts.initialCenter, opts.initialZoom, opts.width, opts.height]);\n\n const [state, setState] = useState<ViewportState>(initial);\n\n const pan = useCallback((dx: number, dy: number) => {\n setState((prev) => ({ ...prev, x: prev.x - dx / prev.zoom, y: prev.y - dy / prev.zoom }));\n }, []);\n\n const setZoom = useCallback((zoom: number) => {\n setState((prev) => ({ ...prev, zoom: clampZoom(zoom) }));\n }, []);\n\n const zoomAt = useCallback(\n (screenX: number, screenY: number, delta: number, _size: ViewportSize) => {\n setState((prev) => {\n const oldZoom = prev.zoom;\n const newZoom = clampZoom(prev.zoom * Math.exp(delta));\n if (newZoom === oldZoom) return prev;\n // World coordinate currently under cursor.\n const worldX = prev.x + screenX / oldZoom;\n const worldY = prev.y + screenY / oldZoom;\n // After zoom, we want the same screen point to map to the same world point.\n // viewBox.x + (screenX / newZoom) === worldX → x = worldX - screenX/newZoom.\n return {\n x: worldX - screenX / newZoom,\n y: worldY - screenY / newZoom,\n zoom: newZoom,\n };\n });\n },\n [],\n );\n\n const reset = useCallback(() => setState(initial), [initial]);\n\n const fitTo = useCallback((bounds: ViewportBounds, size: ViewportSize) => {\n const bboxWidth = Math.max(1, bounds.maxX - bounds.minX);\n const bboxHeight = Math.max(1, bounds.maxY - bounds.minY);\n const zoomX = size.width / bboxWidth;\n const zoomY = size.height / bboxHeight;\n const zoom = clampZoom(Math.min(zoomX, zoomY));\n const cx = (bounds.minX + bounds.maxX) / 2;\n const cy = (bounds.minY + bounds.maxY) / 2;\n setState({\n x: cx - size.width / (2 * zoom),\n y: cy - size.height / (2 * zoom),\n zoom,\n });\n }, []);\n\n const viewBox = useCallback(\n (size: ViewportSize) =>\n `${state.x} ${state.y} ${size.width / state.zoom} ${size.height / state.zoom}`,\n [state],\n );\n\n return { state, pan, setZoom, zoomAt, reset, fitTo, viewBox };\n}\n"
99
+ }
100
+ ]
101
+ }