create-rezi 0.1.0-alpha.38 → 0.1.0-alpha.39

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 CHANGED
@@ -37,8 +37,8 @@ npm create rezi my-app -- --template dashboard
37
37
  npm create rezi my-app -- --template stress-test
38
38
  npm create rezi my-app -- --template cli-tool
39
39
  npm create rezi my-app -- --template animation-lab
40
- npm create rezi my-app -- --template minimal
41
40
  npm create rezi my-app -- --template starship
41
+ npm create rezi my-app -- --template minimal
42
42
  ```
43
43
 
44
44
  List templates and highlights from the CLI:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rezi",
3
- "version": "0.1.0-alpha.38",
3
+ "version": "0.1.0-alpha.39",
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.38"
31
+ "@rezi-ui/testkit": "0.1.0-alpha.39"
32
32
  }
33
33
  }
@@ -66,7 +66,8 @@ test("bridge screen renders core markers", () => {
66
66
 
67
67
  assert.match(output, /USS Rezi/);
68
68
  assert.match(output, /Bridge Overview/);
69
- assert.match(output, /Command Deck/);
69
+ assert.match(output, /Navigation/);
70
+ assert.match(output, /Route Health/);
70
71
  });
71
72
 
72
73
  test("engineering screen renders core markers", () => {
@@ -77,7 +78,20 @@ test("engineering screen renders core markers", () => {
77
78
  .toText();
78
79
 
79
80
  assert.match(output, /Engineering Deck/);
80
- assert.match(output, /Power and Thermal Control/);
81
+ assert.match(output, /Fleet Active/);
82
+ assert.match(output, /Route Health/);
83
+ });
84
+
85
+ test("engineering screen hides secondary panels at medium height", () => {
86
+ const state = createInitialState(0);
87
+ const renderer = createTestRenderer({ viewport: { cols: 160, rows: 46 } });
88
+ const output = renderer
89
+ .render(renderEngineeringScreen(createContext(state, "engineering"), createDeps()))
90
+ .toText();
91
+
92
+ assert.match(output, /Engineering Deck/);
93
+ assert.doesNotMatch(output, /Subsystem Tree/);
94
+ assert.doesNotMatch(output, /Subsystem Diagnostics/);
81
95
  });
82
96
 
83
97
  test("crew screen renders loading/ops markers", () => {
@@ -88,7 +102,8 @@ test("crew screen renders loading/ops markers", () => {
88
102
  .toText();
89
103
 
90
104
  assert.match(output, /Crew Manifest/);
91
- assert.match(output, /Crew Command/);
105
+ assert.match(output, /Fleet Active/);
106
+ assert.match(output, /Navigation/);
92
107
  });
93
108
 
94
109
  test("comms screen renders control markers", () => {
@@ -99,8 +114,8 @@ test("comms screen renders control markers", () => {
99
114
  .toText();
100
115
 
101
116
  assert.match(output, /Communications/);
102
- assert.match(output, /Channel Controls/);
103
- assert.match(output, /Open Hail/);
117
+ assert.match(output, /Fleet Active/);
118
+ assert.match(output, /Palette/);
104
119
  });
105
120
 
106
121
  test("settings screen renders form fields", () => {
@@ -111,8 +126,8 @@ test("settings screen renders form fields", () => {
111
126
  .toText();
112
127
 
113
128
  assert.match(output, /Ship Settings/);
114
- assert.match(output, /Ship Name/);
115
- assert.match(output, /Alert Threshold/);
129
+ assert.match(output, /Theme Night Shift/);
130
+ assert.match(output, /Navigation/);
116
131
  });
117
132
 
118
133
  test("cargo screen renders manifest widgets", () => {
@@ -123,5 +138,6 @@ test("cargo screen renders manifest widgets", () => {
123
138
  .toText();
124
139
 
125
140
  assert.match(output, /Cargo Hold/);
126
- assert.match(output, /Manifest/);
141
+ assert.match(output, /Fleet Active/);
142
+ assert.match(output, /Route Health/);
127
143
  });
@@ -0,0 +1,22 @@
1
+ import { appendFileSync } from "node:fs";
2
+
3
+ const DEBUG_ENABLED = process.env.REZI_STARSHIP_DEBUG === "1";
4
+ const DEBUG_LOG_PATH = process.env.REZI_STARSHIP_DEBUG_LOG ?? "/tmp/rezi-starship-layout.log";
5
+ const lastSnapshotByScope = new Map<string, string>();
6
+
7
+ export function debugSnapshot(scope: string, payload: Readonly<Record<string, unknown>>): void {
8
+ if (!DEBUG_ENABLED) return;
9
+
10
+ const serialized = JSON.stringify(payload);
11
+ if (lastSnapshotByScope.get(scope) === serialized) return;
12
+ lastSnapshotByScope.set(scope, serialized);
13
+
14
+ appendFileSync(
15
+ DEBUG_LOG_PATH,
16
+ `${JSON.stringify({
17
+ ts: new Date().toISOString(),
18
+ scope,
19
+ ...payload,
20
+ })}\n`,
21
+ );
22
+ }
@@ -0,0 +1,66 @@
1
+ import type { RouteId } from "../types.js";
2
+
3
+ export type ResponsiveLayout = Readonly<{
4
+ width: number;
5
+ height: number;
6
+ wide: boolean;
7
+ stackRightRail: boolean;
8
+ compactSidebar: boolean;
9
+ hideNonCritical: boolean;
10
+ sidebarWidth: number;
11
+ crewMasterWidth: number;
12
+ chartWidth: number;
13
+ canvasWidth: number;
14
+ }>;
15
+
16
+ export type ViewportSnapshot = Readonly<{
17
+ width: number;
18
+ height: number;
19
+ }>;
20
+
21
+ function clamp(value: number, min: number, max: number): number {
22
+ if (value < min) return min;
23
+ if (value > max) return max;
24
+ return value;
25
+ }
26
+
27
+ export function resolveLayout(viewport: ViewportSnapshot): ResponsiveLayout {
28
+ const width = Math.max(40, Math.floor(viewport.width));
29
+ const height = Math.max(18, Math.floor(viewport.height));
30
+ const wide = width >= 120;
31
+ const stackRightRail = width < 120;
32
+ const compactSidebar = width < 90;
33
+ const hideNonCritical = width < 80 || height < 26;
34
+ const sidebarWidth = compactSidebar ? 18 : 34;
35
+
36
+ return Object.freeze({
37
+ width,
38
+ height,
39
+ wide,
40
+ stackRightRail,
41
+ compactSidebar,
42
+ hideNonCritical,
43
+ sidebarWidth,
44
+ crewMasterWidth: wide ? 60 : 100,
45
+ chartWidth: clamp(Math.floor(width * (wide ? 0.5 : 0.9)), 28, 132),
46
+ canvasWidth: clamp(Math.floor(width * (wide ? 0.48 : 0.9)), 26, 116),
47
+ });
48
+ }
49
+
50
+ const COMPACT_ROUTE_LABEL: Readonly<Record<RouteId, string>> = Object.freeze({
51
+ bridge: "Br",
52
+ engineering: "Eng",
53
+ crew: "Crew",
54
+ comms: "Com",
55
+ cargo: "Cargo",
56
+ settings: "Set",
57
+ });
58
+
59
+ export function routeLabel(routeId: RouteId, title: string, compact: boolean): string {
60
+ if (!compact) return title;
61
+ return COMPACT_ROUTE_LABEL[routeId] ?? title;
62
+ }
63
+
64
+ export function padLabel(label: string, width: number): string {
65
+ return label.length >= width ? label.slice(0, width) : label.padEnd(width, " ");
66
+ }
@@ -179,6 +179,23 @@ function defaultTelemetry(): TelemetrySnapshot {
179
179
  }
180
180
 
181
181
  export function createInitialState(nowMs = Date.now()): StarshipState {
182
+ const viewportCols = 120;
183
+ const viewportRows = 40;
184
+ return createInitialStateWithViewport(nowMs, {
185
+ cols: viewportCols,
186
+ rows: viewportRows,
187
+ });
188
+ }
189
+
190
+ type InitialViewport = Readonly<{
191
+ cols: number;
192
+ rows: number;
193
+ }>;
194
+
195
+ export function createInitialStateWithViewport(
196
+ nowMs: number,
197
+ viewport: InitialViewport,
198
+ ): StarshipState {
182
199
  const telemetry = defaultTelemetry();
183
200
  const telemetryHistory = Object.freeze(
184
201
  Array.from({ length: TELEMETRY_HISTORY_LIMIT }, (_, index) =>
@@ -199,13 +216,15 @@ export function createInitialState(nowMs = Date.now()): StarshipState {
199
216
  tick: 0,
200
217
  nowMs,
201
218
  alertLevel: "green",
202
- themeName: "day",
219
+ themeName: "night",
203
220
  showHelp: false,
204
221
  showCommandPalette: false,
205
222
  commandQuery: "",
206
223
  commandIndex: 0,
207
224
  autopilot: true,
208
225
  paused: false,
226
+ viewportCols: clampInt(viewport.cols, 40, 300),
227
+ viewportRows: clampInt(viewport.rows, 18, 200),
209
228
 
210
229
  telemetry,
211
230
  telemetryHistory,
@@ -328,6 +347,14 @@ function applyCommand(state: StarshipState, commandId: string): StarshipState {
328
347
  }
329
348
 
330
349
  export function reduceStarshipState(state: StarshipState, action: StarshipAction): StarshipState {
350
+ if (action.type === "set-viewport") {
351
+ return freezeState({
352
+ ...state,
353
+ viewportCols: clampInt(action.cols, 40, 300),
354
+ viewportRows: clampInt(action.rows, 18, 200),
355
+ });
356
+ }
357
+
331
358
  if (action.type === "tick") {
332
359
  const tick = state.tick + 1;
333
360
  const telemetry = state.paused ? state.telemetry : evolveTelemetry(state, tick);
@@ -1,7 +1,12 @@
1
1
  import { exit } from "node:process";
2
2
  import { createNodeApp } from "@rezi-ui/node";
3
+ import { debugSnapshot } from "./helpers/debug.js";
3
4
  import { resolveStarshipCommand } from "./helpers/keybindings.js";
4
- import { createInitialState, filteredMessages, reduceStarshipState } from "./helpers/state.js";
5
+ import {
6
+ createInitialStateWithViewport,
7
+ filteredMessages,
8
+ reduceStarshipState,
9
+ } from "./helpers/state.js";
5
10
  import { STARSHIP_ROUTES, createStarshipRoutes } from "./screens/index.js";
6
11
  import { themeSpec } from "./theme.js";
7
12
  import type { RouteDeps, RouteId, StarshipAction, StarshipState } from "./types.js";
@@ -10,18 +15,36 @@ const UI_FPS_CAP = 30;
10
15
  const TICK_MS = 800;
11
16
  const TOAST_PRUNE_MS = 3000;
12
17
 
13
- const initialState = createInitialState();
18
+ const initialState = createInitialStateWithViewport(Date.now(), {
19
+ cols: process.stdout.columns ?? 120,
20
+ rows: process.stdout.rows ?? 40,
21
+ });
14
22
  const enableHsr = process.argv.includes("--hsr") || process.env.REZI_HSR === "1";
15
23
  const hasInteractiveTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
16
24
  if (!hasInteractiveTty && process.env.ZIREAEL_POSIX_PIPE_MODE === undefined) {
17
25
  process.env.ZIREAEL_POSIX_PIPE_MODE = "1";
18
26
  }
27
+ debugSnapshot("runtime.bootstrap", {
28
+ argv: process.argv.slice(2),
29
+ cwd: process.cwd(),
30
+ pid: process.pid,
31
+ node: process.version,
32
+ stdinIsTty: Boolean(process.stdin.isTTY),
33
+ stdoutIsTty: Boolean(process.stdout.isTTY),
34
+ cols: process.stdout.columns ?? null,
35
+ rows: process.stdout.rows ?? null,
36
+ enableHsr,
37
+ });
19
38
 
20
39
  // biome-ignore lint/style/useConst: circular bootstrap wiring requires post-declaration assignment
21
40
  let app!: ReturnType<typeof createNodeApp<StarshipState>>;
22
41
  let stopping = false;
23
42
  let tickTimer: ReturnType<typeof setInterval> | null = null;
24
43
  let toastTimer: ReturnType<typeof setInterval> | null = null;
44
+ let lastViewport = {
45
+ cols: initialState.viewportCols,
46
+ rows: initialState.viewportRows,
47
+ };
25
48
 
26
49
  type CreateRoutesFn = typeof createStarshipRoutes;
27
50
  type RoutesModule = Readonly<{ createStarshipRoutes?: CreateRoutesFn }>;
@@ -44,6 +67,24 @@ function dispatch(action: StarshipAction): void {
44
67
  }
45
68
  }
46
69
 
70
+ function syncViewport(cols: number, rows: number): void {
71
+ if (cols === lastViewport.cols && rows === lastViewport.rows) return;
72
+ lastViewport = { cols, rows };
73
+ debugSnapshot("runtime.viewport", {
74
+ cols,
75
+ rows,
76
+ route: currentRouteId(),
77
+ });
78
+ dispatch({ type: "set-viewport", cols, rows });
79
+ }
80
+
81
+ function syncViewportFromStdout(): void {
82
+ if (!process.stdout.isTTY) return;
83
+ const cols = process.stdout.columns ?? lastViewport.cols;
84
+ const rows = process.stdout.rows ?? lastViewport.rows;
85
+ syncViewport(cols, rows);
86
+ }
87
+
47
88
  function currentRouteId(): RouteId {
48
89
  const routeId = app.router?.currentRoute().id;
49
90
  if (routeId === "bridge") return "bridge";
@@ -59,7 +100,8 @@ function navigate(routeId: RouteId): void {
59
100
  const router = app.router;
60
101
  if (!router) return;
61
102
  if (router.currentRoute().id === routeId) return;
62
- router.navigate(routeId);
103
+ // Top-level deck switches are peer navigation; replace avoids unbounded breadcrumb growth.
104
+ router.replace(routeId);
63
105
  }
64
106
 
65
107
  function navigateDeckOffset(offset: 1 | -1): void {
@@ -111,6 +153,12 @@ async function stopApp(code = 0): Promise<void> {
111
153
 
112
154
  function applyCommand(command: ReturnType<typeof resolveStarshipCommand>): void {
113
155
  if (!command) return;
156
+ debugSnapshot("runtime.command", {
157
+ command,
158
+ route: currentRouteId(),
159
+ viewportCols: lastViewport.cols,
160
+ viewportRows: lastViewport.rows,
161
+ });
114
162
 
115
163
  if (command === "quit") {
116
164
  void stopApp(0);
@@ -371,21 +419,45 @@ function bindKeys(): void {
371
419
  ) as Record<string, () => void>;
372
420
 
373
421
  bindingMap.escape = () => {
374
- const state = app.getState();
375
- if (state.showHelp) {
376
- dispatch({ type: "toggle-help" });
377
- return;
378
- }
379
- if (state.showCommandPalette) {
380
- dispatch({ type: "toggle-command-palette" });
381
- return;
382
- }
383
- if (state.showHailDialog) {
384
- dispatch({ type: "toggle-hail-dialog" });
385
- return;
386
- }
387
- if (state.showResetDialog) {
388
- dispatch({ type: "toggle-reset-dialog" });
422
+ let nextTheme = initialState.themeName;
423
+ let themeChanged = false;
424
+ app.update((state) => {
425
+ if (state.showHelp) {
426
+ const next = reduceStarshipState(state, { type: "toggle-help" });
427
+ if (next.themeName !== state.themeName) {
428
+ nextTheme = next.themeName;
429
+ themeChanged = true;
430
+ }
431
+ return next;
432
+ }
433
+ if (state.showCommandPalette) {
434
+ const next = reduceStarshipState(state, { type: "toggle-command-palette" });
435
+ if (next.themeName !== state.themeName) {
436
+ nextTheme = next.themeName;
437
+ themeChanged = true;
438
+ }
439
+ return next;
440
+ }
441
+ if (state.showHailDialog) {
442
+ const next = reduceStarshipState(state, { type: "toggle-hail-dialog" });
443
+ if (next.themeName !== state.themeName) {
444
+ nextTheme = next.themeName;
445
+ themeChanged = true;
446
+ }
447
+ return next;
448
+ }
449
+ if (state.showResetDialog) {
450
+ const next = reduceStarshipState(state, { type: "toggle-reset-dialog" });
451
+ if (next.themeName !== state.themeName) {
452
+ nextTheme = next.themeName;
453
+ themeChanged = true;
454
+ }
455
+ return next;
456
+ }
457
+ return state;
458
+ });
459
+ if (themeChanged) {
460
+ app.setTheme(themeSpec(nextTheme).theme);
389
461
  }
390
462
  };
391
463
 
@@ -394,6 +466,13 @@ function bindKeys(): void {
394
466
 
395
467
  const routes = buildRoutes(createStarshipRoutes);
396
468
 
469
+ debugSnapshot("runtime.app.create", {
470
+ routeCount: routes.length,
471
+ initialRoute: "bridge",
472
+ fpsCap: UI_FPS_CAP,
473
+ executionMode: "inline",
474
+ });
475
+
397
476
  app = createNodeApp({
398
477
  initialState,
399
478
  routes,
@@ -421,24 +500,26 @@ app = createNodeApp({
421
500
  });
422
501
 
423
502
  bindKeys();
503
+ syncViewportFromStdout();
424
504
 
425
505
  app.onEvent((event) => {
426
506
  if (event.kind === "fatal") {
507
+ debugSnapshot("runtime.fatal", {
508
+ route: currentRouteId(),
509
+ viewportCols: lastViewport.cols,
510
+ viewportRows: lastViewport.rows,
511
+ });
427
512
  void stopApp(1);
428
513
  return;
429
514
  }
430
515
 
431
516
  if (event.kind === "engine" && event.event.kind === "resize") {
432
- dispatch({
433
- type: "add-toast",
434
- toast: {
435
- id: `resize-${event.event.cols}x${event.event.rows}-${Date.now()}`,
436
- message: `Viewport ${event.event.cols}x${event.event.rows}`,
437
- level: "info",
438
- timestamp: Date.now(),
439
- durationMs: 1800,
440
- },
517
+ debugSnapshot("runtime.resize.event", {
518
+ cols: event.event.cols,
519
+ rows: event.event.rows,
520
+ route: currentRouteId(),
441
521
  });
522
+ syncViewport(event.event.cols, event.event.rows);
442
523
  }
443
524
  });
444
525
 
@@ -450,6 +531,7 @@ process.once("SIGINT", onSignal);
450
531
  process.once("SIGTERM", onSignal);
451
532
 
452
533
  tickTimer = setInterval(() => {
534
+ syncViewportFromStdout();
453
535
  dispatch({ type: "tick", nowMs: Date.now() });
454
536
  }, TICK_MS);
455
537
 
@@ -457,5 +539,11 @@ toastTimer = setInterval(() => {
457
539
  dispatch({ type: "prune-toasts", nowMs: Date.now() });
458
540
  }, TOAST_PRUNE_MS);
459
541
 
542
+ debugSnapshot("runtime.app.start", {
543
+ route: currentRouteId(),
544
+ viewportCols: lastViewport.cols,
545
+ viewportRows: lastViewport.rows,
546
+ });
547
+
460
548
  await app.start();
461
549
  await new Promise<void>(() => {});