@ai-matrx/rich-editor 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/core/commands.d.ts +3 -0
- package/dist/core/commands.js +51 -0
- package/dist/core/extensions.js +3 -1
- package/dist/core/markdown-format.d.ts +1 -1
- package/dist/core/markdown-format.js +11 -0
- package/dist/core/typing-rules.d.ts +11 -0
- package/dist/core/typing-rules.js +96 -0
- package/dist/editor/RichEditorImpl.d.ts +3 -1
- package/dist/editor/RichEditorImpl.js +38 -2
- package/dist/format/FormatButtons.d.ts +2 -0
- package/dist/format/FormatButtons.js +10 -2
- package/dist/format/editor-keys.d.ts +4 -0
- package/dist/format/editor-keys.js +14 -0
- package/dist/visual/TableHandle.d.ts +6 -0
- package/dist/visual/TableHandle.js +129 -0
- package/dist/visual/VisualEditor.js +4 -1
- package/dist/visual/format-actions.d.ts +3 -1
- package/dist/visual/format-actions.js +3 -2
- package/dist/visual/table-menu.d.ts +13 -0
- package/dist/visual/table-menu.js +20 -0
- package/dist/visual/visual-format-target.js +2 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
### Write behaves like a notes app (2026-10-08)
|
|
5
|
+
|
|
6
|
+
- **Typed markdown converts** (`core/typing-rules.ts`, in the schema extensions): `[] `, `[ ] `, `[x] ` at a line's start make a checklist item; `- [ ] ` / `* [ ] ` turn the bullet into a task; an address followed by a space (`https://…`, `www.…`) becomes a link stored bare; ` ` becomes an image. Only on the key press that completes the pattern — load, paste and drags never convert.
|
|
7
|
+
- **Table menu**: while the caret is in a table, a handle on its top-right corner opens add row above/below, add column left/right, Header row (on/off — off stores GFM's empty header line), delete row/column/table (`visual/table-menu.ts`, `visual/TableHandle.tsx`). `core/commands.ts` adds `toggleTableHeaderRow` / `tableHeaderRowShown`.
|
|
8
|
+
- **Image**: a new `image` format command. `FormatButtons` always has More, and More always holds **Image**. In Write it opens the image prompt — paste an address, or upload a file (uploaded to the platform's files as before); text engines insert `` with the address selected. The `/image` slash item opens the same prompt.
|
|
9
|
+
- **`format/editor-keys`**: `editorOwnsKeyEvent(event)` — true inside a format host, a contenteditable or a CodeMirror view. App-wide shortcuts (the shell's ⌘K bar) stand down there so ⌘K is the editor's link prompt.
|
|
10
|
+
|
|
11
|
+
Consumer action: an app-wide ⌘K (or other chord) listener calls `editorOwnsKeyEvent(e)` and skips when true. A host with an exhaustive `Record<FormatCommandId, …>` adds `image`.
|
|
12
|
+
|
|
13
|
+
## 0.4.3
|
|
14
|
+
### `chrome="format"`: the toolbar for hosts with no row of their own (2026-10-08)
|
|
15
|
+
|
|
16
|
+
- `RichEditor` `chrome="format"` renders ONE slim row carrying only the formatting toolbar (no view switch, tools or Save) in every editable view — for hosts like a record's body that had `chrome="bare"` and so no formatting buttons. Editable text always has the toolbar (Arman, 2026-10-08).
|
|
17
|
+
|
|
18
|
+
Consumer action: a `chrome="bare"` host with no toolbar row of its own switches to `chrome="format"`.
|
|
19
|
+
|
|
3
20
|
## 0.4.2
|
|
4
21
|
### A short toolbar keeps what people reach for (2026-10-08)
|
|
5
22
|
|
package/dist/core/commands.d.ts
CHANGED
|
@@ -63,3 +63,6 @@ export type ColumnAlignment = "left" | "center" | "right" | null;
|
|
|
63
63
|
* rewritten and nothing else in the table moves.
|
|
64
64
|
*/
|
|
65
65
|
export declare function setColumnAlign(editor: Editor, align: ColumnAlignment): boolean;
|
|
66
|
+
/** Is the caret's table showing a header row (a first row with any text)? */
|
|
67
|
+
export declare function tableHeaderRowShown(editor: Editor): boolean;
|
|
68
|
+
export declare function toggleTableHeaderRow(editor: Editor): boolean;
|
package/dist/core/commands.js
CHANGED
|
@@ -269,6 +269,55 @@ function setColumnAlign(editor, align) {
|
|
|
269
269
|
return true;
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
|
+
function tableAt(editor) {
|
|
273
|
+
const { $from } = editor.state.selection;
|
|
274
|
+
for (let depth = $from.depth; depth > 0; depth -= 1) {
|
|
275
|
+
const node = $from.node(depth);
|
|
276
|
+
if (node.type.name === "table") return { table: node, pos: $from.before(depth) };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function tableHeaderRowShown(editor) {
|
|
281
|
+
const found = tableAt(editor);
|
|
282
|
+
return Boolean(found?.table.firstChild && found.table.firstChild.textContent.trim().length > 0);
|
|
283
|
+
}
|
|
284
|
+
function toggleTableHeaderRow(editor) {
|
|
285
|
+
const found = tableAt(editor);
|
|
286
|
+
if (!found) return false;
|
|
287
|
+
const { table, pos } = found;
|
|
288
|
+
const { schema } = editor.state;
|
|
289
|
+
const headerType = schema.nodes.tableHeader;
|
|
290
|
+
const cellType = schema.nodes.tableCell;
|
|
291
|
+
const rowType = schema.nodes.tableRow;
|
|
292
|
+
const paragraph = schema.nodes.paragraph;
|
|
293
|
+
const first = table.firstChild;
|
|
294
|
+
if (!headerType || !cellType || !rowType || !paragraph || !first) return false;
|
|
295
|
+
const shown = first.textContent.trim().length > 0;
|
|
296
|
+
if (!shown && table.childCount < 2) return false;
|
|
297
|
+
return editor.chain().focus().command(({ tr }) => {
|
|
298
|
+
const rowPos = pos + 1;
|
|
299
|
+
if (shown) {
|
|
300
|
+
let cellPos = rowPos + 1;
|
|
301
|
+
first.forEach((cell) => {
|
|
302
|
+
tr.setNodeMarkup(cellPos, cellType, cell.attrs);
|
|
303
|
+
cellPos += cell.nodeSize;
|
|
304
|
+
});
|
|
305
|
+
const cells = [];
|
|
306
|
+
first.forEach((cell) => cells.push(headerType.create({ align: cell.attrs.align ?? null }, paragraph.create())));
|
|
307
|
+
tr.insert(rowPos, rowType.create(null, cells));
|
|
308
|
+
} else {
|
|
309
|
+
tr.delete(rowPos, rowPos + first.nodeSize);
|
|
310
|
+
const promoted = tr.doc.nodeAt(rowPos);
|
|
311
|
+
if (!promoted) return false;
|
|
312
|
+
let cellPos = rowPos + 1;
|
|
313
|
+
promoted.forEach((cell) => {
|
|
314
|
+
tr.setNodeMarkup(cellPos, headerType, cell.attrs);
|
|
315
|
+
cellPos += cell.nodeSize;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}).run();
|
|
320
|
+
}
|
|
272
321
|
export {
|
|
273
322
|
CALLOUT_TYPES,
|
|
274
323
|
TASK_DONE,
|
|
@@ -292,6 +341,8 @@ export {
|
|
|
292
341
|
replaceIslandRaw,
|
|
293
342
|
setCallout,
|
|
294
343
|
setColumnAlign,
|
|
344
|
+
tableHeaderRowShown,
|
|
345
|
+
toggleTableHeaderRow,
|
|
295
346
|
toggleTaskChecked,
|
|
296
347
|
toggleTaskList,
|
|
297
348
|
topLevelAt,
|
package/dist/core/extensions.js
CHANGED
|
@@ -21,6 +21,7 @@ import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table
|
|
|
21
21
|
import { Dropcursor, Focus, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
|
|
22
22
|
import { MarkdownTextPaste } from "./paste-markdown.js";
|
|
23
23
|
import { ClipboardPlainText } from "./clipboard-text.js";
|
|
24
|
+
import { MarkdownTypingRules } from "./typing-rules.js";
|
|
24
25
|
const mdAttr = () => ({ default: null, rendered: false, keepOnSplit: false });
|
|
25
26
|
const MD_ID_NODE_TYPES = [
|
|
26
27
|
"paragraph",
|
|
@@ -310,7 +311,8 @@ function createRichEditorExtensions(options = {}) {
|
|
|
310
311
|
Dropcursor,
|
|
311
312
|
Placeholder.configure({ placeholder: options.placeholder ?? "Write\u2026" }),
|
|
312
313
|
MarkdownTextPaste.configure({ onNotice: options.onPasteNotice ?? null }),
|
|
313
|
-
ClipboardPlainText
|
|
314
|
+
ClipboardPlainText,
|
|
315
|
+
MarkdownTypingRules
|
|
314
316
|
];
|
|
315
317
|
return list.map((extension) => extension.extend({ addPasteRules: () => [] }));
|
|
316
318
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SourceEditResult } from "./source-format.js";
|
|
2
|
-
export type FormatCommandId = "bold" | "italic" | "strike" | "code" | "link" | "heading1" | "heading2" | "heading3" | "bulletList" | "orderedList" | "taskList" | "quote" | "codeBlock" | "paragraph" | "table" | "horizontalRule" | "undo" | "redo";
|
|
2
|
+
export type FormatCommandId = "bold" | "italic" | "strike" | "code" | "link" | "heading1" | "heading2" | "heading3" | "bulletList" | "orderedList" | "taskList" | "quote" | "codeBlock" | "paragraph" | "table" | "horizontalRule" | "image" | "undo" | "redo";
|
|
3
3
|
export declare const FORMAT_COMMAND_IDS: readonly FormatCommandId[];
|
|
4
4
|
export declare function isFormatCommandId(id: string): id is FormatCommandId;
|
|
5
5
|
/** Run one formatting command on markdown text. Returns the minimal change. */
|
|
@@ -16,6 +16,7 @@ const FORMAT_COMMAND_IDS = [
|
|
|
16
16
|
"paragraph",
|
|
17
17
|
"table",
|
|
18
18
|
"horizontalRule",
|
|
19
|
+
"image",
|
|
19
20
|
"undo",
|
|
20
21
|
"redo"
|
|
21
22
|
];
|
|
@@ -390,11 +391,19 @@ function formatMarkdown(text, from, to, command) {
|
|
|
390
391
|
return insertBlockAfter(text, a, b, "| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| | | |\n| | | |", 2, 8);
|
|
391
392
|
case "horizontalRule":
|
|
392
393
|
return insertBlockAfter(text, a, b, "---", null, 0);
|
|
394
|
+
case "image":
|
|
395
|
+
return imageCommand(text, a, b);
|
|
393
396
|
case "undo":
|
|
394
397
|
case "redo":
|
|
395
398
|
return { changes: [], anchor: a, head: b };
|
|
396
399
|
}
|
|
397
400
|
}
|
|
401
|
+
function imageCommand(text, from, to) {
|
|
402
|
+
const alt = text.slice(from, to).replace(/[[\]\n]/g, " ").trim() || "image";
|
|
403
|
+
const insert = ``;
|
|
404
|
+
const urlStart = from + 2 + alt.length + 2;
|
|
405
|
+
return { changes: [{ from, to, insert }], anchor: urlStart, head: urlStart + "https://".length };
|
|
406
|
+
}
|
|
398
407
|
function paragraphCommand(text, from, to) {
|
|
399
408
|
const changes = [];
|
|
400
409
|
for (const line of linesTouched(text, from, to)) {
|
|
@@ -455,6 +464,7 @@ function isFormatActive(text, from, to, command) {
|
|
|
455
464
|
case "codeBlock":
|
|
456
465
|
case "table":
|
|
457
466
|
case "horizontalRule":
|
|
467
|
+
case "image":
|
|
458
468
|
case "undo":
|
|
459
469
|
case "redo":
|
|
460
470
|
return false;
|
|
@@ -486,6 +496,7 @@ const FORMAT_COMMAND_KEYS = {
|
|
|
486
496
|
paragraph: ["Mod-Alt-0"],
|
|
487
497
|
table: [],
|
|
488
498
|
horizontalRule: [],
|
|
499
|
+
image: [],
|
|
489
500
|
// The engine's own history answers ⌘Z / ⌘⇧Z; a host never rebinds them.
|
|
490
501
|
undo: [],
|
|
491
502
|
redo: []
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
/** `[ ] ` / `[] ` / `[x] ` typed at the very start of a line. */
|
|
3
|
+
export declare const TASK_TYPED: RegExp;
|
|
4
|
+
/** An address followed by the whitespace that ends it. */
|
|
5
|
+
export declare const URL_TYPED: RegExp;
|
|
6
|
+
/** `` followed by whitespace. */
|
|
7
|
+
export declare const IMAGE_TYPED: RegExp;
|
|
8
|
+
/** Trailing punctuation is sentence, not address (GFM's extended-autolink rule). */
|
|
9
|
+
export declare function trimUrl(url: string): string;
|
|
10
|
+
/** The typed-markdown conversions (header). Image first: its URL must never be autolinked. */
|
|
11
|
+
export declare const MarkdownTypingRules: Extension<any, any>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Extension, InputRule } from "@tiptap/core";
|
|
2
|
+
import { findWrapping } from "@tiptap/pm/transform";
|
|
3
|
+
import { TASK_DONE, TASK_OPEN } from "./commands.js";
|
|
4
|
+
const TASK_TYPED = /^\s*\[([ xX]?)\]\s$/;
|
|
5
|
+
const URL_TYPED = /(?:^|\s)((?:https?:\/\/|www\.)[^\s<>]+)(\s)$/;
|
|
6
|
+
const IMAGE_TYPED = /!\[([^\]\n]*)\]\(([^()\s]+)\)(\s)$/;
|
|
7
|
+
function trimUrl(url) {
|
|
8
|
+
let out = url.replace(/[.,;:!?'"*_~]+$/, "");
|
|
9
|
+
while (out.endsWith(")") && (out.match(/\(/g)?.length ?? 0) < (out.match(/\)/g)?.length ?? 0)) {
|
|
10
|
+
out = out.slice(0, -1).replace(/[.,;:!?'"*_~]+$/, "");
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function listItemDepth($from) {
|
|
15
|
+
for (let depth = $from.depth; depth > 0; depth -= 1) {
|
|
16
|
+
if ($from.node(depth).type.name === "listItem") return depth;
|
|
17
|
+
}
|
|
18
|
+
return -1;
|
|
19
|
+
}
|
|
20
|
+
const taskRule = new InputRule({
|
|
21
|
+
find: TASK_TYPED,
|
|
22
|
+
handler: ({ state, range, match }) => {
|
|
23
|
+
const { tr } = state;
|
|
24
|
+
const mdTask = /[xX]/.test(match[1] ?? "") ? TASK_DONE : TASK_OPEN;
|
|
25
|
+
const $from = state.doc.resolve(range.from);
|
|
26
|
+
const textblock = $from.parent;
|
|
27
|
+
if (textblock.type.name !== "paragraph") return null;
|
|
28
|
+
const itemDepth = listItemDepth($from);
|
|
29
|
+
tr.delete(range.from, range.to);
|
|
30
|
+
if (itemDepth !== -1 && $from.index(itemDepth) === 0) {
|
|
31
|
+
const itemPos = $from.before(itemDepth);
|
|
32
|
+
const item = tr.doc.nodeAt(itemPos);
|
|
33
|
+
if (!item) return null;
|
|
34
|
+
tr.setNodeMarkup(itemPos, void 0, { ...item.attrs, mdTask });
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
const bulletList = state.schema.nodes.bulletList;
|
|
38
|
+
const listItem = state.schema.nodes.listItem;
|
|
39
|
+
if (!bulletList || !listItem) return null;
|
|
40
|
+
const $start = tr.doc.resolve(tr.mapping.map(range.from));
|
|
41
|
+
const blockRange = $start.blockRange();
|
|
42
|
+
const wrapping = blockRange && findWrapping(blockRange, bulletList);
|
|
43
|
+
if (!blockRange || !wrapping) return null;
|
|
44
|
+
tr.wrap(
|
|
45
|
+
blockRange,
|
|
46
|
+
wrapping.map((wrap) => wrap.type === listItem ? { type: listItem, attrs: { ...wrap.attrs, mdTask } } : wrap)
|
|
47
|
+
);
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const imageRule = new InputRule({
|
|
52
|
+
find: IMAGE_TYPED,
|
|
53
|
+
handler: ({ state, range, match }) => {
|
|
54
|
+
const island = state.schema.nodes.inlineIsland;
|
|
55
|
+
if (!island || state.doc.resolve(range.from).parent.type.spec.code) return null;
|
|
56
|
+
const alt = (match[1] ?? "").replace(/[[\]]/g, "");
|
|
57
|
+
const url = match[2] ?? "";
|
|
58
|
+
const space = match[3] ?? " ";
|
|
59
|
+
const { tr } = state;
|
|
60
|
+
tr.replaceWith(range.from, range.to, island.create({ raw: ``, islandType: "md_image" }));
|
|
61
|
+
tr.insertText(space, tr.mapping.map(range.to));
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const urlRule = new InputRule({
|
|
66
|
+
find: URL_TYPED,
|
|
67
|
+
handler: ({ state, range, match }) => {
|
|
68
|
+
const link = state.schema.marks.link;
|
|
69
|
+
const typed = match[1] ?? "";
|
|
70
|
+
const url = trimUrl(typed);
|
|
71
|
+
if (!link || url.length < 5 || /^www\.$/i.test(url)) return null;
|
|
72
|
+
const start = range.from + match[0].indexOf(typed);
|
|
73
|
+
const end = start + url.length;
|
|
74
|
+
if (state.doc.rangeHasMark(start, end, link)) return null;
|
|
75
|
+
const $start = state.doc.resolve(start);
|
|
76
|
+
if ($start.parent.type.spec.code || $start.marks().some((mark) => mark.type.name === "code")) return null;
|
|
77
|
+
const href = /^www\./i.test(url) ? `https://${url}` : url;
|
|
78
|
+
const { tr } = state;
|
|
79
|
+
tr.addMark(start, end, link.create({ href, mdForm: "bare", mdTailHref: href, mdText: url }));
|
|
80
|
+
tr.removeStoredMark(link);
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const MarkdownTypingRules = Extension.create({
|
|
85
|
+
name: "richEditorTypingRules",
|
|
86
|
+
addInputRules() {
|
|
87
|
+
return [taskRule, imageRule, urlRule];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
export {
|
|
91
|
+
IMAGE_TYPED,
|
|
92
|
+
MarkdownTypingRules,
|
|
93
|
+
TASK_TYPED,
|
|
94
|
+
URL_TYPED,
|
|
95
|
+
trimUrl
|
|
96
|
+
};
|
|
@@ -86,9 +86,11 @@ export interface RichEditorProps {
|
|
|
86
86
|
* "full" (default): the editor's toolbar row — views, tools, dictation, Save.
|
|
87
87
|
* "bare": no row of its own; the host's chrome carries the view switch, find,
|
|
88
88
|
* outline and dictation (a host that saves by itself — autosave — has no Save).
|
|
89
|
+
* "format": a host with no row of its own (a record's body): ONE slim row
|
|
90
|
+
* carrying only the formatting toolbar (2026-10-08: editable text always has it).
|
|
89
91
|
* Shortcuts (⌘F, ⌘⌥H, ⌘⇧F, ⌘/) keep working either way.
|
|
90
92
|
*/
|
|
91
|
-
chrome?: "full" | "bare";
|
|
93
|
+
chrome?: "full" | "bare" | "format";
|
|
92
94
|
/**
|
|
93
95
|
* The host wraps the editor in its OWN right-click menu (its surface, scope and
|
|
94
96
|
* record rows) — the editor then mounts none, so one menu answers. Drive text
|
|
@@ -123,7 +123,7 @@ function RichEditorImpl({
|
|
|
123
123
|
const [mountKey, setMountKey] = useState(0);
|
|
124
124
|
const [findMode, setFindMode] = useState(null);
|
|
125
125
|
const [outlineChoice, setOutlineChoice] = useState(
|
|
126
|
-
defaultOutlineOpen ?? (chrome
|
|
126
|
+
defaultOutlineOpen ?? (chrome !== "full" ? false : null)
|
|
127
127
|
);
|
|
128
128
|
const outlineOpen = outlineChoice ?? !isMobile;
|
|
129
129
|
const setOutlineOpen = (next) => setOutlineChoice(typeof next === "function" ? next(outlineOpen) : next);
|
|
@@ -135,6 +135,7 @@ function RichEditorImpl({
|
|
|
135
135
|
const [saving, setSaving] = useState(false);
|
|
136
136
|
const [saveState, setSaveState] = useState(null);
|
|
137
137
|
const [linkDialog, setLinkDialog] = useState({ open: false, href: "" });
|
|
138
|
+
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
|
138
139
|
const [kindRequest, setKindRequest] = useState(null);
|
|
139
140
|
const approved = useRef(/* @__PURE__ */ new Set());
|
|
140
141
|
const handle = useRef(null);
|
|
@@ -256,7 +257,11 @@ function RichEditorImpl({
|
|
|
256
257
|
}
|
|
257
258
|
void doSave(plan);
|
|
258
259
|
};
|
|
259
|
-
const pickImage = () =>
|
|
260
|
+
const pickImage = () => setImageDialogOpen(true);
|
|
261
|
+
const uploadImageFile = () => {
|
|
262
|
+
setImageDialogOpen(false);
|
|
263
|
+
imageInput.current?.click();
|
|
264
|
+
};
|
|
260
265
|
const uploadImage = async (file) => {
|
|
261
266
|
try {
|
|
262
267
|
const uploaded = await upload({ kind: "file", file }, { folderPath: "Editor images" });
|
|
@@ -538,6 +543,7 @@ function RichEditorImpl({
|
|
|
538
543
|
saveButton
|
|
539
544
|
] }),
|
|
540
545
|
/* @__PURE__ */ jsxs("div", { className: "relative z-20 shrink-0", children: [
|
|
546
|
+
!focusMode && chrome === "format" && editable && /* @__PURE__ */ jsx("div", { className: "matrx-touch-targets flex min-h-9 items-center border-b border-border bg-card/80 px-1.5 py-0.5 backdrop-blur", "data-rich-editor-format-row": "", children: /* @__PURE__ */ jsx(FormatButtons, { className: "flex-1", resolve: () => handle.current?.formatTarget?.() ?? null }) }),
|
|
541
547
|
!focusMode && chrome === "full" && /* @__PURE__ */ jsxs("div", { ref: toolbarRowRef, className: "matrx-touch-targets flex min-h-10 items-center gap-1 border-b border-border bg-card/80 px-2 py-1 backdrop-blur", children: [
|
|
542
548
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto", children: [
|
|
543
549
|
isMobile ? (
|
|
@@ -694,6 +700,36 @@ function RichEditorImpl({
|
|
|
694
700
|
}
|
|
695
701
|
}
|
|
696
702
|
),
|
|
703
|
+
/* @__PURE__ */ jsx(
|
|
704
|
+
TextInputDialog,
|
|
705
|
+
{
|
|
706
|
+
open: imageDialogOpen,
|
|
707
|
+
onOpenChange: setImageDialogOpen,
|
|
708
|
+
title: "Image",
|
|
709
|
+
description: /* @__PURE__ */ jsxs("span", { children: [
|
|
710
|
+
"Paste the image address, or",
|
|
711
|
+
" ",
|
|
712
|
+
/* @__PURE__ */ jsx("button", { type: "button", "data-image-upload": "", className: "font-medium text-primary underline-offset-2 hover:underline", onClick: uploadImageFile, children: "upload a file" }),
|
|
713
|
+
"."
|
|
714
|
+
] }),
|
|
715
|
+
placeholder: "https://",
|
|
716
|
+
defaultValue: "",
|
|
717
|
+
confirmLabel: "Insert",
|
|
718
|
+
validate: (value2) => /^(https?:\/\/|\/)\S+$/i.test(value2.trim()) ? null : "Paste an address that starts with https://",
|
|
719
|
+
onConfirm: (href) => {
|
|
720
|
+
const url = href.trim();
|
|
721
|
+
const last = url.split(/[?#]/)[0].split("/").pop() ?? "";
|
|
722
|
+
let name = last;
|
|
723
|
+
try {
|
|
724
|
+
name = decodeURIComponent(last);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
const alt = name.replace(/\.[^.]+$/, "").replace(/[[\]]/g, "") || "image";
|
|
728
|
+
handle.current?.replaceSelection(``);
|
|
729
|
+
setImageDialogOpen(false);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
),
|
|
697
733
|
/* @__PURE__ */ jsx(
|
|
698
734
|
"input",
|
|
699
735
|
{
|
|
@@ -11,6 +11,8 @@ export interface FormatButtonSpec {
|
|
|
11
11
|
export declare const BLOCK_STYLES: readonly FormatButtonSpec[];
|
|
12
12
|
/** The toolbar, in its groups (a divider between each). The text-style menu sits after history. */
|
|
13
13
|
export declare const FORMAT_GROUPS: readonly (readonly FormatButtonSpec[])[];
|
|
14
|
+
/** Always under More (never a strip button): inserts people reach for less often. */
|
|
15
|
+
export declare const MORE_ONLY: readonly FormatButtonSpec[];
|
|
14
16
|
/** Every button the toolbar shows, in order (the text-style menu aside). */
|
|
15
17
|
export declare const FORMAT_BUTTONS: readonly FormatButtonSpec[];
|
|
16
18
|
/** The phone set (Arman, 2026-10-07): text style, B, I, list, checklist, link — the rest under More. */
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Heading2,
|
|
11
11
|
Heading3,
|
|
12
12
|
Italic,
|
|
13
|
+
Image as ImageIcon,
|
|
13
14
|
Link2,
|
|
14
15
|
List,
|
|
15
16
|
ListChecks,
|
|
@@ -52,6 +53,7 @@ const FORMAT_GROUPS = [
|
|
|
52
53
|
[spec("link", "Link", Link2)],
|
|
53
54
|
[spec("codeBlock", "Code block", SquareCode), spec("table", "Table", Table), spec("horizontalRule", "Horizontal rule", Minus)]
|
|
54
55
|
];
|
|
56
|
+
const MORE_ONLY = [spec("image", "Image", ImageIcon)];
|
|
55
57
|
const FORMAT_BUTTONS = FORMAT_GROUPS.flat();
|
|
56
58
|
const ESSENTIAL_COMMANDS = ["bold", "italic", "bulletList", "taskList", "link"];
|
|
57
59
|
const KEEP = {
|
|
@@ -155,7 +157,7 @@ function FormatButtons({ resolve, disabled = false, className, size = "sm", vari
|
|
|
155
157
|
let kept = all;
|
|
156
158
|
let folded = variant === "essential" ? slotsOf("full").filter((s) => s.kind === "button" && !ESSENTIAL_COMMANDS.includes(s.spec.command)) : [];
|
|
157
159
|
const rowWidth = (slots, more) => withDividers(slots).reduce((sum, s) => sum + widthOf(s), 0) + (more ? button : 0);
|
|
158
|
-
if (width > 0 && rowWidth(kept,
|
|
160
|
+
if (width > 0 && rowWidth(kept, true) > width + 4) {
|
|
159
161
|
const byKeep = [...all].sort((x, y) => keepOf(x) - keepOf(y));
|
|
160
162
|
const dropped = /* @__PURE__ */ new Set();
|
|
161
163
|
for (const slot of byKeep) {
|
|
@@ -234,7 +236,7 @@ function FormatButtons({ resolve, disabled = false, className, size = "sm", vari
|
|
|
234
236
|
command
|
|
235
237
|
);
|
|
236
238
|
}),
|
|
237
|
-
(foldedButtons.length > 0 || styleFolded) && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
239
|
+
(foldedButtons.length > 0 || styleFolded || MORE_ONLY.length > 0) && /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
238
240
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "More formatting", title: "More formatting", "data-format-more": "", "data-matrx-button": "", className: buttonClass, children: /* @__PURE__ */ jsx(MoreHorizontal, { className: iconClass }) }) }),
|
|
239
241
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56", onCloseAutoFocus: keepEditorFocus, children: [
|
|
240
242
|
styleFolded && BLOCK_STYLES.map((style) => /* @__PURE__ */ jsxs(DropdownMenuItem, { "data-format-command": style.command, onSelect: () => run(style.command), children: [
|
|
@@ -247,6 +249,11 @@ function FormatButtons({ resolve, disabled = false, className, size = "sm", vari
|
|
|
247
249
|
/* @__PURE__ */ jsx(b.icon, { className: "mr-2 h-4 w-4" }),
|
|
248
250
|
/* @__PURE__ */ jsx("span", { className: "flex-1", children: b.label }),
|
|
249
251
|
b.command !== "undo" && b.command !== "redo" && isActive(b.command) && /* @__PURE__ */ jsx(Check, { className: "ml-2 h-4 w-4 text-primary" })
|
|
252
|
+
] }, b.command)),
|
|
253
|
+
(styleFolded || foldedButtons.length > 0) && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
254
|
+
MORE_ONLY.map((b) => /* @__PURE__ */ jsxs(DropdownMenuItem, { "data-format-command": b.command, onSelect: () => run(b.command), children: [
|
|
255
|
+
/* @__PURE__ */ jsx(b.icon, { className: "mr-2 h-4 w-4" }),
|
|
256
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: b.label })
|
|
250
257
|
] }, b.command))
|
|
251
258
|
] })
|
|
252
259
|
] })
|
|
@@ -261,5 +268,6 @@ export {
|
|
|
261
268
|
FORMAT_BUTTONS,
|
|
262
269
|
FORMAT_GROUPS,
|
|
263
270
|
FormatButtons,
|
|
271
|
+
MORE_ONLY,
|
|
264
272
|
FormatButtons_default as default
|
|
265
273
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** True when `target` (else the focused element) sits inside an editor. */
|
|
2
|
+
export declare function insideEditor(target: EventTarget | null | undefined): boolean;
|
|
3
|
+
/** True when a key press belongs to the editor it was pressed in (shell shortcuts skip it). */
|
|
4
|
+
export declare function editorOwnsKeyEvent(event: Pick<KeyboardEvent, "target">): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const EDITOR_SELECTOR = "[data-format-host], [contenteditable=''], [contenteditable='true'], [contenteditable='plaintext-only'], .cm-editor";
|
|
2
|
+
function insideEditor(target) {
|
|
3
|
+
const element = target && typeof target.closest === "function" ? target : typeof document !== "undefined" ? document.activeElement : null;
|
|
4
|
+
if (!element || typeof element.closest !== "function") return false;
|
|
5
|
+
if (element.closest(EDITOR_SELECTOR)) return true;
|
|
6
|
+
return element.isContentEditable === true;
|
|
7
|
+
}
|
|
8
|
+
function editorOwnsKeyEvent(event) {
|
|
9
|
+
return insideEditor(event.target);
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
editorOwnsKeyEvent,
|
|
13
|
+
insideEditor
|
|
14
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
cn,
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuTrigger
|
|
11
|
+
} from "@ai-matrx/design-system";
|
|
12
|
+
import {
|
|
13
|
+
ArrowDownToLine,
|
|
14
|
+
ArrowLeftToLine,
|
|
15
|
+
ArrowRightToLine,
|
|
16
|
+
ArrowUpToLine,
|
|
17
|
+
Check,
|
|
18
|
+
Columns3,
|
|
19
|
+
Heading,
|
|
20
|
+
Rows3,
|
|
21
|
+
Table2,
|
|
22
|
+
Trash2
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { useRichEditorContext } from "../editor/RichEditorContext.js";
|
|
25
|
+
import { TABLE_MENU } from "./table-menu.js";
|
|
26
|
+
const ICONS = {
|
|
27
|
+
"row-above": ArrowUpToLine,
|
|
28
|
+
"row-below": ArrowDownToLine,
|
|
29
|
+
"col-left": ArrowLeftToLine,
|
|
30
|
+
"col-right": ArrowRightToLine,
|
|
31
|
+
"header-row": Heading,
|
|
32
|
+
"del-row": Rows3,
|
|
33
|
+
"del-col": Columns3,
|
|
34
|
+
"del-table": Trash2
|
|
35
|
+
};
|
|
36
|
+
function anchorOf(editor, host) {
|
|
37
|
+
if (editor.isDestroyed || !editor.isEditable || !editor.isActive("table")) return null;
|
|
38
|
+
const { $from } = editor.state.selection;
|
|
39
|
+
for (let depth = $from.depth; depth > 0; depth -= 1) {
|
|
40
|
+
if ($from.node(depth).type.name !== "table") continue;
|
|
41
|
+
const dom = editor.view.nodeDOM($from.before(depth));
|
|
42
|
+
const element = dom instanceof HTMLElement ? dom.closest(".tableWrapper") ?? dom : null;
|
|
43
|
+
if (!element) return null;
|
|
44
|
+
const box = element.getBoundingClientRect();
|
|
45
|
+
const hostBox = host.getBoundingClientRect();
|
|
46
|
+
return { top: box.top - hostBox.top + host.scrollTop, right: hostBox.right - box.right };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function TableHandle({ editor, container }) {
|
|
51
|
+
const context = useRichEditorContext();
|
|
52
|
+
const [anchor, setAnchor] = useState(null);
|
|
53
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const host = container.current;
|
|
56
|
+
if (!editor || !host || context.readOnly) return void 0;
|
|
57
|
+
const update = () => {
|
|
58
|
+
if (menuOpen) return;
|
|
59
|
+
setAnchor(editor.isFocused ? anchorOf(editor, host) : null);
|
|
60
|
+
};
|
|
61
|
+
update();
|
|
62
|
+
editor.on("selectionUpdate", update);
|
|
63
|
+
editor.on("transaction", update);
|
|
64
|
+
editor.on("focus", update);
|
|
65
|
+
editor.on("blur", update);
|
|
66
|
+
return () => {
|
|
67
|
+
editor.off("selectionUpdate", update);
|
|
68
|
+
editor.off("transaction", update);
|
|
69
|
+
editor.off("focus", update);
|
|
70
|
+
editor.off("blur", update);
|
|
71
|
+
};
|
|
72
|
+
}, [editor, container, context.readOnly, menuOpen]);
|
|
73
|
+
if (!editor || !anchor || context.readOnly) return null;
|
|
74
|
+
let section = null;
|
|
75
|
+
return /* @__PURE__ */ jsx("div", { className: "absolute z-10", style: { top: Math.max(0, anchor.top - 14), right: Math.max(0, anchor.right - 10) }, children: /* @__PURE__ */ jsxs(
|
|
76
|
+
DropdownMenu,
|
|
77
|
+
{
|
|
78
|
+
open: menuOpen,
|
|
79
|
+
onOpenChange: (open) => {
|
|
80
|
+
setMenuOpen(open);
|
|
81
|
+
if (!open) editor.commands.focus();
|
|
82
|
+
},
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
type: "button",
|
|
88
|
+
"aria-label": "Table options",
|
|
89
|
+
title: "Table options",
|
|
90
|
+
"data-table-handle": "",
|
|
91
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
92
|
+
className: cn(
|
|
93
|
+
"flex h-6 w-6 items-center justify-center rounded-md border border-border bg-background text-muted-foreground shadow-sm",
|
|
94
|
+
"hover:bg-accent hover:text-foreground"
|
|
95
|
+
),
|
|
96
|
+
children: /* @__PURE__ */ jsx(Table2, { className: "h-3.5 w-3.5" })
|
|
97
|
+
}
|
|
98
|
+
) }),
|
|
99
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-52", onCloseAutoFocus: (event) => event.preventDefault(), children: TABLE_MENU.map((item) => {
|
|
100
|
+
const Icon = ICONS[item.id] ?? Table2;
|
|
101
|
+
const divider = section !== null && section !== item.section;
|
|
102
|
+
section = item.section;
|
|
103
|
+
const checked = item.checked?.(editor) ?? false;
|
|
104
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
105
|
+
divider && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
106
|
+
/* @__PURE__ */ jsxs(
|
|
107
|
+
DropdownMenuItem,
|
|
108
|
+
{
|
|
109
|
+
"data-table-action": item.id,
|
|
110
|
+
className: cn(item.destructive && "text-destructive focus:text-destructive"),
|
|
111
|
+
onSelect: () => {
|
|
112
|
+
item.run(editor);
|
|
113
|
+
},
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4" }),
|
|
116
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: item.label }),
|
|
117
|
+
item.checked && checked && /* @__PURE__ */ jsx(Check, { className: "ml-2 h-4 w-4 text-primary" })
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
] }, item.id);
|
|
122
|
+
}) })
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
) });
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
TableHandle
|
|
129
|
+
};
|
|
@@ -21,6 +21,7 @@ import { contextAtIndex, locateCaret } from "../core/caret-context.js";
|
|
|
21
21
|
import { createVisualExtensions } from "./visual-extensions.js";
|
|
22
22
|
import { findHighlightKey } from "./decorations.js";
|
|
23
23
|
import { BlockHandle } from "./BlockHandle.js";
|
|
24
|
+
import { TableHandle } from "./TableHandle.js";
|
|
24
25
|
import { useSelectionZone } from "@ai-matrx/rich-content/selection-toolbar/selection-zones";
|
|
25
26
|
import { registerFormatElement } from "../format/format-target.js";
|
|
26
27
|
import { RICH_EDITOR_HOST_KEY, editorFormatTarget, islandSelectionOf, tableAnchorOf } from "./format-actions.js";
|
|
@@ -153,7 +154,7 @@ function VisualEditor({
|
|
|
153
154
|
};
|
|
154
155
|
const shellRef = useRef(shell);
|
|
155
156
|
shellRef.current = shell;
|
|
156
|
-
const formatTarget = useMemo(() => editor ? editorFormatTarget(editor, () => shellRef.current.editLink()) : null, [editor]);
|
|
157
|
+
const formatTarget = useMemo(() => editor ? editorFormatTarget(editor, () => shellRef.current.editLink(), () => shellRef.current.pickImage()) : null, [editor]);
|
|
157
158
|
const [formatZone, setFormatZone] = useState(null);
|
|
158
159
|
useEffect(() => {
|
|
159
160
|
if (!formatZone || !formatTarget || context.readOnly) return void 0;
|
|
@@ -286,6 +287,7 @@ function VisualEditor({
|
|
|
286
287
|
kind: "rich-editor",
|
|
287
288
|
editor,
|
|
288
289
|
onEditLink: shell.editLink,
|
|
290
|
+
onInsertImage: shell.pickImage,
|
|
289
291
|
offerVariables: context.variables !== null,
|
|
290
292
|
inTable: () => !editor.isDestroyed && editor.isActive("table")
|
|
291
293
|
} : null;
|
|
@@ -327,6 +329,7 @@ function VisualEditor({
|
|
|
327
329
|
"data-testid": "rich-editor-visual",
|
|
328
330
|
children: [
|
|
329
331
|
/* @__PURE__ */ jsx(BlockHandle, { editor, container }),
|
|
332
|
+
/* @__PURE__ */ jsx(TableHandle, { editor, container }),
|
|
330
333
|
/* @__PURE__ */ jsx(EditorContent, { editor, className: "mx-auto max-w-3xl pb-[40dvh]" })
|
|
331
334
|
]
|
|
332
335
|
}
|
|
@@ -6,6 +6,8 @@ export interface RichEditorSelectionHost {
|
|
|
6
6
|
kind: "rich-editor";
|
|
7
7
|
editor: Editor;
|
|
8
8
|
onEditLink: () => void;
|
|
9
|
+
/** Open the image prompt (upload a file or paste an address). */
|
|
10
|
+
onInsertImage?: () => void;
|
|
9
11
|
offerVariables: boolean;
|
|
10
12
|
/** The caret or selection is inside a table (its table actions show). */
|
|
11
13
|
inTable: () => boolean;
|
|
@@ -22,6 +24,6 @@ export declare function tableAnchorOf(editor: Editor): DOMRect | null;
|
|
|
22
24
|
* (link field, checklist, code-block island) — the selection toolbar and the
|
|
23
25
|
* visible toolbar (`FormatButtons`, through `registerFormatElement`) share it.
|
|
24
26
|
*/
|
|
25
|
-
export declare function editorFormatTarget(editor: Editor, onEditLink: () => void): FormatTarget;
|
|
27
|
+
export declare function editorFormatTarget(editor: Editor, onEditLink: () => void, onInsertImage?: () => void): FormatTarget;
|
|
26
28
|
/** The formatting provider (declared on load; the toolbar root registers it). */
|
|
27
29
|
export declare const richEditorFormatProvider: ActionProvider;
|
|
@@ -44,11 +44,12 @@ function editorOf(target) {
|
|
|
44
44
|
}
|
|
45
45
|
function visualTargetOf(target) {
|
|
46
46
|
const visual = editorOf(target);
|
|
47
|
-
return visual ? editorFormatTarget(visual.editor, visual.onEditLink) : null;
|
|
47
|
+
return visual ? editorFormatTarget(visual.editor, visual.onEditLink, visual.onInsertImage) : null;
|
|
48
48
|
}
|
|
49
|
-
function editorFormatTarget(editor, onEditLink) {
|
|
49
|
+
function editorFormatTarget(editor, onEditLink, onInsertImage) {
|
|
50
50
|
return visualFormatTarget(editor, {
|
|
51
51
|
link: () => (onEditLink(), true),
|
|
52
|
+
image: () => onInsertImage ? (onInsertImage(), true) : false,
|
|
52
53
|
taskList: () => toggleTaskList(editor),
|
|
53
54
|
codeBlock: () => {
|
|
54
55
|
markAutoEdit(editor, insertCodeBlock(editor));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
export interface TableMenuItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** A divider goes before this item. */
|
|
6
|
+
section: "rows" | "columns" | "header" | "delete";
|
|
7
|
+
destructive?: boolean;
|
|
8
|
+
run: (editor: Editor) => boolean;
|
|
9
|
+
/** A toggle's state (the header row). */
|
|
10
|
+
checked?: (editor: Editor) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const TABLE_MENU: readonly TableMenuItem[];
|
|
13
|
+
export declare function tableMenuItem(id: string): TableMenuItem;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { tableHeaderRowShown, toggleTableHeaderRow } from "../core/commands.js";
|
|
2
|
+
const TABLE_MENU = [
|
|
3
|
+
{ id: "row-above", section: "rows", label: "Add row above", run: (e) => e.chain().focus().addRowBefore().run() },
|
|
4
|
+
{ id: "row-below", section: "rows", label: "Add row below", run: (e) => e.chain().focus().addRowAfter().run() },
|
|
5
|
+
{ id: "col-left", section: "columns", label: "Add column left", run: (e) => e.chain().focus().addColumnBefore().run() },
|
|
6
|
+
{ id: "col-right", section: "columns", label: "Add column right", run: (e) => e.chain().focus().addColumnAfter().run() },
|
|
7
|
+
{ id: "header-row", section: "header", label: "Header row", run: (e) => toggleTableHeaderRow(e), checked: tableHeaderRowShown },
|
|
8
|
+
{ id: "del-row", section: "delete", label: "Delete row", destructive: true, run: (e) => e.chain().focus().deleteRow().run() },
|
|
9
|
+
{ id: "del-col", section: "delete", label: "Delete column", destructive: true, run: (e) => e.chain().focus().deleteColumn().run() },
|
|
10
|
+
{ id: "del-table", section: "delete", label: "Delete table", destructive: true, run: (e) => e.chain().focus().deleteTable().run() }
|
|
11
|
+
];
|
|
12
|
+
function tableMenuItem(id) {
|
|
13
|
+
const item = TABLE_MENU.find((candidate) => candidate.id === id);
|
|
14
|
+
if (!item) throw new Error(`No table menu item "${id}"`);
|
|
15
|
+
return item;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
TABLE_MENU,
|
|
19
|
+
tableMenuItem
|
|
20
|
+
};
|
|
@@ -17,6 +17,8 @@ const VISUAL = {
|
|
|
17
17
|
paragraph: { run: (e) => e.chain().focus().setParagraph().run(), active: (e) => e.isActive("paragraph") },
|
|
18
18
|
table: { run: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), active: (e) => e.isActive("table") },
|
|
19
19
|
horizontalRule: { run: (e) => e.chain().focus().setHorizontalRule().run(), active: () => false },
|
|
20
|
+
// The image prompt (upload or address) is the host shell's — passed in as an override.
|
|
21
|
+
image: { run: () => false, active: () => false },
|
|
20
22
|
undo: { run: (e) => e.chain().focus().undo().run(), active: () => false },
|
|
21
23
|
redo: { run: (e) => e.chain().focus().redo().run(), active: () => false }
|
|
22
24
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/rich-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -90,13 +90,13 @@
|
|
|
90
90
|
"@tiptap/suggestion": "^3.31.3",
|
|
91
91
|
"github-slugger": "^2.0.0",
|
|
92
92
|
"lucide-react": "^1.48.0",
|
|
93
|
-
"@ai-matrx/
|
|
93
|
+
"@ai-matrx/alchemy": "latest",
|
|
94
94
|
"@ai-matrx/agents": "latest",
|
|
95
|
+
"@ai-matrx/content-ir": "latest",
|
|
95
96
|
"@ai-matrx/design-system": "latest",
|
|
96
97
|
"@ai-matrx/kit": "latest",
|
|
97
98
|
"@ai-matrx/print": "latest",
|
|
98
|
-
"@ai-matrx/rich-content": "latest"
|
|
99
|
-
"@ai-matrx/alchemy": "latest"
|
|
99
|
+
"@ai-matrx/rich-content": "latest"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"react": ">=19.0.0",
|