create-rezi 0.1.0-alpha.45 → 0.1.0-alpha.47

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.45",
3
+ "version": "0.1.0-alpha.47",
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.45"
31
+ "@rezi-ui/testkit": "0.1.0-alpha.47"
32
32
  }
33
33
  }
@@ -1,4 +1,9 @@
1
- import { exit } from "node:process";
1
+ import {
2
+ type DebugController,
3
+ type DebugRecord,
4
+ categoriesToMask,
5
+ createDebugController,
6
+ } from "@rezi-ui/core";
2
7
  import { createNodeApp } from "@rezi-ui/node";
3
8
  import { debugSnapshot } from "./helpers/debug.js";
4
9
  import { resolveStarshipCommand } from "./helpers/keybindings.js";
@@ -14,10 +19,21 @@ import type { RouteDeps, RouteId, StarshipAction, StarshipState } from "./types.
14
19
  const UI_FPS_CAP = 30;
15
20
  const TICK_MS = 800;
16
21
  const TOAST_PRUNE_MS = 3000;
22
+ const DEBUG_TRACE_ENABLED = process.env.REZI_STARSHIP_DEBUG_TRACE === "1";
23
+ const EXECUTION_MODE: "inline" | "worker" =
24
+ process.env.REZI_STARSHIP_EXECUTION_MODE === "worker" ? "worker" : "inline";
25
+
26
+ function clampViewportAxis(value: number | undefined, fallback: number): number {
27
+ const safeFallback = Math.max(1, Math.trunc(fallback));
28
+ if (!Number.isFinite(value)) return safeFallback;
29
+ const raw = Math.trunc(value ?? safeFallback);
30
+ if (raw <= 0) return safeFallback;
31
+ return raw;
32
+ }
17
33
 
18
34
  const initialState = createInitialStateWithViewport(Date.now(), {
19
- cols: process.stdout.columns ?? 120,
20
- rows: process.stdout.rows ?? 40,
35
+ cols: clampViewportAxis(process.stdout.columns, 120),
36
+ rows: clampViewportAxis(process.stdout.rows, 40),
21
37
  });
22
38
  const enableHsr = process.argv.includes("--hsr") || process.env.REZI_HSR === "1";
23
39
  const hasInteractiveTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -41,6 +57,15 @@ let app!: ReturnType<typeof createNodeApp<StarshipState>>;
41
57
  let stopping = false;
42
58
  let tickTimer: ReturnType<typeof setInterval> | null = null;
43
59
  let toastTimer: ReturnType<typeof setInterval> | null = null;
60
+ let debugTraceTimer: ReturnType<typeof setInterval> | null = null;
61
+ let debugTraceInFlight = false;
62
+ let debugController: DebugController | null = null;
63
+ let debugLastRecordId = 0n;
64
+ let stopCode = 0;
65
+ let stopResolve: (() => void) | null = null;
66
+ const stopPromise = new Promise<void>((resolve) => {
67
+ stopResolve = resolve;
68
+ });
44
69
  let lastViewport = {
45
70
  cols: initialState.viewportCols,
46
71
  rows: initialState.viewportRows,
@@ -68,20 +93,22 @@ function dispatch(action: StarshipAction): void {
68
93
  }
69
94
 
70
95
  function syncViewport(cols: number, rows: number): void {
71
- if (cols === lastViewport.cols && rows === lastViewport.rows) return;
72
- lastViewport = { cols, rows };
96
+ const safeCols = clampViewportAxis(cols, lastViewport.cols);
97
+ const safeRows = clampViewportAxis(rows, lastViewport.rows);
98
+ if (safeCols === lastViewport.cols && safeRows === lastViewport.rows) return;
99
+ lastViewport = { cols: safeCols, rows: safeRows };
73
100
  debugSnapshot("runtime.viewport", {
74
- cols,
75
- rows,
101
+ cols: safeCols,
102
+ rows: safeRows,
76
103
  route: currentRouteId(),
77
104
  });
78
- dispatch({ type: "set-viewport", cols, rows });
105
+ dispatch({ type: "set-viewport", cols: safeCols, rows: safeRows });
79
106
  }
80
107
 
81
108
  function syncViewportFromStdout(): void {
82
109
  if (!process.stdout.isTTY) return;
83
- const cols = process.stdout.columns ?? lastViewport.cols;
84
- const rows = process.stdout.rows ?? lastViewport.rows;
110
+ const cols = clampViewportAxis(process.stdout.columns, lastViewport.cols);
111
+ const rows = clampViewportAxis(process.stdout.rows, lastViewport.rows);
85
112
  syncViewport(cols, rows);
86
113
  }
87
114
 
@@ -96,6 +123,17 @@ function currentRouteId(): RouteId {
96
123
  return "bridge";
97
124
  }
98
125
 
126
+ const frameAuditGlobal = globalThis as {
127
+ __reziFrameAuditContext?: () => Readonly<Record<string, unknown>>;
128
+ };
129
+ frameAuditGlobal.__reziFrameAuditContext = () =>
130
+ Object.freeze({
131
+ route: currentRouteId(),
132
+ viewportCols: lastViewport.cols,
133
+ viewportRows: lastViewport.rows,
134
+ executionMode: EXECUTION_MODE,
135
+ });
136
+
99
137
  function navigate(routeId: RouteId): void {
100
138
  const router = app.router;
101
139
  if (!router) return;
@@ -126,6 +164,7 @@ function buildRoutes(factory: CreateRoutesFn) {
126
164
  async function stopApp(code = 0): Promise<void> {
127
165
  if (stopping) return;
128
166
  stopping = true;
167
+ stopCode = code;
129
168
 
130
169
  if (tickTimer) {
131
170
  clearInterval(tickTimer);
@@ -137,18 +176,28 @@ async function stopApp(code = 0): Promise<void> {
137
176
  toastTimer = null;
138
177
  }
139
178
 
140
- try {
141
- await app.stop();
142
- } catch {
143
- // Ignore stop races.
179
+ if (debugTraceTimer) {
180
+ clearInterval(debugTraceTimer);
181
+ debugTraceTimer = null;
182
+ }
183
+
184
+ if (debugController) {
185
+ try {
186
+ await debugController.disable();
187
+ } catch {
188
+ // Ignore debug shutdown races.
189
+ }
190
+ debugController = null;
144
191
  }
145
192
 
146
193
  try {
147
- app.dispose();
194
+ await app.stop();
148
195
  } catch {
149
- // Ignore disposal races during shutdown.
196
+ // Ignore stop races.
150
197
  }
151
- exit(code);
198
+ frameAuditGlobal.__reziFrameAuditContext = undefined;
199
+ stopResolve?.();
200
+ stopResolve = null;
152
201
  }
153
202
 
154
203
  function applyCommand(command: ReturnType<typeof resolveStarshipCommand>): void {
@@ -464,13 +513,172 @@ function bindKeys(): void {
464
513
  app.keys(bindingMap);
465
514
  }
466
515
 
516
+ function snapshotDebugRecord(record: DebugRecord): void {
517
+ const { header } = record;
518
+ if (header.category === "frame") {
519
+ if (
520
+ record.payload &&
521
+ typeof record.payload === "object" &&
522
+ "drawlistBytes" in record.payload &&
523
+ "drawlistCmds" in record.payload
524
+ ) {
525
+ debugSnapshot("runtime.debug.frame", {
526
+ recordId: header.recordId.toString(),
527
+ frameId: header.frameId.toString(),
528
+ route: currentRouteId(),
529
+ drawlistBytes: record.payload.drawlistBytes,
530
+ drawlistCmds: record.payload.drawlistCmds,
531
+ diffBytesEmitted:
532
+ "diffBytesEmitted" in record.payload ? record.payload.diffBytesEmitted : null,
533
+ dirtyLines: "dirtyLines" in record.payload ? record.payload.dirtyLines : null,
534
+ dirtyCells: "dirtyCells" in record.payload ? record.payload.dirtyCells : null,
535
+ damageRects: "damageRects" in record.payload ? record.payload.damageRects : null,
536
+ usDrawlist: "usDrawlist" in record.payload ? record.payload.usDrawlist : null,
537
+ usDiff: "usDiff" in record.payload ? record.payload.usDiff : null,
538
+ usWrite: "usWrite" in record.payload ? record.payload.usWrite : null,
539
+ });
540
+ return;
541
+ }
542
+
543
+ debugSnapshot("runtime.debug.frame.raw", {
544
+ recordId: header.recordId.toString(),
545
+ frameId: header.frameId.toString(),
546
+ code: header.code,
547
+ severity: header.severity,
548
+ payloadSize: header.payloadSize,
549
+ route: currentRouteId(),
550
+ });
551
+ }
552
+
553
+ if (header.category === "drawlist") {
554
+ if (
555
+ record.payload &&
556
+ typeof record.payload === "object" &&
557
+ "totalBytes" in record.payload &&
558
+ "cmdCount" in record.payload
559
+ ) {
560
+ debugSnapshot("runtime.debug.drawlist", {
561
+ recordId: header.recordId.toString(),
562
+ frameId: header.frameId.toString(),
563
+ code: header.code,
564
+ severity: header.severity,
565
+ route: currentRouteId(),
566
+ totalBytes: record.payload.totalBytes,
567
+ cmdCount: record.payload.cmdCount,
568
+ validationResult:
569
+ "validationResult" in record.payload ? record.payload.validationResult : null,
570
+ executionResult:
571
+ "executionResult" in record.payload ? record.payload.executionResult : null,
572
+ clipStackMaxDepth:
573
+ "clipStackMaxDepth" in record.payload ? record.payload.clipStackMaxDepth : null,
574
+ textRuns: "textRuns" in record.payload ? record.payload.textRuns : null,
575
+ fillRects: "fillRects" in record.payload ? record.payload.fillRects : null,
576
+ });
577
+ return;
578
+ }
579
+
580
+ if (
581
+ record.payload &&
582
+ typeof record.payload === "object" &&
583
+ "kind" in record.payload &&
584
+ record.payload.kind === "drawlistBytes" &&
585
+ "bytes" in record.payload
586
+ ) {
587
+ debugSnapshot("runtime.debug.drawlistBytes", {
588
+ recordId: header.recordId.toString(),
589
+ frameId: header.frameId.toString(),
590
+ code: header.code,
591
+ severity: header.severity,
592
+ route: currentRouteId(),
593
+ bytes: record.payload.bytes.byteLength,
594
+ });
595
+ return;
596
+ }
597
+
598
+ debugSnapshot("runtime.debug.drawlist.raw", {
599
+ recordId: header.recordId.toString(),
600
+ frameId: header.frameId.toString(),
601
+ code: header.code,
602
+ severity: header.severity,
603
+ payloadSize: header.payloadSize,
604
+ route: currentRouteId(),
605
+ });
606
+ }
607
+ }
608
+
609
+ async function setupDebugTrace(): Promise<void> {
610
+ if (!DEBUG_TRACE_ENABLED) return;
611
+ try {
612
+ debugController = createDebugController({
613
+ backend: app.backend.debug,
614
+ terminalCapsProvider: () => app.backend.getCaps(),
615
+ maxFrames: 512,
616
+ });
617
+
618
+ await debugController.enable({
619
+ minSeverity: "trace",
620
+ categoryMask: categoriesToMask(["frame", "drawlist", "error"]),
621
+ captureRawEvents: false,
622
+ captureDrawlistBytes: false,
623
+ });
624
+ await debugController.reset();
625
+ } catch (error) {
626
+ debugSnapshot("runtime.debug.enable.error", {
627
+ message: error instanceof Error ? error.message : String(error),
628
+ route: currentRouteId(),
629
+ });
630
+ if (debugController) {
631
+ try {
632
+ await debugController.disable();
633
+ } catch {
634
+ // Ignore debug shutdown races.
635
+ }
636
+ debugController = null;
637
+ }
638
+ return;
639
+ }
640
+
641
+ debugLastRecordId = 0n;
642
+
643
+ debugSnapshot("runtime.debug.enable", {
644
+ route: currentRouteId(),
645
+ viewportCols: lastViewport.cols,
646
+ viewportRows: lastViewport.rows,
647
+ });
648
+
649
+ const pump = async () => {
650
+ if (!debugController || debugTraceInFlight) return;
651
+ debugTraceInFlight = true;
652
+ try {
653
+ const records = await debugController.query({ maxRecords: 256 });
654
+ if (records.length === 0) return;
655
+ for (const record of records) {
656
+ if (record.header.recordId <= debugLastRecordId) continue;
657
+ debugLastRecordId = record.header.recordId;
658
+ snapshotDebugRecord(record);
659
+ }
660
+ } catch (error) {
661
+ debugSnapshot("runtime.debug.query.error", {
662
+ message: error instanceof Error ? error.message : String(error),
663
+ route: currentRouteId(),
664
+ });
665
+ } finally {
666
+ debugTraceInFlight = false;
667
+ }
668
+ };
669
+
670
+ debugTraceTimer = setInterval(() => {
671
+ void pump();
672
+ }, 250);
673
+ }
674
+
467
675
  const routes = buildRoutes(createStarshipRoutes);
468
676
 
469
677
  debugSnapshot("runtime.app.create", {
470
678
  routeCount: routes.length,
471
679
  initialRoute: "bridge",
472
680
  fpsCap: UI_FPS_CAP,
473
- executionMode: "inline",
681
+ executionMode: EXECUTION_MODE,
474
682
  });
475
683
 
476
684
  app = createNodeApp({
@@ -479,7 +687,7 @@ app = createNodeApp({
479
687
  initialRoute: "bridge",
480
688
  config: {
481
689
  fpsCap: UI_FPS_CAP,
482
- executionMode: "inline",
690
+ executionMode: EXECUTION_MODE,
483
691
  },
484
692
  theme: themeSpec(initialState.themeName).theme,
485
693
  ...(enableHsr
@@ -545,5 +753,9 @@ debugSnapshot("runtime.app.start", {
545
753
  viewportRows: lastViewport.rows,
546
754
  });
547
755
 
756
+ await setupDebugTrace();
548
757
  await app.start();
549
- await new Promise<void>(() => {});
758
+ await stopPromise;
759
+ if (stopCode !== 0) {
760
+ process.exitCode = stopCode;
761
+ }
@@ -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%" }, [
557
+ body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
558
558
  BridgeCommandDeck({ key: "bridge-command-deck", state, dispatch: deps.dispatch }),
559
559
  ]),
560
560
  });
@@ -30,8 +30,12 @@ export function renderCargoScreen(
30
30
  title: "Cargo Hold",
31
31
  context,
32
32
  deps,
33
- body: ui.column({ gap: SPACE.sm, width: "100%" }, [
34
- CargoDeck({ state: context.state, dispatch: deps.dispatch }),
33
+ body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
34
+ CargoDeck({
35
+ key: "cargo-deck",
36
+ state: context.state,
37
+ dispatch: deps.dispatch,
38
+ }),
35
39
  ]),
36
40
  });
37
41
  }
@@ -462,8 +462,12 @@ export function renderCommsScreen(
462
462
  title: "Communications",
463
463
  context,
464
464
  deps,
465
- body: ui.column({ gap: SPACE.sm, width: "100%" }, [
466
- CommsDeck({ state: context.state, dispatch: deps.dispatch }),
465
+ body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
466
+ CommsDeck({
467
+ key: "comms-deck",
468
+ state: context.state,
469
+ dispatch: deps.dispatch,
470
+ }),
467
471
  ]),
468
472
  });
469
473
  }
@@ -7,6 +7,7 @@ import {
7
7
  type RouteRenderContext,
8
8
  type VNode,
9
9
  } from "@rezi-ui/core";
10
+ import { debugSnapshot } from "../helpers/debug.js";
10
11
  import { resolveLayout } from "../helpers/layout.js";
11
12
  import { crewCounts, departmentLabel, rankBadge, statusBadge } from "../helpers/formatters.js";
12
13
  import { selectedCrew, visibleCrew } from "../helpers/state.js";
@@ -193,7 +194,7 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
193
194
  ...tableSkin(tokens),
194
195
  });
195
196
 
196
- const detailPanel = ui.column({ gap: SPACE.sm }, [
197
+ const detailPanel = ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
197
198
  maybe(selected, (member) =>
198
199
  surfacePanel(
199
200
  tokens,
@@ -300,8 +301,18 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
300
301
  ),
301
302
  ]);
302
303
 
303
- const manifestBlock = ui.column({ gap: SPACE.sm }, [
304
- surfacePanel(tokens, "Crew Manifest", [table]),
304
+ const manifestBlock = ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
305
+ ui.box(
306
+ {
307
+ border: "none",
308
+ p: 0,
309
+ width: "100%",
310
+ flex: 1,
311
+ minHeight: 10,
312
+ overflow: "hidden",
313
+ },
314
+ [surfacePanel(tokens, "Crew Manifest", [table])],
315
+ ),
305
316
  ui.pagination({
306
317
  id: ctx.id("crew-pagination"),
307
318
  page,
@@ -313,126 +324,185 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
313
324
 
314
325
  const deckLayout = layout.wide
315
326
  ? showDetailPane
316
- ? ui.masterDetail({
317
- id: ctx.id("crew-master-detail"),
318
- masterWidth: layout.crewMasterWidth,
319
- master: manifestBlock,
320
- detail: detailPanel,
321
- })
322
- : manifestBlock
323
- : showDetailPane
324
- ? ui.column({ gap: SPACE.sm }, [manifestBlock, detailPanel])
325
- : manifestBlock;
326
-
327
- const operationsPanel = surfacePanel(
328
- tokens,
329
- "Crew Operations",
330
- [
331
- sectionHeader(tokens, "Manifest Controls", "Consistent staffing and assignment flow"),
332
- ui.row({ gap: SPACE.md, wrap: !layout.wide, items: "start" }, [
333
- ui.box(
327
+ ? ui.row(
334
328
  {
335
- border: "none",
336
- p: 0,
329
+ id: ctx.id("crew-master-detail"),
337
330
  gap: SPACE.sm,
338
- ...(layout.wide ? { flex: 2 } : {}),
331
+ width: "100%",
332
+ height: "100%",
333
+ items: "stretch",
334
+ wrap: false,
339
335
  },
340
336
  [
341
- ui.row({ gap: SPACE.sm, wrap: true }, [
342
- ui.badge(`Total ${counts.total}`, { variant: "info" }),
343
- ui.badge(`Active ${counts.active}`, { variant: "success" }),
344
- ui.badge(`Away ${counts.away}`, { variant: "warning" }),
345
- ui.badge(`Injured ${counts.injured}`, { variant: "error" }),
346
- ]),
347
- ui.form([
348
- ui.field({
349
- label: "Search Crew",
350
- hint: "Filter by name, rank, or department",
351
- children: ui.input({
352
- id: ctx.id("crew-search"),
353
- value: props.state.crewSearchQuery,
354
- placeholder: "Type to filter",
355
- onInput: (value) => props.dispatch({ type: "set-crew-search", query: value }),
356
- }),
357
- }),
358
- ]),
359
- ui.actions([
360
- ui.button({
361
- id: ctx.id("crew-new-assignment"),
362
- label: "New Assignment",
363
- intent: "primary",
364
- onPress: () => props.dispatch({ type: "toggle-crew-editor" }),
365
- }),
366
- ui.button({
367
- id: ctx.id("crew-edit-selected"),
368
- label: "Edit Selected",
369
- intent: "secondary",
370
- onPress: () => props.dispatch({ type: "toggle-crew-editor" }),
371
- }),
372
- ]),
337
+ ui.box(
338
+ {
339
+ border: "none",
340
+ p: 0,
341
+ width: layout.crewMasterWidth,
342
+ height: "100%",
343
+ overflow: "hidden",
344
+ },
345
+ [manifestBlock],
346
+ ),
347
+ ui.box(
348
+ {
349
+ border: "none",
350
+ p: 0,
351
+ flex: 1,
352
+ height: "100%",
353
+ overflow: "hidden",
354
+ },
355
+ [detailPanel],
356
+ ),
373
357
  ],
374
- ),
375
- ...(layout.wide
376
- ? [
377
- ui.box(
378
- {
379
- border: "none",
380
- p: 0,
381
- flex: 1,
382
- },
383
- [
384
- surfacePanel(
385
- tokens,
386
- "Crew Snapshot",
358
+ )
359
+ : manifestBlock
360
+ : showDetailPane
361
+ ? ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
362
+ ui.box(
363
+ { border: "none", p: 0, width: "100%", flex: 1, minHeight: 10, overflow: "hidden" },
364
+ [manifestBlock],
365
+ ),
366
+ ui.box(
367
+ { border: "none", p: 0, width: "100%", flex: 1, minHeight: 10, overflow: "hidden" },
368
+ [detailPanel],
369
+ ),
370
+ ])
371
+ : manifestBlock;
372
+
373
+ const operationsPanelMaxHeight = Math.max(12, Math.min(22, Math.floor(layout.height * 0.34)));
374
+ debugSnapshot("crew.render", {
375
+ viewportCols: props.state.viewportCols,
376
+ viewportRows: props.state.viewportRows,
377
+ visibleCount: visible.length,
378
+ sortedCount: sorted.length,
379
+ page,
380
+ totalPages,
381
+ pageDataCount: pageData.length,
382
+ showDetailPane,
383
+ operationsPanelMaxHeight,
384
+ editingCrew: props.state.editingCrew,
385
+ });
386
+ const operationsPanel = ui.box(
387
+ {
388
+ border: "none",
389
+ p: 0,
390
+ width: "100%",
391
+ height: operationsPanelMaxHeight,
392
+ overflow: "scroll",
393
+ },
394
+ [
395
+ surfacePanel(
396
+ tokens,
397
+ "Crew Operations",
398
+ [
399
+ sectionHeader(tokens, "Manifest Controls", "Consistent staffing and assignment flow"),
400
+ ui.row({ gap: SPACE.md, wrap: !layout.wide, items: "start" }, [
401
+ ui.box(
402
+ {
403
+ border: "none",
404
+ p: 0,
405
+ gap: SPACE.sm,
406
+ ...(layout.wide ? { flex: 2 } : {}),
407
+ },
408
+ [
409
+ ui.row({ gap: SPACE.sm, wrap: true }, [
410
+ ui.badge(`Total ${counts.total}`, { variant: "info" }),
411
+ ui.badge(`Active ${counts.active}`, { variant: "success" }),
412
+ ui.badge(`Away ${counts.away}`, { variant: "warning" }),
413
+ ui.badge(`Injured ${counts.injured}`, { variant: "error" }),
414
+ ]),
415
+ ui.form([
416
+ ui.field({
417
+ label: "Search Crew",
418
+ hint: "Filter by name, rank, or department",
419
+ children: ui.input({
420
+ id: ctx.id("crew-search"),
421
+ value: props.state.crewSearchQuery,
422
+ placeholder: "Type to filter",
423
+ onInput: (value) => props.dispatch({ type: "set-crew-search", query: value }),
424
+ }),
425
+ }),
426
+ ]),
427
+ ui.actions([
428
+ ui.button({
429
+ id: ctx.id("crew-new-assignment"),
430
+ label: "New Assignment",
431
+ intent: "primary",
432
+ onPress: () => props.dispatch({ type: "toggle-crew-editor" }),
433
+ }),
434
+ ui.button({
435
+ id: ctx.id("crew-edit-selected"),
436
+ label: "Edit Selected",
437
+ intent: "secondary",
438
+ onPress: () => props.dispatch({ type: "toggle-crew-editor" }),
439
+ }),
440
+ ]),
441
+ ],
442
+ ),
443
+ ...(layout.wide
444
+ ? [
445
+ ui.box(
446
+ {
447
+ border: "none",
448
+ p: 0,
449
+ flex: 1,
450
+ },
387
451
  [
388
- selected
389
- ? ui.column({ gap: SPACE.xs }, [
390
- ui.text(selected.name, { variant: "label" }),
391
- ui.row({ gap: SPACE.xs, wrap: true }, [
392
- ui.badge(rankBadge(selected.rank).text, {
393
- variant: rankBadge(selected.rank).variant,
452
+ surfacePanel(
453
+ tokens,
454
+ "Crew Snapshot",
455
+ [
456
+ selected
457
+ ? ui.column({ gap: SPACE.xs }, [
458
+ ui.text(selected.name, { variant: "label" }),
459
+ ui.row({ gap: SPACE.xs, wrap: true }, [
460
+ ui.badge(rankBadge(selected.rank).text, {
461
+ variant: rankBadge(selected.rank).variant,
462
+ }),
463
+ ui.badge(statusBadge(selected.status).text, {
464
+ variant: statusBadge(selected.status).variant,
465
+ }),
466
+ ui.tag(departmentLabel(selected.department), { variant: "info" }),
467
+ ]),
468
+ ])
469
+ : ui.text("No crew selected", {
470
+ variant: "caption",
471
+ style: { fg: tokens.text.muted, dim: true },
394
472
  }),
395
- ui.badge(statusBadge(selected.status).text, {
396
- variant: statusBadge(selected.status).variant,
473
+ ui.divider(),
474
+ ui.row({ gap: SPACE.xs, wrap: true }, [
475
+ ui.badge(`Visible ${sorted.length}`, { variant: "info" }),
476
+ ui.badge(`Page ${page}/${totalPages}`, { variant: "default" }),
477
+ ]),
478
+ staffingError
479
+ ? ui.callout("Critical staffing below minimum.", {
480
+ title: "Guardrail",
481
+ variant: "warning",
482
+ })
483
+ : ui.callout("Critical staffing thresholds healthy.", {
484
+ title: "Guardrail",
485
+ variant: "success",
397
486
  }),
398
- ui.tag(departmentLabel(selected.department), { variant: "info" }),
399
- ]),
400
- ])
401
- : ui.text("No crew selected", {
402
- variant: "caption",
403
- style: { fg: tokens.text.muted, dim: true },
404
- }),
405
- ui.divider(),
406
- ui.row({ gap: SPACE.xs, wrap: true }, [
407
- ui.badge(`Visible ${sorted.length}`, { variant: "info" }),
408
- ui.badge(`Page ${page}/${totalPages}`, { variant: "default" }),
409
- ]),
410
- staffingError
411
- ? ui.callout("Critical staffing below minimum.", {
412
- title: "Guardrail",
413
- variant: "warning",
414
- })
415
- : ui.callout("Critical staffing thresholds healthy.", {
416
- title: "Guardrail",
417
- variant: "success",
418
- }),
487
+ ],
488
+ {
489
+ tone: "inset",
490
+ p: SPACE.sm,
491
+ gap: SPACE.sm,
492
+ },
493
+ ),
419
494
  ],
420
- {
421
- tone: "inset",
422
- p: SPACE.sm,
423
- gap: SPACE.sm,
424
- },
425
495
  ),
426
- ],
427
- ),
428
- ]
429
- : []),
430
- ]),
496
+ ]
497
+ : []),
498
+ ]),
499
+ ],
500
+ { tone: "base" },
501
+ ),
431
502
  ],
432
- { tone: "base" },
433
503
  );
434
504
 
435
- return ui.column({ gap: SPACE.md, width: "100%" }, [
505
+ return ui.column({ gap: SPACE.md, width: "100%", height: "100%" }, [
436
506
  operationsPanel,
437
507
  show(
438
508
  asyncCrew.loading,
@@ -443,7 +513,20 @@ const CrewDeck = defineWidget<CrewDeckProps>((props, ctx): VNode => {
443
513
  ui.skeleton(44, { variant: "text" }),
444
514
  ], { tone: "inset" }),
445
515
  ),
446
- show(!asyncCrew.loading, deckLayout),
516
+ show(
517
+ !asyncCrew.loading,
518
+ ui.box(
519
+ {
520
+ border: "none",
521
+ p: 0,
522
+ width: "100%",
523
+ flex: 1,
524
+ minHeight: 12,
525
+ overflow: "hidden",
526
+ },
527
+ [deckLayout],
528
+ ),
529
+ ),
447
530
  ]);
448
531
  });
449
532
 
@@ -452,8 +535,12 @@ export function renderCrewScreen(context: RouteRenderContext<StarshipState>, dep
452
535
  title: "Crew Manifest",
453
536
  context,
454
537
  deps,
455
- body: ui.column({ gap: SPACE.sm, width: "100%" }, [
456
- CrewDeck({ state: context.state, dispatch: deps.dispatch }),
538
+ body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
539
+ CrewDeck({
540
+ key: "crew-deck",
541
+ state: context.state,
542
+ dispatch: deps.dispatch,
543
+ }),
457
544
  ]),
458
545
  });
459
546
  }
@@ -371,16 +371,27 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
371
371
  }),
372
372
  ]);
373
373
 
374
- const leftPane = ui.column({ gap: SPACE.sm, width: "100%" }, [
375
- reactorPanel,
376
- ...(showSecondaryPanels ? [treePanel] : []),
377
- ]);
378
- const rightPane = ui.column({ gap: SPACE.sm, width: "100%" }, [
379
- powerPanel,
380
- ...(showSecondaryPanels ? [thermalPanel, diagnosticsPanel] : []),
381
- ]);
374
+ 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" }, [
378
+ treePanel,
379
+ ]),
380
+ ])
381
+ : ui.column({ gap: SPACE.sm, width: "100%" }, [reactorPanel]);
382
+ 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" }, [
385
+ powerPanel,
386
+ ]),
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" }, [
389
+ diagnosticsPanel,
390
+ ]),
391
+ ])
392
+ : ui.column({ gap: SPACE.sm, width: "100%" }, [powerPanel]);
382
393
 
383
- const responsiveDeckHeight = Math.max(
394
+ const responsiveDeckMinHeight = Math.max(
384
395
  16,
385
396
  contentRows - (showControlsSummary ? 12 : 10) - (showSecondaryPanels ? 0 : 2),
386
397
  );
@@ -395,7 +406,8 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
395
406
  border: "none",
396
407
  p: 0,
397
408
  width: "100%",
398
- height: responsiveDeckHeight,
409
+ flex: 1,
410
+ minHeight: responsiveDeckMinHeight,
399
411
  overflow: "scroll",
400
412
  },
401
413
  [responsiveDeckBody],
@@ -491,7 +503,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
491
503
  includeResponsiveDeck: renderMode === "full",
492
504
  responsiveDeckMode: useWideRow ? "row" : "column",
493
505
  forceStackViaEnv,
494
- responsiveDeckHeight,
506
+ responsiveDeckMinHeight,
495
507
  });
496
508
 
497
509
  if (veryCompactHeight) {
@@ -502,7 +514,7 @@ const EngineeringDeck = defineWidget<EngineeringDeckProps>((props, ctx): VNode =
502
514
  return ui.column({ gap: SPACE.sm, width: "100%" }, [controlsPanel, reactorPanel]);
503
515
  }
504
516
 
505
- return ui.column({ gap: SPACE.sm, width: "100%" }, [
517
+ return ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
506
518
  controlsRegion,
507
519
  responsiveDeck,
508
520
  ]);
@@ -516,8 +528,12 @@ export function renderEngineeringScreen(
516
528
  title: "Engineering Deck",
517
529
  context,
518
530
  deps,
519
- body: ui.column({ gap: SPACE.sm, width: "100%" }, [
520
- EngineeringDeck({ state: context.state, dispatch: deps.dispatch }),
531
+ body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
532
+ EngineeringDeck({
533
+ key: "engineering-deck",
534
+ state: context.state,
535
+ dispatch: deps.dispatch,
536
+ }),
521
537
  ]),
522
538
  });
523
539
  }
@@ -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 }, [
212
+ body: ui.column({ gap: SPACE.xs, width: "100%", height: "100%" }, [
213
213
  ui.breadcrumb({
214
214
  items: [{ label: "Bridge" }, { label: "Settings" }, { label: "Theme Preview" }],
215
215
  }),
@@ -277,8 +277,9 @@ export function renderSettingsScreen(
277
277
  title: "Ship Settings",
278
278
  context,
279
279
  deps,
280
- body: ui.column({ gap: SPACE.sm, width: "100%" }, [
280
+ body: ui.column({ gap: SPACE.sm, width: "100%", height: "100%" }, [
281
281
  SettingsDeck({
282
+ key: "settings-deck",
282
283
  state: context.state,
283
284
  dispatch: deps.dispatch,
284
285
  }),
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  type BadgeVariant,
3
- type Rgb,
3
+ type Rgb24,
4
4
  type TextStyle,
5
5
  type ThemeDefinition,
6
- darkTheme,
7
6
  draculaTheme,
8
7
  extendTheme,
9
8
  nordTheme,
9
+ rgb,
10
10
  } from "@rezi-ui/core";
11
11
  import type { AlertLevel, ThemeName } from "./types.js";
12
12
 
@@ -42,52 +42,52 @@ const DAY_SHIFT_THEME = extendTheme(nordTheme, {
42
42
  focusIndicator: {
43
43
  bold: true,
44
44
  underline: false,
45
- focusRingColor: { r: 118, g: 208, b: 255 },
45
+ focusRingColor: rgb(118, 208, 255),
46
46
  },
47
47
  colors: {
48
48
  bg: {
49
- base: { r: 29, g: 42, b: 58 },
50
- elevated: { r: 40, g: 57, b: 76 },
51
- overlay: { r: 52, g: 72, b: 94 },
52
- subtle: { r: 34, g: 49, b: 67 },
49
+ base: rgb(29, 42, 58),
50
+ elevated: rgb(40, 57, 76),
51
+ overlay: rgb(52, 72, 94),
52
+ subtle: rgb(34, 49, 67),
53
53
  },
54
54
  fg: {
55
- primary: { r: 236, g: 243, b: 255 },
56
- secondary: { r: 190, g: 217, b: 242 },
57
- muted: { r: 108, g: 138, b: 168 },
58
- inverse: { r: 20, g: 30, b: 42 },
55
+ primary: rgb(236, 243, 255),
56
+ secondary: rgb(190, 217, 242),
57
+ muted: rgb(108, 138, 168),
58
+ inverse: rgb(20, 30, 42),
59
59
  },
60
60
  border: {
61
- subtle: { r: 74, g: 98, b: 126 },
62
- default: { r: 104, g: 136, b: 168 },
63
- strong: { r: 137, g: 171, b: 206 },
61
+ subtle: rgb(74, 98, 126),
62
+ default: rgb(104, 136, 168),
63
+ strong: rgb(137, 171, 206),
64
64
  },
65
65
  accent: {
66
- primary: { r: 106, g: 195, b: 255 },
67
- secondary: { r: 129, g: 217, b: 255 },
68
- tertiary: { r: 180, g: 231, b: 164 },
66
+ primary: rgb(106, 195, 255),
67
+ secondary: rgb(129, 217, 255),
68
+ tertiary: rgb(180, 231, 164),
69
69
  },
70
- info: { r: 118, g: 208, b: 255 },
71
- success: { r: 166, g: 228, b: 149 },
72
- warning: { r: 255, g: 211, b: 131 },
73
- error: { r: 239, g: 118, b: 132 },
70
+ info: rgb(118, 208, 255),
71
+ success: rgb(166, 228, 149),
72
+ warning: rgb(255, 211, 131),
73
+ error: rgb(239, 118, 132),
74
74
  selected: {
75
- bg: { r: 68, g: 105, b: 139 },
76
- fg: { r: 236, g: 243, b: 255 },
75
+ bg: rgb(68, 105, 139),
76
+ fg: rgb(236, 243, 255),
77
77
  },
78
78
  disabled: {
79
- fg: { r: 95, g: 121, b: 149 },
80
- bg: { r: 39, g: 55, b: 72 },
79
+ fg: rgb(95, 121, 149),
80
+ bg: rgb(39, 55, 72),
81
81
  },
82
82
  diagnostic: {
83
- error: { r: 239, g: 118, b: 132 },
84
- warning: { r: 255, g: 211, b: 131 },
85
- info: { r: 118, g: 208, b: 255 },
86
- hint: { r: 149, g: 187, b: 228 },
83
+ error: rgb(239, 118, 132),
84
+ warning: rgb(255, 211, 131),
85
+ info: rgb(118, 208, 255),
86
+ hint: rgb(149, 187, 228),
87
87
  },
88
88
  focus: {
89
- ring: { r: 118, g: 208, b: 255 },
90
- bg: { r: 63, g: 96, b: 126 },
89
+ ring: rgb(118, 208, 255),
90
+ bg: rgb(63, 96, 126),
91
91
  },
92
92
  },
93
93
  });
@@ -105,52 +105,52 @@ const NIGHT_SHIFT_THEME = extendTheme(draculaTheme, {
105
105
  focusIndicator: {
106
106
  bold: true,
107
107
  underline: false,
108
- focusRingColor: { r: 176, g: 133, b: 255 },
108
+ focusRingColor: rgb(176, 133, 255),
109
109
  },
110
110
  colors: {
111
111
  bg: {
112
- base: { r: 19, g: 22, b: 33 },
113
- elevated: { r: 28, g: 31, b: 46 },
114
- overlay: { r: 37, g: 41, b: 60 },
115
- subtle: { r: 24, g: 27, b: 40 },
112
+ base: rgb(19, 22, 33),
113
+ elevated: rgb(28, 31, 46),
114
+ overlay: rgb(37, 41, 60),
115
+ subtle: rgb(24, 27, 40),
116
116
  },
117
117
  fg: {
118
- primary: { r: 244, g: 246, b: 252 },
119
- secondary: { r: 202, g: 185, b: 252 },
120
- muted: { r: 131, g: 146, b: 186 },
121
- inverse: { r: 19, g: 22, b: 33 },
118
+ primary: rgb(244, 246, 252),
119
+ secondary: rgb(202, 185, 252),
120
+ muted: rgb(131, 146, 186),
121
+ inverse: rgb(19, 22, 33),
122
122
  },
123
123
  accent: {
124
- primary: { r: 176, g: 133, b: 255 },
125
- secondary: { r: 129, g: 235, b: 255 },
126
- tertiary: { r: 119, g: 255, b: 196 },
124
+ primary: rgb(176, 133, 255),
125
+ secondary: rgb(129, 235, 255),
126
+ tertiary: rgb(119, 255, 196),
127
127
  },
128
- info: { r: 129, g: 235, b: 255 },
129
- success: { r: 110, g: 249, b: 174 },
130
- warning: { r: 255, g: 207, b: 124 },
131
- error: { r: 255, g: 118, b: 132 },
128
+ info: rgb(129, 235, 255),
129
+ success: rgb(110, 249, 174),
130
+ warning: rgb(255, 207, 124),
131
+ error: rgb(255, 118, 132),
132
132
  selected: {
133
- bg: { r: 68, g: 76, b: 112 },
134
- fg: { r: 244, g: 246, b: 252 },
133
+ bg: rgb(68, 76, 112),
134
+ fg: rgb(244, 246, 252),
135
135
  },
136
136
  disabled: {
137
- fg: { r: 99, g: 111, b: 146 },
138
- bg: { r: 28, g: 31, b: 46 },
137
+ fg: rgb(99, 111, 146),
138
+ bg: rgb(28, 31, 46),
139
139
  },
140
140
  diagnostic: {
141
- error: { r: 255, g: 118, b: 132 },
142
- warning: { r: 255, g: 207, b: 124 },
143
- info: { r: 129, g: 235, b: 255 },
144
- hint: { r: 206, g: 158, b: 255 },
141
+ error: rgb(255, 118, 132),
142
+ warning: rgb(255, 207, 124),
143
+ info: rgb(129, 235, 255),
144
+ hint: rgb(206, 158, 255),
145
145
  },
146
146
  focus: {
147
- ring: { r: 176, g: 133, b: 255 },
148
- bg: { r: 64, g: 57, b: 96 },
147
+ ring: rgb(176, 133, 255),
148
+ bg: rgb(64, 57, 96),
149
149
  },
150
150
  border: {
151
- subtle: { r: 43, g: 49, b: 71 },
152
- default: { r: 72, g: 81, b: 116 },
153
- strong: { r: 104, g: 115, b: 156 },
151
+ subtle: rgb(43, 49, 71),
152
+ default: rgb(72, 81, 116),
153
+ strong: rgb(104, 115, 156),
154
154
  },
155
155
  },
156
156
  });
@@ -168,52 +168,52 @@ const RED_ALERT_THEME = extendTheme(draculaTheme, {
168
168
  focusIndicator: {
169
169
  bold: true,
170
170
  underline: false,
171
- focusRingColor: { r: 255, g: 112, b: 112 },
171
+ focusRingColor: rgb(255, 112, 112),
172
172
  },
173
173
  colors: {
174
174
  bg: {
175
- base: { r: 24, g: 12, b: 19 },
176
- elevated: { r: 34, g: 15, b: 24 },
177
- overlay: { r: 46, g: 21, b: 32 },
178
- subtle: { r: 29, g: 13, b: 22 },
175
+ base: rgb(24, 12, 19),
176
+ elevated: rgb(34, 15, 24),
177
+ overlay: rgb(46, 21, 32),
178
+ subtle: rgb(29, 13, 22),
179
179
  },
180
180
  fg: {
181
- primary: { r: 255, g: 238, b: 242 },
182
- secondary: { r: 244, g: 190, b: 205 },
183
- muted: { r: 170, g: 122, b: 139 },
184
- inverse: { r: 24, g: 12, b: 19 },
181
+ primary: rgb(255, 238, 242),
182
+ secondary: rgb(244, 190, 205),
183
+ muted: rgb(170, 122, 139),
184
+ inverse: rgb(24, 12, 19),
185
185
  },
186
186
  accent: {
187
- primary: { r: 255, g: 114, b: 144 },
188
- secondary: { r: 255, g: 182, b: 120 },
189
- tertiary: { r: 255, g: 220, b: 146 },
187
+ primary: rgb(255, 114, 144),
188
+ secondary: rgb(255, 182, 120),
189
+ tertiary: rgb(255, 220, 146),
190
190
  },
191
- success: { r: 134, g: 247, b: 176 },
192
- warning: { r: 255, g: 181, b: 112 },
193
- error: { r: 255, g: 93, b: 117 },
194
- info: { r: 255, g: 141, b: 153 },
191
+ success: rgb(134, 247, 176),
192
+ warning: rgb(255, 181, 112),
193
+ error: rgb(255, 93, 117),
194
+ info: rgb(255, 141, 153),
195
195
  selected: {
196
- bg: { r: 82, g: 34, b: 52 },
197
- fg: { r: 255, g: 238, b: 242 },
196
+ bg: rgb(82, 34, 52),
197
+ fg: rgb(255, 238, 242),
198
198
  },
199
199
  disabled: {
200
- fg: { r: 142, g: 96, b: 112 },
201
- bg: { r: 34, g: 15, b: 24 },
200
+ fg: rgb(142, 96, 112),
201
+ bg: rgb(34, 15, 24),
202
202
  },
203
203
  diagnostic: {
204
- error: { r: 255, g: 93, b: 117 },
205
- warning: { r: 255, g: 181, b: 112 },
206
- info: { r: 255, g: 141, b: 153 },
207
- hint: { r: 255, g: 203, b: 133 },
204
+ error: rgb(255, 93, 117),
205
+ warning: rgb(255, 181, 112),
206
+ info: rgb(255, 141, 153),
207
+ hint: rgb(255, 203, 133),
208
208
  },
209
209
  focus: {
210
- ring: { r: 255, g: 112, b: 112 },
211
- bg: { r: 76, g: 35, b: 50 },
210
+ ring: rgb(255, 112, 112),
211
+ bg: rgb(76, 35, 50),
212
212
  },
213
213
  border: {
214
- subtle: { r: 86, g: 45, b: 61 },
215
- default: { r: 124, g: 65, b: 86 },
216
- strong: { r: 172, g: 86, b: 112 },
214
+ subtle: rgb(86, 45, 61),
215
+ default: rgb(124, 65, 86),
216
+ strong: rgb(172, 86, 112),
217
217
  },
218
218
  },
219
219
  });
@@ -244,18 +244,38 @@ function clampChannel(value: number): number {
244
244
  return Math.max(0, Math.min(255, Math.round(value)));
245
245
  }
246
246
 
247
- function blend(a: Rgb, b: Rgb, weight: number): Rgb {
247
+ type ColorInput = Rgb24;
248
+
249
+ function packRgb(value: Rgb24): Rgb24 {
250
+ return (Math.round(value) >>> 0) & 0x00ff_ffff;
251
+ }
252
+
253
+ function rgbChannel(value: Rgb24, shift: 0 | 8 | 16): number {
254
+ return (value >>> shift) & 0xff;
255
+ }
256
+
257
+ function unpackRgb(value: ColorInput): Readonly<{ r: number; g: number; b: number }> {
258
+ return Object.freeze({
259
+ r: rgbChannel(value, 16),
260
+ g: rgbChannel(value, 8),
261
+ b: rgbChannel(value, 0),
262
+ });
263
+ }
264
+
265
+ function blend(a: ColorInput, b: ColorInput, weight: number): Rgb24 {
248
266
  const safe = Math.max(0, Math.min(1, weight));
249
- return {
250
- r: clampChannel(a.r + (b.r - a.r) * safe),
251
- g: clampChannel(a.g + (b.g - a.g) * safe),
252
- b: clampChannel(a.b + (b.b - a.b) * safe),
253
- };
267
+ const left = unpackRgb(a);
268
+ const right = unpackRgb(b);
269
+ return (
270
+ ((clampChannel(left.r + (right.r - left.r) * safe) & 0xff) << 16) |
271
+ ((clampChannel(left.g + (right.g - left.g) * safe) & 0xff) << 8) |
272
+ (clampChannel(left.b + (right.b - left.b) * safe) & 0xff)
273
+ );
254
274
  }
255
275
 
256
- export function toHex(color: Rgb): string {
276
+ export function toHex(color: Rgb24): string {
257
277
  const channel = (value: number) => clampChannel(value).toString(16).padStart(2, "0");
258
- return `#${channel(color.r)}${channel(color.g)}${channel(color.b)}`;
278
+ return `#${channel(rgbChannel(color, 16))}${channel(rgbChannel(color, 8))}${channel(rgbChannel(color, 0))}`;
259
279
  }
260
280
 
261
281
  export function cycleThemeName(current: ThemeName): ThemeName {
@@ -285,52 +305,52 @@ export type StarshipStyles = Readonly<{
285
305
 
286
306
  export type StarshipThemeTokens = Readonly<{
287
307
  bg: Readonly<{
288
- app: Rgb;
308
+ app: Rgb24;
289
309
  panel: Readonly<{
290
- base: Rgb;
291
- inset: Rgb;
292
- elevated: Rgb;
310
+ base: Rgb24;
311
+ inset: Rgb24;
312
+ elevated: Rgb24;
293
313
  }>;
294
- modal: Rgb;
314
+ modal: Rgb24;
295
315
  }>;
296
316
  border: Readonly<{
297
- default: Rgb;
298
- muted: Rgb;
299
- focus: Rgb;
300
- danger: Rgb;
317
+ default: Rgb24;
318
+ muted: Rgb24;
319
+ focus: Rgb24;
320
+ danger: Rgb24;
301
321
  }>;
302
322
  text: Readonly<{
303
- primary: Rgb;
304
- muted: Rgb;
305
- dim: Rgb;
323
+ primary: Rgb24;
324
+ muted: Rgb24;
325
+ dim: Rgb24;
306
326
  }>;
307
327
  accent: Readonly<{
308
- info: Rgb;
309
- success: Rgb;
310
- warn: Rgb;
311
- danger: Rgb;
312
- brand: Rgb;
328
+ info: Rgb24;
329
+ success: Rgb24;
330
+ warn: Rgb24;
331
+ danger: Rgb24;
332
+ brand: Rgb24;
313
333
  }>;
314
334
  state: Readonly<{
315
- selectedBg: Rgb;
316
- selectedText: Rgb;
317
- hoverBg: Rgb;
318
- focusRing: Rgb;
335
+ selectedBg: Rgb24;
336
+ selectedText: Rgb24;
337
+ hoverBg: Rgb24;
338
+ focusRing: Rgb24;
319
339
  }>;
320
340
  progress: Readonly<{
321
- track: Rgb;
322
- fill: Rgb;
341
+ track: Rgb24;
342
+ fill: Rgb24;
323
343
  }>;
324
344
  table: Readonly<{
325
- headerBg: Rgb;
326
- rowAltBg: Rgb;
327
- rowHoverBg: Rgb;
328
- rowSelectedBg: Rgb;
345
+ headerBg: Rgb24;
346
+ rowAltBg: Rgb24;
347
+ rowHoverBg: Rgb24;
348
+ rowSelectedBg: Rgb24;
329
349
  }>;
330
350
  log: Readonly<{
331
- info: Rgb;
332
- warn: Rgb;
333
- error: Rgb;
351
+ info: Rgb24;
352
+ warn: Rgb24;
353
+ error: Rgb24;
334
354
  }>;
335
355
  }>;
336
356
 
@@ -384,37 +404,37 @@ export function themeTokens(themeName: ThemeName): StarshipThemeTokens {
384
404
  : blend(colors.accent.primary, colors.accent.secondary, mode === "day" ? 0.18 : 0.1);
385
405
  return Object.freeze({
386
406
  bg: Object.freeze({
387
- app: colors.bg.base,
407
+ app: packRgb(colors.bg.base),
388
408
  panel: Object.freeze({
389
409
  base: panelBase,
390
410
  inset: panelInset,
391
411
  elevated: panelElevated,
392
412
  }),
393
- modal: colors.bg.overlay,
413
+ modal: packRgb(colors.bg.overlay),
394
414
  }),
395
415
  border: Object.freeze({
396
- default: colors.border.default,
397
- muted: colors.border.subtle,
398
- focus: mode === "alert" ? colors.error : colors.focus.ring,
399
- danger: colors.error,
416
+ default: packRgb(colors.border.default),
417
+ muted: packRgb(colors.border.subtle),
418
+ focus: mode === "alert" ? packRgb(colors.error) : packRgb(colors.focus.ring),
419
+ danger: packRgb(colors.error),
400
420
  }),
401
421
  text: Object.freeze({
402
- primary: colors.fg.primary,
403
- muted: colors.fg.secondary,
404
- dim: colors.fg.muted,
422
+ primary: packRgb(colors.fg.primary),
423
+ muted: packRgb(colors.fg.secondary),
424
+ dim: packRgb(colors.fg.muted),
405
425
  }),
406
426
  accent: Object.freeze({
407
- info: colors.info,
408
- success: colors.success,
409
- warn: colors.warning,
410
- danger: colors.error,
411
- brand: colors.accent.primary,
427
+ info: packRgb(colors.info),
428
+ success: packRgb(colors.success),
429
+ warn: packRgb(colors.warning),
430
+ danger: packRgb(colors.error),
431
+ brand: packRgb(colors.accent.primary),
412
432
  }),
413
433
  state: Object.freeze({
414
434
  selectedBg,
415
- selectedText: colors.selected.fg,
435
+ selectedText: packRgb(colors.selected.fg),
416
436
  hoverBg,
417
- focusRing: mode === "alert" ? colors.error : colors.focus.ring,
437
+ focusRing: mode === "alert" ? packRgb(colors.error) : packRgb(colors.focus.ring),
418
438
  }),
419
439
  progress: Object.freeze({
420
440
  track: blend(