@profullstack/hqtui-demo 0.2.0 → 0.4.0
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/README.md +8 -2
- package/dist/main.js +34 -7
- package/dist/main.js.map +1 -1
- package/dist/screens/components.js +5 -4
- package/dist/screens/components.js.map +1 -1
- package/dist/screens/dashboard.js +46 -36
- package/dist/screens/dashboard.js.map +1 -1
- package/dist/screens/graphics.js +5 -3
- package/dist/screens/graphics.js.map +1 -1
- package/dist/screens/index.js +1 -0
- package/dist/screens/index.js.map +1 -1
- package/dist/screens/input.js +3 -1
- package/dist/screens/input.js.map +1 -1
- package/dist/screens/network.js +5 -4
- package/dist/screens/network.js.map +1 -1
- package/dist/screens/services.js +4 -3
- package/dist/screens/services.js.map +1 -1
- package/dist/screens/sessions.js +5 -4
- package/dist/screens/sessions.js.map +1 -1
- package/dist/screens/stress.js +3 -1
- package/dist/screens/stress.js.map +1 -1
- package/dist/screens/themes.js +3 -1
- package/dist/screens/themes.js.map +1 -1
- package/dist/screens/traffic.js +6 -5
- package/dist/screens/traffic.js.map +1 -1
- package/dist/screens/world.js +97 -0
- package/dist/screens/world.js.map +1 -0
- package/dist/state.js +17 -1
- package/dist/state.js.map +1 -1
- package/package.json +2 -2
- package/src/main.ts +28 -7
- package/src/screens/components.ts +5 -4
- package/src/screens/dashboard.ts +47 -37
- package/src/screens/graphics.ts +5 -4
- package/src/screens/index.ts +1 -0
- package/src/screens/input.ts +3 -2
- package/src/screens/network.ts +5 -4
- package/src/screens/services.ts +4 -3
- package/src/screens/sessions.ts +5 -4
- package/src/screens/stress.ts +3 -2
- package/src/screens/themes.ts +3 -2
- package/src/screens/traffic.ts +6 -5
- package/src/screens/world.ts +102 -0
- package/src/state.ts +31 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
|
+
import { WORLD_COUNTRIES, WORLD_X, WORLD_Y, countryBounds } from "@profullstack/hqtui";
|
|
3
|
+
import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
|
|
4
|
+
|
|
5
|
+
/** How many coordinates a country's outlines are drawn from. */
|
|
6
|
+
function pointCount(rings: number[][]): number {
|
|
7
|
+
let total = 0;
|
|
8
|
+
for (const ring of rings) total += ring.length / 2;
|
|
9
|
+
return total;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function degrees(value: number, positive: string, negative: string): string {
|
|
13
|
+
return `${Math.abs(value).toFixed(1)}°${value >= 0 ? positive : negative}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The clickable world map.
|
|
18
|
+
*
|
|
19
|
+
* The country list and the map are two views of one selection rather than two
|
|
20
|
+
* selections that have to be kept in step: the list's pane index *is* which
|
|
21
|
+
* country is picked, so the arrows move the highlight on the map for free, and
|
|
22
|
+
* a click on the map only has to move the list.
|
|
23
|
+
*/
|
|
24
|
+
export function worldScreen(ui: Container, state: DemoState, theme: Theme): void {
|
|
25
|
+
const gap = panelGap(state);
|
|
26
|
+
const countries = pane(state, "world.countries", WORLD_COUNTRIES.length);
|
|
27
|
+
const selected = WORLD_COUNTRIES[countries.selected];
|
|
28
|
+
const hovered = state.worldHovered ? WORLD_COUNTRIES.find((c) => c.name === state.worldHovered) : undefined;
|
|
29
|
+
|
|
30
|
+
// Zooming is only meaningful with something to zoom to, and a country whose
|
|
31
|
+
// outlines are a single pixel would otherwise zoom to an empty window.
|
|
32
|
+
const window = state.worldZoom && selected ? countryBounds(selected) : { x: WORLD_X, y: WORLD_Y };
|
|
33
|
+
|
|
34
|
+
ui.row({ size: "1fr", gap }, (row) => {
|
|
35
|
+
row.panel({
|
|
36
|
+
title: "World",
|
|
37
|
+
subtitle: hovered ? hovered.name : selected?.name ?? "click a country",
|
|
38
|
+
subtitleColor: hovered ? theme.accent : theme.muted,
|
|
39
|
+
borderColor: theme.primary,
|
|
40
|
+
footer: state.worldZoom ? "click select · z zoom · r whole globe" : "click select · z zoom to selection",
|
|
41
|
+
}, (p) => {
|
|
42
|
+
p.worldMap({
|
|
43
|
+
...window,
|
|
44
|
+
color: theme.border,
|
|
45
|
+
highlight: [selected?.iso || selected?.name || "", hovered?.iso || hovered?.name || ""],
|
|
46
|
+
highlightColor: theme.accent,
|
|
47
|
+
onSelect: (country) => {
|
|
48
|
+
if (!country) return;
|
|
49
|
+
const index = WORLD_COUNTRIES.findIndex((c) => c.name === country.name);
|
|
50
|
+
if (index >= 0) {
|
|
51
|
+
countries.selected = index;
|
|
52
|
+
focusPane(state, "world.countries");
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
onHover: (country) => {
|
|
56
|
+
state.worldHovered = country?.name ?? "";
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
row.column({ width: 34, gap, bordered: true }, (column) => {
|
|
62
|
+
column.panel({ title: "Selection", size: 9, borderColor: theme.accent }, (p) => {
|
|
63
|
+
if (!selected) {
|
|
64
|
+
p.label("Nothing selected.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const b = countryBounds(selected);
|
|
68
|
+
p.keyValues([
|
|
69
|
+
{ label: "Country", value: selected.name, color: theme.accent },
|
|
70
|
+
{ label: "ISO", value: selected.iso || "—" },
|
|
71
|
+
{ label: "Longitude", value: `${degrees(b.x.min, "E", "W")} – ${degrees(b.x.max, "E", "W")}`, color: theme.primary },
|
|
72
|
+
{ label: "Latitude", value: `${degrees(b.y.min, "N", "S")} – ${degrees(b.y.max, "N", "S")}`, color: theme.primary },
|
|
73
|
+
{ label: "Outlines", value: String(selected.rings.length), color: theme.muted },
|
|
74
|
+
{ label: "Points", value: String(pointCount(selected.rings)), color: theme.muted },
|
|
75
|
+
], { spread: false });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
column.panel({
|
|
79
|
+
title: "Countries",
|
|
80
|
+
subtitle: String(WORLD_COUNTRIES.length),
|
|
81
|
+
size: "1fr",
|
|
82
|
+
borderColor: theme.secondary,
|
|
83
|
+
}, (p) => {
|
|
84
|
+
p.table({
|
|
85
|
+
rows: WORLD_COUNTRIES as unknown as Array<{ name: string; iso: string }>,
|
|
86
|
+
selected: countries.selected,
|
|
87
|
+
offset: countries.offset,
|
|
88
|
+
followSelection: true,
|
|
89
|
+
onScroll: (delta) => scrollPane(countries, delta),
|
|
90
|
+
onFocus: () => focusPane(state, "world.countries"),
|
|
91
|
+
onSelectRow: (row) => { countries.selected = countries.offset + row; },
|
|
92
|
+
zebra: true,
|
|
93
|
+
scrollbar: true,
|
|
94
|
+
columns: [
|
|
95
|
+
{ key: "name", title: "Country", min: 16, color: theme.foreground },
|
|
96
|
+
{ key: "iso", title: "ISO", width: 4, color: theme.muted },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -3,13 +3,23 @@ import type { SystemSample } from "./system/index.ts";
|
|
|
3
3
|
|
|
4
4
|
export type ScreenName =
|
|
5
5
|
| "dashboard" | "traffic" | "sessions" | "network" | "services"
|
|
6
|
-
| "components" | "graphics" | "themes" | "input" | "stress";
|
|
6
|
+
| "components" | "graphics" | "themes" | "input" | "stress" | "world";
|
|
7
7
|
|
|
8
8
|
export const SCREENS: ScreenName[] = [
|
|
9
9
|
"dashboard", "traffic", "sessions", "network", "services",
|
|
10
|
-
"components", "graphics", "themes", "input", "stress",
|
|
10
|
+
"components", "graphics", "themes", "input", "stress", "world",
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The key that jumps straight to each screen. Digits run out at ten, so the
|
|
15
|
+
* eleventh takes a letter rather than a second "1" that would shadow the first.
|
|
16
|
+
*/
|
|
17
|
+
export const SCREEN_KEYS: Record<ScreenName, string> = {
|
|
18
|
+
dashboard: "1", traffic: "2", sessions: "3", network: "4", services: "5",
|
|
19
|
+
components: "6", graphics: "7", themes: "8", input: "9", stress: "0",
|
|
20
|
+
world: "w",
|
|
21
|
+
};
|
|
22
|
+
|
|
13
23
|
export interface DemoState {
|
|
14
24
|
sample: SystemSample;
|
|
15
25
|
screen: ScreenName;
|
|
@@ -26,6 +36,13 @@ export interface DemoState {
|
|
|
26
36
|
/** Which pane the arrow keys drive, per screen. Clicking a pane sets it. */
|
|
27
37
|
focused: Partial<Record<ScreenName, string>>;
|
|
28
38
|
sort: "cpu" | "mem" | "pid" | "name";
|
|
39
|
+
/**
|
|
40
|
+
* Whether the [c] key has merged adjacent panel borders. Collapsing only
|
|
41
|
+
* happens where two bordered siblings actually touch, so the screens read
|
|
42
|
+
* this to lay out at a gap of zero; leaving them at one meant the key
|
|
43
|
+
* toggled a flag that could never reach a seam and nothing moved.
|
|
44
|
+
*/
|
|
45
|
+
collapsed: boolean;
|
|
29
46
|
filter: string;
|
|
30
47
|
filtering: boolean;
|
|
31
48
|
showHelp: boolean;
|
|
@@ -35,6 +52,10 @@ export interface DemoState {
|
|
|
35
52
|
paletteIndex: number;
|
|
36
53
|
themeIndex: number;
|
|
37
54
|
/** Component-showcase interactive state. */
|
|
55
|
+
/** The country under the pointer on the world map, by name; "" for open water. */
|
|
56
|
+
worldHovered: string;
|
|
57
|
+
/** Whether the world map is framed on the selection rather than the globe. */
|
|
58
|
+
worldZoom: boolean;
|
|
38
59
|
toggle: boolean;
|
|
39
60
|
checkbox: boolean;
|
|
40
61
|
selectOpen: boolean;
|
|
@@ -51,6 +72,11 @@ export interface DemoState {
|
|
|
51
72
|
bytes: number;
|
|
52
73
|
}
|
|
53
74
|
|
|
75
|
+
/** The seam between panels: zero while collapsed, so their borders can merge. */
|
|
76
|
+
export function panelGap(state: DemoState): number {
|
|
77
|
+
return state.collapsed ? 0 : 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
export interface Pane {
|
|
55
81
|
selected: number;
|
|
56
82
|
offset: number;
|
|
@@ -129,6 +155,9 @@ export function createState(
|
|
|
129
155
|
panes: {},
|
|
130
156
|
focused: {},
|
|
131
157
|
sort: "cpu",
|
|
158
|
+
collapsed: false,
|
|
159
|
+
worldHovered: "",
|
|
160
|
+
worldZoom: false,
|
|
132
161
|
filter: "",
|
|
133
162
|
filtering: false,
|
|
134
163
|
showHelp: false,
|