@sigrun/tui 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/dist/actions.d.ts +0 -119
- package/dist/actions.js +0 -12
- package/dist/api.d.ts +0 -152
- package/dist/api.js +0 -189
- package/dist/blocks/braille.d.ts +0 -41
- package/dist/blocks/braille.js +0 -131
- package/dist/blocks/chart.d.ts +0 -20
- package/dist/blocks/chart.js +0 -397
- package/dist/blocks/collection/calendar.d.ts +0 -28
- package/dist/blocks/collection/calendar.js +0 -232
- package/dist/blocks/collection/card.d.ts +0 -36
- package/dist/blocks/collection/card.js +0 -128
- package/dist/blocks/collection/collection.d.ts +0 -9
- package/dist/blocks/collection/collection.js +0 -26
- package/dist/blocks/collection/grid.d.ts +0 -18
- package/dist/blocks/collection/grid.js +0 -72
- package/dist/blocks/collection/lanes.d.ts +0 -32
- package/dist/blocks/collection/lanes.js +0 -320
- package/dist/blocks/collection/list.d.ts +0 -30
- package/dist/blocks/collection/list.js +0 -273
- package/dist/blocks/collection/move.d.ts +0 -67
- package/dist/blocks/collection/move.js +0 -96
- package/dist/blocks/collection/shared.d.ts +0 -87
- package/dist/blocks/collection/shared.js +0 -177
- package/dist/blocks/collection/table.d.ts +0 -26
- package/dist/blocks/collection/table.js +0 -221
- package/dist/blocks/content.d.ts +0 -48
- package/dist/blocks/content.js +0 -253
- package/dist/blocks/context.d.ts +0 -96
- package/dist/blocks/context.js +0 -65
- package/dist/blocks/diff.d.ts +0 -21
- package/dist/blocks/diff.js +0 -25
- package/dist/blocks/ext-transport.d.ts +0 -70
- package/dist/blocks/ext-transport.js +0 -165
- package/dist/blocks/form/choice.d.ts +0 -54
- package/dist/blocks/form/choice.js +0 -175
- package/dist/blocks/form/fields.d.ts +0 -56
- package/dist/blocks/form/fields.js +0 -136
- package/dist/blocks/form/file.d.ts +0 -61
- package/dist/blocks/form/file.js +0 -121
- package/dist/blocks/form/form.d.ts +0 -53
- package/dist/blocks/form/form.js +0 -703
- package/dist/blocks/form/list.d.ts +0 -37
- package/dist/blocks/form/list.js +0 -70
- package/dist/blocks/form/values.d.ts +0 -20
- package/dist/blocks/form/values.js +0 -104
- package/dist/blocks/icons.d.ts +0 -14
- package/dist/blocks/icons.js +0 -74
- package/dist/blocks/image.d.ts +0 -39
- package/dist/blocks/image.js +0 -82
- package/dist/blocks/layout.d.ts +0 -31
- package/dist/blocks/layout.js +0 -155
- package/dist/blocks/log.d.ts +0 -24
- package/dist/blocks/log.js +0 -111
- package/dist/blocks/meter.d.ts +0 -16
- package/dist/blocks/meter.js +0 -50
- package/dist/blocks/page.d.ts +0 -54
- package/dist/blocks/page.js +0 -355
- package/dist/blocks/render.d.ts +0 -31
- package/dist/blocks/render.js +0 -145
- package/dist/blocks/surface-view.d.ts +0 -33
- package/dist/blocks/surface-view.js +0 -86
- package/dist/blocks/surface.d.ts +0 -75
- package/dist/blocks/surface.js +0 -468
- package/dist/blocks/tones.d.ts +0 -23
- package/dist/blocks/tones.js +0 -63
- package/dist/blocks/treeview.d.ts +0 -22
- package/dist/blocks/treeview.js +0 -121
- package/dist/blocks/width.d.ts +0 -32
- package/dist/blocks/width.js +0 -70
- package/dist/clipboard.d.ts +0 -20
- package/dist/clipboard.js +0 -19
- package/dist/editor-launch.d.ts +0 -34
- package/dist/editor-launch.js +0 -70
- package/dist/keymaps/emacs.d.ts +0 -18
- package/dist/keymaps/emacs.js +0 -172
- package/dist/keymaps/standard.d.ts +0 -17
- package/dist/keymaps/standard.js +0 -175
- package/dist/keymaps/types.d.ts +0 -43
- package/dist/keymaps/types.js +0 -28
- package/dist/keymaps/vim.d.ts +0 -48
- package/dist/keymaps/vim.js +0 -291
- package/dist/launch.d.ts +0 -149
- package/dist/launch.js +0 -446
- package/dist/sse.d.ts +0 -109
- package/dist/sse.js +0 -401
- package/dist/theme.d.ts +0 -51
- package/dist/theme.js +0 -119
- package/dist/ui/ambient.d.ts +0 -81
- package/dist/ui/ambient.js +0 -301
- package/dist/ui/app.d.ts +0 -245
- package/dist/ui/app.js +0 -1325
- package/dist/ui/composer.d.ts +0 -66
- package/dist/ui/composer.js +0 -125
- package/dist/ui/conversation-pane.d.ts +0 -123
- package/dist/ui/conversation-pane.js +0 -463
- package/dist/ui/help.d.ts +0 -15
- package/dist/ui/help.js +0 -80
- package/dist/ui/interactions.d.ts +0 -90
- package/dist/ui/interactions.js +0 -330
- package/dist/ui/picker.d.ts +0 -26
- package/dist/ui/picker.js +0 -130
- package/dist/ui/sidebar.d.ts +0 -99
- package/dist/ui/sidebar.js +0 -269
- package/dist/ui/status-bar.d.ts +0 -77
- package/dist/ui/status-bar.js +0 -146
package/dist/blocks/surface.js
DELETED
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SurfaceState (UI-blocks U2 Task 4, spec §6.3): the confirmed-data plus
|
|
3
|
-
* optimistic-overlay model every TUI surface renders through — the TUI
|
|
4
|
-
* port of U1's `useSurface.ts`, with EXACTLY the same overlay rules:
|
|
5
|
-
*
|
|
6
|
-
* - **Confirmed data** changes ONLY from server data or ops (load,
|
|
7
|
-
* fetch, accepted actions carrying ops).
|
|
8
|
-
* - **The overlay** is a list of pending moves `{actionId, payload,
|
|
9
|
-
* target, untilRev?}`, keyed by `actionId`; the rendered `view` is
|
|
10
|
-
* confirmed data with the overlay applied on top.
|
|
11
|
-
* - `{ok: true, rev, ops}` → apply the ops to confirmed, drop the entry.
|
|
12
|
-
* - `{ok: true, rev}` (no ops) → keep the entry with `untilRev = rev`
|
|
13
|
-
* until a fetch at ≥ that rev arrives, then drop it — the item stays
|
|
14
|
-
* where the user dropped it with no snap-back flicker.
|
|
15
|
-
* - `{ok: false}` or the 10 s timeout → drop the entry and surface a
|
|
16
|
-
* NOTICE. U1 toasts; the TUI's notice is a line the app flashes in
|
|
17
|
-
* the status bar — `SurfaceState` collects `notices` and the host
|
|
18
|
-
* drains them through `consumeNotices` (the status-bar wiring is the
|
|
19
|
-
* app's seam, Task 6's surface host).
|
|
20
|
-
* - A `rev` gap (ops land behind the announced rev) or a `DataOpError`
|
|
21
|
-
* applying ops → a full refetch.
|
|
22
|
-
* - `access: "none"` → the not-found state (§6.5); a `treeRev` change
|
|
23
|
-
* → a full reload.
|
|
24
|
-
*
|
|
25
|
-
* The overlay locates a move in the data by scanning the loaded tree
|
|
26
|
-
* for the collection whose `onMove` handler name matches the action —
|
|
27
|
-
* identical to U1 (a move against an unknown handler still goes to the
|
|
28
|
-
* transport, unoverlaid).
|
|
29
|
-
*/
|
|
30
|
-
import { DataOpError, applyOps, resolvePointer } from "@sigrun/shared/blocks";
|
|
31
|
-
/** An action has 10 s to answer before it counts as a reject (§6.3). */
|
|
32
|
-
export const ACT_TIMEOUT_MS = 10_000;
|
|
33
|
-
/** The notice cap: pending notices drop oldest-first past 50 (a flood
|
|
34
|
-
* of failures must not grow the list without bound; the host drains
|
|
35
|
-
* them through `consumeNotices`). */
|
|
36
|
-
export const NOTICE_CAP = 50;
|
|
37
|
-
/** Recursive block search (children/main/sidebar/detail, plus the
|
|
38
|
-
* columns/grid `{block}` wrapper). */
|
|
39
|
-
function findCollection(block, name) {
|
|
40
|
-
if (typeof block !== "object" || block === null || Array.isArray(block)) {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
const record = block;
|
|
44
|
-
if (record.type === "collection") {
|
|
45
|
-
const collection = block;
|
|
46
|
-
if (collection.onMove === name)
|
|
47
|
-
return collection;
|
|
48
|
-
}
|
|
49
|
-
for (const key of ["children", "main", "sidebar", "detail"]) {
|
|
50
|
-
const nested = record[key];
|
|
51
|
-
if (Array.isArray(nested)) {
|
|
52
|
-
for (const child of nested) {
|
|
53
|
-
const wrapped = typeof child === "object" && child !== null && !Array.isArray(child)
|
|
54
|
-
? child.block
|
|
55
|
-
: undefined;
|
|
56
|
-
const hit = findCollection(child, name) ?? findCollection(wrapped, name);
|
|
57
|
-
if (hit !== undefined)
|
|
58
|
-
return hit;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
/** The move target for a handler name (undefined when no collection
|
|
65
|
-
* declares it — the action still goes to the transport, unoverlaid). */
|
|
66
|
-
function findMoveTarget(tree, name) {
|
|
67
|
-
if (tree === undefined)
|
|
68
|
-
return undefined;
|
|
69
|
-
const found = findCollection(tree.root, name);
|
|
70
|
-
if (found === undefined)
|
|
71
|
-
return undefined;
|
|
72
|
-
if (typeof found.items.$ !== "string")
|
|
73
|
-
return undefined;
|
|
74
|
-
return {
|
|
75
|
-
itemsPath: found.items.$,
|
|
76
|
-
groupBy: found.groupBy,
|
|
77
|
-
timeStart: found.time?.start,
|
|
78
|
-
timeEnd: found.time?.end,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/** Read a move endpoint from a payload value. */
|
|
82
|
-
function spot(value) {
|
|
83
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
const record = value;
|
|
87
|
-
return {
|
|
88
|
-
group: typeof record.group === "string" ? record.group : undefined,
|
|
89
|
-
index: typeof record.index === "number" ? record.index : undefined,
|
|
90
|
-
start: typeof record.start === "string" ? record.start : undefined,
|
|
91
|
-
end: typeof record.end === "string" ? record.end : undefined,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
/** Write the items array back at its pointer, immutably along the path. */
|
|
95
|
-
function writeItems(data, itemsPath, items) {
|
|
96
|
-
const tokens = itemsPath
|
|
97
|
-
.replace(/^\//, "")
|
|
98
|
-
.split("/")
|
|
99
|
-
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
100
|
-
if (tokens.length === 0)
|
|
101
|
-
return data;
|
|
102
|
-
const rebuild = (node, depth) => {
|
|
103
|
-
if (depth === tokens.length)
|
|
104
|
-
return [...items];
|
|
105
|
-
if (Array.isArray(node)) {
|
|
106
|
-
const index = Number(tokens[depth]);
|
|
107
|
-
const copy = [...node];
|
|
108
|
-
copy[index] = rebuild(copy[index], depth + 1);
|
|
109
|
-
return copy;
|
|
110
|
-
}
|
|
111
|
-
if (typeof node === "object" && node !== null) {
|
|
112
|
-
const record = node;
|
|
113
|
-
const key = tokens[depth];
|
|
114
|
-
return { ...record, [key]: rebuild(record[key], depth + 1) };
|
|
115
|
-
}
|
|
116
|
-
return node;
|
|
117
|
-
};
|
|
118
|
-
return rebuild(data, 0);
|
|
119
|
-
}
|
|
120
|
-
/** Apply one overlay entry to `data` (unknown shapes leave the data
|
|
121
|
-
* untouched so a stale entry never corrupts the render). */
|
|
122
|
-
function applyMoveOverlay(data, entry) {
|
|
123
|
-
const target = entry.target;
|
|
124
|
-
if (target === undefined)
|
|
125
|
-
return data;
|
|
126
|
-
const payload = entry.payload;
|
|
127
|
-
const itemId = typeof payload.itemId === "string" ? payload.itemId : undefined;
|
|
128
|
-
const to = spot(payload.to);
|
|
129
|
-
if (itemId === undefined || to === undefined)
|
|
130
|
-
return data;
|
|
131
|
-
const items = resolvePointer(data, target.itemsPath);
|
|
132
|
-
if (!Array.isArray(items))
|
|
133
|
-
return data;
|
|
134
|
-
const fromIndex = items.findIndex((item) => typeof item === "object" &&
|
|
135
|
-
item !== null &&
|
|
136
|
-
!Array.isArray(item) &&
|
|
137
|
-
item.id === itemId);
|
|
138
|
-
if (fromIndex < 0)
|
|
139
|
-
return data;
|
|
140
|
-
const item = items[fromIndex];
|
|
141
|
-
const next = [...items];
|
|
142
|
-
next.splice(fromIndex, 1);
|
|
143
|
-
// A time move (calendar): set start/end, keep the item's position.
|
|
144
|
-
if (to.start !== undefined && target.timeStart !== undefined) {
|
|
145
|
-
const moved = { ...item, [target.timeStart]: to.start };
|
|
146
|
-
if (to.end !== undefined && target.timeEnd !== undefined) {
|
|
147
|
-
moved[target.timeEnd] = to.end;
|
|
148
|
-
}
|
|
149
|
-
next.splice(fromIndex, 0, moved);
|
|
150
|
-
return writeItems(data, target.itemsPath, next);
|
|
151
|
-
}
|
|
152
|
-
// A group/index move (lanes, list reorder).
|
|
153
|
-
if (to.group === undefined || to.index === undefined)
|
|
154
|
-
return data;
|
|
155
|
-
const moved = target.groupBy === undefined ? item : { ...item, [target.groupBy]: to.group };
|
|
156
|
-
const lanePositions = next
|
|
157
|
-
.map((entry, index) => ({ entry, index }))
|
|
158
|
-
.filter(({ entry }) => target.groupBy !== undefined &&
|
|
159
|
-
typeof entry === "object" &&
|
|
160
|
-
entry !== null &&
|
|
161
|
-
!Array.isArray(entry) &&
|
|
162
|
-
entry[target.groupBy] === to.group)
|
|
163
|
-
.map(({ index }) => index);
|
|
164
|
-
const at = to.index < lanePositions.length ? lanePositions[to.index] ?? next.length : next.length;
|
|
165
|
-
next.splice(at, 0, moved);
|
|
166
|
-
return writeItems(data, target.itemsPath, next);
|
|
167
|
-
}
|
|
168
|
-
/** The surface state container (class shape — the TUI has no hook layer). */
|
|
169
|
-
export class SurfaceState {
|
|
170
|
-
#transport;
|
|
171
|
-
#status = "loading";
|
|
172
|
-
#tree;
|
|
173
|
-
#confirmed = {};
|
|
174
|
-
#rev = 0;
|
|
175
|
-
#treeRev = 0;
|
|
176
|
-
#access = "read";
|
|
177
|
-
#overlay = [];
|
|
178
|
-
#notices = [];
|
|
179
|
-
/** Server-side field errors from the latest rejected action
|
|
180
|
-
* (`{error: {field}}`, spec §5.6), keyed by field name; a success
|
|
181
|
-
* clears them. */
|
|
182
|
-
#serverErrors = {};
|
|
183
|
-
/** The channel-owned `/view/` branch (spec §6.2): live forms bound
|
|
184
|
-
* under `/view/` write it through {@link SurfaceState.setView}; it
|
|
185
|
-
* never reaches the transport. */
|
|
186
|
-
#viewBranch = {};
|
|
187
|
-
#actionSeq = 0;
|
|
188
|
-
/** The highest rev a change event announced (gap detect). */
|
|
189
|
-
#announcedRev = 0;
|
|
190
|
-
#timers = new Map();
|
|
191
|
-
#listeners = new Set();
|
|
192
|
-
#unsubscribe;
|
|
193
|
-
constructor(transport) {
|
|
194
|
-
this.#transport = transport;
|
|
195
|
-
}
|
|
196
|
-
/** The current snapshot: confirmed data with the overlay applied. */
|
|
197
|
-
get view() {
|
|
198
|
-
let data = this.#confirmed;
|
|
199
|
-
for (const entry of this.#overlay) {
|
|
200
|
-
data = applyMoveOverlay(data, entry);
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
status: this.#status,
|
|
204
|
-
tree: this.#tree,
|
|
205
|
-
data,
|
|
206
|
-
rev: this.#rev,
|
|
207
|
-
treeRev: this.#treeRev,
|
|
208
|
-
access: this.#access,
|
|
209
|
-
notices: this.#notices,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
/** Subscribe to state changes (returns the unsubscribe). */
|
|
213
|
-
onChange(cb) {
|
|
214
|
-
this.#listeners.add(cb);
|
|
215
|
-
return () => {
|
|
216
|
-
this.#listeners.delete(cb);
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
#emit() {
|
|
220
|
-
for (const listener of [...this.#listeners])
|
|
221
|
-
listener();
|
|
222
|
-
}
|
|
223
|
-
#notice(message) {
|
|
224
|
-
this.#notices = [...this.#notices, message].slice(-NOTICE_CAP);
|
|
225
|
-
this.#emit();
|
|
226
|
-
}
|
|
227
|
-
/** Drain the pending notices (the host flashes them in the status bar). */
|
|
228
|
-
consumeNotices() {
|
|
229
|
-
const drained = this.#notices;
|
|
230
|
-
this.#notices = [];
|
|
231
|
-
return drained;
|
|
232
|
-
}
|
|
233
|
-
/** The channel-owned `/view/` branch (spec §6.2) — a BlockCtx's
|
|
234
|
-
* `view.values` reads this object. */
|
|
235
|
-
get viewValues() {
|
|
236
|
-
return this.#viewBranch;
|
|
237
|
-
}
|
|
238
|
-
/** The server field errors from the latest rejected action. */
|
|
239
|
-
get serverErrors() {
|
|
240
|
-
return this.#serverErrors;
|
|
241
|
-
}
|
|
242
|
-
/** Write a pointer under the `/view/` branch (the form's live
|
|
243
|
-
* `/view/`-bound flush lands here; the write never dispatches). */
|
|
244
|
-
setView(path, value) {
|
|
245
|
-
const tokens = path
|
|
246
|
-
.replace(/^\/+/, "")
|
|
247
|
-
.split("/")
|
|
248
|
-
.filter((token) => token !== "")
|
|
249
|
-
.map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
250
|
-
const write = (node, depth) => {
|
|
251
|
-
if (depth >= tokens.length) {
|
|
252
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
253
|
-
? { ...value }
|
|
254
|
-
: node;
|
|
255
|
-
}
|
|
256
|
-
const key = tokens[depth];
|
|
257
|
-
if (depth === tokens.length - 1)
|
|
258
|
-
return { ...node, [key]: value };
|
|
259
|
-
const child = node[key];
|
|
260
|
-
const childObject = typeof child === "object" && child !== null && !Array.isArray(child)
|
|
261
|
-
? child
|
|
262
|
-
: {};
|
|
263
|
-
return { ...node, [key]: write(childObject, depth + 1) };
|
|
264
|
-
};
|
|
265
|
-
if (tokens.length === 0) {
|
|
266
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
267
|
-
this.#viewBranch = { ...value };
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
this.#viewBranch = write(this.#viewBranch, 0);
|
|
272
|
-
}
|
|
273
|
-
this.#emit();
|
|
274
|
-
}
|
|
275
|
-
/** Reload after an error (SurfaceView's `r` key). */
|
|
276
|
-
async retry() {
|
|
277
|
-
this.#status = "loading";
|
|
278
|
-
this.#emit();
|
|
279
|
-
await this.#doLoad();
|
|
280
|
-
}
|
|
281
|
-
/** Load the tree and initial data; subscribe to change events. */
|
|
282
|
-
async load() {
|
|
283
|
-
await this.#doLoad();
|
|
284
|
-
if (this.#unsubscribe === undefined) {
|
|
285
|
-
this.#unsubscribe = this.#transport.onChanged((event) => {
|
|
286
|
-
this.#announcedRev = Math.max(this.#announcedRev, event.rev);
|
|
287
|
-
if (event.treeRev !== this.#treeRev) {
|
|
288
|
-
void this.#doLoad();
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
void (async () => {
|
|
292
|
-
try {
|
|
293
|
-
this.#applyFetch(await this.#transport.fetchData(this.#rev));
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
// Keep confirmed; the next event retries (§7.3).
|
|
297
|
-
}
|
|
298
|
-
})();
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
/** Stop listening and cancel every pending action timer. */
|
|
303
|
-
dispose() {
|
|
304
|
-
this.#unsubscribe?.();
|
|
305
|
-
this.#unsubscribe = undefined;
|
|
306
|
-
for (const timer of this.#timers.values())
|
|
307
|
-
clearTimeout(timer);
|
|
308
|
-
this.#timers.clear();
|
|
309
|
-
}
|
|
310
|
-
async #doLoad() {
|
|
311
|
-
try {
|
|
312
|
-
const result = await this.#transport.load();
|
|
313
|
-
if (result.access === "none") {
|
|
314
|
-
this.#status = "notFound";
|
|
315
|
-
this.#access = "none";
|
|
316
|
-
this.#emit();
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
this.#announcedRev = result.rev;
|
|
320
|
-
this.#status = "ready";
|
|
321
|
-
this.#tree = result.tree;
|
|
322
|
-
this.#confirmed = result.data;
|
|
323
|
-
this.#rev = result.rev;
|
|
324
|
-
this.#treeRev = result.treeRev;
|
|
325
|
-
this.#access = result.access;
|
|
326
|
-
this.#overlay = [];
|
|
327
|
-
this.#emit();
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
this.#status = "error";
|
|
331
|
-
this.#emit();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
#dropEntry(actionId) {
|
|
335
|
-
const timer = this.#timers.get(actionId);
|
|
336
|
-
if (timer !== undefined) {
|
|
337
|
-
clearTimeout(timer);
|
|
338
|
-
this.#timers.delete(actionId);
|
|
339
|
-
}
|
|
340
|
-
this.#overlay = this.#overlay.filter((entry) => entry.actionId !== actionId);
|
|
341
|
-
this.#emit();
|
|
342
|
-
}
|
|
343
|
-
#applyFetch(result) {
|
|
344
|
-
if (result.treeRev !== this.#treeRev) {
|
|
345
|
-
void this.#doLoad();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (result.data !== undefined) {
|
|
349
|
-
this.#confirmed = result.data;
|
|
350
|
-
this.#rev = result.rev;
|
|
351
|
-
this.#overlay = this.#overlay.filter((entry) => entry.untilRev === undefined || result.rev < entry.untilRev);
|
|
352
|
-
this.#emit();
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
if (result.ops !== undefined) {
|
|
356
|
-
// Gap: the ops land behind the rev the event announced.
|
|
357
|
-
if (result.rev < this.#announcedRev) {
|
|
358
|
-
void this.#fullRefetch();
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
try {
|
|
362
|
-
this.#confirmed = applyOps(this.#confirmed, result.ops);
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
if (error instanceof DataOpError) {
|
|
366
|
-
void this.#fullRefetch();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
371
|
-
this.#rev = result.rev;
|
|
372
|
-
this.#overlay = this.#overlay.filter((entry) => entry.untilRev === undefined || result.rev < entry.untilRev);
|
|
373
|
-
this.#emit();
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
async #fullRefetch() {
|
|
377
|
-
try {
|
|
378
|
-
this.#applyFetch(await this.#transport.fetchData());
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
// A failed refetch keeps the last confirmed data; the next
|
|
382
|
-
// event or keepalive retries (§7.3).
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/** Dispatch an action to the surface; a move joins the overlay first. */
|
|
386
|
-
dispatch(action, payload) {
|
|
387
|
-
const payloadObject = typeof payload === "object" && payload !== null && !Array.isArray(payload)
|
|
388
|
-
? payload
|
|
389
|
-
: {};
|
|
390
|
-
const actionId = `act-${(this.#actionSeq += 1)}`;
|
|
391
|
-
const name = "name" in action ? action.name : action.type;
|
|
392
|
-
const isMove = action.type === "move";
|
|
393
|
-
if (isMove) {
|
|
394
|
-
const target = findMoveTarget(this.#tree, name);
|
|
395
|
-
this.#overlay = [...this.#overlay, { actionId, payload: payloadObject, target }];
|
|
396
|
-
this.#timers.set(actionId, setTimeout(() => {
|
|
397
|
-
this.#dropEntry(actionId);
|
|
398
|
-
this.#notice("The action timed out");
|
|
399
|
-
}, ACT_TIMEOUT_MS));
|
|
400
|
-
this.#emit();
|
|
401
|
-
}
|
|
402
|
-
void this.#transport
|
|
403
|
-
.act({ action: name, payload, actionId })
|
|
404
|
-
.then((result) => {
|
|
405
|
-
// A move whose entry is already gone (the 10 s timeout fired)
|
|
406
|
-
// is STALE: no ops, no notice — the user already saw it fail.
|
|
407
|
-
if (isMove &&
|
|
408
|
-
!this.#overlay.some((entry) => entry.actionId === actionId)) {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (!result.ok) {
|
|
412
|
-
if (isMove)
|
|
413
|
-
this.#dropEntry(actionId);
|
|
414
|
-
// A rejected action's `{error: {field}}` surfaces under
|
|
415
|
-
// that field (spec §5.6); an unfielded rejection clears
|
|
416
|
-
// any previous field error.
|
|
417
|
-
this.#serverErrors =
|
|
418
|
-
result.error.field === undefined
|
|
419
|
-
? {}
|
|
420
|
-
: { [result.error.field]: result.error.message };
|
|
421
|
-
this.#notice(result.error.message);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
this.#serverErrors = {};
|
|
425
|
-
if (result.toast !== undefined)
|
|
426
|
-
this.#notice(result.toast);
|
|
427
|
-
if (!isMove)
|
|
428
|
-
return;
|
|
429
|
-
if (result.ops !== undefined) {
|
|
430
|
-
try {
|
|
431
|
-
this.#confirmed = applyOps(this.#confirmed, result.ops);
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
if (error instanceof DataOpError) {
|
|
435
|
-
void this.#fullRefetch();
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
this.#rev = result.rev;
|
|
441
|
-
const timer = this.#timers.get(actionId);
|
|
442
|
-
if (timer !== undefined) {
|
|
443
|
-
clearTimeout(timer);
|
|
444
|
-
this.#timers.delete(actionId);
|
|
445
|
-
}
|
|
446
|
-
this.#overlay = this.#overlay.filter((entry) => entry.actionId !== actionId);
|
|
447
|
-
this.#emit();
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
// Accept without ops: hold the entry until confirmed reaches
|
|
451
|
-
// the accepted rev (no snap-back flicker, §6.3).
|
|
452
|
-
const timer = this.#timers.get(actionId);
|
|
453
|
-
if (timer !== undefined) {
|
|
454
|
-
clearTimeout(timer);
|
|
455
|
-
this.#timers.delete(actionId);
|
|
456
|
-
}
|
|
457
|
-
this.#rev = Math.max(this.#rev, result.rev);
|
|
458
|
-
this.#overlay = this.#overlay.map((entry) => entry.actionId === actionId ? { ...entry, untilRev: result.rev } : entry);
|
|
459
|
-
this.#emit();
|
|
460
|
-
}
|
|
461
|
-
})
|
|
462
|
-
.catch(() => {
|
|
463
|
-
if (isMove)
|
|
464
|
-
this.#dropEntry(actionId);
|
|
465
|
-
this.#notice("The action failed");
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
}
|
package/dist/blocks/tones.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tone → terminal styling (UI-blocks spec §10.1): extensions set
|
|
3
|
-
* MEANING (`neutral | accent | good | warn | bad | info`); this module
|
|
4
|
-
* is the one place a tone becomes visual treatment, mapped through
|
|
5
|
-
* {@link TuiTheme} tokens only — never raw SGR in block code.
|
|
6
|
-
*
|
|
7
|
-
* The TUI's tone treatment is a left marker column plus colour: the
|
|
8
|
-
* marker makes tone legible without colour (NO_COLOR, Review Focus 5)
|
|
9
|
-
* and doubles as the accent stripe `card.tone` calls for. On a
|
|
10
|
-
* terminal that cannot render Unicode (`ctx.unicode === false`) the
|
|
11
|
-
* markers take their ASCII forms (§10.2) so tone stays legible there
|
|
12
|
-
* too.
|
|
13
|
-
*/
|
|
14
|
-
import type { Tone } from "@sigrun/shared/blocks";
|
|
15
|
-
import type { TuiTheme } from "../theme.js";
|
|
16
|
-
/** Style `text` in the tone's colour. */
|
|
17
|
-
export declare function toneFg(theme: TuiTheme, tone: Tone): (s: string) => string;
|
|
18
|
-
/**
|
|
19
|
-
* The tone's one-character marker (shown in addition to colour);
|
|
20
|
-
* `ascii` selects the no-Unicode form (§10.2 — callers pass
|
|
21
|
-
* `!ctx.unicode`).
|
|
22
|
-
*/
|
|
23
|
-
export declare function toneMarker(tone: Tone, ascii: boolean): string;
|
package/dist/blocks/tones.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tone → terminal styling (UI-blocks spec §10.1): extensions set
|
|
3
|
-
* MEANING (`neutral | accent | good | warn | bad | info`); this module
|
|
4
|
-
* is the one place a tone becomes visual treatment, mapped through
|
|
5
|
-
* {@link TuiTheme} tokens only — never raw SGR in block code.
|
|
6
|
-
*
|
|
7
|
-
* The TUI's tone treatment is a left marker column plus colour: the
|
|
8
|
-
* marker makes tone legible without colour (NO_COLOR, Review Focus 5)
|
|
9
|
-
* and doubles as the accent stripe `card.tone` calls for. On a
|
|
10
|
-
* terminal that cannot render Unicode (`ctx.unicode === false`) the
|
|
11
|
-
* markers take their ASCII forms (§10.2) so tone stays legible there
|
|
12
|
-
* too.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* The palette token each tone paints with (the TUI's tone table,
|
|
16
|
-
* twin of the webui's `TONE_VARS`: `info` is a quiet emphasis, not a
|
|
17
|
-
* state colour, so it maps to `muted` exactly as the webui maps it to
|
|
18
|
-
* `--muted`).
|
|
19
|
-
*/
|
|
20
|
-
const TONE_TOKEN = {
|
|
21
|
-
neutral: "text",
|
|
22
|
-
accent: "accent",
|
|
23
|
-
good: "ok",
|
|
24
|
-
warn: "warn",
|
|
25
|
-
bad: "bad",
|
|
26
|
-
info: "muted",
|
|
27
|
-
};
|
|
28
|
-
/** The colourless marker: tone legible with NO_COLOR set (Focus 5). */
|
|
29
|
-
const TONE_MARKER = {
|
|
30
|
-
neutral: "•",
|
|
31
|
-
accent: "◆",
|
|
32
|
-
good: "✓",
|
|
33
|
-
warn: "▲",
|
|
34
|
-
bad: "!",
|
|
35
|
-
info: "·",
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* The marker on a no-Unicode terminal (§10.2's ASCII path): the same
|
|
39
|
-
* shapes in printable ASCII — `good` is `v`, matching `icons.ts`'s
|
|
40
|
-
* `check` fallback, so a tone and its icon degrade alike. `bad` is
|
|
41
|
-
* already ASCII.
|
|
42
|
-
*/
|
|
43
|
-
const TONE_MARKER_ASCII = {
|
|
44
|
-
neutral: "-",
|
|
45
|
-
accent: "*",
|
|
46
|
-
good: "v",
|
|
47
|
-
warn: "^",
|
|
48
|
-
bad: "!",
|
|
49
|
-
info: ".",
|
|
50
|
-
};
|
|
51
|
-
/** Style `text` in the tone's colour. */
|
|
52
|
-
export function toneFg(theme, tone) {
|
|
53
|
-
return theme.fg(TONE_TOKEN[tone] ?? "text");
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* The tone's one-character marker (shown in addition to colour);
|
|
57
|
-
* `ascii` selects the no-Unicode form (§10.2 — callers pass
|
|
58
|
-
* `!ctx.unicode`).
|
|
59
|
-
*/
|
|
60
|
-
export function toneMarker(tone, ascii) {
|
|
61
|
-
const table = ascii ? TONE_MARKER_ASCII : TONE_MARKER;
|
|
62
|
-
return table[tone] ?? (ascii ? "-" : "•");
|
|
63
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `tree` block (UI-blocks spec §5.3): a `nodes` binding of
|
|
3
|
-
* `{id, label, icon?, hasChildren?, children?}[]` with a cursor row,
|
|
4
|
-
* arrow-key movement, and Enter to expand/collapse or activate.
|
|
5
|
-
*
|
|
6
|
-
* A node with inline `children` toggles locally and never dispatches;
|
|
7
|
-
* a node with `hasChildren` and no inline children dispatches
|
|
8
|
-
* `onExpand` (an `edit`-shaped action carrying the handler NAME — the
|
|
9
|
-
* handler returns `DataOps` inserting the children). A leaf runs
|
|
10
|
-
* `onActivate` with `{id}`. Markers: `▸`/`▾` (ASCII `>`/`v`).
|
|
11
|
-
*/
|
|
12
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
13
|
-
import type { TreeBlock } from "@sigrun/shared/blocks";
|
|
14
|
-
import { type BlockCtx } from "./context.js";
|
|
15
|
-
/** `tree`: cursor + expand/collapse + activate. */
|
|
16
|
-
export declare class TreeComponent implements Component {
|
|
17
|
-
#private;
|
|
18
|
-
constructor(block: TreeBlock, ctx: BlockCtx);
|
|
19
|
-
render(width: number): string[];
|
|
20
|
-
handleInput(data: string): void;
|
|
21
|
-
invalidate(): void;
|
|
22
|
-
}
|
package/dist/blocks/treeview.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `tree` block (UI-blocks spec §5.3): a `nodes` binding of
|
|
3
|
-
* `{id, label, icon?, hasChildren?, children?}[]` with a cursor row,
|
|
4
|
-
* arrow-key movement, and Enter to expand/collapse or activate.
|
|
5
|
-
*
|
|
6
|
-
* A node with inline `children` toggles locally and never dispatches;
|
|
7
|
-
* a node with `hasChildren` and no inline children dispatches
|
|
8
|
-
* `onExpand` (an `edit`-shaped action carrying the handler NAME — the
|
|
9
|
-
* handler returns `DataOps` inserting the children). A leaf runs
|
|
10
|
-
* `onActivate` with `{id}`. Markers: `▸`/`▾` (ASCII `>`/`v`).
|
|
11
|
-
*/
|
|
12
|
-
import { bindRaw } from "./context.js";
|
|
13
|
-
import { iconGlyph } from "./icons.js";
|
|
14
|
-
import { truncate } from "./width.js";
|
|
15
|
-
/** The markers for expandable nodes. */
|
|
16
|
-
const MARKERS = { collapsed: "▸", expanded: "▾", collapsedAscii: ">", expandedAscii: "v" };
|
|
17
|
-
/** Read the bound nodes, dropping malformed entries. */
|
|
18
|
-
function treeNodes(block, ctx) {
|
|
19
|
-
const raw = bindRaw(block.nodes, ctx);
|
|
20
|
-
if (!Array.isArray(raw))
|
|
21
|
-
return [];
|
|
22
|
-
const out = [];
|
|
23
|
-
for (const entry of raw) {
|
|
24
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry))
|
|
25
|
-
continue;
|
|
26
|
-
const record = entry;
|
|
27
|
-
if (typeof record.id !== "string" || typeof record.label !== "string")
|
|
28
|
-
continue;
|
|
29
|
-
out.push({
|
|
30
|
-
id: record.id,
|
|
31
|
-
label: record.label,
|
|
32
|
-
icon: typeof record.icon === "string" ? record.icon : undefined,
|
|
33
|
-
hasChildren: record.hasChildren === true,
|
|
34
|
-
children: Array.isArray(record.children)
|
|
35
|
-
? record.children
|
|
36
|
-
: undefined,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
/** `tree`: cursor + expand/collapse + activate. */
|
|
42
|
-
export class TreeComponent {
|
|
43
|
-
#block;
|
|
44
|
-
#ctx;
|
|
45
|
-
#cursor = 0;
|
|
46
|
-
#expanded = new Set();
|
|
47
|
-
constructor(block, ctx) {
|
|
48
|
-
this.#block = block;
|
|
49
|
-
this.#ctx = ctx;
|
|
50
|
-
}
|
|
51
|
-
/** The visible rows (children of expanded nodes interleave). */
|
|
52
|
-
#rows() {
|
|
53
|
-
const out = [];
|
|
54
|
-
const walk = (nodes, depth) => {
|
|
55
|
-
for (const node of nodes) {
|
|
56
|
-
out.push({ node, depth });
|
|
57
|
-
if (this.#expanded.has(node.id) && node.children !== undefined) {
|
|
58
|
-
walk(node.children, depth + 1);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
walk(treeNodes(this.#block, this.#ctx), 0);
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
render(width) {
|
|
66
|
-
const unicode = this.#ctx.unicode;
|
|
67
|
-
return this.#rows().map((row, index) => {
|
|
68
|
-
const cursor = index === this.#cursor ? "> " : " ";
|
|
69
|
-
// Every row carries a state marker; leaves show the collapsed
|
|
70
|
-
// glyph as their bullet so icon rows align with parent rows.
|
|
71
|
-
const marker = (this.#expanded.has(row.node.id)
|
|
72
|
-
? unicode
|
|
73
|
-
? MARKERS.expanded
|
|
74
|
-
: MARKERS.expandedAscii
|
|
75
|
-
: unicode
|
|
76
|
-
? MARKERS.collapsed
|
|
77
|
-
: MARKERS.collapsedAscii) + " ";
|
|
78
|
-
const icon = row.node.icon === undefined
|
|
79
|
-
? ""
|
|
80
|
-
: `${iconGlyph(row.node.icon, !unicode)} `;
|
|
81
|
-
const indent = " ".repeat(row.depth);
|
|
82
|
-
return truncate(`${cursor}${indent}${marker}${icon}${row.node.label}`, width);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
handleInput(data) {
|
|
86
|
-
const rows = this.#rows();
|
|
87
|
-
if (data === "\x1b[B") {
|
|
88
|
-
this.#cursor = Math.min(rows.length - 1, this.#cursor + 1);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (data === "\x1b[A") {
|
|
92
|
-
this.#cursor = Math.max(0, this.#cursor - 1);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (data !== "\r")
|
|
96
|
-
return;
|
|
97
|
-
const row = rows[this.#cursor];
|
|
98
|
-
if (row === undefined)
|
|
99
|
-
return;
|
|
100
|
-
const node = row.node;
|
|
101
|
-
if (node.children !== undefined) {
|
|
102
|
-
// Inline children toggle locally; nothing dispatches.
|
|
103
|
-
if (this.#expanded.has(node.id))
|
|
104
|
-
this.#expanded.delete(node.id);
|
|
105
|
-
else
|
|
106
|
-
this.#expanded.add(node.id);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (node.hasChildren === true && this.#block.onExpand !== undefined) {
|
|
110
|
-
const action = { type: "edit", name: this.#block.onExpand };
|
|
111
|
-
this.#ctx.dispatch(action, { id: node.id });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (this.#block.onActivate !== undefined) {
|
|
115
|
-
this.#ctx.dispatch(this.#block.onActivate, { id: node.id });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
invalidate() {
|
|
119
|
-
// Cursor and expansion survive re-renders.
|
|
120
|
-
}
|
|
121
|
-
}
|