@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,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Checkbox,
|
|
3
|
+
Table,
|
|
4
|
+
TableBody,
|
|
5
|
+
TableCaption,
|
|
6
|
+
TableCell,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableHeader,
|
|
9
|
+
TableRow,
|
|
10
|
+
cn,
|
|
11
|
+
} from '@gntik-ai/ui';
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
notificationChannels,
|
|
15
|
+
notificationDefaults,
|
|
16
|
+
notificationEvents,
|
|
17
|
+
type NotificationChannel,
|
|
18
|
+
type NotificationEvent,
|
|
19
|
+
type NotificationMatrixValue,
|
|
20
|
+
} from './fixtures';
|
|
21
|
+
|
|
22
|
+
export type { NotificationChannel, NotificationEvent, NotificationMatrixValue } from './fixtures';
|
|
23
|
+
|
|
24
|
+
export interface NotificationMatrixProps {
|
|
25
|
+
channels?: NotificationChannel[];
|
|
26
|
+
events?: NotificationEvent[];
|
|
27
|
+
/** Event id → enabled channel ids (controlled). */
|
|
28
|
+
value?: NotificationMatrixValue;
|
|
29
|
+
defaultValue?: NotificationMatrixValue;
|
|
30
|
+
onValueChange?: (value: NotificationMatrixValue) => void;
|
|
31
|
+
caption?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Event × channel grid of checkboxes. Column headers toggle a channel for every event, the last
|
|
37
|
+
* column toggles every channel for one event; both show the mixed state. Locked cells stay on.
|
|
38
|
+
*/
|
|
39
|
+
export function NotificationMatrix({
|
|
40
|
+
channels = notificationChannels,
|
|
41
|
+
events = notificationEvents,
|
|
42
|
+
value: valueProp,
|
|
43
|
+
defaultValue = notificationDefaults,
|
|
44
|
+
onValueChange,
|
|
45
|
+
caption = 'Notification preferences',
|
|
46
|
+
className,
|
|
47
|
+
}: NotificationMatrixProps) {
|
|
48
|
+
const [inner, setInner] = useState<NotificationMatrixValue>(defaultValue);
|
|
49
|
+
const value = valueProp ?? inner;
|
|
50
|
+
const isOn = (e: NotificationEvent, c: string) => !!e.locked?.includes(c) || !!value[e.id]?.includes(c);
|
|
51
|
+
|
|
52
|
+
const commit = (next: NotificationMatrixValue) => {
|
|
53
|
+
setInner(next);
|
|
54
|
+
onValueChange?.(next);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Sets the given (event, channel) cells, keeping locked ones on and the channel order stable. */
|
|
58
|
+
const setCells = (cells: Array<[NotificationEvent, string]>, on: boolean) => {
|
|
59
|
+
const next: NotificationMatrixValue = { ...value };
|
|
60
|
+
for (const [e, c] of cells) {
|
|
61
|
+
const current = new Set(next[e.id] ?? []);
|
|
62
|
+
if (on || e.locked?.includes(c)) current.add(c);
|
|
63
|
+
else current.delete(c);
|
|
64
|
+
next[e.id] = channels.map((ch) => ch.id).filter((id) => current.has(id));
|
|
65
|
+
}
|
|
66
|
+
commit(next);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const state = (cells: Array<[NotificationEvent, string]>) => {
|
|
70
|
+
const editable = cells.filter(([e, c]) => !e.locked?.includes(c));
|
|
71
|
+
const on = editable.filter(([e, c]) => isOn(e, c)).length;
|
|
72
|
+
return {
|
|
73
|
+
checked: editable.length > 0 && on === editable.length,
|
|
74
|
+
indeterminate: on > 0 && on < editable.length,
|
|
75
|
+
disabled: editable.length === 0,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Table className={cn('min-w-[520px]', className)}>
|
|
81
|
+
<TableCaption srOnly>{caption}</TableCaption>
|
|
82
|
+
<TableHeader>
|
|
83
|
+
<TableRow>
|
|
84
|
+
<TableHead>Event</TableHead>
|
|
85
|
+
{channels.map((c) => {
|
|
86
|
+
const cells = events.map((e): [NotificationEvent, string] => [e, c.id]);
|
|
87
|
+
const s = state(cells);
|
|
88
|
+
return (
|
|
89
|
+
<TableHead key={c.id} align="center" className="w-24">
|
|
90
|
+
<div className="flex flex-col items-center gap-1.5">
|
|
91
|
+
<span>{c.label}</span>
|
|
92
|
+
<Checkbox
|
|
93
|
+
aria-label={`All events by ${c.label}`}
|
|
94
|
+
{...s}
|
|
95
|
+
onCheckedChange={(on) => setCells(cells, on)}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
</TableHead>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
<TableHead align="center" className="w-20">
|
|
102
|
+
All
|
|
103
|
+
</TableHead>
|
|
104
|
+
</TableRow>
|
|
105
|
+
</TableHeader>
|
|
106
|
+
<TableBody>
|
|
107
|
+
{events.map((e) => {
|
|
108
|
+
const cells = channels.map((c): [NotificationEvent, string] => [e, c.id]);
|
|
109
|
+
const s = state(cells);
|
|
110
|
+
return (
|
|
111
|
+
<TableRow key={e.id}>
|
|
112
|
+
<TableCell>
|
|
113
|
+
<div className="text-[13px] font-medium text-foreground">{e.label}</div>
|
|
114
|
+
{e.description && (
|
|
115
|
+
<div className="mt-0.5 text-[12px] leading-5 text-muted-foreground">{e.description}</div>
|
|
116
|
+
)}
|
|
117
|
+
</TableCell>
|
|
118
|
+
{channels.map((c) => {
|
|
119
|
+
const locked = !!e.locked?.includes(c.id);
|
|
120
|
+
return (
|
|
121
|
+
<TableCell key={c.id} align="center">
|
|
122
|
+
<Checkbox
|
|
123
|
+
aria-label={`${e.label} by ${c.label}${locked ? ' (always on)' : ''}`}
|
|
124
|
+
checked={isOn(e, c.id)}
|
|
125
|
+
disabled={locked}
|
|
126
|
+
onCheckedChange={(on) => setCells([[e, c.id]], on)}
|
|
127
|
+
/>
|
|
128
|
+
</TableCell>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
<TableCell align="center">
|
|
132
|
+
<Checkbox
|
|
133
|
+
aria-label={`All channels for ${e.label}`}
|
|
134
|
+
{...s}
|
|
135
|
+
onCheckedChange={(on) => setCells(cells, on)}
|
|
136
|
+
/>
|
|
137
|
+
</TableCell>
|
|
138
|
+
</TableRow>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</TableBody>
|
|
142
|
+
</Table>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Notification matrix',
|
|
5
|
+
family: 'team',
|
|
6
|
+
status: 'stable',
|
|
7
|
+
description: 'Event × channel table of checkboxes with column and row toggles (mixed state) and always-on cells.',
|
|
8
|
+
uses: ['Table', 'Checkbox'],
|
|
9
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface NotificationChannel {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface NotificationEvent {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Channels that are always on for this event (shown checked and disabled). */
|
|
11
|
+
locked?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type NotificationMatrixValue = Record<string, string[]>;
|
|
15
|
+
|
|
16
|
+
export const notificationChannels: NotificationChannel[] = [
|
|
17
|
+
{ id: 'email', label: 'Email' },
|
|
18
|
+
{ id: 'in-app', label: 'In-app' },
|
|
19
|
+
{ id: 'push', label: 'Push' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const notificationEvents: NotificationEvent[] = [
|
|
23
|
+
{ id: 'deploy-failed', label: 'Deployment failed', description: 'A deployment you own fails or is rolled back.' },
|
|
24
|
+
{ id: 'deploy-succeeded', label: 'Deployment succeeded', description: 'A deployment you started goes live.' },
|
|
25
|
+
{ id: 'mentions', label: 'Mentions', description: 'Someone mentions you in a comment or review.' },
|
|
26
|
+
{ id: 'member-joined', label: 'Member joined', description: 'Someone accepts an invitation to the workspace.' },
|
|
27
|
+
{ id: 'usage', label: 'Usage threshold', description: 'Usage reaches 80% or 100% of a quota.' },
|
|
28
|
+
{
|
|
29
|
+
id: 'security',
|
|
30
|
+
label: 'Security alerts',
|
|
31
|
+
description: 'New sign-ins and changes to keys or roles.',
|
|
32
|
+
locked: ['email'],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const notificationDefaults: NotificationMatrixValue = {
|
|
37
|
+
'deploy-failed': ['email', 'in-app', 'push'],
|
|
38
|
+
'deploy-succeeded': ['in-app'],
|
|
39
|
+
mentions: ['email', 'in-app'],
|
|
40
|
+
'member-joined': [],
|
|
41
|
+
usage: ['email'],
|
|
42
|
+
security: ['email', 'in-app'],
|
|
43
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { PendingInvitations } from './PendingInvitations';
|
|
5
|
+
|
|
6
|
+
describe('PendingInvitations', () => {
|
|
7
|
+
it('renders the invitations', async () => {
|
|
8
|
+
const { container } = render(<PendingInvitations />);
|
|
9
|
+
expect(screen.getByRole('region', { name: /Pending invitations/ })).toBeInTheDocument();
|
|
10
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(3);
|
|
11
|
+
expect(screen.getByText('Expired')).toBeInTheDocument();
|
|
12
|
+
await expectNoAxeViolations(container);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('resends and revokes', async () => {
|
|
16
|
+
const onResend = vi.fn();
|
|
17
|
+
const onRevoke = vi.fn();
|
|
18
|
+
const { container } = render(<PendingInvitations onResend={onResend} onRevoke={onRevoke} />);
|
|
19
|
+
await userEvent.click(screen.getByRole('button', { name: 'Resend invitation to taylor@example.com' }));
|
|
20
|
+
expect(onResend).toHaveBeenCalledWith(expect.objectContaining({ id: 'i1' }));
|
|
21
|
+
expect(screen.getByRole('button', { name: 'Resend invitation to taylor@example.com' })).toHaveTextContent('Sent');
|
|
22
|
+
expect(screen.getByRole('status')).toHaveTextContent('Invitation resent to taylor@example.com.');
|
|
23
|
+
for (const email of ['taylor', 'casey', 'drew']) {
|
|
24
|
+
await userEvent.click(screen.getByRole('button', { name: `Revoke invitation to ${email}@example.com` }));
|
|
25
|
+
}
|
|
26
|
+
expect(onRevoke).toHaveBeenCalledTimes(3);
|
|
27
|
+
expect(screen.getByRole('heading', { name: 'No pending invitations' })).toBeInTheDocument();
|
|
28
|
+
await expectNoAxeViolations(container);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Badge, Button, EmptyState, StatusTag, Timestamp, cn } from '@gntik-ai/ui';
|
|
2
|
+
import { Check, Mail, RotateCw, X } from '@gntik-ai/icons';
|
|
3
|
+
import { useId, useState, type ReactNode } from 'react';
|
|
4
|
+
import { invitations as defaultInvitations, type Invitation } from './fixtures';
|
|
5
|
+
|
|
6
|
+
export type { Invitation } from './fixtures';
|
|
7
|
+
|
|
8
|
+
export interface PendingInvitationsProps {
|
|
9
|
+
invitations?: Invitation[];
|
|
10
|
+
/** Resends one invitation. A returned promise shows a loading state. */
|
|
11
|
+
onResend?: (invitation: Invitation) => void | Promise<void>;
|
|
12
|
+
onRevoke?: (invitation: Invitation) => void;
|
|
13
|
+
title?: ReactNode;
|
|
14
|
+
headingLevel?: 'h2' | 'h3' | 'h4';
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Pending invitations with who sent them, expiry, and resend / revoke actions. */
|
|
19
|
+
export function PendingInvitations({
|
|
20
|
+
invitations = defaultInvitations,
|
|
21
|
+
onResend,
|
|
22
|
+
onRevoke,
|
|
23
|
+
title = 'Pending invitations',
|
|
24
|
+
headingLevel: Heading = 'h2',
|
|
25
|
+
className,
|
|
26
|
+
}: PendingInvitationsProps) {
|
|
27
|
+
const id = useId();
|
|
28
|
+
const [revoked, setRevoked] = useState<string[]>([]);
|
|
29
|
+
const [sending, setSending] = useState<string | null>(null);
|
|
30
|
+
const [sent, setSent] = useState<string[]>([]);
|
|
31
|
+
const [announcement, setAnnouncement] = useState('');
|
|
32
|
+
const [now] = useState(() => Date.now());
|
|
33
|
+
const rows = invitations.filter((i) => !revoked.includes(i.id));
|
|
34
|
+
|
|
35
|
+
const resend = async (inv: Invitation) => {
|
|
36
|
+
setSending(inv.id);
|
|
37
|
+
try {
|
|
38
|
+
await onResend?.(inv);
|
|
39
|
+
setSent((s) => [...s, inv.id]);
|
|
40
|
+
setAnnouncement(`Invitation resent to ${inv.email}.`);
|
|
41
|
+
} catch {
|
|
42
|
+
setAnnouncement(`Could not resend the invitation to ${inv.email}.`);
|
|
43
|
+
}
|
|
44
|
+
setSending(null);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<section
|
|
49
|
+
aria-labelledby={`${id}-title`}
|
|
50
|
+
className={cn('rounded-lg border border-border bg-card shadow-sm', className)}
|
|
51
|
+
>
|
|
52
|
+
<div className="flex items-center gap-2 border-b border-border px-5 py-3.5">
|
|
53
|
+
<Heading id={`${id}-title`} className="text-[14px] font-semibold tracking-tight text-foreground">
|
|
54
|
+
{title}
|
|
55
|
+
</Heading>
|
|
56
|
+
<Badge size="sm">{rows.length}</Badge>
|
|
57
|
+
</div>
|
|
58
|
+
{rows.length === 0 ? (
|
|
59
|
+
<EmptyState
|
|
60
|
+
size="sm"
|
|
61
|
+
icon={Mail}
|
|
62
|
+
title="No pending invitations"
|
|
63
|
+
description="Invitations you send appear here until they are accepted."
|
|
64
|
+
className="mx-auto"
|
|
65
|
+
/>
|
|
66
|
+
) : (
|
|
67
|
+
<ul className="divide-y divide-border">
|
|
68
|
+
{rows.map((inv) => {
|
|
69
|
+
const expired = inv.expiresAt != null && new Date(inv.expiresAt).getTime() < now;
|
|
70
|
+
const wasSent = sent.includes(inv.id);
|
|
71
|
+
return (
|
|
72
|
+
<li key={inv.id} className="flex flex-col gap-3 px-5 py-3.5 sm:flex-row sm:items-center sm:gap-4">
|
|
73
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
74
|
+
<span className="grid size-9 shrink-0 place-items-center rounded-full bg-secondary text-muted-foreground">
|
|
75
|
+
<Mail size={16} aria-hidden />
|
|
76
|
+
</span>
|
|
77
|
+
<div className="min-w-0">
|
|
78
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
79
|
+
<span className="truncate text-[13px] font-medium text-foreground">{inv.email}</span>
|
|
80
|
+
<Badge size="sm">{inv.role}</Badge>
|
|
81
|
+
{expired && !wasSent && (
|
|
82
|
+
<StatusTag
|
|
83
|
+
status="expired"
|
|
84
|
+
label="Expired"
|
|
85
|
+
statuses={{ expired: { label: 'Expired', tone: 'warning' } }}
|
|
86
|
+
size="sm"
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="mt-0.5 text-[12px] text-muted-foreground">
|
|
91
|
+
{inv.invitedBy ? `Invited by ${inv.invitedBy} · ` : 'Invited '}
|
|
92
|
+
<Timestamp value={inv.invitedAt} tooltip={false} />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex shrink-0 items-center gap-1.5 ps-12 sm:ps-0">
|
|
97
|
+
<Button
|
|
98
|
+
variant="secondary"
|
|
99
|
+
size="sm"
|
|
100
|
+
icon={wasSent ? Check : RotateCw}
|
|
101
|
+
loading={sending === inv.id}
|
|
102
|
+
disabled={wasSent}
|
|
103
|
+
aria-label={`Resend invitation to ${inv.email}`}
|
|
104
|
+
onClick={() => void resend(inv)}
|
|
105
|
+
>
|
|
106
|
+
{wasSent ? 'Sent' : 'Resend'}
|
|
107
|
+
</Button>
|
|
108
|
+
<Button
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="sm"
|
|
111
|
+
icon={X}
|
|
112
|
+
aria-label={`Revoke invitation to ${inv.email}`}
|
|
113
|
+
onClick={() => {
|
|
114
|
+
setRevoked((r) => [...r, inv.id]);
|
|
115
|
+
setAnnouncement(`Invitation to ${inv.email} revoked.`);
|
|
116
|
+
onRevoke?.(inv);
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
Revoke
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
</li>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</ul>
|
|
126
|
+
)}
|
|
127
|
+
<span role="status" className="sr-only">
|
|
128
|
+
{announcement}
|
|
129
|
+
</span>
|
|
130
|
+
</section>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Pending invitations',
|
|
5
|
+
family: 'team',
|
|
6
|
+
status: 'stable',
|
|
7
|
+
description: 'List of pending invitations with role, sender, age and expiry, and resend / revoke actions.',
|
|
8
|
+
uses: ['Badge', 'StatusTag', 'Timestamp', 'Button', 'EmptyState'],
|
|
9
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const daysFromNow = (d: number) => new Date(Date.now() + d * 86_400_000).toISOString();
|
|
2
|
+
|
|
3
|
+
export interface Invitation {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
role: string;
|
|
7
|
+
invitedBy?: string;
|
|
8
|
+
/** ISO date the invitation was sent. */
|
|
9
|
+
invitedAt: string;
|
|
10
|
+
/** ISO date it expires. */
|
|
11
|
+
expiresAt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const invitations: Invitation[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'i1',
|
|
17
|
+
email: 'taylor@example.com',
|
|
18
|
+
role: 'Member',
|
|
19
|
+
invitedBy: 'Avery Collins',
|
|
20
|
+
invitedAt: daysFromNow(-1),
|
|
21
|
+
expiresAt: daysFromNow(6),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'i2',
|
|
25
|
+
email: 'casey@example.com',
|
|
26
|
+
role: 'Admin',
|
|
27
|
+
invitedBy: 'Sam Rivera',
|
|
28
|
+
invitedAt: daysFromNow(-4),
|
|
29
|
+
expiresAt: daysFromNow(3),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'i3',
|
|
33
|
+
email: 'drew@example.com',
|
|
34
|
+
role: 'Viewer',
|
|
35
|
+
invitedBy: 'Avery Collins',
|
|
36
|
+
invitedAt: daysFromNow(-9),
|
|
37
|
+
expiresAt: daysFromNow(-2),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { ProfileHeader } from './ProfileHeader';
|
|
5
|
+
|
|
6
|
+
describe('ProfileHeader', () => {
|
|
7
|
+
it('renders the profile', async () => {
|
|
8
|
+
const { container } = render(<ProfileHeader />);
|
|
9
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Avery Collins' })).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole('img', { name: 'Avery Collins' })).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByRole('list', { name: 'Details' })).toHaveTextContent('avery@example.com');
|
|
12
|
+
await expectNoAxeViolations(container);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('edits and changes the avatar', async () => {
|
|
16
|
+
const onEdit = vi.fn();
|
|
17
|
+
const onAvatarChange = vi.fn();
|
|
18
|
+
const { container } = render(<ProfileHeader onEdit={onEdit} onAvatarChange={onAvatarChange} />);
|
|
19
|
+
await userEvent.click(screen.getByRole('button', { name: 'Edit profile' }));
|
|
20
|
+
expect(onEdit).toHaveBeenCalledOnce();
|
|
21
|
+
const input = container.querySelector('input[type="file"]') as HTMLInputElement;
|
|
22
|
+
const click = vi.spyOn(input, 'click');
|
|
23
|
+
await userEvent.click(screen.getByRole('button', { name: 'Change avatar' }));
|
|
24
|
+
expect(click).toHaveBeenCalled();
|
|
25
|
+
const file = new File(['x'], 'me.png', { type: 'image/png' });
|
|
26
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
27
|
+
expect(onAvatarChange).toHaveBeenCalledWith(file);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Avatar, Badge, Button, IconButton, cn } from '@gntik-ai/ui';
|
|
2
|
+
import { Camera, Pencil } from '@gntik-ai/icons';
|
|
3
|
+
import { useRef, useState, type ReactNode } from 'react';
|
|
4
|
+
import { profile as defaultProfile, type Profile } from './fixtures';
|
|
5
|
+
|
|
6
|
+
export type { Profile, ProfileMetaItem } from './fixtures';
|
|
7
|
+
|
|
8
|
+
export interface ProfileHeaderProps {
|
|
9
|
+
profile?: Profile;
|
|
10
|
+
/** Called with the picked image; the avatar previews it right away. `null` hides the change action. */
|
|
11
|
+
onAvatarChange?: ((file: File) => void) | null;
|
|
12
|
+
/** Called by the Edit profile button. `null` hides it. */
|
|
13
|
+
onEdit?: (() => void) | null;
|
|
14
|
+
/** Extra actions next to Edit. */
|
|
15
|
+
actions?: ReactNode;
|
|
16
|
+
headingLevel?: 'h1' | 'h2';
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Profile header: large avatar with a change action, name, role, headline, meta row and edit button. */
|
|
21
|
+
export function ProfileHeader({
|
|
22
|
+
profile = defaultProfile,
|
|
23
|
+
onAvatarChange = () => {},
|
|
24
|
+
onEdit = () => {},
|
|
25
|
+
actions,
|
|
26
|
+
headingLevel: Heading = 'h1',
|
|
27
|
+
className,
|
|
28
|
+
}: ProfileHeaderProps) {
|
|
29
|
+
const fileRef = useRef<HTMLInputElement | null>(null);
|
|
30
|
+
const [preview, setPreview] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
const pick = (file: File) => {
|
|
33
|
+
onAvatarChange?.(file);
|
|
34
|
+
try {
|
|
35
|
+
const url = URL.createObjectURL(file);
|
|
36
|
+
if (preview) URL.revokeObjectURL(preview);
|
|
37
|
+
setPreview(url);
|
|
38
|
+
} catch {
|
|
39
|
+
// No object URLs (e.g. server or test environments): keep the current image.
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('flex flex-col gap-5 sm:flex-row sm:items-center sm:gap-6', className)}>
|
|
45
|
+
<div className="relative w-fit shrink-0">
|
|
46
|
+
<Avatar size="xl" name={profile.name} src={preview ?? profile.avatarUrl} />
|
|
47
|
+
{onAvatarChange && (
|
|
48
|
+
<>
|
|
49
|
+
<IconButton
|
|
50
|
+
icon={Camera}
|
|
51
|
+
label="Change avatar"
|
|
52
|
+
size="sm"
|
|
53
|
+
variant="secondary"
|
|
54
|
+
className="absolute -end-1 -bottom-1 rounded-full"
|
|
55
|
+
onClick={() => fileRef.current?.click()}
|
|
56
|
+
/>
|
|
57
|
+
<input
|
|
58
|
+
ref={fileRef}
|
|
59
|
+
type="file"
|
|
60
|
+
accept="image/*"
|
|
61
|
+
hidden
|
|
62
|
+
onChange={(e) => {
|
|
63
|
+
const file = e.target.files?.[0];
|
|
64
|
+
if (file) pick(file);
|
|
65
|
+
e.target.value = '';
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
<div className="min-w-0 flex-1">
|
|
72
|
+
<div className="flex flex-wrap items-center gap-2.5">
|
|
73
|
+
<Heading className="truncate text-[22px] font-semibold tracking-tight text-foreground">
|
|
74
|
+
{profile.name}
|
|
75
|
+
</Heading>
|
|
76
|
+
{profile.role && (
|
|
77
|
+
<Badge tone="primary" size="lg">
|
|
78
|
+
{profile.role}
|
|
79
|
+
</Badge>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
{profile.headline && <p className="mt-1 text-[13.5px] text-muted-foreground">{profile.headline}</p>}
|
|
83
|
+
{profile.meta && profile.meta.length > 0 && (
|
|
84
|
+
<ul aria-label="Details" className="mt-3 flex flex-wrap gap-x-5 gap-y-1.5">
|
|
85
|
+
{profile.meta.map(({ icon: MetaIcon, label }) => (
|
|
86
|
+
<li key={label} className="flex items-center gap-1.5 text-[12.5px] text-muted-foreground">
|
|
87
|
+
{MetaIcon && <MetaIcon size={14} aria-hidden />}
|
|
88
|
+
{label}
|
|
89
|
+
</li>
|
|
90
|
+
))}
|
|
91
|
+
</ul>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
{(onEdit || actions) && (
|
|
95
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
96
|
+
{actions}
|
|
97
|
+
{onEdit && (
|
|
98
|
+
<Button variant="secondary" icon={Pencil} onClick={onEdit}>
|
|
99
|
+
Edit profile
|
|
100
|
+
</Button>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockMeta } from '../../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: BlockMeta = {
|
|
4
|
+
name: 'Profile header',
|
|
5
|
+
family: 'team',
|
|
6
|
+
status: 'stable',
|
|
7
|
+
description:
|
|
8
|
+
'Profile header with a large avatar and change action, name, role badge, headline, meta row and edit button.',
|
|
9
|
+
uses: ['Avatar', 'Badge', 'IconButton', 'Button'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Building2, CalendarDays, Mail, MapPin, type LucideIcon } from '@gntik-ai/icons';
|
|
2
|
+
|
|
3
|
+
export interface ProfileMetaItem {
|
|
4
|
+
icon?: LucideIcon;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Profile {
|
|
9
|
+
name: string;
|
|
10
|
+
/** Role or title shown as a badge. */
|
|
11
|
+
role?: string;
|
|
12
|
+
/** One-line headline under the name. */
|
|
13
|
+
headline?: string;
|
|
14
|
+
avatarUrl?: string;
|
|
15
|
+
meta?: ProfileMetaItem[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const profile: Profile = {
|
|
19
|
+
name: 'Avery Collins',
|
|
20
|
+
role: 'Owner',
|
|
21
|
+
headline: 'Platform lead · keeps deployments boring',
|
|
22
|
+
meta: [
|
|
23
|
+
{ icon: Mail, label: 'avery@example.com' },
|
|
24
|
+
{ icon: Building2, label: 'Platform team' },
|
|
25
|
+
{ icon: MapPin, label: 'Lisbon, Portugal' },
|
|
26
|
+
{ icon: CalendarDays, label: 'Joined March 2024' },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../../test/a11y';
|
|
4
|
+
import { SessionsDevices } from './SessionsDevices';
|
|
5
|
+
|
|
6
|
+
describe('SessionsDevices', () => {
|
|
7
|
+
it('renders the sessions with the current one marked', async () => {
|
|
8
|
+
const { container } = render(<SessionsDevices />);
|
|
9
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(4);
|
|
10
|
+
expect(screen.getByText('This device')).toBeInTheDocument();
|
|
11
|
+
expect(screen.queryByRole('button', { name: 'Revoke session on MacBook Pro' })).not.toBeInTheDocument();
|
|
12
|
+
await expectNoAxeViolations(container);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('revokes one session, then all others', async () => {
|
|
16
|
+
const onRevoke = vi.fn();
|
|
17
|
+
const onRevokeOthers = vi.fn();
|
|
18
|
+
render(<SessionsDevices onRevoke={onRevoke} onRevokeOthers={onRevokeOthers} />);
|
|
19
|
+
await userEvent.click(screen.getByRole('button', { name: 'Revoke session on Pixel 8' }));
|
|
20
|
+
expect(onRevoke).toHaveBeenCalledWith(expect.objectContaining({ id: 's2' }));
|
|
21
|
+
expect(screen.getByRole('status')).toHaveTextContent('Signed out of Pixel 8.');
|
|
22
|
+
await userEvent.click(screen.getByRole('button', { name: 'Sign out other sessions' }));
|
|
23
|
+
expect(onRevokeOthers).toHaveBeenCalledWith([
|
|
24
|
+
expect.objectContaining({ id: 's3' }),
|
|
25
|
+
expect.objectContaining({ id: 's4' }),
|
|
26
|
+
]);
|
|
27
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(1);
|
|
28
|
+
expect(screen.getByRole('button', { name: 'Sign out other sessions' })).toBeDisabled();
|
|
29
|
+
});
|
|
30
|
+
});
|