Files
OpenQode/bin/tui-theme.mjs

232 lines
8.0 KiB
JavaScript

/**
* TUI Theme Module - Premium Design System
* Provides consistent semantic colors, spacing, and rail styling
* With profile-gated backgrounds for cross-platform compatibility
*
* DESIGN PRINCIPLES:
* 1. Single accent color (no neon chaos)
* 2. Semantic roles (fg, muted, accent, border, success/warn/error)
* 3. Profile-gated backgrounds (SAFE_ASCII avoids most backgrounds)
* 4. One-frame rule (only outer container has border)
*/
import { getCapabilities, PROFILE, isBackgroundOK, isDimOK, isUnicodeOK } from './terminal-profile.mjs';
// ═══════════════════════════════════════════════════════════════
// SEMANTIC COLOR TOKENS
// ═══════════════════════════════════════════════════════════════
export const colors = {
// Primary text
fg: 'white',
fgBold: 'whiteBright',
// Muted/secondary text
muted: 'gray',
mutedDim: 'gray', // isDimOK() ? dimmed gray : regular gray
// Single accent (not multi-color chaos)
accent: 'cyan',
accentBold: 'cyanBright',
// Borders and dividers
border: 'gray',
borderFocus: 'cyan',
divider: 'gray',
// Semantic status colors
success: 'green',
warning: 'yellow',
error: 'red',
info: 'blue',
// Role-specific rail colors (left rail indicator)
rail: {
user: 'cyan',
assistant: 'green',
system: 'yellow',
tool: 'magenta',
error: 'red',
thinking: 'gray'
},
// Focus/selection
focus: 'cyan',
selection: 'blue'
};
// ═══════════════════════════════════════════════════════════════
// SPACING SCALE (terminal rows/chars)
// ═══════════════════════════════════════════════════════════════
export const spacing = {
none: 0,
xs: 0,
sm: 1,
md: 2,
lg: 3,
xl: 4
};
// ═══════════════════════════════════════════════════════════════
// TYPOGRAPHY HIERARCHY
// ═══════════════════════════════════════════════════════════════
export const typography = {
// Section headers (e.g., "PROJECT", "SESSION")
header: { bold: true, color: colors.fg },
// Labels (e.g., "Branch:", "Model:")
label: { color: colors.muted },
// Values (e.g., "main", "qwen-coder-plus")
value: { color: colors.fg },
// Muted metadata
meta: { color: colors.muted, dimColor: true },
// Status text
status: { color: colors.accent },
// Error text
error: { color: colors.error, bold: true }
};
// ═══════════════════════════════════════════════════════════════
// BORDER STYLES
// ═══════════════════════════════════════════════════════════════
export function getBorderStyle() {
return isUnicodeOK() ? 'round' : 'single';
}
export const borders = {
// Only use for outer app frame
frame: {
style: getBorderStyle,
color: colors.border
},
// Inner elements use NO borders - only dividers
none: null
};
// ═══════════════════════════════════════════════════════════════
// RAIL STYLING (replaces nested boxes)
// ═══════════════════════════════════════════════════════════════
export const rail = {
width: 2, // Rail column width
// Characters
char: {
active: isUnicodeOK() ? '│' : '|',
streaming: isUnicodeOK() ? '┃' : '|',
dimmed: isUnicodeOK() ? '╎' : ':'
},
// Colors by role
colors: {
user: colors.rail.user,
assistant: colors.rail.assistant,
system: colors.rail.system,
tool: colors.rail.tool,
error: colors.rail.error,
thinking: colors.rail.thinking
}
};
// ═══════════════════════════════════════════════════════════════
// LAYOUT CONSTANTS
// ═══════════════════════════════════════════════════════════════
export const layout = {
// Sidebar
sidebar: {
minWidth: 20,
maxWidth: 28,
defaultWidth: 24
},
// Divider between sidebar and main
divider: {
width: 1,
char: isUnicodeOK() ? '│' : '|',
color: colors.border
},
// Transcript (main content)
transcript: {
maxLineWidth: 90, // Clamp for readability
minLineWidth: 40,
padding: 1
},
// Input bar (fixed height)
inputBar: {
height: 3, // Fixed - never changes
borderTop: true
},
// Status strip (single line)
statusStrip: {
height: 1
},
// Fixed row reservations
reservedRows: {
statusStrip: 1,
inputBar: 3,
frameTop: 1,
frameBottom: 1
}
};
// ═══════════════════════════════════════════════════════════════
// BREAKPOINTS
// ═══════════════════════════════════════════════════════════════
export const breakpoints = {
tiny: 60, // Hide sidebar
narrow: 80, // Minimal sidebar
medium: 100, // Normal sidebar
wide: 120 // Full sidebar
};
// ═══════════════════════════════════════════════════════════════
// PROFILE-GATED BACKGROUNDS
// ═══════════════════════════════════════════════════════════════
export function getBackground(purpose) {
// SAFE_ASCII: avoid backgrounds entirely
if (!isBackgroundOK()) {
return undefined;
}
const backgrounds = {
selection: '#1a1a2e', // Dark selection
focus: '#0d1117', // Focus highlight
error: '#2d1f1f', // Error background
warning: '#2d2a1f', // Warning background
thinking: '#1a1a1a' // Thinking block
};
return backgrounds[purpose];
}
// ═══════════════════════════════════════════════════════════════
// UNIFIED THEME OBJECT (backwards compatible)
// ═══════════════════════════════════════════════════════════════
export const theme = {
colors,
spacing,
typography,
borders,
rail,
layout,
breakpoints,
// Helper functions
getBorderStyle,
getBackground,
// Capability checks
isUnicodeOK,
isBackgroundOK,
isDimOK
};
export default theme;