@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,236 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ type ReactNode,
7
+ } from 'react';
8
+
9
+ import { Maximize2, X } from 'lucide-react';
10
+
11
+ import {
12
+ Box,
13
+ Flex,
14
+ Group,
15
+ Drawer as MantineDrawer,
16
+ type DrawerProps as MantineDrawerProps,
17
+ } from '@mantine/core';
18
+ import { useMediaQuery } from '@mantine/hooks';
19
+
20
+ import { IconButton } from '../IconButton';
21
+ import { Title } from '../Typography';
22
+ import * as styles from './Drawer.css';
23
+
24
+ export interface DrawerProps extends Omit<
25
+ MantineDrawerProps,
26
+ 'opened' | 'onClose' | 'position'
27
+ > {
28
+ /** Whether the drawer is open */
29
+ opened: boolean;
30
+ /** Callback when drawer should close */
31
+ onClose: () => void;
32
+ /** Title to display in the drawer header */
33
+ title?: ReactNode;
34
+ /** Default width of the drawer (default: 400px) */
35
+ defaultWidth?: number;
36
+ /** Minimum width of the drawer (default: 200px) */
37
+ minWidth?: number;
38
+ /** Maximum width of the drawer (default: 90% of viewport) */
39
+ maxWidth?: number;
40
+ /** Whether to close the drawer when clicking/pressing outside or pressing Escape (default: true) */
41
+ closeOnFocusOutside?: boolean;
42
+ /** Actions to render in the top right corner of the drawer */
43
+ topRightActions?: ReactNode;
44
+ /** Test ID for UI testing */
45
+ testId?: string;
46
+ /** Children to render inside the drawer */
47
+ children: ReactNode;
48
+ }
49
+
50
+ /**
51
+ * Drawer component with resize functionality
52
+ * Opens from the right by default, can be resized by dragging, and includes full-width controls
53
+ * On small screens (md breakpoint and below), opens full screen and hides resize controls
54
+ */
55
+ export function Drawer({
56
+ opened,
57
+ onClose,
58
+ title,
59
+ defaultWidth = 400,
60
+ minWidth = 200,
61
+ maxWidth,
62
+ children,
63
+ closeOnFocusOutside = true,
64
+ topRightActions,
65
+ testId,
66
+ ...props
67
+ }: DrawerProps) {
68
+ const isSmallScreen = useMediaQuery('(max-width: 64em)'); // md breakpoint (breakpoint 3) and below
69
+ const [width, setWidth] = useState(defaultWidth);
70
+ const [isFullWidth, setIsFullWidth] = useState(false);
71
+ const [isResizing, setIsResizing] = useState(false);
72
+ const drawerRef = useRef<HTMLDivElement>(null);
73
+ const resizeHandleRef = useRef<HTMLDivElement>(null);
74
+
75
+ // Calculate max width if not provided (in pixels for clamping calculations)
76
+ const effectiveMaxWidth =
77
+ maxWidth ??
78
+ (typeof window !== 'undefined' ? window.innerWidth * 0.9 : 1200);
79
+
80
+ // Reset state when drawer closes
81
+ useEffect(() => {
82
+ if (!opened) {
83
+ setWidth(defaultWidth);
84
+ setIsFullWidth(false);
85
+ setIsResizing(false);
86
+ }
87
+ }, [opened, defaultWidth]);
88
+
89
+ // Reset width when drawer opens
90
+ useEffect(() => {
91
+ if (opened && !isSmallScreen) {
92
+ if (isFullWidth) {
93
+ // Full width uses 100vw, but we don't need to set numeric width for it
94
+ // since drawerWidth will use '100vw' string
95
+ } else {
96
+ setWidth(defaultWidth);
97
+ }
98
+ }
99
+ }, [opened, isSmallScreen, isFullWidth, defaultWidth]);
100
+
101
+ // Handle mouse down on resize handle
102
+ const handleMouseDown = useCallback(
103
+ (e: React.MouseEvent) => {
104
+ if (isSmallScreen || isFullWidth) {
105
+ return;
106
+ }
107
+ e.preventDefault();
108
+ setIsResizing(true);
109
+ },
110
+ [isSmallScreen, isFullWidth],
111
+ );
112
+
113
+ // Handle mouse move for resizing
114
+ useEffect(() => {
115
+ if (!isResizing) {
116
+ return;
117
+ }
118
+
119
+ const handleMouseMove = (e: MouseEvent) => {
120
+ if (!drawerRef.current) {
121
+ return;
122
+ }
123
+
124
+ const newWidth = window.innerWidth - e.clientX;
125
+
126
+ // Clamp width between min and max
127
+ const clampedWidth = Math.max(
128
+ minWidth,
129
+ Math.min(newWidth, effectiveMaxWidth),
130
+ );
131
+ setWidth(clampedWidth);
132
+ };
133
+
134
+ const handleMouseUp = () => {
135
+ setIsResizing(false);
136
+ };
137
+
138
+ document.addEventListener('mousemove', handleMouseMove);
139
+ document.addEventListener('mouseup', handleMouseUp);
140
+
141
+ return () => {
142
+ document.removeEventListener('mousemove', handleMouseMove);
143
+ document.removeEventListener('mouseup', handleMouseUp);
144
+ };
145
+ }, [isResizing, minWidth, effectiveMaxWidth]);
146
+
147
+ // Handle full width toggle
148
+ const handleFullWidth = useCallback(() => {
149
+ if (isFullWidth) {
150
+ setIsFullWidth(false);
151
+ setWidth(defaultWidth);
152
+ } else {
153
+ setIsFullWidth(true);
154
+ // Full width uses 100vw, no need to set numeric width
155
+ }
156
+ }, [isFullWidth, defaultWidth]);
157
+
158
+ // Determine drawer width based on state
159
+ const drawerWidth = isSmallScreen ? '100vw' : isFullWidth ? '100vw' : width;
160
+
161
+ return (
162
+ <MantineDrawer
163
+ {...props}
164
+ opened={opened}
165
+ onClose={onClose}
166
+ position="right"
167
+ size={drawerWidth}
168
+ withCloseButton={false}
169
+ closeOnClickOutside={closeOnFocusOutside}
170
+ closeOnEscape={closeOnFocusOutside}
171
+ trapFocus={false}
172
+ pos="relative"
173
+ padding={0}
174
+ ref={drawerRef}
175
+ >
176
+ <Box data-testid={testId}>
177
+ {/* Resize handle - only visible on larger screens when not full width */}
178
+ {!isSmallScreen && !isFullWidth && (
179
+ <Box
180
+ ref={resizeHandleRef}
181
+ className={`${styles.resizeHandle} ${isResizing ? styles.resizing : ''}`}
182
+ onMouseDown={handleMouseDown}
183
+ />
184
+ )}
185
+
186
+ {/* Sticky Header */}
187
+ <Flex
188
+ align="center"
189
+ justify="space-between"
190
+ px="lg"
191
+ py="lg"
192
+ className={styles.stickyHeader}
193
+ >
194
+ <Group gap="xs">
195
+ {/* Full width button - only on larger screens */}
196
+ {!isSmallScreen && (
197
+ <IconButton
198
+ variant="ghost"
199
+ color="gray"
200
+ onClick={handleFullWidth}
201
+ title={
202
+ isFullWidth ? 'Restore default width' : 'Make full width'
203
+ }
204
+ data-testid="expand-drawer-button"
205
+ >
206
+ <Maximize2 size={16} />
207
+ </IconButton>
208
+ )}
209
+
210
+ {/* Close button */}
211
+ <IconButton
212
+ variant="ghost"
213
+ color="gray"
214
+ onClick={onClose}
215
+ title="Close drawer"
216
+ data-testid="close-drawer-button"
217
+ >
218
+ <X size={16} />
219
+ </IconButton>
220
+ </Group>
221
+ {topRightActions}
222
+ </Flex>
223
+
224
+ {/* Content */}
225
+ <Box px="lg" pb="lg">
226
+ {title && (
227
+ <Title variant="heading4" pt="lg">
228
+ {title}
229
+ </Title>
230
+ )}
231
+ {children}
232
+ </Box>
233
+ </Box>
234
+ </MantineDrawer>
235
+ );
236
+ }
@@ -0,0 +1,61 @@
1
+ import { useCallback } from 'react';
2
+
3
+ export interface DetailDrawerSearch {
4
+ detailId?: string;
5
+ }
6
+
7
+ export interface UseDetailDrawerOptions {
8
+ detailId: string | undefined;
9
+ navigate: (options: {
10
+ to: string;
11
+ search: DetailDrawerSearch;
12
+ }) => void | Promise<void>;
13
+ pathname: string;
14
+ }
15
+
16
+ export interface UseDetailDrawerReturn {
17
+ detailId: string | undefined;
18
+ isOpen: boolean;
19
+ openDetail: (id: string) => void;
20
+ closeDetail: () => void;
21
+ }
22
+
23
+ /**
24
+ * Hook for syncing drawer state with caller-provided navigation/search state.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const navigate = useNavigate();
29
+ * const pathname = useRouterState({ select: (state) => state.location.pathname });
30
+ * const { detailId } = Route.useSearch();
31
+ *
32
+ * const { isOpen, openDetail, closeDetail } = useDetailDrawer({
33
+ * detailId,
34
+ * navigate,
35
+ * pathname,
36
+ * });
37
+ * ```
38
+ */
39
+ export function useDetailDrawer({
40
+ detailId,
41
+ navigate,
42
+ pathname,
43
+ }: UseDetailDrawerOptions): UseDetailDrawerReturn {
44
+ const openDetail = useCallback(
45
+ (id: string) => {
46
+ void navigate({ to: pathname, search: { detailId: id } });
47
+ },
48
+ [navigate, pathname],
49
+ );
50
+
51
+ const closeDetail = useCallback(() => {
52
+ void navigate({ to: pathname, search: {} });
53
+ }, [navigate, pathname]);
54
+
55
+ return {
56
+ detailId,
57
+ isOpen: !!detailId,
58
+ openDetail,
59
+ closeDetail,
60
+ };
61
+ }
@@ -0,0 +1,125 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+
3
+ export interface UseDetailDrawerNavigationOptions {
4
+ detailId?: string;
5
+ orderedIds: string[];
6
+ openDetail: (id: string) => void;
7
+ hasNextPage: boolean;
8
+ isFetchingNextPage: boolean;
9
+ fetchNextPage: () => void | Promise<void>;
10
+ }
11
+
12
+ export interface UseDetailDrawerNavigationReturn {
13
+ currentIndex: number;
14
+ isCurrentLoaded: boolean;
15
+ canGoPrev: boolean;
16
+ canGoNext: boolean;
17
+ goPrev: () => Promise<void>;
18
+ goNext: () => Promise<void>;
19
+ }
20
+
21
+ interface PendingAdvance {
22
+ fromId: string;
23
+ }
24
+
25
+ export function useDetailDrawerNavigation({
26
+ detailId,
27
+ orderedIds,
28
+ openDetail,
29
+ hasNextPage,
30
+ isFetchingNextPage,
31
+ fetchNextPage,
32
+ }: UseDetailDrawerNavigationOptions): UseDetailDrawerNavigationReturn {
33
+ const [pendingAdvance, setPendingAdvance] = useState<PendingAdvance | null>(
34
+ null,
35
+ );
36
+
37
+ const currentIndex = useMemo(() => {
38
+ if (!detailId) {
39
+ return -1;
40
+ }
41
+
42
+ return orderedIds.indexOf(detailId);
43
+ }, [detailId, orderedIds]);
44
+
45
+ const isCurrentLoaded = currentIndex !== -1;
46
+ const canGoPrev = isCurrentLoaded && currentIndex > 0;
47
+ const canGoNext =
48
+ isCurrentLoaded && (currentIndex < orderedIds.length - 1 || hasNextPage);
49
+
50
+ useEffect(() => {
51
+ if (!pendingAdvance) {
52
+ return;
53
+ }
54
+
55
+ if (detailId !== pendingAdvance.fromId) {
56
+ setPendingAdvance(null);
57
+ return;
58
+ }
59
+
60
+ const pendingIndex = orderedIds.indexOf(pendingAdvance.fromId);
61
+ const nextId =
62
+ pendingIndex === -1 ? undefined : orderedIds[pendingIndex + 1];
63
+
64
+ if (nextId) {
65
+ setPendingAdvance(null);
66
+ openDetail(nextId);
67
+ return;
68
+ }
69
+
70
+ if (!hasNextPage && !isFetchingNextPage) {
71
+ setPendingAdvance(null);
72
+ }
73
+ }, [
74
+ detailId,
75
+ hasNextPage,
76
+ isFetchingNextPage,
77
+ openDetail,
78
+ orderedIds,
79
+ pendingAdvance,
80
+ ]);
81
+
82
+ const goPrev = useCallback(async () => {
83
+ if (!canGoPrev) {
84
+ return;
85
+ }
86
+
87
+ openDetail(orderedIds[currentIndex - 1]);
88
+ }, [canGoPrev, currentIndex, openDetail, orderedIds]);
89
+
90
+ const goNext = useCallback(async () => {
91
+ if (!canGoNext || !detailId) {
92
+ return;
93
+ }
94
+
95
+ const nextId = orderedIds[currentIndex + 1];
96
+
97
+ if (nextId) {
98
+ openDetail(nextId);
99
+ return;
100
+ }
101
+
102
+ if (hasNextPage && !isFetchingNextPage) {
103
+ setPendingAdvance({ fromId: detailId });
104
+ await Promise.resolve(fetchNextPage());
105
+ }
106
+ }, [
107
+ canGoNext,
108
+ currentIndex,
109
+ detailId,
110
+ fetchNextPage,
111
+ hasNextPage,
112
+ isFetchingNextPage,
113
+ openDetail,
114
+ orderedIds,
115
+ ]);
116
+
117
+ return {
118
+ currentIndex,
119
+ isCurrentLoaded,
120
+ canGoPrev,
121
+ canGoNext,
122
+ goPrev,
123
+ goNext,
124
+ };
125
+ }
@@ -0,0 +1,66 @@
1
+ import { useEffect, useMemo } from 'react';
2
+
3
+ import type { InfiniteScrollState } from '../../InfiniteScrollTrigger';
4
+
5
+ export interface DetailDrawerNavigationContext<TRow = never> {
6
+ orderedIds: string[];
7
+ hasNextPage: boolean;
8
+ isFetchingNextPage: boolean;
9
+ fetchNextPage: () => void | Promise<void>;
10
+ getRowById?: (id: string) => TRow | null;
11
+ }
12
+
13
+ export interface UseDetailDrawerNavigationContextOptions<TRow> {
14
+ data: TRow[];
15
+ getRowId: (row: TRow) => string;
16
+ pagination: Pick<
17
+ InfiniteScrollState,
18
+ 'fetchNextPage' | 'hasNextPage' | 'isFetchingNextPage'
19
+ >;
20
+ onNavigationContextChange?: (
21
+ context: DetailDrawerNavigationContext<TRow>,
22
+ ) => void;
23
+ includeRowLookup?: boolean;
24
+ }
25
+
26
+ export function useDetailDrawerNavigationContext<TRow>({
27
+ data,
28
+ getRowId,
29
+ pagination,
30
+ onNavigationContextChange,
31
+ includeRowLookup = false,
32
+ }: UseDetailDrawerNavigationContextOptions<TRow>): DetailDrawerNavigationContext<TRow> {
33
+ const rowsById = useMemo(
34
+ () =>
35
+ includeRowLookup
36
+ ? (Object.fromEntries(
37
+ data.map(row => [getRowId(row), row] as const),
38
+ ) as Record<string, TRow>)
39
+ : null,
40
+ [data, getRowId, includeRowLookup],
41
+ );
42
+
43
+ const navigationContext = useMemo<DetailDrawerNavigationContext<TRow>>(
44
+ () => ({
45
+ orderedIds: data.map(row => getRowId(row)),
46
+ hasNextPage: pagination.hasNextPage,
47
+ isFetchingNextPage: pagination.isFetchingNextPage,
48
+ fetchNextPage: pagination.fetchNextPage,
49
+ getRowById: rowsById ? (id: string) => rowsById[id] ?? null : undefined,
50
+ }),
51
+ [
52
+ data,
53
+ getRowId,
54
+ pagination.fetchNextPage,
55
+ pagination.hasNextPage,
56
+ pagination.isFetchingNextPage,
57
+ rowsById,
58
+ ],
59
+ );
60
+
61
+ useEffect(() => {
62
+ onNavigationContextChange?.(navigationContext);
63
+ }, [navigationContext, onNavigationContextChange]);
64
+
65
+ return navigationContext;
66
+ }
@@ -0,0 +1,72 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ import { tokens } from '../../theme/themeContract.css';
4
+
5
+ export const root = style({
6
+ position: 'relative',
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ gap: tokens.spacing.xs,
10
+ borderRadius: tokens.radius.lg,
11
+ border: '1px solid transparent',
12
+ padding: `${tokens.spacing.xs} ${tokens.spacing.sm}`,
13
+ margin: `calc(-1 * ${tokens.spacing.xs}) calc(-1 * ${tokens.spacing.sm})`,
14
+ transition: 'border-color 120ms ease, background-color 120ms ease',
15
+ cursor: 'text',
16
+ selectors: {
17
+ '&:hover:not([aria-disabled="true"])': {
18
+ borderColor: tokens.color.stroke.subduedDefault,
19
+ },
20
+ },
21
+ });
22
+
23
+ export const rootFocused = style({
24
+ borderColor: tokens.color.stroke.focusDefault,
25
+ background: tokens.color.background.input,
26
+ });
27
+
28
+ export const rootDisabled = style({
29
+ cursor: 'default',
30
+ opacity: 0.6,
31
+ });
32
+
33
+ export const toolbar = style({
34
+ display: 'flex',
35
+ alignItems: 'center',
36
+ gap: tokens.spacing.xs,
37
+ paddingBottom: tokens.spacing.xs,
38
+ borderBottom: `1px solid ${tokens.color.stroke.subduedDefault}`,
39
+ flexWrap: 'wrap',
40
+ });
41
+
42
+ export const contentWrapper = style({
43
+ position: 'relative',
44
+ minHeight: '1.5rem',
45
+ });
46
+
47
+ export const placeholder = style({
48
+ position: 'absolute',
49
+ inset: 0,
50
+ color: tokens.color.text.subduedDefault,
51
+ pointerEvents: 'none',
52
+ userSelect: 'none',
53
+ });
54
+
55
+ export const errorRow = style({
56
+ display: 'flex',
57
+ alignItems: 'center',
58
+ gap: tokens.spacing.sm,
59
+ flexWrap: 'wrap',
60
+ });
61
+
62
+ export const srOnlyTextarea = style({
63
+ position: 'absolute',
64
+ width: 1,
65
+ height: 1,
66
+ padding: 0,
67
+ margin: -1,
68
+ overflow: 'hidden',
69
+ clip: 'rect(0, 0, 0, 0)',
70
+ whiteSpace: 'nowrap',
71
+ border: 0,
72
+ });