@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,35 @@
1
+ // Thin re-export wrapper around `@mantine/schedule` so consumers can import the
2
+ // Schedule component and its helpers from the design system surface
3
+ // (`@scalepad/ui`) instead of reaching into Mantine directly. Keep this file
4
+ // intentionally shallow — any future DS-specific styling or wrapping should
5
+ // live alongside.
6
+
7
+ export {
8
+ DEFAULT_SCHEDULE_LABELS,
9
+ DayView,
10
+ MobileMonthView,
11
+ MonthView,
12
+ Schedule,
13
+ ScheduleEvent,
14
+ ScheduleHeader,
15
+ WeekView,
16
+ YearView,
17
+ getLabel,
18
+ } from '@mantine/schedule';
19
+ export type {
20
+ DateStringValue,
21
+ DateTimeStringValue,
22
+ DayViewProps,
23
+ MonthViewProps,
24
+ RenderEventBody,
25
+ ScheduleEventData,
26
+ ScheduleLabels,
27
+ ScheduleLabelsOverride,
28
+ ScheduleLayout,
29
+ ScheduleMode,
30
+ ScheduleProps,
31
+ ScheduleRecurrenceData,
32
+ ScheduleViewLevel,
33
+ WeekViewProps,
34
+ YearViewProps,
35
+ } from '@mantine/schedule';
@@ -0,0 +1,42 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ import { tokens } from '../../theme/themeContract.css';
4
+
5
+ export const quarterGrid = style({
6
+ display: 'grid',
7
+ gridTemplateColumns: 'repeat(4, 1fr)',
8
+ gap: tokens.spacing['2xs'],
9
+ });
10
+
11
+ export const quarterButton = style({
12
+ padding: `${tokens.spacing['2xs']} 0`,
13
+ border: `1px solid ${tokens.color.stroke.subduedDefault}`,
14
+ backgroundColor: tokens.color.background.default,
15
+ borderRadius: tokens.radius.sm,
16
+ cursor: 'pointer',
17
+ color: tokens.color.text.default,
18
+
19
+ ':hover': {
20
+ backgroundColor: tokens.color.background.subduedLight,
21
+ },
22
+ ':focus-visible': {
23
+ outline: 'none',
24
+ borderColor: tokens.color.stroke.focusDefault,
25
+ },
26
+ });
27
+
28
+ export const quarterButtonSelected = style({
29
+ backgroundColor: tokens.color.background.subduedLight,
30
+ borderColor: tokens.color.stroke.default,
31
+ });
32
+
33
+ export const yearRow = style({
34
+ display: 'flex',
35
+ alignItems: 'center',
36
+ gap: tokens.spacing['2xs'],
37
+ });
38
+
39
+ export const yearValue = style({
40
+ flex: 1,
41
+ textAlign: 'center',
42
+ });
@@ -0,0 +1,130 @@
1
+ import { useMemo, type KeyboardEvent as ReactKeyboardEvent } from 'react';
2
+
3
+ import { Minus, Plus } from 'lucide-react';
4
+
5
+ import { IconButton } from '../IconButton';
6
+ import { Text } from '../Typography';
7
+ import * as classes from './SchedulePicker.css';
8
+
9
+ type RadioOrientation = 'vertical' | 'horizontal';
10
+
11
+ function handleRadioGroupKeyDown(
12
+ event: ReactKeyboardEvent<HTMLDivElement>,
13
+ orientation: RadioOrientation,
14
+ ) {
15
+ const forwardKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
16
+ const backwardKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
17
+ const { key } = event;
18
+
19
+ if (
20
+ key !== forwardKey &&
21
+ key !== backwardKey &&
22
+ key !== 'Home' &&
23
+ key !== 'End'
24
+ ) {
25
+ return;
26
+ }
27
+
28
+ const container = event.currentTarget;
29
+ const options = Array.from(
30
+ container.querySelectorAll<HTMLElement>('[data-picker-option]'),
31
+ );
32
+ if (options.length === 0) return;
33
+
34
+ event.preventDefault();
35
+ event.stopPropagation();
36
+
37
+ const active = document.activeElement as HTMLElement | null;
38
+ const currentIndex = active ? options.indexOf(active) : -1;
39
+
40
+ let nextIndex: number;
41
+ if (key === 'Home') {
42
+ nextIndex = 0;
43
+ } else if (key === 'End') {
44
+ nextIndex = options.length - 1;
45
+ } else if (currentIndex === -1) {
46
+ nextIndex = 0;
47
+ } else if (key === forwardKey) {
48
+ nextIndex = (currentIndex + 1) % options.length;
49
+ } else {
50
+ nextIndex = (currentIndex - 1 + options.length) % options.length;
51
+ }
52
+
53
+ options[nextIndex]?.focus();
54
+ }
55
+
56
+ const YEAR_MIN = 2000;
57
+ const YEAR_MAX = 2100;
58
+
59
+ export interface FiscalQuarterValue {
60
+ year: number;
61
+ quarter: number;
62
+ }
63
+
64
+ export interface SchedulePickerProps {
65
+ value: FiscalQuarterValue;
66
+ onChange: (value: FiscalQuarterValue) => void;
67
+ }
68
+
69
+ export function SchedulePicker({ value, onChange }: SchedulePickerProps) {
70
+ const quarters = useMemo(() => [1, 2, 3, 4] as const, []);
71
+ const clampYear = (year: number) =>
72
+ Math.min(YEAR_MAX, Math.max(YEAR_MIN, year));
73
+
74
+ return (
75
+ <>
76
+ <div
77
+ className={classes.quarterGrid}
78
+ role="radiogroup"
79
+ aria-label="Quarter"
80
+ tabIndex={-1}
81
+ onKeyDown={event => handleRadioGroupKeyDown(event, 'horizontal')}
82
+ >
83
+ {quarters.map(q => {
84
+ const selected = value.quarter === q;
85
+ return (
86
+ <button
87
+ key={q}
88
+ type="button"
89
+ role="radio"
90
+ aria-checked={selected}
91
+ data-picker-option
92
+ data-autofocus={selected ? true : undefined}
93
+ className={`${classes.quarterButton} ${
94
+ selected ? classes.quarterButtonSelected : ''
95
+ }`.trim()}
96
+ onClick={() => onChange({ ...value, quarter: q })}
97
+ >
98
+ <Text variant="caption1.strong">Q{q}</Text>
99
+ </button>
100
+ );
101
+ })}
102
+ </div>
103
+ <div className={classes.yearRow}>
104
+ <IconButton
105
+ aria-label="Previous year"
106
+ variant="ghost-muted"
107
+ size="xs"
108
+ onClick={() =>
109
+ onChange({ ...value, year: clampYear(value.year - 1) })
110
+ }
111
+ >
112
+ <Minus size={14} />
113
+ </IconButton>
114
+ <span className={classes.yearValue}>
115
+ <Text variant="body1.strong">{value.year}</Text>
116
+ </span>
117
+ <IconButton
118
+ aria-label="Next year"
119
+ variant="ghost-muted"
120
+ size="xs"
121
+ onClick={() =>
122
+ onChange({ ...value, year: clampYear(value.year + 1) })
123
+ }
124
+ >
125
+ <Plus size={14} />
126
+ </IconButton>
127
+ </div>
128
+ </>
129
+ );
130
+ }
@@ -0,0 +1,2 @@
1
+ export { SchedulePicker } from './SchedulePicker';
2
+ export type { FiscalQuarterValue, SchedulePickerProps } from './SchedulePicker';
@@ -0,0 +1,30 @@
1
+ export type SearchableListSearchMode = 'client' | 'server';
2
+
3
+ /**
4
+ * Generic contract for searchable lists that may be backed by an API.
5
+ * Supports initial loading, error + retry, and infinite scroll pagination.
6
+ *
7
+ * This is intentionally UI-library-agnostic so it can be reused by multiple components.
8
+ */
9
+ export interface SearchableListAsyncProps {
10
+ /** Whether options are currently loading (initial load). */
11
+ isLoading?: boolean;
12
+ /** Whether loading options failed. */
13
+ isError?: boolean;
14
+ /** Optional error message to display when `isError` is true. */
15
+ errorMessage?: string;
16
+ /** Retry loading options (shown when `isError` is true). */
17
+ onRetry?: () => void;
18
+ /** Whether more options are available to load (enables infinite scroll). */
19
+ hasMore?: boolean;
20
+ /** Loads the next page of options (triggered by infinite scroll). */
21
+ onLoadMore?: () => void;
22
+ /** Whether the next page is currently loading. */
23
+ isLoadingMore?: boolean;
24
+ /** Search behavior: client filters loaded items; server calls `onSearchChange`. */
25
+ searchMode?: SearchableListSearchMode;
26
+ /** Called when search query changes (primarily for server-side search). */
27
+ onSearchChange?: (query: string) => void;
28
+ /** Debounce time for `onSearchChange` (defaults to 250ms in the UI component). */
29
+ searchDebounceMs?: number;
30
+ }
@@ -0,0 +1,25 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ import { mantineVars } from '../../theme/mantineVars';
4
+
5
+ export const menuSearchInput = style({
6
+ border: 'none !important',
7
+ boxShadow: 'none !important',
8
+ backgroundColor: 'transparent !important',
9
+ selectors: {
10
+ '&:focus': {
11
+ border: 'none !important',
12
+ boxShadow: 'none !important',
13
+ outline: 'none !important',
14
+ },
15
+ [`${mantineVars.darkSelector} &`]: {
16
+ border: 'none !important',
17
+ backgroundColor: 'transparent !important',
18
+ },
19
+ [`${mantineVars.darkSelector} &:focus`]: {
20
+ border: 'none !important',
21
+ boxShadow: 'none !important',
22
+ outline: 'none !important',
23
+ },
24
+ },
25
+ });
@@ -0,0 +1,139 @@
1
+ import { useCallback, useRef, type ReactNode } from 'react';
2
+
3
+ import { Box } from '@mantine/core';
4
+
5
+ import { Menu } from '../Menu';
6
+ import { SearchTextInput } from '../TextInput';
7
+ import * as classes from './SearchableSubMenu.css';
8
+
9
+ export interface SearchableSubMenuProps {
10
+ /** Label shown in the parent menu item */
11
+ label: string;
12
+ /** Search query state */
13
+ search: string;
14
+ /** Search query change handler */
15
+ onSearchChange: (value: string) => void;
16
+ /** Placeholder shown in the search input */
17
+ placeholder?: string;
18
+ /** Submenu dropdown width */
19
+ width?: string | number;
20
+ /** Whether to show the search input (defaults to true) */
21
+ showSearch?: boolean;
22
+ /** Main submenu content (items, empty states, etc.) */
23
+ children: ReactNode;
24
+ /** Optional footer render prop (e.g. "Create {search}") */
25
+ renderFooter?: (ctx: { search: string }) => ReactNode;
26
+ /** When set, debounces onSearchChange calls by this many ms while keeping the input responsive */
27
+ debounceMs?: number;
28
+ }
29
+
30
+ export function SearchableSubMenu({
31
+ label,
32
+ search,
33
+ onSearchChange,
34
+ placeholder = 'Filter',
35
+ width = '300px',
36
+ showSearch = true,
37
+ children,
38
+ renderFooter,
39
+ debounceMs,
40
+ }: SearchableSubMenuProps) {
41
+ const inputRef = useRef<HTMLInputElement>(null);
42
+ const dropdownRef = useRef<HTMLDivElement>(null);
43
+
44
+ const setInputNode = useCallback((node: HTMLInputElement | null) => {
45
+ inputRef.current = node;
46
+
47
+ if (!node) {
48
+ return;
49
+ }
50
+
51
+ requestAnimationFrame(() => {
52
+ requestAnimationFrame(() => {
53
+ node.focus();
54
+ });
55
+ });
56
+ }, []);
57
+
58
+ const focusFirstMenuItem = () => {
59
+ if (!dropdownRef.current) return;
60
+ const menuItems = Array.from(
61
+ dropdownRef.current.querySelectorAll<HTMLButtonElement>(
62
+ '[role="menuitem"]',
63
+ ),
64
+ );
65
+ if (menuItems.length > 0) {
66
+ menuItems[0].focus();
67
+ }
68
+ };
69
+
70
+ return (
71
+ <Menu.Sub>
72
+ <Menu.Sub.Target>
73
+ <Menu.Sub.Item>{label}</Menu.Sub.Item>
74
+ </Menu.Sub.Target>
75
+ <Menu.Sub.Dropdown w={width}>
76
+ <Box
77
+ ref={dropdownRef}
78
+ onClick={e => e.stopPropagation()}
79
+ onMouseDown={e => e.stopPropagation()}
80
+ >
81
+ {showSearch && (
82
+ <>
83
+ <Box
84
+ p={0}
85
+ onClick={e => e.stopPropagation()}
86
+ onMouseDown={e => e.stopPropagation()}
87
+ >
88
+ <SearchTextInput
89
+ ref={setInputNode}
90
+ placeholder={placeholder}
91
+ value={search}
92
+ onChange={e => onSearchChange(e.currentTarget.value)}
93
+ debounceMs={debounceMs}
94
+ size="sm"
95
+ onClick={e => {
96
+ e.stopPropagation();
97
+ }}
98
+ onMouseDown={e => {
99
+ e.stopPropagation();
100
+ }}
101
+ onKeyDown={e => {
102
+ if (e.key === 'ArrowDown') {
103
+ e.preventDefault();
104
+ e.stopPropagation();
105
+ inputRef.current?.blur();
106
+ requestAnimationFrame(() => {
107
+ focusFirstMenuItem();
108
+ });
109
+ return;
110
+ }
111
+
112
+ if (e.key === 'Escape') {
113
+ return;
114
+ }
115
+
116
+ e.stopPropagation();
117
+ }}
118
+ onKeyUp={e => {
119
+ if (e.key !== 'Escape') {
120
+ e.stopPropagation();
121
+ }
122
+ }}
123
+ onFocus={e => {
124
+ e.stopPropagation();
125
+ }}
126
+ classNames={{ input: classes.menuSearchInput }}
127
+ />
128
+ </Box>
129
+ <Menu.Divider />
130
+ </>
131
+ )}
132
+
133
+ {children}
134
+ {renderFooter?.({ search })}
135
+ </Box>
136
+ </Menu.Sub.Dropdown>
137
+ </Menu.Sub>
138
+ );
139
+ }
@@ -0,0 +1,2 @@
1
+ export { SearchableSubMenu } from './SearchableSubMenu';
2
+ export type { SearchableSubMenuProps } from './SearchableSubMenu';
@@ -0,0 +1,114 @@
1
+ # Select Component
2
+
3
+ A custom select/dropdown component matching Figma design specifications. Supports displaying a label prefix in muted color before the selected value.
4
+
5
+ ## Design Tokens
6
+
7
+ The Select component uses the following Figma design tokens:
8
+
9
+ - **Padding**: `5.5px` vertical, `var(--semantic-xs)` (8px) horizontal
10
+ - **Border Radius**: `var(--semantic-rounded-lg)` (8px)
11
+ - **Border**: `1px solid var(--color-unofficial-border-3)`
12
+ - **Background**: `var(--color-general-input)`
13
+ - **Shadow**: `var(--mantine-shadow-xs)`
14
+ - **Typography**: Paragraph Small (14px, 21px line-height, 0.07px letter-spacing)
15
+ - **Colors**:
16
+ - Label prefix: `var(--color-neutral-500)` (#737373)
17
+ - Selected value: `var(--color-general-foreground)` (#020617)
18
+ - Chevron icon: `var(--color-neutral-500)`
19
+
20
+ ## Usage
21
+
22
+ ### With Label Prefix (Recommended)
23
+
24
+ The label prefix appears in gray color before the selected value, matching the Figma design:
25
+
26
+ ```tsx
27
+ import { Select } from '@acme/ui';
28
+
29
+ function DepartmentFilter() {
30
+ const [department, setDepartment] = useState('All');
31
+
32
+ return (
33
+ <Select
34
+ labelPrefix="Department"
35
+ data={[
36
+ { value: 'All', label: 'All' },
37
+ { value: 'Sales', label: 'Sales' },
38
+ { value: 'Marketing', label: 'Marketing' },
39
+ ]}
40
+ value={department}
41
+ onChange={setDepartment}
42
+ />
43
+ );
44
+ }
45
+ ```
46
+
47
+ ### Without Label Prefix
48
+
49
+ Standard dropdown without the label prefix:
50
+
51
+ ```tsx
52
+ <Select
53
+ data={[
54
+ { value: 'all', label: 'All Departments' },
55
+ { value: 'sales', label: 'Sales' },
56
+ ]}
57
+ value={value}
58
+ onChange={setValue}
59
+ placeholder="Select a department"
60
+ />
61
+ ```
62
+
63
+ ## Props
64
+
65
+ | Prop | Type | Required | Description |
66
+ |------|------|----------|-------------|
67
+ | `labelPrefix` | `string` | No | Label text to display in gray before the selected value (e.g., "Department") |
68
+ | `data` | `Array<{ value: string; label: string }>` | Yes | Array of options with value and label |
69
+ | `value` | `string \| null` | No | Currently selected value |
70
+ | `onChange` | `(value: string \| null) => void` | No | Callback when selection changes |
71
+ | `placeholder` | `string` | No | Placeholder text when no value is selected |
72
+
73
+ ## Figma Reference
74
+
75
+ This component is based on the Figma design:
76
+ - **Component**: Select & Combobox
77
+ - **Node ID**: 108:16040
78
+ - **Design File**: AMM
79
+
80
+ ## Design Specifications
81
+
82
+ ### Layout
83
+ - Height: 36px
84
+ - Padding: 5.5px (top/bottom), 8px (left/right)
85
+ - Gap between label prefix and value: 6px
86
+
87
+ ### Typography
88
+ - Font family: Inter (from design system)
89
+ - Font size: 14px (Paragraph Small)
90
+ - Line height: 21px
91
+ - Letter spacing: 0.07px
92
+ - Font weight: Normal (400)
93
+
94
+ ### Colors
95
+ - Label prefix: Neutral/500 (#737373)
96
+ - Selected value: General/Foreground (#020617)
97
+ - Border: Unofficial/Border-3 (#cbd5e1)
98
+ - Background: General/Input (white)
99
+ - Chevron: Neutral/500 (#737373)
100
+
101
+ ### Effects
102
+ - Shadow: xs (0px 1px 2px 0px rgba(0,0,0,0.05))
103
+ - Border radius: lg (8px)
104
+
105
+ ## Implementation Notes
106
+
107
+ - Built on Mantine's Combobox primitive for flexible rendering
108
+ - Uses `InputBase` as the trigger for custom content display
109
+ - Supports keyboard navigation and accessibility
110
+ - Chevron icon from `lucide-react`
111
+
112
+ ## Examples
113
+
114
+ See `Select.story.tsx` for interactive examples in Storybook.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Select component styles – vanilla-extract with semantic design tokens
3
+ */
4
+
5
+ import { style } from '@vanilla-extract/css';
6
+
7
+ import { mantineVars } from '../../theme/mantineVars';
8
+ import { tokens } from '../../theme/themeContract.css';
9
+
10
+ const focusRing = {
11
+ outline: `2px solid ${tokens.color.stroke.focusStrong}`,
12
+ outlineOffset: 2,
13
+ };
14
+
15
+ export const selectButton = style({
16
+ all: 'unset',
17
+ boxSizing: 'border-box',
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ justifyContent: 'space-between',
21
+ width: '100%',
22
+ minHeight: 36,
23
+ height: 36,
24
+ padding: '5.5px 8px',
25
+ gap: 6,
26
+ borderRadius: 8,
27
+ border: `1px solid ${tokens.color.stroke.default}`,
28
+ background: tokens.color.background.input,
29
+ boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)',
30
+ fontFamily: 'inherit',
31
+ fontSize: 14,
32
+ lineHeight: '21px',
33
+ fontWeight: 400,
34
+ letterSpacing: '0.07px',
35
+ cursor: 'pointer',
36
+ overflow: 'hidden',
37
+ transition: 'border-color 0.15s ease',
38
+ selectors: {
39
+ '&:hover': {
40
+ borderColor: tokens.color.stroke.subduedStrong,
41
+ },
42
+ '&:focus': focusRing,
43
+ '&:active': {
44
+ borderColor: tokens.color.stroke.strong,
45
+ },
46
+ [`${mantineVars.darkSelector} &`]: {
47
+ borderColor: tokens.color.stroke.default,
48
+ backgroundColor: tokens.color.background.default,
49
+ },
50
+ },
51
+ });
52
+
53
+ export const valueContainer = style({
54
+ display: 'flex',
55
+ gap: 6,
56
+ alignItems: 'center',
57
+ flex: 1,
58
+ minWidth: 0,
59
+ overflow: 'hidden',
60
+ });
61
+
62
+ export const labelPrefix = style({
63
+ color: tokens.color.text.subduedDefault,
64
+ fontSize: 14,
65
+ lineHeight: '21px',
66
+ fontWeight: 400,
67
+ letterSpacing: '0.07px',
68
+ whiteSpace: 'nowrap',
69
+ flexShrink: 0,
70
+ selectors: {
71
+ [`${mantineVars.darkSelector} &`]: {
72
+ color: tokens.color.text.default,
73
+ },
74
+ },
75
+ });
76
+
77
+ export const selectedValue = style({
78
+ color: tokens.color.text.default,
79
+ fontSize: 14,
80
+ lineHeight: '21px',
81
+ fontWeight: 400,
82
+ letterSpacing: '0.07px',
83
+ whiteSpace: 'nowrap',
84
+ flexShrink: 0,
85
+ selectors: {
86
+ [`${mantineVars.darkSelector} &`]: {
87
+ color: tokens.color.text.default,
88
+ },
89
+ },
90
+ });
91
+
92
+ export const chevron = style({
93
+ color: tokens.color.text.subduedDefault,
94
+ flexShrink: 0,
95
+ width: 16,
96
+ height: 16,
97
+ selectors: {
98
+ [`${mantineVars.darkSelector} &`]: {
99
+ color: tokens.color.text.default,
100
+ },
101
+ },
102
+ });
103
+
104
+ export const errorText = style({
105
+ color: tokens.color.text.dangerDefault,
106
+ fontSize: 12,
107
+ lineHeight: '18px',
108
+ fontWeight: 400,
109
+ marginTop: 4,
110
+ });