this repo has no description
at main 223 lines 4.9 kB view raw
1import type { ReactNode, CSSProperties } from 'react'; 2import { Stack, Cluster, Divider } from './Layout'; 3 4interface TerminalProps { 5 children: ReactNode; 6 title?: string; 7 subtitle?: string; 8 showBack?: boolean; 9 className?: string; 10} 11 12/** 13 * Terminal - main wrapper with brutalist monospace aesthetic 14 */ 15export function Terminal({ children, title = '@toshi', subtitle, showBack = false, className = '' }: TerminalProps) { 16 const displayTitle = subtitle ? `${title} ${subtitle}` : title; 17 18 return ( 19 <article className={`terminal ${className}`}> 20 <header className="terminal-header"> 21 {showBack && <a href="/" className="back-link">&lt;</a>} 22 <h1 className="terminal-title">{displayTitle}</h1> 23 </header> 24 <Divider /> 25 <Stack gap="lg">{children}</Stack> 26 </article> 27 ); 28} 29 30interface SectionProps { 31 children: ReactNode; 32 title?: string; 33} 34 35/** 36 * Section - grouped content with optional title 37 */ 38export function Section({ children, title }: SectionProps) { 39 return ( 40 <section className="terminal-section"> 41 {title && <h2 className="section-title">{title}</h2>} 42 <Stack gap="sm">{children}</Stack> 43 </section> 44 ); 45} 46 47interface LabelValueProps { 48 label: string; 49 value: ReactNode; 50 status?: 'default' | 'success' | 'error' | 'warning'; 51} 52 53/** 54 * LabelValue - key-value display (e.g., "BALANCE: 1000") 55 */ 56export function LabelValue({ label, value, status = 'default' }: LabelValueProps) { 57 return ( 58 <div className="label-value"> 59 <span className="label">{label}:</span>{' '} 60 <span className={`value status-${status}`}>{value}</span> 61 </div> 62 ); 63} 64 65interface DataRowProps { 66 cells: ReactNode[]; 67 widths?: string[]; 68 href?: string; 69} 70 71/** 72 * DataRow - single row in a data table 73 */ 74export function DataRow({ cells, widths, href }: DataRowProps) { 75 const content = ( 76 <div className="data-row"> 77 {cells.map((cell, i) => ( 78 <span 79 key={i} 80 className="data-cell" 81 style={widths?.[i] ? { width: widths[i], flexShrink: 0 } : { flex: 1 }} 82 > 83 {cell} 84 </span> 85 ))} 86 </div> 87 ); 88 89 if (href) { 90 return <a href={href} className="data-row-link">{content}</a>; 91 } 92 93 return content; 94} 95 96interface DataTableProps { 97 headers: string[]; 98 widths?: string[]; 99 children: ReactNode; 100 emptyMessage?: string; 101} 102 103/** 104 * DataTable - tabular data display 105 */ 106export function DataTable({ headers, widths, children, emptyMessage = '(no data)' }: DataTableProps) { 107 const hasChildren = Array.isArray(children) ? children.length > 0 : !!children; 108 109 return ( 110 <div className="data-table"> 111 <div className="data-row data-header"> 112 {headers.map((header, i) => ( 113 <span 114 key={i} 115 className="data-cell" 116 style={widths?.[i] ? { width: widths[i], flexShrink: 0 } : { flex: 1 }} 117 > 118 {header} 119 </span> 120 ))} 121 </div> 122 <div className="data-body"> 123 {hasChildren ? children : <div className="data-empty">{emptyMessage}</div>} 124 </div> 125 </div> 126 ); 127} 128 129interface ActionBarProps { 130 children: ReactNode; 131} 132 133/** 134 * ActionBar - row of action buttons 135 */ 136export function ActionBar({ children }: ActionBarProps) { 137 return ( 138 <Cluster gap="sm" className="action-bar"> 139 {children} 140 </Cluster> 141 ); 142} 143 144interface StatusBadgeProps { 145 status: 'online' | 'offline' | 'loading'; 146 label?: string; 147} 148 149/** 150 * StatusBadge - inline status indicator 151 */ 152export function StatusBadge({ status, label }: StatusBadgeProps) { 153 const labels = { 154 online: 'ONLINE', 155 offline: 'OFFLINE', 156 loading: 'LOADING...', 157 }; 158 159 return ( 160 <span className={`status-badge status-${status}`}> 161 {label || labels[status]} 162 </span> 163 ); 164} 165 166interface InputFieldProps { 167 label: string; 168 value: string; 169 onChange: (value: string) => void; 170 placeholder?: string; 171 type?: 'text' | 'number'; 172 disabled?: boolean; 173 width?: string; 174 min?: number; 175 max?: number; 176} 177 178/** 179 * InputField - labeled input in terminal style 180 */ 181export function InputField({ 182 label, 183 value, 184 onChange, 185 placeholder, 186 type = 'text', 187 disabled = false, 188 width = '200px', 189 min, 190 max, 191}: InputFieldProps) { 192 return ( 193 <div className="input-field"> 194 <label className="input-label">{label}:</label>{' '} 195 <input 196 type={type} 197 value={value} 198 onChange={(e) => onChange(e.target.value)} 199 placeholder={placeholder} 200 disabled={disabled} 201 style={{ width }} 202 min={min} 203 max={max} 204 /> 205 </div> 206 ); 207} 208 209interface MessageProps { 210 type: 'success' | 'error' | 'info'; 211 children: ReactNode; 212} 213 214/** 215 * Message - styled message display 216 */ 217export function Message({ type, children }: MessageProps) { 218 return ( 219 <div className={`message message-${type}`}> 220 {children} 221 </div> 222 ); 223}