@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,233 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+ import {
3
+ createToast,
4
+ createToastTimer,
5
+ MAX_VISIBLE_TOASTS,
6
+ resolveToastTone,
7
+ TOAST_DURATION_MS,
8
+ TOAST_ERROR_DURATION_MS,
9
+ toastDuration,
10
+ toastReducer,
11
+ type ToastClock,
12
+ type ToastItem,
13
+ type ToastOptions,
14
+ } from "./toast-model.js"
15
+
16
+ const item = (id: number): ToastItem => createToast(id, { title: `t${id}` })
17
+
18
+ describe("toastDuration (CI §8: about 2.8 s, errors about 5 s)", () => {
19
+ it("defaults to 2.8 s and gives errors 5 s", () => {
20
+ expect(TOAST_DURATION_MS).toBe(2800)
21
+ expect(TOAST_ERROR_DURATION_MS).toBe(5000)
22
+ expect(toastDuration("success")).toBe(2800)
23
+ expect(toastDuration("info")).toBe(2800)
24
+ expect(toastDuration("warning")).toBe(2800)
25
+ expect(toastDuration("danger")).toBe(5000)
26
+ })
27
+
28
+ it("honours an explicit positive duration and ignores an unusable one", () => {
29
+ expect(toastDuration("danger", 1200)).toBe(1200)
30
+ expect(toastDuration("info", 0)).toBe(2800)
31
+ expect(toastDuration("info", -5)).toBe(2800)
32
+ expect(toastDuration("danger", Number.NaN)).toBe(5000)
33
+ expect(toastDuration("danger", Number.POSITIVE_INFINITY)).toBe(5000)
34
+ })
35
+ })
36
+
37
+ describe("createToast", () => {
38
+ it("defaults to the info tone and drops an empty description", () => {
39
+ expect(createToast(1, { title: "Saved" })).toEqual({
40
+ id: 1,
41
+ tone: "info",
42
+ title: "Saved",
43
+ duration: 2800,
44
+ })
45
+ expect(createToast(2, { title: "x", description: "" })).not.toHaveProperty("description")
46
+ })
47
+
48
+ it("keeps tone, description and the tone's duration", () => {
49
+ expect(createToast(3, { tone: "danger", title: "Failed", description: "503" })).toEqual({
50
+ id: 3,
51
+ tone: "danger",
52
+ title: "Failed",
53
+ description: "503",
54
+ duration: 5000,
55
+ })
56
+ })
57
+
58
+ it("resolves an untyped retired tone instead of keeping a name no icon exists for", () => {
59
+ // Plain JS or wire data: `error` is `danger`, with the error toast's 5 s.
60
+ const fromWire = { tone: "error", title: "Failed" } as unknown as ToastOptions
61
+ expect(createToast(4, fromWire)).toMatchObject({ tone: "danger", duration: 5000 })
62
+ const unknown = { tone: "purple", title: "x" } as unknown as ToastOptions
63
+ expect(createToast(5, unknown).tone).toBe("info")
64
+ })
65
+ })
66
+
67
+ describe("resolveToastTone (fail-soft for an untyped tone)", () => {
68
+ it("keeps the four toast tones and maps a retired name through resolveTone", () => {
69
+ for (const tone of ["success", "warning", "danger", "info"] as const) {
70
+ expect(resolveToastTone(tone)).toBe(tone)
71
+ }
72
+ expect(resolveToastTone("critical")).toBe("danger")
73
+ expect(resolveToastTone("Warn")).toBe("warning")
74
+ expect(resolveToastTone("ok")).toBe("success")
75
+ expect(resolveToastTone("notice")).toBe("info")
76
+ })
77
+
78
+ it("shows neutral, unknown and missing tones as info (a toast has no neutral edge)", () => {
79
+ expect(resolveToastTone("neutral")).toBe("info")
80
+ expect(resolveToastTone("secondary")).toBe("info")
81
+ expect(resolveToastTone("purple")).toBe("info")
82
+ expect(resolveToastTone(undefined)).toBe("info")
83
+ expect(resolveToastTone(42)).toBe("info")
84
+ })
85
+ })
86
+
87
+ describe("toastReducer", () => {
88
+ it("appends and keeps at most the newest MAX_VISIBLE_TOASTS", () => {
89
+ expect(MAX_VISIBLE_TOASTS).toBe(3)
90
+ let state: ToastItem[] = []
91
+ for (let id = 1; id <= 4; id++) state = toastReducer(state, { type: "add", toast: item(id) })
92
+ expect(state.map((t) => t.id)).toEqual([2, 3, 4])
93
+ })
94
+
95
+ it("dismisses by id and keeps the list identity when nothing matches", () => {
96
+ const state = [item(1), item(2)]
97
+ expect(toastReducer(state, { type: "dismiss", id: 1 }).map((t) => t.id)).toEqual([2])
98
+ expect(toastReducer(state, { type: "dismiss", id: 9 })).toBe(state)
99
+ })
100
+ })
101
+
102
+ describe("createToastTimer (a held toast resumes its REMAINING time)", () => {
103
+ beforeEach(() => {
104
+ vi.useFakeTimers()
105
+ })
106
+ afterEach(() => {
107
+ vi.useRealTimers()
108
+ })
109
+
110
+ it("expires once, after the full duration, when nothing holds it", () => {
111
+ const onExpire = vi.fn()
112
+ const timer = createToastTimer(2800, onExpire)
113
+ expect(timer.remaining()).toBe(2800)
114
+ vi.advanceTimersByTime(2799)
115
+ expect(onExpire).not.toHaveBeenCalled()
116
+ expect(timer.remaining()).toBe(1)
117
+ vi.advanceTimersByTime(1)
118
+ expect(onExpire).toHaveBeenCalledTimes(1)
119
+ expect(timer.remaining()).toBe(0)
120
+ vi.advanceTimersByTime(10_000)
121
+ expect(onExpire).toHaveBeenCalledTimes(1)
122
+ })
123
+
124
+ it("pauses while held and resumes with the time that was left, not a fresh duration", () => {
125
+ const onExpire = vi.fn()
126
+ const timer = createToastTimer(2800, onExpire)
127
+ vi.advanceTimersByTime(1000)
128
+ timer.hold("pointer")
129
+ expect(timer.remaining()).toBe(1800)
130
+ vi.advanceTimersByTime(60_000) // reading for a minute: nothing runs out
131
+ expect(onExpire).not.toHaveBeenCalled()
132
+ expect(timer.remaining()).toBe(1800)
133
+ timer.release("pointer")
134
+ vi.advanceTimersByTime(1799)
135
+ expect(onExpire).not.toHaveBeenCalled()
136
+ vi.advanceTimersByTime(1)
137
+ expect(onExpire).toHaveBeenCalledTimes(1)
138
+ })
139
+
140
+ it("keeps counting down across several holds (each resume continues the rest)", () => {
141
+ const onExpire = vi.fn()
142
+ const timer = createToastTimer(5000, onExpire)
143
+ vi.advanceTimersByTime(2000)
144
+ timer.hold("pointer")
145
+ vi.advanceTimersByTime(500)
146
+ timer.release("pointer")
147
+ vi.advanceTimersByTime(2000)
148
+ timer.hold("focus")
149
+ expect(timer.remaining()).toBe(1000)
150
+ timer.release("focus")
151
+ vi.advanceTimersByTime(999)
152
+ expect(onExpire).not.toHaveBeenCalled()
153
+ vi.advanceTimersByTime(1)
154
+ expect(onExpire).toHaveBeenCalledTimes(1)
155
+ })
156
+
157
+ it("stays held until EVERY reason is released (hover and keyboard focus overlap)", () => {
158
+ const onExpire = vi.fn()
159
+ const timer = createToastTimer(1000, onExpire)
160
+ timer.hold("pointer")
161
+ timer.hold("focus")
162
+ timer.hold("focus")
163
+ timer.release("pointer")
164
+ vi.advanceTimersByTime(5000)
165
+ expect(onExpire).not.toHaveBeenCalled()
166
+ timer.release("unknown")
167
+ vi.advanceTimersByTime(5000)
168
+ expect(onExpire).not.toHaveBeenCalled()
169
+ timer.release("focus")
170
+ vi.advanceTimersByTime(1000)
171
+ expect(onExpire).toHaveBeenCalledTimes(1)
172
+ })
173
+
174
+ it("never fires after cancel, released or not", () => {
175
+ const onExpire = vi.fn()
176
+ const timer = createToastTimer(1000, onExpire)
177
+ vi.advanceTimersByTime(400)
178
+ timer.cancel()
179
+ expect(timer.remaining()).toBe(600)
180
+ timer.release("pointer")
181
+ vi.advanceTimersByTime(5000)
182
+ expect(onExpire).not.toHaveBeenCalled()
183
+ const held = createToastTimer(1000, onExpire)
184
+ held.hold("focus")
185
+ held.cancel()
186
+ held.release("focus")
187
+ vi.advanceTimersByTime(5000)
188
+ expect(onExpire).not.toHaveBeenCalled()
189
+ })
190
+
191
+ it("expires at once for a non-positive duration", () => {
192
+ const onExpire = vi.fn()
193
+ const timer = createToastTimer(-50, onExpire)
194
+ expect(timer.remaining()).toBe(0)
195
+ vi.advanceTimersByTime(0)
196
+ expect(onExpire).toHaveBeenCalledTimes(1)
197
+ })
198
+
199
+ it("runs on an injected clock and clears the handle it scheduled", () => {
200
+ let now = 100
201
+ const pending = new Map<number, () => void>()
202
+ let nextHandle = 0
203
+ const cleared: unknown[] = []
204
+ const scheduled: number[] = []
205
+ const clock: ToastClock = {
206
+ now: () => now,
207
+ setTimeout: (callback, ms) => {
208
+ nextHandle += 1
209
+ pending.set(nextHandle, callback)
210
+ scheduled.push(ms)
211
+ return nextHandle
212
+ },
213
+ clearTimeout: (handle) => {
214
+ cleared.push(handle)
215
+ pending.delete(handle as number)
216
+ },
217
+ }
218
+ const onExpire = vi.fn()
219
+ const timer = createToastTimer(2800, onExpire, clock)
220
+ expect(scheduled).toEqual([2800])
221
+ now += 800
222
+ timer.hold("pointer")
223
+ expect(cleared).toEqual([1])
224
+ expect(pending.size).toBe(0)
225
+ now += 10_000
226
+ timer.release("pointer")
227
+ expect(scheduled).toEqual([2800, 2000])
228
+ now += 2000
229
+ pending.get(2)?.()
230
+ expect(onExpire).toHaveBeenCalledTimes(1)
231
+ expect(timer.remaining()).toBe(0)
232
+ })
233
+ })
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Pure model behind `ToastProvider` / `useToast`: the toast list as a reducer
3
+ * plus the timing rules, so both are testable without a DOM (the house path,
4
+ * FITNESS.md) and the component stays a thin view over them.
5
+ */
6
+
7
+ import { resolveTone } from "./tone-utils.js"
8
+
9
+ /** Status tone of a toast — the four status roles (`--success` … `--info`). */
10
+ export type ToastTone = "success" | "warning" | "danger" | "info"
11
+
12
+ /**
13
+ * The toast tone of any tone name: the four status tones as is, a retired
14
+ * name through `resolveTone` (`error` → `danger`), and `neutral` or anything
15
+ * unknown as `info`. Keeps a toast whose tone arrives untyped (plain JS, wire
16
+ * data) on the screen instead of crashing the view on a missing icon.
17
+ */
18
+ export function resolveToastTone(name: unknown): ToastTone {
19
+ const tone = resolveTone(name, "info")
20
+ return tone === "neutral" ? "info" : tone
21
+ }
22
+
23
+ /** What a caller passes to `useToast().show(...)`. */
24
+ export interface ToastOptions {
25
+ /**
26
+ * Status tone; drives the edge, the icon and the default duration. Default
27
+ * `"info"`. An untyped retired name still resolves ({@link resolveToastTone}).
28
+ */
29
+ tone?: ToastTone
30
+ /** The message. Say what happened, in one short sentence ("Job retried."). */
31
+ title: string
32
+ /** Optional second line (what you can do next, or the affected object). */
33
+ description?: string
34
+ /** Auto-dismiss after this many ms. Defaults per {@link toastDuration}. */
35
+ duration?: number
36
+ }
37
+
38
+ /** A toast as it sits in the list. */
39
+ export interface ToastItem {
40
+ id: number
41
+ tone: ToastTone
42
+ title: string
43
+ description?: string
44
+ duration: number
45
+ }
46
+
47
+ /** Default lifetime of a toast (CI §8: about 2.8 s). */
48
+ export const TOAST_DURATION_MS = 2800
49
+ /** Lifetime of an error toast (CI §8: about 5 s) — long enough to read the reason. */
50
+ export const TOAST_ERROR_DURATION_MS = 5000
51
+ /** At most this many toasts stack; the oldest leaves first. */
52
+ export const MAX_VISIBLE_TOASTS = 3
53
+
54
+ /** The lifetime of a toast: an explicit positive duration, else the tone's default. */
55
+ export function toastDuration(tone: ToastTone, explicit?: number): number {
56
+ if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) return explicit
57
+ return tone === "danger" ? TOAST_ERROR_DURATION_MS : TOAST_DURATION_MS
58
+ }
59
+
60
+ /** Normalise caller options into a list item. */
61
+ export function createToast(id: number, options: ToastOptions): ToastItem {
62
+ const tone = resolveToastTone(options.tone)
63
+ return {
64
+ id,
65
+ tone,
66
+ title: options.title,
67
+ ...(options.description ? { description: options.description } : {}),
68
+ duration: toastDuration(tone, options.duration),
69
+ }
70
+ }
71
+
72
+ export type ToastAction = { type: "add"; toast: ToastItem } | { type: "dismiss"; id: number }
73
+
74
+ /** Add appends (dropping the oldest beyond {@link MAX_VISIBLE_TOASTS}); dismiss removes by id. */
75
+ export function toastReducer(state: readonly ToastItem[], action: ToastAction): ToastItem[] {
76
+ if (action.type === "add") return [...state, action.toast].slice(-MAX_VISIBLE_TOASTS)
77
+ const next = state.filter((t) => t.id !== action.id)
78
+ return next.length === state.length ? (state as ToastItem[]) : next
79
+ }
80
+
81
+ /**
82
+ * The clock a {@link createToastTimer} runs on. The default reads the global
83
+ * timers at call time, so fake timers installed in a test take effect.
84
+ */
85
+ export interface ToastClock {
86
+ now: () => number
87
+ setTimeout: (callback: () => void, ms: number) => unknown
88
+ clearTimeout: (handle: unknown) => void
89
+ }
90
+
91
+ const SYSTEM_CLOCK: ToastClock = {
92
+ now: () => Date.now(),
93
+ setTimeout: (callback, ms) => globalThis.setTimeout(callback, ms),
94
+ clearTimeout: (handle) => globalThis.clearTimeout(handle as ReturnType<typeof setTimeout>),
95
+ }
96
+
97
+ /** The countdown of one toast (see {@link createToastTimer}). */
98
+ export interface ToastTimer {
99
+ /**
100
+ * Hold the toast for `reason` (`"pointer"`, `"focus"`): the countdown
101
+ * pauses and keeps the time that was left.
102
+ */
103
+ hold: (reason: string) => void
104
+ /** Release `reason`; once nothing holds the toast, it resumes with the time that was left. */
105
+ release: (reason: string) => void
106
+ /** The time left in ms — frozen while held. */
107
+ remaining: () => number
108
+ /** Stop for good (the toast left early or unmounted); `onExpire` never fires afterwards. */
109
+ cancel: () => void
110
+ }
111
+
112
+ /**
113
+ * A pausable countdown: starts at once, pauses while anything holds it
114
+ * (hover, keyboard focus — so a reader can finish), and on release RESUMES
115
+ * WITH THE REMAINING TIME instead of restarting the full duration. Fires
116
+ * `onExpire` once.
117
+ */
118
+ export function createToastTimer(
119
+ duration: number,
120
+ onExpire: () => void,
121
+ clock: ToastClock = SYSTEM_CLOCK,
122
+ ): ToastTimer {
123
+ let left = Math.max(duration, 0)
124
+ let startedAt = 0
125
+ let handle: unknown
126
+ let running = false
127
+ let finished = false
128
+ const holds = new Set<string>()
129
+
130
+ const elapsed = () => (running ? clock.now() - startedAt : 0)
131
+ const start = () => {
132
+ if (finished || running || holds.size > 0) return
133
+ running = true
134
+ startedAt = clock.now()
135
+ handle = clock.setTimeout(() => {
136
+ running = false
137
+ finished = true
138
+ left = 0
139
+ onExpire()
140
+ }, left)
141
+ }
142
+ const pause = () => {
143
+ if (!running) return
144
+ left = Math.max(left - elapsed(), 0)
145
+ running = false
146
+ clock.clearTimeout(handle)
147
+ }
148
+
149
+ start()
150
+ return {
151
+ hold: (reason) => {
152
+ holds.add(reason)
153
+ pause()
154
+ },
155
+ release: (reason) => {
156
+ holds.delete(reason)
157
+ start()
158
+ },
159
+ remaining: () => Math.max(left - elapsed(), 0),
160
+ cancel: () => {
161
+ pause()
162
+ finished = true
163
+ },
164
+ }
165
+ }
@@ -1,35 +1,144 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { TONE_DOT, TONE_SOFT, TONE_TEXT, type ToneVariant } from "./tone-utils.js"
2
+ import {
3
+ isToneVariant,
4
+ resolveTone,
5
+ TONE_ALIASES,
6
+ TONE_BORDER,
7
+ TONE_DOT,
8
+ TONE_ICON,
9
+ TONE_INK,
10
+ TONE_SOFT,
11
+ TONE_TEXT,
12
+ TONE_TINT,
13
+ TONE_VARIANTS,
14
+ type ToneVariant,
15
+ } from "./tone-utils.js"
3
16
 
4
17
  const ALL_TONES: ToneVariant[] = ["neutral", "info", "success", "warning", "danger"]
18
+ const STATUS_TONES = ["info", "success", "warning", "danger"] as const
19
+ const ALL_MAPS = { TONE_SOFT, TONE_TINT, TONE_DOT, TONE_INK, TONE_BORDER, TONE_ICON }
5
20
 
6
21
  describe("tone-utils", () => {
22
+ it("has ONE vocabulary of five tones, in severity order", () => {
23
+ expect(TONE_VARIANTS).toEqual(["danger", "warning", "success", "info", "neutral"])
24
+ expect([...TONE_VARIANTS].sort()).toEqual([...ALL_TONES].sort())
25
+ })
26
+
7
27
  it("covers every ToneVariant in each map (no missing tones)", () => {
8
- for (const map of [TONE_SOFT, TONE_DOT, TONE_TEXT]) {
28
+ for (const map of Object.values(ALL_MAPS)) {
9
29
  expect(Object.keys(map).sort()).toEqual([...ALL_TONES].sort())
10
30
  }
11
31
  })
12
32
 
13
33
  it("maps each tone to a non-empty class string in every map", () => {
14
- for (const tone of ALL_TONES) {
15
- expect(TONE_SOFT[tone], `soft:${tone}`).toBeTruthy()
16
- expect(TONE_DOT[tone], `dot:${tone}`).toBeTruthy()
17
- expect(TONE_TEXT[tone], `text:${tone}`).toBeTruthy()
34
+ for (const [name, map] of Object.entries(ALL_MAPS)) {
35
+ for (const tone of ALL_TONES) {
36
+ expect(map[tone], `${name}:${tone}`).toBeTruthy()
37
+ }
38
+ }
39
+ })
40
+
41
+ it("routes each status tone to its three roles (colour, tint, ink)", () => {
42
+ for (const tone of STATUS_TONES) {
43
+ expect(TONE_SOFT[tone]).toBe(`bg-${tone}-soft text-foreground`)
44
+ expect(TONE_TINT[tone]).toBe(`bg-${tone}-soft`)
45
+ expect(TONE_INK[tone]).toBe(`text-${tone}-ink`)
46
+ expect(TONE_DOT[tone]).toBe(`bg-${tone}`)
47
+ expect(TONE_BORDER[tone]).toBe(`border-${tone}`)
48
+ expect(TONE_ICON[tone]).toBe(`text-${tone}`)
49
+ }
50
+ })
51
+
52
+ it("keeps TONE_TEXT as the deprecated name of the very same TONE_INK map", () => {
53
+ expect(TONE_TEXT).toBe(TONE_INK)
54
+ })
55
+
56
+ it("never sets text in a raw status colour outside the icon map (CI §3.3)", () => {
57
+ // Success and warning reach 1.67:1 / 1.83:1 on white; text takes the
58
+ // tone's -ink role. TONE_ICON is the one deliberate exception (SVG
59
+ // currentColor, always next to a readable label).
60
+ const rawStatusText = /\btext-(success|warning|danger|info)(?![-\w])/
61
+ for (const [name, map] of Object.entries({
62
+ TONE_SOFT,
63
+ TONE_TINT,
64
+ TONE_DOT,
65
+ TONE_INK,
66
+ TONE_BORDER,
67
+ })) {
68
+ for (const tone of ALL_TONES) {
69
+ expect(map[tone], `${name}:${tone}`).not.toMatch(rawStatusText)
70
+ }
18
71
  }
19
72
  })
20
73
 
21
- it("routes each status tone to its matching design token", () => {
22
- expect(TONE_SOFT.danger).toBe("bg-danger-soft text-danger")
23
- expect(TONE_SOFT.warning).toBe("bg-warning-soft text-warning")
24
- expect(TONE_SOFT.success).toBe("bg-success-soft text-success")
25
- expect(TONE_SOFT.info).toBe("bg-info-soft text-info")
26
- expect(TONE_DOT.danger).toBe("bg-danger")
27
- expect(TONE_TEXT.success).toBe("text-success")
74
+ it("puts the foreground on every tint, never the ink or the status colour (CI §3.3)", () => {
75
+ // "-soft: Text darauf --cd-schwarz, Rand in der Statusfarbe" — the rim
76
+ // (TONE_BORDER) carries the tone; a neutral count on the muted tint is a
77
+ // figure and stays in the foreground too.
78
+ for (const tone of ALL_TONES) {
79
+ expect(TONE_SOFT[tone]).toBe(`${TONE_TINT[tone]} text-foreground`)
80
+ expect(TONE_TINT[tone]).not.toMatch(/\btext-/)
81
+ }
28
82
  })
29
83
 
30
- it("routes the neutral tone to the shadcn muted ramp", () => {
31
- expect(TONE_SOFT.neutral).toBe("bg-muted text-muted-foreground")
84
+ it("routes the neutral tone to the muted ramp", () => {
85
+ expect(TONE_SOFT.neutral).toBe("bg-muted text-foreground")
86
+ expect(TONE_TINT.neutral).toBe("bg-muted")
32
87
  expect(TONE_DOT.neutral).toBe("bg-muted-foreground")
33
- expect(TONE_TEXT.neutral).toBe("text-foreground")
88
+ expect(TONE_INK.neutral).toBe("text-foreground")
89
+ expect(TONE_BORDER.neutral).toBe("border-border")
90
+ expect(TONE_ICON.neutral).toBe("text-muted-foreground")
91
+ })
92
+ })
93
+
94
+ describe("the tone vocabulary: retired names map onto the five tones", () => {
95
+ it("pins every alias (one red, one amber: critical/error/destructive are danger)", () => {
96
+ expect(TONE_ALIASES).toEqual({
97
+ critical: "danger",
98
+ error: "danger",
99
+ destructive: "danger",
100
+ failure: "danger",
101
+ warn: "warning",
102
+ ok: "success",
103
+ positive: "success",
104
+ notice: "info",
105
+ default: "neutral",
106
+ muted: "neutral",
107
+ secondary: "neutral",
108
+ })
109
+ for (const tone of Object.values(TONE_ALIASES)) expect(isToneVariant(tone)).toBe(true)
110
+ for (const alias of Object.keys(TONE_ALIASES)) expect(isToneVariant(alias)).toBe(false)
111
+ })
112
+
113
+ it("keeps a tone as it is", () => {
114
+ for (const tone of ALL_TONES) expect(resolveTone(tone)).toBe(tone)
115
+ })
116
+
117
+ it("maps every alias onto its tone, ignoring case and padding", () => {
118
+ for (const [alias, tone] of Object.entries(TONE_ALIASES)) {
119
+ expect(resolveTone(alias), alias).toBe(tone)
120
+ }
121
+ expect(resolveTone("Critical")).toBe("danger")
122
+ expect(resolveTone(" WARN ")).toBe("warning")
123
+ expect(resolveTone(" Danger")).toBe("danger")
124
+ })
125
+
126
+ it("falls back to neutral (or the caller's fallback) for anything else", () => {
127
+ for (const unknown of ["", " ", "purple", "constructor", "toString", "__proto__"]) {
128
+ expect(resolveTone(unknown), JSON.stringify(unknown)).toBe("neutral")
129
+ }
130
+ for (const notAString of [undefined, null, 3, true, {}, ["danger"]]) {
131
+ expect(resolveTone(notAString)).toBe("neutral")
132
+ }
133
+ expect(resolveTone("purple", "info")).toBe("info")
134
+ expect(resolveTone(undefined, "warning")).toBe("warning")
135
+ expect(resolveTone("critical", "info")).toBe("danger")
136
+ })
137
+
138
+ it("isToneVariant accepts exactly the five tones", () => {
139
+ for (const tone of ALL_TONES) expect(isToneVariant(tone)).toBe(true)
140
+ for (const other of ["Danger", "critical", "", undefined, null, 1, { tone: "info" }]) {
141
+ expect(isToneVariant(other)).toBe(false)
142
+ }
34
143
  })
35
144
  })