@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,193 @@
1
+ import { Bookmark, ChevronDown } from '@gntik-ai/icons';
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogBody,
6
+ DialogClose,
7
+ DialogContent,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ Input,
12
+ Menu,
13
+ MenuContent,
14
+ MenuGroup,
15
+ MenuGroupLabel,
16
+ MenuItem,
17
+ MenuRadioGroup,
18
+ MenuRadioItem,
19
+ MenuSeparator,
20
+ MenuTrigger,
21
+ } from '@gntik-ai/ui';
22
+ import { useId, useState } from 'react';
23
+ import type { ColumnWidths, PinnedColumns } from './column-sizing';
24
+ import type { DataTableLabels } from './labels';
25
+ import type { SortState } from './data-table-utils';
26
+
27
+ /** What a saved view captures. Every field is optional: a missing field keeps the table's default. */
28
+ export interface DataTableViewState {
29
+ /** Opaque filter state of the page's toolbar (see the table's `filters` / `onFiltersChange`). */
30
+ filters?: unknown;
31
+ sort?: SortState | null;
32
+ hiddenColumns?: readonly string[];
33
+ columnWidths?: ColumnWidths;
34
+ pinnedColumns?: PinnedColumns;
35
+ /** A field key, or `null` for no grouping (function `groupBy`s cannot be saved). */
36
+ groupBy?: string | null;
37
+ }
38
+
39
+ export interface DataTableView {
40
+ id: string;
41
+ name: string;
42
+ state: DataTableViewState;
43
+ }
44
+
45
+ const norm = (s: DataTableViewState) =>
46
+ JSON.stringify({
47
+ filters: s.filters ?? null,
48
+ sort: s.sort ?? null,
49
+ hiddenColumns: [...(s.hiddenColumns ?? [])].sort(),
50
+ columnWidths: Object.fromEntries(Object.entries(s.columnWidths ?? {}).sort(([a], [b]) => a.localeCompare(b))),
51
+ pinnedColumns: { left: s.pinnedColumns?.left ?? [], right: s.pinnedColumns?.right ?? [] },
52
+ groupBy: s.groupBy ?? null,
53
+ });
54
+
55
+ /** Whether two view states are equivalent (key order and empty values ignored). */
56
+ export function sameViewState(a: DataTableViewState, b: DataTableViewState): boolean {
57
+ return norm(a) === norm(b);
58
+ }
59
+
60
+ /** A new view id that does not collide with `views`. */
61
+ export function newViewId(name: string, views: readonly DataTableView[]): string {
62
+ const base =
63
+ name
64
+ .trim()
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, '-')
67
+ .replace(/^-|-$/g, '') || 'view';
68
+ let id = base;
69
+ for (let i = 2; views.some((v) => v.id === id); i++) id = `${base}-${i}`;
70
+ return id;
71
+ }
72
+
73
+ export interface ViewSwitcherProps {
74
+ labels?: Partial<DataTableLabels>;
75
+ views: readonly DataTableView[];
76
+ activeId: string | null;
77
+ /** The table differs from the active view (or from the defaults when none is active). */
78
+ dirty: boolean;
79
+ onSelect: (id: string | null) => void;
80
+ onSaveAs: (name: string) => void;
81
+ onSave: () => void;
82
+ onRename: (id: string, name: string) => void;
83
+ onDelete: (id: string) => void;
84
+ onReset: () => void;
85
+ }
86
+
87
+ const DEFAULT_VALUE = '__default__';
88
+
89
+ /** "Views" menu: switch between saved views, save the current table as a view, rename, delete, reset. */
90
+ export function ViewSwitcher({
91
+ labels,
92
+ views,
93
+ activeId,
94
+ dirty,
95
+ onSelect,
96
+ onSaveAs,
97
+ onSave,
98
+ onRename,
99
+ onDelete,
100
+ onReset,
101
+ }: ViewSwitcherProps) {
102
+ const inputId = useId();
103
+ const active = views.find((v) => v.id === activeId) ?? null;
104
+ const [dialog, setDialog] = useState<'create' | 'rename' | null>(null);
105
+ const [name, setName] = useState('');
106
+ const open = (mode: 'create' | 'rename') => {
107
+ setName(mode === 'rename' && active ? active.name : '');
108
+ setDialog(mode);
109
+ };
110
+ const submit = () => {
111
+ const trimmed = name.trim();
112
+ if (!trimmed) return;
113
+ if (dialog === 'rename' && active) onRename(active.id, trimmed);
114
+ else onSaveAs(trimmed);
115
+ setDialog(null);
116
+ };
117
+ return (
118
+ <>
119
+ <Menu>
120
+ <MenuTrigger render={<Button variant="secondary" size="sm" icon={Bookmark} trailingIcon={ChevronDown} />}>
121
+ <span className="max-w-40 truncate">{active ? active.name : (labels?.defaultView ?? 'Default view')}</span>
122
+ {dirty && (
123
+ <>
124
+ {' '}
125
+ <span aria-hidden className="size-1.5 rounded-full bg-primary" />{' '}
126
+ <span className="sr-only">{labels?.modified ?? '(modified)'}</span>
127
+ </>
128
+ )}
129
+ </MenuTrigger>
130
+ <MenuContent align="end" className="min-w-[220px]">
131
+ <MenuGroup>
132
+ <MenuGroupLabel>{labels?.views ?? 'Views'}</MenuGroupLabel>
133
+ <MenuRadioGroup
134
+ value={activeId ?? DEFAULT_VALUE}
135
+ onValueChange={(v: unknown) => onSelect(v === DEFAULT_VALUE ? null : String(v))}
136
+ >
137
+ <MenuRadioItem value={DEFAULT_VALUE}>{labels?.defaultViewOption ?? 'Default view'}</MenuRadioItem>
138
+ {views.map((v) => (
139
+ <MenuRadioItem key={v.id} value={v.id}>
140
+ {v.name}
141
+ </MenuRadioItem>
142
+ ))}
143
+ </MenuRadioGroup>
144
+ </MenuGroup>
145
+ <MenuSeparator />
146
+ <MenuItem disabled={!active || !dirty} onClick={onSave}>
147
+ {labels?.saveChanges ?? 'Save changes'}
148
+ </MenuItem>
149
+ <MenuItem onClick={() => open('create')}>{labels?.saveAs ?? 'Save as new view…'}</MenuItem>
150
+ <MenuItem disabled={!active} onClick={() => open('rename')}>
151
+ {labels?.renameView ?? 'Rename view…'}
152
+ </MenuItem>
153
+ <MenuItem disabled={!active} onClick={() => active && onDelete(active.id)}>
154
+ {labels?.deleteView ?? 'Delete view'}
155
+ </MenuItem>
156
+ <MenuItem disabled={!dirty} onClick={onReset}>
157
+ {labels?.resetChanges ?? 'Reset changes'}
158
+ </MenuItem>
159
+ </MenuContent>
160
+ </Menu>
161
+ <Dialog open={dialog != null} onOpenChange={(o) => !o && setDialog(null)}>
162
+ <DialogContent size="sm" closeLabel={labels?.closeDialog}>
163
+ <form
164
+ onSubmit={(e) => {
165
+ e.preventDefault();
166
+ submit();
167
+ }}
168
+ >
169
+ <DialogHeader>
170
+ <DialogTitle>
171
+ {dialog === 'rename'
172
+ ? (labels?.renameViewTitle ?? 'Rename view')
173
+ : (labels?.saveViewTitle ?? 'Save view')}
174
+ </DialogTitle>
175
+ </DialogHeader>
176
+ <DialogBody>
177
+ <label htmlFor={inputId} className="mb-1.5 block text-[12.5px] font-medium text-foreground">
178
+ {labels?.viewName ?? 'View name'}
179
+ </label>
180
+ <Input id={inputId} value={name} onChange={(e) => setName(e.target.value)} autoComplete="off" required />
181
+ </DialogBody>
182
+ <DialogFooter>
183
+ <DialogClose render={<Button variant="ghost" />}>{labels?.cancel ?? 'Cancel'}</DialogClose>
184
+ <Button type="submit" disabled={!name.trim()}>
185
+ {dialog === 'rename' ? (labels?.rename ?? 'Rename') : (labels?.saveView ?? 'Save view')}
186
+ </Button>
187
+ </DialogFooter>
188
+ </form>
189
+ </DialogContent>
190
+ </Dialog>
191
+ </>
192
+ );
193
+ }
@@ -0,0 +1,69 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { I18nProvider } from '@gntik-ai/ui';
4
+ import { FilterBar, type FilterBarLabels } from './FilterBar';
5
+
6
+ it('replaces filter chrome including removed chips, saved views and the live result count', async () => {
7
+ const user = userEvent.setup();
8
+ const removeFilter = vi.fn((field: string, value: string) => `Quitar ${field}|${value}`);
9
+ const resultCount = vi.fn((count: number) => `Encontrados: ${count}`);
10
+ const labels: FilterBarLabels = {
11
+ searchLandmark: 'Filtrar',
12
+ searchLabel: 'Buscar registros',
13
+ searchPlaceholder: 'Escriba…',
14
+ savedViewLabel: 'Vista guardada',
15
+ savedViewPlaceholder: 'Vistas guardadas',
16
+ addFilter: 'Añadir filtro',
17
+ activeFilters: 'Filtros activos',
18
+ filters: 'Filtros',
19
+ clearAll: 'Borrar todo',
20
+ removeFilter,
21
+ resultCount,
22
+ };
23
+ const { container } = render(<FilterBar labels={labels} resultCount={1200} />);
24
+ expect(screen.getByRole('search', { name: 'Filtrar' })).toBeVisible();
25
+ expect(screen.getByRole('searchbox', { name: 'Buscar registros' })).toHaveAttribute('placeholder', 'Escriba…');
26
+ expect(screen.getByRole('combobox', { name: 'Vista guardada' })).toHaveTextContent('Vistas guardadas');
27
+ expect(screen.getByRole('list', { name: 'Filtros activos' })).toBeVisible();
28
+ expect(screen.getByText('Filtros', { exact: true })).toBeVisible();
29
+ expect(container.querySelector('[aria-live]')?.textContent).toBe('Encontrados: 1200');
30
+ expect(resultCount).toHaveBeenCalledWith(1200);
31
+ expect(removeFilter).toHaveBeenCalledWith('Status', 'Running');
32
+ const remove = screen.getByRole('button', { name: 'Quitar Status|Running' });
33
+ remove.focus();
34
+ expect(remove).toHaveFocus();
35
+ await user.keyboard('{Delete}');
36
+ expect(screen.queryByRole('button', { name: 'Quitar Status|Running' })).toBeNull();
37
+ await user.click(screen.getByRole('button', { name: 'Añadir filtro' }));
38
+ await user.click(await screen.findByRole('menuitemcheckbox', { name: 'Failed' }));
39
+ await user.keyboard('{Escape}');
40
+ expect(screen.getByRole('button', { name: 'Quitar Status|Failed' })).toBeVisible();
41
+ await user.click(screen.getByRole('button', { name: 'Borrar todo' }));
42
+ expect(screen.queryByRole('list', { name: 'Filtros activos' })).toBeNull();
43
+ await user.click(screen.getByRole('combobox', { name: 'Vista guardada' }));
44
+ expect(await screen.findByRole('option', { name: 'Needs attention' })).toBeVisible();
45
+ });
46
+
47
+ it('retains legacy props, default markup and provider fallbacks for keys that are omitted', () => {
48
+ const { container, rerender } = render(
49
+ <I18nProvider locale="es">
50
+ <FilterBar resultCount={1200} searchLabel="Find" placeholder="Type" />
51
+ </I18nProvider>,
52
+ );
53
+ const original = container.querySelector('[aria-live]')?.outerHTML;
54
+ const landmark = screen.getByRole('search').getAttribute('aria-label');
55
+ rerender(
56
+ <I18nProvider locale="es">
57
+ <FilterBar
58
+ resultCount={1200}
59
+ searchLabel="Find"
60
+ placeholder="Type"
61
+ labels={{ clearAll: 'Clear', searchLabel: 'Override', searchPlaceholder: 'Override type' }}
62
+ />
63
+ </I18nProvider>,
64
+ );
65
+ expect(screen.getByRole('searchbox', { name: 'Override' })).toHaveAttribute('placeholder', 'Override type');
66
+ expect(screen.getByRole('search')).toHaveAttribute('aria-label', landmark);
67
+ expect(container.querySelector('[aria-live]')?.outerHTML).toBe(original);
68
+ expect(screen.getByRole('button', { name: 'Clear' })).toBeVisible();
69
+ });
@@ -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,232 @@
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 FilterBarLabels {
39
+ searchLandmark: string;
40
+ searchLabel: string;
41
+ searchPlaceholder: string;
42
+ savedViewLabel: string;
43
+ savedViewPlaceholder: string;
44
+ addFilter: string;
45
+ activeFilters: string;
46
+ filters: string;
47
+ clearAll: string;
48
+ removeFilter: (field: string, value: string) => string;
49
+ resultCount: (count: number) => string;
50
+ }
51
+
52
+ export interface FilterBarProps {
53
+ /** Per-key overrides take precedence over the legacy search props and provider catalog. */
54
+ labels?: Partial<FilterBarLabels>;
55
+ fields?: readonly FilterField[];
56
+ /** Search text (controlled). */
57
+ query?: string;
58
+ defaultQuery?: string;
59
+ onQueryChange?: (query: string) => void;
60
+ /** Active filters (controlled). */
61
+ filters?: readonly ActiveFilter[];
62
+ defaultFilters?: readonly ActiveFilter[];
63
+ onFiltersChange?: (filters: ActiveFilter[]) => void;
64
+ /** Saved views; picking one applies its query and filters. Empty hides the Select. */
65
+ views?: readonly SavedView[];
66
+ defaultView?: string | null;
67
+ onViewChange?: (view: SavedView) => void;
68
+ searchLabel?: string;
69
+ placeholder?: string;
70
+ /** Optional result count shown at the end ("12 results"). */
71
+ resultCount?: number;
72
+ className?: string;
73
+ }
74
+
75
+ const same = (a: ActiveFilter, b: ActiveFilter) => a.field === b.field && a.value === b.value;
76
+
77
+ /**
78
+ * Filter bar: search Input, saved views Select, an "Add filter" Menu (multi-select per field),
79
+ * removable filter chips (Token) and Clear all.
80
+ */
81
+ export function FilterBar({
82
+ fields = DEPLOYMENT_FILTER_FIELDS,
83
+ query: queryProp,
84
+ defaultQuery = '',
85
+ onQueryChange,
86
+ filters: filtersProp,
87
+ defaultFilters = DEPLOYMENT_DEFAULT_FILTERS,
88
+ onFiltersChange,
89
+ views = DEPLOYMENT_SAVED_VIEWS,
90
+ defaultView = null,
91
+ onViewChange,
92
+ searchLabel: searchLabelProp,
93
+ placeholder: placeholderProp,
94
+ resultCount,
95
+ className,
96
+ labels,
97
+ }: FilterBarProps) {
98
+ const { t } = useI18n();
99
+ const searchLabel = labels?.searchLabel ?? searchLabelProp ?? t('common.search');
100
+ const placeholder = labels?.searchPlaceholder ?? placeholderProp ?? 'Search by name…';
101
+ const [innerQuery, setInnerQuery] = useState(defaultQuery);
102
+ const [innerFilters, setInnerFilters] = useState<readonly ActiveFilter[]>(defaultFilters);
103
+ const [view, setView] = useState<string | null>(defaultView);
104
+ const query = queryProp ?? innerQuery;
105
+ const filters = filtersProp ?? innerFilters;
106
+
107
+ const setQuery = (next: string) => {
108
+ setInnerQuery(next);
109
+ onQueryChange?.(next);
110
+ };
111
+ const setFilters = (next: ActiveFilter[], keepView = false) => {
112
+ setInnerFilters(next);
113
+ onFiltersChange?.(next);
114
+ if (!keepView) setView(null);
115
+ };
116
+ const toggle = (filter: ActiveFilter, on: boolean) =>
117
+ setFilters(on ? [...filters.filter((f) => !same(f, filter)), filter] : filters.filter((f) => !same(f, filter)));
118
+ const applyView = (value: string | null) => {
119
+ const picked = views.find((v) => v.value === value);
120
+ if (!picked) return;
121
+ setView(picked.value);
122
+ setFilters([...picked.filters], true);
123
+ if (picked.query !== undefined) setQuery(picked.query);
124
+ onViewChange?.(picked);
125
+ };
126
+ const clearAll = () => {
127
+ setFilters([]);
128
+ setQuery('');
129
+ };
130
+ const labelOf = (id: string) => fields.find((f) => f.id === id)?.label ?? id;
131
+ const hasAny = filters.length > 0 || query !== '';
132
+
133
+ return (
134
+ <div
135
+ role="search"
136
+ aria-label={labels?.searchLandmark ?? t('filters.label')}
137
+ className={cn('flex flex-col gap-3', className)}
138
+ >
139
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
140
+ <Input
141
+ aria-label={searchLabel}
142
+ type="search"
143
+ size="sm"
144
+ leadingIcon={Search}
145
+ placeholder={placeholder}
146
+ value={query}
147
+ onValueChange={(v) => setQuery(String(v))}
148
+ className="w-full sm:max-w-xs"
149
+ />
150
+ <div className="flex flex-wrap items-center gap-2">
151
+ {views.length > 0 && (
152
+ <SimpleSelect
153
+ aria-label={labels?.savedViewLabel ?? t('filters.savedView')}
154
+ size="sm"
155
+ placeholder={labels?.savedViewPlaceholder ?? t('filters.savedViews')}
156
+ value={view}
157
+ onValueChange={applyView}
158
+ items={views.map((v) => ({ value: v.value, label: v.label }))}
159
+ className="min-w-[160px]"
160
+ />
161
+ )}
162
+ <Menu>
163
+ <MenuTrigger render={<Button variant="secondary" size="sm" icon={Plus} />}>
164
+ {labels?.addFilter ?? t('filters.add')}
165
+ </MenuTrigger>
166
+ <MenuContent className="max-h-80 min-w-[220px] overflow-y-auto">
167
+ {fields.map((field, i) => (
168
+ <Fragment key={field.id}>
169
+ {i > 0 && <MenuSeparator />}
170
+ <MenuGroup>
171
+ <MenuGroupLabel>{field.label}</MenuGroupLabel>
172
+ {field.options.map((option) => {
173
+ const filter = { field: field.id, value: option };
174
+ return (
175
+ <MenuCheckboxItem
176
+ key={option}
177
+ checked={filters.some((f) => same(f, filter))}
178
+ onCheckedChange={(on) => toggle(filter, on)}
179
+ >
180
+ {option}
181
+ </MenuCheckboxItem>
182
+ );
183
+ })}
184
+ </MenuGroup>
185
+ </Fragment>
186
+ ))}
187
+ </MenuContent>
188
+ </Menu>
189
+ </div>
190
+ {resultCount !== undefined && (
191
+ <p className="text-[12.5px] text-muted-foreground sm:ms-auto" aria-live="polite">
192
+ {labels?.resultCount ? (
193
+ labels.resultCount(resultCount)
194
+ ) : (
195
+ <>
196
+ <span className="font-mono text-foreground tabular-nums">{resultCount.toLocaleString('en-US')}</span>{' '}
197
+ {resultCount === 1 ? 'result' : 'results'}
198
+ </>
199
+ )}
200
+ </p>
201
+ )}
202
+ </div>
203
+ {hasAny && (
204
+ <div className="flex flex-wrap items-center gap-2">
205
+ <span className="inline-flex items-center gap-1.5 font-mono text-[10px] tracking-[0.14em] text-muted-foreground uppercase">
206
+ <ListFilter size={12} aria-hidden />
207
+ {labels?.filters ?? 'Filters'}
208
+ </span>
209
+ <ul aria-label={labels?.activeFilters ?? t('filters.active')} className="flex flex-wrap items-center gap-2">
210
+ {filters.map((f) => (
211
+ <li key={`${f.field}:${f.value}`} className="flex">
212
+ <Token
213
+ size="sm"
214
+ prefix={`${labelOf(f.field)}:`}
215
+ label={f.value}
216
+ removeLabel={
217
+ labels?.removeFilter?.(labelOf(f.field), f.value) ??
218
+ t('filters.remove', { field: labelOf(f.field), value: f.value })
219
+ }
220
+ onRemove={() => toggle(f, false)}
221
+ />
222
+ </li>
223
+ ))}
224
+ </ul>
225
+ <Button variant="ghost" size="sm" className="h-7 px-2 hover:text-destructive-text" onClick={clearAll}>
226
+ {labels?.clearAll ?? 'Clear all'}
227
+ </Button>
228
+ </div>
229
+ )}
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,32 @@
1
+ import type { BlockMeta } from '../../meta';
2
+ import type { FilterBarLabels } from './FilterBar';
3
+
4
+ export const meta: BlockMeta = {
5
+ name: 'Filter bar',
6
+ family: 'tables',
7
+ status: 'beta',
8
+ description:
9
+ 'Search Input, saved views Select, an Add filter Menu with multi-select per field, removable filter chips and Clear all.',
10
+ labels: [
11
+ { key: 'searchLandmark', type: 'string', state: 'Always: search landmark name' },
12
+ { key: 'searchLabel', type: 'string', state: 'Always: search input accessible name' },
13
+ { key: 'searchPlaceholder', type: 'string', state: 'Always: search input placeholder' },
14
+ { key: 'savedViewLabel', type: 'string', state: 'When views exist: Select accessible name' },
15
+ { key: 'savedViewPlaceholder', type: 'string', state: 'When no view selected: Select placeholder' },
16
+ { key: 'addFilter', type: 'string', state: 'Always: Add filter trigger text and name' },
17
+ { key: 'activeFilters', type: 'string', state: 'When filters or search exist: active-filters list name' },
18
+ { key: 'filters', type: 'string', state: 'When filters or search exist: chip section heading' },
19
+ { key: 'clearAll', type: 'string', state: 'When filters or search exist: clear button text and name' },
20
+ {
21
+ key: 'removeFilter',
22
+ type: '(field: string, value: string) => string',
23
+ state: 'When chip exists: remove button name; field and value are caller content',
24
+ },
25
+ {
26
+ key: 'resultCount',
27
+ type: '(count: number) => string',
28
+ state: 'When resultCount supplied: visible count and polite live region',
29
+ },
30
+ ] satisfies Array<{ key: keyof FilterBarLabels; type: string; state: string }>,
31
+ uses: ['Input', 'SimpleSelect', 'Menu', 'MenuCheckboxItem', 'Token', 'Button'],
32
+ };
@@ -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
+ });