@scalepad/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/.ai/rules/date-handling.md +39 -0
  2. package/.ai/rules/figma-design-system.md +372 -0
  3. package/.ai/rules/figma-lm-design-system-keys.md +680 -0
  4. package/.ai/rules/file-extensions.md +13 -0
  5. package/.ai/rules/modal-confirmation-mutation.md +56 -0
  6. package/.ai/rules/react-hooks.md +29 -0
  7. package/.ai/rules/styling.md +83 -0
  8. package/AGENTS.md +37 -0
  9. package/README.md +125 -0
  10. package/figma.config.json +9 -0
  11. package/package.json +127 -0
  12. package/scripts/install-ai-rules.mjs +136 -0
  13. package/src/ThemeProvider.tsx +57 -0
  14. package/src/charts.ts +32 -0
  15. package/src/components/ActionCard/ActionCard.css.ts +60 -0
  16. package/src/components/ActionCard/ActionCard.tsx +154 -0
  17. package/src/components/ActionCard/index.ts +2 -0
  18. package/src/components/Anchor/Anchor.tsx +47 -0
  19. package/src/components/Anchor/index.ts +2 -0
  20. package/src/components/AppliedFiltersManagerBar/AppliedFiltersManagerBar.tsx +105 -0
  21. package/src/components/AppliedFiltersManagerBar/FilterBadge.css.ts +23 -0
  22. package/src/components/AppliedFiltersManagerBar/FilterBadge.tsx +50 -0
  23. package/src/components/AppliedFiltersManagerBar/index.ts +5 -0
  24. package/src/components/Badge/Badge.css.ts +72 -0
  25. package/src/components/Badge/Badge.figma.tsx +43 -0
  26. package/src/components/Badge/Badge.tsx +159 -0
  27. package/src/components/Badge/index.ts +2 -0
  28. package/src/components/BreadCrumb/BreadCrumb.tsx +62 -0
  29. package/src/components/BreadCrumb/index.ts +2 -0
  30. package/src/components/BulkActionBar/BulkActionBar.css.ts +26 -0
  31. package/src/components/BulkActionBar/BulkActionBar.tsx +164 -0
  32. package/src/components/BulkActionBar/index.ts +2 -0
  33. package/src/components/Button/Button.css.ts +272 -0
  34. package/src/components/Button/Button.figma.tsx +74 -0
  35. package/src/components/Button/Button.tsx +84 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Charts/ChartTooltip.figma.tsx +33 -0
  38. package/src/components/Charts/ChartTooltip.tsx +101 -0
  39. package/src/components/Charts/MiniBarSparkline.tsx +75 -0
  40. package/src/components/Charts/StackedPatternBarChart.tsx +494 -0
  41. package/src/components/Charts/TrendAreaChart.css.ts +23 -0
  42. package/src/components/Charts/TrendAreaChart.tsx +210 -0
  43. package/src/components/Charts/index.ts +12 -0
  44. package/src/components/CodePanel/CodePanel.css.ts +113 -0
  45. package/src/components/CodePanel/CodePanel.tsx +121 -0
  46. package/src/components/CodePanel/index.ts +2 -0
  47. package/src/components/CommentComposer/CommentComposer.css.ts +60 -0
  48. package/src/components/CommentComposer/CommentComposer.tsx +181 -0
  49. package/src/components/CommentComposer/index.ts +2 -0
  50. package/src/components/ConfirmationModal/ConfirmationModal.tsx +149 -0
  51. package/src/components/ConfirmationModal/index.ts +2 -0
  52. package/src/components/ConfirmationTooltip/ConfirmationTooltip.tsx +132 -0
  53. package/src/components/ConfirmationTooltip/index.ts +2 -0
  54. package/src/components/DataDialog.figma.tsx +33 -0
  55. package/src/components/DataDialog.tsx +46 -0
  56. package/src/components/DataTable/DataTable.tsx +1042 -0
  57. package/src/components/DataTable/RowExpandToggle.tsx +105 -0
  58. package/src/components/DataTable/RowGroupHeader.tsx +190 -0
  59. package/src/components/DataTable/createActionsColumn.tsx +86 -0
  60. package/src/components/DataTable/index.ts +25 -0
  61. package/src/components/DatePicker/CustomRangePicker.tsx +59 -0
  62. package/src/components/DatePicker/DateInput.tsx +329 -0
  63. package/src/components/DatePicker/DateNavigator.tsx +486 -0
  64. package/src/components/DatePicker/DatePicker.tsx +242 -0
  65. package/src/components/DatePicker/MonthlyRangePicker.tsx +231 -0
  66. package/src/components/DatePicker/QuarterlyRangePicker.tsx +224 -0
  67. package/src/components/DatePicker/QuickPicksSidebar.tsx +242 -0
  68. package/src/components/DatePicker/YearlyRangePicker.tsx +171 -0
  69. package/src/components/DatePicker/index.ts +7 -0
  70. package/src/components/DatePicker/types.ts +12 -0
  71. package/src/components/DesignSystemPrimitives/FluidGrid.tsx +44 -0
  72. package/src/components/DesignSystemPrimitives/InteractivePrimitives.tsx +177 -0
  73. package/src/components/DesignSystemPrimitives/LayoutPrimitives.tsx +220 -0
  74. package/src/components/DesignSystemPrimitives/LayoutPrimitives.types.tsx +15 -0
  75. package/src/components/DesignSystemPrimitives/SurfacePrimitives.tsx +46 -0
  76. package/src/components/DesignSystemPrimitives/index.ts +55 -0
  77. package/src/components/Details/Details.css.ts +74 -0
  78. package/src/components/Details/Details.tsx +140 -0
  79. package/src/components/Details/index.ts +2 -0
  80. package/src/components/DownloadCard/DownloadCard.css.ts +22 -0
  81. package/src/components/DownloadCard/DownloadCard.tsx +63 -0
  82. package/src/components/DownloadCard/index.ts +2 -0
  83. package/src/components/Drawer/Drawer.css.ts +32 -0
  84. package/src/components/Drawer/Drawer.tsx +236 -0
  85. package/src/components/Drawer/hooks/useDetailDrawer.ts +61 -0
  86. package/src/components/Drawer/hooks/useDetailDrawerNavigation.ts +125 -0
  87. package/src/components/Drawer/hooks/useDetailDrawerNavigationContext.ts +66 -0
  88. package/src/components/EditableRichText/EditableRichText.css.ts +72 -0
  89. package/src/components/EditableRichText/EditableRichText.tsx +324 -0
  90. package/src/components/EditableRichText/index.ts +2 -0
  91. package/src/components/EditableSelect/EditableSelect.css.ts +62 -0
  92. package/src/components/EditableSelect/EditableSelect.tsx +224 -0
  93. package/src/components/EditableSelect/index.ts +2 -0
  94. package/src/components/EditableText/EditableText.tsx +377 -0
  95. package/src/components/EditableText/index.ts +2 -0
  96. package/src/components/EmptyState/EmptyState.figma.tsx +33 -0
  97. package/src/components/EmptyState/EmptyState.tsx +230 -0
  98. package/src/components/EmptyState/index.ts +2 -0
  99. package/src/components/ErrorBoundary.tsx +135 -0
  100. package/src/components/ErrorState/ErrorState.tsx +197 -0
  101. package/src/components/ErrorState/index.ts +2 -0
  102. package/src/components/FeatureCard.tsx +42 -0
  103. package/src/components/FilterMenu/FilterMenu.figma.tsx +30 -0
  104. package/src/components/FilterMenu/FilterMenu.tsx +198 -0
  105. package/src/components/FilterMenu/FilterSubMenuTypes/BooleanFilterSubmenu.tsx +46 -0
  106. package/src/components/FilterMenu/FilterSubMenuTypes/SearchableFilterSubmenu.tsx +239 -0
  107. package/src/components/FilterMenu/FilterSubMenuTypes/index.ts +8 -0
  108. package/src/components/FilterMenu/defaultFilterSchemas.ts +63 -0
  109. package/src/components/FilterMenu/helpers.ts +115 -0
  110. package/src/components/FilterMenu/index.ts +35 -0
  111. package/src/components/FilterMenu/types.ts +101 -0
  112. package/src/components/IconButton/IconButton.css.ts +272 -0
  113. package/src/components/IconButton/IconButton.figma.tsx +47 -0
  114. package/src/components/IconButton/IconButton.tsx +72 -0
  115. package/src/components/IconButton/README.md +230 -0
  116. package/src/components/IconButton/index.ts +2 -0
  117. package/src/components/InfiniteScrollSentinel.tsx +86 -0
  118. package/src/components/InfiniteScrollTrigger.tsx +78 -0
  119. package/src/components/InfoCard.figma.tsx +47 -0
  120. package/src/components/InfoCard.tsx +216 -0
  121. package/src/components/KbdHint/KbdHint.tsx +23 -0
  122. package/src/components/KbdHint/index.ts +2 -0
  123. package/src/components/LabeledField/LabeledField.tsx +21 -0
  124. package/src/components/LabeledField/index.ts +2 -0
  125. package/src/components/LookupSelect/LookupSelect.css.ts +149 -0
  126. package/src/components/LookupSelect/LookupSelect.tsx +325 -0
  127. package/src/components/LookupSelect/index.ts +2 -0
  128. package/src/components/Menu/Menu.css.ts +89 -0
  129. package/src/components/Menu/Menu.tsx +105 -0
  130. package/src/components/Menu/index.ts +2 -0
  131. package/src/components/MessageBox/MessageBox.tsx +168 -0
  132. package/src/components/MessageBox/index.ts +2 -0
  133. package/src/components/MetricDisplay/MetricDisplay.tsx +55 -0
  134. package/src/components/MetricDisplay/index.ts +1 -0
  135. package/src/components/MultiSelect/MultiSelect.tsx +278 -0
  136. package/src/components/MultiSelect/index.ts +2 -0
  137. package/src/components/Notifications/Notifications.tsx +12 -0
  138. package/src/components/Notifications/README.md +93 -0
  139. package/src/components/Notifications/index.ts +4 -0
  140. package/src/components/Notifications/showToast.tsx +100 -0
  141. package/src/components/PropertyRow/PropertyRow.tsx +96 -0
  142. package/src/components/PropertyRow/index.ts +2 -0
  143. package/src/components/RadioTile/RadioTile.tsx +253 -0
  144. package/src/components/RadioTile/index.ts +2 -0
  145. package/src/components/RichText/FormattingToolbar.css.ts +69 -0
  146. package/src/components/RichText/FormattingToolbar.tsx +112 -0
  147. package/src/components/RichText/RichTextInline.css.ts +54 -0
  148. package/src/components/RichText/RichTextInline.tsx +318 -0
  149. package/src/components/RichText/formattingCommands.ts +181 -0
  150. package/src/components/RichText/formattingTypes.ts +34 -0
  151. package/src/components/RichText/index.ts +49 -0
  152. package/src/components/RichText/richTextExtensions.ts +111 -0
  153. package/src/components/RichText/richTextHelpers.ts +65 -0
  154. package/src/components/RichText/richTextImage.ts +253 -0
  155. package/src/components/RichText/richTextImageHandlers.ts +244 -0
  156. package/src/components/RichText/richTextProse.css.ts +261 -0
  157. package/src/components/RichTextEditor/RichTextEditor.css.ts +82 -0
  158. package/src/components/RichTextEditor/RichTextEditor.tsx +204 -0
  159. package/src/components/RichTextEditor/index.ts +2 -0
  160. package/src/components/RichTextView/RichTextView.css.ts +11 -0
  161. package/src/components/RichTextView/RichTextView.tsx +114 -0
  162. package/src/components/RichTextView/index.ts +2 -0
  163. package/src/components/Schedule/Schedule.tsx +35 -0
  164. package/src/components/SchedulePicker/SchedulePicker.css.ts +42 -0
  165. package/src/components/SchedulePicker/SchedulePicker.tsx +130 -0
  166. package/src/components/SchedulePicker/index.ts +2 -0
  167. package/src/components/SearchableList/types.ts +30 -0
  168. package/src/components/SearchableSubMenu/SearchableSubMenu.css.ts +25 -0
  169. package/src/components/SearchableSubMenu/SearchableSubMenu.tsx +139 -0
  170. package/src/components/SearchableSubMenu/index.ts +2 -0
  171. package/src/components/Select/README.md +114 -0
  172. package/src/components/Select/Select.css.ts +110 -0
  173. package/src/components/Select/Select.tsx +133 -0
  174. package/src/components/Select/index.ts +2 -0
  175. package/src/components/SelectCreatable/SelectCreatable.css.ts +16 -0
  176. package/src/components/SelectCreatable/SelectCreatable.tsx +203 -0
  177. package/src/components/SelectCreatable/index.ts +2 -0
  178. package/src/components/SettingsCard/SettingsCard.tsx +98 -0
  179. package/src/components/SettingsCard/index.ts +2 -0
  180. package/src/components/Sidebar/Sidebar.css.ts +91 -0
  181. package/src/components/Sidebar/Sidebar.tsx +129 -0
  182. package/src/components/Sidebar/index.ts +5 -0
  183. package/src/components/SimpleList/SimpleList.css.ts +12 -0
  184. package/src/components/SimpleList/SimpleList.tsx +44 -0
  185. package/src/components/SimpleList/index.ts +2 -0
  186. package/src/components/SimpleTable/SimpleTable.tsx +296 -0
  187. package/src/components/SimpleTable/index.ts +2 -0
  188. package/src/components/SlashRichTextEditor/SelectionBubbleMenu.css.ts +62 -0
  189. package/src/components/SlashRichTextEditor/SelectionBubbleMenu.tsx +85 -0
  190. package/src/components/SlashRichTextEditor/SlashCommandMenu.css.ts +124 -0
  191. package/src/components/SlashRichTextEditor/SlashCommandMenu.tsx +168 -0
  192. package/src/components/SlashRichTextEditor/SlashRichTextEditor.css.ts +81 -0
  193. package/src/components/SlashRichTextEditor/SlashRichTextEditor.tsx +538 -0
  194. package/src/components/SlashRichTextEditor/SlashSuggestionExtension.ts +48 -0
  195. package/src/components/SlashRichTextEditor/index.ts +13 -0
  196. package/src/components/SlashRichTextEditor/types.ts +48 -0
  197. package/src/components/StatCard/StatCard.css.ts +70 -0
  198. package/src/components/StatCard/StatCard.tsx +201 -0
  199. package/src/components/StatCard/index.ts +1 -0
  200. package/src/components/StatusBadge/StatusBadge.tsx +70 -0
  201. package/src/components/StatusBadge/index.ts +2 -0
  202. package/src/components/StatusIndicator/StatusIndicator.tsx +67 -0
  203. package/src/components/StatusIndicator/index.ts +6 -0
  204. package/src/components/SubNavigation/SubNavigation.css.ts +72 -0
  205. package/src/components/SubNavigation/SubNavigation.tsx +104 -0
  206. package/src/components/SubNavigation/index.ts +2 -0
  207. package/src/components/SuspenseLoader.tsx +22 -0
  208. package/src/components/Table/SortableColumnHeader.tsx +99 -0
  209. package/src/components/Table/TableSkeletonRows.figma.tsx +22 -0
  210. package/src/components/Table/TableSkeletonRows.tsx +113 -0
  211. package/src/components/Table/index.ts +9 -0
  212. package/src/components/TableActionsMenu.tsx +58 -0
  213. package/src/components/TableCard.tsx +29 -0
  214. package/src/components/TableContainer/TableContainer.tsx +86 -0
  215. package/src/components/TableContainer/index.ts +2 -0
  216. package/src/components/TableControlBar/TableControlBar.tsx +156 -0
  217. package/src/components/TableControlBar/TableSelectionButton.tsx +57 -0
  218. package/src/components/TableControlBar/index.ts +13 -0
  219. package/src/components/TableControlBar/useTableControlBar.tsx +314 -0
  220. package/src/components/TableSelection/TableSelection.tsx +43 -0
  221. package/src/components/TableSelection/index.ts +5 -0
  222. package/src/components/Tabs/README.md +76 -0
  223. package/src/components/Tabs/Tabs.css.ts +54 -0
  224. package/src/components/Tabs/Tabs.figma.tsx +47 -0
  225. package/src/components/Tabs/Tabs.tsx +96 -0
  226. package/src/components/Tabs/index.ts +8 -0
  227. package/src/components/TextInput/README.md +98 -0
  228. package/src/components/TextInput/SearchTextInput.figma.tsx +22 -0
  229. package/src/components/TextInput/SearchTextInput.tsx +150 -0
  230. package/src/components/TextInput/TextInput.figma.tsx +44 -0
  231. package/src/components/TextInput/TextInput.tsx +42 -0
  232. package/src/components/TextInput/index.ts +4 -0
  233. package/src/components/ThemeSwitcher.figma.tsx +28 -0
  234. package/src/components/ThemeSwitcher.tsx +69 -0
  235. package/src/components/TrendBadge/TrendBadge.tsx +76 -0
  236. package/src/components/TrendBadge/index.ts +2 -0
  237. package/src/components/TruncatedText.tsx +115 -0
  238. package/src/components/Typography/Text.tsx +74 -0
  239. package/src/components/Typography/Title.tsx +100 -0
  240. package/src/components/Typography/index.ts +4 -0
  241. package/src/geist-fonts.ts +48 -0
  242. package/src/hooks/index.ts +31 -0
  243. package/src/hooks/useFilters.ts +152 -0
  244. package/src/hooks/useInfiniteScroll.ts +62 -0
  245. package/src/hooks/usePlatform.ts +33 -0
  246. package/src/hooks/useServerTable.ts +495 -0
  247. package/src/hooks/useTableSelection.ts +102 -0
  248. package/src/hooks/useTableSort.ts +259 -0
  249. package/src/index.ts +483 -0
  250. package/src/mantine.ts +25 -0
  251. package/src/theme/mantineVars.ts +12 -0
  252. package/src/theme/themeContract.css.ts +131 -0
  253. package/src/theme/themeVars.ts +31 -0
  254. package/src/theme.ts +168 -0
  255. package/src/tokens/color-types.ts +107 -0
  256. package/src/tokens/colors.ts +243 -0
  257. package/src/tokens/index.ts +14 -0
  258. package/src/tokens/radius.ts +17 -0
  259. package/src/tokens/semantic-colors.ts +224 -0
  260. package/src/tokens/semantic-tokens-css.ts +53 -0
  261. package/src/tokens/shadows.ts +11 -0
  262. package/src/tokens/spacing.ts +20 -0
  263. package/src/tokens/text-styles.ts +179 -0
  264. package/src/tokens/typography.ts +40 -0
  265. package/src/tokens/zIndex.ts +27 -0
  266. package/src/types/mantine-theme.d.ts +17 -0
  267. package/src/types/tanstack-table.d.ts +22 -0
  268. package/src/utils/avatar.ts +150 -0
  269. package/src/utils/chartHelpers.ts +53 -0
  270. package/src/utils/color-props.ts +77 -0
  271. package/src/utils/createDesignComponent.tsx +104 -0
  272. package/src/utils/nestFlatRows.ts +111 -0
  273. package/src/utils/withStaticComponents.ts +6 -0
@@ -0,0 +1,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,2 @@
1
+ export { EditableText } from './EditableText';
2
+ export type { EditableTextHandle, EditableTextProps } from './EditableText';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { EmptyState } from './EmptyState';
2
+ export type { EmptyStateAction, EmptyStateProps } from './EmptyState';