@pipelex/mthds-ui 0.19.0 → 0.21.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.
@@ -8,13 +8,16 @@ import {
8
8
  buildGraph,
9
9
  buildValidationDecorations,
10
10
  findCousinControllers,
11
+ findStuffByDigest,
11
12
  getLayoutedElements,
12
13
  getPipeBlueprint,
13
14
  resolveConceptRef,
14
15
  resolveIssueTargetNodeId,
15
16
  validateGraphSpec
16
- } from "../../chunk-BFQHQQ3E.js";
17
- import "../../chunk-2N65P32H.js";
17
+ } from "../../chunk-FD4PKRGZ.js";
18
+ import {
19
+ parsePipeRef
20
+ } from "../../chunk-2N65P32H.js";
18
21
  import {
19
22
  DEFAULT_GRAPH_CONFIG,
20
23
  getPaletteForTheme
@@ -43,8 +46,8 @@ import {
43
46
  // src/graph/react/index.ts
44
47
  import "./graph-core.css";
45
48
  import "./detail/DetailPanel.css";
46
- import "./stuff/StuffViewer.css";
47
49
  import "./viewer/GraphToolbar.css";
50
+ import "../../styles/form-kernel.css";
48
51
 
49
52
  // src/graph/react/viewer/GraphViewer.tsx
50
53
  import React10 from "react";
@@ -101,457 +104,75 @@ function useSystemTheme(injected) {
101
104
  return React.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
102
105
  }
103
106
 
104
- // src/graph/react/stuff/stuffViewerUtils.ts
105
- function isSafeDisplayUrl(url) {
106
- if (!url || typeof url !== "string") return false;
107
- return url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://") || url.startsWith("/");
108
- }
109
- function isInlineRenderableUrl(url) {
110
- if (!url || typeof url !== "string") return false;
111
- return url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://") || url.startsWith("/");
112
- }
113
- function extractUrl(data) {
114
- if (!data) return null;
115
- if (typeof data === "string") {
116
- return isSafeDisplayUrl(data) ? data : null;
117
- }
118
- if (typeof data !== "object") return null;
119
- const obj = data;
120
- const candidates = [obj.public_url, obj.src, obj.href, obj.uri, obj.url];
121
- for (const candidate of candidates) {
122
- if (isSafeDisplayUrl(candidate)) return candidate;
123
- }
124
- return null;
125
- }
126
- function extractInlineUrl(data) {
127
- if (!data) return null;
128
- if (typeof data === "string") {
129
- return isInlineRenderableUrl(data) ? data : null;
130
- }
131
- if (typeof data !== "object") return null;
132
- const obj = data;
133
- const candidates = [obj.public_url, obj.src, obj.href, obj.uri, obj.url];
134
- for (const candidate of candidates) {
135
- if (isInlineRenderableUrl(candidate)) return candidate;
136
- }
137
- return null;
138
- }
139
- function extractStorageUri(data) {
140
- if (!data) return null;
141
- if (typeof data === "string") {
142
- return data.startsWith("pipelex-storage://") ? data : null;
143
- }
144
- if (typeof data !== "object") return null;
145
- const obj = data;
146
- const candidates = [obj.uri, obj.url, obj.src, obj.href];
147
- for (const candidate of candidates) {
148
- if (typeof candidate === "string" && candidate.startsWith("pipelex-storage://")) {
149
- return candidate;
150
- }
151
- }
152
- return null;
153
- }
154
- function extractInnerHtml(data) {
155
- const fromObject = (obj2) => {
156
- const value = obj2.inner_html;
157
- if (typeof value === "string" && value.trim() !== "") return value;
158
- return null;
159
- };
160
- if (!data || typeof data !== "object" || Array.isArray(data)) return null;
161
- const obj = data;
162
- const direct = fromObject(obj);
163
- if (direct != null) return direct;
164
- for (const value of Object.values(obj)) {
165
- if (value && typeof value === "object" && !Array.isArray(value)) {
166
- const nested = fromObject(value);
167
- if (nested != null) return nested;
168
- }
169
- }
170
- return null;
171
- }
172
- function extractFilename(data) {
173
- if (!data || typeof data !== "object") return null;
174
- const obj = data;
175
- if (typeof obj.filename === "string" && obj.filename) return obj.filename;
176
- return null;
177
- }
178
- function resolveMimeType(data, contentType, url) {
179
- var _a;
180
- if (contentType && contentType.includes("/")) return contentType;
181
- if (data && typeof data === "object") {
182
- const obj = data;
183
- if (typeof obj.mime_type === "string" && obj.mime_type) return obj.mime_type;
184
- }
185
- const source = extractFilename(data) || url || (typeof data === "string" ? data : null);
186
- if (source) {
187
- const match = source.match(/\.([a-zA-Z0-9]+)(?:\?|#|$)/);
188
- const ext = (_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.toLowerCase();
189
- if (ext) {
190
- if (ext === "pdf") return "application/pdf";
191
- if (["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"].includes(ext)) {
192
- return `image/${ext === "jpg" ? "jpeg" : ext === "svg" ? "svg+xml" : ext}`;
193
- }
194
- }
195
- }
196
- return null;
197
- }
198
- function getHtmlTabLabel(contentType) {
199
- if (contentType === "application/pdf") return "PDF";
200
- if (contentType == null ? void 0 : contentType.startsWith("image/")) return "Image";
201
- return "HTML";
202
- }
203
- function findStuffDataByDigest(graphspec, digest) {
204
- var _a, _b, _c, _d;
205
- for (const node of graphspec.nodes) {
206
- for (const output of (_b = (_a = node.io) == null ? void 0 : _a.outputs) != null ? _b : []) {
207
- if (output.digest === digest) {
208
- return {
209
- digest,
210
- name: output.name,
211
- concept: output.concept,
212
- contentType: output.content_type,
213
- data: output.data,
214
- dataText: output.data_text,
215
- dataHtml: output.data_html
216
- };
217
- }
218
- }
219
- }
220
- for (const node of graphspec.nodes) {
221
- for (const input of (_d = (_c = node.io) == null ? void 0 : _c.inputs) != null ? _d : []) {
222
- if (input.digest === digest) {
223
- return {
224
- digest,
225
- name: input.name,
226
- concept: input.concept,
227
- contentType: input.content_type,
228
- data: input.data,
229
- dataText: input.data_text,
230
- dataHtml: input.data_html
231
- };
232
- }
233
- }
234
- }
235
- return null;
236
- }
237
-
238
- // src/graph/react/stuff/StuffViewer.tsx
239
- import React2 from "react";
240
- import DOMPurify from "dompurify";
107
+ // src/graph/react/detail/StuffResultPanel.tsx
108
+ import * as React2 from "react";
109
+ import {
110
+ buildResultField,
111
+ getPipeInputForm,
112
+ getPipeIOContract,
113
+ getPipeOutputForm
114
+ } from "@pipelex/mthds-form";
115
+ import {
116
+ JsonView,
117
+ ResultEnvProvider,
118
+ StuffViewer,
119
+ useFieldStrings
120
+ } from "@pipelex/mthds-form/react";
241
121
  import { jsx, jsxs } from "react/jsx-runtime";
242
- var ICON_COPY = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" }) });
243
- var ICON_CHECK = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" }) });
244
- var ICON_DOWNLOAD = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" }) });
245
- var ICON_EXTERNAL = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" }) });
246
- var ICON_LOCAL_FILE = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z" }) });
247
- function escapeHtml(text) {
248
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
249
- }
250
- function downloadBlob(content, filename, mimeType) {
251
- const blob = new Blob([content], { type: mimeType });
252
- const url = URL.createObjectURL(blob);
253
- const link = document.createElement("a");
254
- link.href = url;
255
- link.download = filename;
256
- link.click();
257
- setTimeout(() => URL.revokeObjectURL(url), 1e4);
258
- }
259
- function downloadUrl(url, filename) {
260
- const link = document.createElement("a");
261
- link.href = url;
262
- link.download = filename;
263
- link.target = "_blank";
264
- link.click();
265
- }
266
- function StuffViewer({
122
+ function StuffResultPanel({
123
+ contracts,
124
+ outputForm,
125
+ inputForm,
267
126
  stuff,
268
- className,
269
- resolveStorageUrl,
270
- canEmbedPdf,
271
- onOpenExternally
127
+ producerPipeRef,
128
+ consumer,
129
+ resolveUrl,
130
+ resolveShareUrl
272
131
  }) {
273
- var _a, _b;
274
- const [activeTab, setActiveTab] = React2.useState("html");
275
- const [copied, setCopied] = React2.useState(false);
276
- const contentRef = React2.useRef(null);
277
- const httpInlineUrl = React2.useMemo(() => extractInlineUrl(stuff.data), [stuff.data]);
278
- const httpExternalUrl = React2.useMemo(() => extractUrl(stuff.data), [stuff.data]);
279
- const storageUri = React2.useMemo(() => extractStorageUri(stuff.data), [stuff.data]);
280
- const filename = React2.useMemo(() => extractFilename(stuff.data), [stuff.data]);
281
- const innerHtml = React2.useMemo(() => extractInnerHtml(stuff.data), [stuff.data]);
282
- const [resolvedStorageUrl, setResolvedStorageUrl] = React2.useState(null);
283
- React2.useEffect(() => {
284
- setResolvedStorageUrl(null);
285
- if (!storageUri || !resolveStorageUrl || httpInlineUrl) {
286
- return;
287
- }
288
- let cancelled = false;
289
- resolveStorageUrl(storageUri).then((url) => {
290
- if (cancelled) return;
291
- setResolvedStorageUrl(isInlineRenderableUrl(url) ? url : null);
292
- }).catch(() => {
293
- if (!cancelled) setResolvedStorageUrl(null);
294
- });
295
- return () => {
296
- cancelled = true;
297
- };
298
- }, [storageUri, resolveStorageUrl, httpInlineUrl]);
299
- const inlineUrl = httpInlineUrl != null ? httpInlineUrl : resolvedStorageUrl;
300
- const externalUrl = httpExternalUrl != null ? httpExternalUrl : resolvedStorageUrl;
301
- const effectiveMime = React2.useMemo(
302
- () => resolveMimeType(stuff.data, stuff.contentType, externalUrl != null ? externalUrl : storageUri),
303
- [stuff.data, stuff.contentType, externalUrl, storageUri]
132
+ const field = React2.useMemo(
133
+ () => {
134
+ var _a;
135
+ return (_a = fromProducer(contracts, outputForm, producerPipeRef)) != null ? _a : fromConsumer(contracts, inputForm, consumer);
136
+ },
137
+ [contracts, outputForm, inputForm, producerPipeRef, consumer]
304
138
  );
305
- const isPdf = effectiveMime === "application/pdf";
306
- const isImage = (_a = effectiveMime == null ? void 0 : effectiveMime.startsWith("image/")) != null ? _a : false;
307
- const htmlTabLabel = getHtmlTabLabel(effectiveMime != null ? effectiveMime : stuff.contentType);
308
- const { jsonString, jsonError } = React2.useMemo(() => {
309
- if (stuff.data == null) return { jsonString: null, jsonError: null };
310
- try {
311
- return { jsonString: JSON.stringify(stuff.data, null, 2), jsonError: null };
312
- } catch (err) {
313
- return { jsonString: null, jsonError: err instanceof Error ? err.message : String(err) };
314
- }
315
- }, [stuff.data]);
316
- React2.useEffect(() => {
317
- if (activeTab !== "html" || !contentRef.current) return;
318
- contentRef.current.querySelectorAll("a").forEach((link) => {
319
- link.setAttribute("target", "_blank");
320
- link.setAttribute("rel", "noopener noreferrer");
321
- });
322
- }, [activeTab, stuff.dataHtml]);
323
- function renderMediaFallback(mediaLabel) {
324
- const displayName = filename || stuff.name;
325
- return /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-local-file", children: [
326
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-local-file-icon", children: ICON_LOCAL_FILE }),
327
- /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-local-file-info", children: [
328
- displayName && /* @__PURE__ */ jsx("div", { className: "stuff-viewer-local-file-name", children: displayName }),
329
- /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-local-file-hint", children: [
330
- mediaLabel,
331
- " \u2014 no preview available"
332
- ] })
333
- ] })
334
- ] });
335
- }
336
- function renderJsonError(detail) {
337
- return /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-error", role: "alert", children: [
338
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-error-title", children: "Unable to serialize data" }),
339
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-error-detail", children: detail })
340
- ] });
341
- }
342
- function renderContent() {
343
- var _a2;
344
- if (activeTab === "html") {
345
- if (isPdf) {
346
- const canEmbed = canEmbedPdf !== false;
347
- if (canEmbed && inlineUrl) {
348
- const pdfUrl = inlineUrl.includes("#") ? inlineUrl : `${inlineUrl}#pagemode=none`;
349
- return /* @__PURE__ */ jsx("div", { className: "stuff-viewer-pdf", children: /* @__PURE__ */ jsx("embed", { src: pdfUrl, type: "application/pdf" }) });
350
- }
351
- if (externalUrl) {
352
- const displayName = filename || stuff.name;
353
- return /* @__PURE__ */ jsxs(
354
- "button",
355
- {
356
- type: "button",
357
- className: "stuff-viewer-local-file stuff-viewer-local-file--button",
358
- onClick: handleOpenExternal,
359
- children: [
360
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-local-file-icon", children: ICON_LOCAL_FILE }),
361
- /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-local-file-info", children: [
362
- displayName && /* @__PURE__ */ jsx("div", { className: "stuff-viewer-local-file-name", children: displayName }),
363
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-local-file-hint", children: "Click to open PDF externally" })
364
- ] })
365
- ]
366
- }
367
- );
368
- }
369
- return renderMediaFallback("PDF");
370
- }
371
- if (isImage) {
372
- if (inlineUrl) {
373
- return /* @__PURE__ */ jsx("div", { className: "stuff-viewer-image", children: /* @__PURE__ */ jsx("img", { src: inlineUrl, alt: (_a2 = stuff.name) != null ? _a2 : "(unnamed stuff)" }) });
374
- }
375
- return renderMediaFallback("Image");
376
- }
377
- if (innerHtml != null) {
378
- return /* @__PURE__ */ jsx(
379
- "iframe",
380
- {
381
- className: "stuff-viewer-html-frame",
382
- title: stuff.name ? `${stuff.name} preview` : "HTML preview",
383
- sandbox: "",
384
- srcDoc: DOMPurify.sanitize(innerHtml, { WHOLE_DOCUMENT: true })
385
- }
386
- );
387
- }
388
- if (stuff.dataHtml) {
389
- return /* @__PURE__ */ jsx(
390
- "div",
391
- {
392
- className: "stuff-viewer-html",
393
- dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(stuff.dataHtml) }
394
- }
395
- );
396
- }
397
- if (jsonString) {
398
- return /* @__PURE__ */ jsx(
399
- "pre",
400
- {
401
- className: "stuff-viewer-pre",
402
- dangerouslySetInnerHTML: { __html: escapeHtml(jsonString) }
403
- }
404
- );
405
- }
406
- if (jsonError) return renderJsonError(jsonError);
407
- return /* @__PURE__ */ jsx("div", { className: "stuff-viewer-placeholder", children: "No content available" });
408
- }
409
- if (activeTab === "json") {
410
- if (jsonString) {
411
- return /* @__PURE__ */ jsx(
412
- "pre",
413
- {
414
- className: "stuff-viewer-pre",
415
- dangerouslySetInnerHTML: { __html: escapeHtml(jsonString) }
416
- }
417
- );
418
- }
419
- if (jsonError) return renderJsonError(jsonError);
420
- return /* @__PURE__ */ jsx("div", { className: "stuff-viewer-placeholder", children: "No JSON data available" });
421
- }
422
- if (stuff.dataText) {
423
- return /* @__PURE__ */ jsx(
424
- "pre",
425
- {
426
- className: "stuff-viewer-pre stuff-viewer-pre--nowrap",
427
- dangerouslySetInnerHTML: { __html: escapeHtml(stuff.dataText) }
428
- }
429
- );
430
- }
431
- if (jsonString) {
432
- return /* @__PURE__ */ jsx(
433
- "pre",
434
- {
435
- className: "stuff-viewer-pre",
436
- dangerouslySetInnerHTML: { __html: escapeHtml(jsonString) }
437
- }
438
- );
439
- }
440
- if (jsonError) return renderJsonError(jsonError);
441
- return /* @__PURE__ */ jsx("div", { className: "stuff-viewer-placeholder", children: "No text data available" });
442
- }
443
- function handleCopy() {
444
- let textToCopy;
445
- if (activeTab === "json") {
446
- textToCopy = jsonString || "";
447
- } else if (activeTab === "text") {
448
- textToCopy = stuff.dataText || jsonString || "";
449
- } else {
450
- textToCopy = innerHtml || stuff.dataText || stuff.dataHtml || jsonString || "";
451
- }
452
- if (!textToCopy) return;
453
- if (navigator.clipboard) {
454
- navigator.clipboard.writeText(textToCopy).then(() => {
455
- setCopied(true);
456
- setTimeout(() => setCopied(false), 1500);
457
- }).catch(() => {
458
- });
459
- }
139
+ if (field) {
140
+ const panel = /* @__PURE__ */ jsx(StuffViewer, { field, value: stuff.data });
141
+ return resolveUrl || resolveShareUrl ? /* @__PURE__ */ jsx(ResultEnvProvider, { resolveUrl, resolveShareUrl, children: panel }) : panel;
460
142
  }
461
- function handleDownload() {
462
- if (isImage && externalUrl) {
463
- const ext = (effectiveMime == null ? void 0 : effectiveMime.split("/")[1]) || "png";
464
- downloadUrl(externalUrl, `${stuff.name || "stuff"}.${ext}`);
465
- return;
466
- }
467
- if (isPdf && externalUrl) {
468
- downloadUrl(externalUrl, `${stuff.name || "stuff"}.pdf`);
469
- return;
470
- }
471
- if (activeTab === "json") {
472
- downloadBlob(jsonString || "{}", `${stuff.name || "stuff"}.json`, "application/json");
473
- } else if (activeTab === "text") {
474
- downloadBlob(
475
- stuff.dataText || jsonString || "",
476
- `${stuff.name || "stuff"}.txt`,
477
- "text/plain"
478
- );
479
- } else {
480
- downloadBlob(
481
- innerHtml || stuff.dataHtml || jsonString || "",
482
- `${stuff.name || "stuff"}.html`,
483
- "text/html"
484
- );
485
- }
486
- }
487
- function handleOpenExternal() {
488
- if (!externalUrl) return;
489
- if (onOpenExternally) {
490
- onOpenExternally(externalUrl, filename != null ? filename : void 0);
491
- return;
492
- }
493
- window.open(externalUrl, "_blank", "noopener,noreferrer");
494
- }
495
- const rootClass = ["stuff-viewer", className].filter(Boolean).join(" ");
496
- return /* @__PURE__ */ jsxs("div", { className: rootClass, children: [
497
- /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-header", children: [
498
- /* @__PURE__ */ jsx("h3", { className: "stuff-viewer-title", children: (_b = stuff.name) != null ? _b : "(unnamed stuff)" }),
499
- stuff.concept && /* @__PURE__ */ jsx("p", { className: "stuff-viewer-subtitle", children: stuff.concept })
500
- ] }),
501
- /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-toolbar", children: [
502
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-tabs", children: ["html", "json", "text"].map((tab) => {
503
- const label = tab === "html" ? htmlTabLabel : tab === "json" ? "JSON" : "Pretty";
504
- const isActive = activeTab === tab;
505
- return /* @__PURE__ */ jsx(
506
- "button",
507
- {
508
- type: "button",
509
- className: `stuff-viewer-tab${isActive ? " stuff-viewer-tab--active" : ""}`,
510
- onClick: () => setActiveTab(tab),
511
- children: label
512
- },
513
- tab
514
- );
515
- }) }),
516
- /* @__PURE__ */ jsxs("div", { className: "stuff-viewer-actions", children: [
517
- externalUrl && /* @__PURE__ */ jsx(
518
- "button",
519
- {
520
- type: "button",
521
- className: "stuff-viewer-action-btn",
522
- onClick: handleOpenExternal,
523
- title: "Open in new window",
524
- "aria-label": "Open in new window",
525
- children: ICON_EXTERNAL
526
- }
527
- ),
528
- /* @__PURE__ */ jsx(
529
- "button",
530
- {
531
- type: "button",
532
- className: `stuff-viewer-action-btn${copied ? " stuff-viewer-action-btn--copied" : ""}`,
533
- onClick: handleCopy,
534
- title: "Copy",
535
- "aria-label": "Copy",
536
- children: copied ? ICON_CHECK : ICON_COPY
537
- }
538
- ),
539
- /* @__PURE__ */ jsx(
540
- "button",
541
- {
542
- type: "button",
543
- className: "stuff-viewer-action-btn",
544
- onClick: handleDownload,
545
- title: "Download",
546
- "aria-label": "Download",
547
- children: ICON_DOWNLOAD
548
- }
549
- )
550
- ] })
551
- ] }),
552
- /* @__PURE__ */ jsx("div", { className: "stuff-viewer-content", ref: contentRef, children: renderContent() })
143
+ return stuff.data === void 0 || stuff.data === null ? null : /* @__PURE__ */ jsx(UndescribedValue, { value: stuff.data });
144
+ }
145
+ function UndescribedValue({ value }) {
146
+ const s = useFieldStrings();
147
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
148
+ /* @__PURE__ */ jsx("p", { className: "text-[12px] text-muted-foreground", children: s.resultUndescribed }),
149
+ /* @__PURE__ */ jsx(JsonView, { value })
553
150
  ] });
554
151
  }
152
+ function fromProducer(contracts, outputForm, producerPipeRef) {
153
+ if (!producerPipeRef) return null;
154
+ const parsed = parsePipeRef(producerPipeRef);
155
+ if (!(parsed == null ? void 0 : parsed.domainPath)) return null;
156
+ const descriptor = getPipeOutputForm(outputForm, parsed.domainPath, parsed.pipeCode);
157
+ const contract = getPipeIOContract(contracts, parsed.domainPath, parsed.pipeCode);
158
+ if (!descriptor || !contract) return null;
159
+ return buildResultField(descriptor, contract.output.json_schema);
160
+ }
161
+ function fromConsumer(contracts, inputForm, consumer) {
162
+ if (!inputForm || !consumer) return null;
163
+ const parsed = parsePipeRef(consumer.pipeRef);
164
+ if (!(parsed == null ? void 0 : parsed.domainPath)) return null;
165
+ const descriptor = getPipeInputForm(inputForm, parsed.domainPath, parsed.pipeCode);
166
+ const contract = getPipeIOContract(contracts, parsed.domainPath, parsed.pipeCode);
167
+ if (!descriptor || !contract) return null;
168
+ const slot = contract.inputs[consumer.slotName];
169
+ if (!slot || slot.multiplicity !== "single") return null;
170
+ const node = descriptor.fields.find(
171
+ (candidate) => candidate.name === consumer.slotName
172
+ );
173
+ if (!node) return null;
174
+ return buildResultField({ field: node }, slot.json_schema);
175
+ }
555
176
 
556
177
  // src/graph/react/detail/DetailPanel.tsx
557
178
  import React3 from "react";
@@ -1689,9 +1310,7 @@ function ConceptDetailPanel({
1689
1310
  concept,
1690
1311
  ioData,
1691
1312
  isDryRun,
1692
- resolveStorageUrl,
1693
- canEmbedPdf,
1694
- onOpenExternally,
1313
+ renderData,
1695
1314
  instanceKey
1696
1315
  }) {
1697
1316
  var _a;
@@ -1711,23 +1330,15 @@ function ConceptDetailPanel({
1711
1330
  concept,
1712
1331
  ioData,
1713
1332
  isDryRun,
1714
- resolveStorageUrl,
1715
- canEmbedPdf,
1716
- onOpenExternally
1333
+ renderData
1717
1334
  },
1718
1335
  instanceKey != null ? instanceKey : `${concept.code}:${ioData && "digest" in ioData ? ioData.digest : (_a = ioData == null ? void 0 : ioData.name) != null ? _a : ""}`
1719
1336
  )
1720
1337
  ] });
1721
1338
  }
1722
- function ConceptBody({
1723
- concept,
1724
- ioData,
1725
- isDryRun,
1726
- resolveStorageUrl,
1727
- canEmbedPdf,
1728
- onOpenExternally
1729
- }) {
1730
- const hasData = Boolean(ioData) && !isDryRun;
1339
+ function ConceptBody({ concept, ioData, isDryRun, renderData }) {
1340
+ const dataView = ioData && !isDryRun ? renderData == null ? void 0 : renderData() : void 0;
1341
+ const hasData = Boolean(dataView);
1731
1342
  const [activeTab, setActiveTab] = React7.useState(hasData ? "data" : "structure");
1732
1343
  const baseId = React7.useId();
1733
1344
  const tabId = (tab) => `${baseId}-tab-${tab}`;
@@ -1778,15 +1389,7 @@ function ConceptBody({
1778
1389
  renderTab("data", "Data"),
1779
1390
  renderTab("structure", "Structure")
1780
1391
  ] }),
1781
- /* @__PURE__ */ jsx16("div", { role: "tabpanel", id: panelId(activeTab), "aria-labelledby": tabId(activeTab), children: activeTab === "data" ? /* @__PURE__ */ jsx16(
1782
- StuffViewer,
1783
- {
1784
- stuff: toStuffViewerData(ioData),
1785
- resolveStorageUrl,
1786
- canEmbedPdf,
1787
- onOpenExternally
1788
- }
1789
- ) : structure })
1392
+ /* @__PURE__ */ jsx16("div", { role: "tabpanel", id: panelId(activeTab), "aria-labelledby": tabId(activeTab), children: activeTab === "data" ? dataView : structure })
1790
1393
  ] });
1791
1394
  }
1792
1395
  function SchemaTable({ schema }) {
@@ -1827,22 +1430,6 @@ function extractType(schema) {
1827
1430
  }
1828
1431
  return "(unresolved type)";
1829
1432
  }
1830
- function toStuffViewerData(ioData) {
1831
- var _a;
1832
- if (isStuffViewerData(ioData)) return ioData;
1833
- return {
1834
- digest: (_a = ioData.digest) != null ? _a : "",
1835
- name: ioData.name,
1836
- concept: ioData.concept,
1837
- contentType: ioData.content_type,
1838
- data: ioData.data,
1839
- dataText: ioData.data_text,
1840
- dataHtml: ioData.data_html
1841
- };
1842
- }
1843
- function isStuffViewerData(ioData) {
1844
- return "contentType" in ioData || "dataText" in ioData || "dataHtml" in ioData;
1845
- }
1846
1433
 
1847
1434
  // src/graph/react/rfTypes.ts
1848
1435
  function toAppNodes(nodes) {
@@ -2893,36 +2480,44 @@ var nodeTypes = __spreadProps(__spreadValues({}, controllerNodeTypes), {
2893
2480
  function StuffNodeDetail({
2894
2481
  nodeId,
2895
2482
  stuffData,
2483
+ producerPipeRef,
2484
+ consumer,
2896
2485
  graphspec,
2897
- resolveStorageUrl,
2898
- canEmbedPdf,
2899
- onOpenExternally
2486
+ theme,
2487
+ contracts,
2488
+ outputForm,
2489
+ inputForm,
2490
+ resolveUrl,
2491
+ resolveShareUrl
2900
2492
  }) {
2493
+ var _a;
2901
2494
  const conceptInfo = stuffData.concept && graphspec ? resolveConceptRef(graphspec, stuffData.concept) : void 0;
2902
2495
  const isDryRun = graphSpecMode(graphspec) === GRAPH_SPEC_MODE.DRY;
2903
- return /* @__PURE__ */ jsx24(Fragment14, { children: conceptInfo ? /* @__PURE__ */ jsx24(
2904
- ConceptDetailPanel,
2905
- {
2906
- concept: conceptInfo,
2907
- ioData: stuffData,
2908
- isDryRun,
2909
- instanceKey: nodeId,
2910
- resolveStorageUrl,
2911
- canEmbedPdf,
2912
- onOpenExternally
2913
- }
2914
- ) : isDryRun ? /* @__PURE__ */ jsx24("div", { className: "detail-not-available", children: "Dry run data hidden" }) : (
2915
- /* Fallback: just show the StuffViewer if no concept info */
2916
- /* @__PURE__ */ jsx24(
2917
- StuffViewer,
2496
+ const renderData = contracts && outputForm ? () => /* @__PURE__ */ jsx24(
2497
+ StuffResultPanel,
2498
+ __spreadProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadProps(__spreadValues({
2499
+ contracts,
2500
+ outputForm
2501
+ }, inputForm ? { inputForm } : {}), {
2502
+ stuff: stuffData
2503
+ }), conceptInfo ? { concept: conceptInfo } : {}), producerPipeRef ? { producerPipeRef } : {}), consumer ? { consumer } : {}), resolveUrl ? { resolveUrl } : {}), resolveShareUrl ? { resolveShareUrl } : {}), {
2504
+ theme
2505
+ })
2506
+ ) : void 0;
2507
+ if (conceptInfo) {
2508
+ return /* @__PURE__ */ jsx24(
2509
+ ConceptDetailPanel,
2918
2510
  {
2919
- stuff: stuffData,
2920
- resolveStorageUrl,
2921
- canEmbedPdf,
2922
- onOpenExternally
2511
+ concept: conceptInfo,
2512
+ ioData: stuffData,
2513
+ isDryRun,
2514
+ instanceKey: nodeId,
2515
+ renderData
2923
2516
  }
2924
- )
2925
- ) });
2517
+ );
2518
+ }
2519
+ if (isDryRun) return /* @__PURE__ */ jsx24("div", { className: "detail-not-available", children: "Dry run data hidden" });
2520
+ return /* @__PURE__ */ jsx24(Fragment14, { children: (_a = renderData == null ? void 0 : renderData()) != null ? _a : /* @__PURE__ */ jsx24("div", { className: "detail-not-available", children: "No data" }) });
2926
2521
  }
2927
2522
  function seedFoldedControllers(mode, controllerIds) {
2928
2523
  if (mode === FOLD_MODE.FOLDED) return new Set(controllerIds);
@@ -2989,9 +2584,11 @@ function GraphViewer(props) {
2989
2584
  onNodeSelect,
2990
2585
  onPaneClick,
2991
2586
  renderDetailExtra,
2992
- resolveStorageUrl,
2993
- canEmbedPdf,
2994
- onOpenExternally,
2587
+ contracts,
2588
+ outputForm,
2589
+ inputForm,
2590
+ resolveUrl,
2591
+ resolveShareUrl,
2995
2592
  validationState,
2996
2593
  validationIssues,
2997
2594
  onValidationIssueClick
@@ -3492,9 +3089,8 @@ function GraphViewer(props) {
3492
3089
  onNavigateToPipe(code, (_a2 = nodeData.pipeCardData) == null ? void 0 : _a2.status);
3493
3090
  }
3494
3091
  } else if (nodeData.isStuff && onStuffNodeClick && graphspec) {
3495
- const digest = stuffDigestFromId(node.id);
3496
- const sd = findStuffDataByDigest(graphspec, digest);
3497
- if (sd) onStuffNodeClick(sd);
3092
+ const found = findStuffByDigest(graphspec, stuffDigestFromId(node.id));
3093
+ if (found) onStuffNodeClick(found.item);
3498
3094
  }
3499
3095
  setConceptOverride(null);
3500
3096
  if ((detailSelection == null ? void 0 : detailSelection.nodeId) === node.id && !conceptOverride) {
@@ -3502,13 +3098,14 @@ function GraphViewer(props) {
3502
3098
  } else if (nodeData.isPipe || nodeData.isController) {
3503
3099
  setDetailSelection({ kind: "pipe", nodeId: node.id, nodeData });
3504
3100
  } else if (nodeData.isStuff && graphspec) {
3505
- const digest = stuffDigestFromId(node.id);
3506
- const sd = findStuffDataByDigest(graphspec, digest);
3101
+ const found = findStuffByDigest(graphspec, stuffDigestFromId(node.id));
3507
3102
  setDetailSelection({
3508
3103
  kind: "stuff",
3509
3104
  nodeId: node.id,
3510
3105
  nodeData,
3511
- stuffData: sd != null ? sd : void 0
3106
+ stuffData: found == null ? void 0 : found.item,
3107
+ producerPipeRef: found == null ? void 0 : found.producerPipeRef,
3108
+ consumer: found == null ? void 0 : found.consumer
3512
3109
  });
3513
3110
  }
3514
3111
  setNodes((nds) => nds.map((n) => __spreadProps(__spreadValues({}, n), { selected: n.id === node.id })));
@@ -3661,10 +3258,15 @@ function GraphViewer(props) {
3661
3258
  {
3662
3259
  nodeId: detailSelection.nodeId,
3663
3260
  stuffData: detailSelection.stuffData,
3261
+ producerPipeRef: detailSelection.producerPipeRef,
3262
+ consumer: detailSelection.consumer,
3664
3263
  graphspec,
3665
- resolveStorageUrl,
3666
- canEmbedPdf,
3667
- onOpenExternally
3264
+ theme: resolvedTheme,
3265
+ contracts,
3266
+ outputForm,
3267
+ inputForm,
3268
+ resolveUrl,
3269
+ resolveShareUrl
3668
3270
  }
3669
3271
  ) : null,
3670
3272
  renderDetailExtra && detailSelection && !conceptOverride && renderDetailExtra(detailSelection.nodeId, detailSelection.nodeData)
@@ -3683,22 +3285,13 @@ export {
3683
3285
  PipeCardBase,
3684
3286
  PipeCardNode,
3685
3287
  PipeDetailPanel,
3686
- StuffViewer,
3288
+ StuffResultPanel,
3687
3289
  ValidationPanel,
3688
3290
  applyStatusOverrides,
3689
3291
  controllerNodeTypes,
3690
3292
  detectSystemTheme,
3691
- extractFilename,
3692
- extractInlineUrl,
3693
- extractStorageUri,
3694
- extractUrl,
3695
- findStuffDataByDigest,
3696
- getHtmlTabLabel,
3697
3293
  hydrateLabels,
3698
- isInlineRenderableUrl,
3699
- isSafeDisplayUrl,
3700
3294
  renderLabel,
3701
- resolveMimeType,
3702
3295
  toAppEdges,
3703
3296
  toAppNodes,
3704
3297
  useResizable,