Componenteditors1.05 KB gzipZero dependencies
Log Viewer
Scrolling log stream with level badges and filter chips
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add log-viewerOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { LogViewer } from "@/components/ui/log-viewer";
<LogViewer entries={entries} />Usage guidance
Use Log Viewer when a ticket stub surface needs Scrolling log stream with level badges and filter chips Keep props minimal and prefer composition with sibling stubs.
Avoid Log Viewer for long form flows or dense data grids where a dedicated layout block fits better.
Source code
log-viewer.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface LogEntry {
time?: string;
level: "INFO" | "WARN" | "ERROR";
message: string;
}
export interface LogViewerProps extends React.HTMLAttributes<HTMLDivElement> {
entries: LogEntry[];
className?: string;
}
export function LogViewer({
entries,
className,
...props
}: LogViewerProps) {
const [filterLevel, setFilterLevel] = React.useState<"INFO" | "WARN" | "ERROR" | "all">(
"all",
);
const filteredEntries = entries.filter(
(entry) => filterLevel === "all" || entry.level === filterLevel,
);
const levelClass = {
INFO: "bg-success text-success",
WARN: "bg-success text-success",
ERROR: "bg-destructive/10 text-destructive",
};
return (
<div
className={cn(
"w-full",
"motion-reduce:animate-none",
className,
)}
{...props}
>
{/* Filter chips */}
<div className="flex gap-2 mb-3">
<button
onClick={() => setFilterLevel("all")}
className={cn(
"px-3 py-1 rounded text-[9px] uppercase",
filterLevel === "all"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground transition-colors",
)}
>
All
</button>
<button
onClick={() => setFilterLevel("INFO")}
className={cn(
"px-3 py-1 rounded text-[9px] uppercase",
filterLevel === "INFO"
? "bg-success text-success"
: "text-muted-foreground hover:text-foreground transition-colors",
)}
>
INFO
</button>
<button
onClick={() => setFilterLevel("WARN")}
className={cn(
"px-3 py-1 rounded text-[9px] uppercase",
filterLevel === "WARN"
? "bg-success text-success"
: "text-muted-foreground hover:text-foreground transition-colors",
)}
>
WARN
</button>
<button
onClick={() => setFilterLevel("ERROR")}
className={cn(
"px-3 py-1 rounded text-[9px] uppercase",
filterLevel === "ERROR"
? "bg-destructive/10 text-destructive"
: "text-muted-foreground hover:text-foreground transition-colors",
)}
>
ERROR
</button>
</div>
{/* Log lines with fade-slide stagger */}
<div className="space-y-2 h-64 overflow-y-auto p-2 font-mono text-[10px]">
{filteredEntries.map((entry, idx) => {
const delay = idx * 30;
const levelBg = levelClass[entry.level];
return (
<div
key={entry.message}
className={cn(
"motion-reduce:transition-none",
`animate-[slide-in_0.2s_ease-out_both ${delay}ms fill mode]`,
levelBg,
"p-2 rounded",
)}
>
<span
className={cn(
"inline-block px-2 py-0.5 rounded text-[8px] uppercase",
levelBg,
"me-1",
)}
>
{entry.level}
</span>
<span className="ms-2 break-all">{entry.message}</span>
</div>
);
})}
{/* Placeholder for empty */}
{filteredEntries.length === 0 && (
<div className="pt-4 text-center text-muted-foreground">
No log entries match the current filter.
</div>
)}
</div>
</div>
);
}Props
| Prop | Type | Description |
|---|---|---|
| entries | LogEntry[] | |
| className? | string |