@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,239 @@
1
+ import { useCallback, useMemo, useState, type UIEvent } from 'react';
2
+
3
+ import { Box, Checkbox, Flex, Skeleton } from '@mantine/core';
4
+
5
+ import { Menu } from '../../Menu';
6
+ import { SearchableSubMenu } from '../../SearchableSubMenu';
7
+ import { Text } from '../../Typography';
8
+
9
+ import type {
10
+ SearchableListAsyncProps,
11
+ SearchableListSearchMode,
12
+ } from '../../SearchableList/types';
13
+ import type { SearchableFilterItem } from '../types';
14
+
15
+ export type SearchMode = SearchableListSearchMode;
16
+
17
+ export interface SearchableFilterSubmenuProps extends SearchableListAsyncProps {
18
+ /** Label for the submenu (e.g., "Department", "App Category") */
19
+ label: string;
20
+ /** List of items to filter */
21
+ items: SearchableFilterItem[];
22
+ /**
23
+ * Currently selected items (with both `id` and `name`).
24
+ * Selected items can refer to entries that are not in `items` — e.g. when
25
+ * the parent uses server-side search and previously-selected items have
26
+ * scrolled out of the current result page. The submenu preserves them as-is.
27
+ */
28
+ selectedItems: SearchableFilterItem[];
29
+ /** Callback when selection changes - passes array of selected items with id and name */
30
+ onSelectionChange: (selectedItems: SearchableFilterItem[]) => void;
31
+ /** Optional callback to clear all selections (shows "All [Label]" option) */
32
+ onClearAll?: () => void;
33
+ /** Optional placeholder text for search input (defaults to "Filter") */
34
+ placeholder?: string;
35
+ /** Whether to show search input control (defaults to true) */
36
+ showSearch?: boolean;
37
+ /** Optional message when no items match search (defaults to "No items found") */
38
+ emptyMessage?: string;
39
+ }
40
+
41
+ const LOADING_MORE_SKELETON_COUNT = 5;
42
+
43
+ function LoadingMoreSkeletonMenuItems() {
44
+ return Array.from({ length: LOADING_MORE_SKELETON_COUNT }).map((_, idx) => (
45
+ <Menu.Item key={`loading-more-${idx}`} disabled>
46
+ <Flex align="center" gap="sm">
47
+ <Skeleton h={14} w={14} radius="xs" />
48
+ <Skeleton h={10} w={`${60 + (idx % 3) * 10}%`} />
49
+ </Flex>
50
+ </Menu.Item>
51
+ ));
52
+ }
53
+
54
+ export function SearchableFilterSubmenu({
55
+ label,
56
+ items,
57
+ selectedItems,
58
+ onSelectionChange,
59
+ onClearAll,
60
+ placeholder = 'Filter',
61
+ showSearch = true,
62
+ emptyMessage = 'No items found',
63
+ isLoading = false,
64
+ isError = false,
65
+ errorMessage,
66
+ onRetry,
67
+ hasMore = false,
68
+ onLoadMore,
69
+ isLoadingMore = false,
70
+ searchMode = 'client',
71
+ onSearchChange,
72
+ searchDebounceMs = 250,
73
+ }: SearchableFilterSubmenuProps) {
74
+ const [search, setSearch] = useState('');
75
+
76
+ // Filter items based on search query
77
+ const displayedItems = useMemo(() => {
78
+ if (searchMode === 'server') return items;
79
+ if (!search.trim()) return items;
80
+ const searchLower = search.toLowerCase();
81
+ return items.filter(item => item.name.toLowerCase().includes(searchLower));
82
+ }, [items, search, searchMode]);
83
+
84
+ const handleSearchChange = useCallback(
85
+ (value: string) => {
86
+ setSearch(value);
87
+
88
+ if (searchMode === 'server') {
89
+ onSearchChange?.(value);
90
+ }
91
+ },
92
+ [onSearchChange, searchMode],
93
+ );
94
+
95
+ const handleScroll = (e: UIEvent<HTMLDivElement>) => {
96
+ if (!hasMore) return;
97
+ if (isLoadingMore) return;
98
+ if (!onLoadMore) return;
99
+
100
+ const el = e.currentTarget;
101
+ const thresholdPx = 150;
102
+ const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
103
+ if (distanceToBottom <= thresholdPx) {
104
+ onLoadMore();
105
+ }
106
+ };
107
+
108
+ const selectedIds = useMemo(
109
+ () => new Set(selectedItems.map(item => item.id)),
110
+ [selectedItems],
111
+ );
112
+
113
+ // Toggle item selection. We work off `selectedItems` (not `items`) so that
114
+ // entries previously selected from a different search query are preserved
115
+ // even when they're no longer in the current `items` page.
116
+ const toggleItem = (itemId: string) => {
117
+ if (selectedIds.has(itemId)) {
118
+ onSelectionChange(selectedItems.filter(item => item.id !== itemId));
119
+ return;
120
+ }
121
+
122
+ const newItem = items.find(item => item.id === itemId);
123
+ if (!newItem) {
124
+ // The toggled item is not in the visible list. Should not happen because
125
+ // toggle is only triggered from a checkbox rendered against `items`.
126
+ return;
127
+ }
128
+
129
+ onSelectionChange([
130
+ ...selectedItems,
131
+ { id: newItem.id, name: newItem.name },
132
+ ]);
133
+ };
134
+
135
+ // Handle clear all
136
+ const handleClearAll = () => {
137
+ if (onClearAll) {
138
+ onClearAll();
139
+ setSearch('');
140
+ if (searchMode === 'server') {
141
+ onSearchChange?.('');
142
+ }
143
+ }
144
+ };
145
+
146
+ return (
147
+ <SearchableSubMenu
148
+ label={label}
149
+ search={search}
150
+ onSearchChange={handleSearchChange}
151
+ placeholder={placeholder}
152
+ showSearch={showSearch}
153
+ debounceMs={searchMode === 'server' ? searchDebounceMs : undefined}
154
+ >
155
+ {/* Clear All Option (if provided) */}
156
+ {onClearAll && (
157
+ <>
158
+ <Menu.Item
159
+ onClick={e => {
160
+ e.stopPropagation();
161
+ handleClearAll();
162
+ }}
163
+ closeMenuOnClick={false}
164
+ >
165
+ <Text variant="caption1.strong">Clear All</Text>
166
+ </Menu.Item>
167
+ <Menu.Divider />
168
+ </>
169
+ )}
170
+
171
+ {/* Item List */}
172
+ <Box
173
+ style={{ maxHeight: '300px', overflowY: 'auto' }}
174
+ onScroll={handleScroll}
175
+ >
176
+ {isLoading ? (
177
+ <Box p="xs">
178
+ <Text variant="caption1" c="text.subdued.default">
179
+ Loading…
180
+ </Text>
181
+ </Box>
182
+ ) : isError ? (
183
+ <Box p="xs">
184
+ <Text variant="caption1" c="text.subdued.default">
185
+ {errorMessage || 'Error loading options.'}
186
+ </Text>
187
+ {onRetry && (
188
+ <Menu.Item closeMenuOnClick={false} onClick={onRetry}>
189
+ <Text variant="caption1.strong">Retry</Text>
190
+ </Menu.Item>
191
+ )}
192
+ </Box>
193
+ ) : displayedItems.length > 0 ? (
194
+ <>
195
+ {displayedItems.map(item => (
196
+ <Menu.Item
197
+ key={item.id}
198
+ onClick={e => {
199
+ e.stopPropagation();
200
+ toggleItem(item.id);
201
+ }}
202
+ closeMenuOnClick={false}
203
+ onKeyDown={e => {
204
+ if (e.key === ' ') {
205
+ e.stopPropagation();
206
+ e.preventDefault();
207
+ toggleItem(item.id);
208
+ }
209
+ }}
210
+ >
211
+ <Flex align="center" gap="sm">
212
+ <Checkbox
213
+ checked={selectedIds.has(item.id)}
214
+ onChange={() => toggleItem(item.id)}
215
+ onClick={e => e.stopPropagation()}
216
+ size="sm"
217
+ label={item.name}
218
+ />
219
+ </Flex>
220
+ </Menu.Item>
221
+ ))}
222
+ {isLoadingMore && <LoadingMoreSkeletonMenuItems />}
223
+ </>
224
+ ) : (
225
+ <Box p="xs">
226
+ <Text variant="caption1" c="text.subdued.default">
227
+ {emptyMessage}
228
+ </Text>
229
+ {isLoadingMore && (
230
+ <Box mt="xs">
231
+ <LoadingMoreSkeletonMenuItems />
232
+ </Box>
233
+ )}
234
+ </Box>
235
+ )}
236
+ </Box>
237
+ </SearchableSubMenu>
238
+ );
239
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ BooleanFilterSubmenu,
3
+ type BooleanFilterSubmenuProps,
4
+ } from './BooleanFilterSubmenu';
5
+ export {
6
+ SearchableFilterSubmenu,
7
+ type SearchableFilterSubmenuProps,
8
+ } from './SearchableFilterSubmenu';
@@ -0,0 +1,63 @@
1
+ import type { FilterItem } from './FilterMenu';
2
+ import type { FilterSchema } from './types';
3
+
4
+ // Default departments list
5
+ export const DEFAULT_DEPARTMENTS: FilterItem[] = [
6
+ { id: 'engineering', name: 'Engineering' },
7
+ { id: 'sales', name: 'Sales' },
8
+ { id: 'marketing', name: 'Marketing' },
9
+ { id: 'support', name: 'Support' },
10
+ { id: 'hr', name: 'Human Resources' },
11
+ { id: 'finance', name: 'Finance' },
12
+ { id: 'operations', name: 'Operations' },
13
+ { id: 'product', name: 'Product' },
14
+ { id: 'design', name: 'Design' },
15
+ { id: 'legal', name: 'Legal' },
16
+ ];
17
+
18
+ // Default app categories list
19
+ export const DEFAULT_APP_CATEGORIES: FilterItem[] = [
20
+ { id: 'productivity', name: 'Productivity' },
21
+ { id: 'communication', name: 'Communication' },
22
+ { id: 'development', name: 'Development' },
23
+ { id: 'design', name: 'Design Tools' },
24
+ { id: 'analytics', name: 'Analytics' },
25
+ { id: 'security', name: 'Security' },
26
+ { id: 'storage', name: 'Storage & Backup' },
27
+ { id: 'collaboration', name: 'Collaboration' },
28
+ { id: 'project-management', name: 'Project Management' },
29
+ { id: 'customer-support', name: 'Customer Support' },
30
+ ];
31
+
32
+ /**
33
+ * Default filter schemas for backwards compatibility
34
+ * These match the original hard-coded filter types
35
+ */
36
+ export const defaultFilterSchemas: FilterSchema[] = [
37
+ {
38
+ key: 'favourites',
39
+ categoryName: 'Favourites',
40
+ type: 'boolean',
41
+ label: 'Favourites',
42
+ submenuType: 'boolean',
43
+ },
44
+ {
45
+ key: 'departments',
46
+ categoryName: 'Department',
47
+ type: 'multi-select',
48
+ label: 'Department',
49
+ submenuType: 'searchable',
50
+ items: DEFAULT_DEPARTMENTS,
51
+ showClearAll: true,
52
+ emptyMessage: 'No departments found',
53
+ },
54
+ {
55
+ key: 'appCategories',
56
+ categoryName: 'Category',
57
+ type: 'multi-select',
58
+ label: 'App Category',
59
+ submenuType: 'searchable',
60
+ items: DEFAULT_APP_CATEGORIES,
61
+ emptyMessage: 'No categories found',
62
+ },
63
+ ];
@@ -0,0 +1,115 @@
1
+ import type { FilterItem } from './FilterMenu';
2
+ import type { FilterSchema, FilterValue } from './types';
3
+ import type { FilterCategory } from '../AppliedFiltersManagerBar/AppliedFiltersManagerBar';
4
+
5
+ /**
6
+ * Extract the current filter value from FilterCategory[] based on schema
7
+ */
8
+ export function extractFilterValue(
9
+ schema: FilterSchema,
10
+ filters: FilterCategory[],
11
+ ): FilterValue {
12
+ const category = filters.find(
13
+ cat => cat.categoryName === schema.categoryName,
14
+ );
15
+
16
+ if (schema.type === 'boolean') {
17
+ // Boolean filters: true if category exists and has items, false otherwise
18
+ return category ? category.items.length > 0 : false;
19
+ }
20
+
21
+ if (schema.type === 'multi-select') {
22
+ // Multi-select filters: return array of FilterItems
23
+ return category?.items || [];
24
+ }
25
+
26
+ // Fallback (should never reach here with proper typing)
27
+ return false;
28
+ }
29
+
30
+ /**
31
+ * Create a FilterCategory from schema and value
32
+ */
33
+ export function createFilterCategory(
34
+ schema: FilterSchema,
35
+ value: FilterValue,
36
+ ): FilterCategory | null {
37
+ if (schema.type === 'boolean') {
38
+ const boolValue = value as boolean;
39
+ if (boolValue) {
40
+ return {
41
+ categoryName: schema.categoryName,
42
+ items: [{ id: schema.key, name: 'True' }],
43
+ };
44
+ }
45
+ return null; // Don't create category if boolean is false
46
+ }
47
+
48
+ if (schema.type === 'multi-select') {
49
+ const items = value as FilterItem[];
50
+ if (items.length > 0) {
51
+ return {
52
+ categoryName: schema.categoryName,
53
+ items,
54
+ };
55
+ }
56
+ return null; // Don't create category if no items selected
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Update FilterCategory[] with new value based on schema
64
+ * Preserves other categories that aren't being updated
65
+ */
66
+ export function updateFilterCategory(
67
+ schema: FilterSchema,
68
+ value: FilterValue,
69
+ currentFilters: FilterCategory[],
70
+ ): FilterCategory[] {
71
+ const newCategory = createFilterCategory(schema, value);
72
+ let found = false;
73
+
74
+ const updatedFilters = currentFilters.reduce<FilterCategory[]>((acc, cat) => {
75
+ if (cat.categoryName === schema.categoryName) {
76
+ found = true;
77
+ // Replace the existing category with the new one, or remove it if no value
78
+ if (newCategory) {
79
+ acc.push(newCategory);
80
+ }
81
+ return acc;
82
+ }
83
+
84
+ acc.push(cat);
85
+ return acc;
86
+ }, []);
87
+
88
+ // If the category didn't exist before and we have a new one, append it
89
+ if (!found && newCategory) {
90
+ updatedFilters.push(newCategory);
91
+ }
92
+
93
+ return updatedFilters;
94
+ }
95
+
96
+ /**
97
+ * Update multiple filter categories at once
98
+ */
99
+ export function updateFilterCategories(
100
+ schemas: FilterSchema[],
101
+ updates: Record<string, FilterValue>,
102
+ currentFilters: FilterCategory[],
103
+ ): FilterCategory[] {
104
+ let newFilters = [...currentFilters];
105
+
106
+ // Process each update
107
+ Object.entries(updates).forEach(([key, value]) => {
108
+ const schema = schemas.find(s => s.key === key);
109
+ if (schema) {
110
+ newFilters = updateFilterCategory(schema, value, newFilters);
111
+ }
112
+ });
113
+
114
+ return newFilters;
115
+ }
@@ -0,0 +1,35 @@
1
+ export { FilterMenu } from './FilterMenu';
2
+ export type { FilterItem, FilterMenuProps } from './FilterMenu';
3
+
4
+ // Filter schema types and utilities
5
+ export {
6
+ DEFAULT_APP_CATEGORIES,
7
+ DEFAULT_DEPARTMENTS,
8
+ defaultFilterSchemas,
9
+ } from './defaultFilterSchemas';
10
+ export {
11
+ createFilterCategory,
12
+ extractFilterValue,
13
+ updateFilterCategories,
14
+ updateFilterCategory,
15
+ } from './helpers';
16
+ export type {
17
+ BooleanFilterSchema,
18
+ FilterSchema,
19
+ FilterType,
20
+ FilterValue,
21
+ MultiSelectFilterSchema,
22
+ SearchableFilterItemsController,
23
+ SearchableFilterItemsStatus,
24
+ SubmenuType,
25
+ } from './types';
26
+
27
+ // Generic submenu components (for advanced usage)
28
+ export {
29
+ BooleanFilterSubmenu,
30
+ SearchableFilterSubmenu,
31
+ } from './FilterSubMenuTypes';
32
+ export type {
33
+ BooleanFilterSubmenuProps,
34
+ SearchableFilterSubmenuProps,
35
+ } from './FilterSubMenuTypes';
@@ -0,0 +1,101 @@
1
+ import type { FilterItem } from './FilterMenu';
2
+ import type { SearchableListSearchMode } from '../SearchableList/types';
3
+
4
+ // Filter value types
5
+ export type FilterValue = boolean | FilterItem[];
6
+
7
+ // Filter type discriminator
8
+ export type FilterType = 'boolean' | 'multi-select';
9
+
10
+ // Submenu type discriminator
11
+ export type SubmenuType = 'boolean' | 'searchable';
12
+
13
+ export type SearchableFilterSearchMode = SearchableListSearchMode;
14
+
15
+ export interface SearchableFilterSearchConfig {
16
+ mode: SearchableFilterSearchMode;
17
+ /**
18
+ * Called when the user types in the search input (typically used for server-side search).
19
+ * For client-side search, this is optional and usually omitted.
20
+ */
21
+ onSearchChange?: (query: string) => void;
22
+ /** Debounce time for `onSearchChange` (defaults to 250ms in the UI). */
23
+ debounceMs?: number;
24
+ }
25
+
26
+ export type SearchableFilterItemsStatus =
27
+ | 'idle'
28
+ | 'loading'
29
+ | 'success'
30
+ | 'error';
31
+
32
+ export interface SearchableFilterItemsController {
33
+ status: SearchableFilterItemsStatus;
34
+ /** Optional error message to display when status is `error`. */
35
+ errorMessage?: string;
36
+ /** Retry loading items (shown when status is `error`). */
37
+ retry?: () => void;
38
+ /** Whether more items exist (enables infinite scroll). */
39
+ hasMore?: boolean;
40
+ /** Load the next page of items. */
41
+ loadMore?: () => void;
42
+ /** Whether a next page is currently loading (shows a loading-more row). */
43
+ isLoadingMore?: boolean;
44
+ }
45
+
46
+ // Base filter schema
47
+ export interface BaseFilterSchema {
48
+ /** Unique identifier (e.g., 'favourites', 'departments') */
49
+ key: string;
50
+ /** Display name in FilterCategory (e.g., 'Favourites', 'Department') */
51
+ categoryName: string;
52
+ /** Filter type discriminator */
53
+ type: FilterType;
54
+ /** Display label in menu (e.g., 'Favourites', 'Department') */
55
+ label: string;
56
+ }
57
+
58
+ // Boolean filter schema (e.g., Favourites)
59
+ export interface BooleanFilterSchema extends BaseFilterSchema {
60
+ type: 'boolean';
61
+ /** Type of submenu to render for this filter */
62
+ submenuType: 'boolean';
63
+ }
64
+
65
+ // Multi-select filter schema (e.g., Departments, Categories)
66
+ export interface MultiSelectFilterSchema extends BaseFilterSchema {
67
+ type: 'multi-select';
68
+ /** Type of submenu to render for this filter */
69
+ submenuType: 'searchable';
70
+ /** Items available for selection in this filter */
71
+ items: FilterItem[];
72
+ /**
73
+ * Optional search behavior configuration. Defaults to client-side filtering.
74
+ * - `client`: submenu filters `items` locally.
75
+ * - `server`: submenu calls `search.onSearchChange` (debounced) and renders `items` as-is.
76
+ */
77
+ search?: SearchableFilterSearchConfig;
78
+ /**
79
+ * Optional async/pagination controller for API-backed option lists.
80
+ * The app owns data fetching and passes flattened `items` + controller state.
81
+ */
82
+ itemsController?: SearchableFilterItemsController;
83
+ /** Whether to show "All [Label]" option to clear all selections (defaults to false) */
84
+ showClearAll?: boolean;
85
+ /** Optional custom callback to clear all selections (if not provided, defaults to clearing selections) */
86
+ onClearAll?: () => void;
87
+ /** Optional placeholder text for search input (defaults to "Filter") */
88
+ placeholder?: string;
89
+ /** Whether to show the search/filter input in submenu (defaults to true) */
90
+ showSearch?: boolean;
91
+ /** Optional message when no items match search (defaults to "No items found") */
92
+ emptyMessage?: string;
93
+ }
94
+
95
+ export interface SearchableFilterItem {
96
+ id: string;
97
+ name: string;
98
+ }
99
+
100
+ // Union type for all filter schemas
101
+ export type FilterSchema = BooleanFilterSchema | MultiSelectFilterSchema;