@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.
Files changed (44) hide show
  1. package/README.md +8 -2
  2. package/dist/main.js +34 -7
  3. package/dist/main.js.map +1 -1
  4. package/dist/screens/components.js +5 -4
  5. package/dist/screens/components.js.map +1 -1
  6. package/dist/screens/dashboard.js +46 -36
  7. package/dist/screens/dashboard.js.map +1 -1
  8. package/dist/screens/graphics.js +5 -3
  9. package/dist/screens/graphics.js.map +1 -1
  10. package/dist/screens/index.js +1 -0
  11. package/dist/screens/index.js.map +1 -1
  12. package/dist/screens/input.js +3 -1
  13. package/dist/screens/input.js.map +1 -1
  14. package/dist/screens/network.js +5 -4
  15. package/dist/screens/network.js.map +1 -1
  16. package/dist/screens/services.js +4 -3
  17. package/dist/screens/services.js.map +1 -1
  18. package/dist/screens/sessions.js +5 -4
  19. package/dist/screens/sessions.js.map +1 -1
  20. package/dist/screens/stress.js +3 -1
  21. package/dist/screens/stress.js.map +1 -1
  22. package/dist/screens/themes.js +3 -1
  23. package/dist/screens/themes.js.map +1 -1
  24. package/dist/screens/traffic.js +6 -5
  25. package/dist/screens/traffic.js.map +1 -1
  26. package/dist/screens/world.js +97 -0
  27. package/dist/screens/world.js.map +1 -0
  28. package/dist/state.js +17 -1
  29. package/dist/state.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/main.ts +28 -7
  32. package/src/screens/components.ts +5 -4
  33. package/src/screens/dashboard.ts +47 -37
  34. package/src/screens/graphics.ts +5 -4
  35. package/src/screens/index.ts +1 -0
  36. package/src/screens/input.ts +3 -2
  37. package/src/screens/network.ts +5 -4
  38. package/src/screens/services.ts +4 -3
  39. package/src/screens/sessions.ts +5 -4
  40. package/src/screens/stress.ts +3 -2
  41. package/src/screens/themes.ts +3 -2
  42. package/src/screens/traffic.ts +6 -5
  43. package/src/screens/world.ts +102 -0
  44. 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,