@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.
- package/dist/index.d.ts +3494 -0
- package/dist/index.js +14911 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -3
- package/src/ai/AssistantCardGrid/AssistantCardGrid.test.tsx +32 -0
- package/src/ai/AssistantCardGrid/AssistantCardGrid.tsx +98 -0
- package/src/ai/AssistantCardGrid/block.meta.ts +10 -0
- package/src/ai/AssistantCardGrid/fixtures.ts +72 -0
- package/src/ai/EvaluationScorecard/EvaluationScorecard.test.tsx +39 -0
- package/src/ai/EvaluationScorecard/EvaluationScorecard.tsx +233 -0
- package/src/ai/EvaluationScorecard/block.meta.ts +10 -0
- package/src/ai/EvaluationScorecard/fixtures.ts +79 -0
- package/src/ai/ModelKeysList/ModelKeysList.test.tsx +44 -0
- package/src/ai/ModelKeysList/ModelKeysList.tsx +203 -0
- package/src/ai/ModelKeysList/block.meta.ts +10 -0
- package/src/ai/ModelKeysList/fixtures.ts +44 -0
- package/src/ai/PromptEditor/PromptEditor.test.tsx +97 -0
- package/src/ai/PromptEditor/PromptEditor.tsx +202 -0
- package/src/ai/PromptEditor/block.meta.ts +10 -0
- package/src/ai/PromptEditor/examples/controlled-variables.tsx +23 -0
- package/src/ai/PromptEditor/fixtures.ts +12 -0
- package/src/ai/TokenCostCard/TokenCostCard.test.tsx +36 -0
- package/src/ai/TokenCostCard/TokenCostCard.tsx +130 -0
- package/src/ai/TokenCostCard/block.meta.ts +9 -0
- package/src/ai/TokenCostCard/fixtures.ts +18 -0
- package/src/ai/TraceWaterfall/TraceWaterfall.test.tsx +119 -0
- package/src/ai/TraceWaterfall/TraceWaterfall.tsx +317 -0
- package/src/ai/TraceWaterfall/block.meta.ts +10 -0
- package/src/ai/TraceWaterfall/examples/no-details.tsx +6 -0
- package/src/ai/TraceWaterfall/examples/running.tsx +16 -0
- package/src/ai/TraceWaterfall/fixtures.ts +133 -0
- package/src/ai/TraceWaterfall/span-details.tsx +51 -0
- package/src/ai/index.ts +13 -0
- package/src/auth/ForgotPasswordForm/ForgotPasswordForm.test.tsx +209 -0
- package/src/auth/ForgotPasswordForm/ForgotPasswordForm.tsx +211 -0
- package/src/auth/ForgotPasswordForm/block.meta.ts +10 -0
- package/src/auth/ForgotPasswordForm/examples/UsernameOrEmail.tsx +24 -0
- package/src/auth/MfaChallenge/MfaChallenge.test.tsx +35 -0
- package/src/auth/MfaChallenge/MfaChallenge.tsx +170 -0
- package/src/auth/MfaChallenge/block.meta.ts +9 -0
- package/src/auth/ResetPasswordForm/ResetPasswordForm.test.tsx +27 -0
- package/src/auth/ResetPasswordForm/ResetPasswordForm.tsx +100 -0
- package/src/auth/ResetPasswordForm/block.meta.ts +10 -0
- package/src/auth/SignInForm/SignInForm.test.tsx +222 -0
- package/src/auth/SignInForm/SignInForm.tsx +226 -0
- package/src/auth/SignInForm/block.meta.ts +22 -0
- package/src/auth/SignInForm/examples/Username.tsx +26 -0
- package/src/auth/SignUpForm/SignUpForm.test.tsx +33 -0
- package/src/auth/SignUpForm/SignUpForm.tsx +154 -0
- package/src/auth/SignUpForm/block.meta.ts +9 -0
- package/src/auth/SsoButtons/SsoButtons.test.tsx +23 -0
- package/src/auth/SsoButtons/SsoButtons.tsx +52 -0
- package/src/auth/SsoButtons/block.meta.ts +9 -0
- package/src/auth/SsoButtons/fixtures.ts +15 -0
- package/src/auth/index.ts +15 -0
- package/src/auth/shared.tsx +113 -0
- package/src/billing/CostBreakdown/CostBreakdown.test.tsx +24 -0
- package/src/billing/CostBreakdown/CostBreakdown.tsx +95 -0
- package/src/billing/CostBreakdown/block.meta.ts +10 -0
- package/src/billing/CostBreakdown/fixtures.ts +9 -0
- package/src/billing/InvoiceTable/InvoiceTable.test.tsx +29 -0
- package/src/billing/InvoiceTable/InvoiceTable.tsx +130 -0
- package/src/billing/InvoiceTable/block.meta.ts +10 -0
- package/src/billing/InvoiceTable/fixtures.ts +38 -0
- package/src/billing/PaymentMethodCard/PaymentMethodCard.test.tsx +28 -0
- package/src/billing/PaymentMethodCard/PaymentMethodCard.tsx +103 -0
- package/src/billing/PaymentMethodCard/block.meta.ts +10 -0
- package/src/billing/PaymentMethodCard/fixtures.ts +12 -0
- package/src/billing/PlanCard/PlanCard.baseline.test.tsx +26 -0
- package/src/billing/PlanCard/PlanCard.summary.test.tsx +227 -0
- package/src/billing/PlanCard/PlanCard.test.tsx +27 -0
- package/src/billing/PlanCard/PlanCard.tsx +140 -0
- package/src/billing/PlanCard/PlanSummary.tsx +101 -0
- package/src/billing/PlanCard/README.md +23 -0
- package/src/billing/PlanCard/__snapshots__/PlanCard.baseline.test.tsx.snap +5 -0
- package/src/billing/PlanCard/block.meta.ts +60 -0
- package/src/billing/PlanCard/examples/SummaryActionsOnly.tsx +12 -0
- package/src/billing/PlanCard/examples/SummaryFeaturesOnly.tsx +14 -0
- package/src/billing/PlanCard/examples/SummaryFull.tsx +28 -0
- package/src/billing/PlanCard/examples/SummaryMinimal.tsx +5 -0
- package/src/billing/PlanCard/examples/SummaryPriceOnly.tsx +15 -0
- package/src/billing/PlanCard/examples/SummaryRenewalOnly.tsx +14 -0
- package/src/billing/PlanCard/examples/SummaryUnassigned.tsx +5 -0
- package/src/billing/PlanCard/examples/SummaryUnassignedNoLabel.tsx +5 -0
- package/src/billing/PlanCard/examples/SummaryZeroPrice.tsx +15 -0
- package/src/billing/PlanCard/fixtures.ts +19 -0
- package/src/billing/PlanDefinitionCard/PlanDefinitionCard.test.tsx +111 -0
- package/src/billing/PlanDefinitionCard/PlanDefinitionCard.tsx +86 -0
- package/src/billing/PlanDefinitionCard/README.md +29 -0
- package/src/billing/PlanDefinitionCard/block.meta.ts +46 -0
- package/src/billing/PlanDefinitionCard/examples/Compact.tsx +23 -0
- package/src/billing/PlanDefinitionCard/examples/Full.tsx +23 -0
- package/src/billing/PlanDefinitionCard/fixtures.ts +12 -0
- package/src/billing/QuotaMeters/QuotaMeters.test.tsx +23 -0
- package/src/billing/QuotaMeters/QuotaMeters.tsx +92 -0
- package/src/billing/QuotaMeters/block.meta.ts +10 -0
- package/src/billing/QuotaMeters/fixtures.ts +9 -0
- package/src/billing/SpendVsBudget/SpendVsBudget.test.tsx +24 -0
- package/src/billing/SpendVsBudget/SpendVsBudget.tsx +105 -0
- package/src/billing/SpendVsBudget/block.meta.ts +10 -0
- package/src/billing/SpendVsBudget/fixtures.ts +18 -0
- package/src/billing/format.ts +42 -0
- package/src/billing/index.ts +21 -0
- package/src/builders/CodePanel/CodePanel.test.tsx +280 -0
- package/src/builders/CodePanel/CodePanel.tsx +239 -0
- package/src/builders/CodePanel/block.meta.ts +10 -0
- package/src/builders/CodePanel/examples/controlled.tsx +15 -0
- package/src/builders/CodePanel/examples/diagnostics.tsx +40 -0
- package/src/builders/CodePanel/fixtures.ts +95 -0
- package/src/builders/CodePanel/marker-store.ts +90 -0
- package/src/builders/CodePanel/markers.ts +62 -0
- package/src/builders/FlowBuilder/FlowBuilder.test.tsx +480 -0
- package/src/builders/FlowBuilder/FlowBuilder.tsx +343 -0
- package/src/builders/FlowBuilder/block.meta.ts +20 -0
- package/src/builders/FlowBuilder/connections.ts +67 -0
- package/src/builders/FlowBuilder/decorated-nodes.ts +80 -0
- package/src/builders/FlowBuilder/examples/extensions.tsx +120 -0
- package/src/builders/FlowBuilder/extensions.ts +37 -0
- package/src/builders/FlowBuilder/fixtures.ts +80 -0
- package/src/builders/FlowBuilder/parts.tsx +196 -0
- package/src/builders/FlowBuilder/problems.tsx +40 -0
- package/src/builders/LogViewer/LogViewer.test.tsx +97 -0
- package/src/builders/LogViewer/LogViewer.tsx +253 -0
- package/src/builders/LogViewer/block.meta.ts +10 -0
- package/src/builders/LogViewer/fixtures.ts +49 -0
- package/src/builders/index.ts +15 -0
- package/src/data-display/ActivityFeed/ActivityFeed.test.tsx +62 -0
- package/src/data-display/ActivityFeed/ActivityFeed.tsx +118 -0
- package/src/data-display/ActivityFeed/block.meta.ts +10 -0
- package/src/data-display/ActivityFeed/fixtures.ts +29 -0
- package/src/data-display/ChartCard/ChartCard.ranges.test.tsx +282 -0
- package/src/data-display/ChartCard/ChartCard.test.tsx +54 -0
- package/src/data-display/ChartCard/ChartCard.tsx +218 -0
- package/src/data-display/ChartCard/block.meta.ts +10 -0
- package/src/data-display/ChartCard/examples/custom-ranges.tsx +37 -0
- package/src/data-display/ChartCard/examples/heatmap.tsx +20 -0
- package/src/data-display/ChartCard/examples/toolbar-owned-range.tsx +34 -0
- package/src/data-display/ChartCard/fixtures.ts +82 -0
- package/src/data-display/CommentThread/CommentThread.test.tsx +29 -0
- package/src/data-display/CommentThread/CommentThread.tsx +120 -0
- package/src/data-display/CommentThread/block.meta.ts +10 -0
- package/src/data-display/CommentThread/fixtures.ts +28 -0
- package/src/data-display/DescriptionListCard/DescriptionListCard.test.tsx +28 -0
- package/src/data-display/DescriptionListCard/DescriptionListCard.tsx +120 -0
- package/src/data-display/DescriptionListCard/block.meta.ts +9 -0
- package/src/data-display/DescriptionListCard/fixtures.ts +12 -0
- package/src/data-display/KpiRow/KpiRow.test.tsx +29 -0
- package/src/data-display/KpiRow/KpiRow.tsx +101 -0
- package/src/data-display/KpiRow/block.meta.ts +10 -0
- package/src/data-display/KpiRow/fixtures.ts +34 -0
- package/src/data-display/ResourceCardGrid/ResourceCardGrid.test.tsx +22 -0
- package/src/data-display/ResourceCardGrid/ResourceCardGrid.tsx +98 -0
- package/src/data-display/ResourceCardGrid/block.meta.ts +10 -0
- package/src/data-display/ResourceCardGrid/fixtures.ts +72 -0
- package/src/data-display/StatCard/StatCard.test.tsx +30 -0
- package/src/data-display/StatCard/StatCard.tsx +138 -0
- package/src/data-display/StatCard/block.meta.ts +10 -0
- package/src/data-display/StatCard/fixtures.ts +26 -0
- package/src/data-display/StatusTimeline/StatusTimeline.test.tsx +153 -0
- package/src/data-display/StatusTimeline/StatusTimeline.tsx +114 -0
- package/src/data-display/StatusTimeline/block.meta.ts +10 -0
- package/src/data-display/StatusTimeline/examples/Empty.tsx +6 -0
- package/src/data-display/StatusTimeline/fixtures.ts +62 -0
- package/src/data-display/index.ts +34 -0
- package/src/data-display/shared/TrendDelta.tsx +60 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.test.tsx +47 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.tsx +145 -0
- package/src/feedback/ConfirmDestructive/block.meta.ts +10 -0
- package/src/feedback/EmptyStates/EmptyStates.test.tsx +48 -0
- package/src/feedback/EmptyStates/EmptyStates.tsx +191 -0
- package/src/feedback/EmptyStates/block.meta.ts +10 -0
- package/src/feedback/EmptyStates/fixtures.ts +6 -0
- package/src/feedback/ErrorPanel/ErrorPanel.test.tsx +37 -0
- package/src/feedback/ErrorPanel/ErrorPanel.tsx +146 -0
- package/src/feedback/ErrorPanel/block.meta.ts +10 -0
- package/src/feedback/InlineCallout/InlineCallout.test.tsx +33 -0
- package/src/feedback/InlineCallout/InlineCallout.tsx +81 -0
- package/src/feedback/InlineCallout/block.meta.ts +9 -0
- package/src/feedback/Skeletons/Skeletons.test.tsx +25 -0
- package/src/feedback/Skeletons/Skeletons.tsx +142 -0
- package/src/feedback/Skeletons/block.meta.ts +10 -0
- package/src/feedback/SystemBanner/SystemBanner.test.tsx +29 -0
- package/src/feedback/SystemBanner/SystemBanner.tsx +105 -0
- package/src/feedback/SystemBanner/block.meta.ts +9 -0
- package/src/feedback/index.ts +14 -0
- package/src/feedback/types.ts +13 -0
- package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.test.tsx +36 -0
- package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.tsx +197 -0
- package/src/forms/CreateApiKeyDialog/block.meta.ts +10 -0
- package/src/forms/CreateApiKeyDialog/fixtures.ts +31 -0
- package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.test.tsx +339 -0
- package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.tsx +193 -0
- package/src/forms/CredentialDisclosureDialog/block.meta.ts +10 -0
- package/src/forms/CredentialDisclosureDialog/examples/Fresh.tsx +32 -0
- package/src/forms/CredentialDisclosureDialog/examples/ResponseGating.tsx +69 -0
- package/src/forms/CredentialDisclosureDialog/examples/Stored.tsx +32 -0
- package/src/forms/CredentialDisclosureDialog/fixtures.ts +14 -0
- package/src/forms/DangerZone/ConfirmDestructiveDialog.tsx +101 -0
- package/src/forms/DangerZone/DangerZone.test.tsx +28 -0
- package/src/forms/DangerZone/DangerZone.tsx +86 -0
- package/src/forms/DangerZone/block.meta.ts +10 -0
- package/src/forms/DangerZone/fixtures.ts +36 -0
- package/src/forms/DateRangeFilter/DateRangeFilter.test.tsx +26 -0
- package/src/forms/DateRangeFilter/DateRangeFilter.tsx +83 -0
- package/src/forms/DateRangeFilter/block.meta.ts +9 -0
- package/src/forms/DialogWizard/DialogWizard.test.tsx +95 -0
- package/src/forms/DialogWizard/DialogWizard.tsx +39 -0
- package/src/forms/DialogWizard/block.meta.ts +10 -0
- package/src/forms/DialogWizard/examples/CreateResource.tsx +65 -0
- package/src/forms/DialogWizard/fixtures.ts +20 -0
- package/src/forms/FileUploadPanel/FileUploadPanel.test.tsx +43 -0
- package/src/forms/FileUploadPanel/FileUploadPanel.tsx +244 -0
- package/src/forms/FileUploadPanel/block.meta.ts +10 -0
- package/src/forms/FileUploadPanel/fixtures.ts +66 -0
- package/src/forms/FormSection/FormSection.test.tsx +34 -0
- package/src/forms/FormSection/FormSection.tsx +77 -0
- package/src/forms/FormSection/block.meta.ts +9 -0
- package/src/forms/InviteMembersDialog/InviteMembersDialog.test.tsx +37 -0
- package/src/forms/InviteMembersDialog/InviteMembersDialog.tsx +183 -0
- package/src/forms/InviteMembersDialog/block.meta.ts +10 -0
- package/src/forms/InviteMembersDialog/fixtures.ts +14 -0
- package/src/forms/SettingsRow/SettingsRow.test.tsx +50 -0
- package/src/forms/SettingsRow/SettingsRow.tsx +74 -0
- package/src/forms/SettingsRow/block.meta.ts +9 -0
- package/src/forms/ValidationSummary/ValidationSummary.test.tsx +46 -0
- package/src/forms/ValidationSummary/ValidationSummary.tsx +75 -0
- package/src/forms/ValidationSummary/block.meta.ts +9 -0
- package/src/forms/ValidationSummary/fixtures.ts +13 -0
- package/src/forms/index.ts +27 -0
- package/src/index.ts +14 -0
- package/src/marketing/ComparisonTable/ComparisonTable.test.tsx +69 -0
- package/src/marketing/ComparisonTable/ComparisonTable.tsx +245 -0
- package/src/marketing/ComparisonTable/block.meta.ts +10 -0
- package/src/marketing/ComparisonTable/examples/options.tsx +48 -0
- package/src/marketing/ComparisonTable/fixtures.ts +83 -0
- package/src/marketing/Faq/Faq.test.tsx +25 -0
- package/src/marketing/Faq/Faq.tsx +68 -0
- package/src/marketing/Faq/block.meta.ts +10 -0
- package/src/marketing/Faq/fixtures.ts +32 -0
- package/src/marketing/FeatureGrid/FeatureGrid.test.tsx +19 -0
- package/src/marketing/FeatureGrid/FeatureGrid.tsx +79 -0
- package/src/marketing/FeatureGrid/block.meta.ts +10 -0
- package/src/marketing/FeatureGrid/fixtures.ts +36 -0
- package/src/marketing/Hero/Hero.test.tsx +33 -0
- package/src/marketing/Hero/Hero.tsx +107 -0
- package/src/marketing/Hero/block.meta.ts +10 -0
- package/src/marketing/PricingTable/PricingTable.test.tsx +92 -0
- package/src/marketing/PricingTable/PricingTable.tsx +323 -0
- package/src/marketing/PricingTable/block.meta.ts +10 -0
- package/src/marketing/PricingTable/fixtures.ts +34 -0
- package/src/marketing/actions.tsx +40 -0
- package/src/marketing/index.ts +15 -0
- package/src/meta.ts +23 -0
- package/src/page-chrome/BulkActionBar/BulkActionBar.test.tsx +34 -0
- package/src/page-chrome/BulkActionBar/BulkActionBar.tsx +76 -0
- package/src/page-chrome/BulkActionBar/block.meta.ts +9 -0
- package/src/page-chrome/PageHeader/PageHeader.test.tsx +68 -0
- package/src/page-chrome/PageHeader/PageHeader.tsx +152 -0
- package/src/page-chrome/PageHeader/block.meta.ts +10 -0
- package/src/page-chrome/PageHeader/fixtures.ts +29 -0
- package/src/page-chrome/PageToolbar/PageToolbar.test.tsx +38 -0
- package/src/page-chrome/PageToolbar/PageToolbar.tsx +138 -0
- package/src/page-chrome/PageToolbar/block.meta.ts +10 -0
- package/src/page-chrome/PageToolbar/fixtures.ts +8 -0
- package/src/page-chrome/SectionHeader/SectionHeader.test.tsx +28 -0
- package/src/page-chrome/SectionHeader/SectionHeader.tsx +73 -0
- package/src/page-chrome/SectionHeader/block.meta.ts +9 -0
- package/src/page-chrome/StickyActionBar/StickyActionBar.test.tsx +29 -0
- package/src/page-chrome/StickyActionBar/StickyActionBar.tsx +66 -0
- package/src/page-chrome/StickyActionBar/block.meta.ts +9 -0
- package/src/page-chrome/index.ts +13 -0
- package/src/page-chrome/types.ts +25 -0
- package/src/scheduling/AgendaList/AgendaList.test.tsx +28 -0
- package/src/scheduling/AgendaList/AgendaList.tsx +165 -0
- package/src/scheduling/AgendaList/block.meta.ts +10 -0
- package/src/scheduling/MonthCalendar/MonthCalendar.test.tsx +32 -0
- package/src/scheduling/MonthCalendar/MonthCalendar.tsx +223 -0
- package/src/scheduling/MonthCalendar/block.meta.ts +10 -0
- package/src/scheduling/TimeSlotPicker/TimeSlotPicker.test.tsx +26 -0
- package/src/scheduling/TimeSlotPicker/TimeSlotPicker.tsx +115 -0
- package/src/scheduling/TimeSlotPicker/block.meta.ts +10 -0
- package/src/scheduling/TimeSlotPicker/fixtures.ts +10 -0
- package/src/scheduling/WeekCalendar/WeekCalendar.test.tsx +27 -0
- package/src/scheduling/WeekCalendar/WeekCalendar.tsx +274 -0
- package/src/scheduling/WeekCalendar/block.meta.ts +10 -0
- package/src/scheduling/index.ts +11 -0
- package/src/scheduling/shared/CalendarHeader.tsx +54 -0
- package/src/scheduling/shared/EventTile.tsx +21 -0
- package/src/scheduling/shared/dates.ts +98 -0
- package/src/scheduling/shared/events.ts +90 -0
- package/src/scheduling/shared/fixtures.ts +54 -0
- package/src/shell/AppSidebar/AppSidebar.test.tsx +38 -0
- package/src/shell/AppSidebar/AppSidebar.tsx +226 -0
- package/src/shell/AppSidebar/block.meta.ts +10 -0
- package/src/shell/AppSidebar/fixtures.ts +58 -0
- package/src/shell/AppTopbar/AppTopbar.test.tsx +41 -0
- package/src/shell/AppTopbar/AppTopbar.tsx +82 -0
- package/src/shell/AppTopbar/block.meta.ts +19 -0
- package/src/shell/AppTopbar/fixtures.ts +22 -0
- package/src/shell/EnvironmentBadge/EnvironmentBadge.test.tsx +22 -0
- package/src/shell/EnvironmentBadge/EnvironmentBadge.tsx +55 -0
- package/src/shell/EnvironmentBadge/block.meta.ts +10 -0
- package/src/shell/GlobalSearch/GlobalSearch.test.tsx +28 -0
- package/src/shell/GlobalSearch/GlobalSearch.tsx +80 -0
- package/src/shell/GlobalSearch/block.meta.ts +10 -0
- package/src/shell/GlobalSearch/fixtures.ts +64 -0
- package/src/shell/index.ts +11 -0
- package/src/styles.css +2 -0
- package/src/tables/DataTable/DataTable.edit-views.test.tsx +249 -0
- package/src/tables/DataTable/DataTable.layout.test.tsx +188 -0
- package/src/tables/DataTable/DataTable.single-select.test.tsx +501 -0
- package/src/tables/DataTable/DataTable.test.tsx +268 -0
- package/src/tables/DataTable/DataTable.tsx +520 -0
- package/src/tables/DataTable/block.meta.ts +27 -0
- package/src/tables/DataTable/column-menu.tsx +109 -0
- package/src/tables/DataTable/column-sizing.ts +166 -0
- package/src/tables/DataTable/data-table-body.tsx +194 -0
- package/src/tables/DataTable/data-table-props.ts +156 -0
- package/src/tables/DataTable/data-table-row.tsx +220 -0
- package/src/tables/DataTable/data-table-utils.ts +195 -0
- package/src/tables/DataTable/density-toggle.tsx +30 -0
- package/src/tables/DataTable/editable-cell.tsx +274 -0
- package/src/tables/DataTable/examples/editable.tsx +71 -0
- package/src/tables/DataTable/examples/grouped.tsx +18 -0
- package/src/tables/DataTable/examples/resizable-pinned.tsx +20 -0
- package/src/tables/DataTable/examples/saved-views.tsx +61 -0
- package/src/tables/DataTable/examples/single-selection.tsx +55 -0
- package/src/tables/DataTable/examples/virtualized.tsx +20 -0
- package/src/tables/DataTable/fixtures.ts +119 -0
- package/src/tables/DataTable/header-cell.tsx +86 -0
- package/src/tables/DataTable/resize-handle.tsx +88 -0
- package/src/tables/DataTable/single-selection.tsx +149 -0
- package/src/tables/DataTable/use-table-views.ts +79 -0
- package/src/tables/DataTable/use-virtual-rows.ts +83 -0
- package/src/tables/DataTable/views.tsx +186 -0
- package/src/tables/FilterBar/FilterBar.test.tsx +43 -0
- package/src/tables/FilterBar/FilterBar.tsx +199 -0
- package/src/tables/FilterBar/block.meta.ts +10 -0
- package/src/tables/FilterBar/fixtures.ts +32 -0
- package/src/tables/InlineEditRow/InlineEditRow.test.tsx +41 -0
- package/src/tables/InlineEditRow/InlineEditRow.tsx +215 -0
- package/src/tables/InlineEditRow/block.meta.ts +10 -0
- package/src/tables/InlineEditRow/fixtures.ts +28 -0
- package/src/tables/PaginationFooter/PaginationFooter.test.tsx +29 -0
- package/src/tables/PaginationFooter/PaginationFooter.tsx +94 -0
- package/src/tables/PaginationFooter/block.meta.ts +10 -0
- package/src/tables/SubordinateList/SubordinateList.test.tsx +351 -0
- package/src/tables/SubordinateList/SubordinateList.tsx +140 -0
- package/src/tables/SubordinateList/block.meta.ts +10 -0
- package/src/tables/SubordinateList/examples/ResourceForm.tsx +129 -0
- package/src/tables/SubordinateList/examples/empty.tsx +5 -0
- package/src/tables/SubordinateList/examples/error.tsx +5 -0
- package/src/tables/SubordinateList/examples/loading.tsx +5 -0
- package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.test.tsx +159 -0
- package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.tsx +60 -0
- package/src/tables/TruncatedCollectionFooter/block.meta.ts +10 -0
- package/src/tables/TruncatedCollectionFooter/examples/exact.tsx +6 -0
- package/src/tables/TruncatedCollectionFooter/examples/has-more.tsx +12 -0
- package/src/tables/TruncatedCollectionFooter/fixtures.ts +2 -0
- package/src/tables/index.ts +45 -0
- package/src/team/MembersTable/MembersTable.test.tsx +29 -0
- package/src/team/MembersTable/MembersTable.tsx +141 -0
- package/src/team/MembersTable/block.meta.ts +9 -0
- package/src/team/MembersTable/fixtures.ts +19 -0
- package/src/team/NotificationMatrix/NotificationMatrix.test.tsx +36 -0
- package/src/team/NotificationMatrix/NotificationMatrix.tsx +144 -0
- package/src/team/NotificationMatrix/block.meta.ts +9 -0
- package/src/team/NotificationMatrix/fixtures.ts +43 -0
- package/src/team/PendingInvitations/PendingInvitations.test.tsx +30 -0
- package/src/team/PendingInvitations/PendingInvitations.tsx +132 -0
- package/src/team/PendingInvitations/block.meta.ts +9 -0
- package/src/team/PendingInvitations/fixtures.ts +39 -0
- package/src/team/ProfileHeader/ProfileHeader.test.tsx +29 -0
- package/src/team/ProfileHeader/ProfileHeader.tsx +106 -0
- package/src/team/ProfileHeader/block.meta.ts +10 -0
- package/src/team/ProfileHeader/fixtures.ts +28 -0
- package/src/team/SessionsDevices/SessionsDevices.test.tsx +30 -0
- package/src/team/SessionsDevices/SessionsDevices.tsx +126 -0
- package/src/team/SessionsDevices/block.meta.ts +10 -0
- package/src/team/SessionsDevices/fixtures.ts +58 -0
- package/src/team/index.ts +15 -0
- package/src/test/a11y.ts +18 -0
- package/src/test/heading-levels.test.tsx +69 -0
- package/src/test/setup.ts +36 -0
- package/README.md +0 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
|
|
3
|
+
export interface LogLine {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Display timestamp, e.g. "12:04:31.118". */
|
|
6
|
+
timestamp: string;
|
|
7
|
+
level: LogLevel;
|
|
8
|
+
message: string;
|
|
9
|
+
/** Emitting component, e.g. "worker-2". */
|
|
10
|
+
source?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MESSAGES: Array<[LogLevel, string, string]> = [
|
|
14
|
+
['info', 'api', 'GET /v1/projects 200 in 42ms'],
|
|
15
|
+
['debug', 'worker-1', 'job 8f2c picked from queue "default"'],
|
|
16
|
+
['info', 'worker-1', 'job 8f2c completed in 1.2s'],
|
|
17
|
+
['warn', 'api', 'slow query on deployments (812ms)'],
|
|
18
|
+
['info', 'scheduler', 'next run of nightly-export at 02:00 UTC'],
|
|
19
|
+
['error', 'worker-2', 'job 91ab failed: connection reset by peer'],
|
|
20
|
+
['info', 'api', 'POST /v1/deployments 201 in 118ms'],
|
|
21
|
+
['debug', 'cache', 'evicted 128 entries (lru)'],
|
|
22
|
+
['warn', 'worker-2', 'retrying job 91ab (attempt 2 of 3)'],
|
|
23
|
+
['info', 'worker-2', 'job 91ab completed in 3.4s'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function pad(n: number, width = 2) {
|
|
27
|
+
return String(n).padStart(width, '0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Deterministic sample stream of `count` lines (neutral service logs). */
|
|
31
|
+
export function makeLogLines(count = 480, startSecond = 12 * 3600): LogLine[] {
|
|
32
|
+
return Array.from({ length: count }, (_, i) => {
|
|
33
|
+
const entry = MESSAGES[i % MESSAGES.length] ?? MESSAGES[0]!;
|
|
34
|
+
const t = startSecond + i * 0.37;
|
|
35
|
+
const h = Math.floor(t / 3600);
|
|
36
|
+
const m = Math.floor((t % 3600) / 60);
|
|
37
|
+
const s = Math.floor(t % 60);
|
|
38
|
+
const ms = Math.floor((t % 1) * 1000);
|
|
39
|
+
return {
|
|
40
|
+
id: `log-${i}`,
|
|
41
|
+
timestamp: `${pad(h)}:${pad(m)}:${pad(s)}.${pad(ms, 3)}`,
|
|
42
|
+
level: entry[0],
|
|
43
|
+
source: entry[1],
|
|
44
|
+
message: entry[2],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const logLines: LogLine[] = makeLogLines();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './FlowBuilder/FlowBuilder';
|
|
2
|
+
export {
|
|
3
|
+
NodeInspector,
|
|
4
|
+
NodePalette,
|
|
5
|
+
RunConsole,
|
|
6
|
+
type NodeInspectorProps,
|
|
7
|
+
type NodePaletteProps,
|
|
8
|
+
type PanelHeading,
|
|
9
|
+
type RunConsoleProps,
|
|
10
|
+
} from './FlowBuilder/parts';
|
|
11
|
+
export { meta as flowBuilderMeta } from './FlowBuilder/block.meta';
|
|
12
|
+
export * from './CodePanel/CodePanel';
|
|
13
|
+
export { meta as codePanelMeta } from './CodePanel/block.meta';
|
|
14
|
+
export * from './LogViewer/LogViewer';
|
|
15
|
+
export { meta as logViewerMeta } from './LogViewer/block.meta';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { ActivityFeed, groupActivityByDay, type ActivityItem } from './ActivityFeed';
|
|
5
|
+
|
|
6
|
+
// The default feed's fixtures are relative to Date.now() at load time and the newest item is
|
|
7
|
+
// 12 minutes old, so for 12 minutes after midnight there is no Today group. Pin the clock
|
|
8
|
+
// (Date only; timers stay real) before the fixtures load.
|
|
9
|
+
vi.hoisted(() => {
|
|
10
|
+
vi.useFakeTimers({ now: new Date(2026, 9, 3, 15, 0), toFake: ['Date'] });
|
|
11
|
+
});
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const now = new Date(2026, 9, 3, 15, 0);
|
|
17
|
+
const items: ActivityItem[] = [
|
|
18
|
+
{ id: '1', actor: { name: 'Ana Lopez' }, action: 'deployed', target: 'orders-api', at: new Date(2026, 9, 3, 14, 0) },
|
|
19
|
+
{
|
|
20
|
+
id: '2',
|
|
21
|
+
actor: { name: 'Sam Carter' },
|
|
22
|
+
action: 'invited',
|
|
23
|
+
target: 'lee@example.com',
|
|
24
|
+
at: new Date(2026, 9, 3, 9, 0),
|
|
25
|
+
},
|
|
26
|
+
{ id: '3', actor: { name: 'Lee Wong' }, action: 'paused', target: 'nightly-export', at: new Date(2026, 9, 2, 18, 0) },
|
|
27
|
+
{
|
|
28
|
+
id: '4',
|
|
29
|
+
actor: { name: 'Priya Nair' },
|
|
30
|
+
action: 'created the project',
|
|
31
|
+
target: 'Checkout',
|
|
32
|
+
at: new Date(2026, 8, 28, 10, 0),
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe('ActivityFeed', () => {
|
|
37
|
+
it('renders the default feed', async () => {
|
|
38
|
+
const { container } = render(<ActivityFeed />);
|
|
39
|
+
expect(screen.getByRole('region', { name: 'Recent activity' })).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByRole('heading', { name: 'Today' })).toBeInTheDocument();
|
|
41
|
+
await expectNoAxeViolations(container);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('groups items by day', () => {
|
|
45
|
+
const groups = groupActivityByDay(items, now, 'en-US');
|
|
46
|
+
expect(groups.map((g) => [g.label, g.items.length])).toEqual([
|
|
47
|
+
['Today', 2],
|
|
48
|
+
['Yesterday', 1],
|
|
49
|
+
['Monday, Sep 28', 1],
|
|
50
|
+
]);
|
|
51
|
+
render(<ActivityFeed items={items} now={now} locale="en-US" />);
|
|
52
|
+
expect(screen.getByRole('list', { name: 'Today' }).children).toHaveLength(2);
|
|
53
|
+
expect(screen.getByRole('list', { name: 'Yesterday' }).children).toHaveLength(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('loads more on demand', async () => {
|
|
57
|
+
const onLoadMore = vi.fn();
|
|
58
|
+
render(<ActivityFeed items={items} now={now} onLoadMore={onLoadMore} />);
|
|
59
|
+
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
|
60
|
+
expect(onLoadMore).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Avatar, Button, cn, Timestamp, toDate, type DateInput } from '@gntik-ai/ui';
|
|
2
|
+
import { useId, useState } from 'react';
|
|
3
|
+
import { PROJECT_ACTIVITY } from './fixtures';
|
|
4
|
+
|
|
5
|
+
export interface ActivityItem {
|
|
6
|
+
id: string;
|
|
7
|
+
actor: { name: string; src?: string };
|
|
8
|
+
/** Verb phrase ("deployed", "invited"). */
|
|
9
|
+
action: string;
|
|
10
|
+
/** Object of the action, emphasised ("orders-api v2.14.0"). */
|
|
11
|
+
target?: string;
|
|
12
|
+
at: DateInput;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ActivityFeedProps {
|
|
16
|
+
/** Items, newest first. */
|
|
17
|
+
items?: readonly ActivityItem[];
|
|
18
|
+
/** Accessible name of the feed. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Reference "now" for the Today / Yesterday headings (defaults to the mount time). */
|
|
21
|
+
now?: DateInput;
|
|
22
|
+
locale?: string;
|
|
23
|
+
/** Shows a "Load more" button. */
|
|
24
|
+
onLoadMore?: () => void;
|
|
25
|
+
loadingMore?: boolean;
|
|
26
|
+
/** Heading level of the day-group titles, to fit the page outline (default h3). */
|
|
27
|
+
groupHeadingAs?: 'h2' | 'h3' | 'h4';
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DayGroup {
|
|
32
|
+
key: string;
|
|
33
|
+
label: string;
|
|
34
|
+
items: ActivityItem[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dayKey = (d: Date) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
38
|
+
|
|
39
|
+
/** Groups items by calendar day (local time), labelled Today, Yesterday or the date. */
|
|
40
|
+
export function groupActivityByDay(items: readonly ActivityItem[], now: Date, locale?: string): DayGroup[] {
|
|
41
|
+
const today = dayKey(now);
|
|
42
|
+
const yesterday = dayKey(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1));
|
|
43
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'short', day: 'numeric' });
|
|
44
|
+
const groups: DayGroup[] = [];
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
const date = toDate(item.at);
|
|
47
|
+
if (!date) continue;
|
|
48
|
+
const key = dayKey(date);
|
|
49
|
+
let group = groups.find((g) => g.key === key);
|
|
50
|
+
if (!group) {
|
|
51
|
+
group = { key, label: key === today ? 'Today' : key === yesterday ? 'Yesterday' : fmt.format(date), items: [] };
|
|
52
|
+
groups.push(group);
|
|
53
|
+
}
|
|
54
|
+
group.items.push(item);
|
|
55
|
+
}
|
|
56
|
+
return groups;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Activity feed: actor avatar, action, target and Timestamp, grouped by day. */
|
|
60
|
+
export function ActivityFeed({
|
|
61
|
+
items = PROJECT_ACTIVITY,
|
|
62
|
+
label = 'Recent activity',
|
|
63
|
+
now,
|
|
64
|
+
locale,
|
|
65
|
+
onLoadMore,
|
|
66
|
+
loadingMore,
|
|
67
|
+
groupHeadingAs: GroupTag = 'h3',
|
|
68
|
+
className,
|
|
69
|
+
}: ActivityFeedProps) {
|
|
70
|
+
const [mountedAt] = useState(() => Date.now());
|
|
71
|
+
const reference = toDate(now ?? mountedAt) ?? new Date(mountedAt);
|
|
72
|
+
const groups = groupActivityByDay(items, reference, locale);
|
|
73
|
+
const id = useId();
|
|
74
|
+
return (
|
|
75
|
+
<section aria-label={label} className={cn('flex flex-col gap-6', className)}>
|
|
76
|
+
{groups.map((group) => {
|
|
77
|
+
const headingId = `${id}-${group.key}`;
|
|
78
|
+
return (
|
|
79
|
+
<div key={group.key}>
|
|
80
|
+
<GroupTag
|
|
81
|
+
id={headingId}
|
|
82
|
+
className="mb-3 font-mono text-[10.5px] font-medium tracking-[0.12em] text-muted-foreground uppercase"
|
|
83
|
+
>
|
|
84
|
+
{group.label}
|
|
85
|
+
</GroupTag>
|
|
86
|
+
<ul aria-labelledby={headingId} className="flex flex-col">
|
|
87
|
+
{group.items.map((item, i) => (
|
|
88
|
+
<li key={item.id} className="relative flex gap-3 pb-5 last:pb-0">
|
|
89
|
+
{i < group.items.length - 1 && (
|
|
90
|
+
<span aria-hidden className="absolute top-7 bottom-0.5 start-[11.5px] w-px bg-border" />
|
|
91
|
+
)}
|
|
92
|
+
<Avatar name={item.actor.name} src={item.actor.src} size="xs" className="mt-0.5" />
|
|
93
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4">
|
|
94
|
+
<p className="min-w-0 text-[13px] text-muted-foreground">
|
|
95
|
+
<span className="font-semibold text-foreground">{item.actor.name}</span> {item.action}
|
|
96
|
+
{item.target && (
|
|
97
|
+
<>
|
|
98
|
+
{' '}
|
|
99
|
+
<span className="font-medium text-foreground">{item.target}</span>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</p>
|
|
103
|
+
<Timestamp value={item.at} locale={locale} className="shrink-0 font-mono text-[11px]" />
|
|
104
|
+
</div>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
{onLoadMore && (
|
|
112
|
+
<Button variant="secondary" size="sm" className="self-start" loading={loadingMore} onClick={onLoadMore}>
|
|
113
|
+
Load more
|
|
114
|
+
</Button>
|
|
115
|
+
)}
|
|
116
|
+
</section>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Activity feed',
|
|
5
|
+
family: 'data-display',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'Who did what, when: actor avatar, action, target and Timestamp, grouped by day (Today, Yesterday, date), with optional Load more.',
|
|
9
|
+
uses: ['Avatar', 'Timestamp', 'Button'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ActivityItem } from './ActivityFeed';
|
|
2
|
+
|
|
3
|
+
const MIN = 60_000;
|
|
4
|
+
const HOUR = 60 * MIN;
|
|
5
|
+
const DAY = 24 * HOUR;
|
|
6
|
+
/** Relative to load time so the docs always show Today / Yesterday groups. */
|
|
7
|
+
const NOW = Date.now();
|
|
8
|
+
|
|
9
|
+
/** Project activity, newest first. */
|
|
10
|
+
export const PROJECT_ACTIVITY: ActivityItem[] = [
|
|
11
|
+
{ id: 'a1', actor: { name: 'Ana Lopez' }, action: 'deployed', target: 'orders-api v2.14.0', at: NOW - 12 * MIN },
|
|
12
|
+
{
|
|
13
|
+
id: 'a2',
|
|
14
|
+
actor: { name: 'Sam Carter' },
|
|
15
|
+
action: 'approved the release of',
|
|
16
|
+
target: 'orders-api',
|
|
17
|
+
at: NOW - 40 * MIN,
|
|
18
|
+
},
|
|
19
|
+
{ id: 'a3', actor: { name: 'Priya Nair' }, action: 'invited', target: 'jordan@example.com', at: NOW - 2 * HOUR },
|
|
20
|
+
{
|
|
21
|
+
id: 'a4',
|
|
22
|
+
actor: { name: 'Ana Lopez' },
|
|
23
|
+
action: 'rotated the API key',
|
|
24
|
+
target: 'billing-sync',
|
|
25
|
+
at: NOW - DAY - HOUR,
|
|
26
|
+
},
|
|
27
|
+
{ id: 'a5', actor: { name: 'Lee Wong' }, action: 'paused', target: 'nightly-export', at: NOW - DAY - 5 * HOUR },
|
|
28
|
+
{ id: 'a6', actor: { name: 'Priya Nair' }, action: 'created the project', target: 'Checkout', at: NOW - 3 * DAY },
|
|
29
|
+
];
|
|
@@ -0,0 +1,282 @@
|
|
|
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 { ChartCard } from './ChartCard';
|
|
5
|
+
import CustomRangeChartCard from './examples/custom-ranges';
|
|
6
|
+
import ToolbarOwnedRangeChartCard from './examples/toolbar-owned-range';
|
|
7
|
+
|
|
8
|
+
const ranges = [
|
|
9
|
+
{ value: '24h', label: '24h', summary: { value: '12 requests' } },
|
|
10
|
+
{ value: '7d', label: '7d', summary: { value: '70 requests' } },
|
|
11
|
+
{ value: '30d', label: '30d', summary: { value: '300 requests' } },
|
|
12
|
+
];
|
|
13
|
+
const dataByRange = {
|
|
14
|
+
'24h': [{ time: 'Today', Requests: 12 }],
|
|
15
|
+
'7d': [{ time: 'This week', Requests: 70 }],
|
|
16
|
+
'30d': [{ time: 'This month', Requests: 300 }],
|
|
17
|
+
};
|
|
18
|
+
const custom = {
|
|
19
|
+
title: 'Requests',
|
|
20
|
+
ranges,
|
|
21
|
+
dataByRange,
|
|
22
|
+
index: 'time' as const,
|
|
23
|
+
categories: ['Requests'] as const,
|
|
24
|
+
dataTable: true,
|
|
25
|
+
valueFormatter: (value: number) => String(value),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('ChartCard ranges', () => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.unstubAllEnvs();
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('preserves the default options, initial selection and fixture data', () => {
|
|
35
|
+
render(<ChartCard dataTable />);
|
|
36
|
+
const buttons = within(screen.getByRole('group', { name: 'Infrastructure cost range' })).getAllByRole('button');
|
|
37
|
+
expect(buttons.map((button) => button.textContent)).toEqual(['7d', '30d', '90d']);
|
|
38
|
+
expect(buttons[0]).toHaveAttribute('aria-pressed', 'true');
|
|
39
|
+
expect(screen.getByText('$2,412')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByRole('table', { name: 'Infrastructure cost, 7d' })).toHaveTextContent('Mon$180$42$120');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders the custom range example with the supplied windows', () => {
|
|
44
|
+
render(<CustomRangeChartCard />);
|
|
45
|
+
expect(
|
|
46
|
+
within(screen.getByRole('group', { name: 'Requests range' }))
|
|
47
|
+
.getAllByRole('button')
|
|
48
|
+
.map((b) => b.textContent),
|
|
49
|
+
).toEqual(['24h', '7d', '30d']);
|
|
50
|
+
expect(screen.getByRole('figure', { name: 'Requests, 24h' })).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('lets the toolbar example own the only range control', async () => {
|
|
54
|
+
const user = userEvent.setup();
|
|
55
|
+
render(<ToolbarOwnedRangeChartCard />);
|
|
56
|
+
expect(screen.queryByRole('group', { name: 'Requests range', hidden: true })).toBeNull();
|
|
57
|
+
expect(screen.queryByRole('button', { name: '90d' })).toBeNull();
|
|
58
|
+
expect(screen.getByRole('figure', { name: 'Requests, 24h' })).toBeInTheDocument();
|
|
59
|
+
await user.click(screen.getByRole('combobox', { name: 'Page window' }));
|
|
60
|
+
expect((await screen.findAllByRole('option')).map((option) => option.textContent)).toEqual(['24h', '7d', '30d']);
|
|
61
|
+
await user.click(await screen.findByRole('option', { name: '7d' }));
|
|
62
|
+
expect(screen.getByRole('figure', { name: 'Requests, 7d' })).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText('8,680 requests')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders exactly the supplied range options in order', () => {
|
|
67
|
+
render(<ChartCard {...custom} />);
|
|
68
|
+
const buttons = within(screen.getByRole('group', { name: 'Requests range' })).getAllByRole('button');
|
|
69
|
+
expect(buttons.map((button) => button.textContent)).toEqual(['24h', '7d', '30d']);
|
|
70
|
+
expect(screen.queryByRole('button', { name: '90d' })).toBeNull();
|
|
71
|
+
expect(buttons[0]).toHaveAttribute('aria-pressed', 'true');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('holds controlled selection and data until the parent changes the range', async () => {
|
|
75
|
+
const onRangeChange = vi.fn();
|
|
76
|
+
const { rerender } = render(<ChartCard {...custom} range="7d" onRangeChange={onRangeChange} />);
|
|
77
|
+
await userEvent.click(screen.getByRole('button', { name: '30d' }));
|
|
78
|
+
expect(onRangeChange).toHaveBeenCalledExactlyOnceWith('30d');
|
|
79
|
+
expect(screen.getByRole('button', { name: '7d' })).toHaveAttribute('aria-pressed', 'true');
|
|
80
|
+
expect(screen.getByRole('button', { name: '30d' })).toHaveAttribute('aria-pressed', 'false');
|
|
81
|
+
expect(screen.getByRole('figure', { name: 'Requests, 7d' })).toBeInTheDocument();
|
|
82
|
+
expect(screen.getByRole('table', { name: 'Requests, 7d' })).toHaveTextContent('This week70');
|
|
83
|
+
expect(screen.queryByText('300 requests')).toBeNull();
|
|
84
|
+
rerender(<ChartCard {...custom} range="30d" onRangeChange={onRangeChange} />);
|
|
85
|
+
expect(screen.getByRole('button', { name: '30d' })).toHaveAttribute('aria-pressed', 'true');
|
|
86
|
+
expect(screen.getByRole('figure', { name: 'Requests, 30d' })).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByRole('table', { name: 'Requests, 30d' })).toHaveTextContent('This month300');
|
|
88
|
+
expect(screen.getByText('300 requests')).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('updates uncontrolled selection and calls the consumer when clicked', async () => {
|
|
92
|
+
const onRangeChange = vi.fn();
|
|
93
|
+
render(<ChartCard {...custom} defaultRange="7d" onRangeChange={onRangeChange} />);
|
|
94
|
+
await userEvent.click(screen.getByRole('button', { name: '30d' }));
|
|
95
|
+
expect(onRangeChange).toHaveBeenCalledExactlyOnceWith('30d');
|
|
96
|
+
expect(screen.getByRole('button', { name: '30d' })).toHaveAttribute('aria-pressed', 'true');
|
|
97
|
+
expect(screen.getByRole('table', { name: 'Requests, 30d' })).toHaveTextContent('This month300');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('shows the controlled 24h data and label with the toggle hidden', () => {
|
|
101
|
+
render(<ChartCard {...custom} range="24h" showRangeToggle={false} />);
|
|
102
|
+
expect(screen.queryByRole('group', { name: 'Requests range', hidden: true })).toBeNull();
|
|
103
|
+
expect(screen.getByRole('figure', { name: 'Requests, 24h' })).toBeInTheDocument();
|
|
104
|
+
expect(screen.getByRole('table', { name: 'Requests, 24h' })).toHaveTextContent('Today12');
|
|
105
|
+
expect(screen.getByText('12 requests')).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('uses one tab stop, arrow and boundary focus keys, and Space/Enter selection', async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
render(<ChartCard {...custom} defaultRange="7d" />);
|
|
111
|
+
const group = screen.getByRole('group', { name: 'Requests range' });
|
|
112
|
+
const [first, middle, last] = within(group).getAllByRole('button');
|
|
113
|
+
const expectOneTabStop = () =>
|
|
114
|
+
expect(
|
|
115
|
+
within(group)
|
|
116
|
+
.getAllByRole('button')
|
|
117
|
+
.filter((b) => b.tabIndex === 0),
|
|
118
|
+
).toHaveLength(1);
|
|
119
|
+
await user.tab();
|
|
120
|
+
expect(middle).toHaveFocus();
|
|
121
|
+
expectOneTabStop();
|
|
122
|
+
await user.keyboard('{ArrowRight}');
|
|
123
|
+
expect(last).toHaveFocus();
|
|
124
|
+
expect(middle).toHaveAttribute('aria-pressed', 'true');
|
|
125
|
+
await user.keyboard('{ArrowLeft}');
|
|
126
|
+
expect(middle).toHaveFocus();
|
|
127
|
+
await user.keyboard('{Home}');
|
|
128
|
+
expect(first).toHaveFocus();
|
|
129
|
+
await user.keyboard(' ');
|
|
130
|
+
expect(first).toHaveAttribute('aria-pressed', 'true');
|
|
131
|
+
expect(middle).toHaveAttribute('aria-pressed', 'false');
|
|
132
|
+
await user.keyboard('{End}');
|
|
133
|
+
expect(last).toHaveFocus();
|
|
134
|
+
await user.keyboard('{Enter}');
|
|
135
|
+
expect(last).toHaveAttribute('aria-pressed', 'true');
|
|
136
|
+
expect(first).toHaveAttribute('aria-pressed', 'false');
|
|
137
|
+
expectOneTabStop();
|
|
138
|
+
await user.tab();
|
|
139
|
+
expect(group).not.toContainElement(document.activeElement as HTMLElement);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('keeps pointer focus on the clicked range for subsequent keyboard selection', async () => {
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
render(
|
|
145
|
+
<>
|
|
146
|
+
<button type="button">Outside</button>
|
|
147
|
+
<ChartCard />
|
|
148
|
+
</>,
|
|
149
|
+
);
|
|
150
|
+
const outside = screen.getByRole('button', { name: 'Outside' });
|
|
151
|
+
const selected = screen.getByRole('button', { name: '90d' });
|
|
152
|
+
await user.click(outside);
|
|
153
|
+
await user.click(selected);
|
|
154
|
+
expect(selected).toHaveFocus();
|
|
155
|
+
expect(selected).toHaveAttribute('aria-pressed', 'true');
|
|
156
|
+
await user.keyboard(' ');
|
|
157
|
+
expect(selected).toHaveFocus();
|
|
158
|
+
expect(selected).toHaveAttribute('aria-pressed', 'true');
|
|
159
|
+
expect(screen.getByRole('button', { name: '7d' })).toHaveAttribute('aria-pressed', 'false');
|
|
160
|
+
await user.click(outside);
|
|
161
|
+
await user.tab();
|
|
162
|
+
expect(selected).toHaveFocus();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('preserves an arrow-focused option when focus returns without Tab', async () => {
|
|
166
|
+
const user = userEvent.setup();
|
|
167
|
+
render(<ChartCard {...custom} defaultRange="7d" />);
|
|
168
|
+
await user.tab();
|
|
169
|
+
await user.keyboard('{ArrowRight}');
|
|
170
|
+
const focused = screen.getByRole('button', { name: '30d' });
|
|
171
|
+
expect(focused).toHaveFocus();
|
|
172
|
+
act(() => {
|
|
173
|
+
focused.blur();
|
|
174
|
+
focused.focus();
|
|
175
|
+
});
|
|
176
|
+
expect(focused).toHaveFocus();
|
|
177
|
+
expect(screen.getByRole('button', { name: '7d' })).toHaveAttribute('aria-pressed', 'true');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns keyboard focus to the selected range after a pointer release outside the group', async () => {
|
|
181
|
+
const user = userEvent.setup();
|
|
182
|
+
render(
|
|
183
|
+
<>
|
|
184
|
+
<ChartCard showLegend={false} />
|
|
185
|
+
<button type="button">Outside</button>
|
|
186
|
+
</>,
|
|
187
|
+
);
|
|
188
|
+
const outside = screen.getByRole('button', { name: 'Outside' });
|
|
189
|
+
const selected = screen.getByRole('button', { name: '7d' });
|
|
190
|
+
const other = screen.getByRole('button', { name: '90d' });
|
|
191
|
+
await user.pointer({ keys: '[MouseLeft>]', target: other });
|
|
192
|
+
expect(other).toHaveFocus();
|
|
193
|
+
await user.pointer({ keys: '[/MouseLeft]', target: outside });
|
|
194
|
+
expect(selected).toHaveAttribute('aria-pressed', 'true');
|
|
195
|
+
await user.tab();
|
|
196
|
+
expect(outside).toHaveFocus();
|
|
197
|
+
await user.tab({ shift: true });
|
|
198
|
+
expect(selected).toHaveFocus();
|
|
199
|
+
await user.keyboard(' ');
|
|
200
|
+
expect(selected).toHaveAttribute('aria-pressed', 'true');
|
|
201
|
+
expect(other).toHaveAttribute('aria-pressed', 'false');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it.each(['dark', 'light', 'high_contrast'])(
|
|
205
|
+
'has no axe violations for default, custom and hidden ranges in %s',
|
|
206
|
+
async (theme) => {
|
|
207
|
+
const previousTheme = document.documentElement.className;
|
|
208
|
+
document.documentElement.className = theme;
|
|
209
|
+
try {
|
|
210
|
+
for (const variant of [
|
|
211
|
+
<ChartCard />,
|
|
212
|
+
<ChartCard {...custom} />,
|
|
213
|
+
<ChartCard {...custom} range="24h" showRangeToggle={false} />,
|
|
214
|
+
]) {
|
|
215
|
+
const { container, unmount } = render(variant);
|
|
216
|
+
await expectNoAxeViolations(container);
|
|
217
|
+
unmount();
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
document.documentElement.className = previousTheme;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
it.each(['range', 'defaultRange'] as const)(
|
|
226
|
+
'falls back consistently for an unknown %s, warning only in development',
|
|
227
|
+
(prop) => {
|
|
228
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
229
|
+
vi.stubEnv('NODE_ENV', 'development');
|
|
230
|
+
const { unmount } = render(<ChartCard {...{ [prop]: 'unknown' }} />);
|
|
231
|
+
expect(screen.getByRole('figure', { name: 'Infrastructure cost, 7d' })).toBeInTheDocument();
|
|
232
|
+
expect(screen.getByRole('button', { name: '7d' })).toHaveAttribute('aria-pressed', 'true');
|
|
233
|
+
expect(screen.getByText('$2,412')).toBeInTheDocument();
|
|
234
|
+
expect(warning).toHaveBeenCalledWith(
|
|
235
|
+
'ChartCard: range/defaultRange is not in ranges; falling back to the first option.',
|
|
236
|
+
);
|
|
237
|
+
unmount();
|
|
238
|
+
warning.mockClear();
|
|
239
|
+
vi.stubEnv('NODE_ENV', 'production');
|
|
240
|
+
render(<ChartCard {...{ [prop]: 'unknown' }} />);
|
|
241
|
+
expect(screen.getByRole('button', { name: '7d' })).toHaveAttribute('aria-pressed', 'true');
|
|
242
|
+
expect(warning).not.toHaveBeenCalledWith(
|
|
243
|
+
'ChartCard: range/defaultRange is not in ranges; falling back to the first option.',
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
it('omits the range control while preserving content and chart states', async () => {
|
|
249
|
+
const onRetry = vi.fn();
|
|
250
|
+
const props = {
|
|
251
|
+
showRangeToggle: false,
|
|
252
|
+
action: <button type="button">Details</button>,
|
|
253
|
+
footer: 'Updated just now',
|
|
254
|
+
onRetry,
|
|
255
|
+
};
|
|
256
|
+
const { rerender } = render(<ChartCard {...props} />);
|
|
257
|
+
const expectNoRangeControl = () => {
|
|
258
|
+
expect(screen.queryByRole('group', { name: 'Infrastructure cost range', hidden: true })).toBeNull();
|
|
259
|
+
for (const name of ['7d', '30d', '90d']) {
|
|
260
|
+
expect(screen.queryByRole('button', { name, hidden: true })).toBeNull();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
expectNoRangeControl();
|
|
264
|
+
expect(screen.getByRole('heading', { name: 'Infrastructure cost' })).toBeInTheDocument();
|
|
265
|
+
expect(screen.getByText('$2,412')).toBeInTheDocument();
|
|
266
|
+
const figure = screen.getByRole('figure', { name: 'Infrastructure cost, 7d' });
|
|
267
|
+
expect(within(figure).getByRole('button', { name: 'Compute' })).toHaveAttribute('aria-pressed', 'true');
|
|
268
|
+
expect(screen.getByRole('button', { name: 'Details' })).toBeInTheDocument();
|
|
269
|
+
expect(screen.getByText('Updated just now')).toBeInTheDocument();
|
|
270
|
+
rerender(<ChartCard {...props} state="loading" />);
|
|
271
|
+
expectNoRangeControl();
|
|
272
|
+
expect(screen.getByRole('status', { name: 'Loading chart' })).toBeInTheDocument();
|
|
273
|
+
rerender(<ChartCard {...props} state="empty" emptyMessage="No samples yet" />);
|
|
274
|
+
expectNoRangeControl();
|
|
275
|
+
expect(screen.getByText('No samples yet')).toBeInTheDocument();
|
|
276
|
+
rerender(<ChartCard {...props} state="error" errorMessage="Samples unavailable" />);
|
|
277
|
+
expectNoRangeControl();
|
|
278
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Samples unavailable');
|
|
279
|
+
await userEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
|
280
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { ChartCard } from './ChartCard';
|
|
5
|
+
import HeatmapChartCard from './examples/heatmap';
|
|
6
|
+
import { TRAFFIC_BY_RANGE } from './fixtures';
|
|
7
|
+
|
|
8
|
+
describe('ChartCard', () => {
|
|
9
|
+
it('renders title, legend and chart for the first range', async () => {
|
|
10
|
+
const { container } = render(<ChartCard />);
|
|
11
|
+
expect(screen.getByRole('heading', { name: 'Infrastructure cost' })).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByRole('figure', { name: 'Infrastructure cost, 7d' })).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByRole('button', { name: 'Compute' })).toHaveAttribute('aria-pressed', 'true');
|
|
14
|
+
expect(screen.getByText('$2,412')).toBeInTheDocument();
|
|
15
|
+
await expectNoAxeViolations(container);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('changes range from the toggle group', async () => {
|
|
19
|
+
const onRangeChange = vi.fn();
|
|
20
|
+
render(<ChartCard onRangeChange={onRangeChange} kind="bar" />);
|
|
21
|
+
await userEvent.click(screen.getByRole('button', { name: '30d' }));
|
|
22
|
+
expect(onRangeChange).toHaveBeenCalledWith('30d');
|
|
23
|
+
expect(screen.getByRole('figure', { name: 'Infrastructure cost, 30d' })).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('$9,880')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders a heatmap with its colour scale, keyboard grid and data table', async () => {
|
|
28
|
+
const { container } = render(<HeatmapChartCard />);
|
|
29
|
+
expect(screen.getByRole('heading', { name: 'Traffic by hour' })).toBeInTheDocument();
|
|
30
|
+
// A single range hides the range toggle.
|
|
31
|
+
expect(screen.queryByRole('group', { name: 'Traffic by hour range' })).toBeNull();
|
|
32
|
+
expect(screen.getByRole('figure', { name: 'Traffic by hour, Last 7 days' })).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByRole('group', { name: /colour grid/ })).toHaveAttribute('tabindex', '0');
|
|
34
|
+
const rows = TRAFFIC_BY_RANGE['7d']!;
|
|
35
|
+
expect(container.querySelectorAll('[data-cell]')).toHaveLength(rows.length * 8);
|
|
36
|
+
// Cells fill the card's plot height.
|
|
37
|
+
const cell = container.querySelector('[data-cell="0-0"]') as HTMLElement;
|
|
38
|
+
expect(cell.style.height).toBe(`${Math.floor((240 - 20 - 3 * rows.length) / rows.length)}px`);
|
|
39
|
+
expect(screen.getByRole('table', { name: 'Traffic by hour, Last 7 days' })).toHaveTextContent('Day');
|
|
40
|
+
await expectNoAxeViolations(container);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('hides the heatmap scale with showLegend={false} and passes chart states through', () => {
|
|
44
|
+
const { container, rerender } = render(<HeatmapChartCard />);
|
|
45
|
+
expect(container.querySelector('.h-2\\.5.w-4')).not.toBeNull();
|
|
46
|
+
rerender(<ChartCard kind="heatmap" state="error" errorMessage="Traffic is unavailable" onRetry={() => {}} />);
|
|
47
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Traffic is unavailable');
|
|
48
|
+
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument();
|
|
49
|
+
rerender(<ChartCard state="loading" />);
|
|
50
|
+
expect(screen.getByRole('status', { name: 'Loading chart' })).toBeInTheDocument();
|
|
51
|
+
rerender(<ChartCard kind="heatmap" showLegend={false} />);
|
|
52
|
+
expect(container.querySelector('.h-2\\.5.w-4')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|