Style Guide

Developer12 min read

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)

  1. Intent (1-2 lines): what changes and why.
  2. Plan (bullets): steps, ordered, minimal.
  3. Code (copy/paste ready): include file paths and complete snippets.
  4. Notes: edge cases, performance, security, migrations, rollback.
  5. 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

  1. Never let content push layout — always constrain overflow containers.
  2. Horizontal tab bars: overflow-x-auto scrollbar-none (hidden scrollbar, swipeable on mobile).
  3. Dropdown menus: max-h-80 overflow-y-auto (320px cap).
  4. Dialogs: max-h-[85vh] overflow-y-auto.
  5. Long text: truncate + max-w-[Npx] or min-w-0 on flex parent.
  6. Multi-line clamp: line-clamp-1 or line-clamp-2.
  7. 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 PanelLeft icon, persisted to localStorage("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) or nyx-text-muted (secondary). Never lighter.
  • Font size: Minimum text-[10px] for any visible text. No text-[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-wider for 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:

  1. Uses ProviderModelPicker for any LLM selection
  2. Uses PersonaPicker for any persona assignment
  3. Wraps content in Card / CardContent
  4. Page header follows text-lg font-semibold uppercase tracking-wider pattern
  5. Respects md: breakpoint for layout switching
  6. Long text has truncate or line-clamp-*
  7. Scrollable containers have overflow-*-auto with proper max-height
  8. Empty state follows centered icon + message + CTA pattern
  9. Loading state uses Skeleton components matching content shape
  10. Buttons use correct variant (default / outline / ghost / destructive)