@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,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Create API key dialog',
5
+ family: 'forms',
6
+ status: 'stable',
7
+ description:
8
+ 'Two-step dialog: name, scopes and expiry, then the secret shown once in a copyable CodeBlock with a warning.',
9
+ uses: ['Dialog', 'Field', 'Input', 'CheckboxGroup', 'Checkbox', 'Select', 'CodeBlock', 'Alert', 'Button'],
10
+ };
@@ -0,0 +1,31 @@
1
+ export interface ApiKeyScope {
2
+ value: string;
3
+ label: string;
4
+ description?: string;
5
+ }
6
+
7
+ export interface ApiKeyExpiry {
8
+ value: string;
9
+ label: string;
10
+ }
11
+
12
+ export const apiKeyScopes: ApiKeyScope[] = [
13
+ { value: 'projects:read', label: 'projects:read', description: 'List and read projects and deployments.' },
14
+ { value: 'projects:write', label: 'projects:write', description: 'Create, update and deploy projects.' },
15
+ { value: 'members:read', label: 'members:read', description: 'Read workspace members and roles.' },
16
+ { value: 'billing:read', label: 'billing:read', description: 'Read invoices and usage.' },
17
+ ];
18
+
19
+ export const apiKeyExpiries: ApiKeyExpiry[] = [
20
+ { value: '30d', label: '30 days' },
21
+ { value: '90d', label: '90 days' },
22
+ { value: '1y', label: '1 year' },
23
+ { value: 'never', label: 'No expiry' },
24
+ ];
25
+
26
+ /** Demo secret generator (the real secret comes from your API). */
27
+ export function generateDemoSecret(prefix = 'sk_live_') {
28
+ const bytes = new Uint8Array(24);
29
+ globalThis.crypto.getRandomValues(bytes);
30
+ return prefix + Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
31
+ }
@@ -0,0 +1,339 @@
1
+ import { act, render, screen, waitFor, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { useState } from 'react';
4
+ import { expectNoAxeViolations } from '../../test/a11y';
5
+ import {
6
+ CredentialDisclosureDialog,
7
+ type CredentialDisclosure,
8
+ type CredentialDisclosureDialogProps,
9
+ } from './CredentialDisclosureDialog';
10
+
11
+ const stored: CredentialDisclosure = {
12
+ variant: 'stored',
13
+ credentialId: 'cred_demo_01',
14
+ expiresAt: 'January 1, 2030',
15
+ secret: 'synthetic_secret_for_tests_only',
16
+ };
17
+
18
+ function RequestHarness({ request }: { request: () => Promise<CredentialDisclosure> }) {
19
+ const [disclosure, setDisclosure] = useState<CredentialDisclosure | null>(null);
20
+ return (
21
+ <>
22
+ <button
23
+ onClick={() => {
24
+ void request()
25
+ .then(setDisclosure)
26
+ .catch(() => {});
27
+ }}
28
+ >
29
+ Request
30
+ </button>
31
+ <CredentialDisclosureDialog disclosure={disclosure} onClose={() => setDisclosure(null)} />
32
+ </>
33
+ );
34
+ }
35
+
36
+ it('renders nothing until a successful response, including pending and rejected requests', async () => {
37
+ const user = userEvent.setup();
38
+ let resolve!: (value: CredentialDisclosure) => void;
39
+ let reject!: (reason: Error) => void;
40
+ const pending = new Promise<CredentialDisclosure>((yes, no) => {
41
+ resolve = yes;
42
+ reject = no;
43
+ });
44
+ const { rerender } = render(<RequestHarness request={() => pending} />);
45
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
46
+ expect(document.body.innerHTML).not.toContain(stored.secret);
47
+ await user.click(screen.getByRole('button', { name: 'Request' }));
48
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
49
+ await act(async () => reject(new Error('Synthetic rejection')));
50
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
51
+ expect(document.body.innerHTML).not.toContain(stored.secret);
52
+ const success = new Promise<CredentialDisclosure>((yes) => {
53
+ resolve = yes;
54
+ });
55
+ rerender(<RequestHarness request={() => success} />);
56
+ await user.click(screen.getByRole('button', { name: 'Request' }));
57
+ await act(async () => resolve(stored));
58
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
59
+ expect(document.body.innerHTML).not.toContain(stored.secret);
60
+ });
61
+
62
+ const labels = {
63
+ title: 'Credential value',
64
+ description: 'Keep this value safe.',
65
+ credentialIdLabel: 'Identifier',
66
+ expiryLabel: 'Valid until',
67
+ secretLabel: 'Secret value',
68
+ showLabel: 'Reveal value',
69
+ hideLabel: 'Mask value',
70
+ copyLabel: 'Copy value',
71
+ copiedLabel: 'Saved',
72
+ successAnnouncement: 'Value saved safely',
73
+ failureAnnouncement: 'Select and copy manually.',
74
+ closeLabel: 'Dismiss',
75
+ };
76
+
77
+ it('renders caller labels, copies the real stored value, and resets masking and feedback on reopening or replacement', async () => {
78
+ const user = userEvent.setup();
79
+ const writeText = vi.fn().mockResolvedValue(undefined);
80
+ Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
81
+ const onClose = vi.fn();
82
+ const { rerender } = render(<CredentialDisclosureDialog disclosure={stored} onClose={onClose} {...labels} />);
83
+ const dialog = await screen.findByRole('dialog', { name: labels.title });
84
+ expect(dialog).toHaveAccessibleDescription(labels.description);
85
+ const dl = dialog.querySelector('dl');
86
+ expect(dl).toHaveTextContent('Identifier');
87
+ expect(dl).toHaveTextContent(stored.credentialId);
88
+ expect(dl).toHaveTextContent('Valid until');
89
+ expect(dl).toHaveTextContent('January 1, 2030');
90
+ expect(document.body.innerHTML).not.toContain(stored.secret);
91
+ await user.click(screen.getByRole('button', { name: labels.showLabel }));
92
+ expect(screen.getByRole('button', { name: labels.hideLabel })).toHaveAttribute('aria-pressed', 'true');
93
+ expect(screen.getByRole('textbox', { name: labels.secretLabel })).toHaveValue(stored.secret);
94
+ await user.click(screen.getByRole('button', { name: labels.copyLabel }));
95
+ expect(writeText).toHaveBeenCalledWith(stored.secret);
96
+ expect(screen.getByRole('status')).toHaveTextContent(labels.successAnnouncement);
97
+ expect(screen.getByRole('button', { name: labels.copiedLabel })).toBeInTheDocument();
98
+ await user.click(screen.getByRole('button', { name: labels.closeLabel }));
99
+ expect(onClose).toHaveBeenCalledTimes(1);
100
+ expect(document.body.innerHTML).not.toContain(stored.secret);
101
+ rerender(<CredentialDisclosureDialog disclosure={null} onClose={onClose} {...labels} />);
102
+ rerender(<CredentialDisclosureDialog disclosure={stored} onClose={onClose} {...labels} />);
103
+ await screen.findByRole('dialog');
104
+ expect(screen.getByRole('button', { name: labels.showLabel })).toHaveAttribute('aria-pressed', 'false');
105
+ expect(screen.getByRole('status')).toBeEmptyDOMElement();
106
+ await user.click(screen.getByRole('button', { name: labels.showLabel }));
107
+ await user.click(screen.getByRole('button', { name: labels.copyLabel }));
108
+ rerender(
109
+ <CredentialDisclosureDialog
110
+ disclosure={{ ...stored, secret: 'synthetic_replacement_value' }}
111
+ onClose={onClose}
112
+ {...labels}
113
+ />,
114
+ );
115
+ expect(screen.getByRole('button', { name: labels.showLabel })).toHaveAttribute('aria-pressed', 'false');
116
+ expect(screen.getByRole('status')).toBeEmptyDOMElement();
117
+ expect(document.body.innerHTML).not.toContain('synthetic_replacement_value');
118
+ await user.click(screen.getByRole('button', { name: labels.showLabel }));
119
+ rerender(
120
+ <CredentialDisclosureDialog
121
+ disclosure={{ ...stored, credentialId: 'cred_demo_02' }}
122
+ onClose={onClose}
123
+ {...labels}
124
+ />,
125
+ );
126
+ expect(screen.getByRole('button', { name: labels.showLabel })).toHaveAttribute('aria-pressed', 'false');
127
+ });
128
+
129
+ it('shows a fresh secret with one kit copy action and replacement warning, and discards it on dismissal', async () => {
130
+ const user = userEvent.setup();
131
+ const onClose = vi.fn();
132
+ const fresh: CredentialDisclosure = { ...stored, variant: 'fresh' };
133
+ const { rerender } = render(
134
+ <CredentialDisclosureDialog
135
+ disclosure={fresh}
136
+ onClose={onClose}
137
+ {...labels}
138
+ warningTitle="Replacement"
139
+ warningText="This value replaces the previous secret."
140
+ />,
141
+ );
142
+ const dialog = await screen.findByRole('dialog');
143
+ expect(within(dialog).getByRole('region', { name: labels.secretLabel })).toHaveTextContent(stored.secret);
144
+ expect(within(dialog).getByRole('alert')).toHaveTextContent('Replacement');
145
+ expect(within(dialog).getByRole('alert')).toHaveTextContent('This value replaces the previous secret.');
146
+ expect(screen.getAllByRole('button', { name: labels.copyLabel })).toHaveLength(1);
147
+ await user.click(screen.getByRole('button', { name: labels.closeLabel }));
148
+ expect(onClose).toHaveBeenCalledTimes(1);
149
+ expect(document.body.innerHTML).not.toContain(stored.secret);
150
+ rerender(<CredentialDisclosureDialog disclosure={null} onClose={onClose} {...labels} />);
151
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
152
+ expect(document.body.innerHTML).not.toContain(stored.secret);
153
+ expect(onClose).toHaveBeenCalledTimes(1);
154
+ });
155
+
156
+ function FocusHarness({
157
+ triggerVersion = 0,
158
+ disclosureVersion = 0,
159
+ resolveReturnFocus,
160
+ fallbackFocus,
161
+ }: Pick<CredentialDisclosureDialogProps, 'resolveReturnFocus' | 'fallbackFocus'> & {
162
+ triggerVersion?: number;
163
+ disclosureVersion?: number;
164
+ }) {
165
+ const [disclosure, setDisclosure] = useState<CredentialDisclosure | null>(null);
166
+ return (
167
+ <>
168
+ <h1 id="fallback" tabIndex={-1}>
169
+ Credentials
170
+ </h1>
171
+ <button key={triggerVersion} id="opener" onClick={() => setDisclosure(stored)}>
172
+ Open
173
+ </button>
174
+ <div>
175
+ <button id="return">Return action</button>
176
+ </div>
177
+ <CredentialDisclosureDialog
178
+ disclosure={
179
+ disclosure && { ...disclosure, secret: disclosureVersion ? 'synthetic_new_value' : disclosure.secret }
180
+ }
181
+ onClose={() => setDisclosure(null)}
182
+ resolveReturnFocus={resolveReturnFocus}
183
+ fallbackFocus={fallbackFocus}
184
+ {...labels}
185
+ />
186
+ </>
187
+ );
188
+ }
189
+
190
+ it('focuses the popup, traps Tab, closes on Escape and resolves the live replaced trigger at close time', async () => {
191
+ const user = userEvent.setup();
192
+ const resolveReturnFocus = vi.fn(() => document.getElementById('opener'));
193
+ const props = { resolveReturnFocus, fallbackFocus: () => document.getElementById('fallback') };
194
+ const { rerender } = render(<FocusHarness {...props} />);
195
+ const oldTrigger = screen.getByRole('button', { name: 'Open' });
196
+ await user.click(oldTrigger);
197
+ const dialog = await screen.findByRole('dialog');
198
+ await waitFor(() => expect(dialog).toHaveFocus());
199
+ for (let index = 0; index < 8; index++) {
200
+ await user.tab();
201
+ await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true));
202
+ }
203
+ await user.tab({ shift: true });
204
+ await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true));
205
+ rerender(<FocusHarness {...props} triggerVersion={1} />);
206
+ expect(oldTrigger.isConnected).toBe(false);
207
+ expect(resolveReturnFocus).not.toHaveBeenCalled();
208
+ await user.keyboard('{Escape}');
209
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Open' })).toHaveFocus());
210
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
211
+ });
212
+
213
+ it.each([
214
+ 'null',
215
+ 'detached',
216
+ 'disabled',
217
+ 'removed',
218
+ 'hidden',
219
+ 'inert',
220
+ 'nonfocusable',
221
+ 'hidden-parent',
222
+ 'disabled-fieldset',
223
+ ] as const)('uses the fallback heading for a %s return target', async (reason) => {
224
+ const user = userEvent.setup();
225
+ const resolveReturnFocus = () =>
226
+ reason === 'null'
227
+ ? null
228
+ : reason === 'detached'
229
+ ? document.createElement('button')
230
+ : document.getElementById('return');
231
+ render(
232
+ <FocusHarness resolveReturnFocus={resolveReturnFocus} fallbackFocus={() => document.getElementById('fallback')} />,
233
+ );
234
+ await user.click(screen.getByRole('button', { name: 'Open' }));
235
+ await screen.findByRole('dialog');
236
+ const target = document.getElementById('return')!;
237
+ if (reason === 'disabled') target.setAttribute('disabled', '');
238
+ if (reason === 'removed') target.remove();
239
+ if (reason === 'hidden') target.hidden = true;
240
+ if (reason === 'inert') target.setAttribute('inert', '');
241
+ if (reason === 'nonfocusable') {
242
+ const span = document.createElement('span');
243
+ span.id = 'return';
244
+ target.replaceWith(span);
245
+ }
246
+ if (reason === 'hidden-parent' || reason === 'disabled-fieldset') {
247
+ const wrapper = document.createElement(reason === 'hidden-parent' ? 'div' : 'fieldset');
248
+ if (reason === 'hidden-parent') wrapper.style.display = 'none';
249
+ else wrapper.setAttribute('disabled', '');
250
+ target.before(wrapper);
251
+ wrapper.append(target);
252
+ }
253
+ await user.click(screen.getByRole('button', { name: labels.closeLabel }));
254
+ await waitFor(() => expect(screen.getByRole('heading', { name: 'Credentials' })).toHaveFocus());
255
+ });
256
+
257
+ it.each(['null', 'detached', 'disabled', 'hidden', 'inert', 'nonfocusable'] as const)(
258
+ 'returns to the original opener for a %s fallback',
259
+ async (reason) => {
260
+ const user = userEvent.setup();
261
+ const fallbackFocus = () =>
262
+ reason === 'null'
263
+ ? null
264
+ : reason === 'detached'
265
+ ? document.createElement('h1')
266
+ : document.getElementById('fallback');
267
+ render(<FocusHarness resolveReturnFocus={() => null} fallbackFocus={fallbackFocus} />);
268
+ const opener = screen.getByRole('button', { name: 'Open' });
269
+ await user.click(opener);
270
+ await screen.findByRole('dialog');
271
+ const heading = document.getElementById('fallback')!;
272
+ if (reason === 'disabled') heading.setAttribute('aria-disabled', 'true');
273
+ if (reason === 'hidden') heading.hidden = true;
274
+ if (reason === 'inert') heading.setAttribute('inert', '');
275
+ if (reason === 'nonfocusable') heading.removeAttribute('tabindex');
276
+ await user.keyboard('{Escape}');
277
+ await waitFor(() => expect(opener).toHaveFocus());
278
+ },
279
+ );
280
+
281
+ it.each([
282
+ ['stored', 'rejected'],
283
+ ['stored', 'missing'],
284
+ ['fresh', 'rejected'],
285
+ ['fresh', 'missing'],
286
+ ] as const)('announces %s clipboard %s politely, without logging secrets', async (variant, clipboard) => {
287
+ const user = userEvent.setup();
288
+ const logs = [
289
+ vi.spyOn(console, 'log'),
290
+ vi.spyOn(console, 'info'),
291
+ vi.spyOn(console, 'warn'),
292
+ vi.spyOn(console, 'error'),
293
+ vi.spyOn(console, 'debug'),
294
+ ];
295
+ const writeText = vi.fn().mockRejectedValue(new Error('Synthetic clipboard denial'));
296
+ Object.defineProperty(navigator, 'clipboard', {
297
+ value: clipboard === 'missing' ? undefined : { writeText },
298
+ configurable: true,
299
+ });
300
+ Object.defineProperty(document, 'execCommand', { value: () => false, configurable: true });
301
+ render(<CredentialDisclosureDialog disclosure={{ ...stored, variant }} onClose={() => {}} {...labels} />);
302
+ await screen.findByRole('dialog');
303
+ const feedback = document.querySelector('[data-live-announcer="polite"]');
304
+ expect(feedback).toBeEmptyDOMElement();
305
+ await user.click(screen.getByRole('button', { name: labels.copyLabel }));
306
+ expect(feedback).toHaveAttribute('aria-live', 'polite');
307
+ expect(feedback).toHaveTextContent(labels.failureAnnouncement);
308
+ expect(document.querySelector('[data-live-announcer="assertive"]')).toBeEmptyDOMElement();
309
+ if (clipboard === 'rejected') expect(writeText).toHaveBeenCalledWith(stored.secret);
310
+ else expect(writeText).not.toHaveBeenCalled();
311
+ expect(document.querySelector('textarea')).toBeNull();
312
+ for (const log of logs) {
313
+ expect(log.mock.calls.flat().some((value) => typeof value === 'string' && value.includes(stored.secret))).toBe(
314
+ false,
315
+ );
316
+ log.mockRestore();
317
+ }
318
+ });
319
+
320
+ it.each(['stored', 'fresh'] as const)('%s has no axe violations in all three themes', async (variant) => {
321
+ render(<CredentialDisclosureDialog disclosure={{ ...stored, variant }} onClose={() => {}} />);
322
+ const dialog = await screen.findByRole('dialog');
323
+ for (const theme of ['dark', 'light', 'high_contrast']) {
324
+ document.documentElement.className = theme;
325
+ await expectNoAxeViolations(dialog);
326
+ }
327
+ });
328
+
329
+ it('preserves the original opener when the credential value changes while open', async () => {
330
+ const user = userEvent.setup();
331
+ const { rerender } = render(<FocusHarness resolveReturnFocus={() => null} fallbackFocus={() => null} />);
332
+ const opener = screen.getByRole('button', { name: 'Open' });
333
+ await user.click(opener);
334
+ await waitFor(() => expect(screen.getByRole('dialog')).toHaveFocus());
335
+ rerender(<FocusHarness disclosureVersion={1} resolveReturnFocus={() => null} fallbackFocus={() => null} />);
336
+ await waitFor(() => expect(screen.getByRole('dialog')).toHaveFocus());
337
+ await user.keyboard('{Escape}');
338
+ await waitFor(() => expect(opener).toHaveFocus());
339
+ });
@@ -0,0 +1,193 @@
1
+ import {
2
+ Button,
3
+ CopyButton,
4
+ Dialog,
5
+ DialogContent,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ DialogDescription,
9
+ DialogBody,
10
+ DialogFooter,
11
+ DialogClose,
12
+ LiveAnnouncer,
13
+ SecretField,
14
+ CodeBlock,
15
+ Alert,
16
+ useI18n,
17
+ } from '@gntik-ai/ui';
18
+ import { useRef, useState, type ReactNode } from 'react';
19
+
20
+ export interface CredentialDisclosure {
21
+ variant: 'stored' | 'fresh';
22
+ credentialId: string;
23
+ /** Caller-formatted expiry; the block applies no expiry policy. */
24
+ expiresAt: ReactNode;
25
+ secret: string;
26
+ }
27
+
28
+ export interface CredentialDisclosureDialogProps {
29
+ /** Set only after a successful response. Leave null while pending or on failure. */
30
+ disclosure: CredentialDisclosure | null;
31
+ /** Clear disclosure to null on dismissal, dropping the secret in the consumer. */
32
+ onClose: () => void;
33
+ /** Resolve the current row action at close time, including after list reloads. */
34
+ resolveReturnFocus?: () => HTMLElement | null;
35
+ /** Resolve a fallback such as a page heading with tabIndex={-1}. */
36
+ fallbackFocus?: () => HTMLElement | null;
37
+ title?: string;
38
+ description?: string;
39
+ credentialIdLabel?: string;
40
+ expiryLabel?: string;
41
+ secretLabel?: string;
42
+ showLabel?: string;
43
+ hideLabel?: string;
44
+ copyLabel?: string;
45
+ copiedLabel?: string;
46
+ successAnnouncement?: string;
47
+ failureAnnouncement?: string;
48
+ warningTitle?: string;
49
+ warningText?: string;
50
+ closeLabel?: string;
51
+ }
52
+
53
+ /**
54
+ * Discloses an existing credential only after a successful response. Consumers keep disclosure
55
+ * null while pending or on failure, and clear it on close. No pending/error state or internal
56
+ * secret retention. A fresh secret requires a new successful response to be shown again.
57
+ */
58
+ export function CredentialDisclosureDialog(props: CredentialDisclosureDialogProps) {
59
+ if (!props.disclosure) return null;
60
+ return <DisclosureOpening {...props} disclosure={props.disclosure} />;
61
+ }
62
+
63
+ type OpeningProps = CredentialDisclosureDialogProps & { disclosure: CredentialDisclosure };
64
+
65
+ function DisclosureOpening(props: OpeningProps) {
66
+ // Keep the original opener across credential/value changes; the old popup is not an opener.
67
+ const [opener] = useState(() => (typeof document === 'undefined' ? null : document.activeElement));
68
+ const { variant, credentialId, secret } = props.disclosure;
69
+ // A new credential/value is a new session: reveal and announcement state cannot carry over.
70
+ // React keys are never rendered as DOM attributes.
71
+ return <DisclosureSession key={JSON.stringify([variant, credentialId, secret])} {...props} opener={opener} />;
72
+ }
73
+
74
+ function DisclosureSession({
75
+ disclosure,
76
+ onClose,
77
+ resolveReturnFocus,
78
+ fallbackFocus,
79
+ opener,
80
+ ...labels
81
+ }: OpeningProps & { opener: Element | null }) {
82
+ const { t } = useI18n();
83
+ const [open, setOpen] = useState(true);
84
+ const popup = useRef<HTMLDivElement>(null);
85
+ const dismissed = useRef(false);
86
+ const returnFocus = () => {
87
+ const target = resolveReturnFocus?.();
88
+ if (usableFocusTarget(target)) return target;
89
+ const fallback = fallbackFocus?.();
90
+ if (usableFocusTarget(fallback)) return fallback;
91
+ return usableFocusTarget(opener) ? opener : false;
92
+ };
93
+ const closeLabel = labels.closeLabel ?? t('credentialDisclosure.close');
94
+ const secretLabel = labels.secretLabel ?? t('credentialDisclosure.secret');
95
+ return (
96
+ <Dialog
97
+ open={open}
98
+ onOpenChange={(next) => {
99
+ if (!next && !dismissed.current) {
100
+ dismissed.current = true;
101
+ setOpen(false);
102
+ onClose();
103
+ }
104
+ }}
105
+ >
106
+ <DialogContent
107
+ ref={popup}
108
+ initialFocus={popup}
109
+ finalFocus={returnFocus}
110
+ showClose={false}
111
+ closeLabel={closeLabel}
112
+ >
113
+ {open && (
114
+ <>
115
+ <DialogHeader>
116
+ <DialogTitle>{labels.title ?? t('credentialDisclosure.title')}</DialogTitle>
117
+ <DialogDescription>{labels.description ?? t('credentialDisclosure.description')}</DialogDescription>
118
+ </DialogHeader>
119
+ <DialogBody className="grid gap-4">
120
+ <dl className="grid grid-cols-2 gap-2 text-sm">
121
+ <dt className="text-muted-foreground">
122
+ {labels.credentialIdLabel ?? t('credentialDisclosure.credentialId')}
123
+ </dt>
124
+ <dd className="break-all font-mono">{disclosure.credentialId}</dd>
125
+ <dt className="text-muted-foreground">{labels.expiryLabel ?? t('credentialDisclosure.expiry')}</dt>
126
+ <dd>{disclosure.expiresAt}</dd>
127
+ </dl>
128
+ <LiveAnnouncer>
129
+ {disclosure.variant === 'stored' ? (
130
+ <SecretField
131
+ value={disclosure.secret}
132
+ copyable={false}
133
+ aria-label={secretLabel}
134
+ showLabel={labels.showLabel ?? t('credentialDisclosure.show')}
135
+ hideLabel={labels.hideLabel ?? t('credentialDisclosure.hide')}
136
+ />
137
+ ) : (
138
+ <>
139
+ <Alert
140
+ role="alert"
141
+ tone="warning"
142
+ title={labels.warningTitle ?? t('credentialDisclosure.warningTitle')}
143
+ description={labels.warningText ?? t('credentialDisclosure.warningText')}
144
+ />
145
+ <CodeBlock
146
+ code={disclosure.secret}
147
+ copyable={false}
148
+ label={secretLabel}
149
+ wrapToggle={false}
150
+ defaultWrap
151
+ />
152
+ </>
153
+ )}
154
+ <CopyButton
155
+ value={() => disclosure.secret}
156
+ display="label"
157
+ label={labels.copyLabel ?? t('credentialDisclosure.copy')}
158
+ copiedLabel={labels.copiedLabel ?? t('credentialDisclosure.copied')}
159
+ announcement={labels.successAnnouncement ?? t('credentialDisclosure.success')}
160
+ failureLabel={labels.failureAnnouncement ?? t('credentialDisclosure.failure')}
161
+ failureAnnouncement={labels.failureAnnouncement ?? t('credentialDisclosure.failure')}
162
+ failurePoliteness="polite"
163
+ />
164
+ </LiveAnnouncer>
165
+ </DialogBody>
166
+ <DialogFooter>
167
+ <DialogClose render={<Button variant="secondary" />}>{closeLabel}</DialogClose>
168
+ </DialogFooter>
169
+ </>
170
+ )}
171
+ </DialogContent>
172
+ </Dialog>
173
+ );
174
+ }
175
+
176
+ /** Programmatic focus permits negative tabIndex, but never hidden, inert or disabled targets. */
177
+ function usableFocusTarget(element: Element | null | undefined): element is HTMLElement {
178
+ if (!(element instanceof HTMLElement) || !element.isConnected || element === document.body) return false;
179
+ if (element.matches(':disabled, [disabled], [aria-disabled="true"], input[type="hidden"]')) return false;
180
+ for (let ancestor: HTMLElement | null = element; ancestor; ancestor = ancestor.parentElement) {
181
+ if (ancestor.hidden || ancestor.hasAttribute('inert') || ancestor.getAttribute('aria-disabled') === 'true')
182
+ return false;
183
+ const style = getComputedStyle(ancestor);
184
+ if (
185
+ style.display === 'none' ||
186
+ style.visibility === 'hidden' ||
187
+ style.visibility === 'collapse' ||
188
+ style.contentVisibility === 'hidden'
189
+ )
190
+ return false;
191
+ }
192
+ return element.tabIndex >= 0 || element.hasAttribute('tabindex') || element.isContentEditable;
193
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Credential disclosure dialog',
5
+ family: 'forms',
6
+ status: 'beta',
7
+ description:
8
+ 'Controlled disclosure for an existing credential. Set disclosure ({variant: stored | fresh, credentialId, expiresAt: caller-formatted ReactNode, secret}) only after a successful response; null renders nothing while pending or on failure. Clear disclosure to null in onClose to drop the secret; a fresh secret requires a new successful response before showing it again. No pending/error state or internal secret retention. Stored values start masked; each null-to-value opening or changed credentialId/secret resets masking and copy feedback. Fresh values use a non-copyable CodeBlock with a kit CopyButton and a replacement warning. Optional English defaults through en/es i18n for title, description, credentialIdLabel, expiryLabel, secretLabel, showLabel, hideLabel, copyLabel, copiedLabel, successAnnouncement, failureAnnouncement, warningTitle, warningText and closeLabel. Copy results are announced politely, including manual-selection fallback. Popup initial focus and focus trapping use Base UI; at close, resolveReturnFocus() is validated for connection, visibility, enabled state and focusability, then fallbackFocus(), then the original opener. Synthetic fixtures only; the default Stored preview is masked and the Fresh preview starts closed.',
9
+ uses: ['Dialog', 'SecretField', 'CodeBlock', 'CopyButton', 'Alert', 'LiveAnnouncer', 'Button'],
10
+ };
@@ -0,0 +1,32 @@
1
+ import { Button } from '@gntik-ai/ui';
2
+ import { useRef, useState } from 'react';
3
+ import { CredentialDisclosureDialog, type CredentialDisclosure } from '../CredentialDisclosureDialog';
4
+ import { freshDisclosure } from '../fixtures';
5
+
6
+ /** Closed on load: no preview or visual baseline exposes the synthetic fresh value. */
7
+ export default function Fresh() {
8
+ const [disclosure, setDisclosure] = useState<CredentialDisclosure | null>(null);
9
+ const heading = useRef<HTMLHeadingElement>(null);
10
+ const trigger = useRef<HTMLButtonElement>(null);
11
+ return (
12
+ <>
13
+ <h1 ref={heading} tabIndex={-1}>
14
+ Credentials
15
+ </h1>
16
+ <Button
17
+ ref={trigger}
18
+ onClick={() => {
19
+ void Promise.resolve({ ...freshDisclosure }).then(setDisclosure);
20
+ }}
21
+ >
22
+ Disclose fresh secret
23
+ </Button>
24
+ <CredentialDisclosureDialog
25
+ disclosure={disclosure}
26
+ onClose={() => setDisclosure(null)}
27
+ resolveReturnFocus={() => trigger.current}
28
+ fallbackFocus={() => heading.current}
29
+ />
30
+ </>
31
+ );
32
+ }