@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +2 -2
  3. package/dist/actions.d.ts +0 -119
  4. package/dist/actions.js +0 -12
  5. package/dist/api.d.ts +0 -152
  6. package/dist/api.js +0 -189
  7. package/dist/blocks/braille.d.ts +0 -41
  8. package/dist/blocks/braille.js +0 -131
  9. package/dist/blocks/chart.d.ts +0 -20
  10. package/dist/blocks/chart.js +0 -397
  11. package/dist/blocks/collection/calendar.d.ts +0 -28
  12. package/dist/blocks/collection/calendar.js +0 -232
  13. package/dist/blocks/collection/card.d.ts +0 -36
  14. package/dist/blocks/collection/card.js +0 -128
  15. package/dist/blocks/collection/collection.d.ts +0 -9
  16. package/dist/blocks/collection/collection.js +0 -26
  17. package/dist/blocks/collection/grid.d.ts +0 -18
  18. package/dist/blocks/collection/grid.js +0 -72
  19. package/dist/blocks/collection/lanes.d.ts +0 -32
  20. package/dist/blocks/collection/lanes.js +0 -320
  21. package/dist/blocks/collection/list.d.ts +0 -30
  22. package/dist/blocks/collection/list.js +0 -273
  23. package/dist/blocks/collection/move.d.ts +0 -67
  24. package/dist/blocks/collection/move.js +0 -96
  25. package/dist/blocks/collection/shared.d.ts +0 -87
  26. package/dist/blocks/collection/shared.js +0 -177
  27. package/dist/blocks/collection/table.d.ts +0 -26
  28. package/dist/blocks/collection/table.js +0 -221
  29. package/dist/blocks/content.d.ts +0 -48
  30. package/dist/blocks/content.js +0 -253
  31. package/dist/blocks/context.d.ts +0 -96
  32. package/dist/blocks/context.js +0 -65
  33. package/dist/blocks/diff.d.ts +0 -21
  34. package/dist/blocks/diff.js +0 -25
  35. package/dist/blocks/ext-transport.d.ts +0 -70
  36. package/dist/blocks/ext-transport.js +0 -165
  37. package/dist/blocks/form/choice.d.ts +0 -54
  38. package/dist/blocks/form/choice.js +0 -175
  39. package/dist/blocks/form/fields.d.ts +0 -56
  40. package/dist/blocks/form/fields.js +0 -136
  41. package/dist/blocks/form/file.d.ts +0 -61
  42. package/dist/blocks/form/file.js +0 -121
  43. package/dist/blocks/form/form.d.ts +0 -53
  44. package/dist/blocks/form/form.js +0 -703
  45. package/dist/blocks/form/list.d.ts +0 -37
  46. package/dist/blocks/form/list.js +0 -70
  47. package/dist/blocks/form/values.d.ts +0 -20
  48. package/dist/blocks/form/values.js +0 -104
  49. package/dist/blocks/icons.d.ts +0 -14
  50. package/dist/blocks/icons.js +0 -74
  51. package/dist/blocks/image.d.ts +0 -39
  52. package/dist/blocks/image.js +0 -82
  53. package/dist/blocks/layout.d.ts +0 -31
  54. package/dist/blocks/layout.js +0 -155
  55. package/dist/blocks/log.d.ts +0 -24
  56. package/dist/blocks/log.js +0 -111
  57. package/dist/blocks/meter.d.ts +0 -16
  58. package/dist/blocks/meter.js +0 -50
  59. package/dist/blocks/page.d.ts +0 -54
  60. package/dist/blocks/page.js +0 -355
  61. package/dist/blocks/render.d.ts +0 -31
  62. package/dist/blocks/render.js +0 -145
  63. package/dist/blocks/surface-view.d.ts +0 -33
  64. package/dist/blocks/surface-view.js +0 -86
  65. package/dist/blocks/surface.d.ts +0 -75
  66. package/dist/blocks/surface.js +0 -468
  67. package/dist/blocks/tones.d.ts +0 -23
  68. package/dist/blocks/tones.js +0 -63
  69. package/dist/blocks/treeview.d.ts +0 -22
  70. package/dist/blocks/treeview.js +0 -121
  71. package/dist/blocks/width.d.ts +0 -32
  72. package/dist/blocks/width.js +0 -70
  73. package/dist/clipboard.d.ts +0 -20
  74. package/dist/clipboard.js +0 -19
  75. package/dist/editor-launch.d.ts +0 -34
  76. package/dist/editor-launch.js +0 -70
  77. package/dist/keymaps/emacs.d.ts +0 -18
  78. package/dist/keymaps/emacs.js +0 -172
  79. package/dist/keymaps/standard.d.ts +0 -17
  80. package/dist/keymaps/standard.js +0 -175
  81. package/dist/keymaps/types.d.ts +0 -43
  82. package/dist/keymaps/types.js +0 -28
  83. package/dist/keymaps/vim.d.ts +0 -48
  84. package/dist/keymaps/vim.js +0 -291
  85. package/dist/launch.d.ts +0 -149
  86. package/dist/launch.js +0 -446
  87. package/dist/sse.d.ts +0 -109
  88. package/dist/sse.js +0 -401
  89. package/dist/theme.d.ts +0 -51
  90. package/dist/theme.js +0 -119
  91. package/dist/ui/ambient.d.ts +0 -81
  92. package/dist/ui/ambient.js +0 -301
  93. package/dist/ui/app.d.ts +0 -245
  94. package/dist/ui/app.js +0 -1325
  95. package/dist/ui/composer.d.ts +0 -66
  96. package/dist/ui/composer.js +0 -125
  97. package/dist/ui/conversation-pane.d.ts +0 -123
  98. package/dist/ui/conversation-pane.js +0 -463
  99. package/dist/ui/help.d.ts +0 -15
  100. package/dist/ui/help.js +0 -80
  101. package/dist/ui/interactions.d.ts +0 -90
  102. package/dist/ui/interactions.js +0 -330
  103. package/dist/ui/picker.d.ts +0 -26
  104. package/dist/ui/picker.js +0 -130
  105. package/dist/ui/sidebar.d.ts +0 -99
  106. package/dist/ui/sidebar.js +0 -269
  107. package/dist/ui/status-bar.d.ts +0 -77
  108. package/dist/ui/status-bar.js +0 -146
@@ -1,96 +0,0 @@
1
- /**
2
- * Keyboard moves (UI-blocks U2 Task 4, spec §12.4): the shared
3
- * placement machinery the lanes and calendar displays drive. `m` picks
4
- * the item under the cursor up, arrows move the placement (left/right
5
- * across groups, up/down within), Enter drops — dispatching
6
- * `{itemId, from: {group, index}, to: {group, index}}`, the EXACT
7
- * payload U1's pointer drop builds — and Esc cancels.
8
- *
9
- * The held item counts as PRESENT in the lane sizing (its removal
10
- * happens on drop, not on pickup) — the same rule as U1's lanes. A
11
- * dropped placement equal to the item's current one dispatches nothing.
12
- *
13
- * Review Focus 1: a resize while picked up is handled by the DISPLAYS,
14
- * which recompute placements from the live block data on every key; the
15
- * held state survives because it lives in the component, not the
16
- * rendered lines.
17
- */
18
- import { collectionItems, groupItems, isQuiet, sortItems, } from "./shared.js";
19
- /** The visible items of a group (sorted, loud items — quiet items are
20
- * folded behind the fold row and not placement targets). */
21
- export function visibleLaneItems(block, ctx, group) {
22
- const sorted = sortItems(group.items, block.sort);
23
- return sorted.filter((row) => !isQuiet(block, ctx, row.item));
24
- }
25
- /** Every group with its visible items, in group order. */
26
- export function lanes(block, ctx) {
27
- const groups = groupItems(block, sortItems(collectionItems(block, ctx), block.sort), ctx);
28
- const visible = new Map();
29
- for (const group of groups) {
30
- visible.set(group.id, visibleLaneItems(block, ctx, group));
31
- }
32
- return { groups, visible };
33
- }
34
- /** An item's current placement across every group. */
35
- export function placementOf(groups, visible, itemId) {
36
- for (const group of groups) {
37
- const items = visible.get(group.id) ?? [];
38
- const index = items.findIndex((row) => row.id === itemId);
39
- if (index >= 0)
40
- return { group: group.id, index };
41
- }
42
- return undefined;
43
- }
44
- /** The move payload (spec §6.3 step 2; U1's pointer-drop shape exactly). */
45
- export function movePayload(move) {
46
- return {
47
- itemId: move.itemId,
48
- from: { group: move.from.group, index: move.from.index },
49
- to: { group: move.to.group, index: move.to.index },
50
- };
51
- }
52
- /** Move the placement left/right across groups (the index clamps to
53
- * the target lane's size; out-of-range directions are inert). */
54
- export function shiftAcross(move, groups, visible, delta) {
55
- const order = groups.map((group) => group.id);
56
- const laneIndex = order.indexOf(move.at.group);
57
- const target = order[laneIndex + delta];
58
- if (target === undefined)
59
- return move;
60
- const size = (visible.get(target) ?? []).length;
61
- return {
62
- ...move,
63
- at: { group: target, index: Math.min(move.at.index, size) },
64
- };
65
- }
66
- /** Move the placement up/down within its group (clamped). */
67
- export function shiftWithin(move, visible, delta) {
68
- const size = (visible.get(move.at.group) ?? []).length;
69
- const index = delta === 1 ? Math.min(move.at.index + 1, size) : Math.max(move.at.index - 1, 0);
70
- return { ...move, at: { group: move.at.group, index } };
71
- }
72
- /**
73
- * Dispatch the drop: nothing when the placement equals the current
74
- * position; otherwise the `move` action with the U1 payload. The
75
- * optimistic flip lives in SurfaceState's overlay — the component
76
- * itself keeps no per-item move override.
77
- */
78
- export function dispatchMove(block, ctx, move) {
79
- if (block.onMove === undefined)
80
- return;
81
- if (move.from.group === move.at.group && move.from.index === move.at.index)
82
- return;
83
- ctx.dispatch({ type: "move", name: block.onMove }, movePayload({ itemId: move.itemId, from: move.from, to: move.at }));
84
- }
85
- /** The per-display move session state (held in the component). */
86
- export class MoveSession {
87
- held;
88
- /** Pick up the item (undefined placement = no pickup). */
89
- pickUp(itemId, from) {
90
- this.held = { itemId, from, at: from };
91
- }
92
- /** Cancel (Esc): drop the session without dispatching. */
93
- cancel() {
94
- this.held = undefined;
95
- }
96
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * Shared collection machinery (UI-blocks spec §5.5): item resolution,
3
- * grouping, sorting and the view-only interaction state (selection,
4
- * folds, menu) the list/table/grid displays drive.
5
- *
6
- * Every interactive display owns a {@link CollectionState} — its shape
7
- * mirrors the `/view/<blockId>/` branch Task 4's surface state will own
8
- * (sort, page, selection, expanded, quietShown). View-only state never
9
- * dispatches (§6.2): only `onCheck` (`edit`), `onActivate`, and bulk
10
- * `submit` actions reach `ctx.dispatch`.
11
- */
12
- import type { CollectionBlock, JsonObject, JsonValue, Tree } from "@sigrun/shared/blocks";
13
- import { type BlockCtx } from "../context.js";
14
- /** A resolved collection row: the item and its stable id. */
15
- export interface CollectionItem {
16
- id: string;
17
- item: JsonObject;
18
- }
19
- /** A resolved group: heading text and its items, in `groups` order. */
20
- export interface ItemGroup {
21
- id: string;
22
- title: string;
23
- collapsed: boolean;
24
- items: CollectionItem[];
25
- }
26
- /** Resolve `items` to rows, dropping malformed entries. */
27
- export declare function collectionItems(block: CollectionBlock, ctx: BlockCtx): CollectionItem[];
28
- /** Nested child rows of an item (the `children` field's records). */
29
- export declare function childItems(block: CollectionBlock, item: JsonObject): CollectionItem[];
30
- /** Compare two items on `field`, ascending. */
31
- export declare function compareItems(a: JsonObject, b: JsonObject, field: string): number;
32
- /** Order items by a `{field, dir}` sort (stable; view state wins over the block's). */
33
- export declare function sortItems(items: readonly CollectionItem[], sort: {
34
- field: string;
35
- dir: "asc" | "desc";
36
- } | undefined): CollectionItem[];
37
- /**
38
- * Group items by `groupBy`: declared `groups` in declared order, then
39
- * unseen values in first-appearance item order. With no `groupBy`,
40
- * one anonymous group holds everything.
41
- */
42
- export declare function groupItems(block: CollectionBlock, items: readonly CollectionItem[], ctx: BlockCtx): ItemGroup[];
43
- /** Is an item quiet (folded behind "N more")? */
44
- export declare function isQuiet(block: CollectionBlock, ctx: BlockCtx, item: JsonValue): boolean;
45
- /**
46
- * The view-only interaction state of one collection component. The
47
- * checkable toggle's optimistic flip is part of it: the data update
48
- * that follows a successful `onCheck` replaces the optimistic value
49
- * (§6.3's convention, without moves).
50
- */
51
- export declare class CollectionState {
52
- cursor: number;
53
- readonly selected: Set<string>;
54
- readonly checkedOverrides: Map<string, boolean>;
55
- readonly expanded: Set<string>;
56
- readonly unfoldedGroups: Set<string>;
57
- /** The groups whose quiet fold is open (per-group; Task 3's global
58
- * toggle became per-group with the lanes' per-lane folds). */
59
- readonly quietOpen: Set<string>;
60
- /** Is the quiet fold of `groupId` open? */
61
- quietShownFor(groupId: string): boolean;
62
- /** Toggle the quiet fold of `groupId`. */
63
- toggleQuiet(groupId: string): void;
64
- /** The open overlay: the sort choice, the bulk menu, the seam form,
65
- * or the compact lanes' "Move to…" target list (Task 4). */
66
- menu: {
67
- kind: "sort";
68
- } | {
69
- kind: "bulk";
70
- } | {
71
- kind: "form";
72
- form: Tree;
73
- onSubmit: string;
74
- } | {
75
- kind: "moveTo";
76
- } | null;
77
- menuCursor: number;
78
- formBuffer: string;
79
- /** The effective checked value of an item (data plus the optimistic flip). */
80
- checkedValue(block: CollectionBlock, row: CollectionItem): boolean;
81
- }
82
- /** Dispatch the `onCheck` edit for an item (spec §5.5: `{itemId, value}`). */
83
- export declare function dispatchCheck(block: CollectionBlock, ctx: BlockCtx, state: CollectionState, row: CollectionItem): void;
84
- /** Dispatch `onActivate` for an item, payload `{itemId}`. */
85
- export declare function dispatchActivate(block: CollectionBlock, ctx: BlockCtx, row: CollectionItem): void;
86
- /** Dispatch a bulk action's submit for the current selection. */
87
- export declare function dispatchBulk(ctx: BlockCtx, state: CollectionState, onSubmit: string, values?: JsonObject): void;
@@ -1,177 +0,0 @@
1
- /**
2
- * Shared collection machinery (UI-blocks spec §5.5): item resolution,
3
- * grouping, sorting and the view-only interaction state (selection,
4
- * folds, menu) the list/table/grid displays drive.
5
- *
6
- * Every interactive display owns a {@link CollectionState} — its shape
7
- * mirrors the `/view/<blockId>/` branch Task 4's surface state will own
8
- * (sort, page, selection, expanded, quietShown). View-only state never
9
- * dispatches (§6.2): only `onCheck` (`edit`), `onActivate`, and bulk
10
- * `submit` actions reach `ctx.dispatch`.
11
- */
12
- import { bindRaw, bindText } from "../context.js";
13
- /** Resolve `items` to rows, dropping malformed entries. */
14
- export function collectionItems(block, ctx) {
15
- const raw = bindRaw(block.items, ctx);
16
- if (!Array.isArray(raw))
17
- return [];
18
- const out = [];
19
- for (const entry of raw) {
20
- if (entry === null || typeof entry !== "object" || Array.isArray(entry))
21
- continue;
22
- const record = entry;
23
- const id = typeof record.id === "string" ? record.id : String(record.id ?? "");
24
- if (id === "")
25
- continue;
26
- out.push({ id, item: record });
27
- }
28
- return out;
29
- }
30
- /** Nested child rows of an item (the `children` field's records). */
31
- export function childItems(block, item) {
32
- const field = block.children;
33
- if (field === undefined || field === "")
34
- return [];
35
- const raw = item[field];
36
- if (!Array.isArray(raw))
37
- return [];
38
- const out = [];
39
- for (const entry of raw) {
40
- if (entry === null || typeof entry !== "object" || Array.isArray(entry))
41
- continue;
42
- const record = entry;
43
- const id = typeof record.id === "string" ? record.id : String(record.id ?? "");
44
- if (id === "")
45
- continue;
46
- out.push({ id, item: record });
47
- }
48
- return out;
49
- }
50
- /** The sort key of an item for a field (numbers sort numerically). */
51
- function sortValue(item, field) {
52
- return item[field] ?? null;
53
- }
54
- /** Compare two items on `field`, ascending. */
55
- export function compareItems(a, b, field) {
56
- const left = sortValue(a, field);
57
- const right = sortValue(b, field);
58
- if (typeof left === "number" && typeof right === "number") {
59
- return left === right ? 0 : left < right ? -1 : 1;
60
- }
61
- const leftKey = left === null ? "" : String(left);
62
- const rightKey = right === null ? "" : String(right);
63
- if (leftKey === rightKey)
64
- return 0;
65
- return leftKey < rightKey ? -1 : 1;
66
- }
67
- /** Order items by a `{field, dir}` sort (stable; view state wins over the block's). */
68
- export function sortItems(items, sort) {
69
- if (sort === undefined)
70
- return [...items];
71
- const sign = sort.dir === "desc" ? -1 : 1;
72
- return [...items].sort((a, b) => compareItems(a.item, b.item, sort.field) * sign);
73
- }
74
- /**
75
- * Group items by `groupBy`: declared `groups` in declared order, then
76
- * unseen values in first-appearance item order. With no `groupBy`,
77
- * one anonymous group holds everything.
78
- */
79
- export function groupItems(block, items, ctx) {
80
- const by = block.groupBy;
81
- if (by === undefined || by === "") {
82
- return [{ id: "", title: "", collapsed: false, items: [...items] }];
83
- }
84
- const groups = [];
85
- const indexById = new Map();
86
- for (const declared of block.groups ?? []) {
87
- indexById.set(declared.id, groups.length);
88
- groups.push({
89
- id: declared.id,
90
- title: bindText(declared.title, ctx),
91
- collapsed: declared.collapsed === true,
92
- items: [],
93
- });
94
- }
95
- for (const row of items) {
96
- const value = row.item[by];
97
- const id = value === undefined || value === null ? "" : String(value);
98
- const index = indexById.get(id);
99
- if (index !== undefined) {
100
- groups[index]?.items.push(row);
101
- continue;
102
- }
103
- indexById.set(id, groups.length);
104
- groups.push({ id, title: id, collapsed: false, items: [row] });
105
- }
106
- return groups;
107
- }
108
- /** Is an item quiet (folded behind "N more")? */
109
- export function isQuiet(block, ctx, item) {
110
- if (block.item.quiet === undefined)
111
- return false;
112
- return bindRaw(block.item.quiet, { ...ctx, item }) === true;
113
- }
114
- /**
115
- * The view-only interaction state of one collection component. The
116
- * checkable toggle's optimistic flip is part of it: the data update
117
- * that follows a successful `onCheck` replaces the optimistic value
118
- * (§6.3's convention, without moves).
119
- */
120
- export class CollectionState {
121
- cursor = 0;
122
- selected = new Set();
123
- checkedOverrides = new Map();
124
- expanded = new Set();
125
- unfoldedGroups = new Set();
126
- /** The groups whose quiet fold is open (per-group; Task 3's global
127
- * toggle became per-group with the lanes' per-lane folds). */
128
- quietOpen = new Set();
129
- /** Is the quiet fold of `groupId` open? */
130
- quietShownFor(groupId) {
131
- return this.quietOpen.has(groupId);
132
- }
133
- /** Toggle the quiet fold of `groupId`. */
134
- toggleQuiet(groupId) {
135
- if (this.quietOpen.has(groupId))
136
- this.quietOpen.delete(groupId);
137
- else
138
- this.quietOpen.add(groupId);
139
- }
140
- /** The open overlay: the sort choice, the bulk menu, the seam form,
141
- * or the compact lanes' "Move to…" target list (Task 4). */
142
- menu = null;
143
- menuCursor = 0;
144
- formBuffer = "";
145
- /** The effective checked value of an item (data plus the optimistic flip). */
146
- checkedValue(block, row) {
147
- const override = this.checkedOverrides.get(row.id);
148
- if (override !== undefined)
149
- return override;
150
- if (block.checkable === undefined)
151
- return false;
152
- return row.item[block.checkable] === true;
153
- }
154
- }
155
- /** Dispatch the `onCheck` edit for an item (spec §5.5: `{itemId, value}`). */
156
- export function dispatchCheck(block, ctx, state, row) {
157
- if (block.onCheck === undefined || block.checkable === undefined)
158
- return;
159
- const value = !state.checkedValue(block, row);
160
- state.checkedOverrides.set(row.id, value);
161
- ctx.dispatch({ type: "edit", name: block.onCheck }, { itemId: row.id, value });
162
- }
163
- /** Dispatch `onActivate` for an item, payload `{itemId}`. */
164
- export function dispatchActivate(block, ctx, row) {
165
- if (block.onActivate === undefined)
166
- return;
167
- ctx.dispatch(block.onActivate, { itemId: row.id });
168
- }
169
- /** Dispatch a bulk action's submit for the current selection. */
170
- export function dispatchBulk(ctx, state, onSubmit, values) {
171
- const ids = [...state.selected];
172
- const payload = values === undefined ? { ids } : { ids, values };
173
- const action = { type: "submit", name: onSubmit };
174
- ctx.dispatch(action, payload);
175
- state.selected.clear();
176
- state.menu = null;
177
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * The `table` display (UI-blocks spec §5.5 + §12.3): display-width
3
- * aligned columns (CJK-safe via pi-tui's `visibleWidth`), `s` picks a
4
- * sortable column and cycles asc → desc, 50 rows per page with
5
- * PgUp/PgDn, and the compact fallback — rows as cards with
6
- * `priority: "low"` columns behind Enter-expand.
7
- *
8
- * Sort and page are view-only (§6.2): they never dispatch. The footer
9
- * carries the page range ("1–50 of 120"); the sort menu lists the
10
- * sortable columns only.
11
- */
12
- import type { Component } from "@earendil-works/pi-tui";
13
- import type { CollectionBlock } from "@sigrun/shared/blocks";
14
- import { type BlockCtx } from "../context.js";
15
- import { compareItems } from "./shared.js";
16
- /** The page size (spec §5.5: paged at 50 rows). */
17
- export declare const TABLE_PAGE_SIZE = 50;
18
- /** `table`: aligned, sortable, paged columns; cards at compact width. */
19
- export declare class TableComponent implements Component {
20
- #private;
21
- constructor(block: CollectionBlock, ctx: BlockCtx);
22
- render(width: number): string[];
23
- handleInput(data: string): void;
24
- invalidate(): void;
25
- }
26
- export { compareItems };
@@ -1,221 +0,0 @@
1
- /**
2
- * The `table` display (UI-blocks spec §5.5 + §12.3): display-width
3
- * aligned columns (CJK-safe via pi-tui's `visibleWidth`), `s` picks a
4
- * sortable column and cycles asc → desc, 50 rows per page with
5
- * PgUp/PgDn, and the compact fallback — rows as cards with
6
- * `priority: "low"` columns behind Enter-expand.
7
- *
8
- * Sort and page are view-only (§6.2): they never dispatch. The footer
9
- * carries the page range ("1–50 of 120"); the sort menu lists the
10
- * sortable columns only.
11
- */
12
- import { visibleWidth } from "@earendil-works/pi-tui";
13
- import { formatValue } from "@sigrun/shared/blocks";
14
- import { bindText } from "../context.js";
15
- import { truncate, widthClassOf } from "../width.js";
16
- import { meterBar } from "../meter.js";
17
- import { cardLines } from "./card.js";
18
- import { CollectionState, collectionItems, compareItems, sortItems, } from "./shared.js";
19
- /** The page size (spec §5.5: paged at 50 rows). */
20
- export const TABLE_PAGE_SIZE = 50;
21
- /** The sort menu entries: the sortable columns' labels. */
22
- function sortableColumns(block) {
23
- return (block.columns ?? []).filter((column) => column.sortable === true);
24
- }
25
- /** One cell's display text. */
26
- function cellText(column, row, ctx, width) {
27
- const value = row.item[column.field];
28
- if (column.cell === "meter") {
29
- const level = typeof value === "number" ? value : 0;
30
- return meterBar(level, Math.max(4, Math.min(10, Math.floor(width / 4))), !ctx.unicode);
31
- }
32
- if (value === undefined || value === null)
33
- return "";
34
- return formatValue(value, column.format, ctx.locale, ctx.timeZone);
35
- }
36
- /** `table`: aligned, sortable, paged columns; cards at compact width. */
37
- export class TableComponent {
38
- #block;
39
- #ctx;
40
- #state = new CollectionState();
41
- #sort;
42
- #page = 0;
43
- #tick = 0;
44
- constructor(block, ctx) {
45
- this.#block = block;
46
- this.#ctx = ctx;
47
- this.#sort = block.sort;
48
- }
49
- #rows() {
50
- return sortItems(collectionItems(this.#block, this.#ctx), this.#sort);
51
- }
52
- render(width) {
53
- this.#tick += 1;
54
- if (widthClassOf(width) === "compact")
55
- return this.#renderCompact(width);
56
- return this.#renderFull(width);
57
- }
58
- // ─── the full table ──────────────────────────────────────────────
59
- #renderFull(width) {
60
- const columns = this.#block.columns ?? [];
61
- const rows = this.#rows();
62
- const pages = Math.max(1, Math.ceil(rows.length / TABLE_PAGE_SIZE));
63
- if (this.#page >= pages)
64
- this.#page = pages - 1;
65
- const pageRows = rows.slice(this.#page * TABLE_PAGE_SIZE, (this.#page + 1) * TABLE_PAGE_SIZE);
66
- if (this.#state.menu?.kind === "sort") {
67
- const lines = [truncate("Sort by", width)];
68
- sortableColumns(this.#block).forEach((column, index) => {
69
- const cursor = index === this.#state.menuCursor ? "> " : " ";
70
- const arrow = this.#sort?.field === column.field ? (this.#sort.dir === "asc" ? " ▲" : " ▼") : "";
71
- lines.push(truncate(`${cursor}${bindText(column.label, this.#ctx)}${arrow}`, width));
72
- });
73
- return lines;
74
- }
75
- // Column widths: the widest display width of header or cells,
76
- // clamped so the table fits the pane.
77
- const labels = columns.map((column) => bindText(column.label, this.#ctx));
78
- const widths = columns.map((column, index) => {
79
- let widest = visibleWidth(labels[index] ?? "");
80
- for (const row of pageRows) {
81
- widest = Math.max(widest, visibleWidth(cellText(column, row, this.#ctx, width)));
82
- }
83
- return widest;
84
- });
85
- const gaps = Math.max(0, columns.length - 1) * 2;
86
- let budget = width - 2 - gaps; // the cursor column + separators
87
- for (let i = columns.length - 1; i >= 0 && widths.reduce((a, b) => a + b, 0) > budget; i--) {
88
- widths[i] = Math.max(4, (widths[i] ?? 4) - (widths.reduce((a, b) => a + b, 0) - budget));
89
- budget = width - 2 - gaps;
90
- }
91
- const join = (cells, cursor) => truncate(cursor +
92
- cells
93
- .map((cell, index) => {
94
- const column = columns[index];
95
- const cellWidth = widths[index] ?? 4;
96
- const fits = visibleWidth(cell) <= cellWidth;
97
- const text = fits ? cell : truncate(cell, cellWidth);
98
- const rest = cellWidth - visibleWidth(text);
99
- const padStr = rest > 0 ? " ".repeat(rest) : "";
100
- // numeric formats right-align; text left-aligns.
101
- return column?.format === "number" && text !== ""
102
- ? padStr + text
103
- : text + padStr;
104
- })
105
- .join(" "), width);
106
- const lines = [join(labels, " ")];
107
- for (const row of pageRows) {
108
- lines.push(join(columns.map((column) => cellText(column, row, this.#ctx, width)), " "));
109
- }
110
- const from = this.#page * TABLE_PAGE_SIZE + 1;
111
- const to = Math.min(rows.length, (this.#page + 1) * TABLE_PAGE_SIZE);
112
- const range = rows.length === 0 ? "0 of 0" : `${from}–${to} of ${rows.length}`;
113
- lines.push(truncate(this.#ctx.theme.dim(range), width));
114
- return lines;
115
- }
116
- // ─── the compact fallback (rows as cards, §12.3) ────────────────
117
- #renderCompact(width) {
118
- const rows = this.#rows();
119
- const lines = [];
120
- const cursorRow = Math.min(this.#state.cursor, rows.length - 1);
121
- rows.forEach((row, index) => {
122
- const cursor = index === cursorRow ? "> " : " ";
123
- const inner = width - 2;
124
- const card = cardLines(this.#block.item, row.item, this.#ctx, inner, 0, this.#tick);
125
- lines.push(truncate(cursor + (card[0] ?? ""), width));
126
- for (const line of card.slice(1))
127
- lines.push(truncate(cursor + line, width));
128
- // High-priority columns ride the card as "label: value" lines.
129
- for (const column of this.#block.columns ?? []) {
130
- if (column.priority === "low")
131
- continue;
132
- if (column.cell === "meter")
133
- continue;
134
- const value = cellText(column, row, this.#ctx, width);
135
- if (value === "")
136
- continue;
137
- lines.push(truncate(`${cursor}${bindText(column.label, this.#ctx)}: ${value}`, width));
138
- }
139
- // Low-priority columns sit behind Enter-expand (§12.3).
140
- if (this.#state.expanded.has(row.id)) {
141
- for (const column of this.#block.columns ?? []) {
142
- if (column.priority !== "low")
143
- continue;
144
- const value = cellText(column, row, this.#ctx, width);
145
- if (value === "")
146
- continue;
147
- lines.push(truncate(`${cursor}${bindText(column.label, this.#ctx)}: ${value}`, width));
148
- }
149
- }
150
- });
151
- return lines;
152
- }
153
- handleInput(data) {
154
- if (this.#state.menu?.kind === "sort") {
155
- const columns = sortableColumns(this.#block);
156
- if (data === "\x1b") {
157
- this.#state.menu = null;
158
- return;
159
- }
160
- if (data === "\x1b[B") {
161
- this.#state.menuCursor = Math.min(columns.length - 1, this.#state.menuCursor + 1);
162
- return;
163
- }
164
- if (data === "\x1b[A") {
165
- this.#state.menuCursor = Math.max(0, this.#state.menuCursor - 1);
166
- return;
167
- }
168
- if (data === "\r") {
169
- const column = columns[this.#state.menuCursor];
170
- if (column !== undefined) {
171
- // Sorting the same column again flips the direction.
172
- this.#sort =
173
- this.#sort?.field === column.field && this.#sort.dir === "asc"
174
- ? { field: column.field, dir: "desc" }
175
- : { field: column.field, dir: "asc" };
176
- this.#page = 0;
177
- }
178
- this.#state.menu = null;
179
- }
180
- return;
181
- }
182
- if (data === "s" && sortableColumns(this.#block).length > 0) {
183
- this.#state.menu = { kind: "sort" };
184
- this.#state.menuCursor = 0;
185
- return;
186
- }
187
- if (data === "\x1b[6~") {
188
- const pages = Math.ceil(this.#rows().length / TABLE_PAGE_SIZE);
189
- this.#page = Math.min(Math.max(0, pages - 1), this.#page + 1);
190
- return;
191
- }
192
- if (data === "\x1b[5~") {
193
- this.#page = Math.max(0, this.#page - 1);
194
- return;
195
- }
196
- const rows = this.#rows();
197
- if (data === "\x1b[B") {
198
- this.#state.cursor = Math.min(rows.length - 1, this.#state.cursor + 1);
199
- return;
200
- }
201
- if (data === "\x1b[A") {
202
- this.#state.cursor = Math.max(0, this.#state.cursor - 1);
203
- return;
204
- }
205
- if (data === "\r") {
206
- const row = rows[this.#state.cursor];
207
- if (row === undefined)
208
- return;
209
- // Compact: Enter toggles the low-priority expand; full table:
210
- // Enter activates (onActivate rides the card template's click).
211
- if (this.#state.expanded.has(row.id))
212
- this.#state.expanded.delete(row.id);
213
- else
214
- this.#state.expanded.add(row.id);
215
- }
216
- }
217
- invalidate() {
218
- // View state survives re-renders.
219
- }
220
- }
221
- export { compareItems };
@@ -1,48 +0,0 @@
1
- /**
2
- * The simple content blocks (UI-blocks spec §5.3): `markdown`,
3
- * `fields`, `stat`, `callout`, `diff`, `details`.
4
- *
5
- * - `markdown` rides pi-tui's `Markdown` (the same renderer as chat)
6
- * with the TuiTheme-mapped markdown theme.
7
- * - `fields` aligns labels by display width.
8
- * - `stat` is `label: value unit` plus an optional caption line (its
9
- * `sparkline` becomes a real drawing in Task 2).
10
- * - `callout` leads with the tone marker (colour + the NO_COLOR
11
- * fallback), its body renders as markdown, and its actions render as
12
- * `[1] label` buttons with the confirm flow and the OSC 52 copy.
13
- * - `diff` reuses the interaction diff renderer (blocks/diff.ts).
14
- * - `details` is one collapsed JSON line (the TUI's existing details
15
- * primitive; a later task may add an expandable pretty view).
16
- */
17
- import type { Component } from "@earendil-works/pi-tui";
18
- import type { ActionButton, Block, CalloutBlock, DetailsBlock, DiffBlock, FieldsBlock, MarkdownBlock, StatBlock } from "@sigrun/shared/blocks";
19
- import { type BlockCtx } from "./context.js";
20
- /** `markdown`: the same renderer as chat. */
21
- export declare function renderMarkdown(block: MarkdownBlock, ctx: BlockCtx): Component;
22
- /** `fields`: `label: value` rows, labels aligned by display width. */
23
- export declare function renderFields(block: FieldsBlock, ctx: BlockCtx): Component;
24
- /** `stat`: `label: value unit` plus an optional caption line. */
25
- export declare function renderStat(block: StatBlock, ctx: BlockCtx): Component;
26
- /** `details`: one collapsed JSON line. */
27
- export declare function renderDetails(block: DetailsBlock): Component;
28
- /** `diff`: the interaction diff renderer (blocks/diff.ts). */
29
- export declare function renderDiff(block: DiffBlock): Component;
30
- /** The actions a block offers: read access keeps the view-only kinds
31
- * (§6.5 — copy/open/navigate never edit or answer; twin of the webui's
32
- * `isViewOnly`). */
33
- export declare function visibleActions(actions: readonly ActionButton[] | undefined, ctx: BlockCtx): ActionButton[];
34
- /**
35
- * `callout`: tone-marked title, wrapped markdown body, action buttons.
36
- * `handleInput` picks a button by digit; a `confirm` action gates its
37
- * dispatch behind a `y/N` prompt (anything but `y`/`Y` cancels and
38
- * dispatches nothing).
39
- */
40
- export declare class CalloutComponent implements Component {
41
- #private;
42
- constructor(block: CalloutBlock, ctx: BlockCtx);
43
- render(width: number): string[];
44
- handleInput(data: string): void;
45
- invalidate(): void;
46
- }
47
- /** Render a Task-1 content block by type; the caller routes unknowns. */
48
- export declare function renderContent(block: Block, ctx: BlockCtx): Component | undefined;