@gntik-ai/blocks 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3494 -0
- package/dist/index.js +14911 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -3
- package/src/ai/AssistantCardGrid/AssistantCardGrid.test.tsx +32 -0
- package/src/ai/AssistantCardGrid/AssistantCardGrid.tsx +98 -0
- package/src/ai/AssistantCardGrid/block.meta.ts +10 -0
- package/src/ai/AssistantCardGrid/fixtures.ts +72 -0
- package/src/ai/EvaluationScorecard/EvaluationScorecard.test.tsx +39 -0
- package/src/ai/EvaluationScorecard/EvaluationScorecard.tsx +233 -0
- package/src/ai/EvaluationScorecard/block.meta.ts +10 -0
- package/src/ai/EvaluationScorecard/fixtures.ts +79 -0
- package/src/ai/ModelKeysList/ModelKeysList.test.tsx +44 -0
- package/src/ai/ModelKeysList/ModelKeysList.tsx +203 -0
- package/src/ai/ModelKeysList/block.meta.ts +10 -0
- package/src/ai/ModelKeysList/fixtures.ts +44 -0
- package/src/ai/PromptEditor/PromptEditor.test.tsx +97 -0
- package/src/ai/PromptEditor/PromptEditor.tsx +202 -0
- package/src/ai/PromptEditor/block.meta.ts +10 -0
- package/src/ai/PromptEditor/examples/controlled-variables.tsx +23 -0
- package/src/ai/PromptEditor/fixtures.ts +12 -0
- package/src/ai/TokenCostCard/TokenCostCard.test.tsx +36 -0
- package/src/ai/TokenCostCard/TokenCostCard.tsx +130 -0
- package/src/ai/TokenCostCard/block.meta.ts +9 -0
- package/src/ai/TokenCostCard/fixtures.ts +18 -0
- package/src/ai/TraceWaterfall/TraceWaterfall.test.tsx +119 -0
- package/src/ai/TraceWaterfall/TraceWaterfall.tsx +317 -0
- package/src/ai/TraceWaterfall/block.meta.ts +10 -0
- package/src/ai/TraceWaterfall/examples/no-details.tsx +6 -0
- package/src/ai/TraceWaterfall/examples/running.tsx +16 -0
- package/src/ai/TraceWaterfall/fixtures.ts +133 -0
- package/src/ai/TraceWaterfall/span-details.tsx +51 -0
- package/src/ai/index.ts +13 -0
- package/src/auth/ForgotPasswordForm/ForgotPasswordForm.test.tsx +209 -0
- package/src/auth/ForgotPasswordForm/ForgotPasswordForm.tsx +211 -0
- package/src/auth/ForgotPasswordForm/block.meta.ts +10 -0
- package/src/auth/ForgotPasswordForm/examples/UsernameOrEmail.tsx +24 -0
- package/src/auth/MfaChallenge/MfaChallenge.test.tsx +35 -0
- package/src/auth/MfaChallenge/MfaChallenge.tsx +170 -0
- package/src/auth/MfaChallenge/block.meta.ts +9 -0
- package/src/auth/ResetPasswordForm/ResetPasswordForm.test.tsx +27 -0
- package/src/auth/ResetPasswordForm/ResetPasswordForm.tsx +100 -0
- package/src/auth/ResetPasswordForm/block.meta.ts +10 -0
- package/src/auth/SignInForm/SignInForm.test.tsx +222 -0
- package/src/auth/SignInForm/SignInForm.tsx +226 -0
- package/src/auth/SignInForm/block.meta.ts +22 -0
- package/src/auth/SignInForm/examples/Username.tsx +26 -0
- package/src/auth/SignUpForm/SignUpForm.test.tsx +33 -0
- package/src/auth/SignUpForm/SignUpForm.tsx +154 -0
- package/src/auth/SignUpForm/block.meta.ts +9 -0
- package/src/auth/SsoButtons/SsoButtons.test.tsx +23 -0
- package/src/auth/SsoButtons/SsoButtons.tsx +52 -0
- package/src/auth/SsoButtons/block.meta.ts +9 -0
- package/src/auth/SsoButtons/fixtures.ts +15 -0
- package/src/auth/index.ts +15 -0
- package/src/auth/shared.tsx +113 -0
- package/src/billing/CostBreakdown/CostBreakdown.test.tsx +24 -0
- package/src/billing/CostBreakdown/CostBreakdown.tsx +95 -0
- package/src/billing/CostBreakdown/block.meta.ts +10 -0
- package/src/billing/CostBreakdown/fixtures.ts +9 -0
- package/src/billing/InvoiceTable/InvoiceTable.test.tsx +29 -0
- package/src/billing/InvoiceTable/InvoiceTable.tsx +130 -0
- package/src/billing/InvoiceTable/block.meta.ts +10 -0
- package/src/billing/InvoiceTable/fixtures.ts +38 -0
- package/src/billing/PaymentMethodCard/PaymentMethodCard.test.tsx +28 -0
- package/src/billing/PaymentMethodCard/PaymentMethodCard.tsx +103 -0
- package/src/billing/PaymentMethodCard/block.meta.ts +10 -0
- package/src/billing/PaymentMethodCard/fixtures.ts +12 -0
- package/src/billing/PlanCard/PlanCard.baseline.test.tsx +26 -0
- package/src/billing/PlanCard/PlanCard.summary.test.tsx +227 -0
- package/src/billing/PlanCard/PlanCard.test.tsx +27 -0
- package/src/billing/PlanCard/PlanCard.tsx +140 -0
- package/src/billing/PlanCard/PlanSummary.tsx +101 -0
- package/src/billing/PlanCard/README.md +23 -0
- package/src/billing/PlanCard/__snapshots__/PlanCard.baseline.test.tsx.snap +5 -0
- package/src/billing/PlanCard/block.meta.ts +60 -0
- package/src/billing/PlanCard/examples/SummaryActionsOnly.tsx +12 -0
- package/src/billing/PlanCard/examples/SummaryFeaturesOnly.tsx +14 -0
- package/src/billing/PlanCard/examples/SummaryFull.tsx +28 -0
- package/src/billing/PlanCard/examples/SummaryMinimal.tsx +5 -0
- package/src/billing/PlanCard/examples/SummaryPriceOnly.tsx +15 -0
- package/src/billing/PlanCard/examples/SummaryRenewalOnly.tsx +14 -0
- package/src/billing/PlanCard/examples/SummaryUnassigned.tsx +5 -0
- package/src/billing/PlanCard/examples/SummaryUnassignedNoLabel.tsx +5 -0
- package/src/billing/PlanCard/examples/SummaryZeroPrice.tsx +15 -0
- package/src/billing/PlanCard/fixtures.ts +19 -0
- package/src/billing/PlanDefinitionCard/PlanDefinitionCard.test.tsx +111 -0
- package/src/billing/PlanDefinitionCard/PlanDefinitionCard.tsx +86 -0
- package/src/billing/PlanDefinitionCard/README.md +29 -0
- package/src/billing/PlanDefinitionCard/block.meta.ts +46 -0
- package/src/billing/PlanDefinitionCard/examples/Compact.tsx +23 -0
- package/src/billing/PlanDefinitionCard/examples/Full.tsx +23 -0
- package/src/billing/PlanDefinitionCard/fixtures.ts +12 -0
- package/src/billing/QuotaMeters/QuotaMeters.test.tsx +23 -0
- package/src/billing/QuotaMeters/QuotaMeters.tsx +92 -0
- package/src/billing/QuotaMeters/block.meta.ts +10 -0
- package/src/billing/QuotaMeters/fixtures.ts +9 -0
- package/src/billing/SpendVsBudget/SpendVsBudget.test.tsx +24 -0
- package/src/billing/SpendVsBudget/SpendVsBudget.tsx +105 -0
- package/src/billing/SpendVsBudget/block.meta.ts +10 -0
- package/src/billing/SpendVsBudget/fixtures.ts +18 -0
- package/src/billing/format.ts +42 -0
- package/src/billing/index.ts +21 -0
- package/src/builders/CodePanel/CodePanel.test.tsx +280 -0
- package/src/builders/CodePanel/CodePanel.tsx +239 -0
- package/src/builders/CodePanel/block.meta.ts +10 -0
- package/src/builders/CodePanel/examples/controlled.tsx +15 -0
- package/src/builders/CodePanel/examples/diagnostics.tsx +40 -0
- package/src/builders/CodePanel/fixtures.ts +95 -0
- package/src/builders/CodePanel/marker-store.ts +90 -0
- package/src/builders/CodePanel/markers.ts +62 -0
- package/src/builders/FlowBuilder/FlowBuilder.test.tsx +480 -0
- package/src/builders/FlowBuilder/FlowBuilder.tsx +343 -0
- package/src/builders/FlowBuilder/block.meta.ts +20 -0
- package/src/builders/FlowBuilder/connections.ts +67 -0
- package/src/builders/FlowBuilder/decorated-nodes.ts +80 -0
- package/src/builders/FlowBuilder/examples/extensions.tsx +120 -0
- package/src/builders/FlowBuilder/extensions.ts +37 -0
- package/src/builders/FlowBuilder/fixtures.ts +80 -0
- package/src/builders/FlowBuilder/parts.tsx +196 -0
- package/src/builders/FlowBuilder/problems.tsx +40 -0
- package/src/builders/LogViewer/LogViewer.test.tsx +97 -0
- package/src/builders/LogViewer/LogViewer.tsx +253 -0
- package/src/builders/LogViewer/block.meta.ts +10 -0
- package/src/builders/LogViewer/fixtures.ts +49 -0
- package/src/builders/index.ts +15 -0
- package/src/data-display/ActivityFeed/ActivityFeed.test.tsx +62 -0
- package/src/data-display/ActivityFeed/ActivityFeed.tsx +118 -0
- package/src/data-display/ActivityFeed/block.meta.ts +10 -0
- package/src/data-display/ActivityFeed/fixtures.ts +29 -0
- package/src/data-display/ChartCard/ChartCard.ranges.test.tsx +282 -0
- package/src/data-display/ChartCard/ChartCard.test.tsx +54 -0
- package/src/data-display/ChartCard/ChartCard.tsx +218 -0
- package/src/data-display/ChartCard/block.meta.ts +10 -0
- package/src/data-display/ChartCard/examples/custom-ranges.tsx +37 -0
- package/src/data-display/ChartCard/examples/heatmap.tsx +20 -0
- package/src/data-display/ChartCard/examples/toolbar-owned-range.tsx +34 -0
- package/src/data-display/ChartCard/fixtures.ts +82 -0
- package/src/data-display/CommentThread/CommentThread.test.tsx +29 -0
- package/src/data-display/CommentThread/CommentThread.tsx +120 -0
- package/src/data-display/CommentThread/block.meta.ts +10 -0
- package/src/data-display/CommentThread/fixtures.ts +28 -0
- package/src/data-display/DescriptionListCard/DescriptionListCard.test.tsx +28 -0
- package/src/data-display/DescriptionListCard/DescriptionListCard.tsx +120 -0
- package/src/data-display/DescriptionListCard/block.meta.ts +9 -0
- package/src/data-display/DescriptionListCard/fixtures.ts +12 -0
- package/src/data-display/KpiRow/KpiRow.test.tsx +29 -0
- package/src/data-display/KpiRow/KpiRow.tsx +101 -0
- package/src/data-display/KpiRow/block.meta.ts +10 -0
- package/src/data-display/KpiRow/fixtures.ts +34 -0
- package/src/data-display/ResourceCardGrid/ResourceCardGrid.test.tsx +22 -0
- package/src/data-display/ResourceCardGrid/ResourceCardGrid.tsx +98 -0
- package/src/data-display/ResourceCardGrid/block.meta.ts +10 -0
- package/src/data-display/ResourceCardGrid/fixtures.ts +72 -0
- package/src/data-display/StatCard/StatCard.test.tsx +30 -0
- package/src/data-display/StatCard/StatCard.tsx +138 -0
- package/src/data-display/StatCard/block.meta.ts +10 -0
- package/src/data-display/StatCard/fixtures.ts +26 -0
- package/src/data-display/StatusTimeline/StatusTimeline.test.tsx +153 -0
- package/src/data-display/StatusTimeline/StatusTimeline.tsx +114 -0
- package/src/data-display/StatusTimeline/block.meta.ts +10 -0
- package/src/data-display/StatusTimeline/examples/Empty.tsx +6 -0
- package/src/data-display/StatusTimeline/fixtures.ts +62 -0
- package/src/data-display/index.ts +34 -0
- package/src/data-display/shared/TrendDelta.tsx +60 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.test.tsx +47 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.tsx +145 -0
- package/src/feedback/ConfirmDestructive/block.meta.ts +10 -0
- package/src/feedback/EmptyStates/EmptyStates.test.tsx +48 -0
- package/src/feedback/EmptyStates/EmptyStates.tsx +191 -0
- package/src/feedback/EmptyStates/block.meta.ts +10 -0
- package/src/feedback/EmptyStates/fixtures.ts +6 -0
- package/src/feedback/ErrorPanel/ErrorPanel.test.tsx +37 -0
- package/src/feedback/ErrorPanel/ErrorPanel.tsx +146 -0
- package/src/feedback/ErrorPanel/block.meta.ts +10 -0
- package/src/feedback/InlineCallout/InlineCallout.test.tsx +33 -0
- package/src/feedback/InlineCallout/InlineCallout.tsx +81 -0
- package/src/feedback/InlineCallout/block.meta.ts +9 -0
- package/src/feedback/Skeletons/Skeletons.test.tsx +25 -0
- package/src/feedback/Skeletons/Skeletons.tsx +142 -0
- package/src/feedback/Skeletons/block.meta.ts +10 -0
- package/src/feedback/SystemBanner/SystemBanner.test.tsx +29 -0
- package/src/feedback/SystemBanner/SystemBanner.tsx +105 -0
- package/src/feedback/SystemBanner/block.meta.ts +9 -0
- package/src/feedback/index.ts +14 -0
- package/src/feedback/types.ts +13 -0
- package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.test.tsx +36 -0
- package/src/forms/CreateApiKeyDialog/CreateApiKeyDialog.tsx +197 -0
- package/src/forms/CreateApiKeyDialog/block.meta.ts +10 -0
- package/src/forms/CreateApiKeyDialog/fixtures.ts +31 -0
- package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.test.tsx +339 -0
- package/src/forms/CredentialDisclosureDialog/CredentialDisclosureDialog.tsx +193 -0
- package/src/forms/CredentialDisclosureDialog/block.meta.ts +10 -0
- package/src/forms/CredentialDisclosureDialog/examples/Fresh.tsx +32 -0
- package/src/forms/CredentialDisclosureDialog/examples/ResponseGating.tsx +69 -0
- package/src/forms/CredentialDisclosureDialog/examples/Stored.tsx +32 -0
- package/src/forms/CredentialDisclosureDialog/fixtures.ts +14 -0
- package/src/forms/DangerZone/ConfirmDestructiveDialog.tsx +101 -0
- package/src/forms/DangerZone/DangerZone.test.tsx +28 -0
- package/src/forms/DangerZone/DangerZone.tsx +86 -0
- package/src/forms/DangerZone/block.meta.ts +10 -0
- package/src/forms/DangerZone/fixtures.ts +36 -0
- package/src/forms/DateRangeFilter/DateRangeFilter.test.tsx +26 -0
- package/src/forms/DateRangeFilter/DateRangeFilter.tsx +83 -0
- package/src/forms/DateRangeFilter/block.meta.ts +9 -0
- package/src/forms/DialogWizard/DialogWizard.test.tsx +95 -0
- package/src/forms/DialogWizard/DialogWizard.tsx +39 -0
- package/src/forms/DialogWizard/block.meta.ts +10 -0
- package/src/forms/DialogWizard/examples/CreateResource.tsx +65 -0
- package/src/forms/DialogWizard/fixtures.ts +20 -0
- package/src/forms/FileUploadPanel/FileUploadPanel.test.tsx +43 -0
- package/src/forms/FileUploadPanel/FileUploadPanel.tsx +244 -0
- package/src/forms/FileUploadPanel/block.meta.ts +10 -0
- package/src/forms/FileUploadPanel/fixtures.ts +66 -0
- package/src/forms/FormSection/FormSection.test.tsx +34 -0
- package/src/forms/FormSection/FormSection.tsx +77 -0
- package/src/forms/FormSection/block.meta.ts +9 -0
- package/src/forms/InviteMembersDialog/InviteMembersDialog.test.tsx +37 -0
- package/src/forms/InviteMembersDialog/InviteMembersDialog.tsx +183 -0
- package/src/forms/InviteMembersDialog/block.meta.ts +10 -0
- package/src/forms/InviteMembersDialog/fixtures.ts +14 -0
- package/src/forms/SettingsRow/SettingsRow.test.tsx +50 -0
- package/src/forms/SettingsRow/SettingsRow.tsx +74 -0
- package/src/forms/SettingsRow/block.meta.ts +9 -0
- package/src/forms/ValidationSummary/ValidationSummary.test.tsx +46 -0
- package/src/forms/ValidationSummary/ValidationSummary.tsx +75 -0
- package/src/forms/ValidationSummary/block.meta.ts +9 -0
- package/src/forms/ValidationSummary/fixtures.ts +13 -0
- package/src/forms/index.ts +27 -0
- package/src/index.ts +14 -0
- package/src/marketing/ComparisonTable/ComparisonTable.test.tsx +69 -0
- package/src/marketing/ComparisonTable/ComparisonTable.tsx +245 -0
- package/src/marketing/ComparisonTable/block.meta.ts +10 -0
- package/src/marketing/ComparisonTable/examples/options.tsx +48 -0
- package/src/marketing/ComparisonTable/fixtures.ts +83 -0
- package/src/marketing/Faq/Faq.test.tsx +25 -0
- package/src/marketing/Faq/Faq.tsx +68 -0
- package/src/marketing/Faq/block.meta.ts +10 -0
- package/src/marketing/Faq/fixtures.ts +32 -0
- package/src/marketing/FeatureGrid/FeatureGrid.test.tsx +19 -0
- package/src/marketing/FeatureGrid/FeatureGrid.tsx +79 -0
- package/src/marketing/FeatureGrid/block.meta.ts +10 -0
- package/src/marketing/FeatureGrid/fixtures.ts +36 -0
- package/src/marketing/Hero/Hero.test.tsx +33 -0
- package/src/marketing/Hero/Hero.tsx +107 -0
- package/src/marketing/Hero/block.meta.ts +10 -0
- package/src/marketing/PricingTable/PricingTable.test.tsx +92 -0
- package/src/marketing/PricingTable/PricingTable.tsx +323 -0
- package/src/marketing/PricingTable/block.meta.ts +10 -0
- package/src/marketing/PricingTable/fixtures.ts +34 -0
- package/src/marketing/actions.tsx +40 -0
- package/src/marketing/index.ts +15 -0
- package/src/meta.ts +23 -0
- package/src/page-chrome/BulkActionBar/BulkActionBar.test.tsx +34 -0
- package/src/page-chrome/BulkActionBar/BulkActionBar.tsx +76 -0
- package/src/page-chrome/BulkActionBar/block.meta.ts +9 -0
- package/src/page-chrome/PageHeader/PageHeader.test.tsx +68 -0
- package/src/page-chrome/PageHeader/PageHeader.tsx +152 -0
- package/src/page-chrome/PageHeader/block.meta.ts +10 -0
- package/src/page-chrome/PageHeader/fixtures.ts +29 -0
- package/src/page-chrome/PageToolbar/PageToolbar.test.tsx +38 -0
- package/src/page-chrome/PageToolbar/PageToolbar.tsx +138 -0
- package/src/page-chrome/PageToolbar/block.meta.ts +10 -0
- package/src/page-chrome/PageToolbar/fixtures.ts +8 -0
- package/src/page-chrome/SectionHeader/SectionHeader.test.tsx +28 -0
- package/src/page-chrome/SectionHeader/SectionHeader.tsx +73 -0
- package/src/page-chrome/SectionHeader/block.meta.ts +9 -0
- package/src/page-chrome/StickyActionBar/StickyActionBar.test.tsx +29 -0
- package/src/page-chrome/StickyActionBar/StickyActionBar.tsx +66 -0
- package/src/page-chrome/StickyActionBar/block.meta.ts +9 -0
- package/src/page-chrome/index.ts +13 -0
- package/src/page-chrome/types.ts +25 -0
- package/src/scheduling/AgendaList/AgendaList.test.tsx +28 -0
- package/src/scheduling/AgendaList/AgendaList.tsx +165 -0
- package/src/scheduling/AgendaList/block.meta.ts +10 -0
- package/src/scheduling/MonthCalendar/MonthCalendar.test.tsx +32 -0
- package/src/scheduling/MonthCalendar/MonthCalendar.tsx +223 -0
- package/src/scheduling/MonthCalendar/block.meta.ts +10 -0
- package/src/scheduling/TimeSlotPicker/TimeSlotPicker.test.tsx +26 -0
- package/src/scheduling/TimeSlotPicker/TimeSlotPicker.tsx +115 -0
- package/src/scheduling/TimeSlotPicker/block.meta.ts +10 -0
- package/src/scheduling/TimeSlotPicker/fixtures.ts +10 -0
- package/src/scheduling/WeekCalendar/WeekCalendar.test.tsx +27 -0
- package/src/scheduling/WeekCalendar/WeekCalendar.tsx +274 -0
- package/src/scheduling/WeekCalendar/block.meta.ts +10 -0
- package/src/scheduling/index.ts +11 -0
- package/src/scheduling/shared/CalendarHeader.tsx +54 -0
- package/src/scheduling/shared/EventTile.tsx +21 -0
- package/src/scheduling/shared/dates.ts +98 -0
- package/src/scheduling/shared/events.ts +90 -0
- package/src/scheduling/shared/fixtures.ts +54 -0
- package/src/shell/AppSidebar/AppSidebar.test.tsx +38 -0
- package/src/shell/AppSidebar/AppSidebar.tsx +226 -0
- package/src/shell/AppSidebar/block.meta.ts +10 -0
- package/src/shell/AppSidebar/fixtures.ts +58 -0
- package/src/shell/AppTopbar/AppTopbar.test.tsx +41 -0
- package/src/shell/AppTopbar/AppTopbar.tsx +82 -0
- package/src/shell/AppTopbar/block.meta.ts +19 -0
- package/src/shell/AppTopbar/fixtures.ts +22 -0
- package/src/shell/EnvironmentBadge/EnvironmentBadge.test.tsx +22 -0
- package/src/shell/EnvironmentBadge/EnvironmentBadge.tsx +55 -0
- package/src/shell/EnvironmentBadge/block.meta.ts +10 -0
- package/src/shell/GlobalSearch/GlobalSearch.test.tsx +28 -0
- package/src/shell/GlobalSearch/GlobalSearch.tsx +80 -0
- package/src/shell/GlobalSearch/block.meta.ts +10 -0
- package/src/shell/GlobalSearch/fixtures.ts +64 -0
- package/src/shell/index.ts +11 -0
- package/src/styles.css +2 -0
- package/src/tables/DataTable/DataTable.edit-views.test.tsx +249 -0
- package/src/tables/DataTable/DataTable.layout.test.tsx +188 -0
- package/src/tables/DataTable/DataTable.single-select.test.tsx +501 -0
- package/src/tables/DataTable/DataTable.test.tsx +268 -0
- package/src/tables/DataTable/DataTable.tsx +520 -0
- package/src/tables/DataTable/block.meta.ts +27 -0
- package/src/tables/DataTable/column-menu.tsx +109 -0
- package/src/tables/DataTable/column-sizing.ts +166 -0
- package/src/tables/DataTable/data-table-body.tsx +194 -0
- package/src/tables/DataTable/data-table-props.ts +156 -0
- package/src/tables/DataTable/data-table-row.tsx +220 -0
- package/src/tables/DataTable/data-table-utils.ts +195 -0
- package/src/tables/DataTable/density-toggle.tsx +30 -0
- package/src/tables/DataTable/editable-cell.tsx +274 -0
- package/src/tables/DataTable/examples/editable.tsx +71 -0
- package/src/tables/DataTable/examples/grouped.tsx +18 -0
- package/src/tables/DataTable/examples/resizable-pinned.tsx +20 -0
- package/src/tables/DataTable/examples/saved-views.tsx +61 -0
- package/src/tables/DataTable/examples/single-selection.tsx +55 -0
- package/src/tables/DataTable/examples/virtualized.tsx +20 -0
- package/src/tables/DataTable/fixtures.ts +119 -0
- package/src/tables/DataTable/header-cell.tsx +86 -0
- package/src/tables/DataTable/resize-handle.tsx +88 -0
- package/src/tables/DataTable/single-selection.tsx +149 -0
- package/src/tables/DataTable/use-table-views.ts +79 -0
- package/src/tables/DataTable/use-virtual-rows.ts +83 -0
- package/src/tables/DataTable/views.tsx +186 -0
- package/src/tables/FilterBar/FilterBar.test.tsx +43 -0
- package/src/tables/FilterBar/FilterBar.tsx +199 -0
- package/src/tables/FilterBar/block.meta.ts +10 -0
- package/src/tables/FilterBar/fixtures.ts +32 -0
- package/src/tables/InlineEditRow/InlineEditRow.test.tsx +41 -0
- package/src/tables/InlineEditRow/InlineEditRow.tsx +215 -0
- package/src/tables/InlineEditRow/block.meta.ts +10 -0
- package/src/tables/InlineEditRow/fixtures.ts +28 -0
- package/src/tables/PaginationFooter/PaginationFooter.test.tsx +29 -0
- package/src/tables/PaginationFooter/PaginationFooter.tsx +94 -0
- package/src/tables/PaginationFooter/block.meta.ts +10 -0
- package/src/tables/SubordinateList/SubordinateList.test.tsx +351 -0
- package/src/tables/SubordinateList/SubordinateList.tsx +140 -0
- package/src/tables/SubordinateList/block.meta.ts +10 -0
- package/src/tables/SubordinateList/examples/ResourceForm.tsx +129 -0
- package/src/tables/SubordinateList/examples/empty.tsx +5 -0
- package/src/tables/SubordinateList/examples/error.tsx +5 -0
- package/src/tables/SubordinateList/examples/loading.tsx +5 -0
- package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.test.tsx +159 -0
- package/src/tables/TruncatedCollectionFooter/TruncatedCollectionFooter.tsx +60 -0
- package/src/tables/TruncatedCollectionFooter/block.meta.ts +10 -0
- package/src/tables/TruncatedCollectionFooter/examples/exact.tsx +6 -0
- package/src/tables/TruncatedCollectionFooter/examples/has-more.tsx +12 -0
- package/src/tables/TruncatedCollectionFooter/fixtures.ts +2 -0
- package/src/tables/index.ts +45 -0
- package/src/team/MembersTable/MembersTable.test.tsx +29 -0
- package/src/team/MembersTable/MembersTable.tsx +141 -0
- package/src/team/MembersTable/block.meta.ts +9 -0
- package/src/team/MembersTable/fixtures.ts +19 -0
- package/src/team/NotificationMatrix/NotificationMatrix.test.tsx +36 -0
- package/src/team/NotificationMatrix/NotificationMatrix.tsx +144 -0
- package/src/team/NotificationMatrix/block.meta.ts +9 -0
- package/src/team/NotificationMatrix/fixtures.ts +43 -0
- package/src/team/PendingInvitations/PendingInvitations.test.tsx +30 -0
- package/src/team/PendingInvitations/PendingInvitations.tsx +132 -0
- package/src/team/PendingInvitations/block.meta.ts +9 -0
- package/src/team/PendingInvitations/fixtures.ts +39 -0
- package/src/team/ProfileHeader/ProfileHeader.test.tsx +29 -0
- package/src/team/ProfileHeader/ProfileHeader.tsx +106 -0
- package/src/team/ProfileHeader/block.meta.ts +10 -0
- package/src/team/ProfileHeader/fixtures.ts +28 -0
- package/src/team/SessionsDevices/SessionsDevices.test.tsx +30 -0
- package/src/team/SessionsDevices/SessionsDevices.tsx +126 -0
- package/src/team/SessionsDevices/block.meta.ts +10 -0
- package/src/team/SessionsDevices/fixtures.ts +58 -0
- package/src/team/index.ts +15 -0
- package/src/test/a11y.ts +18 -0
- package/src/test/heading-levels.test.tsx +69 -0
- package/src/test/setup.ts +36 -0
- package/README.md +0 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown } from '@gntik-ai/icons';
|
|
2
|
+
import { cn, nextSortDirection, TableHead, tableVariants, type SortDirection } from '@gntik-ai/ui';
|
|
3
|
+
import { stickyTint, type CellLayout } from './column-sizing';
|
|
4
|
+
import { columnAlign, type DataTableColumn } from './data-table-utils';
|
|
5
|
+
import { ColumnResizeHandle } from './resize-handle';
|
|
6
|
+
|
|
7
|
+
export interface DataTableHeadCellProps<T> {
|
|
8
|
+
column: DataTableColumn<T>;
|
|
9
|
+
sortDirection: SortDirection;
|
|
10
|
+
onSortChange: (direction: Exclude<SortDirection, 'none'>) => void;
|
|
11
|
+
layout?: CellLayout;
|
|
12
|
+
/** Resize handle props; omitted, the column is not resizable. */
|
|
13
|
+
resize?: { width: number; min: number; max: number; onResize: (width: number) => void };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Column header of the DataTable. Without a resize handle it is the kit TableHead; with one, the
|
|
18
|
+
* sort button and the handle sit side by side (the handle never nests inside the button).
|
|
19
|
+
*/
|
|
20
|
+
export function DataTableHeadCell<T>({
|
|
21
|
+
column,
|
|
22
|
+
sortDirection,
|
|
23
|
+
onSortChange,
|
|
24
|
+
layout,
|
|
25
|
+
resize,
|
|
26
|
+
}: DataTableHeadCellProps<T>) {
|
|
27
|
+
const align = columnAlign(column);
|
|
28
|
+
const pinnedClass = layout?.pinned
|
|
29
|
+
? cn(layout.className, 'z-[11]', stickyTint(layout, { head: true }), 'bg-card')
|
|
30
|
+
: undefined;
|
|
31
|
+
if (!resize) {
|
|
32
|
+
return (
|
|
33
|
+
<TableHead
|
|
34
|
+
align={align}
|
|
35
|
+
className={cn(column.headClassName, pinnedClass)}
|
|
36
|
+
style={layout?.style}
|
|
37
|
+
sortable={column.sortable}
|
|
38
|
+
sortDirection={sortDirection}
|
|
39
|
+
onSortChange={onSortChange}
|
|
40
|
+
data-column-id={column.id}
|
|
41
|
+
>
|
|
42
|
+
{column.header}
|
|
43
|
+
</TableHead>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const v = tableVariants({ align: align ?? 'left', active: sortDirection !== 'none' });
|
|
47
|
+
const SortIcon =
|
|
48
|
+
sortDirection === 'ascending' ? ArrowUp : sortDirection === 'descending' ? ArrowDown : ChevronsUpDown;
|
|
49
|
+
return (
|
|
50
|
+
<TableHead
|
|
51
|
+
align={align}
|
|
52
|
+
className={cn('relative overflow-visible', column.headClassName, pinnedClass)}
|
|
53
|
+
style={layout?.style}
|
|
54
|
+
aria-sort={column.sortable ? sortDirection : undefined}
|
|
55
|
+
data-column-id={column.id}
|
|
56
|
+
>
|
|
57
|
+
<span
|
|
58
|
+
className={cn(
|
|
59
|
+
'block min-w-0 truncate pe-2',
|
|
60
|
+
align === 'right' && 'text-end',
|
|
61
|
+
align === 'center' && 'text-center',
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{column.sortable ? (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
className={cn(v.sortButton(), 'max-w-full')}
|
|
68
|
+
onClick={() => onSortChange(nextSortDirection(sortDirection))}
|
|
69
|
+
>
|
|
70
|
+
<span className="truncate">{column.header}</span>
|
|
71
|
+
<SortIcon size={13} aria-hidden className={sortDirection === 'none' ? 'opacity-40' : undefined} />
|
|
72
|
+
</button>
|
|
73
|
+
) : (
|
|
74
|
+
column.header
|
|
75
|
+
)}
|
|
76
|
+
</span>
|
|
77
|
+
<ColumnResizeHandle
|
|
78
|
+
label={column.header}
|
|
79
|
+
width={resize.width}
|
|
80
|
+
min={resize.min}
|
|
81
|
+
max={resize.max}
|
|
82
|
+
onResize={resize.onResize}
|
|
83
|
+
/>
|
|
84
|
+
</TableHead>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cn } from '@gntik-ai/ui';
|
|
2
|
+
import { useRef, type KeyboardEvent, type PointerEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface ColumnResizeHandleProps {
|
|
5
|
+
/** Column name for the accessible label ("Resize Region column"). */
|
|
6
|
+
label: string;
|
|
7
|
+
width: number;
|
|
8
|
+
min: number;
|
|
9
|
+
max: number;
|
|
10
|
+
onResize: (width: number) => void;
|
|
11
|
+
/** Called once a drag or a key press ends a resize. */
|
|
12
|
+
onResizeEnd?: (width: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const STEP = 10;
|
|
16
|
+
const BIG_STEP = 50;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Column resize handle at the end edge of a header cell: a focusable `role="separator"` with
|
|
20
|
+
* aria-valuenow/min/max (the width in px). Drag it with a pointer; from the keyboard,
|
|
21
|
+
* ArrowLeft/ArrowRight change the width by 10px (Shift: 50px), Home/End jump to the limits.
|
|
22
|
+
*/
|
|
23
|
+
export function ColumnResizeHandle({ label, width, min, max, onResize, onResizeEnd }: ColumnResizeHandleProps) {
|
|
24
|
+
const drag = useRef<{ x: number; width: number; dir: 1 | -1 } | null>(null);
|
|
25
|
+
const clamp = (w: number) => Math.round(Math.min(max, Math.max(min, w)));
|
|
26
|
+
|
|
27
|
+
const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
e.stopPropagation();
|
|
30
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
31
|
+
const rtl = typeof getComputedStyle === 'function' && getComputedStyle(e.currentTarget).direction === 'rtl';
|
|
32
|
+
drag.current = { x: e.clientX, width, dir: rtl ? -1 : 1 };
|
|
33
|
+
};
|
|
34
|
+
const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {
|
|
35
|
+
const d = drag.current;
|
|
36
|
+
if (!d) return;
|
|
37
|
+
onResize(clamp(d.width + (e.clientX - d.x) * d.dir));
|
|
38
|
+
};
|
|
39
|
+
const onPointerUp = (e: PointerEvent<HTMLDivElement>) => {
|
|
40
|
+
const d = drag.current;
|
|
41
|
+
drag.current = null;
|
|
42
|
+
if (d) onResizeEnd?.(clamp(d.width + (e.clientX - d.x) * d.dir));
|
|
43
|
+
};
|
|
44
|
+
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
45
|
+
const step = e.shiftKey ? BIG_STEP : STEP;
|
|
46
|
+
const rtl = typeof getComputedStyle === 'function' && getComputedStyle(e.currentTarget).direction === 'rtl';
|
|
47
|
+
const grow = rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
48
|
+
const shrink = rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
49
|
+
let next: number | null = null;
|
|
50
|
+
if (e.key === grow || e.key === 'ArrowUp') next = width + step;
|
|
51
|
+
else if (e.key === shrink || e.key === 'ArrowDown') next = width - step;
|
|
52
|
+
else if (e.key === 'Home') next = min;
|
|
53
|
+
else if (e.key === 'End') next = max;
|
|
54
|
+
if (next == null) return;
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
const w = clamp(next);
|
|
57
|
+
onResize(w);
|
|
58
|
+
onResizeEnd?.(w);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
role="separator"
|
|
64
|
+
tabIndex={0}
|
|
65
|
+
aria-orientation="vertical"
|
|
66
|
+
aria-label={`Resize ${label} column`}
|
|
67
|
+
aria-valuenow={width}
|
|
68
|
+
aria-valuemin={min}
|
|
69
|
+
aria-valuemax={max}
|
|
70
|
+
aria-valuetext={`${width} pixels`}
|
|
71
|
+
data-column-resizer=""
|
|
72
|
+
onPointerDown={onPointerDown}
|
|
73
|
+
onPointerMove={onPointerMove}
|
|
74
|
+
onPointerUp={onPointerUp}
|
|
75
|
+
onPointerCancel={onPointerUp}
|
|
76
|
+
onKeyDown={onKeyDown}
|
|
77
|
+
className={cn(
|
|
78
|
+
'group/resizer absolute inset-y-0 -end-1.5 z-[2] flex w-3 cursor-col-resize touch-none justify-center',
|
|
79
|
+
'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus-ring',
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden
|
|
84
|
+
className="my-2 w-px bg-border transition-colors group-hover/resizer:w-0.5 group-hover/resizer:bg-primary group-focus-visible/resizer:w-0.5 group-focus-visible/resizer:bg-primary motion-reduce:transition-none"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState, type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const mountedNames = new Map<string, number>();
|
|
4
|
+
|
|
5
|
+
export interface SingleSelection<T> {
|
|
6
|
+
selectedId: string | null;
|
|
7
|
+
inputProps: (row: T) => ComponentProps<'input'>;
|
|
8
|
+
entryProps: (row: T) => ComponentProps<'span'>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useSingleSelection<T>(options: {
|
|
12
|
+
selectionName?: string;
|
|
13
|
+
selectedId?: string | null;
|
|
14
|
+
defaultSelectedId?: string | null;
|
|
15
|
+
onSelectedIdChange?: (id: string) => void;
|
|
16
|
+
getRowId: (row: T) => string;
|
|
17
|
+
getRowLabel: (row: T) => string;
|
|
18
|
+
rows: readonly T[];
|
|
19
|
+
isRowDisabled?: (row: T) => boolean;
|
|
20
|
+
}): SingleSelection<T> {
|
|
21
|
+
const generatedName = useId();
|
|
22
|
+
const [innerId, setInnerId] = useState(options.defaultSelectedId ?? null);
|
|
23
|
+
const selectedId = options.selectedId === undefined ? innerId : options.selectedId;
|
|
24
|
+
const inputs = useRef(new Map<string, HTMLInputElement>());
|
|
25
|
+
const entry = useRef<HTMLSpanElement>(null);
|
|
26
|
+
const enabledIds = options.rows.filter((row) => !options.isRowDisabled?.(row)).map(options.getRowId);
|
|
27
|
+
const tabId = selectedId != null && enabledIds.includes(selectedId) ? selectedId : enabledIds[0];
|
|
28
|
+
// Chromium excludes every unchecked same-name radio from sequential focus when
|
|
29
|
+
// a disabled radio is checked. Put the fallback tab stop beside the first
|
|
30
|
+
// enabled radio so other table controls retain their normal Tab order.
|
|
31
|
+
const redirectEntry =
|
|
32
|
+
tabId !== undefined &&
|
|
33
|
+
options.rows.some((row) => options.getRowId(row) === selectedId && options.isRowDisabled?.(row));
|
|
34
|
+
const select = (id: string) => {
|
|
35
|
+
if (id === selectedId) return;
|
|
36
|
+
setInnerId(id);
|
|
37
|
+
options.onSelectedIdChange?.(id);
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
selectedId,
|
|
41
|
+
entryProps: (row) =>
|
|
42
|
+
redirectEntry && options.getRowId(row) === tabId
|
|
43
|
+
? {
|
|
44
|
+
ref: entry,
|
|
45
|
+
tabIndex: 0,
|
|
46
|
+
onFocus: (event) => {
|
|
47
|
+
if (event.target === event.currentTarget && tabId !== undefined)
|
|
48
|
+
inputs.current.get(tabId)?.focus({ preventScroll: true });
|
|
49
|
+
},
|
|
50
|
+
onPointerDown: (event) => {
|
|
51
|
+
// Only keyboard entry redirects focus; clicking the wrapper's blank
|
|
52
|
+
// space must not focus it or interfere with native radio clicks.
|
|
53
|
+
if (event.target === event.currentTarget) event.preventDefault();
|
|
54
|
+
},
|
|
55
|
+
onBlur: (event) => {
|
|
56
|
+
if (!event.currentTarget.contains(event.relatedTarget)) event.currentTarget.tabIndex = 0;
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {},
|
|
60
|
+
inputProps: (row) => {
|
|
61
|
+
const id = options.getRowId(row);
|
|
62
|
+
const disabled = options.isRowDisabled?.(row) ?? false;
|
|
63
|
+
return {
|
|
64
|
+
type: 'radio',
|
|
65
|
+
name: options.selectionName ?? generatedName,
|
|
66
|
+
'aria-label': options.getRowLabel(row),
|
|
67
|
+
checked: selectedId === id,
|
|
68
|
+
disabled,
|
|
69
|
+
tabIndex: !disabled && id === tabId ? 0 : -1,
|
|
70
|
+
ref: (input) => {
|
|
71
|
+
if (input) inputs.current.set(id, input);
|
|
72
|
+
else inputs.current.delete(id);
|
|
73
|
+
},
|
|
74
|
+
onChange: () => {
|
|
75
|
+
if (!disabled) select(id);
|
|
76
|
+
},
|
|
77
|
+
onKeyDown: (event) => {
|
|
78
|
+
// Avoid a second stop on the entry wrapper when leaving its radio.
|
|
79
|
+
// Its blur handler restores keyboard entry after focus has left.
|
|
80
|
+
if (event.key === 'Tab' && entry.current) entry.current.tabIndex = -1;
|
|
81
|
+
const direction = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
82
|
+
if (direction == null || disabled || enabledIds.length === 0) return;
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
const index = enabledIds.indexOf(id);
|
|
85
|
+
const nextId = enabledIds[(index + direction + enabledIds.length) % enabledIds.length]!;
|
|
86
|
+
inputs.current.get(nextId)?.focus();
|
|
87
|
+
select(nextId);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function useSingleSelectionWarnings({
|
|
95
|
+
single,
|
|
96
|
+
selectionName,
|
|
97
|
+
paginated,
|
|
98
|
+
hasLabel,
|
|
99
|
+
}: {
|
|
100
|
+
single: boolean;
|
|
101
|
+
selectionName?: string;
|
|
102
|
+
paginated?: boolean;
|
|
103
|
+
hasLabel: boolean;
|
|
104
|
+
}) {
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (single && paginated === true && process.env.NODE_ENV === 'development') {
|
|
107
|
+
console.warn('DataTable single selection: paginated=true is unsupported; rendering all rows without pagination.');
|
|
108
|
+
}
|
|
109
|
+
}, [single, paginated]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!single || process.env.NODE_ENV !== 'development') return;
|
|
112
|
+
if (!hasLabel) console.warn('DataTable single selection: supply legend, aria-label or aria-labelledby.');
|
|
113
|
+
}, [single, hasLabel]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!single || selectionName === undefined || process.env.NODE_ENV !== 'development') return;
|
|
116
|
+
const count = mountedNames.get(selectionName) ?? 0;
|
|
117
|
+
if (count > 0)
|
|
118
|
+
console.warn(
|
|
119
|
+
`DataTable single selection: duplicate selectionName "${selectionName}"; supplied names must be unique per page.`,
|
|
120
|
+
);
|
|
121
|
+
mountedNames.set(selectionName, count + 1);
|
|
122
|
+
return () => {
|
|
123
|
+
const remaining = (mountedNames.get(selectionName) ?? 1) - 1;
|
|
124
|
+
if (remaining === 0) mountedNames.delete(selectionName);
|
|
125
|
+
else mountedNames.set(selectionName, remaining);
|
|
126
|
+
};
|
|
127
|
+
}, [single, selectionName]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function SelectionGroup({
|
|
131
|
+
single,
|
|
132
|
+
legend,
|
|
133
|
+
children,
|
|
134
|
+
...label
|
|
135
|
+
}: {
|
|
136
|
+
single: boolean;
|
|
137
|
+
legend?: ReactNode;
|
|
138
|
+
'aria-label'?: string;
|
|
139
|
+
'aria-labelledby'?: string;
|
|
140
|
+
children: ReactNode;
|
|
141
|
+
}) {
|
|
142
|
+
if (!single) return <>{children}</>;
|
|
143
|
+
return (
|
|
144
|
+
<fieldset className="m-0 min-w-0 border-0 p-0" {...label}>
|
|
145
|
+
{legend != null && <legend className="px-3 py-2 text-sm font-medium text-foreground">{legend}</legend>}
|
|
146
|
+
{children}
|
|
147
|
+
</fieldset>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { newViewId, sameViewState, type DataTableView, type DataTableViewState } from './views';
|
|
3
|
+
|
|
4
|
+
/** A value that is controlled when `value` is defined, otherwise kept in state. */
|
|
5
|
+
export function useControllable<V>(
|
|
6
|
+
value: V | undefined,
|
|
7
|
+
initial: V | (() => V),
|
|
8
|
+
onChange?: (next: V) => void,
|
|
9
|
+
): [V, (next: V) => void] {
|
|
10
|
+
const [inner, setInner] = useState<V>(initial);
|
|
11
|
+
const current = value === undefined ? inner : value;
|
|
12
|
+
const set = (next: V) => {
|
|
13
|
+
setInner(next);
|
|
14
|
+
onChange?.(next);
|
|
15
|
+
};
|
|
16
|
+
return [current, set];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseTableViewsOptions {
|
|
20
|
+
views?: readonly DataTableView[];
|
|
21
|
+
defaultViews?: readonly DataTableView[];
|
|
22
|
+
onViewsChange?: (views: DataTableView[]) => void;
|
|
23
|
+
activeViewId?: string | null;
|
|
24
|
+
defaultActiveViewId?: string | null;
|
|
25
|
+
onActiveViewChange?: (id: string | null) => void;
|
|
26
|
+
/** The table's state right now. */
|
|
27
|
+
current: DataTableViewState;
|
|
28
|
+
/** The table's state with no view applied (props' defaults). */
|
|
29
|
+
defaults: DataTableViewState;
|
|
30
|
+
/** Applies a complete state to the table. */
|
|
31
|
+
apply: (state: DataTableViewState) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Saved-views state machine for the DataTable: select, save as, save, rename, delete, reset. */
|
|
35
|
+
export function useTableViews({
|
|
36
|
+
views: viewsProp,
|
|
37
|
+
defaultViews = [],
|
|
38
|
+
onViewsChange,
|
|
39
|
+
activeViewId,
|
|
40
|
+
defaultActiveViewId = null,
|
|
41
|
+
onActiveViewChange,
|
|
42
|
+
current,
|
|
43
|
+
defaults,
|
|
44
|
+
apply,
|
|
45
|
+
}: UseTableViewsOptions) {
|
|
46
|
+
const [views, setViews] = useControllable<readonly DataTableView[]>(viewsProp, defaultViews, (next) =>
|
|
47
|
+
onViewsChange?.([...next]),
|
|
48
|
+
);
|
|
49
|
+
const [activeId, setActiveId] = useControllable<string | null>(activeViewId, defaultActiveViewId, onActiveViewChange);
|
|
50
|
+
const active = views.find((v) => v.id === activeId) ?? null;
|
|
51
|
+
const target = (view: DataTableView | null): DataTableViewState => ({ ...defaults, ...(view?.state ?? {}) });
|
|
52
|
+
const dirty = !sameViewState(current, target(active));
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
views,
|
|
56
|
+
activeId: active ? active.id : null,
|
|
57
|
+
dirty,
|
|
58
|
+
select: (id: string | null) => {
|
|
59
|
+
const view = views.find((v) => v.id === id) ?? null;
|
|
60
|
+
setActiveId(view ? view.id : null);
|
|
61
|
+
apply(target(view));
|
|
62
|
+
},
|
|
63
|
+
saveAs: (name: string) => {
|
|
64
|
+
const view: DataTableView = { id: newViewId(name, views), name, state: current };
|
|
65
|
+
setViews([...views, view]);
|
|
66
|
+
setActiveId(view.id);
|
|
67
|
+
},
|
|
68
|
+
save: () => {
|
|
69
|
+
if (!active) return;
|
|
70
|
+
setViews(views.map((v) => (v.id === active.id ? { ...v, state: current } : v)));
|
|
71
|
+
},
|
|
72
|
+
rename: (id: string, name: string) => setViews(views.map((v) => (v.id === id ? { ...v, name } : v))),
|
|
73
|
+
remove: (id: string) => {
|
|
74
|
+
setViews(views.filter((v) => v.id !== id));
|
|
75
|
+
if (id === activeId) setActiveId(null);
|
|
76
|
+
},
|
|
77
|
+
reset: () => apply(target(active)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useState, type RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
/** The items to render: `[start, end)` of the flattened body, each `rowHeight` px tall. */
|
|
4
|
+
export interface VirtualWindow {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
rowHeight: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseVirtualRowsOptions {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
count: number;
|
|
13
|
+
rowHeight: number;
|
|
14
|
+
/** The `<table>`; its parent is the scroll container. */
|
|
15
|
+
tableRef: RefObject<HTMLTableElement | null>;
|
|
16
|
+
/** Fallback viewport height before the container is measured (and in jsdom). */
|
|
17
|
+
viewportHeight: number;
|
|
18
|
+
/** Extra rows rendered above and below the viewport. */
|
|
19
|
+
overscan?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Computes a virtual window from the table container's scroll position. */
|
|
23
|
+
export function windowFor(
|
|
24
|
+
count: number,
|
|
25
|
+
scrollTop: number,
|
|
26
|
+
viewport: number,
|
|
27
|
+
rowHeight: number,
|
|
28
|
+
overscan: number,
|
|
29
|
+
): VirtualWindow {
|
|
30
|
+
const h = Math.max(1, rowHeight);
|
|
31
|
+
const first = Math.floor(Math.max(0, scrollTop) / h);
|
|
32
|
+
const visible = Math.ceil(viewport / h) + 1;
|
|
33
|
+
const start = Math.max(0, Math.min(count, first - overscan));
|
|
34
|
+
const end = Math.max(start, Math.min(count, first + visible + overscan));
|
|
35
|
+
return { start, end, rowHeight: h };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Row virtualization for the DataTable: listens to the scroll container of the table and returns
|
|
40
|
+
* the window of items to render, or `undefined` when disabled.
|
|
41
|
+
*/
|
|
42
|
+
export function useVirtualRows({
|
|
43
|
+
enabled,
|
|
44
|
+
count,
|
|
45
|
+
rowHeight,
|
|
46
|
+
tableRef,
|
|
47
|
+
viewportHeight,
|
|
48
|
+
overscan = 8,
|
|
49
|
+
}: UseVirtualRowsOptions): VirtualWindow | undefined {
|
|
50
|
+
const [scroll, setScroll] = useState({ top: 0, height: viewportHeight });
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const el = tableRef.current?.parentElement;
|
|
54
|
+
if (!enabled || !el) return undefined;
|
|
55
|
+
let frame = 0;
|
|
56
|
+
const read = () => {
|
|
57
|
+
frame = 0;
|
|
58
|
+
setScroll((prev) => {
|
|
59
|
+
const height = el.clientHeight || prev.height;
|
|
60
|
+
return prev.top === el.scrollTop && prev.height === height ? prev : { top: el.scrollTop, height };
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const schedule = () => {
|
|
64
|
+
if (frame) return;
|
|
65
|
+
frame =
|
|
66
|
+
typeof requestAnimationFrame === 'function'
|
|
67
|
+
? requestAnimationFrame(read)
|
|
68
|
+
: (setTimeout(read, 0) as unknown as number);
|
|
69
|
+
};
|
|
70
|
+
el.addEventListener('scroll', schedule, { passive: true });
|
|
71
|
+
const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
72
|
+
ro?.observe(el);
|
|
73
|
+
schedule();
|
|
74
|
+
return () => {
|
|
75
|
+
el.removeEventListener('scroll', schedule);
|
|
76
|
+
ro?.disconnect();
|
|
77
|
+
if (frame && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(frame);
|
|
78
|
+
};
|
|
79
|
+
}, [enabled, tableRef]);
|
|
80
|
+
|
|
81
|
+
if (!enabled) return undefined;
|
|
82
|
+
return windowFor(count, scroll.top, scroll.height, rowHeight, overscan);
|
|
83
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Bookmark, ChevronDown } from '@gntik-ai/icons';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogBody,
|
|
6
|
+
DialogClose,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
Input,
|
|
12
|
+
Menu,
|
|
13
|
+
MenuContent,
|
|
14
|
+
MenuGroup,
|
|
15
|
+
MenuGroupLabel,
|
|
16
|
+
MenuItem,
|
|
17
|
+
MenuRadioGroup,
|
|
18
|
+
MenuRadioItem,
|
|
19
|
+
MenuSeparator,
|
|
20
|
+
MenuTrigger,
|
|
21
|
+
} from '@gntik-ai/ui';
|
|
22
|
+
import { useId, useState } from 'react';
|
|
23
|
+
import type { ColumnWidths, PinnedColumns } from './column-sizing';
|
|
24
|
+
import type { SortState } from './data-table-utils';
|
|
25
|
+
|
|
26
|
+
/** What a saved view captures. Every field is optional: a missing field keeps the table's default. */
|
|
27
|
+
export interface DataTableViewState {
|
|
28
|
+
/** Opaque filter state of the page's toolbar (see the table's `filters` / `onFiltersChange`). */
|
|
29
|
+
filters?: unknown;
|
|
30
|
+
sort?: SortState | null;
|
|
31
|
+
hiddenColumns?: readonly string[];
|
|
32
|
+
columnWidths?: ColumnWidths;
|
|
33
|
+
pinnedColumns?: PinnedColumns;
|
|
34
|
+
/** A field key, or `null` for no grouping (function `groupBy`s cannot be saved). */
|
|
35
|
+
groupBy?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DataTableView {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
state: DataTableViewState;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const norm = (s: DataTableViewState) =>
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
filters: s.filters ?? null,
|
|
47
|
+
sort: s.sort ?? null,
|
|
48
|
+
hiddenColumns: [...(s.hiddenColumns ?? [])].sort(),
|
|
49
|
+
columnWidths: Object.fromEntries(Object.entries(s.columnWidths ?? {}).sort(([a], [b]) => a.localeCompare(b))),
|
|
50
|
+
pinnedColumns: { left: s.pinnedColumns?.left ?? [], right: s.pinnedColumns?.right ?? [] },
|
|
51
|
+
groupBy: s.groupBy ?? null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Whether two view states are equivalent (key order and empty values ignored). */
|
|
55
|
+
export function sameViewState(a: DataTableViewState, b: DataTableViewState): boolean {
|
|
56
|
+
return norm(a) === norm(b);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A new view id that does not collide with `views`. */
|
|
60
|
+
export function newViewId(name: string, views: readonly DataTableView[]): string {
|
|
61
|
+
const base =
|
|
62
|
+
name
|
|
63
|
+
.trim()
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
66
|
+
.replace(/^-|-$/g, '') || 'view';
|
|
67
|
+
let id = base;
|
|
68
|
+
for (let i = 2; views.some((v) => v.id === id); i++) id = `${base}-${i}`;
|
|
69
|
+
return id;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ViewSwitcherProps {
|
|
73
|
+
views: readonly DataTableView[];
|
|
74
|
+
activeId: string | null;
|
|
75
|
+
/** The table differs from the active view (or from the defaults when none is active). */
|
|
76
|
+
dirty: boolean;
|
|
77
|
+
onSelect: (id: string | null) => void;
|
|
78
|
+
onSaveAs: (name: string) => void;
|
|
79
|
+
onSave: () => void;
|
|
80
|
+
onRename: (id: string, name: string) => void;
|
|
81
|
+
onDelete: (id: string) => void;
|
|
82
|
+
onReset: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DEFAULT_VALUE = '__default__';
|
|
86
|
+
|
|
87
|
+
/** "Views" menu: switch between saved views, save the current table as a view, rename, delete, reset. */
|
|
88
|
+
export function ViewSwitcher({
|
|
89
|
+
views,
|
|
90
|
+
activeId,
|
|
91
|
+
dirty,
|
|
92
|
+
onSelect,
|
|
93
|
+
onSaveAs,
|
|
94
|
+
onSave,
|
|
95
|
+
onRename,
|
|
96
|
+
onDelete,
|
|
97
|
+
onReset,
|
|
98
|
+
}: ViewSwitcherProps) {
|
|
99
|
+
const inputId = useId();
|
|
100
|
+
const active = views.find((v) => v.id === activeId) ?? null;
|
|
101
|
+
const [dialog, setDialog] = useState<'create' | 'rename' | null>(null);
|
|
102
|
+
const [name, setName] = useState('');
|
|
103
|
+
const open = (mode: 'create' | 'rename') => {
|
|
104
|
+
setName(mode === 'rename' && active ? active.name : '');
|
|
105
|
+
setDialog(mode);
|
|
106
|
+
};
|
|
107
|
+
const submit = () => {
|
|
108
|
+
const trimmed = name.trim();
|
|
109
|
+
if (!trimmed) return;
|
|
110
|
+
if (dialog === 'rename' && active) onRename(active.id, trimmed);
|
|
111
|
+
else onSaveAs(trimmed);
|
|
112
|
+
setDialog(null);
|
|
113
|
+
};
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<Menu>
|
|
117
|
+
<MenuTrigger render={<Button variant="secondary" size="sm" icon={Bookmark} trailingIcon={ChevronDown} />}>
|
|
118
|
+
<span className="max-w-40 truncate">{active ? active.name : 'Default view'}</span>
|
|
119
|
+
{dirty && (
|
|
120
|
+
<>
|
|
121
|
+
{' '}
|
|
122
|
+
<span aria-hidden className="size-1.5 rounded-full bg-primary" />{' '}
|
|
123
|
+
<span className="sr-only">(modified)</span>
|
|
124
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
</MenuTrigger>
|
|
127
|
+
<MenuContent align="end" className="min-w-[220px]">
|
|
128
|
+
<MenuGroup>
|
|
129
|
+
<MenuGroupLabel>Views</MenuGroupLabel>
|
|
130
|
+
<MenuRadioGroup
|
|
131
|
+
value={activeId ?? DEFAULT_VALUE}
|
|
132
|
+
onValueChange={(v: unknown) => onSelect(v === DEFAULT_VALUE ? null : String(v))}
|
|
133
|
+
>
|
|
134
|
+
<MenuRadioItem value={DEFAULT_VALUE}>Default view</MenuRadioItem>
|
|
135
|
+
{views.map((v) => (
|
|
136
|
+
<MenuRadioItem key={v.id} value={v.id}>
|
|
137
|
+
{v.name}
|
|
138
|
+
</MenuRadioItem>
|
|
139
|
+
))}
|
|
140
|
+
</MenuRadioGroup>
|
|
141
|
+
</MenuGroup>
|
|
142
|
+
<MenuSeparator />
|
|
143
|
+
<MenuItem disabled={!active || !dirty} onClick={onSave}>
|
|
144
|
+
Save changes
|
|
145
|
+
</MenuItem>
|
|
146
|
+
<MenuItem onClick={() => open('create')}>Save as new view…</MenuItem>
|
|
147
|
+
<MenuItem disabled={!active} onClick={() => open('rename')}>
|
|
148
|
+
Rename view…
|
|
149
|
+
</MenuItem>
|
|
150
|
+
<MenuItem disabled={!active} onClick={() => active && onDelete(active.id)}>
|
|
151
|
+
Delete view
|
|
152
|
+
</MenuItem>
|
|
153
|
+
<MenuItem disabled={!dirty} onClick={onReset}>
|
|
154
|
+
Reset changes
|
|
155
|
+
</MenuItem>
|
|
156
|
+
</MenuContent>
|
|
157
|
+
</Menu>
|
|
158
|
+
<Dialog open={dialog != null} onOpenChange={(o) => !o && setDialog(null)}>
|
|
159
|
+
<DialogContent size="sm">
|
|
160
|
+
<form
|
|
161
|
+
onSubmit={(e) => {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
submit();
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<DialogHeader>
|
|
167
|
+
<DialogTitle>{dialog === 'rename' ? 'Rename view' : 'Save view'}</DialogTitle>
|
|
168
|
+
</DialogHeader>
|
|
169
|
+
<DialogBody>
|
|
170
|
+
<label htmlFor={inputId} className="mb-1.5 block text-[12.5px] font-medium text-foreground">
|
|
171
|
+
View name
|
|
172
|
+
</label>
|
|
173
|
+
<Input id={inputId} value={name} onChange={(e) => setName(e.target.value)} autoComplete="off" required />
|
|
174
|
+
</DialogBody>
|
|
175
|
+
<DialogFooter>
|
|
176
|
+
<DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
|
|
177
|
+
<Button type="submit" disabled={!name.trim()}>
|
|
178
|
+
{dialog === 'rename' ? 'Rename' : 'Save view'}
|
|
179
|
+
</Button>
|
|
180
|
+
</DialogFooter>
|
|
181
|
+
</form>
|
|
182
|
+
</DialogContent>
|
|
183
|
+
</Dialog>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|