@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,35 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { MfaChallenge } from './MfaChallenge';
5
+
6
+ describe('MfaChallenge', () => {
7
+ it('renders the code step with a resend countdown', async () => {
8
+ const { container } = render(<MfaChallenge destination="a•••@example.com" onResend={() => {}} />);
9
+ expect(screen.getByText(/sent a 6-digit code to a•••@example.com/)).toBeInTheDocument();
10
+ expect(screen.getByRole('button', { name: 'Resend code in 30s' })).toBeDisabled();
11
+ await expectNoAxeViolations(container);
12
+ });
13
+
14
+ it('auto-submits a complete code', async () => {
15
+ const onVerify = vi.fn();
16
+ render(<MfaChallenge onVerify={onVerify} />);
17
+ await userEvent.click(screen.getAllByRole('textbox')[0]!);
18
+ await userEvent.keyboard('123456');
19
+ expect(onVerify).toHaveBeenCalledWith('123456', 'code');
20
+ });
21
+
22
+ it('switches to a backup code and resends with a new countdown', async () => {
23
+ const onVerify = vi.fn();
24
+ const onResend = vi.fn();
25
+ const { container } = render(<MfaChallenge onVerify={onVerify} onResend={onResend} initialCooldown={0} />);
26
+ await userEvent.click(screen.getByRole('button', { name: 'Resend code' }));
27
+ expect(onResend).toHaveBeenCalledOnce();
28
+ expect(screen.getByRole('button', { name: 'Resend code in 30s' })).toBeDisabled();
29
+ await userEvent.click(screen.getByRole('button', { name: 'Use a backup code' }));
30
+ await userEvent.type(screen.getByLabelText('Backup code'), 'abcd-efgh');
31
+ await userEvent.click(screen.getByRole('button', { name: 'Verify' }));
32
+ expect(onVerify).toHaveBeenCalledWith('abcd-efgh', 'backup');
33
+ await expectNoAxeViolations(container);
34
+ });
35
+ });
@@ -0,0 +1,170 @@
1
+ import { Alert, Button, Field, FieldDescription, FieldError, FieldLabel, Input, OtpInput, cn } from '@gntik-ai/ui';
2
+ import { useEffect, useState, type ReactNode } from 'react';
3
+ import { AuthHeader, authFormClass, useAsyncSubmit } from '../shared';
4
+
5
+ export type MfaMethod = 'code' | 'backup';
6
+
7
+ export interface MfaChallengeProps {
8
+ /** Verifies the code. A returned promise shows the loading state; a rejection shows in the alert. */
9
+ onVerify?: (code: string, method: MfaMethod) => void | Promise<void>;
10
+ /** Sends a new code; the resend button then waits `resendCooldown` seconds. */
11
+ onResend?: () => void | Promise<void>;
12
+ /** Seconds between resends. */
13
+ resendCooldown?: number;
14
+ /** Seconds left before the first resend (a code was just sent). Defaults to `resendCooldown`. */
15
+ initialCooldown?: number;
16
+ /** Code length. */
17
+ length?: 6 | 8;
18
+ /** Where the code was sent, e.g. "a•••@example.com". Omit for authenticator apps. */
19
+ destination?: string;
20
+ error?: string | null;
21
+ loading?: boolean;
22
+ /** Called when the user gives up ("Back to sign in"). */
23
+ onCancel?: () => void;
24
+ eyebrow?: ReactNode;
25
+ title?: ReactNode;
26
+ className?: string;
27
+ }
28
+
29
+ /**
30
+ * Second-factor step: a one-time code (auto-submits when complete), a switch to a backup code,
31
+ * and a resend button with a countdown.
32
+ */
33
+ export function MfaChallenge({
34
+ onVerify,
35
+ onResend,
36
+ resendCooldown = 30,
37
+ initialCooldown,
38
+ length = 6,
39
+ destination,
40
+ error: errorProp,
41
+ loading: loadingProp,
42
+ onCancel,
43
+ eyebrow = 'Two-step verification',
44
+ title = 'Enter your verification code',
45
+ className,
46
+ }: MfaChallengeProps) {
47
+ const [method, setMethod] = useState<MfaMethod>('code');
48
+ const [code, setCode] = useState('');
49
+ const [backup, setBackup] = useState('');
50
+ const [submitted, setSubmitted] = useState(false);
51
+ // One start time for both, so the first render shows the full cooldown (not cooldown + 1s).
52
+ const [start] = useState(() => Date.now());
53
+ const [now, setNow] = useState(start);
54
+ const [resendAt, setResendAt] = useState(() => start + (initialCooldown ?? resendCooldown) * 1000);
55
+ const [resent, setResent] = useState(false);
56
+ const { loading, error, run } = useAsyncSubmit('That code did not work. Try again.');
57
+ const busy = loadingProp ?? loading;
58
+ const alert = errorProp ?? error;
59
+ const remaining = Math.max(0, Math.ceil((resendAt - now) / 1000));
60
+ const value = method === 'code' ? code : backup.trim();
61
+ const invalid = submitted && (method === 'code' ? code.length < length : backup.trim() === '');
62
+
63
+ useEffect(() => {
64
+ if (remaining <= 0) return;
65
+ const id = window.setInterval(() => setNow(Date.now()), 1000);
66
+ return () => window.clearInterval(id);
67
+ }, [remaining]);
68
+
69
+ const verify = (v: string) => {
70
+ setSubmitted(true);
71
+ if (method === 'code' ? v.length < length : v === '') return;
72
+ void run(() => onVerify?.(v, method));
73
+ };
74
+
75
+ const resend = async () => {
76
+ const t = Date.now();
77
+ setNow(t);
78
+ setResendAt(t + resendCooldown * 1000);
79
+ setResent(true);
80
+ await onResend?.();
81
+ };
82
+
83
+ const description =
84
+ method === 'backup'
85
+ ? 'Enter one of the backup codes you saved when you turned on two-step verification. Each code works once.'
86
+ : destination
87
+ ? `We sent a ${length}-digit code to ${destination}.`
88
+ : `Open your authenticator app and enter the ${length}-digit code.`;
89
+
90
+ return (
91
+ <div className={cn(authFormClass, className)}>
92
+ <AuthHeader
93
+ eyebrow={eyebrow}
94
+ title={method === 'backup' ? 'Use a backup code' : title}
95
+ description={description}
96
+ />
97
+ <form
98
+ noValidate
99
+ aria-label="Verify your identity"
100
+ className="flex flex-col gap-5"
101
+ onSubmit={(e) => {
102
+ e.preventDefault();
103
+ verify(value);
104
+ }}
105
+ >
106
+ {alert && <Alert tone="destructive" description={alert} />}
107
+ {method === 'code' ? (
108
+ <Field invalid={invalid}>
109
+ <FieldLabel>Verification code</FieldLabel>
110
+ <OtpInput
111
+ length={length}
112
+ value={code}
113
+ onValueChange={setCode}
114
+ onComplete={verify}
115
+ invalid={invalid}
116
+ groupSize={length / 2}
117
+ disabled={busy}
118
+ />
119
+ <FieldError match={invalid}>Enter all {length} digits.</FieldError>
120
+ </Field>
121
+ ) : (
122
+ <Field invalid={invalid}>
123
+ <FieldLabel>Backup code</FieldLabel>
124
+ <Input
125
+ value={backup}
126
+ onValueChange={setBackup}
127
+ placeholder="xxxx-xxxx"
128
+ autoComplete="one-time-code"
129
+ spellCheck={false}
130
+ inputClassName="font-mono tracking-wider"
131
+ />
132
+ <FieldDescription>Backup codes are 8 characters, with or without the dash.</FieldDescription>
133
+ <FieldError match={invalid}>Enter a backup code.</FieldError>
134
+ </Field>
135
+ )}
136
+ <Button type="submit" size="lg" loading={busy} className="w-full">
137
+ Verify
138
+ </Button>
139
+ </form>
140
+ <div className="mt-6 flex flex-wrap items-center justify-between gap-3 text-[13px]">
141
+ <Button
142
+ variant="ghost"
143
+ size="sm"
144
+ aria-pressed={method === 'backup'}
145
+ onClick={() => {
146
+ setMethod(method === 'code' ? 'backup' : 'code');
147
+ setSubmitted(false);
148
+ }}
149
+ >
150
+ {method === 'code' ? 'Use a backup code' : 'Use a verification code'}
151
+ </Button>
152
+ {method === 'code' && onResend !== undefined && (
153
+ <Button variant="ghost" size="sm" disabled={remaining > 0} onClick={() => void resend()}>
154
+ {remaining > 0 ? `Resend code in ${remaining}s` : 'Resend code'}
155
+ </Button>
156
+ )}
157
+ </div>
158
+ <span role="status" className="sr-only">
159
+ {resent ? 'A new code was sent.' : ''}
160
+ </span>
161
+ {onCancel && (
162
+ <p className="mt-6 text-center">
163
+ <Button variant="ghost" size="sm" onClick={onCancel}>
164
+ Back to sign in
165
+ </Button>
166
+ </p>
167
+ )}
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,9 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'MFA challenge',
5
+ family: 'auth',
6
+ status: 'stable',
7
+ description: 'Second-factor step with a one-time code (auto-submit), a backup-code mode and resend with a countdown.',
8
+ uses: ['OtpInput', 'Field', 'Input', 'Button', 'Alert'],
9
+ };
@@ -0,0 +1,27 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { ResetPasswordForm } from './ResetPasswordForm';
5
+
6
+ describe('ResetPasswordForm', () => {
7
+ it('renders the form', async () => {
8
+ const { container } = render(<ResetPasswordForm />);
9
+ expect(screen.getByRole('form', { name: 'Set a new password' })).toBeInTheDocument();
10
+ await expectNoAxeViolations(container);
11
+ });
12
+
13
+ it('flags a mismatch, then saves and shows the success state', async () => {
14
+ const onSubmit = vi.fn();
15
+ const { container } = render(<ResetPasswordForm onSubmit={onSubmit} />);
16
+ await userEvent.type(screen.getByLabelText('New password'), 'correct-horse-42');
17
+ await userEvent.type(screen.getByLabelText('Confirm password'), 'correct-horse');
18
+ await userEvent.click(screen.getByRole('button', { name: 'Update password' }));
19
+ expect(screen.getByText("Passwords don't match.")).toBeInTheDocument();
20
+ expect(onSubmit).not.toHaveBeenCalled();
21
+ await expectNoAxeViolations(container);
22
+ await userEvent.type(screen.getByLabelText('Confirm password'), '-42');
23
+ await userEvent.click(screen.getByRole('button', { name: 'Update password' }));
24
+ expect(onSubmit).toHaveBeenCalledWith('correct-horse-42');
25
+ expect(await screen.findByRole('heading', { name: 'Password updated' })).toBeInTheDocument();
26
+ });
27
+ });
@@ -0,0 +1,100 @@
1
+ import { Alert, Button, Field, FieldError, FieldLabel, PasswordInput, cn } from '@gntik-ai/ui';
2
+ import { CircleCheck } from '@gntik-ai/icons';
3
+ import { useState, type ReactNode } from 'react';
4
+ import { AuthHeader, authFormClass, useAsyncSubmit } from '../shared';
5
+
6
+ export interface ResetPasswordFormProps {
7
+ /** Saves the new password. A returned promise shows the loading state; a rejection shows in the alert. */
8
+ onSubmit?: (password: string) => void | Promise<void>;
9
+ error?: string | null;
10
+ minPasswordLength?: number;
11
+ /** Called from the success state ("Continue to sign in"). */
12
+ onContinue?: () => void;
13
+ eyebrow?: ReactNode;
14
+ className?: string;
15
+ }
16
+
17
+ /** New password + confirmation with length and mismatch validation, then a success state. */
18
+ export function ResetPasswordForm({
19
+ onSubmit,
20
+ error: errorProp,
21
+ minPasswordLength = 8,
22
+ onContinue,
23
+ eyebrow = 'Account recovery',
24
+ className,
25
+ }: ResetPasswordFormProps) {
26
+ const [password, setPassword] = useState('');
27
+ const [confirm, setConfirm] = useState('');
28
+ const [confirmTouched, setConfirmTouched] = useState(false);
29
+ const [submitted, setSubmitted] = useState(false);
30
+ const [done, setDone] = useState(false);
31
+ const { loading, error, run } = useAsyncSubmit('Your password could not be updated.');
32
+ const alert = errorProp ?? error;
33
+ const tooShort = password.length < minPasswordLength;
34
+ const mismatch = confirm !== password;
35
+ const passwordInvalid = submitted && tooShort;
36
+ const confirmInvalid = (submitted || (confirmTouched && confirm !== '')) && mismatch;
37
+
38
+ if (done) {
39
+ return (
40
+ <div className={cn(authFormClass, className)}>
41
+ <span className="mb-5 grid size-11 place-items-center rounded-xl bg-success/14 text-success-chip-text">
42
+ <CircleCheck size={20} aria-hidden />
43
+ </span>
44
+ <AuthHeader
45
+ eyebrow={eyebrow}
46
+ title="Password updated"
47
+ description="You can now sign in with your new password. Other sessions were signed out."
48
+ />
49
+ <div role="status" className="sr-only">
50
+ Password updated.
51
+ </div>
52
+ <Button size="lg" className="w-full" onClick={onContinue}>
53
+ Continue to sign in
54
+ </Button>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <div className={cn(authFormClass, className)}>
61
+ <AuthHeader
62
+ eyebrow={eyebrow}
63
+ title="Set a new password"
64
+ description={`Use at least ${minPasswordLength} characters. Avoid passwords you use elsewhere.`}
65
+ />
66
+ <form
67
+ noValidate
68
+ aria-label="Set a new password"
69
+ className="flex flex-col gap-4"
70
+ onSubmit={(e) => {
71
+ e.preventDefault();
72
+ setSubmitted(true);
73
+ if (tooShort || mismatch) return;
74
+ void run(() => onSubmit?.(password)).then((ok) => ok && setDone(true));
75
+ }}
76
+ >
77
+ {alert && <Alert tone="destructive" description={alert} />}
78
+ <Field invalid={passwordInvalid}>
79
+ <FieldLabel>New password</FieldLabel>
80
+ <PasswordInput showStrength value={password} onValueChange={setPassword} required />
81
+ <FieldError match={passwordInvalid}>Use at least {minPasswordLength} characters.</FieldError>
82
+ </Field>
83
+ <Field invalid={confirmInvalid}>
84
+ <FieldLabel>Confirm password</FieldLabel>
85
+ <PasswordInput
86
+ autoComplete="new-password"
87
+ value={confirm}
88
+ onValueChange={setConfirm}
89
+ onBlur={() => setConfirmTouched(true)}
90
+ required
91
+ />
92
+ <FieldError match={confirmInvalid}>Passwords don't match.</FieldError>
93
+ </Field>
94
+ <Button type="submit" size="lg" loading={loading} className="mt-2 w-full">
95
+ Update password
96
+ </Button>
97
+ </form>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Reset password form',
5
+ family: 'auth',
6
+ status: 'stable',
7
+ description:
8
+ 'New password with a strength meter and a confirmation field with mismatch validation, then a success state.',
9
+ uses: ['Field', 'PasswordInput', 'Button', 'Alert'],
10
+ };
@@ -0,0 +1,222 @@
1
+ import { ThemeProvider } from '@gntik-ai/ui';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { createRef } from 'react';
5
+ import { expectNoAxeViolations } from '../../test/a11y';
6
+ import { SignInForm, type SignInFormRef } from './SignInForm';
7
+
8
+ describe('SignInForm', () => {
9
+ it('can hide or replace the sign-up prompt alongside the SSO slot', () => {
10
+ const { rerender } = render(<SignInForm sso={null} signUpPrompt={null} />);
11
+ expect(screen.queryByText(/New here/)).not.toBeInTheDocument();
12
+ expect(screen.queryByRole('link', { name: 'Create an account' })).not.toBeInTheDocument();
13
+ expect(screen.queryByText('Or continue with')).not.toBeInTheDocument();
14
+ rerender(<SignInForm signUpPrompt="Ask your administrator for access." />);
15
+ expect(screen.getByText('Ask your administrator for access.')).toBeInTheDocument();
16
+ expect(screen.queryByText(/New here/)).not.toBeInTheDocument();
17
+ });
18
+
19
+ it('suppresses or changes the heading level while retaining an accessible form name', () => {
20
+ const { container, rerender } = render(<SignInForm headingLevel={null} />);
21
+ expect(container.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]')).toBeNull();
22
+ expect(screen.getByRole('form', { name: 'Sign in' })).toBeInTheDocument();
23
+ rerender(<SignInForm headingLevel="h2" />);
24
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Sign in to your workspace');
25
+ rerender(<SignInForm headingLevel={null} aria-label="Account access" />);
26
+ expect(screen.getByRole('form', { name: 'Account access' })).toBeInTheDocument();
27
+ rerender(
28
+ <>
29
+ <h1 id="page-title">Account access</h1>
30
+ <SignInForm headingLevel={null} aria-labelledby="page-title" />
31
+ </>,
32
+ );
33
+ expect(screen.getByRole('form', { name: 'Account access' })).toHaveAttribute('aria-labelledby', 'page-title');
34
+ });
35
+
36
+ it('associates server feedback with both invalid fields and exposes password focus', () => {
37
+ const ref = createRef<SignInFormRef>();
38
+ const { rerender } = render(
39
+ <SignInForm
40
+ ref={ref}
41
+ identifier={{ help: 'Account email', helpId: 'email-help' }}
42
+ passwordHelp="Account password"
43
+ passwordHelpId="password-help"
44
+ fieldInvalid={{ identifier: true, password: true }}
45
+ feedbackId="credential-feedback"
46
+ aria-describedby="form-feedback"
47
+ error="Credentials were not accepted."
48
+ />,
49
+ );
50
+ expect(screen.getByRole('form', { name: 'Sign in' })).toHaveAttribute('aria-describedby', 'form-feedback');
51
+ for (const label of ['Email', 'Password']) {
52
+ const input = screen.getByLabelText(label);
53
+ expect(input).toHaveAttribute('aria-invalid', 'true');
54
+ expect(input.getAttribute('aria-describedby')?.split(' ')).toEqual(
55
+ expect.arrayContaining([label === 'Email' ? 'email-help' : 'password-help', 'credential-feedback']),
56
+ );
57
+ }
58
+ expect(screen.getByRole('alert')).toHaveAttribute('id', 'credential-feedback');
59
+ ref.current?.focus('password');
60
+ expect(screen.getByLabelText('Password')).toHaveFocus();
61
+ ref.current?.focus('identifier');
62
+ expect(screen.getByLabelText('Email')).toHaveFocus();
63
+ rerender(<SignInForm ref={ref} />);
64
+ expect(screen.getByLabelText('Email')).not.toHaveAttribute('aria-invalid');
65
+ expect(screen.getByLabelText('Password')).not.toHaveAttribute('aria-invalid');
66
+ expect(screen.getByLabelText('Password')).not.toHaveAttribute('aria-describedby');
67
+ });
68
+
69
+ it('validates with consumer messages, focuses the first invalid field and clears error associations', async () => {
70
+ const onSubmit = vi.fn();
71
+ render(
72
+ <SignInForm
73
+ identifier={{ type: 'text', label: 'Username', help: 'Account name', helpId: 'username-help', minLength: 3 }}
74
+ passwordHelp="Account password"
75
+ passwordHelpId="password-help"
76
+ messages={{
77
+ identifierRequired: 'Username is required.',
78
+ identifierInvalid: 'Use at least three characters.',
79
+ passwordRequired: 'Password is required.',
80
+ }}
81
+ onSubmit={onSubmit}
82
+ sso={null}
83
+ />,
84
+ );
85
+ const input = screen.getByLabelText('Username');
86
+ const password = screen.getByLabelText('Password');
87
+ expect(input).not.toHaveAttribute('aria-invalid');
88
+ expect(password).not.toHaveAttribute('aria-invalid');
89
+ expect(screen.getByRole('form', { name: 'Sign in' })).toHaveAttribute('novalidate');
90
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
91
+ expect(input).toHaveFocus();
92
+ expect(onSubmit).not.toHaveBeenCalled();
93
+ for (const [field, message, helpId] of [
94
+ [input, 'Username is required.', 'username-help'],
95
+ [password, 'Password is required.', 'password-help'],
96
+ ] as const) {
97
+ const error = screen.getByText(message).closest('[role="alert"]');
98
+ expect(error).toBeInTheDocument();
99
+ expect(field).toHaveAttribute('aria-invalid', 'true');
100
+ expect(field.getAttribute('aria-describedby')?.split(' ')).toEqual(expect.arrayContaining([helpId, error?.id]));
101
+ }
102
+ await userEvent.type(input, 'ab');
103
+ expect(screen.getByText('Use at least three characters.')).toBeInTheDocument();
104
+ await userEvent.type(input, 'c');
105
+ expect(input).not.toHaveAttribute('aria-invalid');
106
+ expect(input).toHaveAccessibleDescription('Account name');
107
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
108
+ expect(password).toHaveFocus();
109
+ await userEvent.type(password, 'secret');
110
+ expect(password).not.toHaveAttribute('aria-invalid');
111
+ expect(password).toHaveAccessibleDescription('Account password');
112
+ });
113
+
114
+ it('accepts a username configuration and submits the trimmed identifier without changing email payloads', async () => {
115
+ const onSubmit = vi.fn();
116
+ render(
117
+ <SignInForm
118
+ identifier={{
119
+ type: 'text',
120
+ autoComplete: 'username',
121
+ label: 'Username',
122
+ help: 'Use your account name.',
123
+ minLength: 3,
124
+ maxLength: 120,
125
+ }}
126
+ onSubmit={onSubmit}
127
+ sso={null}
128
+ />,
129
+ );
130
+ const input = screen.getByRole('textbox', { name: 'Username' });
131
+ expect(input).toHaveAttribute('type', 'text');
132
+ expect(input).toHaveAttribute('autocomplete', 'username');
133
+ expect(input).toHaveAttribute('minlength', '3');
134
+ expect(input).toHaveAttribute('maxlength', '120');
135
+ expect(input).toHaveAccessibleDescription('Use your account name.');
136
+ await userEvent.type(input, ' avery ');
137
+ await userEvent.type(screen.getByLabelText('Password'), 'hunter22');
138
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
139
+ expect(onSubmit).toHaveBeenCalledWith({
140
+ email: 'avery',
141
+ identifier: 'avery',
142
+ password: 'hunter22',
143
+ remember: true,
144
+ });
145
+ });
146
+
147
+ it('renders with SSO by default', async () => {
148
+ const { container } = render(<SignInForm />);
149
+ expect(screen.getByRole('heading', { level: 1, name: 'Sign in to your workspace' })).toBeInTheDocument();
150
+ expect(screen.getByRole('group', { name: 'Single sign-on' })).toBeInTheDocument();
151
+ await expectNoAxeViolations(container);
152
+ });
153
+
154
+ it('validates, submits and shows a rejection as an alert', async () => {
155
+ const onSubmit = vi.fn().mockRejectedValue(new Error('Incorrect email or password.'));
156
+ render(<SignInForm onSubmit={onSubmit} sso={null} />);
157
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
158
+ expect(screen.getByText('Enter a valid email address.')).toBeInTheDocument();
159
+ expect(onSubmit).not.toHaveBeenCalled();
160
+ await userEvent.type(screen.getByLabelText('Email'), 'avery@example.com');
161
+ await userEvent.type(screen.getByLabelText('Password'), 'hunter22');
162
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
163
+ expect(onSubmit).toHaveBeenCalledWith({ email: 'avery@example.com', password: 'hunter22', remember: true });
164
+ expect(await screen.findByRole('alert')).toHaveTextContent('Incorrect email or password.');
165
+ });
166
+ });
167
+
168
+ // Public rendering seams: identifier mode, theme, heading policy and validation/recovery state.
169
+ describe('SignInForm accessibility matrix', () => {
170
+ for (const theme of ['dark', 'light', 'high_contrast'] as const) {
171
+ for (const mode of ['email', 'username'] as const) {
172
+ for (const suppressed of [false, true]) {
173
+ for (const state of ['initial', 'errors'] as const) {
174
+ it(`${theme} / ${mode} / headings ${!suppressed} / ${state}`, async () => {
175
+ const onSubmit = vi.fn();
176
+ const { container } = render(
177
+ <ThemeProvider defaultMode={theme} storageKey={null}>
178
+ <h1>Account access</h1>
179
+ <SignInForm
180
+ identifier={
181
+ mode === 'username'
182
+ ? {
183
+ type: 'text',
184
+ autoComplete: 'username',
185
+ label: 'Username',
186
+ help: 'Use your account name.',
187
+ minLength: 3,
188
+ maxLength: 120,
189
+ }
190
+ : undefined
191
+ }
192
+ headingLevel={suppressed ? null : undefined}
193
+ messages={{
194
+ identifierRequired: 'Account name is required.',
195
+ passwordRequired: 'Password is required.',
196
+ }}
197
+ onSubmit={onSubmit}
198
+ />
199
+ </ThemeProvider>,
200
+ );
201
+ if (state === 'initial' || state === 'errors') {
202
+ const input = screen.getByRole('textbox', { name: mode === 'username' ? 'Username' : 'Email' });
203
+ expect(input).toHaveAttribute('type', mode === 'username' ? 'text' : 'email');
204
+ expect(input).toHaveAttribute('autocomplete', mode === 'username' ? 'username' : 'email');
205
+ expect(screen.getByRole('form', { name: 'Sign in' })).toHaveAttribute('novalidate');
206
+ if (state === 'errors') {
207
+ await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
208
+ expect(input).toHaveFocus();
209
+ expect(input).toHaveAttribute('aria-invalid', 'true');
210
+ expect(onSubmit).not.toHaveBeenCalled();
211
+ expect(screen.getByText('Account name is required.')).toBeInTheDocument();
212
+ expect(screen.getByLabelText('Password')).toHaveAttribute('aria-invalid', 'true');
213
+ }
214
+ }
215
+ if (suppressed) expect(container.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]')).toHaveLength(1);
216
+ await expectNoAxeViolations(container);
217
+ });
218
+ }
219
+ }
220
+ }
221
+ }
222
+ });