@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,538 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
EditorContent,
|
|
13
|
+
type Editor,
|
|
14
|
+
type Range,
|
|
15
|
+
useEditor,
|
|
16
|
+
type JSONContent,
|
|
17
|
+
} from '@tiptap/react';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
resolveFormattingCommands,
|
|
21
|
+
type FormattingToolbarVariant,
|
|
22
|
+
} from '../RichText/formattingCommands';
|
|
23
|
+
import { getRichTextExtensions } from '../RichText/richTextExtensions';
|
|
24
|
+
import {
|
|
25
|
+
docsEqual,
|
|
26
|
+
isDocEmpty,
|
|
27
|
+
normalizeValue,
|
|
28
|
+
} from '../RichText/richTextHelpers';
|
|
29
|
+
import {
|
|
30
|
+
installRichTextImageHandlers,
|
|
31
|
+
useRichTextImageLoader,
|
|
32
|
+
type RichTextImageEditorConfig,
|
|
33
|
+
} from '../RichText/richTextImageHandlers';
|
|
34
|
+
import * as proseStyles from '../RichText/richTextProse.css';
|
|
35
|
+
import { Text } from '../Typography';
|
|
36
|
+
import { SelectionBubbleMenu } from './SelectionBubbleMenu';
|
|
37
|
+
import { SlashCommandMenu } from './SlashCommandMenu';
|
|
38
|
+
import * as classes from './SlashRichTextEditor.css';
|
|
39
|
+
import { SlashSuggestionExtension } from './SlashSuggestionExtension';
|
|
40
|
+
|
|
41
|
+
import type { SlashCommand, SlashSuggestionState } from './types';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Imperative handle for callers that need to focus / blur the editor or
|
|
45
|
+
* inspect the current plain text without re-rendering on every keystroke.
|
|
46
|
+
*/
|
|
47
|
+
export interface SlashRichTextEditorHandle {
|
|
48
|
+
focus: () => void;
|
|
49
|
+
blur: () => void;
|
|
50
|
+
getPlainText: () => string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SlashRichTextEditorProps {
|
|
54
|
+
value: JSONContent | null;
|
|
55
|
+
onChange: (next: JSONContent) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Plain-text mirror — fired alongside `onChange` so callers using the doc
|
|
58
|
+
* for search / validation don't need to re-derive it.
|
|
59
|
+
*/
|
|
60
|
+
onPlainTextChange?: (text: string) => void;
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
autoFocus?: boolean;
|
|
63
|
+
/** Custom field commands appended below the formatting toolbar. */
|
|
64
|
+
extraCommands?: SlashCommand[];
|
|
65
|
+
/** Toggles the built-in formatting toolbar layout. Defaults to 'standard'. */
|
|
66
|
+
formattingToolbar?: FormattingToolbarVariant;
|
|
67
|
+
onSubmit?: () => void;
|
|
68
|
+
onCancel?: () => void;
|
|
69
|
+
/**
|
|
70
|
+
* When true, plain `Enter` (no shift, no menu open) submits the form,
|
|
71
|
+
* matching the legacy `<textarea>` behavior. `Shift+Enter` always inserts
|
|
72
|
+
* a newline and `Cmd/Ctrl+Enter` always submits regardless of this flag.
|
|
73
|
+
* Defaults to false so the editor behaves like a normal multi-line input.
|
|
74
|
+
*/
|
|
75
|
+
submitOnEnter?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Mirrors the legacy `<textarea>` `maxLength` so consumers can keep their
|
|
78
|
+
* existing length validation. Compared against `editor.getText()`.
|
|
79
|
+
*/
|
|
80
|
+
maxLengthPlainText?: number;
|
|
81
|
+
errorMessage?: string;
|
|
82
|
+
className?: string;
|
|
83
|
+
ariaLabel?: string;
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Visual chrome around the editor surface.
|
|
87
|
+
*
|
|
88
|
+
* - `seamless` (default): no border / background / padding so the editor
|
|
89
|
+
* inherits whatever container the consumer wraps it in (e.g. the
|
|
90
|
+
* green-bordered Active draft card in `ActionItemQuickCreate`).
|
|
91
|
+
* - `bordered`: opt-in input-style box with focus ring, useful when the
|
|
92
|
+
* editor stands on its own.
|
|
93
|
+
*/
|
|
94
|
+
variant?: 'seamless' | 'bordered';
|
|
95
|
+
/**
|
|
96
|
+
* When provided, enables inline image upload via paste / drop. See
|
|
97
|
+
* {@link RichTextImageEditorConfig} for the full callback surface.
|
|
98
|
+
*/
|
|
99
|
+
image?: RichTextImageEditorConfig;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Filter the consumer's `extraCommands` by the current slash query.
|
|
104
|
+
* Matches the legacy `filterSlashFields` contract (case-insensitive
|
|
105
|
+
* substring on the label).
|
|
106
|
+
*/
|
|
107
|
+
function filterCommandsByQuery(
|
|
108
|
+
commands: SlashCommand[],
|
|
109
|
+
query: string,
|
|
110
|
+
): SlashCommand[] {
|
|
111
|
+
const normalized = query.trim().toLowerCase();
|
|
112
|
+
if (!normalized) return commands;
|
|
113
|
+
return commands.filter(c => c.label.toLowerCase().includes(normalized));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Tiptap-based replacement for the action item / initiative `<textarea>`s.
|
|
118
|
+
* Renders a single contenteditable surface plus a popup that opens on `/` and
|
|
119
|
+
* exposes both formatting commands (slim icon toolbar) and consumer-provided
|
|
120
|
+
* field commands (filterable list). Markdown shortcuts work via StarterKit's
|
|
121
|
+
* built-in input rules so most users never need to open the menu.
|
|
122
|
+
*/
|
|
123
|
+
export const SlashRichTextEditor = forwardRef<
|
|
124
|
+
SlashRichTextEditorHandle,
|
|
125
|
+
SlashRichTextEditorProps
|
|
126
|
+
>(
|
|
127
|
+
(
|
|
128
|
+
{
|
|
129
|
+
value,
|
|
130
|
+
onChange,
|
|
131
|
+
onPlainTextChange,
|
|
132
|
+
placeholder,
|
|
133
|
+
autoFocus = false,
|
|
134
|
+
extraCommands = [],
|
|
135
|
+
formattingToolbar = 'standard',
|
|
136
|
+
onSubmit,
|
|
137
|
+
onCancel,
|
|
138
|
+
submitOnEnter = false,
|
|
139
|
+
maxLengthPlainText,
|
|
140
|
+
errorMessage,
|
|
141
|
+
className,
|
|
142
|
+
ariaLabel,
|
|
143
|
+
disabled = false,
|
|
144
|
+
variant = 'seamless',
|
|
145
|
+
image,
|
|
146
|
+
},
|
|
147
|
+
ref,
|
|
148
|
+
) => {
|
|
149
|
+
const enableImages = Boolean(image);
|
|
150
|
+
/**
|
|
151
|
+
* The slash plugin's render lifecycle is non-React, so we mirror the
|
|
152
|
+
* relevant pieces into React state and re-render the popup whenever the
|
|
153
|
+
* suggestion plugin tells us the query / range changed.
|
|
154
|
+
*/
|
|
155
|
+
const [suggestion, setSuggestion] = useState<SlashSuggestionState | null>(
|
|
156
|
+
null,
|
|
157
|
+
);
|
|
158
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
159
|
+
const [focused, setFocused] = useState(false);
|
|
160
|
+
|
|
161
|
+
// Refs let the suggestion plugin (which is created once) reach the latest
|
|
162
|
+
// commands + highlight state without re-instantiating Tiptap on every render.
|
|
163
|
+
const suggestionRef = useRef<SlashSuggestionState | null>(suggestion);
|
|
164
|
+
suggestionRef.current = suggestion;
|
|
165
|
+
|
|
166
|
+
const extraCommandsRef = useRef<SlashCommand[]>(extraCommands);
|
|
167
|
+
extraCommandsRef.current = extraCommands;
|
|
168
|
+
|
|
169
|
+
const highlightedRef = useRef(highlightedIndex);
|
|
170
|
+
highlightedRef.current = highlightedIndex;
|
|
171
|
+
|
|
172
|
+
const visibleCommandsRef = useRef<SlashCommand[]>([]);
|
|
173
|
+
|
|
174
|
+
const onSubmitRef = useRef(onSubmit);
|
|
175
|
+
onSubmitRef.current = onSubmit;
|
|
176
|
+
|
|
177
|
+
const onCancelRef = useRef(onCancel);
|
|
178
|
+
onCancelRef.current = onCancel;
|
|
179
|
+
|
|
180
|
+
const submitOnEnterRef = useRef(submitOnEnter);
|
|
181
|
+
submitOnEnterRef.current = submitOnEnter;
|
|
182
|
+
|
|
183
|
+
const closeSuggestion = useCallback(() => {
|
|
184
|
+
setSuggestion(null);
|
|
185
|
+
setHighlightedIndex(0);
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
const runCommand = useCallback(
|
|
189
|
+
(cmd: SlashCommand, currentRange: Range, editor: Editor) => {
|
|
190
|
+
if (cmd.disabled) return;
|
|
191
|
+
// Drop the `/<query>` token first so the consumer doesn't have to.
|
|
192
|
+
editor.chain().focus().deleteRange(currentRange).run();
|
|
193
|
+
cmd.run({ editor, range: currentRange });
|
|
194
|
+
// Hand control back to suggestion's exit() so its plugin state stays
|
|
195
|
+
// consistent. Falling through to setSuggestion(null) is a no-op if the
|
|
196
|
+
// exit handler already cleared us.
|
|
197
|
+
suggestionRef.current?.exit?.();
|
|
198
|
+
closeSuggestion();
|
|
199
|
+
},
|
|
200
|
+
[closeSuggestion],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const toolbarCommands = useMemo(
|
|
204
|
+
() => resolveFormattingCommands(formattingToolbar),
|
|
205
|
+
[formattingToolbar],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Stable ref the suggestion plugin can ask for "what's currently visible?"
|
|
209
|
+
const filterAndCacheVisibleCommands = useCallback((query: string) => {
|
|
210
|
+
const filtered = filterCommandsByQuery(extraCommandsRef.current, query);
|
|
211
|
+
visibleCommandsRef.current = filtered;
|
|
212
|
+
return filtered;
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
// Build the suggestion config once. We avoid passing closures over React
|
|
216
|
+
// state — the plugin would otherwise capture stale values. Refs +
|
|
217
|
+
// imperative setters keep the wiring stable across renders.
|
|
218
|
+
const suggestionOptions = useMemo(
|
|
219
|
+
() => ({
|
|
220
|
+
char: '/',
|
|
221
|
+
allowSpaces: false,
|
|
222
|
+
allowedPrefixes: [' ', '\n', '\t'],
|
|
223
|
+
startOfLine: false,
|
|
224
|
+
items: ({ query }: { query: string }) => {
|
|
225
|
+
return filterAndCacheVisibleCommands(query).map(c => c.id);
|
|
226
|
+
},
|
|
227
|
+
render: () => {
|
|
228
|
+
let cachedRange: Range | null = null;
|
|
229
|
+
let cachedExit: () => void = () => {};
|
|
230
|
+
return {
|
|
231
|
+
onStart: (props: {
|
|
232
|
+
query: string;
|
|
233
|
+
range: Range;
|
|
234
|
+
command: (item: unknown) => void;
|
|
235
|
+
clientRect?: (() => DOMRect | null) | null;
|
|
236
|
+
}) => {
|
|
237
|
+
cachedRange = props.range;
|
|
238
|
+
cachedExit = () => {
|
|
239
|
+
setSuggestion(null);
|
|
240
|
+
setHighlightedIndex(0);
|
|
241
|
+
};
|
|
242
|
+
setHighlightedIndex(0);
|
|
243
|
+
setSuggestion({
|
|
244
|
+
query: props.query,
|
|
245
|
+
range: props.range,
|
|
246
|
+
exit: cachedExit,
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
onUpdate: (props: { query: string; range: Range }) => {
|
|
250
|
+
cachedRange = props.range;
|
|
251
|
+
setSuggestion(prev =>
|
|
252
|
+
prev
|
|
253
|
+
? { ...prev, query: props.query, range: props.range }
|
|
254
|
+
: {
|
|
255
|
+
query: props.query,
|
|
256
|
+
range: props.range,
|
|
257
|
+
exit: cachedExit,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
setHighlightedIndex(0);
|
|
261
|
+
},
|
|
262
|
+
onKeyDown: (props: { event: KeyboardEvent }) => {
|
|
263
|
+
const { event } = props;
|
|
264
|
+
const visible = visibleCommandsRef.current;
|
|
265
|
+
if (event.key === 'ArrowDown') {
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
setHighlightedIndex(idx =>
|
|
268
|
+
visible.length === 0 ? 0 : (idx + 1) % visible.length,
|
|
269
|
+
);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (event.key === 'ArrowUp') {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
setHighlightedIndex(idx =>
|
|
275
|
+
visible.length === 0
|
|
276
|
+
? 0
|
|
277
|
+
: (idx - 1 + visible.length) % visible.length,
|
|
278
|
+
);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (event.key === 'Enter') {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
const visibleNow = visibleCommandsRef.current;
|
|
284
|
+
const target = visibleNow[highlightedRef.current];
|
|
285
|
+
if (target && cachedRange) {
|
|
286
|
+
runCommandFromKeyboardRef.current(target, cachedRange);
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (event.key === 'Escape') {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
cachedExit();
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
},
|
|
297
|
+
onExit: () => {
|
|
298
|
+
cachedRange = null;
|
|
299
|
+
setSuggestion(null);
|
|
300
|
+
setHighlightedIndex(0);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
command: () => {
|
|
305
|
+
// No-op: command application happens through `runCommand` when the
|
|
306
|
+
// user clicks / hits Enter. The suggestion plugin still calls this
|
|
307
|
+
// when a programmatic command callback is requested.
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
[filterAndCacheVisibleCommands],
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Imperative bridge from the suggestion's keyboard handler back to React,
|
|
314
|
+
// so we don't need to thread the editor through the closure. The ref
|
|
315
|
+
// wrapper keeps `useMemo` deps stable while still letting us pull the
|
|
316
|
+
// latest `runCommand` closure on every keystroke.
|
|
317
|
+
const editorRef = useRef<Editor | null>(null);
|
|
318
|
+
const runCommandFromKeyboardRef = useRef<
|
|
319
|
+
(cmd: SlashCommand, currentRange: Range) => void
|
|
320
|
+
>(() => {});
|
|
321
|
+
runCommandFromKeyboardRef.current = (
|
|
322
|
+
cmd: SlashCommand,
|
|
323
|
+
currentRange: Range,
|
|
324
|
+
) => {
|
|
325
|
+
const editor = editorRef.current;
|
|
326
|
+
if (!editor) return;
|
|
327
|
+
runCommand(cmd, currentRange, editor);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const imageHandlers = useMemo(
|
|
331
|
+
() => installRichTextImageHandlers(image),
|
|
332
|
+
[image],
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const editor = useEditor(
|
|
336
|
+
{
|
|
337
|
+
immediatelyRender: false,
|
|
338
|
+
editable: !disabled,
|
|
339
|
+
extensions: [
|
|
340
|
+
...getRichTextExtensions({ placeholder, enableImages }),
|
|
341
|
+
SlashSuggestionExtension.configure({ suggestion: suggestionOptions }),
|
|
342
|
+
],
|
|
343
|
+
content: normalizeValue(value),
|
|
344
|
+
autofocus: autoFocus ? 'end' : false,
|
|
345
|
+
editorProps: {
|
|
346
|
+
attributes: ariaLabel ? { 'aria-label': ariaLabel } : {},
|
|
347
|
+
...imageHandlers,
|
|
348
|
+
handleKeyDown: (_view, event) => {
|
|
349
|
+
// Suggestion plugin gets first crack — only handle global shortcuts
|
|
350
|
+
// when the menu is closed.
|
|
351
|
+
if (suggestionRef.current) return false;
|
|
352
|
+
|
|
353
|
+
if (event.key === 'Escape') {
|
|
354
|
+
if (onCancelRef.current) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
onCancelRef.current();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
event.key === 'Enter' &&
|
|
364
|
+
!event.shiftKey &&
|
|
365
|
+
(event.metaKey || event.ctrlKey)
|
|
366
|
+
) {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
onSubmitRef.current?.();
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (
|
|
373
|
+
submitOnEnterRef.current &&
|
|
374
|
+
event.key === 'Enter' &&
|
|
375
|
+
!event.shiftKey &&
|
|
376
|
+
!event.metaKey &&
|
|
377
|
+
!event.ctrlKey &&
|
|
378
|
+
!event.altKey
|
|
379
|
+
) {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
onSubmitRef.current?.();
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return false;
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
onUpdate: ({ editor: current }) => {
|
|
389
|
+
const next = current.getJSON();
|
|
390
|
+
const plainText = current.getText();
|
|
391
|
+
if (
|
|
392
|
+
maxLengthPlainText != null &&
|
|
393
|
+
plainText.length > maxLengthPlainText
|
|
394
|
+
) {
|
|
395
|
+
// Trim the overflow by deleting the trailing characters in a
|
|
396
|
+
// single ProseMirror transaction. Doing it this way preserves
|
|
397
|
+
// every mark / node / link the user had typed up to the limit —
|
|
398
|
+
// a blanket `setContent` rebuild would flatten the doc back to a
|
|
399
|
+
// single plain paragraph.
|
|
400
|
+
//
|
|
401
|
+
// We compute how many plain-text characters need to drop and
|
|
402
|
+
// walk that many positions backward from doc end. ProseMirror
|
|
403
|
+
// positions are 1-based and include node boundaries, but
|
|
404
|
+
// `delete(from, end)` collapses any non-text positions it
|
|
405
|
+
// crosses automatically, so the conservative "delete the last
|
|
406
|
+
// N positions" approach lands the doc at exactly the limit.
|
|
407
|
+
//
|
|
408
|
+
// Consumers that care about strict enforcement should also apply
|
|
409
|
+
// a server-side check.
|
|
410
|
+
const excess = plainText.length - maxLengthPlainText;
|
|
411
|
+
const { state } = current;
|
|
412
|
+
const end = state.doc.content.size;
|
|
413
|
+
const deleteFrom = Math.max(0, end - excess);
|
|
414
|
+
current.view.dispatch(state.tr.delete(deleteFrom, end));
|
|
415
|
+
onChange(current.getJSON());
|
|
416
|
+
onPlainTextChange?.(current.getText());
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
onChange(next);
|
|
420
|
+
onPlainTextChange?.(plainText);
|
|
421
|
+
},
|
|
422
|
+
onFocus: () => setFocused(true),
|
|
423
|
+
onBlur: () => setFocused(false),
|
|
424
|
+
},
|
|
425
|
+
// Remount only when `disabled` changes (Tiptap doesn't have a runtime
|
|
426
|
+
// setter for it) or when image support is toggled on/off (since that
|
|
427
|
+
// changes the schema). The image callbacks themselves are captured
|
|
428
|
+
// through `imageHandlers` so swapping `image.uploadImage` doesn't
|
|
429
|
+
// rebuild the editor.
|
|
430
|
+
[disabled, enableImages],
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
useRichTextImageLoader(editor, image);
|
|
434
|
+
|
|
435
|
+
editorRef.current = editor;
|
|
436
|
+
|
|
437
|
+
// Keep the editor in sync when the parent replaces the value (e.g. after a
|
|
438
|
+
// successful submit clears the form).
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!editor) return;
|
|
441
|
+
const next = normalizeValue(value);
|
|
442
|
+
if (!docsEqual(editor.getJSON(), next)) {
|
|
443
|
+
editor.commands.setContent(next, { emitUpdate: false });
|
|
444
|
+
}
|
|
445
|
+
}, [editor, value]);
|
|
446
|
+
|
|
447
|
+
// Update the Placeholder extension at runtime so a changing `placeholder`
|
|
448
|
+
// prop doesn't have to remount the editor (which would reset selection
|
|
449
|
+
// and scroll). The extension is added unconditionally by
|
|
450
|
+
// `getRichTextExtensions`, so the lookup will always find it.
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
if (!editor) return;
|
|
453
|
+
const placeholderExt = editor.extensionManager.extensions.find(
|
|
454
|
+
ext => ext.name === 'placeholder',
|
|
455
|
+
);
|
|
456
|
+
if (!placeholderExt) return;
|
|
457
|
+
if (placeholderExt.options.placeholder === (placeholder ?? '')) return;
|
|
458
|
+
placeholderExt.options.placeholder = placeholder ?? '';
|
|
459
|
+
// Force a redraw so the empty-editor placeholder pseudo updates.
|
|
460
|
+
editor.view.dispatch(editor.state.tr);
|
|
461
|
+
}, [editor, placeholder]);
|
|
462
|
+
|
|
463
|
+
useImperativeHandle(
|
|
464
|
+
ref,
|
|
465
|
+
() => ({
|
|
466
|
+
focus: () => editor?.commands.focus(),
|
|
467
|
+
blur: () => editor?.commands.blur(),
|
|
468
|
+
getPlainText: () => editor?.getText() ?? '',
|
|
469
|
+
}),
|
|
470
|
+
[editor],
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const visibleCommands = useMemo(
|
|
474
|
+
() => filterCommandsByQuery(extraCommands, suggestion?.query ?? ''),
|
|
475
|
+
[extraCommands, suggestion?.query],
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Mirror the visible list into the ref so the suggestion plugin's keyboard
|
|
479
|
+
// handler sees the same array React just rendered.
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
visibleCommandsRef.current = visibleCommands;
|
|
482
|
+
}, [visibleCommands]);
|
|
483
|
+
|
|
484
|
+
if (!editor) return null;
|
|
485
|
+
|
|
486
|
+
const showError = Boolean(errorMessage);
|
|
487
|
+
const isEmpty = isDocEmpty(editor.getJSON());
|
|
488
|
+
|
|
489
|
+
const surfaceClass = [
|
|
490
|
+
classes.editorSurface,
|
|
491
|
+
proseStyles.prose,
|
|
492
|
+
variant === 'bordered' ? classes.editorSurfaceBordered : undefined,
|
|
493
|
+
]
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.join(' ');
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<div className={[classes.root, className].filter(Boolean).join(' ')}>
|
|
499
|
+
<div
|
|
500
|
+
className={surfaceClass}
|
|
501
|
+
data-focused={focused ? 'true' : undefined}
|
|
502
|
+
data-error={showError ? 'true' : undefined}
|
|
503
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
504
|
+
data-empty={isEmpty ? 'true' : undefined}
|
|
505
|
+
>
|
|
506
|
+
<EditorContent editor={editor} />
|
|
507
|
+
<SelectionBubbleMenu
|
|
508
|
+
editor={editor}
|
|
509
|
+
hidden={Boolean(suggestion) || disabled}
|
|
510
|
+
/>
|
|
511
|
+
{suggestion ? (
|
|
512
|
+
<div className={`${classes.menuLayer} ${classes.menuLayerActive}`}>
|
|
513
|
+
<SlashCommandMenu
|
|
514
|
+
editor={editor}
|
|
515
|
+
range={suggestion.range}
|
|
516
|
+
query={suggestion.query}
|
|
517
|
+
commands={visibleCommands}
|
|
518
|
+
toolbarCommands={toolbarCommands}
|
|
519
|
+
highlightedIndex={highlightedIndex}
|
|
520
|
+
onHighlightChange={setHighlightedIndex}
|
|
521
|
+
onSelect={cmd => runCommand(cmd, suggestion.range, editor)}
|
|
522
|
+
onVisibleCommandsChange={() => {
|
|
523
|
+
// No-op: highlight reset is driven by suggestion onUpdate.
|
|
524
|
+
}}
|
|
525
|
+
ariaLabel={ariaLabel ? `${ariaLabel} commands` : undefined}
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
) : null}
|
|
529
|
+
</div>
|
|
530
|
+
{showError ? (
|
|
531
|
+
<Text variant="caption2" className={classes.errorMessage}>
|
|
532
|
+
{errorMessage}
|
|
533
|
+
</Text>
|
|
534
|
+
) : null}
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
},
|
|
538
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/react';
|
|
2
|
+
import Suggestion, {
|
|
3
|
+
SuggestionPluginKey,
|
|
4
|
+
type SuggestionOptions,
|
|
5
|
+
} from '@tiptap/suggestion';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tiptap extension that wires `@tiptap/suggestion` to the `/` trigger so
|
|
9
|
+
* `SlashRichTextEditor` can render its own popup. The extension itself stays
|
|
10
|
+
* thin — the actual `items`, `render`, and `command` callbacks are supplied by
|
|
11
|
+
* the React component via `configure({ suggestion })`. Splitting the wiring
|
|
12
|
+
* here keeps the editor file focused on state management and lets unit tests
|
|
13
|
+
* import the extension in isolation.
|
|
14
|
+
*/
|
|
15
|
+
export interface SlashSuggestionOptions {
|
|
16
|
+
suggestion: Omit<SuggestionOptions, 'editor'>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const SlashSuggestionExtension =
|
|
20
|
+
Extension.create<SlashSuggestionOptions>({
|
|
21
|
+
name: 'slashSuggestion',
|
|
22
|
+
addOptions() {
|
|
23
|
+
return {
|
|
24
|
+
suggestion: {
|
|
25
|
+
char: '/',
|
|
26
|
+
allowSpaces: false,
|
|
27
|
+
// Trigger only at start-of-line or after whitespace so `foo/bar`
|
|
28
|
+
// never opens the menu — matches the legacy `findActiveSlashAnchor`
|
|
29
|
+
// contract that consumers were already relying on.
|
|
30
|
+
allowedPrefixes: [' ', '\n', '\t'],
|
|
31
|
+
startOfLine: false,
|
|
32
|
+
items: () => [],
|
|
33
|
+
render: () => ({}),
|
|
34
|
+
command: () => {},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
addProseMirrorPlugins() {
|
|
39
|
+
return [
|
|
40
|
+
Suggestion({
|
|
41
|
+
editor: this.editor,
|
|
42
|
+
...this.options.suggestion,
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export { SuggestionPluginKey };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-specific public surface. The shared toolbar / formatting commands /
|
|
3
|
+
* extension set live in `../RichText` and are re-exported from the package
|
|
4
|
+
* root via `RichText`'s barrel — this module only exposes the slash
|
|
5
|
+
* editor itself plus its slash-specific extension and types.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
SlashRichTextEditor,
|
|
9
|
+
type SlashRichTextEditorHandle,
|
|
10
|
+
type SlashRichTextEditorProps,
|
|
11
|
+
} from './SlashRichTextEditor';
|
|
12
|
+
export { SlashSuggestionExtension } from './SlashSuggestionExtension';
|
|
13
|
+
export type { SlashCommand, SlashSuggestionState } from './types';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Editor, Range } from '@tiptap/react';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single entry in the slash menu's command list (the lower section of
|
|
6
|
+
* the Option B layout). Formatting commands rendered in the toolbar are
|
|
7
|
+
* described separately via `FormattingToolbarCommand` (in
|
|
8
|
+
* `RichText/formattingTypes.ts`) because they don't participate in the
|
|
9
|
+
* filterable list.
|
|
10
|
+
*/
|
|
11
|
+
export interface SlashCommand {
|
|
12
|
+
/** Stable id for keyboard nav and React keys. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Display label rendered in the row. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Lucide icon shown to the left of the label. */
|
|
17
|
+
icon: LucideIcon;
|
|
18
|
+
/** Short keyboard hint shown on the right (e.g. `C`, `D`). */
|
|
19
|
+
shortcut?: string;
|
|
20
|
+
/** Section header label used to bucket the row in the list. */
|
|
21
|
+
group: string;
|
|
22
|
+
/** Optional helper text — currently unused but reserved for future UX. */
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Greyed out and unselectable if true. */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** Replaces the shortcut chip on disabled rows. */
|
|
27
|
+
disabledHint?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Called when the user selects the row. The component drops the `/<query>`
|
|
30
|
+
* token before invoking this so consumers don't have to think about the
|
|
31
|
+
* slash anchor — they just open their flyout / mutate the doc.
|
|
32
|
+
*/
|
|
33
|
+
run: (ctx: { editor: Editor; range: Range }) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shape stored in the React state while the slash suggestion is active.
|
|
38
|
+
* Drives the menu UI (open/close, query, range, command callback).
|
|
39
|
+
*/
|
|
40
|
+
export interface SlashSuggestionState {
|
|
41
|
+
query: string;
|
|
42
|
+
range: Range;
|
|
43
|
+
/**
|
|
44
|
+
* The exit callback registered by `@tiptap/suggestion`. We invoke this when
|
|
45
|
+
* the user picks something so the plugin can clean up its internal state.
|
|
46
|
+
*/
|
|
47
|
+
exit: () => void;
|
|
48
|
+
}
|