@ai-matrx/design-system 0.17.3 → 0.18.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -0
  3. package/dist/content-transfer/alchemy.svg +1 -0
  4. package/dist/content-transfer/icon.cjs +28 -0
  5. package/dist/content-transfer/icon.cjs.map +1 -0
  6. package/dist/content-transfer/icon.d.cts +4 -0
  7. package/dist/content-transfer/icon.d.ts +4 -0
  8. package/dist/content-transfer/icon.js +8 -0
  9. package/dist/content-transfer/icon.js.map +1 -0
  10. package/dist/content-transfer.cjs +1978 -0
  11. package/dist/content-transfer.cjs.map +1 -0
  12. package/dist/content-transfer.css +382 -0
  13. package/dist/content-transfer.d.cts +208 -0
  14. package/dist/content-transfer.d.ts +208 -0
  15. package/dist/content-transfer.js +1974 -0
  16. package/dist/content-transfer.js.map +1 -0
  17. package/dist/data-table/copy-helpers.d.cts +1 -1
  18. package/dist/data-table/copy-helpers.d.ts +1 -1
  19. package/dist/data-table/copy-types.cjs.map +1 -1
  20. package/dist/data-table/copy-types.d.cts +2 -0
  21. package/dist/data-table/copy-types.d.ts +2 -0
  22. package/dist/data-table/data-row-inspector.cjs +1603 -319
  23. package/dist/data-table/data-row-inspector.cjs.map +1 -1
  24. package/dist/data-table/data-row-inspector.js +1634 -324
  25. package/dist/data-table/data-row-inspector.js.map +1 -1
  26. package/dist/data-table/data-row-window.cjs +1531 -245
  27. package/dist/data-table/data-row-window.cjs.map +1 -1
  28. package/dist/data-table/data-row-window.js +1561 -249
  29. package/dist/data-table/data-row-window.js.map +1 -1
  30. package/dist/data-table/filter-engine.d.cts +1 -1
  31. package/dist/data-table/filter-engine.d.ts +1 -1
  32. package/dist/data-table/index.cjs +2558 -1146
  33. package/dist/data-table/index.cjs.map +1 -1
  34. package/dist/data-table/index.d.cts +2 -2
  35. package/dist/data-table/index.d.ts +2 -2
  36. package/dist/data-table/index.js +2589 -1151
  37. package/dist/data-table/index.js.map +1 -1
  38. package/dist/data-table/infer-filter.d.cts +1 -1
  39. package/dist/data-table/infer-filter.d.ts +1 -1
  40. package/dist/data-table/layered-filters.d.cts +1 -1
  41. package/dist/data-table/layered-filters.d.ts +1 -1
  42. package/dist/data-table/query-control.d.cts +1 -1
  43. package/dist/data-table/query-control.d.ts +1 -1
  44. package/dist/data-table/types.cjs.map +1 -1
  45. package/dist/data-table/types.d.cts +1 -1
  46. package/dist/data-table/types.d.ts +1 -1
  47. package/dist/data-table/url-state.d.cts +1 -1
  48. package/dist/data-table/url-state.d.ts +1 -1
  49. package/dist/data-table/xlsx.d.cts +1 -1
  50. package/dist/data-table/xlsx.d.ts +1 -1
  51. package/dist/styles.css +2 -0
  52. package/dist/{types-pZ95iFk5.d.ts → types-BhSkeZQ_.d.ts} +14 -0
  53. package/dist/{types-4fmLG5AN.d.cts → types-CD4xGL5Z.d.cts} +14 -0
  54. package/package.json +25 -2
@@ -0,0 +1,1974 @@
1
+ "use client";
2
+ "use client";
3
+
4
+ // src/content-transfer/index.tsx
5
+ import * as React8 from "react";
6
+ import {
7
+ applySectionSelection,
8
+ applyTransferLimits,
9
+ capture,
10
+ collectAllMatching,
11
+ createBrowserTransport as createBrowserTransport2,
12
+ createDraft,
13
+ directSource as directSource2,
14
+ editDraft,
15
+ projectSnapshot,
16
+ readTransferCell,
17
+ reduceJson,
18
+ resetDraft,
19
+ resolveTransferPreferences,
20
+ resolveTransferRegistry,
21
+ sealTransferArtifact as sealTransferArtifact2,
22
+ serialize,
23
+ serializeMarkdownRich,
24
+ transformRows,
25
+ undoDraft
26
+ } from "@ai-matrx/kit/content-transfer";
27
+
28
+ // src/button.tsx
29
+ import { Slot } from "@radix-ui/react-slot";
30
+ import { cva } from "class-variance-authority";
31
+ import * as React from "react";
32
+
33
+ // src/cn.ts
34
+ import { clsx } from "clsx";
35
+ import { twMerge } from "tailwind-merge";
36
+ function cn(...inputs) {
37
+ return twMerge(clsx(inputs));
38
+ }
39
+
40
+ // src/button.tsx
41
+ import { jsx } from "react/jsx-runtime";
42
+ var buttonVariants = cva(
43
+ "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
44
+ {
45
+ variants: {
46
+ variant: {
47
+ default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
48
+ /**
49
+ * An explicit spelling of `default`, identical by construction. It is
50
+ * not decoration: matrx-frontend's `ButtonMine` fork carried it, ~17
51
+ * call sites say `variant="primary"`, and the alternative to keeping
52
+ * the word was a host shim translating a prop — the massage C22 bans.
53
+ */
54
+ primary: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
55
+ destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
56
+ /** The affirmative twin of `destructive` — confirm, approve, publish. */
57
+ success: "bg-success text-success-foreground shadow-sm hover:bg-success/90",
58
+ outline: "border border-border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground",
59
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
60
+ ghost: "hover:bg-accent hover:text-accent-foreground",
61
+ link: "text-primary underline-offset-4 hover:underline",
62
+ subtle: "border border-transparent bg-muted/50 text-foreground hover:border-border hover:bg-muted"
63
+ },
64
+ /**
65
+ * ONE size scale, xs → 3xl, plus the two icon boxes. The top and bottom
66
+ * of the scale came in from matrx-frontend's `ButtonMine` fork (2026-09-07):
67
+ * a dense toolbar needs `xs`, and a marketing/hero call to action needs
68
+ * `xl`+. A host that has to reach outside this scale forks the button,
69
+ * which is exactly how `ButtonMine` was born.
70
+ */
71
+ size: {
72
+ xs: "h-6 rounded-md px-1 text-xs",
73
+ sm: "h-8 rounded-md px-3 text-xs",
74
+ default: "h-9 px-4 py-2",
75
+ /** An explicit spelling of `default` (the fork's middle rung). */
76
+ md: "h-9 px-4 py-2",
77
+ lg: "h-10 rounded-md px-6",
78
+ xl: "h-12 px-8 py-3 text-base",
79
+ "2xl": "h-14 px-10 py-3 text-base",
80
+ "3xl": "h-16 px-12 py-3 text-base",
81
+ icon: "h-9 w-9",
82
+ "icon-sm": "h-7 w-7",
83
+ /** A circular icon box — an avatar-shaped control, not a square one. */
84
+ roundIcon: "h-8 w-8 rounded-full p-0"
85
+ }
86
+ },
87
+ defaultVariants: {
88
+ variant: "default",
89
+ size: "default"
90
+ }
91
+ }
92
+ );
93
+ var Button = React.forwardRef(
94
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
95
+ const Component = asChild ? Slot : "button";
96
+ return /* @__PURE__ */ jsx(
97
+ Component,
98
+ {
99
+ className: cn(buttonVariants({ variant, size, className })),
100
+ ref,
101
+ ...props
102
+ }
103
+ );
104
+ }
105
+ );
106
+ Button.displayName = "Button";
107
+
108
+ // src/dialog.tsx
109
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
110
+ import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
111
+ import { treeContainsComponent } from "@ai-matrx/kit/react-tree";
112
+ import * as React5 from "react";
113
+
114
+ // src/motion.ts
115
+ var MOTION_OVERLAY = "matrx-motion-overlay";
116
+ var MOTION_DIALOG = "matrx-motion-dialog";
117
+ var MOTION_BOTTOM_SHEET = "matrx-motion-bottom-sheet";
118
+ var MOTION_POPPER = "matrx-motion-popper";
119
+ var MOTION_SHEET = {
120
+ top: "matrx-motion-sheet-top",
121
+ bottom: "matrx-motion-sheet-bottom",
122
+ left: "matrx-motion-sheet-left",
123
+ right: "matrx-motion-sheet-right",
124
+ center: "matrx-motion-sheet-center"
125
+ };
126
+ var MOTION_PULSE = "matrx-pulse";
127
+ var MOTION_SPIN = "matrx-spin";
128
+ var ALL_MOTION_CLASSES = [
129
+ MOTION_OVERLAY,
130
+ MOTION_DIALOG,
131
+ MOTION_BOTTOM_SHEET,
132
+ MOTION_POPPER,
133
+ ...Object.values(MOTION_SHEET),
134
+ MOTION_PULSE,
135
+ MOTION_SPIN
136
+ ];
137
+
138
+ // src/icons.tsx
139
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
140
+ function lucideProps(props) {
141
+ return {
142
+ xmlns: "http://www.w3.org/2000/svg",
143
+ viewBox: "0 0 24 24",
144
+ fill: "none",
145
+ stroke: "currentColor",
146
+ strokeWidth: 2,
147
+ strokeLinecap: "round",
148
+ strokeLinejoin: "round",
149
+ "aria-hidden": true,
150
+ ...props
151
+ };
152
+ }
153
+ function XIcon(props) {
154
+ return /* @__PURE__ */ jsxs("svg", { ...lucideProps(props), children: [
155
+ /* @__PURE__ */ jsx2("path", { d: "M18 6 6 18" }),
156
+ /* @__PURE__ */ jsx2("path", { d: "m6 6 12 12" })
157
+ ] });
158
+ }
159
+ function radixProps(props) {
160
+ return {
161
+ xmlns: "http://www.w3.org/2000/svg",
162
+ viewBox: "0 0 15 15",
163
+ width: 15,
164
+ height: 15,
165
+ fill: "none",
166
+ "aria-hidden": true,
167
+ ...props
168
+ };
169
+ }
170
+ function CheckIcon(props) {
171
+ return /* @__PURE__ */ jsx2("svg", { ...radixProps(props), children: /* @__PURE__ */ jsx2(
172
+ "path",
173
+ {
174
+ d: "M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z",
175
+ fill: "currentColor",
176
+ fillRule: "evenodd",
177
+ clipRule: "evenodd"
178
+ }
179
+ ) });
180
+ }
181
+ function DotFilledIcon(props) {
182
+ return /* @__PURE__ */ jsx2("svg", { ...radixProps(props), children: /* @__PURE__ */ jsx2(
183
+ "path",
184
+ {
185
+ d: "M9.875 7.5C9.875 8.81168 8.81168 9.875 7.5 9.875C6.18832 9.875 5.125 8.81168 5.125 7.5C5.125 6.18832 6.18832 5.125 7.5 5.125C8.81168 5.125 9.875 6.18832 9.875 7.5Z",
186
+ fill: "currentColor"
187
+ }
188
+ ) });
189
+ }
190
+ function RadixChevronRightIcon(props) {
191
+ return /* @__PURE__ */ jsx2("svg", { ...radixProps(props), children: /* @__PURE__ */ jsx2(
192
+ "path",
193
+ {
194
+ d: "M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z",
195
+ fill: "currentColor",
196
+ fillRule: "evenodd",
197
+ clipRule: "evenodd"
198
+ }
199
+ ) });
200
+ }
201
+
202
+ // src/portal-container.tsx
203
+ import * as React2 from "react";
204
+ import { jsx as jsx3 } from "react/jsx-runtime";
205
+ var CONTEXT_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.design-system.portal-container-context");
206
+ var contextRegistry = globalThis;
207
+ var PortalContainerContext = contextRegistry[CONTEXT_SLOT] ??= React2.createContext(void 0);
208
+ function PortalContainerProvider({
209
+ container,
210
+ children
211
+ }) {
212
+ return /* @__PURE__ */ jsx3(PortalContainerContext.Provider, { value: container ?? void 0, children });
213
+ }
214
+ function usePortalContainer(explicit) {
215
+ const injected = React2.useContext(PortalContainerContext);
216
+ if (explicit !== void 0) {
217
+ return explicit ?? void 0;
218
+ }
219
+ return injected ?? void 0;
220
+ }
221
+
222
+ // src/radix-dialog-modal-context.tsx
223
+ import * as React3 from "react";
224
+ import { jsx as jsx4 } from "react/jsx-runtime";
225
+ var CONTEXT_SLOT2 = /* @__PURE__ */ Symbol.for("ai-matrx.design-system.dialog-modal-context");
226
+ var contextRegistry2 = globalThis;
227
+ var RadixDialogModalContext = contextRegistry2[CONTEXT_SLOT2] ??= React3.createContext(true);
228
+ function RadixDialogModalProvider({
229
+ children,
230
+ modal
231
+ }) {
232
+ return /* @__PURE__ */ jsx4(RadixDialogModalContext.Provider, { value: modal, children });
233
+ }
234
+ function useRadixDialogModal() {
235
+ return React3.useContext(RadixDialogModalContext);
236
+ }
237
+
238
+ // src/use-is-mobile.ts
239
+ import * as React4 from "react";
240
+ var MOBILE_BREAKPOINT = 768;
241
+ function useIsMobile() {
242
+ const [isMobile, setIsMobile] = React4.useState(false);
243
+ const [hasMounted, setHasMounted] = React4.useState(false);
244
+ React4.useEffect(() => {
245
+ setHasMounted(true);
246
+ const onChange = () => {
247
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
248
+ };
249
+ onChange();
250
+ if (typeof window.matchMedia !== "function") {
251
+ return void 0;
252
+ }
253
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
254
+ mql.addEventListener("change", onChange);
255
+ return () => mql.removeEventListener("change", onChange);
256
+ }, []);
257
+ if (!hasMounted) {
258
+ return false;
259
+ }
260
+ return isMobile;
261
+ }
262
+
263
+ // src/dialog.tsx
264
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
265
+ var Dialog = ({
266
+ children,
267
+ ...props
268
+ }) => {
269
+ const modal = props.modal ?? true;
270
+ return /* @__PURE__ */ jsx5(RadixDialogModalProvider, { modal, children: /* @__PURE__ */ jsx5(DialogPrimitive.Root, { ...props, modal, children }) });
271
+ };
272
+ Dialog.displayName = "Dialog";
273
+ var DialogPortal = ({
274
+ container,
275
+ ...props
276
+ }) => {
277
+ const resolved = usePortalContainer(container);
278
+ return /* @__PURE__ */ jsx5(DialogPrimitive.Portal, { container: resolved ?? null, ...props });
279
+ };
280
+ DialogPortal.displayName = "DialogPortal";
281
+ var CONTEXT_SLOT3 = /* @__PURE__ */ Symbol.for("ai-matrx.design-system.dialog-container-context");
282
+ var contextRegistry3 = globalThis;
283
+ var DialogContainerContext = contextRegistry3[CONTEXT_SLOT3] ??= React5.createContext(null);
284
+ var DialogOverlay = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
285
+ DialogPrimitive.Overlay,
286
+ {
287
+ ref,
288
+ "data-slot": "dialog-overlay",
289
+ className: cn(
290
+ // Page context behind the modal stays readable; separation comes from a
291
+ // light token scrim, not a blur.
292
+ "fixed inset-0 z-[10000] bg-[var(--matrx-overlay-scrim-soft)]",
293
+ MOTION_OVERLAY,
294
+ className
295
+ ),
296
+ ...props
297
+ }
298
+ ));
299
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
300
+ var DialogContentPrimitive = React5.forwardRef((props, ref) => {
301
+ const isModal = useRadixDialogModal();
302
+ return /* @__PURE__ */ jsx5(
303
+ DialogPrimitive.Content,
304
+ {
305
+ ...props,
306
+ ref,
307
+ "aria-modal": isModal || void 0
308
+ }
309
+ );
310
+ });
311
+ DialogContentPrimitive.displayName = "DialogContentPrimitive";
312
+ var DIALOG_DESKTOP_CLASSES = "matrx-dialog-card fixed inset-0 z-[10000] m-auto grid h-fit min-w-0 w-full max-w-lg gap-4 max-h-[85dvh] overflow-y-auto overscroll-contain [--dialog-pad:1.5rem] overflow-x-clip border bg-background p-6 shadow-lg [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full sm:rounded-lg";
313
+ var DIALOG_MOBILE_SHEET_CLASSES = "matrx-mobile-sheet matrx-dialog-card fixed inset-x-0 bottom-0 left-0 right-0 top-auto z-[10000] flex min-w-0 flex-col w-full max-w-full max-h-[90dvh] gap-4 [--dialog-pad:1rem] overflow-x-clip border-t bg-background p-4 pb-safe shadow-lg [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full rounded-t-2xl rounded-b-none overflow-y-auto overscroll-contain";
314
+ var DIALOG_MOBILE_SHEET_OVERRIDE = "inset-x-0 bottom-0 left-0 right-0 top-auto translate-none w-full max-w-full max-h-[90dvh] rounded-b-none rounded-t-2xl overflow-y-auto";
315
+ var DialogContent = React5.forwardRef(
316
+ ({
317
+ className,
318
+ children,
319
+ mobileSheet = true,
320
+ animated = true,
321
+ showCloseButton = true,
322
+ container,
323
+ ...props
324
+ }, ref) => {
325
+ const isMobile = useIsMobile();
326
+ const isModal = useRadixDialogModal();
327
+ const injectedContainer = usePortalContainer(container);
328
+ const asSheet = mobileSheet && isMobile;
329
+ const [containerEl, setContainerEl] = React5.useState(
330
+ null
331
+ );
332
+ const mergedRef = React5.useCallback(
333
+ (node) => {
334
+ setContainerEl(node);
335
+ if (typeof ref === "function") ref(node);
336
+ else if (ref)
337
+ ref.current = node;
338
+ },
339
+ [ref]
340
+ );
341
+ const hasTitle = treeContainsComponent(children, DialogTitle) || treeContainsComponent(children, DialogPrimitive.Title);
342
+ const hasDescription = treeContainsComponent(children, DialogDescription) || treeContainsComponent(children, DialogPrimitive.Description);
343
+ return /* @__PURE__ */ jsxs2(DialogPortal, { container, children: [
344
+ isModal ? /* @__PURE__ */ jsx5(DialogOverlay, {}) : null,
345
+ /* @__PURE__ */ jsxs2(
346
+ DialogContentPrimitive,
347
+ {
348
+ ref: mergedRef,
349
+ "data-slot": "dialog-content",
350
+ className: cn(
351
+ asSheet ? DIALOG_MOBILE_SHEET_CLASSES : DIALOG_DESKTOP_CLASSES,
352
+ animated && (asSheet ? MOTION_BOTTOM_SHEET : MOTION_DIALOG),
353
+ className,
354
+ asSheet && DIALOG_MOBILE_SHEET_OVERRIDE,
355
+ // A non-modal dialog is the coexistence contract for content that
356
+ // can sit beside a host's floating window manager (which starts at
357
+ // z=1000); staying below that boundary makes a newly focused window
358
+ // usable without coupling the two systems.
359
+ !isModal && "z-[900]"
360
+ ),
361
+ ...hasDescription ? {} : { "aria-describedby": void 0 },
362
+ ...props,
363
+ children: [
364
+ hasTitle ? null : /* @__PURE__ */ jsx5(VisuallyHidden.Root, { children: /* @__PURE__ */ jsx5(DialogPrimitive.Title, { children: "Dialog" }) }),
365
+ /* @__PURE__ */ jsx5(DialogContainerContext.Provider, { value: containerEl, children: /* @__PURE__ */ jsx5(
366
+ PortalContainerProvider,
367
+ {
368
+ container: containerEl ?? injectedContainer ?? null,
369
+ children
370
+ }
371
+ ) }),
372
+ showCloseButton ? /* @__PURE__ */ jsxs2(
373
+ DialogPrimitive.Close,
374
+ {
375
+ "data-slot": "dialog-close",
376
+ "aria-label": "Close",
377
+ className: "absolute right-2 top-4 flex h-11 w-11 items-center justify-center rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground sm:right-4 lg:h-10 lg:w-10",
378
+ children: [
379
+ /* @__PURE__ */ jsx5(XIcon, { className: "h-4 w-4" }),
380
+ /* @__PURE__ */ jsx5("span", { className: "sr-only", children: "Close" })
381
+ ]
382
+ }
383
+ ) : null
384
+ ]
385
+ }
386
+ )
387
+ ] });
388
+ }
389
+ );
390
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
391
+ var DialogHeader = ({
392
+ className,
393
+ ...props
394
+ }) => /* @__PURE__ */ jsx5(
395
+ "div",
396
+ {
397
+ "data-slot": "dialog-header",
398
+ className: cn(
399
+ // DialogContent owns an absolute close control. Reserve its hit area in
400
+ // every header so trailing actions never render underneath it.
401
+ "flex flex-col space-y-1.5 pr-12 text-center sm:text-left",
402
+ className
403
+ ),
404
+ ...props
405
+ }
406
+ );
407
+ DialogHeader.displayName = "DialogHeader";
408
+ var DialogFooter = ({
409
+ className,
410
+ ...props
411
+ }) => /* @__PURE__ */ jsx5(
412
+ "div",
413
+ {
414
+ "data-slot": "dialog-footer",
415
+ className: cn(
416
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
417
+ // Ruling 3/4 — sticky to the card edges so a scrolled body never shows
418
+ // through under the actions, and safe-area padded on a phone.
419
+ //
420
+ // 🚨 THERE IS NO `mb-[…]` HERE, AND THERE MUST NEVER BE ONE AGAIN
421
+ // (0.11.3, feedback bf8fb00b). The bleed over the card's bottom padding
422
+ // used to be a NEGATIVE BLOCK MARGIN, which makes this box taller than
423
+ // the grid area the card allocates it — and `position: sticky` CLAMPS a
424
+ // box to its containing block, so the browser yanked the footer UP by
425
+ // `--dialog-pad` against the card's `gap-4` and painted 8px of opaque
426
+ // footer over the last line of the consequence, at every length. The
427
+ // card now RESERVES this footer's block size instead (`matrx-dialog-card`
428
+ // in styles.css hands over its `padding-bottom`), so the bleed costs no
429
+ // margin at all. Guard: `sticky-footer-geometry.test.tsx`.
430
+ //
431
+ // These stay UTILITIES rather than a shipped rule so a caller's
432
+ // `className` still wins through tailwind-merge — a shipped rule would
433
+ // have to be unlayered to survive Tailwind preflight (§ THE PREFLIGHT
434
+ // RULE in styles.css) and would then override every caller's padding.
435
+ "sticky bottom-0 z-10 bg-background pt-3",
436
+ "mx-[calc(var(--dialog-pad,0px)*-1)] px-[var(--dialog-pad,0px)] pb-[var(--dialog-pad,0px)]",
437
+ className
438
+ ),
439
+ ...props
440
+ }
441
+ );
442
+ DialogFooter.displayName = "DialogFooter";
443
+ var DialogTitle = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
444
+ DialogPrimitive.Title,
445
+ {
446
+ ref,
447
+ "data-slot": "dialog-title",
448
+ className: cn("text-lg font-semibold leading-none tracking-tight", className),
449
+ ...props
450
+ }
451
+ ));
452
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
453
+ var DialogDescription = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
454
+ DialogPrimitive.Description,
455
+ {
456
+ ref,
457
+ "data-slot": "dialog-description",
458
+ className: cn("text-sm text-muted-foreground", className),
459
+ ...props
460
+ }
461
+ ));
462
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
463
+
464
+ // src/dropdown-menu.tsx
465
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
466
+ import * as React6 from "react";
467
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
468
+ var DropdownMenu = DropdownMenuPrimitive.Root;
469
+ var DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
470
+ var MENU_SURFACE_CLASS = "z-[10001] min-w-[8rem] max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto overflow-x-hidden overscroll-contain rounded-md border bg-popover p-1 text-popover-foreground";
471
+ var MENU_ITEM_CLASS = "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
472
+ var MENU_INDICATOR_ITEM_CLASS = "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50";
473
+ var DropdownMenuSubTrigger = React6.forwardRef(({ className, inset, children, ...props }, ref) => /* @__PURE__ */ jsxs3(
474
+ DropdownMenuPrimitive.SubTrigger,
475
+ {
476
+ ref,
477
+ "data-slot": "dropdown-menu-sub-trigger",
478
+ className: cn(
479
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
480
+ inset && "pl-8",
481
+ className
482
+ ),
483
+ ...props,
484
+ children: [
485
+ children,
486
+ /* @__PURE__ */ jsx6(RadixChevronRightIcon, { className: "ml-auto h-4 w-4" })
487
+ ]
488
+ }
489
+ ));
490
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
491
+ var DropdownMenuSubContent = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx6(
492
+ DropdownMenuPrimitive.SubContent,
493
+ {
494
+ ref,
495
+ "data-slot": "dropdown-menu-sub-content",
496
+ className: cn(MENU_SURFACE_CLASS, "shadow-lg", MOTION_POPPER, className),
497
+ ...props
498
+ }
499
+ ));
500
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
501
+ var DropdownMenuContent = React6.forwardRef(({ className, sideOffset = 4, container, ...props }, ref) => {
502
+ const portalContainer = usePortalContainer(container);
503
+ return /* @__PURE__ */ jsx6(DropdownMenuPrimitive.Portal, { container: portalContainer, children: /* @__PURE__ */ jsx6(
504
+ DropdownMenuPrimitive.Content,
505
+ {
506
+ ref,
507
+ "data-slot": "dropdown-menu-content",
508
+ sideOffset,
509
+ className: cn(
510
+ MENU_SURFACE_CLASS,
511
+ "shadow-md",
512
+ MOTION_POPPER,
513
+ className
514
+ ),
515
+ ...props
516
+ }
517
+ ) });
518
+ });
519
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
520
+ var DropdownMenuItem = React6.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx6(
521
+ DropdownMenuPrimitive.Item,
522
+ {
523
+ ref,
524
+ "data-slot": "dropdown-menu-item",
525
+ className: cn(MENU_ITEM_CLASS, inset && "pl-8", className),
526
+ ...props
527
+ }
528
+ ));
529
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
530
+ var DropdownMenuCheckboxItem = React6.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs3(
531
+ DropdownMenuPrimitive.CheckboxItem,
532
+ {
533
+ ref,
534
+ "data-slot": "dropdown-menu-checkbox-item",
535
+ className: cn(MENU_INDICATOR_ITEM_CLASS, className),
536
+ ...props,
537
+ children: [
538
+ /* @__PURE__ */ jsx6("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx6(DropdownMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx6(CheckIcon, { className: "h-4 w-4" }) }) }),
539
+ children
540
+ ]
541
+ }
542
+ ));
543
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
544
+ var DropdownMenuRadioItem = React6.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs3(
545
+ DropdownMenuPrimitive.RadioItem,
546
+ {
547
+ ref,
548
+ "data-slot": "dropdown-menu-radio-item",
549
+ className: cn(MENU_INDICATOR_ITEM_CLASS, className),
550
+ ...props,
551
+ children: [
552
+ /* @__PURE__ */ jsx6("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx6(DropdownMenuPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx6(DotFilledIcon, { className: "h-4 w-4 fill-current" }) }) }),
553
+ children
554
+ ]
555
+ }
556
+ ));
557
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
558
+ var DropdownMenuLabel = React6.forwardRef(({ className, inset, ...props }, ref) => /* @__PURE__ */ jsx6(
559
+ DropdownMenuPrimitive.Label,
560
+ {
561
+ ref,
562
+ "data-slot": "dropdown-menu-label",
563
+ className: cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className),
564
+ ...props
565
+ }
566
+ ));
567
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
568
+ var DropdownMenuSeparator = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx6(
569
+ DropdownMenuPrimitive.Separator,
570
+ {
571
+ ref,
572
+ "data-slot": "dropdown-menu-separator",
573
+ className: cn("-mx-1 my-1 h-px bg-muted", className),
574
+ ...props
575
+ }
576
+ ));
577
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
578
+ var DropdownMenuShortcut = ({
579
+ className,
580
+ ...props
581
+ }) => /* @__PURE__ */ jsx6(
582
+ "span",
583
+ {
584
+ "data-slot": "dropdown-menu-shortcut",
585
+ className: cn("ml-auto text-xs tracking-widest opacity-60", className),
586
+ ...props
587
+ }
588
+ );
589
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
590
+
591
+ // src/content-transfer/icon.ts
592
+ var ALCHEMY_GLYPH_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 5.5h8v11h-8z"/><path d="M8.5 9.5h8v9h-8z"/><path d="m18.5 3 .65 1.85L21 5.5l-1.85.65L18.5 8l-.65-1.85L16 5.5l1.85-.65z"/></svg>';
593
+
594
+ // src/content-transfer/matrx-copy.tsx
595
+ import * as React7 from "react";
596
+ import {
597
+ buildAgentPayload,
598
+ createBrowserTransport,
599
+ directSource,
600
+ normalizeTransferJson,
601
+ sealTransferArtifact
602
+ } from "@ai-matrx/kit/content-transfer";
603
+ import { Fragment, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
604
+ var resolve = (value) => typeof value === "function" ? value() : value;
605
+ var asPayload = (value) => ({
606
+ kind: "markdown",
607
+ text: typeof value === "string" ? value : buildAgentPayload(value)
608
+ });
609
+ var structuredPayload = (value) => {
610
+ if (typeof value === "string") return { kind: "markdown", text: value };
611
+ const clean = (record) => Object.fromEntries(
612
+ Object.entries(record ?? {}).filter(([, entry]) => entry !== void 0)
613
+ );
614
+ return {
615
+ kind: "json",
616
+ value: normalizeTransferJson({
617
+ kind: value.kind,
618
+ location: value.location,
619
+ description: value.description,
620
+ data: value.data,
621
+ ...value.summary !== void 0 ? { summary: value.summary } : {},
622
+ attributes: clean(value.attributes),
623
+ context: clean(value.context)
624
+ })
625
+ };
626
+ };
627
+ var actionDone = (message) => ({
628
+ status: "success",
629
+ delivered: "action",
630
+ mimeTypes: [],
631
+ message
632
+ });
633
+ var sourceFor = (id, label, build) => ({
634
+ id,
635
+ label,
636
+ capture: async ({ signal }) => {
637
+ signal.throwIfAborted();
638
+ const payload = await build();
639
+ signal.throwIfAborted();
640
+ return directSource(payload, { sourceId: id, label });
641
+ }
642
+ });
643
+ function MatrxCopyMenu(props) {
644
+ return /* @__PURE__ */ jsx7(MatrxCopyMenuSession, { ...props }, props.sourceId ?? props.label);
645
+ }
646
+ function MatrxCopyMenuSession(props) {
647
+ const instance = React7.useId();
648
+ const controller = React7.useRef(null);
649
+ const capabilities = useContentTransferCapabilities();
650
+ const transport = capabilities.transport ?? createBrowserTransport();
651
+ const [modal, setModal] = React7.useState(null);
652
+ const [configuration, setConfiguration] = React7.useState(null);
653
+ const copyVisible = !props.hide?.includes("copy");
654
+ const aiVisible = !props.hide?.includes("ai");
655
+ const exportVisible = !props.hide?.includes("export");
656
+ const make = (suffix, build, label = props.label) => sourceFor(
657
+ `${props.sourceId ?? `${instance}:${props.label}`}:${suffix}`,
658
+ label,
659
+ build
660
+ );
661
+ const human = copyVisible && props.human !== void 0 ? make("human", () => ({ kind: "text", text: resolve(props.human) })) : void 0;
662
+ const humanMarkdown = copyVisible && props.human !== void 0 ? make("human-markdown", () => ({
663
+ kind: "markdown",
664
+ text: resolve(props.human)
665
+ })) : void 0;
666
+ const json = aiVisible && props.json !== void 0 ? make("json", () => ({
667
+ kind: "json",
668
+ value: normalizeTransferJson(resolve(props.json))
669
+ })) : void 0;
670
+ const agent = aiVisible && props.agent !== void 0 ? make("agent", () => asPayload(resolve(props.agent))) : void 0;
671
+ const structuredAgent = aiVisible && props.agent !== void 0 ? make("structured-agent", () => structuredPayload(resolve(props.agent))) : void 0;
672
+ const table = exportVisible && props.export?.sheetRows ? make("rows", () => {
673
+ const value = normalizeTransferJson(props.export.sheetRows());
674
+ if (!Array.isArray(value) || value.some(
675
+ (row) => row === null || typeof row !== "object" || Array.isArray(row)
676
+ ))
677
+ throw new Error("Sheet rows must be JSON objects.");
678
+ const rows = value;
679
+ const keys = [...new Set(rows.flatMap((row) => Object.keys(row)))];
680
+ return {
681
+ kind: "rows",
682
+ rows,
683
+ columns: keys.map((key) => ({
684
+ id: key,
685
+ label: key,
686
+ path: `/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`,
687
+ visible: true
688
+ }))
689
+ };
690
+ }) : void 0;
691
+ const builderVariants = aiVisible ? (props.aiVariants ?? []).filter(
692
+ (variant) => variant.build && !variant.onSelect && !variant.modal
693
+ ).map((variant) => ({
694
+ ...variant,
695
+ copyLabel: variant.label,
696
+ source: make(
697
+ `variant:${variant.id}`,
698
+ async () => asPayload(await variant.build()),
699
+ variant.label
700
+ )
701
+ })) : [];
702
+ const faithful = agent ? [
703
+ {
704
+ id: props.agentVariant?.id ?? "faithful",
705
+ label: props.agentVariant?.label ?? "Full AI context",
706
+ source: agent,
707
+ copyLabel: props.agentVariant?.label ?? "Copy for AI",
708
+ ...props.agentVariant?.hint ? { hint: props.agentVariant.hint } : {}
709
+ }
710
+ ] : [];
711
+ const variants = props.agentVariant?.position === "last" ? [...builderVariants, ...faithful] : [...faithful, ...builderVariants];
712
+ const actions = [];
713
+ if (aiVisible)
714
+ for (const variant of props.aiVariants ?? []) {
715
+ if (!variant.onSelect && !variant.modal) continue;
716
+ actions.push({
717
+ ...variant,
718
+ id: `variant-action:${variant.id}`,
719
+ label: variant.label,
720
+ supports: () => true,
721
+ run: async () => {
722
+ if (variant.onSelect) await variant.onSelect();
723
+ else setModal(variant);
724
+ return actionDone(`Opened ${variant.label}`);
725
+ }
726
+ });
727
+ }
728
+ if (aiVisible && props.aiCustom)
729
+ actions.push({
730
+ id: "configure-source",
731
+ label: props.aiCustom.label ?? "Customize source\u2026",
732
+ ...props.aiCustom.hint ? { hint: props.aiCustom.hint } : {},
733
+ supports: () => true,
734
+ run: async () => {
735
+ setConfiguration({ custom: props.aiCustom });
736
+ return actionDone("Source options opened");
737
+ }
738
+ });
739
+ if (aiVisible && props.groomer)
740
+ actions.push({
741
+ id: "groom-source",
742
+ label: "Customize sections\u2026",
743
+ supports: () => true,
744
+ run: async () => {
745
+ setConfiguration({ groomer: props.groomer() });
746
+ return actionDone("Section options opened");
747
+ }
748
+ });
749
+ if (exportVisible)
750
+ for (const item of props.export?.items ?? [])
751
+ actions.push({
752
+ id: `export:${item.id}`,
753
+ label: item.label,
754
+ supports: () => true,
755
+ ...item.build && !item.onSelect ? {
756
+ source: make(
757
+ `export:${item.id}`,
758
+ async () => {
759
+ const file = item.build();
760
+ const bytes = new Uint8Array(
761
+ await new Blob([file.content], {
762
+ type: file.mime
763
+ }).arrayBuffer()
764
+ );
765
+ return {
766
+ kind: "registered",
767
+ format: "matrx-file",
768
+ value: {
769
+ bytes: Array.from(bytes),
770
+ mime: file.mime,
771
+ extension: file.extension
772
+ }
773
+ };
774
+ },
775
+ item.label
776
+ )
777
+ } : {},
778
+ run: async ({ snapshot, draft, signal }) => {
779
+ if (item.onSelect) {
780
+ await item.onSelect();
781
+ return actionDone(`Opened ${item.label}`);
782
+ }
783
+ const value = draft.payload.kind === "registered" ? draft.payload.value : null;
784
+ if (!value || Array.isArray(value) || typeof value !== "object" || !Array.isArray(value.bytes) || typeof value.mime !== "string" || typeof value.extension !== "string" || value.bytes.some(
785
+ (byte) => typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255
786
+ ))
787
+ throw new Error(`${item.label} did not capture a valid file.`);
788
+ if (draft.omissions.some((omission) => omission.reason !== "duplicate"))
789
+ throw new Error(
790
+ "This custom file exceeds the configured export limits or contains excluded content. Use a standard format or revise the source options."
791
+ );
792
+ const bytes = new Uint8Array(value.bytes);
793
+ const filename = `${props.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "export"}.${value.extension.replace(/[^a-zA-Z0-9.-]/g, "") || "bin"}`;
794
+ return transport.download(
795
+ sealTransferArtifact({
796
+ snapshotId: snapshot.id,
797
+ draftRevision: draft.revision,
798
+ format: `export:${item.id}`,
799
+ plainText: "",
800
+ file: { filename, mime: value.mime, bytes },
801
+ omissions: [...draft.omissions]
802
+ }),
803
+ signal
804
+ );
805
+ }
806
+ });
807
+ if (exportVisible && table && props.sendToSheet)
808
+ actions.push({
809
+ id: "send-google-sheet",
810
+ label: "Send to Google Sheet",
811
+ source: table,
812
+ supports: () => true,
813
+ run: async ({ draft }) => {
814
+ if (draft.payload.kind !== "rows")
815
+ throw new Error("Google Sheets requires captured rows.");
816
+ return props.sendToSheet(draft.payload.rows, props.label);
817
+ }
818
+ });
819
+ const source = json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
820
+ if (!source && !actions.length) return null;
821
+ const target = source ?? make("actions", () => ({ kind: "text", text: props.label }));
822
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
823
+ /* @__PURE__ */ jsx7(
824
+ ContentTransferMenu,
825
+ {
826
+ source: target,
827
+ controllerRef: controller,
828
+ label: props.label,
829
+ variants,
830
+ actions,
831
+ hide: [
832
+ ...!copyVisible || !source ? ["copy"] : [],
833
+ ...!aiVisible || !source ? ["prepare"] : [],
834
+ ...!exportVisible || !source ? ["download"] : []
835
+ ],
836
+ formatSources: {
837
+ ...human ? { plain: human, markdown: humanMarkdown, html: humanMarkdown } : {},
838
+ ...json ? { json, "compact-json": json } : {},
839
+ ...table ? { csv: table, tsv: table } : {}
840
+ },
841
+ ...props.disabled !== void 0 ? { disabled: props.disabled } : {},
842
+ className: cn(
843
+ props.className,
844
+ props.size === "xs" && "matrx-alchemy-xs",
845
+ props.size === "sm" && "matrx-alchemy-sm"
846
+ ),
847
+ ...props.stopPropagation !== void 0 ? { stopPropagation: props.stopPropagation } : {}
848
+ }
849
+ ),
850
+ modal?.modal?.({
851
+ open: true,
852
+ onOpenChange: (open) => {
853
+ if (!open) setModal(null);
854
+ }
855
+ }),
856
+ configuration ? /* @__PURE__ */ jsx7(
857
+ SourceConfiguration,
858
+ {
859
+ restoreFocus: () => controller.current?.focusTrigger(),
860
+ configuration,
861
+ onClose: () => setConfiguration(null),
862
+ onPrepare: async (payload, label) => {
863
+ if (!controller.current)
864
+ throw new Error("Alchemy is no longer mounted.");
865
+ await controller.current.prepare(payload, {
866
+ label,
867
+ id: `${instance}:configured:${crypto.randomUUID()}`
868
+ });
869
+ setConfiguration(null);
870
+ }
871
+ },
872
+ `${instance}:configuration`
873
+ ) : null
874
+ ] });
875
+ }
876
+ function SourceConfiguration({
877
+ restoreFocus,
878
+ configuration,
879
+ onClose,
880
+ onPrepare
881
+ }) {
882
+ const custom = configuration.custom;
883
+ const handingOff = React7.useRef(false);
884
+ const groomer = configuration.groomer;
885
+ const [values, setValues] = React7.useState(
886
+ () => Object.fromEntries(
887
+ custom?.options.map((option) => [option.key, option.default]) ?? []
888
+ )
889
+ );
890
+ const [sections, setSections] = React7.useState(
891
+ () => Object.fromEntries(
892
+ groomer?.sections.map((section) => [
893
+ section.id,
894
+ section.defaultSelection ?? "full"
895
+ ]) ?? []
896
+ )
897
+ );
898
+ const [error, setError] = React7.useState(null);
899
+ const [busy, setBusy] = React7.useState(false);
900
+ const title = custom?.dialogTitle ?? groomer?.label ?? "Customize source";
901
+ let prepared = null;
902
+ let buildError = null;
903
+ let text = "";
904
+ try {
905
+ if (custom) {
906
+ const result = custom.build(values);
907
+ prepared = custom.wrap(result.text, values, result.meta);
908
+ } else if (groomer) {
909
+ const data = {};
910
+ const omitted = [];
911
+ for (const section of groomer.sections) {
912
+ const level = sections[section.id] ?? "full";
913
+ if (level === "off") omitted.push(section.id);
914
+ else {
915
+ const value = section.build(level);
916
+ if (value !== void 0 && value !== null) data[section.id] = value;
917
+ }
918
+ }
919
+ prepared = {
920
+ kind: groomer.kind,
921
+ location: groomer.location,
922
+ description: groomer.description,
923
+ data,
924
+ ...groomer.summary ? { summary: groomer.summary } : {},
925
+ attributes: {
926
+ ...groomer.attributes,
927
+ dropped_sections: omitted.join(",")
928
+ },
929
+ ...groomer.context ? { context: groomer.context } : {}
930
+ };
931
+ }
932
+ text = prepared ? buildAgentPayload(prepared) : "";
933
+ } catch (cause) {
934
+ buildError = cause instanceof Error ? cause.message : "Could not prepare this source.";
935
+ }
936
+ return /* @__PURE__ */ jsx7(
937
+ Dialog,
938
+ {
939
+ open: true,
940
+ onOpenChange: (open) => {
941
+ if (!open && !busy) onClose();
942
+ },
943
+ children: /* @__PURE__ */ jsxs4(
944
+ DialogContent,
945
+ {
946
+ onCloseAutoFocus: (event) => {
947
+ event.preventDefault();
948
+ if (!handingOff.current) restoreFocus();
949
+ },
950
+ className: "matrx-alchemy-workspace",
951
+ children: [
952
+ /* @__PURE__ */ jsxs4(DialogHeader, { children: [
953
+ /* @__PURE__ */ jsx7(DialogTitle, { children: title }),
954
+ /* @__PURE__ */ jsx7(DialogDescription, { children: custom?.dialogDescription ?? "Choose the source detail, then prepare, edit, summarize, copy or export it in Alchemy." })
955
+ ] }),
956
+ /* @__PURE__ */ jsxs4("div", { className: "matrx-alchemy-main matrx-alchemy-toolbar", children: [
957
+ custom?.options.map((option) => /* @__PURE__ */ jsxs4("label", { children: [
958
+ option.label,
959
+ "hint" in option && option.hint ? /* @__PURE__ */ jsx7("small", { children: option.hint }) : null,
960
+ option.kind === "toggle" ? /* @__PURE__ */ jsx7(
961
+ "input",
962
+ {
963
+ type: "checkbox",
964
+ checked: Boolean(values[option.key]),
965
+ onChange: (event) => setValues({ ...values, [option.key]: event.target.checked })
966
+ }
967
+ ) : option.kind === "preset" ? /* @__PURE__ */ jsx7(
968
+ "select",
969
+ {
970
+ value: Number(values[option.key]),
971
+ onChange: (event) => setValues({
972
+ ...values,
973
+ [option.key]: Number(event.target.value)
974
+ }),
975
+ children: option.options.map((choice) => /* @__PURE__ */ jsx7("option", { value: choice.value, children: choice.label }, choice.value))
976
+ }
977
+ ) : option.kind === "slider" ? /* @__PURE__ */ jsxs4(Fragment, { children: [
978
+ /* @__PURE__ */ jsx7(
979
+ "input",
980
+ {
981
+ type: "range",
982
+ min: 0,
983
+ max: Math.max(0, option.stops.length - 1),
984
+ step: 1,
985
+ value: option.stops.indexOf(Number(values[option.key])),
986
+ onChange: (event) => setValues({
987
+ ...values,
988
+ [option.key]: option.stops[Number(event.target.value)]
989
+ })
990
+ }
991
+ ),
992
+ /* @__PURE__ */ jsx7("span", { children: option.formatValue(Number(values[option.key])) })
993
+ ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
994
+ /* @__PURE__ */ jsx7(
995
+ "input",
996
+ {
997
+ type: "number",
998
+ min: option.min,
999
+ step: option.step,
1000
+ value: Number(values[option.key]),
1001
+ onChange: (event) => setValues({
1002
+ ...values,
1003
+ [option.key]: Number(event.target.value)
1004
+ })
1005
+ }
1006
+ ),
1007
+ option.presets?.map((preset) => /* @__PURE__ */ jsx7(
1008
+ Button,
1009
+ {
1010
+ type: "button",
1011
+ variant: "ghost",
1012
+ onClick: () => setValues({ ...values, [option.key]: preset.value }),
1013
+ children: preset.label
1014
+ },
1015
+ preset.value
1016
+ ))
1017
+ ] })
1018
+ ] }, option.key)),
1019
+ groomer ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1020
+ /* @__PURE__ */ jsx7("div", { children: ["full", "compact", "brief"].map((level) => /* @__PURE__ */ jsx7(
1021
+ Button,
1022
+ {
1023
+ type: "button",
1024
+ variant: "ghost",
1025
+ onClick: () => setSections(
1026
+ Object.fromEntries(
1027
+ groomer.sections.map((section) => [
1028
+ section.id,
1029
+ level === "brief" && section.cuttable ? "off" : level
1030
+ ])
1031
+ )
1032
+ ),
1033
+ children: level
1034
+ },
1035
+ level
1036
+ )) }),
1037
+ groomer.sections.map((section) => /* @__PURE__ */ jsxs4("label", { children: [
1038
+ section.title,
1039
+ /* @__PURE__ */ jsxs4(
1040
+ "select",
1041
+ {
1042
+ value: sections[section.id],
1043
+ onChange: (event) => setSections({
1044
+ ...sections,
1045
+ [section.id]: event.target.value
1046
+ }),
1047
+ children: [
1048
+ ["full", "compact", "brief"].map((level) => /* @__PURE__ */ jsx7("option", { value: level, children: section.levelLabels?.[level] ?? level }, level)),
1049
+ section.cuttable ? /* @__PURE__ */ jsx7("option", { value: "off", children: "Exclude" }) : null
1050
+ ]
1051
+ }
1052
+ ),
1053
+ section.description ? /* @__PURE__ */ jsx7("span", { children: section.description }) : null
1054
+ ] }, section.id))
1055
+ ] }) : null
1056
+ ] }),
1057
+ /* @__PURE__ */ jsxs4("p", { children: [
1058
+ text.length.toLocaleString(),
1059
+ " characters \xB7 approximately",
1060
+ " ",
1061
+ Math.ceil(text.length / 4).toLocaleString(),
1062
+ " tokens"
1063
+ ] }),
1064
+ error || buildError ? /* @__PURE__ */ jsx7("p", { role: "alert", children: error ?? buildError }) : null,
1065
+ /* @__PURE__ */ jsxs4(DialogFooter, { children: [
1066
+ /* @__PURE__ */ jsx7(
1067
+ Button,
1068
+ {
1069
+ type: "button",
1070
+ variant: "ghost",
1071
+ disabled: busy,
1072
+ onClick: onClose,
1073
+ children: "Cancel"
1074
+ }
1075
+ ),
1076
+ /* @__PURE__ */ jsx7(
1077
+ Button,
1078
+ {
1079
+ type: "button",
1080
+ disabled: busy || !prepared || Boolean(buildError),
1081
+ onClick: async () => {
1082
+ if (!prepared) return;
1083
+ setBusy(true);
1084
+ setError(null);
1085
+ try {
1086
+ handingOff.current = true;
1087
+ await onPrepare(structuredPayload(prepared), title);
1088
+ } catch (cause) {
1089
+ handingOff.current = false;
1090
+ setError(
1091
+ cause instanceof Error ? cause.message : "Could not open Alchemy."
1092
+ );
1093
+ } finally {
1094
+ setBusy(false);
1095
+ }
1096
+ },
1097
+ children: busy ? "Preparing\u2026" : "Prepare in Alchemy"
1098
+ }
1099
+ )
1100
+ ] })
1101
+ ]
1102
+ }
1103
+ )
1104
+ }
1105
+ );
1106
+ }
1107
+ var GROOMER_LEVELS = ["full", "compact", "brief"];
1108
+ function applyGroomerPreset(preset, sections) {
1109
+ return Object.fromEntries(
1110
+ sections.map((section) => [
1111
+ section.id,
1112
+ preset === "everything" ? "full" : preset === "balanced" ? "compact" : section.cuttable ? "off" : "brief"
1113
+ ])
1114
+ );
1115
+ }
1116
+ function defaultGroomerSelections(sections) {
1117
+ return Object.fromEntries(
1118
+ sections.map((section) => [section.id, section.defaultSelection ?? "full"])
1119
+ );
1120
+ }
1121
+ function buildGroomerPresetPayload(config, preset) {
1122
+ const selections = applyGroomerPreset(preset, config.sections);
1123
+ const data = {};
1124
+ const dropped = [];
1125
+ for (const section of config.sections) {
1126
+ const selection = selections[section.id] ?? "full";
1127
+ if (selection === "off") {
1128
+ dropped.push(section.id);
1129
+ continue;
1130
+ }
1131
+ const value = section.build(selection);
1132
+ if (value !== void 0 && value !== null) data[section.id] = value;
1133
+ }
1134
+ return {
1135
+ kind: config.kind,
1136
+ location: config.location,
1137
+ description: preset === "everything" ? config.description : `${config.description} (${preset} detail)`,
1138
+ data,
1139
+ ...config.summary !== void 0 ? { summary: config.summary } : {},
1140
+ attributes: {
1141
+ ...config.attributes,
1142
+ detail: preset,
1143
+ ...dropped.length ? { dropped_sections: dropped.join(",") } : {}
1144
+ },
1145
+ ...config.context ? { context: config.context } : {}
1146
+ };
1147
+ }
1148
+ function groomerPresetVariants(getConfig) {
1149
+ return [
1150
+ {
1151
+ id: "balanced",
1152
+ label: "Balanced",
1153
+ hint: "Compact sections",
1154
+ build: () => buildGroomerPresetPayload(getConfig(), "balanced")
1155
+ },
1156
+ {
1157
+ id: "minimal",
1158
+ label: "Minimal",
1159
+ hint: "Brief sections with optional detail excluded",
1160
+ build: () => buildGroomerPresetPayload(getConfig(), "minimal")
1161
+ }
1162
+ ];
1163
+ }
1164
+
1165
+ // src/content-transfer/index.tsx
1166
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1167
+ function AlchemyGlyph(props) {
1168
+ return /* @__PURE__ */ jsxs5("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...props, children: [
1169
+ /* @__PURE__ */ jsx8("path", { d: "M4.5 5.5h8v11h-8z" }),
1170
+ /* @__PURE__ */ jsx8("path", { d: "M8.5 9.5h8v9h-8z" }),
1171
+ /* @__PURE__ */ jsx8("path", { d: "m18.5 3 .65 1.85L21 5.5l-1.85.65L18.5 8l-.65-1.85L16 5.5l1.85-.65z" })
1172
+ ] });
1173
+ }
1174
+ var CapabilityContext = React8.createContext({});
1175
+ function ContentTransferCapabilitiesProvider({ value, children }) {
1176
+ return /* @__PURE__ */ jsx8(CapabilityContext.Provider, { value, children });
1177
+ }
1178
+ var useContentTransferCapabilities = () => React8.useContext(CapabilityContext);
1179
+ var SurfaceContext = React8.createContext(null);
1180
+ function ContentTransferSurfaceProvider({ handle, children }) {
1181
+ return /* @__PURE__ */ jsx8(SurfaceContext.Provider, { value: handle, children });
1182
+ }
1183
+ var useContentTransferSurface = () => React8.useContext(SurfaceContext);
1184
+ function MenuPresentation({ label, hint, icon: Icon }) {
1185
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1186
+ Icon ? /* @__PURE__ */ jsx8(Icon, { className: "matrx-alchemy-menu-icon" }) : null,
1187
+ /* @__PURE__ */ jsxs5("span", { children: [
1188
+ label,
1189
+ hint ? /* @__PURE__ */ jsx8("small", { className: "matrx-alchemy-menu-hint", children: hint }) : null
1190
+ ] })
1191
+ ] });
1192
+ }
1193
+ var LABEL = { plain: "Plain text", markdown: "Markdown", html: "Formatted", json: "JSON", "compact-json": "Compact JSON", csv: "CSV", tsv: "Google Sheets", xlsx: "Excel workbook" };
1194
+ var BUILT_INS = Object.freeze(Object.keys(LABEL).map((id) => ({ id })));
1195
+ var COPIES = ["plain", "markdown", "html", "json", "compact-json", "csv", "tsv"];
1196
+ var DOWNLOADS = [...COPIES, "xlsx"];
1197
+ var EMPTY_VARIANTS = [];
1198
+ var SCOPE_LABEL = { target: "This content", row: "This row", selected: "Selected rows", view: "Current view", loaded: "Loaded rows", "all-matching": "All matching rows" };
1199
+ var filenameFor = (label) => label.toLocaleLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "export";
1200
+ var estimateTokens = (text) => Math.ceil(text.length / 4);
1201
+ var errorMessage = (error, fallback) => error instanceof Error ? error.message : fallback;
1202
+ var isAbort = (error) => error instanceof DOMException && error.name === "AbortError";
1203
+ var supportsBuiltIn = (format, payload) => !["csv", "tsv", "xlsx"].includes(format) || payload.kind === "rows";
1204
+ var outcomeMessage = (result) => {
1205
+ if (result.status === "success") {
1206
+ const success = result;
1207
+ return success.delivered === "action" ? success.message ?? "Action completed" : success.delivered === "download-started" ? "Download started" : success.message ?? "Copied";
1208
+ }
1209
+ return result.status === "degraded" ? `Copied as plain text: ${result.reason}` : result.status === "cancelled" ? "Cancelled" : result.message;
1210
+ };
1211
+ var materialIds = /* @__PURE__ */ new WeakMap();
1212
+ var nextMaterialId = 0;
1213
+ function materialIdentity(material) {
1214
+ if (!material) return "missing";
1215
+ if ("instanceId" in material) return `surface:${material.instanceId}`;
1216
+ if ("id" in material) return `source:${material.id}`;
1217
+ let id = materialIds.get(material);
1218
+ if (id === void 0) {
1219
+ id = ++nextMaterialId;
1220
+ materialIds.set(material, id);
1221
+ }
1222
+ return `payload:${id}`;
1223
+ }
1224
+ function useFence() {
1225
+ const mounted = React8.useRef(true);
1226
+ const nextId = React8.useRef(0);
1227
+ const active = React8.useRef(/* @__PURE__ */ new Set());
1228
+ React8.useEffect(() => {
1229
+ mounted.current = true;
1230
+ return () => {
1231
+ mounted.current = false;
1232
+ nextId.current += 1;
1233
+ active.current.forEach((c) => c.abort());
1234
+ active.current.clear();
1235
+ };
1236
+ }, []);
1237
+ const begin = React8.useCallback(() => {
1238
+ const controller = new AbortController();
1239
+ active.current.add(controller);
1240
+ return { controller, id: ++nextId.current };
1241
+ }, []);
1242
+ const finish = React8.useCallback((controller) => active.current.delete(controller), []);
1243
+ const valid = React8.useCallback((id, controller) => mounted.current && !controller.signal.aborted && nextId.current === id, []);
1244
+ const abortAll = React8.useCallback(() => {
1245
+ nextId.current += 1;
1246
+ active.current.forEach((c) => c.abort());
1247
+ active.current.clear();
1248
+ }, []);
1249
+ return { begin, finish, valid, abortAll };
1250
+ }
1251
+ async function xlsxArtifact(draft, label) {
1252
+ if (draft.payload.kind !== "rows") throw new Error("Excel workbook export requires tabular content.");
1253
+ const XLSX = await import("xlsx");
1254
+ const columns = draft.payload.columns.filter((c) => c.visible && c.exportable !== false && c.classification !== "secret" && c.classification !== "credential");
1255
+ const cells = draft.payload.rows.map((row) => columns.map((column) => {
1256
+ const value = readTransferCell(row, column);
1257
+ return value === null || ["string", "number", "boolean"].includes(typeof value) ? value : value === void 0 ? "" : JSON.stringify(value);
1258
+ }));
1259
+ const sheet = XLSX.utils.aoa_to_sheet([columns.map((c) => c.label), ...cells]);
1260
+ const book = XLSX.utils.book_new();
1261
+ XLSX.utils.book_append_sheet(book, sheet, label.replace(/[\\/?*[\]:]/g, " ").trim().slice(0, 31) || "Data");
1262
+ const written = XLSX.write(book, { type: "array", bookType: "xlsx", compression: true });
1263
+ return sealTransferArtifact2({ snapshotId: draft.snapshotId, draftRevision: draft.revision, format: "xlsx", plainText: serialize(draft, "tsv", { spreadsheetSafe: true }).plainText, file: { filename: `${filenameFor(label)}.xlsx`, mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", bytes: written instanceof Uint8Array ? written : new Uint8Array(written) }, omissions: [...draft.omissions] });
1264
+ }
1265
+ async function buildArtifact(draft, format, label) {
1266
+ if (!supportsBuiltIn(format, draft.payload)) throw new Error(`${LABEL[format]} is available for tabular content only.`);
1267
+ if (format === "xlsx") return xlsxArtifact(draft, label);
1268
+ if (format === "html" && draft.payload.kind === "markdown") return serializeMarkdownRich(draft, `${filenameFor(label)}.html`);
1269
+ const extension = format === "plain" ? "txt" : format === "compact-json" ? "json" : format;
1270
+ return serialize(draft, format, { spreadsheetSafe: format === "csv" || format === "tsv", filename: `${filenameFor(label)}.${extension}` });
1271
+ }
1272
+ function ContentTransferMenu({ source, surface, label, projection, sections, preferences, table, formatSources, variants = EMPTY_VARIANTS, actions = [], capabilities: local, disabled, enabled = true, className, stopPropagation = true, hide = [], controllerRef }) {
1273
+ const triggerRef = React8.useRef(null);
1274
+ const inherited = React8.useContext(CapabilityContext);
1275
+ const nearestSurface = React8.useContext(SurfaceContext);
1276
+ const fence = useFence();
1277
+ const [menuCapture, setMenuCapture] = React8.useState(null);
1278
+ const [menuStatus, setMenuStatus] = React8.useState("idle");
1279
+ const [workspaces, setWorkspaces] = React8.useState({});
1280
+ const [workspaceKey, setWorkspaceKey] = React8.useState(null);
1281
+ const [workspaceOpen, setWorkspaceOpen] = React8.useState(false);
1282
+ const workspaceCaptures = React8.useRef(/* @__PURE__ */ new Map());
1283
+ const externalWorkspaceKeys = React8.useRef(/* @__PURE__ */ new Set());
1284
+ const [notice, setNotice] = React8.useState(null);
1285
+ const scopes = React8.useMemo(() => table?.availableScopes ?? ["target"], [table]);
1286
+ const initialScope = table?.initialScope ?? "target";
1287
+ if (!scopes.includes(initialScope)) throw new Error("Alchemy initialScope must be included in availableScopes.");
1288
+ const primary = surface ?? source ?? nearestSurface;
1289
+ const sourceLabel = label ?? (surface?.surfaceName ?? (source && "label" in source ? source.label : nearestSurface?.surfaceName) ?? "content");
1290
+ const primaryWorkspaceKey = `primary:${materialIdentity(primary)}`;
1291
+ const nearestWorkspaceKey = source && !surface && nearestSurface ? `surface:${materialIdentity(nearestSurface)}` : null;
1292
+ const variantWorkspaceKeys = React8.useMemo(() => new Map(variants.map((variant) => [variant.id, `variant:${variant.id}:${materialIdentity(variant.source)}`])), [variants]);
1293
+ const transport = local?.transport ?? inherited.transport ?? createBrowserTransport2();
1294
+ const ai = local?.ai ?? inherited.ai;
1295
+ const providerPreferences = React8.useMemo(() => resolveTransferPreferences(inherited.preferences, local?.preferences), [inherited.preferences, local?.preferences]);
1296
+ const adapters = React8.useMemo(() => resolveTransferRegistry(BUILT_INS, inherited.formats ?? [], local?.formats ?? []).filter((x) => "build" in x), [inherited.formats, local?.formats]);
1297
+ const registeredActions = React8.useMemo(() => resolveTransferRegistry(inherited.actions ?? [], local?.actions ?? [], actions), [actions, inherited.actions, local?.actions]);
1298
+ React8.useMemo(() => resolveTransferRegistry(variants), [variants]);
1299
+ const report = React8.useCallback((result) => {
1300
+ inherited.onOutcome?.(result);
1301
+ local?.onOutcome?.(result);
1302
+ setNotice(outcomeMessage(result));
1303
+ }, [inherited, local]);
1304
+ React8.useEffect(() => {
1305
+ const valid = /* @__PURE__ */ new Set([primaryWorkspaceKey, ...nearestWorkspaceKey ? [nearestWorkspaceKey] : [], ...variantWorkspaceKeys.values(), ...externalWorkspaceKeys.current]);
1306
+ setWorkspaces((current) => Object.fromEntries(Object.entries(current).filter(([key]) => valid.has(key))));
1307
+ for (const key of workspaceCaptures.current.keys()) if (!valid.has(key)) workspaceCaptures.current.delete(key);
1308
+ if (workspaceKey && !valid.has(workspaceKey)) {
1309
+ setWorkspaceKey(null);
1310
+ setWorkspaceOpen(false);
1311
+ }
1312
+ }, [nearestWorkspaceKey, primaryWorkspaceKey, variantWorkspaceKeys, workspaceKey]);
1313
+ const captureOne = React8.useCallback(async (material, scope, signal, applyMenuProjection = false, collectPages = true) => {
1314
+ if (!material) throw new Error("Alchemy needs a source or a mounted transfer surface.");
1315
+ let sourceMaterial;
1316
+ if ("instanceId" in material) {
1317
+ if (!material.isMounted()) throw new Error("The requested transfer surface is no longer mounted.");
1318
+ const captured = await material.capture(signal);
1319
+ sourceMaterial = { id: captured.snapshot.sourceId, label: captured.snapshot.label, capture: async () => captured };
1320
+ } else if ("kind" in material) {
1321
+ sourceMaterial = applyMenuProjection ? { id: "direct", label: sourceLabel, capture: async () => directSource2(material, { label: sourceLabel, ...projection ? { projection } : {}, ...sections ? { sections } : {} }) } : material;
1322
+ } else if (applyMenuProjection && (projection || sections)) {
1323
+ sourceMaterial = {
1324
+ id: material.id,
1325
+ label: material.label,
1326
+ capture: async (request) => {
1327
+ const captured = await material.capture(request);
1328
+ return {
1329
+ ...captured,
1330
+ projection: { rules: [...captured.projection.rules, ...projection?.rules ?? []] },
1331
+ snapshot: { ...captured.snapshot, ...sections ? { sections } : {} }
1332
+ };
1333
+ }
1334
+ };
1335
+ } else {
1336
+ sourceMaterial = material;
1337
+ }
1338
+ const raw = "kind" in sourceMaterial ? directSource2(sourceMaterial) : await sourceMaterial.capture({ scope, signal });
1339
+ const constrained = resolveTransferPreferences(providerPreferences, preferences, raw.snapshot.limits);
1340
+ const result = await capture({ id: raw.snapshot.sourceId, label: raw.snapshot.label, capture: async () => ({ ...raw, snapshot: { ...raw.snapshot, limits: constrained.limits } }) }, signal, scope);
1341
+ if (scope !== "all-matching" || !collectPages) return result;
1342
+ if (!result.allMatching) throw new Error("This source cannot provide a stable all-matching export.");
1343
+ const collected = await collectAllMatching(result.allMatching, signal);
1344
+ if (result.snapshot.payload.kind !== "rows") throw new Error("All-matching export requires tabular content.");
1345
+ return { snapshot: projectSnapshot({ ...result.snapshot, payload: { kind: "rows", rows: collected.rows, columns: result.snapshot.payload.columns }, coverage: collected.coverage, omissions: [...collected.omissions] }, { rules: [] }), allMatching: result.allMatching, ...collected.error ? { collectionError: collected.error.message } : {} };
1346
+ }, [preferences, projection, providerPreferences, sections, sourceLabel]);
1347
+ const capturePrimary = React8.useCallback((scope, signal) => captureOne(primary, scope, signal, true), [captureOne, primary]);
1348
+ workspaceCaptures.current.set(primaryWorkspaceKey, capturePrimary);
1349
+ if (nearestWorkspaceKey && nearestSurface) workspaceCaptures.current.set(nearestWorkspaceKey, (scope, signal) => captureOne(nearestSurface, scope, signal, true));
1350
+ for (const variant of variants) {
1351
+ const key = variantWorkspaceKeys.get(variant.id);
1352
+ workspaceCaptures.current.set(key, (scope, signal) => captureOne(variant.source, scope, signal, true));
1353
+ }
1354
+ const preflight = React8.useCallback(async () => {
1355
+ const { controller, id } = fence.begin();
1356
+ setMenuCapture(null);
1357
+ setMenuStatus("loading");
1358
+ try {
1359
+ const result = await captureOne(primary, initialScope, controller.signal, true, false);
1360
+ if (fence.valid(id, controller)) {
1361
+ setMenuCapture(result);
1362
+ setMenuStatus("idle");
1363
+ }
1364
+ } catch (error) {
1365
+ if (fence.valid(id, controller)) {
1366
+ setMenuCapture(null);
1367
+ setMenuStatus(errorMessage(error, "Could not inspect content"));
1368
+ }
1369
+ } finally {
1370
+ fence.finish(controller);
1371
+ }
1372
+ }, [captureOne, fence, initialScope, primary]);
1373
+ const transfer = React8.useCallback(async (format, download, alternate) => {
1374
+ const { controller, id } = fence.begin();
1375
+ try {
1376
+ const explicit = alternate?.source ?? (typeof format === "string" && format !== "xlsx" ? formatSources?.[format] : void 0);
1377
+ const result = await (explicit ? captureOne(explicit, initialScope, controller.signal, true) : capturePrimary(initialScope, controller.signal));
1378
+ if (result.collectionError) throw new Error(`The complete source could not be collected: ${result.collectionError}`);
1379
+ if (result.snapshot.coverage.status !== "complete" || result.snapshot.omissions.length > 0) {
1380
+ const reason = result.snapshot.coverage.reason ?? `${result.snapshot.omissions.length} omission${result.snapshot.omissions.length === 1 ? "" : "s"}`;
1381
+ throw new Error(`Quick transfer is partial (${reason}). Open Prepare for AI to review it before delivery.`);
1382
+ }
1383
+ const resolved = resolveTransferPreferences(providerPreferences, preferences, result.snapshot.limits);
1384
+ const draft = applyTransferLimits(createDraft(result.snapshot), resolved.limits);
1385
+ const revision = draft.revision;
1386
+ if (draft.omissions.length > result.snapshot.omissions.length) throw new Error("Quick transfer requires content reductions. Open Prepare for AI to review them before delivery.");
1387
+ const artifact = typeof format === "string" ? await buildArtifact(draft, format, result.snapshot.label) : await format.build(draft, controller.signal);
1388
+ if (artifact.omissions.length > draft.omissions.length) throw new Error(`Quick format requires ${artifact.omissions.length - draft.omissions.length} adjustment${artifact.omissions.length - draft.omissions.length === 1 ? "" : "s"}. Open Prepare for AI to review before delivery.`);
1389
+ if (!fence.valid(id, controller) || artifact.snapshotId !== result.snapshot.id || artifact.draftRevision !== revision) return;
1390
+ const outcome = await transport[download ? "download" : "copy"](artifact, controller.signal);
1391
+ if (fence.valid(id, controller)) report(outcome.status === "success" && alternate?.successMessage ? { ...outcome, message: alternate.successMessage } : outcome);
1392
+ } catch (error) {
1393
+ if (fence.valid(id, controller)) report({ status: "error", code: "prepare-failed", message: errorMessage(error, "Preparation failed"), retryable: true });
1394
+ } finally {
1395
+ fence.finish(controller);
1396
+ }
1397
+ }, [captureOne, capturePrimary, fence, formatSources, initialScope, preferences, providerPreferences, report, transport]);
1398
+ const openWorkspace = React8.useCallback(async (key = primaryWorkspaceKey, variant) => {
1399
+ const variantMaterial = variant && "kind" in variant.source ? { id: `variant:${key}`, label: variant.label, capture: async () => directSource2(variant.source, { label: variant.label }) } : variant?.source;
1400
+ const captureWorkspace = variantMaterial ? (scope, signal) => captureOne(variantMaterial, scope, signal, true) : capturePrimary;
1401
+ workspaceCaptures.current.set(key, captureWorkspace);
1402
+ if (workspaces[key]) {
1403
+ setWorkspaceKey(key);
1404
+ setWorkspaceOpen(true);
1405
+ return;
1406
+ }
1407
+ const { controller, id } = fence.begin();
1408
+ try {
1409
+ const result = await captureWorkspace(initialScope, controller.signal);
1410
+ if (!fence.valid(id, controller)) return;
1411
+ const resolved = resolveTransferPreferences(providerPreferences, preferences, result.snapshot.limits);
1412
+ const original = createDraft(result.snapshot);
1413
+ const base = { ...result, key, original, draft: original, scope: initialScope, preferences: resolved, ...result.collectionError ? { error: result.collectionError } : {} };
1414
+ setWorkspaces((current) => ({ ...current, [key]: { ...base, draft: rebuild(base, initialControls(base)) } }));
1415
+ setWorkspaceKey(key);
1416
+ setWorkspaceOpen(true);
1417
+ } catch (error) {
1418
+ if (fence.valid(id, controller)) setNotice(errorMessage(error, "Could not capture content"));
1419
+ } finally {
1420
+ fence.finish(controller);
1421
+ }
1422
+ }, [captureOne, capturePrimary, fence, initialScope, preferences, primaryWorkspaceKey, providerPreferences, workspaces]);
1423
+ React8.useImperativeHandle(controllerRef, () => ({
1424
+ focusTrigger: () => triggerRef.current?.focus(),
1425
+ prepare: async (requestedSource, options) => {
1426
+ const requestedLabel = options?.label ?? ("label" in requestedSource ? requestedSource.label : "content");
1427
+ const requestedKey = `controller:${options?.id ?? "source"}:${materialIdentity(requestedSource)}`;
1428
+ externalWorkspaceKeys.current.add(requestedKey);
1429
+ await openWorkspace(requestedKey, { source: requestedSource, label: requestedLabel });
1430
+ }
1431
+ }), [openWorkspace]);
1432
+ const runAction = React8.useCallback(async (action) => {
1433
+ const { controller, id } = fence.begin();
1434
+ try {
1435
+ const actionSource = action.source;
1436
+ const { snapshot } = actionSource ? await captureOne(actionSource, initialScope, controller.signal, true) : await capturePrimary(initialScope, controller.signal);
1437
+ const resolved = resolveTransferPreferences(providerPreferences, preferences, snapshot.limits);
1438
+ const draft = applyTransferLimits(createDraft(snapshot), resolved.limits);
1439
+ if (!action.supports(snapshot)) throw new Error(`${action.label} is unavailable for this content.`);
1440
+ const result = await action.run({ snapshot, draft, signal: controller.signal });
1441
+ if (fence.valid(id, controller) && draft.snapshotId === snapshot.id) report(result);
1442
+ } catch (error) {
1443
+ if (fence.valid(id, controller)) report({ status: "error", code: "action-failed", message: errorMessage(error, "Action failed"), retryable: true });
1444
+ } finally {
1445
+ fence.finish(controller);
1446
+ }
1447
+ }, [captureOne, capturePrimary, fence, initialScope, preferences, providerPreferences, report]);
1448
+ if (!enabled) return null;
1449
+ const payload = menuCapture?.snapshot.payload ?? (source && "kind" in source ? source : void 0);
1450
+ const quickVariants = (section) => variants.filter((v) => v.copyLabel && (v.section ?? "ai") === section).map((v) => /* @__PURE__ */ jsx8(DropdownMenuItem, { "aria-label": v.ariaLabel, onSelect: () => void transfer("markdown", false, v), children: /* @__PURE__ */ jsx8(MenuPresentation, { ...v, label: v.copyLabel }) }, `copy:${v.id}`));
1451
+ const visibleCopies = COPIES.filter((f) => Boolean(formatSources?.[f]) || !["csv", "tsv"].includes(f) || payload?.kind === "rows");
1452
+ const visibleDownloads = DOWNLOADS.filter((f) => f !== "xlsx" && Boolean(formatSources?.[f]) || !["csv", "tsv", "xlsx"].includes(f) || payload?.kind === "rows");
1453
+ const visibleAdapters = menuCapture ? adapters.filter((a) => a.supports(menuCapture.snapshot.payload)) : [];
1454
+ const visibleActions = menuCapture ? registeredActions.filter((a) => a.supports(menuCapture.snapshot)) : [];
1455
+ return /* @__PURE__ */ jsxs5("span", { className: cn("matrx-alchemy", className), onClick: stopPropagation ? (event) => event.stopPropagation() : void 0, children: [
1456
+ /* @__PURE__ */ jsxs5(DropdownMenu, { onOpenChange: (open) => {
1457
+ if (open) void preflight();
1458
+ }, children: [
1459
+ /* @__PURE__ */ jsx8(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx8("button", { ref: triggerRef, type: "button", className: "matrx-alchemy-trigger", disabled, "aria-label": `Copy, transform or export ${sourceLabel}`, title: "Alchemy \u2014 copy, transform, export", children: /* @__PURE__ */ jsx8(AlchemyGlyph, {}) }) }),
1460
+ /* @__PURE__ */ jsxs5(DropdownMenuContent, { align: "end", className: "matrx-alchemy-menu", children: [
1461
+ menuStatus === "loading" ? /* @__PURE__ */ jsx8(DropdownMenuItem, { disabled: true, children: "Inspecting content\u2026" }) : menuStatus !== "idle" ? /* @__PURE__ */ jsx8(DropdownMenuItem, { disabled: true, children: menuStatus }) : null,
1462
+ !hide.includes("copy") ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1463
+ /* @__PURE__ */ jsx8(DropdownMenuLabel, { children: "Copy" }),
1464
+ quickVariants("copy"),
1465
+ visibleCopies.map((format) => /* @__PURE__ */ jsx8(DropdownMenuItem, { onSelect: () => void transfer(format, false), children: format === "plain" ? "Copy" : `Copy ${LABEL[format]}` }, format)),
1466
+ visibleAdapters.map((adapter) => /* @__PURE__ */ jsxs5(DropdownMenuItem, { onSelect: () => void transfer(adapter, false), children: [
1467
+ "Copy ",
1468
+ adapter.label
1469
+ ] }, adapter.id))
1470
+ ] }) : null,
1471
+ !hide.includes("prepare") ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1472
+ /* @__PURE__ */ jsx8(DropdownMenuSeparator, {}),
1473
+ quickVariants("ai"),
1474
+ /* @__PURE__ */ jsx8(DropdownMenuItem, { onSelect: () => void openWorkspace(), children: "Prepare for AI\u2026" }),
1475
+ nearestWorkspaceKey && nearestSurface ? /* @__PURE__ */ jsx8(DropdownMenuItem, { onSelect: () => void openWorkspace(nearestWorkspaceKey, { source: nearestSurface, label: nearestSurface.surfaceName }), children: "Prepare this surface\u2026" }) : null,
1476
+ variants.map((variant) => /* @__PURE__ */ jsx8(DropdownMenuItem, { onSelect: () => void openWorkspace(variantWorkspaceKeys.get(variant.id), variant), children: /* @__PURE__ */ jsx8(MenuPresentation, { ...variant, label: `Prepare ${variant.label}\u2026` }) }, variant.id))
1477
+ ] }) : null,
1478
+ !hide.includes("download") ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1479
+ /* @__PURE__ */ jsx8(DropdownMenuSeparator, {}),
1480
+ /* @__PURE__ */ jsx8(DropdownMenuLabel, { children: "Download" }),
1481
+ visibleDownloads.map((format) => /* @__PURE__ */ jsxs5(DropdownMenuItem, { onSelect: () => void transfer(format, true), children: [
1482
+ "Download ",
1483
+ LABEL[format]
1484
+ ] }, format)),
1485
+ visibleAdapters.map((adapter) => /* @__PURE__ */ jsxs5(DropdownMenuItem, { onSelect: () => void transfer(adapter, true), children: [
1486
+ "Download ",
1487
+ adapter.label
1488
+ ] }, `download:${adapter.id}`))
1489
+ ] }) : null,
1490
+ visibleActions.length ? /* @__PURE__ */ jsx8(DropdownMenuSeparator, {}) : null,
1491
+ visibleActions.map((action) => /* @__PURE__ */ jsx8(DropdownMenuItem, { "aria-label": action.ariaLabel, onSelect: () => void runAction(action), children: /* @__PURE__ */ jsx8(MenuPresentation, { ...action, label: action.label }) }, action.id))
1492
+ ] })
1493
+ ] }),
1494
+ notice ? /* @__PURE__ */ jsx8("span", { className: "matrx-alchemy-notice", role: "status", children: notice }) : null,
1495
+ Object.values(workspaces).map((saved) => /* @__PURE__ */ jsx8(PreparationWorkspace, { restoreFocus: () => triggerRef.current?.focus(), open: workspaceOpen && workspaceKey === saved.key, state: saved, scopes, onClose: () => setWorkspaceOpen(false), onChange: (next) => setWorkspaces((current) => ({ ...current, [saved.key]: next })), transport, adapters, captureScope: workspaceCaptures.current.get(saved.key) ?? capturePrimary, ...ai ? { ai } : {}, onOutcome: report }, saved.key))
1496
+ ] });
1497
+ }
1498
+ function initialControls(state) {
1499
+ const rows = state.original.payload.kind === "rows" ? state.original.payload : null;
1500
+ return {
1501
+ preset: state.preferences.initialPreset,
1502
+ sections: state.snapshot.sections.filter((s) => s.includedByDefault).map((s) => s.id),
1503
+ search: "",
1504
+ filters: {},
1505
+ sort: "",
1506
+ direction: "asc",
1507
+ top: state.preferences.limits.maxRows,
1508
+ visibleColumns: rows?.columns.filter((c) => c.visible).map((c) => c.id) ?? [],
1509
+ selectedOnly: false,
1510
+ selectedIndices: [],
1511
+ maxDepth: state.preferences.limits.maxDepth,
1512
+ maxStringChars: state.preferences.limits.maxStringChars,
1513
+ maxArrayItems: state.preferences.limits.maxArrayItems ?? null
1514
+ };
1515
+ }
1516
+ function presetLimits(preset, controls, preferences) {
1517
+ const min = (choice, cap) => choice == null ? cap : cap == null ? choice : Math.min(choice, cap);
1518
+ if (preset === "full") return preferences.limits;
1519
+ const request = preset === "brief" ? { maxDepth: 4, maxStringChars: 800, maxArrayItems: 25 } : preset === "compact" ? { maxDepth: 8, maxStringChars: 4e3, maxArrayItems: 100 } : { maxDepth: controls.maxDepth, maxStringChars: controls.maxStringChars, maxArrayItems: controls.maxArrayItems };
1520
+ return { ...preferences.limits, maxDepth: min(request.maxDepth, preferences.limits.maxDepth), maxStringChars: min(request.maxStringChars, preferences.limits.maxStringChars), maxArrayItems: min(request.maxArrayItems, preferences.limits.maxArrayItems ?? null) };
1521
+ }
1522
+ function rebuild(state, controls) {
1523
+ let draft = resetDraft(state.snapshot);
1524
+ if (state.snapshot.sections.length) draft = applySectionSelection(draft, state.snapshot, controls.sections);
1525
+ if (draft.payload.kind === "rows") draft = transformRows(draft, {
1526
+ search: controls.search,
1527
+ filters: controls.filters,
1528
+ ...controls.sort ? { sort: { column: controls.sort, direction: controls.direction } } : {},
1529
+ ...controls.selectedOnly ? { selectedIndices: controls.selectedIndices } : {},
1530
+ visibleColumns: controls.visibleColumns,
1531
+ structuredProjection: "visible",
1532
+ top: controls.top
1533
+ });
1534
+ const limits = presetLimits(controls.preset, controls, state.preferences);
1535
+ if (draft.payload.kind === "json" || draft.payload.kind === "registered") {
1536
+ draft = reduceJson(draft, { maxDepth: limits.maxDepth, maxStringChars: limits.maxStringChars, maxArrayItems: limits.maxArrayItems ?? null });
1537
+ }
1538
+ return applyTransferLimits(draft, limits);
1539
+ }
1540
+ function isBuiltInFormat(format) {
1541
+ return format in LABEL;
1542
+ }
1543
+ function preview(draft, format) {
1544
+ const resolved = isBuiltInFormat(format) ? format : "plain";
1545
+ return serialize(draft, resolved === "xlsx" ? "tsv" : resolved, { spreadsheetSafe: resolved === "csv" || resolved === "tsv" || resolved === "xlsx" }).plainText;
1546
+ }
1547
+ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onChange, transport, ai, adapters, onOutcome, captureScope }) {
1548
+ const fence = useFence();
1549
+ const latest = React8.useRef(state);
1550
+ latest.current = state;
1551
+ const aiSession = React8.useRef(void 0);
1552
+ if (!aiSession.current && ai) aiSession.current = ai.fork?.() ?? ai;
1553
+ const [controls, setControls] = React8.useState(() => initialControls(state));
1554
+ const [controlsHistory, setControlsHistory] = React8.useState([]);
1555
+ const [pendingControls, setPendingControls] = React8.useState(null);
1556
+ const requestedFormat = state.preferences.initialFormat;
1557
+ const [format, setFormat] = React8.useState(() => isBuiltInFormat(requestedFormat) ? supportsBuiltIn(requestedFormat, state.draft.payload) ? requestedFormat : "plain" : adapters.some((adapter) => adapter.id === requestedFormat && adapter.supports(state.draft.payload)) ? requestedFormat : "plain");
1558
+ const [buffer, setBuffer] = React8.useState(() => isBuiltInFormat(format) ? preview(state.draft, format) : "Preparing format preview\u2026");
1559
+ const [bufferError, setBufferError] = React8.useState(null);
1560
+ const [formatError, setFormatError] = React8.useState(null);
1561
+ const [preparedArtifact, setPreparedArtifact] = React8.useState(null);
1562
+ const [instruction, setInstruction] = React8.useState("");
1563
+ const [busy, setBusy] = React8.useState(null);
1564
+ const [aiFailed, setAiFailed] = React8.useState(false);
1565
+ const activeAi = React8.useRef(null);
1566
+ const activeCapture = React8.useRef(null);
1567
+ React8.useEffect(() => {
1568
+ if (bufferError) return;
1569
+ const adapter = adapters.find((candidate) => candidate.id === format);
1570
+ const controller = new AbortController();
1571
+ const snapshotId = state.snapshot.id;
1572
+ const revision = state.draft.revision;
1573
+ setPreparedArtifact(null);
1574
+ setFormatError(null);
1575
+ if (adapter) setBuffer("Preparing format preview\u2026");
1576
+ const artifactPromise = adapter ? adapter.build(state.draft, controller.signal) : buildArtifact(state.draft, isBuiltInFormat(format) ? format : "plain", state.snapshot.label);
1577
+ void artifactPromise.then((artifact) => {
1578
+ if (!controller.signal.aborted && artifact.snapshotId === snapshotId && artifact.draftRevision === revision) {
1579
+ setPreparedArtifact(artifact);
1580
+ setBuffer(artifact.plainText);
1581
+ }
1582
+ }).catch((error) => {
1583
+ if (!controller.signal.aborted) setFormatError(errorMessage(error, "Format preview failed"));
1584
+ });
1585
+ return () => controller.abort();
1586
+ }, [adapters, bufferError, format, state.draft, state.snapshot.id]);
1587
+ const close = React8.useCallback(() => {
1588
+ activeAi.current?.abort();
1589
+ activeCapture.current?.abort();
1590
+ activeAi.current = null;
1591
+ activeCapture.current = null;
1592
+ fence.abortAll();
1593
+ setBusy(null);
1594
+ onClose();
1595
+ }, [fence, onClose]);
1596
+ const changeControls = (next) => {
1597
+ if (state.draft.manuallyEdited) {
1598
+ setPendingControls(next);
1599
+ return;
1600
+ }
1601
+ setControlsHistory((history) => [...history, controls]);
1602
+ setControls(next);
1603
+ onChange({ ...state, draft: rebuild(state, next), error: void 0 });
1604
+ };
1605
+ const applyPendingControls = () => {
1606
+ if (!pendingControls) return;
1607
+ setControlsHistory((history) => [...history, controls]);
1608
+ setControls(pendingControls);
1609
+ setBufferError(null);
1610
+ onChange({ ...state, draft: rebuild(state, pendingControls), error: void 0 });
1611
+ setPendingControls(null);
1612
+ };
1613
+ const editBuffer = (text) => {
1614
+ setBuffer(text);
1615
+ setPreparedArtifact(null);
1616
+ setFormatError(null);
1617
+ const payload = state.draft.payload;
1618
+ if (payload.kind === "json" || payload.kind === "registered") {
1619
+ try {
1620
+ const value = JSON.parse(text);
1621
+ const next = payload.kind === "json" ? { kind: "json", value } : { kind: "registered", format: payload.format, value };
1622
+ setBufferError(null);
1623
+ onChange({ ...state, draft: editDraft(state.draft, next), error: void 0 });
1624
+ } catch {
1625
+ setBufferError("JSON is incomplete or invalid. Fix it before copying or downloading.");
1626
+ }
1627
+ return;
1628
+ }
1629
+ setBufferError(null);
1630
+ const kind = payload.kind === "markdown" || format === "markdown" ? "markdown" : "text";
1631
+ if (payload.kind === "rows" || !isBuiltInFormat(format)) setFormat("plain");
1632
+ onChange({ ...state, draft: editDraft(state.draft, { kind, text }), error: void 0 });
1633
+ };
1634
+ const requestCapture = async (scope) => {
1635
+ activeCapture.current?.abort();
1636
+ const { controller, id } = fence.begin();
1637
+ activeCapture.current = controller;
1638
+ const revision = state.draft.revision;
1639
+ setBusy("capture");
1640
+ try {
1641
+ const result = await captureScope(scope, controller.signal);
1642
+ if (fence.valid(id, controller) && latest.current.draft.revision === revision) onChange({ ...latest.current, pending: { ...result, scope }, error: result.collectionError });
1643
+ } catch (error) {
1644
+ if (fence.valid(id, controller)) onChange({ ...latest.current, error: errorMessage(error, "Refresh failed") });
1645
+ } finally {
1646
+ fence.finish(controller);
1647
+ if (activeCapture.current === controller) activeCapture.current = null;
1648
+ if (fence.valid(id, controller)) setBusy(null);
1649
+ }
1650
+ };
1651
+ const resolveCapture = (choice) => {
1652
+ if (!state.pending) return;
1653
+ if (choice === "keep") {
1654
+ const { pending: _pending, ...kept } = state;
1655
+ onChange({ ...kept, error: void 0 });
1656
+ return;
1657
+ }
1658
+ const pending = state.pending;
1659
+ const preferences = resolveTransferPreferences({}, state.preferences, pending.snapshot.limits);
1660
+ const base = { ...pending, key: state.key, original: createDraft(pending.snapshot), draft: createDraft(pending.snapshot), scope: pending.scope, preferences, ...pending.collectionError ? { error: pending.collectionError } : {} };
1661
+ const nextControls = initialControls(base);
1662
+ setControls(nextControls);
1663
+ setControlsHistory([]);
1664
+ setPendingControls(null);
1665
+ setBufferError(null);
1666
+ onChange({ ...base, draft: rebuild(base, nextControls) });
1667
+ };
1668
+ const runAi = async (recover = false) => {
1669
+ const adapter = aiSession.current;
1670
+ if (!adapter || !adapter.supports(state.snapshot) || !recover && !instruction.trim() || recover && !adapter.recover) return;
1671
+ activeAi.current?.abort();
1672
+ const { controller, id } = fence.begin();
1673
+ activeAi.current = controller;
1674
+ const snapshotId = state.snapshot.id;
1675
+ const revision = state.draft.revision;
1676
+ setBusy("ai");
1677
+ setAiFailed(false);
1678
+ try {
1679
+ const next = recover ? await adapter.recover({ snapshot: state.snapshot, draft: state.draft, signal: controller.signal }) : await adapter.run({ snapshot: state.snapshot, draft: state.draft, instruction, signal: controller.signal });
1680
+ const current = latest.current;
1681
+ if (!fence.valid(id, controller) || current.snapshot.id !== snapshotId || current.draft.revision !== revision || next.snapshotId !== snapshotId || next.sourceId !== state.draft.sourceId || next.sourceRevision !== state.draft.sourceRevision || next.revision <= revision) return;
1682
+ try {
1683
+ serialize(next, "plain");
1684
+ } catch {
1685
+ onChange({ ...current, error: "AI returned an invalid transfer draft." });
1686
+ setAiFailed(true);
1687
+ return;
1688
+ }
1689
+ onChange({ ...current, draft: next, error: void 0 });
1690
+ setBufferError(null);
1691
+ } catch (error) {
1692
+ if (!isAbort(error) && fence.valid(id, controller)) {
1693
+ setAiFailed(true);
1694
+ onChange({ ...latest.current, error: errorMessage(error, "AI preparation failed") });
1695
+ }
1696
+ } finally {
1697
+ fence.finish(controller);
1698
+ if (activeAi.current === controller) activeAi.current = null;
1699
+ if (fence.valid(id, controller)) setBusy(null);
1700
+ }
1701
+ };
1702
+ const cancelAi = async () => {
1703
+ const aiController = activeAi.current;
1704
+ const { controller, id } = fence.begin();
1705
+ try {
1706
+ await aiSession.current?.cancel?.();
1707
+ } catch (error) {
1708
+ if (fence.valid(id, controller)) onChange({ ...latest.current, error: errorMessage(error, "AI cancellation failed") });
1709
+ } finally {
1710
+ aiController?.abort();
1711
+ fence.finish(controller);
1712
+ if (activeAi.current === aiController) activeAi.current = null;
1713
+ if (fence.valid(id, controller)) setBusy(null);
1714
+ }
1715
+ };
1716
+ const [deliveryNotice, setDeliveryNotice] = React8.useState(null);
1717
+ React8.useEffect(() => {
1718
+ setDeliveryNotice(null);
1719
+ }, [state.draft.revision]);
1720
+ const reportDelivery = (outcome) => {
1721
+ setDeliveryNotice(outcomeMessage(outcome));
1722
+ onOutcome(outcome);
1723
+ };
1724
+ const deliver = async (download) => {
1725
+ if (bufferError) return;
1726
+ const { controller, id } = fence.begin();
1727
+ const snapshotId = state.snapshot.id;
1728
+ const revision = state.draft.revision;
1729
+ setBusy(download ? "download" : "copy");
1730
+ try {
1731
+ if (!preparedArtifact) throw new Error("Wait for the selected format preview before delivery.");
1732
+ const artifact = preparedArtifact;
1733
+ const current = latest.current;
1734
+ if (!fence.valid(id, controller) || current.snapshot.id !== snapshotId || current.draft.revision !== revision || artifact.snapshotId !== snapshotId || artifact.draftRevision !== revision) return;
1735
+ const result = await transport[download ? "download" : "copy"](artifact, controller.signal);
1736
+ if (fence.valid(id, controller)) reportDelivery(result);
1737
+ } catch (error) {
1738
+ if (fence.valid(id, controller)) reportDelivery({ status: "error", code: "prepare-failed", message: errorMessage(error, "Preparation failed"), retryable: true });
1739
+ } finally {
1740
+ fence.finish(controller);
1741
+ if (fence.valid(id, controller)) setBusy(null);
1742
+ }
1743
+ };
1744
+ const rows = state.original.payload.kind === "rows" ? state.original.payload : null;
1745
+ const formats = COPIES.filter((f) => supportsBuiltIn(f, state.draft.payload));
1746
+ const workspaceAdapters = adapters.filter((adapter) => adapter.supports(state.draft.payload));
1747
+ const displayedOmissions = preparedArtifact?.omissions ?? state.draft.omissions;
1748
+ const limitText = Object.entries(state.preferences.limits).filter(([, value]) => value != null).map(([key, value]) => `${key.replace(/^max/, "max ")} ${Number(value).toLocaleString()}`).join(" \xB7 ");
1749
+ return /* @__PURE__ */ jsx8(Dialog, { open, onOpenChange: (next) => {
1750
+ if (!next) close();
1751
+ }, children: /* @__PURE__ */ jsxs5(DialogContent, { onCloseAutoFocus: (event) => {
1752
+ event.preventDefault();
1753
+ restoreFocus();
1754
+ }, className: "matrx-alchemy-workspace max-w-5xl", "aria-busy": busy !== null, children: [
1755
+ /* @__PURE__ */ jsxs5(DialogHeader, { children: [
1756
+ /* @__PURE__ */ jsxs5(DialogTitle, { children: [
1757
+ "Prepare ",
1758
+ state.snapshot.label
1759
+ ] }),
1760
+ /* @__PURE__ */ jsxs5(DialogDescription, { children: [
1761
+ "Captured ",
1762
+ state.snapshot.unsavedChanges ? "from your unsaved draft" : "as an immutable snapshot",
1763
+ ". Your source is never edited."
1764
+ ] })
1765
+ ] }),
1766
+ /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-workspace-grid", children: [
1767
+ /* @__PURE__ */ jsxs5("section", { className: "matrx-alchemy-main", children: [
1768
+ /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-toolbar", children: [
1769
+ scopes.length > 1 ? /* @__PURE__ */ jsxs5("label", { children: [
1770
+ "Scope",
1771
+ /* @__PURE__ */ jsx8("select", { "aria-label": "Transfer scope", value: state.scope, disabled: busy !== null, onChange: (e) => void requestCapture(e.target.value), children: scopes.map((scope) => /* @__PURE__ */ jsx8("option", { value: scope, children: SCOPE_LABEL[scope] }, scope)) })
1772
+ ] }) : null,
1773
+ /* @__PURE__ */ jsxs5("label", { children: [
1774
+ "Detail",
1775
+ /* @__PURE__ */ jsxs5("select", { "aria-label": "Detail preset", value: controls.preset, onChange: (e) => changeControls({ ...controls, preset: e.target.value }), children: [
1776
+ /* @__PURE__ */ jsx8("option", { value: "full", children: "Full" }),
1777
+ /* @__PURE__ */ jsx8("option", { value: "compact", children: "Compact" }),
1778
+ /* @__PURE__ */ jsx8("option", { value: "brief", children: "Brief" }),
1779
+ /* @__PURE__ */ jsx8("option", { value: "custom", children: "Custom" })
1780
+ ] })
1781
+ ] }),
1782
+ /* @__PURE__ */ jsx8(Button, { type: "button", size: "sm", variant: "outline", disabled: !state.draft.history?.length && !controlsHistory.length || busy !== null, onClick: () => {
1783
+ setBufferError(null);
1784
+ if (controlsHistory.length) {
1785
+ const next = controlsHistory[controlsHistory.length - 1];
1786
+ setControls(next);
1787
+ setControlsHistory((history) => history.slice(0, -1));
1788
+ onChange({ ...state, draft: rebuild(state, next), error: void 0 });
1789
+ } else onChange({ ...state, draft: undoDraft(state.draft), error: void 0 });
1790
+ }, children: "Undo" }),
1791
+ /* @__PURE__ */ jsx8(Button, { type: "button", size: "sm", variant: "outline", disabled: busy !== null, onClick: () => {
1792
+ const next = initialControls(state);
1793
+ setControls(next);
1794
+ setControlsHistory([]);
1795
+ setPendingControls(null);
1796
+ setBufferError(null);
1797
+ onChange({ ...state, draft: rebuild(state, next), error: void 0 });
1798
+ }, children: "Reset" }),
1799
+ /* @__PURE__ */ jsx8(Button, { type: "button", size: "sm", variant: "outline", disabled: busy !== null, onClick: () => void requestCapture(state.scope), children: "Refresh source" })
1800
+ ] }),
1801
+ controls.preset === "custom" && (state.draft.payload.kind === "json" || state.draft.payload.kind === "registered") ? /* @__PURE__ */ jsxs5("fieldset", { className: "matrx-alchemy-options", children: [
1802
+ /* @__PURE__ */ jsx8("legend", { children: "JSON limits" }),
1803
+ [["maxDepth", "Maximum depth"], ["maxStringChars", "Maximum string characters"], ["maxArrayItems", "Maximum array items"]].map(([key, text]) => /* @__PURE__ */ jsxs5("label", { children: [
1804
+ text,
1805
+ /* @__PURE__ */ jsx8("input", { type: "number", min: "0", value: controls[key] ?? "", onChange: (e) => changeControls({ ...controls, [key]: Number.isNaN(e.target.valueAsNumber) ? null : e.target.valueAsNumber }) })
1806
+ ] }, key))
1807
+ ] }) : null,
1808
+ rows ? /* @__PURE__ */ jsx8(RowControls, { rows, controls, change: changeControls }) : null,
1809
+ state.snapshot.sections.length ? /* @__PURE__ */ jsxs5("fieldset", { className: "matrx-alchemy-options", children: [
1810
+ /* @__PURE__ */ jsx8("legend", { children: "Sections" }),
1811
+ state.snapshot.sections.map((section) => /* @__PURE__ */ jsxs5("label", { children: [
1812
+ /* @__PURE__ */ jsx8("input", { type: "checkbox", checked: controls.sections.includes(section.id), onChange: (e) => changeControls({ ...controls, sections: e.target.checked ? [...controls.sections, section.id] : controls.sections.filter((id) => id !== section.id) }) }),
1813
+ " ",
1814
+ section.label
1815
+ ] }, section.id))
1816
+ ] }) : null,
1817
+ /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-coverage", children: [
1818
+ /* @__PURE__ */ jsxs5("span", { children: [
1819
+ state.snapshot.coverage.included.toLocaleString(),
1820
+ " included",
1821
+ state.snapshot.coverage.total == null ? "" : ` of ${state.snapshot.coverage.total.toLocaleString()}`,
1822
+ " \xB7 ",
1823
+ state.snapshot.coverage.status
1824
+ ] }),
1825
+ state.snapshot.coverage.reason ? /* @__PURE__ */ jsx8("span", { children: state.snapshot.coverage.reason }) : null,
1826
+ limitText ? /* @__PURE__ */ jsxs5("span", { children: [
1827
+ "Limits: ",
1828
+ limitText
1829
+ ] }) : null,
1830
+ displayedOmissions.length ? /* @__PURE__ */ jsxs5("details", { children: [
1831
+ /* @__PURE__ */ jsxs5("summary", { children: [
1832
+ displayedOmissions.length,
1833
+ " omissions or adjustments"
1834
+ ] }),
1835
+ /* @__PURE__ */ jsx8("ul", { children: displayedOmissions.map((o, i) => /* @__PURE__ */ jsxs5("li", { children: [
1836
+ o.path || "/",
1837
+ ": ",
1838
+ o.reason,
1839
+ o.count == null ? "" : ` (${o.count})`
1840
+ ] }, `${o.path}-${o.reason}-${i}`)) })
1841
+ ] }) : null
1842
+ ] }),
1843
+ /* @__PURE__ */ jsxs5("label", { className: "matrx-alchemy-preview-label", children: [
1844
+ "Output format",
1845
+ /* @__PURE__ */ jsxs5("select", { "aria-label": "Output format", value: format, onChange: (e) => setFormat(e.target.value), children: [
1846
+ formats.map((f) => /* @__PURE__ */ jsx8("option", { value: f, children: LABEL[f] }, f)),
1847
+ state.draft.payload.kind === "rows" ? /* @__PURE__ */ jsx8("option", { value: "xlsx", children: "Excel workbook" }) : null,
1848
+ workspaceAdapters.map((adapter) => /* @__PURE__ */ jsx8("option", { value: adapter.id, children: adapter.label }, adapter.id))
1849
+ ] })
1850
+ ] }),
1851
+ /* @__PURE__ */ jsx8("textarea", { className: "matrx-alchemy-preview", "aria-label": "Prepared output", "aria-invalid": bufferError ? true : void 0, "aria-describedby": bufferError ? "alchemy-buffer-error" : void 0, value: buffer, onChange: (e) => editBuffer(e.target.value) }),
1852
+ bufferError ? /* @__PURE__ */ jsx8("p", { id: "alchemy-buffer-error", role: "alert", className: "matrx-alchemy-error", children: bufferError }) : null,
1853
+ formatError ? /* @__PURE__ */ jsx8("p", { role: "alert", className: "matrx-alchemy-error", children: formatError }) : null,
1854
+ bufferError ? /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", disabled: busy !== null, onClick: () => {
1855
+ const fallback = editDraft(state.draft, { kind: "text", text: buffer });
1856
+ setBufferError(null);
1857
+ onChange({ ...state, draft: fallback, error: void 0 });
1858
+ setFormat("plain");
1859
+ }, children: "Use invalid JSON as plain text" }) : null
1860
+ ] }),
1861
+ ai || busy === "capture" || state.error ? /* @__PURE__ */ jsxs5("aside", { children: [
1862
+ ai ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
1863
+ /* @__PURE__ */ jsxs5("label", { children: [
1864
+ "AI instruction",
1865
+ /* @__PURE__ */ jsx8("textarea", { value: instruction, onChange: (e) => setInstruction(e.target.value), placeholder: "Tell AI how to prepare this content" })
1866
+ ] }),
1867
+ /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", disabled: !aiSession.current || !instruction.trim() || busy !== null || !aiSession.current.supports(state.snapshot), onClick: () => void runAi(false), children: "Generate preview" }),
1868
+ busy === "ai" ? /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => void cancelAi(), children: aiSession.current?.cancel ? "Cancel AI" : "Stop waiting" }) : null
1869
+ ] }) : null,
1870
+ busy === "capture" ? /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => {
1871
+ activeCapture.current?.abort();
1872
+ activeCapture.current = null;
1873
+ setBusy(null);
1874
+ }, children: "Stop loading rows" }) : null,
1875
+ ai && aiSession.current?.recover && busy !== "ai" ? /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => void runAi(true), children: aiFailed ? "Retry or rejoin AI run" : "Rejoin AI run" }) : null,
1876
+ ai ? /* @__PURE__ */ jsx8("p", { className: "matrx-alchemy-meta", children: "AI creates a preview. It never copies automatically. Closing detaches this view without cancelling server work." }) : null,
1877
+ state.error ? /* @__PURE__ */ jsx8("p", { role: "alert", className: "matrx-alchemy-error", children: state.error }) : null
1878
+ ] }) : null
1879
+ ] }),
1880
+ pendingControls ? /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-decision", role: "alert", children: [
1881
+ /* @__PURE__ */ jsx8("p", { children: "This control would replace your manual edit." }),
1882
+ /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => setPendingControls(null), children: "Keep edited draft" }),
1883
+ /* @__PURE__ */ jsx8(Button, { type: "button", onClick: applyPendingControls, children: "Rebuild from source" })
1884
+ ] }) : null,
1885
+ state.pending ? /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-decision", role: "alert", children: [
1886
+ /* @__PURE__ */ jsx8("p", { children: "A fresh source snapshot is ready." }),
1887
+ /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => resolveCapture("keep"), children: "Keep current snapshot and draft" }),
1888
+ /* @__PURE__ */ jsx8(Button, { type: "button", onClick: () => resolveCapture("regenerate"), children: "Regenerate from fresh source" })
1889
+ ] }) : null,
1890
+ deliveryNotice ? /* @__PURE__ */ jsx8("p", { role: "status", className: "matrx-alchemy-meta", children: deliveryNotice }) : null,
1891
+ /* @__PURE__ */ jsxs5(DialogFooter, { children: [
1892
+ /* @__PURE__ */ jsxs5("span", { className: "matrx-alchemy-meta", children: [
1893
+ buffer.length.toLocaleString(),
1894
+ " characters \xB7 ~",
1895
+ estimateTokens(buffer).toLocaleString(),
1896
+ " tokens"
1897
+ ] }),
1898
+ /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: close, children: "Close" }),
1899
+ /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", disabled: busy !== null || Boolean(bufferError || formatError) || !preparedArtifact, onClick: () => void deliver(true), children: "Download prepared output" }),
1900
+ /* @__PURE__ */ jsx8(Button, { type: "button", disabled: busy !== null || Boolean(bufferError || formatError) || !preparedArtifact, onClick: () => void deliver(false), children: "Copy prepared output" })
1901
+ ] })
1902
+ ] }) });
1903
+ }
1904
+ function RowControls({ rows, controls, change }) {
1905
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1906
+ /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-toolbar", children: [
1907
+ /* @__PURE__ */ jsxs5("label", { children: [
1908
+ "Search",
1909
+ /* @__PURE__ */ jsx8("input", { value: controls.search, onChange: (e) => change({ ...controls, search: e.target.value }) })
1910
+ ] }),
1911
+ /* @__PURE__ */ jsxs5("label", { children: [
1912
+ "Sort",
1913
+ /* @__PURE__ */ jsxs5("select", { value: controls.sort, onChange: (e) => change({ ...controls, sort: e.target.value }), children: [
1914
+ /* @__PURE__ */ jsx8("option", { value: "", children: "None" }),
1915
+ rows.columns.map((column) => /* @__PURE__ */ jsx8("option", { value: column.id, children: column.label }, column.id))
1916
+ ] })
1917
+ ] }),
1918
+ /* @__PURE__ */ jsxs5("label", { children: [
1919
+ "Direction",
1920
+ /* @__PURE__ */ jsxs5("select", { value: controls.direction, disabled: !controls.sort, onChange: (e) => change({ ...controls, direction: e.target.value }), children: [
1921
+ /* @__PURE__ */ jsx8("option", { value: "asc", children: "Ascending" }),
1922
+ /* @__PURE__ */ jsx8("option", { value: "desc", children: "Descending" })
1923
+ ] })
1924
+ ] }),
1925
+ /* @__PURE__ */ jsxs5("label", { children: [
1926
+ "Top rows",
1927
+ /* @__PURE__ */ jsx8("input", { type: "number", min: "0", value: controls.top ?? "", onChange: (e) => change({ ...controls, top: Number.isNaN(e.target.valueAsNumber) ? null : e.target.valueAsNumber }) })
1928
+ ] })
1929
+ ] }),
1930
+ /* @__PURE__ */ jsxs5("fieldset", { className: "matrx-alchemy-options", children: [
1931
+ /* @__PURE__ */ jsx8("legend", { children: "Columns and filters" }),
1932
+ rows.columns.map((column) => /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-column", children: [
1933
+ /* @__PURE__ */ jsxs5("label", { children: [
1934
+ /* @__PURE__ */ jsx8("input", { type: "checkbox", checked: controls.visibleColumns.includes(column.id), onChange: (e) => change({ ...controls, visibleColumns: e.target.checked ? [...controls.visibleColumns, column.id] : controls.visibleColumns.filter((id) => id !== column.id) }) }),
1935
+ " ",
1936
+ column.label
1937
+ ] }),
1938
+ /* @__PURE__ */ jsx8("input", { "aria-label": `Filter ${column.label}`, placeholder: "Filter", value: controls.filters[column.id] ?? "", onChange: (e) => change({ ...controls, filters: { ...controls.filters, [column.id]: e.target.value } }) })
1939
+ ] }, column.id))
1940
+ ] }),
1941
+ /* @__PURE__ */ jsxs5("fieldset", { className: "matrx-alchemy-options", children: [
1942
+ /* @__PURE__ */ jsx8("legend", { children: "Rows" }),
1943
+ /* @__PURE__ */ jsxs5("label", { children: [
1944
+ /* @__PURE__ */ jsx8("input", { type: "checkbox", checked: controls.selectedOnly, onChange: (e) => change({ ...controls, selectedOnly: e.target.checked }) }),
1945
+ " Only selected rows"
1946
+ ] }),
1947
+ /* @__PURE__ */ jsx8("div", { className: "matrx-alchemy-row-list", children: rows.rows.map((row, index) => {
1948
+ const summary = rows.columns.filter((column) => column.visible).slice(0, 2).map((column) => String(readTransferCell(row, column) ?? "")).filter(Boolean).join(" \xB7 ");
1949
+ return /* @__PURE__ */ jsxs5("label", { children: [
1950
+ /* @__PURE__ */ jsx8("input", { type: "checkbox", checked: controls.selectedIndices.includes(index), onChange: (e) => change({ ...controls, selectedIndices: e.target.checked ? [...controls.selectedIndices, index] : controls.selectedIndices.filter((value) => value !== index) }) }),
1951
+ " Row ",
1952
+ index + 1,
1953
+ summary ? ` \u2014 ${summary}` : ""
1954
+ ] }, index);
1955
+ }) })
1956
+ ] })
1957
+ ] });
1958
+ }
1959
+ export {
1960
+ ALCHEMY_GLYPH_SVG,
1961
+ AlchemyGlyph,
1962
+ ContentTransferCapabilitiesProvider,
1963
+ ContentTransferMenu,
1964
+ ContentTransferSurfaceProvider,
1965
+ GROOMER_LEVELS,
1966
+ MatrxCopyMenu,
1967
+ applyGroomerPreset,
1968
+ buildGroomerPresetPayload,
1969
+ defaultGroomerSelections,
1970
+ groomerPresetVariants,
1971
+ useContentTransferCapabilities,
1972
+ useContentTransferSurface
1973
+ };
1974
+ //# sourceMappingURL=content-transfer.js.map