mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
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.
This commit is contained in:
@@ -37,10 +37,12 @@ import {
|
|||||||
isTerminalEnabled,
|
isTerminalEnabled,
|
||||||
isTerminalPasswordRequired,
|
isTerminalPasswordRequired,
|
||||||
} from "./routes/terminal/index.js";
|
} from "./routes/terminal/index.js";
|
||||||
|
import { createSettingsRoutes } from "./routes/settings/index.js";
|
||||||
import { AgentService } from "./services/agent-service.js";
|
import { AgentService } from "./services/agent-service.js";
|
||||||
import { FeatureLoader } from "./services/feature-loader.js";
|
import { FeatureLoader } from "./services/feature-loader.js";
|
||||||
import { AutoModeService } from "./services/auto-mode-service.js";
|
import { AutoModeService } from "./services/auto-mode-service.js";
|
||||||
import { getTerminalService } from "./services/terminal-service.js";
|
import { getTerminalService } from "./services/terminal-service.js";
|
||||||
|
import { SettingsService } from "./services/settings-service.js";
|
||||||
import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js";
|
import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js";
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
@@ -108,6 +110,7 @@ const events: EventEmitter = createEventEmitter();
|
|||||||
const agentService = new AgentService(DATA_DIR, events);
|
const agentService = new AgentService(DATA_DIR, events);
|
||||||
const featureLoader = new FeatureLoader();
|
const featureLoader = new FeatureLoader();
|
||||||
const autoModeService = new AutoModeService(events);
|
const autoModeService = new AutoModeService(events);
|
||||||
|
const settingsService = new SettingsService(DATA_DIR);
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -137,6 +140,7 @@ app.use("/api/running-agents", createRunningAgentsRoutes(autoModeService));
|
|||||||
app.use("/api/workspace", createWorkspaceRoutes());
|
app.use("/api/workspace", createWorkspaceRoutes());
|
||||||
app.use("/api/templates", createTemplatesRoutes());
|
app.use("/api/templates", createTemplatesRoutes());
|
||||||
app.use("/api/terminal", createTerminalRoutes());
|
app.use("/api/terminal", createTerminalRoutes());
|
||||||
|
app.use("/api/settings", createSettingsRoutes(settingsService));
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
|
|||||||
@@ -89,3 +89,38 @@ export async function ensureAutomakerDir(projectPath: string): Promise<string> {
|
|||||||
await fs.mkdir(automakerDir, { recursive: true });
|
await fs.mkdir(automakerDir, { recursive: true });
|
||||||
return automakerDir;
|
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<string> {
|
||||||
|
await fs.mkdir(dataDir, { recursive: true });
|
||||||
|
return dataDir;
|
||||||
|
}
|
||||||
|
|||||||
15
apps/server/src/routes/settings/common.ts
Normal file
15
apps/server/src/routes/settings/common.ts
Normal file
@@ -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);
|
||||||
38
apps/server/src/routes/settings/index.ts
Normal file
38
apps/server/src/routes/settings/index.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
23
apps/server/src/routes/settings/routes/get-credentials.ts
Normal file
23
apps/server/src/routes/settings/routes/get-credentials.ts
Normal file
@@ -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<void> => {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
23
apps/server/src/routes/settings/routes/get-global.ts
Normal file
23
apps/server/src/routes/settings/routes/get-global.ts
Normal file
@@ -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<void> => {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
34
apps/server/src/routes/settings/routes/get-project.ts
Normal file
34
apps/server/src/routes/settings/routes/get-project.ts
Normal file
@@ -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<void> => {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
54
apps/server/src/routes/settings/routes/migrate.ts
Normal file
54
apps/server/src/routes/settings/routes/migrate.ts
Normal file
@@ -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<void> => {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
28
apps/server/src/routes/settings/routes/status.ts
Normal file
28
apps/server/src/routes/settings/routes/status.ts
Normal file
@@ -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<void> => {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
39
apps/server/src/routes/settings/routes/update-credentials.ts
Normal file
39
apps/server/src/routes/settings/routes/update-credentials.ts
Normal file
@@ -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<void> => {
|
||||||
|
try {
|
||||||
|
const updates = req.body as Partial<Credentials>;
|
||||||
|
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
34
apps/server/src/routes/settings/routes/update-global.ts
Normal file
34
apps/server/src/routes/settings/routes/update-global.ts
Normal file
@@ -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<void> => {
|
||||||
|
try {
|
||||||
|
const updates = req.body as Partial<GlobalSettings>;
|
||||||
|
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
48
apps/server/src/routes/settings/routes/update-project.ts
Normal file
48
apps/server/src/routes/settings/routes/update-project.ts
Normal file
@@ -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<void> => {
|
||||||
|
try {
|
||||||
|
const { projectPath, updates } = req.body as {
|
||||||
|
projectPath?: string;
|
||||||
|
updates?: Partial<ProjectSettings>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
542
apps/server/src/services/settings-service.ts
Normal file
542
apps/server/src/services/settings-service.ts
Normal file
@@ -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<void> {
|
||||||
|
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<T>(filePath: string, defaultValue: T): Promise<T> {
|
||||||
|
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<boolean> {
|
||||||
|
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<GlobalSettings> {
|
||||||
|
const settingsPath = getGlobalSettingsPath(this.dataDir);
|
||||||
|
const settings = await readJsonFile<GlobalSettings>(
|
||||||
|
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<GlobalSettings>
|
||||||
|
): Promise<GlobalSettings> {
|
||||||
|
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<boolean> {
|
||||||
|
const settingsPath = getGlobalSettingsPath(this.dataDir);
|
||||||
|
return fileExists(settingsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Credentials
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get credentials
|
||||||
|
*/
|
||||||
|
async getCredentials(): Promise<Credentials> {
|
||||||
|
const credentialsPath = getCredentialsPath(this.dataDir);
|
||||||
|
const credentials = await readJsonFile<Credentials>(
|
||||||
|
credentialsPath,
|
||||||
|
DEFAULT_CREDENTIALS
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_CREDENTIALS,
|
||||||
|
...credentials,
|
||||||
|
apiKeys: {
|
||||||
|
...DEFAULT_CREDENTIALS.apiKeys,
|
||||||
|
...credentials.apiKeys,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update credentials (partial update)
|
||||||
|
*/
|
||||||
|
async updateCredentials(
|
||||||
|
updates: Partial<Credentials>
|
||||||
|
): Promise<Credentials> {
|
||||||
|
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<boolean> {
|
||||||
|
const credentialsPath = getCredentialsPath(this.dataDir);
|
||||||
|
return fileExists(credentialsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Project Settings
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get project settings
|
||||||
|
*/
|
||||||
|
async getProjectSettings(projectPath: string): Promise<ProjectSettings> {
|
||||||
|
const settingsPath = getProjectSettingsPath(projectPath);
|
||||||
|
const settings = await readJsonFile<ProjectSettings>(
|
||||||
|
settingsPath,
|
||||||
|
DEFAULT_PROJECT_SETTINGS
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_PROJECT_SETTINGS,
|
||||||
|
...settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update project settings (partial update)
|
||||||
|
*/
|
||||||
|
async updateProjectSettings(
|
||||||
|
projectPath: string,
|
||||||
|
updates: Partial<ProjectSettings>
|
||||||
|
): Promise<ProjectSettings> {
|
||||||
|
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<boolean> {
|
||||||
|
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<string, unknown> = {};
|
||||||
|
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<GlobalSettings> = {
|
||||||
|
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<string, string>) ||
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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<string, BoardBackgroundSettings>
|
||||||
|
| undefined;
|
||||||
|
const currentWorktreeByProject = appState.currentWorktreeByProject as
|
||||||
|
| Record<string, { path: string | null; branch: string }>
|
||||||
|
| undefined;
|
||||||
|
const worktreesByProject = appState.worktreesByProject as
|
||||||
|
| Record<string, WorktreeInfo[]>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
// Get unique project paths that have per-project settings
|
||||||
|
const projectPaths = new Set<string>();
|
||||||
|
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<ProjectSettings> = {};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
269
apps/server/src/types/settings.ts
Normal file
269
apps/server/src/types/settings.ts
Normal file
@@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
@@ -1,7 +1,35 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
import { RouterProvider } from "@tanstack/react-router";
|
import { RouterProvider } from "@tanstack/react-router";
|
||||||
import { router } from "./utils/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/global.css";
|
||||||
|
import "./styles/theme-imports";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return <RouterProvider router={router} />;
|
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 (
|
||||||
|
<>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
{showSplash && <SplashScreen onComplete={handleSplashComplete} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
309
apps/ui/src/components/splash-screen.tsx
Normal file
309
apps/ui/src/components/splash-screen.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
fixed inset-0 z-[9999] flex items-center justify-center
|
||||||
|
bg-background
|
||||||
|
transition-opacity duration-500 ease-out
|
||||||
|
${phase === "exit" ? "opacity-0" : "opacity-100"}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
pointerEvents: phase === "exit" ? "none" : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<style>{`
|
||||||
|
@keyframes spin-slow {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes spin-slow-reverse {
|
||||||
|
from { transform: rotate(360deg); }
|
||||||
|
to { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(6px, -6px); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Subtle gradient background */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-30"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle at center, var(--brand-500) 0%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Particle container 1 - Clockwise */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex items-center justify-center overflow-hidden"
|
||||||
|
style={{ animation: "spin-slow 60s linear infinite" }}
|
||||||
|
>
|
||||||
|
{particles.slice(0, 25).map((particle) => (
|
||||||
|
<div
|
||||||
|
key={particle.id}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
transform:
|
||||||
|
phase === "exit"
|
||||||
|
? `translate(${Math.cos((particle.angle * Math.PI) / 180) * particle.distance}px, ${Math.sin((particle.angle * Math.PI) / 180) * particle.distance}px)`
|
||||||
|
: `translate(${particle.x}px, ${particle.y}px)`,
|
||||||
|
transition:
|
||||||
|
phase === "enter"
|
||||||
|
? `all 600ms ease-out ${PARTICLES_ENTER_DELAY + particle.delay}ms`
|
||||||
|
: phase === "exit"
|
||||||
|
? `all 800ms cubic-bezier(0.4, 0, 1, 1) ${particle.delay * 0.3}ms`
|
||||||
|
: "all 300ms ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-full"
|
||||||
|
style={{
|
||||||
|
width: particle.size,
|
||||||
|
height: particle.size,
|
||||||
|
background: `linear-gradient(135deg, var(--brand-400), var(--brand-600))`,
|
||||||
|
boxShadow: `0 0 ${particle.size * 2}px var(--brand-500)`,
|
||||||
|
opacity:
|
||||||
|
phase === "enter"
|
||||||
|
? 0
|
||||||
|
: phase === "hold"
|
||||||
|
? particle.opacity
|
||||||
|
: 0,
|
||||||
|
transform: phase === "exit" ? "scale(0)" : "scale(1)",
|
||||||
|
animation: `float ${particle.floatDuration}ms ease-in-out infinite`,
|
||||||
|
transition: "opacity 300ms ease-out, transform 300ms ease-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Particle container 2 - Counter-Clockwise */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex items-center justify-center overflow-hidden"
|
||||||
|
style={{ animation: "spin-slow-reverse 75s linear infinite" }}
|
||||||
|
>
|
||||||
|
{particles.slice(25).map((particle) => (
|
||||||
|
<div
|
||||||
|
key={particle.id}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
transform:
|
||||||
|
phase === "exit"
|
||||||
|
? `translate(${Math.cos((particle.angle * Math.PI) / 180) * particle.distance}px, ${Math.sin((particle.angle * Math.PI) / 180) * particle.distance}px)`
|
||||||
|
: `translate(${particle.x}px, ${particle.y}px)`,
|
||||||
|
transition:
|
||||||
|
phase === "enter"
|
||||||
|
? `all 600ms ease-out ${PARTICLES_ENTER_DELAY + particle.delay}ms`
|
||||||
|
: phase === "exit"
|
||||||
|
? `all 800ms cubic-bezier(0.4, 0, 1, 1) ${particle.delay * 0.3}ms`
|
||||||
|
: "all 300ms ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-full"
|
||||||
|
style={{
|
||||||
|
width: particle.size,
|
||||||
|
height: particle.size,
|
||||||
|
background: `linear-gradient(135deg, var(--brand-400), var(--brand-600))`,
|
||||||
|
boxShadow: `0 0 ${particle.size * 2}px var(--brand-500)`,
|
||||||
|
opacity:
|
||||||
|
phase === "enter"
|
||||||
|
? 0
|
||||||
|
: phase === "hold"
|
||||||
|
? particle.opacity
|
||||||
|
: 0,
|
||||||
|
transform: phase === "exit" ? "scale(0)" : "scale(1)",
|
||||||
|
animation: `float ${particle.floatDuration}ms ease-in-out infinite`,
|
||||||
|
animationDelay: `${particle.delay}ms`,
|
||||||
|
transition: "opacity 300ms ease-out, transform 300ms ease-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo container */}
|
||||||
|
<div
|
||||||
|
className="relative z-10"
|
||||||
|
style={{
|
||||||
|
opacity: phase === "enter" ? 0 : phase === "exit" ? 0 : 1,
|
||||||
|
transform:
|
||||||
|
phase === "enter"
|
||||||
|
? "scale(0.3) rotate(-20deg)"
|
||||||
|
: phase === "exit"
|
||||||
|
? "scale(2.5) translateY(-100px)"
|
||||||
|
: "scale(1) rotate(0deg)",
|
||||||
|
transition:
|
||||||
|
phase === "enter"
|
||||||
|
? `all ${LOGO_ENTER_DURATION}ms cubic-bezier(0.34, 1.56, 0.64, 1)`
|
||||||
|
: phase === "exit"
|
||||||
|
? "all 600ms cubic-bezier(0.4, 0, 1, 1)"
|
||||||
|
: "all 300ms ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Glow effect behind logo */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 blur-3xl"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle, var(--brand-500) 0%, transparent 70%)",
|
||||||
|
transform: "scale(2.5)",
|
||||||
|
opacity: phase === "hold" ? 0.6 : 0,
|
||||||
|
transition: "opacity 500ms ease-out",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* The logo */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
role="img"
|
||||||
|
aria-label="Automaker Logo"
|
||||||
|
className="relative z-10"
|
||||||
|
style={{
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
filter: "drop-shadow(0 0 30px var(--brand-500))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="splash-bg"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="256"
|
||||||
|
y2="256"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0%" style={{ stopColor: "var(--brand-400)" }} />
|
||||||
|
<stop offset="100%" style={{ stopColor: "var(--brand-600)" }} />
|
||||||
|
</linearGradient>
|
||||||
|
<filter
|
||||||
|
id="splash-shadow"
|
||||||
|
x="-20%"
|
||||||
|
y="-20%"
|
||||||
|
width="140%"
|
||||||
|
height="140%"
|
||||||
|
>
|
||||||
|
<feDropShadow
|
||||||
|
dx="0"
|
||||||
|
dy="4"
|
||||||
|
stdDeviation="4"
|
||||||
|
floodColor="#000000"
|
||||||
|
floodOpacity="0.25"
|
||||||
|
/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
x="16"
|
||||||
|
y="16"
|
||||||
|
width="224"
|
||||||
|
height="224"
|
||||||
|
rx="56"
|
||||||
|
fill="url(#splash-bg)"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
strokeWidth="20"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
filter="url(#splash-shadow)"
|
||||||
|
>
|
||||||
|
<path d="M92 92 L52 128 L92 164" />
|
||||||
|
<path d="M144 72 L116 184" />
|
||||||
|
<path d="M164 92 L204 128 L164 164" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Automaker text that fades in below the logo */}
|
||||||
|
<div
|
||||||
|
className="absolute flex items-center gap-1"
|
||||||
|
style={{
|
||||||
|
top: "calc(50% + 80px)",
|
||||||
|
opacity: phase === "enter" ? 0 : phase === "exit" ? 0 : 1,
|
||||||
|
transform:
|
||||||
|
phase === "enter"
|
||||||
|
? "translateY(20px)"
|
||||||
|
: phase === "exit"
|
||||||
|
? "translateY(-30px) scale(1.2)"
|
||||||
|
: "translateY(0)",
|
||||||
|
transition:
|
||||||
|
phase === "enter"
|
||||||
|
? `all 600ms ease-out ${LOGO_ENTER_DURATION - 200}ms`
|
||||||
|
: phase === "exit"
|
||||||
|
? "all 500ms cubic-bezier(0.4, 0, 1, 1)"
|
||||||
|
: "all 300ms ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="font-bold text-foreground text-4xl tracking-tight leading-none">
|
||||||
|
automaker<span className="text-brand-500">.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { useSetupStore } from "@/store/setup-store";
|
|||||||
import { StepIndicator } from "./setup-view/components";
|
import { StepIndicator } from "./setup-view/components";
|
||||||
import {
|
import {
|
||||||
WelcomeStep,
|
WelcomeStep,
|
||||||
|
ThemeStep,
|
||||||
CompleteStep,
|
CompleteStep,
|
||||||
ClaudeSetupStep,
|
ClaudeSetupStep,
|
||||||
GitHubSetupStep,
|
GitHubSetupStep,
|
||||||
@@ -19,12 +20,13 @@ export function SetupView() {
|
|||||||
} = useSetupStore();
|
} = useSetupStore();
|
||||||
const navigate = useNavigate();
|
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];
|
type StepName = (typeof steps)[number];
|
||||||
const getStepName = (): StepName => {
|
const getStepName = (): StepName => {
|
||||||
if (currentStep === "claude_detect" || currentStep === "claude_auth")
|
if (currentStep === "claude_detect" || currentStep === "claude_auth")
|
||||||
return "claude";
|
return "claude";
|
||||||
if (currentStep === "welcome") return "welcome";
|
if (currentStep === "welcome") return "welcome";
|
||||||
|
if (currentStep === "theme") return "theme";
|
||||||
if (currentStep === "github") return "github";
|
if (currentStep === "github") return "github";
|
||||||
return "complete";
|
return "complete";
|
||||||
};
|
};
|
||||||
@@ -39,6 +41,10 @@ export function SetupView() {
|
|||||||
);
|
);
|
||||||
switch (from) {
|
switch (from) {
|
||||||
case "welcome":
|
case "welcome":
|
||||||
|
console.log("[Setup Flow] Moving to theme step");
|
||||||
|
setCurrentStep("theme");
|
||||||
|
break;
|
||||||
|
case "theme":
|
||||||
console.log("[Setup Flow] Moving to claude_detect step");
|
console.log("[Setup Flow] Moving to claude_detect step");
|
||||||
setCurrentStep("claude_detect");
|
setCurrentStep("claude_detect");
|
||||||
break;
|
break;
|
||||||
@@ -56,9 +62,12 @@ export function SetupView() {
|
|||||||
const handleBack = (from: string) => {
|
const handleBack = (from: string) => {
|
||||||
console.log("[Setup Flow] handleBack called from:", from);
|
console.log("[Setup Flow] handleBack called from:", from);
|
||||||
switch (from) {
|
switch (from) {
|
||||||
case "claude":
|
case "theme":
|
||||||
setCurrentStep("welcome");
|
setCurrentStep("welcome");
|
||||||
break;
|
break;
|
||||||
|
case "claude":
|
||||||
|
setCurrentStep("theme");
|
||||||
|
break;
|
||||||
case "github":
|
case "github":
|
||||||
setCurrentStep("claude_detect");
|
setCurrentStep("claude_detect");
|
||||||
break;
|
break;
|
||||||
@@ -98,42 +107,47 @@ export function SetupView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0 flex items-center justify-center">
|
||||||
<div className="p-8">
|
<div className="w-full max-w-2xl mx-auto px-8">
|
||||||
<div className="w-full max-w-2xl mx-auto">
|
<div className="mb-8">
|
||||||
<div className="mb-8">
|
<StepIndicator
|
||||||
<StepIndicator
|
currentStep={currentIndex}
|
||||||
currentStep={currentIndex}
|
totalSteps={steps.length}
|
||||||
totalSteps={steps.length}
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{currentStep === "welcome" && (
|
||||||
|
<WelcomeStep onNext={() => handleNext("welcome")} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === "theme" && (
|
||||||
|
<ThemeStep
|
||||||
|
onNext={() => handleNext("theme")}
|
||||||
|
onBack={() => handleBack("theme")}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="py-8">
|
{(currentStep === "claude_detect" ||
|
||||||
{currentStep === "welcome" && (
|
currentStep === "claude_auth") && (
|
||||||
<WelcomeStep onNext={() => handleNext("welcome")} />
|
<ClaudeSetupStep
|
||||||
)}
|
onNext={() => handleNext("claude")}
|
||||||
|
onBack={() => handleBack("claude")}
|
||||||
|
onSkip={handleSkipClaude}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(currentStep === "claude_detect" ||
|
{currentStep === "github" && (
|
||||||
currentStep === "claude_auth") && (
|
<GitHubSetupStep
|
||||||
<ClaudeSetupStep
|
onNext={() => handleNext("github")}
|
||||||
onNext={() => handleNext("claude")}
|
onBack={() => handleBack("github")}
|
||||||
onBack={() => handleBack("claude")}
|
onSkip={handleSkipGithub}
|
||||||
onSkip={handleSkipClaude}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === "github" && (
|
{currentStep === "complete" && (
|
||||||
<GitHubSetupStep
|
<CompleteStep onFinish={handleFinish} />
|
||||||
onNext={() => handleNext("github")}
|
)}
|
||||||
onBack={() => handleBack("github")}
|
|
||||||
onSkip={handleSkipGithub}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === "complete" && (
|
|
||||||
<CompleteStep onFinish={handleFinish} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Re-export all setup step components for easier imports
|
// Re-export all setup step components for easier imports
|
||||||
export { WelcomeStep } from "./welcome-step";
|
export { WelcomeStep } from "./welcome-step";
|
||||||
|
export { ThemeStep } from "./theme-step";
|
||||||
export { CompleteStep } from "./complete-step";
|
export { CompleteStep } from "./complete-step";
|
||||||
export { ClaudeSetupStep } from "./claude-setup-step";
|
export { ClaudeSetupStep } from "./claude-setup-step";
|
||||||
export { GitHubSetupStep } from "./github-setup-step";
|
export { GitHubSetupStep } from "./github-setup-step";
|
||||||
|
|||||||
90
apps/ui/src/components/views/setup-view/steps/theme-step.tsx
Normal file
90
apps/ui/src/components/views/setup-view/steps/theme-step.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-3xl font-bold text-foreground mb-3">
|
||||||
|
Choose Your Theme
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground max-w-md mx-auto">
|
||||||
|
Pick a theme that suits your style. Hover to preview, click to select.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||||
|
{themeOptions.map((option) => {
|
||||||
|
const Icon = option.Icon;
|
||||||
|
const isSelected = theme === option.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
data-testid={option.testId}
|
||||||
|
onMouseEnter={() => handleThemeHover(option.value)}
|
||||||
|
onMouseLeave={handleThemeLeave}
|
||||||
|
onClick={() => handleThemeClick(option.value)}
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all duration-200",
|
||||||
|
"hover:scale-105 hover:shadow-lg",
|
||||||
|
isSelected
|
||||||
|
? "border-brand-500 bg-brand-500/10"
|
||||||
|
: "border-border hover:border-brand-400 bg-card"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<Check className="w-4 h-4 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Icon className="w-6 h-6 text-foreground" />
|
||||||
|
<span className="text-sm font-medium text-foreground">
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between pt-4">
|
||||||
|
<Button variant="ghost" onClick={onBack}>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-700 text-white"
|
||||||
|
onClick={onNext}
|
||||||
|
data-testid="theme-continue-button"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
261
apps/ui/src/hooks/use-settings-migration.ts
Normal file
261
apps/ui/src/hooks/use-settings-migration.ts
Normal file
@@ -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<MigrationState>({
|
||||||
|
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<string, string> = {};
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<string, unknown>;
|
||||||
|
currentWorktree?: { path: string | null; branch: string };
|
||||||
|
worktrees?: Array<{
|
||||||
|
path: string;
|
||||||
|
branch: string;
|
||||||
|
isMain: boolean;
|
||||||
|
hasChanges?: boolean;
|
||||||
|
changedFilesCount?: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
): Promise<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -837,6 +837,135 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
this.post("/api/templates/clone", { repoUrl, projectName, parentDir }),
|
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<string, string>;
|
||||||
|
aiProfiles: unknown[];
|
||||||
|
projects: unknown[];
|
||||||
|
trashedProjects: unknown[];
|
||||||
|
projectHistory: string[];
|
||||||
|
projectHistoryIndex: number;
|
||||||
|
lastProjectDir?: string;
|
||||||
|
recentFolders: string[];
|
||||||
|
worktreePanelCollapsed: boolean;
|
||||||
|
lastSelectedSessionByProject: Record<string, string>;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}> => this.get("/api/settings/global"),
|
||||||
|
|
||||||
|
updateGlobal: (updates: Record<string, unknown>): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
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<string, unknown>
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
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 API
|
||||||
sessions = {
|
sessions = {
|
||||||
list: (
|
list: (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useState, useCallback } from "react";
|
|||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { FileBrowserProvider, useFileBrowser, setGlobalFileBrowser } from "@/contexts/file-browser-context";
|
import { FileBrowserProvider, useFileBrowser, setGlobalFileBrowser } from "@/contexts/file-browser-context";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
|
import { useSetupStore } from "@/store/setup-store";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { ThemeOption, themeOptions } from "@/config/theme-options";
|
import { ThemeOption, themeOptions } from "@/config/theme-options";
|
||||||
@@ -16,9 +17,13 @@ function RootLayoutContent() {
|
|||||||
previewTheme,
|
previewTheme,
|
||||||
getEffectiveTheme,
|
getEffectiveTheme,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
const { setupComplete } = useSetupStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
||||||
|
const [setupHydrated, setSetupHydrated] = useState(() =>
|
||||||
|
useSetupStore.persist?.hasHydrated?.() ?? false
|
||||||
|
);
|
||||||
const { openFileBrowser } = useFileBrowser();
|
const { openFileBrowser } = useFileBrowser();
|
||||||
|
|
||||||
// Hidden streamer panel - opens with "\" key
|
// Hidden streamer panel - opens with "\" key
|
||||||
@@ -61,6 +66,35 @@ function RootLayoutContent() {
|
|||||||
setIsMounted(true);
|
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(() => {
|
useEffect(() => {
|
||||||
setGlobalFileBrowser(openFileBrowser);
|
setGlobalFileBrowser(openFileBrowser);
|
||||||
}, [openFileBrowser]);
|
}, [openFileBrowser]);
|
||||||
|
|||||||
@@ -2356,3 +2356,205 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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<AppState> | 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<string, NodeJS.Timeout> = {};
|
||||||
|
const PROJECT_SYNC_DEBOUNCE_MS = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule sync of project settings to server
|
||||||
|
*/
|
||||||
|
function scheduleProjectSettingsSync(
|
||||||
|
projectPath: string,
|
||||||
|
updates: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
// 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<string, unknown> = {};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface InstallProgress {
|
|||||||
|
|
||||||
export type SetupStep =
|
export type SetupStep =
|
||||||
| "welcome"
|
| "welcome"
|
||||||
|
| "theme"
|
||||||
| "claude_detect"
|
| "claude_detect"
|
||||||
| "claude_auth"
|
| "claude_auth"
|
||||||
| "github"
|
| "github"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
22
apps/ui/src/styles/theme-imports.ts
Normal file
22
apps/ui/src/styles/theme-imports.ts
Normal file
@@ -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";
|
||||||
|
|
||||||
|
|
||||||
144
apps/ui/src/styles/themes/catppuccin.css
Normal file
144
apps/ui/src/styles/themes/catppuccin.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
116
apps/ui/src/styles/themes/cream.css
Normal file
116
apps/ui/src/styles/themes/cream.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
166
apps/ui/src/styles/themes/dark.css
Normal file
166
apps/ui/src/styles/themes/dark.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
144
apps/ui/src/styles/themes/dracula.css
Normal file
144
apps/ui/src/styles/themes/dracula.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
110
apps/ui/src/styles/themes/gray.css
Normal file
110
apps/ui/src/styles/themes/gray.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
144
apps/ui/src/styles/themes/gruvbox.css
Normal file
144
apps/ui/src/styles/themes/gruvbox.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
103
apps/ui/src/styles/themes/light.css
Normal file
103
apps/ui/src/styles/themes/light.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
|
|
||||||
144
apps/ui/src/styles/themes/monokai.css
Normal file
144
apps/ui/src/styles/themes/monokai.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
144
apps/ui/src/styles/themes/nord.css
Normal file
144
apps/ui/src/styles/themes/nord.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
144
apps/ui/src/styles/themes/onedark.css
Normal file
144
apps/ui/src/styles/themes/onedark.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
70
apps/ui/src/styles/themes/red.css
Normal file
70
apps/ui/src/styles/themes/red.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
227
apps/ui/src/styles/themes/retro.css
Normal file
227
apps/ui/src/styles/themes/retro.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
144
apps/ui/src/styles/themes/solarized.css
Normal file
144
apps/ui/src/styles/themes/solarized.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
111
apps/ui/src/styles/themes/sunset.css
Normal file
111
apps/ui/src/styles/themes/sunset.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
149
apps/ui/src/styles/themes/synthwave.css
Normal file
149
apps/ui/src/styles/themes/synthwave.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
144
apps/ui/src/styles/themes/tokyonight.css
Normal file
144
apps/ui/src/styles/themes/tokyonight.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user