@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.
Files changed (108) hide show
  1. package/dist/src/Component/section-list/index.d.ts +117 -8
  2. package/dist/src/Component/section-list/index.js +118 -46
  3. package/dist/src/Component/stats-panel/index.d.ts +7 -6
  4. package/dist/src/Component/stats-panel/index.js +42 -54
  5. package/dist/src/Component/top-tabs/index.d.ts +89 -23
  6. package/dist/src/Component/top-tabs/index.js +72 -35
  7. package/dist/src/Entity/collection.d.ts +181 -0
  8. package/dist/src/Entity/collection.js +331 -0
  9. package/dist/src/Entity/conductor/Data/conductors.d.ts +159 -0
  10. package/dist/src/Entity/conductor/Data/conductors.js +213 -0
  11. package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -1
  12. package/dist/src/Entity/conductor/Screen/conductors.js +68 -35
  13. package/dist/src/Entity/project/Data/projects.d.ts +119 -1
  14. package/dist/src/Entity/project/Data/projects.js +113 -1
  15. package/dist/src/Entity/project/Screen/project-picker.d.ts +46 -2
  16. package/dist/src/Entity/project/Screen/project-picker.js +109 -8
  17. package/dist/src/Entity/project/Screen/projects-screen.d.ts +13 -1
  18. package/dist/src/Entity/project/Screen/projects-screen.js +26 -7
  19. package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
  20. package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
  21. package/dist/src/Entity/run/Data/runs.d.ts +108 -1
  22. package/dist/src/Entity/run/Data/runs.js +47 -2
  23. package/dist/src/Entity/run/Screen/run-detail.d.ts +29 -1
  24. package/dist/src/Entity/run/Screen/run-detail.js +33 -14
  25. package/dist/src/Entity/run/Screen/runs.d.ts +42 -38
  26. package/dist/src/Entity/run/Screen/runs.js +7 -7
  27. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +37 -1
  28. package/dist/src/Entity/statistics/Screen/dashboard.js +61 -10
  29. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
  30. package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
  31. package/dist/src/Entity/ticket/Component/create-form/index.d.ts +7 -4
  32. package/dist/src/Entity/ticket/Component/create-form/index.js +28 -18
  33. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +9 -5
  34. package/dist/src/Entity/ticket/Component/edit-form/index.js +51 -25
  35. package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
  36. package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
  37. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +64 -7
  38. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +138 -82
  39. package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
  40. package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
  41. package/dist/src/Entity/ticket/Data/tickets.d.ts +66 -1
  42. package/dist/src/Entity/ticket/Data/tickets.js +38 -2
  43. package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
  44. package/dist/src/Entity/ticket/Data/url.js +20 -0
  45. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +14 -1
  46. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +32 -12
  47. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +12 -1
  48. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +30 -10
  49. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +18 -1
  50. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +42 -19
  51. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +92 -1
  52. package/dist/src/Entity/ticket/Screen/ticket-agents.js +127 -5
  53. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +67 -3
  54. package/dist/src/Entity/ticket/Screen/ticket-detail.js +165 -50
  55. package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -50
  56. package/dist/src/Entity/ticket/Screen/tickets.js +51 -20
  57. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +15 -1
  58. package/dist/src/Entity/ticket/Screen/who-filter-form.js +39 -19
  59. package/dist/src/Form/keys.d.ts +18 -0
  60. package/dist/src/Form/keys.js +46 -24
  61. package/dist/src/Kernel/chrome.d.ts +289 -0
  62. package/dist/src/Kernel/chrome.js +404 -0
  63. package/dist/src/Kernel/clipboard.d.ts +30 -0
  64. package/dist/src/Kernel/clipboard.js +51 -0
  65. package/dist/src/Kernel/key-input.d.ts +50 -0
  66. package/dist/src/Kernel/key-input.js +158 -0
  67. package/dist/src/Kernel/mount.d.ts +187 -0
  68. package/dist/src/Kernel/mount.js +399 -0
  69. package/dist/src/Kernel/tui-kernel.d.ts +0 -1
  70. package/dist/src/Kernel/tui-kernel.js +707 -162
  71. package/dist/src/Routing/route.d.ts +0 -4
  72. package/dist/src/Routing/router.d.ts +16 -0
  73. package/dist/src/Routing/router.js +21 -0
  74. package/dist/src/Screen/async-screen.d.ts +39 -2
  75. package/dist/src/Screen/async-screen.js +46 -4
  76. package/dist/src/Screen/list-screen.d.ts +42 -7
  77. package/dist/src/Screen/list-screen.js +31 -7
  78. package/dist/src/Screen/screen.d.ts +156 -22
  79. package/dist/src/Screen/screen.js +22 -6
  80. package/dist/src/Widget/autocomplete.js +75 -3
  81. package/dist/src/Widget/index.d.ts +3 -2
  82. package/dist/src/Widget/index.js +2 -1
  83. package/dist/src/Widget/option-source.d.ts +12 -1
  84. package/dist/src/Widget/option-source.js +14 -1
  85. package/dist/src/Widget/render.d.ts +23 -8
  86. package/dist/src/Widget/render.js +108 -41
  87. package/dist/src/Widget/text.js +79 -35
  88. package/dist/src/Widget/textarea.d.ts +45 -0
  89. package/dist/src/Widget/textarea.js +115 -0
  90. package/dist/src/Widget/types.d.ts +62 -0
  91. package/dist/src/lib/clipboard.d.ts +41 -0
  92. package/dist/src/lib/clipboard.js +65 -0
  93. package/dist/src/lib/cursor.js +2 -2
  94. package/dist/src/lib/format.d.ts +0 -13
  95. package/dist/src/lib/format.js +17 -28
  96. package/dist/src/lib/hyperlink.d.ts +58 -0
  97. package/dist/src/lib/hyperlink.js +104 -0
  98. package/dist/src/lib/input-trace.d.ts +48 -0
  99. package/dist/src/lib/input-trace.js +47 -0
  100. package/dist/src/lib/key-vocabulary.d.ts +19 -0
  101. package/dist/src/lib/key-vocabulary.js +25 -0
  102. package/dist/src/lib/vnode.d.ts +217 -0
  103. package/dist/src/lib/vnode.js +456 -0
  104. package/dist/src/lib/width.d.ts +0 -8
  105. package/dist/src/lib/width.js +0 -14
  106. package/dist/src/plugin.js +7 -1
  107. package/dist/src/types.d.ts +41 -2
  108. 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: string[];
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
- lines: string[];
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
- * Flatten expandable entries: one caret header row per entry (`▸` collapsed,
36
- * `▾` expanded) followed by that state's rows. `headerRows[i]` is entry i's
37
- * header row, which is both the highlight target and the click map (the rows are
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 flattenExpandable(entries: ExpandableEntry[], { expanded, emptyMessage, }: {
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
- * Append one body row, applying the MINIMAL MARKDOWN rule: an ATX heading
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
- * Extracted so `flattenSections` and `flattenExpandable` cannot answer "what does
8
- * a `## Context` line render as" differently: the Description block is rendered
9
- * by BOTH (flat on the piped render, expandable on the Overview tab), and a rule
10
- * living in only one of them would make the same body look different depending on
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
- function pushRow(lines, boldRows, line) {
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
- if (md) {
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
- // Flatten `{heading, lines}` sections into plain rows plus the indices that are
24
- // bold (section headings + markdown ATX headings). Headings carry NO ANSI here —
25
- // the draw step applies bold after fit(), so sanitizeDisplay never sees (and
26
- // never strips) the ESC.
27
- export function flattenSections(sections) {
28
- const lines = [];
29
- const boldRows = new Set();
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
- boldRows.add(lines.length);
32
- lines.push(section.heading);
33
- for (const line of section.lines)
34
- pushRow(lines, boldRows, line);
35
- lines.push('');
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 { lines, boldRows };
72
+ return out;
38
73
  }
39
74
  /**
40
- * Flatten expandable entries: one caret header row per entry (`▸` collapsed,
41
- * `▾` expanded) followed by that state's rows. `headerRows[i]` is entry i's
42
- * header row, which is both the highlight target and the click map (the rows are
43
- * NOT uniformly spaced an expanded entry appends its body).
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 flattenExpandable(entries, { expanded = new Set(), emptyMessage,
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
- lines: [emptyMessage],
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 lines = [];
63
- const boldRows = new Set();
130
+ const rows = [];
64
131
  const headerRows = [];
65
132
  entries.forEach((entry, i) => {
66
- headerRows.push(lines.length);
67
- boldRows.add(lines.length);
68
- lines.push(`${expanded.has(i) ? '▾' : '▸'} ${entry.header}`);
69
- for (const line of expanded.has(i) ? entry.expanded : entry.collapsed) {
70
- pushRow(lines, boldRows, line);
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 { lines, boldRows, headerRows, selectable: entries.length };
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 is padded exactly like a data label, so the title,
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 column is measured with `displayWidth` (through `pad`/`padNumber`), so a
57
- * wide-character label cannot shift the numbers out of alignment — the failure
58
- * that makes a table of counts unreadable exactly when the data is interesting.
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): string[];
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, in display columns: 26 for the text plus {@link LABEL_GAP}.
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 are padded to the same field, which is
39
- * what keeps a title, the labels it captions and the column titles on one
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
- * Load-bearing, not decoration. `pad` fills a label to exactly `LABEL_WIDTH`, so
50
- * a label of precisely that width left NO separator and the row rendered
51
- * `gaia-e2e-project-name██████` a real project name, at exactly 26 columns. A
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 LABEL_GAP = 1;
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 column is measured with `displayWidth` (through `pad`/`padNumber`), so a
92
- * wide-character label cannot shift the numbers out of alignment — the failure
93
- * that makes a table of counts unreadable exactly when the data is interesting.
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(INDENT +
104
- // The SAME label geometry as a data row below (padded to the field width
105
- // minus the gap, then the gap), so a group title sits exactly over the
106
- // labels it captions. With no title this is the blank label field the
107
- // header row always had, byte for byte.
108
- pad(title ?? '', LABEL_WIDTH - LABEL_GAP) +
109
- ' '.repeat(LABEL_GAP) +
110
- ' '.repeat(BAR_WIDTH) +
111
- (columns ?? []).map((c) => padNumber(c, NUMBER_WIDTH)).join(''));
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(INDENT +
115
- // Padded to the field width MINUS the gap, then the gap: the label
116
- // column stays `LABEL_WIDTH` wide either way, so the header row above
117
- // and every bar below still line up.
118
- pad(row.label, LABEL_WIDTH - LABEL_GAP) +
119
- ' '.repeat(LABEL_GAP) +
120
- bar(barValue(row), max) +
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
  }