Style Guide
NYXCORE STYLE GUIDE
Brand essence: engineered in the dark — technical precision, focus, architectural clarity, minimal noise.
This document covers both engineering conventions and UI component patterns. Mobile first, consistent, readable.
VOICE & TONE
- Concise, direct, structured. No hype, no buzzword floods.
- Short, confident sentences. Technical language only when it adds precision.
- If uncertain, state assumptions explicitly and propose a safe default.
OUTPUT FORMAT (for AI-generated code)
- Intent (1-2 lines): what changes and why.
- Plan (bullets): steps, ordered, minimal.
- Code (copy/paste ready): include file paths and complete snippets.
- Notes: edge cases, performance, security, migrations, rollback.
- Tests: what to test, how to verify.
ENGINEERING PRINCIPLES
- Correctness, readability, maintainability over cleverness.
- Strong typing; explicit error handling; no silent failures.
- Deterministic behavior > "magic".
- Small modules, clear boundaries, single responsibility.
- Pure functions and dependency injection over globals.
- Logs: structured, minimal, useful (no noise).
CODE CONVENTIONS
- Naming: explicit and domain-driven.
- Functions: refactor when > ~40-60 lines.
- Control flow: early returns, guard clauses, no nested pyramids.
- Config: via env with validation.
- Comments: only when they add non-obvious reasoning.
- Paths: both happy path and failure path.
SECURITY & RELIABILITY
- Validate inputs; sanitize outputs; escape where relevant.
- Least privilege; no secrets in code; prefer secret managers.
- Handle timeouts, retries, idempotency where applicable.
- APIs: consistent status codes, error envelopes, request IDs.
DESIGN SYSTEM
Color Tokens
All colors use CSS custom properties via the nyx-* namespace.
Never use raw Tailwind colors (blue-500, gray-200) in UI components — always use nyx-* tokens.
| Token | Light | Dark | Usage |
|---|---|---|---|
nyx-bg |
#ffffff |
#0c0c0c |
Page background |
nyx-surface |
#f5f5f5 |
#1a1a1a |
Card/panel backgrounds |
nyx-surface-hover |
#ebebeb |
#262626 |
Hover state for surfaces |
nyx-border |
#dcdcdc |
#363636 |
Borders, dividers, separators |
nyx-text |
#141414 |
#ebebeb |
Primary text |
nyx-text-muted |
#636363 |
#8c8c8c |
Secondary text, labels, captions |
nyx-accent |
#4f46e5 |
#818cf8 |
Primary interactive (buttons, links, active states) |
nyx-accent-hover |
#4338ca |
#6366f1 |
Hover state for accent |
nyx-danger |
#dc2626 |
#f87171 |
Errors, destructive actions |
nyx-success |
#16a34a |
#4ade80 |
Success, positive states |
nyx-warning |
#d97706 |
#fbbf24 |
Warnings, medium-priority states |
Cyberpunk theme ([data-theme="cyberpunk"]): cyan accent (#00e5ff), glow effects on accent elements, CRT scan line overlay.
Typography
Font Stack:
- Sans (primary):
Inter— body text, UI labels, descriptions - Mono (code/data):
JetBrains Mono— identifiers, technical values, badges, code
Size Scale:
| Tailwind | px | Usage |
|---|---|---|
text-[9px] |
9 | System heartbeat count, micro labels |
text-[10px] |
10 | Badges, timestamps, metadata, tracking labels |
text-xs |
12 | Form labels, button text (sm), footnotes |
text-sm |
14 | Body text, descriptions, card content |
text-base |
16 | Rarely used — avoid |
text-lg |
18 | Page titles only |
Weights: font-medium (500) for labels/buttons, font-semibold (600) for titles, font-bold (700) for brand text only.
Label Pattern (all-caps micro label):
<span className="text-[10px] font-mono text-nyx-text-muted uppercase tracking-wider">
LABEL TEXT
</span>
Page Header:
<h1 className="text-lg font-semibold uppercase tracking-wider">Page Title</h1>
<p className="text-sm text-nyx-text-muted">Subtitle or description</p>
Spacing
Section spacing: space-y-6 between major page sections.
Card content: space-y-4 within cards.
Tight groups: space-y-2 for label+input pairs.
Flex gaps: gap-2 (standard), gap-1.5 (icon+label), gap-3 (buttons row).
Card padding: p-4 (header and content).
Page padding: p-4 lg:p-6 (responsive).
Breakpoints
| Prefix | Width | Usage |
|---|---|---|
| (none) | 0+ | Mobile first — default layout |
sm: |
640px | Tablet adjustments |
md: |
768px | Primary layout switch — sidebar vs bottom nav |
lg: |
1024px | Wider spacing, 3-column grids |
xl: |
1280px | Max-width constraints |
Always build mobile first, then add breakpoint overrides.
SHARED COMPONENTS
System Heartbeat
File: src/components/layout/system-heartbeat.tsx
Location: Sidebar header (desktop, expanded mode only)
Live system activity indicator with load-responsive animation:
▸ ● ||||| ▸ 3
↑ ↑ ↑ ↑ ↑
│ │ │ │ └─ Active process count (mono, 9px)
│ │ │ └──── Flow-out arrow (animated)
│ │ └───────── Activity sparkline (5 bars, 500ms transitions)
│ └────────────── Pulsing dot (custom @keyframes nyx-heartbeat)
└───────────────── Flow-in arrow (animated)
Load levels (determined by active process count):
| Level | Count | Dot Color | Beat Duration |
|---|---|---|---|
idle |
0 | text-muted |
2.4s |
nominal |
1-2 | accent |
1.6s |
active |
3-5 | green-400 |
1.0s |
high |
6+ | amber-400 |
0.6s |
Custom keyframes:
nyx-heartbeat: scale 1.0 → 1.4 → 1.0 → 1.25 → 1.0 (4-phase cardiac rhythm)nyx-flow-in/nyx-flow-out: translateX + opacity for directional token flow
When to use: Only in sidebar. For other status indicators, use animate-pulse on a small dot.
ProviderModelPicker
File: src/components/shared/provider-model-picker.tsx
Rule: Use this component for ALL provider/model selection. Never use native <select> for providers.
import { ProviderModelPicker, ProviderModelSelection } from "@/components/shared/provider-model-picker";
const [selection, setSelection] = useState<ProviderModelSelection>({
provider: "auto",
model: "auto",
});
<ProviderModelPicker
label="Judge:" // Optional prefix label
value={selection}
onChange={setSelection}
hideAuto={false} // Show "Auto (first available)" option
/>
Visual anatomy:
┌─────────────────────────────────┐
│ Judge: ▾ ANTHROPIC / CLAUDE-4 │ ← Trigger (h-8, text-xs, font-mono)
└─────────────────────────────────┘
┌─────────────────────────┐
│ ✓ Auto │ ← First option
│ First available │
├─────────────────────────┤
│ ANTHROPIC │ ← Provider group header
│ Claude 4 Sonnet │ (accent if key exists,
│ ● low · Best for... │ muted if missing)
│ Claude 4 Opus │
│ ● high · Best for... │
├─────────────────────────┤
│ GOOGLE │
│ Gemini 2.5 Flash │
│ ● free · Best for... │
└─────────────────────────┘
Data: Self-fetching via trpc.dashboard.availableProviders (staleTime: 60s).
Pass providers prop to override with external data.
Cost indicator dots: bg-nyx-success for free/low, bg-nyx-warning for medium/high.
PersonaPicker
File: src/components/shared/persona-picker.tsx
Rule: Use this component for ALL persona selection. Supports single and multi-select modes.
import { PersonaPicker } from "@/components/shared/persona-picker";
// Single selection
<PersonaPicker
mode="single"
value={selectedPersonaId}
onChange={(id) => setSelectedPersonaId(id)}
showNone // Allow deselecting
/>
// Multi selection
<PersonaPicker
mode="multi"
value={selectedPersonaIds}
onChange={(ids) => setSelectedPersonaIds(ids)}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
mode |
"single" | "multi" |
required | Selection mode |
collapsible |
boolean |
true |
Wrap in collapsible section |
label |
string |
"Personas" |
Section header text |
compact |
boolean |
false |
Hide description, traits, XP bar |
showNone |
boolean |
false |
Show "None" option (single mode) |
Visual anatomy (expanded card):
┌──────────────────────────────────────┐
│ ☐ [Avatar] Aria the Analyst Lv.3 │
│ ████████░░░ 240/300 XP │
│ analytical · structured │
│ Data / Patterns / Risk │
│ Short description... │
└──────────────────────────────────────┘
Compact mode: Hides XP bar, traits, description. Shows name + specializations only.
Collapsible header (default):
┌──────────────────────────────────────┐
│ ▾ 👤 Personas [3] │ ← Badge shows selection count
├──────────────────────────────────────┤
│ (persona cards grid 1-2 cols) │
└──────────────────────────────────────┘
Data: Self-fetching via trpc.admin.personas.list.
BranchSelector
File: src/components/project/sync-controls.tsx
Searchable branch dropdown with resync detection. Branch selection persists immediately to Project.activeBranch via sync.setActiveBranch mutation.
When activeBranch !== lastSyncedBranch: red "Resync required" button with AlertTriangle icon.
UI PATTERNS
Card
<Card>
<CardHeader>
<CardTitle>Section Title</CardTitle> {/* text-sm font-semibold uppercase tracking-wider */}
</CardHeader>
<CardContent className="space-y-4">
{/* Content */}
</CardContent>
</Card>
Borders: border-nyx-border, Background: bg-nyx-surface.
Always use space-y-4 inside CardContent for vertical rhythm.
Button Variants
| Variant | Usage | Appearance |
|---|---|---|
default |
Primary action | bg-nyx-accent text-white |
destructive |
Dangerous action, resync | bg-nyx-danger text-white |
outline |
Secondary action | border-nyx-border bg-transparent |
ghost |
Tertiary/toolbar | No border, hover fill |
link |
Inline navigation | Underline, text-nyx-accent |
Sizes: sm (h-8, text-xs) for toolbar/inline, default (h-9) for forms, icon (h-9 w-9) for icon-only.
Badge
<Badge variant="accent">Lv.3</Badge> {/* Blue tint */}
<Badge variant="success">Active</Badge> {/* Green tint */}
<Badge variant="warning">Pending</Badge> {/* Amber tint */}
<Badge variant="danger">Failed</Badge> {/* Red tint */}
<Badge>Default</Badge> {/* Neutral border */}
Base: text-xs font-mono uppercase tracking-wider.
Always monospace. Always uppercase.
Dialog / Modal
<Dialog>
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
<DialogContent> {/* max-w-lg, max-h-[85vh] overflow-y-auto */}
<DialogHeader>
<DialogTitle>Title</DialogTitle>
</DialogHeader>
{/* Content */}
</DialogContent>
</Dialog>
Width: w-[calc(100%-2rem)] md:w-full max-w-lg (safe margins on mobile).
Tabs
Desktop: Vertical TabsList in sidebar position.
Mobile: Horizontal scrollable TabsList with overflow-x-auto scrollbar-none.
<Tabs defaultValue="tab1">
<TabsList> {/* h-9, gap-1, border-b */}
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content</TabsContent>
</Tabs>
Empty State
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<IconComponent className="h-8 w-8 text-nyx-text-muted mb-3" />
<p className="text-sm text-nyx-text-muted mb-4">No items yet</p>
<Button size="sm">Create first item</Button>
</CardContent>
</Card>
Centered, icon + message + optional CTA. Always py-12 for breathing room.
Loading / Skeleton
<Skeleton className="h-8 w-full" /> {/* animate-pulse bg-nyx-surface-hover */}
Use skeleton blocks that match the shape of the content they replace.
Info Tooltip
import { InfoTip } from "@/components/ui/info-tip";
<InfoTip>Explanation text that appears on hover.</InfoTip>
Trigger: HelpCircle h-3.5 w-3.5 text-nyx-text-muted. Tooltip: w-64 text-xs.
OVERFLOW & SCROLL
Rules
- Never let content push layout — always constrain overflow containers.
- Horizontal tab bars:
overflow-x-auto scrollbar-none(hidden scrollbar, swipeable on mobile). - Dropdown menus:
max-h-80 overflow-y-auto(320px cap). - Dialogs:
max-h-[85vh] overflow-y-auto. - Long text:
truncate+max-w-[Npx]ormin-w-0on flex parent. - Multi-line clamp:
line-clamp-1orline-clamp-2. - File paths / branch names: Always
font-mono truncate max-w-[160px](or responsive equivalent).
Custom Scrollbar
Defined in globals.css. All scrollbars are 6px wide with nyx-border thumb color.
.scrollbar-none { /* Hide scrollbar completely — use for horizontal tab bars */ }
.no-scrollbar { /* Same as scrollbar-none (legacy alias) */ }
Fan-Out Tabs
For workflow fan-out sections with long headings:
<TabsList className="flex overflow-x-auto gap-1 p-1.5 scrollbar-none">
{sections.map(s => (
<TabsTrigger key={s.id} className="shrink-0 whitespace-nowrap">
{s.heading}
</TabsTrigger>
))}
</TabsList>
Key: shrink-0 whitespace-nowrap on each trigger prevents text wrapping.
NAVIGATION
Desktop Sidebar
File: src/components/layout/sidebar.tsx
- Width:
w-48(expanded) /w-12(collapsed) - Transition:
transition-all duration-200 - Collapse: Toggled via
PanelLefticon, persisted tolocalStorage("nyx-sidebar-collapsed") - Visibility:
hidden md:flex(desktop only)
Group headers: text-[9px] uppercase tracking-widest text-nyx-text-muted/60 font-semibold
Nav items:
<Link className={cn(
"flex items-center gap-2 rounded px-2 py-1.5 text-xs font-medium w-full",
"hover:bg-nyx-bg text-nyx-text-muted hover:text-nyx-text",
isActive && "bg-nyx-accent/10 text-nyx-accent"
)}>
<Icon className="h-3.5 w-3.5 shrink-0" />
{!collapsed && <span>{label}</span>}
</Link>
When collapsed: justify-center px-0, icon only.
Mobile Bottom Nav
File: src/components/layout/mobile-nav.tsx
- Position:
fixed bottom-0 left-0 right-0 z-40 md:hidden - Safe area:
pb-[env(safe-area-inset-bottom)](iPhone notch) - Layout: 5 equal-width columns
- Items:
flex flex-col items-center gap-1 py-2 text-[10px] - Active:
text-nyx-accent(default:text-nyx-text-muted)
In-Page Sidebar
File: src/components/layout/in-page-sidebar.tsx
Used within dashboard pages for sub-navigation (e.g., project tabs).
| Mode | Behavior |
|---|---|
Desktop (hidden md:block) |
Sticky vertical sidebar, w-48 / w-12 (collapsible) |
Mobile (md:hidden) |
Sticky horizontal tab bar, overflow-x-auto scrollbar-none, backdrop-blur-sm |
RESPONSIVE PATTERNS
Grid Layouts
{/* Standard responsive grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Dense grid (stats, badges) */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
Stack → Row
<div className="flex flex-col md:flex-row gap-4">
{/* Stacks vertically on mobile, horizontal on desktop */}
</div>
Visibility
<div className="md:hidden">Mobile only</div>
<div className="hidden md:flex">Desktop only</div>
Responsive Text
<span className="text-[10px] md:text-xs">Scales up on desktop</span>
Content Width
{/* Dialog with safe mobile margins */}
<div className="w-[calc(100%-2rem)] md:w-full max-w-lg">
{/* Page content with responsive padding */}
<div className="p-4 lg:p-6">
CONSISTENCY RULES
Mandatory Shared Components
These components MUST be used wherever their pattern appears. No exceptions.
| Component | File | Use For |
|---|---|---|
ProviderModelPicker |
shared/provider-model-picker.tsx |
Any provider/model selection |
PersonaPicker |
shared/persona-picker.tsx |
Any persona selection (single or multi) |
BranchSelector |
project/sync-controls.tsx |
Branch selection (internal, not exported standalone) |
InfoTip |
ui/info-tip.tsx |
Help tooltips |
Badge |
ui/badge.tsx |
Status labels, counts, categories |
Anti-Patterns
| Don't | Do Instead |
|---|---|
Native <select> for providers |
ProviderModelPicker |
Native <select> for personas |
PersonaPicker |
Raw Tailwind colors (text-blue-500) |
text-nyx-accent tokens |
overflow-hidden on scrollable content |
overflow-x-auto scrollbar-none |
| Long text without truncation | truncate max-w-[Npx] or line-clamp-N |
Pixel-based responsive (@media) |
Tailwind breakpoint prefixes (md:, lg:) |
Inline style={{ color }} |
CSS variable classes |
text-base / text-lg in cards |
text-sm (body), text-xs (labels) |
| Custom button styling | Button component with variant prop |
Readability Checklist
- Contrast: Text uses
nyx-text(primary) ornyx-text-muted(secondary). Never lighter. - Font size: Minimum
text-[10px]for any visible text. Notext-[8px]. - Touch targets: Minimum
h-8(32px) for interactive elements on mobile. - Line length: Max
max-w-prose(65ch) for paragraph text. - Spacing: Consistent
space-y-*within containers. No ad-hoc margins. - Labels: Uppercase +
tracking-widerfor section/group labels. - Monospace: Use for IDs, branch names, provider names, file paths, code, numeric data.
- Truncation: All user-generated content and file paths must have overflow handling.
- Mobile scroll: Horizontal lists use
overflow-x-auto scrollbar-none. - Safe areas: Bottom-anchored elements use
pb-[env(safe-area-inset-bottom)].
Component Integration Checklist (New Features)
When building a new feature page or form:
- Uses
ProviderModelPickerfor any LLM selection - Uses
PersonaPickerfor any persona assignment - Wraps content in
Card/CardContent - Page header follows
text-lg font-semibold uppercase tracking-widerpattern - Respects
md:breakpoint for layout switching - Long text has
truncateorline-clamp-* - Scrollable containers have
overflow-*-autowith proper max-height - Empty state follows centered icon + message + CTA pattern
- Loading state uses
Skeletoncomponents matching content shape - Buttons use correct variant (
default/outline/ghost/destructive)
