@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,43 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { FilterBar } from './FilterBar';
5
+
6
+ describe('FilterBar', () => {
7
+ it('renders search, controls and the default chips', async () => {
8
+ const { container } = render(<FilterBar resultCount={12} />);
9
+ expect(screen.getByRole('search', { name: 'Filters' })).toBeInTheDocument();
10
+ expect(screen.getByRole('searchbox', { name: 'Search' })).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: 'Remove filter Status: Running' })).toBeInTheDocument();
12
+ await expectNoAxeViolations(container);
13
+ });
14
+
15
+ it('adds, removes and clears filters', async () => {
16
+ const user = userEvent.setup();
17
+ const onFiltersChange = vi.fn();
18
+ const onQueryChange = vi.fn();
19
+ render(<FilterBar onFiltersChange={onFiltersChange} onQueryChange={onQueryChange} />);
20
+ await user.click(screen.getByRole('button', { name: 'Remove filter Region: eu-west-1' }));
21
+ expect(onFiltersChange).toHaveBeenLastCalledWith([{ field: 'status', value: 'Running' }]);
22
+ await user.click(screen.getByRole('button', { name: 'Add filter' }));
23
+ await user.click(await screen.findByRole('menuitemcheckbox', { name: 'Failed' }));
24
+ await user.keyboard('{Escape}');
25
+ expect(screen.getByRole('button', { name: 'Remove filter Status: Failed' })).toBeInTheDocument();
26
+ await user.type(screen.getByRole('searchbox', { name: 'Search' }), 'api');
27
+ expect(onQueryChange).toHaveBeenLastCalledWith('api');
28
+ await user.click(screen.getByRole('button', { name: 'Clear all' }));
29
+ expect(onFiltersChange).toHaveBeenLastCalledWith([]);
30
+ expect(screen.queryByRole('list', { name: 'Active filters' })).toBeNull();
31
+ });
32
+
33
+ it('applies a saved view', async () => {
34
+ const user = userEvent.setup();
35
+ const onViewChange = vi.fn();
36
+ render(<FilterBar onViewChange={onViewChange} />);
37
+ await user.click(screen.getByRole('combobox', { name: 'Saved view' }));
38
+ await user.click(await screen.findByRole('option', { name: 'Needs attention' }));
39
+ expect(onViewChange).toHaveBeenCalledWith(expect.objectContaining({ value: 'attention' }));
40
+ expect(screen.getByRole('button', { name: 'Remove filter Status: Degraded' })).toBeInTheDocument();
41
+ expect(screen.queryByRole('button', { name: 'Remove filter Status: Running' })).toBeNull();
42
+ });
43
+ });
@@ -0,0 +1,199 @@
1
+ import { ListFilter, Plus, Search } from '@gntik-ai/icons';
2
+ import {
3
+ Button,
4
+ cn,
5
+ Input,
6
+ Menu,
7
+ MenuCheckboxItem,
8
+ MenuContent,
9
+ MenuGroup,
10
+ MenuGroupLabel,
11
+ MenuSeparator,
12
+ MenuTrigger,
13
+ SimpleSelect,
14
+ Token,
15
+ useI18n,
16
+ } from '@gntik-ai/ui';
17
+ import { Fragment, useState } from 'react';
18
+ import { DEPLOYMENT_DEFAULT_FILTERS, DEPLOYMENT_FILTER_FIELDS, DEPLOYMENT_SAVED_VIEWS } from './fixtures';
19
+
20
+ export interface FilterField {
21
+ id: string;
22
+ label: string;
23
+ options: readonly string[];
24
+ }
25
+
26
+ export interface ActiveFilter {
27
+ field: string;
28
+ value: string;
29
+ }
30
+
31
+ export interface SavedView {
32
+ value: string;
33
+ label: string;
34
+ query?: string;
35
+ filters: readonly ActiveFilter[];
36
+ }
37
+
38
+ export interface FilterBarProps {
39
+ fields?: readonly FilterField[];
40
+ /** Search text (controlled). */
41
+ query?: string;
42
+ defaultQuery?: string;
43
+ onQueryChange?: (query: string) => void;
44
+ /** Active filters (controlled). */
45
+ filters?: readonly ActiveFilter[];
46
+ defaultFilters?: readonly ActiveFilter[];
47
+ onFiltersChange?: (filters: ActiveFilter[]) => void;
48
+ /** Saved views; picking one applies its query and filters. Empty hides the Select. */
49
+ views?: readonly SavedView[];
50
+ defaultView?: string | null;
51
+ onViewChange?: (view: SavedView) => void;
52
+ searchLabel?: string;
53
+ placeholder?: string;
54
+ /** Optional result count shown at the end ("12 results"). */
55
+ resultCount?: number;
56
+ className?: string;
57
+ }
58
+
59
+ const same = (a: ActiveFilter, b: ActiveFilter) => a.field === b.field && a.value === b.value;
60
+
61
+ /**
62
+ * Filter bar: search Input, saved views Select, an "Add filter" Menu (multi-select per field),
63
+ * removable filter chips (Token) and Clear all.
64
+ */
65
+ export function FilterBar({
66
+ fields = DEPLOYMENT_FILTER_FIELDS,
67
+ query: queryProp,
68
+ defaultQuery = '',
69
+ onQueryChange,
70
+ filters: filtersProp,
71
+ defaultFilters = DEPLOYMENT_DEFAULT_FILTERS,
72
+ onFiltersChange,
73
+ views = DEPLOYMENT_SAVED_VIEWS,
74
+ defaultView = null,
75
+ onViewChange,
76
+ searchLabel: searchLabelProp,
77
+ placeholder = 'Search by name…',
78
+ resultCount,
79
+ className,
80
+ }: FilterBarProps) {
81
+ const { t } = useI18n();
82
+ const searchLabel = searchLabelProp ?? t('common.search');
83
+ const [innerQuery, setInnerQuery] = useState(defaultQuery);
84
+ const [innerFilters, setInnerFilters] = useState<readonly ActiveFilter[]>(defaultFilters);
85
+ const [view, setView] = useState<string | null>(defaultView);
86
+ const query = queryProp ?? innerQuery;
87
+ const filters = filtersProp ?? innerFilters;
88
+
89
+ const setQuery = (next: string) => {
90
+ setInnerQuery(next);
91
+ onQueryChange?.(next);
92
+ };
93
+ const setFilters = (next: ActiveFilter[], keepView = false) => {
94
+ setInnerFilters(next);
95
+ onFiltersChange?.(next);
96
+ if (!keepView) setView(null);
97
+ };
98
+ const toggle = (filter: ActiveFilter, on: boolean) =>
99
+ setFilters(on ? [...filters.filter((f) => !same(f, filter)), filter] : filters.filter((f) => !same(f, filter)));
100
+ const applyView = (value: string | null) => {
101
+ const picked = views.find((v) => v.value === value);
102
+ if (!picked) return;
103
+ setView(picked.value);
104
+ setFilters([...picked.filters], true);
105
+ if (picked.query !== undefined) setQuery(picked.query);
106
+ onViewChange?.(picked);
107
+ };
108
+ const clearAll = () => {
109
+ setFilters([]);
110
+ setQuery('');
111
+ };
112
+ const labelOf = (id: string) => fields.find((f) => f.id === id)?.label ?? id;
113
+ const hasAny = filters.length > 0 || query !== '';
114
+
115
+ return (
116
+ <div role="search" aria-label={t('filters.label')} className={cn('flex flex-col gap-3', className)}>
117
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
118
+ <Input
119
+ aria-label={searchLabel}
120
+ type="search"
121
+ size="sm"
122
+ leadingIcon={Search}
123
+ placeholder={placeholder}
124
+ value={query}
125
+ onValueChange={(v) => setQuery(String(v))}
126
+ className="w-full sm:max-w-xs"
127
+ />
128
+ <div className="flex flex-wrap items-center gap-2">
129
+ {views.length > 0 && (
130
+ <SimpleSelect
131
+ aria-label={t('filters.savedView')}
132
+ size="sm"
133
+ placeholder={t('filters.savedViews')}
134
+ value={view}
135
+ onValueChange={applyView}
136
+ items={views.map((v) => ({ value: v.value, label: v.label }))}
137
+ className="min-w-[160px]"
138
+ />
139
+ )}
140
+ <Menu>
141
+ <MenuTrigger render={<Button variant="secondary" size="sm" icon={Plus} />}>{t('filters.add')}</MenuTrigger>
142
+ <MenuContent className="max-h-80 min-w-[220px] overflow-y-auto">
143
+ {fields.map((field, i) => (
144
+ <Fragment key={field.id}>
145
+ {i > 0 && <MenuSeparator />}
146
+ <MenuGroup>
147
+ <MenuGroupLabel>{field.label}</MenuGroupLabel>
148
+ {field.options.map((option) => {
149
+ const filter = { field: field.id, value: option };
150
+ return (
151
+ <MenuCheckboxItem
152
+ key={option}
153
+ checked={filters.some((f) => same(f, filter))}
154
+ onCheckedChange={(on) => toggle(filter, on)}
155
+ >
156
+ {option}
157
+ </MenuCheckboxItem>
158
+ );
159
+ })}
160
+ </MenuGroup>
161
+ </Fragment>
162
+ ))}
163
+ </MenuContent>
164
+ </Menu>
165
+ </div>
166
+ {resultCount !== undefined && (
167
+ <p className="text-[12.5px] text-muted-foreground sm:ms-auto" aria-live="polite">
168
+ <span className="font-mono text-foreground tabular-nums">{resultCount.toLocaleString('en-US')}</span>{' '}
169
+ {resultCount === 1 ? 'result' : 'results'}
170
+ </p>
171
+ )}
172
+ </div>
173
+ {hasAny && (
174
+ <div className="flex flex-wrap items-center gap-2">
175
+ <span className="inline-flex items-center gap-1.5 font-mono text-[10px] tracking-[0.14em] text-muted-foreground uppercase">
176
+ <ListFilter size={12} aria-hidden />
177
+ Filters
178
+ </span>
179
+ <ul aria-label={t('filters.active')} className="flex flex-wrap items-center gap-2">
180
+ {filters.map((f) => (
181
+ <li key={`${f.field}:${f.value}`} className="flex">
182
+ <Token
183
+ size="sm"
184
+ prefix={`${labelOf(f.field)}:`}
185
+ label={f.value}
186
+ removeLabel={t('filters.remove', { field: labelOf(f.field), value: f.value })}
187
+ onRemove={() => toggle(f, false)}
188
+ />
189
+ </li>
190
+ ))}
191
+ </ul>
192
+ <Button variant="ghost" size="sm" className="h-7 px-2 hover:text-destructive-text" onClick={clearAll}>
193
+ Clear all
194
+ </Button>
195
+ </div>
196
+ )}
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Filter bar',
5
+ family: 'tables',
6
+ status: 'beta',
7
+ description:
8
+ 'Search Input, saved views Select, an Add filter Menu with multi-select per field, removable filter chips and Clear all.',
9
+ uses: ['Input', 'SimpleSelect', 'Menu', 'MenuCheckboxItem', 'Token', 'Button'],
10
+ };
@@ -0,0 +1,32 @@
1
+ import type { ActiveFilter, FilterField, SavedView } from './FilterBar';
2
+
3
+ export const DEPLOYMENT_FILTER_FIELDS: FilterField[] = [
4
+ { id: 'status', label: 'Status', options: ['Running', 'Queued', 'Paused', 'Degraded', 'Failed'] },
5
+ { id: 'region', label: 'Region', options: ['eu-west-1', 'us-east-1', 'ap-south-1', 'eu-central-1'] },
6
+ { id: 'owner', label: 'Owner', options: ['Platform team', 'Payments team', 'Data team'] },
7
+ ];
8
+
9
+ export const DEPLOYMENT_DEFAULT_FILTERS: ActiveFilter[] = [
10
+ { field: 'status', value: 'Running' },
11
+ { field: 'region', value: 'eu-west-1' },
12
+ ];
13
+
14
+ export const DEPLOYMENT_SAVED_VIEWS: SavedView[] = [
15
+ { value: 'all', label: 'All deployments', filters: [] },
16
+ {
17
+ value: 'attention',
18
+ label: 'Needs attention',
19
+ filters: [
20
+ { field: 'status', value: 'Degraded' },
21
+ { field: 'status', value: 'Failed' },
22
+ ],
23
+ },
24
+ {
25
+ value: 'eu',
26
+ label: 'EU regions',
27
+ filters: [
28
+ { field: 'region', value: 'eu-west-1' },
29
+ { field: 'region', value: 'eu-central-1' },
30
+ ],
31
+ },
32
+ ];
@@ -0,0 +1,41 @@
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 { InlineEditTable } from './InlineEditRow';
5
+
6
+ describe('InlineEditRow', () => {
7
+ it('renders the editable table in read mode', async () => {
8
+ const { container } = render(<InlineEditTable />);
9
+ expect(screen.getByRole('table', { name: 'Members' })).toBeInTheDocument();
10
+ expect(screen.getAllByRole('button', { name: /^Edit / })).toHaveLength(3);
11
+ await expectNoAxeViolations(container);
12
+ });
13
+
14
+ it('edits, saves with Enter and returns focus', async () => {
15
+ const user = userEvent.setup();
16
+ const onRowSave = vi.fn();
17
+ const { container } = render(<InlineEditTable onRowSave={onRowSave} />);
18
+ await user.click(screen.getByRole('button', { name: 'Edit Sam Carter' }));
19
+ const name = screen.getByRole('textbox', { name: 'Name' });
20
+ expect(name).toHaveFocus();
21
+ await expectNoAxeViolations(container);
22
+ await user.clear(name);
23
+ await user.type(name, 'Sam Rivera{Enter}');
24
+ expect(onRowSave).toHaveBeenCalledWith(expect.objectContaining({ id: 'm2', name: 'Sam Rivera', seats: 2 }));
25
+ expect(screen.getByRole('cell', { name: 'Sam Rivera' })).toBeInTheDocument();
26
+ expect(screen.getByRole('button', { name: 'Edit Sam Rivera' })).toHaveFocus();
27
+ });
28
+
29
+ it('cancels with Escape and blocks empty required fields', async () => {
30
+ const user = userEvent.setup();
31
+ render(<InlineEditTable />);
32
+ await user.click(screen.getByRole('button', { name: 'Edit Ana Lopez' }));
33
+ const row = screen.getByRole('textbox', { name: 'Name' }).closest('tr')!;
34
+ await user.clear(screen.getByRole('textbox', { name: 'Name' }));
35
+ expect(within(row).getByRole('button', { name: 'Save' })).toBeDisabled();
36
+ await user.keyboard('{Escape}');
37
+ expect(screen.queryByRole('textbox')).toBeNull();
38
+ expect(screen.getByRole('cell', { name: 'Ana Lopez' })).toBeInTheDocument();
39
+ expect(screen.getByRole('button', { name: 'Edit Ana Lopez' })).toHaveFocus();
40
+ });
41
+ });
@@ -0,0 +1,215 @@
1
+ import { Check, Pencil, X } from '@gntik-ai/icons';
2
+ import {
3
+ cn,
4
+ IconButton,
5
+ Input,
6
+ Table,
7
+ TableBody,
8
+ TableCaption,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ useI18n,
14
+ } from '@gntik-ai/ui';
15
+ import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
16
+ import { EDITABLE_MEMBER_FIELDS, EDITABLE_MEMBERS, type EditableMember } from './fixtures';
17
+
18
+ type Editable = string | number;
19
+
20
+ export interface InlineEditField<T> {
21
+ key: Extract<keyof T, string>;
22
+ label: string;
23
+ type?: 'text' | 'number' | 'email';
24
+ align?: 'left' | 'right';
25
+ required?: boolean;
26
+ /** Read-mode cell classes. */
27
+ className?: string;
28
+ }
29
+
30
+ export interface InlineEditRowProps<T extends object> {
31
+ row: T;
32
+ fields: ReadonlyArray<InlineEditField<T>>;
33
+ /** Called with the edited row on Save (or Enter). */
34
+ onSave?: (next: T) => void;
35
+ onCancel?: () => void;
36
+ /** Row name for the Edit button label ("Edit Ana Lopez"). */
37
+ rowLabel?: string;
38
+ /** Start in edit mode. */
39
+ defaultEditing?: boolean;
40
+ }
41
+
42
+ const read = <T extends object>(row: T, key: string): Editable => {
43
+ const v = (row as Record<string, unknown>)[key];
44
+ return typeof v === 'number' ? v : v == null ? '' : String(v);
45
+ };
46
+
47
+ /**
48
+ * A table row that switches into edit mode: inputs replace the cells, with Save and Cancel.
49
+ * Enter saves, Escape cancels; focus moves to the first input and back to Edit afterwards.
50
+ */
51
+ export function InlineEditRow<T extends object>({
52
+ row,
53
+ fields,
54
+ onSave,
55
+ onCancel,
56
+ rowLabel,
57
+ defaultEditing = false,
58
+ }: InlineEditRowProps<T>) {
59
+ const { t } = useI18n();
60
+ const [editing, setEditing] = useState(defaultEditing);
61
+ const [draft, setDraft] = useState<Record<string, string>>({});
62
+ const editRef = useRef<HTMLButtonElement>(null);
63
+ const firstInputRef = useRef<HTMLInputElement>(null);
64
+ const restoreFocus = useRef(false);
65
+ const label = rowLabel ?? String(read(row, fields[0]?.key ?? ''));
66
+
67
+ useEffect(() => {
68
+ if (editing) firstInputRef.current?.focus();
69
+ else if (restoreFocus.current) {
70
+ restoreFocus.current = false;
71
+ editRef.current?.focus();
72
+ }
73
+ }, [editing]);
74
+
75
+ const start = () => {
76
+ setDraft(Object.fromEntries(fields.map((f) => [f.key, String(read(row, f.key))])));
77
+ setEditing(true);
78
+ };
79
+ const finish = () => {
80
+ restoreFocus.current = true;
81
+ setEditing(false);
82
+ };
83
+ const invalid = (f: InlineEditField<T>) => f.required === true && (draft[f.key] ?? '').trim() === '';
84
+ const save = () => {
85
+ if (fields.some(invalid)) return;
86
+ const next = { ...row } as Record<string, unknown>;
87
+ for (const f of fields) {
88
+ const value = draft[f.key] ?? '';
89
+ next[f.key] = f.type === 'number' ? Number(value) : value;
90
+ }
91
+ onSave?.(next as T);
92
+ finish();
93
+ };
94
+ const cancel = () => {
95
+ onCancel?.();
96
+ finish();
97
+ };
98
+ const onKeyDown = (event: KeyboardEvent<HTMLTableRowElement>) => {
99
+ if (!editing) return;
100
+ if (event.key === 'Escape') {
101
+ event.preventDefault();
102
+ cancel();
103
+ } else if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
104
+ event.preventDefault();
105
+ save();
106
+ }
107
+ };
108
+
109
+ return (
110
+ <TableRow selected={editing} onKeyDown={onKeyDown} data-editing={editing || undefined}>
111
+ {fields.map((f, i) => (
112
+ <TableCell key={f.key} align={f.align} className={cn(!editing && f.className, editing && 'py-1.5')}>
113
+ {editing ? (
114
+ <Input
115
+ ref={i === 0 ? firstInputRef : undefined}
116
+ aria-label={f.label}
117
+ type={f.type ?? 'text'}
118
+ size="sm"
119
+ required={f.required}
120
+ invalid={invalid(f)}
121
+ value={draft[f.key] ?? ''}
122
+ onValueChange={(v) => setDraft((d) => ({ ...d, [f.key]: String(v) }))}
123
+ inputClassName={f.align === 'right' ? 'text-end' : undefined}
124
+ className="min-w-24"
125
+ />
126
+ ) : (
127
+ read(row, f.key)
128
+ )}
129
+ </TableCell>
130
+ ))}
131
+ <TableCell align="right" className="w-24 py-1.5 whitespace-nowrap">
132
+ {editing ? (
133
+ <span className="inline-flex gap-1">
134
+ <IconButton
135
+ icon={Check}
136
+ label={t('common.save')}
137
+ size="sm"
138
+ variant="soft"
139
+ onClick={save}
140
+ disabled={fields.some(invalid)}
141
+ />
142
+ <IconButton icon={X} label={t('common.cancel')} size="sm" variant="ghost" onClick={cancel} />
143
+ </span>
144
+ ) : (
145
+ <IconButton
146
+ ref={editRef}
147
+ icon={Pencil}
148
+ label={t('table.editRow', { label })}
149
+ size="sm"
150
+ variant="ghost"
151
+ onClick={start}
152
+ />
153
+ )}
154
+ </TableCell>
155
+ </TableRow>
156
+ );
157
+ }
158
+
159
+ export interface InlineEditTableProps<T extends object> {
160
+ rows?: readonly T[];
161
+ fields?: ReadonlyArray<InlineEditField<T>>;
162
+ getRowId?: (row: T) => string;
163
+ caption?: string;
164
+ /** Called with the saved row; rows also update locally. */
165
+ onRowSave?: (row: T) => void;
166
+ className?: string;
167
+ }
168
+
169
+ /** Standalone demo of InlineEditRow: a small editable table that keeps its rows in state. */
170
+ export function InlineEditTable<T extends object = EditableMember>({
171
+ rows: initialRows = EDITABLE_MEMBERS as unknown as readonly T[],
172
+ fields = EDITABLE_MEMBER_FIELDS as unknown as ReadonlyArray<InlineEditField<T>>,
173
+ getRowId = (row) => String((row as { id?: unknown }).id ?? ''),
174
+ caption = 'Members',
175
+ onRowSave,
176
+ className,
177
+ }: InlineEditTableProps<T>) {
178
+ const { t } = useI18n();
179
+ const [rows, setRows] = useState<readonly T[]>(initialRows);
180
+ return (
181
+ <div className={cn('overflow-hidden rounded-xl border border-border bg-card shadow-sm', className)}>
182
+ <Table>
183
+ <TableCaption srOnly>{caption}</TableCaption>
184
+ <TableHeader>
185
+ <TableRow className="hover:bg-transparent">
186
+ {fields.map((f) => (
187
+ <TableHead key={f.key} align={f.align}>
188
+ {f.label}
189
+ </TableHead>
190
+ ))}
191
+ <TableHead align="right">
192
+ <span className="sr-only">{t('table.actions')}</span>
193
+ </TableHead>
194
+ </TableRow>
195
+ </TableHeader>
196
+ <TableBody>
197
+ {rows.map((row) => {
198
+ const id = getRowId(row);
199
+ return (
200
+ <InlineEditRow
201
+ key={id}
202
+ row={row}
203
+ fields={fields}
204
+ onSave={(next) => {
205
+ setRows((prev) => prev.map((r) => (getRowId(r) === id ? next : r)));
206
+ onRowSave?.(next);
207
+ }}
208
+ />
209
+ );
210
+ })}
211
+ </TableBody>
212
+ </Table>
213
+ </div>
214
+ );
215
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Inline edit row',
5
+ family: 'tables',
6
+ status: 'beta',
7
+ description:
8
+ 'A table row that switches into edit mode with inputs, Save and Cancel; Enter saves, Escape cancels and focus returns to Edit. InlineEditTable is a ready-made editable table.',
9
+ uses: ['Table', 'Input', 'IconButton'],
10
+ };
@@ -0,0 +1,28 @@
1
+ import type { InlineEditField } from './InlineEditRow';
2
+
3
+ export interface EditableMember {
4
+ id: string;
5
+ name: string;
6
+ email: string;
7
+ role: string;
8
+ seats: number;
9
+ }
10
+
11
+ export const EDITABLE_MEMBER_FIELDS: InlineEditField<EditableMember>[] = [
12
+ { key: 'name', label: 'Name', required: true },
13
+ {
14
+ key: 'email',
15
+ label: 'Email',
16
+ type: 'email',
17
+ required: true,
18
+ className: 'font-mono text-[12px] text-muted-foreground',
19
+ },
20
+ { key: 'role', label: 'Role' },
21
+ { key: 'seats', label: 'Seats', type: 'number', align: 'right', className: 'font-mono text-[12.5px]' },
22
+ ];
23
+
24
+ export const EDITABLE_MEMBERS: EditableMember[] = [
25
+ { id: 'm1', name: 'Ana Lopez', email: 'ana@example.com', role: 'Owner', seats: 3 },
26
+ { id: 'm2', name: 'Sam Carter', email: 'sam@example.com', role: 'Admin', seats: 2 },
27
+ { id: 'm3', name: 'Priya Nair', email: 'priya@example.com', role: 'Member', seats: 1 },
28
+ ];
@@ -0,0 +1,29 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { PaginationFooter } from './PaginationFooter';
5
+
6
+ describe('PaginationFooter', () => {
7
+ it('renders the summary and pagination', async () => {
8
+ const { container } = render(<PaginationFooter />);
9
+ expect(screen.getByText('1–10')).toBeInTheDocument();
10
+ expect(screen.getByRole('navigation', { name: 'Rows pagination' })).toBeInTheDocument();
11
+ expect(screen.getByRole('button', { name: 'Page 10' })).toBeInTheDocument();
12
+ await expectNoAxeViolations(container);
13
+ });
14
+
15
+ it('pages and changes the page size', async () => {
16
+ const user = userEvent.setup();
17
+ const onPageChange = vi.fn();
18
+ const onPageSizeChange = vi.fn();
19
+ render(<PaginationFooter onPageChange={onPageChange} onPageSizeChange={onPageSizeChange} />);
20
+ await user.click(screen.getByRole('button', { name: 'Next page' }));
21
+ expect(onPageChange).toHaveBeenLastCalledWith(2);
22
+ expect(screen.getByText('11–20')).toBeInTheDocument();
23
+ await user.click(screen.getByRole('combobox', { name: 'Rows per page' }));
24
+ await user.click(await screen.findByRole('option', { name: '25' }));
25
+ expect(onPageSizeChange).toHaveBeenCalledWith(25);
26
+ expect(screen.getByText('1–25')).toBeInTheDocument();
27
+ expect(screen.getByRole('button', { name: 'Page 4' })).toBeInTheDocument();
28
+ });
29
+ });