@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.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/dist/actions.d.ts +0 -119
- package/dist/actions.js +0 -12
- package/dist/api.d.ts +0 -152
- package/dist/api.js +0 -189
- package/dist/blocks/braille.d.ts +0 -41
- package/dist/blocks/braille.js +0 -131
- package/dist/blocks/chart.d.ts +0 -20
- package/dist/blocks/chart.js +0 -397
- package/dist/blocks/collection/calendar.d.ts +0 -28
- package/dist/blocks/collection/calendar.js +0 -232
- package/dist/blocks/collection/card.d.ts +0 -36
- package/dist/blocks/collection/card.js +0 -128
- package/dist/blocks/collection/collection.d.ts +0 -9
- package/dist/blocks/collection/collection.js +0 -26
- package/dist/blocks/collection/grid.d.ts +0 -18
- package/dist/blocks/collection/grid.js +0 -72
- package/dist/blocks/collection/lanes.d.ts +0 -32
- package/dist/blocks/collection/lanes.js +0 -320
- package/dist/blocks/collection/list.d.ts +0 -30
- package/dist/blocks/collection/list.js +0 -273
- package/dist/blocks/collection/move.d.ts +0 -67
- package/dist/blocks/collection/move.js +0 -96
- package/dist/blocks/collection/shared.d.ts +0 -87
- package/dist/blocks/collection/shared.js +0 -177
- package/dist/blocks/collection/table.d.ts +0 -26
- package/dist/blocks/collection/table.js +0 -221
- package/dist/blocks/content.d.ts +0 -48
- package/dist/blocks/content.js +0 -253
- package/dist/blocks/context.d.ts +0 -96
- package/dist/blocks/context.js +0 -65
- package/dist/blocks/diff.d.ts +0 -21
- package/dist/blocks/diff.js +0 -25
- package/dist/blocks/ext-transport.d.ts +0 -70
- package/dist/blocks/ext-transport.js +0 -165
- package/dist/blocks/form/choice.d.ts +0 -54
- package/dist/blocks/form/choice.js +0 -175
- package/dist/blocks/form/fields.d.ts +0 -56
- package/dist/blocks/form/fields.js +0 -136
- package/dist/blocks/form/file.d.ts +0 -61
- package/dist/blocks/form/file.js +0 -121
- package/dist/blocks/form/form.d.ts +0 -53
- package/dist/blocks/form/form.js +0 -703
- package/dist/blocks/form/list.d.ts +0 -37
- package/dist/blocks/form/list.js +0 -70
- package/dist/blocks/form/values.d.ts +0 -20
- package/dist/blocks/form/values.js +0 -104
- package/dist/blocks/icons.d.ts +0 -14
- package/dist/blocks/icons.js +0 -74
- package/dist/blocks/image.d.ts +0 -39
- package/dist/blocks/image.js +0 -82
- package/dist/blocks/layout.d.ts +0 -31
- package/dist/blocks/layout.js +0 -155
- package/dist/blocks/log.d.ts +0 -24
- package/dist/blocks/log.js +0 -111
- package/dist/blocks/meter.d.ts +0 -16
- package/dist/blocks/meter.js +0 -50
- package/dist/blocks/page.d.ts +0 -54
- package/dist/blocks/page.js +0 -355
- package/dist/blocks/render.d.ts +0 -31
- package/dist/blocks/render.js +0 -145
- package/dist/blocks/surface-view.d.ts +0 -33
- package/dist/blocks/surface-view.js +0 -86
- package/dist/blocks/surface.d.ts +0 -75
- package/dist/blocks/surface.js +0 -468
- package/dist/blocks/tones.d.ts +0 -23
- package/dist/blocks/tones.js +0 -63
- package/dist/blocks/treeview.d.ts +0 -22
- package/dist/blocks/treeview.js +0 -121
- package/dist/blocks/width.d.ts +0 -32
- package/dist/blocks/width.js +0 -70
- package/dist/clipboard.d.ts +0 -20
- package/dist/clipboard.js +0 -19
- package/dist/editor-launch.d.ts +0 -34
- package/dist/editor-launch.js +0 -70
- package/dist/keymaps/emacs.d.ts +0 -18
- package/dist/keymaps/emacs.js +0 -172
- package/dist/keymaps/standard.d.ts +0 -17
- package/dist/keymaps/standard.js +0 -175
- package/dist/keymaps/types.d.ts +0 -43
- package/dist/keymaps/types.js +0 -28
- package/dist/keymaps/vim.d.ts +0 -48
- package/dist/keymaps/vim.js +0 -291
- package/dist/launch.d.ts +0 -149
- package/dist/launch.js +0 -446
- package/dist/sse.d.ts +0 -109
- package/dist/sse.js +0 -401
- package/dist/theme.d.ts +0 -51
- package/dist/theme.js +0 -119
- package/dist/ui/ambient.d.ts +0 -81
- package/dist/ui/ambient.js +0 -301
- package/dist/ui/app.d.ts +0 -245
- package/dist/ui/app.js +0 -1325
- package/dist/ui/composer.d.ts +0 -66
- package/dist/ui/composer.js +0 -125
- package/dist/ui/conversation-pane.d.ts +0 -123
- package/dist/ui/conversation-pane.js +0 -463
- package/dist/ui/help.d.ts +0 -15
- package/dist/ui/help.js +0 -80
- package/dist/ui/interactions.d.ts +0 -90
- package/dist/ui/interactions.js +0 -330
- package/dist/ui/picker.d.ts +0 -26
- package/dist/ui/picker.js +0 -130
- package/dist/ui/sidebar.d.ts +0 -99
- package/dist/ui/sidebar.js +0 -269
- package/dist/ui/status-bar.d.ts +0 -77
- package/dist/ui/status-bar.js +0 -146
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The card template (UI-blocks spec §5.5.1) as terminal lines: a FIXED
|
|
3
|
-
* line layout, every slot optional except `title`, each slot resolved
|
|
4
|
-
* against the item (`./` pointers).
|
|
5
|
-
*
|
|
6
|
-
* Layout, top to bottom:
|
|
7
|
-
* 1. eyebrow line (dimmed small text)
|
|
8
|
-
* 2. title line: icon glyph, tone marker, title (title carries the
|
|
9
|
-
* tone's colour through TuiTheme)
|
|
10
|
-
* 3. subtitle (one line)
|
|
11
|
-
* 4. body, wrapped and clamped to 4 lines (spec: lists and lanes clamp;
|
|
12
|
-
* the TUI clamps in every display — a 100-cell grid card cannot
|
|
13
|
-
* afford a full body either, and `detail` panes are Task 4's)
|
|
14
|
-
* 5. badges + meta on ONE line (badges first, then meta facts)
|
|
15
|
-
* 6. metrics, one per line (`label: value`, `formatValue` formatted)
|
|
16
|
-
* 7. status: spinner frame (when `spinner` binds true) + tone marker +
|
|
17
|
-
* label — the frame ticks per render (`spinnerFrame`)
|
|
18
|
-
* 8. deps as `after: id, id, …`
|
|
19
|
-
*
|
|
20
|
-
* The card `tone` marker also leads the title; with no tone binding the
|
|
21
|
-
* marker is the neutral one so lines align.
|
|
22
|
-
*/
|
|
23
|
-
import type { Card, JsonValue } from "@sigrun/shared/blocks";
|
|
24
|
-
import { type BlockCtx } from "../context.js";
|
|
25
|
-
/** The body clamp (spec §5.5.1: 4 lines in lists and lanes). */
|
|
26
|
-
export declare const CARD_BODY_LINES = 4;
|
|
27
|
-
/** The spinner frame for render `tick` (a render-indexed clock: the TUI
|
|
28
|
-
* has no card-local timers, so a spinning status advances as renders
|
|
29
|
-
* arrive — the same convention ambient rows use). */
|
|
30
|
-
export declare function spinnerFrame(tick: number, ascii: boolean): string;
|
|
31
|
-
/**
|
|
32
|
-
* The card's lines for `item` at `width`, indented `indent` cells.
|
|
33
|
-
* `tick` indexes the spinner frame (the owning component's render
|
|
34
|
-
* count). Lines are display-width truncated, never wider than the pane.
|
|
35
|
-
*/
|
|
36
|
-
export declare function cardLines(card: Card, item: JsonValue, ctx: BlockCtx, width: number, indent: number, tick: number): string[];
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The card template (UI-blocks spec §5.5.1) as terminal lines: a FIXED
|
|
3
|
-
* line layout, every slot optional except `title`, each slot resolved
|
|
4
|
-
* against the item (`./` pointers).
|
|
5
|
-
*
|
|
6
|
-
* Layout, top to bottom:
|
|
7
|
-
* 1. eyebrow line (dimmed small text)
|
|
8
|
-
* 2. title line: icon glyph, tone marker, title (title carries the
|
|
9
|
-
* tone's colour through TuiTheme)
|
|
10
|
-
* 3. subtitle (one line)
|
|
11
|
-
* 4. body, wrapped and clamped to 4 lines (spec: lists and lanes clamp;
|
|
12
|
-
* the TUI clamps in every display — a 100-cell grid card cannot
|
|
13
|
-
* afford a full body either, and `detail` panes are Task 4's)
|
|
14
|
-
* 5. badges + meta on ONE line (badges first, then meta facts)
|
|
15
|
-
* 6. metrics, one per line (`label: value`, `formatValue` formatted)
|
|
16
|
-
* 7. status: spinner frame (when `spinner` binds true) + tone marker +
|
|
17
|
-
* label — the frame ticks per render (`spinnerFrame`)
|
|
18
|
-
* 8. deps as `after: id, id, …`
|
|
19
|
-
*
|
|
20
|
-
* The card `tone` marker also leads the title; with no tone binding the
|
|
21
|
-
* marker is the neutral one so lines align.
|
|
22
|
-
*/
|
|
23
|
-
import { formatValue } from "@sigrun/shared/blocks";
|
|
24
|
-
import { bindRaw, bindText, bindTone } from "../context.js";
|
|
25
|
-
import { iconGlyph } from "../icons.js";
|
|
26
|
-
import { toneFg, toneMarker } from "../tones.js";
|
|
27
|
-
import { truncate, wrapPlain } from "../width.js";
|
|
28
|
-
/** The body clamp (spec §5.5.1: 4 lines in lists and lanes). */
|
|
29
|
-
export const CARD_BODY_LINES = 4;
|
|
30
|
-
/** The braille spinner frames (ASCII: the classic four-bar cycle). */
|
|
31
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
32
|
-
const SPINNER_FRAMES_ASCII = ["*", "+", "x", "+"];
|
|
33
|
-
/** The spinner frame for render `tick` (a render-indexed clock: the TUI
|
|
34
|
-
* has no card-local timers, so a spinning status advances as renders
|
|
35
|
-
* arrive — the same convention ambient rows use). */
|
|
36
|
-
export function spinnerFrame(tick, ascii) {
|
|
37
|
-
const frames = ascii ? SPINNER_FRAMES_ASCII : SPINNER_FRAMES;
|
|
38
|
-
return frames[tick % frames.length] ?? frames[0] ?? "*";
|
|
39
|
-
}
|
|
40
|
-
/** One meta entry resolved to its display text ("" = omit). */
|
|
41
|
-
function metaText(entry, ctx, item) {
|
|
42
|
-
if ("person" in entry) {
|
|
43
|
-
// The extension supplies the display name; `user` is the handle
|
|
44
|
-
// and is not shown (spec §5.5.1).
|
|
45
|
-
return bindText(entry.person.name, ctx);
|
|
46
|
-
}
|
|
47
|
-
if ("time" in entry) {
|
|
48
|
-
const at = bindRaw(entry.time, ctx);
|
|
49
|
-
return at === undefined ? "" : formatValue(at, entry.format, ctx.locale, ctx.timeZone);
|
|
50
|
-
}
|
|
51
|
-
return bindText(entry.text, ctx);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The card's lines for `item` at `width`, indented `indent` cells.
|
|
55
|
-
* `tick` indexes the spinner frame (the owning component's render
|
|
56
|
-
* count). Lines are display-width truncated, never wider than the pane.
|
|
57
|
-
*/
|
|
58
|
-
export function cardLines(card, item, ctx, width, indent, tick) {
|
|
59
|
-
const pad = " ".repeat(indent);
|
|
60
|
-
const inner = Math.max(1, width - indent);
|
|
61
|
-
const itemCtx = { ...ctx, item };
|
|
62
|
-
const lines = [];
|
|
63
|
-
const clamp = (line) => truncate(line, inner);
|
|
64
|
-
// 1. eyebrow
|
|
65
|
-
const eyebrow = bindText(card.eyebrow, itemCtx);
|
|
66
|
-
if (eyebrow !== "")
|
|
67
|
-
lines.push(pad + clamp(ctx.theme.dim(eyebrow)));
|
|
68
|
-
// 2. title: icon, tone marker, toned title text.
|
|
69
|
-
const tone = bindTone(card.tone, itemCtx);
|
|
70
|
-
const title = bindText(card.title, itemCtx);
|
|
71
|
-
const icon = card.icon === undefined ? "" : `${iconGlyph(bindText(card.icon, itemCtx), !ctx.unicode)} `;
|
|
72
|
-
const marker = `${toneMarker(tone, !ctx.unicode)} `;
|
|
73
|
-
lines.push(pad + clamp(`${icon}${marker}${toneFg(ctx.theme, tone)(title)}`));
|
|
74
|
-
// 3. subtitle
|
|
75
|
-
const subtitle = bindText(card.subtitle, itemCtx);
|
|
76
|
-
if (subtitle !== "")
|
|
77
|
-
lines.push(pad + clamp(subtitle));
|
|
78
|
-
// 4. body, clamped to CARD_BODY_LINES.
|
|
79
|
-
const body = bindText(card.body, itemCtx);
|
|
80
|
-
if (body !== "") {
|
|
81
|
-
for (const line of wrapPlain(body, inner).slice(0, CARD_BODY_LINES)) {
|
|
82
|
-
lines.push(pad + line);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// 5. badges + meta, one line.
|
|
86
|
-
const parts = [];
|
|
87
|
-
for (const badge of card.badges ?? []) {
|
|
88
|
-
const text = bindText(badge.text, itemCtx);
|
|
89
|
-
if (text === "")
|
|
90
|
-
continue;
|
|
91
|
-
const badgeTone = bindTone(badge.tone, itemCtx);
|
|
92
|
-
parts.push(`[${toneFg(ctx.theme, badgeTone)(text)}]`);
|
|
93
|
-
}
|
|
94
|
-
const metaParts = (card.meta ?? [])
|
|
95
|
-
.map((entry) => metaText(entry, itemCtx, item))
|
|
96
|
-
.filter((text) => text !== "");
|
|
97
|
-
if (metaParts.length > 0)
|
|
98
|
-
parts.push(metaParts.join(" · "));
|
|
99
|
-
if (parts.length > 0)
|
|
100
|
-
lines.push(pad + clamp(parts.join(" ")));
|
|
101
|
-
// 6. metrics, one per line.
|
|
102
|
-
for (const metric of card.metrics ?? []) {
|
|
103
|
-
const label = bindText(metric.label, itemCtx);
|
|
104
|
-
const value = bindRaw(metric.value, itemCtx);
|
|
105
|
-
if (label === "" || value === undefined)
|
|
106
|
-
continue;
|
|
107
|
-
lines.push(pad + clamp(`${label}: ${formatValue(value, metric.format, ctx.locale, ctx.timeZone)}`));
|
|
108
|
-
}
|
|
109
|
-
// 7. status (spinner frame + tone marker + label).
|
|
110
|
-
if (card.status !== undefined) {
|
|
111
|
-
const label = bindText(card.status.label, itemCtx);
|
|
112
|
-
if (label !== "") {
|
|
113
|
-
const statusTone = bindTone(card.status.tone, itemCtx);
|
|
114
|
-
const spin = bindRaw(card.status.spinner, itemCtx) === true;
|
|
115
|
-
const frame = spin ? `${spinnerFrame(Math.max(0, tick - 1), !ctx.unicode)} ` : "";
|
|
116
|
-
const statusMarker = `${toneMarker(statusTone, !ctx.unicode)} `;
|
|
117
|
-
lines.push(pad + clamp(`${frame}${statusMarker}${toneFg(ctx.theme, statusTone)(label)}`));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
// 8. deps ("after: …").
|
|
121
|
-
const depsRaw = bindRaw(card.deps, itemCtx);
|
|
122
|
-
if (Array.isArray(depsRaw)) {
|
|
123
|
-
const deps = depsRaw.filter((dep) => typeof dep === "string");
|
|
124
|
-
if (deps.length > 0)
|
|
125
|
-
lines.push(pad + clamp(`after: ${deps.join(", ")}`));
|
|
126
|
-
}
|
|
127
|
-
return lines;
|
|
128
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `collection` block (UI-blocks spec §5.5): the display dispatch.
|
|
3
|
-
* Task 3 owns `list`/`table`/`grid`; Task 4 owns `lanes`/`calendar`.
|
|
4
|
-
*/
|
|
5
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
6
|
-
import type { CollectionBlock } from "@sigrun/shared/blocks";
|
|
7
|
-
import type { BlockCtx } from "../context.js";
|
|
8
|
-
/** The display dispatch. */
|
|
9
|
-
export declare function renderCollection(block: CollectionBlock, ctx: BlockCtx): Component | undefined;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `collection` block (UI-blocks spec §5.5): the display dispatch.
|
|
3
|
-
* Task 3 owns `list`/`table`/`grid`; Task 4 owns `lanes`/`calendar`.
|
|
4
|
-
*/
|
|
5
|
-
import { CalendarComponent } from "./calendar.js";
|
|
6
|
-
import { GridComponent } from "./grid.js";
|
|
7
|
-
import { LanesComponent } from "./lanes.js";
|
|
8
|
-
import { ListComponent } from "./list.js";
|
|
9
|
-
import { TableComponent } from "./table.js";
|
|
10
|
-
/** The display dispatch. */
|
|
11
|
-
export function renderCollection(block, ctx) {
|
|
12
|
-
switch (block.display) {
|
|
13
|
-
case "list":
|
|
14
|
-
return new ListComponent(block, ctx);
|
|
15
|
-
case "table":
|
|
16
|
-
return new TableComponent(block, ctx);
|
|
17
|
-
case "grid":
|
|
18
|
-
return new GridComponent(block, ctx);
|
|
19
|
-
case "lanes":
|
|
20
|
-
return new LanesComponent(block, ctx);
|
|
21
|
-
case "calendar":
|
|
22
|
-
return new CalendarComponent(block, ctx);
|
|
23
|
-
default:
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `grid` display (UI-blocks spec §5.5 + §12.3): cards in as many
|
|
3
|
-
* columns as fit — `floor(width / token)` with the token from the item
|
|
4
|
-
* template's `image` presence (`m` = 45 cells with an image, `s` = 30
|
|
5
|
-
* without; §5.5's minItemWidth). `overflow: "scroll"` stays one
|
|
6
|
-
* horizontal row (the row clips at the pane edge; there is no second
|
|
7
|
-
* row). At compact width the fallback is one column (§12.3).
|
|
8
|
-
*/
|
|
9
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
10
|
-
import type { CollectionBlock } from "@sigrun/shared/blocks";
|
|
11
|
-
import { type BlockCtx } from "../context.js";
|
|
12
|
-
/** `grid`: floor(width / token) card columns. */
|
|
13
|
-
export declare class GridComponent implements Component {
|
|
14
|
-
#private;
|
|
15
|
-
constructor(block: CollectionBlock, ctx: BlockCtx);
|
|
16
|
-
render(width: number): string[];
|
|
17
|
-
invalidate(): void;
|
|
18
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `grid` display (UI-blocks spec §5.5 + §12.3): cards in as many
|
|
3
|
-
* columns as fit — `floor(width / token)` with the token from the item
|
|
4
|
-
* template's `image` presence (`m` = 45 cells with an image, `s` = 30
|
|
5
|
-
* without; §5.5's minItemWidth). `overflow: "scroll"` stays one
|
|
6
|
-
* horizontal row (the row clips at the pane edge; there is no second
|
|
7
|
-
* row). At compact width the fallback is one column (§12.3).
|
|
8
|
-
*/
|
|
9
|
-
import { pad, truncate, widthClassOf } from "../width.js";
|
|
10
|
-
import { cardLines } from "./card.js";
|
|
11
|
-
import { collectionItems, sortItems } from "./shared.js";
|
|
12
|
-
/** `grid`: floor(width / token) card columns. */
|
|
13
|
-
export class GridComponent {
|
|
14
|
-
#block;
|
|
15
|
-
#ctx;
|
|
16
|
-
#tick = 0;
|
|
17
|
-
constructor(block, ctx) {
|
|
18
|
-
this.#block = block;
|
|
19
|
-
this.#ctx = ctx;
|
|
20
|
-
}
|
|
21
|
-
#rows() {
|
|
22
|
-
return sortItems(collectionItems(this.#block, this.#ctx), this.#block.sort);
|
|
23
|
-
}
|
|
24
|
-
render(width) {
|
|
25
|
-
this.#tick += 1;
|
|
26
|
-
const rows = this.#rows();
|
|
27
|
-
const itemWidth = this.#block.item.image === undefined ? 30 : 45; // s / m token (§5.2)
|
|
28
|
-
let columns = Math.max(1, Math.floor(width / itemWidth));
|
|
29
|
-
if (this.#block.overflow === "scroll") {
|
|
30
|
-
// One row, clipped at the pane edge.
|
|
31
|
-
columns = Math.max(1, columns);
|
|
32
|
-
return this.#renderRow(rows, width, itemWidth).slice(0, this.#rowHeight(rows, itemWidth, width));
|
|
33
|
-
}
|
|
34
|
-
if (widthClassOf(width) === "compact")
|
|
35
|
-
columns = 1;
|
|
36
|
-
const lines = [];
|
|
37
|
-
for (let start = 0; start < rows.length; start += columns) {
|
|
38
|
-
lines.push(...this.#renderRow(rows.slice(start, start + columns), width, itemWidth));
|
|
39
|
-
}
|
|
40
|
-
return lines;
|
|
41
|
-
}
|
|
42
|
-
/** The tallest card of a row, in lines. */
|
|
43
|
-
#rowHeight(rows, itemWidth, width) {
|
|
44
|
-
let height = 0;
|
|
45
|
-
for (const row of rows) {
|
|
46
|
-
height = Math.max(height, cardLines(this.#block.item, row.item, this.#ctx, itemWidth - 2, 0, this.#tick).length);
|
|
47
|
-
}
|
|
48
|
-
void width;
|
|
49
|
-
return height;
|
|
50
|
-
}
|
|
51
|
-
/** One grid row: the cards side by side, each padded to `itemWidth`. */
|
|
52
|
-
#renderRow(rows, width, itemWidth) {
|
|
53
|
-
const perCard = rows.map((row) => cardLines(this.#block.item, row.item, this.#ctx, itemWidth - 2, 0, this.#tick));
|
|
54
|
-
const height = perCard.reduce((max, card) => Math.max(max, card.length), 0);
|
|
55
|
-
const lines = [];
|
|
56
|
-
for (let line = 0; line < height; line++) {
|
|
57
|
-
const cells = perCard.map((card) => pad(card[line] ?? "", itemWidth - 2));
|
|
58
|
-
const joined = cells.join(" ");
|
|
59
|
-
const text = joined.trimEnd() === "" ? "" : joined;
|
|
60
|
-
if (text === "") {
|
|
61
|
-
lines.push("");
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
lines.push(truncate(text.replace(/\s+$/, ""), width));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return lines;
|
|
68
|
-
}
|
|
69
|
-
invalidate() {
|
|
70
|
-
// The grid holds no view state beyond the spinner tick.
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `lanes` display (UI-blocks U2 Task 4, spec §5.5 + §12.3 + §12.4):
|
|
3
|
-
* one lane per group, side by side in regular/wide; compact shows ONE
|
|
4
|
-
* lane with a Tab/Shift+Tab switcher and a per-item "Move to…" menu
|
|
5
|
-
* (the `.` action-menu) for cross-lane moves.
|
|
6
|
-
*
|
|
7
|
-
* Keyboard moves (§12.4): `m` picks the item under the cursor up,
|
|
8
|
-
* arrows place it (left/right across lanes, up/down within), Enter
|
|
9
|
-
* drops with the U1 pointer-drop payload `{itemId, from: {group,
|
|
10
|
-
* index}, to: {group, index}}`, Esc cancels. Review Focus 1: the
|
|
11
|
-
* pick-up survives a resize — the placement is recomputed from the
|
|
12
|
-
* live block data on every key, never cached from the rendered lines.
|
|
13
|
-
*
|
|
14
|
-
* `x` checks, `Space` selects, Enter on an item with no pick-up in
|
|
15
|
-
* flight activates it. Quiet items fold behind a per-lane "N more"
|
|
16
|
-
* row. The cursor is (lane, row); at regular/wide lanes render side by
|
|
17
|
-
* side and the cursor lane is the compact-selected one.
|
|
18
|
-
*/
|
|
19
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
20
|
-
import type { CollectionBlock } from "@sigrun/shared/blocks";
|
|
21
|
-
import type { BlockCtx } from "../context.js";
|
|
22
|
-
import { isQuiet } from "./shared.js";
|
|
23
|
-
/** The lanes display. */
|
|
24
|
-
export declare class LanesComponent implements Component {
|
|
25
|
-
#private;
|
|
26
|
-
constructor(block: CollectionBlock, ctx: BlockCtx);
|
|
27
|
-
render(width: number): string[];
|
|
28
|
-
handleInput(data: string): void;
|
|
29
|
-
invalidate(): void;
|
|
30
|
-
}
|
|
31
|
-
/** Re-exported so tests can assert the compact switcher boundary. */
|
|
32
|
-
export { isQuiet };
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `lanes` display (UI-blocks U2 Task 4, spec §5.5 + §12.3 + §12.4):
|
|
3
|
-
* one lane per group, side by side in regular/wide; compact shows ONE
|
|
4
|
-
* lane with a Tab/Shift+Tab switcher and a per-item "Move to…" menu
|
|
5
|
-
* (the `.` action-menu) for cross-lane moves.
|
|
6
|
-
*
|
|
7
|
-
* Keyboard moves (§12.4): `m` picks the item under the cursor up,
|
|
8
|
-
* arrows place it (left/right across lanes, up/down within), Enter
|
|
9
|
-
* drops with the U1 pointer-drop payload `{itemId, from: {group,
|
|
10
|
-
* index}, to: {group, index}}`, Esc cancels. Review Focus 1: the
|
|
11
|
-
* pick-up survives a resize — the placement is recomputed from the
|
|
12
|
-
* live block data on every key, never cached from the rendered lines.
|
|
13
|
-
*
|
|
14
|
-
* `x` checks, `Space` selects, Enter on an item with no pick-up in
|
|
15
|
-
* flight activates it. Quiet items fold behind a per-lane "N more"
|
|
16
|
-
* row. The cursor is (lane, row); at regular/wide lanes render side by
|
|
17
|
-
* side and the cursor lane is the compact-selected one.
|
|
18
|
-
*/
|
|
19
|
-
import { truncate, widthClassOf } from "../width.js";
|
|
20
|
-
import { cardLines } from "./card.js";
|
|
21
|
-
import { MoveSession, dispatchMove, lanes, movePayload, placementOf, shiftAcross, shiftWithin, } from "./move.js";
|
|
22
|
-
import { CollectionState, dispatchActivate, dispatchCheck, isQuiet, } from "./shared.js";
|
|
23
|
-
/** The lanes display. */
|
|
24
|
-
export class LanesComponent {
|
|
25
|
-
#block;
|
|
26
|
-
#ctx;
|
|
27
|
-
#state = new CollectionState();
|
|
28
|
-
#moves = new MoveSession();
|
|
29
|
-
#tick = 0;
|
|
30
|
-
/** The compact lane selection (survives width-class changes — the
|
|
31
|
-
* same rule as U1's Review Focus 3). */
|
|
32
|
-
#lane = 0;
|
|
33
|
-
/** The cursor: a lane id plus a row within it. */
|
|
34
|
-
#cursor = { group: "", row: 0 };
|
|
35
|
-
/** The item the "Move to…" menu was opened for. */
|
|
36
|
-
#menuItem;
|
|
37
|
-
constructor(block, ctx) {
|
|
38
|
-
this.#block = block;
|
|
39
|
-
this.#ctx = ctx;
|
|
40
|
-
}
|
|
41
|
-
#lanes() {
|
|
42
|
-
return lanes(this.#block, this.#ctx);
|
|
43
|
-
}
|
|
44
|
-
/** The cursor lane (defaulting to the first group / compact lane). */
|
|
45
|
-
#cursorLane(groups) {
|
|
46
|
-
if (this.#cursor.group === "") {
|
|
47
|
-
const first = groups[Math.min(this.#lane, groups.length - 1)];
|
|
48
|
-
if (first !== undefined)
|
|
49
|
-
this.#cursor = { group: first.id, row: 0 };
|
|
50
|
-
}
|
|
51
|
-
return (groups.find((group) => group.id === this.#cursor.group) ?? groups[0]);
|
|
52
|
-
}
|
|
53
|
-
/** The addressable rows of a lane: visible items, then quiet items
|
|
54
|
-
* (folded behind "N more" while the lane's fold is closed). */
|
|
55
|
-
#laneRows(group, items) {
|
|
56
|
-
const rows = [];
|
|
57
|
-
const visibleIds = new Set(items.map((row) => row.id));
|
|
58
|
-
const quiet = group.items.filter((row) => !visibleIds.has(row.id));
|
|
59
|
-
if (this.#state.quietShownFor(group.id)) {
|
|
60
|
-
for (const item of [...items, ...quiet]) {
|
|
61
|
-
rows.push({ kind: "item", group: group.id, itemId: item.id });
|
|
62
|
-
}
|
|
63
|
-
if (quiet.length > 0) {
|
|
64
|
-
rows.push({ kind: "quiet", group: group.id, quietCount: 0 });
|
|
65
|
-
}
|
|
66
|
-
return rows;
|
|
67
|
-
}
|
|
68
|
-
for (const item of items)
|
|
69
|
-
rows.push({ kind: "item", group: group.id, itemId: item.id });
|
|
70
|
-
if (quiet.length > 0) {
|
|
71
|
-
rows.push({ kind: "quiet", group: group.id, quietCount: quiet.length });
|
|
72
|
-
}
|
|
73
|
-
return rows;
|
|
74
|
-
}
|
|
75
|
-
render(width) {
|
|
76
|
-
this.#tick += 1;
|
|
77
|
-
const { groups, visible } = this.#lanes();
|
|
78
|
-
const compact = widthClassOf(width) === "compact";
|
|
79
|
-
if (this.#lane >= groups.length)
|
|
80
|
-
this.#lane = 0;
|
|
81
|
-
const shownGroups = compact
|
|
82
|
-
? groups.filter((group) => group.id === (groups[this.#lane]?.id ?? ""))
|
|
83
|
-
: groups;
|
|
84
|
-
if (this.#state.menu?.kind === "moveTo") {
|
|
85
|
-
const lines = [truncate("Move to…", width)];
|
|
86
|
-
this.#moveTargets().forEach((group, index) => {
|
|
87
|
-
const cursor = index === this.#state.menuCursor ? "> " : " ";
|
|
88
|
-
lines.push(truncate(cursor + (group.title || group.id), width));
|
|
89
|
-
});
|
|
90
|
-
return lines;
|
|
91
|
-
}
|
|
92
|
-
const lines = [];
|
|
93
|
-
if (compact && groups.length > 1) {
|
|
94
|
-
const names = groups
|
|
95
|
-
.map((group, index) => {
|
|
96
|
-
const label = group.title || group.id;
|
|
97
|
-
return index === this.#lane ? this.#ctx.theme.bold(label) : label;
|
|
98
|
-
})
|
|
99
|
-
.join(" ");
|
|
100
|
-
lines.push(truncate(names, width));
|
|
101
|
-
}
|
|
102
|
-
const gutter = 2;
|
|
103
|
-
const laneWidth = shownGroups.length === 0
|
|
104
|
-
? width
|
|
105
|
-
: Math.max(8, Math.floor((width - gutter * (shownGroups.length - 1)) / shownGroups.length));
|
|
106
|
-
const perLane = shownGroups.map((group) => this.#laneLines(group, visible.get(group.id) ?? [], laneWidth));
|
|
107
|
-
const height = Math.max(0, ...perLane.map((column) => column.length));
|
|
108
|
-
for (let row = 0; row < height; row++) {
|
|
109
|
-
const parts = perLane.map((column) => (column[row] ?? "").padEnd(laneWidth));
|
|
110
|
-
lines.push(truncate(parts.join(" ".repeat(gutter)), width));
|
|
111
|
-
}
|
|
112
|
-
return lines;
|
|
113
|
-
}
|
|
114
|
-
/** One lane's lines: heading, item cards (with a cursor column and
|
|
115
|
-
* the pick-up marker), the quiet fold, and the drop marker. */
|
|
116
|
-
#laneLines(group, items, width) {
|
|
117
|
-
const lines = [];
|
|
118
|
-
lines.push(this.#ctx.theme.bold(truncate(`## ${group.title || group.id}`, width)));
|
|
119
|
-
const rows = this.#laneRows(group, items);
|
|
120
|
-
rows.forEach((row, rowIndex) => {
|
|
121
|
-
const atCursor = this.#cursor.group === group.id && this.#cursor.row === rowIndex;
|
|
122
|
-
const cursor = atCursor ? "> " : " ";
|
|
123
|
-
if (row.kind === "quiet") {
|
|
124
|
-
const label = row.quietCount === 0 ? "Show less" : `${row.quietCount ?? 0} more`;
|
|
125
|
-
lines.push(truncate(cursor + this.#ctx.theme.dim(label), width));
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const item = items.find((candidate) => candidate.id === row.itemId) ??
|
|
129
|
-
group.items.find((candidate) => candidate.id === row.itemId);
|
|
130
|
-
if (item === undefined)
|
|
131
|
-
return;
|
|
132
|
-
const held = this.#moves.held?.itemId === item.id;
|
|
133
|
-
let prefix = "";
|
|
134
|
-
if (this.#block.checkable !== undefined) {
|
|
135
|
-
prefix += this.#state.checkedValue(this.#block, item) ? "[x] " : "[ ] ";
|
|
136
|
-
}
|
|
137
|
-
if (this.#block.select !== undefined) {
|
|
138
|
-
prefix += this.#state.selected.has(item.id)
|
|
139
|
-
? this.#ctx.unicode
|
|
140
|
-
? "(•) "
|
|
141
|
-
: "(*) "
|
|
142
|
-
: "( ) ";
|
|
143
|
-
}
|
|
144
|
-
if (held)
|
|
145
|
-
prefix += this.#ctx.unicode ? "✋ " : "* ";
|
|
146
|
-
const card = cardLines(this.#block.item, item.item, this.#ctx, width - cursor.length - prefix.length, 0, this.#tick);
|
|
147
|
-
const [first, ...rest] = card;
|
|
148
|
-
lines.push(truncate(`${cursor}${prefix}${first ?? ""}`, width));
|
|
149
|
-
for (const line of rest) {
|
|
150
|
-
lines.push(truncate(`${cursor}${" ".repeat(prefix.length)}${line}`, width));
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
const held = this.#moves.held;
|
|
154
|
-
if (held !== undefined && held.at.group === group.id) {
|
|
155
|
-
const marker = this.#ctx.unicode ? "⌄ drop here" : "v drop here";
|
|
156
|
-
lines.push(this.#ctx.theme.dim(truncate(` ${marker} (${held.at.index})`, width)));
|
|
157
|
-
}
|
|
158
|
-
return lines;
|
|
159
|
-
}
|
|
160
|
-
/** The other groups (the compact "Move to…" menu entries). */
|
|
161
|
-
#moveTargets() {
|
|
162
|
-
const { groups } = this.#lanes();
|
|
163
|
-
return groups.filter((group) => group.id !== this.#cursor.group);
|
|
164
|
-
}
|
|
165
|
-
#item(id) {
|
|
166
|
-
if (id === undefined)
|
|
167
|
-
return undefined;
|
|
168
|
-
const { groups } = this.#lanes();
|
|
169
|
-
for (const group of groups) {
|
|
170
|
-
const found = group.items.find((row) => row.id === id);
|
|
171
|
-
if (found !== undefined)
|
|
172
|
-
return found;
|
|
173
|
-
}
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
handleInput(data) {
|
|
177
|
-
const { groups, visible } = this.#lanes();
|
|
178
|
-
const lane = this.#cursorLane(groups);
|
|
179
|
-
// The "Move to…" menu owns the keys while open.
|
|
180
|
-
if (this.#state.menu?.kind === "moveTo") {
|
|
181
|
-
const targets = this.#moveTargets();
|
|
182
|
-
if (data === "\x1b") {
|
|
183
|
-
this.#state.menu = null;
|
|
184
|
-
this.#menuItem = undefined;
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (data === "\x1b[B") {
|
|
188
|
-
this.#state.menuCursor = Math.min(targets.length - 1, this.#state.menuCursor + 1);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (data === "\x1b[A") {
|
|
192
|
-
this.#state.menuCursor = Math.max(0, this.#state.menuCursor - 1);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (data !== "\r")
|
|
196
|
-
return;
|
|
197
|
-
const target = targets[this.#state.menuCursor];
|
|
198
|
-
const itemId = this.#menuItem;
|
|
199
|
-
this.#state.menu = null;
|
|
200
|
-
this.#menuItem = undefined;
|
|
201
|
-
if (target === undefined || itemId === undefined)
|
|
202
|
-
return;
|
|
203
|
-
const from = placementOf(groups, visible, itemId);
|
|
204
|
-
if (from === undefined || this.#block.onMove === undefined)
|
|
205
|
-
return;
|
|
206
|
-
// The compact move target is the END of the chosen lane (U1's
|
|
207
|
-
// MoveToMenu rule, spec §12.3).
|
|
208
|
-
const to = { group: target.id, index: (visible.get(target.id) ?? []).length };
|
|
209
|
-
this.#ctx.dispatch({ type: "move", name: this.#block.onMove }, movePayload({ itemId, from, to }));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
// A pick-up in flight owns the arrows (spec §12.4).
|
|
213
|
-
const held = this.#moves.held;
|
|
214
|
-
if (held !== undefined) {
|
|
215
|
-
if (data === "\x1b") {
|
|
216
|
-
this.#moves.cancel();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (data === "\x1b[D") {
|
|
220
|
-
this.#moves.held = shiftAcross(held, groups, visible, -1);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (data === "\x1b[C") {
|
|
224
|
-
this.#moves.held = shiftAcross(held, groups, visible, 1);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
if (data === "\x1b[A") {
|
|
228
|
-
this.#moves.held = shiftWithin(held, visible, -1);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
if (data === "\x1b[B") {
|
|
232
|
-
this.#moves.held = shiftWithin(held, visible, 1);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (data === "\r") {
|
|
236
|
-
this.#moves.held = undefined;
|
|
237
|
-
dispatchMove(this.#block, this.#ctx, held);
|
|
238
|
-
}
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
// Compact lane switcher: Tab / Shift+Tab cycle lanes.
|
|
242
|
-
if (data === "\t") {
|
|
243
|
-
if (groups.length > 1) {
|
|
244
|
-
this.#lane = (this.#lane + 1) % groups.length;
|
|
245
|
-
this.#cursor = { group: groups[this.#lane]?.id ?? "", row: 0 };
|
|
246
|
-
}
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (data === "\x1b[Z") {
|
|
250
|
-
if (groups.length > 1) {
|
|
251
|
-
this.#lane = this.#lane - 1 < 0 ? groups.length - 1 : this.#lane - 1;
|
|
252
|
-
this.#cursor = { group: groups[this.#lane]?.id ?? "", row: 0 };
|
|
253
|
-
}
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (lane === undefined)
|
|
257
|
-
return;
|
|
258
|
-
const rows = this.#laneRows(lane, visible.get(lane.id) ?? []);
|
|
259
|
-
if (this.#cursor.row >= rows.length)
|
|
260
|
-
this.#cursor.row = Math.max(0, rows.length - 1);
|
|
261
|
-
const current = rows[this.#cursor.row];
|
|
262
|
-
if (data === "m" && current?.kind === "item" && current.itemId !== undefined) {
|
|
263
|
-
if (this.#block.onMove === undefined)
|
|
264
|
-
return;
|
|
265
|
-
const from = placementOf(groups, visible, current.itemId);
|
|
266
|
-
if (from === undefined)
|
|
267
|
-
return;
|
|
268
|
-
this.#moves.pickUp(current.itemId, from);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (data === ".") {
|
|
272
|
-
if (current?.kind === "item" &&
|
|
273
|
-
this.#block.onMove !== undefined &&
|
|
274
|
-
this.#moveTargets().length > 0) {
|
|
275
|
-
this.#state.menu = { kind: "moveTo" };
|
|
276
|
-
this.#state.menuCursor = 0;
|
|
277
|
-
this.#menuItem = current.itemId;
|
|
278
|
-
}
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (data === "\x1b[B") {
|
|
282
|
-
this.#cursor = { group: lane.id, row: Math.min(rows.length - 1, this.#cursor.row + 1) };
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (data === "\x1b[A") {
|
|
286
|
-
this.#cursor = { group: lane.id, row: Math.max(0, this.#cursor.row - 1) };
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (data === "x" && current?.kind === "item") {
|
|
290
|
-
const item = this.#item(current.itemId);
|
|
291
|
-
if (item !== undefined)
|
|
292
|
-
dispatchCheck(this.#block, this.#ctx, this.#state, item);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (data === " " && this.#block.select !== undefined && current?.kind === "item") {
|
|
296
|
-
const id = current.itemId ?? "";
|
|
297
|
-
if (this.#state.selected.has(id))
|
|
298
|
-
this.#state.selected.delete(id);
|
|
299
|
-
else
|
|
300
|
-
this.#state.selected.add(id);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (data !== "\r")
|
|
304
|
-
return;
|
|
305
|
-
if (current?.kind === "quiet") {
|
|
306
|
-
this.#state.toggleQuiet(current.group);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (current?.kind === "item") {
|
|
310
|
-
const item = this.#item(current.itemId);
|
|
311
|
-
if (item !== undefined)
|
|
312
|
-
dispatchActivate(this.#block, this.#ctx, item);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
invalidate() {
|
|
316
|
-
// State (cursor, selection, folds, the pick-up) survives re-renders.
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/** Re-exported so tests can assert the compact switcher boundary. */
|
|
320
|
-
export { isQuiet };
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `list` display (UI-blocks spec §5.5): rows with group headings in
|
|
3
|
-
* `groups` order, 2-cell-per-level nesting on `children`, `numbered`,
|
|
4
|
-
* checkboxes, quiet items folded behind "N more", and the multi-select
|
|
5
|
-
* bulk action menu (`.`).
|
|
6
|
-
*
|
|
7
|
-
* Keys (spec §12.4): Up/Down move the cursor; `x` toggles the checkable
|
|
8
|
-
* field and dispatches `onCheck` (`edit`, `{itemId, value}`); `Space`
|
|
9
|
-
* selects; `.` opens the bulk menu; Enter activates a row (`onActivate`
|
|
10
|
-
* with `{itemId}`), unfolds the "N more" fold, or picks a menu entry.
|
|
11
|
-
* Selection, folds and menus are view-only — they never dispatch.
|
|
12
|
-
*
|
|
13
|
-
* The bulk menu is the seam Task 5's form renderer plugs into: an
|
|
14
|
-
* action with a `form` opens a minimal seam form here (labeled line
|
|
15
|
-
* per top-level field name, values keyed by name) and its submit rides
|
|
16
|
-
* the bulk payload as `{ids, values}` — mirroring U1 Task 3's note.
|
|
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
|
-
import { widthClassOf } from "../width.js";
|
|
22
|
-
/** `list`: groups, nesting, numbering, checkboxes, quiet fold, bulk menu. */
|
|
23
|
-
export declare class ListComponent implements Component {
|
|
24
|
-
#private;
|
|
25
|
-
constructor(block: CollectionBlock, ctx: BlockCtx);
|
|
26
|
-
render(width: number): string[];
|
|
27
|
-
handleInput(data: string): void;
|
|
28
|
-
invalidate(): void;
|
|
29
|
-
}
|
|
30
|
-
export { widthClassOf };
|