@scalepad/ui 0.1.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/.ai/rules/date-handling.md +39 -0
- package/.ai/rules/figma-design-system.md +372 -0
- package/.ai/rules/figma-lm-design-system-keys.md +680 -0
- package/.ai/rules/file-extensions.md +13 -0
- package/.ai/rules/modal-confirmation-mutation.md +56 -0
- package/.ai/rules/react-hooks.md +29 -0
- package/.ai/rules/styling.md +83 -0
- package/AGENTS.md +37 -0
- package/README.md +125 -0
- package/figma.config.json +9 -0
- package/package.json +127 -0
- package/scripts/install-ai-rules.mjs +136 -0
- package/src/ThemeProvider.tsx +57 -0
- package/src/charts.ts +32 -0
- package/src/components/ActionCard/ActionCard.css.ts +60 -0
- package/src/components/ActionCard/ActionCard.tsx +154 -0
- package/src/components/ActionCard/index.ts +2 -0
- package/src/components/Anchor/Anchor.tsx +47 -0
- package/src/components/Anchor/index.ts +2 -0
- package/src/components/AppliedFiltersManagerBar/AppliedFiltersManagerBar.tsx +105 -0
- package/src/components/AppliedFiltersManagerBar/FilterBadge.css.ts +23 -0
- package/src/components/AppliedFiltersManagerBar/FilterBadge.tsx +50 -0
- package/src/components/AppliedFiltersManagerBar/index.ts +5 -0
- package/src/components/Badge/Badge.css.ts +72 -0
- package/src/components/Badge/Badge.figma.tsx +43 -0
- package/src/components/Badge/Badge.tsx +159 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BreadCrumb/BreadCrumb.tsx +62 -0
- package/src/components/BreadCrumb/index.ts +2 -0
- package/src/components/BulkActionBar/BulkActionBar.css.ts +26 -0
- package/src/components/BulkActionBar/BulkActionBar.tsx +164 -0
- package/src/components/BulkActionBar/index.ts +2 -0
- package/src/components/Button/Button.css.ts +272 -0
- package/src/components/Button/Button.figma.tsx +74 -0
- package/src/components/Button/Button.tsx +84 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Charts/ChartTooltip.figma.tsx +33 -0
- package/src/components/Charts/ChartTooltip.tsx +101 -0
- package/src/components/Charts/MiniBarSparkline.tsx +75 -0
- package/src/components/Charts/StackedPatternBarChart.tsx +494 -0
- package/src/components/Charts/TrendAreaChart.css.ts +23 -0
- package/src/components/Charts/TrendAreaChart.tsx +210 -0
- package/src/components/Charts/index.ts +12 -0
- package/src/components/CodePanel/CodePanel.css.ts +113 -0
- package/src/components/CodePanel/CodePanel.tsx +121 -0
- package/src/components/CodePanel/index.ts +2 -0
- package/src/components/CommentComposer/CommentComposer.css.ts +60 -0
- package/src/components/CommentComposer/CommentComposer.tsx +181 -0
- package/src/components/CommentComposer/index.ts +2 -0
- package/src/components/ConfirmationModal/ConfirmationModal.tsx +149 -0
- package/src/components/ConfirmationModal/index.ts +2 -0
- package/src/components/ConfirmationTooltip/ConfirmationTooltip.tsx +132 -0
- package/src/components/ConfirmationTooltip/index.ts +2 -0
- package/src/components/DataDialog.figma.tsx +33 -0
- package/src/components/DataDialog.tsx +46 -0
- package/src/components/DataTable/DataTable.tsx +1042 -0
- package/src/components/DataTable/RowExpandToggle.tsx +105 -0
- package/src/components/DataTable/RowGroupHeader.tsx +190 -0
- package/src/components/DataTable/createActionsColumn.tsx +86 -0
- package/src/components/DataTable/index.ts +25 -0
- package/src/components/DatePicker/CustomRangePicker.tsx +59 -0
- package/src/components/DatePicker/DateInput.tsx +329 -0
- package/src/components/DatePicker/DateNavigator.tsx +486 -0
- package/src/components/DatePicker/DatePicker.tsx +242 -0
- package/src/components/DatePicker/MonthlyRangePicker.tsx +231 -0
- package/src/components/DatePicker/QuarterlyRangePicker.tsx +224 -0
- package/src/components/DatePicker/QuickPicksSidebar.tsx +242 -0
- package/src/components/DatePicker/YearlyRangePicker.tsx +171 -0
- package/src/components/DatePicker/index.ts +7 -0
- package/src/components/DatePicker/types.ts +12 -0
- package/src/components/DesignSystemPrimitives/FluidGrid.tsx +44 -0
- package/src/components/DesignSystemPrimitives/InteractivePrimitives.tsx +177 -0
- package/src/components/DesignSystemPrimitives/LayoutPrimitives.tsx +220 -0
- package/src/components/DesignSystemPrimitives/LayoutPrimitives.types.tsx +15 -0
- package/src/components/DesignSystemPrimitives/SurfacePrimitives.tsx +46 -0
- package/src/components/DesignSystemPrimitives/index.ts +55 -0
- package/src/components/Details/Details.css.ts +74 -0
- package/src/components/Details/Details.tsx +140 -0
- package/src/components/Details/index.ts +2 -0
- package/src/components/DownloadCard/DownloadCard.css.ts +22 -0
- package/src/components/DownloadCard/DownloadCard.tsx +63 -0
- package/src/components/DownloadCard/index.ts +2 -0
- package/src/components/Drawer/Drawer.css.ts +32 -0
- package/src/components/Drawer/Drawer.tsx +236 -0
- package/src/components/Drawer/hooks/useDetailDrawer.ts +61 -0
- package/src/components/Drawer/hooks/useDetailDrawerNavigation.ts +125 -0
- package/src/components/Drawer/hooks/useDetailDrawerNavigationContext.ts +66 -0
- package/src/components/EditableRichText/EditableRichText.css.ts +72 -0
- package/src/components/EditableRichText/EditableRichText.tsx +324 -0
- package/src/components/EditableRichText/index.ts +2 -0
- package/src/components/EditableSelect/EditableSelect.css.ts +62 -0
- package/src/components/EditableSelect/EditableSelect.tsx +224 -0
- package/src/components/EditableSelect/index.ts +2 -0
- package/src/components/EditableText/EditableText.tsx +377 -0
- package/src/components/EditableText/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.figma.tsx +33 -0
- package/src/components/EmptyState/EmptyState.tsx +230 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/ErrorBoundary.tsx +135 -0
- package/src/components/ErrorState/ErrorState.tsx +197 -0
- package/src/components/ErrorState/index.ts +2 -0
- package/src/components/FeatureCard.tsx +42 -0
- package/src/components/FilterMenu/FilterMenu.figma.tsx +30 -0
- package/src/components/FilterMenu/FilterMenu.tsx +198 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/BooleanFilterSubmenu.tsx +46 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/SearchableFilterSubmenu.tsx +239 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/index.ts +8 -0
- package/src/components/FilterMenu/defaultFilterSchemas.ts +63 -0
- package/src/components/FilterMenu/helpers.ts +115 -0
- package/src/components/FilterMenu/index.ts +35 -0
- package/src/components/FilterMenu/types.ts +101 -0
- package/src/components/IconButton/IconButton.css.ts +272 -0
- package/src/components/IconButton/IconButton.figma.tsx +47 -0
- package/src/components/IconButton/IconButton.tsx +72 -0
- package/src/components/IconButton/README.md +230 -0
- package/src/components/IconButton/index.ts +2 -0
- package/src/components/InfiniteScrollSentinel.tsx +86 -0
- package/src/components/InfiniteScrollTrigger.tsx +78 -0
- package/src/components/InfoCard.figma.tsx +47 -0
- package/src/components/InfoCard.tsx +216 -0
- package/src/components/KbdHint/KbdHint.tsx +23 -0
- package/src/components/KbdHint/index.ts +2 -0
- package/src/components/LabeledField/LabeledField.tsx +21 -0
- package/src/components/LabeledField/index.ts +2 -0
- package/src/components/LookupSelect/LookupSelect.css.ts +149 -0
- package/src/components/LookupSelect/LookupSelect.tsx +325 -0
- package/src/components/LookupSelect/index.ts +2 -0
- package/src/components/Menu/Menu.css.ts +89 -0
- package/src/components/Menu/Menu.tsx +105 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/MessageBox/MessageBox.tsx +168 -0
- package/src/components/MessageBox/index.ts +2 -0
- package/src/components/MetricDisplay/MetricDisplay.tsx +55 -0
- package/src/components/MetricDisplay/index.ts +1 -0
- package/src/components/MultiSelect/MultiSelect.tsx +278 -0
- package/src/components/MultiSelect/index.ts +2 -0
- package/src/components/Notifications/Notifications.tsx +12 -0
- package/src/components/Notifications/README.md +93 -0
- package/src/components/Notifications/index.ts +4 -0
- package/src/components/Notifications/showToast.tsx +100 -0
- package/src/components/PropertyRow/PropertyRow.tsx +96 -0
- package/src/components/PropertyRow/index.ts +2 -0
- package/src/components/RadioTile/RadioTile.tsx +253 -0
- package/src/components/RadioTile/index.ts +2 -0
- package/src/components/RichText/FormattingToolbar.css.ts +69 -0
- package/src/components/RichText/FormattingToolbar.tsx +112 -0
- package/src/components/RichText/RichTextInline.css.ts +54 -0
- package/src/components/RichText/RichTextInline.tsx +318 -0
- package/src/components/RichText/formattingCommands.ts +181 -0
- package/src/components/RichText/formattingTypes.ts +34 -0
- package/src/components/RichText/index.ts +49 -0
- package/src/components/RichText/richTextExtensions.ts +111 -0
- package/src/components/RichText/richTextHelpers.ts +65 -0
- package/src/components/RichText/richTextImage.ts +253 -0
- package/src/components/RichText/richTextImageHandlers.ts +244 -0
- package/src/components/RichText/richTextProse.css.ts +261 -0
- package/src/components/RichTextEditor/RichTextEditor.css.ts +82 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +204 -0
- package/src/components/RichTextEditor/index.ts +2 -0
- package/src/components/RichTextView/RichTextView.css.ts +11 -0
- package/src/components/RichTextView/RichTextView.tsx +114 -0
- package/src/components/RichTextView/index.ts +2 -0
- package/src/components/Schedule/Schedule.tsx +35 -0
- package/src/components/SchedulePicker/SchedulePicker.css.ts +42 -0
- package/src/components/SchedulePicker/SchedulePicker.tsx +130 -0
- package/src/components/SchedulePicker/index.ts +2 -0
- package/src/components/SearchableList/types.ts +30 -0
- package/src/components/SearchableSubMenu/SearchableSubMenu.css.ts +25 -0
- package/src/components/SearchableSubMenu/SearchableSubMenu.tsx +139 -0
- package/src/components/SearchableSubMenu/index.ts +2 -0
- package/src/components/Select/README.md +114 -0
- package/src/components/Select/Select.css.ts +110 -0
- package/src/components/Select/Select.tsx +133 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/SelectCreatable/SelectCreatable.css.ts +16 -0
- package/src/components/SelectCreatable/SelectCreatable.tsx +203 -0
- package/src/components/SelectCreatable/index.ts +2 -0
- package/src/components/SettingsCard/SettingsCard.tsx +98 -0
- package/src/components/SettingsCard/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.css.ts +91 -0
- package/src/components/Sidebar/Sidebar.tsx +129 -0
- package/src/components/Sidebar/index.ts +5 -0
- package/src/components/SimpleList/SimpleList.css.ts +12 -0
- package/src/components/SimpleList/SimpleList.tsx +44 -0
- package/src/components/SimpleList/index.ts +2 -0
- package/src/components/SimpleTable/SimpleTable.tsx +296 -0
- package/src/components/SimpleTable/index.ts +2 -0
- package/src/components/SlashRichTextEditor/SelectionBubbleMenu.css.ts +62 -0
- package/src/components/SlashRichTextEditor/SelectionBubbleMenu.tsx +85 -0
- package/src/components/SlashRichTextEditor/SlashCommandMenu.css.ts +124 -0
- package/src/components/SlashRichTextEditor/SlashCommandMenu.tsx +168 -0
- package/src/components/SlashRichTextEditor/SlashRichTextEditor.css.ts +81 -0
- package/src/components/SlashRichTextEditor/SlashRichTextEditor.tsx +538 -0
- package/src/components/SlashRichTextEditor/SlashSuggestionExtension.ts +48 -0
- package/src/components/SlashRichTextEditor/index.ts +13 -0
- package/src/components/SlashRichTextEditor/types.ts +48 -0
- package/src/components/StatCard/StatCard.css.ts +70 -0
- package/src/components/StatCard/StatCard.tsx +201 -0
- package/src/components/StatCard/index.ts +1 -0
- package/src/components/StatusBadge/StatusBadge.tsx +70 -0
- package/src/components/StatusBadge/index.ts +2 -0
- package/src/components/StatusIndicator/StatusIndicator.tsx +67 -0
- package/src/components/StatusIndicator/index.ts +6 -0
- package/src/components/SubNavigation/SubNavigation.css.ts +72 -0
- package/src/components/SubNavigation/SubNavigation.tsx +104 -0
- package/src/components/SubNavigation/index.ts +2 -0
- package/src/components/SuspenseLoader.tsx +22 -0
- package/src/components/Table/SortableColumnHeader.tsx +99 -0
- package/src/components/Table/TableSkeletonRows.figma.tsx +22 -0
- package/src/components/Table/TableSkeletonRows.tsx +113 -0
- package/src/components/Table/index.ts +9 -0
- package/src/components/TableActionsMenu.tsx +58 -0
- package/src/components/TableCard.tsx +29 -0
- package/src/components/TableContainer/TableContainer.tsx +86 -0
- package/src/components/TableContainer/index.ts +2 -0
- package/src/components/TableControlBar/TableControlBar.tsx +156 -0
- package/src/components/TableControlBar/TableSelectionButton.tsx +57 -0
- package/src/components/TableControlBar/index.ts +13 -0
- package/src/components/TableControlBar/useTableControlBar.tsx +314 -0
- package/src/components/TableSelection/TableSelection.tsx +43 -0
- package/src/components/TableSelection/index.ts +5 -0
- package/src/components/Tabs/README.md +76 -0
- package/src/components/Tabs/Tabs.css.ts +54 -0
- package/src/components/Tabs/Tabs.figma.tsx +47 -0
- package/src/components/Tabs/Tabs.tsx +96 -0
- package/src/components/Tabs/index.ts +8 -0
- package/src/components/TextInput/README.md +98 -0
- package/src/components/TextInput/SearchTextInput.figma.tsx +22 -0
- package/src/components/TextInput/SearchTextInput.tsx +150 -0
- package/src/components/TextInput/TextInput.figma.tsx +44 -0
- package/src/components/TextInput/TextInput.tsx +42 -0
- package/src/components/TextInput/index.ts +4 -0
- package/src/components/ThemeSwitcher.figma.tsx +28 -0
- package/src/components/ThemeSwitcher.tsx +69 -0
- package/src/components/TrendBadge/TrendBadge.tsx +76 -0
- package/src/components/TrendBadge/index.ts +2 -0
- package/src/components/TruncatedText.tsx +115 -0
- package/src/components/Typography/Text.tsx +74 -0
- package/src/components/Typography/Title.tsx +100 -0
- package/src/components/Typography/index.ts +4 -0
- package/src/geist-fonts.ts +48 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/useFilters.ts +152 -0
- package/src/hooks/useInfiniteScroll.ts +62 -0
- package/src/hooks/usePlatform.ts +33 -0
- package/src/hooks/useServerTable.ts +495 -0
- package/src/hooks/useTableSelection.ts +102 -0
- package/src/hooks/useTableSort.ts +259 -0
- package/src/index.ts +483 -0
- package/src/mantine.ts +25 -0
- package/src/theme/mantineVars.ts +12 -0
- package/src/theme/themeContract.css.ts +131 -0
- package/src/theme/themeVars.ts +31 -0
- package/src/theme.ts +168 -0
- package/src/tokens/color-types.ts +107 -0
- package/src/tokens/colors.ts +243 -0
- package/src/tokens/index.ts +14 -0
- package/src/tokens/radius.ts +17 -0
- package/src/tokens/semantic-colors.ts +224 -0
- package/src/tokens/semantic-tokens-css.ts +53 -0
- package/src/tokens/shadows.ts +11 -0
- package/src/tokens/spacing.ts +20 -0
- package/src/tokens/text-styles.ts +179 -0
- package/src/tokens/typography.ts +40 -0
- package/src/tokens/zIndex.ts +27 -0
- package/src/types/mantine-theme.d.ts +17 -0
- package/src/types/tanstack-table.d.ts +22 -0
- package/src/utils/avatar.ts +150 -0
- package/src/utils/chartHelpers.ts +53 -0
- package/src/utils/color-props.ts +77 -0
- package/src/utils/createDesignComponent.tsx +104 -0
- package/src/utils/nestFlatRows.ts +111 -0
- package/src/utils/withStaticComponents.ts +6 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateNavigator Component
|
|
3
|
+
*
|
|
4
|
+
* TIMEZONE SAFETY:
|
|
5
|
+
* This component handles ISO date strings (e.g., "2026-01-25") from URL params.
|
|
6
|
+
* All date parsing uses toDate() from @scalepad/ui-utils/date which leverages dayjs to:
|
|
7
|
+
* - Parse ISO date strings as local timezone midnight (not UTC)
|
|
8
|
+
* - Avoid off-by-one day errors caused by timezone offsets
|
|
9
|
+
* - Maintain consistency: "2026-01-25" → parse → calculate → format → "2026-01-25"
|
|
10
|
+
*
|
|
11
|
+
* All date utilities (addMonths, startOfMonth, etc.) use dayjs internally,
|
|
12
|
+
* ensuring timezone-safe calculations throughout the component.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
Calendar,
|
|
18
|
+
ChevronDown,
|
|
19
|
+
ChevronLeft,
|
|
20
|
+
ChevronRight,
|
|
21
|
+
X,
|
|
22
|
+
} from 'lucide-react';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
Box,
|
|
26
|
+
Flex,
|
|
27
|
+
Overlay,
|
|
28
|
+
Popover,
|
|
29
|
+
Portal,
|
|
30
|
+
UnstyledButton,
|
|
31
|
+
type PopoverProps,
|
|
32
|
+
} from '@mantine/core';
|
|
33
|
+
import type { DateValue } from '@mantine/dates';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
addMonths,
|
|
37
|
+
addYears,
|
|
38
|
+
DateFormat,
|
|
39
|
+
endOfMonth,
|
|
40
|
+
endOfYear,
|
|
41
|
+
formatDate,
|
|
42
|
+
getCurrentDate,
|
|
43
|
+
getCurrentQuarter,
|
|
44
|
+
getNextQuarter,
|
|
45
|
+
getPreviousQuarter,
|
|
46
|
+
getQuarterRange,
|
|
47
|
+
isAfter,
|
|
48
|
+
startOfMonth,
|
|
49
|
+
startOfYear,
|
|
50
|
+
toDate,
|
|
51
|
+
} from '@scalepad/ui-utils/date';
|
|
52
|
+
|
|
53
|
+
import { zIndex } from '../../tokens';
|
|
54
|
+
import { Button, type ButtonProps } from '../Button';
|
|
55
|
+
import { IconButton } from '../IconButton';
|
|
56
|
+
import { Title } from '../Typography';
|
|
57
|
+
import { DatePicker } from './DatePicker';
|
|
58
|
+
|
|
59
|
+
import type { DatePickerGranularity, DateRange } from './types';
|
|
60
|
+
|
|
61
|
+
export interface DateNavigatorProps {
|
|
62
|
+
startDate: DateValue;
|
|
63
|
+
endDate: DateValue;
|
|
64
|
+
granularity: DatePickerGranularity;
|
|
65
|
+
onDateChange: (range: DateRange, granularity: DatePickerGranularity) => void;
|
|
66
|
+
/** Component variant: 'navigation' (with arrows) or 'button' (standalone button) */
|
|
67
|
+
variant?: 'navigation' | 'button';
|
|
68
|
+
/** Allowed range types to show in picker. If not specified, all types are allowed. */
|
|
69
|
+
allowedRangeTypes?: DatePickerGranularity[];
|
|
70
|
+
className?: string;
|
|
71
|
+
/** Show prev/next navigation buttons (only for 'navigation' variant) - defaults to true */
|
|
72
|
+
showNavigation?: boolean;
|
|
73
|
+
/** Show calendar icon beside the date - defaults to true */
|
|
74
|
+
showCalendarIcon?: boolean;
|
|
75
|
+
/** Show dropdown icon (only for 'button' variant) - defaults to true */
|
|
76
|
+
showDropdownIcon?: boolean;
|
|
77
|
+
/** Popover position - defaults to 'bottom-start' */
|
|
78
|
+
popoverPosition?: PopoverProps['position'];
|
|
79
|
+
/** Date format: 'short' (e.g., "Jan 2026") or 'long' (e.g., "January 2026") */
|
|
80
|
+
dateFormat?: 'short' | 'long';
|
|
81
|
+
/** Maximum historical date that can be selected */
|
|
82
|
+
maxDataHistory?: DateValue;
|
|
83
|
+
/** Button variant (only for 'button' variant) */
|
|
84
|
+
buttonVariant?: ButtonProps['variant'];
|
|
85
|
+
/** Button width (only for 'button' variant) */
|
|
86
|
+
buttonWidth?: string | number;
|
|
87
|
+
/** Button height (only for 'button' variant) */
|
|
88
|
+
buttonHeight?: string | number;
|
|
89
|
+
/** Show loading spinner inside the button (only for 'button' variant) */
|
|
90
|
+
loading?: boolean;
|
|
91
|
+
/** Custom right section content (only for 'button' variant) - overrides dropdown icon */
|
|
92
|
+
rightSection?: React.ReactNode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Navigation strategies for different range types
|
|
97
|
+
* Extracted to avoid duplication and make logic testable
|
|
98
|
+
*/
|
|
99
|
+
type NavigationStrategy = {
|
|
100
|
+
getLabel: (start: Date, end: Date, format: 'short' | 'long') => string;
|
|
101
|
+
navigatePrevious: (start: Date, end: Date) => DateRange;
|
|
102
|
+
navigateNext: (start: Date, end: Date) => DateRange;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const navigationStrategies: Record<DatePickerGranularity, NavigationStrategy> =
|
|
106
|
+
{
|
|
107
|
+
monthly: {
|
|
108
|
+
getLabel: (start, _end, format) =>
|
|
109
|
+
formatDate(
|
|
110
|
+
start,
|
|
111
|
+
format === 'long'
|
|
112
|
+
? DateFormat.MONTH_YEAR_LONG
|
|
113
|
+
: DateFormat.MONTH_YEAR,
|
|
114
|
+
),
|
|
115
|
+
navigatePrevious: (start, _end) => {
|
|
116
|
+
const prev = addMonths(start, -1);
|
|
117
|
+
return {
|
|
118
|
+
startDate: startOfMonth(prev),
|
|
119
|
+
endDate: endOfMonth(prev),
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
navigateNext: (start, _end) => {
|
|
123
|
+
const next = addMonths(start, 1);
|
|
124
|
+
return {
|
|
125
|
+
startDate: startOfMonth(next),
|
|
126
|
+
endDate: endOfMonth(next),
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
quarterly: {
|
|
131
|
+
getLabel: (start, _end, _format) => {
|
|
132
|
+
// Quarterly format doesn't change with format option
|
|
133
|
+
const q = getCurrentQuarter(start);
|
|
134
|
+
return `Q${q.quarter + 1} ${q.year}`;
|
|
135
|
+
},
|
|
136
|
+
navigatePrevious: (start, _end) => {
|
|
137
|
+
const prevQ = getPreviousQuarter(start);
|
|
138
|
+
const range = getQuarterRange(prevQ.year, prevQ.quarter);
|
|
139
|
+
return {
|
|
140
|
+
startDate: range.start,
|
|
141
|
+
endDate: range.end,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
navigateNext: (start, _end) => {
|
|
145
|
+
const nextQ = getNextQuarter(start);
|
|
146
|
+
const range = getQuarterRange(nextQ.year, nextQ.quarter);
|
|
147
|
+
return {
|
|
148
|
+
startDate: range.start,
|
|
149
|
+
endDate: range.end,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
yearly: {
|
|
154
|
+
getLabel: (start, _end, _format) => formatDate(start, DateFormat.YEAR),
|
|
155
|
+
navigatePrevious: (start, _end) => {
|
|
156
|
+
const prev = addYears(start, -1);
|
|
157
|
+
return {
|
|
158
|
+
startDate: startOfYear(prev),
|
|
159
|
+
endDate: endOfYear(prev),
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
navigateNext: (start, _end) => {
|
|
163
|
+
const next = addYears(start, 1);
|
|
164
|
+
return {
|
|
165
|
+
startDate: startOfYear(next),
|
|
166
|
+
endDate: endOfYear(next),
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
custom: {
|
|
171
|
+
getLabel: (start, end, _format) => {
|
|
172
|
+
// Custom range format doesn't change with format option
|
|
173
|
+
const currentYear = getCurrentDate().getFullYear();
|
|
174
|
+
const includeYear =
|
|
175
|
+
start.getFullYear() !== currentYear ||
|
|
176
|
+
end.getFullYear() !== currentYear;
|
|
177
|
+
|
|
178
|
+
if (includeYear) {
|
|
179
|
+
return `${formatDate(start, DateFormat.SHORT_NO_COMMA)} - ${formatDate(
|
|
180
|
+
end,
|
|
181
|
+
DateFormat.SHORT_NO_COMMA,
|
|
182
|
+
)}`;
|
|
183
|
+
}
|
|
184
|
+
return `${formatDate(start, DateFormat.SHORT_NO_YEAR)} - ${formatDate(
|
|
185
|
+
end,
|
|
186
|
+
DateFormat.SHORT_NO_YEAR,
|
|
187
|
+
)}`;
|
|
188
|
+
},
|
|
189
|
+
navigatePrevious: (start, end) => {
|
|
190
|
+
// Calculate duration and create new dates from timestamps
|
|
191
|
+
// Timestamps are timezone-safe: getTime() returns UTC ms, Date constructor handles conversion
|
|
192
|
+
const duration = end.getTime() - start.getTime();
|
|
193
|
+
return {
|
|
194
|
+
startDate: new Date(start.getTime() - duration),
|
|
195
|
+
endDate: new Date(start.getTime()),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
navigateNext: (start, end) => {
|
|
199
|
+
// Calculate duration and create new dates from timestamps
|
|
200
|
+
// Timestamps are timezone-safe: getTime() returns UTC ms, Date constructor handles conversion
|
|
201
|
+
const duration = end.getTime() - start.getTime();
|
|
202
|
+
return {
|
|
203
|
+
startDate: new Date(end.getTime()),
|
|
204
|
+
endDate: new Date(end.getTime() + duration),
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export function DateNavigator({
|
|
211
|
+
startDate,
|
|
212
|
+
endDate,
|
|
213
|
+
granularity,
|
|
214
|
+
onDateChange,
|
|
215
|
+
variant = 'navigation',
|
|
216
|
+
allowedRangeTypes,
|
|
217
|
+
className,
|
|
218
|
+
showNavigation = true,
|
|
219
|
+
showCalendarIcon = true,
|
|
220
|
+
showDropdownIcon = true,
|
|
221
|
+
popoverPosition = 'bottom-start',
|
|
222
|
+
dateFormat = 'short',
|
|
223
|
+
maxDataHistory,
|
|
224
|
+
buttonVariant = 'outline',
|
|
225
|
+
buttonWidth = 170,
|
|
226
|
+
buttonHeight = 36,
|
|
227
|
+
loading = false,
|
|
228
|
+
rightSection,
|
|
229
|
+
}: DateNavigatorProps) {
|
|
230
|
+
const [opened, setOpened] = useState(false);
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (opened) {
|
|
234
|
+
document.body.style.overflow = 'hidden';
|
|
235
|
+
return () => {
|
|
236
|
+
document.body.style.overflow = '';
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return undefined;
|
|
241
|
+
}, [opened]);
|
|
242
|
+
|
|
243
|
+
// Temporary selection state while picker is open (before Apply is clicked)
|
|
244
|
+
const [tempStartDate, setTempStartDate] = useState<DateValue>(startDate);
|
|
245
|
+
const [tempEndDate, setTempEndDate] = useState<DateValue>(endDate);
|
|
246
|
+
const [tempRangeType, setTempRangeType] =
|
|
247
|
+
useState<DatePickerGranularity>(granularity);
|
|
248
|
+
|
|
249
|
+
// Convert DateValue to Date for calculations
|
|
250
|
+
// toDate() uses dayjs which correctly parses ISO date strings (e.g., "2026-01-25")
|
|
251
|
+
// as local timezone midnight, avoiding timezone offset issues
|
|
252
|
+
const currentStart = useMemo(
|
|
253
|
+
() => (startDate ? toDate(startDate) : new Date()),
|
|
254
|
+
[startDate],
|
|
255
|
+
);
|
|
256
|
+
const currentEnd = useMemo(
|
|
257
|
+
() => (endDate ? toDate(endDate) : new Date()),
|
|
258
|
+
[endDate],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Get the strategy for current range type
|
|
262
|
+
const strategy = navigationStrategies[granularity];
|
|
263
|
+
|
|
264
|
+
// Get display label using strategy pattern
|
|
265
|
+
const displayLabel = useMemo(
|
|
266
|
+
() => strategy.getLabel(currentStart, currentEnd, dateFormat),
|
|
267
|
+
[strategy, currentStart, currentEnd, dateFormat],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Check if navigating to next period would go into the future
|
|
271
|
+
const isNextDisabled = useMemo(() => {
|
|
272
|
+
if (!startDate || !endDate) return false;
|
|
273
|
+
const today = getCurrentDate();
|
|
274
|
+
const nextRange = strategy.navigateNext(currentStart, currentEnd);
|
|
275
|
+
// Disable if the next period's start date is after today
|
|
276
|
+
if (!nextRange.startDate) return false;
|
|
277
|
+
const nextStart =
|
|
278
|
+
nextRange.startDate instanceof Date
|
|
279
|
+
? nextRange.startDate
|
|
280
|
+
: toDate(nextRange.startDate);
|
|
281
|
+
return isAfter(nextStart, today);
|
|
282
|
+
}, [startDate, endDate, strategy, currentStart, currentEnd]);
|
|
283
|
+
|
|
284
|
+
// Navigate to previous period using strategy
|
|
285
|
+
const handlePrevious = () => {
|
|
286
|
+
if (!startDate || !endDate) return;
|
|
287
|
+
const newRange = strategy.navigatePrevious(currentStart, currentEnd);
|
|
288
|
+
onDateChange(newRange, granularity);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Navigate to next period using strategy
|
|
292
|
+
const handleNext = () => {
|
|
293
|
+
if (!startDate || !endDate || isNextDisabled) return;
|
|
294
|
+
const newRange = strategy.navigateNext(currentStart, currentEnd);
|
|
295
|
+
onDateChange(newRange, granularity);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// When opening picker, initialize temp state with current values
|
|
299
|
+
const handleOpen = () => {
|
|
300
|
+
setTempStartDate(startDate);
|
|
301
|
+
setTempEndDate(endDate);
|
|
302
|
+
setTempRangeType(granularity);
|
|
303
|
+
setOpened(true);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Handle changes while picker is open (before Apply)
|
|
307
|
+
const handleChange = (
|
|
308
|
+
range: DateRange,
|
|
309
|
+
newRangeType: DatePickerGranularity,
|
|
310
|
+
) => {
|
|
311
|
+
setTempStartDate(range.startDate);
|
|
312
|
+
setTempEndDate(range.endDate);
|
|
313
|
+
setTempRangeType(newRangeType);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// When Apply is clicked, commit the changes
|
|
317
|
+
const handleApply = (
|
|
318
|
+
range: DateRange,
|
|
319
|
+
newRangeType: DatePickerGranularity,
|
|
320
|
+
) => {
|
|
321
|
+
setOpened(false);
|
|
322
|
+
onDateChange(range, newRangeType);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Shared dropdown content for both variants
|
|
326
|
+
const renderDatePickerDropdown = () => (
|
|
327
|
+
<Popover.Dropdown w="860px" p="md">
|
|
328
|
+
<Box pos="relative">
|
|
329
|
+
<IconButton
|
|
330
|
+
variant="ghost"
|
|
331
|
+
color="gray"
|
|
332
|
+
onClick={() => setOpened(false)}
|
|
333
|
+
title="Close"
|
|
334
|
+
aria-label="Close date picker"
|
|
335
|
+
pos="absolute"
|
|
336
|
+
top={-8}
|
|
337
|
+
right={-8}
|
|
338
|
+
style={{ zIndex: zIndex.base }}
|
|
339
|
+
>
|
|
340
|
+
<X size={16} />
|
|
341
|
+
</IconButton>
|
|
342
|
+
<DatePicker
|
|
343
|
+
startDate={tempStartDate}
|
|
344
|
+
endDate={tempEndDate}
|
|
345
|
+
granularity={tempRangeType}
|
|
346
|
+
allowedRangeTypes={allowedRangeTypes}
|
|
347
|
+
maxDataHistory={maxDataHistory}
|
|
348
|
+
onChange={handleChange}
|
|
349
|
+
onApply={handleApply}
|
|
350
|
+
/>
|
|
351
|
+
</Box>
|
|
352
|
+
</Popover.Dropdown>
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Shared overlay for both variants
|
|
356
|
+
const renderOverlay = () =>
|
|
357
|
+
opened ? (
|
|
358
|
+
<Portal>
|
|
359
|
+
<Overlay
|
|
360
|
+
opacity={0.5}
|
|
361
|
+
color="black"
|
|
362
|
+
zIndex={zIndex.dropdownOverlay}
|
|
363
|
+
fixed
|
|
364
|
+
/>
|
|
365
|
+
</Portal>
|
|
366
|
+
) : null;
|
|
367
|
+
|
|
368
|
+
// Shared Popover props for both variants
|
|
369
|
+
const popoverProps = {
|
|
370
|
+
opened,
|
|
371
|
+
onChange: setOpened,
|
|
372
|
+
trapFocus: true,
|
|
373
|
+
position: popoverPosition,
|
|
374
|
+
offset: 16,
|
|
375
|
+
transitionProps: { duration: 0 },
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Render button variant
|
|
379
|
+
if (variant === 'button') {
|
|
380
|
+
// Determine right section: custom rightSection takes precedence, then dropdown icon
|
|
381
|
+
const buttonRightSection =
|
|
382
|
+
rightSection ??
|
|
383
|
+
(showDropdownIcon ? <ChevronDown size={16} /> : undefined);
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<>
|
|
387
|
+
<Popover {...popoverProps}>
|
|
388
|
+
<Popover.Target>
|
|
389
|
+
<Button
|
|
390
|
+
variant={buttonVariant}
|
|
391
|
+
onClick={handleOpen}
|
|
392
|
+
aria-label="Select date range"
|
|
393
|
+
leftSection={
|
|
394
|
+
showCalendarIcon ? <Calendar size={20} /> : undefined
|
|
395
|
+
}
|
|
396
|
+
rightSection={buttonRightSection}
|
|
397
|
+
className={className}
|
|
398
|
+
h={buttonHeight}
|
|
399
|
+
px="xs"
|
|
400
|
+
fz="sm"
|
|
401
|
+
fw={400}
|
|
402
|
+
loading={loading}
|
|
403
|
+
styles={{
|
|
404
|
+
root: {
|
|
405
|
+
width:
|
|
406
|
+
typeof buttonWidth === 'number'
|
|
407
|
+
? `${buttonWidth}px`
|
|
408
|
+
: buttonWidth,
|
|
409
|
+
justifyContent: 'flex-start',
|
|
410
|
+
},
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
{displayLabel}
|
|
414
|
+
</Button>
|
|
415
|
+
</Popover.Target>
|
|
416
|
+
{renderDatePickerDropdown()}
|
|
417
|
+
</Popover>
|
|
418
|
+
{renderOverlay()}
|
|
419
|
+
</>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Render navigation variant (default)
|
|
424
|
+
return (
|
|
425
|
+
<>
|
|
426
|
+
<Flex gap={{ base: 'xs', sm: 'lg' }} align="center" className={className}>
|
|
427
|
+
{showNavigation && (
|
|
428
|
+
<IconButton
|
|
429
|
+
variant="ghost"
|
|
430
|
+
color="gray"
|
|
431
|
+
size="md"
|
|
432
|
+
radius="lg"
|
|
433
|
+
aria-label="Previous period"
|
|
434
|
+
onClick={handlePrevious}
|
|
435
|
+
>
|
|
436
|
+
<ChevronLeft size={16} />
|
|
437
|
+
</IconButton>
|
|
438
|
+
)}
|
|
439
|
+
|
|
440
|
+
<Popover {...popoverProps}>
|
|
441
|
+
<Popover.Target>
|
|
442
|
+
<UnstyledButton onClick={handleOpen}>
|
|
443
|
+
<Flex gap="xs" align="center">
|
|
444
|
+
{showCalendarIcon && (
|
|
445
|
+
<Box
|
|
446
|
+
component="span"
|
|
447
|
+
style={{ display: 'flex', alignItems: 'center' }}
|
|
448
|
+
>
|
|
449
|
+
<Calendar size={20} />
|
|
450
|
+
</Box>
|
|
451
|
+
)}
|
|
452
|
+
<Title
|
|
453
|
+
variant="heading5"
|
|
454
|
+
style={{
|
|
455
|
+
cursor: 'pointer',
|
|
456
|
+
userSelect: 'none',
|
|
457
|
+
whiteSpace: 'nowrap',
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
{displayLabel}
|
|
461
|
+
</Title>
|
|
462
|
+
</Flex>
|
|
463
|
+
</UnstyledButton>
|
|
464
|
+
</Popover.Target>
|
|
465
|
+
{renderDatePickerDropdown()}
|
|
466
|
+
</Popover>
|
|
467
|
+
|
|
468
|
+
{showNavigation && (
|
|
469
|
+
<IconButton
|
|
470
|
+
variant="ghost"
|
|
471
|
+
color="gray"
|
|
472
|
+
size={40}
|
|
473
|
+
radius="lg"
|
|
474
|
+
aria-label="Next period"
|
|
475
|
+
onClick={handleNext}
|
|
476
|
+
disabled={isNextDisabled}
|
|
477
|
+
style={{ opacity: isNextDisabled ? 0.3 : 1 }}
|
|
478
|
+
>
|
|
479
|
+
<ChevronRight size={16} />
|
|
480
|
+
</IconButton>
|
|
481
|
+
)}
|
|
482
|
+
</Flex>
|
|
483
|
+
{renderOverlay()}
|
|
484
|
+
</>
|
|
485
|
+
);
|
|
486
|
+
}
|