@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,325 @@
1
+ import {
2
+ forwardRef,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type KeyboardEvent,
8
+ } from 'react';
9
+
10
+ import { ChevronDown, Search } from 'lucide-react';
11
+
12
+ import { Combobox, useCombobox } from '@mantine/core';
13
+
14
+ import { useDebouncedValue } from '@scalepad/ui-utils/hooks';
15
+
16
+ import { Avatar, Loader } from '../DesignSystemPrimitives';
17
+ import { Text } from '../Typography';
18
+ import * as styles from './LookupSelect.css';
19
+
20
+ import type { BackgroundColor } from '../../tokens/color-types';
21
+ import type { SearchableListSearchMode } from '../SearchableList/types';
22
+
23
+ /**
24
+ * Semantic background tokens usable as avatar fill colors. Cycled through to give each
25
+ * option a stable, differentiated accent derived from its label.
26
+ */
27
+ const AVATAR_PALETTE: readonly BackgroundColor[] = [
28
+ 'background.primary.filled',
29
+ 'background.information.filled',
30
+ 'background.success.filled',
31
+ 'background.warning.filled',
32
+ 'background.danger.filled',
33
+ 'background.subdued.filled',
34
+ ];
35
+
36
+ function hashString(value: string): number {
37
+ let hash = 0;
38
+ for (let index = 0; index < value.length; index += 1) {
39
+ hash = (hash * 31 + value.charCodeAt(index)) | 0;
40
+ }
41
+ return Math.abs(hash);
42
+ }
43
+
44
+ function getAvatarColor(label: string): BackgroundColor {
45
+ return AVATAR_PALETTE[hashString(label) % AVATAR_PALETTE.length]!;
46
+ }
47
+
48
+ function getInitial(label: string): string {
49
+ const trimmed = label.trim();
50
+ return trimmed.length > 0 ? trimmed[0]!.toUpperCase() : '?';
51
+ }
52
+
53
+ export interface LookupSelectOption {
54
+ value: string;
55
+ label: string;
56
+ description?: string;
57
+ }
58
+
59
+ export interface LookupSelectProps {
60
+ /** Options to display. When `searchMode` is `server`, this is expected to be pre-filtered. */
61
+ data: LookupSelectOption[];
62
+ /** Current selected value. */
63
+ value?: string | null;
64
+ /**
65
+ * Callback fired when the selected value changes. The second argument is the full option
66
+ * that was selected, which lets consumers persist fields (like `label`) that may not be
67
+ * present in `data` after a subsequent server-side search.
68
+ */
69
+ onChange?: (value: string | null, option: LookupSelectOption | null) => void;
70
+ /** Trigger placeholder shown when no option is selected. */
71
+ placeholder?: string;
72
+ /** Placeholder shown in the dropdown search input. */
73
+ searchPlaceholder?: string;
74
+ /** Disables the trigger and prevents opening the dropdown. */
75
+ disabled?: boolean;
76
+ /**
77
+ * `client` filters `data` locally by the search text. `server` treats `data` as already-filtered
78
+ * and invokes `onSearchChange` with a debounced query whenever the user types.
79
+ *
80
+ * @default 'client'
81
+ */
82
+ searchMode?: SearchableListSearchMode;
83
+ /** Called with the trimmed, debounced search query. Primarily for `searchMode="server"`. */
84
+ onSearchChange?: (query: string) => void;
85
+ /**
86
+ * Debounce delay (ms) applied to `onSearchChange`.
87
+ *
88
+ * @default 250
89
+ */
90
+ searchDebounceMs?: number;
91
+ /** Shows a spinner next to the search input when true. */
92
+ isLoading?: boolean;
93
+ /** Message shown when there are no options to display. */
94
+ nothingFoundMessage?: string;
95
+ /** Persistent message rendered at the bottom of the dropdown (e.g. validation hint). */
96
+ footerMessage?: string;
97
+ /** Width of the trigger. */
98
+ w?: string | number;
99
+ }
100
+
101
+ /**
102
+ * Searchable lookup picker that displays a colored avatar for each option and renders a
103
+ * search-driven dropdown panel separate from the trigger.
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * <LookupSelect
108
+ * data={clients.map((c) => ({ value: c.id, label: c.name }))}
109
+ * value={clientId}
110
+ * onChange={setClientId}
111
+ * searchMode="server"
112
+ * onSearchChange={setSearch}
113
+ * isLoading={query.isFetching}
114
+ * footerMessage="Client is required to save an initiative"
115
+ * />
116
+ * ```
117
+ */
118
+ export const LookupSelect = forwardRef<HTMLButtonElement, LookupSelectProps>(
119
+ (
120
+ {
121
+ data,
122
+ value,
123
+ onChange,
124
+ placeholder = 'Select',
125
+ searchPlaceholder = 'Search',
126
+ disabled = false,
127
+ searchMode = 'client',
128
+ onSearchChange,
129
+ searchDebounceMs = 250,
130
+ isLoading = false,
131
+ nothingFoundMessage,
132
+ footerMessage,
133
+ w,
134
+ },
135
+ ref,
136
+ ) => {
137
+ const [search, setSearch] = useState('');
138
+ const searchInputRef = useRef<HTMLInputElement | null>(null);
139
+
140
+ const combobox = useCombobox({
141
+ onDropdownClose: () => {
142
+ combobox.resetSelectedOption();
143
+ setSearch('');
144
+ },
145
+ onDropdownOpen: () => {
146
+ combobox.updateSelectedOptionIndex('active');
147
+ window.setTimeout(() => searchInputRef.current?.focus(), 0);
148
+ },
149
+ });
150
+
151
+ // Cache the last option whose value matched so the trigger keeps showing the selection
152
+ // even when the consumer replaces `data` (e.g. server-side search results that filter
153
+ // the selected option out of the visible list).
154
+ const selectedOptionRef = useRef<LookupSelectOption | null>(null);
155
+ const matchedOption = data.find(option => option.value === value) ?? null;
156
+ if (matchedOption) {
157
+ selectedOptionRef.current = matchedOption;
158
+ } else if (value == null) {
159
+ selectedOptionRef.current = null;
160
+ }
161
+ const selectedOption = matchedOption ?? selectedOptionRef.current;
162
+
163
+ const filteredData = useMemo(() => {
164
+ if (searchMode === 'server') return data;
165
+ const query = search.trim().toLowerCase();
166
+ if (!query) return data;
167
+ return data.filter(item => item.label.toLowerCase().includes(query));
168
+ }, [data, search, searchMode]);
169
+
170
+ const [debouncedSearch] = useDebouncedValue(search, searchDebounceMs);
171
+ useEffect(() => {
172
+ if (searchMode !== 'server') return;
173
+ onSearchChange?.(debouncedSearch.trim());
174
+ }, [debouncedSearch, onSearchChange, searchMode]);
175
+
176
+ const handleOptionSubmit = (submittedValue: string) => {
177
+ const submittedOption =
178
+ data.find(option => option.value === submittedValue) ?? null;
179
+ onChange?.(submittedValue, submittedOption);
180
+ combobox.closeDropdown();
181
+ };
182
+
183
+ const handleSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
184
+ if (event.nativeEvent.isComposing) {
185
+ return;
186
+ }
187
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
188
+ event.preventDefault();
189
+ if (event.key === 'ArrowDown') {
190
+ combobox.selectNextOption();
191
+ } else {
192
+ combobox.selectPreviousOption();
193
+ }
194
+ } else if (event.key === 'Enter') {
195
+ event.preventDefault();
196
+ combobox.clickSelectedOption();
197
+ } else if (event.key === 'Escape') {
198
+ event.preventDefault();
199
+ combobox.closeDropdown();
200
+ }
201
+ };
202
+
203
+ const triggerStyle = w
204
+ ? { width: typeof w === 'number' ? `${w}px` : w }
205
+ : undefined;
206
+
207
+ return (
208
+ <Combobox store={combobox} onOptionSubmit={handleOptionSubmit}>
209
+ <Combobox.Target>
210
+ <button
211
+ ref={ref}
212
+ type="button"
213
+ className={styles.trigger}
214
+ onClick={() => {
215
+ if (!disabled) combobox.toggleDropdown();
216
+ }}
217
+ disabled={disabled}
218
+ aria-haspopup="listbox"
219
+ aria-expanded={combobox.dropdownOpened}
220
+ style={triggerStyle}
221
+ >
222
+ <span className={styles.triggerValue}>
223
+ {selectedOption ? (
224
+ <>
225
+ <Avatar
226
+ size="sm"
227
+ radius="sm"
228
+ color={getAvatarColor(selectedOption.label)}
229
+ >
230
+ {getInitial(selectedOption.label)}
231
+ </Avatar>
232
+ <Text
233
+ variant="body1.strong"
234
+ c="text.default"
235
+ className={styles.triggerLabel}
236
+ >
237
+ {selectedOption.label}
238
+ </Text>
239
+ </>
240
+ ) : (
241
+ <Text
242
+ variant="body1"
243
+ c="text.subdued.default"
244
+ className={styles.triggerLabel}
245
+ >
246
+ {placeholder}
247
+ </Text>
248
+ )}
249
+ </span>
250
+ <ChevronDown className={styles.chevron} aria-hidden />
251
+ </button>
252
+ </Combobox.Target>
253
+
254
+ <Combobox.Dropdown p={0}>
255
+ <div className={styles.searchRow}>
256
+ <Search className={styles.searchIcon} aria-hidden />
257
+ <input
258
+ ref={searchInputRef}
259
+ type="text"
260
+ className={styles.searchInput}
261
+ value={search}
262
+ onChange={event => setSearch(event.currentTarget.value)}
263
+ onKeyDown={handleSearchKeyDown}
264
+ placeholder={searchPlaceholder}
265
+ aria-label={searchPlaceholder}
266
+ autoComplete="off"
267
+ />
268
+ {isLoading && <Loader size="xs" />}
269
+ </div>
270
+
271
+ <Combobox.Options className={styles.options}>
272
+ {filteredData.map(item => (
273
+ <Combobox.Option
274
+ value={item.value}
275
+ key={item.value}
276
+ className={styles.optionRoot}
277
+ >
278
+ <div className={styles.option}>
279
+ <Avatar
280
+ size="sm"
281
+ radius="sm"
282
+ color={getAvatarColor(item.label)}
283
+ >
284
+ {getInitial(item.label)}
285
+ </Avatar>
286
+ <div className={styles.optionText}>
287
+ <Text
288
+ variant="body1.strong"
289
+ c="text.default"
290
+ className={styles.optionLabel}
291
+ >
292
+ {item.label}
293
+ </Text>
294
+ {item.description && (
295
+ <Text
296
+ variant="caption2"
297
+ c="text.subdued.default"
298
+ className={styles.optionLabel}
299
+ >
300
+ {item.description}
301
+ </Text>
302
+ )}
303
+ </div>
304
+ </div>
305
+ </Combobox.Option>
306
+ ))}
307
+ {filteredData.length === 0 && !isLoading && nothingFoundMessage && (
308
+ <Combobox.Empty>{nothingFoundMessage}</Combobox.Empty>
309
+ )}
310
+ </Combobox.Options>
311
+
312
+ {footerMessage && (
313
+ <div className={styles.footer}>
314
+ <Text variant="caption2" c="text.subdued.default">
315
+ {footerMessage}
316
+ </Text>
317
+ </div>
318
+ )}
319
+ </Combobox.Dropdown>
320
+ </Combobox>
321
+ );
322
+ },
323
+ );
324
+
325
+ LookupSelect.displayName = 'LookupSelect';
@@ -0,0 +1,2 @@
1
+ export { LookupSelect } from './LookupSelect';
2
+ export type { LookupSelectOption, LookupSelectProps } from './LookupSelect';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Menu component styles – vanilla-extract with semantic design tokens.
3
+ * Matches Figma design system: Menu, Menu.Item, Menu.Label, submenu triggers (itemSection), divider.
4
+ */
5
+
6
+ import { style } from '@vanilla-extract/css';
7
+
8
+ import { mantineVars } from '../../theme/mantineVars';
9
+ import { tokens } from '../../theme/themeContract.css';
10
+
11
+ /** Dropdown container (root and Menu.Sub.Dropdown) */
12
+ export const dropdown = style({
13
+ backgroundColor: tokens.color.background.default,
14
+ border: `1px solid ${tokens.color.stroke.default}`,
15
+ borderRadius: tokens.radius.lg,
16
+ boxShadow: tokens.shadow.md,
17
+ padding: tokens.spacing['2xs'],
18
+ gap: 0,
19
+ display: 'flex',
20
+ flexDirection: 'column',
21
+ });
22
+
23
+ /** Menu item and submenu trigger (Menu.Item, Menu.Sub.Item) */
24
+ export const item = style({
25
+ borderRadius: tokens.radius.md,
26
+ minHeight: 32,
27
+ padding: `${tokens.spacing.xs} ${tokens.spacing.xs}`,
28
+ fontSize: 14,
29
+ lineHeight: 1,
30
+ color: tokens.color.text.default,
31
+ transition: 'background-color 150ms ease',
32
+ selectors: {
33
+ '&:hover:not([data-disabled])': {
34
+ backgroundColor: tokens.color.background.primaryLight,
35
+ },
36
+ '&[data-disabled]': {
37
+ color: tokens.color.text.disabledDefault,
38
+ cursor: 'not-allowed',
39
+ },
40
+ [`${mantineVars.darkSelector} &`]: {
41
+ color: tokens.color.text.default,
42
+ },
43
+ [`${mantineVars.darkSelector} &:hover:not([data-disabled])`]: {
44
+ backgroundColor: tokens.color.background.primaryLight,
45
+ },
46
+ },
47
+ });
48
+
49
+ /**
50
+ * Applied when Menu.Item receives a theme-token color prop.
51
+ * Reads --lm-menu-item-color (set inline by MenuItemWrapper) so design-system
52
+ * item colors do not conflict with Mantine's internal menu variables.
53
+ */
54
+ export const itemWithColor = style({
55
+ color: 'var(--lm-menu-item-color)',
56
+ });
57
+
58
+ /** Group label (Menu.Label, optgroup-style) – Caption 2 */
59
+ export const label = style({
60
+ fontSize: 12,
61
+ lineHeight: 1,
62
+ color: tokens.color.text.subduedDefault,
63
+ padding: `5.5px ${tokens.spacing.xs}`,
64
+ minHeight: 32,
65
+ fontWeight: 400,
66
+ });
67
+
68
+ /** Visible text label inside Menu.Item. Inherit row color so semantic item variants color the text. */
69
+ export const itemLabel = style({
70
+ color: 'inherit',
71
+ });
72
+
73
+ /** Left/right sections of Menu.Item (e.g. submenu chevron). Use inherit so item token color flows through. */
74
+ export const itemSection = style({
75
+ color: 'inherit',
76
+ flexShrink: 0,
77
+ width: 20,
78
+ height: 20,
79
+ display: 'flex',
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ });
83
+
84
+ /** Divider between groups */
85
+ export const divider = style({
86
+ borderTop: `1px solid ${tokens.color.stroke.subduedDefault}`,
87
+ marginTop: 0,
88
+ marginBottom: 0,
89
+ });
@@ -0,0 +1,105 @@
1
+ import {
2
+ forwardRef,
3
+ type ComponentPropsWithoutRef,
4
+ type CSSProperties,
5
+ } from 'react';
6
+
7
+ import { clsx } from 'clsx';
8
+
9
+ import {
10
+ Menu as MantineMenu,
11
+ type MenuItemProps as MantineMenuItemProps,
12
+ } from '@mantine/core';
13
+
14
+ import * as classes from './Menu.css';
15
+ import { resolveColorToken } from '../../utils/color-props';
16
+ import { withStaticComponents } from '../../utils/withStaticComponents';
17
+
18
+ type MantineMenuProps = ComponentPropsWithoutRef<typeof MantineMenu>;
19
+ type BaseMenuItemProps = ComponentPropsWithoutRef<'button'> &
20
+ Omit<MantineMenuItemProps, 'color' | 'c' | 'onClick'>;
21
+
22
+ export type MenuItemVariant = 'destructive';
23
+
24
+ export interface MenuItemProps extends BaseMenuItemProps {
25
+ /** Semantic menu row styling. */
26
+ variant?: MenuItemVariant;
27
+ }
28
+
29
+ function resolveMenuItemColor(variant?: MenuItemVariant) {
30
+ if (variant === 'destructive') {
31
+ return resolveColorToken('text.danger.default');
32
+ }
33
+
34
+ return undefined;
35
+ }
36
+
37
+ const MenuItemWrapper = forwardRef<HTMLButtonElement, MenuItemProps>(
38
+ ({ variant, style: styleProp, className, ...rest }, ref) => {
39
+ const resolvedColor = resolveMenuItemColor(variant);
40
+ const baseStyle: CSSProperties =
41
+ styleProp != null &&
42
+ typeof styleProp === 'object' &&
43
+ !Array.isArray(styleProp)
44
+ ? { ...styleProp }
45
+ : {};
46
+
47
+ const resolvedStyle: CSSProperties =
48
+ resolvedColor != null
49
+ ? { ...baseStyle, ['--lm-menu-item-color' as string]: resolvedColor }
50
+ : baseStyle;
51
+
52
+ const resolvedClassName =
53
+ resolvedColor != null
54
+ ? clsx(classes.itemWithColor, className)
55
+ : className;
56
+
57
+ return (
58
+ <MantineMenu.Item
59
+ ref={ref}
60
+ style={resolvedStyle}
61
+ className={resolvedClassName}
62
+ data-variant={variant}
63
+ {...rest}
64
+ />
65
+ );
66
+ },
67
+ );
68
+ MenuItemWrapper.displayName = 'MenuItem';
69
+
70
+ /**
71
+ * Design-system Menu: Mantine Menu with vanilla-extract styles from the theme contract.
72
+ * Preserves full API: Menu.Target, Menu.Dropdown, Menu.Item, Menu.Label, Menu.Divider, Menu.Sub, etc.
73
+ * Menu.Item exposes only safe semantic variants instead of raw color props.
74
+ */
75
+ function MenuWrapper(props: MantineMenuProps) {
76
+ return (
77
+ <MantineMenu
78
+ classNames={{
79
+ dropdown: classes.dropdown,
80
+ item: classes.item,
81
+ label: classes.label,
82
+ itemLabel: classes.itemLabel,
83
+ itemSection: classes.itemSection,
84
+ divider: classes.divider,
85
+ }}
86
+ {...props}
87
+ />
88
+ );
89
+ }
90
+
91
+ type MenuComponent = typeof MenuWrapper &
92
+ Omit<typeof MantineMenu, 'Item'> & {
93
+ Item: typeof MenuItemWrapper;
94
+ };
95
+
96
+ const menuStaticComponents: Omit<typeof MantineMenu, 'Item'> & {
97
+ Item: typeof MenuItemWrapper;
98
+ } = { ...MantineMenu, Item: MenuItemWrapper };
99
+
100
+ export const Menu: MenuComponent = withStaticComponents(
101
+ MenuWrapper,
102
+ menuStaticComponents,
103
+ );
104
+
105
+ export type { MenuProps } from '@mantine/core';
@@ -0,0 +1,2 @@
1
+ export { Menu } from './Menu';
2
+ export type { MenuItemProps, MenuItemVariant, MenuProps } from './Menu';
@@ -0,0 +1,168 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { AlertCircle, AlertTriangle, CheckCircle, Info } from 'lucide-react';
4
+
5
+ import { Flex, Stack } from '@mantine/core';
6
+
7
+ import {
8
+ resolveColorToken,
9
+ type BackgroundColor,
10
+ type IconColor,
11
+ type TextColor,
12
+ } from '../../utils/color-props';
13
+ import { Button } from '../Button';
14
+ import { Text } from '../Typography';
15
+
16
+ export interface MessageBoxProps {
17
+ /**
18
+ * Visual variant of the message box
19
+ * @default 'default'
20
+ */
21
+ variant?: 'default' | 'danger' | 'success' | 'warning' | 'info';
22
+ /**
23
+ * Action button label (if provided, renders a button)
24
+ */
25
+ actionButton?: string;
26
+ /**
27
+ * Action button variant
28
+ */
29
+ actionButtonVariant?:
30
+ | 'primary'
31
+ | 'secondary'
32
+ | 'outline'
33
+ | 'ghost'
34
+ | 'ghost-muted'
35
+ | 'destructive';
36
+ /**
37
+ * Whether the action button is in loading state
38
+ */
39
+ actionLoading?: boolean;
40
+ /**
41
+ * Whether to show the icon
42
+ * @default true
43
+ */
44
+ showIcon?: boolean;
45
+ /**
46
+ * Main content of the message box
47
+ */
48
+ children: React.ReactNode;
49
+ /**
50
+ * Callback when action button is clicked
51
+ */
52
+ onActionClick?: () => void;
53
+ }
54
+
55
+ interface VariantStyles {
56
+ backgroundColor: BackgroundColor;
57
+ iconColor: IconColor;
58
+ textColor: TextColor;
59
+ }
60
+
61
+ function resolveCssColor(token: BackgroundColor | IconColor | TextColor) {
62
+ return resolveColorToken(token) as string | undefined;
63
+ }
64
+
65
+ /**
66
+ * MessageBox component
67
+ * Displays a message with optional icon and action button, with color-coded variants
68
+ */
69
+ export function MessageBox({
70
+ variant = 'default',
71
+ actionButton,
72
+ actionButtonVariant,
73
+ actionLoading = false,
74
+ showIcon = true,
75
+ children,
76
+ onActionClick,
77
+ }: MessageBoxProps) {
78
+ // Get variant-specific styles
79
+ const styles = useMemo((): VariantStyles => {
80
+ switch (variant) {
81
+ case 'danger':
82
+ return {
83
+ backgroundColor: 'background.danger.light',
84
+ iconColor: 'icon.danger.default',
85
+ textColor: 'text.danger.default',
86
+ };
87
+ case 'success':
88
+ return {
89
+ backgroundColor: 'background.success.light',
90
+ iconColor: 'icon.success.default',
91
+ textColor: 'text.success.default',
92
+ };
93
+ case 'warning':
94
+ return {
95
+ backgroundColor: 'background.warning.light',
96
+ iconColor: 'icon.warning.default',
97
+ textColor: 'text.warning.default',
98
+ };
99
+ case 'info':
100
+ return {
101
+ backgroundColor: 'background.information.light',
102
+ iconColor: 'icon.information.default',
103
+ textColor: 'text.information.default',
104
+ };
105
+ case 'default':
106
+ default:
107
+ return {
108
+ backgroundColor: 'background.subdued.light',
109
+ iconColor: 'icon.default',
110
+ textColor: 'text.subdued.default',
111
+ };
112
+ }
113
+ }, [variant]);
114
+
115
+ // Get icon for variant
116
+ const icon = useMemo(() => {
117
+ if (!showIcon) return null;
118
+
119
+ const iconSize = 16;
120
+ const iconColor = resolveCssColor(styles.iconColor);
121
+
122
+ switch (variant) {
123
+ case 'danger':
124
+ return <AlertCircle size={iconSize} color={iconColor} />;
125
+ case 'success':
126
+ return <CheckCircle size={iconSize} color={iconColor} />;
127
+ case 'warning':
128
+ return <AlertTriangle size={iconSize} color={iconColor} />;
129
+ case 'info':
130
+ return <Info size={iconSize} color={iconColor} />;
131
+ case 'default':
132
+ default:
133
+ return <Info size={iconSize} color={iconColor} />;
134
+ }
135
+ }, [variant, showIcon, styles.iconColor]);
136
+
137
+ return (
138
+ <Flex
139
+ direction="column"
140
+ gap="md"
141
+ p="md"
142
+ style={{
143
+ backgroundColor: resolveCssColor(styles.backgroundColor),
144
+ borderRadius: 'var(--radius-md)',
145
+ }}
146
+ >
147
+ <Flex direction="row" gap="xs" align="flex-start">
148
+ {icon && <Flex flex="none">{icon}</Flex>}
149
+ <Stack justify="start">
150
+ <Text variant="caption1.stronger" c={styles.textColor}>
151
+ {children}
152
+ </Text>
153
+ {actionButton && (
154
+ <Button
155
+ variant={actionButtonVariant}
156
+ size="sm"
157
+ w="fit-content"
158
+ onClick={onActionClick}
159
+ loading={actionLoading}
160
+ >
161
+ {actionButton}
162
+ </Button>
163
+ )}
164
+ </Stack>
165
+ </Flex>
166
+ </Flex>
167
+ );
168
+ }
@@ -0,0 +1,2 @@
1
+ export { MessageBox } from './MessageBox';
2
+ export type { MessageBoxProps } from './MessageBox';