@arcote.tech/arc-map 0.7.27 → 0.7.29

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.
@@ -11,7 +11,7 @@ import {
11
11
  } from "@arcote.tech/platform";
12
12
  import type { ModuleInfo } from "./mapper";
13
13
  import type { MapElement } from "./types";
14
- import { normalizePagePath } from "./refs";
14
+ import { normalizePagePath } from "@arcote.tech/arc-process";
15
15
 
16
16
  export function buildModuleIndex(): Map<string, ModuleInfo> {
17
17
  const index = new Map<string, ModuleInfo>();
package/src/scan.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  type SourceFile,
23
23
  } from "ts-morph";
24
24
  import type { MapComponent, MapComponentEdge } from "./types";
25
- import { normalizePagePath } from "./refs";
25
+ import { normalizePagePath } from "@arcote.tech/arc-process";
26
26
 
27
27
  export interface ScanInput {
28
28
  /** Absolute glob(s) or file paths of TSX sources to scan. */
package/src/server.ts CHANGED
@@ -12,11 +12,13 @@
12
12
  * module index, and the scan globs.
13
13
  */
14
14
 
15
- import { readFileSync } from "node:fs";
15
+ import { readFileSync, realpathSync } from "node:fs";
16
+ import { resolve, sep } from "node:path";
16
17
  import { fileURLToPath } from "node:url";
17
18
  import type { ArcContextAny } from "@arcote.tech/arc";
18
19
  import { buildModuleIndex, buildPageNodes } from "./module-index";
19
20
  import { buildMapJson, type ModuleInfo } from "./mapper";
21
+ import { loadCatalog } from "./catalog";
20
22
  import { scanScopeUsages, type ScanInput } from "./scan";
21
23
  import { discoverUseCases, type UseCasePackage } from "./use-cases/discover";
22
24
  import type { MapJson, UseCaseEntry } from "./types";
@@ -123,6 +125,18 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
123
125
  return scanCache;
124
126
  };
125
127
 
128
+ // Cached generic catalog (arc.catalog.yml root + pakiety) — reload on notify.
129
+ let catalogCache: ReturnType<typeof loadCatalog> | null = null;
130
+ const getCatalog = () => {
131
+ if (!catalogCache) {
132
+ catalogCache = loadCatalog(
133
+ opts.scan.workspaceRoot,
134
+ (opts.useCasePackages ?? []).map((p) => p.path),
135
+ );
136
+ }
137
+ return catalogCache;
138
+ };
139
+
126
140
  const buildPayload = (): MapJson => {
127
141
  const context = opts.getContext();
128
142
  if (!context) {
@@ -136,6 +150,7 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
136
150
  componentEdges: scan.componentEdges,
137
151
  components: scan.components,
138
152
  pages: buildPageNodes(),
153
+ catalog: getCatalog(),
139
154
  generatedAt: new Date().toISOString(),
140
155
  });
141
156
  };
@@ -151,6 +166,7 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
151
166
  const notifyReload = () => {
152
167
  scanCache = null;
153
168
  useCaseCache = null;
169
+ catalogCache = null;
154
170
  for (const c of sseClients) {
155
171
  try {
156
172
  c.enqueue("data: reload\n\n");
@@ -211,6 +227,37 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
211
227
  headers: { ...CORS, "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-cache" },
212
228
  });
213
229
  }
230
+ case "/api/source": {
231
+ // Kod źródłowy elementu do drawera. Ścieżka może być absolutna
232
+ // (z getSourceLocation) lub względna wobec workspace root — zawsze
233
+ // MUSI wskazywać plik WEWNĄTRZ workspace (guard path-traversal,
234
+ // sprawdzany na realpath, więc symlinki poza root też odpadają).
235
+ const file = url.searchParams.get("file");
236
+ if (!file) return new Response("Bad Request", { status: 400, headers: CORS });
237
+ const rootReal = realpathSync(opts.scan.workspaceRoot);
238
+ let abs: string;
239
+ try {
240
+ abs = realpathSync(resolve(rootReal, file));
241
+ } catch {
242
+ return new Response("Not Found", { status: 404, headers: CORS });
243
+ }
244
+ if (abs !== rootReal && !abs.startsWith(rootReal + sep)) {
245
+ return new Response("Forbidden", { status: 403, headers: CORS });
246
+ }
247
+ let text: string;
248
+ try {
249
+ text = readFileSync(abs, "utf-8");
250
+ } catch (e) {
251
+ return new Response((e as Error).message, { status: 500, headers: CORS });
252
+ }
253
+ return new Response(text, {
254
+ headers: {
255
+ ...CORS,
256
+ "Content-Type": "text/plain; charset=utf-8",
257
+ "Cache-Control": "no-cache",
258
+ },
259
+ });
260
+ }
214
261
  case "/api/reload-stream": {
215
262
  const stream = new ReadableStream({
216
263
  start(controller) {
package/src/types.ts CHANGED
@@ -14,6 +14,7 @@ export type ElementKind =
14
14
  | "listener"
15
15
  | "route"
16
16
  | "page"
17
+ | "tool"
17
18
  | "unknown";
18
19
 
19
20
  /** Kind of a domain edge between two context elements. */
@@ -38,6 +39,36 @@ export interface MapElementMethod {
38
39
  mutationDeps?: string[];
39
40
  }
40
41
 
42
+ /**
43
+ * Wpis generycznego katalogu deklaracji (`arc.catalog.yml`) — persony,
44
+ * metryki i przyszłe rodzaje przypięć. `kind` = klucz najwyższego poziomu
45
+ * YAML-a, `data` = pełny wpis (name, label?, color?, …dowolne).
46
+ */
47
+ export interface CatalogEntry {
48
+ kind: string;
49
+ name: string;
50
+ data: Record<string, unknown>;
51
+ }
52
+
53
+ /** Lokalizacja źródła deklaracji elementu (z `getSourceLocation()`). */
54
+ export interface SourceLocation {
55
+ file: string;
56
+ line?: number;
57
+ }
58
+
59
+ /** Kontekst (przestrzeń) w drzewie hierarchii — do zwijania na diagramach. */
60
+ export interface MapContextInfo {
61
+ /** Stabilne id kontekstu (core `ArcContext.id`). */
62
+ id: string;
63
+ /** Ścieżka id od korzenia do tego kontekstu (włącznie). */
64
+ path: string[];
65
+ name?: string;
66
+ title?: string;
67
+ description?: string;
68
+ /** Czy kontekst deklaruje komponent-wizytówkę (sam komponent nie przechodzi JSON-em). */
69
+ hasCard?: boolean;
70
+ }
71
+
41
72
  /** A single context element node. */
42
73
  export interface MapElement {
43
74
  /** Element name — unique within the runtime context. */
@@ -46,6 +77,10 @@ export interface MapElement {
46
77
  kind: ElementKind;
47
78
  /** Space (context) this element belongs to. */
48
79
  spaceId: string;
80
+ /** Ścieżka kontekstów-przodków (core `__contextPath`), gdy element ją ma. */
81
+ contextPath?: string[];
82
+ /** Plik+linia deklaracji elementu (core `getSourceLocation()`), gdy znane. */
83
+ source?: SourceLocation;
49
84
  /** Owning `module()` id/name, when the element was attached to a module. */
50
85
  moduleId?: string;
51
86
  moduleName?: string;
@@ -122,6 +157,10 @@ export interface MapJson {
122
157
  edges: MapEdge[];
123
158
  components: MapComponent[];
124
159
  componentEdges: MapComponentEdge[];
160
+ /** Drzewo kontekstów (hierarchia z core) — do zwijania na diagramach. */
161
+ contexts?: MapContextInfo[];
162
+ /** Generyczny katalog deklaracji (arc.catalog.yml) — persony, metryki, …. */
163
+ catalog?: CatalogEntry[];
125
164
  /** ISO timestamp stamped by the server when the payload was built. */
126
165
  generatedAt?: string;
127
166
  }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { CATALOG_FILE, loadCatalog, parseCatalogYaml } from "../src/catalog";
6
+
7
+ describe("parseCatalogYaml", () => {
8
+ test("klucze najwyższego poziomu = rodzaje; wpisy wymagają name", () => {
9
+ const entries = parseCatalogYaml(
10
+ [
11
+ "personas:",
12
+ " - name: prospekt",
13
+ " label: Prospekt",
14
+ ' color: "#0e7490"',
15
+ " - label: bez-name (pomijany)",
16
+ "metrics:",
17
+ " - name: leads.submitted",
18
+ " label: Zgłoszone leady",
19
+ "nieLista: 42",
20
+ ].join("\n"),
21
+ );
22
+ expect(entries).toEqual([
23
+ {
24
+ kind: "personas",
25
+ name: "prospekt",
26
+ data: { name: "prospekt", label: "Prospekt", color: "#0e7490" },
27
+ },
28
+ {
29
+ kind: "metrics",
30
+ name: "leads.submitted",
31
+ data: { name: "leads.submitted", label: "Zgłoszone leady" },
32
+ },
33
+ ]);
34
+ });
35
+
36
+ test("pusty / nie-obiektowy dokument → pusty katalog", () => {
37
+ expect(parseCatalogYaml("")).toEqual([]);
38
+ expect(parseCatalogYaml("- lista")).toEqual([]);
39
+ });
40
+ });
41
+
42
+ describe("loadCatalog", () => {
43
+ test("scala root + pakiety z pierwszeństwem pakietu", () => {
44
+ const root = mkdtempSync(join(tmpdir(), "arc-catalog-"));
45
+ const pkgA = join(root, "packages", "a");
46
+ mkdirSync(pkgA, { recursive: true });
47
+ writeFileSync(
48
+ join(root, CATALOG_FILE),
49
+ "personas:\n - name: prospekt\n label: Root\n - name: admin\n label: Admin\n",
50
+ );
51
+ writeFileSync(
52
+ join(pkgA, CATALOG_FILE),
53
+ "personas:\n - name: prospekt\n label: Pakiet\nmetrics:\n - name: m1\n",
54
+ );
55
+
56
+ const entries = loadCatalog(root, [pkgA]);
57
+ const byKey = new Map(entries.map((e) => [`${e.kind} ${e.name}`, e]));
58
+ expect(byKey.get("personas prospekt")!.data.label).toBe("Pakiet"); // pakiet wygrywa
59
+ expect(byKey.get("personas admin")!.data.label).toBe("Admin");
60
+ expect(byKey.has("metrics m1")).toBe(true);
61
+ });
62
+
63
+ test("brak plików = pusty katalog", () => {
64
+ const root = mkdtempSync(join(tmpdir(), "arc-catalog-empty-"));
65
+ expect(loadCatalog(root, [join(root, "nope")])).toEqual([]);
66
+ });
67
+ });
package/src/app/arc.tsx DELETED
@@ -1,92 +0,0 @@
1
- /**
2
- * `<Arc>` — inline reference to an Arc element inside use-case MDX prose.
3
- *
4
- * Renders a clickable chip and registers its resolved reference (in document
5
- * order) so the scoped diagram knows which elements to show. Validates against
6
- * the loaded map data: an unresolved reference gets a red border.
7
- */
8
-
9
- import { useId, type ReactNode } from "react";
10
- import { resolveRef, type ArcRefProps } from "../refs";
11
- import { ELEMENT_COLOR } from "./theme";
12
- import { useArcRegister, useFocus, useMapData, useStructure } from "./contexts";
13
-
14
- type ArcProps = ArcRefProps & { children?: ReactNode };
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 any))
21
- return textOf((node as any).props?.children);
22
- return "";
23
- }
24
-
25
- export function Arc(props: ArcProps) {
26
- const { children, ...refProps } = props;
27
- const id = useId();
28
- const register = useArcRegister();
29
- const map = useMapData();
30
- const focus = useFocus();
31
- const { path } = useStructure();
32
-
33
- const ref = resolveRef(refProps as ArcRefProps);
34
- const label = textOf(children);
35
-
36
- // Register during render — idempotent (keyed by id), survives StrictMode.
37
- register?.({ id, ref, label, props: refProps as ArcRefProps, path });
38
-
39
- const resolved = !!ref && !!map && map.elements.some((e) => e.id === ref.nodeId);
40
- const active = !!ref && focus.focusNodeId === ref.nodeId;
41
- const color = resolved && ref ? mapColor(ref, map!) : "#e11d48";
42
-
43
- return (
44
- <span
45
- data-arc-id={id}
46
- data-arc-key={ref?.key ?? ""}
47
- data-arc-node={ref?.nodeId ?? ""}
48
- onMouseEnter={() => ref && resolved && focus.setFocusNodeId(ref.nodeId)}
49
- onMouseLeave={() => focus.setFocusNodeId(null)}
50
- title={
51
- resolved
52
- ? ref?.method
53
- ? `${ref.nodeId}.${ref.method.name} (${ref.method.kind})`
54
- : ref?.nodeId
55
- : `Nieznana referencja: ${ref?.nodeId ?? JSON.stringify(refProps)}`
56
- }
57
- style={{
58
- display: "inline-flex",
59
- alignItems: "center",
60
- gap: 4,
61
- padding: "1px 8px",
62
- margin: "0 1px",
63
- borderRadius: 8,
64
- border: `1.5px solid ${resolved ? color : "#e11d48"}`,
65
- background: resolved ? tint(color, 0.1) : "rgba(225,29,72,0.08)",
66
- color: "#2a173f",
67
- fontWeight: 600,
68
- fontSize: "0.92em",
69
- cursor: "pointer",
70
- boxShadow: active ? `0 0 0 2px ${color}` : "none",
71
- whiteSpace: "nowrap",
72
- }}
73
- >
74
- <span style={{ width: 7, height: 7, borderRadius: 2, background: resolved ? color : "#e11d48" }} />
75
- {label || ref?.nodeId || "?"}
76
- {ref?.method ? <span style={{ color: "#6b5b86", fontWeight: 500 }}>·{ref.method.name}</span> : null}
77
- </span>
78
- );
79
- }
80
-
81
- function mapColor(ref: ReturnType<typeof resolveRef>, map: NonNullable<ReturnType<typeof useMapData>>): string {
82
- const node = ref ? map.elements.find((e) => e.id === ref.nodeId) : undefined;
83
- return (node && ELEMENT_COLOR[node.kind]) || "#7c3aed";
84
- }
85
-
86
- function tint(hex: string, alpha: number): string {
87
- const h = hex.replace("#", "");
88
- const r = parseInt(h.slice(0, 2), 16);
89
- const g = parseInt(h.slice(2, 4), 16);
90
- const b = parseInt(h.slice(4, 6), 16);
91
- return `rgba(${r},${g},${b},${alpha})`;
92
- }
@@ -1,72 +0,0 @@
1
- /** Shared React contexts for the use-case view: map data, focus, ref collection. */
2
-
3
- import { createContext, useContext } from "react";
4
- import type { MapJson } from "../types";
5
- import type { ResolvedRef, ArcRefProps } from "../refs";
6
-
7
- /** The full graph data (from /api/map), used for validation + subgraph. */
8
- export const MapDataContext = createContext<MapJson | null>(null);
9
- export const useMapData = () => useContext(MapDataContext);
10
-
11
- /**
12
- * Bidirectional highlight between `<Arc>` chips and diagram nodes. `focusNodeId`
13
- * is the element id currently hovered (in prose or graph); `scrollKey` is the
14
- * ref key a node click wants the prose to scroll to.
15
- */
16
- export interface FocusState {
17
- focusNodeId: string | null;
18
- setFocusNodeId: (id: string | null) => void;
19
- scrollToKey: string | null;
20
- setScrollToKey: (key: string | null) => void;
21
- }
22
- export const FocusContext = createContext<FocusState>({
23
- focusNodeId: null,
24
- setFocusNodeId: () => {},
25
- scrollToKey: null,
26
- setScrollToKey: () => {},
27
- });
28
- export const useFocus = () => useContext(FocusContext);
29
-
30
- /** A structural wrapper segment on the path from MDX root to an `<Arc>`. */
31
- export interface PathSeg {
32
- type: "parallel" | "branch" | "lane" | "step";
33
- id: string;
34
- /** Lane index of this child within a parallel/branch. */
35
- laneIndex?: number;
36
- title?: string;
37
- /** Branch lanes are alternatives (XOR). */
38
- alt?: boolean;
39
- }
40
-
41
- /** Structural context: the wrapper path the current subtree sits in. */
42
- export const StructureContext = createContext<{ path: PathSeg[] }>({ path: [] });
43
- export const useStructure = () => useContext(StructureContext);
44
-
45
- /** One collected `<Arc>` reference (in document order). */
46
- export interface ArcRegEntry {
47
- /** Unique render-instance id (also the chip's DOM `data-arc-id`). */
48
- id: string;
49
- ref: ResolvedRef | null;
50
- label: string;
51
- props: ArcRefProps;
52
- /** Structural path (parallel/branch/lane/step) the ref is nested in. */
53
- path: PathSeg[];
54
- }
55
-
56
- /** Registration channel filled by `<Arc>` during render. */
57
- export const ArcRegisterContext = createContext<
58
- ((entry: ArcRegEntry) => void) | null
59
- >(null);
60
- export const useArcRegister = () => useContext(ArcRegisterContext);
61
-
62
- /** Badge expand/collapse state for the use-case diagram (per step occurrence). */
63
- export interface ExpandState {
64
- /** occId -> expanded relation kinds. */
65
- expanded: Record<string, string[]>;
66
- toggle: (occId: string, relKind: string) => void;
67
- }
68
- export const ExpandContext = createContext<ExpandState>({
69
- expanded: {},
70
- toggle: () => {},
71
- });
72
- export const useExpand = () => useContext(ExpandContext);
@@ -1,74 +0,0 @@
1
- /**
2
- * Renders a use-case MDX string in the browser. Compiles via `@mdx-js/mdx`
3
- * `evaluate` with the injected `<Arc>` component, wrapped in ArcRefsProvider so
4
- * the referenced elements are collected (in document order) for the diagram.
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 } from "react";
11
- // eslint-disable-next-line import/no-unresolved
12
- import * as runtime from "react/jsx-runtime";
13
- import { Arc } from "./arc";
14
- import { Branch, Lane, Parallel, Step } from "./structure";
15
- import { ArcRefsProvider } from "./ref-context";
16
- import type { ArcRegEntry } from "./contexts";
17
- import { GLASS } from "./theme";
18
-
19
- const mdxComponents = {
20
- Arc,
21
- Parallel,
22
- Branch,
23
- Lane,
24
- Step,
25
- h1: (p: any) => <h1 style={{ fontSize: 24, color: GLASS.text, margin: "4px 0 12px" }} {...p} />,
26
- h2: (p: any) => <h2 style={{ fontSize: 18, color: GLASS.text, margin: "22px 0 8px" }} {...p} />,
27
- h3: (p: any) => <h3 style={{ fontSize: 15, color: GLASS.text, margin: "16px 0 6px" }} {...p} />,
28
- p: (p: any) => <p style={{ fontSize: 14, lineHeight: 1.7, color: "#3b2a55", margin: "8px 0" }} {...p} />,
29
- li: (p: any) => <li style={{ fontSize: 14, lineHeight: 1.7, color: "#3b2a55" }} {...p} />,
30
- code: (p: any) => (
31
- <code style={{ background: "rgba(124,58,237,0.08)", padding: "1px 5px", borderRadius: 5, fontSize: "0.9em" }} {...p} />
32
- ),
33
- a: (p: any) => <a style={{ color: "#7c3aed" }} {...p} />,
34
- };
35
-
36
- export function MdxView({ mdx, onRefs }: { mdx: string; onRefs: (refs: ArcRegEntry[]) => void }) {
37
- const [Comp, setComp] = useState<ComponentType<any> | null>(null);
38
- const [error, setError] = useState<string | null>(null);
39
-
40
- useEffect(() => {
41
- let cancelled = false;
42
- setComp(null);
43
- setError(null);
44
- evaluate(mdx, {
45
- ...(runtime as any),
46
- remarkPlugins: [remarkFrontmatter, remarkGfm],
47
- })
48
- .then((mod) => {
49
- if (!cancelled) setComp(() => mod.default as ComponentType<any>);
50
- })
51
- .catch((e) => {
52
- if (!cancelled) setError(String(e));
53
- });
54
- return () => {
55
- cancelled = true;
56
- };
57
- }, [mdx]);
58
-
59
- if (error) {
60
- return (
61
- <pre style={{ color: "#be123c", fontSize: 12, whiteSpace: "pre-wrap" }}>
62
- Błąd renderu MDX:{"\n"}
63
- {error}
64
- </pre>
65
- );
66
- }
67
- if (!Comp) return <div style={{ color: GLASS.textMuted }}>Renderuję dokument…</div>;
68
-
69
- return (
70
- <ArcRefsProvider onRefs={onRefs}>
71
- <Comp components={mdxComponents} />
72
- </ArcRefsProvider>
73
- );
74
- }
@@ -1,49 +0,0 @@
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
- }
@@ -1,95 +0,0 @@
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
- }