diffs-and-logs

Version control UIs and log viewers need clarity and scannability.

Diffs and Log Viewers

Version control UIs and log viewers need clarity and scannability.

Diff Viewers

Show changes clearly and elegantly.

Split View Layout

.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);
}

Unified Diff (Inline View)

.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;
  }
}

Character-Level Diff

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;
}

Log Viewers

Logs need to be scannable, searchable, and filterable.

Log Entry Structure

.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;
}

Virtual Scrolling

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>
  );
}

Log Filtering

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>
  );
}

JSON Log Formatting

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>
  );
}

Best Practices

Diffs:

Logs:

Don't: