@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,147 @@
|
|
|
1
|
+
import { cn } from '@gntik-ai/ui';
|
|
2
|
+
import { useState, type ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface AuthHeaderProps {
|
|
5
|
+
eyebrow?: ReactNode;
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** Defaults to h1. `null` omits the title heading. */
|
|
9
|
+
headingLevel?: 'h1' | 'h2' | 'h3' | null;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Eyebrow + title + supporting line used at the top of every auth form. */
|
|
14
|
+
export function AuthHeader({ eyebrow, title, description, headingLevel: Heading = 'h1', className }: AuthHeaderProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('mb-6', className)}>
|
|
17
|
+
{eyebrow != null && (
|
|
18
|
+
<p className="mb-2.5 font-mono text-[11px] tracking-[0.2em] text-primary-text uppercase">{eyebrow}</p>
|
|
19
|
+
)}
|
|
20
|
+
{Heading != null && title != null && (
|
|
21
|
+
<Heading className="text-[24px] font-semibold tracking-tight text-balance text-foreground sm:text-[27px]">
|
|
22
|
+
{title}
|
|
23
|
+
</Heading>
|
|
24
|
+
)}
|
|
25
|
+
{description != null && (
|
|
26
|
+
<p className="mt-2 text-[14px] leading-relaxed text-pretty text-muted-foreground">{description}</p>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Horizontal rule with a centred caption ("Or continue with"). */
|
|
33
|
+
export function AuthDivider({ children }: { children: ReactNode }) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="my-6 flex items-center gap-3" role="presentation">
|
|
36
|
+
<span className="h-px flex-1 bg-border" />
|
|
37
|
+
<span className="font-mono text-[10.5px] font-semibold tracking-[0.2em] text-muted-foreground uppercase">
|
|
38
|
+
{children}
|
|
39
|
+
</span>
|
|
40
|
+
<span className="h-px flex-1 bg-border" />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const authFormClass = 'w-full max-w-[392px]';
|
|
46
|
+
|
|
47
|
+
/** Loading + error state around an async submit handler. A rejection's message becomes the error. */
|
|
48
|
+
export function useAsyncSubmit(fallbackError = 'Something went wrong. Try again.') {
|
|
49
|
+
const [loading, setLoading] = useState(false);
|
|
50
|
+
const [error, setError] = useState<string | null>(null);
|
|
51
|
+
const run = async (fn: () => void | Promise<void>) => {
|
|
52
|
+
setError(null);
|
|
53
|
+
setLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
await fn();
|
|
56
|
+
setLoading(false);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
setError(e instanceof Error ? e.message : fallbackError);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return { loading, error, setError, run };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
68
|
+
|
|
69
|
+
/** Email by default; text mode supports a username or username-or-email identifier. */
|
|
70
|
+
export interface AuthIdentifierConfig {
|
|
71
|
+
type?: 'email' | 'text';
|
|
72
|
+
autoComplete?: string;
|
|
73
|
+
label?: ReactNode;
|
|
74
|
+
help?: ReactNode;
|
|
75
|
+
/** Optional stable id for the FieldDescription. Otherwise Base UI generates it. */
|
|
76
|
+
helpId?: string;
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
minLength?: number;
|
|
79
|
+
maxLength?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Kit-owned inline validation; consumers supply copy rather than running a second validator. */
|
|
83
|
+
export interface AuthValidationMessages {
|
|
84
|
+
identifierRequired?: string;
|
|
85
|
+
identifierInvalid?: string;
|
|
86
|
+
identifierTooShort?: string;
|
|
87
|
+
passwordRequired?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Server errors use only these props plus the form's error alert; inline validation stays in the kit. */
|
|
91
|
+
export interface AuthFormAccessibilityProps {
|
|
92
|
+
fieldInvalid?: { identifier?: boolean; password?: boolean };
|
|
93
|
+
/** Id of the server alert (or an external feedback element), appended to flagged inputs. */
|
|
94
|
+
feedbackId?: string;
|
|
95
|
+
/** Describes the form itself; does not replace the per-field feedback association. */
|
|
96
|
+
'aria-describedby'?: string;
|
|
97
|
+
'aria-label'?: string;
|
|
98
|
+
'aria-labelledby'?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function authIdentifierError(value: string, config?: AuthIdentifierConfig, messages?: AuthValidationMessages) {
|
|
102
|
+
const trimmed = value.trim();
|
|
103
|
+
const fallback = config?.type === 'text' ? 'Enter a valid identifier.' : 'Enter a valid email address.';
|
|
104
|
+
if (!trimmed) return messages?.identifierRequired ?? fallback;
|
|
105
|
+
if (config?.minLength !== undefined && trimmed.length < config.minLength)
|
|
106
|
+
return messages?.identifierTooShort ?? messages?.identifierInvalid ?? fallback;
|
|
107
|
+
if (
|
|
108
|
+
((config?.type ?? 'email') === 'email' && !EMAIL_RE.test(trimmed)) ||
|
|
109
|
+
(config?.maxLength !== undefined && trimmed.length > config.maxLength)
|
|
110
|
+
)
|
|
111
|
+
return messages?.identifierInvalid ?? fallback;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Ordered text/email fields collected before the sign-up password. Ids must be unique. */
|
|
116
|
+
export interface AuthSignUpField {
|
|
117
|
+
id: string;
|
|
118
|
+
label: string;
|
|
119
|
+
placeholder?: string;
|
|
120
|
+
help?: ReactNode;
|
|
121
|
+
required?: boolean;
|
|
122
|
+
type?: 'text' | 'email';
|
|
123
|
+
autoComplete?: string;
|
|
124
|
+
/** Consumer copy for kit-owned validation. */
|
|
125
|
+
messages?: { required?: string; invalid?: string };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function authSignUpFieldError(value: string, field: AuthSignUpField) {
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
if (!trimmed) return field.required ? (field.messages?.required ?? `Enter ${field.label}.`) : null;
|
|
131
|
+
if (field.type === 'email' && !EMAIL_RE.test(trimmed))
|
|
132
|
+
return field.messages?.invalid ?? 'Enter a valid email address.';
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Consumer-owned copy and optional action when account creation is unavailable. */
|
|
137
|
+
export interface AuthUnavailableState {
|
|
138
|
+
title: ReactNode;
|
|
139
|
+
message: ReactNode;
|
|
140
|
+
action?: ReactNode;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Kit-owned sign-up validation with consumer-owned copy. */
|
|
144
|
+
export interface AuthSignUpValidationMessages {
|
|
145
|
+
passwordTooShort?: (minimum: number) => string;
|
|
146
|
+
termsRequired?: string;
|
|
147
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
3
|
+
import { CostBreakdown } from './CostBreakdown';
|
|
4
|
+
|
|
5
|
+
describe('CostBreakdown', () => {
|
|
6
|
+
it('renders the donut figure and the total', async () => {
|
|
7
|
+
const { container } = render(<CostBreakdown />);
|
|
8
|
+
expect(screen.getByRole('figure', { name: /Cost breakdown: Compute \$740.00/ })).toBeInTheDocument();
|
|
9
|
+
expect(screen.getByText('$1,620.00')).toBeInTheDocument();
|
|
10
|
+
await expectNoAxeViolations(container);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('ranks categories by amount with their share', () => {
|
|
14
|
+
render(<CostBreakdown />);
|
|
15
|
+
const items = within(screen.getByRole('list', { name: 'Categories by cost' })).getAllByRole('listitem');
|
|
16
|
+
expect(items.map((li) => li.textContent)).toEqual([
|
|
17
|
+
expect.stringContaining('Compute'),
|
|
18
|
+
expect.stringContaining('Tokens'),
|
|
19
|
+
expect.stringContaining('Add-ons'),
|
|
20
|
+
expect.stringContaining('Storage'),
|
|
21
|
+
]);
|
|
22
|
+
expect(items[0]).toHaveTextContent('45.7%');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Card, cn } from '@gntik-ai/ui';
|
|
2
|
+
import { CHART_COLORS, DonutChart, type ChartColor } from '@gntik-ai/charts';
|
|
3
|
+
import { formatMoney } from '../format';
|
|
4
|
+
import { sampleCosts } from './fixtures';
|
|
5
|
+
|
|
6
|
+
export interface CostItem {
|
|
7
|
+
label: string;
|
|
8
|
+
amount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CostBreakdownProps {
|
|
12
|
+
items?: CostItem[];
|
|
13
|
+
currency?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Caption under the total in the donut hole. */
|
|
16
|
+
centerLabel?: string;
|
|
17
|
+
/** Donut diameter in px. */
|
|
18
|
+
size?: number;
|
|
19
|
+
/** Heading level of the title, to fit the page outline (default h3). */
|
|
20
|
+
titleAs?: 'h2' | 'h3' | 'h4';
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Swatch classes matching the chart series tokens (categorical accents, never severity). */
|
|
25
|
+
const SWATCH: Record<ChartColor, string> = {
|
|
26
|
+
primary: 'bg-primary',
|
|
27
|
+
emerald: 'bg-brand-accent',
|
|
28
|
+
violet: 'bg-category-violet',
|
|
29
|
+
cyan: 'bg-category-cyan',
|
|
30
|
+
amber: 'bg-category-amber',
|
|
31
|
+
rose: 'bg-category-rose',
|
|
32
|
+
info: 'bg-info',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Where the spend goes: a donut by category plus the categories ranked by amount. */
|
|
36
|
+
export function CostBreakdown({
|
|
37
|
+
items = sampleCosts,
|
|
38
|
+
currency = 'USD',
|
|
39
|
+
title = 'Cost breakdown',
|
|
40
|
+
centerLabel = 'this cycle',
|
|
41
|
+
size = 176,
|
|
42
|
+
titleAs: TitleTag = 'h3',
|
|
43
|
+
className,
|
|
44
|
+
}: CostBreakdownProps) {
|
|
45
|
+
const ranked = [...items].sort((a, b) => b.amount - a.amount);
|
|
46
|
+
const total = ranked.reduce((sum, i) => sum + i.amount, 0);
|
|
47
|
+
const money = (n: number) => formatMoney(n, currency);
|
|
48
|
+
const colorAt = (i: number): ChartColor => CHART_COLORS[i % CHART_COLORS.length] ?? 'primary';
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Card className={cn('p-6', className)}>
|
|
52
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
53
|
+
<TitleTag className="text-[15px] font-semibold tracking-tight text-foreground">{title}</TitleTag>
|
|
54
|
+
<span className="font-mono text-[13px] font-semibold text-foreground tabular-nums">{money(total)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="mt-5 flex flex-col items-center gap-6 sm:flex-row sm:items-center">
|
|
57
|
+
<DonutChart
|
|
58
|
+
data={ranked}
|
|
59
|
+
index="label"
|
|
60
|
+
category="amount"
|
|
61
|
+
showLegend={false}
|
|
62
|
+
centerLabel={centerLabel}
|
|
63
|
+
valueFormatter={(n) => formatMoney(n, currency, true)}
|
|
64
|
+
height={size}
|
|
65
|
+
aria-label={`${title}: ${ranked.map((i) => `${i.label} ${money(i.amount)}`).join(', ')}`}
|
|
66
|
+
className="shrink-0"
|
|
67
|
+
/>
|
|
68
|
+
<ol aria-label="Categories by cost" className="grid w-full min-w-0 flex-1 gap-3">
|
|
69
|
+
{ranked.map((item, i) => {
|
|
70
|
+
const pct = total > 0 ? (item.amount / total) * 100 : 0;
|
|
71
|
+
return (
|
|
72
|
+
<li key={item.label} className="grid gap-1.5">
|
|
73
|
+
<div className="flex items-center justify-between gap-3">
|
|
74
|
+
<span className="flex min-w-0 items-center gap-2 text-[13px] text-foreground">
|
|
75
|
+
<span aria-hidden className="w-4 shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
|
|
76
|
+
{i + 1}
|
|
77
|
+
</span>
|
|
78
|
+
<span aria-hidden className={cn('size-2.5 shrink-0 rounded-[3px]', SWATCH[colorAt(i)])} />
|
|
79
|
+
<span className="truncate">{item.label}</span>
|
|
80
|
+
</span>
|
|
81
|
+
<span className="shrink-0 font-mono text-[12.5px] whitespace-nowrap text-muted-foreground tabular-nums">
|
|
82
|
+
<span className="font-semibold text-foreground">{money(item.amount)}</span> · {pct.toFixed(1)}%
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div aria-hidden className="h-1.5 overflow-hidden rounded-full bg-secondary">
|
|
86
|
+
<div className={cn('h-full rounded-full', SWATCH[colorAt(i)])} style={{ width: `${pct}%` }} />
|
|
87
|
+
</div>
|
|
88
|
+
</li>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</ol>
|
|
92
|
+
</div>
|
|
93
|
+
</Card>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Cost breakdown',
|
|
5
|
+
family: 'billing',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'Cost composition: a donut with the total in the hole and the categories ranked by amount with share bars, in categorical (non-severity) colours.',
|
|
9
|
+
uses: ['Card', 'DonutChart'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CostItem } from './CostBreakdown';
|
|
2
|
+
|
|
3
|
+
/** Sample cycle cost by category. */
|
|
4
|
+
export const sampleCosts: CostItem[] = [
|
|
5
|
+
{ label: 'Tokens', amount: 410 },
|
|
6
|
+
{ label: 'Compute', amount: 740 },
|
|
7
|
+
{ label: 'Add-ons', amount: 280 },
|
|
8
|
+
{ label: 'Storage', amount: 190 },
|
|
9
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { InvoiceTable } from './InvoiceTable';
|
|
5
|
+
import { sampleInvoices } from './fixtures';
|
|
6
|
+
|
|
7
|
+
describe('InvoiceTable', () => {
|
|
8
|
+
it('renders one row per invoice with status and amount', async () => {
|
|
9
|
+
const { container } = render(<InvoiceTable />);
|
|
10
|
+
const table = screen.getByRole('table', { name: 'Invoice history' });
|
|
11
|
+
expect(within(table).getAllByRole('row')).toHaveLength(sampleInvoices.length + 1);
|
|
12
|
+
expect(screen.getByText('$1,584.20')).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByText('Overdue')).toBeInTheDocument();
|
|
14
|
+
expect(screen.getByRole('link', { name: 'Download INV-2026-0004' })).toHaveAttribute(
|
|
15
|
+
'href',
|
|
16
|
+
'#invoice-INV-2026-0004',
|
|
17
|
+
);
|
|
18
|
+
await expectNoAxeViolations(container);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('calls onDownload with the invoice', async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const onDownload = vi.fn();
|
|
24
|
+
const invoices = [{ id: 'a', number: 'INV-1', date: '2026-01-01', amount: 10, status: 'paid' as const }];
|
|
25
|
+
render(<InvoiceTable invoices={invoices} onDownload={onDownload} />);
|
|
26
|
+
await user.click(screen.getByRole('button', { name: 'Download INV-1' }));
|
|
27
|
+
expect(onDownload).toHaveBeenCalledWith(invoices[0]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
Link,
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCaption,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
cn,
|
|
13
|
+
type BadgeTone,
|
|
14
|
+
} from '@gntik-ai/ui';
|
|
15
|
+
import { Download } from '@gntik-ai/icons';
|
|
16
|
+
import { formatMoney, formatShortDate, type DateInput } from '../format';
|
|
17
|
+
import { sampleInvoices } from './fixtures';
|
|
18
|
+
|
|
19
|
+
export type InvoiceStatus = 'paid' | 'open' | 'overdue' | 'void' | 'draft';
|
|
20
|
+
|
|
21
|
+
export interface Invoice {
|
|
22
|
+
id: string;
|
|
23
|
+
/** Human invoice number, e.g. "INV-2026-0004". */
|
|
24
|
+
number: string;
|
|
25
|
+
/** Issue date. */
|
|
26
|
+
date: DateInput;
|
|
27
|
+
amount: number;
|
|
28
|
+
/** ISO 4217 code. Default: the table's currency. */
|
|
29
|
+
currency?: string;
|
|
30
|
+
status: InvoiceStatus;
|
|
31
|
+
/** PDF URL. Without it the download control calls `onDownload` only. */
|
|
32
|
+
url?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface InvoiceTableProps {
|
|
36
|
+
invoices?: Invoice[];
|
|
37
|
+
/** Table caption (screen-reader only unless `showCaption`). */
|
|
38
|
+
caption?: string;
|
|
39
|
+
showCaption?: boolean;
|
|
40
|
+
currency?: string;
|
|
41
|
+
/** Called when a download control is activated. */
|
|
42
|
+
onDownload?: (invoice: Invoice) => void;
|
|
43
|
+
/** Row density; omitted, it follows the surrounding DensityProvider. */
|
|
44
|
+
density?: 'compact' | 'comfortable';
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STATUS: Record<InvoiceStatus, { label: string; tone: BadgeTone }> = {
|
|
49
|
+
paid: { label: 'Paid', tone: 'success' },
|
|
50
|
+
open: { label: 'Due', tone: 'warning' },
|
|
51
|
+
overdue: { label: 'Overdue', tone: 'destructive' },
|
|
52
|
+
void: { label: 'Void', tone: 'neutral' },
|
|
53
|
+
draft: { label: 'Draft', tone: 'neutral' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Invoice history: number, date, amount, status and a per-row PDF download. */
|
|
57
|
+
export function InvoiceTable({
|
|
58
|
+
invoices = sampleInvoices,
|
|
59
|
+
caption = 'Invoice history',
|
|
60
|
+
showCaption = false,
|
|
61
|
+
currency = 'USD',
|
|
62
|
+
onDownload,
|
|
63
|
+
density,
|
|
64
|
+
className,
|
|
65
|
+
}: InvoiceTableProps) {
|
|
66
|
+
return (
|
|
67
|
+
<div className={cn('overflow-hidden rounded-lg border border-border bg-card', className)}>
|
|
68
|
+
<Table density={density}>
|
|
69
|
+
<TableCaption srOnly={!showCaption}>{caption}</TableCaption>
|
|
70
|
+
<TableHeader>
|
|
71
|
+
<TableRow>
|
|
72
|
+
<TableHead>Invoice</TableHead>
|
|
73
|
+
<TableHead>Date</TableHead>
|
|
74
|
+
<TableHead>Status</TableHead>
|
|
75
|
+
<TableHead align="right">Amount</TableHead>
|
|
76
|
+
<TableHead align="right">
|
|
77
|
+
<span className="sr-only">Download</span>
|
|
78
|
+
</TableHead>
|
|
79
|
+
</TableRow>
|
|
80
|
+
</TableHeader>
|
|
81
|
+
<TableBody>
|
|
82
|
+
{invoices.map((inv) => {
|
|
83
|
+
const status = STATUS[inv.status];
|
|
84
|
+
const label = `Download ${inv.number}`;
|
|
85
|
+
return (
|
|
86
|
+
<TableRow key={inv.id}>
|
|
87
|
+
<TableCell className="font-mono text-[12.5px] font-medium text-foreground">{inv.number}</TableCell>
|
|
88
|
+
<TableCell className="whitespace-nowrap text-muted-foreground">{formatShortDate(inv.date)}</TableCell>
|
|
89
|
+
<TableCell>
|
|
90
|
+
<Badge tone={status.tone} dot>
|
|
91
|
+
{status.label}
|
|
92
|
+
</Badge>
|
|
93
|
+
</TableCell>
|
|
94
|
+
<TableCell align="right" className="font-mono tabular-nums text-foreground">
|
|
95
|
+
{formatMoney(inv.amount, inv.currency ?? currency)}
|
|
96
|
+
</TableCell>
|
|
97
|
+
<TableCell align="right">
|
|
98
|
+
{inv.url ? (
|
|
99
|
+
<Link
|
|
100
|
+
href={inv.url}
|
|
101
|
+
download
|
|
102
|
+
aria-label={label}
|
|
103
|
+
onClick={() => onDownload?.(inv)}
|
|
104
|
+
className="inline-flex items-center gap-1.5 text-[12.5px] font-semibold"
|
|
105
|
+
>
|
|
106
|
+
<Download size={14} aria-hidden />
|
|
107
|
+
PDF
|
|
108
|
+
</Link>
|
|
109
|
+
) : onDownload && inv.status !== 'void' && inv.status !== 'draft' ? (
|
|
110
|
+
<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="sm"
|
|
113
|
+
icon={Download}
|
|
114
|
+
aria-label={label}
|
|
115
|
+
onClick={() => onDownload(inv)}
|
|
116
|
+
>
|
|
117
|
+
PDF
|
|
118
|
+
</Button>
|
|
119
|
+
) : (
|
|
120
|
+
<span className="text-[12px] text-muted-foreground">—</span>
|
|
121
|
+
)}
|
|
122
|
+
</TableCell>
|
|
123
|
+
</TableRow>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</TableBody>
|
|
127
|
+
</Table>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Invoice table',
|
|
5
|
+
family: 'billing',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'Invoice history table with number, date, amount, a status badge (paid, due, overdue, void) and a per-row PDF download.',
|
|
9
|
+
uses: ['Table', 'Badge', 'Link', 'Button'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Invoice } from './InvoiceTable';
|
|
2
|
+
|
|
3
|
+
/** Sample invoice history. */
|
|
4
|
+
export const sampleInvoices: Invoice[] = [
|
|
5
|
+
{
|
|
6
|
+
id: 'inv_0526',
|
|
7
|
+
number: 'INV-2026-0005',
|
|
8
|
+
date: '2026-06-01',
|
|
9
|
+
amount: 1620,
|
|
10
|
+
status: 'open',
|
|
11
|
+
url: '#invoice-INV-2026-0005',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'inv_0426',
|
|
15
|
+
number: 'INV-2026-0004',
|
|
16
|
+
date: '2026-05-01',
|
|
17
|
+
amount: 1584.2,
|
|
18
|
+
status: 'paid',
|
|
19
|
+
url: '#invoice-INV-2026-0004',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'inv_0326',
|
|
23
|
+
number: 'INV-2026-0003',
|
|
24
|
+
date: '2026-04-01',
|
|
25
|
+
amount: 1498,
|
|
26
|
+
status: 'paid',
|
|
27
|
+
url: '#invoice-INV-2026-0003',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'inv_0226',
|
|
31
|
+
number: 'INV-2026-0002',
|
|
32
|
+
date: '2026-03-01',
|
|
33
|
+
amount: 1432.6,
|
|
34
|
+
status: 'overdue',
|
|
35
|
+
url: '#invoice-INV-2026-0002',
|
|
36
|
+
},
|
|
37
|
+
{ id: 'inv_0126', number: 'INV-2026-0001', date: '2026-02-01', amount: 1200, status: 'void' },
|
|
38
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { PaymentMethodCard } from './PaymentMethodCard';
|
|
5
|
+
|
|
6
|
+
describe('PaymentMethodCard', () => {
|
|
7
|
+
it('renders the masked card and expiry', async () => {
|
|
8
|
+
const { container } = render(<PaymentMethodCard />);
|
|
9
|
+
expect(screen.getByText(/Visa ending in/)).toHaveTextContent('Visa ending in 4242');
|
|
10
|
+
expect(screen.getByText('08/28')).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByText('Default')).toBeInTheDocument();
|
|
12
|
+
await expectNoAxeViolations(container);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows the expiry status and fires update', async () => {
|
|
16
|
+
const user = userEvent.setup();
|
|
17
|
+
const onUpdate = vi.fn();
|
|
18
|
+
render(
|
|
19
|
+
<PaymentMethodCard
|
|
20
|
+
method={{ brand: 'Mastercard', last4: '0005', expMonth: 1, expYear: 2026, status: 'expired' }}
|
|
21
|
+
onUpdate={onUpdate}
|
|
22
|
+
/>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByText('Expired')).toBeInTheDocument();
|
|
25
|
+
await user.click(screen.getByRole('button', { name: 'Update' }));
|
|
26
|
+
expect(onUpdate).toHaveBeenCalledOnce();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
CardAction,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
cn,
|
|
10
|
+
type BadgeTone,
|
|
11
|
+
} from '@gntik-ai/ui';
|
|
12
|
+
import { CreditCard } from '@gntik-ai/icons';
|
|
13
|
+
import { samplePaymentMethod } from './fixtures';
|
|
14
|
+
|
|
15
|
+
export type PaymentMethodStatus = 'valid' | 'expiring' | 'expired';
|
|
16
|
+
|
|
17
|
+
export interface PaymentMethod {
|
|
18
|
+
/** Card network as text, e.g. "Visa", "Mastercard", "Amex". */
|
|
19
|
+
brand: string;
|
|
20
|
+
last4: string;
|
|
21
|
+
expMonth: number;
|
|
22
|
+
expYear: number;
|
|
23
|
+
holder?: string;
|
|
24
|
+
isDefault?: boolean;
|
|
25
|
+
/** Computed by the app (the block never reads the clock). */
|
|
26
|
+
status?: PaymentMethodStatus;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PaymentMethodCardProps {
|
|
30
|
+
method?: PaymentMethod;
|
|
31
|
+
title?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
onUpdate?: () => void;
|
|
34
|
+
onRemove?: () => void;
|
|
35
|
+
updateLabel?: string;
|
|
36
|
+
/** Heading level of the title, to fit the page outline (default h3). */
|
|
37
|
+
titleAs?: 'h2' | 'h3' | 'h4';
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STATUS: Record<Exclude<PaymentMethodStatus, 'valid'>, { label: string; tone: BadgeTone }> = {
|
|
42
|
+
expiring: { label: 'Expires soon', tone: 'warning' },
|
|
43
|
+
expired: { label: 'Expired', tone: 'destructive' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The payment method on file: brand, masked number, expiry and an update action. No card artwork. */
|
|
47
|
+
export function PaymentMethodCard({
|
|
48
|
+
method = samplePaymentMethod,
|
|
49
|
+
title = 'Payment method',
|
|
50
|
+
description = 'Charged at the start of each billing cycle.',
|
|
51
|
+
onUpdate,
|
|
52
|
+
onRemove,
|
|
53
|
+
updateLabel = 'Update',
|
|
54
|
+
titleAs = 'h3',
|
|
55
|
+
className,
|
|
56
|
+
}: PaymentMethodCardProps) {
|
|
57
|
+
const expiry = `${String(method.expMonth).padStart(2, '0')}/${String(method.expYear).slice(-2)}`;
|
|
58
|
+
const status = method.status && method.status !== 'valid' ? STATUS[method.status] : undefined;
|
|
59
|
+
return (
|
|
60
|
+
<Card className={cn(className)}>
|
|
61
|
+
<CardHeader divided>
|
|
62
|
+
<CardTitle as={titleAs}>{title}</CardTitle>
|
|
63
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
64
|
+
<CardAction className="flex items-center gap-2">
|
|
65
|
+
{onRemove && (
|
|
66
|
+
<Button variant="ghost" size="sm" onClick={onRemove}>
|
|
67
|
+
Remove
|
|
68
|
+
</Button>
|
|
69
|
+
)}
|
|
70
|
+
<Button variant="secondary" size="sm" onClick={onUpdate}>
|
|
71
|
+
{updateLabel}
|
|
72
|
+
</Button>
|
|
73
|
+
</CardAction>
|
|
74
|
+
</CardHeader>
|
|
75
|
+
<div className="flex flex-wrap items-center gap-4 px-5 py-4">
|
|
76
|
+
<span
|
|
77
|
+
aria-hidden
|
|
78
|
+
className="grid h-10 w-14 shrink-0 place-items-center rounded-md border border-border bg-secondary font-mono text-[10.5px] font-semibold tracking-wide text-foreground uppercase"
|
|
79
|
+
>
|
|
80
|
+
{method.brand.slice(0, 4)}
|
|
81
|
+
</span>
|
|
82
|
+
<div className="min-w-0 flex-1">
|
|
83
|
+
<p className="flex flex-wrap items-center gap-2 text-[13.5px] font-medium text-foreground">
|
|
84
|
+
<span>
|
|
85
|
+
{method.brand} ending in <span className="font-mono tabular-nums">{method.last4}</span>
|
|
86
|
+
</span>
|
|
87
|
+
{method.isDefault && <Badge tone="neutral">Default</Badge>}
|
|
88
|
+
{status && (
|
|
89
|
+
<Badge tone={status.tone} dot>
|
|
90
|
+
{status.label}
|
|
91
|
+
</Badge>
|
|
92
|
+
)}
|
|
93
|
+
</p>
|
|
94
|
+
<p className="mt-0.5 flex items-center gap-1.5 text-[12px] text-muted-foreground">
|
|
95
|
+
<CreditCard size={13} aria-hidden className="shrink-0" />
|
|
96
|
+
Expires <span className="font-mono tabular-nums">{expiry}</span>
|
|
97
|
+
{method.holder && <> · {method.holder}</>}
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</Card>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Payment method card',
|
|
5
|
+
family: 'billing',
|
|
6
|
+
status: 'beta',
|
|
7
|
+
description:
|
|
8
|
+
'The card on file as text (brand, last four digits, expiry, holder) with default/expiry badges and update/remove actions; no card artwork.',
|
|
9
|
+
uses: ['Card', 'Badge', 'Button'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PaymentMethod } from './PaymentMethodCard';
|
|
2
|
+
|
|
3
|
+
/** Sample default payment method. */
|
|
4
|
+
export const samplePaymentMethod: PaymentMethod = {
|
|
5
|
+
brand: 'Visa',
|
|
6
|
+
last4: '4242',
|
|
7
|
+
expMonth: 8,
|
|
8
|
+
expYear: 2028,
|
|
9
|
+
holder: 'Alex Morgan',
|
|
10
|
+
isDefault: true,
|
|
11
|
+
status: 'valid',
|
|
12
|
+
};
|