create-rezi 0.1.0-alpha.30 → 0.1.0-alpha.32
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/package.json +2 -2
- package/templates/animation-lab/README.md +1 -0
- package/templates/animation-lab/src/main.ts +3 -8
- package/templates/cli-tool/README.md +2 -1
- package/templates/cli-tool/src/screens/home.ts +20 -10
- package/templates/cli-tool/src/screens/logs.ts +47 -43
- package/templates/cli-tool/src/screens/settings.ts +30 -24
- package/templates/cli-tool/src/screens/shell.ts +49 -28
- package/templates/dashboard/README.md +2 -0
- package/templates/dashboard/src/screens/overview.ts +108 -73
- package/templates/minimal/README.md +1 -0
- package/templates/minimal/src/screens/main-screen.ts +47 -33
- package/templates/stress-test/src/main.ts +6 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-rezi",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.32",
|
|
4
4
|
"description": "Scaffold a Rezi terminal UI app.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://rezitui.dev",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"bun": ">=1.3.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@rezi-ui/testkit": "0.1.0-alpha.
|
|
31
|
+
"@rezi-ui/testkit": "0.1.0-alpha.32"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -5,6 +5,7 @@ Scaffolded with `create-rezi` using the **__TEMPLATE_LABEL__** template.
|
|
|
5
5
|
## What This Template Demonstrates
|
|
6
6
|
|
|
7
7
|
- Declarative animation hooks with first-class primitives: `useTransition`, `useSpring`, `useSequence`, `useStagger`.
|
|
8
|
+
- Built-in easing presets in action (`linear`, quad, and cubic families).
|
|
8
9
|
- Canvas-driven reactor visualization combined with charts, gauges, and staggered module rails.
|
|
9
10
|
- Reducer-managed animation targets with deterministic tick updates.
|
|
10
11
|
- Responsive layout that adapts to terminal resize events.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { exit } from "node:process";
|
|
2
|
-
import {
|
|
3
|
-
import { createNodeBackend } from "@rezi-ui/node";
|
|
2
|
+
import { createNodeApp } from "@rezi-ui/node";
|
|
4
3
|
import { resolveAnimationLabCommand } from "./helpers/keybindings.js";
|
|
5
4
|
import { createInitialState, reduceAnimationLabState } from "./helpers/state.js";
|
|
6
5
|
import { renderReactorLab } from "./screens/reactor-lab.js";
|
|
@@ -11,16 +10,12 @@ function describeThrown(error: unknown): string {
|
|
|
11
10
|
return String(error);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const app =
|
|
15
|
-
backend: createNodeBackend({
|
|
16
|
-
fpsCap: 30,
|
|
17
|
-
executionMode: "inline",
|
|
18
|
-
}),
|
|
13
|
+
const app = createNodeApp({
|
|
19
14
|
initialState: createInitialState({
|
|
20
15
|
cols: typeof process.stdout.columns === "number" ? process.stdout.columns : 96,
|
|
21
16
|
rows: typeof process.stdout.rows === "number" ? process.stdout.rows : 32,
|
|
22
17
|
}),
|
|
23
|
-
config: { fpsCap: 30 },
|
|
18
|
+
config: { fpsCap: 30, executionMode: "inline" },
|
|
24
19
|
});
|
|
25
20
|
|
|
26
21
|
function dispatch(action: AnimationLabAction): void {
|
|
@@ -4,10 +4,11 @@ Scaffolded with `create-rezi` using the **__TEMPLATE_LABEL__** template.
|
|
|
4
4
|
|
|
5
5
|
## What This Template Demonstrates
|
|
6
6
|
|
|
7
|
-
- Multi-screen CLI workflow powered by first-party `
|
|
7
|
+
- Multi-screen CLI workflow powered by first-party `createNodeApp({ routes })` routing.
|
|
8
8
|
- Multi-file structure for maintainability (`types`, `theme`, `helpers`, `screens`, `main`).
|
|
9
9
|
- Streamed logs + settings forms + global route keybindings.
|
|
10
10
|
- Route shell pattern you can extend for additional screens.
|
|
11
|
+
- `ui.page()` + `ui.panel()` composition with intent-based button styling.
|
|
11
12
|
|
|
12
13
|
## Screens
|
|
13
14
|
|
|
@@ -8,7 +8,7 @@ export function buildHomeContent(state: CliState): VNode {
|
|
|
8
8
|
const styles = stylesForTheme(state.themeName);
|
|
9
9
|
const latest = state.logs[state.logs.length - 1];
|
|
10
10
|
|
|
11
|
-
return ui.
|
|
11
|
+
return ui.panel({ title: "Overview", style: styles.panelStyle }, [
|
|
12
12
|
ui.column({ gap: 1 }, [
|
|
13
13
|
ui.text("Route-aware Home screen", { variant: "heading" }),
|
|
14
14
|
ui.text(`Operator: ${state.operatorName}`),
|
|
@@ -32,20 +32,30 @@ export function renderHomeScreen(
|
|
|
32
32
|
context: RouteRenderContext<CliState>,
|
|
33
33
|
deps: HomeScreenDeps,
|
|
34
34
|
): VNode {
|
|
35
|
+
const styles = stylesForTheme(context.state.themeName);
|
|
36
|
+
|
|
35
37
|
return renderShell({
|
|
36
38
|
title: "Home",
|
|
37
39
|
context,
|
|
38
40
|
onNavigate: deps.onNavigate,
|
|
39
41
|
onToggleHelp: deps.onToggleHelp,
|
|
40
|
-
body: ui.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ui.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
body: ui.panel({ title: "Home Workspace", style: styles.panelStyle }, [
|
|
43
|
+
ui.column({ gap: 1 }, [
|
|
44
|
+
buildHomeContent(context.state),
|
|
45
|
+
ui.actions([
|
|
46
|
+
ui.button({
|
|
47
|
+
id: "home-open-logs",
|
|
48
|
+
label: "Open Logs",
|
|
49
|
+
intent: "primary",
|
|
50
|
+
onPress: () => deps.onNavigate("logs"),
|
|
51
|
+
}),
|
|
52
|
+
ui.button({
|
|
53
|
+
id: "home-open-settings",
|
|
54
|
+
label: "Open Settings",
|
|
55
|
+
intent: "secondary",
|
|
56
|
+
onPress: () => deps.onNavigate("settings"),
|
|
57
|
+
}),
|
|
58
|
+
]),
|
|
49
59
|
]),
|
|
50
60
|
]),
|
|
51
61
|
});
|
|
@@ -25,49 +25,53 @@ export function renderLogsScreen(
|
|
|
25
25
|
context,
|
|
26
26
|
onNavigate: deps.onNavigate,
|
|
27
27
|
onToggleHelp: deps.onToggleHelp,
|
|
28
|
-
body: ui.
|
|
29
|
-
ui.
|
|
30
|
-
ui.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
28
|
+
body: ui.panel({ title: "Logs Workspace", style: styles.panelStyle }, [
|
|
29
|
+
ui.column({ gap: 1 }, [
|
|
30
|
+
ui.actions([
|
|
31
|
+
ui.button({
|
|
32
|
+
id: "logs-toggle-refresh",
|
|
33
|
+
label: state.autoRefresh ? "Pause stream" : "Resume stream",
|
|
34
|
+
intent: state.autoRefresh ? "warning" : "primary",
|
|
35
|
+
onPress: () => deps.dispatch({ type: "toggle-refresh" }),
|
|
36
|
+
}),
|
|
37
|
+
ui.button({
|
|
38
|
+
id: "logs-toggle-debug",
|
|
39
|
+
label: state.includeDebug ? "Hide debug" : "Show debug",
|
|
40
|
+
intent: "secondary",
|
|
41
|
+
onPress: () => deps.dispatch({ type: "toggle-debug" }),
|
|
42
|
+
}),
|
|
43
|
+
ui.button({
|
|
44
|
+
id: "logs-clear",
|
|
45
|
+
label: "Clear logs",
|
|
46
|
+
intent: "danger",
|
|
47
|
+
onPress: () => deps.dispatch({ type: "clear-logs" }),
|
|
48
|
+
}),
|
|
49
|
+
]),
|
|
50
|
+
ui.panel({ title: "Live Console", style: styles.panelStyle }, [
|
|
51
|
+
ui.logsConsole({
|
|
52
|
+
id: "logs-console",
|
|
53
|
+
entries: state.logs,
|
|
54
|
+
scrollTop: state.logsScrollTop,
|
|
55
|
+
expandedEntries: state.expandedLogIds,
|
|
56
|
+
...(state.includeDebug
|
|
57
|
+
? {}
|
|
58
|
+
: { levelFilter: Object.freeze(["info", "warn", "error"] as const) }),
|
|
59
|
+
onScroll: (scrollTop) => deps.dispatch({ type: "set-scroll-top", scrollTop }),
|
|
60
|
+
onEntryToggle: (entryId, expanded) =>
|
|
61
|
+
deps.dispatch({ type: "set-entry-expanded", entryId, expanded }),
|
|
62
|
+
onClear: () => deps.dispatch({ type: "clear-logs" }),
|
|
63
|
+
}),
|
|
64
|
+
]),
|
|
65
|
+
ui.panel({ title: "Recent entries", style: styles.panelStyle }, [
|
|
66
|
+
ui.column({ gap: 1 }, [
|
|
67
|
+
...recent.map((entry) =>
|
|
68
|
+
ui.row({ key: entry.id, gap: 1, wrap: true }, [
|
|
69
|
+
ui.badge(entry.level.toUpperCase(), { variant: levelBadgeVariant(entry.level) }),
|
|
70
|
+
ui.text(entry.message, { textOverflow: "ellipsis", maxWidth: 60 }),
|
|
71
|
+
]),
|
|
72
|
+
),
|
|
73
|
+
...(recent.length === 0 ? [ui.text("No log entries.", { style: styles.mutedStyle })] : []),
|
|
74
|
+
]),
|
|
71
75
|
]),
|
|
72
76
|
]),
|
|
73
77
|
]),
|
|
@@ -49,34 +49,40 @@ export function renderSettingsScreen(
|
|
|
49
49
|
context,
|
|
50
50
|
onNavigate: deps.onNavigate,
|
|
51
51
|
onToggleHelp: deps.onToggleHelp,
|
|
52
|
-
body: ui.
|
|
52
|
+
body: ui.panel("Profile Settings", [
|
|
53
53
|
ui.column({ gap: 1 }, [
|
|
54
54
|
ui.text("Profile", { variant: "heading" }),
|
|
55
|
-
ui.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
ui.field({
|
|
56
|
+
label: "Operator",
|
|
57
|
+
children: ui.input({
|
|
58
|
+
id: "settings-operator",
|
|
59
|
+
value: state.operatorName,
|
|
60
|
+
onInput: (operatorName) => deps.dispatch({ type: "set-operator", operatorName }),
|
|
61
|
+
}),
|
|
60
62
|
}),
|
|
61
|
-
ui.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
ui.field({
|
|
64
|
+
label: "Environment",
|
|
65
|
+
children: ui.select({
|
|
66
|
+
id: "settings-environment",
|
|
67
|
+
value: state.environment,
|
|
68
|
+
options: ENVIRONMENT_OPTIONS,
|
|
69
|
+
onChange: (value) => {
|
|
70
|
+
if (!isEnvironmentName(value)) return;
|
|
71
|
+
deps.dispatch({ type: "set-environment", environment: value });
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
70
74
|
}),
|
|
71
|
-
ui.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
ui.field({
|
|
76
|
+
label: "Theme",
|
|
77
|
+
children: ui.select({
|
|
78
|
+
id: "settings-theme",
|
|
79
|
+
value: state.themeName,
|
|
80
|
+
options: THEME_OPTIONS,
|
|
81
|
+
onChange: (value) => {
|
|
82
|
+
if (!isThemeName(value)) return;
|
|
83
|
+
deps.dispatch({ type: "set-theme", themeName: value });
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
80
86
|
}),
|
|
81
87
|
ui.checkbox({
|
|
82
88
|
id: "settings-auto-refresh",
|
|
@@ -16,39 +16,59 @@ export function renderShell(options: ShellOptions): VNode {
|
|
|
16
16
|
const styles = stylesForTheme(state.themeName);
|
|
17
17
|
const theme = themeSpec(state.themeName);
|
|
18
18
|
|
|
19
|
-
const content = ui.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
const content = ui.page({
|
|
20
|
+
p: 1,
|
|
21
|
+
gap: 1,
|
|
22
|
+
header: ui.header({
|
|
23
|
+
title: `${PRODUCT_NAME} · ${options.title}`,
|
|
24
|
+
subtitle: PRODUCT_TAGLINE,
|
|
25
|
+
actions: [
|
|
26
|
+
ui.badge(TEMPLATE_LABEL, { variant: "info" }),
|
|
27
|
+
ui.tag(`Theme ${theme.label}`, { variant: theme.badge }),
|
|
28
|
+
ui.badge(`Tick ${String(state.tick)}`, { variant: "default" }),
|
|
29
|
+
ui.status(state.autoRefresh ? "online" : "away", {
|
|
30
|
+
label: state.autoRefresh ? "Streaming" : "Paused",
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
}),
|
|
34
|
+
body: ui.column({ gap: 1 }, [
|
|
35
|
+
ui.panel({ title: "Navigation", style: styles.stripStyle }, [
|
|
36
|
+
ui.actions([
|
|
37
|
+
ui.button({
|
|
38
|
+
id: "go-home",
|
|
39
|
+
label: "Home",
|
|
40
|
+
intent: "secondary",
|
|
41
|
+
onPress: () => options.onNavigate("home"),
|
|
42
|
+
}),
|
|
43
|
+
ui.button({
|
|
44
|
+
id: "go-logs",
|
|
45
|
+
label: "Logs",
|
|
46
|
+
intent: "secondary",
|
|
47
|
+
onPress: () => options.onNavigate("logs"),
|
|
48
|
+
}),
|
|
49
|
+
ui.button({
|
|
50
|
+
id: "go-settings",
|
|
51
|
+
label: "Settings",
|
|
52
|
+
intent: "secondary",
|
|
53
|
+
onPress: () => options.onNavigate("settings"),
|
|
54
|
+
}),
|
|
55
|
+
ui.button({
|
|
56
|
+
id: "toggle-help",
|
|
57
|
+
label: "Help",
|
|
58
|
+
intent: "link",
|
|
59
|
+
onPress: options.onToggleHelp,
|
|
29
60
|
}),
|
|
30
61
|
]),
|
|
31
|
-
ui.text(PRODUCT_TAGLINE, { style: styles.mutedStyle }),
|
|
32
62
|
]),
|
|
63
|
+
options.body,
|
|
33
64
|
]),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
ui.
|
|
39
|
-
id: "go-settings",
|
|
40
|
-
label: "Settings",
|
|
41
|
-
onPress: () => options.onNavigate("settings"),
|
|
42
|
-
}),
|
|
43
|
-
ui.button({ id: "toggle-help", label: "Help", onPress: options.onToggleHelp }),
|
|
44
|
-
]),
|
|
45
|
-
|
|
46
|
-
options.body,
|
|
47
|
-
|
|
48
|
-
ui.text("Keys: F1/F2/F3 or Alt+1/2/3 · p toggle stream · h/? help · q quit", {
|
|
49
|
-
style: styles.mutedStyle,
|
|
65
|
+
footer: ui.statusBar({
|
|
66
|
+
left: [ui.text("Keys: F1/F2/F3 or Alt+1/2/3 · p toggle stream · h/? help · q quit", {
|
|
67
|
+
style: styles.mutedStyle,
|
|
68
|
+
})],
|
|
69
|
+
right: [ui.text(state.autoRefresh ? "Streaming" : "Paused", { style: styles.mutedStyle })],
|
|
50
70
|
}),
|
|
51
|
-
|
|
71
|
+
});
|
|
52
72
|
|
|
53
73
|
if (!state.showHelp) return content;
|
|
54
74
|
|
|
@@ -59,6 +79,7 @@ export function renderShell(options: ShellOptions): VNode {
|
|
|
59
79
|
title: `${PRODUCT_NAME} Shortcuts`,
|
|
60
80
|
width: 70,
|
|
61
81
|
backdrop: "none",
|
|
82
|
+
initialFocus: "cli-help-close",
|
|
62
83
|
returnFocusTo: "toggle-help",
|
|
63
84
|
content: ui.column({ gap: 1 }, [
|
|
64
85
|
ui.text("F1/F2/F3 : navigate to Home/Logs/Settings"),
|
|
@@ -8,6 +8,8 @@ Scaffolded with `create-rezi` using the **__TEMPLATE_LABEL__** template.
|
|
|
8
8
|
- Deterministic telemetry updates via `useStream(...)` + async iterable ingestion.
|
|
9
9
|
- Operator dashboard patterns: fleet summary, filtered list, service inspector, and help modal.
|
|
10
10
|
- Theme cycling and keyboard-driven controls designed for fast operational workflows.
|
|
11
|
+
- `ui.panel()` section composition with recipe-styled table defaults (`dsSize`/`dsTone`).
|
|
12
|
+
- Wheel-scroll-friendly containers (`overflow: "scroll"`) for dense service lanes.
|
|
11
13
|
|
|
12
14
|
## File Layout
|
|
13
15
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { VNode } from "@rezi-ui/core";
|
|
2
|
-
import { ui } from "@rezi-ui/core";
|
|
2
|
+
import { ui, when } from "@rezi-ui/core";
|
|
3
3
|
import {
|
|
4
4
|
filterLabel,
|
|
5
5
|
fleetCounts,
|
|
@@ -23,7 +23,7 @@ type DashboardScreenHandlers = Readonly<{
|
|
|
23
23
|
}>;
|
|
24
24
|
|
|
25
25
|
function panel(title: string, body: readonly VNode[], style: Readonly<Record<string, unknown>>): VNode {
|
|
26
|
-
return ui.
|
|
26
|
+
return ui.panel({ title, style }, body);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function renderOverviewScreen(state: DashboardState, handlers: DashboardScreenHandlers): VNode {
|
|
@@ -59,86 +59,121 @@ export function renderOverviewScreen(state: DashboardState, handlers: DashboardS
|
|
|
59
59
|
|
|
60
60
|
const uptimeSec = Math.max(1, Math.floor((Date.now() - state.startedAtMs) / 1000));
|
|
61
61
|
const updateRate = (state.tick / uptimeSec).toFixed(2);
|
|
62
|
+
const inspectorContent =
|
|
63
|
+
when(
|
|
64
|
+
Boolean(selected),
|
|
65
|
+
() => {
|
|
66
|
+
const service = selected as NonNullable<typeof selected>;
|
|
67
|
+
return ui.column({ gap: 1 }, [
|
|
68
|
+
ui.row({ gap: 1, wrap: true }, [
|
|
69
|
+
ui.badge(service.name, { variant: statusBadge(service.status).variant }),
|
|
70
|
+
ui.tag(service.owner, { variant: "default" }),
|
|
71
|
+
ui.tag(service.region, { variant: "info" }),
|
|
72
|
+
]),
|
|
73
|
+
ui.text(`Latency: ${formatLatency(service.latencyMs)}`),
|
|
74
|
+
ui.text(`Error Rate: ${formatErrorRate(service.errorRate)}`),
|
|
75
|
+
ui.text(`Traffic: ${formatTraffic(service.trafficRpm)}`),
|
|
76
|
+
ui.text(`Update rate: ${updateRate} Hz`, { style: styles.mutedStyle }),
|
|
77
|
+
ui.sparkline(service.history, { width: 18, min: 0, max: 220 }),
|
|
78
|
+
]);
|
|
79
|
+
},
|
|
80
|
+
() => ui.text("No service selected.", { style: styles.mutedStyle }),
|
|
81
|
+
) ?? ui.text("No service selected.", { style: styles.mutedStyle });
|
|
62
82
|
|
|
63
|
-
const content = ui.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
ui.row({ gap: 1, wrap: true }, [
|
|
81
|
-
ui.button({
|
|
82
|
-
id: "filter",
|
|
83
|
-
label: `Filter: ${filterLabel(state.filter)}`,
|
|
84
|
-
onPress: handlers.onCycleFilter,
|
|
85
|
-
}),
|
|
86
|
-
ui.button({
|
|
87
|
-
id: "theme",
|
|
88
|
-
label: "Cycle Theme",
|
|
89
|
-
onPress: handlers.onCycleTheme,
|
|
90
|
-
}),
|
|
91
|
-
ui.button({
|
|
92
|
-
id: "pause",
|
|
93
|
-
label: state.paused ? "Resume Stream" : "Pause Stream",
|
|
94
|
-
onPress: handlers.onTogglePause,
|
|
95
|
-
}),
|
|
96
|
-
ui.button({
|
|
97
|
-
id: "help",
|
|
98
|
-
label: "Help",
|
|
99
|
-
onPress: handlers.onToggleHelp,
|
|
100
|
-
}),
|
|
101
|
-
]),
|
|
102
|
-
|
|
103
|
-
ui.row({ gap: 1, wrap: true, items: "stretch" }, [
|
|
83
|
+
const content = ui.page({
|
|
84
|
+
p: 1,
|
|
85
|
+
gap: 1,
|
|
86
|
+
header: ui.header({
|
|
87
|
+
title: PRODUCT_NAME,
|
|
88
|
+
subtitle: PRODUCT_TAGLINE,
|
|
89
|
+
actions: [
|
|
90
|
+
ui.badge(TEMPLATE_LABEL, { variant: "info" }),
|
|
91
|
+
ui.badge(`Fleet ${health.label}`, { variant: health.variant }),
|
|
92
|
+
ui.status(state.paused ? "away" : "online", {
|
|
93
|
+
label: state.paused ? "Paused" : "Streaming",
|
|
94
|
+
}),
|
|
95
|
+
ui.tag(`Theme ${theme.label}`, { variant: theme.badge }),
|
|
96
|
+
],
|
|
97
|
+
}),
|
|
98
|
+
body: ui.column({ gap: 1 }, [
|
|
104
99
|
panel(
|
|
105
|
-
"
|
|
100
|
+
"Actions",
|
|
106
101
|
[
|
|
107
|
-
ui.
|
|
108
|
-
ui.
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
ui.actions([
|
|
103
|
+
ui.button({
|
|
104
|
+
id: "filter",
|
|
105
|
+
label: `Filter: ${filterLabel(state.filter)}`,
|
|
106
|
+
intent: "secondary",
|
|
107
|
+
onPress: handlers.onCycleFilter,
|
|
108
|
+
}),
|
|
109
|
+
ui.button({
|
|
110
|
+
id: "theme",
|
|
111
|
+
label: "Cycle Theme",
|
|
112
|
+
intent: "secondary",
|
|
113
|
+
onPress: handlers.onCycleTheme,
|
|
114
|
+
}),
|
|
115
|
+
ui.button({
|
|
116
|
+
id: "pause",
|
|
117
|
+
label: state.paused ? "Resume Stream" : "Pause Stream",
|
|
118
|
+
intent: state.paused ? "primary" : "warning",
|
|
119
|
+
onPress: handlers.onTogglePause,
|
|
120
|
+
}),
|
|
121
|
+
ui.button({
|
|
122
|
+
id: "help",
|
|
123
|
+
label: "Help",
|
|
124
|
+
intent: "link",
|
|
125
|
+
onPress: handlers.onToggleHelp,
|
|
126
|
+
}),
|
|
111
127
|
]),
|
|
112
|
-
...serviceRows,
|
|
113
128
|
],
|
|
114
129
|
styles.panelStyle,
|
|
115
130
|
),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
ui.row({ gap: 1, wrap: true, items: "stretch" }, [
|
|
132
|
+
panel(
|
|
133
|
+
"Service Fleet",
|
|
134
|
+
[
|
|
135
|
+
ui.row({ gap: 1, wrap: true }, [
|
|
136
|
+
ui.badge(`Healthy ${String(counts.healthy)}`, { variant: "success" }),
|
|
137
|
+
ui.badge(`Warning ${String(counts.warning)}`, { variant: "warning" }),
|
|
138
|
+
ui.badge(`Down ${String(counts.down)}`, { variant: "error" }),
|
|
139
|
+
]),
|
|
140
|
+
ui.box({ height: 10, overflow: "scroll", border: "none" }, [...serviceRows]),
|
|
141
|
+
ui.table({
|
|
142
|
+
id: "fleet-table",
|
|
143
|
+
columns: [
|
|
144
|
+
{ key: "name", header: "Service", flex: 1 },
|
|
145
|
+
{ key: "status", header: "Status", width: 8 },
|
|
146
|
+
{ key: "latencyMs", header: "Latency", width: 9, align: "right" },
|
|
147
|
+
],
|
|
148
|
+
data: visible,
|
|
149
|
+
getRowKey: (service) => service.id,
|
|
150
|
+
selection: selected ? [selected.id] : [],
|
|
151
|
+
selectionMode: "single",
|
|
152
|
+
onSelectionChange: (keys) => {
|
|
153
|
+
const key = keys[0];
|
|
154
|
+
if (typeof key === "string") handlers.onSelectService(key);
|
|
155
|
+
},
|
|
156
|
+
onRowPress: (row) => handlers.onSelectService(row.id),
|
|
157
|
+
dsSize: "sm",
|
|
158
|
+
dsTone: "default",
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
styles.panelStyle,
|
|
162
|
+
),
|
|
163
|
+
panel(
|
|
164
|
+
"Inspector",
|
|
165
|
+
[inspectorContent],
|
|
166
|
+
styles.panelStyle,
|
|
167
|
+
),
|
|
168
|
+
]),
|
|
136
169
|
]),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
footer: ui.statusBar({
|
|
171
|
+
left: [ui.text("Keys: q quit · j/k or arrows move · f filter · t theme · p pause · h/? help", {
|
|
172
|
+
style: styles.mutedStyle,
|
|
173
|
+
})],
|
|
174
|
+
right: [ui.text(`Tick ${String(state.tick)}`, { style: styles.mutedStyle })],
|
|
140
175
|
}),
|
|
141
|
-
|
|
176
|
+
});
|
|
142
177
|
|
|
143
178
|
if (!state.showHelp) return content;
|
|
144
179
|
|
|
@@ -5,6 +5,7 @@ Scaffolded with `create-rezi` using the **__TEMPLATE_LABEL__** template.
|
|
|
5
5
|
## What This Template Demonstrates
|
|
6
6
|
|
|
7
7
|
- A single-screen TUI with no routing overhead.
|
|
8
|
+
- `ui.page()` layout shell with panelized sections and intent-based button actions.
|
|
8
9
|
- Minimal state + reducer flow with just a few actions.
|
|
9
10
|
- Keybindings for quit/help/theme/counter updates.
|
|
10
11
|
- Signal-safe startup and shutdown pattern.
|
|
@@ -14,42 +14,56 @@ type ScreenHandlers = Readonly<{
|
|
|
14
14
|
export function renderMainScreen(state: MinimalState, handlers: ScreenHandlers): VNode {
|
|
15
15
|
const theme = themeSpec(state.themeName);
|
|
16
16
|
|
|
17
|
-
const content = ui.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ui.
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
const content = ui.page({
|
|
18
|
+
p: 1,
|
|
19
|
+
gap: 1,
|
|
20
|
+
header: ui.header({
|
|
21
|
+
title: PRODUCT_NAME,
|
|
22
|
+
subtitle: PRODUCT_TAGLINE,
|
|
23
|
+
actions: [
|
|
24
|
+
ui.badge(TEMPLATE_LABEL, { variant: "info" }),
|
|
25
|
+
ui.tag(`Theme ${theme.label}`, { variant: theme.badge }),
|
|
26
|
+
],
|
|
27
|
+
}),
|
|
28
|
+
body: ui.column({ gap: 1 }, [
|
|
29
|
+
ui.panel("Counter", [
|
|
30
|
+
ui.column({ gap: 1 }, [
|
|
31
|
+
ui.text(`Count: ${String(state.count)}`, { variant: "heading" }),
|
|
32
|
+
ui.toolbar({ gap: 1 }, [
|
|
33
|
+
ui.button({ id: "dec", label: "-1", onPress: handlers.onDecrement }),
|
|
34
|
+
ui.button({
|
|
35
|
+
id: "inc",
|
|
36
|
+
label: "+1",
|
|
37
|
+
intent: "primary",
|
|
38
|
+
onPress: handlers.onIncrement,
|
|
39
|
+
}),
|
|
40
|
+
ui.button({ id: "theme", label: "Cycle Theme", onPress: handlers.onCycleTheme }),
|
|
41
|
+
ui.button({ id: "help", label: "Help", intent: "link", onPress: handlers.onToggleHelp }),
|
|
42
|
+
]),
|
|
37
43
|
]),
|
|
38
44
|
]),
|
|
45
|
+
state.lastError
|
|
46
|
+
? ui.panel("Alerts", [
|
|
47
|
+
ui.callout(state.lastError, {
|
|
48
|
+
title: "Example Error Pattern",
|
|
49
|
+
variant: "error",
|
|
50
|
+
}),
|
|
51
|
+
ui.actions([
|
|
52
|
+
ui.button({
|
|
53
|
+
id: "clear-error",
|
|
54
|
+
label: "Clear",
|
|
55
|
+
intent: "danger",
|
|
56
|
+
onPress: handlers.onClearError,
|
|
57
|
+
}),
|
|
58
|
+
]),
|
|
59
|
+
])
|
|
60
|
+
: ui.panel("Status", [ui.text("No runtime error. Press e to simulate one.")]),
|
|
39
61
|
]),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
variant: "error",
|
|
46
|
-
}),
|
|
47
|
-
ui.button({ id: "clear-error", label: "Clear", onPress: handlers.onClearError }),
|
|
48
|
-
])
|
|
49
|
-
: ui.text("No runtime error. Press e to simulate one."),
|
|
50
|
-
|
|
51
|
-
ui.text("Keys: q quit · ? help · +/- counter · t theme · e error"),
|
|
52
|
-
]);
|
|
62
|
+
footer: ui.statusBar({
|
|
63
|
+
left: [ui.status("online"), ui.text("Ready")],
|
|
64
|
+
right: [ui.text("Keys: q quit · ? help · +/- counter · t theme · e error")],
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
53
67
|
|
|
54
68
|
if (!state.showHelp) return content;
|
|
55
69
|
|
|
@@ -3,7 +3,6 @@ import { devNull, tmpdir } from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { BadgeVariant, RichTextSpan, TextStyle, ThemeDefinition, VNode } from "@rezi-ui/core";
|
|
5
5
|
import {
|
|
6
|
-
createApp,
|
|
7
6
|
darkTheme,
|
|
8
7
|
dimmedTheme,
|
|
9
8
|
draculaTheme,
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
rgb,
|
|
14
13
|
ui,
|
|
15
14
|
} from "@rezi-ui/core";
|
|
16
|
-
import {
|
|
15
|
+
import { type NodeBackend, createNodeApp } from "@rezi-ui/node";
|
|
17
16
|
|
|
18
17
|
// ---------------------------------------------------------------------------
|
|
19
18
|
// Types
|
|
@@ -693,7 +692,7 @@ let _lastViewMs = 0;
|
|
|
693
692
|
|
|
694
693
|
let _backendPerfInFlight = false;
|
|
695
694
|
let _lastBackendPerfSampleMs = 0;
|
|
696
|
-
let _backend:
|
|
695
|
+
let _backend: NodeBackend | null = null;
|
|
697
696
|
|
|
698
697
|
let _memoryBallast: Buffer[] = [];
|
|
699
698
|
let _memoryBallastBytes = 0;
|
|
@@ -976,16 +975,10 @@ function simulateTick(state: State, nowMs: number): State {
|
|
|
976
975
|
// ---------------------------------------------------------------------------
|
|
977
976
|
|
|
978
977
|
const initialNowMs = Date.now();
|
|
979
|
-
const
|
|
980
|
-
fpsCap: UI_FPS_CAP,
|
|
981
|
-
executionMode: "worker",
|
|
982
|
-
});
|
|
983
|
-
_backend = backend;
|
|
984
|
-
|
|
985
|
-
const app = createApp<State>({
|
|
986
|
-
backend,
|
|
978
|
+
const app = createNodeApp<State>({
|
|
987
979
|
config: {
|
|
988
980
|
fpsCap: UI_FPS_CAP,
|
|
981
|
+
executionMode: "worker",
|
|
989
982
|
internal_onRender: (metrics) => {
|
|
990
983
|
_lastRenderMs = round2(metrics.renderTime);
|
|
991
984
|
},
|
|
@@ -1037,6 +1030,8 @@ const app = createApp<State>({
|
|
|
1037
1030
|
},
|
|
1038
1031
|
});
|
|
1039
1032
|
|
|
1033
|
+
_backend = app.backend;
|
|
1034
|
+
|
|
1040
1035
|
// ---------------------------------------------------------------------------
|
|
1041
1036
|
// Actions
|
|
1042
1037
|
// ---------------------------------------------------------------------------
|