@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,35 @@
|
|
|
1
|
+
// Thin re-export wrapper around `@mantine/schedule` so consumers can import the
|
|
2
|
+
// Schedule component and its helpers from the design system surface
|
|
3
|
+
// (`@scalepad/ui`) instead of reaching into Mantine directly. Keep this file
|
|
4
|
+
// intentionally shallow — any future DS-specific styling or wrapping should
|
|
5
|
+
// live alongside.
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
DEFAULT_SCHEDULE_LABELS,
|
|
9
|
+
DayView,
|
|
10
|
+
MobileMonthView,
|
|
11
|
+
MonthView,
|
|
12
|
+
Schedule,
|
|
13
|
+
ScheduleEvent,
|
|
14
|
+
ScheduleHeader,
|
|
15
|
+
WeekView,
|
|
16
|
+
YearView,
|
|
17
|
+
getLabel,
|
|
18
|
+
} from '@mantine/schedule';
|
|
19
|
+
export type {
|
|
20
|
+
DateStringValue,
|
|
21
|
+
DateTimeStringValue,
|
|
22
|
+
DayViewProps,
|
|
23
|
+
MonthViewProps,
|
|
24
|
+
RenderEventBody,
|
|
25
|
+
ScheduleEventData,
|
|
26
|
+
ScheduleLabels,
|
|
27
|
+
ScheduleLabelsOverride,
|
|
28
|
+
ScheduleLayout,
|
|
29
|
+
ScheduleMode,
|
|
30
|
+
ScheduleProps,
|
|
31
|
+
ScheduleRecurrenceData,
|
|
32
|
+
ScheduleViewLevel,
|
|
33
|
+
WeekViewProps,
|
|
34
|
+
YearViewProps,
|
|
35
|
+
} from '@mantine/schedule';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { style } from '@vanilla-extract/css';
|
|
2
|
+
|
|
3
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
4
|
+
|
|
5
|
+
export const quarterGrid = style({
|
|
6
|
+
display: 'grid',
|
|
7
|
+
gridTemplateColumns: 'repeat(4, 1fr)',
|
|
8
|
+
gap: tokens.spacing['2xs'],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const quarterButton = style({
|
|
12
|
+
padding: `${tokens.spacing['2xs']} 0`,
|
|
13
|
+
border: `1px solid ${tokens.color.stroke.subduedDefault}`,
|
|
14
|
+
backgroundColor: tokens.color.background.default,
|
|
15
|
+
borderRadius: tokens.radius.sm,
|
|
16
|
+
cursor: 'pointer',
|
|
17
|
+
color: tokens.color.text.default,
|
|
18
|
+
|
|
19
|
+
':hover': {
|
|
20
|
+
backgroundColor: tokens.color.background.subduedLight,
|
|
21
|
+
},
|
|
22
|
+
':focus-visible': {
|
|
23
|
+
outline: 'none',
|
|
24
|
+
borderColor: tokens.color.stroke.focusDefault,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const quarterButtonSelected = style({
|
|
29
|
+
backgroundColor: tokens.color.background.subduedLight,
|
|
30
|
+
borderColor: tokens.color.stroke.default,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const yearRow = style({
|
|
34
|
+
display: 'flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
gap: tokens.spacing['2xs'],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const yearValue = style({
|
|
40
|
+
flex: 1,
|
|
41
|
+
textAlign: 'center',
|
|
42
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useMemo, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Minus, Plus } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { IconButton } from '../IconButton';
|
|
6
|
+
import { Text } from '../Typography';
|
|
7
|
+
import * as classes from './SchedulePicker.css';
|
|
8
|
+
|
|
9
|
+
type RadioOrientation = 'vertical' | 'horizontal';
|
|
10
|
+
|
|
11
|
+
function handleRadioGroupKeyDown(
|
|
12
|
+
event: ReactKeyboardEvent<HTMLDivElement>,
|
|
13
|
+
orientation: RadioOrientation,
|
|
14
|
+
) {
|
|
15
|
+
const forwardKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
16
|
+
const backwardKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
17
|
+
const { key } = event;
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
key !== forwardKey &&
|
|
21
|
+
key !== backwardKey &&
|
|
22
|
+
key !== 'Home' &&
|
|
23
|
+
key !== 'End'
|
|
24
|
+
) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const container = event.currentTarget;
|
|
29
|
+
const options = Array.from(
|
|
30
|
+
container.querySelectorAll<HTMLElement>('[data-picker-option]'),
|
|
31
|
+
);
|
|
32
|
+
if (options.length === 0) return;
|
|
33
|
+
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
|
|
37
|
+
const active = document.activeElement as HTMLElement | null;
|
|
38
|
+
const currentIndex = active ? options.indexOf(active) : -1;
|
|
39
|
+
|
|
40
|
+
let nextIndex: number;
|
|
41
|
+
if (key === 'Home') {
|
|
42
|
+
nextIndex = 0;
|
|
43
|
+
} else if (key === 'End') {
|
|
44
|
+
nextIndex = options.length - 1;
|
|
45
|
+
} else if (currentIndex === -1) {
|
|
46
|
+
nextIndex = 0;
|
|
47
|
+
} else if (key === forwardKey) {
|
|
48
|
+
nextIndex = (currentIndex + 1) % options.length;
|
|
49
|
+
} else {
|
|
50
|
+
nextIndex = (currentIndex - 1 + options.length) % options.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
options[nextIndex]?.focus();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const YEAR_MIN = 2000;
|
|
57
|
+
const YEAR_MAX = 2100;
|
|
58
|
+
|
|
59
|
+
export interface FiscalQuarterValue {
|
|
60
|
+
year: number;
|
|
61
|
+
quarter: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SchedulePickerProps {
|
|
65
|
+
value: FiscalQuarterValue;
|
|
66
|
+
onChange: (value: FiscalQuarterValue) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function SchedulePicker({ value, onChange }: SchedulePickerProps) {
|
|
70
|
+
const quarters = useMemo(() => [1, 2, 3, 4] as const, []);
|
|
71
|
+
const clampYear = (year: number) =>
|
|
72
|
+
Math.min(YEAR_MAX, Math.max(YEAR_MIN, year));
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<div
|
|
77
|
+
className={classes.quarterGrid}
|
|
78
|
+
role="radiogroup"
|
|
79
|
+
aria-label="Quarter"
|
|
80
|
+
tabIndex={-1}
|
|
81
|
+
onKeyDown={event => handleRadioGroupKeyDown(event, 'horizontal')}
|
|
82
|
+
>
|
|
83
|
+
{quarters.map(q => {
|
|
84
|
+
const selected = value.quarter === q;
|
|
85
|
+
return (
|
|
86
|
+
<button
|
|
87
|
+
key={q}
|
|
88
|
+
type="button"
|
|
89
|
+
role="radio"
|
|
90
|
+
aria-checked={selected}
|
|
91
|
+
data-picker-option
|
|
92
|
+
data-autofocus={selected ? true : undefined}
|
|
93
|
+
className={`${classes.quarterButton} ${
|
|
94
|
+
selected ? classes.quarterButtonSelected : ''
|
|
95
|
+
}`.trim()}
|
|
96
|
+
onClick={() => onChange({ ...value, quarter: q })}
|
|
97
|
+
>
|
|
98
|
+
<Text variant="caption1.strong">Q{q}</Text>
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
<div className={classes.yearRow}>
|
|
104
|
+
<IconButton
|
|
105
|
+
aria-label="Previous year"
|
|
106
|
+
variant="ghost-muted"
|
|
107
|
+
size="xs"
|
|
108
|
+
onClick={() =>
|
|
109
|
+
onChange({ ...value, year: clampYear(value.year - 1) })
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
<Minus size={14} />
|
|
113
|
+
</IconButton>
|
|
114
|
+
<span className={classes.yearValue}>
|
|
115
|
+
<Text variant="body1.strong">{value.year}</Text>
|
|
116
|
+
</span>
|
|
117
|
+
<IconButton
|
|
118
|
+
aria-label="Next year"
|
|
119
|
+
variant="ghost-muted"
|
|
120
|
+
size="xs"
|
|
121
|
+
onClick={() =>
|
|
122
|
+
onChange({ ...value, year: clampYear(value.year + 1) })
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<Plus size={14} />
|
|
126
|
+
</IconButton>
|
|
127
|
+
</div>
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type SearchableListSearchMode = 'client' | 'server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic contract for searchable lists that may be backed by an API.
|
|
5
|
+
* Supports initial loading, error + retry, and infinite scroll pagination.
|
|
6
|
+
*
|
|
7
|
+
* This is intentionally UI-library-agnostic so it can be reused by multiple components.
|
|
8
|
+
*/
|
|
9
|
+
export interface SearchableListAsyncProps {
|
|
10
|
+
/** Whether options are currently loading (initial load). */
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
/** Whether loading options failed. */
|
|
13
|
+
isError?: boolean;
|
|
14
|
+
/** Optional error message to display when `isError` is true. */
|
|
15
|
+
errorMessage?: string;
|
|
16
|
+
/** Retry loading options (shown when `isError` is true). */
|
|
17
|
+
onRetry?: () => void;
|
|
18
|
+
/** Whether more options are available to load (enables infinite scroll). */
|
|
19
|
+
hasMore?: boolean;
|
|
20
|
+
/** Loads the next page of options (triggered by infinite scroll). */
|
|
21
|
+
onLoadMore?: () => void;
|
|
22
|
+
/** Whether the next page is currently loading. */
|
|
23
|
+
isLoadingMore?: boolean;
|
|
24
|
+
/** Search behavior: client filters loaded items; server calls `onSearchChange`. */
|
|
25
|
+
searchMode?: SearchableListSearchMode;
|
|
26
|
+
/** Called when search query changes (primarily for server-side search). */
|
|
27
|
+
onSearchChange?: (query: string) => void;
|
|
28
|
+
/** Debounce time for `onSearchChange` (defaults to 250ms in the UI component). */
|
|
29
|
+
searchDebounceMs?: number;
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { style } from '@vanilla-extract/css';
|
|
2
|
+
|
|
3
|
+
import { mantineVars } from '../../theme/mantineVars';
|
|
4
|
+
|
|
5
|
+
export const menuSearchInput = style({
|
|
6
|
+
border: 'none !important',
|
|
7
|
+
boxShadow: 'none !important',
|
|
8
|
+
backgroundColor: 'transparent !important',
|
|
9
|
+
selectors: {
|
|
10
|
+
'&:focus': {
|
|
11
|
+
border: 'none !important',
|
|
12
|
+
boxShadow: 'none !important',
|
|
13
|
+
outline: 'none !important',
|
|
14
|
+
},
|
|
15
|
+
[`${mantineVars.darkSelector} &`]: {
|
|
16
|
+
border: 'none !important',
|
|
17
|
+
backgroundColor: 'transparent !important',
|
|
18
|
+
},
|
|
19
|
+
[`${mantineVars.darkSelector} &:focus`]: {
|
|
20
|
+
border: 'none !important',
|
|
21
|
+
boxShadow: 'none !important',
|
|
22
|
+
outline: 'none !important',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useCallback, useRef, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Box } from '@mantine/core';
|
|
4
|
+
|
|
5
|
+
import { Menu } from '../Menu';
|
|
6
|
+
import { SearchTextInput } from '../TextInput';
|
|
7
|
+
import * as classes from './SearchableSubMenu.css';
|
|
8
|
+
|
|
9
|
+
export interface SearchableSubMenuProps {
|
|
10
|
+
/** Label shown in the parent menu item */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Search query state */
|
|
13
|
+
search: string;
|
|
14
|
+
/** Search query change handler */
|
|
15
|
+
onSearchChange: (value: string) => void;
|
|
16
|
+
/** Placeholder shown in the search input */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Submenu dropdown width */
|
|
19
|
+
width?: string | number;
|
|
20
|
+
/** Whether to show the search input (defaults to true) */
|
|
21
|
+
showSearch?: boolean;
|
|
22
|
+
/** Main submenu content (items, empty states, etc.) */
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
/** Optional footer render prop (e.g. "Create {search}") */
|
|
25
|
+
renderFooter?: (ctx: { search: string }) => ReactNode;
|
|
26
|
+
/** When set, debounces onSearchChange calls by this many ms while keeping the input responsive */
|
|
27
|
+
debounceMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function SearchableSubMenu({
|
|
31
|
+
label,
|
|
32
|
+
search,
|
|
33
|
+
onSearchChange,
|
|
34
|
+
placeholder = 'Filter',
|
|
35
|
+
width = '300px',
|
|
36
|
+
showSearch = true,
|
|
37
|
+
children,
|
|
38
|
+
renderFooter,
|
|
39
|
+
debounceMs,
|
|
40
|
+
}: SearchableSubMenuProps) {
|
|
41
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
42
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
const setInputNode = useCallback((node: HTMLInputElement | null) => {
|
|
45
|
+
inputRef.current = node;
|
|
46
|
+
|
|
47
|
+
if (!node) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
requestAnimationFrame(() => {
|
|
53
|
+
node.focus();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const focusFirstMenuItem = () => {
|
|
59
|
+
if (!dropdownRef.current) return;
|
|
60
|
+
const menuItems = Array.from(
|
|
61
|
+
dropdownRef.current.querySelectorAll<HTMLButtonElement>(
|
|
62
|
+
'[role="menuitem"]',
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
if (menuItems.length > 0) {
|
|
66
|
+
menuItems[0].focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Menu.Sub>
|
|
72
|
+
<Menu.Sub.Target>
|
|
73
|
+
<Menu.Sub.Item>{label}</Menu.Sub.Item>
|
|
74
|
+
</Menu.Sub.Target>
|
|
75
|
+
<Menu.Sub.Dropdown w={width}>
|
|
76
|
+
<Box
|
|
77
|
+
ref={dropdownRef}
|
|
78
|
+
onClick={e => e.stopPropagation()}
|
|
79
|
+
onMouseDown={e => e.stopPropagation()}
|
|
80
|
+
>
|
|
81
|
+
{showSearch && (
|
|
82
|
+
<>
|
|
83
|
+
<Box
|
|
84
|
+
p={0}
|
|
85
|
+
onClick={e => e.stopPropagation()}
|
|
86
|
+
onMouseDown={e => e.stopPropagation()}
|
|
87
|
+
>
|
|
88
|
+
<SearchTextInput
|
|
89
|
+
ref={setInputNode}
|
|
90
|
+
placeholder={placeholder}
|
|
91
|
+
value={search}
|
|
92
|
+
onChange={e => onSearchChange(e.currentTarget.value)}
|
|
93
|
+
debounceMs={debounceMs}
|
|
94
|
+
size="sm"
|
|
95
|
+
onClick={e => {
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
}}
|
|
98
|
+
onMouseDown={e => {
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
}}
|
|
101
|
+
onKeyDown={e => {
|
|
102
|
+
if (e.key === 'ArrowDown') {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
inputRef.current?.blur();
|
|
106
|
+
requestAnimationFrame(() => {
|
|
107
|
+
focusFirstMenuItem();
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (e.key === 'Escape') {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
}}
|
|
118
|
+
onKeyUp={e => {
|
|
119
|
+
if (e.key !== 'Escape') {
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
onFocus={e => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
}}
|
|
126
|
+
classNames={{ input: classes.menuSearchInput }}
|
|
127
|
+
/>
|
|
128
|
+
</Box>
|
|
129
|
+
<Menu.Divider />
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{children}
|
|
134
|
+
{renderFooter?.({ search })}
|
|
135
|
+
</Box>
|
|
136
|
+
</Menu.Sub.Dropdown>
|
|
137
|
+
</Menu.Sub>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Select Component
|
|
2
|
+
|
|
3
|
+
A custom select/dropdown component matching Figma design specifications. Supports displaying a label prefix in muted color before the selected value.
|
|
4
|
+
|
|
5
|
+
## Design Tokens
|
|
6
|
+
|
|
7
|
+
The Select component uses the following Figma design tokens:
|
|
8
|
+
|
|
9
|
+
- **Padding**: `5.5px` vertical, `var(--semantic-xs)` (8px) horizontal
|
|
10
|
+
- **Border Radius**: `var(--semantic-rounded-lg)` (8px)
|
|
11
|
+
- **Border**: `1px solid var(--color-unofficial-border-3)`
|
|
12
|
+
- **Background**: `var(--color-general-input)`
|
|
13
|
+
- **Shadow**: `var(--mantine-shadow-xs)`
|
|
14
|
+
- **Typography**: Paragraph Small (14px, 21px line-height, 0.07px letter-spacing)
|
|
15
|
+
- **Colors**:
|
|
16
|
+
- Label prefix: `var(--color-neutral-500)` (#737373)
|
|
17
|
+
- Selected value: `var(--color-general-foreground)` (#020617)
|
|
18
|
+
- Chevron icon: `var(--color-neutral-500)`
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### With Label Prefix (Recommended)
|
|
23
|
+
|
|
24
|
+
The label prefix appears in gray color before the selected value, matching the Figma design:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Select } from '@acme/ui';
|
|
28
|
+
|
|
29
|
+
function DepartmentFilter() {
|
|
30
|
+
const [department, setDepartment] = useState('All');
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Select
|
|
34
|
+
labelPrefix="Department"
|
|
35
|
+
data={[
|
|
36
|
+
{ value: 'All', label: 'All' },
|
|
37
|
+
{ value: 'Sales', label: 'Sales' },
|
|
38
|
+
{ value: 'Marketing', label: 'Marketing' },
|
|
39
|
+
]}
|
|
40
|
+
value={department}
|
|
41
|
+
onChange={setDepartment}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Without Label Prefix
|
|
48
|
+
|
|
49
|
+
Standard dropdown without the label prefix:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Select
|
|
53
|
+
data={[
|
|
54
|
+
{ value: 'all', label: 'All Departments' },
|
|
55
|
+
{ value: 'sales', label: 'Sales' },
|
|
56
|
+
]}
|
|
57
|
+
value={value}
|
|
58
|
+
onChange={setValue}
|
|
59
|
+
placeholder="Select a department"
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Props
|
|
64
|
+
|
|
65
|
+
| Prop | Type | Required | Description |
|
|
66
|
+
|------|------|----------|-------------|
|
|
67
|
+
| `labelPrefix` | `string` | No | Label text to display in gray before the selected value (e.g., "Department") |
|
|
68
|
+
| `data` | `Array<{ value: string; label: string }>` | Yes | Array of options with value and label |
|
|
69
|
+
| `value` | `string \| null` | No | Currently selected value |
|
|
70
|
+
| `onChange` | `(value: string \| null) => void` | No | Callback when selection changes |
|
|
71
|
+
| `placeholder` | `string` | No | Placeholder text when no value is selected |
|
|
72
|
+
|
|
73
|
+
## Figma Reference
|
|
74
|
+
|
|
75
|
+
This component is based on the Figma design:
|
|
76
|
+
- **Component**: Select & Combobox
|
|
77
|
+
- **Node ID**: 108:16040
|
|
78
|
+
- **Design File**: AMM
|
|
79
|
+
|
|
80
|
+
## Design Specifications
|
|
81
|
+
|
|
82
|
+
### Layout
|
|
83
|
+
- Height: 36px
|
|
84
|
+
- Padding: 5.5px (top/bottom), 8px (left/right)
|
|
85
|
+
- Gap between label prefix and value: 6px
|
|
86
|
+
|
|
87
|
+
### Typography
|
|
88
|
+
- Font family: Inter (from design system)
|
|
89
|
+
- Font size: 14px (Paragraph Small)
|
|
90
|
+
- Line height: 21px
|
|
91
|
+
- Letter spacing: 0.07px
|
|
92
|
+
- Font weight: Normal (400)
|
|
93
|
+
|
|
94
|
+
### Colors
|
|
95
|
+
- Label prefix: Neutral/500 (#737373)
|
|
96
|
+
- Selected value: General/Foreground (#020617)
|
|
97
|
+
- Border: Unofficial/Border-3 (#cbd5e1)
|
|
98
|
+
- Background: General/Input (white)
|
|
99
|
+
- Chevron: Neutral/500 (#737373)
|
|
100
|
+
|
|
101
|
+
### Effects
|
|
102
|
+
- Shadow: xs (0px 1px 2px 0px rgba(0,0,0,0.05))
|
|
103
|
+
- Border radius: lg (8px)
|
|
104
|
+
|
|
105
|
+
## Implementation Notes
|
|
106
|
+
|
|
107
|
+
- Built on Mantine's Combobox primitive for flexible rendering
|
|
108
|
+
- Uses `InputBase` as the trigger for custom content display
|
|
109
|
+
- Supports keyboard navigation and accessibility
|
|
110
|
+
- Chevron icon from `lucide-react`
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
See `Select.story.tsx` for interactive examples in Storybook.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select component styles – vanilla-extract with semantic design tokens
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { style } from '@vanilla-extract/css';
|
|
6
|
+
|
|
7
|
+
import { mantineVars } from '../../theme/mantineVars';
|
|
8
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
9
|
+
|
|
10
|
+
const focusRing = {
|
|
11
|
+
outline: `2px solid ${tokens.color.stroke.focusStrong}`,
|
|
12
|
+
outlineOffset: 2,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const selectButton = style({
|
|
16
|
+
all: 'unset',
|
|
17
|
+
boxSizing: 'border-box',
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'space-between',
|
|
21
|
+
width: '100%',
|
|
22
|
+
minHeight: 36,
|
|
23
|
+
height: 36,
|
|
24
|
+
padding: '5.5px 8px',
|
|
25
|
+
gap: 6,
|
|
26
|
+
borderRadius: 8,
|
|
27
|
+
border: `1px solid ${tokens.color.stroke.default}`,
|
|
28
|
+
background: tokens.color.background.input,
|
|
29
|
+
boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)',
|
|
30
|
+
fontFamily: 'inherit',
|
|
31
|
+
fontSize: 14,
|
|
32
|
+
lineHeight: '21px',
|
|
33
|
+
fontWeight: 400,
|
|
34
|
+
letterSpacing: '0.07px',
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
transition: 'border-color 0.15s ease',
|
|
38
|
+
selectors: {
|
|
39
|
+
'&:hover': {
|
|
40
|
+
borderColor: tokens.color.stroke.subduedStrong,
|
|
41
|
+
},
|
|
42
|
+
'&:focus': focusRing,
|
|
43
|
+
'&:active': {
|
|
44
|
+
borderColor: tokens.color.stroke.strong,
|
|
45
|
+
},
|
|
46
|
+
[`${mantineVars.darkSelector} &`]: {
|
|
47
|
+
borderColor: tokens.color.stroke.default,
|
|
48
|
+
backgroundColor: tokens.color.background.default,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const valueContainer = style({
|
|
54
|
+
display: 'flex',
|
|
55
|
+
gap: 6,
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
flex: 1,
|
|
58
|
+
minWidth: 0,
|
|
59
|
+
overflow: 'hidden',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const labelPrefix = style({
|
|
63
|
+
color: tokens.color.text.subduedDefault,
|
|
64
|
+
fontSize: 14,
|
|
65
|
+
lineHeight: '21px',
|
|
66
|
+
fontWeight: 400,
|
|
67
|
+
letterSpacing: '0.07px',
|
|
68
|
+
whiteSpace: 'nowrap',
|
|
69
|
+
flexShrink: 0,
|
|
70
|
+
selectors: {
|
|
71
|
+
[`${mantineVars.darkSelector} &`]: {
|
|
72
|
+
color: tokens.color.text.default,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const selectedValue = style({
|
|
78
|
+
color: tokens.color.text.default,
|
|
79
|
+
fontSize: 14,
|
|
80
|
+
lineHeight: '21px',
|
|
81
|
+
fontWeight: 400,
|
|
82
|
+
letterSpacing: '0.07px',
|
|
83
|
+
whiteSpace: 'nowrap',
|
|
84
|
+
flexShrink: 0,
|
|
85
|
+
selectors: {
|
|
86
|
+
[`${mantineVars.darkSelector} &`]: {
|
|
87
|
+
color: tokens.color.text.default,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const chevron = style({
|
|
93
|
+
color: tokens.color.text.subduedDefault,
|
|
94
|
+
flexShrink: 0,
|
|
95
|
+
width: 16,
|
|
96
|
+
height: 16,
|
|
97
|
+
selectors: {
|
|
98
|
+
[`${mantineVars.darkSelector} &`]: {
|
|
99
|
+
color: tokens.color.text.default,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const errorText = style({
|
|
105
|
+
color: tokens.color.text.dangerDefault,
|
|
106
|
+
fontSize: 12,
|
|
107
|
+
lineHeight: '18px',
|
|
108
|
+
fontWeight: 400,
|
|
109
|
+
marginTop: 4,
|
|
110
|
+
});
|