@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,133 @@
1
+ import { forwardRef, useId } from 'react';
2
+
3
+ import { ChevronDown } from 'lucide-react';
4
+
5
+ import { Combobox, useCombobox } from '@mantine/core';
6
+
7
+ import * as styles from './Select.css';
8
+
9
+ export interface SelectProps {
10
+ /**
11
+ * The label prefix to display before the selected value (e.g., "Department")
12
+ */
13
+ labelPrefix?: string;
14
+ /**
15
+ * Select options with value and label
16
+ */
17
+ data: Array<{ value: string; label: string }>;
18
+ /**
19
+ * Current selected value
20
+ */
21
+ value?: string | null;
22
+ /**
23
+ * Callback when value changes
24
+ */
25
+ onChange?: (value: string | null) => void;
26
+ /**
27
+ * Placeholder text when no value is selected
28
+ */
29
+ placeholder?: string;
30
+ /**
31
+ * Width of the select
32
+ */
33
+ w?: string | number;
34
+ /**
35
+ * Validation error message displayed below the select
36
+ */
37
+ error?: string;
38
+ }
39
+
40
+ /**
41
+ * Custom Select component matching Figma design specifications.
42
+ * Displays a label prefix in muted color before the selected value.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <Select
47
+ * labelPrefix="Department"
48
+ * data={[
49
+ * { value: 'all', label: 'All' },
50
+ * { value: 'sales', label: 'Sales' },
51
+ * ]}
52
+ * value={value}
53
+ * onChange={setValue}
54
+ * />
55
+ * ```
56
+ */
57
+ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
58
+ ({ labelPrefix, data, value, onChange, placeholder, w, error }, ref) => {
59
+ const id = useId();
60
+ const errorId = error ? `${id}-error` : undefined;
61
+ const combobox = useCombobox({
62
+ onDropdownClose: () => combobox.resetSelectedOption(),
63
+ });
64
+
65
+ // Find the selected option label
66
+ const selectedOption = data.find(option => option.value === value);
67
+ const selectedLabel = selectedOption?.label || '';
68
+
69
+ const options = data.map(item => (
70
+ <Combobox.Option value={item.value} key={item.value}>
71
+ {item.label}
72
+ </Combobox.Option>
73
+ ));
74
+
75
+ return (
76
+ <div>
77
+ <Combobox
78
+ store={combobox}
79
+ width="max-content"
80
+ position="bottom-start"
81
+ withinPortal
82
+ onOptionSubmit={val => {
83
+ onChange?.(val);
84
+ combobox.closeDropdown();
85
+ }}
86
+ >
87
+ <Combobox.Target>
88
+ <button
89
+ ref={ref}
90
+ type="button"
91
+ className={styles.selectButton}
92
+ onClick={() => combobox.toggleDropdown()}
93
+ aria-haspopup="listbox"
94
+ aria-expanded={combobox.dropdownOpened}
95
+ aria-invalid={!!error}
96
+ aria-describedby={errorId}
97
+ style={
98
+ w ? { width: typeof w === 'number' ? `${w}px` : w } : undefined
99
+ }
100
+ >
101
+ <div className={styles.valueContainer}>
102
+ {labelPrefix && selectedLabel ? (
103
+ <>
104
+ <span className={styles.labelPrefix}>{labelPrefix}</span>
105
+ <span className={styles.selectedValue}>
106
+ {selectedLabel}
107
+ </span>
108
+ </>
109
+ ) : (
110
+ <span className={styles.selectedValue}>
111
+ {selectedLabel || placeholder}
112
+ </span>
113
+ )}
114
+ </div>
115
+ <ChevronDown size={16} className={styles.chevron} />
116
+ </button>
117
+ </Combobox.Target>
118
+
119
+ <Combobox.Dropdown>
120
+ <Combobox.Options>{options}</Combobox.Options>
121
+ </Combobox.Dropdown>
122
+ </Combobox>
123
+ {error && (
124
+ <p id={errorId} className={styles.errorText} role="alert">
125
+ {error}
126
+ </p>
127
+ )}
128
+ </div>
129
+ );
130
+ },
131
+ );
132
+
133
+ Select.displayName = 'Select';
@@ -0,0 +1,2 @@
1
+ export { Select } from './Select';
2
+ export type { SelectProps } from './Select';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SelectCreatable component styles – vanilla-extract with semantic design tokens
3
+ */
4
+
5
+ import { style } from '@vanilla-extract/css';
6
+
7
+ import { tokens } from '../../theme/themeContract.css';
8
+
9
+ export const placeholderInput = style({
10
+ selectors: {
11
+ '&::placeholder': {
12
+ color: tokens.color.text.subduedDefault,
13
+ fontStyle: 'italic',
14
+ },
15
+ },
16
+ });
@@ -0,0 +1,203 @@
1
+ import { forwardRef, useMemo, useState } from 'react';
2
+
3
+ import { Combobox, Divider, InputBase, useCombobox } from '@mantine/core';
4
+
5
+ import * as styles from './SelectCreatable.css';
6
+
7
+ const CREATE_OPTION_VALUE = '$create';
8
+
9
+ export interface SelectCreatableProps {
10
+ /**
11
+ * Select options with value and label
12
+ */
13
+ data: Array<{ value: string; label: string }>;
14
+ /**
15
+ * Current selected value
16
+ */
17
+ value?: string | null;
18
+ /**
19
+ * Callback when value changes
20
+ */
21
+ onChange?: (value: string | null) => void;
22
+ /**
23
+ * Placeholder text when no value is selected
24
+ */
25
+ placeholder?: string;
26
+ /**
27
+ * Width of the select
28
+ */
29
+ w?: string | number;
30
+ /**
31
+ * When true, show "Create {searchText}" option when search doesn't match; on submit call onCreate(searchText).
32
+ * If onCreate returns Promise<string>, the resolved id is passed to onChange.
33
+ */
34
+ creatable?: boolean;
35
+ /**
36
+ * Called when user chooses to create a new option. If returns string or Promise<string>, that id is passed to onChange.
37
+ */
38
+ onCreate?: (searchValue: string) => void | string | Promise<string | void>;
39
+ /**
40
+ * Whether the trigger is disabled (e.g. while loading)
41
+ */
42
+ disabled?: boolean;
43
+ }
44
+
45
+ /**
46
+ * Searchable select with optional "Create" option.
47
+ * Uses InputBase as target: type to search, input shows selected label when closed.
48
+ * When creatable, shows "+ Create {searchText}" when search is non-empty and not an exact match.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * <SelectCreatable
53
+ * data={departments.map((d) => ({ value: d.id, label: d.name }))}
54
+ * value={value}
55
+ * onChange={setValue}
56
+ * creatable
57
+ * onCreate={async (name) => {
58
+ * const newDept = await createDepartment(name);
59
+ * return newDept.id;
60
+ * }}
61
+ * />
62
+ * ```
63
+ */
64
+ export const SelectCreatable = forwardRef<
65
+ HTMLInputElement,
66
+ SelectCreatableProps
67
+ >(
68
+ (
69
+ {
70
+ data,
71
+ value,
72
+ onChange,
73
+ placeholder,
74
+ w,
75
+ creatable = false,
76
+ onCreate,
77
+ disabled = false,
78
+ },
79
+ ref,
80
+ ) => {
81
+ const [search, setSearch] = useState('');
82
+
83
+ const combobox = useCombobox({
84
+ onDropdownClose: () => {
85
+ combobox.resetSelectedOption();
86
+ },
87
+ });
88
+
89
+ const selectedOption = data.find(option => option.value === value);
90
+ const selectedLabel = selectedOption?.label ?? '';
91
+
92
+ const exactOptionMatch = data.some(
93
+ item => item.label.toLowerCase() === search.trim().toLowerCase(),
94
+ );
95
+ const filteredData = useMemo(() => {
96
+ if (exactOptionMatch) return data;
97
+ const q = search.trim().toLowerCase();
98
+ if (!q) return data;
99
+ return data.filter(item => item.label.toLowerCase().includes(q));
100
+ }, [data, search, exactOptionMatch]);
101
+
102
+ const searchTrimmed = search.trim();
103
+ const showCreateOption =
104
+ creatable &&
105
+ Boolean(onCreate) &&
106
+ searchTrimmed.length > 0 &&
107
+ !exactOptionMatch;
108
+
109
+ const handleOptionSubmit = (val: string) => {
110
+ if (val === CREATE_OPTION_VALUE) {
111
+ const runCreate = async () => {
112
+ const result = await onCreate?.(searchTrimmed);
113
+ if (typeof result === 'string') {
114
+ onChange?.(result);
115
+ const newLabel =
116
+ data.find(o => o.value === result)?.label ?? searchTrimmed;
117
+ setSearch(newLabel);
118
+ } else {
119
+ setSearch(selectedLabel);
120
+ }
121
+ combobox.closeDropdown();
122
+ };
123
+ void runCreate();
124
+ return;
125
+ }
126
+ const option = data.find(item => item.value === val);
127
+ onChange?.(val);
128
+ setSearch(option?.label ?? '');
129
+ combobox.closeDropdown();
130
+ };
131
+
132
+ const inputValue = combobox.dropdownOpened ? search : selectedLabel;
133
+ const hasValue = Boolean(value && selectedLabel);
134
+
135
+ const openDropdown = () => {
136
+ if (value && search !== selectedLabel) {
137
+ setSearch(selectedLabel);
138
+ }
139
+ combobox.openDropdown();
140
+ };
141
+
142
+ return (
143
+ <Combobox store={combobox} onOptionSubmit={handleOptionSubmit}>
144
+ <Combobox.Target>
145
+ <InputBase
146
+ ref={ref}
147
+ value={inputValue}
148
+ onChange={event => {
149
+ openDropdown();
150
+ combobox.updateSelectedOptionIndex();
151
+ setSearch(event.currentTarget.value);
152
+ }}
153
+ onClick={openDropdown}
154
+ onFocus={openDropdown}
155
+ onBlur={() => {
156
+ combobox.closeDropdown();
157
+ setSearch(selectedLabel);
158
+ }}
159
+ placeholder={placeholder}
160
+ disabled={disabled}
161
+ rightSection={<Combobox.Chevron />}
162
+ rightSectionPointerEvents="none"
163
+ w={w}
164
+ aria-expanded={combobox.dropdownOpened}
165
+ aria-haspopup="listbox"
166
+ classNames={{
167
+ input: !hasValue ? styles.placeholderInput : undefined,
168
+ }}
169
+ />
170
+ </Combobox.Target>
171
+
172
+ <Combobox.Dropdown>
173
+ <Combobox.Options
174
+ style={{
175
+ maxHeight: '300px',
176
+ overflowY: 'auto',
177
+ }}
178
+ >
179
+ {filteredData.map(item => (
180
+ <Combobox.Option value={item.value} key={item.value}>
181
+ {item.label}
182
+ </Combobox.Option>
183
+ ))}
184
+ {showCreateOption && (
185
+ <>
186
+ {filteredData.length > 0 && <Divider />}
187
+
188
+ <Combobox.Option
189
+ value={CREATE_OPTION_VALUE}
190
+ key={CREATE_OPTION_VALUE}
191
+ >
192
+ + Create {searchTrimmed}
193
+ </Combobox.Option>
194
+ </>
195
+ )}
196
+ </Combobox.Options>
197
+ </Combobox.Dropdown>
198
+ </Combobox>
199
+ );
200
+ },
201
+ );
202
+
203
+ SelectCreatable.displayName = 'SelectCreatable';
@@ -0,0 +1,2 @@
1
+ export { SelectCreatable } from './SelectCreatable';
2
+ export type { SelectCreatableProps } from './SelectCreatable';
@@ -0,0 +1,98 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { ChevronRight } from 'lucide-react';
4
+
5
+ import { Card, Flex, UnstyledButton } from '@mantine/core';
6
+
7
+ import { tokens } from '../../theme/themeContract.css';
8
+ import { Text } from '../Typography';
9
+
10
+ export interface SettingsCardProps {
11
+ icon?: ReactNode;
12
+ title: ReactNode;
13
+ description?: ReactNode;
14
+ onClick?: () => void;
15
+ rightSection?: ReactNode;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ function SettingsCardContent({
20
+ icon,
21
+ title,
22
+ description,
23
+ rightSection,
24
+ }: Omit<SettingsCardProps, 'onClick' | 'disabled'>) {
25
+ const resolvedRightSection =
26
+ rightSection === undefined ? (
27
+ <Flex align="center" c={tokens.color.icon.default}>
28
+ <ChevronRight size={20} data-testid="settings-card-chevron-right" />
29
+ </Flex>
30
+ ) : (
31
+ rightSection
32
+ );
33
+
34
+ return (
35
+ <Card withBorder radius="lg" p="md">
36
+ <Flex align="center" gap="xs">
37
+ {icon ? (
38
+ <Flex align="center" c={tokens.color.icon.default}>
39
+ {icon}
40
+ </Flex>
41
+ ) : null}
42
+ <Flex direction="column" style={{ minWidth: 0, flex: 1 }}>
43
+ <Text variant="caption1.strong" c="text.default">
44
+ {title}
45
+ </Text>
46
+ {description ? (
47
+ <Text variant="caption1" c="text.subdued.default">
48
+ {description}
49
+ </Text>
50
+ ) : null}
51
+ </Flex>
52
+ {resolvedRightSection}
53
+ </Flex>
54
+ </Card>
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Shared settings row card used in account settings screens.
60
+ */
61
+ export function SettingsCard({
62
+ icon,
63
+ title,
64
+ description,
65
+ onClick,
66
+ rightSection,
67
+ disabled = false,
68
+ }: SettingsCardProps) {
69
+ if (!onClick) {
70
+ return (
71
+ <SettingsCardContent
72
+ icon={icon}
73
+ title={title}
74
+ description={description}
75
+ rightSection={rightSection}
76
+ />
77
+ );
78
+ }
79
+
80
+ return (
81
+ <UnstyledButton
82
+ w="100%"
83
+ onClick={onClick}
84
+ disabled={disabled}
85
+ style={{
86
+ cursor: disabled ? 'not-allowed' : 'pointer',
87
+ borderRadius: tokens.radius.lg,
88
+ }}
89
+ >
90
+ <SettingsCardContent
91
+ icon={icon}
92
+ title={title}
93
+ description={description}
94
+ rightSection={rightSection}
95
+ />
96
+ </UnstyledButton>
97
+ );
98
+ }
@@ -0,0 +1,2 @@
1
+ export { SettingsCard } from './SettingsCard';
2
+ export type { SettingsCardProps } from './SettingsCard';
@@ -0,0 +1,91 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ import { tokens } from '../../theme/themeContract.css';
4
+ import { textStyleVariants } from '../../tokens/text-styles';
5
+
6
+ export const item = style({
7
+ display: 'flex',
8
+ alignItems: 'center',
9
+ gap: tokens.spacing.xs,
10
+ height: tokens.spacing['2xl'],
11
+ paddingInline: tokens.spacing.sm,
12
+ paddingBlock: tokens.spacing['2xs'],
13
+ borderRadius: tokens.radius.md,
14
+ textDecoration: 'none',
15
+ border: 'none',
16
+ background: 'transparent',
17
+ cursor: 'pointer',
18
+ color: tokens.color.text.subduedStrong,
19
+ ...textStyleVariants.caption1,
20
+ fontFamily: 'inherit',
21
+ transition: 'background-color 120ms ease, color 120ms ease',
22
+ selectors: {
23
+ '&:hover': {
24
+ backgroundColor: tokens.color.background.subduedLightHover,
25
+ },
26
+ },
27
+ });
28
+
29
+ export const itemActive = style({
30
+ backgroundColor: tokens.color.background.primaryLight,
31
+ color: tokens.color.text.primaryDefault,
32
+ ...textStyleVariants['body1.stronger'],
33
+ fontFamily: 'inherit',
34
+ selectors: {
35
+ '&:hover': {
36
+ backgroundColor: tokens.color.background.primaryLightHover,
37
+ },
38
+ },
39
+ });
40
+
41
+ export const itemDisabled = style({
42
+ cursor: 'default',
43
+ color: tokens.color.text.subduedDefault,
44
+ opacity: 0.5,
45
+ selectors: {
46
+ '&:hover': {
47
+ backgroundColor: 'transparent',
48
+ },
49
+ },
50
+ });
51
+
52
+ export const itemCollapsed = style({
53
+ width: '100%',
54
+ justifyContent: 'center',
55
+ paddingInline: tokens.spacing['2xs'],
56
+ });
57
+
58
+ export const itemIcon = style({
59
+ display: 'inline-flex',
60
+ alignItems: 'center',
61
+ justifyContent: 'center',
62
+ flexShrink: 0,
63
+ width: 16,
64
+ height: 16,
65
+ });
66
+
67
+ export const itemLabelCollapsed = style({
68
+ display: 'none',
69
+ });
70
+
71
+ export const tooltipTarget = style({
72
+ display: 'block',
73
+ width: '100%',
74
+ });
75
+
76
+ export const groupLabel = style({
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ paddingInline: tokens.spacing.sm,
80
+ paddingBlock: tokens.spacing['2xs'],
81
+ color: tokens.color.text.subduedDefault,
82
+ ...textStyleVariants['caption2.strong'],
83
+ fontFamily: 'inherit',
84
+ });
85
+
86
+ export const divider = style({
87
+ height: 1,
88
+ width: '100%',
89
+ backgroundColor: tokens.color.stroke.subduedDefault,
90
+ flexShrink: 0,
91
+ });
@@ -0,0 +1,129 @@
1
+ import {
2
+ type ComponentPropsWithoutRef,
3
+ type ElementType,
4
+ type ReactNode,
5
+ } from 'react';
6
+
7
+ import { Tooltip } from '@mantine/core';
8
+
9
+ import * as classes from './Sidebar.css';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Sidebar.Item – polymorphic nav item (renders as <button> by default,
13
+ // accepts `component` to render as a TanStack Router Link, <a>, etc.)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ type SidebarItemOwnProps = {
17
+ icon?: ReactNode;
18
+ label: string;
19
+ active?: boolean;
20
+ disabled?: boolean;
21
+ collapsed?: boolean;
22
+ };
23
+
24
+ type PolymorphicProps<C extends ElementType> = SidebarItemOwnProps &
25
+ Omit<ComponentPropsWithoutRef<C>, keyof SidebarItemOwnProps | 'component'> & {
26
+ component?: C;
27
+ };
28
+
29
+ function SidebarItemInner<C extends ElementType = 'button'>({
30
+ component,
31
+ icon,
32
+ label,
33
+ active = false,
34
+ disabled = false,
35
+ collapsed = false,
36
+ className,
37
+ ...rest
38
+ }: PolymorphicProps<C>) {
39
+ const hasNavigationTarget =
40
+ 'href' in rest || 'to' in rest || 'onClick' in rest;
41
+ const Component: ElementType = hasNavigationTarget
42
+ ? (component ?? 'button')
43
+ : 'button';
44
+ const isDisabled = disabled || !hasNavigationTarget;
45
+ const isButtonComponent = Component === 'button';
46
+ const combinedClassName = [
47
+ classes.item,
48
+ active && classes.itemActive,
49
+ isDisabled && classes.itemDisabled,
50
+ collapsed && classes.itemCollapsed,
51
+ className,
52
+ ]
53
+ .filter(Boolean)
54
+ .join(' ');
55
+
56
+ const itemNode = (
57
+ <Component
58
+ aria-disabled={isDisabled && !isButtonComponent ? true : undefined}
59
+ aria-label={collapsed ? label : undefined}
60
+ className={combinedClassName}
61
+ disabled={isDisabled && isButtonComponent ? true : undefined}
62
+ type={isButtonComponent ? 'button' : undefined}
63
+ {...rest}
64
+ >
65
+ {icon && (
66
+ <span aria-hidden="true" className={classes.itemIcon}>
67
+ {icon}
68
+ </span>
69
+ )}
70
+ <span className={collapsed ? classes.itemLabelCollapsed : undefined}>
71
+ {label}
72
+ </span>
73
+ </Component>
74
+ );
75
+
76
+ if (!collapsed) {
77
+ return itemNode;
78
+ }
79
+
80
+ return (
81
+ <Tooltip label={label} position="right">
82
+ <div className={classes.tooltipTarget}>{itemNode}</div>
83
+ </Tooltip>
84
+ );
85
+ }
86
+
87
+ const SidebarItem = SidebarItemInner as <C extends ElementType = 'button'>(
88
+ props: PolymorphicProps<C>,
89
+ ) => ReactNode;
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Sidebar.GroupLabel – section header (e.g. "Workflow", "Portfolio")
93
+ // ---------------------------------------------------------------------------
94
+
95
+ interface SidebarGroupLabelProps {
96
+ children: ReactNode;
97
+ className?: string;
98
+ }
99
+
100
+ function SidebarGroupLabel({ children, className }: SidebarGroupLabelProps) {
101
+ return (
102
+ <div className={`${classes.groupLabel}${className ? ` ${className}` : ''}`}>
103
+ {children}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Sidebar.Divider – thin horizontal separator between groups
110
+ // ---------------------------------------------------------------------------
111
+
112
+ function SidebarDivider({ className }: { className?: string }) {
113
+ return (
114
+ <div className={`${classes.divider}${className ? ` ${className}` : ''}`} />
115
+ );
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Compound export
120
+ // ---------------------------------------------------------------------------
121
+
122
+ export type { SidebarItemOwnProps as SidebarItemProps };
123
+ export type { SidebarGroupLabelProps };
124
+
125
+ export const Sidebar = {
126
+ Item: SidebarItem,
127
+ GroupLabel: SidebarGroupLabel,
128
+ Divider: SidebarDivider,
129
+ };
@@ -0,0 +1,5 @@
1
+ export {
2
+ Sidebar,
3
+ type SidebarItemProps,
4
+ type SidebarGroupLabelProps,
5
+ } from './Sidebar';
@@ -0,0 +1,12 @@
1
+ import { style } from '@vanilla-extract/css';
2
+
3
+ import { tokens } from '../../theme/themeContract.css';
4
+
5
+ export const list = style({
6
+ margin: 0,
7
+ paddingLeft: tokens.spacing.md,
8
+ });
9
+
10
+ export const item = style({
11
+ margin: 0,
12
+ });