@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,64 @@
1
+ import {
2
+ BarChart3,
3
+ CreditCard,
4
+ FileText,
5
+ FolderKanban,
6
+ FolderPlus,
7
+ Home,
8
+ Rocket,
9
+ Settings,
10
+ UserPlus,
11
+ Users,
12
+ } from '@gntik-ai/icons';
13
+ import type { CommandItem } from '@gntik-ai/ui';
14
+
15
+ export const globalSearchPages: CommandItem[] = [
16
+ { id: 'page-overview', label: 'Overview', icon: Home, shortcut: ['G', 'O'] },
17
+ { id: 'page-projects', label: 'Projects', icon: FolderKanban, shortcut: ['G', 'P'] },
18
+ { id: 'page-deployments', label: 'Deployments', icon: Rocket },
19
+ { id: 'page-analytics', label: 'Analytics', icon: BarChart3 },
20
+ { id: 'page-members', label: 'Members', icon: Users, keywords: ['team', 'people'] },
21
+ { id: 'page-billing', label: 'Billing', icon: CreditCard, keywords: ['invoices', 'plan'] },
22
+ { id: 'page-settings', label: 'Settings', icon: Settings, shortcut: ['⌘', ','] },
23
+ ];
24
+
25
+ export const globalSearchRecords: CommandItem[] = [
26
+ {
27
+ id: 'record-web-frontend',
28
+ label: 'web-frontend',
29
+ icon: FolderKanban,
30
+ mono: true,
31
+ description: 'Project · eu-west',
32
+ },
33
+ { id: 'record-billing-api', label: 'billing-api', icon: FolderKanban, mono: true, description: 'Project · us-east' },
34
+ {
35
+ id: 'record-inv-2041',
36
+ label: 'INV-2041',
37
+ icon: FileText,
38
+ mono: true,
39
+ description: 'Invoice · Orbit Freight · overdue',
40
+ },
41
+ {
42
+ id: 'record-dana',
43
+ label: 'Dana Whitfield',
44
+ icon: Users,
45
+ description: 'Member · Owner',
46
+ keywords: ['dana@example.com'],
47
+ },
48
+ ];
49
+
50
+ export const globalSearchActions: CommandItem[] = [
51
+ { id: 'action-new-project', label: 'Create project', icon: FolderPlus, shortcut: ['⌘', 'N'], keywords: ['new'] },
52
+ {
53
+ id: 'action-deploy',
54
+ label: 'Deploy to production',
55
+ icon: Rocket,
56
+ description: 'Promote the latest build of the current project',
57
+ },
58
+ { id: 'action-invite', label: 'Invite member', icon: UserPlus, keywords: ['team', 'people'] },
59
+ ];
60
+
61
+ export const globalSearchRecent: CommandItem[] = [
62
+ { id: 'recent-web-frontend', label: 'web-frontend', icon: FolderKanban, mono: true, description: 'Project' },
63
+ { id: 'recent-billing', label: 'Billing', icon: CreditCard, description: 'Page' },
64
+ ];
@@ -0,0 +1,11 @@
1
+ export * from './AppSidebar/AppSidebar';
2
+ export * from './AppSidebar/fixtures';
3
+ export { meta as appSidebarMeta } from './AppSidebar/block.meta';
4
+ export * from './AppTopbar/AppTopbar';
5
+ export * from './AppTopbar/fixtures';
6
+ export { meta as appTopbarMeta } from './AppTopbar/block.meta';
7
+ export * from './EnvironmentBadge/EnvironmentBadge';
8
+ export { meta as environmentBadgeMeta } from './EnvironmentBadge/block.meta';
9
+ export * from './GlobalSearch/GlobalSearch';
10
+ export * from './GlobalSearch/fixtures';
11
+ export { meta as globalSearchMeta } from './GlobalSearch/block.meta';
package/src/styles.css ADDED
@@ -0,0 +1,2 @@
1
+ /* @gntik-ai/blocks — lets Tailwind v4 see this package's classes. Import after "@gntik-ai/ui/styles.css". */
2
+ @source "../dist";
@@ -0,0 +1,249 @@
1
+ import { render, screen, waitFor, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { DataTable } from './DataTable';
5
+ import type { DataTableColumn } from './data-table-utils';
6
+ import EditableDataTable from './examples/editable';
7
+ import SavedViewsDataTable from './examples/saved-views';
8
+ import { DEPLOYMENT_ROWS, type DeploymentRow } from './fixtures';
9
+ import { newViewId, sameViewState, type DataTableView } from './views';
10
+
11
+ const first = DEPLOYMENT_ROWS[0]!;
12
+ const second = DEPLOYMENT_ROWS[1]!;
13
+ const editColumns: Array<DataTableColumn<DeploymentRow>> = [
14
+ { id: 'name', header: 'Deployment', accessor: (r) => r.name, editable: true },
15
+ { id: 'region', header: 'Region', accessor: (r) => r.region },
16
+ {
17
+ id: 'cost',
18
+ header: 'Cost',
19
+ accessor: (r) => r.cost,
20
+ editable: (r) => r.status !== 'failed',
21
+ editor: { type: 'number', min: 0, max: 1000 },
22
+ },
23
+ ];
24
+ const rows = DEPLOYMENT_ROWS.slice(0, 3);
25
+ const cell = (text: string) => screen.getByRole('button', { name: text });
26
+
27
+ describe('DataTable inline editing', () => {
28
+ it('edits with Enter, commits with Enter and returns focus', async () => {
29
+ const user = userEvent.setup();
30
+ const onCellEdit = vi.fn();
31
+ render(<DataTable rows={rows} columns={editColumns} paginated={false} onCellEdit={onCellEdit} />);
32
+ const trigger = cell(first.name);
33
+ expect(trigger).toHaveAccessibleDescription('Press Enter or F2 to edit.');
34
+ trigger.focus();
35
+ await user.keyboard('{Enter}');
36
+ const input = screen.getByRole('textbox', { name: `Deployment for ${first.name}` });
37
+ expect(input).toHaveFocus();
38
+ await user.clear(input);
39
+ await user.type(input, 'renamed{Enter}');
40
+ expect(onCellEdit).toHaveBeenCalledWith({
41
+ row: first,
42
+ rowId: first.id,
43
+ columnId: 'name',
44
+ value: 'renamed',
45
+ previous: first.name,
46
+ });
47
+ await waitFor(() => expect(cell(first.name)).toHaveFocus());
48
+ });
49
+
50
+ it('starts with F2 and cancels with Escape', async () => {
51
+ const user = userEvent.setup();
52
+ const onCellEdit = vi.fn();
53
+ render(<DataTable rows={rows} columns={editColumns} paginated={false} onCellEdit={onCellEdit} />);
54
+ cell(first.name).focus();
55
+ await user.keyboard('{F2}');
56
+ await user.keyboard('xyz{Escape}');
57
+ expect(onCellEdit).not.toHaveBeenCalled();
58
+ expect(screen.queryByRole('textbox')).toBeNull();
59
+ expect(cell(first.name)).toHaveFocus();
60
+ });
61
+
62
+ it('commits with Tab and moves to the next editable cell', async () => {
63
+ const user = userEvent.setup();
64
+ const onCellEdit = vi.fn();
65
+ render(<DataTable rows={rows} columns={editColumns} paginated={false} onCellEdit={onCellEdit} />);
66
+ cell(first.name).focus();
67
+ await user.keyboard('{Enter}{End}-v2{Tab}');
68
+ expect(onCellEdit).toHaveBeenLastCalledWith(
69
+ expect.objectContaining({ columnId: 'name', value: `${first.name}-v2` }),
70
+ );
71
+ await waitFor(() => expect(cell(String(first.cost))).toHaveFocus());
72
+ });
73
+
74
+ it('shows a rejection inline and keeps editing', async () => {
75
+ const user = userEvent.setup();
76
+ const onCellEdit = vi.fn(async () => 'That name is taken.');
77
+ const { container } = render(
78
+ <DataTable rows={rows} columns={editColumns} paginated={false} onCellEdit={onCellEdit} />,
79
+ );
80
+ cell(first.name).focus();
81
+ await user.keyboard(`{Enter}{Control>}a{/Control}${second.name}{Enter}`);
82
+ const alert = await screen.findByRole('alert');
83
+ expect(alert).toHaveTextContent('That name is taken.');
84
+ const input = screen.getByRole('textbox', { name: `Deployment for ${first.name}` });
85
+ expect(input).toHaveAttribute('aria-invalid', 'true');
86
+ expect(input).toHaveAccessibleDescription('That name is taken.');
87
+ expect(input).toHaveFocus();
88
+ await expectNoAxeViolations(container);
89
+ });
90
+
91
+ it('reports thrown errors and validates numbers before calling onCellEdit', async () => {
92
+ const user = userEvent.setup();
93
+ const onCellEdit = vi.fn(() => {
94
+ throw new Error('Server unavailable');
95
+ });
96
+ render(<DataTable rows={rows} columns={editColumns} paginated={false} onCellEdit={onCellEdit} />);
97
+ cell(String(first.cost)).focus();
98
+ await user.keyboard('{Enter}');
99
+ const input = screen.getByRole('spinbutton', { name: `Cost for ${first.name}` });
100
+ await user.clear(input);
101
+ await user.type(input, '5000{Enter}');
102
+ expect(screen.getByRole('alert')).toHaveTextContent('Enter 1000 or less.');
103
+ expect(onCellEdit).not.toHaveBeenCalled();
104
+ await user.clear(input);
105
+ await user.type(input, '12{Enter}');
106
+ expect(onCellEdit).toHaveBeenLastCalledWith(expect.objectContaining({ value: 12 }));
107
+ expect(await screen.findByRole('alert')).toHaveTextContent('Server unavailable');
108
+ });
109
+
110
+ it('edits with the select editor of the example and shows server rejections', async () => {
111
+ const user = userEvent.setup();
112
+ render(<EditableDataTable />);
113
+ const statusTrigger = within(screen.getAllByRole('row')[1]!).getAllByRole('button')[1]!;
114
+ statusTrigger.focus();
115
+ await user.keyboard('{Enter}');
116
+ const select = screen.getByRole('combobox', { name: `Status for ${first.name}` });
117
+ await user.selectOptions(select, 'paused');
118
+ await user.keyboard('{Enter}');
119
+ await waitFor(() => expect(within(screen.getAllByRole('row')[1]!).getByText('Paused')).toBeInTheDocument());
120
+ // Example: budgets above $1,000 are rejected by the simulated server.
121
+ const budget = within(screen.getAllByRole('row')[1]!).getAllByRole('button')[2]!;
122
+ budget.focus();
123
+ await user.keyboard('{Enter}{Control>}a{/Control}1500{Enter}');
124
+ expect(await screen.findByRole('alert')).toHaveTextContent('Budgets above $1,000 need approval.');
125
+ });
126
+
127
+ it('per-row editable functions leave other cells read-only', () => {
128
+ const failing = { ...first, id: 'x', status: 'failed' as const, cost: 77 };
129
+ render(<DataTable rows={[failing]} columns={editColumns} paginated={false} onCellEdit={() => undefined} />);
130
+ expect(screen.queryByRole('button', { name: '77' })).toBeNull();
131
+ expect(screen.getByText('77').closest('td')).toBeInTheDocument();
132
+ });
133
+ });
134
+
135
+ describe('DataTable saved views', () => {
136
+ const views: DataTableView[] = [
137
+ {
138
+ id: 'expensive',
139
+ name: 'Most expensive',
140
+ state: { sort: { columnId: 'cost', direction: 'descending' }, hiddenColumns: ['region', 'updated'] },
141
+ },
142
+ {
143
+ id: 'grouped',
144
+ name: 'By region',
145
+ state: { groupBy: 'region', pinnedColumns: { left: ['name'] }, filters: { q: 'api' } },
146
+ },
147
+ ];
148
+
149
+ it('switches views from the menu', async () => {
150
+ const user = userEvent.setup();
151
+ const onActiveViewChange = vi.fn();
152
+ const onFiltersChange = vi.fn();
153
+ const onGroupByChange = vi.fn();
154
+ const { container } = render(
155
+ <DataTable
156
+ defaultViews={views}
157
+ onActiveViewChange={onActiveViewChange}
158
+ onFiltersChange={onFiltersChange}
159
+ onGroupByChange={onGroupByChange}
160
+ paginated={false}
161
+ />,
162
+ );
163
+ await user.click(screen.getByRole('button', { name: 'Default view' }));
164
+ await user.click(await screen.findByRole('menuitemradio', { name: 'Most expensive' }));
165
+ expect(onActiveViewChange).toHaveBeenLastCalledWith('expensive');
166
+ expect(screen.getByRole('columnheader', { name: /Cost/ })).toHaveAttribute('aria-sort', 'descending');
167
+ expect(screen.queryByRole('columnheader', { name: /Region/ })).toBeNull();
168
+ await user.click(screen.getByRole('button', { name: 'Most expensive' }));
169
+ await user.click(await screen.findByRole('menuitemradio', { name: 'By region' }));
170
+ expect(onFiltersChange).toHaveBeenLastCalledWith({ q: 'api' });
171
+ expect(onGroupByChange).toHaveBeenLastCalledWith('region');
172
+ expect(screen.getAllByRole('rowheader')[0]).toHaveAttribute('scope', 'rowgroup');
173
+ expect(screen.getByRole('columnheader', { name: /Region/ })).toBeInTheDocument();
174
+ expect(screen.getByRole('columnheader', { name: /Deployment/ })).toHaveClass('sticky');
175
+ await expectNoAxeViolations(container);
176
+ });
177
+
178
+ it('marks changes, resets and saves them to the active view', async () => {
179
+ const user = userEvent.setup();
180
+ const onViewsChange = vi.fn();
181
+ render(<DataTable defaultViews={views} defaultActiveViewId="expensive" onViewsChange={onViewsChange} />);
182
+ // Seeded from the active view.
183
+ expect(screen.getByRole('columnheader', { name: /Cost/ })).toHaveAttribute('aria-sort', 'descending');
184
+ await user.click(screen.getByRole('button', { name: /Status/ }));
185
+ const trigger = screen.getByRole('button', { name: 'Most expensive (modified)' });
186
+ await user.click(trigger);
187
+ await user.click(await screen.findByRole('menuitem', { name: 'Reset changes' }));
188
+ expect(screen.getByRole('columnheader', { name: /Cost/ })).toHaveAttribute('aria-sort', 'descending');
189
+ expect(screen.getByRole('button', { name: 'Most expensive' })).toBeInTheDocument();
190
+ await user.click(screen.getByRole('button', { name: /Status/ }));
191
+ await user.click(screen.getByRole('button', { name: 'Most expensive (modified)' }));
192
+ await user.click(await screen.findByRole('menuitem', { name: 'Save changes' }));
193
+ expect(onViewsChange.mock.lastCall?.[0][0].state.sort).toEqual({ columnId: 'status', direction: 'ascending' });
194
+ expect(screen.getByRole('button', { name: 'Most expensive' })).toBeInTheDocument();
195
+ });
196
+
197
+ it('saves as a new view, renames and deletes it', async () => {
198
+ const user = userEvent.setup();
199
+ const onViewsChange = vi.fn();
200
+ render(<DataTable defaultViews={views} onViewsChange={onViewsChange} resizable />);
201
+ screen.getByRole('separator', { name: 'Resize Region column' }).focus();
202
+ await user.keyboard('{ArrowRight}');
203
+ await user.click(screen.getByRole('button', { name: 'Default view (modified)' }));
204
+ await user.click(await screen.findByRole('menuitem', { name: 'Save as new view…' }));
205
+ const dialog = await screen.findByRole('dialog', { name: 'Save view' });
206
+ await user.type(within(dialog).getByRole('textbox', { name: 'View name' }), 'Wide region{Enter}');
207
+ const saved = onViewsChange.mock.lastCall?.[0] as DataTableView[];
208
+ expect(saved).toHaveLength(3);
209
+ expect(saved[2]).toMatchObject({
210
+ id: 'wide-region',
211
+ name: 'Wide region',
212
+ state: { columnWidths: { region: 170 } },
213
+ });
214
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
215
+ await user.click(screen.getByRole('button', { name: 'Wide region' }));
216
+ await user.click(await screen.findByRole('menuitem', { name: 'Rename view…' }));
217
+ const rename = await screen.findByRole('dialog', { name: 'Rename view' });
218
+ const field = within(rename).getByRole('textbox', { name: 'View name' });
219
+ expect(field).toHaveValue('Wide region');
220
+ await user.clear(field);
221
+ await user.type(field, 'Regions{Enter}');
222
+ expect((onViewsChange.mock.lastCall?.[0] as DataTableView[])[2]?.name).toBe('Regions');
223
+ await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
224
+ await user.click(screen.getByRole('button', { name: 'Regions' }));
225
+ await user.click(await screen.findByRole('menuitem', { name: 'Delete view' }));
226
+ expect(onViewsChange.mock.lastCall?.[0]).toHaveLength(2);
227
+ expect(screen.getByRole('button', { name: /^Default view/ })).toBeInTheDocument();
228
+ });
229
+
230
+ it('the example restores filters with a view', async () => {
231
+ const user = userEvent.setup();
232
+ render(<SavedViewsDataTable />);
233
+ await user.click(screen.getByRole('button', { name: 'Default view' }));
234
+ await user.click(await screen.findByRole('menuitemradio', { name: 'Needs attention' }));
235
+ const failedCount = DEPLOYMENT_ROWS.filter((r) => r.status === 'failed').length;
236
+ await waitFor(() =>
237
+ expect(within(screen.getAllByRole('rowgroup')[1]!).getAllByRole('row')).toHaveLength(failedCount),
238
+ );
239
+ await user.click(screen.getByRole('button', { name: 'Needs attention' }));
240
+ await user.click(await screen.findByRole('menuitemradio', { name: 'Default view' }));
241
+ await waitFor(() => expect(within(screen.getAllByRole('rowgroup')[1]!).getAllByRole('row')).toHaveLength(10));
242
+ });
243
+
244
+ it('view helpers', () => {
245
+ expect(sameViewState({ hiddenColumns: ['b', 'a'] }, { hiddenColumns: ['a', 'b'], sort: null })).toBe(true);
246
+ expect(sameViewState({ columnWidths: { a: 1 } }, {})).toBe(false);
247
+ expect(newViewId('My view!', [{ id: 'my-view', name: 'x', state: {} }])).toBe('my-view-2');
248
+ });
249
+ });
@@ -0,0 +1,188 @@
1
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { DataTable } from './DataTable';
5
+ import { orderColumns, setColumnPin, tableLayout } from './column-sizing';
6
+ import ResizablePinnedDataTable from './examples/resizable-pinned';
7
+ import VirtualizedDataTable from './examples/virtualized';
8
+ import { DEPLOYMENT_COLUMNS, generateDeployments } from './fixtures';
9
+ import { windowFor } from './use-virtual-rows';
10
+
11
+ const head = (name: RegExp) => screen.getByRole('columnheader', { name });
12
+
13
+ describe('DataTable column resize', () => {
14
+ it('exposes a focusable separator per column and resizes from the keyboard', async () => {
15
+ const user = userEvent.setup();
16
+ const onColumnWidthsChange = vi.fn();
17
+ const { container } = render(<DataTable resizable onColumnWidthsChange={onColumnWidthsChange} />);
18
+ const handle = screen.getByRole('separator', { name: 'Resize Region column' });
19
+ expect(handle).toHaveAttribute('aria-valuenow', '160');
20
+ expect(handle).toHaveAttribute('aria-valuemin', '60');
21
+ expect(handle).toHaveAttribute('aria-valuemax', '640');
22
+ expect(handle).toHaveAttribute('aria-orientation', 'vertical');
23
+ handle.focus();
24
+ await user.keyboard('{ArrowRight}');
25
+ expect(handle).toHaveAttribute('aria-valuenow', '170');
26
+ await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
27
+ expect(handle).toHaveAttribute('aria-valuenow', '120');
28
+ await user.keyboard('{Home}');
29
+ expect(handle).toHaveAttribute('aria-valuenow', '60');
30
+ await user.keyboard('{End}');
31
+ expect(onColumnWidthsChange).toHaveBeenLastCalledWith({ region: 640 });
32
+ expect(head(/Region/)).toHaveStyle({ width: '640px' });
33
+ expect(screen.getByRole('table')).toHaveClass('table-fixed');
34
+ // The sort button and the handle are siblings, never nested.
35
+ expect(within(head(/Cost/)).getByRole('button', { name: /Cost/ })).not.toContainElement(
36
+ screen.getByRole('separator', { name: 'Resize Cost column' }),
37
+ );
38
+ await expectNoAxeViolations(container);
39
+ });
40
+
41
+ it('resizes with a pointer drag and respects column limits', () => {
42
+ const columns = DEPLOYMENT_COLUMNS.map((c) => (c.id === 'name' ? { ...c, width: 200, maxWidth: 260 } : c));
43
+ render(<DataTable resizable columns={columns} />);
44
+ const handle = screen.getByRole('separator', { name: 'Resize Deployment column' });
45
+ expect(handle).toHaveAttribute('aria-valuenow', '200');
46
+ fireEvent.pointerDown(handle, { clientX: 100, pointerId: 1 });
47
+ fireEvent.pointerMove(handle, { clientX: 140, pointerId: 1 });
48
+ expect(handle).toHaveAttribute('aria-valuenow', '240');
49
+ fireEvent.pointerMove(handle, { clientX: 400, pointerId: 1 });
50
+ fireEvent.pointerUp(handle, { clientX: 400, pointerId: 1 });
51
+ expect(handle).toHaveAttribute('aria-valuenow', '260');
52
+ });
53
+
54
+ it('follows controlled widths and keeps sortable headers working', async () => {
55
+ const user = userEvent.setup();
56
+ const onSortChange = vi.fn();
57
+ render(<DataTable resizable columnWidths={{ cost: 120 }} onSortChange={onSortChange} />);
58
+ const handle = screen.getByRole('separator', { name: 'Resize Cost column' });
59
+ expect(handle).toHaveAttribute('aria-valuenow', '120');
60
+ handle.focus();
61
+ await user.keyboard('{ArrowRight}');
62
+ expect(handle).toHaveAttribute('aria-valuenow', '120');
63
+ await user.click(screen.getByRole('button', { name: /Cost/ }));
64
+ expect(head(/Cost/)).toHaveAttribute('aria-sort', 'ascending');
65
+ expect(onSortChange).toHaveBeenLastCalledWith({ columnId: 'cost', direction: 'ascending' });
66
+ });
67
+ });
68
+
69
+ describe('DataTable column pinning', () => {
70
+ it('pins columns to both edges with sticky offsets and an edge rule', async () => {
71
+ const { container } = render(<ResizablePinnedDataTable />);
72
+ const headers = screen.getAllByRole('columnheader');
73
+ // Selection, pinned name, the rest, pinned cost, actions.
74
+ expect(headers[1]).toHaveTextContent('Deployment');
75
+ expect(headers[headers.length - 2]).toHaveTextContent('Cost');
76
+ const name = head(/Deployment/);
77
+ expect(name).toHaveClass('sticky', 'border-e');
78
+ expect(name.style.width).toBe('220px');
79
+ const cost = head(/Cost/);
80
+ expect(cost).toHaveClass('sticky', 'border-s');
81
+ // The selection and actions columns stick with them.
82
+ expect(headers[0]).toHaveClass('sticky');
83
+ expect(headers[headers.length - 1]).toHaveClass('sticky');
84
+ const firstRow = within(screen.getAllByRole('rowgroup')[1]!).getAllByRole('row')[0]!;
85
+ expect(within(firstRow).getAllByRole('cell')[1]).toHaveClass('sticky', 'bg-card');
86
+ await expectNoAxeViolations(container);
87
+ });
88
+
89
+ it('pins and unpins from the column menu', async () => {
90
+ // The submenu opens from the keyboard (ArrowRight on its trigger).
91
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
92
+ const onPinnedColumnsChange = vi.fn();
93
+ render(<DataTable onPinnedColumnsChange={onPinnedColumnsChange} />);
94
+ await user.click(screen.getByRole('button', { name: 'Columns' }));
95
+ const sub = await screen.findByRole('menuitem', { name: /^Region/ });
96
+ act(() => sub.focus());
97
+ await user.keyboard('{ArrowRight}');
98
+ await user.click(await screen.findByRole('menuitemradio', { name: 'Pin left' }));
99
+ expect(onPinnedColumnsChange).toHaveBeenLastCalledWith({ left: ['region'], right: [] });
100
+ expect(screen.getAllByRole('columnheader')[1]).toHaveTextContent('Region');
101
+ expect(head(/Region/)).toHaveClass('sticky');
102
+ });
103
+
104
+ it('computes layout helpers', () => {
105
+ expect(setColumnPin({ left: ['a'] }, 'a', 'right')).toEqual({ left: [], right: ['a'] });
106
+ const cols = [
107
+ { id: 'a', header: 'A' },
108
+ { id: 'b', header: 'B', width: 100 },
109
+ { id: 'c', header: 'C' },
110
+ ];
111
+ expect(orderColumns(cols, { left: ['c'], right: ['a'] }).map((c) => c.id)).toEqual(['c', 'b', 'a']);
112
+ const auto = tableLayout({ columns: cols, widths: {}, pinned: {}, sized: false, lead: true, trail: false });
113
+ expect(auto.tableClassName).toBeUndefined();
114
+ const l = tableLayout({
115
+ columns: cols,
116
+ widths: { a: 90 },
117
+ pinned: { left: ['b', 'c'] },
118
+ sized: false,
119
+ lead: true,
120
+ trail: true,
121
+ });
122
+ expect(l.columns.get('b')?.style).toMatchObject({ width: 100, insetInlineStart: 40 });
123
+ expect(l.columns.get('c')?.style).toMatchObject({ insetInlineStart: 140 });
124
+ expect(l.columns.get('c')?.className).toContain('border-e');
125
+ expect(l.tableStyle).toEqual({ width: 90 + 100 + 160 + 40 + 48, minWidth: '100%' });
126
+ });
127
+ });
128
+
129
+ describe('DataTable virtualization', () => {
130
+ it('renders a window of rows with aria-rowcount and aria-rowindex', async () => {
131
+ const { container } = render(<VirtualizedDataTable />);
132
+ const table = screen.getByRole('table');
133
+ expect(table).toHaveAttribute('data-virtualized', 'true');
134
+ expect(table).toHaveAttribute('aria-rowcount', '5001');
135
+ const rendered = container.querySelectorAll('tbody:not([data-virtual-spacer]) tr');
136
+ expect(rendered.length).toBeGreaterThan(5);
137
+ expect(rendered.length).toBeLessThan(60);
138
+ expect(rendered[0]).toHaveAttribute('aria-rowindex', '2');
139
+ expect(screen.getAllByRole('row')[0]).toHaveAttribute('aria-rowindex', '1');
140
+ expect(container.querySelector('[data-virtual-spacer] td')).toHaveStyle({
141
+ height: `${(5000 - rendered.length) * 44}px`,
142
+ });
143
+ await expectNoAxeViolations(container);
144
+ });
145
+
146
+ it('moves the window on scroll', async () => {
147
+ const { container } = render(<DataTable rows={generateDeployments(1000)} paginated={false} />);
148
+ const scroller = screen.getByRole('table').parentElement!;
149
+ act(() => {
150
+ scroller.scrollTop = 44 * 500;
151
+ fireEvent.scroll(scroller);
152
+ });
153
+ await waitFor(() =>
154
+ expect(container.querySelector('tbody:not([data-virtual-spacer]) tr')?.getAttribute('aria-rowindex')).toBe(
155
+ String(500 - 8 + 2),
156
+ ),
157
+ );
158
+ });
159
+
160
+ it('select-all covers every row, not only the rendered ones', async () => {
161
+ const user = userEvent.setup();
162
+ const onSelectionChange = vi.fn();
163
+ render(<DataTable rows={generateDeployments(400)} paginated={false} onSelectionChange={onSelectionChange} />);
164
+ await user.click(screen.getByRole('checkbox', { name: 'Select all rows on this page' }));
165
+ expect(onSelectionChange.mock.lastCall?.[0]).toHaveLength(400);
166
+ });
167
+
168
+ it('virtualizes grouped rows, keeping a rowgroup per group', () => {
169
+ render(<DataTable rows={generateDeployments(600)} groupBy="region" paginated={false} virtualize />);
170
+ const groups = screen.getAllByRole('rowgroup').filter((g) => g.hasAttribute('data-group'));
171
+ expect(groups.length).toBeGreaterThan(0);
172
+ const header = screen.getAllByRole('rowheader')[0]!;
173
+ expect(header).toHaveAttribute('scope', 'rowgroup');
174
+ expect(header.closest('tr')).toHaveAttribute('aria-rowindex', '2');
175
+ });
176
+
177
+ it('stays off below the threshold and when disabled', () => {
178
+ const { rerender } = render(<DataTable rows={generateDeployments(50)} paginated={false} />);
179
+ expect(screen.getByRole('table')).not.toHaveAttribute('aria-rowcount');
180
+ rerender(<DataTable rows={generateDeployments(500)} paginated={false} virtualize={false} />);
181
+ expect(within(screen.getAllByRole('rowgroup')[1]!).getAllByRole('row')).toHaveLength(500);
182
+ });
183
+
184
+ it('windowFor clamps to the item count', () => {
185
+ expect(windowFor(100, 0, 440, 44, 5)).toEqual({ start: 0, end: 16, rowHeight: 44 });
186
+ expect(windowFor(100, 44 * 98, 440, 44, 5)).toEqual({ start: 93, end: 100, rowHeight: 44 });
187
+ });
188
+ });