@bayonai/rich-text-editor 0.1.2 → 1.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.
Files changed (151) hide show
  1. package/BEHAVIOR.md +396 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +25 -6
  4. package/dist/core/blockTree.d.ts +14 -0
  5. package/dist/core/blockTree.js +126 -0
  6. package/dist/core/blockTypes.d.ts +6 -0
  7. package/dist/core/blockTypes.js +5 -0
  8. package/dist/core/exportImport.d.ts +59 -0
  9. package/dist/core/exportImport.js +51 -0
  10. package/dist/core/features.d.ts +59 -0
  11. package/dist/core/features.js +57 -0
  12. package/dist/core/imageBlockDiagnostics.d.ts +4 -0
  13. package/dist/core/imageBlockDiagnostics.js +19 -0
  14. package/dist/core/proFeatures.d.ts +60 -0
  15. package/dist/core/proFeatures.js +64 -0
  16. package/dist/{richText.d.ts → core/richText.d.ts} +2 -0
  17. package/dist/core/richText.js +566 -0
  18. package/dist/core/types.d.ts +78 -0
  19. package/dist/index.d.ts +14 -8
  20. package/dist/index.js +8 -5
  21. package/dist/react/editor/RichTextBody.d.ts +28 -0
  22. package/dist/react/editor/RichTextBody.js +131 -0
  23. package/dist/react/editor/RichTextEditor.d.ts +138 -0
  24. package/dist/react/editor/RichTextEditor.js +2925 -0
  25. package/dist/react/editor/RichTextRenderedBlock.d.ts +20 -0
  26. package/dist/react/editor/RichTextRenderedBlock.js +162 -0
  27. package/dist/react/editor/RichTextRenderer.d.ts +13 -0
  28. package/dist/react/editor/RichTextRenderer.js +16 -0
  29. package/dist/react/{RichTextTitleInput.d.ts → editor/RichTextTitleInput.d.ts} +11 -1
  30. package/dist/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +17 -2
  31. package/dist/react/editor/blockActions.d.ts +48 -0
  32. package/dist/react/editor/blockActions.js +495 -0
  33. package/dist/react/editor/editorHistory.d.ts +55 -0
  34. package/dist/react/editor/editorHistory.js +111 -0
  35. package/dist/react/{editorNavigation.d.ts → editor/editorNavigation.d.ts} +2 -0
  36. package/dist/react/{editorNavigation.js → editor/editorNavigation.js} +16 -0
  37. package/dist/react/editor/editorOperations.d.ts +10 -0
  38. package/dist/react/editor/editorOperations.js +3 -0
  39. package/dist/react/editor/editorSelection.d.ts +3 -0
  40. package/dist/react/editor/editorSelection.js +215 -0
  41. package/dist/react/{editorShortcuts.d.ts → editor/editorShortcuts.d.ts} +10 -0
  42. package/dist/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  43. package/dist/react/{RichTextIcons.d.ts → icons/RichTextIcons.d.ts} +3 -0
  44. package/dist/react/{RichTextIcons.js → icons/RichTextIcons.js} +9 -0
  45. package/dist/react/index.d.ts +12 -9
  46. package/dist/react/index.js +7 -6
  47. package/dist/react/{EditorSessionProvider.d.ts → session/EditorSessionProvider.d.ts} +2 -2
  48. package/dist/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  49. package/dist/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  50. package/dist/react/styles/RichTextStyles.js +1362 -0
  51. package/dist/react/{BlockActionTool.d.ts → tools/BlockActionTool.d.ts} +1 -1
  52. package/dist/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  53. package/dist/react/tools/LinkCreationInput.d.ts +9 -0
  54. package/dist/react/tools/LinkCreationInput.js +38 -0
  55. package/dist/react/{SelectionFormatToolbar.d.ts → tools/SelectionFormatToolbar.d.ts} +3 -2
  56. package/dist/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  57. package/dist/react/tools/SpecialBlockOption.d.ts +9 -0
  58. package/dist/react/tools/SpecialBlockOption.js +8 -0
  59. package/dist/react/tools/SpecialBlockTool.d.ts +91 -0
  60. package/dist/react/tools/SpecialBlockTool.js +125 -0
  61. package/dist/react/{TranscriptionControl.d.ts → tools/TranscriptionControl.d.ts} +9 -0
  62. package/dist/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +70 -9
  63. package/dist/react/tools/blockActionToolState.d.ts +41 -0
  64. package/dist/react/tools/blockActionToolState.js +177 -0
  65. package/dist/react/tools/imageBlockDiagnostics.d.ts +2 -0
  66. package/dist/react/tools/imageBlockDiagnostics.js +12 -0
  67. package/dist/{session.d.ts → session/session.d.ts} +1 -1
  68. package/dist-cjs/core/blockTree.js +137 -0
  69. package/dist-cjs/core/blockTypes.js +9 -0
  70. package/dist-cjs/core/exportImport.js +56 -0
  71. package/dist-cjs/core/features.js +62 -0
  72. package/dist-cjs/core/proFeatures.js +70 -0
  73. package/dist-cjs/core/richText.js +578 -0
  74. package/dist-cjs/index.js +22 -6
  75. package/dist-cjs/react/editor/RichTextBody.js +134 -0
  76. package/dist-cjs/react/editor/RichTextEditor.js +2956 -0
  77. package/dist-cjs/react/editor/RichTextRenderedBlock.js +166 -0
  78. package/dist-cjs/react/editor/RichTextRenderer.js +20 -0
  79. package/dist-cjs/react/{RichTextTitleInput.js → editor/RichTextTitleInput.js} +18 -2
  80. package/dist-cjs/react/editor/blockActions.js +518 -0
  81. package/dist-cjs/react/editor/editorHistory.js +120 -0
  82. package/dist-cjs/react/{editorNavigation.js → editor/editorNavigation.js} +17 -0
  83. package/dist-cjs/react/editor/editorOperations.js +6 -0
  84. package/dist-cjs/react/editor/editorSelection.js +219 -0
  85. package/dist-cjs/react/{editorShortcuts.js → editor/editorShortcuts.js} +17 -1
  86. package/dist-cjs/react/{RichTextIcons.js → icons/RichTextIcons.js} +12 -0
  87. package/dist-cjs/react/index.js +9 -7
  88. package/dist-cjs/react/{EditorSessionProvider.js → session/EditorSessionProvider.js} +3 -3
  89. package/dist-cjs/react/{UnsavedChangesDialog.js → session/UnsavedChangesDialog.js} +1 -1
  90. package/dist-cjs/react/styles/RichTextStyles.js +1365 -0
  91. package/dist-cjs/react/{BlockActionTool.js → tools/BlockActionTool.js} +6 -2
  92. package/dist-cjs/react/tools/LinkCreationInput.js +41 -0
  93. package/dist-cjs/react/{SelectionFormatToolbar.js → tools/SelectionFormatToolbar.js} +3 -3
  94. package/dist-cjs/react/tools/SpecialBlockOption.js +11 -0
  95. package/dist-cjs/react/tools/SpecialBlockTool.js +129 -0
  96. package/dist-cjs/react/{TranscriptionControl.js → tools/TranscriptionControl.js} +71 -9
  97. package/dist-cjs/react/tools/blockActionToolState.js +186 -0
  98. package/package.json +3 -2
  99. package/dist/react/RichTextBody.d.ts +0 -18
  100. package/dist/react/RichTextBody.js +0 -66
  101. package/dist/react/RichTextEditor.d.ts +0 -45
  102. package/dist/react/RichTextEditor.js +0 -1096
  103. package/dist/react/RichTextRenderedBlock.d.ts +0 -4
  104. package/dist/react/RichTextRenderedBlock.js +0 -36
  105. package/dist/react/RichTextRenderer.d.ts +0 -4
  106. package/dist/react/RichTextRenderer.js +0 -8
  107. package/dist/react/RichTextStyles.js +0 -719
  108. package/dist/react/SpecialBlockOption.d.ts +0 -7
  109. package/dist/react/SpecialBlockOption.js +0 -7
  110. package/dist/react/SpecialBlockTool.d.ts +0 -42
  111. package/dist/react/SpecialBlockTool.js +0 -50
  112. package/dist/react/blockActionToolState.d.ts +0 -18
  113. package/dist/react/blockActionToolState.js +0 -53
  114. package/dist/react/blockActions.d.ts +0 -8
  115. package/dist/react/blockActions.js +0 -111
  116. package/dist/richText.js +0 -297
  117. package/dist/types.d.ts +0 -34
  118. package/dist-cjs/react/RichTextBody.js +0 -69
  119. package/dist-cjs/react/RichTextEditor.js +0 -1108
  120. package/dist-cjs/react/RichTextRenderedBlock.js +0 -39
  121. package/dist-cjs/react/RichTextRenderer.js +0 -11
  122. package/dist-cjs/react/RichTextStyles.js +0 -722
  123. package/dist-cjs/react/SpecialBlockOption.js +0 -10
  124. package/dist-cjs/react/SpecialBlockTool.js +0 -54
  125. package/dist-cjs/react/blockActionToolState.js +0 -58
  126. package/dist-cjs/react/blockActions.js +0 -119
  127. package/dist-cjs/richText.js +0 -307
  128. /package/dist/{types.js → core/types.js} +0 -0
  129. /package/dist/{writingStats.d.ts → core/writingStats.d.ts} +0 -0
  130. /package/dist/{writingStats.js → core/writingStats.js} +0 -0
  131. /package/dist/react/{RichTextDocumentSurface.d.ts → editor/RichTextDocumentSurface.d.ts} +0 -0
  132. /package/dist/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  133. /package/dist/react/{UnsavedChangesDialog.d.ts → session/UnsavedChangesDialog.d.ts} +0 -0
  134. /package/dist/react/{RichTextStyles.d.ts → styles/RichTextStyles.d.ts} +0 -0
  135. /package/dist/react/{richTextBlockStyles.d.ts → styles/richTextBlockStyles.d.ts} +0 -0
  136. /package/dist/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  137. /package/dist/react/{specialBlockStyles.d.ts → styles/specialBlockStyles.d.ts} +0 -0
  138. /package/dist/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  139. /package/dist/{saveControl.d.ts → session/saveControl.d.ts} +0 -0
  140. /package/dist/{saveControl.js → session/saveControl.js} +0 -0
  141. /package/dist/{session.js → session/session.js} +0 -0
  142. /package/dist/{sessionRegistry.d.ts → session/sessionRegistry.d.ts} +0 -0
  143. /package/dist/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
  144. /package/dist-cjs/{types.js → core/types.js} +0 -0
  145. /package/dist-cjs/{writingStats.js → core/writingStats.js} +0 -0
  146. /package/dist-cjs/react/{RichTextDocumentSurface.js → editor/RichTextDocumentSurface.js} +0 -0
  147. /package/dist-cjs/react/{richTextBlockStyles.js → styles/richTextBlockStyles.js} +0 -0
  148. /package/dist-cjs/react/{specialBlockStyles.js → styles/specialBlockStyles.js} +0 -0
  149. /package/dist-cjs/{saveControl.js → session/saveControl.js} +0 -0
  150. /package/dist-cjs/{session.js → session/session.js} +0 -0
  151. /package/dist-cjs/{sessionRegistry.js → session/sessionRegistry.js} +0 -0
@@ -4,13 +4,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.BlockActionTool = BlockActionTool;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
- const RichTextIcons_1 = require("./RichTextIcons");
7
+ const RichTextIcons_1 = require("../icons/RichTextIcons");
8
+ const blockActionToolState_1 = require("../tools/blockActionToolState");
8
9
  function BlockActionTool({ activeBlockId, draggedBlockId, onAction, onHoverChange, onPointerDragEnd, onPointerDragMove, onPointerDragStart, onToggleMenu, placement, }) {
9
10
  const menuOpen = activeBlockId === placement.blockId;
10
11
  const dragging = draggedBlockId === placement.blockId;
11
12
  const pointerStartRef = (0, react_1.useRef)(null);
12
13
  const suppressClickRef = (0, react_1.useRef)(false);
13
- return ((0, jsx_runtime_1.jsxs)("div", { className: `bayon-rte-block-tool${menuOpen ? " bayon-rte-block-tool--menu-open" : ""}`, "data-block-action-tool": placement.blockId, onMouseEnter: () => onHoverChange(true), onMouseLeave: () => onHoverChange(false), style: { "--bayon-rte-block-tool-top": `${placement.top}px` }, children: [menuOpen ? ((0, jsx_runtime_1.jsx)("button", { "aria-label": "Close block actions", className: "bayon-rte-icon-button bayon-rte-block-handle bayon-rte-block-handle--menu-open", onClick: () => onToggleMenu(placement.blockId), onMouseDown: (event) => event.preventDefault(), title: "Close block actions", type: "button", children: (0, jsx_runtime_1.jsx)(RichTextIcons_1.CloseIcon, { size: 19 }) })) : ((0, jsx_runtime_1.jsx)("button", { "aria-label": "Block actions", "aria-pressed": dragging, className: `bayon-rte-icon-button bayon-rte-block-handle${dragging ? " bayon-rte-block-handle--dragging" : ""}`, onClick: () => {
14
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `bayon-rte-block-tool${menuOpen ? " bayon-rte-block-tool--menu-open" : ""}${placement.blockType === "image" ? " bayon-rte-block-tool--image" : ""}`, "data-block-action-tool": placement.blockId, onMouseEnter: () => onHoverChange(true), onMouseLeave: () => onHoverChange(false), style: {
15
+ "--bayon-rte-block-tool-left": `${placement.left}px`,
16
+ "--bayon-rte-block-tool-top": `${(0, blockActionToolState_1.getBlockActionToolRenderTop)(placement)}px`,
17
+ }, children: [menuOpen ? ((0, jsx_runtime_1.jsx)("button", { "aria-label": "Close block actions", className: "bayon-rte-icon-button bayon-rte-block-handle bayon-rte-block-handle--menu-open", onClick: () => onToggleMenu(placement.blockId), onMouseDown: (event) => event.preventDefault(), title: "Close block actions", type: "button", children: (0, jsx_runtime_1.jsx)(RichTextIcons_1.CloseIcon, { size: 19 }) })) : ((0, jsx_runtime_1.jsx)("button", { "aria-label": "Block actions", "aria-pressed": dragging, className: `bayon-rte-icon-button bayon-rte-block-handle${dragging ? " bayon-rte-block-handle--dragging" : ""}`, onClick: () => {
14
18
  if (suppressClickRef.current) {
15
19
  suppressClickRef.current = false;
16
20
  return;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.LinkCreationInput = LinkCreationInput;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const RichTextIcons_1 = require("../icons/RichTextIcons");
8
+ // Renders the focused URL input used by selection link creation.
9
+ function LinkCreationInput({ left, onCancel, onSubmit, placement, top, }) {
10
+ const inputRef = (0, react_1.useRef)(null);
11
+ const [href, setHref] = (0, react_1.useState)("https://");
12
+ (0, react_1.useEffect)(() => {
13
+ inputRef.current?.focus();
14
+ inputRef.current?.select();
15
+ }, []);
16
+ function handleSubmit(event) {
17
+ event.preventDefault();
18
+ submitHref();
19
+ }
20
+ function submitHref() {
21
+ const nextHref = href.trim();
22
+ if (!nextHref) {
23
+ return;
24
+ }
25
+ onSubmit(nextHref);
26
+ }
27
+ function guardButtonMouseDown(event) {
28
+ event.preventDefault();
29
+ event.stopPropagation();
30
+ }
31
+ function handleKeyDown(event) {
32
+ if (event.key === "Escape") {
33
+ event.preventDefault();
34
+ onCancel();
35
+ }
36
+ }
37
+ return ((0, jsx_runtime_1.jsxs)("form", { "aria-label": "Create link", className: `bayon-rte-link-input bayon-rte-link-input--${placement}`, onSubmit: handleSubmit, style: {
38
+ "--bayon-rte-link-input-left": `${left}px`,
39
+ "--bayon-rte-link-input-top": `${top}px`,
40
+ }, children: [(0, jsx_runtime_1.jsx)("input", { "aria-label": "Link URL", className: "bayon-rte-link-input__field", onChange: (event) => setHref(event.target.value), onKeyDown: handleKeyDown, placeholder: "Paste or type a URL", ref: inputRef, type: "url", value: href }), (0, jsx_runtime_1.jsx)("button", { "aria-label": "Apply link", className: "bayon-rte-icon-button bayon-rte-link-input__action", onClick: submitHref, onMouseDown: guardButtonMouseDown, title: "Apply link", type: "button", children: (0, jsx_runtime_1.jsx)(RichTextIcons_1.LinkIcon, { size: 16 }) }), (0, jsx_runtime_1.jsx)("button", { "aria-label": "Cancel link", className: "bayon-rte-icon-button bayon-rte-link-input__action", onClick: onCancel, onMouseDown: guardButtonMouseDown, title: "Cancel link", type: "button", children: (0, jsx_runtime_1.jsx)(RichTextIcons_1.CloseIcon, { size: 16 }) })] }));
41
+ }
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.selectionActions = void 0;
5
5
  exports.SelectionFormatToolbar = SelectionFormatToolbar;
6
6
  const jsx_runtime_1 = require("react/jsx-runtime");
7
- const RichTextIcons_1 = require("./RichTextIcons");
7
+ const RichTextIcons_1 = require("../icons/RichTextIcons");
8
8
  exports.selectionActions = [
9
9
  { command: "bold", icon: RichTextIcons_1.BoldIcon, label: "Bold" },
10
10
  { command: "italic", icon: RichTextIcons_1.ItalicIcon, label: "Italic" },
@@ -14,8 +14,8 @@ exports.selectionActions = [
14
14
  { command: "code", icon: RichTextIcons_1.CodeIcon, label: "Code" },
15
15
  ];
16
16
  // Renders the floating toolbar used to format the current text selection.
17
- function SelectionFormatToolbar({ left, onAction, top, }) {
18
- return ((0, jsx_runtime_1.jsx)("div", { className: "bayon-rte-toolbar", style: {
17
+ function SelectionFormatToolbar({ left, onAction, placement, top, }) {
18
+ return ((0, jsx_runtime_1.jsx)("div", { className: `bayon-rte-toolbar bayon-rte-toolbar--${placement}`, style: {
19
19
  "--bayon-rte-toolbar-left": `${left}px`,
20
20
  "--bayon-rte-toolbar-top": `${top}px`,
21
21
  }, children: exports.selectionActions.map(({ command, icon: Icon, label }, index) => ((0, jsx_runtime_1.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: (0, jsx_runtime_1.jsx)(Icon, { size: 18 }) }, command))) }));
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SpecialBlockOption = SpecialBlockOption;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ // Renders one selectable special-block action inside the insertion tool.
7
+ function SpecialBlockOption({ action, disabled = false, onInsert, title = action.label, }) {
8
+ const Icon = action.icon;
9
+ const label = disabled ? `${action.label} unavailable` : action.label;
10
+ return ((0, jsx_runtime_1.jsx)("button", { "aria-label": label, className: "bayon-rte-icon-button bayon-rte-special-button", disabled: disabled, onClick: () => onInsert(action), onMouseDown: (event) => event.preventDefault(), title: title, type: "button", children: (0, jsx_runtime_1.jsx)(Icon, { size: 18 }) }));
11
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.specialBlockActions = void 0;
5
+ exports.SpecialBlockTool = SpecialBlockTool;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const features_1 = require("../../core/features");
8
+ const RichTextIcons_1 = require("../icons/RichTextIcons");
9
+ const SpecialBlockOption_1 = require("./SpecialBlockOption");
10
+ const specialBlockIcons = [
11
+ RichTextIcons_1.ImageIcon,
12
+ RichTextIcons_1.QuoteIcon,
13
+ RichTextIcons_1.TitleIcon,
14
+ RichTextIcons_1.BulletListIcon,
15
+ RichTextIcons_1.ToggleIcon,
16
+ RichTextIcons_1.CodeIcon,
17
+ RichTextIcons_1.DataObjectIcon,
18
+ RichTextIcons_1.DividerIcon,
19
+ ];
20
+ exports.specialBlockActions = [
21
+ {
22
+ icon: RichTextIcons_1.PasteIcon,
23
+ kind: "paste",
24
+ label: "Paste",
25
+ selectDefault: false,
26
+ },
27
+ {
28
+ featureId: "image-upload",
29
+ fallbackHtml: '<figure data-placeholder="image"><p>Image placeholder</p></figure>',
30
+ icon: RichTextIcons_1.ImageIcon,
31
+ kind: "image-upload",
32
+ label: "Image",
33
+ selectDefault: true,
34
+ },
35
+ {
36
+ html: '<blockquote data-placeholder="quote"></blockquote>',
37
+ icon: RichTextIcons_1.QuoteIcon,
38
+ label: "Quote",
39
+ selectDefault: true,
40
+ },
41
+ {
42
+ html: "<h2>Title</h2>",
43
+ icon: RichTextIcons_1.TitleIcon,
44
+ label: "Title",
45
+ selectDefault: true,
46
+ },
47
+ {
48
+ html: '<ul data-rich-text-children=""><li data-rich-text-bullet=""><div data-rich-text-row=""><span data-bullet-marker="" contenteditable="false" aria-hidden="true"></span><span data-bullet-label="" data-placeholder="List item"></span></div></li></ul>',
49
+ icon: RichTextIcons_1.BulletListIcon,
50
+ label: "Bullet list",
51
+ selectDefault: false,
52
+ },
53
+ {
54
+ focusPosition: "start",
55
+ focusSelector: "[data-toggle-label]",
56
+ html: '<ul data-rich-text-children=""><li data-rich-text-toggle="" data-toggle-collapsed="false"><div data-rich-text-row=""><button aria-label="Collapse toggle" class="bayon-rte-toggle-button" contenteditable="false" data-toggle-collapse="" title="Collapse toggle" type="button"><span class="bayon-rte-toggle-caret" aria-hidden="true"></span></button><span data-toggle-label="" data-placeholder="Toggle title"></span></div><div data-toggle-content="" data-placeholder="Toggle content"></div></li></ul>',
57
+ icon: RichTextIcons_1.ToggleIcon,
58
+ label: "Toggle",
59
+ selectDefault: false,
60
+ },
61
+ {
62
+ html: "<pre><code>Code block</code></pre>",
63
+ icon: RichTextIcons_1.CodeIcon,
64
+ label: "Code block",
65
+ selectDefault: true,
66
+ },
67
+ {
68
+ html: "<pre><code>Embedded HTML</code></pre>",
69
+ icon: RichTextIcons_1.DataObjectIcon,
70
+ label: "Embedded HTML",
71
+ selectDefault: true,
72
+ },
73
+ {
74
+ html: "<hr>",
75
+ icon: RichTextIcons_1.DividerIcon,
76
+ label: "Divider",
77
+ selectDefault: false,
78
+ },
79
+ ];
80
+ // Renders the floating insertion control for image, quote, title, code, and divider blocks.
81
+ function SpecialBlockTool({ features, lockedFeatureMode = "hide", onFeatureGate, onHoverChange, onInsert, onOpenChange, open, toolHover, top, visible, }) {
82
+ const actionItems = exports.specialBlockActions
83
+ .map((action) => ({
84
+ access: getSpecialBlockActionAccess(features, action),
85
+ action,
86
+ }))
87
+ .filter((item) => item.access.enabled ||
88
+ !hasSpecialBlockActionFeature(item.action) ||
89
+ lockedFeatureMode === "disabled");
90
+ return ((0, jsx_runtime_1.jsxs)("div", { "aria-hidden": !visible, className: `bayon-rte-special-tool${visible ? " bayon-rte-special-tool--visible" : ""}`, onMouseEnter: () => onHoverChange(true), onMouseLeave: () => {
91
+ onHoverChange(false);
92
+ onOpenChange(false);
93
+ }, style: { "--bayon-rte-special-tool-top": `${top}px` }, children: [(0, jsx_runtime_1.jsx)("button", { "aria-label": open ? "Close special blocks" : "Add special block", "aria-expanded": open, className: "bayon-rte-icon-button bayon-rte-special-button bayon-rte-special-toggle", onClick: () => onOpenChange(!open), style: {
94
+ transform: visible
95
+ ? "scale(1) rotate(0deg)"
96
+ : "scale(0.84) rotate(-12deg)",
97
+ }, title: open ? "Close special blocks" : "Add special block", type: "button", children: open ? (0, jsx_runtime_1.jsx)(RichTextIcons_1.CloseIcon, { size: 20 }) : (0, jsx_runtime_1.jsx)(RichTextIcons_1.AddIcon, { size: 20 }) }), open ? ((0, jsx_runtime_1.jsx)("div", { className: "bayon-rte-special-tool__actions", children: actionItems.map(({ access, action }) => {
98
+ const disabled = !access.enabled;
99
+ return ((0, jsx_runtime_1.jsx)(SpecialBlockOption_1.SpecialBlockOption, { action: action, disabled: disabled, onInsert: disabled
100
+ ? () => onFeatureGate?.({
101
+ featureId: access.featureId,
102
+ label: access.label,
103
+ reason: access.reason,
104
+ })
105
+ : onInsert, title: disabled
106
+ ? `${access.label}: ${getLockedFeatureTitle(access)}`
107
+ : action.label }, action.label));
108
+ }) })) : null] }));
109
+ }
110
+ function getSpecialBlockActionAccess(features, action) {
111
+ return hasSpecialBlockActionFeature(action)
112
+ ? (0, features_1.getRichTextFeatureAccess)(features, action.featureId)
113
+ : { enabled: true };
114
+ }
115
+ function hasSpecialBlockActionFeature(action) {
116
+ return "featureId" in action;
117
+ }
118
+ function getLockedFeatureTitle(access) {
119
+ if (access.enabled) {
120
+ return access.label;
121
+ }
122
+ if (access.reason === "premium") {
123
+ return "Premium feature";
124
+ }
125
+ if (access.reason === "unavailable") {
126
+ return "Unavailable";
127
+ }
128
+ return "Disabled";
129
+ }
@@ -3,37 +3,39 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.TranscriptionControl = TranscriptionControl;
5
5
  exports.getTranscriptionControlState = getTranscriptionControlState;
6
+ exports.resolveSpeechRecognitionSupport = resolveSpeechRecognitionSupport;
6
7
  const jsx_runtime_1 = require("react/jsx-runtime");
7
8
  const react_1 = require("react");
8
- const RichTextIcons_1 = require("./RichTextIcons");
9
+ const RichTextIcons_1 = require("../icons/RichTextIcons");
9
10
  function TranscriptionControl({ disabled = false, language, onTranscript, }) {
10
11
  const recognitionRef = (0, react_1.useRef)(null);
11
12
  const [errorMessage, setErrorMessage] = (0, react_1.useState)(null);
12
13
  const [lastInserted, setLastInserted] = (0, react_1.useState)(false);
13
14
  const [recording, setRecording] = (0, react_1.useState)(false);
14
- const recognitionSupported = typeof window === "undefined" || Boolean(getSpeechRecognitionConstructor());
15
+ const recognitionSupport = getSpeechRecognitionSupport();
15
16
  const controlState = getTranscriptionControlState({
16
17
  disabled,
17
18
  errorMessage,
18
19
  lastInserted,
19
20
  recording,
20
- supported: recognitionSupported,
21
+ supported: recognitionSupport.supported,
21
22
  });
22
23
  (0, react_1.useEffect)(() => {
23
24
  return () => {
24
- recognitionRef.current?.stop();
25
+ stopRecognitionInstance(recognitionRef.current);
25
26
  recognitionRef.current = null;
26
27
  };
27
28
  }, []);
28
29
  function stopRecording() {
29
- recognitionRef.current?.stop();
30
+ stopRecognitionInstance(recognitionRef.current);
30
31
  recognitionRef.current = null;
31
32
  setRecording(false);
32
33
  setLastInserted(false);
33
34
  }
34
35
  function startRecording() {
36
+ const support = getSpeechRecognitionSupport();
35
37
  const Recognition = getSpeechRecognitionConstructor();
36
- if (!Recognition || disabled) {
38
+ if (!support.supported || !Recognition || disabled) {
37
39
  setErrorMessage("Browser transcription is unavailable.");
38
40
  setLastInserted(false);
39
41
  return;
@@ -63,8 +65,17 @@ function TranscriptionControl({ disabled = false, language, onTranscript, }) {
63
65
  recognitionRef.current = recognition;
64
66
  setErrorMessage(null);
65
67
  setLastInserted(false);
66
- setRecording(true);
67
- recognition.start();
68
+ try {
69
+ recognition.start();
70
+ setRecording(true);
71
+ }
72
+ catch (error) {
73
+ console.warn("[rich-text-editor] Failed to start browser transcription.", error);
74
+ recognitionRef.current = null;
75
+ setErrorMessage("Transcription stopped.");
76
+ setLastInserted(false);
77
+ setRecording(false);
78
+ }
68
79
  }
69
80
  return ((0, jsx_runtime_1.jsxs)("div", { "aria-label": "Browser transcription", className: `bayon-rte-transcription bayon-rte-transcription--${controlState.tone}`, children: [(0, jsx_runtime_1.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 ? (0, jsx_runtime_1.jsx)(RichTextIcons_1.StopIcon, { size: 18 }) : (0, jsx_runtime_1.jsx)(RichTextIcons_1.MicIcon, { size: 18 }) }), controlState.statusLabel ? ((0, jsx_runtime_1.jsxs)("small", { className: "bayon-rte-transcription__status", role: "status", children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", className: "bayon-rte-transcription__dot" }), controlState.statusLabel] })) : null] }));
70
81
  }
@@ -113,7 +124,58 @@ function getSpeechRecognitionConstructor() {
113
124
  return null;
114
125
  }
115
126
  const speechWindow = window;
116
- return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition ?? null;
127
+ return (speechWindow.SpeechRecognition ??
128
+ speechWindow.webkitSpeechRecognition ??
129
+ null);
130
+ }
131
+ function getSpeechRecognitionSupport() {
132
+ if (typeof window === "undefined") {
133
+ return {
134
+ reason: "available",
135
+ supported: true,
136
+ };
137
+ }
138
+ const speechWindow = window;
139
+ return resolveSpeechRecognitionSupport({
140
+ hasSpeechRecognition: Boolean(speechWindow.SpeechRecognition),
141
+ hasWebkitSpeechRecognition: Boolean(speechWindow.webkitSpeechRecognition),
142
+ userAgent: window.navigator.userAgent,
143
+ });
144
+ }
145
+ function resolveSpeechRecognitionSupport({ hasSpeechRecognition, hasWebkitSpeechRecognition, userAgent, }) {
146
+ if (!hasSpeechRecognition && !hasWebkitSpeechRecognition) {
147
+ return {
148
+ reason: "missing-api",
149
+ supported: false,
150
+ };
151
+ }
152
+ if (hasWebkitSpeechRecognition && isMobileSafariUserAgent(userAgent)) {
153
+ return {
154
+ reason: "mobile-safari",
155
+ supported: false,
156
+ };
157
+ }
158
+ return {
159
+ reason: "available",
160
+ supported: true,
161
+ };
162
+ }
163
+ function isMobileSafariUserAgent(userAgent) {
164
+ const normalizedUserAgent = userAgent.toLowerCase();
165
+ const isIosMobile = /\b(iphone|ipod|ipad)\b/.test(normalizedUserAgent) ||
166
+ (normalizedUserAgent.includes("macintosh") &&
167
+ normalizedUserAgent.includes("mobile"));
168
+ const isSafari = normalizedUserAgent.includes("safari");
169
+ const isOtherIosBrowser = /\b(crios|fxios|edgios|opios)\b/.test(normalizedUserAgent);
170
+ return isIosMobile && isSafari && !isOtherIosBrowser;
171
+ }
172
+ function stopRecognitionInstance(recognition) {
173
+ try {
174
+ recognition?.stop();
175
+ }
176
+ catch (error) {
177
+ console.warn("[rich-text-editor] Failed to stop browser transcription.", error);
178
+ }
117
179
  }
118
180
  function readFinalTranscript(event) {
119
181
  let text = "";
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getBlockActionToolAnchorTop = getBlockActionToolAnchorTop;
4
+ exports.getBlockActionToolRenderTop = getBlockActionToolRenderTop;
5
+ exports.getVisibleBlockActionToolPlacements = getVisibleBlockActionToolPlacements;
6
+ exports.getPointerDropTarget = getPointerDropTarget;
7
+ exports.getPointerDragDropResult = getPointerDragDropResult;
8
+ exports.getPointerHoverLaneBlockId = getPointerHoverLaneBlockId;
9
+ exports.getUniqueBlockActionToolPlacements = getUniqueBlockActionToolPlacements;
10
+ const hoverLaneLeftWidth = 72;
11
+ const hoverLaneRightWidth = 36;
12
+ const hoverLaneVerticalPadding = 10;
13
+ const raisedBlockActionToolTopOffset = -4;
14
+ function getBlockActionToolAnchorTop(blockType, top) {
15
+ return blockType === "checkbox" ? top + raisedBlockActionToolTopOffset : top;
16
+ }
17
+ function getBlockActionToolRenderTop(placement) {
18
+ return placement.toolTop ?? placement.top;
19
+ }
20
+ function getVisibleBlockActionToolPlacements(placements, state) {
21
+ const uniquePlacements = getUniqueBlockActionToolPlacements(placements);
22
+ const visibleBlockId = state.activeBlockId ??
23
+ state.draggedBlockId ??
24
+ state.hoveredBlockId ??
25
+ state.focusedBlockId;
26
+ return visibleBlockId
27
+ ? uniquePlacements.filter((placement) => placement.blockId === visibleBlockId)
28
+ : [];
29
+ }
30
+ function getPointerDropTarget(placements, draggedBlockId, pointerClientY, pointerClientX) {
31
+ const targetPlacements = placements.filter((placement) => {
32
+ return placement.blockId !== draggedBlockId;
33
+ });
34
+ const rowPlacements = targetPlacements.filter((placement) => {
35
+ return placement.dropRole !== "content";
36
+ });
37
+ const contentPlacements = targetPlacements.filter((placement) => {
38
+ return placement.dropRole === "content";
39
+ });
40
+ const targetPlacement = findContainingPlacement(rowPlacements, pointerClientY) ??
41
+ findContainingPlacement(contentPlacements, pointerClientY) ??
42
+ getClosestPlacement(rowPlacements, pointerClientY) ??
43
+ getClosestPlacement(contentPlacements, pointerClientY);
44
+ if (!targetPlacement) {
45
+ return null;
46
+ }
47
+ if (targetPlacement.dropRole === "content") {
48
+ return {
49
+ placement: "inside",
50
+ targetBlockId: targetPlacement.blockId,
51
+ };
52
+ }
53
+ if (pointerClientX !== undefined &&
54
+ pointerClientX >= targetPlacement.left + 24) {
55
+ return {
56
+ placement: "inside",
57
+ targetBlockId: targetPlacement.blockId,
58
+ };
59
+ }
60
+ const visualAncestorDropTarget = pointerClientX === undefined
61
+ ? null
62
+ : getVisualAncestorDropTarget(rowPlacements, targetPlacement, pointerClientX);
63
+ if (visualAncestorDropTarget) {
64
+ return visualAncestorDropTarget;
65
+ }
66
+ return {
67
+ placement: pointerClientY >
68
+ targetPlacement.top + (targetPlacement.bottom - targetPlacement.top) / 2
69
+ ? "after"
70
+ : "before",
71
+ targetBlockId: targetPlacement.blockId,
72
+ };
73
+ }
74
+ function getPointerDragDropResult(placements, draggedBlockId, pointerClientY, pointerClientX) {
75
+ const containingPlacement = findContainingPlacement(placements, pointerClientY);
76
+ if (containingPlacement?.blockId === draggedBlockId) {
77
+ return {
78
+ dropTarget: getDropTargetFromPlacement(containingPlacement, pointerClientY, pointerClientX),
79
+ status: "invalid",
80
+ };
81
+ }
82
+ const dropTarget = getPointerDropTarget(placements, draggedBlockId, pointerClientY, pointerClientX);
83
+ if (!dropTarget) {
84
+ return { status: "invalid" };
85
+ }
86
+ const targetPlacement = placements.find((placement) => {
87
+ return (placement.blockId === dropTarget.targetBlockId &&
88
+ (dropTarget.placement === "inside" || placement.dropRole !== "content"));
89
+ });
90
+ if (dropTarget.targetBlockId === draggedBlockId ||
91
+ targetPlacement?.ancestorBlockIds?.includes(draggedBlockId)) {
92
+ return { dropTarget, status: "invalid" };
93
+ }
94
+ return { dropTarget, status: "valid" };
95
+ }
96
+ function getDropTargetFromPlacement(placement, pointerClientY, pointerClientX) {
97
+ if (placement.dropRole === "content" ||
98
+ (pointerClientX !== undefined && pointerClientX >= placement.left + 24)) {
99
+ return {
100
+ placement: "inside",
101
+ targetBlockId: placement.blockId,
102
+ };
103
+ }
104
+ return {
105
+ placement: pointerClientY > placement.top + (placement.bottom - placement.top) / 2
106
+ ? "after"
107
+ : "before",
108
+ targetBlockId: placement.blockId,
109
+ };
110
+ }
111
+ function getPointerHoverLaneBlockId(placements, pointerTop, pointerLeft) {
112
+ const placement = getUniqueBlockActionToolPlacements(placements).find((currentPlacement) => {
113
+ return (pointerTop >= currentPlacement.top - hoverLaneVerticalPadding &&
114
+ pointerTop <= currentPlacement.bottom + hoverLaneVerticalPadding &&
115
+ pointerLeft >= currentPlacement.left - hoverLaneLeftWidth &&
116
+ pointerLeft <= currentPlacement.left + hoverLaneRightWidth);
117
+ });
118
+ return placement?.blockId ?? null;
119
+ }
120
+ function getUniqueBlockActionToolPlacements(placements) {
121
+ const seenBlockIds = new Set();
122
+ return placements.filter((placement) => {
123
+ if (placement.dropRole === "content") {
124
+ return false;
125
+ }
126
+ if (seenBlockIds.has(placement.blockId)) {
127
+ return false;
128
+ }
129
+ seenBlockIds.add(placement.blockId);
130
+ return true;
131
+ });
132
+ }
133
+ function getVisualAncestorDropTarget(rowPlacements, targetPlacement, pointerClientX) {
134
+ const targetDepth = targetPlacement.depth ?? targetPlacement.ancestorBlockIds?.length ?? 0;
135
+ if (targetDepth <= 0 || !targetPlacement.ancestorBlockIds?.length) {
136
+ return null;
137
+ }
138
+ const desiredDepth = clamp(Math.round((pointerClientX - getMinimumPlacementLeft(rowPlacements)) /
139
+ getPlacementIndentSize(rowPlacements)), 0, targetDepth);
140
+ if (desiredDepth >= targetDepth) {
141
+ return null;
142
+ }
143
+ const targetBlockId = targetPlacement.ancestorBlockIds[desiredDepth];
144
+ return targetBlockId
145
+ ? {
146
+ placement: "after",
147
+ targetBlockId,
148
+ }
149
+ : null;
150
+ }
151
+ function getPlacementIndentSize(placements) {
152
+ const leftPositions = Array.from(new Set(placements.map((placement) => Math.round(placement.left)))).sort((a, b) => a - b);
153
+ const positiveDiffs = leftPositions
154
+ .slice(1)
155
+ .map((left, index) => left - (leftPositions[index] ?? left))
156
+ .filter((diff) => diff > 0);
157
+ return Math.min(...positiveDiffs, 42);
158
+ }
159
+ function getMinimumPlacementLeft(placements) {
160
+ return Math.min(...placements.map((placement) => placement.left), 0);
161
+ }
162
+ function clamp(value, min, max) {
163
+ return Math.min(Math.max(value, min), max);
164
+ }
165
+ function getDistanceToPlacement(placement, pointerClientY) {
166
+ if (pointerClientY >= placement.top && pointerClientY <= placement.bottom) {
167
+ return 0;
168
+ }
169
+ return Math.min(Math.abs(pointerClientY - placement.top), Math.abs(pointerClientY - placement.bottom));
170
+ }
171
+ function findContainingPlacement(placements, pointerClientY) {
172
+ return placements.find((placement) => {
173
+ return (pointerClientY >= placement.top && pointerClientY <= placement.bottom);
174
+ });
175
+ }
176
+ function getClosestPlacement(placements, pointerClientY) {
177
+ return placements.reduce((closestPlacement, placement) => {
178
+ if (!closestPlacement) {
179
+ return placement;
180
+ }
181
+ return getDistanceToPlacement(placement, pointerClientY) <
182
+ getDistanceToPlacement(closestPlacement, pointerClientY)
183
+ ? placement
184
+ : closestPlacement;
185
+ }, null);
186
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@bayonai/rich-text-editor",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "Reusable rich text editing, rendering, and unsaved-change session controls.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "main": "./dist/index.cjs",
7
+ "main": "./dist-cjs/index.js",
8
8
  "module": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
10
  "exports": {
@@ -23,6 +23,7 @@
23
23
  "dist",
24
24
  "dist-cjs",
25
25
  "CHANGELOG.md",
26
+ "BEHAVIOR.md",
26
27
  "README.md"
27
28
  ],
28
29
  "scripts": {
@@ -1,18 +0,0 @@
1
- import type { RefObject } from "react";
2
- import { type EditorKeyboardShortcutCommand } from "./editorNavigation";
3
- type RichTextBodyProps = {
4
- bodyRef: RefObject<HTMLDivElement | null>;
5
- disabled: boolean;
6
- label: string;
7
- onArrowUpFromFirstLine: () => void;
8
- onBackspace: () => boolean;
9
- onEnter: (event: {
10
- shiftKey: boolean;
11
- }) => boolean;
12
- onFocus: () => void;
13
- onInput: () => void;
14
- onSelectionChange: () => void;
15
- onShortcutCommand: (command: EditorKeyboardShortcutCommand) => void;
16
- };
17
- export declare function RichTextBody({ bodyRef, disabled, label, onArrowUpFromFirstLine, onBackspace, onEnter, onFocus, onInput, onSelectionChange, onShortcutCommand, }: RichTextBodyProps): import("react/jsx-runtime").JSX.Element;
18
- export {};
@@ -1,66 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { getCodeBlockSelectionTarget, getEditorKeyboardShortcut, isCursorOnFirstLine, isSelectAllShortcut, } from "./editorNavigation.js";
4
- // Renders the editable rich-text body and reports input and selection changes.
5
- export function RichTextBody({ bodyRef, disabled, label, onArrowUpFromFirstLine, onBackspace, onEnter, onFocus, onInput, onSelectionChange, onShortcutCommand, }) {
6
- return (_jsx("div", { "aria-label": label, className: "bayon-rte-body", contentEditable: !disabled, "data-placeholder": "Tell your story...", dir: "auto", onBlur: onSelectionChange, onClick: (event) => {
7
- if (event.target instanceof HTMLInputElement &&
8
- event.target.type === "checkbox") {
9
- onInput();
10
- }
11
- }, onFocus: onFocus, onInput: onInput, onKeyDown: (event) => {
12
- if (isSelectAllShortcut(event)) {
13
- const code = getCodeBlockSelectionTarget(event.currentTarget, window.getSelection());
14
- if (code) {
15
- event.preventDefault();
16
- selectElementContents(code);
17
- onSelectionChange();
18
- return;
19
- }
20
- }
21
- if (event.key === "Enter" && onEnter(event)) {
22
- event.preventDefault();
23
- return;
24
- }
25
- if (event.key === "Backspace" && onBackspace()) {
26
- event.preventDefault();
27
- return;
28
- }
29
- const shortcutCommand = getEditorKeyboardShortcut(event);
30
- if (shortcutCommand) {
31
- event.preventDefault();
32
- onShortcutCommand(shortcutCommand);
33
- return;
34
- }
35
- if (event.key === "ArrowUp" &&
36
- isCursorOnFirstLine(event.currentTarget.textContent ?? "", getTextOffsetWithin(event.currentTarget))) {
37
- event.preventDefault();
38
- onArrowUpFromFirstLine();
39
- }
40
- }, onKeyUp: onSelectionChange, onMouseUp: onSelectionChange, onPaste: (event) => {
41
- event.preventDefault();
42
- document.execCommand("insertText", false, event.clipboardData.getData("text/plain"));
43
- onInput();
44
- }, ref: bodyRef, role: "textbox", suppressContentEditableWarning: true, tabIndex: disabled ? -1 : 0 }));
45
- }
46
- function selectElementContents(element) {
47
- const range = document.createRange();
48
- range.selectNodeContents(element);
49
- const selection = window.getSelection();
50
- selection?.removeAllRanges();
51
- selection?.addRange(range);
52
- }
53
- function getTextOffsetWithin(root) {
54
- const selection = window.getSelection();
55
- if (!selection || selection.rangeCount === 0) {
56
- return 0;
57
- }
58
- const selectionRange = selection.getRangeAt(0);
59
- if (!root.contains(selectionRange.startContainer)) {
60
- return 0;
61
- }
62
- const textRange = document.createRange();
63
- textRange.selectNodeContents(root);
64
- textRange.setEnd(selectionRange.startContainer, selectionRange.startOffset);
65
- return textRange.toString().length;
66
- }