@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
@@ -1,5 +1,5 @@
1
1
  import type { PointerEvent } from "react";
2
- import type { BlockActionToolPlacement } from "./blockActionToolState";
2
+ import { type BlockActionToolPlacement } from "../tools/blockActionToolState";
3
3
  type BlockActionToolProps = {
4
4
  activeBlockId: string | null;
5
5
  draggedBlockId: string | null;
@@ -1,13 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef } from "react";
4
- import { CloseIcon, CopyIcon, CutIcon, DeleteIcon, DragHandleIcon, } from "./RichTextIcons.js";
4
+ import { CloseIcon, CopyIcon, CutIcon, DeleteIcon, DragHandleIcon, } from "../icons/RichTextIcons.js";
5
+ import { getBlockActionToolRenderTop, } from "../tools/blockActionToolState.js";
5
6
  export function BlockActionTool({ activeBlockId, draggedBlockId, onAction, onHoverChange, onPointerDragEnd, onPointerDragMove, onPointerDragStart, onToggleMenu, placement, }) {
6
7
  const menuOpen = activeBlockId === placement.blockId;
7
8
  const dragging = draggedBlockId === placement.blockId;
8
9
  const pointerStartRef = useRef(null);
9
10
  const suppressClickRef = useRef(false);
10
- return (_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 ? (_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: _jsx(CloseIcon, { size: 19 }) })) : (_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: () => {
11
+ return (_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: {
12
+ "--bayon-rte-block-tool-left": `${placement.left}px`,
13
+ "--bayon-rte-block-tool-top": `${getBlockActionToolRenderTop(placement)}px`,
14
+ }, children: [menuOpen ? (_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: _jsx(CloseIcon, { size: 19 }) })) : (_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: () => {
11
15
  if (suppressClickRef.current) {
12
16
  suppressClickRef.current = false;
13
17
  return;
@@ -0,0 +1,9 @@
1
+ type LinkCreationInputProps = {
2
+ left: number;
3
+ onCancel: () => void;
4
+ onSubmit: (href: string) => void;
5
+ placement: "above" | "below";
6
+ top: number;
7
+ };
8
+ export declare function LinkCreationInput({ left, onCancel, onSubmit, placement, top, }: LinkCreationInputProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { CloseIcon, LinkIcon } from "../icons/RichTextIcons.js";
5
+ // Renders the focused URL input used by selection link creation.
6
+ export function LinkCreationInput({ left, onCancel, onSubmit, placement, top, }) {
7
+ const inputRef = useRef(null);
8
+ const [href, setHref] = useState("https://");
9
+ useEffect(() => {
10
+ inputRef.current?.focus();
11
+ inputRef.current?.select();
12
+ }, []);
13
+ function handleSubmit(event) {
14
+ event.preventDefault();
15
+ submitHref();
16
+ }
17
+ function submitHref() {
18
+ const nextHref = href.trim();
19
+ if (!nextHref) {
20
+ return;
21
+ }
22
+ onSubmit(nextHref);
23
+ }
24
+ function guardButtonMouseDown(event) {
25
+ event.preventDefault();
26
+ event.stopPropagation();
27
+ }
28
+ function handleKeyDown(event) {
29
+ if (event.key === "Escape") {
30
+ event.preventDefault();
31
+ onCancel();
32
+ }
33
+ }
34
+ return (_jsxs("form", { "aria-label": "Create link", className: `bayon-rte-link-input bayon-rte-link-input--${placement}`, onSubmit: handleSubmit, style: {
35
+ "--bayon-rte-link-input-left": `${left}px`,
36
+ "--bayon-rte-link-input-top": `${top}px`,
37
+ }, children: [_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 }), _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: _jsx(LinkIcon, { size: 16 }) }), _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: _jsx(CloseIcon, { size: 16 }) })] }));
38
+ }
@@ -1,4 +1,4 @@
1
- import { BoldIcon, CodeIcon, ItalicIcon, LinkIcon, QuoteIcon, TitleIcon } from "./RichTextIcons";
1
+ import { BoldIcon, CodeIcon, ItalicIcon, LinkIcon, QuoteIcon, TitleIcon } from "../icons/RichTextIcons";
2
2
  export declare const selectionActions: readonly [{
3
3
  readonly command: "bold";
4
4
  readonly icon: typeof BoldIcon;
@@ -28,7 +28,8 @@ export type SelectionCommand = (typeof selectionActions)[number]["command"];
28
28
  type SelectionFormatToolbarProps = {
29
29
  left: number;
30
30
  onAction: (command: SelectionCommand) => void;
31
+ placement: "above" | "below";
31
32
  top: number;
32
33
  };
33
- export declare function SelectionFormatToolbar({ left, onAction, top, }: SelectionFormatToolbarProps): import("react/jsx-runtime").JSX.Element;
34
+ export declare function SelectionFormatToolbar({ left, onAction, placement, top, }: SelectionFormatToolbarProps): import("react/jsx-runtime").JSX.Element;
34
35
  export {};
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { BoldIcon, CodeIcon, ItalicIcon, LinkIcon, QuoteIcon, TitleIcon, } from "./RichTextIcons.js";
3
+ import { BoldIcon, CodeIcon, ItalicIcon, LinkIcon, QuoteIcon, TitleIcon, } from "../icons/RichTextIcons.js";
4
4
  export const selectionActions = [
5
5
  { command: "bold", icon: BoldIcon, label: "Bold" },
6
6
  { command: "italic", icon: ItalicIcon, label: "Italic" },
@@ -10,8 +10,8 @@ export const selectionActions = [
10
10
  { command: "code", icon: CodeIcon, label: "Code" },
11
11
  ];
12
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: {
13
+ export function SelectionFormatToolbar({ left, onAction, placement, top, }) {
14
+ return (_jsx("div", { className: `bayon-rte-toolbar bayon-rte-toolbar--${placement}`, style: {
15
15
  "--bayon-rte-toolbar-left": `${left}px`,
16
16
  "--bayon-rte-toolbar-top": `${top}px`,
17
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))) }));
@@ -0,0 +1,9 @@
1
+ import type { SpecialBlockAction } from "../tools/SpecialBlockTool";
2
+ type SpecialBlockOptionProps = {
3
+ action: SpecialBlockAction;
4
+ disabled?: boolean;
5
+ onInsert: (action: SpecialBlockAction) => void;
6
+ title?: string;
7
+ };
8
+ export declare function SpecialBlockOption({ action, disabled, onInsert, title, }: SpecialBlockOptionProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,8 @@
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, disabled = false, onInsert, title = action.label, }) {
5
+ const Icon = action.icon;
6
+ const label = disabled ? `${action.label} unavailable` : action.label;
7
+ return (_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: _jsx(Icon, { size: 18 }) }));
8
+ }
@@ -0,0 +1,91 @@
1
+ import { type RichTextEditorEntitlements, type RichTextEditorFeatureId, type RichTextFeatureGate, type RichTextLockedFeatureMode } from "../../core/features";
2
+ import { BulletListIcon, CodeIcon, DataObjectIcon, DividerIcon, ImageIcon, PasteIcon, QuoteIcon, TitleIcon, ToggleIcon } from "../icons/RichTextIcons";
3
+ type PasteSpecialBlockAction = {
4
+ icon: typeof PasteIcon;
5
+ kind: "paste";
6
+ label: "Paste";
7
+ selectDefault: false;
8
+ };
9
+ type ImageUploadSpecialBlockAction = {
10
+ featureId: "image-upload";
11
+ fallbackHtml: string;
12
+ icon: typeof ImageIcon;
13
+ kind: "image-upload";
14
+ label: "Image";
15
+ selectDefault: true;
16
+ };
17
+ type InsertSpecialBlockAction = {
18
+ featureId?: RichTextEditorFeatureId;
19
+ focusPosition?: "start" | "end";
20
+ focusSelector?: string;
21
+ html: string;
22
+ icon: (typeof specialBlockIcons)[number];
23
+ label: string;
24
+ selectDefault: boolean;
25
+ };
26
+ declare const specialBlockIcons: readonly [typeof ImageIcon, typeof QuoteIcon, typeof TitleIcon, typeof BulletListIcon, typeof ToggleIcon, typeof CodeIcon, typeof DataObjectIcon, typeof DividerIcon];
27
+ export type SpecialBlockAction = PasteSpecialBlockAction | ImageUploadSpecialBlockAction | InsertSpecialBlockAction;
28
+ export declare const specialBlockActions: readonly [{
29
+ readonly icon: typeof PasteIcon;
30
+ readonly kind: "paste";
31
+ readonly label: "Paste";
32
+ readonly selectDefault: false;
33
+ }, {
34
+ readonly featureId: "image-upload";
35
+ readonly fallbackHtml: "<figure data-placeholder=\"image\"><p>Image placeholder</p></figure>";
36
+ readonly icon: typeof ImageIcon;
37
+ readonly kind: "image-upload";
38
+ readonly label: "Image";
39
+ readonly selectDefault: true;
40
+ }, {
41
+ readonly html: "<blockquote data-placeholder=\"quote\"></blockquote>";
42
+ readonly icon: typeof QuoteIcon;
43
+ readonly label: "Quote";
44
+ readonly selectDefault: true;
45
+ }, {
46
+ readonly html: "<h2>Title</h2>";
47
+ readonly icon: typeof TitleIcon;
48
+ readonly label: "Title";
49
+ readonly selectDefault: true;
50
+ }, {
51
+ readonly 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>";
52
+ readonly icon: typeof BulletListIcon;
53
+ readonly label: "Bullet list";
54
+ readonly selectDefault: false;
55
+ }, {
56
+ readonly focusPosition: "start";
57
+ readonly focusSelector: "[data-toggle-label]";
58
+ readonly 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>";
59
+ readonly icon: typeof ToggleIcon;
60
+ readonly label: "Toggle";
61
+ readonly selectDefault: false;
62
+ }, {
63
+ readonly html: "<pre><code>Code block</code></pre>";
64
+ readonly icon: typeof CodeIcon;
65
+ readonly label: "Code block";
66
+ readonly selectDefault: true;
67
+ }, {
68
+ readonly html: "<pre><code>Embedded HTML</code></pre>";
69
+ readonly icon: typeof DataObjectIcon;
70
+ readonly label: "Embedded HTML";
71
+ readonly selectDefault: true;
72
+ }, {
73
+ readonly html: "<hr>";
74
+ readonly icon: typeof DividerIcon;
75
+ readonly label: "Divider";
76
+ readonly selectDefault: false;
77
+ }];
78
+ type SpecialBlockToolProps = {
79
+ features?: RichTextEditorEntitlements;
80
+ lockedFeatureMode?: RichTextLockedFeatureMode;
81
+ onFeatureGate?: (gate: RichTextFeatureGate) => void;
82
+ onHoverChange: (hovering: boolean) => void;
83
+ onInsert: (action: SpecialBlockAction) => void;
84
+ onOpenChange: (open: boolean) => void;
85
+ open: boolean;
86
+ toolHover: boolean;
87
+ top: number;
88
+ visible: boolean;
89
+ };
90
+ export declare function SpecialBlockTool({ features, lockedFeatureMode, onFeatureGate, onHoverChange, onInsert, onOpenChange, open, toolHover, top, visible, }: SpecialBlockToolProps): import("react/jsx-runtime").JSX.Element;
91
+ export {};
@@ -0,0 +1,125 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { getRichTextFeatureAccess, } from "../../core/features.js";
4
+ import { AddIcon, BulletListIcon, CloseIcon, CodeIcon, DataObjectIcon, DividerIcon, ImageIcon, PasteIcon, QuoteIcon, TitleIcon, ToggleIcon, } from "../icons/RichTextIcons.js";
5
+ import { SpecialBlockOption } from "./SpecialBlockOption.js";
6
+ const specialBlockIcons = [
7
+ ImageIcon,
8
+ QuoteIcon,
9
+ TitleIcon,
10
+ BulletListIcon,
11
+ ToggleIcon,
12
+ CodeIcon,
13
+ DataObjectIcon,
14
+ DividerIcon,
15
+ ];
16
+ export const specialBlockActions = [
17
+ {
18
+ icon: PasteIcon,
19
+ kind: "paste",
20
+ label: "Paste",
21
+ selectDefault: false,
22
+ },
23
+ {
24
+ featureId: "image-upload",
25
+ fallbackHtml: '<figure data-placeholder="image"><p>Image placeholder</p></figure>',
26
+ icon: ImageIcon,
27
+ kind: "image-upload",
28
+ label: "Image",
29
+ selectDefault: true,
30
+ },
31
+ {
32
+ html: '<blockquote data-placeholder="quote"></blockquote>',
33
+ icon: QuoteIcon,
34
+ label: "Quote",
35
+ selectDefault: true,
36
+ },
37
+ {
38
+ html: "<h2>Title</h2>",
39
+ icon: TitleIcon,
40
+ label: "Title",
41
+ selectDefault: true,
42
+ },
43
+ {
44
+ 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>',
45
+ icon: BulletListIcon,
46
+ label: "Bullet list",
47
+ selectDefault: false,
48
+ },
49
+ {
50
+ focusPosition: "start",
51
+ focusSelector: "[data-toggle-label]",
52
+ 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>',
53
+ icon: ToggleIcon,
54
+ label: "Toggle",
55
+ selectDefault: false,
56
+ },
57
+ {
58
+ html: "<pre><code>Code block</code></pre>",
59
+ icon: CodeIcon,
60
+ label: "Code block",
61
+ selectDefault: true,
62
+ },
63
+ {
64
+ html: "<pre><code>Embedded HTML</code></pre>",
65
+ icon: DataObjectIcon,
66
+ label: "Embedded HTML",
67
+ selectDefault: true,
68
+ },
69
+ {
70
+ html: "<hr>",
71
+ icon: DividerIcon,
72
+ label: "Divider",
73
+ selectDefault: false,
74
+ },
75
+ ];
76
+ // Renders the floating insertion control for image, quote, title, code, and divider blocks.
77
+ export function SpecialBlockTool({ features, lockedFeatureMode = "hide", onFeatureGate, onHoverChange, onInsert, onOpenChange, open, toolHover, top, visible, }) {
78
+ const actionItems = specialBlockActions
79
+ .map((action) => ({
80
+ access: getSpecialBlockActionAccess(features, action),
81
+ action,
82
+ }))
83
+ .filter((item) => item.access.enabled ||
84
+ !hasSpecialBlockActionFeature(item.action) ||
85
+ lockedFeatureMode === "disabled");
86
+ return (_jsxs("div", { "aria-hidden": !visible, className: `bayon-rte-special-tool${visible ? " bayon-rte-special-tool--visible" : ""}`, onMouseEnter: () => onHoverChange(true), onMouseLeave: () => {
87
+ onHoverChange(false);
88
+ onOpenChange(false);
89
+ }, style: { "--bayon-rte-special-tool-top": `${top}px` }, children: [_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: {
90
+ transform: visible
91
+ ? "scale(1) rotate(0deg)"
92
+ : "scale(0.84) rotate(-12deg)",
93
+ }, title: open ? "Close special blocks" : "Add special block", type: "button", children: open ? _jsx(CloseIcon, { size: 20 }) : _jsx(AddIcon, { size: 20 }) }), open ? (_jsx("div", { className: "bayon-rte-special-tool__actions", children: actionItems.map(({ access, action }) => {
94
+ const disabled = !access.enabled;
95
+ return (_jsx(SpecialBlockOption, { action: action, disabled: disabled, onInsert: disabled
96
+ ? () => onFeatureGate?.({
97
+ featureId: access.featureId,
98
+ label: access.label,
99
+ reason: access.reason,
100
+ })
101
+ : onInsert, title: disabled
102
+ ? `${access.label}: ${getLockedFeatureTitle(access)}`
103
+ : action.label }, action.label));
104
+ }) })) : null] }));
105
+ }
106
+ function getSpecialBlockActionAccess(features, action) {
107
+ return hasSpecialBlockActionFeature(action)
108
+ ? getRichTextFeatureAccess(features, action.featureId)
109
+ : { enabled: true };
110
+ }
111
+ function hasSpecialBlockActionFeature(action) {
112
+ return "featureId" in action;
113
+ }
114
+ function getLockedFeatureTitle(access) {
115
+ if (access.enabled) {
116
+ return access.label;
117
+ }
118
+ if (access.reason === "premium") {
119
+ return "Premium feature";
120
+ }
121
+ if (access.reason === "unavailable") {
122
+ return "Unavailable";
123
+ }
124
+ return "Disabled";
125
+ }
@@ -3,6 +3,10 @@ type TranscriptionControlProps = {
3
3
  language?: string;
4
4
  onTranscript: (text: string) => void;
5
5
  };
6
+ type SpeechRecognitionSupport = {
7
+ reason: "available" | "missing-api" | "mobile-safari";
8
+ supported: boolean;
9
+ };
6
10
  export declare function TranscriptionControl({ disabled, language, onTranscript, }: TranscriptionControlProps): import("react/jsx-runtime").JSX.Element;
7
11
  export declare function getTranscriptionControlState({ disabled, errorMessage, lastInserted, recording, supported, }: {
8
12
  disabled: boolean;
@@ -16,4 +20,9 @@ export declare function getTranscriptionControlState({ disabled, errorMessage, l
16
20
  statusLabel: string | null;
17
21
  tone: "error" | "ready" | "recording" | "success" | "unavailable";
18
22
  };
23
+ export declare function resolveSpeechRecognitionSupport({ hasSpeechRecognition, hasWebkitSpeechRecognition, userAgent, }: {
24
+ hasSpeechRecognition: boolean;
25
+ hasWebkitSpeechRecognition: boolean;
26
+ userAgent: string;
27
+ }): SpeechRecognitionSupport;
19
28
  export {};
@@ -1,35 +1,36 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
- import { MicIcon, StopIcon } from "./RichTextIcons.js";
4
+ import { MicIcon, StopIcon } from "../icons/RichTextIcons.js";
5
5
  export function TranscriptionControl({ disabled = false, language, onTranscript, }) {
6
6
  const recognitionRef = useRef(null);
7
7
  const [errorMessage, setErrorMessage] = useState(null);
8
8
  const [lastInserted, setLastInserted] = useState(false);
9
9
  const [recording, setRecording] = useState(false);
10
- const recognitionSupported = typeof window === "undefined" || Boolean(getSpeechRecognitionConstructor());
10
+ const recognitionSupport = getSpeechRecognitionSupport();
11
11
  const controlState = getTranscriptionControlState({
12
12
  disabled,
13
13
  errorMessage,
14
14
  lastInserted,
15
15
  recording,
16
- supported: recognitionSupported,
16
+ supported: recognitionSupport.supported,
17
17
  });
18
18
  useEffect(() => {
19
19
  return () => {
20
- recognitionRef.current?.stop();
20
+ stopRecognitionInstance(recognitionRef.current);
21
21
  recognitionRef.current = null;
22
22
  };
23
23
  }, []);
24
24
  function stopRecording() {
25
- recognitionRef.current?.stop();
25
+ stopRecognitionInstance(recognitionRef.current);
26
26
  recognitionRef.current = null;
27
27
  setRecording(false);
28
28
  setLastInserted(false);
29
29
  }
30
30
  function startRecording() {
31
+ const support = getSpeechRecognitionSupport();
31
32
  const Recognition = getSpeechRecognitionConstructor();
32
- if (!Recognition || disabled) {
33
+ if (!support.supported || !Recognition || disabled) {
33
34
  setErrorMessage("Browser transcription is unavailable.");
34
35
  setLastInserted(false);
35
36
  return;
@@ -59,8 +60,17 @@ export function TranscriptionControl({ disabled = false, language, onTranscript,
59
60
  recognitionRef.current = recognition;
60
61
  setErrorMessage(null);
61
62
  setLastInserted(false);
62
- setRecording(true);
63
- recognition.start();
63
+ try {
64
+ recognition.start();
65
+ setRecording(true);
66
+ }
67
+ catch (error) {
68
+ console.warn("[rich-text-editor] Failed to start browser transcription.", error);
69
+ recognitionRef.current = null;
70
+ setErrorMessage("Transcription stopped.");
71
+ setLastInserted(false);
72
+ setRecording(false);
73
+ }
64
74
  }
65
75
  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
76
  }
@@ -109,7 +119,58 @@ function getSpeechRecognitionConstructor() {
109
119
  return null;
110
120
  }
111
121
  const speechWindow = window;
112
- return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition ?? null;
122
+ return (speechWindow.SpeechRecognition ??
123
+ speechWindow.webkitSpeechRecognition ??
124
+ null);
125
+ }
126
+ function getSpeechRecognitionSupport() {
127
+ if (typeof window === "undefined") {
128
+ return {
129
+ reason: "available",
130
+ supported: true,
131
+ };
132
+ }
133
+ const speechWindow = window;
134
+ return resolveSpeechRecognitionSupport({
135
+ hasSpeechRecognition: Boolean(speechWindow.SpeechRecognition),
136
+ hasWebkitSpeechRecognition: Boolean(speechWindow.webkitSpeechRecognition),
137
+ userAgent: window.navigator.userAgent,
138
+ });
139
+ }
140
+ export function resolveSpeechRecognitionSupport({ hasSpeechRecognition, hasWebkitSpeechRecognition, userAgent, }) {
141
+ if (!hasSpeechRecognition && !hasWebkitSpeechRecognition) {
142
+ return {
143
+ reason: "missing-api",
144
+ supported: false,
145
+ };
146
+ }
147
+ if (hasWebkitSpeechRecognition && isMobileSafariUserAgent(userAgent)) {
148
+ return {
149
+ reason: "mobile-safari",
150
+ supported: false,
151
+ };
152
+ }
153
+ return {
154
+ reason: "available",
155
+ supported: true,
156
+ };
157
+ }
158
+ function isMobileSafariUserAgent(userAgent) {
159
+ const normalizedUserAgent = userAgent.toLowerCase();
160
+ const isIosMobile = /\b(iphone|ipod|ipad)\b/.test(normalizedUserAgent) ||
161
+ (normalizedUserAgent.includes("macintosh") &&
162
+ normalizedUserAgent.includes("mobile"));
163
+ const isSafari = normalizedUserAgent.includes("safari");
164
+ const isOtherIosBrowser = /\b(crios|fxios|edgios|opios)\b/.test(normalizedUserAgent);
165
+ return isIosMobile && isSafari && !isOtherIosBrowser;
166
+ }
167
+ function stopRecognitionInstance(recognition) {
168
+ try {
169
+ recognition?.stop();
170
+ }
171
+ catch (error) {
172
+ console.warn("[rich-text-editor] Failed to stop browser transcription.", error);
173
+ }
113
174
  }
114
175
  function readFinalTranscript(event) {
115
176
  let text = "";
@@ -0,0 +1,41 @@
1
+ import type { BlockDropPlacement } from "../editor/blockActions";
2
+ export type BlockActionToolPlacement = {
3
+ ancestorBlockIds?: string[];
4
+ blockId: string;
5
+ blockType?: string;
6
+ bottom: number;
7
+ depth?: number;
8
+ dropRole?: "content" | "row";
9
+ left: number;
10
+ top: number;
11
+ toolTop?: number;
12
+ };
13
+ export type BlockActionToolVisibilityState = {
14
+ activeBlockId: string | null;
15
+ draggedBlockId: string | null;
16
+ focusedBlockId: string | null;
17
+ hoveredBlockId: string | null;
18
+ };
19
+ export type PointerDragDropResult = {
20
+ dropTarget: {
21
+ placement: BlockDropPlacement;
22
+ targetBlockId: string;
23
+ };
24
+ status: "valid";
25
+ } | {
26
+ dropTarget?: {
27
+ placement: BlockDropPlacement;
28
+ targetBlockId: string;
29
+ };
30
+ status: "invalid";
31
+ };
32
+ export declare function getBlockActionToolAnchorTop(blockType: string, top: number): number;
33
+ export declare function getBlockActionToolRenderTop(placement: Pick<BlockActionToolPlacement, "toolTop" | "top">): number;
34
+ export declare function getVisibleBlockActionToolPlacements(placements: BlockActionToolPlacement[], state: BlockActionToolVisibilityState): BlockActionToolPlacement[];
35
+ export declare function getPointerDropTarget(placements: BlockActionToolPlacement[], draggedBlockId: string, pointerClientY: number, pointerClientX?: number): {
36
+ placement: BlockDropPlacement;
37
+ targetBlockId: string;
38
+ } | null;
39
+ export declare function getPointerDragDropResult(placements: BlockActionToolPlacement[], draggedBlockId: string, pointerClientY: number, pointerClientX?: number): PointerDragDropResult;
40
+ export declare function getPointerHoverLaneBlockId(placements: BlockActionToolPlacement[], pointerTop: number, pointerLeft: number): string | null;
41
+ export declare function getUniqueBlockActionToolPlacements(placements: BlockActionToolPlacement[]): BlockActionToolPlacement[];