= ({
+ target,
+ usl,
+ lsl,
+ cpkTarget,
+ disabled,
+ idPrefix,
+ ariaPrefix,
+ onChange,
+}) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+interface StepCardProps {
+ step: ProcessMap['nodes'][number];
+ tributaries: ProcessMapTributary[];
+ subgroupAxes: string[];
+ availableColumns: string[];
+ gaps: Gap[];
+ disabled?: boolean;
+ /** Per-step CTQ specs (USL/LSL/target/cpkTarget). Only rendered when ctqColumn is set. */
+ ctqSpecs?: SpecLimits;
+ onRename: (name: string) => void;
+ onCtqChange: (column: string | undefined) => void;
+ onRemove: () => void;
+ onAddTributary: (column: string) => void;
+ onRemoveTributary: (tributaryId: string) => void;
+ onToggleSubgroupAxis: (tributaryId: string) => void;
+ /** Called with the full new SpecLimits shape when any per-step spec input changes. */
+ onCtqSpecsChange?: (next: SpecLimits) => void;
+}
+
+const StepCard: React.FC = ({
+ step,
+ tributaries,
+ subgroupAxes,
+ availableColumns,
+ gaps,
+ disabled,
+ ctqSpecs,
+ onRename,
+ onCtqChange,
+ onRemove,
+ onAddTributary,
+ onRemoveTributary,
+ onToggleSubgroupAxis,
+ onCtqSpecsChange,
+}) => {
+ const [newTribCol, setNewTribCol] = React.useState('');
+ const availableForTrib = availableColumns.filter(
+ c => !tributaries.some(t => t.column === c) && c !== step.ctqColumn
+ );
+
+ return (
+
+
+ onRename(e.target.value)}
+ disabled={disabled}
+ placeholder="Step name"
+ aria-label={`Step ${step.order + 1} name`}
+ className="flex-1 text-sm font-medium bg-transparent border-none focus:outline-none focus:ring-1 focus:ring-edge-strong rounded px-1 disabled:text-content-secondary"
+ data-testid={`process-map-step-name-${step.id}`}
+ />
+ {!disabled && (
+
+ )}
+
+
+
+
+ {step.ctqColumn !== undefined && onCtqSpecsChange && (
+
+ onCtqSpecsChange({ ...next, characteristicType: ctqSpecs?.characteristicType })
+ }
+ />
+ )}
+
+ {tributaries.length > 0 && (
+
+ )}
+
+ {!disabled && availableForTrib.length > 0 && (
+
+
+
+
+ )}
+
+ {gaps.length > 0 && (
+
+ {gaps.map((g, i) => (
+ -
+ ⚠ {g.message}
+
+ ))}
+
+ )}
+
+ );
+};
+
+interface OceanCardProps {
+ ctsColumn?: string;
+ availableColumns: string[];
+ target?: number;
+ usl?: number;
+ lsl?: number;
+ cpkTarget?: number;
+ disabled?: boolean;
+ onCtsChange: (column: string | undefined) => void;
+ onSpecsChange?: (next: {
+ target?: number;
+ usl?: number;
+ lsl?: number;
+ cpkTarget?: number;
+ }) => void;
+}
+
+const OceanCard: React.FC = ({
+ ctsColumn,
+ availableColumns,
+ target,
+ usl,
+ lsl,
+ cpkTarget,
+ disabled,
+ onCtsChange,
+ onSpecsChange,
+}) => {
+ return (
+
+
Customer outcome (CTS)
+
+ {onSpecsChange && (
+
+ onSpecsChange({
+ target: next.target,
+ usl: next.usl,
+ lsl: next.lsl,
+ cpkTarget: next.cpkTarget,
+ })
+ }
+ />
+ )}
+
+ );
+};
+
+interface HunchListProps {
+ hunches: ProcessMapHunch[];
+ steps: ProcessMap['nodes'];
+ tributaries: ProcessMapTributary[];
+ disabled?: boolean;
+ onAdd: (text: string, pin: { stepId?: string; tributaryId?: string }) => void;
+ onRemove: (id: string) => void;
+}
+
+const HunchList: React.FC = ({
+ hunches,
+ steps,
+ tributaries,
+ disabled,
+ onAdd,
+ onRemove,
+}) => {
+ const [text, setText] = React.useState('');
+ const [pinKey, setPinKey] = React.useState('');
+
+ const pinOptions = [
+ ...steps.map(s => ({ key: `step:${s.id}`, label: `step · ${s.name || '(unnamed)'}` })),
+ ...tributaries.map(t => ({ key: `trib:${t.id}`, label: `x · ${t.label || t.column}` })),
+ ];
+
+ const submit = () => {
+ const t = text.trim();
+ if (!t) return;
+ const [kind, id] = pinKey.split(':');
+ onAdd(t, kind === 'step' ? { stepId: id } : kind === 'trib' ? { tributaryId: id } : {});
+ setText('');
+ setPinKey('');
+ };
+
+ const labelForHunch = (h: ProcessMapHunch): string | undefined => {
+ if (h.stepId) return steps.find(s => s.id === h.stepId)?.name;
+ if (h.tributaryId) {
+ const t = tributaries.find(x => x.id === h.tributaryId);
+ return t?.label || t?.column;
+ }
+ return undefined;
+ };
+
+ return (
+
+ );
+};
+
+interface GapStripProps {
+ gaps: Gap[];
+}
+
+const GapStrip: React.FC = ({ gaps }) => {
+ if (gaps.length === 0) return null;
+ const required = gaps.filter(g => g.severity === 'required');
+ const recommended = gaps.filter(g => g.severity === 'recommended');
+ return (
+
+ Missing from your map ({gaps.length})
+ {required.length > 0 && (
+
+ {required.map((g, i) => (
+ -
+ ● required · {g.message}
+
+ ))}
+
+ )}
+ {recommended.length > 0 && (
+
+ {recommended.map((g, i) => (
+ -
+ ○ recommended · {g.message}
+
+ ))}
+
+ )}
+
+ );
+};
+
+// ────────────────────────────────────────────────────────────────────────────
+// Main component
+// ────────────────────────────────────────────────────────────────────────────
+
+export const ProcessMapBase: React.FC = ({
+ map,
+ availableColumns,
+ onChange,
+ gaps,
+ disabled,
+ target,
+ usl,
+ lsl,
+ cpkTarget,
+ onSpecsChange,
+ stepSpecs,
+ onStepSpecsChange,
+ showGaps = true,
+}) => {
+ const sortedSteps = React.useMemo(
+ () => [...map.nodes].sort((a, b) => a.order - b.order),
+ [map.nodes]
+ );
+
+ const update = (next: ProcessMap) => onChange(bumpUpdated(next));
+
+ const addStep = () => {
+ const newOrder = sortedSteps.length;
+ update({
+ ...map,
+ nodes: [...map.nodes, { id: uid('step'), name: '', order: newOrder }],
+ });
+ };
+
+ const renameStep = (stepId: string, name: string) => {
+ update({
+ ...map,
+ nodes: map.nodes.map(n => (n.id === stepId ? { ...n, name } : n)),
+ });
+ };
+
+ const setStepCtq = (stepId: string, ctqColumn: string | undefined) => {
+ update({
+ ...map,
+ nodes: map.nodes.map(n => (n.id === stepId ? { ...n, ctqColumn } : n)),
+ });
+ };
+
+ const removeStep = (stepId: string) => {
+ const remaining = map.nodes.filter(n => n.id !== stepId);
+ // Re-pack `order` so it stays 0..N-1 monotonic.
+ const reordered = [...remaining]
+ .sort((a, b) => a.order - b.order)
+ .map((n, i) => ({ ...n, order: i }));
+ update({
+ ...map,
+ nodes: reordered,
+ tributaries: map.tributaries.filter(t => t.stepId !== stepId),
+ hunches: (map.hunches ?? []).filter(h => h.stepId !== stepId),
+ });
+ };
+
+ const addTributary = (stepId: string, column: string) => {
+ const newT: ProcessMapTributary = { id: uid('trib'), stepId, column };
+ update({ ...map, tributaries: [...map.tributaries, newT] });
+ };
+
+ const removeTributary = (tributaryId: string) => {
+ update({
+ ...map,
+ tributaries: map.tributaries.filter(t => t.id !== tributaryId),
+ subgroupAxes: (map.subgroupAxes ?? []).filter(id => id !== tributaryId),
+ hunches: (map.hunches ?? []).filter(h => h.tributaryId !== tributaryId),
+ });
+ };
+
+ const toggleSubgroupAxis = (tributaryId: string) => {
+ const current = map.subgroupAxes ?? [];
+ const next = current.includes(tributaryId)
+ ? current.filter(id => id !== tributaryId)
+ : [...current, tributaryId];
+ update({ ...map, subgroupAxes: next });
+ };
+
+ const setCts = (ctsColumn: string | undefined) => {
+ update({ ...map, ctsColumn });
+ };
+
+ const addHunch = (text: string, pin: { stepId?: string; tributaryId?: string }) => {
+ const hunch: ProcessMapHunch = { id: uid('hunch'), text, ...pin };
+ update({ ...map, hunches: [...(map.hunches ?? []), hunch] });
+ };
+
+ const removeHunch = (hunchId: string) => {
+ update({
+ ...map,
+ hunches: (map.hunches ?? []).filter(h => h.id !== hunchId),
+ });
+ };
+
+ return (
+
+
+
Process Map
+ {!disabled && (
+
+ )}
+
+
+
+ {sortedSteps.map((step, i) => (
+
+ t.stepId === step.id)}
+ subgroupAxes={map.subgroupAxes ?? []}
+ availableColumns={availableColumns}
+ gaps={gapsForStep(gaps, step.id)}
+ disabled={disabled}
+ ctqSpecs={step.ctqColumn ? stepSpecs?.[step.ctqColumn] : undefined}
+ onRename={name => renameStep(step.id, name)}
+ onCtqChange={col => setStepCtq(step.id, col)}
+ onRemove={() => removeStep(step.id)}
+ onAddTributary={col => addTributary(step.id, col)}
+ onRemoveTributary={removeTributary}
+ onToggleSubgroupAxis={toggleSubgroupAxis}
+ onCtqSpecsChange={
+ onStepSpecsChange && step.ctqColumn
+ ? next => onStepSpecsChange(step.ctqColumn!, next)
+ : undefined
+ }
+ />
+ {i < sortedSteps.length - 1 && (
+
+ →
+
+ )}
+
+ ))}
+ {sortedSteps.length > 0 && (
+
+ →
+
+ )}
+
+
+
+
+
+ {showGaps &&
}
+
+ );
+};
diff --git a/packages/ui/src/components/LayeredProcessView/LayeredProcessView.tsx b/packages/ui/src/components/LayeredProcessView/LayeredProcessView.tsx
index 4df4b45dc..149cd4e16 100644
--- a/packages/ui/src/components/LayeredProcessView/LayeredProcessView.tsx
+++ b/packages/ui/src/components/LayeredProcessView/LayeredProcessView.tsx
@@ -17,7 +17,7 @@
import React from 'react';
import type { ProcessMap, Gap } from '@variscout/core/frame';
import type { SpecLimits } from '@variscout/core';
-import { ProcessMapBase } from '../ProcessMap/ProcessMapBase';
+import { ProcessMapBase } from '../Canvas/internal/ProcessMapBase';
export interface LayeredProcessViewProps {
map: ProcessMap;
diff --git a/packages/ui/src/components/LayeredProcessView/LayeredProcessViewWithCapability.tsx b/packages/ui/src/components/LayeredProcessView/LayeredProcessViewWithCapability.tsx
index d9dcd2d35..d31a5b48e 100644
--- a/packages/ui/src/components/LayeredProcessView/LayeredProcessViewWithCapability.tsx
+++ b/packages/ui/src/components/LayeredProcessView/LayeredProcessViewWithCapability.tsx
@@ -1,72 +1,23 @@
/**
- * LayeredProcessViewWithCapability — composition wrapper.
+ * LayeredProcessViewWithCapability — legacy composition wrapper.
*
- * Mounts ProductionLineGlanceDashboard inside LayeredProcessView's Operations
- * band slot, the dashboard's filter strip above the Outcome band, and a
- * "Show/Hide temporal trends" affordance for progressive reveal.
- *
- * Pure props-based composition — state (mode, filter) owned by the consumer.
+ * Canvas is the canonical FRAME surface. This wrapper remains public during
+ * the canvas migration and delegates to Canvas without changing props.
*
* See spec docs/superpowers/specs/2026-04-28-production-line-glance-surface-wiring-design.md
* section "Three surfaces / 1. LayeredProcessView Operations band".
*/
import React from 'react';
-import { LayeredProcessView, type LayeredProcessViewProps } from './LayeredProcessView';
-import { ProductionLineGlanceDashboard } from '../ProductionLineGlanceDashboard/ProductionLineGlanceDashboard';
-import {
- ProductionLineGlanceFilterStrip,
- type ProductionLineGlanceFilterStripProps,
-} from '../ProductionLineGlanceDashboard/ProductionLineGlanceFilterStrip';
-import type { ProductionLineGlanceDashboardProps } from '../ProductionLineGlanceDashboard/types';
+import { Canvas, type CanvasProps, type ProductionLineGlanceOpsMode } from '../Canvas';
-export type ProductionLineGlanceOpsMode = 'spatial' | 'full';
+export type { ProductionLineGlanceOpsMode };
-export interface LayeredProcessViewWithCapabilityProps extends Omit<
- LayeredProcessViewProps,
- 'operationsBandContent' | 'filterStripContent'
-> {
- data: Pick<
- ProductionLineGlanceDashboardProps,
- 'cpkTrend' | 'cpkGapTrend' | 'capabilityNodes' | 'errorSteps'
- >;
- filter: ProductionLineGlanceFilterStripProps;
- mode: ProductionLineGlanceOpsMode;
- onModeChange: (next: ProductionLineGlanceOpsMode) => void;
- onStepClick?: (nodeId: string) => void;
-}
+export type LayeredProcessViewWithCapabilityProps = CanvasProps;
export const LayeredProcessViewWithCapability: React.FC = ({
- data,
- filter,
- mode,
- onModeChange,
- onStepClick,
- ...layeredProps
+ ...props
}) => {
- const isFull = mode === 'full';
- const affordanceLabel = isFull ? 'Hide temporal trends' : 'Show temporal trends';
- const affordanceArrow = isFull ? '↓' : '↑';
-
- return (
- }
- operationsBandContent={
-
-
-
-
- }
- />
- );
+ return ;
};
export default LayeredProcessViewWithCapability;
diff --git a/packages/ui/src/components/ProcessMap/ProcessMapBase.tsx b/packages/ui/src/components/ProcessMap/ProcessMapBase.tsx
index 422ed98fe..d68eb5eef 100644
--- a/packages/ui/src/components/ProcessMap/ProcessMapBase.tsx
+++ b/packages/ui/src/components/ProcessMap/ProcessMapBase.tsx
@@ -1,807 +1,5 @@
/**
- * ProcessMapBase — the interactive river-styled SIPOC Process Map rendered
- * in the FRAME workspace.
- *
- * See ADR-070 and the design spec `docs/superpowers/specs/2026-04-18-frame-
- * process-map-design.md`. Composes three regions on one canvas:
- *
- * - a left→right spine of process steps (SIPOC temporal axis),
- * - tributaries (little xs / factors) feeding each step from both banks,
- * - an ocean at the right with the CTS (customer-felt outcome) + specs.
- *
- * V1 interactions are deliberately structured (buttons, dropdowns, inline
- * inputs) — not a freeform drag-and-drop canvas. That comes in V2+.
- *
- * Props-based. No store coupling. Parent (FrameView) owns the map state and
- * passes `onChange` callbacks + detected gaps.
+ * @deprecated Canvas owns process-map rendering. Import Canvas instead.
+ * This compatibility re-export remains during the canvas migration.
*/
-
-import React from 'react';
-import type { Gap, ProcessMap, ProcessMapTributary, ProcessMapHunch } from '@variscout/core/frame';
-import type { SpecLimits } from '@variscout/core';
-
-// ────────────────────────────────────────────────────────────────────────────
-// Types
-// ────────────────────────────────────────────────────────────────────────────
-
-export interface ProcessMapBaseProps {
- /** The current map. Parent owns state; pass a new object on every edit. */
- map: ProcessMap;
- /** Column names available from the uploaded dataset. */
- availableColumns: string[];
- /** Called with the next map whenever the user edits. */
- onChange: (next: ProcessMap) => void;
- /** Gaps detected by `detectGaps()` in the parent — rendered inline. */
- gaps?: Gap[];
- /** Disable all edits (read-only mode, e.g. Analysis sidebar thumbnail). */
- disabled?: boolean;
- /** Optional target value for the CTS (current UI: delegated to parent form). */
- target?: number;
- /** Optional USL. */
- usl?: number;
- /** Optional LSL. */
- lsl?: number;
- /** Optional per-characteristic Cpk target ("capability bar" for the CTS column). */
- cpkTarget?: number;
- /** Called when target/usl/lsl/cpkTarget change. Single shape; callers refactor. */
- onSpecsChange?: (next: {
- target?: number;
- usl?: number;
- lsl?: number;
- cpkTarget?: number;
- }) => void;
- /**
- * Per-CTQ-column specs lookup. Each StepCard reads `stepSpecs[step.ctqColumn]`
- * to render its own USL / LSL / target / cpkTarget editor. Mirrors the Ocean
- * pattern (V1 Phase D) — AIAG control plans assume each step's CTQ has its
- * own quality requirement.
- */
- stepSpecs?: Record;
- /**
- * Called when a StepCard's specs change. `column` is the CTQ column for that
- * step. The full `SpecLimits` shape is passed so consumers can `setMeasureSpec(column, next)`.
- */
- onStepSpecsChange?: (column: string, next: SpecLimits) => void;
- /**
- * Whether to render the GapStrip warning bar. Defaults to `true` for backward
- * compatibility with b1+ (process-map authoring) flows. The b0 FrameView passes
- * `false` because the lightweight investigator entry uses inline `+ add spec`
- * affordances and a soft Capability-tab prompt instead of upfront warnings.
- */
- showGaps?: boolean;
-}
-
-// ────────────────────────────────────────────────────────────────────────────
-// Helpers
-// ────────────────────────────────────────────────────────────────────────────
-
-const uid = (prefix: string): string =>
- `${prefix}-${Math.random().toString(36).slice(2, 8)}-${Date.now().toString(36)}`;
-
-const bumpUpdated = (map: ProcessMap): ProcessMap => ({
- ...map,
- updatedAt: new Date().toISOString(),
-});
-
-/** Gaps scoped to a given step, for inline rendering next to the step card. */
-const gapsForStep = (gaps: Gap[] | undefined, stepId: string): Gap[] =>
- (gaps ?? []).filter(g => g.stepId === stepId);
-
-/** Gaps that apply to the whole map (no stepId) — rendered in the GapStrip. */
-const globalGaps = (gaps: Gap[] | undefined): Gap[] => (gaps ?? []).filter(g => !g.stepId);
-
-// ────────────────────────────────────────────────────────────────────────────
-// Sub-components (co-located; not exported from the package)
-// ────────────────────────────────────────────────────────────────────────────
-
-/**
- * SpecsGrid — shared 2x2 input grid for editing USL / LSL / target / cpkTarget.
- *
- * Used by both `OceanCard` (CTS column) and `StepCard` (per-step CTQ column).
- * The grid is fully controlled: it renders the four numeric inputs and emits
- * the full `SpecLimits` shape on each change so callers can route to either
- * the project-wide `setSpecs` or the per-column `setMeasureSpec(column, …)`.
- *
- * `idPrefix` and `ariaPrefix` parameterise the data-testid + aria-label values
- * so the same grid renders distinct accessibility names per surface.
- */
-interface SpecsGridProps {
- target?: number;
- usl?: number;
- lsl?: number;
- cpkTarget?: number;
- disabled?: boolean;
- idPrefix: string;
- ariaPrefix: string;
- onChange: (next: SpecLimits) => void;
-}
-
-const toNum = (s: string): number | undefined => {
- if (s === '') return undefined;
- const n = Number(s);
- return Number.isFinite(n) ? n : undefined;
-};
-
-const SpecsGrid: React.FC = ({
- target,
- usl,
- lsl,
- cpkTarget,
- disabled,
- idPrefix,
- ariaPrefix,
- onChange,
-}) => {
- return (
-
-
-
-
-
-
- );
-};
-
-interface StepCardProps {
- step: ProcessMap['nodes'][number];
- tributaries: ProcessMapTributary[];
- subgroupAxes: string[];
- availableColumns: string[];
- gaps: Gap[];
- disabled?: boolean;
- /** Per-step CTQ specs (USL/LSL/target/cpkTarget). Only rendered when ctqColumn is set. */
- ctqSpecs?: SpecLimits;
- onRename: (name: string) => void;
- onCtqChange: (column: string | undefined) => void;
- onRemove: () => void;
- onAddTributary: (column: string) => void;
- onRemoveTributary: (tributaryId: string) => void;
- onToggleSubgroupAxis: (tributaryId: string) => void;
- /** Called with the full new SpecLimits shape when any per-step spec input changes. */
- onCtqSpecsChange?: (next: SpecLimits) => void;
-}
-
-const StepCard: React.FC = ({
- step,
- tributaries,
- subgroupAxes,
- availableColumns,
- gaps,
- disabled,
- ctqSpecs,
- onRename,
- onCtqChange,
- onRemove,
- onAddTributary,
- onRemoveTributary,
- onToggleSubgroupAxis,
- onCtqSpecsChange,
-}) => {
- const [newTribCol, setNewTribCol] = React.useState('');
- const availableForTrib = availableColumns.filter(
- c => !tributaries.some(t => t.column === c) && c !== step.ctqColumn
- );
-
- return (
-
-
- onRename(e.target.value)}
- disabled={disabled}
- placeholder="Step name"
- aria-label={`Step ${step.order + 1} name`}
- className="flex-1 text-sm font-medium bg-transparent border-none focus:outline-none focus:ring-1 focus:ring-edge-strong rounded px-1 disabled:text-content-secondary"
- data-testid={`process-map-step-name-${step.id}`}
- />
- {!disabled && (
-
- )}
-
-
-
-
- {step.ctqColumn !== undefined && onCtqSpecsChange && (
-
- onCtqSpecsChange({ ...next, characteristicType: ctqSpecs?.characteristicType })
- }
- />
- )}
-
- {tributaries.length > 0 && (
-
- )}
-
- {!disabled && availableForTrib.length > 0 && (
-
-
-
-
- )}
-
- {gaps.length > 0 && (
-
- {gaps.map((g, i) => (
- -
- ⚠ {g.message}
-
- ))}
-
- )}
-
- );
-};
-
-interface OceanCardProps {
- ctsColumn?: string;
- availableColumns: string[];
- target?: number;
- usl?: number;
- lsl?: number;
- cpkTarget?: number;
- disabled?: boolean;
- onCtsChange: (column: string | undefined) => void;
- onSpecsChange?: (next: {
- target?: number;
- usl?: number;
- lsl?: number;
- cpkTarget?: number;
- }) => void;
-}
-
-const OceanCard: React.FC = ({
- ctsColumn,
- availableColumns,
- target,
- usl,
- lsl,
- cpkTarget,
- disabled,
- onCtsChange,
- onSpecsChange,
-}) => {
- return (
-
-
- Customer outcome (CTS)
-
-
- {onSpecsChange && (
-
- onSpecsChange({
- target: next.target,
- usl: next.usl,
- lsl: next.lsl,
- cpkTarget: next.cpkTarget,
- })
- }
- />
- )}
-
- );
-};
-
-interface HunchListProps {
- hunches: ProcessMapHunch[];
- steps: ProcessMap['nodes'];
- tributaries: ProcessMapTributary[];
- disabled?: boolean;
- onAdd: (text: string, pin: { stepId?: string; tributaryId?: string }) => void;
- onRemove: (id: string) => void;
-}
-
-const HunchList: React.FC = ({
- hunches,
- steps,
- tributaries,
- disabled,
- onAdd,
- onRemove,
-}) => {
- const [text, setText] = React.useState('');
- const [pinKey, setPinKey] = React.useState('');
-
- const pinOptions = [
- ...steps.map(s => ({ key: `step:${s.id}`, label: `step · ${s.name || '(unnamed)'}` })),
- ...tributaries.map(t => ({ key: `trib:${t.id}`, label: `x · ${t.label || t.column}` })),
- ];
-
- const submit = () => {
- const t = text.trim();
- if (!t) return;
- const [kind, id] = pinKey.split(':');
- onAdd(t, kind === 'step' ? { stepId: id } : kind === 'trib' ? { tributaryId: id } : {});
- setText('');
- setPinKey('');
- };
-
- const labelForHunch = (h: ProcessMapHunch): string | undefined => {
- if (h.stepId) return steps.find(s => s.id === h.stepId)?.name;
- if (h.tributaryId) {
- const t = tributaries.find(x => x.id === h.tributaryId);
- return t?.label || t?.column;
- }
- return undefined;
- };
-
- return (
-
- );
-};
-
-interface GapStripProps {
- gaps: Gap[];
-}
-
-const GapStrip: React.FC = ({ gaps }) => {
- if (gaps.length === 0) return null;
- const required = gaps.filter(g => g.severity === 'required');
- const recommended = gaps.filter(g => g.severity === 'recommended');
- return (
-
-
- Missing from your map ({gaps.length})
-
- {required.length > 0 && (
-
- {required.map((g, i) => (
- -
- ● required · {g.message}
-
- ))}
-
- )}
- {recommended.length > 0 && (
-
- {recommended.map((g, i) => (
- -
- ○ recommended · {g.message}
-
- ))}
-
- )}
-
- );
-};
-
-// ────────────────────────────────────────────────────────────────────────────
-// Main component
-// ────────────────────────────────────────────────────────────────────────────
-
-export const ProcessMapBase: React.FC = ({
- map,
- availableColumns,
- onChange,
- gaps,
- disabled,
- target,
- usl,
- lsl,
- cpkTarget,
- onSpecsChange,
- stepSpecs,
- onStepSpecsChange,
- showGaps = true,
-}) => {
- const sortedSteps = React.useMemo(
- () => [...map.nodes].sort((a, b) => a.order - b.order),
- [map.nodes]
- );
-
- const update = (next: ProcessMap) => onChange(bumpUpdated(next));
-
- const addStep = () => {
- const newOrder = sortedSteps.length;
- update({
- ...map,
- nodes: [...map.nodes, { id: uid('step'), name: '', order: newOrder }],
- });
- };
-
- const renameStep = (stepId: string, name: string) => {
- update({
- ...map,
- nodes: map.nodes.map(n => (n.id === stepId ? { ...n, name } : n)),
- });
- };
-
- const setStepCtq = (stepId: string, ctqColumn: string | undefined) => {
- update({
- ...map,
- nodes: map.nodes.map(n => (n.id === stepId ? { ...n, ctqColumn } : n)),
- });
- };
-
- const removeStep = (stepId: string) => {
- const remaining = map.nodes.filter(n => n.id !== stepId);
- // Re-pack `order` so it stays 0..N-1 monotonic.
- const reordered = [...remaining]
- .sort((a, b) => a.order - b.order)
- .map((n, i) => ({ ...n, order: i }));
- update({
- ...map,
- nodes: reordered,
- tributaries: map.tributaries.filter(t => t.stepId !== stepId),
- hunches: (map.hunches ?? []).filter(h => h.stepId !== stepId),
- });
- };
-
- const addTributary = (stepId: string, column: string) => {
- const newT: ProcessMapTributary = { id: uid('trib'), stepId, column };
- update({ ...map, tributaries: [...map.tributaries, newT] });
- };
-
- const removeTributary = (tributaryId: string) => {
- update({
- ...map,
- tributaries: map.tributaries.filter(t => t.id !== tributaryId),
- subgroupAxes: (map.subgroupAxes ?? []).filter(id => id !== tributaryId),
- hunches: (map.hunches ?? []).filter(h => h.tributaryId !== tributaryId),
- });
- };
-
- const toggleSubgroupAxis = (tributaryId: string) => {
- const current = map.subgroupAxes ?? [];
- const next = current.includes(tributaryId)
- ? current.filter(id => id !== tributaryId)
- : [...current, tributaryId];
- update({ ...map, subgroupAxes: next });
- };
-
- const setCts = (ctsColumn: string | undefined) => {
- update({ ...map, ctsColumn });
- };
-
- const addHunch = (text: string, pin: { stepId?: string; tributaryId?: string }) => {
- const hunch: ProcessMapHunch = { id: uid('hunch'), text, ...pin };
- update({ ...map, hunches: [...(map.hunches ?? []), hunch] });
- };
-
- const removeHunch = (hunchId: string) => {
- update({
- ...map,
- hunches: (map.hunches ?? []).filter(h => h.id !== hunchId),
- });
- };
-
- return (
-
-
-
Process Map
- {!disabled && (
-
- )}
-
-
-
- {sortedSteps.map((step, i) => (
-
- t.stepId === step.id)}
- subgroupAxes={map.subgroupAxes ?? []}
- availableColumns={availableColumns}
- gaps={gapsForStep(gaps, step.id)}
- disabled={disabled}
- ctqSpecs={step.ctqColumn ? stepSpecs?.[step.ctqColumn] : undefined}
- onRename={name => renameStep(step.id, name)}
- onCtqChange={col => setStepCtq(step.id, col)}
- onRemove={() => removeStep(step.id)}
- onAddTributary={col => addTributary(step.id, col)}
- onRemoveTributary={removeTributary}
- onToggleSubgroupAxis={toggleSubgroupAxis}
- onCtqSpecsChange={
- onStepSpecsChange && step.ctqColumn
- ? next => onStepSpecsChange(step.ctqColumn!, next)
- : undefined
- }
- />
- {i < sortedSteps.length - 1 && (
-
- →
-
- )}
-
- ))}
- {sortedSteps.length > 0 && (
-
- →
-
- )}
-
-
-
-
-
- {showGaps &&
}
-
- );
-};
+export { ProcessMapBase, type ProcessMapBaseProps } from '../Canvas/internal/ProcessMapBase';
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index dd4799e8b..5b0aba45a 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -656,7 +656,8 @@ export {
export { SubgroupConfigPopover, type SubgroupConfigProps } from './components/SubgroupConfig';
// FRAME workspace — visual Process Map (ADR-070)
-export { ProcessMapBase, type ProcessMapBaseProps } from './components/ProcessMap/ProcessMapBase';
+export { Canvas, type CanvasProps } from './components/Canvas';
+export { CanvasWorkspace, type CanvasWorkspaceProps } from './components/Canvas/CanvasWorkspace';
export { LayeredProcessView, type LayeredProcessViewProps } from './components/LayeredProcessView';
export { LayeredProcessViewWithCapability } from './components/LayeredProcessView';
export type {