@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.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@arcote.tech/arc-map",
3
+ "type": "module",
4
+ "version": "0.7.27",
5
+ "private": false,
6
+ "author": "Przemysław Krasiński [arcote.tech]",
7
+ "description": "Arc Context Map — interactive map of context spaces, elements, event-flow and React useScope usages (dev tool)",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./app": "./src/app/index.tsx"
13
+ },
14
+ "scripts": {
15
+ "type-check": "tsc --noEmit",
16
+ "test": "bun test"
17
+ },
18
+ "dependencies": {
19
+ "ts-morph": "^24.0.0",
20
+ "@xyflow/react": "^12.0.0",
21
+ "elkjs": "^0.9.3",
22
+ "@mdx-js/mdx": "^3.1.0",
23
+ "remark-frontmatter": "^5.0.0",
24
+ "remark-gfm": "^4.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@arcote.tech/arc": "^0.7.27",
28
+ "@arcote.tech/platform": "^0.7.27",
29
+ "react": "^18.0.0 || ^19.0.0",
30
+ "react-dom": "^18.0.0 || ^19.0.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.2.7",
35
+ "@types/react-dom": "^19.2.3",
36
+ "typescript": "~5.9.3"
37
+ }
38
+ }
@@ -0,0 +1,92 @@
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
+ }
@@ -0,0 +1,72 @@
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);
@@ -0,0 +1,164 @@
1
+ /** Rich detail panel shown when an element or component node is selected. */
2
+
3
+ import type { MapComponent, MapComponentEdge, MapElement, MapJson } from "../types";
4
+ import { ELEMENT_COLOR, GLASS } from "./theme";
5
+
6
+ interface Props {
7
+ map: MapJson;
8
+ selectedElement?: MapElement;
9
+ selectedComponent?: MapComponent;
10
+ onClose: () => void;
11
+ }
12
+
13
+ function Section({ title, children }: { title: string; children: React.ReactNode }) {
14
+ return (
15
+ <div style={{ marginBottom: 14 }}>
16
+ <div style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: 0.6, color: GLASS.textMuted, marginBottom: 5, fontWeight: 700 }}>
17
+ {title}
18
+ </div>
19
+ {children}
20
+ </div>
21
+ );
22
+ }
23
+
24
+ function Json({ value }: { value: unknown }) {
25
+ if (value === undefined || value === null) return <em style={{ color: GLASS.textFaint }}>—</em>;
26
+ return (
27
+ <pre
28
+ style={{
29
+ margin: 0,
30
+ fontSize: 11,
31
+ lineHeight: 1.4,
32
+ color: "#3b2a55",
33
+ background: "rgba(124,58,237,0.06)",
34
+ border: `1px solid ${GLASS.border}`,
35
+ padding: 9,
36
+ borderRadius: 8,
37
+ overflow: "auto",
38
+ maxHeight: 220,
39
+ }}
40
+ >
41
+ {JSON.stringify(value, null, 2)}
42
+ </pre>
43
+ );
44
+ }
45
+
46
+ function ElementDetail({ el, map }: { el: MapElement; map: MapJson }) {
47
+ const color = ELEMENT_COLOR[el.kind] ?? ELEMENT_COLOR.unknown;
48
+ const consumers = map.componentEdges.filter((e) => e.target === el.id);
49
+ return (
50
+ <>
51
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
52
+ <span style={{ color, fontSize: 12, textTransform: "uppercase" }}>{el.kind}</span>
53
+ {el.scope ? <span style={{ fontSize: 11, color: "#6b5b86" }}>scope: {el.scope}</span> : null}
54
+ </div>
55
+ <h2 style={{ margin: "4px 0 14px", fontSize: 18, color: "#2a173f" }}>{el.name}</h2>
56
+
57
+ {el.description ? <Section title="Description"><div style={{ fontSize: 12.5 }}>{el.description}</div></Section> : null}
58
+
59
+ <Section title="Space / Module">
60
+ <div style={{ fontSize: 12 }}>
61
+ {el.spaceId}
62
+ {el.moduleName ? <span style={{ color: "#6b5b86" }}> · {el.moduleName}</span> : null}
63
+ </div>
64
+ </Section>
65
+
66
+ {el.protections?.length ? (
67
+ <Section title="Protections">
68
+ <div style={{ fontSize: 12, color: "#b45309" }}>🔒 {el.protections.join(", ")}</div>
69
+ </Section>
70
+ ) : null}
71
+
72
+ {el.methods?.length ? (
73
+ <Section title="Methods">
74
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
75
+ {el.methods.map((m) => (
76
+ <span key={`${m.kind}:${m.name}`} style={{ fontSize: 11, padding: "2px 7px", borderRadius: 5, background: "rgba(124,58,237,0.10)", color: "#3b2a55" }}>
77
+ {m.name}<span style={{ color: "#6b5b86" }}> · {m.kind}</span>
78
+ </span>
79
+ ))}
80
+ </div>
81
+ </Section>
82
+ ) : null}
83
+
84
+ {el.events?.length ? (
85
+ <Section title="Events"><div style={{ fontSize: 12 }}>{el.events.join(", ")}</div></Section>
86
+ ) : null}
87
+
88
+ {el.path ? <Section title="Path"><code style={{ fontSize: 12 }}>{el.path}</code></Section> : null}
89
+ {el.schema !== undefined ? <Section title="Schema"><Json value={el.schema} /></Section> : null}
90
+ {el.params !== undefined ? <Section title="Params"><Json value={el.params} /></Section> : null}
91
+ {el.payload !== undefined ? <Section title="Payload"><Json value={el.payload} /></Section> : null}
92
+ {el.results !== undefined ? <Section title="Results"><Json value={el.results} /></Section> : null}
93
+
94
+ {consumers.length ? (
95
+ <Section title={`React consumers (${consumers.length})`}>
96
+ {consumers.map((c: MapComponentEdge) => (
97
+ <div key={c.id} style={{ fontSize: 11.5, marginBottom: 3, color: "#3b2a55" }}>
98
+ <span style={{ color: c.usage === "query" ? "#2563eb" : "#d97706" }}>{c.usage}</span>{" "}
99
+ {c.source}
100
+ {c.methods.length ? <span style={{ color: "#6b5b86" }}> · {c.methods.join(", ")}</span> : null}
101
+ </div>
102
+ ))}
103
+ </Section>
104
+ ) : null}
105
+ </>
106
+ );
107
+ }
108
+
109
+ function ComponentDetail({ c, map }: { c: MapComponent; map: MapJson }) {
110
+ const uses = map.componentEdges.filter((e) => e.source === c.id);
111
+ return (
112
+ <>
113
+ <div style={{ fontSize: 12, color: "#7c3aed" }}>⚛ React component</div>
114
+ <h2 style={{ margin: "4px 0 14px", fontSize: 18, color: "#2a173f" }}>{c.name ?? c.file}</h2>
115
+ <Section title="File"><code style={{ fontSize: 11.5 }}>{c.file}</code></Section>
116
+ {c.package ? <Section title="Package"><div style={{ fontSize: 12 }}>{c.package}</div></Section> : null}
117
+ <Section title={`Uses (${uses.length})`}>
118
+ {uses.map((u) => (
119
+ <div key={u.id} style={{ fontSize: 11.5, marginBottom: 3, color: "#3b2a55" }}>
120
+ <span style={{ color: u.usage === "query" ? "#2563eb" : "#d97706" }}>{u.usage}</span>{" "}
121
+ <strong>{u.target}</strong>
122
+ {u.scope ? <span style={{ color: "#6b5b86" }}> · {u.scope}</span> : null}
123
+ {u.methods.length ? <span style={{ color: "#6b5b86" }}> · {u.methods.join(", ")}</span> : null}
124
+ {u.line ? <span style={{ color: "#9b8cb5" }}> :{u.line}</span> : null}
125
+ </div>
126
+ ))}
127
+ </Section>
128
+ </>
129
+ );
130
+ }
131
+
132
+ export function DetailPanel({ map, selectedElement, selectedComponent, onClose }: Props) {
133
+ if (!selectedElement && !selectedComponent) return null;
134
+ return (
135
+ <div
136
+ style={{
137
+ position: "absolute",
138
+ top: 12,
139
+ right: 12,
140
+ bottom: 12,
141
+ width: 360,
142
+ background: GLASS.cardStrong,
143
+ backdropFilter: GLASS.blur,
144
+ WebkitBackdropFilter: GLASS.blur,
145
+ border: `1px solid ${GLASS.border}`,
146
+ borderRadius: 18,
147
+ padding: 18,
148
+ overflow: "auto",
149
+ zIndex: 10,
150
+ boxShadow: GLASS.shadowStrong,
151
+ color: GLASS.text,
152
+ }}
153
+ >
154
+ <button
155
+ onClick={onClose}
156
+ style={{ float: "right", background: "transparent", border: "none", color: GLASS.textMuted, fontSize: 18, cursor: "pointer" }}
157
+ >
158
+ ×
159
+ </button>
160
+ {selectedElement ? <ElementDetail el={selectedElement} map={map} /> : null}
161
+ {selectedComponent ? <ComponentDetail c={selectedComponent} map={map} /> : null}
162
+ </div>
163
+ );
164
+ }
@@ -0,0 +1,107 @@
1
+ /** Browser entry — fetches /api/map + /api/use-cases, renders the mode switch. */
2
+
3
+ import "@xyflow/react/dist/style.css";
4
+ import { StrictMode, useEffect, useState } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+ import type { MapJson, UseCaseEntry } from "../types";
7
+ import { MapView } from "./map-view";
8
+ import { UseCaseView } from "./usecase-view";
9
+ import { GLASS } from "./theme";
10
+
11
+ type Mode = "use-cases" | "explore";
12
+
13
+ function ModeSwitch({ mode, setMode }: { mode: Mode; setMode: (m: Mode) => void }) {
14
+ const Btn = ({ id, label }: { id: Mode; label: string }) => (
15
+ <button
16
+ onClick={() => setMode(id)}
17
+ style={{
18
+ padding: "6px 14px",
19
+ borderRadius: 9,
20
+ border: "none",
21
+ cursor: "pointer",
22
+ fontSize: 13,
23
+ fontWeight: 600,
24
+ color: mode === id ? "#fff" : GLASS.text,
25
+ background: mode === id ? "#7c3aed" : "transparent",
26
+ }}
27
+ >
28
+ {label}
29
+ </button>
30
+ );
31
+ return (
32
+ <div
33
+ style={{
34
+ display: "flex",
35
+ gap: 4,
36
+ alignItems: "center",
37
+ padding: "8px 12px",
38
+ borderBottom: `1px solid ${GLASS.border}`,
39
+ background: GLASS.card,
40
+ backdropFilter: GLASS.blur,
41
+ }}
42
+ >
43
+ <span style={{ fontWeight: 800, color: GLASS.text, marginRight: 10 }}>Arc Map</span>
44
+ <Btn id="use-cases" label="Use cases" />
45
+ <Btn id="explore" label="Explore all" />
46
+ </div>
47
+ );
48
+ }
49
+
50
+ function App() {
51
+ const [map, setMap] = useState<MapJson | null>(null);
52
+ const [useCases, setUseCases] = useState<UseCaseEntry[]>([]);
53
+ const [error, setError] = useState<string | null>(null);
54
+ const [mode, setMode] = useState<Mode>("use-cases");
55
+
56
+ const load = () => {
57
+ fetch("/api/map")
58
+ .then((r) => r.json())
59
+ .then((data) => (data?.error ? setError(data.error) : (setMap(data), setError(null))))
60
+ .catch((e) => setError(String(e)));
61
+ fetch("/api/use-cases")
62
+ .then((r) => r.json())
63
+ .then((d) => setUseCases(d.useCases ?? []))
64
+ .catch(() => setUseCases([]));
65
+ };
66
+
67
+ useEffect(() => {
68
+ load();
69
+ const es = new EventSource("/api/reload-stream");
70
+ es.onmessage = (ev) => {
71
+ if (ev.data && ev.data !== "connected") load();
72
+ };
73
+ return () => es.close();
74
+ }, []);
75
+
76
+ if (error) {
77
+ return (
78
+ <div style={{ padding: 24, color: "#be123c", fontFamily: "ui-monospace, monospace" }}>
79
+ <strong>arc-map error:</strong>
80
+ <pre>{error}</pre>
81
+ </div>
82
+ );
83
+ }
84
+ if (!map) return <div style={{ padding: 24, color: "#6b5b86" }}>Ładuję kontekst…</div>;
85
+
86
+ return (
87
+ <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
88
+ <ModeSwitch mode={mode} setMode={setMode} />
89
+ <div style={{ flex: 1, minHeight: 0 }}>
90
+ {mode === "use-cases" ? (
91
+ <UseCaseView map={map} useCases={useCases} />
92
+ ) : (
93
+ <MapView map={map} />
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ const root = document.getElementById("root");
101
+ if (root) {
102
+ createRoot(root).render(
103
+ <StrictMode>
104
+ <App />
105
+ </StrictMode>,
106
+ );
107
+ }