@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.
Files changed (273) hide show
  1. package/.ai/rules/date-handling.md +39 -0
  2. package/.ai/rules/figma-design-system.md +372 -0
  3. package/.ai/rules/figma-lm-design-system-keys.md +680 -0
  4. package/.ai/rules/file-extensions.md +13 -0
  5. package/.ai/rules/modal-confirmation-mutation.md +56 -0
  6. package/.ai/rules/react-hooks.md +29 -0
  7. package/.ai/rules/styling.md +83 -0
  8. package/AGENTS.md +37 -0
  9. package/README.md +125 -0
  10. package/figma.config.json +9 -0
  11. package/package.json +127 -0
  12. package/scripts/install-ai-rules.mjs +136 -0
  13. package/src/ThemeProvider.tsx +57 -0
  14. package/src/charts.ts +32 -0
  15. package/src/components/ActionCard/ActionCard.css.ts +60 -0
  16. package/src/components/ActionCard/ActionCard.tsx +154 -0
  17. package/src/components/ActionCard/index.ts +2 -0
  18. package/src/components/Anchor/Anchor.tsx +47 -0
  19. package/src/components/Anchor/index.ts +2 -0
  20. package/src/components/AppliedFiltersManagerBar/AppliedFiltersManagerBar.tsx +105 -0
  21. package/src/components/AppliedFiltersManagerBar/FilterBadge.css.ts +23 -0
  22. package/src/components/AppliedFiltersManagerBar/FilterBadge.tsx +50 -0
  23. package/src/components/AppliedFiltersManagerBar/index.ts +5 -0
  24. package/src/components/Badge/Badge.css.ts +72 -0
  25. package/src/components/Badge/Badge.figma.tsx +43 -0
  26. package/src/components/Badge/Badge.tsx +159 -0
  27. package/src/components/Badge/index.ts +2 -0
  28. package/src/components/BreadCrumb/BreadCrumb.tsx +62 -0
  29. package/src/components/BreadCrumb/index.ts +2 -0
  30. package/src/components/BulkActionBar/BulkActionBar.css.ts +26 -0
  31. package/src/components/BulkActionBar/BulkActionBar.tsx +164 -0
  32. package/src/components/BulkActionBar/index.ts +2 -0
  33. package/src/components/Button/Button.css.ts +272 -0
  34. package/src/components/Button/Button.figma.tsx +74 -0
  35. package/src/components/Button/Button.tsx +84 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Charts/ChartTooltip.figma.tsx +33 -0
  38. package/src/components/Charts/ChartTooltip.tsx +101 -0
  39. package/src/components/Charts/MiniBarSparkline.tsx +75 -0
  40. package/src/components/Charts/StackedPatternBarChart.tsx +494 -0
  41. package/src/components/Charts/TrendAreaChart.css.ts +23 -0
  42. package/src/components/Charts/TrendAreaChart.tsx +210 -0
  43. package/src/components/Charts/index.ts +12 -0
  44. package/src/components/CodePanel/CodePanel.css.ts +113 -0
  45. package/src/components/CodePanel/CodePanel.tsx +121 -0
  46. package/src/components/CodePanel/index.ts +2 -0
  47. package/src/components/CommentComposer/CommentComposer.css.ts +60 -0
  48. package/src/components/CommentComposer/CommentComposer.tsx +181 -0
  49. package/src/components/CommentComposer/index.ts +2 -0
  50. package/src/components/ConfirmationModal/ConfirmationModal.tsx +149 -0
  51. package/src/components/ConfirmationModal/index.ts +2 -0
  52. package/src/components/ConfirmationTooltip/ConfirmationTooltip.tsx +132 -0
  53. package/src/components/ConfirmationTooltip/index.ts +2 -0
  54. package/src/components/DataDialog.figma.tsx +33 -0
  55. package/src/components/DataDialog.tsx +46 -0
  56. package/src/components/DataTable/DataTable.tsx +1042 -0
  57. package/src/components/DataTable/RowExpandToggle.tsx +105 -0
  58. package/src/components/DataTable/RowGroupHeader.tsx +190 -0
  59. package/src/components/DataTable/createActionsColumn.tsx +86 -0
  60. package/src/components/DataTable/index.ts +25 -0
  61. package/src/components/DatePicker/CustomRangePicker.tsx +59 -0
  62. package/src/components/DatePicker/DateInput.tsx +329 -0
  63. package/src/components/DatePicker/DateNavigator.tsx +486 -0
  64. package/src/components/DatePicker/DatePicker.tsx +242 -0
  65. package/src/components/DatePicker/MonthlyRangePicker.tsx +231 -0
  66. package/src/components/DatePicker/QuarterlyRangePicker.tsx +224 -0
  67. package/src/components/DatePicker/QuickPicksSidebar.tsx +242 -0
  68. package/src/components/DatePicker/YearlyRangePicker.tsx +171 -0
  69. package/src/components/DatePicker/index.ts +7 -0
  70. package/src/components/DatePicker/types.ts +12 -0
  71. package/src/components/DesignSystemPrimitives/FluidGrid.tsx +44 -0
  72. package/src/components/DesignSystemPrimitives/InteractivePrimitives.tsx +177 -0
  73. package/src/components/DesignSystemPrimitives/LayoutPrimitives.tsx +220 -0
  74. package/src/components/DesignSystemPrimitives/LayoutPrimitives.types.tsx +15 -0
  75. package/src/components/DesignSystemPrimitives/SurfacePrimitives.tsx +46 -0
  76. package/src/components/DesignSystemPrimitives/index.ts +55 -0
  77. package/src/components/Details/Details.css.ts +74 -0
  78. package/src/components/Details/Details.tsx +140 -0
  79. package/src/components/Details/index.ts +2 -0
  80. package/src/components/DownloadCard/DownloadCard.css.ts +22 -0
  81. package/src/components/DownloadCard/DownloadCard.tsx +63 -0
  82. package/src/components/DownloadCard/index.ts +2 -0
  83. package/src/components/Drawer/Drawer.css.ts +32 -0
  84. package/src/components/Drawer/Drawer.tsx +236 -0
  85. package/src/components/Drawer/hooks/useDetailDrawer.ts +61 -0
  86. package/src/components/Drawer/hooks/useDetailDrawerNavigation.ts +125 -0
  87. package/src/components/Drawer/hooks/useDetailDrawerNavigationContext.ts +66 -0
  88. package/src/components/EditableRichText/EditableRichText.css.ts +72 -0
  89. package/src/components/EditableRichText/EditableRichText.tsx +324 -0
  90. package/src/components/EditableRichText/index.ts +2 -0
  91. package/src/components/EditableSelect/EditableSelect.css.ts +62 -0
  92. package/src/components/EditableSelect/EditableSelect.tsx +224 -0
  93. package/src/components/EditableSelect/index.ts +2 -0
  94. package/src/components/EditableText/EditableText.tsx +377 -0
  95. package/src/components/EditableText/index.ts +2 -0
  96. package/src/components/EmptyState/EmptyState.figma.tsx +33 -0
  97. package/src/components/EmptyState/EmptyState.tsx +230 -0
  98. package/src/components/EmptyState/index.ts +2 -0
  99. package/src/components/ErrorBoundary.tsx +135 -0
  100. package/src/components/ErrorState/ErrorState.tsx +197 -0
  101. package/src/components/ErrorState/index.ts +2 -0
  102. package/src/components/FeatureCard.tsx +42 -0
  103. package/src/components/FilterMenu/FilterMenu.figma.tsx +30 -0
  104. package/src/components/FilterMenu/FilterMenu.tsx +198 -0
  105. package/src/components/FilterMenu/FilterSubMenuTypes/BooleanFilterSubmenu.tsx +46 -0
  106. package/src/components/FilterMenu/FilterSubMenuTypes/SearchableFilterSubmenu.tsx +239 -0
  107. package/src/components/FilterMenu/FilterSubMenuTypes/index.ts +8 -0
  108. package/src/components/FilterMenu/defaultFilterSchemas.ts +63 -0
  109. package/src/components/FilterMenu/helpers.ts +115 -0
  110. package/src/components/FilterMenu/index.ts +35 -0
  111. package/src/components/FilterMenu/types.ts +101 -0
  112. package/src/components/IconButton/IconButton.css.ts +272 -0
  113. package/src/components/IconButton/IconButton.figma.tsx +47 -0
  114. package/src/components/IconButton/IconButton.tsx +72 -0
  115. package/src/components/IconButton/README.md +230 -0
  116. package/src/components/IconButton/index.ts +2 -0
  117. package/src/components/InfiniteScrollSentinel.tsx +86 -0
  118. package/src/components/InfiniteScrollTrigger.tsx +78 -0
  119. package/src/components/InfoCard.figma.tsx +47 -0
  120. package/src/components/InfoCard.tsx +216 -0
  121. package/src/components/KbdHint/KbdHint.tsx +23 -0
  122. package/src/components/KbdHint/index.ts +2 -0
  123. package/src/components/LabeledField/LabeledField.tsx +21 -0
  124. package/src/components/LabeledField/index.ts +2 -0
  125. package/src/components/LookupSelect/LookupSelect.css.ts +149 -0
  126. package/src/components/LookupSelect/LookupSelect.tsx +325 -0
  127. package/src/components/LookupSelect/index.ts +2 -0
  128. package/src/components/Menu/Menu.css.ts +89 -0
  129. package/src/components/Menu/Menu.tsx +105 -0
  130. package/src/components/Menu/index.ts +2 -0
  131. package/src/components/MessageBox/MessageBox.tsx +168 -0
  132. package/src/components/MessageBox/index.ts +2 -0
  133. package/src/components/MetricDisplay/MetricDisplay.tsx +55 -0
  134. package/src/components/MetricDisplay/index.ts +1 -0
  135. package/src/components/MultiSelect/MultiSelect.tsx +278 -0
  136. package/src/components/MultiSelect/index.ts +2 -0
  137. package/src/components/Notifications/Notifications.tsx +12 -0
  138. package/src/components/Notifications/README.md +93 -0
  139. package/src/components/Notifications/index.ts +4 -0
  140. package/src/components/Notifications/showToast.tsx +100 -0
  141. package/src/components/PropertyRow/PropertyRow.tsx +96 -0
  142. package/src/components/PropertyRow/index.ts +2 -0
  143. package/src/components/RadioTile/RadioTile.tsx +253 -0
  144. package/src/components/RadioTile/index.ts +2 -0
  145. package/src/components/RichText/FormattingToolbar.css.ts +69 -0
  146. package/src/components/RichText/FormattingToolbar.tsx +112 -0
  147. package/src/components/RichText/RichTextInline.css.ts +54 -0
  148. package/src/components/RichText/RichTextInline.tsx +318 -0
  149. package/src/components/RichText/formattingCommands.ts +181 -0
  150. package/src/components/RichText/formattingTypes.ts +34 -0
  151. package/src/components/RichText/index.ts +49 -0
  152. package/src/components/RichText/richTextExtensions.ts +111 -0
  153. package/src/components/RichText/richTextHelpers.ts +65 -0
  154. package/src/components/RichText/richTextImage.ts +253 -0
  155. package/src/components/RichText/richTextImageHandlers.ts +244 -0
  156. package/src/components/RichText/richTextProse.css.ts +261 -0
  157. package/src/components/RichTextEditor/RichTextEditor.css.ts +82 -0
  158. package/src/components/RichTextEditor/RichTextEditor.tsx +204 -0
  159. package/src/components/RichTextEditor/index.ts +2 -0
  160. package/src/components/RichTextView/RichTextView.css.ts +11 -0
  161. package/src/components/RichTextView/RichTextView.tsx +114 -0
  162. package/src/components/RichTextView/index.ts +2 -0
  163. package/src/components/Schedule/Schedule.tsx +35 -0
  164. package/src/components/SchedulePicker/SchedulePicker.css.ts +42 -0
  165. package/src/components/SchedulePicker/SchedulePicker.tsx +130 -0
  166. package/src/components/SchedulePicker/index.ts +2 -0
  167. package/src/components/SearchableList/types.ts +30 -0
  168. package/src/components/SearchableSubMenu/SearchableSubMenu.css.ts +25 -0
  169. package/src/components/SearchableSubMenu/SearchableSubMenu.tsx +139 -0
  170. package/src/components/SearchableSubMenu/index.ts +2 -0
  171. package/src/components/Select/README.md +114 -0
  172. package/src/components/Select/Select.css.ts +110 -0
  173. package/src/components/Select/Select.tsx +133 -0
  174. package/src/components/Select/index.ts +2 -0
  175. package/src/components/SelectCreatable/SelectCreatable.css.ts +16 -0
  176. package/src/components/SelectCreatable/SelectCreatable.tsx +203 -0
  177. package/src/components/SelectCreatable/index.ts +2 -0
  178. package/src/components/SettingsCard/SettingsCard.tsx +98 -0
  179. package/src/components/SettingsCard/index.ts +2 -0
  180. package/src/components/Sidebar/Sidebar.css.ts +91 -0
  181. package/src/components/Sidebar/Sidebar.tsx +129 -0
  182. package/src/components/Sidebar/index.ts +5 -0
  183. package/src/components/SimpleList/SimpleList.css.ts +12 -0
  184. package/src/components/SimpleList/SimpleList.tsx +44 -0
  185. package/src/components/SimpleList/index.ts +2 -0
  186. package/src/components/SimpleTable/SimpleTable.tsx +296 -0
  187. package/src/components/SimpleTable/index.ts +2 -0
  188. package/src/components/SlashRichTextEditor/SelectionBubbleMenu.css.ts +62 -0
  189. package/src/components/SlashRichTextEditor/SelectionBubbleMenu.tsx +85 -0
  190. package/src/components/SlashRichTextEditor/SlashCommandMenu.css.ts +124 -0
  191. package/src/components/SlashRichTextEditor/SlashCommandMenu.tsx +168 -0
  192. package/src/components/SlashRichTextEditor/SlashRichTextEditor.css.ts +81 -0
  193. package/src/components/SlashRichTextEditor/SlashRichTextEditor.tsx +538 -0
  194. package/src/components/SlashRichTextEditor/SlashSuggestionExtension.ts +48 -0
  195. package/src/components/SlashRichTextEditor/index.ts +13 -0
  196. package/src/components/SlashRichTextEditor/types.ts +48 -0
  197. package/src/components/StatCard/StatCard.css.ts +70 -0
  198. package/src/components/StatCard/StatCard.tsx +201 -0
  199. package/src/components/StatCard/index.ts +1 -0
  200. package/src/components/StatusBadge/StatusBadge.tsx +70 -0
  201. package/src/components/StatusBadge/index.ts +2 -0
  202. package/src/components/StatusIndicator/StatusIndicator.tsx +67 -0
  203. package/src/components/StatusIndicator/index.ts +6 -0
  204. package/src/components/SubNavigation/SubNavigation.css.ts +72 -0
  205. package/src/components/SubNavigation/SubNavigation.tsx +104 -0
  206. package/src/components/SubNavigation/index.ts +2 -0
  207. package/src/components/SuspenseLoader.tsx +22 -0
  208. package/src/components/Table/SortableColumnHeader.tsx +99 -0
  209. package/src/components/Table/TableSkeletonRows.figma.tsx +22 -0
  210. package/src/components/Table/TableSkeletonRows.tsx +113 -0
  211. package/src/components/Table/index.ts +9 -0
  212. package/src/components/TableActionsMenu.tsx +58 -0
  213. package/src/components/TableCard.tsx +29 -0
  214. package/src/components/TableContainer/TableContainer.tsx +86 -0
  215. package/src/components/TableContainer/index.ts +2 -0
  216. package/src/components/TableControlBar/TableControlBar.tsx +156 -0
  217. package/src/components/TableControlBar/TableSelectionButton.tsx +57 -0
  218. package/src/components/TableControlBar/index.ts +13 -0
  219. package/src/components/TableControlBar/useTableControlBar.tsx +314 -0
  220. package/src/components/TableSelection/TableSelection.tsx +43 -0
  221. package/src/components/TableSelection/index.ts +5 -0
  222. package/src/components/Tabs/README.md +76 -0
  223. package/src/components/Tabs/Tabs.css.ts +54 -0
  224. package/src/components/Tabs/Tabs.figma.tsx +47 -0
  225. package/src/components/Tabs/Tabs.tsx +96 -0
  226. package/src/components/Tabs/index.ts +8 -0
  227. package/src/components/TextInput/README.md +98 -0
  228. package/src/components/TextInput/SearchTextInput.figma.tsx +22 -0
  229. package/src/components/TextInput/SearchTextInput.tsx +150 -0
  230. package/src/components/TextInput/TextInput.figma.tsx +44 -0
  231. package/src/components/TextInput/TextInput.tsx +42 -0
  232. package/src/components/TextInput/index.ts +4 -0
  233. package/src/components/ThemeSwitcher.figma.tsx +28 -0
  234. package/src/components/ThemeSwitcher.tsx +69 -0
  235. package/src/components/TrendBadge/TrendBadge.tsx +76 -0
  236. package/src/components/TrendBadge/index.ts +2 -0
  237. package/src/components/TruncatedText.tsx +115 -0
  238. package/src/components/Typography/Text.tsx +74 -0
  239. package/src/components/Typography/Title.tsx +100 -0
  240. package/src/components/Typography/index.ts +4 -0
  241. package/src/geist-fonts.ts +48 -0
  242. package/src/hooks/index.ts +31 -0
  243. package/src/hooks/useFilters.ts +152 -0
  244. package/src/hooks/useInfiniteScroll.ts +62 -0
  245. package/src/hooks/usePlatform.ts +33 -0
  246. package/src/hooks/useServerTable.ts +495 -0
  247. package/src/hooks/useTableSelection.ts +102 -0
  248. package/src/hooks/useTableSort.ts +259 -0
  249. package/src/index.ts +483 -0
  250. package/src/mantine.ts +25 -0
  251. package/src/theme/mantineVars.ts +12 -0
  252. package/src/theme/themeContract.css.ts +131 -0
  253. package/src/theme/themeVars.ts +31 -0
  254. package/src/theme.ts +168 -0
  255. package/src/tokens/color-types.ts +107 -0
  256. package/src/tokens/colors.ts +243 -0
  257. package/src/tokens/index.ts +14 -0
  258. package/src/tokens/radius.ts +17 -0
  259. package/src/tokens/semantic-colors.ts +224 -0
  260. package/src/tokens/semantic-tokens-css.ts +53 -0
  261. package/src/tokens/shadows.ts +11 -0
  262. package/src/tokens/spacing.ts +20 -0
  263. package/src/tokens/text-styles.ts +179 -0
  264. package/src/tokens/typography.ts +40 -0
  265. package/src/tokens/zIndex.ts +27 -0
  266. package/src/types/mantine-theme.d.ts +17 -0
  267. package/src/types/tanstack-table.d.ts +22 -0
  268. package/src/utils/avatar.ts +150 -0
  269. package/src/utils/chartHelpers.ts +53 -0
  270. package/src/utils/color-props.ts +77 -0
  271. package/src/utils/createDesignComponent.tsx +104 -0
  272. package/src/utils/nestFlatRows.ts +111 -0
  273. 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
+ }