@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.
- package/.ai/rules/date-handling.md +39 -0
- package/.ai/rules/figma-design-system.md +372 -0
- package/.ai/rules/figma-lm-design-system-keys.md +680 -0
- package/.ai/rules/file-extensions.md +13 -0
- package/.ai/rules/modal-confirmation-mutation.md +56 -0
- package/.ai/rules/react-hooks.md +29 -0
- package/.ai/rules/styling.md +83 -0
- package/AGENTS.md +37 -0
- package/README.md +125 -0
- package/figma.config.json +9 -0
- package/package.json +127 -0
- package/scripts/install-ai-rules.mjs +136 -0
- package/src/ThemeProvider.tsx +57 -0
- package/src/charts.ts +32 -0
- package/src/components/ActionCard/ActionCard.css.ts +60 -0
- package/src/components/ActionCard/ActionCard.tsx +154 -0
- package/src/components/ActionCard/index.ts +2 -0
- package/src/components/Anchor/Anchor.tsx +47 -0
- package/src/components/Anchor/index.ts +2 -0
- package/src/components/AppliedFiltersManagerBar/AppliedFiltersManagerBar.tsx +105 -0
- package/src/components/AppliedFiltersManagerBar/FilterBadge.css.ts +23 -0
- package/src/components/AppliedFiltersManagerBar/FilterBadge.tsx +50 -0
- package/src/components/AppliedFiltersManagerBar/index.ts +5 -0
- package/src/components/Badge/Badge.css.ts +72 -0
- package/src/components/Badge/Badge.figma.tsx +43 -0
- package/src/components/Badge/Badge.tsx +159 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BreadCrumb/BreadCrumb.tsx +62 -0
- package/src/components/BreadCrumb/index.ts +2 -0
- package/src/components/BulkActionBar/BulkActionBar.css.ts +26 -0
- package/src/components/BulkActionBar/BulkActionBar.tsx +164 -0
- package/src/components/BulkActionBar/index.ts +2 -0
- package/src/components/Button/Button.css.ts +272 -0
- package/src/components/Button/Button.figma.tsx +74 -0
- package/src/components/Button/Button.tsx +84 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Charts/ChartTooltip.figma.tsx +33 -0
- package/src/components/Charts/ChartTooltip.tsx +101 -0
- package/src/components/Charts/MiniBarSparkline.tsx +75 -0
- package/src/components/Charts/StackedPatternBarChart.tsx +494 -0
- package/src/components/Charts/TrendAreaChart.css.ts +23 -0
- package/src/components/Charts/TrendAreaChart.tsx +210 -0
- package/src/components/Charts/index.ts +12 -0
- package/src/components/CodePanel/CodePanel.css.ts +113 -0
- package/src/components/CodePanel/CodePanel.tsx +121 -0
- package/src/components/CodePanel/index.ts +2 -0
- package/src/components/CommentComposer/CommentComposer.css.ts +60 -0
- package/src/components/CommentComposer/CommentComposer.tsx +181 -0
- package/src/components/CommentComposer/index.ts +2 -0
- package/src/components/ConfirmationModal/ConfirmationModal.tsx +149 -0
- package/src/components/ConfirmationModal/index.ts +2 -0
- package/src/components/ConfirmationTooltip/ConfirmationTooltip.tsx +132 -0
- package/src/components/ConfirmationTooltip/index.ts +2 -0
- package/src/components/DataDialog.figma.tsx +33 -0
- package/src/components/DataDialog.tsx +46 -0
- package/src/components/DataTable/DataTable.tsx +1042 -0
- package/src/components/DataTable/RowExpandToggle.tsx +105 -0
- package/src/components/DataTable/RowGroupHeader.tsx +190 -0
- package/src/components/DataTable/createActionsColumn.tsx +86 -0
- package/src/components/DataTable/index.ts +25 -0
- package/src/components/DatePicker/CustomRangePicker.tsx +59 -0
- package/src/components/DatePicker/DateInput.tsx +329 -0
- package/src/components/DatePicker/DateNavigator.tsx +486 -0
- package/src/components/DatePicker/DatePicker.tsx +242 -0
- package/src/components/DatePicker/MonthlyRangePicker.tsx +231 -0
- package/src/components/DatePicker/QuarterlyRangePicker.tsx +224 -0
- package/src/components/DatePicker/QuickPicksSidebar.tsx +242 -0
- package/src/components/DatePicker/YearlyRangePicker.tsx +171 -0
- package/src/components/DatePicker/index.ts +7 -0
- package/src/components/DatePicker/types.ts +12 -0
- package/src/components/DesignSystemPrimitives/FluidGrid.tsx +44 -0
- package/src/components/DesignSystemPrimitives/InteractivePrimitives.tsx +177 -0
- package/src/components/DesignSystemPrimitives/LayoutPrimitives.tsx +220 -0
- package/src/components/DesignSystemPrimitives/LayoutPrimitives.types.tsx +15 -0
- package/src/components/DesignSystemPrimitives/SurfacePrimitives.tsx +46 -0
- package/src/components/DesignSystemPrimitives/index.ts +55 -0
- package/src/components/Details/Details.css.ts +74 -0
- package/src/components/Details/Details.tsx +140 -0
- package/src/components/Details/index.ts +2 -0
- package/src/components/DownloadCard/DownloadCard.css.ts +22 -0
- package/src/components/DownloadCard/DownloadCard.tsx +63 -0
- package/src/components/DownloadCard/index.ts +2 -0
- package/src/components/Drawer/Drawer.css.ts +32 -0
- package/src/components/Drawer/Drawer.tsx +236 -0
- package/src/components/Drawer/hooks/useDetailDrawer.ts +61 -0
- package/src/components/Drawer/hooks/useDetailDrawerNavigation.ts +125 -0
- package/src/components/Drawer/hooks/useDetailDrawerNavigationContext.ts +66 -0
- package/src/components/EditableRichText/EditableRichText.css.ts +72 -0
- package/src/components/EditableRichText/EditableRichText.tsx +324 -0
- package/src/components/EditableRichText/index.ts +2 -0
- package/src/components/EditableSelect/EditableSelect.css.ts +62 -0
- package/src/components/EditableSelect/EditableSelect.tsx +224 -0
- package/src/components/EditableSelect/index.ts +2 -0
- package/src/components/EditableText/EditableText.tsx +377 -0
- package/src/components/EditableText/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.figma.tsx +33 -0
- package/src/components/EmptyState/EmptyState.tsx +230 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/ErrorBoundary.tsx +135 -0
- package/src/components/ErrorState/ErrorState.tsx +197 -0
- package/src/components/ErrorState/index.ts +2 -0
- package/src/components/FeatureCard.tsx +42 -0
- package/src/components/FilterMenu/FilterMenu.figma.tsx +30 -0
- package/src/components/FilterMenu/FilterMenu.tsx +198 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/BooleanFilterSubmenu.tsx +46 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/SearchableFilterSubmenu.tsx +239 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/index.ts +8 -0
- package/src/components/FilterMenu/defaultFilterSchemas.ts +63 -0
- package/src/components/FilterMenu/helpers.ts +115 -0
- package/src/components/FilterMenu/index.ts +35 -0
- package/src/components/FilterMenu/types.ts +101 -0
- package/src/components/IconButton/IconButton.css.ts +272 -0
- package/src/components/IconButton/IconButton.figma.tsx +47 -0
- package/src/components/IconButton/IconButton.tsx +72 -0
- package/src/components/IconButton/README.md +230 -0
- package/src/components/IconButton/index.ts +2 -0
- package/src/components/InfiniteScrollSentinel.tsx +86 -0
- package/src/components/InfiniteScrollTrigger.tsx +78 -0
- package/src/components/InfoCard.figma.tsx +47 -0
- package/src/components/InfoCard.tsx +216 -0
- package/src/components/KbdHint/KbdHint.tsx +23 -0
- package/src/components/KbdHint/index.ts +2 -0
- package/src/components/LabeledField/LabeledField.tsx +21 -0
- package/src/components/LabeledField/index.ts +2 -0
- package/src/components/LookupSelect/LookupSelect.css.ts +149 -0
- package/src/components/LookupSelect/LookupSelect.tsx +325 -0
- package/src/components/LookupSelect/index.ts +2 -0
- package/src/components/Menu/Menu.css.ts +89 -0
- package/src/components/Menu/Menu.tsx +105 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/MessageBox/MessageBox.tsx +168 -0
- package/src/components/MessageBox/index.ts +2 -0
- package/src/components/MetricDisplay/MetricDisplay.tsx +55 -0
- package/src/components/MetricDisplay/index.ts +1 -0
- package/src/components/MultiSelect/MultiSelect.tsx +278 -0
- package/src/components/MultiSelect/index.ts +2 -0
- package/src/components/Notifications/Notifications.tsx +12 -0
- package/src/components/Notifications/README.md +93 -0
- package/src/components/Notifications/index.ts +4 -0
- package/src/components/Notifications/showToast.tsx +100 -0
- package/src/components/PropertyRow/PropertyRow.tsx +96 -0
- package/src/components/PropertyRow/index.ts +2 -0
- package/src/components/RadioTile/RadioTile.tsx +253 -0
- package/src/components/RadioTile/index.ts +2 -0
- package/src/components/RichText/FormattingToolbar.css.ts +69 -0
- package/src/components/RichText/FormattingToolbar.tsx +112 -0
- package/src/components/RichText/RichTextInline.css.ts +54 -0
- package/src/components/RichText/RichTextInline.tsx +318 -0
- package/src/components/RichText/formattingCommands.ts +181 -0
- package/src/components/RichText/formattingTypes.ts +34 -0
- package/src/components/RichText/index.ts +49 -0
- package/src/components/RichText/richTextExtensions.ts +111 -0
- package/src/components/RichText/richTextHelpers.ts +65 -0
- package/src/components/RichText/richTextImage.ts +253 -0
- package/src/components/RichText/richTextImageHandlers.ts +244 -0
- package/src/components/RichText/richTextProse.css.ts +261 -0
- package/src/components/RichTextEditor/RichTextEditor.css.ts +82 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +204 -0
- package/src/components/RichTextEditor/index.ts +2 -0
- package/src/components/RichTextView/RichTextView.css.ts +11 -0
- package/src/components/RichTextView/RichTextView.tsx +114 -0
- package/src/components/RichTextView/index.ts +2 -0
- package/src/components/Schedule/Schedule.tsx +35 -0
- package/src/components/SchedulePicker/SchedulePicker.css.ts +42 -0
- package/src/components/SchedulePicker/SchedulePicker.tsx +130 -0
- package/src/components/SchedulePicker/index.ts +2 -0
- package/src/components/SearchableList/types.ts +30 -0
- package/src/components/SearchableSubMenu/SearchableSubMenu.css.ts +25 -0
- package/src/components/SearchableSubMenu/SearchableSubMenu.tsx +139 -0
- package/src/components/SearchableSubMenu/index.ts +2 -0
- package/src/components/Select/README.md +114 -0
- package/src/components/Select/Select.css.ts +110 -0
- package/src/components/Select/Select.tsx +133 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/SelectCreatable/SelectCreatable.css.ts +16 -0
- package/src/components/SelectCreatable/SelectCreatable.tsx +203 -0
- package/src/components/SelectCreatable/index.ts +2 -0
- package/src/components/SettingsCard/SettingsCard.tsx +98 -0
- package/src/components/SettingsCard/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.css.ts +91 -0
- package/src/components/Sidebar/Sidebar.tsx +129 -0
- package/src/components/Sidebar/index.ts +5 -0
- package/src/components/SimpleList/SimpleList.css.ts +12 -0
- package/src/components/SimpleList/SimpleList.tsx +44 -0
- package/src/components/SimpleList/index.ts +2 -0
- package/src/components/SimpleTable/SimpleTable.tsx +296 -0
- package/src/components/SimpleTable/index.ts +2 -0
- package/src/components/SlashRichTextEditor/SelectionBubbleMenu.css.ts +62 -0
- package/src/components/SlashRichTextEditor/SelectionBubbleMenu.tsx +85 -0
- package/src/components/SlashRichTextEditor/SlashCommandMenu.css.ts +124 -0
- package/src/components/SlashRichTextEditor/SlashCommandMenu.tsx +168 -0
- package/src/components/SlashRichTextEditor/SlashRichTextEditor.css.ts +81 -0
- package/src/components/SlashRichTextEditor/SlashRichTextEditor.tsx +538 -0
- package/src/components/SlashRichTextEditor/SlashSuggestionExtension.ts +48 -0
- package/src/components/SlashRichTextEditor/index.ts +13 -0
- package/src/components/SlashRichTextEditor/types.ts +48 -0
- package/src/components/StatCard/StatCard.css.ts +70 -0
- package/src/components/StatCard/StatCard.tsx +201 -0
- package/src/components/StatCard/index.ts +1 -0
- package/src/components/StatusBadge/StatusBadge.tsx +70 -0
- package/src/components/StatusBadge/index.ts +2 -0
- package/src/components/StatusIndicator/StatusIndicator.tsx +67 -0
- package/src/components/StatusIndicator/index.ts +6 -0
- package/src/components/SubNavigation/SubNavigation.css.ts +72 -0
- package/src/components/SubNavigation/SubNavigation.tsx +104 -0
- package/src/components/SubNavigation/index.ts +2 -0
- package/src/components/SuspenseLoader.tsx +22 -0
- package/src/components/Table/SortableColumnHeader.tsx +99 -0
- package/src/components/Table/TableSkeletonRows.figma.tsx +22 -0
- package/src/components/Table/TableSkeletonRows.tsx +113 -0
- package/src/components/Table/index.ts +9 -0
- package/src/components/TableActionsMenu.tsx +58 -0
- package/src/components/TableCard.tsx +29 -0
- package/src/components/TableContainer/TableContainer.tsx +86 -0
- package/src/components/TableContainer/index.ts +2 -0
- package/src/components/TableControlBar/TableControlBar.tsx +156 -0
- package/src/components/TableControlBar/TableSelectionButton.tsx +57 -0
- package/src/components/TableControlBar/index.ts +13 -0
- package/src/components/TableControlBar/useTableControlBar.tsx +314 -0
- package/src/components/TableSelection/TableSelection.tsx +43 -0
- package/src/components/TableSelection/index.ts +5 -0
- package/src/components/Tabs/README.md +76 -0
- package/src/components/Tabs/Tabs.css.ts +54 -0
- package/src/components/Tabs/Tabs.figma.tsx +47 -0
- package/src/components/Tabs/Tabs.tsx +96 -0
- package/src/components/Tabs/index.ts +8 -0
- package/src/components/TextInput/README.md +98 -0
- package/src/components/TextInput/SearchTextInput.figma.tsx +22 -0
- package/src/components/TextInput/SearchTextInput.tsx +150 -0
- package/src/components/TextInput/TextInput.figma.tsx +44 -0
- package/src/components/TextInput/TextInput.tsx +42 -0
- package/src/components/TextInput/index.ts +4 -0
- package/src/components/ThemeSwitcher.figma.tsx +28 -0
- package/src/components/ThemeSwitcher.tsx +69 -0
- package/src/components/TrendBadge/TrendBadge.tsx +76 -0
- package/src/components/TrendBadge/index.ts +2 -0
- package/src/components/TruncatedText.tsx +115 -0
- package/src/components/Typography/Text.tsx +74 -0
- package/src/components/Typography/Title.tsx +100 -0
- package/src/components/Typography/index.ts +4 -0
- package/src/geist-fonts.ts +48 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/useFilters.ts +152 -0
- package/src/hooks/useInfiniteScroll.ts +62 -0
- package/src/hooks/usePlatform.ts +33 -0
- package/src/hooks/useServerTable.ts +495 -0
- package/src/hooks/useTableSelection.ts +102 -0
- package/src/hooks/useTableSort.ts +259 -0
- package/src/index.ts +483 -0
- package/src/mantine.ts +25 -0
- package/src/theme/mantineVars.ts +12 -0
- package/src/theme/themeContract.css.ts +131 -0
- package/src/theme/themeVars.ts +31 -0
- package/src/theme.ts +168 -0
- package/src/tokens/color-types.ts +107 -0
- package/src/tokens/colors.ts +243 -0
- package/src/tokens/index.ts +14 -0
- package/src/tokens/radius.ts +17 -0
- package/src/tokens/semantic-colors.ts +224 -0
- package/src/tokens/semantic-tokens-css.ts +53 -0
- package/src/tokens/shadows.ts +11 -0
- package/src/tokens/spacing.ts +20 -0
- package/src/tokens/text-styles.ts +179 -0
- package/src/tokens/typography.ts +40 -0
- package/src/tokens/zIndex.ts +27 -0
- package/src/types/mantine-theme.d.ts +17 -0
- package/src/types/tanstack-table.d.ts +22 -0
- package/src/utils/avatar.ts +150 -0
- package/src/utils/chartHelpers.ts +53 -0
- package/src/utils/color-props.ts +77 -0
- package/src/utils/createDesignComponent.tsx +104 -0
- package/src/utils/nestFlatRows.ts +111 -0
- 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,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;
|