Restyle the UI: instrument-panel palette, type system, and a signal accent

Replaces the default shadcn grayscale/Geist look with a deliberate
identity built around the product's own subject - a control panel for
running scripts and watching their output live:

- Type: Bricolage Grotesque for page/card titles (via the existing
  --font-heading token, used with restraint), IBM Plex Sans for UI body
  text, IBM Plex Mono for technical data (run IDs, commands, timestamps,
  status labels) - all self-hosted at build time via next/font/google,
  no runtime CDN dependency for a self-hosted tool.
- Color: a cool graphite ink/paper base with a warm brass signal accent
  in both themes. The brass tone doubles as the "running" status color,
  so an active run literally lights the UI up with the brand color.
  New --status-* tokens give queued/running/succeeded/failed/cancelled/
  warn a single source of truth instead of ad-hoc Tailwind color classes.
- Structural language: small tracked-out uppercase mono labels mark
  technical fields (Command, Variables, Triggered by...) consistently;
  the live-output terminal gets an instrument-bezel frame (header bar +
  panel) instead of floating loose above the xterm canvas.

Layout/IA is unchanged throughout - this is a token- and detail-level
pass, not a restructuring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 11:44:30 +02:00
co-authored by Claude Sonnet 5
parent f102ace7e8
commit 7e1569b94c
12 changed files with 176 additions and 117 deletions
+4 -2
View File
@@ -11,11 +11,13 @@ export const metadata: Metadata = { title: "Docs" };
export default function DocsIndexPage() {
return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold tracking-tight">Docs</h1>
<h1 className="font-heading text-2xl font-semibold tracking-tight">
Docs
</h1>
<div className="grid gap-3 sm:grid-cols-2">
{DOCS.map((doc) => (
<Link key={doc.slug} href={`/docs/${doc.slug}`}>
<Card className="hover:border-foreground/30 h-full transition-colors">
<Card className="hover:border-primary/50 h-full transition-colors">
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0">
<div className="flex items-center gap-2">
<BookOpen className="text-muted-foreground size-4.5 shrink-0" />
+16 -6
View File
@@ -13,21 +13,31 @@ export default function DashboardPage() {
if (config.scripts.length === 0) {
return (
<div className="text-muted-foreground py-24 text-center">
No scripts configured yet. Add entries under{" "}
<code className="bg-muted rounded px-1.5 py-0.5">scripts:</code> in your
config file.
<div className="flex flex-col items-center gap-2 py-24 text-center">
<PlayCircle className="text-muted-foreground/50 mb-2 size-8" />
<h1 className="font-heading text-lg font-semibold tracking-tight">
No scripts yet
</h1>
<p className="text-muted-foreground max-w-sm text-sm">
Add entries under{" "}
<code className="bg-muted rounded px-1.5 py-0.5 font-mono text-xs">
scripts:
</code>{" "}
in your config file to see them here.
</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold tracking-tight">Scripts</h1>
<h1 className="font-heading text-2xl font-semibold tracking-tight">
Scripts
</h1>
<div className="grid gap-3 sm:grid-cols-2">
{config.scripts.map((script) => (
<Link key={script.id} href={`/scripts/${script.id}`}>
<Card className="hover:border-foreground/30 h-full transition-colors">
<Card className="hover:border-primary/50 h-full transition-colors">
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0">
<div className="flex items-center gap-2">
<PlayCircle className="text-muted-foreground size-4.5 shrink-0" />
+15 -9
View File
@@ -81,36 +81,42 @@ export default async function RunDetailPage({ params }: RunDetailPageProps) {
</Link>
</CardAction>
)}
<dl className="text-muted-foreground grid grid-cols-2 gap-x-4 gap-y-1 text-xs sm:grid-cols-3">
<dl className="text-muted-foreground grid grid-cols-2 gap-x-4 gap-y-2 text-xs sm:grid-cols-3">
<div>
<dt className="font-medium">Triggered by</dt>
<dd>{run.triggeredBy}</dd>
<dt className="font-mono text-[0.7rem] font-medium tracking-widest uppercase">
Triggered by
</dt>
<dd className="font-mono">{run.triggeredBy}</dd>
</div>
<div>
<dt className="font-medium">Started</dt>
<dd>
<dt className="font-mono text-[0.7rem] font-medium tracking-widest uppercase">
Started
</dt>
<dd className="font-mono">
{run.startedAt ? new Date(run.startedAt).toLocaleString() : "-"}
</dd>
</div>
<div>
<dt className="font-medium">Run ID</dt>
<dt className="font-mono text-[0.7rem] font-medium tracking-widest uppercase">
Run ID
</dt>
<dd className="truncate font-mono">{run.id}</dd>
</div>
</dl>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs font-medium">
<span className="text-muted-foreground font-mono text-[0.7rem] font-medium tracking-widest uppercase">
Command
</span>
<pre className="bg-muted overflow-x-auto rounded-md p-3 font-mono text-xs whitespace-pre">
<pre className="bg-muted border-primary/30 overflow-x-auto rounded-md border-l-2 p-3 font-mono text-xs whitespace-pre">
{run.resolvedCommand}
</pre>
</div>
{variableEntries.length > 0 && (
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs font-medium">
<span className="text-muted-foreground font-mono text-[0.7rem] font-medium tracking-widest uppercase">
Variables
</span>
<dl className="grid gap-x-6 gap-y-2 rounded-md border p-3 text-xs sm:grid-cols-2">
+6 -4
View File
@@ -161,7 +161,9 @@ export default async function RunsPage({ searchParams }: RunsPageProps) {
return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold tracking-tight">Run History</h1>
<h1 className="font-heading text-2xl font-semibold tracking-tight">
Run History
</h1>
<RunsToolbar scripts={scripts} />
{rows.length === 0 ? (
<p className="text-muted-foreground py-12 text-center">
@@ -196,15 +198,15 @@ export default async function RunsPage({ searchParams }: RunsPageProps) {
<TableCell>
<RunStatusBadge status={run.status} />
</TableCell>
<TableCell className="text-muted-foreground">
<TableCell className="text-muted-foreground font-mono text-xs">
{run.triggeredBy}
</TableCell>
<TableCell className="text-muted-foreground">
<TableCell className="text-muted-foreground font-mono text-xs">
{run.startedAt
? new Date(run.startedAt).toLocaleString()
: "-"}
</TableCell>
<TableCell className="text-muted-foreground">
<TableCell className="text-muted-foreground font-mono text-xs">
{formatDuration(run.startedAt, run.endedAt)}
</TableCell>
</TableRow>
+86 -63
View File
@@ -9,8 +9,14 @@
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--font-mono: var(--font-mono);
--font-heading: var(--font-display);
--color-status-queued: var(--status-queued);
--color-status-running: var(--status-running);
--color-status-succeeded: var(--status-succeeded);
--color-status-failed: var(--status-failed);
--color-status-cancelled: var(--status-cancelled);
--color-status-warn: var(--status-warn);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -49,73 +55,90 @@
--radius-4xl: calc(var(--radius) * 2.6);
}
/*
* Palette - "instrument panel": cool graphite ink/paper with a warm brass signal accent.
* The brass tone doubles as the "running" status color, so a live run literally lights the UI
* up with the brand color - see run-status-badge.tsx and run-terminal.tsx.
*/
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: #f5f6f7;
--foreground: #15171a;
--card: #ffffff;
--card-foreground: #15171a;
--popover: #ffffff;
--popover-foreground: #15171a;
--primary: #c8842e;
--primary-foreground: #17130a;
--secondary: #ececee;
--secondary-foreground: #15171a;
--muted: #ececee;
--muted-foreground: #6b7280;
--accent: #ececee;
--accent-foreground: #15171a;
--destructive: #b3402e;
--border: #dfe1e3;
--input: #dfe1e3;
--ring: #c8842e;
--status-queued: #6b7280;
--status-running: #c8842e;
--status-succeeded: #3f8f5f;
--status-failed: #b3402e;
--status-cancelled: #6b7280;
--status-warn: #8a7530;
--chart-1: #c8842e;
--chart-2: #3f8f5f;
--chart-3: #6b7280;
--chart-4: #8a7530;
--chart-5: #b3402e;
--radius: 0.5rem;
--sidebar: #f5f6f7;
--sidebar-foreground: #15171a;
--sidebar-primary: #c8842e;
--sidebar-primary-foreground: #17130a;
--sidebar-accent: #ececee;
--sidebar-accent-foreground: #15171a;
--sidebar-border: #dfe1e3;
--sidebar-ring: #c8842e;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--background: #14161a;
--foreground: #edeef0;
--card: #1b1e23;
--card-foreground: #edeef0;
--popover: #1b1e23;
--popover-foreground: #edeef0;
--primary: #e9a857;
--primary-foreground: #1a1305;
--secondary: #23262c;
--secondary-foreground: #edeef0;
--muted: #23262c;
--muted-foreground: #9aa0a8;
--accent: #23262c;
--accent-foreground: #edeef0;
--destructive: #e2685a;
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--input: oklch(1 0 0 / 14%);
--ring: #e9a857;
--status-queued: #9aa0a8;
--status-running: #e9a857;
--status-succeeded: #5fb983;
--status-failed: #e2685a;
--status-cancelled: #9aa0a8;
--status-warn: #c9a344;
--chart-1: #e9a857;
--chart-2: #5fb983;
--chart-3: #9aa0a8;
--chart-4: #c9a344;
--chart-5: #e2685a;
--sidebar: #1b1e23;
--sidebar-foreground: #edeef0;
--sidebar-primary: #e9a857;
--sidebar-primary-foreground: #1a1305;
--sidebar-accent: #23262c;
--sidebar-accent-foreground: #edeef0;
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--sidebar-ring: #e9a857;
}
@layer base {
+3 -3
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<rect width="24" height="24" rx="5" fill="#09090b" />
<path d="M12 19h8" stroke="#fafafa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<path d="m4 17 6-6-6-6" stroke="#fafafa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<rect width="24" height="24" rx="5" fill="#15171a" />
<path d="M12 19h8" stroke="#e9a857" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
<path d="m4 17 6-6-6-6" stroke="#e9a857" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" />
</svg>

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

+16 -6
View File
@@ -1,18 +1,28 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Bricolage_Grotesque, IBM_Plex_Mono, IBM_Plex_Sans } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
// Display face for page titles and card headings (wired via --font-heading in globals.css) - used
// with restraint, never for body copy. Body/UI text and technical data (run IDs, commands,
// timestamps) share the IBM Plex family so the two registers still read as one system.
const displayFont = Bricolage_Grotesque({
variable: "--font-display",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const sansFont = IBM_Plex_Sans({
variable: "--font-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const monoFont = IBM_Plex_Mono({
variable: "--font-mono",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
export const metadata: Metadata = {
@@ -31,7 +41,7 @@ export default function RootLayout({
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${displayFont.variable} ${sansFont.variable} ${monoFont.variable} h-full antialiased`}
suppressHydrationWarning
>
<body
+1 -1
View File
@@ -50,7 +50,7 @@ export function LoginForm() {
<Card className="w-full max-w-sm">
<CardHeader>
<div className="flex items-center gap-2">
<Terminal className="size-5" />
<Terminal className="text-primary size-5" />
<CardTitle>TriggerShell</CardTitle>
</div>
<CardDescription>
+4 -4
View File
@@ -34,9 +34,9 @@ export function Nav({
<div className="flex min-w-0 items-center gap-3 sm:gap-6">
<Link
href="/"
className="flex shrink-0 items-center gap-2 font-semibold tracking-tight"
className="font-heading flex shrink-0 items-center gap-2 text-[1.05rem] font-semibold tracking-tight"
>
<Terminal className="size-5" />
<Terminal className="text-primary size-5" />
TriggerShell
</Link>
<nav className="flex items-center gap-1">
@@ -45,8 +45,8 @@ export function Nav({
key={href}
href={href}
className={cn(
"text-muted-foreground hover:text-foreground flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium transition-colors sm:px-3",
pathname === href && "bg-muted text-foreground",
"text-muted-foreground hover:text-foreground hover:bg-muted/60 flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium transition-colors sm:px-3",
pathname === href && "bg-primary/10 text-foreground",
)}
>
<Icon className="size-4" />
+17 -11
View File
@@ -3,13 +3,13 @@ import type { RunStatus } from "@/lib/db/schema";
import { cn } from "@/lib/utils";
const styles: Record<RunStatus, string> = {
queued: "bg-muted text-muted-foreground",
running: "bg-blue-500/15 text-blue-600 dark:text-blue-400",
succeeded: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
failed: "bg-destructive/15 text-destructive",
cancelled: "bg-muted text-muted-foreground",
timed_out: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
interrupted: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
queued: "bg-status-queued/12 text-status-queued",
running: "bg-status-running/15 text-status-running",
succeeded: "bg-status-succeeded/12 text-status-succeeded",
failed: "bg-status-failed/12 text-status-failed",
cancelled: "bg-status-cancelled/12 text-status-cancelled",
timed_out: "bg-status-warn/12 text-status-warn",
interrupted: "bg-status-warn/12 text-status-warn",
};
export const runStatusLabels: Record<RunStatus, string> = {
@@ -25,12 +25,18 @@ export const runStatusLabels: Record<RunStatus, string> = {
export function RunStatusBadge({ status }: { status: RunStatus }) {
return (
<Badge
className={cn("border-transparent font-medium", styles[status])}
className={cn(
"gap-1.5 border-transparent font-mono text-[0.7rem] font-medium tracking-wide uppercase",
styles[status],
)}
variant="outline"
>
{status === "running" && (
<span className="mr-1 inline-block size-1.5 animate-pulse rounded-full bg-current" />
)}
<span
className={cn(
"size-1.5 shrink-0 rounded-full bg-current",
status === "running" && "animate-pulse",
)}
/>
{runStatusLabels[status]}
</Badge>
);
+4 -4
View File
@@ -74,13 +74,13 @@ export function RunTerminal({
}
return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="border-border overflow-hidden rounded-lg border">
<div className="bg-muted/40 flex items-center justify-between gap-3 border-b px-3 py-2">
<div className="flex items-center gap-3">
<RunStatusBadge status={status} />
{exitCode !== null && (
<span className="text-muted-foreground text-xs">
exit code {exitCode}
<span className="text-muted-foreground font-mono text-xs">
exit {exitCode}
</span>
)}
</div>
+4 -4
View File
@@ -10,9 +10,9 @@ export interface XtermViewHandle {
}
const theme = {
background: "#09090b",
foreground: "#f4f4f5",
cursor: "#f4f4f5",
background: "#101215",
foreground: "#edeef0",
cursor: "#e9a857",
selectionBackground: "#3f3f46",
black: "#18181b",
red: "#f87171",
@@ -95,7 +95,7 @@ export const XtermView = forwardRef<XtermViewHandle, XtermViewProps>(
return (
<div
ref={containerRef}
className="h-[60vh] overflow-hidden rounded-lg bg-zinc-950 p-2"
className="h-[60vh] overflow-hidden bg-[#101215] p-2"
/>
);
},