@gaia-ai/addon-gaia-ui 0.8.0 → 0.9.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/dist/src/Component/section-list/index.d.ts +117 -8
- package/dist/src/Component/section-list/index.js +118 -46
- package/dist/src/Component/stats-panel/index.d.ts +7 -6
- package/dist/src/Component/stats-panel/index.js +42 -54
- package/dist/src/Component/top-tabs/index.d.ts +89 -23
- package/dist/src/Component/top-tabs/index.js +72 -35
- package/dist/src/Entity/collection.d.ts +181 -0
- package/dist/src/Entity/collection.js +331 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +159 -0
- package/dist/src/Entity/conductor/Data/conductors.js +213 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -1
- package/dist/src/Entity/conductor/Screen/conductors.js +68 -35
- package/dist/src/Entity/project/Data/projects.d.ts +119 -1
- package/dist/src/Entity/project/Data/projects.js +113 -1
- package/dist/src/Entity/project/Screen/project-picker.d.ts +46 -2
- package/dist/src/Entity/project/Screen/project-picker.js +109 -8
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +13 -1
- package/dist/src/Entity/project/Screen/projects-screen.js +26 -7
- package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
- package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
- package/dist/src/Entity/run/Data/runs.d.ts +108 -1
- package/dist/src/Entity/run/Data/runs.js +47 -2
- package/dist/src/Entity/run/Screen/run-detail.d.ts +29 -1
- package/dist/src/Entity/run/Screen/run-detail.js +33 -14
- package/dist/src/Entity/run/Screen/runs.d.ts +42 -38
- package/dist/src/Entity/run/Screen/runs.js +7 -7
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +37 -1
- package/dist/src/Entity/statistics/Screen/dashboard.js +61 -10
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
- package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +7 -4
- package/dist/src/Entity/ticket/Component/create-form/index.js +28 -18
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +9 -5
- package/dist/src/Entity/ticket/Component/edit-form/index.js +51 -25
- package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
- package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +64 -7
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +138 -82
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
- package/dist/src/Entity/ticket/Data/tickets.d.ts +66 -1
- package/dist/src/Entity/ticket/Data/tickets.js +38 -2
- package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
- package/dist/src/Entity/ticket/Data/url.js +20 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +14 -1
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +32 -12
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +12 -1
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +30 -10
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +18 -1
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +42 -19
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +92 -1
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +127 -5
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +67 -3
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +165 -50
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -50
- package/dist/src/Entity/ticket/Screen/tickets.js +51 -20
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +15 -1
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +39 -19
- package/dist/src/Form/keys.d.ts +18 -0
- package/dist/src/Form/keys.js +46 -24
- package/dist/src/Kernel/chrome.d.ts +289 -0
- package/dist/src/Kernel/chrome.js +404 -0
- package/dist/src/Kernel/clipboard.d.ts +30 -0
- package/dist/src/Kernel/clipboard.js +51 -0
- package/dist/src/Kernel/key-input.d.ts +50 -0
- package/dist/src/Kernel/key-input.js +158 -0
- package/dist/src/Kernel/mount.d.ts +187 -0
- package/dist/src/Kernel/mount.js +399 -0
- package/dist/src/Kernel/tui-kernel.d.ts +0 -1
- package/dist/src/Kernel/tui-kernel.js +707 -162
- package/dist/src/Routing/route.d.ts +0 -4
- package/dist/src/Routing/router.d.ts +16 -0
- package/dist/src/Routing/router.js +21 -0
- package/dist/src/Screen/async-screen.d.ts +39 -2
- package/dist/src/Screen/async-screen.js +46 -4
- package/dist/src/Screen/list-screen.d.ts +42 -7
- package/dist/src/Screen/list-screen.js +31 -7
- package/dist/src/Screen/screen.d.ts +156 -22
- package/dist/src/Screen/screen.js +22 -6
- package/dist/src/Widget/autocomplete.js +75 -3
- package/dist/src/Widget/index.d.ts +3 -2
- package/dist/src/Widget/index.js +2 -1
- package/dist/src/Widget/option-source.d.ts +12 -1
- package/dist/src/Widget/option-source.js +14 -1
- package/dist/src/Widget/render.d.ts +23 -8
- package/dist/src/Widget/render.js +108 -41
- package/dist/src/Widget/text.js +79 -35
- package/dist/src/Widget/textarea.d.ts +45 -0
- package/dist/src/Widget/textarea.js +115 -0
- package/dist/src/Widget/types.d.ts +62 -0
- package/dist/src/lib/clipboard.d.ts +41 -0
- package/dist/src/lib/clipboard.js +65 -0
- package/dist/src/lib/cursor.js +2 -2
- package/dist/src/lib/format.d.ts +0 -13
- package/dist/src/lib/format.js +17 -28
- package/dist/src/lib/hyperlink.d.ts +58 -0
- package/dist/src/lib/hyperlink.js +104 -0
- package/dist/src/lib/input-trace.d.ts +48 -0
- package/dist/src/lib/input-trace.js +47 -0
- package/dist/src/lib/key-vocabulary.d.ts +19 -0
- package/dist/src/lib/key-vocabulary.js +25 -0
- package/dist/src/lib/vnode.d.ts +217 -0
- package/dist/src/lib/vnode.js +456 -0
- package/dist/src/lib/width.d.ts +0 -8
- package/dist/src/lib/width.js +0 -14
- package/dist/src/plugin.js +7 -1
- package/dist/src/types.d.ts +41 -2
- package/package.json +4 -2
|
@@ -1,8 +1,104 @@
|
|
|
1
|
+
import type { VNode } from '@opentui/core';
|
|
2
|
+
import { type Cell } from '../../lib/vnode.js';
|
|
3
|
+
/**
|
|
4
|
+
* A section body row that is COLUMNS rather than prose — GAIA-340 Task 13.
|
|
5
|
+
*
|
|
6
|
+
* The two producers are the dashboard's bars and the ticket detail's Runs
|
|
7
|
+
* table, and both used to compose their columns into one padded string with
|
|
8
|
+
* `lib/format.ts`'s `pad`. They describe their cells instead: the tree lays them
|
|
9
|
+
* out with Yoga budgets, and a surface with no layout — the pipe — prints
|
|
10
|
+
* {@link lineText}'s join. That is the operator-approved cost of retiring the
|
|
11
|
+
* padding, and it is what lets `pad` (and with it two of `lib/width.ts`'s
|
|
12
|
+
* consumers) go.
|
|
13
|
+
*/
|
|
14
|
+
export interface SectionCells {
|
|
15
|
+
readonly cells: readonly Cell[];
|
|
16
|
+
}
|
|
17
|
+
/** One body row of a section: prose, or columns a renderer places itself. */
|
|
18
|
+
export type SectionLine = string | SectionCells;
|
|
19
|
+
/**
|
|
20
|
+
* A line as the text a surface with no layout prints.
|
|
21
|
+
*
|
|
22
|
+
* The separator is ONE space because that is the gap `lib/vnode.ts`'s `rowNode`
|
|
23
|
+
* puts between two cells: the pipe and the tree then say the same thing about
|
|
24
|
+
* what separates two columns, and only the alignment inside a column is the
|
|
25
|
+
* renderer's own.
|
|
26
|
+
*/
|
|
27
|
+
export declare const lineText: (line: SectionLine) => string;
|
|
1
28
|
/** A titled block of already-flattened physical rows. */
|
|
2
29
|
export interface Section {
|
|
3
30
|
heading: string;
|
|
4
|
-
lines:
|
|
31
|
+
lines: SectionLine[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The MINIMAL MARKDOWN rule: an ATX heading (`#`…`######`) renders as bold with
|
|
35
|
+
* the markers stripped — the same rule a section heading follows. Bullets and
|
|
36
|
+
* code fences stay raw by design (explicit v1 decision).
|
|
37
|
+
*
|
|
38
|
+
* ONE function, because every renderer of a body line goes through it:
|
|
39
|
+
* {@link sectionRows} for a flat section, {@link expandableRows} for a
|
|
40
|
+
* collapsible one, and `Entity/run/Component/run-detail` for a log entry. The
|
|
41
|
+
* Description block is rendered by two of them at once — flat on the piped
|
|
42
|
+
* render, expandable on the Overview tab — and a rule living in only one would
|
|
43
|
+
* make the same body look different depending on which renderer reached it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function markdownRow(line: string): {
|
|
46
|
+
text: string;
|
|
47
|
+
bold: boolean;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* One flattened row: its text, whether it is bold, and WHICH ROW IT IS
|
|
51
|
+
* (GAIA-340 D3/D4).
|
|
52
|
+
*
|
|
53
|
+
* `key` is what a converted screen turns into its node's `props.id`, so a row
|
|
54
|
+
* has to be able to say what it is rather than where it sits. It is scoped by
|
|
55
|
+
* the section's own HEADING — `per-user-0`, never `row-14` — so a section keeps
|
|
56
|
+
* its keys when the section above it is absent. Two sections sharing a heading
|
|
57
|
+
* would collide, and `Kernel/mount.ts` refuses a repeated id loudly rather than
|
|
58
|
+
* patching one row's content onto another's node.
|
|
59
|
+
*/
|
|
60
|
+
export interface SectionRow {
|
|
61
|
+
readonly key: string;
|
|
62
|
+
readonly text: string;
|
|
63
|
+
readonly bold: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The row's columns, when it has any — see {@link SectionCells}.
|
|
66
|
+
*
|
|
67
|
+
* `text` is the same row for a surface that cannot lay one out, so a consumer
|
|
68
|
+
* that only prints never has to know the difference; {@link sectionRowNode} is
|
|
69
|
+
* where the difference is made.
|
|
70
|
+
*/
|
|
71
|
+
readonly cells?: readonly Cell[];
|
|
5
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* A heading as an id fragment: `PER USER` → `per-user`.
|
|
75
|
+
*
|
|
76
|
+
* Exported because a tab that renders one of its sections as a COLLAPSIBLE entry
|
|
77
|
+
* has to key that entry the same way `sectionRows` keys a flat one — otherwise
|
|
78
|
+
* the same section would carry different node ids depending on which tab reached
|
|
79
|
+
* it, and `patchTree` would rebuild it instead of writing to it (GAIA-340 D4).
|
|
80
|
+
*/
|
|
81
|
+
export declare const sectionKey: (heading: string) => string;
|
|
82
|
+
/**
|
|
83
|
+
* Flatten `{heading, lines}` sections into KEYED rows: the heading, its body,
|
|
84
|
+
* and the blank line that closes the section.
|
|
85
|
+
*
|
|
86
|
+
* The one description of what a section renders as. {@link flattenSections}
|
|
87
|
+
* derives the row surface from it and a converted screen derives its nodes from
|
|
88
|
+
* it, so the two cannot answer differently for the same document — which is the
|
|
89
|
+
* property that lets the Dashboard hand over renderables while its pipe keeps
|
|
90
|
+
* emitting the bytes it always did (spec F5).
|
|
91
|
+
*/
|
|
92
|
+
export declare function sectionRows(sections: Section[]): SectionRow[];
|
|
93
|
+
/**
|
|
94
|
+
* One flattened row as the node a converted screen hands over — GAIA-340 D1.
|
|
95
|
+
*
|
|
96
|
+
* The ONE place a `SectionRow` becomes a renderable, so the two screens that
|
|
97
|
+
* render section rows (the dashboard and the ticket detail's tabs) cannot
|
|
98
|
+
* disagree about what a columned row looks like. `prefix` is the screen's own
|
|
99
|
+
* body id, which is what keeps two screens' rows apart in one tree.
|
|
100
|
+
*/
|
|
101
|
+
export declare const sectionRowNode: (prefix: string, row: SectionRow, selected?: boolean) => VNode;
|
|
6
102
|
export declare function flattenSections(sections: Section[]): {
|
|
7
103
|
lines: string[];
|
|
8
104
|
boldRows: Set<number>;
|
|
@@ -16,6 +112,12 @@ export declare function flattenSections(sections: Section[]): {
|
|
|
16
112
|
* and structural sharing is the only form of that claim a test can hold.
|
|
17
113
|
*/
|
|
18
114
|
export interface ExpandableEntry {
|
|
115
|
+
/**
|
|
116
|
+
* What this entry IS, as an id fragment (GAIA-340 D4/R4) — a comment's uuid,
|
|
117
|
+
* a section's slug. Its rows are keyed under it, so an entry's content and its
|
|
118
|
+
* identity move together when the list re-sorts.
|
|
119
|
+
*/
|
|
120
|
+
key: string;
|
|
19
121
|
/** The header row's text, after the caret. */
|
|
20
122
|
header: string;
|
|
21
123
|
/** Rows shown while collapsed (indented like the expanded body). */
|
|
@@ -25,22 +127,29 @@ export interface ExpandableEntry {
|
|
|
25
127
|
}
|
|
26
128
|
/** The rendered rows of a list of expandable entries. */
|
|
27
129
|
export interface ExpandableView {
|
|
28
|
-
|
|
29
|
-
boldRows: Set<number>;
|
|
130
|
+
rows: SectionRow[];
|
|
30
131
|
/** `headerRows[i]` is the row index of entry i's header — the selection map. */
|
|
31
132
|
headerRows: number[];
|
|
32
133
|
selectable: number;
|
|
33
134
|
}
|
|
34
135
|
/**
|
|
35
|
-
*
|
|
36
|
-
* `▾` expanded) followed by that state's rows. `headerRows[i]` is
|
|
37
|
-
* header row, which is both the highlight target and the click map
|
|
38
|
-
* NOT uniformly spaced — an expanded entry appends its body).
|
|
136
|
+
* Expandable entries as KEYED rows: one caret header row per entry (`▸`
|
|
137
|
+
* collapsed, `▾` expanded) followed by that state's rows. `headerRows[i]` is
|
|
138
|
+
* entry i's header row, which is both the highlight target and the click map
|
|
139
|
+
* (the rows are NOT uniformly spaced — an expanded entry appends its body).
|
|
39
140
|
*
|
|
40
141
|
* Zero entries render `emptyMessage` and nothing selectable, so an empty list is
|
|
41
142
|
* an explicit statement rather than a blank screen.
|
|
143
|
+
*
|
|
144
|
+
* GAIA-340 D1/D3 — rows rather than lines-plus-a-`boldRows`-set, and the two
|
|
145
|
+
* halves of that are the same change: the caller turns each row into a node, so
|
|
146
|
+
* "this row is a heading" is a prop on the node and no longer an index into a
|
|
147
|
+
* surface somebody else styles. `SectionRow` is the shape {@link sectionRows}
|
|
148
|
+
* already produces, so a tab that mixes flat sections with expandable ones has
|
|
149
|
+
* one row type rather than two.
|
|
42
150
|
*/
|
|
43
|
-
export declare function
|
|
151
|
+
export declare function expandableRows(entries: ExpandableEntry[], { expanded, emptyMessage, emptyKey, }: {
|
|
44
152
|
expanded?: ReadonlySet<number>;
|
|
45
153
|
emptyMessage: string;
|
|
154
|
+
emptyKey: string;
|
|
46
155
|
}): ExpandableView;
|
|
@@ -1,51 +1,120 @@
|
|
|
1
|
+
import { cellsRow, textNode } from '../../lib/vnode.js';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`#`…`######`) renders as bold with the markers stripped — the same rule a
|
|
4
|
-
* section heading follows, styled after fit(). Bullets and code fences stay raw
|
|
5
|
-
* by design (explicit v1 decision).
|
|
3
|
+
* A line as the text a surface with no layout prints.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* which renderer reached it.
|
|
5
|
+
* The separator is ONE space because that is the gap `lib/vnode.ts`'s `rowNode`
|
|
6
|
+
* puts between two cells: the pipe and the tree then say the same thing about
|
|
7
|
+
* what separates two columns, and only the alignment inside a column is the
|
|
8
|
+
* renderer's own.
|
|
12
9
|
*/
|
|
13
|
-
|
|
10
|
+
export const lineText = (line) => typeof line === 'string'
|
|
11
|
+
? line
|
|
12
|
+
: line.cells
|
|
13
|
+
.map((cell) => cell.text)
|
|
14
|
+
.join(' ')
|
|
15
|
+
.replace(/\s+$/, '');
|
|
16
|
+
/**
|
|
17
|
+
* The MINIMAL MARKDOWN rule: an ATX heading (`#`…`######`) renders as bold with
|
|
18
|
+
* the markers stripped — the same rule a section heading follows. Bullets and
|
|
19
|
+
* code fences stay raw by design (explicit v1 decision).
|
|
20
|
+
*
|
|
21
|
+
* ONE function, because every renderer of a body line goes through it:
|
|
22
|
+
* {@link sectionRows} for a flat section, {@link expandableRows} for a
|
|
23
|
+
* collapsible one, and `Entity/run/Component/run-detail` for a log entry. The
|
|
24
|
+
* Description block is rendered by two of them at once — flat on the piped
|
|
25
|
+
* render, expandable on the Overview tab — and a rule living in only one would
|
|
26
|
+
* make the same body look different depending on which renderer reached it.
|
|
27
|
+
*/
|
|
28
|
+
export function markdownRow(line) {
|
|
14
29
|
const md = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
15
|
-
|
|
16
|
-
boldRows.add(lines.length);
|
|
17
|
-
lines.push(md[2] ?? '');
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
lines.push(line);
|
|
21
|
-
}
|
|
30
|
+
return md ? { text: md[2] ?? '', bold: true } : { text: line, bold: false };
|
|
22
31
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A heading as an id fragment: `PER USER` → `per-user`.
|
|
34
|
+
*
|
|
35
|
+
* Exported because a tab that renders one of its sections as a COLLAPSIBLE entry
|
|
36
|
+
* has to key that entry the same way `sectionRows` keys a flat one — otherwise
|
|
37
|
+
* the same section would carry different node ids depending on which tab reached
|
|
38
|
+
* it, and `patchTree` would rebuild it instead of writing to it (GAIA-340 D4).
|
|
39
|
+
*/
|
|
40
|
+
export const sectionKey = (heading) => heading
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-|-$/g, '');
|
|
44
|
+
/**
|
|
45
|
+
* Flatten `{heading, lines}` sections into KEYED rows: the heading, its body,
|
|
46
|
+
* and the blank line that closes the section.
|
|
47
|
+
*
|
|
48
|
+
* The one description of what a section renders as. {@link flattenSections}
|
|
49
|
+
* derives the row surface from it and a converted screen derives its nodes from
|
|
50
|
+
* it, so the two cannot answer differently for the same document — which is the
|
|
51
|
+
* property that lets the Dashboard hand over renderables while its pipe keeps
|
|
52
|
+
* emitting the bytes it always did (spec F5).
|
|
53
|
+
*/
|
|
54
|
+
export function sectionRows(sections) {
|
|
55
|
+
const out = [];
|
|
30
56
|
for (const section of sections) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
const id = sectionKey(section.heading);
|
|
58
|
+
out.push({ key: `${id}-heading`, text: section.heading, bold: true });
|
|
59
|
+
section.lines.forEach((line, index) => {
|
|
60
|
+
const key = `${id}-${index}`;
|
|
61
|
+
// Columns are never read as markdown: a run number opens with `#`, and the
|
|
62
|
+
// ATX rule would both bold the row and eat the number.
|
|
63
|
+
if (typeof line !== 'string') {
|
|
64
|
+
out.push({ key, text: lineText(line), bold: false, cells: line.cells });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const { text, bold } = markdownRow(line);
|
|
68
|
+
out.push({ key, text, bold });
|
|
69
|
+
});
|
|
70
|
+
out.push({ key: `${id}-gap`, text: '', bold: false });
|
|
36
71
|
}
|
|
37
|
-
return
|
|
72
|
+
return out;
|
|
38
73
|
}
|
|
39
74
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
75
|
+
* One flattened row as the node a converted screen hands over — GAIA-340 D1.
|
|
76
|
+
*
|
|
77
|
+
* The ONE place a `SectionRow` becomes a renderable, so the two screens that
|
|
78
|
+
* render section rows (the dashboard and the ticket detail's tabs) cannot
|
|
79
|
+
* disagree about what a columned row looks like. `prefix` is the screen's own
|
|
80
|
+
* body id, which is what keeps two screens' rows apart in one tree.
|
|
81
|
+
*/
|
|
82
|
+
export const sectionRowNode = (prefix, row, selected = false) => row.cells === undefined
|
|
83
|
+
? textNode({
|
|
84
|
+
id: `${prefix}-${row.key}`,
|
|
85
|
+
content: row.text,
|
|
86
|
+
bold: row.bold,
|
|
87
|
+
selected,
|
|
88
|
+
})
|
|
89
|
+
: cellsRow(`${prefix}-${row.key}`, row.cells, selected);
|
|
90
|
+
// The same rows as plain text plus the INDICES that are bold (section headings +
|
|
91
|
+
// markdown ATX headings) — the shape the row surface needs. Headings carry NO
|
|
92
|
+
// ANSI here: the draw step applies bold after fit(), so sanitizeDisplay never
|
|
93
|
+
// sees (and never strips) the ESC.
|
|
94
|
+
export function flattenSections(sections) {
|
|
95
|
+
const rows = sectionRows(sections);
|
|
96
|
+
return {
|
|
97
|
+
lines: rows.map((row) => row.text),
|
|
98
|
+
boldRows: new Set(rows.flatMap((row, index) => (row.bold ? [index] : []))),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Expandable entries as KEYED rows: one caret header row per entry (`▸`
|
|
103
|
+
* collapsed, `▾` expanded) followed by that state's rows. `headerRows[i]` is
|
|
104
|
+
* entry i's header row, which is both the highlight target and the click map
|
|
105
|
+
* (the rows are NOT uniformly spaced — an expanded entry appends its body).
|
|
44
106
|
*
|
|
45
107
|
* Zero entries render `emptyMessage` and nothing selectable, so an empty list is
|
|
46
108
|
* an explicit statement rather than a blank screen.
|
|
109
|
+
*
|
|
110
|
+
* GAIA-340 D1/D3 — rows rather than lines-plus-a-`boldRows`-set, and the two
|
|
111
|
+
* halves of that are the same change: the caller turns each row into a node, so
|
|
112
|
+
* "this row is a heading" is a prop on the node and no longer an index into a
|
|
113
|
+
* surface somebody else styles. `SectionRow` is the shape {@link sectionRows}
|
|
114
|
+
* already produces, so a tab that mixes flat sections with expandable ones has
|
|
115
|
+
* one row type rather than two.
|
|
47
116
|
*/
|
|
48
|
-
export function
|
|
117
|
+
export function expandableRows(entries, { expanded = new Set(), emptyMessage, emptyKey,
|
|
49
118
|
// GAIA-255 AC-11 review: `ReadonlySet` because the render path is handed ONE
|
|
50
119
|
// shared empty set for every not-yet-expanded tab (`expandedFor`) — growing
|
|
51
120
|
// it here would leak one tab's expansion into all the others. The renderer
|
|
@@ -53,22 +122,25 @@ export function flattenExpandable(entries, { expanded = new Set(), emptyMessage,
|
|
|
53
122
|
}) {
|
|
54
123
|
if (!entries.length) {
|
|
55
124
|
return {
|
|
56
|
-
|
|
57
|
-
boldRows: new Set(),
|
|
125
|
+
rows: [{ key: emptyKey, text: emptyMessage, bold: false }],
|
|
58
126
|
headerRows: [],
|
|
59
127
|
selectable: 0,
|
|
60
128
|
};
|
|
61
129
|
}
|
|
62
|
-
const
|
|
63
|
-
const boldRows = new Set();
|
|
130
|
+
const rows = [];
|
|
64
131
|
const headerRows = [];
|
|
65
132
|
entries.forEach((entry, i) => {
|
|
66
|
-
headerRows.push(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
133
|
+
headerRows.push(rows.length);
|
|
134
|
+
rows.push({
|
|
135
|
+
key: `${entry.key}-header`,
|
|
136
|
+
text: `${expanded.has(i) ? '▾' : '▸'} ${entry.header}`,
|
|
137
|
+
bold: true,
|
|
138
|
+
});
|
|
139
|
+
const body = expanded.has(i) ? entry.expanded : entry.collapsed;
|
|
140
|
+
body.forEach((line, index) => {
|
|
141
|
+
const { text, bold } = markdownRow(line);
|
|
142
|
+
rows.push({ key: `${entry.key}-${index}`, text, bold });
|
|
143
|
+
});
|
|
72
144
|
});
|
|
73
|
-
return {
|
|
145
|
+
return { rows, headerRows, selectable: entries.length };
|
|
74
146
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SectionLine } from '../section-list/index.js';
|
|
1
2
|
/** A filled bar cell. U+2588 FULL BLOCK. */
|
|
2
3
|
export declare const BAR_FULL = "\u2588";
|
|
3
4
|
/** An empty bar cell. U+2591 LIGHT SHADE. */
|
|
@@ -42,8 +43,8 @@ export interface StatsPanelOptions {
|
|
|
42
43
|
* it to say which group each block of bars is.
|
|
43
44
|
*
|
|
44
45
|
* The component stays ENTITY-NEUTRAL: this is a string, and nothing here knows
|
|
45
|
-
* a ticket from a run. It
|
|
46
|
-
* the labels beneath it and the column titles keep one geometry.
|
|
46
|
+
* a ticket from a run. It declares exactly the data label's budget, so the
|
|
47
|
+
* title, the labels beneath it and the column titles keep one geometry.
|
|
47
48
|
*/
|
|
48
49
|
title?: string | undefined;
|
|
49
50
|
/** What to render instead of rows when there are none (spec R7). */
|
|
@@ -53,8 +54,8 @@ export interface StatsPanelOptions {
|
|
|
53
54
|
* One block's body rows: an optional header row (the group's title, the column
|
|
54
55
|
* titles, or both), then one row per entry.
|
|
55
56
|
*
|
|
56
|
-
* Every
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* Every row declares the SAME cell budgets in the same order, which is what
|
|
58
|
+
* keeps the header's titles over the columns they name — the structural form of
|
|
59
|
+
* the claim the padded rows used to make by measuring each field.
|
|
59
60
|
*/
|
|
60
|
-
export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, title, emptyMessage }?: StatsPanelOptions):
|
|
61
|
+
export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, title, emptyMessage }?: StatsPanelOptions): SectionLine[];
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { pad } from '../../lib/format.js';
|
|
2
|
-
import { displayWidth } from '../../lib/width.js';
|
|
3
1
|
// GAIA-256 spec D11 — the Dashboard's `/usage`-style bar rows.
|
|
4
2
|
//
|
|
5
3
|
// A SHARED component (the third in `src/Component/`), and it earns that place by
|
|
@@ -11,6 +9,14 @@ import { displayWidth } from '../../lib/width.js';
|
|
|
11
9
|
// It produces the ROWS OF ONE BLOCK. Stacking blocks under bold headings is
|
|
12
10
|
// `flattenSections`' job (`Component/section-list`), unchanged: this component
|
|
13
11
|
// only fills a block's body.
|
|
12
|
+
//
|
|
13
|
+
// GAIA-340 Task 13 — a row is CELLS AND THEIR BUDGETS, not a padded string.
|
|
14
|
+
// `lib/format.ts`'s `pad` used to fill the label field and a local `padNumber`
|
|
15
|
+
// the count columns, both of which had to measure the value in display columns
|
|
16
|
+
// first (`lib/width.ts`). The renderer places a value inside its budget, so the
|
|
17
|
+
// alignment is Yoga's and nothing here measures. The operator approved what that
|
|
18
|
+
// costs: the pipe prints the same values with one space between them and no
|
|
19
|
+
// column alignment, because a pipe runs no layout (spec F5, A4 re-pinned).
|
|
14
20
|
/** A filled bar cell. U+2588 FULL BLOCK. */
|
|
15
21
|
export const BAR_FULL = '█';
|
|
16
22
|
/** An empty bar cell. U+2591 LIGHT SHADE. */
|
|
@@ -28,47 +34,25 @@ export const BAR_EMPTY = '░';
|
|
|
28
34
|
* ever bites, the fix is an ASCII bar glyph, never a width-table change.
|
|
29
35
|
*/
|
|
30
36
|
export const BAR_WIDTH = 20;
|
|
37
|
+
/** The indent every row of a block carries, matching the other body renderers. */
|
|
38
|
+
const INDENT = ' ';
|
|
31
39
|
/**
|
|
32
|
-
* The label column
|
|
40
|
+
* The label column's budget: {@link INDENT} plus 27 columns of text.
|
|
33
41
|
*
|
|
34
42
|
* The text budget is what the field has to hold, and after GAIA-324 that is two
|
|
35
43
|
* different kinds of text sharing one column: a breakdown row's bare entity name
|
|
36
44
|
* (a real project name reaches 26 columns on its own — `gaia-e2e-project` plus a
|
|
37
45
|
* suffix used to; a long one still does), and the group TITLE printed in the
|
|
38
|
-
* header row's label field above it. Both
|
|
39
|
-
*
|
|
40
|
-
* geometry. Kept at 26 rather than narrowed with the `· tickets` suffix GAIA-324
|
|
41
|
-
* deleted: the budget was never sized for the suffix alone, and a project name
|
|
42
|
-
* cut to `a-very-long-project-…` is the same unreadable row the width exists to
|
|
43
|
-
* prevent.
|
|
44
|
-
*/
|
|
45
|
-
const LABEL_WIDTH = 27;
|
|
46
|
-
/**
|
|
47
|
-
* Columns of the label field reserved as the gap before the bar.
|
|
46
|
+
* header row's label field above it. Both declare the same budget, which is what
|
|
47
|
+
* keeps a title, the labels it captions and the column titles on one geometry.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
* a
|
|
51
|
-
* `
|
|
52
|
-
* truncated label collided the same way, ending in `…` against the bar. Every
|
|
53
|
-
* fixture in the tests was shorter, which is why the suite was green while the
|
|
54
|
-
* screen was wrong; the gap is part of the geometry now so the label length
|
|
55
|
-
* cannot decide whether it exists.
|
|
56
|
-
*
|
|
57
|
-
* It is ADDED to the field rather than taken out of it: the 26 columns above are
|
|
58
|
-
* a content budget with a stated reason, and spending one of them on the gap
|
|
59
|
-
* would truncate one more column off the longest names — the labels that are
|
|
60
|
-
* already hardest to tell apart.
|
|
49
|
+
* The indent rides INSIDE the cell's text rather than beside it as a cell of its
|
|
50
|
+
* own, so a row's first column starts where the other body renderers' rows do
|
|
51
|
+
* and the row's own `gap` is not spent separating an empty cell from the label.
|
|
61
52
|
*/
|
|
62
|
-
const
|
|
63
|
-
/** One number column, in display columns. */
|
|
53
|
+
const LABEL_WIDTH = INDENT.length + 27;
|
|
54
|
+
/** One number column's budget, in display columns. */
|
|
64
55
|
const NUMBER_WIDTH = 8;
|
|
65
|
-
/** The indent every row of a block carries, matching the other body renderers. */
|
|
66
|
-
const INDENT = ' ';
|
|
67
|
-
/** `value` right-aligned to `width` DISPLAY columns — the number columns' rule. */
|
|
68
|
-
function padNumber(value, width) {
|
|
69
|
-
const gap = Math.max(0, width - displayWidth(value));
|
|
70
|
-
return ' '.repeat(gap) + value;
|
|
71
|
-
}
|
|
72
56
|
/**
|
|
73
57
|
* The bar for `value` within a group whose largest bar value is `max`.
|
|
74
58
|
*
|
|
@@ -88,9 +72,9 @@ function bar(value, max) {
|
|
|
88
72
|
* One block's body rows: an optional header row (the group's title, the column
|
|
89
73
|
* titles, or both), then one row per entry.
|
|
90
74
|
*
|
|
91
|
-
* Every
|
|
92
|
-
*
|
|
93
|
-
*
|
|
75
|
+
* Every row declares the SAME cell budgets in the same order, which is what
|
|
76
|
+
* keeps the header's titles over the columns they name — the structural form of
|
|
77
|
+
* the claim the padded rows used to make by measuring each field.
|
|
94
78
|
*/
|
|
95
79
|
export function statsPanelRows(rows, { columns, title, emptyMessage } = {}) {
|
|
96
80
|
if (rows.length === 0) {
|
|
@@ -98,27 +82,31 @@ export function statsPanelRows(rows, { columns, title, emptyMessage } = {}) {
|
|
|
98
82
|
}
|
|
99
83
|
const barValue = (row) => row.numbers.at(-1) ?? 0;
|
|
100
84
|
const max = Math.max(...rows.map(barValue));
|
|
85
|
+
const number = (text) => ({
|
|
86
|
+
text,
|
|
87
|
+
width: NUMBER_WIDTH,
|
|
88
|
+
right: true,
|
|
89
|
+
});
|
|
101
90
|
const out = [];
|
|
102
91
|
if ((columns && columns.length > 0) || title !== undefined) {
|
|
103
|
-
out.push(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
92
|
+
out.push({
|
|
93
|
+
cells: [
|
|
94
|
+
{ text: `${INDENT}${title ?? ''}`, width: LABEL_WIDTH },
|
|
95
|
+
// The bar's column, empty: the header row has no bar, and the cell is
|
|
96
|
+
// what holds the column titles over the numbers rather than over it.
|
|
97
|
+
{ text: '', width: BAR_WIDTH },
|
|
98
|
+
...(columns ?? []).map(number),
|
|
99
|
+
],
|
|
100
|
+
});
|
|
112
101
|
}
|
|
113
102
|
for (const row of rows) {
|
|
114
|
-
out.push(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
row.numbers.map((n) => padNumber(String(n), NUMBER_WIDTH)).join(''));
|
|
103
|
+
out.push({
|
|
104
|
+
cells: [
|
|
105
|
+
{ text: `${INDENT}${row.label}`, width: LABEL_WIDTH },
|
|
106
|
+
{ text: bar(barValue(row), max), width: BAR_WIDTH },
|
|
107
|
+
...row.numbers.map((n) => number(String(n))),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
122
110
|
}
|
|
123
111
|
return out;
|
|
124
112
|
}
|