@miragon/mcp-toolkit-ui 2.5.0 → 2.7.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 (348) hide show
  1. package/README.md +8 -2
  2. package/dist/app/builder/BuilderBanners.d.ts +7 -2
  3. package/dist/app/builder/BuilderBanners.d.ts.map +1 -1
  4. package/dist/app/builder/BuilderBanners.js +8 -3
  5. package/dist/app/builder/BuilderBanners.js.map +1 -1
  6. package/dist/app/builder/CanvasRow.d.ts.map +1 -1
  7. package/dist/app/builder/CanvasRow.js +12 -14
  8. package/dist/app/builder/CanvasRow.js.map +1 -1
  9. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -1
  10. package/dist/app/builder/CatalogueSheet.js +6 -5
  11. package/dist/app/builder/CatalogueSheet.js.map +1 -1
  12. package/dist/app/builder/EmptyHint.d.ts +4 -0
  13. package/dist/app/builder/EmptyHint.d.ts.map +1 -1
  14. package/dist/app/builder/EmptyHint.js +6 -1
  15. package/dist/app/builder/EmptyHint.js.map +1 -1
  16. package/dist/app/builder/LayoutSurface.js +3 -3
  17. package/dist/app/builder/LayoutSurface.js.map +1 -1
  18. package/dist/app/builder/PipelineStrip.d.ts +3 -1
  19. package/dist/app/builder/PipelineStrip.d.ts.map +1 -1
  20. package/dist/app/builder/PipelineStrip.js +11 -6
  21. package/dist/app/builder/PipelineStrip.js.map +1 -1
  22. package/dist/app/builder/SaveDialog.d.ts.map +1 -1
  23. package/dist/app/builder/SaveDialog.js +2 -2
  24. package/dist/app/builder/SaveDialog.js.map +1 -1
  25. package/dist/app/builder/Toolbar.d.ts +3 -1
  26. package/dist/app/builder/Toolbar.d.ts.map +1 -1
  27. package/dist/app/builder/Toolbar.js +9 -3
  28. package/dist/app/builder/Toolbar.js.map +1 -1
  29. package/dist/app/builder/Workspace.js +2 -2
  30. package/dist/app/builder/Workspace.js.map +1 -1
  31. package/dist/app/builder/builder-model.d.ts +3 -2
  32. package/dist/app/builder/builder-model.d.ts.map +1 -1
  33. package/dist/app/builder/builder-model.js +5 -4
  34. package/dist/app/builder/builder-model.js.map +1 -1
  35. package/dist/app/builder/labels.d.ts +13 -62
  36. package/dist/app/builder/labels.d.ts.map +1 -1
  37. package/dist/app/builder/labels.js +9 -58
  38. package/dist/app/builder/labels.js.map +1 -1
  39. package/dist/app/builder/status-chip.d.ts.map +1 -1
  40. package/dist/app/builder/status-chip.js +3 -2
  41. package/dist/app/builder/status-chip.js.map +1 -1
  42. package/dist/app/builder/use-builder-catalogue.d.ts +1 -0
  43. package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -1
  44. package/dist/app/builder/use-builder-catalogue.js +6 -0
  45. package/dist/app/builder/use-builder-catalogue.js.map +1 -1
  46. package/dist/app/builder/use-save-dashboard.d.ts +3 -1
  47. package/dist/app/builder/use-save-dashboard.d.ts.map +1 -1
  48. package/dist/app/builder/use-save-dashboard.js +3 -1
  49. package/dist/app/builder/use-save-dashboard.js.map +1 -1
  50. package/dist/app/index.d.ts +1 -1
  51. package/dist/app/index.d.ts.map +1 -1
  52. package/dist/app/index.js.map +1 -1
  53. package/dist/app/layout-builder.d.ts +7 -2
  54. package/dist/app/layout-builder.d.ts.map +1 -1
  55. package/dist/app/layout-builder.js +17 -5
  56. package/dist/app/layout-builder.js.map +1 -1
  57. package/dist/app/mcp-app-view-chrome.d.ts +94 -20
  58. package/dist/app/mcp-app-view-chrome.d.ts.map +1 -1
  59. package/dist/app/mcp-app-view-chrome.js +109 -40
  60. package/dist/app/mcp-app-view-chrome.js.map +1 -1
  61. package/dist/app/mcp-app-view-states.d.ts +15 -0
  62. package/dist/app/mcp-app-view-states.d.ts.map +1 -0
  63. package/dist/app/mcp-app-view-states.js +34 -0
  64. package/dist/app/mcp-app-view-states.js.map +1 -0
  65. package/dist/app/mcp-app-view.d.ts +40 -5
  66. package/dist/app/mcp-app-view.d.ts.map +1 -1
  67. package/dist/app/mcp-app-view.js +174 -128
  68. package/dist/app/mcp-app-view.js.map +1 -1
  69. package/dist/app/mcp-toolkit-app.d.ts +2 -1
  70. package/dist/app/mcp-toolkit-app.d.ts.map +1 -1
  71. package/dist/app/mcp-toolkit-app.js +25 -3
  72. package/dist/app/mcp-toolkit-app.js.map +1 -1
  73. package/dist/app/use-view-lifecycle.d.ts +38 -0
  74. package/dist/app/use-view-lifecycle.d.ts.map +1 -0
  75. package/dist/app/use-view-lifecycle.js +79 -0
  76. package/dist/app/use-view-lifecycle.js.map +1 -0
  77. package/dist/app/view-lifecycle.d.ts +109 -0
  78. package/dist/app/view-lifecycle.d.ts.map +1 -0
  79. package/dist/app/view-lifecycle.js +120 -0
  80. package/dist/app/view-lifecycle.js.map +1 -0
  81. package/dist/app/widget-props-sheet.d.ts +3 -9
  82. package/dist/app/widget-props-sheet.d.ts.map +1 -1
  83. package/dist/app/widget-props-sheet.js +6 -13
  84. package/dist/app/widget-props-sheet.js.map +1 -1
  85. package/dist/app/widget-renderer.d.ts +41 -1
  86. package/dist/app/widget-renderer.d.ts.map +1 -1
  87. package/dist/app/widget-renderer.js +82 -31
  88. package/dist/app/widget-renderer.js.map +1 -1
  89. package/dist/components/DrillButton.d.ts.map +1 -1
  90. package/dist/components/DrillButton.js +2 -1
  91. package/dist/components/DrillButton.js.map +1 -1
  92. package/dist/components/EmptyState.d.ts +33 -0
  93. package/dist/components/EmptyState.d.ts.map +1 -0
  94. package/dist/components/EmptyState.js +17 -0
  95. package/dist/components/EmptyState.js.map +1 -0
  96. package/dist/components/ErrorState.d.ts +50 -0
  97. package/dist/components/ErrorState.d.ts.map +1 -0
  98. package/dist/components/ErrorState.js +60 -0
  99. package/dist/components/ErrorState.js.map +1 -0
  100. package/dist/components/FilterBar.d.ts +1 -1
  101. package/dist/components/FilterBar.d.ts.map +1 -1
  102. package/dist/components/FilterBar.js +7 -4
  103. package/dist/components/FilterBar.js.map +1 -1
  104. package/dist/components/GroupCard.d.ts.map +1 -1
  105. package/dist/components/GroupCard.js +2 -1
  106. package/dist/components/GroupCard.js.map +1 -1
  107. package/dist/components/KpiGrid.d.ts +10 -3
  108. package/dist/components/KpiGrid.d.ts.map +1 -1
  109. package/dist/components/KpiGrid.js +31 -9
  110. package/dist/components/KpiGrid.js.map +1 -1
  111. package/dist/components/ListFooter.d.ts +3 -2
  112. package/dist/components/ListFooter.d.ts.map +1 -1
  113. package/dist/components/ListFooter.js +14 -4
  114. package/dist/components/ListFooter.js.map +1 -1
  115. package/dist/components/Pills.d.ts +10 -6
  116. package/dist/components/Pills.d.ts.map +1 -1
  117. package/dist/components/Pills.js +12 -6
  118. package/dist/components/Pills.js.map +1 -1
  119. package/dist/components/Toast.d.ts +37 -0
  120. package/dist/components/Toast.d.ts.map +1 -0
  121. package/dist/components/Toast.js +91 -0
  122. package/dist/components/Toast.js.map +1 -0
  123. package/dist/components/WidgetHeader.js +1 -1
  124. package/dist/hooks/use-tool-result-recovery.d.ts +23 -1
  125. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -1
  126. package/dist/hooks/use-tool-result-recovery.js +21 -7
  127. package/dist/hooks/use-tool-result-recovery.js.map +1 -1
  128. package/dist/hooks/use-view-data.d.ts +6 -0
  129. package/dist/hooks/use-view-data.d.ts.map +1 -1
  130. package/dist/hooks/use-view-data.js +6 -1
  131. package/dist/hooks/use-view-data.js.map +1 -1
  132. package/dist/index.d.ts +12 -5
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +14 -4
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/focus-ring.d.ts +18 -0
  137. package/dist/lib/focus-ring.d.ts.map +1 -0
  138. package/dist/lib/focus-ring.js +18 -0
  139. package/dist/lib/focus-ring.js.map +1 -0
  140. package/dist/lib/icon-stroke.d.ts +12 -0
  141. package/dist/lib/icon-stroke.d.ts.map +1 -0
  142. package/dist/lib/icon-stroke.js +12 -0
  143. package/dist/lib/icon-stroke.js.map +1 -0
  144. package/dist/lib/toast-model.d.ts +46 -0
  145. package/dist/lib/toast-model.d.ts.map +1 -0
  146. package/dist/lib/toast-model.js +36 -0
  147. package/dist/lib/toast-model.js.map +1 -0
  148. package/dist/lib/tone-utils.d.ts +43 -9
  149. package/dist/lib/tone-utils.d.ts.map +1 -1
  150. package/dist/lib/tone-utils.js +36 -7
  151. package/dist/lib/tone-utils.js.map +1 -1
  152. package/dist/locale/index.d.ts +3 -1
  153. package/dist/locale/index.d.ts.map +1 -1
  154. package/dist/locale/index.js +2 -1
  155. package/dist/locale/index.js.map +1 -1
  156. package/dist/locale/locale-provider.d.ts +42 -1
  157. package/dist/locale/locale-provider.d.ts.map +1 -1
  158. package/dist/locale/locale-provider.js +44 -3
  159. package/dist/locale/locale-provider.js.map +1 -1
  160. package/dist/locale/message-types.d.ts +223 -0
  161. package/dist/locale/message-types.d.ts.map +1 -0
  162. package/dist/locale/message-types.js +7 -0
  163. package/dist/locale/message-types.js.map +1 -0
  164. package/dist/locale/messages-de.d.ts +9 -0
  165. package/dist/locale/messages-de.d.ts.map +1 -0
  166. package/dist/locale/messages-de.js +151 -0
  167. package/dist/locale/messages-de.js.map +1 -0
  168. package/dist/locale/messages-en.d.ts +8 -0
  169. package/dist/locale/messages-en.d.ts.map +1 -0
  170. package/dist/locale/messages-en.js +150 -0
  171. package/dist/locale/messages-en.js.map +1 -0
  172. package/dist/locale/toolkit-messages.d.ts +39 -0
  173. package/dist/locale/toolkit-messages.d.ts.map +1 -0
  174. package/dist/locale/toolkit-messages.js +50 -0
  175. package/dist/locale/toolkit-messages.js.map +1 -0
  176. package/dist/primitives/alert.d.ts.map +1 -1
  177. package/dist/primitives/alert.js +4 -2
  178. package/dist/primitives/alert.js.map +1 -1
  179. package/dist/primitives/badge.d.ts +1 -1
  180. package/dist/primitives/badge.d.ts.map +1 -1
  181. package/dist/primitives/badge.js +14 -6
  182. package/dist/primitives/badge.js.map +1 -1
  183. package/dist/primitives/button.d.ts.map +1 -1
  184. package/dist/primitives/button.js +12 -8
  185. package/dist/primitives/button.js.map +1 -1
  186. package/dist/primitives/card.js +4 -4
  187. package/dist/primitives/card.js.map +1 -1
  188. package/dist/primitives/dialog.d.ts +7 -3
  189. package/dist/primitives/dialog.d.ts.map +1 -1
  190. package/dist/primitives/dialog.js +14 -7
  191. package/dist/primitives/dialog.js.map +1 -1
  192. package/dist/primitives/input.d.ts.map +1 -1
  193. package/dist/primitives/input.js +4 -1
  194. package/dist/primitives/input.js.map +1 -1
  195. package/dist/primitives/scroll-area.js +1 -1
  196. package/dist/primitives/scroll-area.js.map +1 -1
  197. package/dist/primitives/select.d.ts.map +1 -1
  198. package/dist/primitives/select.js +5 -2
  199. package/dist/primitives/select.js.map +1 -1
  200. package/dist/primitives/sheet.d.ts +3 -1
  201. package/dist/primitives/sheet.d.ts.map +1 -1
  202. package/dist/primitives/sheet.js +12 -6
  203. package/dist/primitives/sheet.js.map +1 -1
  204. package/dist/primitives/skeleton.js +1 -1
  205. package/dist/primitives/skeleton.js.map +1 -1
  206. package/dist/primitives/switch.d.ts.map +1 -1
  207. package/dist/primitives/switch.js +2 -1
  208. package/dist/primitives/switch.js.map +1 -1
  209. package/dist/primitives/tabs.d.ts.map +1 -1
  210. package/dist/primitives/tabs.js +5 -3
  211. package/dist/primitives/tabs.js.map +1 -1
  212. package/dist/render-cases.d.ts.map +1 -1
  213. package/dist/render-cases.js +20 -2
  214. package/dist/render-cases.js.map +1 -1
  215. package/dist/render-scan.d.ts +25 -0
  216. package/dist/render-scan.d.ts.map +1 -0
  217. package/dist/render-scan.js +66 -0
  218. package/dist/render-scan.js.map +1 -0
  219. package/dist/theme/cd-tokens.d.ts +66 -0
  220. package/dist/theme/cd-tokens.d.ts.map +1 -0
  221. package/dist/theme/cd-tokens.js +64 -0
  222. package/dist/theme/cd-tokens.js.map +1 -0
  223. package/dist/theme/create-theme.d.ts +196 -39
  224. package/dist/theme/create-theme.d.ts.map +1 -1
  225. package/dist/theme/create-theme.js +189 -13
  226. package/dist/theme/create-theme.js.map +1 -1
  227. package/dist/theme/fonts.d.ts +38 -0
  228. package/dist/theme/fonts.d.ts.map +1 -0
  229. package/dist/theme/fonts.js +56 -0
  230. package/dist/theme/fonts.js.map +1 -0
  231. package/dist/theme/host-fonts.d.ts +39 -0
  232. package/dist/theme/host-fonts.d.ts.map +1 -0
  233. package/dist/theme/host-fonts.js +44 -0
  234. package/dist/theme/host-fonts.js.map +1 -0
  235. package/dist/theme/index.d.ts +8 -1
  236. package/dist/theme/index.d.ts.map +1 -1
  237. package/dist/theme/index.js +5 -1
  238. package/dist/theme/index.js.map +1 -1
  239. package/dist/theme/portal-container.d.ts +16 -0
  240. package/dist/theme/portal-container.d.ts.map +1 -0
  241. package/dist/theme/portal-container.js +19 -0
  242. package/dist/theme/portal-container.js.map +1 -0
  243. package/dist/theme/presets.d.ts +24 -9
  244. package/dist/theme/presets.d.ts.map +1 -1
  245. package/dist/theme/presets.js +44 -11
  246. package/dist/theme/presets.js.map +1 -1
  247. package/dist/theme/theme-provider.d.ts +27 -9
  248. package/dist/theme/theme-provider.d.ts.map +1 -1
  249. package/dist/theme/theme-provider.js +42 -20
  250. package/dist/theme/theme-provider.js.map +1 -1
  251. package/package.json +13 -8
  252. package/src/app/builder/BuilderBanners.tsx +26 -13
  253. package/src/app/builder/CanvasRow.tsx +22 -21
  254. package/src/app/builder/CatalogueSheet.tsx +16 -14
  255. package/src/app/builder/EmptyHint.tsx +16 -11
  256. package/src/app/builder/LayoutSurface.tsx +7 -7
  257. package/src/app/builder/PipelineStrip.tsx +26 -17
  258. package/src/app/builder/SaveDialog.tsx +11 -12
  259. package/src/app/builder/Toolbar.tsx +19 -7
  260. package/src/app/builder/Workspace.tsx +5 -3
  261. package/src/app/builder/builder-feedback.test.tsx +75 -0
  262. package/src/app/builder/builder-model.test.ts +5 -0
  263. package/src/app/builder/builder-model.ts +5 -4
  264. package/src/app/builder/labels.ts +15 -121
  265. package/src/app/builder/status-chip.tsx +8 -4
  266. package/src/app/builder/use-builder-catalogue.ts +6 -0
  267. package/src/app/builder/use-save-dashboard.ts +5 -0
  268. package/src/app/index.ts +8 -1
  269. package/src/app/layout-builder.tsx +23 -3
  270. package/src/app/mcp-app-view-chrome.test.tsx +347 -14
  271. package/src/app/mcp-app-view-chrome.tsx +278 -100
  272. package/src/app/mcp-app-view-states.tsx +100 -0
  273. package/src/app/mcp-app-view.test.tsx +258 -0
  274. package/src/app/mcp-app-view.tsx +292 -185
  275. package/src/app/mcp-toolkit-app.tsx +31 -7
  276. package/src/app/use-view-lifecycle.ts +116 -0
  277. package/src/app/view-lifecycle.test.ts +227 -0
  278. package/src/app/view-lifecycle.ts +174 -0
  279. package/src/app/widget-props-sheet.tsx +13 -26
  280. package/src/app/widget-renderer.render.test.tsx +131 -0
  281. package/src/app/widget-renderer.tsx +136 -38
  282. package/src/components/DrillButton.tsx +2 -1
  283. package/src/components/EmptyState.tsx +81 -0
  284. package/src/components/ErrorState.tsx +182 -0
  285. package/src/components/FilterBar.tsx +9 -6
  286. package/src/components/GroupCard.tsx +3 -2
  287. package/src/components/KpiGrid.tsx +75 -44
  288. package/src/components/ListFooter.tsx +17 -6
  289. package/src/components/Pills.tsx +20 -11
  290. package/src/components/Toast.tsx +188 -0
  291. package/src/components/WidgetHeader.tsx +1 -1
  292. package/src/components/feedback-states.test.tsx +225 -0
  293. package/src/components/tone-figures.test.tsx +54 -0
  294. package/src/fonts.css +19 -0
  295. package/src/globals.css +9 -165
  296. package/src/hooks/use-tool-result-recovery.test.ts +25 -1
  297. package/src/hooks/use-tool-result-recovery.ts +34 -6
  298. package/src/hooks/use-view-data.test.tsx +60 -0
  299. package/src/hooks/use-view-data.ts +12 -1
  300. package/src/i18n-coverage.test.tsx +323 -0
  301. package/src/index.ts +57 -5
  302. package/src/lib/focus-ring.ts +20 -0
  303. package/src/lib/icon-stroke.test.ts +59 -0
  304. package/src/lib/icon-stroke.ts +13 -0
  305. package/src/lib/toast-model.test.ts +68 -0
  306. package/src/lib/toast-model.ts +63 -0
  307. package/src/lib/tone-utils.test.ts +47 -14
  308. package/src/lib/tone-utils.ts +61 -13
  309. package/src/locale/index.ts +13 -1
  310. package/src/locale/locale-provider.test.tsx +87 -0
  311. package/src/locale/locale-provider.tsx +90 -3
  312. package/src/locale/message-types.ts +221 -0
  313. package/src/locale/messages-de.ts +159 -0
  314. package/src/locale/messages-en.ts +157 -0
  315. package/src/locale/toolkit-messages.test.ts +216 -0
  316. package/src/locale/toolkit-messages.ts +82 -0
  317. package/src/no-hardcoded-copy.test.ts +432 -0
  318. package/src/primitives/alert.tsx +4 -2
  319. package/src/primitives/badge.tsx +14 -7
  320. package/src/primitives/button.tsx +13 -8
  321. package/src/primitives/card.tsx +4 -4
  322. package/src/primitives/dialog.tsx +32 -7
  323. package/src/primitives/input.tsx +5 -3
  324. package/src/primitives/scroll-area.tsx +1 -1
  325. package/src/primitives/select.tsx +6 -3
  326. package/src/primitives/sheet.tsx +26 -6
  327. package/src/primitives/skeleton.tsx +1 -1
  328. package/src/primitives/switch.tsx +4 -2
  329. package/src/primitives/tabs.tsx +9 -6
  330. package/src/render-cases.tsx +45 -1
  331. package/src/render-scan.ts +75 -0
  332. package/src/theme/cd-tokens.generated.css +48 -0
  333. package/src/theme/cd-tokens.test.ts +107 -0
  334. package/src/theme/cd-tokens.ts +67 -0
  335. package/src/theme/create-theme.test.ts +243 -5
  336. package/src/theme/create-theme.ts +313 -41
  337. package/src/theme/fonts.test.ts +85 -0
  338. package/src/theme/fonts.ts +72 -0
  339. package/src/theme/host-fonts.test.ts +135 -0
  340. package/src/theme/host-fonts.ts +52 -0
  341. package/src/theme/index.ts +15 -1
  342. package/src/theme/portal-container.ts +20 -0
  343. package/src/theme/presets.ts +44 -11
  344. package/src/theme/theme-css.test.ts +568 -0
  345. package/src/theme/theme-provider.test.tsx +82 -0
  346. package/src/theme/theme-provider.tsx +60 -24
  347. package/src/theme.css +314 -0
  348. package/ui-catalog.json +344 -17
@@ -1,3 +1,5 @@
1
+ import { FOCUS_RING } from "../lib/focus-ring.js"
2
+ import { useToolkitMessages } from "../locale/locale-provider.js"
1
3
  import { Input } from "../primitives/input.js"
2
4
 
3
5
  export interface FilterChip {
@@ -14,7 +16,7 @@ export interface FilterChip {
14
16
  export interface FilterBarProps {
15
17
  /** Current search text (controlled). */
16
18
  search: string
17
- /** Placeholder for the search input. Default `"Filter…"`. */
19
+ /** Placeholder for the search input. Default: the locale's "Filter…". */
18
20
  searchPlaceholder?: string
19
21
  /** Called with the new search text on every keystroke. */
20
22
  onSearchChange: (next: string) => void
@@ -30,18 +32,19 @@ export interface FilterBarProps {
30
32
  */
31
33
  export function FilterBar({
32
34
  search,
33
- searchPlaceholder = "Filter…",
35
+ searchPlaceholder,
34
36
  onSearchChange,
35
37
  chips,
36
38
  onChipToggle,
37
39
  }: FilterBarProps) {
40
+ const messages = useToolkitMessages()
38
41
  return (
39
42
  <div className="flex flex-wrap items-center gap-2">
40
43
  <Input
41
44
  type="search"
42
45
  value={search}
43
46
  onChange={(e) => onSearchChange(e.target.value)}
44
- placeholder={searchPlaceholder}
47
+ placeholder={searchPlaceholder ?? messages.filterBar.searchPlaceholder}
45
48
  className="h-9 min-w-[220px] flex-1 text-sm"
46
49
  />
47
50
  <div className="flex flex-wrap gap-1.5">
@@ -50,15 +53,15 @@ export function FilterBar({
50
53
  type="button"
51
54
  key={chip.id}
52
55
  onClick={() => onChipToggle(chip.id)}
53
- className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
56
+ className={`inline-flex items-center gap-1.5 rounded-sm border px-2.5 py-1 text-xs font-medium transition-colors ${FOCUS_RING} ${
54
57
  chip.active
55
- ? "bg-accent border-border text-accent-foreground font-semibold"
58
+ ? "bg-accent border-focus text-accent-foreground font-semibold"
56
59
  : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground"
57
60
  }`}
58
61
  >
59
62
  {chip.label}
60
63
  {chip.count !== undefined && chip.count !== "" && (
61
- <span className="tabular-nums opacity-60">{chip.count}</span>
64
+ <span className="font-normal tabular-nums">{chip.count}</span>
62
65
  )}
63
66
  </button>
64
67
  ))}
@@ -1,4 +1,5 @@
1
1
  import type { KeyboardEvent, MouseEvent, ReactNode } from "react"
2
+ import { FOCUS_RING_INSET } from "../lib/focus-ring.js"
2
3
 
3
4
  export interface GroupCardProps {
4
5
  /** Whether the body is expanded. */
@@ -33,14 +34,14 @@ export function GroupCard({ expanded, onToggle, summary, children }: GroupCardPr
33
34
  }
34
35
 
35
36
  return (
36
- <div className="border-border mb-2 overflow-hidden rounded-lg border">
37
+ <div className="border-border mb-2 overflow-hidden rounded-md border">
37
38
  <div
38
39
  role="button"
39
40
  tabIndex={0}
40
41
  aria-expanded={expanded}
41
42
  onClick={handleClick}
42
43
  onKeyDown={handleKeyDown}
43
- className={`bg-card hover:bg-muted focus-visible:ring-ring/50 cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 ${
44
+ className={`bg-card hover:bg-muted cursor-pointer transition-colors ${FOCUS_RING_INSET} ${
44
45
  expanded ? "border-border border-b" : ""
45
46
  }`}
46
47
  >
@@ -1,11 +1,15 @@
1
1
  import type { ReactNode } from "react"
2
- import { TONE_TEXT, type ToneVariant } from "../lib/tone-utils.js"
2
+ import { FOCUS_RING_INSET } from "../lib/focus-ring.js"
3
+ import { TONE_DOT, type ToneVariant } from "../lib/tone-utils.js"
3
4
 
4
- /** Trend direction → tone for the small trend line under a value. */
5
- const TREND_TONE: Record<"up" | "down" | "flat", string> = {
6
- up: "text-danger",
7
- down: "text-success",
8
- flat: "text-muted-foreground",
5
+ /**
6
+ * Trend direction → tone. The tone shows ONLY as a dot before the trend line;
7
+ * the figures stay in the foreground (no coloured digits, CI §3.3).
8
+ */
9
+ const TREND_TONE: Record<"up" | "down" | "flat", ToneVariant> = {
10
+ up: "danger",
11
+ down: "success",
12
+ flat: "neutral",
9
13
  }
10
14
 
11
15
  export interface KpiCell {
@@ -17,9 +21,15 @@ export interface KpiCell {
17
21
  fraction?: ReactNode
18
22
  /** Optional trend line under the value (e.g. `"+12% vs last week"`). */
19
23
  trend?: ReactNode
20
- /** Colours the trend line. `up` reads as bad (danger), `down` as good (success). */
24
+ /**
25
+ * Tones the trend line's dot. `up` reads as bad (danger), `down` as good
26
+ * (success); the trend text itself stays in the foreground.
27
+ */
21
28
  trendDirection?: "up" | "down" | "flat"
22
- /** Tone applied to the value text (e.g. `"danger"` for a breached metric). */
29
+ /**
30
+ * Status of the metric (e.g. `"danger"` for a breached one), shown as a dot
31
+ * before the label. The figure itself stays in the foreground colour.
32
+ */
23
33
  tone?: ToneVariant
24
34
  /** When set, the cell renders as a button — turns a metric into a nav entry point. */
25
35
  onClick?: () => void
@@ -52,12 +62,65 @@ export interface KpiGridProps {
52
62
  header?: KpiGridHeader
53
63
  }
54
64
 
65
+ /** A small status dot; decorative — the label next to it carries the meaning. */
66
+ function ToneDot({ tone, size }: { tone: ToneVariant; size: "size-1.5" | "size-2" }) {
67
+ return <span aria-hidden="true" className={`${size} shrink-0 rounded-full ${TONE_DOT[tone]}`} />
68
+ }
69
+
70
+ /**
71
+ * The trend line under a value: a tone dot plus the trend text in the
72
+ * foreground — never in a tone colour. Without a tone it stays quiet text.
73
+ */
74
+ function KpiTrend({
75
+ trend,
76
+ direction,
77
+ }: {
78
+ trend: ReactNode
79
+ direction?: KpiCell["trendDirection"]
80
+ }) {
81
+ const tone = direction ? TREND_TONE[direction] : "neutral"
82
+ const textClass = tone === "neutral" ? "text-muted-foreground" : "text-foreground"
83
+ return (
84
+ <div className={`mt-1.5 inline-flex items-center gap-1.5 text-xs ${textClass}`}>
85
+ {tone !== "neutral" && <ToneDot tone={tone} size="size-1.5" />}
86
+ {trend}
87
+ </div>
88
+ )
89
+ }
90
+
91
+ /** One cell's label row, figure and optional trend — status as dots, never as coloured digits. */
92
+ function KpiCellContent({ cell }: { cell: KpiCell }) {
93
+ return (
94
+ <>
95
+ <div className="text-muted-foreground flex items-center justify-between gap-2 text-xs font-medium">
96
+ <span className="inline-flex min-w-0 items-center gap-1.5">
97
+ {cell.tone && cell.tone !== "neutral" && <ToneDot tone={cell.tone} size="size-2" />}
98
+ <span>{cell.label}</span>
99
+ </span>
100
+ {cell.onClick && (
101
+ <span aria-hidden="true" className="text-sm leading-none">
102
+ ›
103
+ </span>
104
+ )}
105
+ </div>
106
+ <div className="text-foreground mt-1.5 text-2xl leading-none font-bold tracking-tight tabular-nums">
107
+ {cell.value}
108
+ {cell.fraction && (
109
+ <span className="text-muted-foreground ml-0.5 text-sm font-normal">{cell.fraction}</span>
110
+ )}
111
+ </div>
112
+ {cell.trend && <KpiTrend trend={cell.trend} direction={cell.trendDirection} />}
113
+ </>
114
+ )
115
+ }
116
+
55
117
  /**
56
118
  * Bordered strip of KPI cells — the canonical way to show a row of headline
57
119
  * metrics in a widget (replaces the single-metric KPICard). Cells flow as
58
120
  * columns (1–6) and can be made clickable to drill into a detail view. Use
59
121
  * `boxed` + `header` for a labelled metric block; unboxed (default) renders the
60
- * lighter `border-y` strip.
122
+ * lighter `border-y` strip. A cell's `tone` shows as a status dot next to its
123
+ * label; figures stay in the foreground colour.
61
124
  */
62
125
  export function KpiGrid({ cells, boxed = false, header }: KpiGridProps) {
63
126
  const cols = Math.min(Math.max(cells.length, 1), 6)
@@ -70,7 +133,7 @@ export function KpiGrid({ cells, boxed = false, header }: KpiGridProps) {
70
133
  <div className="border-border bg-muted text-muted-foreground flex items-center gap-2 border-b px-4 py-2 text-[11px] font-semibold tracking-wide uppercase">
71
134
  <span>{header.label}</span>
72
135
  {header.badge && (
73
- <span className="border-border bg-card text-muted-foreground rounded border px-1.5 py-0 text-[10px] font-medium normal-case">
136
+ <span className="border-border bg-card text-muted-foreground rounded-sm border px-1.5 py-0 text-[10px] font-medium normal-case">
74
137
  {header.badge}
75
138
  </span>
76
139
  )}
@@ -79,46 +142,14 @@ export function KpiGrid({ cells, boxed = false, header }: KpiGridProps) {
79
142
  <div className={`grid ${colClass} ${stripClass}`}>
80
143
  {cells.map((cell, idx) => {
81
144
  const borderClass = idx < cells.length - 1 ? "border-border border-r" : ""
82
- const content = (
83
- <>
84
- <div className="text-muted-foreground flex items-center justify-between gap-2 text-xs font-medium">
85
- <span>{cell.label}</span>
86
- {cell.onClick && (
87
- <span aria-hidden="true" className="text-sm leading-none">
88
- ›
89
- </span>
90
- )}
91
- </div>
92
- <div
93
- className={`mt-1.5 text-2xl leading-none font-bold tracking-tight tabular-nums ${
94
- (cell.tone && TONE_TEXT[cell.tone]) || "text-foreground"
95
- }`}
96
- >
97
- {cell.value}
98
- {cell.fraction && (
99
- <span className="text-muted-foreground ml-0.5 text-sm font-normal">
100
- {cell.fraction}
101
- </span>
102
- )}
103
- </div>
104
- {cell.trend && (
105
- <div
106
- className={`mt-1.5 text-xs ${
107
- cell.trendDirection ? TREND_TONE[cell.trendDirection] : "text-muted-foreground"
108
- }`}
109
- >
110
- {cell.trend}
111
- </div>
112
- )}
113
- </>
114
- )
145
+ const content = <KpiCellContent cell={cell} />
115
146
  return cell.onClick ? (
116
147
  <button
117
148
  key={idx}
118
149
  type="button"
119
150
  onClick={cell.onClick}
120
151
  aria-label={cell.ariaLabel}
121
- className={`hover:bg-muted focus-visible:ring-ring cursor-pointer px-5 py-4 text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-inset ${borderClass}`}
152
+ className={`hover:bg-muted cursor-pointer px-5 py-4 text-left transition-colors ${FOCUS_RING_INSET} ${borderClass}`}
122
153
  >
123
154
  {content}
124
155
  </button>
@@ -1,3 +1,7 @@
1
+ import { FOCUS_RING } from "../lib/focus-ring.js"
2
+ import { useToolkitLocale, useToolkitMessages } from "../locale/locale-provider.js"
3
+ import { formatCount } from "../locale/toolkit-messages.js"
4
+
1
5
  export interface ListFooterProps {
2
6
  /** How many rows are currently rendered. */
3
7
  shown: number
@@ -9,7 +13,7 @@ export interface ListFooterProps {
9
13
  loadingMore?: boolean
10
14
  /** Called when the user asks for the next page. */
11
15
  onLoadMore: () => void
12
- /** Plural noun for the count, e.g. `"instances"`. Default `"rows"`. */
16
+ /** Plural noun for the count, e.g. `"instances"`. Default: the locale's word for rows. */
13
17
  noun?: string
14
18
  }
15
19
 
@@ -17,7 +21,8 @@ export interface ListFooterProps {
17
21
  * Footer for a paginated list: an honest "Showing X of Y" total plus an
18
22
  * explicit "Load more" control. Deliberately not infinite scroll — the user
19
23
  * gets the total, a reachable footer, and control over fetching. Renders
20
- * nothing when `total` is 0.
24
+ * nothing when `total` is 0. Text and number format follow the active locale
25
+ * (`LocaleProvider`, else the host's inside `McpAppView`).
21
26
  */
22
27
  export function ListFooter({
23
28
  shown,
@@ -25,22 +30,28 @@ export function ListFooter({
25
30
  hasMore,
26
31
  loadingMore = false,
27
32
  onLoadMore,
28
- noun = "rows",
33
+ noun,
29
34
  }: ListFooterProps) {
35
+ const locale = useToolkitLocale()
36
+ const { listFooter } = useToolkitMessages()
30
37
  if (total <= 0) return null
31
38
  return (
32
39
  <div className="text-muted-foreground flex items-center justify-between gap-3 px-1 py-2 text-xs">
33
40
  <span>
34
- Showing {shown.toLocaleString()} of {total.toLocaleString()} {noun}
41
+ {listFooter.showing({
42
+ shown: formatCount(shown, locale),
43
+ total: formatCount(total, locale),
44
+ noun: noun ?? listFooter.defaultNoun,
45
+ })}
35
46
  </span>
36
47
  {hasMore && (
37
48
  <button
38
49
  type="button"
39
50
  onClick={onLoadMore}
40
51
  disabled={loadingMore}
41
- className="border-border text-foreground hover:bg-muted focus-visible:ring-ring inline-flex items-center gap-1 rounded-md border px-3 py-1.5 font-medium outline-none focus-visible:ring-2 disabled:opacity-50"
52
+ className={`border-border text-foreground hover:border-focus hover:bg-accent hover:text-accent-foreground inline-flex items-center gap-1 rounded-md border px-3 py-1.5 font-medium transition-colors disabled:opacity-50 ${FOCUS_RING}`}
42
53
  >
43
- {loadingMore ? "Loading…" : "Load more"}
54
+ {loadingMore ? listFooter.loadingMore : listFooter.loadMore}
44
55
  </button>
45
56
  )}
46
57
  </div>
@@ -1,43 +1,52 @@
1
1
  import type { ReactNode } from "react"
2
- import { TONE_DOT, TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
2
+ import { useToolkitMessages } from "../locale/locale-provider.js"
3
+ import { TONE_BORDER, TONE_DOT, TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
3
4
 
4
5
  export interface LivePillProps {
5
- /** Status tone driving the dot + tint. Default `"info"`. */
6
+ /** Status tone driving the dot, tint and border. Default `"info"`. */
6
7
  tone?: ToneVariant
7
- /** Pill label. Defaults to `"Live"`. */
8
+ /** Pill label. Defaults to the locale's "Live". */
8
9
  children?: ReactNode
9
10
  }
10
11
 
11
12
  /**
12
13
  * Small tinted pill with a pulsing dot. Use in a widget header to signal that
13
- * the data is live/real-time. Tone-based, so it inherits the active theme.
14
+ * the data is live/real-time. Tone-based (tint, border and dot in the tone,
15
+ * text in the foreground), so it inherits the active theme. The pulse stops
16
+ * under `prefers-reduced-motion`.
14
17
  */
15
18
  export function LivePill({ tone = "info", children }: LivePillProps) {
19
+ const messages = useToolkitMessages()
16
20
  return (
17
21
  <span
18
- className={`inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-semibold ${TONE_SOFT[tone]}`}
22
+ className={`inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-semibold ${TONE_SOFT[tone]} ${TONE_BORDER[tone]}`}
19
23
  >
20
- <span className={`size-1.5 animate-pulse rounded-full ${TONE_DOT[tone]}`} />
21
- {children ?? "Live"}
24
+ <span
25
+ aria-hidden="true"
26
+ className={`size-1.5 animate-pulse rounded-full ${TONE_DOT[tone]}`}
27
+ />
28
+ {children ?? messages.pills.live}
22
29
  </span>
23
30
  )
24
31
  }
25
32
 
26
33
  export interface CountPillProps {
27
- /** Status tone driving the tint. Default `"neutral"`. */
34
+ /** Status tone driving the tint and border. Default `"neutral"`. */
28
35
  tone?: ToneVariant
29
36
  /** The count/value to display (tabular numbers). */
30
37
  children: ReactNode
31
38
  }
32
39
 
33
40
  /**
34
- * Compact count badge — tabular numbers on a tinted background. Use as the
35
- * right-aligned indicator on a list/group row (e.g. a per-group item count).
41
+ * Compact count badge — tabular numbers on a tinted background with a border
42
+ * in the tone. Use as the right-aligned indicator on a list/group row (e.g. a
43
+ * per-group item count). The number stays in the foreground in every tone
44
+ * (no coloured digits): the tint and the border carry the tone.
36
45
  */
37
46
  export function CountPill({ tone = "neutral", children }: CountPillProps) {
38
47
  return (
39
48
  <span
40
- className={`inline-flex min-w-[2.5rem] items-center justify-center rounded-md px-2.5 py-0.5 text-sm font-semibold tabular-nums ${TONE_SOFT[tone]}`}
49
+ className={`inline-flex min-w-[2.5rem] items-center justify-center rounded-sm border px-2.5 py-0.5 text-sm font-semibold tabular-nums ${TONE_SOFT[tone]} ${TONE_BORDER[tone]}`}
41
50
  >
42
51
  {children}
43
52
  </span>
@@ -0,0 +1,188 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useReducer,
8
+ useRef,
9
+ useState,
10
+ type ReactNode,
11
+ } from "react"
12
+ import { CircleAlert, CircleCheck, Info, TriangleAlert, X, type LucideIcon } from "lucide-react"
13
+ import { useToolkitMessages } from "../locale/locale-provider.js"
14
+ import { FOCUS_RING } from "../lib/focus-ring.js"
15
+ import { TONE_ICON } from "../lib/tone-utils.js"
16
+ import { cn } from "../lib/utils.js"
17
+ import {
18
+ createToast,
19
+ toastReducer,
20
+ type ToastItem,
21
+ type ToastOptions,
22
+ type ToastTone,
23
+ } from "../lib/toast-model.js"
24
+
25
+ export type { ToastOptions, ToastTone } from "../lib/toast-model.js"
26
+
27
+ /** What {@link useToast} returns. */
28
+ export interface ToastApi {
29
+ /** Show a toast; returns its id (for an early {@link ToastApi.dismiss}). */
30
+ show: (options: ToastOptions) => number
31
+ /** Remove a toast before its timer runs out. */
32
+ dismiss: (id: number) => void
33
+ }
34
+
35
+ const ToastContext = createContext<ToastApi | null>(null)
36
+
37
+ /** Outside a provider `useToast()` degrades to a no-op instead of throwing. */
38
+ const NOOP_TOASTS: ToastApi = { show: () => -1, dismiss: () => {} }
39
+
40
+ /**
41
+ * Raise toasts from anywhere below a {@link ToastProvider}: `show({ tone,
42
+ * title })` after a write succeeded or failed, never a silent `console` line.
43
+ * Outside a provider it returns a no-op API, so a widget rendered in a bare
44
+ * fixture never crashes on feedback.
45
+ */
46
+ export function useToast(): ToastApi {
47
+ return useContext(ToastContext) ?? NOOP_TOASTS
48
+ }
49
+
50
+ /**
51
+ * Status edge per tone — the status colour as border, never as text. Repeated
52
+ * under `dark:` so the card's dark 3:1 rim (`dark:border-input`, a later rule)
53
+ * does not paint over it.
54
+ */
55
+ const TONE_EDGE: Record<ToastTone, string> = {
56
+ success: "border-l-success dark:border-l-success",
57
+ warning: "border-l-warning dark:border-l-warning",
58
+ danger: "border-l-danger dark:border-l-danger",
59
+ info: "border-l-info dark:border-l-info",
60
+ }
61
+
62
+ /**
63
+ * Lucide icon per tone, drawn in the tone colour (`TONE_ICON`, CI §3.3: status
64
+ * as edge and icon). The title next to it carries the message, so success and
65
+ * warning stay CI-exempt from 3:1 on light like every other status icon.
66
+ */
67
+ const TOAST_ICON: Record<ToastTone, LucideIcon> = {
68
+ success: CircleCheck,
69
+ warning: TriangleAlert,
70
+ danger: CircleAlert,
71
+ info: Info,
72
+ }
73
+
74
+ /**
75
+ * The stack of toasts — a presentational view over a list (the provider owns
76
+ * the state). The container is ONE persistent live region (`role="status"`,
77
+ * `aria-live="polite"`), so a newly added toast is announced without stealing
78
+ * focus. Bottom centre, above dialogs (z 60), per CI §5/§8.
79
+ */
80
+ export function ToastViewport({
81
+ toasts,
82
+ onDismiss,
83
+ }: {
84
+ toasts: readonly ToastItem[]
85
+ onDismiss: (id: number) => void
86
+ }) {
87
+ const messages = useToolkitMessages()
88
+ return (
89
+ <div
90
+ data-slot="toast-viewport"
91
+ role="status"
92
+ aria-live="polite"
93
+ aria-label={messages.common.notifications}
94
+ className="pointer-events-none fixed inset-x-0 bottom-4 z-60 flex flex-col items-center gap-2 px-4"
95
+ >
96
+ {toasts.map((toast) => (
97
+ <ToastCard
98
+ key={toast.id}
99
+ toast={toast}
100
+ dismissLabel={messages.common.dismiss}
101
+ onDismiss={onDismiss}
102
+ />
103
+ ))}
104
+ </div>
105
+ )
106
+ }
107
+
108
+ function ToastCard({
109
+ toast,
110
+ dismissLabel,
111
+ onDismiss,
112
+ }: {
113
+ toast: ToastItem
114
+ dismissLabel: string
115
+ onDismiss: (id: number) => void
116
+ }) {
117
+ // Hover or keyboard focus holds the toast: the timer restarts on leave.
118
+ const [held, setHeld] = useState(false)
119
+ useEffect(() => {
120
+ if (held) return
121
+ const timer = setTimeout(() => onDismiss(toast.id), toast.duration)
122
+ return () => clearTimeout(timer)
123
+ }, [held, onDismiss, toast.id, toast.duration])
124
+ const Icon = TOAST_ICON[toast.tone]
125
+ return (
126
+ <div
127
+ data-slot="toast"
128
+ data-tone={toast.tone}
129
+ onMouseEnter={() => setHeld(true)}
130
+ onMouseLeave={() => setHeld(false)}
131
+ onFocus={() => setHeld(true)}
132
+ onBlur={() => setHeld(false)}
133
+ className={cn(
134
+ // An overlay like Dialog/Sheet: shadow-2 on light, the 3:1 input rim on
135
+ // dark (shadows vanish on the ground).
136
+ "bg-popover text-popover-foreground dark:border-input shadow-2 pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-md border border-l-4 px-4 py-3 text-sm",
137
+ "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-2 motion-safe:duration-(--motion-base) motion-safe:ease-(--ease)",
138
+ TONE_EDGE[toast.tone],
139
+ )}
140
+ >
141
+ <Icon aria-hidden="true" className={cn("mt-0.5 size-4 shrink-0", TONE_ICON[toast.tone])} />
142
+ <div className="min-w-0 flex-1">
143
+ <p className="font-medium break-words">{toast.title}</p>
144
+ {toast.description && (
145
+ <p className="text-muted-foreground mt-0.5 break-words">{toast.description}</p>
146
+ )}
147
+ </div>
148
+ <button
149
+ type="button"
150
+ aria-label={dismissLabel}
151
+ onClick={() => onDismiss(toast.id)}
152
+ className={`text-muted-foreground hover:text-foreground -mr-1 inline-flex size-6 shrink-0 items-center justify-center rounded-sm ${FOCUS_RING}`}
153
+ >
154
+ <X aria-hidden="true" className="size-4" />
155
+ </button>
156
+ </div>
157
+ )
158
+ }
159
+
160
+ function ToastRoot({ children }: { children: ReactNode }) {
161
+ const [toasts, dispatch] = useReducer(toastReducer, [])
162
+ const nextId = useRef(0)
163
+ const dismiss = useCallback((id: number) => dispatch({ type: "dismiss", id }), [])
164
+ const show = useCallback((options: ToastOptions) => {
165
+ nextId.current += 1
166
+ const id = nextId.current
167
+ dispatch({ type: "add", toast: createToast(id, options) })
168
+ return id
169
+ }, [])
170
+ const api = useMemo<ToastApi>(() => ({ show, dismiss }), [show, dismiss])
171
+ return (
172
+ <ToastContext.Provider value={api}>
173
+ {children}
174
+ <ToastViewport toasts={toasts} onDismiss={dismiss} />
175
+ </ToastContext.Provider>
176
+ )
177
+ }
178
+
179
+ /**
180
+ * Provide toasts to a subtree and render their stack. Idempotent: nested
181
+ * inside another `ToastProvider` it adds nothing, so the app shell and an
182
+ * embedded builder share one stack and one live region. `McpAppView` mounts
183
+ * one for you.
184
+ */
185
+ export function ToastProvider({ children }: { children: ReactNode }) {
186
+ const parent = useContext(ToastContext)
187
+ return parent ? children : <ToastRoot>{children}</ToastRoot>
188
+ }
@@ -31,7 +31,7 @@ export function WidgetHeader({
31
31
  <div className="min-w-0">
32
32
  {icon && (
33
33
  <div
34
- className={`mb-3.5 grid size-11 place-items-center rounded-xl text-xl ${TONE_SOFT[iconTone]}`}
34
+ className={`mb-3.5 grid size-11 place-items-center rounded-md text-xl ${TONE_SOFT[iconTone]}`}
35
35
  >
36
36
  {icon}
37
37
  </div>