agent-orchestrator/ui-dioxus/src/theme.rs
Hibryda f3d2ca78ba feat: add Dioxus and GPUI UI prototypes for framework comparison
Dioxus (ui-dioxus/): 2,169 lines, WebView mode (same wry as Tauri),
  Catppuccin theme, 12 components, agor-core integration, compiles clean.
  Evolution path — keeps xterm.js, gradual migration from Tauri.

GPUI (ui-gpui/): 2,490 lines, GPU-accelerated rendering, alacritty_terminal
  for native terminal, 17 files, Catppuccin palette, demo data.
  Revolution path — pure Rust UI, 120fps target, no WebView.

Both are standalone (not in workspace), share agor-core backend.
Created for side-by-side comparison to inform framework decision.
2026-03-19 06:05:58 +01:00

760 lines
17 KiB
Rust

/// Catppuccin Mocha palette + CSS custom property generation.
///
/// All UI colors flow through --ctp-* CSS custom properties.
/// Components NEVER hardcode colors — they reference these variables.
/// Catppuccin Mocha color palette — hex values.
pub struct CatppuccinMocha;
impl CatppuccinMocha {
// Base colors
pub const BASE: &str = "#1e1e2e";
pub const MANTLE: &str = "#181825";
pub const CRUST: &str = "#11111b";
// Surface colors
pub const SURFACE0: &str = "#313244";
pub const SURFACE1: &str = "#45475a";
pub const SURFACE2: &str = "#585b70";
// Overlay colors
pub const OVERLAY0: &str = "#6c7086";
pub const OVERLAY1: &str = "#7f849c";
pub const OVERLAY2: &str = "#9399b2";
// Text colors
pub const SUBTEXT0: &str = "#a6adc8";
pub const SUBTEXT1: &str = "#bac2de";
pub const TEXT: &str = "#cdd6f4";
// Accent colors
pub const ROSEWATER: &str = "#f5e0dc";
pub const FLAMINGO: &str = "#f2cdcd";
pub const PINK: &str = "#f5c2e7";
pub const MAUVE: &str = "#cba6f7";
pub const RED: &str = "#f38ba8";
pub const MAROON: &str = "#eba0ac";
pub const PEACH: &str = "#fab387";
pub const YELLOW: &str = "#f9e2af";
pub const GREEN: &str = "#a6e3a1";
pub const TEAL: &str = "#94e2d5";
pub const SKY: &str = "#89dceb";
pub const SAPPHIRE: &str = "#74c7ec";
pub const BLUE: &str = "#89b4fa";
pub const LAVENDER: &str = "#b4befe";
}
/// Generate complete CSS stylesheet with Catppuccin Mocha custom properties
/// and base layout styles.
pub fn generate_css() -> String {
format!(
r#"
/* === Catppuccin Mocha CSS Custom Properties === */
:root {{
/* Base */
--ctp-base: {base};
--ctp-mantle: {mantle};
--ctp-crust: {crust};
/* Surface */
--ctp-surface0: {surface0};
--ctp-surface1: {surface1};
--ctp-surface2: {surface2};
/* Overlay */
--ctp-overlay0: {overlay0};
--ctp-overlay1: {overlay1};
--ctp-overlay2: {overlay2};
/* Text */
--ctp-subtext0: {subtext0};
--ctp-subtext1: {subtext1};
--ctp-text: {text};
/* Accents */
--ctp-rosewater: {rosewater};
--ctp-flamingo: {flamingo};
--ctp-pink: {pink};
--ctp-mauve: {mauve};
--ctp-red: {red};
--ctp-maroon: {maroon};
--ctp-peach: {peach};
--ctp-yellow: {yellow};
--ctp-green: {green};
--ctp-teal: {teal};
--ctp-sky: {sky};
--ctp-sapphire: {sapphire};
--ctp-blue: {blue};
--ctp-lavender: {lavender};
/* Typography */
--ui-font-family: 'Inter', system-ui, -apple-system, sans-serif;
--ui-font-size: 0.875rem;
--term-font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
--term-font-size: 0.8125rem;
}}
/* === Global Reset === */
*, *::before, *::after {{
box-sizing: border-box;
margin: 0;
padding: 0;
}}
body {{
font-family: var(--ui-font-family);
font-size: var(--ui-font-size);
color: var(--ctp-text);
background: var(--ctp-base);
overflow: hidden;
height: 100vh;
width: 100vw;
}}
#main {{
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
}}
/* === Scrollbar Styling === */
::-webkit-scrollbar {{
width: 0.5rem;
height: 0.5rem;
}}
::-webkit-scrollbar-track {{
background: var(--ctp-mantle);
}}
::-webkit-scrollbar-thumb {{
background: var(--ctp-surface1);
border-radius: 0.25rem;
}}
::-webkit-scrollbar-thumb:hover {{
background: var(--ctp-surface2);
}}
/* === Layout Classes === */
.app-shell {{
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}}
.app-body {{
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
}}
/* === Sidebar === */
.sidebar-rail {{
width: 2.75rem;
background: var(--ctp-crust);
border-right: 1px solid var(--ctp-surface0);
display: flex;
flex-direction: column;
align-items: center;
padding-top: 0.75rem;
gap: 0.5rem;
flex-shrink: 0;
}}
.sidebar-icon {{
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.375rem;
cursor: pointer;
color: var(--ctp-overlay1);
transition: all 0.15s ease;
font-size: 1rem;
}}
.sidebar-icon:hover {{
background: var(--ctp-surface0);
color: var(--ctp-text);
}}
.sidebar-icon.active {{
background: var(--ctp-surface0);
color: var(--ctp-blue);
}}
.sidebar-spacer {{
flex: 1;
}}
/* === Drawer Panel === */
.drawer-panel {{
width: 18rem;
background: var(--ctp-mantle);
border-right: 1px solid var(--ctp-surface0);
overflow-y: auto;
padding: 1rem;
flex-shrink: 0;
}}
.drawer-title {{
font-size: 0.8125rem;
font-weight: 600;
color: var(--ctp-subtext1);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}}
/* === Project Grid === */
.project-grid {{
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(28rem, 1fr));
gap: 0.75rem;
padding: 0.75rem;
overflow-y: auto;
align-content: start;
}}
/* === Project Box === */
.project-box {{
background: var(--ctp-mantle);
border: 1px solid var(--ctp-surface0);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
min-height: 20rem;
max-height: 40rem;
overflow: hidden;
}}
.project-header {{
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--ctp-crust);
border-bottom: 1px solid var(--ctp-surface0);
border-radius: 0.5rem 0.5rem 0 0;
flex-shrink: 0;
}}
.project-header .status-dot {{
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}}
.status-dot.running {{
background: var(--ctp-green);
box-shadow: 0 0 6px var(--ctp-green);
animation: pulse 2s ease-in-out infinite;
}}
.status-dot.idle {{
background: var(--ctp-overlay0);
}}
.status-dot.stalled {{
background: var(--ctp-peach);
animation: pulse 1.5s ease-in-out infinite;
}}
.status-dot.error {{
background: var(--ctp-red);
}}
@keyframes pulse {{
0%, 100% {{ opacity: 1; }}
50% {{ opacity: 0.5; }}
}}
.project-name {{
font-weight: 600;
font-size: 0.875rem;
color: var(--ctp-text);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}}
.project-cwd {{
font-size: 0.6875rem;
color: var(--ctp-overlay0);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
direction: rtl;
max-width: 12rem;
}}
/* === Tab Bar === */
.tab-bar {{
display: flex;
gap: 0;
background: var(--ctp-crust);
border-bottom: 1px solid var(--ctp-surface0);
flex-shrink: 0;
}}
.tab {{
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
color: var(--ctp-overlay1);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.15s ease;
user-select: none;
}}
.tab:hover {{
color: var(--ctp-text);
background: var(--ctp-surface0);
}}
.tab.active {{
color: var(--ctp-blue);
border-bottom-color: var(--ctp-blue);
}}
/* === Tab Content === */
.tab-content {{
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}}
/* === Agent Pane === */
.agent-pane {{
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}}
.message-list {{
flex: 1;
overflow-y: auto;
padding: 0.5rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}}
.message {{
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.8125rem;
line-height: 1.5;
word-wrap: break-word;
}}
.message.user {{
background: color-mix(in srgb, var(--ctp-blue) 12%, transparent);
border-left: 3px solid var(--ctp-blue);
}}
.message.assistant {{
background: color-mix(in srgb, var(--ctp-mauve) 8%, transparent);
border-left: 3px solid var(--ctp-mauve);
}}
.message.tool {{
background: color-mix(in srgb, var(--ctp-teal) 8%, transparent);
border-left: 3px solid var(--ctp-teal);
font-family: var(--term-font-family);
font-size: 0.75rem;
}}
.message-role {{
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}}
.message.user .message-role {{ color: var(--ctp-blue); }}
.message.assistant .message-role {{ color: var(--ctp-mauve); }}
.message.tool .message-role {{ color: var(--ctp-teal); }}
.message-text {{
color: var(--ctp-text);
}}
.tool-details {{
margin-top: 0.25rem;
}}
.tool-details summary {{
cursor: pointer;
color: var(--ctp-overlay1);
font-size: 0.6875rem;
user-select: none;
}}
.tool-details summary:hover {{
color: var(--ctp-text);
}}
.tool-output {{
margin-top: 0.375rem;
padding: 0.375rem 0.5rem;
background: var(--ctp-crust);
border-radius: 0.25rem;
font-family: var(--term-font-family);
font-size: 0.6875rem;
color: var(--ctp-subtext0);
white-space: pre-wrap;
max-height: 10rem;
overflow-y: auto;
}}
/* === Prompt Input === */
.prompt-bar {{
display: flex;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--ctp-crust);
border-top: 1px solid var(--ctp-surface0);
align-items: center;
flex-shrink: 0;
}}
.prompt-input {{
flex: 1;
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.375rem;
padding: 0.4375rem 0.75rem;
color: var(--ctp-text);
font-family: var(--ui-font-family);
font-size: 0.8125rem;
outline: none;
transition: border-color 0.15s ease;
}}
.prompt-input:focus {{
border-color: var(--ctp-blue);
}}
.prompt-input::placeholder {{
color: var(--ctp-overlay0);
}}
.prompt-send {{
background: var(--ctp-blue);
color: var(--ctp-crust);
border: none;
border-radius: 0.375rem;
padding: 0.4375rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s ease;
}}
.prompt-send:hover {{
opacity: 0.85;
}}
.prompt-send:disabled {{
opacity: 0.4;
cursor: not-allowed;
}}
/* === Agent Status Strip === */
.agent-status {{
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
background: var(--ctp-crust);
border-top: 1px solid var(--ctp-surface0);
font-size: 0.6875rem;
color: var(--ctp-overlay1);
flex-shrink: 0;
}}
.status-badge {{
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-weight: 600;
text-transform: uppercase;
font-size: 0.625rem;
letter-spacing: 0.05em;
}}
.status-badge.idle {{
background: color-mix(in srgb, var(--ctp-overlay0) 20%, transparent);
color: var(--ctp-overlay1);
}}
.status-badge.running {{
background: color-mix(in srgb, var(--ctp-green) 20%, transparent);
color: var(--ctp-green);
}}
.status-badge.done {{
background: color-mix(in srgb, var(--ctp-blue) 20%, transparent);
color: var(--ctp-blue);
}}
.status-badge.error {{
background: color-mix(in srgb, var(--ctp-red) 20%, transparent);
color: var(--ctp-red);
}}
/* === Terminal Area === */
.terminal-area {{
background: var(--ctp-crust);
border-top: 1px solid var(--ctp-surface0);
height: 8rem;
overflow-y: auto;
padding: 0.5rem;
font-family: var(--term-font-family);
font-size: var(--term-font-size);
color: var(--ctp-green);
flex-shrink: 0;
}}
.terminal-line {{
line-height: 1.4;
white-space: pre-wrap;
}}
.terminal-prompt {{
color: var(--ctp-blue);
}}
.terminal-output {{
color: var(--ctp-subtext0);
}}
/* === Status Bar === */
.status-bar {{
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0.75rem;
background: var(--ctp-crust);
border-top: 1px solid var(--ctp-surface0);
font-size: 0.6875rem;
color: var(--ctp-overlay1);
flex-shrink: 0;
height: 1.5rem;
}}
.status-bar-left,
.status-bar-right {{
display: flex;
align-items: center;
gap: 1rem;
}}
.status-item {{
display: flex;
align-items: center;
gap: 0.25rem;
}}
.status-count {{
font-weight: 600;
}}
.status-count.running {{ color: var(--ctp-green); }}
.status-count.idle {{ color: var(--ctp-overlay1); }}
.status-count.stalled {{ color: var(--ctp-peach); }}
.status-cost {{
color: var(--ctp-yellow);
font-weight: 600;
}}
/* === Settings Panel === */
.settings-section {{
margin-bottom: 1.5rem;
}}
.settings-section-title {{
font-size: 0.75rem;
font-weight: 600;
color: var(--ctp-subtext0);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
padding-bottom: 0.375rem;
border-bottom: 1px solid var(--ctp-surface0);
}}
.settings-row {{
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.375rem 0;
font-size: 0.8125rem;
}}
.settings-label {{
color: var(--ctp-subtext1);
}}
.settings-value {{
color: var(--ctp-text);
font-weight: 500;
}}
.settings-select {{
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
color: var(--ctp-text);
font-size: 0.75rem;
outline: none;
}}
.settings-select:focus {{
border-color: var(--ctp-blue);
}}
/* === Command Palette === */
.palette-overlay {{
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 20vh;
z-index: 1000;
}}
.palette-box {{
width: 32rem;
max-width: 90vw;
background: var(--ctp-mantle);
border: 1px solid var(--ctp-surface0);
border-radius: 0.5rem;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.4);
overflow: hidden;
}}
.palette-input {{
width: 100%;
padding: 0.75rem 1rem;
background: transparent;
border: none;
border-bottom: 1px solid var(--ctp-surface0);
color: var(--ctp-text);
font-family: var(--ui-font-family);
font-size: 0.9375rem;
outline: none;
}}
.palette-input::placeholder {{
color: var(--ctp-overlay0);
}}
.palette-results {{
max-height: 20rem;
overflow-y: auto;
}}
.palette-item {{
padding: 0.5rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
transition: background 0.1s ease;
}}
.palette-item:hover, .palette-item.selected {{
background: var(--ctp-surface0);
}}
.palette-item-icon {{
color: var(--ctp-overlay1);
font-size: 0.875rem;
width: 1.25rem;
text-align: center;
}}
.palette-item-label {{
color: var(--ctp-text);
font-size: 0.8125rem;
}}
.palette-item-shortcut {{
margin-left: auto;
font-size: 0.6875rem;
color: var(--ctp-overlay0);
font-family: var(--term-font-family);
}}
/* === Docs Tab === */
.docs-pane {{
padding: 0.75rem;
overflow-y: auto;
flex: 1;
}}
.docs-entry {{
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
color: var(--ctp-subtext1);
font-size: 0.8125rem;
transition: background 0.1s ease;
}}
.docs-entry:hover {{
background: var(--ctp-surface0);
color: var(--ctp-text);
}}
/* === Files Tab === */
.files-pane {{
padding: 0.75rem;
overflow-y: auto;
flex: 1;
}}
.file-tree-item {{
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
color: var(--ctp-subtext1);
font-size: 0.75rem;
font-family: var(--term-font-family);
}}
.file-tree-item:hover {{
background: var(--ctp-surface0);
color: var(--ctp-text);
}}
.file-tree-icon {{
color: var(--ctp-overlay0);
width: 1rem;
text-align: center;
}}
/* === Provider Badge === */
.provider-badge {{
font-size: 0.5625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.0625rem 0.3125rem;
border-radius: 0.1875rem;
background: color-mix(in srgb, var(--ctp-mauve) 15%, transparent);
color: var(--ctp-mauve);
}}
"#,
base = CatppuccinMocha::BASE,
mantle = CatppuccinMocha::MANTLE,
crust = CatppuccinMocha::CRUST,
surface0 = CatppuccinMocha::SURFACE0,
surface1 = CatppuccinMocha::SURFACE1,
surface2 = CatppuccinMocha::SURFACE2,
overlay0 = CatppuccinMocha::OVERLAY0,
overlay1 = CatppuccinMocha::OVERLAY1,
overlay2 = CatppuccinMocha::OVERLAY2,
subtext0 = CatppuccinMocha::SUBTEXT0,
subtext1 = CatppuccinMocha::SUBTEXT1,
text = CatppuccinMocha::TEXT,
rosewater = CatppuccinMocha::ROSEWATER,
flamingo = CatppuccinMocha::FLAMINGO,
pink = CatppuccinMocha::PINK,
mauve = CatppuccinMocha::MAUVE,
red = CatppuccinMocha::RED,
maroon = CatppuccinMocha::MAROON,
peach = CatppuccinMocha::PEACH,
yellow = CatppuccinMocha::YELLOW,
green = CatppuccinMocha::GREEN,
teal = CatppuccinMocha::TEAL,
sky = CatppuccinMocha::SKY,
sapphire = CatppuccinMocha::SAPPHIRE,
blue = CatppuccinMocha::BLUE,
lavender = CatppuccinMocha::LAVENDER,
)
}