@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,135 @@
|
|
|
1
|
+
import React, { Component, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Alert, Stack } from '@mantine/core';
|
|
4
|
+
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { Text } from './Typography';
|
|
7
|
+
|
|
8
|
+
export interface ErrorBoundaryProps {
|
|
9
|
+
/**
|
|
10
|
+
* Content to render when no error has occurred
|
|
11
|
+
*/
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fallback UI to render when an error is caught
|
|
16
|
+
* Can be a ReactNode or a function that receives the error and reset function
|
|
17
|
+
*/
|
|
18
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional callback when an error is caught
|
|
22
|
+
*/
|
|
23
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional callback when the error boundary is reset
|
|
27
|
+
*/
|
|
28
|
+
onReset?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ErrorBoundaryState {
|
|
32
|
+
hasError: boolean;
|
|
33
|
+
error: Error | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Error boundary component that catches JavaScript errors in child components
|
|
38
|
+
*
|
|
39
|
+
* Provides a fallback UI when errors occur and allows recovery via reset.
|
|
40
|
+
* Use this to prevent entire sections from crashing when a component fails.
|
|
41
|
+
*
|
|
42
|
+
* @example Basic usage with default fallback
|
|
43
|
+
* ```tsx
|
|
44
|
+
* <ErrorBoundary>
|
|
45
|
+
* <MyComponent />
|
|
46
|
+
* </ErrorBoundary>
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example Custom fallback UI
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <ErrorBoundary
|
|
52
|
+
* fallback={(error, reset) => (
|
|
53
|
+
* <Alert color="red">
|
|
54
|
+
* <Text>Failed to load data</Text>
|
|
55
|
+
* <Button onClick={reset}>Retry</Button>
|
|
56
|
+
* </Alert>
|
|
57
|
+
* )}
|
|
58
|
+
* >
|
|
59
|
+
* <MyComponent />
|
|
60
|
+
* </ErrorBoundary>
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example With error logging
|
|
64
|
+
* ```tsx
|
|
65
|
+
* <ErrorBoundary
|
|
66
|
+
* onError={(error, errorInfo) => {
|
|
67
|
+
* console.error('Caught error:', error, errorInfo);
|
|
68
|
+
* // Send to error tracking service
|
|
69
|
+
* }}
|
|
70
|
+
* >
|
|
71
|
+
* <MyComponent />
|
|
72
|
+
* </ErrorBoundary>
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class ErrorBoundary extends Component<
|
|
76
|
+
ErrorBoundaryProps,
|
|
77
|
+
ErrorBoundaryState
|
|
78
|
+
> {
|
|
79
|
+
constructor(props: ErrorBoundaryProps) {
|
|
80
|
+
super(props);
|
|
81
|
+
this.state = { hasError: false, error: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
85
|
+
return { hasError: true, error };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
89
|
+
// Call optional error callback
|
|
90
|
+
this.props.onError?.(error, errorInfo);
|
|
91
|
+
|
|
92
|
+
// Log error to console in development (Vite: import.meta.env.DEV)
|
|
93
|
+
const isDev =
|
|
94
|
+
typeof import.meta !== 'undefined' &&
|
|
95
|
+
(import.meta as { env?: { DEV?: boolean } }).env?.DEV;
|
|
96
|
+
if (isDev) {
|
|
97
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resetErrorBoundary = (): void => {
|
|
102
|
+
this.props.onReset?.();
|
|
103
|
+
this.setState({ hasError: false, error: null });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
render(): ReactNode {
|
|
107
|
+
if (this.state.hasError && this.state.error) {
|
|
108
|
+
// Use custom fallback if provided
|
|
109
|
+
if (this.props.fallback) {
|
|
110
|
+
if (typeof this.props.fallback === 'function') {
|
|
111
|
+
return this.props.fallback(this.state.error, this.resetErrorBoundary);
|
|
112
|
+
}
|
|
113
|
+
return this.props.fallback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default fallback UI
|
|
117
|
+
return (
|
|
118
|
+
<Alert color="red" title="Something went wrong">
|
|
119
|
+
<Stack gap="sm">
|
|
120
|
+
<Text variant="caption1">{this.state.error.message}</Text>
|
|
121
|
+
<Button
|
|
122
|
+
size="sm"
|
|
123
|
+
variant="destructive"
|
|
124
|
+
onClick={this.resetErrorBoundary}
|
|
125
|
+
>
|
|
126
|
+
Try Again
|
|
127
|
+
</Button>
|
|
128
|
+
</Stack>
|
|
129
|
+
</Alert>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return this.props.children;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { ElementType, FunctionComponent, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { AlertCircle } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { Box, Card, Stack } from '@mantine/core';
|
|
6
|
+
|
|
7
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
8
|
+
import { toCssVar, type BackgroundColorVar } from '../../theme/themeVars';
|
|
9
|
+
import { Button } from '../Button';
|
|
10
|
+
import { Text } from '../Typography';
|
|
11
|
+
|
|
12
|
+
export interface ErrorStateAction {
|
|
13
|
+
label: string;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
/** React Router Link component or other polymorphic component */
|
|
16
|
+
component?: ElementType;
|
|
17
|
+
/** Link destination (when using component) */
|
|
18
|
+
to?: string;
|
|
19
|
+
/** Icon to show on the right side of button */
|
|
20
|
+
rightSection?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ErrorStateProps {
|
|
24
|
+
/**
|
|
25
|
+
* The icon to display at the top
|
|
26
|
+
* @default <AlertCircle size={48} />
|
|
27
|
+
*/
|
|
28
|
+
icon?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Icon size (width and height in pixels)
|
|
31
|
+
* @default 48
|
|
32
|
+
*/
|
|
33
|
+
iconSize?: number;
|
|
34
|
+
/**
|
|
35
|
+
* The title/heading text
|
|
36
|
+
*/
|
|
37
|
+
title?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The description/body text
|
|
40
|
+
*/
|
|
41
|
+
description?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Primary action button configuration
|
|
44
|
+
*/
|
|
45
|
+
action?: ErrorStateAction;
|
|
46
|
+
/**
|
|
47
|
+
* Secondary action button configuration (ghost variant)
|
|
48
|
+
*/
|
|
49
|
+
secondaryAction?: ErrorStateAction;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum width of the content area
|
|
52
|
+
* @default 400
|
|
53
|
+
*/
|
|
54
|
+
maxWidth?: number | string;
|
|
55
|
+
/**
|
|
56
|
+
* Minimum height of the error container
|
|
57
|
+
* @default 200
|
|
58
|
+
*/
|
|
59
|
+
minHeight?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Wrap content in a Card with background and border
|
|
62
|
+
* @default true
|
|
63
|
+
*/
|
|
64
|
+
showCard?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Card background (theme contract background token). Only used when showCard=true.
|
|
67
|
+
* Pass tokens.color.background.* from @scalepad/ui.
|
|
68
|
+
*/
|
|
69
|
+
cardBg?: BackgroundColorVar;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ErrorState component
|
|
74
|
+
* Displays a contained error message without breaking the page layout
|
|
75
|
+
* Similar API to EmptyState for consistency
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Simple error state
|
|
79
|
+
* <ErrorState
|
|
80
|
+
* title="Failed to load data"
|
|
81
|
+
* description="An error occurred while fetching the data"
|
|
82
|
+
* action={{ label: 'Try Again', onClick: retry }}
|
|
83
|
+
* />
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Compact error with custom icon
|
|
87
|
+
* <ErrorState
|
|
88
|
+
* icon={<XCircle size={32} />}
|
|
89
|
+
* iconSize={32}
|
|
90
|
+
* title="Connection failed"
|
|
91
|
+
* minHeight={120}
|
|
92
|
+
* />
|
|
93
|
+
*/
|
|
94
|
+
export function ErrorState({
|
|
95
|
+
icon = <AlertCircle size={48} />,
|
|
96
|
+
iconSize = 48,
|
|
97
|
+
title,
|
|
98
|
+
description,
|
|
99
|
+
action,
|
|
100
|
+
secondaryAction,
|
|
101
|
+
maxWidth = 400,
|
|
102
|
+
minHeight = 200,
|
|
103
|
+
showCard = true,
|
|
104
|
+
cardBg = tokens.color.background.subduedLight,
|
|
105
|
+
}: ErrorStateProps) {
|
|
106
|
+
const iconElement = (
|
|
107
|
+
<Box
|
|
108
|
+
w={iconSize}
|
|
109
|
+
h={iconSize}
|
|
110
|
+
display="flex"
|
|
111
|
+
style={{
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
color: toCssVar(tokens.color.icon.dangerDefault),
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{icon}
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const content = (
|
|
122
|
+
<Stack gap="md" align="center">
|
|
123
|
+
{iconElement}
|
|
124
|
+
|
|
125
|
+
<Stack gap="xs" align="center" maw={maxWidth}>
|
|
126
|
+
{title && (
|
|
127
|
+
<Text variant="body1.stronger" ta="center">
|
|
128
|
+
{title}
|
|
129
|
+
</Text>
|
|
130
|
+
)}
|
|
131
|
+
{description && (
|
|
132
|
+
<Text variant="caption1" c="text.subdued.default" ta="center">
|
|
133
|
+
{description}
|
|
134
|
+
</Text>
|
|
135
|
+
)}
|
|
136
|
+
</Stack>
|
|
137
|
+
|
|
138
|
+
{action && (
|
|
139
|
+
<Button
|
|
140
|
+
variant="outline"
|
|
141
|
+
onClick={action.onClick}
|
|
142
|
+
component={
|
|
143
|
+
action.component as
|
|
144
|
+
| FunctionComponent<Record<string, unknown>>
|
|
145
|
+
| undefined
|
|
146
|
+
}
|
|
147
|
+
to={action.to}
|
|
148
|
+
rightSection={action.rightSection}
|
|
149
|
+
radius="lg"
|
|
150
|
+
size="sm"
|
|
151
|
+
mt="xs"
|
|
152
|
+
>
|
|
153
|
+
{action.label}
|
|
154
|
+
</Button>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{secondaryAction && (
|
|
158
|
+
<Button
|
|
159
|
+
variant="ghost"
|
|
160
|
+
color="dark"
|
|
161
|
+
onClick={secondaryAction.onClick}
|
|
162
|
+
component={
|
|
163
|
+
secondaryAction.component as
|
|
164
|
+
| FunctionComponent<Record<string, unknown>>
|
|
165
|
+
| undefined
|
|
166
|
+
}
|
|
167
|
+
to={secondaryAction.to}
|
|
168
|
+
rightSection={secondaryAction.rightSection}
|
|
169
|
+
radius="lg"
|
|
170
|
+
size="sm"
|
|
171
|
+
>
|
|
172
|
+
{secondaryAction.label}
|
|
173
|
+
</Button>
|
|
174
|
+
)}
|
|
175
|
+
</Stack>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (showCard) {
|
|
179
|
+
return (
|
|
180
|
+
<Card
|
|
181
|
+
p="xl"
|
|
182
|
+
bg={cardBg != null ? toCssVar(cardBg) : undefined}
|
|
183
|
+
bd="1px solid var(--color-general-border)"
|
|
184
|
+
radius="lg"
|
|
185
|
+
style={{ minHeight }}
|
|
186
|
+
>
|
|
187
|
+
{content}
|
|
188
|
+
</Card>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<Box py="2xl" maw={maxWidth} mx="auto" style={{ minHeight }}>
|
|
194
|
+
{content}
|
|
195
|
+
</Box>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Card, Flex, ThemeIcon } from '@mantine/core';
|
|
4
|
+
|
|
5
|
+
import { Text, Title } from './Typography';
|
|
6
|
+
|
|
7
|
+
export interface FeatureCardProps {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
icon: ReactNode;
|
|
11
|
+
color?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* FeatureCard component for showcasing features
|
|
16
|
+
* Displays an icon, title, and description
|
|
17
|
+
*/
|
|
18
|
+
export function FeatureCard({
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
icon,
|
|
22
|
+
color = 'blue',
|
|
23
|
+
}: FeatureCardProps) {
|
|
24
|
+
return (
|
|
25
|
+
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
|
26
|
+
<Flex direction="column" gap="md">
|
|
27
|
+
<ThemeIcon size="xl" radius="md" variant="light" color={color}>
|
|
28
|
+
{icon}
|
|
29
|
+
</ThemeIcon>
|
|
30
|
+
|
|
31
|
+
<Flex direction="column">
|
|
32
|
+
<Title variant="heading4" mb="xs">
|
|
33
|
+
{title}
|
|
34
|
+
</Title>
|
|
35
|
+
<Text variant="caption1" c="text.subdued.default">
|
|
36
|
+
{description}
|
|
37
|
+
</Text>
|
|
38
|
+
</Flex>
|
|
39
|
+
</Flex>
|
|
40
|
+
</Card>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
|
|
3
|
+
import { FilterMenu } from './FilterMenu';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* -- This file was auto-generated by Code Connect --
|
|
7
|
+
* None of your props could be automatically mapped to Figma properties.
|
|
8
|
+
* You should update the `props` object to include a mapping from your
|
|
9
|
+
* code props to Figma properties, and update the `example` function to
|
|
10
|
+
* return the code example you'd like to see in Figma
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
figma.connect(
|
|
14
|
+
FilterMenu,
|
|
15
|
+
'https://www.figma.com/design/VCLfybgU3OaUUPrQdBaVmP/LM-Design-System?node-id=2291%3A16391',
|
|
16
|
+
{
|
|
17
|
+
props: {
|
|
18
|
+
// No matching props could be found for these Figma properties:
|
|
19
|
+
// "component": figma.instance('Component'),
|
|
20
|
+
// "spacing": figma.enum('Spacing', {
|
|
21
|
+
// "None": "none",
|
|
22
|
+
// "2px": "2px",
|
|
23
|
+
// "8px": "8px",
|
|
24
|
+
// "16px": "16px",
|
|
25
|
+
// "24px": "24px"
|
|
26
|
+
// })
|
|
27
|
+
},
|
|
28
|
+
example: _props => <FilterMenu filters={[]} />,
|
|
29
|
+
}
|
|
30
|
+
);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ListFilter } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { Flex } from '@mantine/core';
|
|
6
|
+
|
|
7
|
+
import { Button } from '../Button';
|
|
8
|
+
import { Menu, type MenuProps } from '../Menu';
|
|
9
|
+
import { Text } from '../Typography';
|
|
10
|
+
import { defaultFilterSchemas } from './defaultFilterSchemas';
|
|
11
|
+
import { BooleanFilterSubmenu } from './FilterSubMenuTypes/BooleanFilterSubmenu';
|
|
12
|
+
import { SearchableFilterSubmenu } from './FilterSubMenuTypes/SearchableFilterSubmenu';
|
|
13
|
+
import { extractFilterValue, updateFilterCategories } from './helpers';
|
|
14
|
+
|
|
15
|
+
import type { FilterSchema, FilterValue } from './types';
|
|
16
|
+
import type { FilterCategory } from '../AppliedFiltersManagerBar/AppliedFiltersManagerBar';
|
|
17
|
+
|
|
18
|
+
export interface FilterItem {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FilterMenuProps {
|
|
24
|
+
/** Current filter categories */
|
|
25
|
+
filters: FilterCategory[];
|
|
26
|
+
/** Callback when filters change */
|
|
27
|
+
onFiltersChange?: (filters: FilterCategory[]) => void;
|
|
28
|
+
/** Filter schemas defining available filter types (defaults to defaultFilterSchemas) */
|
|
29
|
+
filterSchemas?: FilterSchema[];
|
|
30
|
+
/** Whether to show the filter button trigger */
|
|
31
|
+
showButton?: boolean;
|
|
32
|
+
/** Custom button content */
|
|
33
|
+
buttonContent?: React.ReactNode;
|
|
34
|
+
/** Additional Mantine Menu props */
|
|
35
|
+
menuProps?: Partial<MenuProps>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function FilterMenu({
|
|
39
|
+
filters,
|
|
40
|
+
onFiltersChange,
|
|
41
|
+
filterSchemas = defaultFilterSchemas,
|
|
42
|
+
showButton = true,
|
|
43
|
+
buttonContent,
|
|
44
|
+
menuProps,
|
|
45
|
+
}: FilterMenuProps) {
|
|
46
|
+
const [menuOpened, setMenuOpened] = useState(false);
|
|
47
|
+
|
|
48
|
+
// Extract filter values from FilterCategory[] structure using schemas
|
|
49
|
+
const filterValues = useMemo(() => {
|
|
50
|
+
const values: Record<string, FilterValue> = {};
|
|
51
|
+
filterSchemas.forEach(schema => {
|
|
52
|
+
values[schema.key] = extractFilterValue(schema, filters);
|
|
53
|
+
});
|
|
54
|
+
return values;
|
|
55
|
+
}, [filterSchemas, filters]);
|
|
56
|
+
|
|
57
|
+
// Helper to update FilterCategory[] structure using schemas
|
|
58
|
+
const updateFilters = useCallback(
|
|
59
|
+
(updates: Record<string, FilterValue>) => {
|
|
60
|
+
if (!onFiltersChange) return;
|
|
61
|
+
|
|
62
|
+
const newFilters = updateFilterCategories(
|
|
63
|
+
filterSchemas,
|
|
64
|
+
updates,
|
|
65
|
+
filters,
|
|
66
|
+
);
|
|
67
|
+
onFiltersChange(newFilters);
|
|
68
|
+
},
|
|
69
|
+
[filterSchemas, filters, onFiltersChange],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Create change handlers for each filter schema
|
|
73
|
+
const createChangeHandler = useCallback(
|
|
74
|
+
(schema: FilterSchema) => {
|
|
75
|
+
if (schema.type === 'boolean') {
|
|
76
|
+
return (checked: boolean) => {
|
|
77
|
+
updateFilters({ [schema.key]: checked });
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (schema.type === 'multi-select') {
|
|
82
|
+
return (selectedItems: FilterItem[]) => {
|
|
83
|
+
updateFilters({ [schema.key]: selectedItems });
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback (should never reach here with proper typing)
|
|
88
|
+
return () => {};
|
|
89
|
+
},
|
|
90
|
+
[updateFilters],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const defaultButtonContent = (
|
|
94
|
+
<Flex align="center" gap="xs">
|
|
95
|
+
<ListFilter size={16} />
|
|
96
|
+
<Text variant="caption1">Filter</Text>
|
|
97
|
+
</Flex>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
{showButton && (
|
|
103
|
+
<Menu
|
|
104
|
+
opened={menuOpened}
|
|
105
|
+
onChange={setMenuOpened}
|
|
106
|
+
position="bottom-start"
|
|
107
|
+
width={280}
|
|
108
|
+
{...menuProps}
|
|
109
|
+
>
|
|
110
|
+
<Menu.Target>
|
|
111
|
+
<Button variant="outline" size="sm" radius="lg">
|
|
112
|
+
{buttonContent || defaultButtonContent}
|
|
113
|
+
</Button>
|
|
114
|
+
</Menu.Target>
|
|
115
|
+
|
|
116
|
+
<Menu.Dropdown>
|
|
117
|
+
{filterSchemas.map((schema, index) => {
|
|
118
|
+
const isLast = index === filterSchemas.length - 1;
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
schema.type === 'boolean' &&
|
|
122
|
+
schema.submenuType === 'boolean'
|
|
123
|
+
) {
|
|
124
|
+
const checked = (filterValues[schema.key] as boolean) || false;
|
|
125
|
+
const onChange = createChangeHandler(schema) as (
|
|
126
|
+
checked: boolean,
|
|
127
|
+
) => void;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div key={schema.key}>
|
|
131
|
+
<BooleanFilterSubmenu
|
|
132
|
+
label={schema.label}
|
|
133
|
+
checked={checked}
|
|
134
|
+
onChange={onChange}
|
|
135
|
+
/>
|
|
136
|
+
{!isLast && <Menu.Divider />}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
schema.type === 'multi-select' &&
|
|
143
|
+
schema.submenuType === 'searchable'
|
|
144
|
+
) {
|
|
145
|
+
const selectedItems =
|
|
146
|
+
(filterValues[schema.key] as FilterItem[]) || [];
|
|
147
|
+
const onSelectionChange = createChangeHandler(schema) as (
|
|
148
|
+
selectedItems: FilterItem[],
|
|
149
|
+
) => void;
|
|
150
|
+
|
|
151
|
+
// Show "Clear All" option if showClearAll is true, or if custom onClearAll is provided
|
|
152
|
+
const handleClearAll =
|
|
153
|
+
schema.showClearAll || schema.onClearAll
|
|
154
|
+
? schema.onClearAll ||
|
|
155
|
+
(() => {
|
|
156
|
+
onSelectionChange([]);
|
|
157
|
+
})
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
160
|
+
const itemsController = schema.itemsController;
|
|
161
|
+
const isLoading = itemsController?.status === 'loading';
|
|
162
|
+
const isError = itemsController?.status === 'error';
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div key={schema.key}>
|
|
166
|
+
<SearchableFilterSubmenu
|
|
167
|
+
label={schema.label}
|
|
168
|
+
items={schema.items}
|
|
169
|
+
selectedItems={selectedItems}
|
|
170
|
+
onSelectionChange={onSelectionChange}
|
|
171
|
+
onClearAll={handleClearAll}
|
|
172
|
+
placeholder={schema.placeholder}
|
|
173
|
+
showSearch={schema.showSearch}
|
|
174
|
+
emptyMessage={schema.emptyMessage}
|
|
175
|
+
searchMode={schema.search?.mode ?? 'client'}
|
|
176
|
+
onSearchChange={schema.search?.onSearchChange}
|
|
177
|
+
searchDebounceMs={schema.search?.debounceMs}
|
|
178
|
+
isLoading={isLoading}
|
|
179
|
+
isError={isError}
|
|
180
|
+
errorMessage={itemsController?.errorMessage}
|
|
181
|
+
onRetry={itemsController?.retry}
|
|
182
|
+
hasMore={itemsController?.hasMore}
|
|
183
|
+
onLoadMore={itemsController?.loadMore}
|
|
184
|
+
isLoadingMore={itemsController?.isLoadingMore}
|
|
185
|
+
/>
|
|
186
|
+
{!isLast && <Menu.Divider />}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
})}
|
|
193
|
+
</Menu.Dropdown>
|
|
194
|
+
</Menu>
|
|
195
|
+
)}
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Checkbox } from '@mantine/core';
|
|
2
|
+
|
|
3
|
+
import { Menu } from '../../Menu';
|
|
4
|
+
|
|
5
|
+
export interface BooleanFilterSubmenuProps {
|
|
6
|
+
/** Label for the submenu (e.g., "Favourites") */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Whether the filter is checked */
|
|
9
|
+
checked: boolean;
|
|
10
|
+
/** Callback when the checkbox toggle changes */
|
|
11
|
+
onChange: (checked: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Boolean filter sub-menu: renders a checkbox whose label and input both
|
|
16
|
+
* toggle the value. The containing `Menu.Item` opts out of the default
|
|
17
|
+
* close-on-click so the user can see the checkbox flip state and then
|
|
18
|
+
* dismiss the menu explicitly (click outside or keyboard escape).
|
|
19
|
+
*
|
|
20
|
+
* All toggle events flow through the `Checkbox`'s native `onChange` — we
|
|
21
|
+
* don't also handle the surrounding `Menu.Item` click, because clicking the
|
|
22
|
+
* label would otherwise double-fire and net-cancel the toggle.
|
|
23
|
+
*/
|
|
24
|
+
export function BooleanFilterSubmenu({
|
|
25
|
+
label,
|
|
26
|
+
checked,
|
|
27
|
+
onChange,
|
|
28
|
+
}: BooleanFilterSubmenuProps) {
|
|
29
|
+
return (
|
|
30
|
+
<Menu.Sub>
|
|
31
|
+
<Menu.Sub.Target>
|
|
32
|
+
<Menu.Sub.Item>{label}</Menu.Sub.Item>
|
|
33
|
+
</Menu.Sub.Target>
|
|
34
|
+
<Menu.Sub.Dropdown>
|
|
35
|
+
<Menu.Item closeMenuOnClick={false}>
|
|
36
|
+
<Checkbox
|
|
37
|
+
checked={checked}
|
|
38
|
+
onChange={event => onChange(event.currentTarget.checked)}
|
|
39
|
+
size="sm"
|
|
40
|
+
label={label}
|
|
41
|
+
/>
|
|
42
|
+
</Menu.Item>
|
|
43
|
+
</Menu.Sub.Dropdown>
|
|
44
|
+
</Menu.Sub>
|
|
45
|
+
);
|
|
46
|
+
}
|