@f0rbit/overview 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/overview +10 -0
  2. package/dist/overview.js +10361 -0
  3. package/package.json +22 -15
  4. package/bunfig.toml +0 -7
  5. package/packages/core/__tests__/concurrency.test.ts +0 -111
  6. package/packages/core/__tests__/helpers.ts +0 -60
  7. package/packages/core/__tests__/integration/git-status.test.ts +0 -62
  8. package/packages/core/__tests__/integration/scanner.test.ts +0 -140
  9. package/packages/core/__tests__/ocn.test.ts +0 -164
  10. package/packages/core/package.json +0 -13
  11. package/packages/core/src/cache.ts +0 -31
  12. package/packages/core/src/concurrency.ts +0 -44
  13. package/packages/core/src/devpad.ts +0 -61
  14. package/packages/core/src/git-graph.ts +0 -54
  15. package/packages/core/src/git-stats.ts +0 -201
  16. package/packages/core/src/git-status.ts +0 -316
  17. package/packages/core/src/github.ts +0 -286
  18. package/packages/core/src/index.ts +0 -58
  19. package/packages/core/src/ocn.ts +0 -74
  20. package/packages/core/src/scanner.ts +0 -118
  21. package/packages/core/src/types.ts +0 -199
  22. package/packages/core/src/watcher.ts +0 -128
  23. package/packages/core/src/worktree.ts +0 -80
  24. package/packages/core/tsconfig.json +0 -5
  25. package/packages/render/bunfig.toml +0 -8
  26. package/packages/render/jsx-runtime.d.ts +0 -3
  27. package/packages/render/package.json +0 -18
  28. package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
  29. package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
  30. package/packages/render/src/components/git-graph.tsx +0 -127
  31. package/packages/render/src/components/help-overlay.tsx +0 -108
  32. package/packages/render/src/components/index.ts +0 -7
  33. package/packages/render/src/components/repo-list.tsx +0 -127
  34. package/packages/render/src/components/stats-panel.tsx +0 -116
  35. package/packages/render/src/components/status-badge.tsx +0 -70
  36. package/packages/render/src/components/status-bar.tsx +0 -56
  37. package/packages/render/src/components/widget-container.tsx +0 -286
  38. package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
  39. package/packages/render/src/components/widgets/branch-list.tsx +0 -93
  40. package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
  41. package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
  42. package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
  43. package/packages/render/src/components/widgets/file-changes.tsx +0 -78
  44. package/packages/render/src/components/widgets/git-status.tsx +0 -125
  45. package/packages/render/src/components/widgets/github-ci.tsx +0 -98
  46. package/packages/render/src/components/widgets/github-issues.tsx +0 -101
  47. package/packages/render/src/components/widgets/github-prs.tsx +0 -119
  48. package/packages/render/src/components/widgets/github-release.tsx +0 -73
  49. package/packages/render/src/components/widgets/index.ts +0 -12
  50. package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
  51. package/packages/render/src/components/widgets/registry.ts +0 -23
  52. package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
  53. package/packages/render/src/config/index.ts +0 -104
  54. package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
  55. package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
  56. package/packages/render/src/lib/actions.ts +0 -68
  57. package/packages/render/src/lib/fetch-context.ts +0 -102
  58. package/packages/render/src/lib/filter.ts +0 -94
  59. package/packages/render/src/lib/format.ts +0 -36
  60. package/packages/render/src/lib/use-devpad.ts +0 -167
  61. package/packages/render/src/lib/use-github.ts +0 -75
  62. package/packages/render/src/lib/widget-grid.ts +0 -204
  63. package/packages/render/src/lib/widget-state.ts +0 -96
  64. package/packages/render/src/overview.tsx +0 -16
  65. package/packages/render/src/screens/index.ts +0 -1
  66. package/packages/render/src/screens/main-screen.tsx +0 -410
  67. package/packages/render/src/theme/index.ts +0 -37
  68. package/packages/render/tsconfig.json +0 -9
  69. package/tsconfig.json +0 -23
@@ -1,780 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { testRender } from "@opentui/solid";
3
- import { createSignal, Show } from "solid-js";
4
- import type { ScrollBoxRenderable, Renderable } from "@opentui/core";
5
-
6
- describe("scrollbox behavior", () => {
7
- // Test 1: Simple case — many text lines in a scrollbox
8
- // Render a scrollbox with height=20 containing 50 lines of text
9
- // Verify scrollHeight >= 50
10
- test("simple text content reports correct scrollHeight", async () => {
11
- let sb_ref: ScrollBoxRenderable | undefined;
12
- const lines = Array.from({ length: 50 }, (_, i) => `Line ${i}`);
13
-
14
- const { renderOnce } = await testRender(
15
- () => (
16
- <box width={60} height={20}>
17
- <scrollbox ref={sb_ref} flexGrow={1}>
18
- <box flexDirection="column">
19
- {lines.map((l) => (
20
- <text content={l} />
21
- ))}
22
- </box>
23
- </scrollbox>
24
- </box>
25
- ),
26
- { width: 60, height: 20 },
27
- );
28
-
29
- await renderOnce();
30
-
31
- console.log("Test 1 - Simple text:");
32
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
33
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
34
- console.log(` content.height=${sb_ref!.content?.height}`);
35
- console.log(` sb_ref.height=${sb_ref!.height}`);
36
-
37
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(50);
38
- });
39
-
40
- // Test 2: Box with flexDirection column wrapping text — mirrors our widget-container structure
41
- // Render a scrollbox with height=20 containing a column box with children that are
42
- // alternating <text> and <box> elements (simulating border lines + widget row boxes)
43
- test("nested boxes report correct scrollHeight", async () => {
44
- let sb_ref: ScrollBoxRenderable | undefined;
45
- const rows = Array.from({ length: 10 }, (_, i) => i);
46
-
47
- const { renderOnce } = await testRender(
48
- () => (
49
- <box width={60} height={20}>
50
- <scrollbox ref={sb_ref} flexGrow={1}>
51
- <box flexDirection="column" width={60} flexShrink={0}>
52
- {rows.map((i) => (
53
- <>
54
- <text content={`═══ Border ${i} ═══`} />
55
- <box flexDirection="row" width={60}>
56
- <box
57
- flexDirection="column"
58
- width={30}
59
- minHeight={5}
60
- border={["left", "right"]}
61
- borderStyle="rounded"
62
- >
63
- <text content={`Widget ${i}a`} />
64
- <text content="line 2" />
65
- <text content="line 3" />
66
- </box>
67
- <box
68
- flexDirection="column"
69
- width={30}
70
- minHeight={5}
71
- border={["left", "right"]}
72
- borderStyle="rounded"
73
- >
74
- <text content={`Widget ${i}b`} />
75
- <text content="line 2" />
76
- <text content="line 3" />
77
- </box>
78
- </box>
79
- </>
80
- ))}
81
- <text content="═══ Bottom Border ═══" />
82
- </box>
83
- </scrollbox>
84
- </box>
85
- ),
86
- { width: 60, height: 20 },
87
- );
88
-
89
- await renderOnce();
90
-
91
- // Expected: 10 border lines + 10 row boxes (each minHeight=5) + 1 bottom border = 10 + 50 + 1 = 61
92
- console.log("Test 2 - Nested boxes (mirrors widget-container):");
93
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
94
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
95
- console.log(` content.height=${sb_ref!.content?.height}`);
96
- console.log(` sb_ref.height=${sb_ref!.height}`);
97
-
98
- // The total content should be at least 61 lines (10 borders + 10*5 rows + 1 bottom)
99
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(60);
100
- });
101
-
102
- // Test 3: Verify scrollTo can actually reach the bottom
103
- test("scrollTo can reach the last row", async () => {
104
- let sb_ref: ScrollBoxRenderable | undefined;
105
- const rows = Array.from({ length: 10 }, (_, i) => i);
106
-
107
- const { renderOnce } = await testRender(
108
- () => (
109
- <box width={60} height={20}>
110
- <scrollbox ref={sb_ref} flexGrow={1}>
111
- <box flexDirection="column" width={60} flexShrink={0}>
112
- {rows.map((i) => (
113
- <>
114
- <text content={`═══ Border ${i} ═══`} />
115
- <box flexDirection="row" width={60}>
116
- <box
117
- flexDirection="column"
118
- width={30}
119
- minHeight={5}
120
- border={["left", "right"]}
121
- borderStyle="rounded"
122
- >
123
- <text content={`Widget ${i}a`} />
124
- </box>
125
- <box
126
- flexDirection="column"
127
- width={30}
128
- minHeight={5}
129
- border={["left", "right"]}
130
- borderStyle="rounded"
131
- >
132
- <text content={`Widget ${i}b`} />
133
- </box>
134
- </box>
135
- </>
136
- ))}
137
- <text content="═══ Bottom Border ═══" />
138
- </box>
139
- </scrollbox>
140
- </box>
141
- ),
142
- { width: 60, height: 20 },
143
- );
144
-
145
- await renderOnce();
146
-
147
- const max_scroll = sb_ref!.scrollHeight - (sb_ref!.viewport?.height ?? sb_ref!.height);
148
- const content_bottom = sb_ref!.scrollHeight;
149
-
150
- console.log("Test 3 - scrollTo reach:");
151
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
152
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
153
- console.log(` max_scroll=${max_scroll}`);
154
- console.log(` content_bottom=${content_bottom}`);
155
-
156
- // Try to scroll to the bottom
157
- sb_ref!.scrollTo({ x: 0, y: max_scroll });
158
- console.log(` after scrollTo(${max_scroll}): scrollTop=${sb_ref!.scrollTop}`);
159
-
160
- // The scrollTop should actually reach max_scroll
161
- expect(sb_ref!.scrollTop).toBe(max_scroll);
162
-
163
- // The last content should be visible (scrollTop + viewport >= content bottom)
164
- expect(sb_ref!.scrollTop + (sb_ref!.viewport?.height ?? sb_ref!.height)).toBeGreaterThanOrEqual(
165
- content_bottom,
166
- );
167
- });
168
-
169
- // Test 4: Fragment children (JSX <> </>) — this is what the widget-container uses
170
- // This tests if Fragment children cause yoga to miscalculate heights
171
- test("fragment children in scrollbox column", async () => {
172
- let sb_ref: ScrollBoxRenderable | undefined;
173
- const items = Array.from({ length: 15 }, (_, i) => i);
174
-
175
- const { renderOnce } = await testRender(
176
- () => (
177
- <box width={60} height={20}>
178
- <scrollbox ref={sb_ref} flexGrow={1}>
179
- <box flexDirection="column" width={60} flexShrink={0}>
180
- {items.map((i) => (
181
- <>
182
- <text content={`Header ${i}`} />
183
- <box flexDirection="column" minHeight={3}>
184
- <text content={`Content ${i} line 1`} />
185
- <text content={`Content ${i} line 2`} />
186
- </box>
187
- </>
188
- ))}
189
- </box>
190
- </scrollbox>
191
- </box>
192
- ),
193
- { width: 60, height: 20 },
194
- );
195
-
196
- await renderOnce();
197
-
198
- // Expected: 15 headers + 15 boxes (each minHeight=3) = 15 + 45 = 60
199
- console.log("Test 4 - Fragment children:");
200
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
201
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
202
- console.log(` content.height=${sb_ref!.content?.height}`);
203
-
204
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(60);
205
- });
206
-
207
- // Test 5: Without intermediate box — children directly in scrollbox
208
- test("children directly in scrollbox (no intermediate box)", async () => {
209
- let sb_ref: ScrollBoxRenderable | undefined;
210
- const lines = Array.from({ length: 50 }, (_, i) => `Line ${i}`);
211
-
212
- const { renderOnce } = await testRender(
213
- () => (
214
- <box width={60} height={20}>
215
- <scrollbox ref={sb_ref} flexGrow={1}>
216
- {lines.map((l) => (
217
- <text content={l} />
218
- ))}
219
- </scrollbox>
220
- </box>
221
- ),
222
- { width: 60, height: 20 },
223
- );
224
-
225
- await renderOnce();
226
-
227
- console.log("Test 5 - Direct children (no intermediate box):");
228
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
229
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
230
- console.log(` content.height=${sb_ref!.content?.height}`);
231
-
232
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(50);
233
- });
234
-
235
- // Test 6: With the outer box having explicit height (like widget-container does with height prop)
236
- test("scrollbox inside box with explicit height", async () => {
237
- let sb_ref: ScrollBoxRenderable | undefined;
238
- const lines = Array.from({ length: 50 }, (_, i) => `Line ${i}`);
239
-
240
- const { renderOnce } = await testRender(
241
- () => (
242
- <box flexDirection="column" width={60} height={30}>
243
- <scrollbox ref={sb_ref} flexGrow={1}>
244
- <box flexDirection="column" width={60} flexShrink={0}>
245
- {lines.map((l) => (
246
- <text content={l} />
247
- ))}
248
- </box>
249
- </scrollbox>
250
- </box>
251
- ),
252
- { width: 60, height: 30 },
253
- );
254
-
255
- await renderOnce();
256
-
257
- console.log("Test 6 - Explicit outer height:");
258
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
259
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
260
- console.log(` content.height=${sb_ref!.content?.height}`);
261
- console.log(` sb_ref.height=${sb_ref!.height}`);
262
-
263
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(50);
264
- });
265
-
266
- // Test 7: Mimic the EXACT widget-container structure as closely as possible
267
- test("exact widget-container structure mimicry", async () => {
268
- let sb_ref: ScrollBoxRenderable | undefined;
269
- const row_refs: Renderable[] = [];
270
-
271
- // Simulate 9 rows of widgets (matching the real app's ~9 rows)
272
- const row_configs = [
273
- { cols: 2, h: 10 }, // row 0: two half-width widgets
274
- { cols: 1, h: 7 }, // row 1: full width
275
- { cols: 2, h: 3 }, // row 2: two half-width
276
- { cols: 1, h: 13 }, // row 3: full width tall
277
- { cols: 2, h: 3 }, // row 4: two half-width
278
- { cols: 2, h: 7 }, // row 5: two half-width
279
- { cols: 1, h: 10 }, // row 6: full width
280
- { cols: 2, h: 4 }, // row 7: two half-width
281
- { cols: 1, h: 3 }, // row 8: full width
282
- ];
283
-
284
- const total_width = 80;
285
-
286
- const { renderOnce } = await testRender(
287
- () => (
288
- <box flexDirection="column" width={total_width} height={26}>
289
- <scrollbox ref={sb_ref} flexGrow={1}>
290
- <box flexDirection="column" width={total_width} flexShrink={0}>
291
- {row_configs.map((rc, row_idx) => {
292
- const border_line = "─".repeat(total_width);
293
- const half = Math.floor(total_width / 2);
294
-
295
- return (
296
- <>
297
- <text content={border_line} />
298
- <box
299
- ref={(el: Renderable) => {
300
- row_refs[row_idx] = el;
301
- }}
302
- flexDirection="row"
303
- alignItems="stretch"
304
- width={total_width}
305
- >
306
- {rc.cols === 1 ? (
307
- <box
308
- width={total_width}
309
- border={["left", "right"]}
310
- borderStyle="rounded"
311
- flexDirection="column"
312
- minHeight={rc.h}
313
- overflow="hidden"
314
- >
315
- <text content={`Widget row ${row_idx}`} />
316
- {Array.from({ length: rc.h - 1 }, (_, j) => (
317
- <text content={` content line ${j}`} />
318
- ))}
319
- </box>
320
- ) : (
321
- <>
322
- <box
323
- width={half}
324
- border={["left", "right"]}
325
- borderStyle="rounded"
326
- flexDirection="column"
327
- minHeight={rc.h}
328
- overflow="hidden"
329
- >
330
- <text content={`Widget row ${row_idx}a`} />
331
- </box>
332
- <box
333
- width={total_width - half}
334
- border={["left", "right"]}
335
- borderStyle="rounded"
336
- flexDirection="column"
337
- minHeight={rc.h}
338
- overflow="hidden"
339
- >
340
- <text content={`Widget row ${row_idx}b`} />
341
- </box>
342
- </>
343
- )}
344
- </box>
345
- </>
346
- );
347
- })}
348
- <text content={"─".repeat(total_width)} />
349
- </box>
350
- </scrollbox>
351
- </box>
352
- ),
353
- { width: 80, height: 26 },
354
- );
355
-
356
- await renderOnce();
357
-
358
- // Expected content height:
359
- // 9 border lines + sum of row heights (10+7+3+13+3+7+10+4+3 = 60) + 1 bottom border = 70
360
- // BUT with border on boxes, each box adds 0 height (left/right borders don't add height)
361
- // So total = 9 + 60 + 1 = 70
362
- console.log("Test 7 - Exact widget-container mimicry:");
363
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
364
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
365
- console.log(` content.height=${sb_ref!.content?.height}`);
366
- console.log(` sb_ref.height=${sb_ref!.height}`);
367
-
368
- let cumulative = 0;
369
- for (let i = 0; i < row_configs.length; i++) {
370
- const ref = row_refs[i];
371
- if (ref) {
372
- const content_y = ref.y + sb_ref!.scrollTop;
373
- console.log(
374
- ` row ${i}: y=${ref.y} content_y=${content_y} height=${ref.height} (expected minHeight=${row_configs[i]!.h})`,
375
- );
376
- cumulative = content_y + ref.height;
377
- }
378
- }
379
- console.log(` cumulative content bottom: ${cumulative}`);
380
- console.log(` + border lines: ~${cumulative + 1} (bottom border)`);
381
-
382
- // Check if we can scroll to make the last row visible
383
- const vp_h = sb_ref!.viewport?.height ?? sb_ref!.height;
384
- const needed_scroll = cumulative + 1 - vp_h; // to see bottom border
385
- console.log(` needed scroll to see bottom: ${needed_scroll}`);
386
- console.log(` max allowed scroll: ${sb_ref!.scrollHeight - vp_h}`);
387
-
388
- sb_ref!.scrollTo({ x: 0, y: needed_scroll });
389
- console.log(` after scrollTo(${needed_scroll}): scrollTop=${sb_ref!.scrollTop}`);
390
-
391
- // This should NOT be clamped
392
- expect(sb_ref!.scrollTop).toBe(needed_scroll);
393
- });
394
-
395
- // Test 8: scrollbox inside height="50%" container (real app layout)
396
- test("scrollbox inside height=50% container (real app layout)", async () => {
397
- let sb_ref: ScrollBoxRenderable | undefined;
398
- const row_refs: Renderable[] = [];
399
-
400
- const row_configs = [
401
- { cols: 2, h: 10 },
402
- { cols: 1, h: 7 },
403
- { cols: 2, h: 3 },
404
- { cols: 1, h: 13 },
405
- { cols: 2, h: 3 },
406
- { cols: 2, h: 7 },
407
- { cols: 1, h: 10 },
408
- { cols: 2, h: 4 },
409
- { cols: 1, h: 3 },
410
- ];
411
-
412
- const right_panel_width = 80;
413
-
414
- const { renderOnce } = await testRender(
415
- () => (
416
- <box flexDirection="column" width="100%" height="100%">
417
- <box height={1}>
418
- <text content="header" />
419
- </box>
420
- <box flexDirection="row" flexGrow={1}>
421
- <box width={40}>
422
- <text content="left panel" />
423
- </box>
424
- <box flexDirection="column" flexGrow={1}>
425
- <box height="50%">
426
- <text content="git graph placeholder" />
427
- </box>
428
- <box flexDirection="column" width={right_panel_width} height="50%">
429
- <scrollbox ref={sb_ref} flexGrow={1}>
430
- <box flexDirection="column" width={right_panel_width} flexShrink={0}>
431
- {row_configs.map((rc, row_idx) => {
432
- const border_line = "─".repeat(right_panel_width);
433
- const half = Math.floor(right_panel_width / 2);
434
- return (
435
- <>
436
- <text content={border_line} />
437
- <box
438
- ref={(el: Renderable) => {
439
- row_refs[row_idx] = el;
440
- }}
441
- flexDirection="row"
442
- alignItems="stretch"
443
- width={right_panel_width}
444
- >
445
- {rc.cols === 1 ? (
446
- <box
447
- width={right_panel_width}
448
- border={["left", "right"]}
449
- borderStyle="rounded"
450
- flexDirection="column"
451
- minHeight={rc.h}
452
- overflow="hidden"
453
- >
454
- <text content={`Widget row ${row_idx}`} />
455
- {Array.from({ length: rc.h - 1 }, (_, j) => (
456
- <text content={` content line ${j}`} />
457
- ))}
458
- </box>
459
- ) : (
460
- <>
461
- <box
462
- width={half}
463
- border={["left", "right"]}
464
- borderStyle="rounded"
465
- flexDirection="column"
466
- minHeight={rc.h}
467
- overflow="hidden"
468
- >
469
- <text content={`Widget row ${row_idx}a`} />
470
- </box>
471
- <box
472
- width={right_panel_width - half}
473
- border={["left", "right"]}
474
- borderStyle="rounded"
475
- flexDirection="column"
476
- minHeight={rc.h}
477
- overflow="hidden"
478
- >
479
- <text content={`Widget row ${row_idx}b`} />
480
- </box>
481
- </>
482
- )}
483
- </box>
484
- </>
485
- );
486
- })}
487
- <text content={"─".repeat(right_panel_width)} />
488
- </box>
489
- </scrollbox>
490
- </box>
491
- </box>
492
- </box>
493
- <box height={1}>
494
- <text content="status bar" />
495
- </box>
496
- </box>
497
- ),
498
- { width: 120, height: 40 },
499
- );
500
-
501
- await renderOnce();
502
-
503
- console.log("Test 8 - scrollbox inside height=50% container:");
504
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
505
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
506
- console.log(` content.height=${sb_ref!.content?.height}`);
507
- console.log(` sb_ref.height=${sb_ref!.height}`);
508
-
509
- let cumulative = 0;
510
- for (let i = 0; i < row_configs.length; i++) {
511
- const ref = row_refs[i];
512
- if (ref) {
513
- const content_y = ref.y + sb_ref!.scrollTop;
514
- console.log(
515
- ` row ${i}: y=${ref.y} content_y=${content_y} height=${ref.height} (expected minHeight=${row_configs[i]!.h})`,
516
- );
517
- cumulative = content_y + ref.height;
518
- }
519
- }
520
- console.log(` cumulative content bottom: ${cumulative}`);
521
- console.log(` + border lines: ~${cumulative + 1} (bottom border)`);
522
-
523
- const vp_h = sb_ref!.viewport?.height ?? sb_ref!.height;
524
- const needed_scroll = cumulative + 1 - vp_h;
525
- console.log(` needed scroll to see bottom: ${needed_scroll}`);
526
- console.log(` max allowed scroll: ${sb_ref!.scrollHeight - vp_h}`);
527
-
528
- // scrollHeight should be 70 (same as test 7 — 9 borders + 60 row heights + 1 bottom)
529
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(70);
530
- });
531
-
532
- // Test 9: scrollbox with flexGrow=1 inside percentage container (simplified)
533
- test("scrollbox with flexGrow=1 inside percentage container", async () => {
534
- let sb_ref: ScrollBoxRenderable | undefined;
535
- const lines = Array.from({ length: 50 }, (_, i) => `Line ${i}`);
536
-
537
- const { renderOnce } = await testRender(
538
- () => (
539
- <box flexDirection="column" width="100%" height="100%">
540
- <box height="50%">
541
- <text content="top half" />
542
- </box>
543
- <box height="50%" flexDirection="column">
544
- <scrollbox ref={sb_ref} flexGrow={1}>
545
- <box flexDirection="column" flexShrink={0}>
546
- {lines.map((l) => (
547
- <text content={l} />
548
- ))}
549
- </box>
550
- </scrollbox>
551
- </box>
552
- </box>
553
- ),
554
- { width: 60, height: 40 },
555
- );
556
-
557
- await renderOnce();
558
-
559
- console.log("Test 9 - flexGrow=1 inside percentage container:");
560
- console.log(` scrollHeight=${sb_ref!.scrollHeight}`);
561
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
562
- console.log(` content.height=${sb_ref!.content?.height}`);
563
- console.log(` sb_ref.height=${sb_ref!.height}`);
564
-
565
- expect(sb_ref!.scrollHeight).toBeGreaterThanOrEqual(50);
566
- });
567
-
568
- // Test 10: Dynamic content — simulate Show/fallback height changes
569
- test("dynamic content with Show/createSignal updates scrollHeight", async () => {
570
- let sb_ref: ScrollBoxRenderable | undefined;
571
- const [data, setData] = createSignal<{ loaded: true } | null>(null);
572
-
573
- const widgets = Array.from({ length: 8 }, (_, i) => i);
574
-
575
- const { renderOnce } = await testRender(
576
- () => (
577
- <box flexDirection="column" width={60} height={20}>
578
- <scrollbox ref={sb_ref} flexGrow={1}>
579
- <box flexDirection="column" width={60} flexShrink={0}>
580
- {widgets.map((i) => (
581
- <>
582
- <text content={`── Widget ${i} ──`} />
583
- <box flexDirection="column" minHeight={1}>
584
- <Show
585
- when={data()}
586
- fallback={<text content="loading..." />}
587
- >
588
- <text content={`Widget ${i} line 1`} />
589
- <text content={`Widget ${i} line 2`} />
590
- <text content={`Widget ${i} line 3`} />
591
- <text content={`Widget ${i} line 4`} />
592
- <text content={`Widget ${i} line 5`} />
593
- </Show>
594
- </box>
595
- </>
596
- ))}
597
- </box>
598
- </scrollbox>
599
- </box>
600
- ),
601
- { width: 60, height: 20 },
602
- );
603
-
604
- await renderOnce();
605
-
606
- const scroll_height_before = sb_ref!.scrollHeight;
607
- console.log("Test 10 - Dynamic content (before setData):");
608
- console.log(` scrollHeight=${scroll_height_before}`);
609
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
610
- console.log(` content.height=${sb_ref!.content?.height}`);
611
-
612
- // Before: 8 headers + 8 boxes (each 1 line fallback) = 8 + 8 = 16
613
- // (minHeight=1 so each box is at least 1)
614
- expect(scroll_height_before).toBeGreaterThanOrEqual(16);
615
-
616
- // Now trigger the reactive update
617
- setData({ loaded: true });
618
- await renderOnce();
619
-
620
- const scroll_height_after = sb_ref!.scrollHeight;
621
- console.log("Test 10 - Dynamic content (after setData):");
622
- console.log(` scrollHeight=${scroll_height_after}`);
623
- console.log(` viewport.height=${sb_ref!.viewport?.height}`);
624
- console.log(` content.height=${sb_ref!.content?.height}`);
625
-
626
- // After: 8 headers + 8 boxes (each 5 lines) = 8 + 40 = 48
627
- expect(scroll_height_after).toBeGreaterThanOrEqual(48);
628
- expect(scroll_height_after).toBeGreaterThan(scroll_height_before);
629
- });
630
-
631
- // Test 11: Verify scroll-to-focused logic works in nested layout
632
- // This is the actual bug reproduction — el.y is screen-absolute
633
- test("scroll-to-focused uses content-relative coordinates in nested layout", async () => {
634
- let sb_ref: ScrollBoxRenderable | undefined;
635
- const row_refs: Renderable[] = [];
636
-
637
- const row_configs = [
638
- { cols: 2, h: 10 },
639
- { cols: 1, h: 7 },
640
- { cols: 2, h: 3 },
641
- { cols: 1, h: 13 },
642
- { cols: 2, h: 3 },
643
- { cols: 2, h: 7 },
644
- { cols: 1, h: 10 },
645
- { cols: 2, h: 4 },
646
- { cols: 1, h: 3 },
647
- ];
648
-
649
- const right_panel_width = 80;
650
-
651
- const { renderOnce } = await testRender(
652
- () => (
653
- <box flexDirection="column" width="100%" height="100%">
654
- <box height={1}>
655
- <text content="header" />
656
- </box>
657
- <box flexDirection="row" flexGrow={1}>
658
- <box width={40}>
659
- <text content="left panel" />
660
- </box>
661
- <box flexDirection="column" flexGrow={1}>
662
- <box height="50%">
663
- <text content="git graph placeholder" />
664
- </box>
665
- <box flexDirection="column" width={right_panel_width} height="50%">
666
- <scrollbox ref={sb_ref} flexGrow={1}>
667
- <box flexDirection="column" width={right_panel_width} flexShrink={0}>
668
- {row_configs.map((rc, row_idx) => {
669
- const border_line = "─".repeat(right_panel_width);
670
- const half = Math.floor(right_panel_width / 2);
671
- return (
672
- <>
673
- <text content={border_line} />
674
- <box
675
- ref={(el: Renderable) => {
676
- row_refs[row_idx] = el;
677
- }}
678
- flexDirection="row"
679
- alignItems="stretch"
680
- width={right_panel_width}
681
- >
682
- {rc.cols === 1 ? (
683
- <box
684
- width={right_panel_width}
685
- border={["left", "right"]}
686
- borderStyle="rounded"
687
- flexDirection="column"
688
- minHeight={rc.h}
689
- overflow="hidden"
690
- >
691
- <text content={`Widget row ${row_idx}`} />
692
- {Array.from({ length: rc.h - 1 }, (_, j) => (
693
- <text content={` content line ${j}`} />
694
- ))}
695
- </box>
696
- ) : (
697
- <>
698
- <box
699
- width={half}
700
- border={["left", "right"]}
701
- borderStyle="rounded"
702
- flexDirection="column"
703
- minHeight={rc.h}
704
- overflow="hidden"
705
- >
706
- <text content={`Widget row ${row_idx}a`} />
707
- </box>
708
- <box
709
- width={right_panel_width - half}
710
- border={["left", "right"]}
711
- borderStyle="rounded"
712
- flexDirection="column"
713
- minHeight={rc.h}
714
- overflow="hidden"
715
- >
716
- <text content={`Widget row ${row_idx}b`} />
717
- </box>
718
- </>
719
- )}
720
- </box>
721
- </>
722
- );
723
- })}
724
- <text content={"─".repeat(right_panel_width)} />
725
- </box>
726
- </scrollbox>
727
- </box>
728
- </box>
729
- </box>
730
- <box height={1}>
731
- <text content="status bar" />
732
- </box>
733
- </box>
734
- ),
735
- { width: 120, height: 40 },
736
- );
737
-
738
- await renderOnce();
739
-
740
- const scroll_height = sb_ref!.scrollHeight;
741
- const vp_height = sb_ref!.viewport?.height ?? sb_ref!.height;
742
- const content_origin_y = sb_ref!.content.y;
743
-
744
- console.log("Test 11 - Content-relative coordinate check:");
745
- console.log(` scrollHeight=${scroll_height}, viewport.height=${vp_height}`);
746
- console.log(` content.y (screen)=${content_origin_y}`);
747
-
748
- // For each row, compute content-relative y using the CORRECT formula
749
- for (let i = 0; i < row_configs.length; i++) {
750
- const ref = row_refs[i];
751
- if (ref) {
752
- // CORRECT: content-relative position
753
- const content_y = ref.y - content_origin_y;
754
- console.log(` row ${i}: screen_y=${ref.y} content_y=${content_y} height=${ref.height}`);
755
- }
756
- }
757
-
758
- // Now simulate scrollToFocused for the LAST row (row 8)
759
- const last_ref = row_refs[8]!;
760
- const content_y = last_ref.y - content_origin_y;
761
- const content_h = last_ref.height;
762
- const region_top = content_y - 1; // 1 line above for border
763
- const region_bottom = content_y + content_h + 1; // 1 line below for bottom border (last row)
764
- const region_height = region_bottom - region_top;
765
-
766
- console.log(` last row region: [${region_top}, ${region_bottom}] h=${region_height}`);
767
-
768
- // To see the bottom, scroll so region_bottom aligns with viewport bottom
769
- const needed_scroll = region_bottom - vp_height;
770
- console.log(` needed_scroll=${needed_scroll}`);
771
- console.log(` max_scroll=${scroll_height - vp_height}`);
772
-
773
- // The needed scroll should be within the allowed range
774
- expect(needed_scroll).toBeLessThanOrEqual(scroll_height - vp_height);
775
-
776
- // Actually scroll there
777
- sb_ref!.scrollTo({ x: 0, y: needed_scroll });
778
- expect(sb_ref!.scrollTop).toBe(needed_scroll);
779
- });
780
- });