feat: implement theme engine with multiple color presets and add top status bar for system telemetry

This commit is contained in:
jarves committed 2026-06-18 12:36:16 +00:00
1 parent 24a5c5a1df
commit e6836467c3
5 files changed
+440 -12

No files matched your search

+284 -5
View File
@@ -86,6 +86,285 @@ html[data-theme="dark"] {
--glass-blur: blur(16px);
}
/* Nord Light Theme */
html[data-theme="light-nord"] {
--md-sys-color-primary: #5e81ac;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: rgba(94, 129, 172, 0.08);
--md-sys-color-on-primary-container: #4c566a;
--md-sys-color-secondary: #88c0d0;
--md-sys-color-on-secondary: #2e3440;
--md-sys-color-secondary-container: rgba(136, 192, 208, 0.08);
--md-sys-color-on-secondary-container: #3b4252;
--md-sys-color-tertiary: #b48ead;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-background: #eceff4;
--md-sys-color-on-background: #2e3440;
--md-sys-color-surface: rgba(242, 244, 248, 0.85);
--md-sys-color-on-surface: #2e3440;
--md-sys-color-surface-variant: rgba(229, 233, 240, 0.85);
--md-sys-color-on-surface-variant: #4c566a;
--md-sys-color-outline: #4c566a;
--md-sys-color-outline-variant: #d8dee9;
--border-color: rgba(46, 52, 64, 0.09);
--border-color-muted: rgba(46, 52, 64, 0.05);
--md-sys-color-error: #bf616a;
--md-sys-color-on-error: #ffffff;
--md-sys-color-success: #a3be8c;
--md-sys-color-on-success: #2e3440;
--md-sys-color-card-border: rgba(46, 52, 64, 0.06);
--glass-shadow: 0 8px 32px 0 rgba(46, 52, 64, 0.03);
--glass-blur: blur(16px);
}
/* Sakura Blossom Light Theme */
html[data-theme="light-sakura"] {
--md-sys-color-primary: #d05a74;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: rgba(208, 90, 116, 0.06);
--md-sys-color-on-primary-container: #8c354a;
--md-sys-color-secondary: #e597a7;
--md-sys-color-on-secondary: #ffffff;
--md-sys-color-secondary-container: rgba(229, 151, 167, 0.08);
--md-sys-color-on-secondary-container: #8c354a;
--md-sys-color-tertiary: #a278b5;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-background: #fdf6f6;
--md-sys-color-on-background: #5c2a39;
--md-sys-color-surface: rgba(255, 250, 250, 0.85);
--md-sys-color-on-surface: #5c2a39;
--md-sys-color-surface-variant: rgba(250, 235, 235, 0.9);
--md-sys-color-on-surface-variant: #8c5264;
--md-sys-color-outline: #8c5264;
--md-sys-color-outline-variant: #dbb2bc;
--border-color: rgba(92, 42, 57, 0.08);
--border-color-muted: rgba(92, 42, 57, 0.04);
--md-sys-color-error: #e05c5c;
--md-sys-color-on-error: #ffffff;
--md-sys-color-success: #5cbe8a;
--md-sys-color-on-success: #ffffff;
--md-sys-color-card-border: rgba(92, 42, 57, 0.06);
--glass-shadow: 0 8px 32px 0 rgba(92, 42, 57, 0.03);
--glass-blur: blur(16px);
}
/* Cyberpunk Neon Dark Theme */
html[data-theme="dark-cyberpunk"] {
--md-sys-color-primary: #ff007f;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: rgba(255, 0, 127, 0.15);
--md-sys-color-on-primary-container: #ff9ebb;
--md-sys-color-secondary: #00f0ff;
--md-sys-color-on-secondary: #0d0714;
--md-sys-color-secondary-container: rgba(0, 240, 255, 0.1);
--md-sys-color-on-secondary-container: #a6f5ff;
--md-sys-color-tertiary: #9d4edd;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-background: #0d0714;
--md-sys-color-on-background: #e0e0ff;
--md-sys-color-surface: rgba(26, 12, 38, 0.7);
--md-sys-color-on-surface: #e0e0ff;
--md-sys-color-surface-variant: rgba(37, 18, 54, 0.6);
--md-sys-color-on-surface-variant: #b5a8cc;
--md-sys-color-outline: #00f0ff;
--md-sys-color-outline-variant: #ff007f;
--border-color: rgba(0, 240, 255, 0.2);
--border-color-muted: rgba(255, 0, 127, 0.15);
--md-sys-color-error: #ff3333;
--md-sys-color-on-error: #ffffff;
--md-sys-color-success: #00ff66;
--md-sys-color-on-success: #0d0714;
--md-sys-color-card-border: rgba(0, 240, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(255, 0, 127, 0.15);
--glass-blur: blur(16px);
}
/* Emerald Forest Dark Theme */
html[data-theme="dark-forest"] {
--md-sys-color-primary: #10b981;
--md-sys-color-on-primary: #040a09;
--md-sys-color-primary-container: rgba(16, 185, 129, 0.12);
--md-sys-color-on-primary-container: #a7f3d0;
--md-sys-color-secondary: #34d399;
--md-sys-color-on-secondary: #040a09;
--md-sys-color-secondary-container: rgba(52, 211, 153, 0.08);
--md-sys-color-on-secondary-container: #d1fae5;
--md-sys-color-tertiary: #6ee7b7;
--md-sys-color-on-tertiary: #040a09;
--md-sys-color-background: #060f0e;
--md-sys-color-on-background: #e2ebe9;
--md-sys-color-surface: rgba(13, 28, 25, 0.65);
--md-sys-color-on-surface: #e2ebe9;
--md-sys-color-surface-variant: rgba(20, 41, 37, 0.55);
--md-sys-color-on-surface-variant: #94a8a4;
--md-sys-color-outline: #86efac;
--md-sys-color-outline-variant: #15803d;
--border-color: rgba(52, 211, 153, 0.15);
--border-color-muted: rgba(52, 211, 153, 0.08);
--md-sys-color-error: #f87171;
--md-sys-color-on-error: #ffffff;
--md-sys-color-success: #34d399;
--md-sys-color-on-success: #040a09;
--md-sys-color-card-border: rgba(52, 211, 153, 0.06);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
--glass-blur: blur(16px);
}
/* Dracula Classic Dark Theme */
html[data-theme="dark-dracula"] {
--md-sys-color-primary: #bd93f9;
--md-sys-color-on-primary: #282a36;
--md-sys-color-primary-container: rgba(189, 147, 249, 0.15);
--md-sys-color-on-primary-container: #d6bbfb;
--md-sys-color-secondary: #ff79c6;
--md-sys-color-on-secondary: #282a36;
--md-sys-color-secondary-container: rgba(255, 121, 198, 0.1);
--md-sys-color-on-secondary-container: #ffb3db;
--md-sys-color-tertiary: #50fa7b;
--md-sys-color-on-tertiary: #282a36;
--md-sys-color-background: #282a36;
--md-sys-color-on-background: #f8f8f2;
--md-sys-color-surface: rgba(33, 34, 44, 0.7);
--md-sys-color-on-surface: #f8f8f2;
--md-sys-color-surface-variant: rgba(40, 42, 54, 0.6);
--md-sys-color-on-surface-variant: #6272a4;
--md-sys-color-outline: #ff79c6;
--md-sys-color-outline-variant: #6272a4;
--border-color: rgba(189, 147, 249, 0.18);
--border-color-muted: rgba(255, 121, 198, 0.1);
--md-sys-color-error: #ff5555;
--md-sys-color-on-error: #ffffff;
--md-sys-color-success: #50fa7b;
--md-sys-color-on-success: #282a36;
--md-sys-color-card-border: rgba(189, 147, 249, 0.08);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.35);
--glass-blur: blur(16px);
}
/* OLED Pitch Black Theme */
html[data-theme="dark-oled"] {
--md-sys-color-primary: #3b82f6;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: rgba(59, 130, 246, 0.15);
--md-sys-color-on-primary-container: #93c5fd;
--md-sys-color-secondary: #60a5fa;
--md-sys-color-on-secondary: #000000;
--md-sys-color-secondary-container: rgba(96, 165, 250, 0.1);
--md-sys-color-on-secondary-container: #bfdbfe;
--md-sys-color-tertiary: #38bdf8;
--md-sys-color-on-tertiary: #000000;
--md-sys-color-background: #000000;
--md-sys-color-on-background: #ffffff;
--md-sys-color-surface: rgba(10, 10, 10, 0.95);
--md-sys-color-on-surface: #ffffff;
--md-sys-color-surface-variant: rgba(20, 20, 20, 0.98);
--md-sys-color-on-surface-variant: #cccccc;
--md-sys-color-outline: #888888;
--md-sys-color-outline-variant: #444444;
--border-color: rgba(255, 255, 255, 0.15);
--border-color-muted: rgba(255, 255, 255, 0.08);
--md-sys-color-error: #ef4444;
--md-sys-color-on-error: #ffffff;
--md-sys-color-success: #10b981;
--md-sys-color-on-success: #ffffff;
--md-sys-color-card-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
--glass-blur: none;
}
html[data-theme="dark-oled"] body {
background-image: none !important;
}
/* Theme Dropdown & Selection Styling */
.theme-dropdown-menu {
position: absolute;
top: 40px;
right: 0;
width: 190px;
background: var(--md-sys-color-surface);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: 1px solid var(--border-color);
border-radius: var(--md-shape-corner-medium);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
padding: 6px;
display: flex;
flex-direction: column;
gap: 2px;
z-index: 9999;
animation: theme-dropdown-fade 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes theme-dropdown-fade {
from {
opacity: 0;
transform: translateY(-8px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.theme-dropdown-header {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--md-sys-color-outline);
padding: 6px 10px 4px;
}
.theme-dropdown-divider {
height: 1px;
background: var(--border-color-muted);
margin: 4px 0;
}
.theme-dropdown-item {
background: transparent;
border: none;
border-radius: calc(var(--md-shape-corner-medium) - 4px);
padding: 8px 10px;
font-family: inherit;
font-size: 0.8rem;
font-weight: 500;
color: var(--md-sys-color-on-surface-variant);
cursor: pointer;
display: flex;
align-items: center;
text-align: left;
transition: all 0.15s ease;
width: 100%;
}
.theme-dropdown-item:hover {
background: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
.theme-dropdown-item.active {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
font-weight: 600;
}
.theme-card-btn {
transform: translateY(0);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease !important;
}
.theme-card-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08) !important;
border-color: var(--md-sys-color-outline) !important;
}
.theme-card-btn.active:hover {
border-color: var(--md-sys-color-primary) !important;
}
/* Global Reset & Base Styles */
* {
box-sizing: border-box;
@@ -1069,14 +1348,14 @@ body {
transform: translateY(-1px);
}
html[data-theme="dark"] .m3-btn-error {
html[data-theme^="dark"] .m3-btn-error {
background-color: rgba(251, 113, 133, 0.09);
border: 1px solid rgba(251, 113, 133, 0.22);
color: #fda4af;
box-shadow: 0 2px 8px rgba(251, 113, 133, 0.04);
}
html[data-theme="dark"] .m3-btn-error:hover:not(:disabled) {
html[data-theme^="dark"] .m3-btn-error:hover:not(:disabled) {
background-color: rgba(251, 113, 133, 0.16);
border-color: rgba(251, 113, 133, 0.35);
}
@@ -1292,7 +1571,7 @@ html[data-theme="dark"] .m3-btn-error:hover:not(:disabled) {
background-color: rgba(0, 0, 0, 0.03);
}
html[data-theme="dark"] .m3-segment-item:hover:not(.active):not(.disabled) {
html[data-theme^="dark"] .m3-segment-item:hover:not(.active):not(.disabled) {
background-color: rgba(255, 255, 255, 0.04);
}
@@ -2458,7 +2737,7 @@ html[data-theme="dark"] .m3-segment-item:hover:not(.active):not(.disabled) {
border-color: rgba(15, 23, 42, 0.12);
}
html[data-theme="dark"] .collapsible-card:hover {
html[data-theme^="dark"] .collapsible-card:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
border-color: rgba(255, 255, 255, 0.08);
}
@@ -2726,7 +3005,7 @@ html[data-theme="dark"] .collapsible-card:hover {
border-color: rgba(15, 23, 42, 0.12);
}
html[data-theme="dark"] .settings-subsection:hover {
html[data-theme^="dark"] .settings-subsection:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
border-color: rgba(255, 255, 255, 0.08);
}
+2
View File
@@ -568,6 +568,8 @@ function App() {
copyDiagnostics={copyDiagnostics}
cleanupSafeItems={cleanupSafeItems}
diagnosticsCopied={diagnosticsCopied}
theme={theme}
setTheme={setTheme}
/>
</div>
</div>
+82 -1
View File
@@ -3,9 +3,10 @@ import {
Crop, Sliders, Cpu, Info, MessageSquare, SlidersHorizontal, Zap,
ChevronDown, Image, Type, Settings2, Gauge, Brain, Sparkles,
Monitor, HardDrive, MemoryStick, Thermometer, Hash, Layers,
ChevronRight, Box, Wand2, Lightbulb, RotateCcw, Check
ChevronRight, Box, Wand2, Lightbulb, RotateCcw, Check, Palette
} from "lucide-react";
import { stopServer, formatBytes, getLlmStatus } from "../services/api";
import { THEMES } from "../themes";
const ASPECT_RATIOS = [
{ id: "1:1", label: "1:1 Square", width: 512, height: 512, sdxl_width: 1024, sdxl_height: 1024, desc: "Social posts & avatars" },
@@ -182,6 +183,8 @@ function Settings({
copyDiagnostics,
cleanupSafeItems,
diagnosticsCopied,
theme,
setTheme,
}) {
const [llmStatus, setLlmStatus] = useState({ ready: false, settings: {} });
@@ -843,6 +846,81 @@ function Settings({
</>
);
// ─── Appearance Settings ───
const AppearanceSettings = () => (
<>
<SectionHeader
icon={Palette}
title="Appearance & Themes"
count={THEMES.length}
color="var(--md-sys-color-primary)"
/>
<div className="settings-subsection" style={{ marginBottom: "28px" }}>
<div className="settings-subsection-title">
<Palette size={16} />
Color Themes
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
gap: "14px",
marginTop: "14px"
}}>
{THEMES.map((t) => {
const isActive = theme === t.id;
return (
<button
key={t.id}
onClick={() => setTheme(t.id)}
className={`theme-card-btn ${isActive ? "active" : ""}`}
style={{
background: t.bg,
color: t.type === "dark" ? "#f4f4f5" : "#0f172a",
border: isActive ? "2px solid var(--md-sys-color-primary)" : "1px solid var(--border-color)",
borderRadius: "12px",
padding: "16px",
textAlign: "left",
cursor: "pointer",
position: "relative",
display: "flex",
flexDirection: "column",
gap: "12px",
boxShadow: isActive ? "0 4px 12px rgba(0,0,0,0.15)" : "none",
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 600, fontSize: "0.85rem" }}>{t.name}</span>
{isActive && (
<div style={{
background: "var(--md-sys-color-primary)",
color: "var(--md-sys-color-on-primary)",
borderRadius: "50%",
width: "18px",
height: "18px",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<Check size={12} />
</div>
)}
</div>
{/* Preview circles for primary and secondary colors */}
<div style={{ display: "flex", gap: "6px" }}>
<div style={{ width: "24px", height: "24px", borderRadius: "50%", background: t.primary, border: "1px solid rgba(255,255,255,0.2)" }} title="Primary" />
<div style={{ width: "24px", height: "24px", borderRadius: "50%", background: t.secondary, border: "1px solid rgba(255,255,255,0.2)" }} title="Secondary" />
<div style={{ width: "24px", height: "24px", borderRadius: "50%", background: t.bg, border: "1px solid rgba(0,0,0,0.15)" }} title="Background" />
</div>
</button>
);
})}
</div>
</div>
</>
);
return (
<div className="workspace-area">
{/* Page Header */}
@@ -861,6 +939,9 @@ function Settings({
{/* Hardware Tier Badge */}
<HardwareTierBadge specs={specs} />
{/* Appearance & Themes Section */}
<AppearanceSettings />
{/* Image Settings Section */}
<ImageSettings />
+62 -6
View File
@@ -1,7 +1,21 @@
import React, { memo } from "react";
import { Cpu, HardDrive, Database, Square, RefreshCw, Sun, Moon } from "lucide-react";
import React, { memo, useState, useRef, useEffect } from "react";
import { Cpu, HardDrive, Database, Square, RefreshCw, Sun, Moon, Palette, Check } from "lucide-react";
import { THEMES } from "../themes";
function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isStoppingServer = false, theme, setTheme }) {
const [showThemeMenu, setShowThemeMenu] = useState(false);
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShowThemeMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const formatGb = (value) => {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number.toFixed(number >= 10 ? 0 : 1) : "--";
@@ -17,6 +31,8 @@ function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isS
return "status-indicator offline";
};
const isDark = (theme || "dark").startsWith("dark");
return (
<div className="top-status-bar">
<div className="current-model-info">
@@ -34,15 +50,55 @@ function TopStatusBar({ telemetry, serverRunning, activeModel, onStopServer, isS
)}
</div>
<div className="telemetry-group">
<div className="telemetry-group" style={{ position: "relative" }}>
<button
className="theme-toggle-btn"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
title={`Switch to ${theme === "dark" ? "light" : "dark"} theme`}
onClick={() => setTheme(isDark ? "light" : "dark")}
title={`Switch to ${isDark ? "light" : "dark"} theme`}
>
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
{isDark ? <Sun size={18} /> : <Moon size={18} />}
</button>
<div ref={menuRef} style={{ position: "relative", display: "inline-block" }}>
<button
className={`theme-toggle-btn ${showThemeMenu ? "active" : ""}`}
onClick={() => setShowThemeMenu(!showThemeMenu)}
title="Choose a custom color theme"
style={{ marginRight: "12px" }}
>
<Palette size={18} />
</button>
{showThemeMenu && (
<div className="theme-dropdown-menu">
<div className="theme-dropdown-header">Select Theme</div>
<div className="theme-dropdown-divider"></div>
{THEMES.map((t) => {
const isActive = theme === t.id;
return (
<button
key={t.id}
className={`theme-dropdown-item ${isActive ? "active" : ""}`}
onClick={() => {
setTheme(t.id);
setShowThemeMenu(false);
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "8px", width: "100%" }}>
<div style={{ display: "flex", gap: "2px" }}>
<div style={{ width: "8px", height: "8px", borderRadius: "50%", background: t.primary }} />
<div style={{ width: "8px", height: "8px", borderRadius: "50%", background: t.secondary }} />
</div>
<span className="theme-name-text">{t.name}</span>
{isActive && <Check size={12} style={{ marginLeft: "auto", color: "var(--md-sys-color-primary)" }} />}
</div>
</button>
);
})}
</div>
)}
</div>
{serverRunning && (
<button
className="m3-btn m3-btn-error"
+10
View File
@@ -0,0 +1,10 @@
export const THEMES = [
{ id: "light", name: "Light Default", type: "light", primary: "#4f46e5", secondary: "#0ea5e9", bg: "#f8fafc" },
{ id: "light-nord", name: "Nord Light", type: "light", primary: "#5e81ac", secondary: "#88c0d0", bg: "#eceff4" },
{ id: "light-sakura", name: "Sakura Blossom", type: "light", primary: "#d05a74", secondary: "#e597a7", bg: "#fdf6f6" },
{ id: "dark", name: "Dark Default", type: "dark", primary: "#6366f1", secondary: "#38bdf8", bg: "#090a0f" },
{ id: "dark-dracula", name: "Dracula Classic", type: "dark", primary: "#bd93f9", secondary: "#ff79c6", bg: "#282a36" },
{ id: "dark-cyberpunk", name: "Cyberpunk Neon", type: "dark", primary: "#ff007f", secondary: "#00f0ff", bg: "#0d0714" },
{ id: "dark-forest", name: "Emerald Forest", type: "dark", primary: "#10b981", secondary: "#34d399", bg: "#060f0e" },
{ id: "dark-oled", name: "OLED Pitch Black", type: "dark", primary: "#3b82f6", secondary: "#60a5fa", bg: "#000000" }
];