@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,261 @@
|
|
|
1
|
+
import { globalStyle, style } from '@vanilla-extract/css';
|
|
2
|
+
|
|
3
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared prose styles for Tiptap / ProseMirror content. Applied by both
|
|
7
|
+
* `SlashRichTextEditor` (edit mode) and `RichTextView` (read-only mode)
|
|
8
|
+
* so the writing surface and the rendered view look pixel-identical.
|
|
9
|
+
*
|
|
10
|
+
* Tiptap wraps editable content in a `.ProseMirror` element and the
|
|
11
|
+
* read-only view renders the same HTML directly under the wrapper, so
|
|
12
|
+
* each rule below targets either path:
|
|
13
|
+
*
|
|
14
|
+
* `${prose} h1` matches both the editor's
|
|
15
|
+
* `<div class=prose><div class=ProseMirror><h1>` and the view's
|
|
16
|
+
* `<div class=prose><h1>`.
|
|
17
|
+
*/
|
|
18
|
+
export const prose = style({
|
|
19
|
+
color: tokens.color.text.default,
|
|
20
|
+
fontSize: '0.875rem',
|
|
21
|
+
lineHeight: 1.5,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
globalStyle(`${prose} [contenteditable="true"]`, {
|
|
25
|
+
outline: 'none',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
globalStyle(`${prose} .ProseMirror`, {
|
|
29
|
+
outline: 'none',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/* Only the editable surface needs a tall click target — the read-only
|
|
33
|
+
* `RichTextView` shares the `.ProseMirror` wrapper but should hug its
|
|
34
|
+
* content so list rows and detail headers don't gain ~4rem of empty
|
|
35
|
+
* space under short rich-text descriptions. */
|
|
36
|
+
globalStyle(`${prose} .ProseMirror[contenteditable="true"]`, {
|
|
37
|
+
minHeight: '4rem',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
41
|
+
* Block-level elements
|
|
42
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
globalStyle(`${prose} p`, {
|
|
45
|
+
margin: 0,
|
|
46
|
+
lineHeight: 1.5,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
globalStyle(`${prose} p + p`, {
|
|
50
|
+
marginTop: tokens.spacing['2xs'],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
globalStyle(`${prose} h1, ${prose} h2, ${prose} h3`, {
|
|
54
|
+
margin: 0,
|
|
55
|
+
fontWeight: 700,
|
|
56
|
+
lineHeight: 1.3,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
globalStyle(`${prose} h1`, {
|
|
60
|
+
fontSize: '1.25rem',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
globalStyle(`${prose} h2`, {
|
|
64
|
+
fontSize: '1.125rem',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
globalStyle(`${prose} h3`, {
|
|
68
|
+
fontSize: '1rem',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/* Headings that follow other content get a small top margin so they
|
|
72
|
+
* read as a new section. Matches the editor's writing rhythm. */
|
|
73
|
+
globalStyle(`${prose} * + h1, ${prose} * + h2, ${prose} * + h3`, {
|
|
74
|
+
marginTop: tokens.spacing.xs,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
78
|
+
* Lists — `list-style-type` is required because Mantine's CSS reset
|
|
79
|
+
* strips it. Bullets/numbers must be explicit so they survive.
|
|
80
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
globalStyle(`${prose} ul, ${prose} ol`, {
|
|
83
|
+
margin: 0,
|
|
84
|
+
paddingLeft: tokens.spacing.lg,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
globalStyle(`${prose} ul`, {
|
|
88
|
+
listStyleType: 'disc',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
globalStyle(`${prose} ol`, {
|
|
92
|
+
listStyleType: 'decimal',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
globalStyle(`${prose} li`, {
|
|
96
|
+
margin: 0,
|
|
97
|
+
paddingLeft: tokens.spacing['3xs'],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
globalStyle(`${prose} li + li`, {
|
|
101
|
+
marginTop: tokens.spacing['3xs'],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/* Tiptap wraps each list item's content in a `<p>` — collapse that
|
|
105
|
+
* paragraph's margin so list items don't gain a second baseline. */
|
|
106
|
+
globalStyle(`${prose} li > p`, {
|
|
107
|
+
margin: 0,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/* Lists adjacent to body content need a touch of breathing room so
|
|
111
|
+
* the bullets don't bump up against a paragraph above them. */
|
|
112
|
+
globalStyle(
|
|
113
|
+
`${prose} p + ul, ${prose} p + ol, ${prose} ul + p, ${prose} ol + p`,
|
|
114
|
+
{
|
|
115
|
+
marginTop: tokens.spacing['2xs'],
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
120
|
+
* Blockquote, code, links
|
|
121
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
122
|
+
|
|
123
|
+
globalStyle(`${prose} blockquote`, {
|
|
124
|
+
margin: 0,
|
|
125
|
+
paddingLeft: tokens.spacing.sm,
|
|
126
|
+
borderLeft: `2px solid ${tokens.color.stroke.subduedDefault}`,
|
|
127
|
+
color: tokens.color.text.subduedStrong,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
globalStyle(`${prose} blockquote > p`, {
|
|
131
|
+
margin: 0,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
globalStyle(`${prose} code`, {
|
|
135
|
+
fontFamily: 'var(--font-family-monospace)',
|
|
136
|
+
background: tokens.color.background.subduedLight,
|
|
137
|
+
padding: '0 4px',
|
|
138
|
+
borderRadius: tokens.radius.xs,
|
|
139
|
+
fontSize: '0.95em',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
globalStyle(`${prose} pre`, {
|
|
143
|
+
margin: 0,
|
|
144
|
+
padding: tokens.spacing.sm,
|
|
145
|
+
borderRadius: tokens.radius.sm,
|
|
146
|
+
background: tokens.color.background.subduedLight,
|
|
147
|
+
overflow: 'auto',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
globalStyle(`${prose} pre > code`, {
|
|
151
|
+
background: 'transparent',
|
|
152
|
+
padding: 0,
|
|
153
|
+
borderRadius: 0,
|
|
154
|
+
fontSize: '0.95em',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
globalStyle(`${prose} a`, {
|
|
158
|
+
color: tokens.color.text.primaryDefault,
|
|
159
|
+
textDecoration: 'underline',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
globalStyle(`${prose} hr`, {
|
|
163
|
+
margin: `${tokens.spacing.sm} 0`,
|
|
164
|
+
border: 'none',
|
|
165
|
+
borderTop: `1px solid ${tokens.color.stroke.subduedDefault}`,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
169
|
+
* Inline marks — these have browser defaults, but Mantine's reset can
|
|
170
|
+
* normalise them away. Re-state them so bold/italic/underline always
|
|
171
|
+
* survive the document round-trip.
|
|
172
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
173
|
+
|
|
174
|
+
globalStyle(`${prose} strong, ${prose} b`, {
|
|
175
|
+
fontWeight: 700,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
globalStyle(`${prose} em, ${prose} i`, {
|
|
179
|
+
fontStyle: 'italic',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
globalStyle(`${prose} u`, {
|
|
183
|
+
textDecoration: 'underline',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
globalStyle(`${prose} s, ${prose} del`, {
|
|
187
|
+
textDecoration: 'line-through',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
191
|
+
* Placeholder — Tiptap's `@tiptap/extension-placeholder` toggles
|
|
192
|
+
* `is-editor-empty` on the first child when the doc has no content.
|
|
193
|
+
* Editor-only behaviour, but lives here so the editor can drop its
|
|
194
|
+
* own duplicated copy.
|
|
195
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
196
|
+
|
|
197
|
+
globalStyle(`${prose} .ProseMirror p.is-editor-empty:first-child::before`, {
|
|
198
|
+
content: 'attr(data-placeholder)',
|
|
199
|
+
color: tokens.color.text.subduedDefault,
|
|
200
|
+
pointerEvents: 'none',
|
|
201
|
+
height: 0,
|
|
202
|
+
float: 'left',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
206
|
+
* Read-only collapse — when the surface isn't editable (i.e.
|
|
207
|
+
* `RichTextView`), collapse runs of empty paragraphs so legacy docs
|
|
208
|
+
* (Beast's plain-text-to-JSON converter emits one `<p></p>` per
|
|
209
|
+
* `\n\n`) read as the user-intended content rather than as a wall of
|
|
210
|
+
* whitespace. The editor leaves them alone so the cursor still has a
|
|
211
|
+
* line to live on while typing.
|
|
212
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
213
|
+
|
|
214
|
+
globalStyle(`${prose} .ProseMirror[contenteditable="false"] p:empty`, {
|
|
215
|
+
display: 'none',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/* `<p><br></p>` is the other empty-paragraph shape Tiptap can emit
|
|
219
|
+
* (used as a hard-break placeholder). Drop those too in read-only
|
|
220
|
+
* mode so the trailing line-height doesn't bloat the layout. */
|
|
221
|
+
globalStyle(
|
|
222
|
+
`${prose} .ProseMirror[contenteditable="false"] p:has(> br:only-child)`,
|
|
223
|
+
{
|
|
224
|
+
display: 'none',
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
229
|
+
* Image nodes — uploaded images sit in the doc with an empty `src`
|
|
230
|
+
* and a `data-rich-text-file-id` attribute until the lazy loader
|
|
231
|
+
* resolves them (see `useRichTextImageLoader`). The states below
|
|
232
|
+
* keep the placeholder readable while loading and give the layout a
|
|
233
|
+
* stable footprint so other content doesn't reflow as images come
|
|
234
|
+
* in.
|
|
235
|
+
* ────────────────────────────────────────────────────────────────── */
|
|
236
|
+
|
|
237
|
+
globalStyle(`${prose} img.rich-text-image`, {
|
|
238
|
+
maxWidth: '100%',
|
|
239
|
+
height: 'auto',
|
|
240
|
+
borderRadius: tokens.radius.md,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
globalStyle(
|
|
244
|
+
`${prose} img.rich-text-image[data-rich-text-image-state="loading"], ${prose} img.rich-text-image[data-rich-text-image-state="idle"]`,
|
|
245
|
+
{
|
|
246
|
+
minHeight: '4rem',
|
|
247
|
+
width: '100%',
|
|
248
|
+
backgroundColor: tokens.color.background.subduedLight,
|
|
249
|
+
border: `1px dashed ${tokens.color.stroke.subduedDefault}`,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
globalStyle(
|
|
254
|
+
`${prose} img.rich-text-image[data-rich-text-image-state="error"]`,
|
|
255
|
+
{
|
|
256
|
+
minHeight: '4rem',
|
|
257
|
+
width: '100%',
|
|
258
|
+
backgroundColor: tokens.color.background.dangerLight,
|
|
259
|
+
border: `1px dashed ${tokens.color.stroke.dangerDefault}`,
|
|
260
|
+
},
|
|
261
|
+
);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { globalStyle, style } from '@vanilla-extract/css';
|
|
2
|
+
|
|
3
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
4
|
+
|
|
5
|
+
// Prose styles (<p>, <ul>, <ol>, <a>) are in ../RichText/richTextProse.css.ts
|
|
6
|
+
// so they can be shared by RichTextView and EditableRichText.
|
|
7
|
+
|
|
8
|
+
export const root = style({
|
|
9
|
+
border: `1px solid ${tokens.color.stroke.default}`,
|
|
10
|
+
borderRadius: tokens.radius.lg,
|
|
11
|
+
background: tokens.color.background.input,
|
|
12
|
+
overflow: 'hidden',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const toolbar = style({
|
|
16
|
+
display: 'flex',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
gap: tokens.spacing.xs,
|
|
19
|
+
padding: tokens.spacing.sm,
|
|
20
|
+
borderBottom: `1px solid ${tokens.color.stroke.subduedDefault}`,
|
|
21
|
+
background: tokens.color.background.subduedUltralight,
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const editor = style({
|
|
26
|
+
minHeight: '9.5rem',
|
|
27
|
+
padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
|
|
28
|
+
color: tokens.color.text.default,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const actions = style({
|
|
32
|
+
display: 'flex',
|
|
33
|
+
justifyContent: 'space-between',
|
|
34
|
+
alignItems: 'flex-end',
|
|
35
|
+
gap: tokens.spacing.sm,
|
|
36
|
+
padding: tokens.spacing.sm,
|
|
37
|
+
borderTop: `1px solid ${tokens.color.stroke.subduedDefault}`,
|
|
38
|
+
background: tokens.color.background.default,
|
|
39
|
+
flexWrap: 'wrap',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const actionMeta = style({
|
|
43
|
+
display: 'flex',
|
|
44
|
+
flexDirection: 'column',
|
|
45
|
+
gap: tokens.spacing.xs,
|
|
46
|
+
minWidth: 0,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const keyboardHint = style({
|
|
50
|
+
color: tokens.color.text.subduedDefault,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const errorRow = style({
|
|
54
|
+
display: 'flex',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
gap: tokens.spacing.sm,
|
|
57
|
+
flexWrap: 'wrap',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const actionButtons = style({
|
|
61
|
+
display: 'flex',
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
gap: tokens.spacing.xs,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const srOnlyTextarea = style({
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
width: 1,
|
|
69
|
+
height: 1,
|
|
70
|
+
padding: 0,
|
|
71
|
+
margin: -1,
|
|
72
|
+
overflow: 'hidden',
|
|
73
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
74
|
+
whiteSpace: 'nowrap',
|
|
75
|
+
border: 0,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Keep the modal-editor's taller empty-state minimum so single-line edits
|
|
79
|
+
// still present a large, obvious input target.
|
|
80
|
+
globalStyle(`${editor} [contenteditable="true"]`, {
|
|
81
|
+
minHeight: '7rem',
|
|
82
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useEffect, useMemo, type ChangeEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
import { EditorContent, useEditor, type JSONContent } from '@tiptap/react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '../Button';
|
|
7
|
+
import { IconButton } from '../IconButton';
|
|
8
|
+
import { STANDARD_FORMATTING_COMMANDS } from '../RichText/formattingCommands';
|
|
9
|
+
import { FormattingToolbar } from '../RichText/FormattingToolbar';
|
|
10
|
+
import { getRichTextExtensions } from '../RichText/richTextExtensions';
|
|
11
|
+
import {
|
|
12
|
+
docsEqual,
|
|
13
|
+
normalizeValue,
|
|
14
|
+
plainTextFromDoc,
|
|
15
|
+
richTextDocFromPlainText,
|
|
16
|
+
} from '../RichText/richTextHelpers';
|
|
17
|
+
import {
|
|
18
|
+
installRichTextImageHandlers,
|
|
19
|
+
useRichTextImageLoader,
|
|
20
|
+
type RichTextImageEditorConfig,
|
|
21
|
+
} from '../RichText/richTextImageHandlers';
|
|
22
|
+
import * as proseStyles from '../RichText/richTextProse.css';
|
|
23
|
+
import { Text } from '../Typography';
|
|
24
|
+
import * as styles from './RichTextEditor.css';
|
|
25
|
+
|
|
26
|
+
export interface RichTextEditorProps {
|
|
27
|
+
value: JSONContent | null;
|
|
28
|
+
onChange: (value: JSONContent, plainText: string) => void;
|
|
29
|
+
onSubmit: () => void;
|
|
30
|
+
onCancel: () => void;
|
|
31
|
+
label: string;
|
|
32
|
+
submitLabel?: string;
|
|
33
|
+
submitDisabled?: boolean;
|
|
34
|
+
cancelDisabled?: boolean;
|
|
35
|
+
submitLoading?: boolean;
|
|
36
|
+
submitError?: string | null;
|
|
37
|
+
onSubmitErrorRetry?: () => void;
|
|
38
|
+
submitErrorRetryDisabled?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* When provided, enables inline image upload via paste / drop. Pass
|
|
42
|
+
* `undefined` (the default) on surfaces where images shouldn't be
|
|
43
|
+
* allowed — the Image extension is omitted from the schema entirely so
|
|
44
|
+
* users can't even paste one. See
|
|
45
|
+
* {@link RichTextImageEditorConfig} for the full callback surface.
|
|
46
|
+
*/
|
|
47
|
+
image?: RichTextImageEditorConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Form-style rich-text editor with explicit Save / Cancel buttons,
|
|
52
|
+
* Cmd/Ctrl+Enter submit, Esc cancel, and a built-in submit-error row with
|
|
53
|
+
* retry. Built on the canonical `RichText` foundation (shared extensions,
|
|
54
|
+
* shared `FormattingToolbar`) so a doc authored here round-trips through
|
|
55
|
+
* `RichTextView` without losing marks.
|
|
56
|
+
*/
|
|
57
|
+
export function RichTextEditor({
|
|
58
|
+
value,
|
|
59
|
+
onChange,
|
|
60
|
+
onSubmit,
|
|
61
|
+
onCancel,
|
|
62
|
+
label,
|
|
63
|
+
submitLabel = 'Save',
|
|
64
|
+
submitDisabled = false,
|
|
65
|
+
cancelDisabled = false,
|
|
66
|
+
submitLoading = false,
|
|
67
|
+
submitError = null,
|
|
68
|
+
onSubmitErrorRetry,
|
|
69
|
+
submitErrorRetryDisabled = false,
|
|
70
|
+
disabled = false,
|
|
71
|
+
image,
|
|
72
|
+
}: RichTextEditorProps) {
|
|
73
|
+
const normalizedValue = useMemo(() => normalizeValue(value), [value]);
|
|
74
|
+
const enableImages = Boolean(image);
|
|
75
|
+
const editor = useEditor(
|
|
76
|
+
{
|
|
77
|
+
immediatelyRender: false,
|
|
78
|
+
editable: !disabled,
|
|
79
|
+
extensions: getRichTextExtensions({ enableImages }),
|
|
80
|
+
content: normalizedValue,
|
|
81
|
+
editorProps: {
|
|
82
|
+
handleKeyDown: (_view, event) => {
|
|
83
|
+
if (event.key === 'Escape') {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
onCancel();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
if (!submitDisabled) {
|
|
92
|
+
onSubmit();
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
...installRichTextImageHandlers(image),
|
|
100
|
+
},
|
|
101
|
+
onUpdate: ({ editor: currentEditor }) => {
|
|
102
|
+
onChange(currentEditor.getJSON(), currentEditor.getText());
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
// Remount when `disabled` changes (Tiptap doesn't have a runtime
|
|
106
|
+
// setter for `editable`) or when image-config presence toggles (the
|
|
107
|
+
// schema gains/loses the Image extension). The image callbacks
|
|
108
|
+
// themselves are captured at install time, so swapping them without
|
|
109
|
+
// toggling `enableImages` doesn't rebuild the editor.
|
|
110
|
+
[disabled, enableImages],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
useRichTextImageLoader(editor, image);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!editor) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const current = editor.getJSON();
|
|
121
|
+
if (!docsEqual(current, normalizedValue)) {
|
|
122
|
+
editor.commands.setContent(normalizedValue, { emitUpdate: false });
|
|
123
|
+
}
|
|
124
|
+
}, [editor, normalizedValue]);
|
|
125
|
+
|
|
126
|
+
if (!editor) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className={styles.root}>
|
|
132
|
+
<textarea
|
|
133
|
+
aria-label={label}
|
|
134
|
+
className={styles.srOnlyTextarea}
|
|
135
|
+
value={plainTextFromDoc(normalizedValue)}
|
|
136
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
137
|
+
editor.commands.setContent(
|
|
138
|
+
richTextDocFromPlainText(event.target.value),
|
|
139
|
+
{ emitUpdate: true },
|
|
140
|
+
);
|
|
141
|
+
}}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
/>
|
|
144
|
+
<FormattingToolbar
|
|
145
|
+
editor={editor}
|
|
146
|
+
commands={STANDARD_FORMATTING_COMMANDS}
|
|
147
|
+
className={styles.toolbar}
|
|
148
|
+
ariaLabel={`${label} formatting`}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<div className={`${styles.editor} ${proseStyles.prose}`}>
|
|
152
|
+
<EditorContent editor={editor} />
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className={styles.actions}>
|
|
156
|
+
<div className={styles.actionMeta}>
|
|
157
|
+
{submitError ? (
|
|
158
|
+
<div className={styles.errorRow}>
|
|
159
|
+
<Text variant="caption1" c="text.danger.default">
|
|
160
|
+
{submitError}
|
|
161
|
+
</Text>
|
|
162
|
+
{onSubmitErrorRetry ? (
|
|
163
|
+
<Button
|
|
164
|
+
type="button"
|
|
165
|
+
size="xs"
|
|
166
|
+
variant="outline"
|
|
167
|
+
onClick={onSubmitErrorRetry}
|
|
168
|
+
disabled={submitErrorRetryDisabled}
|
|
169
|
+
>
|
|
170
|
+
Retry
|
|
171
|
+
</Button>
|
|
172
|
+
) : null}
|
|
173
|
+
</div>
|
|
174
|
+
) : null}
|
|
175
|
+
<Text variant="caption1" className={styles.keyboardHint}>
|
|
176
|
+
Cmd/Ctrl+Enter to submit
|
|
177
|
+
</Text>
|
|
178
|
+
</div>
|
|
179
|
+
<div className={styles.actionButtons}>
|
|
180
|
+
<IconButton
|
|
181
|
+
type="button"
|
|
182
|
+
variant="ghost"
|
|
183
|
+
size="sm"
|
|
184
|
+
aria-label="Cancel"
|
|
185
|
+
onClick={onCancel}
|
|
186
|
+
disabled={cancelDisabled}
|
|
187
|
+
>
|
|
188
|
+
<X size={16} />
|
|
189
|
+
</IconButton>
|
|
190
|
+
<Button
|
|
191
|
+
type="button"
|
|
192
|
+
variant="primary"
|
|
193
|
+
size="xs"
|
|
194
|
+
onClick={onSubmit}
|
|
195
|
+
disabled={submitDisabled}
|
|
196
|
+
loading={submitLoading}
|
|
197
|
+
>
|
|
198
|
+
{submitLabel}
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { style } from '@vanilla-extract/css';
|
|
2
|
+
|
|
3
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
4
|
+
|
|
5
|
+
export const root = style({
|
|
6
|
+
color: tokens.color.text.default,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const empty = style({
|
|
10
|
+
color: tokens.color.text.subduedDefault,
|
|
11
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RichTextView
|
|
3
|
+
*
|
|
4
|
+
* Read-only renderer for Tiptap / ProseMirror `JSONContent`. Produces the
|
|
5
|
+
* same prose styling as `RichTextEditor` (via the shared prose class) so
|
|
6
|
+
* display and edit modes look identical.
|
|
7
|
+
*
|
|
8
|
+
* Renders a `placeholder` string in a muted style when the document is
|
|
9
|
+
* empty (no non-whitespace text). Pass `null` for `value` to render the
|
|
10
|
+
* placeholder.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <RichTextView value={doc} placeholder="No summary yet." />
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useMemo } from 'react';
|
|
17
|
+
|
|
18
|
+
import { EditorContent, type JSONContent, useEditor } from '@tiptap/react';
|
|
19
|
+
|
|
20
|
+
import { getRichTextExtensions } from '../RichText/richTextExtensions';
|
|
21
|
+
import {
|
|
22
|
+
docsEqual,
|
|
23
|
+
isDocEmpty,
|
|
24
|
+
normalizeValue,
|
|
25
|
+
} from '../RichText/richTextHelpers';
|
|
26
|
+
import {
|
|
27
|
+
useRichTextImageLoader,
|
|
28
|
+
type RichTextImageEditorConfig,
|
|
29
|
+
} from '../RichText/richTextImageHandlers';
|
|
30
|
+
import * as proseStyles from '../RichText/richTextProse.css';
|
|
31
|
+
import { Text } from '../Typography';
|
|
32
|
+
import * as styles from './RichTextView.css';
|
|
33
|
+
|
|
34
|
+
export interface RichTextViewProps {
|
|
35
|
+
/** Document to render. `null` is treated as empty. */
|
|
36
|
+
value: JSONContent | null;
|
|
37
|
+
/** Text shown (muted) when the document has no visible content. */
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** Optional className for the outer wrapper. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/**
|
|
42
|
+
* When provided, enables the Image node in the schema so docs that
|
|
43
|
+
* carry images render faithfully. Pass `loadImage` to resolve
|
|
44
|
+
* `data-rich-text-file-id`s into real `src`s; pass `autoLoad: false`
|
|
45
|
+
* (the default) on surfaces that render many viewers at once (e.g.
|
|
46
|
+
* Assessments) so they don't fan out one request per section. The
|
|
47
|
+
* returned `loadImages()` handle isn't exposed here — for explicit
|
|
48
|
+
* lazy-loading from this surface use `EditableRichText` or wrap your
|
|
49
|
+
* own observer that calls back into a shared loader.
|
|
50
|
+
*/
|
|
51
|
+
image?: RichTextImageEditorConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function RichTextView({
|
|
55
|
+
value,
|
|
56
|
+
placeholder,
|
|
57
|
+
className,
|
|
58
|
+
image,
|
|
59
|
+
}: RichTextViewProps) {
|
|
60
|
+
const normalizedValue = useMemo(() => normalizeValue(value), [value]);
|
|
61
|
+
const enableImages = Boolean(image);
|
|
62
|
+
|
|
63
|
+
const editor = useEditor(
|
|
64
|
+
{
|
|
65
|
+
immediatelyRender: false,
|
|
66
|
+
editable: false,
|
|
67
|
+
// `editable: false` flips Link.openOnClick on so anchor clicks navigate
|
|
68
|
+
// when rendered as a view. Same canonical extension set as every editor
|
|
69
|
+
// surface — that's what guarantees a doc renders identically here as it
|
|
70
|
+
// edits in `RichTextEditor`, `EditableRichText`, or `SlashRichTextEditor`.
|
|
71
|
+
extensions: getRichTextExtensions({ editable: false, enableImages }),
|
|
72
|
+
content: normalizedValue,
|
|
73
|
+
},
|
|
74
|
+
[enableImages],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useRichTextImageLoader(editor, image);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!editor) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const current = editor.getJSON();
|
|
85
|
+
if (!docsEqual(current, normalizedValue)) {
|
|
86
|
+
editor.commands.setContent(normalizedValue, { emitUpdate: false });
|
|
87
|
+
}
|
|
88
|
+
}, [editor, normalizedValue]);
|
|
89
|
+
|
|
90
|
+
if (isDocEmpty(value) && placeholder) {
|
|
91
|
+
return (
|
|
92
|
+
<Text
|
|
93
|
+
variant="body1"
|
|
94
|
+
className={[styles.empty, className].filter(Boolean).join(' ')}
|
|
95
|
+
>
|
|
96
|
+
{placeholder}
|
|
97
|
+
</Text>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!editor) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className={[styles.root, proseStyles.prose, className]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' ')}
|
|
110
|
+
>
|
|
111
|
+
<EditorContent editor={editor} />
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|