@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,274 @@
1
+ import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from 'react';
2
+ import { cn, useI18n } from '@gntik-ai/ui';
3
+ import { CalendarHeader } from '../shared/CalendarHeader';
4
+ import {
5
+ addDays,
6
+ dayKey,
7
+ formatDay,
8
+ formatRange,
9
+ formatTime,
10
+ formatWeekday,
11
+ formatWeekRange,
12
+ hourOf,
13
+ sameDay,
14
+ startOfWeek,
15
+ weekDays,
16
+ } from '../shared/dates';
17
+ import { defaultEventKinds, groupByDay, kindOf, TONE, type EventKind, type ScheduleEvent } from '../shared/events';
18
+ import { sampleEvents, sampleToday } from '../shared/fixtures';
19
+
20
+ export interface WeekCalendarProps {
21
+ events?: ScheduleEvent[];
22
+ /** The app's "today" (the block never reads the clock). */
23
+ today?: Date;
24
+ /** Current time for the "now" line, shown when it falls in the visible week. Defaults to `today`. */
25
+ now?: Date;
26
+ /** A date inside the week shown first (uncontrolled). Defaults to `today`. */
27
+ defaultWeek?: Date;
28
+ /** Called with the Monday of the newly shown week. */
29
+ onWeekChange?: (weekStart: Date) => void;
30
+ /** Makes events buttons. */
31
+ onEventClick?: (event: ScheduleEvent) => void;
32
+ kinds?: Record<string, EventKind>;
33
+ /** Hour the grid scrolls to on mount. */
34
+ scrollToHour?: number;
35
+ /** Pixels per hour. */
36
+ hourHeight?: number;
37
+ /** Max height of the scrolling time grid in px. */
38
+ height?: number;
39
+ actions?: ReactNode;
40
+ className?: string;
41
+ }
42
+
43
+ const HOURS = Array.from({ length: 24 }, (_, h) => h);
44
+ const pad = (n: number) => String(n).padStart(2, '0');
45
+
46
+ interface Placed {
47
+ event: ScheduleEvent;
48
+ top: number;
49
+ height: number;
50
+ lane: number;
51
+ lanes: number;
52
+ }
53
+
54
+ /** Side-by-side lanes for overlapping events within one day. */
55
+ function layoutDay(list: readonly ScheduleEvent[], hourHeight: number): Placed[] {
56
+ const placed: Placed[] = [];
57
+ let cluster: Placed[] = [];
58
+ let clusterEnd = -1;
59
+ let laneEnds: number[] = [];
60
+ const close = () => {
61
+ for (const p of cluster) p.lanes = laneEnds.length;
62
+ cluster = [];
63
+ laneEnds = [];
64
+ };
65
+ for (const event of list) {
66
+ const start = hourOf(event.start);
67
+ const end = Math.max(sameDay(event.start, event.end) ? hourOf(event.end) : 24, start + 0.25);
68
+ if (start >= clusterEnd) close();
69
+ let lane = laneEnds.findIndex((e) => e <= start);
70
+ if (lane === -1) {
71
+ lane = laneEnds.length;
72
+ laneEnds.push(end);
73
+ } else laneEnds[lane] = end;
74
+ clusterEnd = Math.max(clusterEnd, end);
75
+ const p: Placed = {
76
+ event,
77
+ top: start * hourHeight + 1,
78
+ height: Math.max((end - start) * hourHeight - 2, 18),
79
+ lane,
80
+ lanes: 1,
81
+ };
82
+ cluster.push(p);
83
+ placed.push(p);
84
+ }
85
+ close();
86
+ return placed;
87
+ }
88
+
89
+ /** Week view: seven day columns over a scrolling 24-hour time grid, with a "now" line. */
90
+ export function WeekCalendar({
91
+ events = sampleEvents,
92
+ today = sampleToday,
93
+ now,
94
+ defaultWeek,
95
+ onWeekChange,
96
+ onEventClick,
97
+ kinds = defaultEventKinds,
98
+ scrollToHour = 7,
99
+ hourHeight = 52,
100
+ height = 480,
101
+ actions,
102
+ className,
103
+ }: WeekCalendarProps) {
104
+ const { t, locale } = useI18n();
105
+ const titleId = useId();
106
+ const [weekStart, setWeekStart] = useState<Date>(() => startOfWeek(defaultWeek ?? today));
107
+ const scrollRef = useRef<HTMLDivElement>(null);
108
+ const byDay = useMemo(() => groupByDay(events), [events]);
109
+ const days = weekDays(weekStart);
110
+ const nowAt = now ?? today;
111
+ const nowVisible = days.some((d) => sameDay(d, nowAt));
112
+
113
+ useEffect(() => {
114
+ if (scrollRef.current) scrollRef.current.scrollTop = Math.max(scrollToHour * hourHeight - 8, 0);
115
+ }, [scrollToHour, hourHeight]);
116
+
117
+ const show = (start: Date) => {
118
+ setWeekStart(start);
119
+ onWeekChange?.(start);
120
+ };
121
+
122
+ return (
123
+ <div className={cn('overflow-hidden rounded-xl border border-border bg-card', className)}>
124
+ <CalendarHeader
125
+ titleId={titleId}
126
+ title={formatWeekRange(days, locale)}
127
+ prevLabel={t('calendar.previousWeek')}
128
+ nextLabel={t('calendar.nextWeek')}
129
+ onPrev={() => show(addDays(weekStart, -7))}
130
+ onNext={() => show(addDays(weekStart, 7))}
131
+ onToday={() => show(startOfWeek(today))}
132
+ actions={actions}
133
+ />
134
+ <div className="overflow-x-auto">
135
+ <div className="min-w-[640px]">
136
+ <div aria-hidden className="flex border-b border-border">
137
+ <div className="w-14 shrink-0 border-e border-border/70" />
138
+ <div className="grid flex-1 grid-cols-7">
139
+ {days.map((d) => {
140
+ const isToday = sameDay(d, today);
141
+ return (
142
+ <div
143
+ key={dayKey(d)}
144
+ className="flex flex-col items-center justify-center border-s border-border/70 py-2 first:border-s-0"
145
+ >
146
+ <span className="text-[11px] text-muted-foreground">{formatWeekday(d, locale)}</span>
147
+ <span
148
+ className={cn(
149
+ 'mt-1 inline-flex size-7 items-center justify-center rounded-full text-[13px] tabular-nums',
150
+ isToday ? 'bg-primary font-semibold text-primary-foreground' : 'font-medium text-foreground',
151
+ )}
152
+ >
153
+ {d.getDate()}
154
+ </span>
155
+ </div>
156
+ );
157
+ })}
158
+ </div>
159
+ </div>
160
+ <div
161
+ ref={scrollRef}
162
+ tabIndex={0}
163
+ role="region"
164
+ aria-labelledby={titleId}
165
+ className="overflow-y-auto focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring"
166
+ style={{ maxHeight: height }}
167
+ >
168
+ <div className="relative flex" style={{ height: 24 * hourHeight }}>
169
+ <div aria-hidden className="relative w-14 shrink-0 border-e border-border/70">
170
+ {HOURS.slice(1).map((h) => (
171
+ <span
172
+ key={h}
173
+ className="absolute end-2 font-mono text-[10px] text-muted-foreground"
174
+ style={{ top: h * hourHeight - 6 }}
175
+ >
176
+ {pad(h)}:00
177
+ </span>
178
+ ))}
179
+ </div>
180
+ <div className="relative flex-1">
181
+ <div aria-hidden className="absolute inset-0">
182
+ {HOURS.map((h) => (
183
+ <div
184
+ key={h}
185
+ className="border-t border-border/70 first:border-t-0"
186
+ style={{ height: hourHeight }}
187
+ />
188
+ ))}
189
+ </div>
190
+ <div className="relative grid h-full grid-cols-7">
191
+ {days.map((d) => {
192
+ const list = byDay.get(dayKey(d)) ?? [];
193
+ return (
194
+ <ul
195
+ key={dayKey(d)}
196
+ aria-label={formatDay(d, locale)}
197
+ className="relative border-s border-border/70 first:border-s-0"
198
+ >
199
+ {layoutDay(list, hourHeight).map(({ event, top, height: h, lane, lanes }) => {
200
+ const kind = kindOf(event, kinds);
201
+ const tone = TONE[kind.tone];
202
+ const range = formatRange(event.start, event.end, locale);
203
+ const body = (
204
+ <>
205
+ <span className="block truncate text-[11px] leading-tight font-semibold text-foreground">
206
+ {event.title}
207
+ </span>
208
+ <span className="block truncate font-mono text-[10px] text-muted-foreground">
209
+ {formatTime(event.start, locale)}
210
+ </span>
211
+ </>
212
+ );
213
+ const box = cn(
214
+ 'block h-full w-full overflow-hidden rounded-md border-s-2 px-2 py-1 text-start',
215
+ tone.soft,
216
+ tone.bar,
217
+ );
218
+ return (
219
+ <li
220
+ key={event.id}
221
+ className="absolute px-0.5"
222
+ style={{
223
+ top,
224
+ height: h,
225
+ insetInlineStart: `${(lane / lanes) * 100}%`,
226
+ width: `${100 / lanes}%`,
227
+ }}
228
+ >
229
+ {onEventClick ? (
230
+ <button
231
+ type="button"
232
+ aria-label={`${event.title}, ${range}${event.location ? `, ${event.location}` : ''}`}
233
+ onClick={() => onEventClick(event)}
234
+ className={cn(
235
+ box,
236
+ 'transition-colors hover:bg-accent/40 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-focus-ring motion-reduce:transition-none',
237
+ )}
238
+ >
239
+ {body}
240
+ </button>
241
+ ) : (
242
+ <div className={box} title={`${event.title} · ${range}`}>
243
+ {body}
244
+ <span className="sr-only">, {range}</span>
245
+ </div>
246
+ )}
247
+ </li>
248
+ );
249
+ })}
250
+ </ul>
251
+ );
252
+ })}
253
+ </div>
254
+ {nowVisible && (
255
+ <div
256
+ aria-hidden
257
+ className="pointer-events-none absolute inset-x-0 z-10 flex items-center"
258
+ style={{ top: hourOf(nowAt) * hourHeight }}
259
+ >
260
+ <span className="-ms-1 size-2 shrink-0 rounded-full bg-primary" />
261
+ <span className="h-px flex-1 bg-primary" />
262
+ <span className="shrink-0 rounded bg-primary px-1 font-mono text-[9px] text-primary-foreground">
263
+ {t('calendar.now')}
264
+ </span>
265
+ </div>
266
+ )}
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ );
274
+ }
@@ -0,0 +1,10 @@
1
+ import type { BlockMeta } from '../../meta';
2
+
3
+ export const meta: BlockMeta = {
4
+ name: 'Week calendar',
5
+ family: 'scheduling',
6
+ status: 'beta',
7
+ description:
8
+ 'Seven-day time grid with positioned events (overlaps side by side), a now line, week navigation and optional clickable events.',
9
+ uses: ['Button', 'IconButton'],
10
+ };
@@ -0,0 +1,11 @@
1
+ export * from './MonthCalendar/MonthCalendar';
2
+ export { meta as monthCalendarMeta } from './MonthCalendar/block.meta';
3
+ export * from './WeekCalendar/WeekCalendar';
4
+ export { meta as weekCalendarMeta } from './WeekCalendar/block.meta';
5
+ export * from './AgendaList/AgendaList';
6
+ export { meta as agendaListMeta } from './AgendaList/block.meta';
7
+ export * from './TimeSlotPicker/TimeSlotPicker';
8
+ export { meta as timeSlotPickerMeta } from './TimeSlotPicker/block.meta';
9
+ export { sampleTimeSlots } from './TimeSlotPicker/fixtures';
10
+ export { defaultEventKinds, type ScheduleEvent, type EventKind, type EventTone } from './shared/events';
11
+ export { sampleEvents as sampleScheduleEvents, sampleToday as sampleScheduleToday } from './shared/fixtures';
@@ -0,0 +1,54 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Button, IconButton, useI18n } from '@gntik-ai/ui';
3
+ import { ChevronLeft, ChevronRight } from '@gntik-ai/icons';
4
+
5
+ export interface CalendarHeaderProps {
6
+ titleId: string;
7
+ title: string;
8
+ subtitle?: string;
9
+ prevLabel: string;
10
+ nextLabel: string;
11
+ onPrev: () => void;
12
+ onNext: () => void;
13
+ onToday: () => void;
14
+ actions?: ReactNode;
15
+ }
16
+
17
+ /** Title + previous / today / next navigation + caller actions. */
18
+ export function CalendarHeader({
19
+ titleId,
20
+ title,
21
+ subtitle,
22
+ prevLabel,
23
+ nextLabel,
24
+ onPrev,
25
+ onNext,
26
+ onToday,
27
+ actions,
28
+ }: CalendarHeaderProps) {
29
+ const { t } = useI18n();
30
+ return (
31
+ <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3 sm:px-5">
32
+ <div className="min-w-0">
33
+ <h3
34
+ id={titleId}
35
+ aria-live="polite"
36
+ className="truncate text-[15px] font-semibold tracking-tight text-foreground"
37
+ >
38
+ {title}
39
+ </h3>
40
+ {subtitle && <p className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground">{subtitle}</p>}
41
+ </div>
42
+ <div className="flex shrink-0 items-center gap-2">
43
+ <div className="flex items-center gap-1">
44
+ <IconButton icon={ChevronLeft} label={prevLabel} variant="secondary" size="sm" onClick={onPrev} />
45
+ <Button variant="secondary" size="sm" onClick={onToday}>
46
+ {t('calendar.today')}
47
+ </Button>
48
+ <IconButton icon={ChevronRight} label={nextLabel} variant="secondary" size="sm" onClick={onNext} />
49
+ </div>
50
+ {actions}
51
+ </div>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,21 @@
1
+ import { cn } from '@gntik-ai/ui';
2
+ import { TONE, type EventKind } from './events';
3
+
4
+ /** Square type tile (no avatars on scheduled work). */
5
+ export function EventTile({ kind, className }: { kind: EventKind; className?: string }) {
6
+ const Glyph = kind.icon;
7
+ const tone = TONE[kind.tone];
8
+ return (
9
+ <span
10
+ aria-hidden
11
+ className={cn(
12
+ 'inline-flex size-9 shrink-0 items-center justify-center rounded-lg',
13
+ tone.soft,
14
+ tone.icon,
15
+ className,
16
+ )}
17
+ >
18
+ <Glyph size={17} />
19
+ </span>
20
+ );
21
+ }
@@ -0,0 +1,98 @@
1
+ /** Native-Date helpers for the scheduling blocks. Weeks start on Monday. */
2
+
3
+ const pad = (n: number) => String(n).padStart(2, '0');
4
+
5
+ /** Local calendar key "YYYY-MM-DD". */
6
+ export function dayKey(d: Date): string {
7
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
8
+ }
9
+
10
+ export function startOfDay(d: Date): Date {
11
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
12
+ }
13
+
14
+ export function addDays(d: Date, n: number): Date {
15
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
16
+ }
17
+
18
+ export function addMonths(d: Date, n: number): Date {
19
+ return new Date(d.getFullYear(), d.getMonth() + n, 1);
20
+ }
21
+
22
+ export function startOfMonth(d: Date): Date {
23
+ return new Date(d.getFullYear(), d.getMonth(), 1);
24
+ }
25
+
26
+ export function sameDay(a: Date, b: Date): boolean {
27
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
28
+ }
29
+
30
+ export function sameMonth(a: Date, b: Date): boolean {
31
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
32
+ }
33
+
34
+ /** Monday of the week containing `d`. */
35
+ export function startOfWeek(d: Date): Date {
36
+ return addDays(startOfDay(d), -((d.getDay() + 6) % 7));
37
+ }
38
+
39
+ export function weekDays(d: Date): Date[] {
40
+ const mon = startOfWeek(d);
41
+ return Array.from({ length: 7 }, (_, i) => addDays(mon, i));
42
+ }
43
+
44
+ /** Six Monday-first weeks covering the month of `d`. */
45
+ export function monthGrid(d: Date): Date[][] {
46
+ const start = startOfWeek(startOfMonth(d));
47
+ return Array.from({ length: 6 }, (_, w) => Array.from({ length: 7 }, (_, i) => addDays(start, w * 7 + i)));
48
+ }
49
+
50
+ /** Decimal hour of the day: 10:30 → 10.5. */
51
+ export function hourOf(d: Date): number {
52
+ return d.getHours() + d.getMinutes() / 60;
53
+ }
54
+
55
+ const cache = new Map<string, Intl.DateTimeFormat>();
56
+ /** Cached Intl formatter; `locale` defaults to en-US (pass `useI18n().locale` to localise). */
57
+ const fmt = (locale: string | undefined, opts: Intl.DateTimeFormatOptions) => {
58
+ const key = `${locale ?? 'en-US'}|${JSON.stringify(opts)}`;
59
+ let f = cache.get(key);
60
+ if (!f) {
61
+ f = new Intl.DateTimeFormat(locale ?? 'en-US', opts);
62
+ cache.set(key, f);
63
+ }
64
+ return f;
65
+ };
66
+
67
+ /** "09:30". */
68
+ export const formatTime = (d: Date, locale?: string) =>
69
+ fmt(locale, { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(d);
70
+ /** "09:30–10:00". */
71
+ export const formatRange = (start: Date, end: Date, locale?: string) =>
72
+ `${formatTime(start, locale)}–${formatTime(end, locale)}`;
73
+ /** "Wednesday, April 15, 2026". */
74
+ export const formatFullDate = (d: Date, locale?: string) =>
75
+ fmt(locale, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }).format(d);
76
+ /** "Wednesday, April 15". */
77
+ export const formatDay = (d: Date, locale?: string) =>
78
+ fmt(locale, { weekday: 'long', month: 'long', day: 'numeric' }).format(d);
79
+ /** "April 2026". */
80
+ export const formatMonth = (d: Date, locale?: string) => fmt(locale, { month: 'long', year: 'numeric' }).format(d);
81
+ /** "Apr 15". */
82
+ export const formatShort = (d: Date, locale?: string) => fmt(locale, { month: 'short', day: 'numeric' }).format(d);
83
+ /** "Wed". */
84
+ export const formatWeekday = (d: Date, locale?: string, weekday: 'short' | 'long' = 'short') =>
85
+ fmt(locale, { weekday }).format(d);
86
+
87
+ /** "Apr 13 – 19, 2026" (or across months / years). */
88
+ export function formatWeekRange(days: readonly Date[], locale?: string): string {
89
+ const first = days[0];
90
+ const last = days[days.length - 1];
91
+ if (!first || !last) return '';
92
+ if (locale && !locale.toLowerCase().startsWith('en'))
93
+ return fmt(locale, { month: 'short', day: 'numeric', year: 'numeric' }).formatRange(first, last);
94
+ if (sameMonth(first, last)) return `${formatShort(first)} – ${last.getDate()}, ${last.getFullYear()}`;
95
+ if (first.getFullYear() === last.getFullYear())
96
+ return `${formatShort(first)} – ${formatShort(last)}, ${last.getFullYear()}`;
97
+ return `${formatShort(first)}, ${first.getFullYear()} – ${formatShort(last)}, ${last.getFullYear()}`;
98
+ }
@@ -0,0 +1,90 @@
1
+ import { CalendarCheck, Cog, Database, RefreshCw, ShieldCheck, Upload, type LucideIcon } from '@gntik-ai/icons';
2
+ import type { BadgeTone } from '@gntik-ai/ui';
3
+ import { dayKey } from './dates';
4
+
5
+ /** A scheduled item on a calendar. */
6
+ export interface ScheduleEvent {
7
+ id: string;
8
+ title: string;
9
+ start: Date;
10
+ end: Date;
11
+ /** Key into the `kinds` map (job, deploy, review, backup, maintenance…). */
12
+ kind?: string;
13
+ /** Region, room or target, shown after the time. */
14
+ location?: string;
15
+ }
16
+
17
+ export type EventTone = 'primary' | 'violet' | 'cyan' | 'amber' | 'neutral';
18
+
19
+ export interface EventKind {
20
+ label: string;
21
+ icon: LucideIcon;
22
+ tone: EventTone;
23
+ }
24
+
25
+ /** Default kinds: brand green for jobs, categorical accents for the rest (never severity). */
26
+ export const defaultEventKinds: Record<string, EventKind> = {
27
+ job: { label: 'Job', icon: RefreshCw, tone: 'primary' },
28
+ deploy: { label: 'Deploy', icon: Upload, tone: 'violet' },
29
+ review: { label: 'Review', icon: ShieldCheck, tone: 'cyan' },
30
+ backup: { label: 'Backup', icon: Database, tone: 'amber' },
31
+ maintenance: { label: 'Maintenance', icon: Cog, tone: 'neutral' },
32
+ };
33
+
34
+ const FALLBACK: EventKind = { label: 'Event', icon: CalendarCheck, tone: 'primary' };
35
+
36
+ export function kindOf(event: ScheduleEvent, kinds: Record<string, EventKind>): EventKind {
37
+ return (event.kind ? kinds[event.kind] : undefined) ?? FALLBACK;
38
+ }
39
+
40
+ /** Token classes per tone. Text stays foreground on category tints (no contrast alias for them). */
41
+ export const TONE: Record<EventTone, { dot: string; soft: string; bar: string; icon: string; badge: BadgeTone }> = {
42
+ primary: {
43
+ dot: 'bg-primary',
44
+ soft: 'bg-primary/12',
45
+ bar: 'border-primary',
46
+ icon: 'text-primary-text',
47
+ badge: 'primary',
48
+ },
49
+ violet: {
50
+ dot: 'bg-category-violet',
51
+ soft: 'bg-category-violet/14',
52
+ bar: 'border-category-violet',
53
+ icon: 'text-foreground',
54
+ badge: 'violet',
55
+ },
56
+ cyan: {
57
+ dot: 'bg-category-cyan',
58
+ soft: 'bg-category-cyan/14',
59
+ bar: 'border-category-cyan',
60
+ icon: 'text-foreground',
61
+ badge: 'cyan',
62
+ },
63
+ amber: {
64
+ dot: 'bg-category-amber',
65
+ soft: 'bg-category-amber/16',
66
+ bar: 'border-category-amber',
67
+ icon: 'text-foreground',
68
+ badge: 'amber',
69
+ },
70
+ neutral: {
71
+ dot: 'bg-muted-foreground',
72
+ soft: 'bg-secondary',
73
+ bar: 'border-muted-foreground',
74
+ icon: 'text-muted-foreground',
75
+ badge: 'neutral',
76
+ },
77
+ };
78
+
79
+ /** Events grouped by local day key, each day sorted by start. */
80
+ export function groupByDay(events: readonly ScheduleEvent[]): Map<string, ScheduleEvent[]> {
81
+ const map = new Map<string, ScheduleEvent[]>();
82
+ for (const e of events) {
83
+ const key = dayKey(e.start);
84
+ const list = map.get(key);
85
+ if (list) list.push(e);
86
+ else map.set(key, [e]);
87
+ }
88
+ for (const list of map.values()) list.sort((a, b) => a.start.getTime() - b.start.getTime());
89
+ return map;
90
+ }
@@ -0,0 +1,54 @@
1
+ import type { ScheduleEvent } from './events';
2
+
3
+ /** The sample calendar's "today" (Wed 15 Apr 2026, 10:20). Pass your own `today` in apps. */
4
+ export const sampleToday = new Date(2026, 3, 15, 10, 20);
5
+
6
+ type Row = [day: number, kind: string, title: string, start: number, hours: number, location?: string];
7
+
8
+ const ROWS: Row[] = [
9
+ [3, 'deploy', 'API gateway v3.1', 10, 1, 'eu-west-1'],
10
+ [7, 'job', 'Data import batch', 6.5, 2],
11
+ [7, 'review', 'Access review', 14, 1, 'eu-west-1'],
12
+ [9, 'backup', 'Snapshot · search index', 11, 1.5],
13
+ [13, 'job', 'Inbox sync', 7, 0.5],
14
+ [13, 'deploy', 'Billing service v2.4 rollout', 9.5, 1.5, 'us-east-1'],
15
+ [13, 'review', 'Access review', 14, 1, 'eu-west-1'],
16
+ [14, 'job', 'Data import batch', 6.5, 2],
17
+ [14, 'backup', 'Snapshot · search index', 11, 1.5],
18
+ [14, 'job', 'Usage rollup', 21, 0.5],
19
+ [15, 'job', 'Inbox sync', 6, 0.5],
20
+ [15, 'job', 'Report export', 7, 0.5],
21
+ [15, 'deploy', 'Checkout hotfix', 9, 1, 'eu-west-1'],
22
+ [15, 'review', 'Compliance review', 10.5, 1.5, 'all regions'],
23
+ [15, 'backup', 'Snapshot · primary database', 13, 1],
24
+ [15, 'job', 'Weekly digest', 15, 0.5],
25
+ [15, 'maintenance', 'Maintenance window', 16, 2, 'ap-south-1'],
26
+ [15, 'job', 'Search reindex', 19, 1],
27
+ [15, 'job', 'Usage rollup', 21.5, 0.5],
28
+ [16, 'job', 'Inbox sync', 7, 0.5],
29
+ [16, 'deploy', 'Notifications v1.8', 13.5, 1, 'us-east-1'],
30
+ [16, 'maintenance', 'Node pool upgrade', 22, 1.5],
31
+ [17, 'review', 'Weekly cost review', 10, 1],
32
+ [17, 'backup', 'Full backup', 22.5, 1],
33
+ [18, 'job', 'Weekend health check', 9, 0.5],
34
+ [19, 'maintenance', 'Certificate rotation', 3, 1],
35
+ [22, 'job', 'Inbox sync', 7, 0.5],
36
+ [22, 'deploy', 'Billing service v2.5', 11, 1, 'us-east-1'],
37
+ [22, 'review', 'Policy review', 15, 1],
38
+ [24, 'job', 'Data import batch', 6.5, 2],
39
+ [28, 'maintenance', 'Maintenance window', 16, 2, 'eu-west-1'],
40
+ [30, 'review', 'Month-end review', 9, 2],
41
+ [30, 'backup', 'Full backup', 22.5, 1],
42
+ ];
43
+
44
+ const at = (day: number, hour: number) => new Date(2026, 3, day, Math.floor(hour), Math.round((hour % 1) * 60));
45
+
46
+ /** Sample scheduled work for April 2026 (jobs, deploys, reviews, backups, maintenance). */
47
+ export const sampleEvents: ScheduleEvent[] = ROWS.map(([day, kind, title, start, hours, location], i) => ({
48
+ id: `evt-${i + 1}`,
49
+ title,
50
+ kind,
51
+ start: at(day, start),
52
+ end: at(day, start + hours),
53
+ location,
54
+ }));
@@ -0,0 +1,38 @@
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 { AppSidebar } from './AppSidebar';
5
+
6
+ describe('AppSidebar', () => {
7
+ it('renders the workspace switcher, navigation, usage, help and user', async () => {
8
+ const { container } = render(<AppSidebar />);
9
+ expect(screen.getByRole('button', { name: /Acme Industries/ })).toBeInTheDocument();
10
+ const nav = screen.getByRole('navigation', { name: 'Main' });
11
+ expect(within(nav).getByRole('link', { name: 'Projects 12' })).toHaveAttribute('aria-current', 'page');
12
+ expect(screen.getByRole('meter', { name: 'Monthly requests' })).toBeInTheDocument();
13
+ expect(screen.getByRole('link', { name: 'Help & docs' })).toBeInTheDocument();
14
+ expect(screen.getByText('Dana Whitfield')).toBeInTheDocument();
15
+ await expectNoAxeViolations(container);
16
+ }, 15_000);
17
+
18
+ it('switches workspace and reports navigation', async () => {
19
+ const user = userEvent.setup();
20
+ const onWorkspaceChange = vi.fn();
21
+ const onNavigate = vi.fn();
22
+ render(<AppSidebar onWorkspaceChange={onWorkspaceChange} onNavigate={onNavigate} />);
23
+ await user.click(screen.getByRole('button', { name: /Acme Industries/ }));
24
+ await user.click(await screen.findByRole('menuitemradio', { name: /Northbeam Labs/ }));
25
+ expect(onWorkspaceChange).toHaveBeenCalledWith(expect.objectContaining({ id: 'northbeam' }));
26
+ expect(screen.getByRole('button', { name: /Northbeam Labs/ })).toBeInTheDocument();
27
+ await user.click(screen.getByRole('link', { name: 'Billing' }));
28
+ expect(onNavigate).toHaveBeenCalledWith(expect.objectContaining({ href: '/billing' }));
29
+ });
30
+
31
+ it('collapsed: icon rail with the brand mark and an icon-only help link', async () => {
32
+ const { container } = render(<AppSidebar collapsed />);
33
+ expect(screen.queryByRole('meter')).not.toBeInTheDocument();
34
+ expect(screen.getByRole('link', { name: 'Help & docs' })).toBeInTheDocument();
35
+ expect(screen.getByRole('navigation')).toHaveAttribute('data-collapsed');
36
+ await expectNoAxeViolations(container);
37
+ });
38
+ });