From 0c6447a6f522e81de290bfbbfc8553e87b6d8984 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sat, 20 Dec 2025 01:52:25 -0500 Subject: [PATCH 1/4] Implement settings service and routes for file-based settings management - Add SettingsService to handle reading/writing global and project settings. - Introduce API routes for managing settings, including global settings, credentials, and project-specific settings. - Implement migration functionality to transfer settings from localStorage to file-based storage. - Create common utilities for settings routes and integrate logging for error handling. - Update server entry point to include new settings routes. --- apps/server/src/index.ts | 4 + apps/server/src/lib/automaker-paths.ts | 35 + apps/server/src/routes/settings/common.ts | 15 + apps/server/src/routes/settings/index.ts | 38 + .../routes/settings/routes/get-credentials.ts | 23 + .../src/routes/settings/routes/get-global.ts | 23 + .../src/routes/settings/routes/get-project.ts | 34 + .../src/routes/settings/routes/migrate.ts | 54 + .../src/routes/settings/routes/status.ts | 28 + .../settings/routes/update-credentials.ts | 39 + .../routes/settings/routes/update-global.ts | 34 + .../routes/settings/routes/update-project.ts | 48 + apps/server/src/services/settings-service.ts | 542 +++++ apps/server/src/types/settings.ts | 269 +++ apps/ui/src/App.tsx | 30 +- apps/ui/src/components/splash-screen.tsx | 309 +++ apps/ui/src/components/views/setup-view.tsx | 82 +- .../views/setup-view/steps/index.ts | 1 + .../views/setup-view/steps/theme-step.tsx | 90 + apps/ui/src/hooks/use-settings-migration.ts | 261 +++ apps/ui/src/lib/http-api-client.ts | 129 ++ apps/ui/src/routes/__root.tsx | 34 + apps/ui/src/store/app-store.ts | 202 ++ apps/ui/src/store/setup-store.ts | 1 + apps/ui/src/styles/global.css | 1949 ----------------- apps/ui/src/styles/theme-imports.ts | 22 + apps/ui/src/styles/themes/catppuccin.css | 144 ++ apps/ui/src/styles/themes/cream.css | 116 + apps/ui/src/styles/themes/dark.css | 166 ++ apps/ui/src/styles/themes/dracula.css | 144 ++ apps/ui/src/styles/themes/gray.css | 110 + apps/ui/src/styles/themes/gruvbox.css | 144 ++ apps/ui/src/styles/themes/light.css | 103 + apps/ui/src/styles/themes/monokai.css | 144 ++ apps/ui/src/styles/themes/nord.css | 144 ++ apps/ui/src/styles/themes/onedark.css | 144 ++ apps/ui/src/styles/themes/red.css | 70 + apps/ui/src/styles/themes/retro.css | 227 ++ apps/ui/src/styles/themes/solarized.css | 144 ++ apps/ui/src/styles/themes/sunset.css | 111 + apps/ui/src/styles/themes/synthwave.css | 149 ++ apps/ui/src/styles/themes/tokyonight.css | 144 ++ 42 files changed, 4516 insertions(+), 1984 deletions(-) create mode 100644 apps/server/src/routes/settings/common.ts create mode 100644 apps/server/src/routes/settings/index.ts create mode 100644 apps/server/src/routes/settings/routes/get-credentials.ts create mode 100644 apps/server/src/routes/settings/routes/get-global.ts create mode 100644 apps/server/src/routes/settings/routes/get-project.ts create mode 100644 apps/server/src/routes/settings/routes/migrate.ts create mode 100644 apps/server/src/routes/settings/routes/status.ts create mode 100644 apps/server/src/routes/settings/routes/update-credentials.ts create mode 100644 apps/server/src/routes/settings/routes/update-global.ts create mode 100644 apps/server/src/routes/settings/routes/update-project.ts create mode 100644 apps/server/src/services/settings-service.ts create mode 100644 apps/server/src/types/settings.ts create mode 100644 apps/ui/src/components/splash-screen.tsx create mode 100644 apps/ui/src/components/views/setup-view/steps/theme-step.tsx create mode 100644 apps/ui/src/hooks/use-settings-migration.ts create mode 100644 apps/ui/src/styles/theme-imports.ts create mode 100644 apps/ui/src/styles/themes/catppuccin.css create mode 100644 apps/ui/src/styles/themes/cream.css create mode 100644 apps/ui/src/styles/themes/dark.css create mode 100644 apps/ui/src/styles/themes/dracula.css create mode 100644 apps/ui/src/styles/themes/gray.css create mode 100644 apps/ui/src/styles/themes/gruvbox.css create mode 100644 apps/ui/src/styles/themes/light.css create mode 100644 apps/ui/src/styles/themes/monokai.css create mode 100644 apps/ui/src/styles/themes/nord.css create mode 100644 apps/ui/src/styles/themes/onedark.css create mode 100644 apps/ui/src/styles/themes/red.css create mode 100644 apps/ui/src/styles/themes/retro.css create mode 100644 apps/ui/src/styles/themes/solarized.css create mode 100644 apps/ui/src/styles/themes/sunset.css create mode 100644 apps/ui/src/styles/themes/synthwave.css create mode 100644 apps/ui/src/styles/themes/tokyonight.css diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index a4b32872..0cbece15 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -37,10 +37,12 @@ import { isTerminalEnabled, isTerminalPasswordRequired, } from "./routes/terminal/index.js"; +import { createSettingsRoutes } from "./routes/settings/index.js"; import { AgentService } from "./services/agent-service.js"; import { FeatureLoader } from "./services/feature-loader.js"; import { AutoModeService } from "./services/auto-mode-service.js"; import { getTerminalService } from "./services/terminal-service.js"; +import { SettingsService } from "./services/settings-service.js"; import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js"; // Load environment variables @@ -108,6 +110,7 @@ const events: EventEmitter = createEventEmitter(); const agentService = new AgentService(DATA_DIR, events); const featureLoader = new FeatureLoader(); const autoModeService = new AutoModeService(events); +const settingsService = new SettingsService(DATA_DIR); // Initialize services (async () => { @@ -137,6 +140,7 @@ app.use("/api/running-agents", createRunningAgentsRoutes(autoModeService)); app.use("/api/workspace", createWorkspaceRoutes()); app.use("/api/templates", createTemplatesRoutes()); app.use("/api/terminal", createTerminalRoutes()); +app.use("/api/settings", createSettingsRoutes(settingsService)); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/lib/automaker-paths.ts b/apps/server/src/lib/automaker-paths.ts index e11c6d7b..7aad73a7 100644 --- a/apps/server/src/lib/automaker-paths.ts +++ b/apps/server/src/lib/automaker-paths.ts @@ -89,3 +89,38 @@ export async function ensureAutomakerDir(projectPath: string): Promise { await fs.mkdir(automakerDir, { recursive: true }); return automakerDir; } + +// ============================================================================ +// Global Settings Paths (stored in DATA_DIR from app.getPath('userData')) +// ============================================================================ + +/** + * Get the global settings file path + * DATA_DIR is typically ~/Library/Application Support/automaker (macOS) + * or %APPDATA%\automaker (Windows) or ~/.config/automaker (Linux) + */ +export function getGlobalSettingsPath(dataDir: string): string { + return path.join(dataDir, "settings.json"); +} + +/** + * Get the credentials file path (separate from settings for security) + */ +export function getCredentialsPath(dataDir: string): string { + return path.join(dataDir, "credentials.json"); +} + +/** + * Get the project settings file path within a project's .automaker directory + */ +export function getProjectSettingsPath(projectPath: string): string { + return path.join(getAutomakerDir(projectPath), "settings.json"); +} + +/** + * Ensure the global data directory exists + */ +export async function ensureDataDir(dataDir: string): Promise { + await fs.mkdir(dataDir, { recursive: true }); + return dataDir; +} diff --git a/apps/server/src/routes/settings/common.ts b/apps/server/src/routes/settings/common.ts new file mode 100644 index 00000000..bbadf18d --- /dev/null +++ b/apps/server/src/routes/settings/common.ts @@ -0,0 +1,15 @@ +/** + * Common utilities for settings routes + */ + +import { createLogger } from "../../lib/logger.js"; +import { + getErrorMessage as getErrorMessageShared, + createLogError, +} from "../common.js"; + +export const logger = createLogger("Settings"); + +// Re-export shared utilities +export { getErrorMessageShared as getErrorMessage }; +export const logError = createLogError(logger); diff --git a/apps/server/src/routes/settings/index.ts b/apps/server/src/routes/settings/index.ts new file mode 100644 index 00000000..180876b9 --- /dev/null +++ b/apps/server/src/routes/settings/index.ts @@ -0,0 +1,38 @@ +/** + * Settings routes - HTTP API for persistent file-based settings + */ + +import { Router } from "express"; +import type { SettingsService } from "../../services/settings-service.js"; +import { createGetGlobalHandler } from "./routes/get-global.js"; +import { createUpdateGlobalHandler } from "./routes/update-global.js"; +import { createGetCredentialsHandler } from "./routes/get-credentials.js"; +import { createUpdateCredentialsHandler } from "./routes/update-credentials.js"; +import { createGetProjectHandler } from "./routes/get-project.js"; +import { createUpdateProjectHandler } from "./routes/update-project.js"; +import { createMigrateHandler } from "./routes/migrate.js"; +import { createStatusHandler } from "./routes/status.js"; + +export function createSettingsRoutes(settingsService: SettingsService): Router { + const router = Router(); + + // Status endpoint (check if migration needed) + router.get("/status", createStatusHandler(settingsService)); + + // Global settings + router.get("/global", createGetGlobalHandler(settingsService)); + router.put("/global", createUpdateGlobalHandler(settingsService)); + + // Credentials (separate for security) + router.get("/credentials", createGetCredentialsHandler(settingsService)); + router.put("/credentials", createUpdateCredentialsHandler(settingsService)); + + // Project settings + router.post("/project", createGetProjectHandler(settingsService)); + router.put("/project", createUpdateProjectHandler(settingsService)); + + // Migration from localStorage + router.post("/migrate", createMigrateHandler(settingsService)); + + return router; +} diff --git a/apps/server/src/routes/settings/routes/get-credentials.ts b/apps/server/src/routes/settings/routes/get-credentials.ts new file mode 100644 index 00000000..63f93a99 --- /dev/null +++ b/apps/server/src/routes/settings/routes/get-credentials.ts @@ -0,0 +1,23 @@ +/** + * GET /api/settings/credentials - Get credentials (masked for security) + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createGetCredentialsHandler(settingsService: SettingsService) { + return async (_req: Request, res: Response): Promise => { + try { + const credentials = await settingsService.getMaskedCredentials(); + + res.json({ + success: true, + credentials, + }); + } catch (error) { + logError(error, "Get credentials failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/get-global.ts b/apps/server/src/routes/settings/routes/get-global.ts new file mode 100644 index 00000000..4a2c28ca --- /dev/null +++ b/apps/server/src/routes/settings/routes/get-global.ts @@ -0,0 +1,23 @@ +/** + * GET /api/settings/global - Get global settings + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createGetGlobalHandler(settingsService: SettingsService) { + return async (_req: Request, res: Response): Promise => { + try { + const settings = await settingsService.getGlobalSettings(); + + res.json({ + success: true, + settings, + }); + } catch (error) { + logError(error, "Get global settings failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/get-project.ts b/apps/server/src/routes/settings/routes/get-project.ts new file mode 100644 index 00000000..1a380a23 --- /dev/null +++ b/apps/server/src/routes/settings/routes/get-project.ts @@ -0,0 +1,34 @@ +/** + * POST /api/settings/project - Get project settings + * Uses POST because projectPath may contain special characters + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createGetProjectHandler(settingsService: SettingsService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body as { projectPath?: string }; + + if (!projectPath || typeof projectPath !== "string") { + res.status(400).json({ + success: false, + error: "projectPath is required", + }); + return; + } + + const settings = await settingsService.getProjectSettings(projectPath); + + res.json({ + success: true, + settings, + }); + } catch (error) { + logError(error, "Get project settings failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/migrate.ts b/apps/server/src/routes/settings/routes/migrate.ts new file mode 100644 index 00000000..ce056a60 --- /dev/null +++ b/apps/server/src/routes/settings/routes/migrate.ts @@ -0,0 +1,54 @@ +/** + * POST /api/settings/migrate - Migrate settings from localStorage + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import { getErrorMessage, logError, logger } from "../common.js"; + +export function createMigrateHandler(settingsService: SettingsService) { + return async (req: Request, res: Response): Promise => { + try { + const { data } = req.body as { + data?: { + "automaker-storage"?: string; + "automaker-setup"?: string; + "worktree-panel-collapsed"?: string; + "file-browser-recent-folders"?: string; + "automaker:lastProjectDir"?: string; + }; + }; + + if (!data || typeof data !== "object") { + res.status(400).json({ + success: false, + error: "data object is required containing localStorage data", + }); + return; + } + + logger.info("Starting settings migration from localStorage"); + + const result = await settingsService.migrateFromLocalStorage(data); + + if (result.success) { + logger.info( + `Migration successful: ${result.migratedProjectCount} projects migrated` + ); + } else { + logger.warn(`Migration completed with errors: ${result.errors.join(", ")}`); + } + + res.json({ + success: result.success, + migratedGlobalSettings: result.migratedGlobalSettings, + migratedCredentials: result.migratedCredentials, + migratedProjectCount: result.migratedProjectCount, + errors: result.errors, + }); + } catch (error) { + logError(error, "Migration failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/status.ts b/apps/server/src/routes/settings/routes/status.ts new file mode 100644 index 00000000..ee7dff58 --- /dev/null +++ b/apps/server/src/routes/settings/routes/status.ts @@ -0,0 +1,28 @@ +/** + * GET /api/settings/status - Get settings migration status + * Returns whether settings files exist (to determine if migration is needed) + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createStatusHandler(settingsService: SettingsService) { + return async (_req: Request, res: Response): Promise => { + try { + const hasGlobalSettings = await settingsService.hasGlobalSettings(); + const hasCredentials = await settingsService.hasCredentials(); + + res.json({ + success: true, + hasGlobalSettings, + hasCredentials, + dataDir: settingsService.getDataDir(), + needsMigration: !hasGlobalSettings, + }); + } catch (error) { + logError(error, "Get settings status failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/update-credentials.ts b/apps/server/src/routes/settings/routes/update-credentials.ts new file mode 100644 index 00000000..82d544f0 --- /dev/null +++ b/apps/server/src/routes/settings/routes/update-credentials.ts @@ -0,0 +1,39 @@ +/** + * PUT /api/settings/credentials - Update credentials + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import type { Credentials } from "../../../types/settings.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createUpdateCredentialsHandler( + settingsService: SettingsService +) { + return async (req: Request, res: Response): Promise => { + try { + const updates = req.body as Partial; + + if (!updates || typeof updates !== "object") { + res.status(400).json({ + success: false, + error: "Invalid request body - expected credentials object", + }); + return; + } + + await settingsService.updateCredentials(updates); + + // Return masked credentials for confirmation + const masked = await settingsService.getMaskedCredentials(); + + res.json({ + success: true, + credentials: masked, + }); + } catch (error) { + logError(error, "Update credentials failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/update-global.ts b/apps/server/src/routes/settings/routes/update-global.ts new file mode 100644 index 00000000..973efd74 --- /dev/null +++ b/apps/server/src/routes/settings/routes/update-global.ts @@ -0,0 +1,34 @@ +/** + * PUT /api/settings/global - Update global settings + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import type { GlobalSettings } from "../../../types/settings.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createUpdateGlobalHandler(settingsService: SettingsService) { + return async (req: Request, res: Response): Promise => { + try { + const updates = req.body as Partial; + + if (!updates || typeof updates !== "object") { + res.status(400).json({ + success: false, + error: "Invalid request body - expected settings object", + }); + return; + } + + const settings = await settingsService.updateGlobalSettings(updates); + + res.json({ + success: true, + settings, + }); + } catch (error) { + logError(error, "Update global settings failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/settings/routes/update-project.ts b/apps/server/src/routes/settings/routes/update-project.ts new file mode 100644 index 00000000..4b48e52e --- /dev/null +++ b/apps/server/src/routes/settings/routes/update-project.ts @@ -0,0 +1,48 @@ +/** + * PUT /api/settings/project - Update project settings + */ + +import type { Request, Response } from "express"; +import type { SettingsService } from "../../../services/settings-service.js"; +import type { ProjectSettings } from "../../../types/settings.js"; +import { getErrorMessage, logError } from "../common.js"; + +export function createUpdateProjectHandler(settingsService: SettingsService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, updates } = req.body as { + projectPath?: string; + updates?: Partial; + }; + + if (!projectPath || typeof projectPath !== "string") { + res.status(400).json({ + success: false, + error: "projectPath is required", + }); + return; + } + + if (!updates || typeof updates !== "object") { + res.status(400).json({ + success: false, + error: "updates object is required", + }); + return; + } + + const settings = await settingsService.updateProjectSettings( + projectPath, + updates + ); + + res.json({ + success: true, + settings, + }); + } catch (error) { + logError(error, "Update project settings failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts new file mode 100644 index 00000000..0682854f --- /dev/null +++ b/apps/server/src/services/settings-service.ts @@ -0,0 +1,542 @@ +/** + * Settings Service - Handles reading/writing settings to JSON files + * + * Provides persistent storage for: + * - Global settings (DATA_DIR/settings.json) + * - Credentials (DATA_DIR/credentials.json) + * - Per-project settings ({projectPath}/.automaker/settings.json) + */ + +import fs from "fs/promises"; +import path from "path"; +import { createLogger } from "../lib/logger.js"; +import { + getGlobalSettingsPath, + getCredentialsPath, + getProjectSettingsPath, + ensureDataDir, + ensureAutomakerDir, +} from "../lib/automaker-paths.js"; +import type { + GlobalSettings, + Credentials, + ProjectSettings, + KeyboardShortcuts, + AIProfile, + ProjectRef, + TrashedProjectRef, + BoardBackgroundSettings, + WorktreeInfo, +} from "../types/settings.js"; +import { + DEFAULT_GLOBAL_SETTINGS, + DEFAULT_CREDENTIALS, + DEFAULT_PROJECT_SETTINGS, + SETTINGS_VERSION, + CREDENTIALS_VERSION, + PROJECT_SETTINGS_VERSION, +} from "../types/settings.js"; + +const logger = createLogger("SettingsService"); + +/** + * Atomic file write - write to temp file then rename + */ +async function atomicWriteJson(filePath: string, data: unknown): Promise { + const tempPath = `${filePath}.tmp.${Date.now()}`; + const content = JSON.stringify(data, null, 2); + + try { + await fs.writeFile(tempPath, content, "utf-8"); + await fs.rename(tempPath, filePath); + } catch (error) { + // Clean up temp file if it exists + try { + await fs.unlink(tempPath); + } catch { + // Ignore cleanup errors + } + throw error; + } +} + +/** + * Safely read JSON file with fallback to default + */ +async function readJsonFile(filePath: string, defaultValue: T): Promise { + try { + const content = await fs.readFile(filePath, "utf-8"); + return JSON.parse(content) as T; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return defaultValue; + } + logger.error(`Error reading ${filePath}:`, error); + return defaultValue; + } +} + +/** + * Check if a file exists + */ +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +export class SettingsService { + private dataDir: string; + + constructor(dataDir: string) { + this.dataDir = dataDir; + } + + // ============================================================================ + // Global Settings + // ============================================================================ + + /** + * Get global settings + */ + async getGlobalSettings(): Promise { + const settingsPath = getGlobalSettingsPath(this.dataDir); + const settings = await readJsonFile( + settingsPath, + DEFAULT_GLOBAL_SETTINGS + ); + + // Apply any missing defaults (for backwards compatibility) + return { + ...DEFAULT_GLOBAL_SETTINGS, + ...settings, + keyboardShortcuts: { + ...DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts, + ...settings.keyboardShortcuts, + }, + }; + } + + /** + * Update global settings (partial update) + */ + async updateGlobalSettings( + updates: Partial + ): Promise { + await ensureDataDir(this.dataDir); + const settingsPath = getGlobalSettingsPath(this.dataDir); + + const current = await this.getGlobalSettings(); + const updated: GlobalSettings = { + ...current, + ...updates, + version: SETTINGS_VERSION, + }; + + // Deep merge keyboard shortcuts if provided + if (updates.keyboardShortcuts) { + updated.keyboardShortcuts = { + ...current.keyboardShortcuts, + ...updates.keyboardShortcuts, + }; + } + + await atomicWriteJson(settingsPath, updated); + logger.info("Global settings updated"); + + return updated; + } + + /** + * Check if global settings file exists + */ + async hasGlobalSettings(): Promise { + const settingsPath = getGlobalSettingsPath(this.dataDir); + return fileExists(settingsPath); + } + + // ============================================================================ + // Credentials + // ============================================================================ + + /** + * Get credentials + */ + async getCredentials(): Promise { + const credentialsPath = getCredentialsPath(this.dataDir); + const credentials = await readJsonFile( + credentialsPath, + DEFAULT_CREDENTIALS + ); + + return { + ...DEFAULT_CREDENTIALS, + ...credentials, + apiKeys: { + ...DEFAULT_CREDENTIALS.apiKeys, + ...credentials.apiKeys, + }, + }; + } + + /** + * Update credentials (partial update) + */ + async updateCredentials( + updates: Partial + ): Promise { + await ensureDataDir(this.dataDir); + const credentialsPath = getCredentialsPath(this.dataDir); + + const current = await this.getCredentials(); + const updated: Credentials = { + ...current, + ...updates, + version: CREDENTIALS_VERSION, + }; + + // Deep merge api keys if provided + if (updates.apiKeys) { + updated.apiKeys = { + ...current.apiKeys, + ...updates.apiKeys, + }; + } + + await atomicWriteJson(credentialsPath, updated); + logger.info("Credentials updated"); + + return updated; + } + + /** + * Get masked credentials (for UI display - don't expose full keys) + */ + async getMaskedCredentials(): Promise<{ + anthropic: { configured: boolean; masked: string }; + google: { configured: boolean; masked: string }; + openai: { configured: boolean; masked: string }; + }> { + const credentials = await this.getCredentials(); + + const maskKey = (key: string): string => { + if (!key || key.length < 8) return ""; + return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`; + }; + + return { + anthropic: { + configured: !!credentials.apiKeys.anthropic, + masked: maskKey(credentials.apiKeys.anthropic), + }, + google: { + configured: !!credentials.apiKeys.google, + masked: maskKey(credentials.apiKeys.google), + }, + openai: { + configured: !!credentials.apiKeys.openai, + masked: maskKey(credentials.apiKeys.openai), + }, + }; + } + + /** + * Check if credentials file exists + */ + async hasCredentials(): Promise { + const credentialsPath = getCredentialsPath(this.dataDir); + return fileExists(credentialsPath); + } + + // ============================================================================ + // Project Settings + // ============================================================================ + + /** + * Get project settings + */ + async getProjectSettings(projectPath: string): Promise { + const settingsPath = getProjectSettingsPath(projectPath); + const settings = await readJsonFile( + settingsPath, + DEFAULT_PROJECT_SETTINGS + ); + + return { + ...DEFAULT_PROJECT_SETTINGS, + ...settings, + }; + } + + /** + * Update project settings (partial update) + */ + async updateProjectSettings( + projectPath: string, + updates: Partial + ): Promise { + await ensureAutomakerDir(projectPath); + const settingsPath = getProjectSettingsPath(projectPath); + + const current = await this.getProjectSettings(projectPath); + const updated: ProjectSettings = { + ...current, + ...updates, + version: PROJECT_SETTINGS_VERSION, + }; + + // Deep merge board background if provided + if (updates.boardBackground) { + updated.boardBackground = { + ...current.boardBackground, + ...updates.boardBackground, + }; + } + + await atomicWriteJson(settingsPath, updated); + logger.info(`Project settings updated for ${projectPath}`); + + return updated; + } + + /** + * Check if project settings file exists + */ + async hasProjectSettings(projectPath: string): Promise { + const settingsPath = getProjectSettingsPath(projectPath); + return fileExists(settingsPath); + } + + // ============================================================================ + // Migration + // ============================================================================ + + /** + * Migrate settings from localStorage data + * This is called when the UI detects it has localStorage data but no settings files + */ + async migrateFromLocalStorage(localStorageData: { + "automaker-storage"?: string; + "automaker-setup"?: string; + "worktree-panel-collapsed"?: string; + "file-browser-recent-folders"?: string; + "automaker:lastProjectDir"?: string; + }): Promise<{ + success: boolean; + migratedGlobalSettings: boolean; + migratedCredentials: boolean; + migratedProjectCount: number; + errors: string[]; + }> { + const errors: string[] = []; + let migratedGlobalSettings = false; + let migratedCredentials = false; + let migratedProjectCount = 0; + + try { + // Parse the main automaker-storage + let appState: Record = {}; + if (localStorageData["automaker-storage"]) { + try { + const parsed = JSON.parse(localStorageData["automaker-storage"]); + appState = parsed.state || parsed; + } catch (e) { + errors.push(`Failed to parse automaker-storage: ${e}`); + } + } + + // Extract global settings + const globalSettings: Partial = { + theme: (appState.theme as GlobalSettings["theme"]) || "dark", + sidebarOpen: + appState.sidebarOpen !== undefined + ? (appState.sidebarOpen as boolean) + : true, + chatHistoryOpen: (appState.chatHistoryOpen as boolean) || false, + kanbanCardDetailLevel: + (appState.kanbanCardDetailLevel as GlobalSettings["kanbanCardDetailLevel"]) || + "standard", + maxConcurrency: (appState.maxConcurrency as number) || 3, + defaultSkipTests: + appState.defaultSkipTests !== undefined + ? (appState.defaultSkipTests as boolean) + : true, + enableDependencyBlocking: + appState.enableDependencyBlocking !== undefined + ? (appState.enableDependencyBlocking as boolean) + : true, + useWorktrees: (appState.useWorktrees as boolean) || false, + showProfilesOnly: (appState.showProfilesOnly as boolean) || false, + defaultPlanningMode: + (appState.defaultPlanningMode as GlobalSettings["defaultPlanningMode"]) || + "skip", + defaultRequirePlanApproval: + (appState.defaultRequirePlanApproval as boolean) || false, + defaultAIProfileId: + (appState.defaultAIProfileId as string | null) || null, + muteDoneSound: (appState.muteDoneSound as boolean) || false, + enhancementModel: + (appState.enhancementModel as GlobalSettings["enhancementModel"]) || + "sonnet", + keyboardShortcuts: + (appState.keyboardShortcuts as KeyboardShortcuts) || + DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts, + aiProfiles: (appState.aiProfiles as AIProfile[]) || [], + projects: (appState.projects as ProjectRef[]) || [], + trashedProjects: + (appState.trashedProjects as TrashedProjectRef[]) || [], + projectHistory: (appState.projectHistory as string[]) || [], + projectHistoryIndex: (appState.projectHistoryIndex as number) || -1, + lastSelectedSessionByProject: + (appState.lastSelectedSessionByProject as Record) || + {}, + }; + + // Add direct localStorage values + if (localStorageData["automaker:lastProjectDir"]) { + globalSettings.lastProjectDir = + localStorageData["automaker:lastProjectDir"]; + } + + if (localStorageData["file-browser-recent-folders"]) { + try { + globalSettings.recentFolders = JSON.parse( + localStorageData["file-browser-recent-folders"] + ); + } catch { + globalSettings.recentFolders = []; + } + } + + if (localStorageData["worktree-panel-collapsed"]) { + globalSettings.worktreePanelCollapsed = + localStorageData["worktree-panel-collapsed"] === "true"; + } + + // Save global settings + await this.updateGlobalSettings(globalSettings); + migratedGlobalSettings = true; + logger.info("Migrated global settings from localStorage"); + + // Extract and save credentials + if (appState.apiKeys) { + const apiKeys = appState.apiKeys as { + anthropic?: string; + google?: string; + openai?: string; + }; + await this.updateCredentials({ + apiKeys: { + anthropic: apiKeys.anthropic || "", + google: apiKeys.google || "", + openai: apiKeys.openai || "", + }, + }); + migratedCredentials = true; + logger.info("Migrated credentials from localStorage"); + } + + // Migrate per-project settings + const boardBackgroundByProject = appState.boardBackgroundByProject as + | Record + | undefined; + const currentWorktreeByProject = appState.currentWorktreeByProject as + | Record + | undefined; + const worktreesByProject = appState.worktreesByProject as + | Record + | undefined; + + // Get unique project paths that have per-project settings + const projectPaths = new Set(); + if (boardBackgroundByProject) { + Object.keys(boardBackgroundByProject).forEach((p) => + projectPaths.add(p) + ); + } + if (currentWorktreeByProject) { + Object.keys(currentWorktreeByProject).forEach((p) => + projectPaths.add(p) + ); + } + if (worktreesByProject) { + Object.keys(worktreesByProject).forEach((p) => projectPaths.add(p)); + } + + // Also check projects list for theme settings + const projects = (appState.projects as ProjectRef[]) || []; + for (const project of projects) { + if (project.theme) { + projectPaths.add(project.path); + } + } + + // Migrate each project's settings + for (const projectPath of projectPaths) { + try { + const projectSettings: Partial = {}; + + // Get theme from project object + const project = projects.find((p) => p.path === projectPath); + if (project?.theme) { + projectSettings.theme = + project.theme as ProjectSettings["theme"]; + } + + if (boardBackgroundByProject?.[projectPath]) { + projectSettings.boardBackground = + boardBackgroundByProject[projectPath]; + } + + if (currentWorktreeByProject?.[projectPath]) { + projectSettings.currentWorktree = + currentWorktreeByProject[projectPath]; + } + + if (worktreesByProject?.[projectPath]) { + projectSettings.worktrees = worktreesByProject[projectPath]; + } + + if (Object.keys(projectSettings).length > 0) { + await this.updateProjectSettings(projectPath, projectSettings); + migratedProjectCount++; + } + } catch (e) { + errors.push(`Failed to migrate project settings for ${projectPath}: ${e}`); + } + } + + logger.info( + `Migration complete: ${migratedProjectCount} projects migrated` + ); + + return { + success: errors.length === 0, + migratedGlobalSettings, + migratedCredentials, + migratedProjectCount, + errors, + }; + } catch (error) { + logger.error("Migration failed:", error); + errors.push(`Migration failed: ${error}`); + return { + success: false, + migratedGlobalSettings, + migratedCredentials, + migratedProjectCount, + errors, + }; + } + } + + /** + * Get the DATA_DIR path (for debugging/info) + */ + getDataDir(): string { + return this.dataDir; + } +} diff --git a/apps/server/src/types/settings.ts b/apps/server/src/types/settings.ts new file mode 100644 index 00000000..d0fc2cfc --- /dev/null +++ b/apps/server/src/types/settings.ts @@ -0,0 +1,269 @@ +/** + * Settings Types - Shared types for file-based settings storage + */ + +// Theme modes (matching UI ThemeMode type) +export type ThemeMode = + | "light" + | "dark" + | "system" + | "retro" + | "dracula" + | "nord" + | "monokai" + | "tokyonight" + | "solarized" + | "gruvbox" + | "catppuccin" + | "onedark" + | "synthwave" + | "red" + | "cream" + | "sunset" + | "gray"; + +export type KanbanCardDetailLevel = "minimal" | "standard" | "detailed"; +export type AgentModel = "opus" | "sonnet" | "haiku"; +export type PlanningMode = "skip" | "lite" | "spec" | "full"; +export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink"; +export type ModelProvider = "claude"; + +// Keyboard Shortcuts +export interface KeyboardShortcuts { + board: string; + agent: string; + spec: string; + context: string; + settings: string; + profiles: string; + terminal: string; + toggleSidebar: string; + addFeature: string; + addContextFile: string; + startNext: string; + newSession: string; + openProject: string; + projectPicker: string; + cyclePrevProject: string; + cycleNextProject: string; + addProfile: string; + splitTerminalRight: string; + splitTerminalDown: string; + closeTerminal: string; +} + +// AI Profile +export interface AIProfile { + id: string; + name: string; + description: string; + model: AgentModel; + thinkingLevel: ThinkingLevel; + provider: ModelProvider; + isBuiltIn: boolean; + icon?: string; +} + +// Project reference (minimal info stored in global settings) +export interface ProjectRef { + id: string; + name: string; + path: string; + lastOpened?: string; + theme?: string; +} + +// Trashed project reference +export interface TrashedProjectRef extends ProjectRef { + trashedAt: string; + deletedFromDisk?: boolean; +} + +// Chat session (minimal info, full content can be loaded separately) +export interface ChatSessionRef { + id: string; + title: string; + projectId: string; + createdAt: string; + updatedAt: string; + archived: boolean; +} + +/** + * Global Settings - stored in {DATA_DIR}/settings.json + */ +export interface GlobalSettings { + version: number; + + // Theme + theme: ThemeMode; + + // UI State + sidebarOpen: boolean; + chatHistoryOpen: boolean; + kanbanCardDetailLevel: KanbanCardDetailLevel; + + // Feature Defaults + maxConcurrency: number; + defaultSkipTests: boolean; + enableDependencyBlocking: boolean; + useWorktrees: boolean; + showProfilesOnly: boolean; + defaultPlanningMode: PlanningMode; + defaultRequirePlanApproval: boolean; + defaultAIProfileId: string | null; + + // Audio + muteDoneSound: boolean; + + // Enhancement + enhancementModel: AgentModel; + + // Keyboard Shortcuts + keyboardShortcuts: KeyboardShortcuts; + + // AI Profiles + aiProfiles: AIProfile[]; + + // Projects + projects: ProjectRef[]; + trashedProjects: TrashedProjectRef[]; + projectHistory: string[]; + projectHistoryIndex: number; + + // UI Preferences (previously in direct localStorage) + lastProjectDir?: string; + recentFolders: string[]; + worktreePanelCollapsed: boolean; + + // Session tracking (per-project, keyed by project path) + lastSelectedSessionByProject: Record; +} + +/** + * Credentials - stored in {DATA_DIR}/credentials.json + */ +export interface Credentials { + version: number; + apiKeys: { + anthropic: string; + google: string; + openai: string; + }; +} + +/** + * Board Background Settings + */ +export interface BoardBackgroundSettings { + imagePath: string | null; + imageVersion?: number; + cardOpacity: number; + columnOpacity: number; + columnBorderEnabled: boolean; + cardGlassmorphism: boolean; + cardBorderEnabled: boolean; + cardBorderOpacity: number; + hideScrollbar: boolean; +} + +/** + * Worktree Info + */ +export interface WorktreeInfo { + path: string; + branch: string; + isMain: boolean; + hasChanges?: boolean; + changedFilesCount?: number; +} + +/** + * Per-Project Settings - stored in {projectPath}/.automaker/settings.json + */ +export interface ProjectSettings { + version: number; + + // Theme override (null = use global) + theme?: ThemeMode; + + // Worktree settings + useWorktrees?: boolean; + currentWorktree?: { path: string | null; branch: string }; + worktrees?: WorktreeInfo[]; + + // Board background + boardBackground?: BoardBackgroundSettings; + + // Last selected session + lastSelectedSessionId?: string; +} + +// Default values +export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { + board: "K", + agent: "A", + spec: "D", + context: "C", + settings: "S", + profiles: "M", + terminal: "T", + toggleSidebar: "`", + addFeature: "N", + addContextFile: "N", + startNext: "G", + newSession: "N", + openProject: "O", + projectPicker: "P", + cyclePrevProject: "Q", + cycleNextProject: "E", + addProfile: "N", + splitTerminalRight: "Alt+D", + splitTerminalDown: "Alt+S", + closeTerminal: "Alt+W", +}; + +export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { + version: 1, + theme: "dark", + sidebarOpen: true, + chatHistoryOpen: false, + kanbanCardDetailLevel: "standard", + maxConcurrency: 3, + defaultSkipTests: true, + enableDependencyBlocking: true, + useWorktrees: false, + showProfilesOnly: false, + defaultPlanningMode: "skip", + defaultRequirePlanApproval: false, + defaultAIProfileId: null, + muteDoneSound: false, + enhancementModel: "sonnet", + keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, + aiProfiles: [], + projects: [], + trashedProjects: [], + projectHistory: [], + projectHistoryIndex: -1, + lastProjectDir: undefined, + recentFolders: [], + worktreePanelCollapsed: false, + lastSelectedSessionByProject: {}, +}; + +export const DEFAULT_CREDENTIALS: Credentials = { + version: 1, + apiKeys: { + anthropic: "", + google: "", + openai: "", + }, +}; + +export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = { + version: 1, +}; + +export const SETTINGS_VERSION = 1; +export const CREDENTIALS_VERSION = 1; +export const PROJECT_SETTINGS_VERSION = 1; diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index a38bfb42..a38de6b2 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -1,7 +1,35 @@ +import { useState, useCallback } from "react"; import { RouterProvider } from "@tanstack/react-router"; import { router } from "./utils/router"; +import { SplashScreen } from "./components/splash-screen"; +import { useSettingsMigration } from "./hooks/use-settings-migration"; import "./styles/global.css"; +import "./styles/theme-imports"; export default function App() { - return ; + const [showSplash, setShowSplash] = useState(() => { + // Only show splash once per session + if (sessionStorage.getItem("automaker-splash-shown")) { + return false; + } + return true; + }); + + // Run settings migration on startup (localStorage -> file storage) + const migrationState = useSettingsMigration(); + if (migrationState.migrated) { + console.log("[App] Settings migrated to file storage"); + } + + const handleSplashComplete = useCallback(() => { + sessionStorage.setItem("automaker-splash-shown", "true"); + setShowSplash(false); + }, []); + + return ( + <> + + {showSplash && } + + ); } diff --git a/apps/ui/src/components/splash-screen.tsx b/apps/ui/src/components/splash-screen.tsx new file mode 100644 index 00000000..83af7f86 --- /dev/null +++ b/apps/ui/src/components/splash-screen.tsx @@ -0,0 +1,309 @@ +import { useEffect, useState, useMemo } from "react"; + +const TOTAL_DURATION = 2300; // Total animation duration in ms (tightened from 4000) +const LOGO_ENTER_DURATION = 500; // Tightened from 1200 +const PARTICLES_ENTER_DELAY = 100; // Tightened from 400 +const EXIT_START = 1800; // Adjusted for shorter duration + +interface Particle { + id: number; + x: number; + y: number; + size: number; + delay: number; + angle: number; + distance: number; + opacity: number; + floatDuration: number; +} + +function generateParticles(count: number): Particle[] { + return Array.from({ length: count }, (_, i) => { + const angle = (i / count) * 360 + Math.random() * 30; + const distance = 60 + Math.random() * 80; // Increased spread + return { + id: i, + x: Math.cos((angle * Math.PI) / 180) * distance, + y: Math.sin((angle * Math.PI) / 180) * distance, + size: 3 + Math.random() * 6, // Slightly smaller range for more subtle look + delay: Math.random() * 400, + angle, + distance: 300 + Math.random() * 200, + opacity: 0.4 + Math.random() * 0.6, + floatDuration: 3000 + Math.random() * 4000, + }; + }); +} + +export function SplashScreen({ onComplete }: { onComplete: () => void }) { + const [phase, setPhase] = useState<"enter" | "hold" | "exit" | "done">( + "enter" + ); + + const particles = useMemo(() => generateParticles(50), []); + + useEffect(() => { + const timers: NodeJS.Timeout[] = []; + + // Phase transitions + timers.push(setTimeout(() => setPhase("hold"), LOGO_ENTER_DURATION)); + timers.push(setTimeout(() => setPhase("exit"), EXIT_START)); + timers.push( + setTimeout(() => { + setPhase("done"); + onComplete(); + }, TOTAL_DURATION) + ); + + return () => timers.forEach(clearTimeout); + }, [onComplete]); + + if (phase === "done") return null; + + return ( +
+ + + {/* Subtle gradient background */} +
+ + {/* Particle container 1 - Clockwise */} +
+ {particles.slice(0, 25).map((particle) => ( +
+
+
+ ))} +
+ + {/* Particle container 2 - Counter-Clockwise */} +
+ {particles.slice(25).map((particle) => ( +
+
+
+ ))} +
+ + {/* Logo container */} +
+ {/* Glow effect behind logo */} +
+ + {/* The logo */} + + + + + + + + + + + + + + + + + +
+ + {/* Automaker text that fades in below the logo */} +
+ + automaker. + +
+
+ ); +} diff --git a/apps/ui/src/components/views/setup-view.tsx b/apps/ui/src/components/views/setup-view.tsx index d31862ad..f0546839 100644 --- a/apps/ui/src/components/views/setup-view.tsx +++ b/apps/ui/src/components/views/setup-view.tsx @@ -3,6 +3,7 @@ import { useSetupStore } from "@/store/setup-store"; import { StepIndicator } from "./setup-view/components"; import { WelcomeStep, + ThemeStep, CompleteStep, ClaudeSetupStep, GitHubSetupStep, @@ -19,12 +20,13 @@ export function SetupView() { } = useSetupStore(); const navigate = useNavigate(); - const steps = ["welcome", "claude", "github", "complete"] as const; + const steps = ["welcome", "theme", "claude", "github", "complete"] as const; type StepName = (typeof steps)[number]; const getStepName = (): StepName => { if (currentStep === "claude_detect" || currentStep === "claude_auth") return "claude"; if (currentStep === "welcome") return "welcome"; + if (currentStep === "theme") return "theme"; if (currentStep === "github") return "github"; return "complete"; }; @@ -39,6 +41,10 @@ export function SetupView() { ); switch (from) { case "welcome": + console.log("[Setup Flow] Moving to theme step"); + setCurrentStep("theme"); + break; + case "theme": console.log("[Setup Flow] Moving to claude_detect step"); setCurrentStep("claude_detect"); break; @@ -56,9 +62,12 @@ export function SetupView() { const handleBack = (from: string) => { console.log("[Setup Flow] handleBack called from:", from); switch (from) { - case "claude": + case "theme": setCurrentStep("welcome"); break; + case "claude": + setCurrentStep("theme"); + break; case "github": setCurrentStep("claude_detect"); break; @@ -98,42 +107,47 @@ export function SetupView() {
{/* Content */} -
-
-
-
- +
+
+ +
+ +
+ {currentStep === "welcome" && ( + handleNext("welcome")} /> + )} + + {currentStep === "theme" && ( + handleNext("theme")} + onBack={() => handleBack("theme")} /> -
+ )} -
- {currentStep === "welcome" && ( - handleNext("welcome")} /> - )} + {(currentStep === "claude_detect" || + currentStep === "claude_auth") && ( + handleNext("claude")} + onBack={() => handleBack("claude")} + onSkip={handleSkipClaude} + /> + )} - {(currentStep === "claude_detect" || - currentStep === "claude_auth") && ( - handleNext("claude")} - onBack={() => handleBack("claude")} - onSkip={handleSkipClaude} - /> - )} + {currentStep === "github" && ( + handleNext("github")} + onBack={() => handleBack("github")} + onSkip={handleSkipGithub} + /> + )} - {currentStep === "github" && ( - handleNext("github")} - onBack={() => handleBack("github")} - onSkip={handleSkipGithub} - /> - )} - - {currentStep === "complete" && ( - - )} -
+ {currentStep === "complete" && ( + + )}
diff --git a/apps/ui/src/components/views/setup-view/steps/index.ts b/apps/ui/src/components/views/setup-view/steps/index.ts index 7299a070..fef2d09d 100644 --- a/apps/ui/src/components/views/setup-view/steps/index.ts +++ b/apps/ui/src/components/views/setup-view/steps/index.ts @@ -1,5 +1,6 @@ // Re-export all setup step components for easier imports export { WelcomeStep } from "./welcome-step"; +export { ThemeStep } from "./theme-step"; export { CompleteStep } from "./complete-step"; export { ClaudeSetupStep } from "./claude-setup-step"; export { GitHubSetupStep } from "./github-setup-step"; diff --git a/apps/ui/src/components/views/setup-view/steps/theme-step.tsx b/apps/ui/src/components/views/setup-view/steps/theme-step.tsx new file mode 100644 index 00000000..850340b1 --- /dev/null +++ b/apps/ui/src/components/views/setup-view/steps/theme-step.tsx @@ -0,0 +1,90 @@ +import { Button } from "@/components/ui/button"; +import { ArrowRight, ArrowLeft, Check } from "lucide-react"; +import { themeOptions } from "@/config/theme-options"; +import { useAppStore } from "@/store/app-store"; +import { cn } from "@/lib/utils"; + +interface ThemeStepProps { + onNext: () => void; + onBack: () => void; +} + +export function ThemeStep({ onNext, onBack }: ThemeStepProps) { + const { theme, setTheme, setPreviewTheme } = useAppStore(); + + const handleThemeHover = (themeValue: string) => { + setPreviewTheme(themeValue as typeof theme); + }; + + const handleThemeLeave = () => { + setPreviewTheme(null); + }; + + const handleThemeClick = (themeValue: string) => { + setTheme(themeValue as typeof theme); + setPreviewTheme(null); + }; + + return ( +
+
+

+ Choose Your Theme +

+

+ Pick a theme that suits your style. Hover to preview, click to select. +

+
+ +
+ {themeOptions.map((option) => { + const Icon = option.Icon; + const isSelected = theme === option.value; + + return ( + + ); + })} +
+ +
+ + +
+
+ ); +} diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts new file mode 100644 index 00000000..9a941605 --- /dev/null +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -0,0 +1,261 @@ +/** + * Settings Migration Hook + * + * This hook handles migrating settings from localStorage to file-based storage. + * It runs on app startup and: + * 1. Checks if server has settings files + * 2. If not, migrates localStorage data to server + * 3. Clears old localStorage keys after successful migration + * + * This approach keeps localStorage as a fast cache while ensuring + * settings are persisted to files that survive app updates. + */ + +import { useEffect, useState, useRef } from "react"; +import { getHttpApiClient } from "@/lib/http-api-client"; +import { isElectron } from "@/lib/electron"; + +interface MigrationState { + checked: boolean; + migrated: boolean; + error: string | null; +} + +// localStorage keys to migrate +const LOCALSTORAGE_KEYS = [ + "automaker-storage", + "automaker-setup", + "worktree-panel-collapsed", + "file-browser-recent-folders", + "automaker:lastProjectDir", +] as const; + +// Keys to clear after migration (not automaker-storage as it's still used by Zustand) +const KEYS_TO_CLEAR_AFTER_MIGRATION = [ + "worktree-panel-collapsed", + "file-browser-recent-folders", + "automaker:lastProjectDir", + // Legacy keys + "automaker_projects", + "automaker_current_project", + "automaker_trashed_projects", +] as const; + +/** + * Hook to handle settings migration from localStorage to file-based storage + */ +export function useSettingsMigration(): MigrationState { + const [state, setState] = useState({ + checked: false, + migrated: false, + error: null, + }); + const migrationAttempted = useRef(false); + + useEffect(() => { + // Only run once + if (migrationAttempted.current) return; + migrationAttempted.current = true; + + async function checkAndMigrate() { + // Only run migration in Electron mode (web mode uses different storage) + if (!isElectron()) { + setState({ checked: true, migrated: false, error: null }); + return; + } + + try { + const api = getHttpApiClient(); + + // Check if server has settings files + const status = await api.settings.getStatus(); + + if (!status.success) { + console.error("[Settings Migration] Failed to get status:", status); + setState({ + checked: true, + migrated: false, + error: "Failed to check settings status", + }); + return; + } + + // If settings files already exist, no migration needed + if (!status.needsMigration) { + console.log( + "[Settings Migration] Settings files exist, no migration needed" + ); + setState({ checked: true, migrated: false, error: null }); + return; + } + + // Check if we have localStorage data to migrate + const automakerStorage = localStorage.getItem("automaker-storage"); + if (!automakerStorage) { + console.log( + "[Settings Migration] No localStorage data to migrate" + ); + setState({ checked: true, migrated: false, error: null }); + return; + } + + console.log("[Settings Migration] Starting migration..."); + + // Collect all localStorage data + const localStorageData: Record = {}; + for (const key of LOCALSTORAGE_KEYS) { + const value = localStorage.getItem(key); + if (value) { + localStorageData[key] = value; + } + } + + // Send to server for migration + const result = await api.settings.migrate(localStorageData); + + if (result.success) { + console.log("[Settings Migration] Migration successful:", { + globalSettings: result.migratedGlobalSettings, + credentials: result.migratedCredentials, + projects: result.migratedProjectCount, + }); + + // Clear old localStorage keys (but keep automaker-storage for Zustand) + for (const key of KEYS_TO_CLEAR_AFTER_MIGRATION) { + localStorage.removeItem(key); + } + + setState({ checked: true, migrated: true, error: null }); + } else { + console.warn( + "[Settings Migration] Migration had errors:", + result.errors + ); + setState({ + checked: true, + migrated: false, + error: result.errors.join(", "), + }); + } + } catch (error) { + console.error("[Settings Migration] Migration failed:", error); + setState({ + checked: true, + migrated: false, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + checkAndMigrate(); + }, []); + + return state; +} + +/** + * Sync current settings to the server + * Call this when important settings change + */ +export async function syncSettingsToServer(): Promise { + if (!isElectron()) return false; + + try { + const api = getHttpApiClient(); + const automakerStorage = localStorage.getItem("automaker-storage"); + + if (!automakerStorage) { + return false; + } + + const parsed = JSON.parse(automakerStorage); + const state = parsed.state || parsed; + + // Extract settings to sync + const updates = { + theme: state.theme, + sidebarOpen: state.sidebarOpen, + chatHistoryOpen: state.chatHistoryOpen, + kanbanCardDetailLevel: state.kanbanCardDetailLevel, + maxConcurrency: state.maxConcurrency, + defaultSkipTests: state.defaultSkipTests, + enableDependencyBlocking: state.enableDependencyBlocking, + useWorktrees: state.useWorktrees, + showProfilesOnly: state.showProfilesOnly, + defaultPlanningMode: state.defaultPlanningMode, + defaultRequirePlanApproval: state.defaultRequirePlanApproval, + defaultAIProfileId: state.defaultAIProfileId, + muteDoneSound: state.muteDoneSound, + enhancementModel: state.enhancementModel, + keyboardShortcuts: state.keyboardShortcuts, + aiProfiles: state.aiProfiles, + projects: state.projects, + trashedProjects: state.trashedProjects, + projectHistory: state.projectHistory, + projectHistoryIndex: state.projectHistoryIndex, + lastSelectedSessionByProject: state.lastSelectedSessionByProject, + }; + + const result = await api.settings.updateGlobal(updates); + return result.success; + } catch (error) { + console.error("[Settings Sync] Failed to sync settings:", error); + return false; + } +} + +/** + * Sync credentials to the server + * Call this when API keys change + */ +export async function syncCredentialsToServer(apiKeys: { + anthropic?: string; + google?: string; + openai?: string; +}): Promise { + if (!isElectron()) return false; + + try { + const api = getHttpApiClient(); + const result = await api.settings.updateCredentials({ apiKeys }); + return result.success; + } catch (error) { + console.error("[Settings Sync] Failed to sync credentials:", error); + return false; + } +} + +/** + * Sync project settings to the server + * Call this when project-specific settings change + */ +export async function syncProjectSettingsToServer( + projectPath: string, + updates: { + theme?: string; + useWorktrees?: boolean; + boardBackground?: Record; + currentWorktree?: { path: string | null; branch: string }; + worktrees?: Array<{ + path: string; + branch: string; + isMain: boolean; + hasChanges?: boolean; + changedFilesCount?: number; + }>; + } +): Promise { + if (!isElectron()) return false; + + try { + const api = getHttpApiClient(); + const result = await api.settings.updateProject(projectPath, updates); + return result.success; + } catch (error) { + console.error( + "[Settings Sync] Failed to sync project settings:", + error + ); + return false; + } +} diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 5814fa08..5b863f2d 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -837,6 +837,135 @@ export class HttpApiClient implements ElectronAPI { this.post("/api/templates/clone", { repoUrl, projectName, parentDir }), }; + // Settings API - persistent file-based settings + settings = { + // Get settings status (check if migration needed) + getStatus: (): Promise<{ + success: boolean; + hasGlobalSettings: boolean; + hasCredentials: boolean; + dataDir: string; + needsMigration: boolean; + }> => this.get("/api/settings/status"), + + // Global settings + getGlobal: (): Promise<{ + success: boolean; + settings?: { + version: number; + theme: string; + sidebarOpen: boolean; + chatHistoryOpen: boolean; + kanbanCardDetailLevel: string; + maxConcurrency: number; + defaultSkipTests: boolean; + enableDependencyBlocking: boolean; + useWorktrees: boolean; + showProfilesOnly: boolean; + defaultPlanningMode: string; + defaultRequirePlanApproval: boolean; + defaultAIProfileId: string | null; + muteDoneSound: boolean; + enhancementModel: string; + keyboardShortcuts: Record; + aiProfiles: unknown[]; + projects: unknown[]; + trashedProjects: unknown[]; + projectHistory: string[]; + projectHistoryIndex: number; + lastProjectDir?: string; + recentFolders: string[]; + worktreePanelCollapsed: boolean; + lastSelectedSessionByProject: Record; + }; + error?: string; + }> => this.get("/api/settings/global"), + + updateGlobal: (updates: Record): Promise<{ + success: boolean; + settings?: Record; + error?: string; + }> => this.put("/api/settings/global", updates), + + // Credentials (masked for security) + getCredentials: (): Promise<{ + success: boolean; + credentials?: { + anthropic: { configured: boolean; masked: string }; + google: { configured: boolean; masked: string }; + openai: { configured: boolean; masked: string }; + }; + error?: string; + }> => this.get("/api/settings/credentials"), + + updateCredentials: (updates: { + apiKeys?: { anthropic?: string; google?: string; openai?: string }; + }): Promise<{ + success: boolean; + credentials?: { + anthropic: { configured: boolean; masked: string }; + google: { configured: boolean; masked: string }; + openai: { configured: boolean; masked: string }; + }; + error?: string; + }> => this.put("/api/settings/credentials", updates), + + // Project settings + getProject: (projectPath: string): Promise<{ + success: boolean; + settings?: { + version: number; + theme?: string; + useWorktrees?: boolean; + currentWorktree?: { path: string | null; branch: string }; + worktrees?: Array<{ + path: string; + branch: string; + isMain: boolean; + hasChanges?: boolean; + changedFilesCount?: number; + }>; + boardBackground?: { + imagePath: string | null; + imageVersion?: number; + cardOpacity: number; + columnOpacity: number; + columnBorderEnabled: boolean; + cardGlassmorphism: boolean; + cardBorderEnabled: boolean; + cardBorderOpacity: number; + hideScrollbar: boolean; + }; + lastSelectedSessionId?: string; + }; + error?: string; + }> => this.post("/api/settings/project", { projectPath }), + + updateProject: ( + projectPath: string, + updates: Record + ): Promise<{ + success: boolean; + settings?: Record; + error?: string; + }> => this.put("/api/settings/project", { projectPath, updates }), + + // Migration from localStorage + migrate: (data: { + "automaker-storage"?: string; + "automaker-setup"?: string; + "worktree-panel-collapsed"?: string; + "file-browser-recent-folders"?: string; + "automaker:lastProjectDir"?: string; + }): Promise<{ + success: boolean; + migratedGlobalSettings: boolean; + migratedCredentials: boolean; + migratedProjectCount: number; + errors: string[]; + }> => this.post("/api/settings/migrate", { data }), + }; + // Sessions API sessions = { list: ( diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx index c144dd78..ed06c601 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/apps/ui/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from "react"; import { Sidebar } from "@/components/layout/sidebar"; import { FileBrowserProvider, useFileBrowser, setGlobalFileBrowser } from "@/contexts/file-browser-context"; import { useAppStore } from "@/store/app-store"; +import { useSetupStore } from "@/store/setup-store"; import { getElectronAPI } from "@/lib/electron"; import { Toaster } from "sonner"; import { ThemeOption, themeOptions } from "@/config/theme-options"; @@ -16,9 +17,13 @@ function RootLayoutContent() { previewTheme, getEffectiveTheme, } = useAppStore(); + const { setupComplete } = useSetupStore(); const navigate = useNavigate(); const [isMounted, setIsMounted] = useState(false); const [streamerPanelOpen, setStreamerPanelOpen] = useState(false); + const [setupHydrated, setSetupHydrated] = useState(() => + useSetupStore.persist?.hasHydrated?.() ?? false + ); const { openFileBrowser } = useFileBrowser(); // Hidden streamer panel - opens with "\" key @@ -61,6 +66,35 @@ function RootLayoutContent() { setIsMounted(true); }, []); + // Wait for setup store hydration before enforcing routing rules + useEffect(() => { + if (useSetupStore.persist?.hasHydrated?.()) { + setSetupHydrated(true); + return; + } + + const unsubscribe = useSetupStore.persist?.onFinishHydration?.(() => { + setSetupHydrated(true); + }); + + return () => { + if (typeof unsubscribe === "function") { + unsubscribe(); + } + }; + }, []); + + // Redirect first-run users (or anyone who reopened the wizard) to /setup + useEffect(() => { + if (!setupHydrated) return; + + if (!setupComplete && location.pathname !== "/setup") { + navigate({ to: "/setup" }); + } else if (setupComplete && location.pathname === "/setup") { + navigate({ to: "/" }); + } + }, [setupComplete, setupHydrated, location.pathname, navigate]); + useEffect(() => { setGlobalFileBrowser(openFileBrowser); }, [openFileBrowser]); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index ee128598..f433578a 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -2356,3 +2356,205 @@ export const useAppStore = create()( } ) ); + +// ============================================================================ +// Settings Sync to Server (file-based storage) +// ============================================================================ + +// Debounced sync function to avoid excessive server calls +let syncTimeoutId: NodeJS.Timeout | null = null; +const SYNC_DEBOUNCE_MS = 2000; // Wait 2 seconds after last change before syncing + +/** + * Schedule a sync of current settings to the server + * This is debounced to avoid excessive API calls + */ +function scheduleSyncToServer() { + // Only sync in Electron mode + if (typeof window === "undefined") return; + + // Clear any pending sync + if (syncTimeoutId) { + clearTimeout(syncTimeoutId); + } + + // Schedule new sync + syncTimeoutId = setTimeout(async () => { + try { + // Dynamic import to avoid circular dependencies + const { syncSettingsToServer } = await import( + "@/hooks/use-settings-migration" + ); + await syncSettingsToServer(); + } catch (error) { + console.error("[AppStore] Failed to sync settings to server:", error); + } + }, SYNC_DEBOUNCE_MS); +} + +// Subscribe to store changes and sync to server +// Only sync when important settings change (not every state change) +let previousState: Partial | null = null; +let previousProjectSettings: Record< + string, + { + theme?: string; + boardBackground?: typeof initialState.boardBackgroundByProject[string]; + currentWorktree?: typeof initialState.currentWorktreeByProject[string]; + worktrees?: typeof initialState.worktreesByProject[string]; + } +> = {}; + +// Track pending project syncs (debounced per project) +const projectSyncTimeouts: Record = {}; +const PROJECT_SYNC_DEBOUNCE_MS = 2000; + +/** + * Schedule sync of project settings to server + */ +function scheduleProjectSettingsSync( + projectPath: string, + updates: Record +) { + // Only sync in Electron mode + if (typeof window === "undefined") return; + + // Clear any pending sync for this project + if (projectSyncTimeouts[projectPath]) { + clearTimeout(projectSyncTimeouts[projectPath]); + } + + // Schedule new sync + projectSyncTimeouts[projectPath] = setTimeout(async () => { + try { + const { syncProjectSettingsToServer } = await import( + "@/hooks/use-settings-migration" + ); + await syncProjectSettingsToServer(projectPath, updates); + } catch (error) { + console.error( + `[AppStore] Failed to sync project settings for ${projectPath}:`, + error + ); + } + delete projectSyncTimeouts[projectPath]; + }, PROJECT_SYNC_DEBOUNCE_MS); +} + +useAppStore.subscribe((state) => { + // Skip if this is the initial load + if (!previousState) { + previousState = { + theme: state.theme, + projects: state.projects, + trashedProjects: state.trashedProjects, + keyboardShortcuts: state.keyboardShortcuts, + aiProfiles: state.aiProfiles, + maxConcurrency: state.maxConcurrency, + defaultSkipTests: state.defaultSkipTests, + enableDependencyBlocking: state.enableDependencyBlocking, + useWorktrees: state.useWorktrees, + showProfilesOnly: state.showProfilesOnly, + muteDoneSound: state.muteDoneSound, + enhancementModel: state.enhancementModel, + defaultPlanningMode: state.defaultPlanningMode, + defaultRequirePlanApproval: state.defaultRequirePlanApproval, + defaultAIProfileId: state.defaultAIProfileId, + }; + // Initialize project settings tracking + for (const project of state.projects) { + previousProjectSettings[project.path] = { + theme: project.theme, + boardBackground: state.boardBackgroundByProject[project.path], + currentWorktree: state.currentWorktreeByProject[project.path], + worktrees: state.worktreesByProject[project.path], + }; + } + return; + } + + // Check if any important global settings changed + const importantSettingsChanged = + state.theme !== previousState.theme || + state.projects !== previousState.projects || + state.trashedProjects !== previousState.trashedProjects || + state.keyboardShortcuts !== previousState.keyboardShortcuts || + state.aiProfiles !== previousState.aiProfiles || + state.maxConcurrency !== previousState.maxConcurrency || + state.defaultSkipTests !== previousState.defaultSkipTests || + state.enableDependencyBlocking !== previousState.enableDependencyBlocking || + state.useWorktrees !== previousState.useWorktrees || + state.showProfilesOnly !== previousState.showProfilesOnly || + state.muteDoneSound !== previousState.muteDoneSound || + state.enhancementModel !== previousState.enhancementModel || + state.defaultPlanningMode !== previousState.defaultPlanningMode || + state.defaultRequirePlanApproval !== previousState.defaultRequirePlanApproval || + state.defaultAIProfileId !== previousState.defaultAIProfileId; + + if (importantSettingsChanged) { + // Update previous state + previousState = { + theme: state.theme, + projects: state.projects, + trashedProjects: state.trashedProjects, + keyboardShortcuts: state.keyboardShortcuts, + aiProfiles: state.aiProfiles, + maxConcurrency: state.maxConcurrency, + defaultSkipTests: state.defaultSkipTests, + enableDependencyBlocking: state.enableDependencyBlocking, + useWorktrees: state.useWorktrees, + showProfilesOnly: state.showProfilesOnly, + muteDoneSound: state.muteDoneSound, + enhancementModel: state.enhancementModel, + defaultPlanningMode: state.defaultPlanningMode, + defaultRequirePlanApproval: state.defaultRequirePlanApproval, + defaultAIProfileId: state.defaultAIProfileId, + }; + + // Schedule sync to server + scheduleSyncToServer(); + } + + // Check for per-project settings changes + for (const project of state.projects) { + const projectPath = project.path; + const prev = previousProjectSettings[projectPath] || {}; + const updates: Record = {}; + + // Check if project theme changed + if (project.theme !== prev.theme) { + updates.theme = project.theme; + } + + // Check if board background changed + const currentBg = state.boardBackgroundByProject[projectPath]; + if (currentBg !== prev.boardBackground) { + updates.boardBackground = currentBg; + } + + // Check if current worktree changed + const currentWt = state.currentWorktreeByProject[projectPath]; + if (currentWt !== prev.currentWorktree) { + updates.currentWorktree = currentWt; + } + + // Check if worktrees list changed + const worktrees = state.worktreesByProject[projectPath]; + if (worktrees !== prev.worktrees) { + updates.worktrees = worktrees; + } + + // If any project settings changed, sync them + if (Object.keys(updates).length > 0) { + scheduleProjectSettingsSync(projectPath, updates); + + // Update tracking + previousProjectSettings[projectPath] = { + theme: project.theme, + boardBackground: currentBg, + currentWorktree: currentWt, + worktrees: worktrees, + }; + } + } +}); diff --git a/apps/ui/src/store/setup-store.ts b/apps/ui/src/store/setup-store.ts index 4d2ac6f7..6e2fa907 100644 --- a/apps/ui/src/store/setup-store.ts +++ b/apps/ui/src/store/setup-store.ts @@ -53,6 +53,7 @@ export interface InstallProgress { export type SetupStep = | "welcome" + | "theme" | "claude_detect" | "claude_auth" | "github" diff --git a/apps/ui/src/styles/global.css b/apps/ui/src/styles/global.css index 04e15212..59dce140 100644 --- a/apps/ui/src/styles/global.css +++ b/apps/ui/src/styles/global.css @@ -336,1138 +336,6 @@ --running-indicator-text: oklch(0.6 0.22 265); } -.dark { - /* Deep dark backgrounds - zinc-950 family */ - --background: oklch(0.04 0 0); /* zinc-950 */ - --background-50: oklch(0.04 0 0 / 0.5); /* zinc-950/50 */ - --background-80: oklch(0.04 0 0 / 0.8); /* zinc-950/80 */ - - /* Text colors following hierarchy */ - --foreground: oklch(1 0 0); /* text-white */ - --foreground-secondary: oklch(0.588 0 0); /* text-zinc-400 */ - --foreground-muted: oklch(0.525 0 0); /* text-zinc-500 */ - - /* Card and popover backgrounds */ - --card: oklch(0.14 0 0); /* slightly lighter than background for contrast */ - --card-foreground: oklch(1 0 0); - --popover: oklch(0.10 0 0); /* slightly lighter than background */ - --popover-foreground: oklch(1 0 0); - - /* Brand colors - purple/violet theme */ - --primary: oklch(0.55 0.25 265); /* brand-500 */ - --primary-foreground: oklch(1 0 0); - --brand-400: oklch(0.6 0.22 265); - --brand-500: oklch(0.55 0.25 265); - --brand-600: oklch(0.5 0.28 270); /* purple-600 for gradients */ - - /* Glass morphism borders and accents */ - --secondary: oklch(1 0 0 / 0.05); /* bg-white/5 */ - --secondary-foreground: oklch(1 0 0); - --muted: oklch(0.176 0 0); /* zinc-800 */ - --muted-foreground: oklch(0.588 0 0); /* text-zinc-400 */ - --accent: oklch(1 0 0 / 0.1); /* bg-white/10 for hover */ - --accent-foreground: oklch(1 0 0); - - /* Borders with transparency for glass effect */ - --border: oklch(0.176 0 0); /* zinc-800 */ - --border-glass: oklch(1 0 0 / 0.1); /* white/10 for glass morphism */ - --destructive: oklch(0.6 0.25 25); - --input: oklch(0.04 0 0 / 0.8); /* Semi-transparent dark */ - --ring: oklch(0.55 0.25 265); - - /* Chart colors with brand theme */ - --chart-1: oklch(0.55 0.25 265); - --chart-2: oklch(0.65 0.2 160); - --chart-3: oklch(0.75 0.2 70); - --chart-4: oklch(0.6 0.25 300); - --chart-5: oklch(0.6 0.25 20); - - /* Sidebar with glass morphism */ - --sidebar: oklch(0.04 0 0 / 0.5); /* zinc-950/50 with backdrop blur */ - --sidebar-foreground: oklch(1 0 0); - --sidebar-primary: oklch(0.55 0.25 265); - --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(1 0 0 / 0.05); /* bg-white/5 */ - --sidebar-accent-foreground: oklch(1 0 0); - --sidebar-border: oklch(1 0 0 / 0.1); /* white/10 for glass borders */ - --sidebar-ring: oklch(0.55 0.25 265); - - /* Action button colors */ - --action-view: oklch(0.6 0.25 265); /* Purple */ - --action-view-hover: oklch(0.55 0.27 270); - --action-followup: oklch(0.6 0.2 230); /* Blue */ - --action-followup-hover: oklch(0.55 0.22 230); - --action-commit: oklch(0.55 0.2 140); /* Green */ - --action-commit-hover: oklch(0.5 0.22 140); - --action-verify: oklch(0.55 0.2 140); /* Green */ - --action-verify-hover: oklch(0.5 0.22 140); - - /* Running indicator - Purple */ - --running-indicator: oklch(0.6 0.25 265); - --running-indicator-text: oklch(0.65 0.22 265); - - /* Status colors - Dark mode */ - --status-success: oklch(0.65 0.2 140); - --status-success-bg: oklch(0.65 0.2 140 / 0.2); - --status-warning: oklch(0.75 0.15 70); - --status-warning-bg: oklch(0.75 0.15 70 / 0.2); - --status-error: oklch(0.65 0.22 25); - --status-error-bg: oklch(0.65 0.22 25 / 0.2); - --status-info: oklch(0.65 0.2 230); - --status-info-bg: oklch(0.65 0.2 230 / 0.2); - --status-backlog: oklch(0.6 0 0); - --status-in-progress: oklch(0.75 0.15 70); - --status-waiting: oklch(0.7 0.18 50); - - /* Shadow tokens - darker for dark mode */ - --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3); -} - -.retro { - /* Retro / Cyberpunk Theme */ - --background: oklch(0 0 0); /* Pure Black */ - --background-50: oklch(0 0 0 / 0.5); - --background-80: oklch(0 0 0 / 0.8); - - /* Neon Green Text */ - --foreground: oklch(0.85 0.25 145); /* Neon Green */ - --foreground-secondary: oklch(0.7 0.2 145); - --foreground-muted: oklch(0.5 0.15 145); - - /* Hard Edges */ - --radius: 0px; - - /* UI Elements */ - --card: oklch(0 0 0); /* Black card */ - --card-foreground: oklch(0.85 0.25 145); - --popover: oklch(0.05 0.05 145); - --popover-foreground: oklch(0.85 0.25 145); - - --primary: oklch(0.85 0.25 145); /* Neon Green */ - --primary-foreground: oklch(0 0 0); /* Black text on green */ - - --brand-400: oklch(0.85 0.25 145); - --brand-500: oklch(0.85 0.25 145); - --brand-600: oklch(0.75 0.25 145); - - --secondary: oklch(0.1 0.1 145); /* Dark Green bg */ - --secondary-foreground: oklch(0.85 0.25 145); - - --muted: oklch(0.1 0.05 145); - --muted-foreground: oklch(0.5 0.15 145); - - --accent: oklch(0.2 0.2 145); /* Brighter green accent */ - --accent-foreground: oklch(0.85 0.25 145); - - --destructive: oklch(0.6 0.25 25); /* Keep red for destructive */ - - --border: oklch(0.3 0.15 145); /* Visible Green Border */ - --border-glass: oklch(0.85 0.25 145 / 0.3); - - --input: oklch(0.1 0.1 145); - --ring: oklch(0.85 0.25 145); - - /* Charts - various neons */ - --chart-1: oklch(0.85 0.25 145); /* Green */ - --chart-2: oklch(0.8 0.25 300); /* Purple Neon */ - --chart-3: oklch(0.8 0.25 200); /* Cyan Neon */ - --chart-4: oklch(0.8 0.25 60); /* Yellow Neon */ - --chart-5: oklch(0.8 0.25 20); /* Red Neon */ - - /* Sidebar */ - --sidebar: oklch(0 0 0); - --sidebar-foreground: oklch(0.85 0.25 145); - --sidebar-primary: oklch(0.85 0.25 145); - --sidebar-primary-foreground: oklch(0 0 0); - --sidebar-accent: oklch(0.1 0.1 145); - --sidebar-accent-foreground: oklch(0.85 0.25 145); - --sidebar-border: oklch(0.3 0.15 145); - --sidebar-ring: oklch(0.85 0.25 145); - - /* Fonts */ - --font-sans: var(--font-geist-mono); /* Force Mono everywhere */ - - /* Action button colors - All green neon for retro theme */ - --action-view: oklch(0.85 0.25 145); /* Neon Green */ - --action-view-hover: oklch(0.9 0.25 145); - --action-followup: oklch(0.85 0.25 145); /* Neon Green */ - --action-followup-hover: oklch(0.9 0.25 145); - --action-commit: oklch(0.85 0.25 145); /* Neon Green */ - --action-commit-hover: oklch(0.9 0.25 145); - --action-verify: oklch(0.85 0.25 145); /* Neon Green */ - --action-verify-hover: oklch(0.9 0.25 145); - - /* Running indicator - Neon Green for retro */ - --running-indicator: oklch(0.85 0.25 145); - --running-indicator-text: oklch(0.85 0.25 145); -} - -/* ======================================== - DRACULA THEME - Inspired by the popular Dracula VS Code theme - ======================================== */ -.dracula { - --background: oklch(0.18 0.02 280); /* #282a36 */ - --background-50: oklch(0.18 0.02 280 / 0.5); - --background-80: oklch(0.18 0.02 280 / 0.8); - - --foreground: oklch(0.95 0.01 280); /* #f8f8f2 */ - --foreground-secondary: oklch(0.7 0.05 280); - --foreground-muted: oklch(0.55 0.08 280); /* #6272a4 */ - - --card: oklch(0.22 0.02 280); /* #44475a */ - --card-foreground: oklch(0.95 0.01 280); - --popover: oklch(0.2 0.02 280); - --popover-foreground: oklch(0.95 0.01 280); - - --primary: oklch(0.7 0.2 320); /* #bd93f9 purple */ - --primary-foreground: oklch(0.18 0.02 280); - - --brand-400: oklch(0.75 0.2 320); - --brand-500: oklch(0.7 0.2 320); /* #bd93f9 */ - --brand-600: oklch(0.65 0.22 320); - - --secondary: oklch(0.28 0.03 280); /* #44475a */ - --secondary-foreground: oklch(0.95 0.01 280); - - --muted: oklch(0.28 0.03 280); - --muted-foreground: oklch(0.55 0.08 280); /* #6272a4 */ - - --accent: oklch(0.32 0.04 280); - --accent-foreground: oklch(0.95 0.01 280); - - --destructive: oklch(0.65 0.25 15); /* #ff5555 */ - - --border: oklch(0.35 0.05 280); - --border-glass: oklch(0.7 0.2 320 / 0.3); - - --input: oklch(0.22 0.02 280); - --ring: oklch(0.7 0.2 320); - - --chart-1: oklch(0.7 0.2 320); /* Purple */ - --chart-2: oklch(0.75 0.2 180); /* Cyan #8be9fd */ - --chart-3: oklch(0.8 0.2 130); /* Green #50fa7b */ - --chart-4: oklch(0.7 0.25 350); /* Pink #ff79c6 */ - --chart-5: oklch(0.85 0.2 90); /* Yellow #f1fa8c */ - - --sidebar: oklch(0.16 0.02 280); - --sidebar-foreground: oklch(0.95 0.01 280); - --sidebar-primary: oklch(0.7 0.2 320); - --sidebar-primary-foreground: oklch(0.18 0.02 280); - --sidebar-accent: oklch(0.28 0.03 280); - --sidebar-accent-foreground: oklch(0.95 0.01 280); - --sidebar-border: oklch(0.35 0.05 280); - --sidebar-ring: oklch(0.7 0.2 320); - - /* Action button colors - Dracula purple/pink theme */ - --action-view: oklch(0.7 0.2 320); /* Purple */ - --action-view-hover: oklch(0.65 0.22 320); - --action-followup: oklch(0.65 0.25 350); /* Pink */ - --action-followup-hover: oklch(0.6 0.27 350); - --action-commit: oklch(0.75 0.2 130); /* Green */ - --action-commit-hover: oklch(0.7 0.22 130); - --action-verify: oklch(0.75 0.2 130); /* Green */ - --action-verify-hover: oklch(0.7 0.22 130); - - /* Running indicator - Purple */ - --running-indicator: oklch(0.7 0.2 320); - --running-indicator-text: oklch(0.75 0.18 320); -} - -/* ======================================== - NORD THEME - Inspired by the Arctic, north-bluish color palette - ======================================== */ -.nord { - --background: oklch(0.23 0.02 240); /* #2e3440 */ - --background-50: oklch(0.23 0.02 240 / 0.5); - --background-80: oklch(0.23 0.02 240 / 0.8); - - --foreground: oklch(0.9 0.01 230); /* #eceff4 */ - --foreground-secondary: oklch(0.75 0.02 230); /* #d8dee9 */ - --foreground-muted: oklch(0.6 0.03 230); /* #4c566a */ - - --card: oklch(0.27 0.02 240); /* #3b4252 */ - --card-foreground: oklch(0.9 0.01 230); - --popover: oklch(0.25 0.02 240); - --popover-foreground: oklch(0.9 0.01 230); - - --primary: oklch(0.7 0.12 220); /* #88c0d0 frost */ - --primary-foreground: oklch(0.23 0.02 240); - - --brand-400: oklch(0.75 0.12 220); - --brand-500: oklch(0.7 0.12 220); /* #88c0d0 */ - --brand-600: oklch(0.65 0.14 220); /* #81a1c1 */ - - --secondary: oklch(0.31 0.02 240); /* #434c5e */ - --secondary-foreground: oklch(0.9 0.01 230); - - --muted: oklch(0.31 0.02 240); - --muted-foreground: oklch(0.55 0.03 230); - - --accent: oklch(0.35 0.03 240); /* #4c566a */ - --accent-foreground: oklch(0.9 0.01 230); - - --destructive: oklch(0.65 0.2 15); /* #bf616a */ - - --border: oklch(0.35 0.03 240); - --border-glass: oklch(0.7 0.12 220 / 0.3); - - --input: oklch(0.27 0.02 240); - --ring: oklch(0.7 0.12 220); - - --chart-1: oklch(0.7 0.12 220); /* Frost blue */ - --chart-2: oklch(0.65 0.14 220); /* #81a1c1 */ - --chart-3: oklch(0.7 0.15 140); /* #a3be8c green */ - --chart-4: oklch(0.7 0.2 320); /* #b48ead purple */ - --chart-5: oklch(0.75 0.15 70); /* #ebcb8b yellow */ - - --sidebar: oklch(0.21 0.02 240); - --sidebar-foreground: oklch(0.9 0.01 230); - --sidebar-primary: oklch(0.7 0.12 220); - --sidebar-primary-foreground: oklch(0.23 0.02 240); - --sidebar-accent: oklch(0.31 0.02 240); - --sidebar-accent-foreground: oklch(0.9 0.01 230); - --sidebar-border: oklch(0.35 0.03 240); - --sidebar-ring: oklch(0.7 0.12 220); - - /* Action button colors - Nord frost blue theme */ - --action-view: oklch(0.7 0.12 220); /* Frost blue */ - --action-view-hover: oklch(0.65 0.14 220); - --action-followup: oklch(0.65 0.14 220); /* Darker frost */ - --action-followup-hover: oklch(0.6 0.16 220); - --action-commit: oklch(0.7 0.15 140); /* Green */ - --action-commit-hover: oklch(0.65 0.17 140); - --action-verify: oklch(0.7 0.15 140); /* Green */ - --action-verify-hover: oklch(0.65 0.17 140); - - /* Running indicator - Frost blue */ - --running-indicator: oklch(0.7 0.12 220); - --running-indicator-text: oklch(0.75 0.1 220); -} - -/* ======================================== - MONOKAI THEME - The classic Monokai color scheme - ======================================== */ -.monokai { - --background: oklch(0.17 0.01 90); /* #272822 */ - --background-50: oklch(0.17 0.01 90 / 0.5); - --background-80: oklch(0.17 0.01 90 / 0.8); - - --foreground: oklch(0.95 0.02 100); /* #f8f8f2 */ - --foreground-secondary: oklch(0.8 0.02 100); - --foreground-muted: oklch(0.55 0.04 100); /* #75715e */ - - --card: oklch(0.22 0.01 90); /* #3e3d32 */ - --card-foreground: oklch(0.95 0.02 100); - --popover: oklch(0.2 0.01 90); - --popover-foreground: oklch(0.95 0.02 100); - - --primary: oklch(0.8 0.2 350); /* #f92672 pink */ - --primary-foreground: oklch(0.17 0.01 90); - - --brand-400: oklch(0.85 0.2 350); - --brand-500: oklch(0.8 0.2 350); /* #f92672 */ - --brand-600: oklch(0.75 0.22 350); - - --secondary: oklch(0.25 0.02 90); - --secondary-foreground: oklch(0.95 0.02 100); - - --muted: oklch(0.25 0.02 90); - --muted-foreground: oklch(0.55 0.04 100); - - --accent: oklch(0.3 0.02 90); - --accent-foreground: oklch(0.95 0.02 100); - - --destructive: oklch(0.65 0.25 15); /* red */ - - --border: oklch(0.35 0.03 90); - --border-glass: oklch(0.8 0.2 350 / 0.3); - - --input: oklch(0.22 0.01 90); - --ring: oklch(0.8 0.2 350); - - --chart-1: oklch(0.8 0.2 350); /* Pink #f92672 */ - --chart-2: oklch(0.85 0.2 90); /* Yellow #e6db74 */ - --chart-3: oklch(0.8 0.2 140); /* Green #a6e22e */ - --chart-4: oklch(0.75 0.2 200); /* Cyan #66d9ef */ - --chart-5: oklch(0.75 0.2 30); /* Orange #fd971f */ - - --sidebar: oklch(0.15 0.01 90); - --sidebar-foreground: oklch(0.95 0.02 100); - --sidebar-primary: oklch(0.8 0.2 350); - --sidebar-primary-foreground: oklch(0.17 0.01 90); - --sidebar-accent: oklch(0.25 0.02 90); - --sidebar-accent-foreground: oklch(0.95 0.02 100); - --sidebar-border: oklch(0.35 0.03 90); - --sidebar-ring: oklch(0.8 0.2 350); - - /* Action button colors - Monokai pink/yellow theme */ - --action-view: oklch(0.8 0.2 350); /* Pink */ - --action-view-hover: oklch(0.75 0.22 350); - --action-followup: oklch(0.75 0.2 200); /* Cyan */ - --action-followup-hover: oklch(0.7 0.22 200); - --action-commit: oklch(0.8 0.2 140); /* Green */ - --action-commit-hover: oklch(0.75 0.22 140); - --action-verify: oklch(0.8 0.2 140); /* Green */ - --action-verify-hover: oklch(0.75 0.22 140); - - /* Running indicator - Pink */ - --running-indicator: oklch(0.8 0.2 350); - --running-indicator-text: oklch(0.85 0.18 350); -} - -/* ======================================== - TOKYO NIGHT THEME - A clean dark theme celebrating Tokyo at night - ======================================== */ -.tokyonight { - --background: oklch(0.16 0.03 260); /* #1a1b26 */ - --background-50: oklch(0.16 0.03 260 / 0.5); - --background-80: oklch(0.16 0.03 260 / 0.8); - - --foreground: oklch(0.85 0.02 250); /* #a9b1d6 */ - --foreground-secondary: oklch(0.7 0.03 250); - --foreground-muted: oklch(0.5 0.04 250); /* #565f89 */ - - --card: oklch(0.2 0.03 260); /* #24283b */ - --card-foreground: oklch(0.85 0.02 250); - --popover: oklch(0.18 0.03 260); - --popover-foreground: oklch(0.85 0.02 250); - - --primary: oklch(0.7 0.18 280); /* #7aa2f7 blue */ - --primary-foreground: oklch(0.16 0.03 260); - - --brand-400: oklch(0.75 0.18 280); - --brand-500: oklch(0.7 0.18 280); /* #7aa2f7 */ - --brand-600: oklch(0.65 0.2 280); /* #7dcfff */ - - --secondary: oklch(0.24 0.03 260); /* #292e42 */ - --secondary-foreground: oklch(0.85 0.02 250); - - --muted: oklch(0.24 0.03 260); - --muted-foreground: oklch(0.5 0.04 250); - - --accent: oklch(0.28 0.04 260); - --accent-foreground: oklch(0.85 0.02 250); - - --destructive: oklch(0.65 0.2 15); /* #f7768e */ - - --border: oklch(0.32 0.04 260); - --border-glass: oklch(0.7 0.18 280 / 0.3); - - --input: oklch(0.2 0.03 260); - --ring: oklch(0.7 0.18 280); - - --chart-1: oklch(0.7 0.18 280); /* Blue #7aa2f7 */ - --chart-2: oklch(0.75 0.18 200); /* Cyan #7dcfff */ - --chart-3: oklch(0.75 0.18 140); /* Green #9ece6a */ - --chart-4: oklch(0.7 0.2 320); /* Magenta #bb9af7 */ - --chart-5: oklch(0.8 0.18 70); /* Yellow #e0af68 */ - - --sidebar: oklch(0.14 0.03 260); - --sidebar-foreground: oklch(0.85 0.02 250); - --sidebar-primary: oklch(0.7 0.18 280); - --sidebar-primary-foreground: oklch(0.16 0.03 260); - --sidebar-accent: oklch(0.24 0.03 260); - --sidebar-accent-foreground: oklch(0.85 0.02 250); - --sidebar-border: oklch(0.32 0.04 260); - --sidebar-ring: oklch(0.7 0.18 280); - - /* Action button colors - Tokyo Night blue/magenta theme */ - --action-view: oklch(0.7 0.18 280); /* Blue */ - --action-view-hover: oklch(0.65 0.2 280); - --action-followup: oklch(0.75 0.18 200); /* Cyan */ - --action-followup-hover: oklch(0.7 0.2 200); - --action-commit: oklch(0.75 0.18 140); /* Green */ - --action-commit-hover: oklch(0.7 0.2 140); - --action-verify: oklch(0.75 0.18 140); /* Green */ - --action-verify-hover: oklch(0.7 0.2 140); - - /* Running indicator - Blue */ - --running-indicator: oklch(0.7 0.18 280); - --running-indicator-text: oklch(0.75 0.16 280); -} - -/* ======================================== - SOLARIZED DARK THEME - The classic color scheme by Ethan Schoonover - ======================================== */ -.solarized { - --background: oklch(0.2 0.02 230); /* #002b36 base03 */ - --background-50: oklch(0.2 0.02 230 / 0.5); - --background-80: oklch(0.2 0.02 230 / 0.8); - - --foreground: oklch(0.75 0.02 90); /* #839496 base0 */ - --foreground-secondary: oklch(0.6 0.03 200); /* #657b83 base00 */ - --foreground-muted: oklch(0.5 0.04 200); /* #586e75 base01 */ - - --card: oklch(0.23 0.02 230); /* #073642 base02 */ - --card-foreground: oklch(0.75 0.02 90); - --popover: oklch(0.22 0.02 230); - --popover-foreground: oklch(0.75 0.02 90); - - --primary: oklch(0.65 0.15 220); /* #268bd2 blue */ - --primary-foreground: oklch(0.2 0.02 230); - - --brand-400: oklch(0.7 0.15 220); - --brand-500: oklch(0.65 0.15 220); /* #268bd2 */ - --brand-600: oklch(0.6 0.17 220); - - --secondary: oklch(0.25 0.02 230); - --secondary-foreground: oklch(0.75 0.02 90); - - --muted: oklch(0.25 0.02 230); - --muted-foreground: oklch(0.5 0.04 200); - - --accent: oklch(0.28 0.03 230); - --accent-foreground: oklch(0.75 0.02 90); - - --destructive: oklch(0.55 0.2 25); /* #dc322f red */ - - --border: oklch(0.35 0.03 230); - --border-glass: oklch(0.65 0.15 220 / 0.3); - - --input: oklch(0.23 0.02 230); - --ring: oklch(0.65 0.15 220); - - --chart-1: oklch(0.65 0.15 220); /* Blue */ - --chart-2: oklch(0.6 0.18 180); /* Cyan #2aa198 */ - --chart-3: oklch(0.65 0.2 140); /* Green #859900 */ - --chart-4: oklch(0.7 0.18 55); /* Yellow #b58900 */ - --chart-5: oklch(0.6 0.2 30); /* Orange #cb4b16 */ - - --sidebar: oklch(0.18 0.02 230); - --sidebar-foreground: oklch(0.75 0.02 90); - --sidebar-primary: oklch(0.65 0.15 220); - --sidebar-primary-foreground: oklch(0.2 0.02 230); - --sidebar-accent: oklch(0.25 0.02 230); - --sidebar-accent-foreground: oklch(0.75 0.02 90); - --sidebar-border: oklch(0.35 0.03 230); - --sidebar-ring: oklch(0.65 0.15 220); - - /* Action button colors - Solarized blue/cyan theme */ - --action-view: oklch(0.65 0.15 220); /* Blue */ - --action-view-hover: oklch(0.6 0.17 220); - --action-followup: oklch(0.6 0.18 180); /* Cyan */ - --action-followup-hover: oklch(0.55 0.2 180); - --action-commit: oklch(0.65 0.2 140); /* Green */ - --action-commit-hover: oklch(0.6 0.22 140); - --action-verify: oklch(0.65 0.2 140); /* Green */ - --action-verify-hover: oklch(0.6 0.22 140); - - /* Running indicator - Blue */ - --running-indicator: oklch(0.65 0.15 220); - --running-indicator-text: oklch(0.7 0.13 220); -} - -/* ======================================== - GRUVBOX THEME - Retro groove color scheme - ======================================== */ -.gruvbox { - --background: oklch(0.18 0.02 60); /* #282828 bg */ - --background-50: oklch(0.18 0.02 60 / 0.5); - --background-80: oklch(0.18 0.02 60 / 0.8); - - --foreground: oklch(0.85 0.05 85); /* #ebdbb2 fg */ - --foreground-secondary: oklch(0.7 0.04 85); /* #d5c4a1 */ - --foreground-muted: oklch(0.55 0.04 85); /* #928374 */ - - --card: oklch(0.22 0.02 60); /* #3c3836 bg1 */ - --card-foreground: oklch(0.85 0.05 85); - --popover: oklch(0.2 0.02 60); - --popover-foreground: oklch(0.85 0.05 85); - - --primary: oklch(0.7 0.18 55); /* #fabd2f yellow */ - --primary-foreground: oklch(0.18 0.02 60); - - --brand-400: oklch(0.75 0.18 55); - --brand-500: oklch(0.7 0.18 55); /* Yellow */ - --brand-600: oklch(0.65 0.2 55); - - --secondary: oklch(0.26 0.02 60); /* #504945 bg2 */ - --secondary-foreground: oklch(0.85 0.05 85); - - --muted: oklch(0.26 0.02 60); - --muted-foreground: oklch(0.55 0.04 85); - - --accent: oklch(0.3 0.03 60); - --accent-foreground: oklch(0.85 0.05 85); - - --destructive: oklch(0.55 0.22 25); /* #fb4934 red */ - - --border: oklch(0.35 0.03 60); - --border-glass: oklch(0.7 0.18 55 / 0.3); - - --input: oklch(0.22 0.02 60); - --ring: oklch(0.7 0.18 55); - - --chart-1: oklch(0.7 0.18 55); /* Yellow */ - --chart-2: oklch(0.65 0.2 140); /* Green #b8bb26 */ - --chart-3: oklch(0.7 0.15 200); /* Aqua #8ec07c */ - --chart-4: oklch(0.6 0.2 30); /* Orange #fe8019 */ - --chart-5: oklch(0.6 0.2 320); /* Purple #d3869b */ - - --sidebar: oklch(0.16 0.02 60); - --sidebar-foreground: oklch(0.85 0.05 85); - --sidebar-primary: oklch(0.7 0.18 55); - --sidebar-primary-foreground: oklch(0.18 0.02 60); - --sidebar-accent: oklch(0.26 0.02 60); - --sidebar-accent-foreground: oklch(0.85 0.05 85); - --sidebar-border: oklch(0.35 0.03 60); - --sidebar-ring: oklch(0.7 0.18 55); - - /* Action button colors - Gruvbox yellow/orange theme */ - --action-view: oklch(0.7 0.18 55); /* Yellow */ - --action-view-hover: oklch(0.65 0.2 55); - --action-followup: oklch(0.7 0.15 200); /* Aqua */ - --action-followup-hover: oklch(0.65 0.17 200); - --action-commit: oklch(0.65 0.2 140); /* Green */ - --action-commit-hover: oklch(0.6 0.22 140); - --action-verify: oklch(0.65 0.2 140); /* Green */ - --action-verify-hover: oklch(0.6 0.22 140); - - /* Running indicator - Yellow */ - --running-indicator: oklch(0.7 0.18 55); - --running-indicator-text: oklch(0.75 0.16 55); -} - -/* ======================================== - CATPPUCCIN MOCHA THEME - Soothing pastel theme for the high-spirited - ======================================== */ -.catppuccin { - --background: oklch(0.18 0.02 260); /* #1e1e2e base */ - --background-50: oklch(0.18 0.02 260 / 0.5); - --background-80: oklch(0.18 0.02 260 / 0.8); - - --foreground: oklch(0.9 0.01 280); /* #cdd6f4 text */ - --foreground-secondary: oklch(0.75 0.02 280); /* #bac2de subtext1 */ - --foreground-muted: oklch(0.6 0.03 280); /* #a6adc8 subtext0 */ - - --card: oklch(0.22 0.02 260); /* #313244 surface0 */ - --card-foreground: oklch(0.9 0.01 280); - --popover: oklch(0.2 0.02 260); - --popover-foreground: oklch(0.9 0.01 280); - - --primary: oklch(0.75 0.15 280); /* #cba6f7 mauve */ - --primary-foreground: oklch(0.18 0.02 260); - - --brand-400: oklch(0.8 0.15 280); - --brand-500: oklch(0.75 0.15 280); /* Mauve */ - --brand-600: oklch(0.7 0.17 280); - - --secondary: oklch(0.26 0.02 260); /* #45475a surface1 */ - --secondary-foreground: oklch(0.9 0.01 280); - - --muted: oklch(0.26 0.02 260); - --muted-foreground: oklch(0.6 0.03 280); - - --accent: oklch(0.3 0.03 260); /* #585b70 surface2 */ - --accent-foreground: oklch(0.9 0.01 280); - - --destructive: oklch(0.65 0.2 15); /* #f38ba8 red */ - - --border: oklch(0.35 0.03 260); - --border-glass: oklch(0.75 0.15 280 / 0.3); - - --input: oklch(0.22 0.02 260); - --ring: oklch(0.75 0.15 280); - - --chart-1: oklch(0.75 0.15 280); /* Mauve */ - --chart-2: oklch(0.75 0.15 220); /* Blue #89b4fa */ - --chart-3: oklch(0.8 0.15 160); /* Green #a6e3a1 */ - --chart-4: oklch(0.8 0.15 350); /* Pink #f5c2e7 */ - --chart-5: oklch(0.85 0.12 90); /* Yellow #f9e2af */ - - --sidebar: oklch(0.16 0.02 260); /* #181825 mantle */ - --sidebar-foreground: oklch(0.9 0.01 280); - --sidebar-primary: oklch(0.75 0.15 280); - --sidebar-primary-foreground: oklch(0.18 0.02 260); - --sidebar-accent: oklch(0.26 0.02 260); - --sidebar-accent-foreground: oklch(0.9 0.01 280); - --sidebar-border: oklch(0.35 0.03 260); - --sidebar-ring: oklch(0.75 0.15 280); - - /* Action button colors - Catppuccin mauve/pink theme */ - --action-view: oklch(0.75 0.15 280); /* Mauve */ - --action-view-hover: oklch(0.7 0.17 280); - --action-followup: oklch(0.75 0.15 220); /* Blue */ - --action-followup-hover: oklch(0.7 0.17 220); - --action-commit: oklch(0.8 0.15 160); /* Green */ - --action-commit-hover: oklch(0.75 0.17 160); - --action-verify: oklch(0.8 0.15 160); /* Green */ - --action-verify-hover: oklch(0.75 0.17 160); - - /* Running indicator - Mauve */ - --running-indicator: oklch(0.75 0.15 280); - --running-indicator-text: oklch(0.8 0.13 280); -} - -/* ======================================== - ONE DARK THEME - Atom's iconic One Dark theme - ======================================== */ -.onedark { - --background: oklch(0.19 0.01 250); /* #282c34 */ - --background-50: oklch(0.19 0.01 250 / 0.5); - --background-80: oklch(0.19 0.01 250 / 0.8); - - --foreground: oklch(0.85 0.02 240); /* #abb2bf */ - --foreground-secondary: oklch(0.7 0.02 240); - --foreground-muted: oklch(0.5 0.03 240); /* #5c6370 */ - - --card: oklch(0.23 0.01 250); /* #21252b */ - --card-foreground: oklch(0.85 0.02 240); - --popover: oklch(0.21 0.01 250); - --popover-foreground: oklch(0.85 0.02 240); - - --primary: oklch(0.7 0.18 230); /* #61afef blue */ - --primary-foreground: oklch(0.19 0.01 250); - - --brand-400: oklch(0.75 0.18 230); - --brand-500: oklch(0.7 0.18 230); /* Blue */ - --brand-600: oklch(0.65 0.2 230); - - --secondary: oklch(0.25 0.01 250); - --secondary-foreground: oklch(0.85 0.02 240); - - --muted: oklch(0.25 0.01 250); - --muted-foreground: oklch(0.5 0.03 240); - - --accent: oklch(0.28 0.02 250); - --accent-foreground: oklch(0.85 0.02 240); - - --destructive: oklch(0.6 0.2 20); /* #e06c75 red */ - - --border: oklch(0.35 0.02 250); - --border-glass: oklch(0.7 0.18 230 / 0.3); - - --input: oklch(0.23 0.01 250); - --ring: oklch(0.7 0.18 230); - - --chart-1: oklch(0.7 0.18 230); /* Blue */ - --chart-2: oklch(0.75 0.15 320); /* Magenta #c678dd */ - --chart-3: oklch(0.75 0.18 150); /* Green #98c379 */ - --chart-4: oklch(0.8 0.15 80); /* Yellow #e5c07b */ - --chart-5: oklch(0.7 0.15 180); /* Cyan #56b6c2 */ - - --sidebar: oklch(0.17 0.01 250); - --sidebar-foreground: oklch(0.85 0.02 240); - --sidebar-primary: oklch(0.7 0.18 230); - --sidebar-primary-foreground: oklch(0.19 0.01 250); - --sidebar-accent: oklch(0.25 0.01 250); - --sidebar-accent-foreground: oklch(0.85 0.02 240); - --sidebar-border: oklch(0.35 0.02 250); - --sidebar-ring: oklch(0.7 0.18 230); - - /* Action button colors - One Dark blue/magenta theme */ - --action-view: oklch(0.7 0.18 230); /* Blue */ - --action-view-hover: oklch(0.65 0.2 230); - --action-followup: oklch(0.75 0.15 320); /* Magenta */ - --action-followup-hover: oklch(0.7 0.17 320); - --action-commit: oklch(0.75 0.18 150); /* Green */ - --action-commit-hover: oklch(0.7 0.2 150); - --action-verify: oklch(0.75 0.18 150); /* Green */ - --action-verify-hover: oklch(0.7 0.2 150); - - /* Running indicator - Blue */ - --running-indicator: oklch(0.7 0.18 230); - --running-indicator-text: oklch(0.75 0.16 230); -} - -/* ======================================== - SYNTHWAVE '84 THEME - Neon dreams of the 80s - ======================================== */ -.synthwave { - --background: oklch(0.15 0.05 290); /* #262335 */ - --background-50: oklch(0.15 0.05 290 / 0.5); - --background-80: oklch(0.15 0.05 290 / 0.8); - - --foreground: oklch(0.95 0.02 320); /* #ffffff with warm tint */ - --foreground-secondary: oklch(0.75 0.05 320); - --foreground-muted: oklch(0.55 0.08 290); - - --card: oklch(0.2 0.06 290); /* #34294f */ - --card-foreground: oklch(0.95 0.02 320); - --popover: oklch(0.18 0.05 290); - --popover-foreground: oklch(0.95 0.02 320); - - --primary: oklch(0.7 0.28 350); /* #f97e72 hot pink */ - --primary-foreground: oklch(0.15 0.05 290); - - --brand-400: oklch(0.75 0.28 350); - --brand-500: oklch(0.7 0.28 350); /* Hot pink */ - --brand-600: oklch(0.65 0.3 350); - - --secondary: oklch(0.25 0.07 290); - --secondary-foreground: oklch(0.95 0.02 320); - - --muted: oklch(0.25 0.07 290); - --muted-foreground: oklch(0.55 0.08 290); - - --accent: oklch(0.3 0.08 290); - --accent-foreground: oklch(0.95 0.02 320); - - --destructive: oklch(0.6 0.25 15); - - --border: oklch(0.4 0.1 290); - --border-glass: oklch(0.7 0.28 350 / 0.3); - - --input: oklch(0.2 0.06 290); - --ring: oklch(0.7 0.28 350); - - --chart-1: oklch(0.7 0.28 350); /* Hot pink */ - --chart-2: oklch(0.8 0.25 200); /* Cyan #72f1b8 */ - --chart-3: oklch(0.85 0.2 60); /* Yellow #fede5d */ - --chart-4: oklch(0.7 0.25 280); /* Purple #ff7edb */ - --chart-5: oklch(0.7 0.2 30); /* Orange #f97e72 */ - - --sidebar: oklch(0.13 0.05 290); - --sidebar-foreground: oklch(0.95 0.02 320); - --sidebar-primary: oklch(0.7 0.28 350); - --sidebar-primary-foreground: oklch(0.15 0.05 290); - --sidebar-accent: oklch(0.25 0.07 290); - --sidebar-accent-foreground: oklch(0.95 0.02 320); - --sidebar-border: oklch(0.4 0.1 290); - --sidebar-ring: oklch(0.7 0.28 350); - - /* Action button colors - Synthwave hot pink/cyan theme */ - --action-view: oklch(0.7 0.28 350); /* Hot pink */ - --action-view-hover: oklch(0.65 0.3 350); - --action-followup: oklch(0.8 0.25 200); /* Cyan */ - --action-followup-hover: oklch(0.75 0.27 200); - --action-commit: oklch(0.85 0.2 60); /* Yellow */ - --action-commit-hover: oklch(0.8 0.22 60); - --action-verify: oklch(0.85 0.2 60); /* Yellow */ - --action-verify-hover: oklch(0.8 0.22 60); - - /* Running indicator - Hot pink */ - --running-indicator: oklch(0.7 0.28 350); - --running-indicator-text: oklch(0.75 0.26 350); -} - -/* Red Theme - Bold crimson/red aesthetic */ -.red { - --background: oklch(0.12 0.03 15); /* Deep dark red-tinted black */ - --background-50: oklch(0.12 0.03 15 / 0.5); - --background-80: oklch(0.12 0.03 15 / 0.8); - - --foreground: oklch(0.95 0.01 15); /* Off-white with warm tint */ - --foreground-secondary: oklch(0.7 0.02 15); - --foreground-muted: oklch(0.5 0.03 15); - - --card: oklch(0.18 0.04 15); /* Slightly lighter dark red */ - --card-foreground: oklch(0.95 0.01 15); - --popover: oklch(0.15 0.035 15); - --popover-foreground: oklch(0.95 0.01 15); - - --primary: oklch(0.55 0.25 25); /* Vibrant crimson red */ - --primary-foreground: oklch(0.98 0 0); - - --brand-400: oklch(0.6 0.23 25); - --brand-500: oklch(0.55 0.25 25); /* Crimson */ - --brand-600: oklch(0.5 0.27 25); - - --secondary: oklch(0.22 0.05 15); - --secondary-foreground: oklch(0.95 0.01 15); - - --muted: oklch(0.22 0.05 15); - --muted-foreground: oklch(0.5 0.03 15); - - --accent: oklch(0.28 0.06 15); - --accent-foreground: oklch(0.95 0.01 15); - - --destructive: oklch(0.6 0.28 30); /* Bright orange-red for destructive */ - - --border: oklch(0.35 0.08 15); - --border-glass: oklch(0.55 0.25 25 / 0.3); - - --input: oklch(0.18 0.04 15); - --ring: oklch(0.55 0.25 25); - - --chart-1: oklch(0.55 0.25 25); /* Crimson */ - --chart-2: oklch(0.7 0.2 50); /* Orange */ - --chart-3: oklch(0.8 0.18 80); /* Gold */ - --chart-4: oklch(0.6 0.22 0); /* Pure red */ - --chart-5: oklch(0.65 0.2 350); /* Pink-red */ - - --sidebar: oklch(0.1 0.025 15); - --sidebar-foreground: oklch(0.95 0.01 15); - --sidebar-primary: oklch(0.55 0.25 25); - --sidebar-primary-foreground: oklch(0.98 0 0); - --sidebar-accent: oklch(0.22 0.05 15); - --sidebar-accent-foreground: oklch(0.95 0.01 15); - --sidebar-border: oklch(0.35 0.08 15); - --sidebar-ring: oklch(0.55 0.25 25); - - /* Action button colors - Red theme */ - --action-view: oklch(0.55 0.25 25); /* Crimson */ - --action-view-hover: oklch(0.5 0.27 25); - --action-followup: oklch(0.7 0.2 50); /* Orange */ - --action-followup-hover: oklch(0.65 0.22 50); - --action-commit: oklch(0.6 0.2 140); /* Green for positive actions */ - --action-commit-hover: oklch(0.55 0.22 140); - --action-verify: oklch(0.6 0.2 140); /* Green */ - --action-verify-hover: oklch(0.55 0.22 140); - - /* Running indicator - Crimson */ - --running-indicator: oklch(0.55 0.25 25); - --running-indicator-text: oklch(0.6 0.23 25); -} - -.cream { - /* Cream Theme - Warm, soft, easy on the eyes */ - --background: oklch(0.95 0.01 70); /* Warm cream background */ - --background-50: oklch(0.95 0.01 70 / 0.5); - --background-80: oklch(0.95 0.01 70 / 0.8); - - --foreground: oklch(0.25 0.02 60); /* Dark warm brown */ - --foreground-secondary: oklch(0.45 0.02 60); /* Medium brown */ - --foreground-muted: oklch(0.55 0.02 60); /* Light brown */ - - --card: oklch(0.98 0.005 70); /* Slightly lighter cream */ - --card-foreground: oklch(0.25 0.02 60); - --popover: oklch(0.97 0.008 70); - --popover-foreground: oklch(0.25 0.02 60); - - --primary: oklch(0.5 0.12 45); /* Warm terracotta/rust */ - --primary-foreground: oklch(0.98 0.005 70); - - --brand-400: oklch(0.55 0.12 45); - --brand-500: oklch(0.5 0.12 45); /* Terracotta */ - --brand-600: oklch(0.45 0.13 45); - - --secondary: oklch(0.88 0.02 70); - --secondary-foreground: oklch(0.25 0.02 60); - - --muted: oklch(0.9 0.015 70); - --muted-foreground: oklch(0.45 0.02 60); - - --accent: oklch(0.85 0.025 70); - --accent-foreground: oklch(0.25 0.02 60); - - --destructive: oklch(0.55 0.22 25); /* Warm red */ - - --border: oklch(0.85 0.015 70); - --border-glass: oklch(0.5 0.12 45 / 0.2); - - --input: oklch(0.98 0.005 70); - --ring: oklch(0.5 0.12 45); - - --chart-1: oklch(0.5 0.12 45); /* Terracotta */ - --chart-2: oklch(0.55 0.15 35); /* Burnt orange */ - --chart-3: oklch(0.6 0.12 100); /* Olive */ - --chart-4: oklch(0.5 0.15 20); /* Deep rust */ - --chart-5: oklch(0.65 0.1 80); /* Golden */ - - --sidebar: oklch(0.93 0.012 70); - --sidebar-foreground: oklch(0.25 0.02 60); - --sidebar-primary: oklch(0.5 0.12 45); - --sidebar-primary-foreground: oklch(0.98 0.005 70); - --sidebar-accent: oklch(0.88 0.02 70); - --sidebar-accent-foreground: oklch(0.25 0.02 60); - --sidebar-border: oklch(0.85 0.015 70); - --sidebar-ring: oklch(0.5 0.12 45); - - /* Action button colors - Warm earth tones */ - --action-view: oklch(0.5 0.12 45); /* Terracotta */ - --action-view-hover: oklch(0.45 0.13 45); - --action-followup: oklch(0.55 0.15 35); /* Burnt orange */ - --action-followup-hover: oklch(0.5 0.16 35); - --action-commit: oklch(0.55 0.12 130); /* Sage green */ - --action-commit-hover: oklch(0.5 0.13 130); - --action-verify: oklch(0.55 0.12 130); /* Sage green */ - --action-verify-hover: oklch(0.5 0.13 130); - - /* Running indicator - Terracotta */ - --running-indicator: oklch(0.5 0.12 45); - --running-indicator-text: oklch(0.55 0.12 45); - - /* Status colors - Cream theme */ - --status-success: oklch(0.55 0.15 130); - --status-success-bg: oklch(0.55 0.15 130 / 0.15); - --status-warning: oklch(0.6 0.15 70); - --status-warning-bg: oklch(0.6 0.15 70 / 0.15); - --status-error: oklch(0.55 0.22 25); - --status-error-bg: oklch(0.55 0.22 25 / 0.15); - --status-info: oklch(0.5 0.15 230); - --status-info-bg: oklch(0.5 0.15 230 / 0.15); - --status-backlog: oklch(0.6 0.02 60); - --status-in-progress: oklch(0.6 0.15 70); - --status-waiting: oklch(0.58 0.13 50); -} - -.sunset { - /* Sunset Theme - Mellow oranges and soft purples */ - --background: oklch(0.15 0.02 280); /* Deep twilight blue-purple */ - --background-50: oklch(0.15 0.02 280 / 0.5); - --background-80: oklch(0.15 0.02 280 / 0.8); - - --foreground: oklch(0.95 0.01 80); /* Warm white */ - --foreground-secondary: oklch(0.75 0.02 60); - --foreground-muted: oklch(0.6 0.02 60); - - --card: oklch(0.2 0.025 280); - --card-foreground: oklch(0.95 0.01 80); - --popover: oklch(0.18 0.02 280); - --popover-foreground: oklch(0.95 0.01 80); - - --primary: oklch(0.68 0.18 45); /* Mellow sunset orange */ - --primary-foreground: oklch(0.15 0.02 280); - - --brand-400: oklch(0.72 0.17 45); - --brand-500: oklch(0.68 0.18 45); /* Soft sunset orange */ - --brand-600: oklch(0.64 0.19 42); - - --secondary: oklch(0.25 0.03 280); - --secondary-foreground: oklch(0.95 0.01 80); - - --muted: oklch(0.27 0.03 280); - --muted-foreground: oklch(0.6 0.02 60); - - --accent: oklch(0.35 0.04 310); - --accent-foreground: oklch(0.95 0.01 80); - - --destructive: oklch(0.6 0.2 25); /* Muted red */ - - --border: oklch(0.32 0.04 280); - --border-glass: oklch(0.68 0.18 45 / 0.3); - - --input: oklch(0.2 0.025 280); - --ring: oklch(0.68 0.18 45); - - --chart-1: oklch(0.68 0.18 45); /* Mellow orange */ - --chart-2: oklch(0.75 0.16 340); /* Soft pink sunset */ - --chart-3: oklch(0.78 0.18 70); /* Soft golden */ - --chart-4: oklch(0.66 0.19 42); /* Subtle coral */ - --chart-5: oklch(0.72 0.14 310); /* Pastel purple */ - - --sidebar: oklch(0.13 0.015 280); - --sidebar-foreground: oklch(0.95 0.01 80); - --sidebar-primary: oklch(0.68 0.18 45); - --sidebar-primary-foreground: oklch(0.15 0.02 280); - --sidebar-accent: oklch(0.25 0.03 280); - --sidebar-accent-foreground: oklch(0.95 0.01 80); - --sidebar-border: oklch(0.32 0.04 280); - --sidebar-ring: oklch(0.68 0.18 45); - - /* Action button colors - Mellow sunset palette */ - --action-view: oklch(0.68 0.18 45); /* Mellow orange */ - --action-view-hover: oklch(0.64 0.19 42); - --action-followup: oklch(0.75 0.16 340); /* Soft pink */ - --action-followup-hover: oklch(0.7 0.17 340); - --action-commit: oklch(0.65 0.16 140); /* Soft green */ - --action-commit-hover: oklch(0.6 0.17 140); - --action-verify: oklch(0.65 0.16 140); /* Soft green */ - --action-verify-hover: oklch(0.6 0.17 140); - - /* Running indicator - Mellow orange */ - --running-indicator: oklch(0.68 0.18 45); - --running-indicator-text: oklch(0.72 0.17 45); - - /* Status colors - Sunset theme */ - --status-success: oklch(0.65 0.16 140); - --status-success-bg: oklch(0.65 0.16 140 / 0.2); - --status-warning: oklch(0.78 0.18 70); - --status-warning-bg: oklch(0.78 0.18 70 / 0.2); - --status-error: oklch(0.65 0.2 25); - --status-error-bg: oklch(0.65 0.2 25 / 0.2); - --status-info: oklch(0.75 0.16 340); - --status-info-bg: oklch(0.75 0.16 340 / 0.2); - --status-backlog: oklch(0.65 0.02 280); - --status-in-progress: oklch(0.78 0.18 70); - --status-waiting: oklch(0.72 0.17 60); -} - -.gray { - /* Gray Theme - Modern, minimal gray scheme inspired by Cursor */ - --background: oklch(0.2 0.005 250); /* Medium-dark neutral gray */ - --background-50: oklch(0.2 0.005 250 / 0.5); - --background-80: oklch(0.2 0.005 250 / 0.8); - - --foreground: oklch(0.9 0.005 250); /* Light gray */ - --foreground-secondary: oklch(0.65 0.005 250); - --foreground-muted: oklch(0.5 0.005 250); - - --card: oklch(0.24 0.005 250); - --card-foreground: oklch(0.9 0.005 250); - --popover: oklch(0.22 0.005 250); - --popover-foreground: oklch(0.9 0.005 250); - - --primary: oklch(0.6 0.08 250); /* Subtle blue-gray */ - --primary-foreground: oklch(0.95 0.005 250); - - --brand-400: oklch(0.65 0.08 250); - --brand-500: oklch(0.6 0.08 250); /* Blue-gray */ - --brand-600: oklch(0.55 0.09 250); - - --secondary: oklch(0.28 0.005 250); - --secondary-foreground: oklch(0.9 0.005 250); - - --muted: oklch(0.3 0.005 250); - --muted-foreground: oklch(0.6 0.005 250); - - --accent: oklch(0.35 0.01 250); - --accent-foreground: oklch(0.9 0.005 250); - - --destructive: oklch(0.6 0.2 25); /* Muted red */ - - --border: oklch(0.32 0.005 250); - --border-glass: oklch(0.6 0.08 250 / 0.2); - - --input: oklch(0.24 0.005 250); - --ring: oklch(0.6 0.08 250); - - --chart-1: oklch(0.6 0.08 250); /* Blue-gray */ - --chart-2: oklch(0.65 0.1 210); /* Cyan */ - --chart-3: oklch(0.7 0.12 160); /* Teal */ - --chart-4: oklch(0.65 0.1 280); /* Purple */ - --chart-5: oklch(0.7 0.08 300); /* Violet */ - - --sidebar: oklch(0.18 0.005 250); - --sidebar-foreground: oklch(0.9 0.005 250); - --sidebar-primary: oklch(0.6 0.08 250); - --sidebar-primary-foreground: oklch(0.95 0.005 250); - --sidebar-accent: oklch(0.28 0.005 250); - --sidebar-accent-foreground: oklch(0.9 0.005 250); - --sidebar-border: oklch(0.32 0.005 250); - --sidebar-ring: oklch(0.6 0.08 250); - - /* Action button colors - Subtle modern colors */ - --action-view: oklch(0.6 0.08 250); /* Blue-gray */ - --action-view-hover: oklch(0.55 0.09 250); - --action-followup: oklch(0.65 0.1 210); /* Cyan */ - --action-followup-hover: oklch(0.6 0.11 210); - --action-commit: oklch(0.65 0.12 150); /* Teal-green */ - --action-commit-hover: oklch(0.6 0.13 150); - --action-verify: oklch(0.65 0.12 150); /* Teal-green */ - --action-verify-hover: oklch(0.6 0.13 150); - - /* Running indicator - Blue-gray */ - --running-indicator: oklch(0.6 0.08 250); - --running-indicator-text: oklch(0.65 0.08 250); - - /* Status colors - Gray theme */ - --status-success: oklch(0.65 0.12 150); - --status-success-bg: oklch(0.65 0.12 150 / 0.2); - --status-warning: oklch(0.7 0.15 70); - --status-warning-bg: oklch(0.7 0.15 70 / 0.2); - --status-error: oklch(0.6 0.2 25); - --status-error-bg: oklch(0.6 0.2 25 / 0.2); - --status-info: oklch(0.65 0.1 210); - --status-info-bg: oklch(0.65 0.1 210 / 0.2); - --status-backlog: oklch(0.6 0.005 250); - --status-in-progress: oklch(0.7 0.15 70); - --status-waiting: oklch(0.68 0.1 220); -} @layer base { * { @@ -1545,62 +413,6 @@ background: oklch(0.15 0.05 25); } -/* Cream theme scrollbar */ -.cream ::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -.cream ::-webkit-scrollbar-thumb, -.cream .scrollbar-visible::-webkit-scrollbar-thumb { - background: oklch(0.7 0.03 60); - border-radius: 4px; -} - -.cream ::-webkit-scrollbar-thumb:hover, -.cream .scrollbar-visible::-webkit-scrollbar-thumb:hover { - background: oklch(0.6 0.04 60); -} - -.cream ::-webkit-scrollbar-track, -.cream .scrollbar-visible::-webkit-scrollbar-track { - background: oklch(0.9 0.015 70); -} - -/* Sunset theme scrollbar */ -.sunset ::-webkit-scrollbar-thumb, -.sunset .scrollbar-visible::-webkit-scrollbar-thumb { - background: oklch(0.5 0.14 45); - border-radius: 4px; -} - -.sunset ::-webkit-scrollbar-thumb:hover, -.sunset .scrollbar-visible::-webkit-scrollbar-thumb:hover { - background: oklch(0.58 0.16 45); -} - -.sunset ::-webkit-scrollbar-track, -.sunset .scrollbar-visible::-webkit-scrollbar-track { - background: oklch(0.18 0.03 280); -} - -/* Gray theme scrollbar */ -.gray ::-webkit-scrollbar-thumb, -.gray .scrollbar-visible::-webkit-scrollbar-thumb { - background: oklch(0.4 0.01 250); - border-radius: 4px; -} - -.gray ::-webkit-scrollbar-thumb:hover, -.gray .scrollbar-visible::-webkit-scrollbar-thumb:hover { - background: oklch(0.5 0.02 250); -} - -.gray ::-webkit-scrollbar-track, -.gray .scrollbar-visible::-webkit-scrollbar-track { - background: oklch(0.25 0.005 250); -} - /* Always visible scrollbar for file diffs and code blocks */ .scrollbar-visible { overflow-y: auto !important; @@ -1633,30 +445,6 @@ visibility: visible; } -/* Light mode scrollbar-visible adjustments */ -.light .scrollbar-visible::-webkit-scrollbar-track { - background: oklch(0.95 0 0); -} - -.light .scrollbar-visible::-webkit-scrollbar-thumb { - background: oklch(0.7 0 0); -} - -.light .scrollbar-visible::-webkit-scrollbar-thumb:hover { - background: oklch(0.6 0 0); -} - -/* Retro mode scrollbar-visible adjustments */ -.retro .scrollbar-visible::-webkit-scrollbar-thumb { - background: var(--primary); - border-radius: 0; -} - -.retro .scrollbar-visible::-webkit-scrollbar-track { - background: var(--background); - border-radius: 0; -} - /* Styled scrollbar for code blocks and log entries (horizontal/vertical) */ .scrollbar-styled { scrollbar-width: thin; @@ -1682,53 +470,6 @@ background: oklch(0.45 0 0); } -/* Light mode scrollbar-styled adjustments */ -.light .scrollbar-styled::-webkit-scrollbar-thumb { - background: oklch(0.75 0 0); -} - -.light .scrollbar-styled::-webkit-scrollbar-thumb:hover { - background: oklch(0.65 0 0); -} - -/* Cream theme scrollbar-styled */ -.cream .scrollbar-styled::-webkit-scrollbar-thumb { - background: oklch(0.7 0.03 60); -} - -.cream .scrollbar-styled::-webkit-scrollbar-thumb:hover { - background: oklch(0.6 0.04 60); -} - -/* Retro theme scrollbar-styled */ -.retro .scrollbar-styled::-webkit-scrollbar-thumb { - background: var(--primary); - border-radius: 0; -} - -.retro .scrollbar-styled::-webkit-scrollbar-track { - background: var(--background); - border-radius: 0; -} - -/* Sunset theme scrollbar-styled */ -.sunset .scrollbar-styled::-webkit-scrollbar-thumb { - background: oklch(0.5 0.14 45); -} - -.sunset .scrollbar-styled::-webkit-scrollbar-thumb:hover { - background: oklch(0.58 0.16 45); -} - -/* Gray theme scrollbar-styled */ -.gray .scrollbar-styled::-webkit-scrollbar-thumb { - background: oklch(0.4 0.01 250); -} - -.gray .scrollbar-styled::-webkit-scrollbar-thumb:hover { - background: oklch(0.5 0.02 250); -} - /* Glass morphism utilities */ @layer utilities { .glass { @@ -1778,13 +519,7 @@ -webkit-backdrop-filter: blur(12px); } - .light .bg-glass { - background: oklch(1 0 0 / 0.8); - } - .light .bg-glass-80 { - background: oklch(1 0 0 / 0.95); - } /* Hover state utilities */ .hover-glass { @@ -1808,13 +543,7 @@ background: var(--background); } - .light .content-bg { - background: linear-gradient(135deg, oklch(0.99 0 0), oklch(0.98 0 0), oklch(0.99 0 0)); - } - .dark .content-bg { - background: linear-gradient(135deg, oklch(0.04 0 0), oklch(0.08 0 0), oklch(0.04 0 0)); - } /* Action button utilities */ .bg-action-view { @@ -1902,28 +631,8 @@ } /* Retro Overrides for Utilities */ -.retro .glass, -.retro .glass-subtle, -.retro .glass-strong, -.retro .bg-glass, -.retro .bg-glass-80 { - backdrop-filter: none; - background: var(--background); - border: 1px solid var(--border); -} -.retro .gradient-brand { - background: var(--primary); - color: var(--primary-foreground); -} -.retro .content-bg { - background: - linear-gradient(rgba(0, 255, 65, 0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 255, 65, 0.03) 1px, transparent 1px), - var(--background); - background-size: 20px 20px; -} .retro * { border-radius: 0 !important; @@ -1936,41 +645,14 @@ } /* Light mode - deeper purple to blue gradient for better visibility */ -.light .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #7c3aed 0%, #2563eb 50%, #7c3aed 100%); -} -.light .animated-outline-inner { - background: oklch(100% 0 0) !important; - color: #7c3aed !important; - border: 1px solid oklch(92% 0 0); -} -.light [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(97% 0.02 270) !important; - color: #5b21b6 !important; -} /* Dark mode - purple to blue gradient */ -.dark .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #3b82f6 50%, #a855f7 100%); -} -.dark .animated-outline-inner { - background: oklch(0.15 0 0) !important; - color: #c084fc !important; -} -.dark [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.2 0.02 270) !important; - color: #e9d5ff !important; -} /* Retro mode - unique scanline + neon effect */ -.retro .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #00ff41 0%, #00ffff 25%, #ff00ff 50%, #00ffff 75%, #00ff41 100%); - animation: spin 2s linear infinite, retro-glow 1s ease-in-out infinite alternate; -} @keyframes retro-glow { from { @@ -1981,155 +663,42 @@ } } -.retro [data-slot="button"][class*="animated-outline"] { - border-radius: 0 !important; -} -.retro .animated-outline-inner { - background: oklch(0 0 0) !important; - color: #00ff41 !important; - border-radius: 0 !important; - text-shadow: 0 0 5px #00ff41; - font-family: var(--font-geist-mono), monospace; - text-transform: uppercase; - letter-spacing: 0.1em; -} -.retro [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.1 0.1 145) !important; - color: #00ff41 !important; - box-shadow: - 0 0 10px #00ff41, - 0 0 20px #00ff41, - inset 0 0 10px rgba(0, 255, 65, 0.1); - text-shadow: 0 0 10px #00ff41, 0 0 20px #00ff41; -} /* Dracula animated-outline - purple/pink */ -.dracula .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #bd93f9 0%, #ff79c6 50%, #bd93f9 100%); -} -.dracula .animated-outline-inner { - background: oklch(0.18 0.02 280) !important; - color: #bd93f9 !important; -} -.dracula [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.24 0.03 280) !important; - color: #ff79c6 !important; -} /* Nord animated-outline - frost blue */ -.nord .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #88c0d0 0%, #81a1c1 50%, #88c0d0 100%); -} -.nord .animated-outline-inner { - background: oklch(0.23 0.02 240) !important; - color: #88c0d0 !important; -} -.nord [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.28 0.03 240) !important; - color: #8fbcbb !important; -} /* Monokai animated-outline - pink/yellow */ -.monokai .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #f92672 0%, #e6db74 50%, #f92672 100%); -} -.monokai .animated-outline-inner { - background: oklch(0.17 0.01 90) !important; - color: #f92672 !important; -} -.monokai [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.22 0.02 90) !important; - color: #e6db74 !important; -} /* Tokyo Night animated-outline - blue/magenta */ -.tokyonight .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #7aa2f7 0%, #bb9af7 50%, #7aa2f7 100%); -} -.tokyonight .animated-outline-inner { - background: oklch(0.16 0.03 260) !important; - color: #7aa2f7 !important; -} -.tokyonight [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.22 0.04 260) !important; - color: #bb9af7 !important; -} /* Solarized animated-outline - blue/cyan */ -.solarized .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #268bd2 0%, #2aa198 50%, #268bd2 100%); -} -.solarized .animated-outline-inner { - background: oklch(0.2 0.02 230) !important; - color: #268bd2 !important; -} -.solarized [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.25 0.03 230) !important; - color: #2aa198 !important; -} /* Gruvbox animated-outline - yellow/orange */ -.gruvbox .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #fabd2f 0%, #fe8019 50%, #fabd2f 100%); -} -.gruvbox .animated-outline-inner { - background: oklch(0.18 0.02 60) !important; - color: #fabd2f !important; -} -.gruvbox [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.24 0.03 60) !important; - color: #fe8019 !important; -} /* Catppuccin animated-outline - mauve/pink */ -.catppuccin .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #cba6f7 0%, #f5c2e7 50%, #cba6f7 100%); -} -.catppuccin .animated-outline-inner { - background: oklch(0.18 0.02 260) !important; - color: #cba6f7 !important; -} -.catppuccin [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.24 0.03 260) !important; - color: #f5c2e7 !important; -} /* One Dark animated-outline - blue/magenta */ -.onedark .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #61afef 0%, #c678dd 50%, #61afef 100%); -} -.onedark .animated-outline-inner { - background: oklch(0.19 0.01 250) !important; - color: #61afef !important; -} -.onedark [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.25 0.02 250) !important; - color: #c678dd !important; -} /* Synthwave animated-outline - hot pink/cyan with glow */ -.synthwave .animated-outline-gradient { - background: conic-gradient(from 90deg at 50% 50%, #f97e72 0%, #72f1b8 25%, #ff7edb 50%, #72f1b8 75%, #f97e72 100%); - animation: spin 2s linear infinite, synthwave-glow 1.5s ease-in-out infinite alternate; -} @keyframes synthwave-glow { from { @@ -2140,197 +709,54 @@ } } -.synthwave .animated-outline-inner { - background: oklch(0.15 0.05 290) !important; - color: #f97e72 !important; - text-shadow: 0 0 8px #f97e72; -} -.synthwave [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { - background: oklch(0.22 0.07 290) !important; - color: #72f1b8 !important; - text-shadow: 0 0 12px #72f1b8; - box-shadow: 0 0 15px rgba(114, 241, 184, 0.3); -} /* Slider Theme Styles */ -.light .slider-track { - background: oklch(90% 0 0); -} -.light .slider-range { - background: linear-gradient(to right, #7c3aed, #2563eb); -} -.light .slider-thumb { - background: oklch(100% 0 0); - border-color: oklch(80% 0 0); -} -.dark .slider-track { - background: oklch(0.2 0 0); -} -.dark .slider-range { - background: linear-gradient(to right, #a855f7, #3b82f6); -} -.dark .slider-thumb { - background: oklch(0.25 0 0); - border-color: oklch(0.4 0 0); -} -.retro .slider-track { - background: oklch(0.15 0.05 145); - border: 1px solid #00ff41; - border-radius: 0 !important; -} -.retro .slider-range { - background: #00ff41; - box-shadow: 0 0 10px #00ff41, 0 0 5px #00ff41; - border-radius: 0 !important; -} -.retro .slider-thumb { - background: oklch(0 0 0); - border: 2px solid #00ff41; - border-radius: 0 !important; - box-shadow: 0 0 8px #00ff41; -} -.retro .slider-thumb:hover { - background: oklch(0.1 0.1 145); - box-shadow: 0 0 12px #00ff41, 0 0 20px #00ff41; -} /* Dracula slider */ -.dracula .slider-track { - background: oklch(0.28 0.03 280); -} -.dracula .slider-range { - background: linear-gradient(to right, #bd93f9, #ff79c6); -} -.dracula .slider-thumb { - background: oklch(0.22 0.02 280); - border-color: #bd93f9; -} /* Nord slider */ -.nord .slider-track { - background: oklch(0.31 0.02 240); -} -.nord .slider-range { - background: linear-gradient(to right, #88c0d0, #81a1c1); -} -.nord .slider-thumb { - background: oklch(0.27 0.02 240); - border-color: #88c0d0; -} /* Monokai slider */ -.monokai .slider-track { - background: oklch(0.25 0.02 90); -} -.monokai .slider-range { - background: linear-gradient(to right, #f92672, #fd971f); -} -.monokai .slider-thumb { - background: oklch(0.22 0.01 90); - border-color: #f92672; -} /* Tokyo Night slider */ -.tokyonight .slider-track { - background: oklch(0.24 0.03 260); -} -.tokyonight .slider-range { - background: linear-gradient(to right, #7aa2f7, #bb9af7); -} -.tokyonight .slider-thumb { - background: oklch(0.2 0.03 260); - border-color: #7aa2f7; -} /* Solarized slider */ -.solarized .slider-track { - background: oklch(0.25 0.02 230); -} -.solarized .slider-range { - background: linear-gradient(to right, #268bd2, #2aa198); -} -.solarized .slider-thumb { - background: oklch(0.23 0.02 230); - border-color: #268bd2; -} /* Gruvbox slider */ -.gruvbox .slider-track { - background: oklch(0.26 0.02 60); -} -.gruvbox .slider-range { - background: linear-gradient(to right, #fabd2f, #fe8019); -} -.gruvbox .slider-thumb { - background: oklch(0.22 0.02 60); - border-color: #fabd2f; -} /* Catppuccin slider */ -.catppuccin .slider-track { - background: oklch(0.26 0.02 260); -} -.catppuccin .slider-range { - background: linear-gradient(to right, #cba6f7, #89b4fa); -} -.catppuccin .slider-thumb { - background: oklch(0.22 0.02 260); - border-color: #cba6f7; -} /* One Dark slider */ -.onedark .slider-track { - background: oklch(0.25 0.01 250); -} -.onedark .slider-range { - background: linear-gradient(to right, #61afef, #c678dd); -} -.onedark .slider-thumb { - background: oklch(0.23 0.01 250); - border-color: #61afef; -} /* Synthwave slider */ -.synthwave .slider-track { - background: oklch(0.25 0.07 290); -} -.synthwave .slider-range { - background: linear-gradient(to right, #f97e72, #ff7edb); - box-shadow: 0 0 10px #f97e72, 0 0 5px #ff7edb; -} -.synthwave .slider-thumb { - background: oklch(0.2 0.06 290); - border-color: #f97e72; - box-shadow: 0 0 8px #f97e72; -} /* Line clamp utilities for text overflow prevention */ .line-clamp-2 { @@ -2380,511 +806,136 @@ ======================================== */ /* Light theme - professional and readable */ -.light .xml-highlight { - color: oklch(0.3 0 0); /* Default text */ -} -.light .xml-tag-bracket { - color: oklch(0.45 0.15 250); /* Blue-gray for < > */ -} -.light .xml-tag-name { - color: oklch(0.45 0.22 25); /* Red/maroon for tag names */ -} -.light .xml-attribute-name { - color: oklch(0.45 0.18 280); /* Purple for attributes */ -} -.light .xml-attribute-equals { - color: oklch(0.4 0 0); /* Dark gray for = */ -} -.light .xml-attribute-value { - color: oklch(0.45 0.18 145); /* Green for string values */ -} -.light .xml-comment { - color: oklch(0.55 0.05 100); /* Muted olive for comments */ - font-style: italic; -} -.light .xml-cdata { - color: oklch(0.5 0.1 200); /* Teal for CDATA */ -} -.light .xml-doctype { - color: oklch(0.5 0.15 280); /* Purple for DOCTYPE */ -} -.light .xml-text { - color: oklch(0.25 0 0); /* Near-black for text content */ -} /* Dark theme - high contrast */ -.dark .xml-highlight { - color: oklch(0.9 0 0); /* Default light text */ -} -.dark .xml-tag-bracket { - color: oklch(0.7 0.12 220); /* Soft blue for < > */ -} -.dark .xml-tag-name { - color: oklch(0.75 0.2 25); /* Coral/salmon for tag names */ -} -.dark .xml-attribute-name { - color: oklch(0.8 0.15 280); /* Light purple for attributes */ -} -.dark .xml-attribute-equals { - color: oklch(0.6 0 0); /* Gray for = */ -} -.dark .xml-attribute-value { - color: oklch(0.8 0.18 145); /* Bright green for strings */ -} -.dark .xml-comment { - color: oklch(0.55 0.05 100); /* Muted for comments */ - font-style: italic; -} -.dark .xml-cdata { - color: oklch(0.7 0.12 200); /* Teal for CDATA */ -} -.dark .xml-doctype { - color: oklch(0.7 0.15 280); /* Purple for DOCTYPE */ -} -.dark .xml-text { - color: oklch(0.85 0 0); /* Off-white for text */ -} /* Retro theme - neon green on black */ -.retro .xml-highlight { - color: oklch(0.85 0.25 145); /* Neon green default */ -} -.retro .xml-tag-bracket { - color: oklch(0.8 0.25 200); /* Cyan for brackets */ -} -.retro .xml-tag-name { - color: oklch(0.85 0.25 145); /* Bright green for tags */ - text-shadow: 0 0 5px oklch(0.85 0.25 145 / 0.5); -} -.retro .xml-attribute-name { - color: oklch(0.8 0.25 300); /* Purple neon for attrs */ -} -.retro .xml-attribute-equals { - color: oklch(0.6 0.15 145); /* Dim green for = */ -} -.retro .xml-attribute-value { - color: oklch(0.8 0.25 60); /* Yellow neon for strings */ -} -.retro .xml-comment { - color: oklch(0.5 0.15 145); /* Dim green for comments */ - font-style: italic; -} -.retro .xml-cdata { - color: oklch(0.75 0.2 200); /* Cyan for CDATA */ -} -.retro .xml-doctype { - color: oklch(0.75 0.2 300); /* Purple for DOCTYPE */ -} -.retro .xml-text { - color: oklch(0.7 0.2 145); /* Green text */ -} /* Dracula theme */ -.dracula .xml-highlight { - color: oklch(0.95 0.01 280); /* #f8f8f2 */ -} -.dracula .xml-tag-bracket { - color: oklch(0.7 0.25 350); /* Pink #ff79c6 */ -} -.dracula .xml-tag-name { - color: oklch(0.7 0.25 350); /* Pink for tags */ -} -.dracula .xml-attribute-name { - color: oklch(0.8 0.2 130); /* Green #50fa7b */ -} -.dracula .xml-attribute-equals { - color: oklch(0.95 0.01 280); /* White */ -} -.dracula .xml-attribute-value { - color: oklch(0.85 0.2 90); /* Yellow #f1fa8c */ -} -.dracula .xml-comment { - color: oklch(0.55 0.08 280); /* #6272a4 */ - font-style: italic; -} -.dracula .xml-cdata { - color: oklch(0.75 0.2 180); /* Cyan */ -} -.dracula .xml-doctype { - color: oklch(0.7 0.2 320); /* Purple #bd93f9 */ -} -.dracula .xml-text { - color: oklch(0.95 0.01 280); /* White */ -} /* Nord theme */ -.nord .xml-highlight { - color: oklch(0.9 0.01 230); /* #eceff4 */ -} -.nord .xml-tag-bracket { - color: oklch(0.65 0.14 220); /* #81a1c1 */ -} -.nord .xml-tag-name { - color: oklch(0.65 0.14 220); /* Frost blue for tags */ -} -.nord .xml-attribute-name { - color: oklch(0.7 0.12 220); /* #88c0d0 */ -} -.nord .xml-attribute-equals { - color: oklch(0.75 0.02 230); /* Dim white */ -} -.nord .xml-attribute-value { - color: oklch(0.7 0.15 140); /* #a3be8c green */ -} -.nord .xml-comment { - color: oklch(0.5 0.04 230); /* Dim text */ - font-style: italic; -} -.nord .xml-cdata { - color: oklch(0.7 0.12 220); /* Frost blue */ -} -.nord .xml-doctype { - color: oklch(0.7 0.2 320); /* #b48ead purple */ -} -.nord .xml-text { - color: oklch(0.9 0.01 230); /* Snow white */ -} /* Monokai theme */ -.monokai .xml-highlight { - color: oklch(0.95 0.02 100); /* #f8f8f2 */ -} -.monokai .xml-tag-bracket { - color: oklch(0.95 0.02 100); /* White */ -} -.monokai .xml-tag-name { - color: oklch(0.8 0.2 350); /* #f92672 pink */ -} -.monokai .xml-attribute-name { - color: oklch(0.8 0.2 140); /* #a6e22e green */ -} -.monokai .xml-attribute-equals { - color: oklch(0.95 0.02 100); /* White */ -} -.monokai .xml-attribute-value { - color: oklch(0.85 0.2 90); /* #e6db74 yellow */ -} -.monokai .xml-comment { - color: oklch(0.55 0.04 100); /* #75715e */ - font-style: italic; -} -.monokai .xml-cdata { - color: oklch(0.75 0.2 200); /* Cyan #66d9ef */ -} -.monokai .xml-doctype { - color: oklch(0.75 0.2 200); /* Cyan */ -} -.monokai .xml-text { - color: oklch(0.95 0.02 100); /* White */ -} /* Tokyo Night theme */ -.tokyonight .xml-highlight { - color: oklch(0.85 0.02 250); /* #a9b1d6 */ -} -.tokyonight .xml-tag-bracket { - color: oklch(0.65 0.2 15); /* #f7768e red */ -} -.tokyonight .xml-tag-name { - color: oklch(0.65 0.2 15); /* Red for tags */ -} -.tokyonight .xml-attribute-name { - color: oklch(0.7 0.2 320); /* #bb9af7 purple */ -} -.tokyonight .xml-attribute-equals { - color: oklch(0.75 0.02 250); /* Dim text */ -} -.tokyonight .xml-attribute-value { - color: oklch(0.75 0.18 140); /* #9ece6a green */ -} -.tokyonight .xml-comment { - color: oklch(0.5 0.04 250); /* #565f89 */ - font-style: italic; -} -.tokyonight .xml-cdata { - color: oklch(0.75 0.18 200); /* #7dcfff cyan */ -} -.tokyonight .xml-doctype { - color: oklch(0.7 0.18 280); /* #7aa2f7 blue */ -} -.tokyonight .xml-text { - color: oklch(0.85 0.02 250); /* Text color */ -} /* Solarized theme */ -.solarized .xml-highlight { - color: oklch(0.75 0.02 90); /* #839496 */ -} -.solarized .xml-tag-bracket { - color: oklch(0.65 0.15 220); /* #268bd2 blue */ -} -.solarized .xml-tag-name { - color: oklch(0.65 0.15 220); /* Blue for tags */ -} -.solarized .xml-attribute-name { - color: oklch(0.6 0.18 180); /* #2aa198 cyan */ -} -.solarized .xml-attribute-equals { - color: oklch(0.75 0.02 90); /* Base text */ -} -.solarized .xml-attribute-value { - color: oklch(0.65 0.2 140); /* #859900 green */ -} -.solarized .xml-comment { - color: oklch(0.5 0.04 200); /* #586e75 */ - font-style: italic; -} -.solarized .xml-cdata { - color: oklch(0.6 0.18 180); /* Cyan */ -} -.solarized .xml-doctype { - color: oklch(0.6 0.2 290); /* #6c71c4 violet */ -} -.solarized .xml-text { - color: oklch(0.75 0.02 90); /* Base text */ -} /* Gruvbox theme */ -.gruvbox .xml-highlight { - color: oklch(0.85 0.05 85); /* #ebdbb2 */ -} -.gruvbox .xml-tag-bracket { - color: oklch(0.55 0.22 25); /* #fb4934 red */ -} -.gruvbox .xml-tag-name { - color: oklch(0.55 0.22 25); /* Red for tags */ -} -.gruvbox .xml-attribute-name { - color: oklch(0.7 0.15 200); /* #8ec07c aqua */ -} -.gruvbox .xml-attribute-equals { - color: oklch(0.7 0.04 85); /* Dim text */ -} -.gruvbox .xml-attribute-value { - color: oklch(0.65 0.2 140); /* #b8bb26 green */ -} -.gruvbox .xml-comment { - color: oklch(0.55 0.04 85); /* #928374 gray */ - font-style: italic; -} -.gruvbox .xml-cdata { - color: oklch(0.7 0.15 200); /* Aqua */ -} -.gruvbox .xml-doctype { - color: oklch(0.6 0.2 320); /* #d3869b purple */ -} -.gruvbox .xml-text { - color: oklch(0.85 0.05 85); /* Foreground */ -} /* Catppuccin theme */ -.catppuccin .xml-highlight { - color: oklch(0.9 0.01 280); /* #cdd6f4 */ -} -.catppuccin .xml-tag-bracket { - color: oklch(0.65 0.2 15); /* #f38ba8 red */ -} -.catppuccin .xml-tag-name { - color: oklch(0.65 0.2 15); /* Red for tags */ -} -.catppuccin .xml-attribute-name { - color: oklch(0.75 0.15 280); /* #cba6f7 mauve */ -} -.catppuccin .xml-attribute-equals { - color: oklch(0.75 0.02 280); /* Subtext */ -} -.catppuccin .xml-attribute-value { - color: oklch(0.8 0.15 160); /* #a6e3a1 green */ -} -.catppuccin .xml-comment { - color: oklch(0.5 0.04 280); /* Overlay */ - font-style: italic; -} -.catppuccin .xml-cdata { - color: oklch(0.75 0.15 220); /* #89b4fa blue */ -} -.catppuccin .xml-doctype { - color: oklch(0.8 0.15 350); /* #f5c2e7 pink */ -} -.catppuccin .xml-text { - color: oklch(0.9 0.01 280); /* Text */ -} /* One Dark theme */ -.onedark .xml-highlight { - color: oklch(0.85 0.02 240); /* #abb2bf */ -} -.onedark .xml-tag-bracket { - color: oklch(0.6 0.2 20); /* #e06c75 red */ -} -.onedark .xml-tag-name { - color: oklch(0.6 0.2 20); /* Red for tags */ -} -.onedark .xml-attribute-name { - color: oklch(0.8 0.15 80); /* #e5c07b yellow */ -} -.onedark .xml-attribute-equals { - color: oklch(0.7 0.02 240); /* Dim text */ -} -.onedark .xml-attribute-value { - color: oklch(0.75 0.18 150); /* #98c379 green */ -} -.onedark .xml-comment { - color: oklch(0.5 0.03 240); /* #5c6370 */ - font-style: italic; -} -.onedark .xml-cdata { - color: oklch(0.7 0.15 180); /* #56b6c2 cyan */ -} -.onedark .xml-doctype { - color: oklch(0.75 0.15 320); /* #c678dd magenta */ -} -.onedark .xml-text { - color: oklch(0.85 0.02 240); /* Text */ -} /* Synthwave theme */ -.synthwave .xml-highlight { - color: oklch(0.95 0.02 320); /* Warm white */ -} -.synthwave .xml-tag-bracket { - color: oklch(0.7 0.28 350); /* #f97e72 hot pink */ -} -.synthwave .xml-tag-name { - color: oklch(0.7 0.28 350); /* Hot pink */ - text-shadow: 0 0 8px oklch(0.7 0.28 350 / 0.5); -} -.synthwave .xml-attribute-name { - color: oklch(0.7 0.25 280); /* #ff7edb purple */ -} -.synthwave .xml-attribute-equals { - color: oklch(0.8 0.02 320); /* White-ish */ -} -.synthwave .xml-attribute-value { - color: oklch(0.85 0.2 60); /* #fede5d yellow */ - text-shadow: 0 0 5px oklch(0.85 0.2 60 / 0.3); -} -.synthwave .xml-comment { - color: oklch(0.55 0.08 290); /* Dim purple */ - font-style: italic; -} -.synthwave .xml-cdata { - color: oklch(0.8 0.25 200); /* #72f1b8 cyan */ -} -.synthwave .xml-doctype { - color: oklch(0.8 0.25 200); /* Cyan */ -} -.synthwave .xml-text { - color: oklch(0.95 0.02 320); /* White */ -} /* XML Editor container styles */ .xml-editor { diff --git a/apps/ui/src/styles/theme-imports.ts b/apps/ui/src/styles/theme-imports.ts new file mode 100644 index 00000000..c662342e --- /dev/null +++ b/apps/ui/src/styles/theme-imports.ts @@ -0,0 +1,22 @@ +/** + * Bundles all individual theme styles so the build pipeline + * doesn't tree-shake their CSS when imported dynamically. + */ +import "./themes/dark.css"; +import "./themes/light.css"; +import "./themes/retro.css"; +import "./themes/dracula.css"; +import "./themes/nord.css"; +import "./themes/monokai.css"; +import "./themes/tokyonight.css"; +import "./themes/solarized.css"; +import "./themes/gruvbox.css"; +import "./themes/catppuccin.css"; +import "./themes/onedark.css"; +import "./themes/synthwave.css"; +import "./themes/red.css"; +import "./themes/cream.css"; +import "./themes/sunset.css"; +import "./themes/gray.css"; + + diff --git a/apps/ui/src/styles/themes/catppuccin.css b/apps/ui/src/styles/themes/catppuccin.css new file mode 100644 index 00000000..422b6e52 --- /dev/null +++ b/apps/ui/src/styles/themes/catppuccin.css @@ -0,0 +1,144 @@ +/* Catppuccin Theme */ + +.catppuccin { + --background: oklch(0.18 0.02 260); /* #1e1e2e base */ + --background-50: oklch(0.18 0.02 260 / 0.5); + --background-80: oklch(0.18 0.02 260 / 0.8); + + --foreground: oklch(0.9 0.01 280); /* #cdd6f4 text */ + --foreground-secondary: oklch(0.75 0.02 280); /* #bac2de subtext1 */ + --foreground-muted: oklch(0.6 0.03 280); /* #a6adc8 subtext0 */ + + --card: oklch(0.22 0.02 260); /* #313244 surface0 */ + --card-foreground: oklch(0.9 0.01 280); + --popover: oklch(0.2 0.02 260); + --popover-foreground: oklch(0.9 0.01 280); + + --primary: oklch(0.75 0.15 280); /* #cba6f7 mauve */ + --primary-foreground: oklch(0.18 0.02 260); + + --brand-400: oklch(0.8 0.15 280); + --brand-500: oklch(0.75 0.15 280); /* Mauve */ + --brand-600: oklch(0.7 0.17 280); + + --secondary: oklch(0.26 0.02 260); /* #45475a surface1 */ + --secondary-foreground: oklch(0.9 0.01 280); + + --muted: oklch(0.26 0.02 260); + --muted-foreground: oklch(0.6 0.03 280); + + --accent: oklch(0.3 0.03 260); /* #585b70 surface2 */ + --accent-foreground: oklch(0.9 0.01 280); + + --destructive: oklch(0.65 0.2 15); /* #f38ba8 red */ + + --border: oklch(0.35 0.03 260); + --border-glass: oklch(0.75 0.15 280 / 0.3); + + --input: oklch(0.22 0.02 260); + --ring: oklch(0.75 0.15 280); + + --chart-1: oklch(0.75 0.15 280); /* Mauve */ + --chart-2: oklch(0.75 0.15 220); /* Blue #89b4fa */ + --chart-3: oklch(0.8 0.15 160); /* Green #a6e3a1 */ + --chart-4: oklch(0.8 0.15 350); /* Pink #f5c2e7 */ + --chart-5: oklch(0.85 0.12 90); /* Yellow #f9e2af */ + + --sidebar: oklch(0.16 0.02 260); /* #181825 mantle */ + --sidebar-foreground: oklch(0.9 0.01 280); + --sidebar-primary: oklch(0.75 0.15 280); + --sidebar-primary-foreground: oklch(0.18 0.02 260); + --sidebar-accent: oklch(0.26 0.02 260); + --sidebar-accent-foreground: oklch(0.9 0.01 280); + --sidebar-border: oklch(0.35 0.03 260); + --sidebar-ring: oklch(0.75 0.15 280); + + /* Action button colors - Catppuccin mauve/pink theme */ + --action-view: oklch(0.75 0.15 280); /* Mauve */ + --action-view-hover: oklch(0.7 0.17 280); + --action-followup: oklch(0.75 0.15 220); /* Blue */ + --action-followup-hover: oklch(0.7 0.17 220); + --action-commit: oklch(0.8 0.15 160); /* Green */ + --action-commit-hover: oklch(0.75 0.17 160); + --action-verify: oklch(0.8 0.15 160); /* Green */ + --action-verify-hover: oklch(0.75 0.17 160); + + /* Running indicator - Mauve */ + --running-indicator: oklch(0.75 0.15 280); + --running-indicator-text: oklch(0.8 0.13 280); +} + +/* ======================================== + ONE DARK THEME + Atom's iconic One Dark theme + ======================================== */ + +/* Theme-specific overrides */ + +.catppuccin .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #cba6f7 0%, #f5c2e7 50%, #cba6f7 100%); +} + +.catppuccin .animated-outline-inner { + background: oklch(0.18 0.02 260) !important; + color: #cba6f7 !important; +} + +.catppuccin [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.24 0.03 260) !important; + color: #f5c2e7 !important; +} + +.catppuccin .slider-track { + background: oklch(0.26 0.02 260); +} + +.catppuccin .slider-range { + background: linear-gradient(to right, #cba6f7, #89b4fa); +} + +.catppuccin .slider-thumb { + background: oklch(0.22 0.02 260); + border-color: #cba6f7; +} + +.catppuccin .xml-highlight { + color: oklch(0.9 0.01 280); /* #cdd6f4 */ +} + +.catppuccin .xml-tag-bracket { + color: oklch(0.65 0.2 15); /* #f38ba8 red */ +} + +.catppuccin .xml-tag-name { + color: oklch(0.65 0.2 15); /* Red for tags */ +} + +.catppuccin .xml-attribute-name { + color: oklch(0.75 0.15 280); /* #cba6f7 mauve */ +} + +.catppuccin .xml-attribute-equals { + color: oklch(0.75 0.02 280); /* Subtext */ +} + +.catppuccin .xml-attribute-value { + color: oklch(0.8 0.15 160); /* #a6e3a1 green */ +} + +.catppuccin .xml-comment { + color: oklch(0.5 0.04 280); /* Overlay */ + font-style: italic; +} + +.catppuccin .xml-cdata { + color: oklch(0.75 0.15 220); /* #89b4fa blue */ +} + +.catppuccin .xml-doctype { + color: oklch(0.8 0.15 350); /* #f5c2e7 pink */ +} + +.catppuccin .xml-text { + color: oklch(0.9 0.01 280); /* Text */ +} diff --git a/apps/ui/src/styles/themes/cream.css b/apps/ui/src/styles/themes/cream.css new file mode 100644 index 00000000..95fb349b --- /dev/null +++ b/apps/ui/src/styles/themes/cream.css @@ -0,0 +1,116 @@ +/* Cream Theme */ + +.cream { + /* Cream Theme - Warm, soft, easy on the eyes */ + --background: oklch(0.95 0.01 70); /* Warm cream background */ + --background-50: oklch(0.95 0.01 70 / 0.5); + --background-80: oklch(0.95 0.01 70 / 0.8); + + --foreground: oklch(0.25 0.02 60); /* Dark warm brown */ + --foreground-secondary: oklch(0.45 0.02 60); /* Medium brown */ + --foreground-muted: oklch(0.55 0.02 60); /* Light brown */ + + --card: oklch(0.98 0.005 70); /* Slightly lighter cream */ + --card-foreground: oklch(0.25 0.02 60); + --popover: oklch(0.97 0.008 70); + --popover-foreground: oklch(0.25 0.02 60); + + --primary: oklch(0.5 0.12 45); /* Warm terracotta/rust */ + --primary-foreground: oklch(0.98 0.005 70); + + --brand-400: oklch(0.55 0.12 45); + --brand-500: oklch(0.5 0.12 45); /* Terracotta */ + --brand-600: oklch(0.45 0.13 45); + + --secondary: oklch(0.88 0.02 70); + --secondary-foreground: oklch(0.25 0.02 60); + + --muted: oklch(0.9 0.015 70); + --muted-foreground: oklch(0.45 0.02 60); + + --accent: oklch(0.85 0.025 70); + --accent-foreground: oklch(0.25 0.02 60); + + --destructive: oklch(0.55 0.22 25); /* Warm red */ + + --border: oklch(0.85 0.015 70); + --border-glass: oklch(0.5 0.12 45 / 0.2); + + --input: oklch(0.98 0.005 70); + --ring: oklch(0.5 0.12 45); + + --chart-1: oklch(0.5 0.12 45); /* Terracotta */ + --chart-2: oklch(0.55 0.15 35); /* Burnt orange */ + --chart-3: oklch(0.6 0.12 100); /* Olive */ + --chart-4: oklch(0.5 0.15 20); /* Deep rust */ + --chart-5: oklch(0.65 0.1 80); /* Golden */ + + --sidebar: oklch(0.93 0.012 70); + --sidebar-foreground: oklch(0.25 0.02 60); + --sidebar-primary: oklch(0.5 0.12 45); + --sidebar-primary-foreground: oklch(0.98 0.005 70); + --sidebar-accent: oklch(0.88 0.02 70); + --sidebar-accent-foreground: oklch(0.25 0.02 60); + --sidebar-border: oklch(0.85 0.015 70); + --sidebar-ring: oklch(0.5 0.12 45); + + /* Action button colors - Warm earth tones */ + --action-view: oklch(0.5 0.12 45); /* Terracotta */ + --action-view-hover: oklch(0.45 0.13 45); + --action-followup: oklch(0.55 0.15 35); /* Burnt orange */ + --action-followup-hover: oklch(0.5 0.16 35); + --action-commit: oklch(0.55 0.12 130); /* Sage green */ + --action-commit-hover: oklch(0.5 0.13 130); + --action-verify: oklch(0.55 0.12 130); /* Sage green */ + --action-verify-hover: oklch(0.5 0.13 130); + + /* Running indicator - Terracotta */ + --running-indicator: oklch(0.5 0.12 45); + --running-indicator-text: oklch(0.55 0.12 45); + + /* Status colors - Cream theme */ + --status-success: oklch(0.55 0.15 130); + --status-success-bg: oklch(0.55 0.15 130 / 0.15); + --status-warning: oklch(0.6 0.15 70); + --status-warning-bg: oklch(0.6 0.15 70 / 0.15); + --status-error: oklch(0.55 0.22 25); + --status-error-bg: oklch(0.55 0.22 25 / 0.15); + --status-info: oklch(0.5 0.15 230); + --status-info-bg: oklch(0.5 0.15 230 / 0.15); + --status-backlog: oklch(0.6 0.02 60); + --status-in-progress: oklch(0.6 0.15 70); + --status-waiting: oklch(0.58 0.13 50); +} + + +/* Theme-specific overrides */ + +/* Cream theme scrollbar */ +.cream ::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.cream ::-webkit-scrollbar-thumb, +.cream .scrollbar-visible::-webkit-scrollbar-thumb { + background: oklch(0.7 0.03 60); + border-radius: 4px; +} + +.cream ::-webkit-scrollbar-thumb:hover, +.cream .scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0.04 60); +} + +.cream ::-webkit-scrollbar-track, +.cream .scrollbar-visible::-webkit-scrollbar-track { + background: oklch(0.9 0.015 70); +} + +.cream .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.7 0.03 60); +} + +.cream .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0.04 60); +} diff --git a/apps/ui/src/styles/themes/dark.css b/apps/ui/src/styles/themes/dark.css new file mode 100644 index 00000000..81aeb244 --- /dev/null +++ b/apps/ui/src/styles/themes/dark.css @@ -0,0 +1,166 @@ +/* Dark Theme */ + +.dark { + /* Deep dark backgrounds - zinc-950 family */ + --background: oklch(0.04 0 0); /* zinc-950 */ + --background-50: oklch(0.04 0 0 / 0.5); /* zinc-950/50 */ + --background-80: oklch(0.04 0 0 / 0.8); /* zinc-950/80 */ + + /* Text colors following hierarchy */ + --foreground: oklch(1 0 0); /* text-white */ + --foreground-secondary: oklch(0.588 0 0); /* text-zinc-400 */ + --foreground-muted: oklch(0.525 0 0); /* text-zinc-500 */ + + /* Card and popover backgrounds */ + --card: oklch(0.14 0 0); /* slightly lighter than background for contrast */ + --card-foreground: oklch(1 0 0); + --popover: oklch(0.10 0 0); /* slightly lighter than background */ + --popover-foreground: oklch(1 0 0); + + /* Brand colors - purple/violet theme */ + --primary: oklch(0.55 0.25 265); /* brand-500 */ + --primary-foreground: oklch(1 0 0); + --brand-400: oklch(0.6 0.22 265); + --brand-500: oklch(0.55 0.25 265); + --brand-600: oklch(0.5 0.28 270); /* purple-600 for gradients */ + + /* Glass morphism borders and accents */ + --secondary: oklch(1 0 0 / 0.05); /* bg-white/5 */ + --secondary-foreground: oklch(1 0 0); + --muted: oklch(0.176 0 0); /* zinc-800 */ + --muted-foreground: oklch(0.588 0 0); /* text-zinc-400 */ + --accent: oklch(1 0 0 / 0.1); /* bg-white/10 for hover */ + --accent-foreground: oklch(1 0 0); + + /* Borders with transparency for glass effect */ + --border: oklch(0.176 0 0); /* zinc-800 */ + --border-glass: oklch(1 0 0 / 0.1); /* white/10 for glass morphism */ + --destructive: oklch(0.6 0.25 25); + --input: oklch(0.04 0 0 / 0.8); /* Semi-transparent dark */ + --ring: oklch(0.55 0.25 265); + + /* Chart colors with brand theme */ + --chart-1: oklch(0.55 0.25 265); + --chart-2: oklch(0.65 0.2 160); + --chart-3: oklch(0.75 0.2 70); + --chart-4: oklch(0.6 0.25 300); + --chart-5: oklch(0.6 0.25 20); + + /* Sidebar with glass morphism */ + --sidebar: oklch(0.04 0 0 / 0.5); /* zinc-950/50 with backdrop blur */ + --sidebar-foreground: oklch(1 0 0); + --sidebar-primary: oklch(0.55 0.25 265); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(1 0 0 / 0.05); /* bg-white/5 */ + --sidebar-accent-foreground: oklch(1 0 0); + --sidebar-border: oklch(1 0 0 / 0.1); /* white/10 for glass borders */ + --sidebar-ring: oklch(0.55 0.25 265); + + /* Action button colors */ + --action-view: oklch(0.6 0.25 265); /* Purple */ + --action-view-hover: oklch(0.55 0.27 270); + --action-followup: oklch(0.6 0.2 230); /* Blue */ + --action-followup-hover: oklch(0.55 0.22 230); + --action-commit: oklch(0.55 0.2 140); /* Green */ + --action-commit-hover: oklch(0.5 0.22 140); + --action-verify: oklch(0.55 0.2 140); /* Green */ + --action-verify-hover: oklch(0.5 0.22 140); + + /* Running indicator - Purple */ + --running-indicator: oklch(0.6 0.25 265); + --running-indicator-text: oklch(0.65 0.22 265); + + /* Status colors - Dark mode */ + --status-success: oklch(0.65 0.2 140); + --status-success-bg: oklch(0.65 0.2 140 / 0.2); + --status-warning: oklch(0.75 0.15 70); + --status-warning-bg: oklch(0.75 0.15 70 / 0.2); + --status-error: oklch(0.65 0.22 25); + --status-error-bg: oklch(0.65 0.22 25 / 0.2); + --status-info: oklch(0.65 0.2 230); + --status-info-bg: oklch(0.65 0.2 230 / 0.2); + --status-backlog: oklch(0.6 0 0); + --status-in-progress: oklch(0.75 0.15 70); + --status-waiting: oklch(0.7 0.18 50); + + /* Shadow tokens - darker for dark mode */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3); +} + +/* Theme-specific overrides */ + + .dark .content-bg { + background: linear-gradient(135deg, oklch(0.04 0 0), oklch(0.08 0 0), oklch(0.04 0 0)); + } + +.dark .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #3b82f6 50%, #a855f7 100%); +} + +.dark .animated-outline-inner { + background: oklch(0.15 0 0) !important; + color: #c084fc !important; +} + +.dark [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.2 0.02 270) !important; + color: #e9d5ff !important; +} + +.dark .slider-track { + background: oklch(0.2 0 0); +} + +.dark .slider-range { + background: linear-gradient(to right, #a855f7, #3b82f6); +} + +.dark .slider-thumb { + background: oklch(0.25 0 0); + border-color: oklch(0.4 0 0); +} + +.dark .xml-highlight { + color: oklch(0.9 0 0); /* Default light text */ +} + +.dark .xml-tag-bracket { + color: oklch(0.7 0.12 220); /* Soft blue for < > */ +} + +.dark .xml-tag-name { + color: oklch(0.75 0.2 25); /* Coral/salmon for tag names */ +} + +.dark .xml-attribute-name { + color: oklch(0.8 0.15 280); /* Light purple for attributes */ +} + +.dark .xml-attribute-equals { + color: oklch(0.6 0 0); /* Gray for = */ +} + +.dark .xml-attribute-value { + color: oklch(0.8 0.18 145); /* Bright green for strings */ +} + +.dark .xml-comment { + color: oklch(0.55 0.05 100); /* Muted for comments */ + font-style: italic; +} + +.dark .xml-cdata { + color: oklch(0.7 0.12 200); /* Teal for CDATA */ +} + +.dark .xml-doctype { + color: oklch(0.7 0.15 280); /* Purple for DOCTYPE */ +} + +.dark .xml-text { + color: oklch(0.85 0 0); /* Off-white for text */ +} diff --git a/apps/ui/src/styles/themes/dracula.css b/apps/ui/src/styles/themes/dracula.css new file mode 100644 index 00000000..d7f569b3 --- /dev/null +++ b/apps/ui/src/styles/themes/dracula.css @@ -0,0 +1,144 @@ +/* Dracula Theme */ + +.dracula { + --background: oklch(0.18 0.02 280); /* #282a36 */ + --background-50: oklch(0.18 0.02 280 / 0.5); + --background-80: oklch(0.18 0.02 280 / 0.8); + + --foreground: oklch(0.95 0.01 280); /* #f8f8f2 */ + --foreground-secondary: oklch(0.7 0.05 280); + --foreground-muted: oklch(0.55 0.08 280); /* #6272a4 */ + + --card: oklch(0.22 0.02 280); /* #44475a */ + --card-foreground: oklch(0.95 0.01 280); + --popover: oklch(0.2 0.02 280); + --popover-foreground: oklch(0.95 0.01 280); + + --primary: oklch(0.7 0.2 320); /* #bd93f9 purple */ + --primary-foreground: oklch(0.18 0.02 280); + + --brand-400: oklch(0.75 0.2 320); + --brand-500: oklch(0.7 0.2 320); /* #bd93f9 */ + --brand-600: oklch(0.65 0.22 320); + + --secondary: oklch(0.28 0.03 280); /* #44475a */ + --secondary-foreground: oklch(0.95 0.01 280); + + --muted: oklch(0.28 0.03 280); + --muted-foreground: oklch(0.55 0.08 280); /* #6272a4 */ + + --accent: oklch(0.32 0.04 280); + --accent-foreground: oklch(0.95 0.01 280); + + --destructive: oklch(0.65 0.25 15); /* #ff5555 */ + + --border: oklch(0.35 0.05 280); + --border-glass: oklch(0.7 0.2 320 / 0.3); + + --input: oklch(0.22 0.02 280); + --ring: oklch(0.7 0.2 320); + + --chart-1: oklch(0.7 0.2 320); /* Purple */ + --chart-2: oklch(0.75 0.2 180); /* Cyan #8be9fd */ + --chart-3: oklch(0.8 0.2 130); /* Green #50fa7b */ + --chart-4: oklch(0.7 0.25 350); /* Pink #ff79c6 */ + --chart-5: oklch(0.85 0.2 90); /* Yellow #f1fa8c */ + + --sidebar: oklch(0.16 0.02 280); + --sidebar-foreground: oklch(0.95 0.01 280); + --sidebar-primary: oklch(0.7 0.2 320); + --sidebar-primary-foreground: oklch(0.18 0.02 280); + --sidebar-accent: oklch(0.28 0.03 280); + --sidebar-accent-foreground: oklch(0.95 0.01 280); + --sidebar-border: oklch(0.35 0.05 280); + --sidebar-ring: oklch(0.7 0.2 320); + + /* Action button colors - Dracula purple/pink theme */ + --action-view: oklch(0.7 0.2 320); /* Purple */ + --action-view-hover: oklch(0.65 0.22 320); + --action-followup: oklch(0.65 0.25 350); /* Pink */ + --action-followup-hover: oklch(0.6 0.27 350); + --action-commit: oklch(0.75 0.2 130); /* Green */ + --action-commit-hover: oklch(0.7 0.22 130); + --action-verify: oklch(0.75 0.2 130); /* Green */ + --action-verify-hover: oklch(0.7 0.22 130); + + /* Running indicator - Purple */ + --running-indicator: oklch(0.7 0.2 320); + --running-indicator-text: oklch(0.75 0.18 320); +} + +/* ======================================== + NORD THEME + Inspired by the Arctic, north-bluish color palette + ======================================== */ + +/* Theme-specific overrides */ + +.dracula .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #bd93f9 0%, #ff79c6 50%, #bd93f9 100%); +} + +.dracula .animated-outline-inner { + background: oklch(0.18 0.02 280) !important; + color: #bd93f9 !important; +} + +.dracula [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.24 0.03 280) !important; + color: #ff79c6 !important; +} + +.dracula .slider-track { + background: oklch(0.28 0.03 280); +} + +.dracula .slider-range { + background: linear-gradient(to right, #bd93f9, #ff79c6); +} + +.dracula .slider-thumb { + background: oklch(0.22 0.02 280); + border-color: #bd93f9; +} + +.dracula .xml-highlight { + color: oklch(0.95 0.01 280); /* #f8f8f2 */ +} + +.dracula .xml-tag-bracket { + color: oklch(0.7 0.25 350); /* Pink #ff79c6 */ +} + +.dracula .xml-tag-name { + color: oklch(0.7 0.25 350); /* Pink for tags */ +} + +.dracula .xml-attribute-name { + color: oklch(0.8 0.2 130); /* Green #50fa7b */ +} + +.dracula .xml-attribute-equals { + color: oklch(0.95 0.01 280); /* White */ +} + +.dracula .xml-attribute-value { + color: oklch(0.85 0.2 90); /* Yellow #f1fa8c */ +} + +.dracula .xml-comment { + color: oklch(0.55 0.08 280); /* #6272a4 */ + font-style: italic; +} + +.dracula .xml-cdata { + color: oklch(0.75 0.2 180); /* Cyan */ +} + +.dracula .xml-doctype { + color: oklch(0.7 0.2 320); /* Purple #bd93f9 */ +} + +.dracula .xml-text { + color: oklch(0.95 0.01 280); /* White */ +} diff --git a/apps/ui/src/styles/themes/gray.css b/apps/ui/src/styles/themes/gray.css new file mode 100644 index 00000000..3ee72483 --- /dev/null +++ b/apps/ui/src/styles/themes/gray.css @@ -0,0 +1,110 @@ +/* Gray Theme */ + +.gray { + /* Gray Theme - Modern, minimal gray scheme inspired by Cursor */ + --background: oklch(0.2 0.005 250); /* Medium-dark neutral gray */ + --background-50: oklch(0.2 0.005 250 / 0.5); + --background-80: oklch(0.2 0.005 250 / 0.8); + + --foreground: oklch(0.9 0.005 250); /* Light gray */ + --foreground-secondary: oklch(0.65 0.005 250); + --foreground-muted: oklch(0.5 0.005 250); + + --card: oklch(0.24 0.005 250); + --card-foreground: oklch(0.9 0.005 250); + --popover: oklch(0.22 0.005 250); + --popover-foreground: oklch(0.9 0.005 250); + + --primary: oklch(0.6 0.08 250); /* Subtle blue-gray */ + --primary-foreground: oklch(0.95 0.005 250); + + --brand-400: oklch(0.65 0.08 250); + --brand-500: oklch(0.6 0.08 250); /* Blue-gray */ + --brand-600: oklch(0.55 0.09 250); + + --secondary: oklch(0.28 0.005 250); + --secondary-foreground: oklch(0.9 0.005 250); + + --muted: oklch(0.3 0.005 250); + --muted-foreground: oklch(0.6 0.005 250); + + --accent: oklch(0.35 0.01 250); + --accent-foreground: oklch(0.9 0.005 250); + + --destructive: oklch(0.6 0.2 25); /* Muted red */ + + --border: oklch(0.32 0.005 250); + --border-glass: oklch(0.6 0.08 250 / 0.2); + + --input: oklch(0.24 0.005 250); + --ring: oklch(0.6 0.08 250); + + --chart-1: oklch(0.6 0.08 250); /* Blue-gray */ + --chart-2: oklch(0.65 0.1 210); /* Cyan */ + --chart-3: oklch(0.7 0.12 160); /* Teal */ + --chart-4: oklch(0.65 0.1 280); /* Purple */ + --chart-5: oklch(0.7 0.08 300); /* Violet */ + + --sidebar: oklch(0.18 0.005 250); + --sidebar-foreground: oklch(0.9 0.005 250); + --sidebar-primary: oklch(0.6 0.08 250); + --sidebar-primary-foreground: oklch(0.95 0.005 250); + --sidebar-accent: oklch(0.28 0.005 250); + --sidebar-accent-foreground: oklch(0.9 0.005 250); + --sidebar-border: oklch(0.32 0.005 250); + --sidebar-ring: oklch(0.6 0.08 250); + + /* Action button colors - Subtle modern colors */ + --action-view: oklch(0.6 0.08 250); /* Blue-gray */ + --action-view-hover: oklch(0.55 0.09 250); + --action-followup: oklch(0.65 0.1 210); /* Cyan */ + --action-followup-hover: oklch(0.6 0.11 210); + --action-commit: oklch(0.65 0.12 150); /* Teal-green */ + --action-commit-hover: oklch(0.6 0.13 150); + --action-verify: oklch(0.65 0.12 150); /* Teal-green */ + --action-verify-hover: oklch(0.6 0.13 150); + + /* Running indicator - Blue-gray */ + --running-indicator: oklch(0.6 0.08 250); + --running-indicator-text: oklch(0.65 0.08 250); + + /* Status colors - Gray theme */ + --status-success: oklch(0.65 0.12 150); + --status-success-bg: oklch(0.65 0.12 150 / 0.2); + --status-warning: oklch(0.7 0.15 70); + --status-warning-bg: oklch(0.7 0.15 70 / 0.2); + --status-error: oklch(0.6 0.2 25); + --status-error-bg: oklch(0.6 0.2 25 / 0.2); + --status-info: oklch(0.65 0.1 210); + --status-info-bg: oklch(0.65 0.1 210 / 0.2); + --status-backlog: oklch(0.6 0.005 250); + --status-in-progress: oklch(0.7 0.15 70); + --status-waiting: oklch(0.68 0.1 220); +} + +/* Theme-specific overrides */ + +/* Gray theme scrollbar */ +.gray ::-webkit-scrollbar-thumb, +.gray .scrollbar-visible::-webkit-scrollbar-thumb { + background: oklch(0.4 0.01 250); + border-radius: 4px; +} + +.gray ::-webkit-scrollbar-thumb:hover, +.gray .scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: oklch(0.5 0.02 250); +} + +.gray ::-webkit-scrollbar-track, +.gray .scrollbar-visible::-webkit-scrollbar-track { + background: oklch(0.25 0.005 250); +} + +.gray .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.4 0.01 250); +} + +.gray .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.5 0.02 250); +} diff --git a/apps/ui/src/styles/themes/gruvbox.css b/apps/ui/src/styles/themes/gruvbox.css new file mode 100644 index 00000000..074dddbd --- /dev/null +++ b/apps/ui/src/styles/themes/gruvbox.css @@ -0,0 +1,144 @@ +/* Gruvbox Theme */ + +.gruvbox { + --background: oklch(0.18 0.02 60); /* #282828 bg */ + --background-50: oklch(0.18 0.02 60 / 0.5); + --background-80: oklch(0.18 0.02 60 / 0.8); + + --foreground: oklch(0.85 0.05 85); /* #ebdbb2 fg */ + --foreground-secondary: oklch(0.7 0.04 85); /* #d5c4a1 */ + --foreground-muted: oklch(0.55 0.04 85); /* #928374 */ + + --card: oklch(0.22 0.02 60); /* #3c3836 bg1 */ + --card-foreground: oklch(0.85 0.05 85); + --popover: oklch(0.2 0.02 60); + --popover-foreground: oklch(0.85 0.05 85); + + --primary: oklch(0.7 0.18 55); /* #fabd2f yellow */ + --primary-foreground: oklch(0.18 0.02 60); + + --brand-400: oklch(0.75 0.18 55); + --brand-500: oklch(0.7 0.18 55); /* Yellow */ + --brand-600: oklch(0.65 0.2 55); + + --secondary: oklch(0.26 0.02 60); /* #504945 bg2 */ + --secondary-foreground: oklch(0.85 0.05 85); + + --muted: oklch(0.26 0.02 60); + --muted-foreground: oklch(0.55 0.04 85); + + --accent: oklch(0.3 0.03 60); + --accent-foreground: oklch(0.85 0.05 85); + + --destructive: oklch(0.55 0.22 25); /* #fb4934 red */ + + --border: oklch(0.35 0.03 60); + --border-glass: oklch(0.7 0.18 55 / 0.3); + + --input: oklch(0.22 0.02 60); + --ring: oklch(0.7 0.18 55); + + --chart-1: oklch(0.7 0.18 55); /* Yellow */ + --chart-2: oklch(0.65 0.2 140); /* Green #b8bb26 */ + --chart-3: oklch(0.7 0.15 200); /* Aqua #8ec07c */ + --chart-4: oklch(0.6 0.2 30); /* Orange #fe8019 */ + --chart-5: oklch(0.6 0.2 320); /* Purple #d3869b */ + + --sidebar: oklch(0.16 0.02 60); + --sidebar-foreground: oklch(0.85 0.05 85); + --sidebar-primary: oklch(0.7 0.18 55); + --sidebar-primary-foreground: oklch(0.18 0.02 60); + --sidebar-accent: oklch(0.26 0.02 60); + --sidebar-accent-foreground: oklch(0.85 0.05 85); + --sidebar-border: oklch(0.35 0.03 60); + --sidebar-ring: oklch(0.7 0.18 55); + + /* Action button colors - Gruvbox yellow/orange theme */ + --action-view: oklch(0.7 0.18 55); /* Yellow */ + --action-view-hover: oklch(0.65 0.2 55); + --action-followup: oklch(0.7 0.15 200); /* Aqua */ + --action-followup-hover: oklch(0.65 0.17 200); + --action-commit: oklch(0.65 0.2 140); /* Green */ + --action-commit-hover: oklch(0.6 0.22 140); + --action-verify: oklch(0.65 0.2 140); /* Green */ + --action-verify-hover: oklch(0.6 0.22 140); + + /* Running indicator - Yellow */ + --running-indicator: oklch(0.7 0.18 55); + --running-indicator-text: oklch(0.75 0.16 55); +} + +/* ======================================== + CATPPUCCIN MOCHA THEME + Soothing pastel theme for the high-spirited + ======================================== */ + +/* Theme-specific overrides */ + +.gruvbox .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #fabd2f 0%, #fe8019 50%, #fabd2f 100%); +} + +.gruvbox .animated-outline-inner { + background: oklch(0.18 0.02 60) !important; + color: #fabd2f !important; +} + +.gruvbox [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.24 0.03 60) !important; + color: #fe8019 !important; +} + +.gruvbox .slider-track { + background: oklch(0.26 0.02 60); +} + +.gruvbox .slider-range { + background: linear-gradient(to right, #fabd2f, #fe8019); +} + +.gruvbox .slider-thumb { + background: oklch(0.22 0.02 60); + border-color: #fabd2f; +} + +.gruvbox .xml-highlight { + color: oklch(0.85 0.05 85); /* #ebdbb2 */ +} + +.gruvbox .xml-tag-bracket { + color: oklch(0.55 0.22 25); /* #fb4934 red */ +} + +.gruvbox .xml-tag-name { + color: oklch(0.55 0.22 25); /* Red for tags */ +} + +.gruvbox .xml-attribute-name { + color: oklch(0.7 0.15 200); /* #8ec07c aqua */ +} + +.gruvbox .xml-attribute-equals { + color: oklch(0.7 0.04 85); /* Dim text */ +} + +.gruvbox .xml-attribute-value { + color: oklch(0.65 0.2 140); /* #b8bb26 green */ +} + +.gruvbox .xml-comment { + color: oklch(0.55 0.04 85); /* #928374 gray */ + font-style: italic; +} + +.gruvbox .xml-cdata { + color: oklch(0.7 0.15 200); /* Aqua */ +} + +.gruvbox .xml-doctype { + color: oklch(0.6 0.2 320); /* #d3869b purple */ +} + +.gruvbox .xml-text { + color: oklch(0.85 0.05 85); /* Foreground */ +} diff --git a/apps/ui/src/styles/themes/light.css b/apps/ui/src/styles/themes/light.css new file mode 100644 index 00000000..2c8cdc4b --- /dev/null +++ b/apps/ui/src/styles/themes/light.css @@ -0,0 +1,103 @@ +/* Light Theme Overrides */ + +.light .scrollbar-visible::-webkit-scrollbar-track { + background: oklch(0.95 0 0); +} + +.light .scrollbar-visible::-webkit-scrollbar-thumb { + background: oklch(0.7 0 0); +} + +.light .scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0 0); +} + +.light .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.75 0 0); +} + +.light .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.65 0 0); +} + + .light .bg-glass { + background: oklch(1 0 0 / 0.8); + } + + .light .bg-glass-80 { + background: oklch(1 0 0 / 0.95); + } + + .light .content-bg { + background: linear-gradient(135deg, oklch(0.99 0 0), oklch(0.98 0 0), oklch(0.99 0 0)); + } + +.light .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #7c3aed 0%, #2563eb 50%, #7c3aed 100%); +} + +.light .animated-outline-inner { + background: oklch(100% 0 0) !important; + color: #7c3aed !important; + border: 1px solid oklch(92% 0 0); +} + +.light [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(97% 0.02 270) !important; + color: #5b21b6 !important; +} + +.light .slider-track { + background: oklch(90% 0 0); +} + +.light .slider-range { + background: linear-gradient(to right, #7c3aed, #2563eb); +} + +.light .slider-thumb { + background: oklch(100% 0 0); + border-color: oklch(80% 0 0); +} + +.light .xml-highlight { + color: oklch(0.3 0 0); /* Default text */ +} + +.light .xml-tag-bracket { + color: oklch(0.45 0.15 250); /* Blue-gray for < > */ +} + +.light .xml-tag-name { + color: oklch(0.45 0.22 25); /* Red/maroon for tag names */ +} + +.light .xml-attribute-name { + color: oklch(0.45 0.18 280); /* Purple for attributes */ +} + +.light .xml-attribute-equals { + color: oklch(0.4 0 0); /* Dark gray for = */ +} + +.light .xml-attribute-value { + color: oklch(0.45 0.18 145); /* Green for string values */ +} + +.light .xml-comment { + color: oklch(0.55 0.05 100); /* Muted olive for comments */ + font-style: italic; +} + +.light .xml-cdata { + color: oklch(0.5 0.1 200); /* Teal for CDATA */ +} + +.light .xml-doctype { + color: oklch(0.5 0.15 280); /* Purple for DOCTYPE */ +} + +.light .xml-text { + color: oklch(0.25 0 0); /* Near-black for text content */ +} + diff --git a/apps/ui/src/styles/themes/monokai.css b/apps/ui/src/styles/themes/monokai.css new file mode 100644 index 00000000..f25cf0e2 --- /dev/null +++ b/apps/ui/src/styles/themes/monokai.css @@ -0,0 +1,144 @@ +/* Monokai Theme */ + +.monokai { + --background: oklch(0.17 0.01 90); /* #272822 */ + --background-50: oklch(0.17 0.01 90 / 0.5); + --background-80: oklch(0.17 0.01 90 / 0.8); + + --foreground: oklch(0.95 0.02 100); /* #f8f8f2 */ + --foreground-secondary: oklch(0.8 0.02 100); + --foreground-muted: oklch(0.55 0.04 100); /* #75715e */ + + --card: oklch(0.22 0.01 90); /* #3e3d32 */ + --card-foreground: oklch(0.95 0.02 100); + --popover: oklch(0.2 0.01 90); + --popover-foreground: oklch(0.95 0.02 100); + + --primary: oklch(0.8 0.2 350); /* #f92672 pink */ + --primary-foreground: oklch(0.17 0.01 90); + + --brand-400: oklch(0.85 0.2 350); + --brand-500: oklch(0.8 0.2 350); /* #f92672 */ + --brand-600: oklch(0.75 0.22 350); + + --secondary: oklch(0.25 0.02 90); + --secondary-foreground: oklch(0.95 0.02 100); + + --muted: oklch(0.25 0.02 90); + --muted-foreground: oklch(0.55 0.04 100); + + --accent: oklch(0.3 0.02 90); + --accent-foreground: oklch(0.95 0.02 100); + + --destructive: oklch(0.65 0.25 15); /* red */ + + --border: oklch(0.35 0.03 90); + --border-glass: oklch(0.8 0.2 350 / 0.3); + + --input: oklch(0.22 0.01 90); + --ring: oklch(0.8 0.2 350); + + --chart-1: oklch(0.8 0.2 350); /* Pink #f92672 */ + --chart-2: oklch(0.85 0.2 90); /* Yellow #e6db74 */ + --chart-3: oklch(0.8 0.2 140); /* Green #a6e22e */ + --chart-4: oklch(0.75 0.2 200); /* Cyan #66d9ef */ + --chart-5: oklch(0.75 0.2 30); /* Orange #fd971f */ + + --sidebar: oklch(0.15 0.01 90); + --sidebar-foreground: oklch(0.95 0.02 100); + --sidebar-primary: oklch(0.8 0.2 350); + --sidebar-primary-foreground: oklch(0.17 0.01 90); + --sidebar-accent: oklch(0.25 0.02 90); + --sidebar-accent-foreground: oklch(0.95 0.02 100); + --sidebar-border: oklch(0.35 0.03 90); + --sidebar-ring: oklch(0.8 0.2 350); + + /* Action button colors - Monokai pink/yellow theme */ + --action-view: oklch(0.8 0.2 350); /* Pink */ + --action-view-hover: oklch(0.75 0.22 350); + --action-followup: oklch(0.75 0.2 200); /* Cyan */ + --action-followup-hover: oklch(0.7 0.22 200); + --action-commit: oklch(0.8 0.2 140); /* Green */ + --action-commit-hover: oklch(0.75 0.22 140); + --action-verify: oklch(0.8 0.2 140); /* Green */ + --action-verify-hover: oklch(0.75 0.22 140); + + /* Running indicator - Pink */ + --running-indicator: oklch(0.8 0.2 350); + --running-indicator-text: oklch(0.85 0.18 350); +} + +/* ======================================== + TOKYO NIGHT THEME + A clean dark theme celebrating Tokyo at night + ======================================== */ + +/* Theme-specific overrides */ + +.monokai .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #f92672 0%, #e6db74 50%, #f92672 100%); +} + +.monokai .animated-outline-inner { + background: oklch(0.17 0.01 90) !important; + color: #f92672 !important; +} + +.monokai [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.22 0.02 90) !important; + color: #e6db74 !important; +} + +.monokai .slider-track { + background: oklch(0.25 0.02 90); +} + +.monokai .slider-range { + background: linear-gradient(to right, #f92672, #fd971f); +} + +.monokai .slider-thumb { + background: oklch(0.22 0.01 90); + border-color: #f92672; +} + +.monokai .xml-highlight { + color: oklch(0.95 0.02 100); /* #f8f8f2 */ +} + +.monokai .xml-tag-bracket { + color: oklch(0.95 0.02 100); /* White */ +} + +.monokai .xml-tag-name { + color: oklch(0.8 0.2 350); /* #f92672 pink */ +} + +.monokai .xml-attribute-name { + color: oklch(0.8 0.2 140); /* #a6e22e green */ +} + +.monokai .xml-attribute-equals { + color: oklch(0.95 0.02 100); /* White */ +} + +.monokai .xml-attribute-value { + color: oklch(0.85 0.2 90); /* #e6db74 yellow */ +} + +.monokai .xml-comment { + color: oklch(0.55 0.04 100); /* #75715e */ + font-style: italic; +} + +.monokai .xml-cdata { + color: oklch(0.75 0.2 200); /* Cyan #66d9ef */ +} + +.monokai .xml-doctype { + color: oklch(0.75 0.2 200); /* Cyan */ +} + +.monokai .xml-text { + color: oklch(0.95 0.02 100); /* White */ +} diff --git a/apps/ui/src/styles/themes/nord.css b/apps/ui/src/styles/themes/nord.css new file mode 100644 index 00000000..2cc98ec0 --- /dev/null +++ b/apps/ui/src/styles/themes/nord.css @@ -0,0 +1,144 @@ +/* Nord Theme */ + +.nord { + --background: oklch(0.23 0.02 240); /* #2e3440 */ + --background-50: oklch(0.23 0.02 240 / 0.5); + --background-80: oklch(0.23 0.02 240 / 0.8); + + --foreground: oklch(0.9 0.01 230); /* #eceff4 */ + --foreground-secondary: oklch(0.75 0.02 230); /* #d8dee9 */ + --foreground-muted: oklch(0.6 0.03 230); /* #4c566a */ + + --card: oklch(0.27 0.02 240); /* #3b4252 */ + --card-foreground: oklch(0.9 0.01 230); + --popover: oklch(0.25 0.02 240); + --popover-foreground: oklch(0.9 0.01 230); + + --primary: oklch(0.7 0.12 220); /* #88c0d0 frost */ + --primary-foreground: oklch(0.23 0.02 240); + + --brand-400: oklch(0.75 0.12 220); + --brand-500: oklch(0.7 0.12 220); /* #88c0d0 */ + --brand-600: oklch(0.65 0.14 220); /* #81a1c1 */ + + --secondary: oklch(0.31 0.02 240); /* #434c5e */ + --secondary-foreground: oklch(0.9 0.01 230); + + --muted: oklch(0.31 0.02 240); + --muted-foreground: oklch(0.55 0.03 230); + + --accent: oklch(0.35 0.03 240); /* #4c566a */ + --accent-foreground: oklch(0.9 0.01 230); + + --destructive: oklch(0.65 0.2 15); /* #bf616a */ + + --border: oklch(0.35 0.03 240); + --border-glass: oklch(0.7 0.12 220 / 0.3); + + --input: oklch(0.27 0.02 240); + --ring: oklch(0.7 0.12 220); + + --chart-1: oklch(0.7 0.12 220); /* Frost blue */ + --chart-2: oklch(0.65 0.14 220); /* #81a1c1 */ + --chart-3: oklch(0.7 0.15 140); /* #a3be8c green */ + --chart-4: oklch(0.7 0.2 320); /* #b48ead purple */ + --chart-5: oklch(0.75 0.15 70); /* #ebcb8b yellow */ + + --sidebar: oklch(0.21 0.02 240); + --sidebar-foreground: oklch(0.9 0.01 230); + --sidebar-primary: oklch(0.7 0.12 220); + --sidebar-primary-foreground: oklch(0.23 0.02 240); + --sidebar-accent: oklch(0.31 0.02 240); + --sidebar-accent-foreground: oklch(0.9 0.01 230); + --sidebar-border: oklch(0.35 0.03 240); + --sidebar-ring: oklch(0.7 0.12 220); + + /* Action button colors - Nord frost blue theme */ + --action-view: oklch(0.7 0.12 220); /* Frost blue */ + --action-view-hover: oklch(0.65 0.14 220); + --action-followup: oklch(0.65 0.14 220); /* Darker frost */ + --action-followup-hover: oklch(0.6 0.16 220); + --action-commit: oklch(0.7 0.15 140); /* Green */ + --action-commit-hover: oklch(0.65 0.17 140); + --action-verify: oklch(0.7 0.15 140); /* Green */ + --action-verify-hover: oklch(0.65 0.17 140); + + /* Running indicator - Frost blue */ + --running-indicator: oklch(0.7 0.12 220); + --running-indicator-text: oklch(0.75 0.1 220); +} + +/* ======================================== + MONOKAI THEME + The classic Monokai color scheme + ======================================== */ + +/* Theme-specific overrides */ + +.nord .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #88c0d0 0%, #81a1c1 50%, #88c0d0 100%); +} + +.nord .animated-outline-inner { + background: oklch(0.23 0.02 240) !important; + color: #88c0d0 !important; +} + +.nord [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.28 0.03 240) !important; + color: #8fbcbb !important; +} + +.nord .slider-track { + background: oklch(0.31 0.02 240); +} + +.nord .slider-range { + background: linear-gradient(to right, #88c0d0, #81a1c1); +} + +.nord .slider-thumb { + background: oklch(0.27 0.02 240); + border-color: #88c0d0; +} + +.nord .xml-highlight { + color: oklch(0.9 0.01 230); /* #eceff4 */ +} + +.nord .xml-tag-bracket { + color: oklch(0.65 0.14 220); /* #81a1c1 */ +} + +.nord .xml-tag-name { + color: oklch(0.65 0.14 220); /* Frost blue for tags */ +} + +.nord .xml-attribute-name { + color: oklch(0.7 0.12 220); /* #88c0d0 */ +} + +.nord .xml-attribute-equals { + color: oklch(0.75 0.02 230); /* Dim white */ +} + +.nord .xml-attribute-value { + color: oklch(0.7 0.15 140); /* #a3be8c green */ +} + +.nord .xml-comment { + color: oklch(0.5 0.04 230); /* Dim text */ + font-style: italic; +} + +.nord .xml-cdata { + color: oklch(0.7 0.12 220); /* Frost blue */ +} + +.nord .xml-doctype { + color: oklch(0.7 0.2 320); /* #b48ead purple */ +} + +.nord .xml-text { + color: oklch(0.9 0.01 230); /* Snow white */ +} diff --git a/apps/ui/src/styles/themes/onedark.css b/apps/ui/src/styles/themes/onedark.css new file mode 100644 index 00000000..403dfd9e --- /dev/null +++ b/apps/ui/src/styles/themes/onedark.css @@ -0,0 +1,144 @@ +/* Onedark Theme */ + +.onedark { + --background: oklch(0.19 0.01 250); /* #282c34 */ + --background-50: oklch(0.19 0.01 250 / 0.5); + --background-80: oklch(0.19 0.01 250 / 0.8); + + --foreground: oklch(0.85 0.02 240); /* #abb2bf */ + --foreground-secondary: oklch(0.7 0.02 240); + --foreground-muted: oklch(0.5 0.03 240); /* #5c6370 */ + + --card: oklch(0.23 0.01 250); /* #21252b */ + --card-foreground: oklch(0.85 0.02 240); + --popover: oklch(0.21 0.01 250); + --popover-foreground: oklch(0.85 0.02 240); + + --primary: oklch(0.7 0.18 230); /* #61afef blue */ + --primary-foreground: oklch(0.19 0.01 250); + + --brand-400: oklch(0.75 0.18 230); + --brand-500: oklch(0.7 0.18 230); /* Blue */ + --brand-600: oklch(0.65 0.2 230); + + --secondary: oklch(0.25 0.01 250); + --secondary-foreground: oklch(0.85 0.02 240); + + --muted: oklch(0.25 0.01 250); + --muted-foreground: oklch(0.5 0.03 240); + + --accent: oklch(0.28 0.02 250); + --accent-foreground: oklch(0.85 0.02 240); + + --destructive: oklch(0.6 0.2 20); /* #e06c75 red */ + + --border: oklch(0.35 0.02 250); + --border-glass: oklch(0.7 0.18 230 / 0.3); + + --input: oklch(0.23 0.01 250); + --ring: oklch(0.7 0.18 230); + + --chart-1: oklch(0.7 0.18 230); /* Blue */ + --chart-2: oklch(0.75 0.15 320); /* Magenta #c678dd */ + --chart-3: oklch(0.75 0.18 150); /* Green #98c379 */ + --chart-4: oklch(0.8 0.15 80); /* Yellow #e5c07b */ + --chart-5: oklch(0.7 0.15 180); /* Cyan #56b6c2 */ + + --sidebar: oklch(0.17 0.01 250); + --sidebar-foreground: oklch(0.85 0.02 240); + --sidebar-primary: oklch(0.7 0.18 230); + --sidebar-primary-foreground: oklch(0.19 0.01 250); + --sidebar-accent: oklch(0.25 0.01 250); + --sidebar-accent-foreground: oklch(0.85 0.02 240); + --sidebar-border: oklch(0.35 0.02 250); + --sidebar-ring: oklch(0.7 0.18 230); + + /* Action button colors - One Dark blue/magenta theme */ + --action-view: oklch(0.7 0.18 230); /* Blue */ + --action-view-hover: oklch(0.65 0.2 230); + --action-followup: oklch(0.75 0.15 320); /* Magenta */ + --action-followup-hover: oklch(0.7 0.17 320); + --action-commit: oklch(0.75 0.18 150); /* Green */ + --action-commit-hover: oklch(0.7 0.2 150); + --action-verify: oklch(0.75 0.18 150); /* Green */ + --action-verify-hover: oklch(0.7 0.2 150); + + /* Running indicator - Blue */ + --running-indicator: oklch(0.7 0.18 230); + --running-indicator-text: oklch(0.75 0.16 230); +} + +/* ======================================== + SYNTHWAVE '84 THEME + Neon dreams of the 80s + ======================================== */ + +/* Theme-specific overrides */ + +.onedark .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #61afef 0%, #c678dd 50%, #61afef 100%); +} + +.onedark .animated-outline-inner { + background: oklch(0.19 0.01 250) !important; + color: #61afef !important; +} + +.onedark [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.25 0.02 250) !important; + color: #c678dd !important; +} + +.onedark .slider-track { + background: oklch(0.25 0.01 250); +} + +.onedark .slider-range { + background: linear-gradient(to right, #61afef, #c678dd); +} + +.onedark .slider-thumb { + background: oklch(0.23 0.01 250); + border-color: #61afef; +} + +.onedark .xml-highlight { + color: oklch(0.85 0.02 240); /* #abb2bf */ +} + +.onedark .xml-tag-bracket { + color: oklch(0.6 0.2 20); /* #e06c75 red */ +} + +.onedark .xml-tag-name { + color: oklch(0.6 0.2 20); /* Red for tags */ +} + +.onedark .xml-attribute-name { + color: oklch(0.8 0.15 80); /* #e5c07b yellow */ +} + +.onedark .xml-attribute-equals { + color: oklch(0.7 0.02 240); /* Dim text */ +} + +.onedark .xml-attribute-value { + color: oklch(0.75 0.18 150); /* #98c379 green */ +} + +.onedark .xml-comment { + color: oklch(0.5 0.03 240); /* #5c6370 */ + font-style: italic; +} + +.onedark .xml-cdata { + color: oklch(0.7 0.15 180); /* #56b6c2 cyan */ +} + +.onedark .xml-doctype { + color: oklch(0.75 0.15 320); /* #c678dd magenta */ +} + +.onedark .xml-text { + color: oklch(0.85 0.02 240); /* Text */ +} diff --git a/apps/ui/src/styles/themes/red.css b/apps/ui/src/styles/themes/red.css new file mode 100644 index 00000000..5e746adb --- /dev/null +++ b/apps/ui/src/styles/themes/red.css @@ -0,0 +1,70 @@ +/* Red Theme */ + +.red { + --background: oklch(0.12 0.03 15); /* Deep dark red-tinted black */ + --background-50: oklch(0.12 0.03 15 / 0.5); + --background-80: oklch(0.12 0.03 15 / 0.8); + + --foreground: oklch(0.95 0.01 15); /* Off-white with warm tint */ + --foreground-secondary: oklch(0.7 0.02 15); + --foreground-muted: oklch(0.5 0.03 15); + + --card: oklch(0.18 0.04 15); /* Slightly lighter dark red */ + --card-foreground: oklch(0.95 0.01 15); + --popover: oklch(0.15 0.035 15); + --popover-foreground: oklch(0.95 0.01 15); + + --primary: oklch(0.55 0.25 25); /* Vibrant crimson red */ + --primary-foreground: oklch(0.98 0 0); + + --brand-400: oklch(0.6 0.23 25); + --brand-500: oklch(0.55 0.25 25); /* Crimson */ + --brand-600: oklch(0.5 0.27 25); + + --secondary: oklch(0.22 0.05 15); + --secondary-foreground: oklch(0.95 0.01 15); + + --muted: oklch(0.22 0.05 15); + --muted-foreground: oklch(0.5 0.03 15); + + --accent: oklch(0.28 0.06 15); + --accent-foreground: oklch(0.95 0.01 15); + + --destructive: oklch(0.6 0.28 30); /* Bright orange-red for destructive */ + + --border: oklch(0.35 0.08 15); + --border-glass: oklch(0.55 0.25 25 / 0.3); + + --input: oklch(0.18 0.04 15); + --ring: oklch(0.55 0.25 25); + + --chart-1: oklch(0.55 0.25 25); /* Crimson */ + --chart-2: oklch(0.7 0.2 50); /* Orange */ + --chart-3: oklch(0.8 0.18 80); /* Gold */ + --chart-4: oklch(0.6 0.22 0); /* Pure red */ + --chart-5: oklch(0.65 0.2 350); /* Pink-red */ + + --sidebar: oklch(0.1 0.025 15); + --sidebar-foreground: oklch(0.95 0.01 15); + --sidebar-primary: oklch(0.55 0.25 25); + --sidebar-primary-foreground: oklch(0.98 0 0); + --sidebar-accent: oklch(0.22 0.05 15); + --sidebar-accent-foreground: oklch(0.95 0.01 15); + --sidebar-border: oklch(0.35 0.08 15); + --sidebar-ring: oklch(0.55 0.25 25); + + /* Action button colors - Red theme */ + --action-view: oklch(0.55 0.25 25); /* Crimson */ + --action-view-hover: oklch(0.5 0.27 25); + --action-followup: oklch(0.7 0.2 50); /* Orange */ + --action-followup-hover: oklch(0.65 0.22 50); + --action-commit: oklch(0.6 0.2 140); /* Green for positive actions */ + --action-commit-hover: oklch(0.55 0.22 140); + --action-verify: oklch(0.6 0.2 140); /* Green */ + --action-verify-hover: oklch(0.55 0.22 140); + + /* Running indicator - Crimson */ + --running-indicator: oklch(0.55 0.25 25); + --running-indicator-text: oklch(0.6 0.23 25); +} + diff --git a/apps/ui/src/styles/themes/retro.css b/apps/ui/src/styles/themes/retro.css new file mode 100644 index 00000000..4c0c8a4c --- /dev/null +++ b/apps/ui/src/styles/themes/retro.css @@ -0,0 +1,227 @@ +/* Retro Theme */ + +.retro { + /* Retro / Cyberpunk Theme */ + --background: oklch(0 0 0); /* Pure Black */ + --background-50: oklch(0 0 0 / 0.5); + --background-80: oklch(0 0 0 / 0.8); + + /* Neon Green Text */ + --foreground: oklch(0.85 0.25 145); /* Neon Green */ + --foreground-secondary: oklch(0.7 0.2 145); + --foreground-muted: oklch(0.5 0.15 145); + + /* Hard Edges */ + --radius: 0px; + + /* UI Elements */ + --card: oklch(0 0 0); /* Black card */ + --card-foreground: oklch(0.85 0.25 145); + --popover: oklch(0.05 0.05 145); + --popover-foreground: oklch(0.85 0.25 145); + + --primary: oklch(0.85 0.25 145); /* Neon Green */ + --primary-foreground: oklch(0 0 0); /* Black text on green */ + + --brand-400: oklch(0.85 0.25 145); + --brand-500: oklch(0.85 0.25 145); + --brand-600: oklch(0.75 0.25 145); + + --secondary: oklch(0.1 0.1 145); /* Dark Green bg */ + --secondary-foreground: oklch(0.85 0.25 145); + + --muted: oklch(0.1 0.05 145); + --muted-foreground: oklch(0.5 0.15 145); + + --accent: oklch(0.2 0.2 145); /* Brighter green accent */ + --accent-foreground: oklch(0.85 0.25 145); + + --destructive: oklch(0.6 0.25 25); /* Keep red for destructive */ + + --border: oklch(0.3 0.15 145); /* Visible Green Border */ + --border-glass: oklch(0.85 0.25 145 / 0.3); + + --input: oklch(0.1 0.1 145); + --ring: oklch(0.85 0.25 145); + + /* Charts - various neons */ + --chart-1: oklch(0.85 0.25 145); /* Green */ + --chart-2: oklch(0.8 0.25 300); /* Purple Neon */ + --chart-3: oklch(0.8 0.25 200); /* Cyan Neon */ + --chart-4: oklch(0.8 0.25 60); /* Yellow Neon */ + --chart-5: oklch(0.8 0.25 20); /* Red Neon */ + + /* Sidebar */ + --sidebar: oklch(0 0 0); + --sidebar-foreground: oklch(0.85 0.25 145); + --sidebar-primary: oklch(0.85 0.25 145); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.1 0.1 145); + --sidebar-accent-foreground: oklch(0.85 0.25 145); + --sidebar-border: oklch(0.3 0.15 145); + --sidebar-ring: oklch(0.85 0.25 145); + + /* Fonts */ + --font-sans: var(--font-geist-mono); /* Force Mono everywhere */ + + /* Action button colors - All green neon for retro theme */ + --action-view: oklch(0.85 0.25 145); /* Neon Green */ + --action-view-hover: oklch(0.9 0.25 145); + --action-followup: oklch(0.85 0.25 145); /* Neon Green */ + --action-followup-hover: oklch(0.9 0.25 145); + --action-commit: oklch(0.85 0.25 145); /* Neon Green */ + --action-commit-hover: oklch(0.9 0.25 145); + --action-verify: oklch(0.85 0.25 145); /* Neon Green */ + --action-verify-hover: oklch(0.9 0.25 145); + + /* Running indicator - Neon Green for retro */ + --running-indicator: oklch(0.85 0.25 145); + --running-indicator-text: oklch(0.85 0.25 145); +} + +/* ======================================== + DRACULA THEME + Inspired by the popular Dracula VS Code theme + ======================================== */ + +/* Theme-specific overrides */ + +.retro .scrollbar-visible::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 0; +} + +.retro .scrollbar-visible::-webkit-scrollbar-track { + background: var(--background); + border-radius: 0; +} + +.retro .scrollbar-styled::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 0; +} + +.retro .scrollbar-styled::-webkit-scrollbar-track { + background: var(--background); + border-radius: 0; +} + +.retro .glass, +.retro .glass-subtle, + +.retro .glass-strong, +.retro .bg-glass, + +.retro .bg-glass-80 { + backdrop-filter: none; + background: var(--background); + border: 1px solid var(--border); +} + +.retro .gradient-brand { + background: var(--primary); + color: var(--primary-foreground); +} + +.retro .content-bg { + background: + linear-gradient(rgba(0, 255, 65, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 255, 65, 0.03) 1px, transparent 1px), + var(--background); + background-size: 20px 20px; +} + +.retro .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #00ff41 0%, #00ffff 25%, #ff00ff 50%, #00ffff 75%, #00ff41 100%); + animation: spin 2s linear infinite, retro-glow 1s ease-in-out infinite alternate; +} + +.retro [data-slot="button"][class*="animated-outline"] { + border-radius: 0 !important; +} + +.retro .animated-outline-inner { + background: oklch(0 0 0) !important; + color: #00ff41 !important; + border-radius: 0 !important; + text-shadow: 0 0 5px #00ff41; + font-family: var(--font-geist-mono), monospace; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.retro [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.1 0.1 145) !important; + color: #00ff41 !important; + box-shadow: + 0 0 10px #00ff41, + 0 0 20px #00ff41, + inset 0 0 10px rgba(0, 255, 65, 0.1); + text-shadow: 0 0 10px #00ff41, 0 0 20px #00ff41; +} + +.retro .slider-track { + background: oklch(0.15 0.05 145); + border: 1px solid #00ff41; + border-radius: 0 !important; +} + +.retro .slider-range { + background: #00ff41; + box-shadow: 0 0 10px #00ff41, 0 0 5px #00ff41; + border-radius: 0 !important; +} + +.retro .slider-thumb { + background: oklch(0 0 0); + border: 2px solid #00ff41; + border-radius: 0 !important; + box-shadow: 0 0 8px #00ff41; +} + +.retro .slider-thumb:hover { + background: oklch(0.1 0.1 145); + box-shadow: 0 0 12px #00ff41, 0 0 20px #00ff41; +} + +.retro .xml-highlight { + color: oklch(0.85 0.25 145); /* Neon green default */ +} + +.retro .xml-tag-bracket { + color: oklch(0.8 0.25 200); /* Cyan for brackets */ +} + +.retro .xml-tag-name { + color: oklch(0.85 0.25 145); /* Bright green for tags */ + text-shadow: 0 0 5px oklch(0.85 0.25 145 / 0.5); +} + +.retro .xml-attribute-name { + color: oklch(0.8 0.25 300); /* Purple neon for attrs */ +} + +.retro .xml-attribute-equals { + color: oklch(0.6 0.15 145); /* Dim green for = */ +} + +.retro .xml-attribute-value { + color: oklch(0.8 0.25 60); /* Yellow neon for strings */ +} + +.retro .xml-comment { + color: oklch(0.5 0.15 145); /* Dim green for comments */ + font-style: italic; +} + +.retro .xml-cdata { + color: oklch(0.75 0.2 200); /* Cyan for CDATA */ +} + +.retro .xml-doctype { + color: oklch(0.75 0.2 300); /* Purple for DOCTYPE */ +} + +.retro .xml-text { + color: oklch(0.7 0.2 145); /* Green text */ +} diff --git a/apps/ui/src/styles/themes/solarized.css b/apps/ui/src/styles/themes/solarized.css new file mode 100644 index 00000000..eb0989ae --- /dev/null +++ b/apps/ui/src/styles/themes/solarized.css @@ -0,0 +1,144 @@ +/* Solarized Theme */ + +.solarized { + --background: oklch(0.2 0.02 230); /* #002b36 base03 */ + --background-50: oklch(0.2 0.02 230 / 0.5); + --background-80: oklch(0.2 0.02 230 / 0.8); + + --foreground: oklch(0.75 0.02 90); /* #839496 base0 */ + --foreground-secondary: oklch(0.6 0.03 200); /* #657b83 base00 */ + --foreground-muted: oklch(0.5 0.04 200); /* #586e75 base01 */ + + --card: oklch(0.23 0.02 230); /* #073642 base02 */ + --card-foreground: oklch(0.75 0.02 90); + --popover: oklch(0.22 0.02 230); + --popover-foreground: oklch(0.75 0.02 90); + + --primary: oklch(0.65 0.15 220); /* #268bd2 blue */ + --primary-foreground: oklch(0.2 0.02 230); + + --brand-400: oklch(0.7 0.15 220); + --brand-500: oklch(0.65 0.15 220); /* #268bd2 */ + --brand-600: oklch(0.6 0.17 220); + + --secondary: oklch(0.25 0.02 230); + --secondary-foreground: oklch(0.75 0.02 90); + + --muted: oklch(0.25 0.02 230); + --muted-foreground: oklch(0.5 0.04 200); + + --accent: oklch(0.28 0.03 230); + --accent-foreground: oklch(0.75 0.02 90); + + --destructive: oklch(0.55 0.2 25); /* #dc322f red */ + + --border: oklch(0.35 0.03 230); + --border-glass: oklch(0.65 0.15 220 / 0.3); + + --input: oklch(0.23 0.02 230); + --ring: oklch(0.65 0.15 220); + + --chart-1: oklch(0.65 0.15 220); /* Blue */ + --chart-2: oklch(0.6 0.18 180); /* Cyan #2aa198 */ + --chart-3: oklch(0.65 0.2 140); /* Green #859900 */ + --chart-4: oklch(0.7 0.18 55); /* Yellow #b58900 */ + --chart-5: oklch(0.6 0.2 30); /* Orange #cb4b16 */ + + --sidebar: oklch(0.18 0.02 230); + --sidebar-foreground: oklch(0.75 0.02 90); + --sidebar-primary: oklch(0.65 0.15 220); + --sidebar-primary-foreground: oklch(0.2 0.02 230); + --sidebar-accent: oklch(0.25 0.02 230); + --sidebar-accent-foreground: oklch(0.75 0.02 90); + --sidebar-border: oklch(0.35 0.03 230); + --sidebar-ring: oklch(0.65 0.15 220); + + /* Action button colors - Solarized blue/cyan theme */ + --action-view: oklch(0.65 0.15 220); /* Blue */ + --action-view-hover: oklch(0.6 0.17 220); + --action-followup: oklch(0.6 0.18 180); /* Cyan */ + --action-followup-hover: oklch(0.55 0.2 180); + --action-commit: oklch(0.65 0.2 140); /* Green */ + --action-commit-hover: oklch(0.6 0.22 140); + --action-verify: oklch(0.65 0.2 140); /* Green */ + --action-verify-hover: oklch(0.6 0.22 140); + + /* Running indicator - Blue */ + --running-indicator: oklch(0.65 0.15 220); + --running-indicator-text: oklch(0.7 0.13 220); +} + +/* ======================================== + GRUVBOX THEME + Retro groove color scheme + ======================================== */ + +/* Theme-specific overrides */ + +.solarized .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #268bd2 0%, #2aa198 50%, #268bd2 100%); +} + +.solarized .animated-outline-inner { + background: oklch(0.2 0.02 230) !important; + color: #268bd2 !important; +} + +.solarized [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.25 0.03 230) !important; + color: #2aa198 !important; +} + +.solarized .slider-track { + background: oklch(0.25 0.02 230); +} + +.solarized .slider-range { + background: linear-gradient(to right, #268bd2, #2aa198); +} + +.solarized .slider-thumb { + background: oklch(0.23 0.02 230); + border-color: #268bd2; +} + +.solarized .xml-highlight { + color: oklch(0.75 0.02 90); /* #839496 */ +} + +.solarized .xml-tag-bracket { + color: oklch(0.65 0.15 220); /* #268bd2 blue */ +} + +.solarized .xml-tag-name { + color: oklch(0.65 0.15 220); /* Blue for tags */ +} + +.solarized .xml-attribute-name { + color: oklch(0.6 0.18 180); /* #2aa198 cyan */ +} + +.solarized .xml-attribute-equals { + color: oklch(0.75 0.02 90); /* Base text */ +} + +.solarized .xml-attribute-value { + color: oklch(0.65 0.2 140); /* #859900 green */ +} + +.solarized .xml-comment { + color: oklch(0.5 0.04 200); /* #586e75 */ + font-style: italic; +} + +.solarized .xml-cdata { + color: oklch(0.6 0.18 180); /* Cyan */ +} + +.solarized .xml-doctype { + color: oklch(0.6 0.2 290); /* #6c71c4 violet */ +} + +.solarized .xml-text { + color: oklch(0.75 0.02 90); /* Base text */ +} diff --git a/apps/ui/src/styles/themes/sunset.css b/apps/ui/src/styles/themes/sunset.css new file mode 100644 index 00000000..7f523f6e --- /dev/null +++ b/apps/ui/src/styles/themes/sunset.css @@ -0,0 +1,111 @@ +/* Sunset Theme */ + +.sunset { + /* Sunset Theme - Mellow oranges and soft purples */ + --background: oklch(0.15 0.02 280); /* Deep twilight blue-purple */ + --background-50: oklch(0.15 0.02 280 / 0.5); + --background-80: oklch(0.15 0.02 280 / 0.8); + + --foreground: oklch(0.95 0.01 80); /* Warm white */ + --foreground-secondary: oklch(0.75 0.02 60); + --foreground-muted: oklch(0.6 0.02 60); + + --card: oklch(0.2 0.025 280); + --card-foreground: oklch(0.95 0.01 80); + --popover: oklch(0.18 0.02 280); + --popover-foreground: oklch(0.95 0.01 80); + + --primary: oklch(0.68 0.18 45); /* Mellow sunset orange */ + --primary-foreground: oklch(0.15 0.02 280); + + --brand-400: oklch(0.72 0.17 45); + --brand-500: oklch(0.68 0.18 45); /* Soft sunset orange */ + --brand-600: oklch(0.64 0.19 42); + + --secondary: oklch(0.25 0.03 280); + --secondary-foreground: oklch(0.95 0.01 80); + + --muted: oklch(0.27 0.03 280); + --muted-foreground: oklch(0.6 0.02 60); + + --accent: oklch(0.35 0.04 310); + --accent-foreground: oklch(0.95 0.01 80); + + --destructive: oklch(0.6 0.2 25); /* Muted red */ + + --border: oklch(0.32 0.04 280); + --border-glass: oklch(0.68 0.18 45 / 0.3); + + --input: oklch(0.2 0.025 280); + --ring: oklch(0.68 0.18 45); + + --chart-1: oklch(0.68 0.18 45); /* Mellow orange */ + --chart-2: oklch(0.75 0.16 340); /* Soft pink sunset */ + --chart-3: oklch(0.78 0.18 70); /* Soft golden */ + --chart-4: oklch(0.66 0.19 42); /* Subtle coral */ + --chart-5: oklch(0.72 0.14 310); /* Pastel purple */ + + --sidebar: oklch(0.13 0.015 280); + --sidebar-foreground: oklch(0.95 0.01 80); + --sidebar-primary: oklch(0.68 0.18 45); + --sidebar-primary-foreground: oklch(0.15 0.02 280); + --sidebar-accent: oklch(0.25 0.03 280); + --sidebar-accent-foreground: oklch(0.95 0.01 80); + --sidebar-border: oklch(0.32 0.04 280); + --sidebar-ring: oklch(0.68 0.18 45); + + /* Action button colors - Mellow sunset palette */ + --action-view: oklch(0.68 0.18 45); /* Mellow orange */ + --action-view-hover: oklch(0.64 0.19 42); + --action-followup: oklch(0.75 0.16 340); /* Soft pink */ + --action-followup-hover: oklch(0.7 0.17 340); + --action-commit: oklch(0.65 0.16 140); /* Soft green */ + --action-commit-hover: oklch(0.6 0.17 140); + --action-verify: oklch(0.65 0.16 140); /* Soft green */ + --action-verify-hover: oklch(0.6 0.17 140); + + /* Running indicator - Mellow orange */ + --running-indicator: oklch(0.68 0.18 45); + --running-indicator-text: oklch(0.72 0.17 45); + + /* Status colors - Sunset theme */ + --status-success: oklch(0.65 0.16 140); + --status-success-bg: oklch(0.65 0.16 140 / 0.2); + --status-warning: oklch(0.78 0.18 70); + --status-warning-bg: oklch(0.78 0.18 70 / 0.2); + --status-error: oklch(0.65 0.2 25); + --status-error-bg: oklch(0.65 0.2 25 / 0.2); + --status-info: oklch(0.75 0.16 340); + --status-info-bg: oklch(0.75 0.16 340 / 0.2); + --status-backlog: oklch(0.65 0.02 280); + --status-in-progress: oklch(0.78 0.18 70); + --status-waiting: oklch(0.72 0.17 60); +} + + +/* Theme-specific overrides */ + +/* Sunset theme scrollbar */ +.sunset ::-webkit-scrollbar-thumb, +.sunset .scrollbar-visible::-webkit-scrollbar-thumb { + background: oklch(0.5 0.14 45); + border-radius: 4px; +} + +.sunset ::-webkit-scrollbar-thumb:hover, +.sunset .scrollbar-visible::-webkit-scrollbar-thumb:hover { + background: oklch(0.58 0.16 45); +} + +.sunset ::-webkit-scrollbar-track, +.sunset .scrollbar-visible::-webkit-scrollbar-track { + background: oklch(0.18 0.03 280); +} + +.sunset .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.5 0.14 45); +} + +.sunset .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.58 0.16 45); +} diff --git a/apps/ui/src/styles/themes/synthwave.css b/apps/ui/src/styles/themes/synthwave.css new file mode 100644 index 00000000..ddb956ba --- /dev/null +++ b/apps/ui/src/styles/themes/synthwave.css @@ -0,0 +1,149 @@ +/* Synthwave Theme */ + +.synthwave { + --background: oklch(0.15 0.05 290); /* #262335 */ + --background-50: oklch(0.15 0.05 290 / 0.5); + --background-80: oklch(0.15 0.05 290 / 0.8); + + --foreground: oklch(0.95 0.02 320); /* #ffffff with warm tint */ + --foreground-secondary: oklch(0.75 0.05 320); + --foreground-muted: oklch(0.55 0.08 290); + + --card: oklch(0.2 0.06 290); /* #34294f */ + --card-foreground: oklch(0.95 0.02 320); + --popover: oklch(0.18 0.05 290); + --popover-foreground: oklch(0.95 0.02 320); + + --primary: oklch(0.7 0.28 350); /* #f97e72 hot pink */ + --primary-foreground: oklch(0.15 0.05 290); + + --brand-400: oklch(0.75 0.28 350); + --brand-500: oklch(0.7 0.28 350); /* Hot pink */ + --brand-600: oklch(0.65 0.3 350); + + --secondary: oklch(0.25 0.07 290); + --secondary-foreground: oklch(0.95 0.02 320); + + --muted: oklch(0.25 0.07 290); + --muted-foreground: oklch(0.55 0.08 290); + + --accent: oklch(0.3 0.08 290); + --accent-foreground: oklch(0.95 0.02 320); + + --destructive: oklch(0.6 0.25 15); + + --border: oklch(0.4 0.1 290); + --border-glass: oklch(0.7 0.28 350 / 0.3); + + --input: oklch(0.2 0.06 290); + --ring: oklch(0.7 0.28 350); + + --chart-1: oklch(0.7 0.28 350); /* Hot pink */ + --chart-2: oklch(0.8 0.25 200); /* Cyan #72f1b8 */ + --chart-3: oklch(0.85 0.2 60); /* Yellow #fede5d */ + --chart-4: oklch(0.7 0.25 280); /* Purple #ff7edb */ + --chart-5: oklch(0.7 0.2 30); /* Orange #f97e72 */ + + --sidebar: oklch(0.13 0.05 290); + --sidebar-foreground: oklch(0.95 0.02 320); + --sidebar-primary: oklch(0.7 0.28 350); + --sidebar-primary-foreground: oklch(0.15 0.05 290); + --sidebar-accent: oklch(0.25 0.07 290); + --sidebar-accent-foreground: oklch(0.95 0.02 320); + --sidebar-border: oklch(0.4 0.1 290); + --sidebar-ring: oklch(0.7 0.28 350); + + /* Action button colors - Synthwave hot pink/cyan theme */ + --action-view: oklch(0.7 0.28 350); /* Hot pink */ + --action-view-hover: oklch(0.65 0.3 350); + --action-followup: oklch(0.8 0.25 200); /* Cyan */ + --action-followup-hover: oklch(0.75 0.27 200); + --action-commit: oklch(0.85 0.2 60); /* Yellow */ + --action-commit-hover: oklch(0.8 0.22 60); + --action-verify: oklch(0.85 0.2 60); /* Yellow */ + --action-verify-hover: oklch(0.8 0.22 60); + + /* Running indicator - Hot pink */ + --running-indicator: oklch(0.7 0.28 350); + --running-indicator-text: oklch(0.75 0.26 350); +} + +/* Red Theme - Bold crimson/red aesthetic */ + +/* Theme-specific overrides */ + +.synthwave .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #f97e72 0%, #72f1b8 25%, #ff7edb 50%, #72f1b8 75%, #f97e72 100%); + animation: spin 2s linear infinite, synthwave-glow 1.5s ease-in-out infinite alternate; +} + +.synthwave .animated-outline-inner { + background: oklch(0.15 0.05 290) !important; + color: #f97e72 !important; + text-shadow: 0 0 8px #f97e72; +} + +.synthwave [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.22 0.07 290) !important; + color: #72f1b8 !important; + text-shadow: 0 0 12px #72f1b8; + box-shadow: 0 0 15px rgba(114, 241, 184, 0.3); +} + +.synthwave .slider-track { + background: oklch(0.25 0.07 290); +} + +.synthwave .slider-range { + background: linear-gradient(to right, #f97e72, #ff7edb); + box-shadow: 0 0 10px #f97e72, 0 0 5px #ff7edb; +} + +.synthwave .slider-thumb { + background: oklch(0.2 0.06 290); + border-color: #f97e72; + box-shadow: 0 0 8px #f97e72; +} + +.synthwave .xml-highlight { + color: oklch(0.95 0.02 320); /* Warm white */ +} + +.synthwave .xml-tag-bracket { + color: oklch(0.7 0.28 350); /* #f97e72 hot pink */ +} + +.synthwave .xml-tag-name { + color: oklch(0.7 0.28 350); /* Hot pink */ + text-shadow: 0 0 8px oklch(0.7 0.28 350 / 0.5); +} + +.synthwave .xml-attribute-name { + color: oklch(0.7 0.25 280); /* #ff7edb purple */ +} + +.synthwave .xml-attribute-equals { + color: oklch(0.8 0.02 320); /* White-ish */ +} + +.synthwave .xml-attribute-value { + color: oklch(0.85 0.2 60); /* #fede5d yellow */ + text-shadow: 0 0 5px oklch(0.85 0.2 60 / 0.3); +} + +.synthwave .xml-comment { + color: oklch(0.55 0.08 290); /* Dim purple */ + font-style: italic; +} + +.synthwave .xml-cdata { + color: oklch(0.8 0.25 200); /* #72f1b8 cyan */ +} + +.synthwave .xml-doctype { + color: oklch(0.8 0.25 200); /* Cyan */ +} + +.synthwave .xml-text { + color: oklch(0.95 0.02 320); /* White */ +} diff --git a/apps/ui/src/styles/themes/tokyonight.css b/apps/ui/src/styles/themes/tokyonight.css new file mode 100644 index 00000000..8bc907b7 --- /dev/null +++ b/apps/ui/src/styles/themes/tokyonight.css @@ -0,0 +1,144 @@ +/* Tokyonight Theme */ + +.tokyonight { + --background: oklch(0.16 0.03 260); /* #1a1b26 */ + --background-50: oklch(0.16 0.03 260 / 0.5); + --background-80: oklch(0.16 0.03 260 / 0.8); + + --foreground: oklch(0.85 0.02 250); /* #a9b1d6 */ + --foreground-secondary: oklch(0.7 0.03 250); + --foreground-muted: oklch(0.5 0.04 250); /* #565f89 */ + + --card: oklch(0.2 0.03 260); /* #24283b */ + --card-foreground: oklch(0.85 0.02 250); + --popover: oklch(0.18 0.03 260); + --popover-foreground: oklch(0.85 0.02 250); + + --primary: oklch(0.7 0.18 280); /* #7aa2f7 blue */ + --primary-foreground: oklch(0.16 0.03 260); + + --brand-400: oklch(0.75 0.18 280); + --brand-500: oklch(0.7 0.18 280); /* #7aa2f7 */ + --brand-600: oklch(0.65 0.2 280); /* #7dcfff */ + + --secondary: oklch(0.24 0.03 260); /* #292e42 */ + --secondary-foreground: oklch(0.85 0.02 250); + + --muted: oklch(0.24 0.03 260); + --muted-foreground: oklch(0.5 0.04 250); + + --accent: oklch(0.28 0.04 260); + --accent-foreground: oklch(0.85 0.02 250); + + --destructive: oklch(0.65 0.2 15); /* #f7768e */ + + --border: oklch(0.32 0.04 260); + --border-glass: oklch(0.7 0.18 280 / 0.3); + + --input: oklch(0.2 0.03 260); + --ring: oklch(0.7 0.18 280); + + --chart-1: oklch(0.7 0.18 280); /* Blue #7aa2f7 */ + --chart-2: oklch(0.75 0.18 200); /* Cyan #7dcfff */ + --chart-3: oklch(0.75 0.18 140); /* Green #9ece6a */ + --chart-4: oklch(0.7 0.2 320); /* Magenta #bb9af7 */ + --chart-5: oklch(0.8 0.18 70); /* Yellow #e0af68 */ + + --sidebar: oklch(0.14 0.03 260); + --sidebar-foreground: oklch(0.85 0.02 250); + --sidebar-primary: oklch(0.7 0.18 280); + --sidebar-primary-foreground: oklch(0.16 0.03 260); + --sidebar-accent: oklch(0.24 0.03 260); + --sidebar-accent-foreground: oklch(0.85 0.02 250); + --sidebar-border: oklch(0.32 0.04 260); + --sidebar-ring: oklch(0.7 0.18 280); + + /* Action button colors - Tokyo Night blue/magenta theme */ + --action-view: oklch(0.7 0.18 280); /* Blue */ + --action-view-hover: oklch(0.65 0.2 280); + --action-followup: oklch(0.75 0.18 200); /* Cyan */ + --action-followup-hover: oklch(0.7 0.2 200); + --action-commit: oklch(0.75 0.18 140); /* Green */ + --action-commit-hover: oklch(0.7 0.2 140); + --action-verify: oklch(0.75 0.18 140); /* Green */ + --action-verify-hover: oklch(0.7 0.2 140); + + /* Running indicator - Blue */ + --running-indicator: oklch(0.7 0.18 280); + --running-indicator-text: oklch(0.75 0.16 280); +} + +/* ======================================== + SOLARIZED DARK THEME + The classic color scheme by Ethan Schoonover + ======================================== */ + +/* Theme-specific overrides */ + +.tokyonight .animated-outline-gradient { + background: conic-gradient(from 90deg at 50% 50%, #7aa2f7 0%, #bb9af7 50%, #7aa2f7 100%); +} + +.tokyonight .animated-outline-inner { + background: oklch(0.16 0.03 260) !important; + color: #7aa2f7 !important; +} + +.tokyonight [data-slot="button"][class*="animated-outline"]:hover .animated-outline-inner { + background: oklch(0.22 0.04 260) !important; + color: #bb9af7 !important; +} + +.tokyonight .slider-track { + background: oklch(0.24 0.03 260); +} + +.tokyonight .slider-range { + background: linear-gradient(to right, #7aa2f7, #bb9af7); +} + +.tokyonight .slider-thumb { + background: oklch(0.2 0.03 260); + border-color: #7aa2f7; +} + +.tokyonight .xml-highlight { + color: oklch(0.85 0.02 250); /* #a9b1d6 */ +} + +.tokyonight .xml-tag-bracket { + color: oklch(0.65 0.2 15); /* #f7768e red */ +} + +.tokyonight .xml-tag-name { + color: oklch(0.65 0.2 15); /* Red for tags */ +} + +.tokyonight .xml-attribute-name { + color: oklch(0.7 0.2 320); /* #bb9af7 purple */ +} + +.tokyonight .xml-attribute-equals { + color: oklch(0.75 0.02 250); /* Dim text */ +} + +.tokyonight .xml-attribute-value { + color: oklch(0.75 0.18 140); /* #9ece6a green */ +} + +.tokyonight .xml-comment { + color: oklch(0.5 0.04 250); /* #565f89 */ + font-style: italic; +} + +.tokyonight .xml-cdata { + color: oklch(0.75 0.18 200); /* #7dcfff cyan */ +} + +.tokyonight .xml-doctype { + color: oklch(0.7 0.18 280); /* #7aa2f7 blue */ +} + +.tokyonight .xml-text { + color: oklch(0.85 0.02 250); /* Text color */ +} From 1a78304ca2ee36164d6ba5e2457f6ecf27cc0e06 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sat, 20 Dec 2025 01:52:49 -0500 Subject: [PATCH 2/4] Refactor SetupView component for improved readability - Consolidate destructuring of useSetupStore into a single line for cleaner code. - Remove unnecessary blank line at the beginning of the file. --- apps/ui/src/components/views/setup-view.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/ui/src/components/views/setup-view.tsx b/apps/ui/src/components/views/setup-view.tsx index f0546839..5f1452e6 100644 --- a/apps/ui/src/components/views/setup-view.tsx +++ b/apps/ui/src/components/views/setup-view.tsx @@ -1,4 +1,3 @@ - import { useSetupStore } from "@/store/setup-store"; import { StepIndicator } from "./setup-view/components"; import { @@ -12,12 +11,8 @@ import { useNavigate } from "@tanstack/react-router"; // Main Setup View export function SetupView() { - const { - currentStep, - setCurrentStep, - completeSetup, - setSkipClaudeSetup, - } = useSetupStore(); + const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup } = + useSetupStore(); const navigate = useNavigate(); const steps = ["welcome", "theme", "claude", "github", "complete"] as const; From ace736c7c214cfd831fd389e5e999a3df9e16234 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sat, 20 Dec 2025 02:08:13 -0500 Subject: [PATCH 3/4] Update README and enhance Electron app initialization - Update the link in the README for the Agentic Jumpstart course to include a GitHub-specific query parameter. - Ensure consistent userData path across development and production environments in the Electron app, with error handling for path setting. - Improve the isElectron function to check for Electron context more robustly. --- README.md | 2 +- apps/ui/src/lib/electron.ts | 10 +++++++++- apps/ui/src/main.ts | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 39c31d4b..b65ccd63 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ > > Automaker itself was built by a group of engineers using AI and agentic coding techniques to build features faster than ever. By leveraging tools like Cursor IDE and Claude Code CLI, the team orchestrated AI agents to implement complex functionality in days instead of weeks. > -> **Learn how:** Master these same techniques and workflows in the [Agentic Jumpstart course](https://agenticjumpstart.com/?utm=automaker). +> **Learn how:** Master these same techniques and workflows in the [Agentic Jumpstart course](https://agenticjumpstart.com/?utm=automaker-gh). # Automaker diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 83ba64f3..0c170d39 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -508,7 +508,15 @@ const mockFileSystem: Record = {}; // Check if we're in Electron (for UI indicators only) export const isElectron = (): boolean => { - return typeof window !== "undefined" && window.isElectron === true; + if (typeof window === "undefined") { + return false; + } + + if ((window as any).isElectron === true) { + return true; + } + + return window.electronAPI?.isElectron === true; }; // Check if backend server is available diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index 4d84ffb7..f2157806 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -304,6 +304,20 @@ function createWindow(): void { // App lifecycle app.whenReady().then(async () => { + // Ensure userData path is consistent across dev/prod so files land in Automaker dir + try { + const desiredUserDataPath = path.join(app.getPath("appData"), "Automaker"); + if (app.getPath("userData") !== desiredUserDataPath) { + app.setPath("userData", desiredUserDataPath); + console.log("[Electron] userData path set to:", desiredUserDataPath); + } + } catch (error) { + console.warn( + "[Electron] Failed to set userData path:", + (error as Error).message + ); + } + if (process.platform === "darwin" && app.dock) { const iconPath = getIconPath(); if (iconPath) { From c76ba691a47836db4bdf7f227d5505e1132a6bf5 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sat, 20 Dec 2025 09:03:32 -0500 Subject: [PATCH 4/4] Enhance unit tests for settings service and error handling - Add comprehensive unit tests for SettingsService, covering global and project settings management, including creation, updates, and merging with defaults. - Implement tests for handling credentials, ensuring proper masking and merging of API keys. - Introduce tests for migration from localStorage, validating successful data transfer and error handling. - Enhance error handling in subprocess management tests, ensuring robust timeout and output reading scenarios. --- .../tests/unit/lib/automaker-paths.test.ts | 91 +++ .../tests/unit/lib/error-handler.test.ts | 65 ++ .../server/tests/unit/lib/sdk-options.test.ts | 94 +++ apps/server/tests/unit/lib/security.test.ts | 16 + .../tests/unit/lib/subprocess-manager.test.ts | 63 +- .../tests/unit/lib/worktree-metadata.test.ts | 26 + .../unit/providers/claude-provider.test.ts | 24 + .../unit/services/settings-service.test.ts | 643 ++++++++++++++++++ 8 files changed, 1019 insertions(+), 3 deletions(-) create mode 100644 apps/server/tests/unit/services/settings-service.test.ts diff --git a/apps/server/tests/unit/lib/automaker-paths.test.ts b/apps/server/tests/unit/lib/automaker-paths.test.ts index 10797eb8..5dcfd5cc 100644 --- a/apps/server/tests/unit/lib/automaker-paths.test.ts +++ b/apps/server/tests/unit/lib/automaker-paths.test.ts @@ -13,6 +13,10 @@ import { getAppSpecPath, getBranchTrackingPath, ensureAutomakerDir, + getGlobalSettingsPath, + getCredentialsPath, + getProjectSettingsPath, + ensureDataDir, } from "@/lib/automaker-paths.js"; describe("automaker-paths.ts", () => { @@ -136,4 +140,91 @@ describe("automaker-paths.ts", () => { expect(result).toBe(automakerDir); }); }); + + describe("getGlobalSettingsPath", () => { + it("should return path to settings.json in data directory", () => { + const dataDir = "/test/data"; + const result = getGlobalSettingsPath(dataDir); + expect(result).toBe(path.join(dataDir, "settings.json")); + }); + + it("should handle paths with trailing slashes", () => { + const dataDir = "/test/data" + path.sep; + const result = getGlobalSettingsPath(dataDir); + expect(result).toBe(path.join(dataDir, "settings.json")); + }); + }); + + describe("getCredentialsPath", () => { + it("should return path to credentials.json in data directory", () => { + const dataDir = "/test/data"; + const result = getCredentialsPath(dataDir); + expect(result).toBe(path.join(dataDir, "credentials.json")); + }); + + it("should handle paths with trailing slashes", () => { + const dataDir = "/test/data" + path.sep; + const result = getCredentialsPath(dataDir); + expect(result).toBe(path.join(dataDir, "credentials.json")); + }); + }); + + describe("getProjectSettingsPath", () => { + it("should return path to settings.json in project .automaker directory", () => { + const projectPath = "/test/project"; + const result = getProjectSettingsPath(projectPath); + expect(result).toBe( + path.join(projectPath, ".automaker", "settings.json") + ); + }); + + it("should handle paths with trailing slashes", () => { + const projectPath = "/test/project" + path.sep; + const result = getProjectSettingsPath(projectPath); + expect(result).toBe( + path.join(projectPath, ".automaker", "settings.json") + ); + }); + }); + + describe("ensureDataDir", () => { + let testDir: string; + + beforeEach(async () => { + testDir = path.join(os.tmpdir(), `data-dir-test-${Date.now()}`); + }); + + afterEach(async () => { + try { + await fs.rm(testDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }); + + it("should create data directory and return path", async () => { + const result = await ensureDataDir(testDir); + + expect(result).toBe(testDir); + const stats = await fs.stat(testDir); + expect(stats.isDirectory()).toBe(true); + }); + + it("should succeed if directory already exists", async () => { + await fs.mkdir(testDir, { recursive: true }); + + const result = await ensureDataDir(testDir); + + expect(result).toBe(testDir); + }); + + it("should create nested directories", async () => { + const nestedDir = path.join(testDir, "nested", "deep"); + const result = await ensureDataDir(nestedDir); + + expect(result).toBe(nestedDir); + const stats = await fs.stat(nestedDir); + expect(stats.isDirectory()).toBe(true); + }); + }); }); diff --git a/apps/server/tests/unit/lib/error-handler.test.ts b/apps/server/tests/unit/lib/error-handler.test.ts index d479de87..cbf5132b 100644 --- a/apps/server/tests/unit/lib/error-handler.test.ts +++ b/apps/server/tests/unit/lib/error-handler.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { isAbortError, isAuthenticationError, + isCancellationError, classifyError, getUserFriendlyErrorMessage, type ErrorType, @@ -32,6 +33,34 @@ describe("error-handler.ts", () => { }); }); + describe("isCancellationError", () => { + it("should detect 'cancelled' message", () => { + expect(isCancellationError("Operation was cancelled")).toBe(true); + }); + + it("should detect 'canceled' message", () => { + expect(isCancellationError("Request was canceled")).toBe(true); + }); + + it("should detect 'stopped' message", () => { + expect(isCancellationError("Process was stopped")).toBe(true); + }); + + it("should detect 'aborted' message", () => { + expect(isCancellationError("Task was aborted")).toBe(true); + }); + + it("should be case insensitive", () => { + expect(isCancellationError("CANCELLED")).toBe(true); + expect(isCancellationError("Canceled")).toBe(true); + }); + + it("should return false for non-cancellation errors", () => { + expect(isCancellationError("File not found")).toBe(false); + expect(isCancellationError("Network error")).toBe(false); + }); + }); + describe("isAuthenticationError", () => { it("should detect 'Authentication failed' message", () => { expect(isAuthenticationError("Authentication failed")).toBe(true); @@ -91,6 +120,42 @@ describe("error-handler.ts", () => { expect(result.isAbort).toBe(true); // Still detected as abort too }); + it("should classify cancellation errors", () => { + const error = new Error("Operation was cancelled"); + const result = classifyError(error); + + expect(result.type).toBe("cancellation"); + expect(result.isCancellation).toBe(true); + expect(result.isAbort).toBe(false); + expect(result.isAuth).toBe(false); + }); + + it("should prioritize abort over cancellation if both match", () => { + const error = new Error("Operation aborted"); + error.name = "AbortError"; + const result = classifyError(error); + + expect(result.type).toBe("abort"); + expect(result.isAbort).toBe(true); + expect(result.isCancellation).toBe(true); // Still detected as cancellation too + }); + + it("should classify cancellation errors with 'canceled' spelling", () => { + const error = new Error("Request was canceled"); + const result = classifyError(error); + + expect(result.type).toBe("cancellation"); + expect(result.isCancellation).toBe(true); + }); + + it("should classify cancellation errors with 'stopped' message", () => { + const error = new Error("Process was stopped"); + const result = classifyError(error); + + expect(result.type).toBe("cancellation"); + expect(result.isCancellation).toBe(true); + }); + it("should classify generic Error as execution error", () => { const error = new Error("Something went wrong"); const result = classifyError(error); diff --git a/apps/server/tests/unit/lib/sdk-options.test.ts b/apps/server/tests/unit/lib/sdk-options.test.ts index dc802178..0a95312e 100644 --- a/apps/server/tests/unit/lib/sdk-options.test.ts +++ b/apps/server/tests/unit/lib/sdk-options.test.ts @@ -144,6 +144,40 @@ describe("sdk-options.ts", () => { expect(options.maxTurns).toBe(MAX_TURNS.extended); expect(options.allowedTools).toEqual([...TOOL_PRESETS.readOnly]); }); + + it("should include systemPrompt when provided", async () => { + const { createSuggestionsOptions } = await import("@/lib/sdk-options.js"); + + const options = createSuggestionsOptions({ + cwd: "/test/path", + systemPrompt: "Custom prompt", + }); + + expect(options.systemPrompt).toBe("Custom prompt"); + }); + + it("should include abortController when provided", async () => { + const { createSuggestionsOptions } = await import("@/lib/sdk-options.js"); + + const abortController = new AbortController(); + const options = createSuggestionsOptions({ + cwd: "/test/path", + abortController, + }); + + expect(options.abortController).toBe(abortController); + }); + + it("should include outputFormat when provided", async () => { + const { createSuggestionsOptions } = await import("@/lib/sdk-options.js"); + + const options = createSuggestionsOptions({ + cwd: "/test/path", + outputFormat: { type: "json" }, + }); + + expect(options.outputFormat).toEqual({ type: "json" }); + }); }); describe("createChatOptions", () => { @@ -205,6 +239,29 @@ describe("sdk-options.ts", () => { autoAllowBashIfSandboxed: true, }); }); + + it("should include systemPrompt when provided", async () => { + const { createAutoModeOptions } = await import("@/lib/sdk-options.js"); + + const options = createAutoModeOptions({ + cwd: "/test/path", + systemPrompt: "Custom prompt", + }); + + expect(options.systemPrompt).toBe("Custom prompt"); + }); + + it("should include abortController when provided", async () => { + const { createAutoModeOptions } = await import("@/lib/sdk-options.js"); + + const abortController = new AbortController(); + const options = createAutoModeOptions({ + cwd: "/test/path", + abortController, + }); + + expect(options.abortController).toBe(abortController); + }); }); describe("createCustomOptions", () => { @@ -234,5 +291,42 @@ describe("sdk-options.ts", () => { expect(options.maxTurns).toBe(MAX_TURNS.maximum); expect(options.allowedTools).toEqual([...TOOL_PRESETS.readOnly]); }); + + it("should include sandbox when provided", async () => { + const { createCustomOptions } = await import("@/lib/sdk-options.js"); + + const options = createCustomOptions({ + cwd: "/test/path", + sandbox: { enabled: true, autoAllowBashIfSandboxed: false }, + }); + + expect(options.sandbox).toEqual({ + enabled: true, + autoAllowBashIfSandboxed: false, + }); + }); + + it("should include systemPrompt when provided", async () => { + const { createCustomOptions } = await import("@/lib/sdk-options.js"); + + const options = createCustomOptions({ + cwd: "/test/path", + systemPrompt: "Custom prompt", + }); + + expect(options.systemPrompt).toBe("Custom prompt"); + }); + + it("should include abortController when provided", async () => { + const { createCustomOptions } = await import("@/lib/sdk-options.js"); + + const abortController = new AbortController(); + const options = createCustomOptions({ + cwd: "/test/path", + abortController, + }); + + expect(options.abortController).toBe(abortController); + }); }); }); diff --git a/apps/server/tests/unit/lib/security.test.ts b/apps/server/tests/unit/lib/security.test.ts index b078ca2f..c4d63add 100644 --- a/apps/server/tests/unit/lib/security.test.ts +++ b/apps/server/tests/unit/lib/security.test.ts @@ -53,9 +53,24 @@ describe("security.ts", () => { expect(allowed).toContain(path.resolve("/data/dir")); }); + it("should include WORKSPACE_DIR if set", async () => { + process.env.ALLOWED_PROJECT_DIRS = ""; + process.env.DATA_DIR = ""; + process.env.WORKSPACE_DIR = "/workspace/dir"; + + const { initAllowedPaths, getAllowedPaths } = await import( + "@/lib/security.js" + ); + initAllowedPaths(); + + const allowed = getAllowedPaths(); + expect(allowed).toContain(path.resolve("/workspace/dir")); + }); + it("should handle empty ALLOWED_PROJECT_DIRS", async () => { process.env.ALLOWED_PROJECT_DIRS = ""; process.env.DATA_DIR = "/data"; + delete process.env.WORKSPACE_DIR; const { initAllowedPaths, getAllowedPaths } = await import( "@/lib/security.js" @@ -70,6 +85,7 @@ describe("security.ts", () => { it("should skip empty entries in comma list", async () => { process.env.ALLOWED_PROJECT_DIRS = "/path1,,/path2, ,/path3"; process.env.DATA_DIR = ""; + delete process.env.WORKSPACE_DIR; const { initAllowedPaths, getAllowedPaths } = await import( "@/lib/security.js" diff --git a/apps/server/tests/unit/lib/subprocess-manager.test.ts b/apps/server/tests/unit/lib/subprocess-manager.test.ts index 9ca39671..34bfd19a 100644 --- a/apps/server/tests/unit/lib/subprocess-manager.test.ts +++ b/apps/server/tests/unit/lib/subprocess-manager.test.ts @@ -264,9 +264,66 @@ describe("subprocess-manager.ts", () => { ); }); - // Note: Timeout behavior tests are omitted from unit tests as they involve - // complex timing interactions that are difficult to mock reliably. - // These scenarios are better covered by integration tests with real subprocesses. + // Note: Timeout behavior is difficult to test reliably with mocks due to + // timing interactions. The timeout functionality is covered by integration tests. + // The error handling path (lines 117-118) is tested below. + + it("should reset timeout when output is received", async () => { + vi.useFakeTimers(); + const mockProcess = createMockProcess({ + stdoutLines: [ + '{"type":"first"}', + '{"type":"second"}', + '{"type":"third"}', + ], + exitCode: 0, + delayMs: 50, + }); + + vi.mocked(cp.spawn).mockReturnValue(mockProcess); + + const generator = spawnJSONLProcess({ + ...baseOptions, + timeout: 200, + }); + + const promise = collectAsyncGenerator(generator); + + // Advance time but not enough to trigger timeout + await vi.advanceTimersByTimeAsync(150); + // Process should not be killed yet + expect(mockProcess.kill).not.toHaveBeenCalled(); + + vi.useRealTimers(); + await promise; + }); + + it("should handle errors when reading stdout", async () => { + const mockProcess = new EventEmitter() as any; + const stdout = new Readable({ + read() { + // Emit an error after a short delay + setTimeout(() => { + this.emit("error", new Error("Read error")); + }, 10); + }, + }); + const stderr = new Readable({ read() {} }); + + mockProcess.stdout = stdout; + mockProcess.stderr = stderr; + mockProcess.kill = vi.fn(); + + vi.mocked(cp.spawn).mockReturnValue(mockProcess); + + const generator = spawnJSONLProcess(baseOptions); + + await expect(collectAsyncGenerator(generator)).rejects.toThrow("Read error"); + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringContaining("Error reading stdout"), + expect.any(Error) + ); + }); it("should spawn process with correct arguments", async () => { const mockProcess = createMockProcess({ exitCode: 0 }); diff --git a/apps/server/tests/unit/lib/worktree-metadata.test.ts b/apps/server/tests/unit/lib/worktree-metadata.test.ts index 0071f207..82f3242b 100644 --- a/apps/server/tests/unit/lib/worktree-metadata.test.ts +++ b/apps/server/tests/unit/lib/worktree-metadata.test.ts @@ -66,6 +66,32 @@ describe("worktree-metadata.ts", () => { const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); + + it("should handle empty branch name", async () => { + const branch = ""; + const metadata: WorktreeMetadata = { + branch: "branch", + createdAt: new Date().toISOString(), + }; + + // Empty branch name should be sanitized to "_branch" + await writeWorktreeMetadata(testProjectPath, branch, metadata); + const result = await readWorktreeMetadata(testProjectPath, branch); + expect(result).toEqual(metadata); + }); + + it("should handle branch name that becomes empty after sanitization", async () => { + // Test branch that would become empty after removing invalid chars + const branch = "///"; + const metadata: WorktreeMetadata = { + branch: "branch", + createdAt: new Date().toISOString(), + }; + + await writeWorktreeMetadata(testProjectPath, branch, metadata); + const result = await readWorktreeMetadata(testProjectPath, branch); + expect(result).toEqual(metadata); + }); }); describe("readWorktreeMetadata", () => { diff --git a/apps/server/tests/unit/providers/claude-provider.test.ts b/apps/server/tests/unit/providers/claude-provider.test.ts index 6ffd2ea2..41c5bf71 100644 --- a/apps/server/tests/unit/providers/claude-provider.test.ts +++ b/apps/server/tests/unit/providers/claude-provider.test.ts @@ -234,6 +234,30 @@ describe("claude-provider.ts", () => { }), }); }); + + it("should handle errors during execution and rethrow", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const testError = new Error("SDK execution failed"); + + vi.mocked(sdk.query).mockReturnValue( + (async function* () { + throw testError; + })() + ); + + const generator = provider.executeQuery({ + prompt: "Test", + cwd: "/test", + }); + + await expect(collectAsyncGenerator(generator)).rejects.toThrow("SDK execution failed"); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[ClaudeProvider] executeQuery() error during execution:", + testError + ); + + consoleErrorSpy.mockRestore(); + }); }); describe("detectInstallation", () => { diff --git a/apps/server/tests/unit/services/settings-service.test.ts b/apps/server/tests/unit/services/settings-service.test.ts new file mode 100644 index 00000000..bed7d3e6 --- /dev/null +++ b/apps/server/tests/unit/services/settings-service.test.ts @@ -0,0 +1,643 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import { SettingsService } from "@/services/settings-service.js"; +import { + DEFAULT_GLOBAL_SETTINGS, + DEFAULT_CREDENTIALS, + DEFAULT_PROJECT_SETTINGS, + SETTINGS_VERSION, + CREDENTIALS_VERSION, + PROJECT_SETTINGS_VERSION, + type GlobalSettings, + type Credentials, + type ProjectSettings, +} from "@/types/settings.js"; + +describe("settings-service.ts", () => { + let testDataDir: string; + let testProjectDir: string; + let settingsService: SettingsService; + + beforeEach(async () => { + testDataDir = path.join(os.tmpdir(), `settings-test-${Date.now()}`); + testProjectDir = path.join(os.tmpdir(), `project-test-${Date.now()}`); + await fs.mkdir(testDataDir, { recursive: true }); + await fs.mkdir(testProjectDir, { recursive: true }); + settingsService = new SettingsService(testDataDir); + }); + + afterEach(async () => { + try { + await fs.rm(testDataDir, { recursive: true, force: true }); + await fs.rm(testProjectDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }); + + describe("getGlobalSettings", () => { + it("should return default settings when file does not exist", async () => { + const settings = await settingsService.getGlobalSettings(); + expect(settings).toEqual(DEFAULT_GLOBAL_SETTINGS); + }); + + it("should read and return existing settings", async () => { + const customSettings: GlobalSettings = { + ...DEFAULT_GLOBAL_SETTINGS, + theme: "light", + sidebarOpen: false, + maxConcurrency: 5, + }; + const settingsPath = path.join(testDataDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2)); + + const settings = await settingsService.getGlobalSettings(); + expect(settings.theme).toBe("light"); + expect(settings.sidebarOpen).toBe(false); + expect(settings.maxConcurrency).toBe(5); + }); + + it("should merge with defaults for missing properties", async () => { + const partialSettings = { + version: SETTINGS_VERSION, + theme: "dark", + }; + const settingsPath = path.join(testDataDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2)); + + const settings = await settingsService.getGlobalSettings(); + expect(settings.theme).toBe("dark"); + expect(settings.sidebarOpen).toBe(DEFAULT_GLOBAL_SETTINGS.sidebarOpen); + expect(settings.maxConcurrency).toBe(DEFAULT_GLOBAL_SETTINGS.maxConcurrency); + }); + + it("should merge keyboard shortcuts deeply", async () => { + const customSettings: GlobalSettings = { + ...DEFAULT_GLOBAL_SETTINGS, + keyboardShortcuts: { + ...DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts, + board: "B", + }, + }; + const settingsPath = path.join(testDataDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2)); + + const settings = await settingsService.getGlobalSettings(); + expect(settings.keyboardShortcuts.board).toBe("B"); + expect(settings.keyboardShortcuts.agent).toBe( + DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent + ); + }); + }); + + describe("updateGlobalSettings", () => { + it("should create settings file with updates", async () => { + const updates: Partial = { + theme: "light", + sidebarOpen: false, + }; + + const updated = await settingsService.updateGlobalSettings(updates); + + expect(updated.theme).toBe("light"); + expect(updated.sidebarOpen).toBe(false); + expect(updated.version).toBe(SETTINGS_VERSION); + + const settingsPath = path.join(testDataDir, "settings.json"); + const fileContent = await fs.readFile(settingsPath, "utf-8"); + const saved = JSON.parse(fileContent); + expect(saved.theme).toBe("light"); + expect(saved.sidebarOpen).toBe(false); + }); + + it("should merge updates with existing settings", async () => { + const initial: GlobalSettings = { + ...DEFAULT_GLOBAL_SETTINGS, + theme: "dark", + maxConcurrency: 3, + }; + const settingsPath = path.join(testDataDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2)); + + const updates: Partial = { + theme: "light", + }; + + const updated = await settingsService.updateGlobalSettings(updates); + + expect(updated.theme).toBe("light"); + expect(updated.maxConcurrency).toBe(3); // Preserved from initial + }); + + it("should deep merge keyboard shortcuts", async () => { + const updates: Partial = { + keyboardShortcuts: { + board: "B", + }, + }; + + const updated = await settingsService.updateGlobalSettings(updates); + + expect(updated.keyboardShortcuts.board).toBe("B"); + expect(updated.keyboardShortcuts.agent).toBe( + DEFAULT_GLOBAL_SETTINGS.keyboardShortcuts.agent + ); + }); + + it("should create data directory if it does not exist", async () => { + const newDataDir = path.join(os.tmpdir(), `new-data-dir-${Date.now()}`); + const newService = new SettingsService(newDataDir); + + await newService.updateGlobalSettings({ theme: "light" }); + + const stats = await fs.stat(newDataDir); + expect(stats.isDirectory()).toBe(true); + + await fs.rm(newDataDir, { recursive: true, force: true }); + }); + }); + + describe("hasGlobalSettings", () => { + it("should return false when settings file does not exist", async () => { + const exists = await settingsService.hasGlobalSettings(); + expect(exists).toBe(false); + }); + + it("should return true when settings file exists", async () => { + await settingsService.updateGlobalSettings({ theme: "light" }); + const exists = await settingsService.hasGlobalSettings(); + expect(exists).toBe(true); + }); + }); + + describe("getCredentials", () => { + it("should return default credentials when file does not exist", async () => { + const credentials = await settingsService.getCredentials(); + expect(credentials).toEqual(DEFAULT_CREDENTIALS); + }); + + it("should read and return existing credentials", async () => { + const customCredentials: Credentials = { + ...DEFAULT_CREDENTIALS, + apiKeys: { + anthropic: "sk-test-key", + google: "", + openai: "", + }, + }; + const credentialsPath = path.join(testDataDir, "credentials.json"); + await fs.writeFile(credentialsPath, JSON.stringify(customCredentials, null, 2)); + + const credentials = await settingsService.getCredentials(); + expect(credentials.apiKeys.anthropic).toBe("sk-test-key"); + }); + + it("should merge with defaults for missing api keys", async () => { + const partialCredentials = { + version: CREDENTIALS_VERSION, + apiKeys: { + anthropic: "sk-test", + }, + }; + const credentialsPath = path.join(testDataDir, "credentials.json"); + await fs.writeFile(credentialsPath, JSON.stringify(partialCredentials, null, 2)); + + const credentials = await settingsService.getCredentials(); + expect(credentials.apiKeys.anthropic).toBe("sk-test"); + expect(credentials.apiKeys.google).toBe(""); + expect(credentials.apiKeys.openai).toBe(""); + }); + }); + + describe("updateCredentials", () => { + it("should create credentials file with updates", async () => { + const updates: Partial = { + apiKeys: { + anthropic: "sk-test-key", + google: "", + openai: "", + }, + }; + + const updated = await settingsService.updateCredentials(updates); + + expect(updated.apiKeys.anthropic).toBe("sk-test-key"); + expect(updated.version).toBe(CREDENTIALS_VERSION); + + const credentialsPath = path.join(testDataDir, "credentials.json"); + const fileContent = await fs.readFile(credentialsPath, "utf-8"); + const saved = JSON.parse(fileContent); + expect(saved.apiKeys.anthropic).toBe("sk-test-key"); + }); + + it("should merge updates with existing credentials", async () => { + const initial: Credentials = { + ...DEFAULT_CREDENTIALS, + apiKeys: { + anthropic: "sk-initial", + google: "google-key", + openai: "", + }, + }; + const credentialsPath = path.join(testDataDir, "credentials.json"); + await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2)); + + const updates: Partial = { + apiKeys: { + anthropic: "sk-updated", + }, + }; + + const updated = await settingsService.updateCredentials(updates); + + expect(updated.apiKeys.anthropic).toBe("sk-updated"); + expect(updated.apiKeys.google).toBe("google-key"); // Preserved + }); + + it("should deep merge api keys", async () => { + const initial: Credentials = { + ...DEFAULT_CREDENTIALS, + apiKeys: { + anthropic: "sk-anthropic", + google: "google-key", + openai: "openai-key", + }, + }; + const credentialsPath = path.join(testDataDir, "credentials.json"); + await fs.writeFile(credentialsPath, JSON.stringify(initial, null, 2)); + + const updates: Partial = { + apiKeys: { + openai: "new-openai-key", + }, + }; + + const updated = await settingsService.updateCredentials(updates); + + expect(updated.apiKeys.anthropic).toBe("sk-anthropic"); + expect(updated.apiKeys.google).toBe("google-key"); + expect(updated.apiKeys.openai).toBe("new-openai-key"); + }); + }); + + describe("getMaskedCredentials", () => { + it("should return masked credentials for empty keys", async () => { + const masked = await settingsService.getMaskedCredentials(); + expect(masked.anthropic.configured).toBe(false); + expect(masked.anthropic.masked).toBe(""); + expect(masked.google.configured).toBe(false); + expect(masked.openai.configured).toBe(false); + }); + + it("should mask keys correctly", async () => { + await settingsService.updateCredentials({ + apiKeys: { + anthropic: "sk-ant-api03-1234567890abcdef", + google: "AIzaSy1234567890abcdef", + openai: "sk-1234567890abcdef", + }, + }); + + const masked = await settingsService.getMaskedCredentials(); + expect(masked.anthropic.configured).toBe(true); + expect(masked.anthropic.masked).toBe("sk-a...cdef"); + expect(masked.google.configured).toBe(true); + expect(masked.google.masked).toBe("AIza...cdef"); + expect(masked.openai.configured).toBe(true); + expect(masked.openai.masked).toBe("sk-1...cdef"); + }); + + it("should handle short keys", async () => { + await settingsService.updateCredentials({ + apiKeys: { + anthropic: "short", + google: "", + openai: "", + }, + }); + + const masked = await settingsService.getMaskedCredentials(); + expect(masked.anthropic.configured).toBe(true); + expect(masked.anthropic.masked).toBe(""); + }); + }); + + describe("hasCredentials", () => { + it("should return false when credentials file does not exist", async () => { + const exists = await settingsService.hasCredentials(); + expect(exists).toBe(false); + }); + + it("should return true when credentials file exists", async () => { + await settingsService.updateCredentials({ + apiKeys: { anthropic: "test", google: "", openai: "" }, + }); + const exists = await settingsService.hasCredentials(); + expect(exists).toBe(true); + }); + }); + + describe("getProjectSettings", () => { + it("should return default settings when file does not exist", async () => { + const settings = await settingsService.getProjectSettings(testProjectDir); + expect(settings).toEqual(DEFAULT_PROJECT_SETTINGS); + }); + + it("should read and return existing project settings", async () => { + const customSettings: ProjectSettings = { + ...DEFAULT_PROJECT_SETTINGS, + theme: "light", + useWorktrees: true, + }; + const automakerDir = path.join(testProjectDir, ".automaker"); + await fs.mkdir(automakerDir, { recursive: true }); + const settingsPath = path.join(automakerDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(customSettings, null, 2)); + + const settings = await settingsService.getProjectSettings(testProjectDir); + expect(settings.theme).toBe("light"); + expect(settings.useWorktrees).toBe(true); + }); + + it("should merge with defaults for missing properties", async () => { + const partialSettings = { + version: PROJECT_SETTINGS_VERSION, + theme: "dark", + }; + const automakerDir = path.join(testProjectDir, ".automaker"); + await fs.mkdir(automakerDir, { recursive: true }); + const settingsPath = path.join(automakerDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(partialSettings, null, 2)); + + const settings = await settingsService.getProjectSettings(testProjectDir); + expect(settings.theme).toBe("dark"); + expect(settings.version).toBe(PROJECT_SETTINGS_VERSION); + }); + }); + + describe("updateProjectSettings", () => { + it("should create project settings file with updates", async () => { + const updates: Partial = { + theme: "light", + useWorktrees: true, + }; + + const updated = await settingsService.updateProjectSettings(testProjectDir, updates); + + expect(updated.theme).toBe("light"); + expect(updated.useWorktrees).toBe(true); + expect(updated.version).toBe(PROJECT_SETTINGS_VERSION); + + const automakerDir = path.join(testProjectDir, ".automaker"); + const settingsPath = path.join(automakerDir, "settings.json"); + const fileContent = await fs.readFile(settingsPath, "utf-8"); + const saved = JSON.parse(fileContent); + expect(saved.theme).toBe("light"); + expect(saved.useWorktrees).toBe(true); + }); + + it("should merge updates with existing project settings", async () => { + const initial: ProjectSettings = { + ...DEFAULT_PROJECT_SETTINGS, + theme: "dark", + useWorktrees: false, + }; + const automakerDir = path.join(testProjectDir, ".automaker"); + await fs.mkdir(automakerDir, { recursive: true }); + const settingsPath = path.join(automakerDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2)); + + const updates: Partial = { + theme: "light", + }; + + const updated = await settingsService.updateProjectSettings(testProjectDir, updates); + + expect(updated.theme).toBe("light"); + expect(updated.useWorktrees).toBe(false); // Preserved + }); + + it("should deep merge board background", async () => { + const initial: ProjectSettings = { + ...DEFAULT_PROJECT_SETTINGS, + boardBackground: { + imagePath: "/path/to/image.jpg", + cardOpacity: 0.8, + columnOpacity: 0.9, + columnBorderEnabled: true, + cardGlassmorphism: false, + cardBorderEnabled: true, + cardBorderOpacity: 0.5, + hideScrollbar: false, + }, + }; + const automakerDir = path.join(testProjectDir, ".automaker"); + await fs.mkdir(automakerDir, { recursive: true }); + const settingsPath = path.join(automakerDir, "settings.json"); + await fs.writeFile(settingsPath, JSON.stringify(initial, null, 2)); + + const updates: Partial = { + boardBackground: { + cardOpacity: 0.9, + }, + }; + + const updated = await settingsService.updateProjectSettings(testProjectDir, updates); + + expect(updated.boardBackground?.imagePath).toBe("/path/to/image.jpg"); + expect(updated.boardBackground?.cardOpacity).toBe(0.9); + expect(updated.boardBackground?.columnOpacity).toBe(0.9); + }); + + it("should create .automaker directory if it does not exist", async () => { + const newProjectDir = path.join(os.tmpdir(), `new-project-${Date.now()}`); + + await settingsService.updateProjectSettings(newProjectDir, { theme: "light" }); + + const automakerDir = path.join(newProjectDir, ".automaker"); + const stats = await fs.stat(automakerDir); + expect(stats.isDirectory()).toBe(true); + + await fs.rm(newProjectDir, { recursive: true, force: true }); + }); + }); + + describe("hasProjectSettings", () => { + it("should return false when project settings file does not exist", async () => { + const exists = await settingsService.hasProjectSettings(testProjectDir); + expect(exists).toBe(false); + }); + + it("should return true when project settings file exists", async () => { + await settingsService.updateProjectSettings(testProjectDir, { theme: "light" }); + const exists = await settingsService.hasProjectSettings(testProjectDir); + expect(exists).toBe(true); + }); + }); + + describe("migrateFromLocalStorage", () => { + it("should migrate global settings from localStorage data", async () => { + const localStorageData = { + "automaker-storage": JSON.stringify({ + state: { + theme: "light", + sidebarOpen: false, + maxConcurrency: 5, + }, + }), + }; + + const result = await settingsService.migrateFromLocalStorage(localStorageData); + + expect(result.success).toBe(true); + expect(result.migratedGlobalSettings).toBe(true); + expect(result.migratedCredentials).toBe(false); + expect(result.migratedProjectCount).toBe(0); + + const settings = await settingsService.getGlobalSettings(); + expect(settings.theme).toBe("light"); + expect(settings.sidebarOpen).toBe(false); + expect(settings.maxConcurrency).toBe(5); + }); + + it("should migrate credentials from localStorage data", async () => { + const localStorageData = { + "automaker-storage": JSON.stringify({ + state: { + apiKeys: { + anthropic: "sk-test-key", + google: "google-key", + openai: "openai-key", + }, + }, + }), + }; + + const result = await settingsService.migrateFromLocalStorage(localStorageData); + + expect(result.success).toBe(true); + expect(result.migratedCredentials).toBe(true); + + const credentials = await settingsService.getCredentials(); + expect(credentials.apiKeys.anthropic).toBe("sk-test-key"); + expect(credentials.apiKeys.google).toBe("google-key"); + expect(credentials.apiKeys.openai).toBe("openai-key"); + }); + + it("should migrate project settings from localStorage data", async () => { + const localStorageData = { + "automaker-storage": JSON.stringify({ + state: { + projects: [ + { + id: "proj1", + name: "Project 1", + path: testProjectDir, + theme: "light", + }, + ], + boardBackgroundByProject: { + [testProjectDir]: { + imagePath: "/path/to/image.jpg", + cardOpacity: 0.8, + columnOpacity: 0.9, + columnBorderEnabled: true, + cardGlassmorphism: false, + cardBorderEnabled: true, + cardBorderOpacity: 0.5, + hideScrollbar: false, + }, + }, + }, + }), + }; + + const result = await settingsService.migrateFromLocalStorage(localStorageData); + + expect(result.success).toBe(true); + expect(result.migratedProjectCount).toBe(1); + + const projectSettings = await settingsService.getProjectSettings(testProjectDir); + expect(projectSettings.theme).toBe("light"); + expect(projectSettings.boardBackground?.imagePath).toBe("/path/to/image.jpg"); + }); + + it("should handle direct localStorage values", async () => { + const localStorageData = { + "automaker:lastProjectDir": "/path/to/project", + "file-browser-recent-folders": JSON.stringify(["/path1", "/path2"]), + "worktree-panel-collapsed": "true", + }; + + const result = await settingsService.migrateFromLocalStorage(localStorageData); + + expect(result.success).toBe(true); + const settings = await settingsService.getGlobalSettings(); + expect(settings.lastProjectDir).toBe("/path/to/project"); + expect(settings.recentFolders).toEqual(["/path1", "/path2"]); + expect(settings.worktreePanelCollapsed).toBe(true); + }); + + it("should handle invalid JSON gracefully", async () => { + const localStorageData = { + "automaker-storage": "invalid json", + "file-browser-recent-folders": "invalid json", + }; + + const result = await settingsService.migrateFromLocalStorage(localStorageData); + + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it("should handle migration errors gracefully", async () => { + // Create a read-only directory to cause write errors + const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`); + await fs.mkdir(readOnlyDir, { recursive: true }); + await fs.chmod(readOnlyDir, 0o444); + + const readOnlyService = new SettingsService(readOnlyDir); + const localStorageData = { + "automaker-storage": JSON.stringify({ + state: { theme: "light" }, + }), + }; + + const result = await readOnlyService.migrateFromLocalStorage(localStorageData); + + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + + await fs.chmod(readOnlyDir, 0o755); + await fs.rm(readOnlyDir, { recursive: true, force: true }); + }); + }); + + describe("getDataDir", () => { + it("should return the data directory path", () => { + const dataDir = settingsService.getDataDir(); + expect(dataDir).toBe(testDataDir); + }); + }); + + describe("atomicWriteJson", () => { + it("should handle write errors and clean up temp file", async () => { + // Create a read-only directory to cause write errors + const readOnlyDir = path.join(os.tmpdir(), `readonly-${Date.now()}`); + await fs.mkdir(readOnlyDir, { recursive: true }); + await fs.chmod(readOnlyDir, 0o444); + + const readOnlyService = new SettingsService(readOnlyDir); + + await expect( + readOnlyService.updateGlobalSettings({ theme: "light" }) + ).rejects.toThrow(); + + await fs.chmod(readOnlyDir, 0o755); + await fs.rm(readOnlyDir, { recursive: true, force: true }); + }); + }); +}); +