@sigrun/tui 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/dist/actions.d.ts +119 -0
  3. package/dist/actions.js +12 -0
  4. package/dist/api.d.ts +209 -0
  5. package/dist/api.js +282 -0
  6. package/dist/blocks/braille.d.ts +41 -0
  7. package/dist/blocks/braille.js +131 -0
  8. package/dist/blocks/chart.d.ts +20 -0
  9. package/dist/blocks/chart.js +397 -0
  10. package/dist/blocks/chat-transport.d.ts +44 -0
  11. package/dist/blocks/chat-transport.js +116 -0
  12. package/dist/blocks/collection/calendar.d.ts +28 -0
  13. package/dist/blocks/collection/calendar.js +232 -0
  14. package/dist/blocks/collection/card.d.ts +36 -0
  15. package/dist/blocks/collection/card.js +128 -0
  16. package/dist/blocks/collection/collection.d.ts +9 -0
  17. package/dist/blocks/collection/collection.js +26 -0
  18. package/dist/blocks/collection/grid.d.ts +18 -0
  19. package/dist/blocks/collection/grid.js +72 -0
  20. package/dist/blocks/collection/lanes.d.ts +32 -0
  21. package/dist/blocks/collection/lanes.js +320 -0
  22. package/dist/blocks/collection/list.d.ts +30 -0
  23. package/dist/blocks/collection/list.js +273 -0
  24. package/dist/blocks/collection/move.d.ts +67 -0
  25. package/dist/blocks/collection/move.js +96 -0
  26. package/dist/blocks/collection/shared.d.ts +87 -0
  27. package/dist/blocks/collection/shared.js +177 -0
  28. package/dist/blocks/collection/table.d.ts +26 -0
  29. package/dist/blocks/collection/table.js +221 -0
  30. package/dist/blocks/content.d.ts +48 -0
  31. package/dist/blocks/content.js +253 -0
  32. package/dist/blocks/context.d.ts +117 -0
  33. package/dist/blocks/context.js +65 -0
  34. package/dist/blocks/diff.d.ts +21 -0
  35. package/dist/blocks/diff.js +25 -0
  36. package/dist/blocks/ext-transport.d.ts +70 -0
  37. package/dist/blocks/ext-transport.js +165 -0
  38. package/dist/blocks/form/choice.d.ts +54 -0
  39. package/dist/blocks/form/choice.js +175 -0
  40. package/dist/blocks/form/fields.d.ts +56 -0
  41. package/dist/blocks/form/fields.js +136 -0
  42. package/dist/blocks/form/file.d.ts +61 -0
  43. package/dist/blocks/form/file.js +121 -0
  44. package/dist/blocks/form/form.d.ts +66 -0
  45. package/dist/blocks/form/form.js +764 -0
  46. package/dist/blocks/form/list.d.ts +37 -0
  47. package/dist/blocks/form/list.js +70 -0
  48. package/dist/blocks/form/values.d.ts +20 -0
  49. package/dist/blocks/form/values.js +106 -0
  50. package/dist/blocks/icons.d.ts +14 -0
  51. package/dist/blocks/icons.js +74 -0
  52. package/dist/blocks/image.d.ts +39 -0
  53. package/dist/blocks/image.js +82 -0
  54. package/dist/blocks/interaction-transport.d.ts +42 -0
  55. package/dist/blocks/interaction-transport.js +79 -0
  56. package/dist/blocks/layout.d.ts +31 -0
  57. package/dist/blocks/layout.js +155 -0
  58. package/dist/blocks/log.d.ts +24 -0
  59. package/dist/blocks/log.js +111 -0
  60. package/dist/blocks/meter.d.ts +16 -0
  61. package/dist/blocks/meter.js +50 -0
  62. package/dist/blocks/page.d.ts +54 -0
  63. package/dist/blocks/page.js +360 -0
  64. package/dist/blocks/render.d.ts +31 -0
  65. package/dist/blocks/render.js +163 -0
  66. package/dist/blocks/surface-view.d.ts +41 -0
  67. package/dist/blocks/surface-view.js +137 -0
  68. package/dist/blocks/surface.d.ts +80 -0
  69. package/dist/blocks/surface.js +506 -0
  70. package/dist/blocks/tones.d.ts +23 -0
  71. package/dist/blocks/tones.js +63 -0
  72. package/dist/blocks/treeview.d.ts +22 -0
  73. package/dist/blocks/treeview.js +121 -0
  74. package/dist/blocks/width.d.ts +32 -0
  75. package/dist/blocks/width.js +70 -0
  76. package/dist/clipboard.d.ts +20 -0
  77. package/dist/clipboard.js +19 -0
  78. package/dist/editor-launch.d.ts +34 -0
  79. package/dist/editor-launch.js +70 -0
  80. package/dist/keymaps/emacs.d.ts +18 -0
  81. package/dist/keymaps/emacs.js +172 -0
  82. package/dist/keymaps/standard.d.ts +17 -0
  83. package/dist/keymaps/standard.js +175 -0
  84. package/dist/keymaps/types.d.ts +43 -0
  85. package/dist/keymaps/types.js +28 -0
  86. package/dist/keymaps/vim.d.ts +48 -0
  87. package/dist/keymaps/vim.js +291 -0
  88. package/dist/launch.d.ts +159 -0
  89. package/dist/launch.js +478 -0
  90. package/dist/sse.d.ts +112 -0
  91. package/dist/sse.js +418 -0
  92. package/dist/theme.d.ts +51 -0
  93. package/dist/theme.js +119 -0
  94. package/dist/ui/ambient.d.ts +78 -0
  95. package/dist/ui/ambient.js +264 -0
  96. package/dist/ui/app.d.ts +319 -0
  97. package/dist/ui/app.js +1539 -0
  98. package/dist/ui/composer.d.ts +66 -0
  99. package/dist/ui/composer.js +133 -0
  100. package/dist/ui/conversation-pane.d.ts +143 -0
  101. package/dist/ui/conversation-pane.js +615 -0
  102. package/dist/ui/help.d.ts +15 -0
  103. package/dist/ui/help.js +80 -0
  104. package/dist/ui/interactions.d.ts +91 -0
  105. package/dist/ui/interactions.js +477 -0
  106. package/dist/ui/picker.d.ts +26 -0
  107. package/dist/ui/picker.js +130 -0
  108. package/dist/ui/settings.d.ts +68 -0
  109. package/dist/ui/settings.js +254 -0
  110. package/dist/ui/sidebar.d.ts +99 -0
  111. package/dist/ui/sidebar.js +269 -0
  112. package/dist/ui/status-bar.d.ts +77 -0
  113. package/dist/ui/status-bar.js +146 -0
  114. package/package.json +4 -3
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The layout blocks (UI-blocks spec §5.2): `stack`, `columns`, `grid`,
3
+ * `section` as pi-tui components.
4
+ *
5
+ * Columns/grid honour the compact fallbacks (spec §12.3): `columns`
6
+ * stack when the minimums don't fit; `grid` runs
7
+ * `max(1, floor(width / minItemWidth))` columns so a compact pane gets
8
+ * one. Every line every block emits is clamped to the pane
9
+ * ({@link truncate} — Review Focus 2).
10
+ */
11
+ import { visibleWidth } from "@earendil-works/pi-tui";
12
+ import { renderBlock } from "./render.js";
13
+ import { bindText, bindTone } from "./context.js";
14
+ import { SIZE_CELLS, pad, truncate } from "./width.js";
15
+ import { toneFg, toneMarker } from "./tones.js";
16
+ /** The blank line counts the `gap` tokens map to. */
17
+ const GAP_LINES = { s: 0, m: 1, l: 2 };
18
+ /** The section header rule character. */
19
+ const RULE = "─";
20
+ /**
21
+ * A component from a width-aware line builder. Each produced line is
22
+ * clamped to the render width (the invariant every block must keep).
23
+ */
24
+ export class Lines {
25
+ #build;
26
+ constructor(build) {
27
+ this.#build = build;
28
+ }
29
+ render(width) {
30
+ return this.#build(width).map((line) => truncate(line, width));
31
+ }
32
+ invalidate() {
33
+ // Stateless render: lines re-assemble on the next render call.
34
+ }
35
+ }
36
+ /** Join child blocks vertically, a blank line between neighbours. */
37
+ function stacked(children, ctx, width, gap) {
38
+ const out = [];
39
+ for (const child of children) {
40
+ const lines = renderBlock(child, ctx).render(width);
41
+ if (out.length > 0 && lines.length > 0) {
42
+ for (let i = 0; i < gap; i++)
43
+ out.push("");
44
+ }
45
+ out.push(...lines);
46
+ }
47
+ return out;
48
+ }
49
+ /** Render `children` vertically with `gap` blank lines between. */
50
+ export function renderStack(block, ctx) {
51
+ return new Lines((width) => stacked(block.children, ctx, width, GAP_LINES[block.gap ?? "m"]));
52
+ }
53
+ /** Render children side by side, stacking when the minimums don't fit. */
54
+ export function renderColumns(block, ctx) {
55
+ return new Lines((width) => {
56
+ const minimums = block.children.map((child) => child.minWidth === undefined ? SIZE_CELLS.s : SIZE_CELLS[child.minWidth]);
57
+ const total = minimums.reduce((a, b) => a + b, 0) + Math.max(0, block.children.length - 1);
58
+ // Compact fallback (spec §12.3): stack when the minimums don't fit.
59
+ if (width < total) {
60
+ return stacked(block.children.map((child) => child.block), ctx, width, 1);
61
+ }
62
+ // Side by side: render each child at a weighted slot, then zip
63
+ // the line arrays into rows (shorter columns pad with blanks).
64
+ const weights = block.children.map((child) => child.weight ?? 1);
65
+ const weightSum = weights.reduce((a, b) => a + b, 0);
66
+ const separators = block.children.length - 1;
67
+ const slots = weights.map((weight) => Math.max(1, Math.floor(((width - separators) * weight) / weightSum)));
68
+ const rendered = block.children.map((child, index) => renderBlock(child.block, ctx).render(slots[index]));
69
+ const height = Math.max(...rendered.map((lines) => lines.length));
70
+ const rows = [];
71
+ for (let row = 0; row < height; row++) {
72
+ rows.push(rendered
73
+ .map((lines, index) => pad(lines[row] ?? "", slots[index]))
74
+ .join(" "));
75
+ }
76
+ return rows;
77
+ });
78
+ }
79
+ /** Render children in as many columns as `minItemWidth` allows. */
80
+ export function renderGrid(block, ctx) {
81
+ return new Lines((width) => {
82
+ const minItem = SIZE_CELLS[block.minItemWidth];
83
+ const columns = Math.max(1, Math.floor(width / minItem));
84
+ const columnWidth = Math.max(1, Math.floor((width - (columns - 1)) / columns));
85
+ // Slot each child by its span ("full" spans every column).
86
+ const rows = [];
87
+ let current = [];
88
+ let used = 0;
89
+ const flush = () => {
90
+ if (current.length > 0)
91
+ rows.push(current);
92
+ current = [];
93
+ used = 0;
94
+ };
95
+ for (const child of block.children) {
96
+ let span = 1;
97
+ if (child.span === "full")
98
+ span = columns;
99
+ else if (child.span !== undefined)
100
+ span = Math.min(child.span, columns);
101
+ if (used > 0 && used + span > columns)
102
+ flush();
103
+ current.push({
104
+ span,
105
+ lines: renderBlock(child.block, ctx).render(columnWidth * span + (span - 1)),
106
+ });
107
+ used += span;
108
+ }
109
+ flush();
110
+ // Zip each row's cells into lines (shorter cells pad with blanks).
111
+ const out = [];
112
+ for (const row of rows) {
113
+ const height = Math.max(...row.map((cell) => cell.lines.length));
114
+ const widths = row.map((cell) => columnWidth * cell.span + (cell.span - 1));
115
+ for (let line = 0; line < height; line++) {
116
+ out.push(row
117
+ .map((cell, index) => pad(cell.lines[line] ?? "", widths[index]))
118
+ .join(" "));
119
+ }
120
+ }
121
+ return out;
122
+ });
123
+ }
124
+ /** Render a titled, optionally toned group of children. */
125
+ export function renderSection(block, ctx) {
126
+ return new Lines((width) => {
127
+ const title = bindText(block.title ?? "", ctx);
128
+ const subtitle = bindText(block.subtitle ?? "", ctx);
129
+ const out = [];
130
+ if (title !== "") {
131
+ if (block.tone !== undefined) {
132
+ const tone = bindTone(block.tone, ctx);
133
+ const head = `${toneFg(ctx.theme, tone)(toneMarker(tone, !ctx.unicode))} ${title}`;
134
+ const rule = RULE.repeat(Math.max(0, width - visibleWidth(head) - 1));
135
+ out.push(`${head} ${ctx.theme.dim(rule)}`);
136
+ }
137
+ else {
138
+ const rule = RULE.repeat(Math.max(0, width - visibleWidth(title) - 1));
139
+ out.push(`${title} ${ctx.theme.dim(rule)}`);
140
+ }
141
+ }
142
+ if (subtitle !== "")
143
+ out.push(ctx.theme.dim(subtitle));
144
+ // `collapsed` is the author's INITIAL state (§5.2): children stay
145
+ // hidden until a later task's toggle expands them.
146
+ const children = block.collapsed
147
+ ? []
148
+ : block.children.filter((child) => "name" in child === false);
149
+ const inner = width - 2;
150
+ const body = stacked(children, ctx, Math.max(1, inner), GAP_LINES.m);
151
+ for (const line of body)
152
+ out.push(line === "" ? "" : ` ${line}`);
153
+ return out;
154
+ });
155
+ }
@@ -0,0 +1,24 @@
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 type { Component } from "@earendil-works/pi-tui";
13
+ import type { LogBlock } from "@sigrun/shared/blocks";
14
+ import { type BlockCtx } from "./context.js";
15
+ /** The viewport height in rows. */
16
+ export declare const LOG_VIEWPORT_ROWS = 10;
17
+ /** `log`: the follow-tail viewport. */
18
+ export declare class LogComponent implements Component {
19
+ #private;
20
+ constructor(block: LogBlock, ctx: BlockCtx);
21
+ render(width: number): string[];
22
+ handleInput(data: string): void;
23
+ invalidate(): void;
24
+ }
@@ -0,0 +1,111 @@
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
+ }
@@ -0,0 +1,16 @@
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;
@@ -0,0 +1,50 @@
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
+ }
@@ -0,0 +1,54 @@
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
+ }