@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.
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { defineProfile } from "../model/profile";
14
14
  import type {
15
+ CatalogEntry,
15
16
  ContextInfo,
16
17
  MethodRef,
17
18
  ProcessGraph,
@@ -32,6 +33,7 @@ export const ARC_ELEMENT_COLOR: Record<string, string> = {
32
33
  listener: "#7048e8", // grape/lilac — policy / reaction
33
34
  route: "#e64980", // pink — external boundary
34
35
  page: "#4263eb", // indigo — UI / page
36
+ tool: "#0891b2", // cyan — AI tool (wołany przez LLM)
35
37
  context: "#7c3aed", // violet — zwinięty kontekst
36
38
  note: "#eab308", // yellow — notatka
37
39
  unknown: "#868e96", // slate
@@ -59,6 +61,8 @@ export interface ArcRefProps {
59
61
  staticView?: string;
60
62
  listener?: string;
61
63
  route?: string;
64
+ /** AI tool (element tool() z arc-ai, np. tool czatu). */
65
+ tool?: string;
62
66
  aggregate?: string;
63
67
  /** Z `aggregate`: metoda mutująca. */
64
68
  mutationMethod?: string;
@@ -107,6 +111,9 @@ export function resolveArcRef(props: ArcRefProps): ResolvedRef | null {
107
111
  if (props.route) {
108
112
  return { nodeId: props.route, kind: "route", key: `route:${props.route}` };
109
113
  }
114
+ if (props.tool) {
115
+ return { nodeId: props.tool, kind: "tool", key: `tool:${props.tool}` };
116
+ }
110
117
  if (props.page) {
111
118
  const path = normalizePagePath(props.page);
112
119
  return { nodeId: path, kind: "page", key: `page:${path}` };
@@ -192,6 +199,7 @@ export interface ArcGraphInput {
192
199
  components?: ArcGraphComponent[];
193
200
  componentEdges?: ArcGraphComponentEdge[];
194
201
  contexts?: ContextInfo[];
202
+ catalog?: CatalogEntry[];
195
203
  }
196
204
 
197
205
  /** Przekształca dane mapy Arka na generyczny graf procesu. */
@@ -222,7 +230,12 @@ export function toProcessGraph(input: ArcGraphInput): ProcessGraph {
222
230
  kind: e.usage === "query" ? "uiQuery" : "uiMutation",
223
231
  })),
224
232
  ];
225
- return { nodes, relations, contexts: input.contexts ?? [] };
233
+ return {
234
+ nodes,
235
+ relations,
236
+ contexts: input.contexts ?? [],
237
+ catalog: input.catalog ?? [],
238
+ };
226
239
  }
227
240
 
228
241
  // ---------------------------------------------------------------------------
@@ -259,6 +272,7 @@ export const arcProfile = defineProfile({
259
272
  listener: { color: ARC_ELEMENT_COLOR.listener, label: "listener" },
260
273
  route: { color: ARC_ELEMENT_COLOR.route, label: "route" },
261
274
  page: { color: ARC_ELEMENT_COLOR.page, label: "page" },
275
+ tool: { color: ARC_ELEMENT_COLOR.tool, label: "tool" },
262
276
  context: { color: ARC_ELEMENT_COLOR.context, label: "context" },
263
277
  note: { color: ARC_ELEMENT_COLOR.note, label: "note" },
264
278
  unknown: { color: ARC_ELEMENT_COLOR.unknown },
@@ -327,6 +341,20 @@ export const arcProfile = defineProfile({
327
341
  label: "używany w UI",
328
342
  },
329
343
  },
344
+ pinKinds: {
345
+ persona: {
346
+ icon: "👤",
347
+ color: "#0e7490",
348
+ label: "persona",
349
+ catalogKind: "personas",
350
+ },
351
+ metric: {
352
+ icon: "📈",
353
+ color: "#9333ea",
354
+ label: "metryka",
355
+ catalogKind: "metrics",
356
+ },
357
+ },
330
358
  resolveRef: (props) => resolveArcRef(props as ArcRefProps),
331
359
  forwardDeps: arcForwardDeps,
332
360
  });
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Drawer szczegółów kroku — overlay z prawej NAD diagramem.
3
+ *
4
+ * Otwierany klikiem w nod lub chip prozy; zamykany Esc/×/klik w tło.
5
+ * Sekcje: nagłówek (rodzaj, status istnienia), Przypięcia (dane z katalogu),
6
+ * Wystąpienie (etykieta, faza, tor, notatki), Element (opis/schema/metody/
7
+ * relacje z grafu) i OTWARTY kod źródłowy (przez `sourceProvider`,
8
+ * kolorowany Shiki ładowanym leniwie — pierwsze otwarcie kodu, nie start).
9
+ */
10
+
11
+ import {
12
+ useEffect,
13
+ useMemo,
14
+ useState,
15
+ type CSSProperties,
16
+ type ReactNode,
17
+ } from "react";
18
+ import { kindColor, type ProcessProfile } from "../model/profile";
19
+ import type { ProcessNode, ProcessStep } from "../model/types";
20
+ import { GLASS, tint } from "./theme";
21
+
22
+ /** Dostawca kodu źródłowego dla noda (np. fetch /api/source w arc-map). */
23
+ export type SourceProvider = (
24
+ node: ProcessNode,
25
+ ) => Promise<{ file: string; line?: number; code: string } | null>;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Shiki — leniwy singleton (moduł ładowany dopiero przy pierwszym kodzie)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ let shikiPromise: Promise<
32
+ ((code: string) => string) | null
33
+ > | null = null;
34
+
35
+ function getHighlighter() {
36
+ if (!shikiPromise) {
37
+ shikiPromise = import("shiki")
38
+ .then(async (shiki) => {
39
+ const highlighter = await shiki.createHighlighter({
40
+ themes: ["github-light"],
41
+ langs: ["typescript", "tsx"],
42
+ });
43
+ return (code: string) =>
44
+ highlighter.codeToHtml(code, { lang: "tsx", theme: "github-light" });
45
+ })
46
+ .catch(() => null);
47
+ }
48
+ return shikiPromise;
49
+ }
50
+
51
+ function CodeBlock({ code }: { code: string }) {
52
+ const [html, setHtml] = useState<string | null>(null);
53
+ useEffect(() => {
54
+ let cancelled = false;
55
+ getHighlighter().then((highlight) => {
56
+ if (!cancelled && highlight) setHtml(highlight(code));
57
+ });
58
+ return () => {
59
+ cancelled = true;
60
+ };
61
+ }, [code]);
62
+
63
+ const base: CSSProperties = {
64
+ margin: 0,
65
+ padding: "10px 12px",
66
+ borderRadius: 10,
67
+ border: `1px solid ${GLASS.border}`,
68
+ background: "rgba(255,255,255,0.75)",
69
+ fontSize: 11,
70
+ lineHeight: 1.55,
71
+ overflow: "auto",
72
+ maxHeight: 420,
73
+ };
74
+ if (html) {
75
+ return (
76
+ <div
77
+ style={base}
78
+ // Shiki generuje zaufany HTML z naszego lokalnego kodu źródłowego.
79
+ dangerouslySetInnerHTML={{ __html: html }}
80
+ />
81
+ );
82
+ }
83
+ return (
84
+ <pre style={{ ...base, fontFamily: "ui-monospace, monospace" }}>{code}</pre>
85
+ );
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Sekcje (wzorzec detail-panel z arc-map)
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function Section({ title, children }: { title: string; children: ReactNode }) {
93
+ return (
94
+ <div style={{ marginBottom: 14 }}>
95
+ <div
96
+ style={{
97
+ fontSize: 10.5,
98
+ textTransform: "uppercase",
99
+ letterSpacing: 0.6,
100
+ fontWeight: 700,
101
+ color: GLASS.textFaint,
102
+ marginBottom: 5,
103
+ }}
104
+ >
105
+ {title}
106
+ </div>
107
+ {children}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ function Json({ value }: { value: unknown }) {
113
+ if (value === undefined || value === null) {
114
+ return <em style={{ color: GLASS.textFaint }}>—</em>;
115
+ }
116
+ return (
117
+ <pre
118
+ style={{
119
+ margin: 0,
120
+ padding: "8px 10px",
121
+ borderRadius: 8,
122
+ border: `1px solid ${GLASS.border}`,
123
+ background: "rgba(255,255,255,0.6)",
124
+ fontSize: 10.5,
125
+ maxHeight: 220,
126
+ overflow: "auto",
127
+ }}
128
+ >
129
+ {JSON.stringify(value, null, 2)}
130
+ </pre>
131
+ );
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Drawer
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export interface ProcessDrawerProps {
139
+ step: ProcessStep;
140
+ profile: ProcessProfile;
141
+ onClose: () => void;
142
+ sourceProvider?: SourceProvider;
143
+ }
144
+
145
+ export function ProcessDrawer({
146
+ step,
147
+ profile,
148
+ onClose,
149
+ sourceProvider,
150
+ }: ProcessDrawerProps) {
151
+ const [source, setSource] = useState<
152
+ { file: string; line?: number; code: string } | null | "loading"
153
+ >(sourceProvider && step.node ? "loading" : null);
154
+
155
+ useEffect(() => {
156
+ if (!sourceProvider || !step.node) {
157
+ setSource(null);
158
+ return;
159
+ }
160
+ let cancelled = false;
161
+ setSource("loading");
162
+ sourceProvider(step.node)
163
+ .then((s) => {
164
+ if (!cancelled) setSource(s);
165
+ })
166
+ .catch(() => {
167
+ if (!cancelled) setSource(null);
168
+ });
169
+ return () => {
170
+ cancelled = true;
171
+ };
172
+ }, [sourceProvider, step.node]);
173
+
174
+ useEffect(() => {
175
+ const onKey = (e: KeyboardEvent) => {
176
+ if (e.key === "Escape") onClose();
177
+ };
178
+ window.addEventListener("keydown", onKey);
179
+ return () => window.removeEventListener("keydown", onKey);
180
+ }, [onClose]);
181
+
182
+ const kind = step.node?.kind ?? step.ref?.kind ?? "unknown";
183
+ const color = kindColor(profile, kind);
184
+ const meta = (step.node?.meta ?? {}) as Record<string, unknown>;
185
+
186
+ const metaSections = useMemo(
187
+ () =>
188
+ (
189
+ [
190
+ ["Opis", meta.description],
191
+ ["Schema", meta.schema],
192
+ ["Parametry", meta.params],
193
+ ["Payload", meta.payload],
194
+ ["Metody", meta.methods],
195
+ ["Protekcje", meta.protections],
196
+ ] as [string, unknown][]
197
+ ).filter(([, v]) => v !== undefined && v !== null),
198
+ [meta],
199
+ );
200
+
201
+ return (
202
+ <>
203
+ {/* Tło — klik zamyka. */}
204
+ <div
205
+ onClick={onClose}
206
+ style={{ position: "absolute", inset: 0, zIndex: 20 }}
207
+ />
208
+ <div
209
+ style={{
210
+ position: "absolute",
211
+ top: 12,
212
+ right: 12,
213
+ bottom: 12,
214
+ width: 480,
215
+ maxWidth: "85%",
216
+ zIndex: 21,
217
+ borderRadius: 16,
218
+ border: `1px solid ${GLASS.border}`,
219
+ background: GLASS.cardStrong,
220
+ backdropFilter: GLASS.blur,
221
+ boxShadow: GLASS.shadowStrong,
222
+ display: "flex",
223
+ flexDirection: "column",
224
+ overflow: "hidden",
225
+ }}
226
+ >
227
+ {/* Nagłówek */}
228
+ <div
229
+ style={{
230
+ padding: "12px 16px",
231
+ borderBottom: `1px solid ${GLASS.border}`,
232
+ display: "flex",
233
+ alignItems: "center",
234
+ gap: 8,
235
+ }}
236
+ >
237
+ <span
238
+ style={{
239
+ width: 10,
240
+ height: 10,
241
+ borderRadius: 3,
242
+ background: color,
243
+ boxShadow: `0 0 0 3px ${tint(color, 0.18)}`,
244
+ }}
245
+ />
246
+ <div style={{ minWidth: 0 }}>
247
+ <div
248
+ style={{
249
+ fontWeight: 800,
250
+ fontSize: 14,
251
+ color: GLASS.text,
252
+ overflow: "hidden",
253
+ textOverflow: "ellipsis",
254
+ whiteSpace: "nowrap",
255
+ }}
256
+ >
257
+ {step.node?.label ?? step.ref?.nodeId ?? step.label}
258
+ </div>
259
+ <div style={{ fontSize: 11, color: GLASS.textMuted }}>
260
+ {kind}
261
+ {step.method ? ` · ${step.method.name} (${step.method.kind})` : ""}
262
+ {!step.exists ? (
263
+ <span style={{ color, fontWeight: 700 }}> · niezaimplementowany</span>
264
+ ) : null}
265
+ </div>
266
+ </div>
267
+ <button
268
+ onClick={onClose}
269
+ style={{
270
+ marginLeft: "auto",
271
+ border: "none",
272
+ background: "transparent",
273
+ cursor: "pointer",
274
+ fontSize: 16,
275
+ color: GLASS.textMuted,
276
+ }}
277
+ >
278
+ ×
279
+ </button>
280
+ </div>
281
+
282
+ {/* Treść */}
283
+ <div style={{ padding: 16, overflow: "auto", flex: 1, minHeight: 0 }}>
284
+ {step.pins.length ? (
285
+ <Section title="Przypięcia">
286
+ {step.pins.map((pin) => {
287
+ const spec = profile.pinKinds?.[pin.kind];
288
+ const pinColor =
289
+ (typeof pin.data?.color === "string"
290
+ ? pin.data.color
291
+ : undefined) ?? spec?.color ?? "#64748b";
292
+ return (
293
+ <div
294
+ key={`${pin.kind}:${pin.name}`}
295
+ style={{
296
+ display: "flex",
297
+ alignItems: "baseline",
298
+ gap: 6,
299
+ fontSize: 12,
300
+ color: GLASS.text,
301
+ marginBottom: 4,
302
+ }}
303
+ >
304
+ <span style={{ color: pinColor }}>{spec?.icon ?? "◦"}</span>
305
+ <b>{pin.label}</b>
306
+ <span style={{ color: GLASS.textFaint, fontSize: 10.5 }}>
307
+ {spec?.label ?? pin.kind} · {pin.name}
308
+ {pin.resolved ? "" : " · brak w katalogu"}
309
+ </span>
310
+ {typeof pin.data?.description === "string" ? (
311
+ <span style={{ color: GLASS.textMuted, fontSize: 11 }}>
312
+ — {pin.data.description}
313
+ </span>
314
+ ) : null}
315
+ </div>
316
+ );
317
+ })}
318
+ </Section>
319
+ ) : null}
320
+
321
+ <Section title="Wystąpienie">
322
+ <div style={{ fontSize: 12, color: GLASS.text }}>
323
+ <div>
324
+ #{step.seq} — <b>{step.label || "(bez etykiety)"}</b>
325
+ </div>
326
+ {step.stepTitle ? <div>Faza: {step.stepTitle}</div> : null}
327
+ {step.entryTitle ? <div>Wejście: {step.entryTitle}</div> : null}
328
+ {step.laneTitle ? <div>Ścieżka: {step.laneTitle}</div> : null}
329
+ {step.notes.map((n) => (
330
+ <div
331
+ key={n.occId}
332
+ style={{
333
+ marginTop: 4,
334
+ padding: "4px 8px",
335
+ borderRadius: 6,
336
+ background: "rgba(234,179,8,0.10)",
337
+ borderLeft: "3px solid #eab308",
338
+ fontSize: 11.5,
339
+ color: GLASS.textMuted,
340
+ }}
341
+ >
342
+ {n.title ? <b>{n.title}: </b> : "📝 "}
343
+ {n.text}
344
+ </div>
345
+ ))}
346
+ </div>
347
+ </Section>
348
+
349
+ {metaSections.map(([title, value]) => (
350
+ <Section key={title} title={title}>
351
+ {typeof value === "string" ? (
352
+ <div style={{ fontSize: 12, color: GLASS.text }}>{value}</div>
353
+ ) : (
354
+ <Json value={value} />
355
+ )}
356
+ </Section>
357
+ ))}
358
+
359
+ {source === "loading" ? (
360
+ <Section title="Kod">
361
+ <div style={{ color: GLASS.textMuted, fontSize: 12 }}>Ładuję…</div>
362
+ </Section>
363
+ ) : source ? (
364
+ <Section
365
+ title={`Kod — ${source.file}${source.line ? `:${source.line}` : ""}`}
366
+ >
367
+ <CodeBlock code={source.code} />
368
+ </Section>
369
+ ) : null}
370
+ </div>
371
+ </div>
372
+ </>
373
+ );
374
+ }
@@ -3,7 +3,9 @@
3
3
  */
4
4
 
5
5
  export { ProcessDiagram, type ProcessDiagramProps } from "./process-diagram";
6
+ export { ProcessDrawer, type ProcessDrawerProps, type SourceProvider } from "./drawer";
6
7
  export { makeNodeTypes } from "./nodes";
8
+ export { Pin, Person, Metric, type PinProps } from "../mdx/pin";
7
9
  export {
8
10
  cardFor,
9
11
  contextCard,
@@ -28,15 +30,18 @@ export {
28
30
  FocusContext,
29
31
  ProcessDataContext,
30
32
  RegisterContext,
33
+ SelectionContext,
31
34
  StructureContext,
32
35
  ViewStateContext,
33
36
  useFocus,
34
37
  useProcessData,
35
38
  useRegister,
39
+ useSelection,
36
40
  useStructure,
37
41
  useViewState,
38
42
  type FocusState,
39
43
  type ProcessData,
40
44
  type RegEntry,
45
+ type SelectionState,
41
46
  type ViewStateActions,
42
47
  } from "../mdx/contexts";
@@ -7,10 +7,15 @@
7
7
 
8
8
  import { Handle, Position, type NodeProps } from "@xyflow/react";
9
9
  import { kindColor, type ProcessProfile } from "../model/profile";
10
- import type { ContextInfo, ProcessNode, ProcessStep } from "../model/types";
10
+ import type {
11
+ ContextInfo,
12
+ ProcessNode,
13
+ ProcessStep,
14
+ ResolvedPin,
15
+ } from "../model/types";
11
16
  import { useViewState } from "../mdx/contexts";
12
17
  import { cardFor, DefaultContextCard } from "./context-card";
13
- import { GLASS, MISSING_COLOR, tint } from "./theme";
18
+ import { GLASS, tint } from "./theme";
14
19
 
15
20
  const KIND_ICON: Record<string, string> = {
16
21
  aggregate: "◆",
@@ -21,6 +26,7 @@ const KIND_ICON: Record<string, string> = {
21
26
  listener: "♪",
22
27
  route: "⇄",
23
28
  page: "▢",
29
+ tool: "⚙",
24
30
  context: "▣",
25
31
  note: "✎",
26
32
  unknown: "•",
@@ -36,6 +42,50 @@ function hiddenHandles() {
36
42
  );
37
43
  }
38
44
 
45
+ /** Badge'e przypięć (persony/metryki/…) — kolor z katalogu lub rodzaju pinu. */
46
+ function PinBadges({
47
+ profile,
48
+ pins,
49
+ }: {
50
+ profile: ProcessProfile;
51
+ pins: ResolvedPin[];
52
+ }) {
53
+ if (!pins.length) return null;
54
+ return (
55
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 3, marginTop: 2 }}>
56
+ {pins.map((pin) => {
57
+ const spec = profile.pinKinds?.[pin.kind];
58
+ const color =
59
+ (typeof pin.data?.color === "string" ? pin.data.color : undefined) ??
60
+ spec?.color ??
61
+ "#64748b";
62
+ return (
63
+ <span
64
+ key={`${pin.kind}:${pin.name}`}
65
+ title={`${spec?.label ?? pin.kind}: ${pin.name}${
66
+ pin.resolved ? "" : " (brak w katalogu)"
67
+ }`}
68
+ style={{
69
+ fontSize: 9,
70
+ fontWeight: 700,
71
+ color: GLASS.textMuted,
72
+ background: tint(color, 0.1),
73
+ border: `1px ${pin.resolved ? "solid" : "dashed"} ${tint(color, 0.65)}`,
74
+ borderRadius: 99,
75
+ padding: "0 6px",
76
+ lineHeight: "14px",
77
+ whiteSpace: "nowrap",
78
+ opacity: pin.resolved ? 1 : 0.8,
79
+ }}
80
+ >
81
+ {spec?.icon ?? "◦"} {pin.label}
82
+ </span>
83
+ );
84
+ })}
85
+ </div>
86
+ );
87
+ }
88
+
39
89
  /** Badge'e grup doczepień — toggle rozwinięcia (wspólne dla kroku i karty). */
40
90
  function GroupBadges({
41
91
  profile,
@@ -114,7 +164,12 @@ export function makeNodeTypes(profile: ProcessProfile) {
114
164
  );
115
165
  }
116
166
 
117
- const color = s.exists ? kindColor(profile, s.node!.kind) : MISSING_COLOR;
167
+ // Kolor z ZADEKLAROWANEGO rodzaju — element nieistniejący (TODO) wygląda
168
+ // jak każdy inny nod swojego rodzaju, tylko z kreskowaną ramką i lekko
169
+ // wyblakły (parytet z chipami w prozie).
170
+ const declaredKind = s.node?.kind ?? s.ref?.kind ?? "unknown";
171
+ const color = kindColor(profile, declaredKind);
172
+ const missing = !s.exists;
118
173
  return (
119
174
  <div
120
175
  style={{
@@ -122,8 +177,8 @@ export function makeNodeTypes(profile: ProcessProfile) {
122
177
  height: "100%",
123
178
  boxSizing: "border-box",
124
179
  borderRadius: 13,
125
- border: `1px solid ${GLASS.border}`,
126
- borderLeft: `4px solid ${color}`,
180
+ border: `1px ${missing ? "dashed" : "solid"} ${missing ? color : GLASS.border}`,
181
+ borderLeft: `4px ${missing ? "dashed" : "solid"} ${color}`,
127
182
  background: GLASS.card,
128
183
  backdropFilter: GLASS.blurSoft,
129
184
  boxShadow: GLASS.shadow,
@@ -132,6 +187,7 @@ export function makeNodeTypes(profile: ProcessProfile) {
132
187
  flexDirection: "column",
133
188
  gap: 2,
134
189
  cursor: "pointer",
190
+ opacity: missing ? 0.8 : 1,
135
191
  }}
136
192
  >
137
193
  {hiddenHandles()}
@@ -166,12 +222,11 @@ export function makeNodeTypes(profile: ProcessProfile) {
166
222
  {s.label || s.ref?.nodeId || "?"}
167
223
  </span>
168
224
  </div>
169
- <div style={{ fontSize: 10, color: s.exists ? GLASS.textMuted : MISSING_COLOR }}>
170
- {KIND_ICON[s.node?.kind ?? "unknown"] ?? "•"}{" "}
171
- {s.exists
172
- ? `${s.node!.kind}${s.method ? ` · ${s.method.name}` : ""}`
173
- : `TODO: ${s.ref?.nodeId ?? "brak referencji"}`}
225
+ <div style={{ fontSize: 10, color: GLASS.textMuted }}>
226
+ {KIND_ICON[declaredKind] ?? "•"} {declaredKind}
227
+ {s.method ? ` · ${s.method.name}` : ""}
174
228
  </div>
229
+ <PinBadges profile={profile} pins={s.pins} />
175
230
  {s.exists ? (
176
231
  <GroupBadges
177
232
  profile={profile}