@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,326 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { testRender } from "@opentui/solid";
3
- import { buildBorderLine, resolveSpan, type GridRow } from "../../../lib/widget-grid";
4
-
5
- // ── helpers ────────────────────────────────────────────────────────────────
6
-
7
- const twoColRow: GridRow = { widgets: [{} as any, {} as any], columns: 2 };
8
- const oneColRow: GridRow = { widgets: [{} as any], columns: 1 };
9
- const threeColRow: GridRow = { widgets: [{} as any, {} as any, {} as any], columns: 3 };
10
-
11
- // ── grid layout rendering ──────────────────────────────────────────────────
12
-
13
- describe("widget grid rendering (integration)", () => {
14
- test("two half-width widgets render side-by-side with shared border", async () => {
15
- const width = 60;
16
- const junction_col = Math.floor(width / 2);
17
- const { renderOnce, captureCharFrame } = await testRender(
18
- () => (
19
- <box flexDirection="column" width={width}>
20
- <text content={buildBorderLine("top", width, null, twoColRow)} />
21
- <box flexDirection="row" alignItems="stretch" width={width}>
22
- <box width={junction_col} border={["left"]} borderStyle="rounded" flexDirection="column">
23
- <text content="Widget A" />
24
- <text content="content a" />
25
- </box>
26
- <box width={width - junction_col} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
27
- <text content="Widget B" />
28
- <text content="content b" />
29
- </box>
30
- </box>
31
- <text content={buildBorderLine("bottom", width, twoColRow, null)} />
32
- </box>
33
- ),
34
- { width: 80, height: 20 },
35
- );
36
-
37
- await renderOnce();
38
- const frame = captureCharFrame();
39
-
40
- expect(frame).toContain("Widget A");
41
- expect(frame).toContain("Widget B");
42
- expect(frame).toContain("content a");
43
- expect(frame).toContain("content b");
44
-
45
- // top border has junction
46
- expect(frame).toContain("┬");
47
- // bottom border has junction
48
- expect(frame).toContain("┴");
49
-
50
- // no doubled borders — shared divider means no ╮╭ or ╯╰ adjacency
51
- expect(frame).not.toContain("╮╭");
52
- expect(frame).not.toContain("╯╰");
53
- });
54
-
55
- test("full-width widget renders with full border", async () => {
56
- const width = 40;
57
- const { renderOnce, captureCharFrame } = await testRender(
58
- () => (
59
- <box flexDirection="column" width={width}>
60
- <text content={buildBorderLine("top", width, null, oneColRow)} />
61
- <box flexDirection="row" width={width}>
62
- <box width={width} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
63
- <text content="Full Widget" />
64
- <text content="full content" />
65
- </box>
66
- </box>
67
- <text content={buildBorderLine("bottom", width, oneColRow, null)} />
68
- </box>
69
- ),
70
- { width: 60, height: 20 },
71
- );
72
-
73
- await renderOnce();
74
- const frame = captureCharFrame();
75
-
76
- expect(frame).toContain("Full Widget");
77
- expect(frame).toContain("full content");
78
-
79
- const lines = frame.split("\n").filter((l: string) => l.trim().length > 0);
80
- const top_line = lines[0]!;
81
- const bottom_line = lines[lines.length - 1]!;
82
-
83
- // top border corners
84
- expect(top_line.trimStart()).toMatch(/^╭/);
85
- expect(top_line.trimEnd()).toMatch(/╮$/);
86
-
87
- // bottom border corners
88
- expect(bottom_line.trimStart()).toMatch(/^╰/);
89
- expect(bottom_line.trimEnd()).toMatch(/╯$/);
90
-
91
- // no junction characters in single-column layout
92
- expect(frame).not.toContain("┬");
93
- expect(frame).not.toContain("┴");
94
- expect(frame).not.toContain("┼");
95
- });
96
-
97
- test("mixed layout — half-width row followed by full-width row", async () => {
98
- const width = 60;
99
- const junction_col = Math.floor(width / 2);
100
- const { renderOnce, captureCharFrame } = await testRender(
101
- () => (
102
- <box flexDirection="column" width={width}>
103
- <text content={buildBorderLine("top", width, null, twoColRow)} />
104
- <box flexDirection="row" alignItems="stretch" width={width}>
105
- <box width={junction_col} border={["left"]} borderStyle="rounded" flexDirection="column">
106
- <text content="Half A" />
107
- </box>
108
- <box width={width - junction_col} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
109
- <text content="Half B" />
110
- </box>
111
- </box>
112
- <text content={buildBorderLine("mid", width, twoColRow, oneColRow)} />
113
- <box flexDirection="row" width={width}>
114
- <box width={width} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
115
- <text content="Full C" />
116
- </box>
117
- </box>
118
- <text content={buildBorderLine("bottom", width, oneColRow, null)} />
119
- </box>
120
- ),
121
- { width: 80, height: 20 },
122
- );
123
-
124
- await renderOnce();
125
- const frame = captureCharFrame();
126
-
127
- expect(frame).toContain("Half A");
128
- expect(frame).toContain("Half B");
129
- expect(frame).toContain("Full C");
130
-
131
- // verify border line characters directly from buildBorderLine output
132
- const top_border = buildBorderLine("top", width, null, twoColRow);
133
- const mid_border = buildBorderLine("mid", width, twoColRow, oneColRow);
134
- const bottom_border = buildBorderLine("bottom", width, oneColRow, null);
135
-
136
- // top border has ┬ at midpoint (2-col below)
137
- expect(top_border).toContain("┬");
138
-
139
- // mid border: 2-col above merges into 1-col below → ┴ at junction
140
- expect(mid_border).toContain("┴");
141
- // mid border starts with ├ and ends with ┤
142
- expect(mid_border[0]).toBe("├");
143
- expect(mid_border[mid_border.length - 1]).toBe("┤");
144
-
145
- // bottom border has no junction chars
146
- expect(bottom_border).not.toContain("┬");
147
- expect(bottom_border).not.toContain("┴");
148
- expect(bottom_border).not.toContain("┼");
149
-
150
- // all border lines appear in the rendered frame
151
- expect(frame).toContain(top_border);
152
- expect(frame).toContain(mid_border);
153
- expect(frame).toContain(bottom_border);
154
- });
155
-
156
- test("narrow panel falls back to single column", () => {
157
- // pure function — no rendering needed
158
- expect(resolveSpan("half", 39)).toBe("full");
159
- expect(resolveSpan("half", 40)).toBe("half");
160
- expect(resolveSpan("half", 50)).toBe("half");
161
- expect(resolveSpan("half", 100)).toBe("half");
162
- expect(resolveSpan("full", 100)).toBe("full");
163
- });
164
-
165
- test("collapsed widget shows label text", async () => {
166
- const width = 40;
167
- const { renderOnce, captureCharFrame } = await testRender(
168
- () => (
169
- <box flexDirection="row" width={width}>
170
- <box width={width} border={["left", "right"]} borderStyle="rounded" flexDirection="column" minHeight={1}>
171
- <text content="[>] Widget Name (collapsed)" />
172
- </box>
173
- </box>
174
- ),
175
- { width: 60, height: 20 },
176
- );
177
-
178
- await renderOnce();
179
- const frame = captureCharFrame();
180
-
181
- expect(frame).toContain("[>]");
182
- expect(frame).toContain("collapsed");
183
- });
184
-
185
- test("three third-width widgets render side-by-side with correct borders", async () => {
186
- const width = 90;
187
- const j1 = Math.floor(width / 3); // 30
188
- const j2 = Math.floor(2 * width / 3); // 60
189
- const { renderOnce, captureCharFrame } = await testRender(
190
- () => (
191
- <box flexDirection="column" width={width}>
192
- <text content={buildBorderLine("top", width, null, threeColRow)} />
193
- <box flexDirection="row" alignItems="stretch" width={width}>
194
- <box width={j1} border={["left"]} borderStyle="rounded" flexDirection="column">
195
- <text content="Widget A" />
196
- <text content="content a" />
197
- </box>
198
- <box width={j2 - j1} border={["left"]} borderStyle="rounded" flexDirection="column">
199
- <text content="Widget B" />
200
- <text content="content b" />
201
- </box>
202
- <box width={width - j2} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
203
- <text content="Widget C" />
204
- <text content="content c" />
205
- </box>
206
- </box>
207
- <text content={buildBorderLine("bottom", width, threeColRow, null)} />
208
- </box>
209
- ),
210
- { width: 100, height: 20 },
211
- );
212
-
213
- await renderOnce();
214
- const frame = captureCharFrame();
215
-
216
- expect(frame).toContain("Widget A");
217
- expect(frame).toContain("Widget B");
218
- expect(frame).toContain("Widget C");
219
-
220
- // Top border has two ┬ junctions
221
- const top_border = buildBorderLine("top", width, null, threeColRow);
222
- expect(top_border.split("┬").length - 1).toBe(2);
223
- expect(frame).toContain(top_border);
224
-
225
- // Bottom border has two ┴ junctions
226
- const bottom_border = buildBorderLine("bottom", width, threeColRow, null);
227
- expect(bottom_border.split("┴").length - 1).toBe(2);
228
- expect(frame).toContain(bottom_border);
229
-
230
- // No doubled borders
231
- expect(frame).not.toContain("╮╭");
232
- expect(frame).not.toContain("╯╰");
233
- });
234
-
235
- test("mixed 3-col and 2-col rows have correct junction characters", async () => {
236
- const width = 60;
237
- const j_third_1 = Math.floor(width / 3); // 20
238
- const j_third_2 = Math.floor(2 * width / 3); // 40
239
- const j_half = Math.floor(width / 2); // 30
240
- const { renderOnce, captureCharFrame } = await testRender(
241
- () => (
242
- <box flexDirection="column" width={width}>
243
- <text content={buildBorderLine("top", width, null, threeColRow)} />
244
- <box flexDirection="row" alignItems="stretch" width={width}>
245
- <box width={j_third_1} border={["left"]} borderStyle="rounded" flexDirection="column">
246
- <text content="Third A" />
247
- </box>
248
- <box width={j_third_2 - j_third_1} border={["left"]} borderStyle="rounded" flexDirection="column">
249
- <text content="Third B" />
250
- </box>
251
- <box width={width - j_third_2} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
252
- <text content="Third C" />
253
- </box>
254
- </box>
255
- <text content={buildBorderLine("mid", width, threeColRow, twoColRow)} />
256
- <box flexDirection="row" alignItems="stretch" width={width}>
257
- <box width={j_half} border={["left"]} borderStyle="rounded" flexDirection="column">
258
- <text content="Half D" />
259
- </box>
260
- <box width={width - j_half} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
261
- <text content="Half E" />
262
- </box>
263
- </box>
264
- <text content={buildBorderLine("bottom", width, twoColRow, null)} />
265
- </box>
266
- ),
267
- { width: 80, height: 20 },
268
- );
269
-
270
- await renderOnce();
271
- const frame = captureCharFrame();
272
-
273
- expect(frame).toContain("Third A");
274
- expect(frame).toContain("Third B");
275
- expect(frame).toContain("Third C");
276
- expect(frame).toContain("Half D");
277
- expect(frame).toContain("Half E");
278
-
279
- // Mid border has junctions from both rows:
280
- // 3-col junctions at 20, 40 (from above)
281
- // 2-col junction at 30 (from below)
282
- const mid_border = buildBorderLine("mid", width, threeColRow, twoColRow);
283
- expect(mid_border[0]).toBe("├");
284
- expect(mid_border[width - 1]).toBe("┤");
285
- expect(mid_border[j_third_1]).toBe("┴"); // from 3-col above only
286
- expect(mid_border[j_half]).toBe("┬"); // from 2-col below only
287
- expect(mid_border[j_third_2]).toBe("┴"); // from 3-col above only
288
- expect(frame).toContain(mid_border);
289
- });
290
-
291
- test("auto-expand: lone third renders as full-width", async () => {
292
- const width = 40;
293
- const { renderOnce, captureCharFrame } = await testRender(
294
- () => (
295
- <box flexDirection="column" width={width}>
296
- <text content={buildBorderLine("top", width, null, oneColRow)} />
297
- <box flexDirection="row" width={width}>
298
- <box width={width} border={["left", "right"]} borderStyle="rounded" flexDirection="column">
299
- <text content="Expanded Third" />
300
- <text content="I have full width!" />
301
- </box>
302
- </box>
303
- <text content={buildBorderLine("bottom", width, oneColRow, null)} />
304
- </box>
305
- ),
306
- { width: 60, height: 20 },
307
- );
308
-
309
- await renderOnce();
310
- const frame = captureCharFrame();
311
-
312
- expect(frame).toContain("Expanded Third");
313
- expect(frame).toContain("I have full width!");
314
-
315
- // No junction characters — single-column row
316
- expect(frame).not.toContain("┬");
317
- expect(frame).not.toContain("┴");
318
- expect(frame).not.toContain("┼");
319
-
320
- // Full-width borders
321
- const top_border = buildBorderLine("top", width, null, oneColRow);
322
- const bottom_border = buildBorderLine("bottom", width, oneColRow, null);
323
- expect(frame).toContain(top_border);
324
- expect(frame).toContain(bottom_border);
325
- });
326
- });
@@ -1,93 +0,0 @@
1
- import { For, Show, createMemo } from "solid-js";
2
- import type { WidgetRenderProps, RepoStatus, BranchInfo } from "@overview/core";
3
- import { registerWidget } from "./registry";
4
- import { theme } from "../../theme";
5
- import { truncate } from "../../lib/format";
6
-
7
- const size_hint = { span: "half" as const, min_height: 2 };
8
- const MAX_VISIBLE = 10;
9
- const STALE_THRESHOLD = 30 * 24 * 60 * 60; // 30 days in seconds
10
-
11
- function sortBranches(branches: BranchInfo[]): BranchInfo[] {
12
- return [...branches].sort((a, b) => {
13
- if (a.is_current !== b.is_current) return a.is_current ? -1 : 1;
14
- return b.last_commit_time - a.last_commit_time;
15
- });
16
- }
17
-
18
- function isStale(branch: BranchInfo): boolean {
19
- const now = Math.floor(Date.now() / 1000);
20
- return now - branch.last_commit_time > STALE_THRESHOLD;
21
- }
22
-
23
- function BranchListWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
24
- const branches = createMemo(() =>
25
- props.status ? sortBranches(props.status.branches) : []
26
- );
27
- const visible = () => branches().slice(0, MAX_VISIBLE);
28
- const overflow = () => Math.max(0, branches().length - MAX_VISIBLE);
29
- const has_sync = (b: BranchInfo) => b.ahead > 0 || b.behind > 0;
30
-
31
- return (
32
- <box flexDirection="column">
33
- <Show
34
- when={props.status}
35
- fallback={
36
- <text fg={theme.fg_dim} content="no repo selected" />
37
- }
38
- >
39
- {(status) => (
40
- <>
41
- <text fg={theme.fg_dark} content={`Branches (${status().branches.length})`} />
42
-
43
- <Show
44
- when={branches().length > 0}
45
- fallback={
46
- <text fg={theme.fg_dim} content="(no branches)" />
47
- }
48
- >
49
- <For each={visible()}>
50
- {(branch) => (
51
- <box flexDirection="row" height={1}>
52
- <text
53
- fg={branch.is_current ? theme.green : theme.fg}
54
- content={branch.is_current ? "* " : " "}
55
- />
56
- <text
57
- fg={branch.is_current ? theme.green : theme.fg}
58
- content={truncate(branch.name, props.width - 20)}
59
- />
60
- <Show when={has_sync(branch)}>
61
- <Show when={branch.ahead > 0}>
62
- <text fg={theme.yellow} content={` ↑${branch.ahead}`} />
63
- </Show>
64
- <Show when={branch.behind > 0}>
65
- <text fg={theme.cyan} content={` ↓${branch.behind}`} />
66
- </Show>
67
- </Show>
68
- <Show when={isStale(branch)}>
69
- <text fg={theme.orange} content=" (stale)" />
70
- </Show>
71
- </box>
72
- )}
73
- </For>
74
-
75
- <Show when={overflow() > 0}>
76
- <text fg={theme.fg_dim} content={`+${overflow()} more`} />
77
- </Show>
78
- </Show>
79
- </>
80
- )}
81
- </Show>
82
- </box>
83
- );
84
- }
85
-
86
- registerWidget({
87
- id: "branch-list",
88
- label: "Branches",
89
- size_hint,
90
- component: BranchListWidget,
91
- });
92
-
93
- export { BranchListWidget };
@@ -1,112 +0,0 @@
1
- import { Show, createMemo } from "solid-js";
2
- import type { WidgetRenderProps, RepoStatus } from "@overview/core";
3
- import { registerWidget } from "./registry";
4
- import { theme } from "../../theme";
5
-
6
- const size_hint = { span: "third" as const, min_height: 2 };
7
-
8
- const BLOCKS = " ▁▂▃▄▅▆▇█";
9
-
10
- function renderSparkline(counts: number[]): string {
11
- const max = Math.max(...counts, 1);
12
- return counts
13
- .map((c) => {
14
- const level = Math.round((c / max) * 8);
15
- return BLOCKS[level] ?? " ";
16
- })
17
- .join("");
18
- }
19
-
20
- function CommitActivityWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
21
- const activity = () => props.status?.commit_activity ?? null;
22
-
23
- const sparkline = createMemo(() => {
24
- const a = activity();
25
- if (!a) return "";
26
- return renderSparkline(a.daily_counts);
27
- });
28
-
29
- const delta = createMemo(() => {
30
- const a = activity();
31
- if (!a) return 0;
32
- return a.total_this_week - a.total_last_week;
33
- });
34
-
35
- const delta_str = createMemo(() => {
36
- const d = delta();
37
- if (d > 0) return `+${d}`;
38
- if (d < 0) return `${d}`;
39
- return "0";
40
- });
41
-
42
- const delta_color = createMemo(() => {
43
- const d = delta();
44
- if (d > 0) return theme.green;
45
- if (d < 0) return theme.red;
46
- return theme.fg_dim;
47
- });
48
-
49
- const total_14d = createMemo(() => {
50
- const a = activity();
51
- if (!a) return 0;
52
- return a.daily_counts.reduce((sum, c) => sum + c, 0);
53
- });
54
-
55
- const sparkline_colored = createMemo(() => {
56
- const a = activity();
57
- if (!a) return [];
58
- return a.daily_counts.map((c) => ({
59
- char: BLOCKS[Math.round((c / Math.max(...a.daily_counts, 1)) * 8)] ?? " ",
60
- color: c > 0 ? theme.green : theme.fg_dim,
61
- }));
62
- });
63
-
64
- return (
65
- <box flexDirection="column">
66
- <Show
67
- when={activity()}
68
- fallback={
69
- <text fg={theme.fg_dim} content="(no activity data)" />
70
- }
71
- >
72
- {/* Row 1: Sparkline */}
73
- <box flexDirection="row" height={1}>
74
- {sparkline_colored().map((s) => (
75
- <text fg={s.color} content={s.char} />
76
- ))}
77
- </box>
78
-
79
- {/* Row 2: Weekly stats */}
80
- <box flexDirection="row" height={1} gap={2}>
81
- <box flexDirection="row" gap={1}>
82
- <text fg={theme.fg_dim} content="this week:" />
83
- <text fg={theme.yellow} content={`${activity()!.total_this_week}`} />
84
- </box>
85
- <box flexDirection="row" gap={1}>
86
- <text fg={theme.fg_dim} content="last week:" />
87
- <text fg={theme.yellow} content={`${activity()!.total_last_week}`} />
88
- </box>
89
- <box flexDirection="row" gap={1}>
90
- <text fg={theme.fg_dim} content="delta:" />
91
- <text fg={delta_color()} content={delta_str()} />
92
- </box>
93
- </box>
94
-
95
- {/* Row 3: Total */}
96
- <box flexDirection="row" height={1} gap={1}>
97
- <text fg={theme.yellow} content={`${total_14d()}`} />
98
- <text fg={theme.fg_dim} content="commits in 14 days" />
99
- </box>
100
- </Show>
101
- </box>
102
- );
103
- }
104
-
105
- registerWidget({
106
- id: "commit-activity",
107
- label: "Commit Activity",
108
- size_hint,
109
- component: CommitActivityWidget,
110
- });
111
-
112
- export { CommitActivityWidget };
@@ -1,88 +0,0 @@
1
- import { For, Show, Switch, Match, createMemo } from "solid-js";
2
- import type { WidgetRenderProps, RepoStatus, DevpadMilestone } from "@overview/core";
3
- import { registerWidget } from "./registry";
4
- import { theme } from "../../theme";
5
- import { truncate } from "../../lib/format";
6
- import { useDevpad } from "../../lib/use-devpad";
7
-
8
- const size_hint = { span: "half" as const, min_height: 2 };
9
-
10
- function progressBar(total: number, completed: number, width: number): string {
11
- if (total === 0) return "░".repeat(width);
12
- const filled = Math.round((completed / total) * width);
13
- return "█".repeat(filled) + "░".repeat(width - filled);
14
- }
15
-
16
- function DevpadMilestonesWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
17
- const remote_url = createMemo(() => props.status?.remote_url ?? null);
18
- const repo_name = createMemo(() => props.status?.name ?? "");
19
- const devpad = useDevpad(remote_url, repo_name);
20
-
21
- const milestones = createMemo(() => devpad.data()?.milestones ?? []);
22
- const max_visible = 4;
23
- const visible = () => milestones().slice(0, max_visible);
24
- const overflow = () => Math.max(0, milestones().length - max_visible);
25
-
26
- return (
27
- <box flexDirection="column">
28
- <Switch>
29
- <Match when={devpad.error()}>
30
- {(err) => <text fg={theme.fg_dim} content={err()} />}
31
- </Match>
32
- <Match when={devpad.loading() && !devpad.data()}>
33
- <text fg={theme.fg_dim} content="loading…" />
34
- </Match>
35
- <Match when={!devpad.data()?.project && devpad.data() !== null}>
36
- <text fg={theme.fg_dim} content="no devpad project" />
37
- </Match>
38
- <Match when={true}>
39
- <Show
40
- when={milestones().length > 0}
41
- fallback={<text fg={theme.fg_dim} content="no milestones" />}
42
- >
43
- <For each={visible()}>
44
- {(ms) => {
45
- const pct = () =>
46
- ms.goals_total > 0
47
- ? Math.round((ms.goals_completed / ms.goals_total) * 100)
48
- : 0;
49
- const bar_width = 12;
50
- const label = () => {
51
- const version_str = ms.target_version ? ` (${ms.target_version})` : "";
52
- return truncate(`${ms.name}${version_str}`, Math.max(1, props.width - bar_width - 8));
53
- };
54
- const bar = () => progressBar(ms.goals_total, ms.goals_completed, bar_width);
55
- const bar_color = () => (pct() === 100 ? theme.green : theme.blue);
56
-
57
- return (
58
- <box flexDirection="column" height={2}>
59
- <box flexDirection="row" height={1}>
60
- <text content={label()} />
61
- <text fg={theme.fg_dim} content={` ${ms.goals_completed}/${ms.goals_total}`} />
62
- </box>
63
- <box flexDirection="row" height={1}>
64
- <text fg={bar_color()} content={bar()} />
65
- <text fg={theme.fg_dim} content={` ${pct()}%`} />
66
- </box>
67
- </box>
68
- );
69
- }}
70
- </For>
71
- <Show when={overflow() > 0}>
72
- <text fg={theme.fg_dim} content={`+${overflow()} more`} />
73
- </Show>
74
- </Show>
75
- </Match>
76
- </Switch>
77
- </box>
78
- );
79
- }
80
-
81
- registerWidget({
82
- id: "devpad-milestones",
83
- label: "Devpad Milestones",
84
- size_hint,
85
- component: DevpadMilestonesWidget,
86
- });
87
-
88
- export { DevpadMilestonesWidget };