feat: implement theme engine with multiple color presets and add top status bar for system telemetry
This commit is contained in:
1 parent
24a5c5a1df
commit
e6836467c3
5 files changed
+440
-12
No files matched your search
+284
-5
@@ -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);
|
||||
}
|
||||
|
||||
@@ -568,6 +568,8 @@ function App() {
|
||||
copyDiagnostics={copyDiagnostics}
|
||||
cleanupSafeItems={cleanupSafeItems}
|
||||
diagnosticsCopied={diagnosticsCopied}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
];
|
||||
Reference in new issue
Block a user