@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,33 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { Hero } from './Hero';
5
+
6
+ describe('Hero', () => {
7
+ it('renders the centered hero with link CTAs', async () => {
8
+ const { container } = render(<Hero />);
9
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Ship your product faster');
10
+ expect(screen.getByRole('link', { name: 'Get started' })).toHaveAttribute('href', '#get-started');
11
+ expect(screen.getByRole('link', { name: 'Book a demo' })).toBeInTheDocument();
12
+ await expectNoAxeViolations(container);
13
+ });
14
+
15
+ it('renders the split variant with button actions and a custom media slot', async () => {
16
+ const user = userEvent.setup();
17
+ const onClick = vi.fn();
18
+ render(
19
+ <Hero
20
+ variant="split"
21
+ headingLevel="h2"
22
+ primaryAction={{ label: 'Start free trial', onClick }}
23
+ secondaryAction={null}
24
+ media={<img src="data:," alt="Dashboard screenshot" />}
25
+ />,
26
+ );
27
+ expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
28
+ expect(screen.getByRole('img', { name: 'Dashboard screenshot' })).toBeInTheDocument();
29
+ await user.click(screen.getByRole('button', { name: 'Start free trial' }));
30
+ expect(onClick).toHaveBeenCalledOnce();
31
+ await expectNoAxeViolations(document.body);
32
+ });
33
+ });
@@ -0,0 +1,107 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Badge, cn } from '@gntik-ai/ui';
3
+ import { ArrowRight } from '@gntik-ai/icons';
4
+ import { ActionButton, type MarketingAction } from '../actions';
5
+
6
+ export interface HeroProps {
7
+ /** `centered` (copy over the media) or `split` (copy left, media right from `lg`). */
8
+ variant?: 'centered' | 'split';
9
+ /** Small label above the title. */
10
+ eyebrow?: ReactNode;
11
+ title?: ReactNode;
12
+ description?: ReactNode;
13
+ primaryAction?: MarketingAction | null;
14
+ secondaryAction?: MarketingAction | null;
15
+ /** Screenshot slot. Defaults to a neutral app-window placeholder; null hides it. */
16
+ media?: ReactNode;
17
+ /** Heading level of the title. */
18
+ headingLevel?: 'h1' | 'h2';
19
+ className?: string;
20
+ }
21
+
22
+ /** Neutral app-window placeholder for the screenshot slot (decorative). */
23
+ export function HeroScreenshotPlaceholder({ className }: { className?: string }) {
24
+ return (
25
+ <div aria-hidden className={cn('overflow-hidden rounded-xl border border-border bg-card shadow-sm', className)}>
26
+ <div className="flex h-9 items-center gap-1.5 border-b border-border bg-secondary/40 px-3">
27
+ <span className="size-2.5 rounded-full bg-muted-foreground/30" />
28
+ <span className="size-2.5 rounded-full bg-muted-foreground/30" />
29
+ <span className="size-2.5 rounded-full bg-muted-foreground/30" />
30
+ </div>
31
+ <div className="flex">
32
+ <div className="hidden w-36 shrink-0 space-y-2 border-e border-border bg-secondary/30 p-3 sm:block">
33
+ {[70, 55, 80, 60, 45].map((w, i) => (
34
+ <div
35
+ key={i}
36
+ className={cn('h-2 rounded-full', i === 0 ? 'bg-primary/60' : 'bg-muted-foreground/20')}
37
+ style={{ width: `${w}%` }}
38
+ />
39
+ ))}
40
+ </div>
41
+ <div className="min-w-0 flex-1 space-y-4 p-4">
42
+ <div className="grid grid-cols-3 gap-3">
43
+ {[0, 1, 2].map((i) => (
44
+ <div key={i} className="space-y-2 rounded-lg border border-border p-3">
45
+ <div className="h-1.5 w-1/2 rounded-full bg-muted-foreground/25" />
46
+ <div className="h-3 w-3/4 rounded-full bg-foreground/20" />
47
+ </div>
48
+ ))}
49
+ </div>
50
+ <div className="flex h-28 items-end gap-1.5 rounded-lg border border-border p-3">
51
+ {[40, 55, 35, 70, 60, 85, 65, 90, 75, 95, 80, 100].map((h, i) => (
52
+ <div key={i} className="flex-1 rounded-t-sm bg-primary/50" style={{ height: `${h}%` }} />
53
+ ))}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ /** Landing hero: eyebrow, title, description, two calls to action and a screenshot slot. */
62
+ export function Hero({
63
+ variant = 'centered',
64
+ eyebrow = 'New · Usage-based billing',
65
+ title = 'Ship your product faster, with less to maintain',
66
+ description = 'Projects, deployments and team access in one place. Start in minutes, scale without rewriting, and keep every change reviewable.',
67
+ primaryAction = { label: 'Get started', href: '#get-started', icon: ArrowRight },
68
+ secondaryAction = { label: 'Book a demo', href: '#demo' },
69
+ media,
70
+ headingLevel: Heading = 'h1',
71
+ className,
72
+ }: HeroProps) {
73
+ const split = variant === 'split';
74
+ const slot = media === undefined ? <HeroScreenshotPlaceholder /> : media;
75
+ return (
76
+ <section
77
+ className={cn(
78
+ 'px-4 py-16 sm:px-6 sm:py-24',
79
+ split ? 'grid items-center gap-12 lg:grid-cols-2 lg:gap-16' : 'flex flex-col items-center text-center',
80
+ className,
81
+ )}
82
+ >
83
+ <div className={cn('min-w-0', !split && 'flex max-w-3xl flex-col items-center')}>
84
+ {eyebrow && (
85
+ <Badge tone="primary" size="lg" shape="pill" dot>
86
+ {eyebrow}
87
+ </Badge>
88
+ )}
89
+ <Heading className="mt-5 text-[36px] leading-[1.1] font-semibold tracking-tight text-balance text-foreground sm:text-[48px]">
90
+ {title}
91
+ </Heading>
92
+ {description && (
93
+ <p className="mt-5 max-w-2xl text-[16px] leading-relaxed text-pretty text-muted-foreground sm:text-[17px]">
94
+ {description}
95
+ </p>
96
+ )}
97
+ {(primaryAction || secondaryAction) && (
98
+ <div className={cn('mt-8 flex flex-wrap items-center gap-3', !split && 'justify-center')}>
99
+ {primaryAction && <ActionButton action={primaryAction} variant="primary" />}
100
+ {secondaryAction && <ActionButton action={secondaryAction} variant="secondary" />}
101
+ </div>
102
+ )}
103
+ </div>
104
+ {slot && <div className={cn('w-full min-w-0', !split && 'mt-14 max-w-5xl')}>{slot}</div>}
105
+ </section>
106
+ );
107
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Hero',
5
+ family: 'marketing',
6
+ status: 'beta',
7
+ description:
8
+ 'Landing hero in centered or split layout: eyebrow badge, title, description, primary/secondary calls to action and a screenshot slot with a neutral placeholder.',
9
+ uses: ['Badge', 'Button', 'buttonVariants'],
10
+ };
@@ -0,0 +1,92 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { PricingTable } from './PricingTable';
5
+
6
+ describe('PricingTable', () => {
7
+ it('renders the tiers with monthly prices', async () => {
8
+ const { container } = render(<PricingTable />);
9
+ const team = screen.getByRole('listitem', { name: 'Team' });
10
+ expect(within(team).getByText('$49')).toBeInTheDocument();
11
+ expect(within(team).getByText('Most popular')).toBeInTheDocument();
12
+ expect(within(screen.getByRole('listitem', { name: 'Enterprise' })).getByText('Custom')).toBeInTheDocument();
13
+ await expectNoAxeViolations(container);
14
+ });
15
+
16
+ it('switches to annual pricing and reports the chosen tier', async () => {
17
+ const user = userEvent.setup();
18
+ const onSelectTier = vi.fn();
19
+ render(<PricingTable onSelectTier={onSelectTier} />);
20
+ await user.click(screen.getByRole('button', { name: /Annual/ }));
21
+ const team = screen.getByRole('listitem', { name: 'Team' });
22
+ expect(within(team).getByText('$39')).toBeInTheDocument();
23
+ expect(within(team).getByText('$468 billed yearly')).toBeInTheDocument();
24
+ await user.click(within(team).getByRole('button', { name: 'Start free trial' }));
25
+ expect(onSelectTier).toHaveBeenCalledWith(expect.objectContaining({ id: 'team' }), 'annual');
26
+ });
27
+
28
+ it('marks the current plan with a badge, aria-current and a disabled CTA', async () => {
29
+ const onSelectTier = vi.fn();
30
+ const { container } = render(<PricingTable currentTierId="starter" onSelectTier={onSelectTier} />);
31
+ const starter = screen.getByRole('listitem', { name: 'Starter' });
32
+ expect(starter).toHaveAttribute('aria-current', 'true');
33
+ expect(within(starter).getByText('Current plan', { selector: 'span' })).toBeInTheDocument();
34
+ expect(within(starter).getByRole('button', { name: 'Current plan' })).toBeDisabled();
35
+ expect(screen.getByRole('listitem', { name: 'Team' })).not.toHaveAttribute('aria-current');
36
+ await expectNoAxeViolations(container);
37
+ });
38
+
39
+ it('works as a radio group in selectable mode (uncontrolled)', async () => {
40
+ const user = userEvent.setup();
41
+ const onValueChange = vi.fn();
42
+ const { container } = render(<PricingTable selectable defaultValue="team" onValueChange={onValueChange} />);
43
+ expect(screen.getByRole('radiogroup', { name: 'Simple pricing that scales with you' })).toBeInTheDocument();
44
+ expect(screen.queryByRole('button', { name: 'Start free trial' })).not.toBeInTheDocument();
45
+ const team = screen.getByRole('radio', { name: 'Team' });
46
+ expect(team).toBeChecked();
47
+ expect(screen.getAllByTestId('tier-check')).toHaveLength(1);
48
+ await expectNoAxeViolations(container);
49
+ await user.click(screen.getByText('For organizations with advanced security needs.'));
50
+ expect(screen.getByRole('radio', { name: 'Enterprise' })).toBeChecked();
51
+ expect(onValueChange).toHaveBeenLastCalledWith('enterprise');
52
+ screen.getByRole('radio', { name: 'Enterprise' }).focus();
53
+ await user.keyboard('{ArrowLeft}');
54
+ expect(screen.getByRole('radio', { name: 'Team' })).toBeChecked();
55
+ expect(onValueChange).toHaveBeenLastCalledWith('team');
56
+ expect(onValueChange).toHaveBeenCalledTimes(2);
57
+ });
58
+
59
+ it('respects a controlled value', async () => {
60
+ const user = userEvent.setup();
61
+ const onValueChange = vi.fn();
62
+ render(<PricingTable selectable value="starter" onValueChange={onValueChange} currentTierId="starter" />);
63
+ await user.click(screen.getByRole('radio', { name: 'Team' }));
64
+ expect(onValueChange).toHaveBeenCalledWith('team');
65
+ expect(screen.getByRole('radio', { name: 'Starter' })).toBeChecked();
66
+ expect(screen.getByRole('radio', { name: 'Starter' }).closest('[aria-current]')).not.toBeNull();
67
+ });
68
+
69
+ it('renders without its own header, labelled from outside, with tier names at headingLevel', async () => {
70
+ const { container } = render(
71
+ <>
72
+ <h1 id="ext">Pick a plan</h1>
73
+ <PricingTable
74
+ title={null}
75
+ eyebrow={null}
76
+ description={null}
77
+ aria-labelledby="ext"
78
+ headingLevel="h2"
79
+ showCycleToggle={false}
80
+ columns={1}
81
+ selectable
82
+ defaultValue="team"
83
+ />
84
+ </>,
85
+ );
86
+ expect(screen.getByRole('radiogroup', { name: 'Pick a plan' })).toBeInTheDocument();
87
+ expect(screen.getByRole('heading', { level: 2, name: 'Team' })).toBeInTheDocument();
88
+ expect(screen.queryByRole('group', { name: 'Billing period' })).toBeNull();
89
+ expect(screen.queryByRole('button', { name: /Annual/ })).toBeNull();
90
+ await expectNoAxeViolations(container);
91
+ });
92
+ });
@@ -0,0 +1,323 @@
1
+ import { useId, useState, type MouseEvent, type ReactNode } from 'react';
2
+ import { Badge, Button, Radio, RadioGroup, Toggle, ToggleGroup, cn } from '@gntik-ai/ui';
3
+ import { Check } from '@gntik-ai/icons';
4
+ import { samplePricingTiers } from './fixtures';
5
+
6
+ export type PricingCycle = 'monthly' | 'annual';
7
+
8
+ export interface PricingTier {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ /** Price per month on monthly billing; null shows "Custom". */
13
+ priceMonthly: number | null;
14
+ /** Price per month on annual billing; null shows "Custom". */
15
+ priceAnnual: number | null;
16
+ /** Call-to-action label. */
17
+ cta: string;
18
+ features: string[];
19
+ /** Emphasised with a primary border (no colour fill). */
20
+ highlighted?: boolean;
21
+ /** Label on the tier, e.g. "Most popular". */
22
+ badge?: string;
23
+ }
24
+
25
+ export interface PricingTableProps {
26
+ tiers?: PricingTier[];
27
+ eyebrow?: ReactNode;
28
+ title?: ReactNode;
29
+ description?: ReactNode;
30
+ currency?: string;
31
+ defaultCycle?: PricingCycle;
32
+ onCycleChange?: (cycle: PricingCycle) => void;
33
+ /** Called with the tier and the cycle shown when a CTA is activated. */
34
+ onSelectTier?: (tier: PricingTier, cycle: PricingCycle) => void;
35
+ /** Note next to the annual option, e.g. "Save 20%". */
36
+ annualNote?: string;
37
+ /** Level of the section title; tier names use the next level (or this one when there is no title). */
38
+ headingLevel?: 'h2' | 'h3';
39
+ /** Shows the monthly/annual toggle (default true). */
40
+ showCycleToggle?: boolean;
41
+ /** Columns at the widest breakpoint (default: one per tier, up to 4). */
42
+ columns?: 1 | 2 | 3 | 4;
43
+ /** Names the tier list / radio group when `title` is null (the block then renders no landmark), e.g. inside a wizard step. */
44
+ 'aria-label'?: string;
45
+ /** Id of an external heading that names the section when `title` is null. */
46
+ 'aria-labelledby'?: string;
47
+ /**
48
+ * Id of the tier the account is on: its card gets a "Current plan" badge and
49
+ * `aria-current`, and its CTA becomes a disabled "Current plan" button.
50
+ */
51
+ currentTierId?: string;
52
+ /** Label of the current tier's badge and disabled CTA. */
53
+ currentPlanLabel?: string;
54
+ /**
55
+ * Turns the tiers into a single-choice radio group (role radiogroup/radio, arrow keys,
56
+ * selected card with a primary border and a check). CTAs are not rendered in this mode.
57
+ */
58
+ selectable?: boolean;
59
+ /** Selected tier id (controlled, `selectable` only). */
60
+ value?: string;
61
+ /** Initially selected tier id (uncontrolled, `selectable` only). */
62
+ defaultValue?: string;
63
+ /** Called with the newly selected tier id (`selectable` only). */
64
+ onValueChange?: (value: string) => void;
65
+ className?: string;
66
+ }
67
+
68
+ const CARD = 'relative flex flex-col rounded-xl bg-card p-6 shadow-sm';
69
+ const GRID = { 1: 'lg:grid-cols-1', 2: 'lg:grid-cols-2', 3: 'lg:grid-cols-3', 4: 'lg:grid-cols-4' } as const;
70
+
71
+ /** Pricing section: tiers with a monthly/annual toggle, a highlighted tier and a feature checklist. */
72
+ export function PricingTable({
73
+ tiers = samplePricingTiers,
74
+ eyebrow = 'Pricing',
75
+ title = 'Simple pricing that scales with you',
76
+ description = 'Start free, upgrade when you need more. Every plan includes the core platform.',
77
+ currency = 'USD',
78
+ defaultCycle = 'monthly',
79
+ onCycleChange,
80
+ onSelectTier,
81
+ annualNote = 'Save 20%',
82
+ headingLevel: Heading = 'h2',
83
+ showCycleToggle = true,
84
+ columns,
85
+ 'aria-label': ariaLabel,
86
+ 'aria-labelledby': ariaLabelledBy,
87
+ currentTierId,
88
+ currentPlanLabel = 'Current plan',
89
+ selectable = false,
90
+ value,
91
+ defaultValue,
92
+ onValueChange,
93
+ className,
94
+ }: PricingTableProps) {
95
+ const titleId = useId();
96
+ const [cycle, setCycle] = useState<PricingCycle>(defaultCycle);
97
+ const [innerValue, setInnerValue] = useState(defaultValue);
98
+ const selected = value !== undefined ? value : innerValue;
99
+ const hasTitle = title != null && title !== false && title !== '';
100
+ const hasHeader = hasTitle || Boolean(eyebrow) || Boolean(description);
101
+ const TierHeading: 'h2' | 'h3' | 'h4' = !hasTitle ? Heading : Heading === 'h2' ? 'h3' : 'h4';
102
+ // Without a title the block is not a landmark of its own: the list / radio group carries the name.
103
+ const Wrapper = hasTitle ? 'section' : 'div';
104
+ const labelling = hasTitle
105
+ ? { 'aria-labelledby': titleId }
106
+ : { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy };
107
+ const money = new Intl.NumberFormat('en-US', { style: 'currency', currency, maximumFractionDigits: 0 });
108
+ const cols = columns ?? (Math.min(Math.max(tiers.length, 1), 4) as 1 | 2 | 3 | 4);
109
+ const gridClass = cn(
110
+ 'mx-auto grid max-w-6xl gap-6',
111
+ (hasHeader || showCycleToggle) && 'mt-10',
112
+ cols > 1 && 'md:grid-cols-2',
113
+ GRID[cols],
114
+ );
115
+ const bodyProps = { cycle, money, TierHeading, currentTierId, currentPlanLabel };
116
+
117
+ const select = (id: string) => {
118
+ if (id === selected) return;
119
+ if (value === undefined) setInnerValue(id);
120
+ onValueChange?.(id);
121
+ };
122
+
123
+ const changeCycle = (value: string[]) => {
124
+ const next = value[0];
125
+ if (next !== 'monthly' && next !== 'annual') return;
126
+ setCycle(next);
127
+ onCycleChange?.(next);
128
+ };
129
+
130
+ return (
131
+ <Wrapper {...(hasTitle ? labelling : {})} className={cn('px-4 py-16 sm:px-6 sm:py-20', className)}>
132
+ {hasHeader && (
133
+ <div className="mx-auto max-w-2xl text-center">
134
+ {eyebrow && (
135
+ <p className="font-mono text-[12px] font-semibold tracking-wide text-primary-text uppercase">{eyebrow}</p>
136
+ )}
137
+ {hasTitle && (
138
+ <Heading
139
+ id={titleId}
140
+ className="mt-2 text-[28px] leading-tight font-semibold tracking-tight text-balance text-foreground sm:text-[34px]"
141
+ >
142
+ {title}
143
+ </Heading>
144
+ )}
145
+ {description && (
146
+ <p className="mt-4 text-[15.5px] leading-relaxed text-pretty text-muted-foreground">{description}</p>
147
+ )}
148
+ </div>
149
+ )}
150
+ {showCycleToggle && (
151
+ <div className={cn('flex justify-center', hasHeader && 'mt-8')}>
152
+ <ToggleGroup aria-label="Billing period" value={[cycle]} onValueChange={changeCycle}>
153
+ <Toggle value="monthly">Monthly</Toggle>
154
+ <Toggle value="annual">
155
+ Annual
156
+ {annualNote && <span className="font-mono text-[10.5px] text-primary-text">{annualNote}</span>}
157
+ </Toggle>
158
+ </ToggleGroup>
159
+ </div>
160
+ )}
161
+ {selectable ? (
162
+ <RadioGroup
163
+ {...labelling}
164
+ value={selected ?? ''}
165
+ onValueChange={(v) => select(String(v))}
166
+ className={gridClass}
167
+ >
168
+ {tiers.map((tier) => {
169
+ const tierId = `${titleId}-${tier.id}`;
170
+ const isSelected = selected === tier.id;
171
+ const onCardClick = (event: MouseEvent<HTMLDivElement>) => {
172
+ if (!(event.target instanceof Element) || !event.target.closest('[role="radio"]')) select(tier.id);
173
+ };
174
+ return (
175
+ // The radio inside is the keyboard target; the card click is a pointer convenience.
176
+ <div
177
+ key={tier.id}
178
+ data-selected={isSelected || undefined}
179
+ aria-current={tier.id === currentTierId ? 'true' : undefined}
180
+ onClick={onCardClick}
181
+ className={cn(
182
+ CARD,
183
+ 'cursor-pointer has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-focus-ring',
184
+ isSelected ? 'border-2 border-primary' : 'border border-border hover:border-muted-foreground/40',
185
+ )}
186
+ >
187
+ <TierBody
188
+ tier={tier}
189
+ {...bodyProps}
190
+ tierId={tierId}
191
+ selected={isSelected}
192
+ control={<Radio value={tier.id} aria-labelledby={tierId} aria-describedby={`${tierId}-price`} />}
193
+ />
194
+ </div>
195
+ );
196
+ })}
197
+ </RadioGroup>
198
+ ) : (
199
+ <ul {...(hasTitle ? {} : labelling)} className={gridClass}>
200
+ {tiers.map((tier) => {
201
+ const tierId = `${titleId}-${tier.id}`;
202
+ const isCurrent = tier.id === currentTierId;
203
+ return (
204
+ <li
205
+ key={tier.id}
206
+ aria-labelledby={tierId}
207
+ aria-current={isCurrent ? 'true' : undefined}
208
+ className={cn(CARD, tier.highlighted ? 'border-2 border-primary' : 'border border-border')}
209
+ >
210
+ <TierBody
211
+ tier={tier}
212
+ {...bodyProps}
213
+ tierId={tierId}
214
+ action={
215
+ <Button
216
+ variant={tier.highlighted && !isCurrent ? 'primary' : 'secondary'}
217
+ className="mt-6 w-full"
218
+ aria-describedby={tierId}
219
+ disabled={isCurrent}
220
+ onClick={() => onSelectTier?.(tier, cycle)}
221
+ >
222
+ {isCurrent ? currentPlanLabel : tier.cta}
223
+ </Button>
224
+ }
225
+ />
226
+ </li>
227
+ );
228
+ })}
229
+ </ul>
230
+ )}
231
+ </Wrapper>
232
+ );
233
+ }
234
+
235
+ interface TierBodyProps {
236
+ tier: PricingTier;
237
+ tierId: string;
238
+ cycle: PricingCycle;
239
+ money: Intl.NumberFormat;
240
+ TierHeading: 'h2' | 'h3' | 'h4';
241
+ currentTierId?: string;
242
+ currentPlanLabel: string;
243
+ /** Radio control (selectable mode). */
244
+ control?: ReactNode;
245
+ selected?: boolean;
246
+ /** CTA (default mode). */
247
+ action?: ReactNode;
248
+ }
249
+
250
+ function TierBody({
251
+ tier,
252
+ tierId,
253
+ cycle,
254
+ money,
255
+ TierHeading,
256
+ currentTierId,
257
+ currentPlanLabel,
258
+ control,
259
+ selected,
260
+ action,
261
+ }: TierBodyProps) {
262
+ const price = cycle === 'annual' ? tier.priceAnnual : tier.priceMonthly;
263
+ const isCurrent = tier.id === currentTierId;
264
+ return (
265
+ <>
266
+ <div className="flex items-center justify-between gap-3">
267
+ <div className="flex min-w-0 items-center gap-2.5">
268
+ {control}
269
+ <TierHeading id={tierId} className="text-[15px] font-semibold tracking-tight text-foreground">
270
+ {tier.name}
271
+ </TierHeading>
272
+ </div>
273
+ <div className="flex shrink-0 items-center gap-1.5">
274
+ {isCurrent && (
275
+ <Badge tone="neutral" shape="pill">
276
+ {currentPlanLabel}
277
+ </Badge>
278
+ )}
279
+ {tier.badge && (
280
+ <Badge tone="primary" shape="pill">
281
+ {tier.badge}
282
+ </Badge>
283
+ )}
284
+ {selected && (
285
+ <span
286
+ data-testid="tier-check"
287
+ className="grid size-5 place-items-center rounded-full bg-primary text-primary-foreground"
288
+ >
289
+ <Check size={13} strokeWidth={2.6} aria-hidden />
290
+ </span>
291
+ )}
292
+ </div>
293
+ </div>
294
+ {tier.description && <p className="mt-1.5 text-[13px] text-muted-foreground">{tier.description}</p>}
295
+ <div id={`${tierId}-price`}>
296
+ <p className="mt-5 flex items-baseline gap-1.5">
297
+ <span className="text-[34px] font-semibold tracking-tight text-foreground tabular-nums">
298
+ {price == null ? 'Custom' : money.format(price)}
299
+ </span>
300
+ {price != null && <span className="text-[13px] text-muted-foreground">/ month</span>}
301
+ </p>
302
+ <p className="mt-1 h-4 text-[12px] text-muted-foreground">
303
+ {price == null
304
+ ? 'Tailored to your volume'
305
+ : price === 0
306
+ ? 'Free forever'
307
+ : cycle === 'annual'
308
+ ? `${money.format(price * 12)} billed yearly`
309
+ : 'Billed monthly'}
310
+ </p>
311
+ </div>
312
+ {action}
313
+ <ul aria-label={`${tier.name} features`} className="mt-6 space-y-2.5 border-t border-border pt-6">
314
+ {tier.features.map((f) => (
315
+ <li key={f} className="flex items-start gap-2.5 text-[13px] text-foreground">
316
+ <Check size={15} aria-hidden className="mt-0.5 shrink-0 text-primary-text" />
317
+ {f}
318
+ </li>
319
+ ))}
320
+ </ul>
321
+ </>
322
+ );
323
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Pricing table',
5
+ family: 'marketing',
6
+ status: 'beta',
7
+ description:
8
+ 'Pricing tiers with a monthly/annual toggle, a highlighted tier (primary border, no fill), price per cycle, CTA and feature checklist.',
9
+ uses: ['ToggleGroup', 'Toggle', 'Badge', 'Button', 'RadioGroup', 'Radio'],
10
+ };
@@ -0,0 +1,34 @@
1
+ import type { PricingTier } from './PricingTable';
2
+
3
+ /** Sample three-tier pricing. Prices are per month; annual is per month billed yearly. */
4
+ export const samplePricingTiers: PricingTier[] = [
5
+ {
6
+ id: 'starter',
7
+ name: 'Starter',
8
+ description: 'For individuals and small side projects.',
9
+ priceMonthly: 0,
10
+ priceAnnual: 0,
11
+ cta: 'Start for free',
12
+ features: ['1 project', '3 members', '50,000 requests / month', 'Community support'],
13
+ },
14
+ {
15
+ id: 'team',
16
+ name: 'Team',
17
+ description: 'For growing teams shipping every day.',
18
+ priceMonthly: 49,
19
+ priceAnnual: 39,
20
+ cta: 'Start free trial',
21
+ highlighted: true,
22
+ badge: 'Most popular',
23
+ features: ['Unlimited projects', '25 members', '500,000 requests / month', 'Preview deployments', 'Email support'],
24
+ },
25
+ {
26
+ id: 'enterprise',
27
+ name: 'Enterprise',
28
+ description: 'For organizations with advanced security needs.',
29
+ priceMonthly: null,
30
+ priceAnnual: null,
31
+ cta: 'Contact sales',
32
+ features: ['Everything in Team', 'SSO and audit log', 'Custom quotas', 'Data residency', 'Dedicated support'],
33
+ },
34
+ ];
@@ -0,0 +1,40 @@
1
+ import type { ComponentType } from 'react';
2
+ import { Button, buttonVariants, cn } from '@gntik-ai/ui';
3
+
4
+ type IconComponent = ComponentType<{ size?: number; 'aria-hidden'?: boolean }>;
5
+
6
+ /** A call to action: a link when `href` is set, otherwise a button. */
7
+ export interface MarketingAction {
8
+ label: string;
9
+ href?: string;
10
+ onClick?: () => void;
11
+ /** Trailing icon (lucide component). */
12
+ icon?: IconComponent;
13
+ }
14
+
15
+ export function ActionButton({
16
+ action,
17
+ variant,
18
+ size = 'lg',
19
+ className,
20
+ }: {
21
+ action: MarketingAction;
22
+ variant: 'primary' | 'secondary' | 'ghost';
23
+ size?: 'md' | 'lg';
24
+ className?: string;
25
+ }) {
26
+ const Trailing = action.icon;
27
+ if (action.href) {
28
+ return (
29
+ <a href={action.href} onClick={action.onClick} className={cn(buttonVariants({ variant, size }), className)}>
30
+ {action.label}
31
+ {Trailing && <Trailing size={size === 'lg' ? 17 : 15} aria-hidden />}
32
+ </a>
33
+ );
34
+ }
35
+ return (
36
+ <Button variant={variant} size={size} trailingIcon={Trailing} onClick={action.onClick} className={className}>
37
+ {action.label}
38
+ </Button>
39
+ );
40
+ }
@@ -0,0 +1,15 @@
1
+ export * from './Hero/Hero';
2
+ export { meta as heroMeta } from './Hero/block.meta';
3
+ export * from './FeatureGrid/FeatureGrid';
4
+ export { meta as featureGridMeta } from './FeatureGrid/block.meta';
5
+ export { sampleFeatures } from './FeatureGrid/fixtures';
6
+ export * from './PricingTable/PricingTable';
7
+ export { meta as pricingTableMeta } from './PricingTable/block.meta';
8
+ export { samplePricingTiers } from './PricingTable/fixtures';
9
+ export * from './Faq/Faq';
10
+ export { meta as faqMeta } from './Faq/block.meta';
11
+ export { sampleFaqs } from './Faq/fixtures';
12
+ export type { MarketingAction } from './actions';
13
+ export * from './ComparisonTable/ComparisonTable';
14
+ export { meta as comparisonTableMeta } from './ComparisonTable/block.meta';
15
+ export { sampleComparisonPlans, sampleComparisonSections } from './ComparisonTable/fixtures';