create-rezi 0.1.0-alpha.49 → 0.1.0-alpha.51

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.49",
3
+ "version": "0.1.0-alpha.51",
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.49"
31
+ "@rezi-ui/testkit": "0.1.0-alpha.51"
32
32
  }
33
33
  }
@@ -15,7 +15,7 @@ Scaffolded with `create-rezi` using the **__TEMPLATE_LABEL__** template.
15
15
 
16
16
  - `src/types.ts`: animation state and action contracts.
17
17
  - `src/theme.ts`: template identity constants.
18
- - `src/helpers/state.ts`: layout resolver + reducer transitions.
18
+ - `src/helpers/state.ts`: viewport normalization + reducer transitions.
19
19
  - `src/helpers/keybindings.ts`: key to command resolver.
20
20
  - `src/screens/reactor-lab.ts`: reactor field screen renderer.
21
21
  - `src/main.ts`: app bootstrap, resize handling, animation loop.
@@ -23,8 +23,8 @@ test("animation lab reducer applies viewport", () => {
23
23
 
24
24
  assert.equal(resized.viewportCols, 70);
25
25
  assert.equal(resized.viewportRows, 20);
26
- assert.ok(resized.panelWidth <= 66);
27
- assert.ok(resized.panelHeight <= 18);
26
+ assert.equal("panelWidth" in resized, false);
27
+ assert.equal("panelHeight" in resized, false);
28
28
  });
29
29
 
30
30
  test("animation lab reducer supports nudge and phase cycle", () => {
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { createTestRenderer } from "@rezi-ui/core/testing";
4
- import { createInitialState } from "../helpers/state.js";
4
+ import { createInitialState, reduceAnimationLabState } from "../helpers/state.js";
5
5
  import { renderReactorLab } from "../screens/reactor-lab.js";
6
6
 
7
7
  test("animation lab screen renders core sections", () => {
@@ -13,3 +13,17 @@ test("animation lab screen renders core sections", () => {
13
13
  assert.match(output, /Animation Lab/);
14
14
  assert.match(output, /controls: space\/p autoplay/);
15
15
  });
16
+
17
+ test("animation lab screen renders after compact viewport resize", () => {
18
+ const initial = createInitialState({ cols: 140, rows: 48 });
19
+ const compact = reduceAnimationLabState(initial, {
20
+ type: "apply-viewport",
21
+ cols: 68,
22
+ rows: 20,
23
+ });
24
+ const renderer = createTestRenderer({ viewport: { cols: 68, rows: 20 } });
25
+ const output = renderer.render(renderReactorLab(compact)).toText();
26
+
27
+ assert.match(output, /Animation Lab/);
28
+ assert.match(output, /controls: space\/p autoplay/);
29
+ });
@@ -31,36 +31,25 @@ function toPositiveInt(value: number | undefined, fallback: number): number {
31
31
  return value;
32
32
  }
33
33
 
34
- function resolveLayout(
34
+ function resolveViewport(
35
35
  cols: number,
36
36
  rows: number,
37
37
  ): Readonly<{
38
38
  viewportCols: number;
39
39
  viewportRows: number;
40
- panelWidth: number;
41
- panelHeight: number;
42
40
  }> {
43
41
  const viewportCols = clamp(toPositiveInt(cols, 96), 20, 500);
44
42
  const viewportRows = clamp(toPositiveInt(rows, 32), 10, 200);
45
-
46
- const maxPanelWidth = Math.max(20, viewportCols - 4);
47
- const panelWidth = maxPanelWidth < 44 ? maxPanelWidth : clamp(maxPanelWidth, 44, 140);
48
-
49
- const maxPanelHeight = Math.max(8, viewportRows - 4);
50
- const panelHeight = maxPanelHeight < 18 ? maxPanelHeight : clamp(maxPanelHeight, 18, 40);
51
-
52
- return Object.freeze({ viewportCols, viewportRows, panelWidth, panelHeight });
43
+ return Object.freeze({ viewportCols, viewportRows });
53
44
  }
54
45
 
55
46
  export function createInitialState(viewport?: Viewport): AnimationLabState {
56
- const layout = resolveLayout(viewport?.cols ?? 96, viewport?.rows ?? 32);
47
+ const layout = resolveViewport(viewport?.cols ?? 96, viewport?.rows ?? 32);
57
48
  return Object.freeze({
58
49
  tick: 0,
59
50
  phase: 0,
60
51
  viewportCols: layout.viewportCols,
61
52
  viewportRows: layout.viewportRows,
62
- panelWidth: layout.panelWidth,
63
- panelHeight: layout.panelHeight,
64
53
  panelOpacity: 0.9,
65
54
  driftTarget: 0.15,
66
55
  fluxTarget: 0.58,
@@ -97,8 +86,6 @@ function advanceState(previous: AnimationLabState): AnimationLabState {
97
86
  phase: previous.phase,
98
87
  viewportCols: previous.viewportCols,
99
88
  viewportRows: previous.viewportRows,
100
- panelWidth: previous.panelWidth,
101
- panelHeight: previous.panelHeight,
102
89
  panelOpacity: clamp(panelOpacityTarget, 0.3, 1),
103
90
  driftTarget,
104
91
  fluxTarget,
@@ -109,12 +96,10 @@ function advanceState(previous: AnimationLabState): AnimationLabState {
109
96
  }
110
97
 
111
98
  function applyViewport(previous: AnimationLabState, cols: number, rows: number): AnimationLabState {
112
- const layout = resolveLayout(cols, rows);
99
+ const layout = resolveViewport(cols, rows);
113
100
  if (
114
101
  previous.viewportCols === layout.viewportCols &&
115
- previous.viewportRows === layout.viewportRows &&
116
- previous.panelWidth === layout.panelWidth &&
117
- previous.panelHeight === layout.panelHeight
102
+ previous.viewportRows === layout.viewportRows
118
103
  ) {
119
104
  return previous;
120
105
  }
@@ -123,8 +108,6 @@ function applyViewport(previous: AnimationLabState, cols: number, rows: number):
123
108
  ...previous,
124
109
  viewportCols: layout.viewportCols,
125
110
  viewportRows: layout.viewportRows,
126
- panelWidth: layout.panelWidth,
127
- panelHeight: layout.panelHeight,
128
111
  });
129
112
  }
130
113
 
@@ -1,10 +1,14 @@
1
1
  import {
2
2
  defineWidget,
3
+ heightConstraints,
4
+ rgb,
3
5
  ui,
4
6
  useSequence,
5
7
  useSpring,
6
8
  useStagger,
7
9
  useTransition,
10
+ visibilityConstraints,
11
+ widthConstraints,
8
12
  type CanvasContext,
9
13
  type VNode,
10
14
  } from "@rezi-ui/core";
@@ -12,46 +16,46 @@ import { APP_NAME, PRODUCT_TAGLINE, TEMPLATE_LABEL } from "../theme.js";
12
16
  import type { AnimationLabState } from "../types.js";
13
17
 
14
18
  type Palette = Readonly<{
15
- title: Readonly<{ r: number; g: number; b: number }>;
19
+ title: number;
16
20
  accent: string;
17
21
  core: string;
18
22
  hot: string;
19
23
  wave: string;
20
- module: Readonly<{ r: number; g: number; b: number }>;
24
+ module: number;
21
25
  }>;
22
26
 
23
27
  const PALETTES: readonly Palette[] = Object.freeze([
24
28
  Object.freeze({
25
- title: Object.freeze({ r: 120, g: 225, b: 255 }),
29
+ title: rgb(120, 225, 255),
26
30
  accent: "#80dfff",
27
31
  core: "#62ffd2",
28
32
  hot: "#ffd28a",
29
33
  wave: "#98ffc2",
30
- module: Object.freeze({ r: 204, g: 232, b: 244 }),
34
+ module: rgb(204, 232, 244),
31
35
  }),
32
36
  Object.freeze({
33
- title: Object.freeze({ r: 173, g: 198, b: 255 }),
37
+ title: rgb(173, 198, 255),
34
38
  accent: "#9fb4ff",
35
39
  core: "#7ee6ff",
36
40
  hot: "#ffb26b",
37
41
  wave: "#c4ff8f",
38
- module: Object.freeze({ r: 214, g: 222, b: 245 }),
42
+ module: rgb(214, 222, 245),
39
43
  }),
40
44
  Object.freeze({
41
- title: Object.freeze({ r: 160, g: 255, b: 205 }),
45
+ title: rgb(160, 255, 205),
42
46
  accent: "#84ffd4",
43
47
  core: "#65f5b2",
44
48
  hot: "#ffc66f",
45
49
  wave: "#9cf4ff",
46
- module: Object.freeze({ r: 209, g: 239, b: 224 }),
50
+ module: rgb(209, 239, 224),
47
51
  }),
48
52
  Object.freeze({
49
- title: Object.freeze({ r: 255, g: 212, b: 150 }),
53
+ title: rgb(255, 212, 150),
50
54
  accent: "#ffd48e",
51
55
  core: "#ffc871",
52
56
  hot: "#ff8c6a",
53
57
  wave: "#a7ffe3",
54
- module: Object.freeze({ r: 240, g: 224, b: 208 }),
58
+ module: rgb(240, 224, 208),
55
59
  }),
56
60
  ]);
57
61
 
@@ -390,8 +394,8 @@ type CommandDeckProps = Readonly<{
390
394
  key?: string;
391
395
  tick: number;
392
396
  phase: number;
393
- panelWidth: number;
394
- panelHeight: number;
397
+ viewportCols: number;
398
+ viewportRows: number;
395
399
  driftTarget: number;
396
400
  fluxTarget: number;
397
401
  orbitTarget: number;
@@ -432,11 +436,13 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
432
436
  });
433
437
 
434
438
  const palette = paletteForPhase(props.phase);
435
- const compact = props.panelWidth < 72 || props.panelHeight < 26;
436
- const sidePanelWidth = compact ? clamp(Math.floor(props.panelWidth * 0.31), 14, 20) : 22;
437
- const leftPanelWidth = clamp(props.panelWidth - sidePanelWidth - 2, 20, 64);
439
+ const shellWidth = clamp(props.viewportCols - 4, 20, 140);
440
+ const shellHeight = clamp(props.viewportRows - 4, 8, 40);
441
+ const compact = shellWidth < 72 || shellHeight < 26;
442
+ const sidePanelWidth = compact ? clamp(Math.floor(shellWidth * 0.31), 14, 20) : 22;
443
+ const leftPanelWidth = clamp(shellWidth - sidePanelWidth - 2, 20, 64);
438
444
  const coreCanvasWidth = clamp(leftPanelWidth - 4, 14, 48);
439
- const coreCanvasHeight = clamp(Math.floor(props.panelHeight * 0.24), 7, 13);
445
+ const coreCanvasHeight = clamp(Math.floor(shellHeight * 0.24), 7, 13);
440
446
  const sideCanvasWidth = clamp(sidePanelWidth - 4, 8, 18);
441
447
  const spectrumHeight = compact ? 8 : 10;
442
448
  const streamChartHeight = compact ? 4 : 6;
@@ -496,8 +502,8 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
496
502
  width: moduleProgressWidth,
497
503
  variant: "blocks",
498
504
  showPercent: false,
499
- style: { fg: { r: 122, g: 255, b: 203 } },
500
- trackStyle: { fg: { r: 58, g: 86, b: 82 } },
505
+ style: { fg: rgb(122, 255, 203) },
506
+ trackStyle: { fg: rgb(58, 86, 82) },
501
507
  }),
502
508
  ]);
503
509
  });
@@ -526,7 +532,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
526
532
  ]),
527
533
  ui.text(PRODUCT_TAGLINE, {
528
534
  key: "core-tagline",
529
- style: { fg: { r: 152, g: 176, b: 200 } },
535
+ style: { fg: rgb(152, 176, 200) },
530
536
  }),
531
537
  ui.row({ key: "beacon-lane", gap: 0 }, [
532
538
  ui.spacer({ key: "beacon-spacer", size: beaconLane }),
@@ -546,7 +552,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
546
552
  [
547
553
  ui.text("◆", {
548
554
  key: "beacon-dot",
549
- style: { fg: { r: 255, g: 228, b: 158 } },
555
+ style: { fg: rgb(255, 228, 158) },
550
556
  }),
551
557
  ],
552
558
  ),
@@ -585,7 +591,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
585
591
  width: sparklineWidth,
586
592
  highRes: true,
587
593
  blitter: "braille",
588
- style: { fg: { r: 132, g: 246, b: 198 } },
594
+ style: { fg: rgb(132, 246, 198) },
589
595
  }),
590
596
  ]),
591
597
  ],
@@ -596,6 +602,8 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
596
602
  key: "spectrum-radar-panel",
597
603
  border: "single",
598
604
  width: sidePanelWidth,
605
+ // Helper-first visibility over `expr("if(viewport.w < 70, 0, 1)")`.
606
+ display: visibilityConstraints.viewportWidthAtLeast(70),
599
607
  p: 1,
600
608
  transition: {
601
609
  duration: 300,
@@ -606,7 +614,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
606
614
  [
607
615
  ui.text("Spectrum and Radar", {
608
616
  key: "spectrum-radar-title",
609
- style: { fg: { r: 174, g: 232, b: 255 } },
617
+ style: { fg: rgb(174, 232, 255) },
610
618
  }),
611
619
  ui.canvas({
612
620
  key: "spectrum-radar-canvas",
@@ -632,6 +640,8 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
632
640
  key: "stream-panel",
633
641
  border: "single",
634
642
  width: sidePanelWidth,
643
+ // Helper-first visibility over `expr("if(viewport.h < 24, 0, 1)")`.
644
+ display: visibilityConstraints.viewportHeightAtLeast(24),
635
645
  p: 1,
636
646
  transition: {
637
647
  duration: 320,
@@ -642,7 +652,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
642
652
  [
643
653
  ui.text("Telemetry Streams", {
644
654
  key: "stream-title",
645
- style: { fg: { r: 154, g: 198, b: 255 } },
655
+ style: { fg: rgb(154, 198, 255) },
646
656
  }),
647
657
  ui.lineChart({
648
658
  key: "stream-chart",
@@ -681,6 +691,8 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
681
691
  {
682
692
  key: "modules-panel",
683
693
  border: "single",
694
+ // Helper-first visibility over `expr("if(viewport.h < 22, 0, 1)")`.
695
+ display: visibilityConstraints.viewportHeightAtLeast(22),
684
696
  p: 1,
685
697
  transition: {
686
698
  duration: 260,
@@ -691,7 +703,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
691
703
  [
692
704
  ui.text("Module Sync Rails", {
693
705
  key: "modules-title",
694
- style: { fg: { r: 153, g: 255, b: 213 } },
706
+ style: { fg: rgb(153, 255, 213) },
695
707
  }),
696
708
  ui.column({ key: "modules-list", gap: 0 }, moduleRows),
697
709
  ],
@@ -702,7 +714,7 @@ const CommandDeck = defineWidget<CommandDeckProps>((props, ctx): VNode => {
702
714
  ).padStart(3, "0")}% drift=${drift.toFixed(2)} burst=${String(Math.round(burst * 100)).padStart(3, "0")}%`,
703
715
  {
704
716
  key: "metrics-readout",
705
- style: { fg: { r: 138, g: 166, b: 193 } },
717
+ style: { fg: rgb(138, 166, 193) },
706
718
  },
707
719
  ),
708
720
  ]);
@@ -718,15 +730,16 @@ export function renderReactorLab(state: AnimationLabState): VNode {
718
730
  ui.text("•", { key: "brand-dot" }),
719
731
  ui.text("Animation Lab", {
720
732
  key: "brand-name",
721
- style: { fg: { r: 150, g: 225, b: 255 } },
733
+ style: { fg: rgb(150, 225, 255) },
722
734
  }),
723
735
  ]),
724
736
  ui.box(
725
737
  {
726
738
  key: "stage-shell",
727
739
  border: "double",
728
- width: state.panelWidth,
729
- height: state.panelHeight,
740
+ // Helper-first viewport clamping over fragile raw `clamp(...)` expression strings.
741
+ width: widthConstraints.clampedViewportMinus({ minus: 4, min: 20, max: 140 }),
742
+ height: heightConstraints.clampedViewportMinus({ minus: 4, min: 8, max: 40 }),
730
743
  opacity: state.panelOpacity,
731
744
  p: 1,
732
745
  transition: {
@@ -740,8 +753,8 @@ export function renderReactorLab(state: AnimationLabState): VNode {
740
753
  key: "command-deck",
741
754
  tick: state.tick,
742
755
  phase: state.phase,
743
- panelWidth: state.panelWidth,
744
- panelHeight: state.panelHeight,
756
+ viewportCols: state.viewportCols,
757
+ viewportRows: state.viewportRows,
745
758
  driftTarget: state.driftTarget,
746
759
  fluxTarget: state.fluxTarget,
747
760
  orbitTarget: state.orbitTarget,
@@ -756,7 +769,7 @@ export function renderReactorLab(state: AnimationLabState): VNode {
756
769
  )} controls: space/p autoplay, enter step, arrows tune vectors, b burst, m palette, r random, q quit`,
757
770
  {
758
771
  key: "footer",
759
- style: { fg: { r: 130, g: 150, b: 180 } },
772
+ style: { fg: rgb(130, 150, 180) },
760
773
  },
761
774
  ),
762
775
  ]);
@@ -3,8 +3,6 @@ export type AnimationLabState = Readonly<{
3
3
  phase: number;
4
4
  viewportCols: number;
5
5
  viewportRows: number;
6
- panelWidth: number;
7
- panelHeight: number;
8
6
  panelOpacity: number;
9
7
  driftTarget: number;
10
8
  fluxTarget: number;
@@ -57,9 +57,9 @@ export function renderLogsScreen(
57
57
  ? {}
58
58
  : { levelFilter: Object.freeze(["info", "warn", "error"] as const) }),
59
59
  onScroll: (scrollTop) => deps.dispatch({ type: "set-scroll-top", scrollTop }),
60
- onEntryToggle: (entryId, expanded) =>
60
+ onChange: (entryId, expanded) =>
61
61
  deps.dispatch({ type: "set-entry-expanded", entryId, expanded }),
62
- onClear: () => deps.dispatch({ type: "clear-logs" }),
62
+ onPress: () => deps.dispatch({ type: "clear-logs" }),
63
63
  }),
64
64
  ]),
65
65
  ui.panel({ title: "Recent entries", style: styles.panelStyle }, [
@@ -1,5 +1,5 @@
1
1
  import type { VNode } from "@rezi-ui/core";
2
- import { ui, when } from "@rezi-ui/core";
2
+ import { groupConstraints, heightConstraints, ui, when, widthConstraints } from "@rezi-ui/core";
3
3
  import {
4
4
  filterLabel,
5
5
  fleetCounts,
@@ -34,6 +34,22 @@ export function renderOverviewScreen(state: DashboardState, handlers: DashboardS
34
34
  const health = statusBadge(overallStatus(state.services));
35
35
  const theme = themeSpec(state.themeName);
36
36
 
37
+ // Helper-first constraints: sibling label equalization via max_sibling(#id.min_w) and intrinsic-aware modals.
38
+ function inspectorRow(label: string, value: string): VNode {
39
+ return ui.row({ key: label, gap: 2, wrap: true, items: "center" }, [
40
+ ui.box(
41
+ {
42
+ id: "inspector-key",
43
+ width: groupConstraints.maxSiblingMinWidth("inspector-key"),
44
+ border: "none",
45
+ p: 0,
46
+ },
47
+ [ui.text(label, { style: styles.mutedStyle })],
48
+ ),
49
+ ui.text(value),
50
+ ]);
51
+ }
52
+
37
53
  const serviceRows: readonly VNode[] =
38
54
  visible.length === 0
39
55
  ? [ui.text("No services match the current filter.", { style: styles.mutedStyle })]
@@ -70,10 +86,10 @@ export function renderOverviewScreen(state: DashboardState, handlers: DashboardS
70
86
  ui.tag(service.owner, { variant: "default" }),
71
87
  ui.tag(service.region, { variant: "info" }),
72
88
  ]),
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 }),
89
+ inspectorRow("Latency", formatLatency(service.latencyMs)),
90
+ inspectorRow("Error Rate", formatErrorRate(service.errorRate)),
91
+ inspectorRow("Traffic", formatTraffic(service.trafficRpm)),
92
+ inspectorRow("Update rate", `${updateRate} Hz`),
77
93
  ui.sparkline(service.history, { width: 18, min: 0, max: 220 }),
78
94
  ]);
79
95
  },
@@ -182,7 +198,9 @@ export function renderOverviewScreen(state: DashboardState, handlers: DashboardS
182
198
  ui.modal({
183
199
  id: "dashboard-help",
184
200
  title: `${PRODUCT_NAME} Commands`,
185
- width: 70,
201
+ // Helper-first intrinsic sizing: this modal adapts to its content but stays within viewport bounds.
202
+ width: widthConstraints.clampedIntrinsicPlus({ pad: 8, min: 44, max: "parent" }),
203
+ height: heightConstraints.clampedIntrinsicPlus({ pad: 4, min: 10, max: "parent" }),
186
204
  backdrop: "none",
187
205
  returnFocusTo: "help",
188
206
  content: ui.column({ gap: 1 }, [
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import type { RouteRenderContext, RouterApi } from "@rezi-ui/core";
4
4
  import { createTestRenderer } from "@rezi-ui/core/testing";
5
- import { createInitialState } from "../helpers/state.js";
5
+ import { createInitialState, reduceStarshipState } from "../helpers/state.js";
6
6
  import { renderBridgeScreen } from "../screens/bridge.js";
7
7
  import { renderCargoScreen } from "../screens/cargo.js";
8
8
  import { renderCommsScreen } from "../screens/comms.js";
@@ -70,6 +70,22 @@ test("bridge screen renders core markers", () => {
70
70
  assert.match(output, /Route Health/);
71
71
  });
72
72
 
73
+ test("bridge screen remains deterministic in compact viewport", () => {
74
+ const state = reduceStarshipState(createInitialState(0), {
75
+ type: "set-viewport",
76
+ cols: 76,
77
+ rows: 30,
78
+ });
79
+ const renderer = createTestRenderer({ viewport: { cols: 76, rows: 30 } });
80
+ const output = renderer
81
+ .render(renderBridgeScreen(createContext(state, "bridge"), createDeps()))
82
+ .toText();
83
+
84
+ assert.match(output, /Bridge Overview/);
85
+ assert.doesNotMatch(output, /Navigation/);
86
+ assert.doesNotMatch(output, /Route Health/);
87
+ });
88
+
73
89
  test("engineering screen renders core markers", () => {
74
90
  const state = createInitialState(0);
75
91
  const renderer = createTestRenderer({ viewport: { cols: 140, rows: 48 } });
@@ -7,10 +7,6 @@ export type ResponsiveLayout = Readonly<{
7
7
  stackRightRail: boolean;
8
8
  compactSidebar: boolean;
9
9
  hideNonCritical: boolean;
10
- sidebarWidth: number;
11
- crewMasterWidth: number;
12
- chartWidth: number;
13
- canvasWidth: number;
14
10
  }>;
15
11
 
16
12
  export type ViewportSnapshot = Readonly<{
@@ -24,14 +20,21 @@ function clamp(value: number, min: number, max: number): number {
24
20
  return value;
25
21
  }
26
22
 
23
+ function toPositiveInt(value: number, fallback: number): number {
24
+ if (typeof value !== "number" || !Number.isFinite(value)) {
25
+ return fallback;
26
+ }
27
+ if (value >= 0) return Math.floor(value);
28
+ return Math.ceil(value);
29
+ }
30
+
27
31
  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));
32
+ const width = clamp(toPositiveInt(viewport.width, 96), 40, 500);
33
+ const height = clamp(toPositiveInt(viewport.height, 32), 18, 200);
30
34
  const wide = width >= 120;
31
35
  const stackRightRail = width < 120;
32
36
  const compactSidebar = width < 90;
33
37
  const hideNonCritical = width < 80 || height < 26;
34
- const sidebarWidth = compactSidebar ? 18 : 34;
35
38
 
36
39
  return Object.freeze({
37
40
  width,
@@ -40,10 +43,6 @@ export function resolveLayout(viewport: ViewportSnapshot): ResponsiveLayout {
40
43
  stackRightRail,
41
44
  compactSidebar,
42
45
  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
46
  });
48
47
  }
49
48
 
@@ -195,7 +195,7 @@ async function stopApp(code = 0): Promise<void> {
195
195
  } catch {
196
196
  // Ignore stop races.
197
197
  }
198
- frameAuditGlobal.__reziFrameAuditContext = undefined;
198
+ frameAuditGlobal.__reziFrameAuditContext = () => Object.freeze({});
199
199
  stopResolve?.();
200
200
  stopResolve = null;
201
201
  }
@@ -131,8 +131,8 @@ const BridgeCommandDeck = defineWidget<BridgeCommandDeckProps>((props, ctx): VNo
131
131
 
132
132
  const selected = selectedCrew(props.state);
133
133
  const subsystemNames = props.state.subsystems.map((item) => item.name);
134
- const chartWidth = layout.chartWidth;
135
- const schematicWidth = layout.canvasWidth;
134
+ const chartWidth = clamp(Math.floor(layout.width * (layout.wide ? 0.5 : 0.9)), 28, 132);
135
+ const schematicWidth = clamp(Math.floor(layout.width * (layout.wide ? 0.48 : 0.9)), 26, 116);
136
136
  const schematicHeight = clamp(Math.floor(contentRows * 0.38), 8, 14);
137
137
  const showGaugeRow = contentRows >= 28;
138
138
  const showSparkline = contentRows >= 24;
@@ -326,7 +326,7 @@ const BridgeCommandDeck = defineWidget<BridgeCommandDeckProps>((props, ctx): VNo
326
326
  showSystemsPanel: contentRows >= 60,
327
327
  });
328
328
  const commandRegion = showCommandSummary
329
- ? ui.row({ gap: SPACE.sm, items: "stretch", width: "100%" }, [
329
+ ? ui.row({ gap: SPACE.sm, items: "stretch", width: "full" }, [
330
330
  ui.box({ border: "none", p: 0, flex: 2 }, [commandDeck]),
331
331
  ui.box({ border: "none", p: 0, flex: 1 }, [commandSummary]),
332
332
  ])
@@ -498,18 +498,18 @@ const BridgeCommandDeck = defineWidget<BridgeCommandDeckProps>((props, ctx): VNo
498
498
 
499
499
  const showSchematicRail = layout.wide && !layout.hideNonCritical && contentRows >= 28;
500
500
  const telemetryRegion = showSchematicRail
501
- ? ui.row({ gap: SPACE.sm, items: "stretch", wrap: false, width: "100%" }, [
501
+ ? ui.row({ gap: SPACE.sm, items: "stretch", wrap: false, width: "full" }, [
502
502
  ui.box({ border: "none", p: 0, flex: 2 }, [telemetryPanel]),
503
503
  ui.box({ border: "none", p: 0, flex: 1 }, [schematicPanel]),
504
504
  ])
505
505
  : telemetryPanel;
506
506
 
507
507
  if (veryCompactHeight) {
508
- return ui.column({ gap: SPACE.sm, width: "100%" }, [commandDeck]);
508
+ return ui.column({ gap: SPACE.sm, width: "full" }, [commandDeck]);
509
509
  }
510
510
 
511
511
  if (compactHeight) {
512
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
512
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
513
513
  commandDeck,
514
514
  surfacePanel(
515
515
  tokens,
@@ -532,10 +532,10 @@ const BridgeCommandDeck = defineWidget<BridgeCommandDeckProps>((props, ctx): VNo
532
532
  }
533
533
 
534
534
  if (constrainedHeight) {
535
- return ui.column({ gap: SPACE.sm, width: "100%" }, [commandDeck, telemetryPanel]);
535
+ return ui.column({ gap: SPACE.sm, width: "full" }, [commandDeck, telemetryPanel]);
536
536
  }
537
537
 
538
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
538
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
539
539
  commandRegion,
540
540
  telemetryRegion,
541
541
  ...(contentRows >= 60 ? [systemsPanel] : []),
@@ -554,7 +554,7 @@ export function renderBridgeScreen(
554
554
  title: "Bridge Overview",
555
555
  context,
556
556
  deps,
557
- body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
557
+ body: ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
558
558
  BridgeCommandDeck({ key: "bridge-command-deck", state, dispatch: deps.dispatch }),
559
559
  ]),
560
560
  });
@@ -22,6 +22,12 @@ function categoryLabel(category: CargoItem["category"]): string {
22
22
  return "Ordnance";
23
23
  }
24
24
 
25
+ function clamp(value: number, min: number, max: number): number {
26
+ if (value < min) return min;
27
+ if (value > max) return max;
28
+ return value;
29
+ }
30
+
25
31
  export function renderCargoScreen(
26
32
  context: RouteRenderContext<StarshipState>,
27
33
  deps: RouteDeps,
@@ -30,7 +36,7 @@ export function renderCargoScreen(
30
36
  title: "Cargo Hold",
31
37
  context,
32
38
  deps,
33
- body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
39
+ body: ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
34
40
  CargoDeck({
35
41
  key: "cargo-deck",
36
42
  state: context.state,
@@ -53,6 +59,7 @@ const CargoDeck = defineWidget<CargoDeckProps>((props, ctx): VNode => {
53
59
  width: state.viewportCols,
54
60
  height: state.viewportRows,
55
61
  });
62
+ const chartWidth = clamp(Math.floor(layout.width * (layout.wide ? 0.5 : 0.9)), 28, 132);
56
63
  const cargo = sortedCargo(state);
57
64
  const summary = cargoSummary(cargo);
58
65
  const totalItems = cargo.length;
@@ -62,7 +69,7 @@ const CargoDeck = defineWidget<CargoDeckProps>((props, ctx): VNode => {
62
69
  : cargo.reduce((sum, item) => sum + item.priority, 0) / Math.max(1, cargo.length);
63
70
  const selected =
64
71
  (state.selectedCargoId && cargo.find((item) => item.id === state.selectedCargoId)) || cargo[0] || null;
65
- const nameWidth = Math.max(12, Math.min(24, layout.chartWidth - 14));
72
+ const nameWidth = Math.max(12, Math.min(24, chartWidth - 14));
66
73
  const showMetricsPanel = !layout.hideNonCritical && layout.height >= 40;
67
74
  const showSelectedPanel = !layout.hideNonCritical && layout.height >= 48;
68
75
 
@@ -99,7 +106,7 @@ const CargoDeck = defineWidget<CargoDeckProps>((props, ctx): VNode => {
99
106
  });
100
107
 
101
108
  if (layout.height <= 28) {
102
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
109
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
103
110
  surfacePanel(tokens, "Cargo Snapshot", [
104
111
  sectionHeader(tokens, "Compact Cargo View", "Expand terminal height for full manifest + charts"),
105
112
  ui.row({ gap: SPACE.xs, wrap: true }, [
@@ -188,7 +195,7 @@ const CargoDeck = defineWidget<CargoDeckProps>((props, ctx): VNode => {
188
195
  ui.barChart(chartItems, { orientation: "horizontal", showValues: true }),
189
196
  ui.scatter({
190
197
  id: "cargo-scatter",
191
- width: Math.max(32, layout.chartWidth + 6),
198
+ width: Math.max(32, chartWidth + 6),
192
199
  height: 10,
193
200
  points: scatterPoints,
194
201
  blitter: "braille",
@@ -308,7 +315,7 @@ const CargoDeck = defineWidget<CargoDeckProps>((props, ctx): VNode => {
308
315
  { tone: "base" },
309
316
  );
310
317
 
311
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
318
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
312
319
  controlsPanel,
313
320
  showMetricsPanel
314
321
  ? ui.row({ gap: SPACE.sm, wrap: true, items: "stretch" }, [
@@ -73,7 +73,7 @@ const CommsDeck = defineWidget<CommsDeckProps>((props, ctx): VNode => {
73
73
  };
74
74
 
75
75
  if (compactHeight) {
76
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
76
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
77
77
  surfacePanel(tokens, "Channel Controls", [
78
78
  sectionHeader(tokens, "Compact Comms View", "Expand terminal height for full traffic console"),
79
79
  ui.tabs({
@@ -165,7 +165,7 @@ const CommsDeck = defineWidget<CommsDeckProps>((props, ctx): VNode => {
165
165
  focusedStyle: { fg: tokens.text.primary, bg: tokens.bg.panel.elevated, bold: true },
166
166
  onScroll: (scrollTop) => props.dispatch({ type: "set-comms-scroll", scrollTop }),
167
167
  expandedEntries: state.expandedMessageIds,
168
- onEntryToggle: (entryId, expanded) =>
168
+ onChange: (entryId, expanded) =>
169
169
  props.dispatch({
170
170
  type: "toggle-message-expanded",
171
171
  messageId: entryId,
@@ -259,7 +259,7 @@ const CommsDeck = defineWidget<CommsDeckProps>((props, ctx): VNode => {
259
259
  }) ?? ui.text(""),
260
260
  ]);
261
261
 
262
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
262
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
263
263
  show(
264
264
  state.activeChannel === "emergency",
265
265
  ui.callout("Emergency channel monitored with elevated priority.", {
@@ -462,7 +462,7 @@ export function renderCommsScreen(
462
462
  title: "Communications",
463
463
  context,
464
464
  deps,
465
- body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
465
+ body: ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
466
466
  CommsDeck({
467
467
  key: "comms-deck",
468
468
  state: context.state,
@@ -1,9 +1,12 @@
1
1
  import {
2
+ conditionalConstraints,
2
3
  defineWidget,
4
+ heightConstraints,
3
5
  maybe,
4
6
  show,
5
7
  ui,
6
8
  useAsync,
9
+ visibilityConstraints,
7
10
  type RouteRenderContext,
8
11
  type VNode,
9
12
  } from "@rezi-ui/core";
@@ -36,6 +39,12 @@ function validateCriticalDepartments(crew: readonly CrewMember[]): string | null
36
39
  return null;
37
40
  }
38
41
 
42
+ function clamp(value: number, min: number, max: number): number {
43
+ if (value < min) return min;
44
+ if (value > max) return max;
45
+ return value;
46
+ }
47
+
39
48
  type CrewDeckProps = Readonly<{
40
49
  key?: string;
41
50
  state: StarshipState;
@@ -48,6 +57,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
48
57
  width: props.state.viewportCols,
49
58
  height: props.state.viewportRows,
50
59
  });
60
+ const chartWidth = clamp(Math.floor(layout.width * (layout.wide ? 0.5 : 0.9)), 28, 132);
51
61
  const compactHeight = layout.height < 34;
52
62
  const showDetailPane = layout.height >= 38;
53
63
  const visible = visibleCrew(props.state);
@@ -194,7 +204,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
194
204
  ...tableSkin(tokens),
195
205
  });
196
206
 
197
- const detailPanel = ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
207
+ const detailPanel = ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
198
208
  maybe(selected, (member) =>
199
209
  surfacePanel(
200
210
  tokens,
@@ -208,7 +218,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
208
218
  ]),
209
219
  progressRow(tokens, "Efficiency", member.efficiency / 100, {
210
220
  labelWidth: 12,
211
- width: Math.max(18, layout.chartWidth - 10),
221
+ width: Math.max(18, chartWidth - 10),
212
222
  tone: member.efficiency < 45 ? "warning" : "success",
213
223
  trend: member.efficiency >= 50 ? 1 : -1,
214
224
  }),
@@ -301,12 +311,12 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
301
311
  ),
302
312
  ]);
303
313
 
304
- const manifestBlock = ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
314
+ const manifestBlock = ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
305
315
  ui.box(
306
316
  {
307
317
  border: "none",
308
318
  p: 0,
309
- width: "100%",
319
+ width: "full",
310
320
  flex: 1,
311
321
  minHeight: 10,
312
322
  overflow: "hidden",
@@ -328,8 +338,8 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
328
338
  {
329
339
  id: ctx.id("crew-master-detail"),
330
340
  gap: SPACE.sm,
331
- width: "100%",
332
- height: "100%",
341
+ width: "full",
342
+ height: "full",
333
343
  items: "stretch",
334
344
  wrap: false,
335
345
  },
@@ -338,8 +348,13 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
338
348
  {
339
349
  border: "none",
340
350
  p: 0,
341
- width: layout.crewMasterWidth,
342
- height: "100%",
351
+ // Helper-first responsive sizing: wide terminals get a stable master width, narrow gets fallback width.
352
+ width: conditionalConstraints.ifThenElse(
353
+ visibilityConstraints.viewportWidthAtLeast(120),
354
+ 60,
355
+ 100,
356
+ ),
357
+ height: "full",
343
358
  overflow: "hidden",
344
359
  },
345
360
  [manifestBlock],
@@ -349,7 +364,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
349
364
  border: "none",
350
365
  p: 0,
351
366
  flex: 1,
352
- height: "100%",
367
+ height: "full",
353
368
  overflow: "hidden",
354
369
  },
355
370
  [detailPanel],
@@ -358,19 +373,24 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
358
373
  )
359
374
  : manifestBlock
360
375
  : showDetailPane
361
- ? ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
376
+ ? ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
362
377
  ui.box(
363
- { border: "none", p: 0, width: "100%", flex: 1, minHeight: 10, overflow: "hidden" },
378
+ { border: "none", p: 0, width: "full", flex: 1, minHeight: 10, overflow: "hidden" },
364
379
  [manifestBlock],
365
380
  ),
366
381
  ui.box(
367
- { border: "none", p: 0, width: "100%", flex: 1, minHeight: 10, overflow: "hidden" },
382
+ { border: "none", p: 0, width: "full", flex: 1, minHeight: 10, overflow: "hidden" },
368
383
  [detailPanel],
369
384
  ),
370
385
  ])
371
386
  : manifestBlock;
372
387
 
373
- const operationsPanelMaxHeight = Math.max(12, Math.min(22, Math.floor(layout.height * 0.34)));
388
+ // Helper-first viewport-derived sizing (clamp 12..22 at 34% of viewport height).
389
+ const operationsPanelHeightExpr = heightConstraints.clampedPercentOfViewport({
390
+ ratio: 0.34,
391
+ min: 12,
392
+ max: 22,
393
+ });
374
394
  debugSnapshot("crew.render", {
375
395
  viewportCols: props.state.viewportCols,
376
396
  viewportRows: props.state.viewportRows,
@@ -380,15 +400,15 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
380
400
  totalPages,
381
401
  pageDataCount: pageData.length,
382
402
  showDetailPane,
383
- operationsPanelMaxHeight,
403
+ operationsPanelHeight: operationsPanelHeightExpr.source,
384
404
  editingCrew: props.state.editingCrew,
385
405
  });
386
406
  const operationsPanel = ui.box(
387
407
  {
388
408
  border: "none",
389
409
  p: 0,
390
- width: "100%",
391
- height: operationsPanelMaxHeight,
410
+ width: "full",
411
+ height: operationsPanelHeightExpr,
392
412
  overflow: "scroll",
393
413
  },
394
414
  [
@@ -502,7 +522,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
502
522
  ],
503
523
  );
504
524
 
505
- return ui.column({ gap: SPACE.md, width: "100%", height: "100%" }, [
525
+ return ui.column({ gap: SPACE.md, width: "full", height: "full" }, [
506
526
  operationsPanel,
507
527
  show(
508
528
  asyncCrew.loading,
@@ -519,7 +539,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
519
539
  {
520
540
  border: "none",
521
541
  p: 0,
522
- width: "100%",
542
+ width: "full",
523
543
  flex: 1,
524
544
  minHeight: 12,
525
545
  overflow: "hidden",
@@ -535,7 +555,7 @@ export function renderCrewScreen(context: RouteRenderContext<StarshipState>, dep
535
555
  title: "Crew Manifest",
536
556
  context,
537
557
  deps,
538
- body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
558
+ body: ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
539
559
  CrewDeck({
540
560
  key: "crew-deck",
541
561
  state: context.state,
@@ -5,6 +5,7 @@ import {
5
5
  useSequence,
6
6
  useSpring,
7
7
  useStagger,
8
+ widthConstraints,
8
9
  type CanvasContext,
9
10
  type NodeState,
10
11
  type RouteRenderContext,
@@ -28,6 +29,12 @@ function buildSubsystemChildren(subsystems: readonly Subsystem[]): Map<string |
28
29
  return map;
29
30
  }
30
31
 
32
+ function clamp(value: number, min: number, max: number): number {
33
+ if (value < min) return min;
34
+ if (value > max) return max;
35
+ return value;
36
+ }
37
+
31
38
  type ReactorPalette = Readonly<{
32
39
  background: string;
33
40
  border: string;
@@ -105,6 +112,8 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
105
112
  const leftPanePanelCount = 1 + (showSecondaryPanels ? 1 : 0);
106
113
  const rightPanePanelCount = 1 + (showSecondaryPanels ? 2 : 0);
107
114
  const reactorCanvasHeight = Math.max(8, Math.min(14, Math.floor(contentRows * 0.42)));
115
+ const chartWidth = clamp(Math.floor(layout.width * (layout.wide ? 0.5 : 0.9)), 28, 132);
116
+ const canvasWidth = clamp(Math.floor(layout.width * (layout.wide ? 0.48 : 0.9)), 26, 116);
108
117
  debugSnapshot("engineering.layout", {
109
118
  viewportCols: props.state.viewportCols,
110
119
  viewportRows: props.state.viewportRows,
@@ -214,7 +223,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
214
223
  : surfacePanel(tokens, "Reactor Schematic", [
215
224
  ui.canvas({
216
225
  id: ctx.id("reactor-canvas"),
217
- width: Math.max(32, layout.canvasWidth),
226
+ width: Math.max(32, canvasWidth),
218
227
  height: reactorCanvasHeight,
219
228
  blitter: "braille",
220
229
  draw: (canvas) => {
@@ -272,7 +281,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
272
281
  ...(selectedSubsystemId ? { selected: selectedSubsystemId } : {}),
273
282
  showLines: true,
274
283
  indentSize: 2,
275
- onToggle: (node) => props.dispatch({ type: "toggle-subsystem", subsystemId: node.id }),
284
+ onChange: (node) => props.dispatch({ type: "toggle-subsystem", subsystemId: node.id }),
276
285
  onSelect: (node) => setSelectedSubsystemId(node.id),
277
286
  renderNode: (node, depth, state: NodeState) =>
278
287
  ui.row({ gap: SPACE.xs, wrap: false }, [
@@ -310,7 +319,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
310
319
  ui.column({ key: subsystem.id, gap: SPACE.xs }, [
311
320
  progressRow(tokens, subsystem.name, subsystem.power / 100, {
312
321
  labelWidth: 18,
313
- width: Math.max(22, layout.chartWidth - 8),
322
+ width: Math.max(22, chartWidth - 8),
314
323
  tone: subsystem.health < props.state.alertThreshold ? "warning" : "default",
315
324
  trend: subsystem.health < props.state.alertThreshold ? -1 : 1,
316
325
  }),
@@ -318,7 +327,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
318
327
  ? [
319
328
  progressRow(tokens, "Boot", stagger[index] ?? 0, {
320
329
  labelWidth: 18,
321
- width: Math.max(22, layout.chartWidth - 8),
330
+ width: Math.max(22, chartWidth - 8),
322
331
  tone: "success",
323
332
  trend: 1,
324
333
  }),
@@ -332,7 +341,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
332
341
  const thermalPanel = surfacePanel(tokens, "Thermal Map", [
333
342
  ui.heatmap({
334
343
  id: ctx.id("engineering-heatmap"),
335
- width: Math.max(30, layout.chartWidth),
344
+ width: Math.max(30, chartWidth),
336
345
  height: 10,
337
346
  data: heatmapData,
338
347
  colorScale: "inferno",
@@ -372,40 +381,40 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
372
381
  ]);
373
382
 
374
383
  const leftPane = showSecondaryPanels
375
- ? ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
376
- ui.box({ border: "none", p: 0, width: "100%", flex: 3, minHeight: 12 }, [reactorPanel]),
377
- ui.box({ border: "none", p: 0, width: "100%", flex: 2, minHeight: 10, overflow: "hidden" }, [
384
+ ? ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
385
+ ui.box({ border: "none", p: 0, width: "full", flex: 3, minHeight: 12 }, [reactorPanel]),
386
+ ui.box({ border: "none", p: 0, width: "full", flex: 2, minHeight: 10, overflow: "hidden" }, [
378
387
  treePanel,
379
388
  ]),
380
389
  ])
381
- : ui.column({ gap: SPACE.sm, width: "100%" }, [reactorPanel]);
390
+ : ui.column({ gap: SPACE.sm, width: "full" }, [reactorPanel]);
382
391
  const rightPane = showSecondaryPanels
383
- ? ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
384
- ui.box({ border: "none", p: 0, width: "100%", flex: 3, minHeight: 12, overflow: "hidden" }, [
392
+ ? ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
393
+ ui.box({ border: "none", p: 0, width: "full", flex: 3, minHeight: 12, overflow: "hidden" }, [
385
394
  powerPanel,
386
395
  ]),
387
- ui.box({ border: "none", p: 0, width: "100%", flex: 2, minHeight: 10 }, [thermalPanel]),
388
- ui.box({ border: "none", p: 0, width: "100%", flex: 2, minHeight: 10, overflow: "hidden" }, [
396
+ ui.box({ border: "none", p: 0, width: "full", flex: 2, minHeight: 10 }, [thermalPanel]),
397
+ ui.box({ border: "none", p: 0, width: "full", flex: 2, minHeight: 10, overflow: "hidden" }, [
389
398
  diagnosticsPanel,
390
399
  ]),
391
400
  ])
392
- : ui.column({ gap: SPACE.sm, width: "100%" }, [powerPanel]);
401
+ : ui.column({ gap: SPACE.sm, width: "full" }, [powerPanel]);
393
402
 
394
403
  const responsiveDeckMinHeight = Math.max(
395
404
  16,
396
405
  contentRows - (showControlsSummary ? 12 : 10) - (showSecondaryPanels ? 0 : 2),
397
406
  );
398
407
  const responsiveDeckBody = useWideRow
399
- ? ui.row({ gap: SPACE.sm, items: "stretch", width: "100%" }, [
408
+ ? ui.row({ gap: SPACE.sm, items: "stretch", width: "full" }, [
400
409
  ui.box({ border: "none", p: 0, flex: 2 }, [leftPane]),
401
410
  ui.box({ border: "none", p: 0, flex: 3 }, [rightPane]),
402
411
  ])
403
- : ui.column({ gap: SPACE.sm, width: "100%" }, [leftPane, rightPane]);
412
+ : ui.column({ gap: SPACE.sm, width: "full" }, [leftPane, rightPane]);
404
413
  const responsiveDeck = ui.box(
405
414
  {
406
415
  border: "none",
407
416
  p: 0,
408
- width: "100%",
417
+ width: "full",
409
418
  flex: 1,
410
419
  minHeight: responsiveDeckMinHeight,
411
420
  overflow: "scroll",
@@ -475,12 +484,13 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
475
484
  );
476
485
 
477
486
  const controlsRegion = showControlsSummary
478
- ? ui.row({ gap: SPACE.sm, items: "start", width: "100%", wrap: false }, [
487
+ ? ui.row({ gap: SPACE.sm, items: "start", width: "full", wrap: false }, [
479
488
  ui.box(
480
489
  {
481
490
  border: "none",
482
491
  p: 0,
483
- width: Math.max(56, Math.floor(layout.width * 0.62)),
492
+ // Helper-first: replaces raw `expr("max(56, viewport.w * 0.62)")`.
493
+ width: widthConstraints.minViewportPercent({ ratio: 0.62, min: 56 }),
484
494
  },
485
495
  [controlsPanel],
486
496
  ),
@@ -488,7 +498,8 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
488
498
  {
489
499
  border: "none",
490
500
  p: 0,
491
- width: Math.max(34, Math.floor(layout.width * 0.34)),
501
+ // Helper-first: replaces raw `expr("max(34, viewport.w * 0.34)")`.
502
+ width: widthConstraints.minViewportPercent({ ratio: 0.34, min: 34 }),
492
503
  },
493
504
  [controlsSummary],
494
505
  ),
@@ -507,14 +518,14 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
507
518
  });
508
519
 
509
520
  if (veryCompactHeight) {
510
- return ui.column({ gap: SPACE.sm, width: "100%" }, [controlsPanel]);
521
+ return ui.column({ gap: SPACE.sm, width: "full" }, [controlsPanel]);
511
522
  }
512
523
 
513
524
  if (compactHeight) {
514
- return ui.column({ gap: SPACE.sm, width: "100%" }, [controlsPanel, reactorPanel]);
525
+ return ui.column({ gap: SPACE.sm, width: "full" }, [controlsPanel, reactorPanel]);
515
526
  }
516
527
 
517
- return ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
528
+ return ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
518
529
  controlsRegion,
519
530
  responsiveDeck,
520
531
  ]);
@@ -528,7 +539,7 @@ export function renderEngineeringScreen(
528
539
  title: "Engineering Deck",
529
540
  context,
530
541
  deps,
531
- body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
542
+ body: ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
532
543
  EngineeringDeck({
533
544
  key: "engineering-deck",
534
545
  state: context.state,
@@ -66,7 +66,7 @@ export function surfacePanel(
66
66
  border: "rounded",
67
67
  p: options?.p ?? SPACE.sm,
68
68
  gap: options?.gap ?? SPACE.sm,
69
- ...(fill ? { width: "100%" } : {}),
69
+ ...(fill ? { width: "full" } : {}),
70
70
  style: { bg: colors.background, fg: tokens.text.primary },
71
71
  borderStyle: { fg: colors.border, bg: colors.background },
72
72
  inheritStyle: { fg: tokens.text.primary },
@@ -159,7 +159,7 @@ const SettingsDeck = defineWidget<SettingsDeckProps>((props, ctx): VNode => {
159
159
  ], { tone: "base" });
160
160
 
161
161
  return ui.layers([
162
- ui.column({ gap: SPACE.md, width: "100%" }, [
162
+ ui.column({ gap: SPACE.md, width: "full" }, [
163
163
  settingsForm,
164
164
  layout.hideNonCritical
165
165
  ? surfacePanel(tokens, "Theme Snapshot", [
@@ -209,7 +209,7 @@ function settingsRightRail(state: StarshipState, deps: RouteDeps): VNode {
209
209
  subtitle: activeTheme.label,
210
210
  actions: [ui.badge("Preview", { variant: "info" })],
211
211
  }),
212
- body: ui.column({ gap: SPACE.xs, width: "100%", height: "100%" }, [
212
+ body: ui.column({ gap: SPACE.xs, width: "full", height: "full" }, [
213
213
  ui.breadcrumb({
214
214
  items: [{ label: "Bridge" }, { label: "Settings" }, { label: "Theme Preview" }],
215
215
  }),
@@ -228,7 +228,7 @@ function settingsRightRail(state: StarshipState, deps: RouteDeps): VNode {
228
228
  }),
229
229
  });
230
230
 
231
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
231
+ return ui.column({ gap: SPACE.sm, width: "full" }, [
232
232
  surfacePanel(tokens, "Theme Preview", [
233
233
  sectionHeader(tokens, "Theme Modes", "Changes apply instantly across the console"),
234
234
  ui.grid(
@@ -277,7 +277,7 @@ export function renderSettingsScreen(
277
277
  title: "Ship Settings",
278
278
  context,
279
279
  deps,
280
- body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
280
+ body: ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [
281
281
  SettingsDeck({
282
282
  key: "settings-deck",
283
283
  state: context.state,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ui,
3
+ visibilityConstraints,
3
4
  type CommandItem,
4
5
  type CommandSource,
5
6
  type RegisteredBinding,
@@ -149,12 +150,15 @@ export function renderShell(options: ShellOptions): VNode {
149
150
  });
150
151
  const compactHeight = layout.height <= 34;
151
152
  const minimalHeight = layout.height <= 30;
152
- const showBreadcrumbStrip = !compactHeight;
153
- const showTabsStrip = !minimalHeight;
154
153
  const showSidebar = !minimalHeight && layout.width >= 78;
155
154
  const showRouteHealth = !compactHeight && showSidebar;
156
155
  const showToastOverlay = !compactHeight && state.toasts.length > 0;
157
- const showRightRail = Boolean(options.rightRail && !layout.hideNonCritical && layout.height >= 40);
156
+ const showRightRail = Boolean(
157
+ options.rightRail && !layout.hideNonCritical && layout.height >= 40 && layout.width >= 80,
158
+ );
159
+ const sidebarWidth = layout.compactSidebar ? 18 : 34;
160
+ // Helper-first constraints (readable intent) over raw `expr("if(viewport.w < ...)")` strings.
161
+ const rightRailDisplay = visibilityConstraints.viewportAtLeast({ width: 80, height: 40 });
158
162
  debugSnapshot("shell.layout", {
159
163
  route: options.context.router.currentRoute().id,
160
164
  viewportCols: state.viewportCols,
@@ -298,7 +302,7 @@ export function renderShell(options: ShellOptions): VNode {
298
302
  {
299
303
  border: "none",
300
304
  p: SPACE.xs,
301
- width: "100%",
305
+ width: "full",
302
306
  style: { bg: tokens.bg.panel.inset, fg: tokens.text.primary },
303
307
  inheritStyle: { fg: tokens.text.primary },
304
308
  },
@@ -337,7 +341,7 @@ export function renderShell(options: ShellOptions): VNode {
337
341
  {
338
342
  border: "none",
339
343
  p: SPACE.xs,
340
- width: "100%",
344
+ width: "full",
341
345
  style: { bg: tokens.bg.panel.inset, fg: tokens.text.primary },
342
346
  inheritStyle: { fg: tokens.text.primary },
343
347
  },
@@ -394,17 +398,33 @@ export function renderShell(options: ShellOptions): VNode {
394
398
  const bodyMain = ui.column(
395
399
  {
396
400
  gap: SPACE.sm,
397
- width: "100%",
398
- height: "100%",
401
+ width: "full",
402
+ height: "full",
399
403
  },
400
404
  [
401
- ...(showBreadcrumbStrip ? [breadcrumbStrip] : []),
402
- ...(showTabsStrip ? [tabsStrip] : []),
403
405
  ui.box(
404
406
  {
405
407
  border: "none",
406
408
  p: 0,
407
- width: "100%",
409
+ width: "full",
410
+ display: visibilityConstraints.viewportHeightAtLeast(35),
411
+ },
412
+ [breadcrumbStrip],
413
+ ),
414
+ ui.box(
415
+ {
416
+ border: "none",
417
+ p: 0,
418
+ width: "full",
419
+ display: visibilityConstraints.viewportHeightAtLeast(31),
420
+ },
421
+ [tabsStrip],
422
+ ),
423
+ ui.box(
424
+ {
425
+ border: "none",
426
+ p: 0,
427
+ width: "full",
408
428
  flex: 1,
409
429
  style: { bg: tokens.bg.app, fg: tokens.text.primary },
410
430
  inheritStyle: { fg: tokens.text.primary },
@@ -414,12 +434,22 @@ export function renderShell(options: ShellOptions): VNode {
414
434
  ],
415
435
  );
416
436
 
417
- const rightRailNode = showRightRail ? options.rightRail ?? null : null;
437
+ const rightRailNode = showRightRail
438
+ ? ui.box(
439
+ {
440
+ border: "none",
441
+ p: 0,
442
+ width: "full",
443
+ display: rightRailDisplay,
444
+ },
445
+ [options.rightRail],
446
+ )
447
+ : null;
418
448
  const bodyWithRail =
419
449
  rightRailNode
420
450
  ? layout.stackRightRail
421
- ? ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [bodyMain, rightRailNode])
422
- : ui.row({ gap: SPACE.sm, items: "stretch", width: "100%", height: "100%" }, [
451
+ ? ui.column({ gap: SPACE.sm, width: "full", height: "full" }, [bodyMain, rightRailNode])
452
+ : ui.row({ gap: SPACE.sm, items: "stretch", width: "full", height: "full" }, [
423
453
  ui.box({ flex: 2, border: "none", p: 0 }, [bodyMain]),
424
454
  ui.box({ flex: 1, border: "none", p: 0 }, [rightRailNode]),
425
455
  ])
@@ -456,7 +486,7 @@ export function renderShell(options: ShellOptions): VNode {
456
486
  {
457
487
  border: "none",
458
488
  p: 0,
459
- width: "100%",
489
+ width: "full",
460
490
  style: { bg: tokens.bg.panel.base, fg: tokens.text.primary },
461
491
  inheritStyle: { fg: tokens.text.primary },
462
492
  },
@@ -467,8 +497,8 @@ export function renderShell(options: ShellOptions): VNode {
467
497
  {
468
498
  border: "none",
469
499
  p: 0,
470
- width: "100%",
471
- height: "100%",
500
+ width: "full",
501
+ height: "full",
472
502
  style: { bg: tokens.bg.app, fg: tokens.text.primary },
473
503
  inheritStyle: { fg: tokens.text.primary },
474
504
  },
@@ -479,8 +509,8 @@ export function renderShell(options: ShellOptions): VNode {
479
509
  {
480
510
  border: "none",
481
511
  p: 0,
482
- width: "100%",
483
- height: "100%",
512
+ width: "full",
513
+ height: "full",
484
514
  style: { bg: tokens.bg.app, fg: tokens.text.primary },
485
515
  inheritStyle: { fg: tokens.text.primary },
486
516
  },
@@ -509,8 +539,8 @@ export function renderShell(options: ShellOptions): VNode {
509
539
  {
510
540
  border: "none",
511
541
  p: 0,
512
- width: "100%",
513
- height: "100%",
542
+ width: "full",
543
+ height: "full",
514
544
  style: { bg: tokens.bg.app, fg: tokens.text.primary },
515
545
  inheritStyle: { fg: tokens.text.primary },
516
546
  },
@@ -522,7 +552,7 @@ export function renderShell(options: ShellOptions): VNode {
522
552
  ...(showSidebar
523
553
  ? {
524
554
  sidebar: {
525
- width: layout.sidebarWidth,
555
+ width: sidebarWidth,
526
556
  content: sidebarContent,
527
557
  },
528
558
  }
@@ -588,7 +618,7 @@ export function renderShell(options: ShellOptions): VNode {
588
618
  background: tokens.bg.panel.elevated,
589
619
  foreground: tokens.text.primary,
590
620
  },
591
- onDismiss: (id) => options.deps.dispatch({ type: "dismiss-toast", toastId: id }),
621
+ onClose: (id) => options.deps.dispatch({ type: "dismiss-toast", toastId: id }),
592
622
  }),
593
623
  })
594
624
  : null,
@@ -612,7 +642,7 @@ export function renderShell(options: ShellOptions): VNode {
612
642
  sources: commandSources,
613
643
  selectedIndex: state.commandIndex,
614
644
  placeholder: "Type command or route",
615
- onQueryChange: (query) => options.deps.dispatch({ type: "set-command-query", query }),
645
+ onChange: (query) => options.deps.dispatch({ type: "set-command-query", query }),
616
646
  onSelectionChange: (index) =>
617
647
  options.deps.dispatch({ type: "set-command-index", index }),
618
648
  onSelect: (item) => {