@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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EditableText Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a value as plain text; switches to an input when clicked (or
|
|
5
|
+
* focused via keyboard) and commits the new value on blur or Enter.
|
|
6
|
+
* Escape cancels and reverts to the original value.
|
|
7
|
+
*
|
|
8
|
+
* Works in both controlled and uncontrolled modes:
|
|
9
|
+
* - Controlled: pass `value` + `onSave`. Parent owns the value; the
|
|
10
|
+
* component only reports commits.
|
|
11
|
+
* - Uncontrolled: pass `defaultValue` + `onSave`. The component owns
|
|
12
|
+
* the current value internally.
|
|
13
|
+
*
|
|
14
|
+
* `onSave` may be async. While a save is in flight the input is disabled
|
|
15
|
+
* and shows a saving state; if the promise rejects, edit mode stays open
|
|
16
|
+
* and the error is shown so the user can retry or cancel.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Uncontrolled
|
|
20
|
+
* <EditableText defaultValue="Untitled" onSave={(v) => console.log(v)} />
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Controlled, async
|
|
24
|
+
* <EditableText
|
|
25
|
+
* value={name}
|
|
26
|
+
* onSave={async (v) => { await api.rename(v); setName(v); }}
|
|
27
|
+
* placeholder="Add a name"
|
|
28
|
+
* />
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
forwardRef,
|
|
33
|
+
useCallback,
|
|
34
|
+
useEffect,
|
|
35
|
+
useImperativeHandle,
|
|
36
|
+
useRef,
|
|
37
|
+
useState,
|
|
38
|
+
type KeyboardEvent,
|
|
39
|
+
type ReactNode,
|
|
40
|
+
} from 'react';
|
|
41
|
+
|
|
42
|
+
import { Pencil } from 'lucide-react';
|
|
43
|
+
|
|
44
|
+
import { TextInput } from '../TextInput';
|
|
45
|
+
import { Text } from '../Typography';
|
|
46
|
+
|
|
47
|
+
export interface EditableTextProps {
|
|
48
|
+
/** Controlled value. If provided, the parent owns the value. */
|
|
49
|
+
value?: string;
|
|
50
|
+
/** Initial value for uncontrolled use. Ignored if `value` is set. */
|
|
51
|
+
defaultValue?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Called when the user commits an edit (blur or Enter).
|
|
54
|
+
* May return a Promise — while pending the input shows a saving state.
|
|
55
|
+
* If it rejects, edit mode stays open and the error is displayed.
|
|
56
|
+
*/
|
|
57
|
+
onSave: (nextValue: string) => void | Promise<void>;
|
|
58
|
+
/** Text shown (in a muted style) when the current value is empty. */
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
/** Input placeholder when in edit mode. Defaults to `placeholder`. */
|
|
61
|
+
inputPlaceholder?: string;
|
|
62
|
+
/** Accessible label; required for screen readers if there's no visible label. */
|
|
63
|
+
ariaLabel?: string;
|
|
64
|
+
/** When true, the field cannot enter edit mode and the pencil is hidden. */
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Optional validator run before `onSave`.
|
|
68
|
+
* Return `null` for valid, or an error message string to block the commit.
|
|
69
|
+
*/
|
|
70
|
+
validate?: (value: string) => string | null;
|
|
71
|
+
/**
|
|
72
|
+
* When true, a commit is blocked and an inline error is shown if the
|
|
73
|
+
* trimmed input value is empty.
|
|
74
|
+
*/
|
|
75
|
+
required?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Error message shown when `required` fails.
|
|
78
|
+
* Defaults to "This field is required".
|
|
79
|
+
*/
|
|
80
|
+
requiredMessage?: string;
|
|
81
|
+
/** Trim whitespace before validating/committing. Defaults to true. */
|
|
82
|
+
trim?: boolean;
|
|
83
|
+
/** Maximum characters. Passed through to the input. */
|
|
84
|
+
maxLength?: number;
|
|
85
|
+
/** Custom render for the read-only display. Receives the current value. */
|
|
86
|
+
renderDisplay?: (value: string, isEmpty: boolean) => ReactNode;
|
|
87
|
+
/** Called whenever edit mode starts. */
|
|
88
|
+
onEditStart?: () => void;
|
|
89
|
+
/** Called whenever edit mode ends (commit or cancel). */
|
|
90
|
+
onEditEnd?: () => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface EditableTextHandle {
|
|
94
|
+
/** Programmatically enter edit mode. */
|
|
95
|
+
startEditing: () => void;
|
|
96
|
+
/** Commit the current draft (same as blur/Enter). */
|
|
97
|
+
commit: () => void;
|
|
98
|
+
/** Cancel editing and revert (same as Escape). */
|
|
99
|
+
cancel: () => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const EditableText = forwardRef<EditableTextHandle, EditableTextProps>(
|
|
103
|
+
(
|
|
104
|
+
{
|
|
105
|
+
value,
|
|
106
|
+
defaultValue = '',
|
|
107
|
+
onSave,
|
|
108
|
+
placeholder = 'Click to edit',
|
|
109
|
+
inputPlaceholder,
|
|
110
|
+
ariaLabel,
|
|
111
|
+
disabled = false,
|
|
112
|
+
validate,
|
|
113
|
+
required = false,
|
|
114
|
+
requiredMessage,
|
|
115
|
+
trim = true,
|
|
116
|
+
maxLength,
|
|
117
|
+
renderDisplay,
|
|
118
|
+
onEditStart,
|
|
119
|
+
onEditEnd,
|
|
120
|
+
},
|
|
121
|
+
ref,
|
|
122
|
+
) => {
|
|
123
|
+
const isControlled = value !== undefined;
|
|
124
|
+
|
|
125
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
126
|
+
const currentValue = isControlled ? (value as string) : internalValue;
|
|
127
|
+
|
|
128
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
129
|
+
const [draft, setDraft] = useState(currentValue);
|
|
130
|
+
const [error, setError] = useState<string | null>(null);
|
|
131
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
132
|
+
|
|
133
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
134
|
+
|
|
135
|
+
// Once an edit session ends (commit or cancel), any follow-on blur
|
|
136
|
+
// that fires in the same tick should be a no-op. Cleared when we
|
|
137
|
+
// re-enter edit mode.
|
|
138
|
+
const settledRef = useRef(false);
|
|
139
|
+
|
|
140
|
+
const stopEditing = useCallback(() => {
|
|
141
|
+
settledRef.current = true;
|
|
142
|
+
setIsEditing(false);
|
|
143
|
+
setError(null);
|
|
144
|
+
onEditEnd?.();
|
|
145
|
+
}, [onEditEnd]);
|
|
146
|
+
|
|
147
|
+
const startEditing = useCallback(() => {
|
|
148
|
+
if (disabled || isEditing) return;
|
|
149
|
+
settledRef.current = false;
|
|
150
|
+
setDraft(currentValue);
|
|
151
|
+
setError(null);
|
|
152
|
+
setIsEditing(true);
|
|
153
|
+
onEditStart?.();
|
|
154
|
+
}, [currentValue, disabled, isEditing, onEditStart]);
|
|
155
|
+
|
|
156
|
+
const cancel = useCallback(() => {
|
|
157
|
+
if (!isEditing || settledRef.current) return;
|
|
158
|
+
stopEditing();
|
|
159
|
+
}, [isEditing, stopEditing]);
|
|
160
|
+
|
|
161
|
+
const commit = useCallback(() => {
|
|
162
|
+
if (!isEditing || settledRef.current || isSaving) return;
|
|
163
|
+
|
|
164
|
+
const next = trim ? draft.trim() : draft;
|
|
165
|
+
|
|
166
|
+
if (required && draft.trim().length === 0) {
|
|
167
|
+
setError(requiredMessage ?? 'This field is required');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (next === currentValue) {
|
|
172
|
+
stopEditing();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (validate) {
|
|
177
|
+
const message = validate(next);
|
|
178
|
+
if (message != null) {
|
|
179
|
+
setError(message);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setIsSaving(true);
|
|
185
|
+
|
|
186
|
+
// Exit edit mode immediately so the optimistic cache update (owned
|
|
187
|
+
// by the parent's mutation) can flip us back to display mode without
|
|
188
|
+
// waiting on the network round-trip.
|
|
189
|
+
if (!isControlled) {
|
|
190
|
+
setInternalValue(next);
|
|
191
|
+
}
|
|
192
|
+
stopEditing();
|
|
193
|
+
|
|
194
|
+
Promise.resolve(onSave(next))
|
|
195
|
+
.catch(err => {
|
|
196
|
+
// Late failure: we already exited edit mode. Roll back the
|
|
197
|
+
// local display value (for uncontrolled use), re-open edit
|
|
198
|
+
// mode with the user's attempted value, and surface the error.
|
|
199
|
+
if (!isControlled) {
|
|
200
|
+
setInternalValue(currentValue);
|
|
201
|
+
}
|
|
202
|
+
settledRef.current = false;
|
|
203
|
+
setDraft(next);
|
|
204
|
+
setError(err instanceof Error ? err.message : 'Unable to save');
|
|
205
|
+
setIsEditing(true);
|
|
206
|
+
})
|
|
207
|
+
.finally(() => {
|
|
208
|
+
setIsSaving(false);
|
|
209
|
+
});
|
|
210
|
+
}, [
|
|
211
|
+
currentValue,
|
|
212
|
+
draft,
|
|
213
|
+
isControlled,
|
|
214
|
+
isEditing,
|
|
215
|
+
isSaving,
|
|
216
|
+
onSave,
|
|
217
|
+
required,
|
|
218
|
+
requiredMessage,
|
|
219
|
+
stopEditing,
|
|
220
|
+
trim,
|
|
221
|
+
validate,
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
useImperativeHandle(ref, () => ({ startEditing, commit, cancel }), [
|
|
225
|
+
startEditing,
|
|
226
|
+
commit,
|
|
227
|
+
cancel,
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
// Focus + select-all when we enter edit mode.
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (isEditing && inputRef.current) {
|
|
233
|
+
inputRef.current.focus();
|
|
234
|
+
inputRef.current.select();
|
|
235
|
+
}
|
|
236
|
+
}, [isEditing]);
|
|
237
|
+
|
|
238
|
+
// If the external value changes while we're NOT editing, keep the
|
|
239
|
+
// draft in sync so a later edit starts from the right place.
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!isEditing) {
|
|
242
|
+
setDraft(currentValue);
|
|
243
|
+
}
|
|
244
|
+
}, [currentValue, isEditing]);
|
|
245
|
+
|
|
246
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
247
|
+
if (e.key === 'Enter') {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
void commit();
|
|
250
|
+
} else if (e.key === 'Escape') {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
cancel();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const isEmpty = currentValue.length === 0;
|
|
257
|
+
|
|
258
|
+
if (isEditing) {
|
|
259
|
+
return (
|
|
260
|
+
<TextInput
|
|
261
|
+
ref={inputRef}
|
|
262
|
+
value={draft}
|
|
263
|
+
onChange={e => {
|
|
264
|
+
setDraft(e.currentTarget.value);
|
|
265
|
+
if (error) setError(null);
|
|
266
|
+
}}
|
|
267
|
+
onBlur={() => {
|
|
268
|
+
void commit();
|
|
269
|
+
}}
|
|
270
|
+
onKeyDown={handleKeyDown}
|
|
271
|
+
placeholder={inputPlaceholder ?? placeholder}
|
|
272
|
+
aria-label={ariaLabel}
|
|
273
|
+
maxLength={maxLength}
|
|
274
|
+
error={error ?? undefined}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (renderDisplay) {
|
|
280
|
+
return (
|
|
281
|
+
<DisplayShell
|
|
282
|
+
onStart={startEditing}
|
|
283
|
+
disabled={disabled}
|
|
284
|
+
ariaLabel={ariaLabel}
|
|
285
|
+
>
|
|
286
|
+
{renderDisplay(currentValue, isEmpty)}
|
|
287
|
+
</DisplayShell>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<DisplayShell
|
|
293
|
+
onStart={startEditing}
|
|
294
|
+
disabled={disabled}
|
|
295
|
+
ariaLabel={ariaLabel}
|
|
296
|
+
>
|
|
297
|
+
<Text
|
|
298
|
+
variant="body1"
|
|
299
|
+
c={isEmpty ? 'text.subdued.default' : undefined}
|
|
300
|
+
style={{
|
|
301
|
+
flex: 1,
|
|
302
|
+
minWidth: 0,
|
|
303
|
+
overflow: 'hidden',
|
|
304
|
+
textOverflow: 'ellipsis',
|
|
305
|
+
whiteSpace: 'nowrap',
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
{isEmpty ? placeholder : currentValue}
|
|
309
|
+
</Text>
|
|
310
|
+
</DisplayShell>
|
|
311
|
+
);
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
interface DisplayShellProps {
|
|
316
|
+
onStart: () => void;
|
|
317
|
+
disabled: boolean;
|
|
318
|
+
ariaLabel?: string;
|
|
319
|
+
children: ReactNode;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function DisplayShell({
|
|
323
|
+
onStart,
|
|
324
|
+
disabled,
|
|
325
|
+
ariaLabel,
|
|
326
|
+
children,
|
|
327
|
+
}: DisplayShellProps) {
|
|
328
|
+
const [hover, setHover] = useState(false);
|
|
329
|
+
|
|
330
|
+
const showAffordance = hover && !disabled;
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<button
|
|
334
|
+
type="button"
|
|
335
|
+
onClick={onStart}
|
|
336
|
+
onFocus={() => setHover(true)}
|
|
337
|
+
onBlur={() => setHover(false)}
|
|
338
|
+
onMouseEnter={() => setHover(true)}
|
|
339
|
+
onMouseLeave={() => setHover(false)}
|
|
340
|
+
disabled={disabled}
|
|
341
|
+
aria-label={ariaLabel}
|
|
342
|
+
style={{
|
|
343
|
+
position: 'relative',
|
|
344
|
+
display: 'inline-flex',
|
|
345
|
+
alignItems: 'center',
|
|
346
|
+
padding: '2px 22px 2px 6px',
|
|
347
|
+
margin: '-2px -6px',
|
|
348
|
+
background: 'transparent',
|
|
349
|
+
border: '1px solid transparent',
|
|
350
|
+
borderColor: showAffordance
|
|
351
|
+
? 'var(--color-stroke-subdued-default)'
|
|
352
|
+
: 'transparent',
|
|
353
|
+
borderRadius: 'var(--radius-lg)',
|
|
354
|
+
cursor: disabled ? 'default' : 'text',
|
|
355
|
+
textAlign: 'left',
|
|
356
|
+
font: 'inherit',
|
|
357
|
+
color: 'inherit',
|
|
358
|
+
width: '100%',
|
|
359
|
+
maxWidth: '100%',
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
{children}
|
|
363
|
+
<Pencil
|
|
364
|
+
size={14}
|
|
365
|
+
aria-hidden
|
|
366
|
+
style={{
|
|
367
|
+
position: 'absolute',
|
|
368
|
+
right: 6,
|
|
369
|
+
top: '50%',
|
|
370
|
+
transform: 'translateY(-50%)',
|
|
371
|
+
opacity: showAffordance ? 0.6 : 0,
|
|
372
|
+
transition: 'opacity 120ms ease',
|
|
373
|
+
}}
|
|
374
|
+
/>
|
|
375
|
+
</button>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
|
|
3
|
+
import { EmptyState } from './EmptyState';
|
|
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
|
+
EmptyState,
|
|
15
|
+
'https://www.figma.com/design/VCLfybgU3OaUUPrQdBaVmP/LM-Design-System?node-id=2037%3A2949',
|
|
16
|
+
{
|
|
17
|
+
props: {
|
|
18
|
+
// No matching props could be found for these Figma properties:
|
|
19
|
+
// "showLearnMore": figma.boolean('Show "Learn More"'),
|
|
20
|
+
// "layout": figma.enum('Layout', {
|
|
21
|
+
// "1 Button": "1-button",
|
|
22
|
+
// "2 Buttons": "2-buttons",
|
|
23
|
+
// "No Buttons": "no-buttons"
|
|
24
|
+
// })
|
|
25
|
+
},
|
|
26
|
+
example: _props => (
|
|
27
|
+
<EmptyState
|
|
28
|
+
title="No results found"
|
|
29
|
+
description="Try adjusting your filters."
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { ElementType, FunctionComponent, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SearchX } 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 EmptyStateAction {
|
|
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 EmptyStateProps {
|
|
24
|
+
/**
|
|
25
|
+
* The icon to display at the top of the empty state
|
|
26
|
+
* @default <SearchX size={48} strokeWidth={1.5} />
|
|
27
|
+
*/
|
|
28
|
+
icon?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Icon container variant
|
|
31
|
+
* - 'bordered': Default dimmed bordered box
|
|
32
|
+
* - 'filled': Filled background (use with iconBg prop)
|
|
33
|
+
* @default 'bordered'
|
|
34
|
+
*/
|
|
35
|
+
iconVariant?: 'bordered' | 'filled';
|
|
36
|
+
/**
|
|
37
|
+
* Background color for filled icon variant (theme contract background token).
|
|
38
|
+
* Only used when iconVariant='filled'. Pass tokens.color.background.* from @acme/ui.
|
|
39
|
+
*/
|
|
40
|
+
iconBg?: BackgroundColorVar;
|
|
41
|
+
/**
|
|
42
|
+
* Icon size (width and height in pixels)
|
|
43
|
+
* @default 64 for 'bordered', 36 for 'filled'
|
|
44
|
+
*/
|
|
45
|
+
iconSize?: number;
|
|
46
|
+
/**
|
|
47
|
+
* The title/heading text
|
|
48
|
+
*/
|
|
49
|
+
title?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The description/body text
|
|
52
|
+
*/
|
|
53
|
+
description?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Primary action button configuration
|
|
56
|
+
*/
|
|
57
|
+
action?: EmptyStateAction;
|
|
58
|
+
/**
|
|
59
|
+
* Secondary action button configuration (ghost variant)
|
|
60
|
+
*/
|
|
61
|
+
secondaryAction?: EmptyStateAction;
|
|
62
|
+
/**
|
|
63
|
+
* Maximum width of the content area
|
|
64
|
+
* @default 500
|
|
65
|
+
*/
|
|
66
|
+
maxWidth?: number | string;
|
|
67
|
+
/**
|
|
68
|
+
* Wrap content in a Card with background and border
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
showCard?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Card background (theme contract background token). Only used when showCard=true.
|
|
74
|
+
* Pass tokens.color.background.* from @acme/ui.
|
|
75
|
+
*/
|
|
76
|
+
cardBg?: BackgroundColorVar;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* EmptyState component
|
|
81
|
+
* Displays a centered empty state with icon, title, description, and optional action buttons
|
|
82
|
+
*
|
|
83
|
+
* Supports two main variants:
|
|
84
|
+
* 1. Default transparent layout (used in tables, lists)
|
|
85
|
+
* 2. Card variant with background (used for full-page empty states)
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Simple table empty state
|
|
89
|
+
* <EmptyState
|
|
90
|
+
* title="No results found"
|
|
91
|
+
* description="Try adjusting your filters"
|
|
92
|
+
* action={{ label: 'Clear filters', onClick: clearFilters }}
|
|
93
|
+
* />
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Full-page empty state with card (use theme contract tokens only)
|
|
97
|
+
* <EmptyState
|
|
98
|
+
* icon={<Cloud size={20} />}
|
|
99
|
+
* iconVariant="filled"
|
|
100
|
+
* iconBg={tokens.color.background.primaryLight}
|
|
101
|
+
* title="No Apps Detected Yet"
|
|
102
|
+
* description="Deploy agents to start seeing data"
|
|
103
|
+
* action={{ label: 'Deploy', onClick: deploy }}
|
|
104
|
+
* secondaryAction={{ label: 'Learn More', onClick: learnMore }}
|
|
105
|
+
* showCard
|
|
106
|
+
* maxWidth={420}
|
|
107
|
+
* />
|
|
108
|
+
*/
|
|
109
|
+
export function EmptyState({
|
|
110
|
+
icon = <SearchX size={48} strokeWidth={1.5} />,
|
|
111
|
+
iconVariant = 'bordered',
|
|
112
|
+
iconBg,
|
|
113
|
+
iconSize,
|
|
114
|
+
title,
|
|
115
|
+
description,
|
|
116
|
+
action,
|
|
117
|
+
secondaryAction,
|
|
118
|
+
maxWidth = 500,
|
|
119
|
+
showCard = false,
|
|
120
|
+
cardBg = tokens.color.background.subduedLight,
|
|
121
|
+
}: EmptyStateProps) {
|
|
122
|
+
// Determine icon size based on variant if not explicitly set
|
|
123
|
+
const computedIconSize = iconSize ?? (iconVariant === 'filled' ? 36 : 64);
|
|
124
|
+
|
|
125
|
+
const iconElement =
|
|
126
|
+
iconVariant === 'filled' ? (
|
|
127
|
+
<Box
|
|
128
|
+
w={computedIconSize}
|
|
129
|
+
h={computedIconSize}
|
|
130
|
+
p="xs"
|
|
131
|
+
display="flex"
|
|
132
|
+
style={{
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
justifyContent: 'center',
|
|
135
|
+
backgroundColor: iconBg != null ? toCssVar(iconBg) : undefined,
|
|
136
|
+
borderRadius: 'var(--radius-sm)',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{icon}
|
|
140
|
+
</Box>
|
|
141
|
+
) : (
|
|
142
|
+
<Box
|
|
143
|
+
c="text.subdued.default"
|
|
144
|
+
bd="1px solid var(--color-general-border)"
|
|
145
|
+
bdrs="md"
|
|
146
|
+
w={computedIconSize}
|
|
147
|
+
h={computedIconSize}
|
|
148
|
+
display="flex"
|
|
149
|
+
style={{ alignItems: 'center', justifyContent: 'center' }}
|
|
150
|
+
>
|
|
151
|
+
{icon}
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const content = (
|
|
156
|
+
<Stack gap="md" align="center">
|
|
157
|
+
{iconElement}
|
|
158
|
+
|
|
159
|
+
<Stack gap="xs" align="center" maw={400}>
|
|
160
|
+
{title && (
|
|
161
|
+
<Text variant="body1.stronger" ta="center">
|
|
162
|
+
{title}
|
|
163
|
+
</Text>
|
|
164
|
+
)}
|
|
165
|
+
{description && (
|
|
166
|
+
<Text variant="caption1" c="text.subdued.default" ta="center">
|
|
167
|
+
{description}
|
|
168
|
+
</Text>
|
|
169
|
+
)}
|
|
170
|
+
</Stack>
|
|
171
|
+
|
|
172
|
+
{action && (
|
|
173
|
+
<Button
|
|
174
|
+
variant="primary"
|
|
175
|
+
onClick={action.onClick}
|
|
176
|
+
component={
|
|
177
|
+
action.component as
|
|
178
|
+
| FunctionComponent<Record<string, unknown>>
|
|
179
|
+
| undefined
|
|
180
|
+
}
|
|
181
|
+
to={action.to}
|
|
182
|
+
radius="lg"
|
|
183
|
+
size="sm"
|
|
184
|
+
mt="md"
|
|
185
|
+
>
|
|
186
|
+
{action.label}
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{secondaryAction && (
|
|
191
|
+
<Button
|
|
192
|
+
variant="ghost"
|
|
193
|
+
color="dark"
|
|
194
|
+
onClick={secondaryAction.onClick}
|
|
195
|
+
component={
|
|
196
|
+
secondaryAction.component as
|
|
197
|
+
| FunctionComponent<Record<string, unknown>>
|
|
198
|
+
| undefined
|
|
199
|
+
}
|
|
200
|
+
to={secondaryAction.to}
|
|
201
|
+
rightSection={secondaryAction.rightSection}
|
|
202
|
+
radius="lg"
|
|
203
|
+
size="sm"
|
|
204
|
+
>
|
|
205
|
+
{secondaryAction.label}
|
|
206
|
+
</Button>
|
|
207
|
+
)}
|
|
208
|
+
</Stack>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (showCard) {
|
|
212
|
+
return (
|
|
213
|
+
<Card
|
|
214
|
+
p="2xl"
|
|
215
|
+
w={maxWidth}
|
|
216
|
+
bg={cardBg != null ? toCssVar(cardBg) : undefined}
|
|
217
|
+
bd="1px solid var(--color-general-border)"
|
|
218
|
+
radius="lg"
|
|
219
|
+
>
|
|
220
|
+
{content}
|
|
221
|
+
</Card>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<Box py="2xl" maw={maxWidth} mx="auto">
|
|
227
|
+
{content}
|
|
228
|
+
</Box>
|
|
229
|
+
);
|
|
230
|
+
}
|