@ai-matrx/rich-content 0.2.50 → 0.2.52
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 +13 -0
- package/dist/copy/ContentActions.d.ts +45 -0
- package/dist/copy/ContentActions.js +231 -0
- package/dist/copy/content-export.d.ts +9 -0
- package/dist/copy/content-export.js +62 -0
- package/dist/copy/content-view-store.d.ts +10 -0
- package/dist/copy/content-view-store.js +49 -0
- package/dist/display/chat-markdown/ConfigurableMarkdownContent.js +9 -5
- package/dist/levels/prose/prose-block-elements.js +8 -5
- package/dist/rich-document/RichDocument.js +29 -23
- package/dist/rich-document/actions/export-palette-action.js +3 -1
- package/dist/rich-document/runtime/useActionSurfaceProvider.d.ts +6 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.js +6 -0
- package/dist/rich-document/variants/ActionBar.d.ts +1 -1
- package/dist/rich-document/variants/ActionBar.js +22 -3
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.52
|
|
4
|
+
|
|
5
|
+
- `copy/ContentActions` — THE content action set (Arman, 2026-10-08): compact icons for a host's existing bar — split Copy, Plain/Formatted (the item's exact source, monospace, wrapped, selectable; per item, in memory), Export (one click: PDF · Word · HTML · Markdown file · Text file), Print, Transform (the Alchemy palette). On a phone it folds to Copy + "…" with every action one tap inside. `ContentView` / `PlainTextView` / `usePlainView` (copy/content-view-store) let a host's content area honor the switch; `hasContentActions(key)` lets a ⋯ row that duplicates the set hide itself.
|
|
6
|
+
- `copy/content-export` — default Export (the one print-grade document tree) and Print (`@ai-matrx/print/markdown`).
|
|
7
|
+
- The rich-document `ActionBar` leads with `ContentActions` (its Export/Print run the registry's own `download-*` / `print` rows; Transform opens the palette); `RichDocument` renders the plain source when its item is switched. The ⋯ "Export…" row hides where the set is mounted (Transform is that door). `richDocumentViewKey(source, content)` gives a host that renders the content beside `<RichDocumentActions>` the same key.
|
|
8
|
+
|
|
9
|
+
Consumer action: hide registry rows that duplicate Export / Print with `hasContentActions(ctx.instanceKey("alchemy"))`; wrap self-rendered content in `ContentView`.
|
|
10
|
+
|
|
11
|
+
## 0.2.51
|
|
12
|
+
|
|
13
|
+
- A list holding a checklist item keeps its bullets and indent: remark-gfm's `contains-task-list` class is merged into the list's own classes instead of replacing them (prose renderer and the configurable chat renderer). Checklist items show only their checkbox (`list-none`). Live: notes Split showed `- item a` beside `- [ ] todo` as plain lines.
|
|
14
|
+
|
|
15
|
+
Consumer action: none.
|
|
3
16
|
|
|
4
17
|
## 0.2.50
|
|
5
18
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type SplitCopyFlavor } from "./CopySplitButton.js";
|
|
3
|
+
import { type ContentExportFormat } from "./content-export.js";
|
|
4
|
+
export interface ContentActionsProps {
|
|
5
|
+
/** The item's markdown source (a function reads it at click time). */
|
|
6
|
+
content: string | (() => string);
|
|
7
|
+
/** File name / print title. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Copy override (a registry's own copy rows). Default `copyRichContent`. */
|
|
10
|
+
copy?: (flavor: SplitCopyFlavor) => Promise<boolean | void> | boolean | void;
|
|
11
|
+
/**
|
|
12
|
+
* The item's Plain / Formatted key. The host's content area renders
|
|
13
|
+
* <ContentView viewKey={…}> (or reads `usePlainView`). Without a key the
|
|
14
|
+
* Plain switch is absent — a host that cannot show its source says so by
|
|
15
|
+
* leaving it out.
|
|
16
|
+
*/
|
|
17
|
+
viewKey?: string;
|
|
18
|
+
/** Opens the Alchemy palette for this item. Absent = no Transform icon. */
|
|
19
|
+
onTransform?: () => void;
|
|
20
|
+
/** Per-format overrides (a registry's own download rows). */
|
|
21
|
+
exporters?: Partial<Record<ContentExportFormat, () => unknown>>;
|
|
22
|
+
/** Print override (a registry's own print row). */
|
|
23
|
+
onPrint?: () => unknown;
|
|
24
|
+
/** What is copied, for the copy button's name. */
|
|
25
|
+
label?: string;
|
|
26
|
+
size?: "sm" | "xs";
|
|
27
|
+
/** Keep the page's text selection when pressed (the selection toolbar). */
|
|
28
|
+
preserveSelection?: boolean;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function ContentActions({ content, title, copy, viewKey, onTransform, exporters, onPrint, label, size, preserveSelection, disabled, className, }: ContentActionsProps): React.ReactElement;
|
|
33
|
+
/** The exact source, shown in place of the render: monospace, wrapped, selectable. */
|
|
34
|
+
export declare function PlainTextView({ text, className }: {
|
|
35
|
+
text: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}): React.ReactElement;
|
|
38
|
+
/** A content area that honors its item's Plain / Formatted switch. */
|
|
39
|
+
export declare function ContentView({ viewKey, text, children, className, }: {
|
|
40
|
+
viewKey: string | undefined | null;
|
|
41
|
+
text: string | (() => string);
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
}): React.ReactElement;
|
|
45
|
+
export default ContentActions;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { BookOpen, Code2, Download, MoreHorizontal, Printer, WandSparkles } from "lucide-react";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger, cn } from "@ai-matrx/design-system";
|
|
6
|
+
import { CopySplitButton } from "./CopySplitButton.js";
|
|
7
|
+
import { copyRichContent } from "./copy-commands.js";
|
|
8
|
+
import { CONTENT_EXPORT_FORMATS, exportContent, printContent } from "./content-export.js";
|
|
9
|
+
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
|
+
const menuRow = "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent";
|
|
15
|
+
function ContentActions({
|
|
16
|
+
content,
|
|
17
|
+
title,
|
|
18
|
+
copy,
|
|
19
|
+
viewKey,
|
|
20
|
+
onTransform,
|
|
21
|
+
exporters,
|
|
22
|
+
onPrint,
|
|
23
|
+
label = "Copy",
|
|
24
|
+
size = "sm",
|
|
25
|
+
preserveSelection = false,
|
|
26
|
+
disabled = false,
|
|
27
|
+
className
|
|
28
|
+
}) {
|
|
29
|
+
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
|
+
const plain = usePlainView(viewKey);
|
|
35
|
+
React.useEffect(() => viewKey ? markContentActions(viewKey) : void 0, [viewKey]);
|
|
36
|
+
const [exportOpen, setExportOpen] = React.useState(false);
|
|
37
|
+
const [moreOpen, setMoreOpen] = React.useState(false);
|
|
38
|
+
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
|
+
const plainName = plain ? "Show formatted" : "Show plain text";
|
|
46
|
+
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
|
+
return /* @__PURE__ */ jsxs(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
69
|
+
"data-content-actions": "",
|
|
70
|
+
className: cn("inline-flex shrink-0 items-center gap-0.5", disabled && "pointer-events-none opacity-50", className),
|
|
71
|
+
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: [
|
|
131
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
"aria-label": "More content actions",
|
|
136
|
+
title: "More",
|
|
137
|
+
"data-content-actions-more": "",
|
|
138
|
+
onMouseDown: keep,
|
|
139
|
+
className: cn(iconButton(size), "sm:hidden"),
|
|
140
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: iconSize })
|
|
141
|
+
}
|
|
142
|
+
) }),
|
|
143
|
+
/* @__PURE__ */ jsx(PopoverContent, { align: "start", className: "w-auto min-w-52 p-1", "data-content-actions-sheet": "", children: /* @__PURE__ */ jsxs("div", { role: "menu", "aria-label": "Content actions", children: [
|
|
144
|
+
viewKey ? /* @__PURE__ */ jsxs(
|
|
145
|
+
"button",
|
|
146
|
+
{
|
|
147
|
+
type: "button",
|
|
148
|
+
role: "menuitem",
|
|
149
|
+
"data-content-plain-toggle": plain ? "plain" : "formatted",
|
|
150
|
+
onClick: () => {
|
|
151
|
+
setMoreOpen(false);
|
|
152
|
+
togglePlainView(viewKey);
|
|
153
|
+
},
|
|
154
|
+
className: menuRow,
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsx(PlainIcon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
157
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: plainName })
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
) : null,
|
|
161
|
+
exportRows(() => setMoreOpen(false)),
|
|
162
|
+
/* @__PURE__ */ jsxs(
|
|
163
|
+
"button",
|
|
164
|
+
{
|
|
165
|
+
type: "button",
|
|
166
|
+
role: "menuitem",
|
|
167
|
+
"data-content-print": "",
|
|
168
|
+
onClick: () => {
|
|
169
|
+
setMoreOpen(false);
|
|
170
|
+
runPrint();
|
|
171
|
+
},
|
|
172
|
+
className: menuRow,
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ jsx(Printer, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
175
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: "Print" })
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
onTransform ? /* @__PURE__ */ jsxs(
|
|
180
|
+
"button",
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
role: "menuitem",
|
|
184
|
+
"data-content-transform": "",
|
|
185
|
+
onClick: () => {
|
|
186
|
+
setMoreOpen(false);
|
|
187
|
+
onTransform();
|
|
188
|
+
},
|
|
189
|
+
className: menuRow,
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
192
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: "Transform" })
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
) : null
|
|
196
|
+
] }) })
|
|
197
|
+
] })
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function PlainTextView({ text, className }) {
|
|
203
|
+
return /* @__PURE__ */ jsx(
|
|
204
|
+
"pre",
|
|
205
|
+
{
|
|
206
|
+
"data-content-plain-view": "",
|
|
207
|
+
className: cn(
|
|
208
|
+
"m-0 select-text whitespace-pre-wrap break-words rounded-md bg-muted/40 p-3 font-mono text-sm leading-relaxed text-foreground",
|
|
209
|
+
className
|
|
210
|
+
),
|
|
211
|
+
children: text
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function ContentView({
|
|
216
|
+
viewKey,
|
|
217
|
+
text,
|
|
218
|
+
children,
|
|
219
|
+
className
|
|
220
|
+
}) {
|
|
221
|
+
const plain = usePlainView(viewKey);
|
|
222
|
+
if (!plain) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
223
|
+
return /* @__PURE__ */ jsx(PlainTextView, { text: typeof text === "function" ? text() : text, className });
|
|
224
|
+
}
|
|
225
|
+
var ContentActions_default = ContentActions;
|
|
226
|
+
export {
|
|
227
|
+
ContentActions,
|
|
228
|
+
ContentView,
|
|
229
|
+
PlainTextView,
|
|
230
|
+
ContentActions_default as default
|
|
231
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ContentExportFormat = "pdf" | "docx" | "html" | "markdown" | "text";
|
|
2
|
+
export declare const CONTENT_EXPORT_FORMATS: ReadonlyArray<{
|
|
3
|
+
format: ContentExportFormat;
|
|
4
|
+
label: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** Download `markdown` in one format. Never silent: success and failure are both said. */
|
|
7
|
+
export declare function exportContent(markdown: string, format: ContentExportFormat, title?: string): Promise<void>;
|
|
8
|
+
/** Open the print view (Print / Save PDF) for `markdown`. */
|
|
9
|
+
export declare function printContent(markdown: string, title?: string): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toast } from "../host/toast.js";
|
|
2
|
+
import { markdownToReadableText } from "./markdown-readable-text.js";
|
|
3
|
+
import { removeThinkingContent } from "./markdown-copy-html.js";
|
|
4
|
+
const CONTENT_EXPORT_FORMATS = [
|
|
5
|
+
{ format: "pdf", label: "PDF" },
|
|
6
|
+
{ format: "docx", label: "Word" },
|
|
7
|
+
{ format: "html", label: "HTML" },
|
|
8
|
+
{ format: "markdown", label: "Markdown file" },
|
|
9
|
+
{ format: "text", label: "Text file" }
|
|
10
|
+
];
|
|
11
|
+
function fileBase(title) {
|
|
12
|
+
const base = (title ?? "").replace(/[\\/:*?"<>|\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
13
|
+
return base || "document";
|
|
14
|
+
}
|
|
15
|
+
function save(name, bytes, mime) {
|
|
16
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: mime }));
|
|
17
|
+
const link = document.createElement("a");
|
|
18
|
+
link.href = url;
|
|
19
|
+
link.download = name;
|
|
20
|
+
link.rel = "noopener";
|
|
21
|
+
document.body.appendChild(link);
|
|
22
|
+
link.click();
|
|
23
|
+
link.remove();
|
|
24
|
+
setTimeout(() => URL.revokeObjectURL(url), 3e4);
|
|
25
|
+
}
|
|
26
|
+
const DONE = {
|
|
27
|
+
pdf: "PDF downloaded",
|
|
28
|
+
docx: "Word document downloaded",
|
|
29
|
+
html: "HTML page downloaded",
|
|
30
|
+
markdown: "Markdown file downloaded",
|
|
31
|
+
text: "Text file downloaded"
|
|
32
|
+
};
|
|
33
|
+
async function exportContent(markdown, format, title) {
|
|
34
|
+
const source = removeThinkingContent(markdown);
|
|
35
|
+
const name = fileBase(title);
|
|
36
|
+
try {
|
|
37
|
+
if (format === "markdown") save(`${name}.md`, source, "text/markdown;charset=utf-8");
|
|
38
|
+
else if (format === "text") save(`${name}.txt`, markdownToReadableText(source), "text/plain;charset=utf-8");
|
|
39
|
+
else {
|
|
40
|
+
const { exportDocument } = await import("@ai-matrx/print/document");
|
|
41
|
+
const exp = await exportDocument(source, format, { fileName: name });
|
|
42
|
+
save(exp.fileName, exp.bytes, exp.mime);
|
|
43
|
+
}
|
|
44
|
+
toast.success(DONE[format]);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
toast.error(`Export failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function printContent(markdown, title) {
|
|
50
|
+
try {
|
|
51
|
+
const { printMarkdown } = await import("@ai-matrx/print/markdown");
|
|
52
|
+
const outcome = printMarkdown(removeThinkingContent(markdown), { title: title || "Document" });
|
|
53
|
+
if (outcome === "downloaded") toast.info("Pop-ups are blocked, so the print page downloaded as an HTML file.");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
toast.error(`Print failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
CONTENT_EXPORT_FORMATS,
|
|
60
|
+
exportContent,
|
|
61
|
+
printContent
|
|
62
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Is this item showing its plain text? */
|
|
2
|
+
export declare function isPlainView(key: string | undefined | null): boolean;
|
|
3
|
+
export declare function setPlainView(key: string, on: boolean): void;
|
|
4
|
+
export declare function togglePlainView(key: string): void;
|
|
5
|
+
/** Subscribe a content area to its item's Plain / Formatted switch. */
|
|
6
|
+
export declare function usePlainView(key: string | undefined | null): boolean;
|
|
7
|
+
/** A ContentActions set registers its item while mounted. */
|
|
8
|
+
export declare function markContentActions(key: string): () => void;
|
|
9
|
+
/** Does this item already show Export / Print / Transform in its bar? */
|
|
10
|
+
export declare function hasContentActions(key: string | undefined | null): boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
const plain = /* @__PURE__ */ new Set();
|
|
3
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
const mounted = /* @__PURE__ */ new Map();
|
|
5
|
+
function emit() {
|
|
6
|
+
for (const listener of listeners) listener();
|
|
7
|
+
}
|
|
8
|
+
function subscribe(listener) {
|
|
9
|
+
listeners.add(listener);
|
|
10
|
+
return () => listeners.delete(listener);
|
|
11
|
+
}
|
|
12
|
+
function isPlainView(key) {
|
|
13
|
+
return Boolean(key) && plain.has(key);
|
|
14
|
+
}
|
|
15
|
+
function setPlainView(key, on) {
|
|
16
|
+
if (on === plain.has(key)) return;
|
|
17
|
+
if (on) plain.add(key);
|
|
18
|
+
else plain.delete(key);
|
|
19
|
+
emit();
|
|
20
|
+
}
|
|
21
|
+
function togglePlainView(key) {
|
|
22
|
+
setPlainView(key, !plain.has(key));
|
|
23
|
+
}
|
|
24
|
+
function usePlainView(key) {
|
|
25
|
+
return React.useSyncExternalStore(
|
|
26
|
+
subscribe,
|
|
27
|
+
() => isPlainView(key),
|
|
28
|
+
() => false
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function markContentActions(key) {
|
|
32
|
+
mounted.set(key, (mounted.get(key) ?? 0) + 1);
|
|
33
|
+
return () => {
|
|
34
|
+
const next = (mounted.get(key) ?? 1) - 1;
|
|
35
|
+
if (next <= 0) mounted.delete(key);
|
|
36
|
+
else mounted.set(key, next);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function hasContentActions(key) {
|
|
40
|
+
return Boolean(key) && mounted.has(key);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
hasContentActions,
|
|
44
|
+
isPlainView,
|
|
45
|
+
markContentActions,
|
|
46
|
+
setPlainView,
|
|
47
|
+
togglePlainView,
|
|
48
|
+
usePlainView
|
|
49
|
+
};
|
|
@@ -364,7 +364,7 @@ const ConfigurableMarkdownContentBody = ({
|
|
|
364
364
|
);
|
|
365
365
|
}),
|
|
366
366
|
// ---- ul ----
|
|
367
|
-
ul: componentOverrides?.ul ?? (({ node, children, ...props }) => {
|
|
367
|
+
ul: componentOverrides?.ul ?? (({ node, children, className, ...props }) => {
|
|
368
368
|
const listText = extractText(children);
|
|
369
369
|
const listDirection = detectTextDirection(listText);
|
|
370
370
|
return /* @__PURE__ */ jsx(
|
|
@@ -377,7 +377,9 @@ const ConfigurableMarkdownContentBody = ({
|
|
|
377
377
|
typography.leading,
|
|
378
378
|
getFontSize(listDirection),
|
|
379
379
|
spacing.listPl,
|
|
380
|
-
getDirectionClasses(listDirection)
|
|
380
|
+
getDirectionClasses(listDirection),
|
|
381
|
+
// remark-gfm's `contains-task-list`: merged, never spread over ours.
|
|
382
|
+
className
|
|
381
383
|
),
|
|
382
384
|
dir: listDirection,
|
|
383
385
|
...props,
|
|
@@ -386,7 +388,7 @@ const ConfigurableMarkdownContentBody = ({
|
|
|
386
388
|
);
|
|
387
389
|
}),
|
|
388
390
|
// ---- ol ----
|
|
389
|
-
ol: componentOverrides?.ol ?? (({ node, children, ...props }) => {
|
|
391
|
+
ol: componentOverrides?.ol ?? (({ node, children, className, ...props }) => {
|
|
390
392
|
const listText = extractText(children);
|
|
391
393
|
const listDirection = detectTextDirection(listText);
|
|
392
394
|
return /* @__PURE__ */ jsx(
|
|
@@ -399,7 +401,9 @@ const ConfigurableMarkdownContentBody = ({
|
|
|
399
401
|
typography.leading,
|
|
400
402
|
getFontSize(listDirection),
|
|
401
403
|
spacing.listPl,
|
|
402
|
-
getDirectionClasses(listDirection)
|
|
404
|
+
getDirectionClasses(listDirection),
|
|
405
|
+
// remark-gfm's `contains-task-list`: merged, never spread over ours.
|
|
406
|
+
className
|
|
403
407
|
),
|
|
404
408
|
dir: listDirection,
|
|
405
409
|
...props,
|
|
@@ -419,7 +423,7 @@ const ConfigurableMarkdownContentBody = ({
|
|
|
419
423
|
{
|
|
420
424
|
className: cn(
|
|
421
425
|
spacing.listItemMb,
|
|
422
|
-
"flex items-center",
|
|
426
|
+
"flex items-center list-none",
|
|
423
427
|
getFontSize(itemDirection),
|
|
424
428
|
getDirectionClasses(itemDirection)
|
|
425
429
|
),
|
|
@@ -20,7 +20,7 @@ const ListItemComponent = ({ children, node, id, value }) => {
|
|
|
20
20
|
{
|
|
21
21
|
id,
|
|
22
22
|
value,
|
|
23
|
-
className: `mb-1 ${getDirectionFontSize(itemDirection)} ${getDirectionClasses(itemDirection)}`,
|
|
23
|
+
className: `task-list-item mb-1 list-none ${getDirectionFontSize(itemDirection)} ${getDirectionClasses(itemDirection)}`,
|
|
24
24
|
dir: itemDirection,
|
|
25
25
|
children
|
|
26
26
|
}
|
|
@@ -113,26 +113,29 @@ const PROSE_BLOCK_ELEMENTS = {
|
|
|
113
113
|
}
|
|
114
114
|
);
|
|
115
115
|
},
|
|
116
|
-
|
|
116
|
+
// remark-gfm marks a list holding a checklist item `contains-task-list` — MERGED into our
|
|
117
|
+
// classes, never spread over them (it used to replace them: a list mixing bullets and
|
|
118
|
+
// tasks lost its bullets and indent, notes Split 2026-10-07).
|
|
119
|
+
ul: ({ node, children, className, ...props }) => {
|
|
117
120
|
const listText = typeof children === "string" ? children : Array.isArray(children) ? children.join("") : "";
|
|
118
121
|
const listDirection = detectTextDirection(listText);
|
|
119
122
|
return /* @__PURE__ */ jsx(
|
|
120
123
|
"ul",
|
|
121
124
|
{
|
|
122
|
-
className: `matrx-md-ul mb-3 leading-relaxed ${getDirectionFontSize(listDirection)} pl-6 ${getDirectionClasses(listDirection)}`,
|
|
125
|
+
className: `matrx-md-ul mb-3 leading-relaxed ${getDirectionFontSize(listDirection)} pl-6 ${getDirectionClasses(listDirection)}${className ? ` ${className}` : ""}`,
|
|
123
126
|
dir: listDirection,
|
|
124
127
|
...props,
|
|
125
128
|
children
|
|
126
129
|
}
|
|
127
130
|
);
|
|
128
131
|
},
|
|
129
|
-
ol: ({ node, children, ...props }) => {
|
|
132
|
+
ol: ({ node, children, className, ...props }) => {
|
|
130
133
|
const listText = typeof children === "string" ? children : Array.isArray(children) ? children.join("") : "";
|
|
131
134
|
const listDirection = detectTextDirection(listText);
|
|
132
135
|
return /* @__PURE__ */ jsx(
|
|
133
136
|
"ol",
|
|
134
137
|
{
|
|
135
|
-
className: `matrx-md-ol mb-3 leading-relaxed ${getDirectionFontSize(listDirection)} pl-6 ${getDirectionClasses(listDirection)}`,
|
|
138
|
+
className: `matrx-md-ol mb-3 leading-relaxed ${getDirectionFontSize(listDirection)} pl-6 ${getDirectionClasses(listDirection)}${className ? ` ${className}` : ""}`,
|
|
136
139
|
dir: listDirection,
|
|
137
140
|
...props,
|
|
138
141
|
children
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
MarkdownStream
|
|
15
15
|
} from "../host/app-bindings.js";
|
|
16
16
|
import { ActionBar } from "./variants/ActionBar.js";
|
|
17
|
+
import { PlainTextView } from "../copy/ContentActions.js";
|
|
18
|
+
import { usePlainView } from "../copy/content-view-store.js";
|
|
17
19
|
import { MiniActionBar } from "./variants/MiniActionBar.js";
|
|
18
20
|
import { MenuVariant } from "./variants/MenuVariant.js";
|
|
19
21
|
import { contextMenuFor } from "./runtime/contextMenuFor.js";
|
|
@@ -98,6 +100,7 @@ function RichDocumentGated(props) {
|
|
|
98
100
|
actionsVariant: effectiveActionsVariant,
|
|
99
101
|
actionsSurfaceId
|
|
100
102
|
});
|
|
103
|
+
const plainView = usePlainView(ctx.instanceKey("alchemy"));
|
|
101
104
|
let variantNode = null;
|
|
102
105
|
switch (effectiveActionsVariant) {
|
|
103
106
|
case "bar":
|
|
@@ -167,29 +170,32 @@ function RichDocumentGated(props) {
|
|
|
167
170
|
...record.href ? { href: record.href } : {},
|
|
168
171
|
...record.conversationId ? { conversationId: record.conversationId } : {}
|
|
169
172
|
} : null;
|
|
170
|
-
const engineInner = /* @__PURE__ */ jsx(RecordAnnotations, { record: annotationSource, children: /* @__PURE__ */
|
|
171
|
-
|
|
172
|
-
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
173
|
+
const engineInner = /* @__PURE__ */ jsx(RecordAnnotations, { record: annotationSource, children: /* @__PURE__ */ jsxs("div", { className: cn("rich-document__content", contentClassName), children: [
|
|
174
|
+
plainView ? /* @__PURE__ */ jsx(PlainTextView, { text: content ?? "" }) : null,
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: plainView ? "hidden" : "contents", children: /* @__PURE__ */ jsx(
|
|
176
|
+
MarkdownStream,
|
|
177
|
+
{
|
|
178
|
+
imagePolicy,
|
|
179
|
+
content,
|
|
180
|
+
events,
|
|
181
|
+
serverProcessedBlocks,
|
|
182
|
+
taskId,
|
|
183
|
+
requestId,
|
|
184
|
+
turnId,
|
|
185
|
+
conversationId,
|
|
186
|
+
isStreamActive,
|
|
187
|
+
onContentChange,
|
|
188
|
+
applyLocalEdits,
|
|
189
|
+
analysisData,
|
|
190
|
+
messageId,
|
|
191
|
+
allowFullScreenEditor,
|
|
192
|
+
hideCopyButton,
|
|
193
|
+
onError,
|
|
194
|
+
onPhaseUpdate,
|
|
195
|
+
strictServerData
|
|
196
|
+
}
|
|
197
|
+
) })
|
|
198
|
+
] }) });
|
|
193
199
|
let engine = engineInner;
|
|
194
200
|
const contextMenu = contextMenuFor(enableContextMenu, effectiveActionsVariant);
|
|
195
201
|
if (contextMenu && !specimenMode) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Share } 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";
|
|
4
5
|
const EXPORT_PALETTE_ACTION_ID = "export-palette";
|
|
5
6
|
const EXPORT_PALETTE_ACTION = {
|
|
6
7
|
id: EXPORT_PALETTE_ACTION_ID,
|
|
@@ -10,7 +11,8 @@ const EXPORT_PALETTE_ACTION = {
|
|
|
10
11
|
supportedSources: "*",
|
|
11
12
|
renderSlot: "overflow",
|
|
12
13
|
order: 0,
|
|
13
|
-
|
|
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
16
|
run: (ctx) => {
|
|
15
17
|
openExportPalette(ctx.instanceKey("alchemy"));
|
|
16
18
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ClickTarget } from "@ai-matrx/alchemy/actions";
|
|
2
2
|
import type { ContentSource, RichDocumentAction, RichDocumentActionContext, RichDocumentActionsProp, RichDocumentActionsVariant } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The key a document's bar uses for its item (`ctx.instanceKey("alchemy")`) — the
|
|
5
|
+
* ContentActions Plain switch is keyed by it, so a host that renders the content
|
|
6
|
+
* itself beside <RichDocumentActions> reads the same switch (`usePlainView`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function richDocumentViewKey(source: ContentSource, content: string): string;
|
|
3
9
|
export interface UseActionSurfaceProviderArgs {
|
|
4
10
|
content: string | undefined;
|
|
5
11
|
source: ContentSource;
|
|
@@ -66,6 +66,11 @@ function buildContext(args) {
|
|
|
66
66
|
extensions: args.extensions
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
function richDocumentViewKey(source, content) {
|
|
70
|
+
const prefix = getSourceAdapter(source.type).instanceKeyPrefix(source);
|
|
71
|
+
const instancePrefix = source.type === "raw" ? `${prefix}-${shortHash(content).slice(0, 8)}` : prefix;
|
|
72
|
+
return `${instancePrefix}-alchemy`;
|
|
73
|
+
}
|
|
69
74
|
function useActionSurfaceProvider(args) {
|
|
70
75
|
const {
|
|
71
76
|
content,
|
|
@@ -210,5 +215,6 @@ function useActionSurfaceProvider(args) {
|
|
|
210
215
|
return { ctx, getCtx, resolvedActions, target };
|
|
211
216
|
}
|
|
212
217
|
export {
|
|
218
|
+
richDocumentViewKey,
|
|
213
219
|
useActionSurfaceProvider
|
|
214
220
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { ClickTarget } from "@ai-matrx/alchemy/actions";
|
|
3
|
-
import {
|
|
3
|
+
import type { SplitCopyFlavor } from "../../copy/CopySplitButton.js";
|
|
4
4
|
import type { RichDocumentAction, RichDocumentActionContext } from "../types.js";
|
|
5
5
|
export interface ActionBarProps {
|
|
6
6
|
actions: RichDocumentAction[];
|
|
@@ -5,7 +5,7 @@ import { ActionBar as AlchemyActionBar } from "@ai-matrx/alchemy/react/bar";
|
|
|
5
5
|
import { useRichDocumentProvider } from "../actions/useRichDocumentProvider.js";
|
|
6
6
|
import { cn } from "@ai-matrx/design-system";
|
|
7
7
|
import { AlchemyDocumentMenu } from "./shared/AlchemyDocumentMenu.js";
|
|
8
|
-
import {
|
|
8
|
+
import { ContentActions } from "../../copy/ContentActions.js";
|
|
9
9
|
import { copyRichContent } from "../../copy/copy-commands.js";
|
|
10
10
|
import { ExportPaletteAnchor, openExportPalette } from "../../copy/ExportPalette.js";
|
|
11
11
|
import { ensureExportPaletteAction } from "../actions/export-palette-action.js";
|
|
@@ -22,6 +22,11 @@ async function runDocumentCopy(actions, getCtx, flavor) {
|
|
|
22
22
|
if (action) return action.run(getCtx());
|
|
23
23
|
return copyRichContent(getCtx().content, flavor);
|
|
24
24
|
}
|
|
25
|
+
const EXPORT_ACTION_IDS = {
|
|
26
|
+
pdf: "download-pdf",
|
|
27
|
+
docx: "download-docx",
|
|
28
|
+
html: "download-html"
|
|
29
|
+
};
|
|
25
30
|
function ActionBar(props) {
|
|
26
31
|
const { actions, getCtx, target, sourceId, className, hideOverflow = false, mini = false } = props;
|
|
27
32
|
useRichDocumentProvider();
|
|
@@ -29,6 +34,15 @@ function ActionBar(props) {
|
|
|
29
34
|
const hasCopy = actions.some((action) => action.category === "copy");
|
|
30
35
|
const palette = React.useRef(null);
|
|
31
36
|
const copy = React.useCallback((flavor) => runDocumentCopy(actions, getCtx, flavor), [actions, getCtx]);
|
|
37
|
+
const exporters = React.useMemo(() => {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [format, id] of Object.entries(EXPORT_ACTION_IDS)) {
|
|
40
|
+
const action = actions.find((a) => a.id === id);
|
|
41
|
+
if (action) out[format] = () => action.run(getCtx());
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}, [actions, getCtx]);
|
|
45
|
+
const printAction = actions.find((a) => a.id === "print");
|
|
32
46
|
return /* @__PURE__ */ jsxs(
|
|
33
47
|
"div",
|
|
34
48
|
{
|
|
@@ -44,11 +58,16 @@ function ActionBar(props) {
|
|
|
44
58
|
children: [
|
|
45
59
|
hasCopy ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
60
|
/* @__PURE__ */ jsx(
|
|
47
|
-
|
|
61
|
+
ContentActions,
|
|
48
62
|
{
|
|
63
|
+
content: () => getCtx().content,
|
|
64
|
+
title: getCtx().source.type === "note" ? "Note" : void 0,
|
|
49
65
|
copy,
|
|
50
66
|
size: mini ? "xs" : "sm",
|
|
51
|
-
|
|
67
|
+
viewKey: sourceId,
|
|
68
|
+
exporters,
|
|
69
|
+
onPrint: printAction ? () => printAction.run(getCtx()) : void 0,
|
|
70
|
+
onTransform: () => void openExportPalette(sourceId)
|
|
52
71
|
}
|
|
53
72
|
),
|
|
54
73
|
/* @__PURE__ */ jsx(
|
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.52",
|
|
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",
|
|
@@ -133,18 +133,18 @@
|
|
|
133
133
|
"@ai-matrx/agents": "latest",
|
|
134
134
|
"@ai-matrx/alchemy": "latest",
|
|
135
135
|
"@ai-matrx/content-ir": "latest",
|
|
136
|
-
"@ai-matrx/
|
|
136
|
+
"@ai-matrx/canvas": "latest",
|
|
137
137
|
"@ai-matrx/data": "latest",
|
|
138
|
+
"@ai-matrx/content-ir-react": "latest",
|
|
138
139
|
"@ai-matrx/design-system": "latest",
|
|
139
|
-
"@ai-matrx/
|
|
140
|
+
"@ai-matrx/diff": "latest",
|
|
140
141
|
"@ai-matrx/icons": "latest",
|
|
141
142
|
"@ai-matrx/kit": "latest",
|
|
142
143
|
"@ai-matrx/print": "latest",
|
|
143
144
|
"@ai-matrx/records": "latest",
|
|
144
|
-
"@ai-matrx/
|
|
145
|
+
"@ai-matrx/tap-target": "latest",
|
|
145
146
|
"@ai-matrx/records-ui": "latest",
|
|
146
|
-
"@ai-matrx/
|
|
147
|
-
"@ai-matrx/tap-target": "latest"
|
|
147
|
+
"@ai-matrx/media": "latest"
|
|
148
148
|
},
|
|
149
149
|
"peerDependencies": {
|
|
150
150
|
"react": ">=19.0.0",
|