@contraagent/elements 0.8.0 → 0.9.0

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.
Files changed (70) hide show
  1. package/custom-elements.json +31 -6
  2. package/dist/graph/layered-graph.d.ts +7 -1
  3. package/dist/graph/layered-graph.d.ts.map +1 -1
  4. package/dist/graph/layered-graph.js +6 -2
  5. package/dist/graph/layered-graph.js.map +1 -1
  6. package/dist/graph/strings.d.ts +3 -1
  7. package/dist/graph/strings.d.ts.map +1 -1
  8. package/dist/graph/strings.js.map +1 -1
  9. package/dist/graph/styles.d.ts.map +1 -1
  10. package/dist/graph/styles.js +2 -0
  11. package/dist/graph/styles.js.map +1 -1
  12. package/dist/graph/types.d.ts +1 -1
  13. package/dist/graph/types.d.ts.map +1 -1
  14. package/dist/graph/views/dag.js +4 -4
  15. package/dist/graph/views/dag.js.map +1 -1
  16. package/dist/graph/views/foot.d.ts +4 -3
  17. package/dist/graph/views/foot.d.ts.map +1 -1
  18. package/dist/graph/views/foot.js +8 -23
  19. package/dist/graph/views/foot.js.map +1 -1
  20. package/dist/graph/views/node-table.js +2 -2
  21. package/dist/graph/views/node-table.js.map +1 -1
  22. package/dist/lanes/lanes.d.ts +3 -3
  23. package/dist/lanes/lanes.d.ts.map +1 -1
  24. package/dist/lanes/lanes.js +7 -4
  25. package/dist/lanes/lanes.js.map +1 -1
  26. package/dist/primitives/share-bar.d.ts +4 -1
  27. package/dist/primitives/share-bar.d.ts.map +1 -1
  28. package/dist/primitives/share-bar.js +8 -5
  29. package/dist/primitives/share-bar.js.map +1 -1
  30. package/dist/styles/motion.js +1 -1
  31. package/dist/tiles/stat-tiles.d.ts +3 -2
  32. package/dist/tiles/stat-tiles.d.ts.map +1 -1
  33. package/dist/tiles/stat-tiles.js +5 -4
  34. package/dist/tiles/stat-tiles.js.map +1 -1
  35. package/dist/tiles/styles.d.ts.map +1 -1
  36. package/dist/tiles/styles.js +4 -1
  37. package/dist/tiles/styles.js.map +1 -1
  38. package/dist/tiles/types.d.ts +1 -1
  39. package/dist/tiles/types.d.ts.map +1 -1
  40. package/dist/transcript/styles.d.ts.map +1 -1
  41. package/dist/transcript/styles.js +0 -1
  42. package/dist/transcript/styles.js.map +1 -1
  43. package/dist/transcript/transcript.d.ts +1 -0
  44. package/dist/transcript/transcript.d.ts.map +1 -1
  45. package/dist/transcript/transcript.js +10 -7
  46. package/dist/transcript/transcript.js.map +1 -1
  47. package/dist/transcript/types.d.ts +2 -1
  48. package/dist/transcript/types.d.ts.map +1 -1
  49. package/dist/transcript/types.js.map +1 -1
  50. package/docs/graph.md +5 -1
  51. package/docs/lanes.md +2 -1
  52. package/docs/tiles.md +6 -4
  53. package/docs/transcript.md +5 -3
  54. package/package.json +1 -1
  55. package/src/graph/layered-graph.ts +6 -2
  56. package/src/graph/strings.ts +3 -1
  57. package/src/graph/styles.ts +2 -0
  58. package/src/graph/types.ts +1 -1
  59. package/src/graph/views/dag.ts +4 -4
  60. package/src/graph/views/foot.ts +8 -23
  61. package/src/graph/views/node-table.ts +2 -2
  62. package/src/lanes/lanes.ts +6 -4
  63. package/src/primitives/share-bar.ts +12 -7
  64. package/src/styles/motion.ts +1 -1
  65. package/src/tiles/stat-tiles.ts +5 -4
  66. package/src/tiles/styles.ts +4 -1
  67. package/src/tiles/types.ts +1 -1
  68. package/src/transcript/styles.ts +0 -1
  69. package/src/transcript/transcript.ts +10 -7
  70. package/src/transcript/types.ts +2 -1
package/docs/tiles.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Stat tiles
2
2
 
3
3
  `<cae-stat-tiles>` shows figures as a grid of tiles: each a label, a value and
4
- lines of facts under it, and a link. Register it with
4
+ lines of facts under it. A tile with an action is a link as a whole. Register it with
5
5
  `@contraagent/elements/tiles/define`, which also registers the `<cae-count>` a
6
6
  tile's number counts up in; the types are in `@contraagent/elements/tiles`.
7
7
 
@@ -29,12 +29,14 @@ of failures above 0; a total or a measure stays neutral.
29
29
 
30
30
  ## Events
31
31
 
32
- A tile's link sends `cae-action` with the tile's `id` and the action's `id`;
33
- the host decides what it opens.
32
+ A tile with an `action` is a link over the whole tile. The action's `label` is
33
+ the link's accessible name, and the tile shows no link text of its own. A click
34
+ sends `cae-action` with the tile's `id` and the action's `id`; the host decides
35
+ what it opens.
34
36
 
35
37
  ## Look
36
38
 
37
39
  `columns` sets the tiles per row in a wide element (5); a narrower one has 4
38
40
  below 1100px of its width, 2 below 660px and 1 below 240px. `--cae-stat-tiles-gap` sets the space between
39
41
  the tiles. The parts are `tiles`, `tile` (with `data-id` and `data-tone`),
40
- `label`, `value`, `sub` and `action`.
42
+ `label`, `value`, `sub` and `action` (the link over a tile).
@@ -27,7 +27,9 @@ a list (errors) or a note.
27
27
  ::code{file="../src/transcript/types.ts" from="export interface TranscriptSection {" to="/** Where the reader was in a transcript. */"}
28
28
 
29
29
  An item is a prompt, a message or reasoning (markdown), an event (a glyph, a
30
- title and a detail), a notice, or a tool card:
30
+ title and a detail), a notice, or a tool card. A prompt and reasoning show a
31
+ head over their bubble; the agent's messages show none, and their avatar
32
+ carries the name (`strings.assistant`) as its accessible name and tooltip:
31
33
 
32
34
  ::code{file="../src/transcript/types.ts" from="/** One tool call:" to="/** A labelled value in a section's head (who ran it). */"}
33
35
 
@@ -69,5 +71,5 @@ Markdown is turned into HTML by `markdown` (`md` of
69
71
  reasoning show as plain text. `renderDetail` draws an open prose detail. The
70
72
  parts are `picker`, `section`, `tag`, `bubble`, `notice`, `tool`, `tool-body`
71
73
  and `verdict`; `tagChips` and `tagStyles` draw tags elsewhere in the same
72
- look. The words are `strings` ([Strings](strings.md)): the picker's name and
73
- tooltip, the bubbles' heads, the tool cards' statuses, the counts.
74
+ look. The words are `strings` ([Strings](strings.md)): the picker's accessible
75
+ name (not shown) and tooltip, the bubbles' heads and the messages' name, the tool cards' statuses, the counts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contraagent/elements",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Generic web components (Lit) and their parts: design tokens, themes, status pills, legends, a segmented control, a data table, a layered graph, a mark matrix, stat tiles, time lanes, charts (columns, lines, a heatmap, horizontal bars), a check table, a transcript, a JSON tree, markdown, highlighted code, a tree of items and counters. No contraagent data, no localization library",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -51,7 +51,8 @@ const EMPTY: GraphInput = { series: [], nodes: [], clusters: [], edges: [], stub
51
51
  * graph; Ctrl (or ⌘) and the wheel, or the buttons, zoom; dragging pans; "Show all"
52
52
  * goes back. "Graph | Table" changes the view (`cae-view-change`): the table is a row
53
53
  * per node, with its status and the nodes it is joined to. With `matrix`, the frame
54
- * shows that `<cae-mark-matrix>` as the table instead.
54
+ * shows that `<cae-mark-matrix>` as the table instead. With `controls` false, the
55
+ * buttons and "Graph | Table" are not drawn.
55
56
  *
56
57
  * Size: the element is as wide as its container. Without a height set on it, the
57
58
  * frame is as tall as the whole drawing at that width (never scaled up), up to 70% of
@@ -103,6 +104,7 @@ export class CaeLayeredGraph extends LitElement {
103
104
  view: {},
104
105
  matrix: { attribute: false },
105
106
  barAt: { type: Number },
107
+ controls: { type: Boolean },
106
108
  related: { attribute: false },
107
109
  strings: { attribute: false },
108
110
  matrixStrings: { attribute: false },
@@ -128,6 +130,8 @@ export class CaeLayeredGraph extends LitElement {
128
130
  direction: 'auto' | 'across' | 'down' = 'auto';
129
131
  /** The node table instead of the drawing. */
130
132
  view: 'graph' | 'table' = 'graph';
133
+ /** The zoom buttons and "Graph | Table" under the frame; false for a graph that is only shown (the wheel with Ctrl or ⌘, and dragging, still zoom and pan). */
134
+ controls = true;
131
135
  /** A mark matrix shown in the frame as the table, or null. */
132
136
  matrix: MarkMatrixInput | null = null;
133
137
  /** Above this many marks, a node shows a bar of its counts. */
@@ -370,7 +374,7 @@ export class CaeLayeredGraph extends LitElement {
370
374
  return html`<div class="col">
371
375
  <slot name="crumbs"></slot>
372
376
  <div class="main">${body}${card}<div class="sr" role="status">${this.announced}</div></div>
373
- ${foot(h, mx)}
377
+ ${foot(h, mx, this.controls)}
374
378
  </div>`;
375
379
  }
376
380
 
@@ -26,11 +26,13 @@ export interface LayeredGraphStrings {
26
26
  more: (n: number) => string;
27
27
  /** A node's count without `progress`: its marks of the tone `done`, of all its marks ("2 of 3 done"). */
28
28
  progress: (done: number, total: number) => string;
29
- /** Legend words: a badge in a mark, the three edge styles, a cluster's return, an inner view. */
29
+ /** The label of a mark's badge in its tooltip ("count: 3"). */
30
30
  badge: string;
31
+ /** Legend words: the three edge styles. */
31
32
  solidEdge: string;
32
33
  dottedEdge: string;
33
34
  dashedEdge: string;
35
+ /** Tooltips: the mark of a cluster's return, and of an item with an inner view. */
34
36
  cycle: string;
35
37
  drill: string;
36
38
  /** The open and close controls of an item that can be drawn open ("Show inside", "Hide inside"). */
@@ -41,6 +41,8 @@ export const graphStyles = css`
41
41
  .edges path.ret-tip, .legend svg path.ret-head { fill: var(--text-2); stroke: none; opacity: 0.8; }
42
42
  .ic { display: block; flex: none; }
43
43
  .loop-ic { color: var(--text-2); }
44
+ /* an icon that names itself in a tooltip */
45
+ .tip-ic { display: inline-flex; line-height: 0; }
44
46
  .in-ic { color: var(--text-muted); }
45
47
 
46
48
  .stub { z-index: 1; font-family: var(--mono); font-size: 11.5px; color: var(--text-muted); padding: 3px 9px;
@@ -23,7 +23,7 @@ export interface GraphMark {
23
23
  label: string;
24
24
  /** The tooltip. */
25
25
  tip?: string;
26
- /** A short text in the mark: a count. */
26
+ /** A short text in the mark: a count. The tooltip adds it after `strings.badge` ("count: 3"). */
27
27
  badge?: string;
28
28
  /** A short name, as a chip shows it (a node drawn as a bar lists its failed marks by it); default: its series id. */
29
29
  name?: string;
@@ -109,7 +109,7 @@ function toggleControl(h: GraphHost, id: string, label: string, open: boolean) {
109
109
  function clusterName(h: GraphHost, b: GraphClusterInput, sel: boolean) {
110
110
  const first = b.path?.[0];
111
111
  const name = html`<button type="button" class="name" title=${(first?.tip ?? b.tip) || nothing} aria-pressed=${sel}
112
- @focus=${() => h.focusSelect(b.id)} @keydown=${(e: KeyboardEvent) => onNameKey(h, e, b.id)}>${b.cycle ? cycleIcon() : nothing}${first?.label ?? b.label}</button>`;
112
+ @focus=${() => h.focusSelect(b.id)} @keydown=${(e: KeyboardEvent) => onNameKey(h, e, b.id)}>${b.cycle ? html`<span class="tip-ic" title=${h.words.cycle}>${cycleIcon()}</span>` : nothing}${first?.label ?? b.label}</button>`;
113
113
  if (!b.path || b.path.length < 2) return name;
114
114
  return html`${name}${b.path.slice(1).map(
115
115
  (p) => html`<span class="psep" aria-hidden="true">›</span><button type="button" class="part" title=${p.tip || nothing}
@@ -139,7 +139,7 @@ function cluster(h: GraphHost, b: GraphClusterInput, parent: Rect | null, item:
139
139
  ${b.toggle
140
140
  ? html`<span class="cue">${toggleControl(h, b.id, b.path?.map((p) => p.label).join(' › ') ?? b.label, true)}</span>`
141
141
  : b.drill
142
- ? html`<span class="cue" aria-hidden="true">${innerIcon()}</span>`
142
+ ? html`<span class="cue" aria-hidden="true" title=${h.words.drill}>${innerIcon()}</span>`
143
143
  : nothing}
144
144
  <div class="in">${repeat(kids, (id) => id, (id) => item(id, r))}</div>
145
145
  </section>`;
@@ -169,7 +169,7 @@ function node(h: GraphHost, n: GraphNode, parent: Rect | null) {
169
169
  <span class="right"><span class="count" aria-hidden="true" title=${n.progress.label}>${n.progress.done}/${n.progress.total}</span>${n.toggle
170
170
  ? toggleControl(h, n.id, n.label, false)
171
171
  : n.drill
172
- ? html`<span class="cue" aria-hidden="true">${innerIcon()}</span>`
172
+ ? html`<span class="cue" aria-hidden="true" title=${h.words.drill}>${innerIcon()}</span>`
173
173
  : nothing}</span>
174
174
  </div>
175
175
  ${bar ? statusBar(h, n, group) : marks(h, group)}
@@ -230,7 +230,7 @@ function mark(h: GraphHost, g: MarkGroup, s: Slot, i: number, tabStop: boolean,
230
230
  const look = h.states[m.state];
231
231
  return html`<button type="button" class="mark ${lit ? 'on' : ''} ${sel}" part="mark" data-tone=${toneAttr(look?.tone)} data-shape=${look?.shape ?? nothing}
232
232
  data-link=${link ?? nothing} data-i=${i} data-n=${m.badge ?? nothing} tabindex=${tabStop ? 0 : -1}
233
- title=${m.tip ?? nothing}
233
+ title=${[m.tip, m.badge ? `${h.words.badge}: ${m.badge}` : ''].filter(Boolean).join('\n') || nothing}
234
234
  aria-label=${m.label} aria-pressed=${!!sel}
235
235
  @mouseenter=${() => (h.hover = hlKey)}
236
236
  @focus=${focus}
@@ -5,17 +5,17 @@ import { legend as legendRow, type LegendItem } from '../../primitives/legend.js
5
5
  import type { MarkMatrixInput } from '../../matrix/types.js';
6
6
  import { asBar } from '../marks.js';
7
7
  import { slotsOf, type GraphHost } from '../host.js';
8
- import { innerIcon } from '../icons.js';
9
8
 
10
9
  /**
11
- * The legend on the left, the tools at the right end of the first line, and the keys
12
- * under them: their line keeps its room, and shows its words while the graph has the focus.
10
+ * The legend on the left, the tools at the right end of the first line (with
11
+ * `controls`), and the keys under them: their line keeps its room, and shows its
12
+ * words while the graph has the focus.
13
13
  */
14
- export function foot(h: GraphHost, matrix: MarkMatrixInput | null) {
14
+ export function foot(h: GraphHost, matrix: MarkMatrixInput | null, controls: boolean) {
15
15
  const table = !!matrix || h.asTable;
16
16
  return html`<div class="foot">
17
17
  ${legend(h, matrix)}
18
- <div class="tools">${table ? nothing : zoomTools(h)}${viewSwitch(h, table)}</div>
18
+ ${controls ? html`<div class="tools">${table ? nothing : zoomTools(h)}${viewSwitch(h, table)}</div>` : nothing}
19
19
  <div class="keys" part="keys">${keyHints(h, matrix).map((k, i) => html`${i ? ' | ' : ''}<span>${k}</span>`)}</div>
20
20
  </div>`;
21
21
  }
@@ -58,9 +58,9 @@ const legendMark = (h: GraphHost, state: string, badge?: string) => {
58
58
  };
59
59
 
60
60
  /**
61
- * What the view shows that needs a word: the states of the marks shown, a badge when a
62
- * mark shows one, the icons the host names, the edge styles when another than solid is drawn, a
63
- * cluster's return, and the mark of an item with an inner view.
61
+ * What the view shows that needs a word: the states of the marks shown, the icons the
62
+ * host names, and the edge styles when another than solid is drawn. A badge, a cluster's
63
+ * return and the mark of an item with an inner view name themselves in their tooltips.
64
64
  */
65
65
  function legend(h: GraphHost, mx: MarkMatrixInput | null) {
66
66
  const g = h.drawn;
@@ -73,13 +73,9 @@ function legend(h: GraphHost, mx: MarkMatrixInput | null) {
73
73
  : g.nodes.flatMap((n) => slotsOf(n, g.series).map((s) => s && { state: s.m.state, badge: asBar(n, h.barAt) ? undefined : s.m.badge }));
74
74
  const shown = marks.filter((m): m is { state: string; badge: string | undefined } => m !== null);
75
75
  const present = new Set(shown.map((m) => m.state));
76
- const badge = shown.some((m) => m.badge);
77
76
  const styles = new Set(!mx && !flat ? [...g.edges, ...g.stubs].map((e) => e.style) : []);
78
77
  const edgeStyles = styles.size > 1 || (styles.size === 1 && !styles.has('solid')) ? (['solid', 'dotted', 'dashed'] as const).filter((s) => styles.has(s)) : [];
79
78
  const icons = new Set(mx ? mx.head.flat().flatMap((c) => c.icons ?? []) : g.nodes.flatMap((n) => n.icons ?? []));
80
- const cycles = !mx && !flat && g.clusters.some((c) => c.cycle);
81
- const inner = !mx && ((!flat && g.clusters.some((c) => c.drill)) || g.nodes.some((n) => n.drill));
82
- const first = Object.keys(h.states)[0] ?? '';
83
79
  const line = (style: 'solid' | 'dotted' | 'dashed') =>
84
80
  svg`<svg width="22" height="8" aria-hidden="true"><path class=${style} d="M1,4 H21" stroke-dasharray=${style === 'dashed' ? '5 4' : style === 'dotted' ? '0.1 4' : 'none'}></path></svg>`;
85
81
  const edgeWord = { solid: h.words.solidEdge, dotted: h.words.dottedEdge, dashed: h.words.dashedEdge };
@@ -87,21 +83,10 @@ function legend(h: GraphHost, mx: MarkMatrixInput | null) {
87
83
  ...Object.entries(h.states)
88
84
  .filter(([s]) => present.has(s))
89
85
  .map(([s, look]) => ({ label: look.label, icon: legendMark(h, s), decorative: true })),
90
- ...(badge ? [{ label: h.words.badge, icon: legendMark(h, first, '2'), decorative: true }] : []),
91
86
  ...Object.entries(h.words.iconNames)
92
87
  .filter(([name]) => icons.has(name))
93
88
  .map(([name, label]) => ({ label, icon: h.renderIcon(name) })),
94
89
  ...edgeStyles.map((s) => ({ label: edgeWord[s], icon: line(s) })),
95
- ...(cycles
96
- ? [
97
- {
98
- label: h.words.cycle,
99
- icon: html`<svg width="24" height="12" aria-hidden="true"><path class="ret" d="M20,1.5 V5.5 Q20,9.5 16,9.5 H8 Q4,9.5 4,5.5 V5"></path>
100
- <path class="ret-head" d="M4,0.8 L1.4,5 L6.6,5 z"></path></svg>`,
101
- },
102
- ]
103
- : []),
104
- ...(inner ? [{ label: h.words.drill, icon: innerIcon() }] : []),
105
90
  ];
106
91
  return legendRow(items, { cls: 'big' });
107
92
  }
@@ -57,7 +57,7 @@ export function nodeTable(h: GraphHost) {
57
57
  const nb = neighbours(n, g, h.nodes, w.inCluster);
58
58
  const box = n.cluster ? h.clusters.get(n.cluster) : undefined;
59
59
  const on = h.selected === n.id;
60
- const icons = html`${(n.icons ?? []).map((x) => h.renderIcon(x))}${n.drill ? innerIcon() : nothing}`;
60
+ const icons = html`${(n.icons ?? []).map((x) => h.renderIcon(x))}${n.drill ? html`<span class="tip-ic" title=${w.drill}>${innerIcon()}</span>` : nothing}`;
61
61
  return html`<tr class=${on ? 'sel' : ''} part="row" data-id=${n.id}
62
62
  @click=${(e: MouseEvent) => !(e.target as Element).closest('.loopchip') && e.detail < 2 && h.select(n.id)}
63
63
  @dblclick=${(e: MouseEvent) => !(e.target as Element).closest('.loopchip') && h.act(n.id)}>
@@ -69,7 +69,7 @@ export function nodeTable(h: GraphHost) {
69
69
  h.act(n.id);
70
70
  }}>${n.label}</button>${box?.drill
71
71
  ? html`<button type="button" class="taskchip loopchip" title=${box.chipTip ?? nothing} aria-label=${box.chipLabel ?? nothing}
72
- @click=${() => h.drill(box.id)}>${box.cycle ? cycleIcon(12) : nothing}${box.note ?? box.label}</button>`
72
+ @click=${() => h.drill(box.id)}>${box.cycle ? html`<span class="tip-ic" title=${w.cycle}>${cycleIcon(12)}</span>` : nothing}${box.note ?? box.label}</button>`
73
73
  : nothing}</span>
74
74
  </th>
75
75
  <td class="fit st">${statusLine(n)}</td>
@@ -14,8 +14,8 @@ const SECONDS_BELOW_MS = 10 * 60e3;
14
14
  /**
15
15
  * Bars on a time axis, a lane per group: each bar as long as it took, the
16
16
  * overlapping bars of a lane stacked, a running bar (`end` null) growing until
17
- * now and a line at now. A bar's look is its style in `barStyles`, whose
18
- * labels are the legend (a row per `group`). The bars grow in when they are
17
+ * now and a line at now. A bar's look is its style in `barStyles`; the labels
18
+ * of the styles the bars use are the legend (a row per `group`). The bars grow in when they are
19
19
  * first drawn.
20
20
  *
21
21
  * Its look: `--cae-lanes-label-width` (the lanes' labels; 112px, 78px on a
@@ -117,10 +117,12 @@ export class CaeLanes extends LitElement {
117
117
  </div>`;
118
118
  }
119
119
 
120
- /** A legend row per group of styles, in key order; the note of omitted bars ends the last. */
120
+ /** A legend row per group of the styles the bars use, in key order; the note of omitted bars ends the last. */
121
121
  private legends(s: LanesStrings): TemplateResult[] {
122
122
  const groups: LegendItem[][] = [];
123
- for (const look of Object.values(this.barStyles)) {
123
+ const used = new Set(this.lanes.flatMap((l) => l.bars.map((b) => b.style)));
124
+ for (const [key, look] of Object.entries(this.barStyles)) {
125
+ if (!used.has(key)) continue;
124
126
  const g = look.group ?? 0;
125
127
  (groups[g] ??= []).push({
126
128
  label: look.label,
@@ -20,6 +20,8 @@ export interface ShareBarOptions {
20
20
  label?: string;
21
21
  /** The smallest share (0..1) whose part is named under the bar; default 0.15. */
22
22
  labelAt?: number;
23
+ /** Names the parts under the bar; default true. False when a table beside the bar names each part with its share. */
24
+ names?: boolean;
23
25
  }
24
26
 
25
27
  /**
@@ -42,7 +44,8 @@ export const sharePercent = (pct: number, value: number): string => (pct === 0 &
42
44
  /**
43
45
  * The parts of a whole as one bar (`.share`, part `share`): each part a segment as
44
46
  * wide as its share, 2px apart, in the order given (so a part keeps its place when
45
- * the amounts change), with its name and share under it when it is wide enough.
47
+ * the amounts change), with its name and share under it when it is wide enough
48
+ * (unless `names` is false).
46
49
  * Hovering or focusing a segment shows its name, share and amount; each segment is
47
50
  * a tab stop with the same words. Name every part beside the bar too (a legend or a
48
51
  * table): a colour alone never says which part is which.
@@ -62,12 +65,14 @@ export function shareBar(parts: readonly SharePart[], o: ShareBarOptions = {}):
62
65
  ><span class="share-tip" aria-hidden="true">${words(p, i)}</span></span>`,
63
66
  )}
64
67
  </div>
65
- <div class="share-names" aria-hidden="true">
66
- ${shown.map(
67
- (p, i) => html`<span style=${styleMap({ flexGrow: String(p.value / total) })}
68
- >${p.value / total >= labelAt ? html`<b>${p.label}</b> ${sharePercent(pcts[i]!, p.value)}` : nothing}</span>`,
69
- )}
70
- </div>
68
+ ${o.names === false
69
+ ? nothing
70
+ : html`<div class="share-names" aria-hidden="true">
71
+ ${shown.map(
72
+ (p, i) => html`<span style=${styleMap({ flexGrow: String(p.value / total) })}
73
+ >${p.value / total >= labelAt ? html`<b>${p.label}</b> ${sharePercent(pcts[i]!, p.value)}` : nothing}</span>`,
74
+ )}
75
+ </div>`}
71
76
  </div>`;
72
77
  }
73
78
 
@@ -13,7 +13,7 @@ export const motionStyles = css`
13
13
  @keyframes ping { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-running) 55%, transparent); } 80%, 100% { box-shadow: 0 0 0 9px transparent; } }
14
14
  @keyframes enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
15
15
  @keyframes bump { 0% { transform: scale(1); } 35% { transform: scale(1.04); box-shadow: 0 0 0 1px var(--line2); } 100% { transform: scale(1); } }
16
- @keyframes scan { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
16
+ @keyframes scan { to { left: 100%; } }
17
17
  @keyframes growx { from { transform: scaleX(0); } to { transform: scaleX(1); } }
18
18
  @keyframes stripes { to { background-position: 17px 0; } }
19
19
  @keyframes toast-in { from { opacity: 0; transform: translateY(12px) scale(0.97); } to { opacity: 1; transform: none; } }
@@ -13,7 +13,8 @@ const reducedMotion = () => globalThis.matchMedia?.('(prefers-reduced-motion: re
13
13
 
14
14
  /**
15
15
  * Figures as a grid of tiles: each its label, its value (a number counts up in
16
- * `<cae-count>`), the lines of facts under it and a link. A tile whose number
16
+ * `<cae-count>`) and the lines of facts under it. A tile with an action is a
17
+ * link as a whole, named by the action's label. A tile whose number
17
18
  * changes bumps, unless `bump` is off or the reader asks for reduced motion.
18
19
  *
19
20
  * Its look: `columns` (fewer in a narrow element: 4 below 1100px of its
@@ -26,7 +27,7 @@ const reducedMotion = () => globalThis.matchMedia?.('(prefers-reduced-motion: re
26
27
  * @csspart label - a tile's label
27
28
  * @csspart value - a tile's value
28
29
  * @csspart sub - each line under the value
29
- * @csspart action - a tile's link
30
+ * @csspart action - a tile's link, over the whole tile
30
31
  */
31
32
  export class CaeStatTiles extends LitElement {
32
33
  static override styles = [base, statTilesStyles];
@@ -63,10 +64,10 @@ export class CaeStatTiles extends LitElement {
63
64
  <div class="value" part="value">${typeof t.value === 'number' ? html`<cae-count .value=${t.value}></cae-count>` : t.value}</div>
64
65
  ${(t.sub ?? []).map((line) => html`<div class="sub words" part="sub">${factLine(line)}</div>`)}
65
66
  ${t.action
66
- ? html`<div class="action words"><a class="lnk" part="action" href="#" @click=${(e: Event) => {
67
+ ? html`<a class="action" part="action" href="#" @click=${(e: Event) => {
67
68
  e.preventDefault();
68
69
  if (t.action) emit(this, 'cae-action', { id: t.id, action: t.action.id });
69
- }}>${t.action.label}</a></div>`
70
+ }}><span class="sr">${t.action.label}</span></a>`
70
71
  : nothing}
71
72
  </div>`;
72
73
  }
@@ -16,7 +16,10 @@ export const statTilesStyles = css`
16
16
  .label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
17
17
  .value { overflow-wrap: anywhere; font-size: 28px; font-weight: 700; line-height: 1.15; margin-top: 6px; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; color: var(--text); }
18
18
  .sub { font-size: 11.5px; color: var(--text-muted); margin-top: 2px; }
19
- .action { font-size: 11.5px; margin-top: 4px; }
19
+ /* a tile with an action is a link as a whole: the link covers the tile */
20
+ .action { position: absolute; inset: 0; border-radius: inherit; }
21
+ .action:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
22
+ .tile:has(.action):hover { border-color: var(--accent); }
20
23
  .tile[data-tone] { --edge: var(--tone); }
21
24
  .tile[data-tone] .value, .tile[data-tone] .label::before { color: var(--tone); }
22
25
  .label[data-mark]::before { display: inline-block; }
@@ -15,6 +15,6 @@ export interface StatTile {
15
15
  tip?: string;
16
16
  /** The lines under the value, each a row of facts (`factLine`); an empty line keeps its place. */
17
17
  sub?: readonly (readonly (string | Fact)[])[];
18
- /** A link under the lines: a click sends `cae-action` with the tile's id and this id. */
18
+ /** Makes the whole tile a link named `label`: a click sends `cae-action` with the tile's id and this id. */
19
19
  action?: { id: string; label: string };
20
20
  }
@@ -7,7 +7,6 @@ export const transcriptStyles = css`
7
7
  :host([mode='tabs']) .scroller, :host([mode='tabs']) .newpill { display: none; }
8
8
 
9
9
  .rounds { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 10px; padding-left: 2px; }
10
- .rounds-lbl { color: var(--text-muted); font-size: 11.5px; font-weight: 600; margin-right: 4px; cursor: help; }
11
10
  .stab { padding: 3px 11px; border: 1px solid var(--line); border-radius: 999px; background: var(--card); color: var(--text-muted);
12
11
  cursor: pointer; font-size: 12px; display: inline-flex; align-items: center; }
13
12
  .stab:hover { border-color: var(--accent); }
@@ -254,8 +254,8 @@ export class CaeTranscript extends LitElement {
254
254
 
255
255
  private renderPicker(sections: readonly TranscriptSection[], shown: number, s: TranscriptStrings) {
256
256
  if (sections.length < 2) return nothing;
257
- return html`<div class="rounds" role="group" aria-label=${s.sections} part="picker">
258
- <span class="rounds-lbl" title=${s.sectionsTip || nothing}>${s.sections}</span>
257
+ // The buttons say what they pick: the picker's name is for screen readers, its tooltip on the group.
258
+ return html`<div class="rounds" role="group" aria-label=${s.sections} title=${s.sectionsTip || nothing} part="picker">
259
259
  ${sections.map((sec, i) => {
260
260
  const v = sec.verdict;
261
261
  return html`<button type="button" class="stab ${i === shown ? 'sel' : ''}" title=${v?.tip || nothing} aria-pressed=${i === shown}
@@ -320,10 +320,13 @@ export class CaeTranscript extends LitElement {
320
320
  return unsafeHTML(out);
321
321
  }
322
322
 
323
- private bubble(av: string, avCls: string, head: string, body: TemplateResult | string, title?: string) {
323
+ /** A bubble under its head; `named`: no head, the avatar carries the name (the agent's own messages). */
324
+ private bubble(av: string, avCls: string, head: string, body: TemplateResult | string, title?: string, named = false) {
324
325
  return html`<div class="msg" part="bubble" title=${title ?? nothing}>
325
- <div class="av ${avCls}">${av}</div>
326
- <div class="bd"><div class="hd">${head}</div>${body}</div>
326
+ ${named
327
+ ? html`<div class="av ${avCls}" role="img" aria-label=${head} title=${head}>${av}</div>`
328
+ : html`<div class="av ${avCls}" aria-hidden="true">${av}</div>`}
329
+ <div class="bd">${named ? nothing : html`<div class="hd">${head}</div>`}${body}</div>
327
330
  </div>`;
328
331
  }
329
332
 
@@ -332,7 +335,7 @@ export class CaeTranscript extends LitElement {
332
335
  case 'prompt':
333
336
  return this.bubble('▸', 'av-l', s.prompt, html`<div class="tx meta">${s.characters(it.text.length)}</div>`, it.text.slice(0, 600));
334
337
  case 'message':
335
- return this.bubble('A', 'av-a', s.assistant, html`<div class="tx">${this.md(it.id, it.text)}</div>`);
338
+ return this.bubble('A', 'av-a', s.assistant, html`<div class="tx">${this.md(it.id, it.text)}</div>`, undefined, true);
336
339
  case 'reasoning':
337
340
  return this.bubble('✦', 'av-t', s.reasoning, html`<div class="tx">${this.md(it.id, it.text)}</div>`);
338
341
  case 'event':
@@ -347,7 +350,7 @@ export class CaeTranscript extends LitElement {
347
350
  }
348
351
 
349
352
  private typing(s: TranscriptStrings) {
350
- return html`<div class="typing"><span class="av av-a">A</span><span class="dots"><i></i><i></i><i></i></span><span class="meta">${s.running}</span></div>`;
353
+ return html`<div class="typing"><span class="av av-a" aria-hidden="true">A</span><span class="dots"><i></i><i></i><i></i></span><span class="meta">${s.running}</span></div>`;
351
354
  }
352
355
  }
353
356
 
@@ -106,7 +106,7 @@ export interface TranscriptMemory {
106
106
  }
107
107
 
108
108
  export interface TranscriptStrings {
109
- /** The section picker's name. */
109
+ /** The section picker's accessible name (not shown). */
110
110
  sections: string;
111
111
  /** The section picker's tooltip. */
112
112
  sectionsTip: string;
@@ -117,6 +117,7 @@ export interface TranscriptStrings {
117
117
  /** As `newBelow`, while another section than the latest is shown. */
118
118
  newInLatest: (n: number) => string;
119
119
  prompt: string;
120
+ /** The name of the agent's messages: their avatar's accessible name and tooltip (no head is shown). */
120
121
  assistant: string;
121
122
  reasoning: string;
122
123
  characters: (n: number) => string;