@miragon/mcp-toolkit-ui 0.10.1

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 (320) hide show
  1. package/README.md +36 -0
  2. package/dist/app/adapt-data-widget.d.ts +33 -0
  3. package/dist/app/adapt-data-widget.d.ts.map +1 -0
  4. package/dist/app/adapt-data-widget.js +35 -0
  5. package/dist/app/adapt-data-widget.js.map +1 -0
  6. package/dist/app/builder/CatalogueSheet.d.ts +13 -0
  7. package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
  8. package/dist/app/builder/CatalogueSheet.js +33 -0
  9. package/dist/app/builder/CatalogueSheet.js.map +1 -0
  10. package/dist/app/builder/PipelineStrip.d.ts +22 -0
  11. package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
  12. package/dist/app/builder/PipelineStrip.js +31 -0
  13. package/dist/app/builder/PipelineStrip.js.map +1 -0
  14. package/dist/app/builder/PreviewPane.d.ts +13 -0
  15. package/dist/app/builder/PreviewPane.d.ts.map +1 -0
  16. package/dist/app/builder/PreviewPane.js +21 -0
  17. package/dist/app/builder/PreviewPane.js.map +1 -0
  18. package/dist/app/builder/SaveDialog.d.ts +14 -0
  19. package/dist/app/builder/SaveDialog.d.ts.map +1 -0
  20. package/dist/app/builder/SaveDialog.js +14 -0
  21. package/dist/app/builder/SaveDialog.js.map +1 -0
  22. package/dist/app/builder/Toolbar.d.ts +17 -0
  23. package/dist/app/builder/Toolbar.d.ts.map +1 -0
  24. package/dist/app/builder/Toolbar.js +15 -0
  25. package/dist/app/builder/Toolbar.js.map +1 -0
  26. package/dist/app/builder/Workspace.d.ts +28 -0
  27. package/dist/app/builder/Workspace.d.ts.map +1 -0
  28. package/dist/app/builder/Workspace.js +117 -0
  29. package/dist/app/builder/Workspace.js.map +1 -0
  30. package/dist/app/builder/builder-model.d.ts +86 -0
  31. package/dist/app/builder/builder-model.d.ts.map +1 -0
  32. package/dist/app/builder/builder-model.js +174 -0
  33. package/dist/app/builder/builder-model.js.map +1 -0
  34. package/dist/app/builder/builder-reducer.d.ts +71 -0
  35. package/dist/app/builder/builder-reducer.d.ts.map +1 -0
  36. package/dist/app/builder/builder-reducer.js +201 -0
  37. package/dist/app/builder/builder-reducer.js.map +1 -0
  38. package/dist/app/builder/labels.d.ts +78 -0
  39. package/dist/app/builder/labels.d.ts.map +1 -0
  40. package/dist/app/builder/labels.js +71 -0
  41. package/dist/app/builder/labels.js.map +1 -0
  42. package/dist/app/builder/status-chip.d.ts +10 -0
  43. package/dist/app/builder/status-chip.d.ts.map +1 -0
  44. package/dist/app/builder/status-chip.js +16 -0
  45. package/dist/app/builder/status-chip.js.map +1 -0
  46. package/dist/app/host-bridge.d.ts +211 -0
  47. package/dist/app/host-bridge.d.ts.map +1 -0
  48. package/dist/app/host-bridge.js +247 -0
  49. package/dist/app/host-bridge.js.map +1 -0
  50. package/dist/app/index.d.ts +16 -0
  51. package/dist/app/index.d.ts.map +1 -0
  52. package/dist/app/index.js +18 -0
  53. package/dist/app/index.js.map +1 -0
  54. package/dist/app/layout-builder.d.ts +48 -0
  55. package/dist/app/layout-builder.d.ts.map +1 -0
  56. package/dist/app/layout-builder.js +418 -0
  57. package/dist/app/layout-builder.js.map +1 -0
  58. package/dist/app/mcp-app-view.d.ts +43 -0
  59. package/dist/app/mcp-app-view.d.ts.map +1 -0
  60. package/dist/app/mcp-app-view.js +169 -0
  61. package/dist/app/mcp-app-view.js.map +1 -0
  62. package/dist/app/mcp-toolkit-app.d.ts +18 -0
  63. package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
  64. package/dist/app/mcp-toolkit-app.js +22 -0
  65. package/dist/app/mcp-toolkit-app.js.map +1 -0
  66. package/dist/app/use-host-actions.d.ts +57 -0
  67. package/dist/app/use-host-actions.d.ts.map +1 -0
  68. package/dist/app/use-host-actions.js +42 -0
  69. package/dist/app/use-host-actions.js.map +1 -0
  70. package/dist/app/widget-fixture.d.ts +180 -0
  71. package/dist/app/widget-fixture.d.ts.map +1 -0
  72. package/dist/app/widget-fixture.js +273 -0
  73. package/dist/app/widget-fixture.js.map +1 -0
  74. package/dist/app/widget-props-sheet.d.ts +20 -0
  75. package/dist/app/widget-props-sheet.d.ts.map +1 -0
  76. package/dist/app/widget-props-sheet.js +87 -0
  77. package/dist/app/widget-props-sheet.js.map +1 -0
  78. package/dist/app/widget-renderer.d.ts +87 -0
  79. package/dist/app/widget-renderer.d.ts.map +1 -0
  80. package/dist/app/widget-renderer.js +105 -0
  81. package/dist/app/widget-renderer.js.map +1 -0
  82. package/dist/components/DrillButton.d.ts +22 -0
  83. package/dist/components/DrillButton.d.ts.map +1 -0
  84. package/dist/components/DrillButton.js +13 -0
  85. package/dist/components/DrillButton.js.map +1 -0
  86. package/dist/components/FilterBar.d.ts +28 -0
  87. package/dist/components/FilterBar.d.ts.map +1 -0
  88. package/dist/components/FilterBar.js +12 -0
  89. package/dist/components/FilterBar.js.map +1 -0
  90. package/dist/components/GridLayout.d.ts +26 -0
  91. package/dist/components/GridLayout.d.ts.map +1 -0
  92. package/dist/components/GridLayout.js +31 -0
  93. package/dist/components/GridLayout.js.map +1 -0
  94. package/dist/components/GroupCard.d.ts +21 -0
  95. package/dist/components/GroupCard.d.ts.map +1 -0
  96. package/dist/components/GroupCard.js +24 -0
  97. package/dist/components/GroupCard.js.map +1 -0
  98. package/dist/components/KpiGrid.d.ts +43 -0
  99. package/dist/components/KpiGrid.d.ts.map +1 -0
  100. package/dist/components/KpiGrid.js +35 -0
  101. package/dist/components/KpiGrid.js.map +1 -0
  102. package/dist/components/ListFooter.d.ts +22 -0
  103. package/dist/components/ListFooter.d.ts.map +1 -0
  104. package/dist/components/ListFooter.js +13 -0
  105. package/dist/components/ListFooter.js.map +1 -0
  106. package/dist/components/Pills.d.ts +25 -0
  107. package/dist/components/Pills.d.ts.map +1 -0
  108. package/dist/components/Pills.js +17 -0
  109. package/dist/components/Pills.js.map +1 -0
  110. package/dist/components/SectionHeading.d.ts +16 -0
  111. package/dist/components/SectionHeading.d.ts.map +1 -0
  112. package/dist/components/SectionHeading.js +10 -0
  113. package/dist/components/SectionHeading.js.map +1 -0
  114. package/dist/components/WidgetHeader.d.ts +21 -0
  115. package/dist/components/WidgetHeader.d.ts.map +1 -0
  116. package/dist/components/WidgetHeader.js +11 -0
  117. package/dist/components/WidgetHeader.js.map +1 -0
  118. package/dist/hooks/index.d.ts +11 -0
  119. package/dist/hooks/index.d.ts.map +1 -0
  120. package/dist/hooks/index.js +7 -0
  121. package/dist/hooks/index.js.map +1 -0
  122. package/dist/hooks/use-debounced-value.d.ts +7 -0
  123. package/dist/hooks/use-debounced-value.d.ts.map +1 -0
  124. package/dist/hooks/use-debounced-value.js +15 -0
  125. package/dist/hooks/use-debounced-value.js.map +1 -0
  126. package/dist/hooks/use-mobile.d.ts +6 -0
  127. package/dist/hooks/use-mobile.d.ts.map +1 -0
  128. package/dist/hooks/use-mobile.js +19 -0
  129. package/dist/hooks/use-mobile.js.map +1 -0
  130. package/dist/hooks/use-tool-query.d.ts +21 -0
  131. package/dist/hooks/use-tool-query.d.ts.map +1 -0
  132. package/dist/hooks/use-tool-query.js +47 -0
  133. package/dist/hooks/use-tool-query.js.map +1 -0
  134. package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
  135. package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
  136. package/dist/hooks/use-tool-result-recovery.js +93 -0
  137. package/dist/hooks/use-tool-result-recovery.js.map +1 -0
  138. package/dist/hooks/use-view-data.d.ts +19 -0
  139. package/dist/hooks/use-view-data.d.ts.map +1 -0
  140. package/dist/hooks/use-view-data.js +18 -0
  141. package/dist/hooks/use-view-data.js.map +1 -0
  142. package/dist/hooks/use-view-tool-query.d.ts +18 -0
  143. package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
  144. package/dist/hooks/use-view-tool-query.js +29 -0
  145. package/dist/hooks/use-view-tool-query.js.map +1 -0
  146. package/dist/index.d.ts +47 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +56 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/lib/parse-tool-result.d.ts +39 -0
  151. package/dist/lib/parse-tool-result.d.ts.map +1 -0
  152. package/dist/lib/parse-tool-result.js +66 -0
  153. package/dist/lib/parse-tool-result.js.map +1 -0
  154. package/dist/lib/tone-utils.d.ts +18 -0
  155. package/dist/lib/tone-utils.d.ts.map +1 -0
  156. package/dist/lib/tone-utils.js +25 -0
  157. package/dist/lib/tone-utils.js.map +1 -0
  158. package/dist/lib/utils.d.ts +3 -0
  159. package/dist/lib/utils.d.ts.map +1 -0
  160. package/dist/lib/utils.js +6 -0
  161. package/dist/lib/utils.js.map +1 -0
  162. package/dist/locale/index.d.ts +3 -0
  163. package/dist/locale/index.d.ts.map +1 -0
  164. package/dist/locale/index.js +7 -0
  165. package/dist/locale/index.js.map +1 -0
  166. package/dist/locale/locale-provider.d.ts +50 -0
  167. package/dist/locale/locale-provider.d.ts.map +1 -0
  168. package/dist/locale/locale-provider.js +47 -0
  169. package/dist/locale/locale-provider.js.map +1 -0
  170. package/dist/primitives/alert.d.ts +10 -0
  171. package/dist/primitives/alert.d.ts.map +1 -0
  172. package/dist/primitives/alert.js +25 -0
  173. package/dist/primitives/alert.js.map +1 -0
  174. package/dist/primitives/badge.d.ts +8 -0
  175. package/dist/primitives/badge.d.ts.map +1 -0
  176. package/dist/primitives/badge.js +34 -0
  177. package/dist/primitives/badge.js.map +1 -0
  178. package/dist/primitives/button.d.ts +11 -0
  179. package/dist/primitives/button.d.ts.map +1 -0
  180. package/dist/primitives/button.js +36 -0
  181. package/dist/primitives/button.js.map +1 -0
  182. package/dist/primitives/card.d.ts +12 -0
  183. package/dist/primitives/card.d.ts.map +1 -0
  184. package/dist/primitives/card.js +25 -0
  185. package/dist/primitives/card.js.map +1 -0
  186. package/dist/primitives/dialog.d.ts +18 -0
  187. package/dist/primitives/dialog.d.ts.map +1 -0
  188. package/dist/primitives/dialog.js +38 -0
  189. package/dist/primitives/dialog.js.map +1 -0
  190. package/dist/primitives/input.d.ts +4 -0
  191. package/dist/primitives/input.d.ts.map +1 -0
  192. package/dist/primitives/input.js +7 -0
  193. package/dist/primitives/input.js.map +1 -0
  194. package/dist/primitives/scroll-area.d.ts +6 -0
  195. package/dist/primitives/scroll-area.d.ts.map +1 -0
  196. package/dist/primitives/scroll-area.js +11 -0
  197. package/dist/primitives/scroll-area.js.map +1 -0
  198. package/dist/primitives/select.d.ts +16 -0
  199. package/dist/primitives/select.d.ts.map +1 -0
  200. package/dist/primitives/select.js +38 -0
  201. package/dist/primitives/select.js.map +1 -0
  202. package/dist/primitives/separator.d.ts +4 -0
  203. package/dist/primitives/separator.d.ts.map +1 -0
  204. package/dist/primitives/separator.js +9 -0
  205. package/dist/primitives/separator.js.map +1 -0
  206. package/dist/primitives/sheet.d.ts +15 -0
  207. package/dist/primitives/sheet.d.ts.map +1 -0
  208. package/dist/primitives/sheet.js +41 -0
  209. package/dist/primitives/sheet.js.map +1 -0
  210. package/dist/primitives/skeleton.d.ts +3 -0
  211. package/dist/primitives/skeleton.d.ts.map +1 -0
  212. package/dist/primitives/skeleton.js +7 -0
  213. package/dist/primitives/skeleton.js.map +1 -0
  214. package/dist/primitives/switch.d.ts +5 -0
  215. package/dist/primitives/switch.d.ts.map +1 -0
  216. package/dist/primitives/switch.js +8 -0
  217. package/dist/primitives/switch.js.map +1 -0
  218. package/dist/primitives/table.d.ts +11 -0
  219. package/dist/primitives/table.d.ts.map +1 -0
  220. package/dist/primitives/table.js +28 -0
  221. package/dist/primitives/table.js.map +1 -0
  222. package/dist/primitives/tabs.d.ts +12 -0
  223. package/dist/primitives/tabs.d.ts.map +1 -0
  224. package/dist/primitives/tabs.js +29 -0
  225. package/dist/primitives/tabs.js.map +1 -0
  226. package/dist/providers/query-provider.d.ts +11 -0
  227. package/dist/providers/query-provider.d.ts.map +1 -0
  228. package/dist/providers/query-provider.js +20 -0
  229. package/dist/providers/query-provider.js.map +1 -0
  230. package/dist/theme/create-theme.d.ts +130 -0
  231. package/dist/theme/create-theme.d.ts.map +1 -0
  232. package/dist/theme/create-theme.js +65 -0
  233. package/dist/theme/create-theme.js.map +1 -0
  234. package/dist/theme/index.d.ts +7 -0
  235. package/dist/theme/index.d.ts.map +1 -0
  236. package/dist/theme/index.js +7 -0
  237. package/dist/theme/index.js.map +1 -0
  238. package/dist/theme/presets.d.ts +25 -0
  239. package/dist/theme/presets.d.ts.map +1 -0
  240. package/dist/theme/presets.js +68 -0
  241. package/dist/theme/presets.js.map +1 -0
  242. package/dist/theme/theme-provider.d.ts +69 -0
  243. package/dist/theme/theme-provider.d.ts.map +1 -0
  244. package/dist/theme/theme-provider.js +79 -0
  245. package/dist/theme/theme-provider.js.map +1 -0
  246. package/package.json +71 -0
  247. package/src/app/adapt-data-widget.tsx +51 -0
  248. package/src/app/builder/CatalogueSheet.tsx +230 -0
  249. package/src/app/builder/PipelineStrip.tsx +260 -0
  250. package/src/app/builder/PreviewPane.tsx +72 -0
  251. package/src/app/builder/SaveDialog.tsx +95 -0
  252. package/src/app/builder/Toolbar.tsx +95 -0
  253. package/src/app/builder/Workspace.tsx +483 -0
  254. package/src/app/builder/builder-model.test.ts +351 -0
  255. package/src/app/builder/builder-model.ts +213 -0
  256. package/src/app/builder/builder-reducer.test.ts +441 -0
  257. package/src/app/builder/builder-reducer.ts +289 -0
  258. package/src/app/builder/labels.ts +139 -0
  259. package/src/app/builder/status-chip.tsx +37 -0
  260. package/src/app/host-bridge.test.ts +206 -0
  261. package/src/app/host-bridge.tsx +371 -0
  262. package/src/app/index.ts +51 -0
  263. package/src/app/layout-builder.tsx +872 -0
  264. package/src/app/mcp-app-view.tsx +367 -0
  265. package/src/app/mcp-toolkit-app.tsx +25 -0
  266. package/src/app/use-host-actions.test.ts +16 -0
  267. package/src/app/use-host-actions.tsx +88 -0
  268. package/src/app/widget-fixture.test.ts +108 -0
  269. package/src/app/widget-fixture.tsx +441 -0
  270. package/src/app/widget-props-sheet.tsx +285 -0
  271. package/src/app/widget-renderer.test.ts +42 -0
  272. package/src/app/widget-renderer.tsx +200 -0
  273. package/src/components/DrillButton.tsx +37 -0
  274. package/src/components/FilterBar.tsx +68 -0
  275. package/src/components/GridLayout.tsx +49 -0
  276. package/src/components/GroupCard.tsx +52 -0
  277. package/src/components/KpiGrid.tsx +134 -0
  278. package/src/components/ListFooter.tsx +48 -0
  279. package/src/components/Pills.tsx +45 -0
  280. package/src/components/SectionHeading.tsx +26 -0
  281. package/src/components/WidgetHeader.tsx +51 -0
  282. package/src/globals.css +168 -0
  283. package/src/hooks/index.ts +17 -0
  284. package/src/hooks/use-debounced-value.ts +15 -0
  285. package/src/hooks/use-mobile.ts +24 -0
  286. package/src/hooks/use-tool-query.ts +63 -0
  287. package/src/hooks/use-tool-result-recovery.test.ts +65 -0
  288. package/src/hooks/use-tool-result-recovery.ts +144 -0
  289. package/src/hooks/use-view-data.ts +31 -0
  290. package/src/hooks/use-view-tool-query.ts +38 -0
  291. package/src/index.ts +134 -0
  292. package/src/lib/parse-tool-result.test.ts +139 -0
  293. package/src/lib/parse-tool-result.ts +89 -0
  294. package/src/lib/tone-utils.test.ts +35 -0
  295. package/src/lib/tone-utils.ts +38 -0
  296. package/src/lib/utils.ts +6 -0
  297. package/src/locale/index.ts +7 -0
  298. package/src/locale/locale-provider.tsx +81 -0
  299. package/src/primitives/alert.tsx +60 -0
  300. package/src/primitives/badge.tsx +49 -0
  301. package/src/primitives/button.tsx +62 -0
  302. package/src/primitives/card.tsx +92 -0
  303. package/src/primitives/dialog.tsx +144 -0
  304. package/src/primitives/input.tsx +21 -0
  305. package/src/primitives/scroll-area.tsx +54 -0
  306. package/src/primitives/select.tsx +173 -0
  307. package/src/primitives/separator.tsx +21 -0
  308. package/src/primitives/sheet.tsx +134 -0
  309. package/src/primitives/skeleton.tsx +13 -0
  310. package/src/primitives/switch.tsx +24 -0
  311. package/src/primitives/table.tsx +87 -0
  312. package/src/primitives/tabs.tsx +79 -0
  313. package/src/providers/query-provider.tsx +38 -0
  314. package/src/theme/create-theme.test.ts +108 -0
  315. package/src/theme/create-theme.ts +158 -0
  316. package/src/theme/index.ts +9 -0
  317. package/src/theme/presets.ts +82 -0
  318. package/src/theme/theme-provider.tsx +152 -0
  319. package/src/ui-catalog.test.ts +112 -0
  320. package/ui-catalog.json +1280 -0
@@ -0,0 +1,52 @@
1
+ import type { KeyboardEvent, MouseEvent, ReactNode } from "react"
2
+
3
+ export interface GroupCardProps {
4
+ /** Whether the body is expanded. */
5
+ expanded: boolean
6
+ /** Toggle handler for the summary bar. */
7
+ onToggle: () => void
8
+ /** Always-visible summary bar (may contain its own buttons/links). */
9
+ summary: ReactNode
10
+ /** Collapsible body, rendered only when `expanded`. */
11
+ children?: ReactNode
12
+ }
13
+
14
+ /**
15
+ * Bordered card with a clickable summary bar and a collapsible body. Use to
16
+ * group related rows behind an expandable header (e.g. items grouped by a key).
17
+ *
18
+ * The summary uses `role="button"` rather than a `<button>` element so it can
19
+ * contain its own controls (drill buttons, links) without producing invalid
20
+ * nested-interactive HTML; clicks landing on those controls do not toggle.
21
+ */
22
+ export function GroupCard({ expanded, onToggle, summary, children }: GroupCardProps) {
23
+ function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
24
+ if (e.key === "Enter" || e.key === " ") {
25
+ e.preventDefault()
26
+ onToggle()
27
+ }
28
+ }
29
+
30
+ function handleClick(e: MouseEvent<HTMLDivElement>) {
31
+ if ((e.target as HTMLElement).closest("button, a, input, select, textarea, label")) return
32
+ onToggle()
33
+ }
34
+
35
+ return (
36
+ <div className="border-border mb-2 overflow-hidden rounded-lg border">
37
+ <div
38
+ role="button"
39
+ tabIndex={0}
40
+ aria-expanded={expanded}
41
+ onClick={handleClick}
42
+ 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
+ expanded ? "border-border border-b" : ""
45
+ }`}
46
+ >
47
+ {summary}
48
+ </div>
49
+ {expanded && children && <div>{children}</div>}
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,134 @@
1
+ import type { ReactNode } from "react"
2
+ import { TONE_TEXT, type ToneVariant } from "../lib/tone-utils.js"
3
+
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",
9
+ }
10
+
11
+ export interface KpiCell {
12
+ /** Caption shown above the value. */
13
+ label: ReactNode
14
+ /** The headline figure. Pre-format numbers for locale/currency. */
15
+ value: ReactNode
16
+ /** Small fraction shown next to the value, e.g. ` /14`. */
17
+ fraction?: ReactNode
18
+ /** Optional trend line under the value (e.g. `"+12% vs last week"`). */
19
+ trend?: ReactNode
20
+ /** Colours the trend line. `up` reads as bad (danger), `down` as good (success). */
21
+ trendDirection?: "up" | "down" | "flat"
22
+ /** Tone applied to the value text (e.g. `"danger"` for a breached metric). */
23
+ tone?: ToneVariant
24
+ /** When set, the cell renders as a button — turns a metric into a nav entry point. */
25
+ onClick?: () => void
26
+ /** Accessible label for the clickable cell (recommended when `onClick` is set). */
27
+ ariaLabel?: string
28
+ }
29
+
30
+ const COL_CLASS: Record<number, string> = {
31
+ 1: "grid-cols-1",
32
+ 2: "grid-cols-2",
33
+ 3: "grid-cols-3",
34
+ 4: "grid-cols-4",
35
+ 5: "grid-cols-5",
36
+ 6: "grid-cols-6",
37
+ }
38
+
39
+ export interface KpiGridHeader {
40
+ /** Group label, rendered uppercase in the strip header. */
41
+ label: ReactNode
42
+ /** Optional muted badge to the right of the label. */
43
+ badge?: ReactNode
44
+ }
45
+
46
+ export interface KpiGridProps {
47
+ /** The metric cells, flowed as columns (1–6, clamped). */
48
+ cells: KpiCell[]
49
+ /** Wrap the strip in a bordered card with an optional group `header`. Default `false`. */
50
+ boxed?: boolean
51
+ /** Group header shown above the strip when `boxed`. */
52
+ header?: KpiGridHeader
53
+ }
54
+
55
+ /**
56
+ * Bordered strip of KPI cells — the canonical way to show a row of headline
57
+ * metrics in a widget (replaces the single-metric KPICard). Cells flow as
58
+ * columns (1–6) and can be made clickable to drill into a detail view. Use
59
+ * `boxed` + `header` for a labelled metric block; unboxed (default) renders the
60
+ * lighter `border-y` strip.
61
+ */
62
+ export function KpiGrid({ cells, boxed = false, header }: KpiGridProps) {
63
+ const cols = Math.min(Math.max(cells.length, 1), 6)
64
+ const colClass = COL_CLASS[cols] ?? "grid-cols-4"
65
+ const wrapperClass = boxed ? "border-border overflow-hidden rounded-lg border" : ""
66
+ const stripClass = boxed ? "" : "border-border border-y"
67
+ return (
68
+ <div className={wrapperClass}>
69
+ {header && (
70
+ <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
+ <span>{header.label}</span>
72
+ {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">
74
+ {header.badge}
75
+ </span>
76
+ )}
77
+ </div>
78
+ )}
79
+ <div className={`grid ${colClass} ${stripClass}`}>
80
+ {cells.map((cell, idx) => {
81
+ 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
+ )
115
+ return cell.onClick ? (
116
+ <button
117
+ key={idx}
118
+ type="button"
119
+ onClick={cell.onClick}
120
+ 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}`}
122
+ >
123
+ {content}
124
+ </button>
125
+ ) : (
126
+ <div key={idx} className={`px-5 py-4 ${borderClass}`}>
127
+ {content}
128
+ </div>
129
+ )
130
+ })}
131
+ </div>
132
+ </div>
133
+ )
134
+ }
@@ -0,0 +1,48 @@
1
+ export interface ListFooterProps {
2
+ /** How many rows are currently rendered. */
3
+ shown: number
4
+ /** The total number of rows available. */
5
+ total: number
6
+ /** Whether more rows can be loaded. */
7
+ hasMore: boolean
8
+ /** Disables the button and shows a loading label while a fetch is in flight. */
9
+ loadingMore?: boolean
10
+ /** Called when the user asks for the next page. */
11
+ onLoadMore: () => void
12
+ /** Plural noun for the count, e.g. `"instances"`. Default `"rows"`. */
13
+ noun?: string
14
+ }
15
+
16
+ /**
17
+ * Footer for a paginated list: an honest "Showing X of Y" total plus an
18
+ * explicit "Load more" control. Deliberately not infinite scroll — the user
19
+ * gets the total, a reachable footer, and control over fetching. Renders
20
+ * nothing when `total` is 0.
21
+ */
22
+ export function ListFooter({
23
+ shown,
24
+ total,
25
+ hasMore,
26
+ loadingMore = false,
27
+ onLoadMore,
28
+ noun = "rows",
29
+ }: ListFooterProps) {
30
+ if (total <= 0) return null
31
+ return (
32
+ <div className="text-muted-foreground flex items-center justify-between gap-3 px-1 py-2 text-xs">
33
+ <span>
34
+ Showing {shown.toLocaleString()} of {total.toLocaleString()} {noun}
35
+ </span>
36
+ {hasMore && (
37
+ <button
38
+ type="button"
39
+ onClick={onLoadMore}
40
+ 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"
42
+ >
43
+ {loadingMore ? "Loading…" : "Load more"}
44
+ </button>
45
+ )}
46
+ </div>
47
+ )
48
+ }
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react"
2
+ import { TONE_DOT, TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
3
+
4
+ export interface LivePillProps {
5
+ /** Status tone driving the dot + tint. Default `"info"`. */
6
+ tone?: ToneVariant
7
+ /** Pill label. Defaults to `"Live"`. */
8
+ children?: ReactNode
9
+ }
10
+
11
+ /**
12
+ * 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
+ */
15
+ export function LivePill({ tone = "info", children }: LivePillProps) {
16
+ return (
17
+ <span
18
+ className={`inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-semibold ${TONE_SOFT[tone]}`}
19
+ >
20
+ <span className={`size-1.5 animate-pulse rounded-full ${TONE_DOT[tone]}`} />
21
+ {children ?? "Live"}
22
+ </span>
23
+ )
24
+ }
25
+
26
+ export interface CountPillProps {
27
+ /** Status tone driving the tint. Default `"neutral"`. */
28
+ tone?: ToneVariant
29
+ /** The count/value to display (tabular numbers). */
30
+ children: ReactNode
31
+ }
32
+
33
+ /**
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).
36
+ */
37
+ export function CountPill({ tone = "neutral", children }: CountPillProps) {
38
+ return (
39
+ <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]}`}
41
+ >
42
+ {children}
43
+ </span>
44
+ )
45
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export interface SectionHeadingProps {
4
+ /** The section title. */
5
+ title: ReactNode
6
+ /** Optional muted hint shown on the right (e.g. a count or last-updated note). */
7
+ hint?: ReactNode
8
+ /** Right-aligned slot that overrides `hint` when set (e.g. a control). */
9
+ trailing?: ReactNode
10
+ }
11
+
12
+ /**
13
+ * Section title row — a small semibold heading with an optional muted hint or
14
+ * control on the right. Use to label a block inside a widget body (e.g. a list
15
+ * or sub-table) without the visual weight of a full card header.
16
+ */
17
+ export function SectionHeading({ title, hint, trailing }: SectionHeadingProps) {
18
+ return (
19
+ <div className="text-muted-foreground mb-3 flex items-center justify-between text-sm font-semibold">
20
+ <span>{title}</span>
21
+ {(hint || trailing) && (
22
+ <span className="text-muted-foreground text-xs font-medium">{trailing ?? hint}</span>
23
+ )}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,51 @@
1
+ import type { ReactNode } from "react"
2
+ import { TONE_SOFT, type ToneVariant } from "../lib/tone-utils.js"
3
+
4
+ export interface WidgetHeaderProps {
5
+ /** Optional icon/glyph rendered in a tinted tile above the title. */
6
+ icon?: ReactNode
7
+ /** Tone for the icon tile. Default `"neutral"`. */
8
+ iconTone?: ToneVariant
9
+ /** The widget title. */
10
+ title: ReactNode
11
+ /** Subtitle/meta row under the title (e.g. a live pill + counts). */
12
+ sub?: ReactNode
13
+ /** Right-aligned actions slot (buttons, drill controls). */
14
+ actions?: ReactNode
15
+ }
16
+
17
+ /**
18
+ * Page-level header for the top of a dashboard widget: an optional tinted icon
19
+ * tile, a large title, a subtitle/meta row, and a right-aligned actions slot.
20
+ * Use once per widget surface to anchor it.
21
+ */
22
+ export function WidgetHeader({
23
+ icon,
24
+ iconTone = "neutral",
25
+ title,
26
+ sub,
27
+ actions,
28
+ }: WidgetHeaderProps) {
29
+ return (
30
+ <header className="flex flex-wrap items-start justify-between gap-4">
31
+ <div className="min-w-0">
32
+ {icon && (
33
+ <div
34
+ className={`mb-3.5 grid size-11 place-items-center rounded-xl text-xl ${TONE_SOFT[iconTone]}`}
35
+ >
36
+ {icon}
37
+ </div>
38
+ )}
39
+ <h1 className="text-foreground mb-1.5 text-3xl leading-tight font-bold tracking-tight">
40
+ {title}
41
+ </h1>
42
+ {sub && (
43
+ <div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
44
+ {sub}
45
+ </div>
46
+ )}
47
+ </div>
48
+ {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
49
+ </header>
50
+ )
51
+ }
@@ -0,0 +1,168 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "@fontsource-variable/geist";
4
+
5
+ @source "../**/*.{ts,tsx}";
6
+
7
+ @custom-variant dark (&:is(.dark *));
8
+
9
+ @theme inline {
10
+ --font-heading: var(--font-sans);
11
+ --font-sans: "Geist Variable", sans-serif;
12
+ --color-sidebar-ring: var(--sidebar-ring);
13
+ --color-sidebar-border: var(--sidebar-border);
14
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
15
+ --color-sidebar-accent: var(--sidebar-accent);
16
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
17
+ --color-sidebar-primary: var(--sidebar-primary);
18
+ --color-sidebar-foreground: var(--sidebar-foreground);
19
+ --color-sidebar: var(--sidebar);
20
+ --color-chart-5: var(--chart-5);
21
+ --color-chart-4: var(--chart-4);
22
+ --color-chart-3: var(--chart-3);
23
+ --color-chart-2: var(--chart-2);
24
+ --color-chart-1: var(--chart-1);
25
+ --color-ring: var(--ring);
26
+ --color-input: var(--input);
27
+ --color-border: var(--border);
28
+ --color-destructive: var(--destructive);
29
+ --color-accent-foreground: var(--accent-foreground);
30
+ --color-accent: var(--accent);
31
+ --color-muted-foreground: var(--muted-foreground);
32
+ --color-muted: var(--muted);
33
+ --color-secondary-foreground: var(--secondary-foreground);
34
+ --color-secondary: var(--secondary);
35
+ --color-primary-foreground: var(--primary-foreground);
36
+ --color-primary: var(--primary);
37
+ --color-popover-foreground: var(--popover-foreground);
38
+ --color-popover: var(--popover);
39
+ --color-card-foreground: var(--card-foreground);
40
+ --color-card: var(--card);
41
+ --color-foreground: var(--foreground);
42
+ --color-background: var(--background);
43
+ /* Semantic status tones — the token surface behind ToneVariant/TONE_* maps.
44
+ Each tone exposes a readable foreground (`text-danger`) and a tinted
45
+ surface (`bg-danger-soft`); both carry light + dark values so tone-based
46
+ components (Pills, KpiGrid, WidgetHeader, FilterBar) re-theme automatically. */
47
+ --color-danger: var(--danger);
48
+ --color-danger-soft: var(--danger-soft);
49
+ --color-warning: var(--warning);
50
+ --color-warning-soft: var(--warning-soft);
51
+ --color-success: var(--success);
52
+ --color-success-soft: var(--success-soft);
53
+ --color-info: var(--info);
54
+ --color-info-soft: var(--info-soft);
55
+ --radius-sm: calc(var(--radius) * 0.6);
56
+ --radius-md: calc(var(--radius) * 0.8);
57
+ --radius-lg: var(--radius);
58
+ --radius-xl: calc(var(--radius) * 1.4);
59
+ --radius-2xl: calc(var(--radius) * 1.8);
60
+ --radius-3xl: calc(var(--radius) * 2.2);
61
+ --radius-4xl: calc(var(--radius) * 2.6);
62
+ }
63
+
64
+ :root {
65
+ --background: oklch(1 0 0);
66
+ --foreground: oklch(0.145 0 0);
67
+ --card: oklch(1 0 0);
68
+ --card-foreground: oklch(0.145 0 0);
69
+ --popover: oklch(1 0 0);
70
+ --popover-foreground: oklch(0.145 0 0);
71
+ --primary: oklch(0.205 0 0);
72
+ --primary-foreground: oklch(0.985 0 0);
73
+ --secondary: oklch(0.97 0 0);
74
+ --secondary-foreground: oklch(0.205 0 0);
75
+ --muted: oklch(0.97 0 0);
76
+ --muted-foreground: oklch(0.556 0 0);
77
+ --accent: oklch(0.97 0 0);
78
+ --accent-foreground: oklch(0.205 0 0);
79
+ --destructive: oklch(0.577 0.245 27.325);
80
+ --border: oklch(0.922 0 0);
81
+ --input: oklch(0.922 0 0);
82
+ --ring: oklch(0.708 0 0);
83
+ --chart-1: oklch(0.87 0 0);
84
+ --chart-2: oklch(0.556 0 0);
85
+ --chart-3: oklch(0.439 0 0);
86
+ --chart-4: oklch(0.371 0 0);
87
+ --chart-5: oklch(0.269 0 0);
88
+ --radius: 0.625rem;
89
+ --sidebar: oklch(0.985 0 0);
90
+ --sidebar-foreground: oklch(0.145 0 0);
91
+ --sidebar-primary: oklch(0.205 0 0);
92
+ --sidebar-primary-foreground: oklch(0.985 0 0);
93
+ --sidebar-accent: oklch(0.97 0 0);
94
+ --sidebar-accent-foreground: oklch(0.205 0 0);
95
+ --sidebar-border: oklch(0.922 0 0);
96
+ --sidebar-ring: oklch(0.708 0 0);
97
+ /* Semantic status tones (light). Foreground = readable on a card; *-soft =
98
+ a tinted surface for badges/dots/icon tiles. Neutral tone reuses the
99
+ muted ramp and needs no dedicated token. */
100
+ --danger: oklch(0.55 0.21 27.3);
101
+ --danger-soft: oklch(0.95 0.04 27.3);
102
+ --warning: oklch(0.62 0.16 70);
103
+ --warning-soft: oklch(0.95 0.05 80);
104
+ --success: oklch(0.58 0.15 152);
105
+ --success-soft: oklch(0.95 0.05 152);
106
+ --info: oklch(0.55 0.2 255);
107
+ --info-soft: oklch(0.95 0.04 255);
108
+ }
109
+
110
+ .dark {
111
+ --background: oklch(0.145 0 0);
112
+ --foreground: oklch(0.985 0 0);
113
+ --card: oklch(0.205 0 0);
114
+ --card-foreground: oklch(0.985 0 0);
115
+ --popover: oklch(0.205 0 0);
116
+ --popover-foreground: oklch(0.985 0 0);
117
+ --primary: oklch(0.922 0 0);
118
+ --primary-foreground: oklch(0.205 0 0);
119
+ --secondary: oklch(0.269 0 0);
120
+ --secondary-foreground: oklch(0.985 0 0);
121
+ --muted: oklch(0.269 0 0);
122
+ --muted-foreground: oklch(0.708 0 0);
123
+ --accent: oklch(0.269 0 0);
124
+ --accent-foreground: oklch(0.985 0 0);
125
+ --destructive: oklch(0.704 0.191 22.216);
126
+ --border: oklch(1 0 0 / 10%);
127
+ --input: oklch(1 0 0 / 15%);
128
+ --ring: oklch(0.556 0 0);
129
+ --chart-1: oklch(0.87 0 0);
130
+ --chart-2: oklch(0.556 0 0);
131
+ --chart-3: oklch(0.439 0 0);
132
+ --chart-4: oklch(0.371 0 0);
133
+ --chart-5: oklch(0.269 0 0);
134
+ --sidebar: oklch(0.205 0 0);
135
+ --sidebar-foreground: oklch(0.985 0 0);
136
+ --sidebar-primary: oklch(0.488 0.243 264.376);
137
+ --sidebar-primary-foreground: oklch(0.985 0 0);
138
+ --sidebar-accent: oklch(0.269 0 0);
139
+ --sidebar-accent-foreground: oklch(0.985 0 0);
140
+ --sidebar-border: oklch(1 0 0 / 10%);
141
+ --sidebar-ring: oklch(0.556 0 0);
142
+ /* Semantic status tones (dark). Brighter foregrounds for contrast on the
143
+ dark surface; *-soft is a low-alpha tint of the same hue. */
144
+ --danger: oklch(0.7 0.19 22.2);
145
+ --danger-soft: oklch(0.7 0.19 22.2 / 18%);
146
+ --warning: oklch(0.78 0.15 75);
147
+ --warning-soft: oklch(0.78 0.15 75 / 18%);
148
+ --success: oklch(0.72 0.17 152);
149
+ --success-soft: oklch(0.72 0.17 152 / 18%);
150
+ --info: oklch(0.7 0.16 255);
151
+ --info-soft: oklch(0.7 0.16 255 / 18%);
152
+ }
153
+
154
+ * {
155
+ font-family: "Geist Variable", ui-sans-serif, system-ui, sans-serif !important;
156
+ }
157
+
158
+ @layer base {
159
+ *,
160
+ *::before,
161
+ *::after {
162
+ @apply border-border outline-ring/50;
163
+ }
164
+ html,
165
+ body {
166
+ @apply bg-background text-foreground;
167
+ }
168
+ }
@@ -0,0 +1,17 @@
1
+ export { useIsMobile } from "./use-mobile.js"
2
+ export { useDebouncedValue } from "./use-debounced-value.js"
3
+ export { useToolQuery, useToolMutation } from "./use-tool-query.js"
4
+ export type { UseToolQueryOptions } from "./use-tool-query.js"
5
+ export { useViewToolQuery } from "./use-view-tool-query.js"
6
+ export type { UseViewToolQueryOptions } from "./use-view-tool-query.js"
7
+ export { useViewData } from "./use-view-data.js"
8
+ export type { ViewDataResult } from "./use-view-data.js"
9
+ export {
10
+ useToolResultRecovery,
11
+ resolveRecoveryToolName,
12
+ decodeRecoveredResult,
13
+ } from "./use-tool-result-recovery.js"
14
+ export type {
15
+ ToolResultRecoveryOptions,
16
+ ToolResultRecoveryState,
17
+ } from "./use-tool-result-recovery.js"
@@ -0,0 +1,15 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ /**
4
+ * Returns `value` delayed by `delayMs`, resetting the timer on every change.
5
+ * Use to debounce a search box so a server-side filter only re-queries once the
6
+ * user pauses typing (not on every keystroke). Pure React — no host dependency.
7
+ */
8
+ export function useDebouncedValue<T>(value: T, delayMs = 300): T {
9
+ const [debounced, setDebounced] = useState(value)
10
+ useEffect(() => {
11
+ const id = setTimeout(() => setDebounced(value), delayMs)
12
+ return () => clearTimeout(id)
13
+ }, [value, delayMs])
14
+ return debounced
15
+ }
@@ -0,0 +1,24 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ /**
6
+ * Tracks whether the viewport is below the mobile breakpoint (768px), updating
7
+ * on resize. Use to switch a widget between a compact and a wide layout.
8
+ */
9
+ export function useIsMobile() {
10
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(() =>
11
+ typeof window === "undefined" ? undefined : window.innerWidth < MOBILE_BREAKPOINT,
12
+ )
13
+
14
+ React.useEffect(() => {
15
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
16
+ const onChange = () => {
17
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
18
+ }
19
+ mql.addEventListener("change", onChange)
20
+ return () => mql.removeEventListener("change", onChange)
21
+ }, [])
22
+
23
+ return !!isMobile
24
+ }
@@ -0,0 +1,63 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
2
+ import { useCallTool } from "../providers/query-provider.js"
3
+ import { parseToolResult } from "../lib/parse-tool-result.js"
4
+
5
+ export interface UseToolQueryOptions<TData = unknown, TSelected = TData> {
6
+ enabled?: boolean
7
+ select?: (data: TData) => TSelected
8
+ }
9
+
10
+ /**
11
+ * Loads data from a widget tool via TanStack Query.
12
+ *
13
+ * `args` is appended to the effective query key so two callers that share
14
+ * the supplied `queryKey` but pass different `args` get separate cache
15
+ * entries (otherwise the second caller silently reads stale data from the
16
+ * first — the classic queryKey-vs-args trap). `select` transforms data on
17
+ * each read without mutating the cache.
18
+ */
19
+ export function useToolQuery<TData = unknown, TSelected = TData>(
20
+ queryKey: unknown[],
21
+ toolName: string,
22
+ args: object,
23
+ opts?: UseToolQueryOptions<TData, TSelected>,
24
+ ) {
25
+ const callTool = useCallTool()
26
+
27
+ return useQuery<TData, Error, TSelected>({
28
+ queryKey: [...queryKey, args],
29
+ queryFn: async () => {
30
+ if (!callTool) throw new Error("callTool not available")
31
+ const result = await callTool(toolName, args)
32
+ return parseToolResult<TData>(result)
33
+ },
34
+ select: opts?.select,
35
+ enabled: !!callTool && (opts?.enabled ?? true),
36
+ })
37
+ }
38
+
39
+ /**
40
+ * Executes a mutation via a widget tool and invalidates configured query keys.
41
+ */
42
+ export function useToolMutation<TData = unknown>(
43
+ toolName: string,
44
+ opts?: { invalidateKeys?: unknown[][] },
45
+ ) {
46
+ const callTool = useCallTool()
47
+ const queryClient = useQueryClient()
48
+
49
+ return useMutation<TData, Error, Record<string, unknown>>({
50
+ mutationFn: async (args: Record<string, unknown>) => {
51
+ if (!callTool) throw new Error("callTool not available")
52
+ const result = await callTool(toolName, args)
53
+ return parseToolResult<TData>(result)
54
+ },
55
+ onSuccess: async () => {
56
+ if (opts?.invalidateKeys) {
57
+ await Promise.all(
58
+ opts.invalidateKeys.map((key) => queryClient.refetchQueries({ queryKey: key })),
59
+ )
60
+ }
61
+ },
62
+ })
63
+ }