@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,242 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Flex,
|
|
6
|
+
Group,
|
|
7
|
+
ScrollArea,
|
|
8
|
+
SegmentedControl,
|
|
9
|
+
Stack,
|
|
10
|
+
} from '@mantine/core';
|
|
11
|
+
import { type DateValue } from '@mantine/dates';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
endOfMonth,
|
|
15
|
+
endOfYear,
|
|
16
|
+
getCurrentDate,
|
|
17
|
+
getCurrentQuarter,
|
|
18
|
+
startOfMonth,
|
|
19
|
+
startOfYear,
|
|
20
|
+
toDate,
|
|
21
|
+
} from '@scalepad/ui-utils/date';
|
|
22
|
+
|
|
23
|
+
import { Button } from '../Button';
|
|
24
|
+
import { Text } from '../Typography';
|
|
25
|
+
import { CustomRangePicker } from './CustomRangePicker';
|
|
26
|
+
import { DateInput } from './DateInput';
|
|
27
|
+
import { MonthlyRangePicker } from './MonthlyRangePicker';
|
|
28
|
+
import { QuarterlyRangePicker } from './QuarterlyRangePicker';
|
|
29
|
+
import { QuickPicksSidebar } from './QuickPicksSidebar';
|
|
30
|
+
import { YearlyRangePicker } from './YearlyRangePicker';
|
|
31
|
+
|
|
32
|
+
import type { DatePickerGranularity, DateRange } from './types';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fully controlled DatePicker component.
|
|
36
|
+
* Parent component must manage all state (startDate, endDate, granularity).
|
|
37
|
+
*/
|
|
38
|
+
export interface DatePickerProps {
|
|
39
|
+
/** Current start date (controlled) */
|
|
40
|
+
startDate: DateValue;
|
|
41
|
+
/** Current end date (controlled) */
|
|
42
|
+
endDate: DateValue;
|
|
43
|
+
/** Current range type (controlled) */
|
|
44
|
+
granularity?: DatePickerGranularity;
|
|
45
|
+
/** Allowed range types to show. If not specified, all types are allowed. */
|
|
46
|
+
allowedRangeTypes?: DatePickerGranularity[];
|
|
47
|
+
/** Maximum historical date that can be selected */
|
|
48
|
+
maxDataHistory?: DateValue;
|
|
49
|
+
/** Called when any date or range type changes */
|
|
50
|
+
onChange?: (range: DateRange, granularity: DatePickerGranularity) => void;
|
|
51
|
+
/** Called when user clicks Apply button */
|
|
52
|
+
onApply?: (range: DateRange, granularity: DatePickerGranularity) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function DatePicker({
|
|
56
|
+
startDate,
|
|
57
|
+
endDate,
|
|
58
|
+
granularity = 'quarterly',
|
|
59
|
+
allowedRangeTypes,
|
|
60
|
+
maxDataHistory,
|
|
61
|
+
onChange,
|
|
62
|
+
onApply,
|
|
63
|
+
}: DatePickerProps) {
|
|
64
|
+
const today = getCurrentDate();
|
|
65
|
+
|
|
66
|
+
// Filter allowed range types (default to all if not specified)
|
|
67
|
+
const allRangeTypes: DatePickerGranularity[] = [
|
|
68
|
+
'monthly',
|
|
69
|
+
'quarterly',
|
|
70
|
+
'yearly',
|
|
71
|
+
'custom',
|
|
72
|
+
];
|
|
73
|
+
const availableRangeTypes = allowedRangeTypes || allRangeTypes;
|
|
74
|
+
|
|
75
|
+
// Convert DateValue to Date for Mantine components
|
|
76
|
+
const minDate = maxDataHistory ? toDate(maxDataHistory) : undefined;
|
|
77
|
+
const maxDate = today;
|
|
78
|
+
|
|
79
|
+
// All changes immediately propagate to parent via onChange
|
|
80
|
+
const handleChange = useCallback(
|
|
81
|
+
(range: DateRange, nextRangeType: DatePickerGranularity = granularity) => {
|
|
82
|
+
onChange?.(range, nextRangeType);
|
|
83
|
+
},
|
|
84
|
+
[onChange, granularity],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleRangeTypeChange = useCallback(
|
|
88
|
+
(newRangeType: DatePickerGranularity) => {
|
|
89
|
+
// When range type changes, reset to current period for that type
|
|
90
|
+
let newStartDate: Date;
|
|
91
|
+
let newEndDate: Date;
|
|
92
|
+
|
|
93
|
+
switch (newRangeType) {
|
|
94
|
+
case 'monthly':
|
|
95
|
+
newStartDate = startOfMonth(today);
|
|
96
|
+
newEndDate = endOfMonth(today);
|
|
97
|
+
break;
|
|
98
|
+
case 'quarterly': {
|
|
99
|
+
const currentQ = getCurrentQuarter(today);
|
|
100
|
+
newStartDate = currentQ.start;
|
|
101
|
+
newEndDate = currentQ.end;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'yearly':
|
|
105
|
+
newStartDate = startOfYear(today);
|
|
106
|
+
newEndDate = endOfYear(today);
|
|
107
|
+
break;
|
|
108
|
+
case 'custom':
|
|
109
|
+
// For custom, keep the current dates or fallback to current month
|
|
110
|
+
if (startDate && endDate) {
|
|
111
|
+
newStartDate =
|
|
112
|
+
startDate instanceof Date ? startDate : new Date(startDate);
|
|
113
|
+
newEndDate = endDate instanceof Date ? endDate : new Date(endDate);
|
|
114
|
+
} else {
|
|
115
|
+
newStartDate = startOfMonth(today);
|
|
116
|
+
newEndDate = endOfMonth(today);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onChange?.(
|
|
122
|
+
{ startDate: newStartDate, endDate: newEndDate },
|
|
123
|
+
newRangeType,
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
[onChange, today, startDate, endDate],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const handleApply = useCallback(() => {
|
|
130
|
+
onApply?.({ startDate, endDate }, granularity);
|
|
131
|
+
}, [startDate, endDate, onApply, granularity]);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Flex gap="md" align="stretch">
|
|
135
|
+
{/* Quick Picks Sidebar */}
|
|
136
|
+
<Box
|
|
137
|
+
style={{ borderRight: '1px solid var(--color-stroke-subdued-default)' }}
|
|
138
|
+
pr="lg"
|
|
139
|
+
>
|
|
140
|
+
<QuickPicksSidebar
|
|
141
|
+
minDate={minDate}
|
|
142
|
+
maxDate={maxDate}
|
|
143
|
+
allowedRangeTypes={availableRangeTypes}
|
|
144
|
+
onChange={(range, nextGranularity) => {
|
|
145
|
+
handleChange(range, nextGranularity);
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</Box>
|
|
149
|
+
|
|
150
|
+
{/* Main Content */}
|
|
151
|
+
<Stack gap="0" style={{ flex: 1 }}>
|
|
152
|
+
{/* Tabs */}
|
|
153
|
+
<Group justify="center" align="center">
|
|
154
|
+
<SegmentedControl
|
|
155
|
+
value={granularity}
|
|
156
|
+
onChange={value =>
|
|
157
|
+
handleRangeTypeChange(value as DatePickerGranularity)
|
|
158
|
+
}
|
|
159
|
+
data={[
|
|
160
|
+
{ label: 'Monthly', value: 'monthly' },
|
|
161
|
+
{ label: 'Quarterly', value: 'quarterly' },
|
|
162
|
+
{ label: 'Yearly', value: 'yearly' },
|
|
163
|
+
{ label: 'Custom', value: 'custom' },
|
|
164
|
+
].filter(item =>
|
|
165
|
+
availableRangeTypes.includes(item.value as DatePickerGranularity),
|
|
166
|
+
)}
|
|
167
|
+
/>
|
|
168
|
+
</Group>
|
|
169
|
+
|
|
170
|
+
{/* Content Area */}
|
|
171
|
+
<ScrollArea h="420px">
|
|
172
|
+
<Box py="md">
|
|
173
|
+
{granularity === 'quarterly' && (
|
|
174
|
+
<QuarterlyRangePicker
|
|
175
|
+
startDate={startDate}
|
|
176
|
+
endDate={endDate}
|
|
177
|
+
minDate={minDate}
|
|
178
|
+
maxDate={maxDate}
|
|
179
|
+
onChange={handleChange}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{granularity === 'monthly' && (
|
|
184
|
+
<MonthlyRangePicker
|
|
185
|
+
startDate={startDate}
|
|
186
|
+
endDate={endDate}
|
|
187
|
+
minDate={minDate}
|
|
188
|
+
maxDate={maxDate}
|
|
189
|
+
onChange={handleChange}
|
|
190
|
+
/>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{granularity === 'yearly' && (
|
|
194
|
+
<YearlyRangePicker
|
|
195
|
+
startDate={startDate}
|
|
196
|
+
endDate={endDate}
|
|
197
|
+
minDate={minDate}
|
|
198
|
+
maxDate={maxDate}
|
|
199
|
+
onChange={handleChange}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{granularity === 'custom' && (
|
|
204
|
+
<CustomRangePicker
|
|
205
|
+
startDate={startDate}
|
|
206
|
+
endDate={endDate}
|
|
207
|
+
minDate={minDate}
|
|
208
|
+
maxDate={maxDate}
|
|
209
|
+
onChange={handleChange}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</Box>
|
|
213
|
+
</ScrollArea>
|
|
214
|
+
|
|
215
|
+
{/* Date Range Inputs and Apply Button */}
|
|
216
|
+
<Group
|
|
217
|
+
justify="space-between"
|
|
218
|
+
align="center"
|
|
219
|
+
style={{ borderTop: '1px solid var(--color-stroke-subdued-default)' }}
|
|
220
|
+
pt="md"
|
|
221
|
+
>
|
|
222
|
+
<Group gap="xs">
|
|
223
|
+
<DateInput
|
|
224
|
+
date={startDate}
|
|
225
|
+
onChange={date => {
|
|
226
|
+
handleChange({ startDate: date, endDate }, 'custom');
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
<Text variant="caption1">to</Text>
|
|
230
|
+
<DateInput
|
|
231
|
+
date={endDate}
|
|
232
|
+
onChange={date => {
|
|
233
|
+
handleChange({ startDate, endDate: date }, 'custom');
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</Group>
|
|
237
|
+
<Button onClick={handleApply}>Apply</Button>
|
|
238
|
+
</Group>
|
|
239
|
+
</Stack>
|
|
240
|
+
</Flex>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Box, Card, Group, Stack } from '@mantine/core';
|
|
4
|
+
import type { DateValue } from '@mantine/dates';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createDate,
|
|
8
|
+
DateFormat,
|
|
9
|
+
endOfMonth,
|
|
10
|
+
formatDate,
|
|
11
|
+
getCurrentDate,
|
|
12
|
+
getMonth,
|
|
13
|
+
getYear,
|
|
14
|
+
isAfter,
|
|
15
|
+
isBefore,
|
|
16
|
+
isSameDay,
|
|
17
|
+
startOfMonth,
|
|
18
|
+
} from '@scalepad/ui-utils/date';
|
|
19
|
+
|
|
20
|
+
import { Text } from '../Typography';
|
|
21
|
+
|
|
22
|
+
import type { DateRange } from './types';
|
|
23
|
+
|
|
24
|
+
interface MonthlyRangePickerProps {
|
|
25
|
+
startDate: DateValue;
|
|
26
|
+
endDate: DateValue;
|
|
27
|
+
minDate?: DateValue;
|
|
28
|
+
maxDate: DateValue;
|
|
29
|
+
onChange?: (range: DateRange) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function MonthlyRangePicker({
|
|
33
|
+
startDate,
|
|
34
|
+
endDate,
|
|
35
|
+
minDate,
|
|
36
|
+
maxDate,
|
|
37
|
+
onChange,
|
|
38
|
+
}: MonthlyRangePickerProps) {
|
|
39
|
+
const today = getCurrentDate();
|
|
40
|
+
const currentYear = getYear(today);
|
|
41
|
+
const currentMonth = getMonth(today);
|
|
42
|
+
|
|
43
|
+
// Generate months for display
|
|
44
|
+
const monthsByYear = useMemo(() => {
|
|
45
|
+
const months: Array<{ year: number; month: number; label: string }> = [];
|
|
46
|
+
const startYear = minDate ? getYear(minDate) : currentYear - 1;
|
|
47
|
+
const endYear = currentYear;
|
|
48
|
+
|
|
49
|
+
for (let year = endYear; year >= startYear; year--) {
|
|
50
|
+
// For current year, show all months (they'll be disabled if in the future)
|
|
51
|
+
// For past years, show all 12 months (they've all ended)
|
|
52
|
+
const endMonth = 11; // Always show all 12 months
|
|
53
|
+
for (let m = endMonth; m >= 0; m--) {
|
|
54
|
+
const monthEnd = endOfMonth(createDate(year, m, 1));
|
|
55
|
+
|
|
56
|
+
// Skip if before minDate
|
|
57
|
+
if (minDate && isBefore(monthEnd, minDate)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Skip if after today (only for past years - current year shows all months)
|
|
61
|
+
if (year !== currentYear && isAfter(monthEnd, today)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
months.push({
|
|
66
|
+
year,
|
|
67
|
+
month: m,
|
|
68
|
+
label: formatDate(createDate(year, m, 1), DateFormat.MONTH_SHORT),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Group months by year
|
|
74
|
+
const grouped: Record<
|
|
75
|
+
number,
|
|
76
|
+
Array<{ year: number; month: number; label: string }>
|
|
77
|
+
> = {};
|
|
78
|
+
months.forEach(m => {
|
|
79
|
+
if (!grouped[m.year]) {
|
|
80
|
+
grouped[m.year] = [];
|
|
81
|
+
}
|
|
82
|
+
grouped[m.year].push(m);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Reverse the order of months within each year (Jan, Feb, ..., Dec instead of Dec, ..., Feb, Jan)
|
|
86
|
+
Object.keys(grouped).forEach(year => {
|
|
87
|
+
grouped[parseInt(year, 10)].reverse();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return grouped;
|
|
91
|
+
}, [minDate, currentYear, today]);
|
|
92
|
+
|
|
93
|
+
// Check if a month is the current month
|
|
94
|
+
const isCurrentMonth = (year: number, month: number) => {
|
|
95
|
+
return year === currentYear && month === currentMonth;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Handle month selection
|
|
99
|
+
const handleMonthSelect = (year: number, month: number) => {
|
|
100
|
+
const monthDate = createDate(year, month, 1);
|
|
101
|
+
const monthStart = startOfMonth(monthDate);
|
|
102
|
+
const monthEnd = endOfMonth(monthDate);
|
|
103
|
+
|
|
104
|
+
let newStartDate = monthStart;
|
|
105
|
+
let newEndDate = monthEnd;
|
|
106
|
+
|
|
107
|
+
// Ensure dates are within bounds
|
|
108
|
+
if (minDate) {
|
|
109
|
+
const minDateObj =
|
|
110
|
+
minDate instanceof Date ? minDate : minDate ? new Date(minDate) : null;
|
|
111
|
+
if (minDateObj && isBefore(newStartDate, minDateObj)) {
|
|
112
|
+
newStartDate = minDateObj;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const maxDateObj =
|
|
116
|
+
maxDate instanceof Date ? maxDate : maxDate ? new Date(maxDate) : null;
|
|
117
|
+
if (maxDateObj && isAfter(newEndDate, maxDateObj)) {
|
|
118
|
+
newEndDate = maxDateObj;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onChange?.({
|
|
122
|
+
startDate: newStartDate,
|
|
123
|
+
endDate: newEndDate,
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Check if month is selected
|
|
128
|
+
const isMonthSelected = (year: number, month: number) => {
|
|
129
|
+
if (!startDate || !endDate) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const monthDate = createDate(year, month, 1);
|
|
133
|
+
const monthStart = startOfMonth(monthDate);
|
|
134
|
+
const monthEnd = endOfMonth(monthDate);
|
|
135
|
+
// Check if the selected range matches this month
|
|
136
|
+
// Start date should be at or before month start, end date should be within or at month end
|
|
137
|
+
return (
|
|
138
|
+
(isBefore(startDate, monthStart) || isSameDay(startDate, monthStart)) &&
|
|
139
|
+
(isAfter(endDate, monthEnd) ||
|
|
140
|
+
isSameDay(endDate, monthEnd) ||
|
|
141
|
+
(isAfter(endDate, monthStart) && isBefore(endDate, monthEnd)))
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Stack gap="xs">
|
|
147
|
+
{Object.entries(monthsByYear)
|
|
148
|
+
.sort(([a], [b]) => parseInt(b, 10) - parseInt(a, 10))
|
|
149
|
+
.map(([year, months]) => {
|
|
150
|
+
const yearNum = parseInt(year, 10);
|
|
151
|
+
return (
|
|
152
|
+
<Stack key={year} gap="xs">
|
|
153
|
+
<Text variant="caption1.stronger">{year}</Text>
|
|
154
|
+
<Group gap="md">
|
|
155
|
+
{months.map(m => {
|
|
156
|
+
const isCurrent = isCurrentMonth(yearNum, m.month);
|
|
157
|
+
const isSelected = isMonthSelected(yearNum, m.month);
|
|
158
|
+
const monthDate = createDate(yearNum, m.month, 1);
|
|
159
|
+
const monthStart = startOfMonth(monthDate);
|
|
160
|
+
const monthEnd = endOfMonth(monthDate);
|
|
161
|
+
const minDateObj = minDate
|
|
162
|
+
? minDate instanceof Date
|
|
163
|
+
? minDate
|
|
164
|
+
: new Date(minDate)
|
|
165
|
+
: null;
|
|
166
|
+
// Disable if before minDate or if the month hasn't started yet (completely in the future)
|
|
167
|
+
// Allow current month to be selected even if incomplete
|
|
168
|
+
const isDisabled =
|
|
169
|
+
(minDateObj && isBefore(monthEnd, minDateObj)) ||
|
|
170
|
+
isAfter(monthStart, today);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Card
|
|
174
|
+
key={`${year}-${m.month}`}
|
|
175
|
+
p="xs"
|
|
176
|
+
withBorder
|
|
177
|
+
shadow="none"
|
|
178
|
+
style={{
|
|
179
|
+
width: 103,
|
|
180
|
+
minHeight: 60,
|
|
181
|
+
display: 'flex',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
justifyContent: 'center',
|
|
184
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
185
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
186
|
+
borderColor: isSelected
|
|
187
|
+
? 'var(--color-stroke-primary-default)'
|
|
188
|
+
: undefined,
|
|
189
|
+
position: 'relative',
|
|
190
|
+
}}
|
|
191
|
+
onClick={() =>
|
|
192
|
+
!isDisabled && handleMonthSelect(yearNum, m.month)
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
<Text
|
|
196
|
+
variant="caption1.stronger"
|
|
197
|
+
c={
|
|
198
|
+
isSelected
|
|
199
|
+
? 'text.primary.default'
|
|
200
|
+
: isDisabled
|
|
201
|
+
? 'text.disabled.default'
|
|
202
|
+
: undefined
|
|
203
|
+
}
|
|
204
|
+
>
|
|
205
|
+
{m.label}
|
|
206
|
+
</Text>
|
|
207
|
+
{isCurrent && (
|
|
208
|
+
<Box
|
|
209
|
+
style={{
|
|
210
|
+
position: 'absolute',
|
|
211
|
+
bottom: 4,
|
|
212
|
+
left: '50%',
|
|
213
|
+
transform: 'translateX(-50%)',
|
|
214
|
+
width: 6,
|
|
215
|
+
height: 6,
|
|
216
|
+
borderRadius: '50%',
|
|
217
|
+
backgroundColor:
|
|
218
|
+
'var(--color-background-primary-filled)',
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</Card>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
</Group>
|
|
226
|
+
</Stack>
|
|
227
|
+
);
|
|
228
|
+
})}
|
|
229
|
+
</Stack>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Box, Card, Group, Stack } from '@mantine/core';
|
|
4
|
+
import type { DateValue } from '@mantine/dates';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getCurrentDate,
|
|
8
|
+
getQuarter,
|
|
9
|
+
getQuarterEnd,
|
|
10
|
+
getQuarterStart,
|
|
11
|
+
getYear,
|
|
12
|
+
isAfter,
|
|
13
|
+
isBefore,
|
|
14
|
+
isSameDay,
|
|
15
|
+
} from '@scalepad/ui-utils/date';
|
|
16
|
+
|
|
17
|
+
import { Text } from '../Typography';
|
|
18
|
+
|
|
19
|
+
import type { DateRange } from './types';
|
|
20
|
+
|
|
21
|
+
interface QuarterlyRangePickerProps {
|
|
22
|
+
startDate: DateValue;
|
|
23
|
+
endDate: DateValue;
|
|
24
|
+
minDate?: DateValue;
|
|
25
|
+
maxDate: DateValue;
|
|
26
|
+
onChange?: (range: DateRange) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function QuarterlyRangePicker({
|
|
30
|
+
startDate,
|
|
31
|
+
endDate,
|
|
32
|
+
minDate,
|
|
33
|
+
maxDate,
|
|
34
|
+
onChange,
|
|
35
|
+
}: QuarterlyRangePickerProps) {
|
|
36
|
+
const today = getCurrentDate();
|
|
37
|
+
const currentYear = getYear(today);
|
|
38
|
+
const currentQuarter = getQuarter(today);
|
|
39
|
+
|
|
40
|
+
// Generate quarters for display
|
|
41
|
+
const quartersByYear = useMemo(() => {
|
|
42
|
+
const quarters: Array<{ year: number; quarter: number; label: string }> =
|
|
43
|
+
[];
|
|
44
|
+
const startYear = minDate ? getYear(minDate) : currentYear - 2;
|
|
45
|
+
const endYear = currentYear;
|
|
46
|
+
|
|
47
|
+
for (let year = endYear; year >= startYear; year--) {
|
|
48
|
+
// For current year, show all quarters (they'll be disabled if in the future)
|
|
49
|
+
// For past years, only show quarters that have ended
|
|
50
|
+
for (let q = 3; q >= 0; q--) {
|
|
51
|
+
const quarterEnd = getQuarterEnd(year, q);
|
|
52
|
+
|
|
53
|
+
// Skip if before minDate
|
|
54
|
+
if (minDate && isBefore(quarterEnd, minDate)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Skip if after today (only for past years - current year shows all quarters)
|
|
58
|
+
if (year !== currentYear && isAfter(quarterEnd, today)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
quarters.push({
|
|
63
|
+
year,
|
|
64
|
+
quarter: q,
|
|
65
|
+
label: `Q${q + 1}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Group quarters by year
|
|
71
|
+
const grouped: Record<
|
|
72
|
+
number,
|
|
73
|
+
Array<{ year: number; quarter: number; label: string }>
|
|
74
|
+
> = {};
|
|
75
|
+
quarters.forEach(q => {
|
|
76
|
+
if (!grouped[q.year]) {
|
|
77
|
+
grouped[q.year] = [];
|
|
78
|
+
}
|
|
79
|
+
grouped[q.year].push(q);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Reverse the order of quarters within each year (Q1, Q2, Q3, Q4 instead of Q4, Q3, Q2, Q1)
|
|
83
|
+
Object.keys(grouped).forEach(year => {
|
|
84
|
+
grouped[parseInt(year, 10)].reverse();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return grouped;
|
|
88
|
+
}, [minDate, currentYear, today]);
|
|
89
|
+
|
|
90
|
+
// Check if a quarter is the current quarter
|
|
91
|
+
const isCurrentQuarter = (year: number, quarter: number) => {
|
|
92
|
+
return year === currentYear && quarter === currentQuarter;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Handle quarter selection
|
|
96
|
+
const handleQuarterSelect = (year: number, quarter: number) => {
|
|
97
|
+
const quarterStart = getQuarterStart(year, quarter);
|
|
98
|
+
const quarterEnd = getQuarterEnd(year, quarter);
|
|
99
|
+
|
|
100
|
+
let newStartDate = quarterStart;
|
|
101
|
+
let newEndDate = quarterEnd;
|
|
102
|
+
|
|
103
|
+
// Ensure dates are within bounds
|
|
104
|
+
if (minDate) {
|
|
105
|
+
const minDateObj =
|
|
106
|
+
minDate instanceof Date ? minDate : minDate ? new Date(minDate) : null;
|
|
107
|
+
if (minDateObj && isBefore(newStartDate, minDateObj)) {
|
|
108
|
+
newStartDate = minDateObj;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const maxDateObj =
|
|
112
|
+
maxDate instanceof Date ? maxDate : maxDate ? new Date(maxDate) : null;
|
|
113
|
+
if (maxDateObj && isAfter(newEndDate, maxDateObj)) {
|
|
114
|
+
newEndDate = maxDateObj;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onChange?.({
|
|
118
|
+
startDate: newStartDate,
|
|
119
|
+
endDate: newEndDate,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Check if quarter is selected
|
|
124
|
+
const isQuarterSelected = (year: number, quarter: number) => {
|
|
125
|
+
if (!startDate || !endDate) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const quarterStart = getQuarterStart(year, quarter);
|
|
129
|
+
const quarterEnd = getQuarterEnd(year, quarter);
|
|
130
|
+
// Check if the selected range matches this quarter
|
|
131
|
+
// Start date should be at or before quarter start, end date should be within or at quarter end
|
|
132
|
+
return (
|
|
133
|
+
(isBefore(startDate, quarterStart) ||
|
|
134
|
+
isSameDay(startDate, quarterStart)) &&
|
|
135
|
+
(isAfter(endDate, quarterEnd) ||
|
|
136
|
+
isSameDay(endDate, quarterEnd) ||
|
|
137
|
+
(isAfter(endDate, quarterStart) && isBefore(endDate, quarterEnd)))
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Stack gap="xs">
|
|
143
|
+
{Object.entries(quartersByYear)
|
|
144
|
+
.sort(([a], [b]) => parseInt(b, 10) - parseInt(a, 10))
|
|
145
|
+
.map(([year, quarters]) => (
|
|
146
|
+
<Stack key={year} gap="xs">
|
|
147
|
+
<Text variant="caption1.stronger">{year}</Text>
|
|
148
|
+
<Group gap="md">
|
|
149
|
+
{quarters.map(q => {
|
|
150
|
+
const yearNum = parseInt(year, 10);
|
|
151
|
+
const isCurrent = isCurrentQuarter(yearNum, q.quarter);
|
|
152
|
+
const isSelected = isQuarterSelected(yearNum, q.quarter);
|
|
153
|
+
const quarterStart = getQuarterStart(yearNum, q.quarter);
|
|
154
|
+
const quarterEnd = getQuarterEnd(yearNum, q.quarter);
|
|
155
|
+
const minDateObj = minDate
|
|
156
|
+
? minDate instanceof Date
|
|
157
|
+
? minDate
|
|
158
|
+
: new Date(minDate)
|
|
159
|
+
: null;
|
|
160
|
+
// Disable if before minDate or if the quarter hasn't started yet (completely in the future)
|
|
161
|
+
// Allow current quarter to be selected even if incomplete
|
|
162
|
+
const isDisabled =
|
|
163
|
+
(minDateObj && isBefore(quarterEnd, minDateObj)) ||
|
|
164
|
+
isAfter(quarterStart, today);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Card
|
|
168
|
+
key={`${year}-Q${q.quarter + 1}`}
|
|
169
|
+
p="xs"
|
|
170
|
+
withBorder
|
|
171
|
+
shadow="none"
|
|
172
|
+
style={{
|
|
173
|
+
flex: 1,
|
|
174
|
+
minHeight: 60,
|
|
175
|
+
display: 'flex',
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
justifyContent: 'center',
|
|
178
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
179
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
180
|
+
borderColor: isSelected
|
|
181
|
+
? 'var(--color-stroke-primary-default)'
|
|
182
|
+
: undefined,
|
|
183
|
+
position: 'relative',
|
|
184
|
+
}}
|
|
185
|
+
onClick={() =>
|
|
186
|
+
!isDisabled && handleQuarterSelect(yearNum, q.quarter)
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<Text
|
|
190
|
+
variant="caption1.stronger"
|
|
191
|
+
c={
|
|
192
|
+
isSelected
|
|
193
|
+
? 'text.primary.default'
|
|
194
|
+
: isDisabled
|
|
195
|
+
? 'text.disabled.default'
|
|
196
|
+
: undefined
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
{q.label}
|
|
200
|
+
</Text>
|
|
201
|
+
{isCurrent && (
|
|
202
|
+
<Box
|
|
203
|
+
style={{
|
|
204
|
+
position: 'absolute',
|
|
205
|
+
bottom: 4,
|
|
206
|
+
left: '50%',
|
|
207
|
+
transform: 'translateX(-50%)',
|
|
208
|
+
width: 6,
|
|
209
|
+
height: 6,
|
|
210
|
+
borderRadius: '50%',
|
|
211
|
+
backgroundColor:
|
|
212
|
+
'var(--color-background-primary-filled)',
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</Card>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
</Group>
|
|
220
|
+
</Stack>
|
|
221
|
+
))}
|
|
222
|
+
</Stack>
|
|
223
|
+
);
|
|
224
|
+
}
|