@bayonai/rich-text-editor 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +37 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +5 -0
  5. package/dist/react/BlockActionTool.d.ts +15 -0
  6. package/dist/react/BlockActionTool.js +37 -0
  7. package/dist/react/EditorSessionProvider.d.ts +28 -0
  8. package/dist/react/EditorSessionProvider.js +74 -0
  9. package/dist/react/RichTextBody.d.ts +18 -0
  10. package/dist/react/RichTextBody.js +66 -0
  11. package/dist/react/RichTextDocumentSurface.d.ts +6 -0
  12. package/dist/react/RichTextDocumentSurface.js +5 -0
  13. package/dist/react/RichTextEditor.d.ts +45 -0
  14. package/dist/react/RichTextEditor.js +1096 -0
  15. package/dist/react/RichTextIcons.d.ts +21 -0
  16. package/dist/react/RichTextIcons.js +55 -0
  17. package/dist/react/RichTextRenderedBlock.d.ts +4 -0
  18. package/dist/react/RichTextRenderedBlock.js +36 -0
  19. package/dist/react/RichTextRenderer.d.ts +4 -0
  20. package/dist/react/RichTextRenderer.js +8 -0
  21. package/dist/react/RichTextStyles.d.ts +1 -0
  22. package/dist/react/RichTextStyles.js +719 -0
  23. package/dist/react/RichTextTitleInput.d.ts +20 -0
  24. package/dist/react/RichTextTitleInput.js +39 -0
  25. package/dist/react/SelectionFormatToolbar.d.ts +34 -0
  26. package/dist/react/SelectionFormatToolbar.js +18 -0
  27. package/dist/react/SpecialBlockOption.d.ts +7 -0
  28. package/dist/react/SpecialBlockOption.js +7 -0
  29. package/dist/react/SpecialBlockTool.d.ts +42 -0
  30. package/dist/react/SpecialBlockTool.js +50 -0
  31. package/dist/react/TranscriptionControl.d.ts +19 -0
  32. package/dist/react/TranscriptionControl.js +129 -0
  33. package/dist/react/UnsavedChangesDialog.d.ts +9 -0
  34. package/dist/react/UnsavedChangesDialog.js +13 -0
  35. package/dist/react/blockActionToolState.d.ts +18 -0
  36. package/dist/react/blockActionToolState.js +53 -0
  37. package/dist/react/blockActions.d.ts +8 -0
  38. package/dist/react/blockActions.js +111 -0
  39. package/dist/react/editorNavigation.d.ts +19 -0
  40. package/dist/react/editorNavigation.js +39 -0
  41. package/dist/react/editorShortcuts.d.ts +20 -0
  42. package/dist/react/editorShortcuts.js +25 -0
  43. package/dist/react/index.d.ts +9 -0
  44. package/dist/react/index.js +6 -0
  45. package/dist/react/richTextBlockStyles.d.ts +7 -0
  46. package/dist/react/richTextBlockStyles.js +7 -0
  47. package/dist/react/specialBlockStyles.d.ts +15 -0
  48. package/dist/react/specialBlockStyles.js +9 -0
  49. package/dist/richText.d.ts +15 -0
  50. package/dist/richText.js +297 -0
  51. package/dist/saveControl.d.ts +8 -0
  52. package/dist/saveControl.js +9 -0
  53. package/dist/session.d.ts +27 -0
  54. package/dist/session.js +78 -0
  55. package/dist/sessionRegistry.d.ts +24 -0
  56. package/dist/sessionRegistry.js +87 -0
  57. package/dist/types.d.ts +34 -0
  58. package/dist/types.js +1 -0
  59. package/dist/writingStats.d.ts +5 -0
  60. package/dist/writingStats.js +9 -0
  61. package/dist-cjs/index.js +22 -0
  62. package/dist-cjs/package.json +3 -0
  63. package/dist-cjs/react/BlockActionTool.js +40 -0
  64. package/dist-cjs/react/EditorSessionProvider.js +79 -0
  65. package/dist-cjs/react/RichTextBody.js +69 -0
  66. package/dist-cjs/react/RichTextDocumentSurface.js +8 -0
  67. package/dist-cjs/react/RichTextEditor.js +1108 -0
  68. package/dist-cjs/react/RichTextIcons.js +74 -0
  69. package/dist-cjs/react/RichTextRenderedBlock.js +39 -0
  70. package/dist-cjs/react/RichTextRenderer.js +11 -0
  71. package/dist-cjs/react/RichTextStyles.js +722 -0
  72. package/dist-cjs/react/RichTextTitleInput.js +44 -0
  73. package/dist-cjs/react/SelectionFormatToolbar.js +22 -0
  74. package/dist-cjs/react/SpecialBlockOption.js +10 -0
  75. package/dist-cjs/react/SpecialBlockTool.js +54 -0
  76. package/dist-cjs/react/TranscriptionControl.js +133 -0
  77. package/dist-cjs/react/UnsavedChangesDialog.js +16 -0
  78. package/dist-cjs/react/blockActionToolState.js +58 -0
  79. package/dist-cjs/react/blockActions.js +119 -0
  80. package/dist-cjs/react/editorNavigation.js +45 -0
  81. package/dist-cjs/react/editorShortcuts.js +28 -0
  82. package/dist-cjs/react/index.js +17 -0
  83. package/dist-cjs/react/richTextBlockStyles.js +10 -0
  84. package/dist-cjs/react/specialBlockStyles.js +12 -0
  85. package/dist-cjs/richText.js +307 -0
  86. package/dist-cjs/saveControl.js +12 -0
  87. package/dist-cjs/session.js +83 -0
  88. package/dist-cjs/sessionRegistry.js +90 -0
  89. package/dist-cjs/types.js +2 -0
  90. package/dist-cjs/writingStats.js +12 -0
  91. package/package.json +45 -0
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef } from "react";
4
+ import { RichTextStyleScope } from "./RichTextStyles.js";
5
+ // Renders the title field and handles keyboard movement into the editor body.
6
+ export function RichTextTitleInput({ disabled, inputRef, label, onArrowDown, onChange, required, title, validationMessage = "", }) {
7
+ const validationMessageId = useId();
8
+ useEffect(() => {
9
+ resizeTitleTextArea(inputRef.current);
10
+ }, [inputRef, title]);
11
+ return (_jsxs(_Fragment, { children: [_jsx("textarea", { "aria-describedby": validationMessage ? validationMessageId : undefined, "aria-invalid": validationMessage ? true : undefined, "aria-label": label, "aria-required": required ? true : undefined, className: "bayon-rte-title", dir: "auto", disabled: disabled, onChange: (event) => {
12
+ resizeTitleTextArea(event.currentTarget);
13
+ onChange(event.target.value);
14
+ }, onInput: (event) => resizeTitleTextArea(event.currentTarget), onKeyDown: (event) => {
15
+ if (getTitleKeyCommand(event) === "focus-body-start") {
16
+ event.preventDefault();
17
+ onArrowDown();
18
+ }
19
+ }, placeholder: "Title", ref: inputRef, rows: 1, value: title }), validationMessage ? (_jsx("div", { className: "bayon-rte-title-error", id: validationMessageId, role: "alert", children: validationMessage })) : null] }));
20
+ }
21
+ export function getTitleKeyCommand(event) {
22
+ return event.key === "ArrowDown" || event.key === "Enter"
23
+ ? "focus-body-start"
24
+ : null;
25
+ }
26
+ export function RichTextReadTitle({ title }) {
27
+ const titleRef = useRef(null);
28
+ useEffect(() => {
29
+ resizeTitleTextArea(titleRef.current);
30
+ }, [title]);
31
+ return (_jsxs(_Fragment, { children: [_jsx(RichTextStyleScope, {}), _jsx("textarea", { "aria-label": "Entry title", className: "bayon-rte-title", dir: "auto", placeholder: "Title", readOnly: true, ref: titleRef, rows: 1, value: title })] }));
32
+ }
33
+ function resizeTitleTextArea(textarea) {
34
+ if (!textarea) {
35
+ return;
36
+ }
37
+ textarea.style.height = "auto";
38
+ textarea.style.height = `${textarea.scrollHeight}px`;
39
+ }
@@ -0,0 +1,34 @@
1
+ import { BoldIcon, CodeIcon, ItalicIcon, LinkIcon, QuoteIcon, TitleIcon } from "./RichTextIcons";
2
+ export declare const selectionActions: readonly [{
3
+ readonly command: "bold";
4
+ readonly icon: typeof BoldIcon;
5
+ readonly label: "Bold";
6
+ }, {
7
+ readonly command: "italic";
8
+ readonly icon: typeof ItalicIcon;
9
+ readonly label: "Italic";
10
+ }, {
11
+ readonly command: "link";
12
+ readonly icon: typeof LinkIcon;
13
+ readonly label: "Link";
14
+ }, {
15
+ readonly command: "heading";
16
+ readonly icon: typeof TitleIcon;
17
+ readonly label: "Title";
18
+ }, {
19
+ readonly command: "quote";
20
+ readonly icon: typeof QuoteIcon;
21
+ readonly label: "Quote";
22
+ }, {
23
+ readonly command: "code";
24
+ readonly icon: typeof CodeIcon;
25
+ readonly label: "Code";
26
+ }];
27
+ export type SelectionCommand = (typeof selectionActions)[number]["command"];
28
+ type SelectionFormatToolbarProps = {
29
+ left: number;
30
+ onAction: (command: SelectionCommand) => void;
31
+ top: number;
32
+ };
33
+ export declare function SelectionFormatToolbar({ left, onAction, top, }: SelectionFormatToolbarProps): import("react/jsx-runtime").JSX.Element;
34
+ export {};
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { BoldIcon, CodeIcon, ItalicIcon, LinkIcon, QuoteIcon, TitleIcon, } from "./RichTextIcons.js";
4
+ export const selectionActions = [
5
+ { command: "bold", icon: BoldIcon, label: "Bold" },
6
+ { command: "italic", icon: ItalicIcon, label: "Italic" },
7
+ { command: "link", icon: LinkIcon, label: "Link" },
8
+ { command: "heading", icon: TitleIcon, label: "Title" },
9
+ { command: "quote", icon: QuoteIcon, label: "Quote" },
10
+ { command: "code", icon: CodeIcon, label: "Code" },
11
+ ];
12
+ // Renders the floating toolbar used to format the current text selection.
13
+ export function SelectionFormatToolbar({ left, onAction, top, }) {
14
+ return (_jsx("div", { className: "bayon-rte-toolbar", style: {
15
+ "--bayon-rte-toolbar-left": `${left}px`,
16
+ "--bayon-rte-toolbar-top": `${top}px`,
17
+ }, children: selectionActions.map(({ command, icon: Icon, label }, index) => (_jsx("button", { "aria-label": label, className: `bayon-rte-icon-button bayon-rte-toolbar__button${index === 3 ? " bayon-rte-toolbar__button--divider" : ""}`, onClick: () => onAction(command), onMouseDown: (event) => event.preventDefault(), title: label, type: "button", children: _jsx(Icon, { size: 18 }) }, command))) }));
18
+ }
@@ -0,0 +1,7 @@
1
+ import type { SpecialBlockAction } from "./SpecialBlockTool";
2
+ type SpecialBlockOptionProps = {
3
+ action: SpecialBlockAction;
4
+ onInsert: (action: SpecialBlockAction) => void;
5
+ };
6
+ export declare function SpecialBlockOption({ action, onInsert, }: SpecialBlockOptionProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ // Renders one selectable special-block action inside the insertion tool.
4
+ export function SpecialBlockOption({ action, onInsert, }) {
5
+ const Icon = action.icon;
6
+ return (_jsx("button", { "aria-label": action.label, className: "bayon-rte-icon-button bayon-rte-special-button", onClick: () => onInsert(action), onMouseDown: (event) => event.preventDefault(), title: action.label, type: "button", children: _jsx(Icon, { size: 18 }) }));
7
+ }
@@ -0,0 +1,42 @@
1
+ import { CodeIcon, DataObjectIcon, DividerIcon, ImageIcon, QuoteIcon, TitleIcon } from "./RichTextIcons";
2
+ export declare const specialBlockActions: readonly [{
3
+ readonly html: "<figure data-placeholder=\"image\"><p>Image placeholder</p></figure>";
4
+ readonly icon: typeof ImageIcon;
5
+ readonly label: "Image";
6
+ readonly selectDefault: true;
7
+ }, {
8
+ readonly html: "<blockquote data-placeholder=\"quote\"></blockquote>";
9
+ readonly icon: typeof QuoteIcon;
10
+ readonly label: "Quote";
11
+ readonly selectDefault: true;
12
+ }, {
13
+ readonly html: "<h2>Title</h2>";
14
+ readonly icon: typeof TitleIcon;
15
+ readonly label: "Title";
16
+ readonly selectDefault: true;
17
+ }, {
18
+ readonly html: "<pre><code>Code block</code></pre>";
19
+ readonly icon: typeof CodeIcon;
20
+ readonly label: "Code block";
21
+ readonly selectDefault: true;
22
+ }, {
23
+ readonly html: "<pre><code>Embedded HTML</code></pre>";
24
+ readonly icon: typeof DataObjectIcon;
25
+ readonly label: "Embedded HTML";
26
+ readonly selectDefault: true;
27
+ }, {
28
+ readonly html: "<hr>";
29
+ readonly icon: typeof DividerIcon;
30
+ readonly label: "Divider";
31
+ readonly selectDefault: false;
32
+ }];
33
+ export type SpecialBlockAction = (typeof specialBlockActions)[number];
34
+ type SpecialBlockToolProps = {
35
+ onHoverChange: (hovering: boolean) => void;
36
+ onInsert: (action: SpecialBlockAction) => void;
37
+ toolHover: boolean;
38
+ top: number;
39
+ visible: boolean;
40
+ };
41
+ export declare function SpecialBlockTool({ onHoverChange, onInsert, toolHover, top, visible, }: SpecialBlockToolProps): import("react/jsx-runtime").JSX.Element;
42
+ export {};
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AddIcon, CloseIcon, CodeIcon, DataObjectIcon, DividerIcon, ImageIcon, QuoteIcon, TitleIcon, } from "./RichTextIcons.js";
4
+ import { SpecialBlockOption } from "./SpecialBlockOption.js";
5
+ export const specialBlockActions = [
6
+ {
7
+ html: '<figure data-placeholder="image"><p>Image placeholder</p></figure>',
8
+ icon: ImageIcon,
9
+ label: "Image",
10
+ selectDefault: true,
11
+ },
12
+ {
13
+ html: '<blockquote data-placeholder="quote"></blockquote>',
14
+ icon: QuoteIcon,
15
+ label: "Quote",
16
+ selectDefault: true,
17
+ },
18
+ {
19
+ html: "<h2>Title</h2>",
20
+ icon: TitleIcon,
21
+ label: "Title",
22
+ selectDefault: true,
23
+ },
24
+ {
25
+ html: "<pre><code>Code block</code></pre>",
26
+ icon: CodeIcon,
27
+ label: "Code block",
28
+ selectDefault: true,
29
+ },
30
+ {
31
+ html: "<pre><code>Embedded HTML</code></pre>",
32
+ icon: DataObjectIcon,
33
+ label: "Embedded HTML",
34
+ selectDefault: true,
35
+ },
36
+ {
37
+ html: "<hr>",
38
+ icon: DividerIcon,
39
+ label: "Divider",
40
+ selectDefault: false,
41
+ },
42
+ ];
43
+ // Renders the floating insertion control for image, quote, title, code, and divider blocks.
44
+ export function SpecialBlockTool({ onHoverChange, onInsert, toolHover, top, visible, }) {
45
+ return (_jsxs("div", { "aria-hidden": !visible, className: `bayon-rte-special-tool${visible ? " bayon-rte-special-tool--visible" : ""}`, onMouseEnter: () => onHoverChange(true), onMouseLeave: () => onHoverChange(false), style: { "--bayon-rte-special-tool-top": `${top}px` }, children: [_jsx("button", { "aria-label": toolHover ? "Close special blocks" : "Add special block", className: "bayon-rte-icon-button bayon-rte-special-button bayon-rte-special-toggle", onClick: () => onHoverChange(!toolHover), style: {
46
+ transform: visible
47
+ ? "scale(1) rotate(0deg)"
48
+ : "scale(0.84) rotate(-12deg)",
49
+ }, title: toolHover ? "Close special blocks" : "Add special block", type: "button", children: toolHover ? _jsx(CloseIcon, { size: 20 }) : _jsx(AddIcon, { size: 20 }) }), toolHover ? (_jsx("div", { className: "bayon-rte-special-tool__actions", children: specialBlockActions.map((action) => (_jsx(SpecialBlockOption, { action: action, onInsert: onInsert }, action.label))) })) : null] }));
50
+ }
@@ -0,0 +1,19 @@
1
+ type TranscriptionControlProps = {
2
+ disabled?: boolean;
3
+ language?: string;
4
+ onTranscript: (text: string) => void;
5
+ };
6
+ export declare function TranscriptionControl({ disabled, language, onTranscript, }: TranscriptionControlProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function getTranscriptionControlState({ disabled, errorMessage, lastInserted, recording, supported, }: {
8
+ disabled: boolean;
9
+ errorMessage: string | null;
10
+ lastInserted: boolean;
11
+ recording: boolean;
12
+ supported: boolean;
13
+ }): {
14
+ buttonLabel: string;
15
+ disabled: boolean;
16
+ statusLabel: string | null;
17
+ tone: "error" | "ready" | "recording" | "success" | "unavailable";
18
+ };
19
+ export {};
@@ -0,0 +1,129 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { MicIcon, StopIcon } from "./RichTextIcons.js";
5
+ export function TranscriptionControl({ disabled = false, language, onTranscript, }) {
6
+ const recognitionRef = useRef(null);
7
+ const [errorMessage, setErrorMessage] = useState(null);
8
+ const [lastInserted, setLastInserted] = useState(false);
9
+ const [recording, setRecording] = useState(false);
10
+ const recognitionSupported = typeof window === "undefined" || Boolean(getSpeechRecognitionConstructor());
11
+ const controlState = getTranscriptionControlState({
12
+ disabled,
13
+ errorMessage,
14
+ lastInserted,
15
+ recording,
16
+ supported: recognitionSupported,
17
+ });
18
+ useEffect(() => {
19
+ return () => {
20
+ recognitionRef.current?.stop();
21
+ recognitionRef.current = null;
22
+ };
23
+ }, []);
24
+ function stopRecording() {
25
+ recognitionRef.current?.stop();
26
+ recognitionRef.current = null;
27
+ setRecording(false);
28
+ setLastInserted(false);
29
+ }
30
+ function startRecording() {
31
+ const Recognition = getSpeechRecognitionConstructor();
32
+ if (!Recognition || disabled) {
33
+ setErrorMessage("Browser transcription is unavailable.");
34
+ setLastInserted(false);
35
+ return;
36
+ }
37
+ const recognition = new Recognition();
38
+ recognition.continuous = true;
39
+ recognition.interimResults = false;
40
+ if (language?.trim()) {
41
+ recognition.lang = language.trim();
42
+ }
43
+ recognition.onresult = (event) => {
44
+ const transcript = readFinalTranscript(event);
45
+ if (transcript) {
46
+ onTranscript(transcript);
47
+ setLastInserted(true);
48
+ }
49
+ };
50
+ recognition.onerror = (event) => {
51
+ setErrorMessage(readRecognitionErrorMessage(event));
52
+ setLastInserted(false);
53
+ setRecording(false);
54
+ };
55
+ recognition.onend = () => {
56
+ setRecording(false);
57
+ recognitionRef.current = null;
58
+ };
59
+ recognitionRef.current = recognition;
60
+ setErrorMessage(null);
61
+ setLastInserted(false);
62
+ setRecording(true);
63
+ recognition.start();
64
+ }
65
+ return (_jsxs("div", { "aria-label": "Browser transcription", className: `bayon-rte-transcription bayon-rte-transcription--${controlState.tone}`, children: [_jsx("button", { "aria-label": controlState.buttonLabel, "aria-pressed": recording, className: `bayon-rte-icon-button bayon-rte-transcription__button${recording ? " bayon-rte-transcription__button--recording" : ""}`, disabled: controlState.disabled, onClick: () => (recording ? stopRecording() : startRecording()), title: controlState.buttonLabel, type: "button", children: recording ? _jsx(StopIcon, { size: 18 }) : _jsx(MicIcon, { size: 18 }) }), controlState.statusLabel ? (_jsxs("small", { className: "bayon-rte-transcription__status", role: "status", children: [_jsx("span", { "aria-hidden": "true", className: "bayon-rte-transcription__dot" }), controlState.statusLabel] })) : null] }));
66
+ }
67
+ export function getTranscriptionControlState({ disabled, errorMessage, lastInserted, recording, supported, }) {
68
+ if (!supported || disabled) {
69
+ return {
70
+ buttonLabel: "Transcription unavailable",
71
+ disabled: true,
72
+ statusLabel: "Unavailable",
73
+ tone: "unavailable",
74
+ };
75
+ }
76
+ if (errorMessage) {
77
+ return {
78
+ buttonLabel: "Transcribe",
79
+ disabled: false,
80
+ statusLabel: errorMessage,
81
+ tone: "error",
82
+ };
83
+ }
84
+ if (recording) {
85
+ return {
86
+ buttonLabel: "Stop transcription",
87
+ disabled: false,
88
+ statusLabel: "Listening",
89
+ tone: "recording",
90
+ };
91
+ }
92
+ if (lastInserted) {
93
+ return {
94
+ buttonLabel: "Transcribe",
95
+ disabled: false,
96
+ statusLabel: "Added",
97
+ tone: "success",
98
+ };
99
+ }
100
+ return {
101
+ buttonLabel: "Transcribe",
102
+ disabled: false,
103
+ statusLabel: null,
104
+ tone: "ready",
105
+ };
106
+ }
107
+ function getSpeechRecognitionConstructor() {
108
+ if (typeof window === "undefined") {
109
+ return null;
110
+ }
111
+ const speechWindow = window;
112
+ return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition ?? null;
113
+ }
114
+ function readFinalTranscript(event) {
115
+ let text = "";
116
+ for (let index = event.resultIndex; index < event.results.length; index += 1) {
117
+ const result = event.results[index];
118
+ if (result?.isFinal) {
119
+ text += result[0]?.transcript ?? "";
120
+ }
121
+ }
122
+ return text.trim();
123
+ }
124
+ function readRecognitionErrorMessage(event) {
125
+ if (event.error === "not-allowed" || event.error === "service-not-allowed") {
126
+ return "Microphone access was blocked.";
127
+ }
128
+ return "Transcription stopped.";
129
+ }
@@ -0,0 +1,9 @@
1
+ export type UnsavedChangesDialogProps = {
2
+ error: string | null;
3
+ onDiscardAndLeave: () => void;
4
+ onSaveAndLeave: () => void;
5
+ onStay: () => void;
6
+ open: boolean;
7
+ saving: boolean;
8
+ };
9
+ export declare function UnsavedChangesDialog({ error, onDiscardAndLeave, onSaveAndLeave, onStay, open, saving, }: UnsavedChangesDialogProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { RichTextStyleScope } from "./RichTextStyles.js";
4
+ export function UnsavedChangesDialog({ error, onDiscardAndLeave, onSaveAndLeave, onStay, open, saving, }) {
5
+ if (!open) {
6
+ return null;
7
+ }
8
+ return (_jsxs("div", { className: "bayon-rte-unsaved-backdrop", onMouseDown: () => {
9
+ if (!saving) {
10
+ onStay();
11
+ }
12
+ }, children: [_jsx(RichTextStyleScope, {}), _jsxs("section", { "aria-describedby": "unsaved-changes-description", "aria-labelledby": "unsaved-changes-title", className: "bayon-rte-unsaved-dialog", onMouseDown: (event) => event.stopPropagation(), role: "dialog", children: [_jsx("h2", { id: "unsaved-changes-title", children: "Unsaved changes" }), _jsx("p", { id: "unsaved-changes-description", children: "Save your changes before leaving, discard them, or stay on this page." }), error ? (_jsx("div", { className: "bayon-rte-unsaved-error", role: "alert", children: error })) : null, _jsxs("div", { className: "bayon-rte-unsaved-actions", children: [_jsx("button", { className: "bayon-rte-button", disabled: saving, onClick: onStay, type: "button", children: "Stay" }), _jsx("button", { className: "bayon-rte-button bayon-rte-button--danger", disabled: saving, onClick: onDiscardAndLeave, type: "button", children: "Discard and leave" }), _jsx("button", { className: "bayon-rte-button bayon-rte-button--primary", disabled: saving, onClick: onSaveAndLeave, type: "button", children: saving ? "Saving..." : "Save and leave" })] })] })] }));
13
+ }
@@ -0,0 +1,18 @@
1
+ import type { BlockDropPlacement } from "./blockActions";
2
+ export type BlockActionToolPlacement = {
3
+ blockId: string;
4
+ bottom: number;
5
+ top: number;
6
+ };
7
+ export type BlockActionToolVisibilityState = {
8
+ activeBlockId: string | null;
9
+ draggedBlockId: string | null;
10
+ focusedBlockId: string | null;
11
+ hoveredBlockId: string | null;
12
+ };
13
+ export declare function getVisibleBlockActionToolPlacements(placements: BlockActionToolPlacement[], state: BlockActionToolVisibilityState): BlockActionToolPlacement[];
14
+ export declare function getPointerDropTarget(placements: BlockActionToolPlacement[], draggedBlockId: string, pointerClientY: number): {
15
+ placement: BlockDropPlacement;
16
+ targetBlockId: string;
17
+ } | null;
18
+ export declare function getUniqueBlockActionToolPlacements(placements: BlockActionToolPlacement[]): BlockActionToolPlacement[];
@@ -0,0 +1,53 @@
1
+ export function getVisibleBlockActionToolPlacements(placements, state) {
2
+ const uniquePlacements = getUniqueBlockActionToolPlacements(placements);
3
+ const visibleBlockId = state.activeBlockId ??
4
+ state.draggedBlockId ??
5
+ state.focusedBlockId ??
6
+ state.hoveredBlockId;
7
+ return visibleBlockId
8
+ ? uniquePlacements.filter((placement) => placement.blockId === visibleBlockId)
9
+ : [];
10
+ }
11
+ export function getPointerDropTarget(placements, draggedBlockId, pointerClientY) {
12
+ const targetPlacements = getUniqueBlockActionToolPlacements(placements).filter((placement) => {
13
+ return placement.blockId !== draggedBlockId;
14
+ });
15
+ const targetPlacement = targetPlacements.find((placement) => {
16
+ return (pointerClientY >= placement.top && pointerClientY <= placement.bottom);
17
+ }) ??
18
+ targetPlacements.reduce((closestPlacement, placement) => {
19
+ if (!closestPlacement) {
20
+ return placement;
21
+ }
22
+ return getDistanceToPlacement(placement, pointerClientY) <
23
+ getDistanceToPlacement(closestPlacement, pointerClientY)
24
+ ? placement
25
+ : closestPlacement;
26
+ }, null);
27
+ if (!targetPlacement) {
28
+ return null;
29
+ }
30
+ return {
31
+ placement: pointerClientY >
32
+ targetPlacement.top + (targetPlacement.bottom - targetPlacement.top) / 2
33
+ ? "after"
34
+ : "before",
35
+ targetBlockId: targetPlacement.blockId,
36
+ };
37
+ }
38
+ export function getUniqueBlockActionToolPlacements(placements) {
39
+ const seenBlockIds = new Set();
40
+ return placements.filter((placement) => {
41
+ if (seenBlockIds.has(placement.blockId)) {
42
+ return false;
43
+ }
44
+ seenBlockIds.add(placement.blockId);
45
+ return true;
46
+ });
47
+ }
48
+ function getDistanceToPlacement(placement, pointerClientY) {
49
+ if (pointerClientY >= placement.top && pointerClientY <= placement.bottom) {
50
+ return 0;
51
+ }
52
+ return Math.min(Math.abs(pointerClientY - placement.top), Math.abs(pointerClientY - placement.bottom));
53
+ }
@@ -0,0 +1,8 @@
1
+ import type { RichTextBlock } from "../types";
2
+ export type BlockDropPlacement = "before" | "after";
3
+ export declare function isBlockActionTarget(block: RichTextBlock): boolean;
4
+ export declare function reorderBlock(blocks: RichTextBlock[], draggedBlockId: string, targetBlockId: string, placement?: BlockDropPlacement): RichTextBlock[];
5
+ export declare function deleteBlockById(blocks: RichTextBlock[], blockId: string): RichTextBlock[];
6
+ export declare function convertCheckboxBlockToParagraph(blocks: RichTextBlock[], blockId: string): RichTextBlock[];
7
+ export declare function blockToClipboardText(block: RichTextBlock): string;
8
+ export declare function blockActionToClipboardText(blocks: RichTextBlock[], blockId: string): string;
@@ -0,0 +1,111 @@
1
+ import { richTextBlocksToPlainText, sanitizeRichTextBlocks } from "../richText.js";
2
+ export function isBlockActionTarget(block) {
3
+ if (block.type === "paragraph") {
4
+ return blockToClipboardText(block).trim().length > 0;
5
+ }
6
+ return true;
7
+ }
8
+ export function reorderBlock(blocks, draggedBlockId, targetBlockId, placement = "before") {
9
+ if (draggedBlockId === targetBlockId) {
10
+ return sanitizeRichTextBlocks(blocks);
11
+ }
12
+ const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
13
+ const draggedRange = getSingleBlockRange(sanitizedBlocks, draggedBlockId);
14
+ const targetRange = getSingleBlockRange(sanitizedBlocks, targetBlockId);
15
+ if (!draggedRange ||
16
+ !targetRange ||
17
+ rangesOverlap(draggedRange, targetRange)) {
18
+ return sanitizedBlocks;
19
+ }
20
+ const draggedBlocks = sanitizedBlocks.slice(draggedRange.start, draggedRange.end);
21
+ const withoutDraggedBlocks = [
22
+ ...sanitizedBlocks.slice(0, draggedRange.start),
23
+ ...sanitizedBlocks.slice(draggedRange.end),
24
+ ];
25
+ const remainingTargetRange = getSingleBlockRange(withoutDraggedBlocks, targetBlockId);
26
+ if (!remainingTargetRange) {
27
+ return sanitizedBlocks;
28
+ }
29
+ const insertionIndex = placement === "after"
30
+ ? remainingTargetRange.end
31
+ : remainingTargetRange.start;
32
+ return [
33
+ ...withoutDraggedBlocks.slice(0, insertionIndex),
34
+ ...draggedBlocks,
35
+ ...withoutDraggedBlocks.slice(insertionIndex),
36
+ ];
37
+ }
38
+ export function deleteBlockById(blocks, blockId) {
39
+ const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
40
+ const range = getSingleBlockRange(sanitizedBlocks, blockId);
41
+ if (!range) {
42
+ return sanitizedBlocks;
43
+ }
44
+ const remainingBlocks = [
45
+ ...sanitizedBlocks.slice(0, range.start),
46
+ ...sanitizedBlocks.slice(range.end),
47
+ ];
48
+ return remainingBlocks.length > 0
49
+ ? remainingBlocks
50
+ : [{ id: "block-empty", markdown: "", type: "paragraph" }];
51
+ }
52
+ export function convertCheckboxBlockToParagraph(blocks, blockId) {
53
+ return sanitizeRichTextBlocks(blocks).map((block) => {
54
+ if (block.id !== blockId || block.type !== "checkbox") {
55
+ return block;
56
+ }
57
+ return {
58
+ id: block.id,
59
+ markdown: block.markdown,
60
+ type: "paragraph",
61
+ };
62
+ });
63
+ }
64
+ export function blockToClipboardText(block) {
65
+ if (block.type === "checkbox") {
66
+ const text = richTextBlocksToPlainText([block]);
67
+ return text ? `[${block.checked ? "x" : " "}] ${text}` : "";
68
+ }
69
+ return richTextBlocksToPlainText([block]);
70
+ }
71
+ export function blockActionToClipboardText(blocks, blockId) {
72
+ const sanitizedBlocks = sanitizeRichTextBlocks(blocks);
73
+ const range = getActionBlockRange(sanitizedBlocks, blockId);
74
+ if (!range) {
75
+ return "";
76
+ }
77
+ return sanitizedBlocks
78
+ .slice(range.start, range.end)
79
+ .map(blockToClipboardText)
80
+ .filter((text) => text.trim())
81
+ .join("\n");
82
+ }
83
+ function getSingleBlockRange(blocks, blockId) {
84
+ const index = blocks.findIndex((block) => {
85
+ return block.id === blockId;
86
+ });
87
+ return index === -1 ? null : { end: index + 1, start: index };
88
+ }
89
+ function getActionBlockRange(blocks, blockId) {
90
+ const index = blocks.findIndex((block) => {
91
+ return block.id === blockId;
92
+ });
93
+ if (index === -1) {
94
+ return null;
95
+ }
96
+ if (blocks[index]?.type !== "checkbox") {
97
+ return { end: index + 1, start: index };
98
+ }
99
+ let start = index;
100
+ let end = index + 1;
101
+ while (start > 0 && blocks[start - 1]?.type === "checkbox") {
102
+ start -= 1;
103
+ }
104
+ while (end < blocks.length && blocks[end]?.type === "checkbox") {
105
+ end += 1;
106
+ }
107
+ return { end, start };
108
+ }
109
+ function rangesOverlap(first, second) {
110
+ return first.start < second.end && second.start < first.end;
111
+ }
@@ -0,0 +1,19 @@
1
+ export declare function isCursorOnFirstLine(value: string, selectionStart: number): boolean;
2
+ type SelectAllShortcutLike = {
3
+ altKey?: boolean;
4
+ ctrlKey?: boolean;
5
+ key: string;
6
+ metaKey?: boolean;
7
+ shiftKey?: boolean;
8
+ };
9
+ type SelectionLike = {
10
+ rangeCount: number;
11
+ getRangeAt: (index: number) => {
12
+ startContainer: Node;
13
+ };
14
+ };
15
+ export type EditorKeyboardShortcutCommand = "bold" | "italic" | "link";
16
+ export declare function isSelectAllShortcut(event: SelectAllShortcutLike): boolean | undefined;
17
+ export declare function getEditorKeyboardShortcut(event: SelectAllShortcutLike): EditorKeyboardShortcutCommand | null;
18
+ export declare function getCodeBlockSelectionTarget(root: Element, selection: SelectionLike | null): Element | null;
19
+ export {};
@@ -0,0 +1,39 @@
1
+ export function isCursorOnFirstLine(value, selectionStart) {
2
+ return !value.slice(0, selectionStart).includes("\n");
3
+ }
4
+ export function isSelectAllShortcut(event) {
5
+ return (event.key.toLowerCase() === "a" &&
6
+ (event.ctrlKey || event.metaKey) &&
7
+ !event.altKey &&
8
+ !event.shiftKey);
9
+ }
10
+ export function getEditorKeyboardShortcut(event) {
11
+ if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey) {
12
+ return null;
13
+ }
14
+ const key = event.key.toLowerCase();
15
+ if (key === "b") {
16
+ return "bold";
17
+ }
18
+ if (key === "i") {
19
+ return "italic";
20
+ }
21
+ if (key === "k") {
22
+ return "link";
23
+ }
24
+ return null;
25
+ }
26
+ export function getCodeBlockSelectionTarget(root, selection) {
27
+ if (!selection || selection.rangeCount === 0) {
28
+ return null;
29
+ }
30
+ const { startContainer } = selection.getRangeAt(0);
31
+ const activeElement = startContainer.nodeType === 3
32
+ ? startContainer.parentElement
33
+ : startContainer.nodeType === 1
34
+ ? startContainer
35
+ : null;
36
+ const code = activeElement?.closest("pre code");
37
+ const pre = code?.closest("pre");
38
+ return code && pre && root.contains(pre) ? code : null;
39
+ }
@@ -0,0 +1,20 @@
1
+ export type TextBlockShortcut = {
2
+ type: "paragraph";
3
+ markdown: string;
4
+ } | {
5
+ type: "heading";
6
+ markdown: string;
7
+ } | {
8
+ type: "quote";
9
+ markdown: string;
10
+ } | {
11
+ type: "checkbox";
12
+ checked: boolean;
13
+ markdown: string;
14
+ } | {
15
+ type: "code";
16
+ text: string;
17
+ } | {
18
+ type: "divider";
19
+ };
20
+ export declare function getTextBlockShortcut(markdown: string): TextBlockShortcut | null;