Version control UIs and log viewers need clarity and scannability.
Version control UIs and log viewers need clarity and scannability.
Show changes clearly and elegantly.
.diff-viewer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--color-border);
border-radius: 8px;
overflow: hidden;
font-family: 'Zen Mono', monospace;
font-size: 14px;
}
.diff-side {
background: var(--color-background);
overflow-x: auto;
}
.diff-line {
padding: 2px 8px;
line-height: 1.6;
min-height: 1.6em;
}
.diff-line-removed {
background: rgba(255, 0, 0, 0.1);
border-left: 3px solid rgba(255, 0, 0, 0.5);
}
.diff-line-added {
background: rgba(0, 255, 0, 0.1);
border-left: 3px solid rgba(0, 255, 0, 0.5);
}
.diff-line-modified {
background: rgba(255, 165, 0, 0.1);
border-left: 3px solid rgba(255, 165, 0, 0.5);
}
.diff-line-unchanged {
color: var(--color-muted-foreground);
}
.diff-unified {
font-family: 'Zen Mono', monospace;
font-size: 14px;
line-height: 1.6;
}
.diff-line {
padding: 2px 8px;
display: flex;
gap: 1rem;
}
.diff-line-number {
color: var(--color-muted-foreground);
min-width: 3ch;
text-align: right;
user-select: none;
}
.diff-line-removed {
background: #fff5f5;
color: #c53030;
}
.diff-line-removed::before {
content: '- ';
color: #c53030;
user-select: none;
}
.diff-line-added {
background: #f0fff4;
color: #22543d;
}
.diff-line-added::before {
content: '+ ';
color: #22543d;
user-select: none;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.diff-line-removed {
background: rgba(255, 0, 0, 0.15);
color: #ff6b6b;
}
.diff-line-added {
background: rgba(0, 255, 0, 0.15);
color: #4ade80;
}
}
Highlight changed characters within lines:
import * as Diff from 'diff';
function renderWordDiff(oldText: string, newText: string) {
const diffs = Diff.diffWords(oldText, newText);
return diffs.map((part, index) => {
if (part.added) {
return <ins key={index} className="char-added">{part.value}</ins>;
}
if (part.removed) {
return <del key={index} className="char-removed">{part.value}</del>;
}
return <span key={index}>{part.value}</span>;
});
}
.char-added {
background: rgba(0, 255, 0, 0.3);
text-decoration: none;
}
.char-removed {
background: rgba(255, 0, 0, 0.3);
text-decoration: line-through;
}
Logs need to be scannable, searchable, and filterable.
.log-viewer {
font-family: 'Zen Mono', monospace;
font-size: 13px;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
}
.log-entry {
display: grid;
grid-template-columns: auto auto 1fr auto;
gap: 1rem;
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--color-border);
align-items: center;
}
.log-entry:hover {
background: rgba(0, 0, 0, 0.03);
}
.log-timestamp {
color: var(--color-muted-foreground);
font-size: 12px;
user-select: none;
}
.log-level {
font-weight: 600;
text-transform: uppercase;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
.log-level-error {
background: rgba(255, 0, 0, 0.1);
color: #c53030;
}
.log-level-warn {
background: rgba(255, 165, 0, 0.1);
color: #dd6b20;
}
.log-level-info {
background: rgba(0, 123, 255, 0.1);
color: #2b6cb0;
}
.log-level-debug {
background: rgba(128, 128, 128, 0.1);
color: #718096;
}
.log-message {
font-family: 'Zen Mono', monospace;
word-break: break-word;
}
.log-metadata {
color: var(--color-muted-foreground);
font-size: 12px;
}
For 10,000+ log entries, use virtual scrolling:
import { FixedSizeList } from 'react-window';
function LogViewer({ logs }: { logs: LogEntry[] }) {
return (
<FixedSizeList
height={600}
itemCount={logs.length}
itemSize={30}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<LogEntry log={logs[index]} />
</div>
)}
</FixedSizeList>
);
}
function LogViewer({ logs }: { logs: LogEntry[] }) {
const [filter, setFilter] = useState({
levels: ['error', 'warn', 'info', 'debug'],
search: '',
});
const filteredLogs = useMemo(() => {
return logs.filter(log => {
if (!filter.levels.includes(log.level)) return false;
if (filter.search && !log.message.toLowerCase().includes(filter.search.toLowerCase())) {
return false;
}
return true;
});
}, [logs, filter]);
return (
<div>
<LogFilter filter={filter} setFilter={setFilter} />
<div className="log-entries">
{filteredLogs.map((log, i) => (
<LogEntry key={i} log={log} />
))}
</div>
</div>
);
}
Pretty-print JSON logs:
function LogMessage({ message }: { message: string }) {
// Try to parse as JSON
let parsed: any;
try {
parsed = JSON.parse(message);
} catch {
return <span>{message}</span>;
}
return (
<details>
<summary>JSON log (click to expand)</summary>
<pre style={{ marginTop: '0.5rem' }}>
{JSON.stringify(parsed, null, 2)}
</pre>
</details>
);
}