@arcote.tech/arc-map 0.7.27

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,478 @@
1
+ /** Custom React Flow node renderers: space box, element, React component.
2
+ * Light glassmorphism styling (frosted glass + purple accents, ndt-like). */
3
+
4
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
5
+ import type { MapComponent, MapElement } from "../types";
6
+ import { COMPONENT_COLOR, ELEMENT_COLOR, GLASS } from "./theme";
7
+ import { useExpand, useFocus } from "./contexts";
8
+ import type { RelKind, StepNode as StepModel } from "./usecase-model";
9
+
10
+ const REL_LABEL: Record<RelKind, string> = {
11
+ queries: "czyta",
12
+ mutates: "mutuje",
13
+ mutatedBy: "mutowane przez",
14
+ queriedBy: "czytane przez",
15
+ handledBy: "obsługiwane przez",
16
+ usedByUI: "UI",
17
+ };
18
+
19
+ const KIND_ICON: Record<string, string> = {
20
+ aggregate: "◆",
21
+ command: "▶",
22
+ event: "✶",
23
+ view: "▦",
24
+ "static-view": "▤",
25
+ listener: "♪",
26
+ route: "⇄",
27
+ page: "▢",
28
+ unknown: "•",
29
+ };
30
+
31
+ /** Hex accent -> translucent tint for the element surface. */
32
+ function tint(hex: string, alpha: number): string {
33
+ const h = hex.replace("#", "");
34
+ const r = parseInt(h.slice(0, 2), 16);
35
+ const g = parseInt(h.slice(2, 4), 16);
36
+ const b = parseInt(h.slice(4, 6), 16);
37
+ return `rgba(${r},${g},${b},${alpha})`;
38
+ }
39
+
40
+ export function SpaceNode({ data }: NodeProps) {
41
+ const d = data as { label: string; count: number };
42
+ return (
43
+ <div
44
+ style={{
45
+ width: "100%",
46
+ height: "100%",
47
+ borderRadius: 18,
48
+ border: `1px solid ${GLASS.border}`,
49
+ background: GLASS.space,
50
+ backdropFilter: GLASS.blur,
51
+ WebkitBackdropFilter: GLASS.blur,
52
+ boxShadow: GLASS.shadow,
53
+ }}
54
+ >
55
+ <div
56
+ style={{
57
+ padding: "11px 18px",
58
+ fontWeight: 700,
59
+ fontSize: 14,
60
+ color: GLASS.text,
61
+ letterSpacing: 0.2,
62
+ borderBottom: `1px solid ${GLASS.border}`,
63
+ display: "flex",
64
+ gap: 9,
65
+ alignItems: "center",
66
+ }}
67
+ >
68
+ <span
69
+ style={{
70
+ width: 8,
71
+ height: 8,
72
+ borderRadius: 99,
73
+ background: "#7c3aed",
74
+ boxShadow: "0 0 0 4px rgba(124,58,237,0.18)",
75
+ }}
76
+ />
77
+ <span>{d.label}</span>
78
+ <span
79
+ style={{
80
+ fontSize: 11,
81
+ color: GLASS.textMuted,
82
+ background: "rgba(124,58,237,0.10)",
83
+ borderRadius: 99,
84
+ padding: "1px 8px",
85
+ }}
86
+ >
87
+ {d.count}
88
+ </span>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ export function ElementNode({ data, selected }: NodeProps) {
95
+ const d = data as { element: MapElement; seq?: number; referenced?: boolean };
96
+ const el = d.element;
97
+ const color = ELEMENT_COLOR[el.kind] ?? ELEMENT_COLOR.unknown;
98
+ // In use-case mode `referenced` is set: referenced nodes stay solid, related
99
+ // (connector/neighbour) nodes are de-emphasized.
100
+ const related = d.referenced === false;
101
+ return (
102
+ <div
103
+ style={{
104
+ position: "relative",
105
+ width: 196,
106
+ height: 62,
107
+ boxSizing: "border-box",
108
+ borderRadius: 13,
109
+ border: `1px solid ${selected ? color : GLASS.border}`,
110
+ borderLeft: `4px solid ${color}`,
111
+ background: selected ? GLASS.cardStrong : GLASS.card,
112
+ backdropFilter: GLASS.blurSoft,
113
+ WebkitBackdropFilter: GLASS.blurSoft,
114
+ padding: "9px 12px",
115
+ display: "flex",
116
+ flexDirection: "column",
117
+ gap: 3,
118
+ cursor: "pointer",
119
+ opacity: related ? 0.62 : 1,
120
+ boxShadow: selected ? GLASS.shadowStrong : GLASS.shadow,
121
+ }}
122
+ >
123
+ {d.seq != null ? (
124
+ <span
125
+ style={{
126
+ position: "absolute",
127
+ top: -9,
128
+ left: -9,
129
+ width: 20,
130
+ height: 20,
131
+ borderRadius: 99,
132
+ background: color,
133
+ color: "#fff",
134
+ fontSize: 11,
135
+ fontWeight: 700,
136
+ display: "flex",
137
+ alignItems: "center",
138
+ justifyContent: "center",
139
+ boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
140
+ }}
141
+ >
142
+ {d.seq}
143
+ </span>
144
+ ) : null}
145
+ <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
146
+ <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
147
+ <div style={{ display: "flex", gap: 7, alignItems: "center" }}>
148
+ <span style={{ color, fontSize: 13 }}>{KIND_ICON[el.kind] ?? "•"}</span>
149
+ <span
150
+ style={{
151
+ fontWeight: 700,
152
+ fontSize: 12.5,
153
+ color: GLASS.text,
154
+ overflow: "hidden",
155
+ textOverflow: "ellipsis",
156
+ whiteSpace: "nowrap",
157
+ }}
158
+ >
159
+ {el.name}
160
+ </span>
161
+ </div>
162
+ <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
163
+ <span
164
+ style={{
165
+ fontSize: 9.5,
166
+ fontWeight: 600,
167
+ color,
168
+ background: tint(color, 0.12),
169
+ borderRadius: 5,
170
+ padding: "1px 6px",
171
+ textTransform: "uppercase",
172
+ letterSpacing: 0.4,
173
+ }}
174
+ >
175
+ {el.kind}
176
+ </span>
177
+ {el.protections?.length ? (
178
+ <span style={{ fontSize: 10 }} title={el.protections.join(", ")}>
179
+ 🔒
180
+ </span>
181
+ ) : null}
182
+ {el.moduleName ? (
183
+ <span style={{ fontSize: 9.5, color: GLASS.textFaint, marginLeft: "auto" }}>
184
+ {el.moduleName}
185
+ </span>
186
+ ) : null}
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ export function ComponentNode({ data, selected }: NodeProps) {
193
+ const c = (data as { component: MapComponent }).component;
194
+ return (
195
+ <div
196
+ style={{
197
+ width: 214,
198
+ height: 54,
199
+ boxSizing: "border-box",
200
+ borderRadius: 13,
201
+ border: `1.5px dashed ${selected ? COMPONENT_COLOR : GLASS.border}`,
202
+ background: selected ? GLASS.cardStrong : "rgba(255,255,255,0.55)",
203
+ backdropFilter: GLASS.blurSoft,
204
+ WebkitBackdropFilter: GLASS.blurSoft,
205
+ padding: "8px 12px",
206
+ cursor: "pointer",
207
+ boxShadow: GLASS.shadow,
208
+ }}
209
+ >
210
+ <Handle type="source" position={Position.Right} style={{ background: COMPONENT_COLOR, border: "none" }} />
211
+ <div
212
+ style={{
213
+ fontSize: 12,
214
+ fontWeight: 700,
215
+ color: GLASS.text,
216
+ overflow: "hidden",
217
+ textOverflow: "ellipsis",
218
+ whiteSpace: "nowrap",
219
+ }}
220
+ >
221
+ ⚛ {c.name ?? c.file}
222
+ </div>
223
+ <div
224
+ style={{
225
+ fontSize: 9.5,
226
+ color: GLASS.textFaint,
227
+ overflow: "hidden",
228
+ textOverflow: "ellipsis",
229
+ whiteSpace: "nowrap",
230
+ }}
231
+ >
232
+ {c.file}
233
+ </div>
234
+ </div>
235
+ );
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Use-case data-flow nodes
240
+ // ---------------------------------------------------------------------------
241
+
242
+ /** A narrated `<Arc>` step on the backbone — with badges per relation kind. */
243
+ export function StepNode({ data }: NodeProps) {
244
+ const step = (data as { step: StepModel }).step;
245
+ const expandedKinds = (data as { expanded: RelKind[] }).expanded ?? [];
246
+ const { toggle } = useExpand();
247
+ const { focusNodeId } = useFocus();
248
+ const el = step.element;
249
+ const missing = !step.exists;
250
+ // Keep the declared kind even when the element does not exist yet, so we don't
251
+ // lose knowledge — colour/label like any other node, just dashed + faded.
252
+ const kind = (el?.kind ?? (step.ref?.kind as string) ?? "unknown") as keyof typeof ELEMENT_COLOR;
253
+ const color = ELEMENT_COLOR[kind] ?? ELEMENT_COLOR.unknown;
254
+ const myId = el?.id ?? step.ref?.nodeId;
255
+ const dimmed = !!focusNodeId && focusNodeId !== myId;
256
+ const focused = !!focusNodeId && focusNodeId === myId;
257
+ const badgeKinds = Object.keys(step.badges) as RelKind[];
258
+
259
+ return (
260
+ <div
261
+ style={{
262
+ minWidth: 200,
263
+ maxWidth: 230,
264
+ boxSizing: "border-box",
265
+ borderRadius: 14,
266
+ // Dashed is reserved for MISSING (not-yet-implemented) elements. Branch
267
+ // alternatives stay solid — their "alt" nature is shown by the ⋔ badge,
268
+ // not by a dashed border that reads as "not implemented".
269
+ border: `1px ${missing ? "dashed" : "solid"} ${color}`,
270
+ borderLeft: `5px ${missing ? "dashed" : "solid"} ${color}`,
271
+ background: GLASS.cardStrong,
272
+ backdropFilter: GLASS.blurSoft,
273
+ WebkitBackdropFilter: GLASS.blurSoft,
274
+ padding: "10px 12px",
275
+ position: "relative",
276
+ opacity: dimmed ? 0.35 : missing ? 0.7 : 1,
277
+ boxShadow: focused ? `0 0 0 2px ${color}` : GLASS.shadow,
278
+ transition: "opacity 120ms ease, box-shadow 120ms ease",
279
+ }}
280
+ >
281
+ <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
282
+ <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
283
+ <span
284
+ style={{
285
+ position: "absolute",
286
+ top: -10,
287
+ left: -10,
288
+ width: 22,
289
+ height: 22,
290
+ borderRadius: 99,
291
+ background: color,
292
+ color: "#fff",
293
+ fontSize: 12,
294
+ fontWeight: 700,
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "center",
298
+ boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
299
+ }}
300
+ >
301
+ {step.seq}
302
+ </span>
303
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
304
+ <span style={{ color, fontSize: 13 }}>{KIND_ICON[kind] ?? "•"}</span>
305
+ <span style={{ fontWeight: 700, fontSize: 13, color: GLASS.text }}>
306
+ {step.ref?.nodeId ?? step.label}
307
+ </span>
308
+ </div>
309
+ <div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 2 }}>
310
+ <span
311
+ style={{
312
+ fontSize: 9.5,
313
+ fontWeight: 600,
314
+ color,
315
+ background: tint(color, 0.12),
316
+ borderRadius: 5,
317
+ padding: "1px 6px",
318
+ textTransform: "uppercase",
319
+ letterSpacing: 0.4,
320
+ }}
321
+ >
322
+ {kind}
323
+ </span>
324
+ {step.method ? (
325
+ <span style={{ fontSize: 11, color: "#3b2a55" }}>
326
+ ·{step.method.name}
327
+ <span style={{ color: GLASS.textFaint }}> ({step.method.kind})</span>
328
+ </span>
329
+ ) : null}
330
+ </div>
331
+ {step.label && step.label !== step.ref?.nodeId ? (
332
+ <div style={{ fontSize: 11, color: GLASS.textMuted, marginTop: 3 }}>{step.label}</div>
333
+ ) : null}
334
+ {step.stepTitle || step.laneTitle || step.alt ? (
335
+ <div style={{ display: "flex", gap: 5, marginTop: 4, flexWrap: "wrap" }}>
336
+ {step.stepTitle ? (
337
+ <span style={{ fontSize: 9, fontWeight: 700, color: "#1c7ed6", background: "rgba(28,126,214,0.1)", borderRadius: 4, padding: "0 5px" }}>
338
+ ▸ {step.stepTitle}
339
+ </span>
340
+ ) : null}
341
+ {step.alt ? (
342
+ <span style={{ fontSize: 9, fontWeight: 700, color: "#e64980", background: "rgba(230,73,128,0.1)", borderRadius: 4, padding: "0 5px" }}>
343
+ ⋔ {step.laneTitle ?? "alt"}
344
+ </span>
345
+ ) : step.laneTitle ? (
346
+ <span style={{ fontSize: 9, fontWeight: 700, color: "#7c3aed", background: "rgba(124,58,237,0.1)", borderRadius: 4, padding: "0 5px" }}>
347
+ ∥ {step.laneTitle}
348
+ </span>
349
+ ) : null}
350
+ </div>
351
+ ) : null}
352
+ {badgeKinds.length ? (
353
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 7 }}>
354
+ {badgeKinds.map((k) => {
355
+ const active = expandedKinds.includes(k);
356
+ const c = REL_BADGE_COLOR[k];
357
+ return (
358
+ <button
359
+ key={k}
360
+ onClick={(e) => {
361
+ e.stopPropagation();
362
+ toggle(step.occId, k);
363
+ }}
364
+ style={{
365
+ fontSize: 9.5,
366
+ fontWeight: 600,
367
+ border: `1px solid ${c}`,
368
+ background: active ? c : tint(c, 0.1),
369
+ color: active ? "#fff" : c,
370
+ borderRadius: 6,
371
+ padding: "1px 6px",
372
+ cursor: "pointer",
373
+ }}
374
+ title={`${REL_LABEL[k]}: ${step.badges[k]!.join(", ")}`}
375
+ >
376
+ {relBadgeLabel(k, el?.kind)} {step.badges[k]!.length}
377
+ </button>
378
+ );
379
+ })}
380
+ </div>
381
+ ) : null}
382
+ </div>
383
+ );
384
+ }
385
+
386
+ const REL_BADGE_COLOR: Record<RelKind, string> = {
387
+ queries: "#2f9e44",
388
+ mutates: "#e11d48",
389
+ mutatedBy: "#d97706",
390
+ queriedBy: "#0ca678",
391
+ handledBy: "#1c7ed6",
392
+ usedByUI: "#4263eb",
393
+ };
394
+
395
+ function relBadgeLabel(k: RelKind, elKind?: string): string {
396
+ if (k === "mutatedBy" && elKind === "event") return "emitowane przez";
397
+ return REL_LABEL[k];
398
+ }
399
+
400
+ /** A dependency element revealed in a left/right column when a badge expands. */
401
+ export function DepNode({ data }: NodeProps) {
402
+ const el = (data as { element: MapElement }).element;
403
+ const { focusNodeId } = useFocus();
404
+ const color = ELEMENT_COLOR[el.kind] ?? "#64748b";
405
+ const dimmed = !!focusNodeId && focusNodeId !== el.id;
406
+ const focused = !!focusNodeId && focusNodeId === el.id;
407
+ return (
408
+ <div
409
+ style={{
410
+ width: 178,
411
+ boxSizing: "border-box",
412
+ borderRadius: 10,
413
+ border: `1px solid ${color}`,
414
+ borderLeft: `4px solid ${color}`,
415
+ background: GLASS.card,
416
+ padding: "6px 9px",
417
+ opacity: dimmed ? 0.35 : 1,
418
+ boxShadow: focused ? `0 0 0 2px ${color}` : GLASS.shadow,
419
+ transition: "opacity 120ms ease",
420
+ }}
421
+ >
422
+ <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
423
+ <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
424
+ <div style={{ display: "flex", gap: 5, alignItems: "center" }}>
425
+ <span style={{ color, fontSize: 11 }}>{KIND_ICON[el.kind] ?? "•"}</span>
426
+ <span style={{ fontWeight: 600, fontSize: 11.5, color: GLASS.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
427
+ {el.name}
428
+ </span>
429
+ </div>
430
+ <div style={{ fontSize: 8.5, color, textTransform: "uppercase", letterSpacing: 0.3 }}>{el.kind}</div>
431
+ </div>
432
+ );
433
+ }
434
+
435
+ /** An element on a connecting flow path between two narrated steps. Rendered
436
+ * exactly like any other element node — no special "connector" marking. */
437
+ export function ConnectorNode({ data }: NodeProps) {
438
+ const el = (data as { element: MapElement }).element;
439
+ const { focusNodeId } = useFocus();
440
+ const color = ELEMENT_COLOR[el.kind] ?? "#64748b";
441
+ const dimmed = !!focusNodeId && focusNodeId !== el.id;
442
+ const focused = !!focusNodeId && focusNodeId === el.id;
443
+ return (
444
+ <div
445
+ style={{
446
+ width: 178,
447
+ boxSizing: "border-box",
448
+ borderRadius: 10,
449
+ border: `1px solid ${color}`,
450
+ borderLeft: `4px solid ${color}`,
451
+ background: GLASS.card,
452
+ padding: "6px 9px",
453
+ opacity: dimmed ? 0.35 : 1,
454
+ boxShadow: focused ? `0 0 0 2px ${color}` : GLASS.shadow,
455
+ transition: "opacity 120ms ease",
456
+ }}
457
+ >
458
+ <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
459
+ <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
460
+ <div style={{ display: "flex", gap: 5, alignItems: "center" }}>
461
+ <span style={{ color, fontSize: 11 }}>{KIND_ICON[el.kind] ?? "•"}</span>
462
+ <span style={{ fontWeight: 600, fontSize: 11.5, color: GLASS.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
463
+ {el.name}
464
+ </span>
465
+ </div>
466
+ <div style={{ fontSize: 8.5, color, textTransform: "uppercase", letterSpacing: 0.3 }}>{el.kind}</div>
467
+ </div>
468
+ );
469
+ }
470
+
471
+ export const nodeTypes = {
472
+ space: SpaceNode,
473
+ element: ElementNode,
474
+ component: ComponentNode,
475
+ step: StepNode,
476
+ dep: DepNode,
477
+ connector: ConnectorNode,
478
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * ArcRefsProvider — collects `<Arc>` registrations rendered inside it and
3
+ * reports them to the parent in DOCUMENT ORDER (read from the DOM, so nesting
4
+ * and ordering are exact). Remount per use-case (via `key`) gives a fresh
5
+ * registry. Guarded against update loops by comparing the ordered key list.
6
+ */
7
+
8
+ import { useCallback, useEffect, useRef, type ReactNode } from "react";
9
+ import { ArcRegisterContext, type ArcRegEntry } from "./contexts";
10
+
11
+ export function ArcRefsProvider({
12
+ children,
13
+ onRefs,
14
+ }: {
15
+ children: ReactNode;
16
+ onRefs: (refs: ArcRegEntry[]) => void;
17
+ }) {
18
+ const registry = useRef(new Map<string, ArcRegEntry>());
19
+ const containerRef = useRef<HTMLDivElement>(null);
20
+ const lastSig = useRef<string>("");
21
+
22
+ const register = useCallback((entry: ArcRegEntry) => {
23
+ registry.current.set(entry.id, entry);
24
+ }, []);
25
+
26
+ // After every render, read the chips in DOM order and lift the list up —
27
+ // but only when the ordered set actually changed (prevents render loops).
28
+ useEffect(() => {
29
+ const el = containerRef.current;
30
+ if (!el) return;
31
+ const ordered: ArcRegEntry[] = [];
32
+ el.querySelectorAll<HTMLElement>("[data-arc-id]").forEach((node) => {
33
+ const e = registry.current.get(node.dataset.arcId ?? "");
34
+ if (e) ordered.push(e);
35
+ });
36
+ const sig = ordered.map((e) => e.ref?.key ?? `?${e.id}`).join("|");
37
+ if (sig === lastSig.current) return;
38
+ lastSig.current = sig;
39
+ onRefs(ordered);
40
+ });
41
+
42
+ return (
43
+ <div ref={containerRef}>
44
+ <ArcRegisterContext.Provider value={register}>
45
+ {children}
46
+ </ArcRegisterContext.Provider>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Structural MDX components for use-case flows. They render a light visual cue
3
+ * in the prose AND give each nested `<Arc>` a structural path (via context),
4
+ * which the model turns into depth (time) × lane (parallel / branch).
5
+ *
6
+ * <Parallel> — children run concurrently (AND); same depth, lanes
7
+ * <Branch><Lane>…</Lane> — alternative paths (XOR); branch lanes
8
+ * <Step title="…"> — a named phase grouping a sub-sequence
9
+ */
10
+
11
+ import { Children, isValidElement, useId, type ReactNode } from "react";
12
+ import { StructureContext, useStructure, type PathSeg } from "./contexts";
13
+ import { GLASS } from "./theme";
14
+
15
+ /** Wrap each element child with a structural segment + lane index. */
16
+ function mapLanes(children: ReactNode, base: PathSeg[], seg: (i: number) => PathSeg): ReactNode {
17
+ let i = 0;
18
+ return Children.map(children, (child) => {
19
+ if (!isValidElement(child)) return child;
20
+ const s = seg(i++);
21
+ return <StructureContext.Provider value={{ path: [...base, s] }}>{child}</StructureContext.Provider>;
22
+ });
23
+ }
24
+
25
+ const wrapStyle: React.CSSProperties = {
26
+ display: "block",
27
+ borderLeft: `3px solid ${GLASS.border}`,
28
+ paddingLeft: 12,
29
+ margin: "8px 0",
30
+ };
31
+
32
+ const tagStyle = (color: string): React.CSSProperties => ({
33
+ display: "inline-block",
34
+ fontSize: 10,
35
+ fontWeight: 700,
36
+ textTransform: "uppercase",
37
+ letterSpacing: 0.5,
38
+ color,
39
+ background: "rgba(124,58,237,0.08)",
40
+ borderRadius: 5,
41
+ padding: "1px 7px",
42
+ marginBottom: 4,
43
+ });
44
+
45
+ export function Parallel({ children }: { children?: ReactNode }) {
46
+ const id = useId();
47
+ const { path } = useStructure();
48
+ return (
49
+ <span style={{ ...wrapStyle, borderLeftColor: "#7c3aed" }}>
50
+ <span style={tagStyle("#7c3aed")}>∥ równolegle</span>
51
+ {mapLanes(children, path, (i) => ({ type: "parallel", id, laneIndex: i }))}
52
+ </span>
53
+ );
54
+ }
55
+
56
+ export function Branch({ children }: { children?: ReactNode }) {
57
+ const id = useId();
58
+ const { path } = useStructure();
59
+ return (
60
+ <span style={{ ...wrapStyle, borderLeftColor: "#e64980" }}>
61
+ <span style={tagStyle("#e64980")}>⋔ alternatywnie</span>
62
+ {mapLanes(children, path, (i) => ({ type: "branch", id, laneIndex: i }))}
63
+ </span>
64
+ );
65
+ }
66
+
67
+ export function Lane({ title, children }: { title?: string; children?: ReactNode }) {
68
+ const id = useId();
69
+ const { path } = useStructure();
70
+ return (
71
+ <StructureContext.Provider value={{ path: [...path, { type: "lane", id, title, alt: true }] }}>
72
+ <span style={{ display: "block", margin: "4px 0" }}>
73
+ {title ? (
74
+ <span style={{ fontSize: 11, fontWeight: 700, color: "#e64980", marginRight: 6 }}>
75
+ {title}:
76
+ </span>
77
+ ) : null}
78
+ {children}
79
+ </span>
80
+ </StructureContext.Provider>
81
+ );
82
+ }
83
+
84
+ export function Step({ title, children }: { title?: string; children?: ReactNode }) {
85
+ const id = useId();
86
+ const { path } = useStructure();
87
+ return (
88
+ <StructureContext.Provider value={{ path: [...path, { type: "step", id, title }] }}>
89
+ <span style={{ ...wrapStyle, borderLeftColor: "#1c7ed6" }}>
90
+ {title ? <span style={tagStyle("#1c7ed6")}>▸ {title}</span> : null}
91
+ {children}
92
+ </span>
93
+ </StructureContext.Provider>
94
+ );
95
+ }