@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,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
+ }