@sigrun/tui 0.0.0-stage → 0.1.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 +22 -0
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/actions.d.ts +119 -0
- package/dist/actions.js +12 -0
- package/dist/api.d.ts +152 -0
- package/dist/api.js +189 -0
- package/dist/blocks/braille.d.ts +41 -0
- package/dist/blocks/braille.js +131 -0
- package/dist/blocks/chart.d.ts +20 -0
- package/dist/blocks/chart.js +397 -0
- package/dist/blocks/collection/calendar.d.ts +28 -0
- package/dist/blocks/collection/calendar.js +232 -0
- package/dist/blocks/collection/card.d.ts +36 -0
- package/dist/blocks/collection/card.js +128 -0
- package/dist/blocks/collection/collection.d.ts +9 -0
- package/dist/blocks/collection/collection.js +26 -0
- package/dist/blocks/collection/grid.d.ts +18 -0
- package/dist/blocks/collection/grid.js +72 -0
- package/dist/blocks/collection/lanes.d.ts +32 -0
- package/dist/blocks/collection/lanes.js +320 -0
- package/dist/blocks/collection/list.d.ts +30 -0
- package/dist/blocks/collection/list.js +273 -0
- package/dist/blocks/collection/move.d.ts +67 -0
- package/dist/blocks/collection/move.js +96 -0
- package/dist/blocks/collection/shared.d.ts +87 -0
- package/dist/blocks/collection/shared.js +177 -0
- package/dist/blocks/collection/table.d.ts +26 -0
- package/dist/blocks/collection/table.js +221 -0
- package/dist/blocks/content.d.ts +48 -0
- package/dist/blocks/content.js +253 -0
- package/dist/blocks/context.d.ts +96 -0
- package/dist/blocks/context.js +65 -0
- package/dist/blocks/diff.d.ts +21 -0
- package/dist/blocks/diff.js +25 -0
- package/dist/blocks/ext-transport.d.ts +70 -0
- package/dist/blocks/ext-transport.js +165 -0
- package/dist/blocks/form/choice.d.ts +54 -0
- package/dist/blocks/form/choice.js +175 -0
- package/dist/blocks/form/fields.d.ts +56 -0
- package/dist/blocks/form/fields.js +136 -0
- package/dist/blocks/form/file.d.ts +61 -0
- package/dist/blocks/form/file.js +121 -0
- package/dist/blocks/form/form.d.ts +53 -0
- package/dist/blocks/form/form.js +703 -0
- package/dist/blocks/form/list.d.ts +37 -0
- package/dist/blocks/form/list.js +70 -0
- package/dist/blocks/form/values.d.ts +20 -0
- package/dist/blocks/form/values.js +104 -0
- package/dist/blocks/icons.d.ts +14 -0
- package/dist/blocks/icons.js +74 -0
- package/dist/blocks/image.d.ts +39 -0
- package/dist/blocks/image.js +82 -0
- package/dist/blocks/layout.d.ts +31 -0
- package/dist/blocks/layout.js +155 -0
- package/dist/blocks/log.d.ts +24 -0
- package/dist/blocks/log.js +111 -0
- package/dist/blocks/meter.d.ts +16 -0
- package/dist/blocks/meter.js +50 -0
- package/dist/blocks/page.d.ts +54 -0
- package/dist/blocks/page.js +355 -0
- package/dist/blocks/render.d.ts +31 -0
- package/dist/blocks/render.js +145 -0
- package/dist/blocks/surface-view.d.ts +33 -0
- package/dist/blocks/surface-view.js +86 -0
- package/dist/blocks/surface.d.ts +75 -0
- package/dist/blocks/surface.js +468 -0
- package/dist/blocks/tones.d.ts +23 -0
- package/dist/blocks/tones.js +63 -0
- package/dist/blocks/treeview.d.ts +22 -0
- package/dist/blocks/treeview.js +121 -0
- package/dist/blocks/width.d.ts +32 -0
- package/dist/blocks/width.js +70 -0
- package/dist/clipboard.d.ts +20 -0
- package/dist/clipboard.js +19 -0
- package/dist/editor-launch.d.ts +34 -0
- package/dist/editor-launch.js +70 -0
- package/dist/keymaps/emacs.d.ts +18 -0
- package/dist/keymaps/emacs.js +172 -0
- package/dist/keymaps/standard.d.ts +17 -0
- package/dist/keymaps/standard.js +175 -0
- package/dist/keymaps/types.d.ts +43 -0
- package/dist/keymaps/types.js +28 -0
- package/dist/keymaps/vim.d.ts +48 -0
- package/dist/keymaps/vim.js +291 -0
- package/dist/launch.d.ts +149 -0
- package/dist/launch.js +446 -0
- package/dist/sse.d.ts +109 -0
- package/dist/sse.js +401 -0
- package/dist/theme.d.ts +51 -0
- package/dist/theme.js +119 -0
- package/dist/ui/ambient.d.ts +81 -0
- package/dist/ui/ambient.js +301 -0
- package/dist/ui/app.d.ts +245 -0
- package/dist/ui/app.js +1325 -0
- package/dist/ui/composer.d.ts +66 -0
- package/dist/ui/composer.js +125 -0
- package/dist/ui/conversation-pane.d.ts +123 -0
- package/dist/ui/conversation-pane.js +463 -0
- package/dist/ui/help.d.ts +15 -0
- package/dist/ui/help.js +80 -0
- package/dist/ui/interactions.d.ts +90 -0
- package/dist/ui/interactions.js +330 -0
- package/dist/ui/picker.d.ts +26 -0
- package/dist/ui/picker.js +130 -0
- package/dist/ui/sidebar.d.ts +99 -0
- package/dist/ui/sidebar.js +269 -0
- package/dist/ui/status-bar.d.ts +77 -0
- package/dist/ui/status-bar.js +146 -0
- package/package.json +49 -5
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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;
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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 };
|
|
@@ -0,0 +1,221 @@
|
|
|
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 };
|
|
@@ -0,0 +1,48 @@
|
|
|
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;
|