@miragon/mcp-toolkit-ui 2.6.0 → 2.8.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 (511) hide show
  1. package/README.md +14 -5
  2. package/dist/app/adapt-data-widget.d.ts +1 -22
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -1
  4. package/dist/app/adapt-data-widget.js +1 -35
  5. package/dist/app/adapt-data-widget.js.map +1 -1
  6. package/dist/app/builder/BuilderBanners.d.ts +7 -2
  7. package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
  8. package/dist/app/builder/BuilderBanners.js +8 -3
  9. package/dist/app/builder/BuilderBanners.js.map +1 -1
  10. package/dist/app/builder/CanvasRow.d.ts +4 -2
  11. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  12. package/dist/app/builder/CanvasRow.js +29 -17
  13. package/dist/app/builder/CanvasRow.js.map +1 -1
  14. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  15. package/dist/app/builder/CatalogueSheet.js +14 -6
  16. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  17. package/dist/app/builder/CellPropsSheet.d.ts +7 -3
  18. package/dist/app/builder/CellPropsSheet.d.ts.map +1 -1
  19. package/dist/app/builder/CellPropsSheet.js +10 -3
  20. package/dist/app/builder/CellPropsSheet.js.map +1 -1
  21. package/dist/app/builder/DiscardDialog.d.ts +18 -0
  22. package/dist/app/builder/DiscardDialog.d.ts.map +1 -0
  23. package/dist/app/builder/DiscardDialog.js +18 -0
  24. package/dist/app/builder/DiscardDialog.js.map +1 -0
  25. package/dist/app/builder/EmptyHint.d.ts +4 -0
  26. package/dist/app/builder/EmptyHint.d.ts.map +1 -1
  27. package/dist/app/builder/EmptyHint.js +6 -1
  28. package/dist/app/builder/EmptyHint.js.map +1 -1
  29. package/dist/app/builder/LayoutSurface.d.ts +33 -9
  30. package/dist/app/builder/LayoutSurface.d.ts.map +1 -1
  31. package/dist/app/builder/LayoutSurface.js +74 -23
  32. package/dist/app/builder/LayoutSurface.js.map +1 -1
  33. package/dist/app/builder/PipelineStrip.d.ts +5 -2
  34. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  35. package/dist/app/builder/PipelineStrip.js +33 -15
  36. package/dist/app/builder/PipelineStrip.js.map +1 -1
  37. package/dist/app/builder/SaveDialog.d.ts +9 -1
  38. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  39. package/dist/app/builder/SaveDialog.js +9 -3
  40. package/dist/app/builder/SaveDialog.js.map +1 -1
  41. package/dist/app/builder/Toolbar.d.ts +14 -4
  42. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  43. package/dist/app/builder/Toolbar.js +24 -7
  44. package/dist/app/builder/Toolbar.js.map +1 -1
  45. package/dist/app/builder/Workspace.d.ts +23 -3
  46. package/dist/app/builder/Workspace.d.ts.map +1 -1
  47. package/dist/app/builder/Workspace.js +58 -8
  48. package/dist/app/builder/Workspace.js.map +1 -1
  49. package/dist/app/builder/builder-model.d.ts +146 -3
  50. package/dist/app/builder/builder-model.d.ts.map +1 -1
  51. package/dist/app/builder/builder-model.js +196 -5
  52. package/dist/app/builder/builder-model.js.map +1 -1
  53. package/dist/app/builder/labels.d.ts +13 -62
  54. package/dist/app/builder/labels.d.ts.map +1 -1
  55. package/dist/app/builder/labels.js +9 -58
  56. package/dist/app/builder/labels.js.map +1 -1
  57. package/dist/app/builder/reveal.d.ts +10 -0
  58. package/dist/app/builder/reveal.d.ts.map +1 -0
  59. package/dist/app/builder/reveal.js +10 -0
  60. package/dist/app/builder/reveal.js.map +1 -0
  61. package/dist/app/builder/status-chip.d.ts.map +1 -1
  62. package/dist/app/builder/status-chip.js +3 -2
  63. package/dist/app/builder/status-chip.js.map +1 -1
  64. package/dist/app/builder/use-builder-catalogue.d.ts +5 -1
  65. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  66. package/dist/app/builder/use-builder-catalogue.js +50 -17
  67. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  68. package/dist/app/builder/use-builder-session.d.ts +30 -0
  69. package/dist/app/builder/use-builder-session.d.ts.map +1 -0
  70. package/dist/app/builder/use-builder-session.js +58 -0
  71. package/dist/app/builder/use-builder-session.js.map +1 -0
  72. package/dist/app/builder/use-display-resolver.d.ts +17 -0
  73. package/dist/app/builder/use-display-resolver.d.ts.map +1 -0
  74. package/dist/app/builder/use-display-resolver.js +16 -0
  75. package/dist/app/builder/use-display-resolver.js.map +1 -0
  76. package/dist/app/builder/use-save-dashboard.d.ts +3 -1
  77. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
  78. package/dist/app/builder/use-save-dashboard.js +3 -1
  79. package/dist/app/builder/use-save-dashboard.js.map +1 -1
  80. package/dist/app/index.d.ts +8 -3
  81. package/dist/app/index.d.ts.map +1 -1
  82. package/dist/app/index.js +8 -2
  83. package/dist/app/index.js.map +1 -1
  84. package/dist/app/layout-builder.d.ts +18 -25
  85. package/dist/app/layout-builder.d.ts.map +1 -1
  86. package/dist/app/layout-builder.js +84 -78
  87. package/dist/app/layout-builder.js.map +1 -1
  88. package/dist/app/mcp-app-view-chrome.d.ts +103 -20
  89. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  90. package/dist/app/mcp-app-view-chrome.js +125 -40
  91. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  92. package/dist/app/mcp-app-view-states.d.ts +23 -0
  93. package/dist/app/mcp-app-view-states.d.ts.map +1 -0
  94. package/dist/app/mcp-app-view-states.js +55 -0
  95. package/dist/app/mcp-app-view-states.js.map +1 -0
  96. package/dist/app/mcp-app-view.d.ts +72 -5
  97. package/dist/app/mcp-app-view.d.ts.map +1 -1
  98. package/dist/app/mcp-app-view.js +241 -141
  99. package/dist/app/mcp-app-view.js.map +1 -1
  100. package/dist/app/mcp-toolkit-app.d.ts +8 -2
  101. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  102. package/dist/app/mcp-toolkit-app.js +33 -4
  103. package/dist/app/mcp-toolkit-app.js.map +1 -1
  104. package/dist/app/mcp-use-host-bridge.d.ts +15 -6
  105. package/dist/app/mcp-use-host-bridge.d.ts.map +1 -1
  106. package/dist/app/mcp-use-host-bridge.js +51 -22
  107. package/dist/app/mcp-use-host-bridge.js.map +1 -1
  108. package/dist/app/model-context.d.ts +71 -0
  109. package/dist/app/model-context.d.ts.map +1 -0
  110. package/dist/app/model-context.js +101 -0
  111. package/dist/app/model-context.js.map +1 -0
  112. package/dist/app/use-build-mode-focus.d.ts +33 -0
  113. package/dist/app/use-build-mode-focus.d.ts.map +1 -0
  114. package/dist/app/use-build-mode-focus.js +59 -0
  115. package/dist/app/use-build-mode-focus.js.map +1 -0
  116. package/dist/app/use-view-actions.d.ts +31 -0
  117. package/dist/app/use-view-actions.d.ts.map +1 -0
  118. package/dist/app/use-view-actions.js +81 -0
  119. package/dist/app/use-view-actions.js.map +1 -0
  120. package/dist/app/use-view-lifecycle.d.ts +42 -0
  121. package/dist/app/use-view-lifecycle.d.ts.map +1 -0
  122. package/dist/app/use-view-lifecycle.js +81 -0
  123. package/dist/app/use-view-lifecycle.js.map +1 -0
  124. package/dist/app/view-lifecycle.d.ts +185 -0
  125. package/dist/app/view-lifecycle.d.ts.map +1 -0
  126. package/dist/app/view-lifecycle.js +207 -0
  127. package/dist/app/view-lifecycle.js.map +1 -0
  128. package/dist/app/widget-fixture.d.ts +10 -3
  129. package/dist/app/widget-fixture.d.ts.map +1 -1
  130. package/dist/app/widget-fixture.js +21 -8
  131. package/dist/app/widget-fixture.js.map +1 -1
  132. package/dist/app/widget-props-sheet.d.ts +6 -10
  133. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  134. package/dist/app/widget-props-sheet.js +25 -20
  135. package/dist/app/widget-props-sheet.js.map +1 -1
  136. package/dist/app/widget-renderer.d.ts +41 -1
  137. package/dist/app/widget-renderer.d.ts.map +1 -1
  138. package/dist/app/widget-renderer.js +82 -31
  139. package/dist/app/widget-renderer.js.map +1 -1
  140. package/dist/components/DrillButton.d.ts +9 -6
  141. package/dist/components/DrillButton.d.ts.map +1 -1
  142. package/dist/components/DrillButton.js +9 -6
  143. package/dist/components/DrillButton.js.map +1 -1
  144. package/dist/components/EmptyState.d.ts +33 -0
  145. package/dist/components/EmptyState.d.ts.map +1 -0
  146. package/dist/components/EmptyState.js +19 -0
  147. package/dist/components/EmptyState.js.map +1 -0
  148. package/dist/components/ErrorState.d.ts +53 -0
  149. package/dist/components/ErrorState.d.ts.map +1 -0
  150. package/dist/components/ErrorState.js +77 -0
  151. package/dist/components/ErrorState.js.map +1 -0
  152. package/dist/components/FilterBar.d.ts +16 -5
  153. package/dist/components/FilterBar.d.ts.map +1 -1
  154. package/dist/components/FilterBar.js +13 -5
  155. package/dist/components/FilterBar.js.map +1 -1
  156. package/dist/components/GroupCard.d.ts.map +1 -1
  157. package/dist/components/GroupCard.js +2 -1
  158. package/dist/components/GroupCard.js.map +1 -1
  159. package/dist/components/KpiGrid.d.ts +70 -18
  160. package/dist/components/KpiGrid.d.ts.map +1 -1
  161. package/dist/components/KpiGrid.js +112 -23
  162. package/dist/components/KpiGrid.js.map +1 -1
  163. package/dist/components/ListFooter.d.ts +32 -4
  164. package/dist/components/ListFooter.d.ts.map +1 -1
  165. package/dist/components/ListFooter.js +35 -6
  166. package/dist/components/ListFooter.js.map +1 -1
  167. package/dist/components/PagedListFooter.d.ts +57 -0
  168. package/dist/components/PagedListFooter.d.ts.map +1 -0
  169. package/dist/components/PagedListFooter.js +27 -0
  170. package/dist/components/PagedListFooter.js.map +1 -0
  171. package/dist/components/Pills.d.ts +36 -12
  172. package/dist/components/Pills.d.ts.map +1 -1
  173. package/dist/components/Pills.js +29 -8
  174. package/dist/components/Pills.js.map +1 -1
  175. package/dist/components/SectionHeading.d.ts +3 -1
  176. package/dist/components/SectionHeading.d.ts.map +1 -1
  177. package/dist/components/SectionHeading.js +6 -2
  178. package/dist/components/SectionHeading.js.map +1 -1
  179. package/dist/components/Toast.d.ts +43 -0
  180. package/dist/components/Toast.d.ts.map +1 -0
  181. package/dist/components/Toast.js +106 -0
  182. package/dist/components/Toast.js.map +1 -0
  183. package/dist/components/WidgetHeader.d.ts +34 -10
  184. package/dist/components/WidgetHeader.d.ts.map +1 -1
  185. package/dist/components/WidgetHeader.js +11 -6
  186. package/dist/components/WidgetHeader.js.map +1 -1
  187. package/dist/hooks/index.d.ts +4 -4
  188. package/dist/hooks/index.d.ts.map +1 -1
  189. package/dist/hooks/index.js +1 -1
  190. package/dist/hooks/index.js.map +1 -1
  191. package/dist/hooks/tool-result-recovery-model.d.ts +155 -0
  192. package/dist/hooks/tool-result-recovery-model.d.ts.map +1 -0
  193. package/dist/hooks/tool-result-recovery-model.js +273 -0
  194. package/dist/hooks/tool-result-recovery-model.js.map +1 -0
  195. package/dist/hooks/use-tool-query.d.ts +56 -14
  196. package/dist/hooks/use-tool-query.d.ts.map +1 -1
  197. package/dist/hooks/use-tool-query.js +102 -25
  198. package/dist/hooks/use-tool-query.js.map +1 -1
  199. package/dist/hooks/use-tool-result-recovery.d.ts +61 -44
  200. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  201. package/dist/hooks/use-tool-result-recovery.js +62 -110
  202. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  203. package/dist/hooks/use-view-data.d.ts +55 -7
  204. package/dist/hooks/use-view-data.d.ts.map +1 -1
  205. package/dist/hooks/use-view-data.js +81 -11
  206. package/dist/hooks/use-view-data.js.map +1 -1
  207. package/dist/hooks/use-view-tool-query.d.ts +12 -7
  208. package/dist/hooks/use-view-tool-query.d.ts.map +1 -1
  209. package/dist/hooks/use-view-tool-query.js +15 -14
  210. package/dist/hooks/use-view-tool-query.js.map +1 -1
  211. package/dist/index.d.ts +27 -13
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +29 -7
  214. package/dist/index.js.map +1 -1
  215. package/dist/lib/focus-return.d.ts +61 -0
  216. package/dist/lib/focus-return.d.ts.map +1 -0
  217. package/dist/lib/focus-return.js +46 -0
  218. package/dist/lib/focus-return.js.map +1 -0
  219. package/dist/lib/focus-ring.d.ts +18 -0
  220. package/dist/lib/focus-ring.d.ts.map +1 -0
  221. package/dist/lib/focus-ring.js +18 -0
  222. package/dist/lib/focus-ring.js.map +1 -0
  223. package/dist/lib/icon-stroke.d.ts +12 -0
  224. package/dist/lib/icon-stroke.d.ts.map +1 -0
  225. package/dist/lib/icon-stroke.js +12 -0
  226. package/dist/lib/icon-stroke.js.map +1 -0
  227. package/dist/lib/parse-tool-result.d.ts +23 -1
  228. package/dist/lib/parse-tool-result.d.ts.map +1 -1
  229. package/dist/lib/parse-tool-result.js +38 -2
  230. package/dist/lib/parse-tool-result.js.map +1 -1
  231. package/dist/lib/toast-model.d.ts +86 -0
  232. package/dist/lib/toast-model.d.ts.map +1 -0
  233. package/dist/lib/toast-model.js +102 -0
  234. package/dist/lib/toast-model.js.map +1 -0
  235. package/dist/lib/tone-utils.d.ts +95 -9
  236. package/dist/lib/tone-utils.d.ts.map +1 -1
  237. package/dist/lib/tone-utils.js +100 -9
  238. package/dist/lib/tone-utils.js.map +1 -1
  239. package/dist/lib/tool-query.d.ts +107 -0
  240. package/dist/lib/tool-query.d.ts.map +1 -0
  241. package/dist/lib/tool-query.js +182 -0
  242. package/dist/lib/tool-query.js.map +1 -0
  243. package/dist/lib/tool-result-observer.d.ts +108 -0
  244. package/dist/lib/tool-result-observer.d.ts.map +1 -0
  245. package/dist/lib/tool-result-observer.js +158 -0
  246. package/dist/lib/tool-result-observer.js.map +1 -0
  247. package/dist/lib/typography.d.ts +37 -0
  248. package/dist/lib/typography.d.ts.map +1 -0
  249. package/dist/lib/typography.js +41 -0
  250. package/dist/lib/typography.js.map +1 -0
  251. package/dist/lib/utils.d.ts.map +1 -1
  252. package/dist/lib/utils.js +11 -1
  253. package/dist/lib/utils.js.map +1 -1
  254. package/dist/locale/index.d.ts +3 -1
  255. package/dist/locale/index.d.ts.map +1 -1
  256. package/dist/locale/index.js +2 -1
  257. package/dist/locale/index.js.map +1 -1
  258. package/dist/locale/locale-provider.d.ts +42 -1
  259. package/dist/locale/locale-provider.d.ts.map +1 -1
  260. package/dist/locale/locale-provider.js +44 -3
  261. package/dist/locale/locale-provider.js.map +1 -1
  262. package/dist/locale/message-types.d.ts +336 -0
  263. package/dist/locale/message-types.d.ts.map +1 -0
  264. package/dist/locale/message-types.js +7 -0
  265. package/dist/locale/message-types.js.map +1 -0
  266. package/dist/locale/messages-de.d.ts +9 -0
  267. package/dist/locale/messages-de.d.ts.map +1 -0
  268. package/dist/locale/messages-de.js +185 -0
  269. package/dist/locale/messages-de.js.map +1 -0
  270. package/dist/locale/messages-en.d.ts +8 -0
  271. package/dist/locale/messages-en.d.ts.map +1 -0
  272. package/dist/locale/messages-en.js +184 -0
  273. package/dist/locale/messages-en.js.map +1 -0
  274. package/dist/locale/toolkit-messages.d.ts +57 -0
  275. package/dist/locale/toolkit-messages.d.ts.map +1 -0
  276. package/dist/locale/toolkit-messages.js +103 -0
  277. package/dist/locale/toolkit-messages.js.map +1 -0
  278. package/dist/primitives/alert.d.ts +1 -1
  279. package/dist/primitives/alert.d.ts.map +1 -1
  280. package/dist/primitives/alert.js +6 -2
  281. package/dist/primitives/alert.js.map +1 -1
  282. package/dist/primitives/badge.d.ts +1 -1
  283. package/dist/primitives/badge.d.ts.map +1 -1
  284. package/dist/primitives/badge.js +19 -6
  285. package/dist/primitives/badge.js.map +1 -1
  286. package/dist/primitives/button.d.ts +1 -1
  287. package/dist/primitives/button.d.ts.map +1 -1
  288. package/dist/primitives/button.js +12 -8
  289. package/dist/primitives/button.js.map +1 -1
  290. package/dist/primitives/card.js +4 -4
  291. package/dist/primitives/card.js.map +1 -1
  292. package/dist/primitives/dialog.d.ts +7 -3
  293. package/dist/primitives/dialog.d.ts.map +1 -1
  294. package/dist/primitives/dialog.js +22 -8
  295. package/dist/primitives/dialog.js.map +1 -1
  296. package/dist/primitives/input.d.ts.map +1 -1
  297. package/dist/primitives/input.js +4 -1
  298. package/dist/primitives/input.js.map +1 -1
  299. package/dist/primitives/scroll-area.js +1 -1
  300. package/dist/primitives/scroll-area.js.map +1 -1
  301. package/dist/primitives/select.d.ts.map +1 -1
  302. package/dist/primitives/select.js +6 -3
  303. package/dist/primitives/select.js.map +1 -1
  304. package/dist/primitives/sheet.d.ts +3 -1
  305. package/dist/primitives/sheet.d.ts.map +1 -1
  306. package/dist/primitives/sheet.js +17 -7
  307. package/dist/primitives/sheet.js.map +1 -1
  308. package/dist/primitives/skeleton.js +1 -1
  309. package/dist/primitives/skeleton.js.map +1 -1
  310. package/dist/primitives/switch.d.ts.map +1 -1
  311. package/dist/primitives/switch.js +2 -1
  312. package/dist/primitives/switch.js.map +1 -1
  313. package/dist/primitives/tabs.d.ts.map +1 -1
  314. package/dist/primitives/tabs.js +5 -3
  315. package/dist/primitives/tabs.js.map +1 -1
  316. package/dist/providers/query-provider.d.ts +32 -1
  317. package/dist/providers/query-provider.d.ts.map +1 -1
  318. package/dist/providers/query-provider.js +53 -4
  319. package/dist/providers/query-provider.js.map +1 -1
  320. package/dist/render-cases-primitives.js +1 -1
  321. package/dist/render-cases-primitives.js.map +1 -1
  322. package/dist/render-cases.d.ts.map +1 -1
  323. package/dist/render-cases.js +48 -6
  324. package/dist/render-cases.js.map +1 -1
  325. package/dist/render-effects.d.ts +42 -0
  326. package/dist/render-effects.d.ts.map +1 -0
  327. package/dist/render-effects.js +81 -0
  328. package/dist/render-effects.js.map +1 -0
  329. package/dist/render-scan.d.ts +25 -0
  330. package/dist/render-scan.d.ts.map +1 -0
  331. package/dist/render-scan.js +66 -0
  332. package/dist/render-scan.js.map +1 -0
  333. package/dist/theme/cd-tokens.d.ts +66 -0
  334. package/dist/theme/cd-tokens.d.ts.map +1 -0
  335. package/dist/theme/cd-tokens.js +64 -0
  336. package/dist/theme/cd-tokens.js.map +1 -0
  337. package/dist/theme/create-theme.d.ts +214 -39
  338. package/dist/theme/create-theme.d.ts.map +1 -1
  339. package/dist/theme/create-theme.js +204 -13
  340. package/dist/theme/create-theme.js.map +1 -1
  341. package/dist/theme/fonts.d.ts +38 -0
  342. package/dist/theme/fonts.d.ts.map +1 -0
  343. package/dist/theme/fonts.js +56 -0
  344. package/dist/theme/fonts.js.map +1 -0
  345. package/dist/theme/host-fonts.d.ts +39 -0
  346. package/dist/theme/host-fonts.d.ts.map +1 -0
  347. package/dist/theme/host-fonts.js +44 -0
  348. package/dist/theme/host-fonts.js.map +1 -0
  349. package/dist/theme/index.d.ts +8 -1
  350. package/dist/theme/index.d.ts.map +1 -1
  351. package/dist/theme/index.js +5 -1
  352. package/dist/theme/index.js.map +1 -1
  353. package/dist/theme/portal-container.d.ts +16 -0
  354. package/dist/theme/portal-container.d.ts.map +1 -0
  355. package/dist/theme/portal-container.js +19 -0
  356. package/dist/theme/portal-container.js.map +1 -0
  357. package/dist/theme/presets.d.ts +24 -9
  358. package/dist/theme/presets.d.ts.map +1 -1
  359. package/dist/theme/presets.js +44 -11
  360. package/dist/theme/presets.js.map +1 -1
  361. package/dist/theme/theme-provider.d.ts +27 -9
  362. package/dist/theme/theme-provider.d.ts.map +1 -1
  363. package/dist/theme/theme-provider.js +42 -20
  364. package/dist/theme/theme-provider.js.map +1 -1
  365. package/package.json +11 -5
  366. package/src/app/adapt-data-widget.tsx +2 -42
  367. package/src/app/builder/BuilderBanners.tsx +26 -13
  368. package/src/app/builder/CanvasRow.tsx +79 -32
  369. package/src/app/builder/CatalogueSheet.tsx +58 -35
  370. package/src/app/builder/CellPropsSheet.tsx +17 -2
  371. package/src/app/builder/DiscardDialog.tsx +59 -0
  372. package/src/app/builder/EmptyHint.tsx +16 -11
  373. package/src/app/builder/LayoutSurface.tsx +207 -67
  374. package/src/app/builder/PipelineStrip.tsx +265 -156
  375. package/src/app/builder/SaveDialog.tsx +25 -14
  376. package/src/app/builder/Toolbar.tsx +154 -57
  377. package/src/app/builder/Workspace.tsx +226 -56
  378. package/src/app/builder/builder-feedback.test.tsx +75 -0
  379. package/src/app/builder/builder-model.test.ts +5 -0
  380. package/src/app/builder/builder-model.ts +346 -6
  381. package/src/app/builder/builder-model.ux.test.ts +456 -0
  382. package/src/app/builder/builder-ux.test.tsx +844 -0
  383. package/src/app/builder/labels.ts +15 -121
  384. package/src/app/builder/reveal.ts +10 -0
  385. package/src/app/builder/status-chip.tsx +8 -4
  386. package/src/app/builder/use-builder-catalogue.ts +62 -19
  387. package/src/app/builder/use-builder-session.ts +68 -0
  388. package/src/app/builder/use-display-resolver.ts +24 -0
  389. package/src/app/builder/use-save-dashboard.ts +5 -0
  390. package/src/app/index.ts +20 -3
  391. package/src/app/layout-builder.tsx +193 -186
  392. package/src/app/mcp-app-view-chrome.test.tsx +487 -14
  393. package/src/app/mcp-app-view-chrome.tsx +328 -100
  394. package/src/app/mcp-app-view-states.tsx +140 -0
  395. package/src/app/mcp-app-view.effects.test.tsx +292 -0
  396. package/src/app/mcp-app-view.test.tsx +478 -0
  397. package/src/app/mcp-app-view.tsx +447 -190
  398. package/src/app/mcp-toolkit-app.test.tsx +40 -0
  399. package/src/app/mcp-toolkit-app.tsx +39 -8
  400. package/src/app/mcp-use-host-bridge.test.tsx +180 -0
  401. package/src/app/mcp-use-host-bridge.tsx +67 -24
  402. package/src/app/model-context.test.tsx +116 -0
  403. package/src/app/model-context.tsx +125 -0
  404. package/src/app/use-build-mode-focus.ts +62 -0
  405. package/src/app/use-view-actions.ts +89 -0
  406. package/src/app/use-view-lifecycle.ts +122 -0
  407. package/src/app/view-lifecycle.test.ts +443 -0
  408. package/src/app/view-lifecycle.ts +334 -0
  409. package/src/app/widget-fixture.effects.test.tsx +51 -0
  410. package/src/app/widget-fixture.render.test.tsx +81 -0
  411. package/src/app/widget-fixture.tsx +34 -11
  412. package/src/app/widget-props-sheet.tsx +67 -39
  413. package/src/app/widget-renderer.render.test.tsx +131 -0
  414. package/src/app/widget-renderer.tsx +136 -38
  415. package/src/components/DrillButton.tsx +24 -10
  416. package/src/components/EmptyState.tsx +83 -0
  417. package/src/components/ErrorState.tsx +204 -0
  418. package/src/components/FilterBar.test.tsx +82 -0
  419. package/src/components/FilterBar.tsx +56 -27
  420. package/src/components/GroupCard.tsx +3 -2
  421. package/src/components/KpiGrid.test.tsx +279 -0
  422. package/src/components/KpiGrid.tsx +329 -91
  423. package/src/components/ListFooter.test.tsx +116 -0
  424. package/src/components/ListFooter.tsx +110 -12
  425. package/src/components/PagedListFooter.test.tsx +140 -0
  426. package/src/components/PagedListFooter.tsx +107 -0
  427. package/src/components/Pills.test.tsx +77 -0
  428. package/src/components/Pills.tsx +78 -16
  429. package/src/components/SectionHeading.tsx +9 -3
  430. package/src/components/Toast.tsx +206 -0
  431. package/src/components/WidgetHeader.test.tsx +78 -0
  432. package/src/components/WidgetHeader.tsx +63 -16
  433. package/src/components/feedback-states.test.tsx +314 -0
  434. package/src/components/tone-figures.test.tsx +58 -0
  435. package/src/fonts.css +19 -0
  436. package/src/globals.css +9 -165
  437. package/src/hooks/index.ts +6 -3
  438. package/src/hooks/tool-result-recovery-model.test.ts +744 -0
  439. package/src/hooks/tool-result-recovery-model.ts +418 -0
  440. package/src/hooks/use-tool-query.effects.test.tsx +205 -0
  441. package/src/hooks/use-tool-query.ts +156 -33
  442. package/src/hooks/use-tool-result-recovery.effects.test.tsx +141 -0
  443. package/src/hooks/use-tool-result-recovery.test.ts +28 -5
  444. package/src/hooks/use-tool-result-recovery.ts +101 -131
  445. package/src/hooks/use-view-data.test.tsx +169 -0
  446. package/src/hooks/use-view-data.ts +131 -10
  447. package/src/hooks/use-view-tool-query.ts +25 -19
  448. package/src/i18n-coverage.test.tsx +425 -0
  449. package/src/index.ts +102 -13
  450. package/src/lib/focus-return.test.ts +288 -0
  451. package/src/lib/focus-return.ts +96 -0
  452. package/src/lib/focus-ring.ts +20 -0
  453. package/src/lib/icon-stroke.test.ts +59 -0
  454. package/src/lib/icon-stroke.ts +13 -0
  455. package/src/lib/parse-tool-result.test.ts +46 -1
  456. package/src/lib/parse-tool-result.ts +40 -2
  457. package/src/lib/toast-model.test.ts +233 -0
  458. package/src/lib/toast-model.ts +165 -0
  459. package/src/lib/tone-utils.test.ts +125 -16
  460. package/src/lib/tone-utils.ts +144 -15
  461. package/src/lib/tool-query.test.ts +569 -0
  462. package/src/lib/tool-query.ts +253 -0
  463. package/src/lib/tool-result-observer.test.ts +296 -0
  464. package/src/lib/tool-result-observer.ts +211 -0
  465. package/src/lib/typography.test.ts +45 -0
  466. package/src/lib/typography.ts +47 -0
  467. package/src/lib/utils.ts +12 -1
  468. package/src/locale/index.ts +14 -1
  469. package/src/locale/locale-provider.test.tsx +87 -0
  470. package/src/locale/locale-provider.tsx +90 -3
  471. package/src/locale/message-types.ts +337 -0
  472. package/src/locale/messages-de.ts +198 -0
  473. package/src/locale/messages-en.ts +196 -0
  474. package/src/locale/toolkit-messages.test.ts +349 -0
  475. package/src/locale/toolkit-messages.ts +143 -0
  476. package/src/no-hardcoded-copy.test.ts +432 -0
  477. package/src/primitives/alert.tsx +7 -2
  478. package/src/primitives/badge.tsx +19 -7
  479. package/src/primitives/button.tsx +13 -8
  480. package/src/primitives/card.tsx +4 -4
  481. package/src/primitives/dialog.tsx +42 -8
  482. package/src/primitives/input.tsx +5 -3
  483. package/src/primitives/scroll-area.tsx +1 -1
  484. package/src/primitives/select.tsx +7 -4
  485. package/src/primitives/sheet.tsx +33 -6
  486. package/src/primitives/skeleton.tsx +1 -1
  487. package/src/primitives/switch.tsx +4 -2
  488. package/src/primitives/tabs.tsx +9 -6
  489. package/src/providers/query-provider.test.tsx +154 -0
  490. package/src/providers/query-provider.tsx +66 -5
  491. package/src/render-cases-primitives.tsx +1 -1
  492. package/src/render-cases.tsx +82 -6
  493. package/src/render-effects.tsx +96 -0
  494. package/src/render-scan.ts +75 -0
  495. package/src/theme/cd-tokens.generated.css +48 -0
  496. package/src/theme/cd-tokens.test.ts +109 -0
  497. package/src/theme/cd-tokens.ts +67 -0
  498. package/src/theme/create-theme.test.ts +253 -5
  499. package/src/theme/create-theme.ts +341 -41
  500. package/src/theme/fonts.test.ts +85 -0
  501. package/src/theme/fonts.ts +72 -0
  502. package/src/theme/host-fonts.test.ts +135 -0
  503. package/src/theme/host-fonts.ts +52 -0
  504. package/src/theme/index.ts +15 -1
  505. package/src/theme/portal-container.ts +20 -0
  506. package/src/theme/presets.ts +44 -11
  507. package/src/theme/theme-css.test.ts +619 -0
  508. package/src/theme/theme-provider.test.tsx +82 -0
  509. package/src/theme/theme-provider.tsx +60 -24
  510. package/src/theme.css +344 -0
  511. package/ui-catalog.json +783 -49
@@ -0,0 +1,619 @@
1
+ /// <reference types="node" />
2
+ import { readFileSync } from "node:fs"
3
+ import { describe, expect, it } from "vitest"
4
+ import {
5
+ MODE_INVARIANT_VARS,
6
+ ROLE_TOKENS,
7
+ TOKEN_TO_CSS_VAR,
8
+ resolveThemeVars,
9
+ type ThemeVars,
10
+ } from "./create-theme.js"
11
+ import { TYPE_FLOOR_PX, TYPE_STEPS } from "../lib/typography.js"
12
+ import { HOST_FONT_VARIABLES } from "./host-fonts.js"
13
+ import { themePresets } from "./presets.js"
14
+
15
+ /**
16
+ * The role-token layer (`theme.css`) against its contract and the Miragon CI:
17
+ * every role defined in light and — for colours — dark, pointing only at the
18
+ * vendored `--cd-*` tokens, and every role/tone pair the kit actually renders
19
+ * meeting WCAG contrast (text 4.5:1, non-text 3:1) in BOTH modes, computed
20
+ * from the vendored token values — for the default theme and for every
21
+ * shipped preset.
22
+ */
23
+
24
+ const read = (rel: string) => readFileSync(new URL(rel, import.meta.url), "utf8")
25
+ const themeCss = read("../theme.css")
26
+ const globalsCss = read("../globals.css")
27
+ const fontsCss = read("../fonts.css")
28
+ const cdCss = read("./cd-tokens.generated.css")
29
+
30
+ const stripComments = (css: string) => css.replace(/\/\*[\s\S]*?\*\//g, "")
31
+ const rules = stripComments(themeCss)
32
+
33
+ const LIGHT_SELECTOR =
34
+ ':root, :where(.dark, [data-theme="dark"], [data-theme-mode="dark"]) [data-theme-mode="light"]'
35
+ const DARK_SELECTOR = '.dark, [data-theme="dark"], [data-theme-mode="dark"]'
36
+ const norm = (s: string) => s.replace(/\s+/g, " ").trim()
37
+
38
+ /** Every innermost block of theme.css: its normalized selector, offset and declarations. */
39
+ const BLOCKS = [...rules.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((match) => {
40
+ // The prelude runs back to the previous brace; drop finished statements
41
+ // (`@import …;`, `@custom-variant …;`) in front of the selector.
42
+ const selector = norm((match[1] as string).split(";").pop() ?? "")
43
+ const decls: Record<string, string> = {}
44
+ for (const decl of (match[2] as string).split(";")) {
45
+ const at = decl.indexOf(":")
46
+ if (at < 0) continue
47
+ const name = decl.slice(0, at).trim()
48
+ if (name.startsWith("--") || name === "color-scheme") decls[name] = norm(decl.slice(at + 1))
49
+ }
50
+ return { selector, offset: match.index, decls }
51
+ })
52
+
53
+ /** Declarations of the innermost block whose (normalized) selector is `selector`. */
54
+ function block(selector: string): Record<string, string> {
55
+ const found = BLOCKS.find((b) => b.selector === norm(selector))
56
+ if (!found) throw new Error(`theme.css has no block "${selector}"`)
57
+ return found.decls
58
+ }
59
+
60
+ const ROOT = block(":root")
61
+ const LIGHT = block(LIGHT_SELECTOR)
62
+ const DARK = block(DARK_SELECTOR)
63
+ const THEME_INLINE = block("@theme inline")
64
+ const THEME_REFERENCE = block("@theme inline reference")
65
+
66
+ const CD: Record<string, string> = {}
67
+ for (const m of cdCss.matchAll(/(--cd-[a-z0-9-]+):\s*([^;]+);/g))
68
+ CD[m[1] as string] = m[2] as string
69
+
70
+ // ── Colour maths (sRGB, WCAG 2.x relative luminance) ──────────────────────────
71
+
72
+ type Rgba = { r: number; g: number; b: number; a: number }
73
+
74
+ function hex(value: string): Rgba {
75
+ const h = value.trim().replace(/^#/, "")
76
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) throw new Error(`not a #rrggbb colour: ${value}`)
77
+ return {
78
+ r: parseInt(h.slice(0, 2), 16),
79
+ g: parseInt(h.slice(2, 4), 16),
80
+ b: parseInt(h.slice(4, 6), 16),
81
+ a: 1,
82
+ }
83
+ }
84
+
85
+ /** CSS Color 4 `oklch(L C H)` → sRGB (gamut-clipped), for preset values. */
86
+ function oklch(l: number, c: number, h: number): Rgba {
87
+ const rad = (h * Math.PI) / 180
88
+ const a = c * Math.cos(rad)
89
+ const b = c * Math.sin(rad)
90
+ const l3 = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3
91
+ const m3 = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3
92
+ const s3 = (l - 0.0894841775 * a - 1.291485548 * b) ** 3
93
+ const encode = (linear: number) => {
94
+ const v = Math.min(1, Math.max(0, linear))
95
+ return 255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055)
96
+ }
97
+ return {
98
+ r: encode(4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3),
99
+ g: encode(-1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3),
100
+ b: encode(-0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3),
101
+ a: 1,
102
+ }
103
+ }
104
+
105
+ const TRANSPARENT: Rgba = { r: 0, g: 0, b: 0, a: 0 }
106
+
107
+ /**
108
+ * A role value → colour. theme.css uses var(--cd-*), color-mix(in srgb, …) and
109
+ * transparent; a theme (preset) may also use #rrggbb and oklch(L C H).
110
+ */
111
+ function colour(value: string): Rgba {
112
+ const v = value.trim()
113
+ if (v === "transparent") return TRANSPARENT
114
+ const ref = /^var\((--cd-[a-z0-9-]+)\)$/.exec(v)
115
+ if (ref) {
116
+ const cd = CD[ref[1] as string]
117
+ if (!cd) throw new Error(`unknown token ${ref[1]}`)
118
+ return hex(cd)
119
+ }
120
+ if (v.startsWith("#")) return hex(v)
121
+ const lch = /^oklch\(([\d.]+) ([\d.]+) ([\d.]+)\)$/.exec(v)
122
+ if (lch) return oklch(Number(lch[1]), Number(lch[2]), Number(lch[3]))
123
+ const mix = /^color-mix\(in srgb, (.+) (\d+(?:\.\d+)?)%, (.+)\)$/.exec(v)
124
+ if (mix) {
125
+ const a = colour(mix[1] as string)
126
+ const b = colour(mix[3] as string)
127
+ const p = Number(mix[2]) / 100
128
+ // Premultiplied interpolation (CSS Color 5): mixing with transparent keeps
129
+ // the colour and scales its alpha.
130
+ const alpha = a.a * p + b.a * (1 - p)
131
+ if (alpha === 0) return TRANSPARENT
132
+ const ch = (k: "r" | "g" | "b") => (a[k] * a.a * p + b[k] * b.a * (1 - p)) / alpha
133
+ return { r: ch("r"), g: ch("g"), b: ch("b"), a: alpha }
134
+ }
135
+ throw new Error(`value outside the token grammar: ${v}`)
136
+ }
137
+
138
+ /** `top` painted over an opaque `bottom`. */
139
+ function over(top: Rgba, bottom: Rgba): Rgba {
140
+ const a = top.a
141
+ return {
142
+ r: top.r * a + bottom.r * (1 - a),
143
+ g: top.g * a + bottom.g * (1 - a),
144
+ b: top.b * a + bottom.b * (1 - a),
145
+ a: 1,
146
+ }
147
+ }
148
+
149
+ function luminance(c: Rgba): number {
150
+ const lin = (v: number) => {
151
+ const s = v / 255
152
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
153
+ }
154
+ return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b)
155
+ }
156
+
157
+ function contrast(a: Rgba, b: Rgba): number {
158
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number]
159
+ return (hi + 0.05) / (lo + 0.05)
160
+ }
161
+
162
+ type Mode = "light" | "dark"
163
+ const MODES: Mode[] = ["light", "dark"]
164
+
165
+ /**
166
+ * A role's value in `mode`: the theme scope's own variables first (`theme`,
167
+ * what resolveThemeVars puts on a ThemeProvider), then theme.css (dark falls
168
+ * back to light/root where not redeclared).
169
+ */
170
+ function role(name: string, mode: Mode, theme: ThemeVars = {}): string {
171
+ const value =
172
+ theme[name as `--${string}`] ??
173
+ (mode === "dark" ? DARK[name] : undefined) ??
174
+ LIGHT[name] ??
175
+ ROOT[name]
176
+ if (!value) throw new Error(`role ${name} is not defined for ${mode}`)
177
+ return value
178
+ }
179
+
180
+ /** A surface role resolved to an opaque colour: translucent surfaces sit on --background. */
181
+ function surface(name: string, mode: Mode, theme?: ThemeVars): Rgba {
182
+ const ground = colour(role("--background", mode, theme))
183
+ return over(colour(role(name, mode, theme)), ground)
184
+ }
185
+
186
+ /** Contrast of a foreground role on a surface role (the surface itself may be a tint). */
187
+ function ratio(fg: string, bg: string, mode: Mode, theme?: ThemeVars): number {
188
+ const back = surface(bg, mode, theme)
189
+ return contrast(over(colour(role(fg, mode, theme)), back), back)
190
+ }
191
+
192
+ // ── The contract ──────────────────────────────────────────────────────────────
193
+
194
+ const COLOUR_ROLES = ROLE_TOKENS.filter((name) => name in LIGHT)
195
+
196
+ describe("theme.css — the role-token contract", () => {
197
+ it("defines every contract role", () => {
198
+ for (const name of ROLE_TOKENS) {
199
+ expect(name in LIGHT || name in ROOT, `${name} missing from theme.css`).toBe(true)
200
+ }
201
+ })
202
+
203
+ it("defines every colour role for light AND dark", () => {
204
+ expect(COLOUR_ROLES.length).toBeGreaterThanOrEqual(25)
205
+ for (const name of COLOUR_ROLES) {
206
+ expect(name in DARK, `${name} has no dark value`).toBe(true)
207
+ }
208
+ })
209
+
210
+ it("points every role at --cd-* tokens only (never at another role; fonts also at the host's font)", () => {
211
+ const hostSlots = new Set<string>(Object.values(HOST_FONT_VARIABLES))
212
+ for (const decls of [ROOT, LIGHT, DARK]) {
213
+ for (const [name, value] of Object.entries(decls)) {
214
+ for (const ref of value.matchAll(/var\((--[a-z0-9-]+)/g)) {
215
+ const target = ref[1] as string
216
+ if (hostSlots.has(target)) {
217
+ // Only the font role may name the host's font, and only its own slot.
218
+ expect(HOST_FONT_VARIABLES[name as keyof typeof HOST_FONT_VARIABLES], name).toBe(target)
219
+ } else {
220
+ expect(target, `${name}: ${value}`).toMatch(/^--cd-/)
221
+ }
222
+ }
223
+ }
224
+ }
225
+ })
226
+
227
+ it("resolves every colour role to a colour from the vendored tokens", () => {
228
+ for (const mode of MODES) {
229
+ for (const name of COLOUR_ROLES) {
230
+ expect(() => colour(role(name, mode)), `${name} (${mode})`).not.toThrow()
231
+ }
232
+ }
233
+ })
234
+
235
+ it("maps the self-named utilities (rounded-*, shadow-*, font-*) onto the roles, not calc()", () => {
236
+ for (const name of ["--radius-sm", "--radius-md", "--radius-lg", "--radius-pill"]) {
237
+ expect(THEME_REFERENCE[name], name).toBe(`var(${name})`)
238
+ expect(THEME_INLINE, name).not.toHaveProperty(name)
239
+ }
240
+ for (const name of ["--shadow-1", "--shadow-2", "--shadow-3", "--font-sans", "--font-mono"]) {
241
+ expect(THEME_REFERENCE[name], name).toBe(`var(${name})`)
242
+ }
243
+ for (const value of Object.values(THEME_REFERENCE)) expect(value).not.toContain("calc(")
244
+ })
245
+
246
+ it("defines the type scale per CI §4 (page title h2, section h3, body, dense, small) with a 13 px floor", () => {
247
+ const px = (rem: string | undefined) => Number(/^([\d.]+)rem$/.exec(rem ?? "")?.[1]) * 16
248
+ const scale = Object.fromEntries(
249
+ TYPE_STEPS.map((step) => [
250
+ step,
251
+ {
252
+ px: px(ROOT[`--text-${step}`]),
253
+ lineHeight: ROOT[`--text-${step}--line-height`],
254
+ weight: ROOT[`--text-${step}--font-weight`],
255
+ },
256
+ ]),
257
+ )
258
+ expect(scale).toEqual({
259
+ title: { px: 28, lineHeight: "1.2", weight: "700" },
260
+ section: { px: 21, lineHeight: "1.3", weight: "600" },
261
+ body: { px: 16, lineHeight: "1.6", weight: undefined },
262
+ dense: { px: 14, lineHeight: "1.5", weight: undefined },
263
+ small: { px: 13, lineHeight: "1.5", weight: undefined },
264
+ })
265
+ for (const { px: size } of Object.values(scale)) {
266
+ expect(size).toBeGreaterThanOrEqual(TYPE_FLOOR_PX)
267
+ }
268
+ // Mode-independent roles of the contract, themeable like the radii.
269
+ for (const step of TYPE_STEPS) {
270
+ expect(ROLE_TOKENS).toContain(`--text-${step}`)
271
+ expect(MODE_INVARIANT_VARS.has(`--text-${step}`)).toBe(true)
272
+ expect(LIGHT, step).not.toHaveProperty(`--text-${step}`)
273
+ expect(DARK, step).not.toHaveProperty(`--text-${step}`)
274
+ }
275
+ })
276
+
277
+ it("maps the type-scale utilities (text-title … text-small) onto the roles, size, leading and weight", () => {
278
+ for (const step of TYPE_STEPS) {
279
+ expect(THEME_REFERENCE[`--text-${step}`], step).toBe(`var(--text-${step})`)
280
+ expect(THEME_REFERENCE[`--text-${step}--line-height`], step).toBe(
281
+ `var(--text-${step}--line-height)`,
282
+ )
283
+ }
284
+ for (const step of ["title", "section"]) {
285
+ expect(THEME_REFERENCE[`--text-${step}--font-weight`], step).toBe(
286
+ `var(--text-${step}--font-weight)`,
287
+ )
288
+ }
289
+ // Tailwind's own steps stay untouched (text-xs is still 12 px): the floor
290
+ // is a gate on the kit's sources, not a redefinition of a consumer's scale.
291
+ for (const name of Object.keys({ ...THEME_INLINE, ...THEME_REFERENCE })) {
292
+ expect(name).not.toMatch(/^--text-(?:xs|sm|base|lg|\d?xl)\b/)
293
+ }
294
+ })
295
+
296
+ it("exposes the new colour roles as utilities (bg-scrim, text-link, ring-focus, text-*-ink, …)", () => {
297
+ for (const name of [
298
+ "link",
299
+ "focus",
300
+ "primary-hover",
301
+ "scrim",
302
+ "destructive-foreground",
303
+ "success-ink",
304
+ "warning-ink",
305
+ "danger-ink",
306
+ "info-ink",
307
+ ]) {
308
+ expect(THEME_INLINE[`--color-${name}`], name).toBe(`var(--${name})`)
309
+ }
310
+ })
311
+
312
+ it("follows the NEAREST mode scope for dark: (dark scope, or a dark scope nested in a light island)", () => {
313
+ const D = String.raw`:is\(\.dark, \[data-theme="dark"\], \[data-theme-mode="dark"\]\)`
314
+ const L = String.raw`\[data-theme-mode="light"\]`
315
+ expect(rules).toMatch(
316
+ new RegExp(
317
+ String.raw`@custom-variant dark \(&:is\(${D} \*:not\(${L} \*\), ${L} ${D} \*:not\(${L} ${D} ${L} \*\)\)\);`,
318
+ ),
319
+ )
320
+ })
321
+
322
+ it("re-declares the light roles on a light island only inside a dark scope (a light document keeps a consumer's :root brand)", () => {
323
+ expect(BLOCKS.some((b) => b.selector === LIGHT_SELECTOR)).toBe(true)
324
+ // No rule re-declares roles on EVERY light scope: that would reset a
325
+ // consumer's `:root { --primary: … }` inside each light ThemeProvider.
326
+ for (const b of BLOCKS) {
327
+ const selectors = b.selector.split(/,(?![^(]*\))/).map(norm)
328
+ if (Object.keys(b.decls).some((d) => d.startsWith("--"))) {
329
+ expect(selectors, b.selector).not.toContain('[data-theme-mode="light"]')
330
+ }
331
+ }
332
+ })
333
+
334
+ it("declares the dark roles AFTER the light ones (same specificity on <html>: order decides)", () => {
335
+ const light = BLOCKS.find((b) => b.selector === norm(LIGHT_SELECTOR))
336
+ const dark = BLOCKS.find((b) => b.selector === norm(DARK_SELECTOR))
337
+ expect(light && dark && light.offset < dark.offset).toBe(true)
338
+ })
339
+
340
+ it("keeps MODE_INVARIANT_VARS in step with theme.css", () => {
341
+ for (const name of Object.values(TOKEN_TO_CSS_VAR)) {
342
+ const light = LIGHT[name] ?? ROOT[name]
343
+ const invariant = DARK[name] === undefined || DARK[name] === light
344
+ expect(MODE_INVARIANT_VARS.has(name), `${name} invariant=${invariant}`).toBe(invariant)
345
+ }
346
+ })
347
+
348
+ it("forces no font: no font import, no !important font-family, Geist named first", () => {
349
+ for (const css of [themeCss, globalsCss]) {
350
+ const r = stripComments(css)
351
+ expect(r).not.toMatch(/@fontsource/)
352
+ expect(r).not.toMatch(/font-family[^;}]*!important/)
353
+ }
354
+ expect(ROOT["--font-sans"]).toMatch(/^"Geist", /)
355
+ expect(ROOT["--font-mono"]).toMatch(/^"Geist Mono", /)
356
+ // The opt-in file is the only place that loads them.
357
+ expect(stripComments(fontsCss)).toMatch(/@import "@fontsource-variable\/geist\/wght\.css";/)
358
+ expect(stripComments(fontsCss)).toMatch(
359
+ /@import "@fontsource-variable\/geist-mono\/wght\.css";/,
360
+ )
361
+ })
362
+
363
+ it("puts the host's font right after Geist and before the system stack", () => {
364
+ expect(ROOT["--font-sans"]).toBe(
365
+ '"Geist", "Geist Variable", var(--host-font-sans, ui-sans-serif), system-ui, sans-serif',
366
+ )
367
+ expect(ROOT["--font-mono"]).toBe(
368
+ '"Geist Mono", "Geist Mono Variable", var(--host-font-mono, ui-monospace), SFMono-Regular, Menlo, Consolas, monospace',
369
+ )
370
+ })
371
+
372
+ it("respects prefers-reduced-motion (modeler-tool-design §7)", () => {
373
+ expect(rules).toMatch(
374
+ /@media \(prefers-reduced-motion: reduce\) \{\s*\*,\s*\*::before,\s*\*::after \{\s*animation-duration: 0\.001ms !important;\s*animation-iteration-count: 1 !important;\s*transition-duration: 0\.001ms !important;/,
375
+ )
376
+ })
377
+
378
+ it("gives every focusable element an opaque focus outline in --focus", () => {
379
+ expect(rules).toMatch(/:focus-visible \{\s*outline: 2px solid var\(--focus\);/)
380
+ })
381
+
382
+ it("sets color-scheme per mode on <body> and the mode scopes, never on <html>", () => {
383
+ const schemes = BLOCKS.filter((b) => "color-scheme" in b.decls)
384
+ const light = schemes.find((b) => b.decls["color-scheme"] === "light")
385
+ const dark = schemes.find((b) => b.decls["color-scheme"] === "dark")
386
+ expect(light?.selector).toBe('body, :where(body) [data-theme-mode="light"]')
387
+ expect(dark?.selector).toBe(
388
+ ':is(.dark, [data-theme="dark"]) body, body:is(.dark, [data-theme="dark"], [data-theme-mode="dark"]), :where(body) :is(.dark, [data-theme="dark"], [data-theme-mode="dark"])',
389
+ )
390
+ // A root color-scheme that differs from the host's iframe paints an opaque
391
+ // canvas behind the view; mcp-use keeps it transparent on purpose.
392
+ for (const b of BLOCKS) {
393
+ if (b.selector.includes(":root") || /(^|[\s,])html\b/.test(b.selector)) {
394
+ expect(b.decls, b.selector).not.toHaveProperty("color-scheme")
395
+ }
396
+ }
397
+ })
398
+
399
+ it("is what globals.css loads, after Tailwind, with the vendored tokens", () => {
400
+ const g = stripComments(globalsCss)
401
+ expect(g.indexOf('@import "tailwindcss";')).toBeGreaterThanOrEqual(0)
402
+ expect(g.indexOf('@import "./theme.css";')).toBeGreaterThan(g.indexOf('@import "tailwindcss";'))
403
+ expect(rules).toContain('@import "./theme/cd-tokens.generated.css";')
404
+ })
405
+ })
406
+
407
+ // ── Contrast over every role/tone pair the kit renders ───────────────────────
408
+
409
+ const TONES = ["success", "warning", "danger", "info"] as const
410
+
411
+ /** Text pairs (WCAG 1.4.3, 4.5:1): [foreground role, surface role, where it renders]. */
412
+ const TEXT_PAIRS: [string, string, string][] = [
413
+ ["--foreground", "--background", "body text"],
414
+ ["--foreground", "--muted", "text on quiet surfaces (table heads, chips)"],
415
+ ["--card-foreground", "--card", "Card"],
416
+ ["--popover-foreground", "--popover", "Dialog, Sheet, Select menu"],
417
+ ["--secondary-foreground", "--secondary", "secondary Button/Badge"],
418
+ ["--accent-foreground", "--accent", "hovered outline/ghost Button, active FilterBar chip"],
419
+ ["--muted-foreground", "--background", "secondary text"],
420
+ ["--muted-foreground", "--card", "CardDescription, AlertDescription"],
421
+ ["--muted-foreground", "--popover", "SelectLabel, dialog descriptions"],
422
+ ["--muted-foreground", "--muted", "KpiGrid header, TabsList, TONE_SOFT.neutral"],
423
+ ["--primary-foreground", "--primary", "primary Button, default Badge"],
424
+ ["--primary-foreground", "--primary-hover", "hovered primary Button"],
425
+ ["--destructive-foreground", "--destructive", "destructive Button"],
426
+ ["--link", "--background", "link Button, links"],
427
+ ["--link", "--card", "links in cards"],
428
+ ["--link", "--popover", "links in dialogs"],
429
+ ["--link", "--muted", "links on quiet surfaces"],
430
+ ["--link", "--accent", "a link-coloured control while hovered (builder props button)"],
431
+ ["--sidebar-foreground", "--sidebar", "sidebar"],
432
+ ["--sidebar-primary-foreground", "--sidebar-primary", "sidebar primary"],
433
+ ["--sidebar-accent-foreground", "--sidebar-accent", "sidebar accent"],
434
+ ...TONES.flatMap((tone): [string, string, string][] => [
435
+ [`--foreground`, `--${tone}-soft`, `TONE_SOFT.${tone} (Badge, Pills, banners, Alert)`],
436
+ [`--${tone}-ink`, "--background", `TONE_TEXT.${tone}`],
437
+ [`--${tone}-ink`, "--card", `TONE_TEXT.${tone} in a card`],
438
+ [`--${tone}-ink`, "--popover", `TONE_TEXT.${tone} in a dialog or sheet`],
439
+ [`--${tone}-ink`, "--muted", `TONE_TEXT.${tone} on a quiet surface`],
440
+ ]),
441
+ ]
442
+
443
+ /**
444
+ * Text pairs that hold in LIGHT only, by design: dark lifts the hover/selected
445
+ * (--accent) and secondary surfaces 10 % so they stay visible, and
446
+ * --cd-kontur keeps 4.5:1 only up to a 3 % lift. On these surfaces quiet text
447
+ * takes the surface's own foreground (`group-hover:text-accent-foreground`);
448
+ * the design-system gate checks that a class string switching to one of these
449
+ * surfaces switches the text too. Pinned below so the exemption is dropped
450
+ * the day it is no longer needed.
451
+ */
452
+ const LIGHT_ONLY_TEXT_PAIRS: [string, string, string][] = [
453
+ ["--muted-foreground", "--accent", "quiet text on a hovered/selected item"],
454
+ ["--muted-foreground", "--secondary", "quiet text on a secondary surface"],
455
+ ]
456
+
457
+ /** Non-text pairs (WCAG 1.4.11, 3:1) that identify a control or its state. */
458
+ const NON_TEXT_PAIRS: [string, string, string][] = [
459
+ ["--input", "--background", "Input/Select border, unchecked Switch track"],
460
+ ["--input", "--card", "inputs in cards"],
461
+ ["--input", "--popover", "inputs in dialogs and sheets"],
462
+ ["--input", "--muted", "inputs on quiet surfaces"],
463
+ ...["--background", "--card", "--popover", "--muted", "--accent"].flatMap(
464
+ (bg): [string, string, string][] => [
465
+ ["--focus", bg, "FOCUS_RING, :focus-visible outline"],
466
+ ["--ring", bg, "legacy ring utilities"],
467
+ ],
468
+ ),
469
+ ["--primary", "--background", "checked Switch track, active tab indicator"],
470
+ ["--primary", "--card", "checked Switch in a card"],
471
+ ["--primary", "--popover", "checked Switch in a sheet"],
472
+ ]
473
+
474
+ /**
475
+ * Status colours as fills/dots/borders that are NOT asserted on light, by CI
476
+ * rule (§3.3): success and amber reach only 1.67:1 / 1.83:1 on white, so a
477
+ * status is never colour alone — every toned dot, border or icon sits next to
478
+ * text that carries it. The test pins that the exemption is still needed.
479
+ */
480
+ const LIGHT_EXEMPT_STATUS = ["--success", "--warning"]
481
+
482
+ /**
483
+ * Where a status colour renders as a dot, border or icon (non-text, 3:1), per
484
+ * tone: the page (KpiGrid dot, EmptyState icon, Pills), an overlay card (the
485
+ * Toast's left edge and icon) and the tone's own tint (the ErrorState's rim and
486
+ * icon, the builder banners). Same CI exemption as above on light.
487
+ */
488
+ const STATUS_SURFACES = (tone: (typeof TONES)[number]): [string, string][] => [
489
+ ["--background", "dot, border or icon on the page"],
490
+ ["--popover", "Toast edge and icon"],
491
+ [`--${tone}-soft`, "ErrorState rim and icon on its own tint"],
492
+ ]
493
+
494
+ describe.each(MODES)("contrast in %s mode (computed from the vendored tokens)", (mode) => {
495
+ it.each(TEXT_PAIRS)("text %s on %s ≥ 4.5:1 — %s", (fg, bg) => {
496
+ expect(ratio(fg, bg, mode)).toBeGreaterThanOrEqual(4.5)
497
+ })
498
+
499
+ it.each(LIGHT_ONLY_TEXT_PAIRS)(
500
+ "text %s on %s ≥ 4.5:1 in light only (dark exemption still needed) — %s",
501
+ (fg, bg) => {
502
+ if (mode === "light") expect(ratio(fg, bg, mode)).toBeGreaterThanOrEqual(4.5)
503
+ else expect(ratio(fg, bg, mode)).toBeLessThan(4.5)
504
+ },
505
+ )
506
+
507
+ it.each(NON_TEXT_PAIRS)("non-text %s on %s ≥ 3:1 — %s", (fg, bg) => {
508
+ expect(ratio(fg, bg, mode)).toBeGreaterThanOrEqual(3)
509
+ })
510
+
511
+ it.each(
512
+ TONES.flatMap((tone) =>
513
+ STATUS_SURFACES(tone).map(([bg, where]): [string, string, string] => [
514
+ `--${tone}`,
515
+ bg,
516
+ where,
517
+ ]),
518
+ ),
519
+ )("status colour %s on %s ≥ 3:1 unless CI-exempt on light — %s", (name, bg) => {
520
+ const r = ratio(name, bg, mode)
521
+ if (mode === "light" && LIGHT_EXEMPT_STATUS.includes(name)) {
522
+ // Still exempt? If the CI ever lifts it to 3:1, drop the exemption.
523
+ expect(r).toBeLessThan(3)
524
+ } else {
525
+ expect(r).toBeGreaterThanOrEqual(3)
526
+ }
527
+ })
528
+ })
529
+
530
+ // The presets are customer-brand demos: their oklch values are data here.
531
+ /* brand-lint-disable */
532
+ const PRESETS = Object.entries(themePresets)
533
+
534
+ describe.each(PRESETS)("preset %s meets the same bar in both modes", (_name, preset) => {
535
+ it.each(MODES)("every text pair ≥ 4.5:1 and non-text pair ≥ 3:1 in %s", (mode) => {
536
+ const scope = resolveThemeVars(preset, mode)
537
+ for (const [fg, bg, where] of TEXT_PAIRS) {
538
+ expect(ratio(fg, bg, mode, scope), `${fg} on ${bg} (${where})`).toBeGreaterThanOrEqual(4.5)
539
+ }
540
+ for (const [fg, bg, where] of NON_TEXT_PAIRS) {
541
+ expect(ratio(fg, bg, mode, scope), `${fg} on ${bg} (${where})`).toBeGreaterThanOrEqual(3)
542
+ }
543
+ })
544
+
545
+ it("sets its own hover, link and focus in both modes (never the Miragon defaults)", () => {
546
+ for (const name of ["--primary-hover", "--link", "--focus"] as const) {
547
+ expect(preset.vars[name], `${name} light`).toBeDefined()
548
+ expect(preset.darkVars?.[name], `${name} dark`).toBeDefined()
549
+ }
550
+ })
551
+ })
552
+ /* brand-lint-enable */
553
+
554
+ describe("dark mode follows the CI dark-ground rules", () => {
555
+ it("grounds on --cd-schwarz with white text, green links, blue buttons, blau-hell focus", () => {
556
+ expect(DARK["--background"]).toBe("var(--cd-schwarz)")
557
+ expect(DARK["--foreground"]).toBe("var(--cd-weiss)")
558
+ expect(DARK["--link"]).toBe("var(--cd-gruen)")
559
+ expect(DARK["--primary"]).toBe("var(--cd-blau)")
560
+ expect(DARK["--primary-foreground"]).toBe("var(--cd-weiss)")
561
+ expect(DARK["--destructive-foreground"]).toBe("var(--cd-weiss)")
562
+ expect(DARK["--focus"]).toBe("var(--cd-blau-hell)")
563
+ expect(DARK["--muted-foreground"]).toBe("var(--cd-kontur)")
564
+ })
565
+
566
+ it("uses the -hell status values with no tinted fills", () => {
567
+ for (const tone of TONES) {
568
+ expect(DARK[`--${tone}`]).toBe(`var(--cd-${tone}-hell)`)
569
+ expect(DARK[`--${tone}-ink`]).toBe(`var(--cd-${tone}-hell)`)
570
+ expect(DARK[`--${tone}-soft`]).toBe("transparent")
571
+ }
572
+ })
573
+
574
+ it("lifts surfaces only with white mixed into --cd-schwarz", () => {
575
+ for (const name of ["--muted", "--secondary", "--accent"]) {
576
+ expect(DARK[name]).toMatch(
577
+ /^color-mix\(in srgb, var\(--cd-weiss\) \d+%, var\(--cd-schwarz\)\)$/,
578
+ )
579
+ }
580
+ })
581
+
582
+ it("keeps overlays on the ground (any lift drops a checked Switch below 3:1) and pushes the page back harder", () => {
583
+ expect(DARK["--popover"]).toBe("var(--cd-schwarz)")
584
+ const onLift = over(
585
+ colour(role("--primary", "dark")),
586
+ colour("color-mix(in srgb, var(--cd-weiss) 2%, var(--cd-schwarz))"),
587
+ )
588
+ // Why not lift: --cd-blau is 3.08:1 on the ground and under 3:1 from a 2 % lift.
589
+ expect(
590
+ contrast(onLift, colour("color-mix(in srgb, var(--cd-weiss) 2%, var(--cd-schwarz))")),
591
+ ).toBeLessThan(3)
592
+ const scrimAlpha = (mode: Mode) => colour(role("--scrim", mode)).a
593
+ expect(role("--scrim", "dark")).toMatch(
594
+ /^color-mix\(in srgb, var\(--cd-schwarz\) \d+%, transparent\)$/,
595
+ )
596
+ expect(scrimAlpha("dark")).toBeGreaterThan(scrimAlpha("light"))
597
+ })
598
+
599
+ it("draws the overlay edge in dark with the 3:1 input border (shadows vanish on the ground)", () => {
600
+ for (const name of ["dialog", "sheet", "select"]) {
601
+ const panel = read(`../primitives/${name}.tsx`)
602
+ .split("\n")
603
+ .find((line) => /\bbg-popover\b/.test(line))
604
+ expect(panel, name).toMatch(/\sdark:border-input\s/)
605
+ }
606
+ expect(ratio("--input", "--popover", "dark")).toBeGreaterThanOrEqual(3)
607
+ })
608
+ })
609
+
610
+ describe("light mode text never uses a status colour that fails on white", () => {
611
+ it("routes success/warning text to the foreground ink", () => {
612
+ expect(LIGHT["--success-ink"]).toBe("var(--cd-schwarz)")
613
+ expect(LIGHT["--warning-ink"]).toBe("var(--cd-schwarz)")
614
+ })
615
+
616
+ it("keeps the CI backdrop on light (modeler-tool-design §8)", () => {
617
+ expect(LIGHT["--scrim"]).toBe("color-mix(in srgb, var(--cd-schwarz) 42%, transparent)")
618
+ })
619
+ })