@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
|
@@ -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 };
|
|
@@ -1,273 +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 { bindText } from "../context.js";
|
|
19
|
-
import { truncate, widthClassOf } from "../width.js";
|
|
20
|
-
import { cardLines } from "./card.js";
|
|
21
|
-
import { CollectionState, childItems, collectionItems, dispatchActivate, dispatchBulk, dispatchCheck, groupItems, isQuiet, sortItems, } from "./shared.js";
|
|
22
|
-
/** Top-level `form` block fields of a bulk action's form tree. */
|
|
23
|
-
function seamFields(form, ctx) {
|
|
24
|
-
const root = form.root;
|
|
25
|
-
if (root.type !== "form")
|
|
26
|
-
return [];
|
|
27
|
-
const fields = [];
|
|
28
|
-
for (const child of root.children) {
|
|
29
|
-
if (child.type === "section")
|
|
30
|
-
continue;
|
|
31
|
-
fields.push({ name: child.name, label: bindText(child.label, ctx) || child.name });
|
|
32
|
-
}
|
|
33
|
-
return fields;
|
|
34
|
-
}
|
|
35
|
-
/** `list`: groups, nesting, numbering, checkboxes, quiet fold, bulk menu. */
|
|
36
|
-
export class ListComponent {
|
|
37
|
-
#block;
|
|
38
|
-
#ctx;
|
|
39
|
-
#state = new CollectionState();
|
|
40
|
-
#tick = 0;
|
|
41
|
-
constructor(block, ctx) {
|
|
42
|
-
this.#block = block;
|
|
43
|
-
this.#ctx = ctx;
|
|
44
|
-
}
|
|
45
|
-
/** The items in render order (block's initial `sort`). */
|
|
46
|
-
#items() {
|
|
47
|
-
return sortItems(collectionItems(this.#block, this.#ctx), this.#block.sort);
|
|
48
|
-
}
|
|
49
|
-
/** Flatten the render into addressable rows. */
|
|
50
|
-
#rows() {
|
|
51
|
-
const rows = [];
|
|
52
|
-
const push = (row, depth) => {
|
|
53
|
-
rows.push({ kind: "item", row, depth });
|
|
54
|
-
for (const child of childItems(this.#block, row.item))
|
|
55
|
-
push(child, depth + 1);
|
|
56
|
-
};
|
|
57
|
-
for (const group of this.#groups()) {
|
|
58
|
-
if (group.id !== "" && group.title !== "") {
|
|
59
|
-
rows.push({ kind: "group", groupId: group.id });
|
|
60
|
-
}
|
|
61
|
-
if (group.collapsed && !this.#state.unfoldedGroups.has(group.id))
|
|
62
|
-
continue;
|
|
63
|
-
const quiet = group.items.filter((row) => isQuiet(this.#block, this.#ctx, row.item));
|
|
64
|
-
const loud = group.items.filter((row) => !isQuiet(this.#block, this.#ctx, row.item));
|
|
65
|
-
for (const row of loud)
|
|
66
|
-
push(row, 0);
|
|
67
|
-
if (quiet.length > 0) {
|
|
68
|
-
if (this.#state.quietShownFor(group.id)) {
|
|
69
|
-
for (const row of quiet)
|
|
70
|
-
push(row, 0);
|
|
71
|
-
rows.push({ kind: "quiet", groupId: group.id, quietRows: [] }); // the "Show less" row
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
rows.push({ kind: "quiet", groupId: group.id, quietRows: quiet });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return rows;
|
|
79
|
-
}
|
|
80
|
-
#groups() {
|
|
81
|
-
return groupItems(this.#block, this.#items(), this.#ctx);
|
|
82
|
-
}
|
|
83
|
-
render(width) {
|
|
84
|
-
this.#tick += 1;
|
|
85
|
-
const rows = this.#rows();
|
|
86
|
-
if (this.#state.cursor >= rows.length)
|
|
87
|
-
this.#state.cursor = Math.max(0, rows.length - 1);
|
|
88
|
-
const lines = [];
|
|
89
|
-
const unicode = this.#ctx.unicode;
|
|
90
|
-
if (this.#state.menu?.kind === "bulk") {
|
|
91
|
-
lines.push(truncate(`Bulk actions (${this.#state.selected.size} selected)`, width));
|
|
92
|
-
const actions = this.#block.bulkActions ?? [];
|
|
93
|
-
actions.forEach((action, index) => {
|
|
94
|
-
const cursor = index === this.#state.menuCursor ? "> " : " ";
|
|
95
|
-
lines.push(truncate(`${cursor}${bindText(action.label, this.#ctx) || action.id}`, width));
|
|
96
|
-
});
|
|
97
|
-
return lines;
|
|
98
|
-
}
|
|
99
|
-
if (this.#state.menu?.kind === "form") {
|
|
100
|
-
const menu = this.#state.menu;
|
|
101
|
-
const value = this.#state.formBuffer === "" ? "…" : this.#state.formBuffer;
|
|
102
|
-
for (const field of seamFields(menu.form, this.#ctx)) {
|
|
103
|
-
lines.push(truncate(`${field.label}: ${value}`, width));
|
|
104
|
-
}
|
|
105
|
-
lines.push(truncate("Enter to submit · Esc to cancel", width));
|
|
106
|
-
return lines;
|
|
107
|
-
}
|
|
108
|
-
rows.forEach((row, index) => {
|
|
109
|
-
const cursor = index === this.#state.cursor ? "> " : " ";
|
|
110
|
-
if (row.kind === "group") {
|
|
111
|
-
lines.push(truncate(cursor + this.#ctx.theme.bold(`## ${this.#groupTitle(row.groupId ?? "")}`), width));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (row.kind === "quiet") {
|
|
115
|
-
const count = row.quietRows?.length ?? 0;
|
|
116
|
-
const label = this.#state.quietShownFor(row.groupId ?? "") ? "Show less" : `${count} more`;
|
|
117
|
-
lines.push(truncate(cursor + this.#ctx.theme.dim(label), width));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const item = row.row;
|
|
121
|
-
if (item === undefined)
|
|
122
|
-
return;
|
|
123
|
-
let prefix = "";
|
|
124
|
-
const indentStr = " ".repeat(row.depth ?? 0);
|
|
125
|
-
if (this.#block.numbered === true && (row.depth ?? 0) === 0) {
|
|
126
|
-
prefix += `${this.#numberOf(rows, index)}. `;
|
|
127
|
-
}
|
|
128
|
-
if (this.#block.checkable !== undefined) {
|
|
129
|
-
prefix += this.#state.checkedValue(this.#block, item) ? "[x] " : "[ ] ";
|
|
130
|
-
}
|
|
131
|
-
if (this.#block.select !== undefined) {
|
|
132
|
-
const box = this.#state.selected.has(item.id)
|
|
133
|
-
? unicode
|
|
134
|
-
? "(•) "
|
|
135
|
-
: "(*) "
|
|
136
|
-
: "( ) ";
|
|
137
|
-
prefix += box;
|
|
138
|
-
}
|
|
139
|
-
const card = cardLines(this.#block.item, item.item, this.#ctx, width - cursor.length - indentStr.length - prefix.length, 0, this.#tick);
|
|
140
|
-
// The first card line carries the prefixes (title line by slot
|
|
141
|
-
// order — an eyebrow hoists the prefixes onto the eyebrow line,
|
|
142
|
-
// keeping one fixed line layout).
|
|
143
|
-
const [first, ...rest] = card;
|
|
144
|
-
lines.push(truncate(`${cursor}${indentStr}${prefix}${first ?? ""}`, width));
|
|
145
|
-
for (const line of rest) {
|
|
146
|
-
lines.push(truncate(`${cursor}${indentStr}${" ".repeat(prefix.length)}${line}`, width));
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
return lines;
|
|
150
|
-
}
|
|
151
|
-
#groupTitle(id) {
|
|
152
|
-
return this.#groups().find((group) => group.id === id)?.title ?? id;
|
|
153
|
-
}
|
|
154
|
-
/** The running number of a depth-0 item row (numbered lists). */
|
|
155
|
-
#numberOf(rows, at) {
|
|
156
|
-
let number = 0;
|
|
157
|
-
for (let i = 0; i <= at; i++) {
|
|
158
|
-
const row = rows[i];
|
|
159
|
-
if (row?.kind === "item" && row.depth === 0)
|
|
160
|
-
number += 1;
|
|
161
|
-
}
|
|
162
|
-
return number;
|
|
163
|
-
}
|
|
164
|
-
handleInput(data) {
|
|
165
|
-
if (this.#state.menu !== null) {
|
|
166
|
-
this.#menuInput(data);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const rows = this.#rows();
|
|
170
|
-
if (data === "\x1b[B") {
|
|
171
|
-
this.#state.cursor = Math.min(rows.length - 1, this.#state.cursor + 1);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (data === "\x1b[A") {
|
|
175
|
-
this.#state.cursor = Math.max(0, this.#state.cursor - 1);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const row = rows[this.#state.cursor];
|
|
179
|
-
if (row === undefined)
|
|
180
|
-
return;
|
|
181
|
-
if (data === "x" && row.kind === "item" && row.row !== undefined) {
|
|
182
|
-
dispatchCheck(this.#block, this.#ctx, this.#state, row.row);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (data === " " && this.#block.select !== undefined && row.kind === "item" && row.row !== undefined) {
|
|
186
|
-
const id = row.row.id;
|
|
187
|
-
if (this.#state.selected.has(id))
|
|
188
|
-
this.#state.selected.delete(id);
|
|
189
|
-
else
|
|
190
|
-
this.#state.selected.add(id);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (data === "." && this.#block.select === "multi" && this.#state.selected.size > 0) {
|
|
194
|
-
if ((this.#block.bulkActions ?? []).length > 0) {
|
|
195
|
-
this.#state.menu = { kind: "bulk" };
|
|
196
|
-
this.#state.menuCursor = 0;
|
|
197
|
-
}
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (data !== "\r")
|
|
201
|
-
return;
|
|
202
|
-
if (row.kind === "quiet") {
|
|
203
|
-
this.#state.toggleQuiet(row.groupId ?? "");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (row.kind === "group" && row.groupId !== undefined) {
|
|
207
|
-
if (this.#state.unfoldedGroups.has(row.groupId)) {
|
|
208
|
-
this.#state.unfoldedGroups.delete(row.groupId);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
this.#state.unfoldedGroups.add(row.groupId);
|
|
212
|
-
}
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (row.row !== undefined)
|
|
216
|
-
dispatchActivate(this.#block, this.#ctx, row.row);
|
|
217
|
-
}
|
|
218
|
-
#menuInput(data) {
|
|
219
|
-
const menu = this.#state.menu;
|
|
220
|
-
if (menu === null)
|
|
221
|
-
return;
|
|
222
|
-
if (data === "\x1b") {
|
|
223
|
-
this.#state.menu = null;
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
if (menu.kind === "form") {
|
|
227
|
-
if (data === "\r") {
|
|
228
|
-
const values = {};
|
|
229
|
-
for (const field of seamFields(menu.form, this.#ctx)) {
|
|
230
|
-
values[field.name] = this.#state.formBuffer;
|
|
231
|
-
}
|
|
232
|
-
this.#state.formBuffer = "";
|
|
233
|
-
dispatchBulk(this.#ctx, this.#state, menu.onSubmit, values);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (data === "\x7f") {
|
|
237
|
-
this.#state.formBuffer = this.#state.formBuffer.slice(0, -1);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (!data.startsWith("\x1b") && data.length >= 1) {
|
|
241
|
-
this.#state.formBuffer += data;
|
|
242
|
-
}
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const actions = this.#block.bulkActions ?? [];
|
|
246
|
-
if (data === "\x1b[B") {
|
|
247
|
-
this.#state.menuCursor = Math.min(actions.length - 1, this.#state.menuCursor + 1);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
if (data === "\x1b[A") {
|
|
251
|
-
this.#state.menuCursor = Math.max(0, this.#state.menuCursor - 1);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
if (data !== "\r")
|
|
255
|
-
return;
|
|
256
|
-
const action = actions[this.#state.menuCursor];
|
|
257
|
-
if (action === undefined)
|
|
258
|
-
return;
|
|
259
|
-
if (action.form !== undefined) {
|
|
260
|
-
// The Task-5 seam: the form's full rendering lands with the
|
|
261
|
-
// form block; this menu opens the seam form and its values
|
|
262
|
-
// ride the payload (mirroring U1 Task 3's note in rows.tsx).
|
|
263
|
-
this.#state.menu = { kind: "form", form: action.form, onSubmit: action.onSubmit };
|
|
264
|
-
this.#state.formBuffer = "";
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
dispatchBulk(this.#ctx, this.#state, action.onSubmit);
|
|
268
|
-
}
|
|
269
|
-
invalidate() {
|
|
270
|
-
// State survives re-renders (view-only by design).
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
export { widthClassOf };
|
|
@@ -1,67 +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 type { CollectionBlock, JsonObject } from "@sigrun/shared/blocks";
|
|
19
|
-
import { type CollectionItem, type ItemGroup } from "./shared.js";
|
|
20
|
-
import type { BlockCtx } from "../context.js";
|
|
21
|
-
/** A placement: a group plus the index within its VISIBLE items. */
|
|
22
|
-
export interface Placement {
|
|
23
|
-
readonly group: string;
|
|
24
|
-
readonly index: number;
|
|
25
|
-
}
|
|
26
|
-
/** The keyboard-move session (the picked-up item and its placement). */
|
|
27
|
-
export interface MoveState {
|
|
28
|
-
readonly itemId: string;
|
|
29
|
-
readonly from: Placement;
|
|
30
|
-
readonly at: Placement;
|
|
31
|
-
}
|
|
32
|
-
/** The visible items of a group (sorted, loud items — quiet items are
|
|
33
|
-
* folded behind the fold row and not placement targets). */
|
|
34
|
-
export declare function visibleLaneItems(block: CollectionBlock, ctx: BlockCtx, group: ItemGroup): CollectionItem[];
|
|
35
|
-
/** Every group with its visible items, in group order. */
|
|
36
|
-
export declare function lanes(block: CollectionBlock, ctx: BlockCtx): {
|
|
37
|
-
groups: ItemGroup[];
|
|
38
|
-
visible: Map<string, CollectionItem[]>;
|
|
39
|
-
};
|
|
40
|
-
/** An item's current placement across every group. */
|
|
41
|
-
export declare function placementOf(groups: readonly ItemGroup[], visible: ReadonlyMap<string, CollectionItem[]>, itemId: string): Placement | undefined;
|
|
42
|
-
/** The move payload (spec §6.3 step 2; U1's pointer-drop shape exactly). */
|
|
43
|
-
export declare function movePayload(move: {
|
|
44
|
-
itemId: string;
|
|
45
|
-
from: Placement;
|
|
46
|
-
to: Placement;
|
|
47
|
-
}): JsonObject;
|
|
48
|
-
/** Move the placement left/right across groups (the index clamps to
|
|
49
|
-
* the target lane's size; out-of-range directions are inert). */
|
|
50
|
-
export declare function shiftAcross(move: MoveState, groups: readonly ItemGroup[], visible: ReadonlyMap<string, CollectionItem[]>, delta: 1 | -1): MoveState;
|
|
51
|
-
/** Move the placement up/down within its group (clamped). */
|
|
52
|
-
export declare function shiftWithin(move: MoveState, visible: ReadonlyMap<string, CollectionItem[]>, delta: 1 | -1): MoveState;
|
|
53
|
-
/**
|
|
54
|
-
* Dispatch the drop: nothing when the placement equals the current
|
|
55
|
-
* position; otherwise the `move` action with the U1 payload. The
|
|
56
|
-
* optimistic flip lives in SurfaceState's overlay — the component
|
|
57
|
-
* itself keeps no per-item move override.
|
|
58
|
-
*/
|
|
59
|
-
export declare function dispatchMove(block: CollectionBlock, ctx: BlockCtx, move: MoveState): void;
|
|
60
|
-
/** The per-display move session state (held in the component). */
|
|
61
|
-
export declare class MoveSession {
|
|
62
|
-
held: MoveState | undefined;
|
|
63
|
-
/** Pick up the item (undefined placement = no pickup). */
|
|
64
|
-
pickUp(itemId: string, from: Placement): void;
|
|
65
|
-
/** Cancel (Esc): drop the session without dispatching. */
|
|
66
|
-
cancel(): void;
|
|
67
|
-
}
|