diff --git a/bun.lock b/bun.lock
index c81936d..7f33149 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,9 +4,6 @@
"workspaces": {
"": {
"name": "logsdx",
- "dependencies": {
- "zod": "^3.25.74",
- },
"devDependencies": {
"@types/bun": "^1.2.11",
"mprocs": "^0.7.3",
@@ -14,7 +11,6 @@
"prettier": "^3.5.3",
"turbo": "^2.5.6",
"typescript": "^5.0.0",
- "zod-to-json-schema": "^3.24.6",
},
},
"site": {
@@ -37,6 +33,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",
@@ -508,6 +505,8 @@
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
+ "ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
+
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -944,10 +943,6 @@
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
- "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
-
- "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
-
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
diff --git a/docs/CREATE_THEME.md b/docs/CREATE_THEME.md
deleted file mode 100644
index 1ff8c0b..0000000
--- a/docs/CREATE_THEME.md
+++ /dev/null
@@ -1,171 +0,0 @@
-# LogsDX Interactive Theme Creator
-
-The LogsDX theme creator is an interactive CLI tool that guides you through creating custom themes for your log styling needs.
-
-## Getting Started
-
-Run the theme creator with:
-
-```bash
-bun run create-theme
-```
-
-> **Note:** The theme creator uses `@inquirer/prompts` for an enhanced interactive experience. This dependency is automatically installed with LogsDX.
-
-## Features
-
-### šØ Interactive Color Selection
-
-- Choose from predefined color presets (Vibrant, Pastel, Neon, Earth, Ocean)
-- Customize each color individually with live preview
-- Supports hex color codes
-
-### š Feature Presets
-
-Select which log features to highlight:
-
-- Log levels (ERROR, WARN, INFO, DEBUG)
-- Numbers and numeric values
-- Dates and timestamps
-- Boolean values (true, false, null)
-- Brackets and punctuation
-- Quoted strings
-
-### š Custom Patterns
-
-Add custom regex patterns to highlight specific text:
-
-- IP addresses
-- UUIDs
-- Email addresses
-- Custom identifiers
-
-### ⨠Custom Word Highlighting
-
-Highlight specific words with custom colors and styles:
-
-- Choose color from theme palette
-- Add styles: bold, italic, underline, blink
-
-### āæ Accessibility Features
-
-- Automatic WCAG compliance checking
-- Shows accessibility score and level (A, AA, AAA)
-- Auto-fix option for contrast issues
-- Validates theme colors for readability
-
-### š¾ Export Options
-
-Save your theme in multiple formats:
-
-- **JSON file**: For direct use with `--config`
-- **TypeScript file**: For importing in code
-- **Clipboard**: Quick copy for sharing
-- **Register**: Use immediately in current session
-
-## Example Workflow
-
-1. **Theme Information**
-
- ```
- Theme name: my-awesome-theme
- Description: A vibrant theme for production logs
- Mode: Dark
- ```
-
-2. **Color Configuration**
- - Select a preset or start from scratch
- - Customize each color with live preview
- - See sample logs rendered with your colors
-
-3. **Feature Selection**
- - Choose which log features to highlight
- - Add custom regex patterns for your use case
- - Define custom words with special styling
-
-4. **Accessibility Check**
- - Review WCAG compliance score
- - Auto-fix contrast issues if needed
- - Ensure readability across different displays
-
-5. **Save & Use**
-
- ```bash
- # Save as JSON
- logsdx --config ./themes/my-awesome-theme.json input.log
-
- # Or register for immediate use
- logsdx --theme my-awesome-theme input.log
- ```
-
-## Color Presets
-
-### Vibrant
-
-Bright, high-contrast colors perfect for dark terminals
-
-### Pastel
-
-Soft, muted colors that are easy on the eyes
-
-### Neon
-
-Bold, fluorescent colors for maximum visibility
-
-### Earth
-
-Natural, warm tones inspired by nature
-
-### Ocean
-
-Cool blues and teals with calming effect
-
-## Tips
-
-- Use the live preview to see how your theme looks with real log data
-- Test your theme with both light and dark terminal backgrounds
-- Consider accessibility - aim for at least AA compliance
-- Export to TypeScript for version control and sharing
-- Use custom patterns to highlight domain-specific content
-
-## Advanced Usage
-
-### Extending Existing Themes
-
-After creating a theme, you can extend it programmatically:
-
-```typescript
-import { extendTheme } from "logsdx";
-import { myAwesomeTheme } from "./themes/my-awesome-theme";
-
-const extendedTheme = extendTheme(myAwesomeTheme, {
- customWords: {
- CRITICAL: { color: "error", styleCodes: ["bold", "blink"] },
- },
-});
-```
-
-### Batch Theme Creation
-
-Create multiple theme variants:
-
-```typescript
-import { generateThemeVariants } from "logsdx";
-
-const { light, dark } = generateThemeVariants({
- name: "my-theme",
- colors: {
- /* ... */
- },
-});
-```
-
-## Integration
-
-Themes created with the interactive tool are fully compatible with:
-
-- LogsDX CLI (`--theme` flag)
-- Configuration files (`.logsdxrc`)
-- Programmatic API
-- Web browser rendering
-- CI/CD pipelines
diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md
deleted file mode 100644
index 79aee3c..0000000
--- a/docs/GETTING_STARTED.md
+++ /dev/null
@@ -1,476 +0,0 @@
-# Getting Started with LogsDX
-
-This guide will walk you through setting up and using LogsDX in your project step by step.
-
-## Table of Contents
-
-- [Prerequisites](#prerequisites)
-- [Installation](#installation)
-- [Quick Verification](#quick-verification)
-- [Step-by-Step Tutorial](#step-by-step-tutorial)
- - [Step 1: Basic Log Styling](#step-1-basic-log-styling)
- - [Step 2: Processing Log Files](#step-2-processing-log-files)
- - [Step 3: Real-time Log Streaming](#step-3-real-time-log-streaming)
- - [Step 4: Browser Integration](#step-4-browser-integration)
- - [Step 5: Custom Theme Creation](#step-5-custom-theme-creation)
- - [Step 6: Integration with Winston Logger](#step-6-integration-with-winston-logger)
- - [Step 7: Using the CLI Tool](#step-7-using-the-cli-tool)
-- [Common Patterns](#common-patterns)
- - [Pattern 1: Environment-based Themes](#pattern-1-environment-based-themes)
- - [Pattern 2: Multiple Output Formats](#pattern-2-multiple-output-formats)
- - [Pattern 3: Conditional Styling](#pattern-3-conditional-styling)
- - [Pattern 4: Log Level Filtering](#pattern-4-log-level-filtering)
-- [Testing Your Setup](#testing-your-setup)
-- [Troubleshooting](#troubleshooting)
-- [Next Steps](#next-steps)
-- [Resources](#resources)
-- [Quick Reference](#quick-reference)
-
-## Prerequisites
-
-- **Node.js 18+** or **Bun 1.0+**
-- Basic knowledge of JavaScript/TypeScript
-- A project with logs to style
-
-## Installation
-
-Choose your package manager:
-
-```bash
-# Using npm
-npm install logsdx
-
-# Using bun (recommended for faster installation)
-bun add logsdx
-
-# Using yarn
-yarn add logsdx
-
-# Using pnpm
-pnpm add logsdx
-```
-
-## Quick Verification
-
-After installation, verify LogsDX is working:
-
-```javascript
-// test-logsdx.js
-import { getLogsDX } from "logsdx";
-
-const logger = getLogsDX("dracula");
-console.log(logger.processLine("ERROR: Test message"));
-```
-
-Run it:
-
-```bash
-node test-logsdx.js
-# You should see "ERROR" in red and "Test message" in styled text
-```
-
-## Step-by-Step Tutorial
-
-### Step 1: Basic Log Styling
-
-Let's start with the simplest use case - styling console output:
-
-```javascript
-// 1-basic.js
-import { getLogsDX } from "logsdx";
-
-// Create a logger with the 'nord' theme
-const logger = getLogsDX("nord");
-
-// Style different log levels
-console.log(logger.processLine("INFO: Application starting..."));
-console.log(logger.processLine("WARN: Config file not found, using defaults"));
-console.log(logger.processLine("ERROR: Database connection failed"));
-console.log(logger.processLine("SUCCESS: Server started on port 3000"));
-```
-
-### Step 2: Processing Log Files
-
-Read and style existing log files:
-
-```javascript
-// 2-files.js
-import { getLogsDX } from "logsdx";
-import fs from "fs";
-
-const logger = getLogsDX("dracula");
-
-// Read a log file
-const logContent = fs.readFileSync("app.log", "utf-8");
-const lines = logContent.split("\n");
-
-// Process and display styled logs
-const styledLines = logger.processLines(lines);
-styledLines.forEach((line) => console.log(line));
-
-// Or save to a new file (ANSI codes included)
-fs.writeFileSync("styled-app.log", styledLines.join("\n"));
-```
-
-### Step 3: Real-time Log Streaming
-
-Style logs as they're generated:
-
-```javascript
-// 3-streaming.js
-import { getLogsDX } from "logsdx";
-import { createReadStream } from "fs";
-import { createInterface } from "readline";
-
-const logger = getLogsDX("monokai");
-
-// Stream and style logs line by line
-const rl = createInterface({
- input: createReadStream("server.log"),
- crlfDelay: Infinity,
-});
-
-rl.on("line", (line) => {
- console.log(logger.processLine(line));
-});
-```
-
-### Step 4: Browser Integration
-
-For web applications, use HTML output:
-
-```javascript
-// 4-browser.js
-import { LogsDX } from "logsdx";
-
-// Configure for HTML output
-const logger = LogsDX.getInstance({
- theme: "github-dark",
- outputFormat: "html",
- htmlStyleFormat: "css",
-});
-
-// Generate HTML with inline styles
-const htmlLog = logger.processLine("ERROR: User authentication failed");
-// Returns: ERROR: User authentication failed
-
-// For React/Vue/Angular
-document.getElementById("log-container").innerHTML = htmlLog;
-```
-
-### Step 5: Custom Theme Creation
-
-Create a theme tailored to your needs:
-
-```javascript
-// 5-custom-theme.js
-import { getLogsDX } from "logsdx";
-
-const customTheme = {
- name: "my-app-theme",
- mode: "dark",
- schema: {
- defaultStyle: {
- color: "#ffffff",
- },
- matchWords: {
- // Log levels
- FATAL: { color: "#ff0000", styleCodes: ["bold", "underline"] },
- ERROR: { color: "#ff5555", styleCodes: ["bold"] },
- WARN: { color: "#ffaa00" },
- INFO: { color: "#00aaff" },
- DEBUG: { color: "#888888", styleCodes: ["dim"] },
-
- // Custom keywords
- DATABASE: { color: "#00ff00", styleCodes: ["italic"] },
- API: { color: "#ff00ff", styleCodes: ["italic"] },
- CACHE: { color: "#ffff00" },
- },
- matchPatterns: [
- // Highlight timestamps
- {
- name: "timestamp",
- pattern: "\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}",
- options: { color: "#666666", styleCodes: ["dim"] },
- },
- // Highlight IPs
- {
- name: "ip",
- pattern: "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b",
- options: { color: "#cyan" },
- },
- // Highlight UUIDs
- {
- name: "uuid",
- pattern: "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
- options: { color: "#magenta", styleCodes: ["italic"] },
- },
- ],
- },
-};
-
-const logger = getLogsDX(customTheme);
-console.log(
- logger.processLine(
- "2024-01-15T10:30:00 ERROR: DATABASE connection to 192.168.1.100 failed",
- ),
-);
-```
-
-### Step 6: Integration with Winston Logger
-
-```javascript
-// 6-winston.js
-import winston from "winston";
-import { getLogsDX } from "logsdx";
-
-const logsDX = getLogsDX("dracula");
-
-// Create a custom Winston format
-const logsDXFormat = winston.format.printf(({ level, message, timestamp }) => {
- const logLine = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
- return logsDX.processLine(logLine);
-});
-
-// Configure Winston with LogsDX
-const logger = winston.createLogger({
- format: winston.format.combine(winston.format.timestamp(), logsDXFormat),
- transports: [new winston.transports.Console()],
-});
-
-// Use Winston as normal - output will be styled
-logger.info("Application started");
-logger.error("Connection failed");
-logger.warn("Low memory");
-```
-
-### Step 7: Using the CLI Tool
-
-After installing LogsDX globally, use the CLI:
-
-```bash
-# Install globally for CLI access
-npm install -g logsdx
-
-# Basic usage
-logsdx server.log
-
-# With specific theme
-logsdx server.log --theme dracula
-
-# Process from stdin
-tail -f /var/log/app.log | logsdx
-
-# Save styled output
-logsdx input.log --output styled.log --theme nord
-
-# List all themes
-logsdx --list-themes
-```
-
-## Common Patterns
-
-### Pattern 1: Environment-based Themes
-
-```javascript
-import { getLogsDX } from "logsdx";
-
-const theme = process.env.NODE_ENV === "production" ? "github-dark" : "dracula";
-const logger = getLogsDX(theme);
-```
-
-### Pattern 2: Multiple Output Formats
-
-```javascript
-import { getLogsDX } from "logsdx";
-
-const terminalLogger = getLogsDX("nord", { outputFormat: "ansi" });
-const htmlLogger = getLogsDX("nord", { outputFormat: "html" });
-
-const line = "ERROR: Critical failure";
-
-// For terminal
-console.log(terminalLogger.processLine(line));
-
-// For web dashboard
-const htmlOutput = htmlLogger.processLine(line);
-```
-
-### Pattern 3: Conditional Styling
-
-```javascript
-import { getLogsDX } from "logsdx";
-import chalk from "chalk";
-
-const logger = getLogsDX("dracula");
-
-function log(message, level = "INFO") {
- const styledMessage = logger.processLine(`${level}: ${message}`);
-
- if (level === "ERROR") {
- console.error(styledMessage);
- } else {
- console.log(styledMessage);
- }
-}
-
-log("Server started", "INFO");
-log("Database error", "ERROR");
-```
-
-### Pattern 4: Log Level Filtering
-
-```javascript
-import { getLogsDX } from "logsdx";
-
-const logger = getLogsDX("nord");
-const MIN_LEVEL = process.env.LOG_LEVEL || "INFO";
-
-const levels = {
- ERROR: 0,
- WARN: 1,
- INFO: 2,
- DEBUG: 3,
-};
-
-function shouldLog(line) {
- const match = line.match(/^(ERROR|WARN|INFO|DEBUG):/);
- if (!match) return true;
-
- const lineLevel = match[1];
- return levels[lineLevel] <= levels[MIN_LEVEL];
-}
-
-function processLog(line) {
- if (shouldLog(line)) {
- console.log(logger.processLine(line));
- }
-}
-```
-
-## Testing Your Setup
-
-Create a test file to verify everything works:
-
-```javascript
-// test-setup.js
-import { getLogsDX, getThemeNames, validateTheme } from "logsdx";
-
-// Test 1: List available themes
-console.log("Available themes:", getThemeNames());
-
-// Test 2: Process different log types
-const logger = getLogsDX("dracula");
-const testLines = [
- "INFO: Server starting on port 3000",
- "WARN: Deprecated API used",
- "ERROR: Connection timeout at 192.168.1.1",
- "DEBUG: Cache miss for key abc-123-def",
- "2024-01-15T10:30:00Z ERROR: Database connection failed",
-];
-
-console.log("\nStyled output:");
-testLines.forEach((line) => {
- console.log(logger.processLine(line));
-});
-
-// Test 3: Validate custom theme
-const customTheme = {
- name: "test",
- mode: "dark",
- schema: {
- defaultStyle: { color: "#fff" },
- matchWords: {
- ERROR: { color: "#f00" },
- },
- },
-};
-
-const validation = validateTheme(customTheme);
-console.log("\nTheme valid:", validation.success);
-```
-
-## Troubleshooting
-
-### Issue: Colors not showing in terminal
-
-**Solution:** Ensure your terminal supports ANSI colors. Set `FORCE_COLOR=1` environment variable:
-
-```bash
-FORCE_COLOR=1 node your-script.js
-```
-
-### Issue: CLI command not found
-
-**Solution:** Install globally or use npx:
-
-```bash
-# Global install
-npm install -g logsdx
-
-# Or use npx
-npx logsdx your-log-file.log
-```
-
-### Issue: Custom theme not working
-
-**Solution:** Validate your theme:
-
-```javascript
-import { validateTheme } from "logsdx";
-
-const result = validateTheme(yourTheme);
-if (!result.success) {
- console.error("Theme validation errors:", result.error);
-}
-```
-
-### Issue: HTML output shows raw tags
-
-**Solution:** Use proper HTML rendering:
-
-```javascript
-// React
-
;
-
-// Vanilla JS
-element.innerHTML = logger.processLine(log);
-```
-
-## Next Steps
-
-1. **Explore Themes**: Try different built-in themes to find your favorite
-2. **Create Custom Themes**: Build themes that match your brand
-3. **Integrate with Loggers**: Add LogsDX to your existing logging setup
-4. **Share Themes**: Contribute your themes to the community
-
-## Resources
-
-- [API Documentation](../README.md#api-reference)
-- [Theme Creation Guide](../README.md#creating-custom-themes)
-- [GitHub Repository](https://github.com/yowainwright/logsdx)
-- [Report Issues](https://github.com/yowainwright/logsdx/issues)
-
-## Quick Reference
-
-```javascript
-import {
- getLogsDX, // Get logger instance
- LogsDX, // Class for singleton pattern
- getThemeNames, // List available themes
- getTheme, // Get theme by name
- getAllThemes, // Get all theme objects
- registerTheme, // Register custom theme
- validateTheme, // Validate theme structure
- createTheme, // Create theme helper
- renderLightBox, // Light theme rendering
- checkWCAGCompliance, // Accessibility check
-} from "logsdx";
-
-// Configuration options
-const options = {
- outputFormat: "ansi", // "ansi" | "html"
- htmlStyleFormat: "css", // "css" | "className"
- debug: false, // Enable debug output
-};
-```
diff --git a/docs/TERMINAL_ADAPTATION.md b/docs/TERMINAL_ADAPTATION.md
deleted file mode 100644
index 92646fc..0000000
--- a/docs/TERMINAL_ADAPTATION.md
+++ /dev/null
@@ -1,111 +0,0 @@
-# Terminal Background Adaptation
-
-LogsDX automatically detects your terminal's background color and adjusts themes for optimal visibility. This prevents issues like dark text on dark backgrounds.
-
-## The Problem
-
-Light themes (like `github-light` or `solarized-light`) use dark text colors designed for light backgrounds. When used in dark terminals, the text becomes nearly invisible.
-
-## The Solution
-
-LogsDX includes automatic terminal background detection that:
-
-1. **Detects Terminal Background** - Checks environment variables and terminal programs
-2. **Adjusts Theme Selection** - Automatically uses dark variants in dark terminals
-3. **Modifies Text Colors** - Ensures text remains visible regardless of theme
-
-## How It Works
-
-```typescript
-// Automatic adjustment (default behavior)
-const logger = getLogsDX({ theme: "github-light" });
-// In dark terminal: automatically uses github-dark
-
-// Disable auto-adjustment if needed
-const logger = getLogsDX({
- theme: "github-light",
- autoAdjustTerminal: false,
-});
-```
-
-## Terminal Detection
-
-LogsDX detects dark terminals by checking:
-
-- `COLORFGBG` environment variable
-- `TERM_PROGRAM` (iTerm, Hyper, VSCode, etc.)
-- `TERMINAL_EMULATOR` settings
-- Common terminal indicators
-
-## Theme Mappings
-
-| Requested Theme | Dark Terminal | Light Terminal |
-| --------------- | -------------- | --------------- |
-| github-light | github-dark | github-light |
-| solarized-light | solarized-dark | solarized-light |
-| github-dark | github-dark | github-dark |
-| solarized-dark | solarized-dark | solarized-dark |
-| dracula | dracula | dracula |
-| oh-my-zsh | oh-my-zsh | oh-my-zsh |
-
-## Examples
-
-### Before (No Adaptation)
-
-```
-# github-light in dark terminal
-[Nearly invisible dark text on dark background]
-```
-
-### After (With Adaptation)
-
-```
-# github-light ā github-dark (auto-adjusted)
-[Clearly visible light text on dark background]
-```
-
-## Configuration Options
-
-```typescript
-interface LogsDXOptions {
- // Enable/disable automatic terminal adaptation
- autoAdjustTerminal?: boolean; // default: true
-}
-```
-
-## Testing Your Terminal
-
-Run this command to see what LogsDX detects:
-
-```bash
-bun run scripts/test-terminal-adaptation.ts
-```
-
-This will show:
-
-- Your terminal environment variables
-- The original vs adjusted theme
-- Side-by-side comparison of outputs
-
-## Best Practices
-
-1. **Use Theme Pairs** - Always provide both light and dark variants
-2. **Test Both Modes** - Check your logs in both light and dark terminals
-3. **Document Behavior** - Let users know about automatic adjustments
-4. **Provide Override** - Allow users to disable auto-adjustment if needed
-
-## Manual Theme Selection
-
-If you prefer manual control:
-
-```typescript
-// Detect terminal yourself
-const isDarkTerminal = process.env.COLORFGBG?.includes("0;");
-
-// Choose theme accordingly
-const themeName = isDarkTerminal ? "github-dark" : "github-light";
-const logger = getLogsDX({
- theme: themeName,
- autoAdjustTerminal: false,
-});
-```
diff --git a/package.json b/package.json
index a01a10e..2f05889 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"scripts": {
"setup": "bash scripts/setup.sh",
"clean": "rm -rf dist node_modules/.bun site/node_modules",
- "build": "bun run build:cjs && bun run build:esm && bun run build:cli && bun run build:types && bun run generate-schema",
+ "build": "bun run build:cjs && bun run build:esm && bun run build:cli && bun run build:types",
"build:cjs": "bun build src/index.ts --outfile dist/index.cjs --format cjs --minify",
"build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm --minify",
"build:cli": "bun build src/cli/bin.ts --outfile dist/cli.js --format cjs --minify --target node",
@@ -66,7 +66,6 @@
"format:check": "bun run prettier --check .",
"check": "bun run lint && bun run format:check && bun run test",
"generate-css": "bun run scripts/generate-css.ts",
- "generate-schema": "bun run scripts/generate-schema.ts",
"demo": "bun run demo-dracula-consistency.js",
"turbo:build": "turbo run build",
"turbo:dev": "turbo run dev",
@@ -91,10 +90,7 @@
"oxlint": "^0.15.0",
"prettier": "^3.5.3",
"turbo": "^2.5.6",
- "typescript": "^5.0.0",
- "zod-to-json-schema": "^3.24.6"
+ "typescript": "^5.0.0"
},
- "dependencies": {
- "zod": "^3.25.74"
- }
+ "dependencies": {}
}
diff --git a/scripts/build-themes.ts b/scripts/build-themes.ts
deleted file mode 100644
index c61a5c4..0000000
--- a/scripts/build-themes.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bun
-import { build } from "bun";
-import { readdirSync } from "fs";
-import { join } from "path";
-
-const themesDir = join(import.meta.dir, "../src/themes/presets");
-const outputDir = join(import.meta.dir, "../dist/themes/presets");
-
-const themeFiles = readdirSync(themesDir).filter((file) =>
- file.endsWith(".ts"),
-);
-
-console.log(`Building ${themeFiles.length} theme presets...`);
-
-const buildPromises = themeFiles.map(async (file) => {
- const themeName = file.replace(".ts", "");
- const inputPath = join(themesDir, file);
-
- await Promise.all([
- build({
- entrypoints: [inputPath],
- outdir: outputDir,
- format: "esm",
- minify: true,
- naming: `${themeName}.mjs`,
- target: "browser",
- }),
-
- build({
- entrypoints: [inputPath],
- outdir: outputDir,
- format: "cjs",
- minify: true,
- naming: `${themeName}.cjs`,
- target: "node",
- }),
- ]);
-
- console.log(`ā Built theme: ${themeName}`);
-});
-
-await Promise.all(buildPromises);
-
-console.log("ā All themes built successfully!");
diff --git a/scripts/generate-schema.ts b/scripts/generate-schema.ts
deleted file mode 100644
index c2bc28c..0000000
--- a/scripts/generate-schema.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/env bun
-
-import fs from "node:fs/promises";
-import path from "node:path";
-import { zodToJsonSchema } from "zod-to-json-schema";
-import {
- tokenSchema,
- tokenListSchema,
- themePresetSchema,
- styleOptionsSchema,
- schemaConfigSchema,
-} from "../src/schema/index";
-import { JsonSchemaOptions } from "../src/schema/types";
-
-async function main() {
- const args = process.argv.slice(2);
- const outputDir = args[0] || "dist/schemas";
-
- const packageJson = JSON.parse(
- await fs.readFile(path.join(process.cwd(), "package.json"), "utf-8"),
- );
- const version = packageJson.version;
-
- try {
- await fs.mkdir(outputDir, { recursive: true });
- console.log(`Creating schemas in ${outputDir}`);
-
- const schemas = [
- {
- name: "token",
- schema: tokenSchema,
- filename: "token.schema.json",
- },
- {
- name: "token-list",
- schema: tokenListSchema,
- filename: "token-list.schema.json",
- },
- {
- name: "theme",
- schema: themePresetSchema,
- filename: "theme.schema.json",
- },
- {
- name: "style-options",
- schema: styleOptionsSchema,
- filename: "style-options.schema.json",
- },
- {
- name: "schema-config",
- schema: schemaConfigSchema,
- filename: "schema-config.schema.json",
- },
- ];
-
- for (const { name, schema, filename } of schemas) {
- console.log(`Generating ${name} schema...`);
-
- const jsonSchema = zodToJsonSchema(schema, {
- name,
- $refStrategy: "relative",
- definitionPath: "$defs",
- definitions: {},
- } as JsonSchemaOptions);
-
- // Add version to the schema
- jsonSchema.version = version;
-
- const outputPath = path.join(outputDir, filename);
- await fs.writeFile(outputPath, JSON.stringify(jsonSchema, null, 2));
- console.log(`ā ${filename}`);
- }
-
- console.log(`\nā
Successfully generated ${schemas.length} schema files`);
- } catch (error) {
- console.error("Error generating schemas:", error);
- process.exit(1);
- }
-}
-
-if (import.meta.main) {
- main();
-}
diff --git a/scripts/remove-comments.ts b/scripts/remove-comments.ts
deleted file mode 100644
index 157b26e..0000000
--- a/scripts/remove-comments.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/env bun
-
-import { readdir, readFile, writeFile } from "fs/promises";
-import { join } from "path";
-
-async function getAllTsFiles(dir: string): Promise {
- const files: string[] = [];
- const entries = await readdir(dir, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = join(dir, entry.name);
- if (entry.isDirectory()) {
- files.push(...(await getAllTsFiles(fullPath)));
- } else if (entry.name.endsWith(".ts")) {
- files.push(fullPath);
- }
- }
-
- return files;
-}
-
-function removeComments(code: string): string {
- let result = "";
- let i = 0;
- let inString = false;
- let stringChar = "";
- let inTemplate = false;
-
- while (i < code.length) {
- const char = code[i];
- const nextChar = code[i + 1];
-
- if (!inString && !inTemplate && char === "/" && nextChar === "/") {
- const lineEnd = code.indexOf("\n", i);
- if (lineEnd === -1) {
- break;
- }
- result += "\n";
- i = lineEnd + 1;
- continue;
- }
-
- if (!inString && !inTemplate && char === "/" && nextChar === "*") {
- const commentEnd = code.indexOf("*/", i + 2);
- if (commentEnd === -1) {
- break;
- }
- const commentContent = code.substring(i, commentEnd + 2);
- const newlines = (commentContent.match(/\n/g) || []).length;
- result += "\n".repeat(newlines);
- i = commentEnd + 2;
- continue;
- }
-
- if (!inTemplate && (char === '"' || char === "'")) {
- if (!inString) {
- inString = true;
- stringChar = char;
- } else if (char === stringChar && code[i - 1] !== "\\") {
- inString = false;
- stringChar = "";
- }
- }
-
- if (!inString && char === "`") {
- inTemplate = !inTemplate;
- }
-
- result += char;
- i++;
- }
-
- return result;
-}
-
-async function processFile(filePath: string) {
- console.log(`Processing: ${filePath}`);
- const content = await readFile(filePath, "utf-8");
- const cleaned = removeComments(content);
- await writeFile(filePath, cleaned, "utf-8");
-}
-
-async function main() {
- const srcFiles = await getAllTsFiles("src");
- const testFiles = await getAllTsFiles("tests");
- const allFiles = [...srcFiles, ...testFiles];
-
- console.log(`Found ${allFiles.length} TypeScript files`);
-
- for (const file of allFiles) {
- await processFile(file);
- }
-
- console.log("Done!");
-}
-
-main().catch(console.error);
diff --git a/site/app/demos/page.tsx b/site/app/demos/page.tsx
new file mode 100644
index 0000000..1877a77
--- /dev/null
+++ b/site/app/demos/page.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { Navbar } from "@/components/navbar";
+import { OutputComparison } from "@/components/output-comparison";
+import { LogPlayground } from "@/components/log-playground";
+import { CliDemo } from "@/components/cli-demo";
+import { SchemaVisualization } from "@/components/schema-viz";
+
+export default function DemosPage() {
+ return (
+
+
+
+
+
+
+
+
+ Live
+ {" "}
+ Demos
+
+
+ See logsDX in action with real terminal and browser output
+ comparisons
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/site/bunfig.toml b/site/bunfig.toml
index d5e19ca..2c651e1 100644
--- a/site/bunfig.toml
+++ b/site/bunfig.toml
@@ -1,4 +1,4 @@
[test]
preload = ["./test-setup.ts"]
root = "./tests"
-isolate = true
+isolate = false
diff --git a/site/components/cli-demo/constants.ts b/site/components/cli-demo/constants.ts
new file mode 100644
index 0000000..7060470
--- /dev/null
+++ b/site/components/cli-demo/constants.ts
@@ -0,0 +1,50 @@
+import type { CliFeature } from "./types";
+
+export const CLI_FEATURES: CliFeature[] = [
+ {
+ title: "Pipe Logs",
+ description: "Pipe any log output through logsdx for instant styling",
+ command: "cat server.log | logsdx --theme dracula",
+ },
+ {
+ title: "Process Files",
+ description: "Process log files directly with your chosen theme",
+ command: "logsdx process app.log --theme github-dark",
+ },
+ {
+ title: "Interactive Theme Creator",
+ description: "Create custom themes with an interactive wizard",
+ command: "logsdx create-theme",
+ },
+ {
+ title: "Preview Themes",
+ description: "Preview any theme with sample logs before using",
+ command: "logsdx preview --theme nord",
+ },
+ {
+ title: "Tail with Style",
+ description: "Follow logs in real-time with beautiful styling",
+ command: "tail -f /var/log/app.log | logsdx --theme monokai",
+ },
+ {
+ title: "List Themes",
+ description: "View all available built-in themes",
+ command: "logsdx themes",
+ },
+];
+
+export const INSTALL_COMMANDS = {
+ npm: "npm install -g logsdx",
+ pnpm: "pnpm add -g logsdx",
+ bun: "bun add -g logsdx",
+} as const;
+
+export const TERMINAL_COLORS = {
+ bg: "#1a1b26",
+ headerBg: "#24283b",
+ border: "#414868",
+ text: "#a9b1d6",
+ prompt: "#7aa2f7",
+ command: "#9ece6a",
+ output: "#787c99",
+} as const;
diff --git a/site/components/cli-demo/index.tsx b/site/components/cli-demo/index.tsx
new file mode 100644
index 0000000..7110845
--- /dev/null
+++ b/site/components/cli-demo/index.tsx
@@ -0,0 +1,283 @@
+"use client";
+
+import React, { useState } from "react";
+import { CLI_FEATURES, INSTALL_COMMANDS, TERMINAL_COLORS } from "./constants";
+
+type PackageManager = keyof typeof INSTALL_COMMANDS;
+
+export function CliDemo() {
+ const [activeFeature, setActiveFeature] = useState(0);
+ const [packageManager, setPackageManager] = useState("npm");
+
+ const feature = CLI_FEATURES[activeFeature];
+
+ return (
+
+
+
+
+
+ Powerful
+ {" "}
+ CLI
+
+
+ Style your logs from anywhere with a single command
+
+
+
+
+ {(Object.keys(INSTALL_COMMANDS) as PackageManager[]).map((pm) => (
+
+ ))}
+
+
+ $
+
+ {INSTALL_COMMANDS[packageManager]}
+
+
+
+
+
+
+ {CLI_FEATURES.map((f, i) => (
+
+ ))}
+
+
+
+
+
+
+ ~ $
+
+ {feature.command}
+
+
+
+ {activeFeature === 0 && (
+
+ )}
+ {activeFeature === 1 && (
+
+ )}
+ {activeFeature === 2 && (
+
+ )}
+ {activeFeature === 3 && (
+
+ )}
+ {activeFeature === 4 && (
+
+ )}
+ {activeFeature === 5 && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+interface TerminalLine {
+ text: string;
+ color: string;
+ blink?: boolean;
+}
+
+function TerminalOutput({ lines }: { lines: TerminalLine[] }) {
+ return (
+ <>
+ {lines.map((line, i) => (
+
+ {line.text}
+
+ ))}
+ >
+ );
+}
+
+export type { CliFeature } from "./types";
diff --git a/site/components/cli-demo/types.ts b/site/components/cli-demo/types.ts
new file mode 100644
index 0000000..1889381
--- /dev/null
+++ b/site/components/cli-demo/types.ts
@@ -0,0 +1,10 @@
+export interface CliCommand {
+ command: string;
+ description: string;
+}
+
+export interface CliFeature {
+ title: string;
+ description: string;
+ command: string;
+}
diff --git a/site/components/interactive/CodeExample/constants.ts b/site/components/interactive/CodeExample/constants.ts
new file mode 100644
index 0000000..12b6b0c
--- /dev/null
+++ b/site/components/interactive/CodeExample/constants.ts
@@ -0,0 +1,18 @@
+import type { ThemeBackground } from "./types";
+
+export const THEME_BACKGROUNDS: Record = {
+ "github-light": { bg: "#ffffff", headerBg: "#f6f8fa", border: "#d1d9e0" },
+ "github-dark": { bg: "#0d1117", headerBg: "#161b22", border: "#30363d" },
+ "solarized-light": { bg: "#fdf6e3", headerBg: "#eee8d5", border: "#eee8d5" },
+ "solarized-dark": { bg: "#002b36", headerBg: "#073642", border: "#073642" },
+ dracula: { bg: "#282a36", headerBg: "#1e1f29", border: "#44475a" },
+ nord: { bg: "#2e3440", headerBg: "#3b4252", border: "#4c566a" },
+ monokai: { bg: "#272822", headerBg: "#3e3d32", border: "#75715e" },
+ "oh-my-zsh": { bg: "#2c3e50", headerBg: "#34495e", border: "#34495e" },
+};
+
+export const DEFAULT_BACKGROUND: ThemeBackground = {
+ bg: "#0d1117",
+ headerBg: "#161b22",
+ border: "#30363d",
+};
diff --git a/site/components/interactive/CodeExample/index.tsx b/site/components/interactive/CodeExample/index.tsx
new file mode 100644
index 0000000..98594da
--- /dev/null
+++ b/site/components/interactive/CodeExample/index.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import React from "react";
+import { THEME_BACKGROUNDS, DEFAULT_BACKGROUND } from "./constants";
+import type { CodeExampleProps } from "./types";
+
+export function CodeExample({ title, code, themeName }: CodeExampleProps) {
+ const bg = THEME_BACKGROUNDS[themeName] || DEFAULT_BACKGROUND;
+
+ return (
+
+
+
+
+ Theme: {themeName}
+
+
+ );
+}
+
+export type { CodeExampleProps } from "./types";
diff --git a/site/components/interactive/CodeExample/types.ts b/site/components/interactive/CodeExample/types.ts
new file mode 100644
index 0000000..2312cc0
--- /dev/null
+++ b/site/components/interactive/CodeExample/types.ts
@@ -0,0 +1,11 @@
+export interface CodeExampleProps {
+ title: string;
+ code: string;
+ themeName: string;
+}
+
+export interface ThemeBackground {
+ bg: string;
+ headerBg: string;
+ border: string;
+}
diff --git a/site/components/interactive/PreviewPane/constants.ts b/site/components/interactive/PreviewPane/constants.ts
new file mode 100644
index 0000000..d978bdc
--- /dev/null
+++ b/site/components/interactive/PreviewPane/constants.ts
@@ -0,0 +1,7 @@
+export const WINDOW_BUTTON_COLORS = {
+ close: "bg-red-500",
+ minimize: "bg-yellow-500",
+ maximize: "bg-green-500",
+} as const;
+
+export const DEFAULT_HEIGHT = "h-96";
diff --git a/site/components/interactive/PreviewPane/index.tsx b/site/components/interactive/PreviewPane/index.tsx
new file mode 100644
index 0000000..5567602
--- /dev/null
+++ b/site/components/interactive/PreviewPane/index.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import React from "react";
+import type { PreviewPaneProps } from "./types";
+import { WINDOW_BUTTON_COLORS, DEFAULT_HEIGHT } from "./constants";
+
+export function PreviewPane({
+ title,
+ themeName,
+ logs,
+ backgroundColor,
+ headerBg,
+ borderColor,
+ isLoading = false,
+ showBorder = false,
+}: PreviewPaneProps) {
+ return (
+
+
+
+ {isLoading ? (
+
+ ) : (
+ logs.map((log, i) => (
+
+ ))
+ )}
+
+
+ Theme: {themeName}
+
+
+ );
+}
+
+export type { PreviewPaneProps } from "./types";
diff --git a/site/components/interactive/PreviewPane/types.ts b/site/components/interactive/PreviewPane/types.ts
new file mode 100644
index 0000000..a52acdf
--- /dev/null
+++ b/site/components/interactive/PreviewPane/types.ts
@@ -0,0 +1,10 @@
+export interface PreviewPaneProps {
+ title: string;
+ themeName: string;
+ logs: string[];
+ backgroundColor: string;
+ headerBg: string;
+ borderColor: string;
+ isLoading?: boolean;
+ showBorder?: boolean;
+}
diff --git a/site/components/interactive/ThemeControls/constants.ts b/site/components/interactive/ThemeControls/constants.ts
new file mode 100644
index 0000000..228e4dc
--- /dev/null
+++ b/site/components/interactive/ThemeControls/constants.ts
@@ -0,0 +1,8 @@
+export const THEME_NAMES = [
+ "GitHub",
+ "Solarized",
+ "Dracula",
+ "Nord",
+ "Monokai",
+ "Oh My Zsh",
+] as const;
diff --git a/site/components/interactive/ThemeControls/index.tsx b/site/components/interactive/ThemeControls/index.tsx
new file mode 100644
index 0000000..bba6953
--- /dev/null
+++ b/site/components/interactive/ThemeControls/index.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { Moon, Sun, Monitor } from "lucide-react";
+import { THEME_NAMES } from "./constants";
+import type { ThemeControlsProps } from "./types";
+
+export function ThemeControls({
+ selectedTheme,
+ colorMode,
+ isDarkOnly,
+ onThemeChange,
+ onColorModeChange,
+}: ThemeControlsProps) {
+ return (
+
+
+
+
+ {THEME_NAMES.map((theme) => (
+
+ ))}
+
+
+ {!isDarkOnly && (
+
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+export type { ThemeControlsProps, ColorMode } from "./types";
diff --git a/site/components/interactive/ThemeControls/types.ts b/site/components/interactive/ThemeControls/types.ts
new file mode 100644
index 0000000..5eab84f
--- /dev/null
+++ b/site/components/interactive/ThemeControls/types.ts
@@ -0,0 +1,9 @@
+export type ColorMode = "light" | "dark" | "system";
+
+export interface ThemeControlsProps {
+ selectedTheme: string;
+ colorMode: ColorMode;
+ isDarkOnly: boolean;
+ onThemeChange: (theme: string) => void;
+ onColorModeChange: (mode: ColorMode) => void;
+}
diff --git a/site/components/interactive/constants.ts b/site/components/interactive/constants.ts
new file mode 100644
index 0000000..3b3011b
--- /dev/null
+++ b/site/components/interactive/constants.ts
@@ -0,0 +1,37 @@
+import type { ThemePair } from "./types";
+
+export const SAMPLE_LOGS = [
+ "[2024-01-15 10:23:45] INFO: Server started on port 3000",
+ "GET /api/users 200 OK (123ms)",
+ "POST /api/auth/login 401 Unauthorized",
+ "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)",
+ "CRITICAL: System shutdown initiated",
+ "Processing batch job... [āāāāāāāāāāāāāāāāāāāā] 100%",
+ "Application ready at http://localhost:3000",
+];
+
+export const THEME_PAIRS: Record = {
+ GitHub: { light: "github-light", dark: "github-dark" },
+ Solarized: { light: "solarized-light", dark: "solarized-dark" },
+ Dracula: { light: "dracula", dark: "dracula" },
+ Nord: { light: "nord", dark: "nord" },
+ Monokai: { light: "monokai", dark: "monokai" },
+ "Oh My Zsh": { light: "oh-my-zsh", dark: "oh-my-zsh" },
+};
+
+export const THEME_BACKGROUNDS: Record<
+ string,
+ { bg: string; headerBg: string; border: string }
+> = {
+ "github-light": { bg: "#ffffff", headerBg: "#f6f8fa", border: "#d1d9e0" },
+ "github-dark": { bg: "#0d1117", headerBg: "#161b22", border: "#30363d" },
+ "solarized-light": { bg: "#fdf6e3", headerBg: "#eee8d5", border: "#eee8d5" },
+ "solarized-dark": { bg: "#002b36", headerBg: "#073642", border: "#073642" },
+ dracula: { bg: "#282a36", headerBg: "#1e1f29", border: "#44475a" },
+ nord: { bg: "#2e3440", headerBg: "#3b4252", border: "#4c566a" },
+ monokai: { bg: "#272822", headerBg: "#3e3d32", border: "#75715e" },
+ "oh-my-zsh": { bg: "#2c3e50", headerBg: "#34495e", border: "#34495e" },
+};
diff --git a/site/components/interactive/index.tsx b/site/components/interactive/index.tsx
index ac2f0fb..fbdb449 100644
--- a/site/components/interactive/index.tsx
+++ b/site/components/interactive/index.tsx
@@ -1,295 +1,13 @@
"use client";
-import React, { useState, useEffect, useRef } from "react";
-import { Button } from "@/components/ui/button";
-import { Moon, Sun, Monitor } from "lucide-react";
-import type { ThemeConfig, ThemePair, ColorMode } from "./types";
-
-// Sample log messages to demonstrate styling
-const SAMPLE_LOGS = [
- "[2024-01-15 10:23:45] INFO: Server started on port 3000",
- "GET /api/users 200 OK (123ms)",
- "POST /api/auth/login 401 Unauthorized",
- "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)",
- "CRITICAL: System shutdown initiated",
- "SUCCESS: Deployment completed to production",
- "TODO: Refactor authentication module",
- "Processing batch job... [āāāāāāāāāāāāāāāāāāāā] 100%",
- "š Application ready at http://localhost:3000",
-];
-
-// Theme configurations with light/dark pairs
-const THEME_PAIRS: Record = {
- GitHub: { light: "github-light", dark: "github-dark" },
- Solarized: { light: "solarized-light", dark: "solarized-dark" },
- Dracula: { light: "dracula", dark: "dracula" }, // Dracula is dark-only
- Nord: { light: "nord", dark: "nord" }, // Nord is dark-only
- Monokai: { light: "monokai", dark: "monokai" }, // Monokai is dark-only
- "Oh My Zsh": { light: "oh-my-zsh", dark: "oh-my-zsh" }, // OMZ is dark-only
-};
-
-// Get theme colors and styling
-const getThemeStyles = (themeName: string): ThemeConfig => {
- const themes: Record = {
- "github-light": {
- bg: "#ffffff",
- headerBg: "#f6f8fa",
- text: "#1f2328",
- border: "#d1d9e0",
- colors: {
- info: "#0969da",
- warn: "#fb8500",
- error: "#cf222e",
- success: "#1f883d",
- debug: "#8250df",
- critical: "#a40e26",
- },
- },
- "github-dark": {
- bg: "#0d1117",
- headerBg: "#161b22",
- text: "#e6edf3",
- border: "#30363d",
- colors: {
- info: "#58a6ff",
- warn: "#f0883e",
- error: "#f85149",
- success: "#3fb950",
- debug: "#a5a5ff",
- critical: "#ff6b6b",
- },
- },
- "solarized-light": {
- bg: "#fdf6e3",
- headerBg: "#eee8d5",
- text: "#657b83",
- border: "#eee8d5",
- colors: {
- info: "#268bd2",
- warn: "#cb4b16",
- error: "#dc322f",
- success: "#859900",
- debug: "#6c71c4",
- critical: "#d33682",
- },
- },
- "solarized-dark": {
- bg: "#002b36",
- headerBg: "#073642",
- text: "#839496",
- border: "#073642",
- colors: {
- info: "#268bd2",
- warn: "#cb4b16",
- error: "#dc322f",
- success: "#859900",
- debug: "#6c71c4",
- critical: "#d33682",
- },
- },
- dracula: {
- bg: "#282a36",
- headerBg: "#1e1f29",
- text: "#f8f8f2",
- border: "#44475a",
- colors: {
- info: "#8be9fd",
- warn: "#ffb86c",
- error: "#ff5555",
- success: "#50fa7b",
- debug: "#bd93f9",
- critical: "#ff79c6",
- },
- },
- nord: {
- bg: "#2e3440",
- headerBg: "#3b4252",
- text: "#eceff4",
- border: "#4c566a",
- colors: {
- info: "#5e81ac",
- warn: "#d08770",
- error: "#bf616a",
- success: "#a3be8c",
- debug: "#b48ead",
- critical: "#d8dee9",
- },
- },
- monokai: {
- bg: "#272822",
- headerBg: "#3e3d32",
- text: "#f8f8f2",
- border: "#75715e",
- colors: {
- info: "#66d9ef",
- warn: "#fd971f",
- error: "#f92672",
- success: "#a6e22e",
- debug: "#ae81ff",
- critical: "#e6db74",
- },
- },
- "oh-my-zsh": {
- bg: "#2c3e50",
- headerBg: "#34495e",
- text: "#ecf0f1",
- border: "#34495e",
- colors: {
- info: "#3498db",
- warn: "#f39c12",
- error: "#e74c3c",
- success: "#27ae60",
- debug: "#2ecc71",
- critical: "#c0392b",
- },
- },
- };
-
- return themes[themeName] || themes["github-dark"];
-};
-
-// Style individual log lines based on content
-const styleLogLine = (log: string, theme: ThemeConfig) => {
- const { colors } = theme;
-
- // Determine log type and apply color
- if (log.includes("CRITICAL")) {
- return `${log}`;
- } else if (
- log.includes("ERROR") ||
- log.includes("failed") ||
- log.includes("401") ||
- log.includes("Unauthorized")
- ) {
- return `${log}`;
- } else if (log.includes("WARN") || log.includes("Memory usage")) {
- return `${log}`;
- } else if (
- log.includes("SUCCESS") ||
- log.includes("ā") ||
- log.includes("š") ||
- log.includes("200 OK")
- ) {
- return `${log}`;
- } else if (log.includes("DEBUG")) {
- return `${log}`;
- } else if (log.includes("INFO")) {
- return `${log}`;
- } else if (log.includes("TODO")) {
- return `${log}`;
- } else {
- return `${log}`;
- }
-};
-
-// Code block component with syntax highlighting
-function CodeBlock({
- children,
- theme,
- className = "",
-}: {
- children: string;
- theme: string;
- className?: string;
-}) {
- const themeStyle = getThemeStyles(theme);
-
- // Simple syntax highlighting for JavaScript
- const highlightCode = (code: string) => {
- const keywords = [
- "import",
- "from",
- "const",
- "let",
- "var",
- "function",
- "return",
- "export",
- "default",
- "true",
- "false",
- "new",
- ];
- const strings = /(["'`])(?:(?=(\\?))\2.)*?\1/g;
- const comments = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)/gm;
-
- let highlighted = code
- // Escape HTML
- .replace(/&/g, "&")
- .replace(//g, ">")
- // Highlight strings
- .replace(
- strings,
- `$&`,
- )
- // Highlight comments
- .replace(
- comments,
- `$&`,
- );
-
- // Highlight keywords
- keywords.forEach((keyword) => {
- const regex = new RegExp(`\\b${keyword}\\b`, "g");
- highlighted = highlighted.replace(
- regex,
- `${keyword}`,
- );
- });
-
- // Highlight function names and properties
- highlighted = highlighted
- .replace(
- /(\w+)(?=\()/g,
- `$1`,
- )
- .replace(
- /\.(\w+)/g,
- `.$1`,
- );
-
- return highlighted;
- };
-
- return (
- <>
-
-
- >
- );
-}
+import React, { useState, useEffect, useRef, useMemo } from "react";
+import { useThemeProcessor } from "@/hooks/useThemeProcessor";
+import { PreviewPane } from "./PreviewPane";
+import { ThemeControls } from "./ThemeControls";
+import { CodeExample } from "./CodeExample";
+import { SAMPLE_LOGS, THEME_PAIRS } from "./constants";
+import { THEME_BACKGROUNDS } from "./CodeExample/constants";
+import type { ColorMode, ThemePair } from "./types";
export function InteractiveExamplesSection() {
const [selectedTheme, setSelectedTheme] = useState("GitHub");
@@ -311,7 +29,6 @@ export function InteractiveExamplesSection() {
return () => clearInterval(interval);
}, []);
- // Detect system preference
useEffect(() => {
const detectMode = () => {
if (colorMode === "system") {
@@ -339,11 +56,23 @@ export function InteractiveExamplesSection() {
const currentThemePair =
THEME_PAIRS[selectedTheme as keyof typeof THEME_PAIRS];
const currentThemeName = currentThemePair[effectiveMode];
- const currentTheme = getThemeStyles(currentThemeName);
-
- // Check if theme only has dark mode
const isDarkOnly = currentThemePair.light === currentThemePair.dark;
+ const logs = useMemo(() => SAMPLE_LOGS, []);
+ const { processedLogs, isLoading } = useThemeProcessor(
+ currentThemeName,
+ logs,
+ );
+
+ const handleThemeChange = (theme: string) => {
+ setSelectedTheme(theme);
+ autoRotateRef.current = false;
+ };
+
+ const bg =
+ THEME_BACKGROUNDS[currentThemeName] || THEME_BACKGROUNDS["github-dark"];
+ const htmlLogs = processedLogs.map((p) => p.html);
+
return (
@@ -357,338 +86,109 @@ export function InteractiveExamplesSection() {
-
-
-
-
- {Object.keys(THEME_PAIRS).map((theme) => (
-
- ))}
-
-
- {!isDarkOnly && (
-
-
-
-
-
- )}
-
-
-
+
- {/* 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..f3b106d
--- /dev/null
+++ b/site/components/output-comparison/GhosttyTerminal.tsx
@@ -0,0 +1,126 @@
+"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..6bd0ad4
--- /dev/null
+++ b/site/components/output-comparison/constants.ts
@@ -0,0 +1,47 @@
+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..30d6c0f
--- /dev/null
+++ b/site/components/output-comparison/index.tsx
@@ -0,0 +1,299 @@
+"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..c185b7c
--- /dev/null
+++ b/site/components/output-comparison/types.ts
@@ -0,0 +1,20 @@
+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..4c08cac
--- /dev/null
+++ b/site/components/schema-viz/index.tsx
@@ -0,0 +1,160 @@
+"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..04b4285
--- /dev/null
+++ b/site/components/theme-card/index.tsx
@@ -0,0 +1,65 @@
+"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..27676a3
--- /dev/null
+++ b/site/hooks/useThemeProcessor.ts
@@ -0,0 +1,140 @@
+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/test-setup.ts b/site/test-setup.ts
index 02b17e2..cafd50d 100644
--- a/site/test-setup.ts
+++ b/site/test-setup.ts
@@ -1,3 +1,53 @@
+import { mock } from "bun:test";
+
+const logsdxMock = {
+ createSimpleTheme: (
+ name: string,
+ colors: unknown,
+ options?: { mode?: string },
+ ) => ({
+ name,
+ colors,
+ mode: options?.mode || "dark",
+ schema: {},
+ }),
+ registerTheme: () => {},
+ getLogsDX: async () => ({
+ processLine: (line: string) =>
+ `${line}`,
+ processLines: (lines: string[]) =>
+ lines.map((line: string) => `${line}`),
+ setTheme: () => {},
+ getCurrentTheme: () => {},
+ }),
+ getTheme: async () => ({
+ name: "mock-theme",
+ mode: "dark",
+ schema: { defaultStyle: { color: "#f8f8f2" } },
+ }),
+ renderLine: (
+ line: string,
+ _theme: unknown,
+ options?: { outputFormat?: string },
+ ) => {
+ if (options?.outputFormat === "html") {
+ return `${line}`;
+ }
+ return line;
+ },
+ getAllThemes: () => ({}),
+ getThemeNames: () => [],
+ LogsDX: class {
+ static getInstance = async () => ({
+ processLine: (line: string) => line,
+ });
+ static resetInstance = () => {};
+ },
+};
+
+// Mock logsdx BEFORE any other imports to ensure it's hoisted
+mock.module("logsdx", () => logsdxMock);
+
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import "@testing-library/jest-dom";
diff --git a/site/tests/__mocks__/logsdx.ts b/site/tests/__mocks__/logsdx.ts
index 7736f1f..3cb0a26 100644
--- a/site/tests/__mocks__/logsdx.ts
+++ b/site/tests/__mocks__/logsdx.ts
@@ -1,7 +1,7 @@
-import { vi } from "bun:test";
+import { mock } from "bun:test";
-export const createSimpleTheme = vi.fn(
- (name: string, colors: any, options?: any) => ({
+export const createSimpleTheme = mock(
+ (name: string, colors: unknown, options?: { mode?: string }) => ({
name,
colors,
mode: options?.mode || "dark",
@@ -9,16 +9,30 @@ export const createSimpleTheme = vi.fn(
}),
);
-export const registerTheme = vi.fn();
+export const registerTheme = mock(() => {});
-export const getLogsDX = vi.fn().mockResolvedValue({
+export const getLogsDX = mock(async () => ({
processLine: (line: string) => `${line}`,
processLines: (lines: string[]) =>
lines.map((line) => `${line}`),
- setTheme: vi.fn(),
- getCurrentTheme: vi.fn(),
-});
+ setTheme: mock(() => {}),
+ getCurrentTheme: mock(() => {}),
+}));
-export const getTheme = vi.fn();
-export const getAllThemes = vi.fn(() => ({}));
-export const getThemeNames = vi.fn(() => []);
+export const getTheme = mock(async () => ({
+ name: "mock-theme",
+ mode: "dark",
+ schema: { defaultStyle: { color: "#f8f8f2" } },
+}));
+
+export const renderLine = mock(
+ (line: string, _theme: unknown, options?: { outputFormat?: string }) => {
+ if (options?.outputFormat === "html") {
+ return `${line}`;
+ }
+ return line;
+ },
+);
+
+export const getAllThemes = mock(() => ({}));
+export const getThemeNames = mock(() => []);
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..6914c3a
--- /dev/null
+++ b/site/tests/components/interactive/PreviewPane.test.tsx
@@ -0,0 +1,66 @@
+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..95ce71f
--- /dev/null
+++ b/site/tests/components/interactive/ThemeControls.test.tsx
@@ -0,0 +1,59 @@
+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..587fc0f
--- /dev/null
+++ b/site/tests/components/log-playground/LogPlayground.test.tsx
@@ -0,0 +1,62 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
+
+mock.module("@/hooks/useThemeProcessor", () => ({
+ useThemeProcessor: () => ({
+ processedLogs: [
+ { html: "INFO: Test", ansi: "\x1b[34mINFO: Test\x1b[0m" },
+ ],
+ isLoading: false,
+ error: null,
+ theme: { name: "dracula", mode: "dark" },
+ }),
+}));
+
+import { render, screen, cleanup, fireEvent } from "../../utils/test-utils";
+import { LogPlayground } from "@/components/log-playground";
+
+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..008bb5d
--- /dev/null
+++ b/site/tests/components/output-comparison/OutputComparison.test.tsx
@@ -0,0 +1,53 @@
+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..1606b78
--- /dev/null
+++ b/site/tests/components/schema-viz/SchemaVisualization.test.tsx
@@ -0,0 +1,67 @@
+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..a490e21
--- /dev/null
+++ b/site/tests/components/theme-card/ThemeCard.test.tsx
@@ -0,0 +1,54 @@
+import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
+
+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" },
+ }),
+}));
+
+import { render, screen, cleanup } from "../../utils/test-utils";
+import { ThemeCard } from "@/components/theme-card";
+
+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/commands.ts b/src/cli/commands.ts
index 510a8cb..2d9431a 100644
--- a/src/cli/commands.ts
+++ b/src/cli/commands.ts
@@ -127,9 +127,9 @@ export async function createInteractiveTheme(
const mode = await select({
message: "Theme mode:",
choices: [
- { name: "š Dark (for dark terminals)", value: "dark" },
- { name: "āļø Light (for light terminals)", value: "light" },
- { name: "š Auto (system preference)", value: "auto" },
+ { name: "Dark (for dark terminals)", value: "dark" },
+ { name: "Light (for light terminals)", value: "light" },
+ { name: "Auto (system preference)", value: "auto" },
],
default: "dark",
});
@@ -143,7 +143,7 @@ export async function createInteractiveTheme(
const preset = await select({
message: "Choose a color preset:",
choices: Object.keys(COLOR_PRESETS).map((name) => ({
- name: name === "Custom" ? "šØ Custom (define your own)" : `šØ ${name}`,
+ name: name === "Custom" ? "Custom (define your own)" : name,
value: name,
})),
});
@@ -216,23 +216,15 @@ export async function createInteractiveTheme(
message: "Select features to highlight:",
choices: [
{
- name: "š Log levels (ERROR, WARN, INFO)",
+ name: "Log levels (ERROR, WARN, INFO)",
value: "logLevels",
checked: true,
},
- {
- name: "š¢ Numbers and numeric values",
- value: "numbers",
- checked: true,
- },
- { name: "š
Dates and timestamps", value: "dates", checked: true },
- { name: "ā
Boolean values", value: "booleans", checked: true },
- {
- name: "š¤ Brackets and punctuation",
- value: "brackets",
- checked: true,
- },
- { name: "š¬ Quoted strings", value: "strings", checked: false },
+ { name: "Numbers and numeric values", value: "numbers", checked: true },
+ { name: "Dates and timestamps", value: "dates", checked: true },
+ { name: "Boolean values", value: "booleans", checked: true },
+ { name: "Brackets and punctuation", value: "brackets", checked: true },
+ { name: "Quoted strings", value: "strings", checked: false },
],
});
@@ -250,7 +242,7 @@ export async function createInteractiveTheme(
createSpinner.succeed("Theme created!");
console.log("\n");
- await renderPreview(theme, `⨠${theme.name} Preview`);
+ await renderPreview(theme, `${theme.name} Preview`);
const checkAccessibility = await confirm({
message: "Check accessibility compliance?",
@@ -263,14 +255,14 @@ export async function createInteractiveTheme(
accessSpinner.stop();
const accessBox = boxen(
- `WCAG Level: ${result.level} ${result.level === "AAA" ? "š" : result.level === "AA" ? "ā
" : result.level === "A" ? "ā ļø" : "ā"}\n` +
+ `WCAG Level: ${result.level}\n` +
`Min Contrast Ratio: ${result.details.normalText.ratio.toFixed(2)}\n` +
(result.recommendations.length > 0
? "\nRecommendations:\n" +
- result.recommendations.map((r: string) => `⢠${r}`).join("\n")
- : "\nā
No issues found!"),
+ result.recommendations.map((r: string) => `- ${r}`).join("\n")
+ : "\nNo issues found"),
{
- title: "āæ Accessibility Report",
+ title: "Accessibility Report",
padding: 1,
borderStyle: "round",
borderColor:
@@ -301,11 +293,11 @@ export async function createInteractiveTheme(
const saveOption = await select({
message: "How would you like to save the theme?",
choices: [
- { name: "š¾ Export as JSON file", value: "json" },
- { name: "š Export as TypeScript file", value: "typescript" },
- { name: "š Copy to clipboard", value: "clipboard" },
- { name: "š Register for immediate use", value: "register" },
- { name: "ā Don't save", value: "none" },
+ { name: "Export as JSON file", value: "json" },
+ { name: "Export as TypeScript file", value: "typescript" },
+ { name: "Copy to clipboard", value: "clipboard" },
+ { name: "Register for immediate use", value: "register" },
+ { name: "Don't save", value: "none" },
],
});
@@ -315,7 +307,7 @@ export async function createInteractiveTheme(
console.log(
boxen(
- colorUtil.green("š Theme creation complete!\n\n") +
+ colorUtil.green("Theme creation complete!\n\n") +
colorUtil.dim(
`Use your theme with: ${colorUtil.cyan(`logsdx --theme ${theme.name}`)}`,
),
@@ -353,7 +345,7 @@ async function saveTheme(theme: Theme, saveOption: string) {
}
writeFileSync(filepath, JSON.stringify(themeData, null, 2));
- console.log(colorUtil.green(`ā
Saved to ${filepath}`));
+ console.log(colorUtil.green(`Saved to ${filepath}`));
} else if (saveOption === "typescript") {
const filepath = await input({
message: "Save as:",
@@ -371,6 +363,6 @@ export const ${theme.name.replace(/[^a-zA-Z0-9]/g, "_")}Theme: Theme = ${JSON.st
`;
writeFileSync(filepath, tsContent);
- console.log(colorUtil.green(`ā
Saved to ${filepath}`));
+ console.log(colorUtil.green(`Saved to ${filepath}`));
}
}
diff --git a/src/cli/constants.ts b/src/cli/constants.ts
index 261dd07..3d4ca99 100644
--- a/src/cli/constants.ts
+++ b/src/cli/constants.ts
@@ -2,15 +2,6 @@ export const CLI_NAME = "logsdx";
export const CLI_VERSION = "0.1.1";
export const CLI_DESCRIPTION = "Enhanced log styling and visualization tool";
-export const SUCCESS_ICON = "ā
";
-export const ERROR_ICON = "ā";
-export const INFO_ICON = "ā¹ļø";
-export const WARNING_ICON = "ā ļø";
-export const FILE_ICON = "š";
-export const STATS_ICON = "š";
-export const SIZE_ICON = "š";
-export const LIGHTBULB_ICON = "š”";
-
export const DEFAULT_THEME = "default";
export const DEFAULT_OUTPUT = "styled";
diff --git a/src/cli/index.ts b/src/cli/index.ts
index c948e50..bc6fdf6 100755
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -1,11 +1,7 @@
import fs from "fs";
import path from "path";
import { LogsDX, getThemeNames } from "../index";
-import {
- type CliOptions,
- type CommanderOptions,
- cliOptionsSchema,
-} from "./types";
+import type { CliOptions, CommanderOptions } from "./types";
import type { LogsDXOptions } from "../types";
import { ui } from "./ui";
import type { InteractiveConfig } from "./interactive";
@@ -15,6 +11,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 +40,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;
@@ -185,30 +184,28 @@ export async function main(
input: string | undefined,
rawOptions: CommanderOptions,
): Promise {
- const validatedOptions = cliOptionsSchema.parse(rawOptions);
-
- const options: CliOptions = cliOptionsSchema.parse({
+ const options: CliOptions = {
input,
- output: validatedOptions.output,
- theme: validatedOptions.theme,
- config: validatedOptions.config,
- debug: validatedOptions.debug,
- quiet: validatedOptions.quiet,
- listThemes: validatedOptions.listThemes,
- interactive: validatedOptions.interactive,
- preview: validatedOptions.preview,
- noSpinner: validatedOptions.noSpinner,
- generateTheme: validatedOptions.generateTheme,
- listPalettes: validatedOptions.listPalettes,
- listPatterns: validatedOptions.listPatterns,
- exportTheme: validatedOptions.exportTheme,
- importTheme: validatedOptions.importTheme,
- listThemeFiles: validatedOptions.listThemeFiles,
+ output: rawOptions.output,
+ theme: rawOptions.theme,
+ config: rawOptions.config,
+ debug: rawOptions.debug ?? false,
+ quiet: rawOptions.quiet ?? false,
+ listThemes: rawOptions.listThemes ?? false,
+ interactive: rawOptions.interactive ?? false,
+ preview: rawOptions.preview ?? false,
+ noSpinner: rawOptions.noSpinner ?? false,
+ generateTheme: rawOptions.generateTheme ?? false,
+ listPalettes: rawOptions.listPalettes ?? false,
+ listPatterns: rawOptions.listPatterns ?? false,
+ exportTheme: rawOptions.exportTheme,
+ importTheme: rawOptions.importTheme,
+ listThemeFiles: rawOptions.listThemeFiles ?? false,
format:
- validatedOptions.format === "ansi" || validatedOptions.format === "html"
- ? validatedOptions.format
+ rawOptions.format === "ansi" || rawOptions.format === "html"
+ ? rawOptions.format
: undefined,
- });
+ };
if (options.interactive) {
try {
const { runInteractiveMode } = await import("./interactive");
@@ -289,8 +286,8 @@ export async function main(
getThemeNames().forEach((theme) => {
console.log(` ⢠${theme}`);
});
- console.log("\nš” Use --preview to see themes with sample logs");
- console.log("š” Use --interactive for guided selection");
+ console.log("\nUse --preview to see themes with sample logs");
+ console.log("Use --interactive for guided selection");
}
return;
}
diff --git a/src/cli/interactive.ts b/src/cli/interactive.ts
index 35871d6..b179aef 100644
--- a/src/cli/interactive.ts
+++ b/src/cli/interactive.ts
@@ -1,28 +1,23 @@
import { select, confirm } from "../utils/prompts";
import { LogsDX, getThemeNames, getTheme } from "../index";
import { ui } from "./ui";
-import chalk from "chalk";
-import { z } from "zod";
+import colors from "../utils/colors";
-export const interactiveConfigSchema = z.object({
- theme: z.string(),
- outputFormat: z.enum(["ansi", "html"]),
- preview: z.boolean(),
-});
+export type InteractiveConfig = {
+ theme: string;
+ outputFormat: "ansi" | "html";
+ preview: boolean;
+};
-export type InteractiveConfig = z.infer;
-
-export const themeChoiceSchema = z.object({
- name: z.string(),
- value: z.string(),
- description: z.string(),
-});
-
-export type ThemeChoice = z.infer;
+export type ThemeChoice = {
+ name: string;
+ value: string;
+ description: string;
+};
const SAMPLE_LOG = `2024-01-15 10:30:45 INFO [server] Application started successfully
2024-01-15 10:30:46 DEBUG [auth] Loading user credentials from /etc/config
-2024-01-15 10:30:47 WARN [database] Connection pool at 80% capacity
+2024-01-15 10:30:47 WARN [database] Connection pool at 80% capacity
2024-01-15 10:30:48 ERROR [api] Failed to process request: /users/123/profile
2024-01-15 10:30:49 INFO [cache] Cache hit ratio: 94.5%
GET /api/users/123 200 142ms - "Mozilla/5.0"
@@ -31,10 +26,10 @@ POST /api/auth/login 401 23ms - Invalid credentials
export async function runInteractiveMode(): Promise {
ui.showHeader();
- ui.showInfo("Welcome to LogsDX Interactive Mode! š");
+ ui.showInfo("Welcome to LogsDX Interactive Mode!");
console.log(
- chalk.dim(
+ colors.dim(
"This wizard will help you select the perfect theme and settings for your logs.\n",
),
);
@@ -42,7 +37,7 @@ export async function runInteractiveMode(): Promise {
const themeNames = getThemeNames();
const themeChoices: ThemeChoice[] = await Promise.all(
themeNames.map(async (name: string) => ({
- name: chalk.cyan(name),
+ name: colors.cyan(name),
value: name,
description:
(await getTheme(name))?.description || "No description available",
@@ -50,11 +45,11 @@ export async function runInteractiveMode(): Promise {
);
const selectedTheme = await select({
- message: "šØ Choose a theme:",
+ message: "Choose a theme:",
choices: [
...themeChoices,
{
- name: chalk.yellow("š Preview themes"),
+ name: colors.yellow("Preview themes"),
value: "__preview__",
description: "See how each theme looks with sample logs",
},
@@ -76,21 +71,21 @@ export async function runInteractiveMode(): Promise {
}
finalTheme = await select({
- message: "šØ Now choose your theme:",
+ message: "Now choose your theme:",
choices: themeChoices,
});
}
const outputFormat = await select({
- message: "š¤ Choose output format:",
+ message: "Choose output format:",
choices: [
{
- name: chalk.green("ANSI") + chalk.dim(" (terminal colors)"),
+ name: colors.green("ANSI") + colors.dim(" (terminal colors)"),
value: "ansi" as const,
description: "Perfect for terminal output with colors and styling",
},
{
- name: chalk.blue("HTML") + chalk.dim(" (web/browser)"),
+ name: colors.blue("HTML") + colors.dim(" (web/browser)"),
value: "html" as const,
description: "Generates HTML with inline styles for web display",
},
@@ -98,12 +93,12 @@ export async function runInteractiveMode(): Promise {
});
const wantPreview = await confirm({
- message: "š Show a preview with your settings?",
+ message: "Show a preview with your settings?",
default: true,
});
if (wantPreview) {
- console.log("\n" + chalk.bold("š¬ Preview with your selected settings:"));
+ console.log("\n" + colors.bold("Preview with your selected settings:"));
const logsDX = await LogsDX.getInstance({
theme: finalTheme,
outputFormat: outputFormat as "ansi" | "html",
@@ -116,7 +111,7 @@ export async function runInteractiveMode(): Promise {
}
const saveConfig = await confirm({
- message: "š¾ Save these settings as default?",
+ message: "Save these settings as default?",
default: false,
});
@@ -124,22 +119,20 @@ export async function runInteractiveMode(): Promise {
ui.showInfo("Configuration saved to ~/.logsdxrc.json");
}
- const result = interactiveConfigSchema.parse({
+ return {
theme: finalTheme,
- outputFormat,
+ outputFormat: outputFormat as "ansi" | "html",
preview: wantPreview,
- });
-
- return result;
+ };
}
export async function selectThemeInteractively(): Promise {
const themeNames = getThemeNames();
return await select({
- message: "šØ Select a theme:",
+ message: "Select a theme:",
choices: themeNames.map((name: string) => ({
- name: chalk.cyan(name),
+ name: colors.cyan(name),
value: name,
})),
});
@@ -162,17 +155,15 @@ export async function showThemeList(): Promise {
`INFO Sample log with ${themeName} theme - GET /api/test 200 OK`,
);
- console.log(chalk.bold.cyan(`\n${sample}:`));
+ console.log(colors.bold.cyan(`\n${sample}:`));
if (theme?.description) {
- console.log(chalk.dim(` ${theme.description}`));
+ console.log(colors.dim(` ${theme.description}`));
}
console.log(` ${styledSample}`);
}
+ console.log(colors.yellow("\nUse --interactive for guided theme selection"));
console.log(
- chalk.yellow("\nš” Use --interactive for guided theme selection"),
- );
- console.log(
- chalk.yellow("š” Use --preview to see all themes with sample logs"),
+ colors.yellow("Use --preview to see all themes with sample logs"),
);
}
diff --git a/src/cli/theme-gen.ts b/src/cli/theme-gen.ts
index 2eb8fba..8d8f20b 100644
--- a/src/cli/theme-gen.ts
+++ b/src/cli/theme-gen.ts
@@ -1,6 +1,6 @@
import { select, input, checkbox, confirm } from "../utils/prompts";
import { ui } from "./ui";
-import chalk from "chalk";
+import colors, { hex } from "../utils/colors";
import fs from "fs";
import path from "path";
import {
@@ -13,15 +13,15 @@ import {
} from "../themes/presets";
import { registerTheme, getAllThemes, getTheme } from "../themes";
import type { Theme, PatternMatch } from "../types";
-import { themePresetSchema } from "../schema";
+import { parseTheme } from "../schema";
import { LogsDX } from "../index";
export async function runThemeGenerator(): Promise {
ui.showHeader();
- ui.showInfo("šØ Welcome to the LogsDX Theme Generator!");
+ ui.showInfo("Welcome to the LogsDX Theme Generator");
console.log(
- chalk.dim(
+ colors.dim(
"Create custom themes by combining color palettes with pattern presets.\n",
),
);
@@ -43,9 +43,9 @@ export async function runThemeGenerator(): Promise {
const palettes = listColorPalettes();
const selectedPalette = await select({
- message: "šØ Choose a color palette:",
+ message: "Choose a color palette:",
choices: palettes.map((palette) => ({
- name: `${chalk.bold(palette.name)} - ${palette.description}`,
+ name: `${colors.bold(palette.name)} - ${palette.description}`,
value: palette.name,
description: `Contrast: ${palette.accessibility.contrastRatio.toFixed(1)}, ${
palette.accessibility.colorBlindSafe
@@ -66,7 +66,7 @@ export async function runThemeGenerator(): Promise {
);
const selectedPresets = await checkbox({
- message: "š Select pattern presets to include:",
+ message: "Select pattern presets to include:",
choices: Object.entries(presetsByCategory).flatMap(
([category, categoryPresets]) =>
categoryPresets.map((preset) => ({
@@ -79,7 +79,7 @@ export async function runThemeGenerator(): Promise {
const filteredPresets = selectedPresets;
const addCustomPatterns = await confirm({
- message: "ā Add custom patterns?",
+ message: "Add custom patterns?",
default: false,
});
@@ -89,7 +89,7 @@ export async function runThemeGenerator(): Promise {
}
const addCustomWords = await confirm({
- message: "ā Add custom word matches?",
+ message: "Add custom word matches?",
default: false,
});
@@ -115,7 +115,7 @@ export async function runThemeGenerator(): Promise {
await showThemePreview(theme, palette);
const shouldSave = await confirm({
- message: "š¾ Save this theme?",
+ message: "Save this theme?",
default: true,
});
@@ -146,11 +146,11 @@ export async function runThemeGenerator(): Promise {
if (saveLocation === "file" || saveLocation === "both") {
const filename = `${themeName}.theme.json`;
fs.writeFileSync(filename, JSON.stringify(theme, null, 2));
- ui.showSuccess(`Theme saved to ${chalk.cyan(filename)}`);
+ ui.showSuccess(`Theme saved to ${colors.cyan(filename)}`);
}
console.log(
- chalk.green(
+ colors.green(
`\n⨠Your theme "${themeName}" is ready to use!\nTry it with: logsdx --theme ${themeName} your-log-file.log`,
),
);
@@ -291,7 +291,7 @@ async function collectCustomWords(): Promise<
}
async function showThemePreview(theme: Theme, palette: ColorPalette) {
- console.log(chalk.bold("\nš¬ Theme Preview:\n"));
+ console.log(colors.bold("\nTheme Preview:\n"));
const sampleLogs = [
"2024-01-15 10:30:45 INFO API server started on port 3000",
@@ -313,53 +313,51 @@ async function showThemePreview(theme: Theme, palette: ColorPalette) {
console.log(` ${logsDX.processLine(log)}`);
});
- console.log(chalk.bold("\nš Color Palette Details:"));
- console.log(` Name: ${chalk.cyan(palette.name)}`);
+ console.log(colors.bold("\nColor Palette Details:"));
+ console.log(` Name: ${colors.cyan(palette.name)}`);
console.log(` Description: ${palette.description}`);
console.log(
- ` Contrast Ratio: ${chalk.yellow(palette.accessibility.contrastRatio.toFixed(1))}`,
+ ` Contrast Ratio: ${colors.yellow(palette.accessibility.contrastRatio.toFixed(1))}`,
);
console.log(
` Color-blind Safe: ${
palette.accessibility.colorBlindSafe
- ? chalk.green("Yes")
- : chalk.red("No")
+ ? colors.green("Yes")
+ : colors.red("No")
}`,
);
console.log(
- ` Mode: ${palette.accessibility.darkMode ? chalk.blue("Dark") : chalk.yellow("Light")}`,
+ ` Mode: ${palette.accessibility.darkMode ? colors.blue("Dark") : colors.yellow("Light")}`,
);
}
export function listColorPalettesCommand(): void {
- ui.showInfo("šØ Available Color Palettes:\n");
+ ui.showInfo("Available Color Palettes:\n");
const palettes = listColorPalettes();
palettes.forEach((palette, index) => {
- console.log(chalk.bold.cyan(`${index + 1}. ${palette.name}`));
+ console.log(colors.bold.cyan(`${index + 1}. ${palette.name}`));
console.log(` ${palette.description}`);
console.log(
- ` ${chalk.dim(`Contrast: ${palette.accessibility.contrastRatio.toFixed(1)}`)} ${chalk.dim(
+ ` ${colors.dim(`Contrast: ${palette.accessibility.contrastRatio.toFixed(1)}`)} ${colors.dim(
`| ${palette.accessibility.colorBlindSafe ? "Color-blind safe" : "Not color-blind safe"}`,
- )} ${chalk.dim(`| ${palette.accessibility.darkMode ? "Dark" : "Light"} mode`)}`,
+ )} ${colors.dim(`| ${palette.accessibility.darkMode ? "Dark" : "Light"} mode`)}`,
);
console.log(" Colors:");
Object.entries(palette.colors).forEach(([role, color]) => {
- console.log(` ${role}: ${chalk.hex(color)(color)}`);
+ console.log(` ${role}: ${hex(color)(color)}`);
});
console.log();
});
console.log(
- chalk.yellow(
- "š” Use --generate-theme to create a theme with these palettes",
- ),
+ colors.yellow("Use --generate-theme to create a theme with these palettes"),
);
}
export function listPatternPresetsCommand(): void {
- ui.showInfo("š Available Pattern Presets:\n");
+ ui.showInfo("Available Pattern Presets:\n");
const presets = listPatternPresets();
const presetsByCategory = presets.reduce(
@@ -372,19 +370,19 @@ export function listPatternPresetsCommand(): void {
);
Object.entries(presetsByCategory).forEach(([category, categoryPresets]) => {
- console.log(chalk.bold.yellow(`\n${category.toUpperCase()}:`));
+ console.log(colors.bold.yellow(`\n${category.toUpperCase()}:`));
categoryPresets.forEach((preset) => {
- console.log(chalk.bold.cyan(` ${preset.name}`));
+ console.log(colors.bold.cyan(` ${preset.name}`));
console.log(` ${preset.description}`);
console.log(
- ` ${chalk.dim(`${preset.patterns.length} patterns, ${Object.keys(preset.matchWords).length} word matches`)}`,
+ ` ${colors.dim(`${preset.patterns.length} patterns, ${Object.keys(preset.matchWords).length} word matches`)}`,
);
});
});
console.log(
- chalk.yellow(
- "\nš” Use --generate-theme to create a theme with these presets",
+ colors.yellow(
+ "\nUse --generate-theme to create a theme with these presets",
),
);
}
@@ -576,7 +574,7 @@ export async function exportTheme(themeName?: string): Promise {
(await select({
message: "Select theme to export:",
choices: availableThemes.map((name) => ({
- name: chalk.cyan(name),
+ name: colors.cyan(name),
value: name,
})),
}));
@@ -608,7 +606,7 @@ export async function exportTheme(themeName?: string): Promise {
fs.writeFileSync(filename, JSON.stringify(exportData, null, 2));
ui.showSuccess(
- `Theme "${themeToExport}" exported to ${chalk.cyan(filename)}`,
+ `Theme "${themeToExport}" exported to ${colors.cyan(filename)}`,
);
const showPreview = await confirm({
@@ -617,10 +615,10 @@ export async function exportTheme(themeName?: string): Promise {
});
if (showPreview) {
- console.log(chalk.dim("\nFile contents:"));
- console.log(chalk.dim("ā".repeat(50)));
+ console.log(colors.dim("\nFile contents:"));
+ console.log(colors.dim("ā".repeat(50)));
console.log(JSON.stringify(exportData, null, 2));
- console.log(chalk.dim("ā".repeat(50)));
+ console.log(colors.dim("ā".repeat(50)));
}
} catch (error) {
ui.showError(
@@ -712,9 +710,9 @@ export async function importTheme(filename?: string): Promise {
const fileContent = fs.readFileSync(themeFile, "utf8");
const themeData = JSON.parse(fileContent);
- const validatedTheme = themePresetSchema.parse(themeData);
+ const validatedTheme = parseTheme(themeData);
- ui.showInfo(`Importing theme: ${chalk.cyan(validatedTheme.name)}`);
+ ui.showInfo(`Importing theme: ${colors.cyan(validatedTheme.name)}`);
if (validatedTheme.description) {
console.log(`Description: ${validatedTheme.description}`);
}
@@ -754,7 +752,7 @@ export async function importTheme(filename?: string): Promise {
registerTheme(validatedTheme);
ui.showSuccess(`Theme "${validatedTheme.name}" imported successfully!`);
console.log(
- chalk.green(
+ colors.green(
`\n⨠Use your imported theme with: logsdx --theme ${validatedTheme.name} your-log-file.log`,
),
);
@@ -783,7 +781,7 @@ export async function importTheme(filename?: string): Promise {
}
async function previewImportedTheme(theme: Theme) {
- console.log(chalk.bold("\nš¬ Theme Preview:\n"));
+ console.log(colors.bold("\nTheme Preview:\n"));
const sampleLogs = [
"2024-01-15 10:30:45 INFO Starting application server",
@@ -804,22 +802,22 @@ async function previewImportedTheme(theme: Theme) {
console.log(` ${logsDX.processLine(log)}`);
});
- console.log(chalk.bold("\nš Theme Details:"));
- console.log(` Name: ${chalk.cyan(theme.name)}`);
+ console.log(colors.bold("\nTheme Details:"));
+ console.log(` Name: ${colors.cyan(theme.name)}`);
if (theme.description) {
console.log(` Description: ${theme.description}`);
}
const exportedTheme = theme as Theme & { exportedAt?: string };
if (exportedTheme.exportedAt) {
console.log(
- ` Exported: ${chalk.dim(new Date(exportedTheme.exportedAt).toLocaleString())}`,
+ ` Exported: ${colors.dim(new Date(exportedTheme.exportedAt).toLocaleString())}`,
);
}
const wordCount = Object.keys(theme.schema.matchWords || {}).length;
const patternCount = (theme.schema.matchPatterns || []).length;
console.log(
- ` Patterns: ${chalk.yellow(patternCount)}, Words: ${chalk.yellow(wordCount)}`,
+ ` Patterns: ${colors.yellow(patternCount)}, Words: ${colors.yellow(wordCount)}`,
);
}
@@ -867,7 +865,7 @@ export function listThemeFilesCommand(directory = "."): void {
if (files.length === 0) {
ui.showInfo("No theme files found in current directory");
console.log(
- chalk.dim("Theme files should have the extension .theme.json"),
+ colors.dim("Theme files should have the extension .theme.json"),
);
return;
}
@@ -879,28 +877,28 @@ export function listThemeFilesCommand(directory = "."): void {
const content = fs.readFileSync(file, "utf8");
const themeData = JSON.parse(content);
- console.log(chalk.bold.cyan(`${index + 1}. ${path.basename(file)}`));
+ console.log(colors.bold.cyan(`${index + 1}. ${path.basename(file)}`));
console.log(` Theme: ${themeData.name || "Unknown"}`);
if (themeData.description) {
console.log(` Description: ${themeData.description}`);
}
if (themeData.exportedAt) {
console.log(
- ` Exported: ${chalk.dim(new Date(themeData.exportedAt).toLocaleString())}`,
+ ` Exported: ${colors.dim(new Date(themeData.exportedAt).toLocaleString())}`,
);
}
- console.log(` File: ${chalk.dim(file)}`);
+ console.log(` File: ${colors.dim(file)}`);
console.log();
} catch {
- console.log(chalk.bold.red(`${index + 1}. ${path.basename(file)}`));
- console.log(chalk.red(` Error: Invalid theme file`));
- console.log(` File: ${chalk.dim(file)}`);
+ console.log(colors.bold.red(`${index + 1}. ${path.basename(file)}`));
+ console.log(colors.red(` Error: Invalid theme file`));
+ console.log(` File: ${colors.dim(file)}`);
console.log();
}
});
console.log(
- chalk.yellow("š” Use --import-theme to import a theme"),
+ colors.yellow("Use --import-theme to import a theme"),
);
} catch (error) {
ui.showError(
diff --git a/src/cli/types.ts b/src/cli/types.ts
index b852979..1e17315 100644
--- a/src/cli/types.ts
+++ b/src/cli/types.ts
@@ -1,27 +1,23 @@
-import { z } from "zod";
+export type CliOptions = {
+ input?: string;
+ output?: string;
+ theme?: string;
+ config?: string;
+ debug?: boolean;
+ quiet?: boolean;
+ listThemes?: boolean;
+ interactive?: boolean;
+ preview?: boolean;
+ noSpinner?: boolean;
+ format?: "ansi" | "html";
+ generateTheme?: boolean;
+ listPalettes?: boolean;
+ listPatterns?: boolean;
+ exportTheme?: string;
+ importTheme?: string;
+ listThemeFiles?: boolean;
+};
-export const cliOptionsSchema = z.object({
- input: z.string().optional(),
- output: z.string().optional(),
- theme: z.string().optional(),
- config: z.string().optional(),
- debug: z.boolean().optional().default(false),
- quiet: z.boolean().optional().default(false),
- listThemes: z.boolean().optional().default(false),
- interactive: z.boolean().optional().default(false),
- preview: z.boolean().optional().default(false),
- noSpinner: z.boolean().optional().default(false),
- format: z.enum(["ansi", "html"]).optional(),
-
- generateTheme: z.boolean().optional().default(false),
- listPalettes: z.boolean().optional().default(false),
- listPatterns: z.boolean().optional().default(false),
- exportTheme: z.string().optional(),
- importTheme: z.string().optional(),
- listThemeFiles: z.boolean().optional().default(false),
-});
-
-export type CliOptions = z.infer;
export type CommanderOptions = CliOptions;
export interface SpinnerLike {
diff --git a/src/cli/ui.ts b/src/cli/ui.ts
index 389e00a..a54e4ee 100644
--- a/src/cli/ui.ts
+++ b/src/cli/ui.ts
@@ -56,22 +56,22 @@ export class CliUI {
}
showSuccess(message: string) {
- console.log(colors.green("ā
"), colors.bold(message));
+ console.log(colors.green("[ok]"), colors.bold(message));
}
showError(message: string, suggestion?: string) {
- console.log(colors.red("ā"), colors.bold.red("Error:"), message);
+ console.log(colors.red("[error]"), colors.bold.red(message));
if (suggestion) {
- console.log(colors.yellow("š”"), colors.italic(suggestion));
+ console.log(colors.yellow(" hint:"), colors.italic(suggestion));
}
}
showWarning(message: string) {
- console.log(colors.yellow("ā ļø"), colors.bold.yellow("Warning:"), message);
+ console.log(colors.yellow("[warn]"), colors.bold.yellow(message));
}
showInfo(message: string) {
- console.log(colors.blue("ā¹ļø"), message);
+ console.log(colors.blue("[info]"), message);
}
showThemePreview(themeName: string, sample: string) {
@@ -87,9 +87,9 @@ export class CliUI {
showFileStats(filename: string, lineCount: number, fileSize: number) {
const stats = [
- `š File: ${colors.cyan(filename)}`,
- `š Lines: ${colors.yellow(lineCount.toLocaleString())}`,
- `š Size: ${colors.green(this.formatFileSize(fileSize))}`,
+ `File: ${colors.cyan(filename)}`,
+ `Lines: ${colors.yellow(lineCount.toLocaleString())}`,
+ `Size: ${colors.green(this.formatFileSize(fileSize))}`,
].join(" ");
console.log(
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/lib/validate.ts b/src/lib/validate.ts
new file mode 100644
index 0000000..8ddff3a
--- /dev/null
+++ b/src/lib/validate.ts
@@ -0,0 +1,187 @@
+type ValidationResult =
+ | { success: true; data: T }
+ | { success: false; error: ValidationError };
+
+export class ValidationError extends Error {
+ constructor(
+ message: string,
+ public path: string[] = [],
+ public issues: { path: string[]; message: string }[] = [],
+ ) {
+ super(message);
+ this.name = "ValidationError";
+ }
+}
+
+type Validator = {
+ parse: (value: unknown) => T;
+ safeParse: (value: unknown) => ValidationResult;
+ optional: () => Validator;
+};
+
+function createValidator(
+ validate: (value: unknown, path: string[]) => T,
+): Validator {
+ return {
+ parse(value: unknown): T {
+ return validate(value, []);
+ },
+ safeParse(value: unknown): ValidationResult {
+ try {
+ return { success: true, data: validate(value, []) };
+ } catch (e) {
+ return { success: false, error: e as ValidationError };
+ }
+ },
+ optional(): Validator {
+ return createValidator((v, path) =>
+ v === undefined ? undefined : validate(v, path),
+ );
+ },
+ };
+}
+
+function fail(message: string, path: string[]): never {
+ throw new ValidationError(message, path, [{ path, message }]);
+}
+
+export const v = {
+ string(): Validator {
+ return createValidator((value, path) => {
+ if (typeof value !== "string")
+ fail(`Expected string, got ${typeof value}`, path);
+ return value;
+ });
+ },
+
+ number(): Validator {
+ return createValidator((value, path) => {
+ if (typeof value !== "number")
+ fail(`Expected number, got ${typeof value}`, path);
+ return value;
+ });
+ },
+
+ boolean(): Validator {
+ return createValidator((value, path) => {
+ if (typeof value !== "boolean")
+ fail(`Expected boolean, got ${typeof value}`, path);
+ return value;
+ });
+ },
+
+ literal(expected: T): Validator {
+ return createValidator((value, path) => {
+ if (value !== expected)
+ fail(`Expected ${String(expected)}, got ${String(value)}`, path);
+ return expected;
+ });
+ },
+
+ enum(values: readonly T[]): Validator {
+ return createValidator((value, path) => {
+ if (typeof value !== "string" || !values.includes(value as T)) {
+ fail(`Expected one of: ${values.join(", ")}`, path);
+ }
+ return value as T;
+ });
+ },
+
+ array(itemValidator: Validator): Validator {
+ return createValidator((value, path) => {
+ if (!Array.isArray(value)) fail("Expected array", path);
+ return value.map((item, i) => itemValidator.parse(item));
+ });
+ },
+
+ object>>(
+ shape: T,
+ ): Validator<{
+ [K in keyof T]: T[K] extends Validator ? U : never;
+ }> {
+ return createValidator((value, path) => {
+ if (typeof value !== "object" || value === null)
+ fail("Expected object", path);
+ const result: Record = {};
+ const obj = value as Record;
+
+ for (const [key, validator] of Object.entries(shape)) {
+ try {
+ result[key] = validator.parse(obj[key]);
+ } catch (e) {
+ if (e instanceof ValidationError) {
+ fail(e.message, [...path, key]);
+ }
+ throw e;
+ }
+ }
+ return result as {
+ [K in keyof T]: T[K] extends Validator ? U : never;
+ };
+ });
+ },
+
+ record(valueValidator: Validator): Validator> {
+ return createValidator((value, path) => {
+ if (typeof value !== "object" || value === null)
+ fail("Expected object", path);
+ const result: Record = {};
+ for (const [key, val] of Object.entries(value)) {
+ result[key] = valueValidator.parse(val);
+ }
+ return result;
+ });
+ },
+
+ union[]>(
+ ...validators: T
+ ): Validator ? U : never> {
+ return createValidator((value, path) => {
+ for (const validator of validators) {
+ const result = validator.safeParse(value);
+ if (result.success)
+ return result.data as T[number] extends Validator
+ ? U
+ : never;
+ }
+ fail("Value did not match any variant", path);
+ });
+ },
+
+ refine(
+ validator: Validator,
+ check: (value: T) => boolean,
+ message: string,
+ ): Validator {
+ return createValidator((value, path) => {
+ const parsed = validator.parse(value);
+ if (!check(parsed)) fail(message, path);
+ return parsed;
+ });
+ },
+
+ withDefault(validator: Validator, defaultValue: T): Validator {
+ return createValidator((value, path) => {
+ if (value === undefined) return defaultValue;
+ return validator.parse(value);
+ });
+ },
+};
+
+export function isValidationError(error: unknown): error is ValidationError {
+ return (
+ error instanceof ValidationError ||
+ (typeof error === "object" &&
+ error !== null &&
+ "issues" in error &&
+ Array.isArray((error as ValidationError).issues))
+ );
+}
+
+export function formatValidationIssues(
+ issues: { path: string[]; message: string }[],
+): string {
+ return issues
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .join(", ");
+}
diff --git a/src/schema/index.ts b/src/schema/index.ts
index da18542..e65da90 100644
--- a/src/schema/index.ts
+++ b/src/schema/index.ts
@@ -1,7 +1,11 @@
-import { z } from "zod";
+import {
+ v,
+ ValidationError,
+ isValidationError,
+ formatValidationIssues,
+} from "../lib/validate";
import {
COLOR_VALIDATION_MESSAGE,
- EMPTY_COLOR_MESSAGE,
STYLE_CODES,
WHITESPACE_OPTIONS,
NEWLINE_OPTIONS,
@@ -9,61 +13,131 @@ import {
HTML_STYLE_FORMATS,
DEFAULT_WHITESPACE,
DEFAULT_NEWLINE,
- THEME_MODE_DESCRIPTION,
- TOKEN_CONTENT_DESCRIPTION,
- TOKEN_METADATA_DESCRIPTION,
- TOKEN_STYLE_DESCRIPTION,
- HTML_STYLE_FORMAT_DESCRIPTION,
} from "./constants";
import { isValidColorFormat } from "./utils";
+import type { StyleOptions, PatternMatch, SchemaConfig, Theme } from "../types";
+
+const styleOptionsValidator = v.object({
+ color: v.refine(v.string(), isValidColorFormat, COLOR_VALIDATION_MESSAGE),
+ styleCodes: v.array(v.enum(STYLE_CODES)).optional(),
+ htmlStyleFormat: v.enum(HTML_STYLE_FORMATS).optional(),
+});
+
+const patternMatchValidator = v.object({
+ name: v.string(),
+ pattern: v.string(),
+ options: styleOptionsValidator,
+});
-export const styleOptionsSchema = z.object({
- color: z.string().min(1, EMPTY_COLOR_MESSAGE).refine(isValidColorFormat, {
- message: COLOR_VALIDATION_MESSAGE,
- }),
- styleCodes: z.array(z.enum(STYLE_CODES)).optional(),
- htmlStyleFormat: z
- .enum(HTML_STYLE_FORMATS)
- .optional()
- .describe(HTML_STYLE_FORMAT_DESCRIPTION),
+const schemaConfigValidator = v.object({
+ defaultStyle: styleOptionsValidator.optional(),
+ matchWords: v.record(styleOptionsValidator).optional(),
+ matchStartsWith: v.record(styleOptionsValidator).optional(),
+ matchEndsWith: v.record(styleOptionsValidator).optional(),
+ matchContains: v.record(styleOptionsValidator).optional(),
+ matchPatterns: v.array(patternMatchValidator).optional(),
+ whiteSpace: v.withDefault(v.enum(WHITESPACE_OPTIONS), DEFAULT_WHITESPACE),
+ newLine: v.withDefault(v.enum(NEWLINE_OPTIONS), DEFAULT_NEWLINE),
});
-export const tokenMetadataSchema = z
+const themePresetValidator = v.object({
+ name: v.string(),
+ description: v.string().optional(),
+ mode: v.enum(THEME_MODES).optional(),
+ schema: schemaConfigValidator,
+});
+
+const tokenMetadataValidator = v
.object({
- style: styleOptionsSchema.optional().describe(TOKEN_STYLE_DESCRIPTION),
+ style: styleOptionsValidator.optional(),
})
- .catchall(z.unknown())
.optional();
-export const patternMatchSchema = z.object({
- name: z.string(),
- pattern: z.string(),
- options: styleOptionsSchema,
+const tokenValidator = v.object({
+ content: v.string(),
+ metadata: tokenMetadataValidator,
});
-export const schemaConfigSchema = z.object({
- defaultStyle: styleOptionsSchema.optional(),
- matchWords: z.record(z.string(), styleOptionsSchema).optional(),
- matchStartsWith: z.record(z.string(), styleOptionsSchema).optional(),
- matchEndsWith: z.record(z.string(), styleOptionsSchema).optional(),
- matchContains: z.record(z.string(), styleOptionsSchema).optional(),
- matchPatterns: z.array(patternMatchSchema).optional(),
- whiteSpace: z.enum(WHITESPACE_OPTIONS).optional().default(DEFAULT_WHITESPACE),
- newLine: z.enum(NEWLINE_OPTIONS).optional().default(DEFAULT_NEWLINE),
-});
+const tokenListValidator = v.array(tokenValidator);
-export const themePresetSchema = z.object({
- name: z.string(),
- description: z.string().optional(),
- mode: z.enum(THEME_MODES).optional().describe(THEME_MODE_DESCRIPTION),
- schema: schemaConfigSchema,
-});
+export type TokenMetadata = {
+ style?: StyleOptions;
+ matchType?: string;
+ matchPattern?: string;
+ pattern?: string | RegExp;
+ trimmed?: boolean;
+ originalLength?: number;
+};
-export const tokenSchema = z.object({
- content: z.string().describe(TOKEN_CONTENT_DESCRIPTION),
- metadata: tokenMetadataSchema.describe(TOKEN_METADATA_DESCRIPTION),
-});
+export type Token = { content: string; metadata?: TokenMetadata };
+export type TokenList = Token[];
+
+export function parseToken(token: unknown): Token {
+ return tokenValidator.parse(token) as Token;
+}
+
+export function parseTokenSafe(token: unknown): {
+ success: boolean;
+ data?: Token;
+ error?: ValidationError;
+} {
+ const result = tokenValidator.safeParse(token);
+ if (result.success) return { success: true, data: result.data as Token };
+ return { success: false, error: result.error };
+}
+
+export function parseTokenList(tokens: unknown): TokenList {
+ return tokenListValidator.parse(tokens) as TokenList;
+}
+
+export function parseTokenListSafe(tokens: unknown): {
+ success: boolean;
+ data?: TokenList;
+ error?: ValidationError;
+} {
+ const result = tokenListValidator.safeParse(tokens);
+ if (result.success) return { success: true, data: result.data as TokenList };
+ return { success: false, error: result.error };
+}
+
+export function parseTheme(theme: unknown): Theme {
+ return themePresetValidator.parse(theme) as Theme;
+}
+
+export function parseThemeSafe(theme: unknown): {
+ success: boolean;
+ data?: Theme;
+ error?: ValidationError;
+} {
+ const result = themePresetValidator.safeParse(theme);
+ if (result.success) return { success: true, data: result.data as Theme };
+ return { success: false, error: result.error };
+}
+
+export function createThemeValidationError(error: unknown): Error {
+ if (!isValidationError(error)) {
+ return error instanceof Error ? error : new Error(String(error));
+ }
+ const message = `Theme validation failed: ${formatValidationIssues(error.issues)}`;
+ const err = new Error(message);
+ err.cause = error;
+ return err;
+}
+
+export function validateTheme(theme: unknown): Theme {
+ try {
+ return parseTheme(theme);
+ } catch (error) {
+ throw createThemeValidationError(error);
+ }
+}
-export const tokenListSchema = z.array(tokenSchema);
+export function validateThemeSafe(theme: unknown): {
+ success: boolean;
+ data?: Theme;
+ error?: ValidationError;
+} {
+ return parseThemeSafe(theme);
+}
-export * from "./validator";
+export { isValidationError, formatValidationIssues, ValidationError };
diff --git a/src/schema/types.ts b/src/schema/types.ts
index 15a7b6a..d4d75d5 100644
--- a/src/schema/types.ts
+++ b/src/schema/types.ts
@@ -1,9 +1,7 @@
-import { z } from "zod";
-import { styleOptionsSchema, tokenSchema, tokenListSchema } from "./index";
+import type { StyleOptions } from "../types";
+import type { Token, TokenList } from "./index";
-export type StyleOptions = z.infer;
-export type Token = z.infer;
-export type TokenList = z.infer;
+export type { StyleOptions, Token, TokenList };
export type JsonSchemaOptions = {
name?: string;
diff --git a/src/schema/utils.ts b/src/schema/utils.ts
index 618030c..357929d 100644
--- a/src/schema/utils.ts
+++ b/src/schema/utils.ts
@@ -1,27 +1,18 @@
-import type { z } from "zod";
import { COLOR_PATTERN } from "./constants";
+import {
+ ValidationError,
+ isValidationError,
+ formatValidationIssues,
+} from "../lib/validate";
export function isValidColorFormat(color: string): boolean {
return COLOR_PATTERN.test(color);
}
-export function formatZodIssues(issues: ReadonlyArray): string {
- return issues
- .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
- .join(", ");
-}
-
export function createValidationError(message: string, cause: Error): Error {
const error = new Error(message);
error.cause = cause;
return error;
}
-export function isZodError(error: unknown): error is z.ZodError {
- return (
- typeof error === "object" &&
- error !== null &&
- "issues" in error &&
- Array.isArray((error as z.ZodError).issues)
- );
-}
+export { isValidationError, formatValidationIssues, ValidationError };
diff --git a/src/schema/validator.ts b/src/schema/validator.ts
index d2ff437..bb2c468 100644
--- a/src/schema/validator.ts
+++ b/src/schema/validator.ts
@@ -1,120 +1,32 @@
-import type { z } from "zod";
-import { zodToJsonSchema } from "zod-to-json-schema";
-import { tokenSchema, tokenListSchema, themePresetSchema } from "./index";
-import type { JsonSchemaOptions } from "./types";
-import type { Theme } from "../types";
+export {
+ parseToken,
+ parseTokenSafe,
+ parseTokenList,
+ parseTokenListSafe,
+ parseTheme,
+ parseThemeSafe,
+ createThemeValidationError,
+ validateTheme,
+ validateThemeSafe,
+ isValidationError,
+ formatValidationIssues,
+ ValidationError,
+} from "./index";
+
+export type { Token, TokenList } from "./index";
+export type { JsonSchemaOptions } from "./types";
+
import {
TOKEN_SCHEMA_NAME,
TOKEN_SCHEMA_DESCRIPTION,
THEME_SCHEMA_NAME,
THEME_SCHEMA_DESCRIPTION,
} from "./constants";
-import { formatZodIssues, createValidationError, isZodError } from "./utils";
-
-export function parseToken(token: unknown): z.infer {
- return tokenSchema.parse(token);
-}
-
-export function parseTokenSafe(token: unknown): {
- success: boolean;
- data?: z.infer;
- error?: z.ZodError;
-} {
- const result = tokenSchema.safeParse(token);
-
- if (result.success) {
- return { success: true, data: result.data };
- }
-
- return { success: false, error: result.error };
-}
-
-export function parseTokenList(
- tokens: unknown,
-): z.infer {
- return tokenListSchema.parse(tokens);
-}
-
-export function parseTokenListSafe(tokens: unknown): {
- success: boolean;
- data?: z.infer;
- error?: z.ZodError;
-} {
- const result = tokenListSchema.safeParse(tokens);
-
- if (result.success) {
- return { success: true, data: result.data };
- }
-
- return { success: false, error: result.error };
-}
-
-export function createTokenJsonSchemaOptions(): JsonSchemaOptions {
- return {
- name: TOKEN_SCHEMA_NAME,
- description: TOKEN_SCHEMA_DESCRIPTION,
- };
-}
-
-export function convertTokenSchemaToJson() {
- const options = createTokenJsonSchemaOptions();
- return zodToJsonSchema(tokenSchema, options);
-}
-
-export function parseTheme(theme: unknown): Theme {
- return themePresetSchema.parse(theme) as Theme;
-}
-
-export function parseThemeSafe(theme: unknown): {
- success: boolean;
- data?: Theme;
- error?: z.ZodError;
-} {
- const result = themePresetSchema.safeParse(theme);
-
- if (result.success) {
- return { success: true, data: result.data as Theme };
- }
-
- return { success: false, error: result.error };
-}
-
-export function createThemeValidationError(error: unknown): Error {
- if (!isZodError(error)) {
- if (error instanceof Error) {
- return error;
- }
- return new Error(String(error));
- }
-
- const message = `Theme validation failed: ${formatZodIssues(error.issues)}`;
- return createValidationError(message, error);
-}
-
-export function validateTheme(theme: unknown): Theme {
- try {
- return parseTheme(theme);
- } catch (error) {
- throw createThemeValidationError(error);
- }
-}
-
-export function validateThemeSafe(theme: unknown): {
- success: boolean;
- data?: Theme;
- error?: z.ZodError;
-} {
- return parseThemeSafe(theme);
-}
-export function createThemeJsonSchemaOptions(): JsonSchemaOptions {
- return {
- name: THEME_SCHEMA_NAME,
- description: THEME_SCHEMA_DESCRIPTION,
- };
+export function createTokenJsonSchemaOptions() {
+ return { name: TOKEN_SCHEMA_NAME, description: TOKEN_SCHEMA_DESCRIPTION };
}
-export function convertThemeSchemaToJson() {
- const options = createThemeJsonSchemaOptions();
- return zodToJsonSchema(themePresetSchema, options);
+export function createThemeJsonSchemaOptions() {
+ return { name: THEME_SCHEMA_NAME, description: THEME_SCHEMA_DESCRIPTION };
}
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/presets.ts b/src/themes/presets.ts
index f2a38d6..f9a263f 100644
--- a/src/themes/presets.ts
+++ b/src/themes/presets.ts
@@ -1,133 +1,75 @@
-import { z } from "zod";
import type { ThemePreset, StyleOptions, PatternMatch } from "../types";
import { filterStyleCodes } from "../types";
-export const colorPaletteSchema = z.object({
- name: z.string(),
- description: z.string(),
- colors: z.object({
- primary: z.string().describe("Main accent color"),
- secondary: z.string().describe("Secondary accent color"),
- success: z.string().describe("Success/OK color"),
- warning: z.string().describe("Warning/attention color"),
- error: z.string().describe("Error/danger color"),
- info: z.string().describe("Information color"),
- muted: z.string().describe("Muted/subtle color"),
- background: z.string().describe("Background color"),
- text: z.string().describe("Primary text color"),
- accent: z.string().optional().describe("Additional accent color"),
- }),
- accessibility: z.object({
- contrastRatio: z.number().min(1).max(21).describe("WCAG contrast ratio"),
- colorBlindSafe: z.boolean().describe("Safe for color blind users"),
- darkMode: z.boolean().describe("Optimized for dark backgrounds"),
- }),
-});
+type ColorRole =
+ | "primary"
+ | "secondary"
+ | "success"
+ | "warning"
+ | "error"
+ | "info"
+ | "muted"
+ | "accent";
-export type ColorPalette = z.infer;
-
-export const patternPresetSchema = z.object({
- name: z.string(),
- description: z.string(),
- category: z.enum([
- "api",
- "system",
- "application",
- "security",
- "database",
- "generic",
- ]),
- patterns: z.array(
- z.object({
- name: z.string(),
- pattern: z.string(),
- description: z.string(),
- colorRole: z.enum([
- "primary",
- "secondary",
- "success",
- "warning",
- "error",
- "info",
- "muted",
- "accent",
- ]),
- styleCodes: z.array(z.string()).optional(),
- }),
- ),
- matchWords: z.record(
- z.string(),
- z.object({
- colorRole: z.enum([
- "primary",
- "secondary",
- "success",
- "warning",
- "error",
- "info",
- "muted",
- "accent",
- ]),
- styleCodes: z.array(z.string()).optional(),
- }),
- ),
-});
-
-export type PatternPreset = z.infer;
+export type ColorPalette = {
+ name: string;
+ description: string;
+ colors: {
+ primary: string;
+ secondary: string;
+ success: string;
+ warning: string;
+ error: string;
+ info: string;
+ muted: string;
+ background: string;
+ text: string;
+ accent?: string;
+ };
+ accessibility: {
+ contrastRatio: number;
+ colorBlindSafe: boolean;
+ darkMode: boolean;
+ };
+};
-export const themeGeneratorConfigSchema = z.object({
- name: z.string(),
- description: z.string().optional(),
- colorPalette: z.string().describe("Color palette name to use"),
- patternPresets: z
- .array(z.string())
- .describe("Pattern preset names to combine"),
- customPatterns: z
- .array(
- z.object({
- name: z.string(),
- pattern: z.string(),
- colorRole: z.enum([
- "primary",
- "secondary",
- "success",
- "warning",
- "error",
- "info",
- "muted",
- "accent",
- ]),
- styleCodes: z.array(z.string()).optional(),
- }),
- )
- .optional(),
- customWords: z
- .record(
- z.string(),
- z.object({
- colorRole: z.enum([
- "primary",
- "secondary",
- "success",
- "warning",
- "error",
- "info",
- "muted",
- "accent",
- ]),
- styleCodes: z.array(z.string()).optional(),
- }),
- )
- .optional(),
- options: z
- .object({
- whiteSpace: z.enum(["preserve", "trim"]).optional(),
- newLine: z.enum(["preserve", "trim"]).optional(),
- })
- .optional(),
-});
+export type PatternPreset = {
+ name: string;
+ description: string;
+ category:
+ | "api"
+ | "system"
+ | "application"
+ | "security"
+ | "database"
+ | "generic";
+ patterns: {
+ name: string;
+ pattern: string;
+ description: string;
+ colorRole: ColorRole;
+ styleCodes?: string[];
+ }[];
+ matchWords: Record;
+};
-export type ThemeGeneratorConfig = z.infer;
+export type ThemeGeneratorConfig = {
+ name: string;
+ description?: string;
+ colorPalette: string;
+ patternPresets: string[];
+ customPatterns?: {
+ name: string;
+ pattern: string;
+ colorRole: ColorRole;
+ styleCodes?: string[];
+ }[];
+ customWords?: Record;
+ options?: {
+ whiteSpace?: "preserve" | "trim";
+ newLine?: "preserve" | "trim";
+ };
+};
export const COLOR_PALETTES: ColorPalette[] = [
{
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..befa8a5 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;
}
@@ -344,11 +347,11 @@ export function addThemeRules(lexer: SimpleLexer, theme: Theme): void {
pattern: string | RegExp;
name?: string;
identifier?: string;
- options?: unknown;
+ options?: StyleOptions;
}>,
);
} 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/colors.ts b/src/utils/colors.ts
index 1eeaa46..95e08d2 100644
--- a/src/utils/colors.ts
+++ b/src/utils/colors.ts
@@ -28,15 +28,15 @@ const styles = {
};
function createColorFunction(style: string) {
- return (text: string) => `${style}${text}${styles.reset}`;
+ return (text: unknown) => `${style}${String(text)}${styles.reset}`;
}
function createChainableColor(
appliedStyles: string[] = [],
): ChainableColorFunction {
- const fn = ((text: string) => {
+ const fn = ((text: unknown) => {
const prefix = appliedStyles.join("");
- return `${prefix}${text}${styles.reset}`;
+ return `${prefix}${String(text)}${styles.reset}`;
}) as ChainableColorFunction;
Object.keys(styles).forEach((key) => {
@@ -67,4 +67,12 @@ export const gray = createColorFunction(styles.gray);
export const dim = createColorFunction(styles.dim);
export const bold = createColorFunction(styles.bold);
+export function hex(color: string): (text: string) => string {
+ const c = color.startsWith("#") ? color.slice(1) : color;
+ const r = parseInt(c.slice(0, 2), 16);
+ const g = parseInt(c.slice(2, 4), 16);
+ const b = parseInt(c.slice(4, 6), 16);
+ return (text: string) => `\x1B[38;2;${r};${g};${b}m${text}${styles.reset}`;
+}
+
export default colors;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 7458374..19c2fbd 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -3,7 +3,13 @@ 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..de6d47e 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -1,25 +1,76 @@
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("[info]"), formatMessage(prefix, message));
+ }
+ },
+
+ success(message: string): void {
+ if (shouldLog("info")) {
+ console.log(colors.green("[ok]"), formatMessage(prefix, message));
+ }
+ },
+
+ warn(message: string): void {
+ if (shouldLog("warn")) {
+ console.log(colors.yellow("[warn]"), formatMessage(prefix, message));
+ }
+ },
+
+ error(message: string): void {
+ if (shouldLog("error")) {
+ console.error(colors.red("[error]"), formatMessage(prefix, message));
+ }
+ },
+
+ debug(message: string): void {
+ if (shouldLog("debug")) {
+ console.log(colors.gray("[debug]"), formatMessage(prefix, message));
+ }
+ },
+ };
+}
+
+export const logger = createLogger();
+
export default logger;
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 0ae1c1e..5a8501f 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -27,6 +27,6 @@ const styles = {
export type StyleName = keyof typeof styles;
-export type ChainableColorFunction = ((text: string) => string) & {
+export type ChainableColorFunction = ((text: unknown) => string) & {
[K in Exclude]: ChainableColorFunction;
};
diff --git a/tests/unit/cli/index.test.ts b/tests/unit/cli/index.test.ts
index e63d619..6d78966 100644
--- a/tests/unit/cli/index.test.ts
+++ b/tests/unit/cli/index.test.ts
@@ -1,6 +1,5 @@
import { expect, test, describe } from "bun:test";
import { parseArgs, loadConfig } from "../../../src/cli/index";
-import { cliOptionsSchema } from "../../../src/cli/types";
import fs from "fs";
import os from "os";
import path from "path";
@@ -302,55 +301,3 @@ describe("loadConfig", () => {
}
});
});
-
-describe("Zod schema validation", () => {
- test("cliOptionsSchema should validate valid options", () => {
- const validOptions = {
- theme: "dracula",
- debug: true,
- output: "result.log",
- format: "ansi" as const,
- };
-
- const result = cliOptionsSchema.parse(validOptions);
- expect(result.theme).toBe("dracula");
- expect(result.debug).toBe(true);
- expect(result.output).toBe("result.log");
- expect(result.format).toBe("ansi");
- });
-
- test("cliOptionsSchema should apply defaults", () => {
- const minimalOptions = {};
-
- const result = cliOptionsSchema.parse(minimalOptions);
- expect(result.debug).toBe(false);
- expect(result.quiet).toBe(false);
- expect(result.listThemes).toBe(false);
- expect(result.interactive).toBe(false);
- expect(result.preview).toBe(false);
- expect(result.noSpinner).toBe(false);
- });
-
- test("cliOptionsSchema should reject invalid format", () => {
- const invalidOptions = {
- format: "invalid",
- };
-
- expect(() => cliOptionsSchema.parse(invalidOptions)).toThrow();
- });
-
- test("cliOptionsSchema should validate commander options", () => {
- const commanderOptions = {
- theme: "oh-my-zsh",
- debug: true,
- interactive: false,
- format: "html",
- };
-
- const result = cliOptionsSchema.parse(commanderOptions);
- expect(result.theme).toBe("oh-my-zsh");
- expect(result.debug).toBe(true);
- expect(result.interactive).toBe(false);
- expect(result.format).toBe("html");
- });
-});
diff --git a/tests/unit/cli/interactive.test.ts b/tests/unit/cli/interactive.test.ts
index 64be571..f9751a1 100644
--- a/tests/unit/cli/interactive.test.ts
+++ b/tests/unit/cli/interactive.test.ts
@@ -1,103 +1,10 @@
import { expect, test, describe } from "bun:test";
import {
- interactiveConfigSchema,
- themeChoiceSchema,
showThemeList,
+ InteractiveConfig,
+ ThemeChoice,
} from "../../../src/cli/interactive";
-describe("Interactive schemas", () => {
- describe("interactiveConfigSchema", () => {
- test("should validate valid interactive config", () => {
- const validConfig = {
- theme: "oh-my-zsh",
- outputFormat: "ansi" as const,
- preview: true,
- };
-
- const result = interactiveConfigSchema.parse(validConfig);
- expect(result.theme).toBe("oh-my-zsh");
- expect(result.outputFormat).toBe("ansi");
- expect(result.preview).toBe(true);
- });
-
- test("should validate with html output format", () => {
- const validConfig = {
- theme: "dracula",
- outputFormat: "html" as const,
- preview: false,
- };
-
- const result = interactiveConfigSchema.parse(validConfig);
- expect(result.theme).toBe("dracula");
- expect(result.outputFormat).toBe("html");
- expect(result.preview).toBe(false);
- });
-
- test("should reject invalid output format", () => {
- const invalidConfig = {
- theme: "oh-my-zsh",
- outputFormat: "invalid",
- preview: true,
- };
-
- expect(() => interactiveConfigSchema.parse(invalidConfig)).toThrow();
- });
-
- test("should require all fields", () => {
- const incompleteConfig = {
- theme: "oh-my-zsh",
- outputFormat: "ansi" as const,
- };
-
- expect(() => interactiveConfigSchema.parse(incompleteConfig)).toThrow();
- });
-
- test("should reject non-boolean preview", () => {
- const invalidConfig = {
- theme: "oh-my-zsh",
- outputFormat: "ansi" as const,
- preview: "yes",
- };
-
- expect(() => interactiveConfigSchema.parse(invalidConfig)).toThrow();
- });
- });
-
- describe("themeChoiceSchema", () => {
- test("should validate valid theme choice", () => {
- const validChoice = {
- name: "Dracula Theme",
- value: "dracula",
- description: "A dark theme with purple accents",
- };
-
- const result = themeChoiceSchema.parse(validChoice);
- expect(result.name).toBe("Dracula Theme");
- expect(result.value).toBe("dracula");
- expect(result.description).toBe("A dark theme with purple accents");
- });
-
- test("should require all fields", () => {
- const incompleteChoice = {
- name: "Theme Name",
- value: "theme-value",
- };
-
- expect(() => themeChoiceSchema.parse(incompleteChoice)).toThrow();
- });
-
- test("should reject non-string fields", () => {
- const invalidChoice = {
- name: 123,
- value: "theme-value",
- description: "Description",
- };
-
- expect(() => themeChoiceSchema.parse(invalidChoice)).toThrow();
- });
- });
-});
-
describe("showThemeList", () => {
test("should not throw when called", () => {
expect(() => showThemeList()).not.toThrow();
@@ -106,7 +13,7 @@ describe("showThemeList", () => {
describe("Type exports", () => {
test("should export InteractiveConfig type", () => {
- const config: typeof import("./interactive").InteractiveConfig = {
+ const config: InteractiveConfig = {
theme: "test",
outputFormat: "ansi",
preview: false,
@@ -118,7 +25,7 @@ describe("Type exports", () => {
});
test("should export ThemeChoice type", () => {
- const choice: typeof import("./interactive").ThemeChoice = {
+ const choice: ThemeChoice = {
name: "Test Theme",
value: "test",
description: "A test theme",
diff --git a/tests/unit/schema/index.test.ts b/tests/unit/schema/index.test.ts
index f7bed66..360ce0d 100644
--- a/tests/unit/schema/index.test.ts
+++ b/tests/unit/schema/index.test.ts
@@ -1,129 +1,75 @@
import { expect, test, describe } from "bun:test";
import {
- styleOptionsSchema,
- tokenMetadataSchema,
- patternMatchSchema,
- schemaConfigSchema,
- themePresetSchema,
- tokenSchema,
- tokenListSchema,
+ parseToken,
+ parseTokenSafe,
+ parseTokenList,
+ parseTokenListSafe,
+ parseTheme,
+ parseThemeSafe,
+ ValidationError,
} from "../../../src/schema/index";
describe("Schema Definitions", () => {
- describe("styleOptionsSchema", () => {
- test("validates valid style options", () => {
- const validStyle = {
- color: "red",
- styleCodes: ["bold", "underline"],
- htmlStyleFormat: "css",
+ describe("parseToken", () => {
+ test("validates token with style", () => {
+ const validToken = {
+ content: "error",
+ metadata: {
+ style: { color: "red" },
+ },
};
- const result = styleOptionsSchema.safeParse(validStyle);
+ const result = parseTokenSafe(validToken);
expect(result.success).toBe(true);
});
- test("requires color property", () => {
- const invalidStyle = {
- styleCodes: ["bold"],
+ test("requires content property", () => {
+ const missingContent = {
+ metadata: { style: { color: "red" } },
};
- const result = styleOptionsSchema.safeParse(invalidStyle);
+ const result = parseTokenSafe(missingContent);
expect(result.success).toBe(false);
});
- test("validates htmlStyleFormat enum values", () => {
- const validStyle = { color: "blue", htmlStyleFormat: "className" };
- const invalidStyle = { color: "blue", htmlStyleFormat: "invalid" };
-
- expect(styleOptionsSchema.safeParse(validStyle).success).toBe(true);
- expect(styleOptionsSchema.safeParse(invalidStyle).success).toBe(false);
- });
- });
-
- describe("tokenMetadataSchema", () => {
- test("validates metadata with style", () => {
- const validMetadata = {
- style: { color: "green" },
- matchType: "word",
- };
-
- const result = tokenMetadataSchema.safeParse(validMetadata);
- expect(result.success).toBe(true);
- });
-
- test("allows additional properties", () => {
- const metadataWithExtra = {
- style: { color: "blue" },
- customField: "value",
- matchType: "regex",
+ test("validates token without metadata", () => {
+ const tokenWithoutMetadata = {
+ content: "just content",
};
- const result = tokenMetadataSchema.safeParse(metadataWithExtra);
+ const result = parseTokenSafe(tokenWithoutMetadata);
expect(result.success).toBe(true);
});
});
- describe("patternMatchSchema", () => {
- test("validates pattern match definition", () => {
- const validPattern = {
- name: "errorPattern",
- pattern: "Error:\\s.*",
- options: { color: "red" },
- };
-
- const result = patternMatchSchema.safeParse(validPattern);
- expect(result.success).toBe(true);
- });
-
- test("requires all properties", () => {
- const missingOptions = {
- name: "errorPattern",
- pattern: "Error:\\s.*",
- };
-
- const result = patternMatchSchema.safeParse(missingOptions);
- expect(result.success).toBe(false);
- });
- });
+ describe("parseTokenList", () => {
+ test("validates token list", () => {
+ const validList = [
+ { content: "error", metadata: { style: { color: "red" } } },
+ { content: " message", metadata: { style: { color: "white" } } },
+ ];
- describe("schemaConfigSchema", () => {
- test("validates empty config", () => {
- const result = schemaConfigSchema.safeParse({});
+ const result = parseTokenListSafe(validList);
expect(result.success).toBe(true);
});
- test("validates full config", () => {
- const fullConfig = {
- defaultStyle: { color: "white" },
- matchWords: { error: { color: "red" } },
- matchStartsWith: { "[ERR]": { color: "red" } },
- matchEndsWith: { failed: { color: "red" } },
- matchContains: { warning: { color: "yellow" } },
- matchPatterns: [
- {
- name: "timestamp",
- pattern: "\\d{4}-\\d{2}-\\d{2}",
- options: { color: "blue" },
- },
- ],
- whiteSpace: "preserve",
- newLine: "trim",
- };
-
- const result = schemaConfigSchema.safeParse(fullConfig);
+ test("validates empty list", () => {
+ const result = parseTokenListSafe([]);
expect(result.success).toBe(true);
});
- test("validates default values", () => {
- const emptyConfig = {};
- const result = schemaConfigSchema.parse(emptyConfig);
+ test("fails on invalid tokens", () => {
+ const invalidList = [
+ { content: "valid" },
+ { invalidProp: "not a token" },
+ ];
- expect(result.whiteSpace).toBe("preserve");
- expect(result.newLine).toBe("preserve");
+ const result = parseTokenListSafe(invalidList);
+ expect(result.success).toBe(false);
});
});
- describe("themePresetSchema", () => {
+ describe("parseTheme", () => {
test("validates theme preset", () => {
const validTheme = {
name: "Dark Theme",
@@ -134,7 +80,7 @@ describe("Schema Definitions", () => {
},
};
- const result = themePresetSchema.safeParse(validTheme);
+ const result = parseThemeSafe(validTheme);
expect(result.success).toBe(true);
});
@@ -143,58 +89,131 @@ describe("Schema Definitions", () => {
name: "Dark Theme",
};
- const result = themePresetSchema.safeParse(missingSchema);
+ const result = parseThemeSafe(missingSchema);
expect(result.success).toBe(false);
});
+
+ test("validates full config", () => {
+ const fullConfig = {
+ name: "Full Theme",
+ schema: {
+ defaultStyle: { color: "white" },
+ matchWords: { error: { color: "red" } },
+ matchStartsWith: { "[ERR]": { color: "red" } },
+ matchEndsWith: { failed: { color: "red" } },
+ matchContains: { warning: { color: "yellow" } },
+ matchPatterns: [
+ {
+ name: "timestamp",
+ pattern: "\\d{4}-\\d{2}-\\d{2}",
+ options: { color: "blue" },
+ },
+ ],
+ whiteSpace: "preserve",
+ newLine: "trim",
+ },
+ };
+
+ const result = parseThemeSafe(fullConfig);
+ expect(result.success).toBe(true);
+ });
+
+ test("validates default values for whiteSpace and newLine", () => {
+ const minimalTheme = {
+ name: "Minimal",
+ schema: {},
+ };
+ const result = parseTheme(minimalTheme);
+
+ expect(result.schema.whiteSpace).toBe("preserve");
+ expect(result.schema.newLine).toBe("preserve");
+ });
});
- describe("tokenSchema", () => {
- test("validates token", () => {
+ describe("Style validation", () => {
+ test("validates valid style in token", () => {
const validToken = {
- content: "error",
+ content: "test",
metadata: {
- style: { color: "red" },
- matchType: "word",
+ style: {
+ color: "red",
+ styleCodes: ["bold", "underline"],
+ htmlStyleFormat: "css",
+ },
},
};
- const result = tokenSchema.safeParse(validToken);
+ const result = parseTokenSafe(validToken);
expect(result.success).toBe(true);
});
- test("requires content property", () => {
- const missingContent = {
- metadata: { style: { color: "red" } },
+ test("requires color in style", () => {
+ const invalidToken = {
+ content: "test",
+ metadata: {
+ style: {
+ styleCodes: ["bold"],
+ },
+ },
};
- const result = tokenSchema.safeParse(missingContent);
+ const result = parseTokenSafe(invalidToken);
expect(result.success).toBe(false);
});
- });
- describe("tokenListSchema", () => {
- test("validates token list", () => {
- const validList = [
- { content: "error", metadata: { style: { color: "red" } } },
- { content: " message", metadata: { style: { color: "white" } } },
- ];
+ test("validates htmlStyleFormat enum values", () => {
+ const validCss = {
+ content: "test",
+ metadata: { style: { color: "blue", htmlStyleFormat: "css" } },
+ };
+ const validClassName = {
+ content: "test",
+ metadata: { style: { color: "blue", htmlStyleFormat: "className" } },
+ };
+ const invalidFormat = {
+ content: "test",
+ metadata: { style: { color: "blue", htmlStyleFormat: "invalid" } },
+ };
- const result = tokenListSchema.safeParse(validList);
- expect(result.success).toBe(true);
+ expect(parseTokenSafe(validCss).success).toBe(true);
+ expect(parseTokenSafe(validClassName).success).toBe(true);
+ expect(parseTokenSafe(invalidFormat).success).toBe(false);
});
+ });
- test("validates empty list", () => {
- const result = tokenListSchema.safeParse([]);
+ describe("Pattern match validation", () => {
+ test("validates pattern match in theme", () => {
+ const theme = {
+ name: "Test",
+ schema: {
+ matchPatterns: [
+ {
+ name: "errorPattern",
+ pattern: "Error:\\s.*",
+ options: { color: "red" },
+ },
+ ],
+ },
+ };
+
+ const result = parseThemeSafe(theme);
expect(result.success).toBe(true);
});
- test("fails on invalid tokens", () => {
- const invalidList = [
- { content: "valid" },
- { invalidProp: "not a token" },
- ];
+ test("requires all pattern match properties", () => {
+ const theme = {
+ name: "Test",
+ schema: {
+ matchPatterns: [
+ {
+ name: "errorPattern",
+ pattern: "Error:\\s.*",
+ },
+ ],
+ },
+ };
- const result = tokenListSchema.safeParse(invalidList);
+ const result = parseThemeSafe(theme);
expect(result.success).toBe(false);
});
});
diff --git a/tests/unit/schema/validator.test.ts b/tests/unit/schema/validator.test.ts
index 1d32bd5..9b86c69 100644
--- a/tests/unit/schema/validator.test.ts
+++ b/tests/unit/schema/validator.test.ts
@@ -6,11 +6,9 @@ import {
parseTokenListSafe,
validateTheme,
validateThemeSafe,
- convertTokenSchemaToJson,
- convertThemeSchemaToJson,
createThemeValidationError,
-} from "../../../src/schema/validator";
-import { z } from "zod";
+ ValidationError,
+} from "../../../src/schema";
describe("Schema Validator", () => {
describe("parseToken", () => {
@@ -19,12 +17,12 @@ describe("Schema Validator", () => {
content: "error",
metadata: {
style: { color: "red" },
- matchType: "word",
},
};
const result = parseToken(validToken);
- expect(result).toEqual(validToken);
+ expect(result.content).toBe("error");
+ expect(result.metadata?.style?.color).toBe("red");
});
test("throws on invalid token", () => {
@@ -57,7 +55,7 @@ describe("Schema Validator", () => {
const result = parseTokenSafe(invalidToken);
expect(result.success).toBe(false);
- expect(result.error).toBeInstanceOf(z.ZodError);
+ expect(result.error).toBeInstanceOf(ValidationError);
});
});
@@ -102,7 +100,7 @@ describe("Schema Validator", () => {
const result = parseTokenListSafe(invalidList);
expect(result.success).toBe(false);
- expect(result.error).toBeInstanceOf(z.ZodError);
+ expect(result.error).toBeInstanceOf(ValidationError);
});
});
@@ -157,33 +155,7 @@ describe("Schema Validator", () => {
const result = validateThemeSafe(invalidTheme);
expect(result.success).toBe(false);
- expect(result.error).toBeInstanceOf(z.ZodError);
- });
- });
-
- describe("convertTokenSchemaToJson", () => {
- test("converts token schema to JSON schema", () => {
- const jsonSchema = convertTokenSchemaToJson();
-
- expect(jsonSchema).toHaveProperty("$schema");
-
- expect(typeof jsonSchema).toBe("object");
-
- const hasNameReference = JSON.stringify(jsonSchema).includes("Token");
- expect(hasNameReference).toBe(true);
- });
- });
-
- describe("convertThemeSchemaToJson", () => {
- test("converts theme schema to JSON schema", () => {
- const jsonSchema = convertThemeSchemaToJson();
-
- expect(jsonSchema).toHaveProperty("$schema");
-
- expect(typeof jsonSchema).toBe("object");
-
- const hasNameReference = JSON.stringify(jsonSchema).includes("Theme");
- expect(hasNameReference).toBe(true);
+ expect(result.error).toBeInstanceOf(ValidationError);
});
});
@@ -215,7 +187,7 @@ describe("Schema Validator", () => {
expect(result.message).toBe("404");
});
- test("formats ZodError with validation message", () => {
+ test("formats ValidationError with validation message", () => {
const invalidTheme = {
name: "test",
};
diff --git a/tests/unit/themes/presets.test.ts b/tests/unit/themes/presets.test.ts
index 682497a..a7b71f0 100644
--- a/tests/unit/themes/presets.test.ts
+++ b/tests/unit/themes/presets.test.ts
@@ -1,8 +1,5 @@
import { expect, test, describe } from "bun:test";
import {
- colorPaletteSchema,
- patternPresetSchema,
- themeGeneratorConfigSchema,
getColorPalette,
getPatternPreset,
listColorPalettes,
@@ -12,94 +9,6 @@ import {
PATTERN_PRESETS,
} from "../../../src/themes/presets";
-describe("Theme Generator Schemas", () => {
- test("colorPaletteSchema should validate valid palette", () => {
- const validPalette = {
- name: "test-palette",
- description: "A test palette",
- colors: {
- primary: "#007acc",
- secondary: "#28a745",
- success: "#1a7f37",
- warning: "#bf8700",
- error: "#d1242f",
- info: "#0969da",
- muted: "#656d76",
- background: "#ffffff",
- text: "#24292f",
- },
- accessibility: {
- contrastRatio: 7.2,
- colorBlindSafe: true,
- darkMode: false,
- },
- };
-
- const result = colorPaletteSchema.parse(validPalette);
- expect(result.name).toBe("test-palette");
- expect(result.colors.primary).toBe("#007acc");
- expect(result.accessibility.contrastRatio).toBe(7.2);
- });
-
- test("patternPresetSchema should validate valid preset", () => {
- const validPreset = {
- name: "test-preset",
- description: "A test preset",
- category: "api" as const,
- patterns: [
- {
- name: "status-code",
- pattern: "\\b\\d{3}\\b",
- description: "HTTP status codes",
- colorRole: "primary" as const,
- styleCodes: ["bold"],
- },
- ],
- matchWords: {
- GET: {
- colorRole: "primary" as const,
- },
- },
- };
-
- const result = patternPresetSchema.parse(validPreset);
- expect(result.name).toBe("test-preset");
- expect(result.category).toBe("api");
- expect(result.patterns[0].colorRole).toBe("primary");
- });
-
- test("themeGeneratorConfigSchema should validate valid config", () => {
- const validConfig = {
- name: "test-theme",
- description: "A test theme",
- colorPalette: "github-light",
- patternPresets: ["log-levels", "http-api"],
- customPatterns: [
- {
- name: "custom-pattern",
- pattern: "\\bCUSTOM\\b",
- colorRole: "warning" as const,
- },
- ],
- customWords: {
- CUSTOM: {
- colorRole: "error" as const,
- styleCodes: ["bold"],
- },
- },
- options: {
- whiteSpace: "preserve" as const,
- newLine: "preserve" as const,
- },
- };
-
- const result = themeGeneratorConfigSchema.parse(validConfig);
- expect(result.name).toBe("test-theme");
- expect(result.patternPresets).toEqual(["log-levels", "http-api"]);
- expect(result.customWords?.CUSTOM.colorRole).toBe("error");
- });
-});
-
describe("Color Palette Functions", () => {
test("getColorPalette should return palette by name", () => {
const palette = getColorPalette("github-light");
@@ -119,9 +28,11 @@ describe("Color Palette Functions", () => {
expect(palettes).toEqual(COLOR_PALETTES);
});
- test("all built-in palettes should be valid", () => {
+ test("all built-in palettes should have required fields", () => {
COLOR_PALETTES.forEach((palette) => {
- expect(() => colorPaletteSchema.parse(palette)).not.toThrow();
+ expect(palette.name).toBeDefined();
+ expect(palette.colors.primary).toBeDefined();
+ expect(palette.colors.error).toBeDefined();
});
});
});
@@ -153,9 +64,11 @@ describe("Pattern Preset Functions", () => {
});
});
- test("all built-in presets should be valid", () => {
+ test("all built-in presets should have required fields", () => {
PATTERN_PRESETS.forEach((preset) => {
- expect(() => patternPresetSchema.parse(preset)).not.toThrow();
+ expect(preset.name).toBeDefined();
+ expect(preset.category).toBeDefined();
+ expect(preset.patterns).toBeDefined();
});
});
});
diff --git a/tests/unit/utils/logger.test.ts b/tests/unit/utils/logger.test.ts
index 06e8492..accd2f0 100644
--- a/tests/unit/utils/logger.test.ts
+++ b/tests/unit/utils/logger.test.ts
@@ -1,60 +1,79 @@
-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;
- test("info() logs with blue info icon", () => {
+ beforeEach(() => {
+ originalLevel = getLogLevel();
+ });
+
+ afterEach(() => {
+ setLogLevel(originalLevel);
+ });
+
+ test("info() logs with blue info label", () => {
const spy = spyOn(console, "log");
logger.info("test message");
expect(spy).toHaveBeenCalledWith(
- expect.stringContaining("ā¹"),
+ expect.stringContaining("[info]"),
"test message",
);
spy.mockRestore();
});
- test("success() logs with green check icon", () => {
+ test("success() logs with green ok label", () => {
const spy = spyOn(console, "log");
logger.success("operation completed");
expect(spy).toHaveBeenCalledWith(
- expect.stringContaining("ā"),
+ expect.stringContaining("[ok]"),
"operation completed",
);
spy.mockRestore();
});
- test("warn() logs with yellow warning icon", () => {
+ test("warn() logs with yellow warn label", () => {
const spy = spyOn(console, "log");
logger.warn("warning message");
expect(spy).toHaveBeenCalledWith(
- expect.stringContaining("ā "),
+ expect.stringContaining("[warn]"),
"warning message",
);
spy.mockRestore();
});
- test("error() logs with red X icon", () => {
+ test("error() logs with red error label", () => {
const spy = spyOn(console, "error");
logger.error("error message");
expect(spy).toHaveBeenCalledWith(
- expect.stringContaining("ā"),
+ expect.stringContaining("[error]"),
"error message",
);
spy.mockRestore();
});
- test("debug() logs with gray gear icon", () => {
+ test("debug() logs with gray debug label when log level is debug", () => {
+ setLogLevel("debug");
const spy = spyOn(console, "log");
logger.debug("debug message");
expect(spy).toHaveBeenCalledWith(
- expect.stringContaining("ā"),
+ expect.stringContaining("[debug]"),
"debug message",
);
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");
@@ -77,10 +96,68 @@ describe("logger", () => {
logger.info(multiline);
expect(logSpy).toHaveBeenCalledWith(
- expect.stringContaining("ā¹"),
+ expect.stringContaining("[info]"),
multiline,
);
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();
+ });
});