@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,86 @@
1
+ import { ArrowDown, ArrowUp, ChevronsUpDown } from '@gntik-ai/icons';
2
+ import { cn, nextSortDirection, TableHead, tableVariants, type SortDirection } from '@gntik-ai/ui';
3
+ import { stickyTint, type CellLayout } from './column-sizing';
4
+ import { columnAlign, type DataTableColumn } from './data-table-utils';
5
+ import { ColumnResizeHandle } from './resize-handle';
6
+
7
+ export interface DataTableHeadCellProps<T> {
8
+ column: DataTableColumn<T>;
9
+ sortDirection: SortDirection;
10
+ onSortChange: (direction: Exclude<SortDirection, 'none'>) => void;
11
+ layout?: CellLayout;
12
+ /** Resize handle props; omitted, the column is not resizable. */
13
+ resize?: { width: number; min: number; max: number; onResize: (width: number) => void };
14
+ }
15
+
16
+ /**
17
+ * Column header of the DataTable. Without a resize handle it is the kit TableHead; with one, the
18
+ * sort button and the handle sit side by side (the handle never nests inside the button).
19
+ */
20
+ export function DataTableHeadCell<T>({
21
+ column,
22
+ sortDirection,
23
+ onSortChange,
24
+ layout,
25
+ resize,
26
+ }: DataTableHeadCellProps<T>) {
27
+ const align = columnAlign(column);
28
+ const pinnedClass = layout?.pinned
29
+ ? cn(layout.className, 'z-[11]', stickyTint(layout, { head: true }), 'bg-card')
30
+ : undefined;
31
+ if (!resize) {
32
+ return (
33
+ <TableHead
34
+ align={align}
35
+ className={cn(column.headClassName, pinnedClass)}
36
+ style={layout?.style}
37
+ sortable={column.sortable}
38
+ sortDirection={sortDirection}
39
+ onSortChange={onSortChange}
40
+ data-column-id={column.id}
41
+ >
42
+ {column.header}
43
+ </TableHead>
44
+ );
45
+ }
46
+ const v = tableVariants({ align: align ?? 'left', active: sortDirection !== 'none' });
47
+ const SortIcon =
48
+ sortDirection === 'ascending' ? ArrowUp : sortDirection === 'descending' ? ArrowDown : ChevronsUpDown;
49
+ return (
50
+ <TableHead
51
+ align={align}
52
+ className={cn('relative overflow-visible', column.headClassName, pinnedClass)}
53
+ style={layout?.style}
54
+ aria-sort={column.sortable ? sortDirection : undefined}
55
+ data-column-id={column.id}
56
+ >
57
+ <span
58
+ className={cn(
59
+ 'block min-w-0 truncate pe-2',
60
+ align === 'right' && 'text-end',
61
+ align === 'center' && 'text-center',
62
+ )}
63
+ >
64
+ {column.sortable ? (
65
+ <button
66
+ type="button"
67
+ className={cn(v.sortButton(), 'max-w-full')}
68
+ onClick={() => onSortChange(nextSortDirection(sortDirection))}
69
+ >
70
+ <span className="truncate">{column.header}</span>
71
+ <SortIcon size={13} aria-hidden className={sortDirection === 'none' ? 'opacity-40' : undefined} />
72
+ </button>
73
+ ) : (
74
+ column.header
75
+ )}
76
+ </span>
77
+ <ColumnResizeHandle
78
+ label={column.header}
79
+ width={resize.width}
80
+ min={resize.min}
81
+ max={resize.max}
82
+ onResize={resize.onResize}
83
+ />
84
+ </TableHead>
85
+ );
86
+ }
@@ -0,0 +1,88 @@
1
+ import { cn } from '@gntik-ai/ui';
2
+ import { useRef, type KeyboardEvent, type PointerEvent } from 'react';
3
+
4
+ export interface ColumnResizeHandleProps {
5
+ /** Column name for the accessible label ("Resize Region column"). */
6
+ label: string;
7
+ width: number;
8
+ min: number;
9
+ max: number;
10
+ onResize: (width: number) => void;
11
+ /** Called once a drag or a key press ends a resize. */
12
+ onResizeEnd?: (width: number) => void;
13
+ }
14
+
15
+ const STEP = 10;
16
+ const BIG_STEP = 50;
17
+
18
+ /**
19
+ * Column resize handle at the end edge of a header cell: a focusable `role="separator"` with
20
+ * aria-valuenow/min/max (the width in px). Drag it with a pointer; from the keyboard,
21
+ * ArrowLeft/ArrowRight change the width by 10px (Shift: 50px), Home/End jump to the limits.
22
+ */
23
+ export function ColumnResizeHandle({ label, width, min, max, onResize, onResizeEnd }: ColumnResizeHandleProps) {
24
+ const drag = useRef<{ x: number; width: number; dir: 1 | -1 } | null>(null);
25
+ const clamp = (w: number) => Math.round(Math.min(max, Math.max(min, w)));
26
+
27
+ const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
28
+ e.preventDefault();
29
+ e.stopPropagation();
30
+ e.currentTarget.setPointerCapture?.(e.pointerId);
31
+ const rtl = typeof getComputedStyle === 'function' && getComputedStyle(e.currentTarget).direction === 'rtl';
32
+ drag.current = { x: e.clientX, width, dir: rtl ? -1 : 1 };
33
+ };
34
+ const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {
35
+ const d = drag.current;
36
+ if (!d) return;
37
+ onResize(clamp(d.width + (e.clientX - d.x) * d.dir));
38
+ };
39
+ const onPointerUp = (e: PointerEvent<HTMLDivElement>) => {
40
+ const d = drag.current;
41
+ drag.current = null;
42
+ if (d) onResizeEnd?.(clamp(d.width + (e.clientX - d.x) * d.dir));
43
+ };
44
+ const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
45
+ const step = e.shiftKey ? BIG_STEP : STEP;
46
+ const rtl = typeof getComputedStyle === 'function' && getComputedStyle(e.currentTarget).direction === 'rtl';
47
+ const grow = rtl ? 'ArrowLeft' : 'ArrowRight';
48
+ const shrink = rtl ? 'ArrowRight' : 'ArrowLeft';
49
+ let next: number | null = null;
50
+ if (e.key === grow || e.key === 'ArrowUp') next = width + step;
51
+ else if (e.key === shrink || e.key === 'ArrowDown') next = width - step;
52
+ else if (e.key === 'Home') next = min;
53
+ else if (e.key === 'End') next = max;
54
+ if (next == null) return;
55
+ e.preventDefault();
56
+ const w = clamp(next);
57
+ onResize(w);
58
+ onResizeEnd?.(w);
59
+ };
60
+
61
+ return (
62
+ <div
63
+ role="separator"
64
+ tabIndex={0}
65
+ aria-orientation="vertical"
66
+ aria-label={`Resize ${label} column`}
67
+ aria-valuenow={width}
68
+ aria-valuemin={min}
69
+ aria-valuemax={max}
70
+ aria-valuetext={`${width} pixels`}
71
+ data-column-resizer=""
72
+ onPointerDown={onPointerDown}
73
+ onPointerMove={onPointerMove}
74
+ onPointerUp={onPointerUp}
75
+ onPointerCancel={onPointerUp}
76
+ onKeyDown={onKeyDown}
77
+ className={cn(
78
+ 'group/resizer absolute inset-y-0 -end-1.5 z-[2] flex w-3 cursor-col-resize touch-none justify-center',
79
+ 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring',
80
+ )}
81
+ >
82
+ <span
83
+ aria-hidden
84
+ className="my-2 w-px bg-border transition-colors group-hover/resizer:w-0.5 group-hover/resizer:bg-primary group-focus-visible/resizer:w-0.5 group-focus-visible/resizer:bg-primary motion-reduce:transition-none"
85
+ />
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,149 @@
1
+ import { useEffect, useId, useRef, useState, type ComponentProps, type ReactNode } from 'react';
2
+
3
+ const mountedNames = new Map<string, number>();
4
+
5
+ export interface SingleSelection<T> {
6
+ selectedId: string | null;
7
+ inputProps: (row: T) => ComponentProps<'input'>;
8
+ entryProps: (row: T) => ComponentProps<'span'>;
9
+ }
10
+
11
+ export function useSingleSelection<T>(options: {
12
+ selectionName?: string;
13
+ selectedId?: string | null;
14
+ defaultSelectedId?: string | null;
15
+ onSelectedIdChange?: (id: string) => void;
16
+ getRowId: (row: T) => string;
17
+ getRowLabel: (row: T) => string;
18
+ rows: readonly T[];
19
+ isRowDisabled?: (row: T) => boolean;
20
+ }): SingleSelection<T> {
21
+ const generatedName = useId();
22
+ const [innerId, setInnerId] = useState(options.defaultSelectedId ?? null);
23
+ const selectedId = options.selectedId === undefined ? innerId : options.selectedId;
24
+ const inputs = useRef(new Map<string, HTMLInputElement>());
25
+ const entry = useRef<HTMLSpanElement>(null);
26
+ const enabledIds = options.rows.filter((row) => !options.isRowDisabled?.(row)).map(options.getRowId);
27
+ const tabId = selectedId != null && enabledIds.includes(selectedId) ? selectedId : enabledIds[0];
28
+ // Chromium excludes every unchecked same-name radio from sequential focus when
29
+ // a disabled radio is checked. Put the fallback tab stop beside the first
30
+ // enabled radio so other table controls retain their normal Tab order.
31
+ const redirectEntry =
32
+ tabId !== undefined &&
33
+ options.rows.some((row) => options.getRowId(row) === selectedId && options.isRowDisabled?.(row));
34
+ const select = (id: string) => {
35
+ if (id === selectedId) return;
36
+ setInnerId(id);
37
+ options.onSelectedIdChange?.(id);
38
+ };
39
+ return {
40
+ selectedId,
41
+ entryProps: (row) =>
42
+ redirectEntry && options.getRowId(row) === tabId
43
+ ? {
44
+ ref: entry,
45
+ tabIndex: 0,
46
+ onFocus: (event) => {
47
+ if (event.target === event.currentTarget && tabId !== undefined)
48
+ inputs.current.get(tabId)?.focus({ preventScroll: true });
49
+ },
50
+ onPointerDown: (event) => {
51
+ // Only keyboard entry redirects focus; clicking the wrapper's blank
52
+ // space must not focus it or interfere with native radio clicks.
53
+ if (event.target === event.currentTarget) event.preventDefault();
54
+ },
55
+ onBlur: (event) => {
56
+ if (!event.currentTarget.contains(event.relatedTarget)) event.currentTarget.tabIndex = 0;
57
+ },
58
+ }
59
+ : {},
60
+ inputProps: (row) => {
61
+ const id = options.getRowId(row);
62
+ const disabled = options.isRowDisabled?.(row) ?? false;
63
+ return {
64
+ type: 'radio',
65
+ name: options.selectionName ?? generatedName,
66
+ 'aria-label': options.getRowLabel(row),
67
+ checked: selectedId === id,
68
+ disabled,
69
+ tabIndex: !disabled && id === tabId ? 0 : -1,
70
+ ref: (input) => {
71
+ if (input) inputs.current.set(id, input);
72
+ else inputs.current.delete(id);
73
+ },
74
+ onChange: () => {
75
+ if (!disabled) select(id);
76
+ },
77
+ onKeyDown: (event) => {
78
+ // Avoid a second stop on the entry wrapper when leaving its radio.
79
+ // Its blur handler restores keyboard entry after focus has left.
80
+ if (event.key === 'Tab' && entry.current) entry.current.tabIndex = -1;
81
+ const direction = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
82
+ if (direction == null || disabled || enabledIds.length === 0) return;
83
+ event.preventDefault();
84
+ const index = enabledIds.indexOf(id);
85
+ const nextId = enabledIds[(index + direction + enabledIds.length) % enabledIds.length]!;
86
+ inputs.current.get(nextId)?.focus();
87
+ select(nextId);
88
+ },
89
+ };
90
+ },
91
+ };
92
+ }
93
+
94
+ export function useSingleSelectionWarnings({
95
+ single,
96
+ selectionName,
97
+ paginated,
98
+ hasLabel,
99
+ }: {
100
+ single: boolean;
101
+ selectionName?: string;
102
+ paginated?: boolean;
103
+ hasLabel: boolean;
104
+ }) {
105
+ useEffect(() => {
106
+ if (single && paginated === true && process.env.NODE_ENV === 'development') {
107
+ console.warn('DataTable single selection: paginated=true is unsupported; rendering all rows without pagination.');
108
+ }
109
+ }, [single, paginated]);
110
+ useEffect(() => {
111
+ if (!single || process.env.NODE_ENV !== 'development') return;
112
+ if (!hasLabel) console.warn('DataTable single selection: supply legend, aria-label or aria-labelledby.');
113
+ }, [single, hasLabel]);
114
+ useEffect(() => {
115
+ if (!single || selectionName === undefined || process.env.NODE_ENV !== 'development') return;
116
+ const count = mountedNames.get(selectionName) ?? 0;
117
+ if (count > 0)
118
+ console.warn(
119
+ `DataTable single selection: duplicate selectionName "${selectionName}"; supplied names must be unique per page.`,
120
+ );
121
+ mountedNames.set(selectionName, count + 1);
122
+ return () => {
123
+ const remaining = (mountedNames.get(selectionName) ?? 1) - 1;
124
+ if (remaining === 0) mountedNames.delete(selectionName);
125
+ else mountedNames.set(selectionName, remaining);
126
+ };
127
+ }, [single, selectionName]);
128
+ }
129
+
130
+ export function SelectionGroup({
131
+ single,
132
+ legend,
133
+ children,
134
+ ...label
135
+ }: {
136
+ single: boolean;
137
+ legend?: ReactNode;
138
+ 'aria-label'?: string;
139
+ 'aria-labelledby'?: string;
140
+ children: ReactNode;
141
+ }) {
142
+ if (!single) return <>{children}</>;
143
+ return (
144
+ <fieldset className="m-0 min-w-0 border-0 p-0" {...label}>
145
+ {legend != null && <legend className="px-3 py-2 text-sm font-medium text-foreground">{legend}</legend>}
146
+ {children}
147
+ </fieldset>
148
+ );
149
+ }
@@ -0,0 +1,79 @@
1
+ import { useState } from 'react';
2
+ import { newViewId, sameViewState, type DataTableView, type DataTableViewState } from './views';
3
+
4
+ /** A value that is controlled when `value` is defined, otherwise kept in state. */
5
+ export function useControllable<V>(
6
+ value: V | undefined,
7
+ initial: V | (() => V),
8
+ onChange?: (next: V) => void,
9
+ ): [V, (next: V) => void] {
10
+ const [inner, setInner] = useState<V>(initial);
11
+ const current = value === undefined ? inner : value;
12
+ const set = (next: V) => {
13
+ setInner(next);
14
+ onChange?.(next);
15
+ };
16
+ return [current, set];
17
+ }
18
+
19
+ export interface UseTableViewsOptions {
20
+ views?: readonly DataTableView[];
21
+ defaultViews?: readonly DataTableView[];
22
+ onViewsChange?: (views: DataTableView[]) => void;
23
+ activeViewId?: string | null;
24
+ defaultActiveViewId?: string | null;
25
+ onActiveViewChange?: (id: string | null) => void;
26
+ /** The table's state right now. */
27
+ current: DataTableViewState;
28
+ /** The table's state with no view applied (props' defaults). */
29
+ defaults: DataTableViewState;
30
+ /** Applies a complete state to the table. */
31
+ apply: (state: DataTableViewState) => void;
32
+ }
33
+
34
+ /** Saved-views state machine for the DataTable: select, save as, save, rename, delete, reset. */
35
+ export function useTableViews({
36
+ views: viewsProp,
37
+ defaultViews = [],
38
+ onViewsChange,
39
+ activeViewId,
40
+ defaultActiveViewId = null,
41
+ onActiveViewChange,
42
+ current,
43
+ defaults,
44
+ apply,
45
+ }: UseTableViewsOptions) {
46
+ const [views, setViews] = useControllable<readonly DataTableView[]>(viewsProp, defaultViews, (next) =>
47
+ onViewsChange?.([...next]),
48
+ );
49
+ const [activeId, setActiveId] = useControllable<string | null>(activeViewId, defaultActiveViewId, onActiveViewChange);
50
+ const active = views.find((v) => v.id === activeId) ?? null;
51
+ const target = (view: DataTableView | null): DataTableViewState => ({ ...defaults, ...(view?.state ?? {}) });
52
+ const dirty = !sameViewState(current, target(active));
53
+
54
+ return {
55
+ views,
56
+ activeId: active ? active.id : null,
57
+ dirty,
58
+ select: (id: string | null) => {
59
+ const view = views.find((v) => v.id === id) ?? null;
60
+ setActiveId(view ? view.id : null);
61
+ apply(target(view));
62
+ },
63
+ saveAs: (name: string) => {
64
+ const view: DataTableView = { id: newViewId(name, views), name, state: current };
65
+ setViews([...views, view]);
66
+ setActiveId(view.id);
67
+ },
68
+ save: () => {
69
+ if (!active) return;
70
+ setViews(views.map((v) => (v.id === active.id ? { ...v, state: current } : v)));
71
+ },
72
+ rename: (id: string, name: string) => setViews(views.map((v) => (v.id === id ? { ...v, name } : v))),
73
+ remove: (id: string) => {
74
+ setViews(views.filter((v) => v.id !== id));
75
+ if (id === activeId) setActiveId(null);
76
+ },
77
+ reset: () => apply(target(active)),
78
+ };
79
+ }
@@ -0,0 +1,83 @@
1
+ import { useEffect, useState, type RefObject } from 'react';
2
+
3
+ /** The items to render: `[start, end)` of the flattened body, each `rowHeight` px tall. */
4
+ export interface VirtualWindow {
5
+ start: number;
6
+ end: number;
7
+ rowHeight: number;
8
+ }
9
+
10
+ export interface UseVirtualRowsOptions {
11
+ enabled: boolean;
12
+ count: number;
13
+ rowHeight: number;
14
+ /** The `<table>`; its parent is the scroll container. */
15
+ tableRef: RefObject<HTMLTableElement | null>;
16
+ /** Fallback viewport height before the container is measured (and in jsdom). */
17
+ viewportHeight: number;
18
+ /** Extra rows rendered above and below the viewport. */
19
+ overscan?: number;
20
+ }
21
+
22
+ /** Computes a virtual window from the table container's scroll position. */
23
+ export function windowFor(
24
+ count: number,
25
+ scrollTop: number,
26
+ viewport: number,
27
+ rowHeight: number,
28
+ overscan: number,
29
+ ): VirtualWindow {
30
+ const h = Math.max(1, rowHeight);
31
+ const first = Math.floor(Math.max(0, scrollTop) / h);
32
+ const visible = Math.ceil(viewport / h) + 1;
33
+ const start = Math.max(0, Math.min(count, first - overscan));
34
+ const end = Math.max(start, Math.min(count, first + visible + overscan));
35
+ return { start, end, rowHeight: h };
36
+ }
37
+
38
+ /**
39
+ * Row virtualization for the DataTable: listens to the scroll container of the table and returns
40
+ * the window of items to render, or `undefined` when disabled.
41
+ */
42
+ export function useVirtualRows({
43
+ enabled,
44
+ count,
45
+ rowHeight,
46
+ tableRef,
47
+ viewportHeight,
48
+ overscan = 8,
49
+ }: UseVirtualRowsOptions): VirtualWindow | undefined {
50
+ const [scroll, setScroll] = useState({ top: 0, height: viewportHeight });
51
+
52
+ useEffect(() => {
53
+ const el = tableRef.current?.parentElement;
54
+ if (!enabled || !el) return undefined;
55
+ let frame = 0;
56
+ const read = () => {
57
+ frame = 0;
58
+ setScroll((prev) => {
59
+ const height = el.clientHeight || prev.height;
60
+ return prev.top === el.scrollTop && prev.height === height ? prev : { top: el.scrollTop, height };
61
+ });
62
+ };
63
+ const schedule = () => {
64
+ if (frame) return;
65
+ frame =
66
+ typeof requestAnimationFrame === 'function'
67
+ ? requestAnimationFrame(read)
68
+ : (setTimeout(read, 0) as unknown as number);
69
+ };
70
+ el.addEventListener('scroll', schedule, { passive: true });
71
+ const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
72
+ ro?.observe(el);
73
+ schedule();
74
+ return () => {
75
+ el.removeEventListener('scroll', schedule);
76
+ ro?.disconnect();
77
+ if (frame && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(frame);
78
+ };
79
+ }, [enabled, tableRef]);
80
+
81
+ if (!enabled) return undefined;
82
+ return windowFor(count, scroll.top, scroll.height, rowHeight, overscan);
83
+ }
@@ -0,0 +1,186 @@
1
+ import { Bookmark, ChevronDown } from '@gntik-ai/icons';
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogBody,
6
+ DialogClose,
7
+ DialogContent,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ Input,
12
+ Menu,
13
+ MenuContent,
14
+ MenuGroup,
15
+ MenuGroupLabel,
16
+ MenuItem,
17
+ MenuRadioGroup,
18
+ MenuRadioItem,
19
+ MenuSeparator,
20
+ MenuTrigger,
21
+ } from '@gntik-ai/ui';
22
+ import { useId, useState } from 'react';
23
+ import type { ColumnWidths, PinnedColumns } from './column-sizing';
24
+ import type { SortState } from './data-table-utils';
25
+
26
+ /** What a saved view captures. Every field is optional: a missing field keeps the table's default. */
27
+ export interface DataTableViewState {
28
+ /** Opaque filter state of the page's toolbar (see the table's `filters` / `onFiltersChange`). */
29
+ filters?: unknown;
30
+ sort?: SortState | null;
31
+ hiddenColumns?: readonly string[];
32
+ columnWidths?: ColumnWidths;
33
+ pinnedColumns?: PinnedColumns;
34
+ /** A field key, or `null` for no grouping (function `groupBy`s cannot be saved). */
35
+ groupBy?: string | null;
36
+ }
37
+
38
+ export interface DataTableView {
39
+ id: string;
40
+ name: string;
41
+ state: DataTableViewState;
42
+ }
43
+
44
+ const norm = (s: DataTableViewState) =>
45
+ JSON.stringify({
46
+ filters: s.filters ?? null,
47
+ sort: s.sort ?? null,
48
+ hiddenColumns: [...(s.hiddenColumns ?? [])].sort(),
49
+ columnWidths: Object.fromEntries(Object.entries(s.columnWidths ?? {}).sort(([a], [b]) => a.localeCompare(b))),
50
+ pinnedColumns: { left: s.pinnedColumns?.left ?? [], right: s.pinnedColumns?.right ?? [] },
51
+ groupBy: s.groupBy ?? null,
52
+ });
53
+
54
+ /** Whether two view states are equivalent (key order and empty values ignored). */
55
+ export function sameViewState(a: DataTableViewState, b: DataTableViewState): boolean {
56
+ return norm(a) === norm(b);
57
+ }
58
+
59
+ /** A new view id that does not collide with `views`. */
60
+ export function newViewId(name: string, views: readonly DataTableView[]): string {
61
+ const base =
62
+ name
63
+ .trim()
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9]+/g, '-')
66
+ .replace(/^-|-$/g, '') || 'view';
67
+ let id = base;
68
+ for (let i = 2; views.some((v) => v.id === id); i++) id = `${base}-${i}`;
69
+ return id;
70
+ }
71
+
72
+ export interface ViewSwitcherProps {
73
+ views: readonly DataTableView[];
74
+ activeId: string | null;
75
+ /** The table differs from the active view (or from the defaults when none is active). */
76
+ dirty: boolean;
77
+ onSelect: (id: string | null) => void;
78
+ onSaveAs: (name: string) => void;
79
+ onSave: () => void;
80
+ onRename: (id: string, name: string) => void;
81
+ onDelete: (id: string) => void;
82
+ onReset: () => void;
83
+ }
84
+
85
+ const DEFAULT_VALUE = '__default__';
86
+
87
+ /** "Views" menu: switch between saved views, save the current table as a view, rename, delete, reset. */
88
+ export function ViewSwitcher({
89
+ views,
90
+ activeId,
91
+ dirty,
92
+ onSelect,
93
+ onSaveAs,
94
+ onSave,
95
+ onRename,
96
+ onDelete,
97
+ onReset,
98
+ }: ViewSwitcherProps) {
99
+ const inputId = useId();
100
+ const active = views.find((v) => v.id === activeId) ?? null;
101
+ const [dialog, setDialog] = useState<'create' | 'rename' | null>(null);
102
+ const [name, setName] = useState('');
103
+ const open = (mode: 'create' | 'rename') => {
104
+ setName(mode === 'rename' && active ? active.name : '');
105
+ setDialog(mode);
106
+ };
107
+ const submit = () => {
108
+ const trimmed = name.trim();
109
+ if (!trimmed) return;
110
+ if (dialog === 'rename' && active) onRename(active.id, trimmed);
111
+ else onSaveAs(trimmed);
112
+ setDialog(null);
113
+ };
114
+ return (
115
+ <>
116
+ <Menu>
117
+ <MenuTrigger render={<Button variant="secondary" size="sm" icon={Bookmark} trailingIcon={ChevronDown} />}>
118
+ <span className="max-w-40 truncate">{active ? active.name : 'Default view'}</span>
119
+ {dirty && (
120
+ <>
121
+ {' '}
122
+ <span aria-hidden className="size-1.5 rounded-full bg-primary" />{' '}
123
+ <span className="sr-only">(modified)</span>
124
+ </>
125
+ )}
126
+ </MenuTrigger>
127
+ <MenuContent align="end" className="min-w-[220px]">
128
+ <MenuGroup>
129
+ <MenuGroupLabel>Views</MenuGroupLabel>
130
+ <MenuRadioGroup
131
+ value={activeId ?? DEFAULT_VALUE}
132
+ onValueChange={(v: unknown) => onSelect(v === DEFAULT_VALUE ? null : String(v))}
133
+ >
134
+ <MenuRadioItem value={DEFAULT_VALUE}>Default view</MenuRadioItem>
135
+ {views.map((v) => (
136
+ <MenuRadioItem key={v.id} value={v.id}>
137
+ {v.name}
138
+ </MenuRadioItem>
139
+ ))}
140
+ </MenuRadioGroup>
141
+ </MenuGroup>
142
+ <MenuSeparator />
143
+ <MenuItem disabled={!active || !dirty} onClick={onSave}>
144
+ Save changes
145
+ </MenuItem>
146
+ <MenuItem onClick={() => open('create')}>Save as new view…</MenuItem>
147
+ <MenuItem disabled={!active} onClick={() => open('rename')}>
148
+ Rename view…
149
+ </MenuItem>
150
+ <MenuItem disabled={!active} onClick={() => active && onDelete(active.id)}>
151
+ Delete view
152
+ </MenuItem>
153
+ <MenuItem disabled={!dirty} onClick={onReset}>
154
+ Reset changes
155
+ </MenuItem>
156
+ </MenuContent>
157
+ </Menu>
158
+ <Dialog open={dialog != null} onOpenChange={(o) => !o && setDialog(null)}>
159
+ <DialogContent size="sm">
160
+ <form
161
+ onSubmit={(e) => {
162
+ e.preventDefault();
163
+ submit();
164
+ }}
165
+ >
166
+ <DialogHeader>
167
+ <DialogTitle>{dialog === 'rename' ? 'Rename view' : 'Save view'}</DialogTitle>
168
+ </DialogHeader>
169
+ <DialogBody>
170
+ <label htmlFor={inputId} className="mb-1.5 block text-[12.5px] font-medium text-foreground">
171
+ View name
172
+ </label>
173
+ <Input id={inputId} value={name} onChange={(e) => setName(e.target.value)} autoComplete="off" required />
174
+ </DialogBody>
175
+ <DialogFooter>
176
+ <DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
177
+ <Button type="submit" disabled={!name.trim()}>
178
+ {dialog === 'rename' ? 'Rename' : 'Save view'}
179
+ </Button>
180
+ </DialogFooter>
181
+ </form>
182
+ </DialogContent>
183
+ </Dialog>
184
+ </>
185
+ );
186
+ }