@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,80 @@
1
+ import type { FlowCanvasProps, FlowNode, FlowNodeKind } from '@gntik-ai/flow';
2
+
3
+ /** React Flow edge, as FlowCanvas takes it. */
4
+ export type FlowEdge = NonNullable<FlowCanvasProps['edges']>[number];
5
+
6
+ export interface PaletteItem {
7
+ kind: FlowNodeKind;
8
+ /** Default title of a node created from this item. */
9
+ title: string;
10
+ description: string;
11
+ }
12
+
13
+ export interface ConsoleEntry {
14
+ id: string;
15
+ /** Display time, e.g. "10:42:07". */
16
+ time: string;
17
+ level: 'info' | 'success' | 'warn' | 'error';
18
+ message: string;
19
+ }
20
+
21
+ const CARD = { width: 208, height: 90 } as const;
22
+ const SLIM = { width: 208, height: 56 } as const;
23
+
24
+ /** Neutral starter flow: webhook → validate → route → notify / store. */
25
+ export const builderNodes: FlowNode[] = [
26
+ {
27
+ id: 'webhook',
28
+ type: 'trigger',
29
+ position: { x: 0, y: 112 },
30
+ ...SLIM,
31
+ data: { title: 'Webhook', subtitle: 'POST /v1/orders' },
32
+ },
33
+ {
34
+ id: 'validate',
35
+ type: 'step',
36
+ position: { x: 256, y: 96 },
37
+ ...CARD,
38
+ data: { title: 'Validate payload', subtitle: 'schema check', meta: 'v1.4', tone: 'done' },
39
+ },
40
+ {
41
+ id: 'route',
42
+ type: 'router',
43
+ position: { x: 512, y: 112 },
44
+ ...SLIM,
45
+ data: { title: 'Route by region', subtitle: '2 branches' },
46
+ },
47
+ {
48
+ id: 'notify',
49
+ type: 'output',
50
+ position: { x: 768, y: 16 },
51
+ ...CARD,
52
+ data: { title: 'Notify team', subtitle: 'email digest' },
53
+ },
54
+ {
55
+ id: 'store',
56
+ type: 'output',
57
+ position: { x: 768, y: 192 },
58
+ ...CARD,
59
+ data: { title: 'Store record', subtitle: 'orders table' },
60
+ },
61
+ ];
62
+
63
+ export const builderEdges: FlowEdge[] = [
64
+ { id: 'e-webhook-validate', source: 'webhook', target: 'validate', animated: true },
65
+ { id: 'e-validate-route', source: 'validate', target: 'route' },
66
+ { id: 'e-route-notify', source: 'route', target: 'notify', label: 'eu' },
67
+ { id: 'e-route-store', source: 'route', target: 'store', label: 'other' },
68
+ ];
69
+
70
+ export const builderPalette: PaletteItem[] = [
71
+ { kind: 'trigger', title: 'Trigger', description: 'Starts the flow on an event' },
72
+ { kind: 'step', title: 'Step', description: 'Processes or transforms data' },
73
+ { kind: 'router', title: 'Router', description: 'Branches on a condition' },
74
+ { kind: 'output', title: 'Output', description: 'Sends the final result' },
75
+ ];
76
+
77
+ export const builderConsole: ConsoleEntry[] = [
78
+ { id: 'c1', time: '10:41:58', level: 'info', message: 'Flow loaded · 5 nodes, 4 edges' },
79
+ { id: 'c2', time: '10:42:01', level: 'success', message: 'Validation passed' },
80
+ ];
@@ -0,0 +1,196 @@
1
+ import { useId } from 'react';
2
+ import { MousePointerClick, Plus, Trash2 } from '@gntik-ai/icons';
3
+ import {
4
+ FLOW_TONES,
5
+ KIND_CHIP,
6
+ KindGlyph,
7
+ type FlowNode,
8
+ type FlowNodeData,
9
+ type FlowTone,
10
+ type FlowNodeKind,
11
+ } from '@gntik-ai/flow';
12
+ import { Button, Field, FieldLabel, Input, SimpleSelect, cn } from '@gntik-ai/ui';
13
+ import type { ConsoleEntry, PaletteItem } from './fixtures';
14
+
15
+ /** Heading level of a panel title (one below the builder's title). */
16
+ export type PanelHeading = 'h3' | 'h4' | 'h5';
17
+
18
+ const panelTitle =
19
+ 'px-3 pt-3 pb-2 font-mono text-[10.5px] font-medium tracking-[0.12em] text-muted-foreground uppercase';
20
+
21
+ export interface NodePaletteProps {
22
+ items: PaletteItem[];
23
+ onAdd: (item: PaletteItem) => void;
24
+ /** Heading level of the panel title (default h3). */
25
+ titleAs?: PanelHeading;
26
+ }
27
+
28
+ /** Left rail: node kinds to add to the canvas. */
29
+ export function NodePalette({ items, onAdd, titleAs: TitleTag = 'h3' }: NodePaletteProps) {
30
+ const titleId = useId();
31
+ return (
32
+ <div className="flex h-full min-h-0 flex-col bg-card">
33
+ <TitleTag id={titleId} className={panelTitle}>
34
+ Nodes
35
+ </TitleTag>
36
+ <ul aria-labelledby={titleId} className="flex-1 overflow-y-auto px-2 pb-2">
37
+ {items.map((item) => (
38
+ <li key={item.kind + item.title}>
39
+ <button
40
+ type="button"
41
+ onClick={() => onAdd(item)}
42
+ aria-label={`Add ${item.title}`}
43
+ title={item.description}
44
+ className="group flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-start hover:bg-secondary/70 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"
45
+ >
46
+ <span className={cn('grid size-7 shrink-0 place-items-center rounded-md', KIND_CHIP[item.kind])}>
47
+ <KindGlyph kind={item.kind} size={14} />
48
+ </span>
49
+ <span className="min-w-0 flex-1">
50
+ <span className="block text-[12.5px] font-medium text-foreground">{item.title}</span>
51
+ <span className="block truncate text-[11px] text-muted-foreground">{item.description}</span>
52
+ </span>
53
+ <Plus size={14} aria-hidden className="shrink-0 text-muted-foreground group-hover:text-foreground" />
54
+ </button>
55
+ </li>
56
+ ))}
57
+ </ul>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function inspectorKind(type: string | undefined): FlowNodeKind {
63
+ return type === 'trigger' || type === 'router' || type === 'output' ? type : 'step';
64
+ }
65
+
66
+ const TONE_ITEMS = [
67
+ { value: 'none', label: 'No status' },
68
+ ...(Object.keys(FLOW_TONES) as FlowTone[]).map((t) => ({ value: t, label: FLOW_TONES[t].label })),
69
+ ];
70
+
71
+ export interface NodeInspectorProps<Kind extends string = FlowNodeKind> {
72
+ node: FlowNode<Kind> | undefined;
73
+ onChange: (id: string, patch: Partial<FlowNodeData>) => void;
74
+ onDelete: (id: string) => void;
75
+ /** Heading level of the panel title (default h3). */
76
+ titleAs?: PanelHeading;
77
+ }
78
+
79
+ /** Right rail: the selected node's editable fields. */
80
+ export function NodeInspector<Kind extends string = FlowNodeKind>({
81
+ node,
82
+ onChange,
83
+ onDelete,
84
+ titleAs: TitleTag = 'h3',
85
+ }: NodeInspectorProps<Kind>) {
86
+ const titleId = useId();
87
+ return (
88
+ <div className="flex h-full min-h-0 flex-col bg-card">
89
+ <TitleTag id={titleId} className={panelTitle}>
90
+ Inspector
91
+ </TitleTag>
92
+ {node ? (
93
+ <form
94
+ aria-labelledby={titleId}
95
+ onSubmit={(e) => e.preventDefault()}
96
+ className="flex flex-1 flex-col gap-3 overflow-y-auto px-3 pb-3"
97
+ >
98
+ <p className="flex items-center gap-2 text-[12px] text-muted-foreground">
99
+ <span className={cn('grid size-5 place-items-center rounded', KIND_CHIP[inspectorKind(node.type)])}>
100
+ <KindGlyph kind={inspectorKind(node.type)} size={11} />
101
+ </span>
102
+ <span className="capitalize">{node.type}</span>
103
+ <span className="font-mono text-[11px]">· {node.id}</span>
104
+ </p>
105
+ <Field>
106
+ <FieldLabel>Title</FieldLabel>
107
+ <Input size="sm" value={node.data.title} onValueChange={(v) => onChange(node.id, { title: String(v) })} />
108
+ </Field>
109
+ <Field>
110
+ <FieldLabel>Subtitle</FieldLabel>
111
+ <Input
112
+ size="sm"
113
+ value={node.data.subtitle ?? ''}
114
+ onValueChange={(v) => onChange(node.id, { subtitle: String(v) || undefined })}
115
+ />
116
+ </Field>
117
+ <Field>
118
+ <FieldLabel>Meta</FieldLabel>
119
+ <Input
120
+ size="sm"
121
+ value={node.data.meta ?? ''}
122
+ onValueChange={(v) => onChange(node.id, { meta: String(v) || undefined })}
123
+ />
124
+ </Field>
125
+ <SimpleSelect
126
+ label="Status"
127
+ size="sm"
128
+ items={TONE_ITEMS}
129
+ value={node.data.tone ?? 'none'}
130
+ onValueChange={(v) => onChange(node.id, { tone: v && v !== 'none' ? (v as FlowTone) : undefined })}
131
+ />
132
+ <Button
133
+ variant="ghost"
134
+ size="sm"
135
+ icon={Trash2}
136
+ onClick={() => onDelete(node.id)}
137
+ className="mt-auto self-start"
138
+ >
139
+ Delete node
140
+ </Button>
141
+ </form>
142
+ ) : (
143
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 px-4 pb-6 text-center">
144
+ <MousePointerClick size={18} aria-hidden className="text-muted-foreground" />
145
+ <p className="text-[12.5px] text-muted-foreground">Select a node to edit its fields.</p>
146
+ </div>
147
+ )}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ const CONSOLE_LEVEL: Record<ConsoleEntry['level'], string> = {
153
+ info: 'text-muted-foreground',
154
+ success: 'text-success-text',
155
+ warn: 'text-warning-text',
156
+ error: 'text-destructive-text',
157
+ };
158
+
159
+ export interface RunConsoleProps {
160
+ entries: ConsoleEntry[];
161
+ onClear: () => void;
162
+ /** Heading level of the panel title (default h3). */
163
+ titleAs?: PanelHeading;
164
+ }
165
+
166
+ /** Bottom panel: run output (role="log"). */
167
+ export function RunConsole({ entries, onClear, titleAs: TitleTag = 'h3' }: RunConsoleProps) {
168
+ return (
169
+ <div className="flex h-full min-h-0 flex-col bg-card">
170
+ <div className="flex items-center justify-between border-b border-border pe-2">
171
+ <TitleTag className={cn(panelTitle, 'pb-2')}>Run console</TitleTag>
172
+ <Button variant="ghost" size="sm" onClick={onClear} disabled={entries.length === 0} className="h-7">
173
+ Clear
174
+ </Button>
175
+ </div>
176
+ <div
177
+ role="log"
178
+ aria-label="Run console"
179
+ tabIndex={0}
180
+ className="flex-1 overflow-y-auto px-3 py-2 font-mono text-[11.5px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring"
181
+ >
182
+ {entries.length === 0 ? (
183
+ <p className="font-sans text-[12.5px] text-muted-foreground">No output yet. Run the flow to see its steps.</p>
184
+ ) : (
185
+ entries.map((e) => (
186
+ <p key={e.id} className="flex gap-3 py-0.5">
187
+ <span className="shrink-0 text-muted-foreground tabular-nums">{e.time}</span>
188
+ <span className={cn('w-14 shrink-0 uppercase', CONSOLE_LEVEL[e.level])}>{e.level}</span>
189
+ <span className="min-w-0 text-foreground">{e.message}</span>
190
+ </p>
191
+ ))
192
+ )}
193
+ </div>
194
+ </div>
195
+ );
196
+ }
@@ -0,0 +1,40 @@
1
+ import { TriangleAlert } from '@gntik-ai/icons';
2
+ import type { FlowValidationIssue } from './extensions';
3
+ import type { PanelHeading } from './parts';
4
+
5
+ export function FlowProblems({
6
+ issues,
7
+ onSelect,
8
+ titleAs: Title = 'h3',
9
+ }: {
10
+ issues: readonly FlowValidationIssue[];
11
+ onSelect: (id: string) => void;
12
+ titleAs?: PanelHeading;
13
+ }) {
14
+ if (!issues.length) return null;
15
+ return (
16
+ <div className="max-h-40 shrink-0 overflow-auto border-b border-border px-3 py-2">
17
+ <Title className="text-[12px] font-semibold text-foreground">Problems</Title>
18
+ <ul aria-label="Flow problems" className="text-[12px] text-foreground">
19
+ {issues.map((issue, i) => (
20
+ <li key={`${issue.code}-${issue.nodeId ?? ''}-${i}`} className="flex items-center gap-2">
21
+ <TriangleAlert size={14} aria-hidden className="shrink-0 text-destructive-text" />
22
+ {issue.nodeId ? (
23
+ <button
24
+ type="button"
25
+ onClick={() => onSelect(issue.nodeId!)}
26
+ className="text-start underline focus-visible:outline-2 focus-visible:outline-focus-ring"
27
+ >
28
+ {issue.code}: {issue.message} · {issue.nodeId}
29
+ </button>
30
+ ) : (
31
+ <span>
32
+ {issue.code}: {issue.message} (Flow)
33
+ </span>
34
+ )}
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,97 @@
1
+ import { fireEvent, render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { LogViewer } from './LogViewer';
5
+ import { makeLogLines } from './fixtures';
6
+
7
+ const renderedLines = () => document.querySelectorAll('[data-slot="log-line"]');
8
+
9
+ describe('LogViewer', () => {
10
+ it('follows the bottom, stops after scrolling up more than a row and resumes on demand', async () => {
11
+ const user = userEvent.setup();
12
+ const { rerender } = render(<LogViewer lines={makeLogLines(100)} height={240} rowHeight={24} />);
13
+ const log = screen.getByRole('log');
14
+ expect(log.scrollTop).toBe(2160);
15
+ rerender(<LogViewer lines={makeLogLines(101)} height={240} rowHeight={24} />);
16
+ expect(log.scrollTop).toBe(2184);
17
+ log.scrollTop = 2135;
18
+ fireEvent.scroll(log);
19
+ expect(screen.getByRole('switch', { name: 'Follow' })).not.toBeChecked();
20
+ rerender(<LogViewer lines={makeLogLines(102)} height={240} rowHeight={24} />);
21
+ expect(log.scrollTop).toBe(2135);
22
+ expect(log).toHaveAttribute('aria-live', 'off');
23
+ await user.click(screen.getByRole('switch', { name: 'Follow' }));
24
+ expect(log.scrollTop).toBe(2208);
25
+ expect(log).toHaveAttribute('aria-live', 'polite');
26
+ });
27
+
28
+ it('can suppress event announcements while preserving the default live behavior', () => {
29
+ const { rerender } = render(<LogViewer lines={makeLogLines(2)} />);
30
+ expect(screen.getByRole('log')).toHaveAttribute('aria-live', 'polite');
31
+ expect(screen.queryByRole('button', { name: /^Inspect event/ })).toBeNull();
32
+ rerender(<LogViewer lines={makeLogLines(3)} announceLines={false} />);
33
+ expect(screen.getByRole('log')).toHaveAttribute('aria-live', 'off');
34
+ expect(screen.getByRole('switch', { name: 'Follow' })).toBeChecked();
35
+ });
36
+
37
+ it('offers keyboard and mouse selection independently of copying', async () => {
38
+ const user = userEvent.setup();
39
+ const lines = makeLogLines(2).map((line, index) => ({ ...line, timestamp: `12:00:0${index}.000` }));
40
+ const onSelectLine = vi.fn();
41
+ const onCopyLine = vi.fn();
42
+ render(
43
+ <LogViewer lines={lines} onSelectLine={onSelectLine} selectedLineId={lines[0]!.id} onCopyLine={onCopyLine} />,
44
+ );
45
+ const first = screen.getByRole('button', { name: 'Inspect event 12:00:00.000' });
46
+ expect(first).toHaveAttribute('aria-pressed', 'true');
47
+ const second = screen.getByRole('button', { name: 'Inspect event 12:00:01.000' });
48
+ expect(second).toHaveAttribute('aria-pressed', 'false');
49
+ await user.click(second);
50
+ expect(onSelectLine).toHaveBeenLastCalledWith(lines[1]);
51
+ await user.tab();
52
+ expect(screen.getByRole('button', { name: 'Copy line 12:00:01.000' })).toHaveFocus();
53
+ await user.tab({ shift: true });
54
+ expect(second).toHaveFocus();
55
+ await user.keyboard('{Enter}');
56
+ await user.keyboard(' ');
57
+ expect(onSelectLine).toHaveBeenCalledTimes(3);
58
+ await user.click(screen.getByRole('button', { name: 'Copy line 12:00:00.000' }));
59
+ expect(onCopyLine).toHaveBeenCalledWith(lines[0]);
60
+ expect(onSelectLine).toHaveBeenCalledTimes(3);
61
+ await expectNoAxeViolations();
62
+ });
63
+
64
+ it('renders only a window of rows, pinned to the tail while following', async () => {
65
+ render(<LogViewer />);
66
+ const log = screen.getByRole('log', { name: 'Logs' });
67
+ expect(log).toBeInTheDocument();
68
+ expect(renderedLines().length).toBeLessThan(40);
69
+ expect(within(log).getByText('12:02:57.230')).toBeInTheDocument(); // last line (index 479)
70
+ await expectNoAxeViolations();
71
+ });
72
+
73
+ it('scrolling moves the window and turns follow off', () => {
74
+ render(<LogViewer lines={makeLogLines(1000)} defaultFollow={false} height={240} rowHeight={24} overscan={2} />);
75
+ const log = screen.getByRole('log');
76
+ expect(screen.getByRole('switch', { name: 'Follow' })).not.toBeChecked();
77
+ expect(within(log).getByText('12:00:00.000')).toBeInTheDocument();
78
+ log.scrollTop = 24 * 500;
79
+ fireEvent.scroll(log);
80
+ expect(within(log).queryByText('12:00:00.000')).not.toBeInTheDocument();
81
+ expect(renderedLines()).toHaveLength(14);
82
+ });
83
+
84
+ it('filters by level, highlights search matches and copies a line', async () => {
85
+ const user = userEvent.setup();
86
+ const onCopyLine = vi.fn();
87
+ render(<LogViewer lines={makeLogLines(20)} onCopyLine={onCopyLine} />);
88
+ await user.click(screen.getByRole('button', { name: /^debug/ }));
89
+ expect(screen.getByText('16 of 20')).toBeInTheDocument();
90
+ await user.type(screen.getByRole('searchbox', { name: 'Search logs' }), 'failed');
91
+ expect(screen.getByText('2 of 20')).toBeInTheDocument();
92
+ expect(document.querySelectorAll('mark')).toHaveLength(2);
93
+ await user.click(screen.getAllByRole('button', { name: /^Copy line/ })[0]!);
94
+ expect(onCopyLine).toHaveBeenCalledWith(expect.objectContaining({ level: 'error' }));
95
+ expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument();
96
+ });
97
+ });
@@ -0,0 +1,253 @@
1
+ import { useEffect, useId, useMemo, useRef, useState, type ReactNode, type UIEvent } from 'react';
2
+ import { Check, Copy, Eye, Search } from '@gntik-ai/icons';
3
+ import { IconButton, Input, Switch, Toggle, ToggleGroup, cn } from '@gntik-ai/ui';
4
+ import { logLines as defaultLines, type LogLevel, type LogLine } from './fixtures';
5
+
6
+ export type { LogLevel, LogLine } from './fixtures';
7
+
8
+ export const LOG_LEVELS: LogLevel[] = ['debug', 'info', 'warn', 'error'];
9
+
10
+ const LEVEL_STYLE: Record<LogLevel, { label: string; className: string }> = {
11
+ debug: { label: 'DEBUG', className: 'bg-muted-foreground/16 text-foreground' },
12
+ info: { label: 'INFO', className: 'bg-info/15 text-foreground' },
13
+ warn: { label: 'WARN', className: 'bg-warning/16 text-warning-chip-text' },
14
+ error: { label: 'ERROR', className: 'bg-destructive/15 text-destructive-chip-text' },
15
+ };
16
+
17
+ export interface LogViewerProps {
18
+ /** Log lines, oldest first. */
19
+ lines?: LogLine[];
20
+ /** Accessible name of the log region and visible title. */
21
+ title?: string;
22
+ /** Height of the scrolling viewport in px. */
23
+ height?: number;
24
+ /** Fixed row height in px (the windowing math relies on it). */
25
+ rowHeight?: number;
26
+ /** Rows rendered above and below the visible window. */
27
+ overscan?: number;
28
+ /** Levels shown initially. */
29
+ defaultLevels?: LogLevel[];
30
+ /** Start pinned to the newest line. */
31
+ defaultFollow?: boolean;
32
+ /** Called after a line is copied. */
33
+ onCopyLine?: (line: LogLine) => void;
34
+ /** Enables an inspect button and mouse selection on each visible row. */
35
+ onSelectLine?: (line: LogLine) => void;
36
+ /** Selected inspect button, exposed through aria-pressed. */
37
+ selectedLineId?: string;
38
+ /** Announce appended lines while following (default true). False keeps aria-live off. */
39
+ announceLines?: boolean;
40
+ /** Heading level of the title, to fit the page outline (default h3). */
41
+ titleAs?: 'h2' | 'h3' | 'h4';
42
+ className?: string;
43
+ }
44
+
45
+ /** Wraps case-insensitive matches of `query` in <mark>. */
46
+ function highlight(text: string, query: string): ReactNode {
47
+ if (!query) return text;
48
+ const lower = text.toLowerCase();
49
+ const q = query.toLowerCase();
50
+ const parts: ReactNode[] = [];
51
+ let from = 0;
52
+ let at = lower.indexOf(q, from);
53
+ while (at !== -1) {
54
+ if (at > from) parts.push(text.slice(from, at));
55
+ parts.push(
56
+ <mark key={at} className="rounded-sm bg-warning/30 text-foreground">
57
+ {text.slice(at, at + q.length)}
58
+ </mark>,
59
+ );
60
+ from = at + q.length;
61
+ at = lower.indexOf(q, from);
62
+ }
63
+ if (from < text.length) parts.push(text.slice(from));
64
+ return parts;
65
+ }
66
+
67
+ function lineText(line: LogLine) {
68
+ return `${line.timestamp} ${LEVEL_STYLE[line.level].label} ${line.source ? `[${line.source}] ` : ''}${line.message}`;
69
+ }
70
+
71
+ /**
72
+ * Log stream viewer. Only the visible rows are rendered (fixed row height + scroll offset),
73
+ * so it stays fast with thousands of lines. Level filter, search with highlighted matches,
74
+ * follow-tail and per-line copy.
75
+ */
76
+ export function LogViewer({
77
+ lines = defaultLines,
78
+ title = 'Logs',
79
+ height = 360,
80
+ rowHeight = 24,
81
+ overscan = 8,
82
+ defaultLevels = LOG_LEVELS,
83
+ defaultFollow = true,
84
+ onCopyLine,
85
+ onSelectLine,
86
+ selectedLineId,
87
+ announceLines = true,
88
+ titleAs: TitleTag = 'h3',
89
+ className,
90
+ }: LogViewerProps) {
91
+ const titleId = useId();
92
+ const viewport = useRef<HTMLDivElement>(null);
93
+ const copyTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
94
+ const [levels, setLevels] = useState<string[]>(defaultLevels);
95
+ const [query, setQuery] = useState('');
96
+ const [follow, setFollow] = useState(defaultFollow);
97
+ const [scrollTop, setScrollTop] = useState(0);
98
+ const [copied, setCopied] = useState<string | null>(null);
99
+
100
+ const counts = useMemo(() => {
101
+ const c: Record<LogLevel, number> = { debug: 0, info: 0, warn: 0, error: 0 };
102
+ for (const l of lines) c[l.level] += 1;
103
+ return c;
104
+ }, [lines]);
105
+
106
+ const visible = useMemo(() => {
107
+ const q = query.trim().toLowerCase();
108
+ return lines.filter(
109
+ (l) =>
110
+ levels.includes(l.level) &&
111
+ (!q || l.message.toLowerCase().includes(q) || (l.source ?? '').toLowerCase().includes(q)),
112
+ );
113
+ }, [lines, levels, query]);
114
+
115
+ const total = visible.length * rowHeight;
116
+ const maxScroll = Math.max(0, total - height);
117
+ // While following, the window is pinned to the end (the DOM scroll catches up in the effect).
118
+ const offset = follow ? maxScroll : Math.min(scrollTop, maxScroll);
119
+ const start = Math.max(0, Math.floor(offset / rowHeight) - overscan);
120
+ const end = Math.min(visible.length, Math.ceil((offset + height) / rowHeight) + overscan);
121
+ const rows = visible.slice(start, end);
122
+
123
+ useEffect(() => {
124
+ if (follow && viewport.current) viewport.current.scrollTop = maxScroll;
125
+ }, [follow, maxScroll]);
126
+
127
+ useEffect(() => () => clearTimeout(copyTimer.current), []);
128
+
129
+ const onScroll = (event: UIEvent<HTMLDivElement>) => {
130
+ const top = event.currentTarget.scrollTop;
131
+ setScrollTop(top);
132
+ if (follow && top < maxScroll - rowHeight) setFollow(false);
133
+ };
134
+
135
+ const copy = (line: LogLine) => {
136
+ void navigator.clipboard?.writeText(lineText(line)).catch(() => undefined);
137
+ onCopyLine?.(line);
138
+ setCopied(line.id);
139
+ clearTimeout(copyTimer.current);
140
+ copyTimer.current = setTimeout(() => setCopied(null), 1500);
141
+ };
142
+
143
+ return (
144
+ <section
145
+ aria-labelledby={titleId}
146
+ className={cn('flex flex-col overflow-hidden rounded-lg border border-border bg-card shadow-sm', className)}
147
+ >
148
+ <div className="flex flex-col gap-3 border-b border-border bg-secondary/35 px-3 py-2.5 lg:flex-row lg:items-center">
149
+ <div className="flex items-center gap-2">
150
+ <TitleTag id={titleId} className="text-[13px] font-semibold text-foreground">
151
+ {title}
152
+ </TitleTag>
153
+ <span className="font-mono text-[11px] text-muted-foreground">
154
+ {visible.length} of {lines.length}
155
+ </span>
156
+ </div>
157
+ <div className="flex flex-wrap items-center gap-2 lg:ms-auto">
158
+ <ToggleGroup aria-label="Log levels" size="sm" multiple value={levels} onValueChange={(v) => setLevels(v)}>
159
+ {LOG_LEVELS.map((lv) => (
160
+ <Toggle key={lv} value={lv}>
161
+ {lv}
162
+ <span className="font-mono text-[10.5px] opacity-70">{counts[lv]}</span>
163
+ </Toggle>
164
+ ))}
165
+ </ToggleGroup>
166
+ <Input
167
+ size="sm"
168
+ type="search"
169
+ aria-label="Search logs"
170
+ placeholder="Search…"
171
+ leadingIcon={Search}
172
+ value={query}
173
+ onValueChange={(v) => setQuery(String(v))}
174
+ className="w-full sm:w-48"
175
+ />
176
+ <Switch size="sm" label="Follow" checked={follow} onCheckedChange={setFollow} />
177
+ </div>
178
+ </div>
179
+ <div
180
+ ref={viewport}
181
+ role="log"
182
+ aria-label={title}
183
+ aria-live={announceLines && follow ? 'polite' : 'off'}
184
+ tabIndex={0}
185
+ onScroll={onScroll}
186
+ className="relative overflow-auto bg-background font-mono text-[12px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring"
187
+ style={{ height }}
188
+ >
189
+ {visible.length === 0 ? (
190
+ <p className="px-4 py-6 text-center font-sans text-[13px] text-muted-foreground">
191
+ No lines match the current filters.
192
+ </p>
193
+ ) : (
194
+ <div className="relative w-full" style={{ height: total }}>
195
+ {rows.map((line, i) => {
196
+ const lv = LEVEL_STYLE[line.level];
197
+ return (
198
+ <div
199
+ key={line.id}
200
+ data-slot="log-line"
201
+ onClick={onSelectLine ? () => onSelectLine(line) : undefined}
202
+ className="group absolute inset-x-0 flex items-center gap-2.5 px-3 hover:bg-secondary/50"
203
+ style={{ top: (start + i) * rowHeight, height: rowHeight }}
204
+ >
205
+ <span className="shrink-0 text-muted-foreground tabular-nums">{line.timestamp}</span>
206
+ <span
207
+ className={cn(
208
+ 'inline-flex h-4 w-12 shrink-0 items-center justify-center rounded text-[10px] font-semibold',
209
+ lv.className,
210
+ )}
211
+ >
212
+ {lv.label}
213
+ </span>
214
+ {line.source && (
215
+ <span className="hidden shrink-0 text-muted-foreground sm:inline">
216
+ [{highlight(line.source, query.trim())}]
217
+ </span>
218
+ )}
219
+ <span className="min-w-0 flex-1 truncate text-foreground">
220
+ {highlight(line.message, query.trim())}
221
+ </span>
222
+ {onSelectLine && (
223
+ <IconButton
224
+ size="sm"
225
+ icon={Eye}
226
+ label={`Inspect event ${line.timestamp}`}
227
+ aria-pressed={line.id === selectedLineId}
228
+ onClick={(event) => {
229
+ event.stopPropagation();
230
+ onSelectLine(line);
231
+ }}
232
+ className="size-6 shrink-0"
233
+ />
234
+ )}
235
+ <IconButton
236
+ size="sm"
237
+ icon={copied === line.id ? Check : Copy}
238
+ label={copied === line.id ? 'Copied' : `Copy line ${line.timestamp}`}
239
+ onClick={(event) => {
240
+ if (onSelectLine) event.stopPropagation();
241
+ copy(line);
242
+ }}
243
+ className="size-6 shrink-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
244
+ />
245
+ </div>
246
+ );
247
+ })}
248
+ </div>
249
+ )}
250
+ </div>
251
+ </section>
252
+ );
253
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Log viewer',
5
+ family: 'builders',
6
+ status: 'beta',
7
+ description:
8
+ 'Windowed log stream (role="log"): level filter, search with highlighted matches, follow-tail and per-line copy. Renders only the visible rows.',
9
+ uses: ['ToggleGroup', 'Toggle', 'Input', 'Switch', 'IconButton'],
10
+ };