@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.
- package/bin/overview +10 -0
- package/dist/overview.js +10361 -0
- package/package.json +22 -15
- package/bunfig.toml +0 -7
- package/packages/core/__tests__/concurrency.test.ts +0 -111
- package/packages/core/__tests__/helpers.ts +0 -60
- package/packages/core/__tests__/integration/git-status.test.ts +0 -62
- package/packages/core/__tests__/integration/scanner.test.ts +0 -140
- package/packages/core/__tests__/ocn.test.ts +0 -164
- package/packages/core/package.json +0 -13
- package/packages/core/src/cache.ts +0 -31
- package/packages/core/src/concurrency.ts +0 -44
- package/packages/core/src/devpad.ts +0 -61
- package/packages/core/src/git-graph.ts +0 -54
- package/packages/core/src/git-stats.ts +0 -201
- package/packages/core/src/git-status.ts +0 -316
- package/packages/core/src/github.ts +0 -286
- package/packages/core/src/index.ts +0 -58
- package/packages/core/src/ocn.ts +0 -74
- package/packages/core/src/scanner.ts +0 -118
- package/packages/core/src/types.ts +0 -199
- package/packages/core/src/watcher.ts +0 -128
- package/packages/core/src/worktree.ts +0 -80
- package/packages/core/tsconfig.json +0 -5
- package/packages/render/bunfig.toml +0 -8
- package/packages/render/jsx-runtime.d.ts +0 -3
- package/packages/render/package.json +0 -18
- package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
- package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
- package/packages/render/src/components/git-graph.tsx +0 -127
- package/packages/render/src/components/help-overlay.tsx +0 -108
- package/packages/render/src/components/index.ts +0 -7
- package/packages/render/src/components/repo-list.tsx +0 -127
- package/packages/render/src/components/stats-panel.tsx +0 -116
- package/packages/render/src/components/status-badge.tsx +0 -70
- package/packages/render/src/components/status-bar.tsx +0 -56
- package/packages/render/src/components/widget-container.tsx +0 -286
- package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
- package/packages/render/src/components/widgets/branch-list.tsx +0 -93
- package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
- package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
- package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
- package/packages/render/src/components/widgets/file-changes.tsx +0 -78
- package/packages/render/src/components/widgets/git-status.tsx +0 -125
- package/packages/render/src/components/widgets/github-ci.tsx +0 -98
- package/packages/render/src/components/widgets/github-issues.tsx +0 -101
- package/packages/render/src/components/widgets/github-prs.tsx +0 -119
- package/packages/render/src/components/widgets/github-release.tsx +0 -73
- package/packages/render/src/components/widgets/index.ts +0 -12
- package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
- package/packages/render/src/components/widgets/registry.ts +0 -23
- package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
- package/packages/render/src/config/index.ts +0 -104
- package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
- package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
- package/packages/render/src/lib/actions.ts +0 -68
- package/packages/render/src/lib/fetch-context.ts +0 -102
- package/packages/render/src/lib/filter.ts +0 -94
- package/packages/render/src/lib/format.ts +0 -36
- package/packages/render/src/lib/use-devpad.ts +0 -167
- package/packages/render/src/lib/use-github.ts +0 -75
- package/packages/render/src/lib/widget-grid.ts +0 -204
- package/packages/render/src/lib/widget-state.ts +0 -96
- package/packages/render/src/overview.tsx +0 -16
- package/packages/render/src/screens/index.ts +0 -1
- package/packages/render/src/screens/main-screen.tsx +0 -410
- package/packages/render/src/theme/index.ts +0 -37
- package/packages/render/tsconfig.json +0 -9
- 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 };
|