- {/* Terminal Preview */}
-
-
-
- {SAMPLE_LOGS.map((log, i) => (
-
- ))}
-
-
-
- Theme: {currentThemeName}
-
-
-
-
- {/* Browser Console Preview */}
-
-
-
-
- Browser Console
-
-
-
- {SAMPLE_LOGS.map((log, i) => (
-
- ))}
-
-
-
- Theme: {currentThemeName}
-
-
-
+
+
- {/* Integration Examples */}
Quick Integration
-
-
-
- {`import { getLogsDX } from 'logsdx'
+
-
-
- Theme: {currentThemeName}
-
-
-
-
-
-
-
-
- Auto Theme Detection
-
-
-
- {`import { getLogsDX } from 'logsdx'
+ />
+
-
-
- Theme: {currentThemeName}
-
-
-
+// Logs adapt to user's theme preference
+console.log(logger.processLine('[INFO] Adaptive theming'))`}
+ />
- {/* Popular Logger Integration */}
Logger Integration Examples
-
-
-
- {`import winston from 'winston'
+ {
return logsDX.processLine(info.message)
})
})`}
-
-
-
- Theme: {currentThemeName}
-
-
-
-
-
-
-
- {`import pino from 'pino'
+ />
+
-
-
- Theme: {currentThemeName}
-
-
-
-
-
-
-
-
- Console Override
-
-
-
- {`import { getLogsDX } from 'logsdx'
-
-const logsDX = getLogsDX('${currentThemeName}')
+ />
+ {
)
originalLog(...styled)
}`}
-
-
-
- Theme: {currentThemeName}
-
-
-
+ />
diff --git a/site/components/log-playground/OutputPane.tsx b/site/components/log-playground/OutputPane.tsx
new file mode 100644
index 0000000..08ce9d4
--- /dev/null
+++ b/site/components/log-playground/OutputPane.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import React from "react";
+import { Copy, Check } from "lucide-react";
+import { Button } from "@/components/ui/button";
+
+interface OutputPaneProps {
+ title: string;
+ content: string[];
+ backgroundColor: string;
+ isLoading?: boolean;
+ onCopy?: () => void;
+ isCopied?: boolean;
+}
+
+export function OutputPane({
+ title,
+ content,
+ backgroundColor,
+ isLoading = false,
+ onCopy,
+ isCopied = false,
+}: OutputPaneProps) {
+ return (
+
+
+ {title}
+ {onCopy && (
+
+ )}
+
+
+ {isLoading ? (
+
+ ) : (
+
+ {content.map((line, i) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/site/components/log-playground/ThemeSelector.tsx b/site/components/log-playground/ThemeSelector.tsx
new file mode 100644
index 0000000..6df715d
--- /dev/null
+++ b/site/components/log-playground/ThemeSelector.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import React from "react";
+import { AVAILABLE_THEMES, THEME_LABELS } from "./constants";
+
+interface ThemeSelectorProps {
+ value: string;
+ onChange: (theme: string) => void;
+}
+
+export function ThemeSelector({ value, onChange }: ThemeSelectorProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/site/components/log-playground/constants.ts b/site/components/log-playground/constants.ts
new file mode 100644
index 0000000..db27fa8
--- /dev/null
+++ b/site/components/log-playground/constants.ts
@@ -0,0 +1,32 @@
+export const DEFAULT_LOGS = `[2024-01-15 10:23:45] INFO: Application starting...
+[2024-01-15 10:23:46] DEBUG: Loading configuration from /etc/app/config.json
+[2024-01-15 10:23:47] SUCCESS: Database connected to postgres://localhost:5432/myapp
+[2024-01-15 10:23:48] WARN: Memory usage at 75% - consider scaling
+[2024-01-15 10:23:49] ERROR: Failed to connect to Redis: ECONNREFUSED 127.0.0.1:6379
+GET /api/users 200 OK (45ms)
+POST /api/auth/login 401 Unauthorized (12ms)
+{"level":"info","message":"User logged in","userId":123,"timestamp":"2024-01-15T10:23:50Z"}
+Processing batch job... [████████████████████] 100%
+✓ All 42 tests passed in 3.2s`;
+
+export const AVAILABLE_THEMES = [
+ "oh-my-zsh",
+ "dracula",
+ "nord",
+ "monokai",
+ "github-light",
+ "github-dark",
+ "solarized-light",
+ "solarized-dark",
+] as const;
+
+export const THEME_LABELS: Record
= {
+ "oh-my-zsh": "Oh My Zsh",
+ dracula: "Dracula",
+ nord: "Nord",
+ monokai: "Monokai",
+ "github-light": "GitHub Light",
+ "github-dark": "GitHub Dark",
+ "solarized-light": "Solarized Light",
+ "solarized-dark": "Solarized Dark",
+};
diff --git a/site/components/log-playground/index.tsx b/site/components/log-playground/index.tsx
new file mode 100644
index 0000000..40d2529
--- /dev/null
+++ b/site/components/log-playground/index.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import React, { useState, useCallback, useMemo } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { RotateCcw } from "lucide-react";
+import { useThemeProcessor } from "@/hooks/useThemeProcessor";
+import { ThemeSelector } from "./ThemeSelector";
+import { OutputPane } from "./OutputPane";
+import { DEFAULT_LOGS } from "./constants";
+import type { LogPlaygroundProps } from "./types";
+
+export function LogPlayground({
+ defaultTheme = "dracula",
+ defaultLogs = DEFAULT_LOGS,
+}: LogPlaygroundProps) {
+ const [inputText, setInputText] = useState(defaultLogs);
+ const [selectedTheme, setSelectedTheme] = useState(defaultTheme);
+ const [copiedHtml, setCopiedHtml] = useState(false);
+ const [copiedAnsi, setCopiedAnsi] = useState(false);
+
+ const logs = useMemo(
+ () => inputText.split("\n").filter((line) => line.trim()),
+ [inputText],
+ );
+
+ const { processedLogs, isLoading, theme } = useThemeProcessor(
+ selectedTheme,
+ logs,
+ );
+
+ const handleReset = useCallback(() => {
+ setInputText(defaultLogs);
+ }, [defaultLogs]);
+
+ const handleCopyHtml = useCallback(async () => {
+ const html = processedLogs.map((p) => p.html).join("\n");
+ await navigator.clipboard.writeText(html);
+ setCopiedHtml(true);
+ setTimeout(() => setCopiedHtml(false), 2000);
+ }, [processedLogs]);
+
+ const handleCopyAnsi = useCallback(async () => {
+ const ansi = processedLogs.map((p) => p.ansi).join("\n");
+ await navigator.clipboard.writeText(ansi);
+ setCopiedAnsi(true);
+ setTimeout(() => setCopiedAnsi(false), 2000);
+ }, [processedLogs]);
+
+ const backgroundColor = theme?.mode === "light" ? "#ffffff" : "#1e1e1e";
+
+ return (
+
+
+
+
Live Log Playground
+
+ Paste your logs below and see them transformed in real-time. The
+ same output works identically in your terminal and browser.
+
+
+
+
+
+
+ Try It Yourself
+
+
+
+
+
+
+ {/* Input */}
+
+
+
+
+ {/* Output Panes */}
+
+
+ p.html)}
+ backgroundColor={backgroundColor}
+ isLoading={isLoading}
+ onCopy={handleCopyHtml}
+ isCopied={copiedHtml}
+ />
+
+
+ p.html)}
+ backgroundColor={backgroundColor}
+ isLoading={isLoading}
+ onCopy={handleCopyAnsi}
+ isCopied={copiedAnsi}
+ />
+
+
+
+ {/* Code Example */}
+
+
Usage
+
+ {`import { getLogsDX } from 'logsdx';
+
+const logsdx = await getLogsDX({ theme: '${selectedTheme}' });
+console.log(logsdx.processLine('[INFO] Your log here'));`}
+
+
+
+
+
+
+
+ );
+}
+
+export type { LogPlaygroundProps } from "./types";
diff --git a/site/components/log-playground/types.ts b/site/components/log-playground/types.ts
new file mode 100644
index 0000000..520caae
--- /dev/null
+++ b/site/components/log-playground/types.ts
@@ -0,0 +1,9 @@
+export interface LogPlaygroundProps {
+ defaultTheme?: string;
+ defaultLogs?: string;
+}
+
+export interface ProcessedOutput {
+ html: string;
+ ansi: string;
+}
diff --git a/site/components/output-comparison/GhosttyTerminal.tsx b/site/components/output-comparison/GhosttyTerminal.tsx
new file mode 100644
index 0000000..01688e0
--- /dev/null
+++ b/site/components/output-comparison/GhosttyTerminal.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import React, { useEffect, useRef, useState } from "react";
+import type { GhosttyTerminalProps } from "./types";
+
+export function GhosttyTerminal({ ansiOutputs, isLoading }: GhosttyTerminalProps) {
+ const containerRef = useRef(null);
+ const terminalRef = useRef(null);
+ const [isInitialized, setIsInitialized] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let mounted = true;
+
+ async function initTerminal() {
+ if (!containerRef.current) return;
+
+ try {
+ const ghostty = await import("ghostty-web");
+ await ghostty.init();
+
+ if (!mounted || !containerRef.current) return;
+
+ containerRef.current.innerHTML = "";
+
+ const term = new ghostty.Terminal({
+ fontSize: 14,
+ fontFamily: "JetBrains Mono, Menlo, Monaco, Consolas, monospace",
+ theme: {
+ background: "#1e1e1e",
+ foreground: "#d4d4d4",
+ cursor: "#d4d4d4",
+ cursorAccent: "#1e1e1e",
+ selectionBackground: "#264f78",
+ black: "#000000",
+ red: "#cd3131",
+ green: "#0dbc79",
+ yellow: "#e5e510",
+ blue: "#2472c8",
+ magenta: "#bc3fbc",
+ cyan: "#11a8cd",
+ white: "#e5e5e5",
+ brightBlack: "#666666",
+ brightRed: "#f14c4c",
+ brightGreen: "#23d18b",
+ brightYellow: "#f5f543",
+ brightBlue: "#3b8eea",
+ brightMagenta: "#d670d6",
+ brightCyan: "#29b8db",
+ brightWhite: "#ffffff",
+ },
+ });
+
+ term.open(containerRef.current);
+ terminalRef.current = term;
+ setIsInitialized(true);
+ } catch (err) {
+ console.error("Failed to initialize Ghostty terminal:", err);
+ setError(err instanceof Error ? err.message : "Failed to load terminal");
+ }
+ }
+
+ initTerminal();
+
+ return () => {
+ mounted = false;
+ if (terminalRef.current && typeof (terminalRef.current as { dispose?: () => void }).dispose === "function") {
+ (terminalRef.current as { dispose: () => void }).dispose();
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!isInitialized || !terminalRef.current || isLoading) return;
+
+ const term = terminalRef.current as { write: (data: string) => void; clear: () => void };
+
+ term.clear();
+
+ for (const output of ansiOutputs) {
+ term.write(output + "\r\n");
+ }
+ }, [ansiOutputs, isInitialized, isLoading]);
+
+ if (error) {
+ return (
+
+
+
Terminal failed to load
+
{error}
+
+
+ );
+ }
+
+ if (isLoading || !isInitialized) {
+ return (
+
+
+
Loading terminal...
+
Powered by Ghostty WASM
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/site/components/output-comparison/constants.ts b/site/components/output-comparison/constants.ts
new file mode 100644
index 0000000..fd1f6af
--- /dev/null
+++ b/site/components/output-comparison/constants.ts
@@ -0,0 +1,43 @@
+import type { OutputTab } from "./types";
+
+export const SAMPLE_LOGS = [
+ "[INFO] Server started on port 3000",
+ "[WARN] Memory usage: 85%",
+ "[ERROR] Connection failed: timeout",
+ "[DEBUG] Request id=abc123 processed",
+ "[SUCCESS] Deploy complete ✓",
+];
+
+export const OUTPUT_TABS: { id: OutputTab; label: string; description: string }[] = [
+ {
+ id: "ansi-raw",
+ label: "ANSI (Raw)",
+ description: "Raw escape codes sent to terminal",
+ },
+ {
+ id: "ansi-rendered",
+ label: "ANSI (Terminal)",
+ description: "Real terminal rendering via Ghostty WASM",
+ },
+ {
+ id: "html-raw",
+ label: "HTML (Source)",
+ description: "Raw HTML markup",
+ },
+ {
+ id: "html-rendered",
+ label: "HTML (Browser)",
+ description: "How it looks in browser",
+ },
+];
+
+export const THEME_OPTIONS = [
+ "dracula",
+ "github-dark",
+ "github-light",
+ "nord",
+ "monokai",
+ "solarized-dark",
+ "solarized-light",
+ "oh-my-zsh",
+];
diff --git a/site/components/output-comparison/index.tsx b/site/components/output-comparison/index.tsx
new file mode 100644
index 0000000..9639238
--- /dev/null
+++ b/site/components/output-comparison/index.tsx
@@ -0,0 +1,291 @@
+"use client";
+
+import React, { useState, useEffect, useMemo } from "react";
+import dynamic from "next/dynamic";
+import { getTheme, renderLine } from "logsdx";
+import { SAMPLE_LOGS, OUTPUT_TABS, THEME_OPTIONS } from "./constants";
+import type { OutputTab, ProcessedOutput } from "./types";
+
+const GhosttyTerminal = dynamic(
+ () => import("./GhosttyTerminal").then((mod) => mod.GhosttyTerminal),
+ {
+ ssr: false,
+ loading: () => (
+
+ Loading terminal...
+
+ ),
+ }
+);
+
+function escapeAnsiForDisplay(ansi: string): string {
+ return ansi
+ .replace(/\x1b/g, "\\x1b")
+ .replace(/\[/g, "[");
+}
+
+function escapeHtmlForDisplay(html: string): string {
+ return html
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+}
+
+export function OutputComparison() {
+ const [theme, setTheme] = useState("dracula");
+ const [activeTab, setActiveTab] = useState("ansi-raw");
+ const [outputs, setOutputs] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [customLog, setCustomLog] = useState("");
+
+ const logs = useMemo(() => {
+ if (customLog.trim()) {
+ return customLog.split("\n").filter(Boolean);
+ }
+ return SAMPLE_LOGS;
+ }, [customLog]);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function processLogs() {
+ setIsLoading(true);
+ try {
+ const loadedTheme = await getTheme(theme);
+
+ if (cancelled) return;
+
+ const results: ProcessedOutput[] = logs.map((log) => {
+ const ansi = renderLine(log, loadedTheme, {
+ outputFormat: "ansi",
+ });
+
+ const html = renderLine(log, loadedTheme, {
+ outputFormat: "html",
+ htmlStyleFormat: "css",
+ escapeHtml: true,
+ });
+
+ return {
+ ansi,
+ html,
+ ansiVisible: escapeAnsiForDisplay(ansi),
+ };
+ });
+
+ if (!cancelled) {
+ setOutputs(results);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ console.error("Failed to process logs:", err);
+ }
+ } finally {
+ if (!cancelled) {
+ setIsLoading(false);
+ }
+ }
+ }
+
+ processLogs();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [theme, logs]);
+
+ const renderContent = () => {
+ if (isLoading) {
+ return (
+
+ Processing...
+
+ );
+ }
+
+ switch (activeTab) {
+ case "ansi-raw":
+ return (
+
+ {outputs.map((output, i) => (
+
+ {output.ansiVisible}
+
+ ))}
+
+ );
+
+ case "ansi-rendered":
+ return (
+ o.ansi)}
+ isLoading={isLoading}
+ />
+ );
+
+ case "html-raw":
+ return (
+
+ {outputs.map((output, i) => (
+
+ {escapeHtmlForDisplay(output.html)}
+
+ ))}
+
+ );
+
+ case "html-rendered":
+ return (
+
+ {outputs.map((output, i) => (
+
+ ))}
+
+ );
+ }
+ };
+
+ return (
+
+
+
+
+
+ Real
+ {" "}
+ Output Comparison
+
+
+ See exactly what logsDX outputs for terminal vs browser
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Output Formats
+
+
+
+ ANSI:
+
+ Escape codes for terminals
+
+
+
+ HTML:
+
+ Styled spans for browsers
+
+
+
+
+
+
+
+
+ {OUTPUT_TABS.map((tab) => (
+
+ ))}
+
+
+
+ {OUTPUT_TABS.find((t) => t.id === activeTab)?.description}
+
+
+
+
+
+
+ {activeTab.includes("ansi") ? "Terminal" : "Browser"}
+
+
+
+ {renderContent()}
+
+
+
+
+
+
+ Terminal Output
+
+
+ logsdx.processLine(log)
+
+
+ outputFormat: "ansi"
+
+
+
+
+ Browser Output
+
+
+ logsdx.processLine(log)
+
+
+ outputFormat: "html"
+
+
+
+
+
+
+
+
+ );
+}
+
+export { GhosttyTerminal } from "./GhosttyTerminal";
+export type { OutputComparisonProps, OutputTab, GhosttyTerminalProps } from "./types";
diff --git a/site/components/output-comparison/types.ts b/site/components/output-comparison/types.ts
new file mode 100644
index 0000000..2ea7793
--- /dev/null
+++ b/site/components/output-comparison/types.ts
@@ -0,0 +1,16 @@
+export interface OutputComparisonProps {
+ initialTheme?: string;
+}
+
+export interface ProcessedOutput {
+ ansi: string;
+ html: string;
+ ansiVisible: string;
+}
+
+export type OutputTab = "ansi-raw" | "ansi-rendered" | "html-raw" | "html-rendered";
+
+export interface GhosttyTerminalProps {
+ ansiOutputs: string[];
+ isLoading: boolean;
+}
diff --git a/site/components/schema-viz/constants.ts b/site/components/schema-viz/constants.ts
new file mode 100644
index 0000000..1a89c4f
--- /dev/null
+++ b/site/components/schema-viz/constants.ts
@@ -0,0 +1,136 @@
+import type { SchemaSection } from "./types";
+
+export const SCHEMA_SECTIONS: SchemaSection[] = [
+ {
+ title: "Theme",
+ description: "Root theme object that defines styling rules",
+ properties: [
+ {
+ name: "name",
+ type: "string",
+ description: "Unique identifier for the theme",
+ required: true,
+ example: '"dracula"',
+ },
+ {
+ name: "description",
+ type: "string",
+ description: "Human-readable description",
+ example: '"A dark theme inspired by Dracula"',
+ },
+ {
+ name: "mode",
+ type: '"light" | "dark" | "auto"',
+ description: "Theme color mode for terminal detection",
+ example: '"dark"',
+ },
+ {
+ name: "schema",
+ type: "SchemaConfig",
+ description: "Matching rules and styling definitions",
+ required: true,
+ },
+ {
+ name: "colors",
+ type: "Record",
+ description: "Named color palette for the theme",
+ example: '{ error: "#ff5555", info: "#8be9fd" }',
+ },
+ ],
+ },
+ {
+ title: "SchemaConfig",
+ description: "Defines how log content is matched and styled",
+ properties: [
+ {
+ name: "defaultStyle",
+ type: "StyleOptions",
+ description: "Default styling for unmatched content",
+ example: '{ color: "#f8f8f2" }',
+ },
+ {
+ name: "matchWords",
+ type: "Record",
+ description: "Exact word matches (case-insensitive)",
+ example: '{ "ERROR": { color: "#ff5555", styleCodes: ["bold"] } }',
+ },
+ {
+ name: "matchStartsWith",
+ type: "Record",
+ description: "Match tokens starting with a prefix",
+ example: '{ "[": { color: "#6272a4" } }',
+ },
+ {
+ name: "matchEndsWith",
+ type: "Record",
+ description: "Match tokens ending with a suffix",
+ example: '{ "ms": { color: "#bd93f9" } }',
+ },
+ {
+ name: "matchContains",
+ type: "Record",
+ description: "Match tokens containing a substring",
+ example: '{ "://": { color: "#8be9fd" } }',
+ },
+ {
+ name: "matchPatterns",
+ type: "PatternMatch[]",
+ description: "Regex patterns for complex matching",
+ example: '{ pattern: /\\d+\\.\\d+/, options: { color: "#bd93f9" } }',
+ },
+ ],
+ },
+ {
+ title: "StyleOptions",
+ description: "Styling applied to matched content",
+ properties: [
+ {
+ name: "color",
+ type: "string",
+ description: "Hex color code for the text",
+ required: true,
+ example: '"#ff5555"',
+ },
+ {
+ name: "styleCodes",
+ type: "StyleCode[]",
+ description: "Text decorations: bold, italic, underline, dim, etc.",
+ example: '["bold", "underline"]',
+ },
+ {
+ name: "htmlStyleFormat",
+ type: '"css" | "className"',
+ description: "HTML output format preference",
+ example: '"css"',
+ },
+ ],
+ },
+];
+
+export const MATCHING_PRIORITY = [
+ { name: "matchPatterns", description: "Checked first, highest priority" },
+ { name: "matchWords", description: "Exact word match" },
+ { name: "matchStartsWith", description: "Prefix match" },
+ { name: "matchEndsWith", description: "Suffix match" },
+ { name: "matchContains", description: "Substring match" },
+ { name: "defaultStyle", description: "Fallback for unmatched tokens" },
+];
+
+export const EXAMPLE_THEME = `{
+ "name": "my-theme",
+ "mode": "dark",
+ "schema": {
+ "defaultStyle": { "color": "#f8f8f2" },
+ "matchWords": {
+ "ERROR": { "color": "#ff5555", "styleCodes": ["bold"] },
+ "WARN": { "color": "#ffb86c" },
+ "INFO": { "color": "#8be9fd" }
+ },
+ "matchPatterns": [
+ {
+ "pattern": "\\\\d{4}-\\\\d{2}-\\\\d{2}",
+ "options": { "color": "#6272a4" }
+ }
+ ]
+ }
+}`;
diff --git a/site/components/schema-viz/index.tsx b/site/components/schema-viz/index.tsx
new file mode 100644
index 0000000..e9cb3db
--- /dev/null
+++ b/site/components/schema-viz/index.tsx
@@ -0,0 +1,159 @@
+"use client";
+
+import React, { useState } from "react";
+import { SCHEMA_SECTIONS, MATCHING_PRIORITY, EXAMPLE_THEME } from "./constants";
+
+export function SchemaVisualization() {
+ const [activeSection, setActiveSection] = useState(0);
+ const section = SCHEMA_SECTIONS[activeSection];
+
+ return (
+
+
+
+
+
+ Theme
+ {" "}
+ Schema
+
+
+ Understand how themes work under the hood
+
+
+
+
+
+ {SCHEMA_SECTIONS.map((s, i) => (
+
+ ))}
+
+
+
+
+ {section.title}
+
+
+ {section.description}
+
+
+
+ {section.properties.map((prop) => (
+
+
+
+ {prop.name}
+
+ {prop.required && (
+
+ required
+
+ )}
+
+ {prop.type}
+
+
+
+ {prop.description}
+
+ {prop.example && (
+
+ {prop.example}
+
+ )}
+
+ ))}
+
+
+
+
+
+ Matching Priority
+
+
+ {MATCHING_PRIORITY.map((item, i) => (
+
+
+ {i + 1}
+
+
+ {item.name}
+
+
+ {item.description}
+
+
+ ))}
+
+
+
+
+
+
+ Example Theme
+
+
+
+
+ {EXAMPLE_THEME}
+
+
+
+
+
+ How Matching Works
+
+
+ -
+ 1.
+ Log line is tokenized into individual words and symbols
+
+ -
+ 2.
+ Each token is checked against matching rules in priority order
+
+ -
+ 3.
+ First matching rule determines the token's style
+
+ -
+ 4.
+ Unmatched tokens use defaultStyle
+
+ -
+ 5.
+ Styled tokens are rendered as ANSI or HTML
+
+
+
+
+
+
+
+
+ );
+}
+
+export type { SchemaSection, SchemaProperty } from "./types";
diff --git a/site/components/schema-viz/types.ts b/site/components/schema-viz/types.ts
new file mode 100644
index 0000000..361886f
--- /dev/null
+++ b/site/components/schema-viz/types.ts
@@ -0,0 +1,13 @@
+export interface SchemaProperty {
+ name: string;
+ type: string;
+ description: string;
+ required?: boolean;
+ example?: string;
+}
+
+export interface SchemaSection {
+ title: string;
+ description: string;
+ properties: SchemaProperty[];
+}
diff --git a/site/components/theme-card.tsx b/site/components/theme-card.tsx
deleted file mode 100644
index 7ebed91..0000000
--- a/site/components/theme-card.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-"use client";
-
-import React from "react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-
-interface ThemeCardProps {
- themeName: string;
- isVisible?: boolean;
-}
-
-// Enhanced sample logs with different levels for better theme demonstration
-const sampleLogs = [
- "[2024-01-15 10:23:45] INFO: Server started on port 3000",
- "GET /api/users 200 OK (123ms)",
- "WARN: Memory usage high: 85% (1.7GB/2GB)",
- "[ERROR] Database connection failed: ECONNREFUSED 127.0.0.1:5432",
- "DEBUG: SQL Query executed in 45ms",
- "✓ All tests passed (42 tests, 0 failures)",
- "Processing batch job... [████████████████████] 100%",
- "🚀 Deployment completed to production environment",
-];
-
-// Format theme name for display
-const formatThemeName = (name: string) => {
- return name
- .split("-")
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(" ");
-};
-
-// Get theme colors with proper contrast - using direct theme definitions
-const getThemeColors = (themeName: string) => {
- const themeColors: Record<
- string,
- { bg: string; text: string; border: string; mode: "light" | "dark" }
- > = {
- "oh-my-zsh": {
- bg: "#2c3e50",
- text: "#ecf0f1",
- border: "#34495e",
- mode: "dark",
- },
- dracula: {
- bg: "#282a36",
- text: "#f8f8f2",
- border: "#44475a",
- mode: "dark",
- },
- "github-light": {
- bg: "#ffffff",
- text: "#1f2328",
- border: "#d1d9e0",
- mode: "light",
- },
- "github-dark": {
- bg: "#0d1117",
- text: "#e6edf3",
- border: "#30363d",
- mode: "dark",
- },
- "solarized-light": {
- bg: "#fdf6e3",
- text: "#657b83",
- border: "#eee8d5",
- mode: "light",
- },
- "solarized-dark": {
- bg: "#002b36",
- text: "#839496",
- border: "#073642",
- mode: "dark",
- },
- nord: {
- bg: "#2e3440",
- text: "#eceff4",
- border: "#4c566a",
- mode: "dark",
- },
- monokai: {
- bg: "#272822",
- text: "#f8f8f2",
- border: "#75715e",
- mode: "dark",
- },
- };
-
- return (
- themeColors[themeName] || {
- bg: "#1a1a1a",
- text: "#ffffff",
- border: "#333333",
- mode: "dark",
- }
- );
-};
-
-// Simple theme-based log styling
-const getStyledLogs = (themeName: string) => {
- const colors = getThemeColors(themeName);
-
- // Theme-specific color mappings
- const getLogColors = (logType: string) => {
- const colorMaps: Record> = {
- "oh-my-zsh": {
- info: "#3498db",
- warn: "#f39c12",
- error: "#e74c3c",
- success: "#27ae60",
- debug: "#2ecc71",
- },
- dracula: {
- info: "#8be9fd",
- warn: "#ffb86c",
- error: "#ff5555",
- success: "#50fa7b",
- debug: "#bd93f9",
- },
- "github-light": {
- info: "#0969da",
- warn: "#fb8500",
- error: "#cf222e",
- success: "#1f883d",
- debug: "#8250df",
- },
- "github-dark": {
- info: "#58a6ff",
- warn: "#f0883e",
- error: "#f85149",
- success: "#3fb950",
- debug: "#a5a5ff",
- },
- "solarized-light": {
- info: "#268bd2",
- warn: "#cb4b16",
- error: "#dc322f",
- success: "#859900",
- debug: "#6c71c4",
- },
- "solarized-dark": {
- info: "#268bd2",
- warn: "#cb4b16",
- error: "#dc322f",
- success: "#859900",
- debug: "#6c71c4",
- },
- nord: {
- info: "#5e81ac",
- warn: "#d08770",
- error: "#bf616a",
- success: "#a3be8c",
- debug: "#b48ead",
- },
- monokai: {
- info: "#66d9ef",
- warn: "#fd971f",
- error: "#f92672",
- success: "#a6e22e",
- debug: "#ae81ff",
- },
- };
-
- return colorMaps[themeName]?.[logType] || colors.text;
- };
-
- return sampleLogs.map((log) => {
- let styledLog = log;
-
- if (log.includes("WARN") || log.includes("Memory usage")) {
- styledLog = `${log}`;
- } else if (log.includes("ERROR") || log.includes("failed")) {
- styledLog = `${log}`;
- } else if (
- log.includes("✓") ||
- log.includes("🚀") ||
- log.includes("successful")
- ) {
- styledLog = `${log}`;
- } else if (log.includes("DEBUG")) {
- styledLog = `${log}`;
- } else {
- styledLog = `${log}`;
- }
-
- return styledLog;
- });
-};
-
-export function ThemeCard({ themeName, isVisible = true }: ThemeCardProps) {
- const colors = getThemeColors(themeName);
- const styledLogs = getStyledLogs(themeName);
-
- if (!isVisible) return null;
-
- return (
-
-
- {formatThemeName(themeName)}
-
-
- {/* Dual-pane layout */}
-
- {/* Browser Pane */}
-
-
- {/* Browser header */}
-
-
- Browser
-
-
-
- {/* Browser content with styled logs */}
-
- {styledLogs.slice(0, 6).map((log, i) => (
-
- ))}
-
-
-
-
- {/* Terminal Pane */}
-
-
- {/* Terminal header */}
-
-
- Terminal
-
-
-
- {/* Terminal content with styled logs */}
-
- {styledLogs.slice(0, 6).map((log, i) => (
-
- ))}
-
-
-
-
-
-
- );
-}
diff --git a/site/components/theme-card/LogPane/constants.ts b/site/components/theme-card/LogPane/constants.ts
new file mode 100644
index 0000000..3ea1bfb
--- /dev/null
+++ b/site/components/theme-card/LogPane/constants.ts
@@ -0,0 +1,9 @@
+export const HEADER_GRADIENTS = {
+ light: "linear-gradient(to bottom, rgba(0,0,0,0.05), transparent)",
+ dark: "linear-gradient(to bottom, rgba(255,255,255,0.1), transparent)",
+} as const;
+
+export const HEADER_TEXT_COLORS = {
+ light: "rgba(0,0,0,0.6)",
+ dark: "rgba(255,255,255,0.6)",
+} as const;
diff --git a/site/components/theme-card/LogPane/index.tsx b/site/components/theme-card/LogPane/index.tsx
new file mode 100644
index 0000000..b3d7b11
--- /dev/null
+++ b/site/components/theme-card/LogPane/index.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import React from "react";
+import type { LogPaneProps } from "./types";
+import { HEADER_GRADIENTS, HEADER_TEXT_COLORS } from "./constants";
+
+export function LogPane({
+ title,
+ logs,
+ backgroundColor,
+ mode,
+ isLoading = false,
+}: LogPaneProps) {
+ const headerGradient = HEADER_GRADIENTS[mode];
+ const headerText = HEADER_TEXT_COLORS[mode];
+
+ return (
+
+
+
+ {title}
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ logs.map((log, i) => (
+
+ ))
+ )}
+
+
+ );
+}
+
+export type { LogPaneProps } from "./types";
diff --git a/site/components/theme-card/LogPane/types.ts b/site/components/theme-card/LogPane/types.ts
new file mode 100644
index 0000000..7d9b4a7
--- /dev/null
+++ b/site/components/theme-card/LogPane/types.ts
@@ -0,0 +1,7 @@
+export interface LogPaneProps {
+ title: string;
+ logs: string[];
+ backgroundColor: string;
+ mode: "light" | "dark";
+ isLoading?: boolean;
+}
diff --git a/site/components/theme-card/constants.ts b/site/components/theme-card/constants.ts
new file mode 100644
index 0000000..1456e92
--- /dev/null
+++ b/site/components/theme-card/constants.ts
@@ -0,0 +1,26 @@
+import type { ThemeBackground } from "./types";
+
+export const SAMPLE_LOGS = [
+ "[2024-01-15 10:23:45] INFO: Server started on port 3000",
+ "GET /api/users 200 OK (123ms)",
+ "WARN: Memory usage high: 85% (1.7GB/2GB)",
+ "[ERROR] Database connection failed: ECONNREFUSED 127.0.0.1:5432",
+ "DEBUG: SQL Query executed in 45ms",
+ "SUCCESS: All tests passed (42 tests, 0 failures)",
+];
+
+export const THEME_BACKGROUNDS: Record = {
+ "oh-my-zsh": { bg: "#2c3e50", mode: "dark" },
+ dracula: { bg: "#282a36", mode: "dark" },
+ "github-light": { bg: "#ffffff", mode: "light" },
+ "github-dark": { bg: "#0d1117", mode: "dark" },
+ "solarized-light": { bg: "#fdf6e3", mode: "light" },
+ "solarized-dark": { bg: "#002b36", mode: "dark" },
+ nord: { bg: "#2e3440", mode: "dark" },
+ monokai: { bg: "#272822", mode: "dark" },
+};
+
+export const DEFAULT_BACKGROUND: ThemeBackground = {
+ bg: "#1a1a1a",
+ mode: "dark",
+};
diff --git a/site/components/theme-card/index.tsx b/site/components/theme-card/index.tsx
new file mode 100644
index 0000000..5fa2fbd
--- /dev/null
+++ b/site/components/theme-card/index.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import React, { useMemo } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useThemeProcessor } from "@/hooks/useThemeProcessor";
+import { LogPane } from "./LogPane";
+import { formatThemeName } from "./utils";
+import { SAMPLE_LOGS, THEME_BACKGROUNDS, DEFAULT_BACKGROUND } from "./constants";
+import type { ThemeCardProps } from "./types";
+
+export function ThemeCard({ themeName, isVisible = true }: ThemeCardProps) {
+ const logs = useMemo(() => SAMPLE_LOGS, []);
+ const { processedLogs, isLoading, theme } = useThemeProcessor(themeName, logs);
+ const colors = THEME_BACKGROUNDS[themeName] || DEFAULT_BACKGROUND;
+
+ if (!isVisible) return null;
+
+ const htmlLogs = processedLogs.map((log) => log.html);
+
+ return (
+
+
+
+ {formatThemeName(themeName)}
+ {theme?.mode && (
+
+ {theme.mode}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export type { ThemeCardProps } from "./types";
diff --git a/site/components/theme-card/types.ts b/site/components/theme-card/types.ts
new file mode 100644
index 0000000..85fd615
--- /dev/null
+++ b/site/components/theme-card/types.ts
@@ -0,0 +1,9 @@
+export interface ThemeCardProps {
+ themeName: string;
+ isVisible?: boolean;
+}
+
+export interface ThemeBackground {
+ bg: string;
+ mode: "light" | "dark";
+}
diff --git a/site/components/theme-card/utils.ts b/site/components/theme-card/utils.ts
new file mode 100644
index 0000000..94687bc
--- /dev/null
+++ b/site/components/theme-card/utils.ts
@@ -0,0 +1,6 @@
+export const formatThemeName = (name: string): string => {
+ return name
+ .split("-")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ");
+};
diff --git a/site/hooks/useThemeProcessor.ts b/site/hooks/useThemeProcessor.ts
new file mode 100644
index 0000000..2eb2987
--- /dev/null
+++ b/site/hooks/useThemeProcessor.ts
@@ -0,0 +1,138 @@
+import { useState, useEffect, useCallback } from "react";
+import { getTheme, renderLine } from "logsdx";
+import type { Theme } from "logsdx";
+
+interface ProcessedLog {
+ html: string;
+ ansi: string;
+}
+
+interface ThemeProcessorResult {
+ processedLogs: ProcessedLog[];
+ isLoading: boolean;
+ error: string | null;
+ theme: Theme | null;
+}
+
+const LOG_CACHE = new Map();
+
+export function useThemeProcessor(
+ themeName: string,
+ logs: string[],
+): ThemeProcessorResult {
+ const [processedLogs, setProcessedLogs] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [theme, setTheme] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function processLogs() {
+ const cacheKey = `${themeName}:${logs.join("|")}`;
+
+ if (LOG_CACHE.has(cacheKey)) {
+ setProcessedLogs(LOG_CACHE.get(cacheKey)!);
+ setIsLoading(false);
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const loadedTheme = await getTheme(themeName);
+
+ if (cancelled) return;
+ setTheme(loadedTheme);
+
+ const results: ProcessedLog[] = [];
+
+ for (const log of logs) {
+ if (cancelled) return;
+
+ const html = renderLine(log, loadedTheme, {
+ outputFormat: "html",
+ htmlStyleFormat: "css",
+ escapeHtml: true,
+ });
+
+ const ansi = renderLine(log, loadedTheme, {
+ outputFormat: "ansi",
+ });
+
+ results.push({ html, ansi });
+ }
+
+ LOG_CACHE.set(cacheKey, results);
+ setProcessedLogs(results);
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : "Failed to process logs");
+ }
+ } finally {
+ if (!cancelled) {
+ setIsLoading(false);
+ }
+ }
+ }
+
+ processLogs();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [themeName, logs]);
+
+ return { processedLogs, isLoading, error, theme };
+}
+
+export function useLogProcessor() {
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ const processLog = useCallback(
+ async (
+ log: string,
+ themeName: string,
+ format: "html" | "ansi" = "html",
+ ): Promise => {
+ setIsProcessing(true);
+ try {
+ const theme = await getTheme(themeName);
+ return renderLine(log, theme, {
+ outputFormat: format,
+ htmlStyleFormat: "css",
+ escapeHtml: true,
+ });
+ } finally {
+ setIsProcessing(false);
+ }
+ },
+ [],
+ );
+
+ const processLogs = useCallback(
+ async (
+ logs: string[],
+ themeName: string,
+ format: "html" | "ansi" = "html",
+ ): Promise => {
+ setIsProcessing(true);
+ try {
+ const theme = await getTheme(themeName);
+ return logs.map((log) =>
+ renderLine(log, theme, {
+ outputFormat: format,
+ htmlStyleFormat: "css",
+ escapeHtml: true,
+ })
+ );
+ } finally {
+ setIsProcessing(false);
+ }
+ },
+ [],
+ );
+
+ return { processLog, processLogs, isProcessing };
+}
diff --git a/site/next-env.d.ts b/site/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/site/next-env.d.ts
+++ b/site/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/site/package.json b/site/package.json
index 1766cf0..2a0aba8 100644
--- a/site/package.json
+++ b/site/package.json
@@ -32,6 +32,7 @@
"ansi-to-html": "^0.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "ghostty-web": "^0.4.0",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"idb": "^8.0.3",
diff --git a/site/tests/components/cli-demo/CliDemo.test.tsx b/site/tests/components/cli-demo/CliDemo.test.tsx
new file mode 100644
index 0000000..a3b7fe3
--- /dev/null
+++ b/site/tests/components/cli-demo/CliDemo.test.tsx
@@ -0,0 +1,54 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup, fireEvent } from "../../utils/test-utils";
+import { CliDemo } from "@/components/cli-demo";
+
+describe("CliDemo", () => {
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders section heading", () => {
+ render();
+ expect(screen.getByText("Powerful")).toBeDefined();
+ expect(screen.getByText("CLI")).toBeDefined();
+ });
+
+ it("renders package manager buttons", () => {
+ render();
+ expect(screen.getByText("npm")).toBeDefined();
+ expect(screen.getByText("pnpm")).toBeDefined();
+ expect(screen.getByText("bun")).toBeDefined();
+ });
+
+ it("renders all CLI features", () => {
+ render();
+ expect(screen.getByText("Pipe Logs")).toBeDefined();
+ expect(screen.getByText("Process Files")).toBeDefined();
+ expect(screen.getByText("Interactive Theme Creator")).toBeDefined();
+ expect(screen.getByText("Preview Themes")).toBeDefined();
+ });
+
+ it("changes install command when package manager is clicked", () => {
+ render();
+ const bunButton = screen.getByText("bun");
+ fireEvent.click(bunButton);
+ expect(screen.getByText("bun add -g logsdx")).toBeDefined();
+ });
+
+ it("changes terminal output when feature is clicked", () => {
+ render();
+ const processFilesButton = screen.getByText("Process Files");
+ fireEvent.click(processFilesButton);
+ expect(screen.getByText(/Processing app.log/)).toBeDefined();
+ });
+
+ it("shows terminal window with controls", () => {
+ const { container } = render();
+ const terminalDots = container.querySelectorAll(".rounded-full");
+ expect(terminalDots.length).toBeGreaterThanOrEqual(3);
+ });
+});
diff --git a/site/tests/components/interactive/CodeExample.test.tsx b/site/tests/components/interactive/CodeExample.test.tsx
new file mode 100644
index 0000000..f971822
--- /dev/null
+++ b/site/tests/components/interactive/CodeExample.test.tsx
@@ -0,0 +1,46 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup } from "../../utils/test-utils";
+import { CodeExample } from "@/components/interactive/CodeExample";
+
+describe("CodeExample", () => {
+ const defaultProps = {
+ title: "Basic Usage",
+ code: `import { getLogsDX } from 'logsdx'
+const logger = getLogsDX('dracula')`,
+ themeName: "dracula",
+ };
+
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders title", () => {
+ render();
+ expect(screen.getByText("Basic Usage")).toBeDefined();
+ });
+
+ it("renders code content", () => {
+ render();
+ expect(screen.getByText(/import.*getLogsDX/)).toBeDefined();
+ });
+
+ it("renders theme name in footer", () => {
+ render();
+ expect(screen.getByText("Theme: dracula")).toBeDefined();
+ });
+
+ it("renders window control buttons", () => {
+ const { container } = render();
+ const buttons = container.querySelectorAll(".rounded-full");
+ expect(buttons.length).toBe(3);
+ });
+
+ it("handles unknown theme gracefully", () => {
+ render();
+ expect(screen.getByText("Theme: unknown-theme")).toBeDefined();
+ });
+});
diff --git a/site/tests/components/interactive/PreviewPane.test.tsx b/site/tests/components/interactive/PreviewPane.test.tsx
new file mode 100644
index 0000000..4228117
--- /dev/null
+++ b/site/tests/components/interactive/PreviewPane.test.tsx
@@ -0,0 +1,64 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup } from "../../utils/test-utils";
+import { PreviewPane } from "@/components/interactive/PreviewPane";
+
+describe("PreviewPane", () => {
+ const defaultProps = {
+ title: "Terminal",
+ themeName: "dracula",
+ logs: ["INFO: Test log", "ERROR: Test error"],
+ backgroundColor: "#282a36",
+ headerBg: "#1e1f29",
+ borderColor: "#44475a",
+ };
+
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders title", () => {
+ render();
+ expect(screen.getByText("Terminal")).toBeDefined();
+ });
+
+ it("renders theme name in footer", () => {
+ render();
+ expect(screen.getByText("Theme: dracula")).toBeDefined();
+ });
+
+ it("renders logs as HTML", () => {
+ render();
+ expect(screen.getAllByText("INFO: Test log").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("ERROR: Test error").length).toBeGreaterThan(0);
+ });
+
+ it("shows loading state", () => {
+ render();
+ expect(screen.getByText("Loading...")).toBeDefined();
+ });
+
+ it("hides logs when loading", () => {
+ render();
+ expect(screen.queryByText("INFO: Test log")).toBeNull();
+ });
+
+ it("renders window control buttons", () => {
+ const { container } = render();
+ const buttons = container.querySelectorAll(".rounded-full");
+ expect(buttons.length).toBe(3);
+ });
+
+ it("renders with showBorder prop", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it("renders empty logs array", () => {
+ render();
+ expect(screen.getByText("Terminal")).toBeDefined();
+ });
+});
diff --git a/site/tests/components/interactive/ThemeControls.test.tsx b/site/tests/components/interactive/ThemeControls.test.tsx
new file mode 100644
index 0000000..6245c27
--- /dev/null
+++ b/site/tests/components/interactive/ThemeControls.test.tsx
@@ -0,0 +1,57 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
+import { render, screen, cleanup, fireEvent } from "../../utils/test-utils";
+import { ThemeControls } from "@/components/interactive/ThemeControls";
+
+describe("ThemeControls", () => {
+ const mockOnThemeChange = mock(() => {});
+ const mockOnColorModeChange = mock(() => {});
+
+ const defaultProps = {
+ selectedTheme: "GitHub",
+ colorMode: "system" as const,
+ isDarkOnly: false,
+ onThemeChange: mockOnThemeChange,
+ onColorModeChange: mockOnColorModeChange,
+ };
+
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ mockOnThemeChange.mockClear();
+ mockOnColorModeChange.mockClear();
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders all theme buttons", () => {
+ render();
+ expect(screen.getByText("GitHub")).toBeDefined();
+ expect(screen.getByText("Dracula")).toBeDefined();
+ expect(screen.getByText("Nord")).toBeDefined();
+ });
+
+ it("highlights selected theme", () => {
+ render();
+ const draculaButton = screen.getByText("Dracula");
+ expect(draculaButton.closest("button")).toBeDefined();
+ });
+
+ it("calls onThemeChange when theme button clicked", () => {
+ render();
+ fireEvent.click(screen.getByText("Dracula"));
+ expect(mockOnThemeChange).toHaveBeenCalledWith("Dracula");
+ });
+
+ it("renders color mode buttons when not dark only", () => {
+ const { container } = render();
+ const iconButtons = container.querySelectorAll("button.h-8.w-8");
+ expect(iconButtons.length).toBe(3);
+ });
+
+ it("hides color mode buttons when dark only", () => {
+ const { container } = render();
+ const iconButtons = container.querySelectorAll("button.h-8.w-8");
+ expect(iconButtons.length).toBe(0);
+ });
+});
diff --git a/site/tests/components/log-playground/LogPlayground.test.tsx b/site/tests/components/log-playground/LogPlayground.test.tsx
new file mode 100644
index 0000000..9397a5d
--- /dev/null
+++ b/site/tests/components/log-playground/LogPlayground.test.tsx
@@ -0,0 +1,61 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
+import { render, screen, cleanup, fireEvent } from "../../utils/test-utils";
+import { LogPlayground } from "@/components/log-playground";
+
+mock.module("@/hooks/useThemeProcessor", () => ({
+ useThemeProcessor: () => ({
+ processedLogs: [
+ { html: "INFO: Test", ansi: "\x1b[34mINFO: Test\x1b[0m" },
+ ],
+ isLoading: false,
+ error: null,
+ theme: { name: "dracula", mode: "dark" },
+ }),
+}));
+
+describe("LogPlayground", () => {
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders the playground title", () => {
+ render();
+ expect(screen.getByText("Live Log Playground")).toBeDefined();
+ });
+
+ it("renders theme selector", () => {
+ render();
+ expect(screen.getByLabelText("Theme:")).toBeDefined();
+ });
+
+ it("renders input textarea", () => {
+ render();
+ expect(screen.getByLabelText("Input Logs")).toBeDefined();
+ });
+
+ it("renders browser and terminal panes", () => {
+ render();
+ expect(screen.getByText("Browser Console")).toBeDefined();
+ expect(screen.getByText("Terminal")).toBeDefined();
+ });
+
+ it("renders reset button", () => {
+ render();
+ expect(screen.getByText("Reset")).toBeDefined();
+ });
+
+ it("shows usage code example", () => {
+ render();
+ expect(screen.getByText("Usage")).toBeDefined();
+ });
+
+ it("uses default theme from props", () => {
+ render();
+ const select = screen.getByLabelText("Theme:") as HTMLSelectElement;
+ expect(select.value).toBe("nord");
+ });
+});
diff --git a/site/tests/components/output-comparison/OutputComparison.test.tsx b/site/tests/components/output-comparison/OutputComparison.test.tsx
new file mode 100644
index 0000000..b391009
--- /dev/null
+++ b/site/tests/components/output-comparison/OutputComparison.test.tsx
@@ -0,0 +1,51 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup, fireEvent } from "../../utils/test-utils";
+import { OutputComparison } from "@/components/output-comparison";
+
+describe("OutputComparison", () => {
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders section heading", () => {
+ render();
+ expect(screen.getByText("Real")).toBeDefined();
+ expect(screen.getByText("Output Comparison")).toBeDefined();
+ });
+
+ it("renders all output tabs", () => {
+ render();
+ expect(screen.getByText("ANSI (Raw)")).toBeDefined();
+ expect(screen.getByText("ANSI (Terminal)")).toBeDefined();
+ expect(screen.getByText("HTML (Source)")).toBeDefined();
+ expect(screen.getByText("HTML (Browser)")).toBeDefined();
+ });
+
+ it("renders theme selector", () => {
+ render();
+ expect(screen.getByText("Theme")).toBeDefined();
+ expect(screen.getByRole("combobox")).toBeDefined();
+ });
+
+ it("renders custom log input", () => {
+ render();
+ expect(screen.getByPlaceholderText("Paste your own logs here...")).toBeDefined();
+ });
+
+ it("switches tabs when clicked", () => {
+ render();
+ const htmlRawTab = screen.getByText("HTML (Source)");
+ fireEvent.click(htmlRawTab);
+ expect(screen.getByText("Raw HTML markup")).toBeDefined();
+ });
+
+ it("shows format descriptions", () => {
+ render();
+ expect(screen.getByText("Escape codes for terminals")).toBeDefined();
+ expect(screen.getByText("Styled spans for browsers")).toBeDefined();
+ });
+});
diff --git a/site/tests/components/schema-viz/SchemaVisualization.test.tsx b/site/tests/components/schema-viz/SchemaVisualization.test.tsx
new file mode 100644
index 0000000..4d38271
--- /dev/null
+++ b/site/tests/components/schema-viz/SchemaVisualization.test.tsx
@@ -0,0 +1,61 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup, fireEvent } from "../../utils/test-utils";
+import { SchemaVisualization } from "@/components/schema-viz";
+
+describe("SchemaVisualization", () => {
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders section heading", () => {
+ render();
+ const themeElements = screen.getAllByText("Theme");
+ expect(themeElements.length).toBeGreaterThan(0);
+ expect(screen.getByText("Schema")).toBeDefined();
+ });
+
+ it("renders schema section tabs", () => {
+ render();
+ const schemaConfigElements = screen.getAllByText("SchemaConfig");
+ expect(schemaConfigElements.length).toBeGreaterThan(0);
+ const styleOptionsElements = screen.getAllByText("StyleOptions");
+ expect(styleOptionsElements.length).toBeGreaterThan(0);
+ });
+
+ it("shows Theme section by default", () => {
+ render();
+ expect(screen.getByText("Root theme object that defines styling rules")).toBeDefined();
+ });
+
+ it("switches sections when tab is clicked", () => {
+ render();
+ const schemaConfigButton = screen.getByRole("button", { name: "SchemaConfig" });
+ fireEvent.click(schemaConfigButton);
+ expect(screen.getByText("Defines how log content is matched and styled")).toBeDefined();
+ });
+
+ it("renders matching priority list", () => {
+ render();
+ expect(screen.getByText("Matching Priority")).toBeDefined();
+ const matchPatterns = screen.getAllByText("matchPatterns");
+ expect(matchPatterns.length).toBeGreaterThan(0);
+ const matchWords = screen.getAllByText("matchWords");
+ expect(matchWords.length).toBeGreaterThan(0);
+ });
+
+ it("renders example theme code", () => {
+ render();
+ expect(screen.getByText("Example Theme")).toBeDefined();
+ expect(screen.getByText("my-theme.json")).toBeDefined();
+ });
+
+ it("shows required badge for required properties", () => {
+ render();
+ const requiredBadges = screen.getAllByText("required");
+ expect(requiredBadges.length).toBeGreaterThan(0);
+ });
+});
diff --git a/site/tests/components/theme-card/LogPane.test.tsx b/site/tests/components/theme-card/LogPane.test.tsx
new file mode 100644
index 0000000..843a6df
--- /dev/null
+++ b/site/tests/components/theme-card/LogPane.test.tsx
@@ -0,0 +1,62 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup } from "../../utils/test-utils";
+import { LogPane } from "@/components/theme-card/LogPane";
+
+describe("LogPane", () => {
+ const defaultProps = {
+ title: "Browser",
+ logs: ["Log line 1", "Log line 2"],
+ backgroundColor: "#282a36",
+ mode: "dark" as const,
+ };
+
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders title", () => {
+ render();
+ expect(screen.getByText("Browser")).toBeDefined();
+ });
+
+ it("renders logs as HTML", () => {
+ render();
+ expect(screen.getAllByText("Log line 1").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("Log line 2").length).toBeGreaterThan(0);
+ });
+
+ it("shows loading state", () => {
+ render();
+ expect(screen.getByText("Loading...")).toBeDefined();
+ });
+
+ it("hides logs when loading", () => {
+ render();
+ expect(screen.queryByText("Log line 1")).toBeNull();
+ });
+
+ it("renders with dark mode", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it("renders with light mode", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeDefined();
+ });
+
+ it("applies background color", () => {
+ const { container } = render();
+ const pane = container.firstChild as HTMLElement;
+ expect(pane).toBeDefined();
+ });
+
+ it("renders empty logs array", () => {
+ render();
+ expect(screen.getByText("Browser")).toBeDefined();
+ });
+});
diff --git a/site/tests/components/theme-card/ThemeCard.test.tsx b/site/tests/components/theme-card/ThemeCard.test.tsx
new file mode 100644
index 0000000..8ddfada
--- /dev/null
+++ b/site/tests/components/theme-card/ThemeCard.test.tsx
@@ -0,0 +1,51 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
+import { render, screen, cleanup } from "../../utils/test-utils";
+import { ThemeCard } from "@/components/theme-card";
+
+mock.module("@/hooks/useThemeProcessor", () => ({
+ useThemeProcessor: () => ({
+ processedLogs: [
+ { html: "INFO: Test log", ansi: "INFO: Test log" },
+ { html: "ERROR: Test error", ansi: "ERROR: Test error" },
+ ],
+ isLoading: false,
+ error: null,
+ theme: { name: "dracula", mode: "dark" },
+ }),
+}));
+
+describe("ThemeCard", () => {
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders theme name formatted", () => {
+ render();
+ expect(screen.getByText("Oh My Zsh")).toBeDefined();
+ });
+
+ it("renders theme mode badge", () => {
+ render();
+ expect(screen.getByText("dark")).toBeDefined();
+ });
+
+ it("renders browser and terminal panes", () => {
+ render();
+ expect(screen.getByText("Browser")).toBeDefined();
+ expect(screen.getByText("Terminal")).toBeDefined();
+ });
+
+ it("returns null when not visible", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("renders processed logs", () => {
+ render();
+ expect(screen.getAllByText("INFO: Test log").length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/cli/index.ts b/src/cli/index.ts
index c948e50..6a6eb77 100755
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -15,6 +15,9 @@ import {
listPatternPresetsCommand,
} from "./theme-gen";
import { exportTheme, importTheme, listThemeFiles } from "./theme-gen";
+import { createLogger } from "../utils/logger";
+
+const log = createLogger("cli");
export function loadConfig(configPath?: string): LogsDXOptions {
const defaultConfig: LogsDXOptions = {
@@ -41,7 +44,7 @@ export function loadConfig(configPath?: string): LogsDXOptions {
}
}
} catch (error) {
- console.warn(`Failed to load config: ${error}`);
+ log.debug(`Failed to load config: ${error}`);
}
return defaultConfig;
diff --git a/src/index.ts b/src/index.ts
index 60f78f5..fdb4810 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -16,6 +16,7 @@ import {
} from "./themes";
import { validateTheme, validateThemeSafe } from "./schema/validator";
import { tokenize, applyTheme } from "./tokenizer";
+import { createLogger } from "./utils/logger";
import type { TokenList } from "./schema/types";
import type { RenderOptions } from "./renderer/types";
import type {
@@ -37,6 +38,8 @@ import {
getRecommendedThemeMode,
} from "./renderer";
+const log = createLogger("logsdx");
+
/**
* LogsDX - A powerful log processing and styling tool
*
@@ -161,9 +164,7 @@ export class LogsDX {
try {
return validateTheme(theme as Theme);
} catch (error) {
- if (this.options.debug) {
- console.warn("Invalid custom theme:", error);
- }
+ log.debug(`Invalid custom theme: ${error}`);
return {
name: "none",
@@ -307,9 +308,7 @@ export class LogsDX {
this.currentTheme = await this.resolveTheme(theme);
return true;
} catch (error) {
- if (this.options.debug) {
- console.warn("Invalid theme:", error);
- }
+ log.debug(`Invalid theme: ${error}`);
return false;
}
}
diff --git a/src/themes/index.ts b/src/themes/index.ts
index ed5065f..bf70fab 100644
--- a/src/themes/index.ts
+++ b/src/themes/index.ts
@@ -1,5 +1,4 @@
import type { Theme } from "../types";
-import { DEFAULT_THEME } from "./constants";
import {
createTheme,
createSimpleTheme,
diff --git a/src/themes/registry.ts b/src/themes/registry.ts
index 41fb32c..8c22636 100644
--- a/src/themes/registry.ts
+++ b/src/themes/registry.ts
@@ -1,4 +1,7 @@
import type { Theme } from "../types";
+import { createLogger } from "../utils/logger";
+
+const log = createLogger("themes");
type ThemeLoader = () => Promise<{ default: Theme }>;
@@ -40,7 +43,7 @@ class ThemeRegistry {
try {
await this.preloadTheme(this.defaultThemeName);
} catch (error) {
- console.warn("Failed to preload default theme:", error);
+ log.debug(`Failed to preload default theme: ${error}`);
}
}
diff --git a/src/tokenizer/index.ts b/src/tokenizer/index.ts
index cdbdd73..d1322a3 100644
--- a/src/tokenizer/index.ts
+++ b/src/tokenizer/index.ts
@@ -1,6 +1,7 @@
-import type { Token, TokenList } from "../schema/types";
+import type { Token, TokenList, StyleOptions } from "../schema/types";
import type { Theme } from "../types";
import type { MatcherType } from "./types";
+import { createLogger } from "../utils/logger";
import {
TIMESTAMP_PATTERN,
LOG_LEVEL_PATTERN,
@@ -38,6 +39,8 @@ import {
isValidMatchPatternsArray,
} from "./utils";
+const log = createLogger("tokenizer");
+
export class TokenContext {
public value?: unknown;
public ignored?: boolean;
@@ -269,7 +272,7 @@ export function addPatternMatchRules(
pattern: string | RegExp;
name?: string;
identifier?: string;
- options?: unknown;
+ options?: StyleOptions;
}>,
): void {
for (let index = 0; index < matchPatterns.length; index++) {
@@ -302,7 +305,7 @@ export function addPatternMatchRules(
: patternObj.pattern;
if (!regex) {
- console.warn(`Invalid regex pattern in theme: ${patternObj.pattern}`);
+ log.debug(`Invalid regex pattern in theme: ${patternObj.pattern}`);
continue;
}
@@ -348,7 +351,7 @@ export function addThemeRules(lexer: SimpleLexer, theme: Theme): void {
}>,
);
} else if (schema.matchPatterns) {
- console.warn("matchPatterns is not an array in theme schema");
+ log.debug("matchPatterns is not an array in theme schema");
}
}
@@ -468,7 +471,7 @@ export function tokenize(line: string, theme?: Theme): TokenList {
const lexerTokens = lexer.tokenize(line);
return convertLexerTokens(lexerTokens);
} catch (error) {
- console.warn("Tokenization failed:", error);
+ log.debug(`Tokenization failed: ${error}`);
return [createDefaultToken(line)];
}
}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 7458374..6721d2e 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -3,7 +3,8 @@ export type { Spinner } from "./spinner";
export { default as colors } from "./colors";
export { default as gradient } from "./gradient";
export { default as ascii } from "./ascii";
-export { default as logger } from "./logger";
+export { default as logger, createLogger, setLogLevel, getLogLevel } from "./logger";
+export type { LogLevel } from "./logger";
export { CONTRAST } from "./constants";
export {
hexContrastRatio,
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index 8041495..35cebcc 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -1,25 +1,74 @@
import colors from "./colors";
-export const logger = {
- info(message: string) {
- console.log(colors.blue("ℹ"), message);
- },
-
- success(message: string) {
- console.log(colors.green("✔"), message);
- },
-
- warn(message: string) {
- console.log(colors.yellow("⚠"), message);
- },
-
- error(message: string) {
- console.error(colors.red("✖"), message);
- },
-
- debug(message: string) {
- console.log(colors.gray("⚙"), message);
- },
+export type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
+
+const LOG_LEVEL_PRIORITY: Record = {
+ silent: 0,
+ error: 1,
+ warn: 2,
+ info: 3,
+ debug: 4,
+};
+
+interface LoggerConfig {
+ level: LogLevel;
+ prefix?: string;
+}
+
+let globalConfig: LoggerConfig = {
+ level: "info",
};
+function shouldLog(messageLevel: LogLevel): boolean {
+ return LOG_LEVEL_PRIORITY[messageLevel] <= LOG_LEVEL_PRIORITY[globalConfig.level];
+}
+
+function formatMessage(prefix: string | undefined, message: string): string {
+ return prefix ? `[${prefix}] ${message}` : message;
+}
+
+export function setLogLevel(level: LogLevel): void {
+ globalConfig.level = level;
+}
+
+export function getLogLevel(): LogLevel {
+ return globalConfig.level;
+}
+
+export function createLogger(prefix?: string) {
+ return {
+ info(message: string): void {
+ if (shouldLog("info")) {
+ console.log(colors.blue("ℹ"), formatMessage(prefix, message));
+ }
+ },
+
+ success(message: string): void {
+ if (shouldLog("info")) {
+ console.log(colors.green("✔"), formatMessage(prefix, message));
+ }
+ },
+
+ warn(message: string): void {
+ if (shouldLog("warn")) {
+ console.log(colors.yellow("⚠"), formatMessage(prefix, message));
+ }
+ },
+
+ error(message: string): void {
+ if (shouldLog("error")) {
+ console.error(colors.red("✖"), formatMessage(prefix, message));
+ }
+ },
+
+ debug(message: string): void {
+ if (shouldLog("debug")) {
+ console.log(colors.gray("⚙"), formatMessage(prefix, message));
+ }
+ },
+ };
+}
+
+export const logger = createLogger();
+
export default logger;
diff --git a/tests/unit/utils/logger.test.ts b/tests/unit/utils/logger.test.ts
index 06e8492..60ae82b 100644
--- a/tests/unit/utils/logger.test.ts
+++ b/tests/unit/utils/logger.test.ts
@@ -1,8 +1,17 @@
-import { describe, expect, test, spyOn, afterEach } from "bun:test";
-import { logger } from "../../../src/utils/logger";
+import { describe, expect, test, spyOn, afterEach, beforeEach } from "bun:test";
+import { logger, setLogLevel, getLogLevel } from "../../../src/utils/logger";
+import type { LogLevel } from "../../../src/utils/logger";
describe("logger", () => {
- afterEach(() => {});
+ let originalLevel: LogLevel;
+
+ beforeEach(() => {
+ originalLevel = getLogLevel();
+ });
+
+ afterEach(() => {
+ setLogLevel(originalLevel);
+ });
test("info() logs with blue info icon", () => {
const spy = spyOn(console, "log");
@@ -44,7 +53,8 @@ describe("logger", () => {
spy.mockRestore();
});
- test("debug() logs with gray gear icon", () => {
+ test("debug() logs with gray gear icon when log level is debug", () => {
+ setLogLevel("debug");
const spy = spyOn(console, "log");
logger.debug("debug message");
expect(spy).toHaveBeenCalledWith(
@@ -54,7 +64,16 @@ describe("logger", () => {
spy.mockRestore();
});
- test("all methods handle empty strings", () => {
+ test("debug() does not log when log level is info", () => {
+ setLogLevel("info");
+ const spy = spyOn(console, "log");
+ logger.debug("debug message");
+ expect(spy).not.toHaveBeenCalled();
+ spy.mockRestore();
+ });
+
+ test("all methods handle empty strings at debug level", () => {
+ setLogLevel("debug");
const logSpy = spyOn(console, "log");
const errorSpy = spyOn(console, "error");
@@ -83,4 +102,62 @@ describe("logger", () => {
logSpy.mockRestore();
});
+
+ test("setLogLevel and getLogLevel work correctly", () => {
+ setLogLevel("error");
+ expect(getLogLevel()).toBe("error");
+
+ setLogLevel("debug");
+ expect(getLogLevel()).toBe("debug");
+ });
+
+ test("silent level suppresses all output", () => {
+ setLogLevel("silent");
+ const logSpy = spyOn(console, "log");
+ const errorSpy = spyOn(console, "error");
+
+ logger.info("info");
+ logger.success("success");
+ logger.warn("warn");
+ logger.error("error");
+ logger.debug("debug");
+
+ expect(logSpy).not.toHaveBeenCalled();
+ expect(errorSpy).not.toHaveBeenCalled();
+
+ logSpy.mockRestore();
+ errorSpy.mockRestore();
+ });
+
+ test("error level only shows errors", () => {
+ setLogLevel("error");
+ const logSpy = spyOn(console, "log");
+ const errorSpy = spyOn(console, "error");
+
+ logger.info("info");
+ logger.warn("warn");
+ logger.error("error");
+
+ expect(logSpy).not.toHaveBeenCalled();
+ expect(errorSpy).toHaveBeenCalledTimes(1);
+
+ logSpy.mockRestore();
+ errorSpy.mockRestore();
+ });
+
+ test("warn level shows warn and error", () => {
+ setLogLevel("warn");
+ const logSpy = spyOn(console, "log");
+ const errorSpy = spyOn(console, "error");
+
+ logger.info("info");
+ logger.warn("warn");
+ logger.error("error");
+
+ expect(logSpy).toHaveBeenCalledTimes(1);
+ expect(errorSpy).toHaveBeenCalledTimes(1);
+
+ logSpy.mockRestore();
+ errorSpy.mockRestore();
+ });
});
From c691acd8e038ffb373c9fe3850896dcb5fec4379 Mon Sep 17 00:00:00 2001
From: Jeff Wainwright <1074042+yowainwright@users.noreply.github.com>
Date: Fri, 2 Jan 2026 03:42:43 -0800
Subject: [PATCH 2/4] chore: code tweaks
---
site/app/demos/page.tsx | 3 +-
site/bunfig.toml | 2 +-
site/components/cli-demo/index.tsx | 111 ++++++++++++++----
.../interactive/PreviewPane/index.tsx | 18 ++-
site/components/interactive/constants.ts | 5 +-
site/components/interactive/index.tsx | 15 ++-
.../output-comparison/GhosttyTerminal.tsx | 20 +++-
.../components/output-comparison/constants.ts | 6 +-
site/components/output-comparison/index.tsx | 22 ++--
site/components/output-comparison/types.ts | 6 +-
site/components/schema-viz/index.tsx | 3 +-
site/components/theme-card/index.tsx | 11 +-
site/hooks/useThemeProcessor.ts | 6 +-
site/test-setup.ts | 50 ++++++++
site/tests/__mocks__/logsdx.ts | 36 ++++--
.../interactive/PreviewPane.test.tsx | 4 +-
.../interactive/ThemeControls.test.tsx | 4 +-
.../log-playground/LogPlayground.test.tsx | 5 +-
.../OutputComparison.test.tsx | 4 +-
.../schema-viz/SchemaVisualization.test.tsx | 12 +-
.../components/theme-card/ThemeCard.test.tsx | 9 +-
src/utils/index.ts | 7 +-
src/utils/logger.ts | 4 +-
23 files changed, 288 insertions(+), 75 deletions(-)
diff --git a/site/app/demos/page.tsx b/site/app/demos/page.tsx
index 604e81f..1877a77 100644
--- a/site/app/demos/page.tsx
+++ b/site/app/demos/page.tsx
@@ -21,7 +21,8 @@ export default function DemosPage() {
Demos
- See logsDX in action with real terminal and browser output comparisons
+ See logsDX in action with real terminal and browser output
+ comparisons