@gntik-ai/blocks 0.0.0-stage → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3626 -0
- package/dist/index.js +15417 -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.configured.test.tsx +316 -0
- package/src/auth/SignUpForm/SignUpForm.test.tsx +33 -0
- package/src/auth/SignUpForm/SignUpForm.tsx +298 -0
- package/src/auth/SignUpForm/block.meta.ts +22 -0
- package/src/auth/SignUpForm/examples/CustomFields.tsx +56 -0
- package/src/auth/SignUpForm/examples/Error.tsx +5 -0
- package/src/auth/SignUpForm/examples/Loading.tsx +6 -0
- package/src/auth/SignUpForm/examples/NoTerms.tsx +6 -0
- package/src/auth/SignUpForm/examples/Submitting.tsx +5 -0
- package/src/auth/SignUpForm/examples/Unavailable.tsx +15 -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 +16 -0
- package/src/auth/shared.tsx +147 -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 +25 -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.labels.test.tsx +291 -0
- package/src/tables/DataTable/DataTable.layout.test.tsx +188 -0
- package/src/tables/DataTable/DataTable.overflow-labels.test.tsx +59 -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 +531 -0
- package/src/tables/DataTable/block.meta.ts +129 -0
- package/src/tables/DataTable/column-menu.tsx +112 -0
- package/src/tables/DataTable/column-sizing.ts +166 -0
- package/src/tables/DataTable/data-table-body.tsx +195 -0
- package/src/tables/DataTable/data-table-props.ts +159 -0
- package/src/tables/DataTable/data-table-row.tsx +231 -0
- package/src/tables/DataTable/data-table-utils.ts +195 -0
- package/src/tables/DataTable/density-toggle.tsx +33 -0
- package/src/tables/DataTable/editable-cell.tsx +287 -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 +90 -0
- package/src/tables/DataTable/labels.ts +54 -0
- package/src/tables/DataTable/resize-handle.tsx +91 -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 +193 -0
- package/src/tables/FilterBar/FilterBar.labels.test.tsx +69 -0
- package/src/tables/FilterBar/FilterBar.test.tsx +43 -0
- package/src/tables/FilterBar/FilterBar.tsx +232 -0
- package/src/tables/FilterBar/block.meta.ts +32 -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.labels.test.tsx +57 -0
- package/src/tables/PaginationFooter/PaginationFooter.test.tsx +29 -0
- package/src/tables/PaginationFooter/PaginationFooter.tsx +118 -0
- package/src/tables/PaginationFooter/block.meta.ts +33 -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,145 @@
|
|
|
1
|
+
import { Trash2 } from '@gntik-ai/icons';
|
|
2
|
+
import {
|
|
3
|
+
AlertDialog,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
AlertDialogTrigger,
|
|
11
|
+
Button,
|
|
12
|
+
Field,
|
|
13
|
+
FieldLabel,
|
|
14
|
+
Input,
|
|
15
|
+
} from '@gntik-ai/ui';
|
|
16
|
+
import { useState, type FormEvent, type ReactNode } from 'react';
|
|
17
|
+
|
|
18
|
+
export interface ConfirmDestructiveProps {
|
|
19
|
+
/** Exact name the user must type to unlock the action. */
|
|
20
|
+
resourceName?: string;
|
|
21
|
+
/** What kind of resource it is (used in the default copy). */
|
|
22
|
+
resourceType?: string;
|
|
23
|
+
title?: ReactNode;
|
|
24
|
+
description?: ReactNode;
|
|
25
|
+
/** What will be lost, as a short list. */
|
|
26
|
+
consequences?: string[];
|
|
27
|
+
/** Label of the confirming button (and of the default trigger). */
|
|
28
|
+
confirmLabel?: string;
|
|
29
|
+
/** Default trigger button text; `null` renders no trigger (control `open` instead). */
|
|
30
|
+
triggerLabel?: string | null;
|
|
31
|
+
open?: boolean;
|
|
32
|
+
defaultOpen?: boolean;
|
|
33
|
+
onOpenChange?: (open: boolean) => void;
|
|
34
|
+
/** Runs on confirm. Returning a promise keeps the dialog open with a spinner until it settles; a rejection shows its message. */
|
|
35
|
+
onConfirm?: () => void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Typed-confirmation for irreversible actions: an AlertDialog whose destructive button stays
|
|
40
|
+
* disabled until the resource name is typed exactly.
|
|
41
|
+
*/
|
|
42
|
+
export function ConfirmDestructive({
|
|
43
|
+
resourceName = 'acme-web',
|
|
44
|
+
resourceType = 'project',
|
|
45
|
+
title,
|
|
46
|
+
description,
|
|
47
|
+
consequences = [
|
|
48
|
+
'All deployments and preview URLs',
|
|
49
|
+
'Environment variables and secrets',
|
|
50
|
+
'The audit log for this project',
|
|
51
|
+
],
|
|
52
|
+
confirmLabel,
|
|
53
|
+
triggerLabel,
|
|
54
|
+
open,
|
|
55
|
+
defaultOpen = false,
|
|
56
|
+
onOpenChange,
|
|
57
|
+
onConfirm,
|
|
58
|
+
}: ConfirmDestructiveProps) {
|
|
59
|
+
const [innerOpen, setInnerOpen] = useState(defaultOpen);
|
|
60
|
+
const [typed, setTyped] = useState('');
|
|
61
|
+
const [pending, setPending] = useState(false);
|
|
62
|
+
const [error, setError] = useState<string | null>(null);
|
|
63
|
+
const isOpen = open ?? innerOpen;
|
|
64
|
+
const label = confirmLabel ?? `Delete ${resourceType}`;
|
|
65
|
+
const matches = typed === resourceName;
|
|
66
|
+
|
|
67
|
+
const setOpen = (next: boolean) => {
|
|
68
|
+
if (!next) {
|
|
69
|
+
setTyped('');
|
|
70
|
+
setError(null);
|
|
71
|
+
}
|
|
72
|
+
setInnerOpen(next);
|
|
73
|
+
onOpenChange?.(next);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const confirm = async (event: FormEvent) => {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
if (!matches || pending) return;
|
|
79
|
+
setPending(true);
|
|
80
|
+
setError(null);
|
|
81
|
+
try {
|
|
82
|
+
await onConfirm?.();
|
|
83
|
+
setPending(false);
|
|
84
|
+
setOpen(false);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
setPending(false);
|
|
87
|
+
setError(err instanceof Error ? err.message : 'Something went wrong. Try again.');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<AlertDialog open={isOpen} onOpenChange={(next) => !pending && setOpen(next)}>
|
|
93
|
+
{triggerLabel !== null && (
|
|
94
|
+
<AlertDialogTrigger render={<Button variant="destructive" icon={Trash2} />}>
|
|
95
|
+
{triggerLabel ?? label}
|
|
96
|
+
</AlertDialogTrigger>
|
|
97
|
+
)}
|
|
98
|
+
<AlertDialogContent>
|
|
99
|
+
<AlertDialogHeader tone="destructive">
|
|
100
|
+
<AlertDialogTitle>{title ?? `Delete ${resourceType}?`}</AlertDialogTitle>
|
|
101
|
+
<AlertDialogDescription>
|
|
102
|
+
{description ?? (
|
|
103
|
+
<>
|
|
104
|
+
This permanently deletes <span className="font-mono text-foreground">{resourceName}</span>. This action
|
|
105
|
+
cannot be undone.
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</AlertDialogDescription>
|
|
109
|
+
</AlertDialogHeader>
|
|
110
|
+
{consequences.length > 0 && (
|
|
111
|
+
<ul className="mt-4 list-disc space-y-1 rounded-md border border-destructive/25 bg-destructive/5 py-2.5 pe-3 ps-8 text-[12.5px] text-muted-foreground">
|
|
112
|
+
{consequences.map((c) => (
|
|
113
|
+
<li key={c}>{c}</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
)}
|
|
117
|
+
<form onSubmit={(e) => void confirm(e)} className="mt-4">
|
|
118
|
+
<Field>
|
|
119
|
+
<FieldLabel>
|
|
120
|
+
Type <span className="font-mono font-semibold text-foreground">{resourceName}</span> to confirm
|
|
121
|
+
</FieldLabel>
|
|
122
|
+
<Input
|
|
123
|
+
value={typed}
|
|
124
|
+
onChange={(e) => setTyped(e.target.value)}
|
|
125
|
+
autoComplete="off"
|
|
126
|
+
spellCheck={false}
|
|
127
|
+
disabled={pending}
|
|
128
|
+
/>
|
|
129
|
+
</Field>
|
|
130
|
+
{error && (
|
|
131
|
+
<p role="alert" className="mt-2 text-[12.5px] text-destructive-text">
|
|
132
|
+
{error}
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
<AlertDialogFooter className="mt-5">
|
|
136
|
+
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
|
|
137
|
+
<Button type="submit" variant="destructive" loading={pending} disabled={!matches}>
|
|
138
|
+
{label}
|
|
139
|
+
</Button>
|
|
140
|
+
</AlertDialogFooter>
|
|
141
|
+
</form>
|
|
142
|
+
</AlertDialogContent>
|
|
143
|
+
</AlertDialog>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Confirm destructive',
|
|
5
|
+
family: 'feedback',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'AlertDialog for irreversible actions that requires typing the resource name, with async confirm and error display.',
|
|
9
|
+
uses: ['AlertDialog', 'Button', 'Field', 'Input'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { FirstRunEmpty, NoAccessEmpty, NoResultsEmpty } from './EmptyStates';
|
|
5
|
+
|
|
6
|
+
describe('EmptyStates', () => {
|
|
7
|
+
it('renders the three defaults', async () => {
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<>
|
|
10
|
+
<FirstRunEmpty />
|
|
11
|
+
<NoResultsEmpty />
|
|
12
|
+
<NoAccessEmpty />
|
|
13
|
+
</>,
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByRole('heading', { name: 'Create your first project' })).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByRole('heading', { name: /No projects match/ })).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByRole('heading', { name: 'You don’t have access to Billing' })).toBeInTheDocument();
|
|
18
|
+
await expectNoAxeViolations(container);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('wires the first-run and no-results actions', async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const onAction = vi.fn();
|
|
24
|
+
const onClearFilters = vi.fn();
|
|
25
|
+
const onClearSearch = vi.fn();
|
|
26
|
+
render(
|
|
27
|
+
<>
|
|
28
|
+
<FirstRunEmpty onAction={onAction} />
|
|
29
|
+
<NoResultsEmpty onClearFilters={onClearFilters} onClearSearch={onClearSearch} />
|
|
30
|
+
</>,
|
|
31
|
+
);
|
|
32
|
+
await user.click(screen.getByRole('button', { name: 'New project' }));
|
|
33
|
+
await user.click(screen.getByRole('button', { name: 'Clear filters' }));
|
|
34
|
+
await user.click(screen.getByRole('button', { name: 'Clear search' }));
|
|
35
|
+
expect(onAction).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(onClearFilters).toHaveBeenCalledTimes(1);
|
|
37
|
+
expect(onClearSearch).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('requests access once', async () => {
|
|
41
|
+
const onRequestAccess = vi.fn(() => Promise.resolve());
|
|
42
|
+
render(<NoAccessEmpty onRequestAccess={onRequestAccess} />);
|
|
43
|
+
await userEvent.setup().click(screen.getByRole('button', { name: 'Request access' }));
|
|
44
|
+
expect(onRequestAccess).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(await screen.findByRole('button', { name: 'Access requested' })).toBeDisabled();
|
|
46
|
+
expect(screen.getByRole('status')).toHaveTextContent('Access request sent');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Check, FolderPlus, Lock, SearchX } from '@gntik-ai/icons';
|
|
2
|
+
import type { LucideIcon } from '@gntik-ai/icons';
|
|
3
|
+
import { Button, EmptyState, Link, cn } from '@gntik-ai/ui';
|
|
4
|
+
import { useState, type ReactNode } from 'react';
|
|
5
|
+
import { firstRunSteps, noAccessSample, noResultsSample } from './fixtures';
|
|
6
|
+
|
|
7
|
+
interface SharedEmptyProps {
|
|
8
|
+
/** Heading level that fits the page outline. */
|
|
9
|
+
titleAs?: 'h2' | 'h3' | 'h4';
|
|
10
|
+
/** Dashed border around the state (use inside a card or table). */
|
|
11
|
+
bordered?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FirstRunEmptyProps extends SharedEmptyProps {
|
|
16
|
+
icon?: LucideIcon;
|
|
17
|
+
title?: ReactNode;
|
|
18
|
+
description?: ReactNode;
|
|
19
|
+
actionLabel?: string;
|
|
20
|
+
onAction?: () => void;
|
|
21
|
+
/** Secondary link (docs, guide). `null` hides it. */
|
|
22
|
+
secondary?: { label: string; href: string } | null;
|
|
23
|
+
/** Short checklist of what the first run involves. */
|
|
24
|
+
steps?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** First-run state: nothing created yet; explains the value and offers the first action. */
|
|
28
|
+
export function FirstRunEmpty({
|
|
29
|
+
icon = FolderPlus,
|
|
30
|
+
title = 'Create your first project',
|
|
31
|
+
description = 'Projects group your services, environments and deployments. Start one from a repository in a couple of minutes.',
|
|
32
|
+
actionLabel = 'New project',
|
|
33
|
+
onAction,
|
|
34
|
+
secondary = { label: 'Read the guide', href: '#guide' },
|
|
35
|
+
steps = firstRunSteps,
|
|
36
|
+
titleAs,
|
|
37
|
+
bordered = true,
|
|
38
|
+
className,
|
|
39
|
+
}: FirstRunEmptyProps) {
|
|
40
|
+
return (
|
|
41
|
+
<EmptyState
|
|
42
|
+
icon={icon}
|
|
43
|
+
title={title}
|
|
44
|
+
titleAs={titleAs}
|
|
45
|
+
description={description}
|
|
46
|
+
bordered={bordered}
|
|
47
|
+
className={cn('mx-auto', className)}
|
|
48
|
+
primaryAction={<Button onClick={onAction}>{actionLabel}</Button>}
|
|
49
|
+
secondaryAction={
|
|
50
|
+
secondary ? (
|
|
51
|
+
<Link
|
|
52
|
+
underline="hover"
|
|
53
|
+
href={secondary.href}
|
|
54
|
+
className="inline-flex h-9 items-center px-2 text-[13px] font-semibold"
|
|
55
|
+
>
|
|
56
|
+
{secondary.label}
|
|
57
|
+
</Link>
|
|
58
|
+
) : undefined
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
{steps.length > 0 && (
|
|
62
|
+
<ol className="mt-6 grid gap-2 text-start text-[12.5px] text-muted-foreground">
|
|
63
|
+
{steps.map((step, i) => (
|
|
64
|
+
<li key={step} className="flex items-center gap-2.5">
|
|
65
|
+
<span className="grid size-5 shrink-0 place-items-center rounded-full bg-primary/14 font-mono text-[10.5px] font-semibold text-primary-chip-text">
|
|
66
|
+
{i + 1}
|
|
67
|
+
</span>
|
|
68
|
+
{step}
|
|
69
|
+
</li>
|
|
70
|
+
))}
|
|
71
|
+
</ol>
|
|
72
|
+
)}
|
|
73
|
+
</EmptyState>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface NoResultsEmptyProps extends SharedEmptyProps {
|
|
78
|
+
/** The search text that matched nothing. */
|
|
79
|
+
query?: string;
|
|
80
|
+
/** Number of active filters (shows "Clear filters" when > 0). */
|
|
81
|
+
filterCount?: number;
|
|
82
|
+
/** Plural noun for what is being searched. */
|
|
83
|
+
entity?: string;
|
|
84
|
+
onClearFilters?: () => void;
|
|
85
|
+
onClearSearch?: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** No-results state for a filtered list or table: echoes the query and offers ways out. */
|
|
89
|
+
export function NoResultsEmpty({
|
|
90
|
+
query = noResultsSample.query,
|
|
91
|
+
filterCount = noResultsSample.filterCount,
|
|
92
|
+
entity = noResultsSample.entity,
|
|
93
|
+
onClearFilters,
|
|
94
|
+
onClearSearch,
|
|
95
|
+
titleAs,
|
|
96
|
+
bordered = false,
|
|
97
|
+
className,
|
|
98
|
+
}: NoResultsEmptyProps) {
|
|
99
|
+
const filters = filterCount > 0 ? ` with ${filterCount} ${filterCount === 1 ? 'filter' : 'filters'} applied` : '';
|
|
100
|
+
return (
|
|
101
|
+
<EmptyState
|
|
102
|
+
icon={SearchX}
|
|
103
|
+
size="sm"
|
|
104
|
+
titleAs={titleAs}
|
|
105
|
+
bordered={bordered}
|
|
106
|
+
className={cn('mx-auto', className)}
|
|
107
|
+
title={
|
|
108
|
+
query ? (
|
|
109
|
+
<>
|
|
110
|
+
No {entity} match “<span className="font-mono">{query}</span>”
|
|
111
|
+
</>
|
|
112
|
+
) : (
|
|
113
|
+
`No ${entity} found`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
description={`Check the spelling or try a broader search${filters}.`}
|
|
117
|
+
primaryAction={
|
|
118
|
+
filterCount > 0 ? (
|
|
119
|
+
<Button variant="secondary" size="sm" onClick={onClearFilters}>
|
|
120
|
+
Clear filters
|
|
121
|
+
</Button>
|
|
122
|
+
) : undefined
|
|
123
|
+
}
|
|
124
|
+
secondaryAction={
|
|
125
|
+
query ? (
|
|
126
|
+
<Button variant="ghost" size="sm" onClick={onClearSearch}>
|
|
127
|
+
Clear search
|
|
128
|
+
</Button>
|
|
129
|
+
) : undefined
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface NoAccessEmptyProps extends SharedEmptyProps {
|
|
136
|
+
/** What the viewer cannot see. */
|
|
137
|
+
resource?: string;
|
|
138
|
+
/** Who can grant access. */
|
|
139
|
+
owner?: string;
|
|
140
|
+
/** Called on "Request access"; may return a promise (the button shows a spinner). */
|
|
141
|
+
onRequestAccess?: () => void | Promise<void>;
|
|
142
|
+
/** Start in the "requested" state (e.g. a pending request exists). */
|
|
143
|
+
defaultRequested?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Permission state: the viewer lacks access; explains who can grant it and lets them ask. */
|
|
147
|
+
export function NoAccessEmpty({
|
|
148
|
+
resource = noAccessSample.resource,
|
|
149
|
+
owner = noAccessSample.owner,
|
|
150
|
+
onRequestAccess,
|
|
151
|
+
defaultRequested = false,
|
|
152
|
+
titleAs,
|
|
153
|
+
bordered = true,
|
|
154
|
+
className,
|
|
155
|
+
}: NoAccessEmptyProps) {
|
|
156
|
+
const [state, setState] = useState<'idle' | 'pending' | 'requested'>(defaultRequested ? 'requested' : 'idle');
|
|
157
|
+
const request = async () => {
|
|
158
|
+
setState('pending');
|
|
159
|
+
try {
|
|
160
|
+
await onRequestAccess?.();
|
|
161
|
+
setState('requested');
|
|
162
|
+
} catch {
|
|
163
|
+
setState('idle');
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
return (
|
|
167
|
+
<EmptyState
|
|
168
|
+
icon={Lock}
|
|
169
|
+
titleAs={titleAs}
|
|
170
|
+
bordered={bordered}
|
|
171
|
+
className={cn('mx-auto', className)}
|
|
172
|
+
title={`You don’t have access to ${resource}`}
|
|
173
|
+
description={`Ask ${owner} to grant you a role that includes ${resource.toLowerCase()} permissions.`}
|
|
174
|
+
primaryAction={
|
|
175
|
+
<Button
|
|
176
|
+
variant={state === 'requested' ? 'secondary' : 'primary'}
|
|
177
|
+
icon={state === 'requested' ? Check : undefined}
|
|
178
|
+
loading={state === 'pending'}
|
|
179
|
+
disabled={state === 'requested'}
|
|
180
|
+
onClick={() => void request()}
|
|
181
|
+
>
|
|
182
|
+
{state === 'requested' ? 'Access requested' : 'Request access'}
|
|
183
|
+
</Button>
|
|
184
|
+
}
|
|
185
|
+
>
|
|
186
|
+
<p role="status" className="sr-only">
|
|
187
|
+
{state === 'requested' ? 'Access request sent' : ''}
|
|
188
|
+
</p>
|
|
189
|
+
</EmptyState>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Empty states',
|
|
5
|
+
family: 'feedback',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'Three canonical empty states: first run (with checklist), no results (clear filters/search) and no access (request access).',
|
|
9
|
+
uses: ['EmptyState', 'Button', 'Link'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Neutral sample copy for the empty states. */
|
|
2
|
+
export const firstRunSteps = ['Connect a Git repository', 'Configure build settings', 'Deploy to an environment'];
|
|
3
|
+
|
|
4
|
+
export const noResultsSample = { query: 'billing-api', filterCount: 2, entity: 'projects' } as const;
|
|
5
|
+
|
|
6
|
+
export const noAccessSample = { resource: 'Billing', owner: 'an organization admin' } as const;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { ErrorPanel } from './ErrorPanel';
|
|
5
|
+
|
|
6
|
+
describe('ErrorPanel', () => {
|
|
7
|
+
it('accepts a null code to hide the fixture chip', () => {
|
|
8
|
+
render(<ErrorPanel code={null} />);
|
|
9
|
+
expect(screen.queryByText('503')).not.toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders the default panel', async () => {
|
|
13
|
+
const { container } = render(<ErrorPanel onRetry={() => {}} />);
|
|
14
|
+
expect(screen.getByRole('region', { name: 'We couldn’t load deployments' })).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByText('req_8f2c41d07a9b')).toBeInTheDocument();
|
|
16
|
+
await expectNoAxeViolations(container);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('shows a loading state while retrying and reveals details', async () => {
|
|
20
|
+
const user = userEvent.setup();
|
|
21
|
+
let resolve = () => {};
|
|
22
|
+
const onRetry = vi.fn(() => new Promise<void>((r) => (resolve = r)));
|
|
23
|
+
render(<ErrorPanel onRetry={onRetry} />);
|
|
24
|
+
await user.click(screen.getByRole('button', { name: 'Try again' }));
|
|
25
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
26
|
+
const busy = screen.getByRole('button', { name: 'Retrying…' });
|
|
27
|
+
expect(busy).toHaveAttribute('aria-busy', 'true');
|
|
28
|
+
resolve();
|
|
29
|
+
expect(await screen.findByRole('button', { name: 'Try again' })).toBeEnabled();
|
|
30
|
+
|
|
31
|
+
const toggle = screen.getByRole('button', { name: 'Technical details' });
|
|
32
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
33
|
+
await user.click(toggle);
|
|
34
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
35
|
+
expect(screen.getByText(/503 Service Unavailable/)).toBeVisible();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Check, Copy, RotateCw, ServerCrash } from '@gntik-ai/icons';
|
|
2
|
+
import type { LucideIcon } from '@gntik-ai/icons';
|
|
3
|
+
import { Button, Collapsible, CollapsiblePanel, CollapsibleTrigger, IconButton, cn, useI18n } from '@gntik-ai/ui';
|
|
4
|
+
import { useId, useRef, useState, type ReactNode } from 'react';
|
|
5
|
+
import type { FeedbackAction } from '../types';
|
|
6
|
+
|
|
7
|
+
export interface ErrorPanelProps {
|
|
8
|
+
icon?: LucideIcon;
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
/** Human summary of what failed and what to do. */
|
|
11
|
+
message?: ReactNode;
|
|
12
|
+
/** Status or error code shown as a mono chip (e.g. 503, ECONNRESET). */
|
|
13
|
+
code?: string | number | null;
|
|
14
|
+
/** Correlation id for support; gets a copy button. */
|
|
15
|
+
requestId?: string;
|
|
16
|
+
/** Raw details (response body, stack) behind a disclosure. */
|
|
17
|
+
details?: string;
|
|
18
|
+
/** Retry handler. A returned promise shows the loading state until it settles. */
|
|
19
|
+
onRetry?: () => void | Promise<void>;
|
|
20
|
+
/** Force the loading state from outside. */
|
|
21
|
+
retrying?: boolean;
|
|
22
|
+
retryLabel?: string;
|
|
23
|
+
/** Extra action next to Retry (e.g. contact support). */
|
|
24
|
+
secondaryAction?: FeedbackAction | null;
|
|
25
|
+
titleAs?: 'h2' | 'h3';
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Error summary for a failed load or action: what happened, request id, retry and technical details. */
|
|
30
|
+
export function ErrorPanel({
|
|
31
|
+
icon: Icon = ServerCrash,
|
|
32
|
+
title = 'We couldn’t load deployments',
|
|
33
|
+
message = 'The service didn’t respond in time. Your data is safe; try again in a moment.',
|
|
34
|
+
code = 503,
|
|
35
|
+
requestId = 'req_8f2c41d07a9b',
|
|
36
|
+
details = 'GET /v1/projects/acme-web/deployments\n503 Service Unavailable\nupstream timed out after 30000 ms (pool: api-eu-west-1)',
|
|
37
|
+
onRetry,
|
|
38
|
+
retrying,
|
|
39
|
+
retryLabel: retryLabelProp,
|
|
40
|
+
secondaryAction = { label: 'Contact support', variant: 'ghost' },
|
|
41
|
+
titleAs: Heading = 'h3',
|
|
42
|
+
className,
|
|
43
|
+
}: ErrorPanelProps) {
|
|
44
|
+
const { t } = useI18n();
|
|
45
|
+
const retryLabel = retryLabelProp ?? t('error.retry');
|
|
46
|
+
const id = useId();
|
|
47
|
+
const [innerRetrying, setInnerRetrying] = useState(false);
|
|
48
|
+
const [copied, setCopied] = useState(false);
|
|
49
|
+
const copyTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
50
|
+
const busy = retrying ?? innerRetrying;
|
|
51
|
+
|
|
52
|
+
const retry = async () => {
|
|
53
|
+
if (!onRetry || busy) return;
|
|
54
|
+
setInnerRetrying(true);
|
|
55
|
+
try {
|
|
56
|
+
await onRetry();
|
|
57
|
+
} finally {
|
|
58
|
+
setInnerRetrying(false);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const copy = async () => {
|
|
63
|
+
if (!requestId) return;
|
|
64
|
+
try {
|
|
65
|
+
await navigator.clipboard.writeText(requestId);
|
|
66
|
+
setCopied(true);
|
|
67
|
+
clearTimeout(copyTimer.current);
|
|
68
|
+
copyTimer.current = setTimeout(() => setCopied(false), 2000);
|
|
69
|
+
} catch {
|
|
70
|
+
setCopied(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<section
|
|
76
|
+
aria-labelledby={`${id}-title`}
|
|
77
|
+
className={cn('rounded-lg border border-destructive/30 bg-card p-5 shadow-sm', className)}
|
|
78
|
+
>
|
|
79
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
|
|
80
|
+
<span className="grid size-10 shrink-0 place-items-center rounded-lg bg-destructive/10 text-destructive-chip-text">
|
|
81
|
+
<Icon size={20} aria-hidden />
|
|
82
|
+
</span>
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
85
|
+
<Heading id={`${id}-title`} className="text-[15px] font-semibold tracking-tight text-foreground">
|
|
86
|
+
{title}
|
|
87
|
+
</Heading>
|
|
88
|
+
{code != null && (
|
|
89
|
+
<span className="rounded bg-destructive/10 px-1.5 py-0.5 font-mono text-[11px] font-semibold text-destructive-chip-text">
|
|
90
|
+
{code}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
{message != null && <p className="mt-1.5 max-w-xl text-[13px] leading-6 text-muted-foreground">{message}</p>}
|
|
95
|
+
{requestId && (
|
|
96
|
+
<div className="mt-3 flex items-center gap-1.5 text-[12px] text-muted-foreground">
|
|
97
|
+
<span>{t('error.requestId')}</span>
|
|
98
|
+
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
|
|
99
|
+
{requestId}
|
|
100
|
+
</code>
|
|
101
|
+
<IconButton
|
|
102
|
+
icon={copied ? Check : Copy}
|
|
103
|
+
label={copied ? t('common.copied') : t('error.copyRequestId')}
|
|
104
|
+
size="sm"
|
|
105
|
+
variant="ghost"
|
|
106
|
+
className="size-7"
|
|
107
|
+
onClick={() => void copy()}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
112
|
+
{onRetry && (
|
|
113
|
+
<Button variant="secondary" size="sm" icon={RotateCw} loading={busy} onClick={() => void retry()}>
|
|
114
|
+
{busy ? t('error.retrying') : retryLabel}
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
{secondaryAction && (
|
|
118
|
+
<Button
|
|
119
|
+
size="sm"
|
|
120
|
+
variant={secondaryAction.variant ?? 'ghost'}
|
|
121
|
+
icon={secondaryAction.icon}
|
|
122
|
+
disabled={secondaryAction.disabled}
|
|
123
|
+
onClick={secondaryAction.onClick}
|
|
124
|
+
>
|
|
125
|
+
{secondaryAction.label}
|
|
126
|
+
</Button>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
{details && (
|
|
130
|
+
<Collapsible className="mt-4">
|
|
131
|
+
<CollapsibleTrigger className="text-[12.5px]">{t('error.details')}</CollapsibleTrigger>
|
|
132
|
+
<CollapsiblePanel>
|
|
133
|
+
<pre className="mt-2 max-h-60 overflow-auto rounded-md border border-border bg-muted/50 p-3 font-mono text-[11.5px] leading-5 whitespace-pre-wrap text-foreground">
|
|
134
|
+
{details}
|
|
135
|
+
</pre>
|
|
136
|
+
</CollapsiblePanel>
|
|
137
|
+
</Collapsible>
|
|
138
|
+
)}
|
|
139
|
+
<p role="status" className="sr-only">
|
|
140
|
+
{busy ? t('error.retryingStatus') : copied ? t('error.requestIdCopied') : ''}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Error panel',
|
|
5
|
+
family: 'feedback',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'Error summary with status code, copyable request id, retry with loading state and a technical-details disclosure.',
|
|
9
|
+
uses: ['Button', 'IconButton', 'Collapsible'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { InlineCallout } from './InlineCallout';
|
|
5
|
+
|
|
6
|
+
describe('InlineCallout', () => {
|
|
7
|
+
it('renders the default callout', async () => {
|
|
8
|
+
const { container } = render(<InlineCallout />);
|
|
9
|
+
expect(screen.getByRole('status', { name: /92% of your build minutes/ })).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole('link', { name: 'View usage' })).toBeInTheDocument();
|
|
11
|
+
await expectNoAxeViolations(container);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('runs actions and dismisses', async () => {
|
|
15
|
+
const user = userEvent.setup();
|
|
16
|
+
const onRetry = vi.fn();
|
|
17
|
+
const onDismiss = vi.fn();
|
|
18
|
+
render(
|
|
19
|
+
<InlineCallout
|
|
20
|
+
tone="destructive"
|
|
21
|
+
title="Sync failed"
|
|
22
|
+
actions={[{ label: 'Retry', onClick: onRetry }]}
|
|
23
|
+
dismissible
|
|
24
|
+
onDismiss={onDismiss}
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
await user.click(screen.getByRole('button', { name: 'Retry' }));
|
|
28
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
29
|
+
await user.click(screen.getByRole('button', { name: 'Dismiss' }));
|
|
30
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|