@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,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Alert,
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogBody,
|
|
6
|
+
DialogClose,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
Field,
|
|
14
|
+
FieldDescription,
|
|
15
|
+
FieldError,
|
|
16
|
+
FieldLabel,
|
|
17
|
+
Select,
|
|
18
|
+
SelectContent,
|
|
19
|
+
SelectItem,
|
|
20
|
+
SelectTrigger,
|
|
21
|
+
Textarea,
|
|
22
|
+
TokenInput,
|
|
23
|
+
} from '@gntik-ai/ui';
|
|
24
|
+
import { UserPlus } from '@gntik-ai/icons';
|
|
25
|
+
import { useState, type ReactElement } from 'react';
|
|
26
|
+
import { EMAIL_RE, memberRoles, type RoleOption } from './fixtures';
|
|
27
|
+
|
|
28
|
+
export type { RoleOption } from './fixtures';
|
|
29
|
+
|
|
30
|
+
export interface InvitePayload {
|
|
31
|
+
emails: string[];
|
|
32
|
+
role: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface InviteMembersDialogProps {
|
|
37
|
+
roles?: RoleOption[];
|
|
38
|
+
defaultRole?: string;
|
|
39
|
+
/** Sends the invitations. A returned promise shows a loading state; a rejection shows its message. */
|
|
40
|
+
onInvite?: (payload: InvitePayload) => void | Promise<void>;
|
|
41
|
+
/** Maximum number of addresses per batch. */
|
|
42
|
+
maxInvites?: number;
|
|
43
|
+
open?: boolean;
|
|
44
|
+
defaultOpen?: boolean;
|
|
45
|
+
onOpenChange?: (open: boolean) => void;
|
|
46
|
+
/** Trigger element (rendered through Dialog.Trigger). Pass `null` when controlling `open` yourself. */
|
|
47
|
+
trigger?: ReactElement | null;
|
|
48
|
+
title?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const emailError = (t: string) => (EMAIL_RE.test(t) ? null : `${t} is not a valid email address`);
|
|
53
|
+
|
|
54
|
+
/** Invite dialog: email chips with validation, a role and an optional message; submit with loading. */
|
|
55
|
+
export function InviteMembersDialog({
|
|
56
|
+
roles = memberRoles,
|
|
57
|
+
defaultRole = 'member',
|
|
58
|
+
onInvite,
|
|
59
|
+
maxInvites = 20,
|
|
60
|
+
open: openProp,
|
|
61
|
+
defaultOpen = false,
|
|
62
|
+
onOpenChange,
|
|
63
|
+
trigger = <Button icon={UserPlus} />,
|
|
64
|
+
title = 'Invite members',
|
|
65
|
+
description = 'Invitations expire after 7 days. Separate addresses with commas or Enter.',
|
|
66
|
+
}: InviteMembersDialogProps) {
|
|
67
|
+
const [innerOpen, setInnerOpen] = useState(defaultOpen);
|
|
68
|
+
const [emails, setEmails] = useState<string[]>([]);
|
|
69
|
+
const [role, setRole] = useState(defaultRole);
|
|
70
|
+
const [message, setMessage] = useState('');
|
|
71
|
+
const [submitted, setSubmitted] = useState(false);
|
|
72
|
+
const [loading, setLoading] = useState(false);
|
|
73
|
+
const [error, setError] = useState<string | null>(null);
|
|
74
|
+
const open = openProp ?? innerOpen;
|
|
75
|
+
|
|
76
|
+
const invalidCount = emails.filter((e) => emailError(e)).length;
|
|
77
|
+
const emailsProblem =
|
|
78
|
+
emails.length === 0
|
|
79
|
+
? 'Add at least one email address.'
|
|
80
|
+
: invalidCount > 0
|
|
81
|
+
? `Fix ${invalidCount} invalid ${invalidCount === 1 ? 'address' : 'addresses'}.`
|
|
82
|
+
: null;
|
|
83
|
+
const showEmailError = submitted && emailsProblem != null;
|
|
84
|
+
|
|
85
|
+
const setOpen = (next: boolean) => {
|
|
86
|
+
if (loading && !next) return;
|
|
87
|
+
if (!next) {
|
|
88
|
+
setEmails([]);
|
|
89
|
+
setRole(defaultRole);
|
|
90
|
+
setMessage('');
|
|
91
|
+
setSubmitted(false);
|
|
92
|
+
setError(null);
|
|
93
|
+
}
|
|
94
|
+
setInnerOpen(next);
|
|
95
|
+
onOpenChange?.(next);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const submit = async () => {
|
|
99
|
+
setSubmitted(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
if (emailsProblem) return;
|
|
102
|
+
setLoading(true);
|
|
103
|
+
try {
|
|
104
|
+
await onInvite?.({ emails, role, message: message.trim() });
|
|
105
|
+
setLoading(false);
|
|
106
|
+
setOpen(false);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
setLoading(false);
|
|
109
|
+
setError(e instanceof Error ? e.message : 'The invitations could not be sent.');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const roleLabel = roles.find((r) => r.value === role)?.description;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
117
|
+
{trigger && <DialogTrigger render={trigger}>{title}</DialogTrigger>}
|
|
118
|
+
<DialogContent size="lg">
|
|
119
|
+
<DialogHeader>
|
|
120
|
+
<DialogTitle>{title}</DialogTitle>
|
|
121
|
+
<DialogDescription>{description}</DialogDescription>
|
|
122
|
+
</DialogHeader>
|
|
123
|
+
<form
|
|
124
|
+
noValidate
|
|
125
|
+
onSubmit={(e) => {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
void submit();
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<DialogBody className="grid gap-5">
|
|
131
|
+
<Field invalid={showEmailError}>
|
|
132
|
+
<FieldLabel required>Email addresses</FieldLabel>
|
|
133
|
+
<TokenInput
|
|
134
|
+
value={emails}
|
|
135
|
+
onValueChange={setEmails}
|
|
136
|
+
validate={emailError}
|
|
137
|
+
maxItems={maxInvites}
|
|
138
|
+
placeholder="name@company.com"
|
|
139
|
+
listLabel="Invited addresses"
|
|
140
|
+
invalid={showEmailError}
|
|
141
|
+
/>
|
|
142
|
+
<FieldError match={showEmailError}>{emailsProblem}</FieldError>
|
|
143
|
+
</Field>
|
|
144
|
+
<Field>
|
|
145
|
+
<FieldLabel>Role</FieldLabel>
|
|
146
|
+
<Select
|
|
147
|
+
value={role}
|
|
148
|
+
onValueChange={(v) => v != null && setRole(v)}
|
|
149
|
+
items={roles.map(({ value, label }) => ({ value, label }))}
|
|
150
|
+
>
|
|
151
|
+
<SelectTrigger className="w-full" />
|
|
152
|
+
<SelectContent>
|
|
153
|
+
{roles.map((r) => (
|
|
154
|
+
<SelectItem key={r.value} value={r.value}>
|
|
155
|
+
{r.label}
|
|
156
|
+
</SelectItem>
|
|
157
|
+
))}
|
|
158
|
+
</SelectContent>
|
|
159
|
+
</Select>
|
|
160
|
+
{roleLabel && <FieldDescription>{roleLabel}</FieldDescription>}
|
|
161
|
+
</Field>
|
|
162
|
+
<Field>
|
|
163
|
+
<FieldLabel>Message (optional)</FieldLabel>
|
|
164
|
+
<Textarea
|
|
165
|
+
rows={3}
|
|
166
|
+
value={message}
|
|
167
|
+
onValueChange={setMessage}
|
|
168
|
+
placeholder="Join us on the billing-dashboard project."
|
|
169
|
+
/>
|
|
170
|
+
</Field>
|
|
171
|
+
{error && <Alert tone="destructive" title="Invitations not sent" description={error} />}
|
|
172
|
+
</DialogBody>
|
|
173
|
+
<DialogFooter>
|
|
174
|
+
<DialogClose render={<Button variant="ghost" disabled={loading} />}>Cancel</DialogClose>
|
|
175
|
+
<Button type="submit" loading={loading}>
|
|
176
|
+
{emails.length > 1 ? `Send ${emails.length} invitations` : 'Send invitation'}
|
|
177
|
+
</Button>
|
|
178
|
+
</DialogFooter>
|
|
179
|
+
</form>
|
|
180
|
+
</DialogContent>
|
|
181
|
+
</Dialog>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Invite members dialog',
|
|
5
|
+
family: 'forms',
|
|
6
|
+
status: 'stable',
|
|
7
|
+
description:
|
|
8
|
+
'Dialog to invite people by email (validated chips) with a role and an optional message; submits with a loading state.',
|
|
9
|
+
uses: ['Dialog', 'TokenInput', 'Select', 'Textarea', 'Field', 'Button', 'Alert'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RoleOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const memberRoles: RoleOption[] = [
|
|
8
|
+
{ value: 'owner', label: 'Owner', description: 'Full access, including billing and deletion.' },
|
|
9
|
+
{ value: 'admin', label: 'Admin', description: 'Manage members, projects and settings.' },
|
|
10
|
+
{ value: 'member', label: 'Member', description: 'Create and deploy projects.' },
|
|
11
|
+
{ value: 'viewer', label: 'Viewer', description: 'Read-only access.' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from '@gntik-ai/ui';
|
|
4
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
5
|
+
import { SettingsRow } from './SettingsRow';
|
|
6
|
+
|
|
7
|
+
describe('SettingsRow', () => {
|
|
8
|
+
it('renders a labelled, described switch', async () => {
|
|
9
|
+
const { container } = render(<SettingsRow />);
|
|
10
|
+
const sw = screen.getByRole('switch', { name: 'Deployment notifications' });
|
|
11
|
+
expect(sw).toHaveAccessibleDescription(/deployment fails/);
|
|
12
|
+
expect(sw).toBeChecked();
|
|
13
|
+
await expectNoAxeViolations(container);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('toggles and reports the new state', async () => {
|
|
17
|
+
const onCheckedChange = vi.fn();
|
|
18
|
+
render(
|
|
19
|
+
<SettingsRow label="Weekly digest" description={null} defaultChecked={false} onCheckedChange={onCheckedChange} />,
|
|
20
|
+
);
|
|
21
|
+
await userEvent.click(screen.getByRole('switch', { name: 'Weekly digest' }));
|
|
22
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('wires a custom control through the ids', async () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<SettingsRow
|
|
28
|
+
label="Default region"
|
|
29
|
+
description="New projects are created here."
|
|
30
|
+
control={({ labelId }) => (
|
|
31
|
+
<Select
|
|
32
|
+
defaultValue="eu"
|
|
33
|
+
items={[
|
|
34
|
+
{ value: 'eu', label: 'Europe' },
|
|
35
|
+
{ value: 'us', label: 'United States' },
|
|
36
|
+
]}
|
|
37
|
+
>
|
|
38
|
+
<SelectTrigger aria-labelledby={labelId} />
|
|
39
|
+
<SelectContent>
|
|
40
|
+
<SelectItem value="eu">Europe</SelectItem>
|
|
41
|
+
<SelectItem value="us">United States</SelectItem>
|
|
42
|
+
</SelectContent>
|
|
43
|
+
</Select>
|
|
44
|
+
)}
|
|
45
|
+
/>,
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByRole('combobox', { name: /Default region/ })).toBeInTheDocument();
|
|
48
|
+
await expectNoAxeViolations(container);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Switch, cn } from '@gntik-ai/ui';
|
|
2
|
+
import { useId, type ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface SettingsRowControlIds {
|
|
5
|
+
/** Id of the label element: pass it as `aria-labelledby` to the control. */
|
|
6
|
+
labelId: string;
|
|
7
|
+
/** Id of the description element (when there is one): pass it as `aria-describedby`. */
|
|
8
|
+
descriptionId: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SettingsRowProps {
|
|
12
|
+
label?: ReactNode;
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Custom control. A function receives the label/description ids to wire the control's
|
|
16
|
+
* accessible name. Without it, a Switch driven by `checked` / `defaultChecked` is rendered.
|
|
17
|
+
*/
|
|
18
|
+
control?: ReactNode | ((ids: SettingsRowControlIds) => ReactNode);
|
|
19
|
+
checked?: boolean;
|
|
20
|
+
defaultChecked?: boolean;
|
|
21
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Extra content under the description (status, link). */
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One setting: label and description on the left, its control (a Switch by default) on the right. */
|
|
29
|
+
export function SettingsRow({
|
|
30
|
+
label = 'Deployment notifications',
|
|
31
|
+
description = 'Email the project owners when a deployment fails or is rolled back.',
|
|
32
|
+
control,
|
|
33
|
+
checked,
|
|
34
|
+
defaultChecked = true,
|
|
35
|
+
onCheckedChange,
|
|
36
|
+
disabled,
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
}: SettingsRowProps) {
|
|
40
|
+
const id = useId();
|
|
41
|
+
const labelId = `${id}-label`;
|
|
42
|
+
const descriptionId = description != null ? `${id}-desc` : undefined;
|
|
43
|
+
const content =
|
|
44
|
+
typeof control === 'function' ? (
|
|
45
|
+
control({ labelId, descriptionId })
|
|
46
|
+
) : control !== undefined ? (
|
|
47
|
+
control
|
|
48
|
+
) : (
|
|
49
|
+
<Switch
|
|
50
|
+
aria-labelledby={labelId}
|
|
51
|
+
aria-describedby={descriptionId}
|
|
52
|
+
checked={checked}
|
|
53
|
+
defaultChecked={checked === undefined ? defaultChecked : undefined}
|
|
54
|
+
onCheckedChange={(next) => onCheckedChange?.(next)}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
return (
|
|
59
|
+
<div className={cn('flex items-start justify-between gap-6 py-4', disabled && 'opacity-70', className)}>
|
|
60
|
+
<div className="min-w-0">
|
|
61
|
+
<div id={labelId} className="text-[13.5px] font-medium text-foreground">
|
|
62
|
+
{label}
|
|
63
|
+
</div>
|
|
64
|
+
{description != null && (
|
|
65
|
+
<p id={descriptionId} className="mt-0.5 max-w-xl text-[12.5px] leading-5 text-pretty text-muted-foreground">
|
|
66
|
+
{description}
|
|
67
|
+
</p>
|
|
68
|
+
)}
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="shrink-0 pt-0.5">{content}</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Settings row',
|
|
5
|
+
family: 'forms',
|
|
6
|
+
status: 'stable',
|
|
7
|
+
description: 'A setting with its label and description on the left and a control (Switch by default) on the right.',
|
|
8
|
+
uses: ['Switch'],
|
|
9
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { Button, Field, FieldLabel, Input } from '@gntik-ai/ui';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
6
|
+
import { ValidationSummary, type ValidationIssue } from './ValidationSummary';
|
|
7
|
+
|
|
8
|
+
function Demo() {
|
|
9
|
+
const [name, setName] = useState('');
|
|
10
|
+
const [count, setCount] = useState(0);
|
|
11
|
+
const errors: ValidationIssue[] =
|
|
12
|
+
count > 0 && !name ? [{ fieldId: 'project-name', label: 'Project name', message: 'Enter a project name.' }] : [];
|
|
13
|
+
return (
|
|
14
|
+
<form
|
|
15
|
+
onSubmit={(e) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setCount((c) => c + 1);
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<ValidationSummary errors={errors} submitCount={count} />
|
|
21
|
+
<Field>
|
|
22
|
+
<FieldLabel>Project name</FieldLabel>
|
|
23
|
+
<Input id="project-name" value={name} onValueChange={setName} />
|
|
24
|
+
</Field>
|
|
25
|
+
<Button type="submit">Create</Button>
|
|
26
|
+
</form>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('ValidationSummary', () => {
|
|
31
|
+
it('renders the default issues', async () => {
|
|
32
|
+
const { container } = render(<ValidationSummary />);
|
|
33
|
+
expect(screen.getByRole('region', { name: 'There are 3 problems with this form' })).toBeInTheDocument();
|
|
34
|
+
expect(screen.getAllByRole('link')).toHaveLength(3);
|
|
35
|
+
await expectNoAxeViolations(container);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('takes focus on submit and links to the field', async () => {
|
|
39
|
+
render(<Demo />);
|
|
40
|
+
expect(screen.queryByRole('region')).not.toBeInTheDocument();
|
|
41
|
+
await userEvent.click(screen.getByRole('button', { name: 'Create' }));
|
|
42
|
+
expect(screen.getByRole('region', { name: 'There is 1 problem with this form' })).toHaveFocus();
|
|
43
|
+
await userEvent.click(screen.getByRole('link', { name: /Enter a project name/ }));
|
|
44
|
+
expect(screen.getByLabelText('Project name')).toHaveFocus();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Alert, Link, cn } from '@gntik-ai/ui';
|
|
2
|
+
import { useEffect, useRef, type ReactNode } from 'react';
|
|
3
|
+
import { validationIssues, type ValidationIssue } from './fixtures';
|
|
4
|
+
|
|
5
|
+
export type { ValidationIssue } from './fixtures';
|
|
6
|
+
|
|
7
|
+
export interface ValidationSummaryProps {
|
|
8
|
+
errors?: ValidationIssue[];
|
|
9
|
+
/** Increment on every submit attempt: the summary takes focus when there are errors. */
|
|
10
|
+
submitCount?: number;
|
|
11
|
+
/** Title; defaults to "There are N problems with this form". */
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const focusField = (fieldId: string) => {
|
|
17
|
+
const el = document.getElementById(fieldId);
|
|
18
|
+
if (!el) return;
|
|
19
|
+
el.focus();
|
|
20
|
+
el.scrollIntoView?.({ block: 'center' });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Error summary for long forms: lists every problem with a link that moves focus to the field.
|
|
25
|
+
* After a failed submit it takes focus, so screen readers announce it.
|
|
26
|
+
*/
|
|
27
|
+
export function ValidationSummary({
|
|
28
|
+
errors = validationIssues,
|
|
29
|
+
submitCount = 0,
|
|
30
|
+
title,
|
|
31
|
+
className,
|
|
32
|
+
}: ValidationSummaryProps) {
|
|
33
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
34
|
+
const hasErrors = errors.length > 0;
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (submitCount > 0 && hasErrors) ref.current?.focus();
|
|
38
|
+
}, [submitCount, hasErrors]);
|
|
39
|
+
|
|
40
|
+
if (!hasErrors) return null;
|
|
41
|
+
const heading =
|
|
42
|
+
title ?? `There ${errors.length === 1 ? 'is 1 problem' : `are ${errors.length} problems`} with this form`;
|
|
43
|
+
return (
|
|
44
|
+
<Alert
|
|
45
|
+
ref={ref}
|
|
46
|
+
tabIndex={-1}
|
|
47
|
+
role="region"
|
|
48
|
+
tone="destructive"
|
|
49
|
+
title={heading}
|
|
50
|
+
className={cn(
|
|
51
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring',
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<ul className="mt-1 list-disc space-y-1 ps-4">
|
|
56
|
+
{errors.map((e) => (
|
|
57
|
+
<li key={e.fieldId}>
|
|
58
|
+
<Link
|
|
59
|
+
href={`#${e.fieldId}`}
|
|
60
|
+
tone="inherit"
|
|
61
|
+
underline="always"
|
|
62
|
+
onClick={(ev) => {
|
|
63
|
+
ev.preventDefault();
|
|
64
|
+
focusField(e.fieldId);
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{e.label ? `${e.label}: ` : ''}
|
|
68
|
+
{e.message}
|
|
69
|
+
</Link>
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
</Alert>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Validation summary',
|
|
5
|
+
family: 'forms',
|
|
6
|
+
status: 'stable',
|
|
7
|
+
description: 'Error summary that lists every problem with a link to its field and takes focus after a failed submit.',
|
|
8
|
+
uses: ['Alert', 'Link'],
|
|
9
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ValidationIssue {
|
|
2
|
+
/** Id of the control to focus (the link target). */
|
|
3
|
+
fieldId: string;
|
|
4
|
+
/** Field name shown before the message. */
|
|
5
|
+
label?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const validationIssues: ValidationIssue[] = [
|
|
10
|
+
{ fieldId: 'project-name', label: 'Project name', message: 'Enter a project name.' },
|
|
11
|
+
{ fieldId: 'billing-email', label: 'Billing email', message: 'Enter a valid email address, like name@company.com.' },
|
|
12
|
+
{ fieldId: 'region', label: 'Region', message: 'Choose a region.' },
|
|
13
|
+
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export * from './DangerZone/DangerZone';
|
|
2
|
+
export { meta as dangerZoneMeta } from './DangerZone/block.meta';
|
|
3
|
+
export * from './CreateApiKeyDialog/CreateApiKeyDialog';
|
|
4
|
+
export { meta as createApiKeyDialogMeta } from './CreateApiKeyDialog/block.meta';
|
|
5
|
+
export * from './CredentialDisclosureDialog/CredentialDisclosureDialog';
|
|
6
|
+
export { meta as credentialDisclosureDialogMeta } from './CredentialDisclosureDialog/block.meta';
|
|
7
|
+
export * from './DateRangeFilter/DateRangeFilter';
|
|
8
|
+
export { meta as dateRangeFilterMeta } from './DateRangeFilter/block.meta';
|
|
9
|
+
export * from './FileUploadPanel/FileUploadPanel';
|
|
10
|
+
export { meta as fileUploadPanelMeta } from './FileUploadPanel/block.meta';
|
|
11
|
+
export * from './FormSection/FormSection';
|
|
12
|
+
export { meta as formSectionMeta } from './FormSection/block.meta';
|
|
13
|
+
export * from './InviteMembersDialog/InviteMembersDialog';
|
|
14
|
+
export { meta as inviteMembersDialogMeta } from './InviteMembersDialog/block.meta';
|
|
15
|
+
export * from './SettingsRow/SettingsRow';
|
|
16
|
+
export { meta as settingsRowMeta } from './SettingsRow/block.meta';
|
|
17
|
+
export * from './ValidationSummary/ValidationSummary';
|
|
18
|
+
export * from './DialogWizard/DialogWizard';
|
|
19
|
+
export { meta as dialogWizardMeta } from './DialogWizard/block.meta';
|
|
20
|
+
export { dialogWizardCopy, dialogWizardSteps } from './DialogWizard/fixtures';
|
|
21
|
+
export { meta as validationSummaryMeta } from './ValidationSummary/block.meta';
|
|
22
|
+
export { ConfirmDestructiveDialog, type ConfirmDestructiveDialogProps } from './DangerZone/ConfirmDestructiveDialog';
|
|
23
|
+
export { dangerZoneActions } from './DangerZone/fixtures';
|
|
24
|
+
export { memberRoles } from './InviteMembersDialog/fixtures';
|
|
25
|
+
export { apiKeyScopes, apiKeyExpiries, generateDemoSecret } from './CreateApiKeyDialog/fixtures';
|
|
26
|
+
export { uploadItems, simulateUpload } from './FileUploadPanel/fixtures';
|
|
27
|
+
export { validationIssues } from './ValidationSummary/fixtures';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { BlockMeta } from './meta';
|
|
2
|
+
export * from './ai';
|
|
3
|
+
export * from './auth';
|
|
4
|
+
export * from './billing';
|
|
5
|
+
export * from './builders';
|
|
6
|
+
export * from './data-display';
|
|
7
|
+
export * from './feedback';
|
|
8
|
+
export * from './forms';
|
|
9
|
+
export * from './marketing';
|
|
10
|
+
export * from './page-chrome';
|
|
11
|
+
export * from './scheduling';
|
|
12
|
+
export * from './shell';
|
|
13
|
+
export * from './tables';
|
|
14
|
+
export * from './team';
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { ComparisonTable } from './ComparisonTable';
|
|
5
|
+
import RunnerOptions from './examples/options';
|
|
6
|
+
import { sampleComparisonPlans, sampleComparisonSections } from './fixtures';
|
|
7
|
+
|
|
8
|
+
const sectionGroups = () => screen.getAllByRole('rowgroup').filter((g) => g.hasAttribute('data-section'));
|
|
9
|
+
|
|
10
|
+
describe('ComparisonTable', () => {
|
|
11
|
+
it('renders plans as columns and features grouped by section', async () => {
|
|
12
|
+
const { container } = render(<ComparisonTable />);
|
|
13
|
+
expect(screen.getByRole('region', { name: 'Find the plan that fits' })).toBeInTheDocument();
|
|
14
|
+
const table = screen.getByRole('table', { name: 'Find the plan that fits' });
|
|
15
|
+
const headers = within(table).getAllByRole('columnheader');
|
|
16
|
+
expect(headers).toHaveLength(sampleComparisonPlans.length + 1);
|
|
17
|
+
expect(headers[2]).toHaveTextContent('TeamRecommended$49');
|
|
18
|
+
expect(sectionGroups()).toHaveLength(sampleComparisonSections.length);
|
|
19
|
+
const usage = sectionGroups()[0]!;
|
|
20
|
+
const head = within(usage).getAllByRole('rowheader')[0]!;
|
|
21
|
+
expect(head).toHaveAttribute('scope', 'rowgroup');
|
|
22
|
+
expect(head).toHaveAttribute('colspan', String(sampleComparisonPlans.length + 1));
|
|
23
|
+
expect(head).toHaveTextContent('Usage');
|
|
24
|
+
expect(within(usage).getByRole('rowheader', { name: /Log retention/ })).toHaveAttribute('scope', 'row');
|
|
25
|
+
await expectNoAxeViolations(container);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders booleans with text alternatives, numbers formatted and notes', () => {
|
|
29
|
+
render(<ComparisonTable />);
|
|
30
|
+
const row = (name: RegExp) => screen.getByRole('rowheader', { name }).closest('tr')!;
|
|
31
|
+
const previews = within(row(/Preview deployments/)).getAllByRole('cell');
|
|
32
|
+
expect(previews.map((c) => c.textContent)).toEqual(['Not included', 'Included', 'Included']);
|
|
33
|
+
const requests = within(row(/Requests per month/)).getAllByRole('cell');
|
|
34
|
+
expect(requests.map((c) => c.textContent)).toEqual(['50,000', '500,000', 'CustomVolume pricing']);
|
|
35
|
+
expect(within(row(/Uptime SLA/)).getAllByRole('cell')[0]).toHaveTextContent('Not included');
|
|
36
|
+
expect(within(row(/Single sign-on/)).getAllByRole('cell')[1]).toHaveTextContent('IncludedAdd-on');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('marks the recommended column with text, not colour alone', () => {
|
|
40
|
+
const { container } = render(<ComparisonTable highlightedPlanId="enterprise" highlightLabel="Best value" />);
|
|
41
|
+
const enterprise = screen.getByRole('columnheader', { name: /Enterprise/ });
|
|
42
|
+
expect(within(enterprise).getByText('Best value')).toBeInTheDocument();
|
|
43
|
+
expect(enterprise).toHaveClass('border-t-primary');
|
|
44
|
+
const tinted = container.querySelectorAll('td[data-highlighted]');
|
|
45
|
+
expect(tinted).toHaveLength(sampleComparisonSections.reduce((n, s) => n + s.features.length, 0));
|
|
46
|
+
expect(screen.queryByText('Recommended')).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('pins the feature column and keeps CTAs keyboard reachable', async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
render(<ComparisonTable />);
|
|
52
|
+
expect(screen.getByRole('rowheader', { name: /Projects/ })).toHaveClass('sticky', 'start-0');
|
|
53
|
+
expect(screen.getByRole('table')).toHaveClass('min-w-[40rem]');
|
|
54
|
+
await user.tab();
|
|
55
|
+
expect(screen.getByRole('link', { name: 'Start for free' })).toHaveFocus();
|
|
56
|
+
await user.tab();
|
|
57
|
+
expect(screen.getByRole('link', { name: 'Start free trial' })).toHaveFocus();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('works without a title, CTAs or prices (options example)', async () => {
|
|
61
|
+
const { container } = render(<RunnerOptions />);
|
|
62
|
+
expect(screen.queryByRole('region')).toBeNull();
|
|
63
|
+
expect(screen.getByRole('table', { name: 'Runner options' })).toBeInTheDocument();
|
|
64
|
+
expect(screen.getByRole('columnheader', { name: 'Capability' })).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByRole('columnheader', { name: /Dedicated runner/ })).toHaveTextContent('Best for teams');
|
|
66
|
+
expect(screen.queryByRole('link')).toBeNull();
|
|
67
|
+
await expectNoAxeViolations(container);
|
|
68
|
+
});
|
|
69
|
+
});
|