@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,44 @@
1
+ import { act, render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import { ModelKeysList } from './ModelKeysList';
5
+
6
+ const row = (name: string) => screen.getByText(name).closest('li') as HTMLElement;
7
+
8
+ describe('ModelKeysList', () => {
9
+ it('lists keys masked, with provider and status', async () => {
10
+ render(<ModelKeysList onAdd={() => {}} />);
11
+ expect(screen.getByRole('region', { name: 'Model provider keys' })).toBeInTheDocument();
12
+ expect(within(row('Production')).getByText('pa-…4f2a')).toBeInTheDocument();
13
+ expect(within(row('Gateway')).getByText('Untested')).toBeInTheDocument();
14
+ expect(within(row('On-prem cluster')).getByText('Failing')).toBeInTheDocument();
15
+ expect(screen.getByText(/openai-compatible/)).toBeInTheDocument();
16
+ await expectNoAxeViolations();
17
+ });
18
+
19
+ it('tests a connection with a spinner, then shows the result', async () => {
20
+ const user = userEvent.setup();
21
+ let resolve: (ok: boolean) => void = () => {};
22
+ const onTest = vi.fn(() => new Promise<boolean>((r) => (resolve = r)));
23
+ render(<ModelKeysList onTest={onTest} />);
24
+ await user.click(screen.getByRole('button', { name: 'Test Gateway' }));
25
+ const busy = screen.getByRole('button', { name: 'Testing Gateway' });
26
+ expect(busy).toHaveAttribute('aria-busy', 'true');
27
+ expect(busy.querySelector('svg.animate-spin')).not.toBeNull();
28
+ await act(async () => resolve(true));
29
+ expect(within(row('Gateway')).getByText('Connected')).toBeInTheDocument();
30
+ expect(screen.getByText('Gateway: connection OK')).toBeInTheDocument();
31
+ });
32
+
33
+ it('revokes a key after confirming in an alert dialog', async () => {
34
+ const user = userEvent.setup();
35
+ const onRevoke = vi.fn();
36
+ render(<ModelKeysList onRevoke={onRevoke} />);
37
+ await user.click(screen.getByRole('button', { name: 'Revoke Production' }));
38
+ const dialog = await screen.findByRole('alertdialog', { name: 'Revoke “Production”?' });
39
+ await user.click(within(dialog).getByRole('button', { name: 'Revoke key' }));
40
+ expect(onRevoke).toHaveBeenCalledWith(expect.objectContaining({ id: 'k1' }));
41
+ expect(within(row('Production')).getByText('Revoked')).toBeInTheDocument();
42
+ expect(within(row('Production')).queryByRole('button')).not.toBeInTheDocument();
43
+ });
44
+ });
@@ -0,0 +1,203 @@
1
+ import { useEffect, useId, useRef, useState } from 'react';
2
+ import { KeyRound, Plus, PlugZap, Trash2 } from '@gntik-ai/icons';
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ AlertDialogTrigger,
13
+ Button,
14
+ Card,
15
+ CardAction,
16
+ CardDescription,
17
+ CardHeader,
18
+ CardTitle,
19
+ EmptyState,
20
+ Spinner,
21
+ StatusTag,
22
+ cn,
23
+ type StatusDefinition,
24
+ } from '@gntik-ai/ui';
25
+ import { modelKeys, type ModelKey, type ModelKeyStatus } from './fixtures';
26
+
27
+ export type { ModelKey, ModelKeyStatus } from './fixtures';
28
+
29
+ export const MODEL_KEY_STATUSES: Record<ModelKeyStatus, StatusDefinition> = {
30
+ connected: { label: 'Connected', tone: 'success' },
31
+ failing: { label: 'Failing', tone: 'destructive' },
32
+ untested: { label: 'Untested', tone: 'neutral' },
33
+ revoked: { label: 'Revoked', tone: 'neutral' },
34
+ };
35
+
36
+ export interface ModelKeysListProps {
37
+ keys?: ModelKey[];
38
+ title?: string;
39
+ /** Tests a key; resolve true when the provider answered. Defaults to a simulated check. */
40
+ onTest?: (key: ModelKey) => Promise<boolean> | boolean;
41
+ /** Called after the revoke is confirmed. */
42
+ onRevoke?: (key: ModelKey) => void;
43
+ /** Shows an "Add key" button. */
44
+ onAdd?: () => void;
45
+ /** Heading level of the title, to fit the page outline (default h3). */
46
+ titleAs?: 'h2' | 'h3' | 'h4';
47
+ className?: string;
48
+ }
49
+
50
+ const simulateTest = (key: ModelKey) =>
51
+ new Promise<boolean>((resolve) => setTimeout(() => resolve(key.status !== 'failing'), 900));
52
+
53
+ /** Provider keys with masked secrets, connection tests and confirmed revocation. */
54
+ export function ModelKeysList({
55
+ keys = modelKeys,
56
+ title = 'Model provider keys',
57
+ onTest = simulateTest,
58
+ onRevoke,
59
+ onAdd,
60
+ titleAs = 'h3',
61
+ className,
62
+ }: ModelKeysListProps) {
63
+ const titleId = useId();
64
+ const [statuses, setStatuses] = useState<Record<string, ModelKeyStatus>>({});
65
+ const [testing, setTesting] = useState<Record<string, boolean>>({});
66
+ const [announcement, setAnnouncement] = useState('');
67
+ const mounted = useRef(true);
68
+ useEffect(() => {
69
+ mounted.current = true;
70
+ return () => {
71
+ mounted.current = false;
72
+ };
73
+ }, []);
74
+
75
+ const statusOf = (k: ModelKey) => statuses[k.id] ?? k.status;
76
+
77
+ const test = async (key: ModelKey) => {
78
+ setTesting((t) => ({ ...t, [key.id]: true }));
79
+ setAnnouncement(`Testing ${key.name}…`);
80
+ let ok = false;
81
+ try {
82
+ ok = await onTest(key);
83
+ } catch {
84
+ ok = false;
85
+ }
86
+ if (!mounted.current) return;
87
+ setTesting((t) => ({ ...t, [key.id]: false }));
88
+ setStatuses((s) => ({ ...s, [key.id]: ok ? 'connected' : 'failing' }));
89
+ setAnnouncement(ok ? `${key.name}: connection OK` : `${key.name}: connection failed`);
90
+ };
91
+
92
+ const revoke = (key: ModelKey) => {
93
+ setStatuses((s) => ({ ...s, [key.id]: 'revoked' }));
94
+ setAnnouncement(`${key.name} revoked`);
95
+ onRevoke?.(key);
96
+ };
97
+
98
+ return (
99
+ <Card className={cn('w-full', className)} aria-labelledby={titleId} role="region">
100
+ <CardHeader divided>
101
+ <div>
102
+ <CardTitle as={titleAs} id={titleId}>
103
+ {title}
104
+ </CardTitle>
105
+ <CardDescription>Keys are stored encrypted and only shown masked.</CardDescription>
106
+ </div>
107
+ {onAdd && (
108
+ <CardAction>
109
+ <Button size="sm" icon={Plus} onClick={onAdd}>
110
+ Add key
111
+ </Button>
112
+ </CardAction>
113
+ )}
114
+ </CardHeader>
115
+ {keys.length === 0 ? (
116
+ <EmptyState
117
+ icon={KeyRound}
118
+ title="No provider keys"
119
+ description="Add a key to let assistants call a model provider."
120
+ />
121
+ ) : (
122
+ <ul aria-labelledby={titleId} className="divide-y divide-border">
123
+ {keys.map((key) => {
124
+ const status = statusOf(key);
125
+ const revoked = status === 'revoked';
126
+ const busy = testing[key.id] ?? false;
127
+ return (
128
+ <li
129
+ key={key.id}
130
+ className={cn('flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center', revoked && 'opacity-60')}
131
+ >
132
+ <div className="flex min-w-0 flex-1 items-center gap-3">
133
+ <span className="grid size-9 shrink-0 place-items-center rounded-lg bg-secondary text-muted-foreground">
134
+ <KeyRound size={16} aria-hidden />
135
+ </span>
136
+ <div className="min-w-0">
137
+ <p className="flex flex-wrap items-center gap-2">
138
+ <span className="text-[13px] font-medium text-foreground">{key.name}</span>
139
+ <StatusTag size="sm" status={status} statuses={MODEL_KEY_STATUSES} />
140
+ </p>
141
+ <p className="truncate text-[12px] text-muted-foreground">
142
+ {key.provider}
143
+ {key.endpoint && <span className="font-mono"> · {key.endpoint}</span>}
144
+ </p>
145
+ </div>
146
+ </div>
147
+ <div className="flex items-center gap-3 sm:justify-end">
148
+ <code className="rounded bg-secondary px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
149
+ {key.maskedKey}
150
+ </code>
151
+ {key.lastUsed && (
152
+ <span className="hidden text-[11.5px] text-muted-foreground md:inline">Used {key.lastUsed}</span>
153
+ )}
154
+ {!revoked && (
155
+ <div className="ms-auto flex items-center gap-1.5 sm:ms-0">
156
+ <Button
157
+ variant="secondary"
158
+ size="sm"
159
+ aria-label={busy ? `Testing ${key.name}` : `Test ${key.name}`}
160
+ aria-busy={busy || undefined}
161
+ disabled={busy}
162
+ focusableWhenDisabled
163
+ onClick={() => void test(key)}
164
+ >
165
+ {busy ? <Spinner size={14} /> : <PlugZap size={14} aria-hidden />}
166
+ {busy ? 'Testing…' : 'Test'}
167
+ </Button>
168
+ <AlertDialog>
169
+ <AlertDialogTrigger
170
+ render={<Button variant="ghost" size="sm" icon={Trash2} aria-label={`Revoke ${key.name}`} />}
171
+ >
172
+ Revoke
173
+ </AlertDialogTrigger>
174
+ <AlertDialogContent>
175
+ <AlertDialogHeader tone="destructive">
176
+ <AlertDialogTitle>Revoke “{key.name}”?</AlertDialogTitle>
177
+ <AlertDialogDescription>
178
+ Requests that use this {key.provider} key will fail immediately. You can add a new key at
179
+ any time.
180
+ </AlertDialogDescription>
181
+ </AlertDialogHeader>
182
+ <AlertDialogFooter>
183
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
184
+ <AlertDialogAction icon={Trash2} onClick={() => revoke(key)}>
185
+ Revoke key
186
+ </AlertDialogAction>
187
+ </AlertDialogFooter>
188
+ </AlertDialogContent>
189
+ </AlertDialog>
190
+ </div>
191
+ )}
192
+ </div>
193
+ </li>
194
+ );
195
+ })}
196
+ </ul>
197
+ )}
198
+ <p aria-live="polite" className="sr-only">
199
+ {announcement}
200
+ </p>
201
+ </Card>
202
+ );
203
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Model keys list',
5
+ family: 'ai',
6
+ status: 'beta',
7
+ description:
8
+ 'Model provider keys with masked secrets and status: test the connection (spinner while it runs, result announced) and revoke behind a confirmation dialog.',
9
+ uses: ['Card', 'StatusTag', 'Button', 'Spinner', 'AlertDialog', 'EmptyState'],
10
+ };
@@ -0,0 +1,44 @@
1
+ export type ModelKeyStatus = 'connected' | 'failing' | 'untested' | 'revoked';
2
+
3
+ export interface ModelKey {
4
+ id: string;
5
+ /** Display name, e.g. "Production". */
6
+ name: string;
7
+ /** Provider or protocol, e.g. "Provider A", "openai-compatible", "self-hosted". */
8
+ provider: string;
9
+ /** Base URL for compatible or self-hosted endpoints. */
10
+ endpoint?: string;
11
+ /** Masked secret, e.g. "sk-…4f2a". Never pass the full key to the UI. */
12
+ maskedKey: string;
13
+ status: ModelKeyStatus;
14
+ /** Relative last-use time, e.g. "3 min ago". */
15
+ lastUsed?: string;
16
+ }
17
+
18
+ export const modelKeys: ModelKey[] = [
19
+ {
20
+ id: 'k1',
21
+ name: 'Production',
22
+ provider: 'Provider A',
23
+ maskedKey: 'pa-…4f2a',
24
+ status: 'connected',
25
+ lastUsed: '3 min ago',
26
+ },
27
+ {
28
+ id: 'k2',
29
+ name: 'Gateway',
30
+ provider: 'openai-compatible',
31
+ endpoint: 'https://llm-gateway.internal/v1',
32
+ maskedKey: 'sk-…9c01',
33
+ status: 'untested',
34
+ },
35
+ {
36
+ id: 'k3',
37
+ name: 'On-prem cluster',
38
+ provider: 'self-hosted',
39
+ endpoint: 'http://10.0.4.12:8000/v1',
40
+ maskedKey: 'tok-…77de',
41
+ status: 'failing',
42
+ lastUsed: '2 days ago',
43
+ },
44
+ ];
@@ -0,0 +1,97 @@
1
+ import { act, fireEvent, render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../../test/a11y';
4
+ import ControlledVariables from './examples/controlled-variables';
5
+ import { PromptEditor, extractPromptVariables } from './PromptEditor';
6
+
7
+ const chips = () =>
8
+ within(screen.getByRole('list', { name: 'Detected variables' }))
9
+ .getAllByRole('listitem')
10
+ .map((li) => li.textContent);
11
+
12
+ describe('PromptEditor', () => {
13
+ it('renders prompts, detected variables and parameters', async () => {
14
+ render(<PromptEditor />);
15
+ expect((screen.getByRole('textbox', { name: 'System prompt' }) as HTMLTextAreaElement).value).toContain(
16
+ '{{company}}',
17
+ );
18
+ expect(chips()).toEqual(['company', 'language', 'ticket']);
19
+ expect(screen.getByText('1 variable has no value')).toBeInTheDocument();
20
+ expect(screen.getByRole('slider', { name: 'Temperature' })).toHaveValue('0.7');
21
+ expect(screen.getByRole('spinbutton', { name: 'Max tokens' })).toHaveValue(1024);
22
+ await expectNoAxeViolations();
23
+ });
24
+
25
+ it('detects new variables and runs with the resolved input', async () => {
26
+ const user = userEvent.setup();
27
+ let finish: () => void = () => {};
28
+ const onRun = vi.fn(() => new Promise<void>((r) => (finish = r)));
29
+ render(<PromptEditor onRun={onRun} />);
30
+ fireEvent.change(screen.getByRole('textbox', { name: /User prompt/ }), {
31
+ target: { value: 'Reply to {{customer}} about {{ticket}}' },
32
+ });
33
+ expect(chips()).toEqual(['company', 'language', 'customer', 'ticket']);
34
+ await user.type(screen.getByRole('textbox', { name: 'customer' }), 'Ada');
35
+ await user.type(screen.getByRole('textbox', { name: 'ticket' }), 'Login fails');
36
+ await user.click(screen.getByRole('button', { name: 'Run' }));
37
+ expect(onRun).toHaveBeenCalledWith({
38
+ system: expect.stringContaining('{{company}}'),
39
+ user: 'Reply to {{customer}} about {{ticket}}',
40
+ variables: { company: 'Northwind', language: 'English', customer: 'Ada', ticket: 'Login fails' },
41
+ temperature: 0.7,
42
+ maxTokens: 1024,
43
+ });
44
+ expect(screen.getByRole('button', { name: 'Running…' })).toHaveAttribute('aria-busy', 'true');
45
+ await act(async () => finish());
46
+ expect(screen.getByRole('button', { name: 'Run' })).toBeEnabled();
47
+ });
48
+
49
+ it('blocks Run on an empty user prompt or invalid max tokens', () => {
50
+ render(<PromptEditor defaultUser="" />);
51
+ expect(screen.getByRole('button', { name: 'Run' })).toBeDisabled();
52
+ expect(extractPromptVariables('{{ a }} {{b.c}} {{a}}')).toEqual(['a', 'b.c']);
53
+ });
54
+
55
+ it('supports controlled variables', async () => {
56
+ const user = userEvent.setup();
57
+ const onVariablesChange = vi.fn();
58
+ const onRun = vi.fn();
59
+ const { rerender } = render(
60
+ <PromptEditor
61
+ variables={{ company: 'Acme', language: 'French', ticket: 'T-1' }}
62
+ onVariablesChange={onVariablesChange}
63
+ onRun={onRun}
64
+ />,
65
+ );
66
+ const company = screen.getByRole('textbox', { name: 'company' });
67
+ expect(company).toHaveValue('Acme');
68
+ await user.type(company, 'x');
69
+ expect(onVariablesChange).toHaveBeenLastCalledWith({ company: 'Acmex', language: 'French', ticket: 'T-1' });
70
+ // Controlled: the parent has not updated the value.
71
+ expect(company).toHaveValue('Acme');
72
+ rerender(
73
+ <PromptEditor
74
+ variables={{ company: 'Globex', language: 'French', ticket: 'T-1' }}
75
+ onVariablesChange={onVariablesChange}
76
+ onRun={onRun}
77
+ />,
78
+ );
79
+ expect(company).toHaveValue('Globex');
80
+ await user.click(screen.getByRole('button', { name: 'Run' }));
81
+ expect(onRun).toHaveBeenCalledWith(
82
+ expect.objectContaining({ variables: { company: 'Globex', language: 'French', ticket: 'T-1' } }),
83
+ );
84
+ });
85
+
86
+ it('controlled example: the page fills a variable', async () => {
87
+ const user = userEvent.setup();
88
+ render(<ControlledVariables />);
89
+ expect(screen.getByText('1 variable has no value')).toBeInTheDocument();
90
+ await user.click(screen.getByRole('button', { name: 'Fill from sample ticket' }));
91
+ expect(screen.getByRole('textbox', { name: 'ticket' })).toHaveValue('Cannot reset my password');
92
+ expect(screen.queryByText('1 variable has no value')).toBeNull();
93
+ await user.type(screen.getByRole('textbox', { name: 'company' }), ' Inc');
94
+ expect(screen.getByRole('textbox', { name: 'company' })).toHaveValue('Acme Inc');
95
+ await expectNoAxeViolations();
96
+ });
97
+ });
@@ -0,0 +1,202 @@
1
+ import { useId, useState } from 'react';
2
+ import { Braces, Play } from '@gntik-ai/icons';
3
+ import { Button, Field, FieldDescription, FieldLabel, Input, Slider, Textarea, Token, cn } from '@gntik-ai/ui';
4
+ import { promptDefaults } from './fixtures';
5
+
6
+ export interface PromptRunInput {
7
+ system: string;
8
+ user: string;
9
+ /** Values for every variable found in the prompts (missing ones are empty strings). */
10
+ variables: Record<string, string>;
11
+ temperature: number;
12
+ maxTokens: number;
13
+ }
14
+
15
+ export interface PromptEditorProps {
16
+ title?: string;
17
+ defaultSystem?: string;
18
+ defaultUser?: string;
19
+ /** Initial variable values (uncontrolled). */
20
+ defaultVariables?: Record<string, string>;
21
+ /** Variable values (controlled). Pair with `onVariablesChange`. */
22
+ variables?: Readonly<Record<string, string>>;
23
+ /** Called with every variable value (all keys kept) when one changes. */
24
+ onVariablesChange?: (variables: Record<string, string>) => void;
25
+ defaultTemperature?: number;
26
+ defaultMaxTokens?: number;
27
+ /** Upper bound of the max-tokens input. */
28
+ maxTokensLimit?: number;
29
+ /** Called on Run; return a promise to show the running state until it settles. */
30
+ onRun?: (input: PromptRunInput) => void | Promise<unknown>;
31
+ /** Heading level of the title, to fit the page outline (default h3); inner headings use the next level. */
32
+ titleAs?: 'h2' | 'h3' | 'h4';
33
+ className?: string;
34
+ }
35
+
36
+ const VARIABLE = /\{\{\s*([A-Za-z_][\w.]*)\s*\}\}/g;
37
+
38
+ /** Unique `{{variable}}` names, in order of appearance. */
39
+ export function extractPromptVariables(...texts: string[]): string[] {
40
+ const seen = new Set<string>();
41
+ for (const text of texts) for (const m of text.matchAll(VARIABLE)) if (m[1]) seen.add(m[1]);
42
+ return [...seen];
43
+ }
44
+
45
+ const estimateTokens = (text: string) => Math.ceil(text.length / 4);
46
+
47
+ /** Prompt playground: system + user prompts, detected variables, parameters and Run. */
48
+ export function PromptEditor({
49
+ title = 'Prompt',
50
+ defaultSystem = promptDefaults.system,
51
+ defaultUser = promptDefaults.user,
52
+ defaultVariables = promptDefaults.variables,
53
+ variables: variablesProp,
54
+ onVariablesChange,
55
+ defaultTemperature = promptDefaults.temperature,
56
+ defaultMaxTokens = promptDefaults.maxTokens,
57
+ maxTokensLimit = 8192,
58
+ onRun,
59
+ titleAs: TitleTag = 'h3',
60
+ className,
61
+ }: PromptEditorProps) {
62
+ const SubTag = TitleTag === 'h2' ? 'h3' : TitleTag === 'h3' ? 'h4' : 'h5';
63
+ const titleId = useId();
64
+ const [system, setSystem] = useState(defaultSystem);
65
+ const [user, setUser] = useState(defaultUser);
66
+ const [innerValues, setInnerValues] = useState<Record<string, string>>(defaultVariables);
67
+ const values: Readonly<Record<string, string>> = variablesProp ?? innerValues;
68
+ const setValue = (name: string, value: string) => {
69
+ const next = { ...values, [name]: value };
70
+ setInnerValues(next);
71
+ onVariablesChange?.(next);
72
+ };
73
+ const [temperature, setTemperature] = useState(defaultTemperature);
74
+ const [maxTokens, setMaxTokens] = useState(String(defaultMaxTokens));
75
+ const [running, setRunning] = useState(false);
76
+
77
+ const variables = extractPromptVariables(system, user);
78
+ const missing = variables.filter((v) => !values[v]?.trim());
79
+ const maxTokensNumber = Number(maxTokens);
80
+ const maxTokensInvalid =
81
+ !Number.isInteger(maxTokensNumber) || maxTokensNumber < 1 || maxTokensNumber > maxTokensLimit;
82
+ const canRun = user.trim().length > 0 && !maxTokensInvalid && !running;
83
+
84
+ const run = () => {
85
+ if (!canRun) return;
86
+ const result = onRun?.({
87
+ system,
88
+ user,
89
+ variables: Object.fromEntries(variables.map((v) => [v, values[v] ?? ''])),
90
+ temperature,
91
+ maxTokens: maxTokensNumber,
92
+ });
93
+ if (result && typeof (result as Promise<unknown>).then === 'function') {
94
+ setRunning(true);
95
+ void (result as Promise<unknown>).finally(() => setRunning(false));
96
+ }
97
+ };
98
+
99
+ return (
100
+ <section
101
+ aria-labelledby={titleId}
102
+ className={cn('overflow-hidden rounded-lg border border-border bg-card shadow-sm', className)}
103
+ >
104
+ <header className="flex items-center justify-between gap-3 border-b border-border bg-secondary/35 px-4 py-2.5">
105
+ <TitleTag id={titleId} className="text-[13.5px] font-semibold text-foreground">
106
+ {title}
107
+ </TitleTag>
108
+ <Button size="sm" icon={Play} loading={running} disabled={!canRun && !running} onClick={run}>
109
+ {running ? 'Running…' : 'Run'}
110
+ </Button>
111
+ </header>
112
+ <div className="grid md:grid-cols-[minmax(0,1fr)_272px]">
113
+ <div className="flex flex-col gap-4 p-4">
114
+ <Field>
115
+ <FieldLabel>System prompt</FieldLabel>
116
+ <Textarea rows={4} value={system} onValueChange={setSystem} className="font-mono text-[12.5px]" />
117
+ <FieldDescription>~{estimateTokens(system)} tokens</FieldDescription>
118
+ </Field>
119
+ <Field>
120
+ <FieldLabel required>User prompt</FieldLabel>
121
+ <Textarea
122
+ rows={6}
123
+ value={user}
124
+ onValueChange={setUser}
125
+ invalid={user.trim().length === 0}
126
+ className="font-mono text-[12.5px]"
127
+ />
128
+ <FieldDescription>
129
+ ~{estimateTokens(user)} tokens · use <code className="font-mono">{'{{name}}'}</code> for variables
130
+ </FieldDescription>
131
+ </Field>
132
+ </div>
133
+ <div className="flex flex-col gap-5 border-t border-border bg-secondary/20 p-4 md:border-t-0 md:border-s">
134
+ <div>
135
+ <SubTag className="text-[12px] font-semibold tracking-wide text-muted-foreground uppercase">
136
+ Variables
137
+ </SubTag>
138
+ {variables.length === 0 ? (
139
+ <p className="mt-2 text-[12.5px] text-muted-foreground">No variables in the prompts.</p>
140
+ ) : (
141
+ <>
142
+ <ul aria-label="Detected variables" className="mt-2 flex flex-wrap gap-1.5">
143
+ {variables.map((v) => (
144
+ <li key={v}>
145
+ <Token
146
+ size="sm"
147
+ tone={values[v]?.trim() ? 'primary' : 'warning'}
148
+ icon={<Braces size={12} aria-hidden />}
149
+ label={v}
150
+ />
151
+ </li>
152
+ ))}
153
+ </ul>
154
+ <div className="mt-3 flex flex-col gap-2.5">
155
+ {variables.map((v) => (
156
+ <Field key={v}>
157
+ <FieldLabel className="font-mono text-[12px]">{v}</FieldLabel>
158
+ <Input size="sm" value={values[v] ?? ''} onValueChange={(next) => setValue(v, String(next))} />
159
+ </Field>
160
+ ))}
161
+ </div>
162
+ {missing.length > 0 && (
163
+ <p className="mt-2 text-[12px] text-warning-text">
164
+ {missing.length} {missing.length === 1 ? 'variable has' : 'variables have'} no value
165
+ </p>
166
+ )}
167
+ </>
168
+ )}
169
+ </div>
170
+ <div className="flex flex-col gap-4">
171
+ <SubTag className="text-[12px] font-semibold tracking-wide text-muted-foreground uppercase">
172
+ Parameters
173
+ </SubTag>
174
+ <Slider
175
+ label="Temperature"
176
+ showValue
177
+ min={0}
178
+ max={2}
179
+ step={0.1}
180
+ value={temperature}
181
+ onValueChange={(v) => setTemperature(Array.isArray(v) ? (v[0] ?? 0) : v)}
182
+ />
183
+ <Field invalid={maxTokensInvalid}>
184
+ <FieldLabel>Max tokens</FieldLabel>
185
+ <Input
186
+ size="sm"
187
+ type="number"
188
+ min={1}
189
+ max={maxTokensLimit}
190
+ step={1}
191
+ value={maxTokens}
192
+ onValueChange={(v) => setMaxTokens(String(v))}
193
+ invalid={maxTokensInvalid}
194
+ />
195
+ <FieldDescription>1 – {maxTokensLimit.toLocaleString('en-US')}</FieldDescription>
196
+ </Field>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </section>
201
+ );
202
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Prompt editor',
5
+ family: 'ai',
6
+ status: 'beta',
7
+ description:
8
+ 'System and user prompt editors that detect {{variables}} as chips with value inputs, plus temperature and max-token parameters and a Run action. Variable values can be controlled (variables, onVariablesChange).',
9
+ uses: ['Textarea', 'Token', 'Field', 'Input', 'Slider', 'Button'],
10
+ };
@@ -0,0 +1,23 @@
1
+ import { useState } from 'react';
2
+ import { PromptEditor } from '../PromptEditor';
3
+
4
+ /** The page owns the variable values, e.g. to fill them from a selected dataset row. */
5
+ export default function ControlledVariablesPromptEditor() {
6
+ const [variables, setVariables] = useState<Record<string, string>>({
7
+ company: 'Acme',
8
+ language: 'English',
9
+ ticket: '',
10
+ });
11
+ return (
12
+ <div className="flex flex-col gap-2">
13
+ <button
14
+ type="button"
15
+ onClick={() => setVariables((prev) => ({ ...prev, ticket: 'Cannot reset my password' }))}
16
+ className="self-start text-[12.5px] text-primary-text underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"
17
+ >
18
+ Fill from sample ticket
19
+ </button>
20
+ <PromptEditor variables={variables} onVariablesChange={setVariables} />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,12 @@
1
+ export const promptDefaults = {
2
+ system:
3
+ 'You are a concise assistant for {{company}}. Answer in {{language}} and cite the source document when you use one.',
4
+ user: 'Summarise the ticket below in three bullet points and suggest a next step.\n\n{{ticket}}',
5
+ variables: {
6
+ company: 'Northwind',
7
+ language: 'English',
8
+ ticket: '',
9
+ } as Record<string, string>,
10
+ temperature: 0.7,
11
+ maxTokens: 1024,
12
+ };