@gaia-ai/addon-gaia-ui 0.16.0 → 0.16.2

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 (220) hide show
  1. package/dist/src/Component/badge/index.d.ts +11 -0
  2. package/dist/src/Component/badge/index.js +10 -0
  3. package/dist/src/Component/bar-chart/index.d.ts +16 -0
  4. package/dist/src/Component/bar-chart/index.js +63 -0
  5. package/dist/src/Component/card/index.d.ts +16 -0
  6. package/dist/src/Component/card/index.js +39 -0
  7. package/dist/src/Component/divider/index.d.ts +7 -0
  8. package/dist/src/Component/divider/index.js +13 -0
  9. package/dist/src/Component/heat-map/index.d.ts +21 -0
  10. package/dist/src/Component/heat-map/index.js +86 -0
  11. package/dist/src/Component/key-value/index.d.ts +18 -0
  12. package/dist/src/Component/key-value/index.js +30 -0
  13. package/dist/src/Component/markdown/index.d.ts +35 -5
  14. package/dist/src/Component/markdown/index.js +72 -16
  15. package/dist/src/Entity/team/Data/workload.d.ts +3 -1
  16. package/dist/src/Entity/team/Data/workload.js +6 -4
  17. package/dist/src/Entity/team/Screen/team-work.d.ts +12 -1
  18. package/dist/src/Entity/team/Screen/team-work.js +47 -2
  19. package/dist/src/Entity/ticket/Component/demo-statistics.d.ts +14 -0
  20. package/dist/src/Entity/ticket/Component/demo-statistics.js +88 -0
  21. package/dist/src/Entity/ticket/Component/react-statistics.d.ts +14 -22
  22. package/dist/src/Entity/ticket/Component/react-statistics.js +21 -42
  23. package/dist/src/Entity/ticket/Component/ticket-content/index.d.ts +28 -3
  24. package/dist/src/Entity/ticket/Component/ticket-content/index.js +331 -157
  25. package/dist/src/Entity/ticket/Data/parents.js +1 -1
  26. package/dist/src/Entity/ticket/Data/query.d.ts +2 -2
  27. package/dist/src/Entity/ticket/Data/query.js +13 -9
  28. package/dist/src/Entity/ticket/Data/store.js +1 -1
  29. package/dist/src/Entity/ticket/Data/tickets.d.ts +1 -1
  30. package/dist/src/Entity/ticket/Data/tickets.js +1 -1
  31. package/dist/src/Entity/ticket/Form/edit.d.ts +13 -0
  32. package/dist/src/Entity/ticket/Form/edit.js +17 -7
  33. package/dist/src/Entity/ticket/Screen/conductor-tickets.d.ts +7 -1
  34. package/dist/src/Entity/ticket/Screen/conductor-tickets.js +30 -6
  35. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +3 -1
  36. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +21 -6
  37. package/dist/src/Entity/ticket/Screen/my-work.d.ts +8 -1
  38. package/dist/src/Entity/ticket/Screen/my-work.js +37 -4
  39. package/dist/src/Entity/ticket/Screen/ticket-comments.d.ts +6 -12
  40. package/dist/src/Entity/ticket/Screen/ticket-comments.js +11 -16
  41. package/dist/src/Entity/ticket/Screen/ticket-content-screen.d.ts +25 -44
  42. package/dist/src/Entity/ticket/Screen/ticket-content-screen.js +79 -87
  43. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +6 -11
  44. package/dist/src/Entity/ticket/Screen/ticket-detail.js +14 -19
  45. package/dist/src/Entity/ticket/Screen/ticket-filter-bar.js +2 -9
  46. package/dist/src/Entity/ticket/Screen/ticket-planning.d.ts +6 -12
  47. package/dist/src/Entity/ticket/Screen/ticket-planning.js +11 -16
  48. package/dist/src/Entity/ticket/Screen/ticket-react-markdown.d.ts +9 -7
  49. package/dist/src/Entity/ticket/Screen/ticket-react-markdown.js +36 -13
  50. package/dist/src/Entity/ticket/Screen/ticket-react.d.ts +21 -8
  51. package/dist/src/Entity/ticket/Screen/ticket-react.js +109 -34
  52. package/dist/src/Entity/ticket/Screen/ticket-search.d.ts +44 -0
  53. package/dist/src/Entity/ticket/Screen/ticket-search.js +97 -0
  54. package/dist/src/Entity/ticket/Screen/tickets.d.ts +9 -7
  55. package/dist/src/Entity/ticket/Screen/tickets.js +25 -85
  56. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +7 -0
  57. package/dist/src/Entity/ticket/Screen/user-tickets.js +30 -2
  58. package/dist/src/Kernel/renderer.d.ts +1 -3
  59. package/dist/src/Kernel/renderer.js +2 -5
  60. package/dist/src/Kernel/tui-kernel.js +7 -16
  61. package/dist/src/Screen/async-screen.d.ts +13 -0
  62. package/dist/src/Screen/async-screen.js +34 -5
  63. package/dist/src/Screen/list-screen.d.ts +11 -2
  64. package/dist/src/Screen/list-screen.js +20 -2
  65. package/dist/src/Screen/screen.d.ts +6 -2
  66. package/dist/src/Statistics/comparison-chart.d.ts +4 -2
  67. package/dist/src/Statistics/comparison-chart.js +14 -6
  68. package/dist/src/Statistics/figures.d.ts +17 -3
  69. package/dist/src/Statistics/figures.js +104 -41
  70. package/dist/src/Statistics/presentation.d.ts +3 -5
  71. package/dist/src/Statistics/presentation.js +3 -5
  72. package/dist/src/Statistics/screen.d.ts +16 -5
  73. package/dist/src/Statistics/screen.js +59 -26
  74. package/dist/src/lib/theme.d.ts +0 -56
  75. package/dist/src/lib/theme.js +0 -42
  76. package/dist/src/lib/vnode.d.ts +29 -0
  77. package/dist/src/lib/vnode.js +69 -0
  78. package/dist/src/route.js +37 -0
  79. package/dist/src/types.d.ts +5 -1
  80. package/dist/src/ui.d.ts +0 -1
  81. package/dist/src/ui.js +0 -1
  82. package/package.json +2 -2
  83. package/dist/src/Agent/Component/agent-list/index.d.ts +0 -6
  84. package/dist/src/Agent/Component/agent-list/index.js +0 -14
  85. package/dist/src/Component/agent-list/index.d.ts +0 -6
  86. package/dist/src/Component/agent-list/index.js +0 -14
  87. package/dist/src/Component/comment-item/index.d.ts +0 -21
  88. package/dist/src/Component/comment-item/index.js +0 -39
  89. package/dist/src/Component/create-form/index.d.ts +0 -3
  90. package/dist/src/Component/create-form/index.js +0 -26
  91. package/dist/src/Component/dashboard-list/index.d.ts +0 -8
  92. package/dist/src/Component/dashboard-list/index.js +0 -7
  93. package/dist/src/Component/project-picker/index.d.ts +0 -15
  94. package/dist/src/Component/project-picker/index.js +0 -42
  95. package/dist/src/Component/run-table/index.d.ts +0 -10
  96. package/dist/src/Component/run-table/index.js +0 -20
  97. package/dist/src/Component/tab-bar/index.d.ts +0 -20
  98. package/dist/src/Component/tab-bar/index.js +0 -42
  99. package/dist/src/Component/ticket-detail/index.d.ts +0 -38
  100. package/dist/src/Component/ticket-detail/index.js +0 -193
  101. package/dist/src/Component/ticket-table/index.d.ts +0 -5
  102. package/dist/src/Component/ticket-table/index.js +0 -14
  103. package/dist/src/Component/ticket-teaser/index.d.ts +0 -7
  104. package/dist/src/Component/ticket-teaser/index.js +0 -76
  105. package/dist/src/Deriver/liveness.d.ts +0 -12
  106. package/dist/src/Deriver/liveness.js +0 -34
  107. package/dist/src/Deriver/run-status.d.ts +0 -12
  108. package/dist/src/Deriver/run-status.js +0 -36
  109. package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +0 -25
  110. package/dist/src/Entity/conductor/Component/conductor-list/index.js +0 -60
  111. package/dist/src/Entity/project/Component/dashboard-list/index.d.ts +0 -8
  112. package/dist/src/Entity/project/Component/dashboard-list/index.js +0 -7
  113. package/dist/src/Entity/project/Component/project-list/index.d.ts +0 -8
  114. package/dist/src/Entity/project/Component/project-list/index.js +0 -7
  115. package/dist/src/Entity/project/Component/project-picker/index.d.ts +0 -15
  116. package/dist/src/Entity/project/Component/project-picker/index.js +0 -42
  117. package/dist/src/Entity/project/Screen/project-work.d.ts +0 -33
  118. package/dist/src/Entity/project/Screen/project-work.js +0 -114
  119. package/dist/src/Entity/run/Component/run-table/index.d.ts +0 -8
  120. package/dist/src/Entity/run/Component/run-table/index.js +0 -11
  121. package/dist/src/Entity/statistics/Data/statistics.d.ts +0 -147
  122. package/dist/src/Entity/statistics/Data/statistics.js +0 -235
  123. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +0 -70
  124. package/dist/src/Entity/statistics/Screen/dashboard.js +0 -216
  125. package/dist/src/Entity/statistics/render.d.ts +0 -6
  126. package/dist/src/Entity/statistics/render.js +0 -71
  127. package/dist/src/Entity/ticket/Component/tab-bar/index.d.ts +0 -20
  128. package/dist/src/Entity/ticket/Component/tab-bar/index.js +0 -71
  129. package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +0 -7
  130. package/dist/src/Entity/ticket/Component/ticket-table/index.js +0 -10
  131. package/dist/src/Entity/user/Screen/user-overview.d.ts +0 -33
  132. package/dist/src/Entity/user/Screen/user-overview.js +0 -80
  133. package/dist/src/Extension/contract.d.ts +0 -204
  134. package/dist/src/Extension/contract.js +0 -1
  135. package/dist/src/Kernel/react-body.d.ts +0 -9
  136. package/dist/src/Kernel/react-body.js +0 -42
  137. package/dist/src/Screen/async-react-screen.d.ts +0 -45
  138. package/dist/src/Screen/async-react-screen.js +0 -120
  139. package/dist/src/Screen/dashboard.d.ts +0 -1
  140. package/dist/src/Screen/dashboard.js +0 -1
  141. package/dist/src/Screen/project.d.ts +0 -1
  142. package/dist/src/Screen/project.js +0 -1
  143. package/dist/src/Screen/projects.d.ts +0 -1
  144. package/dist/src/Screen/projects.js +0 -1
  145. package/dist/src/Screen/react-screen.d.ts +0 -14
  146. package/dist/src/Screen/react-screen.js +0 -34
  147. package/dist/src/Screen/ticket.d.ts +0 -1
  148. package/dist/src/Screen/ticket.js +0 -1
  149. package/dist/src/Service/agent.d.ts +0 -17
  150. package/dist/src/Service/agent.js +0 -28
  151. package/dist/src/Service/create-form-data.d.ts +0 -10
  152. package/dist/src/Service/create-form-data.js +0 -52
  153. package/dist/src/Service/create-form.d.ts +0 -52
  154. package/dist/src/Service/create-form.js +0 -98
  155. package/dist/src/Service/project-data.d.ts +0 -48
  156. package/dist/src/Service/project-data.js +0 -92
  157. package/dist/src/Service/projects.d.ts +0 -21
  158. package/dist/src/Service/projects.js +0 -80
  159. package/dist/src/Service/route.d.ts +0 -11
  160. package/dist/src/Service/route.js +0 -215
  161. package/dist/src/Service/run-data.d.ts +0 -35
  162. package/dist/src/Service/run-data.js +0 -62
  163. package/dist/src/Service/ticket-data.d.ts +0 -6
  164. package/dist/src/Service/ticket-data.js +0 -7
  165. package/dist/src/Tui/controller.d.ts +0 -13
  166. package/dist/src/Tui/controller.js +0 -1794
  167. package/dist/src/Vendor/termcn/hooks/use-motion.d.ts +0 -5
  168. package/dist/src/Vendor/termcn/hooks/use-motion.js +0 -7
  169. package/dist/src/Vendor/termcn/hooks/use-theme.d.ts +0 -5
  170. package/dist/src/Vendor/termcn/hooks/use-theme.js +0 -10
  171. package/dist/src/Vendor/termcn/hooks/use-unicode.d.ts +0 -5
  172. package/dist/src/Vendor/termcn/hooks/use-unicode.js +0 -34
  173. package/dist/src/Vendor/termcn/lib/dither-chart-utils.d.ts +0 -130
  174. package/dist/src/Vendor/termcn/lib/dither-chart-utils.js +0 -357
  175. package/dist/src/Vendor/termcn/lib/dither-line-chart-utils.d.ts +0 -2
  176. package/dist/src/Vendor/termcn/lib/dither-line-chart-utils.js +0 -72
  177. package/dist/src/Vendor/termcn/lib/dither-radar-chart-utils.d.ts +0 -2
  178. package/dist/src/Vendor/termcn/lib/dither-radar-chart-utils.js +0 -113
  179. package/dist/src/Vendor/termcn/themes/default.d.ts +0 -1
  180. package/dist/src/Vendor/termcn/themes/default.js +0 -3
  181. package/dist/src/Vendor/termcn/ui/badge.d.ts +0 -13
  182. package/dist/src/Vendor/termcn/ui/badge.js +0 -33
  183. package/dist/src/Vendor/termcn/ui/bar-chart.d.ts +0 -16
  184. package/dist/src/Vendor/termcn/ui/bar-chart.js +0 -61
  185. package/dist/src/Vendor/termcn/ui/card.d.ts +0 -18
  186. package/dist/src/Vendor/termcn/ui/card.js +0 -7
  187. package/dist/src/Vendor/termcn/ui/dither-chart.d.ts +0 -118
  188. package/dist/src/Vendor/termcn/ui/dither-chart.js +0 -418
  189. package/dist/src/Vendor/termcn/ui/dither-line-chart.d.ts +0 -10
  190. package/dist/src/Vendor/termcn/ui/dither-line-chart.js +0 -11
  191. package/dist/src/Vendor/termcn/ui/dither-radar-chart.d.ts +0 -21
  192. package/dist/src/Vendor/termcn/ui/dither-radar-chart.js +0 -11
  193. package/dist/src/Vendor/termcn/ui/divider.d.ts +0 -14
  194. package/dist/src/Vendor/termcn/ui/divider.js +0 -25
  195. package/dist/src/Vendor/termcn/ui/gauge.d.ts +0 -12
  196. package/dist/src/Vendor/termcn/ui/gauge.js +0 -48
  197. package/dist/src/Vendor/termcn/ui/heat-map.d.ts +0 -13
  198. package/dist/src/Vendor/termcn/ui/heat-map.js +0 -59
  199. package/dist/src/Vendor/termcn/ui/key-value.d.ts +0 -16
  200. package/dist/src/Vendor/termcn/ui/key-value.js +0 -24
  201. package/dist/src/Vendor/termcn/ui/line-chart.d.ts +0 -15
  202. package/dist/src/Vendor/termcn/ui/line-chart.js +0 -102
  203. package/dist/src/Vendor/termcn/ui/markdown.d.ts +0 -24
  204. package/dist/src/Vendor/termcn/ui/markdown.js +0 -39
  205. package/dist/src/Vendor/termcn/ui/progress-bar.d.ts +0 -13
  206. package/dist/src/Vendor/termcn/ui/progress-bar.js +0 -13
  207. package/dist/src/Vendor/termcn/ui/sparkline.d.ts +0 -8
  208. package/dist/src/Vendor/termcn/ui/sparkline.js +0 -30
  209. package/dist/src/Vendor/termcn/ui/types.d.ts +0 -82
  210. package/dist/src/Vendor/termcn/ui/types.js +0 -1
  211. package/dist/src/lib/ansi.d.ts +0 -34
  212. package/dist/src/lib/ansi.js +0 -97
  213. package/dist/src/lib/hit-test.d.ts +0 -138
  214. package/dist/src/lib/hit-test.js +0 -138
  215. package/dist/src/lib/mouse.d.ts +0 -35
  216. package/dist/src/lib/mouse.js +0 -89
  217. package/dist/src/lib/table.d.ts +0 -37
  218. package/dist/src/lib/table.js +0 -20
  219. package/dist/src/lib/ticket-query.d.ts +0 -21
  220. package/dist/src/lib/ticket-query.js +0 -72
@@ -8,7 +8,6 @@ export interface TerminalShellInput {
8
8
  readonly output: typeof process.stdout;
9
9
  readonly trace: InputTrace | undefined;
10
10
  readonly interactive: boolean;
11
- readonly reactEnabled?: boolean;
12
11
  }
13
12
  /**
14
13
  * The terminal, as the kernel holds it.
@@ -22,6 +21,5 @@ export interface TerminalShell {
22
21
  readonly renderer: CliRenderer | undefined;
23
22
  readonly shell: BoxRenderable | undefined;
24
23
  readonly ownsRenderer: boolean;
25
- readonly reactBody: import('./react-body.js').ReactBody | undefined;
26
24
  }
27
- export declare function createTerminalShell({ options, input, output, trace, interactive, reactEnabled, }: TerminalShellInput): Promise<TerminalShell>;
25
+ export declare function createTerminalShell({ options, input, output, trace, interactive, }: TerminalShellInput): Promise<TerminalShell>;
@@ -47,7 +47,7 @@ import { DEFAULT_BACKGROUND } from '../lib/theme.js';
47
47
  * the alternate one changes what the operator's terminal shows after `q`.
48
48
  */
49
49
  const HOME_AND_CLEAR = '\x1b[H\x1b[2J';
50
- export async function createTerminalShell({ options, input, output, trace, interactive, reactEnabled = false, }) {
50
+ export async function createTerminalShell({ options, input, output, trace, interactive, }) {
51
51
  /**
52
52
  * GAIA-326 — the ONE renderer that owns the terminal.
53
53
  *
@@ -123,9 +123,6 @@ export async function createTerminalShell({ options, input, output, trace, inter
123
123
  * kernel computed every row's `top` itself, which is how a framework bought
124
124
  * partly for its layout engine ended up using none of it.
125
125
  */
126
- const reactBody = renderer && reactEnabled
127
- ? (await import('./react-body.js')).createReactBody(renderer)
128
- : undefined;
129
126
  const shell = renderer
130
127
  ? new BoxRenderable(renderer, {
131
128
  id: 'shell',
@@ -141,5 +138,5 @@ export async function createTerminalShell({ options, input, output, trace, inter
141
138
  : undefined;
142
139
  if (renderer && shell)
143
140
  renderer.root.add(shell);
144
- return { renderer, shell, ownsRenderer, reactBody };
141
+ return { renderer, shell, ownsRenderer };
145
142
  }
@@ -7,7 +7,6 @@ import { createRefreshSignal, } from '../lib/refresh-signal.js';
7
7
  import { scrollTo } from '../lib/scroll.js';
8
8
  import { Router } from '../Routing/router.js';
9
9
  import { bootLocationFor, buildRoutes, WORKSPACE_PICKER_ROUTE_ID, } from '../Routing/routes.js';
10
- import { ReactScreen } from '../Screen/react-screen.js';
11
10
  import { timerDebounce } from '../Widget/option-source.js';
12
11
  import { bodyRowsFor, chromeInputFor, FOOTER_SPINNER_MS, frameTrees, modalBoxWidth, NOTICE_ANIM_MS, } from './chrome.js';
13
12
  import { clipboardWriter } from './clipboard.js';
@@ -88,13 +87,12 @@ export async function runTicketTui(_document, _context, services, options = {})
88
87
  // over both and the router paints on its first `navigate`. GAIA-382 moved the
89
88
  // bring-up itself to `Kernel/renderer.ts`, rationale included: it reads five
90
89
  // values and returns a terminal, so it never belonged to this closure.
91
- const { renderer, shell, ownsRenderer, reactBody } = await createTerminalShell({
90
+ const { renderer, shell, ownsRenderer } = await createTerminalShell({
92
91
  options,
93
92
  input,
94
93
  output,
95
94
  trace,
96
95
  interactive,
97
- reactEnabled: [...session.screens().values()].some((screen) => screen instanceof ReactScreen),
98
96
  });
99
97
  /**
100
98
  * The selection this kernel last scrolled into view.
@@ -122,6 +120,11 @@ export async function runTicketTui(_document, _context, services, options = {})
122
120
  store: createSessionStore(),
123
121
  options,
124
122
  copyToClipboard: clipboardWriter(renderer),
123
+ // Read when a context is built — once per present — so a screen sees the
124
+ // width of the frame it is drawing. The pipe has no terminal to measure.
125
+ get terminalColumns() {
126
+ return interactive ? terminalColumns() : undefined;
127
+ },
125
128
  };
126
129
  let router;
127
130
  const context = () => router.context();
@@ -209,15 +212,10 @@ export async function runTicketTui(_document, _context, services, options = {})
209
212
  },
210
213
  });
211
214
  };
212
- let reactVisit = -1;
213
215
  const draw = () => {
214
216
  const bodyBox = regions.body;
215
217
  if (closing || !renderer || !shell || !bodyBox || !active())
216
218
  return;
217
- if (reactVisit !== router.visitCount) {
218
- reactBody?.clear();
219
- reactVisit = router.visitCount;
220
- }
221
219
  regions.retire(router.visitCount);
222
220
  const shown = active()?.present(context());
223
221
  if (!shown)
@@ -280,11 +278,6 @@ export async function runTicketTui(_document, _context, services, options = {})
280
278
  slot.width = id === 'body' ? '100%' : rowWidth;
281
279
  regions.patch(id, next);
282
280
  }
283
- const reactHost = regions.treeOf('body')?.byId.get('react-screen-host');
284
- if (reactHost && shown.reactBody !== undefined && !modal)
285
- reactBody?.show(reactHost, shown.reactBody, router.visitCount);
286
- else
287
- reactBody?.clear();
288
281
  // Keep the cursor in view — when it MOVES. See `scrolledSelection`.
289
282
  //
290
283
  // GAIA-340 Task 12 — AFTER the patch, and asking the LAYOUT rather than the
@@ -300,8 +293,7 @@ export async function runTicketTui(_document, _context, services, options = {})
300
293
  if (!modal &&
301
294
  selectedRowId !== undefined &&
302
295
  selection !== scrolledSelection) {
303
- const row = regions.treeOf('body')?.byId.get(selectedRowId) ??
304
- reactHost?.findDescendantById(selectedRowId);
296
+ const row = regions.treeOf('body')?.byId.get(selectedRowId);
305
297
  if (row) {
306
298
  renderer.root.calculateLayout();
307
299
  // `lib/scroll.ts` — the clamp the autocomplete already scrolls by, fed
@@ -405,7 +397,6 @@ export async function runTicketTui(_document, _context, services, options = {})
405
397
  // them, so `regions` takes them down. The shell is the kernel's, and it
406
398
  // goes last: destroying a parent before its children leaves their Yoga
407
399
  // nodes unfreed.
408
- reactBody?.destroy();
409
400
  regions.destroy();
410
401
  renderer.root.remove(shell);
411
402
  shell.destroy();
@@ -54,6 +54,12 @@ export declare abstract class AsyncScreen<T> extends BaseScreen {
54
54
  protected abstract renderReady(data: T, context: ScreenContext): VNode;
55
55
  protected renderError(error: Error): string[];
56
56
  protected didLoad(_data: T, _context: ScreenContext): void;
57
+ /**
58
+ * Whether the body sits flush against the frame. A document screen that
59
+ * reads as content inside the frame returns `false` for the body-window
60
+ * inset every framed row surface used to have.
61
+ */
62
+ protected flushBody(): boolean;
57
63
  /** Optional filter chips above the FRAME. */
58
64
  protected badges(_context: ScreenContext): readonly ListFilterBadge[];
59
65
  /** title / meta / header / selection — not footer. */
@@ -74,6 +80,13 @@ export declare abstract class AsyncScreen<T> extends BaseScreen {
74
80
  */
75
81
  protected actionStillCurrent(guard: LifecycleGuard, context: ScreenContext): boolean;
76
82
  protected replaceReadyIfCurrent(guard: LifecycleGuard, data: T, context: ScreenContext): boolean;
83
+ /**
84
+ * Re-read over the ready surface, superseding any read still in flight.
85
+ * Resolves `false` when a newer read or a local mutation overtook this one;
86
+ * rejects only while it is still the current read, so a stale failure
87
+ * cannot overwrite a newer answer.
88
+ */
89
+ protected reloadInPlace(context: ScreenContext): Promise<boolean>;
77
90
  protected requestActiveDraw(context: ScreenContext): boolean;
78
91
  enter(context: ScreenContext): void;
79
92
  retry(context: ScreenContext): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { filterBadgeStrip, } from '../Component/list-chrome/index.js';
2
+ import { serverMessage } from '../lib/jsonapi-error.js';
2
3
  import { columnNode, messageTree } from '../lib/vnode.js';
3
4
  import { BaseScreen, presentation, } from './screen.js';
4
5
  /**
@@ -46,6 +47,14 @@ export class AsyncScreen extends BaseScreen {
46
47
  return [error.message, '', 'Press Enter to retry.'];
47
48
  }
48
49
  didLoad(_data, _context) { }
50
+ /**
51
+ * Whether the body sits flush against the frame. A document screen that
52
+ * reads as content inside the frame returns `false` for the body-window
53
+ * inset every framed row surface used to have.
54
+ */
55
+ flushBody() {
56
+ return true;
57
+ }
49
58
  /** Optional filter chips above the FRAME. */
50
59
  badges(_context) {
51
60
  return [];
@@ -95,6 +104,28 @@ export class AsyncScreen extends BaseScreen {
95
104
  this.state = 'ready';
96
105
  return true;
97
106
  }
107
+ /**
108
+ * Re-read over the ready surface, superseding any read still in flight.
109
+ * Resolves `false` when a newer read or a local mutation overtook this one;
110
+ * rejects only while it is still the current read, so a stale failure
111
+ * cannot overwrite a newer answer.
112
+ */
113
+ async reloadInPlace(context) {
114
+ const guard = this.captureLifecycle(context, true);
115
+ let data;
116
+ try {
117
+ data = await this.load(context);
118
+ }
119
+ catch (caught) {
120
+ if (!this.lifecycleIsCurrent(guard, context))
121
+ return false;
122
+ throw caught;
123
+ }
124
+ if (!this.replaceReadyIfCurrent(guard, data, context))
125
+ return false;
126
+ this.requestActiveDraw(context);
127
+ return true;
128
+ }
98
129
  requestActiveDraw(context) {
99
130
  if (!context.isActive()) {
100
131
  this.staleRedrawCount++;
@@ -159,13 +190,11 @@ export class AsyncScreen extends BaseScreen {
159
190
  return;
160
191
  this.refreshing = false;
161
192
  if (keepSurface) {
162
- this.toast =
163
- caught instanceof Error ? caught.message : String(caught);
193
+ this.toast = serverMessage(caught);
164
194
  this.requestActiveDraw(context);
165
195
  return;
166
196
  }
167
- this.failure =
168
- caught instanceof Error ? caught : new Error(String(caught));
197
+ this.failure = new Error(serverMessage(caught));
169
198
  this.state = 'error';
170
199
  this.requestActiveDraw(context);
171
200
  });
@@ -204,7 +233,7 @@ export class AsyncScreen extends BaseScreen {
204
233
  }
205
234
  children.push(this.surface(context));
206
235
  return presentation(columnNode(`${id}-body`, ...children), {
207
- flush: true,
236
+ flush: this.flushBody(),
208
237
  ...this.chrome(context),
209
238
  ...(actions.length > 0 ? { actions } : {}),
210
239
  ...(notice !== undefined && notice !== '' ? { status: notice } : {}),
@@ -14,7 +14,7 @@ import type { ScreenContext } from './screen.js';
14
14
  * enough hooks to be a framework.
15
15
  *
16
16
  * It does not claim keys from `handleInput` either. The current dispatch ORDER is
17
- * load-bearing: `←`/`→` sit after the ticket list's `searchMode` branch, where the
17
+ * load-bearing: `←`/`→` sit after the ticket list's `/` search branch, where the
18
18
  * arrows are inert while the `/` field is open. A base that claimed them first
19
19
  * would make them page there — a behaviour change, and GAIA-294 is
20
20
  * behaviour-neutral. So the dispatch TABLE lives here once and each subclass calls
@@ -93,7 +93,7 @@ export declare abstract class ListScreen<Row extends {
93
93
  * `←` forwards too (GAIA-292 R1).
94
94
  *
95
95
  * WHERE it is called stays the subclass's decision (GAIA-294 D3) — on the ticket
96
- * list it must sit after the `searchMode` branch, where the arrows are inert
96
+ * list it must sit after the `/` search branch, where the arrows are inert
97
97
  * while the `/` field is open.
98
98
  */
99
99
  protected handlePagingInput(input: string, context: ScreenContext): boolean;
@@ -103,6 +103,15 @@ export declare abstract class ListScreen<Row extends {
103
103
  * chain continues.
104
104
  */
105
105
  protected handleCursorInput(input: string, context: ScreenContext): boolean;
106
+ /**
107
+ * Show `rows` in place of the loaded page until the next read answers — the
108
+ * optimistic narrowing of a search. It keeps the LOADED page number so a
109
+ * keystroke does not flip `page N`; the total becomes the explicit unknown
110
+ * (`N of ?`); a narrowing that dropped rows stops the window positioning
111
+ * the survivors (GAIA-312 D4). An emptied list (a refusal) also drops
112
+ * `hasNext`, so `→` cannot re-send the refused query.
113
+ */
114
+ showRows(rows: readonly Row[]): void;
106
115
  /** `page N` for the strip above the table (count sits beside it). */
107
116
  protected pagingSegments(): string[];
108
117
  /**
@@ -24,7 +24,7 @@ import { AsyncScreen } from './async-screen.js';
24
24
  * enough hooks to be a framework.
25
25
  *
26
26
  * It does not claim keys from `handleInput` either. The current dispatch ORDER is
27
- * load-bearing: `←`/`→` sit after the ticket list's `searchMode` branch, where the
27
+ * load-bearing: `←`/`→` sit after the ticket list's `/` search branch, where the
28
28
  * arrows are inert while the `/` field is open. A base that claimed them first
29
29
  * would make them page there — a behaviour change, and GAIA-294 is
30
30
  * behaviour-neutral. So the dispatch TABLE lives here once and each subclass calls
@@ -92,7 +92,7 @@ export class ListScreen extends AsyncScreen {
92
92
  * `←` forwards too (GAIA-292 R1).
93
93
  *
94
94
  * WHERE it is called stays the subclass's decision (GAIA-294 D3) — on the ticket
95
- * list it must sit after the `searchMode` branch, where the arrows are inert
95
+ * list it must sit after the `/` search branch, where the arrows are inert
96
96
  * while the `/` field is open.
97
97
  */
98
98
  handlePagingInput(input, context) {
@@ -116,6 +116,24 @@ export class ListScreen extends AsyncScreen {
116
116
  context.requestDraw();
117
117
  return true;
118
118
  }
119
+ /**
120
+ * Show `rows` in place of the loaded page until the next read answers — the
121
+ * optimistic narrowing of a search. It keeps the LOADED page number so a
122
+ * keystroke does not flip `page N`; the total becomes the explicit unknown
123
+ * (`N of ?`); a narrowing that dropped rows stops the window positioning
124
+ * the survivors (GAIA-312 D4). An emptied list (a refusal) also drops
125
+ * `hasNext`, so `→` cannot re-send the refused query.
126
+ */
127
+ showRows(rows) {
128
+ const current = this.readyValue;
129
+ this.replaceReady({
130
+ hasNext: rows.length === 0 ? false : (current?.hasNext ?? false),
131
+ page: this.loadedPage,
132
+ total: null,
133
+ window: current && rows.length === current.rows.length ? current.window : null,
134
+ rows: [...rows],
135
+ });
136
+ }
119
137
  /** `page N` for the strip above the table (count sits beside it). */
120
138
  pagingSegments() {
121
139
  return [`page ${this.loadedPage + 1}`];
@@ -25,8 +25,6 @@ export interface ScreenPresentation {
25
25
  * the id the kernel mounts the body slot's tree under.
26
26
  */
27
27
  readonly body: VNode;
28
- /** Optional React-owned body; native patching never traverses its children. */
29
- readonly reactBody?: import('react').ReactNode;
30
28
  readonly selectable: number;
31
29
  readonly selectedIndex: number;
32
30
  /**
@@ -189,6 +187,12 @@ export interface ScreenContext {
189
187
  * Absent on the non-interactive path, which has no terminal at all.
190
188
  */
191
189
  readonly copyToClipboard?: import('../types.js').ClipboardWriter;
190
+ /**
191
+ * The terminal's width in columns, as the kernel last measured it — the one
192
+ * geometry fact a layout breakpoint needs before Yoga has run. Absent on the
193
+ * non-interactive path, which has no terminal; a reader takes a default.
194
+ */
195
+ readonly terminalColumns?: number | undefined;
192
196
  }
193
197
  export interface Screen {
194
198
  readonly id: ScreenId;
@@ -1,5 +1,7 @@
1
+ import { type VNode } from '@opentui/core';
1
2
  import type { StatisticFigure } from './client.js';
2
- export declare function ComparisonChart({ figure, width, }: {
3
+ export declare function ComparisonChart({ id, figure, width, }: {
4
+ id: string;
3
5
  figure: StatisticFigure;
4
6
  width: number;
5
- }): import("react").ReactNode;
7
+ }): VNode;
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
1
+ import { BoxRenderable, h } from '@opentui/core';
2
2
  import { FOREGROUND, SERIES_A, SERIES_B, SERIES_C, SERIES_D, SERIES_E, SERIES_F, } from '../lib/theme.js';
3
- import { BOLD } from '../lib/vnode.js';
3
+ import { BOLD, paragraphNode, plainRun } from '../lib/vnode.js';
4
4
  import { displayWidth, truncateToWidth, wrapToWidth } from '../lib/width.js';
5
5
  import { formatAxisMagnitude } from './presentation.js';
6
6
  const MIN_PLOT_ROWS = 8;
@@ -20,11 +20,13 @@ const CROSS = '┼';
20
20
  const INVALID_MESSAGE = 'Comparison data is invalid.';
21
21
  const EMPTY_MESSAGE = 'No comparison values for this period.';
22
22
  const NARROW_MESSAGE = 'Plot area is too narrow to draw this comparison.';
23
- export function ComparisonChart({ figure, width, }) {
23
+ export function ComparisonChart({ id, figure, width, }) {
24
24
  const rows = layoutChart(figure, Math.max(1, width));
25
- return (_jsx("box", { flexDirection: "column", flexShrink: 0, children: rows.map((row, index) => (_jsx(ChartRow, { cells: row }, index))) }));
25
+ // A chart is a fixed grid of rows: the row index is its identity.
26
+ return h(BoxRenderable, { id, flexDirection: 'column', flexShrink: 0 }, ...rows.map((row, index) => chartRow(`${id}-${index}`, row)));
26
27
  }
27
- function ChartRow({ cells }) {
28
+ /** One plot row as one text, its cells merged into runs of equal style. */
29
+ function chartRow(id, cells) {
28
30
  const runs = [];
29
31
  for (const cell of cells) {
30
32
  const last = runs.at(-1);
@@ -33,7 +35,13 @@ function ChartRow({ cells }) {
33
35
  else
34
36
  runs.push({ text: cell.ch, fg: cell.fg, bold: cell.bold });
35
37
  }
36
- return (_jsx("box", { flexDirection: "row", height: 1, flexShrink: 0, children: runs.map((run, index) => (_jsx("text", { fg: run.fg, ...(run.bold ? { attributes: BOLD } : {}), wrapMode: "none", children: run.text }, index))) }));
38
+ return paragraphNode({
39
+ id,
40
+ runs: runs.map((run) => plainRun(run.text, run.bold ? BOLD : 0, run.fg)),
41
+ fg: FOREGROUND,
42
+ wrapMode: 'none',
43
+ flexShrink: 0,
44
+ });
37
45
  }
38
46
  function layoutChart(figure, width) {
39
47
  const styles = seriesStyles(figure.measurements);
@@ -1,4 +1,18 @@
1
+ import { type VNode } from '@opentui/core';
1
2
  import type { StatisticFigureGroup } from './client.js';
2
- export declare function StatisticsFigures({ groups, }: {
3
- groups: readonly StatisticFigureGroup[];
4
- }): import("react").ReactNode;
3
+ export interface StatisticsFiguresProps {
4
+ /** The grid's own id; every group and figure inside derives from it. */
5
+ readonly id: string;
6
+ readonly groups: readonly StatisticFigureGroup[];
7
+ /** The grid's laid-out width, as last reported through {@link onResize}. */
8
+ readonly width: number;
9
+ /** Called with the grid's new width whenever the layout resizes it. */
10
+ readonly onResize: (width: number) => void;
11
+ }
12
+ /**
13
+ * The figure groups as cards. The charts are laid out for `width` — the
14
+ * screen's last measurement of this grid — and the grid reports its real
15
+ * width after every layout, so a first estimate is corrected on the next draw
16
+ * instead of the charts waiting for a measurement to appear at all.
17
+ */
18
+ export declare function StatisticsFigures({ id, groups, width, onResize, }: StatisticsFiguresProps): VNode;
@@ -1,16 +1,24 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { FOREGROUND } from '../lib/theme.js';
4
- import { BarChart } from '../Vendor/termcn/ui/bar-chart.js';
5
- import { Card } from '../Vendor/termcn/ui/card.js';
6
- import { HeatMap } from '../Vendor/termcn/ui/heat-map.js';
1
+ import { BoxRenderable, h } from '@opentui/core';
2
+ import { BarChart } from '../Component/bar-chart/index.js';
3
+ import { Card } from '../Component/card/index.js';
4
+ import { HeatMap } from '../Component/heat-map/index.js';
5
+ import { FOREGROUND, SERIES_A, SERIES_B, SERIES_C, SERIES_D, } from '../lib/theme.js';
6
+ import { BOLD, paragraphNode, plainRun } from '../lib/vnode.js';
7
7
  import { ComparisonChart } from './comparison-chart.js';
8
8
  import { formatAxisMagnitude } from './presentation.js';
9
- const COLORS = ['blue', 'purple', 'orange', 'green'];
9
+ // Per-measurement bar colours: the comparison-series slots, in the order the
10
+ // line chart uses them, so a measurement keeps its colour across widgets.
11
+ const COLORS = [SERIES_A, SERIES_B, SERIES_C, SERIES_D];
10
12
  const valueAt = (figure, measurement, key) => figure.measurements[measurement]?.values[key] ?? null;
11
13
  const numeric = (value) => value ?? 0;
12
14
  const shown = (value) => value === null ? '—' : String(value);
13
- function MatrixDisplay({ figure, width, }) {
15
+ const text = (id, content, attributes = 0) => paragraphNode({
16
+ id,
17
+ runs: [plainRun(content, attributes)],
18
+ fg: FOREGROUND,
19
+ });
20
+ const column = (id, gap, ...children) => h(BoxRenderable, { id, flexDirection: 'column', gap }, ...children);
21
+ function matrixDisplay(id, figure, width) {
14
22
  // Categories are rows, measurements are columns — matching the web UI's
15
23
  // matrix orientation (docs/notes/statistics.md).
16
24
  const rawValues = figure.categories.map((category) => figure.measurements.map((_, measurement) => numeric(valueAt(figure, measurement, category.key))));
@@ -19,7 +27,7 @@ function MatrixDisplay({ figure, width, }) {
19
27
  // whole matrix's maximum, so columns with a smaller value range still show
20
28
  // visible contrast. HeatMap itself only supports one global min/max, so the
21
29
  // per-column scaling happens here: values are rescaled to 0-100 percent of
22
- // their column's own max before being handed to the (untouched) component.
30
+ // their column's own max before being handed to the HeatMap module.
23
31
  const columnMaximums = figure.measurements.map((_, measurement) => Math.max(0, ...rawValues.map((row) => row[measurement] ?? 0)));
24
32
  const shadeValues = rawValues.map((row) => row.map((value, column) => {
25
33
  const columnMax = columnMaximums[column] ?? 0;
@@ -28,7 +36,18 @@ function MatrixDisplay({ figure, width, }) {
28
36
  const labelWidth = Math.max(1, ...figure.categories.map((category) => category.label.length));
29
37
  const cellWidth = Math.max(7, Math.floor((Math.max(width, 20) - labelWidth - 2) /
30
38
  Math.max(1, figure.measurements.length)));
31
- return (_jsx(HeatMap, { data: shadeValues, rowLabels: figure.categories.map((category) => category.label), colLabels: figure.measurements.map((measurement) => measurement.label), cellLabels: labels, cellWidth: cellWidth, showValues: true, showLegend: false, minValue: 0, maxValue: 100 }));
39
+ return HeatMap({
40
+ id,
41
+ data: shadeValues,
42
+ rowLabels: figure.categories.map((category) => category.label),
43
+ colLabels: figure.measurements.map((measurement) => measurement.label),
44
+ cellLabels: labels,
45
+ cellWidth,
46
+ showValues: true,
47
+ showLegend: false,
48
+ minValue: 0,
49
+ maxValue: 100,
50
+ });
32
51
  }
33
52
  function partialPeriodUnit(figure) {
34
53
  return figure.filters.period?.value === '365' ? 'month' : 'day';
@@ -39,13 +58,19 @@ function partialPeriodUnit(figure) {
39
58
  // figure's own words. Rendered once here rather than per-widget so every
40
59
  // shipped widget (matrix, grouped_horizontal_bars) gets both without having
41
60
  // to know about them.
42
- function FigureCaption({ figure }) {
61
+ function figureCaption(id, figure) {
43
62
  const partial = figure.categories.some((category) => category.partial);
44
63
  if (!partial && !figure.description)
45
64
  return null;
46
- return (_jsxs("box", { flexDirection: "column", children: [figure.description && _jsx("text", { fg: FOREGROUND, children: figure.description }), partial && (_jsxs("text", { fg: FOREGROUND, children: ["Current ", partialPeriodUnit(figure), " partial (UTC)"] }))] }));
65
+ return column(id, 0, ...(figure.description
66
+ ? [text(`${id}-description`, figure.description)]
67
+ : []), ...(partial
68
+ ? [
69
+ text(`${id}-partial`, `Current ${partialPeriodUnit(figure)} partial (UTC)`),
70
+ ]
71
+ : []));
47
72
  }
48
- function GroupedHorizontalBars({ figure, width, }) {
73
+ function groupedHorizontalBars(id, figure, width) {
49
74
  const maximum = Math.max(0, ...figure.categories.flatMap((category) => figure.measurements.map((measurement) => numeric(measurement.values[category.key] ?? null))));
50
75
  // Share = this category's slice of the measurement's total across every
51
76
  // category (e.g. one model's percent of total token spend) — every
@@ -63,37 +88,75 @@ function GroupedHorizontalBars({ figure, width, }) {
63
88
  // into the label-width budget the bar itself needs, which is what let long
64
89
  // category (model) labels crowd out or collapse against the bar. Elide it
65
90
  // — the category heading above already names the row.
91
+ const single = figure.measurements[0];
66
92
  const singleMeasurement = figure.measurements.length === 1;
67
- return (_jsx("box", { flexDirection: "column", gap: 1, children: figure.categories.map((category) => (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { children: _jsx("b", { children: category.label }) }), _jsx(BarChart, { data: figure.measurements.map((measurement, index) => {
68
- const value = numeric(measurement.values[category.key] ?? null);
69
- return {
70
- label: singleMeasurement
71
- ? ''
72
- : `${measurement.label}${shareOf(measurement.id, value)}`,
73
- value,
74
- color: COLORS[index % COLORS.length] ?? 'blue',
75
- };
76
- }), direction: "horizontal", maxValue: maximum,
77
- // The vendored chart's own showValues renders the raw number,
78
- // which is unreadable at token-spend (billion) scale — the
79
- // formatted line below (or inline per-row text, for multiple
80
- // measurements) replaces it.
81
- showValues: false, width: width }), !singleMeasurement && (_jsx("box", { flexDirection: "row", gap: 2, children: figure.measurements.map((measurement, index) => (_jsxs("text", { children: [measurement.label, ":", ' ', formatAxisMagnitude(numeric(valueAt(figure, index, category.key)))] }, measurement.id))) })), singleMeasurement && figure.measurements[0] && (_jsxs("text", { children: [formatAxisMagnitude(numeric(valueAt(figure, 0, category.key))), shareOf(figure.measurements[0].id, numeric(valueAt(figure, 0, category.key)))] }))] }, category.key))) }));
93
+ return column(id, 1, ...figure.categories.map((category) => {
94
+ const row = `${id}-${category.key}`;
95
+ return column(row, 0, text(`${row}-label`, category.label, BOLD), BarChart({
96
+ id: `${row}-bars`,
97
+ data: figure.measurements.map((measurement, index) => {
98
+ const value = numeric(measurement.values[category.key] ?? null);
99
+ return {
100
+ label: singleMeasurement
101
+ ? ''
102
+ : `${measurement.label}${shareOf(measurement.id, value)}`,
103
+ value,
104
+ color: COLORS[index % COLORS.length],
105
+ };
106
+ }),
107
+ maxValue: maximum,
108
+ // The bar chart's own showValues renders the raw number,
109
+ // which is unreadable at token-spend (billion) scale — the
110
+ // formatted line below (or inline per-row text, for multiple
111
+ // measurements) replaces it.
112
+ showValues: false,
113
+ width,
114
+ }), singleMeasurement && single
115
+ ? text(`${row}-value`, `${formatAxisMagnitude(numeric(valueAt(figure, 0, category.key)))}${shareOf(single.id, numeric(valueAt(figure, 0, category.key)))}`)
116
+ : h(BoxRenderable, { id: `${row}-values`, flexDirection: 'row', gap: 2 }, ...figure.measurements.map((measurement, index) => text(`${row}-value-${measurement.id}`, `${measurement.label}: ${formatAxisMagnitude(numeric(valueAt(figure, index, category.key)))}`))));
117
+ }));
82
118
  }
83
- function FigureDisplay({ figure, width, }) {
119
+ function figureDisplay(id, figure, width) {
84
120
  if (figure.empty.is_empty)
85
- return _jsx("text", { children: figure.empty.message ?? 'No data for this selection.' });
86
- return (_jsxs("box", { flexDirection: "column", gap: 1, children: [_jsx(FigureCaption, { figure: figure }), figure.widget === 'matrix' && (_jsx(MatrixDisplay, { figure: figure, width: width })), figure.widget === 'line' && (_jsx(ComparisonChart, { figure: figure, width: width })), figure.widget === 'grouped_horizontal_bars' && (_jsx(GroupedHorizontalBars, { figure: figure, width: width }))] }));
121
+ return text(`${id}-empty`, figure.empty.message ?? 'No data for this selection.');
122
+ const caption = figureCaption(`${id}-caption`, figure);
123
+ const widget = figure.widget === 'matrix'
124
+ ? matrixDisplay(`${id}-matrix`, figure, width)
125
+ : figure.widget === 'line'
126
+ ? ComparisonChart({ id: `${id}-line`, figure, width })
127
+ : groupedHorizontalBars(`${id}-bars`, figure, width);
128
+ return column(id, 1, ...(caption === null ? [] : [caption]), widget);
87
129
  }
88
- export function StatisticsFigures({ groups, }) {
89
- const [width, setWidth] = useState(0);
90
- function measure() {
91
- setWidth(this.width);
92
- }
130
+ /**
131
+ * The figure groups as cards. The charts are laid out for `width` — the
132
+ * screen's last measurement of this grid — and the grid reports its real
133
+ * width after every layout, so a first estimate is corrected on the next draw
134
+ * instead of the charts waiting for a measurement to appear at all.
135
+ */
136
+ export function StatisticsFigures({ id, groups, width, onResize, }) {
93
137
  const contentWidth = Math.max(1, width - 2);
94
- return (_jsx("box", { id: "statistics-grid", width: "100%", paddingLeft: 1, paddingRight: 1, flexDirection: "column", gap: 0, onSizeChange: measure, ref: (node) => {
95
- if (node)
96
- setWidth(node.width);
97
- }, children: width > 0 &&
98
- groups.map((group) => (_jsxs("box", { width: "100%", flexDirection: "column", gap: 0, children: [_jsx("box", { height: 1 }), _jsx("text", { children: _jsx("b", { children: group.label }) }), group.figures.map((figure) => (_jsx("box", { id: `figure-${figure.id}`, width: contentWidth, flexShrink: 0, flexDirection: "column", children: _jsx(Card, { title: figure.label, width: contentWidth, paddingX: 1, children: _jsx("box", { flexDirection: "column", gap: 1, children: _jsx(FigureDisplay, { figure: figure, width: Math.max(15, contentWidth - 4) }) }) }) }, figure.id)))] }, group.id))) }));
138
+ return h(BoxRenderable, {
139
+ id,
140
+ width: '100%',
141
+ paddingLeft: 1,
142
+ paddingRight: 1,
143
+ flexDirection: 'column',
144
+ gap: 0,
145
+ onSizeChange() {
146
+ onResize(this.width);
147
+ },
148
+ }, ...groups.map((group) => column(`${id}-${group.id}`, 0, h(BoxRenderable, { id: `${id}-${group.id}-spacer`, height: 1 }), text(`${id}-${group.id}-label`, group.label, BOLD), ...group.figures.map((figure) => h(BoxRenderable, {
149
+ id: `figure-${figure.id}`,
150
+ width: contentWidth,
151
+ flexShrink: 0,
152
+ flexDirection: 'column',
153
+ }, Card({
154
+ id: `figure-${figure.id}-card`,
155
+ title: figure.label,
156
+ width: contentWidth,
157
+ paddingX: 1,
158
+ children: [
159
+ column(`figure-${figure.id}-display`, 1, figureDisplay(`figure-${figure.id}-widget`, figure, Math.max(15, contentWidth - 4))),
160
+ ],
161
+ }))))));
99
162
  }
@@ -1,10 +1,8 @@
1
1
  import type { StatisticFigureGroup } from './client.js';
2
2
  /**
3
- * Formats a y-axis magnitude with the correct thousands/millions/billions
4
- * suffix — the vendored dither-chart default formatter only ever divides
5
- * once (`value / 1000`), so 844,000,000 renders as "844.0k" instead of
6
- * "844.0M". This formatter is passed in as a `tickFormatter` call-site
7
- * override; the vendored chart lib itself is untouched.
3
+ * Formats a magnitude with the correct thousands/millions/billions suffix,
4
+ * dividing as often as the size needs: 844,000,000 is "844.0M", never the
5
+ * "844.0k" a single division by 1000 gives.
8
6
  */
9
7
  export declare function formatAxisMagnitude(value: number): string;
10
8
  export declare function groupedFigureLines(groups: readonly StatisticFigureGroup[]): string[];
@@ -1,10 +1,8 @@
1
1
  const displayValue = (value) => value === null || value === undefined ? '—' : String(value);
2
2
  /**
3
- * Formats a y-axis magnitude with the correct thousands/millions/billions
4
- * suffix — the vendored dither-chart default formatter only ever divides
5
- * once (`value / 1000`), so 844,000,000 renders as "844.0k" instead of
6
- * "844.0M". This formatter is passed in as a `tickFormatter` call-site
7
- * override; the vendored chart lib itself is untouched.
3
+ * Formats a magnitude with the correct thousands/millions/billions suffix,
4
+ * dividing as often as the size needs: 844,000,000 is "844.0M", never the
5
+ * "844.0k" a single division by 1000 gives.
8
6
  */
9
7
  export function formatAxisMagnitude(value) {
10
8
  const sign = value < 0 ? '-' : '';