@adres/mgda-ui 0.0.0-stage → 0.0.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 (325) hide show
  1. package/brand-knowledge.json +451 -0
  2. package/knowledge/accordion.md +135 -0
  3. package/knowledge/activity-rings.md +123 -0
  4. package/knowledge/alert.md +184 -0
  5. package/knowledge/answer-actions.md +217 -0
  6. package/knowledge/aspect-ratio.md +82 -0
  7. package/knowledge/avatar.md +158 -0
  8. package/knowledge/badge.md +172 -0
  9. package/knowledge/bar-graph.md +167 -0
  10. package/knowledge/bento-grid.md +115 -0
  11. package/knowledge/blockquote.md +153 -0
  12. package/knowledge/box-list.md +177 -0
  13. package/knowledge/bullet-list.md +128 -0
  14. package/knowledge/button-group.md +179 -0
  15. package/knowledge/button.md +231 -0
  16. package/knowledge/calendar.md +168 -0
  17. package/knowledge/card.md +321 -0
  18. package/knowledge/cards-nav.md +170 -0
  19. package/knowledge/chain-of-thought.md +164 -0
  20. package/knowledge/chat-container.md +135 -0
  21. package/knowledge/chat-message.md +224 -0
  22. package/knowledge/checkbox-card.md +172 -0
  23. package/knowledge/checkbox.md +111 -0
  24. package/knowledge/citations.md +241 -0
  25. package/knowledge/combobox.md +265 -0
  26. package/knowledge/countdown-pill.md +153 -0
  27. package/knowledge/cta.md +118 -0
  28. package/knowledge/data-list.md +173 -0
  29. package/knowledge/data-table.md +220 -0
  30. package/knowledge/dialog.md +249 -0
  31. package/knowledge/drawer.md +236 -0
  32. package/knowledge/dropdown-menu.md +238 -0
  33. package/knowledge/dropzone.md +153 -0
  34. package/knowledge/email-chips-input.md +161 -0
  35. package/knowledge/empty-state.md +106 -0
  36. package/knowledge/entity-header.md +166 -0
  37. package/knowledge/feature-grid.md +120 -0
  38. package/knowledge/feedback-bar.md +127 -0
  39. package/knowledge/footer.md +116 -0
  40. package/knowledge/heading.md +189 -0
  41. package/knowledge/hero.md +361 -0
  42. package/knowledge/input-button.md +187 -0
  43. package/knowledge/input-group.md +185 -0
  44. package/knowledge/input.md +256 -0
  45. package/knowledge/inset.md +177 -0
  46. package/knowledge/kpi-card.md +206 -0
  47. package/knowledge/list-nav.md +181 -0
  48. package/knowledge/logo-cloud.md +102 -0
  49. package/knowledge/media-gallery.md +158 -0
  50. package/knowledge/message-actions.md +175 -0
  51. package/knowledge/nav-sidebar.md +225 -0
  52. package/knowledge/number-stepper.md +159 -0
  53. package/knowledge/page-header.md +109 -0
  54. package/knowledge/pagination.md +225 -0
  55. package/knowledge/popover.md +159 -0
  56. package/knowledge/pricing.md +132 -0
  57. package/knowledge/progress.md +156 -0
  58. package/knowledge/prompt-input.md +337 -0
  59. package/knowledge/prompt-suggestions.md +168 -0
  60. package/knowledge/quota-bar.md +160 -0
  61. package/knowledge/quote.md +116 -0
  62. package/knowledge/radio-card.md +172 -0
  63. package/knowledge/radio-group.md +135 -0
  64. package/knowledge/reasoning.md +142 -0
  65. package/knowledge/search.md +131 -0
  66. package/knowledge/section-nav.md +162 -0
  67. package/knowledge/segmented-control.md +181 -0
  68. package/knowledge/select-button.md +205 -0
  69. package/knowledge/select.md +210 -0
  70. package/knowledge/separator.md +75 -0
  71. package/knowledge/shimmer-text.md +140 -0
  72. package/knowledge/sidebar.md +227 -0
  73. package/knowledge/skeleton.md +142 -0
  74. package/knowledge/slider.md +133 -0
  75. package/knowledge/slideshow.md +159 -0
  76. package/knowledge/source.md +156 -0
  77. package/knowledge/spinner.md +120 -0
  78. package/knowledge/stat-strip.md +128 -0
  79. package/knowledge/stats.md +117 -0
  80. package/knowledge/step-indicator.md +157 -0
  81. package/knowledge/switch.md +88 -0
  82. package/knowledge/table.md +190 -0
  83. package/knowledge/tabs.md +169 -0
  84. package/knowledge/testimonials.md +115 -0
  85. package/knowledge/text-link.md +192 -0
  86. package/knowledge/text.md +194 -0
  87. package/knowledge/thinking-bar.md +115 -0
  88. package/knowledge/thinking-steps.md +140 -0
  89. package/knowledge/toaster.md +127 -0
  90. package/knowledge/toggle.md +113 -0
  91. package/knowledge/tool-call-card.md +241 -0
  92. package/knowledge/tooltip.md +120 -0
  93. package/knowledge/top-nav.md +138 -0
  94. package/knowledge.json +19854 -0
  95. package/package.json +94 -4
  96. package/src/components/accordion.stories.tsx +46 -0
  97. package/src/components/accordion.tsx +74 -0
  98. package/src/components/activity-rings.stories.tsx +73 -0
  99. package/src/components/activity-rings.tsx +121 -0
  100. package/src/components/alert.stories.tsx +43 -0
  101. package/src/components/alert.tsx +132 -0
  102. package/src/components/answer-actions.stories.tsx +54 -0
  103. package/src/components/answer-actions.test.tsx +37 -0
  104. package/src/components/answer-actions.tsx +313 -0
  105. package/src/components/aspect-ratio.stories.tsx +32 -0
  106. package/src/components/aspect-ratio.tsx +5 -0
  107. package/src/components/avatar.stories.tsx +40 -0
  108. package/src/components/avatar.tsx +154 -0
  109. package/src/components/badge.stories.tsx +27 -0
  110. package/src/components/badge.tsx +63 -0
  111. package/src/components/bar-graph.stories.tsx +28 -0
  112. package/src/components/bar-graph.tsx +204 -0
  113. package/src/components/bento-grid.stories.tsx +23 -0
  114. package/src/components/bento-grid.tsx +67 -0
  115. package/src/components/blockquote.stories.tsx +28 -0
  116. package/src/components/blockquote.tsx +88 -0
  117. package/src/components/box-list.stories.tsx +25 -0
  118. package/src/components/box-list.tsx +108 -0
  119. package/src/components/bullet-list.stories.tsx +22 -0
  120. package/src/components/bullet-list.tsx +101 -0
  121. package/src/components/button-group.stories.tsx +31 -0
  122. package/src/components/button-group.tsx +156 -0
  123. package/src/components/button.stories.tsx +66 -0
  124. package/src/components/button.test.ts +17 -0
  125. package/src/components/button.tsx +139 -0
  126. package/src/components/calendar.stories.tsx +25 -0
  127. package/src/components/calendar.tsx +367 -0
  128. package/src/components/card.stories.tsx +44 -0
  129. package/src/components/card.tsx +392 -0
  130. package/src/components/cards-nav.stories.tsx +35 -0
  131. package/src/components/cards-nav.tsx +115 -0
  132. package/src/components/chain-of-thought.stories.tsx +67 -0
  133. package/src/components/chain-of-thought.test.tsx +50 -0
  134. package/src/components/chain-of-thought.tsx +170 -0
  135. package/src/components/chat-container.stories.tsx +47 -0
  136. package/src/components/chat-container.test.tsx +49 -0
  137. package/src/components/chat-container.tsx +222 -0
  138. package/src/components/chat-message.stories.tsx +52 -0
  139. package/src/components/chat-message.test.tsx +36 -0
  140. package/src/components/chat-message.tsx +163 -0
  141. package/src/components/checkbox-card.stories.tsx +27 -0
  142. package/src/components/checkbox-card.tsx +200 -0
  143. package/src/components/checkbox.stories.tsx +26 -0
  144. package/src/components/checkbox.tsx +61 -0
  145. package/src/components/citations.stories.tsx +64 -0
  146. package/src/components/citations.test.tsx +61 -0
  147. package/src/components/citations.tsx +220 -0
  148. package/src/components/combobox.stories.tsx +184 -0
  149. package/src/components/combobox.test.tsx +82 -0
  150. package/src/components/combobox.tsx +407 -0
  151. package/src/components/countdown-pill.stories.tsx +24 -0
  152. package/src/components/countdown-pill.tsx +129 -0
  153. package/src/components/cta.stories.tsx +29 -0
  154. package/src/components/cta.tsx +52 -0
  155. package/src/components/data-list.stories.tsx +50 -0
  156. package/src/components/data-list.test.tsx +35 -0
  157. package/src/components/data-list.tsx +181 -0
  158. package/src/components/data-table.stories.tsx +29 -0
  159. package/src/components/data-table.tsx +200 -0
  160. package/src/components/dialog.stories.tsx +42 -0
  161. package/src/components/dialog.tsx +200 -0
  162. package/src/components/drawer.stories.tsx +48 -0
  163. package/src/components/drawer.tsx +385 -0
  164. package/src/components/dropdown-menu.stories.tsx +41 -0
  165. package/src/components/dropdown-menu.tsx +252 -0
  166. package/src/components/dropzone.stories.tsx +19 -0
  167. package/src/components/dropzone.tsx +250 -0
  168. package/src/components/email-chips-input.stories.tsx +135 -0
  169. package/src/components/email-chips-input.tsx +210 -0
  170. package/src/components/empty-state.stories.tsx +22 -0
  171. package/src/components/empty-state.tsx +35 -0
  172. package/src/components/entity-header.stories.tsx +43 -0
  173. package/src/components/entity-header.test.tsx +35 -0
  174. package/src/components/entity-header.tsx +92 -0
  175. package/src/components/feature-grid.stories.tsx +25 -0
  176. package/src/components/feature-grid.tsx +87 -0
  177. package/src/components/feedback-bar.stories.tsx +25 -0
  178. package/src/components/feedback-bar.test.tsx +24 -0
  179. package/src/components/feedback-bar.tsx +62 -0
  180. package/src/components/footer.stories.tsx +23 -0
  181. package/src/components/footer.tsx +78 -0
  182. package/src/components/heading.stories.tsx +45 -0
  183. package/src/components/heading.test.tsx +22 -0
  184. package/src/components/heading.tsx +77 -0
  185. package/src/components/hero.stories.tsx +38 -0
  186. package/src/components/hero.tsx +498 -0
  187. package/src/components/input-button.stories.tsx +19 -0
  188. package/src/components/input-button.tsx +112 -0
  189. package/src/components/input-group.stories.tsx +38 -0
  190. package/src/components/input-group.tsx +157 -0
  191. package/src/components/input.stories.tsx +31 -0
  192. package/src/components/input.tsx +284 -0
  193. package/src/components/inset.stories.tsx +29 -0
  194. package/src/components/inset.tsx +70 -0
  195. package/src/components/kpi-card.stories.tsx +29 -0
  196. package/src/components/kpi-card.tsx +284 -0
  197. package/src/components/list-nav.stories.tsx +22 -0
  198. package/src/components/list-nav.tsx +145 -0
  199. package/src/components/logo-cloud.stories.tsx +23 -0
  200. package/src/components/logo-cloud.tsx +42 -0
  201. package/src/components/media-gallery.stories.tsx +25 -0
  202. package/src/components/media-gallery.tsx +347 -0
  203. package/src/components/message-actions.stories.tsx +45 -0
  204. package/src/components/message-actions.test.tsx +32 -0
  205. package/src/components/message-actions.tsx +119 -0
  206. package/src/components/nav-sidebar.stories.tsx +58 -0
  207. package/src/components/nav-sidebar.test.tsx +54 -0
  208. package/src/components/nav-sidebar.tsx +462 -0
  209. package/src/components/number-stepper.stories.tsx +19 -0
  210. package/src/components/number-stepper.tsx +158 -0
  211. package/src/components/page-header.stories.tsx +25 -0
  212. package/src/components/page-header.tsx +64 -0
  213. package/src/components/pagination.stories.tsx +110 -0
  214. package/src/components/pagination.test.tsx +46 -0
  215. package/src/components/pagination.tsx +144 -0
  216. package/src/components/popover.stories.tsx +26 -0
  217. package/src/components/popover.tsx +62 -0
  218. package/src/components/pricing.stories.tsx +42 -0
  219. package/src/components/pricing.tsx +130 -0
  220. package/src/components/progress.stories.tsx +22 -0
  221. package/src/components/progress.tsx +189 -0
  222. package/src/components/prompt-input.stories.tsx +62 -0
  223. package/src/components/prompt-input.test.tsx +106 -0
  224. package/src/components/prompt-input.tsx +343 -0
  225. package/src/components/prompt-suggestions.stories.tsx +53 -0
  226. package/src/components/prompt-suggestions.test.tsx +37 -0
  227. package/src/components/prompt-suggestions.tsx +122 -0
  228. package/src/components/quota-bar.stories.tsx +19 -0
  229. package/src/components/quota-bar.tsx +191 -0
  230. package/src/components/quote.stories.tsx +18 -0
  231. package/src/components/quote.tsx +43 -0
  232. package/src/components/radio-card.stories.tsx +27 -0
  233. package/src/components/radio-card.tsx +225 -0
  234. package/src/components/radio-group.stories.tsx +32 -0
  235. package/src/components/radio-group.tsx +68 -0
  236. package/src/components/reasoning.stories.tsx +50 -0
  237. package/src/components/reasoning.test.tsx +48 -0
  238. package/src/components/reasoning.tsx +143 -0
  239. package/src/components/search.stories.tsx +18 -0
  240. package/src/components/search.tsx +133 -0
  241. package/src/components/section-nav.stories.tsx +59 -0
  242. package/src/components/section-nav.test.tsx +36 -0
  243. package/src/components/section-nav.tsx +146 -0
  244. package/src/components/segmented-control.stories.tsx +22 -0
  245. package/src/components/segmented-control.tsx +301 -0
  246. package/src/components/select-button.stories.tsx +27 -0
  247. package/src/components/select-button.tsx +174 -0
  248. package/src/components/select.stories.tsx +27 -0
  249. package/src/components/select.tsx +198 -0
  250. package/src/components/separator.stories.tsx +22 -0
  251. package/src/components/separator.tsx +23 -0
  252. package/src/components/shimmer-text.stories.tsx +23 -0
  253. package/src/components/shimmer-text.test.tsx +35 -0
  254. package/src/components/shimmer-text.tsx +43 -0
  255. package/src/components/sidebar.stories.tsx +25 -0
  256. package/src/components/sidebar.tsx +234 -0
  257. package/src/components/skeleton.stories.tsx +22 -0
  258. package/src/components/skeleton.tsx +96 -0
  259. package/src/components/slider.stories.tsx +20 -0
  260. package/src/components/slider.tsx +176 -0
  261. package/src/components/slideshow.stories.tsx +30 -0
  262. package/src/components/slideshow.tsx +291 -0
  263. package/src/components/source.stories.tsx +32 -0
  264. package/src/components/source.test.tsx +40 -0
  265. package/src/components/source.tsx +161 -0
  266. package/src/components/spinner.stories.tsx +19 -0
  267. package/src/components/spinner.tsx +76 -0
  268. package/src/components/stat-strip.stories.tsx +36 -0
  269. package/src/components/stat-strip.test.tsx +25 -0
  270. package/src/components/stat-strip.tsx +85 -0
  271. package/src/components/stats.stories.tsx +24 -0
  272. package/src/components/stats.tsx +83 -0
  273. package/src/components/step-indicator.stories.tsx +18 -0
  274. package/src/components/step-indicator.test.tsx +18 -0
  275. package/src/components/step-indicator.tsx +286 -0
  276. package/src/components/switch.stories.tsx +26 -0
  277. package/src/components/switch.tsx +32 -0
  278. package/src/components/table.stories.tsx +38 -0
  279. package/src/components/table.tsx +220 -0
  280. package/src/components/tabs.stories.tsx +25 -0
  281. package/src/components/tabs.tsx +276 -0
  282. package/src/components/testimonials.stories.tsx +22 -0
  283. package/src/components/testimonials.tsx +109 -0
  284. package/src/components/text-link.stories.tsx +47 -0
  285. package/src/components/text-link.test.tsx +18 -0
  286. package/src/components/text-link.tsx +88 -0
  287. package/src/components/text.stories.tsx +49 -0
  288. package/src/components/text.tsx +105 -0
  289. package/src/components/thinking-bar.stories.tsx +19 -0
  290. package/src/components/thinking-bar.test.tsx +27 -0
  291. package/src/components/thinking-bar.tsx +64 -0
  292. package/src/components/thinking-steps.stories.tsx +44 -0
  293. package/src/components/thinking-steps.test.tsx +42 -0
  294. package/src/components/thinking-steps.tsx +92 -0
  295. package/src/components/toaster.stories.tsx +80 -0
  296. package/src/components/toaster.tsx +36 -0
  297. package/src/components/toggle.stories.tsx +21 -0
  298. package/src/components/toggle.tsx +103 -0
  299. package/src/components/tool-call-card.stories.tsx +97 -0
  300. package/src/components/tool-call-card.test.tsx +88 -0
  301. package/src/components/tool-call-card.tsx +231 -0
  302. package/src/components/tooltip.stories.tsx +25 -0
  303. package/src/components/tooltip.tsx +43 -0
  304. package/src/components/top-nav.stories.tsx +20 -0
  305. package/src/components/top-nav.tsx +76 -0
  306. package/src/docs/Colors.stories.tsx +192 -0
  307. package/src/docs/GettingStarted.mdx +72 -0
  308. package/src/docs/Mcp.mdx +52 -0
  309. package/src/docs/Typography.stories.tsx +114 -0
  310. package/src/hooks/use-in-view.ts +32 -0
  311. package/src/hooks/use-reduced-motion.ts +26 -0
  312. package/src/index.ts +343 -0
  313. package/src/lib/cn.test.ts +67 -0
  314. package/src/lib/cn.ts +81 -0
  315. package/src/lib/motion.ts +109 -0
  316. package/src/lib/prefix-utilities.test.ts +56 -0
  317. package/src/lib/prefix-utilities.ts +50 -0
  318. package/src/styles/base.css +489 -0
  319. package/src/styles/motion.css +69 -0
  320. package/src/styles/overrides.css +44 -0
  321. package/src/styles/typography.css +173 -0
  322. package/src/theme/theme.css +391 -0
  323. package/src/theme/tokens.ts +29 -0
  324. package/src/theme/types.ts +26 -0
  325. package/README.md +0 -3
@@ -0,0 +1,97 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { CalendarCheck, LineChart, Search } from "lucide-react"
4
+ import { Button } from "./button"
5
+ import { Text } from "./text"
6
+ import { ToolCallCard, toolPartToCallCard } from "./tool-call-card"
7
+
8
+ const INPUT = [
9
+ { label: "Community", value: "Yas Island" },
10
+ { label: "Type", value: "Townhouse" },
11
+ { label: "Max price", value: "AED 5,000,000" },
12
+ ]
13
+
14
+ const OUTPUT = [
15
+ { label: "Matches", value: "12" },
16
+ { label: "Lowest price", value: "AED 4,200,000" },
17
+ ]
18
+
19
+ const meta: Meta<typeof ToolCallCard> = {
20
+ title: "Components/ToolCallCard",
21
+ component: ToolCallCard,
22
+ tags: ["autodocs"],
23
+ decorators: [(Story) => <div style={{ maxWidth: 640 }}><Story /></div>],
24
+ argTypes: {
25
+ status: { control: "select", options: ["awaiting-approval", "running", "success", "error", "declined", "cancelled"] },
26
+ icon: { control: false },
27
+ },
28
+ args: {
29
+ icon: Search,
30
+ title: "Searched listings",
31
+ toolName: "search_listings",
32
+ subtitle: "12 townhouses under AED 5M",
33
+ status: "success",
34
+ durationMs: 1400,
35
+ input: INPUT,
36
+ output: OUTPUT,
37
+ defaultOpen: true,
38
+ },
39
+ }
40
+
41
+ export default meta
42
+ type Story = StoryObj<typeof ToolCallCard>
43
+
44
+ export const Success: Story = {}
45
+
46
+ export const Running: Story = {
47
+ args: { title: "Searching listings", subtitle: undefined, status: "running", onCancel: () => undefined, defaultOpen: false },
48
+ }
49
+
50
+ export const Failed: Story = {
51
+ args: {
52
+ icon: LineChart,
53
+ title: "Check registered sales",
54
+ toolName: "get_dld_transactions",
55
+ subtitle: "Couldn't finish",
56
+ status: "error",
57
+ error: "The sales registry didn't respond. Nothing was changed.",
58
+ onRetry: () => undefined,
59
+ },
60
+ }
61
+
62
+ export const AwaitingApproval: Story = {
63
+ args: {
64
+ icon: CalendarCheck,
65
+ title: "Book a viewing",
66
+ toolName: "book_viewing",
67
+ subtitle: "Waiting for your approval",
68
+ status: "awaiting-approval",
69
+ children: (
70
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
71
+ <Text as="p" size="2">The assistant wants to book a viewing on Saturday at 11:00. Nothing happens until you approve.</Text>
72
+ <div style={{ display: "flex", gap: "0.5rem" }}>
73
+ <Button size="sm">Approve</Button>
74
+ <Button variant="ghost" size="sm">Decline</Button>
75
+ </div>
76
+ </div>
77
+ ),
78
+ },
79
+ }
80
+
81
+ export const Compact: Story = { args: { compact: true, defaultOpen: false } }
82
+
83
+ export const FromAiSdkToolPart: Story = {
84
+ render: () => (
85
+ <ToolCallCard
86
+ icon={Search}
87
+ defaultOpen
88
+ {...toolPartToCallCard({
89
+ type: "tool-search_listings",
90
+ state: "output-available",
91
+ input: { community: "Yas Island", maxPrice: 5000000 },
92
+ output: { count: 12, cheapest: { price: 4200000, ref: "YA-22B" } },
93
+ toolCallId: "call_8f2a91",
94
+ })}
95
+ />
96
+ ),
97
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as React from "react"
3
+ import { renderToStaticMarkup } from "react-dom/server"
4
+ import {
5
+ TOOL_CALL_STATUS, ToolCallCard, toolCallNeedsAttention, toolPartToCallCard, type ToolCallStatus, type ToolPart,
6
+ } from "./tool-call-card"
7
+
8
+ const Icon = () => <svg />
9
+ const INPUT = [{ label: "Community", value: "Yas Island" }]
10
+ const OUTPUT = [{ label: "Matches", value: "12" }]
11
+
12
+ const render = (status: ToolCallStatus, extra: Partial<React.ComponentProps<typeof ToolCallCard>> = {}) =>
13
+ renderToStaticMarkup(<ToolCallCard icon={Icon} title="Search listings" status={status} input={INPUT} output={OUTPUT} {...extra} />)
14
+
15
+ describe("ToolCallCard", () => {
16
+ it.each(Object.entries(TOOL_CALL_STATUS))("labels %s as %o", (status, { label }) => {
17
+ expect(render(status as ToolCallStatus)).toContain(`>${label}</`)
18
+ })
19
+
20
+ it("opens for approval and errors, stays closed otherwise", () => {
21
+ expect(toolCallNeedsAttention("awaiting-approval")).toBe(true)
22
+ expect(toolCallNeedsAttention("error")).toBe(true)
23
+ expect(toolCallNeedsAttention("success")).toBe(false)
24
+ expect(render("error")).toContain('aria-expanded="true"')
25
+ expect(render("success")).toContain('aria-expanded="false"')
26
+ })
27
+
28
+ it("shows the approval body in place of the fields while awaiting approval", () => {
29
+ const html = render("awaiting-approval", { children: <p>Approve this booking?</p> })
30
+ expect(html).toContain("Approve this booking?")
31
+ expect(html).not.toContain("Yas Island")
32
+ })
33
+
34
+ it("shows results only once the call succeeded", () => {
35
+ expect(render("success", { defaultOpen: true })).toContain(">Result<")
36
+ expect(render("running", { defaultOpen: true })).not.toContain(">Result<")
37
+ })
38
+
39
+ it("marks the card busy while running", () => {
40
+ expect(render("running")).toContain('aria-busy="true"')
41
+ })
42
+ })
43
+
44
+ describe("toolPartToCallCard", () => {
45
+ const part: ToolPart = {
46
+ type: "tool-search_listings",
47
+ state: "output-available",
48
+ input: { community: "Yas Island", maxPrice: 5_000_000 },
49
+ output: { count: 12, cheapest: { price: 4_200_000, ref: "YA-22B" } },
50
+ toolCallId: "call_8f2",
51
+ }
52
+
53
+ it("names the tool and turns its id into a plain title", () => {
54
+ const props = toolPartToCallCard(part)
55
+ expect(props.toolName).toBe("search_listings")
56
+ expect(props.title).toBe("Search listings")
57
+ expect(props.callId).toBe("call_8f2")
58
+ })
59
+
60
+ it.each([
61
+ ["input-streaming", "running"],
62
+ ["input-available", "running"],
63
+ ["approval-requested", "awaiting-approval"],
64
+ ["output-available", "success"],
65
+ ["output-error", "error"],
66
+ ["output-denied", "declined"],
67
+ ] as const)("maps %s to %s", (state, status) => {
68
+ expect(toolPartToCallCard({ ...part, state }).status).toBe(status)
69
+ })
70
+
71
+ it("lists record entries as fields and pretty-prints nested values", () => {
72
+ const props = toolPartToCallCard(part)
73
+ expect(props.input?.map((field) => field.label)).toEqual(["community", "maxPrice"])
74
+ const html = renderToStaticMarkup(<>{props.output?.find((field) => field.label === "cheapest")?.value}</>)
75
+ expect(html).toContain("<pre")
76
+ expect(html).toContain("&quot;price&quot;: 4200000")
77
+ })
78
+
79
+ it("wraps a non-record output in a single result field and passes errors through", () => {
80
+ expect(toolPartToCallCard({ ...part, output: [1, 2] }).output?.map((field) => field.label)).toEqual(["result"])
81
+ expect(toolPartToCallCard({ ...part, state: "output-error", errorText: "Timed out" }).error).toBe("Timed out")
82
+ })
83
+
84
+ it("shows the call id at the foot of open details", () => {
85
+ const html = renderToStaticMarkup(<ToolCallCard icon={Icon} defaultOpen {...toolPartToCallCard(part)} />)
86
+ expect(html).toContain("Call ID: call_8f2")
87
+ })
88
+ })
@@ -0,0 +1,231 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronDown } from "lucide-react"
5
+ import { cn } from "../lib/cn"
6
+ import { Alert, AlertDescription } from "./alert"
7
+ import { Badge, type BadgeProps } from "./badge"
8
+ import { Button } from "./button"
9
+ import { Spinner } from "./spinner"
10
+ import { Text } from "./text"
11
+
12
+ export type ToolCallStatus = "awaiting-approval" | "running" | "success" | "error" | "declined" | "cancelled"
13
+
14
+ export interface ToolCallField {
15
+ label: string
16
+ value: React.ReactNode
17
+ }
18
+
19
+ export const TOOL_CALL_STATUS: Record<ToolCallStatus, { label: string; variant: NonNullable<BadgeProps["variant"]> }> = {
20
+ "awaiting-approval": { label: "Needs approval", variant: "warning" },
21
+ running: { label: "Running", variant: "muted" },
22
+ success: { label: "Done", variant: "success" },
23
+ error: { label: "Failed", variant: "destructive" },
24
+ declined: { label: "Declined", variant: "muted" },
25
+ cancelled: { label: "Cancelled", variant: "muted" },
26
+ }
27
+
28
+ /** Approval and errors need the person, so the card opens for them. */
29
+ export const toolCallNeedsAttention = (status: ToolCallStatus) => status === "awaiting-approval" || status === "error"
30
+
31
+ const seconds = (ms: number) => `${(ms / 1000).toFixed(1)}s`
32
+
33
+ function FieldGroup({ label, fields }: { label: string; fields: ToolCallField[] }) {
34
+ return (
35
+ <div className="ax:flex ax:flex-col ax:gap-xs">
36
+ <Text as="p" size="2" weight="medium" color="muted">{label}</Text>
37
+ <dl className="ax:flex ax:flex-col ax:gap-1 ax:rounded ax:border ax:border-border ax:bg-background ax:p-xs ax:font-mono ax:text-sm">
38
+ {fields.map((field) => (
39
+ <div key={field.label} className="ax:flex ax:min-w-0 ax:flex-wrap ax:gap-x-xs">
40
+ <dt className="ax:text-muted-foreground">{field.label}:</dt>
41
+ <dd className="ax:min-w-0 ax:wrap-break-word ax:text-foreground">{field.value}</dd>
42
+ </div>
43
+ ))}
44
+ </dl>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export interface ToolCallCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
50
+ /** The tool's icon; replaced by a spinner while running. */
51
+ icon: React.ElementType
52
+ /** What the call does in plain words, in the tense of its status ("Searching listings", "Searched listings"). */
53
+ title: string
54
+ /** The technical tool name, shown in mono from `sm` up. */
55
+ toolName?: string
56
+ /** One line under the title: the result summary, or why it stopped. */
57
+ subtitle?: string
58
+ status: ToolCallStatus
59
+ /** Shown once the call succeeds. */
60
+ durationMs?: number
61
+ input?: ToolCallField[]
62
+ /** Shown once the call succeeds. */
63
+ output?: ToolCallField[]
64
+ /** Shown when the call fails, with Retry when `onRetry` is set. */
65
+ error?: string
66
+ onRetry?: () => void
67
+ /** Adds Cancel while running. */
68
+ onCancel?: () => void
69
+ /** Tighter header for a run of several calls. */
70
+ compact?: boolean
71
+ /** Whether the body starts open. Defaults to open while awaiting approval or after an error. */
72
+ defaultOpen?: boolean
73
+ /** The approval body shown while `awaiting-approval`, in place of the fields. */
74
+ children?: React.ReactNode
75
+ /** The call's id, shown small at the foot of the details for support and debugging. */
76
+ callId?: string
77
+ }
78
+
79
+ /**
80
+ * ToolCallCard - one tool call by an assistant: what it called in plain
81
+ * words, live status, and the inputs and results as labelled fields on
82
+ * demand. Calls with side effects stop at an approval step (`children`).
83
+ *
84
+ * @example
85
+ * <ToolCallCard icon={Search} title="Searched listings" toolName="search_listings" status="success"
86
+ * subtitle="12 matches" durationMs={1400} input={inputFields} output={resultFields} />
87
+ */
88
+ export const ToolCallCard = React.forwardRef<HTMLDivElement, ToolCallCardProps>(
89
+ (
90
+ {
91
+ icon: Icon, title, toolName, subtitle, status, durationMs, input = [], output = [], error, onRetry, onCancel,
92
+ compact = false, defaultOpen, children, callId, className, ...props
93
+ },
94
+ ref,
95
+ ) => {
96
+ const id = React.useId()
97
+ const needsAttention = toolCallNeedsAttention(status)
98
+ const [open, setOpen] = React.useState(defaultOpen ?? needsAttention)
99
+ React.useEffect(() => {
100
+ if (needsAttention) setOpen(true)
101
+ }, [needsAttention])
102
+
103
+ return (
104
+ <div
105
+ ref={ref}
106
+ data-slot="tool-call-card"
107
+ className={cn(
108
+ "ax:overflow-hidden ax:rounded ax:border ax:border-border ax:bg-card",
109
+ status === "awaiting-approval" && "ax:border-warning-weaker",
110
+ className,
111
+ )}
112
+ aria-busy={status === "running"}
113
+ {...props}
114
+ >
115
+ <div className={cn("ax:flex ax:items-center ax:gap-sm ax:px-sm", compact ? "ax:py-1.5" : "ax:py-xs")}>
116
+ <span className="ax:flex ax:size-4 ax:shrink-0 ax:items-center ax:justify-center ax:text-muted-foreground">
117
+ {status === "running" ? <Spinner size="sm" decorative /> : <Icon className="ax:size-4" aria-hidden />}
118
+ </span>
119
+ <div className="ax:flex ax:min-w-0 ax:flex-1 ax:flex-col">
120
+ <span className="ax:flex ax:min-w-0 ax:items-baseline ax:gap-xs">
121
+ <Text size="2" weight="medium" truncate>{title}</Text>
122
+ {toolName && <Text size="1" color="muted" className="ax:hidden ax:font-mono ax:sm:inline">{toolName}</Text>}
123
+ </span>
124
+ {subtitle && <Text size="1" color={status === "error" ? "destructive" : "muted"} truncate>{subtitle}</Text>}
125
+ </div>
126
+ {status === "success" && durationMs !== undefined && (
127
+ <Text size="1" color="muted" className="ax:hidden ax:tabular-nums ax:sm:inline">{seconds(durationMs)}</Text>
128
+ )}
129
+ <Badge variant={TOOL_CALL_STATUS[status].variant} size="sm" className="ax:rounded-full" aria-live="polite">
130
+ {TOOL_CALL_STATUS[status].label}
131
+ </Badge>
132
+ {status === "running" && onCancel && <Button variant="ghost" size="sm" className="ax:rounded-full" onClick={onCancel}>Cancel</Button>}
133
+ <Button
134
+ variant="ghost"
135
+ size="icon-sm"
136
+ className="ax:rounded-full ax:text-muted-foreground ax:hover:text-foreground"
137
+ aria-expanded={open}
138
+ aria-controls={`${id}-body`}
139
+ aria-label={open ? `Hide details for ${title}` : `Show details for ${title}`}
140
+ onClick={() => setOpen((value) => !value)}
141
+ >
142
+ <ChevronDown className={cn("ax:transition-transform ax:motion-reduce:transition-none", open && "ax:rotate-180")} />
143
+ </Button>
144
+ </div>
145
+
146
+ {open && (
147
+ <div id={`${id}-body`} className="ax:flex ax:flex-col ax:gap-sm ax:border-t ax:border-border ax:p-sm">
148
+ {status === "awaiting-approval" && children ? children : (
149
+ <>
150
+ {status === "error" && error && (
151
+ <Alert variant="destructive" size="md">
152
+ <AlertDescription>{error}</AlertDescription>
153
+ {onRetry && (
154
+ <Button variant="ghost" size="sm" className="ax:rounded-full ax:self-start" onClick={onRetry}>Retry</Button>
155
+ )}
156
+ </Alert>
157
+ )}
158
+ {input.length > 0 && <FieldGroup label="Input" fields={input} />}
159
+ {status === "success" && output.length > 0 && <FieldGroup label="Result" fields={output} />}
160
+ </>
161
+ )}
162
+ {callId && <Text as="p" size="1" color="muted" className="ax:font-mono ax:break-all">Call ID: {callId}</Text>}
163
+ </div>
164
+ )}
165
+ </div>
166
+ )
167
+ },
168
+ )
169
+ ToolCallCard.displayName = "ToolCallCard"
170
+
171
+ /** A tool call part as the Vercel AI SDK streams it: `type` is "tool-<name>". */
172
+ export interface ToolPart {
173
+ type: string
174
+ state: "input-streaming" | "input-available" | "approval-requested" | "output-available" | "output-error" | "output-denied"
175
+ input?: unknown
176
+ output?: unknown
177
+ errorText?: string
178
+ toolCallId?: string
179
+ }
180
+
181
+ const TOOL_PART_STATUS: Record<ToolPart["state"], ToolCallStatus> = {
182
+ "input-streaming": "running",
183
+ "input-available": "running",
184
+ "approval-requested": "awaiting-approval",
185
+ "output-available": "success",
186
+ "output-error": "error",
187
+ "output-denied": "declined",
188
+ }
189
+
190
+ /** Plain values as text; objects and arrays as scrollable pretty-printed JSON. */
191
+ export function formatToolValue(value: unknown): React.ReactNode {
192
+ if (value === null || value === undefined) return String(value)
193
+ if (typeof value !== "object") return String(value)
194
+ return <pre className="ax:max-h-60 ax:overflow-auto ax:whitespace-pre-wrap">{JSON.stringify(value, null, 2)}</pre>
195
+ }
196
+
197
+ function toFields(value: unknown, fallbackLabel: string): ToolCallField[] {
198
+ if (value === undefined) return []
199
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
200
+ return Object.entries(value).map(([label, entry]) => ({ label, value: formatToolValue(entry) }))
201
+ }
202
+ return [{ label: fallbackLabel, value: formatToolValue(value) }]
203
+ }
204
+
205
+ /** "tool-search_listings" to "Search listings". */
206
+ const humanize = (name: string) => {
207
+ const words = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase()
208
+ return words.charAt(0).toUpperCase() + words.slice(1)
209
+ }
210
+
211
+ export type ToolPartCardProps = Pick<ToolCallCardProps, "title" | "toolName" | "status" | "input" | "output" | "error" | "callId">
212
+
213
+ /**
214
+ * Maps an AI SDK tool part to ToolCallCard props. Pass the icon yourself, and
215
+ * override `title` with plain words in the right tense when you have them.
216
+ *
217
+ * @example
218
+ * <ToolCallCard icon={Search} {...toolPartToCallCard(part)} />
219
+ */
220
+ export function toolPartToCallCard(part: ToolPart): ToolPartCardProps {
221
+ const toolName = part.type.replace(/^tool-/, "")
222
+ return {
223
+ title: humanize(toolName),
224
+ toolName,
225
+ status: TOOL_PART_STATUS[part.state],
226
+ input: toFields(part.input, "input"),
227
+ output: toFields(part.output, "result"),
228
+ error: part.errorText,
229
+ callId: part.toolCallId,
230
+ }
231
+ }
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Button } from "./button"
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"
5
+
6
+ const meta: Meta<typeof Tooltip> = {
7
+ title: "Components/Tooltip",
8
+ component: Tooltip,
9
+ tags: ["autodocs"],
10
+ decorators: [(Story) => <TooltipProvider><Story /></TooltipProvider>],
11
+ }
12
+
13
+ export default meta
14
+ type Story = StoryObj<typeof Tooltip>
15
+
16
+ export const Default: Story = {
17
+ render: () => (
18
+ <Tooltip>
19
+ <TooltipTrigger asChild>
20
+ <Button variant="outline">Hover me</Button>
21
+ </TooltipTrigger>
22
+ <TooltipContent>Helpful tooltip text</TooltipContent>
23
+ </Tooltip>
24
+ ),
25
+ }
@@ -0,0 +1,43 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+ import { cn } from "../lib/cn"
6
+
7
+ export const TooltipProvider = TooltipPrimitive.Provider
8
+ export const Tooltip = TooltipPrimitive.Root
9
+ export const TooltipTrigger = TooltipPrimitive.Trigger
10
+
11
+ export interface TooltipContentProps
12
+ extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> {}
13
+
14
+ export const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ TooltipContentProps
17
+ >(({ className, sideOffset = 6, ...props }, ref) => (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ // Layered above Dialog/Drawer (z-[1100]) so tooltips on overlay UI stay visible.
24
+ "ax:z-1200 ax:overflow-hidden ax:rounded-none",
25
+ "ax:bg-foreground ax:px-sm ax:py-xs",
26
+ "ax:t-body-sm ax:text-background",
27
+ "ax:shadow-md",
28
+ "ax:animate-in ax:fade-in-0 ax:zoom-in-[0.96]",
29
+ "ax:data-[state=closed]:animate-out ax:data-[state=closed]:fade-out-0 ax:data-[state=closed]:zoom-out-[0.96]",
30
+ "ax:data-[side=bottom]:slide-in-from-top-1",
31
+ "ax:data-[side=left]:slide-in-from-right-1",
32
+ "ax:data-[side=right]:slide-in-from-left-1",
33
+ "ax:data-[side=top]:slide-in-from-bottom-1",
34
+ // Tooltips need to feel instant, intentionally faster than --motion-fast (160ms).
35
+ "ax:[animation-duration:100ms] ax:[animation-timing-function:var(--motion-ease-out)]",
36
+ "ax:motion-reduce:animate-none",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ </TooltipPrimitive.Portal>
42
+ ))
43
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
@@ -0,0 +1,20 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import { Breadcrumbs, TopNav } from "./top-nav"
4
+
5
+ const meta: Meta<typeof TopNav> = {
6
+ title: "Components/TopNav",
7
+ component: TopNav,
8
+ tags: ["autodocs"],
9
+ }
10
+
11
+ export default meta
12
+ type Story = StoryObj<typeof TopNav>
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <TopNav>
17
+ <Breadcrumbs items={[{ label: "Home", href: "#" }, { label: "Projects", href: "#" }, { label: "Overview" }]} />
18
+ </TopNav>
19
+ ),
20
+ }
@@ -0,0 +1,76 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../lib/cn"
5
+
6
+ export interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
7
+ /** Left-aligned slot (typically breadcrumbs or a page title). */
8
+ start?: React.ReactNode
9
+ /** Right-aligned slot (search, notifications, user menu). */
10
+ end?: React.ReactNode
11
+ }
12
+
13
+ /**
14
+ * TopNav - sticky top bar for the web app shell.
15
+ * Supports both webapp (with sidebar context) and standalone usage.
16
+ */
17
+ export const TopNav = React.forwardRef<HTMLElement, TopNavProps>(
18
+ ({ className, start, end, children, ...props }, ref) => (
19
+ <header
20
+ ref={ref}
21
+ className={cn(
22
+ "ax:sticky ax:top-0 ax:z-100 ax:flex ax:h-5xl ax:w-full ax:shrink-0 ax:items-center ax:gap-md",
23
+ "ax:border-b ax:border-border ax:bg-background/80 ax:backdrop-blur-md",
24
+ "ax:px-md ax:md:px-xl",
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ <div className="ax:flex ax:items-center ax:gap-sm ax:flex-1 ax:min-w-0">{start}</div>
30
+ <div className="ax:flex-1 ax:flex ax:items-center ax:justify-center ax:min-w-0">{children}</div>
31
+ <div className="ax:flex ax:items-center ax:gap-xs ax:flex-1 ax:min-w-0 ax:justify-end">{end}</div>
32
+ </header>
33
+ )
34
+ )
35
+ TopNav.displayName = "TopNav"
36
+
37
+ export interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
38
+ items: { label: string; href?: string }[]
39
+ /** Custom separator. Default: '/'. */
40
+ separator?: React.ReactNode
41
+ }
42
+
43
+ export const Breadcrumbs = React.forwardRef<HTMLElement, BreadcrumbsProps>(
44
+ ({ className, items, separator = "/", ...props }, ref) => (
45
+ <nav ref={ref} aria-label="Breadcrumb" className={cn("ax:flex ax:items-center", className)} {...props}>
46
+ <ol className="ax:flex ax:items-center ax:gap-xs">
47
+ {items.map((item, i) => {
48
+ const isLast = i === items.length - 1
49
+ return (
50
+ <li key={`${item.label}-${i}`} className="ax:flex ax:items-center ax:gap-xs">
51
+ {i > 0 && (
52
+ <span aria-hidden="true" className="ax:t-caption ax:text-muted-foreground/60">
53
+ {separator}
54
+ </span>
55
+ )}
56
+ {item.href ? (
57
+ <a
58
+ href={item.href}
59
+ aria-current={isLast ? "page" : undefined}
60
+ className="ax:t-body-sm ax:text-muted-foreground ax:hover:text-foreground ax:transition-colors ax:focus-visible:outline-none ax:focus-visible:underline"
61
+ >
62
+ {item.label}
63
+ </a>
64
+ ) : (
65
+ <span aria-current={isLast ? "page" : undefined} className="ax:t-label-sm ax:text-foreground">
66
+ {item.label}
67
+ </span>
68
+ )}
69
+ </li>
70
+ )
71
+ })}
72
+ </ol>
73
+ </nav>
74
+ )
75
+ )
76
+ Breadcrumbs.displayName = "Breadcrumbs"