@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,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { ChevronDown, Search } from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
import { Combobox, useCombobox } from '@mantine/core';
|
|
13
|
+
|
|
14
|
+
import { useDebouncedValue } from '@scalepad/ui-utils/hooks';
|
|
15
|
+
|
|
16
|
+
import { Avatar, Loader } from '../DesignSystemPrimitives';
|
|
17
|
+
import { Text } from '../Typography';
|
|
18
|
+
import * as styles from './LookupSelect.css';
|
|
19
|
+
|
|
20
|
+
import type { BackgroundColor } from '../../tokens/color-types';
|
|
21
|
+
import type { SearchableListSearchMode } from '../SearchableList/types';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Semantic background tokens usable as avatar fill colors. Cycled through to give each
|
|
25
|
+
* option a stable, differentiated accent derived from its label.
|
|
26
|
+
*/
|
|
27
|
+
const AVATAR_PALETTE: readonly BackgroundColor[] = [
|
|
28
|
+
'background.primary.filled',
|
|
29
|
+
'background.information.filled',
|
|
30
|
+
'background.success.filled',
|
|
31
|
+
'background.warning.filled',
|
|
32
|
+
'background.danger.filled',
|
|
33
|
+
'background.subdued.filled',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function hashString(value: string): number {
|
|
37
|
+
let hash = 0;
|
|
38
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
39
|
+
hash = (hash * 31 + value.charCodeAt(index)) | 0;
|
|
40
|
+
}
|
|
41
|
+
return Math.abs(hash);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getAvatarColor(label: string): BackgroundColor {
|
|
45
|
+
return AVATAR_PALETTE[hashString(label) % AVATAR_PALETTE.length]!;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getInitial(label: string): string {
|
|
49
|
+
const trimmed = label.trim();
|
|
50
|
+
return trimmed.length > 0 ? trimmed[0]!.toUpperCase() : '?';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LookupSelectOption {
|
|
54
|
+
value: string;
|
|
55
|
+
label: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LookupSelectProps {
|
|
60
|
+
/** Options to display. When `searchMode` is `server`, this is expected to be pre-filtered. */
|
|
61
|
+
data: LookupSelectOption[];
|
|
62
|
+
/** Current selected value. */
|
|
63
|
+
value?: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Callback fired when the selected value changes. The second argument is the full option
|
|
66
|
+
* that was selected, which lets consumers persist fields (like `label`) that may not be
|
|
67
|
+
* present in `data` after a subsequent server-side search.
|
|
68
|
+
*/
|
|
69
|
+
onChange?: (value: string | null, option: LookupSelectOption | null) => void;
|
|
70
|
+
/** Trigger placeholder shown when no option is selected. */
|
|
71
|
+
placeholder?: string;
|
|
72
|
+
/** Placeholder shown in the dropdown search input. */
|
|
73
|
+
searchPlaceholder?: string;
|
|
74
|
+
/** Disables the trigger and prevents opening the dropdown. */
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* `client` filters `data` locally by the search text. `server` treats `data` as already-filtered
|
|
78
|
+
* and invokes `onSearchChange` with a debounced query whenever the user types.
|
|
79
|
+
*
|
|
80
|
+
* @default 'client'
|
|
81
|
+
*/
|
|
82
|
+
searchMode?: SearchableListSearchMode;
|
|
83
|
+
/** Called with the trimmed, debounced search query. Primarily for `searchMode="server"`. */
|
|
84
|
+
onSearchChange?: (query: string) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Debounce delay (ms) applied to `onSearchChange`.
|
|
87
|
+
*
|
|
88
|
+
* @default 250
|
|
89
|
+
*/
|
|
90
|
+
searchDebounceMs?: number;
|
|
91
|
+
/** Shows a spinner next to the search input when true. */
|
|
92
|
+
isLoading?: boolean;
|
|
93
|
+
/** Message shown when there are no options to display. */
|
|
94
|
+
nothingFoundMessage?: string;
|
|
95
|
+
/** Persistent message rendered at the bottom of the dropdown (e.g. validation hint). */
|
|
96
|
+
footerMessage?: string;
|
|
97
|
+
/** Width of the trigger. */
|
|
98
|
+
w?: string | number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Searchable lookup picker that displays a colored avatar for each option and renders a
|
|
103
|
+
* search-driven dropdown panel separate from the trigger.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* <LookupSelect
|
|
108
|
+
* data={clients.map((c) => ({ value: c.id, label: c.name }))}
|
|
109
|
+
* value={clientId}
|
|
110
|
+
* onChange={setClientId}
|
|
111
|
+
* searchMode="server"
|
|
112
|
+
* onSearchChange={setSearch}
|
|
113
|
+
* isLoading={query.isFetching}
|
|
114
|
+
* footerMessage="Client is required to save an initiative"
|
|
115
|
+
* />
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export const LookupSelect = forwardRef<HTMLButtonElement, LookupSelectProps>(
|
|
119
|
+
(
|
|
120
|
+
{
|
|
121
|
+
data,
|
|
122
|
+
value,
|
|
123
|
+
onChange,
|
|
124
|
+
placeholder = 'Select',
|
|
125
|
+
searchPlaceholder = 'Search',
|
|
126
|
+
disabled = false,
|
|
127
|
+
searchMode = 'client',
|
|
128
|
+
onSearchChange,
|
|
129
|
+
searchDebounceMs = 250,
|
|
130
|
+
isLoading = false,
|
|
131
|
+
nothingFoundMessage,
|
|
132
|
+
footerMessage,
|
|
133
|
+
w,
|
|
134
|
+
},
|
|
135
|
+
ref,
|
|
136
|
+
) => {
|
|
137
|
+
const [search, setSearch] = useState('');
|
|
138
|
+
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
|
139
|
+
|
|
140
|
+
const combobox = useCombobox({
|
|
141
|
+
onDropdownClose: () => {
|
|
142
|
+
combobox.resetSelectedOption();
|
|
143
|
+
setSearch('');
|
|
144
|
+
},
|
|
145
|
+
onDropdownOpen: () => {
|
|
146
|
+
combobox.updateSelectedOptionIndex('active');
|
|
147
|
+
window.setTimeout(() => searchInputRef.current?.focus(), 0);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Cache the last option whose value matched so the trigger keeps showing the selection
|
|
152
|
+
// even when the consumer replaces `data` (e.g. server-side search results that filter
|
|
153
|
+
// the selected option out of the visible list).
|
|
154
|
+
const selectedOptionRef = useRef<LookupSelectOption | null>(null);
|
|
155
|
+
const matchedOption = data.find(option => option.value === value) ?? null;
|
|
156
|
+
if (matchedOption) {
|
|
157
|
+
selectedOptionRef.current = matchedOption;
|
|
158
|
+
} else if (value == null) {
|
|
159
|
+
selectedOptionRef.current = null;
|
|
160
|
+
}
|
|
161
|
+
const selectedOption = matchedOption ?? selectedOptionRef.current;
|
|
162
|
+
|
|
163
|
+
const filteredData = useMemo(() => {
|
|
164
|
+
if (searchMode === 'server') return data;
|
|
165
|
+
const query = search.trim().toLowerCase();
|
|
166
|
+
if (!query) return data;
|
|
167
|
+
return data.filter(item => item.label.toLowerCase().includes(query));
|
|
168
|
+
}, [data, search, searchMode]);
|
|
169
|
+
|
|
170
|
+
const [debouncedSearch] = useDebouncedValue(search, searchDebounceMs);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (searchMode !== 'server') return;
|
|
173
|
+
onSearchChange?.(debouncedSearch.trim());
|
|
174
|
+
}, [debouncedSearch, onSearchChange, searchMode]);
|
|
175
|
+
|
|
176
|
+
const handleOptionSubmit = (submittedValue: string) => {
|
|
177
|
+
const submittedOption =
|
|
178
|
+
data.find(option => option.value === submittedValue) ?? null;
|
|
179
|
+
onChange?.(submittedValue, submittedOption);
|
|
180
|
+
combobox.closeDropdown();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
184
|
+
if (event.nativeEvent.isComposing) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
if (event.key === 'ArrowDown') {
|
|
190
|
+
combobox.selectNextOption();
|
|
191
|
+
} else {
|
|
192
|
+
combobox.selectPreviousOption();
|
|
193
|
+
}
|
|
194
|
+
} else if (event.key === 'Enter') {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
combobox.clickSelectedOption();
|
|
197
|
+
} else if (event.key === 'Escape') {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
combobox.closeDropdown();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const triggerStyle = w
|
|
204
|
+
? { width: typeof w === 'number' ? `${w}px` : w }
|
|
205
|
+
: undefined;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Combobox store={combobox} onOptionSubmit={handleOptionSubmit}>
|
|
209
|
+
<Combobox.Target>
|
|
210
|
+
<button
|
|
211
|
+
ref={ref}
|
|
212
|
+
type="button"
|
|
213
|
+
className={styles.trigger}
|
|
214
|
+
onClick={() => {
|
|
215
|
+
if (!disabled) combobox.toggleDropdown();
|
|
216
|
+
}}
|
|
217
|
+
disabled={disabled}
|
|
218
|
+
aria-haspopup="listbox"
|
|
219
|
+
aria-expanded={combobox.dropdownOpened}
|
|
220
|
+
style={triggerStyle}
|
|
221
|
+
>
|
|
222
|
+
<span className={styles.triggerValue}>
|
|
223
|
+
{selectedOption ? (
|
|
224
|
+
<>
|
|
225
|
+
<Avatar
|
|
226
|
+
size="sm"
|
|
227
|
+
radius="sm"
|
|
228
|
+
color={getAvatarColor(selectedOption.label)}
|
|
229
|
+
>
|
|
230
|
+
{getInitial(selectedOption.label)}
|
|
231
|
+
</Avatar>
|
|
232
|
+
<Text
|
|
233
|
+
variant="body1.strong"
|
|
234
|
+
c="text.default"
|
|
235
|
+
className={styles.triggerLabel}
|
|
236
|
+
>
|
|
237
|
+
{selectedOption.label}
|
|
238
|
+
</Text>
|
|
239
|
+
</>
|
|
240
|
+
) : (
|
|
241
|
+
<Text
|
|
242
|
+
variant="body1"
|
|
243
|
+
c="text.subdued.default"
|
|
244
|
+
className={styles.triggerLabel}
|
|
245
|
+
>
|
|
246
|
+
{placeholder}
|
|
247
|
+
</Text>
|
|
248
|
+
)}
|
|
249
|
+
</span>
|
|
250
|
+
<ChevronDown className={styles.chevron} aria-hidden />
|
|
251
|
+
</button>
|
|
252
|
+
</Combobox.Target>
|
|
253
|
+
|
|
254
|
+
<Combobox.Dropdown p={0}>
|
|
255
|
+
<div className={styles.searchRow}>
|
|
256
|
+
<Search className={styles.searchIcon} aria-hidden />
|
|
257
|
+
<input
|
|
258
|
+
ref={searchInputRef}
|
|
259
|
+
type="text"
|
|
260
|
+
className={styles.searchInput}
|
|
261
|
+
value={search}
|
|
262
|
+
onChange={event => setSearch(event.currentTarget.value)}
|
|
263
|
+
onKeyDown={handleSearchKeyDown}
|
|
264
|
+
placeholder={searchPlaceholder}
|
|
265
|
+
aria-label={searchPlaceholder}
|
|
266
|
+
autoComplete="off"
|
|
267
|
+
/>
|
|
268
|
+
{isLoading && <Loader size="xs" />}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<Combobox.Options className={styles.options}>
|
|
272
|
+
{filteredData.map(item => (
|
|
273
|
+
<Combobox.Option
|
|
274
|
+
value={item.value}
|
|
275
|
+
key={item.value}
|
|
276
|
+
className={styles.optionRoot}
|
|
277
|
+
>
|
|
278
|
+
<div className={styles.option}>
|
|
279
|
+
<Avatar
|
|
280
|
+
size="sm"
|
|
281
|
+
radius="sm"
|
|
282
|
+
color={getAvatarColor(item.label)}
|
|
283
|
+
>
|
|
284
|
+
{getInitial(item.label)}
|
|
285
|
+
</Avatar>
|
|
286
|
+
<div className={styles.optionText}>
|
|
287
|
+
<Text
|
|
288
|
+
variant="body1.strong"
|
|
289
|
+
c="text.default"
|
|
290
|
+
className={styles.optionLabel}
|
|
291
|
+
>
|
|
292
|
+
{item.label}
|
|
293
|
+
</Text>
|
|
294
|
+
{item.description && (
|
|
295
|
+
<Text
|
|
296
|
+
variant="caption2"
|
|
297
|
+
c="text.subdued.default"
|
|
298
|
+
className={styles.optionLabel}
|
|
299
|
+
>
|
|
300
|
+
{item.description}
|
|
301
|
+
</Text>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</Combobox.Option>
|
|
306
|
+
))}
|
|
307
|
+
{filteredData.length === 0 && !isLoading && nothingFoundMessage && (
|
|
308
|
+
<Combobox.Empty>{nothingFoundMessage}</Combobox.Empty>
|
|
309
|
+
)}
|
|
310
|
+
</Combobox.Options>
|
|
311
|
+
|
|
312
|
+
{footerMessage && (
|
|
313
|
+
<div className={styles.footer}>
|
|
314
|
+
<Text variant="caption2" c="text.subdued.default">
|
|
315
|
+
{footerMessage}
|
|
316
|
+
</Text>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</Combobox.Dropdown>
|
|
320
|
+
</Combobox>
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
LookupSelect.displayName = 'LookupSelect';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu component styles – vanilla-extract with semantic design tokens.
|
|
3
|
+
* Matches Figma design system: Menu, Menu.Item, Menu.Label, submenu triggers (itemSection), divider.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { style } from '@vanilla-extract/css';
|
|
7
|
+
|
|
8
|
+
import { mantineVars } from '../../theme/mantineVars';
|
|
9
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
10
|
+
|
|
11
|
+
/** Dropdown container (root and Menu.Sub.Dropdown) */
|
|
12
|
+
export const dropdown = style({
|
|
13
|
+
backgroundColor: tokens.color.background.default,
|
|
14
|
+
border: `1px solid ${tokens.color.stroke.default}`,
|
|
15
|
+
borderRadius: tokens.radius.lg,
|
|
16
|
+
boxShadow: tokens.shadow.md,
|
|
17
|
+
padding: tokens.spacing['2xs'],
|
|
18
|
+
gap: 0,
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Menu item and submenu trigger (Menu.Item, Menu.Sub.Item) */
|
|
24
|
+
export const item = style({
|
|
25
|
+
borderRadius: tokens.radius.md,
|
|
26
|
+
minHeight: 32,
|
|
27
|
+
padding: `${tokens.spacing.xs} ${tokens.spacing.xs}`,
|
|
28
|
+
fontSize: 14,
|
|
29
|
+
lineHeight: 1,
|
|
30
|
+
color: tokens.color.text.default,
|
|
31
|
+
transition: 'background-color 150ms ease',
|
|
32
|
+
selectors: {
|
|
33
|
+
'&:hover:not([data-disabled])': {
|
|
34
|
+
backgroundColor: tokens.color.background.primaryLight,
|
|
35
|
+
},
|
|
36
|
+
'&[data-disabled]': {
|
|
37
|
+
color: tokens.color.text.disabledDefault,
|
|
38
|
+
cursor: 'not-allowed',
|
|
39
|
+
},
|
|
40
|
+
[`${mantineVars.darkSelector} &`]: {
|
|
41
|
+
color: tokens.color.text.default,
|
|
42
|
+
},
|
|
43
|
+
[`${mantineVars.darkSelector} &:hover:not([data-disabled])`]: {
|
|
44
|
+
backgroundColor: tokens.color.background.primaryLight,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Applied when Menu.Item receives a theme-token color prop.
|
|
51
|
+
* Reads --lm-menu-item-color (set inline by MenuItemWrapper) so design-system
|
|
52
|
+
* item colors do not conflict with Mantine's internal menu variables.
|
|
53
|
+
*/
|
|
54
|
+
export const itemWithColor = style({
|
|
55
|
+
color: 'var(--lm-menu-item-color)',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/** Group label (Menu.Label, optgroup-style) – Caption 2 */
|
|
59
|
+
export const label = style({
|
|
60
|
+
fontSize: 12,
|
|
61
|
+
lineHeight: 1,
|
|
62
|
+
color: tokens.color.text.subduedDefault,
|
|
63
|
+
padding: `5.5px ${tokens.spacing.xs}`,
|
|
64
|
+
minHeight: 32,
|
|
65
|
+
fontWeight: 400,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** Visible text label inside Menu.Item. Inherit row color so semantic item variants color the text. */
|
|
69
|
+
export const itemLabel = style({
|
|
70
|
+
color: 'inherit',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Left/right sections of Menu.Item (e.g. submenu chevron). Use inherit so item token color flows through. */
|
|
74
|
+
export const itemSection = style({
|
|
75
|
+
color: 'inherit',
|
|
76
|
+
flexShrink: 0,
|
|
77
|
+
width: 20,
|
|
78
|
+
height: 20,
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
justifyContent: 'center',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Divider between groups */
|
|
85
|
+
export const divider = style({
|
|
86
|
+
borderTop: `1px solid ${tokens.color.stroke.subduedDefault}`,
|
|
87
|
+
marginTop: 0,
|
|
88
|
+
marginBottom: 0,
|
|
89
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type ComponentPropsWithoutRef,
|
|
4
|
+
type CSSProperties,
|
|
5
|
+
} from 'react';
|
|
6
|
+
|
|
7
|
+
import { clsx } from 'clsx';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Menu as MantineMenu,
|
|
11
|
+
type MenuItemProps as MantineMenuItemProps,
|
|
12
|
+
} from '@mantine/core';
|
|
13
|
+
|
|
14
|
+
import * as classes from './Menu.css';
|
|
15
|
+
import { resolveColorToken } from '../../utils/color-props';
|
|
16
|
+
import { withStaticComponents } from '../../utils/withStaticComponents';
|
|
17
|
+
|
|
18
|
+
type MantineMenuProps = ComponentPropsWithoutRef<typeof MantineMenu>;
|
|
19
|
+
type BaseMenuItemProps = ComponentPropsWithoutRef<'button'> &
|
|
20
|
+
Omit<MantineMenuItemProps, 'color' | 'c' | 'onClick'>;
|
|
21
|
+
|
|
22
|
+
export type MenuItemVariant = 'destructive';
|
|
23
|
+
|
|
24
|
+
export interface MenuItemProps extends BaseMenuItemProps {
|
|
25
|
+
/** Semantic menu row styling. */
|
|
26
|
+
variant?: MenuItemVariant;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveMenuItemColor(variant?: MenuItemVariant) {
|
|
30
|
+
if (variant === 'destructive') {
|
|
31
|
+
return resolveColorToken('text.danger.default');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MenuItemWrapper = forwardRef<HTMLButtonElement, MenuItemProps>(
|
|
38
|
+
({ variant, style: styleProp, className, ...rest }, ref) => {
|
|
39
|
+
const resolvedColor = resolveMenuItemColor(variant);
|
|
40
|
+
const baseStyle: CSSProperties =
|
|
41
|
+
styleProp != null &&
|
|
42
|
+
typeof styleProp === 'object' &&
|
|
43
|
+
!Array.isArray(styleProp)
|
|
44
|
+
? { ...styleProp }
|
|
45
|
+
: {};
|
|
46
|
+
|
|
47
|
+
const resolvedStyle: CSSProperties =
|
|
48
|
+
resolvedColor != null
|
|
49
|
+
? { ...baseStyle, ['--lm-menu-item-color' as string]: resolvedColor }
|
|
50
|
+
: baseStyle;
|
|
51
|
+
|
|
52
|
+
const resolvedClassName =
|
|
53
|
+
resolvedColor != null
|
|
54
|
+
? clsx(classes.itemWithColor, className)
|
|
55
|
+
: className;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<MantineMenu.Item
|
|
59
|
+
ref={ref}
|
|
60
|
+
style={resolvedStyle}
|
|
61
|
+
className={resolvedClassName}
|
|
62
|
+
data-variant={variant}
|
|
63
|
+
{...rest}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
MenuItemWrapper.displayName = 'MenuItem';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Design-system Menu: Mantine Menu with vanilla-extract styles from the theme contract.
|
|
72
|
+
* Preserves full API: Menu.Target, Menu.Dropdown, Menu.Item, Menu.Label, Menu.Divider, Menu.Sub, etc.
|
|
73
|
+
* Menu.Item exposes only safe semantic variants instead of raw color props.
|
|
74
|
+
*/
|
|
75
|
+
function MenuWrapper(props: MantineMenuProps) {
|
|
76
|
+
return (
|
|
77
|
+
<MantineMenu
|
|
78
|
+
classNames={{
|
|
79
|
+
dropdown: classes.dropdown,
|
|
80
|
+
item: classes.item,
|
|
81
|
+
label: classes.label,
|
|
82
|
+
itemLabel: classes.itemLabel,
|
|
83
|
+
itemSection: classes.itemSection,
|
|
84
|
+
divider: classes.divider,
|
|
85
|
+
}}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type MenuComponent = typeof MenuWrapper &
|
|
92
|
+
Omit<typeof MantineMenu, 'Item'> & {
|
|
93
|
+
Item: typeof MenuItemWrapper;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const menuStaticComponents: Omit<typeof MantineMenu, 'Item'> & {
|
|
97
|
+
Item: typeof MenuItemWrapper;
|
|
98
|
+
} = { ...MantineMenu, Item: MenuItemWrapper };
|
|
99
|
+
|
|
100
|
+
export const Menu: MenuComponent = withStaticComponents(
|
|
101
|
+
MenuWrapper,
|
|
102
|
+
menuStaticComponents,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export type { MenuProps } from '@mantine/core';
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { AlertCircle, AlertTriangle, CheckCircle, Info } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { Flex, Stack } from '@mantine/core';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
resolveColorToken,
|
|
9
|
+
type BackgroundColor,
|
|
10
|
+
type IconColor,
|
|
11
|
+
type TextColor,
|
|
12
|
+
} from '../../utils/color-props';
|
|
13
|
+
import { Button } from '../Button';
|
|
14
|
+
import { Text } from '../Typography';
|
|
15
|
+
|
|
16
|
+
export interface MessageBoxProps {
|
|
17
|
+
/**
|
|
18
|
+
* Visual variant of the message box
|
|
19
|
+
* @default 'default'
|
|
20
|
+
*/
|
|
21
|
+
variant?: 'default' | 'danger' | 'success' | 'warning' | 'info';
|
|
22
|
+
/**
|
|
23
|
+
* Action button label (if provided, renders a button)
|
|
24
|
+
*/
|
|
25
|
+
actionButton?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Action button variant
|
|
28
|
+
*/
|
|
29
|
+
actionButtonVariant?:
|
|
30
|
+
| 'primary'
|
|
31
|
+
| 'secondary'
|
|
32
|
+
| 'outline'
|
|
33
|
+
| 'ghost'
|
|
34
|
+
| 'ghost-muted'
|
|
35
|
+
| 'destructive';
|
|
36
|
+
/**
|
|
37
|
+
* Whether the action button is in loading state
|
|
38
|
+
*/
|
|
39
|
+
actionLoading?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to show the icon
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
showIcon?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Main content of the message box
|
|
47
|
+
*/
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Callback when action button is clicked
|
|
51
|
+
*/
|
|
52
|
+
onActionClick?: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface VariantStyles {
|
|
56
|
+
backgroundColor: BackgroundColor;
|
|
57
|
+
iconColor: IconColor;
|
|
58
|
+
textColor: TextColor;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveCssColor(token: BackgroundColor | IconColor | TextColor) {
|
|
62
|
+
return resolveColorToken(token) as string | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* MessageBox component
|
|
67
|
+
* Displays a message with optional icon and action button, with color-coded variants
|
|
68
|
+
*/
|
|
69
|
+
export function MessageBox({
|
|
70
|
+
variant = 'default',
|
|
71
|
+
actionButton,
|
|
72
|
+
actionButtonVariant,
|
|
73
|
+
actionLoading = false,
|
|
74
|
+
showIcon = true,
|
|
75
|
+
children,
|
|
76
|
+
onActionClick,
|
|
77
|
+
}: MessageBoxProps) {
|
|
78
|
+
// Get variant-specific styles
|
|
79
|
+
const styles = useMemo((): VariantStyles => {
|
|
80
|
+
switch (variant) {
|
|
81
|
+
case 'danger':
|
|
82
|
+
return {
|
|
83
|
+
backgroundColor: 'background.danger.light',
|
|
84
|
+
iconColor: 'icon.danger.default',
|
|
85
|
+
textColor: 'text.danger.default',
|
|
86
|
+
};
|
|
87
|
+
case 'success':
|
|
88
|
+
return {
|
|
89
|
+
backgroundColor: 'background.success.light',
|
|
90
|
+
iconColor: 'icon.success.default',
|
|
91
|
+
textColor: 'text.success.default',
|
|
92
|
+
};
|
|
93
|
+
case 'warning':
|
|
94
|
+
return {
|
|
95
|
+
backgroundColor: 'background.warning.light',
|
|
96
|
+
iconColor: 'icon.warning.default',
|
|
97
|
+
textColor: 'text.warning.default',
|
|
98
|
+
};
|
|
99
|
+
case 'info':
|
|
100
|
+
return {
|
|
101
|
+
backgroundColor: 'background.information.light',
|
|
102
|
+
iconColor: 'icon.information.default',
|
|
103
|
+
textColor: 'text.information.default',
|
|
104
|
+
};
|
|
105
|
+
case 'default':
|
|
106
|
+
default:
|
|
107
|
+
return {
|
|
108
|
+
backgroundColor: 'background.subdued.light',
|
|
109
|
+
iconColor: 'icon.default',
|
|
110
|
+
textColor: 'text.subdued.default',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}, [variant]);
|
|
114
|
+
|
|
115
|
+
// Get icon for variant
|
|
116
|
+
const icon = useMemo(() => {
|
|
117
|
+
if (!showIcon) return null;
|
|
118
|
+
|
|
119
|
+
const iconSize = 16;
|
|
120
|
+
const iconColor = resolveCssColor(styles.iconColor);
|
|
121
|
+
|
|
122
|
+
switch (variant) {
|
|
123
|
+
case 'danger':
|
|
124
|
+
return <AlertCircle size={iconSize} color={iconColor} />;
|
|
125
|
+
case 'success':
|
|
126
|
+
return <CheckCircle size={iconSize} color={iconColor} />;
|
|
127
|
+
case 'warning':
|
|
128
|
+
return <AlertTriangle size={iconSize} color={iconColor} />;
|
|
129
|
+
case 'info':
|
|
130
|
+
return <Info size={iconSize} color={iconColor} />;
|
|
131
|
+
case 'default':
|
|
132
|
+
default:
|
|
133
|
+
return <Info size={iconSize} color={iconColor} />;
|
|
134
|
+
}
|
|
135
|
+
}, [variant, showIcon, styles.iconColor]);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Flex
|
|
139
|
+
direction="column"
|
|
140
|
+
gap="md"
|
|
141
|
+
p="md"
|
|
142
|
+
style={{
|
|
143
|
+
backgroundColor: resolveCssColor(styles.backgroundColor),
|
|
144
|
+
borderRadius: 'var(--radius-md)',
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<Flex direction="row" gap="xs" align="flex-start">
|
|
148
|
+
{icon && <Flex flex="none">{icon}</Flex>}
|
|
149
|
+
<Stack justify="start">
|
|
150
|
+
<Text variant="caption1.stronger" c={styles.textColor}>
|
|
151
|
+
{children}
|
|
152
|
+
</Text>
|
|
153
|
+
{actionButton && (
|
|
154
|
+
<Button
|
|
155
|
+
variant={actionButtonVariant}
|
|
156
|
+
size="sm"
|
|
157
|
+
w="fit-content"
|
|
158
|
+
onClick={onActionClick}
|
|
159
|
+
loading={actionLoading}
|
|
160
|
+
>
|
|
161
|
+
{actionButton}
|
|
162
|
+
</Button>
|
|
163
|
+
)}
|
|
164
|
+
</Stack>
|
|
165
|
+
</Flex>
|
|
166
|
+
</Flex>
|
|
167
|
+
);
|
|
168
|
+
}
|