@arcote.tech/arc-process 0.7.28 → 0.7.30
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 +5 -4
- package/src/layout/layout.ts +60 -9
- package/src/mdx/arc.tsx +50 -10
- package/src/mdx/contexts.tsx +11 -0
- package/src/mdx/mdx-view.tsx +4 -0
- package/src/mdx/pin.tsx +101 -0
- package/src/model/build.ts +73 -22
- package/src/model/collapse.ts +2 -0
- package/src/model/profile.ts +15 -0
- package/src/model/types.ts +37 -0
- package/src/profiles/arc.ts +29 -1
- package/src/react/drawer.tsx +374 -0
- package/src/react/index.ts +5 -0
- package/src/react/nodes.tsx +65 -10
- package/src/react/process-diagram.tsx +47 -4
- package/tests/build.test.ts +98 -4
- package/tests/layout.test.ts +34 -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.30",
|
|
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",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"@mdx-js/mdx": "^3.1.0",
|
|
19
19
|
"@xyflow/react": "^12.0.0",
|
|
20
20
|
"remark-frontmatter": "^5.0.0",
|
|
21
|
-
"remark-gfm": "^4.0.0"
|
|
21
|
+
"remark-gfm": "^4.0.0",
|
|
22
|
+
"shiki": "^1.24.0"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
24
|
-
"@arcote.tech/arc": "^0.7.
|
|
25
|
-
"@arcote.tech/platform": "^0.7.
|
|
25
|
+
"@arcote.tech/arc": "^0.7.30",
|
|
26
|
+
"@arcote.tech/platform": "^0.7.30",
|
|
26
27
|
"react": ">=18.0.0",
|
|
27
28
|
"react-dom": ">=18.0.0",
|
|
28
29
|
"typescript": "^5.0.0"
|
package/src/layout/layout.ts
CHANGED
|
@@ -163,16 +163,15 @@ export function layoutProcess(input: LayoutInput): ProcessLayout {
|
|
|
163
163
|
placeGroups(m.groups, s.occId, cx, cy, STEP_W, STEP_H, nodes, edges);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// --- Szkielet narracji
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (a.entryId !== b.entryId) continue;
|
|
166
|
+
// --- Szkielet narracji (per tor, z fork/join na blokach) ---------------------
|
|
167
|
+
// Oś główna i każde Entry to osobne tory — blok Entry wtrącony w dokument
|
|
168
|
+
// nie przerywa ciągłości osi. Bloki Parallel/Branch dostają jawne
|
|
169
|
+
// rozwidlenie (krok przed blokiem → pierwszy krok KAŻDEGO lane'a) i
|
|
170
|
+
// scalenie (ostatni krok każdego lane'a → krok po bloku).
|
|
171
|
+
let bbIdx = 0;
|
|
172
|
+
const backbone = (a: (typeof steps)[number], b: (typeof steps)[number]) =>
|
|
174
173
|
edges.push({
|
|
175
|
-
id: `bb:${
|
|
174
|
+
id: `bb:${bbIdx++}`,
|
|
176
175
|
source: a.occId,
|
|
177
176
|
target: b.occId,
|
|
178
177
|
color: BACKBONE_COLOR,
|
|
@@ -180,6 +179,58 @@ export function layoutProcess(input: LayoutInput): ProcessLayout {
|
|
|
180
179
|
opacity: 0.6,
|
|
181
180
|
backbone: true,
|
|
182
181
|
});
|
|
182
|
+
|
|
183
|
+
const trackMap = new Map<string, (typeof steps)[number][]>();
|
|
184
|
+
for (const s of steps) {
|
|
185
|
+
const key = s.entryId ?? "";
|
|
186
|
+
let track = trackMap.get(key);
|
|
187
|
+
if (!track) trackMap.set(key, (track = []));
|
|
188
|
+
track.push(s);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface TrackSeg {
|
|
192
|
+
block?: string;
|
|
193
|
+
members: (typeof steps)[number][];
|
|
194
|
+
}
|
|
195
|
+
for (const track of trackMap.values()) {
|
|
196
|
+
// Segmentacja toru: pojedyncze kroki i runy wspólnego blockId.
|
|
197
|
+
const segs: TrackSeg[] = [];
|
|
198
|
+
for (const s of track) {
|
|
199
|
+
const last = segs[segs.length - 1];
|
|
200
|
+
if (s.blockId && last?.block === s.blockId) last.members.push(s);
|
|
201
|
+
else segs.push({ block: s.blockId, members: [s] });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const laneGroups = (seg: TrackSeg) => {
|
|
205
|
+
const byLane = new Map<number, (typeof steps)[number][]>();
|
|
206
|
+
for (const s of seg.members) {
|
|
207
|
+
let g = byLane.get(s.lane);
|
|
208
|
+
if (!g) byLane.set(s.lane, (g = []));
|
|
209
|
+
g.push(s);
|
|
210
|
+
}
|
|
211
|
+
return [...byLane.values()];
|
|
212
|
+
};
|
|
213
|
+
const firstsOf = (seg: TrackSeg) =>
|
|
214
|
+
seg.block ? laneGroups(seg).map((g) => g[0]) : [seg.members[0]];
|
|
215
|
+
const lastsOf = (seg: TrackSeg) =>
|
|
216
|
+
seg.block
|
|
217
|
+
? laneGroups(seg).map((g) => g[g.length - 1])
|
|
218
|
+
: [seg.members[seg.members.length - 1]];
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < segs.length; i++) {
|
|
221
|
+
const seg = segs[i];
|
|
222
|
+
// Sekwencje wewnątrz lane'ów bloku.
|
|
223
|
+
if (seg.block) {
|
|
224
|
+
for (const g of laneGroups(seg)) {
|
|
225
|
+
for (let k = 1; k < g.length; k++) backbone(g[k - 1], g[k]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (i === 0) continue;
|
|
229
|
+
// Fork/join: każdy koniec poprzedniego segmentu → każdy początek bieżącego.
|
|
230
|
+
for (const a of lastsOf(segs[i - 1])) {
|
|
231
|
+
for (const b of firstsOf(seg)) backbone(a, b);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
183
234
|
}
|
|
184
235
|
|
|
185
236
|
// --- Krawędzie continues (Entry → faza, skosem) ------------------------------
|
package/src/mdx/arc.tsx
CHANGED
|
@@ -16,14 +16,17 @@ import { kindColor } from "../model/profile";
|
|
|
16
16
|
import {
|
|
17
17
|
CONTEXT_REF_KIND,
|
|
18
18
|
type Direction,
|
|
19
|
+
type PinSpec,
|
|
19
20
|
type ResolvedRef,
|
|
20
21
|
} from "../model/types";
|
|
21
22
|
import { resolveContextRef } from "../model/build";
|
|
22
23
|
import { MISSING_COLOR, tint } from "../react/theme";
|
|
24
|
+
import { isPinElement, PinCollectorContext } from "./pin";
|
|
23
25
|
import {
|
|
24
26
|
useFocus,
|
|
25
27
|
useProcessData,
|
|
26
28
|
useRegister,
|
|
29
|
+
useSelection,
|
|
27
30
|
useStructure,
|
|
28
31
|
} from "./contexts";
|
|
29
32
|
|
|
@@ -43,6 +46,7 @@ function textOf(node: ReactNode): string {
|
|
|
43
46
|
if (node == null || node === false) return "";
|
|
44
47
|
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
45
48
|
if (Array.isArray(node)) return node.map(textOf).join("");
|
|
49
|
+
if (isPinElement(node)) return ""; // treść pinów nie wchodzi do etykiety
|
|
46
50
|
if (typeof node === "object" && "props" in (node as unknown as Record<string, unknown>)) {
|
|
47
51
|
return textOf(
|
|
48
52
|
(node as unknown as { props?: { children?: ReactNode } }).props?.children,
|
|
@@ -57,6 +61,7 @@ export function Arc(props: ArcProps) {
|
|
|
57
61
|
const register = useRegister();
|
|
58
62
|
const data = useProcessData();
|
|
59
63
|
const focus = useFocus();
|
|
64
|
+
const selection = useSelection();
|
|
60
65
|
const { path } = useStructure();
|
|
61
66
|
|
|
62
67
|
const ref: ResolvedRef | null = context
|
|
@@ -64,6 +69,17 @@ export function Arc(props: ArcProps) {
|
|
|
64
69
|
: (data?.profile.resolveRef(refProps) ?? null);
|
|
65
70
|
const label = textOf(children);
|
|
66
71
|
|
|
72
|
+
// Piny z dzieci (<Person>/<Metric>/<Pin>): świeża tablica per render;
|
|
73
|
+
// dzieci renderują się PRZED efektami, a ArcRefsProvider czyta wpisy w
|
|
74
|
+
// useEffect — więc mutacja tej tablicy podczas renderu dzieci jest
|
|
75
|
+
// bezpieczna i occurrence ma komplet pinów, zanim ktokolwiek go odczyta.
|
|
76
|
+
const pins: PinSpec[] = [];
|
|
77
|
+
const collectPin = (pin: PinSpec) => {
|
|
78
|
+
if (!pins.some((p) => p.kind === pin.kind && p.name === pin.name)) {
|
|
79
|
+
pins.push(pin);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
67
83
|
// Rejestracja podczas renderu — idempotentna (klucz domId), przeżywa StrictMode.
|
|
68
84
|
register?.({
|
|
69
85
|
domId,
|
|
@@ -75,6 +91,7 @@ export function Arc(props: ArcProps) {
|
|
|
75
91
|
path,
|
|
76
92
|
sides,
|
|
77
93
|
expand,
|
|
94
|
+
pins,
|
|
78
95
|
},
|
|
79
96
|
});
|
|
80
97
|
|
|
@@ -90,11 +107,17 @@ export function Arc(props: ArcProps) {
|
|
|
90
107
|
? `ctx:${resolvedNode.id}`
|
|
91
108
|
: ref?.nodeId;
|
|
92
109
|
const active = !!focusId && focus.focusNodeId === focusId;
|
|
93
|
-
|
|
110
|
+
// Kolor zawsze z ZADEKLAROWANEGO rodzaju (ref.kind) — element nieistniejący
|
|
111
|
+
// (design-first TODO) zachowuje kolor eventu/komendy itd., a brak w grafie
|
|
112
|
+
// sygnalizuje wyłącznie kreskowana ramka. Czerwień tylko dla refów
|
|
113
|
+
// nierozpoznanych w ogóle (złe propsy).
|
|
114
|
+
const declaredKind = resolved
|
|
94
115
|
? ref!.kind === CONTEXT_REF_KIND
|
|
95
|
-
?
|
|
96
|
-
:
|
|
97
|
-
:
|
|
116
|
+
? CONTEXT_REF_KIND
|
|
117
|
+
: (resolvedNode as { kind: string }).kind
|
|
118
|
+
: ref?.kind;
|
|
119
|
+
const color =
|
|
120
|
+
declaredKind && data ? kindColor(data.profile, declaredKind) : MISSING_COLOR;
|
|
98
121
|
|
|
99
122
|
return (
|
|
100
123
|
<span
|
|
@@ -103,6 +126,7 @@ export function Arc(props: ArcProps) {
|
|
|
103
126
|
data-arc-node={focusId ?? ""}
|
|
104
127
|
onMouseEnter={() => resolved && focusId && focus.setFocusNodeId(focusId)}
|
|
105
128
|
onMouseLeave={() => focus.setFocusNodeId(null)}
|
|
129
|
+
onClick={() => selection.selectOcc(domId)}
|
|
106
130
|
title={
|
|
107
131
|
resolved
|
|
108
132
|
? ref?.method
|
|
@@ -117,23 +141,39 @@ export function Arc(props: ArcProps) {
|
|
|
117
141
|
padding: "1px 8px",
|
|
118
142
|
margin: "0 1px",
|
|
119
143
|
borderRadius: 8,
|
|
120
|
-
border: `1.5px solid ${color}`,
|
|
121
|
-
background:
|
|
144
|
+
border: `1.5px ${resolved ? "solid" : "dashed"} ${color}`,
|
|
145
|
+
background: tint(color, resolved ? 0.1 : 0.05),
|
|
122
146
|
color: "#2a173f",
|
|
123
147
|
fontWeight: 600,
|
|
124
148
|
fontSize: "0.92em",
|
|
125
149
|
cursor: "pointer",
|
|
126
150
|
boxShadow: active ? `0 0 0 2px ${color}` : "none",
|
|
127
151
|
whiteSpace: "nowrap",
|
|
152
|
+
opacity: resolved ? 1 : 0.85,
|
|
128
153
|
}}
|
|
129
154
|
>
|
|
130
155
|
<span
|
|
131
156
|
style={{ width: 7, height: 7, borderRadius: 2, background: color }}
|
|
132
157
|
/>
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
<PinCollectorContext.Provider value={collectPin}>
|
|
159
|
+
{label || ref?.nodeId || "?"}
|
|
160
|
+
{ref?.method ? (
|
|
161
|
+
<span style={{ color: "#6b5b86", fontWeight: 500 }}>·{ref.method.name}</span>
|
|
162
|
+
) : null}
|
|
163
|
+
{renderPins(children)}
|
|
164
|
+
</PinCollectorContext.Provider>
|
|
137
165
|
</span>
|
|
138
166
|
);
|
|
139
167
|
}
|
|
168
|
+
|
|
169
|
+
/** Wyrenderuj z dzieci WYŁĄCZNIE komponenty pinów (tekst poszedł do etykiety). */
|
|
170
|
+
function renderPins(children: ReactNode): ReactNode {
|
|
171
|
+
if (children == null || typeof children !== "object") return null;
|
|
172
|
+
if (Array.isArray(children)) {
|
|
173
|
+
return children.map((c, i) =>
|
|
174
|
+
isPinElement(c) ? <span key={i}>{c}</span> : renderPins(c),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (isPinElement(children)) return children;
|
|
178
|
+
return null;
|
|
179
|
+
}
|
package/src/mdx/contexts.tsx
CHANGED
|
@@ -40,6 +40,17 @@ export interface RegEntry {
|
|
|
40
40
|
export const RegisterContext = createContext<((entry: RegEntry) => void) | null>(null);
|
|
41
41
|
export const useRegister = () => useContext(RegisterContext);
|
|
42
42
|
|
|
43
|
+
/** Wybór kroku → drawer szczegółów (occId lub null = zamknięty). */
|
|
44
|
+
export interface SelectionState {
|
|
45
|
+
selectedOcc: string | null;
|
|
46
|
+
selectOcc: (occId: string | null) => void;
|
|
47
|
+
}
|
|
48
|
+
export const SelectionContext = createContext<SelectionState>({
|
|
49
|
+
selectedOcc: null,
|
|
50
|
+
selectOcc: () => {},
|
|
51
|
+
});
|
|
52
|
+
export const useSelection = () => useContext(SelectionContext);
|
|
53
|
+
|
|
43
54
|
/** Stan widoku + akcje (toggle rozwinięć / zwinięć kontekstów). */
|
|
44
55
|
export interface ViewStateActions {
|
|
45
56
|
state: ProcessViewState;
|
package/src/mdx/mdx-view.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
|
|
11
11
|
import * as runtime from "react/jsx-runtime";
|
|
12
12
|
import type { Occurrence } from "../model/types";
|
|
13
13
|
import { Arc } from "./arc";
|
|
14
|
+
import { Metric, Person, Pin } from "./pin";
|
|
14
15
|
import { Branch, Continue, Entry, Lane, Note, Parallel, Step } from "./structure";
|
|
15
16
|
import { ArcRefsProvider } from "./ref-context";
|
|
16
17
|
import { GLASS } from "../react/theme";
|
|
@@ -26,6 +27,9 @@ const mdxComponents = {
|
|
|
26
27
|
Entry,
|
|
27
28
|
Continue,
|
|
28
29
|
Note,
|
|
30
|
+
Pin,
|
|
31
|
+
Person,
|
|
32
|
+
Metric,
|
|
29
33
|
h1: (p: ElProps) => (
|
|
30
34
|
<h1 style={{ fontSize: 24, color: GLASS.text, margin: "4px 0 12px" }} {...p} />
|
|
31
35
|
),
|
package/src/mdx/pin.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Przypięcia (`<Pin>`, `<Person>`, `<Metric>`) — deklarowane w MDX WEWNĄTRZ
|
|
3
|
+
* `<Arc>`:
|
|
4
|
+
*
|
|
5
|
+
* ```mdx
|
|
6
|
+
* <Arc aggregate="leads" mutationMethod="submitLead">
|
|
7
|
+
* zgłoszenie leada<Person name="prospekt"/><Metric name="leads.submitted"/>
|
|
8
|
+
* </Arc>
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Pin rejestruje się u rodzica (kolektor z kontekstu `<Arc>`) podczas
|
|
12
|
+
* renderu i renderuje mini-chip inline. Rodzaje pinów definiuje PROFIL
|
|
13
|
+
* (`pinKinds`); wartości rozwiązują się z generycznego katalogu
|
|
14
|
+
* (arc.catalog.yml) po `name` — w modelu, nie tutaj. `Person`/`Metric` to
|
|
15
|
+
* cienkie aliasy generycznego `Pin`, przyszłe rodzaje nie wymagają zmian.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
19
|
+
import type { PinSpec } from "../model/types";
|
|
20
|
+
import { GLASS, tint } from "../react/theme";
|
|
21
|
+
import { useProcessData } from "./contexts";
|
|
22
|
+
|
|
23
|
+
/** Kolektor pinów dostarczany przez `<Arc>` swoim dzieciom. */
|
|
24
|
+
export const PinCollectorContext = createContext<((pin: PinSpec) => void) | null>(
|
|
25
|
+
null,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function textOf(node: ReactNode): string {
|
|
29
|
+
if (node == null || node === false) return "";
|
|
30
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
31
|
+
if (Array.isArray(node)) return node.map(textOf).join("");
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PinProps {
|
|
36
|
+
/** Rodzaj przypięcia (klucz `pinKinds` profilu), np. "persona", "metric". */
|
|
37
|
+
type: string;
|
|
38
|
+
/** Nazwa wpisu katalogu (arc.catalog.yml). */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Etykieta (fallback: label z katalogu → name). */
|
|
41
|
+
label?: string;
|
|
42
|
+
children?: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Pin({ type, name, label, children }: PinProps) {
|
|
46
|
+
const collect = useContext(PinCollectorContext);
|
|
47
|
+
const data = useProcessData();
|
|
48
|
+
const resolvedLabel = label ?? (textOf(children) || undefined);
|
|
49
|
+
|
|
50
|
+
collect?.({ kind: type, name, label: resolvedLabel });
|
|
51
|
+
|
|
52
|
+
const spec = data?.profile.pinKinds?.[type];
|
|
53
|
+
const color = spec?.color ?? "#64748b";
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
data-arc-pin={type}
|
|
57
|
+
title={`${spec?.label ?? type}: ${name}`}
|
|
58
|
+
style={{
|
|
59
|
+
display: "inline-flex",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
gap: 3,
|
|
62
|
+
marginLeft: 4,
|
|
63
|
+
padding: "0 6px",
|
|
64
|
+
borderRadius: 99,
|
|
65
|
+
border: `1px solid ${tint(color, 0.55)}`,
|
|
66
|
+
background: tint(color, 0.08),
|
|
67
|
+
color: GLASS.textMuted,
|
|
68
|
+
fontSize: "0.78em",
|
|
69
|
+
fontWeight: 600,
|
|
70
|
+
whiteSpace: "nowrap",
|
|
71
|
+
verticalAlign: "baseline",
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{spec?.icon ?? "◦"} {resolvedLabel ?? name}
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Persona-wykonawca kroku. */
|
|
80
|
+
export function Person(props: Omit<PinProps, "type">) {
|
|
81
|
+
return <Pin type="persona" {...props} />;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Metryka przypięta do kroku. */
|
|
85
|
+
export function Metric(props: Omit<PinProps, "type">) {
|
|
86
|
+
return <Pin type="metric" {...props} />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Znacznik dla textOf w <Arc> — treść pinów nie wchodzi do etykiety kroku.
|
|
90
|
+
(Pin as unknown as Record<string, unknown>).__arcPin = true;
|
|
91
|
+
(Person as unknown as Record<string, unknown>).__arcPin = true;
|
|
92
|
+
(Metric as unknown as Record<string, unknown>).__arcPin = true;
|
|
93
|
+
|
|
94
|
+
/** Czy węzeł React jest komponentem przypięcia (pomijany w etykiecie). */
|
|
95
|
+
export function isPinElement(node: unknown): boolean {
|
|
96
|
+
const type = (node as { type?: unknown } | null)?.type;
|
|
97
|
+
return (
|
|
98
|
+
typeof type === "function" &&
|
|
99
|
+
(type as unknown as Record<string, unknown>).__arcPin === true
|
|
100
|
+
);
|
|
101
|
+
}
|
package/src/model/build.ts
CHANGED
|
@@ -21,12 +21,14 @@ import {
|
|
|
21
21
|
type NoteOccurrence,
|
|
22
22
|
type Occurrence,
|
|
23
23
|
type PathSeg,
|
|
24
|
+
type PinSpec,
|
|
24
25
|
type ProcessConnector,
|
|
25
26
|
type ProcessGraph,
|
|
26
27
|
type ProcessModel,
|
|
27
28
|
type ProcessNode,
|
|
28
29
|
type ProcessRelation,
|
|
29
30
|
type ProcessStep,
|
|
31
|
+
type ResolvedPin,
|
|
30
32
|
type StepOccurrence,
|
|
31
33
|
} from "./types";
|
|
32
34
|
|
|
@@ -212,36 +214,62 @@ function assignDepthLane(
|
|
|
212
214
|
.filter((s) => !s.entryId)
|
|
213
215
|
.map((s) => s.lane);
|
|
214
216
|
const maxMainLane = mainLanes.length ? Math.max(...mainLanes) : 0;
|
|
215
|
-
let aboveCount = 0;
|
|
216
|
-
let belowCount = 0;
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
interface EntryPlan {
|
|
219
|
+
e: EntryBlock;
|
|
220
|
+
targetDepth: number;
|
|
221
|
+
targetOcc?: string;
|
|
222
|
+
toPhase?: string;
|
|
223
|
+
}
|
|
224
|
+
const plans: EntryPlan[] = entryOrder.map((entryId) => {
|
|
219
225
|
const e = entries.get(entryId)!;
|
|
220
|
-
const target =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
const target =
|
|
227
|
+
(e.to !== undefined ? phaseStart.get(e.to) : undefined) ?? firstPhase;
|
|
228
|
+
return {
|
|
229
|
+
e,
|
|
230
|
+
targetDepth: target?.depth ?? 0,
|
|
231
|
+
targetOcc: target?.occId,
|
|
232
|
+
toPhase: e.to,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Wyprzedzenie: oś główna przesuwa się w prawo na tyle, by KAŻDY łańcuch
|
|
237
|
+
// Entry zmieścił się PRZED swoją fazą docelową — krawędź „continues” zawsze
|
|
238
|
+
// biegnie w prawo, nigdy wstecz przez diagram.
|
|
239
|
+
const maxLead = Math.max(
|
|
240
|
+
0,
|
|
241
|
+
...plans.map((p) => p.e.stepIdx.length - p.targetDepth),
|
|
242
|
+
);
|
|
243
|
+
if (maxLead > 0) {
|
|
244
|
+
for (const s of steps) if (!s.entryId) s.depth += maxLead;
|
|
245
|
+
for (const p of plans) p.targetDepth += maxLead;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const placeEntry = (p: EntryPlan, lane: number) => {
|
|
249
|
+
const L = p.e.stepIdx.length;
|
|
250
|
+
const start = Math.max(0, p.targetDepth - L);
|
|
251
|
+
p.e.stepIdx.forEach((idx, i) => {
|
|
227
252
|
steps[idx].depth = start + i;
|
|
228
253
|
steps[idx].lane = lane;
|
|
229
254
|
});
|
|
230
|
-
if (
|
|
231
|
-
continueEdges.push({
|
|
232
|
-
fromOcc: steps[e.stepIdx[L - 1]].occId,
|
|
233
|
-
toOcc: target.occId,
|
|
234
|
-
phaseId: e.to,
|
|
235
|
-
});
|
|
236
|
-
} else if (target && L > 0) {
|
|
237
|
-
// Entry bez Continue → wpięcie w pierwszą fazę dokumentu.
|
|
255
|
+
if (p.targetOcc && L > 0) {
|
|
238
256
|
continueEdges.push({
|
|
239
|
-
fromOcc: steps[e.stepIdx[L - 1]].occId,
|
|
240
|
-
toOcc:
|
|
241
|
-
phaseId: "",
|
|
257
|
+
fromOcc: steps[p.e.stepIdx[L - 1]].occId,
|
|
258
|
+
toOcc: p.targetOcc,
|
|
259
|
+
phaseId: p.toPhase ?? "",
|
|
242
260
|
});
|
|
243
261
|
}
|
|
244
|
-
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Tory naprzemiennie nad/pod osią (kolejność dokumentu), a w obrębie strony
|
|
265
|
+
// wejścia celujące dalej w prawo lądują bliżej osi — ich krawędzie
|
|
266
|
+
// „continues” nie przecinają wtedy torów pozostałych wejść.
|
|
267
|
+
const above = plans.filter((_, k) => k % 2 === 0);
|
|
268
|
+
const below = plans.filter((_, k) => k % 2 === 1);
|
|
269
|
+
above.sort((a, b) => b.targetDepth - a.targetDepth);
|
|
270
|
+
below.sort((a, b) => b.targetDepth - a.targetDepth);
|
|
271
|
+
above.forEach((p, i) => placeEntry(p, -(1 + i)));
|
|
272
|
+
below.forEach((p, i) => placeEntry(p, maxMainLane + 1 + i));
|
|
245
273
|
|
|
246
274
|
return continueEdges;
|
|
247
275
|
}
|
|
@@ -381,6 +409,28 @@ export function buildProcessModel(input: BuildModelInput): ProcessModel {
|
|
|
381
409
|
return nodeById.get(nodeId);
|
|
382
410
|
};
|
|
383
411
|
|
|
412
|
+
// Rozwiązywanie pinów przeciw katalogowi: rodzaj pinu (profil) wskazuje
|
|
413
|
+
// rodzaj wpisu katalogu (catalogKind, fallback: klucz pinu), wpis po name.
|
|
414
|
+
const catalogByKey = new Map(
|
|
415
|
+
(graph.catalog ?? []).map((c) => [`${c.kind} ${c.name}`, c]),
|
|
416
|
+
);
|
|
417
|
+
const resolvePin = (pin: PinSpec): ResolvedPin => {
|
|
418
|
+
const spec = profile.pinKinds?.[pin.kind];
|
|
419
|
+
const catalogKind = spec?.catalogKind ?? pin.kind;
|
|
420
|
+
const entry = catalogByKey.get(`${catalogKind} ${pin.name}`);
|
|
421
|
+
const label =
|
|
422
|
+
pin.label ??
|
|
423
|
+
(typeof entry?.data.label === "string" ? entry.data.label : undefined) ??
|
|
424
|
+
pin.name;
|
|
425
|
+
return {
|
|
426
|
+
kind: pin.kind,
|
|
427
|
+
name: pin.name,
|
|
428
|
+
label,
|
|
429
|
+
data: entry?.data,
|
|
430
|
+
resolved: !!entry,
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
|
|
384
434
|
// 1) Kroki — jeden na wystąpienie.
|
|
385
435
|
const steps: ProcessStep[] = stepOccs.map((o, i) => {
|
|
386
436
|
const node = o.ref ? resolve(o.ref.nodeId, o.ref.kind) : undefined;
|
|
@@ -396,6 +446,7 @@ export function buildProcessModel(input: BuildModelInput): ProcessModel {
|
|
|
396
446
|
attachments: {},
|
|
397
447
|
sides: o.sides,
|
|
398
448
|
expand: o.expand,
|
|
449
|
+
pins: (o.pins ?? []).map(resolvePin),
|
|
399
450
|
notes: (notesFor.get(o.occId) ?? []).map((n) => ({
|
|
400
451
|
occId: n.occId,
|
|
401
452
|
side: n.side ?? ("N" as const),
|
package/src/model/collapse.ts
CHANGED
|
@@ -115,6 +115,8 @@ function groupStep(
|
|
|
115
115
|
},
|
|
116
116
|
label: ctx.title ?? ctx.name ?? ctx.id,
|
|
117
117
|
attachments: {},
|
|
118
|
+
// Box kontekstu nie agreguje pinów członków (v1) — szczegóły po otwarciu.
|
|
119
|
+
pins: [],
|
|
118
120
|
notes: members.flatMap((m) => m.notes),
|
|
119
121
|
depth: Math.min(...members.map((m) => m.depth)),
|
|
120
122
|
lane: first.lane,
|
package/src/model/profile.ts
CHANGED
|
@@ -54,6 +54,19 @@ export interface AttachmentGroupSpec {
|
|
|
54
54
|
label?: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Rodzaj przypięcia (Pin) — wygląd badge'a i mapowanie na katalog. */
|
|
58
|
+
export interface PinKindSpec {
|
|
59
|
+
icon: string;
|
|
60
|
+
color: string;
|
|
61
|
+
/** Etykieta rodzaju (fallback: klucz). */
|
|
62
|
+
label?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Rodzaj wpisu katalogu (klucz YAML), z którego pin się rozwiązuje —
|
|
65
|
+
* np. pin "persona" czyta wpisy "personas". Fallback: klucz pinu.
|
|
66
|
+
*/
|
|
67
|
+
catalogKind?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
export interface ProcessProfile {
|
|
58
71
|
id: string;
|
|
59
72
|
/** Rodzaje nodów tego profilu. */
|
|
@@ -62,6 +75,8 @@ export interface ProcessProfile {
|
|
|
62
75
|
relations: Record<string, RelationSpec>;
|
|
63
76
|
/** Grupy doczepień (badge/rozwinięcia) wokół kroków. */
|
|
64
77
|
attachmentGroups: Record<string, AttachmentGroupSpec>;
|
|
78
|
+
/** Rodzaje przypięć (Pin) — persony, metryki, przyszłe. */
|
|
79
|
+
pinKinds?: Record<string, PinKindSpec>;
|
|
65
80
|
/**
|
|
66
81
|
* Mapuje propsy `<Arc>` na referencję do noda (bez walidacji istnienia —
|
|
67
82
|
* tę robi model przeciw grafowi). Zwraca null, gdy propsy nie zawierają
|
package/src/model/types.ts
CHANGED
|
@@ -70,11 +70,23 @@ export interface ContextInfo {
|
|
|
70
70
|
hasCard?: boolean;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Wpis generycznego katalogu deklaracji (arc.catalog.yml, ładowany przez
|
|
75
|
+
* narzędzie-hosta np. arc-map) — persony, metryki i przyszłe rodzaje.
|
|
76
|
+
*/
|
|
77
|
+
export interface CatalogEntry {
|
|
78
|
+
kind: string;
|
|
79
|
+
name: string;
|
|
80
|
+
/** Pełny wpis YAML: name, label?, color?, description?, …dowolne pola. */
|
|
81
|
+
data: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
/** Kompletny graf wejściowy diagramu. */
|
|
74
85
|
export interface ProcessGraph {
|
|
75
86
|
nodes: ProcessNode[];
|
|
76
87
|
relations: ProcessRelation[];
|
|
77
88
|
contexts?: ContextInfo[];
|
|
89
|
+
catalog?: CatalogEntry[];
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/** Pusty graf — wszystkie referencje będą czerwonymi chipami TODO. */
|
|
@@ -125,6 +137,27 @@ export interface PathSeg {
|
|
|
125
137
|
// Wystąpienia (rejestrowane przez MDX w kolejności dokumentu)
|
|
126
138
|
// ---------------------------------------------------------------------------
|
|
127
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Przypięcie zadeklarowane w MDX wewnątrz `<Arc>` (np. `<Person name>`,
|
|
142
|
+
* `<Metric name>`) — rodzaj z profilu, wpis rozwiązywany z katalogu po name.
|
|
143
|
+
*/
|
|
144
|
+
export interface PinSpec {
|
|
145
|
+
kind: string;
|
|
146
|
+
name: string;
|
|
147
|
+
/** Etykieta z MDX (fallback: label z katalogu → name). */
|
|
148
|
+
label?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Przypięcie rozwiązane przeciw katalogowi grafu. */
|
|
152
|
+
export interface ResolvedPin {
|
|
153
|
+
kind: string;
|
|
154
|
+
name: string;
|
|
155
|
+
label: string;
|
|
156
|
+
/** Dane wpisu katalogu (color, description, …), gdy rozwiązany. */
|
|
157
|
+
data?: Record<string, unknown>;
|
|
158
|
+
resolved: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
/** Wystąpienie kroku — jedno na każdy `<Arc>` w narracji. */
|
|
129
162
|
export interface StepOccurrence {
|
|
130
163
|
type: "step";
|
|
@@ -136,6 +169,8 @@ export interface StepOccurrence {
|
|
|
136
169
|
sides?: Partial<Record<string, Direction>>;
|
|
137
170
|
/** Grupy relacji otwarte na starcie (`<Arc expand={["mutates"]}>`). */
|
|
138
171
|
expand?: string[];
|
|
172
|
+
/** Przypięcia z komponentów wewnątrz `<Arc>` (Person/Metric/Pin). */
|
|
173
|
+
pins?: PinSpec[];
|
|
139
174
|
}
|
|
140
175
|
|
|
141
176
|
/** Marker `<Continue to="faza"/>` — zamyka bieżące Entry, wskazuje cel. */
|
|
@@ -184,6 +219,8 @@ export interface ProcessStep {
|
|
|
184
219
|
sides?: Partial<Record<string, Direction>>;
|
|
185
220
|
/** Grupy otwarte na starcie (z MDX). */
|
|
186
221
|
expand?: string[];
|
|
222
|
+
/** Przypięcia rozwiązane przeciw katalogowi (persony, metryki, …). */
|
|
223
|
+
pins: ResolvedPin[];
|
|
187
224
|
notes: StepNote[];
|
|
188
225
|
/** Pozycja pozioma (czas / głębokość zależności), 0-based. */
|
|
189
226
|
depth: number;
|