@ai-matrx/rich-editor 0.4.1 → 0.4.3

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.3
4
+ ### `chrome="format"`: the toolbar for hosts with no row of their own (2026-10-08)
5
+
6
+ - `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).
7
+
8
+ Consumer action: a `chrome="bare"` host with no toolbar row of its own switches to `chrome="format"`.
9
+
10
+ ## 0.4.2
11
+ ### A short toolbar keeps what people reach for (2026-10-08)
12
+
13
+ - `FormatButtons` folds by priority, not position: Bold, Italic, Text style, Bulleted list, Checklist and Link stay in view longest; history, inserts and the rest fold into More first. Display order never changes; dividers sit only between visible groups. The text-style menu is narrower (88px). Live: notes beside the chat panel showed only Undo, Redo, Text style and More.
14
+
15
+ Consumer action: none.
16
+
3
17
  ## 0.4.1
4
18
  ### Edit in place: Write, Split, Source, Preview (2026-10-08)
5
19
 
@@ -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 === "bare" ? false : null)
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);
@@ -538,6 +538,7 @@ function RichEditorImpl({
538
538
  saveButton
539
539
  ] }),
540
540
  /* @__PURE__ */ jsxs("div", { className: "relative z-20 shrink-0", children: [
541
+ !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
542
  !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
543
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto", children: [
543
544
  isMobile ? (
@@ -54,15 +54,41 @@ const FORMAT_GROUPS = [
54
54
  ];
55
55
  const FORMAT_BUTTONS = FORMAT_GROUPS.flat();
56
56
  const ESSENTIAL_COMMANDS = ["bold", "italic", "bulletList", "taskList", "link"];
57
+ const KEEP = {
58
+ bold: 20,
59
+ italic: 19,
60
+ style: 18,
61
+ bulletList: 17,
62
+ taskList: 16,
63
+ link: 15,
64
+ orderedList: 14,
65
+ undo: 13,
66
+ redo: 12,
67
+ strike: 11,
68
+ code: 10,
69
+ quote: 9,
70
+ codeBlock: 8,
71
+ table: 7,
72
+ horizontalRule: 6
73
+ };
74
+ const keepOf = (slot) => slot.kind === "button" ? KEEP[slot.spec.command] ?? 0 : slot.kind === "style" ? KEEP.style : 0;
57
75
  function slotsOf(variant) {
58
76
  if (variant === "essential") {
59
77
  return [{ kind: "style" }, ...ESSENTIAL_COMMANDS.map((command) => ({ kind: "button", spec: FORMAT_BUTTONS.find((b) => b.command === command) }))];
60
78
  }
61
79
  const out = [];
62
80
  FORMAT_GROUPS.forEach((group, index) => {
63
- if (index > 0) out.push({ kind: "divider" });
64
- out.push(...group.map((b) => ({ kind: "button", spec: b })));
65
- if (index === 0) out.push({ kind: "divider" }, { kind: "style" });
81
+ const g = index === 0 ? 0 : index + 1;
82
+ out.push(...group.map((b) => ({ kind: "button", spec: b, group: g })));
83
+ if (index === 0) out.push({ kind: "style", group: 1 });
84
+ });
85
+ return out;
86
+ }
87
+ function withDividers(slots) {
88
+ const out = [];
89
+ slots.forEach((slot, i) => {
90
+ if (i > 0 && slot.group !== void 0 && slot.group !== slots[i - 1].group) out.push({ kind: "divider" });
91
+ out.push(slot);
66
92
  });
67
93
  return out;
68
94
  }
@@ -123,27 +149,23 @@ function FormatButtons({ resolve, disabled = false, className, size = "sm", vari
123
149
  resolve()?.run(command);
124
150
  };
125
151
  const button = size === "xs" ? 26 : 30;
126
- const styleWidth = variant === "essential" ? size === "xs" ? 40 : 44 : 102;
152
+ const styleWidth = variant === "essential" ? size === "xs" ? 40 : 44 : 90;
127
153
  const widthOf = (slot) => slot.kind === "button" ? button : slot.kind === "style" ? styleWidth : 11;
128
154
  const all = slotsOf(variant);
129
- let shown = all;
155
+ let kept = all;
130
156
  let folded = variant === "essential" ? slotsOf("full").filter((s) => s.kind === "button" && !ESSENTIAL_COMMANDS.includes(s.spec.command)) : [];
131
- if (width > 0) {
132
- const total = all.reduce((sum, s) => sum + widthOf(s), 0) + (folded.length ? button : 0);
133
- if (total > width + 4) {
134
- const budget = width - button;
135
- let used = 0;
136
- let count = 0;
137
- for (const slot of all) {
138
- if (used + widthOf(slot) > budget) break;
139
- used += widthOf(slot);
140
- count += 1;
141
- }
142
- shown = all.slice(0, count);
143
- folded = [...all.slice(count), ...folded];
157
+ const rowWidth = (slots, more) => withDividers(slots).reduce((sum, s) => sum + widthOf(s), 0) + (more ? button : 0);
158
+ if (width > 0 && rowWidth(kept, folded.length > 0) > width + 4) {
159
+ const byKeep = [...all].sort((x, y) => keepOf(x) - keepOf(y));
160
+ const dropped = /* @__PURE__ */ new Set();
161
+ for (const slot of byKeep) {
162
+ if (rowWidth(all.filter((s) => !dropped.has(s)), true) <= width + 4) break;
163
+ dropped.add(slot);
144
164
  }
165
+ kept = all.filter((s) => !dropped.has(s));
166
+ folded = [...all.filter((s) => dropped.has(s)), ...folded];
145
167
  }
146
- while (shown.length && shown[shown.length - 1].kind === "divider") shown = shown.slice(0, -1);
168
+ const shown = withDividers(kept);
147
169
  const foldedButtons = folded.filter((s) => s.kind === "button");
148
170
  const styleFolded = folded.some((s) => s.kind === "style");
149
171
  const iconClass = size === "xs" ? "h-3.5 w-3.5" : "h-4 w-4";
@@ -178,7 +200,7 @@ function FormatButtons({ resolve, disabled = false, className, size = "sm", vari
178
200
  className: cn(
179
201
  "inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground",
180
202
  size === "xs" ? "h-6" : "h-7",
181
- variant === "full" && "w-[100px] justify-between"
203
+ variant === "full" && "w-[88px] justify-between"
182
204
  ),
183
205
  children: [
184
206
  variant === "essential" ? /* @__PURE__ */ jsx(StyleIcon, { className: iconClass }) : /* @__PURE__ */ jsx("span", { className: "truncate", children: BLOCK_SHORT[currentStyle.command] }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-editor",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",
@@ -93,9 +93,9 @@
93
93
  "@ai-matrx/agents": "latest",
94
94
  "@ai-matrx/alchemy": "latest",
95
95
  "@ai-matrx/content-ir": "latest",
96
+ "@ai-matrx/design-system": "latest",
96
97
  "@ai-matrx/kit": "latest",
97
98
  "@ai-matrx/print": "latest",
98
- "@ai-matrx/design-system": "latest",
99
99
  "@ai-matrx/rich-content": "latest"
100
100
  },
101
101
  "peerDependencies": {