@ai-matrx/rich-content 0.2.57 → 0.2.60

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,7 +1,29 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.57
4
- ### parse5 and the YAML parser leave the first screen
3
+ ## 0.2.60
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/rich-content/v0.2.59..npm/rich-content/v0.2.60 -- apps/shared/rich-content`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.2.59
10
+
11
+ - The content action set, revised (Arman, 2026-10-08): the bar is ONE Copy icon (`copy/CopyMenuButton`, no chevron — one click copies the raw contents, then Copy raw · Copy formatted · Copy for AI (with context)) and the Raw/Formatted toggle. Export, Print and Transform are no longer bar icons: the set's own "…" carries Alchemy… (the palette), the five downloads and Print; `menu={false}` where the host's ⋯ already does. Every icon is the bar's strip box (28px, `h-4` glyph, muted colour).
12
+ - The rich-document bar renders Copy + Raw/Formatted only; its ⋯ "Export…" row is now "Alchemy…" and always shows (it is the palette's one door).
13
+
14
+ Consumer action: drop any `hasContentActions` gate that hid ⋯ download/print rows beside the set.
15
+
16
+ ## 0.2.58 — one message print
17
+
18
+ - New `copy/print-message` → `printMessage(markdown, { title, skin, onNotice })`: THE message print —
19
+ prose plus every embedded block (fence or `<artifact>`) through `@ai-matrx/print`'s block-printer
20
+ registry (`toPrintHtml`), mermaid drawn once, "web pages print as a picture" announced.
21
+ - `printContent` (the content actions' Print) uses it: an HTML page in a message no longer prints as
22
+ its source code.
23
+
24
+ Consumer action: none (hosts register block printers with `registerBlockPrinter`).
25
+
26
+ ## Unreleased — parse5 and the YAML parser leave the first screen
5
27
 
6
28
  - The raw-HTML tag allow-list lives alone in `./display/chat-markdown/raw-html-tags`
7
29
  (`rehypeSafeRawHtml` re-exports it). The content splitter, table vectors and prose prep read only the
@@ -14,7 +36,7 @@
14
36
  Consumer action: none (a direct caller of `extractFrontmatter` that needs the parsed properties at
15
37
  once awaits `loadFrontmatterParsers()` first).
16
38
 
17
- ### tap-target comes from design-system
39
+ ## Unreleased — tap-target comes from design-system
18
40
 
19
41
  - Imports `@ai-matrx/design-system/tap-target` (and `/tap-target/buttons`, `tap-target.css`) instead of the
20
42
  retired `@ai-matrx/tap-target` (Arman, 2026-10-08: tap-target merges into design-system); the
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { type SplitCopyFlavor } from "./CopySplitButton.js";
2
+ import type { SplitCopyFlavor } from "./CopySplitButton.js";
3
3
  import { type ContentExportFormat } from "./content-export.js";
4
4
  export interface ContentActionsProps {
5
5
  /** The item's markdown source (a function reads it at click time). */
@@ -15,8 +15,15 @@ export interface ContentActionsProps {
15
15
  * leaving it out.
16
16
  */
17
17
  viewKey?: string;
18
- /** Opens the Alchemy palette for this item. Absent = no Transform icon. */
18
+ /** Opens the Alchemy palette for this item (the "Alchemy…" row in "…"). */
19
19
  onTransform?: () => void;
20
+ /** The AI copy (the host's envelope). Default: the content with where it came from. */
21
+ copyForAi?: () => Promise<string> | string;
22
+ /**
23
+ * Render the set's own "…" (Alchemy, the five exports, Print). False where the host's bar has a ⋯
24
+ * that already carries them (the rich-document bar).
25
+ */
26
+ menu?: boolean;
20
27
  /** Per-format overrides (a registry's own download rows). */
21
28
  exporters?: Partial<Record<ContentExportFormat, () => unknown>>;
22
29
  /** Print override (a registry's own print row). */
@@ -29,7 +36,7 @@ export interface ContentActionsProps {
29
36
  disabled?: boolean;
30
37
  className?: string;
31
38
  }
32
- export declare function ContentActions({ content, title, copy, viewKey, onTransform, exporters, onPrint, label, size, preserveSelection, disabled, className, }: ContentActionsProps): React.ReactElement;
39
+ export declare function ContentActions({ content, title, copy, copyForAi, viewKey, onTransform, exporters, onPrint, menu, label, size, preserveSelection, disabled, className, }: ContentActionsProps): React.ReactElement;
33
40
  export interface ContentActionMenuRowsProps extends Pick<ContentActionsProps, "content" | "title" | "viewKey" | "onTransform" | "exporters" | "onPrint"> {
34
41
  /** Close the host's menu / sheet (called before the action runs). */
35
42
  onDone?: () => void;
@@ -37,7 +44,8 @@ export interface ContentActionMenuRowsProps extends Pick<ContentActionsProps, "c
37
44
  rowClassName?: string;
38
45
  }
39
46
  /**
40
- * The set's rows — Plain, PDF · Word · HTML · Markdown file · Text file, Print, Transform — each ONE tap.
47
+ * The set's "…" rows — Alchemy…, Plain (only where the host passes `viewKey`: a ⋯ sheet with no bar
48
+ * toggle), PDF · Word · HTML · Markdown file · Text file, Print — each ONE tap.
41
49
  * The set's own phone "…" renders them; a host whose phone bar already has a ⋯ sheet (the note dock)
42
50
  * renders them there instead of a second "…".
43
51
  */
@@ -2,132 +2,68 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { BookOpen, Code2, Download, MoreHorizontal, Printer, WandSparkles } from "lucide-react";
5
+ import { CopyMenuButton, contentIconButton, contentIconSize } from "./CopyMenuButton.js";
5
6
  import { Popover, PopoverContent, PopoverTrigger, cn } from "@ai-matrx/design-system";
6
- import { CopySplitButton } from "./CopySplitButton.js";
7
- import { copyRichContent } from "./copy-commands.js";
8
7
  import { CONTENT_EXPORT_FORMATS, exportContent, printContent } from "./content-export.js";
9
8
  import { markContentActions, togglePlainView, usePlainView } from "./content-view-store.js";
10
- const iconButton = (size) => cn(
11
- "inline-flex shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground pointer-coarse:h-11 pointer-coarse:min-w-11",
12
- size === "xs" ? "h-7 min-w-7" : "h-8 min-w-8"
13
- );
14
9
  const menuRow = "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent";
15
10
  function ContentActions({
16
11
  content,
17
12
  title,
18
13
  copy,
14
+ copyForAi,
19
15
  viewKey,
20
16
  onTransform,
21
17
  exporters,
22
18
  onPrint,
23
- label = "Copy",
19
+ menu = true,
20
+ label = "content",
24
21
  size = "sm",
25
22
  preserveSelection = false,
26
23
  disabled = false,
27
24
  className
28
25
  }) {
29
26
  const read = React.useCallback(() => typeof content === "function" ? content() : content, [content]);
30
- const runCopy = React.useCallback(
31
- (flavor) => copy ? copy(flavor) : copyRichContent(read(), flavor),
32
- [copy, read]
33
- );
34
27
  const plain = usePlainView(viewKey);
35
28
  React.useEffect(() => viewKey ? markContentActions(viewKey) : void 0, [viewKey]);
36
- const [exportOpen, setExportOpen] = React.useState(false);
37
29
  const [moreOpen, setMoreOpen] = React.useState(false);
38
30
  const keep = preserveSelection ? (event) => event.preventDefault() : void 0;
39
- const iconSize = size === "xs" ? "h-3.5 w-3.5" : "h-4 w-4";
40
- const runExport = (format) => {
41
- const own = exporters?.[format];
42
- void (own ? own() : exportContent(read(), format, title));
43
- };
44
- const runPrint = () => void (onPrint ? onPrint() : printContent(read(), title));
45
31
  const plainName = plain ? "Show formatted" : "Show source text";
46
32
  const PlainIcon = plain ? BookOpen : Code2;
47
- const exportRows = (close) => CONTENT_EXPORT_FORMATS.map(({ format, label: rowLabel }) => /* @__PURE__ */ jsxs(
48
- "button",
49
- {
50
- type: "button",
51
- role: "menuitem",
52
- "data-content-export": format,
53
- onMouseDown: keep,
54
- onClick: () => {
55
- close();
56
- runExport(format);
57
- },
58
- className: menuRow,
59
- children: [
60
- /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
61
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: rowLabel })
62
- ]
63
- },
64
- format
65
- ));
66
33
  return /* @__PURE__ */ jsxs(
67
34
  "div",
68
35
  {
69
36
  "data-content-actions": "",
70
37
  className: cn("inline-flex shrink-0 items-center gap-0.5", disabled && "pointer-events-none opacity-50", className),
71
38
  children: [
72
- /* @__PURE__ */ jsx(CopySplitButton, { copy: runCopy, label, size, preserveSelection, disabled }),
73
- /* @__PURE__ */ jsxs("div", { className: "hidden items-center gap-0.5 sm:inline-flex", children: [
74
- viewKey ? /* @__PURE__ */ jsx(
75
- "button",
76
- {
77
- type: "button",
78
- "aria-label": plainName,
79
- title: plainName,
80
- "aria-pressed": plain,
81
- "data-content-plain-toggle": plain ? "plain" : "formatted",
82
- onMouseDown: keep,
83
- onClick: () => togglePlainView(viewKey),
84
- className: cn(iconButton(size), plain && "bg-accent text-foreground"),
85
- children: /* @__PURE__ */ jsx(PlainIcon, { className: iconSize })
86
- }
87
- ) : null,
88
- /* @__PURE__ */ jsxs(Popover, { open: exportOpen, onOpenChange: setExportOpen, children: [
89
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
90
- "button",
91
- {
92
- type: "button",
93
- "aria-label": "Export",
94
- title: "Export",
95
- "data-content-export-trigger": "",
96
- onMouseDown: keep,
97
- className: iconButton(size),
98
- children: /* @__PURE__ */ jsx(Download, { className: iconSize })
99
- }
100
- ) }),
101
- /* @__PURE__ */ jsx(PopoverContent, { align: "start", className: "w-auto min-w-44 p-1", "data-content-export-menu": "", children: /* @__PURE__ */ jsx("div", { role: "menu", "aria-label": "Export as", children: exportRows(() => setExportOpen(false)) }) })
102
- ] }),
103
- /* @__PURE__ */ jsx(
104
- "button",
105
- {
106
- type: "button",
107
- "aria-label": "Print",
108
- title: "Print",
109
- "data-content-print": "",
110
- onMouseDown: keep,
111
- onClick: runPrint,
112
- className: iconButton(size),
113
- children: /* @__PURE__ */ jsx(Printer, { className: iconSize })
114
- }
115
- ),
116
- onTransform ? /* @__PURE__ */ jsx(
117
- "button",
118
- {
119
- type: "button",
120
- "aria-label": "Transform",
121
- title: "Transform",
122
- "data-content-transform": "",
123
- onMouseDown: keep,
124
- onClick: onTransform,
125
- className: iconButton(size),
126
- children: /* @__PURE__ */ jsx(WandSparkles, { className: iconSize })
127
- }
128
- ) : null
129
- ] }),
130
- /* @__PURE__ */ jsxs(Popover, { open: moreOpen, onOpenChange: setMoreOpen, children: [
39
+ /* @__PURE__ */ jsx(
40
+ CopyMenuButton,
41
+ {
42
+ content: read,
43
+ copyRaw: copy ? () => copy("markdown") : void 0,
44
+ copyForAi,
45
+ label: label.replace(/^Copy\s+/i, "") || "content",
46
+ title,
47
+ size,
48
+ preserveSelection,
49
+ disabled
50
+ }
51
+ ),
52
+ viewKey ? /* @__PURE__ */ jsx(
53
+ "button",
54
+ {
55
+ type: "button",
56
+ "aria-label": plainName,
57
+ title: plainName,
58
+ "aria-pressed": plain,
59
+ "data-content-plain-toggle": plain ? "plain" : "formatted",
60
+ onMouseDown: keep,
61
+ onClick: () => togglePlainView(viewKey),
62
+ className: cn(contentIconButton(size), plain && "bg-accent text-foreground"),
63
+ children: /* @__PURE__ */ jsx(PlainIcon, { className: contentIconSize(size) })
64
+ }
65
+ ) : null,
66
+ menu ? /* @__PURE__ */ jsxs(Popover, { open: moreOpen, onOpenChange: setMoreOpen, children: [
131
67
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
132
68
  "button",
133
69
  {
@@ -136,23 +72,22 @@ function ContentActions({
136
72
  title: "More",
137
73
  "data-content-actions-more": "",
138
74
  onMouseDown: keep,
139
- className: cn(iconButton(size), "sm:hidden"),
140
- children: /* @__PURE__ */ jsx(MoreHorizontal, { className: iconSize })
75
+ className: contentIconButton(size),
76
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: contentIconSize(size) })
141
77
  }
142
78
  ) }),
143
- /* @__PURE__ */ jsx(PopoverContent, { align: "start", className: "w-auto min-w-52 p-1", "data-content-actions-sheet": "", children: /* @__PURE__ */ jsx("div", { role: "menu", "aria-label": "Content actions", children: /* @__PURE__ */ jsx(
79
+ /* @__PURE__ */ jsx(PopoverContent, { align: "end", className: "w-auto min-w-52 p-1", "data-content-actions-sheet": "", children: /* @__PURE__ */ jsx("div", { role: "menu", "aria-label": "Content actions", children: /* @__PURE__ */ jsx(
144
80
  ContentActionMenuRows,
145
81
  {
146
82
  content: read,
147
83
  title,
148
- viewKey,
149
84
  onTransform,
150
85
  exporters,
151
86
  onPrint,
152
87
  onDone: () => setMoreOpen(false)
153
88
  }
154
89
  ) }) })
155
- ] })
90
+ ] }) : null
156
91
  ]
157
92
  }
158
93
  );
@@ -177,6 +112,13 @@ function ContentActionMenuRows({
177
112
  void action();
178
113
  };
179
114
  return /* @__PURE__ */ jsxs(Fragment, { children: [
115
+ onTransform ? (
116
+ // THE Alchemy palette (transforms, formatted, JSON, every format, send to…): one entry.
117
+ /* @__PURE__ */ jsxs("button", { type: "button", role: "menuitem", "data-content-alchemy": "", onClick: run(onTransform), className: row, children: [
118
+ /* @__PURE__ */ jsx(WandSparkles, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
119
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: "Alchemy\u2026" })
120
+ ] })
121
+ ) : null,
180
122
  viewKey ? /* @__PURE__ */ jsxs(
181
123
  "button",
182
124
  {
@@ -222,11 +164,7 @@ function ContentActionMenuRows({
222
164
  /* @__PURE__ */ jsx("span", { className: "flex-1", children: "Print" })
223
165
  ]
224
166
  }
225
- ),
226
- onTransform ? /* @__PURE__ */ jsxs("button", { type: "button", role: "menuitem", "data-content-transform": "", onClick: run(onTransform), className: row, children: [
227
- /* @__PURE__ */ jsx(WandSparkles, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
228
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: "Transform" })
229
- ] }) : null
167
+ )
230
168
  ] });
231
169
  }
232
170
  function PlainTextView({ text, className }) {
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ export type CopyMenuRow = "raw" | "formatted" | "ai";
3
+ export interface CopyMenuButtonProps {
4
+ /** The item's raw source (markdown). A function reads it at click time. */
5
+ content: string | (() => string);
6
+ /** Raw copy override (a registry's own "copy markdown" row). Default `copyRichContent(…, "markdown")`. */
7
+ copyRaw?: () => Promise<boolean | void> | boolean | void;
8
+ /** The AI copy: the host's envelope (record, location, context). Default: the content with where it came from. */
9
+ copyForAi?: () => Promise<string> | string;
10
+ /** What is copied ("answer", "note"), for names and the AI envelope. */
11
+ label?: string;
12
+ /** Title for the AI envelope's location. */
13
+ title?: string;
14
+ /** Icon box: the bar's 28px ("sm") or the dense strip's ("xs"). */
15
+ size?: "sm" | "xs";
16
+ preserveSelection?: boolean;
17
+ disabled?: boolean;
18
+ className?: string;
19
+ }
20
+ export declare const contentIconButton: (size?: "sm" | "xs") => string;
21
+ export declare const contentIconSize: (size?: "sm" | "xs") => "h-3.5 w-3.5" | "h-4 w-4";
22
+ export declare function CopyMenuButton({ content, copyRaw, copyForAi, label, title, size, preserveSelection, disabled, className, }: CopyMenuButtonProps): React.ReactElement;
23
+ export default CopyMenuButton;
@@ -0,0 +1,126 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Bot, Check, Copy, FileCode2, Type } from "lucide-react";
5
+ import { buildAgentPayload } from "@ai-matrx/alchemy/operate";
6
+ import { Popover, PopoverAnchor, PopoverContent, cn } from "@ai-matrx/design-system";
7
+ import { toast } from "../host/toast.js";
8
+ import { cleanCopyMarkdown, copyRichContent, writeClipboardFlavors } from "./copy-commands.js";
9
+ import { formattedCopyHtml } from "./markdown-copy-html.js";
10
+ import { markdownToReadableText } from "./markdown-readable-text.js";
11
+ const ROWS = [
12
+ { row: "raw", label: "Copy raw", icon: FileCode2 },
13
+ { row: "formatted", label: "Copy formatted", icon: Type },
14
+ { row: "ai", label: "Copy for AI (with context)", icon: Bot }
15
+ ];
16
+ const contentIconButton = (size = "sm") => cn(
17
+ // THE bar button box (alchemy's strip button: 28px, px-1.5) — every icon in a bar is one size.
18
+ "inline-flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md px-1.5 text-muted-foreground hover:bg-accent hover:text-foreground pointer-coarse:h-11 pointer-coarse:min-w-11",
19
+ size === "xs" && "px-1"
20
+ );
21
+ const contentIconSize = (size = "sm") => size === "xs" ? "h-3.5 w-3.5" : "h-4 w-4";
22
+ function CopyMenuButton({
23
+ content,
24
+ copyRaw,
25
+ copyForAi,
26
+ label = "content",
27
+ title,
28
+ size = "sm",
29
+ preserveSelection = false,
30
+ disabled = false,
31
+ className
32
+ }) {
33
+ const [open, setOpen] = React.useState(false);
34
+ const [copied, setCopied] = React.useState(false);
35
+ const timer = React.useRef(null);
36
+ React.useEffect(() => () => {
37
+ if (timer.current) clearTimeout(timer.current);
38
+ }, []);
39
+ const read = () => typeof content === "function" ? content() : content;
40
+ const flash = (ok) => {
41
+ if (ok === false) return;
42
+ setCopied(true);
43
+ if (timer.current) clearTimeout(timer.current);
44
+ timer.current = setTimeout(() => setCopied(false), 1600);
45
+ };
46
+ const run = async (row) => {
47
+ if (row === "raw") return copyRaw ? copyRaw() : copyRichContent(read(), "markdown", { toast: "Copied" });
48
+ const source = cleanCopyMarkdown(read());
49
+ if (row === "formatted") {
50
+ const ok2 = await writeClipboardFlavors(markdownToReadableText(source), formattedCopyHtml(source));
51
+ if (ok2) toast.success("Formatted copy");
52
+ return ok2;
53
+ }
54
+ const text = copyForAi ? await copyForAi() : buildAgentPayload(
55
+ {
56
+ kind: "content",
57
+ location: title ? `AI Matrx \u2014 ${title}` : typeof document === "undefined" ? "AI Matrx" : document.title || "AI Matrx",
58
+ description: `The ${label} as shown in AI Matrx.`,
59
+ data: source,
60
+ dataFormat: "markdown"
61
+ },
62
+ typeof window === "undefined" ? {} : { url: window.location.href, capturedAt: (/* @__PURE__ */ new Date()).toISOString() }
63
+ );
64
+ const ok = await writeClipboardFlavors(text);
65
+ if (ok) toast.success("Copied for AI");
66
+ return ok;
67
+ };
68
+ const keep = preserveSelection ? (event) => event.preventDefault() : void 0;
69
+ const Icon = copied ? Check : Copy;
70
+ const name = `Copy ${label}`;
71
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
72
+ /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsx(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ "aria-label": name,
77
+ title: name,
78
+ "data-content-copy": "",
79
+ "data-matrx-button": "",
80
+ disabled,
81
+ onMouseDown: keep,
82
+ onClick: () => {
83
+ void Promise.resolve(run("raw")).then(flash);
84
+ setOpen(true);
85
+ },
86
+ className: cn(contentIconButton(size), copied && "text-foreground", disabled && "pointer-events-none opacity-50", className),
87
+ children: /* @__PURE__ */ jsx(Icon, { className: contentIconSize(size) })
88
+ }
89
+ ) }),
90
+ /* @__PURE__ */ jsx(
91
+ PopoverContent,
92
+ {
93
+ align: "start",
94
+ className: "w-auto min-w-56 p-1",
95
+ onOpenAutoFocus: (event) => event.preventDefault(),
96
+ "data-content-copy-menu": "",
97
+ children: /* @__PURE__ */ jsx("div", { role: "menu", "aria-label": "Copy as", children: ROWS.map(({ row, label: rowLabel, icon: RowIcon }) => /* @__PURE__ */ jsxs(
98
+ "button",
99
+ {
100
+ type: "button",
101
+ role: "menuitem",
102
+ "data-copy-row": row,
103
+ onMouseDown: keep,
104
+ onClick: () => {
105
+ setOpen(false);
106
+ void Promise.resolve(run(row)).then(flash);
107
+ },
108
+ className: "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent",
109
+ children: [
110
+ /* @__PURE__ */ jsx(RowIcon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
111
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: rowLabel })
112
+ ]
113
+ },
114
+ row
115
+ )) })
116
+ }
117
+ )
118
+ ] });
119
+ }
120
+ var CopyMenuButton_default = CopyMenuButton;
121
+ export {
122
+ CopyMenuButton,
123
+ contentIconButton,
124
+ contentIconSize,
125
+ CopyMenuButton_default as default
126
+ };
@@ -48,8 +48,12 @@ async function exportContent(markdown, format, title) {
48
48
  }
49
49
  async function printContent(markdown, title) {
50
50
  try {
51
- const { printMarkdown } = await import("@ai-matrx/print/markdown");
52
- const outcome = printMarkdown(removeThinkingContent(markdown), { title: title || "Document" });
51
+ const { printMessage } = await import("./print-message.js");
52
+ const outcome = await printMessage(removeThinkingContent(markdown), {
53
+ title: title || "Document",
54
+ skin: "document",
55
+ onNotice: (level, message) => level === "warning" ? toast.warning(message) : toast.info(message)
56
+ });
53
57
  if (outcome === "downloaded") toast.info("Pop-ups are blocked, so the print page downloaded as an HTML file.");
54
58
  } catch (error) {
55
59
  toast.error(`Print failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * THE message print: markdown prose, and every embedded block (a fence or an
3
+ * `<artifact>`) through the SAME adapter its own print uses — `@ai-matrx/print`'s
4
+ * one block-printer registry, keyed by artifact type / `__kind` (a quiz as its
5
+ * questions, a deck as its study sheet, an HTML page as its picture — a print
6
+ * window runs no author scripts) — and every mermaid diagram drawn once.
7
+ *
8
+ * Every "Print this content" door calls this (the content actions' Print, the
9
+ * chat message's Print), so a block never prints as its source on one surface
10
+ * and as itself on another.
11
+ */
12
+ import type { PrintOutcome } from "@ai-matrx/print/core";
13
+ import type { MarkdownSkin } from "@ai-matrx/print/markdown";
14
+ /** Block types that print as a PICTURE of a live page inside a composed print. */
15
+ export declare const FRAME_PRINT_BLOCK_TYPES: ReadonlySet<string>;
16
+ export interface PrintMessageOptions {
17
+ title?: string;
18
+ skin?: MarkdownSkin;
19
+ /** Where degraded output is announced (a diagram printed as source, a page printed as a picture). */
20
+ onNotice?: (level: "info" | "warning", message: string) => void;
21
+ }
22
+ /**
23
+ * Opens the print window (inside the click when no block needs preparing,
24
+ * else at once with "Preparing…") and returns how it reached the person.
25
+ */
26
+ export declare function printMessage(markdown: string, options?: PrintMessageOptions): Promise<PrintOutcome>;
@@ -0,0 +1,69 @@
1
+ const FRAME_PRINT_BLOCK_TYPES = /* @__PURE__ */ new Set(["html", "react", "iframe"]);
2
+ function blockData(block) {
3
+ const text = block.body.trim();
4
+ if (text.startsWith("{") || text.startsWith("[")) {
5
+ try {
6
+ return JSON.parse(text);
7
+ } catch {
8
+ return block.body;
9
+ }
10
+ }
11
+ return block.body;
12
+ }
13
+ async function printMessage(markdown, options = {}) {
14
+ const [{ listPrintBlocks, printMarkdown, printMarkdownWhenReady }, { getBlockPrinter }] = await Promise.all([
15
+ import("@ai-matrx/print/markdown"),
16
+ import("@ai-matrx/print/core")
17
+ ]);
18
+ const skin = options.skin ?? "article";
19
+ const title = options.title ?? "AI Response";
20
+ const notice = options.onNotice ?? (() => void 0);
21
+ const adapterFor = (block) => {
22
+ const printer = getBlockPrinter(block.type);
23
+ return printer?.toPrintHtml ? printer : null;
24
+ };
25
+ const blocks = listPrintBlocks(markdown, skin);
26
+ const hasMermaid = blocks.some((block) => block.source === "fence" && block.type === "mermaid");
27
+ const adapted = blocks.filter((block) => adapterFor(block) !== null);
28
+ if (!hasMermaid && adapted.length === 0) return printMarkdown(markdown, { skin, title });
29
+ let drawing = null;
30
+ const drawAll = () => drawing ??= (async () => {
31
+ try {
32
+ const { drawMermaidForPrint } = await import("../mermaid/print-render.js");
33
+ const { pictures, failed } = await drawMermaidForPrint(markdown);
34
+ if (failed > 0) {
35
+ notice("warning", `${failed} diagram${failed === 1 ? "" : "s"} could not be drawn and print${failed === 1 ? "s" : ""} as source.`);
36
+ }
37
+ return pictures;
38
+ } catch (error) {
39
+ console.error("[print] diagrams could not be drawn", error);
40
+ notice("warning", "Diagrams could not be drawn, so they print as source.");
41
+ return null;
42
+ }
43
+ })();
44
+ const outcome = await printMarkdownWhenReady(markdown, {
45
+ skin,
46
+ title,
47
+ renderBlock: async (block) => {
48
+ if (block.source === "fence" && block.type === "mermaid") {
49
+ const svg = (await drawAll())?.get(block.body);
50
+ return svg ? { svg, alt: "Diagram" } : null;
51
+ }
52
+ const printer = adapterFor(block);
53
+ if (!printer?.toPrintHtml) return null;
54
+ return printer.toPrintHtml(blockData(block), {
55
+ type: block.type,
56
+ raw: block.body,
57
+ ...block.title ? { title: block.title } : {}
58
+ });
59
+ }
60
+ });
61
+ if (outcome === "opened" && adapted.some((block) => FRAME_PRINT_BLOCK_TYPES.has(block.type))) {
62
+ notice("info", "Web pages print as a picture inside a message. For sharp text, print the page from its card.");
63
+ }
64
+ return outcome;
65
+ }
66
+ export {
67
+ FRAME_PRINT_BLOCK_TYPES,
68
+ printMessage
69
+ };
@@ -1,18 +1,17 @@
1
- import { Share } from "lucide-react";
1
+ import { WandSparkles } from "lucide-react";
2
2
  import { getAction, registerAction } from "./provider.js";
3
3
  import { hasExportPalette, openExportPalette } from "../../copy/ExportPalette.js";
4
- import { hasContentActions } from "../../copy/content-view-store.js";
5
4
  const EXPORT_PALETTE_ACTION_ID = "export-palette";
6
5
  const EXPORT_PALETTE_ACTION = {
7
6
  id: EXPORT_PALETTE_ACTION_ID,
8
- label: "Export\u2026",
9
- icon: Share,
7
+ // THE Alchemy palette (transforms, formatted, JSON, every format, send to…) — one ⋯ entry (Arman, 2026-10-08).
8
+ label: "Alchemy\u2026",
9
+ icon: WandSparkles,
10
10
  category: "export",
11
11
  supportedSources: "*",
12
12
  renderSlot: "overflow",
13
13
  order: 0,
14
- // Where the ContentActions set is mounted its Transform icon IS this door — once per surface.
15
- visible: (ctx) => hasExportPalette(ctx.instanceKey("alchemy")) && !hasContentActions(ctx.instanceKey("alchemy")),
14
+ visible: (ctx) => hasExportPalette(ctx.instanceKey("alchemy")),
16
15
  run: (ctx) => {
17
16
  openExportPalette(ctx.instanceKey("alchemy"));
18
17
  }
@@ -7,7 +7,7 @@ import { cn } from "@ai-matrx/design-system";
7
7
  import { clientLazy } from "@ai-matrx/kit/client-lazy";
8
8
  import { ContentActions } from "../../copy/ContentActions.js";
9
9
  import { copyRichContent } from "../../copy/copy-commands.js";
10
- import { ExportPaletteAnchor, openExportPalette } from "../../copy/ExportPalette.js";
10
+ import { ExportPaletteAnchor } from "../../copy/ExportPalette.js";
11
11
  import { ensureExportPaletteAction } from "../actions/export-palette-action.js";
12
12
  import { OpenOneMenuButton, useOneMenuFor } from "./shared/OpenOneMenuButton.js";
13
13
  import { StandaloneOneMenu } from "./shared/StandaloneOneMenu.js";
@@ -25,28 +25,13 @@ async function runDocumentCopy(actions, getCtx, flavor) {
25
25
  if (action) return action.run(getCtx());
26
26
  return copyRichContent(getCtx().content, flavor);
27
27
  }
28
- const EXPORT_ACTION_IDS = {
29
- pdf: "download-pdf",
30
- docx: "download-docx",
31
- html: "download-html"
32
- };
33
28
  function ActionBar(props) {
34
29
  const { actions, getCtx, target, sourceId, className, hideOverflow = false, mini = false, contentActions = true } = props;
35
30
  useRichDocumentProvider();
36
31
  const oneMenu = useOneMenuFor(getCtx().source);
37
32
  const hasCopy = actions.some((action) => action.category === "copy");
38
33
  const palette = React.useRef(null);
39
- const openOwnPalette = React.useRef(null);
40
34
  const copy = React.useCallback((flavor) => runDocumentCopy(actions, getCtx, flavor), [actions, getCtx]);
41
- const exporters = React.useMemo(() => {
42
- const out = {};
43
- for (const [format, id] of Object.entries(EXPORT_ACTION_IDS)) {
44
- const action = actions.find((a) => a.id === id);
45
- if (action) out[format] = () => action.run(getCtx());
46
- }
47
- return out;
48
- }, [actions, getCtx]);
49
- const printAction = actions.find((a) => a.id === "print");
50
35
  return /* @__PURE__ */ jsxs(
51
36
  "div",
52
37
  {
@@ -66,12 +51,11 @@ function ActionBar(props) {
66
51
  {
67
52
  content: () => getCtx().content,
68
53
  title: getCtx().source.type === "note" ? "Note" : void 0,
54
+ label: getCtx().source.type === "note" ? "note" : "content",
69
55
  copy,
70
56
  size: mini ? "xs" : "sm",
71
57
  viewKey: sourceId,
72
- exporters,
73
- onPrint: printAction ? () => printAction.run(getCtx()) : void 0,
74
- onTransform: () => openOwnPalette.current ? openOwnPalette.current() : void openExportPalette(sourceId)
58
+ menu: false
75
59
  }
76
60
  ) : null,
77
61
  /* @__PURE__ */ jsx(
@@ -97,7 +81,7 @@ function ActionBar(props) {
97
81
  hideOverflow: true
98
82
  }
99
83
  ),
100
- hasCopy ? /* @__PURE__ */ jsx(ExportPaletteAnchor, { exportKey: sourceId, controllerRef: palette, openerRef: openOwnPalette }) : null,
84
+ hasCopy ? /* @__PURE__ */ jsx(ExportPaletteAnchor, { exportKey: sourceId, controllerRef: palette }) : null,
101
85
  hideOverflow ? null : oneMenu ? /* @__PURE__ */ jsx(OpenOneMenuButton, { source: getCtx().source }) : /* @__PURE__ */ jsx(StandaloneOneMenu, { getCtx, target })
102
86
  ]
103
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-content",
3
- "version": "0.2.57",
3
+ "version": "0.2.60",
4
4
  "description": "The AI Matrx formatted-text engine: the one markdown renderer, streaming markdown, the block splitter and renderer, the generic blocks, <RichContent> levels, the rich-document action registry and menus, copy, and the selection toolbar — as portable React.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -130,20 +130,20 @@
130
130
  "unist-util-visit": "^5.1.0",
131
131
  "unist-util-visit-parents": "^6.0.2",
132
132
  "yaml": "^2.9.1",
133
- "@ai-matrx/alchemy": "latest",
134
133
  "@ai-matrx/agents": "latest",
135
- "@ai-matrx/content-ir": "latest",
136
- "@ai-matrx/content-ir-react": "latest",
137
- "@ai-matrx/data": "latest",
134
+ "@ai-matrx/alchemy": "latest",
138
135
  "@ai-matrx/canvas": "latest",
136
+ "@ai-matrx/data": "latest",
139
137
  "@ai-matrx/diff": "latest",
138
+ "@ai-matrx/design-system": "latest",
140
139
  "@ai-matrx/icons": "latest",
140
+ "@ai-matrx/kit": "latest",
141
+ "@ai-matrx/media": "latest",
141
142
  "@ai-matrx/print": "latest",
142
143
  "@ai-matrx/records": "latest",
143
144
  "@ai-matrx/records-ui": "latest",
144
- "@ai-matrx/design-system": "latest",
145
- "@ai-matrx/kit": "latest",
146
- "@ai-matrx/media": "latest"
145
+ "@ai-matrx/content-ir-react": "latest",
146
+ "@ai-matrx/content-ir": "latest"
147
147
  },
148
148
  "peerDependencies": {
149
149
  "react": ">=19.0.0",