@ai-matrx/rich-editor 0.2.1 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/editor/RichEditorImpl.js +6 -0
- package/dist/format/FormatButtons.d.ts +22 -0
- package/dist/format/FormatButtons.js +41 -0
- package/dist/format/format-actions.js +7 -6
- package/dist/format/format-target.d.ts +12 -0
- package/dist/format/format-target.js +35 -0
- package/dist/format/useTextareaFormatting.js +6 -2
- package/dist/in-place/InPlaceEditor.d.ts +9 -0
- package/dist/in-place/InPlaceEditor.js +45 -3
- package/dist/panels/OutlinePanel.js +2 -6
- package/dist/source/SourceEditor.js +5 -2
- package/dist/visual/VisualEditor.d.ts +3 -0
- package/dist/visual/VisualEditor.js +1 -1
- package/dist/visual/nodes/IslandBlockView.js +3 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
- The selection toolbar's formatting reaches a focused plain-text editor even when its zone's host half never reached the toolbar's target (2026-10-07 live: notes Split's textarea answered ⌘B and the header buttons, yet its selection toolbar showed no Bold). `focusedFormatTarget()` (format-target) is the fallback for an editable selection.
|
|
6
|
+
|
|
7
|
+
### Consumer action
|
|
8
|
+
|
|
9
|
+
- None required.
|
|
10
|
+
|
|
11
|
+
## 0.2.4
|
|
12
|
+
|
|
13
|
+
- Visible format buttons in plain text (Arman, 2026-10-04: "The formatting buttons need to work even when we are just showing plain text"). New `format/FormatButtons` (Bold, Italic, Code, Link, Heading, List — compact icons with tooltips) run THE command layer on a FormatTarget. The editor's Source view shows them inside its existing toolbar row.
|
|
14
|
+
- `registerFormatElement` / `formatTargetWithin(root)`: every wired textarea (`useTextareaFormatting`) and the Source view register themselves, so a host's buttons act on the editor inside their container (focused › last focused › first). `EditorViewHandle.formatTarget()` returns the Source view's target.
|
|
15
|
+
- The selection toolbar's formatting strip is the same compact six; Strikethrough, Heading 1, Quote and the rest live under More.
|
|
16
|
+
- Edit in place: the caret placed at the double-click point now stays in view while the editor finishes drawing (tables, diagrams and code blocks grew above it and pushed it off screen — why 0.2.1's placement looked like "opened at the top"); a click at a line's end lands at that end, not the next block's start.
|
|
17
|
+
|
|
18
|
+
### Consumer action
|
|
19
|
+
|
|
20
|
+
- matrx-frontend: render `<FormatButtons resolve={() => formatTargetWithin(container)} />` inside a plain-text editor's existing toolbar row (notes Plain and Split).
|
|
21
|
+
|
|
22
|
+
## 0.2.3
|
|
23
|
+
|
|
24
|
+
- The outline panel and the island block pass their failure words to the kit copy (kit 0.30 `failureMessage`): a refused copy shows ONE toast, not the generic one plus ours.
|
|
25
|
+
|
|
26
|
+
### Consumer action
|
|
27
|
+
|
|
28
|
+
none.
|
|
29
|
+
|
|
30
|
+
## 0.2.2
|
|
31
|
+
|
|
32
|
+
- The outline panel's "copy link" and the island block's copy show their failure toast from the kit copy's `false` branch; the `catch` that held it could never run.
|
|
33
|
+
|
|
34
|
+
### Consumer action
|
|
35
|
+
|
|
36
|
+
- None required.
|
|
37
|
+
|
|
3
38
|
## 0.1.3
|
|
4
39
|
### Unreleased
|
|
5
40
|
|
|
@@ -49,6 +49,7 @@ import { outlineOf } from "../core/outline.js";
|
|
|
49
49
|
import { RichEditorContext } from "./RichEditorContext.js";
|
|
50
50
|
import { VisualEditor } from "../visual/VisualEditor.js";
|
|
51
51
|
import { SourceEditor } from "../source/SourceEditor.js";
|
|
52
|
+
import { FormatButtons } from "../format/FormatButtons.js";
|
|
52
53
|
import { FindReplacePanel } from "../panels/FindReplacePanel.js";
|
|
53
54
|
import { OutlinePanel } from "../panels/OutlinePanel.js";
|
|
54
55
|
import { ShortcutsDialog } from "../panels/ShortcutsDialog.js";
|
|
@@ -425,6 +426,10 @@ function RichEditorImpl({
|
|
|
425
426
|
// Desktop shows the count in the toolbar; a phone reads it from the tools menu.
|
|
426
427
|
...isMobile ? [{ id: "count", label: `${formatCount(metrics.words)} words \xB7 ${saveStatus}`, icon: Hash, run: () => toast.info(documentFacts) }] : []
|
|
427
428
|
];
|
|
429
|
+
const formatButtons = view === "source" && !readOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
430
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px shrink-0 bg-border" }),
|
|
431
|
+
/* @__PURE__ */ jsx(FormatButtons, { resolve: () => handle.current?.formatTarget?.() ?? null })
|
|
432
|
+
] }) : null;
|
|
428
433
|
const micButton = mic.available && !readOnly && view !== "preview" ? /* @__PURE__ */ jsx(
|
|
429
434
|
ToolbarButton,
|
|
430
435
|
{
|
|
@@ -542,6 +547,7 @@ function RichEditorImpl({
|
|
|
542
547
|
option
|
|
543
548
|
);
|
|
544
549
|
}) }),
|
|
550
|
+
formatButtons,
|
|
545
551
|
!isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
546
552
|
/* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px shrink-0 bg-border" }),
|
|
547
553
|
tools.map((tool) => /* @__PURE__ */ jsx(ToolbarButton, { label: tool.label, active: tool.active, onClick: tool.run, disabled: tool.disabled, children: /* @__PURE__ */ jsx(tool.icon, { className: "h-4 w-4" }) }, tool.id)),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type LucideIcon } from "lucide-react";
|
|
3
|
+
import type { FormatCommandId } from "../core/markdown-format.js";
|
|
4
|
+
import type { FormatTarget } from "./format-target.js";
|
|
5
|
+
export interface FormatButtonSpec {
|
|
6
|
+
command: FormatCommandId;
|
|
7
|
+
label: string;
|
|
8
|
+
icon: LucideIcon;
|
|
9
|
+
}
|
|
10
|
+
/** The compact set every plain-text toolbar shows (the rest stay on the selection toolbar and the keys). */
|
|
11
|
+
export declare const FORMAT_BUTTONS: readonly FormatButtonSpec[];
|
|
12
|
+
export interface FormatButtonsProps {
|
|
13
|
+
/** The editor the buttons act on, read at press time. Null = nothing to format (buttons stay, inert). */
|
|
14
|
+
resolve: () => FormatTarget | null;
|
|
15
|
+
/** Hide (render nothing) — a read-only host. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
/** "sm" (28px, the editor toolbar) or "xs" (24px, a dense strip). */
|
|
19
|
+
size?: "sm" | "xs";
|
|
20
|
+
}
|
|
21
|
+
export declare function FormatButtons({ resolve, disabled, className, size }: FormatButtonsProps): React.ReactElement | null;
|
|
22
|
+
export default FormatButtons;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Bold, Code, Heading2, Italic, Link2, List } from "lucide-react";
|
|
4
|
+
import { cn } from "@ai-matrx/design-system";
|
|
5
|
+
const FORMAT_BUTTONS = [
|
|
6
|
+
{ command: "bold", label: "Bold (\u2318B)", icon: Bold },
|
|
7
|
+
{ command: "italic", label: "Italic (\u2318I)", icon: Italic },
|
|
8
|
+
{ command: "code", label: "Inline code (\u2318E)", icon: Code },
|
|
9
|
+
{ command: "link", label: "Link (\u2318K)", icon: Link2 },
|
|
10
|
+
{ command: "heading2", label: "Heading", icon: Heading2 },
|
|
11
|
+
{ command: "bulletList", label: "Bulleted list (\u2318\u21E78)", icon: List }
|
|
12
|
+
];
|
|
13
|
+
function FormatButtons({ resolve, disabled = false, className, size = "sm" }) {
|
|
14
|
+
if (disabled) return null;
|
|
15
|
+
return /* @__PURE__ */ jsx("div", { role: "group", "aria-label": "Formatting", "data-format-buttons": "", className: cn("flex shrink-0 items-center gap-0.5", className), children: FORMAT_BUTTONS.map(({ command, label, icon: Icon }) => /* @__PURE__ */ jsx(
|
|
16
|
+
"button",
|
|
17
|
+
{
|
|
18
|
+
type: "button",
|
|
19
|
+
"aria-label": label,
|
|
20
|
+
title: label,
|
|
21
|
+
"data-format-command": command,
|
|
22
|
+
"data-matrx-button": "",
|
|
23
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
24
|
+
onClick: () => {
|
|
25
|
+
resolve()?.run(command);
|
|
26
|
+
},
|
|
27
|
+
className: cn(
|
|
28
|
+
"inline-flex shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
29
|
+
size === "xs" ? "h-6 w-6" : "h-7 w-7"
|
|
30
|
+
),
|
|
31
|
+
children: /* @__PURE__ */ jsx(Icon, { className: size === "xs" ? "h-3.5 w-3.5" : "h-4 w-4" })
|
|
32
|
+
},
|
|
33
|
+
command
|
|
34
|
+
)) });
|
|
35
|
+
}
|
|
36
|
+
var FormatButtons_default = FormatButtons;
|
|
37
|
+
export {
|
|
38
|
+
FORMAT_BUTTONS,
|
|
39
|
+
FormatButtons,
|
|
40
|
+
FormatButtons_default as default
|
|
41
|
+
};
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "lucide-react";
|
|
16
16
|
import { registerAlchemyIcon } from "@ai-matrx/rich-content/utils/alchemy-icon-keys";
|
|
17
17
|
import { declareSelectionProvider, hostHalf, placeSelectionActions, shownInSelectionMode } from "@ai-matrx/rich-content/selection-toolbar/selection-actions";
|
|
18
|
-
import { MARKDOWN_FORMAT_HOST_KEY } from "./format-target.js";
|
|
18
|
+
import { MARKDOWN_FORMAT_HOST_KEY, focusedFormatTarget } from "./format-target.js";
|
|
19
19
|
let visualResolver = null;
|
|
20
20
|
function registerVisualFormatResolver(resolver) {
|
|
21
21
|
visualResolver = resolver;
|
|
@@ -24,18 +24,19 @@ function formatTargetOf(target) {
|
|
|
24
24
|
const visual = visualResolver?.(target) ?? null;
|
|
25
25
|
if (visual) return visual;
|
|
26
26
|
const text = hostHalf(target, MARKDOWN_FORMAT_HOST_KEY);
|
|
27
|
-
|
|
27
|
+
if (text?.kind === "markdown-format") return text.target;
|
|
28
|
+
return target.selection?.type === "editable" ? focusedFormatTarget() : null;
|
|
28
29
|
}
|
|
29
30
|
const FORMATS = [
|
|
30
31
|
{ id: "bold", command: "bold", label: "Bold (\u2318B)", icon: Bold },
|
|
31
32
|
{ id: "italic", command: "italic", label: "Italic (\u2318I)", icon: Italic },
|
|
32
|
-
{ id: "strike", command: "strike", label: "Strikethrough (\u2318\u21E7X)", icon: Strikethrough },
|
|
33
|
+
{ id: "strike", command: "strike", label: "Strikethrough (\u2318\u21E7X)", icon: Strikethrough, more: true },
|
|
33
34
|
{ id: "code", command: "code", label: "Inline code (\u2318E)", icon: Code },
|
|
34
35
|
{ id: "link", command: "link", label: "Link (\u2318K)", icon: Link2 },
|
|
35
|
-
{ id: "h1", command: "heading1", label: "Heading 1", icon: Heading1 },
|
|
36
|
-
{ id: "h2", command: "heading2", label: "Heading
|
|
36
|
+
{ id: "h1", command: "heading1", label: "Heading 1", icon: Heading1, more: true },
|
|
37
|
+
{ id: "h2", command: "heading2", label: "Heading", icon: Heading2 },
|
|
37
38
|
{ id: "h3", command: "heading3", label: "Heading 3", icon: Heading3, more: true },
|
|
38
|
-
{ id: "quote", command: "quote", label: "Quote", icon: Quote },
|
|
39
|
+
{ id: "quote", command: "quote", label: "Quote", icon: Quote, more: true },
|
|
39
40
|
{ id: "list", command: "bulletList", label: "Bulleted list (\u2318\u21E78)", icon: List },
|
|
40
41
|
{ id: "numbered", command: "orderedList", label: "Numbered list (\u2318\u21E77)", icon: ListOrdered, more: true },
|
|
41
42
|
{ id: "tasks", command: "taskList", label: "Checklist (\u2318\u21E79)", icon: ListChecks, more: true },
|
|
@@ -37,3 +37,15 @@ export declare function textFormatTarget(engine: "source" | "textarea", read: ()
|
|
|
37
37
|
export declare function applyEditToTextarea(el: HTMLTextAreaElement, result: SourceEditResult): void;
|
|
38
38
|
/** The textarea engine. */
|
|
39
39
|
export declare function textareaFormatTarget(el: HTMLTextAreaElement): FormatTarget;
|
|
40
|
+
/** Register `el` as a formatting host; returns the unregister. */
|
|
41
|
+
export declare function registerFormatElement(el: Element, target: FormatTarget): () => void;
|
|
42
|
+
/** The registered editor inside `root` the person is in (focused › last focused › first). */
|
|
43
|
+
export declare function formatTargetWithin(root: ParentNode | null | undefined): FormatTarget | null;
|
|
44
|
+
/**
|
|
45
|
+
* The focused plain-text editor (a registered textarea or Source view), whatever
|
|
46
|
+
* selection zones are mounted around it. The selection toolbar's format buttons
|
|
47
|
+
* fall back to it: 2026-10-07 live, notes Split's textarea answered ⌘B and the
|
|
48
|
+
* header's buttons, yet its selection toolbar showed no formatting — its zone's
|
|
49
|
+
* host half never reached the toolbar's target.
|
|
50
|
+
*/
|
|
51
|
+
export declare function focusedFormatTarget(): FormatTarget | null;
|
|
@@ -70,10 +70,45 @@ function textareaFormatTarget(el) {
|
|
|
70
70
|
}
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
|
+
const FORMAT_HOST_ATTR = "data-format-host";
|
|
74
|
+
const registeredTargets = /* @__PURE__ */ new WeakMap();
|
|
75
|
+
let lastFocused = null;
|
|
76
|
+
function registerFormatElement(el, target) {
|
|
77
|
+
registeredTargets.set(el, target);
|
|
78
|
+
el.setAttribute(FORMAT_HOST_ATTR, "");
|
|
79
|
+
const onFocus = () => {
|
|
80
|
+
lastFocused = el;
|
|
81
|
+
};
|
|
82
|
+
el.addEventListener("focusin", onFocus);
|
|
83
|
+
return () => {
|
|
84
|
+
el.removeEventListener("focusin", onFocus);
|
|
85
|
+
if (registeredTargets.get(el) === target) {
|
|
86
|
+
registeredTargets.delete(el);
|
|
87
|
+
el.removeAttribute(FORMAT_HOST_ATTR);
|
|
88
|
+
}
|
|
89
|
+
if (lastFocused === el) lastFocused = null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function formatTargetWithin(root) {
|
|
93
|
+
if (!root) return null;
|
|
94
|
+
const hosts = Array.from(root.querySelectorAll(`[${FORMAT_HOST_ATTR}]`));
|
|
95
|
+
if (hosts.length === 0) return null;
|
|
96
|
+
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
97
|
+
const pick = hosts.find((el) => active && (el === active || el.contains(active))) ?? (lastFocused && hosts.includes(lastFocused) ? lastFocused : null) ?? hosts[0];
|
|
98
|
+
return registeredTargets.get(pick) ?? null;
|
|
99
|
+
}
|
|
100
|
+
function focusedFormatTarget() {
|
|
101
|
+
if (typeof document === "undefined") return null;
|
|
102
|
+
const host = document.activeElement?.closest(`[${FORMAT_HOST_ATTR}]`) ?? null;
|
|
103
|
+
return host ? registeredTargets.get(host) ?? null : null;
|
|
104
|
+
}
|
|
73
105
|
export {
|
|
74
106
|
MARKDOWN_FORMAT_HOST_KEY,
|
|
75
107
|
applyEditToTextarea,
|
|
108
|
+
focusedFormatTarget,
|
|
109
|
+
formatTargetWithin,
|
|
76
110
|
markdownFormatHost,
|
|
111
|
+
registerFormatElement,
|
|
77
112
|
textFormatTarget,
|
|
78
113
|
textareaFormatTarget
|
|
79
114
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { useSelectionZone } from "@ai-matrx/rich-content/selection-toolbar/selection-zones";
|
|
4
4
|
import { formatCommandForKey, keyNameOf } from "../core/markdown-format.js";
|
|
5
|
-
import { markdownFormatHost, textareaFormatTarget } from "./format-target.js";
|
|
5
|
+
import { markdownFormatHost, registerFormatElement, textareaFormatTarget } from "./format-target.js";
|
|
6
6
|
function isApplePlatform() {
|
|
7
7
|
if (typeof navigator === "undefined") return false;
|
|
8
8
|
const platform = navigator.userAgentData?.platform ?? navigator.platform ?? "";
|
|
@@ -23,7 +23,11 @@ function useTextareaFormatting(element, enabled = true) {
|
|
|
23
23
|
handleFormatChord(element, event);
|
|
24
24
|
};
|
|
25
25
|
element.addEventListener("keydown", onKeyDown);
|
|
26
|
-
|
|
26
|
+
const unregister = registerFormatElement(element, textareaFormatTarget(element));
|
|
27
|
+
return () => {
|
|
28
|
+
element.removeEventListener("keydown", onKeyDown);
|
|
29
|
+
unregister();
|
|
30
|
+
};
|
|
27
31
|
}, [element, enabled]);
|
|
28
32
|
useSelectionZone(
|
|
29
33
|
element,
|
|
@@ -39,3 +39,12 @@ export interface InPlaceAction {
|
|
|
39
39
|
run: (text: string) => Promise<void>;
|
|
40
40
|
}
|
|
41
41
|
export declare function InPlaceEditor({ value, mode, write, close, caret, minHeight, expandable, startExpanded, discardDescription, editor, className, id, toolbarExtras, actions, }: InPlaceEditorProps): import("react").JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* The caret was placed where the person double-clicked, but the editor keeps
|
|
44
|
+
* growing for a moment after (tables, diagrams and code blocks finish drawing
|
|
45
|
+
* above it), which pushed it out of view: 0.2.1 placed the caret correctly and
|
|
46
|
+
* the person still saw the top of the answer. For a short settle window, every
|
|
47
|
+
* size change brings the caret back into view — until the person scrolls,
|
|
48
|
+
* types or clicks, which ends it at once.
|
|
49
|
+
*/
|
|
50
|
+
export declare function keepCaretInView(root: HTMLElement, settleMs?: number): () => void;
|
|
@@ -41,9 +41,13 @@ function InPlaceEditor({
|
|
|
41
41
|
useEffect(() => {
|
|
42
42
|
let tries = 0;
|
|
43
43
|
let frame = 0;
|
|
44
|
+
let release = null;
|
|
44
45
|
const place = () => {
|
|
45
46
|
const c = controller.current;
|
|
46
|
-
if (c && (caret ? c.placeCaret(caret) : (c.focus(), true)))
|
|
47
|
+
if (c && (caret ? c.placeCaret(caret) : (c.focus(), true))) {
|
|
48
|
+
if (caret && shellRef.current) release = keepCaretInView(shellRef.current);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
47
51
|
if (++tries > 90) {
|
|
48
52
|
controller.current?.focus();
|
|
49
53
|
return;
|
|
@@ -51,7 +55,10 @@ function InPlaceEditor({
|
|
|
51
55
|
frame = requestAnimationFrame(place);
|
|
52
56
|
};
|
|
53
57
|
frame = requestAnimationFrame(place);
|
|
54
|
-
return () =>
|
|
58
|
+
return () => {
|
|
59
|
+
cancelAnimationFrame(frame);
|
|
60
|
+
release?.();
|
|
61
|
+
};
|
|
55
62
|
}, [caret]);
|
|
56
63
|
const inputSinceReport = useRef(false);
|
|
57
64
|
const onDraft = (text) => {
|
|
@@ -164,6 +171,41 @@ function InPlaceEditor({
|
|
|
164
171
|
}
|
|
165
172
|
);
|
|
166
173
|
}
|
|
174
|
+
function keepCaretInView(root, settleMs = 2500) {
|
|
175
|
+
let done = false;
|
|
176
|
+
const caretElement = () => {
|
|
177
|
+
const sel = typeof window === "undefined" ? null : window.getSelection();
|
|
178
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
179
|
+
const range = sel.getRangeAt(0);
|
|
180
|
+
const node = range.startContainer;
|
|
181
|
+
const el = node.nodeType === 1 ? node : node.parentElement;
|
|
182
|
+
if (!el || !root.contains(el)) return null;
|
|
183
|
+
const r = range.getBoundingClientRect();
|
|
184
|
+
return { el, rect: r.height || r.width ? r : el.getBoundingClientRect() };
|
|
185
|
+
};
|
|
186
|
+
const show = () => {
|
|
187
|
+
if (done) return;
|
|
188
|
+
const at = caretElement();
|
|
189
|
+
if (!at) return;
|
|
190
|
+
if (at.rect.top < 0 || at.rect.bottom > window.innerHeight) at.el.scrollIntoView({ block: "center" });
|
|
191
|
+
};
|
|
192
|
+
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(show);
|
|
193
|
+
ro?.observe(root);
|
|
194
|
+
const tick = window.setInterval(show, 200);
|
|
195
|
+
const stop = () => {
|
|
196
|
+
if (done) return;
|
|
197
|
+
done = true;
|
|
198
|
+
ro?.disconnect();
|
|
199
|
+
window.clearInterval(tick);
|
|
200
|
+
window.clearTimeout(timer);
|
|
201
|
+
for (const type of ["wheel", "touchmove", "keydown", "pointerdown"]) window.removeEventListener(type, stop, true);
|
|
202
|
+
};
|
|
203
|
+
const timer = window.setTimeout(stop, settleMs);
|
|
204
|
+
for (const type of ["wheel", "touchmove", "keydown", "pointerdown"]) window.addEventListener(type, stop, { capture: true, passive: true });
|
|
205
|
+
show();
|
|
206
|
+
return stop;
|
|
207
|
+
}
|
|
167
208
|
export {
|
|
168
|
-
InPlaceEditor
|
|
209
|
+
InPlaceEditor,
|
|
210
|
+
keepCaretInView
|
|
169
211
|
};
|
|
@@ -15,12 +15,8 @@ function OutlinePanel({
|
|
|
15
15
|
const minLevel = Math.min(...entries.map((entry) => entry.level), 6);
|
|
16
16
|
const copyLink = async (entry) => {
|
|
17
17
|
const url = `${window.location.origin}${window.location.pathname}${window.location.search}#${entry.slug}`;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
toast.success(`Link to \u201C${entry.text}\u201D copied.`);
|
|
21
|
-
} catch {
|
|
22
|
-
toast.error("Copy failed \u2014 your browser blocked clipboard access.");
|
|
23
|
-
}
|
|
18
|
+
if (!await copyText(url, void 0, "Copy failed \u2014 your browser blocked clipboard access.")) return;
|
|
19
|
+
toast.success(`Link to \u201C${entry.text}\u201D copied.`);
|
|
24
20
|
};
|
|
25
21
|
return /* @__PURE__ */ jsxs("nav", { "aria-label": "Outline", className: cn("flex h-full flex-col overflow-hidden", className), children: [
|
|
26
22
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-3 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
@@ -28,7 +28,7 @@ import { findMatches, replaceMatches } from "../core/find-replace.js";
|
|
|
28
28
|
import { continueMarkupOnEnter, makeLink, setLinePrefix, toggleWrap } from "../core/source-format.js";
|
|
29
29
|
import { markdownSourceLanguage } from "./markdown-language.js";
|
|
30
30
|
import { formatMarkdown } from "../core/markdown-format.js";
|
|
31
|
-
import { markdownFormatHost, textFormatTarget } from "../format/format-target.js";
|
|
31
|
+
import { markdownFormatHost, registerFormatElement, textFormatTarget } from "../format/format-target.js";
|
|
32
32
|
import { useSelectionZone } from "@ai-matrx/rich-content/selection-toolbar/selection-zones";
|
|
33
33
|
import { RICH_EDITOR_SHORTCUTS, TYPED_TRIGGERS } from "../core/shortcuts.js";
|
|
34
34
|
import { variableNamesInText, variableSuggestions } from "../core/variables.js";
|
|
@@ -273,9 +273,12 @@ function SourceEditor({
|
|
|
273
273
|
});
|
|
274
274
|
};
|
|
275
275
|
const [zoneElement, setZoneElement] = useState(null);
|
|
276
|
-
|
|
276
|
+
const [formatTarget] = useState(() => sourceFormatTarget(() => view.current));
|
|
277
|
+
useSelectionZone(zoneElement, { editable: !context.readOnly, host: markdownFormatHost(formatTarget) });
|
|
278
|
+
useEffect(() => zoneElement ? registerFormatElement(zoneElement, formatTarget) : void 0, [zoneElement, formatTarget]);
|
|
277
279
|
useImperativeHandle(handleRef, () => ({
|
|
278
280
|
focus: () => view.current?.focus(),
|
|
281
|
+
formatTarget: () => formatTarget,
|
|
279
282
|
selectedText: () => {
|
|
280
283
|
const v = view.current;
|
|
281
284
|
if (!v) return "";
|
|
@@ -3,6 +3,7 @@ import { type VisualLoadStats } from "../core/visual-document.js";
|
|
|
3
3
|
import { type CaretContext } from "../core/caret-context.js";
|
|
4
4
|
import type { FindOptions } from "../core/find-replace.js";
|
|
5
5
|
import { type RichShellActions } from "./visual-extensions.js";
|
|
6
|
+
import type { FormatTarget } from "../format/format-target.js";
|
|
6
7
|
export interface ViewFindState {
|
|
7
8
|
count: number;
|
|
8
9
|
current: number;
|
|
@@ -12,6 +13,8 @@ export interface ViewFindState {
|
|
|
12
13
|
/** What the shell drives in whichever view is showing. */
|
|
13
14
|
export interface EditorViewHandle {
|
|
14
15
|
focus: () => void;
|
|
16
|
+
/** The plain-text engines' FormatTarget (Source view) — the toolbar's format buttons act through it. */
|
|
17
|
+
formatTarget?: () => FormatTarget | null;
|
|
15
18
|
selectedText: () => string;
|
|
16
19
|
/** Replace the selection with literal text (markdown typed by an AI stays markdown). */
|
|
17
20
|
replaceSelection: (text: string) => void;
|
|
@@ -196,7 +196,7 @@ function VisualEditor({
|
|
|
196
196
|
});
|
|
197
197
|
const index = locateCaret(flat, context2);
|
|
198
198
|
if (index === null) return false;
|
|
199
|
-
const pos = index
|
|
199
|
+
const pos = index > 0 && positions[index - 1] !== void 0 ? positions[index - 1] + 1 : positions[index] ?? editor.state.doc.content.size - 1;
|
|
200
200
|
try {
|
|
201
201
|
editor.view.dispatch(editor.state.tr.setSelection(TextSelection.near(editor.state.doc.resolve(pos))).scrollIntoView());
|
|
202
202
|
editor.view.focus();
|
|
@@ -49,13 +49,9 @@ function IslandBlockView({ node, updateAttributes, deleteNode, selected, editor,
|
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
51
|
const copy = async () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
window.setTimeout(() => setCopied(false), 1200);
|
|
56
|
-
} catch {
|
|
57
|
-
toast.error("Copy failed \u2014 your browser blocked clipboard access.");
|
|
58
|
-
}
|
|
52
|
+
if (!await copyText(raw, void 0, "Copy failed \u2014 your browser blocked clipboard access.")) return;
|
|
53
|
+
setCopied(true);
|
|
54
|
+
window.setTimeout(() => setCopied(false), 1200);
|
|
59
55
|
};
|
|
60
56
|
const isFence = islandType === "fence";
|
|
61
57
|
const isMath = meta.language === "math";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/rich-editor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "The AI Matrx editor: Visual (Tiptap), Source (CodeMirror), Preview, the markdown formatting command layer, and edit-in-place — over @ai-matrx/rich-content. Consumers that only render never install it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|