@sigrun/tui 0.1.1 → 0.2.1
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 +11 -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
package/dist/blocks/log.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `log` block (UI-blocks spec §5.3): bound `{at?, level?, text}[]`
|
|
3
|
-
* lines in a block-local viewport of {@link LOG_VIEWPORT_ROWS} rows.
|
|
4
|
-
*
|
|
5
|
-
* With `follow` the viewport pins to the tail until the user scrolls
|
|
6
|
-
* up; scrolling back to the bottom re-arms the following (the
|
|
7
|
-
* transcript convention). Per-level tones default `error`→bad,
|
|
8
|
-
* `warn`→warn, `info`→neutral, `debug`→info (the muted token);
|
|
9
|
-
* `tones` overrides per level. `wrap` folds long lines instead of
|
|
10
|
-
* truncating them.
|
|
11
|
-
*/
|
|
12
|
-
import { formatValue } from "@sigrun/shared/blocks";
|
|
13
|
-
import { bindRaw } from "./context.js";
|
|
14
|
-
import { truncate } from "./width.js";
|
|
15
|
-
import { toneFg } from "./tones.js";
|
|
16
|
-
/** The viewport height in rows. */
|
|
17
|
-
export const LOG_VIEWPORT_ROWS = 10;
|
|
18
|
-
/** The default per-level tones (spec §5.3's table; `debug` mutes). */
|
|
19
|
-
const LEVEL_TONES = {
|
|
20
|
-
error: "bad",
|
|
21
|
-
warn: "warn",
|
|
22
|
-
info: "neutral",
|
|
23
|
-
debug: "info",
|
|
24
|
-
};
|
|
25
|
-
/** Read the bound lines, dropping malformed entries. */
|
|
26
|
-
function logLines(block, ctx) {
|
|
27
|
-
const raw = bindRaw(block.lines, ctx);
|
|
28
|
-
if (!Array.isArray(raw))
|
|
29
|
-
return [];
|
|
30
|
-
const out = [];
|
|
31
|
-
for (const entry of raw) {
|
|
32
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry))
|
|
33
|
-
continue;
|
|
34
|
-
const record = entry;
|
|
35
|
-
if (typeof record.text !== "string")
|
|
36
|
-
continue;
|
|
37
|
-
out.push({
|
|
38
|
-
at: typeof record.at === "number" || typeof record.at === "string" ? record.at : undefined,
|
|
39
|
-
level: typeof record.level === "string" ? record.level : undefined,
|
|
40
|
-
text: record.text,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
/** The head of one line: `time [level] ` (each part optional). */
|
|
46
|
-
function lineHead(line, ctx) {
|
|
47
|
-
const at = line.at === undefined
|
|
48
|
-
? ""
|
|
49
|
-
: formatValue(line.at, "time", ctx.locale, ctx.timeZone);
|
|
50
|
-
const level = line.level === undefined ? "" : `[${line.level}]`;
|
|
51
|
-
return [at, level].filter((part) => part !== "").join(" ");
|
|
52
|
-
}
|
|
53
|
-
/** `log`: the follow-tail viewport. */
|
|
54
|
-
export class LogComponent {
|
|
55
|
-
#block;
|
|
56
|
-
#ctx;
|
|
57
|
-
/** The first visible row in the stream; `undefined` = the tail. */
|
|
58
|
-
#offset;
|
|
59
|
-
constructor(block, ctx) {
|
|
60
|
-
this.#block = block;
|
|
61
|
-
this.#ctx = ctx;
|
|
62
|
-
}
|
|
63
|
-
/** Every display row: one source line plus its wrapped folds. */
|
|
64
|
-
#rows(width) {
|
|
65
|
-
const rows = [];
|
|
66
|
-
for (const line of logLines(this.#block, this.#ctx)) {
|
|
67
|
-
const head = lineHead(line, this.#ctx);
|
|
68
|
-
const text = head === "" ? line.text : `${head} ${line.text}`;
|
|
69
|
-
const tone = this.#block.tones?.[line.level ?? ""] ??
|
|
70
|
-
(line.level === undefined ? undefined : LEVEL_TONES[line.level]);
|
|
71
|
-
const paint = tone === undefined ? (s) => s : toneFg(this.#ctx.theme, tone);
|
|
72
|
-
if (this.#block.wrap === true) {
|
|
73
|
-
// Hard-fold at the cell budget (logs are not prose).
|
|
74
|
-
for (let at = 0; at < text.length; at += Math.max(1, width)) {
|
|
75
|
-
rows.push(paint(text.slice(at, at + Math.max(1, width))));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
rows.push(truncate(paint(text), width));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return rows;
|
|
83
|
-
}
|
|
84
|
-
render(width) {
|
|
85
|
-
const rows = this.#rows(width);
|
|
86
|
-
if (rows.length <= LOG_VIEWPORT_ROWS)
|
|
87
|
-
return rows;
|
|
88
|
-
const tail = this.#block.follow === true ? rows.length - LOG_VIEWPORT_ROWS : 0;
|
|
89
|
-
const start = Math.max(0, Math.min(this.#offset ?? tail, rows.length - LOG_VIEWPORT_ROWS));
|
|
90
|
-
return rows.slice(start, start + LOG_VIEWPORT_ROWS);
|
|
91
|
-
}
|
|
92
|
-
handleInput(data) {
|
|
93
|
-
// The viewport moves by display rows within the last render's
|
|
94
|
-
// stream; `render` clamps the offset back into range.
|
|
95
|
-
const tail = Math.max(0, logLines(this.#block, this.#ctx).length - LOG_VIEWPORT_ROWS);
|
|
96
|
-
if (data === "\x1b[A") {
|
|
97
|
-
const current = this.#offset ?? tail;
|
|
98
|
-
this.#offset = Math.max(0, current - 1);
|
|
99
|
-
}
|
|
100
|
-
else if (data === "\x1b[B") {
|
|
101
|
-
if (this.#offset !== undefined) {
|
|
102
|
-
this.#offset += 1;
|
|
103
|
-
if (this.#offset >= tail)
|
|
104
|
-
this.#offset = undefined; // re-arm follow
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
invalidate() {
|
|
109
|
-
// The viewport offset is the only mutable state.
|
|
110
|
-
}
|
|
111
|
-
}
|
package/dist/blocks/meter.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `meter` block (UI-blocks spec §5.3): `label value` plus a bar
|
|
3
|
-
* filling the rest of the line — `█`/`░` on a Unicode terminal,
|
|
4
|
-
* `#`/`-` without one. A crossed threshold's tone paints the bar and
|
|
5
|
-
* the value (the LAST crossed threshold wins, matching the webui).
|
|
6
|
-
*/
|
|
7
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
8
|
-
import type { MeterBlock } from "@sigrun/shared/blocks";
|
|
9
|
-
import { type BlockCtx } from "./context.js";
|
|
10
|
-
/** `meter`: one line, `label value bar` exactly `width` cells. */
|
|
11
|
-
export declare function renderMeter(block: MeterBlock, ctx: BlockCtx): Component;
|
|
12
|
-
/**
|
|
13
|
-
* The bare meter bar (no label): `filled`/`empty` cells of the 0–1
|
|
14
|
-
* `level`, for inline cells (the collection table's `meter` column).
|
|
15
|
-
*/
|
|
16
|
-
export declare function meterBar(level: number, cells: number, ascii: boolean): string;
|
package/dist/blocks/meter.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `meter` block (UI-blocks spec §5.3): `label value` plus a bar
|
|
3
|
-
* filling the rest of the line — `█`/`░` on a Unicode terminal,
|
|
4
|
-
* `#`/`-` without one. A crossed threshold's tone paints the bar and
|
|
5
|
-
* the value (the LAST crossed threshold wins, matching the webui).
|
|
6
|
-
*/
|
|
7
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
-
import { formatValue } from "@sigrun/shared/blocks";
|
|
9
|
-
import { bindRaw, bindText } from "./context.js";
|
|
10
|
-
import { truncate } from "./width.js";
|
|
11
|
-
import { toneFg } from "./tones.js";
|
|
12
|
-
import { Lines } from "./layout.js";
|
|
13
|
-
/** The highest threshold the value crosses, if any. */
|
|
14
|
-
function crossedTone(block, value) {
|
|
15
|
-
let tone;
|
|
16
|
-
for (const threshold of block.thresholds ?? []) {
|
|
17
|
-
if (value >= threshold.at)
|
|
18
|
-
tone = threshold.tone;
|
|
19
|
-
}
|
|
20
|
-
return tone;
|
|
21
|
-
}
|
|
22
|
-
/** `meter`: one line, `label value bar` exactly `width` cells. */
|
|
23
|
-
export function renderMeter(block, ctx) {
|
|
24
|
-
return new Lines((width) => {
|
|
25
|
-
const raw = bindRaw(block.value, ctx);
|
|
26
|
-
const value = typeof raw === "number" ? raw : 0;
|
|
27
|
-
const min = block.min ?? 0;
|
|
28
|
-
const max = block.max ?? 1;
|
|
29
|
-
const label = bindText(block.label, ctx);
|
|
30
|
-
const text = formatValue(value, block.format, ctx.locale);
|
|
31
|
-
const head = label === "" ? text : `${label} ${text}`;
|
|
32
|
-
const tone = crossedTone(block, value);
|
|
33
|
-
const paint = tone === undefined ? (s) => s : toneFg(ctx.theme, tone);
|
|
34
|
-
const barCells = Math.max(1, width - visibleWidth(head) - 1);
|
|
35
|
-
const t = max === min ? 0 : Math.max(0, Math.min(1, (value - min) / (max - min)));
|
|
36
|
-
const bar = meterBar(t, barCells, !ctx.unicode);
|
|
37
|
-
return [truncate(`${paint(head)} ${paint(bar)}`, width)];
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* The bare meter bar (no label): `filled`/`empty` cells of the 0–1
|
|
42
|
-
* `level`, for inline cells (the collection table's `meter` column).
|
|
43
|
-
*/
|
|
44
|
-
export function meterBar(level, cells, ascii) {
|
|
45
|
-
const fill = ascii ? "#" : "█";
|
|
46
|
-
const empty = ascii ? "-" : "░";
|
|
47
|
-
const t = Math.max(0, Math.min(1, level));
|
|
48
|
-
const filled = Math.floor(cells * t);
|
|
49
|
-
return fill.repeat(filled) + empty.repeat(cells - filled);
|
|
50
|
-
}
|
package/dist/blocks/page.d.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `page` shell (UI-blocks U2 Task 5, spec §5.1/§12.3 — the TUI port
|
|
3
|
-
* of U1's `Page.tsx`): a header line (title, subtitle), sidebar, main
|
|
4
|
-
* and detail regions — the channel draws them, the extension never
|
|
5
|
-
* positions them.
|
|
6
|
-
*
|
|
7
|
-
* - `empty` replaces MAIN ONLY when main's first collection binds to a
|
|
8
|
-
* missing path, `null` or `[]`; the sidebar and detail keep rendering.
|
|
9
|
-
* - At ≥ 100 columns the sidebar is a focusable pane beside main
|
|
10
|
-
* (Tab cycles sidebar → main); below 100 it is a toggled overlay
|
|
11
|
-
* (Tab toggles it; Esc closes it).
|
|
12
|
-
* - `ctrl+s` opens the header switcher (a `choice` field): Up/Down
|
|
13
|
-
* pick, Enter dispatches a `navigate` action with `{<name>: id}`.
|
|
14
|
-
* - `.` opens the actions menu: Up/Down pick, Enter dispatches the
|
|
15
|
-
* action, Esc closes. (Access `read` hides edit/answer actions.)
|
|
16
|
-
* - The detail region is a pushed view: Enter on main pushes it, Esc
|
|
17
|
-
* pops back. At ≥ 100 columns it renders beside main once pushed.
|
|
18
|
-
* - Esc with nothing open resolves at THIS layer (Task 4's host note:
|
|
19
|
-
* the app-level Esc handling for a surface with no pick-up and no
|
|
20
|
-
* open menu is the page's "unfocus" — the host routes it; the page
|
|
21
|
-
* reports `hasOverlay` so the host knows when Esc is already spoken
|
|
22
|
-
* for).
|
|
23
|
-
*/
|
|
24
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
25
|
-
import type { Block, CollectionBlock, JsonObject, PageBlock } from "@sigrun/shared/blocks";
|
|
26
|
-
import { type BlockCtx } from "./context.js";
|
|
27
|
-
import { type RenderOptions } from "./render.js";
|
|
28
|
-
/** The sidebar-pane minimum (spec §5.1/brief: ≥ 100 columns). */
|
|
29
|
-
export declare const SIDEBAR_PANE_MIN = 100;
|
|
30
|
-
/** The sidebar's pane width at the side-by-side layout. */
|
|
31
|
-
export declare const SIDEBAR_CELLS = 30;
|
|
32
|
-
/** The first `collection` block in a slot list, depth-first (mirrors
|
|
33
|
-
* U1's `firstCollection`). */
|
|
34
|
-
export declare function firstCollection(blocks: readonly Block[]): CollectionBlock | undefined;
|
|
35
|
-
/** Whether `empty` replaces main (spec §5.1): the first collection's
|
|
36
|
-
* `items` binding resolves missing, null or an empty array. */
|
|
37
|
-
export declare function isMainEmpty(block: PageBlock, data: JsonObject): boolean;
|
|
38
|
-
export declare class PageComponent implements Component {
|
|
39
|
-
#private;
|
|
40
|
-
constructor(block: PageBlock, ctx: BlockCtx, options?: RenderOptions);
|
|
41
|
-
/** True while an overlay (menu, sidebar drawer, pushed detail) owns Esc. */
|
|
42
|
-
get hasOverlay(): boolean;
|
|
43
|
-
render(width: number): string[];
|
|
44
|
-
handleInput(data: string): void;
|
|
45
|
-
/** The host tells the page the current pane width's layout class
|
|
46
|
-
* (render computes it too; this setter keeps Tab consistent when
|
|
47
|
-
* input arrives between renders). */
|
|
48
|
-
setSideBySide(sideBySide: boolean): void;
|
|
49
|
-
invalidate(): void;
|
|
50
|
-
/** Test introspection: the pushed detail / overlay state. */
|
|
51
|
-
get detailPushed(): boolean;
|
|
52
|
-
get sidebarOpen(): boolean;
|
|
53
|
-
get menuKind(): "switcher" | "actions" | null;
|
|
54
|
-
}
|
package/dist/blocks/page.js
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `page` shell (UI-blocks U2 Task 5, spec §5.1/§12.3 — the TUI port
|
|
3
|
-
* of U1's `Page.tsx`): a header line (title, subtitle), sidebar, main
|
|
4
|
-
* and detail regions — the channel draws them, the extension never
|
|
5
|
-
* positions them.
|
|
6
|
-
*
|
|
7
|
-
* - `empty` replaces MAIN ONLY when main's first collection binds to a
|
|
8
|
-
* missing path, `null` or `[]`; the sidebar and detail keep rendering.
|
|
9
|
-
* - At ≥ 100 columns the sidebar is a focusable pane beside main
|
|
10
|
-
* (Tab cycles sidebar → main); below 100 it is a toggled overlay
|
|
11
|
-
* (Tab toggles it; Esc closes it).
|
|
12
|
-
* - `ctrl+s` opens the header switcher (a `choice` field): Up/Down
|
|
13
|
-
* pick, Enter dispatches a `navigate` action with `{<name>: id}`.
|
|
14
|
-
* - `.` opens the actions menu: Up/Down pick, Enter dispatches the
|
|
15
|
-
* action, Esc closes. (Access `read` hides edit/answer actions.)
|
|
16
|
-
* - The detail region is a pushed view: Enter on main pushes it, Esc
|
|
17
|
-
* pops back. At ≥ 100 columns it renders beside main once pushed.
|
|
18
|
-
* - Esc with nothing open resolves at THIS layer (Task 4's host note:
|
|
19
|
-
* the app-level Esc handling for a surface with no pick-up and no
|
|
20
|
-
* open menu is the page's "unfocus" — the host routes it; the page
|
|
21
|
-
* reports `hasOverlay` so the host knows when Esc is already spoken
|
|
22
|
-
* for).
|
|
23
|
-
*/
|
|
24
|
-
import { resolvePointer } from "@sigrun/shared/blocks";
|
|
25
|
-
import { bindText } from "./context.js";
|
|
26
|
-
import { iconGlyph } from "./icons.js";
|
|
27
|
-
import { renderBlock } from "./render.js";
|
|
28
|
-
import { pad, truncate, wrapPlain } from "./width.js";
|
|
29
|
-
/** The sidebar-pane minimum (spec §5.1/brief: ≥ 100 columns). */
|
|
30
|
-
export const SIDEBAR_PANE_MIN = 100;
|
|
31
|
-
/** The sidebar's pane width at the side-by-side layout. */
|
|
32
|
-
export const SIDEBAR_CELLS = 30;
|
|
33
|
-
/** The first `collection` block in a slot list, depth-first (mirrors
|
|
34
|
-
* U1's `firstCollection`). */
|
|
35
|
-
export function firstCollection(blocks) {
|
|
36
|
-
for (const block of blocks) {
|
|
37
|
-
if (block.type === "collection")
|
|
38
|
-
return block;
|
|
39
|
-
if (block.type === "stack") {
|
|
40
|
-
const found = firstCollection(block.children);
|
|
41
|
-
if (found !== undefined)
|
|
42
|
-
return found;
|
|
43
|
-
}
|
|
44
|
-
if (block.type === "section") {
|
|
45
|
-
const found = firstCollection(block.children.filter((child) => child.type === "collection" ||
|
|
46
|
-
child.type === "stack" ||
|
|
47
|
-
child.type === "columns" ||
|
|
48
|
-
child.type === "grid" ||
|
|
49
|
-
child.type === "section"));
|
|
50
|
-
if (found !== undefined)
|
|
51
|
-
return found;
|
|
52
|
-
}
|
|
53
|
-
if (block.type === "columns" || block.type === "grid") {
|
|
54
|
-
for (const child of block.children) {
|
|
55
|
-
const found = firstCollection([child.block]);
|
|
56
|
-
if (found !== undefined)
|
|
57
|
-
return found;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
/** Whether `empty` replaces main (spec §5.1): the first collection's
|
|
64
|
-
* `items` binding resolves missing, null or an empty array. */
|
|
65
|
-
export function isMainEmpty(block, data) {
|
|
66
|
-
if (block.empty === undefined)
|
|
67
|
-
return false;
|
|
68
|
-
const collection = firstCollection(block.main);
|
|
69
|
-
if (collection === undefined)
|
|
70
|
-
return false;
|
|
71
|
-
const items = resolvePointer(data, collection.items.$);
|
|
72
|
-
return !Array.isArray(items) || items.length === 0;
|
|
73
|
-
}
|
|
74
|
-
export class PageComponent {
|
|
75
|
-
#block;
|
|
76
|
-
#ctx;
|
|
77
|
-
#options;
|
|
78
|
-
/** The open overlay: the switcher or the actions menu. */
|
|
79
|
-
#menu = null;
|
|
80
|
-
/** The pushed detail view (spec §12.3: detail → pushed page, Esc back). */
|
|
81
|
-
#detailPushed = false;
|
|
82
|
-
/** The sidebar overlay (compact) / focused pane (side-by-side). */
|
|
83
|
-
#sidebarOpen = false;
|
|
84
|
-
#region = "main";
|
|
85
|
-
/** The rendered region components (input forwards to the focused one). */
|
|
86
|
-
#main;
|
|
87
|
-
#sidebar;
|
|
88
|
-
#detail;
|
|
89
|
-
constructor(block, ctx, options = {}) {
|
|
90
|
-
this.#block = block;
|
|
91
|
-
this.#ctx = ctx;
|
|
92
|
-
this.#options = options;
|
|
93
|
-
this.#main = block.main.map((child) => renderBlock(child, ctx, options));
|
|
94
|
-
this.#sidebar = (block.sidebar ?? []).map((child) => renderBlock(child, ctx, options));
|
|
95
|
-
this.#detail = (block.detail ?? []).map((child) => renderBlock(child, ctx, options));
|
|
96
|
-
}
|
|
97
|
-
get #hasSidebar() {
|
|
98
|
-
return this.#sidebar.length > 0;
|
|
99
|
-
}
|
|
100
|
-
get #hasDetail() {
|
|
101
|
-
return this.#detail.length > 0;
|
|
102
|
-
}
|
|
103
|
-
/** True while an overlay (menu, sidebar drawer, pushed detail) owns Esc. */
|
|
104
|
-
get hasOverlay() {
|
|
105
|
-
return this.#menu !== null || this.#detailPushed || this.#sidebarOpen;
|
|
106
|
-
}
|
|
107
|
-
/** The header line: title (+ subtitle), switcher/actions hints. */
|
|
108
|
-
#headerLines(width) {
|
|
109
|
-
const title = bindText(this.#block.title, this.#ctx);
|
|
110
|
-
const subtitle = bindText(this.#block.subtitle, this.#ctx);
|
|
111
|
-
const hints = [];
|
|
112
|
-
if (this.#block.switcher !== undefined)
|
|
113
|
-
hints.push("ctrl+s switch");
|
|
114
|
-
if ((this.#block.actions ?? []).length > 0)
|
|
115
|
-
hints.push(". actions");
|
|
116
|
-
const hint = hints.length > 0 ? this.#ctx.theme.dim(` (${hints.join(" · ")})`) : "";
|
|
117
|
-
const lines = [truncate(`${this.#ctx.theme.bold(title)}${hint}`, width)];
|
|
118
|
-
if (subtitle !== "")
|
|
119
|
-
lines.push(truncate(this.#ctx.theme.dim(subtitle), width));
|
|
120
|
-
return lines;
|
|
121
|
-
}
|
|
122
|
-
#emptyLines(width) {
|
|
123
|
-
const empty = this.#block.empty;
|
|
124
|
-
if (empty === undefined)
|
|
125
|
-
return [];
|
|
126
|
-
const icon = iconGlyph(empty.icon, !this.#ctx.unicode);
|
|
127
|
-
const lines = [`${icon} ${empty.title}`];
|
|
128
|
-
if (empty.hint !== undefined && empty.hint !== "") {
|
|
129
|
-
for (const line of wrapPlain(empty.hint, width))
|
|
130
|
-
lines.push(this.#ctx.theme.dim(line));
|
|
131
|
-
}
|
|
132
|
-
return lines.map((line) => truncate(line, width));
|
|
133
|
-
}
|
|
134
|
-
#menuLines(width) {
|
|
135
|
-
const menu = this.#menu;
|
|
136
|
-
if (menu === null)
|
|
137
|
-
return [];
|
|
138
|
-
if (menu.kind === "switcher") {
|
|
139
|
-
const field = this.#block.switcher;
|
|
140
|
-
if (field === undefined)
|
|
141
|
-
return [];
|
|
142
|
-
const options = this.#switcherOptions(field);
|
|
143
|
-
return [
|
|
144
|
-
truncate(this.#ctx.theme.bold("Switch"), width),
|
|
145
|
-
...options.map((option, index) => {
|
|
146
|
-
const cursor = index === menu.cursor ? "> " : " ";
|
|
147
|
-
return truncate(`${cursor}${bindText(option.label, this.#ctx) || option.id}`, width);
|
|
148
|
-
}),
|
|
149
|
-
truncate(this.#ctx.theme.dim("Enter selects · Esc closes"), width),
|
|
150
|
-
];
|
|
151
|
-
}
|
|
152
|
-
const actions = this.#visibleActions();
|
|
153
|
-
return [
|
|
154
|
-
truncate(this.#ctx.theme.bold("Actions"), width),
|
|
155
|
-
...actions.map((button, index) => {
|
|
156
|
-
const cursor = index === menu.cursor ? "> " : " ";
|
|
157
|
-
const icon = button.icon === undefined ? "" : `${iconGlyph(button.icon, !this.#ctx.unicode)} `;
|
|
158
|
-
return truncate(`${cursor}${icon}${bindText(button.label, this.#ctx) || button.id}`, width);
|
|
159
|
-
}),
|
|
160
|
-
truncate(this.#ctx.theme.dim("Enter runs · Esc closes"), width),
|
|
161
|
-
];
|
|
162
|
-
}
|
|
163
|
-
#switcherOptions(field) {
|
|
164
|
-
const raw = field.options;
|
|
165
|
-
if (Array.isArray(raw))
|
|
166
|
-
return raw;
|
|
167
|
-
if (typeof raw === "object" && raw !== null && typeof raw.$ === "string") {
|
|
168
|
-
const resolved = resolvePointer(this.#ctx.data, raw.$);
|
|
169
|
-
if (Array.isArray(resolved))
|
|
170
|
-
return resolved;
|
|
171
|
-
}
|
|
172
|
-
return [];
|
|
173
|
-
}
|
|
174
|
-
/** The header actions the principal may run (access read hides
|
|
175
|
-
* edit/answer actions; navigate/open/copy stay — §6.5's read rule). */
|
|
176
|
-
#visibleActions() {
|
|
177
|
-
if (this.#ctx.access === "write")
|
|
178
|
-
return this.#block.actions ?? [];
|
|
179
|
-
return (this.#block.actions ?? []).filter((button) => button.action.type === "navigate" ||
|
|
180
|
-
button.action.type === "open" ||
|
|
181
|
-
button.action.type === "copy");
|
|
182
|
-
}
|
|
183
|
-
render(width) {
|
|
184
|
-
const lines = this.#headerLines(width);
|
|
185
|
-
if (this.#menu !== null) {
|
|
186
|
-
return [...lines, ...this.#menuLines(width)];
|
|
187
|
-
}
|
|
188
|
-
if (this.#detailPushed && this.#hasDetail) {
|
|
189
|
-
lines.push(truncate(this.#ctx.theme.dim("Esc back"), width));
|
|
190
|
-
for (const component of this.#detail) {
|
|
191
|
-
lines.push(...component.render(width));
|
|
192
|
-
}
|
|
193
|
-
return lines;
|
|
194
|
-
}
|
|
195
|
-
const empty = isMainEmpty(this.#block, this.#ctx.data);
|
|
196
|
-
const mainLines = empty
|
|
197
|
-
? this.#emptyLines(width)
|
|
198
|
-
: this.#main.flatMap((component) => component.render(this.#mainWidth(width)));
|
|
199
|
-
const sideBySide = width >= SIDEBAR_PANE_MIN && this.#hasSidebar;
|
|
200
|
-
if (!sideBySide) {
|
|
201
|
-
// Compact/regular below the pane minimum: the sidebar is a
|
|
202
|
-
// toggled overlay (rendered above main while open).
|
|
203
|
-
if (this.#sidebarOpen && this.#hasSidebar) {
|
|
204
|
-
lines.push(truncate(this.#ctx.theme.dim("Esc closes the sidebar"), width));
|
|
205
|
-
for (const component of this.#sidebar) {
|
|
206
|
-
lines.push(...component.render(width));
|
|
207
|
-
}
|
|
208
|
-
return lines;
|
|
209
|
-
}
|
|
210
|
-
return [...lines, ...mainLines];
|
|
211
|
-
}
|
|
212
|
-
// The side-by-side layout: sidebar pane, gutter, main.
|
|
213
|
-
const sidebarLines = this.#sidebar.flatMap((component) => component.render(SIDEBAR_CELLS));
|
|
214
|
-
const height = Math.max(sidebarLines.length, mainLines.length);
|
|
215
|
-
for (let row = 0; row < height; row++) {
|
|
216
|
-
const left = pad(sidebarLines[row] ?? "", SIDEBAR_CELLS);
|
|
217
|
-
const right = mainLines[row] ?? "";
|
|
218
|
-
lines.push(truncate(`${left} ${right}`, width));
|
|
219
|
-
}
|
|
220
|
-
return lines;
|
|
221
|
-
}
|
|
222
|
-
#mainWidth(width) {
|
|
223
|
-
return width >= SIDEBAR_PANE_MIN && this.#hasSidebar
|
|
224
|
-
? Math.max(1, width - SIDEBAR_CELLS - 1)
|
|
225
|
-
: width;
|
|
226
|
-
}
|
|
227
|
-
handleInput(data) {
|
|
228
|
-
// An open menu owns the keys.
|
|
229
|
-
if (this.#menu !== null) {
|
|
230
|
-
if (data === "\x1b") {
|
|
231
|
-
this.#menu = null;
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
if (this.#menu.kind === "switcher") {
|
|
235
|
-
const field = this.#block.switcher;
|
|
236
|
-
if (field === undefined)
|
|
237
|
-
return;
|
|
238
|
-
const options = this.#switcherOptions(field);
|
|
239
|
-
if (data === "\x1b[A") {
|
|
240
|
-
this.#menu.cursor = Math.max(0, this.#menu.cursor - 1);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (data === "\x1b[B") {
|
|
244
|
-
this.#menu.cursor = Math.min(options.length - 1, this.#menu.cursor + 1);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (data === "\r") {
|
|
248
|
-
const option = options[this.#menu.cursor];
|
|
249
|
-
if (option !== undefined) {
|
|
250
|
-
// The switcher navigates (spec §5.1): params carry the id.
|
|
251
|
-
this.#ctx.dispatch({ type: "navigate", params: { [field.name]: option.id } }, { [field.name]: option.id });
|
|
252
|
-
}
|
|
253
|
-
this.#menu = null;
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const actions = this.#visibleActions();
|
|
259
|
-
if (data === "\x1b[A") {
|
|
260
|
-
this.#menu.cursor = Math.max(0, this.#menu.cursor - 1);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (data === "\x1b[B") {
|
|
264
|
-
this.#menu.cursor = Math.min(actions.length - 1, this.#menu.cursor + 1);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (data === "\r") {
|
|
268
|
-
const button = actions[this.#menu.cursor];
|
|
269
|
-
if (button !== undefined)
|
|
270
|
-
this.#ctx.dispatch(button.action, {});
|
|
271
|
-
this.#menu = null;
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
// The pushed detail owns Esc (back).
|
|
277
|
-
if (this.#detailPushed) {
|
|
278
|
-
if (data === "\x1b") {
|
|
279
|
-
this.#detailPushed = false;
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
for (const component of this.#detail)
|
|
283
|
-
component.handleInput?.(data);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
// The compact sidebar overlay owns Esc (close).
|
|
287
|
-
if (this.#sidebarOpen) {
|
|
288
|
-
if (data === "\x1b") {
|
|
289
|
-
this.#sidebarOpen = false;
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
for (const component of this.#sidebar)
|
|
293
|
-
component.handleInput?.(data);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (data === "\x13") {
|
|
297
|
-
// ctrl+s opens the switcher.
|
|
298
|
-
if (this.#block.switcher !== undefined) {
|
|
299
|
-
this.#menu = { kind: "switcher", cursor: 0 };
|
|
300
|
-
}
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (data === ".") {
|
|
304
|
-
if (this.#visibleActions().length > 0)
|
|
305
|
-
this.#menu = { kind: "actions", cursor: 0 };
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (data === "\t") {
|
|
309
|
-
// Tab cycles the sidebar (a focused pane at ≥ 100 columns, a
|
|
310
|
-
// toggled overlay below).
|
|
311
|
-
if (this.#hasSidebar) {
|
|
312
|
-
if (this.#sideBySideKnown === true) {
|
|
313
|
-
this.#region = this.#region === "sidebar" ? "main" : "sidebar";
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
this.#sidebarOpen = !this.#sidebarOpen;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (data === "\r" && this.#hasDetail && !this.#detailPushed) {
|
|
322
|
-
this.#detailPushed = true;
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
// Forward to the focused region.
|
|
326
|
-
const target = this.#region === "sidebar" && this.#hasSidebar ? this.#sidebar : this.#main;
|
|
327
|
-
for (const component of target)
|
|
328
|
-
component.handleInput?.(data);
|
|
329
|
-
}
|
|
330
|
-
/** Whether the last render was the side-by-side layout (Tab's
|
|
331
|
-
* pane-vs-overlay decision — set by the host when it knows; the
|
|
332
|
-
* render path computes it from the width). */
|
|
333
|
-
#sideBySideKnown;
|
|
334
|
-
/** The host tells the page the current pane width's layout class
|
|
335
|
-
* (render computes it too; this setter keeps Tab consistent when
|
|
336
|
-
* input arrives between renders). */
|
|
337
|
-
setSideBySide(sideBySide) {
|
|
338
|
-
this.#sideBySideKnown = sideBySide;
|
|
339
|
-
}
|
|
340
|
-
invalidate() {
|
|
341
|
-
for (const component of [...this.#main, ...this.#sidebar, ...this.#detail]) {
|
|
342
|
-
component.invalidate();
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
/** Test introspection: the pushed detail / overlay state. */
|
|
346
|
-
get detailPushed() {
|
|
347
|
-
return this.#detailPushed;
|
|
348
|
-
}
|
|
349
|
-
get sidebarOpen() {
|
|
350
|
-
return this.#sidebarOpen;
|
|
351
|
-
}
|
|
352
|
-
get menuKind() {
|
|
353
|
-
return this.#menu?.kind ?? null;
|
|
354
|
-
}
|
|
355
|
-
}
|
package/dist/blocks/render.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The renderer's front door (UI-blocks U2 Task 1): one pi-tui
|
|
3
|
-
* component per block, dispatched on `block.type`. Layout blocks come
|
|
4
|
-
* from `layout.ts`, the simple content blocks from `content.ts`; later
|
|
5
|
-
* tasks slot their blocks into the same dispatch.
|
|
6
|
-
*
|
|
7
|
-
* - `visibleAt` (spec §12.2) gates here so every block inherits it:
|
|
8
|
-
* `atLeast:regular` renders nothing in a compact pane. An unknown
|
|
9
|
-
* condition fails visible, matching §4.4's lean (a newer spec's
|
|
10
|
-
* blocks stay on screen).
|
|
11
|
-
* - A type this client can't render yet (spec §4.4) shows the block's
|
|
12
|
-
* `fallback` text plus "Not supported in this client" — never
|
|
13
|
-
* dropped silently.
|
|
14
|
-
*/
|
|
15
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
16
|
-
import type { Block } from "@sigrun/shared/blocks";
|
|
17
|
-
import { type BlockCtx } from "./context.js";
|
|
18
|
-
import type { ImageOptions } from "./image.js";
|
|
19
|
-
export { widthClassOf } from "./width.js";
|
|
20
|
-
export type { WidthClass } from "./width.js";
|
|
21
|
-
export type { BlockCtx, ViewState } from "./context.js";
|
|
22
|
-
export type { ImageOptions } from "./image.js";
|
|
23
|
-
/** The fallback notice (spec §4.4's exact words). */
|
|
24
|
-
export declare const NOT_SUPPORTED = "Not supported in this client";
|
|
25
|
-
/** The seams `renderBlock` threads to the blocks that need them. */
|
|
26
|
-
export type RenderOptions = ImageOptions & {
|
|
27
|
-
/** Whether the form sits in an interaction placement (§9.1 stepper). */
|
|
28
|
-
interaction?: boolean;
|
|
29
|
-
};
|
|
30
|
-
/** Render any block as a pi-tui component (the renderer's dispatch). */
|
|
31
|
-
export declare function renderBlock(block: Block, ctx: BlockCtx, options?: RenderOptions): Component;
|