@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,311 @@
1
+ /**
2
+ * Renderery nodów ReactFlow diagramu procesu: krok narracji, doczepiona
3
+ * karteczka (z własnymi togglami — rekurencja), connector, notatka i box
4
+ * kontekstu (zwinięty krok-box lub otwarty obrys context-group).
5
+ * Kolory rodzajów przychodzą z profilu (przez data), szkło z theme.
6
+ */
7
+
8
+ import { Handle, Position, type NodeProps } from "@xyflow/react";
9
+ import { kindColor, type ProcessProfile } from "../model/profile";
10
+ import type { ContextInfo, ProcessNode, ProcessStep } from "../model/types";
11
+ import { useViewState } from "../mdx/contexts";
12
+ import { cardFor, DefaultContextCard } from "./context-card";
13
+ import { GLASS, MISSING_COLOR, tint } from "./theme";
14
+
15
+ const KIND_ICON: Record<string, string> = {
16
+ aggregate: "◆",
17
+ command: "▶",
18
+ event: "✶",
19
+ view: "▦",
20
+ "static-view": "▤",
21
+ listener: "♪",
22
+ route: "⇄",
23
+ page: "▢",
24
+ context: "▣",
25
+ note: "✎",
26
+ unknown: "•",
27
+ };
28
+
29
+ function hiddenHandles() {
30
+ const style = { opacity: 0, pointerEvents: "none" as const };
31
+ return (
32
+ <>
33
+ <Handle type="target" position={Position.Left} style={style} />
34
+ <Handle type="source" position={Position.Right} style={style} />
35
+ </>
36
+ );
37
+ }
38
+
39
+ /** Badge'e grup doczepień — toggle rozwinięcia (wspólne dla kroku i karty). */
40
+ function GroupBadges({
41
+ profile,
42
+ attachments,
43
+ expanded,
44
+ expandKey,
45
+ }: {
46
+ profile: ProcessProfile;
47
+ attachments: Partial<Record<string, string[]>>;
48
+ expanded: string[];
49
+ expandKey: string;
50
+ }) {
51
+ const { toggleGroup } = useViewState();
52
+ const groups = Object.entries(attachments).filter(([, ids]) => ids?.length);
53
+ if (!groups.length) return null;
54
+ return (
55
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 3, marginTop: 3 }}>
56
+ {groups.map(([group, ids]) => {
57
+ const spec = profile.attachmentGroups[group];
58
+ const open = expanded.includes(group);
59
+ const color = spec?.color ?? "#868e96";
60
+ return (
61
+ <button
62
+ key={group}
63
+ onClick={(e) => {
64
+ e.stopPropagation();
65
+ toggleGroup(expandKey, group);
66
+ }}
67
+ title={`${spec?.label ?? group}: ${ids!.join(", ")}`}
68
+ style={{
69
+ fontSize: 9,
70
+ fontWeight: 700,
71
+ color: open ? "#fff" : color,
72
+ background: open ? color : tint(color, 0.12),
73
+ border: `1px solid ${color}`,
74
+ borderRadius: 99,
75
+ padding: "0 6px",
76
+ cursor: "pointer",
77
+ lineHeight: "14px",
78
+ }}
79
+ >
80
+ {spec?.label ?? group} {ids!.length}
81
+ </button>
82
+ );
83
+ })}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ export function makeNodeTypes(profile: ProcessProfile) {
89
+ function StepNode({ data }: NodeProps) {
90
+ const d = data as unknown as {
91
+ step: ProcessStep;
92
+ expandKey: string;
93
+ expanded: string[];
94
+ isContext: boolean;
95
+ memberCount?: number;
96
+ };
97
+ const { toggleContext } = useViewState();
98
+ const s = d.step;
99
+
100
+ if (d.isContext && s.node) {
101
+ const ctx = (s.node.meta as { context?: ContextInfo })?.context;
102
+ const Card = (ctx && cardFor(ctx)) || DefaultContextCard;
103
+ return (
104
+ <div style={{ width: "100%", height: "100%" }}>
105
+ {hiddenHandles()}
106
+ <Card
107
+ context={ctx ?? { id: s.node.id, path: s.node.contextPath ?? [] }}
108
+ collapsed
109
+ memberCount={d.memberCount}
110
+ label={s.label}
111
+ onToggle={() => ctx && toggleContext(ctx.id)}
112
+ />
113
+ </div>
114
+ );
115
+ }
116
+
117
+ const color = s.exists ? kindColor(profile, s.node!.kind) : MISSING_COLOR;
118
+ return (
119
+ <div
120
+ style={{
121
+ width: "100%",
122
+ height: "100%",
123
+ boxSizing: "border-box",
124
+ borderRadius: 13,
125
+ border: `1px solid ${GLASS.border}`,
126
+ borderLeft: `4px solid ${color}`,
127
+ background: GLASS.card,
128
+ backdropFilter: GLASS.blurSoft,
129
+ boxShadow: GLASS.shadow,
130
+ padding: "8px 11px",
131
+ display: "flex",
132
+ flexDirection: "column",
133
+ gap: 2,
134
+ cursor: "pointer",
135
+ }}
136
+ >
137
+ {hiddenHandles()}
138
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
139
+ <span
140
+ style={{
141
+ fontSize: 10,
142
+ fontWeight: 800,
143
+ color: "#fff",
144
+ background: color,
145
+ borderRadius: 99,
146
+ minWidth: 16,
147
+ height: 16,
148
+ display: "inline-flex",
149
+ alignItems: "center",
150
+ justifyContent: "center",
151
+ padding: "0 4px",
152
+ }}
153
+ >
154
+ {s.seq}
155
+ </span>
156
+ <span
157
+ style={{
158
+ fontSize: 12.5,
159
+ fontWeight: 700,
160
+ color: GLASS.text,
161
+ overflow: "hidden",
162
+ textOverflow: "ellipsis",
163
+ whiteSpace: "nowrap",
164
+ }}
165
+ >
166
+ {s.label || s.ref?.nodeId || "?"}
167
+ </span>
168
+ </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"}`}
174
+ </div>
175
+ {s.exists ? (
176
+ <GroupBadges
177
+ profile={profile}
178
+ attachments={s.attachments}
179
+ expanded={d.expanded}
180
+ expandKey={d.expandKey}
181
+ />
182
+ ) : null}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ function AttachmentNode({ data }: NodeProps) {
188
+ const d = data as unknown as {
189
+ node: ProcessNode;
190
+ group: string;
191
+ expandKey: string;
192
+ expanded: string[];
193
+ attachments: Partial<Record<string, string[]>>;
194
+ };
195
+ const color = kindColor(profile, d.node.kind);
196
+ return (
197
+ <div
198
+ style={{
199
+ width: "100%",
200
+ height: "100%",
201
+ boxSizing: "border-box",
202
+ borderRadius: 10,
203
+ border: `1px solid ${tint(color, 0.5)}`,
204
+ borderLeft: `3px solid ${color}`,
205
+ background: GLASS.card,
206
+ padding: "5px 9px",
207
+ display: "flex",
208
+ flexDirection: "column",
209
+ justifyContent: "center",
210
+ gap: 1,
211
+ }}
212
+ >
213
+ {hiddenHandles()}
214
+ <div
215
+ style={{
216
+ fontSize: 11.5,
217
+ fontWeight: 600,
218
+ color: GLASS.text,
219
+ overflow: "hidden",
220
+ textOverflow: "ellipsis",
221
+ whiteSpace: "nowrap",
222
+ }}
223
+ >
224
+ {KIND_ICON[d.node.kind] ?? "•"} {d.node.label}
225
+ </div>
226
+ <GroupBadges
227
+ profile={profile}
228
+ attachments={d.attachments}
229
+ expanded={d.expanded}
230
+ expandKey={d.expandKey}
231
+ />
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function ConnectorNode({ data }: NodeProps) {
237
+ const d = data as unknown as { node: ProcessNode };
238
+ const color = kindColor(profile, d.node.kind);
239
+ return (
240
+ <div
241
+ style={{
242
+ width: "100%",
243
+ height: "100%",
244
+ boxSizing: "border-box",
245
+ borderRadius: 10,
246
+ border: `1px dashed ${tint(color, 0.7)}`,
247
+ borderLeft: `3px solid ${color}`,
248
+ background: tint(color, 0.06),
249
+ padding: "5px 9px",
250
+ display: "flex",
251
+ alignItems: "center",
252
+ opacity: 0.85,
253
+ }}
254
+ >
255
+ {hiddenHandles()}
256
+ <span style={{ fontSize: 11.5, color: GLASS.textMuted }}>
257
+ {KIND_ICON[d.node.kind] ?? "•"} {d.node.label}
258
+ </span>
259
+ </div>
260
+ );
261
+ }
262
+
263
+ function NoteNode({ data }: NodeProps) {
264
+ const d = data as unknown as { node: ProcessNode };
265
+ const title = (d.node.meta as { title?: string })?.title;
266
+ return (
267
+ <div
268
+ style={{
269
+ width: "100%",
270
+ height: "100%",
271
+ boxSizing: "border-box",
272
+ borderRadius: 8,
273
+ border: "1px solid rgba(234,179,8,0.6)",
274
+ background: "rgba(254,249,195,0.9)",
275
+ padding: "5px 9px",
276
+ fontSize: 10.5,
277
+ lineHeight: 1.35,
278
+ color: "#713f12",
279
+ overflow: "hidden",
280
+ }}
281
+ >
282
+ {hiddenHandles()}
283
+ {title ? <b>{title}: </b> : "📝 "}
284
+ {d.node.label}
285
+ </div>
286
+ );
287
+ }
288
+
289
+ function ContextGroupNode({ data }: NodeProps) {
290
+ const d = data as unknown as { context: ContextInfo };
291
+ const { toggleContext } = useViewState();
292
+ const Card = cardFor(d.context) ?? DefaultContextCard;
293
+ return (
294
+ <div style={{ width: "100%", height: "100%" }}>
295
+ <Card
296
+ context={d.context}
297
+ collapsed={false}
298
+ onToggle={() => toggleContext(d.context.id)}
299
+ />
300
+ </div>
301
+ );
302
+ }
303
+
304
+ return {
305
+ step: StepNode,
306
+ attachment: AttachmentNode,
307
+ connector: ConnectorNode,
308
+ note: NoteNode,
309
+ "context-group": ContextGroupNode,
310
+ };
311
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * `ProcessDiagram` — fetch-free viewer procesu z MDX.
3
+ *
4
+ * Proza (MDX z chipami) po lewej, diagram ReactFlow po prawej. MDX przychodzi
5
+ * propsem; `graph` jest opcjonalny — domyślnie pusty, więc każda referencja
6
+ * renderuje się jako czerwony chip TODO (design-first).
7
+ *
8
+ * Pipeline: MDX → wystąpienia (DOM-order) → buildProcessModel →
9
+ * collapseContexts (hierarchia kontekstów) → layoutProcess (8 kierunków,
10
+ * track sizing) → ReactFlow. Stan otwarć: MDX deklaruje start, czytelnik
11
+ * toggluje, persystencja przez `store` (localStorage / agregat).
12
+ */
13
+
14
+ import "@xyflow/react/dist/style.css";
15
+
16
+ import {
17
+ Background,
18
+ Controls,
19
+ MarkerType,
20
+ MiniMap,
21
+ ReactFlow,
22
+ type Edge,
23
+ type Node,
24
+ } from "@xyflow/react";
25
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
+ import { buildProcessModel } from "../model/build";
27
+ import { collapseContexts } from "../model/collapse";
28
+ import { layoutProcess, type DiagramEdge, type DiagramNode } from "../layout/layout";
29
+ import { kindColor, type ProcessProfile } from "../model/profile";
30
+ import {
31
+ EMPTY_GRAPH,
32
+ type Occurrence,
33
+ type ProcessGraph,
34
+ type ProcessStep,
35
+ } from "../model/types";
36
+ import {
37
+ initialViewState,
38
+ toggleContext as toggleContextState,
39
+ toggleGroup as toggleGroupState,
40
+ type ProcessViewState,
41
+ } from "../state/view-state";
42
+ import type { ProcessViewStore } from "../state/store";
43
+ import { MdxView, parseProcessFrontmatter } from "../mdx/mdx-view";
44
+ import {
45
+ FocusContext,
46
+ ProcessDataContext,
47
+ ViewStateContext,
48
+ } from "../mdx/contexts";
49
+ import { makeNodeTypes } from "./nodes";
50
+ import { GLASS, MISSING_COLOR } from "./theme";
51
+
52
+ export interface ProcessDiagramProps {
53
+ /** Surowy MDX procesu. */
54
+ mdx: string;
55
+ /** Profil semantyki (np. arcProfile). */
56
+ profile: ProcessProfile;
57
+ /** Graf rozstrzygający `exists` — domyślnie pusty (wszystko TODO). */
58
+ graph?: ProcessGraph;
59
+ /** Persystencja stanu widoku (localStorageStore / functionStore / agregat). */
60
+ store?: ProcessViewStore;
61
+ /** Id dokumentu dla store (fallback: title / hash mdx). */
62
+ docId?: string;
63
+ /** Opcjonalny tytuł (remount przy zmianie procesu). */
64
+ title?: string;
65
+ /**
66
+ * Tryb zwijania kontekstów, gdy frontmatter nie mówi inaczej:
67
+ * true = górny rzut (obce konteksty zwinięte). Domyślnie z frontmatter
68
+ * `collapse: all|none` (all gdy brak).
69
+ */
70
+ defaultCollapsed?: boolean;
71
+ /** Id/nazwa kontekstu domowego (fallback: frontmatter `context:`). */
72
+ homeContextId?: string;
73
+ }
74
+
75
+ function rfNode(n: DiagramNode): Node {
76
+ return {
77
+ id: n.id,
78
+ type: n.type,
79
+ position: { x: n.x, y: n.y },
80
+ width: n.w,
81
+ height: n.h,
82
+ data: n.data,
83
+ draggable: false,
84
+ selectable: n.type !== "context-group",
85
+ zIndex: n.type === "context-group" ? -1 : undefined,
86
+ };
87
+ }
88
+
89
+ function rfEdge(e: DiagramEdge): Edge {
90
+ return {
91
+ id: e.id,
92
+ source: e.source,
93
+ target: e.target,
94
+ label: e.label,
95
+ style: {
96
+ stroke: e.color,
97
+ strokeWidth: 1.5,
98
+ opacity: e.opacity ?? 0.75,
99
+ strokeDasharray: e.dashed ? "4 3" : undefined,
100
+ },
101
+ labelStyle: { fill: e.color, fontSize: 9, fontWeight: 600 },
102
+ labelBgStyle: { fill: "#fff", fillOpacity: 0.8 },
103
+ markerEnd: e.backbone
104
+ ? undefined
105
+ : { type: MarkerType.ArrowClosed, color: e.color },
106
+ };
107
+ }
108
+
109
+ export function ProcessDiagram({
110
+ mdx,
111
+ profile,
112
+ graph = EMPTY_GRAPH,
113
+ store,
114
+ docId,
115
+ title,
116
+ defaultCollapsed,
117
+ homeContextId,
118
+ }: ProcessDiagramProps) {
119
+ const [occurrences, setOccurrences] = useState<Occurrence[]>([]);
120
+ const [viewState, setViewState] = useState<ProcessViewState | null>(null);
121
+ const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
122
+ const [scrollToKey, setScrollToKey] = useState<string | null>(null);
123
+ const proseRef = useRef<HTMLDivElement>(null);
124
+
125
+ const frontmatter = useMemo(() => parseProcessFrontmatter(mdx), [mdx]);
126
+ const docKey = docId ?? title ?? frontmatter.title ?? mdx.slice(0, 48);
127
+ const collapsedByDefault =
128
+ defaultCollapsed ?? (frontmatter.collapse ?? "all") === "all";
129
+ const home = homeContextId ?? frontmatter.context;
130
+
131
+ // Reset przy zmianie dokumentu.
132
+ useEffect(() => {
133
+ setOccurrences([]);
134
+ setViewState(null);
135
+ }, [mdx]);
136
+
137
+ const model = useMemo(
138
+ () => buildProcessModel({ graph, profile, occurrences }),
139
+ [graph, profile, occurrences],
140
+ );
141
+
142
+ // Kontekst domowy: id lub nazwa z frontmatter/propsa.
143
+ const homeId = useMemo(() => {
144
+ if (!home) return undefined;
145
+ const ctxs = graph.contexts ?? [];
146
+ return (ctxs.find((c) => c.id === home) ?? ctxs.find((c) => c.name === home))?.id;
147
+ }, [graph, home]);
148
+
149
+ // Stan efektywny: jawny stan czytelnika → zapisany w store → startowy z MDX.
150
+ const effectiveState = useMemo<ProcessViewState>(() => {
151
+ if (viewState) return viewState;
152
+ const stored = store?.load(docKey);
153
+ if (stored) return stored;
154
+ return initialViewState(model);
155
+ }, [viewState, store, docKey, model]);
156
+
157
+ const updateState = useCallback(
158
+ (next: ProcessViewState) => {
159
+ setViewState(next);
160
+ store?.save(docKey, next);
161
+ },
162
+ [store, docKey],
163
+ );
164
+
165
+ const collapse = useMemo(
166
+ () =>
167
+ collapseContexts(model, {
168
+ contexts: graph.contexts ?? [],
169
+ state: effectiveState,
170
+ defaultCollapsed: collapsedByDefault,
171
+ homeContextId: homeId,
172
+ }),
173
+ [model, graph, effectiveState, collapsedByDefault, homeId],
174
+ );
175
+
176
+ const layout = useMemo(
177
+ () => layoutProcess({ model, collapse, graph, profile, state: effectiveState }),
178
+ [model, collapse, graph, profile, effectiveState],
179
+ );
180
+
181
+ const nodeTypes = useMemo(() => makeNodeTypes(profile), [profile]);
182
+ const nodes = useMemo(() => layout.nodes.map(rfNode), [layout]);
183
+ const edges = useMemo(() => layout.edges.map(rfEdge), [layout]);
184
+
185
+ // occId kroku -> klucz refa (klik w nod przewija prozę do chipu).
186
+ const keyByOcc = useMemo(() => {
187
+ const m = new Map<string, string>();
188
+ for (const s of model.steps) if (s.ref) m.set(s.occId, s.ref.key);
189
+ return m;
190
+ }, [model]);
191
+
192
+ useEffect(() => {
193
+ if (!scrollToKey || !proseRef.current) return;
194
+ const el = proseRef.current.querySelector(
195
+ `[data-arc-key="${CSS.escape(scrollToKey)}"]`,
196
+ );
197
+ el?.scrollIntoView({ behavior: "smooth", block: "center" });
198
+ setScrollToKey(null);
199
+ }, [scrollToKey]);
200
+
201
+ const focusState = useMemo(
202
+ () => ({ focusNodeId, setFocusNodeId, scrollToKey, setScrollToKey }),
203
+ [focusNodeId, scrollToKey],
204
+ );
205
+
206
+ const viewStateActions = useMemo(
207
+ () => ({
208
+ state: effectiveState,
209
+ toggleGroup: (key: string, group: string) =>
210
+ updateState(toggleGroupState(effectiveState, key, group)),
211
+ toggleContext: (contextId: string) =>
212
+ updateState(
213
+ toggleContextState(effectiveState, contextId, collapsedByDefault),
214
+ ),
215
+ }),
216
+ [effectiveState, updateState, collapsedByDefault],
217
+ );
218
+
219
+ const processData = useMemo(() => ({ graph, profile }), [graph, profile]);
220
+
221
+ return (
222
+ <ProcessDataContext.Provider value={processData}>
223
+ <FocusContext.Provider value={focusState}>
224
+ <ViewStateContext.Provider value={viewStateActions}>
225
+ <div
226
+ style={{
227
+ display: "grid",
228
+ gridTemplateColumns: "minmax(320px, 1fr) minmax(420px, 1.3fr)",
229
+ height: "100%",
230
+ }}
231
+ >
232
+ {/* Proza */}
233
+ <div
234
+ ref={proseRef}
235
+ style={{
236
+ overflow: "auto",
237
+ padding: "20px 24px",
238
+ borderRight: `1px solid ${GLASS.border}`,
239
+ }}
240
+ >
241
+ <MdxView key={docKey} mdx={mdx} onOccurrences={setOccurrences} />
242
+ </div>
243
+
244
+ {/* Diagram */}
245
+ <div style={{ position: "relative" }}>
246
+ <ReactFlow
247
+ key={docKey}
248
+ nodes={nodes}
249
+ edges={edges}
250
+ nodeTypes={nodeTypes}
251
+ nodesDraggable={false}
252
+ onNodeMouseEnter={(_, n) => {
253
+ const step = (n.data as { step?: ProcessStep }).step;
254
+ const id =
255
+ step?.node?.id ??
256
+ (n.data as { node?: { id: string } }).node?.id;
257
+ if (id) setFocusNodeId(id);
258
+ }}
259
+ onNodeMouseLeave={() => setFocusNodeId(null)}
260
+ onNodeClick={(_, n) => {
261
+ const k = keyByOcc.get(n.id);
262
+ if (k) setScrollToKey(k);
263
+ }}
264
+ fitView
265
+ fitViewOptions={{ padding: 0.18 }}
266
+ minZoom={0.1}
267
+ proOptions={{ hideAttribution: true }}
268
+ >
269
+ <Background color="#dbeafe" gap={26} size={1.5} />
270
+ <Controls />
271
+ <MiniMap
272
+ pannable
273
+ zoomable
274
+ nodeColor={(n) => {
275
+ const step = (n.data as { step?: ProcessStep }).step;
276
+ if (step && !step.exists) return MISSING_COLOR;
277
+ const kind =
278
+ step?.node?.kind ??
279
+ (n.data as { node?: { kind: string } }).node?.kind;
280
+ return kind ? kindColor(profile, kind) : "#64748b";
281
+ }}
282
+ style={{
283
+ background: "rgba(255,255,255,0.6)",
284
+ border: `1px solid ${GLASS.border}`,
285
+ borderRadius: 12,
286
+ }}
287
+ />
288
+ </ReactFlow>
289
+ </div>
290
+ </div>
291
+ </ViewStateContext.Provider>
292
+ </FocusContext.Provider>
293
+ </ProcessDataContext.Provider>
294
+ );
295
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Tokeny powierzchni (glassmorphism) dla diagramu procesu.
3
+ * Neutralne względem profilu — kolory RODZAJÓW nodów i relacji przychodzą
4
+ * z ProcessProfile, tu tylko szkło, tło i typografia.
5
+ */
6
+
7
+ export const GLASS = {
8
+ space: "rgba(255,255,255,0.42)",
9
+ card: "rgba(255,255,255,0.72)",
10
+ cardStrong: "rgba(255,255,255,0.9)",
11
+ border: "rgba(168,85,247,0.30)",
12
+ borderStrong: "rgba(124,58,237,0.55)",
13
+ blur: "blur(14px)",
14
+ blurSoft: "blur(8px)",
15
+ shadow:
16
+ "0 8px 28px -8px rgba(124,58,237,0.22), 0 2px 8px -2px rgba(124,58,237,0.12)",
17
+ shadowStrong:
18
+ "0 18px 40px -10px rgba(124,58,237,0.30), 0 4px 12px -2px rgba(124,58,237,0.18)",
19
+ text: "#2a173f",
20
+ textMuted: "#6b5b86",
21
+ textFaint: "#9b8cb5",
22
+ };
23
+
24
+ export const BG_GRADIENT =
25
+ "linear-gradient(135deg,#dbeafe 0%,#ffffff 48%,#f3e8ff 100%)";
26
+
27
+ export const MISSING_COLOR = "#e11d48";
28
+
29
+ /** Hex → półprzezroczysty tint powierzchni. */
30
+ export function tint(hex: string, alpha: number): string {
31
+ const h = hex.replace("#", "");
32
+ const r = parseInt(h.slice(0, 2), 16);
33
+ const g = parseInt(h.slice(2, 4), 16);
34
+ const b = parseInt(h.slice(4, 6), 16);
35
+ return `rgba(${r},${g},${b},${alpha})`;
36
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Ustandaryzowany agregat stanu widoku diagramu procesu.
3
+ *
4
+ * Fabryka w stylu fragmentów Arc (por. `createMessageAggregate` z arc-chat):
5
+ * aplikacja podaje własne id, scope i nazwę, a dostaje agregat trzymający
6
+ * stan otwarć per (scopeId, docId) — współdzielony między urządzeniami.
7
+ *
8
+ * Wpięcie w diagram: opakuj query/mutację swojego scope w
9
+ * {@link functionStore} i podaj jako prop `store` do `ProcessDiagram`.
10
+ */
11
+
12
+ import { aggregate, date, string, type ArcId } from "@arcote.tech/arc";
13
+
14
+ export interface ProcessViewAggregateData {
15
+ /** Prefiks nazwy agregatu (np. "bmf"). */
16
+ name: string;
17
+ /** Fabryka id rekordu stanu widoku. */
18
+ viewStateId: ArcId<string>;
19
+ /** Id scope'a właściciela (np. accountId / workspaceId). */
20
+ scopeId: ArcId<string>;
21
+ }
22
+
23
+ export const createProcessViewAggregate = <
24
+ const Data extends ProcessViewAggregateData,
25
+ >(
26
+ data: Data,
27
+ ) => {
28
+ const { viewStateId, scopeId } = data;
29
+ return aggregate(`${data.name}ProcessViewStates`, viewStateId, {
30
+ scopeId,
31
+ /** Id dokumentu procesu (use case / proces), którego dotyczy stan. */
32
+ docId: string(),
33
+ /** JSON-serializowany ProcessViewState (expanded + collapsedContexts). */
34
+ state: string(),
35
+ updatedAt: date(),
36
+ }).publicEvent(
37
+ "processViewSaved",
38
+ {
39
+ viewStateId,
40
+ scopeId,
41
+ docId: string(),
42
+ state: string(),
43
+ },
44
+ async (ctx, event) => {
45
+ const p = event.payload;
46
+ await ctx.set(p.viewStateId, {
47
+ scopeId: p.scopeId,
48
+ docId: p.docId,
49
+ state: p.state,
50
+ updatedAt: event.createdAt,
51
+ });
52
+ },
53
+ );
54
+ };