@olympusoss/canvas 2.20.2 → 4.0.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/package.json +41 -177
- package/src/cn.ts +3 -0
- package/src/index.ts +12 -603
- package/src/theme.ts +41 -0
- package/src/tokens.ts +11 -0
- package/styles/base.css +17 -0
- package/styles/canvas.css +69 -52
- package/styles/components/alert.css +66 -0
- package/styles/components/app-shell.css +46 -0
- package/styles/components/avatar.css +15 -0
- package/styles/components/badge.css +83 -0
- package/styles/components/breadcrumb.css +35 -0
- package/styles/components/button-group.css +23 -0
- package/styles/components/button.css +107 -0
- package/styles/components/calendar.css +73 -0
- package/styles/components/card.css +58 -0
- package/styles/components/checkbox.css +55 -0
- package/styles/components/code-block.css +18 -0
- package/styles/components/combobox.css +75 -0
- package/styles/components/command.css +94 -0
- package/styles/components/data-table.css +142 -0
- package/styles/components/dialog.css +72 -0
- package/styles/components/dropdown.css +54 -0
- package/styles/components/empty-state.css +17 -0
- package/styles/components/field.css +27 -0
- package/styles/components/filter-panel.css +58 -0
- package/styles/components/form.css +27 -0
- package/styles/components/icon.css +8 -0
- package/styles/components/input-group.css +45 -0
- package/styles/components/input.css +56 -0
- package/styles/components/kbd.css +15 -0
- package/styles/components/page-header.css +52 -0
- package/styles/components/pagination.css +48 -0
- package/styles/components/popover.css +14 -0
- package/styles/components/radio.css +28 -0
- package/styles/components/row-menu.css +69 -0
- package/styles/components/section-card.css +49 -0
- package/styles/components/select.css +57 -0
- package/styles/components/separator.css +32 -0
- package/styles/components/sheet.css +70 -0
- package/styles/components/sidebar.css +146 -0
- package/styles/components/skeleton.css +32 -0
- package/styles/components/spinner.css +26 -0
- package/styles/components/stat-card.css +71 -0
- package/styles/components/stepper.css +63 -0
- package/styles/components/switch.css +45 -0
- package/styles/components/tabs.css +40 -0
- package/styles/components/textarea.css +31 -0
- package/styles/components/toast.css +95 -0
- package/styles/components/tooltip.css +53 -0
- package/styles/components/topbar.css +24 -0
- package/styles/components/typography.css +105 -0
- package/styles/patterns/backdrops.css +35 -0
- package/styles/patterns/density.css +66 -0
- package/styles/patterns/focus.css +38 -0
- package/styles/patterns/glass.css +85 -0
- package/styles/patterns/high-contrast.css +70 -0
- package/styles/patterns/reduced-motion.css +12 -0
- package/styles/patterns/scrollbar.css +10 -0
- package/styles/reset.css +89 -0
- package/styles/tokens/colors.css +106 -0
- package/styles/tokens/motion.css +33 -0
- package/styles/tokens/radius.css +10 -0
- package/styles/tokens/shadows.css +35 -0
- package/styles/tokens/spacing.css +19 -0
- package/styles/tokens/typography.css +6 -0
- package/styles/tokens/z-index.css +12 -0
- package/tsconfig.json +20 -21
- package/README.md +0 -60
- package/src/components/atoms/README.md +0 -11
- package/src/components/atoms/aspect-ratio.tsx +0 -32
- package/src/components/atoms/avatar.tsx +0 -98
- package/src/components/atoms/badge.tsx +0 -44
- package/src/components/atoms/brand-mark.tsx +0 -74
- package/src/components/atoms/button.tsx +0 -105
- package/src/components/atoms/checkbox.tsx +0 -63
- package/src/components/atoms/flex-box.tsx +0 -105
- package/src/components/atoms/icon.tsx +0 -34
- package/src/components/atoms/input.tsx +0 -92
- package/src/components/atoms/label.tsx +0 -41
- package/src/components/atoms/logo.tsx +0 -89
- package/src/components/atoms/progress.tsx +0 -55
- package/src/components/atoms/radio-group.tsx +0 -122
- package/src/components/atoms/scroll-area.tsx +0 -106
- package/src/components/atoms/section.tsx +0 -48
- package/src/components/atoms/separator.tsx +0 -45
- package/src/components/atoms/skeleton.tsx +0 -17
- package/src/components/atoms/slider.tsx +0 -93
- package/src/components/atoms/spinner.tsx +0 -47
- package/src/components/atoms/switch.tsx +0 -60
- package/src/components/atoms/textarea.tsx +0 -78
- package/src/components/atoms/toggle.tsx +0 -80
- package/src/components/charts/activity-heatmap.tsx +0 -186
- package/src/components/charts/axes.tsx +0 -21
- package/src/components/charts/chart-container.tsx +0 -254
- package/src/components/charts/chart-legend.tsx +0 -67
- package/src/components/charts/chart-tooltip.tsx +0 -161
- package/src/components/charts/chart-types.tsx +0 -49
- package/src/components/charts/containers.tsx +0 -11
- package/src/components/charts/data.tsx +0 -16
- package/src/components/charts/details.tsx +0 -25
- package/src/components/charts/dot-pulse.tsx +0 -61
- package/src/components/charts/gauge.tsx +0 -106
- package/src/components/charts/grids.tsx +0 -8
- package/src/components/charts/index.ts +0 -62
- package/src/components/charts/labeled-bar-list.tsx +0 -85
- package/src/components/charts/metric-breakdown.tsx +0 -316
- package/src/components/charts/references.tsx +0 -8
- package/src/components/charts/service-health-list.tsx +0 -85
- package/src/components/charts/sparkline-area.tsx +0 -80
- package/src/components/charts/sparkline.tsx +0 -52
- package/src/components/charts/stacked-bar.tsx +0 -104
- package/src/components/charts/text.tsx +0 -10
- package/src/components/charts/world-heat-map-inner.tsx +0 -317
- package/src/components/charts/world-heat-map.tsx +0 -184
- package/src/components/molecules/README.md +0 -12
- package/src/components/molecules/action-bar.tsx +0 -73
- package/src/components/molecules/activity-item.tsx +0 -74
- package/src/components/molecules/alert.tsx +0 -86
- package/src/components/molecules/animated-background.tsx +0 -92
- package/src/components/molecules/auth-shell.tsx +0 -95
- package/src/components/molecules/brand-lockup.tsx +0 -48
- package/src/components/molecules/breadcrumb.tsx +0 -157
- package/src/components/molecules/button-group.tsx +0 -104
- package/src/components/molecules/calendar.tsx +0 -217
- package/src/components/molecules/card.tsx +0 -102
- package/src/components/molecules/client-brand.tsx +0 -95
- package/src/components/molecules/code-block.tsx +0 -86
- package/src/components/molecules/countdown-button.tsx +0 -92
- package/src/components/molecules/empty-state.tsx +0 -56
- package/src/components/molecules/error-state.tsx +0 -42
- package/src/components/molecules/field-display.tsx +0 -35
- package/src/components/molecules/input-otp.tsx +0 -74
- package/src/components/molecules/launcher-card.tsx +0 -152
- package/src/components/molecules/loading-state.tsx +0 -36
- package/src/components/molecules/notification-item.tsx +0 -67
- package/src/components/molecules/notification-list.tsx +0 -45
- package/src/components/molecules/number-badge.tsx +0 -53
- package/src/components/molecules/or-separator.tsx +0 -38
- package/src/components/molecules/page-header.tsx +0 -88
- package/src/components/molecules/page-tabs.tsx +0 -94
- package/src/components/molecules/pagination.tsx +0 -150
- package/src/components/molecules/password-input.tsx +0 -83
- package/src/components/molecules/password-strength-meter.tsx +0 -104
- package/src/components/molecules/phone-input.tsx +0 -200
- package/src/components/molecules/search-bar.tsx +0 -64
- package/src/components/molecules/secret-field.tsx +0 -158
- package/src/components/molecules/section-card.tsx +0 -91
- package/src/components/molecules/social-buttons.tsx +0 -165
- package/src/components/molecules/stat-card.tsx +0 -100
- package/src/components/molecules/status-badge.tsx +0 -42
- package/src/components/molecules/stepper.tsx +0 -96
- package/src/components/molecules/table.tsx +0 -157
- package/src/components/molecules/terminal.tsx +0 -74
- package/src/components/molecules/toggle-group.tsx +0 -145
- package/src/components/molecules/tooltip.tsx +0 -155
- package/src/components/molecules/user-avatar-chip.tsx +0 -71
- package/src/components/organisms/README.md +0 -14
- package/src/components/organisms/accordion.tsx +0 -154
- package/src/components/organisms/alert-dialog.tsx +0 -277
- package/src/components/organisms/carousel.tsx +0 -244
- package/src/components/organisms/collapsible.tsx +0 -69
- package/src/components/organisms/command.tsx +0 -144
- package/src/components/organisms/context-menu.tsx +0 -339
- package/src/components/organisms/dashboard-grid.tsx +0 -369
- package/src/components/organisms/data-table.tsx +0 -330
- package/src/components/organisms/dialog.tsx +0 -312
- package/src/components/organisms/drawer.tsx +0 -123
- package/src/components/organisms/dropdown-menu.tsx +0 -440
- package/src/components/organisms/editors/code-editor.tsx +0 -144
- package/src/components/organisms/editors/index.ts +0 -4
- package/src/components/organisms/editors/markdown-editor.tsx +0 -153
- package/src/components/organisms/editors/markdown-renderer.ts +0 -27
- package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
- package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
- package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
- package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
- package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
- package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
- package/src/components/organisms/error-boundary.tsx +0 -61
- package/src/components/organisms/form.tsx +0 -174
- package/src/components/organisms/hover-card.tsx +0 -115
- package/src/components/organisms/menubar.tsx +0 -498
- package/src/components/organisms/navbar.tsx +0 -104
- package/src/components/organisms/navigation-menu.tsx +0 -235
- package/src/components/organisms/popover.tsx +0 -149
- package/src/components/organisms/resizable.tsx +0 -58
- package/src/components/organisms/schema-form.tsx +0 -232
- package/src/components/organisms/select.tsx +0 -309
- package/src/components/organisms/sheet.tsx +0 -265
- package/src/components/organisms/sidebar.tsx +0 -1040
- package/src/components/organisms/sonner.tsx +0 -96
- package/src/components/organisms/tabs.tsx +0 -133
- package/src/components/organisms/theme-provider.tsx +0 -101
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/lib/portal-container.tsx +0 -35
- package/src/lib/utils.ts +0 -6
- package/src/native.ts +0 -23
- package/src/tokens/colors.ts +0 -91
- package/src/tokens/index.ts +0 -3
- package/src/tokens/spacing.ts +0 -55
- package/src/tokens/typography.ts +0 -27
- package/styles/dashboard-grid.css +0 -47
- package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
- package/styles/glass.css +0 -175
- package/styles/leaflet.css +0 -13
- package/styles/tokens.css +0 -317
- package/tailwind.config.ts +0 -70
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/* c8 ignore file -- CodeMirror requires real DOM measurements; jsdom-incompatible.
|
|
4
|
-
* Verified visually in the docs site. */
|
|
5
|
-
|
|
6
|
-
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
7
|
-
import { markdown } from "@codemirror/lang-markdown";
|
|
8
|
-
import { EditorState } from "@codemirror/state";
|
|
9
|
-
import { EditorView, keymap, placeholder as placeholderExt } from "@codemirror/view";
|
|
10
|
-
import * as React from "react";
|
|
11
|
-
|
|
12
|
-
import { cn } from "../../../lib/utils";
|
|
13
|
-
import { renderMarkdown } from "./markdown-renderer";
|
|
14
|
-
import { PROSE_CANVAS_CLASSES } from "./prose-canvas-classes";
|
|
15
|
-
import { MdToolbar } from "./toolbar/md-toolbar";
|
|
16
|
-
import { useCodemirrorTheme } from "./use-codemirror-theme";
|
|
17
|
-
|
|
18
|
-
export interface MarkdownEditorProps {
|
|
19
|
-
value: string;
|
|
20
|
-
onChange: (next: string) => void;
|
|
21
|
-
placeholder?: string;
|
|
22
|
-
disabled?: boolean;
|
|
23
|
-
readonly?: boolean;
|
|
24
|
-
ariaLabel: string;
|
|
25
|
-
className?: string;
|
|
26
|
-
/** Show the formatting toolbar above the editor. Default true. */
|
|
27
|
-
toolbar?: boolean;
|
|
28
|
-
/** Render a side-by-side preview pane that re-renders on every keystroke. Default false. */
|
|
29
|
-
preview?: boolean;
|
|
30
|
-
/** Pixel height (or any CSS length) of the editing surface. Default `240`. */
|
|
31
|
-
height?: number | string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Markdown editor backed by CodeMirror 6 with markdown syntax highlighting,
|
|
36
|
-
* a canvas-styled toolbar, and an optional sanitized preview pane.
|
|
37
|
-
*
|
|
38
|
-
* Controlled component. `value` is the markdown source string; `onChange`
|
|
39
|
-
* fires on every doc change.
|
|
40
|
-
*/
|
|
41
|
-
const MarkdownEditor = React.forwardRef<HTMLDivElement, MarkdownEditorProps>(
|
|
42
|
-
(
|
|
43
|
-
{
|
|
44
|
-
value,
|
|
45
|
-
onChange,
|
|
46
|
-
placeholder,
|
|
47
|
-
disabled = false,
|
|
48
|
-
readonly = false,
|
|
49
|
-
ariaLabel,
|
|
50
|
-
className,
|
|
51
|
-
toolbar = true,
|
|
52
|
-
preview = false,
|
|
53
|
-
height = 240,
|
|
54
|
-
},
|
|
55
|
-
ref,
|
|
56
|
-
) => {
|
|
57
|
-
const editorParentRef = React.useRef<HTMLDivElement>(null);
|
|
58
|
-
const viewRef = React.useRef<EditorView | null>(null);
|
|
59
|
-
const onChangeRef = React.useRef(onChange);
|
|
60
|
-
onChangeRef.current = onChange;
|
|
61
|
-
const theme = useCodemirrorTheme();
|
|
62
|
-
const [previewOpen, setPreviewOpen] = React.useState(preview);
|
|
63
|
-
const [view, setView] = React.useState<EditorView | null>(null);
|
|
64
|
-
|
|
65
|
-
// Mount/teardown the CodeMirror view whenever extensions change.
|
|
66
|
-
React.useEffect(() => {
|
|
67
|
-
if (!editorParentRef.current) return;
|
|
68
|
-
const extensions = [
|
|
69
|
-
history(),
|
|
70
|
-
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
71
|
-
markdown(),
|
|
72
|
-
EditorView.lineWrapping,
|
|
73
|
-
theme,
|
|
74
|
-
EditorView.editable.of(!disabled),
|
|
75
|
-
EditorState.readOnly.of(disabled || readonly),
|
|
76
|
-
placeholder ? placeholderExt(placeholder) : [],
|
|
77
|
-
EditorView.updateListener.of((update) => {
|
|
78
|
-
if (update.docChanged) {
|
|
79
|
-
onChangeRef.current(update.state.doc.toString());
|
|
80
|
-
}
|
|
81
|
-
}),
|
|
82
|
-
].flat();
|
|
83
|
-
|
|
84
|
-
const v = new EditorView({
|
|
85
|
-
state: EditorState.create({ doc: value, extensions }),
|
|
86
|
-
parent: editorParentRef.current,
|
|
87
|
-
});
|
|
88
|
-
viewRef.current = v;
|
|
89
|
-
setView(v);
|
|
90
|
-
return () => {
|
|
91
|
-
v.destroy();
|
|
92
|
-
viewRef.current = null;
|
|
93
|
-
setView(null);
|
|
94
|
-
};
|
|
95
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
-
}, [theme, disabled, readonly, placeholder]);
|
|
97
|
-
|
|
98
|
-
// Sync external `value` changes into the editor (without echoing back).
|
|
99
|
-
React.useEffect(() => {
|
|
100
|
-
const v = viewRef.current;
|
|
101
|
-
if (!v) return;
|
|
102
|
-
const current = v.state.doc.toString();
|
|
103
|
-
if (current !== value) {
|
|
104
|
-
v.dispatch({
|
|
105
|
-
changes: { from: 0, to: current.length, insert: value },
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}, [value]);
|
|
109
|
-
|
|
110
|
-
const heightStyle = typeof height === "number" ? `${height}px` : height;
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<div
|
|
114
|
-
ref={ref}
|
|
115
|
-
role="group"
|
|
116
|
-
aria-label={ariaLabel}
|
|
117
|
-
className={cn(
|
|
118
|
-
"overflow-hidden rounded-md border border-input bg-background text-sm shadow-sm focus-within:ring-1 focus-within:ring-ring",
|
|
119
|
-
disabled && "pointer-events-none opacity-50",
|
|
120
|
-
className,
|
|
121
|
-
)}
|
|
122
|
-
>
|
|
123
|
-
{toolbar && (
|
|
124
|
-
<MdToolbar
|
|
125
|
-
view={view}
|
|
126
|
-
previewActive={previewOpen}
|
|
127
|
-
onTogglePreview={() => setPreviewOpen((p) => !p)}
|
|
128
|
-
allowPreview
|
|
129
|
-
/>
|
|
130
|
-
)}
|
|
131
|
-
<div
|
|
132
|
-
className={cn("grid", previewOpen ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1")}
|
|
133
|
-
style={{ height: heightStyle }}
|
|
134
|
-
>
|
|
135
|
-
<div ref={editorParentRef} className="overflow-y-auto" />
|
|
136
|
-
{previewOpen && (
|
|
137
|
-
<div
|
|
138
|
-
className={cn(
|
|
139
|
-
"overflow-y-auto border-l border-border bg-muted/20 px-4 py-3",
|
|
140
|
-
PROSE_CANVAS_CLASSES,
|
|
141
|
-
)}
|
|
142
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: output is sanitized by DOMPurify in renderMarkdown
|
|
143
|
-
dangerouslySetInnerHTML={{ __html: renderMarkdown(value) }}
|
|
144
|
-
/>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
);
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
MarkdownEditor.displayName = "MarkdownEditor";
|
|
152
|
-
|
|
153
|
-
export { MarkdownEditor };
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/* c8 ignore file -- DOMPurify + marked require a real DOM; jsdom-incompatible.
|
|
4
|
-
* Verified visually in the docs site. */
|
|
5
|
-
|
|
6
|
-
import DOMPurify from "dompurify";
|
|
7
|
-
import { marked } from "marked";
|
|
8
|
-
|
|
9
|
-
// GFM (tables, task lists, autolinks) is on by default in marked v15+ but
|
|
10
|
-
// kept explicit for clarity and forward-compat.
|
|
11
|
-
marked.setOptions({ gfm: true, breaks: false });
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Convert a markdown string to a DOMPurify-sanitized HTML string suitable for
|
|
15
|
-
* `dangerouslySetInnerHTML`. Strips `<script>`, inline event handlers, and
|
|
16
|
-
* `javascript:` URLs.
|
|
17
|
-
*
|
|
18
|
-
* Used by `<MarkdownEditor preview>` for the live preview pane. The editor
|
|
19
|
-
* is a `"use client"` component, so this only ever runs in the browser.
|
|
20
|
-
*/
|
|
21
|
-
export function renderMarkdown(source: string): string {
|
|
22
|
-
if (typeof window === "undefined") return "";
|
|
23
|
-
const html = marked.parse(source, { async: false }) as string;
|
|
24
|
-
return DOMPurify.sanitize(html, {
|
|
25
|
-
ADD_ATTR: ["target", "rel"],
|
|
26
|
-
});
|
|
27
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tailwind class string applied to the editable surface of `RichTextEditor`
|
|
3
|
-
* and the rendered preview of `MarkdownEditor`. Replaces the role of
|
|
4
|
-
* `@tailwindcss/typography`'s `prose` plugin with token-aware styles so the
|
|
5
|
-
* surface respects whichever canvas theme is active.
|
|
6
|
-
*
|
|
7
|
-
* Use this on a wrapping `<div>` around `<EditorContent />` (Tiptap) or
|
|
8
|
-
* around the rendered HTML output (markdown preview).
|
|
9
|
-
*/
|
|
10
|
-
export const PROSE_CANVAS_CLASSES = [
|
|
11
|
-
// base text
|
|
12
|
-
"text-foreground text-sm leading-relaxed",
|
|
13
|
-
// paragraphs
|
|
14
|
-
"[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0",
|
|
15
|
-
// headings
|
|
16
|
-
"[&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-2xl [&_h1]:font-semibold [&_h1]:tracking-tight [&_h1]:text-foreground",
|
|
17
|
-
"[&_h2]:mt-3 [&_h2]:mb-2 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:text-foreground",
|
|
18
|
-
"[&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-foreground",
|
|
19
|
-
"[&_h4]:mt-3 [&_h4]:mb-1 [&_h4]:text-base [&_h4]:font-semibold [&_h4]:text-foreground",
|
|
20
|
-
"[&_h1:first-child]:mt-0 [&_h2:first-child]:mt-0 [&_h3:first-child]:mt-0 [&_h4:first-child]:mt-0",
|
|
21
|
-
// inline emphasis
|
|
22
|
-
"[&_strong]:font-semibold [&_strong]:text-foreground",
|
|
23
|
-
"[&_em]:italic",
|
|
24
|
-
"[&_u]:underline [&_u]:underline-offset-2",
|
|
25
|
-
"[&_s]:line-through",
|
|
26
|
-
// links — brand color, fades slightly on hover (no underline)
|
|
27
|
-
"[&_a]:text-brand [&_a]:transition-colors hover:[&_a]:text-brand/80",
|
|
28
|
-
// code
|
|
29
|
-
"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.85em] [&_code]:text-foreground",
|
|
30
|
-
"[&_pre]:rounded-md [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted/50 [&_pre]:p-3 [&_pre]:my-3 [&_pre]:overflow-x-auto",
|
|
31
|
-
"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-sm",
|
|
32
|
-
// lists
|
|
33
|
-
"[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-6",
|
|
34
|
-
"[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-6",
|
|
35
|
-
"[&_li]:my-1 [&_li_p]:my-0",
|
|
36
|
-
"[&_li::marker]:text-muted-foreground",
|
|
37
|
-
// blockquote
|
|
38
|
-
"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:my-3 [&_blockquote]:text-muted-foreground [&_blockquote]:italic",
|
|
39
|
-
// horizontal rule
|
|
40
|
-
"[&_hr]:my-4 [&_hr]:border-border",
|
|
41
|
-
// tables (GFM)
|
|
42
|
-
"[&_table]:my-3 [&_table]:w-full [&_table]:text-sm [&_table]:border-collapse",
|
|
43
|
-
"[&_th]:border [&_th]:border-border [&_th]:bg-muted/40 [&_th]:px-3 [&_th]:py-1.5 [&_th]:text-left [&_th]:font-semibold",
|
|
44
|
-
"[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-1.5",
|
|
45
|
-
].join(" ");
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/* c8 ignore file -- Tiptap/ProseMirror requires real DOM measurements;
|
|
4
|
-
* jsdom-incompatible. Verified visually in the docs site. */
|
|
5
|
-
|
|
6
|
-
import { Link } from "@tiptap/extension-link";
|
|
7
|
-
import { Placeholder } from "@tiptap/extension-placeholder";
|
|
8
|
-
import { EditorContent, type Extensions, useEditor } from "@tiptap/react";
|
|
9
|
-
import { StarterKit } from "@tiptap/starter-kit";
|
|
10
|
-
import * as React from "react";
|
|
11
|
-
|
|
12
|
-
import { cn } from "../../../lib/utils";
|
|
13
|
-
import { PROSE_CANVAS_CLASSES } from "./prose-canvas-classes";
|
|
14
|
-
import { RteToolbar, type ToolbarItemId } from "./toolbar/rte-toolbar";
|
|
15
|
-
|
|
16
|
-
export interface RichTextEditorProps {
|
|
17
|
-
value: string;
|
|
18
|
-
onChange: (next: string) => void;
|
|
19
|
-
placeholder?: string;
|
|
20
|
-
disabled?: boolean;
|
|
21
|
-
readonly?: boolean;
|
|
22
|
-
ariaLabel: string;
|
|
23
|
-
className?: string;
|
|
24
|
-
/** Show the toolbar above the editor. Default true. */
|
|
25
|
-
toolbar?: boolean;
|
|
26
|
-
/** Whitelist of toolbar buttons. Default: all. See `TOOLBAR_ITEM_IDS`. */
|
|
27
|
-
toolbarItems?: readonly ToolbarItemId[];
|
|
28
|
-
/** Output format for `value` / `onChange`. Default `"html"`. */
|
|
29
|
-
outputFormat?: "html" | "json";
|
|
30
|
-
/** Pixel height (or any CSS length) of the editing surface. Default `240`. */
|
|
31
|
-
height?: number | string;
|
|
32
|
-
/** Extra Tiptap extensions appended after the canvas defaults. */
|
|
33
|
-
extensions?: Extensions;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Rich text editor backed by Tiptap (ProseMirror). Ships with bold / italic /
|
|
38
|
-
* strike / code / list / blockquote / code-block / link / heading levels.
|
|
39
|
-
*
|
|
40
|
-
* Controlled component: `value` is an HTML string by default, or a JSON string
|
|
41
|
-
* when `outputFormat="json"`. `onChange` fires on every doc update.
|
|
42
|
-
*/
|
|
43
|
-
const RichTextEditor = React.forwardRef<HTMLDivElement, RichTextEditorProps>(
|
|
44
|
-
(
|
|
45
|
-
{
|
|
46
|
-
value,
|
|
47
|
-
onChange,
|
|
48
|
-
placeholder,
|
|
49
|
-
disabled = false,
|
|
50
|
-
readonly = false,
|
|
51
|
-
ariaLabel,
|
|
52
|
-
className,
|
|
53
|
-
toolbar = true,
|
|
54
|
-
toolbarItems,
|
|
55
|
-
outputFormat = "html",
|
|
56
|
-
height = 240,
|
|
57
|
-
extensions: extraExtensions,
|
|
58
|
-
},
|
|
59
|
-
ref,
|
|
60
|
-
) => {
|
|
61
|
-
const onChangeRef = React.useRef(onChange);
|
|
62
|
-
onChangeRef.current = onChange;
|
|
63
|
-
|
|
64
|
-
const editor = useEditor({
|
|
65
|
-
extensions: [
|
|
66
|
-
StarterKit,
|
|
67
|
-
Link.configure({ openOnClick: false, autolink: true }),
|
|
68
|
-
...(placeholder ? [Placeholder.configure({ placeholder })] : []),
|
|
69
|
-
...(extraExtensions ?? []),
|
|
70
|
-
],
|
|
71
|
-
content: value,
|
|
72
|
-
editable: !disabled && !readonly,
|
|
73
|
-
editorProps: {
|
|
74
|
-
attributes: {
|
|
75
|
-
"aria-label": ariaLabel,
|
|
76
|
-
"aria-multiline": "true",
|
|
77
|
-
role: "textbox",
|
|
78
|
-
class: cn("min-h-full w-full px-4 py-3 outline-none", PROSE_CANVAS_CLASSES),
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
onUpdate: ({ editor: e }) => {
|
|
82
|
-
const next = outputFormat === "json" ? JSON.stringify(e.getJSON()) : e.getHTML();
|
|
83
|
-
onChangeRef.current(next);
|
|
84
|
-
},
|
|
85
|
-
immediatelyRender: false,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Keep `editable` in sync with prop changes.
|
|
89
|
-
React.useEffect(() => {
|
|
90
|
-
editor?.setEditable(!disabled && !readonly);
|
|
91
|
-
}, [editor, disabled, readonly]);
|
|
92
|
-
|
|
93
|
-
// Sync external `value` changes into the editor (without echoing back).
|
|
94
|
-
React.useEffect(() => {
|
|
95
|
-
if (!editor) return;
|
|
96
|
-
const current = outputFormat === "json" ? JSON.stringify(editor.getJSON()) : editor.getHTML();
|
|
97
|
-
if (current !== value) {
|
|
98
|
-
const incoming = outputFormat === "json" && value ? JSON.parse(value) : value;
|
|
99
|
-
editor.commands.setContent(incoming, false);
|
|
100
|
-
}
|
|
101
|
-
}, [editor, value, outputFormat]);
|
|
102
|
-
|
|
103
|
-
const heightStyle = typeof height === "number" ? `${height}px` : height;
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div
|
|
107
|
-
ref={ref}
|
|
108
|
-
role="group"
|
|
109
|
-
aria-label={ariaLabel}
|
|
110
|
-
className={cn(
|
|
111
|
-
"overflow-hidden rounded-md border border-input bg-background text-sm shadow-sm focus-within:ring-1 focus-within:ring-ring",
|
|
112
|
-
disabled && "pointer-events-none opacity-50",
|
|
113
|
-
className,
|
|
114
|
-
)}
|
|
115
|
-
>
|
|
116
|
-
{toolbar && <RteToolbar editor={editor} items={toolbarItems} />}
|
|
117
|
-
<div className="overflow-y-auto" style={{ height: heightStyle }}>
|
|
118
|
-
<EditorContent editor={editor} className="h-full" />
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
);
|
|
124
|
-
RichTextEditor.displayName = "RichTextEditor";
|
|
125
|
-
|
|
126
|
-
export { RichTextEditor };
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/* c8 ignore file -- CodeMirror toolbar requires real DOM; jsdom-incompatible. */
|
|
4
|
-
|
|
5
|
-
import { EditorSelection } from "@codemirror/state";
|
|
6
|
-
import type { EditorView } from "@codemirror/view";
|
|
7
|
-
|
|
8
|
-
import { Icon } from "../../../atoms/icon";
|
|
9
|
-
import { Toggle } from "../../../atoms/toggle";
|
|
10
|
-
import { ToolbarDivider, ToolbarShell } from "./toolbar-shell";
|
|
11
|
-
|
|
12
|
-
export interface MdToolbarProps {
|
|
13
|
-
/** CodeMirror view ref (held by the editor). */
|
|
14
|
-
view: EditorView | null;
|
|
15
|
-
/** Whether the preview pane is currently shown. */
|
|
16
|
-
previewActive: boolean;
|
|
17
|
-
/** Callback when the user clicks the preview toggle. */
|
|
18
|
-
onTogglePreview?: () => void;
|
|
19
|
-
/** Hide the preview button entirely (when `<MarkdownEditor preview>` isn't enabled at all). */
|
|
20
|
-
allowPreview: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Wrap the current selection (or insert at the cursor) with `prefix`/`suffix`.
|
|
25
|
-
* Used for **bold**, *italic*, `code`, and link insertion.
|
|
26
|
-
*/
|
|
27
|
-
function wrap(view: EditorView | null, prefix: string, suffix = prefix) {
|
|
28
|
-
if (!view) return;
|
|
29
|
-
const { state } = view;
|
|
30
|
-
const changes = state.changeByRange((range) => ({
|
|
31
|
-
changes: [
|
|
32
|
-
{ from: range.from, insert: prefix },
|
|
33
|
-
{ from: range.to, insert: suffix },
|
|
34
|
-
],
|
|
35
|
-
range: range.empty
|
|
36
|
-
? EditorSelection.cursor(range.from + prefix.length)
|
|
37
|
-
: EditorSelection.range(range.from + prefix.length, range.to + prefix.length),
|
|
38
|
-
}));
|
|
39
|
-
view.dispatch({ ...changes, userEvent: "input.format" });
|
|
40
|
-
view.focus();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Toggle a per-line prefix (e.g. `- ` or `1. `) on every line in the selection. */
|
|
44
|
-
function togglePrefix(view: EditorView | null, prefix: string) {
|
|
45
|
-
if (!view) return;
|
|
46
|
-
const { state } = view;
|
|
47
|
-
const changes = state.changeByRange((range) => {
|
|
48
|
-
const startLine = state.doc.lineAt(range.from);
|
|
49
|
-
const endLine = state.doc.lineAt(range.to);
|
|
50
|
-
const inserts: { from: number; insert: string }[] = [];
|
|
51
|
-
for (let n = startLine.number; n <= endLine.number; n++) {
|
|
52
|
-
const line = state.doc.line(n);
|
|
53
|
-
if (!line.text.startsWith(prefix)) {
|
|
54
|
-
inserts.push({ from: line.from, insert: prefix });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
changes: inserts,
|
|
59
|
-
range,
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
view.dispatch({ ...changes, userEvent: "input.format" });
|
|
63
|
-
view.focus();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Insert a markdown link at the cursor with the selected text as the label. */
|
|
67
|
-
function insertLink(view: EditorView | null) {
|
|
68
|
-
if (!view) return;
|
|
69
|
-
const { state } = view;
|
|
70
|
-
const sel = state.sliceDoc(state.selection.main.from, state.selection.main.to);
|
|
71
|
-
const label = sel || "link";
|
|
72
|
-
const replacement = `[${label}](https://)`;
|
|
73
|
-
view.dispatch({
|
|
74
|
-
changes: { from: state.selection.main.from, to: state.selection.main.to, insert: replacement },
|
|
75
|
-
selection: {
|
|
76
|
-
anchor: state.selection.main.from + label.length + 3,
|
|
77
|
-
head: state.selection.main.from + label.length + 11,
|
|
78
|
-
},
|
|
79
|
-
userEvent: "input.format",
|
|
80
|
-
});
|
|
81
|
-
view.focus();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function MdToolbar({ view, previewActive, onTogglePreview, allowPreview }: MdToolbarProps) {
|
|
85
|
-
return (
|
|
86
|
-
<ToolbarShell aria-label="Markdown formatting">
|
|
87
|
-
<Toggle size="sm" aria-label="Bold" onPressedChange={() => wrap(view, "**")}>
|
|
88
|
-
<Icon name="Bold" />
|
|
89
|
-
</Toggle>
|
|
90
|
-
<Toggle size="sm" aria-label="Italic" onPressedChange={() => wrap(view, "*")}>
|
|
91
|
-
<Icon name="Italic" />
|
|
92
|
-
</Toggle>
|
|
93
|
-
<Toggle size="sm" aria-label="Inline code" onPressedChange={() => wrap(view, "`")}>
|
|
94
|
-
<Icon name="Code" />
|
|
95
|
-
</Toggle>
|
|
96
|
-
<ToolbarDivider />
|
|
97
|
-
<Toggle size="sm" aria-label="Bullet list" onPressedChange={() => togglePrefix(view, "- ")}>
|
|
98
|
-
<Icon name="List" />
|
|
99
|
-
</Toggle>
|
|
100
|
-
<Toggle
|
|
101
|
-
size="sm"
|
|
102
|
-
aria-label="Numbered list"
|
|
103
|
-
onPressedChange={() => togglePrefix(view, "1. ")}
|
|
104
|
-
>
|
|
105
|
-
<Icon name="ListOrdered" />
|
|
106
|
-
</Toggle>
|
|
107
|
-
<Toggle size="sm" aria-label="Block quote" onPressedChange={() => togglePrefix(view, "> ")}>
|
|
108
|
-
<Icon name="Quote" />
|
|
109
|
-
</Toggle>
|
|
110
|
-
<ToolbarDivider />
|
|
111
|
-
<Toggle size="sm" aria-label="Insert link" onPressedChange={() => insertLink(view)}>
|
|
112
|
-
<Icon name="Link" />
|
|
113
|
-
</Toggle>
|
|
114
|
-
{allowPreview && onTogglePreview && (
|
|
115
|
-
<>
|
|
116
|
-
<ToolbarDivider />
|
|
117
|
-
<Toggle
|
|
118
|
-
size="sm"
|
|
119
|
-
aria-label={previewActive ? "Hide preview" : "Show preview"}
|
|
120
|
-
pressed={previewActive}
|
|
121
|
-
onPressedChange={onTogglePreview}
|
|
122
|
-
>
|
|
123
|
-
<Icon name={previewActive ? "EyeOff" : "Eye"} />
|
|
124
|
-
</Toggle>
|
|
125
|
-
</>
|
|
126
|
-
)}
|
|
127
|
-
</ToolbarShell>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/* c8 ignore file -- Tiptap toolbar requires real DOM; jsdom-incompatible. */
|
|
4
|
-
|
|
5
|
-
import type { Editor } from "@tiptap/react";
|
|
6
|
-
|
|
7
|
-
import { Button } from "../../../atoms/button";
|
|
8
|
-
import { Icon } from "../../../atoms/icon";
|
|
9
|
-
import { Toggle } from "../../../atoms/toggle";
|
|
10
|
-
import { ToolbarDivider, ToolbarShell } from "./toolbar-shell";
|
|
11
|
-
|
|
12
|
-
export const TOOLBAR_ITEM_IDS = [
|
|
13
|
-
"heading",
|
|
14
|
-
"bold",
|
|
15
|
-
"italic",
|
|
16
|
-
"underline",
|
|
17
|
-
"strike",
|
|
18
|
-
"code",
|
|
19
|
-
"codeBlock",
|
|
20
|
-
"bulletList",
|
|
21
|
-
"orderedList",
|
|
22
|
-
"blockquote",
|
|
23
|
-
"link",
|
|
24
|
-
"undo",
|
|
25
|
-
"redo",
|
|
26
|
-
] as const;
|
|
27
|
-
export type ToolbarItemId = (typeof TOOLBAR_ITEM_IDS)[number];
|
|
28
|
-
|
|
29
|
-
export interface RteToolbarProps {
|
|
30
|
-
editor: Editor | null;
|
|
31
|
-
/** Whitelist of toolbar items to render. Default: every item in `TOOLBAR_ITEM_IDS`. */
|
|
32
|
-
items?: readonly ToolbarItemId[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function show(items: readonly ToolbarItemId[] | undefined, id: ToolbarItemId) {
|
|
36
|
-
return !items || items.includes(id);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function RteToolbar({ editor, items }: RteToolbarProps) {
|
|
40
|
-
if (!editor) return null;
|
|
41
|
-
|
|
42
|
-
const setHeading = (level: 1 | 2 | 3 | null) => {
|
|
43
|
-
if (level === null) editor.chain().focus().setParagraph().run();
|
|
44
|
-
else editor.chain().focus().toggleHeading({ level }).run();
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const insertLink = () => {
|
|
48
|
-
const previous = editor.getAttributes("link").href as string | undefined;
|
|
49
|
-
const url = window.prompt("URL", previous ?? "https://");
|
|
50
|
-
if (url === null) return;
|
|
51
|
-
if (url === "") editor.chain().focus().unsetLink().run();
|
|
52
|
-
else
|
|
53
|
-
editor
|
|
54
|
-
.chain()
|
|
55
|
-
.focus()
|
|
56
|
-
.extendMarkRange("link")
|
|
57
|
-
.setLink({ href: url, target: "_blank", rel: "noopener noreferrer" })
|
|
58
|
-
.run();
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<ToolbarShell aria-label="Rich text formatting">
|
|
63
|
-
{show(items, "heading") && (
|
|
64
|
-
<>
|
|
65
|
-
<Toggle
|
|
66
|
-
size="sm"
|
|
67
|
-
aria-label="Heading 1"
|
|
68
|
-
pressed={editor.isActive("heading", { level: 1 })}
|
|
69
|
-
onPressedChange={() => setHeading(1)}
|
|
70
|
-
>
|
|
71
|
-
<Icon name="Heading1" />
|
|
72
|
-
</Toggle>
|
|
73
|
-
<Toggle
|
|
74
|
-
size="sm"
|
|
75
|
-
aria-label="Heading 2"
|
|
76
|
-
pressed={editor.isActive("heading", { level: 2 })}
|
|
77
|
-
onPressedChange={() => setHeading(2)}
|
|
78
|
-
>
|
|
79
|
-
<Icon name="Heading2" />
|
|
80
|
-
</Toggle>
|
|
81
|
-
<Toggle
|
|
82
|
-
size="sm"
|
|
83
|
-
aria-label="Heading 3"
|
|
84
|
-
pressed={editor.isActive("heading", { level: 3 })}
|
|
85
|
-
onPressedChange={() => setHeading(3)}
|
|
86
|
-
>
|
|
87
|
-
<Icon name="Heading3" />
|
|
88
|
-
</Toggle>
|
|
89
|
-
<ToolbarDivider />
|
|
90
|
-
</>
|
|
91
|
-
)}
|
|
92
|
-
{show(items, "bold") && (
|
|
93
|
-
<Toggle
|
|
94
|
-
size="sm"
|
|
95
|
-
aria-label="Bold"
|
|
96
|
-
pressed={editor.isActive("bold")}
|
|
97
|
-
onPressedChange={() => editor.chain().focus().toggleBold().run()}
|
|
98
|
-
>
|
|
99
|
-
<Icon name="Bold" />
|
|
100
|
-
</Toggle>
|
|
101
|
-
)}
|
|
102
|
-
{show(items, "italic") && (
|
|
103
|
-
<Toggle
|
|
104
|
-
size="sm"
|
|
105
|
-
aria-label="Italic"
|
|
106
|
-
pressed={editor.isActive("italic")}
|
|
107
|
-
onPressedChange={() => editor.chain().focus().toggleItalic().run()}
|
|
108
|
-
>
|
|
109
|
-
<Icon name="Italic" />
|
|
110
|
-
</Toggle>
|
|
111
|
-
)}
|
|
112
|
-
{show(items, "strike") && (
|
|
113
|
-
<Toggle
|
|
114
|
-
size="sm"
|
|
115
|
-
aria-label="Strikethrough"
|
|
116
|
-
pressed={editor.isActive("strike")}
|
|
117
|
-
onPressedChange={() => editor.chain().focus().toggleStrike().run()}
|
|
118
|
-
>
|
|
119
|
-
<Icon name="Strikethrough" />
|
|
120
|
-
</Toggle>
|
|
121
|
-
)}
|
|
122
|
-
{show(items, "code") && (
|
|
123
|
-
<Toggle
|
|
124
|
-
size="sm"
|
|
125
|
-
aria-label="Inline code"
|
|
126
|
-
pressed={editor.isActive("code")}
|
|
127
|
-
onPressedChange={() => editor.chain().focus().toggleCode().run()}
|
|
128
|
-
>
|
|
129
|
-
<Icon name="Code" />
|
|
130
|
-
</Toggle>
|
|
131
|
-
)}
|
|
132
|
-
<ToolbarDivider />
|
|
133
|
-
{show(items, "bulletList") && (
|
|
134
|
-
<Toggle
|
|
135
|
-
size="sm"
|
|
136
|
-
aria-label="Bullet list"
|
|
137
|
-
pressed={editor.isActive("bulletList")}
|
|
138
|
-
onPressedChange={() => editor.chain().focus().toggleBulletList().run()}
|
|
139
|
-
>
|
|
140
|
-
<Icon name="List" />
|
|
141
|
-
</Toggle>
|
|
142
|
-
)}
|
|
143
|
-
{show(items, "orderedList") && (
|
|
144
|
-
<Toggle
|
|
145
|
-
size="sm"
|
|
146
|
-
aria-label="Ordered list"
|
|
147
|
-
pressed={editor.isActive("orderedList")}
|
|
148
|
-
onPressedChange={() => editor.chain().focus().toggleOrderedList().run()}
|
|
149
|
-
>
|
|
150
|
-
<Icon name="ListOrdered" />
|
|
151
|
-
</Toggle>
|
|
152
|
-
)}
|
|
153
|
-
{show(items, "blockquote") && (
|
|
154
|
-
<Toggle
|
|
155
|
-
size="sm"
|
|
156
|
-
aria-label="Block quote"
|
|
157
|
-
pressed={editor.isActive("blockquote")}
|
|
158
|
-
onPressedChange={() => editor.chain().focus().toggleBlockquote().run()}
|
|
159
|
-
>
|
|
160
|
-
<Icon name="Quote" />
|
|
161
|
-
</Toggle>
|
|
162
|
-
)}
|
|
163
|
-
{show(items, "codeBlock") && (
|
|
164
|
-
<Toggle
|
|
165
|
-
size="sm"
|
|
166
|
-
aria-label="Code block"
|
|
167
|
-
pressed={editor.isActive("codeBlock")}
|
|
168
|
-
onPressedChange={() => editor.chain().focus().toggleCodeBlock().run()}
|
|
169
|
-
>
|
|
170
|
-
<Icon name="SquareCode" />
|
|
171
|
-
</Toggle>
|
|
172
|
-
)}
|
|
173
|
-
<ToolbarDivider />
|
|
174
|
-
{show(items, "link") && (
|
|
175
|
-
<Toggle
|
|
176
|
-
size="sm"
|
|
177
|
-
aria-label="Insert link"
|
|
178
|
-
pressed={editor.isActive("link")}
|
|
179
|
-
onPressedChange={insertLink}
|
|
180
|
-
>
|
|
181
|
-
<Icon name="Link" />
|
|
182
|
-
</Toggle>
|
|
183
|
-
)}
|
|
184
|
-
<ToolbarDivider />
|
|
185
|
-
{show(items, "undo") && (
|
|
186
|
-
<Button
|
|
187
|
-
type="button"
|
|
188
|
-
variant="ghost"
|
|
189
|
-
size="sm"
|
|
190
|
-
aria-label="Undo"
|
|
191
|
-
disabled={!editor.can().undo()}
|
|
192
|
-
onClick={() => editor.chain().focus().undo().run()}
|
|
193
|
-
>
|
|
194
|
-
<Icon name="Undo2" />
|
|
195
|
-
</Button>
|
|
196
|
-
)}
|
|
197
|
-
{show(items, "redo") && (
|
|
198
|
-
<Button
|
|
199
|
-
type="button"
|
|
200
|
-
variant="ghost"
|
|
201
|
-
size="sm"
|
|
202
|
-
aria-label="Redo"
|
|
203
|
-
disabled={!editor.can().redo()}
|
|
204
|
-
onClick={() => editor.chain().focus().redo().run()}
|
|
205
|
-
>
|
|
206
|
-
<Icon name="Redo2" />
|
|
207
|
-
</Button>
|
|
208
|
-
)}
|
|
209
|
-
</ToolbarShell>
|
|
210
|
-
);
|
|
211
|
-
}
|