footprint-explainable-ui 0.25.2 → 0.25.4

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.
@@ -94,6 +94,9 @@ var rawDefaults = {
94
94
  success: "#22c55e",
95
95
  error: "#ef4444",
96
96
  warning: "#f59e0b",
97
+ nodeCursor: "#f59e0b",
98
+ nodeVisited: "#22c55e",
99
+ nodeMain: "#6366f1",
97
100
  bgPrimary: "#0f172a",
98
101
  bgSecondary: "#1e293b",
99
102
  bgTertiary: "#334155",
@@ -114,6 +117,9 @@ var defaultTokens = {
114
117
  success: `var(--fp-color-success, ${rawDefaults.colors.success})`,
115
118
  error: `var(--fp-color-error, ${rawDefaults.colors.error})`,
116
119
  warning: `var(--fp-color-warning, ${rawDefaults.colors.warning})`,
120
+ nodeCursor: `var(--fp-node-cursor, ${rawDefaults.colors.nodeCursor})`,
121
+ nodeVisited: `var(--fp-node-visited, ${rawDefaults.colors.nodeVisited})`,
122
+ nodeMain: `var(--fp-node-main, ${rawDefaults.colors.nodeMain})`,
117
123
  bgPrimary: `var(--fp-bg-primary, ${rawDefaults.colors.bgPrimary})`,
118
124
  bgSecondary: `var(--fp-bg-secondary, ${rawDefaults.colors.bgSecondary})`,
119
125
  bgTertiary: `var(--fp-bg-tertiary, ${rawDefaults.colors.bgTertiary})`,
@@ -142,6 +148,15 @@ var theme = {
142
148
  success: v("--fp-color-success", "#22c55e"),
143
149
  error: v("--fp-color-error", "#ef4444"),
144
150
  warning: v("--fp-color-warning", "#f59e0b"),
151
+ // Semantic NODE-STATE colors — first-class, themeable roles a runtime overlay
152
+ // maps onto (scrub cursor / executed / a group's lead node). Distinct from the
153
+ // generic `primary` accent so the three read as three different things.
154
+ nodeCursor: v("--fp-node-cursor", "#f59e0b"),
155
+ // the current / scrubbed-to step
156
+ nodeVisited: v("--fp-node-visited", "#22c55e"),
157
+ // executed up to the cursor
158
+ nodeMain: v("--fp-node-main", "#6366f1"),
159
+ // the lead / "hero" node of a group
145
160
  bgPrimary: v("--fp-bg-primary", "#0f172a"),
146
161
  bgSecondary: v("--fp-bg-secondary", "#1e293b"),
147
162
  bgTertiary: v("--fp-bg-tertiary", "#334155"),
@@ -357,12 +372,12 @@ var StageNode = (0, import_react3.memo)(function StageNode2({
357
372
  const isHero = data.emphasis === "hero";
358
373
  const isMuted = data.emphasis === "muted";
359
374
  const sizeScale = data.size === "lg" ? 1.3 : data.size === "sm" ? 0.85 : 1;
360
- const restingBg = isHero ? `color-mix(in srgb, ${theme.primary} 12%, ${theme.bgSecondary})` : theme.bgSecondary;
361
- const restingBorder = isHero ? theme.primary : theme.border;
362
- const restingShadow = isHero ? `0 0 10px color-mix(in srgb, ${theme.primary} 22%, transparent)` : `0 2px 8px rgba(0,0,0,0.15)`;
363
- const bg = active ? theme.primary : done ? theme.success : error ? theme.error : restingBg;
364
- const borderColor = active ? theme.primary : done ? theme.success : error ? theme.error : restingBorder;
365
- const shadow = active ? `0 0 22px color-mix(in srgb, ${theme.primary} 55%, transparent)` : done ? `0 0 8px color-mix(in srgb, ${theme.success} 20%, transparent)` : error ? `0 0 12px color-mix(in srgb, ${theme.error} 30%, transparent)` : restingShadow;
375
+ const restingBg = isHero ? `color-mix(in srgb, ${theme.nodeMain} 12%, ${theme.bgSecondary})` : theme.bgSecondary;
376
+ const restingBorder = isHero ? theme.nodeMain : theme.border;
377
+ const restingShadow = isHero ? `0 0 10px color-mix(in srgb, ${theme.nodeMain} 22%, transparent)` : `0 2px 8px rgba(0,0,0,0.15)`;
378
+ const bg = active ? theme.nodeCursor : done ? theme.nodeVisited : error ? theme.error : restingBg;
379
+ const borderColor = active ? theme.nodeCursor : done ? theme.nodeVisited : error ? theme.error : restingBorder;
380
+ const shadow = active ? `0 0 22px color-mix(in srgb, ${theme.nodeCursor} 55%, transparent)` : done ? `0 0 8px color-mix(in srgb, ${theme.nodeVisited} 20%, transparent)` : error ? `0 0 12px color-mix(in srgb, ${theme.error} 30%, transparent)` : restingShadow;
366
381
  const textColor = active || done || error ? "#fff" : theme.textPrimary;
367
382
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
368
383
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Handle, { type: "target", position: import_react4.Position.Top, style: { opacity: 0 } }),
@@ -392,8 +407,8 @@ var StageNode = (0, import_react3.memo)(function StageNode2({
392
407
  },
393
408
  children: stepNumbers.map((num, i) => {
394
409
  const isLatest = i === stepNumbers.length - 1;
395
- const badgeBg = isLatest && active ? theme.primary : theme.success;
396
- const glow = isLatest && active ? `color-mix(in srgb, ${theme.primary} 50%, transparent)` : `color-mix(in srgb, ${theme.success} 40%, transparent)`;
410
+ const badgeBg = isLatest && active ? theme.nodeCursor : theme.nodeVisited;
411
+ const glow = isLatest && active ? `color-mix(in srgb, ${theme.nodeCursor} 50%, transparent)` : `color-mix(in srgb, ${theme.nodeVisited} 40%, transparent)`;
397
412
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
398
413
  "div",
399
414
  {
@@ -439,7 +454,7 @@ var StageNode = (0, import_react3.memo)(function StageNode2({
439
454
  inset: -6,
440
455
  borderRadius: isDecider ? 0 : `calc(${theme.radius} + 4px)`,
441
456
  clipPath: isDecider ? "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)" : void 0,
442
- border: `2px solid ${theme.primary}`,
457
+ border: `2px solid ${theme.nodeCursor}`,
443
458
  opacity: 0.3,
444
459
  animation: "fp-pulse 1.5s ease-out infinite"
445
460
  }
@@ -2066,6 +2081,18 @@ function centerForkParents(graph, options = {}) {
2066
2081
  }
2067
2082
  return minX <= maxX ? Math.max(minX, Math.min(maxX, desiredX)) : x0;
2068
2083
  };
2084
+ const evenFanKids = (forkCenter, kids) => {
2085
+ if (kids.length < 2) return;
2086
+ const sorted = [...kids].sort((a, b) => centerX(a) - centerX(b));
2087
+ let gap = 0;
2088
+ for (let i = 0; i < sorted.length - 1; i++) {
2089
+ gap = Math.max(gap, width.get(sorted[i]) / 2 + nodeSep + width.get(sorted[i + 1]) / 2);
2090
+ }
2091
+ const mid = (sorted.length - 1) / 2;
2092
+ for (let i = 0; i < sorted.length; i++) {
2093
+ workingX.set(sorted[i], forkCenter + (i - mid) * gap - width.get(sorted[i]) / 2);
2094
+ }
2095
+ };
2069
2096
  const order = [...graph.nodes].sort(
2070
2097
  (a, b) => b.position.y - a.position.y || a.position.x - b.position.x || a.id.localeCompare(b.id)
2071
2098
  );
@@ -2084,6 +2111,11 @@ function centerForkParents(graph, options = {}) {
2084
2111
  const wN = width.get(n.id);
2085
2112
  const span = (Math.min(...centers) + Math.max(...centers)) / 2;
2086
2113
  workingX.set(n.id, clampX(n.id, span - wN / 2));
2114
+ if (isFork) {
2115
+ const succSets = kin.map((k) => childrenOf.get(k) ?? []);
2116
+ const isDiamond = kin.length >= 2 && succSets[0].some((s) => succSets.every((ss) => ss.includes(s)));
2117
+ if (isDiamond) evenFanKids(centerX(n.id), kin);
2118
+ }
2087
2119
  const stepOf = isFork ? predsOf : childrenOf;
2088
2120
  let curId = n.id;
2089
2121
  const walked = /* @__PURE__ */ new Set([curId]);
@@ -2100,6 +2132,28 @@ function centerForkParents(graph, options = {}) {
2100
2132
  curId = m;
2101
2133
  }
2102
2134
  }
2135
+ for (const n of order) {
2136
+ const outD = outDegree.get(n.id) ?? 0;
2137
+ const inD = inDegree.get(n.id) ?? 0;
2138
+ if (!(outD >= 2 && inD <= 1)) continue;
2139
+ const kids = (childrenOf.get(n.id) ?? []).filter(
2140
+ (k) => byId.get(k)?.parentId === n.parentId
2141
+ );
2142
+ if (kids.length < 2) continue;
2143
+ const succSets = kids.map((k) => childrenOf.get(k) ?? []);
2144
+ const isDiamond = succSets[0].some((s) => succSets.every((ss) => ss.includes(s)));
2145
+ if (isDiamond) continue;
2146
+ const ps = predsOf.get(n.id);
2147
+ if (!ps || ps.length !== 1) continue;
2148
+ const pred = ps[0];
2149
+ if ((outDegree.get(pred) ?? 0) !== 1) continue;
2150
+ if (byId.get(pred)?.parentId !== byId.get(n.id)?.parentId) continue;
2151
+ const before = centerX(n.id);
2152
+ workingX.set(n.id, clampX(n.id, centerX(pred) - width.get(n.id) / 2));
2153
+ const delta = centerX(n.id) - before;
2154
+ if (delta === 0) continue;
2155
+ for (const k of kids) workingX.set(k, clampX(k, workingX.get(k) + delta));
2156
+ }
2103
2157
  const nodes = graph.nodes.map(
2104
2158
  (n) => workingX.get(n.id) === n.position.x ? n : { ...n, position: { x: workingX.get(n.id), y: n.position.y } }
2105
2159
  );