@gntik-ai/blocks 0.0.0-stage → 0.6.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 (397) hide show
  1. package/dist/index.d.ts +3626 -0
  2. package/dist/index.js +15417 -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.configured.test.tsx +316 -0
  49. package/src/auth/SignUpForm/SignUpForm.test.tsx +33 -0
  50. package/src/auth/SignUpForm/SignUpForm.tsx +298 -0
  51. package/src/auth/SignUpForm/block.meta.ts +22 -0
  52. package/src/auth/SignUpForm/examples/CustomFields.tsx +56 -0
  53. package/src/auth/SignUpForm/examples/Error.tsx +5 -0
  54. package/src/auth/SignUpForm/examples/Loading.tsx +6 -0
  55. package/src/auth/SignUpForm/examples/NoTerms.tsx +6 -0
  56. package/src/auth/SignUpForm/examples/Submitting.tsx +5 -0
  57. package/src/auth/SignUpForm/examples/Unavailable.tsx +15 -0
  58. package/src/auth/SsoButtons/SsoButtons.test.tsx +23 -0
  59. package/src/auth/SsoButtons/SsoButtons.tsx +52 -0
  60. package/src/auth/SsoButtons/block.meta.ts +9 -0
  61. package/src/auth/SsoButtons/fixtures.ts +15 -0
  62. package/src/auth/index.ts +16 -0
  63. package/src/auth/shared.tsx +147 -0
  64. package/src/billing/CostBreakdown/CostBreakdown.test.tsx +24 -0
  65. package/src/billing/CostBreakdown/CostBreakdown.tsx +95 -0
  66. package/src/billing/CostBreakdown/block.meta.ts +10 -0
  67. package/src/billing/CostBreakdown/fixtures.ts +9 -0
  68. package/src/billing/InvoiceTable/InvoiceTable.test.tsx +29 -0
  69. package/src/billing/InvoiceTable/InvoiceTable.tsx +130 -0
  70. package/src/billing/InvoiceTable/block.meta.ts +10 -0
  71. package/src/billing/InvoiceTable/fixtures.ts +38 -0
  72. package/src/billing/PaymentMethodCard/PaymentMethodCard.test.tsx +28 -0
  73. package/src/billing/PaymentMethodCard/PaymentMethodCard.tsx +103 -0
  74. package/src/billing/PaymentMethodCard/block.meta.ts +10 -0
  75. package/src/billing/PaymentMethodCard/fixtures.ts +12 -0
  76. package/src/billing/PlanCard/PlanCard.baseline.test.tsx +26 -0
  77. package/src/billing/PlanCard/PlanCard.summary.test.tsx +227 -0
  78. package/src/billing/PlanCard/PlanCard.test.tsx +27 -0
  79. package/src/billing/PlanCard/PlanCard.tsx +140 -0
  80. package/src/billing/PlanCard/PlanSummary.tsx +101 -0
  81. package/src/billing/PlanCard/README.md +23 -0
  82. package/src/billing/PlanCard/__snapshots__/PlanCard.baseline.test.tsx.snap +5 -0
  83. package/src/billing/PlanCard/block.meta.ts +60 -0
  84. package/src/billing/PlanCard/examples/SummaryActionsOnly.tsx +12 -0
  85. package/src/billing/PlanCard/examples/SummaryFeaturesOnly.tsx +14 -0
  86. package/src/billing/PlanCard/examples/SummaryFull.tsx +28 -0
  87. package/src/billing/PlanCard/examples/SummaryMinimal.tsx +5 -0
  88. package/src/billing/PlanCard/examples/SummaryPriceOnly.tsx +15 -0
  89. package/src/billing/PlanCard/examples/SummaryRenewalOnly.tsx +14 -0
  90. package/src/billing/PlanCard/examples/SummaryUnassigned.tsx +5 -0
  91. package/src/billing/PlanCard/examples/SummaryUnassignedNoLabel.tsx +5 -0
  92. package/src/billing/PlanCard/examples/SummaryZeroPrice.tsx +15 -0
  93. package/src/billing/PlanCard/fixtures.ts +19 -0
  94. package/src/billing/PlanDefinitionCard/PlanDefinitionCard.test.tsx +111 -0
  95. package/src/billing/PlanDefinitionCard/PlanDefinitionCard.tsx +86 -0
  96. package/src/billing/PlanDefinitionCard/README.md +29 -0
  97. package/src/billing/PlanDefinitionCard/block.meta.ts +46 -0
  98. package/src/billing/PlanDefinitionCard/examples/Compact.tsx +23 -0
  99. package/src/billing/PlanDefinitionCard/examples/Full.tsx +23 -0
  100. package/src/billing/PlanDefinitionCard/fixtures.ts +12 -0
  101. package/src/billing/QuotaMeters/QuotaMeters.test.tsx +23 -0
  102. package/src/billing/QuotaMeters/QuotaMeters.tsx +92 -0
  103. package/src/billing/QuotaMeters/block.meta.ts +10 -0
  104. package/src/billing/QuotaMeters/fixtures.ts +9 -0
  105. package/src/billing/SpendVsBudget/SpendVsBudget.test.tsx +24 -0
  106. package/src/billing/SpendVsBudget/SpendVsBudget.tsx +105 -0
  107. package/src/billing/SpendVsBudget/block.meta.ts +10 -0
  108. package/src/billing/SpendVsBudget/fixtures.ts +18 -0
  109. package/src/billing/format.ts +42 -0
  110. package/src/billing/index.ts +21 -0
  111. package/src/builders/CodePanel/CodePanel.test.tsx +280 -0
  112. package/src/builders/CodePanel/CodePanel.tsx +239 -0
  113. package/src/builders/CodePanel/block.meta.ts +10 -0
  114. package/src/builders/CodePanel/examples/controlled.tsx +15 -0
  115. package/src/builders/CodePanel/examples/diagnostics.tsx +40 -0
  116. package/src/builders/CodePanel/fixtures.ts +95 -0
  117. package/src/builders/CodePanel/marker-store.ts +90 -0
  118. package/src/builders/CodePanel/markers.ts +62 -0
  119. package/src/builders/FlowBuilder/FlowBuilder.test.tsx +480 -0
  120. package/src/builders/FlowBuilder/FlowBuilder.tsx +343 -0
  121. package/src/builders/FlowBuilder/block.meta.ts +20 -0
  122. package/src/builders/FlowBuilder/connections.ts +67 -0
  123. package/src/builders/FlowBuilder/decorated-nodes.ts +80 -0
  124. package/src/builders/FlowBuilder/examples/extensions.tsx +120 -0
  125. package/src/builders/FlowBuilder/extensions.ts +37 -0
  126. package/src/builders/FlowBuilder/fixtures.ts +80 -0
  127. package/src/builders/FlowBuilder/parts.tsx +196 -0
  128. package/src/builders/FlowBuilder/problems.tsx +40 -0
  129. package/src/builders/LogViewer/LogViewer.test.tsx +97 -0
  130. package/src/builders/LogViewer/LogViewer.tsx +253 -0
  131. package/src/builders/LogViewer/block.meta.ts +10 -0
  132. package/src/builders/LogViewer/fixtures.ts +49 -0
  133. package/src/builders/index.ts +15 -0
  134. package/src/data-display/ActivityFeed/ActivityFeed.test.tsx +62 -0
  135. package/src/data-display/ActivityFeed/ActivityFeed.tsx +118 -0
  136. package/src/data-display/ActivityFeed/block.meta.ts +10 -0
  137. package/src/data-display/ActivityFeed/fixtures.ts +29 -0
  138. package/src/data-display/ChartCard/ChartCard.ranges.test.tsx +282 -0
  139. package/src/data-display/ChartCard/ChartCard.test.tsx +54 -0
  140. package/src/data-display/ChartCard/ChartCard.tsx +218 -0
  141. package/src/data-display/ChartCard/block.meta.ts +10 -0
  142. package/src/data-display/ChartCard/examples/custom-ranges.tsx +37 -0
  143. package/src/data-display/ChartCard/examples/heatmap.tsx +20 -0
  144. package/src/data-display/ChartCard/examples/toolbar-owned-range.tsx +34 -0
  145. package/src/data-display/ChartCard/fixtures.ts +82 -0
  146. package/src/data-display/CommentThread/CommentThread.test.tsx +29 -0
  147. package/src/data-display/CommentThread/CommentThread.tsx +120 -0
  148. package/src/data-display/CommentThread/block.meta.ts +10 -0
  149. package/src/data-display/CommentThread/fixtures.ts +28 -0
  150. package/src/data-display/DescriptionListCard/DescriptionListCard.test.tsx +28 -0
  151. package/src/data-display/DescriptionListCard/DescriptionListCard.tsx +120 -0
  152. package/src/data-display/DescriptionListCard/block.meta.ts +9 -0
  153. package/src/data-display/DescriptionListCard/fixtures.ts +12 -0
  154. package/src/data-display/KpiRow/KpiRow.test.tsx +29 -0
  155. package/src/data-display/KpiRow/KpiRow.tsx +101 -0
  156. package/src/data-display/KpiRow/block.meta.ts +10 -0
  157. package/src/data-display/KpiRow/fixtures.ts +34 -0
  158. package/src/data-display/ResourceCardGrid/ResourceCardGrid.test.tsx +22 -0
  159. package/src/data-display/ResourceCardGrid/ResourceCardGrid.tsx +98 -0
  160. package/src/data-display/ResourceCardGrid/block.meta.ts +10 -0
  161. package/src/data-display/ResourceCardGrid/fixtures.ts +72 -0
  162. package/src/data-display/StatCard/StatCard.test.tsx +30 -0
  163. package/src/data-display/StatCard/StatCard.tsx +138 -0
  164. package/src/data-display/StatCard/block.meta.ts +10 -0
  165. package/src/data-display/StatCard/fixtures.ts +26 -0
  166. package/src/data-display/StatusTimeline/StatusTimeline.test.tsx +153 -0
  167. package/src/data-display/StatusTimeline/StatusTimeline.tsx +114 -0
  168. package/src/data-display/StatusTimeline/block.meta.ts +10 -0
  169. package/src/data-display/StatusTimeline/examples/Empty.tsx +6 -0
  170. package/src/data-display/StatusTimeline/fixtures.ts +62 -0
  171. package/src/data-display/index.ts +34 -0
  172. package/src/data-display/shared/TrendDelta.tsx +60 -0
  173. package/src/feedback/ConfirmDestructive/ConfirmDestructive.test.tsx +47 -0
  174. package/src/feedback/ConfirmDestructive/ConfirmDestructive.tsx +145 -0
  175. package/src/feedback/ConfirmDestructive/block.meta.ts +10 -0
  176. package/src/feedback/EmptyStates/EmptyStates.test.tsx +48 -0
  177. package/src/feedback/EmptyStates/EmptyStates.tsx +191 -0
  178. package/src/feedback/EmptyStates/block.meta.ts +10 -0
  179. package/src/feedback/EmptyStates/fixtures.ts +6 -0
  180. package/src/feedback/ErrorPanel/ErrorPanel.test.tsx +37 -0
  181. package/src/feedback/ErrorPanel/ErrorPanel.tsx +146 -0
  182. package/src/feedback/ErrorPanel/block.meta.ts +10 -0
  183. package/src/feedback/InlineCallout/InlineCallout.test.tsx +33 -0
  184. package/src/feedback/InlineCallout/InlineCallout.tsx +81 -0
  185. package/src/feedback/InlineCallout/block.meta.ts +9 -0
  186. package/src/feedback/Skeletons/Skeletons.test.tsx +25 -0
  187. package/src/feedback/Skeletons/Skeletons.tsx +142 -0
  188. package/src/feedback/Skeletons/block.meta.ts +10 -0
  189. package/src/feedback/SystemBanner/SystemBanner.test.tsx +29 -0
  190. package/src/feedback/SystemBanner/SystemBanner.tsx +105 -0
  191. package/src/feedback/SystemBanner/block.meta.ts +9 -0
  192. package/src/feedback/index.ts +14 -0
  193. package/src/feedback/types.ts +13 -0
  194. package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.test.tsx +36 -0
  195. package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.tsx +197 -0
  196. package/src/forms/CreateApiKeyDialog/block.meta.ts +10 -0
  197. package/src/forms/CreateApiKeyDialog/fixtures.ts +31 -0
  198. package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.test.tsx +339 -0
  199. package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.tsx +193 -0
  200. package/src/forms/CredentialDisclosureDialog/block.meta.ts +10 -0
  201. package/src/forms/CredentialDisclosureDialog/examples/Fresh.tsx +32 -0
  202. package/src/forms/CredentialDisclosureDialog/examples/ResponseGating.tsx +69 -0
  203. package/src/forms/CredentialDisclosureDialog/examples/Stored.tsx +32 -0
  204. package/src/forms/CredentialDisclosureDialog/fixtures.ts +14 -0
  205. package/src/forms/DangerZone/ConfirmDestructiveDialog.tsx +101 -0
  206. package/src/forms/DangerZone/DangerZone.test.tsx +28 -0
  207. package/src/forms/DangerZone/DangerZone.tsx +86 -0
  208. package/src/forms/DangerZone/block.meta.ts +10 -0
  209. package/src/forms/DangerZone/fixtures.ts +36 -0
  210. package/src/forms/DateRangeFilter/DateRangeFilter.test.tsx +26 -0
  211. package/src/forms/DateRangeFilter/DateRangeFilter.tsx +83 -0
  212. package/src/forms/DateRangeFilter/block.meta.ts +9 -0
  213. package/src/forms/DialogWizard/DialogWizard.test.tsx +95 -0
  214. package/src/forms/DialogWizard/DialogWizard.tsx +39 -0
  215. package/src/forms/DialogWizard/block.meta.ts +10 -0
  216. package/src/forms/DialogWizard/examples/CreateResource.tsx +65 -0
  217. package/src/forms/DialogWizard/fixtures.ts +20 -0
  218. package/src/forms/FileUploadPanel/FileUploadPanel.test.tsx +43 -0
  219. package/src/forms/FileUploadPanel/FileUploadPanel.tsx +244 -0
  220. package/src/forms/FileUploadPanel/block.meta.ts +10 -0
  221. package/src/forms/FileUploadPanel/fixtures.ts +66 -0
  222. package/src/forms/FormSection/FormSection.test.tsx +34 -0
  223. package/src/forms/FormSection/FormSection.tsx +77 -0
  224. package/src/forms/FormSection/block.meta.ts +9 -0
  225. package/src/forms/InviteMembersDialog/InviteMembersDialog.test.tsx +37 -0
  226. package/src/forms/InviteMembersDialog/InviteMembersDialog.tsx +183 -0
  227. package/src/forms/InviteMembersDialog/block.meta.ts +10 -0
  228. package/src/forms/InviteMembersDialog/fixtures.ts +14 -0
  229. package/src/forms/SettingsRow/SettingsRow.test.tsx +50 -0
  230. package/src/forms/SettingsRow/SettingsRow.tsx +74 -0
  231. package/src/forms/SettingsRow/block.meta.ts +9 -0
  232. package/src/forms/ValidationSummary/ValidationSummary.test.tsx +46 -0
  233. package/src/forms/ValidationSummary/ValidationSummary.tsx +75 -0
  234. package/src/forms/ValidationSummary/block.meta.ts +9 -0
  235. package/src/forms/ValidationSummary/fixtures.ts +13 -0
  236. package/src/forms/index.ts +27 -0
  237. package/src/index.ts +14 -0
  238. package/src/marketing/ComparisonTable/ComparisonTable.test.tsx +69 -0
  239. package/src/marketing/ComparisonTable/ComparisonTable.tsx +245 -0
  240. package/src/marketing/ComparisonTable/block.meta.ts +10 -0
  241. package/src/marketing/ComparisonTable/examples/options.tsx +48 -0
  242. package/src/marketing/ComparisonTable/fixtures.ts +83 -0
  243. package/src/marketing/Faq/Faq.test.tsx +25 -0
  244. package/src/marketing/Faq/Faq.tsx +68 -0
  245. package/src/marketing/Faq/block.meta.ts +10 -0
  246. package/src/marketing/Faq/fixtures.ts +32 -0
  247. package/src/marketing/FeatureGrid/FeatureGrid.test.tsx +19 -0
  248. package/src/marketing/FeatureGrid/FeatureGrid.tsx +79 -0
  249. package/src/marketing/FeatureGrid/block.meta.ts +10 -0
  250. package/src/marketing/FeatureGrid/fixtures.ts +36 -0
  251. package/src/marketing/Hero/Hero.test.tsx +33 -0
  252. package/src/marketing/Hero/Hero.tsx +107 -0
  253. package/src/marketing/Hero/block.meta.ts +10 -0
  254. package/src/marketing/PricingTable/PricingTable.test.tsx +92 -0
  255. package/src/marketing/PricingTable/PricingTable.tsx +323 -0
  256. package/src/marketing/PricingTable/block.meta.ts +10 -0
  257. package/src/marketing/PricingTable/fixtures.ts +34 -0
  258. package/src/marketing/actions.tsx +40 -0
  259. package/src/marketing/index.ts +15 -0
  260. package/src/meta.ts +25 -0
  261. package/src/page-chrome/BulkActionBar/BulkActionBar.test.tsx +34 -0
  262. package/src/page-chrome/BulkActionBar/BulkActionBar.tsx +76 -0
  263. package/src/page-chrome/BulkActionBar/block.meta.ts +9 -0
  264. package/src/page-chrome/PageHeader/PageHeader.test.tsx +68 -0
  265. package/src/page-chrome/PageHeader/PageHeader.tsx +152 -0
  266. package/src/page-chrome/PageHeader/block.meta.ts +10 -0
  267. package/src/page-chrome/PageHeader/fixtures.ts +29 -0
  268. package/src/page-chrome/PageToolbar/PageToolbar.test.tsx +38 -0
  269. package/src/page-chrome/PageToolbar/PageToolbar.tsx +138 -0
  270. package/src/page-chrome/PageToolbar/block.meta.ts +10 -0
  271. package/src/page-chrome/PageToolbar/fixtures.ts +8 -0
  272. package/src/page-chrome/SectionHeader/SectionHeader.test.tsx +28 -0
  273. package/src/page-chrome/SectionHeader/SectionHeader.tsx +73 -0
  274. package/src/page-chrome/SectionHeader/block.meta.ts +9 -0
  275. package/src/page-chrome/StickyActionBar/StickyActionBar.test.tsx +29 -0
  276. package/src/page-chrome/StickyActionBar/StickyActionBar.tsx +66 -0
  277. package/src/page-chrome/StickyActionBar/block.meta.ts +9 -0
  278. package/src/page-chrome/index.ts +13 -0
  279. package/src/page-chrome/types.ts +25 -0
  280. package/src/scheduling/AgendaList/AgendaList.test.tsx +28 -0
  281. package/src/scheduling/AgendaList/AgendaList.tsx +165 -0
  282. package/src/scheduling/AgendaList/block.meta.ts +10 -0
  283. package/src/scheduling/MonthCalendar/MonthCalendar.test.tsx +32 -0
  284. package/src/scheduling/MonthCalendar/MonthCalendar.tsx +223 -0
  285. package/src/scheduling/MonthCalendar/block.meta.ts +10 -0
  286. package/src/scheduling/TimeSlotPicker/TimeSlotPicker.test.tsx +26 -0
  287. package/src/scheduling/TimeSlotPicker/TimeSlotPicker.tsx +115 -0
  288. package/src/scheduling/TimeSlotPicker/block.meta.ts +10 -0
  289. package/src/scheduling/TimeSlotPicker/fixtures.ts +10 -0
  290. package/src/scheduling/WeekCalendar/WeekCalendar.test.tsx +27 -0
  291. package/src/scheduling/WeekCalendar/WeekCalendar.tsx +274 -0
  292. package/src/scheduling/WeekCalendar/block.meta.ts +10 -0
  293. package/src/scheduling/index.ts +11 -0
  294. package/src/scheduling/shared/CalendarHeader.tsx +54 -0
  295. package/src/scheduling/shared/EventTile.tsx +21 -0
  296. package/src/scheduling/shared/dates.ts +98 -0
  297. package/src/scheduling/shared/events.ts +90 -0
  298. package/src/scheduling/shared/fixtures.ts +54 -0
  299. package/src/shell/AppSidebar/AppSidebar.test.tsx +38 -0
  300. package/src/shell/AppSidebar/AppSidebar.tsx +226 -0
  301. package/src/shell/AppSidebar/block.meta.ts +10 -0
  302. package/src/shell/AppSidebar/fixtures.ts +58 -0
  303. package/src/shell/AppTopbar/AppTopbar.test.tsx +41 -0
  304. package/src/shell/AppTopbar/AppTopbar.tsx +82 -0
  305. package/src/shell/AppTopbar/block.meta.ts +19 -0
  306. package/src/shell/AppTopbar/fixtures.ts +22 -0
  307. package/src/shell/EnvironmentBadge/EnvironmentBadge.test.tsx +22 -0
  308. package/src/shell/EnvironmentBadge/EnvironmentBadge.tsx +55 -0
  309. package/src/shell/EnvironmentBadge/block.meta.ts +10 -0
  310. package/src/shell/GlobalSearch/GlobalSearch.test.tsx +28 -0
  311. package/src/shell/GlobalSearch/GlobalSearch.tsx +80 -0
  312. package/src/shell/GlobalSearch/block.meta.ts +10 -0
  313. package/src/shell/GlobalSearch/fixtures.ts +64 -0
  314. package/src/shell/index.ts +11 -0
  315. package/src/styles.css +2 -0
  316. package/src/tables/DataTable/DataTable.edit-views.test.tsx +249 -0
  317. package/src/tables/DataTable/DataTable.labels.test.tsx +291 -0
  318. package/src/tables/DataTable/DataTable.layout.test.tsx +188 -0
  319. package/src/tables/DataTable/DataTable.overflow-labels.test.tsx +59 -0
  320. package/src/tables/DataTable/DataTable.single-select.test.tsx +501 -0
  321. package/src/tables/DataTable/DataTable.test.tsx +268 -0
  322. package/src/tables/DataTable/DataTable.tsx +531 -0
  323. package/src/tables/DataTable/block.meta.ts +129 -0
  324. package/src/tables/DataTable/column-menu.tsx +112 -0
  325. package/src/tables/DataTable/column-sizing.ts +166 -0
  326. package/src/tables/DataTable/data-table-body.tsx +195 -0
  327. package/src/tables/DataTable/data-table-props.ts +159 -0
  328. package/src/tables/DataTable/data-table-row.tsx +231 -0
  329. package/src/tables/DataTable/data-table-utils.ts +195 -0
  330. package/src/tables/DataTable/density-toggle.tsx +33 -0
  331. package/src/tables/DataTable/editable-cell.tsx +287 -0
  332. package/src/tables/DataTable/examples/editable.tsx +71 -0
  333. package/src/tables/DataTable/examples/grouped.tsx +18 -0
  334. package/src/tables/DataTable/examples/resizable-pinned.tsx +20 -0
  335. package/src/tables/DataTable/examples/saved-views.tsx +61 -0
  336. package/src/tables/DataTable/examples/single-selection.tsx +55 -0
  337. package/src/tables/DataTable/examples/virtualized.tsx +20 -0
  338. package/src/tables/DataTable/fixtures.ts +119 -0
  339. package/src/tables/DataTable/header-cell.tsx +90 -0
  340. package/src/tables/DataTable/labels.ts +54 -0
  341. package/src/tables/DataTable/resize-handle.tsx +91 -0
  342. package/src/tables/DataTable/single-selection.tsx +149 -0
  343. package/src/tables/DataTable/use-table-views.ts +79 -0
  344. package/src/tables/DataTable/use-virtual-rows.ts +83 -0
  345. package/src/tables/DataTable/views.tsx +193 -0
  346. package/src/tables/FilterBar/FilterBar.labels.test.tsx +69 -0
  347. package/src/tables/FilterBar/FilterBar.test.tsx +43 -0
  348. package/src/tables/FilterBar/FilterBar.tsx +232 -0
  349. package/src/tables/FilterBar/block.meta.ts +32 -0
  350. package/src/tables/FilterBar/fixtures.ts +32 -0
  351. package/src/tables/InlineEditRow/InlineEditRow.test.tsx +41 -0
  352. package/src/tables/InlineEditRow/InlineEditRow.tsx +215 -0
  353. package/src/tables/InlineEditRow/block.meta.ts +10 -0
  354. package/src/tables/InlineEditRow/fixtures.ts +28 -0
  355. package/src/tables/PaginationFooter/PaginationFooter.labels.test.tsx +57 -0
  356. package/src/tables/PaginationFooter/PaginationFooter.test.tsx +29 -0
  357. package/src/tables/PaginationFooter/PaginationFooter.tsx +118 -0
  358. package/src/tables/PaginationFooter/block.meta.ts +33 -0
  359. package/src/tables/SubordinateList/SubordinateList.test.tsx +351 -0
  360. package/src/tables/SubordinateList/SubordinateList.tsx +140 -0
  361. package/src/tables/SubordinateList/block.meta.ts +10 -0
  362. package/src/tables/SubordinateList/examples/ResourceForm.tsx +129 -0
  363. package/src/tables/SubordinateList/examples/empty.tsx +5 -0
  364. package/src/tables/SubordinateList/examples/error.tsx +5 -0
  365. package/src/tables/SubordinateList/examples/loading.tsx +5 -0
  366. package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.test.tsx +159 -0
  367. package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.tsx +60 -0
  368. package/src/tables/TruncatedCollectionFooter/block.meta.ts +10 -0
  369. package/src/tables/TruncatedCollectionFooter/examples/exact.tsx +6 -0
  370. package/src/tables/TruncatedCollectionFooter/examples/has-more.tsx +12 -0
  371. package/src/tables/TruncatedCollectionFooter/fixtures.ts +2 -0
  372. package/src/tables/index.ts +45 -0
  373. package/src/team/MembersTable/MembersTable.test.tsx +29 -0
  374. package/src/team/MembersTable/MembersTable.tsx +141 -0
  375. package/src/team/MembersTable/block.meta.ts +9 -0
  376. package/src/team/MembersTable/fixtures.ts +19 -0
  377. package/src/team/NotificationMatrix/NotificationMatrix.test.tsx +36 -0
  378. package/src/team/NotificationMatrix/NotificationMatrix.tsx +144 -0
  379. package/src/team/NotificationMatrix/block.meta.ts +9 -0
  380. package/src/team/NotificationMatrix/fixtures.ts +43 -0
  381. package/src/team/PendingInvitations/PendingInvitations.test.tsx +30 -0
  382. package/src/team/PendingInvitations/PendingInvitations.tsx +132 -0
  383. package/src/team/PendingInvitations/block.meta.ts +9 -0
  384. package/src/team/PendingInvitations/fixtures.ts +39 -0
  385. package/src/team/ProfileHeader/ProfileHeader.test.tsx +29 -0
  386. package/src/team/ProfileHeader/ProfileHeader.tsx +106 -0
  387. package/src/team/ProfileHeader/block.meta.ts +10 -0
  388. package/src/team/ProfileHeader/fixtures.ts +28 -0
  389. package/src/team/SessionsDevices/SessionsDevices.test.tsx +30 -0
  390. package/src/team/SessionsDevices/SessionsDevices.tsx +126 -0
  391. package/src/team/SessionsDevices/block.meta.ts +10 -0
  392. package/src/team/SessionsDevices/fixtures.ts +58 -0
  393. package/src/team/index.ts +15 -0
  394. package/src/test/a11y.ts +18 -0
  395. package/src/test/heading-levels.test.tsx +69 -0
  396. package/src/test/setup.ts +36 -0
  397. package/README.md +0 -3
@@ -0,0 +1,159 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { I18nProvider } from '@gntik-ai/ui';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { expectNoAxeViolations } from '../../test/a11y';
5
+ import { TruncatedCollectionFooter, type TruncatedCollectionFooterProps } from './TruncatedCollectionFooter';
6
+ import ExactExample from './examples/exact';
7
+ import HasMoreExample from './examples/has-more';
8
+
9
+ // Compile-time checks of the public presentation-only contract.
10
+ function propContract() {
11
+ // @ts-expect-error visibleCount and hasMore are required for consumers
12
+ <TruncatedCollectionFooter />;
13
+ // @ts-expect-error no total is accepted
14
+ <TruncatedCollectionFooter visibleCount={37} hasMore={false} total={97} />;
15
+ // @ts-expect-error no page count is accepted
16
+ <TruncatedCollectionFooter visibleCount={37} hasMore={false} pageCount={10} />;
17
+ // @ts-expect-error no current page is accepted
18
+ <TruncatedCollectionFooter visibleCount={37} hasMore={false} page={1} />;
19
+ // @ts-expect-error no navigation handlers are accepted
20
+ <TruncatedCollectionFooter visibleCount={37} hasMore={false} onPageChange={() => {}} />;
21
+ // @ts-expect-error no event handlers are accepted
22
+ <TruncatedCollectionFooter visibleCount={37} hasMore={false} onClick={() => {}} />;
23
+ }
24
+ void propContract;
25
+
26
+ describe('TruncatedCollectionFooter', () => {
27
+ it('shows the exact loaded count without a truncation note', () => {
28
+ const { container } = render(<TruncatedCollectionFooter visibleCount={37} hasMore={false} />);
29
+ expect(screen.getByText('37', { exact: true })).toBeVisible();
30
+ expect(container).not.toHaveTextContent('+');
31
+ expect(screen.queryByText(/Only the first/)).not.toBeInTheDocument();
32
+ expect(container.querySelectorAll('p')).toHaveLength(1);
33
+ });
34
+
35
+ it('shows a lower bound and a readable, linkable truncation note', () => {
36
+ const { container } = render(<TruncatedCollectionFooter visibleCount={100} hasMore={true} noteId="loaded-note" />);
37
+ expect(screen.getByText('100+', { exact: true })).toBeVisible();
38
+ expect(screen.getByText('items:')).toBeVisible();
39
+ const note = screen.getByText('Only the first 100 items are shown; more exist.');
40
+ expect(note).toHaveAttribute('id', 'loaded-note');
41
+ expect(container.querySelector('[aria-hidden], [aria-live]')).toBeNull();
42
+ });
43
+
44
+ it('lets consumers replace every visible string with plain text nodes', () => {
45
+ const { container } = render(
46
+ <TruncatedCollectionFooter
47
+ visibleCount={100}
48
+ hasMore={true}
49
+ summaryLabel="Loaded records"
50
+ formatCount={(count, hasMore) => <span>{hasMore ? `At least ${count}` : `Exactly ${count}`}</span>}
51
+ truncationNote={(count) => <span>{`Showing ${count} records from the first page.`}</span>}
52
+ className="gap-2.5"
53
+ />,
54
+ );
55
+ expect(screen.getByText('Loaded records')).toBeVisible();
56
+ expect(screen.getByText('At least 100')).toBeVisible();
57
+ expect(screen.getByText('Showing 100 records from the first page.')).toBeVisible();
58
+ expect(container).not.toHaveTextContent('items:');
59
+ expect(container).not.toHaveTextContent('Only the first');
60
+ expect(container.firstElementChild).toHaveClass('gap-2.5');
61
+ });
62
+
63
+ it('formats counts in the chosen locale and uses the supplied noun', () => {
64
+ render(<TruncatedCollectionFooter visibleCount={12345} hasMore={true} noun="records" locale="de-DE" />);
65
+ expect(screen.getByText('12.345+', { exact: true })).toBeVisible();
66
+ expect(screen.getByText('records:')).toBeVisible();
67
+ expect(screen.getByText('Only the first 12.345 records are shown; more exist.')).toBeVisible();
68
+ });
69
+
70
+ it('inherits Spanish defaults and number formatting from the provider', () => {
71
+ render(
72
+ <I18nProvider locale="es">
73
+ <TruncatedCollectionFooter visibleCount={12345} hasMore={true} />
74
+ </I18nProvider>,
75
+ );
76
+ expect(screen.getByText('12.345+', { exact: true })).toBeVisible();
77
+ expect(screen.getByText('elementos:')).toBeVisible();
78
+ expect(screen.getByText('Solo se muestran los primeros 12.345 elementos; hay más.')).toBeVisible();
79
+ });
80
+
81
+ it('renders a meaningful no-props catalogue preview at runtime', () => {
82
+ // The JavaScript preview loader passes no props; consumers still require both inputs.
83
+ render(<TruncatedCollectionFooter {...({} as TruncatedCollectionFooterProps)} />);
84
+ expect(screen.getByText('37', { exact: true })).toBeVisible();
85
+ expect(screen.getByText('items:')).toBeVisible();
86
+ expect(screen.queryByText(/Only the first/)).not.toBeInTheDocument();
87
+ });
88
+
89
+ it('accepts a static note and an empty label without restoring default copy', () => {
90
+ render(
91
+ <TruncatedCollectionFooter
92
+ visibleCount={100}
93
+ hasMore={true}
94
+ summaryLabel={null}
95
+ truncationNote="More records exist outside this view."
96
+ />,
97
+ );
98
+ expect(screen.getByText('More records exist outside this view.')).toBeVisible();
99
+ expect(screen.queryByText('items:')).not.toBeInTheDocument();
100
+ });
101
+
102
+ it('does not render or evaluate the note when the loaded count is exact', () => {
103
+ const { container } = render(
104
+ <TruncatedCollectionFooter
105
+ visibleCount={37}
106
+ hasMore={false}
107
+ formatCount={(count, hasMore) => `${count}${hasMore ? ' or more' : ' exact'}`}
108
+ truncationNote={() => {
109
+ throw new Error('The exact state must not evaluate its note');
110
+ }}
111
+ noteId="absent-note"
112
+ />,
113
+ );
114
+ expect(screen.getByText('37 exact')).toBeVisible();
115
+ expect(container.querySelector('#absent-note')).toBeNull();
116
+ expect(container.querySelectorAll('p')).toHaveLength(1);
117
+ });
118
+
119
+ it.each([
120
+ ['en', 'item:', 'Only the first 1 item is shown; more exist.'],
121
+ ['es', 'elemento:', 'Solo se muestra el primer 1 elemento; hay más.'],
122
+ ])('uses singular catalogue defaults in %s', (locale, label, note) => {
123
+ render(
124
+ <I18nProvider locale={locale}>
125
+ <TruncatedCollectionFooter visibleCount={1} hasMore={true} />
126
+ </I18nProvider>,
127
+ );
128
+ expect(screen.getByText(label)).toBeVisible();
129
+ expect(screen.getByText(note)).toBeVisible();
130
+ });
131
+
132
+ it.each([true, false])('has no controls, navigation or Tab stops with hasMore=%s', async (hasMore) => {
133
+ const user = userEvent.setup();
134
+ const { container } = render(<TruncatedCollectionFooter visibleCount={100} hasMore={hasMore} />);
135
+ for (const role of ['navigation', 'button', 'link', 'combobox', 'textbox']) {
136
+ expect(screen.queryAllByRole(role)).toHaveLength(0);
137
+ }
138
+ expect(container.querySelector('button, a, input, select, textarea, [tabindex], [contenteditable]')).toBeNull();
139
+ expect(container.querySelector('[aria-hidden], [aria-live]')).toBeNull();
140
+ await user.tab();
141
+ expect(document.body).toHaveFocus();
142
+ });
143
+
144
+ for (const theme of ['dark', 'light', 'high_contrast']) {
145
+ it.each([
146
+ ['exact', ExactExample],
147
+ ['has-more', HasMoreExample],
148
+ ] as const)(`has no axe violations in ${theme} / %s`, async (_state, Example) => {
149
+ const previousTheme = document.documentElement.className;
150
+ document.documentElement.className = theme;
151
+ try {
152
+ const { container } = render(<Example />);
153
+ await expectNoAxeViolations(container);
154
+ } finally {
155
+ document.documentElement.className = previousTheme;
156
+ }
157
+ });
158
+ }
159
+ });
@@ -0,0 +1,60 @@
1
+ import { cn, useI18n } from '@gntik-ai/ui';
2
+ import type { ReactNode } from 'react';
3
+ import { TRUNCATED_COLLECTION_PREVIEW } from './fixtures';
4
+
5
+ export interface TruncatedCollectionFooterProps {
6
+ /** Number of records already loaded; never an estimated total. */
7
+ visibleCount: number;
8
+ /** More records exist beyond this first page, making the count a lower bound. */
9
+ hasMore: boolean;
10
+ /** Localized noun used in the default summary label and note. */
11
+ noun?: string;
12
+ /** Number-formatting locale; translated defaults come from I18nProvider. */
13
+ locale?: string;
14
+ /** Plain text label before the count; null hides it. */
15
+ summaryLabel?: ReactNode;
16
+ /** Replaces the entire count, including the lower-bound marker. Return plain text nodes. */
17
+ formatCount?: (count: number, hasMore: boolean) => ReactNode;
18
+ /** Plain text note or formatter, rendered only when hasMore is true. */
19
+ truncationNote?: ((count: number) => ReactNode) | ReactNode;
20
+ /** Id of the note for a consumer's aria-describedby reference. */
21
+ noteId?: string;
22
+ className?: string;
23
+ }
24
+
25
+ /** Static first-page summary with no total, navigation, focus stops or live region. */
26
+ export function TruncatedCollectionFooter({
27
+ visibleCount = TRUNCATED_COLLECTION_PREVIEW.visibleCount,
28
+ hasMore = TRUNCATED_COLLECTION_PREVIEW.hasMore,
29
+ noun: nounProp,
30
+ locale: localeProp,
31
+ summaryLabel,
32
+ formatCount,
33
+ truncationNote,
34
+ noteId,
35
+ className,
36
+ }: TruncatedCollectionFooterProps) {
37
+ const { t, locale: providerLocale } = useI18n();
38
+ const locale = localeProp ?? providerLocale;
39
+ const noun = nounProp ?? t('table.collectionNoun', { count: visibleCount });
40
+ const formattedCount = visibleCount.toLocaleString(locale);
41
+ return (
42
+ <div className={cn('flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between', className)}>
43
+ <p className="text-[12.5px] text-muted-foreground tabular-nums">
44
+ {summaryLabel === undefined ? t('table.visibleCount', { noun }) : summaryLabel}{' '}
45
+ <span className="font-mono text-foreground">
46
+ {formatCount ? formatCount(visibleCount, hasMore) : `${formattedCount}${hasMore ? '+' : ''}`}
47
+ </span>
48
+ </p>
49
+ {hasMore && (
50
+ <p id={noteId} className="text-[12.5px] text-muted-foreground">
51
+ {typeof truncationNote === 'function'
52
+ ? truncationNote(visibleCount)
53
+ : truncationNote === undefined
54
+ ? t('table.truncatedNote', { count: visibleCount, formattedCount, noun })
55
+ : truncationNote}
56
+ </p>
57
+ )}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Truncated collection footer',
5
+ family: 'tables',
6
+ status: 'beta',
7
+ description:
8
+ 'Presentation-only summary of a loaded first cursor page without a total or pager. N+ is a lower bound when hasMore is true; the note explains that more items exist. Compose below data-table (DataTable with paginated={false}). First-page recipe: use hasMore={true} for N+ plus a note, and supply noteId so a consumer table can reference it with aria-describedby. DataTable does not forward aria-describedby; its named containing region can reference the note instead. Count-only recipe: use hasMore={false} for the visible count without a note. PageToolbar has no count prop; this block carries the count. No navigation or focusable elements. Every visible string is overridable through noun, summaryLabel, formatCount and truncationNote; ReactNode overrides must remain plain text without interactive or hidden content. The i18n provider supplies translated defaults; locale overrides number formatting. Required inputs are visibleCount and hasMore, with meaningful runtime defaults for the JavaScript catalogue preview.',
9
+ uses: [],
10
+ };
@@ -0,0 +1,6 @@
1
+ import { TruncatedCollectionFooter } from '../TruncatedCollectionFooter';
2
+
3
+ /** Count-only recipe: retain the visible count without introducing truncation messaging. */
4
+ export default function ExactExample() {
5
+ return <TruncatedCollectionFooter visibleCount={37} hasMore={false} noun="records" />;
6
+ }
@@ -0,0 +1,12 @@
1
+ import { TruncatedCollectionFooter } from '../TruncatedCollectionFooter';
2
+
3
+ /**
4
+ * First-page recipe: place below DataTable with paginated={false}.
5
+ * A consumer table can reference this note with aria-describedby="records-truncation-note".
6
+ * DataTable does not forward aria-describedby; a named containing region can reference it instead.
7
+ */
8
+ export default function HasMoreExample() {
9
+ return (
10
+ <TruncatedCollectionFooter visibleCount={100} hasMore={true} noun="records" noteId="records-truncation-note" />
11
+ );
12
+ }
@@ -0,0 +1,2 @@
1
+ /** Meaningful runtime defaults for the JavaScript catalogue preview. */
2
+ export const TRUNCATED_COLLECTION_PREVIEW = { visibleCount: 37, hasMore: false } as const;
@@ -0,0 +1,45 @@
1
+ // @gntik-ai/blocks — tables family.
2
+ export * from './DataTable/DataTable';
3
+ export * from './DataTable/data-table-utils';
4
+ export { ColumnVisibilityMenu, type ColumnVisibilityMenuProps } from './DataTable/column-menu';
5
+ export { DensityToggle, type DensityToggleProps } from './DataTable/density-toggle';
6
+ export {
7
+ clampColumnWidth,
8
+ columnWidthOf,
9
+ orderColumns,
10
+ setColumnPin,
11
+ DEFAULT_COLUMN_WIDTH,
12
+ MIN_COLUMN_WIDTH,
13
+ MAX_COLUMN_WIDTH,
14
+ type ColumnWidths,
15
+ type PinnedColumns,
16
+ type PinSide,
17
+ } from './DataTable/column-sizing';
18
+ export { ColumnResizeHandle, type ColumnResizeHandleProps } from './DataTable/resize-handle';
19
+ export {
20
+ EditableCell,
21
+ type DataTableCellEdit,
22
+ type DataTableCellEditResult,
23
+ type DataTableCellEditHandler,
24
+ } from './DataTable/editable-cell';
25
+ export {
26
+ ViewSwitcher as DataTableViewSwitcher,
27
+ sameViewState,
28
+ type ViewSwitcherProps as DataTableViewSwitcherProps,
29
+ type DataTableView,
30
+ type DataTableViewState,
31
+ } from './DataTable/views';
32
+ export { meta as dataTableMeta } from './DataTable/block.meta';
33
+ export { DEPLOYMENT_COLUMNS, DEPLOYMENT_ROWS, generateDeployments, type DeploymentRow } from './DataTable/fixtures';
34
+ export * from './FilterBar/FilterBar';
35
+ export { meta as filterBarMeta } from './FilterBar/block.meta';
36
+ export { DEPLOYMENT_DEFAULT_FILTERS, DEPLOYMENT_FILTER_FIELDS, DEPLOYMENT_SAVED_VIEWS } from './FilterBar/fixtures';
37
+ export * from './PaginationFooter/PaginationFooter';
38
+ export { meta as paginationFooterMeta } from './PaginationFooter/block.meta';
39
+ export * from './TruncatedCollectionFooter/TruncatedCollectionFooter';
40
+ export { meta as truncatedCollectionFooterMeta } from './TruncatedCollectionFooter/block.meta';
41
+ export * from './InlineEditRow/InlineEditRow';
42
+ export { meta as inlineEditRowMeta } from './InlineEditRow/block.meta';
43
+ export { EDITABLE_MEMBER_FIELDS, EDITABLE_MEMBERS, type EditableMember } from './InlineEditRow/fixtures';
44
+ export * from './SubordinateList/SubordinateList';
45
+ export { meta as subordinateListMeta } from './SubordinateList/block.meta';
@@ -0,0 +1,29 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { MembersTable } from './MembersTable';
5
+
6
+ describe('MembersTable', () => {
7
+ it('renders the members', async () => {
8
+ const { container } = render(<MembersTable />);
9
+ const table = screen.getByRole('table', { name: 'Workspace members' });
10
+ expect(within(table).getAllByRole('row')).toHaveLength(6);
11
+ expect(screen.getByRole('combobox', { name: 'Role for Avery Collins' })).toHaveAttribute('data-disabled');
12
+ expect(screen.getByText('Never')).toBeInTheDocument();
13
+ await expectNoAxeViolations(container);
14
+ });
15
+
16
+ it('changes a role and removes a member', async () => {
17
+ const onRoleChange = vi.fn();
18
+ const onRemove = vi.fn();
19
+ render(<MembersTable onRoleChange={onRoleChange} onRemove={onRemove} />);
20
+ await userEvent.click(screen.getByRole('combobox', { name: 'Role for Jordan Lee' }));
21
+ await userEvent.click(await screen.findByRole('option', { name: 'Admin' }));
22
+ expect(onRoleChange).toHaveBeenCalledWith('u3', 'admin');
23
+
24
+ await userEvent.click(screen.getByRole('button', { name: 'Actions for Riley Chen' }));
25
+ await userEvent.click(await screen.findByRole('menuitem', { name: 'Remove from workspace' }));
26
+ expect(onRemove).toHaveBeenCalledWith(expect.objectContaining({ id: 'u4' }));
27
+ expect(screen.queryByText('Riley Chen')).not.toBeInTheDocument();
28
+ });
29
+ });
@@ -0,0 +1,141 @@
1
+ import {
2
+ Avatar,
3
+ Badge,
4
+ MoreMenu,
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ Table,
10
+ TableBody,
11
+ TableCaption,
12
+ TableCell,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow,
16
+ Timestamp,
17
+ cn,
18
+ } from '@gntik-ai/ui';
19
+ import { Mail, UserMinus } from '@gntik-ai/icons';
20
+ import { useState } from 'react';
21
+ import { memberRoles, type RoleOption } from '../../forms/InviteMembersDialog/fixtures';
22
+ import { members as defaultMembers, type Member } from './fixtures';
23
+
24
+ export type { Member } from './fixtures';
25
+
26
+ export interface MembersTableProps {
27
+ members?: Member[];
28
+ roles?: RoleOption[];
29
+ /** Id of the signed-in member: shown with a "You" badge, and their role and removal are locked. */
30
+ currentUserId?: string;
31
+ onRoleChange?: (memberId: string, role: string) => void;
32
+ onRemove?: (member: Member) => void;
33
+ onEmail?: (member: Member) => void;
34
+ /** Table caption (screen-reader only by default). */
35
+ caption?: string;
36
+ className?: string;
37
+ }
38
+
39
+ /** Workspace members: avatar, name and email, inline role select, last activity and a row menu. */
40
+ export function MembersTable({
41
+ members = defaultMembers,
42
+ roles = memberRoles,
43
+ currentUserId = 'u1',
44
+ onRoleChange,
45
+ onRemove,
46
+ onEmail,
47
+ caption = 'Workspace members',
48
+ className,
49
+ }: MembersTableProps) {
50
+ const [roleOverrides, setRoleOverrides] = useState<Record<string, string>>({});
51
+ const [removed, setRemoved] = useState<string[]>([]);
52
+ const rows = members.filter((m) => !removed.includes(m.id));
53
+ const items = roles.map(({ value, label }) => ({ value, label }));
54
+
55
+ return (
56
+ <Table className={cn('min-w-[560px]', className)}>
57
+ <TableCaption srOnly>{caption}</TableCaption>
58
+ <TableHeader>
59
+ <TableRow>
60
+ <TableHead>Member</TableHead>
61
+ <TableHead className="w-44">Role</TableHead>
62
+ <TableHead className="hidden sm:table-cell">Last active</TableHead>
63
+ <TableHead align="right">
64
+ <span className="sr-only">Actions</span>
65
+ </TableHead>
66
+ </TableRow>
67
+ </TableHeader>
68
+ <TableBody>
69
+ {rows.map((m) => {
70
+ const you = m.id === currentUserId;
71
+ const role = roleOverrides[m.id] ?? m.role;
72
+ return (
73
+ <TableRow key={m.id}>
74
+ <TableCell>
75
+ <div className="flex min-w-0 items-center gap-3">
76
+ <Avatar size="sm" name={m.name} src={m.avatarUrl} aria-hidden />
77
+ <div className="min-w-0">
78
+ <div className="flex items-center gap-2">
79
+ <span className="truncate text-[13px] font-medium text-foreground">{m.name}</span>
80
+ {you && (
81
+ <Badge size="sm" tone="primary">
82
+ You
83
+ </Badge>
84
+ )}
85
+ </div>
86
+ <div className="truncate text-[12px] text-muted-foreground">{m.email}</div>
87
+ </div>
88
+ </div>
89
+ </TableCell>
90
+ <TableCell>
91
+ <Select
92
+ value={role}
93
+ items={items}
94
+ disabled={you}
95
+ onValueChange={(v) => {
96
+ if (v == null) return;
97
+ setRoleOverrides((o) => ({ ...o, [m.id]: v }));
98
+ onRoleChange?.(m.id, v);
99
+ }}
100
+ >
101
+ <SelectTrigger size="sm" aria-label={`Role for ${m.name}`} className="w-full" />
102
+ <SelectContent>
103
+ {roles.map((r) => (
104
+ <SelectItem key={r.value} value={r.value}>
105
+ {r.label}
106
+ </SelectItem>
107
+ ))}
108
+ </SelectContent>
109
+ </Select>
110
+ </TableCell>
111
+ <TableCell className="hidden text-[12.5px] text-muted-foreground sm:table-cell">
112
+ {m.lastActive ? <Timestamp value={m.lastActive} /> : 'Never'}
113
+ </TableCell>
114
+ <TableCell align="right">
115
+ <MoreMenu
116
+ label={`Actions for ${m.name}`}
117
+ variant="ghost"
118
+ size="sm"
119
+ items={[
120
+ { label: 'Send email', icon: Mail, onSelect: () => onEmail?.(m) },
121
+ 'separator',
122
+ {
123
+ label: 'Remove from workspace',
124
+ icon: UserMinus,
125
+ destructive: true,
126
+ disabled: you,
127
+ onSelect: () => {
128
+ setRemoved((r) => [...r, m.id]);
129
+ onRemove?.(m);
130
+ },
131
+ },
132
+ ]}
133
+ />
134
+ </TableCell>
135
+ </TableRow>
136
+ );
137
+ })}
138
+ </TableBody>
139
+ </Table>
140
+ );
141
+ }
@@ -0,0 +1,9 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Members table',
5
+ family: 'team',
6
+ status: 'stable',
7
+ description: 'Workspace members with avatar, email, an inline role select, last activity and a row menu to remove.',
8
+ uses: ['Table', 'Avatar', 'Badge', 'Select', 'Timestamp', 'MoreMenu'],
9
+ };
@@ -0,0 +1,19 @@
1
+ const ago = (minutes: number) => new Date(Date.now() - minutes * 60_000).toISOString();
2
+
3
+ export interface Member {
4
+ id: string;
5
+ name: string;
6
+ email: string;
7
+ avatarUrl?: string;
8
+ role: string;
9
+ /** ISO date of the last activity; null when the member never signed in. */
10
+ lastActive: string | null;
11
+ }
12
+
13
+ export const members: Member[] = [
14
+ { id: 'u1', name: 'Avery Collins', email: 'avery@example.com', role: 'owner', lastActive: ago(2) },
15
+ { id: 'u2', name: 'Sam Rivera', email: 'sam@example.com', role: 'admin', lastActive: ago(95) },
16
+ { id: 'u3', name: 'Jordan Lee', email: 'jordan@example.com', role: 'member', lastActive: ago(60 * 26) },
17
+ { id: 'u4', name: 'Riley Chen', email: 'riley@example.com', role: 'member', lastActive: ago(60 * 24 * 9) },
18
+ { id: 'u5', name: 'Morgan Patel', email: 'morgan@example.com', role: 'viewer', lastActive: null },
19
+ ];
@@ -0,0 +1,36 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { NotificationMatrix } from './NotificationMatrix';
5
+
6
+ describe('NotificationMatrix', () => {
7
+ it('renders the grid with mixed column state and locked cells', async () => {
8
+ const { container } = render(<NotificationMatrix />);
9
+ expect(screen.getByRole('table', { name: 'Notification preferences' })).toBeInTheDocument();
10
+ expect(screen.getByRole('checkbox', { name: 'All events by Email' })).toHaveAttribute('aria-checked', 'mixed');
11
+ const locked = screen.getByRole('checkbox', { name: 'Security alerts by Email (always on)' });
12
+ expect(locked).toBeChecked();
13
+ expect(locked).toHaveAttribute('aria-disabled', 'true');
14
+ await expectNoAxeViolations(container);
15
+ });
16
+
17
+ it('toggles cells, rows and columns', async () => {
18
+ const onValueChange = vi.fn();
19
+ render(<NotificationMatrix onValueChange={onValueChange} />);
20
+ await userEvent.click(screen.getByRole('checkbox', { name: 'Member joined by Push' }));
21
+ expect(onValueChange).toHaveBeenLastCalledWith(expect.objectContaining({ 'member-joined': ['push'] }));
22
+
23
+ await userEvent.click(screen.getByRole('checkbox', { name: 'All channels for Member joined' }));
24
+ expect(onValueChange).toHaveBeenLastCalledWith(
25
+ expect.objectContaining({ 'member-joined': ['email', 'in-app', 'push'] }),
26
+ );
27
+
28
+ const emailCol = screen.getByRole('checkbox', { name: 'All events by Email' });
29
+ await userEvent.click(emailCol);
30
+ expect(emailCol).toBeChecked();
31
+ await userEvent.click(emailCol);
32
+ const last = onValueChange.mock.lastCall?.[0] as Record<string, string[]>;
33
+ expect(last.security).toContain('email');
34
+ expect(last['deploy-failed']).not.toContain('email');
35
+ });
36
+ });