{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"flow-viewer","type":"registry:ui","title":"FlowViewer","description":"A workflow, read-only \u2014 by construction, not by a prop. The canvas variant draws the graph; the list variant renders nodes as rows for docs, narrow columns and print, where a pan-zoom surface cannot go. Pass statuses and the same component shows what a run did.","package":"fancy-flow","dependencies":[],"registryDependencies":["canvas","registry","types"],"files":[{"path":"components\/fancy\/flow-viewer\/FlowViewer.tsx","content":"import { useMemo, type CSSProperties, type ReactNode } from \"react\";\nimport { FlowCanvas } from \"..\/canvas\";\nimport { buildNodeTypes } from \"..\/..\/registry\";\nimport { categoryAccent, getNodeKind } from \"..\/..\/registry\/registry\";\nimport type { FlowGraph, FlowNode } from \"..\/..\/types\";\n\n\/** Outcome of one node in a run, for annotating the graph after the fact. *\/\nexport type FlowNodeStatus = \"ok\" | \"running\" | \"failed\" | \"skipped\" | \"pending\";\n\nexport interface FlowViewerClassNames {\n  root?: string;\n  list?: string;\n  row?: string;\n  rowIndex?: string;\n  rowTitle?: string;\n  rowDescription?: string;\n  rowStatus?: string;\n}\n\nexport interface FlowViewerProps {\n  \/** The graph to show. *\/\n  graph: FlowGraph;\n  \/**\n   * `canvas` draws the graph. `list` renders the nodes as rows \u2014 for a docs\n   * page, a narrow column, print, or an audit view, where a pan-zoom surface is\n   * the wrong shape and often not usable at all.\n   *\/\n  variant?: \"canvas\" | \"list\";\n  \/** Canvas height. Ignored by `list`. Default 480. *\/\n  height?: number | string;\n  \/** Canvas minimap. Default false. *\/\n  showMinimap?: boolean;\n  \/** Canvas pan\/zoom\/fit controls. Default true. *\/\n  showControls?: boolean;\n  \/**\n   * Per-node run outcome, keyed by node id. Lets the same component serve\n   * \"here is the workflow\" and \"here is what happened on Tuesday\".\n   *\/\n  statuses?: Record<string, FlowNodeStatus>;\n  \/** Controlled selection. *\/\n  selectedNodeId?: string | null;\n  \/** Omit and nothing is clickable \u2014 a viewer with no handler is inert. *\/\n  onSelectNode?: (node: FlowNode) => void;\n  className?: string;\n  classNames?: FlowViewerClassNames;\n  style?: CSSProperties;\n  \/** Rendered when the graph has no nodes. *\/\n  empty?: ReactNode;\n}\n\nconst STATUS_LABEL: Record<FlowNodeStatus, string> = {\n  ok: \"ok\",\n  running: \"running\",\n  failed: \"failed\",\n  skipped: \"skipped\",\n  pending: \"pending\",\n};\n\n\/**\n * FlowViewer \u2014 a workflow, read-only.\n *\n * **Read-only by construction, not by configuration.** There is no prop that\n * makes this editable, because a viewer that can be switched into an editor is\n * a viewer somebody eventually switches into an editor by accident. Before this\n * existed the only way to show a flow was `FlowEditor` (the whole editor) or\n * `FlowCanvas` plus four React Flow flags a consumer had to know to pass \u2014\n * assembly, not an affordance, and nothing stopped the next person shipping a\n * fully editable canvas where they wanted a picture.\n *\n * Node titles come from the registry, so `overrideNodeKind()` renames apply\n * here too.\n *\/\nexport function FlowViewer({\n  graph,\n  variant = \"canvas\",\n  height = 480,\n  showMinimap = false,\n  showControls = true,\n  statuses,\n  selectedNodeId = null,\n  onSelectNode,\n  className,\n  classNames = {},\n  style,\n  empty,\n}: FlowViewerProps) {\n  const nodeTypes = useMemo(() => buildNodeTypes(), []);\n\n  const rows = useMemo(\n    () =>\n      graph.nodes.map((node, index) => {\n        const kind = getNodeKind(node.data?.kind ?? node.type ?? \"\");\n        return {\n          node,\n          index,\n          title: kind?.label ?? node.data?.label ?? node.data?.kind ?? node.id,\n          description: kind?.description ?? null,\n          accent: kind?.accent ?? categoryAccent(kind?.category ?? \"custom\"),\n          status: statuses?.[node.id] ?? null,\n        };\n      }),\n    [graph.nodes, statuses],\n  );\n\n  if (graph.nodes.length === 0) {\n    return (\n      <div className={cx(\"ff-viewer ff-viewer--empty\", className, classNames.root)} style={style}>\n        {empty ?? <span className=\"ff-viewer__empty\">This flow has no nodes.<\/span>}\n      <\/div>\n    );\n  }\n\n  if (variant === \"list\") {\n    return (\n      <div\n        className={cx(\"ff-viewer ff-viewer--list\", className, classNames.root)}\n        style={style}\n        data-flow-viewer=\"list\"\n      >\n        <ol className={cx(\"ff-viewer__list\", classNames.list)}>\n          {rows.map(({ node, index, title, description, accent, status }) => {\n            const selected = node.id === selectedNodeId;\n            const Row = onSelectNode ? \"button\" : \"div\";\n\n            return (\n              <li key={node.id}>\n                <Row\n                  {...(onSelectNode\n                    ? { type: \"button\" as const, onClick: () => onSelectNode(node) }\n                    : {})}\n                  className={cx(\n                    \"ff-viewer__row\",\n                    selected && \"ff-viewer__row--selected\",\n                    onSelectNode && \"ff-viewer__row--interactive\",\n                    classNames.row,\n                  )}\n                  data-flow-viewer-node={node.id}\n                  data-flow-viewer-status={status ?? undefined}\n                  aria-current={selected || undefined}\n                >\n                  <span\n                    className={cx(\"ff-viewer__index\", classNames.rowIndex)}\n                    style={{ backgroundColor: accent }}\n                    aria-hidden\n                  >\n                    {index + 1}\n                  <\/span>\n\n                  <span className=\"ff-viewer__body\">\n                    <span className={cx(\"ff-viewer__title\", classNames.rowTitle)}>{title}<\/span>\n                    {description && (\n                      <span className={cx(\"ff-viewer__desc\", classNames.rowDescription)}>\n                        {description}\n                      <\/span>\n                    )}\n                  <\/span>\n\n                  {status && (\n                    <span\n                      className={cx(\n                        \"ff-viewer__status\",\n                        `ff-viewer__status--${status}`,\n                        classNames.rowStatus,\n                      )}\n                    >\n                      {STATUS_LABEL[status]}\n                    <\/span>\n                  )}\n                <\/Row>\n              <\/li>\n            );\n          })}\n        <\/ol>\n      <\/div>\n    );\n  }\n\n  return (\n    <div\n      className={cx(\"ff-viewer ff-viewer--canvas\", className, classNames.root)}\n      style={style}\n      data-flow-viewer=\"canvas\"\n    >\n      <FlowCanvas\n        nodes={graph.nodes}\n        edges={graph.edges}\n        nodeTypes={nodeTypes}\n        height={height}\n        showControls={showControls}\n        showMinimap={showMinimap}\n        \/\/ The read-only contract. Every mutation path React Flow offers is\n        \/\/ closed here rather than left to the caller to remember.\n        nodesDraggable={false}\n        nodesConnectable={false}\n        nodesFocusable={Boolean(onSelectNode)}\n        edgesFocusable={false}\n        elementsSelectable={Boolean(onSelectNode)}\n        deleteKeyCode={null}\n        selectionKeyCode={null}\n        multiSelectionKeyCode={null}\n        connectOnClick={false}\n        \/\/ A canvas embedded mid-page must not trap a reader's scroll.\n        zoomOnScroll={false}\n        onNodeClick={onSelectNode ? (_, node) => onSelectNode(node as FlowNode) : undefined}\n        fitView\n      \/>\n    <\/div>\n  );\n}\n\nfunction cx(...parts: Array<string | false | null | undefined>): string {\n  return parts.filter(Boolean).join(\" \");\n}\n","type":"registry:ui","target":"components\/fancy\/flow-viewer\/FlowViewer.tsx"},{"path":"components\/fancy\/flow-viewer\/index.ts","content":"export {\n  FlowViewer,\n  type FlowViewerProps,\n  type FlowViewerClassNames,\n  type FlowNodeStatus,\n} from \".\/FlowViewer\";\n","type":"registry:ui","target":"components\/fancy\/flow-viewer\/index.ts"}]}