@sigrun/tui 0.1.1 → 0.2.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 (108) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/dist/actions.d.ts +0 -119
  4. package/dist/actions.js +0 -12
  5. package/dist/api.d.ts +0 -152
  6. package/dist/api.js +0 -189
  7. package/dist/blocks/braille.d.ts +0 -41
  8. package/dist/blocks/braille.js +0 -131
  9. package/dist/blocks/chart.d.ts +0 -20
  10. package/dist/blocks/chart.js +0 -397
  11. package/dist/blocks/collection/calendar.d.ts +0 -28
  12. package/dist/blocks/collection/calendar.js +0 -232
  13. package/dist/blocks/collection/card.d.ts +0 -36
  14. package/dist/blocks/collection/card.js +0 -128
  15. package/dist/blocks/collection/collection.d.ts +0 -9
  16. package/dist/blocks/collection/collection.js +0 -26
  17. package/dist/blocks/collection/grid.d.ts +0 -18
  18. package/dist/blocks/collection/grid.js +0 -72
  19. package/dist/blocks/collection/lanes.d.ts +0 -32
  20. package/dist/blocks/collection/lanes.js +0 -320
  21. package/dist/blocks/collection/list.d.ts +0 -30
  22. package/dist/blocks/collection/list.js +0 -273
  23. package/dist/blocks/collection/move.d.ts +0 -67
  24. package/dist/blocks/collection/move.js +0 -96
  25. package/dist/blocks/collection/shared.d.ts +0 -87
  26. package/dist/blocks/collection/shared.js +0 -177
  27. package/dist/blocks/collection/table.d.ts +0 -26
  28. package/dist/blocks/collection/table.js +0 -221
  29. package/dist/blocks/content.d.ts +0 -48
  30. package/dist/blocks/content.js +0 -253
  31. package/dist/blocks/context.d.ts +0 -96
  32. package/dist/blocks/context.js +0 -65
  33. package/dist/blocks/diff.d.ts +0 -21
  34. package/dist/blocks/diff.js +0 -25
  35. package/dist/blocks/ext-transport.d.ts +0 -70
  36. package/dist/blocks/ext-transport.js +0 -165
  37. package/dist/blocks/form/choice.d.ts +0 -54
  38. package/dist/blocks/form/choice.js +0 -175
  39. package/dist/blocks/form/fields.d.ts +0 -56
  40. package/dist/blocks/form/fields.js +0 -136
  41. package/dist/blocks/form/file.d.ts +0 -61
  42. package/dist/blocks/form/file.js +0 -121
  43. package/dist/blocks/form/form.d.ts +0 -53
  44. package/dist/blocks/form/form.js +0 -703
  45. package/dist/blocks/form/list.d.ts +0 -37
  46. package/dist/blocks/form/list.js +0 -70
  47. package/dist/blocks/form/values.d.ts +0 -20
  48. package/dist/blocks/form/values.js +0 -104
  49. package/dist/blocks/icons.d.ts +0 -14
  50. package/dist/blocks/icons.js +0 -74
  51. package/dist/blocks/image.d.ts +0 -39
  52. package/dist/blocks/image.js +0 -82
  53. package/dist/blocks/layout.d.ts +0 -31
  54. package/dist/blocks/layout.js +0 -155
  55. package/dist/blocks/log.d.ts +0 -24
  56. package/dist/blocks/log.js +0 -111
  57. package/dist/blocks/meter.d.ts +0 -16
  58. package/dist/blocks/meter.js +0 -50
  59. package/dist/blocks/page.d.ts +0 -54
  60. package/dist/blocks/page.js +0 -355
  61. package/dist/blocks/render.d.ts +0 -31
  62. package/dist/blocks/render.js +0 -145
  63. package/dist/blocks/surface-view.d.ts +0 -33
  64. package/dist/blocks/surface-view.js +0 -86
  65. package/dist/blocks/surface.d.ts +0 -75
  66. package/dist/blocks/surface.js +0 -468
  67. package/dist/blocks/tones.d.ts +0 -23
  68. package/dist/blocks/tones.js +0 -63
  69. package/dist/blocks/treeview.d.ts +0 -22
  70. package/dist/blocks/treeview.js +0 -121
  71. package/dist/blocks/width.d.ts +0 -32
  72. package/dist/blocks/width.js +0 -70
  73. package/dist/clipboard.d.ts +0 -20
  74. package/dist/clipboard.js +0 -19
  75. package/dist/editor-launch.d.ts +0 -34
  76. package/dist/editor-launch.js +0 -70
  77. package/dist/keymaps/emacs.d.ts +0 -18
  78. package/dist/keymaps/emacs.js +0 -172
  79. package/dist/keymaps/standard.d.ts +0 -17
  80. package/dist/keymaps/standard.js +0 -175
  81. package/dist/keymaps/types.d.ts +0 -43
  82. package/dist/keymaps/types.js +0 -28
  83. package/dist/keymaps/vim.d.ts +0 -48
  84. package/dist/keymaps/vim.js +0 -291
  85. package/dist/launch.d.ts +0 -149
  86. package/dist/launch.js +0 -446
  87. package/dist/sse.d.ts +0 -109
  88. package/dist/sse.js +0 -401
  89. package/dist/theme.d.ts +0 -51
  90. package/dist/theme.js +0 -119
  91. package/dist/ui/ambient.d.ts +0 -81
  92. package/dist/ui/ambient.js +0 -301
  93. package/dist/ui/app.d.ts +0 -245
  94. package/dist/ui/app.js +0 -1325
  95. package/dist/ui/composer.d.ts +0 -66
  96. package/dist/ui/composer.js +0 -125
  97. package/dist/ui/conversation-pane.d.ts +0 -123
  98. package/dist/ui/conversation-pane.js +0 -463
  99. package/dist/ui/help.d.ts +0 -15
  100. package/dist/ui/help.js +0 -80
  101. package/dist/ui/interactions.d.ts +0 -90
  102. package/dist/ui/interactions.js +0 -330
  103. package/dist/ui/picker.d.ts +0 -26
  104. package/dist/ui/picker.js +0 -130
  105. package/dist/ui/sidebar.d.ts +0 -99
  106. package/dist/ui/sidebar.js +0 -269
  107. package/dist/ui/status-bar.d.ts +0 -77
  108. package/dist/ui/status-bar.js +0 -146
@@ -1,20 +0,0 @@
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 };
@@ -1,397 +0,0 @@
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
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * The `calendar` display (UI-blocks U2 Task 4, spec §5.5 + §12.3 +
3
- * §12.4): a month grid at regular/wide (day-rows — the same v1 shape
4
- * as U1's grid, not a weekday-aligned matrix); compact shows the
5
- * agenda (§12.3), as does `time.range: "agenda"` at any width.
6
- *
7
- * Keyboard (§12.4): `[`/`]` shift the visible month, `t` returns to
8
- * today — all view-only, nothing dispatches. `m` on an event picks it
9
- * up; arrows move it by DAY; Enter drops, dispatching `onMove` with
10
- * `{itemId, to: {start, end}}` — the duration preserved by shifting
11
- * start AND end by the day delta (the same payload as U1's pointer
12
- * drop). Esc cancels.
13
- *
14
- * The v1 TUI calendar has no pointer create/resize gestures (`onCreate`
15
- * and `onResize` stay U1-pointer-only affordances; noted in the task
16
- * report).
17
- */
18
- import type { Component } from "@earendil-works/pi-tui";
19
- import type { CollectionBlock } from "@sigrun/shared/blocks";
20
- import type { BlockCtx } from "../context.js";
21
- /** The calendar display. */
22
- export declare class CalendarComponent implements Component {
23
- #private;
24
- constructor(block: CollectionBlock, ctx: BlockCtx);
25
- render(width: number): string[];
26
- handleInput(data: string): void;
27
- invalidate(): void;
28
- }
@@ -1,232 +0,0 @@
1
- /**
2
- * The `calendar` display (UI-blocks U2 Task 4, spec §5.5 + §12.3 +
3
- * §12.4): a month grid at regular/wide (day-rows — the same v1 shape
4
- * as U1's grid, not a weekday-aligned matrix); compact shows the
5
- * agenda (§12.3), as does `time.range: "agenda"` at any width.
6
- *
7
- * Keyboard (§12.4): `[`/`]` shift the visible month, `t` returns to
8
- * today — all view-only, nothing dispatches. `m` on an event picks it
9
- * up; arrows move it by DAY; Enter drops, dispatching `onMove` with
10
- * `{itemId, to: {start, end}}` — the duration preserved by shifting
11
- * start AND end by the day delta (the same payload as U1's pointer
12
- * drop). Esc cancels.
13
- *
14
- * The v1 TUI calendar has no pointer create/resize gestures (`onCreate`
15
- * and `onResize` stay U1-pointer-only affordances; noted in the task
16
- * report).
17
- */
18
- import { truncate, widthClassOf } from "../width.js";
19
- import { cardLines } from "./card.js";
20
- import { CollectionState, collectionItems, dispatchActivate, sortItems, } from "./shared.js";
21
- /** The day key (YYYY-MM-DD) an ISO timestamp falls on. */
22
- function dayKey(iso) {
23
- return iso.slice(0, 10);
24
- }
25
- /** Shift a day key by `days`. */
26
- function shiftDay(key, days) {
27
- const date = new Date(`${key}T00:00:00Z`);
28
- date.setUTCDate(date.getUTCDate() + days);
29
- return date.toISOString().slice(0, 10);
30
- }
31
- /** Shift an ISO instant by whole days, keeping its clock time. */
32
- function shiftInstant(iso, days) {
33
- const date = new Date(iso);
34
- date.setUTCDate(date.getUTCDate() + days);
35
- return date.toISOString().replace(/\.\d{3}Z$/, "Z");
36
- }
37
- /** Today's month key (view-range default; fake timers drive tests). */
38
- function currentMonth() {
39
- return new Date().toISOString().slice(0, 7);
40
- }
41
- /** Shift a month key by `months`. */
42
- function shiftMonth(key, months) {
43
- const [yearRaw, monthRaw] = key.split("-");
44
- const year = Number(yearRaw);
45
- const month = Number(monthRaw) - 1 + months;
46
- return new Date(Date.UTC(year, month, 1)).toISOString().slice(0, 7);
47
- }
48
- /** The days a month grid renders (1st through the month's end). */
49
- function daysInMonth(month) {
50
- const [yearRaw, monthRaw] = month.split("-");
51
- const year = Number(yearRaw);
52
- const m = Number(monthRaw);
53
- const days = [];
54
- const date = new Date(Date.UTC(year, m - 1, 1));
55
- while (date.getUTCMonth() === m - 1) {
56
- days.push(date.toISOString().slice(0, 10));
57
- date.setUTCDate(date.getUTCDate() + 1);
58
- }
59
- return days;
60
- }
61
- /** An item's start/end ISO strings under the block's `time` config. */
62
- function itemRange(block, item) {
63
- const time = block.time;
64
- if (time === undefined)
65
- return undefined;
66
- if (typeof item !== "object" || item === null || Array.isArray(item))
67
- return undefined;
68
- const record = item;
69
- const start = record[time.start];
70
- if (typeof start !== "string")
71
- return undefined;
72
- const end = time.end === undefined ? undefined : record[time.end];
73
- return { start, end: typeof end === "string" ? end : undefined };
74
- }
75
- /** The calendar display. */
76
- export class CalendarComponent {
77
- #block;
78
- #ctx;
79
- #state = new CollectionState();
80
- #tick = 0;
81
- /** The visible month (view-only; `[`/`]`/`t` shift it). */
82
- #month = currentMonth();
83
- /** The agenda cursor (a flat item index over the agenda order). */
84
- #cursor = 0;
85
- /** The picked-up event (calendar moves use a day placement, not the
86
- * lanes' group/index placement — the session wrapper tracks it). */
87
- #held;
88
- constructor(block, ctx) {
89
- this.#block = block;
90
- this.#ctx = ctx;
91
- }
92
- /** Every item with a parseable time range, in start order. */
93
- #events() {
94
- const rows = [];
95
- for (const row of sortItems(collectionItems(this.#block, this.#ctx), this.#block.sort)) {
96
- const range = itemRange(this.#block, row.item);
97
- if (range === undefined)
98
- continue;
99
- rows.push({ row, start: range.start, end: range.end });
100
- }
101
- rows.sort((a, b) => a.start.localeCompare(b.start));
102
- return rows;
103
- }
104
- render(width) {
105
- this.#tick += 1;
106
- const compact = widthClassOf(width) === "compact" || this.#block.time?.range === "agenda";
107
- const events = this.#events();
108
- if (compact)
109
- return this.#agenda(width, events);
110
- return this.#monthGrid(width, events);
111
- }
112
- /** The agenda: items sorted by start, one row each. */
113
- #agenda(width, events) {
114
- const lines = [this.#ctx.theme.dim(truncate(this.#month, width))];
115
- events.forEach((event, index) => {
116
- const cursor = index === this.#cursor ? "> " : " ";
117
- const held = this.#held?.itemId === event.row.id;
118
- const mark = held ? (this.#ctx.unicode ? "✋ " : "* ") : "";
119
- const card = cardLines(this.#block.item, event.row.item, this.#ctx, width - cursor.length - mark.length - 12, 0, this.#tick);
120
- const when = `${event.start.slice(0, 10)} `;
121
- const [first, ...rest] = card;
122
- lines.push(truncate(`${cursor}${mark}${when}${first ?? ""}`, width));
123
- for (const line of rest) {
124
- lines.push(truncate(`${cursor}${" ".repeat(mark.length + when.length)}${line}`, width));
125
- }
126
- });
127
- return lines;
128
- }
129
- /** The month grid: one row per day, events under their day. */
130
- #monthGrid(width, events) {
131
- const byDay = new Map();
132
- for (const event of events) {
133
- const day = dayKey(event.start);
134
- const list = byDay.get(day) ?? [];
135
- list.push(event);
136
- byDay.set(day, list);
137
- }
138
- const lines = [this.#ctx.theme.bold(truncate(this.#month, width))];
139
- for (const day of daysInMonth(this.#month)) {
140
- const todays = byDay.get(day) ?? [];
141
- const heldHere = this.#held !== undefined && this.#held.at === day;
142
- const marker = heldHere ? (this.#ctx.unicode ? " ⌄" : " v") : "";
143
- lines.push(truncate(`${day.slice(8)}${marker}`, width));
144
- for (const event of todays) {
145
- const held = this.#held?.itemId === event.row.id;
146
- const mark = held ? (this.#ctx.unicode ? "✋ " : "* ") : "";
147
- const card = cardLines(this.#block.item, event.row.item, this.#ctx, width - 2 - mark.length, 0, this.#tick);
148
- const [first, ...rest] = card;
149
- lines.push(truncate(` ${mark}${first ?? ""}`, width));
150
- for (const line of rest) {
151
- lines.push(truncate(` ${" ".repeat(mark.length)}${line}`, width));
152
- }
153
- }
154
- }
155
- return lines;
156
- }
157
- handleInput(data) {
158
- // Range keys are always view-only (§12.4).
159
- if (data === "[") {
160
- this.#month = shiftMonth(this.#month, -1);
161
- return;
162
- }
163
- if (data === "]") {
164
- this.#month = shiftMonth(this.#month, 1);
165
- return;
166
- }
167
- if (data === "t") {
168
- this.#month = currentMonth();
169
- return;
170
- }
171
- const events = this.#events();
172
- // A pick-up in flight: arrows move the event by DAY.
173
- const held = this.#held;
174
- if (held !== undefined) {
175
- if (data === "\x1b") {
176
- this.#held = undefined;
177
- return;
178
- }
179
- let delta = 0;
180
- if (data === "\x1b[C" || data === "\x1b[B")
181
- delta = 1;
182
- if (data === "\x1b[D" || data === "\x1b[A")
183
- delta = -1;
184
- if (delta !== 0) {
185
- this.#held = { ...held, at: shiftDay(held.at, delta) };
186
- return;
187
- }
188
- if (data === "\r") {
189
- this.#held = undefined;
190
- const days = (Date.parse(`${held.at}T00:00:00Z`) -
191
- Date.parse(`${dayKey(held.start)}T00:00:00Z`)) /
192
- 86_400_000;
193
- if (days === 0)
194
- return;
195
- const onMove = this.#block.onMove;
196
- if (onMove === undefined)
197
- return;
198
- const start = shiftInstant(held.start, days);
199
- const end = held.end === undefined ? undefined : shiftInstant(held.end, days);
200
- const to = end === undefined ? { start } : { start, end };
201
- this.#ctx.dispatch({ type: "move", name: onMove }, { itemId: held.itemId, to });
202
- }
203
- return;
204
- }
205
- if (this.#cursor >= events.length)
206
- this.#cursor = Math.max(0, events.length - 1);
207
- const current = events[this.#cursor];
208
- if (data === "\x1b[B") {
209
- this.#cursor = Math.min(events.length - 1, this.#cursor + 1);
210
- return;
211
- }
212
- if (data === "\x1b[A") {
213
- this.#cursor = Math.max(0, this.#cursor - 1);
214
- return;
215
- }
216
- if (data === "m" && current !== undefined && this.#block.onMove !== undefined) {
217
- this.#held = {
218
- itemId: current.row.id,
219
- start: current.start,
220
- end: current.end,
221
- at: dayKey(current.start),
222
- };
223
- return;
224
- }
225
- if (data === "\r" && current !== undefined) {
226
- dispatchActivate(this.#block, this.#ctx, current.row);
227
- }
228
- }
229
- invalidate() {
230
- // State (range, cursor, the pick-up) survives re-renders.
231
- }
232
- }