@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,329 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import { Box, TextInput } from '@mantine/core';
4
+ import type { DateValue } from '@mantine/dates';
5
+
6
+ import {
7
+ createDate,
8
+ getDayOfMonth,
9
+ getDaysInMonth,
10
+ getMonth,
11
+ getYear,
12
+ isValidDate,
13
+ } from '@scalepad/ui-utils/date';
14
+
15
+ import { Text } from '../Typography';
16
+
17
+ export interface DateInputProps {
18
+ date: DateValue | null | undefined;
19
+ onChange: (date: DateValue | null) => void;
20
+ }
21
+
22
+ export function DateInput({ date, onChange }: DateInputProps) {
23
+ const dayRef = useRef<HTMLInputElement>(null);
24
+ const monthRef = useRef<HTMLInputElement>(null);
25
+ const yearRef = useRef<HTMLInputElement>(null);
26
+
27
+ const [day, setDay] = useState<string>('');
28
+ const [month, setMonth] = useState<string>('');
29
+ const [year, setYear] = useState<string>('');
30
+
31
+ // Initialize from date prop
32
+ useEffect(() => {
33
+ if (date) {
34
+ setDay(String(getDayOfMonth(date)));
35
+ setMonth(String(getMonth(date) + 1)); // getMonth returns 0-11
36
+ setYear(String(getYear(date)));
37
+ } else {
38
+ setDay('');
39
+ setMonth('');
40
+ setYear('');
41
+ }
42
+ }, [date]);
43
+
44
+ const updateDate = useCallback(
45
+ (newDay: string, newMonth: string, newYear: string) => {
46
+ // If all fields are empty, return null
47
+ if (!newDay && !newMonth && !newYear) {
48
+ onChange(null);
49
+ return;
50
+ }
51
+
52
+ // Only construct date if we have all three parts
53
+ if (!newDay || !newMonth || !newYear) {
54
+ return;
55
+ }
56
+
57
+ // Try to construct a valid date
58
+ const dayNum = parseInt(newDay, 10);
59
+ const monthNum = parseInt(newMonth, 10);
60
+ const yearNum = parseInt(newYear, 10);
61
+
62
+ // Validate ranges
63
+ if (isNaN(dayNum) || dayNum < 1 || dayNum > 31) {
64
+ return;
65
+ }
66
+ if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) {
67
+ return;
68
+ }
69
+ if (isNaN(yearNum) || yearNum < 1 || yearNum > 9999) {
70
+ return;
71
+ }
72
+
73
+ // Use createDate (month is 0-indexed)
74
+ const constructedDate = createDate(yearNum, monthNum - 1, dayNum);
75
+
76
+ // Check if date is valid
77
+ if (isValidDate(constructedDate)) {
78
+ onChange(constructedDate);
79
+ }
80
+ },
81
+ [onChange],
82
+ );
83
+
84
+ // Get maximum days in a month for given month and year
85
+ const getMaxDaysInMonth = useCallback(
86
+ (monthStr: string, yearStr: string): number => {
87
+ if (!monthStr || !yearStr) {
88
+ return 31; // Default to 31 if month/year not provided
89
+ }
90
+
91
+ const monthNum = parseInt(monthStr, 10);
92
+ const yearNum = parseInt(yearStr, 10);
93
+
94
+ if (isNaN(monthNum) || isNaN(yearNum) || monthNum < 1 || monthNum > 12) {
95
+ return 31; // Default to 31 if invalid
96
+ }
97
+
98
+ // Use our getDaysInMonth helper (handles leap years)
99
+ const testDate = createDate(yearNum, monthNum - 1, 1);
100
+ if (!isValidDate(testDate)) {
101
+ return 31; // Default to 31 if invalid date
102
+ }
103
+
104
+ return getDaysInMonth(testDate);
105
+ },
106
+ [],
107
+ );
108
+
109
+ const handleDayChange = useCallback(
110
+ (value: string) => {
111
+ // Only filter non-digits, take input verbatim otherwise
112
+ const digitsOnly = value.replace(/\D/g, '');
113
+
114
+ // If empty, allow it
115
+ if (!digitsOnly) {
116
+ setDay('');
117
+ updateDate('', month, year);
118
+ return;
119
+ }
120
+
121
+ // Get max days for the current month/year
122
+ const maxDays = getMaxDaysInMonth(month, year);
123
+
124
+ // Parse the number and clamp between 1 and maxDays
125
+ const dayNum = parseInt(digitsOnly, 10);
126
+ let clampedDay: string;
127
+
128
+ if (isNaN(dayNum)) {
129
+ clampedDay = '';
130
+ } else if (dayNum < 1) {
131
+ clampedDay = '1';
132
+ } else if (dayNum > maxDays) {
133
+ clampedDay = maxDays.toString();
134
+ } else {
135
+ clampedDay = digitsOnly;
136
+ }
137
+
138
+ setDay(clampedDay);
139
+ updateDate(clampedDay, month, year);
140
+ },
141
+ [month, year, updateDate, getMaxDaysInMonth],
142
+ );
143
+
144
+ const handleMonthChange = useCallback(
145
+ (value: string) => {
146
+ // Only filter non-digits, take input verbatim otherwise
147
+ const digitsOnly = value.replace(/\D/g, '');
148
+
149
+ // If empty, allow it
150
+ if (!digitsOnly) {
151
+ setMonth('');
152
+ updateDate(day, '', year);
153
+ return;
154
+ }
155
+
156
+ // Parse the number and clamp between 1 and 12
157
+ const monthNum = parseInt(digitsOnly, 10);
158
+ let clampedMonth: string;
159
+
160
+ if (isNaN(monthNum)) {
161
+ clampedMonth = '';
162
+ } else if (monthNum < 1) {
163
+ clampedMonth = '1';
164
+ } else if (monthNum > 12) {
165
+ clampedMonth = '12';
166
+ } else {
167
+ clampedMonth = digitsOnly;
168
+ }
169
+
170
+ setMonth(clampedMonth);
171
+
172
+ // Re-validate day if it exceeds max days in new month
173
+ if (day && year) {
174
+ const maxDays = getMaxDaysInMonth(clampedMonth, year);
175
+ const dayNum = parseInt(day, 10);
176
+ if (!isNaN(dayNum) && dayNum > maxDays) {
177
+ const adjustedDay = maxDays.toString();
178
+ setDay(adjustedDay);
179
+ updateDate(adjustedDay, clampedMonth, year);
180
+ } else {
181
+ updateDate(day, clampedMonth, year);
182
+ }
183
+ } else {
184
+ updateDate(day, clampedMonth, year);
185
+ }
186
+ },
187
+ [day, year, updateDate, getMaxDaysInMonth],
188
+ );
189
+
190
+ const handleYearChange = useCallback(
191
+ (value: string) => {
192
+ // Only filter non-digits, take input verbatim otherwise
193
+ const digitsOnly = value.replace(/\D/g, '');
194
+ // Limit to 4 digits
195
+ const limited = digitsOnly.slice(0, 4);
196
+ setYear(limited);
197
+
198
+ // Only validate and update date when year is complete (4 digits)
199
+ // This allows users to type partial years without manipulation
200
+ if (limited.length === 4) {
201
+ // Re-validate day if it exceeds max days in month for new year (handles leap years)
202
+ if (day && month) {
203
+ const maxDays = getMaxDaysInMonth(month, limited);
204
+ const dayNum = parseInt(day, 10);
205
+ if (!isNaN(dayNum) && dayNum > maxDays) {
206
+ const adjustedDay = maxDays.toString();
207
+ setDay(adjustedDay);
208
+ updateDate(adjustedDay, month, limited);
209
+ } else {
210
+ updateDate(day, month, limited);
211
+ }
212
+ } else {
213
+ updateDate(day, month, limited);
214
+ }
215
+ }
216
+ // If year is incomplete, don't call updateDate - just store the raw input
217
+ },
218
+ [day, month, updateDate, getMaxDaysInMonth],
219
+ );
220
+
221
+ const handleDayFocus = useCallback(
222
+ (e: React.FocusEvent<HTMLInputElement>) => {
223
+ e.target.select();
224
+ },
225
+ [],
226
+ );
227
+
228
+ const handleMonthFocus = useCallback(
229
+ (e: React.FocusEvent<HTMLInputElement>) => {
230
+ e.target.select();
231
+ },
232
+ [],
233
+ );
234
+
235
+ const handleYearFocus = useCallback(
236
+ (e: React.FocusEvent<HTMLInputElement>) => {
237
+ e.target.select();
238
+ },
239
+ [],
240
+ );
241
+
242
+ return (
243
+ <Box
244
+ style={{
245
+ display: 'inline-flex',
246
+ alignItems: 'center',
247
+ border: '1px solid var(--color-stroke-default)',
248
+ borderRadius: 'var(--radius-sm)',
249
+ gap: 0,
250
+ }}
251
+ >
252
+ <TextInput
253
+ ref={dayRef}
254
+ value={day}
255
+ onChange={e => handleDayChange(e.target.value)}
256
+ onFocus={handleDayFocus}
257
+ placeholder="DD"
258
+ style={{
259
+ width: 36,
260
+ border: 'none',
261
+ paddingLeft: 0,
262
+ paddingRight: 0,
263
+ }}
264
+ styles={{
265
+ input: {
266
+ border: 'none',
267
+ boxShadow: 'none',
268
+ padding: '0px',
269
+ textAlign: 'center',
270
+ },
271
+ }}
272
+ size="sm"
273
+ variant="unstyled"
274
+ />
275
+ <Text variant="caption1" c="text.subdued.default">
276
+ /
277
+ </Text>
278
+ <TextInput
279
+ ref={monthRef}
280
+ value={month}
281
+ onChange={e => handleMonthChange(e.target.value)}
282
+ onFocus={handleMonthFocus}
283
+ placeholder="MM"
284
+ style={{
285
+ width: 28,
286
+ border: 'none',
287
+ paddingLeft: 0,
288
+ paddingRight: 0,
289
+ }}
290
+ styles={{
291
+ input: {
292
+ border: 'none',
293
+ boxShadow: 'none',
294
+ padding: '0px',
295
+ textAlign: 'center',
296
+ },
297
+ }}
298
+ size="sm"
299
+ variant="unstyled"
300
+ />
301
+ <Text variant="caption1" c="text.subdued.default">
302
+ /
303
+ </Text>
304
+ <TextInput
305
+ ref={yearRef}
306
+ value={year}
307
+ onChange={e => handleYearChange(e.target.value)}
308
+ onFocus={handleYearFocus}
309
+ placeholder="YYYY"
310
+ style={{
311
+ width: 64,
312
+ border: 'none',
313
+ paddingLeft: 0,
314
+ paddingRight: 0,
315
+ }}
316
+ styles={{
317
+ input: {
318
+ border: 'none',
319
+ boxShadow: 'none',
320
+ padding: '0px',
321
+ textAlign: 'center',
322
+ },
323
+ }}
324
+ size="sm"
325
+ variant="unstyled"
326
+ />
327
+ </Box>
328
+ );
329
+ }