@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
@@ -0,0 +1,432 @@
1
+ import ts from "typescript"
2
+ import { describe, expect, it } from "vitest"
3
+ import { TEXT_ATTRIBUTES } from "./render-scan.js"
4
+
5
+ /**
6
+ * Static half of the i18n guard (the render half is `i18n-coverage.test.tsx`):
7
+ * no toolkit component may carry user-facing copy in its source. Every word a
8
+ * user reads or hears comes from the locale catalog (`locale/messages-*.ts`)
9
+ * or from the consumer's props. This catches what an SSR render cannot reach —
10
+ * portal content (dialogs, sheets, select menus) and branches the fixtures do
11
+ * not hit.
12
+ *
13
+ * Flagged in every `.tsx` under src (tests and the render-case fixtures
14
+ * excepted):
15
+ * - JSX text containing a letter;
16
+ * - a string literal with a letter rendered as a child (`{"…"}`, `{x ? "a" : "b"}`,
17
+ * `{x && "a"}`, template literals);
18
+ * - such a literal in a text attribute of a DOM element (aria-label, title,
19
+ * placeholder, alt, …), or in any prop of a component except the
20
+ * non-text props below (variants, sizes, roles, ids, …).
21
+ *
22
+ * And in every `.ts`/`.tsx` source, the feedback copy rule: the sentences of
23
+ * a feedback surface carry catalog text only, never a literal and never a raw
24
+ * error. Feedback surfaces are the `title`/`description` of a
25
+ * `<x>.show({ … })` toast call and of `ErrorState`/`EmptyState`, and the
26
+ * content of an inline alert (any element with `role="alert"` or
27
+ * `role="status"`, and the `Alert` primitive). A raw error is `e.message`,
28
+ * `String(e)`, an `errorText(e)` helper, or a value named as one once it sits
29
+ * in state or props (`error`, `err`, `reason`, `saveError`, `callErrorReason`
30
+ * …). The raw reason belongs in the console or under an ErrorState's
31
+ * "Details", not in a sentence the user reads.
32
+ */
33
+
34
+ declare global {
35
+ interface ImportMeta {
36
+ glob(
37
+ pattern: string | string[],
38
+ options: { query: "?raw"; import: "default"; eager: true },
39
+ ): Record<string, string>
40
+ }
41
+ }
42
+
43
+ const SOURCES = import.meta.glob(["./**/*.tsx", "!./**/*.test.tsx", "!./render-cases*.tsx"], {
44
+ query: "?raw",
45
+ import: "default",
46
+ eager: true,
47
+ })
48
+
49
+ const TS_SOURCES = import.meta.glob(["./**/*.ts", "!./**/*.test.ts", "!./**/*.d.ts"], {
50
+ query: "?raw",
51
+ import: "default",
52
+ eager: true,
53
+ })
54
+
55
+ /** Component props that never carry user-facing copy. */
56
+ const NON_TEXT_PROPS = new Set([
57
+ "className",
58
+ "variant",
59
+ "size",
60
+ "type",
61
+ "side",
62
+ "role",
63
+ "tone",
64
+ "iconTone",
65
+ "titleAs",
66
+ "titleMode",
67
+ "orientation",
68
+ "align",
69
+ "value",
70
+ "defaultValue",
71
+ "id",
72
+ "name",
73
+ "htmlFor",
74
+ "href",
75
+ "target",
76
+ "rel",
77
+ "mode",
78
+ "asChild",
79
+ "dir",
80
+ ])
81
+
82
+ const HAS_LETTER = /\p{L}/u
83
+ const TEXT_ATTRS = new Set<string>(TEXT_ATTRIBUTES)
84
+
85
+ /** String literals in an expression that would end up rendered as text. */
86
+ function renderedLiterals(node: ts.Expression): ts.Node[] {
87
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return [node]
88
+ if (ts.isTemplateExpression(node)) return [node.head, ...node.templateSpans.map((s) => s.literal)]
89
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) {
90
+ return renderedLiterals(node.expression)
91
+ }
92
+ if (ts.isConditionalExpression(node)) {
93
+ return [...renderedLiterals(node.whenTrue), ...renderedLiterals(node.whenFalse)]
94
+ }
95
+ if (ts.isBinaryExpression(node)) {
96
+ const op = node.operatorToken.kind
97
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken) return renderedLiterals(node.right)
98
+ if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) {
99
+ return [...renderedLiterals(node.left), ...renderedLiterals(node.right)]
100
+ }
101
+ }
102
+ return []
103
+ }
104
+
105
+ function literalText(node: ts.Node): string {
106
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
107
+ if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
108
+ return node.text
109
+ }
110
+ return ""
111
+ }
112
+
113
+ function tagName(attr: ts.JsxAttribute): string {
114
+ const opening = attr.parent.parent
115
+ return opening.tagName.getText()
116
+ }
117
+
118
+ /**
119
+ * Whether an attribute is rendered text: the text attributes on any element;
120
+ * on a component additionally every prop that is not a known non-text prop.
121
+ */
122
+ function isTextAttribute(attr: ts.JsxAttribute): boolean {
123
+ const name = attr.name.getText()
124
+ if (TEXT_ATTRS.has(name)) return true
125
+ if (/^[a-z]/.test(tagName(attr))) return false
126
+ if (name.endsWith("ClassName")) return false
127
+ return !NON_TEXT_PROPS.has(name) && !name.startsWith("data-") && !name.startsWith("aria-")
128
+ }
129
+
130
+ function hardcodedCopy(file: string, source: string): string[] {
131
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
132
+ const found: string[] = []
133
+ const report = (node: ts.Node, text: string) => {
134
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
135
+ found.push(`${file}:${line + 1}: "${text.trim().replace(/\s+/g, " ")}"`)
136
+ }
137
+ const visit = (node: ts.Node) => {
138
+ if (ts.isJsxText(node) && HAS_LETTER.test(node.text)) report(node, node.text)
139
+ if (ts.isJsxExpression(node) && node.expression) {
140
+ const parent = node.parent
141
+ const rendered =
142
+ ts.isJsxElement(parent) ||
143
+ ts.isJsxFragment(parent) ||
144
+ (ts.isJsxAttribute(parent) && isTextAttribute(parent))
145
+ if (rendered) {
146
+ for (const lit of renderedLiterals(node.expression)) {
147
+ const text = literalText(lit)
148
+ if (HAS_LETTER.test(text)) report(lit, text)
149
+ }
150
+ }
151
+ }
152
+ if (
153
+ ts.isJsxAttribute(node) &&
154
+ node.initializer &&
155
+ ts.isStringLiteral(node.initializer) &&
156
+ HAS_LETTER.test(node.initializer.text) &&
157
+ isTextAttribute(node)
158
+ ) {
159
+ report(node.initializer, node.initializer.text)
160
+ }
161
+ ts.forEachChild(node, visit)
162
+ }
163
+ visit(sf)
164
+ return found
165
+ }
166
+
167
+ /** Calls that turn a caught error into text. */
168
+ const RAW_ERROR_CALLS = new Set(["errorText", "errorMessage", "String"])
169
+
170
+ /**
171
+ * Names a caught error (or its text) takes once it is held in state or passed
172
+ * as a prop: `e`, `err`, `error`, `reason`, `cause`, or a name ending in
173
+ * `Error`/`Reason` (`saveError`, `callErrorReason`). Flags (`isError`,
174
+ * `hasError`, `showError`) are not errors.
175
+ */
176
+ const RAW_ERROR_NAME = /^(?:e|err|error|reason|cause)$|^(?!(?:is|has|show)[A-Z]).*(?:Error|Reason)$/
177
+
178
+ /**
179
+ * Whether an expression reads a raw error: `.message`/`.stack`, a raw-error
180
+ * call, or a reference to a value named as an error. Declarations, property
181
+ * names, types and the attributes of nested JSX are not reads; the text
182
+ * children of nested JSX are.
183
+ */
184
+ function readsRawError(node: ts.Node): boolean {
185
+ if (ts.isTypeNode(node) || ts.isJsxAttributes(node) || ts.isJsxSelfClosingElement(node)) {
186
+ return false
187
+ }
188
+ if (ts.isPropertyAccessExpression(node)) {
189
+ if (node.name.text === "message" || node.name.text === "stack") return true
190
+ return readsRawError(node.expression)
191
+ }
192
+ if (
193
+ ts.isCallExpression(node) &&
194
+ ts.isIdentifier(node.expression) &&
195
+ RAW_ERROR_CALLS.has(node.expression.text)
196
+ ) {
197
+ return true
198
+ }
199
+ if (ts.isIdentifier(node)) return RAW_ERROR_NAME.test(node.text)
200
+ if (ts.isJsxElement(node) || ts.isJsxFragment(node)) return node.children.some(readsRawError)
201
+ if (ts.isParameter(node) || ts.isBindingElement(node) || ts.isVariableDeclaration(node)) {
202
+ return node.initializer ? readsRawError(node.initializer) : false
203
+ }
204
+ if (ts.isPropertyAssignment(node)) return readsRawError(node.initializer)
205
+ return ts.forEachChild(node, readsRawError) ?? false
206
+ }
207
+
208
+ /** `title`/`description` of `<x>.show({ … })` calls that are literals or raw errors. */
209
+ function toastCopyLeaks(file: string, source: string): string[] {
210
+ const kind = file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
211
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind)
212
+ const found: string[] = []
213
+ const visit = (node: ts.Node) => {
214
+ const options = ts.isCallExpression(node) ? node.arguments[0] : undefined
215
+ if (
216
+ ts.isCallExpression(node) &&
217
+ ts.isPropertyAccessExpression(node.expression) &&
218
+ node.expression.name.text === "show" &&
219
+ options &&
220
+ ts.isObjectLiteralExpression(options)
221
+ ) {
222
+ for (const prop of options.properties) {
223
+ if (!ts.isPropertyAssignment(prop)) continue
224
+ const key = prop.name.getText(sf)
225
+ if (key !== "title" && key !== "description") continue
226
+ const literal = renderedLiterals(prop.initializer).some((lit) =>
227
+ HAS_LETTER.test(literalText(lit)),
228
+ )
229
+ if (literal || readsRawError(prop.initializer)) {
230
+ const { line } = sf.getLineAndCharacterOfPosition(prop.getStart(sf))
231
+ found.push(`${file}:${line + 1}: ${key}: ${prop.initializer.getText(sf)}`)
232
+ }
233
+ }
234
+ }
235
+ ts.forEachChild(node, visit)
236
+ }
237
+ visit(sf)
238
+ return found
239
+ }
240
+
241
+ /** Feedback components whose `title`/`description` are sentences the user reads. */
242
+ const FEEDBACK_COMPONENTS = new Set(["ErrorState", "EmptyState"])
243
+ const FEEDBACK_PROPS = new Set(["title", "description"])
244
+ /** Components that are an inline alert by themselves (their role is built in). */
245
+ const ALERT_COMPONENTS = new Set(["Alert"])
246
+
247
+ function jsxTag(node: ts.JsxElement | ts.JsxSelfClosingElement): string {
248
+ return (ts.isJsxElement(node) ? node.openingElement : node).tagName.getText()
249
+ }
250
+
251
+ /** Whether a JSX element is an inline alert: `role="alert"|"status"` or the `Alert` primitive. */
252
+ function isInlineAlert(node: ts.JsxElement): boolean {
253
+ if (ALERT_COMPONENTS.has(jsxTag(node))) return true
254
+ return node.openingElement.attributes.properties.some(
255
+ (attr) =>
256
+ ts.isJsxAttribute(attr) &&
257
+ attr.name.getText() === "role" &&
258
+ attr.initializer !== undefined &&
259
+ ts.isStringLiteral(attr.initializer) &&
260
+ (attr.initializer.text === "alert" || attr.initializer.text === "status"),
261
+ )
262
+ }
263
+
264
+ /**
265
+ * Raw errors in the JSX feedback surfaces: the `title`/`description` of an
266
+ * ErrorState or EmptyState, and the content of an inline alert. (Literals in
267
+ * them are the hard-coded-copy gate's job.)
268
+ */
269
+ function feedbackCopyLeaks(file: string, source: string): string[] {
270
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
271
+ const found: string[] = []
272
+ const report = (node: ts.Node, where: string) => {
273
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
274
+ found.push(`${file}:${line + 1}: ${where}: ${node.getText(sf).replace(/\s+/g, " ")}`)
275
+ }
276
+ const visit = (node: ts.Node) => {
277
+ if (
278
+ ts.isJsxAttribute(node) &&
279
+ node.initializer &&
280
+ FEEDBACK_PROPS.has(node.name.getText()) &&
281
+ FEEDBACK_COMPONENTS.has(tagName(node)) &&
282
+ readsRawError(node.initializer)
283
+ ) {
284
+ report(node.initializer, `${tagName(node)} ${node.name.getText()}`)
285
+ }
286
+ if (ts.isJsxElement(node) && isInlineAlert(node)) {
287
+ for (const child of node.children) {
288
+ if (readsRawError(child)) report(child, `${jsxTag(node)} alert content`)
289
+ }
290
+ }
291
+ ts.forEachChild(node, visit)
292
+ }
293
+ visit(sf)
294
+ return found
295
+ }
296
+
297
+ describe("feedback copy comes from the catalog, never from a raw error", () => {
298
+ it("scans the feedback surfaces", () => {
299
+ const tsx = Object.entries(SOURCES)
300
+ expect(tsx.filter(([, src]) => /<(ErrorState|EmptyState)\b/.test(src)).length).toBeGreaterThan(
301
+ 3,
302
+ )
303
+ expect(tsx.filter(([, src]) => /role="(alert|status)"/.test(src)).length).toBeGreaterThan(2)
304
+ })
305
+
306
+ it("finds no raw error in an ErrorState/EmptyState title or description, or in an inline alert", () => {
307
+ const offenders = Object.entries(SOURCES).flatMap(([file, source]) =>
308
+ feedbackCopyLeaks(file, source),
309
+ )
310
+ expect(
311
+ offenders,
312
+ "Say what happened and what to do in catalog words (locale/messages-*.ts); pass the raw reason as an ErrorState's `details` (or log it with console.warn).",
313
+ ).toEqual([])
314
+ })
315
+
316
+ it("catches the shapes it promises to catch (gate self-test)", () => {
317
+ const src = `
318
+ export const A = ({ L, e }: P) => <ErrorState title={L.x} description={e.message} />
319
+ export const B = ({ error }: P) => <ErrorState title={error} details={error} />
320
+ export const C = ({ L, err }: P) => <EmptyState title={L.empty} description={String(err)} />
321
+ export const D = ({ L, error }: P) => (
322
+ <div role="alert">
323
+ <span>{L.saveError}:</span> {error}
324
+ </div>
325
+ )
326
+ export const E = ({ L, name, refreshError }: P) => (
327
+ <div role="status">
328
+ <p>{L.catalogueError}</p>
329
+ <p><span>{name}</span>: {refreshError}</p>
330
+ </div>
331
+ )
332
+ export const F = ({ reason }: P) => <Alert><AlertDescription>{reason}</AlertDescription></Alert>
333
+ export const G = ({ L, error, isError }: P) => (
334
+ <>
335
+ <ErrorState title={L.saveError} description={L.saveErrorHint} details={error} />
336
+ {isError && <p role="alert">{L.failed}</p>}
337
+ <p role="status">{L.cancelled}</p>
338
+ <div role="alert">{items.map(({ id, reason }) => <li key={id}>{id}</li>)}</div>
339
+ <ErrorState title={human?.title ?? L.errorTitle} description={(x as ViewError).hint} />
340
+ </>
341
+ )
342
+ `
343
+ expect(
344
+ feedbackCopyLeaks("self.tsx", src).map((s) => s.replace(/^self\.tsx:\d+: /, "")),
345
+ ).toEqual([
346
+ "ErrorState description: {e.message}",
347
+ "ErrorState title: {error}",
348
+ "EmptyState description: {String(err)}",
349
+ "div alert content: {error}",
350
+ "div alert content: <p><span>{name}</span>: {refreshError}</p>",
351
+ "Alert alert content: <AlertDescription>{reason}</AlertDescription>",
352
+ ])
353
+ })
354
+ })
355
+
356
+ describe("toast copy comes from the catalog, never from a raw error", () => {
357
+ it("scans the toast call sites", () => {
358
+ const all = { ...SOURCES, ...TS_SOURCES }
359
+ expect(Object.keys(all)).toContain("./app/mcp-app-view.tsx")
360
+ expect(Object.values(all).filter((src) => /\.show\(\{/.test(src)).length).toBeGreaterThan(1)
361
+ })
362
+
363
+ it("finds no literal and no raw error in a toast's title or description", () => {
364
+ const offenders = Object.entries({ ...SOURCES, ...TS_SOURCES }).flatMap(([file, source]) =>
365
+ toastCopyLeaks(file, source),
366
+ )
367
+ expect(
368
+ offenders,
369
+ "Say what happened and what to do in catalog words (locale/messages-*.ts); log the raw reason (console.warn) or show it under an ErrorState's Details.",
370
+ ).toEqual([])
371
+ })
372
+
373
+ it("catches the shapes it promises to catch (gate self-test)", () => {
374
+ const src = `
375
+ toast.show({ tone: "danger", title: "Refresh failed", description: errorText(e) })
376
+ toast.show({ title: L.refreshFailed, description: e.message })
377
+ toast.show({ title: L.x, description: String(err) })
378
+ toast.show({ title: L.refreshFailed, description: refreshError })
379
+ toast.show({ tone: "success", title: L.saved(name) })
380
+ toast.show({ title: L.refreshFailed, description: L.refreshFailedHint })
381
+ toast.show({ title: L.failed, description: isError ? L.a : L.b })
382
+ other.show(options)
383
+ `
384
+ expect(toastCopyLeaks("self.ts", src).map((s) => s.replace(/^self\.ts:\d+: /, ""))).toEqual([
385
+ 'title: "Refresh failed"',
386
+ "description: errorText(e)",
387
+ "description: e.message",
388
+ "description: String(err)",
389
+ "description: refreshError",
390
+ ])
391
+ })
392
+ })
393
+
394
+ describe("no hard-coded copy in toolkit components", () => {
395
+ it("scans the component sources", () => {
396
+ // A broken glob would make the gate vacuous.
397
+ expect(Object.keys(SOURCES).length).toBeGreaterThan(40)
398
+ expect(Object.keys(SOURCES)).toContain("./app/mcp-app-view-chrome.tsx")
399
+ expect(Object.keys(SOURCES)).toContain("./primitives/dialog.tsx")
400
+ })
401
+
402
+ it("finds every user-facing word in the catalog or in props, never in the source", () => {
403
+ const offenders = Object.entries(SOURCES).flatMap(([file, source]) =>
404
+ hardcodedCopy(file, source),
405
+ )
406
+ expect(
407
+ offenders,
408
+ "Move these strings into the toolkit catalog (locale/message-types.ts + messages-en.ts + messages-de.ts) and read them via useToolkitMessages() or the component's labels.",
409
+ ).toEqual([])
410
+ })
411
+
412
+ it("catches the shapes it promises to catch (gate self-test)", () => {
413
+ const src = `
414
+ export const A = () => <p>Loading…</p>
415
+ export const B = ({ x }: { x: boolean }) => <span>{x ? "Load more" : "…"}</span>
416
+ export const C = () => <button aria-label="Close" title={"Shut"} className="text-sm" />
417
+ export const D = () => <ErrorState title="Oops" variant="inline" data-slot="x" />
418
+ export const E = ({ n }: { n: number }) => <i>{\`\${n} rows\`}</i>
419
+ export const F = ({ ok }: { ok: boolean }) => <b>{ok && "Done"}{ok === Boolean("x") && 1}</b>
420
+ export const G = ({ L }: { L: { a: string } }) => <p title={L.a}>{L.a} · {"→"}</p>
421
+ `
422
+ expect(hardcodedCopy("self.tsx", src).map((s) => s.replace(/^self\.tsx:\d+: /, ""))).toEqual([
423
+ '"Loading…"',
424
+ '"Load more"',
425
+ '"Close"',
426
+ '"Shut"',
427
+ '"Oops"',
428
+ '"rows"',
429
+ '"Done"',
430
+ ])
431
+ })
432
+ })
@@ -4,13 +4,15 @@ import { cva, type VariantProps } from "class-variance-authority"
4
4
  import { cn } from "../lib/utils.js"
5
5
 
6
6
  const alertVariants = cva(
7
- "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
7
+ "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-md border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
8
  {
9
9
  variants: {
10
10
  variant: {
11
11
  default: "bg-card text-card-foreground",
12
+ // Tone model: tint, a border and icon in the danger colour; title and
13
+ // description in the foreground (CI §3.3: text on a tint is black).
12
14
  destructive:
13
- "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
15
+ "border-danger bg-danger-soft text-foreground *:data-[slot=alert-title]:text-foreground *:data-[slot=alert-description]:text-foreground [&>svg]:text-danger",
14
16
  },
15
17
  },
16
18
  defaultVariants: {
@@ -2,20 +2,27 @@ import { mergeProps } from "@base-ui/react/merge-props"
2
2
  import { useRender } from "@base-ui/react/use-render"
3
3
  import { cva, type VariantProps } from "class-variance-authority"
4
4
 
5
+ import { FOCUS_RING } from "../lib/focus-ring.js"
6
+ import { TONE_BORDER, TONE_SOFT } from "../lib/tone-utils.js"
5
7
  import { cn } from "../lib/utils.js"
6
8
 
9
+ // Badges and chips take radius sm (CI §6). The status variants follow the tone
10
+ // model: tint + foreground text, rim in the tone (TONE_SOFT + TONE_BORDER;
11
+ // CI §3.3 "-soft: Text darauf --cd-schwarz"). The ink is never text on a tint.
7
12
  const badgeVariants = cva(
8
- "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
13
+ `group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-sm border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all ${FOCUS_RING} has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-danger [&>svg]:pointer-events-none [&>svg]:size-3!`,
9
14
  {
10
15
  variants: {
11
16
  variant: {
12
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
17
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary-hover",
13
18
  secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
14
- destructive:
15
- "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
16
- outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
17
- ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
18
- link: "text-primary underline-offset-4 hover:underline",
19
+ destructive: `${TONE_SOFT.danger} ${TONE_BORDER.danger}`,
20
+ success: `${TONE_SOFT.success} ${TONE_BORDER.success}`,
21
+ warning: `${TONE_SOFT.warning} ${TONE_BORDER.warning}`,
22
+ info: `${TONE_SOFT.info} ${TONE_BORDER.info}`,
23
+ outline: "border-border text-foreground [a]:hover:bg-muted",
24
+ ghost: "hover:bg-muted",
25
+ link: "text-link underline-offset-4 hover:underline",
19
26
  },
20
27
  },
21
28
  defaultVariants: {
@@ -2,29 +2,34 @@ import * as React from "react"
2
2
  import { cva, type VariantProps } from "class-variance-authority"
3
3
  import { Slot } from "radix-ui"
4
4
 
5
+ import { FOCUS_RING } from "../lib/focus-ring.js"
5
6
  import { cn } from "../lib/utils.js"
6
7
 
8
+ // Miragon CI §8: radius md, weight 600; primary = blue fill with white text and
9
+ // shadow-1, hover deeper blue + shadow-2 + a 1px lift; secondary/outline =
10
+ // surface with a line border, hover blue border and text on the accent tint.
7
11
  const buttonVariants = cva(
8
- "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
12
+ `inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-semibold whitespace-nowrap transition-[color,background-color,border-color,box-shadow,translate] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-danger ${FOCUS_RING} [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`,
9
13
  {
10
14
  variants: {
11
15
  variant: {
12
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
16
+ default:
17
+ "bg-primary text-primary-foreground shadow-1 hover:bg-primary-hover hover:shadow-2 hover:-translate-y-px active:translate-y-0",
13
18
  destructive:
14
- "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
19
+ "bg-destructive text-destructive-foreground shadow-1 hover:bg-destructive/90 hover:shadow-2",
15
20
  outline:
16
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
21
+ "border border-border bg-background text-foreground hover:border-focus hover:bg-accent hover:text-accent-foreground",
17
22
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
19
- link: "text-primary underline-offset-4 hover:underline",
23
+ ghost: "hover:bg-accent hover:text-accent-foreground",
24
+ link: "text-link underline-offset-4 hover:underline",
20
25
  },
21
26
  size: {
22
27
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
23
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
28
+ xs: "h-6 gap-1 rounded-sm px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
24
29
  sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
25
30
  lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
26
31
  icon: "size-9",
27
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
32
+ "icon-xs": "size-6 rounded-sm [&_svg:not([class*='size-'])]:size-3",
28
33
  "icon-sm": "size-8",
29
34
  "icon-lg": "size-10",
30
35
  },
@@ -12,7 +12,7 @@ function Card({
12
12
  data-slot="card"
13
13
  data-size={size}
14
14
  className={cn(
15
- "group/card bg-card text-card-foreground ring-foreground/10 flex flex-col gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
15
+ "group/card bg-card text-card-foreground ring-border flex flex-col gap-4 overflow-hidden rounded-md py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-md *:[img:last-child]:rounded-b-md",
16
16
  className,
17
17
  )}
18
18
  {...props}
@@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
25
25
  <div
26
26
  data-slot="card-header"
27
27
  className={cn(
28
- "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-md px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
29
29
  className,
30
30
  )}
31
31
  {...props}
@@ -38,7 +38,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
38
38
  <div
39
39
  data-slot="card-title"
40
40
  className={cn(
41
- "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
41
+ "font-heading text-base leading-snug font-semibold group-data-[size=sm]/card:text-sm",
42
42
  className,
43
43
  )}
44
44
  {...props}
@@ -81,7 +81,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
81
81
  <div
82
82
  data-slot="card-footer"
83
83
  className={cn(
84
- "bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3",
84
+ "bg-muted/50 flex items-center rounded-b-md border-t p-4 group-data-[size=sm]/card:p-3",
85
85
  className,
86
86
  )}
87
87
  {...props}