@arcote.tech/arc-process 0.7.29 → 0.7.31
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.
- package/package.json +3 -3
- package/src/layout/layout.ts +4 -1
- package/src/mdx/arc.tsx +11 -2
- package/src/mdx/contexts.tsx +15 -1
- package/src/mdx/declare.tsx +147 -0
- package/src/mdx/mdx-view.tsx +10 -3
- package/src/mdx/ref-context.tsx +5 -1
- package/src/model/types.ts +40 -1
- package/src/react/index.ts +12 -0
- package/src/react/nodes.tsx +5 -1
- package/src/react/process-diagram.tsx +46 -284
- package/src/react/process-flow-canvas.tsx +179 -0
- package/src/react/prose-root.tsx +233 -0
- package/tests/build.test.ts +20 -0
- package/tests/layout.test.ts +22 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-process",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.31",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Process description & diagram fragment for Arc framework — generic graph model with profiles, MDX flow components and a deterministic 8-direction layout",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"shiki": "^1.24.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@arcote.tech/arc": "^0.7.
|
|
26
|
-
"@arcote.tech/platform": "^0.7.
|
|
25
|
+
"@arcote.tech/arc": "^0.7.31",
|
|
26
|
+
"@arcote.tech/platform": "^0.7.31",
|
|
27
27
|
"react": ">=18.0.0",
|
|
28
28
|
"react-dom": ">=18.0.0",
|
|
29
29
|
"typescript": "^5.0.0"
|
package/src/layout/layout.ts
CHANGED
|
@@ -323,7 +323,10 @@ export function layoutProcess(input: LayoutInput): ProcessLayout {
|
|
|
323
323
|
const semantics = spec?.semantics ?? "in";
|
|
324
324
|
const from = semantics === "in" ? tId : sId;
|
|
325
325
|
const to = semantics === "in" ? sId : tId;
|
|
326
|
-
|
|
326
|
+
// Nie rysujemy relacji biegnącej prawo→lewo: gdy cel jest w kolumnie
|
|
327
|
+
// wcześniejszej LUB tej samej co źródło (strzałka wsteczna albo pionowa
|
|
328
|
+
// to szum wizualny — przepływ czyta się lewo→prawo).
|
|
329
|
+
if (depthOf(to) <= depthOf(from)) continue;
|
|
327
330
|
const key = `${from}->${to}:${r.kind}`;
|
|
328
331
|
if (seenEdge.has(key)) continue;
|
|
329
332
|
seenEdge.add(key);
|
package/src/mdx/arc.tsx
CHANGED
|
@@ -95,17 +95,26 @@ export function Arc(props: ArcProps) {
|
|
|
95
95
|
},
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
// Most token→realId: gdy konsument poda `resolveNodeId` (np. canvas grafu
|
|
99
|
+
// wiedzy), rozwiązanie i focusId idą przez niego — chip i nod dzielą tę samą
|
|
100
|
+
// przestrzeń id. Brak mostu = domyślne dopasowanie po `graph.nodes`.
|
|
101
|
+
const mappedId =
|
|
102
|
+
ref && data?.resolveNodeId && ref.kind !== CONTEXT_REF_KIND
|
|
103
|
+
? data.resolveNodeId(ref)
|
|
104
|
+
: null;
|
|
98
105
|
const resolvedNode =
|
|
99
106
|
ref && data
|
|
100
107
|
? ref.kind === CONTEXT_REF_KIND
|
|
101
108
|
? resolveContextRef(data.graph.contexts ?? [], ref.nodeId)
|
|
102
|
-
:
|
|
109
|
+
: mappedId
|
|
110
|
+
? { id: mappedId, kind: ref.kind, label: label }
|
|
111
|
+
: data.graph.nodes.find((n) => n.id === ref.nodeId)
|
|
103
112
|
: undefined;
|
|
104
113
|
const resolved = !!resolvedNode;
|
|
105
114
|
const focusId =
|
|
106
115
|
ref?.kind === CONTEXT_REF_KIND && resolvedNode && "id" in resolvedNode
|
|
107
116
|
? `ctx:${resolvedNode.id}`
|
|
108
|
-
: ref?.nodeId;
|
|
117
|
+
: (mappedId ?? ref?.nodeId);
|
|
109
118
|
const active = !!focusId && focus.focusNodeId === focusId;
|
|
110
119
|
// Kolor zawsze z ZADEKLAROWANEGO rodzaju (ref.kind) — element nieistniejący
|
|
111
120
|
// (design-first TODO) zachowuje kolor eventu/komendy itd., a brak w grafie
|
package/src/mdx/contexts.tsx
CHANGED
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
4
|
import type { ProcessProfile } from "../model/profile";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
Direction,
|
|
7
|
+
Occurrence,
|
|
8
|
+
PathSeg,
|
|
9
|
+
ProcessGraph,
|
|
10
|
+
ResolvedRef,
|
|
11
|
+
} from "../model/types";
|
|
6
12
|
import type { ExpansionKey, ProcessViewState } from "../state/view-state";
|
|
7
13
|
|
|
8
14
|
/** Graf + profil — walidacja referencji i kolory chipów. */
|
|
9
15
|
export interface ProcessData {
|
|
10
16
|
graph: ProcessGraph;
|
|
11
17
|
profile: ProcessProfile;
|
|
18
|
+
/**
|
|
19
|
+
* Opcjonalny MOST token→rzeczywiste id noda renderera. Gdy konsument (np.
|
|
20
|
+
* canvas grafu wiedzy BMF) go poda, `<Arc>` liczy `focusId` przez niego —
|
|
21
|
+
* dzięki temu hover chipa i hover noda operują w TEJ SAMEJ przestrzeni id,
|
|
22
|
+
* a dwukierunkowy highlight działa bez zgadywania. Brak = zachowanie
|
|
23
|
+
* domyślne (dopasowanie po `graph.nodes`), jak w arc-map.
|
|
24
|
+
*/
|
|
25
|
+
resolveNodeId?: (ref: ResolvedRef) => string | null;
|
|
12
26
|
}
|
|
13
27
|
export const ProcessDataContext = createContext<ProcessData | null>(null);
|
|
14
28
|
export const useProcessData = () => useContext(ProcessDataContext);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<DeclareNode>` / `<DeclareRelation>` — deklaracja nodów i relacji INLINE
|
|
3
|
+
* w prozie MDX (nie tylko referencja istniejących).
|
|
4
|
+
*
|
|
5
|
+
* Generyczne prymitywy rdzenia: rejestrują wystąpienie (jak `<Arc>`/`<Note>`),
|
|
6
|
+
* uczestniczą w focusie (hover chip↔nod) i selekcji (klik→drawer), a renderer
|
|
7
|
+
* diagramu (np. canvas grafu wiedzy BMF) decyduje, jak je pokazać. Konsument
|
|
8
|
+
* owija je cienkim aliasem (`<Node>`/`<Rel>` dokumentów).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useId, type ReactNode } from "react";
|
|
12
|
+
import { kindColor } from "../model/profile";
|
|
13
|
+
import { useFocus, useProcessData, useRegister, useSelection } from "./contexts";
|
|
14
|
+
import { MISSING_COLOR, tint } from "../react/theme";
|
|
15
|
+
|
|
16
|
+
function textOf(node: ReactNode): string {
|
|
17
|
+
if (node == null || node === false) return "";
|
|
18
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
19
|
+
if (Array.isArray(node)) return node.map(textOf).join("");
|
|
20
|
+
if (typeof node === "object" && "props" in (node as unknown as Record<string, unknown>)) {
|
|
21
|
+
return textOf(
|
|
22
|
+
(node as unknown as { props?: { children?: ReactNode } }).props?.children,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeclareNodeProps {
|
|
29
|
+
/** Id noda (stabilny). Gdy brak — pochodzi z etykiety. */
|
|
30
|
+
id?: string;
|
|
31
|
+
/** Rodzaj (kind) — kolor/ikona z profilu. */
|
|
32
|
+
kind?: string;
|
|
33
|
+
/** Etykieta wyświetlana (fallback: children). */
|
|
34
|
+
label?: string;
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
/** Dowolne dodatkowe dane noda (props domeny). */
|
|
37
|
+
[k: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function slug(s: string): string {
|
|
41
|
+
return s.toLowerCase().trim().replace(/\s+/g, "-");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function DeclareNode(props: DeclareNodeProps) {
|
|
45
|
+
const { id, kind, label, children, ...rest } = props;
|
|
46
|
+
const domId = useId();
|
|
47
|
+
const register = useRegister();
|
|
48
|
+
const data = useProcessData();
|
|
49
|
+
const focus = useFocus();
|
|
50
|
+
const selection = useSelection();
|
|
51
|
+
|
|
52
|
+
const text = label ?? textOf(children);
|
|
53
|
+
const nodeId = id ?? `eph:${slug(text || domId)}`;
|
|
54
|
+
const nodeKind = kind ?? "ephemeral";
|
|
55
|
+
|
|
56
|
+
register?.({
|
|
57
|
+
domId,
|
|
58
|
+
occurrence: {
|
|
59
|
+
type: "declare-node",
|
|
60
|
+
occId: domId,
|
|
61
|
+
node: { id: nodeId, kind: nodeKind, label: text || nodeId, meta: rest },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const active = focus.focusNodeId === nodeId;
|
|
66
|
+
const color = data ? kindColor(data.profile, nodeKind) : MISSING_COLOR;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<span
|
|
70
|
+
data-arc-id={domId}
|
|
71
|
+
data-arc-node={nodeId}
|
|
72
|
+
onMouseEnter={() => focus.setFocusNodeId(nodeId)}
|
|
73
|
+
onMouseLeave={() => focus.setFocusNodeId(null)}
|
|
74
|
+
onClick={() => selection.selectOcc(domId)}
|
|
75
|
+
title={`${nodeKind}: ${nodeId}`}
|
|
76
|
+
style={{
|
|
77
|
+
display: "inline-flex",
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
gap: 4,
|
|
80
|
+
padding: "1px 8px",
|
|
81
|
+
margin: "0 1px",
|
|
82
|
+
borderRadius: 8,
|
|
83
|
+
border: `1.5px solid ${color}`,
|
|
84
|
+
background: tint(color, 0.1),
|
|
85
|
+
color: "#2a173f",
|
|
86
|
+
fontWeight: 600,
|
|
87
|
+
fontSize: "0.92em",
|
|
88
|
+
cursor: "pointer",
|
|
89
|
+
boxShadow: active ? `0 0 0 2px ${color}` : "none",
|
|
90
|
+
whiteSpace: "nowrap",
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<span style={{ width: 7, height: 7, borderRadius: 2, background: color }} />
|
|
94
|
+
{text || nodeId}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DeclareRelationProps {
|
|
100
|
+
from: string;
|
|
101
|
+
to: string;
|
|
102
|
+
kind?: string;
|
|
103
|
+
dir?: "out" | "in" | "both";
|
|
104
|
+
label?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function DeclareRelation({
|
|
108
|
+
from,
|
|
109
|
+
to,
|
|
110
|
+
kind = "rel",
|
|
111
|
+
dir = "out",
|
|
112
|
+
label,
|
|
113
|
+
}: DeclareRelationProps) {
|
|
114
|
+
const domId = useId();
|
|
115
|
+
const register = useRegister();
|
|
116
|
+
const data = useProcessData();
|
|
117
|
+
|
|
118
|
+
register?.({
|
|
119
|
+
domId,
|
|
120
|
+
occurrence: { type: "relation", occId: domId, from, to, kind, dir, label },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const color = data ? kindColor(data.profile, kind) : MISSING_COLOR;
|
|
124
|
+
const arrow = dir === "in" ? "←" : dir === "both" ? "↔" : "→";
|
|
125
|
+
return (
|
|
126
|
+
<span
|
|
127
|
+
data-arc-id={domId}
|
|
128
|
+
title={`${from} ${arrow} ${to}`}
|
|
129
|
+
style={{
|
|
130
|
+
display: "inline-flex",
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
gap: 4,
|
|
133
|
+
padding: "1px 8px",
|
|
134
|
+
margin: "0 1px",
|
|
135
|
+
borderRadius: 999,
|
|
136
|
+
border: `1px solid ${tint(color, 0.55)}`,
|
|
137
|
+
background: tint(color, 0.08),
|
|
138
|
+
color: "#2a173f",
|
|
139
|
+
fontSize: "0.85em",
|
|
140
|
+
fontWeight: 600,
|
|
141
|
+
whiteSpace: "nowrap",
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{from} {label ? `—${label}${arrow}` : arrow} {to}
|
|
145
|
+
</span>
|
|
146
|
+
);
|
|
147
|
+
}
|
package/src/mdx/mdx-view.tsx
CHANGED
|
@@ -87,12 +87,18 @@ export function parseProcessFrontmatter(mdx: string): ProcessFrontmatter {
|
|
|
87
87
|
export function MdxView({
|
|
88
88
|
mdx,
|
|
89
89
|
onOccurrences,
|
|
90
|
+
components,
|
|
90
91
|
}: {
|
|
91
92
|
mdx: string;
|
|
92
93
|
onOccurrences: (occurrences: Occurrence[]) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Dodatkowe / nadpisujące komponenty MDX (np. Ref/Node/Rel/Metric/Formula
|
|
96
|
+
* konsumenta — dokumenty BMF). Scalane nad domyślnym słownikiem procesu.
|
|
97
|
+
*/
|
|
98
|
+
components?: Record<string, ComponentType<Record<string, unknown>>>;
|
|
93
99
|
}) {
|
|
94
100
|
const [Comp, setComp] = useState<ComponentType<{
|
|
95
|
-
components:
|
|
101
|
+
components: Record<string, unknown>;
|
|
96
102
|
}> | null>(null);
|
|
97
103
|
const [error, setError] = useState<string | null>(null);
|
|
98
104
|
|
|
@@ -106,7 +112,7 @@ export function MdxView({
|
|
|
106
112
|
} as Parameters<typeof evaluate>[1])
|
|
107
113
|
.then((mod) => {
|
|
108
114
|
if (!cancelled) {
|
|
109
|
-
setComp(() => mod.default as ComponentType<{ components:
|
|
115
|
+
setComp(() => mod.default as ComponentType<{ components: Record<string, unknown> }>);
|
|
110
116
|
}
|
|
111
117
|
})
|
|
112
118
|
.catch((e) => {
|
|
@@ -127,9 +133,10 @@ export function MdxView({
|
|
|
127
133
|
}
|
|
128
134
|
if (!Comp) return <div style={{ color: GLASS.textMuted }}>Renderuję dokument…</div>;
|
|
129
135
|
|
|
136
|
+
const merged = components ? { ...mdxComponents, ...components } : mdxComponents;
|
|
130
137
|
return (
|
|
131
138
|
<ArcRefsProvider onOccurrences={onOccurrences}>
|
|
132
|
-
<Comp components={
|
|
139
|
+
<Comp components={merged} />
|
|
133
140
|
</ArcRefsProvider>
|
|
134
141
|
);
|
|
135
142
|
}
|
package/src/mdx/ref-context.tsx
CHANGED
|
@@ -41,7 +41,11 @@ export function ArcRefsProvider({
|
|
|
41
41
|
? (o.ref?.key ?? `?${o.occId}`)
|
|
42
42
|
: o.type === "continue"
|
|
43
43
|
? `cont:${o.to}`
|
|
44
|
-
:
|
|
44
|
+
: o.type === "note"
|
|
45
|
+
? `note:${o.occId}`
|
|
46
|
+
: o.type === "declare-node"
|
|
47
|
+
? `decl:${o.node.id}`
|
|
48
|
+
: `rel:${o.from}->${o.to}:${o.kind}`,
|
|
45
49
|
)
|
|
46
50
|
.join("|");
|
|
47
51
|
if (sig === lastSig.current) return;
|
package/src/model/types.ts
CHANGED
|
@@ -190,7 +190,46 @@ export interface NoteOccurrence {
|
|
|
190
190
|
path?: PathSeg[];
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Deklaracja noda INLINE w MDX (`<Node>` dokumentów) — nod istniejący tylko w
|
|
195
|
+
* tej narracji (efemeryczny). Rejestrowany jak krok; renderer diagramu (canvas
|
|
196
|
+
* grafu wiedzy) decyduje, jak go pokazać. `buildProcessModel` (profil
|
|
197
|
+
* procesowy) go ignoruje.
|
|
198
|
+
*/
|
|
199
|
+
export interface DeclareNodeOccurrence {
|
|
200
|
+
type: "declare-node";
|
|
201
|
+
occId: string;
|
|
202
|
+
node: {
|
|
203
|
+
id: string;
|
|
204
|
+
kind: string;
|
|
205
|
+
label: string;
|
|
206
|
+
meta?: Record<string, unknown>;
|
|
207
|
+
};
|
|
208
|
+
path?: PathSeg[];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Deklaracja relacji INLINE w MDX (`<Rel from to>` dokumentów) — krawędź między
|
|
213
|
+
* dowolnymi nodami. Konsumowana przez renderer grafu wiedzy; profil procesowy
|
|
214
|
+
* ją ignoruje.
|
|
215
|
+
*/
|
|
216
|
+
export interface RelationOccurrence {
|
|
217
|
+
type: "relation";
|
|
218
|
+
occId: string;
|
|
219
|
+
from: string;
|
|
220
|
+
to: string;
|
|
221
|
+
kind: string;
|
|
222
|
+
dir?: "out" | "in" | "both";
|
|
223
|
+
label?: string;
|
|
224
|
+
path?: PathSeg[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type Occurrence =
|
|
228
|
+
| StepOccurrence
|
|
229
|
+
| ContinueOccurrence
|
|
230
|
+
| NoteOccurrence
|
|
231
|
+
| DeclareNodeOccurrence
|
|
232
|
+
| RelationOccurrence;
|
|
194
233
|
|
|
195
234
|
// ---------------------------------------------------------------------------
|
|
196
235
|
// Model wynikowy
|
package/src/react/index.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { ProcessDiagram, type ProcessDiagramProps } from "./process-diagram";
|
|
6
|
+
export {
|
|
7
|
+
ProseInteractionRoot,
|
|
8
|
+
type ProseInteractionRootProps,
|
|
9
|
+
type ProseRootRenderApi,
|
|
10
|
+
} from "./prose-root";
|
|
11
|
+
export { ProcessFlowCanvas, type ProcessFlowCanvasProps } from "./process-flow-canvas";
|
|
6
12
|
export { ProcessDrawer, type ProcessDrawerProps, type SourceProvider } from "./drawer";
|
|
7
13
|
export { makeNodeTypes } from "./nodes";
|
|
8
14
|
export { Pin, Person, Metric, type PinProps } from "../mdx/pin";
|
|
@@ -15,6 +21,12 @@ export {
|
|
|
15
21
|
} from "./context-card";
|
|
16
22
|
export { GLASS, BG_GRADIENT, MISSING_COLOR, tint } from "./theme";
|
|
17
23
|
export { Arc, type ArcProps } from "../mdx/arc";
|
|
24
|
+
export {
|
|
25
|
+
DeclareNode,
|
|
26
|
+
DeclareRelation,
|
|
27
|
+
type DeclareNodeProps,
|
|
28
|
+
type DeclareRelationProps,
|
|
29
|
+
} from "../mdx/declare";
|
|
18
30
|
export {
|
|
19
31
|
Branch,
|
|
20
32
|
Continue,
|
package/src/react/nodes.tsx
CHANGED
|
@@ -223,7 +223,11 @@ export function makeNodeTypes(profile: ProcessProfile) {
|
|
|
223
223
|
</span>
|
|
224
224
|
</div>
|
|
225
225
|
<div style={{ fontSize: 10, color: GLASS.textMuted }}>
|
|
226
|
-
{KIND_ICON[declaredKind] ?? "•"}
|
|
226
|
+
{KIND_ICON[declaredKind] ?? "•"}{" "}
|
|
227
|
+
{/* NAZWA elementu (id) zamiast słowa-rodzaju — rodzaj widać po
|
|
228
|
+
ikonie i kolorze, a nazwa mówi, którego elementu dotyczy krok.
|
|
229
|
+
Dla nieistniejących (TODO) pada na nazwę z referencji. */}
|
|
230
|
+
{s.node?.id ?? s.ref?.nodeId ?? declaredKind}
|
|
227
231
|
{s.method ? ` · ${s.method.name}` : ""}
|
|
228
232
|
</div>
|
|
229
233
|
<PinBadges profile={profile} pins={s.pins} />
|
|
@@ -1,54 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ProcessDiagram` — fetch-free viewer procesu z MDX.
|
|
3
3
|
*
|
|
4
|
-
* Proza (MDX z chipami) po lewej, diagram ReactFlow po prawej.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Proza (MDX z chipami) po lewej, diagram ReactFlow po prawej. Cienki wrapper
|
|
5
|
+
* nad wspólnym rdzeniem: `ProseInteractionRoot` (providery + proza + stan) +
|
|
6
|
+
* `ProcessFlowCanvas` (procesowy renderer). Publiczne API bez zmian.
|
|
7
7
|
*
|
|
8
8
|
* Pipeline: MDX → wystąpienia (DOM-order) → buildProcessModel →
|
|
9
|
-
* collapseContexts (
|
|
10
|
-
*
|
|
11
|
-
* toggluje, persystencja przez `store` (localStorage / agregat).
|
|
9
|
+
* collapseContexts → layoutProcess (8 kierunków) → ReactFlow. Stan otwarć:
|
|
10
|
+
* MDX deklaruje start, czytelnik toggluje, persystencja przez `store`.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
import "
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
Background,
|
|
18
|
-
Controls,
|
|
19
|
-
MarkerType,
|
|
20
|
-
MiniMap,
|
|
21
|
-
ReactFlow,
|
|
22
|
-
type Edge,
|
|
23
|
-
type Node,
|
|
24
|
-
} from "@xyflow/react";
|
|
25
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
26
|
-
import { buildProcessModel } from "../model/build";
|
|
27
|
-
import { collapseContexts } from "../model/collapse";
|
|
28
|
-
import { layoutProcess, type DiagramEdge, type DiagramNode } from "../layout/layout";
|
|
29
|
-
import { kindColor, type ProcessProfile } from "../model/profile";
|
|
30
|
-
import {
|
|
31
|
-
EMPTY_GRAPH,
|
|
32
|
-
type Occurrence,
|
|
33
|
-
type ProcessGraph,
|
|
34
|
-
type ProcessStep,
|
|
35
|
-
} from "../model/types";
|
|
36
|
-
import {
|
|
37
|
-
initialViewState,
|
|
38
|
-
toggleContext as toggleContextState,
|
|
39
|
-
toggleGroup as toggleGroupState,
|
|
40
|
-
type ProcessViewState,
|
|
41
|
-
} from "../state/view-state";
|
|
13
|
+
import type { ProcessProfile } from "../model/profile";
|
|
14
|
+
import type { ProcessGraph } from "../model/types";
|
|
42
15
|
import type { ProcessViewStore } from "../state/store";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
ProcessDataContext,
|
|
47
|
-
SelectionContext,
|
|
48
|
-
ViewStateContext,
|
|
49
|
-
} from "../mdx/contexts";
|
|
50
|
-
import { makeNodeTypes } from "./nodes";
|
|
51
|
-
import { ProcessDrawer, type SourceProvider } from "./drawer";
|
|
16
|
+
import { ProseInteractionRoot } from "./prose-root";
|
|
17
|
+
import { ProcessFlowCanvas } from "./process-flow-canvas";
|
|
18
|
+
import type { SourceProvider } from "./drawer";
|
|
52
19
|
import { GLASS } from "./theme";
|
|
53
20
|
|
|
54
21
|
export interface ProcessDiagramProps {
|
|
@@ -76,44 +43,10 @@ export interface ProcessDiagramProps {
|
|
|
76
43
|
sourceProvider?: SourceProvider;
|
|
77
44
|
}
|
|
78
45
|
|
|
79
|
-
function rfNode(n: DiagramNode): Node {
|
|
80
|
-
return {
|
|
81
|
-
id: n.id,
|
|
82
|
-
type: n.type,
|
|
83
|
-
position: { x: n.x, y: n.y },
|
|
84
|
-
width: n.w,
|
|
85
|
-
height: n.h,
|
|
86
|
-
data: n.data,
|
|
87
|
-
draggable: false,
|
|
88
|
-
selectable: n.type !== "context-group",
|
|
89
|
-
zIndex: n.type === "context-group" ? -1 : undefined,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function rfEdge(e: DiagramEdge): Edge {
|
|
94
|
-
return {
|
|
95
|
-
id: e.id,
|
|
96
|
-
source: e.source,
|
|
97
|
-
target: e.target,
|
|
98
|
-
label: e.label,
|
|
99
|
-
style: {
|
|
100
|
-
stroke: e.color,
|
|
101
|
-
strokeWidth: 1.5,
|
|
102
|
-
opacity: e.opacity ?? 0.75,
|
|
103
|
-
strokeDasharray: e.dashed ? "4 3" : undefined,
|
|
104
|
-
},
|
|
105
|
-
labelStyle: { fill: e.color, fontSize: 9, fontWeight: 600 },
|
|
106
|
-
labelBgStyle: { fill: "#fff", fillOpacity: 0.8 },
|
|
107
|
-
markerEnd: e.backbone
|
|
108
|
-
? undefined
|
|
109
|
-
: { type: MarkerType.ArrowClosed, color: e.color },
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
46
|
export function ProcessDiagram({
|
|
114
47
|
mdx,
|
|
115
48
|
profile,
|
|
116
|
-
graph
|
|
49
|
+
graph,
|
|
117
50
|
store,
|
|
118
51
|
docId,
|
|
119
52
|
title,
|
|
@@ -121,218 +54,47 @@ export function ProcessDiagram({
|
|
|
121
54
|
homeContextId,
|
|
122
55
|
sourceProvider,
|
|
123
56
|
}: ProcessDiagramProps) {
|
|
124
|
-
const
|
|
125
|
-
const [viewState, setViewState] = useState<ProcessViewState | null>(null);
|
|
126
|
-
const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
|
|
127
|
-
const [scrollToKey, setScrollToKey] = useState<string | null>(null);
|
|
128
|
-
const [selectedOcc, setSelectedOcc] = useState<string | null>(null);
|
|
129
|
-
const proseRef = useRef<HTMLDivElement>(null);
|
|
130
|
-
|
|
131
|
-
const frontmatter = useMemo(() => parseProcessFrontmatter(mdx), [mdx]);
|
|
132
|
-
const docKey = docId ?? title ?? frontmatter.title ?? mdx.slice(0, 48);
|
|
133
|
-
const collapsedByDefault =
|
|
134
|
-
defaultCollapsed ?? (frontmatter.collapse ?? "all") === "all";
|
|
135
|
-
const home = homeContextId ?? frontmatter.context;
|
|
136
|
-
|
|
137
|
-
// Reset przy zmianie dokumentu.
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
setOccurrences([]);
|
|
140
|
-
setViewState(null);
|
|
141
|
-
setSelectedOcc(null);
|
|
142
|
-
}, [mdx]);
|
|
143
|
-
|
|
144
|
-
const model = useMemo(
|
|
145
|
-
() => buildProcessModel({ graph, profile, occurrences }),
|
|
146
|
-
[graph, profile, occurrences],
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// Kontekst domowy: id lub nazwa z frontmatter/propsa.
|
|
150
|
-
const homeId = useMemo(() => {
|
|
151
|
-
if (!home) return undefined;
|
|
152
|
-
const ctxs = graph.contexts ?? [];
|
|
153
|
-
return (ctxs.find((c) => c.id === home) ?? ctxs.find((c) => c.name === home))?.id;
|
|
154
|
-
}, [graph, home]);
|
|
155
|
-
|
|
156
|
-
// Stan efektywny: jawny stan czytelnika → zapisany w store → startowy z MDX.
|
|
157
|
-
const effectiveState = useMemo<ProcessViewState>(() => {
|
|
158
|
-
if (viewState) return viewState;
|
|
159
|
-
const stored = store?.load(docKey);
|
|
160
|
-
if (stored) return stored;
|
|
161
|
-
return initialViewState(model);
|
|
162
|
-
}, [viewState, store, docKey, model]);
|
|
163
|
-
|
|
164
|
-
const updateState = useCallback(
|
|
165
|
-
(next: ProcessViewState) => {
|
|
166
|
-
setViewState(next);
|
|
167
|
-
store?.save(docKey, next);
|
|
168
|
-
},
|
|
169
|
-
[store, docKey],
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const collapse = useMemo(
|
|
173
|
-
() =>
|
|
174
|
-
collapseContexts(model, {
|
|
175
|
-
contexts: graph.contexts ?? [],
|
|
176
|
-
state: effectiveState,
|
|
177
|
-
defaultCollapsed: collapsedByDefault,
|
|
178
|
-
homeContextId: homeId,
|
|
179
|
-
}),
|
|
180
|
-
[model, graph, effectiveState, collapsedByDefault, homeId],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const layout = useMemo(
|
|
184
|
-
() => layoutProcess({ model, collapse, graph, profile, state: effectiveState }),
|
|
185
|
-
[model, collapse, graph, profile, effectiveState],
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
const nodeTypes = useMemo(() => makeNodeTypes(profile), [profile]);
|
|
189
|
-
const nodes = useMemo(() => layout.nodes.map(rfNode), [layout]);
|
|
190
|
-
const edges = useMemo(() => layout.edges.map(rfEdge), [layout]);
|
|
191
|
-
|
|
192
|
-
// occId kroku -> klucz refa (klik w nod przewija prozę do chipu).
|
|
193
|
-
const keyByOcc = useMemo(() => {
|
|
194
|
-
const m = new Map<string, string>();
|
|
195
|
-
for (const s of model.steps) if (s.ref) m.set(s.occId, s.ref.key);
|
|
196
|
-
return m;
|
|
197
|
-
}, [model]);
|
|
198
|
-
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
if (!scrollToKey || !proseRef.current) return;
|
|
201
|
-
const el = proseRef.current.querySelector(
|
|
202
|
-
`[data-arc-key="${CSS.escape(scrollToKey)}"]`,
|
|
203
|
-
);
|
|
204
|
-
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
205
|
-
setScrollToKey(null);
|
|
206
|
-
}, [scrollToKey]);
|
|
207
|
-
|
|
208
|
-
const focusState = useMemo(
|
|
209
|
-
() => ({ focusNodeId, setFocusNodeId, scrollToKey, setScrollToKey }),
|
|
210
|
-
[focusNodeId, scrollToKey],
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const viewStateActions = useMemo(
|
|
214
|
-
() => ({
|
|
215
|
-
state: effectiveState,
|
|
216
|
-
toggleGroup: (key: string, group: string) =>
|
|
217
|
-
updateState(toggleGroupState(effectiveState, key, group)),
|
|
218
|
-
toggleContext: (contextId: string) =>
|
|
219
|
-
updateState(
|
|
220
|
-
toggleContextState(effectiveState, contextId, collapsedByDefault),
|
|
221
|
-
),
|
|
222
|
-
}),
|
|
223
|
-
[effectiveState, updateState, collapsedByDefault],
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
const processData = useMemo(() => ({ graph, profile }), [graph, profile]);
|
|
227
|
-
|
|
228
|
-
const selectionState = useMemo(
|
|
229
|
-
() => ({ selectedOcc, selectOcc: setSelectedOcc }),
|
|
230
|
-
[selectedOcc],
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
// Krok do drawera: wybrany occId szukamy najpierw wśród kroków po collapse
|
|
234
|
-
// (pseudo-kroki boxów), potem w oryginalnych (chip prozy członka boxa).
|
|
235
|
-
const selectedStep = useMemo(() => {
|
|
236
|
-
if (!selectedOcc) return null;
|
|
237
|
-
const group = collapse.groupByOcc.get(selectedOcc);
|
|
238
|
-
return (
|
|
239
|
-
collapse.steps.find((s) => s.occId === (group ?? selectedOcc)) ??
|
|
240
|
-
model.steps.find((s) => s.occId === selectedOcc) ??
|
|
241
|
-
null
|
|
242
|
-
);
|
|
243
|
-
}, [selectedOcc, collapse, model]);
|
|
244
|
-
|
|
57
|
+
const docKey = docId ?? title;
|
|
245
58
|
return (
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
59
|
+
<ProseInteractionRoot
|
|
60
|
+
mdx={mdx}
|
|
61
|
+
profile={profile}
|
|
62
|
+
graph={graph}
|
|
63
|
+
store={store}
|
|
64
|
+
docId={docId}
|
|
65
|
+
title={title}
|
|
66
|
+
defaultCollapsed={defaultCollapsed}
|
|
67
|
+
homeContextId={homeContextId}
|
|
68
|
+
>
|
|
69
|
+
{({ Prose, proseRef, model, collapse }) => (
|
|
70
|
+
<div
|
|
71
|
+
style={{
|
|
72
|
+
display: "grid",
|
|
73
|
+
gridTemplateColumns: "minmax(320px, 1fr) minmax(420px, 1.3fr)",
|
|
74
|
+
height: "100%",
|
|
75
|
+
minHeight: 0,
|
|
76
|
+
overflow: "hidden",
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
250
79
|
<div
|
|
80
|
+
ref={proseRef}
|
|
251
81
|
style={{
|
|
252
|
-
|
|
253
|
-
gridTemplateColumns: "minmax(320px, 1fr) minmax(420px, 1.3fr)",
|
|
254
|
-
height: "100%",
|
|
82
|
+
overflow: "auto",
|
|
255
83
|
minHeight: 0,
|
|
256
|
-
|
|
84
|
+
padding: "20px 24px",
|
|
85
|
+
borderRight: `1px solid ${GLASS.border}`,
|
|
257
86
|
}}
|
|
258
87
|
>
|
|
259
|
-
{
|
|
260
|
-
<div
|
|
261
|
-
ref={proseRef}
|
|
262
|
-
style={{
|
|
263
|
-
overflow: "auto",
|
|
264
|
-
minHeight: 0,
|
|
265
|
-
padding: "20px 24px",
|
|
266
|
-
borderRight: `1px solid ${GLASS.border}`,
|
|
267
|
-
}}
|
|
268
|
-
>
|
|
269
|
-
<MdxView key={docKey} mdx={mdx} onOccurrences={setOccurrences} />
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
{/* Diagram — sztywno wypełnia dostępną wysokość, bez scrolla strony */}
|
|
273
|
-
<div style={{ position: "relative", minHeight: 0, overflow: "hidden" }}>
|
|
274
|
-
<ReactFlow
|
|
275
|
-
key={docKey}
|
|
276
|
-
nodes={nodes}
|
|
277
|
-
edges={edges}
|
|
278
|
-
nodeTypes={nodeTypes}
|
|
279
|
-
nodesDraggable={false}
|
|
280
|
-
onNodeMouseEnter={(_, n) => {
|
|
281
|
-
const step = (n.data as { step?: ProcessStep }).step;
|
|
282
|
-
const id =
|
|
283
|
-
step?.node?.id ??
|
|
284
|
-
(n.data as { node?: { id: string } }).node?.id;
|
|
285
|
-
if (id) setFocusNodeId(id);
|
|
286
|
-
}}
|
|
287
|
-
onNodeMouseLeave={() => setFocusNodeId(null)}
|
|
288
|
-
onNodeClick={(_, n) => {
|
|
289
|
-
const k = keyByOcc.get(n.id);
|
|
290
|
-
if (k) setScrollToKey(k);
|
|
291
|
-
// Kroki (także boxy kontekstów) otwierają drawer.
|
|
292
|
-
if (n.type === "step") setSelectedOcc(n.id);
|
|
293
|
-
}}
|
|
294
|
-
onPaneClick={() => setSelectedOcc(null)}
|
|
295
|
-
fitView
|
|
296
|
-
fitViewOptions={{ padding: 0.18 }}
|
|
297
|
-
minZoom={0.1}
|
|
298
|
-
proOptions={{ hideAttribution: true }}
|
|
299
|
-
>
|
|
300
|
-
<Background color="#dbeafe" gap={26} size={1.5} />
|
|
301
|
-
<Controls />
|
|
302
|
-
<MiniMap
|
|
303
|
-
pannable
|
|
304
|
-
zoomable
|
|
305
|
-
nodeColor={(n) => {
|
|
306
|
-
const step = (n.data as { step?: ProcessStep }).step;
|
|
307
|
-
// Kolor z zadeklarowanego rodzaju — także dla TODO.
|
|
308
|
-
const kind =
|
|
309
|
-
step?.node?.kind ??
|
|
310
|
-
step?.ref?.kind ??
|
|
311
|
-
(n.data as { node?: { kind: string } }).node?.kind;
|
|
312
|
-
return kind ? kindColor(profile, kind) : "#64748b";
|
|
313
|
-
}}
|
|
314
|
-
style={{
|
|
315
|
-
background: "rgba(255,255,255,0.6)",
|
|
316
|
-
border: `1px solid ${GLASS.border}`,
|
|
317
|
-
borderRadius: 12,
|
|
318
|
-
}}
|
|
319
|
-
/>
|
|
320
|
-
</ReactFlow>
|
|
321
|
-
|
|
322
|
-
{/* Drawer szczegółów wybranego kroku. */}
|
|
323
|
-
{selectedStep ? (
|
|
324
|
-
<ProcessDrawer
|
|
325
|
-
step={selectedStep}
|
|
326
|
-
profile={profile}
|
|
327
|
-
onClose={() => setSelectedOcc(null)}
|
|
328
|
-
sourceProvider={sourceProvider}
|
|
329
|
-
/>
|
|
330
|
-
) : null}
|
|
331
|
-
</div>
|
|
88
|
+
{Prose}
|
|
332
89
|
</div>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
90
|
+
<ProcessFlowCanvas
|
|
91
|
+
model={model}
|
|
92
|
+
collapse={collapse}
|
|
93
|
+
docKey={docKey}
|
|
94
|
+
sourceProvider={sourceProvider}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</ProseInteractionRoot>
|
|
337
99
|
);
|
|
338
100
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ProcessFlowCanvas` — procesowy renderer diagramu (ReactFlow + drawer).
|
|
3
|
+
*
|
|
4
|
+
* Slot diagramu dla `ProseInteractionRoot`: czyta graf/profil/focus/selekcję/
|
|
5
|
+
* stan z kontekstu, liczy layout (8 kierunków, track sizing) i renderuje
|
|
6
|
+
* ReactFlow. To JEDYNE miejsce importujące `@xyflow` — rdzeń prozy jest od
|
|
7
|
+
* niego wolny.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import "@xyflow/react/dist/style.css";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Background,
|
|
14
|
+
Controls,
|
|
15
|
+
MarkerType,
|
|
16
|
+
MiniMap,
|
|
17
|
+
ReactFlow,
|
|
18
|
+
type Edge,
|
|
19
|
+
type Node,
|
|
20
|
+
} from "@xyflow/react";
|
|
21
|
+
import { useMemo } from "react";
|
|
22
|
+
import type { CollapseResult } from "../model/collapse";
|
|
23
|
+
import { layoutProcess, type DiagramEdge, type DiagramNode } from "../layout/layout";
|
|
24
|
+
import { kindColor } from "../model/profile";
|
|
25
|
+
import type { ProcessModel, ProcessStep } from "../model/types";
|
|
26
|
+
import {
|
|
27
|
+
useFocus,
|
|
28
|
+
useProcessData,
|
|
29
|
+
useSelection,
|
|
30
|
+
useViewState,
|
|
31
|
+
} from "../mdx/contexts";
|
|
32
|
+
import { makeNodeTypes } from "./nodes";
|
|
33
|
+
import { ProcessDrawer, type SourceProvider } from "./drawer";
|
|
34
|
+
import { GLASS } from "./theme";
|
|
35
|
+
|
|
36
|
+
function rfNode(n: DiagramNode): Node {
|
|
37
|
+
return {
|
|
38
|
+
id: n.id,
|
|
39
|
+
type: n.type,
|
|
40
|
+
position: { x: n.x, y: n.y },
|
|
41
|
+
width: n.w,
|
|
42
|
+
height: n.h,
|
|
43
|
+
data: n.data,
|
|
44
|
+
draggable: false,
|
|
45
|
+
selectable: n.type !== "context-group",
|
|
46
|
+
zIndex: n.type === "context-group" ? -1 : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rfEdge(e: DiagramEdge): Edge {
|
|
51
|
+
return {
|
|
52
|
+
id: e.id,
|
|
53
|
+
source: e.source,
|
|
54
|
+
target: e.target,
|
|
55
|
+
label: e.label,
|
|
56
|
+
style: {
|
|
57
|
+
stroke: e.color,
|
|
58
|
+
strokeWidth: 1.5,
|
|
59
|
+
opacity: e.opacity ?? 0.75,
|
|
60
|
+
strokeDasharray: e.dashed ? "4 3" : undefined,
|
|
61
|
+
},
|
|
62
|
+
labelStyle: { fill: e.color, fontSize: 9, fontWeight: 600 },
|
|
63
|
+
labelBgStyle: { fill: "#fff", fillOpacity: 0.8 },
|
|
64
|
+
markerEnd: e.backbone
|
|
65
|
+
? undefined
|
|
66
|
+
: { type: MarkerType.ArrowClosed, color: e.color },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProcessFlowCanvasProps {
|
|
71
|
+
model: ProcessModel;
|
|
72
|
+
collapse: CollapseResult;
|
|
73
|
+
/** Klucz remountu (docId) — świeży ReactFlow przy zmianie dokumentu. */
|
|
74
|
+
docKey?: string;
|
|
75
|
+
sourceProvider?: SourceProvider;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ProcessFlowCanvas({
|
|
79
|
+
model,
|
|
80
|
+
collapse,
|
|
81
|
+
docKey,
|
|
82
|
+
sourceProvider,
|
|
83
|
+
}: ProcessFlowCanvasProps) {
|
|
84
|
+
const data = useProcessData();
|
|
85
|
+
const focus = useFocus();
|
|
86
|
+
const selection = useSelection();
|
|
87
|
+
const viewState = useViewState();
|
|
88
|
+
const profile = data!.profile;
|
|
89
|
+
const graph = data!.graph;
|
|
90
|
+
|
|
91
|
+
const layout = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
layoutProcess({
|
|
94
|
+
model,
|
|
95
|
+
collapse,
|
|
96
|
+
graph,
|
|
97
|
+
profile,
|
|
98
|
+
state: viewState.state,
|
|
99
|
+
}),
|
|
100
|
+
[model, collapse, graph, profile, viewState.state],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const nodeTypes = useMemo(() => makeNodeTypes(profile), [profile]);
|
|
104
|
+
const nodes = useMemo(() => layout.nodes.map(rfNode), [layout]);
|
|
105
|
+
const edges = useMemo(() => layout.edges.map(rfEdge), [layout]);
|
|
106
|
+
|
|
107
|
+
const keyByOcc = useMemo(() => {
|
|
108
|
+
const m = new Map<string, string>();
|
|
109
|
+
for (const s of model.steps) if (s.ref) m.set(s.occId, s.ref.key);
|
|
110
|
+
return m;
|
|
111
|
+
}, [model]);
|
|
112
|
+
|
|
113
|
+
const selectedStep = useMemo(() => {
|
|
114
|
+
if (!selection.selectedOcc) return null;
|
|
115
|
+
const group = collapse.groupByOcc.get(selection.selectedOcc);
|
|
116
|
+
return (
|
|
117
|
+
collapse.steps.find((s) => s.occId === (group ?? selection.selectedOcc)) ??
|
|
118
|
+
model.steps.find((s) => s.occId === selection.selectedOcc) ??
|
|
119
|
+
null
|
|
120
|
+
);
|
|
121
|
+
}, [selection.selectedOcc, collapse, model]);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div style={{ position: "relative", minHeight: 0, height: "100%", overflow: "hidden" }}>
|
|
125
|
+
<ReactFlow
|
|
126
|
+
key={docKey}
|
|
127
|
+
nodes={nodes}
|
|
128
|
+
edges={edges}
|
|
129
|
+
nodeTypes={nodeTypes}
|
|
130
|
+
nodesDraggable={false}
|
|
131
|
+
onNodeMouseEnter={(_, n) => {
|
|
132
|
+
const step = (n.data as { step?: ProcessStep }).step;
|
|
133
|
+
const id = step?.node?.id ?? (n.data as { node?: { id: string } }).node?.id;
|
|
134
|
+
if (id) focus.setFocusNodeId(id);
|
|
135
|
+
}}
|
|
136
|
+
onNodeMouseLeave={() => focus.setFocusNodeId(null)}
|
|
137
|
+
onNodeClick={(_, n) => {
|
|
138
|
+
const k = keyByOcc.get(n.id);
|
|
139
|
+
if (k) focus.setScrollToKey(k);
|
|
140
|
+
if (n.type === "step") selection.selectOcc(n.id);
|
|
141
|
+
}}
|
|
142
|
+
onPaneClick={() => selection.selectOcc(null)}
|
|
143
|
+
fitView
|
|
144
|
+
fitViewOptions={{ padding: 0.18 }}
|
|
145
|
+
minZoom={0.1}
|
|
146
|
+
proOptions={{ hideAttribution: true }}
|
|
147
|
+
>
|
|
148
|
+
<Background color="#dbeafe" gap={26} size={1.5} />
|
|
149
|
+
<Controls />
|
|
150
|
+
<MiniMap
|
|
151
|
+
pannable
|
|
152
|
+
zoomable
|
|
153
|
+
nodeColor={(n) => {
|
|
154
|
+
const step = (n.data as { step?: ProcessStep }).step;
|
|
155
|
+
const kind =
|
|
156
|
+
step?.node?.kind ??
|
|
157
|
+
step?.ref?.kind ??
|
|
158
|
+
(n.data as { node?: { kind: string } }).node?.kind;
|
|
159
|
+
return kind ? kindColor(profile, kind) : "#64748b";
|
|
160
|
+
}}
|
|
161
|
+
style={{
|
|
162
|
+
background: "rgba(255,255,255,0.6)",
|
|
163
|
+
border: `1px solid ${GLASS.border}`,
|
|
164
|
+
borderRadius: 12,
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</ReactFlow>
|
|
168
|
+
|
|
169
|
+
{selectedStep ? (
|
|
170
|
+
<ProcessDrawer
|
|
171
|
+
step={selectedStep}
|
|
172
|
+
profile={profile}
|
|
173
|
+
onClose={() => selection.selectOcc(null)}
|
|
174
|
+
sourceProvider={sourceProvider}
|
|
175
|
+
/>
|
|
176
|
+
) : null}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ProseInteractionRoot` — headless rdzeń interakcji „proza MDX + chipy".
|
|
3
|
+
*
|
|
4
|
+
* Trzyma CAŁY stan (wystąpienia, model, stan widoku, focus, selekcja) i
|
|
5
|
+
* providery (ProcessData/Focus/Selection/ViewState), renderuje prozę i
|
|
6
|
+
* udostępnia współdzielone API render-propem. NIE importuje ReactFlow — dzięki
|
|
7
|
+
* temu konsument prozy (np. dokumenty BMF z własnym canvasem grafu wiedzy) nie
|
|
8
|
+
* ciągnie `@xyflow`. Renderer diagramu jest SLOTEM: `ProcessDiagram` wpina
|
|
9
|
+
* `ProcessFlowCanvas`, dokumenty wpinają swój canvas — OBA czytają ten sam
|
|
10
|
+
* `FocusContext`/`SelectionContext`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
useCallback,
|
|
15
|
+
useEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
type ComponentType,
|
|
20
|
+
type ReactNode,
|
|
21
|
+
type RefObject,
|
|
22
|
+
} from "react";
|
|
23
|
+
import { buildProcessModel } from "../model/build";
|
|
24
|
+
import { collapseContexts, type CollapseResult } from "../model/collapse";
|
|
25
|
+
import type { ProcessProfile } from "../model/profile";
|
|
26
|
+
import {
|
|
27
|
+
EMPTY_GRAPH,
|
|
28
|
+
type Occurrence,
|
|
29
|
+
type ProcessGraph,
|
|
30
|
+
type ProcessModel,
|
|
31
|
+
type ResolvedRef,
|
|
32
|
+
} from "../model/types";
|
|
33
|
+
import {
|
|
34
|
+
initialViewState,
|
|
35
|
+
toggleContext as toggleContextState,
|
|
36
|
+
toggleGroup as toggleGroupState,
|
|
37
|
+
type ProcessViewState,
|
|
38
|
+
} from "../state/view-state";
|
|
39
|
+
import type { ProcessViewStore } from "../state/store";
|
|
40
|
+
import { MdxView, parseProcessFrontmatter } from "../mdx/mdx-view";
|
|
41
|
+
import {
|
|
42
|
+
FocusContext,
|
|
43
|
+
ProcessDataContext,
|
|
44
|
+
SelectionContext,
|
|
45
|
+
ViewStateContext,
|
|
46
|
+
type FocusState,
|
|
47
|
+
type SelectionState,
|
|
48
|
+
type ViewStateActions,
|
|
49
|
+
} from "../mdx/contexts";
|
|
50
|
+
|
|
51
|
+
/** API współdzielone przez rdzeń z rendererem diagramu (render-prop). */
|
|
52
|
+
export interface ProseRootRenderApi {
|
|
53
|
+
/** Gotowy element prozy (MdxView w ArcRefsProvider). Osadź w kolumnie prozy. */
|
|
54
|
+
Prose: ReactNode;
|
|
55
|
+
/** Ref kolumny prozy — podepnij do kontenera, by działał scroll-do-chipu. */
|
|
56
|
+
proseRef: RefObject<HTMLDivElement | null>;
|
|
57
|
+
model: ProcessModel;
|
|
58
|
+
collapse: CollapseResult;
|
|
59
|
+
graph: ProcessGraph;
|
|
60
|
+
profile: ProcessProfile;
|
|
61
|
+
focus: FocusState;
|
|
62
|
+
selection: SelectionState;
|
|
63
|
+
viewState: ViewStateActions;
|
|
64
|
+
/** Domyślny tryb zwijania (z frontmatter/propsa). */
|
|
65
|
+
collapsedByDefault: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ProseInteractionRootProps {
|
|
69
|
+
mdx: string;
|
|
70
|
+
profile: ProcessProfile;
|
|
71
|
+
graph?: ProcessGraph;
|
|
72
|
+
/** Extra/override komponenty MDX (Ref/Node/Rel/Metric/Formula konsumenta). */
|
|
73
|
+
components?: Record<string, ComponentType<Record<string, unknown>>>;
|
|
74
|
+
/** Most token→realId dla focusu (canvas grafu wiedzy). Brak = jak arc-map. */
|
|
75
|
+
resolveNodeId?: (ref: ResolvedRef) => string | null;
|
|
76
|
+
/** Podgląd zebranych wystąpień (np. do zbudowania overlayu grafu wiedzy). */
|
|
77
|
+
onOccurrences?: (occ: Occurrence[]) => void;
|
|
78
|
+
store?: ProcessViewStore;
|
|
79
|
+
docId?: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
defaultCollapsed?: boolean;
|
|
82
|
+
homeContextId?: string;
|
|
83
|
+
children: (api: ProseRootRenderApi) => ReactNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function ProseInteractionRoot({
|
|
87
|
+
mdx,
|
|
88
|
+
profile,
|
|
89
|
+
graph = EMPTY_GRAPH,
|
|
90
|
+
components,
|
|
91
|
+
resolveNodeId,
|
|
92
|
+
onOccurrences,
|
|
93
|
+
store,
|
|
94
|
+
docId,
|
|
95
|
+
title,
|
|
96
|
+
defaultCollapsed,
|
|
97
|
+
homeContextId,
|
|
98
|
+
children,
|
|
99
|
+
}: ProseInteractionRootProps) {
|
|
100
|
+
const [occurrences, setOccurrences] = useState<Occurrence[]>([]);
|
|
101
|
+
const [viewState, setViewState] = useState<ProcessViewState | null>(null);
|
|
102
|
+
const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
|
|
103
|
+
const [scrollToKey, setScrollToKey] = useState<string | null>(null);
|
|
104
|
+
const [selectedOcc, setSelectedOcc] = useState<string | null>(null);
|
|
105
|
+
const proseRef = useRef<HTMLDivElement | null>(null);
|
|
106
|
+
|
|
107
|
+
const frontmatter = useMemo(() => parseProcessFrontmatter(mdx), [mdx]);
|
|
108
|
+
const docKey = docId ?? title ?? frontmatter.title ?? mdx.slice(0, 48);
|
|
109
|
+
const collapsedByDefault =
|
|
110
|
+
defaultCollapsed ?? (frontmatter.collapse ?? "all") === "all";
|
|
111
|
+
const home = homeContextId ?? frontmatter.context;
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
setOccurrences([]);
|
|
115
|
+
setViewState(null);
|
|
116
|
+
setSelectedOcc(null);
|
|
117
|
+
}, [mdx]);
|
|
118
|
+
|
|
119
|
+
const handleOccurrences = useCallback(
|
|
120
|
+
(occ: Occurrence[]) => {
|
|
121
|
+
setOccurrences(occ);
|
|
122
|
+
onOccurrences?.(occ);
|
|
123
|
+
},
|
|
124
|
+
[onOccurrences],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const model = useMemo(
|
|
128
|
+
() => buildProcessModel({ graph, profile, occurrences }),
|
|
129
|
+
[graph, profile, occurrences],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const homeId = useMemo(() => {
|
|
133
|
+
if (!home) return undefined;
|
|
134
|
+
const ctxs = graph.contexts ?? [];
|
|
135
|
+
return (ctxs.find((c) => c.id === home) ?? ctxs.find((c) => c.name === home))?.id;
|
|
136
|
+
}, [graph, home]);
|
|
137
|
+
|
|
138
|
+
const effectiveState = useMemo<ProcessViewState>(() => {
|
|
139
|
+
if (viewState) return viewState;
|
|
140
|
+
const stored = store?.load(docKey);
|
|
141
|
+
if (stored) return stored;
|
|
142
|
+
return initialViewState(model);
|
|
143
|
+
}, [viewState, store, docKey, model]);
|
|
144
|
+
|
|
145
|
+
const updateState = useCallback(
|
|
146
|
+
(next: ProcessViewState) => {
|
|
147
|
+
setViewState(next);
|
|
148
|
+
store?.save(docKey, next);
|
|
149
|
+
},
|
|
150
|
+
[store, docKey],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const collapse = useMemo(
|
|
154
|
+
() =>
|
|
155
|
+
collapseContexts(model, {
|
|
156
|
+
contexts: graph.contexts ?? [],
|
|
157
|
+
state: effectiveState,
|
|
158
|
+
defaultCollapsed: collapsedByDefault,
|
|
159
|
+
homeContextId: homeId,
|
|
160
|
+
}),
|
|
161
|
+
[model, graph, effectiveState, collapsedByDefault, homeId],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Klik w nod przewija prozę do odpowiadającego chipu (data-arc-key).
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!scrollToKey || !proseRef.current) return;
|
|
167
|
+
const el = proseRef.current.querySelector(
|
|
168
|
+
`[data-arc-key="${CSS.escape(scrollToKey)}"]`,
|
|
169
|
+
);
|
|
170
|
+
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
171
|
+
setScrollToKey(null);
|
|
172
|
+
}, [scrollToKey]);
|
|
173
|
+
|
|
174
|
+
const focusState = useMemo<FocusState>(
|
|
175
|
+
() => ({ focusNodeId, setFocusNodeId, scrollToKey, setScrollToKey }),
|
|
176
|
+
[focusNodeId, scrollToKey],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const viewStateActions = useMemo<ViewStateActions>(
|
|
180
|
+
() => ({
|
|
181
|
+
state: effectiveState,
|
|
182
|
+
toggleGroup: (key, group) =>
|
|
183
|
+
updateState(toggleGroupState(effectiveState, key, group)),
|
|
184
|
+
toggleContext: (contextId) =>
|
|
185
|
+
updateState(
|
|
186
|
+
toggleContextState(effectiveState, contextId, collapsedByDefault),
|
|
187
|
+
),
|
|
188
|
+
}),
|
|
189
|
+
[effectiveState, updateState, collapsedByDefault],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const processData = useMemo(
|
|
193
|
+
() => ({ graph, profile, resolveNodeId }),
|
|
194
|
+
[graph, profile, resolveNodeId],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const selectionState = useMemo<SelectionState>(
|
|
198
|
+
() => ({ selectedOcc, selectOcc: setSelectedOcc }),
|
|
199
|
+
[selectedOcc],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const Prose = (
|
|
203
|
+
<MdxView
|
|
204
|
+
key={docKey}
|
|
205
|
+
mdx={mdx}
|
|
206
|
+
components={components}
|
|
207
|
+
onOccurrences={handleOccurrences}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<ProcessDataContext.Provider value={processData}>
|
|
213
|
+
<FocusContext.Provider value={focusState}>
|
|
214
|
+
<SelectionContext.Provider value={selectionState}>
|
|
215
|
+
<ViewStateContext.Provider value={viewStateActions}>
|
|
216
|
+
{children({
|
|
217
|
+
Prose,
|
|
218
|
+
proseRef,
|
|
219
|
+
model,
|
|
220
|
+
collapse,
|
|
221
|
+
graph,
|
|
222
|
+
profile,
|
|
223
|
+
focus: focusState,
|
|
224
|
+
selection: selectionState,
|
|
225
|
+
viewState: viewStateActions,
|
|
226
|
+
collapsedByDefault,
|
|
227
|
+
})}
|
|
228
|
+
</ViewStateContext.Provider>
|
|
229
|
+
</SelectionContext.Provider>
|
|
230
|
+
</FocusContext.Provider>
|
|
231
|
+
</ProcessDataContext.Provider>
|
|
232
|
+
);
|
|
233
|
+
}
|
package/tests/build.test.ts
CHANGED
|
@@ -66,6 +66,26 @@ describe("buildProcessModel — parytet z buildUseCaseModel", () => {
|
|
|
66
66
|
expect(m.steps[0].node).toBeUndefined();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
test("inline declare-node / relation occurrences are ignored (nie wchodzą w kroki)", () => {
|
|
70
|
+
const m = buildProcessModel({
|
|
71
|
+
graph,
|
|
72
|
+
profile: arcProfile,
|
|
73
|
+
occurrences: [
|
|
74
|
+
occ("o1", { command: "cmd" }),
|
|
75
|
+
{
|
|
76
|
+
type: "declare-node",
|
|
77
|
+
occId: "d1",
|
|
78
|
+
node: { id: "eph:x", kind: "ephemeral", label: "X" },
|
|
79
|
+
},
|
|
80
|
+
{ type: "relation", occId: "r1", from: "a", to: "b", kind: "rel" },
|
|
81
|
+
occ("o2", { command: "cmd" }),
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
// Tylko dwa kroki `<Arc>` — inline node/rel pomijane przez profil procesowy.
|
|
85
|
+
expect(m.steps.map((s) => s.occId)).toEqual(["o1", "o2"]);
|
|
86
|
+
expect(m.steps.map((s) => s.seq)).toEqual([1, 2]);
|
|
87
|
+
});
|
|
88
|
+
|
|
69
89
|
test("forward attachments exclude visible; query dep becomes attachment", () => {
|
|
70
90
|
const m = buildProcessModel({
|
|
71
91
|
graph,
|
package/tests/layout.test.ts
CHANGED
|
@@ -269,6 +269,28 @@ describe("collapse kontekstów", () => {
|
|
|
269
269
|
expect(rel[0].source.startsWith("grp:ctx_chat")).toBe(true);
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
+
test("relacja prawo→lewo pominięta; lewo→prawo narysowana", () => {
|
|
273
|
+
// cmd queries v (semantyka "in"): strzałka biegnie v→cmd.
|
|
274
|
+
// Narracja cmd(depth0) → v(depth1): v jest NA PRAWO, strzałka wsteczna → skip.
|
|
275
|
+
const back = run([occ("o1", { command: "cmd" }), occ("o2", { view: "v" })]);
|
|
276
|
+
expect(back.layout.edges.filter((e) => e.id.startsWith("rel:"))).toHaveLength(0);
|
|
277
|
+
// Narracja v(depth0) → cmd(depth1): v na lewo, strzałka lewo→prawo → rysuj.
|
|
278
|
+
const fwd = run([occ("o1", { view: "v" }), occ("o2", { command: "cmd" })]);
|
|
279
|
+
expect(fwd.layout.edges.filter((e) => e.id.startsWith("rel:")).length).toBeGreaterThan(0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("relacja w tej samej kolumnie (Parallel) pominięta", () => {
|
|
283
|
+
// cmd mutates e (semantyka "out"): oba kroki w Parallel = ta sama kolumna.
|
|
284
|
+
const par = run([
|
|
285
|
+
{ ...occ("o1", { command: "cmd" }), path: [{ type: "parallel", id: "P", laneIndex: 0 }] },
|
|
286
|
+
{ ...occ("o2", { event: "e" }), path: [{ type: "parallel", id: "P", laneIndex: 1 }] },
|
|
287
|
+
]);
|
|
288
|
+
const a = nodeById(par.layout.nodes, "o1");
|
|
289
|
+
const b = nodeById(par.layout.nodes, "o2");
|
|
290
|
+
expect(a.x).toBe(b.x); // ta sama kolumna
|
|
291
|
+
expect(par.layout.edges.filter((e) => e.id.startsWith("rel:"))).toHaveLength(0);
|
|
292
|
+
});
|
|
293
|
+
|
|
272
294
|
test("initialViewState otwiera grupy z expand", () => {
|
|
273
295
|
const o: StepOccurrence = { ...occ("o1", { command: "cmd" }), expand: ["mutates"] };
|
|
274
296
|
const model = buildProcessModel({ graph, profile: arcProfile, occurrences: [o] });
|