@ai-matrx/design-system 0.18.1 → 0.18.3

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.
@@ -103,6 +103,8 @@ interface MatrxCopyMenuProps {
103
103
  label: string;
104
104
  /** Stable record identity when this mounted control can switch between records. */
105
105
  sourceId?: string;
106
+ /** Prepare the tabular view while keeping raw JSON and human copy formats. */
107
+ primarySource?: "table";
106
108
  human?: Resolvable<string>;
107
109
  json?: Resolvable<unknown>;
108
110
  agent?: Resolvable<AgentPayloadInput | string>;
@@ -141,6 +143,8 @@ declare function groomerPresetVariants(getConfig: () => AlchemyGroomerConfig): A
141
143
 
142
144
  declare function AlchemyGlyph(props: React.SVGAttributes<SVGSVGElement>): React.JSX.Element;
143
145
  type ContentTransferCapabilities = TransferCapabilities & {
146
+ /** Opaque host session boundary. It is used only to reset local menu state. */
147
+ identityKey?: string;
144
148
  transport?: Transport;
145
149
  actions?: PageAction[];
146
150
  preferences?: TransferPreferenceOverrides;
@@ -203,6 +207,6 @@ interface ContentTransferController {
203
207
  label?: string;
204
208
  }): Promise<void>;
205
209
  }
206
- declare function ContentTransferMenu({ source, surface, label, projection, sections, preferences, table, formatSources, variants, actions, capabilities: local, disabled, enabled, className, stopPropagation, hide, controllerRef }: ContentTransferMenuProps): React.JSX.Element | null;
210
+ declare function ContentTransferMenu(props: ContentTransferMenuProps): React.JSX.Element;
207
211
 
208
212
  export { type AlchemyCopyVariant, type AlchemyCustomSource, type AlchemyDetail, type AlchemyExportItem, AlchemyGlyph, type AlchemyGroomerConfig, type AlchemyGroomerPreset, type AlchemyGroomerSection, type AlchemyOptionValues, type AlchemySourceOption, type ContentTransferCapabilities, ContentTransferCapabilitiesProvider, type ContentTransferController, ContentTransferMenu, type ContentTransferMenuProps, ContentTransferSurfaceProvider, GROOMER_LEVELS, MatrxCopyMenu, type MatrxCopyMenuProps, type SerializableFormat, type TransferMenuPresentation, type TransferMenuVariant, applyGroomerPreset, buildGroomerPresetPayload, defaultGroomerSelections, groomerPresetVariants, useContentTransferCapabilities, useContentTransferSurface };
@@ -103,6 +103,8 @@ interface MatrxCopyMenuProps {
103
103
  label: string;
104
104
  /** Stable record identity when this mounted control can switch between records. */
105
105
  sourceId?: string;
106
+ /** Prepare the tabular view while keeping raw JSON and human copy formats. */
107
+ primarySource?: "table";
106
108
  human?: Resolvable<string>;
107
109
  json?: Resolvable<unknown>;
108
110
  agent?: Resolvable<AgentPayloadInput | string>;
@@ -141,6 +143,8 @@ declare function groomerPresetVariants(getConfig: () => AlchemyGroomerConfig): A
141
143
 
142
144
  declare function AlchemyGlyph(props: React.SVGAttributes<SVGSVGElement>): React.JSX.Element;
143
145
  type ContentTransferCapabilities = TransferCapabilities & {
146
+ /** Opaque host session boundary. It is used only to reset local menu state. */
147
+ identityKey?: string;
144
148
  transport?: Transport;
145
149
  actions?: PageAction[];
146
150
  preferences?: TransferPreferenceOverrides;
@@ -203,6 +207,6 @@ interface ContentTransferController {
203
207
  label?: string;
204
208
  }): Promise<void>;
205
209
  }
206
- declare function ContentTransferMenu({ source, surface, label, projection, sections, preferences, table, formatSources, variants, actions, capabilities: local, disabled, enabled, className, stopPropagation, hide, controllerRef }: ContentTransferMenuProps): React.JSX.Element | null;
210
+ declare function ContentTransferMenu(props: ContentTransferMenuProps): React.JSX.Element;
207
211
 
208
212
  export { type AlchemyCopyVariant, type AlchemyCustomSource, type AlchemyDetail, type AlchemyExportItem, AlchemyGlyph, type AlchemyGroomerConfig, type AlchemyGroomerPreset, type AlchemyGroomerSection, type AlchemyOptionValues, type AlchemySourceOption, type ContentTransferCapabilities, ContentTransferCapabilitiesProvider, type ContentTransferController, ContentTransferMenu, type ContentTransferMenuProps, ContentTransferSurfaceProvider, GROOMER_LEVELS, MatrxCopyMenu, type MatrxCopyMenuProps, type SerializableFormat, type TransferMenuPresentation, type TransferMenuVariant, applyGroomerPreset, buildGroomerPresetPayload, defaultGroomerSelections, groomerPresetVariants, useContentTransferCapabilities, useContentTransferSurface };
@@ -641,7 +641,17 @@ var sourceFor = (id, label, build) => ({
641
641
  }
642
642
  });
643
643
  function MatrxCopyMenu(props) {
644
- return /* @__PURE__ */ jsx7(MatrxCopyMenuSession, { ...props }, props.sourceId ?? props.label);
644
+ const inherited = useContentTransferCapabilities();
645
+ return /* @__PURE__ */ jsx7(
646
+ MatrxCopyMenuSession,
647
+ {
648
+ ...props
649
+ },
650
+ JSON.stringify([
651
+ inherited.identityKey ?? null,
652
+ props.sourceId ?? props.label
653
+ ])
654
+ );
645
655
  }
646
656
  function MatrxCopyMenuSession(props) {
647
657
  const instance = React7.useId();
@@ -669,7 +679,7 @@ function MatrxCopyMenuSession(props) {
669
679
  })) : void 0;
670
680
  const agent = aiVisible && props.agent !== void 0 ? make("agent", () => asPayload(resolve(props.agent))) : void 0;
671
681
  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", () => {
682
+ const table = (exportVisible || props.primarySource === "table") && props.export?.sheetRows ? make("rows", () => {
673
683
  const value = normalizeTransferJson(props.export.sheetRows());
674
684
  if (!Array.isArray(value) || value.some(
675
685
  (row) => row === null || typeof row !== "object" || Array.isArray(row)
@@ -816,7 +826,7 @@ function MatrxCopyMenuSession(props) {
816
826
  return props.sendToSheet(draft.payload.rows, props.label);
817
827
  }
818
828
  });
819
- const source = json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
829
+ const source = (props.primarySource === "table" ? table : void 0) ?? json ?? table ?? structuredAgent ?? human ?? builderVariants[0]?.source;
820
830
  if (!source && !actions.length) return null;
821
831
  const target = source ?? make("actions", () => ({ kind: "text", text: props.label }));
822
832
  return /* @__PURE__ */ jsxs4(Fragment, { children: [
@@ -836,7 +846,7 @@ function MatrxCopyMenuSession(props) {
836
846
  formatSources: {
837
847
  ...human ? { plain: human, markdown: humanMarkdown, html: humanMarkdown } : {},
838
848
  ...json ? { json, "compact-json": json } : {},
839
- ...table ? { csv: table, tsv: table } : {}
849
+ ...table && exportVisible ? { csv: table, tsv: table } : {}
840
850
  },
841
851
  ...props.disabled !== void 0 ? { disabled: props.disabled } : {},
842
852
  className: cn(
@@ -1221,6 +1231,10 @@ function materialIdentity(material) {
1221
1231
  }
1222
1232
  return `payload:${id}`;
1223
1233
  }
1234
+ function snapshotContent(snapshot) {
1235
+ const { sourceId, label, payload, coverage, unsavedChanges, omissions, sections, limits } = snapshot;
1236
+ return JSON.stringify({ sourceId, label, payload, coverage, unsavedChanges, omissions, sections, limits });
1237
+ }
1224
1238
  function useFence() {
1225
1239
  const mounted = React8.useRef(true);
1226
1240
  const nextId = React8.useRef(0);
@@ -1269,7 +1283,12 @@ async function buildArtifact(draft, format, label) {
1269
1283
  const extension = format === "plain" ? "txt" : format === "compact-json" ? "json" : format;
1270
1284
  return serialize(draft, format, { spreadsheetSafe: format === "csv" || format === "tsv", filename: `${filenameFor(label)}.${extension}` });
1271
1285
  }
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 }) {
1286
+ function ContentTransferMenu(props) {
1287
+ const inherited = React8.useContext(CapabilityContext);
1288
+ const identityKey = props.capabilities?.identityKey ?? inherited.identityKey;
1289
+ return /* @__PURE__ */ jsx8(ContentTransferMenuSession, { ...props }, identityKey ?? "anonymous");
1290
+ }
1291
+ function ContentTransferMenuSession({ source, surface, label, projection, sections, preferences, table, formatSources, variants = EMPTY_VARIANTS, actions = [], capabilities: local, disabled, enabled = true, className, stopPropagation = true, hide = [], controllerRef }) {
1273
1292
  const triggerRef = React8.useRef(null);
1274
1293
  const inherited = React8.useContext(CapabilityContext);
1275
1294
  const nearestSurface = React8.useContext(SurfaceContext);
@@ -1318,7 +1337,7 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
1318
1337
  const captured = await material.capture(signal);
1319
1338
  sourceMaterial = { id: captured.snapshot.sourceId, label: captured.snapshot.label, capture: async () => captured };
1320
1339
  } else if ("kind" in material) {
1321
- sourceMaterial = applyMenuProjection ? { id: "direct", label: sourceLabel, capture: async () => directSource2(material, { label: sourceLabel, ...projection ? { projection } : {}, ...sections ? { sections } : {} }) } : material;
1340
+ sourceMaterial = { id: materialIdentity(material), label: sourceLabel, capture: async () => directSource2(material, { sourceId: materialIdentity(material), label: sourceLabel, ...applyMenuProjection && projection ? { projection } : {}, ...applyMenuProjection && sections ? { sections } : {} }) };
1322
1341
  } else if (applyMenuProjection && (projection || sections)) {
1323
1342
  sourceMaterial = {
1324
1343
  id: material.id,
@@ -1335,7 +1354,7 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
1335
1354
  } else {
1336
1355
  sourceMaterial = material;
1337
1356
  }
1338
- const raw = "kind" in sourceMaterial ? directSource2(sourceMaterial) : await sourceMaterial.capture({ scope, signal });
1357
+ const raw = await sourceMaterial.capture({ scope, signal });
1339
1358
  const constrained = resolveTransferPreferences(providerPreferences, preferences, raw.snapshot.limits);
1340
1359
  const result = await capture({ id: raw.snapshot.sourceId, label: raw.snapshot.label, capture: async () => ({ ...raw, snapshot: { ...raw.snapshot, limits: constrained.limits } }) }, signal, scope);
1341
1360
  if (scope !== "all-matching" || !collectPages) return result;
@@ -1395,31 +1414,117 @@ function ContentTransferMenu({ source, surface, label, projection, sections, pre
1395
1414
  fence.finish(controller);
1396
1415
  }
1397
1416
  }, [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]);
1417
+ const openWorkspace = React8.useCallback(
1418
+ async (key = primaryWorkspaceKey, variant) => {
1419
+ const variantMaterial = variant && "kind" in variant.source ? {
1420
+ id: `variant:${key}`,
1421
+ label: variant.label,
1422
+ capture: async () => directSource2(variant.source, {
1423
+ sourceId: `variant:${key}`,
1424
+ label: variant.label
1425
+ })
1426
+ } : variant?.source;
1427
+ const captureWorkspace = (scope, signal, collectPages = true) => captureOne(variantMaterial ?? primary, scope, signal, true, collectPages);
1428
+ workspaceCaptures.current.set(key, captureWorkspace);
1429
+ const existing = workspaces[key];
1430
+ const { controller, id } = fence.begin();
1431
+ if (existing) {
1432
+ try {
1433
+ const allMatching = existing.scope === "all-matching";
1434
+ const fresh = await captureWorkspace(
1435
+ existing.scope,
1436
+ controller.signal,
1437
+ !allMatching
1438
+ );
1439
+ if (!fence.valid(id, controller) || !workspaceCaptures.current.has(key))
1440
+ return;
1441
+ if (allMatching && (!existing.allMatching || !fresh.allMatching))
1442
+ throw new Error(
1443
+ "This source can no longer provide an all-matching snapshot."
1444
+ );
1445
+ const collectionIdentity = (value) => value && {
1446
+ query: value.source.query,
1447
+ projection: value.projection,
1448
+ columns: value.columns,
1449
+ limits: value.limits
1450
+ };
1451
+ const changed = allMatching ? JSON.stringify(collectionIdentity(existing.allMatching)) !== JSON.stringify(collectionIdentity(fresh.allMatching)) : snapshotContent(existing.snapshot) !== snapshotContent(fresh.snapshot);
1452
+ setWorkspaces((current) => {
1453
+ const retained = current[key];
1454
+ if (!retained) return current;
1455
+ return {
1456
+ ...current,
1457
+ [key]: {
1458
+ ...retained,
1459
+ error: fresh.collectionError,
1460
+ ...allMatching ? { sourceStale: changed } : changed ? { pending: { ...fresh, scope: retained.scope } } : { pending: void 0 }
1461
+ }
1462
+ };
1463
+ });
1464
+ setWorkspaceKey(key);
1465
+ setWorkspaceOpen(true);
1466
+ } catch (error) {
1467
+ if (fence.valid(id, controller) && workspaceCaptures.current.has(key)) {
1468
+ setWorkspaces(
1469
+ (current) => current[key] ? {
1470
+ ...current,
1471
+ [key]: {
1472
+ ...current[key],
1473
+ error: errorMessage(error, "Could not refresh source")
1474
+ }
1475
+ } : current
1476
+ );
1477
+ setWorkspaceKey(key);
1478
+ setWorkspaceOpen(true);
1479
+ }
1480
+ } finally {
1481
+ fence.finish(controller);
1482
+ }
1483
+ return;
1484
+ }
1485
+ try {
1486
+ const result = await captureWorkspace(initialScope, controller.signal);
1487
+ if (!fence.valid(id, controller)) return;
1488
+ const resolved = resolveTransferPreferences(
1489
+ providerPreferences,
1490
+ preferences,
1491
+ result.snapshot.limits
1492
+ );
1493
+ const original = createDraft(result.snapshot);
1494
+ const base = {
1495
+ ...result,
1496
+ key,
1497
+ original,
1498
+ draft: original,
1499
+ scope: initialScope,
1500
+ preferences: resolved,
1501
+ ...result.collectionError ? { error: result.collectionError } : {}
1502
+ };
1503
+ setWorkspaces((current) => ({
1504
+ ...current,
1505
+ [key]: { ...base, draft: rebuild(base, initialControls(base)) }
1506
+ }));
1507
+ setWorkspaceKey(key);
1508
+ setWorkspaceOpen(true);
1509
+ } catch (error) {
1510
+ if (fence.valid(id, controller))
1511
+ setNotice(errorMessage(error, "Could not capture content"));
1512
+ } finally {
1513
+ fence.finish(controller);
1514
+ }
1515
+ },
1516
+ [
1517
+ captureOne,
1518
+ capturePrimary,
1519
+ fence,
1520
+ initialScope,
1521
+ preferences,
1522
+ primaryWorkspaceKey,
1523
+ primary,
1524
+ providerPreferences,
1525
+ workspaces
1526
+ ]
1527
+ );
1423
1528
  React8.useImperativeHandle(controllerRef, () => ({
1424
1529
  focusTrigger: () => triggerRef.current?.focus(),
1425
1530
  prepare: async (requestedSource, options) => {
@@ -1562,6 +1667,9 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1562
1667
  const [instruction, setInstruction] = React8.useState("");
1563
1668
  const [busy, setBusy] = React8.useState(null);
1564
1669
  const [aiFailed, setAiFailed] = React8.useState(false);
1670
+ React8.useEffect(() => {
1671
+ setAiFailed(false);
1672
+ }, [state.snapshot.id, state.draft.revision]);
1565
1673
  const activeAi = React8.useRef(null);
1566
1674
  const activeCapture = React8.useRef(null);
1567
1675
  React8.useEffect(() => {
@@ -1639,7 +1747,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1639
1747
  setBusy("capture");
1640
1748
  try {
1641
1749
  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 });
1750
+ if (fence.valid(id, controller) && latest.current.draft.revision === revision) onChange({ ...latest.current, pending: { ...result, scope }, sourceStale: false, error: result.collectionError });
1643
1751
  } catch (error) {
1644
1752
  if (fence.valid(id, controller)) onChange({ ...latest.current, error: errorMessage(error, "Refresh failed") });
1645
1753
  } finally {
@@ -1872,7 +1980,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1872
1980
  activeCapture.current = null;
1873
1981
  setBusy(null);
1874
1982
  }, 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,
1983
+ ai && aiFailed && 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
1984
  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
1985
  state.error ? /* @__PURE__ */ jsx8("p", { role: "alert", className: "matrx-alchemy-error", children: state.error }) : null
1878
1986
  ] }) : null
@@ -1882,6 +1990,7 @@ function PreparationWorkspace({ restoreFocus, open, state, scopes, onClose, onCh
1882
1990
  /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => setPendingControls(null), children: "Keep edited draft" }),
1883
1991
  /* @__PURE__ */ jsx8(Button, { type: "button", onClick: applyPendingControls, children: "Rebuild from source" })
1884
1992
  ] }) : null,
1993
+ state.sourceStale ? /* @__PURE__ */ jsx8("p", { role: "alert", className: "matrx-alchemy-decision", children: "The source changed since this draft. Use Refresh source to review the latest data." }) : null,
1885
1994
  state.pending ? /* @__PURE__ */ jsxs5("div", { className: "matrx-alchemy-decision", role: "alert", children: [
1886
1995
  /* @__PURE__ */ jsx8("p", { children: "A fresh source snapshot is ready." }),
1887
1996
  /* @__PURE__ */ jsx8(Button, { type: "button", variant: "outline", onClick: () => resolveCapture("keep"), children: "Keep current snapshot and draft" }),