@arcote.tech/arc-process 0.7.28

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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Pass 1 layoutu — pomiar rozwinięć.
3
+ *
4
+ * Dla każdego widocznego kroku budowane jest rekurencyjne drzewo rozwinięć
5
+ * (grupy relacji otwarte w stanie widoku, w 8 kierunkach) i mierzony
6
+ * bounding-box w 4 pasmach: W/E (szerokości) i N/S (wysokości). Skosy
7
+ * (NE/SE/SW/NW) to kolumny przy pasmach E/W przesunięte PONAD/POD nod —
8
+ * liczą się do pasma poziomego (szerokość kolumny) i pionowego (wysokość).
9
+ *
10
+ * Pasma zasilają track sizing w pass 2 — kolumny (depth) i wiersze (lane)
11
+ * rosną tak, by rozwinięcia nigdy nie nachodziły na sąsiednie tory.
12
+ */
13
+
14
+ import {
15
+ attachmentsFor,
16
+ type RelationIndex,
17
+ } from "../model/build";
18
+ import type {
19
+ AttachmentGroupSpec,
20
+ ProcessProfile,
21
+ } from "../model/profile";
22
+ import type {
23
+ Direction,
24
+ ProcessNode,
25
+ ProcessStep,
26
+ } from "../model/types";
27
+ import {
28
+ childKey,
29
+ rootKey,
30
+ type ExpansionKey,
31
+ type ProcessViewState,
32
+ } from "../state/view-state";
33
+
34
+ // --- Wymiary (parytet z dotychczasowym usecase-layout) ----------------------
35
+
36
+ export const STEP_W = 215;
37
+ export const STEP_H = 110;
38
+ export const CARD_W = 178;
39
+ export const CARD_H = 48;
40
+ export const NOTE_W = 178;
41
+ export const NOTE_H = 54;
42
+ export const COL_GAP_X = 250;
43
+ export const CARD_GAP = 14;
44
+ export const DIAG_GAP_Y = 42;
45
+ export const MARGIN = 80;
46
+ export const LANE_MARGIN = 120;
47
+ export const PAD = 60;
48
+
49
+ /** Maksymalna głębokość rekurencji rozwinięć (bezpiecznik). */
50
+ export const MAX_EXPANSION_DEPTH = 6;
51
+
52
+ /** Grupa-pseudo notatek (zawsze widoczna, nie podlega toggle). */
53
+ export const NOTES_GROUP = "notes";
54
+
55
+ // --- Drzewo rozwinięć --------------------------------------------------------
56
+
57
+ export interface ExpansionItem {
58
+ /** Klucz rozwinięcia TEGO węzła (adres w stanie widoku). */
59
+ key: ExpansionKey;
60
+ node: ProcessNode;
61
+ /** Grupy rozwinięte na tym węźle. */
62
+ groups: ExpansionGroup[];
63
+ /** Dostępne doczepienia węzła (do badge'y/toggli na karcie). */
64
+ attachments: Partial<Record<string, string[]>>;
65
+ /** Wymiary karty. */
66
+ w: number;
67
+ h: number;
68
+ /** Pasma rozwinięć wokół karty. */
69
+ bands: Bands;
70
+ }
71
+
72
+ export interface ExpansionGroup {
73
+ group: string;
74
+ side: Direction;
75
+ spec: AttachmentGroupSpec;
76
+ items: ExpansionItem[];
77
+ /** Zmierzone wymiary slotu grupy (kolumna/wiersz z rekurencją). */
78
+ slotW: number;
79
+ slotH: number;
80
+ }
81
+
82
+ export interface Bands {
83
+ W: number;
84
+ E: number;
85
+ N: number;
86
+ S: number;
87
+ }
88
+
89
+ export const ZERO_BANDS: Bands = { W: 0, E: 0, N: 0, S: 0 };
90
+
91
+ export interface MeasuredStep {
92
+ step: ProcessStep;
93
+ key: ExpansionKey;
94
+ groups: ExpansionGroup[];
95
+ bands: Bands;
96
+ }
97
+
98
+ export interface MeasureInput {
99
+ steps: ProcessStep[];
100
+ index: RelationIndex;
101
+ profile: ProcessProfile;
102
+ state: ProcessViewState;
103
+ /** Id nodów widocznych na osi (wykluczane z rozwinięć). */
104
+ visible: ReadonlySet<string>;
105
+ }
106
+
107
+ /** Spec grupy notatek — notatki wchodzą krawędzią "kropkowaną" do kroku. */
108
+ const NOTES_SPEC: AttachmentGroupSpec = {
109
+ relations: [],
110
+ direction: "out",
111
+ defaultSide: "N",
112
+ color: "#eab308",
113
+ intoNode: false,
114
+ label: "notatka",
115
+ };
116
+
117
+ function isColumn(side: Direction): boolean {
118
+ return side !== "N" && side !== "S";
119
+ }
120
+
121
+ /** Zmierz slot grupy: kolumna (W/E/skosy) lub wiersz (N/S) z rekurencją. */
122
+ function measureGroup(items: ExpansionItem[], side: Direction): { w: number; h: number } {
123
+ if (!items.length) return { w: 0, h: 0 };
124
+ if (isColumn(side)) {
125
+ // Kolumna: szerokość = najszerszy element z jego pasmami; wysokość = suma.
126
+ const w = Math.max(...items.map((it) => it.bands.W + it.w + it.bands.E));
127
+ const h =
128
+ items.reduce((acc, it) => acc + it.bands.N + it.h + it.bands.S, 0) +
129
+ (items.length - 1) * CARD_GAP;
130
+ return { w, h };
131
+ }
132
+ // Wiersz: wysokość = najwyższy element z pasmami; szerokość = suma.
133
+ const h = Math.max(...items.map((it) => it.bands.N + it.h + it.bands.S));
134
+ const w =
135
+ items.reduce((acc, it) => acc + it.bands.W + it.w + it.bands.E, 0) +
136
+ (items.length - 1) * CARD_GAP;
137
+ return { w, h };
138
+ }
139
+
140
+ /** Pasma węzła z jego zmierzonych grup. */
141
+ function bandsOf(groups: ExpansionGroup[], nodeW: number, nodeH: number): Bands {
142
+ const bands: Bands = { W: 0, E: 0, N: 0, S: 0 };
143
+ for (const g of groups) {
144
+ const dx = g.side.includes("W") ? -1 : g.side.includes("E") ? 1 : 0;
145
+ const dy = g.side.includes("N") ? -1 : g.side.includes("S") ? 1 : 0;
146
+ if (dx !== 0) {
147
+ // Kolumna przy E/W: pasmo poziome = odstęp + szerokość slotu.
148
+ const w = COL_GAP_X - nodeW / 2 + g.slotW;
149
+ if (dx < 0) bands.W = Math.max(bands.W, w);
150
+ else bands.E = Math.max(bands.E, w);
151
+ if (dy !== 0) {
152
+ // Skos: kolumna w całości nad/pod nodem.
153
+ const h = DIAG_GAP_Y + g.slotH;
154
+ if (dy < 0) bands.N = Math.max(bands.N, h);
155
+ else bands.S = Math.max(bands.S, h);
156
+ } else {
157
+ // Czysta kolumna E/W wystaje pionowo poza nod o połowę nadmiaru.
158
+ const overflow = Math.max(0, (g.slotH - nodeH) / 2);
159
+ bands.N = Math.max(bands.N, overflow);
160
+ bands.S = Math.max(bands.S, overflow);
161
+ }
162
+ } else if (dy !== 0) {
163
+ // Wiersz N/S.
164
+ const h = DIAG_GAP_Y + g.slotH;
165
+ if (dy < 0) bands.N = Math.max(bands.N, h);
166
+ else bands.S = Math.max(bands.S, h);
167
+ const overflow = Math.max(0, (g.slotW - nodeW) / 2);
168
+ bands.W = Math.max(bands.W, overflow);
169
+ bands.E = Math.max(bands.E, overflow);
170
+ }
171
+ }
172
+ return bands;
173
+ }
174
+
175
+ /** Zbuduj rekurencyjnie rozwinięcia węzła (karteczki lub kroku). */
176
+ function buildGroups(
177
+ key: ExpansionKey,
178
+ node: ProcessNode,
179
+ attachments: Partial<Record<string, string[]>>,
180
+ sides: Partial<Record<string, Direction>> | undefined,
181
+ input: MeasureInput,
182
+ guard: number,
183
+ ): ExpansionGroup[] {
184
+ if (guard >= MAX_EXPANSION_DEPTH) return [];
185
+ const open = input.state.expanded[key] ?? [];
186
+ const groups: ExpansionGroup[] = [];
187
+ for (const group of open) {
188
+ const spec = input.profile.attachmentGroups[group];
189
+ const ids = attachments[group];
190
+ if (!spec || !ids?.length) continue;
191
+ const side = sides?.[group] ?? spec.defaultSide;
192
+ const items: ExpansionItem[] = [];
193
+ for (const id of ids) {
194
+ const child = input.index.nodeById.get(id);
195
+ if (!child) continue;
196
+ const ck = childKey(key, group, id);
197
+ const childAttachments = attachmentsFor(
198
+ input.index,
199
+ input.profile,
200
+ child,
201
+ undefined,
202
+ new Set([...input.visible, node.id, id]),
203
+ );
204
+ const childGroups = buildGroups(ck, child, childAttachments, undefined, input, guard + 1);
205
+ const bands = bandsOf(childGroups, CARD_W, CARD_H);
206
+ items.push({
207
+ key: ck,
208
+ node: child,
209
+ groups: childGroups,
210
+ attachments: childAttachments,
211
+ w: CARD_W,
212
+ h: CARD_H,
213
+ bands,
214
+ });
215
+ }
216
+ const { w, h } = measureGroup(items, side);
217
+ groups.push({ group, side, spec, items, slotW: w, slotH: h });
218
+ }
219
+ return groups;
220
+ }
221
+
222
+ /** Zmierz wszystkie kroki: drzewa rozwinięć + pasma (w tym notatki). */
223
+ export function measureSteps(input: MeasureInput): MeasuredStep[] {
224
+ return input.steps.map((step) => {
225
+ const key = rootKey(step.occId);
226
+ const groups: ExpansionGroup[] = [];
227
+ if (step.exists) {
228
+ groups.push(
229
+ ...buildGroups(key, step.node!, step.attachments, step.sides, input, 0),
230
+ );
231
+ }
232
+ // Notatki — zawsze widoczne, pogrupowane po stronie.
233
+ const bySide = new Map<Direction, typeof step.notes>();
234
+ for (const n of step.notes) {
235
+ const list = bySide.get(n.side) ?? [];
236
+ list.push(n);
237
+ bySide.set(n.side, list);
238
+ }
239
+ for (const [side, notes] of bySide) {
240
+ const items: ExpansionItem[] = notes.map((n) => ({
241
+ key: childKey(key, NOTES_GROUP, n.occId),
242
+ node: {
243
+ id: `note:${n.occId}`,
244
+ kind: "note",
245
+ label: n.text,
246
+ meta: { title: n.title },
247
+ },
248
+ groups: [],
249
+ attachments: {},
250
+ w: NOTE_W,
251
+ h: NOTE_H,
252
+ bands: ZERO_BANDS,
253
+ }));
254
+ const { w, h } = measureGroup(items, side);
255
+ groups.push({ group: NOTES_GROUP, side, spec: NOTES_SPEC, items, slotW: w, slotH: h });
256
+ }
257
+ const bands = bandsOf(groups, STEP_W, STEP_H);
258
+ return { step, key, groups, bands };
259
+ });
260
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `<Arc>` — inline'owa referencja do noda procesu w prozie MDX.
3
+ *
4
+ * Renderuje klikalny chip i rejestruje rozwiązaną referencję (w kolejności
5
+ * dokumentu), by diagram wiedział, które nody pokazać. Waliduje przeciw
6
+ * grafowi: nierozwiązana referencja dostaje czerwoną ramkę (TODO design-first).
7
+ *
8
+ * Nowe propsy względem arc-map:
9
+ * - `context="ai.chat"` — referencja CAŁEGO kontekstu jako kroku (box),
10
+ * - `sides={{ mutates: "SE" }}` — override strony doczepienia per grupa,
11
+ * - `expand={["mutates"]}` — grupy otwarte na starcie diagramu.
12
+ */
13
+
14
+ import { useId, type ReactNode } from "react";
15
+ import { kindColor } from "../model/profile";
16
+ import {
17
+ CONTEXT_REF_KIND,
18
+ type Direction,
19
+ type ResolvedRef,
20
+ } from "../model/types";
21
+ import { resolveContextRef } from "../model/build";
22
+ import { MISSING_COLOR, tint } from "../react/theme";
23
+ import {
24
+ useFocus,
25
+ useProcessData,
26
+ useRegister,
27
+ useStructure,
28
+ } from "./contexts";
29
+
30
+ export interface ArcProps {
31
+ /** Referencja całego kontekstu (kropkowana ścieżka nazw lub id). */
32
+ context?: string;
33
+ /** Override strony doczepienia per grupa relacji. */
34
+ sides?: Partial<Record<string, Direction>>;
35
+ /** Grupy relacji otwarte na starcie. */
36
+ expand?: string[];
37
+ children?: ReactNode;
38
+ /** Propsy referencyjne profilu (command/event/aggregate/… dla profilu Arc). */
39
+ [refProp: string]: unknown;
40
+ }
41
+
42
+ function textOf(node: ReactNode): string {
43
+ if (node == null || node === false) return "";
44
+ if (typeof node === "string" || typeof node === "number") return String(node);
45
+ if (Array.isArray(node)) return node.map(textOf).join("");
46
+ if (typeof node === "object" && "props" in (node as unknown as Record<string, unknown>)) {
47
+ return textOf(
48
+ (node as unknown as { props?: { children?: ReactNode } }).props?.children,
49
+ );
50
+ }
51
+ return "";
52
+ }
53
+
54
+ export function Arc(props: ArcProps) {
55
+ const { children, context, sides, expand, ...refProps } = props;
56
+ const domId = useId();
57
+ const register = useRegister();
58
+ const data = useProcessData();
59
+ const focus = useFocus();
60
+ const { path } = useStructure();
61
+
62
+ const ref: ResolvedRef | null = context
63
+ ? { nodeId: context, kind: CONTEXT_REF_KIND, key: `context:${context}` }
64
+ : (data?.profile.resolveRef(refProps) ?? null);
65
+ const label = textOf(children);
66
+
67
+ // Rejestracja podczas renderu — idempotentna (klucz domId), przeżywa StrictMode.
68
+ register?.({
69
+ domId,
70
+ occurrence: {
71
+ type: "step",
72
+ occId: domId,
73
+ ref,
74
+ label,
75
+ path,
76
+ sides,
77
+ expand,
78
+ },
79
+ });
80
+
81
+ const resolvedNode =
82
+ ref && data
83
+ ? ref.kind === CONTEXT_REF_KIND
84
+ ? resolveContextRef(data.graph.contexts ?? [], ref.nodeId)
85
+ : data.graph.nodes.find((n) => n.id === ref.nodeId)
86
+ : undefined;
87
+ const resolved = !!resolvedNode;
88
+ const focusId =
89
+ ref?.kind === CONTEXT_REF_KIND && resolvedNode && "id" in resolvedNode
90
+ ? `ctx:${resolvedNode.id}`
91
+ : ref?.nodeId;
92
+ const active = !!focusId && focus.focusNodeId === focusId;
93
+ const color = resolved
94
+ ? ref!.kind === CONTEXT_REF_KIND
95
+ ? (data ? kindColor(data.profile, CONTEXT_REF_KIND) : "#7c3aed")
96
+ : kindColor(data!.profile, (resolvedNode as { kind: string }).kind)
97
+ : MISSING_COLOR;
98
+
99
+ return (
100
+ <span
101
+ data-arc-id={domId}
102
+ data-arc-key={ref?.key ?? ""}
103
+ data-arc-node={focusId ?? ""}
104
+ onMouseEnter={() => resolved && focusId && focus.setFocusNodeId(focusId)}
105
+ onMouseLeave={() => focus.setFocusNodeId(null)}
106
+ title={
107
+ resolved
108
+ ? ref?.method
109
+ ? `${ref.nodeId}.${ref.method.name} (${ref.method.kind})`
110
+ : ref?.nodeId
111
+ : `Nieznana referencja: ${ref?.nodeId ?? JSON.stringify(refProps)}`
112
+ }
113
+ style={{
114
+ display: "inline-flex",
115
+ alignItems: "center",
116
+ gap: 4,
117
+ padding: "1px 8px",
118
+ margin: "0 1px",
119
+ borderRadius: 8,
120
+ border: `1.5px solid ${color}`,
121
+ background: resolved ? tint(color, 0.1) : "rgba(225,29,72,0.08)",
122
+ color: "#2a173f",
123
+ fontWeight: 600,
124
+ fontSize: "0.92em",
125
+ cursor: "pointer",
126
+ boxShadow: active ? `0 0 0 2px ${color}` : "none",
127
+ whiteSpace: "nowrap",
128
+ }}
129
+ >
130
+ <span
131
+ style={{ width: 7, height: 7, borderRadius: 2, background: color }}
132
+ />
133
+ {label || ref?.nodeId || "?"}
134
+ {ref?.method ? (
135
+ <span style={{ color: "#6b5b86", fontWeight: 500 }}>·{ref.method.name}</span>
136
+ ) : null}
137
+ </span>
138
+ );
139
+ }
@@ -0,0 +1,57 @@
1
+ /** Wspólne konteksty React widoku procesu: dane, fokus, struktura, rejestracja, stan. */
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type { ProcessProfile } from "../model/profile";
5
+ import type { Direction, Occurrence, PathSeg, ProcessGraph } from "../model/types";
6
+ import type { ExpansionKey, ProcessViewState } from "../state/view-state";
7
+
8
+ /** Graf + profil — walidacja referencji i kolory chipów. */
9
+ export interface ProcessData {
10
+ graph: ProcessGraph;
11
+ profile: ProcessProfile;
12
+ }
13
+ export const ProcessDataContext = createContext<ProcessData | null>(null);
14
+ export const useProcessData = () => useContext(ProcessDataContext);
15
+
16
+ /** Dwukierunkowy highlight chip <-> nod diagramu. */
17
+ export interface FocusState {
18
+ focusNodeId: string | null;
19
+ setFocusNodeId: (id: string | null) => void;
20
+ scrollToKey: string | null;
21
+ setScrollToKey: (key: string | null) => void;
22
+ }
23
+ export const FocusContext = createContext<FocusState>({
24
+ focusNodeId: null,
25
+ setFocusNodeId: () => {},
26
+ scrollToKey: null,
27
+ setScrollToKey: () => {},
28
+ });
29
+ export const useFocus = () => useContext(FocusContext);
30
+
31
+ /** Ścieżka strukturalna bieżącego poddrzewa MDX. */
32
+ export const StructureContext = createContext<{ path: PathSeg[] }>({ path: [] });
33
+ export const useStructure = () => useContext(StructureContext);
34
+
35
+ /** Wpis rejestracji: wystąpienie + DOM id (kolejność czytana z DOM). */
36
+ export interface RegEntry {
37
+ domId: string;
38
+ occurrence: Occurrence;
39
+ }
40
+ export const RegisterContext = createContext<((entry: RegEntry) => void) | null>(null);
41
+ export const useRegister = () => useContext(RegisterContext);
42
+
43
+ /** Stan widoku + akcje (toggle rozwinięć / zwinięć kontekstów). */
44
+ export interface ViewStateActions {
45
+ state: ProcessViewState;
46
+ toggleGroup: (key: ExpansionKey, group: string) => void;
47
+ toggleContext: (contextId: string) => void;
48
+ }
49
+ export const ViewStateContext = createContext<ViewStateActions>({
50
+ state: { expanded: {}, collapsedContexts: {} },
51
+ toggleGroup: () => {},
52
+ toggleContext: () => {},
53
+ });
54
+ export const useViewState = () => useContext(ViewStateContext);
55
+
56
+ /** Re-eksport typu dla wygody konsumentów mdx/. */
57
+ export type { Direction };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Renderuje dokument MDX procesu w przeglądarce. Kompilacja przez
3
+ * `@mdx-js/mdx` `evaluate` z wstrzykniętymi komponentami przepływu,
4
+ * w ArcRefsProvider — wystąpienia zbierane są w kolejności dokumentu.
5
+ */
6
+
7
+ import { evaluate } from "@mdx-js/mdx";
8
+ import remarkFrontmatter from "remark-frontmatter";
9
+ import remarkGfm from "remark-gfm";
10
+ import { useEffect, useState, type ComponentType, type ReactNode } from "react";
11
+ import * as runtime from "react/jsx-runtime";
12
+ import type { Occurrence } from "../model/types";
13
+ import { Arc } from "./arc";
14
+ import { Branch, Continue, Entry, Lane, Note, Parallel, Step } from "./structure";
15
+ import { ArcRefsProvider } from "./ref-context";
16
+ import { GLASS } from "../react/theme";
17
+
18
+ type ElProps = Record<string, unknown> & { children?: ReactNode };
19
+
20
+ const mdxComponents = {
21
+ Arc,
22
+ Parallel,
23
+ Branch,
24
+ Lane,
25
+ Step,
26
+ Entry,
27
+ Continue,
28
+ Note,
29
+ h1: (p: ElProps) => (
30
+ <h1 style={{ fontSize: 24, color: GLASS.text, margin: "4px 0 12px" }} {...p} />
31
+ ),
32
+ h2: (p: ElProps) => (
33
+ <h2 style={{ fontSize: 18, color: GLASS.text, margin: "22px 0 8px" }} {...p} />
34
+ ),
35
+ h3: (p: ElProps) => (
36
+ <h3 style={{ fontSize: 15, color: GLASS.text, margin: "16px 0 6px" }} {...p} />
37
+ ),
38
+ p: (p: ElProps) => (
39
+ <p style={{ fontSize: 14, lineHeight: 1.7, color: "#3b2a55", margin: "8px 0" }} {...p} />
40
+ ),
41
+ li: (p: ElProps) => (
42
+ <li style={{ fontSize: 14, lineHeight: 1.7, color: "#3b2a55" }} {...p} />
43
+ ),
44
+ code: (p: ElProps) => (
45
+ <code
46
+ style={{
47
+ background: "rgba(124,58,237,0.08)",
48
+ padding: "1px 5px",
49
+ borderRadius: 5,
50
+ fontSize: "0.9em",
51
+ }}
52
+ {...p}
53
+ />
54
+ ),
55
+ a: (p: ElProps) => <a style={{ color: "#7c3aed" }} {...p} />,
56
+ };
57
+
58
+ /** Frontmatter dokumentu procesu (parsowany lekko, bez YAML-parsera). */
59
+ export interface ProcessFrontmatter {
60
+ title?: string;
61
+ /** Tryb zwijania kontekstów: "all" (górny rzut, domyślnie) | "none". */
62
+ collapse?: "all" | "none";
63
+ /** Nazwa/id kontekstu domowego — jego narracja nie zwija się domyślnie. */
64
+ context?: string;
65
+ }
66
+
67
+ export function parseProcessFrontmatter(mdx: string): ProcessFrontmatter {
68
+ const m = mdx.match(/^---\n([\s\S]*?)\n---/);
69
+ if (!m) return {};
70
+ const out: ProcessFrontmatter = {};
71
+ for (const line of m[1].split("\n")) {
72
+ const kv = line.match(/^(\w+):\s*(.+)\s*$/);
73
+ if (!kv) continue;
74
+ const value = kv[2].replace(/^["']|["']$/g, "");
75
+ if (kv[1] === "title") out.title = value;
76
+ else if (kv[1] === "collapse" && (value === "all" || value === "none")) {
77
+ out.collapse = value;
78
+ } else if (kv[1] === "context") out.context = value;
79
+ }
80
+ return out;
81
+ }
82
+
83
+ export function MdxView({
84
+ mdx,
85
+ onOccurrences,
86
+ }: {
87
+ mdx: string;
88
+ onOccurrences: (occurrences: Occurrence[]) => void;
89
+ }) {
90
+ const [Comp, setComp] = useState<ComponentType<{
91
+ components: typeof mdxComponents;
92
+ }> | null>(null);
93
+ const [error, setError] = useState<string | null>(null);
94
+
95
+ useEffect(() => {
96
+ let cancelled = false;
97
+ setComp(null);
98
+ setError(null);
99
+ evaluate(mdx, {
100
+ ...(runtime as object),
101
+ remarkPlugins: [remarkFrontmatter, remarkGfm],
102
+ } as Parameters<typeof evaluate>[1])
103
+ .then((mod) => {
104
+ if (!cancelled) {
105
+ setComp(() => mod.default as ComponentType<{ components: typeof mdxComponents }>);
106
+ }
107
+ })
108
+ .catch((e) => {
109
+ if (!cancelled) setError(String(e));
110
+ });
111
+ return () => {
112
+ cancelled = true;
113
+ };
114
+ }, [mdx]);
115
+
116
+ if (error) {
117
+ return (
118
+ <pre style={{ color: "#be123c", fontSize: 12, whiteSpace: "pre-wrap" }}>
119
+ Błąd renderu MDX:{"\n"}
120
+ {error}
121
+ </pre>
122
+ );
123
+ }
124
+ if (!Comp) return <div style={{ color: GLASS.textMuted }}>Renderuję dokument…</div>;
125
+
126
+ return (
127
+ <ArcRefsProvider onOccurrences={onOccurrences}>
128
+ <Comp components={mdxComponents} />
129
+ </ArcRefsProvider>
130
+ );
131
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ArcRefsProvider — zbiera rejestracje wystąpień (`<Arc>`, `<Continue>`,
3
+ * `<Note>`) wyrenderowanych w środku i raportuje je rodzicowi w KOLEJNOŚCI
4
+ * DOKUMENTU (czytanej z DOM, więc zagnieżdżenia i porządek są dokładne).
5
+ * Remount per dokument (przez `key`) daje świeży rejestr. Chroniony przed
6
+ * pętlami update'ów porównaniem sygnatury uporządkowanej listy.
7
+ */
8
+
9
+ import { useCallback, useEffect, useRef, type ReactNode } from "react";
10
+ import type { Occurrence } from "../model/types";
11
+ import { RegisterContext, type RegEntry } from "./contexts";
12
+
13
+ export function ArcRefsProvider({
14
+ children,
15
+ onOccurrences,
16
+ }: {
17
+ children: ReactNode;
18
+ onOccurrences: (occurrences: Occurrence[]) => void;
19
+ }) {
20
+ const registry = useRef(new Map<string, RegEntry>());
21
+ const containerRef = useRef<HTMLDivElement>(null);
22
+ const lastSig = useRef<string>("");
23
+
24
+ const register = useCallback((entry: RegEntry) => {
25
+ registry.current.set(entry.domId, entry);
26
+ }, []);
27
+
28
+ // Po każdym renderze czytamy znaczniki w kolejności DOM i podnosimy listę —
29
+ // tylko gdy uporządkowany zestaw faktycznie się zmienił.
30
+ useEffect(() => {
31
+ const el = containerRef.current;
32
+ if (!el) return;
33
+ const ordered: Occurrence[] = [];
34
+ el.querySelectorAll<HTMLElement>("[data-arc-id]").forEach((node) => {
35
+ const e = registry.current.get(node.dataset.arcId ?? "");
36
+ if (e) ordered.push(e.occurrence);
37
+ });
38
+ const sig = ordered
39
+ .map((o) =>
40
+ o.type === "step"
41
+ ? (o.ref?.key ?? `?${o.occId}`)
42
+ : o.type === "continue"
43
+ ? `cont:${o.to}`
44
+ : `note:${o.occId}`,
45
+ )
46
+ .join("|");
47
+ if (sig === lastSig.current) return;
48
+ lastSig.current = sig;
49
+ onOccurrences(ordered);
50
+ });
51
+
52
+ return (
53
+ <div ref={containerRef}>
54
+ <RegisterContext.Provider value={register}>{children}</RegisterContext.Provider>
55
+ </div>
56
+ );
57
+ }