omkit 0.8.17 → 0.8.18

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.
@@ -164,7 +164,8 @@ button { font: inherit; color: inherit; }
164
164
 
165
165
  .chart { position: relative; min-width: 0; }
166
166
  .chart svg { display: block; overflow: hidden; }
167
- .chart .bar-label { font-size: 12px; fill: var(--bar-text); font-weight: 500; cursor: pointer; }
167
+ /* the row's hit target already spans the label — letting it through keeps the marks under it hoverable */
168
+ .chart .bar-label { font-size: 12px; fill: var(--bar-text); font-weight: 500; pointer-events: none; }
168
169
  .chart .hit { fill: transparent; cursor: pointer; }
169
170
  .chart .hit:hover + .bar { filter: brightness(1.15); }
170
171
  .chart .bar-label.outside { fill: var(--text); }
@@ -190,7 +191,7 @@ button { font: inherit; color: inherit; }
190
191
  .flow-seq.parallel { stroke-dasharray: 1 4; stroke-linecap: round; stroke-width: 2; opacity: 0.8; }
191
192
  .flow-arrowhead { fill: var(--text-3); }
192
193
  .flow-par { fill: var(--text-3); font-size: 10px; font-style: italic; }
193
- .flow-shot-t { fill: var(--text-3); font-size: 10.5px; pointer-events: none; }
194
+ .flow-shot-t { fill: var(--text-3); font-size: 10.5px; pointer-events: none; stroke: var(--panel); stroke-width: 4px; paint-order: stroke; }
194
195
  .flow-card text.sub { fill: var(--text-3); font-size: 11px; }
195
196
  .flow-card .tchip text { fill: var(--text); font-size: 10.5px; }
196
197
  .flow-card .tchip.more text { fill: var(--text-3); }
@@ -224,12 +225,12 @@ button { font: inherit; color: inherit; }
224
225
  .acc-b > .params { margin-bottom: 8px; }
225
226
  .acc-b .section-title:first-child { margin-top: 0; }
226
227
  .acc-file { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; color: var(--text-2); }
227
- .shots { display: grid; gap: 6px; }
228
- .shot { display: grid; grid-template-columns: 64px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 4px; border: 1px solid transparent; border-radius: 8px; cursor: pointer; }
229
- .shot:hover { border-color: var(--accent); background: var(--accent-soft); }
230
- .shot .thumb { width: 64px; }
231
- .shot .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
232
- .shot .t { color: var(--text-3); font-size: 11px; font-variant-numeric: tabular-nums; }
228
+ .dshots { display: grid; gap: 6px; }
229
+ .dshot { display: grid; grid-template-columns: 64px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 4px; border: 1px solid transparent; border-radius: 8px; cursor: pointer; }
230
+ .dshot:hover { border-color: var(--accent); background: var(--accent-soft); }
231
+ .dshot .thumb { width: 64px; }
232
+ .dshot .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
233
+ .dshot .t { color: var(--text-3); font-size: 11px; font-variant-numeric: tabular-nums; }
233
234
  .kv { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 6px 10px; margin: 2px 0 12px; }
234
235
  .kv .k { color: var(--text-2); }
235
236
  .kv .v { min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
@@ -472,18 +473,13 @@ a.icon-btn { text-decoration: none; }
472
473
  #app.hl-on .flow-card.err-path rect.box { stroke: var(--fail); stroke-width: 1.2; stroke-dasharray: 4 3; }
473
474
  #app.hl-on .tree .row.err { background: rgba(239, 68, 68, 0.1); }
474
475
  .chart g.r, .flow-card, .tree .row { transition: opacity 0.15s; }
475
- /* cross-node links: waits (dashed), ref calls (solid), connectors between rows / cards */
476
- .xl-span { stroke-width: 3; stroke-linecap: round; cursor: pointer; }
477
- .xl-span.once { stroke-width: 2.5; stroke-dasharray: 4 3; stroke-linecap: butt; }
478
- .xl-wire { fill: none; stroke-width: 1.4; stroke-dasharray: 3 3; opacity: 0.85; cursor: pointer; }
479
- .xl-span:hover { stroke-width: 4.5; }
480
- .xl-hit { fill: none; stroke: transparent; stroke-width: 10; cursor: pointer; }
481
- #xl-arrow path { fill: var(--text-2); }
482
- .flow-xlink { cursor: pointer; }
483
- .flow-xlink path { fill: none; stroke-width: 1.4; stroke-dasharray: 4 4; opacity: 0.8; }
484
- .flow-xlink path.hit { stroke: transparent; stroke-width: 12; stroke-dasharray: none; }
485
- .flow-xlink:hover path:not(.hit) { stroke-width: 2.6; opacity: 1; }
486
- .flow-xlink text { font-size: 10.5px; fill: var(--text-2); stroke: var(--panel); stroke-width: 3px; paint-order: stroke; }
476
+ /* cross-node links: hollow points on the caller's row, chips in the flow */
477
+ .chart circle.mark.link { stroke-width: 2; }
478
+ .xwire { stroke-width: 1.2; stroke-dasharray: 3 3; opacity: 0.55; pointer-events: none; }
479
+ .xwire.hot { stroke-width: 2; opacity: 1; }
480
+ .fwire { fill: none; stroke-width: 1.2; stroke-dasharray: 3 3; opacity: 0.5; pointer-events: none; }
481
+ .fwire.hot { stroke-width: 2; opacity: 1; }
482
+ .flow-shots .shot rect[stroke] { stroke-width: 1.5; }
487
483
 
488
484
  @media (max-width: 1100px) {
489
485
  .bottom { grid-template-columns: minmax(0, 1fr); }
@@ -195,6 +195,12 @@
195
195
  if (!from) continue;
196
196
  links.push({ item: it, op: l.op, from, to: it.node, t: it.t, t0: it.t - (l.ms || 0), key: l.key || "", ms: l.ms, outcome: l.outcome || "", site: l.site || "" });
197
197
  }
198
+ // …drawn as a point on the row of the action that made it: what it did, when it did it
199
+ for (const l of links) {
200
+ l.label = linkLabel(l);
201
+ l.from.marks.push({ idx: l.item.idx, e: l.item.e, kind: "link", t: l.t, label: l.label, node: l.from, link: l });
202
+ }
203
+ for (const l of links) l.from.marks.sort((a, b) => a.t - b.t);
198
204
 
199
205
  const asserts = items.filter((i) => i.kind === "pass" || i.kind === "fail");
200
206
  const errors = collectErrors(items, nodes, run, tEnd - t0);
@@ -277,6 +283,12 @@
277
283
  out.forEach((e, i) => (e.idx = i));
278
284
  return out;
279
285
  }
286
+ /** A link's short label, as its point or chip shows it. */
287
+ function linkLabel(l) {
288
+ if (l.op === "once") return `waited ${l.key}`;
289
+ if (l.op === "cancel") return `cancel ${displayName(l.to)}`;
290
+ return `${displayName(l.to)}.${l.key}()`;
291
+ }
280
292
  function firstLine(s) {
281
293
  return String(s).split("\n")[0];
282
294
  }
@@ -515,21 +527,8 @@
515
527
  .on("mouseleave", () => moveCursor(null));
516
528
  chartSvg.append("g").attr("class", "grid");
517
529
  chartSvg.append("g").attr("class", "links");
518
- chartSvg
519
- .append("defs")
520
- .append("marker")
521
- .attr("id", "xl-arrow")
522
- .attr("viewBox", "0 0 10 10")
523
- .attr("refX", 9)
524
- .attr("refY", 5)
525
- .attr("markerWidth", 6)
526
- .attr("markerHeight", 6)
527
- .attr("orient", "auto")
528
- .append("path")
529
- .attr("d", "M0,1 L10,5 L0,9 z");
530
+ chartSvg.append("g").attr("class", "xwires");
530
531
  chartSvg.append("g").attr("class", "rows");
531
- // over the rows: their hover targets would otherwise swallow the link strips
532
- chartSvg.append("g").attr("class", "xlinks");
533
532
  chartSvg.append("line").attr("class", "pin").attr("stroke", css("--accent")).attr("stroke-width", 1.5).attr("y1", 0).attr("y2", H);
534
533
  chartSvg.append("line").attr("class", "cursor").attr("y1", 0).attr("y2", H).attr("visibility", "hidden");
535
534
  chartSvg.call(chartZoom).call(chartZoom.transform, state.zoom);
@@ -645,74 +644,15 @@
645
644
  g.select("g.marks").each(function (n) {
646
645
  drawMarks(d3.select(this), n, x);
647
646
  });
648
- drawLinks(x, index);
647
+ rowIndex = index;
648
+ drawWires(x);
649
649
 
650
650
  chartSvg.select("line.pin").attr("x1", state.pin != null ? x(state.pin) : -10).attr("x2", state.pin != null ? x(state.pin) : -10);
651
651
  if (app.classList.contains("hl-on") || state.errHl) applyHl();
652
652
  }
653
653
 
654
- /** The visible row standing in for `n`: its own, or its nearest expanded ancestor's. */
655
- function rowIndexOf(n, index) {
656
- for (let p = n; p; p = p.parent) if (index.has(p.id)) return index.get(p.id);
657
- return -1;
658
- }
659
- /**
660
- * Cross-node links on the timeline. A strip under the caller's bar holds its waits
661
- * (`once`, dashed) and its calls on another action's ref (solid), in the target's colour;
662
- * a dashed connector joins the two rows where a wait ended (target → waiter) and where a
663
- * cancel landed (canceller → target).
664
- */
665
- function drawLinks(x, index) {
666
- const STRIP_Y = BAR_Y + BAR_H + 3;
667
- const spans = [];
668
- const wires = [];
669
- for (const l of model.links) {
670
- const fi = rowIndexOf(l.from, index);
671
- const ti = rowIndexOf(l.to, index);
672
- if (fi < 0) continue;
673
- if (l.op !== "cancel") spans.push({ l, fi });
674
- if (l.op !== "ref" && ti >= 0 && ti !== fi) wires.push({ l, fi, ti });
675
- }
676
- const layer = chartSvg.select(".xlinks");
677
- layer
678
- .selectAll("line.xl-span")
679
- .data(spans)
680
- .join("line")
681
- .attr("class", (d) => `xl-span ${d.l.op}`)
682
- .attr("x1", (d) => x(d.l.t0))
683
- .attr("x2", (d) => Math.max(x(d.l.t), x(d.l.t0) + 2))
684
- .attr("y1", (d) => d.fi * ROW_H + STRIP_Y)
685
- .attr("y2", (d) => d.fi * ROW_H + STRIP_Y)
686
- .attr("stroke", (d) => colorOf(d.l.to))
687
- .call(bindLink);
688
- const wirePath = (d) => {
689
- const px = x(d.l.t);
690
- const fy = d.fi * ROW_H + STRIP_Y;
691
- const ty = d.ti * ROW_H + BAR_Y + BAR_H / 2;
692
- const [y1, y2] = d.l.op === "once" ? [ty, fy] : [fy, ty];
693
- return `M${px},${y1} C${px + 10},${(2 * y1 + y2) / 3} ${px + 10},${(y1 + 2 * y2) / 3} ${px},${y2}`;
694
- };
695
- layer
696
- .selectAll("path.xl-wire")
697
- .data(wires)
698
- .join("path")
699
- .attr("class", (d) => `xl-wire ${d.l.op}`)
700
- .attr("d", wirePath)
701
- .attr("stroke", (d) => (d.l.op === "cancel" ? css("--cancel") : colorOf(d.l.to)))
702
- .attr("marker-end", "url(#xl-arrow)");
703
- // a wide transparent twin over each wire, so a 1px dashed line is easy to hover
704
- layer.selectAll("path.xl-hit").data(wires).join("path").attr("class", "xl-hit").attr("d", wirePath).call(bindLink);
705
- }
706
- function bindLink(sel) {
707
- sel
708
- .on("click", (ev, d) => {
709
- ev.stopPropagation();
710
- setPin(d.l.t);
711
- select(d.l.from.id, "links");
712
- })
713
- .on("mouseenter", (ev, d) => showTip(ev, linkTip(d.l), true))
714
- .on("mousemove", (ev) => moveTip(ev))
715
- .on("mouseleave", hideTip);
654
+ function linkColor(l) {
655
+ return l.op === "cancel" ? css("--cancel") : colorOf(l.to);
716
656
  }
717
657
  /** A link in words: who did what to whom, how it ended, and the line that did it. */
718
658
  function linkTip(l, count) {
@@ -737,6 +677,40 @@
737
677
  ${l.site ? `<div class="mono small" style="margin-top:6px">${esc(relOf(l.site))}${line ? ":" + line : ""}</div>` : ""}`;
738
678
  }
739
679
 
680
+ let rowIndex = new Map();
681
+ /** The visible row standing in for `n`: its own, or its nearest expanded ancestor's. */
682
+ function rowOf(n) {
683
+ for (let p = n; p; p = p.parent) if (rowIndex.has(p.id)) return rowIndex.get(p.id);
684
+ return -1;
685
+ }
686
+ /**
687
+ * A link's wire: straight down (or up) the timeline at the moment it happened, from the
688
+ * caller's point to the target's row — both ends share the instant, so the line reads as
689
+ * "this, then, to that". Waits and cancels always have one; a ref call's shows on hover only,
690
+ * since a chatty handle would otherwise hatch the chart.
691
+ */
692
+ function wireOf(l, x) {
693
+ const a = rowOf(l.from);
694
+ const b = rowOf(l.to);
695
+ if (a < 0 || b < 0 || a === b) return null;
696
+ const cy = BAR_Y + BAR_H / 2;
697
+ return { l, x: x(l.t), y1: a * ROW_H + cy, y2: b * ROW_H + cy };
698
+ }
699
+ function drawWires(x, extra) {
700
+ const shown = model.links.filter((l) => l.op !== "ref" || l === extra);
701
+ chartSvg
702
+ .select(".xwires")
703
+ .selectAll("line")
704
+ .data(shown.map((l) => wireOf(l, x)).filter(Boolean))
705
+ .join("line")
706
+ .attr("class", (w) => `xwire ${w.l.op}${w.l === extra ? " hot" : ""}`)
707
+ .attr("x1", (w) => w.x)
708
+ .attr("x2", (w) => w.x)
709
+ .attr("y1", (w) => w.y1)
710
+ .attr("y2", (w) => w.y2)
711
+ .attr("stroke", (w) => linkColor(w.l));
712
+ }
713
+
740
714
  function drawMarks(g, n, x) {
741
715
  const cy = BAR_Y + BAR_H / 2;
742
716
  // cluster marks closer than 4px so a chatty node draws a readable line of dots
@@ -754,18 +728,27 @@
754
728
  g.selectAll("circle.mark")
755
729
  .data(marks)
756
730
  .join("circle")
757
- .attr("class", "mark")
731
+ .attr("class", (d) => `mark ${worst(d)}`)
758
732
  .attr("cx", (d) => d.px)
759
733
  .attr("cy", cy)
760
734
  .attr("r", (d) => (d.more.length ? 6 : 5))
761
735
  .attr("fill", (d) => css(MARK[worst(d)].color))
736
+ // a link is a ring in its target's colour — set as styles, over the `.mark` stroke rule
737
+ .style("fill", (d) => (worst(d) === "link" ? "var(--panel)" : null))
738
+ .style("stroke", (d) => (worst(d) === "link" ? linkColor(d.m.link) : null))
762
739
  .on("click", (ev, d) => {
763
740
  ev.stopPropagation();
764
741
  openItem(d.m);
765
742
  })
766
- .on("mouseenter", (ev, d) => showTip(ev, markTip(d)))
743
+ .on("mouseenter", (ev, d) => {
744
+ showTip(ev, markTip(d), d.m.kind === "link");
745
+ if (d.m.kind === "link") drawWires(xz(), d.m.link);
746
+ })
767
747
  .on("mousemove", (ev) => moveTip(ev))
768
- .on("mouseleave", hideTip);
748
+ .on("mouseleave", (ev, d) => {
749
+ hideTip();
750
+ if (d.m.kind === "link") drawWires(xz());
751
+ });
769
752
  // labels under the bar, skipped where they would collide
770
753
  const labels = [];
771
754
  let edge = -Infinity;
@@ -852,6 +835,18 @@
852
835
  .attr("stroke", color ? alpha(color, 0.45) : css("--line"));
853
836
  return { g, w };
854
837
  }
838
+ /** A link chip's text: what kind of interaction, at a glance. */
839
+ function linkGlyph(x2) {
840
+ if (x2.l.op === "once") return "wait";
841
+ if (x2.l.op === "cancel") return "stop";
842
+ return x2.n > 1 ? `()×${x2.n}` : "()";
843
+ }
844
+ /** "3 snapshots", "2 links", or "4 items" for a mix. */
845
+ function chipCount(shots) {
846
+ const links = shots.filter((x2) => x2.type === "link").length;
847
+ if (!links) return `${shots.length} snapshots`;
848
+ return links === shots.length ? `${links} links` : `${shots.length} items`;
849
+ }
855
850
  function renderFlow() {
856
851
  d3.select(el.flow).selectAll("svg").remove();
857
852
  const svg = d3.select(el.flow).append("svg");
@@ -879,6 +874,16 @@
879
874
  if (!shotsBy.has(g.node)) shotsBy.set(g.node, []);
880
875
  shotsBy.get(g.node).push(g);
881
876
  }
877
+ // a node's links sit with its snapshots, in time order; a run of identical ref calls is one chip
878
+ for (const l of model.links) {
879
+ const list = shotsBy.get(l.from) || shotsBy.set(l.from, []).get(l.from);
880
+ const prev = list[list.length - 1];
881
+ if (l.op === "ref" && prev && prev.type === "link" && prev.l.op === "ref" && prev.l.to === l.to && prev.l.key === l.key) {
882
+ prev.n++;
883
+ continue;
884
+ }
885
+ list.push({ type: "link", l, n: 1, t: l.t, name: l.label });
886
+ }
882
887
  const byTime = (a, b) => (a.t ?? Infinity) - (b.t ?? Infinity);
883
888
  const pos = new Map();
884
889
  const lanes = [];
@@ -965,70 +970,6 @@
965
970
  }
966
971
 
967
972
  const nodes = [...pos.keys()].map((id) => model.byId.get(id));
968
- // cross-node links: one curve per (caller, target, kind), arcing out past the cards' right
969
- // edges; drawn first so the cards sit on top of their ends
970
- const agg = new Map();
971
- for (const l of model.links) {
972
- if (!pos.has(l.from.id) || !pos.has(l.to.id) || l.from === l.to) continue;
973
- const k = `${l.from.id}\0${l.to.id}\0${l.op}`;
974
- if (!agg.has(k)) agg.set(k, { op: l.op, from: l.from, to: l.to, list: [] });
975
- agg.get(k).list.push(l);
976
- }
977
- const xl = flowG.append("g").attr("class", "flow-xlinks");
978
- const fan = new Map(); // edges between the same two cards fan out instead of overlapping
979
- for (const a of agg.values()) {
980
- const f = pos.get(a.from.id);
981
- const t = pos.get(a.to.id);
982
- const [s, e] = a.op === "once" ? [t, f] : [f, t];
983
- const pair = [a.from.id, a.to.id].sort().join("");
984
- const k = fan.get(pair) || 0;
985
- fan.set(pair, k + 1);
986
- const off = (k % 2 ? 1 : -1) * Math.ceil(k / 2) * 10;
987
- let x1, y1, x2, y2, path, lx, ly;
988
- if (Math.abs(s.x - e.x) < CW) {
989
- // same column: arc out past the right edges
990
- x1 = s.x + CW;
991
- x2 = e.x + CW;
992
- y1 = s.y + CH / 2 + off;
993
- y2 = e.y + CH / 2 + off;
994
- const bulge = 36 + Math.min(140, Math.abs(y2 - y1) * 0.2) + Math.abs(off);
995
- path = `M${x1},${y1} C${x1 + bulge},${y1} ${x2 + bulge},${y2} ${x2 + 2},${y2}`;
996
- lx = (x1 + x2) / 2 + bulge * 0.75 + 4;
997
- ly = (y1 + y2) / 2 + 4;
998
- } else {
999
- // different columns: facing sides, an S-curve between them, below the tree's own links
1000
- const rightward = e.x > s.x;
1001
- x1 = rightward ? s.x + CW : s.x;
1002
- x2 = rightward ? e.x : e.x + CW;
1003
- y1 = s.y + CH * 0.72 + off;
1004
- y2 = e.y + CH * 0.72 + off;
1005
- const mx = (x1 + x2) / 2;
1006
- path = `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2 + (rightward ? -2 : 2)},${y2}`;
1007
- lx = mx + 4;
1008
- ly = (y1 + y2) / 2 - 4;
1009
- }
1010
- const keys = [...new Set(a.list.map((l) => l.key))];
1011
- const label =
1012
- a.op === "cancel" ? "cancel" : a.op === "once" ? `once ${keys.join(", ")}` : keys.length === 1 ? `${keys[0]}() ×${a.list.length}` : `ref ×${a.list.length}`;
1013
- const g = xl
1014
- .append("g")
1015
- .attr("class", `flow-xlink ${a.op}`)
1016
- .on("click", (ev) => {
1017
- ev.stopPropagation();
1018
- select(a.from.id, "links");
1019
- })
1020
- .on("mouseenter", (ev) => showTip(ev, linkTip(a.list[a.list.length - 1], a.list.length), true))
1021
- .on("mousemove", (ev) => moveTip(ev))
1022
- .on("mouseleave", hideTip);
1023
- g.append("path")
1024
- .attr("d", path)
1025
- .attr("stroke", a.op === "cancel" ? css("--cancel") : colorOf(a.to))
1026
- .attr("marker-end", "url(#flow-arrow)");
1027
- g.append("path").attr("class", "hit").attr("d", path);
1028
- // between neighbouring columns there is no room for words — the tooltip carries them
1029
- if (Math.abs(x2 - x1) >= 90) g.append("text").attr("x", lx).attr("y", ly).text(truncate(label, 28));
1030
- }
1031
-
1032
973
  const card = flowG
1033
974
  .append("g")
1034
975
  .selectAll("g.flow-card")
@@ -1118,6 +1059,31 @@
1118
1059
 
1119
1060
  // snapshot chips — on lane arrows, and in strips under leaf cards
1120
1061
  const chipLayer = flowG.append("g").attr("class", "flow-shots");
1062
+ // A link chip's wire, drawn under everything else: out of the chip to a gutter right of the
1063
+ // column, along it, and into the target card's right edge — so it never runs behind the cards
1064
+ // between. Waits and cancels always have one; a ref call's shows while hovered.
1065
+ const wireLayer = flowG.insert("g", ":first-child").attr("class", "flow-wires");
1066
+ const linkChips = [];
1067
+ const drawFlowWires = (hot) => {
1068
+ const wires = [];
1069
+ for (const c of linkChips) {
1070
+ const p = pos.get(c.x2.l.to.id);
1071
+ if (!p || (c.x2.l.op === "ref" && c.x2 !== hot)) continue;
1072
+ wires.push({ c, p });
1073
+ }
1074
+ wireLayer
1075
+ .selectAll("path")
1076
+ .data(wires)
1077
+ .join("path")
1078
+ .attr("class", (w) => `fwire ${w.c.x2.l.op}${w.c.x2 === hot ? " hot" : ""}`)
1079
+ .attr("d", (w, i) => {
1080
+ const tx = w.p.x + CW;
1081
+ const ty = w.p.y + CH / 2;
1082
+ const gx = Math.max(tx, w.c.cx + TW / 2) + 12 + (i % 6) * 6; // staggered within the column gap
1083
+ return `M${w.c.cx + TW / 2},${w.c.cy} H${gx} V${ty} H${tx}`;
1084
+ })
1085
+ .attr("stroke", (w) => linkColor(w.c.x2.l));
1086
+ };
1121
1087
  const drawChips = (x, y, shots, max, owner) => {
1122
1088
  const shown = shots.length > max ? shots.slice(0, max - 1) : shots;
1123
1089
  const g = chipLayer.append("g").attr("transform", `translate(${x},${y})`);
@@ -1129,12 +1095,28 @@
1129
1095
  .attr("transform", (_x, i) => `translate(${i * (TW + 4)},0)`)
1130
1096
  .on("click", (ev, x2) => {
1131
1097
  ev.stopPropagation();
1132
- openGallery(x2);
1098
+ if (x2.type !== "link") return openGallery(x2);
1099
+ setPin(x2.t);
1100
+ select(x2.l.from.id, "links");
1101
+ })
1102
+ .on("mouseenter", (ev, x2) => {
1103
+ showTip(ev, x2.type === "link" ? linkTip(x2.l, x2.n) : galleryTip(x2), x2.type === "link");
1104
+ if (x2.type === "link") drawFlowWires(x2);
1133
1105
  })
1134
- .on("mouseenter", (ev, x2) => showTip(ev, galleryTip(x2)))
1135
1106
  .on("mousemove", (ev) => moveTip(ev))
1136
- .on("mouseleave", hideTip);
1137
- cell.append("rect").attr("width", TW).attr("height", TH).attr("rx", 5);
1107
+ .on("mouseleave", (ev, x2) => {
1108
+ hideTip();
1109
+ if (x2.type === "link") drawFlowWires(null);
1110
+ });
1111
+ shown.forEach((x2, i) => {
1112
+ if (x2.type === "link") linkChips.push({ x2, cx: x + i * (TW + 4) + TW / 2, cy: y + TH / 2 });
1113
+ });
1114
+ cell
1115
+ .append("rect")
1116
+ .attr("width", TW)
1117
+ .attr("height", TH)
1118
+ .attr("rx", 5)
1119
+ .attr("stroke", (x2) => (x2.type === "link" ? linkColor(x2.l) : null));
1138
1120
  cell
1139
1121
  .filter((x2) => x2.image)
1140
1122
  .append("image")
@@ -1150,7 +1132,7 @@
1150
1132
  .attr("x", TW / 2)
1151
1133
  .attr("y", TH / 2 + 3.5)
1152
1134
  .attr("text-anchor", "middle")
1153
- .text((x2) => (x2.type === "snapshot" ? "{ }" : "file"));
1135
+ .text((x2) => (x2.type === "link" ? linkGlyph(x2) : x2.type === "snapshot" ? "{ }" : "file"));
1154
1136
  let end = shown.length * (TW + 4);
1155
1137
  if (shots.length > shown.length) {
1156
1138
  const more = g
@@ -1176,7 +1158,7 @@
1176
1158
  .attr("class", "flow-shot-t")
1177
1159
  .attr("x", end + 2)
1178
1160
  .attr("y", TH / 2 + 4)
1179
- .text(`${fmtRel(t)} · ${shots.length > 1 ? `${shots.length} snapshots` : esc(truncate(shots[0].name, 22))}`);
1161
+ .text(`${fmtRel(t)} · ${shots.length > 1 ? chipCount(shots) : truncate(shots[0].name, 22)}`);
1180
1162
  }
1181
1163
  };
1182
1164
  for (const r of chipRows) {
@@ -1184,6 +1166,7 @@
1184
1166
  drawChips(r.x, r.y, r.shots, 6, owner);
1185
1167
  }
1186
1168
  for (const s of strips) drawChips(s.x, s.y, s.shots, Math.floor((CW - 12) / (TW + 4)), s.n);
1169
+ drawFlowWires(null);
1187
1170
 
1188
1171
  flowPos = new Map([...pos].map(([id, p]) => [id, { x: p.x + CW / 2, y: p.y + CH / 2 }]));
1189
1172
  svg.call(flowZoom).on("dblclick.zoom", null);
@@ -1446,7 +1429,7 @@
1446
1429
  title: "Snapshots & files",
1447
1430
  count: shots.length,
1448
1431
  empty: !shots.length,
1449
- body: () => `<div class="shots">${shots.map((g) => shotHtml(g, n)).join("")}</div>`,
1432
+ body: () => `<div class="dshots">${shots.map((g) => shotHtml(g, n)).join("")}</div>`,
1450
1433
  },
1451
1434
  {
1452
1435
  key: "events",
@@ -1527,7 +1510,7 @@
1527
1510
  /** A snapshot or file in the details panel: a small thumb with its name and when it landed. */
1528
1511
  function shotHtml(g, n) {
1529
1512
  const i = model.gallery.indexOf(g);
1530
- return `<div class="shot" data-gallery="${i}" title="${esc(g.name)}">
1513
+ return `<div class="dshot" data-gallery="${i}" title="${esc(g.name)}">
1531
1514
  ${thumbHtml(g)}
1532
1515
  <span class="nm">${esc(g.name)}</span>
1533
1516
  ${g.t != null ? `<span class="t">${fmtRel(g.t - n.start)}</span>` : ""}
@@ -1569,7 +1552,8 @@
1569
1552
  function openItem(item) {
1570
1553
  if (!item) return;
1571
1554
  setPin(item.t);
1572
- if (item.node && item.node.id !== state.selected) select(item.node.id, "events");
1555
+ if (item.kind === "link") select(item.node.id, "links");
1556
+ else if (item.node && item.node.id !== state.selected) select(item.node.id, "events");
1573
1557
  if (item.gallery) openGallery(item.gallery);
1574
1558
  }
1575
1559
  function setPin(t) {
@@ -1929,6 +1913,9 @@
1929
1913
  }
1930
1914
  function markTip(d) {
1931
1915
  const m = d.m;
1916
+ if (m.kind === "link") {
1917
+ return linkTip(m.link) + (d.more.length ? `<div class="ts" style="margin-top:4px">+${d.more.length} more within a few ms</div>` : "");
1918
+ }
1932
1919
  const mk = MARK[m.kind] || MARK.event;
1933
1920
  const img = m.gallery && m.gallery.image ? `<img src="${model.asset(m.gallery.f.asset)}" alt="">` : "";
1934
1921
  const more = d.more.length ? `<div class="ts">+${d.more.length} more within a few ms</div>` : "";
@@ -2321,8 +2308,7 @@
2321
2308
  ${legendItem("--fail", "Failure / error", "dot")}
2322
2309
  ${legendItem("--cancel", "Cancelled", "dash")}
2323
2310
  ${legendItem("--accent", "Pinned time", "line")}
2324
- ${legendItem("--text-2", "Waits for an event", "dash")}
2325
- ${legendItem("--text-2", "Call on a ref", "tick")}
2311
+ ${legendItem("--text-2", "Wait · cancel · ref call", "ring")}
2326
2312
  </div>
2327
2313
  <div class="muted" style="margin-top:12px;font-size:11px">Ctrl + wheel to zoom · drag to pan · double-click a bar to focus · ↑/↓ to step</div>
2328
2314
  </div>
@@ -2339,8 +2325,8 @@
2339
2325
  const sw =
2340
2326
  shape === "bar"
2341
2327
  ? `<span style="width:16px;height:9px;border-radius:3px;background:${c}"></span>`
2342
- : shape === "tick"
2343
- ? `<span style="width:16px;height:3px;border-radius:2px;background:${c}"></span>`
2328
+ : shape === "ring"
2329
+ ? `<span class="dot" style="width:11px;height:11px;background:var(--panel);box-shadow:inset 0 0 0 2px ${c}"></span>`
2344
2330
  : shape === "dash"
2345
2331
  ? `<span style="width:16px;height:0;border-top:2px dashed ${c}"></span>`
2346
2332
  : shape === "line"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.8.17",
3
+ "version": "0.8.18",
4
4
  "description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",