@ai-matrx/rich-editor 0.4.0 → 0.4.1

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+ ### Edit in place: Write, Split, Source, Preview (2026-10-08)
5
+
6
+ - Every edit-in-place editor (an AI answer in chat, window panels and the side drawer) offers Write (the visual editor, default), **Split** (the raw markdown editable on the left, the full live render on the right; stacked on a phone), Source and Preview — in the editor's own view switch, never behind a menu on desktop.
7
+ - `RichEditor` gains `views` (the views offered, in order) and `viewLabels` (a host's own word for a view). Hosts that pass neither are unchanged.
8
+ - The formatting toolbar works in Split's raw pane like in Source.
9
+
3
10
  ## 0.4.0
4
11
  ### ONE formatting toolbar in every editor and every mode (2026-10-07)
5
12
 
@@ -8,7 +8,9 @@ import type { BlockPlacement } from "../core/text-insertion.js";
8
8
  import type { DeclaredVariable } from "../core/variables.js";
9
9
  import { type ImagePolicyDeclaration } from "@ai-matrx/rich-content/levels/prose/remote-image-policy";
10
10
  import type { AnnotationRecord } from "@ai-matrx/rich-content/rich-document/host-types";
11
- export type RichEditorView = "visual" | "source" | "preview";
11
+ export type RichEditorView = "visual" | "split" | "source" | "preview";
12
+ /** The views a host gets unless it names its own (`views`). Split is offered where a host asks for it. */
13
+ export declare const DEFAULT_VIEWS: readonly RichEditorView[];
12
14
  /**
13
15
  * What a host that carries its OWN chrome (view switch, find, outline, dictation,
14
16
  * its own right-click menu) drives the editor through. Every verb acts on the
@@ -72,6 +74,14 @@ export interface RichEditorProps {
72
74
  */
73
75
  view?: RichEditorView;
74
76
  onViewChange?: (view: RichEditorView) => void;
77
+ /**
78
+ * The views the switch offers, in order (default Visual, Source, Preview).
79
+ * Split — the raw text editable on the left, the full render live on the
80
+ * right — is offered by naming it here (edit-in-place: Write, Split, Source, Preview).
81
+ */
82
+ views?: readonly RichEditorView[];
83
+ /** A host's own word for a view ("Write" for Visual in edit-in-place). */
84
+ viewLabels?: Partial<Record<RichEditorView, string>>;
75
85
  /**
76
86
  * "full" (default): the editor's toolbar row — views, tools, dictation, Save.
77
87
  * "bare": no row of its own; the host's chrome carries the view switch, find,
@@ -135,4 +145,4 @@ export interface RichEditorProps {
135
145
  */
136
146
  imagePolicy?: ImagePolicyDeclaration;
137
147
  }
138
- export default function RichEditorImpl({ value, onSave, onChange, variables, onDeclareVariable, defaultView, view: controlledView, onViewChange, chrome, hostContextMenu, controllerRef, readOnly, placeholder, surfaceName, sourceFeature, contentSource, annotationRecord, saveLabel, className, toolbarExtras, onCancel, cancelLabel, defaultOutlineOpen, onNothingToSave, imagePolicy, }: RichEditorProps): import("react").JSX.Element;
148
+ export default function RichEditorImpl({ value, onSave, onChange, variables, onDeclareVariable, defaultView, view: controlledView, onViewChange, views: viewsProp, viewLabels, chrome, hostContextMenu, controllerRef, readOnly, placeholder, surfaceName, sourceFeature, contentSource, annotationRecord, saveLabel, className, toolbarExtras, onCancel, cancelLabel, defaultOutlineOpen, onNothingToSave, imagePolicy, }: RichEditorProps): import("react").JSX.Element;
@@ -7,6 +7,7 @@ import {
7
7
  Check,
8
8
  ChevronDown,
9
9
  Code2,
10
+ Columns2,
10
11
  Eye,
11
12
  Focus,
12
13
  Hash,
@@ -50,6 +51,7 @@ import { RichEditorContext } from "./RichEditorContext.js";
50
51
  import { VisualEditor } from "../visual/VisualEditor.js";
51
52
  import { SourceEditor } from "../source/SourceEditor.js";
52
53
  import { FormatButtons } from "../format/FormatButtons.js";
54
+ import { SplitPanes } from "./SplitPanes.js";
53
55
  import { FindReplacePanel } from "../panels/FindReplacePanel.js";
54
56
  import { OutlinePanel } from "../panels/OutlinePanel.js";
55
57
  import { ShortcutsDialog } from "../panels/ShortcutsDialog.js";
@@ -59,7 +61,7 @@ import { withImagePolicy } from "@ai-matrx/rich-content/levels/prose/remote-imag
59
61
  import { RecordAnnotations } from "@ai-matrx/rich-content/host/app-bindings";
60
62
  import { annotationRecordOf } from "@ai-matrx/rich-content/host/app-bindings";
61
63
  import { formatCount } from "@ai-matrx/kit/format";
62
- const VIEWS = ["visual", "source", "preview"];
64
+ const DEFAULT_VIEWS = ["visual", "source", "preview"];
63
65
  function describeDelta(delta) {
64
66
  const raw = delta.before ?? delta.after ?? "";
65
67
  const block = islandMeta(delta.islandType, raw);
@@ -79,6 +81,7 @@ function describeDelta(delta) {
79
81
  }
80
82
  const VIEW_META = {
81
83
  visual: { label: "Visual", icon: Type },
84
+ split: { label: "Split", icon: Columns2 },
82
85
  source: { label: "Source", icon: Code2 },
83
86
  preview: { label: "Preview", icon: Eye }
84
87
  };
@@ -91,6 +94,8 @@ function RichEditorImpl({
91
94
  defaultView = "visual",
92
95
  view: controlledView,
93
96
  onViewChange,
97
+ views: viewsProp,
98
+ viewLabels,
94
99
  chrome = "full",
95
100
  hostContextMenu = false,
96
101
  controllerRef,
@@ -110,6 +115,8 @@ function RichEditorImpl({
110
115
  imagePolicy
111
116
  }) {
112
117
  const isMobile = useIsMobile();
118
+ const VIEWS = viewsProp?.length ? viewsProp : DEFAULT_VIEWS;
119
+ const viewLabel = (option) => viewLabels?.[option] ?? VIEW_META[option].label;
113
120
  const [stored, setStored] = useState(value);
114
121
  const [current, setCurrent] = useState(value);
115
122
  const [view, setView] = useState(controlledView ?? defaultView);
@@ -404,18 +411,19 @@ function RichEditorImpl({
404
411
  },
405
412
  `source-${mountKey}`
406
413
  );
407
- const body = view === "preview" ? /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto", children: /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl px-6 py-6 sm:px-10", children: /* @__PURE__ */ jsx(
414
+ const rendered = (content) => /* @__PURE__ */ jsx(
408
415
  RichDocument,
409
416
  {
410
417
  imagePolicy: "inherit",
411
- content: current,
418
+ content,
412
419
  source: contentSource,
413
420
  enableContextMenu: true,
414
421
  hideCopyButton: true,
415
422
  allowFullScreenEditor: false,
416
423
  actions: { exclude: ["edit", "delete-message"] }
417
424
  }
418
- ) }) }) : hostContextMenu ? /* @__PURE__ */ jsx("div", { className: "h-full min-h-0", children: viewEditor }) : /* @__PURE__ */ jsx(
425
+ );
426
+ const editingBody = hostContextMenu ? /* @__PURE__ */ jsx("div", { className: "h-full min-h-0", children: viewEditor }) : /* @__PURE__ */ jsx(
419
427
  EditableContextMenu,
420
428
  {
421
429
  sourceFeature,
@@ -433,6 +441,10 @@ function RichEditorImpl({
433
441
  children: /* @__PURE__ */ jsx("div", { className: "h-full min-h-0", children: viewEditor })
434
442
  }
435
443
  );
444
+ const body = view === "preview" ? /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto", children: /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-3xl px-6 py-6 sm:px-10", children: rendered(current) }) }) : view === "split" ? (
445
+ // Split: the raw text (Source, raw) on the left, the live render of the same text on the right.
446
+ /* @__PURE__ */ jsx(SplitPanes, { stacked: isMobile, raw: editingBody, render: rendered(deferred) })
447
+ ) : editingBody;
436
448
  const CurrentViewIcon = VIEW_META[view].icon;
437
449
  const tools = [
438
450
  { id: "find", label: "Find & replace (\u2318F)", icon: Search, active: findMode !== null, disabled: view === "preview", run: () => setFindMode((mode) => mode ? null : "find") },
@@ -535,7 +547,7 @@ function RichEditorImpl({
535
547
  "button",
536
548
  {
537
549
  type: "button",
538
- "aria-label": `View: ${VIEW_META[view].label}`,
550
+ "aria-label": `View: ${viewLabel(view)}`,
539
551
  title: "Switch view",
540
552
  className: "flex h-8 shrink-0 items-center gap-1 rounded-md border border-border bg-background/60 px-2 text-xs font-medium text-primary",
541
553
  children: [
@@ -548,7 +560,7 @@ function RichEditorImpl({
548
560
  const Icon = VIEW_META[option].icon;
549
561
  return /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: () => switchView(option), children: [
550
562
  /* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4" }),
551
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: VIEW_META[option].label }),
563
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: viewLabel(option) }),
552
564
  view === option && /* @__PURE__ */ jsx(Check, { className: "ml-2 h-4 w-4 text-primary" })
553
565
  ] }, option);
554
566
  }) })
@@ -560,7 +572,7 @@ function RichEditorImpl({
560
572
  {
561
573
  type: "button",
562
574
  role: "tab",
563
- title: VIEW_META[option].label,
575
+ title: viewLabel(option),
564
576
  "aria-selected": view === option,
565
577
  onClick: () => switchView(option),
566
578
  className: cn(
@@ -569,7 +581,7 @@ function RichEditorImpl({
569
581
  ),
570
582
  children: [
571
583
  /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
572
- VIEW_META[option].label
584
+ viewLabel(option)
573
585
  ]
574
586
  },
575
587
  option
@@ -744,5 +756,6 @@ function ToolbarButton({
744
756
  );
745
757
  }
746
758
  export {
759
+ DEFAULT_VIEWS,
747
760
  RichEditorImpl as default
748
761
  };
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+ export interface SplitPanesProps {
3
+ /** The editable raw text (the Source engine). */
4
+ raw: ReactNode;
5
+ /** The live render of the same text. */
6
+ render: ReactNode;
7
+ /** Stack top/bottom instead of side by side (phones). */
8
+ stacked?: boolean;
9
+ }
10
+ export declare function SplitPanes({ raw, render, stacked }: SplitPanesProps): import("react").JSX.Element;
11
+ export default SplitPanes;
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "@ai-matrx/design-system";
4
+ function SplitPanes({ raw, render, stacked = false }) {
5
+ return /* @__PURE__ */ jsxs(
6
+ "div",
7
+ {
8
+ "data-split-view": "",
9
+ className: cn(
10
+ "grid h-full min-h-0",
11
+ stacked ? "grid-cols-1 grid-rows-2 divide-y divide-border" : "grid-cols-2 divide-x divide-border"
12
+ ),
13
+ children: [
14
+ /* @__PURE__ */ jsx("section", { "aria-label": "Raw text", "data-split-pane": "raw", className: "min-h-0 min-w-0 overflow-hidden", children: raw }),
15
+ /* @__PURE__ */ jsx("section", { "aria-label": "Rendered", "data-split-pane": "render", className: "min-h-0 min-w-0 overflow-y-auto", children: /* @__PURE__ */ jsx("div", { className: "px-4 py-4 sm:px-6", children: render }) })
16
+ ]
17
+ }
18
+ );
19
+ }
20
+ var SplitPanes_default = SplitPanes;
21
+ export {
22
+ SplitPanes,
23
+ SplitPanes_default as default
24
+ };
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import { type RichEditorProps } from "../editor/RichEditor.js";
2
+ import { type RichEditorProps, type RichEditorView } from "../editor/RichEditor.js";
3
3
  import type { CaretContext } from "../core/caret-context.js";
4
4
  import { type InPlaceSaveMode } from "./in-place-session.js";
5
5
  export type InPlaceEditorPassThrough = Omit<RichEditorProps, "value" | "onSave" | "onChange" | "onCancel" | "onNothingToSave" | "cancelLabel" | "controllerRef">;
@@ -33,6 +33,16 @@ export interface InPlaceEditorProps {
33
33
  */
34
34
  actions?: readonly InPlaceAction[];
35
35
  }
36
+ /**
37
+ * The modes of every edit-in-place editor, in this order (Arman, 2026-10-08:
38
+ * "Default: Write. Next option: Split (plain text on the left and full render
39
+ * on the right). Then, whatever else we need. But you cannot gobble them up and
40
+ * hide the things we need."). Write is the visual editor and always opens
41
+ * first (no per-person memory exists for this); Split shows the raw string
42
+ * next to its render. All four sit in the editor's own toolbar row.
43
+ */
44
+ export declare const IN_PLACE_VIEWS: readonly RichEditorView[];
45
+ export declare const IN_PLACE_VIEW_LABELS: Partial<Record<RichEditorView, string>>;
36
46
  export interface InPlaceAction {
37
47
  id: string;
38
48
  label: string;
@@ -8,6 +8,8 @@ import { cn } from "@ai-matrx/design-system";
8
8
  import { ConfirmDialog } from "@ai-matrx/rich-content/host/app-bindings";
9
9
  import RichEditor from "../editor/RichEditor.js";
10
10
  import { createInPlaceSession } from "./in-place-session.js";
11
+ const IN_PLACE_VIEWS = ["visual", "split", "source", "preview"];
12
+ const IN_PLACE_VIEW_LABELS = { visual: "Write" };
11
13
  function InPlaceEditor({
12
14
  value,
13
15
  mode = "explicit",
@@ -129,6 +131,8 @@ function InPlaceEditor({
129
131
  {
130
132
  defaultView: "visual",
131
133
  defaultOutlineOpen: false,
134
+ views: IN_PLACE_VIEWS,
135
+ viewLabels: IN_PLACE_VIEW_LABELS,
132
136
  ...editor,
133
137
  controllerRef: controller,
134
138
  value: openedOn,
@@ -206,6 +210,8 @@ function keepCaretInView(root, settleMs = 2500) {
206
210
  return stop;
207
211
  }
208
212
  export {
213
+ IN_PLACE_VIEWS,
214
+ IN_PLACE_VIEW_LABELS,
209
215
  InPlaceEditor,
210
216
  keepCaretInView
211
217
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-editor",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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,12 +90,12 @@
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",
93
94
  "@ai-matrx/alchemy": "latest",
94
95
  "@ai-matrx/content-ir": "latest",
95
- "@ai-matrx/design-system": "latest",
96
96
  "@ai-matrx/kit": "latest",
97
97
  "@ai-matrx/print": "latest",
98
- "@ai-matrx/agents": "latest",
98
+ "@ai-matrx/design-system": "latest",
99
99
  "@ai-matrx/rich-content": "latest"
100
100
  },
101
101
  "peerDependencies": {