Skip to content

Commit 6df3292

Browse files
author
John Donmoyer
committed
style: polish sandbox dashboard with colors, icons, and visual hierarchy
Add cyan/yellow header, box-drawing separator, status dot indicators (● running / ○ stopped), scroll arrows (▲/▼), bold shortcut keys, styled org picker, and ✓/✗ status message icons.
1 parent c5c475c commit 6df3292

1 file changed

Lines changed: 44 additions & 21 deletions

File tree

sandbox/dashboard.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { Command } from "@cliffy/command";
2-
import { bold, dim, green, red, stripAnsiCode } from "@std/fmt/colors";
2+
import {
3+
bold,
4+
cyan,
5+
dim,
6+
green,
7+
red,
8+
stripAnsiCode,
9+
yellow,
10+
} from "@std/fmt/colors";
311
import { Sandbox } from "@deno/sandbox";
412

513
import { formatDuration, renderTemporalTimestamp } from "../util.ts";
@@ -108,8 +116,8 @@ function renderScreen(state: DashboardState): string {
108116
const displayList = sortSandboxes(filtered, state.sortBy, state.sortAsc);
109117

110118
// Header
111-
const title = bold(" Sandbox Dashboard");
112-
const orgLabel = dim(`Org: ${state.org}`);
119+
const title = bold(cyan(" Sandbox Dashboard"));
120+
const orgLabel = yellow(`Org: ${state.org}`);
113121
const headerPadding = columns - stripAnsiCode(title).length -
114122
stripAnsiCode(orgLabel).length;
115123
lines.push(title + " ".repeat(Math.max(1, headerPadding)) + orgLabel);
@@ -138,7 +146,7 @@ function renderScreen(state: DashboardState): string {
138146
stripAnsiCode(timeStr).length;
139147
lines.push(summary + " ".repeat(Math.max(1, summaryPadding)) + timeStr);
140148

141-
lines.push("");
149+
lines.push(dim("─".repeat(columns)));
142150

143151
// How many rows are available for list items (sandboxes or orgs)?
144152
// We subtract: header (3 lines above), table/org header (1 line), footer (2 lines).
@@ -147,20 +155,21 @@ function renderScreen(state: DashboardState): string {
147155

148156
if (state.mode === "org") {
149157
// Org picker — replaces the sandbox table when choosing an org
158+
lines.push(bold(cyan(" Select Organization")));
150159
lines.push(dim(" " + "NAME".padEnd(30) + " " + "SLUG"));
151160

152161
const { start, end } = getVisibleRange(
153162
state.orgs.length,
154163
state.orgSelectedIndex,
155-
maxVisibleRows,
164+
maxVisibleRows - 1,
156165
);
157166

158167
for (let i = start; i < end; i++) {
159168
const org = state.orgs[i];
160169
const isHighlighted = i === state.orgSelectedIndex;
161170
const isActive = org.slug === state.org;
162171
const marker = isHighlighted ? ">" : " ";
163-
const activeMarker = isActive ? " *" : "";
172+
const activeMarker = isActive ? " " + green("●") : "";
164173

165174
const row = ` ${marker} ${org.name.padEnd(30)} ${
166175
dim(org.slug)
@@ -174,9 +183,11 @@ function renderScreen(state: DashboardState): string {
174183
}
175184

176185
if (state.orgs.length > maxVisibleRows) {
177-
lines.push(
178-
dim(` [${start + 1}${end} of ${state.orgs.length}]`),
179-
);
186+
const parts: string[] = [];
187+
if (start > 0) parts.push(dim("▲ more above"));
188+
parts.push(yellow(`[${start + 1}${end} of ${state.orgs.length}]`));
189+
if (end < state.orgs.length) parts.push(dim("▼ more below"));
190+
lines.push(" " + parts.join(" "));
180191
}
181192
} else {
182193
// Normal sandbox table
@@ -233,8 +244,8 @@ function renderScreen(state: DashboardState): string {
233244
const marker = isSelected ? ">" : " ";
234245
const region = sandbox.cluster_hostname.split(".")[0];
235246
const statusText = sandbox.status === "running"
236-
? green(sandbox.status)
237-
: red(sandbox.status);
247+
? green("● running")
248+
: red("○ stopped");
238249
const uptime = formatDuration(duration);
239250
const created = renderTemporalTimestamp(
240251
new Date(sandbox.created_at).toISOString(),
@@ -260,11 +271,14 @@ function renderScreen(state: DashboardState): string {
260271
}
261272
}
262273

263-
// Show a scroll indicator when the list doesn't fit on one screen
274+
// Show scroll indicators when the list doesn't fit on one screen.
275+
// ▲/▼ arrows only appear when there's content in that direction.
264276
if (displayList.length > maxVisibleRows) {
265-
lines.push(
266-
dim(` [${start + 1}${end} of ${displayList.length}]`),
267-
);
277+
const parts: string[] = [];
278+
if (start > 0) parts.push(dim("▲ more above"));
279+
parts.push(yellow(`[${start + 1}${end} of ${displayList.length}]`));
280+
if (end < displayList.length) parts.push(dim("▼ more below"));
281+
lines.push(" " + parts.join(" "));
268282
}
269283
}
270284
}
@@ -288,20 +302,29 @@ function renderScreen(state: DashboardState): string {
288302
bold(" Select org: ") + dim("↑/↓ Navigate Enter Select Esc Cancel"),
289303
);
290304
} else if (state.error) {
291-
lines.push(red(` Error: ${state.error}`));
305+
lines.push(red(` Error: ${state.error}`));
292306
} else if (state.statusMessage) {
293-
lines.push(green(` ${state.statusMessage}`));
307+
lines.push(green(` ${state.statusMessage}`));
294308
} else if (state.loading) {
295309
lines.push(dim(" Refreshing..."));
296310
} else {
297311
lines.push("");
298312
}
299313

300314
const shortcuts = state.mode === "org"
301-
? dim(" * = active org")
302-
: dim(
303-
" ↑/↓ Navigate s SSH k Kill e Extend c Copy ID f Filter o/O Sort t Org r Refresh q Quit",
304-
);
315+
? " " + green("●") + dim(" = active org")
316+
: " " + [
317+
bold("↑/↓") + dim(" Navigate"),
318+
bold("s") + dim(" SSH"),
319+
bold("k") + dim(" Kill"),
320+
bold("e") + dim(" Extend"),
321+
bold("c") + dim(" Copy ID"),
322+
bold("f") + dim(" Filter"),
323+
bold("o/O") + dim(" Sort"),
324+
bold("t") + dim(" Org"),
325+
bold("r") + dim(" Refresh"),
326+
bold("q") + dim(" Quit"),
327+
].join(" ");
305328
lines.push(shortcuts);
306329

307330
return CLEAR_SCREEN + CURSOR_HOME + lines.join("\n");

0 commit comments

Comments
 (0)