@ai-matrx/rich-content 0.2.51 → 0.2.53
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 +14 -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/rich-document/RichDocument.js +29 -23
- package/dist/rich-document/RichDocumentActions.d.ts +2 -0
- package/dist/rich-document/RichDocumentActions.js +3 -2
- 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 +6 -1
- package/dist/rich-document/variants/ActionBar.js +25 -6
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.53
|
|
4
|
+
|
|
5
|
+
- `RichDocumentActions` / `ActionBar` take `contentActions={false}` for a host whose own bar already carries the content action set for the same item (a `<RichCopySplit>` beside it), so Copy, Export, Print and Transform show once.
|
|
6
|
+
|
|
7
|
+
Consumer action: none.
|
|
8
|
+
|
|
9
|
+
## 0.2.52
|
|
10
|
+
|
|
11
|
+
- `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.
|
|
12
|
+
- `copy/content-export` — default Export (the one print-grade document tree) and Print (`@ai-matrx/print/markdown`).
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
Consumer action: hide registry rows that duplicate Export / Print with `hasContentActions(ctx.instanceKey("alchemy"))`; wrap self-rendered content in `ContentView`.
|
|
16
|
+
|
|
3
17
|
## 0.2.51
|
|
4
18
|
|
|
5
19
|
- 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.
|
|
@@ -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
|
+
};
|
|
@@ -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) {
|
|
@@ -7,6 +7,8 @@ export interface RichDocumentActionsProps {
|
|
|
7
7
|
className?: string;
|
|
8
8
|
/** Hide the ⋯ overflow menu (the primary row still renders). */
|
|
9
9
|
hideOverflow?: boolean;
|
|
10
|
+
/** False only where the host's own bar already carries the content action set for this item. */
|
|
11
|
+
contentActions?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export declare function RichDocumentActions(props: RichDocumentActionsProps): React.ReactElement;
|
|
12
14
|
export default RichDocumentActions;
|
|
@@ -3,7 +3,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useActionSurfaceProvider } from "./runtime/useActionSurfaceProvider.js";
|
|
4
4
|
import { ActionBar } from "./variants/ActionBar.js";
|
|
5
5
|
function RichDocumentActions(props) {
|
|
6
|
-
const { content, source, actions, className, hideOverflow } = props;
|
|
6
|
+
const { content, source, actions, className, hideOverflow, contentActions } = props;
|
|
7
7
|
const { ctx, getCtx, resolvedActions, target } = useActionSurfaceProvider({
|
|
8
8
|
content,
|
|
9
9
|
source,
|
|
@@ -18,7 +18,8 @@ function RichDocumentActions(props) {
|
|
|
18
18
|
target,
|
|
19
19
|
sourceId: ctx.instanceKey("alchemy"),
|
|
20
20
|
className,
|
|
21
|
-
hideOverflow
|
|
21
|
+
hideOverflow,
|
|
22
|
+
contentActions
|
|
22
23
|
}
|
|
23
24
|
);
|
|
24
25
|
}
|
|
@@ -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[];
|
|
@@ -13,6 +13,11 @@ export interface ActionBarProps {
|
|
|
13
13
|
hideOverflow?: boolean;
|
|
14
14
|
/** Condensed presentation (the "mini-bar" variant). */
|
|
15
15
|
mini?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Render the content action set (default true). False ONLY where the host's own bar
|
|
18
|
+
* already carries it for the same item (a `<RichCopySplit>` beside this bar) — never twice.
|
|
19
|
+
*/
|
|
20
|
+
contentActions?: boolean;
|
|
16
21
|
}
|
|
17
22
|
/** The registry rows the split Copy runs, by flavor (features/rich-document/actions/handlers/transfer.ts). */
|
|
18
23
|
export declare const SPLIT_COPY_ACTION_IDS: Readonly<Record<SplitCopyFlavor, string>>;
|
|
@@ -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,13 +22,27 @@ 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
|
-
const { actions, getCtx, target, sourceId, className, hideOverflow = false, mini = false } = props;
|
|
31
|
+
const { actions, getCtx, target, sourceId, className, hideOverflow = false, mini = false, contentActions = true } = props;
|
|
27
32
|
useRichDocumentProvider();
|
|
28
33
|
const oneMenu = useOneMenuFor(getCtx().source);
|
|
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
|
{
|
|
@@ -43,14 +57,19 @@ function ActionBar(props) {
|
|
|
43
57
|
),
|
|
44
58
|
children: [
|
|
45
59
|
hasCopy ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
-
/* @__PURE__ */ jsx(
|
|
47
|
-
|
|
60
|
+
contentActions ? /* @__PURE__ */ jsx(
|
|
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
|
+
) : null,
|
|
54
73
|
/* @__PURE__ */ jsx(
|
|
55
74
|
AlchemyDocumentMenu,
|
|
56
75
|
{
|
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.53",
|
|
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,16 +133,16 @@
|
|
|
133
133
|
"@ai-matrx/agents": "latest",
|
|
134
134
|
"@ai-matrx/alchemy": "latest",
|
|
135
135
|
"@ai-matrx/canvas": "latest",
|
|
136
|
-
"@ai-matrx/content-ir": "latest",
|
|
137
136
|
"@ai-matrx/content-ir-react": "latest",
|
|
138
137
|
"@ai-matrx/data": "latest",
|
|
139
|
-
"@ai-matrx/
|
|
138
|
+
"@ai-matrx/content-ir": "latest",
|
|
140
139
|
"@ai-matrx/design-system": "latest",
|
|
140
|
+
"@ai-matrx/diff": "latest",
|
|
141
141
|
"@ai-matrx/kit": "latest",
|
|
142
142
|
"@ai-matrx/icons": "latest",
|
|
143
143
|
"@ai-matrx/media": "latest",
|
|
144
|
-
"@ai-matrx/print": "latest",
|
|
145
144
|
"@ai-matrx/records": "latest",
|
|
145
|
+
"@ai-matrx/print": "latest",
|
|
146
146
|
"@ai-matrx/records-ui": "latest",
|
|
147
147
|
"@ai-matrx/tap-target": "latest"
|
|
148
148
|
},
|