@gntik-ai/blocks 0.0.0-stage → 0.5.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 (385) hide show
  1. package/dist/index.d.ts +3494 -0
  2. package/dist/index.js +14911 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +62 -3
  5. package/src/ai/AssistantCardGrid/AssistantCardGrid.test.tsx +32 -0
  6. package/src/ai/AssistantCardGrid/AssistantCardGrid.tsx +98 -0
  7. package/src/ai/AssistantCardGrid/block.meta.ts +10 -0
  8. package/src/ai/AssistantCardGrid/fixtures.ts +72 -0
  9. package/src/ai/EvaluationScorecard/EvaluationScorecard.test.tsx +39 -0
  10. package/src/ai/EvaluationScorecard/EvaluationScorecard.tsx +233 -0
  11. package/src/ai/EvaluationScorecard/block.meta.ts +10 -0
  12. package/src/ai/EvaluationScorecard/fixtures.ts +79 -0
  13. package/src/ai/ModelKeysList/ModelKeysList.test.tsx +44 -0
  14. package/src/ai/ModelKeysList/ModelKeysList.tsx +203 -0
  15. package/src/ai/ModelKeysList/block.meta.ts +10 -0
  16. package/src/ai/ModelKeysList/fixtures.ts +44 -0
  17. package/src/ai/PromptEditor/PromptEditor.test.tsx +97 -0
  18. package/src/ai/PromptEditor/PromptEditor.tsx +202 -0
  19. package/src/ai/PromptEditor/block.meta.ts +10 -0
  20. package/src/ai/PromptEditor/examples/controlled-variables.tsx +23 -0
  21. package/src/ai/PromptEditor/fixtures.ts +12 -0
  22. package/src/ai/TokenCostCard/TokenCostCard.test.tsx +36 -0
  23. package/src/ai/TokenCostCard/TokenCostCard.tsx +130 -0
  24. package/src/ai/TokenCostCard/block.meta.ts +9 -0
  25. package/src/ai/TokenCostCard/fixtures.ts +18 -0
  26. package/src/ai/TraceWaterfall/TraceWaterfall.test.tsx +119 -0
  27. package/src/ai/TraceWaterfall/TraceWaterfall.tsx +317 -0
  28. package/src/ai/TraceWaterfall/block.meta.ts +10 -0
  29. package/src/ai/TraceWaterfall/examples/no-details.tsx +6 -0
  30. package/src/ai/TraceWaterfall/examples/running.tsx +16 -0
  31. package/src/ai/TraceWaterfall/fixtures.ts +133 -0
  32. package/src/ai/TraceWaterfall/span-details.tsx +51 -0
  33. package/src/ai/index.ts +13 -0
  34. package/src/auth/ForgotPasswordForm/ForgotPasswordForm.test.tsx +209 -0
  35. package/src/auth/ForgotPasswordForm/ForgotPasswordForm.tsx +211 -0
  36. package/src/auth/ForgotPasswordForm/block.meta.ts +10 -0
  37. package/src/auth/ForgotPasswordForm/examples/UsernameOrEmail.tsx +24 -0
  38. package/src/auth/MfaChallenge/MfaChallenge.test.tsx +35 -0
  39. package/src/auth/MfaChallenge/MfaChallenge.tsx +170 -0
  40. package/src/auth/MfaChallenge/block.meta.ts +9 -0
  41. package/src/auth/ResetPasswordForm/ResetPasswordForm.test.tsx +27 -0
  42. package/src/auth/ResetPasswordForm/ResetPasswordForm.tsx +100 -0
  43. package/src/auth/ResetPasswordForm/block.meta.ts +10 -0
  44. package/src/auth/SignInForm/SignInForm.test.tsx +222 -0
  45. package/src/auth/SignInForm/SignInForm.tsx +226 -0
  46. package/src/auth/SignInForm/block.meta.ts +22 -0
  47. package/src/auth/SignInForm/examples/Username.tsx +26 -0
  48. package/src/auth/SignUpForm/SignUpForm.test.tsx +33 -0
  49. package/src/auth/SignUpForm/SignUpForm.tsx +154 -0
  50. package/src/auth/SignUpForm/block.meta.ts +9 -0
  51. package/src/auth/SsoButtons/SsoButtons.test.tsx +23 -0
  52. package/src/auth/SsoButtons/SsoButtons.tsx +52 -0
  53. package/src/auth/SsoButtons/block.meta.ts +9 -0
  54. package/src/auth/SsoButtons/fixtures.ts +15 -0
  55. package/src/auth/index.ts +15 -0
  56. package/src/auth/shared.tsx +113 -0
  57. package/src/billing/CostBreakdown/CostBreakdown.test.tsx +24 -0
  58. package/src/billing/CostBreakdown/CostBreakdown.tsx +95 -0
  59. package/src/billing/CostBreakdown/block.meta.ts +10 -0
  60. package/src/billing/CostBreakdown/fixtures.ts +9 -0
  61. package/src/billing/InvoiceTable/InvoiceTable.test.tsx +29 -0
  62. package/src/billing/InvoiceTable/InvoiceTable.tsx +130 -0
  63. package/src/billing/InvoiceTable/block.meta.ts +10 -0
  64. package/src/billing/InvoiceTable/fixtures.ts +38 -0
  65. package/src/billing/PaymentMethodCard/PaymentMethodCard.test.tsx +28 -0
  66. package/src/billing/PaymentMethodCard/PaymentMethodCard.tsx +103 -0
  67. package/src/billing/PaymentMethodCard/block.meta.ts +10 -0
  68. package/src/billing/PaymentMethodCard/fixtures.ts +12 -0
  69. package/src/billing/PlanCard/PlanCard.baseline.test.tsx +26 -0
  70. package/src/billing/PlanCard/PlanCard.summary.test.tsx +227 -0
  71. package/src/billing/PlanCard/PlanCard.test.tsx +27 -0
  72. package/src/billing/PlanCard/PlanCard.tsx +140 -0
  73. package/src/billing/PlanCard/PlanSummary.tsx +101 -0
  74. package/src/billing/PlanCard/README.md +23 -0
  75. package/src/billing/PlanCard/__snapshots__/PlanCard.baseline.test.tsx.snap +5 -0
  76. package/src/billing/PlanCard/block.meta.ts +60 -0
  77. package/src/billing/PlanCard/examples/SummaryActionsOnly.tsx +12 -0
  78. package/src/billing/PlanCard/examples/SummaryFeaturesOnly.tsx +14 -0
  79. package/src/billing/PlanCard/examples/SummaryFull.tsx +28 -0
  80. package/src/billing/PlanCard/examples/SummaryMinimal.tsx +5 -0
  81. package/src/billing/PlanCard/examples/SummaryPriceOnly.tsx +15 -0
  82. package/src/billing/PlanCard/examples/SummaryRenewalOnly.tsx +14 -0
  83. package/src/billing/PlanCard/examples/SummaryUnassigned.tsx +5 -0
  84. package/src/billing/PlanCard/examples/SummaryUnassignedNoLabel.tsx +5 -0
  85. package/src/billing/PlanCard/examples/SummaryZeroPrice.tsx +15 -0
  86. package/src/billing/PlanCard/fixtures.ts +19 -0
  87. package/src/billing/PlanDefinitionCard/PlanDefinitionCard.test.tsx +111 -0
  88. package/src/billing/PlanDefinitionCard/PlanDefinitionCard.tsx +86 -0
  89. package/src/billing/PlanDefinitionCard/README.md +29 -0
  90. package/src/billing/PlanDefinitionCard/block.meta.ts +46 -0
  91. package/src/billing/PlanDefinitionCard/examples/Compact.tsx +23 -0
  92. package/src/billing/PlanDefinitionCard/examples/Full.tsx +23 -0
  93. package/src/billing/PlanDefinitionCard/fixtures.ts +12 -0
  94. package/src/billing/QuotaMeters/QuotaMeters.test.tsx +23 -0
  95. package/src/billing/QuotaMeters/QuotaMeters.tsx +92 -0
  96. package/src/billing/QuotaMeters/block.meta.ts +10 -0
  97. package/src/billing/QuotaMeters/fixtures.ts +9 -0
  98. package/src/billing/SpendVsBudget/SpendVsBudget.test.tsx +24 -0
  99. package/src/billing/SpendVsBudget/SpendVsBudget.tsx +105 -0
  100. package/src/billing/SpendVsBudget/block.meta.ts +10 -0
  101. package/src/billing/SpendVsBudget/fixtures.ts +18 -0
  102. package/src/billing/format.ts +42 -0
  103. package/src/billing/index.ts +21 -0
  104. package/src/builders/CodePanel/CodePanel.test.tsx +280 -0
  105. package/src/builders/CodePanel/CodePanel.tsx +239 -0
  106. package/src/builders/CodePanel/block.meta.ts +10 -0
  107. package/src/builders/CodePanel/examples/controlled.tsx +15 -0
  108. package/src/builders/CodePanel/examples/diagnostics.tsx +40 -0
  109. package/src/builders/CodePanel/fixtures.ts +95 -0
  110. package/src/builders/CodePanel/marker-store.ts +90 -0
  111. package/src/builders/CodePanel/markers.ts +62 -0
  112. package/src/builders/FlowBuilder/FlowBuilder.test.tsx +480 -0
  113. package/src/builders/FlowBuilder/FlowBuilder.tsx +343 -0
  114. package/src/builders/FlowBuilder/block.meta.ts +20 -0
  115. package/src/builders/FlowBuilder/connections.ts +67 -0
  116. package/src/builders/FlowBuilder/decorated-nodes.ts +80 -0
  117. package/src/builders/FlowBuilder/examples/extensions.tsx +120 -0
  118. package/src/builders/FlowBuilder/extensions.ts +37 -0
  119. package/src/builders/FlowBuilder/fixtures.ts +80 -0
  120. package/src/builders/FlowBuilder/parts.tsx +196 -0
  121. package/src/builders/FlowBuilder/problems.tsx +40 -0
  122. package/src/builders/LogViewer/LogViewer.test.tsx +97 -0
  123. package/src/builders/LogViewer/LogViewer.tsx +253 -0
  124. package/src/builders/LogViewer/block.meta.ts +10 -0
  125. package/src/builders/LogViewer/fixtures.ts +49 -0
  126. package/src/builders/index.ts +15 -0
  127. package/src/data-display/ActivityFeed/ActivityFeed.test.tsx +62 -0
  128. package/src/data-display/ActivityFeed/ActivityFeed.tsx +118 -0
  129. package/src/data-display/ActivityFeed/block.meta.ts +10 -0
  130. package/src/data-display/ActivityFeed/fixtures.ts +29 -0
  131. package/src/data-display/ChartCard/ChartCard.ranges.test.tsx +282 -0
  132. package/src/data-display/ChartCard/ChartCard.test.tsx +54 -0
  133. package/src/data-display/ChartCard/ChartCard.tsx +218 -0
  134. package/src/data-display/ChartCard/block.meta.ts +10 -0
  135. package/src/data-display/ChartCard/examples/custom-ranges.tsx +37 -0
  136. package/src/data-display/ChartCard/examples/heatmap.tsx +20 -0
  137. package/src/data-display/ChartCard/examples/toolbar-owned-range.tsx +34 -0
  138. package/src/data-display/ChartCard/fixtures.ts +82 -0
  139. package/src/data-display/CommentThread/CommentThread.test.tsx +29 -0
  140. package/src/data-display/CommentThread/CommentThread.tsx +120 -0
  141. package/src/data-display/CommentThread/block.meta.ts +10 -0
  142. package/src/data-display/CommentThread/fixtures.ts +28 -0
  143. package/src/data-display/DescriptionListCard/DescriptionListCard.test.tsx +28 -0
  144. package/src/data-display/DescriptionListCard/DescriptionListCard.tsx +120 -0
  145. package/src/data-display/DescriptionListCard/block.meta.ts +9 -0
  146. package/src/data-display/DescriptionListCard/fixtures.ts +12 -0
  147. package/src/data-display/KpiRow/KpiRow.test.tsx +29 -0
  148. package/src/data-display/KpiRow/KpiRow.tsx +101 -0
  149. package/src/data-display/KpiRow/block.meta.ts +10 -0
  150. package/src/data-display/KpiRow/fixtures.ts +34 -0
  151. package/src/data-display/ResourceCardGrid/ResourceCardGrid.test.tsx +22 -0
  152. package/src/data-display/ResourceCardGrid/ResourceCardGrid.tsx +98 -0
  153. package/src/data-display/ResourceCardGrid/block.meta.ts +10 -0
  154. package/src/data-display/ResourceCardGrid/fixtures.ts +72 -0
  155. package/src/data-display/StatCard/StatCard.test.tsx +30 -0
  156. package/src/data-display/StatCard/StatCard.tsx +138 -0
  157. package/src/data-display/StatCard/block.meta.ts +10 -0
  158. package/src/data-display/StatCard/fixtures.ts +26 -0
  159. package/src/data-display/StatusTimeline/StatusTimeline.test.tsx +153 -0
  160. package/src/data-display/StatusTimeline/StatusTimeline.tsx +114 -0
  161. package/src/data-display/StatusTimeline/block.meta.ts +10 -0
  162. package/src/data-display/StatusTimeline/examples/Empty.tsx +6 -0
  163. package/src/data-display/StatusTimeline/fixtures.ts +62 -0
  164. package/src/data-display/index.ts +34 -0
  165. package/src/data-display/shared/TrendDelta.tsx +60 -0
  166. package/src/feedback/ConfirmDestructive/ConfirmDestructive.test.tsx +47 -0
  167. package/src/feedback/ConfirmDestructive/ConfirmDestructive.tsx +145 -0
  168. package/src/feedback/ConfirmDestructive/block.meta.ts +10 -0
  169. package/src/feedback/EmptyStates/EmptyStates.test.tsx +48 -0
  170. package/src/feedback/EmptyStates/EmptyStates.tsx +191 -0
  171. package/src/feedback/EmptyStates/block.meta.ts +10 -0
  172. package/src/feedback/EmptyStates/fixtures.ts +6 -0
  173. package/src/feedback/ErrorPanel/ErrorPanel.test.tsx +37 -0
  174. package/src/feedback/ErrorPanel/ErrorPanel.tsx +146 -0
  175. package/src/feedback/ErrorPanel/block.meta.ts +10 -0
  176. package/src/feedback/InlineCallout/InlineCallout.test.tsx +33 -0
  177. package/src/feedback/InlineCallout/InlineCallout.tsx +81 -0
  178. package/src/feedback/InlineCallout/block.meta.ts +9 -0
  179. package/src/feedback/Skeletons/Skeletons.test.tsx +25 -0
  180. package/src/feedback/Skeletons/Skeletons.tsx +142 -0
  181. package/src/feedback/Skeletons/block.meta.ts +10 -0
  182. package/src/feedback/SystemBanner/SystemBanner.test.tsx +29 -0
  183. package/src/feedback/SystemBanner/SystemBanner.tsx +105 -0
  184. package/src/feedback/SystemBanner/block.meta.ts +9 -0
  185. package/src/feedback/index.ts +14 -0
  186. package/src/feedback/types.ts +13 -0
  187. package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.test.tsx +36 -0
  188. package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.tsx +197 -0
  189. package/src/forms/CreateApiKeyDialog/block.meta.ts +10 -0
  190. package/src/forms/CreateApiKeyDialog/fixtures.ts +31 -0
  191. package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.test.tsx +339 -0
  192. package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.tsx +193 -0
  193. package/src/forms/CredentialDisclosureDialog/block.meta.ts +10 -0
  194. package/src/forms/CredentialDisclosureDialog/examples/Fresh.tsx +32 -0
  195. package/src/forms/CredentialDisclosureDialog/examples/ResponseGating.tsx +69 -0
  196. package/src/forms/CredentialDisclosureDialog/examples/Stored.tsx +32 -0
  197. package/src/forms/CredentialDisclosureDialog/fixtures.ts +14 -0
  198. package/src/forms/DangerZone/ConfirmDestructiveDialog.tsx +101 -0
  199. package/src/forms/DangerZone/DangerZone.test.tsx +28 -0
  200. package/src/forms/DangerZone/DangerZone.tsx +86 -0
  201. package/src/forms/DangerZone/block.meta.ts +10 -0
  202. package/src/forms/DangerZone/fixtures.ts +36 -0
  203. package/src/forms/DateRangeFilter/DateRangeFilter.test.tsx +26 -0
  204. package/src/forms/DateRangeFilter/DateRangeFilter.tsx +83 -0
  205. package/src/forms/DateRangeFilter/block.meta.ts +9 -0
  206. package/src/forms/DialogWizard/DialogWizard.test.tsx +95 -0
  207. package/src/forms/DialogWizard/DialogWizard.tsx +39 -0
  208. package/src/forms/DialogWizard/block.meta.ts +10 -0
  209. package/src/forms/DialogWizard/examples/CreateResource.tsx +65 -0
  210. package/src/forms/DialogWizard/fixtures.ts +20 -0
  211. package/src/forms/FileUploadPanel/FileUploadPanel.test.tsx +43 -0
  212. package/src/forms/FileUploadPanel/FileUploadPanel.tsx +244 -0
  213. package/src/forms/FileUploadPanel/block.meta.ts +10 -0
  214. package/src/forms/FileUploadPanel/fixtures.ts +66 -0
  215. package/src/forms/FormSection/FormSection.test.tsx +34 -0
  216. package/src/forms/FormSection/FormSection.tsx +77 -0
  217. package/src/forms/FormSection/block.meta.ts +9 -0
  218. package/src/forms/InviteMembersDialog/InviteMembersDialog.test.tsx +37 -0
  219. package/src/forms/InviteMembersDialog/InviteMembersDialog.tsx +183 -0
  220. package/src/forms/InviteMembersDialog/block.meta.ts +10 -0
  221. package/src/forms/InviteMembersDialog/fixtures.ts +14 -0
  222. package/src/forms/SettingsRow/SettingsRow.test.tsx +50 -0
  223. package/src/forms/SettingsRow/SettingsRow.tsx +74 -0
  224. package/src/forms/SettingsRow/block.meta.ts +9 -0
  225. package/src/forms/ValidationSummary/ValidationSummary.test.tsx +46 -0
  226. package/src/forms/ValidationSummary/ValidationSummary.tsx +75 -0
  227. package/src/forms/ValidationSummary/block.meta.ts +9 -0
  228. package/src/forms/ValidationSummary/fixtures.ts +13 -0
  229. package/src/forms/index.ts +27 -0
  230. package/src/index.ts +14 -0
  231. package/src/marketing/ComparisonTable/ComparisonTable.test.tsx +69 -0
  232. package/src/marketing/ComparisonTable/ComparisonTable.tsx +245 -0
  233. package/src/marketing/ComparisonTable/block.meta.ts +10 -0
  234. package/src/marketing/ComparisonTable/examples/options.tsx +48 -0
  235. package/src/marketing/ComparisonTable/fixtures.ts +83 -0
  236. package/src/marketing/Faq/Faq.test.tsx +25 -0
  237. package/src/marketing/Faq/Faq.tsx +68 -0
  238. package/src/marketing/Faq/block.meta.ts +10 -0
  239. package/src/marketing/Faq/fixtures.ts +32 -0
  240. package/src/marketing/FeatureGrid/FeatureGrid.test.tsx +19 -0
  241. package/src/marketing/FeatureGrid/FeatureGrid.tsx +79 -0
  242. package/src/marketing/FeatureGrid/block.meta.ts +10 -0
  243. package/src/marketing/FeatureGrid/fixtures.ts +36 -0
  244. package/src/marketing/Hero/Hero.test.tsx +33 -0
  245. package/src/marketing/Hero/Hero.tsx +107 -0
  246. package/src/marketing/Hero/block.meta.ts +10 -0
  247. package/src/marketing/PricingTable/PricingTable.test.tsx +92 -0
  248. package/src/marketing/PricingTable/PricingTable.tsx +323 -0
  249. package/src/marketing/PricingTable/block.meta.ts +10 -0
  250. package/src/marketing/PricingTable/fixtures.ts +34 -0
  251. package/src/marketing/actions.tsx +40 -0
  252. package/src/marketing/index.ts +15 -0
  253. package/src/meta.ts +23 -0
  254. package/src/page-chrome/BulkActionBar/BulkActionBar.test.tsx +34 -0
  255. package/src/page-chrome/BulkActionBar/BulkActionBar.tsx +76 -0
  256. package/src/page-chrome/BulkActionBar/block.meta.ts +9 -0
  257. package/src/page-chrome/PageHeader/PageHeader.test.tsx +68 -0
  258. package/src/page-chrome/PageHeader/PageHeader.tsx +152 -0
  259. package/src/page-chrome/PageHeader/block.meta.ts +10 -0
  260. package/src/page-chrome/PageHeader/fixtures.ts +29 -0
  261. package/src/page-chrome/PageToolbar/PageToolbar.test.tsx +38 -0
  262. package/src/page-chrome/PageToolbar/PageToolbar.tsx +138 -0
  263. package/src/page-chrome/PageToolbar/block.meta.ts +10 -0
  264. package/src/page-chrome/PageToolbar/fixtures.ts +8 -0
  265. package/src/page-chrome/SectionHeader/SectionHeader.test.tsx +28 -0
  266. package/src/page-chrome/SectionHeader/SectionHeader.tsx +73 -0
  267. package/src/page-chrome/SectionHeader/block.meta.ts +9 -0
  268. package/src/page-chrome/StickyActionBar/StickyActionBar.test.tsx +29 -0
  269. package/src/page-chrome/StickyActionBar/StickyActionBar.tsx +66 -0
  270. package/src/page-chrome/StickyActionBar/block.meta.ts +9 -0
  271. package/src/page-chrome/index.ts +13 -0
  272. package/src/page-chrome/types.ts +25 -0
  273. package/src/scheduling/AgendaList/AgendaList.test.tsx +28 -0
  274. package/src/scheduling/AgendaList/AgendaList.tsx +165 -0
  275. package/src/scheduling/AgendaList/block.meta.ts +10 -0
  276. package/src/scheduling/MonthCalendar/MonthCalendar.test.tsx +32 -0
  277. package/src/scheduling/MonthCalendar/MonthCalendar.tsx +223 -0
  278. package/src/scheduling/MonthCalendar/block.meta.ts +10 -0
  279. package/src/scheduling/TimeSlotPicker/TimeSlotPicker.test.tsx +26 -0
  280. package/src/scheduling/TimeSlotPicker/TimeSlotPicker.tsx +115 -0
  281. package/src/scheduling/TimeSlotPicker/block.meta.ts +10 -0
  282. package/src/scheduling/TimeSlotPicker/fixtures.ts +10 -0
  283. package/src/scheduling/WeekCalendar/WeekCalendar.test.tsx +27 -0
  284. package/src/scheduling/WeekCalendar/WeekCalendar.tsx +274 -0
  285. package/src/scheduling/WeekCalendar/block.meta.ts +10 -0
  286. package/src/scheduling/index.ts +11 -0
  287. package/src/scheduling/shared/CalendarHeader.tsx +54 -0
  288. package/src/scheduling/shared/EventTile.tsx +21 -0
  289. package/src/scheduling/shared/dates.ts +98 -0
  290. package/src/scheduling/shared/events.ts +90 -0
  291. package/src/scheduling/shared/fixtures.ts +54 -0
  292. package/src/shell/AppSidebar/AppSidebar.test.tsx +38 -0
  293. package/src/shell/AppSidebar/AppSidebar.tsx +226 -0
  294. package/src/shell/AppSidebar/block.meta.ts +10 -0
  295. package/src/shell/AppSidebar/fixtures.ts +58 -0
  296. package/src/shell/AppTopbar/AppTopbar.test.tsx +41 -0
  297. package/src/shell/AppTopbar/AppTopbar.tsx +82 -0
  298. package/src/shell/AppTopbar/block.meta.ts +19 -0
  299. package/src/shell/AppTopbar/fixtures.ts +22 -0
  300. package/src/shell/EnvironmentBadge/EnvironmentBadge.test.tsx +22 -0
  301. package/src/shell/EnvironmentBadge/EnvironmentBadge.tsx +55 -0
  302. package/src/shell/EnvironmentBadge/block.meta.ts +10 -0
  303. package/src/shell/GlobalSearch/GlobalSearch.test.tsx +28 -0
  304. package/src/shell/GlobalSearch/GlobalSearch.tsx +80 -0
  305. package/src/shell/GlobalSearch/block.meta.ts +10 -0
  306. package/src/shell/GlobalSearch/fixtures.ts +64 -0
  307. package/src/shell/index.ts +11 -0
  308. package/src/styles.css +2 -0
  309. package/src/tables/DataTable/DataTable.edit-views.test.tsx +249 -0
  310. package/src/tables/DataTable/DataTable.layout.test.tsx +188 -0
  311. package/src/tables/DataTable/DataTable.single-select.test.tsx +501 -0
  312. package/src/tables/DataTable/DataTable.test.tsx +268 -0
  313. package/src/tables/DataTable/DataTable.tsx +520 -0
  314. package/src/tables/DataTable/block.meta.ts +27 -0
  315. package/src/tables/DataTable/column-menu.tsx +109 -0
  316. package/src/tables/DataTable/column-sizing.ts +166 -0
  317. package/src/tables/DataTable/data-table-body.tsx +194 -0
  318. package/src/tables/DataTable/data-table-props.ts +156 -0
  319. package/src/tables/DataTable/data-table-row.tsx +220 -0
  320. package/src/tables/DataTable/data-table-utils.ts +195 -0
  321. package/src/tables/DataTable/density-toggle.tsx +30 -0
  322. package/src/tables/DataTable/editable-cell.tsx +274 -0
  323. package/src/tables/DataTable/examples/editable.tsx +71 -0
  324. package/src/tables/DataTable/examples/grouped.tsx +18 -0
  325. package/src/tables/DataTable/examples/resizable-pinned.tsx +20 -0
  326. package/src/tables/DataTable/examples/saved-views.tsx +61 -0
  327. package/src/tables/DataTable/examples/single-selection.tsx +55 -0
  328. package/src/tables/DataTable/examples/virtualized.tsx +20 -0
  329. package/src/tables/DataTable/fixtures.ts +119 -0
  330. package/src/tables/DataTable/header-cell.tsx +86 -0
  331. package/src/tables/DataTable/resize-handle.tsx +88 -0
  332. package/src/tables/DataTable/single-selection.tsx +149 -0
  333. package/src/tables/DataTable/use-table-views.ts +79 -0
  334. package/src/tables/DataTable/use-virtual-rows.ts +83 -0
  335. package/src/tables/DataTable/views.tsx +186 -0
  336. package/src/tables/FilterBar/FilterBar.test.tsx +43 -0
  337. package/src/tables/FilterBar/FilterBar.tsx +199 -0
  338. package/src/tables/FilterBar/block.meta.ts +10 -0
  339. package/src/tables/FilterBar/fixtures.ts +32 -0
  340. package/src/tables/InlineEditRow/InlineEditRow.test.tsx +41 -0
  341. package/src/tables/InlineEditRow/InlineEditRow.tsx +215 -0
  342. package/src/tables/InlineEditRow/block.meta.ts +10 -0
  343. package/src/tables/InlineEditRow/fixtures.ts +28 -0
  344. package/src/tables/PaginationFooter/PaginationFooter.test.tsx +29 -0
  345. package/src/tables/PaginationFooter/PaginationFooter.tsx +94 -0
  346. package/src/tables/PaginationFooter/block.meta.ts +10 -0
  347. package/src/tables/SubordinateList/SubordinateList.test.tsx +351 -0
  348. package/src/tables/SubordinateList/SubordinateList.tsx +140 -0
  349. package/src/tables/SubordinateList/block.meta.ts +10 -0
  350. package/src/tables/SubordinateList/examples/ResourceForm.tsx +129 -0
  351. package/src/tables/SubordinateList/examples/empty.tsx +5 -0
  352. package/src/tables/SubordinateList/examples/error.tsx +5 -0
  353. package/src/tables/SubordinateList/examples/loading.tsx +5 -0
  354. package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.test.tsx +159 -0
  355. package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.tsx +60 -0
  356. package/src/tables/TruncatedCollectionFooter/block.meta.ts +10 -0
  357. package/src/tables/TruncatedCollectionFooter/examples/exact.tsx +6 -0
  358. package/src/tables/TruncatedCollectionFooter/examples/has-more.tsx +12 -0
  359. package/src/tables/TruncatedCollectionFooter/fixtures.ts +2 -0
  360. package/src/tables/index.ts +45 -0
  361. package/src/team/MembersTable/MembersTable.test.tsx +29 -0
  362. package/src/team/MembersTable/MembersTable.tsx +141 -0
  363. package/src/team/MembersTable/block.meta.ts +9 -0
  364. package/src/team/MembersTable/fixtures.ts +19 -0
  365. package/src/team/NotificationMatrix/NotificationMatrix.test.tsx +36 -0
  366. package/src/team/NotificationMatrix/NotificationMatrix.tsx +144 -0
  367. package/src/team/NotificationMatrix/block.meta.ts +9 -0
  368. package/src/team/NotificationMatrix/fixtures.ts +43 -0
  369. package/src/team/PendingInvitations/PendingInvitations.test.tsx +30 -0
  370. package/src/team/PendingInvitations/PendingInvitations.tsx +132 -0
  371. package/src/team/PendingInvitations/block.meta.ts +9 -0
  372. package/src/team/PendingInvitations/fixtures.ts +39 -0
  373. package/src/team/ProfileHeader/ProfileHeader.test.tsx +29 -0
  374. package/src/team/ProfileHeader/ProfileHeader.tsx +106 -0
  375. package/src/team/ProfileHeader/block.meta.ts +10 -0
  376. package/src/team/ProfileHeader/fixtures.ts +28 -0
  377. package/src/team/SessionsDevices/SessionsDevices.test.tsx +30 -0
  378. package/src/team/SessionsDevices/SessionsDevices.tsx +126 -0
  379. package/src/team/SessionsDevices/block.meta.ts +10 -0
  380. package/src/team/SessionsDevices/fixtures.ts +58 -0
  381. package/src/team/index.ts +15 -0
  382. package/src/test/a11y.ts +18 -0
  383. package/src/test/heading-levels.test.tsx +69 -0
  384. package/src/test/setup.ts +36 -0
  385. package/README.md +0 -3
@@ -0,0 +1,36 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { TokenCostCard } from './TokenCostCard';
5
+
6
+ describe('TokenCostCard', () => {
7
+ it('shows token counts, cost and the budget meter for the default period', async () => {
8
+ render(<TokenCostCard />);
9
+ expect(screen.getByText('8.9M')).toBeInTheDocument();
10
+ expect(screen.getByText('1.4M')).toBeInTheDocument();
11
+ expect(screen.getByText('$102.40')).toBeInTheDocument();
12
+ const meter = screen.getByRole('meter', { name: 'Budget' });
13
+ expect(meter).toHaveAttribute('aria-valuetext', '$102.40 of $250.00');
14
+ expect(screen.getByText('$147.60 left in this period')).toBeInTheDocument();
15
+ await expectNoAxeViolations();
16
+ });
17
+
18
+ it('switches period', async () => {
19
+ const user = userEvent.setup();
20
+ render(<TokenCostCard />);
21
+ await user.click(screen.getByRole('button', { name: '30d' }));
22
+ expect(screen.getByText('$431.75')).toBeInTheDocument();
23
+ expect(screen.getByRole('meter', { name: 'Budget' })).toHaveAttribute('aria-valuetext', '$431.75 of $500.00');
24
+ });
25
+
26
+ it('reports overspend and hides the switch for a single period', () => {
27
+ render(
28
+ <TokenCostCard
29
+ periods={[{ id: 'm', label: 'Month', inputTokens: 10, outputTokens: 0, cost: 60, budget: 50 }]}
30
+ currency="EUR"
31
+ />,
32
+ );
33
+ expect(screen.queryByRole('group', { name: 'Period' })).not.toBeInTheDocument();
34
+ expect(screen.getByText('€10.00 over budget')).toBeInTheDocument();
35
+ });
36
+ });
@@ -0,0 +1,130 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Card,
4
+ CardAction,
5
+ CardBody,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ Meter,
10
+ Toggle,
11
+ ToggleGroup,
12
+ cn,
13
+ } from '@gntik-ai/ui';
14
+ import { tokenUsagePeriods, type TokenUsagePeriod } from './fixtures';
15
+
16
+ export type { TokenUsagePeriod } from './fixtures';
17
+
18
+ export interface TokenCostCardProps {
19
+ /** One entry per selectable period (a single entry hides the switch). */
20
+ periods?: TokenUsagePeriod[];
21
+ defaultPeriod?: string;
22
+ /** ISO 4217 currency code for cost and budget. */
23
+ currency?: string;
24
+ title?: string;
25
+ /** Subtitle, e.g. the model or project the usage belongs to. */
26
+ description?: string;
27
+ /** Heading level of the title, to fit the page outline (default h3). */
28
+ titleAs?: 'h2' | 'h3' | 'h4';
29
+ className?: string;
30
+ }
31
+
32
+ const compact = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
33
+ const full = new Intl.NumberFormat('en-US');
34
+
35
+ /** Token usage and spend for a period, with a budget meter. */
36
+ export function TokenCostCard({
37
+ periods = tokenUsagePeriods,
38
+ defaultPeriod,
39
+ currency = 'USD',
40
+ title = 'Token usage',
41
+ description = 'All models · current project',
42
+ titleAs = 'h3',
43
+ className,
44
+ }: TokenCostCardProps) {
45
+ const [periodId, setPeriodId] = useState(defaultPeriod ?? periods[1]?.id ?? periods[0]?.id ?? '');
46
+ const period = periods.find((p) => p.id === periodId) ?? periods[0];
47
+ const money = new Intl.NumberFormat('en-US', { style: 'currency', currency });
48
+ if (!period) return null;
49
+
50
+ const totalTokens = period.inputTokens + period.outputTokens;
51
+ const inputShare = totalTokens > 0 ? (period.inputTokens / totalTokens) * 100 : 0;
52
+ const remaining = period.budget - period.cost;
53
+
54
+ const stats = [
55
+ { label: 'Input tokens', value: period.inputTokens, swatch: 'bg-primary' },
56
+ { label: 'Output tokens', value: period.outputTokens, swatch: 'bg-category-violet' },
57
+ ];
58
+
59
+ return (
60
+ <Card className={cn('w-full', className)}>
61
+ <CardHeader>
62
+ <div>
63
+ <CardTitle as={titleAs}>{title}</CardTitle>
64
+ <CardDescription>{description}</CardDescription>
65
+ </div>
66
+ {periods.length > 1 && (
67
+ <CardAction>
68
+ <ToggleGroup
69
+ aria-label="Period"
70
+ size="sm"
71
+ value={[period.id]}
72
+ onValueChange={(v) => v[0] && setPeriodId(v[0])}
73
+ >
74
+ {periods.map((p) => (
75
+ <Toggle key={p.id} value={p.id}>
76
+ {p.label}
77
+ </Toggle>
78
+ ))}
79
+ </ToggleGroup>
80
+ </CardAction>
81
+ )}
82
+ </CardHeader>
83
+ <CardBody className="flex flex-col gap-5">
84
+ <dl className="grid grid-cols-2 gap-4 sm:grid-cols-3">
85
+ {stats.map((s) => (
86
+ <div key={s.label}>
87
+ <dt className="flex items-center gap-1.5 text-[12px] text-muted-foreground">
88
+ <span aria-hidden className={cn('size-2 rounded-sm', s.swatch)} />
89
+ {s.label}
90
+ </dt>
91
+ <dd
92
+ className="mt-1 font-mono text-[20px] font-semibold tracking-tight text-foreground"
93
+ title={full.format(s.value)}
94
+ >
95
+ {compact.format(s.value)}
96
+ </dd>
97
+ </div>
98
+ ))}
99
+ <div className="col-span-2 sm:col-span-1">
100
+ <dt className="text-[12px] text-muted-foreground">Cost</dt>
101
+ <dd className="mt-1 font-mono text-[20px] font-semibold tracking-tight text-foreground">
102
+ {money.format(period.cost)}
103
+ </dd>
104
+ </div>
105
+ </dl>
106
+ <div>
107
+ <div aria-hidden className="flex h-2 overflow-hidden rounded-full bg-secondary">
108
+ <span className="bg-primary" style={{ width: `${inputShare}%` }} />
109
+ <span className="flex-1 bg-category-violet" />
110
+ </div>
111
+ <p className="mt-1.5 font-mono text-[11px] text-muted-foreground">
112
+ {Math.round(inputShare)}% input · {100 - Math.round(inputShare)}% output · {full.format(totalTokens)} tokens
113
+ </p>
114
+ </div>
115
+ <Meter
116
+ label="Budget"
117
+ value={period.cost}
118
+ max={period.budget}
119
+ valueLabel={`${money.format(period.cost)} of ${money.format(period.budget)}`}
120
+ aria-valuetext={`${money.format(period.cost)} of ${money.format(period.budget)}`}
121
+ note={
122
+ remaining >= 0
123
+ ? `${money.format(remaining)} left in this period`
124
+ : `${money.format(-remaining)} over budget`
125
+ }
126
+ />
127
+ </CardBody>
128
+ </Card>
129
+ );
130
+ }
@@ -0,0 +1,9 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Token cost card',
5
+ family: 'ai',
6
+ status: 'beta',
7
+ description: 'Input and output token counts, their split, total cost and a budget meter, with a period switch.',
8
+ uses: ['Card', 'ToggleGroup', 'Toggle', 'Meter'],
9
+ };
@@ -0,0 +1,18 @@
1
+ export interface TokenUsagePeriod {
2
+ /** Stable id, e.g. "7d". */
3
+ id: string;
4
+ /** Toggle label, e.g. "7 days". */
5
+ label: string;
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ /** Spend in the period, in `currency`. */
9
+ cost: number;
10
+ /** Budget for the period, in `currency`. */
11
+ budget: number;
12
+ }
13
+
14
+ export const tokenUsagePeriods: TokenUsagePeriod[] = [
15
+ { id: '24h', label: '24h', inputTokens: 1_284_000, outputTokens: 212_400, cost: 14.82, budget: 40 },
16
+ { id: '7d', label: '7d', inputTokens: 8_930_500, outputTokens: 1_402_900, cost: 102.4, budget: 250 },
17
+ { id: '30d', label: '30d', inputTokens: 36_120_000, outputTokens: 6_015_300, cost: 431.75, budget: 500 },
18
+ ];
@@ -0,0 +1,119 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import NoDetails from './examples/no-details';
5
+ import RunningTrace from './examples/running';
6
+ import { TraceWaterfall, formatSpanDuration } from './TraceWaterfall';
7
+
8
+ const spanList = () => screen.getByRole('list', { name: 'Spans' });
9
+
10
+ describe('TraceWaterfall', () => {
11
+ it('renders the span tree with bars positioned on the time axis', async () => {
12
+ render(<TraceWaterfall />);
13
+ expect(within(spanList()).getAllByRole('listitem')).toHaveLength(8);
14
+ expect(screen.getByText('8 spans · 2.45s')).toBeInTheDocument();
15
+ expect(screen.getByText('1 error')).toBeInTheDocument();
16
+ const details = screen.getByRole('complementary', { name: 'Span details' });
17
+ expect(within(details).getByRole('heading', { name: 'POST /v1/answer' })).toBeInTheDocument();
18
+ const bar = screen
19
+ .getByRole('button', { name: /^generate answer/ })
20
+ .querySelector('[style*="width"]') as HTMLElement;
21
+ expect(bar.style.insetInlineStart).toBe(`${(440 / 2450) * 100}%`);
22
+ await expectNoAxeViolations();
23
+ });
24
+
25
+ it('collapses and expands spans, and selects a span to show its details', async () => {
26
+ const user = userEvent.setup();
27
+ const onSelect = vi.fn();
28
+ render(<TraceWaterfall onSelect={onSelect} />);
29
+ const collapse = screen.getByRole('button', { name: 'Collapse retrieve context' });
30
+ expect(collapse).toHaveAttribute('aria-expanded', 'true');
31
+ await user.click(collapse);
32
+ expect(within(spanList()).getAllByRole('listitem')).toHaveLength(6);
33
+ expect(screen.queryByRole('button', { name: /^embed query/ })).not.toBeInTheDocument();
34
+ await user.click(screen.getByRole('button', { name: 'Expand retrieve context' }));
35
+ expect(within(spanList()).getAllByRole('listitem')).toHaveLength(8);
36
+
37
+ const span = screen.getByRole('button', { name: 'tool: lookup_order, 150ms, error' });
38
+ await user.click(span);
39
+ expect(span).toHaveAttribute('aria-pressed', 'true');
40
+ expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 's6' }));
41
+ const details = screen.getByRole('complementary', { name: 'Span details' });
42
+ expect(within(details).getByText('timeout after 150ms')).toBeInTheDocument();
43
+ });
44
+
45
+ it('formats durations', () => {
46
+ expect(formatSpanDuration(412)).toBe('412ms');
47
+ expect(formatSpanDuration(2450)).toBe('2.45s');
48
+ expect(formatSpanDuration(12_500)).toBe('12.5s');
49
+ });
50
+
51
+ it('moves through spans with the arrow keys and selects with Enter (one tab stop)', async () => {
52
+ const user = userEvent.setup();
53
+ const onSelect = vi.fn();
54
+ render(<TraceWaterfall onSelect={onSelect} />);
55
+ await user.tab();
56
+ const root = screen.getByRole('button', { name: 'POST /v1/answer, 2.45s' });
57
+ expect(root).toHaveFocus();
58
+ await user.keyboard('{ArrowDown}');
59
+ expect(screen.getByRole('button', { name: /^retrieve context,/ })).toHaveFocus();
60
+ await user.keyboard('{ArrowLeft}');
61
+ expect(screen.getByRole('button', { name: 'Expand retrieve context' })).toHaveAttribute('aria-expanded', 'false');
62
+ await user.keyboard('{ArrowRight}');
63
+ expect(screen.getByRole('button', { name: 'Collapse retrieve context' })).toHaveAttribute('aria-expanded', 'true');
64
+ await user.keyboard('{ArrowRight}');
65
+ expect(screen.getByRole('button', { name: /^embed query,/ })).toHaveFocus();
66
+ await user.keyboard('{ArrowLeft}');
67
+ expect(screen.getByRole('button', { name: /^retrieve context,/ })).toHaveFocus();
68
+ await user.keyboard('{End}');
69
+ const last = screen.getByRole('button', { name: /^format response,/ });
70
+ expect(last).toHaveFocus();
71
+ await user.keyboard('{Enter}');
72
+ expect(last).toHaveAttribute('aria-pressed', 'true');
73
+ expect(onSelect).toHaveBeenLastCalledWith(expect.objectContaining({ id: 's8' }));
74
+ await user.keyboard('{Home}');
75
+ expect(root).toHaveFocus();
76
+ // Only one span button is in the tab order.
77
+ expect(
78
+ within(spanList())
79
+ .getAllByRole('button')
80
+ .filter((b) => b.tabIndex === 0),
81
+ ).toHaveLength(1);
82
+ });
83
+
84
+ it('supports controlled selection and running spans (not colour-only)', async () => {
85
+ const user = userEvent.setup();
86
+ render(<RunningTrace />);
87
+ expect(screen.getByText('2 running')).toBeInTheDocument();
88
+ const generating = screen.getByRole('button', { name: 'generate answer, 1.39s, running' });
89
+ expect(generating).toHaveAttribute('aria-pressed', 'true');
90
+ expect(generating).toHaveTextContent('running');
91
+ expect(generating.querySelector('[data-span-bar]')).toHaveClass('motion-reduce:animate-none');
92
+ const details = screen.getByRole('complementary', { name: 'Span details' });
93
+ expect(within(details).getByText('Running')).toBeInTheDocument();
94
+ expect(within(details).getByText('Elapsed')).toBeInTheDocument();
95
+ await user.click(screen.getByRole('button', { name: /^tool: search_docs/ }));
96
+ expect(within(details).getByRole('heading', { name: 'tool: search_docs' })).toBeInTheDocument();
97
+ await expectNoAxeViolations();
98
+ });
99
+
100
+ it('ignores clicks in controlled mode until the parent updates selectedId', async () => {
101
+ const user = userEvent.setup();
102
+ const onSelectedIdChange = vi.fn();
103
+ render(<TraceWaterfall selectedId="s2" onSelectedIdChange={onSelectedIdChange} />);
104
+ await user.click(screen.getByRole('button', { name: /^format response,/ }));
105
+ expect(onSelectedIdChange).toHaveBeenCalledWith('s8');
106
+ expect(screen.getByRole('button', { name: /^retrieve context,/ })).toHaveAttribute('aria-pressed', 'true');
107
+ });
108
+
109
+ it('hides the details pane or renders custom details', async () => {
110
+ const { unmount } = render(<NoDetails />);
111
+ expect(screen.queryByRole('complementary')).toBeNull();
112
+ await expectNoAxeViolations();
113
+ unmount();
114
+ render(<TraceWaterfall renderDetails={(span) => <p>Custom {span.id}</p>} />);
115
+ expect(
116
+ within(screen.getByRole('complementary', { name: 'Span details' })).getByText('Custom s1'),
117
+ ).toBeInTheDocument();
118
+ });
119
+ });
@@ -0,0 +1,317 @@
1
+ import { useId, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
2
+ import { ChevronRight } from '@gntik-ai/icons';
3
+ import { Badge, cn, useI18n } from '@gntik-ai/ui';
4
+ import { traceSpans, type SpanKind, type TraceSpan } from './fixtures';
5
+ import { SpanDetails, formatSpanDuration } from './span-details';
6
+
7
+ export type { SpanKind, TraceSpan } from './fixtures';
8
+ export { SPAN_STATUSES, SpanDetails, formatSpanDuration, type SpanDetailsProps } from './span-details';
9
+
10
+ /** Bar colour per span kind (category tokens; errors always use destructive). */
11
+ const KIND_BAR: Record<SpanKind, string> = {
12
+ http: 'bg-muted-foreground/70',
13
+ chain: 'bg-category-amber',
14
+ llm: 'bg-primary',
15
+ tool: 'bg-category-violet',
16
+ retrieval: 'bg-category-cyan',
17
+ };
18
+
19
+ export interface TraceWaterfallProps {
20
+ spans?: TraceSpan[];
21
+ title?: string;
22
+ /** Selected span (controlled). `null` selects nothing. */
23
+ selectedId?: string | null;
24
+ /** Span selected initially (defaults to the first root). */
25
+ defaultSelectedId?: string;
26
+ /** Called with the span selected by click, Enter or Space. */
27
+ onSelect?: (span: TraceSpan) => void;
28
+ /** Called with the id of the span selected (pair with `selectedId`). */
29
+ onSelectedIdChange?: (id: string) => void;
30
+ /** Span ids collapsed initially. */
31
+ defaultCollapsed?: string[];
32
+ /** Shows the details pane of the selected span (default true). */
33
+ showDetails?: boolean;
34
+ /** Custom content of the details pane (default: name, kind, status, timing and attributes). */
35
+ renderDetails?: (span: TraceSpan) => ReactNode;
36
+ /** Heading level of the title, to fit the page outline (default h3); inner headings use the next level. */
37
+ titleAs?: 'h2' | 'h3' | 'h4';
38
+ className?: string;
39
+ }
40
+
41
+ interface Row {
42
+ span: TraceSpan;
43
+ depth: number;
44
+ hasChildren: boolean;
45
+ }
46
+
47
+ /** Depth-first rows, skipping the descendants of collapsed spans. */
48
+ function flatten(spans: TraceSpan[], collapsed: Set<string>): Row[] {
49
+ const ids = new Set(spans.map((s) => s.id));
50
+ const children = new Map<string | undefined, TraceSpan[]>();
51
+ for (const s of spans) {
52
+ const key = s.parentId && ids.has(s.parentId) ? s.parentId : undefined;
53
+ children.set(key, [...(children.get(key) ?? []), s]);
54
+ }
55
+ const rows: Row[] = [];
56
+ const walk = (parent: string | undefined, depth: number) => {
57
+ const list = [...(children.get(parent) ?? [])].sort((a, b) => a.start - b.start);
58
+ for (const span of list) {
59
+ const hasChildren = (children.get(span.id)?.length ?? 0) > 0;
60
+ rows.push({ span, depth, hasChildren });
61
+ if (hasChildren && !collapsed.has(span.id)) walk(span.id, depth + 1);
62
+ }
63
+ };
64
+ walk(undefined, 0);
65
+ return rows;
66
+ }
67
+
68
+ const statusSuffix = (span: TraceSpan) =>
69
+ span.status === 'error' ? ', error' : span.status === 'running' ? ', running' : '';
70
+
71
+ /**
72
+ * Trace waterfall: span tree + duration bars on a shared time axis + an optional details pane for
73
+ * the selected span. The span list is one tab stop: ↑/↓ move, Home/End jump, → expands or moves to
74
+ * the first child, ← collapses or moves to the parent, Enter/Space select.
75
+ */
76
+ export function TraceWaterfall({
77
+ spans = traceSpans,
78
+ title = 'Trace',
79
+ selectedId: selectedProp,
80
+ defaultSelectedId,
81
+ onSelect,
82
+ onSelectedIdChange,
83
+ defaultCollapsed = [],
84
+ showDetails = true,
85
+ renderDetails,
86
+ titleAs: TitleTag = 'h3',
87
+ className,
88
+ }: TraceWaterfallProps) {
89
+ const { t } = useI18n();
90
+ const SubTag = TitleTag === 'h2' ? 'h3' : TitleTag === 'h3' ? 'h4' : 'h5';
91
+ const titleId = useId();
92
+ const [collapsed, setCollapsed] = useState(() => new Set(defaultCollapsed));
93
+ const [innerSelected, setInnerSelected] = useState<string | undefined>(
94
+ defaultSelectedId ?? spans.find((s) => !s.parentId)?.id,
95
+ );
96
+ const [focusId, setFocusId] = useState<string | undefined>(undefined);
97
+ const buttons = useRef(new Map<string, HTMLButtonElement>());
98
+ const rows = useMemo(() => flatten(spans, collapsed), [spans, collapsed]);
99
+ const total = Math.max(1, ...spans.map((s) => s.start + s.duration));
100
+ const selectedId = selectedProp === undefined ? innerSelected : (selectedProp ?? undefined);
101
+ const selected = spans.find((s) => s.id === selectedId);
102
+ const errors = spans.filter((s) => s.status === 'error').length;
103
+ const running = spans.filter((s) => s.status === 'running').length;
104
+ // Roving tab stop: the focused span, else the selected one, else the first row.
105
+ const tabStop =
106
+ rows.find((r) => r.span.id === focusId)?.span.id ??
107
+ rows.find((r) => r.span.id === selectedId)?.span.id ??
108
+ rows[0]?.span.id;
109
+
110
+ const setOpen = (id: string, open: boolean) =>
111
+ setCollapsed((prev) => {
112
+ if (prev.has(id) === !open) return prev;
113
+ const next = new Set(prev);
114
+ if (open) next.delete(id);
115
+ else next.add(id);
116
+ return next;
117
+ });
118
+
119
+ const select = (span: TraceSpan) => {
120
+ setInnerSelected(span.id);
121
+ onSelectedIdChange?.(span.id);
122
+ onSelect?.(span);
123
+ };
124
+
125
+ const focusSpan = (id: string | undefined) => {
126
+ if (!id) return;
127
+ setFocusId(id);
128
+ buttons.current.get(id)?.focus();
129
+ };
130
+
131
+ const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
132
+ const row = rows[index];
133
+ if (!row) return;
134
+ const open = !collapsed.has(row.span.id);
135
+ let handled = true;
136
+ if (e.key === 'ArrowDown') focusSpan(rows[index + 1]?.span.id);
137
+ else if (e.key === 'ArrowUp') focusSpan(rows[index - 1]?.span.id);
138
+ else if (e.key === 'Home') focusSpan(rows[0]?.span.id);
139
+ else if (e.key === 'End') focusSpan(rows[rows.length - 1]?.span.id);
140
+ else if (e.key === 'ArrowRight' && row.hasChildren) {
141
+ if (open) focusSpan(rows[index + 1]?.span.id);
142
+ else setOpen(row.span.id, true);
143
+ } else if (e.key === 'ArrowLeft') {
144
+ if (row.hasChildren && open) setOpen(row.span.id, false);
145
+ else focusSpan(row.span.parentId);
146
+ } else handled = false;
147
+ if (handled) e.preventDefault();
148
+ };
149
+
150
+ return (
151
+ <section
152
+ aria-labelledby={titleId}
153
+ className={cn(
154
+ 'grid overflow-hidden rounded-lg border border-border bg-card shadow-sm',
155
+ showDetails && 'lg:grid-cols-[minmax(0,1fr)_288px]',
156
+ className,
157
+ )}
158
+ >
159
+ <div className="min-w-0">
160
+ <header className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border bg-secondary/35 px-3 py-2.5">
161
+ <TitleTag id={titleId} className="text-[13px] font-semibold text-foreground">
162
+ {title}
163
+ </TitleTag>
164
+ <span className="font-mono text-[11px] text-muted-foreground">
165
+ {spans.length} spans · {formatSpanDuration(total)}
166
+ </span>
167
+ {running > 0 && (
168
+ <Badge tone="info" variant="soft" size="sm">
169
+ {running} running
170
+ </Badge>
171
+ )}
172
+ {errors > 0 && (
173
+ <Badge tone="destructive" variant="soft" size="sm">
174
+ {errors} {errors === 1 ? 'error' : 'errors'}
175
+ </Badge>
176
+ )}
177
+ </header>
178
+ <div
179
+ aria-hidden
180
+ className="grid grid-cols-[minmax(0,2fr)_minmax(0,3fr)] border-b border-border px-3 py-1.5 font-mono text-[10.5px] text-muted-foreground"
181
+ >
182
+ <span>Span</span>
183
+ <span className="relative h-3.5">
184
+ {[0, 0.25, 0.5, 0.75, 1].map((f) => (
185
+ <span
186
+ key={f}
187
+ className={cn(
188
+ 'absolute top-0',
189
+ f === 1 ? '-translate-x-full rtl:translate-x-full' : f > 0 && '-translate-x-1/2 rtl:translate-x-1/2',
190
+ )}
191
+ style={{ insetInlineStart: `${f * 100}%` }}
192
+ >
193
+ {formatSpanDuration(total * f)}
194
+ </span>
195
+ ))}
196
+ </span>
197
+ </div>
198
+ <ol aria-label="Spans" className="py-1">
199
+ {rows.map(({ span, depth, hasChildren }, index) => {
200
+ const open = !collapsed.has(span.id);
201
+ const isSelected = span.id === selectedId;
202
+ const isRunning = span.status === 'running';
203
+ const indent = 12 + depth * 16;
204
+ const bar = span.status === 'error' ? 'bg-destructive' : KIND_BAR[span.kind];
205
+ const label = `${formatSpanDuration(span.duration)}${isRunning ? ' · running' : ''}`;
206
+ return (
207
+ <li key={span.id} className="relative">
208
+ {hasChildren && (
209
+ <button
210
+ type="button"
211
+ tabIndex={-1}
212
+ aria-expanded={open}
213
+ aria-label={t(open ? 'common.collapseItem' : 'common.expandItem', { label: span.name })}
214
+ onClick={() => setOpen(span.id, !open)}
215
+ className="absolute top-1/2 z-10 grid size-6 -translate-y-1/2 place-items-center rounded text-muted-foreground hover:bg-secondary hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"
216
+ style={{ insetInlineStart: indent - 4 }}
217
+ >
218
+ <ChevronRight
219
+ size={13}
220
+ aria-hidden
221
+ className={cn(
222
+ 'transition-transform motion-reduce:transition-none',
223
+ open ? 'rotate-90' : 'rtl:-scale-x-100',
224
+ )}
225
+ />
226
+ </button>
227
+ )}
228
+ <button
229
+ ref={(el) => {
230
+ if (el) buttons.current.set(span.id, el);
231
+ else buttons.current.delete(span.id);
232
+ }}
233
+ type="button"
234
+ tabIndex={span.id === tabStop ? 0 : -1}
235
+ aria-pressed={isSelected}
236
+ aria-label={`${span.name}, ${formatSpanDuration(span.duration)}${statusSuffix(span)}`}
237
+ data-status={span.status}
238
+ onClick={() => select(span)}
239
+ onFocus={() => setFocusId(span.id)}
240
+ onKeyDown={(e) => onKeyDown(e, index)}
241
+ className={cn(
242
+ 'grid h-8 w-full grid-cols-[minmax(0,2fr)_minmax(0,3fr)] items-center pe-3 text-start hover:bg-secondary/50',
243
+ 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring',
244
+ isSelected && 'bg-primary/8 hover:bg-primary/12',
245
+ )}
246
+ >
247
+ <span className="flex min-w-0 items-center gap-1.5 pe-3" style={{ paddingInlineStart: indent + 22 }}>
248
+ <span
249
+ aria-hidden
250
+ className={cn(
251
+ 'size-2 shrink-0 rounded-full',
252
+ isRunning ? 'border-[1.5px] border-foreground bg-transparent' : bar,
253
+ )}
254
+ />
255
+ <span
256
+ className={cn(
257
+ 'truncate text-[12.5px]',
258
+ span.status === 'error' ? 'text-destructive-text' : 'text-foreground',
259
+ )}
260
+ >
261
+ {span.name}
262
+ </span>
263
+ </span>
264
+ <span className="relative h-full">
265
+ <span
266
+ aria-hidden
267
+ data-span-bar=""
268
+ className={cn(
269
+ 'absolute top-1/2 h-2.5 min-w-0.5 -translate-y-1/2',
270
+ bar,
271
+ // In progress: an open (square, outlined) end and a soft pulse that stops under reduced motion.
272
+ isRunning
273
+ ? 'animate-pulse rounded-s-sm border-e-2 border-foreground opacity-70 motion-reduce:animate-none'
274
+ : 'rounded-sm',
275
+ )}
276
+ style={{
277
+ insetInlineStart: `${(span.start / total) * 100}%`,
278
+ width: `${(span.duration / total) * 100}%`,
279
+ }}
280
+ />
281
+ <span
282
+ aria-hidden
283
+ className="absolute top-1/2 -translate-y-1/2 ps-1.5 font-mono text-[10.5px] whitespace-nowrap text-muted-foreground"
284
+ style={
285
+ span.start + span.duration > total * 0.8
286
+ ? { insetInlineEnd: `${(1 - span.start / total) * 100}%`, paddingInlineEnd: 6 }
287
+ : { insetInlineStart: `${((span.start + span.duration) / total) * 100}%` }
288
+ }
289
+ >
290
+ {label}
291
+ </span>
292
+ </span>
293
+ </button>
294
+ </li>
295
+ );
296
+ })}
297
+ </ol>
298
+ </div>
299
+ {showDetails && (
300
+ <aside
301
+ aria-label="Span details"
302
+ className="border-t border-border bg-secondary/20 p-4 lg:border-t-0 lg:border-s"
303
+ >
304
+ {selected ? (
305
+ renderDetails ? (
306
+ renderDetails(selected)
307
+ ) : (
308
+ <SpanDetails span={selected} headingAs={SubTag} />
309
+ )
310
+ ) : (
311
+ <p className="text-[12.5px] text-muted-foreground">Select a span to see its details.</p>
312
+ )}
313
+ </aside>
314
+ )}
315
+ </section>
316
+ );
317
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Trace waterfall',
5
+ family: 'ai',
6
+ status: 'beta',
7
+ description:
8
+ 'Span tree of a trace with indentation and duration bars positioned on a shared time axis; spans expand and collapse; the list is one tab stop with arrow-key navigation (←/→ collapse and expand), selection is controlled or uncontrolled, the details pane is optional or custom, and running spans show an in-progress bar (pulse stops under reduced motion) with a visible "running" label.',
9
+ uses: ['Badge', 'StatusTag'],
10
+ };
@@ -0,0 +1,6 @@
1
+ import { TraceWaterfall } from '../TraceWaterfall';
2
+
3
+ /** Waterfall only: the page shows the selected span elsewhere (e.g. a drawer). */
4
+ export default function TraceWaterfallWithoutDetails() {
5
+ return <TraceWaterfall showDetails={false} onSelect={() => {}} />;
6
+ }
@@ -0,0 +1,16 @@
1
+ import { useState } from 'react';
2
+ import { TraceWaterfall } from '../TraceWaterfall';
3
+ import { runningTraceSpans } from '../fixtures';
4
+
5
+ /** A trace still in progress, with controlled selection driving the details pane. */
6
+ export default function RunningTraceWaterfall() {
7
+ const [selected, setSelected] = useState<string | null>('r3');
8
+ return (
9
+ <TraceWaterfall
10
+ title="Live trace"
11
+ spans={runningTraceSpans}
12
+ selectedId={selected}
13
+ onSelectedIdChange={setSelected}
14
+ />
15
+ );
16
+ }