@tangle-network/charts 2.0.6 → 2.0.11

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.
package/README.md CHANGED
@@ -337,9 +337,9 @@ const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(cata
337
337
  <FlowWaterfall trace={trace} live={running} defaultView="agents" renderIcon={brandMark} />;
338
338
  ```
339
339
 
340
- `defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. `detail` becomes the agent's subtitle. The card is a table of what the row recorded, every value with its unit: model, duration, tokens in, out and cached (the span's `promptTokens`, `completionTokens` and `cachedTokens`, else the sum of its children's), LLM latency (median and p95 of its model calls), cost, LLM calls, tool calls by tool (the name before " · "), failed steps, and retries and restarts (`retryOf`, `marks` of kind `restart`); a value nobody recorded is left out. `facts` (`[{ label, value }]`) adds the host's rows after it. The card opens at the pointer, above or below its row, whichever side fits the viewport. `onTimeChange` reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (`costCoverage`), and `billedUsd` shows the real charge beside the list price. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
340
+ `defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. `detail` becomes the agent's subtitle. The card is a table of what the row recorded, every value with its unit: model, duration, tokens in, out and cached (the span's `promptTokens`, `completionTokens` and `cachedTokens`, else the sum of its children's), LLM latency (median and p95 of its model calls), cost, LLM calls, tool calls by tool (the name before " · "), failed steps, and retries and restarts (`retryOf`, `marks` of kind `restart`); a value nobody recorded is left out. `facts` (`[{ label, value }]`) supplies authoritative host rows: a matching label replaces the inferred metric, case-insensitively, including explicit unknown or zero values; additional labels append. The last explicit value wins for a repeated label. Agent lane summaries also use `Model calls` and `Tool calls` facts verbatim (`Model calls: 413`), including unknowns and zero; no count is parsed from a formatted fact. Model/Provider identity and Ran for/Cost end labels also honor facts verbatim; full values remain in the card and label title when phone metadata clips. Opaque duration/cost facts do not compete in numeric superlatives. Family lanes summarize agent and failure counts. Synthetic family cards show `Not recorded` for model identity, model/tool counts, model latency, failures or retries when any member supplies that authoritative fact: formatted counts and percentiles cannot be aggregated. Family tokens become unknown if a member supplies a Tokens fact without its own numeric usage. Cost rollups use full numeric span prices when a member supplies a Cost fact; partial known prices retain their lower bound, and an entirely opaque price is Not recorded. Strings and percentiles are never parsed or summed. The card opens at the pointer, above or below its row, whichever side fits the viewport. `onTimeChange` reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (`costCoverage`), and `billedUsd` shows the real charge beside the list price. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
341
341
 
342
- Live views append spans as they arrive: `traces watch <id|run-dir> --format jsonl --spans` emits a `traces.stream.span` event per new or changed span, and `otlpFlowTrace` keeps the last span per `span_id`. Running agent spans grow to the clock between deltas without React renders. `activityAt` supplies trace-relative event times and `medianEventGapMs` preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 44px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Token totals are optional: omit unknown counts, retain measured zero, and guard optional token fields when reading a trace. Missing, partial and nonfinite usage displays a dash, including during playback. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row `—` timing mark. The static phase waterfall remains a separate chart for disjoint phases.
342
+ Live views append spans as they arrive: `traces watch <id|run-dir> --format jsonl --spans` emits a `traces.stream.span` event per new or changed span, and `otlpFlowTrace` keeps the last span per `span_id`. Running agent spans grow to the clock between deltas without React renders. `activityAt` supplies trace-relative event times and `medianEventGapMs` preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 52px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Token totals are optional: omit unknown counts, retain measured zero, and guard optional token fields when reading a trace. Missing, partial and nonfinite usage displays a dash, including during playback. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row `—` timing mark. The static phase waterfall remains a separate chart for disjoint phases.
343
343
 
344
344
  `scripts/flow-waterfall-demo.mjs` serves the demo routes (reference-shaped fixtures, recorded runs, a synthetic load trace and OTLP files from `FW_DATA_DIR`); `scripts/flow-waterfall-capture.mjs` captures frames, hover cards, fps, first paint and recordings, and with `FW_LIVE_RELAY` records a live session through `scripts/flow-waterfall-live-relay.mjs` with every span's event-to-pixel time.
345
345
 
@@ -343,7 +343,7 @@ interface FlowSpan {
343
343
  handoffTo?: string;
344
344
  /** The host's verdict on an agent span, such as shipped or crashed: colors its lane and joins the legend. */
345
345
  outcome?: FlowOutcome;
346
- /** Host facts shown as extra rows of the card's table, such as releases or commits; a label the table already has is skipped. */
346
+ /** Authoritative host rows: replace same-label inferred card metrics and lane model/count/duration/cost labels, case-insensitively; unknowns stay unknown. */
347
347
  facts?: readonly {
348
348
  label: string;
349
349
  value: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-CF4Q07cA.js";
1
+ import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-VOGEAdl8.js";
2
2
  //#region src/block.d.ts
3
3
  interface RenderFigureOptions {
4
4
  /** Chart uses a closed data disclosure; plot keeps only the SVG and accessible data. */
@@ -271,7 +271,7 @@
271
271
  left: min(calc(var(--x0) + max(var(--w), 3px) + 4px), calc(100% - var(--fw-end-w, 0px) - 4px)); padding: 0 4px; border-radius: var(--radius-tag);
272
272
  background: linear-gradient(var(--fw-tint, transparent) 0 0), var(--fw-surface); white-space: nowrap; pointer-events: none; }
273
273
  .fw-root .fw-outcome-end { display: inline-flex; pointer-events: auto; }
274
- .fw-root .fw-duration { font-size: 14px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
274
+ .fw-root .fw-duration { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
275
275
  .fw-root .fw-k-failed ~ .fw-end .fw-duration { color: var(--fw-failed); }
276
276
  .fw-root .fw-badge { display: inline-flex; flex: none; height: 18px; padding: 0 6px; border-radius: var(--radius-pill); font-size: 12px; line-height: 18px; font-weight: 500; color: var(--fw-muted); background: transparent; box-shadow: inset 0 0 0 1px var(--fw-border); }
277
277
 
@@ -416,7 +416,7 @@
416
416
  .fw-root .fw-view-agents :is(.fw-subtitle, .fw-seg-label) { display: none; }
417
417
  .fw-root .fw-view-agents .fw-label { padding-left: calc(8px + var(--depth) * 12px); }
418
418
  .fw-root .fw-lane { grid-column: 2; }
419
- .fw-root .fw-end { top: 20px; height: 18px; left: auto; right: 4px; }
419
+ .fw-root .fw-end { top: 20px; height: 18px; left: auto; right: 4px; max-width: calc(100% - 144px); }
420
420
  .fw-root .fw-time { display: block; grid-column: 2; position: absolute; top: -12px; left: var(--fw-progress); transform: translateX(calc(-1 * var(--fw-progress))); width: max-content; max-width: 100%; background: var(--fw-surface); }
421
421
  .fw-root .fw-playback { position: relative; padding: 12px 8px 8px; }
422
422
  .fw-root .fw-overview { margin-top: 12px; grid-template-columns: 0 minmax(0, 1fr); column-gap: 0; }
@@ -1,4 +1,4 @@
1
- import { B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, O as YTarget, S as ChartMark, T as MarkerShape, a as TimelineLane, b as BandKind, c as TimelineTick, g as TickOutcome, i as FlowTrace, k as YTick, n as FlowOutcome, o as TimelineOptions, r as FlowSpan, s as TimelineSpan, t as FlowMarker, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph } from "../flow-trace-CF4Q07cA.js";
1
+ import { B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, O as YTarget, S as ChartMark, T as MarkerShape, a as TimelineLane, b as BandKind, c as TimelineTick, g as TickOutcome, i as FlowTrace, k as YTick, n as FlowOutcome, o as TimelineOptions, r as FlowSpan, s as TimelineSpan, t as FlowMarker, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph } from "../flow-trace-VOGEAdl8.js";
2
2
  import { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  //#region src/react/sparkline.d.ts
4
4
  type SparklineDirection = "rising" | "falling" | "flat";
@@ -223,7 +223,11 @@ interface MeterSegment {
223
223
  }
224
224
  /** A used-of-total meter with its numbers. With `segments` it is a part-of-whole bar: the remainder of `total` that no
225
225
  * segment accounts for is drawn hatched and labelled `untraced`, and a missing total hatches the whole track. */
226
- export declare function Meter({ used, total, format, tone, text, segments, untraced }: {
226
+ export declare function Meter({ used, total, format, tone, text, segments, steps, untraced }: {
227
+ steps?: {
228
+ label: string;
229
+ state: 'pass' | 'fail' | 'undecided' | 'unrun';
230
+ }[];
227
231
  used?: number | null | undefined;
228
232
  total: number | null | undefined;
229
233
  format?: (v: number) => string;
@@ -260,7 +264,11 @@ export declare function Events({ at, from, to, label, color, ticks, axis, time }
260
264
  * state; hovering a span names it, its state and its span. Times are epoch ms; `axis` labels the time axis.
261
265
  * `lanes` give a row several spans (the same shape as the static `timeline`); a span with `end: null` is open and runs
262
266
  * to `now`. `marks` draw recorded events across every lane, and states in `hatch` are drawn hatched (no data). */
263
- export declare function Gantt({ rows, lanes: laneInput, colors, axis, span, marks, now, hatch }: {
267
+ export declare function Gantt({ rows, lanes: laneInput, colors, axis, span, marks, now, hatch, selectedId, onSelect, bestMark, maxHeight }: {
268
+ selectedId?: string;
269
+ onSelect?: (id: string) => void;
270
+ bestMark?: string;
271
+ maxHeight?: number;
264
272
  rows?: {
265
273
  label: string;
266
274
  start: number;
@@ -1385,7 +1385,19 @@ const SEGMENT_TONES = [
1385
1385
  ];
1386
1386
  /** A used-of-total meter with its numbers. With `segments` it is a part-of-whole bar: the remainder of `total` that no
1387
1387
  * segment accounts for is drawn hatched and labelled `untraced`, and a missing total hatches the whole track. */
1388
- function Meter({ used, total, format = formatCompact, tone, text, segments, untraced = "untraced" }) {
1388
+ function Meter({ used, total, format = formatCompact, tone, text, segments, steps, untraced = "untraced" }) {
1389
+ if (steps) {
1390
+ const current = steps.findIndex((step) => step.state !== "pass");
1391
+ return /* @__PURE__ */ jsx("ol", {
1392
+ className: "ov-meter-steps",
1393
+ "aria-label": text ?? "Check steps",
1394
+ children: steps.map((step, i) => /* @__PURE__ */ jsxs("li", {
1395
+ "data-state": step.state,
1396
+ "aria-current": i === current ? "step" : void 0,
1397
+ children: [/* @__PURE__ */ jsx("strong", { children: i + 1 }), /* @__PURE__ */ jsx("span", { children: step.label })]
1398
+ }, i))
1399
+ });
1400
+ }
1389
1401
  if (segments) return /* @__PURE__ */ jsx(SegmentMeter, {
1390
1402
  segments,
1391
1403
  total,
@@ -1586,9 +1598,11 @@ function Events({ at, from, to, label, color = LIVE_SERIES[0], ticks = 7, axis =
1586
1598
  * state; hovering a span names it, its state and its span. Times are epoch ms; `axis` labels the time axis.
1587
1599
  * `lanes` give a row several spans (the same shape as the static `timeline`); a span with `end: null` is open and runs
1588
1600
  * to `now`. `marks` draw recorded events across every lane, and states in `hatch` are drawn hatched (no data). */
1589
- function Gantt({ rows, lanes: laneInput, colors, axis, span, marks = [], now, hatch = ["missing", "untraced"] }) {
1601
+ function Gantt({ rows, lanes: laneInput, colors, axis, span, marks = [], now, hatch = ["missing", "untraced"], selectedId, onSelect, bestMark, maxHeight }) {
1590
1602
  const [ref, width, em] = useWidth();
1591
1603
  const [tip, setTip] = useState(null);
1604
+ const scroller = useRef(null);
1605
+ const [scrollTop, setScrollTop] = useState(0);
1592
1606
  const lanes = laneInput ?? (rows ?? []).map((row, i) => ({
1593
1607
  id: `${row.label}-${i}`,
1594
1608
  label: row.label,
@@ -1612,9 +1626,21 @@ function Gantt({ rows, lanes: laneInput, colors, axis, span, marks = [], now, ha
1612
1626
  children: "No agent has started."
1613
1627
  });
1614
1628
  const ends = (s) => s.end ?? now;
1615
- const rowH = Math.round(1.25 * em);
1616
- const labelW = Math.min(Math.round(13 * em), Math.round(width * .32));
1617
- const top = marks.length ? 4 + Math.round(Math.min(3, marks.length) * em + .3 * em) : 4;
1629
+ const phone = width < 600;
1630
+ const rowH = Math.round((phone ? 2.8 : 2.2) * em);
1631
+ const labelW = phone ? 8 : Math.min(Math.round(16 * em), Math.round(width * .28));
1632
+ const top = maxHeight ? 4 : marks.length ? 48 : 8;
1633
+ const fractions = phone ? [
1634
+ 0,
1635
+ .5,
1636
+ 1
1637
+ ] : [
1638
+ 0,
1639
+ .25,
1640
+ .5,
1641
+ .75,
1642
+ 1
1643
+ ];
1618
1644
  const bottom = Math.round(1.8 * em);
1619
1645
  const height = top + lanes.length * rowH + bottom;
1620
1646
  const times = [
@@ -1629,131 +1655,233 @@ function Gantt({ rows, lanes: laneInput, colors, axis, span, marks = [], now, ha
1629
1655
  const fallback = colors.unknown ?? "var(--chart-live-faint, var(--chart-muted))";
1630
1656
  const hatched = new Set(hatch);
1631
1657
  const plotBottom = top + lanes.length * rowH;
1658
+ const first = maxHeight && lanes.length > 100 ? Math.max(0, Math.floor((scrollTop - top) / rowH) - 6) : 0;
1659
+ const visible = maxHeight && lanes.length > 100 ? lanes.slice(first, first + Math.ceil(maxHeight / rowH) + 14) : lanes;
1660
+ const select = (id) => onSelect?.(id);
1661
+ const focusLane = (index) => {
1662
+ const at = Math.max(0, Math.min(lanes.length - 1, index));
1663
+ const position = at === 0 ? 0 : top + at * rowH - rowH;
1664
+ if (scroller.current) scroller.current.scrollTop = position;
1665
+ setScrollTop(Math.max(0, position));
1666
+ requestAnimationFrame(() => scroller.current?.querySelector(`[data-lane-index="${at}"]`)?.focus());
1667
+ };
1668
+ const tipY = (y) => Math.max(0, y - (maxHeight ? scrollTop - 52 : 0) - em);
1669
+ const tipX = (x) => Math.max(0, Math.min(x + 8, width - Math.min(240, width)));
1632
1670
  return /* @__PURE__ */ jsxs("div", {
1633
1671
  ref,
1634
- className: "ov-plot",
1672
+ className: "ov-plot ov-gantt",
1673
+ "data-featured": bestMark ? true : void 0,
1635
1674
  onMouseLeave: () => setTip(null),
1636
1675
  children: [
1637
- width > 0 && /* @__PURE__ */ jsxs("svg", {
1676
+ maxHeight && width > 0 && /* @__PURE__ */ jsxs("svg", {
1638
1677
  width,
1639
- height,
1640
- role: "img",
1641
- "aria-label": `${lanes.length} ${laneInput ? "lanes" : "agents"} over time`,
1642
- children: [
1643
- [
1644
- 0,
1645
- .25,
1646
- .5,
1647
- .75,
1648
- 1
1649
- ].map((f) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1650
- x1: labelW + f * plotW,
1651
- x2: labelW + f * plotW,
1652
- y1: top,
1653
- y2: plotBottom,
1654
- className: "ov-gridline"
1655
- }), /* @__PURE__ */ jsx("text", {
1656
- x: labelW + f * plotW,
1657
- y: height - em * .35,
1658
- className: "ov-axis",
1659
- textAnchor: f === 0 ? "start" : f === 1 ? "end" : "middle",
1660
- children: axis(t0 + f * (t1 - t0))
1661
- })] }, f)),
1662
- lanes.map((lane, i) => {
1663
- const y = top + i * rowH;
1664
- return /* @__PURE__ */ jsxs("g", {
1665
- "data-lane": lane.id,
1666
- children: [
1667
- /* @__PURE__ */ jsx("rect", {
1668
- x: 0,
1669
- y,
1670
- width,
1671
- height: rowH,
1672
- fill: "transparent",
1673
- onMouseEnter: () => setTip({
1674
- x: Math.min(x(ends(lane.spans.at(-1) ?? { end: t1 })) + 8, width - em * 16),
1675
- y: Math.max(0, y - em),
1676
- title: lane.label,
1677
- lines: lane.spans.flatMap((s) => [{
1678
- label: s.end === null ? `${s.state} (open)` : s.state,
1679
- value: span(s.start, ends(s)),
1680
- color: hatched.has(s.state) ? void 0 : colors[s.state] ?? fallback
1681
- }, ...s.note ? [{
1682
- label: s.note,
1683
- value: ""
1684
- }] : []])
1685
- })
1686
- }),
1687
- /* @__PURE__ */ jsx("text", {
1688
- x: labelW - em * .5,
1689
- y: y + rowH / 2,
1690
- dominantBaseline: "middle",
1691
- textAnchor: "end",
1692
- className: "ov-axis ov-gantt-label",
1693
- children: lane.label.length > 26 ? `${lane.label.slice(0, 25)}…` : lane.label
1694
- }),
1695
- lane.spans.map((s, j) => {
1696
- const color = colors[s.state] ?? fallback;
1697
- const bx = x(s.start), bw = Math.max(2, x(ends(s)) - bx), by = y + rowH * .18, bh = rowH * .64;
1698
- const open = s.end === null;
1699
- return /* @__PURE__ */ jsx("g", {
1700
- "data-state": s.state,
1701
- "data-open": open ? "true" : void 0,
1702
- "data-hatched": hatched.has(s.state) ? "true" : void 0,
1678
+ height: 52,
1679
+ "aria-label": "Releases and time axis",
1680
+ children: [marks.map((mark, i) => /* @__PURE__ */ jsxs("g", {
1681
+ "data-best": mark.label === bestMark || void 0,
1682
+ children: [/* @__PURE__ */ jsx(Mark, {
1683
+ mark,
1684
+ x: x(mark.at),
1685
+ top: 10,
1686
+ bottom: 28,
1687
+ row: 0,
1688
+ em,
1689
+ flip: x(mark.at) > width * .62
1690
+ }), mark.label === bestMark && /* @__PURE__ */ jsx("circle", {
1691
+ cx: x(mark.at),
1692
+ cy: 14,
1693
+ r: 8,
1694
+ fill: "none",
1695
+ stroke: "var(--accent-text)",
1696
+ strokeWidth: 2
1697
+ })]
1698
+ }, i)), fractions.map((f) => /* @__PURE__ */ jsx("text", {
1699
+ x: labelW + f * plotW,
1700
+ y: 46,
1701
+ className: "ov-axis",
1702
+ textAnchor: f === 0 ? "start" : f === 1 ? "end" : "middle",
1703
+ children: axis(t0 + f * (t1 - t0))
1704
+ }, f))]
1705
+ }),
1706
+ /* @__PURE__ */ jsx("div", {
1707
+ ref: scroller,
1708
+ className: "ov-gantt-scroll",
1709
+ style: {
1710
+ maxHeight,
1711
+ overflow: maxHeight ? "auto" : void 0
1712
+ },
1713
+ onScroll: (event) => {
1714
+ setScrollTop(event.currentTarget.scrollTop);
1715
+ setTip(null);
1716
+ },
1717
+ children: width > 0 && /* @__PURE__ */ jsxs("svg", {
1718
+ width,
1719
+ height,
1720
+ role: onSelect ? "group" : "img",
1721
+ "aria-label": `${lanes.length} ${laneInput ? "lanes" : "agents"} over time`,
1722
+ children: [
1723
+ fractions.map((f) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1724
+ x1: labelW + f * plotW,
1725
+ x2: labelW + f * plotW,
1726
+ y1: top,
1727
+ y2: plotBottom,
1728
+ className: "ov-gridline"
1729
+ }), !maxHeight && /* @__PURE__ */ jsx("text", {
1730
+ x: labelW + f * plotW,
1731
+ y: height - em * .35,
1732
+ className: "ov-axis",
1733
+ textAnchor: f === 0 ? "start" : f === 1 ? "end" : "middle",
1734
+ children: axis(t0 + f * (t1 - t0))
1735
+ })] }, f)),
1736
+ visible.map((lane, offset) => {
1737
+ const i = first + offset, y = top + i * rowH;
1738
+ return /* @__PURE__ */ jsxs("g", {
1739
+ "data-lane": lane.id,
1740
+ "data-lane-index": i,
1741
+ role: onSelect ? "button" : void 0,
1742
+ tabIndex: onSelect ? 0 : void 0,
1743
+ "aria-label": lane.label,
1744
+ "aria-pressed": onSelect ? selectedId === lane.id : void 0,
1745
+ onClick: () => select(lane.id),
1746
+ onKeyDown: (event) => {
1747
+ if (event.key === "Enter" || event.key === " ") {
1748
+ event.preventDefault();
1749
+ select(lane.id);
1750
+ }
1751
+ const next = {
1752
+ ArrowDown: i + 1,
1753
+ ArrowUp: i - 1,
1754
+ Home: 0,
1755
+ End: lanes.length - 1
1756
+ }[event.key];
1757
+ if (next !== void 0) {
1758
+ event.preventDefault();
1759
+ focusLane(next);
1760
+ }
1761
+ },
1762
+ children: [
1763
+ selectedId === lane.id && /* @__PURE__ */ jsx("rect", {
1764
+ x: 0,
1765
+ y,
1766
+ width,
1767
+ height: rowH,
1768
+ fill: "var(--ix-sel-bg)",
1769
+ rx: 4
1770
+ }),
1771
+ /* @__PURE__ */ jsx("rect", {
1772
+ x: 0,
1773
+ y,
1774
+ width,
1775
+ height: rowH,
1776
+ fill: "transparent",
1703
1777
  onMouseEnter: () => setTip({
1704
- x: Math.min(bx + bw + 8, width - em * 16),
1705
- y: Math.max(0, y - em),
1778
+ x: tipX(x(ends(lane.spans.at(-1) ?? { end: t1 }))),
1779
+ y: tipY(y),
1706
1780
  title: lane.label,
1707
- lines: [{
1708
- label: open ? `${s.state} (open)` : s.state,
1781
+ lines: lane.spans.flatMap((s) => [{
1782
+ label: s.end === null ? `${s.state} (open)` : s.state,
1709
1783
  value: span(s.start, ends(s)),
1710
- color: hatched.has(s.state) ? void 0 : color
1784
+ color: hatched.has(s.state) ? void 0 : colors[s.state] ?? fallback
1711
1785
  }, ...s.note ? [{
1712
1786
  label: s.note,
1713
1787
  value: ""
1714
- }] : []]
1715
- }),
1716
- children: hatched.has(s.state) ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
1717
- x: bx,
1718
- y: by,
1719
- width: bw,
1720
- height: bh,
1721
- className: "ov-hatch-bg"
1722
- }), /* @__PURE__ */ jsx("path", {
1723
- d: hatchPath(bx, by, bw, bh, 4),
1724
- className: "ov-hatch"
1725
- })] }) : /* @__PURE__ */ jsx("rect", {
1726
- x: bx,
1727
- y: by,
1728
- width: bw,
1729
- height: bh,
1730
- rx: 2,
1731
- fill: color,
1732
- className: open ? "ov-gantt-open" : void 0
1788
+ }] : []])
1733
1789
  })
1734
- }, j);
1790
+ }),
1791
+ /* @__PURE__ */ jsxs("text", {
1792
+ x: phone ? 8 : labelW - em * .5,
1793
+ y: phone ? y + 13 : y + rowH / 2,
1794
+ dominantBaseline: "middle",
1795
+ textAnchor: phone ? "start" : "end",
1796
+ className: "ov-axis ov-gantt-label",
1797
+ children: [/* @__PURE__ */ jsx("title", { children: lane.label }), lane.label.length > 26 ? `${lane.label.slice(0, 25)}…` : lane.label]
1798
+ }),
1799
+ lane.spans.map((s, j) => {
1800
+ const color = colors[s.state] ?? fallback;
1801
+ const bx = x(s.start), bw = Math.max(2, x(ends(s)) - bx), by = y + (phone ? 24 : rowH * .25), bh = rowH * .5;
1802
+ const open = s.end === null;
1803
+ return /* @__PURE__ */ jsx("g", {
1804
+ "data-state": s.state,
1805
+ "data-open": open ? "true" : void 0,
1806
+ "data-hatched": hatched.has(s.state) ? "true" : void 0,
1807
+ onMouseEnter: () => setTip({
1808
+ x: tipX(bx + bw),
1809
+ y: tipY(y),
1810
+ title: lane.label,
1811
+ lines: [{
1812
+ label: open ? `${s.state} (open)` : s.state,
1813
+ value: span(s.start, ends(s)),
1814
+ color: hatched.has(s.state) ? void 0 : color
1815
+ }, ...s.note ? [{
1816
+ label: s.note,
1817
+ value: ""
1818
+ }] : []]
1819
+ }),
1820
+ children: hatched.has(s.state) ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
1821
+ x: bx,
1822
+ y: by,
1823
+ width: bw,
1824
+ height: bh,
1825
+ className: "ov-hatch-bg"
1826
+ }), /* @__PURE__ */ jsx("path", {
1827
+ d: hatchPath(bx, by, bw, bh, 4),
1828
+ className: "ov-hatch"
1829
+ })] }) : /* @__PURE__ */ jsx("rect", {
1830
+ x: bx,
1831
+ y: by,
1832
+ width: bw,
1833
+ height: bh,
1834
+ rx: 2,
1835
+ fill: color,
1836
+ className: open ? "ov-gantt-open" : void 0
1837
+ })
1838
+ }, j);
1839
+ })
1840
+ ]
1841
+ }, lane.id);
1842
+ }),
1843
+ now !== void 0 && /* @__PURE__ */ jsx("line", {
1844
+ x1: x(now),
1845
+ x2: x(now),
1846
+ y1: top,
1847
+ y2: plotBottom,
1848
+ className: "ov-now",
1849
+ "data-now": "true"
1850
+ }),
1851
+ [...marks].sort((a, b) => a.at - b.at).map((mark, i) => /* @__PURE__ */ jsxs("g", {
1852
+ className: mark.verdict ? `ov-verdict-${mark.verdict}` : void 0,
1853
+ "data-best": mark.label === bestMark || void 0,
1854
+ style: maxHeight ? { pointerEvents: "none" } : void 0,
1855
+ children: [
1856
+ maxHeight && /* @__PURE__ */ jsx("line", {
1857
+ x1: x(mark.at),
1858
+ x2: x(mark.at),
1859
+ y1: top,
1860
+ y2: plotBottom,
1861
+ className: `ov-mark ov-mark-${mark.kind}`,
1862
+ style: { opacity: mark.label === bestMark ? .8 : .3 }
1863
+ }),
1864
+ !maxHeight && /* @__PURE__ */ jsx(Mark, {
1865
+ mark,
1866
+ x: x(mark.at),
1867
+ top: 4,
1868
+ bottom: plotBottom,
1869
+ row: i % 2,
1870
+ em,
1871
+ flip: x(mark.at) > labelW + plotW * .62
1872
+ }),
1873
+ !maxHeight && mark.label === bestMark && /* @__PURE__ */ jsx("circle", {
1874
+ cx: x(mark.at),
1875
+ cy: 4 + Math.max(3, em / 4),
1876
+ r: 8,
1877
+ fill: "none",
1878
+ stroke: "var(--accent-text)",
1879
+ strokeWidth: 2
1735
1880
  })
1736
1881
  ]
1737
- }, lane.id);
1738
- }),
1739
- now !== void 0 && /* @__PURE__ */ jsx("line", {
1740
- x1: x(now),
1741
- x2: x(now),
1742
- y1: top,
1743
- y2: plotBottom,
1744
- className: "ov-now",
1745
- "data-now": "true"
1746
- }),
1747
- [...marks].sort((a, b) => a.at - b.at).map((mark, i) => /* @__PURE__ */ jsx(Mark, {
1748
- mark,
1749
- x: x(mark.at),
1750
- top: 4,
1751
- bottom: plotBottom,
1752
- row: i % 3,
1753
- em,
1754
- flip: x(mark.at) > labelW + plotW * .62
1755
- }, `mark-${i}`))
1756
- ]
1882
+ }, `mark-${i}`))
1883
+ ]
1884
+ })
1757
1885
  }),
1758
1886
  /* @__PURE__ */ jsx(Tooltip, { tip }),
1759
1887
  /* @__PURE__ */ jsx(EventList, {
@@ -6500,6 +6628,7 @@ const toolOf = (name) => name?.split(" · ")[0];
6500
6628
  const toneKey = (tone) => tone > 0 && tone <= 5 ? tone : 0;
6501
6629
  const spanFailed = (span) => span.status === "failed";
6502
6630
  const spanRunning = (span) => span.status === "running";
6631
+ const spanFact = (span, label) => new Map((span.facts ?? []).map((fact) => [fact.label.toLowerCase(), fact.value])).get(label.toLowerCase());
6503
6632
  const ordered = (span) => Number.isFinite(span.startMs) && Number.isFinite(span.endMs) && span.endMs >= span.startMs;
6504
6633
  /** 542ms, 6.9s, 23s, 1m 22s, 2h 05m: the precision a reader compares at each scale. */
6505
6634
  function formatSpanDuration(ms) {
@@ -6570,9 +6699,10 @@ function flowWaterfallRows(trace, resolutionMs) {
6570
6699
  tone: state === "running" ? "live" : state === "done" ? "ok" : state === "needs-you" ? "warn" : state === "failed" ? "failed" : "neutral",
6571
6700
  ...state === "unknown" ? { ring: true } : {}
6572
6701
  };
6573
- const span = recorded.outcome ? recorded : {
6702
+ const span = {
6574
6703
  ...recorded,
6575
- outcome
6704
+ outcome: recorded.outcome ?? outcome,
6705
+ detail: spanFact(recorded, recorded.kind === "tool" ? "Provider" : "Model") ?? recorded.detail
6576
6706
  };
6577
6707
  return {
6578
6708
  span,
@@ -6825,12 +6955,14 @@ function agentCallouts(agents, clockMs) {
6825
6955
  for (const row of agents) {
6826
6956
  if (row.durationLabel === "—" || row.span.startMs > clockMs || row.kind !== "pipeline" || row.hasAgents || row.id.startsWith("family:") || row.id.startsWith("run:")) continue;
6827
6957
  const ms = (spanRunning(row.span) ? Math.max(row.span.endMs, clockMs) : Math.min(row.span.endMs, clockMs)) - row.span.startMs;
6828
- timed += 1;
6829
- if (ms > slowestMs) {
6830
- slowest = row;
6831
- slowestMs = ms;
6958
+ if (spanFact(row.span, "Ran for") === void 0) {
6959
+ timed += 1;
6960
+ if (ms > slowestMs) {
6961
+ slowest = row;
6962
+ slowestMs = ms;
6963
+ }
6832
6964
  }
6833
- const cost = row.span.costUsd ?? row.rollupCostUsd;
6965
+ const cost = spanFact(row.span, "Cost") === void 0 ? row.span.costUsd ?? row.rollupCostUsd : void 0;
6834
6966
  if (typeof cost !== "number" || !Number.isFinite(cost)) continue;
6835
6967
  priced += 1;
6836
6968
  if (cost > top) {
@@ -6949,9 +7081,24 @@ function agentFamilies(agents, open) {
6949
7081
  const startMs = Math.min(...members.map((row) => row.span.startMs));
6950
7082
  const endMs = Math.max(...members.map((row) => row.span.endMs));
6951
7083
  const running = members.some((row) => spanRunning(row.span));
6952
- const known = members.map((row) => row.span.costUsd ?? row.rollupCostUsd).filter((cost) => cost !== void 0);
7084
+ const known = members.map((row) => row.span.costUsd ?? (spanFact(row.span, "Cost") === void 0 ? row.rollupCostUsd : void 0)).filter((cost) => cost !== void 0 && Number.isFinite(cost));
6953
7085
  const failed = members.filter((row) => row.span.outcome ? row.span.outcome.tone === "failed" : !row.ok).length;
6954
7086
  const outcome = members.every((row) => row.span.outcome && row.span.outcome.tone === members[0].span.outcome?.tone && !row.span.outcome.ring === !members[0].span.outcome?.ring) ? members[0].span.outcome : void 0;
7087
+ const facts = members.flatMap((row) => (row.span.facts ?? []).filter(({ label }) => [
7088
+ "model",
7089
+ "model calls",
7090
+ "tool calls",
7091
+ "model latency",
7092
+ "failures",
7093
+ "retries"
7094
+ ].includes(label.toLowerCase()) || label.toLowerCase() === "tokens" && ![
7095
+ row.span.promptTokens,
7096
+ row.span.completionTokens,
7097
+ row.span.cachedTokens
7098
+ ].some(Number.isFinite) || label.toLowerCase() === "cost" && known.length === 0)).map(({ label }) => ({
7099
+ label,
7100
+ value: "Not recorded"
7101
+ }));
6955
7102
  out.push({
6956
7103
  costFloor: known.length < members.length || members.some((row) => row.costFloor),
6957
7104
  id,
@@ -6992,7 +7139,8 @@ function agentFamilies(agents, open) {
6992
7139
  activityAt: members.flatMap((row) => row.span.activityAt ?? row.own.flatMap((mark) => [mark.startMs, mark.endMs])),
6993
7140
  status: running ? "running" : failed ? "failed" : "done",
6994
7141
  ...outcome ? { outcome } : {},
6995
- detail: members[0].span.detail,
7142
+ detail: members.every((row) => row.span.detail === members[0].span.detail) ? members[0].span.detail : void 0,
7143
+ facts,
6996
7144
  meta: {
6997
7145
  failedAgents: failed,
6998
7146
  costPartial: known.length < members.length,
@@ -7025,13 +7173,17 @@ function agentSummary(row) {
7025
7173
  const failed = row.own.filter((mark) => mark.kind === "failed").length;
7026
7174
  return [failed ? `${failed} failed` : "all done"];
7027
7175
  }
7028
- const calls = row.counts?.model ?? 0;
7029
- const tools = row.counts?.tool ?? 0;
7030
- return [
7031
- row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : !row.ok ? "failed" : row.kind === "pipeline" ? "" : "done"),
7032
- calls ? `${calls} model ${calls === 1 ? "call" : "calls"}` : "",
7033
- tools ? `${tools} tool ${tools === 1 ? "call" : "calls"}` : ""
7034
- ].filter(Boolean);
7176
+ const facts = new Map((row.span.facts ?? []).map(({ label, value }) => [label.toLowerCase(), value]));
7177
+ const calls = [[
7178
+ "Model calls",
7179
+ row.counts?.model,
7180
+ "model"
7181
+ ], [
7182
+ "Tool calls",
7183
+ row.counts?.tool,
7184
+ "tool"
7185
+ ]].map(([label, count, kind]) => facts.has(label.toLowerCase()) ? `${label}: ${facts.get(label.toLowerCase())}` : count ? `${count} ${kind} ${count === 1 ? "call" : "calls"}` : "");
7186
+ return [row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : !row.ok ? "failed" : row.kind === "pipeline" ? "" : "done"), ...calls].filter(Boolean);
7035
7187
  }
7036
7188
  /**
7037
7189
  * The agent view's rows: families folded until opened, and an opened agent's own steps (its turns and tool calls, not its
@@ -7115,7 +7267,7 @@ function drawnRow(row, visible) {
7115
7267
  } : row;
7116
7268
  }
7117
7269
  /** A row's cost as printed: "≥" when it is a floor. */
7118
- const rowCostText = (row, cost) => `${row.costFloor ? "≥" : ""}${formatCost(cost)}`;
7270
+ const rowCostText = (row, cost) => spanFact(row.span, "Cost") ?? (cost === void 0 ? "" : `${row.costFloor ? "≥" : ""}${formatCost(cost)}`);
7119
7271
  /** Quiet is elapsed silence beyond both five minutes and three median event gaps. */
7120
7272
  function quietActivity(row, now) {
7121
7273
  const times = [...row.span.activityAt ?? row.own.flatMap((mark) => [mark.startMs, mark.endMs])].filter(Number.isFinite).sort((a, b) => a - b);
@@ -7346,8 +7498,8 @@ function metrics(row, clockMs, full) {
7346
7498
  const restarts = span.marks?.filter((mark) => mark.kind === "restart").length ?? 0;
7347
7499
  if (counts?.retried || restarts) out.push(["Retries", [counts?.retried ? plural(counts.retried, "step retried", "steps retried") : "", restarts ? plural(restarts, "restart") : ""].filter(Boolean).join(" · ")]);
7348
7500
  if (full && row.hasChildren) out.push(["Steps", row.descendants.toLocaleString()]);
7349
- for (const fact of span.facts ?? []) if (!out.some(([label]) => label.toLowerCase() === fact.label.toLowerCase())) out.push([fact.label, fact.value]);
7350
- return out;
7501
+ const facts = (span.facts ?? []).map(({ label, value }) => [label, value]);
7502
+ return [...new Map([...out, ...facts].map((row) => [row[0].toLowerCase(), row])).values()];
7351
7503
  }
7352
7504
  function Table({ rows }) {
7353
7505
  return /* @__PURE__ */ jsx("dl", {
@@ -7358,7 +7510,7 @@ function Table({ rows }) {
7358
7510
  /** The span card: what ran, for how long, on what tokens and at what recorded cost, then its input and output. */
7359
7511
  function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderIcon, compact }) {
7360
7512
  const span = row.span;
7361
- const failure = span.facts?.find((fact) => fact.label.toLowerCase() === "failure")?.value;
7513
+ const failure = spanFact(span, "Failure");
7362
7514
  const state = runStatus(span.status ?? "unknown");
7363
7515
  const meta = statusMeta(state);
7364
7516
  const status = meta.label;
@@ -7572,11 +7724,8 @@ const open = (row, clockMs) => spanRunning(row.span) || row.span.startMs <= cloc
7572
7724
  function endLabel(row, clockMs) {
7573
7725
  const span = row.span;
7574
7726
  if (row.durationLabel === "—") return "—";
7575
- if (row.approx && span.endMs === span.startMs && !spanRunning(span)) return span.costUsd === void 0 ? "" : rowCostText(row, span.costUsd);
7576
- if (!spanRunning(span) && clockMs < span.endMs) return `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}`;
7577
- const duration = spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel;
7578
- const cost = span.costUsd ?? (row.rollupCostUsd ? row.rollupCostUsd : void 0);
7579
- return cost === void 0 ? duration : `${duration} · ${rowCostText(row, cost)}`;
7727
+ if (row.approx && span.endMs === span.startMs && !spanRunning(span)) return rowCostText(row, span.costUsd);
7728
+ return [spanFact(span, "Ran for") ?? (!spanRunning(span) && clockMs < span.endMs ? `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}` : spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel), !spanRunning(span) && clockMs < span.endMs && spanFact(span, "Cost") === void 0 ? "" : rowCostText(row, span.costUsd ?? row.rollupCostUsd)].filter(Boolean).join(" · ");
7580
7729
  }
7581
7730
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
7582
7731
  const FlowRow = memo(function FlowRow(props) {
@@ -7826,6 +7975,7 @@ const FlowRow = memo(function FlowRow(props) {
7826
7975
  })
7827
7976
  }), /* @__PURE__ */ jsx("span", {
7828
7977
  className: "fw-duration",
7978
+ title: label,
7829
7979
  children: label
7830
7980
  })]
7831
7981
  })
@@ -9301,7 +9451,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9301
9451
  const errorEnds = Array.from({ length: errors ?? 0 }, (_, index) => Number.isFinite(errorSpans[index]?.endMs) ? errorSpans[index].endMs : void 0);
9302
9452
  const done = !isLive && !trace.spans.some(spanRunning);
9303
9453
  const shown = (row) => row && drawnRow(row, visible);
9304
- const cardRow = shown(card ? byId.get(card.target) : void 0);
9454
+ const cardRow = shown(card ? byId.get(card.target) ?? visible.find((row) => row.id === card.target) : void 0);
9305
9455
  const cardAgent = cardRow && (() => {
9306
9456
  for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return shown(cursor);
9307
9457
  })();
@@ -97,3 +97,28 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
97
97
  .ov-gap-stretch { fill: none; stroke: var(--chart-live-pass, #2f9e6e); stroke-width: 1; stroke-dasharray: 3 3; }
98
98
  .ov-gap-span { stroke-width: 2; stroke-dasharray: 2 4; stroke-linecap: round; }
99
99
  .ov-gap-value { cursor: default; }
100
+
101
+ .ov-gantt { font: 14px/20px var(--font-sans, sans-serif); }
102
+ .ov-gantt .ov-axis { fill: var(--chart-live-muted, var(--text-muted, #666)); font-size: 13px; }
103
+ .ov-gantt .ov-mark-label { fill: var(--chart-live-fg, var(--text-primary, currentColor)); font-size: 13px; }
104
+ .ov-gantt .ov-tip { background: var(--chart-live-raised, var(--bg-elevated, #fff)); border-color: var(--chart-live-border, var(--border-color, #aaa)); max-width: min(240px, 100%); box-sizing: border-box; }
105
+ .ov-gantt-scroll { overscroll-behavior: contain; }
106
+ .ov-gantt [role='button'] { cursor: pointer; outline: none; }
107
+ .ov-gantt [role='button']:hover > rect:first-child { fill: var(--ix-hover-bg); }
108
+ .ov-gantt [role='button']:focus-visible > rect:first-child { stroke: var(--accent-text); stroke-width: 2; }
109
+ .ov-gantt [data-state] rect { transition: width var(--motion-stream), fill var(--motion-control); }
110
+ .ov-gantt [data-open='true'] rect { animation: ov-gantt-live var(--motion-ambient) infinite; }
111
+ .ov-gantt[data-featured] [data-mark] text { display: none; }
112
+ .ov-gantt[data-featured] [data-best] text { display: block; }
113
+ .ov-gantt[data-featured] .ov-events { display: none; }
114
+ @keyframes ov-gantt-live { 50% { opacity: .55; } }
115
+ @media (prefers-reduced-motion: reduce) { .ov-gantt [data-state] rect { transition: none; animation: none; } }
116
+
117
+ .ov-meter-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 6px; padding: 0; margin: 0; list-style: none; }
118
+ .ov-meter-steps li { display: flex; flex-direction: column; justify-content: space-between; gap: 12px; min-height: 104px; padding: 16px; border-radius: var(--radius-lg); background: var(--bg-elevated); }
119
+ .ov-meter-steps strong { font-size: 26px; line-height: 30px; }
120
+ .ov-meter-steps [data-state='pass'] { background: var(--surface-success-bg); }
121
+ .ov-meter-steps [aria-current='step'] { background: var(--surface-warning-bg); color: var(--surface-warning-text); }
122
+
123
+ .ov-meter-steps span { overflow-wrap: anywhere; }
124
+ @media (max-width: 720px) { .ov-meter-steps { grid-template-columns: repeat(3, minmax(0, 1fr)); } .ov-meter-steps li { padding: 12px; min-height: 94px; } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "2.0.6",
3
+ "version": "2.0.11",
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",