@sigrun/tui 0.4.1 → 0.5.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.
- package/CHANGELOG.md +10 -1
- package/dist/actions.d.ts +119 -0
- package/dist/actions.js +12 -0
- package/dist/api.d.ts +209 -0
- package/dist/api.js +282 -0
- package/dist/blocks/braille.d.ts +41 -0
- package/dist/blocks/braille.js +131 -0
- package/dist/blocks/chart.d.ts +20 -0
- package/dist/blocks/chart.js +397 -0
- package/dist/blocks/chat-transport.d.ts +44 -0
- package/dist/blocks/chat-transport.js +116 -0
- package/dist/blocks/collection/calendar.d.ts +28 -0
- package/dist/blocks/collection/calendar.js +232 -0
- package/dist/blocks/collection/card.d.ts +36 -0
- package/dist/blocks/collection/card.js +128 -0
- package/dist/blocks/collection/collection.d.ts +9 -0
- package/dist/blocks/collection/collection.js +26 -0
- package/dist/blocks/collection/grid.d.ts +18 -0
- package/dist/blocks/collection/grid.js +72 -0
- package/dist/blocks/collection/lanes.d.ts +32 -0
- package/dist/blocks/collection/lanes.js +320 -0
- package/dist/blocks/collection/list.d.ts +30 -0
- package/dist/blocks/collection/list.js +273 -0
- package/dist/blocks/collection/move.d.ts +67 -0
- package/dist/blocks/collection/move.js +96 -0
- package/dist/blocks/collection/shared.d.ts +87 -0
- package/dist/blocks/collection/shared.js +177 -0
- package/dist/blocks/collection/table.d.ts +26 -0
- package/dist/blocks/collection/table.js +221 -0
- package/dist/blocks/content.d.ts +48 -0
- package/dist/blocks/content.js +253 -0
- package/dist/blocks/context.d.ts +117 -0
- package/dist/blocks/context.js +65 -0
- package/dist/blocks/diff.d.ts +21 -0
- package/dist/blocks/diff.js +25 -0
- package/dist/blocks/ext-transport.d.ts +70 -0
- package/dist/blocks/ext-transport.js +165 -0
- package/dist/blocks/form/choice.d.ts +54 -0
- package/dist/blocks/form/choice.js +175 -0
- package/dist/blocks/form/fields.d.ts +56 -0
- package/dist/blocks/form/fields.js +136 -0
- package/dist/blocks/form/file.d.ts +61 -0
- package/dist/blocks/form/file.js +121 -0
- package/dist/blocks/form/form.d.ts +66 -0
- package/dist/blocks/form/form.js +764 -0
- package/dist/blocks/form/list.d.ts +37 -0
- package/dist/blocks/form/list.js +70 -0
- package/dist/blocks/form/values.d.ts +20 -0
- package/dist/blocks/form/values.js +106 -0
- package/dist/blocks/icons.d.ts +14 -0
- package/dist/blocks/icons.js +74 -0
- package/dist/blocks/image.d.ts +39 -0
- package/dist/blocks/image.js +82 -0
- package/dist/blocks/interaction-transport.d.ts +42 -0
- package/dist/blocks/interaction-transport.js +79 -0
- package/dist/blocks/layout.d.ts +31 -0
- package/dist/blocks/layout.js +155 -0
- package/dist/blocks/log.d.ts +24 -0
- package/dist/blocks/log.js +111 -0
- package/dist/blocks/meter.d.ts +16 -0
- package/dist/blocks/meter.js +50 -0
- package/dist/blocks/page.d.ts +54 -0
- package/dist/blocks/page.js +360 -0
- package/dist/blocks/render.d.ts +31 -0
- package/dist/blocks/render.js +163 -0
- package/dist/blocks/surface-view.d.ts +41 -0
- package/dist/blocks/surface-view.js +137 -0
- package/dist/blocks/surface.d.ts +80 -0
- package/dist/blocks/surface.js +506 -0
- package/dist/blocks/tones.d.ts +23 -0
- package/dist/blocks/tones.js +63 -0
- package/dist/blocks/treeview.d.ts +22 -0
- package/dist/blocks/treeview.js +121 -0
- package/dist/blocks/width.d.ts +32 -0
- package/dist/blocks/width.js +70 -0
- package/dist/clipboard.d.ts +20 -0
- package/dist/clipboard.js +19 -0
- package/dist/editor-launch.d.ts +34 -0
- package/dist/editor-launch.js +70 -0
- package/dist/keymaps/emacs.d.ts +18 -0
- package/dist/keymaps/emacs.js +172 -0
- package/dist/keymaps/standard.d.ts +17 -0
- package/dist/keymaps/standard.js +175 -0
- package/dist/keymaps/types.d.ts +43 -0
- package/dist/keymaps/types.js +28 -0
- package/dist/keymaps/vim.d.ts +48 -0
- package/dist/keymaps/vim.js +291 -0
- package/dist/launch.d.ts +159 -0
- package/dist/launch.js +478 -0
- package/dist/sse.d.ts +112 -0
- package/dist/sse.js +418 -0
- package/dist/theme.d.ts +51 -0
- package/dist/theme.js +119 -0
- package/dist/ui/ambient.d.ts +78 -0
- package/dist/ui/ambient.js +264 -0
- package/dist/ui/app.d.ts +319 -0
- package/dist/ui/app.js +1539 -0
- package/dist/ui/composer.d.ts +66 -0
- package/dist/ui/composer.js +133 -0
- package/dist/ui/conversation-pane.d.ts +143 -0
- package/dist/ui/conversation-pane.js +615 -0
- package/dist/ui/help.d.ts +15 -0
- package/dist/ui/help.js +80 -0
- package/dist/ui/interactions.d.ts +91 -0
- package/dist/ui/interactions.js +477 -0
- package/dist/ui/picker.d.ts +26 -0
- package/dist/ui/picker.js +130 -0
- package/dist/ui/settings.d.ts +68 -0
- package/dist/ui/settings.js +254 -0
- package/dist/ui/sidebar.d.ts +99 -0
- package/dist/ui/sidebar.js +269 -0
- package/dist/ui/status-bar.d.ts +77 -0
- package/dist/ui/status-bar.js +146 -0
- package/package.json +4 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Braille dot canvas and the display ramps (UI-blocks spec §5.4):
|
|
3
|
+
* the pieces `chart` and the narrow-fallback sparkline draw with.
|
|
4
|
+
*
|
|
5
|
+
* - {@link BrailleCanvas} is a `cells×rows` cell grid of 2×4 dot
|
|
6
|
+
* subpixels (Unicode Braille Patterns, U+2800 base). Each cell also
|
|
7
|
+
* records the index of the series that plotted into it LAST, which
|
|
8
|
+
* is how multi-colour charts colour a shared cell.
|
|
9
|
+
* - {@link sparkline} renders a value series onto the eighth-block
|
|
10
|
+
* ramp `▁▂▃▄▅▆▇█` (or the ASCII ramp `_.,-+=*#` on a no-Unicode
|
|
11
|
+
* terminal), downsampling with the shared `lttb` when the series is
|
|
12
|
+
* longer than the cell budget.
|
|
13
|
+
*/
|
|
14
|
+
/** A Braille dot canvas: `cells` columns × `rows` rows of 2×4 dots. */
|
|
15
|
+
export declare class BrailleCanvas {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(cells: number, rows: number);
|
|
18
|
+
/** Plot one dot; out-of-range coordinates are ignored. */
|
|
19
|
+
plot(x: number, y: number, series?: number): void;
|
|
20
|
+
/** A straight dot-space line from (x0, y0) to (x1, y1): the steps
|
|
21
|
+
* are the CHEBYSHEV distance (diagonal-adjacent dots step one
|
|
22
|
+
* dot-space cell at a time). */
|
|
23
|
+
line(x0: number, y0: number, x1: number, y1: number, series?: number): void;
|
|
24
|
+
/** Fill the dot rectangle (x0..x1, y0..y1) inclusive. */
|
|
25
|
+
fill(x0: number, y0: number, x1: number, y1: number, series?: number): void;
|
|
26
|
+
/** The dot bits set in one cell. */
|
|
27
|
+
cellBits(cellX: number, cellY: number): number;
|
|
28
|
+
/** The series index that plotted into this cell last (−1: none). */
|
|
29
|
+
cellOwner(cellX: number, cellY: number): number;
|
|
30
|
+
/** Clear one cell row (a threshold rule paints over it). */
|
|
31
|
+
blankRow(cellY: number): void;
|
|
32
|
+
/** The canvas as one Braille string per cell row. */
|
|
33
|
+
rows(): string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A one-cell-per-sample sparkline of `values`, at most `maxCells`
|
|
37
|
+
* cells (longer series downsample with `lttb`, keeping both ends).
|
|
38
|
+
* `unicode: false` selects the ASCII ramp. Empty input renders empty;
|
|
39
|
+
* a flat series renders mid-height cells.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sparkline(values: readonly number[], maxCells: number, unicode: boolean): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Braille dot canvas and the display ramps (UI-blocks spec §5.4):
|
|
3
|
+
* the pieces `chart` and the narrow-fallback sparkline draw with.
|
|
4
|
+
*
|
|
5
|
+
* - {@link BrailleCanvas} is a `cells×rows` cell grid of 2×4 dot
|
|
6
|
+
* subpixels (Unicode Braille Patterns, U+2800 base). Each cell also
|
|
7
|
+
* records the index of the series that plotted into it LAST, which
|
|
8
|
+
* is how multi-colour charts colour a shared cell.
|
|
9
|
+
* - {@link sparkline} renders a value series onto the eighth-block
|
|
10
|
+
* ramp `▁▂▃▄▅▆▇█` (or the ASCII ramp `_.,-+=*#` on a no-Unicode
|
|
11
|
+
* terminal), downsampling with the shared `lttb` when the series is
|
|
12
|
+
* longer than the cell budget.
|
|
13
|
+
*/
|
|
14
|
+
import { lttb } from "@sigrun/shared/blocks";
|
|
15
|
+
/**
|
|
16
|
+
* Dot bit per (x, y) subpixel: the Unicode standard layout — the
|
|
17
|
+
* left column holds dots 1,2,3,7 and the right column dots 4,5,6,8,
|
|
18
|
+
* TOP-DOWN. Dot space is TOP-DOWN too: (x, 0) is the TOP row of the
|
|
19
|
+
* canvas (plot(0,0) = dot 1 = 0x01). The suite pins plot(0,0)=0x01,
|
|
20
|
+
* plot(0,2)=0x04, plot(1,1)=0x10, plot(1,3)=0x80 on a one-row
|
|
21
|
+
* canvas; chart inverts its y axis before plotting.
|
|
22
|
+
*/
|
|
23
|
+
const DOT_BIT = [
|
|
24
|
+
[0x01, 0x02, 0x04, 0x40],
|
|
25
|
+
[0x08, 0x10, 0x20, 0x80],
|
|
26
|
+
];
|
|
27
|
+
/** A Braille dot canvas: `cells` columns × `rows` rows of 2×4 dots. */
|
|
28
|
+
export class BrailleCanvas {
|
|
29
|
+
#cells;
|
|
30
|
+
#rows;
|
|
31
|
+
#bits;
|
|
32
|
+
#owner;
|
|
33
|
+
constructor(cells, rows) {
|
|
34
|
+
this.#cells = Math.max(0, cells);
|
|
35
|
+
this.#rows = Math.max(0, rows);
|
|
36
|
+
this.#bits = new Uint8Array(this.#cells * this.#rows);
|
|
37
|
+
this.#owner = new Int32Array(this.#cells * this.#rows).fill(-1);
|
|
38
|
+
}
|
|
39
|
+
/** Plot one dot; out-of-range coordinates are ignored. */
|
|
40
|
+
plot(x, y, series) {
|
|
41
|
+
const cellX = Math.floor(x / 2);
|
|
42
|
+
const cellY = Math.floor(y / 4);
|
|
43
|
+
if (cellX < 0 || cellY < 0 || cellX >= this.#cells || cellY >= this.#rows)
|
|
44
|
+
return;
|
|
45
|
+
const index = cellY * this.#cells + cellX;
|
|
46
|
+
const row = y % 4;
|
|
47
|
+
const col = x % 2;
|
|
48
|
+
const bit = DOT_BIT[col][row] ?? 0;
|
|
49
|
+
this.#bits[index] = (this.#bits[index] ?? 0) | bit;
|
|
50
|
+
if (series !== undefined)
|
|
51
|
+
this.#owner[index] = series;
|
|
52
|
+
}
|
|
53
|
+
/** A straight dot-space line from (x0, y0) to (x1, y1): the steps
|
|
54
|
+
* are the CHEBYSHEV distance (diagonal-adjacent dots step one
|
|
55
|
+
* dot-space cell at a time). */
|
|
56
|
+
line(x0, y0, x1, y1, series) {
|
|
57
|
+
const steps = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0), 1);
|
|
58
|
+
for (let i = 0; i <= steps; i++) {
|
|
59
|
+
this.plot(Math.round(x0 + ((x1 - x0) * i) / steps), Math.round(y0 + ((y1 - y0) * i) / steps), series);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Fill the dot rectangle (x0..x1, y0..y1) inclusive. */
|
|
63
|
+
fill(x0, y0, x1, y1, series) {
|
|
64
|
+
for (let y = Math.min(y0, y1); y <= Math.max(y0, y1); y++) {
|
|
65
|
+
for (let x = Math.min(x0, x1); x <= Math.max(x0, x1); x++) {
|
|
66
|
+
this.plot(x, y, series);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The dot bits set in one cell. */
|
|
71
|
+
cellBits(cellX, cellY) {
|
|
72
|
+
return this.#bits[cellY * this.#cells + cellX] ?? 0;
|
|
73
|
+
}
|
|
74
|
+
/** The series index that plotted into this cell last (−1: none). */
|
|
75
|
+
cellOwner(cellX, cellY) {
|
|
76
|
+
return this.#owner[cellY * this.#cells + cellX] ?? -1;
|
|
77
|
+
}
|
|
78
|
+
/** Clear one cell row (a threshold rule paints over it). */
|
|
79
|
+
blankRow(cellY) {
|
|
80
|
+
if (cellY < 0 || cellY >= this.#rows)
|
|
81
|
+
return;
|
|
82
|
+
for (let cell = 0; cell < this.#cells; cell++) {
|
|
83
|
+
const index = cellY * this.#cells + cell;
|
|
84
|
+
this.#bits[index] = 0;
|
|
85
|
+
this.#owner[index] = -1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The canvas as one Braille string per cell row. */
|
|
89
|
+
rows() {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (let row = 0; row < this.#rows; row++) {
|
|
92
|
+
let line = "";
|
|
93
|
+
for (let cell = 0; cell < this.#cells; cell++) {
|
|
94
|
+
line += String.fromCharCode(0x2800 + this.cellBits(cell, row));
|
|
95
|
+
}
|
|
96
|
+
out.push(line);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The eighth-block ramp (index 0..8: empty through full). */
|
|
102
|
+
const BLOCK_RAMP = "▁▂▃▄▅▆▇█"; // 8 cells, index 0..7
|
|
103
|
+
/** The ASCII ramp for no-Unicode terminals (same 8 shape heights). */
|
|
104
|
+
const ASCII_RAMP = "_.,-+=*#";
|
|
105
|
+
/**
|
|
106
|
+
* A one-cell-per-sample sparkline of `values`, at most `maxCells`
|
|
107
|
+
* cells (longer series downsample with `lttb`, keeping both ends).
|
|
108
|
+
* `unicode: false` selects the ASCII ramp. Empty input renders empty;
|
|
109
|
+
* a flat series renders mid-height cells.
|
|
110
|
+
*/
|
|
111
|
+
export function sparkline(values, maxCells, unicode) {
|
|
112
|
+
if (values.length === 0 || maxCells <= 0)
|
|
113
|
+
return "";
|
|
114
|
+
const points = values.map((value, index) => [index, value]);
|
|
115
|
+
const sampled = lttb(points, Math.max(1, maxCells));
|
|
116
|
+
const ys = sampled.map((point) => point[1]);
|
|
117
|
+
const min = Math.min(...ys);
|
|
118
|
+
const max = Math.max(...ys);
|
|
119
|
+
const ramp = unicode ? BLOCK_RAMP : ASCII_RAMP;
|
|
120
|
+
// One shape per cell: floor(t * 8) clamped to the ramp (the top
|
|
121
|
+
// eighth of the range is the full block). A flat series renders
|
|
122
|
+
// mid-height (index 3 of 8).
|
|
123
|
+
return sampled
|
|
124
|
+
.map((point) => {
|
|
125
|
+
if (max === min)
|
|
126
|
+
return ramp[3];
|
|
127
|
+
const t = (point[1] - min) / (max - min);
|
|
128
|
+
return ramp[Math.max(0, Math.min(7, Math.floor(t * 8)))];
|
|
129
|
+
})
|
|
130
|
+
.join("");
|
|
131
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `chart` block (UI-blocks spec §5.4): one y-axis, mixed series
|
|
3
|
+
* kinds, thresholds as horizontal rules.
|
|
4
|
+
*
|
|
5
|
+
* - Lines and areas draw on a {@link BrailleCanvas} (2×4 dots per
|
|
6
|
+
* cell); bars draw with the block ramp `▁▂▃▄▅▆▇█` on one row per
|
|
7
|
+
* tick; stacked `area`/`bar` series accumulate.
|
|
8
|
+
* - A threshold draws as a dashed rule (`╌`, ASCII `-`) in its tone.
|
|
9
|
+
* - Below 30 cells the chart degrades to a sparkline plus the latest
|
|
10
|
+
* value per series (§12.3); below 80 the legend moves below the
|
|
11
|
+
* plot.
|
|
12
|
+
* - Long series downsample with the shared `lttb` before plotting.
|
|
13
|
+
*/
|
|
14
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
15
|
+
import type { ChartBlock, ChartSeries } from "@sigrun/shared/blocks";
|
|
16
|
+
import { type BlockCtx } from "./context.js";
|
|
17
|
+
/** `chart`: the spec §5.4 block. */
|
|
18
|
+
export declare function renderChart(block: ChartBlock, ctx: BlockCtx): Component;
|
|
19
|
+
/** Re-exported so the render dispatch imports one module per block. */
|
|
20
|
+
export type { ChartSeries };
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `chart` block (UI-blocks spec §5.4): one y-axis, mixed series
|
|
3
|
+
* kinds, thresholds as horizontal rules.
|
|
4
|
+
*
|
|
5
|
+
* - Lines and areas draw on a {@link BrailleCanvas} (2×4 dots per
|
|
6
|
+
* cell); bars draw with the block ramp `▁▂▃▄▅▆▇█` on one row per
|
|
7
|
+
* tick; stacked `area`/`bar` series accumulate.
|
|
8
|
+
* - A threshold draws as a dashed rule (`╌`, ASCII `-`) in its tone.
|
|
9
|
+
* - Below 30 cells the chart degrades to a sparkline plus the latest
|
|
10
|
+
* value per series (§12.3); below 80 the legend moves below the
|
|
11
|
+
* plot.
|
|
12
|
+
* - Long series downsample with the shared `lttb` before plotting.
|
|
13
|
+
*/
|
|
14
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
15
|
+
import { formatValue, lttb } from "@sigrun/shared/blocks";
|
|
16
|
+
import { bindRaw, bindText } from "./context.js";
|
|
17
|
+
import { BrailleCanvas, sparkline } from "./braille.js";
|
|
18
|
+
import { pad, truncate } from "./width.js";
|
|
19
|
+
import { toneFg } from "./tones.js";
|
|
20
|
+
import { Lines } from "./layout.js";
|
|
21
|
+
/** Below this width the chart is sparklines plus latest values. */
|
|
22
|
+
const SPARKLINE_BELOW = 30;
|
|
23
|
+
/** The legend sits below the plot below this width (the compact class). */
|
|
24
|
+
const LEGEND_BELOW = 80;
|
|
25
|
+
/** Plot rows for the full chart (the axes and legend add the rest). */
|
|
26
|
+
const PLOT_ROWS = 8;
|
|
27
|
+
/** The series palette (chart is the one block that goes beyond tones). */
|
|
28
|
+
const SERIES_TOKENS = ["accent", "ok", "warn", "muted", "code"];
|
|
29
|
+
/** The dashed-rule glyphs for thresholds. */
|
|
30
|
+
const DASH = "╌";
|
|
31
|
+
const DASH_ASCII = "-";
|
|
32
|
+
/** Resolve `block.series` to points (bindings and literal arrays). */
|
|
33
|
+
function seriesOf(block, ctx) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const series of block.series) {
|
|
36
|
+
const name = bindText(series.name, ctx);
|
|
37
|
+
const data = bindRaw(series.data, ctx);
|
|
38
|
+
if (!Array.isArray(data))
|
|
39
|
+
continue;
|
|
40
|
+
const points = [];
|
|
41
|
+
for (const pair of data) {
|
|
42
|
+
if (Array.isArray(pair) &&
|
|
43
|
+
typeof pair[0] === "number" &&
|
|
44
|
+
typeof pair[1] === "number") {
|
|
45
|
+
points.push([pair[0], pair[1]]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
out.push({ name, kind: series.kind, data: points });
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/** Downsample line/area series for the dot budget (spec §5.4: lttb).
|
|
53
|
+
* The original series index rides along — the downsampled copies are
|
|
54
|
+
* fresh objects, so identity lookup against `all` would miss. */
|
|
55
|
+
function displaySeries(all, cells) {
|
|
56
|
+
return all.map((series, index) => ({
|
|
57
|
+
series: series.kind === "bar"
|
|
58
|
+
? series
|
|
59
|
+
: { ...series, data: lttb(series.data, Math.max(2, cells * 2)) },
|
|
60
|
+
index,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
/** Domain/range across all series, honouring the axis's pinned bounds. */
|
|
64
|
+
function bounds(block, ctx, all) {
|
|
65
|
+
let xMin = Infinity;
|
|
66
|
+
let xMax = -Infinity;
|
|
67
|
+
let yMin = Infinity;
|
|
68
|
+
let yMax = -Infinity;
|
|
69
|
+
const stacked = block.stacked === true;
|
|
70
|
+
for (const series of all) {
|
|
71
|
+
if (stacked && series.kind !== "line")
|
|
72
|
+
continue; // sums handled below
|
|
73
|
+
for (const [x, y] of series.data) {
|
|
74
|
+
xMin = Math.min(xMin, x);
|
|
75
|
+
xMax = Math.max(xMax, x);
|
|
76
|
+
yMin = Math.min(yMin, y);
|
|
77
|
+
yMax = Math.max(yMax, y);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (stacked) {
|
|
81
|
+
// Sum same-x points across the stacked series for the range.
|
|
82
|
+
const sums = new Map();
|
|
83
|
+
for (const series of all) {
|
|
84
|
+
if (series.kind === "line")
|
|
85
|
+
continue;
|
|
86
|
+
for (const [x, y] of series.data) {
|
|
87
|
+
sums.set(x, (sums.get(x) ?? 0) + y);
|
|
88
|
+
xMin = Math.min(xMin, x);
|
|
89
|
+
xMax = Math.max(xMax, x);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const sum of sums.values()) {
|
|
93
|
+
yMin = Math.min(yMin, 0, sum);
|
|
94
|
+
yMax = Math.max(yMax, sum);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Thresholds never stretch the domain: the y axis labels the data
|
|
98
|
+
// bounds; a threshold outside them clamps to the edge rule row.
|
|
99
|
+
if (!Number.isFinite(xMin)) {
|
|
100
|
+
xMin = 0;
|
|
101
|
+
xMax = 1;
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isFinite(yMin)) {
|
|
104
|
+
yMin = 0;
|
|
105
|
+
yMax = 1;
|
|
106
|
+
}
|
|
107
|
+
// Lines never stack (the chart keeps them over any stacked fill).
|
|
108
|
+
for (const series of all) {
|
|
109
|
+
if (series.kind !== "line")
|
|
110
|
+
continue;
|
|
111
|
+
for (const [, y] of series.data) {
|
|
112
|
+
yMin = Math.min(yMin, y);
|
|
113
|
+
yMax = Math.max(yMax, y);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
yMin = block.y?.min ?? yMin;
|
|
117
|
+
yMax = block.y?.max ?? yMax;
|
|
118
|
+
if (yMax === yMin)
|
|
119
|
+
yMax = yMin + 1;
|
|
120
|
+
if (xMax === xMin)
|
|
121
|
+
xMax = xMin + 1;
|
|
122
|
+
void ctx;
|
|
123
|
+
return { xMin, xMax, yMin, yMax };
|
|
124
|
+
}
|
|
125
|
+
/** The y label for one bound: the value plus the unit. */
|
|
126
|
+
function yLabel(block, ctx, value) {
|
|
127
|
+
const unit = bindText(block.y?.unit, ctx);
|
|
128
|
+
const text = formatValue(value, block.y?.format, ctx.locale);
|
|
129
|
+
return unit === "" ? text : `${text} ${unit}`;
|
|
130
|
+
}
|
|
131
|
+
/** One x tick label. */
|
|
132
|
+
function xLabel(block, ctx, value) {
|
|
133
|
+
if (block.x.kind === "time") {
|
|
134
|
+
return formatValue(value, block.x.format ?? "time", ctx.locale, ctx.timeZone);
|
|
135
|
+
}
|
|
136
|
+
return formatValue(value, block.x.format, ctx.locale);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A left-aligned ruler of `cells` cells with `labels` spread across
|
|
140
|
+
* it (first label at the left edge, last at the right edge).
|
|
141
|
+
*/
|
|
142
|
+
function ruler(labels, cells) {
|
|
143
|
+
const out = Array.from({ length: cells }, () => " ");
|
|
144
|
+
if (labels.length === 0 || cells <= 0)
|
|
145
|
+
return out.join("");
|
|
146
|
+
const slots = Math.min(labels.length, Math.max(2, Math.floor(cells / 8)));
|
|
147
|
+
for (let i = 0; i < slots; i++) {
|
|
148
|
+
const label = labels[Math.round((i * (labels.length - 1)) / Math.max(1, slots - 1))] ?? "";
|
|
149
|
+
const at = Math.min(cells - visibleWidth(label), Math.round((i * (cells - 1)) / Math.max(1, slots - 1)));
|
|
150
|
+
if (at < 0)
|
|
151
|
+
continue;
|
|
152
|
+
for (let c = 0; c < [...label].length && at + c < cells; c++) {
|
|
153
|
+
out[at + c] = label[c] ?? " ";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out.join("");
|
|
157
|
+
}
|
|
158
|
+
/** The narrow fallback: `name <sparkline> <latest>` per series. */
|
|
159
|
+
function sparklineRows(block, ctx, width) {
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const series of seriesOf(block, ctx)) {
|
|
162
|
+
const values = series.data.map((point) => point[1]);
|
|
163
|
+
const latest = values[values.length - 1];
|
|
164
|
+
const latestText = latest === undefined
|
|
165
|
+
? ""
|
|
166
|
+
: formatValue(latest, block.y?.format, ctx.locale);
|
|
167
|
+
const budget = Math.max(1, width - visibleWidth(series.name) - visibleWidth(latestText) - 2);
|
|
168
|
+
const ramp = sparkline(values, Math.min(5, budget), ctx.unicode);
|
|
169
|
+
out.push(truncate(`${ctx.theme.bold(series.name)} ${series.kind === "bar" ? ramp : ctx.theme.fg("accent")(ramp)} ${latestText}`, width));
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/** The legend line: `name · name · name`, coloured per series. */
|
|
174
|
+
function legendLine(ctx, all, width) {
|
|
175
|
+
const sep = ctx.unicode ? " · " : " | ";
|
|
176
|
+
const parts = all.map((series, index) => {
|
|
177
|
+
const token = SERIES_TOKENS[index % SERIES_TOKENS.length] ?? "accent";
|
|
178
|
+
return ctx.theme.fg(token)(series.name);
|
|
179
|
+
});
|
|
180
|
+
return truncate(parts.join(sep), width);
|
|
181
|
+
}
|
|
182
|
+
/** The full chart (width ≥ 30). */
|
|
183
|
+
function fullChart(block, ctx, width) {
|
|
184
|
+
const title = bindText(block.title, ctx);
|
|
185
|
+
const all = seriesOf(block, ctx);
|
|
186
|
+
if (all.length === 0) {
|
|
187
|
+
return title === "" ? [] : [truncate(ctx.theme.bold(title), width)];
|
|
188
|
+
}
|
|
189
|
+
const { xMin, xMax, yMin, yMax } = bounds(block, ctx, all);
|
|
190
|
+
const labelMax = yLabel(block, ctx, yMax);
|
|
191
|
+
const labelMin = yLabel(block, ctx, yMin);
|
|
192
|
+
const gutter = Math.max(visibleWidth(labelMax), visibleWidth(labelMin)) + 1;
|
|
193
|
+
const plotCells = Math.max(1, width - gutter - 1);
|
|
194
|
+
const plotted = displaySeries(all, plotCells);
|
|
195
|
+
const unicode = ctx.unicode;
|
|
196
|
+
const canvas = new BrailleCanvas(plotCells, PLOT_ROWS);
|
|
197
|
+
const xDot = (x) => Math.round(((x - xMin) / (xMax - xMin)) * (plotCells * 2 - 1));
|
|
198
|
+
// The canvas's dot space is TOP-DOWN (Unicode cell layout); the
|
|
199
|
+
// chart's values grow upward, so y inverts here.
|
|
200
|
+
const yDot = (y) => Math.round(((yMax - y) / (yMax - yMin)) * (PLOT_ROWS * 4 - 1));
|
|
201
|
+
// Bars draw first (per x tick, as a column of blocks); lines and
|
|
202
|
+
// areas then plot over them (the last plotter owns the cell).
|
|
203
|
+
// Bars plot from the ORIGINAL series (their sparse ticks must not
|
|
204
|
+
// downsample); lines/areas plot from the downsampled copies.
|
|
205
|
+
const barSeries = all.filter((series) => series.kind === "bar");
|
|
206
|
+
const otherSeries = plotted
|
|
207
|
+
.filter(({ series }) => series.kind !== "bar")
|
|
208
|
+
.map(({ series, index }) => ({ series, index }));
|
|
209
|
+
// Cumulative heights for stacked bars (same-x points accumulate).
|
|
210
|
+
const cumulative = new Map();
|
|
211
|
+
const bars = [];
|
|
212
|
+
for (const series of barSeries) {
|
|
213
|
+
const index = all.indexOf(series);
|
|
214
|
+
for (const [x, y] of series.data) {
|
|
215
|
+
const base = block.stacked === true ? (cumulative.get(x) ?? 0) : 0;
|
|
216
|
+
cumulative.set(x, base + y);
|
|
217
|
+
bars.push({ x, from: base, to: base + y, index });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** One bar's cell text at a plot row (eighth-block partial at the top). */
|
|
221
|
+
const barAt = (bar, row) => {
|
|
222
|
+
const rowTop = yMax - (row / PLOT_ROWS) * (yMax - yMin);
|
|
223
|
+
const rowBottom = yMax - ((row + 1) / PLOT_ROWS) * (yMax - yMin);
|
|
224
|
+
if (bar.to <= rowBottom || bar.from >= rowTop)
|
|
225
|
+
return undefined;
|
|
226
|
+
const top = Math.min(bar.to, rowTop);
|
|
227
|
+
const frac = (top - rowBottom) / (rowTop - rowBottom);
|
|
228
|
+
const level = Math.max(1, Math.min(8, Math.round(frac * 8)));
|
|
229
|
+
const glyph = unicode
|
|
230
|
+
? (level === 8 ? "█" : "▁▂▃▄▅▆▇"[level - 1])
|
|
231
|
+
: (level === 8 ? "#" : "_.,-+=*"[level - 1]);
|
|
232
|
+
const token = SERIES_TOKENS[bar.index % SERIES_TOKENS.length] ?? "accent";
|
|
233
|
+
return ctx.theme.fg(token)(glyph);
|
|
234
|
+
};
|
|
235
|
+
// Lines and areas on the Braille canvas.
|
|
236
|
+
const lineCumulative = new Map();
|
|
237
|
+
if (block.stacked === true) {
|
|
238
|
+
// Areas stack: each area plots its top edge at the running sum.
|
|
239
|
+
for (const { series } of otherSeries) {
|
|
240
|
+
if (series.kind !== "area")
|
|
241
|
+
continue;
|
|
242
|
+
for (const [x, y] of series.data) {
|
|
243
|
+
lineCumulative.set(x, (lineCumulative.get(x) ?? 0) + y);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const areas = otherSeries.filter(({ series }) => series.kind === "area");
|
|
248
|
+
const lines = otherSeries.filter(({ series }) => series.kind === "line");
|
|
249
|
+
const areaTops = [];
|
|
250
|
+
const running = new Map();
|
|
251
|
+
for (const { series, index } of areas) {
|
|
252
|
+
const top = series.data.map(([x, y]) => {
|
|
253
|
+
const base = block.stacked === true ? (running.get(x) ?? 0) : 0;
|
|
254
|
+
running.set(x, base + y);
|
|
255
|
+
return [x, base + y];
|
|
256
|
+
});
|
|
257
|
+
areaTops.push({ series, top, index });
|
|
258
|
+
}
|
|
259
|
+
// Plot regardless of unicode: the canvas stores glyph-agnostic
|
|
260
|
+
// bits; only the ASSEMBLY chooses Braille glyphs vs ASCII marks.
|
|
261
|
+
{
|
|
262
|
+
// Areas: fill from the axis (dot-space bottom row) up to the top edge.
|
|
263
|
+
const bottomDot = PLOT_ROWS * 4 - 1;
|
|
264
|
+
for (const { top, index } of areaTops) {
|
|
265
|
+
for (let i = 0; i < top.length - 1; i++) {
|
|
266
|
+
const a = top[i];
|
|
267
|
+
const b = top[i + 1];
|
|
268
|
+
const xa = xDot(a[0]);
|
|
269
|
+
const xb = xDot(b[0]);
|
|
270
|
+
for (let x = Math.min(xa, xb); x <= Math.max(xa, xb); x++) {
|
|
271
|
+
const t = xa === xb ? 0 : (x - xa) / (xb - xa);
|
|
272
|
+
const y = yDot(a[1] + (b[1] - a[1]) * t);
|
|
273
|
+
canvas.fill(x, y, x, bottomDot, index);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Lines (and area top edges) plot over the fills.
|
|
278
|
+
const strokes = [
|
|
279
|
+
...areaTops.map(({ top, index }) => ({ data: top, index })),
|
|
280
|
+
...lines.map(({ series, index }) => ({ data: series.data, index })),
|
|
281
|
+
];
|
|
282
|
+
for (const { data, index } of strokes) {
|
|
283
|
+
for (let i = 0; i < data.length - 1; i++) {
|
|
284
|
+
const a = data[i];
|
|
285
|
+
const b = data[i + 1];
|
|
286
|
+
canvas.line(xDot(a[0]), yDot(a[1]), xDot(b[0]), yDot(b[1]), index);
|
|
287
|
+
}
|
|
288
|
+
if (data.length === 1) {
|
|
289
|
+
const only = data[0];
|
|
290
|
+
canvas.plot(xDot(only[0]), yDot(only[1]), index);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Thresholds: one dashed rule per threshold, at its plot row; the
|
|
295
|
+
// row is blanked first so the dashes paint over the series.
|
|
296
|
+
const thresholdRows = (block.thresholds ?? []).map((threshold) => ({
|
|
297
|
+
threshold,
|
|
298
|
+
row: Math.max(0, Math.min(PLOT_ROWS - 1, Math.round(((yMax - threshold.at) / (yMax - yMin)) * (PLOT_ROWS - 1)))),
|
|
299
|
+
}));
|
|
300
|
+
for (const { row } of thresholdRows)
|
|
301
|
+
canvas.blankRow(row);
|
|
302
|
+
// Assemble the plot rows.
|
|
303
|
+
const plotLines = [];
|
|
304
|
+
for (let row = 0; row < PLOT_ROWS; row++) {
|
|
305
|
+
let gutterText = " ".repeat(gutter);
|
|
306
|
+
if (row === 0)
|
|
307
|
+
gutterText = pad(labelMax, gutter);
|
|
308
|
+
if (row === PLOT_ROWS - 1)
|
|
309
|
+
gutterText = pad(labelMin, gutter);
|
|
310
|
+
let body;
|
|
311
|
+
const rule = thresholdRows.find((entry) => entry.row === row);
|
|
312
|
+
if (rule !== undefined) {
|
|
313
|
+
const glyph = unicode ? DASH : DASH_ASCII;
|
|
314
|
+
const label = bindText(rule.threshold.label, ctx);
|
|
315
|
+
// Label FIRST: terminal themes wrap the rule in zero-width SGR,
|
|
316
|
+
// and a tail label would sit past the width clamp when a theme's
|
|
317
|
+
// markers are not ANSI (test themes mark tones visibly).
|
|
318
|
+
const dashes = glyph.repeat(Math.max(0, plotCells - (label === "" ? 0 : visibleWidth(label) + 1)));
|
|
319
|
+
const dashed = label === "" ? dashes : `${label} ${dashes}`;
|
|
320
|
+
body = toneFg(ctx.theme, rule.threshold.tone)(dashed);
|
|
321
|
+
}
|
|
322
|
+
else if (unicode) {
|
|
323
|
+
const raw = canvas.rows()[row] ?? "";
|
|
324
|
+
let painted = "";
|
|
325
|
+
for (let cell = 0; cell < plotCells; cell++) {
|
|
326
|
+
const ch = raw[cell] ?? " ";
|
|
327
|
+
// Stacked bars share one x cell: pick the segment whose
|
|
328
|
+
// value range covers THIS row (not just the first at x).
|
|
329
|
+
let barGlyph;
|
|
330
|
+
for (const one of bars) {
|
|
331
|
+
const xCell = Math.round(((one.x - xMin) / (xMax - xMin)) * (plotCells - 1));
|
|
332
|
+
if (xCell !== cell)
|
|
333
|
+
continue;
|
|
334
|
+
barGlyph = barAt(one, row);
|
|
335
|
+
if (barGlyph !== undefined)
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (barGlyph !== undefined && (ch === " " || canvas.cellOwner(cell, row) < 0)) {
|
|
339
|
+
painted += barGlyph;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const owner = canvas.cellOwner(cell, row);
|
|
343
|
+
const token = SERIES_TOKENS[(owner < 0 ? 0 : owner) % SERIES_TOKENS.length] ?? "accent";
|
|
344
|
+
painted += owner < 0 || ch === " " ? " " : ctx.theme.fg(token)(ch);
|
|
345
|
+
}
|
|
346
|
+
body = painted;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// ASCII: one marker per occupied cell ("*", line "/" for slopes).
|
|
350
|
+
let ascii = "";
|
|
351
|
+
for (let cell = 0; cell < plotCells; cell++) {
|
|
352
|
+
let barGlyph;
|
|
353
|
+
for (const one of bars) {
|
|
354
|
+
const xCell = Math.round(((one.x - xMin) / (xMax - xMin)) * (plotCells - 1));
|
|
355
|
+
if (xCell !== cell)
|
|
356
|
+
continue;
|
|
357
|
+
barGlyph = barAt(one, row);
|
|
358
|
+
if (barGlyph !== undefined)
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (barGlyph !== undefined) {
|
|
362
|
+
ascii += barGlyph;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const bits = canvas.cellBits(cell, row);
|
|
366
|
+
ascii += bits === 0 ? " " : "*";
|
|
367
|
+
}
|
|
368
|
+
body = ascii;
|
|
369
|
+
}
|
|
370
|
+
plotLines.push(pad(truncate(gutterText + body, width), width));
|
|
371
|
+
}
|
|
372
|
+
// The x axis: tick labels across the plot width.
|
|
373
|
+
const tickCount = Math.min(6, Math.max(2, Math.floor(plotCells / 12)));
|
|
374
|
+
const ticks = [];
|
|
375
|
+
for (let i = 0; i < tickCount; i++) {
|
|
376
|
+
ticks.push(xLabel(block, ctx, xMin + ((xMax - xMin) * i) / (tickCount - 1)));
|
|
377
|
+
}
|
|
378
|
+
plotLines.push(pad(truncate(" ".repeat(gutter) + ruler(ticks, plotCells), width), width));
|
|
379
|
+
const legend = legendLine(ctx, all, width);
|
|
380
|
+
const out = [];
|
|
381
|
+
if (title !== "")
|
|
382
|
+
out.push(truncate(ctx.theme.bold(title), width));
|
|
383
|
+
if (width >= LEGEND_BELOW)
|
|
384
|
+
out.push(legend);
|
|
385
|
+
out.push(...plotLines);
|
|
386
|
+
if (width < LEGEND_BELOW)
|
|
387
|
+
out.push(legend);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
/** `chart`: the spec §5.4 block. */
|
|
391
|
+
export function renderChart(block, ctx) {
|
|
392
|
+
return new Lines((width) => {
|
|
393
|
+
if (width < SPARKLINE_BELOW)
|
|
394
|
+
return sparklineRows(block, ctx, width);
|
|
395
|
+
return fullChart(block, ctx, width);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TUI's chat-block transport (UI-blocks U5 Task 4, spec §8.2): the
|
|
3
|
+
* shared `SurfaceTransport` contract over the chat-block routes
|
|
4
|
+
* (`/v1/conversations/:id/blocks/:entryId`, `…/data?since=`, `POST
|
|
5
|
+
* …/actions`) — the port of the webui's `chatTransport.ts`, addressed
|
|
6
|
+
* by (conversationId, entryId) instead of (slug, id, params).
|
|
7
|
+
*
|
|
8
|
+
* Unlike the TUI's ext transport there is no `X-Sigrun-Stream` watcher
|
|
9
|
+
* registration: chat change events are conversation-scoped (the events
|
|
10
|
+
* route delivers them to every stream whose principal sees the
|
|
11
|
+
* conversation), so `setStreamId` does not exist here. The launcher's
|
|
12
|
+
* events loop hands the conversation-scoped frames to
|
|
13
|
+
* {@link ChatSurfaceHub.dispatch}.
|
|
14
|
+
*/
|
|
15
|
+
import type { SurfaceTransport } from "@sigrun/shared/blocks";
|
|
16
|
+
export interface ChatTransportOptions {
|
|
17
|
+
readonly conversationId: number;
|
|
18
|
+
readonly entryId: string;
|
|
19
|
+
/** The server's origin (the launcher owns it). */
|
|
20
|
+
readonly baseUrl: string;
|
|
21
|
+
readonly user?: string;
|
|
22
|
+
readonly token?: string;
|
|
23
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
24
|
+
}
|
|
25
|
+
/** Create the TUI's chat-block transport. */
|
|
26
|
+
export declare function createChatTransport(options: ChatTransportOptions): SurfaceTransport;
|
|
27
|
+
/**
|
|
28
|
+
* The TUI's chat-block change router: `attach` creates a transport whose
|
|
29
|
+
* `onChanged` subscribes into this hub under (conversationId, entryId);
|
|
30
|
+
* `dispatch` fans a conversation-scoped `surface.changed` frame out to
|
|
31
|
+
* the matching subscriptions only.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ChatSurfaceHub {
|
|
34
|
+
#private;
|
|
35
|
+
/** Create a transport whose `onChanged` subscribes into this hub. */
|
|
36
|
+
attach(options: ChatTransportOptions): SurfaceTransport;
|
|
37
|
+
/** Fire every subscription matching the event's (conversationId, entryId). */
|
|
38
|
+
dispatch(event: {
|
|
39
|
+
readonly conversationId: number;
|
|
40
|
+
readonly entryId: string;
|
|
41
|
+
readonly rev: number;
|
|
42
|
+
readonly treeRev?: number;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|