@f0rbit/overview 0.2.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 +1 -1
- package/dist/overview.js +10361 -0
- package/package.json +10 -13
- 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 -306
- 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,116 +0,0 @@
|
|
|
1
|
-
import { Show } from "solid-js";
|
|
2
|
-
import type { RepoStatus } from "@overview/core";
|
|
3
|
-
import { theme } from "../theme";
|
|
4
|
-
import { formatRelativeTime } from "../lib/format";
|
|
5
|
-
|
|
6
|
-
interface StatsPanelProps {
|
|
7
|
-
status: RepoStatus | null;
|
|
8
|
-
repoName: string;
|
|
9
|
-
loading: boolean;
|
|
10
|
-
focused: boolean;
|
|
11
|
-
height: number | `${number}%` | "auto";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function StatRow(props: { label: string; value: string; color?: string }) {
|
|
15
|
-
return (
|
|
16
|
-
<box flexDirection="row" height={1}>
|
|
17
|
-
<text fg={theme.fg_dim} content={props.label.padEnd(12)} />
|
|
18
|
-
<text fg={props.color ?? theme.fg} content={props.value} />
|
|
19
|
-
</box>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function CleanState(props: { status: RepoStatus }) {
|
|
24
|
-
return (
|
|
25
|
-
<box flexDirection="column" gap={1}>
|
|
26
|
-
<text fg={theme.status.clean} content="✓ Everything clean & up to date" />
|
|
27
|
-
<box flexDirection="column">
|
|
28
|
-
<StatRow label="branch" value={props.status.current_branch} />
|
|
29
|
-
<StatRow label="last commit" value={formatRelativeTime(props.status.head_time)} />
|
|
30
|
-
</box>
|
|
31
|
-
</box>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function StatusDetails(props: { status: RepoStatus }) {
|
|
36
|
-
return (
|
|
37
|
-
<box flexDirection="column" gap={1}>
|
|
38
|
-
<box flexDirection="column">
|
|
39
|
-
<StatRow label="branch" value={props.status.current_branch} />
|
|
40
|
-
<StatRow label="remote" value={props.status.remote_url ?? "(none)"} />
|
|
41
|
-
</box>
|
|
42
|
-
|
|
43
|
-
<Show when={props.status.ahead > 0 || props.status.behind > 0}>
|
|
44
|
-
<box flexDirection="row" height={1} gap={2}>
|
|
45
|
-
<text fg={theme.status.ahead} content={`↑ ${props.status.ahead} ahead`} />
|
|
46
|
-
<text fg={theme.status.behind} content={`↓ ${props.status.behind} behind`} />
|
|
47
|
-
</box>
|
|
48
|
-
</Show>
|
|
49
|
-
|
|
50
|
-
<Show
|
|
51
|
-
when={
|
|
52
|
-
props.status.modified_count > 0 ||
|
|
53
|
-
props.status.staged_count > 0 ||
|
|
54
|
-
props.status.untracked_count > 0 ||
|
|
55
|
-
props.status.conflict_count > 0
|
|
56
|
-
}
|
|
57
|
-
>
|
|
58
|
-
<box flexDirection="column">
|
|
59
|
-
<Show when={props.status.modified_count > 0 || props.status.staged_count > 0}>
|
|
60
|
-
<box flexDirection="row" height={1} gap={2}>
|
|
61
|
-
<text fg={theme.status.modified} content={`~ ${props.status.modified_count} modified`} />
|
|
62
|
-
<text fg={theme.green} content={`+ ${props.status.staged_count} staged`} />
|
|
63
|
-
</box>
|
|
64
|
-
</Show>
|
|
65
|
-
<Show when={props.status.untracked_count > 0 || props.status.conflict_count > 0}>
|
|
66
|
-
<box flexDirection="row" height={1} gap={2}>
|
|
67
|
-
<text fg={theme.status.untracked} content={`? ${props.status.untracked_count} untracked`} />
|
|
68
|
-
<text fg={theme.status.conflict} content={`! ${props.status.conflict_count} conflicts`} />
|
|
69
|
-
</box>
|
|
70
|
-
</Show>
|
|
71
|
-
</box>
|
|
72
|
-
</Show>
|
|
73
|
-
|
|
74
|
-
<Show when={props.status.stash_count > 0}>
|
|
75
|
-
<text fg={theme.status.stash} content={`✂ ${props.status.stash_count} stashes`} />
|
|
76
|
-
</Show>
|
|
77
|
-
|
|
78
|
-
<StatRow label="last commit" value={formatRelativeTime(props.status.head_time)} />
|
|
79
|
-
</box>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function StatsPanel(props: StatsPanelProps) {
|
|
84
|
-
return (
|
|
85
|
-
<box
|
|
86
|
-
borderStyle="rounded"
|
|
87
|
-
borderColor={props.focused ? theme.border_highlight : theme.border}
|
|
88
|
-
title={`stats: ${props.repoName}`}
|
|
89
|
-
titleAlignment="left"
|
|
90
|
-
flexDirection="column"
|
|
91
|
-
flexGrow={1}
|
|
92
|
-
height={props.height}
|
|
93
|
-
padding={1}
|
|
94
|
-
gap={1}
|
|
95
|
-
>
|
|
96
|
-
<Show
|
|
97
|
-
when={!props.loading}
|
|
98
|
-
fallback={<text fg={theme.fg_dim}>loading...</text>}
|
|
99
|
-
>
|
|
100
|
-
<Show
|
|
101
|
-
when={props.status}
|
|
102
|
-
fallback={<text fg={theme.fg_dim}>(select a repo)</text>}
|
|
103
|
-
>
|
|
104
|
-
{(status) => (
|
|
105
|
-
<Show
|
|
106
|
-
when={!status().is_clean}
|
|
107
|
-
fallback={<CleanState status={status()} />}
|
|
108
|
-
>
|
|
109
|
-
<StatusDetails status={status()} />
|
|
110
|
-
</Show>
|
|
111
|
-
)}
|
|
112
|
-
</Show>
|
|
113
|
-
</Show>
|
|
114
|
-
</box>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { For } from "solid-js";
|
|
2
|
-
import type { RepoStatus } from "@overview/core";
|
|
3
|
-
import { theme } from "../theme";
|
|
4
|
-
|
|
5
|
-
interface StatusBadgeProps {
|
|
6
|
-
status: RepoStatus | null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface BadgePart {
|
|
10
|
-
text: string;
|
|
11
|
-
color: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function buildBadgeParts(status: RepoStatus): BadgePart[] {
|
|
15
|
-
const parts: BadgePart[] = [];
|
|
16
|
-
|
|
17
|
-
if (status.ocn_status) {
|
|
18
|
-
switch (status.ocn_status.status) {
|
|
19
|
-
case "busy":
|
|
20
|
-
parts.push({ text: "*", color: theme.yellow });
|
|
21
|
-
break;
|
|
22
|
-
case "prompting":
|
|
23
|
-
parts.push({ text: ">", color: theme.magenta });
|
|
24
|
-
break;
|
|
25
|
-
case "error":
|
|
26
|
-
parts.push({ text: "!", color: theme.red });
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (status.health === "conflict") {
|
|
32
|
-
parts.push({ text: "!", color: theme.status.conflict });
|
|
33
|
-
} else if (status.health === "clean" && status.modified_count === 0 && status.untracked_count === 0) {
|
|
34
|
-
parts.push({ text: "✓", color: theme.status.clean });
|
|
35
|
-
} else {
|
|
36
|
-
if (status.modified_count > 0) {
|
|
37
|
-
parts.push({ text: `~${status.modified_count}`, color: theme.status.modified });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (status.untracked_count > 0 && status.modified_count === 0 && status.ahead === 0 && status.behind === 0) {
|
|
41
|
-
parts.push({ text: "?", color: theme.status.untracked });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (status.ahead > 0) {
|
|
45
|
-
parts.push({ text: `↑${status.ahead}`, color: theme.status.ahead });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (status.behind > 0) {
|
|
49
|
-
parts.push({ text: `↓${status.behind}`, color: theme.status.behind });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return parts;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function StatusBadge(props: StatusBadgeProps) {
|
|
57
|
-
const parts = () => {
|
|
58
|
-
const s = props.status;
|
|
59
|
-
if (!s) return [{ text: "…", color: theme.fg_dim }] as BadgePart[];
|
|
60
|
-
return buildBadgeParts(s);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<box flexDirection="row">
|
|
65
|
-
<For each={parts()}>
|
|
66
|
-
{(p) => <text fg={p.color} content={p.text} />}
|
|
67
|
-
</For>
|
|
68
|
-
</box>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { createMemo, Show } from "solid-js";
|
|
2
|
-
import { theme } from "../theme";
|
|
3
|
-
|
|
4
|
-
export type AppMode = "NORMAL" | "DETAIL" | "SEARCH" | "HELP";
|
|
5
|
-
|
|
6
|
-
interface StatusBarProps {
|
|
7
|
-
mode: AppMode;
|
|
8
|
-
repoCount: number;
|
|
9
|
-
dirtyCount: number;
|
|
10
|
-
aheadCount: number;
|
|
11
|
-
scanning: boolean;
|
|
12
|
-
message: string | null;
|
|
13
|
-
widgetSummary?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const KEY_HINTS: Record<AppMode, string> = {
|
|
17
|
-
NORMAL: "j/k:nav Enter:expand g:ggi r:refresh q:quit ?:help",
|
|
18
|
-
DETAIL: "j/k:scroll h/l:panel g:ggi r:refresh q:back",
|
|
19
|
-
SEARCH: "type to filter Esc:cancel",
|
|
20
|
-
HELP: "q:close",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function StatusBar(props: StatusBarProps) {
|
|
24
|
-
const keyHints = createMemo(() => KEY_HINTS[props.mode]);
|
|
25
|
-
|
|
26
|
-
const summaryColor = createMemo(() => {
|
|
27
|
-
if (props.scanning) return theme.fg_dim;
|
|
28
|
-
if (props.dirtyCount === 0 && props.aheadCount === 0) return theme.green;
|
|
29
|
-
return theme.yellow;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const summaryText = createMemo(() => {
|
|
33
|
-
if (props.scanning) return "scanning...";
|
|
34
|
-
if (props.dirtyCount === 0 && props.aheadCount === 0) {
|
|
35
|
-
return `✓ all ${props.repoCount} repos clean`;
|
|
36
|
-
}
|
|
37
|
-
const parts: string[] = [];
|
|
38
|
-
if (props.dirtyCount > 0) parts.push(`${props.dirtyCount} dirty`);
|
|
39
|
-
if (props.aheadCount > 0) parts.push(`${props.aheadCount} ahead`);
|
|
40
|
-
return parts.join(", ");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<box height={1} width="100%" backgroundColor={theme.bg_dark} flexDirection="row" paddingLeft={1} paddingRight={1}>
|
|
45
|
-
<text fg={theme.blue} content={`[${props.mode}]`} />
|
|
46
|
-
<Show when={props.message} fallback={<text fg={theme.fg_dim} content={` ${keyHints()}`} />}>
|
|
47
|
-
<text fg={theme.yellow} content={` ${props.message}`} />
|
|
48
|
-
</Show>
|
|
49
|
-
<box flexGrow={1} />
|
|
50
|
-
<Show when={props.widgetSummary}>
|
|
51
|
-
<text fg={theme.fg_dim} content={`${props.widgetSummary} `} />
|
|
52
|
-
</Show>
|
|
53
|
-
<text fg={summaryColor()} content={summaryText()} />
|
|
54
|
-
</box>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { For, Show, createMemo, createSignal, createEffect, on } from "solid-js";
|
|
2
|
-
import { useKeyboard } from "@opentui/solid";
|
|
3
|
-
import type { ScrollBoxRenderable, Renderable } from "@opentui/core";
|
|
4
|
-
import type { RepoStatus, WidgetConfig, WidgetId } from "@overview/core";
|
|
5
|
-
import { getWidget } from "./widgets/registry";
|
|
6
|
-
import "./widgets/index";
|
|
7
|
-
import { theme } from "../theme";
|
|
8
|
-
import {
|
|
9
|
-
computeGridLayout,
|
|
10
|
-
buildBorderLine,
|
|
11
|
-
buildBorderLineWithTitle,
|
|
12
|
-
getWidgetBorderSides,
|
|
13
|
-
type GridWidget,
|
|
14
|
-
type GridRow,
|
|
15
|
-
} from "../lib/widget-grid";
|
|
16
|
-
|
|
17
|
-
interface WidgetContainerProps {
|
|
18
|
-
status: RepoStatus | null;
|
|
19
|
-
repoName: string;
|
|
20
|
-
loading: boolean;
|
|
21
|
-
focused: boolean;
|
|
22
|
-
height: number | `${number}%` | "auto";
|
|
23
|
-
availableWidth: number;
|
|
24
|
-
widgetConfigs: WidgetConfig[];
|
|
25
|
-
onWidgetConfigChange?: (configs: WidgetConfig[]) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function WidgetContainer(props: WidgetContainerProps) {
|
|
29
|
-
const [focused_idx, setFocusedIdx] = createSignal(0);
|
|
30
|
-
let scrollbox_ref: ScrollBoxRenderable | undefined;
|
|
31
|
-
const row_refs = new Map<number, Renderable>();
|
|
32
|
-
|
|
33
|
-
const enabled_widgets = createMemo(() => {
|
|
34
|
-
const configs = props.widgetConfigs
|
|
35
|
-
.filter((c) => c.enabled)
|
|
36
|
-
.sort((a, b) => a.priority - b.priority);
|
|
37
|
-
return configs
|
|
38
|
-
.map((c): GridWidget | null => {
|
|
39
|
-
const def = getWidget(c.id);
|
|
40
|
-
if (!def) return null;
|
|
41
|
-
return { id: c.id, size_hint: def.size_hint, config: c };
|
|
42
|
-
})
|
|
43
|
-
.filter((gw): gw is GridWidget => gw !== null);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const content_width = createMemo(() => props.availableWidth - 1);
|
|
47
|
-
|
|
48
|
-
const grid_layout = createMemo(() =>
|
|
49
|
-
computeGridLayout(enabled_widgets(), content_width()),
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const flat_widget_ids = createMemo(() => {
|
|
53
|
-
const rows = grid_layout().rows;
|
|
54
|
-
const ids: WidgetId[] = [];
|
|
55
|
-
for (const row of rows) {
|
|
56
|
-
for (const w of row.widgets) {
|
|
57
|
-
ids.push(w.id);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return ids;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
function scrollToFocused() {
|
|
64
|
-
if (!scrollbox_ref) return;
|
|
65
|
-
const focused_id = flat_widget_ids()[focused_idx()];
|
|
66
|
-
if (!focused_id) return;
|
|
67
|
-
|
|
68
|
-
const rows = grid_layout().rows;
|
|
69
|
-
|
|
70
|
-
// Find which row contains the focused widget
|
|
71
|
-
let target_row_index = -1;
|
|
72
|
-
for (let i = 0; i < rows.length; i++) {
|
|
73
|
-
if (rows[i]!.widgets.some((w) => w.id === focused_id)) {
|
|
74
|
-
target_row_index = i;
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (target_row_index < 0) return;
|
|
79
|
-
|
|
80
|
-
// Get the rendered element for this row
|
|
81
|
-
const row_el = row_refs.get(target_row_index);
|
|
82
|
-
if (!row_el) return;
|
|
83
|
-
|
|
84
|
-
// el.y is screen-absolute, not relative to scrollbox content.
|
|
85
|
-
// Use content.y as origin — both are screen-absolute and shift together,
|
|
86
|
-
// so their difference gives stable content-relative position.
|
|
87
|
-
const scroll_top = scrollbox_ref.scrollTop;
|
|
88
|
-
const content_y = row_el.y - scrollbox_ref.content.y;
|
|
89
|
-
const content_h = row_el.height;
|
|
90
|
-
|
|
91
|
-
// The region we want fully visible (in content-space):
|
|
92
|
-
// - 1 line above for the border/title text
|
|
93
|
-
// - the row box itself
|
|
94
|
-
// - 1 line below for the bottom border (only last row)
|
|
95
|
-
const region_top = content_y - 1;
|
|
96
|
-
const is_last = target_row_index === rows.length - 1;
|
|
97
|
-
const region_bottom = content_y + content_h + (is_last ? 1 : 0);
|
|
98
|
-
|
|
99
|
-
// Viewport bounds (in content-space)
|
|
100
|
-
const vp_top = scroll_top;
|
|
101
|
-
const vp_height = scrollbox_ref.viewport?.height ?? scrollbox_ref.height;
|
|
102
|
-
const vp_bottom = vp_top + vp_height;
|
|
103
|
-
|
|
104
|
-
const top_visible = region_top >= vp_top;
|
|
105
|
-
const bottom_visible = region_bottom <= vp_bottom;
|
|
106
|
-
const region_height = region_bottom - region_top;
|
|
107
|
-
|
|
108
|
-
// Already fully in view — do nothing
|
|
109
|
-
if (top_visible && bottom_visible) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Region fits within viewport — scroll minimally
|
|
114
|
-
if (region_height <= vp_height) {
|
|
115
|
-
if (!top_visible) {
|
|
116
|
-
// Top is clipped — align top of region with top of viewport
|
|
117
|
-
const target = Math.max(0, region_top);
|
|
118
|
-
scrollbox_ref.scrollTo({ x: 0, y: target });
|
|
119
|
-
} else {
|
|
120
|
-
// Bottom is clipped — align bottom of region with bottom of viewport
|
|
121
|
-
const target = region_bottom - vp_height;
|
|
122
|
-
scrollbox_ref.scrollTo({ x: 0, y: target });
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
// Region taller than viewport — show the top
|
|
126
|
-
const target = Math.max(0, region_top);
|
|
127
|
-
scrollbox_ref.scrollTo({ x: 0, y: target });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
createEffect(on(focused_idx, () => scrollToFocused()));
|
|
132
|
-
|
|
133
|
-
useKeyboard((key) => {
|
|
134
|
-
if (!props.focused) return;
|
|
135
|
-
|
|
136
|
-
const ids = flat_widget_ids();
|
|
137
|
-
if (ids.length === 0) return;
|
|
138
|
-
|
|
139
|
-
switch (key.name) {
|
|
140
|
-
case "j":
|
|
141
|
-
setFocusedIdx(Math.min(focused_idx() + 1, ids.length - 1));
|
|
142
|
-
return;
|
|
143
|
-
case "k":
|
|
144
|
-
setFocusedIdx(Math.max(focused_idx() - 1, 0));
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (key.raw === "c") {
|
|
149
|
-
const widget_id = ids[focused_idx()];
|
|
150
|
-
if (widget_id) {
|
|
151
|
-
const updated = props.widgetConfigs.map((c) =>
|
|
152
|
-
c.id === widget_id ? { ...c, collapsed: !c.collapsed } : c,
|
|
153
|
-
);
|
|
154
|
-
props.onWidgetConfigChange?.(updated);
|
|
155
|
-
}
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (key.raw === "C") {
|
|
160
|
-
const all_collapsed = props.widgetConfigs.every((c) => !c.enabled || c.collapsed);
|
|
161
|
-
const updated = props.widgetConfigs.map((c) => ({ ...c, collapsed: !all_collapsed }));
|
|
162
|
-
props.onWidgetConfigChange?.(updated);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
function flatIndexOf(widget_id: WidgetId): number {
|
|
168
|
-
return flat_widget_ids().indexOf(widget_id);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function isFocused(widget_id: WidgetId): boolean {
|
|
172
|
-
return props.focused && flatIndexOf(widget_id) === focused_idx();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function borderLine(type: "top" | "mid" | "bottom", prev: GridRow | null, next: GridRow | null): string {
|
|
176
|
-
return buildBorderLine(type, content_width(), prev, next);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<box flexDirection="column" width={props.availableWidth} height={props.height}>
|
|
181
|
-
<Show
|
|
182
|
-
when={!props.loading}
|
|
183
|
-
fallback={<text fg={theme.fg_dim} content="loading..." />}
|
|
184
|
-
>
|
|
185
|
-
<Show
|
|
186
|
-
when={props.status}
|
|
187
|
-
fallback={<text fg={theme.fg_dim} content="(select a repo)" />}
|
|
188
|
-
>
|
|
189
|
-
<scrollbox ref={scrollbox_ref} flexGrow={1}>
|
|
190
|
-
<box flexDirection="column" width={content_width()} flexShrink={0}>
|
|
191
|
-
<For each={grid_layout().rows}>
|
|
192
|
-
{(row, row_index) => {
|
|
193
|
-
const rows = grid_layout().rows;
|
|
194
|
-
const prev_row = () => row_index() > 0 ? rows[row_index() - 1]! : null;
|
|
195
|
-
const is_first = () => row_index() === 0;
|
|
196
|
-
const is_last = () => row_index() === rows.length - 1;
|
|
197
|
-
|
|
198
|
-
const top_line = () => {
|
|
199
|
-
const type = is_first() ? "top" : "mid";
|
|
200
|
-
const line = borderLine(type, prev_row(), row);
|
|
201
|
-
if (is_first()) {
|
|
202
|
-
return buildBorderLineWithTitle(line, `widgets: ${props.repoName}`);
|
|
203
|
-
}
|
|
204
|
-
return line;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<>
|
|
209
|
-
<text fg={props.focused && is_first() ? theme.border_highlight : theme.border} content={top_line()} />
|
|
210
|
-
|
|
211
|
-
<box ref={(el: Renderable) => { row_refs.set(row_index(), el); }} flexDirection="row" alignItems="stretch" width={content_width()}>
|
|
212
|
-
<For each={row.widgets}>
|
|
213
|
-
{(gw, widget_idx) => {
|
|
214
|
-
const def = getWidget(gw.id);
|
|
215
|
-
if (!def) return null;
|
|
216
|
-
|
|
217
|
-
const focused = () => isFocused(gw.id);
|
|
218
|
-
const box_width = () => {
|
|
219
|
-
if (row.columns === 1) return content_width();
|
|
220
|
-
if (row.columns === 2) {
|
|
221
|
-
const junction = Math.floor(content_width() / 2);
|
|
222
|
-
if (widget_idx() === 0) return junction;
|
|
223
|
-
return content_width() - junction;
|
|
224
|
-
}
|
|
225
|
-
// 3-column
|
|
226
|
-
const j1 = Math.floor(content_width() / 3);
|
|
227
|
-
const j2 = Math.floor(2 * content_width() / 3);
|
|
228
|
-
if (widget_idx() === 0) return j1;
|
|
229
|
-
if (widget_idx() === 1) return j2 - j1;
|
|
230
|
-
return content_width() - j2;
|
|
231
|
-
};
|
|
232
|
-
const widget_content_width = () => {
|
|
233
|
-
const sides = getWidgetBorderSides(row, widget_idx());
|
|
234
|
-
return Math.max(1, box_width() - sides.length);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
return (
|
|
238
|
-
<box
|
|
239
|
-
width={box_width()}
|
|
240
|
-
border={getWidgetBorderSides(row, widget_idx())}
|
|
241
|
-
borderStyle="rounded"
|
|
242
|
-
borderColor={(focused() || (props.focused && (widget_idx() === 0 || widget_idx() === row.columns - 1))) ? theme.border_highlight : theme.border}
|
|
243
|
-
backgroundColor={focused() ? theme.bg_highlight : undefined}
|
|
244
|
-
flexDirection="column"
|
|
245
|
-
minHeight={gw.size_hint.min_height}
|
|
246
|
-
overflow="hidden"
|
|
247
|
-
>
|
|
248
|
-
<Show
|
|
249
|
-
when={!gw.config.collapsed}
|
|
250
|
-
fallback={
|
|
251
|
-
<text
|
|
252
|
-
fg={focused() ? theme.yellow : theme.fg_dim}
|
|
253
|
-
content={focused() ? `▸ [>] ${def.label} (collapsed)` : `[>] ${def.label} (collapsed)`}
|
|
254
|
-
/>
|
|
255
|
-
}
|
|
256
|
-
>
|
|
257
|
-
<text
|
|
258
|
-
fg={focused() ? theme.yellow : theme.fg_dim}
|
|
259
|
-
content={focused() ? `▸ ${def.label}` : ` ${def.label}`}
|
|
260
|
-
/>
|
|
261
|
-
<def.component
|
|
262
|
-
width={widget_content_width()}
|
|
263
|
-
focused={focused()}
|
|
264
|
-
status={props.status}
|
|
265
|
-
/>
|
|
266
|
-
</Show>
|
|
267
|
-
</box>
|
|
268
|
-
);
|
|
269
|
-
}}
|
|
270
|
-
</For>
|
|
271
|
-
</box>
|
|
272
|
-
|
|
273
|
-
<Show when={is_last()}>
|
|
274
|
-
<text fg={props.focused ? theme.border_highlight : theme.border} content={borderLine("bottom", row, null)} />
|
|
275
|
-
</Show>
|
|
276
|
-
</>
|
|
277
|
-
);
|
|
278
|
-
}}
|
|
279
|
-
</For>
|
|
280
|
-
</box>
|
|
281
|
-
</scrollbox>
|
|
282
|
-
</Show>
|
|
283
|
-
</Show>
|
|
284
|
-
</box>
|
|
285
|
-
);
|
|
286
|
-
}
|