@ai-matrx/rich-content 0.2.25 → 0.2.27
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 +17 -0
- package/dist/copy/CopySplitButton.d.ts +18 -0
- package/dist/copy/CopySplitButton.js +148 -0
- package/dist/copy/copy-commands.d.ts +5 -1
- package/dist/copy/copy-commands.js +14 -7
- package/dist/display/blocks/chart/ChartBlock.js +1 -4
- package/dist/display/blocks/svg/SvgBlock.js +1 -4
- package/dist/rich-document/variants/ActionBar.d.ts +5 -0
- package/dist/rich-document/variants/ActionBar.js +36 -3
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.d.ts +7 -1
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.js +15 -3
- package/dist/selection-toolbar/SelectionToolbarFrame.js +39 -2
- package/dist/selection-toolbar/common-actions.d.ts +11 -1
- package/dist/selection-toolbar/common-actions.js +19 -7
- package/dist/selection-toolbar/selection-copy.js +0 -3
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.27
|
|
4
|
+
|
|
5
|
+
- ONE copy truth (Arman, 2026-10-04: "one click to get either the markdown version or the no-markup version, everywhere"). `copyRichContent(md, "default")` now writes EXACTLY the person's flavor (`copy.default_flavor`, default markdown) — the same bytes as that flavor's menu row; it no longer adds an HTML flavor (formatted copies live in the Alchemy palette). Every flavor is clean: `<artifact …>` wrappers are unwrapped to their body (`cleanCopyMarkdown`, `richCopyPlainText`).
|
|
6
|
+
- New `copy/CopySplitButton`: `[Copy | ⌄]` — the main part copies the default flavor, the chevron offers "Copy markdown" and "Copy plain text" (and, with `mountMore`, the Alchemy palette below them). Main and rows run the same `copy(flavor)`.
|
|
7
|
+
- The rich-document action bar leads with the split Copy; it runs the registry's `copy-markdown` / `copy-plain-text` actions (the ⋯ "Copy as" rows), so the button and the menu cannot disagree. The Alchemy palette (formatted, download, AI) moved under the chevron; its captured text is unwrapped too.
|
|
8
|
+
- The selection toolbar's FIRST control is the split Copy (copies the selection; markdown and plain text one click away), on every selection — read-only answers included (the copy rows were missing from the strip). `copySelection(target, flavor)` is the one selection copy; the strip no longer repeats the copy rows.
|
|
9
|
+
|
|
10
|
+
### Consumer action
|
|
11
|
+
|
|
12
|
+
- matrx-frontend: none required; the bar and the selection toolbar pick it up. A host with its own copy control renders `CopySplitButton` with a `copy` that ends in `copyRichContent`.
|
|
13
|
+
|
|
14
|
+
## 0.2.26
|
|
15
|
+
|
|
16
|
+
- The SVG and chart-source copies pass their failure words to the kit (`copyText(text, undefined, failureMessage)`, kit 0.30) instead of a second toast in the `false` branch: a refused copy shows ONE toast.
|
|
17
|
+
|
|
18
|
+
**Consumer action:** none.
|
|
19
|
+
|
|
3
20
|
## 0.2.25
|
|
4
21
|
|
|
5
22
|
- The server level no longer calls a client reference: `RichContentServer`, the inline/prose element maps and `DetailsElements` import `cn` from `@ai-matrx/design-system/cn` (server-safe) instead of the design-system root, whose `"use client"` stamp made `cn()` throw "Attempted to call cn() from the server" in every Server Component render (it failed matrx-frontend's production build at "Collecting page data").
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type SplitCopyFlavor = "markdown" | "text";
|
|
3
|
+
export interface CopySplitButtonProps {
|
|
4
|
+
/** Copy in one flavor. Resolve true (or nothing) when the clipboard holds it, false when it did not. */
|
|
5
|
+
copy: (flavor: SplitCopyFlavor) => Promise<boolean | void> | boolean | void;
|
|
6
|
+
/** What is copied, for the button's name ("Copy answer"). Default "Copy". */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** "sm" (32px, an action bar) or "xs" (28px, a dense strip). */
|
|
9
|
+
size?: "sm" | "xs";
|
|
10
|
+
/** Mount more copy options (the Alchemy palette) under the two rows; called with null on close. */
|
|
11
|
+
mountMore?: (element: HTMLElement | null, close: () => void) => void;
|
|
12
|
+
/** Keep the page's text selection when pressed (the selection toolbar). */
|
|
13
|
+
preserveSelection?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function CopySplitButton({ copy, label, size, mountMore, preserveSelection, disabled, className, }: CopySplitButtonProps): React.ReactElement;
|
|
18
|
+
export default CopySplitButton;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, ChevronDown, ClipboardCopy, Copy, FileCode2, Type } from "lucide-react";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger, cn } from "@ai-matrx/design-system";
|
|
6
|
+
import { defaultCopyFlavor } from "./copy-commands.js";
|
|
7
|
+
const ROWS = [
|
|
8
|
+
{ flavor: "markdown", label: "Copy markdown", icon: FileCode2 },
|
|
9
|
+
{ flavor: "text", label: "Copy plain text", icon: Type }
|
|
10
|
+
];
|
|
11
|
+
function CopySplitButton({
|
|
12
|
+
copy,
|
|
13
|
+
label = "Copy",
|
|
14
|
+
size = "sm",
|
|
15
|
+
mountMore,
|
|
16
|
+
preserveSelection = false,
|
|
17
|
+
disabled = false,
|
|
18
|
+
className
|
|
19
|
+
}) {
|
|
20
|
+
const [open, setOpen] = React.useState(false);
|
|
21
|
+
const [copied, setCopied] = React.useState(null);
|
|
22
|
+
const timer = React.useRef(null);
|
|
23
|
+
React.useEffect(() => () => {
|
|
24
|
+
if (timer.current) clearTimeout(timer.current);
|
|
25
|
+
}, []);
|
|
26
|
+
const flavor = defaultCopyFlavor();
|
|
27
|
+
const run = async (which) => {
|
|
28
|
+
const ok = await copy(which);
|
|
29
|
+
if (ok === false) return;
|
|
30
|
+
setCopied(which);
|
|
31
|
+
if (timer.current) clearTimeout(timer.current);
|
|
32
|
+
timer.current = setTimeout(() => setCopied(null), 1600);
|
|
33
|
+
};
|
|
34
|
+
const close = React.useCallback(() => setOpen(false), []);
|
|
35
|
+
const keep = preserveSelection ? (event) => event.preventDefault() : void 0;
|
|
36
|
+
const mainName = `${label} (${flavor === "text" ? "plain text" : "markdown"})`;
|
|
37
|
+
const box = size === "xs" ? "h-7" : "h-8";
|
|
38
|
+
const MainIcon = copied ? Check : size === "xs" ? ClipboardCopy : Copy;
|
|
39
|
+
return /* @__PURE__ */ jsxs(
|
|
40
|
+
"div",
|
|
41
|
+
{
|
|
42
|
+
"data-copy-split": "",
|
|
43
|
+
className: cn(
|
|
44
|
+
"inline-flex shrink-0 items-center rounded-md text-muted-foreground hover:bg-accent/40",
|
|
45
|
+
disabled && "pointer-events-none opacity-50",
|
|
46
|
+
className
|
|
47
|
+
),
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
"button",
|
|
51
|
+
{
|
|
52
|
+
type: "button",
|
|
53
|
+
"aria-label": mainName,
|
|
54
|
+
title: mainName,
|
|
55
|
+
"data-copy-split-main": flavor,
|
|
56
|
+
"data-matrx-button": "",
|
|
57
|
+
"data-tap-floor": "",
|
|
58
|
+
"data-tap-floor-square": "",
|
|
59
|
+
disabled,
|
|
60
|
+
onMouseDown: keep,
|
|
61
|
+
onClick: () => void run(flavor),
|
|
62
|
+
className: cn(
|
|
63
|
+
"inline-flex items-center justify-center rounded-l-md hover:bg-accent hover:text-foreground pointer-coarse:h-11 pointer-coarse:min-w-11",
|
|
64
|
+
box,
|
|
65
|
+
size === "xs" ? "min-w-7" : "min-w-8",
|
|
66
|
+
copied && "text-green-600 dark:text-green-400"
|
|
67
|
+
),
|
|
68
|
+
children: /* @__PURE__ */ jsx(MainIcon, { className: size === "xs" ? "h-3.5 w-3.5" : "h-4 w-4" })
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
/* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
72
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
73
|
+
"button",
|
|
74
|
+
{
|
|
75
|
+
type: "button",
|
|
76
|
+
"aria-label": "More ways to copy",
|
|
77
|
+
title: "Copy markdown or plain text",
|
|
78
|
+
"data-copy-split-more": "",
|
|
79
|
+
"data-matrx-button": "",
|
|
80
|
+
disabled,
|
|
81
|
+
onMouseDown: keep,
|
|
82
|
+
className: cn(
|
|
83
|
+
"inline-flex w-4 items-center justify-center rounded-r-md hover:bg-accent hover:text-foreground pointer-coarse:h-11 pointer-coarse:w-6",
|
|
84
|
+
box
|
|
85
|
+
),
|
|
86
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" })
|
|
87
|
+
}
|
|
88
|
+
) }),
|
|
89
|
+
/* @__PURE__ */ jsxs(
|
|
90
|
+
PopoverContent,
|
|
91
|
+
{
|
|
92
|
+
align: "start",
|
|
93
|
+
className: "w-60 p-1",
|
|
94
|
+
onOpenAutoFocus: preserveSelection ? (event) => event.preventDefault() : void 0,
|
|
95
|
+
"data-copy-split-menu": "",
|
|
96
|
+
children: [
|
|
97
|
+
/* @__PURE__ */ jsx("div", { role: "menu", "aria-label": "Copy as", children: ROWS.map(({ flavor: rowFlavor, label: rowLabel, icon: Icon }) => /* @__PURE__ */ jsxs(
|
|
98
|
+
"button",
|
|
99
|
+
{
|
|
100
|
+
type: "button",
|
|
101
|
+
role: "menuitem",
|
|
102
|
+
"data-copy-flavor": rowFlavor,
|
|
103
|
+
onMouseDown: keep,
|
|
104
|
+
onClick: () => {
|
|
105
|
+
setOpen(false);
|
|
106
|
+
void run(rowFlavor);
|
|
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(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
111
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: rowLabel }),
|
|
112
|
+
rowFlavor === flavor ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Default" }) : null
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
rowFlavor
|
|
116
|
+
)) }),
|
|
117
|
+
mountMore ? /* @__PURE__ */ jsx(MoreSlot, { mount: mountMore, close }) : null
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
] })
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function MoreSlot({ mount, close }) {
|
|
127
|
+
const ref = React.useRef(null);
|
|
128
|
+
React.useEffect(() => {
|
|
129
|
+
const element = ref.current;
|
|
130
|
+
try {
|
|
131
|
+
mount(element, close);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("[copy] more copy options could not open:", error);
|
|
134
|
+
}
|
|
135
|
+
return () => {
|
|
136
|
+
try {
|
|
137
|
+
mount(null, close);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}, [mount, close]);
|
|
142
|
+
return /* @__PURE__ */ jsx("div", { ref, "data-copy-split-more-slot": "", className: "mt-1 border-t border-border pt-1 empty:hidden" });
|
|
143
|
+
}
|
|
144
|
+
var CopySplitButton_default = CopySplitButton;
|
|
145
|
+
export {
|
|
146
|
+
CopySplitButton,
|
|
147
|
+
CopySplitButton_default as default
|
|
148
|
+
};
|
|
@@ -30,7 +30,11 @@ export interface RichCopyOptions {
|
|
|
30
30
|
/** Toast on success: a label, or false for none. Default: the flavor's label. */
|
|
31
31
|
toast?: string | false;
|
|
32
32
|
}
|
|
33
|
-
/** The
|
|
33
|
+
/** The markdown a person copies: no reasoning blocks (unless asked), no `<artifact>` wrappers. */
|
|
34
|
+
export declare function cleanCopyMarkdown(markdown: string, options?: {
|
|
35
|
+
includeThinking?: boolean;
|
|
36
|
+
}): string;
|
|
37
|
+
/** The bytes a flavor writes — pure, the one place both flavors are decided. */
|
|
34
38
|
export declare function richCopyPlainText(markdown: string, flavor: CopyFlavor, defaultFlavor?: "markdown" | "text"): string;
|
|
35
39
|
/**
|
|
36
40
|
* Write the plain flavor, or html + plain in one clipboard item (kit's `copyRich`; `html` may be a
|
|
@@ -11,7 +11,8 @@ import { showManualCopy } from "../host/manual-copy.js";
|
|
|
11
11
|
import { toast } from "../host/toast.js";
|
|
12
12
|
import { getSessionKnob } from "../host/session-knob.js";
|
|
13
13
|
import { markdownToReadableText } from "./markdown-readable-text.js";
|
|
14
|
-
import {
|
|
14
|
+
import { unwrapKindEnvelopes } from "@ai-matrx/content-ir/source";
|
|
15
|
+
import { removeThinkingContent } from "./markdown-copy-html.js";
|
|
15
16
|
async function copyContent(content, options = {}) {
|
|
16
17
|
let capturedText;
|
|
17
18
|
try {
|
|
@@ -86,11 +87,16 @@ function defaultCopyFlavor() {
|
|
|
86
87
|
const FLAVOR_TOAST = {
|
|
87
88
|
default: "Copied",
|
|
88
89
|
markdown: "Markdown copied",
|
|
89
|
-
text: "
|
|
90
|
+
text: "Plain text copied"
|
|
90
91
|
};
|
|
92
|
+
function cleanCopyMarkdown(markdown, options = {}) {
|
|
93
|
+
const source = options.includeThinking ? markdown : removeThinkingContent(markdown);
|
|
94
|
+
return unwrapKindEnvelopes(source);
|
|
95
|
+
}
|
|
91
96
|
function richCopyPlainText(markdown, flavor, defaultFlavor = "markdown") {
|
|
92
97
|
const plainFlavor = flavor === "default" ? defaultFlavor : flavor;
|
|
93
|
-
|
|
98
|
+
const clean = unwrapKindEnvelopes(markdown);
|
|
99
|
+
return plainFlavor === "text" ? markdownToReadableText(clean) : clean;
|
|
94
100
|
}
|
|
95
101
|
async function writeClipboardFlavors(plain, html) {
|
|
96
102
|
const landed = html === void 0 ? await copyText(plain) : await copyRich(
|
|
@@ -101,15 +107,16 @@ async function writeClipboardFlavors(plain, html) {
|
|
|
101
107
|
return landed;
|
|
102
108
|
}
|
|
103
109
|
async function copyRichContent(markdown, flavor = "default", options = {}) {
|
|
104
|
-
const source =
|
|
105
|
-
const
|
|
106
|
-
const ok = await writeClipboardFlavors(
|
|
107
|
-
const label = options.toast === void 0 ? FLAVOR_TOAST[
|
|
110
|
+
const source = cleanCopyMarkdown(markdown, options);
|
|
111
|
+
const resolved = flavor === "default" ? defaultCopyFlavor() : flavor;
|
|
112
|
+
const ok = await writeClipboardFlavors(richCopyPlainText(source, resolved));
|
|
113
|
+
const label = options.toast === void 0 ? FLAVOR_TOAST[resolved] : options.toast;
|
|
108
114
|
if (ok && label) toast.success(label);
|
|
109
115
|
return ok;
|
|
110
116
|
}
|
|
111
117
|
export {
|
|
112
118
|
COPY_DEFAULT_FLAVOR_KNOB,
|
|
119
|
+
cleanCopyMarkdown,
|
|
113
120
|
copyContent,
|
|
114
121
|
copyRichContent,
|
|
115
122
|
defaultCopyFlavor,
|
|
@@ -37,10 +37,7 @@ const ChartBlock = ({ content = "", isStreamActive = false, className }) => {
|
|
|
37
37
|
const presentation = useCanvasPresentation();
|
|
38
38
|
const paneLayout = chartPaneLayout(presentation, spec?.type ?? "");
|
|
39
39
|
const handleCopy = async () => {
|
|
40
|
-
if (!await copyText(source))
|
|
41
|
-
toast.error("Couldn't copy the chart source");
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
40
|
+
if (!await copyText(source, void 0, "Couldn't copy the chart source")) return;
|
|
44
41
|
setCopied(true);
|
|
45
42
|
setTimeout(() => setCopied(false), 1500);
|
|
46
43
|
};
|
|
@@ -40,10 +40,7 @@ const SvgBlock = ({ content = "", isStreamActive = false, className }) => {
|
|
|
40
40
|
const [copied, setCopied] = useState(false);
|
|
41
41
|
const [fullscreen, setFullscreen] = useState(false);
|
|
42
42
|
const handleCopy = async () => {
|
|
43
|
-
if (!await copyText(svg))
|
|
44
|
-
toast.error("Couldn't copy the SVG");
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
43
|
+
if (!await copyText(svg, void 0, "Couldn't copy the SVG")) return;
|
|
47
44
|
setCopied(true);
|
|
48
45
|
setTimeout(() => setCopied(false), 1500);
|
|
49
46
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { ClickTarget } from "@ai-matrx/alchemy/actions";
|
|
3
|
+
import { type SplitCopyFlavor } from "../../copy/CopySplitButton.js";
|
|
3
4
|
import type { RichDocumentAction, RichDocumentActionContext } from "../types.js";
|
|
4
5
|
export interface ActionBarProps {
|
|
5
6
|
actions: RichDocumentAction[];
|
|
@@ -13,5 +14,9 @@ export interface ActionBarProps {
|
|
|
13
14
|
/** Condensed presentation (the "mini-bar" variant). */
|
|
14
15
|
mini?: boolean;
|
|
15
16
|
}
|
|
17
|
+
/** The registry rows the split Copy runs, by flavor (features/rich-document/actions/handlers/transfer.ts). */
|
|
18
|
+
export declare const SPLIT_COPY_ACTION_IDS: Readonly<Record<SplitCopyFlavor, string>>;
|
|
19
|
+
/** Copy the document in one flavor through the SAME action its ⋯ "Copy as" row runs. */
|
|
20
|
+
export declare function runDocumentCopy(actions: readonly RichDocumentAction[], getCtx: () => RichDocumentActionContext, flavor: SplitCopyFlavor): Promise<boolean | void>;
|
|
16
21
|
export declare function ActionBar(props: ActionBarProps): React.ReactElement;
|
|
17
22
|
export default ActionBar;
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
import { ActionBar as AlchemyActionBar } from "@ai-matrx/alchemy/react/bar";
|
|
4
5
|
import { useRichDocumentProvider } from "../actions/useRichDocumentProvider.js";
|
|
5
6
|
import { cn } from "@ai-matrx/design-system";
|
|
6
7
|
import { AlchemyDocumentMenu } from "./shared/AlchemyDocumentMenu.js";
|
|
8
|
+
import { CopySplitButton } from "../../copy/CopySplitButton.js";
|
|
9
|
+
import { copyRichContent } from "../../copy/copy-commands.js";
|
|
7
10
|
import { OpenOneMenuButton, useOneMenuFor } from "./shared/OpenOneMenuButton.js";
|
|
8
11
|
import { StandaloneOneMenu } from "./shared/StandaloneOneMenu.js";
|
|
9
12
|
const BAR_RESTRICT = { exclude: ["copy"] };
|
|
13
|
+
const SPLIT_COPY_ACTION_IDS = {
|
|
14
|
+
markdown: "copy-markdown",
|
|
15
|
+
text: "copy-plain-text"
|
|
16
|
+
};
|
|
17
|
+
async function runDocumentCopy(actions, getCtx, flavor) {
|
|
18
|
+
const action = actions.find((a) => a.id === SPLIT_COPY_ACTION_IDS[flavor]);
|
|
19
|
+
if (action) return action.run(getCtx());
|
|
20
|
+
return copyRichContent(getCtx().content, flavor);
|
|
21
|
+
}
|
|
10
22
|
function ActionBar(props) {
|
|
11
23
|
const { actions, getCtx, target, sourceId, className, hideOverflow = false, mini = false } = props;
|
|
12
24
|
useRichDocumentProvider();
|
|
13
25
|
const oneMenu = useOneMenuFor(getCtx().source);
|
|
14
26
|
const hasCopy = actions.some((action) => action.category === "copy");
|
|
27
|
+
const palette = React.useRef(null);
|
|
28
|
+
const copy = React.useCallback((flavor) => runDocumentCopy(actions, getCtx, flavor), [actions, getCtx]);
|
|
29
|
+
const mountMore = React.useCallback((element, close) => {
|
|
30
|
+
palette.current?.mountPalette(element, close);
|
|
31
|
+
}, []);
|
|
15
32
|
return /* @__PURE__ */ jsxs(
|
|
16
33
|
"div",
|
|
17
34
|
{
|
|
@@ -25,7 +42,21 @@ function ActionBar(props) {
|
|
|
25
42
|
className
|
|
26
43
|
),
|
|
27
44
|
children: [
|
|
28
|
-
hasCopy ? /* @__PURE__ */
|
|
45
|
+
hasCopy ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
+
/* @__PURE__ */ jsx(CopySplitButton, { copy, size: mini ? "xs" : "sm", mountMore }),
|
|
47
|
+
/* @__PURE__ */ jsx(
|
|
48
|
+
AlchemyDocumentMenu,
|
|
49
|
+
{
|
|
50
|
+
getCtx,
|
|
51
|
+
sourceId,
|
|
52
|
+
size: mini ? "xs" : "sm",
|
|
53
|
+
triggerHidden: true,
|
|
54
|
+
controllerRef: palette,
|
|
55
|
+
richCopy: copy
|
|
56
|
+
},
|
|
57
|
+
sourceId
|
|
58
|
+
)
|
|
59
|
+
] }) : null,
|
|
29
60
|
/* @__PURE__ */ jsx(
|
|
30
61
|
AlchemyActionBar,
|
|
31
62
|
{
|
|
@@ -44,5 +75,7 @@ function ActionBar(props) {
|
|
|
44
75
|
var ActionBar_default = ActionBar;
|
|
45
76
|
export {
|
|
46
77
|
ActionBar,
|
|
47
|
-
|
|
78
|
+
SPLIT_COPY_ACTION_IDS,
|
|
79
|
+
ActionBar_default as default,
|
|
80
|
+
runDocumentCopy
|
|
48
81
|
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { type ContentTransferController } from "@ai-matrx/alchemy/react/workspace";
|
|
2
3
|
import type { RichDocumentActionContext } from "../../types.js";
|
|
3
|
-
export declare function AlchemyDocumentMenu({ getCtx, sourceId, size, }: {
|
|
4
|
+
export declare function AlchemyDocumentMenu({ getCtx, sourceId, size, triggerHidden, controllerRef, richCopy, }: {
|
|
4
5
|
getCtx: () => RichDocumentActionContext;
|
|
5
6
|
sourceId: string;
|
|
6
7
|
size?: "sm" | "xs";
|
|
8
|
+
/** No trigger of its own: the host mounts the palette (the split Copy's chevron). */
|
|
9
|
+
triggerHidden?: boolean;
|
|
10
|
+
controllerRef?: React.Ref<ContentTransferController>;
|
|
11
|
+
/** THE copy of this document, by flavor — the palette's copy tiles run it (never the raw text). */
|
|
12
|
+
richCopy?: (flavor: "markdown" | "text") => Promise<boolean | void> | boolean | void;
|
|
7
13
|
}): React.JSX.Element;
|
|
@@ -3,12 +3,16 @@ import { jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useAlchemyDisclosure } from "../../../host/app-bindings.js";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { ContentTransferMenu } from "@ai-matrx/alchemy/react/workspace";
|
|
6
|
+
import { unwrapKindEnvelopes } from "@ai-matrx/content-ir/source";
|
|
6
7
|
import { directSource } from "@ai-matrx/alchemy/operate";
|
|
7
8
|
import { removeThinkingContent } from "@ai-matrx/print/markdown";
|
|
8
9
|
function AlchemyDocumentMenu({
|
|
9
10
|
getCtx,
|
|
10
11
|
sourceId,
|
|
11
|
-
size = "sm"
|
|
12
|
+
size = "sm",
|
|
13
|
+
triggerHidden = false,
|
|
14
|
+
controllerRef,
|
|
15
|
+
richCopy
|
|
12
16
|
}) {
|
|
13
17
|
useAlchemyDisclosure();
|
|
14
18
|
const [hasThinking, setHasThinking] = React.useState(false);
|
|
@@ -24,8 +28,9 @@ function AlchemyDocumentMenu({
|
|
|
24
28
|
throw new Error(
|
|
25
29
|
"The document changed. Reopen Alchemy Menu for the current document."
|
|
26
30
|
);
|
|
27
|
-
const
|
|
28
|
-
setHasThinking(
|
|
31
|
+
const unthought = removeThinkingContent(ctx.content);
|
|
32
|
+
setHasThinking(unthought !== ctx.content);
|
|
33
|
+
const text = unwrapKindEnvelopes(unthought);
|
|
29
34
|
return directSource(
|
|
30
35
|
{ kind: "markdown", text },
|
|
31
36
|
{
|
|
@@ -65,6 +70,13 @@ function AlchemyDocumentMenu({
|
|
|
65
70
|
source,
|
|
66
71
|
label: "Document",
|
|
67
72
|
className: size === "xs" ? "matrx-alchemy-xs" : void 0,
|
|
73
|
+
...triggerHidden ? { triggerHidden: true } : {},
|
|
74
|
+
...controllerRef ? { controllerRef } : {},
|
|
75
|
+
...richCopy ? {
|
|
76
|
+
// The split button carries Copy markdown / Copy plain text; the palette keeps the rest.
|
|
77
|
+
richCopy: (flavor) => richCopy(flavor === "text" ? "text" : "markdown"),
|
|
78
|
+
richCopyFlavors: []
|
|
79
|
+
} : {},
|
|
68
80
|
variants: hasThinking ? [
|
|
69
81
|
{
|
|
70
82
|
id: "include-thinking",
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { cn } from "@ai-matrx/design-system";
|
|
5
6
|
import { SelectionToolbar } from "@ai-matrx/alchemy/react/selection";
|
|
7
|
+
import { CopySplitButton } from "../copy/CopySplitButton.js";
|
|
8
|
+
import { SELECTION_COPY_IDS, canCopySelection, copySelection } from "./common-actions.js";
|
|
9
|
+
const STRIP_RESTRICT = { exclude: [...SELECTION_COPY_IDS] };
|
|
6
10
|
const GAP = 8;
|
|
7
11
|
const EDGE = 8;
|
|
8
12
|
function scrollBoundsOf(node) {
|
|
@@ -177,7 +181,40 @@ function SelectionToolbarFrame({
|
|
|
177
181
|
children: panel
|
|
178
182
|
}
|
|
179
183
|
)
|
|
180
|
-
) :
|
|
184
|
+
) : (
|
|
185
|
+
// ONE row: the split Copy first (copies the selection; markdown and
|
|
186
|
+
// plain text one click away), then the Alchemy strip (formatting while
|
|
187
|
+
// editing, highlight / comment / AI while reading).
|
|
188
|
+
/* @__PURE__ */ jsxs(
|
|
189
|
+
"div",
|
|
190
|
+
{
|
|
191
|
+
role: "toolbar",
|
|
192
|
+
"aria-label": "Selection actions",
|
|
193
|
+
"data-selection-strip": "",
|
|
194
|
+
className: cn(
|
|
195
|
+
"flex items-center gap-0.5 rounded-lg border border-border bg-popover p-0.5 shadow-md",
|
|
196
|
+
"[&>[data-alchemy-layout]]:border-0 [&>[data-alchemy-layout]]:bg-transparent [&>[data-alchemy-layout]]:p-0 [&>[data-alchemy-layout]]:shadow-none",
|
|
197
|
+
docked && "max-w-full"
|
|
198
|
+
),
|
|
199
|
+
children: [
|
|
200
|
+
canCopySelection(target) ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
201
|
+
/* @__PURE__ */ jsx(
|
|
202
|
+
CopySplitButton,
|
|
203
|
+
{
|
|
204
|
+
size: "xs",
|
|
205
|
+
label: "Copy selection",
|
|
206
|
+
preserveSelection: true,
|
|
207
|
+
copy: (flavor) => copySelection(target, flavor)
|
|
208
|
+
},
|
|
209
|
+
seq
|
|
210
|
+
),
|
|
211
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-0.5 h-4 w-px shrink-0 bg-border" })
|
|
212
|
+
] }) : null,
|
|
213
|
+
/* @__PURE__ */ jsx(SelectionToolbar, { target, content: text, restrict: STRIP_RESTRICT, className: docked ? "min-w-0" : void 0 }, seq)
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
)
|
|
181
218
|
}
|
|
182
219
|
),
|
|
183
220
|
document.body
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import type { ActionProvider } from "@ai-matrx/alchemy/actions";
|
|
1
|
+
import type { ActionProvider, ClickTarget } from "@ai-matrx/alchemy/actions";
|
|
2
|
+
/** The ids of the selection's copy rows (the toolbar's split Copy button stands for them). */
|
|
3
|
+
export declare const SELECTION_COPY_IDS: readonly ["selection:copy", "selection:copy-markdown", "selection:copy-text"];
|
|
4
|
+
/**
|
|
5
|
+
* Copy the selection in one flavor — THE selection copy: the toolbar's split
|
|
6
|
+
* button and the copy rows (right-click, More) all run this, so they write the
|
|
7
|
+
* same bytes. "default" is the person's flavor (`copy.default_flavor`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function copySelection(target: ClickTarget, flavor: "default" | "markdown" | "text"): Promise<boolean>;
|
|
10
|
+
/** Is there a selection this target can copy? */
|
|
11
|
+
export declare function canCopySelection(target: ClickTarget): boolean;
|
|
2
12
|
export declare const selectionCommonProvider: ActionProvider;
|
|
@@ -23,10 +23,24 @@ function copyEligible(id) {
|
|
|
23
23
|
function eligible(id) {
|
|
24
24
|
return (t) => commonOf(t) && !hostHalf(t, "annotation") && shownInSelectionMode(id, t) ? { status: "available" } : { status: "absent" };
|
|
25
25
|
}
|
|
26
|
+
const SELECTION_COPY_IDS = ["selection:copy", "selection:copy-markdown", "selection:copy-text"];
|
|
27
|
+
async function copySelection(target, flavor) {
|
|
28
|
+
const common = commonOf(target);
|
|
29
|
+
if (!common) return false;
|
|
30
|
+
const { copyRenderedSelection, renderedSelectionRange } = await import("./selection-copy.js");
|
|
31
|
+
const { copyRichContent, defaultCopyFlavor } = await import("../copy/copy-commands.js");
|
|
32
|
+
const resolved = flavor === "default" ? defaultCopyFlavor() : flavor;
|
|
33
|
+
const ok = renderedSelectionRange() ? await copyRenderedSelection(resolved, common.shapeText ?? common.text) : await copyRichContent(common.text, resolved, { toast: false });
|
|
34
|
+
if (ok) (await import("../host/toast.js")).toast.success(resolved === "markdown" ? "Markdown copied" : "Plain text copied");
|
|
35
|
+
return ok;
|
|
36
|
+
}
|
|
37
|
+
function canCopySelection(target) {
|
|
38
|
+
return Boolean(commonOf(target));
|
|
39
|
+
}
|
|
26
40
|
const COPY_FLAVORS = [
|
|
27
41
|
{ id: "selection:copy", label: "Copy", flavor: "default", order: 0, placement: "primary" },
|
|
28
42
|
{ id: "selection:copy-markdown", label: "Copy markdown", flavor: "markdown", order: 1, placement: "overflow" },
|
|
29
|
-
{ id: "selection:copy-text", label: "Copy text", flavor: "text", order: 2, placement: "overflow" }
|
|
43
|
+
{ id: "selection:copy-text", label: "Copy plain text", flavor: "text", order: 2, placement: "overflow" }
|
|
30
44
|
];
|
|
31
45
|
const ACTIONS = [
|
|
32
46
|
// THE one copy module (copy-commands.ts, selection-copy.ts): Copy writes
|
|
@@ -43,12 +57,7 @@ const ACTIONS = [
|
|
|
43
57
|
preserveSelection: true,
|
|
44
58
|
eligible: copyEligible(id),
|
|
45
59
|
run: async (t) => {
|
|
46
|
-
|
|
47
|
-
if (!common) return;
|
|
48
|
-
const { copyRenderedSelection, renderedSelectionRange } = await import("./selection-copy.js");
|
|
49
|
-
const { copyRichContent } = await import("../copy/copy-commands.js");
|
|
50
|
-
const ok = renderedSelectionRange() ? await copyRenderedSelection(flavor, common.shapeText ?? common.text) : await copyRichContent(common.text, flavor, { toast: false });
|
|
51
|
-
if (ok) (await import("../host/toast.js")).toast.success(flavor === "default" ? "Copied" : flavor === "markdown" ? "Markdown copied" : "Text copied");
|
|
60
|
+
await copySelection(t, flavor);
|
|
52
61
|
}
|
|
53
62
|
})
|
|
54
63
|
),
|
|
@@ -99,5 +108,8 @@ const selectionCommonProvider = {
|
|
|
99
108
|
};
|
|
100
109
|
declareSelectionProvider(selectionCommonProvider);
|
|
101
110
|
export {
|
|
111
|
+
SELECTION_COPY_IDS,
|
|
112
|
+
canCopySelection,
|
|
113
|
+
copySelection,
|
|
102
114
|
selectionCommonProvider
|
|
103
115
|
};
|
|
@@ -49,9 +49,6 @@ async function copyRenderedSelection(flavor, fallbackText) {
|
|
|
49
49
|
const range = renderedSelectionRange();
|
|
50
50
|
const html = range ? rangeHtml(range) : null;
|
|
51
51
|
const markdown = (html ? await rangeMarkdown(html) : null) ?? fallbackText;
|
|
52
|
-
if (flavor === "default" && html) {
|
|
53
|
-
return writeClipboardFlavors(richCopyPlainText(markdown, "default", defaultCopyFlavor()), html);
|
|
54
|
-
}
|
|
55
52
|
return copyRichContent(markdown, flavor, { toast: false });
|
|
56
53
|
}
|
|
57
54
|
function installRichCopyKeys() {
|
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.27",
|
|
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,20 +131,20 @@
|
|
|
131
131
|
"unist-util-visit-parents": "^6.0.2",
|
|
132
132
|
"yaml": "^2.9.1",
|
|
133
133
|
"@ai-matrx/agents": "latest",
|
|
134
|
-
"@ai-matrx/alchemy": "latest",
|
|
135
134
|
"@ai-matrx/canvas": "latest",
|
|
136
135
|
"@ai-matrx/content-ir": "latest",
|
|
137
|
-
"@ai-matrx/
|
|
136
|
+
"@ai-matrx/alchemy": "latest",
|
|
138
137
|
"@ai-matrx/data": "latest",
|
|
139
|
-
"@ai-matrx/
|
|
138
|
+
"@ai-matrx/content-ir-react": "latest",
|
|
140
139
|
"@ai-matrx/diff": "latest",
|
|
141
|
-
"@ai-matrx/icons": "latest",
|
|
142
140
|
"@ai-matrx/kit": "latest",
|
|
143
|
-
"@ai-matrx/
|
|
141
|
+
"@ai-matrx/icons": "latest",
|
|
144
142
|
"@ai-matrx/media": "latest",
|
|
143
|
+
"@ai-matrx/print": "latest",
|
|
145
144
|
"@ai-matrx/records": "latest",
|
|
146
145
|
"@ai-matrx/records-ui": "latest",
|
|
147
|
-
"@ai-matrx/tap-target": "latest"
|
|
146
|
+
"@ai-matrx/tap-target": "latest",
|
|
147
|
+
"@ai-matrx/design-system": "latest"
|
|
148
148
|
},
|
|
149
149
|
"peerDependencies": {
|
|
150
150
|
"react": ">=19.0.0",
|