@tangle-network/charts 0.21.5 → 0.21.7

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.
@@ -47,7 +47,8 @@
47
47
  font-family: var(--font-sans, Inter, ui-sans-serif, system-ui, sans-serif);
48
48
  font-size: 14px;
49
49
  line-height: 20px;
50
- font-feature-settings: "tnum" 1, "cv11" 1;
50
+ /* Tabular figures only on numeric cells: on names they widen hyphens ("v6 - battery", "glm - 5.3"). */
51
+ font-feature-settings: "cv11" 1;
51
52
  -webkit-font-smoothing: antialiased;
52
53
  }
53
54
  .fw-root-agents { --fw-bar-h: 24px; --fw-label-col: min(var(--fw-name-w), 28%); border-radius: 16px; }
@@ -81,7 +82,7 @@
81
82
  .fw-root .fw-state::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
82
83
  .fw-root .fw-state-live { color: var(--fw-accent); background: color-mix(in oklab, var(--fw-accent) 12%, transparent); }
83
84
  .fw-root .fw-state-live::before { animation: fw-pulse 1.6s var(--fw-ease) infinite; }
84
- .fw-root .fw-headline { color: var(--fw-muted); font-variant-numeric: tabular-nums; display: flex; align-items: center; gap: 8px; }
85
+ .fw-root .fw-headline { color: var(--fw-muted); display: flex; align-items: center; gap: 8px; }
85
86
  .fw-root .fw-done-mark { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid var(--fw-ok); color: var(--fw-ok); font-size: 12px; line-height: 1; }
86
87
  .fw-root .fw-tools { display: flex; align-items: center; gap: 8px; }
87
88
  .fw-root .fw-views { display: inline-flex; padding: 4px; border-radius: 8px; background: var(--fw-well); border: 1px solid var(--fw-border); }
@@ -135,7 +136,8 @@
135
136
  background: repeating-linear-gradient(-45deg, color-mix(in oklab, var(--fw-text) 9%, transparent) 0 1px, transparent 1px 6px), color-mix(in oklab, var(--fw-text) 4%, transparent);
136
137
  border-inline: 1px dashed color-mix(in oklab, var(--fw-text) 24%, transparent); }
137
138
  .fw-root .fw-pause-label b { font-weight: 600; color: var(--fw-text); }
138
- .fw-root .fw-pause-label { position: sticky; top: 4px; display: block; width: max-content; max-width: calc(100% - 8px); margin: 4px auto 0; padding: 0 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
139
+ .fw-root .fw-pause { overflow: visible; }
140
+ .fw-root .fw-pause-label { position: sticky; top: 4px; display: block; width: max-content; margin: 4px 0 0 50%; translate: -50% 0; padding: 0 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
139
141
  border-radius: 4px; font-size: 12px; line-height: 20px; color: var(--fw-muted); background: var(--fw-surface); box-shadow: inset 0 0 0 1px var(--fw-border); }
140
142
  /* The label alone takes the pointer, for its reason; the band stays click-through to the rows. */
141
143
  .fw-root .fw-pause-label[title] { pointer-events: auto; cursor: help; }
@@ -234,7 +236,11 @@
234
236
  /* An agent bar with no child marks says who it is by its lane color, never a neutral grey. */
235
237
  /* In the agent view the bar's own extent between steps is waiting: hatched in the outcome tone, under the step segments. */
236
238
  .fw-root .fw-view-agents .fw-bar.fw-k-pipeline.fw-has-marks { background: repeating-linear-gradient(-45deg, color-mix(in oklab, var(--tone) 28%, transparent) 0 1px, transparent 1px 5px), color-mix(in oklab, var(--tone) 9%, transparent); }
237
- .fw-root .fw-view-agents .fw-bar.fw-k-pipeline:not(.fw-has-marks, .fw-has-agents) { background: var(--fill); box-shadow: inset 0 0 0 1px var(--line); }
239
+ /* An agent lane is a tint of its color; full saturation is kept for its model and tool segments and its status dot, so a run never
240
+ reads as a wall of colored slabs. */
241
+ .fw-root .fw-view-agents .fw-bar.fw-k-pipeline:not(.fw-has-marks, .fw-has-agents) { background: color-mix(in oklab, var(--tone) 30%, var(--fw-surface)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--tone) 55%, transparent); }
242
+ /* A family row without one shared outcome is neutral when bars mean outcomes, not a hue of its own. */
243
+ .fw-root.fw-fill-outcome .fw-view-agents .fw-row[data-span-id^="family:"]:not([data-outcome]), .fw-root.fw-fill-outcome .fw-view-agents .fw-row[data-span-id^="family:"]:not([data-outcome]) [data-tone] { --tone: var(--fw-h0); }
238
244
  /* A host outcome re-tones the whole lane: dot, bar and marks. */
239
245
  /* Muted agents (the default): one quiet tone for every agent bar, its steps and handoffs; the slowest agent alone takes amber, and
240
246
  failure keeps its red glyph, so color only ever means state. */
@@ -265,7 +271,6 @@
265
271
  .fw-root .fw-seg-failed { background: color-mix(in oklab, var(--fw-failed) 18%, var(--fw-surface)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 70%, transparent); color: var(--fw-failed); }
266
272
  /* In the agent view a failed turn draws like any turn; the agent's own failure shows once, by its name and its end label. */
267
273
  .fw-root .fw-view-agents .fw-seg-failed { background: var(--dim); box-shadow: inset 0 0 0 1px var(--dim-line); }
268
- .fw-root .fw-view-agents .fw-alert { color: var(--fw-muted); }
269
274
  .fw-root .fw-glyph { display: inline-flex; color: currentColor; }
270
275
  .fw-root .fw-glyph svg { width: 11px; height: 11px; stroke-width: 2; }
271
276
  .fw-root .fw-seg-model .fw-glyph, .fw-root .fw-k-model .fw-glyph { color: light-dark(var(--tone), #fff); }
@@ -304,7 +309,10 @@
304
309
  .fw-root.fw-root-agents .fw-card, .fw-root.fw-root-agents .fw-card-above .fw-card { position: relative; left: auto !important; top: auto !important; transform: none; animation-name: fw-arrive; width: auto; max-width: 560px; }
305
310
  .fw-root .fw-seg-alt { filter: brightness(.78); }
306
311
  /* Agent view: the scrubber track shows how far the run has played, so at rest it reads full, not empty. */
307
- .fw-root-agents .fw-slider { background: linear-gradient(90deg, color-mix(in oklab, var(--fw-accent) 45%, transparent) var(--fw-progress), var(--fw-well) var(--fw-progress)) 0 50% / 100% 4px no-repeat; }
312
+ .fw-root.fw-root-agents .fw-slider { background: linear-gradient(90deg, var(--fw-accent) var(--fw-progress), var(--fw-well) var(--fw-progress)) 0 50% / 100% 6px no-repeat; }
313
+ /* The playhead: a round knob on a filled track, visible at rest as the references show it. */
314
+ .fw-root.fw-root-agents .fw-slider::-webkit-slider-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--fw-text); box-shadow: 0 0 0 3px var(--fw-surface); }
315
+ .fw-root.fw-root-agents .fw-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--fw-text); box-shadow: 0 0 0 3px var(--fw-surface); }
308
316
  .fw-root .fw-card { position: absolute; z-index: 20; width: 320px; padding: 12px; border-radius: 12px; background: var(--fw-raised); border: 1px solid var(--fw-border);
309
317
  box-shadow: 0 12px 32px -8px rgb(0 0 0 / .28), 0 2px 6px rgb(0 0 0 / .08); animation: fw-arrive 140ms var(--fw-ease); }
310
318
  /* A hover card never covers the next mark: only a pinned card (a dialog) takes the pointer. */
@@ -399,9 +399,9 @@ interface FlowWaterfallProps {
399
399
  markers?: FlowMarker[];
400
400
  /** The time the reader moved to, in trace ms; `null` when following live or showing the whole trace. */
401
401
  onTimeChange?: (t: number | null) => void;
402
- /** What an agent bar's color means. `muted` (default): every agent one quiet tone, with color spent on state only: the outcome on
403
- * the row dot, red for failure, amber for the slowest. `agent`: each agent its own hue, shared by its steps and handoffs. `outcome`: the
404
- * bar takes its outcome's color. */
402
+ /** What an agent bar's color means. Default: `outcome` when any agent carries one, so bars match the outcome legend, else `muted`.
403
+ * `muted`: every agent one quiet tone, color spent on state only (red for failure, amber for the slowest). `agent`: each agent its
404
+ * own hue, shared by its steps and handoffs. `outcome`: the bar takes its outcome's color. */
405
405
  agentFill?: 'muted' | 'outcome' | 'agent';
406
406
  /** One line beside the title saying what the run was for, such as its goal. */
407
407
  description?: string;
@@ -415,6 +415,6 @@ interface FlowWaterfallProps {
415
415
  }
416
416
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
417
417
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
418
- export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill, selectedId, onDeselect, description, summary: showSummary }: FlowWaterfallProps): import("react").JSX.Element;
418
+ export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary }: FlowWaterfallProps): import("react").JSX.Element;
419
419
  //#endregion
420
420
  export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, GapBand, GapProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
@@ -2631,8 +2631,8 @@ function slowestRow(rows, clockMs) {
2631
2631
  function agentCallouts(agents, clockMs) {
2632
2632
  let slowest, slowestMs = -1, costliest, top = -1, priced = 0, timed = 0;
2633
2633
  for (const row of agents) {
2634
- if (row.durationLabel === "—" || row.span.startMs > clockMs) continue;
2635
- if (row.kind === "pipeline" && row.parentId !== void 0) {
2634
+ if (row.durationLabel === "—" || row.span.startMs > clockMs || row.kind !== "pipeline" || row.id.startsWith("family:") || row.id.startsWith("run:")) continue;
2635
+ if (row.parentId !== void 0) {
2636
2636
  const ms = (spanRunning(row.span) ? Math.max(row.span.endMs, clockMs) : Math.min(row.span.endMs, clockMs)) - row.span.startMs;
2637
2637
  timed += 1;
2638
2638
  if (ms > slowestMs) {
@@ -2755,6 +2755,7 @@ function agentFamilies(agents, open) {
2755
2755
  const running = members.some((row) => spanRunning(row.span));
2756
2756
  const known = members.map((row) => row.span.costUsd ?? row.rollupCostUsd).filter((cost) => cost !== void 0);
2757
2757
  const failed = members.filter((row) => !row.ok).length;
2758
+ const outcome = members.every((row) => row.span.outcome && row.span.outcome.tone === members[0].span.outcome?.tone) ? members[0].span.outcome : void 0;
2758
2759
  out.push({
2759
2760
  costFloor: known.length < members.length || members.some((row) => row.costFloor),
2760
2761
  id,
@@ -2784,6 +2785,7 @@ function agentFamilies(agents, open) {
2784
2785
  startMs,
2785
2786
  endMs,
2786
2787
  status: running ? "running" : failed ? "failed" : "done",
2788
+ ...outcome ? { outcome } : {},
2787
2789
  detail: members[0].span.detail,
2788
2790
  meta: {
2789
2791
  failedAgents: failed,
@@ -3442,7 +3444,7 @@ const FlowRow = memo(function FlowRow(props) {
3442
3444
  "aria-label": `${row.descendants} hidden steps`,
3443
3445
  children: ["+", row.descendants]
3444
3446
  }),
3445
- !row.ok && /* @__PURE__ */ jsx("span", {
3447
+ !row.ok && !agentView && /* @__PURE__ */ jsx("span", {
3446
3448
  className: "fw-alert",
3447
3449
  children: /* @__PURE__ */ jsx(AlertGlyph, { label: "failed" })
3448
3450
  })
@@ -3733,6 +3735,7 @@ function FlowBody(props) {
3733
3735
  const ticks = clockTicks(win.startMs, tickEnd, Math.max(1, trackPx + (view === "spans" ? 72 : 0)));
3734
3736
  const markerLabels = useMemo(() => {
3735
3737
  const shown = /* @__PURE__ */ new Map();
3738
+ if (view === "agents") return shown;
3736
3739
  let freeFrom = -Infinity;
3737
3740
  for (const marker of [...props.markers ?? []].sort((a, b) => a.at - b.at)) {
3738
3741
  const x = (marker.at - win.startMs) / msPerPx;
@@ -3748,7 +3751,24 @@ function FlowBody(props) {
3748
3751
  props.markers,
3749
3752
  win.startMs,
3750
3753
  msPerPx,
3751
- trackPx
3754
+ trackPx,
3755
+ view
3756
+ ]);
3757
+ const pauseLabels = useMemo(() => {
3758
+ const shown = /* @__PURE__ */ new Set();
3759
+ let freeFrom = -Infinity;
3760
+ for (const pause of [...props.pauses ?? []].sort((a, b) => a.startMs - b.startMs)) {
3761
+ const width = duration(pause.endMs - pause.startMs).length * 7 + 24;
3762
+ const centre = ((pause.startMs + pause.endMs) / 2 - win.startMs) / msPerPx;
3763
+ if (centre - width / 2 < freeFrom) continue;
3764
+ shown.add(pause);
3765
+ freeFrom = centre + width / 2 + 8;
3766
+ }
3767
+ return shown;
3768
+ }, [
3769
+ props.pauses,
3770
+ win.startMs,
3771
+ msPerPx
3752
3772
  ]);
3753
3773
  useEffect(() => {
3754
3774
  const element = scroller.current;
@@ -4026,17 +4046,13 @@ function FlowBody(props) {
4026
4046
  "--s": pause.startMs,
4027
4047
  "--e": pause.endMs
4028
4048
  },
4029
- title: pause.label,
4030
- children: /* @__PURE__ */ jsxs("span", {
4049
+ title: `${pause.label}, ${duration(pause.endMs - pause.startMs)}`,
4050
+ children: pauseLabels.has(pause) && /* @__PURE__ */ jsx("span", {
4031
4051
  className: "fw-pause-label",
4032
- title: pause.detail,
4033
- tabIndex: pause.detail ? 0 : void 0,
4034
- "aria-label": pause.detail ? `${pause.label}: ${pause.detail}` : void 0,
4035
- children: [
4036
- /* @__PURE__ */ jsx("b", { children: duration(pause.endMs - pause.startMs) }),
4037
- " ",
4038
- pause.label
4039
- ]
4052
+ title: pause.detail ? `${pause.label}: ${pause.detail}` : pause.label,
4053
+ tabIndex: 0,
4054
+ "aria-label": `${pause.label}, ${duration(pause.endMs - pause.startMs)}${pause.detail ? `: ${pause.detail}` : ""}`,
4055
+ children: /* @__PURE__ */ jsx("b", { children: duration(pause.endMs - pause.startMs) })
4040
4056
  })
4041
4057
  }, `p-${pause.startMs}`)), props.markers?.map((marker) => /* @__PURE__ */ jsx("span", {
4042
4058
  className: "fw-marker",
@@ -4318,6 +4334,7 @@ function PlaybackBar({ view, playing, limit, clockMs, speed, zoom, marks, slider
4318
4334
  //#region src/react/flow-waterfall.tsx
4319
4335
  const LANE_PAD = 12;
4320
4336
  const AGENT_ROWS = 20;
4337
+ const UNFOLD_ROWS = 12;
4321
4338
  const END_LABEL_GUTTER = {
4322
4339
  spans: 120,
4323
4340
  agents: 176
@@ -4428,7 +4445,7 @@ function OutcomeLegend({ agents }) {
4428
4445
  }
4429
4446
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
4430
4447
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
4431
- function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill = "muted", selectedId, onDeselect, description, summary: showSummary = true }) {
4448
+ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary = true }) {
4432
4449
  const root = useRef(null);
4433
4450
  const search = useRef(null);
4434
4451
  const slider = useRef(null);
@@ -4450,6 +4467,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4450
4467
  const [laneElement, setLaneElement] = useState(null);
4451
4468
  const observedTotal = Number.isFinite(trace.totalMs) && trace.totalMs >= 0 ? trace.totalMs : 0;
4452
4469
  const isLive = live ?? trace.spans.some(spanRunning);
4470
+ const agentFill = fillProp ?? (trace.spans.some((span) => span.kind === "pipeline" && span.outcome) ? "outcome" : "muted");
4453
4471
  const dataEnd = useMemo(() => Math.max(observedTotal, ...trace.spans.map((span) => Number.isFinite(span.endMs) ? span.endMs : 0)), [trace, observedTotal]);
4454
4472
  const trackPx = Math.max(40, laneWidth - LANE_PAD - Math.min(END_LABEL_GUTTER[view], laneWidth * .3));
4455
4473
  useEffect(() => {
@@ -4503,7 +4521,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4503
4521
  }, [rows]);
4504
4522
  const sharedDetail = agents.length > 1 && agents.every((row) => row.span.detail && row.span.detail === agents[0]?.span.detail) ? agents[0]?.span.detail : void 0;
4505
4523
  const [flippedFamilies, setFlippedFamilies] = useState(/* @__PURE__ */ new Set());
4506
- const openFamilies = useMemo(() => defaultOpenFamilies(agents, AGENT_ROWS, flippedFamilies), [agents, flippedFamilies]);
4524
+ const openFamilies = useMemo(() => defaultOpenFamilies(agents, UNFOLD_ROWS, flippedFamilies), [agents, flippedFamilies]);
4507
4525
  const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
4508
4526
  const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed), [
4509
4527
  view,
@@ -4718,8 +4736,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4718
4736
  const cardAgent = cardRow && (() => {
4719
4737
  for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return shown(cursor);
4720
4738
  })();
4721
- const moving = isLive || position !== null || playing;
4722
- const showCursor = moving || view === "agents";
4739
+ const showCursor = isLive || position !== null || playing || view === "agents";
4723
4740
  const overviewMarks = useMemo(() => rows.filter((row) => !row.parentId).flatMap((row) => row.hasChildren ? row.segments : row.kind === "pipeline" ? [] : [{
4724
4741
  startMs: row.span.startMs,
4725
4742
  endMs: row.span.endMs,
@@ -4886,7 +4903,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4886
4903
  collapsed,
4887
4904
  searching: Boolean(query.trim()),
4888
4905
  pauses: trace.pauses,
4889
- cursorLine: moving,
4906
+ cursorLine: position !== null || playing,
4890
4907
  sharedDetail,
4891
4908
  expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
4892
4909
  holdHeight: isLive,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.21.5",
3
+ "version": "0.21.7",
4
4
  "description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
5
5
  "type": "module",
6
6
  "license": "MIT",