@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,55 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { Flex } from '@mantine/core';
4
+
5
+ import { Text } from '../Typography/Text';
6
+ import { Title } from '../Typography/Title';
7
+
8
+ export interface MetricDisplayProps {
9
+ /**
10
+ * Label text to display above the metric value
11
+ */
12
+ label: string;
13
+ /**
14
+ * Metric value to display (can be string or React node)
15
+ */
16
+ value: ReactNode;
17
+ /**
18
+ * Size variant for the metric display
19
+ * - 'sm': heading4 variant with order 5 (smaller)
20
+ * - 'md': heading4 variant (default)
21
+ * - 'lg': heading3 variant (larger)
22
+ */
23
+ size?: 'sm' | 'md' | 'lg';
24
+ }
25
+
26
+ /**
27
+ * MetricDisplay component
28
+ * Displays a metric with a label and value in a consistent format
29
+ * Used for KPIs, statistics, and other key metrics throughout the app
30
+ */
31
+ export function MetricDisplay({
32
+ label,
33
+ value,
34
+ size = 'md',
35
+ }: MetricDisplayProps) {
36
+ const titleVariantMap = {
37
+ sm: 'heading4',
38
+ md: 'heading4',
39
+ lg: 'heading3',
40
+ } as const;
41
+
42
+ return (
43
+ <Flex direction="column" gap="xs">
44
+ <Text variant="caption1.strong" c="text.subdued.default">
45
+ {label}
46
+ </Text>
47
+ <Title
48
+ variant={titleVariantMap[size]}
49
+ {...(size === 'sm' ? { order: 5 } : {})}
50
+ >
51
+ {value}
52
+ </Title>
53
+ </Flex>
54
+ );
55
+ }
@@ -0,0 +1 @@
1
+ export { MetricDisplay, type MetricDisplayProps } from './MetricDisplay';
@@ -0,0 +1,278 @@
1
+ import { forwardRef, useMemo } from 'react';
2
+
3
+ import {
4
+ CheckIcon,
5
+ Combobox,
6
+ Group,
7
+ Input,
8
+ MultiSelect as MantineMultiSelect,
9
+ Pill,
10
+ PillsInput,
11
+ type MultiSelectProps as MantineMultiSelectProps,
12
+ useCombobox,
13
+ } from '@mantine/core';
14
+ import { useUncontrolled } from '@mantine/hooks';
15
+
16
+ type ParsedOption = {
17
+ value: string;
18
+ label: string;
19
+ disabled?: boolean;
20
+ };
21
+
22
+ export interface MultiSelectProps extends MantineMultiSelectProps {
23
+ maxDisplayedItems?: number;
24
+ }
25
+
26
+ interface LimitedPillMultiSelectProps extends Omit<
27
+ MultiSelectProps,
28
+ 'maxDisplayedItems'
29
+ > {
30
+ maxDisplayedItems: number;
31
+ }
32
+
33
+ function parseOption(
34
+ item: string | { value: string; label?: string; disabled?: boolean },
35
+ ) {
36
+ if (typeof item === 'string') {
37
+ return { value: item, label: item };
38
+ }
39
+
40
+ return {
41
+ value: item.value,
42
+ label: item.label ?? item.value,
43
+ disabled: item.disabled,
44
+ };
45
+ }
46
+
47
+ function parseOptions(data: MantineMultiSelectProps['data']): ParsedOption[] {
48
+ if (!data) {
49
+ return [];
50
+ }
51
+
52
+ return data.flatMap(item => {
53
+ if (
54
+ typeof item === 'string' ||
55
+ ('value' in item && typeof item.value === 'string')
56
+ ) {
57
+ return [parseOption(item)];
58
+ }
59
+
60
+ if ('items' in item && Array.isArray(item.items)) {
61
+ return item.items.map(groupItem => parseOption(groupItem));
62
+ }
63
+
64
+ return [];
65
+ });
66
+ }
67
+
68
+ function getDisplayedValues(values: string[], maxDisplayedItems?: number) {
69
+ if (maxDisplayedItems == null) {
70
+ return values;
71
+ }
72
+
73
+ const normalizedMaxDisplayedItems = Math.max(1, maxDisplayedItems);
74
+
75
+ if (values.length <= normalizedMaxDisplayedItems) {
76
+ return values;
77
+ }
78
+
79
+ return values.slice(0, Math.max(0, normalizedMaxDisplayedItems - 1));
80
+ }
81
+
82
+ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
83
+ ({ maxDisplayedItems, ...props }, ref) => {
84
+ if (maxDisplayedItems == null) {
85
+ return <MantineMultiSelect ref={ref} {...props} />;
86
+ }
87
+
88
+ return (
89
+ <LimitedPillMultiSelect
90
+ ref={ref}
91
+ maxDisplayedItems={maxDisplayedItems}
92
+ {...props}
93
+ />
94
+ );
95
+ },
96
+ );
97
+
98
+ MultiSelect.displayName = 'MultiSelect';
99
+
100
+ const LimitedPillMultiSelect = forwardRef<
101
+ HTMLInputElement,
102
+ LimitedPillMultiSelectProps
103
+ >(
104
+ (
105
+ {
106
+ maxDisplayedItems,
107
+ data,
108
+ value,
109
+ defaultValue,
110
+ onChange,
111
+ placeholder,
112
+ comboboxProps,
113
+ hidePickedOptions,
114
+ nothingFoundMessage = 'Nothing found',
115
+ disabled,
116
+ readOnly,
117
+ label,
118
+ description,
119
+ error,
120
+ required,
121
+ withAsterisk,
122
+ name,
123
+ id,
124
+ 'aria-label': ariaLabel,
125
+ },
126
+ ref,
127
+ ) => {
128
+ const parsedOptions = useMemo(() => parseOptions(data), [data]);
129
+ const [selectedValues, setSelectedValues] = useUncontrolled({
130
+ value,
131
+ defaultValue,
132
+ finalValue: [],
133
+ onChange,
134
+ });
135
+
136
+ const combobox = useCombobox({
137
+ onDropdownClose: () => combobox.resetSelectedOption(),
138
+ onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'),
139
+ });
140
+
141
+ const selectedLabelsByValue = useMemo(
142
+ () => new Map(parsedOptions.map(option => [option.value, option.label])),
143
+ [parsedOptions],
144
+ );
145
+
146
+ const availableOptions = hidePickedOptions
147
+ ? parsedOptions.filter(option => !selectedValues.includes(option.value))
148
+ : parsedOptions;
149
+
150
+ const displayedValues = getDisplayedValues(
151
+ selectedValues,
152
+ maxDisplayedItems,
153
+ );
154
+ const hiddenValuesCount = selectedValues.length - displayedValues.length;
155
+
156
+ function handleOptionSubmit(optionValue: string) {
157
+ if (disabled || readOnly) {
158
+ return;
159
+ }
160
+
161
+ setSelectedValues(
162
+ selectedValues.includes(optionValue)
163
+ ? selectedValues.filter(
164
+ selectedValue => selectedValue !== optionValue,
165
+ )
166
+ : [...selectedValues, optionValue],
167
+ );
168
+ }
169
+
170
+ const options = availableOptions.map(option => {
171
+ const isSelected = selectedValues.includes(option.value);
172
+
173
+ return (
174
+ <Combobox.Option
175
+ value={option.value}
176
+ key={option.value}
177
+ active={isSelected}
178
+ disabled={option.disabled}
179
+ >
180
+ <Group gap="sm">
181
+ {isSelected ? <CheckIcon size={12} /> : null}
182
+ <span>{option.label}</span>
183
+ </Group>
184
+ </Combobox.Option>
185
+ );
186
+ });
187
+
188
+ return (
189
+ <Input.Wrapper
190
+ label={label}
191
+ description={description}
192
+ error={error}
193
+ required={required}
194
+ withAsterisk={withAsterisk}
195
+ >
196
+ <Combobox
197
+ store={combobox}
198
+ onOptionSubmit={handleOptionSubmit}
199
+ {...comboboxProps}
200
+ >
201
+ <Combobox.DropdownTarget>
202
+ <PillsInput
203
+ pointer={!disabled && !readOnly}
204
+ disabled={disabled}
205
+ onClick={() => {
206
+ if (!disabled && !readOnly) {
207
+ combobox.toggleDropdown();
208
+ }
209
+ }}
210
+ >
211
+ <Pill.Group>
212
+ {selectedValues.length > 0 ? (
213
+ <>
214
+ {displayedValues.map(selectedValue => (
215
+ <Pill
216
+ key={selectedValue}
217
+ withRemoveButton={!disabled && !readOnly}
218
+ onRemove={() =>
219
+ setSelectedValues(
220
+ selectedValues.filter(
221
+ valueItem => valueItem !== selectedValue,
222
+ ),
223
+ )
224
+ }
225
+ >
226
+ {selectedLabelsByValue.get(selectedValue) ??
227
+ selectedValue}
228
+ </Pill>
229
+ ))}
230
+ {hiddenValuesCount > 0 ? (
231
+ <Pill>+ {hiddenValuesCount} More</Pill>
232
+ ) : null}
233
+ </>
234
+ ) : (
235
+ <Input.Placeholder>{placeholder}</Input.Placeholder>
236
+ )}
237
+
238
+ <Combobox.EventsTarget>
239
+ <PillsInput.Field
240
+ ref={ref}
241
+ type="hidden"
242
+ name={name}
243
+ id={id}
244
+ required={required}
245
+ aria-label={
246
+ ariaLabel ??
247
+ (typeof label === 'string' ? label : undefined)
248
+ }
249
+ onBlur={() => combobox.closeDropdown()}
250
+ onKeyDown={event => {
251
+ if (
252
+ event.key === 'Backspace' &&
253
+ selectedValues.length > 0
254
+ ) {
255
+ event.preventDefault();
256
+ setSelectedValues(selectedValues.slice(0, -1));
257
+ }
258
+ }}
259
+ />
260
+ </Combobox.EventsTarget>
261
+ </Pill.Group>
262
+ </PillsInput>
263
+ </Combobox.DropdownTarget>
264
+
265
+ <Combobox.Dropdown>
266
+ {options.length > 0 ? (
267
+ <Combobox.Options>{options}</Combobox.Options>
268
+ ) : (
269
+ <div>{nothingFoundMessage}</div>
270
+ )}
271
+ </Combobox.Dropdown>
272
+ </Combobox>
273
+ </Input.Wrapper>
274
+ );
275
+ },
276
+ );
277
+
278
+ LimitedPillMultiSelect.displayName = 'LimitedPillMultiSelect';
@@ -0,0 +1,2 @@
1
+ export { MultiSelect } from './MultiSelect';
2
+ export type { MultiSelectProps } from './MultiSelect';
@@ -0,0 +1,12 @@
1
+ import { Notifications as MantineNotifications } from '@mantine/notifications';
2
+
3
+ /**
4
+ * Notifications container for toast messages.
5
+ * Renders the Mantine notifications provider; must be mounted inside MantineProvider.
6
+ * Typically included inside ThemeProvider so toasts can display app-wide.
7
+ */
8
+ export function Notifications() {
9
+ return (
10
+ <MantineNotifications position="bottom-right" autoClose={4000} limit={5} />
11
+ );
12
+ }
@@ -0,0 +1,93 @@
1
+ # Notifications
2
+
3
+ Toast notifications via `showSuccessToast` / `showErrorToast` helpers, plus the `<Notifications />` provider that renders them.
4
+
5
+ The helpers wrap `@mantine/notifications` with design-system defaults: green / red color, auto-close timing, and a styled-caption message body. The raw `notifications` object from Mantine is re-exported for cases the helpers don't cover, but new code should reach for the helpers.
6
+
7
+ ## Quick Start
8
+
9
+ ```tsx
10
+ import { showSuccessToast, showErrorToast } from '@scalepad/ui';
11
+
12
+ // Plain success / error
13
+ showSuccessToast('User moved to department');
14
+ showErrorToast('Failed to update approval status.');
15
+
16
+ // With a custom title
17
+ showSuccessToast('Foo was removed.', { title: 'Initiative deleted' });
18
+
19
+ // With a retry action (auto-close disables so the user can click)
20
+ showErrorToast('Could not update completion status.', {
21
+ title: 'Could not update completion status',
22
+ action: { label: 'Retry', onClick: retry },
23
+ });
24
+
25
+ // Override auto-close
26
+ showSuccessToast('Saved.', { autoClose: 8000 });
27
+ showSuccessToast('Saved.', { autoClose: false }); // persist until dismissed
28
+ ```
29
+
30
+ ## API
31
+
32
+ ### `showSuccessToast(message, options?)` / `showErrorToast(message, options?)`
33
+
34
+ | Param | Type | Description |
35
+ | --------- | ----------------------------- | ------------------------------------------------------------------------------------------------- |
36
+ | `message` | `string` | Body text rendered as a subdued caption. |
37
+ | `options` | `string \| ShowToastOptions` | Optional. Pass a string to set just the title (legacy form), or an options bag for richer toasts. |
38
+
39
+ ### `ShowToastOptions`
40
+
41
+ | Field | Type | Default | Description |
42
+ | ----------- | ------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------- |
43
+ | `title` | `string` | `'Success'` / `'Error'` | Heading text. |
44
+ | `action` | `ShowToastAction` | — | Optional action button rendered below the body. |
45
+ | `autoClose` | `number \| false` | 4000 (success) / 5000 (error); `false` when `action` is present and not overridden | Override auto-close. `false` keeps the toast up. |
46
+
47
+ ### `ShowToastAction`
48
+
49
+ | Field | Type | Description |
50
+ | --------- | -------------- | ------------------------------------ |
51
+ | `label` | `string` | Button label, e.g. `'Retry'`. |
52
+ | `onClick` | `() => void` | Click handler. |
53
+
54
+ ## Defaults
55
+
56
+ - **Auto-close**: 4s for success, 5s for error.
57
+ - **Action toasts**: when `options.action` is present and `autoClose` is unspecified, the toast persists until dismissed so the user can click the action. Pass `autoClose: 8000` (or any number) to opt back into auto-close.
58
+ - **Color**: green (success) / red (error) — encoded in the function name. Never pass color manually.
59
+
60
+ ## When to use what
61
+
62
+ - **Plain string** — simple confirmation, single line. `showSuccessToast('Saved.')`.
63
+ - **With `title`** — when heading and body convey different information ("Initiative deleted" / "Foo was removed.").
64
+ - **With `action`** — recoverable errors where a one-click retry is meaningful (transient API failures). Persists by default.
65
+ - **Raw `notifications.show()`** — escape hatch for cases the helpers don't cover (custom icons, loading states, multi-step UI). Prefer extending the helpers instead so call sites stay consistent.
66
+
67
+ ## Don't
68
+
69
+ - Don't call `showSuccessToast` / `showErrorToast` inside a hook — toasting is a UI concern with no view context. See `apps/lm-web/.ai/rules/mutations.md` and `apps/lm-web/.ai/rules/error-handling.md` for the canonical pattern (hooks expose `error: Error | null`; components decide UX).
70
+ - Don't bypass the helpers to pass `color` directly via raw `notifications.show()` for semantics — the function-name choice is the source of truth.
71
+
72
+ ## Provider
73
+
74
+ Mount `<Notifications />` once at the app root (or in a shared overlays component):
75
+
76
+ ```tsx
77
+ import { Notifications } from '@scalepad/ui';
78
+
79
+ <App>
80
+ <Notifications />
81
+ {/* ... */}
82
+ </App>
83
+ ```
84
+
85
+ ## More Examples
86
+
87
+ See Storybook for interactive demos: `SuccessToast`, `ErrorToast`, `CustomTitles`, `WithRetryAction`.
88
+
89
+ ## Real-World Usage
90
+
91
+ - `apps/lm-web/src/features/assessments/components/AssessmentDetail.tsx` — error toast with retry action.
92
+ - `apps/lm-web/src/features/initiatives/components/initiativeOverflowMenu/InitiativeOverflowMenu.tsx` — success / error toasts in mutation callbacks.
93
+ - `apps/amm-web/src/features/catalog/components/CatalogAppActionsCell.tsx` — string-only success / error toasts.
@@ -0,0 +1,4 @@
1
+ export { Notifications } from './Notifications';
2
+ export { showSuccessToast, showErrorToast } from './showToast';
3
+ export type { ShowToastAction, ShowToastOptions } from './showToast';
4
+ export { notifications } from '@mantine/notifications';
@@ -0,0 +1,100 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { notifications } from '@mantine/notifications';
4
+
5
+ import { Button } from '../Button';
6
+ import { Stack } from '../DesignSystemPrimitives';
7
+ import { Text } from '../Typography';
8
+
9
+ export interface ShowToastAction {
10
+ label: string;
11
+ onClick: () => void;
12
+ }
13
+
14
+ export interface ShowToastOptions {
15
+ title?: string;
16
+ action?: ShowToastAction;
17
+ /** Override auto-close. Pass `false` to keep the toast until dismissed. */
18
+ autoClose?: number | false;
19
+ }
20
+
21
+ type SecondArg = string | ShowToastOptions;
22
+
23
+ const SUCCESS_AUTO_CLOSE_MS = 4000;
24
+ const ERROR_AUTO_CLOSE_MS = 5000;
25
+
26
+ function normalizeOptions(secondArg?: SecondArg): ShowToastOptions {
27
+ if (secondArg == null) return {};
28
+ return typeof secondArg === 'string' ? { title: secondArg } : secondArg;
29
+ }
30
+
31
+ function buildBody(message: string, action?: ShowToastAction): ReactNode {
32
+ const caption = (
33
+ <Text variant="caption1" c="text.subdued.default">
34
+ {message}
35
+ </Text>
36
+ );
37
+
38
+ if (!action) {
39
+ return caption;
40
+ }
41
+
42
+ return (
43
+ <Stack gap="xs">
44
+ {caption}
45
+ <Button
46
+ type="button"
47
+ size="xs"
48
+ variant="outline"
49
+ onClick={action.onClick}
50
+ >
51
+ {action.label}
52
+ </Button>
53
+ </Stack>
54
+ );
55
+ }
56
+
57
+ function resolveAutoClose(
58
+ options: ShowToastOptions,
59
+ defaultMs: number,
60
+ ): number | false {
61
+ if (options.autoClose !== undefined) return options.autoClose;
62
+ // When an action is present, persist by default so the user can click it.
63
+ return options.action ? false : defaultMs;
64
+ }
65
+
66
+ /**
67
+ * Show a brief success toast (e.g. after create/update/delete).
68
+ * Pass a string as the second arg to set just the title, or an options bag
69
+ * for `action` / `autoClose` overrides.
70
+ */
71
+ export function showSuccessToast(
72
+ message: string,
73
+ optionsOrTitle?: SecondArg,
74
+ ): void {
75
+ const options = normalizeOptions(optionsOrTitle);
76
+ notifications.show({
77
+ title: options.title ?? 'Success',
78
+ message: buildBody(message, options.action),
79
+ color: 'green',
80
+ autoClose: resolveAutoClose(options, SUCCESS_AUTO_CLOSE_MS),
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Show a brief error toast.
86
+ * Pass a string as the second arg to set just the title, or an options bag
87
+ * for `action` (e.g. a Retry button) / `autoClose` overrides.
88
+ */
89
+ export function showErrorToast(
90
+ message: string,
91
+ optionsOrTitle?: SecondArg,
92
+ ): void {
93
+ const options = normalizeOptions(optionsOrTitle);
94
+ notifications.show({
95
+ title: options.title ?? 'Error',
96
+ message: buildBody(message, options.action),
97
+ color: 'red',
98
+ autoClose: resolveAutoClose(options, ERROR_AUTO_CLOSE_MS),
99
+ });
100
+ }
@@ -0,0 +1,96 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { Box, Group } from '../DesignSystemPrimitives';
4
+ import { Text } from '../Typography';
5
+
6
+ export interface PropertyRowProps {
7
+ /** Small icon (typically a 14px lucide glyph) rendered to the left of the label. */
8
+ icon: ReactNode;
9
+ /** Plain label text — e.g. "Client", "Due date", "Status". */
10
+ label: string;
11
+ /**
12
+ * Right-hand value content. Free-form so each property can render its
13
+ * own widget (chip, picker, plain text, link, etc.) without the row
14
+ * imposing structure.
15
+ */
16
+ children: ReactNode;
17
+ /**
18
+ * Override the fixed width of the label column. Defaults to 120 — the
19
+ * value the task drawer was designed against. Bump it for surfaces
20
+ * with longer label text so labels and values stay vertically aligned
21
+ * across rows.
22
+ */
23
+ labelWidth?: number;
24
+ /**
25
+ * Cross-axis alignment of the label column against the value content.
26
+ * Defaults to `'center'` for single-line values; pass `'flex-start'`
27
+ * when the value can grow vertically (e.g. a stack of assignees) so
28
+ * the label stays anchored to the first row of the value instead of
29
+ * floating in the middle.
30
+ */
31
+ align?: 'center' | 'flex-start';
32
+ /**
33
+ * When true, blocks interaction (e.g. while a mutation is in-flight).
34
+ */
35
+ disabled?: boolean;
36
+ }
37
+
38
+ const DEFAULT_LABEL_COLUMN_WIDTH = 120;
39
+ const ROW_MIN_HEIGHT = 28;
40
+
41
+ /**
42
+ * Two-column property row used by detail-drawer metadata stacks. Renders
43
+ * a subdued icon + label on the left and the consumer-provided value on
44
+ * the right. The label column is fixed-width (overridable via
45
+ * `labelWidth`) so labels stay vertically aligned across rows even when
46
+ * value widths vary — that lock-step alignment is what makes a stack of
47
+ * these rows scan as a coherent property table rather than a jagged list.
48
+ *
49
+ * Designed to be domain-agnostic: drawers in tasks, initiatives,
50
+ * playbooks, etc. should all reach for this primitive instead of
51
+ * re-implementing the same two-column layout.
52
+ */
53
+ export function PropertyRow({
54
+ icon,
55
+ label,
56
+ children,
57
+ labelWidth = DEFAULT_LABEL_COLUMN_WIDTH,
58
+ align = 'center',
59
+ disabled = false,
60
+ }: PropertyRowProps) {
61
+ return (
62
+ <Group
63
+ gap="sm"
64
+ wrap="nowrap"
65
+ align={align}
66
+ mih={ROW_MIN_HEIGHT}
67
+ opacity={disabled ? 0.5 : 1}
68
+ aria-disabled={disabled || undefined}
69
+ // Block clicks / focus through to children when disabled so a
70
+ // mutation-in-flight row can't fire a second mutation. The visual
71
+ // opacity above is intentionally kept so the row still reads as
72
+ // "soft" rather than removed entirely.
73
+ style={
74
+ disabled ? { pointerEvents: 'none', userSelect: 'none' } : undefined
75
+ }
76
+ >
77
+ <Group
78
+ gap="2xs"
79
+ wrap="nowrap"
80
+ align="center"
81
+ w={labelWidth}
82
+ miw={labelWidth}
83
+ mih={ROW_MIN_HEIGHT}
84
+ c="text.subdued.default"
85
+ >
86
+ {icon}
87
+ <Text variant="body1" c="text.subdued.default">
88
+ {label}
89
+ </Text>
90
+ </Group>
91
+ <Box flex={1} miw={0}>
92
+ {children}
93
+ </Box>
94
+ </Group>
95
+ );
96
+ }
@@ -0,0 +1,2 @@
1
+ export { PropertyRow } from './PropertyRow';
2
+ export type { PropertyRowProps } from './PropertyRow';