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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rezi",
3
- "version": "0.1.0-alpha.30",
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.30"
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 { createApp } from "@rezi-ui/core";
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 = createApp({
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 `createApp({ routes })` routing.
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.box({ border: "rounded", px: 1, py: 0, style: styles.panelStyle }, [
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.column({ gap: 1 }, [
41
- buildHomeContent(context.state),
42
- ui.row({ gap: 1, wrap: true }, [
43
- ui.button({ id: "home-open-logs", label: "Open Logs", onPress: () => deps.onNavigate("logs") }),
44
- ui.button({
45
- id: "home-open-settings",
46
- label: "Open Settings",
47
- onPress: () => deps.onNavigate("settings"),
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.column({ gap: 1 }, [
29
- ui.row({ gap: 1, wrap: true }, [
30
- ui.button({
31
- id: "logs-toggle-refresh",
32
- label: state.autoRefresh ? "Pause stream" : "Resume stream",
33
- onPress: () => deps.dispatch({ type: "toggle-refresh" }),
34
- }),
35
- ui.button({
36
- id: "logs-toggle-debug",
37
- label: state.includeDebug ? "Hide debug" : "Show debug",
38
- onPress: () => deps.dispatch({ type: "toggle-debug" }),
39
- }),
40
- ui.button({
41
- id: "logs-clear",
42
- label: "Clear logs",
43
- onPress: () => deps.dispatch({ type: "clear-logs" }),
44
- }),
45
- ]),
46
- ui.box({ border: "rounded", px: 1, py: 0, style: styles.panelStyle }, [
47
- ui.logsConsole({
48
- id: "logs-console",
49
- entries: state.logs,
50
- scrollTop: state.logsScrollTop,
51
- expandedEntries: state.expandedLogIds,
52
- ...(state.includeDebug
53
- ? {}
54
- : { levelFilter: Object.freeze(["info", "warn", "error"] as const) }),
55
- onScroll: (scrollTop) => deps.dispatch({ type: "set-scroll-top", scrollTop }),
56
- onEntryToggle: (entryId, expanded) =>
57
- deps.dispatch({ type: "set-entry-expanded", entryId, expanded }),
58
- onClear: () => deps.dispatch({ type: "clear-logs" }),
59
- }),
60
- ]),
61
- ui.box({ border: "rounded", px: 1, py: 0, style: styles.panelStyle }, [
62
- ui.column({ gap: 1 }, [
63
- ui.text("Recent entries", { variant: "heading" }),
64
- ...recent.map((entry) =>
65
- ui.row({ key: entry.id, gap: 1, wrap: true }, [
66
- ui.badge(entry.level.toUpperCase(), { variant: levelBadgeVariant(entry.level) }),
67
- ui.text(entry.message, { textOverflow: "ellipsis", maxWidth: 60 }),
68
- ]),
69
- ),
70
- ...(recent.length === 0 ? [ui.text("No log entries.", { style: styles.mutedStyle })] : []),
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.box({ border: "rounded", px: 1, py: 0 }, [
52
+ body: ui.panel("Profile Settings", [
53
53
  ui.column({ gap: 1 }, [
54
54
  ui.text("Profile", { variant: "heading" }),
55
- ui.text("Operator"),
56
- ui.input({
57
- id: "settings-operator",
58
- value: state.operatorName,
59
- onInput: (operatorName) => deps.dispatch({ type: "set-operator", operatorName }),
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.text("Environment"),
62
- ui.select({
63
- id: "settings-environment",
64
- value: state.environment,
65
- options: ENVIRONMENT_OPTIONS,
66
- onChange: (value) => {
67
- if (!isEnvironmentName(value)) return;
68
- deps.dispatch({ type: "set-environment", environment: value });
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.text("Theme"),
72
- ui.select({
73
- id: "settings-theme",
74
- value: state.themeName,
75
- options: THEME_OPTIONS,
76
- onChange: (value) => {
77
- if (!isThemeName(value)) return;
78
- deps.dispatch({ type: "set-theme", themeName: value });
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.column({ p: 1, gap: 1, items: "stretch", style: styles.rootStyle }, [
20
- ui.box({ border: "rounded", px: 1, py: 0, style: styles.stripStyle }, [
21
- ui.column({ gap: 1 }, [
22
- ui.row({ gap: 1, wrap: true, items: "center" }, [
23
- ui.text(`${PRODUCT_NAME} · ${options.title}`, { variant: "heading" }),
24
- ui.badge(TEMPLATE_LABEL, { variant: "info" }),
25
- ui.tag(`Theme ${theme.label}`, { variant: theme.badge }),
26
- ui.badge(`Tick ${String(state.tick)}`, { variant: "default" }),
27
- ui.status(state.autoRefresh ? "online" : "away", {
28
- label: state.autoRefresh ? "Streaming" : "Paused",
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
- ui.row({ gap: 1, wrap: true }, [
36
- ui.button({ id: "go-home", label: "Home", onPress: () => options.onNavigate("home") }),
37
- ui.button({ id: "go-logs", label: "Logs", onPress: () => options.onNavigate("logs") }),
38
- ui.button({
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.box({ title, border: "rounded", px: 1, py: 0, style }, body);
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.column({ p: 1, gap: 1, items: "stretch", style: styles.rootStyle }, [
64
- ui.box({ border: "rounded", px: 1, py: 0, style: styles.stripStyle }, [
65
- ui.column({ gap: 1 }, [
66
- ui.row({ gap: 1, items: "center", wrap: true }, [
67
- ui.text(PRODUCT_NAME, { variant: "heading" }),
68
- ui.badge(TEMPLATE_LABEL, { variant: "info" }),
69
- ui.badge(`Fleet ${health.label}`, { variant: health.variant }),
70
- ui.status(state.paused ? "away" : "online", {
71
- label: state.paused ? "Paused" : "Streaming",
72
- }),
73
- ui.spacer({ flex: 1 }),
74
- ui.tag(`Theme ${theme.label}`, { variant: theme.badge }),
75
- ]),
76
- ui.text(PRODUCT_TAGLINE, { style: styles.mutedStyle }),
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
- "Service Fleet",
100
+ "Actions",
106
101
  [
107
- ui.row({ gap: 1, wrap: true }, [
108
- ui.badge(`Healthy ${String(counts.healthy)}`, { variant: "success" }),
109
- ui.badge(`Warning ${String(counts.warning)}`, { variant: "warning" }),
110
- ui.badge(`Down ${String(counts.down)}`, { variant: "error" }),
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
- panel(
117
- "Inspector",
118
- selected
119
- ? [
120
- ui.column({ gap: 1 }, [
121
- ui.row({ gap: 1, wrap: true }, [
122
- ui.badge(selected.name, { variant: statusBadge(selected.status).variant }),
123
- ui.tag(selected.owner, { variant: "default" }),
124
- ui.tag(selected.region, { variant: "info" }),
125
- ]),
126
- ui.text(`Latency: ${formatLatency(selected.latencyMs)}`),
127
- ui.text(`Error Rate: ${formatErrorRate(selected.errorRate)}`),
128
- ui.text(`Traffic: ${formatTraffic(selected.trafficRpm)}`),
129
- ui.text(`Update rate: ${updateRate} Hz`, { style: styles.mutedStyle }),
130
- ui.sparkline(selected.history, { width: 18, min: 0, max: 220 }),
131
- ]),
132
- ]
133
- : [ui.text("No service selected.", { style: styles.mutedStyle })],
134
- styles.panelStyle,
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
- ui.text("Keys: q quit · j/k or arrows move · f filter · t theme · p pause · h/? help", {
139
- style: styles.mutedStyle,
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.column({ p: 1, gap: 1 }, [
18
- ui.box({ border: "rounded", px: 1, py: 0 }, [
19
- ui.column({ gap: 1 }, [
20
- ui.row({ gap: 1, wrap: true }, [
21
- ui.text(PRODUCT_NAME, { variant: "heading" }),
22
- ui.badge(TEMPLATE_LABEL, { variant: "info" }),
23
- ui.tag(`Theme ${theme.label}`, { variant: theme.badge }),
24
- ]),
25
- ui.text(PRODUCT_TAGLINE),
26
- ]),
27
- ]),
28
-
29
- ui.box({ title: "Counter", border: "rounded", px: 1, py: 0 }, [
30
- ui.column({ gap: 1 }, [
31
- ui.text(`Count: ${String(state.count)}`, { variant: "heading" }),
32
- ui.row({ gap: 1, wrap: true }, [
33
- ui.button({ id: "dec", label: "-1", onPress: handlers.onDecrement }),
34
- ui.button({ id: "inc", label: "+1", onPress: handlers.onIncrement }),
35
- ui.button({ id: "theme", label: "Cycle Theme", onPress: handlers.onCycleTheme }),
36
- ui.button({ id: "help", label: "Help", onPress: handlers.onToggleHelp }),
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
- state.lastError
42
- ? ui.column({ gap: 1 }, [
43
- ui.callout(state.lastError, {
44
- title: "Example Error Pattern",
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 { createNodeBackend } from "@rezi-ui/node";
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: ReturnType<typeof createNodeBackend> | null = null;
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 backend = createNodeBackend({
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
  // ---------------------------------------------------------------------------