@ai-matrx/rich-editor 0.2.10 → 0.3.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+ ### A block lands on the side of the click; a trailing space stays typed (G11B, 2026-10-07)
5
+
6
+ - `insertText(text, where)` takes `"nearest"` (new `BlockPlacement` type, `core/text-insertion`): from the start or the first half of the caret's paragraph the block goes BEFORE it, otherwise AFTER. `insertAtRichCaret(…, "block")` uses it. New helpers `blockSide`, `nearestSide` (plain text) and `blockSideOfSelection` (visual).
7
+ - A space typed at the end of a paragraph or heading survives the editor rebuilding from its own text — typing after "six." used to give "six.Then".
8
+ - A reference/action card in Write takes the whole line on a phone; its grip and tools wrap under it.
9
+
10
+ Consumer action: none required; a host passing `"before" | "after"` keeps that exact behaviour.
11
+
12
+ ## 0.2.11
13
+ ### Bold (and every inline mark) toggles like a real editor
14
+
15
+ - A selection that is partly bold becomes ONE bold span: the bold inside it loses its markers and one pair wraps the whole (`Alpha **bravo** delta` -> `**Alpha bravo delta**`, never `**Alpha **bravo** delta**`). A selection reaching into a bold span joins it.
16
+ - A selection inside one bold span unbolds just that part (`**a [b] c**` -> `**a** b **c**`), and the toolbar's pressed state is on there.
17
+ - Same rule for italic, strike and inline code — one command layer, every host.
18
+
19
+
3
20
  ## 0.2.10
4
21
  ### The caret survives a view switch
5
22
 
@@ -18,10 +18,18 @@
18
18
  */
19
19
  import { type Node as PMNode, type Schema } from "@tiptap/pm/model";
20
20
  import type { Transaction } from "@tiptap/pm/state";
21
+ import { type BlockPlacement } from "./text-insertion.js";
21
22
  /** The top-level boundary a block goes to, relative to [from, to]. */
22
23
  export declare function blockInsertPosition(doc: PMNode, from: number, to: number, where: "before" | "after"): number;
24
+ /**
25
+ * The side of its block a selection is nearer to (`blockSide`): a collapsed
26
+ * caret at the start or in the first half of a textblock → "before"; anything
27
+ * else — the second half, the end, an empty block, a selected chip, ⌘A,
28
+ * a position between blocks — → "after".
29
+ */
30
+ export declare function blockSideOfSelection(doc: PMNode, from: number, to: number): "before" | "after";
23
31
  /**
24
32
  * Insert markdown as its own block(s) before / after the selection's block.
25
33
  * Throws nothing it can avoid; returns false when the text parses to nothing.
26
34
  */
27
- export declare function insertMarkdownBlock(tr: Transaction, schema: Schema, text: string, where: "before" | "after"): boolean;
35
+ export declare function insertMarkdownBlock(tr: Transaction, schema: Schema, text: string, where: BlockPlacement): boolean;
@@ -1,15 +1,23 @@
1
1
  import { Fragment, Slice } from "@tiptap/pm/model";
2
2
  import { Selection } from "@tiptap/pm/state";
3
3
  import { markdownToSlice } from "./paste-markdown.js";
4
+ import { blockSide } from "./text-insertion.js";
4
5
  function blockInsertPosition(doc, from, to, where) {
5
6
  const clamp = (pos) => Math.max(0, Math.min(pos, doc.content.size));
6
7
  const $pos = doc.resolve(clamp(where === "before" ? from : to));
7
8
  if ($pos.depth === 0) return $pos.pos;
8
9
  return where === "before" ? $pos.before(1) : $pos.after(1);
9
10
  }
11
+ function blockSideOfSelection(doc, from, to) {
12
+ if (from !== to) return "after";
13
+ const $pos = doc.resolve(Math.max(0, Math.min(from, doc.content.size)));
14
+ if ($pos.depth === 0 || !$pos.parent.isTextblock) return "after";
15
+ return blockSide($pos.parentOffset, $pos.parent.content.size);
16
+ }
10
17
  function insertMarkdownBlock(tr, schema, text, where) {
11
18
  const { from, to } = tr.selection;
12
- const at = blockInsertPosition(tr.doc, from, to, where);
19
+ const side = where === "nearest" ? blockSideOfSelection(tr.doc, from, to) : where;
20
+ const at = blockInsertPosition(tr.doc, from, to, side);
13
21
  const parsed = markdownToSlice(text, schema);
14
22
  if (parsed.size === 0) return false;
15
23
  const blocks = new Slice(Fragment.from(parsed.content), 0, 0);
@@ -20,5 +28,6 @@ function insertMarkdownBlock(tr, schema, text, where) {
20
28
  }
21
29
  export {
22
30
  blockInsertPosition,
31
+ blockSideOfSelection,
23
32
  insertMarkdownBlock
24
33
  };
@@ -94,6 +94,83 @@ function mapPos(pos, changes, assoc) {
94
94
  function sorted(changes) {
95
95
  return [...changes].sort((a, b) => a.from - b.from || a.to - b.to);
96
96
  }
97
+ function markSpansOnLine(text, pos, spec) {
98
+ const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
99
+ const nl = text.indexOf("\n", pos);
100
+ const lineEnd = nl === -1 ? text.length : nl;
101
+ const spans = [];
102
+ const open = {};
103
+ let i = lineStart;
104
+ while (i < lineEnd) {
105
+ const ch = text[i];
106
+ if (ch === "`" && !spec.chars.includes("`")) {
107
+ const close = text.indexOf("`", i + 1);
108
+ i = close === -1 || close >= lineEnd ? i + 1 : close + 1;
109
+ continue;
110
+ }
111
+ if (!spec.chars.includes(ch)) {
112
+ i += 1;
113
+ continue;
114
+ }
115
+ const len = runLength(text, i, 1, ch);
116
+ if (spec.runs.includes(len)) {
117
+ const start = open[ch];
118
+ if (start === void 0) {
119
+ open[ch] = i + len;
120
+ } else {
121
+ if (i > start) spans.push({ innerFrom: start, innerTo: i });
122
+ open[ch] = void 0;
123
+ }
124
+ }
125
+ i += len;
126
+ }
127
+ return spans;
128
+ }
129
+ function contentOf(seg, spans, spec) {
130
+ let { from, to } = seg;
131
+ for (const sp of spans) {
132
+ if (from >= sp.innerFrom - spec.size && from < sp.innerFrom) from = sp.innerFrom;
133
+ if (to > sp.innerTo && to <= sp.innerTo + spec.size) to = sp.innerTo;
134
+ }
135
+ return { from, to: Math.max(from, to) };
136
+ }
137
+ function mergeOn(seg, spans, spec, changes) {
138
+ const touched = spans.filter((sp) => sp.innerFrom <= seg.to && sp.innerTo >= seg.from);
139
+ const from = Math.min(seg.from, ...touched.map((sp) => sp.innerFrom));
140
+ const to = Math.max(seg.to, ...touched.map((sp) => sp.innerTo));
141
+ let keepOpen = false;
142
+ let keepClose = false;
143
+ for (const sp of touched) {
144
+ if (sp.innerFrom === from && !keepOpen) keepOpen = true;
145
+ else changes.push({ from: sp.innerFrom - spec.size, to: sp.innerFrom, insert: "" });
146
+ if (sp.innerTo === to && !keepClose) keepClose = true;
147
+ else changes.push({ from: sp.innerTo, to: sp.innerTo + spec.size, insert: "" });
148
+ }
149
+ if (!keepOpen) changes.push({ from, to: from, insert: spec.marker });
150
+ if (!keepClose) changes.push({ from: to, to, insert: spec.marker });
151
+ return { from, to };
152
+ }
153
+ function splitOff(text, seg, sp, spec, changes) {
154
+ if (seg.from <= sp.innerFrom) {
155
+ changes.push({ from: sp.innerFrom - spec.size, to: sp.innerFrom, insert: "" });
156
+ } else {
157
+ let at = seg.from;
158
+ while (at > sp.innerFrom && /\s/.test(text[at - 1] ?? "")) at -= 1;
159
+ if (at === sp.innerFrom) changes.push({ from: sp.innerFrom - spec.size, to: sp.innerFrom, insert: "" });
160
+ else changes.push({ from: at, to: at, insert: spec.marker });
161
+ }
162
+ if (seg.to >= sp.innerTo) {
163
+ changes.push({ from: sp.innerTo, to: sp.innerTo + spec.size, insert: "" });
164
+ } else {
165
+ let at = seg.to;
166
+ while (at < sp.innerTo && /\s/.test(text[at] ?? "")) at += 1;
167
+ if (at === sp.innerTo) changes.push({ from: sp.innerTo, to: sp.innerTo + spec.size, insert: "" });
168
+ else changes.push({ from: at, to: at, insert: spec.marker });
169
+ }
170
+ }
171
+ function spanHolding(seg, spans) {
172
+ return spans.find((sp) => sp.innerFrom <= seg.from && seg.to <= sp.innerTo) ?? null;
173
+ }
97
174
  function inlineCommand(text, from, to, spec) {
98
175
  if (from === to) {
99
176
  const word = wordAt(text, from);
@@ -109,21 +186,43 @@ function inlineCommand(text, from, to, spec) {
109
186
  const segments = inlineSegments(text, from, to);
110
187
  if (segments.length === 0) return { changes: [], anchor: from, head: to };
111
188
  const wraps = segments.map((seg) => wrapOf(text, seg, spec));
112
- const allOn = wraps.every((w) => w !== null);
189
+ if (wraps.every((w) => w !== null)) {
190
+ const changes2 = [];
191
+ wraps.forEach((w) => {
192
+ changes2.push({ from: w.innerFrom - spec.size, to: w.innerFrom, insert: "" });
193
+ changes2.push({ from: w.innerTo, to: w.innerTo + spec.size, insert: "" });
194
+ });
195
+ const ordered2 = sorted(changes2);
196
+ const anchor2 = mapPos(wraps[0].innerFrom, ordered2, 1);
197
+ const head2 = mapPos(wraps[wraps.length - 1].innerTo, ordered2, -1);
198
+ return { changes: ordered2, anchor: anchor2, head: Math.max(anchor2, head2) };
199
+ }
200
+ const lineSpans = segments.map((seg) => markSpansOnLine(text, seg.from, spec));
201
+ const contents = segments.map((seg, i) => contentOf(seg, lineSpans[i], spec));
202
+ const holders = contents.map((c, i) => wraps[i] ? null : spanHolding(c, lineSpans[i]));
203
+ const allOn = wraps.every((w, i) => w !== null || holders[i] !== null);
113
204
  const changes = [];
205
+ let startAt = contents[0].from;
206
+ let endAt = contents[contents.length - 1].to;
114
207
  segments.forEach((seg, i) => {
115
208
  const w = wraps[i];
116
- if (allOn && w) {
117
- changes.push({ from: w.innerFrom - spec.size, to: w.innerFrom, insert: "" });
118
- changes.push({ from: w.innerTo, to: w.innerTo + spec.size, insert: "" });
209
+ if (allOn) {
210
+ if (w) {
211
+ changes.push({ from: w.innerFrom - spec.size, to: w.innerFrom, insert: "" });
212
+ changes.push({ from: w.innerTo, to: w.innerTo + spec.size, insert: "" });
213
+ } else {
214
+ splitOff(text, contents[i], holders[i], spec, changes);
215
+ }
119
216
  } else if (!w) {
120
- changes.push({ from: seg.from, to: seg.from, insert: spec.marker });
121
- changes.push({ from: seg.to, to: seg.to, insert: spec.marker });
217
+ const merged = mergeOn(contents[i], lineSpans[i], spec, changes);
218
+ if (i === 0) startAt = merged.from;
219
+ if (i === segments.length - 1) endAt = merged.to;
220
+ } else {
221
+ if (i === 0) startAt = w.innerFrom;
222
+ if (i === segments.length - 1) endAt = w.innerTo;
122
223
  }
123
224
  });
124
225
  const ordered = sorted(changes);
125
- const startAt = allOn ? wraps[0].innerFrom : segments[0].from;
126
- const endAt = allOn ? wraps[wraps.length - 1].innerTo : segments[segments.length - 1].to;
127
226
  const anchor = mapPos(startAt, ordered, 1);
128
227
  const head = mapPos(endAt, ordered, -1);
129
228
  return { changes: ordered, anchor, head: Math.max(anchor, head) };
@@ -293,7 +392,12 @@ function isFormatActive(text, from, to, command) {
293
392
  const range = a === b ? wordAt(text, a) : { from: a, to: b };
294
393
  if (!range) return false;
295
394
  const segs = inlineSegments(text, range.from, range.to);
296
- return segs.length > 0 && segs.every((s) => wrapOf(text, s, INLINE[command]) !== null);
395
+ const spec = INLINE[command];
396
+ return segs.length > 0 && segs.every((s) => {
397
+ if (wrapOf(text, s, spec) !== null) return true;
398
+ const spans = markSpansOnLine(text, s.from, spec);
399
+ return spanHolding(contentOf(s, spans, spec), spans) !== null;
400
+ });
297
401
  }
298
402
  case "link":
299
403
  return WHOLE_LINK.test(text.slice(a, b)) || text[a - 1] === "[" && text.slice(b, b + 2) === "](";
@@ -453,7 +453,8 @@ function parseProseBlock(block, schema, adjacency, nextId, linkDefinitions) {
453
453
  if (!last || children.length === segmentStart) {
454
454
  return { children: [], lockedReason: "markdown the parser could not map" };
455
455
  }
456
- last.trail += segment.after;
456
+ const kept = keepTrailingSpaces(last, segment.after, state);
457
+ last.trail += segment.after.slice(kept);
457
458
  }
458
459
  children.forEach(
459
460
  (child, index) => adjacency.set(child.id, { trail: child.trail, next: children[index + 1]?.id ?? null })
@@ -494,6 +495,30 @@ function trimSegmentEnd(text, after) {
494
495
  const tail = /[ \t]+$/.exec(text)?.[0] ?? "";
495
496
  return tail ? { text: text.slice(0, text.length - tail.length), after: tail + after } : { text, after };
496
497
  }
498
+ function keepTrailingSpaces(last, after, state) {
499
+ const tail = /^[ \t]+/.exec(after)?.[0] ?? "";
500
+ if (!tail || last.lockedReason !== null || last.trail !== "") return 0;
501
+ const type = last.json.type;
502
+ if (type !== "paragraph" && type !== "heading") return 0;
503
+ const content = [...last.json.content ?? []];
504
+ const end = content[content.length - 1];
505
+ if (end?.type === "text" && !end.marks?.length) {
506
+ content[content.length - 1] = { ...end, text: `${end.text ?? ""}${tail}` };
507
+ } else {
508
+ content.push({ type: "text", text: tail });
509
+ }
510
+ const json = { ...last.json, content };
511
+ try {
512
+ const node = state.schema.nodeFromJSON(json);
513
+ node.check();
514
+ if (serializeBlock(node, createSerializeContext(new Map(state.adjacency))) !== last.raw + tail) return 0;
515
+ } catch {
516
+ return 0;
517
+ }
518
+ last.json = json;
519
+ last.raw += tail;
520
+ return tail.length;
521
+ }
497
522
  function parseTopToken(token, state) {
498
523
  const { schema, islands, adjacency, nextId } = state;
499
524
  const { body, trail } = splitTrail(token.raw);
@@ -11,4 +11,22 @@ export declare function insertTextByRef(textareaRef: HTMLTextAreaElement | null,
11
11
  * surface (textarea, the rich editor's source view, a contentEditable text
12
12
  * node) uses this one rule.
13
13
  */
14
- export declare function blockBoundary(text: string, from: number, to: number, where?: "before" | "after"): number;
14
+ export declare function blockBoundary(text: string, from: number, to: number, where?: BlockPlacement): number;
15
+ /**
16
+ * Where a block goes relative to the caret's block: `before` it, `after` it, or
17
+ * `nearest` — the side the click was closer to.
18
+ */
19
+ export type BlockPlacement = "before" | "after" | "nearest";
20
+ /**
21
+ * 🚨 A BLOCK LANDS ON THE SIDE OF THE CLICK (G11B review, 2026-10-07: every
22
+ * reference and button went AFTER its paragraph, even from a right-click at the
23
+ * start of a line). A matrx fence is block-level, so it can never sit
24
+ * mid-sentence — but a caret at the START of its block, or in its first half,
25
+ * puts the block BEFORE that block; anywhere else (the middle, the second half,
26
+ * the end, an empty block) puts it AFTER. `offset` is the caret's place inside
27
+ * its block, `length` the block's size. ONE rule for the visual editor, the
28
+ * source view and every plain-text field.
29
+ */
30
+ export declare function blockSide(offset: number, length: number): "before" | "after";
31
+ /** `blockSide` for the caret's LINE in plain text (a non-empty selection keeps "after"). */
32
+ export declare function nearestSide(text: string, from: number, to: number): "before" | "after";
@@ -40,6 +40,7 @@ function insertTextByRef(textareaRef, text) {
40
40
  return insertTextAtTextareaCursor(textareaRef, text);
41
41
  }
42
42
  function blockBoundary(text, from, to, where = "after") {
43
+ if (where === "nearest") where = nearestSide(text, from, to);
43
44
  if (where === "after") {
44
45
  const newline2 = text.indexOf("\n", to);
45
46
  return newline2 === -1 ? text.length : newline2;
@@ -48,8 +49,21 @@ function blockBoundary(text, from, to, where = "after") {
48
49
  const newline = text.lastIndexOf("\n", from - 1);
49
50
  return newline === -1 ? 0 : newline + 1;
50
51
  }
52
+ function blockSide(offset, length) {
53
+ if (length <= 0) return "after";
54
+ return offset * 2 < length ? "before" : "after";
55
+ }
56
+ function nearestSide(text, from, to) {
57
+ if (from !== to) return "after";
58
+ const start = from <= 0 ? 0 : text.lastIndexOf("\n", from - 1) + 1;
59
+ const newline = text.indexOf("\n", from);
60
+ const end = newline === -1 ? text.length : newline;
61
+ return blockSide(from - start, end - start);
62
+ }
51
63
  export {
52
64
  blockBoundary,
65
+ blockSide,
53
66
  insertTextAtTextareaCursor,
54
- insertTextByRef
67
+ insertTextByRef,
68
+ nearestSide
55
69
  };
@@ -4,6 +4,7 @@ import type { SourceFeature } from "@ai-matrx/agents/generated/source-attributio
4
4
  import type { ContentSource } from "@ai-matrx/rich-content/rich-document/types";
5
5
  import { type SavePlan } from "../core/save-plan.js";
6
6
  import type { CaretContext } from "../core/caret-context.js";
7
+ import type { BlockPlacement } from "../core/text-insertion.js";
7
8
  import type { DeclaredVariable } from "../core/variables.js";
8
9
  import { type ImagePolicyDeclaration } from "@ai-matrx/rich-content/levels/prose/remote-image-policy";
9
10
  import type { AnnotationRecord } from "@ai-matrx/rich-content/rich-document/host-types";
@@ -20,7 +21,7 @@ export interface RichEditorController {
20
21
  /** Put text at the caret, replacing the selection (markdown stays markdown). */
21
22
  replaceSelection: (text: string) => void;
22
23
  /** Insert a new block before / after the selection; false when it could not. */
23
- insertText: (text: string, where: "before" | "after") => boolean;
24
+ insertText: (text: string, where: BlockPlacement) => boolean;
24
25
  /**
25
26
  * Replace the WHOLE text (the context menu's full-content `onTextReplace`
26
27
  * contract: Cut / Paste / an AI result's Replace / an agent's
@@ -2,10 +2,11 @@
2
2
  * THE context menu's insert-at-the-caret for a host that drives a RichEditor.
3
3
  *
4
4
  * `inline` text replaces the selection where it is. A `block` (a reference
5
- * fence) goes through `insertText(…, "after")`, which places it on its own
6
- * paragraph at the END of the caret's block — never inside a word (G5 review,
7
- * 2026-10-02: "of" became "o" + block + "f"). One helper so the three note
8
- * hosts (desktop, content editor, mobile) cannot drift.
5
+ * fence) goes through `insertText(…, "nearest")`: its own paragraph, never
6
+ * inside a word (G5 review, 2026-10-02: "of" became "o" + block + "f"), on the
7
+ * side of the caret's block the click was nearer to — BEFORE it from its start
8
+ * or first half, AFTER it otherwise (G11B review, 2026-10-07). One helper so
9
+ * the three note hosts (desktop, content editor, mobile) cannot drift.
9
10
  */
10
11
  import type { RichEditorController } from "./RichEditorImpl.js";
11
12
  export declare function insertAtRichCaret(rich: RichEditorController | null, text: string, placement?: "inline" | "block"): boolean;
@@ -1,6 +1,6 @@
1
1
  function insertAtRichCaret(rich, text, placement = "inline") {
2
2
  if (!rich) return false;
3
- if (placement === "block") return rich.insertText(text, "after");
3
+ if (placement === "block") return rich.insertText(text, "nearest");
4
4
  rich.replaceSelection(text);
5
5
  return true;
6
6
  }
@@ -44,7 +44,7 @@ import {
44
44
  setFindHighlights,
45
45
  setIslandRendering
46
46
  } from "./live-preview.js";
47
- import { blockBoundary } from "../core/text-insertion.js";
47
+ import { blockBoundary, nearestSide } from "../core/text-insertion.js";
48
48
  const RAW_THEME = EditorView.theme({
49
49
  ".cm-content, .cm-line": {
50
50
  fontFamily: "var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)",
@@ -300,8 +300,10 @@ function SourceEditor({
300
300
  const v = view.current;
301
301
  if (!v) return false;
302
302
  const { from, to } = v.state.selection.main;
303
- const at = blockBoundary(v.state.doc.toString(), from, to, where);
304
- const insert = where === "before" ? `${text}
303
+ const doc = v.state.doc.toString();
304
+ const side = where === "nearest" ? nearestSide(doc, from, to) : where;
305
+ const at = blockBoundary(doc, from, to, side);
306
+ const insert = side === "before" ? `${text}
305
307
 
306
308
  ` : `
307
309
 
@@ -1,4 +1,5 @@
1
1
  import { type Ref } from "react";
2
+ import type { BlockPlacement } from "../core/text-insertion.js";
2
3
  import { type VisualLoadStats } from "../core/visual-document.js";
3
4
  import { type CaretCarry, type CaretContext } from "../core/caret-context.js";
4
5
  import type { FindOptions } from "../core/find-replace.js";
@@ -19,7 +20,7 @@ export interface EditorViewHandle {
19
20
  /** Replace the selection with literal text (markdown typed by an AI stays markdown). */
20
21
  replaceSelection: (text: string) => void;
21
22
  /** Insert a block before / after the selection; false when it could not. */
22
- insertText: (text: string, where: "before" | "after") => boolean;
23
+ insertText: (text: string, where: BlockPlacement) => boolean;
23
24
  /** Flush pending text now (before a save or a view switch). */
24
25
  flush: () => string;
25
26
  /**
@@ -61,7 +61,7 @@ function IslandBlockView({ node, updateAttributes, deleteNode, selected, editor,
61
61
  NodeViewWrapper,
62
62
  {
63
63
  className: cn(
64
- "rich-editor-island rich-editor-reference group/island relative my-1 flex min-w-0 items-start gap-1 rounded-md transition-colors",
64
+ "rich-editor-island rich-editor-reference group/island relative my-1 flex min-w-0 flex-wrap items-center gap-1 rounded-md transition-colors sm:flex-nowrap sm:items-start",
65
65
  selected && "ring-1 ring-primary/40"
66
66
  ),
67
67
  "data-island-type": "matrx",
@@ -71,13 +71,13 @@ function IslandBlockView({ node, updateAttributes, deleteNode, selected, editor,
71
71
  "span",
72
72
  {
73
73
  "data-drag-handle": true,
74
- className: "mt-1.5 shrink-0 cursor-grab touch-none text-muted-foreground/60 opacity-100 hover:text-foreground sm:opacity-0 sm:group-hover/island:opacity-100",
74
+ className: "shrink-0 cursor-grab touch-none text-muted-foreground/60 opacity-100 hover:text-foreground sm:mt-1.5 sm:opacity-0 sm:group-hover/island:opacity-100",
75
75
  "aria-hidden": true,
76
76
  children: /* @__PURE__ */ jsx(GripVertical, { className: "h-3.5 w-3.5" })
77
77
  }
78
78
  ),
79
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(IslandPreview, { raw, islandType }) }),
80
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-0.5 text-muted-foreground opacity-100 sm:opacity-0 sm:transition-opacity sm:group-hover/island:opacity-100 sm:group-focus-within/island:opacity-100", children: [
79
+ /* @__PURE__ */ jsx("div", { className: "order-first min-w-0 basis-full sm:order-none sm:basis-0 sm:flex-1", "data-island-body": "", children: /* @__PURE__ */ jsx(IslandPreview, { raw, islandType }) }),
80
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-0.5 text-muted-foreground opacity-100 sm:ml-0 sm:opacity-0 sm:transition-opacity sm:group-hover/island:opacity-100 sm:group-focus-within/island:opacity-100", children: [
81
81
  /* @__PURE__ */ jsx("button", { type: "button", className: "rounded p-1 hover:bg-muted hover:text-foreground pointer-coarse:flex pointer-coarse:h-11 pointer-coarse:w-11 pointer-coarse:items-center pointer-coarse:justify-center", onClick: copy, title: `Copy this ${meta.label.toLowerCase()}`, children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" }) }),
82
82
  !readOnly && /* @__PURE__ */ jsx(
83
83
  "button",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-editor",
3
- "version": "0.2.10",
3
+ "version": "0.3.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,11 +90,11 @@
90
90
  "@tiptap/suggestion": "^3.31.3",
91
91
  "github-slugger": "^2.0.0",
92
92
  "lucide-react": "^1.48.0",
93
+ "@ai-matrx/agents": "latest",
94
+ "@ai-matrx/alchemy": "latest",
93
95
  "@ai-matrx/content-ir": "latest",
94
96
  "@ai-matrx/design-system": "latest",
95
- "@ai-matrx/alchemy": "latest",
96
97
  "@ai-matrx/kit": "latest",
97
- "@ai-matrx/agents": "latest",
98
98
  "@ai-matrx/print": "latest",
99
99
  "@ai-matrx/rich-content": "latest"
100
100
  },