@ai-matrx/rich-content 0.2.57 → 0.2.59
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 +19 -3
- package/dist/copy/ContentActions.d.ts +12 -4
- package/dist/copy/ContentActions.js +44 -106
- package/dist/copy/CopyMenuButton.d.ts +23 -0
- package/dist/copy/CopyMenuButton.js +126 -0
- package/dist/copy/content-export.js +6 -2
- package/dist/copy/print-message.d.ts +26 -0
- package/dist/copy/print-message.js +69 -0
- package/dist/rich-document/actions/export-palette-action.js +5 -6
- package/dist/rich-document/variants/ActionBar.js +4 -20
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.2.
|
|
4
|
-
|
|
3
|
+
## 0.2.59
|
|
4
|
+
|
|
5
|
+
- 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).
|
|
6
|
+
- 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).
|
|
7
|
+
|
|
8
|
+
Consumer action: drop any `hasContentActions` gate that hid ⋯ download/print rows beside the set.
|
|
9
|
+
|
|
10
|
+
## 0.2.58 — one message print
|
|
11
|
+
|
|
12
|
+
- New `copy/print-message` → `printMessage(markdown, { title, skin, onNotice })`: THE message print —
|
|
13
|
+
prose plus every embedded block (fence or `<artifact>`) through `@ai-matrx/print`'s block-printer
|
|
14
|
+
registry (`toPrintHtml`), mermaid drawn once, "web pages print as a picture" announced.
|
|
15
|
+
- `printContent` (the content actions' Print) uses it: an HTML page in a message no longer prints as
|
|
16
|
+
its source code.
|
|
17
|
+
|
|
18
|
+
Consumer action: none (hosts register block printers with `registerBlockPrinter`).
|
|
19
|
+
|
|
20
|
+
## Unreleased — parse5 and the YAML parser leave the first screen
|
|
5
21
|
|
|
6
22
|
- The raw-HTML tag allow-list lives alone in `./display/chat-markdown/raw-html-tags`
|
|
7
23
|
(`rehypeSafeRawHtml` re-exports it). The content splitter, table vectors and prose prep read only the
|
|
@@ -14,7 +30,7 @@
|
|
|
14
30
|
Consumer action: none (a direct caller of `extractFrontmatter` that needs the parsed properties at
|
|
15
31
|
once awaits `loadFrontmatterParsers()` first).
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
## Unreleased — tap-target comes from design-system
|
|
18
34
|
|
|
19
35
|
- Imports `@ai-matrx/design-system/tap-target` (and `/tap-target/buttons`, `tap-target.css`) instead of the
|
|
20
36
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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:
|
|
140
|
-
children: /* @__PURE__ */ jsx(MoreHorizontal, { className:
|
|
75
|
+
className: contentIconButton(size),
|
|
76
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: contentIconSize(size) })
|
|
141
77
|
}
|
|
142
78
|
) }),
|
|
143
|
-
/* @__PURE__ */ jsx(PopoverContent, { align: "
|
|
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 {
|
|
52
|
-
const outcome =
|
|
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 {
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.2.59",
|
|
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",
|
|
@@ -131,17 +131,17 @@
|
|
|
131
131
|
"unist-util-visit-parents": "^6.0.2",
|
|
132
132
|
"yaml": "^2.9.1",
|
|
133
133
|
"@ai-matrx/alchemy": "latest",
|
|
134
|
-
"@ai-matrx/
|
|
134
|
+
"@ai-matrx/canvas": "latest",
|
|
135
135
|
"@ai-matrx/content-ir": "latest",
|
|
136
136
|
"@ai-matrx/content-ir-react": "latest",
|
|
137
137
|
"@ai-matrx/data": "latest",
|
|
138
|
-
"@ai-matrx/
|
|
138
|
+
"@ai-matrx/agents": "latest",
|
|
139
139
|
"@ai-matrx/diff": "latest",
|
|
140
|
-
"@ai-matrx/
|
|
140
|
+
"@ai-matrx/design-system": "latest",
|
|
141
141
|
"@ai-matrx/print": "latest",
|
|
142
142
|
"@ai-matrx/records": "latest",
|
|
143
143
|
"@ai-matrx/records-ui": "latest",
|
|
144
|
-
"@ai-matrx/
|
|
144
|
+
"@ai-matrx/icons": "latest",
|
|
145
145
|
"@ai-matrx/kit": "latest",
|
|
146
146
|
"@ai-matrx/media": "latest"
|
|
147
147
|
},
|