refactor(web): redesign frontend for native web UX
All checks were successful
Beta Release / beta (push) Successful in 33s
All checks were successful
Beta Release / beta (push) Successful in 33s
- Replace all TUI artifacts: [OK], [FAIL], >>, [■], [$] with proper web components (badges, cards, chips, avatars) - Rename CSS variables from TUI names (cyberRed, dimRed, bgVoid) to semantic names (accent, accent-dim, bg) - Add proper interactive elements: hover states, cursor pointer, click feedback (scale), focus rings, spinner animation - Fix user-select: was none globally, now allows text selection - Redesign navigation: proper tabs with role="tab" and aria attributes - Add keyboard shortcuts only when not in input/textarea (1-4 for tabs) - Replace footer TUI shortcuts with clean statusbar - Dashboard: card-based layout, badge status, progress bar, activity log - Studio: message bubbles (aligned left/right), agent cards with avatars - Shell: command history (ArrowUp/Down), toggleable AI panel button, panel header with current directory - Config: provider cards, color swatches for theme picker, clean field rows with empty states - CSS imported via main.jsx (not HTML link) for proper Vite hashing - Remove glitch/scanline/typewriter TUI animations - Add favicon 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -3,8 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0A0A0C" />
|
||||||
<title>muyue</title>
|
<title>muyue</title>
|
||||||
<link rel="stylesheet" href="/src/styles/global.css" />
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⬡</text></svg>" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import Shell from './Shell'
|
|||||||
import Config from './Config'
|
import Config from './Config'
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'dash', label: 'DASH', icon: '[■]' },
|
{ id: 'dash', label: 'Dashboard', icon: '\u25A0' },
|
||||||
{ id: 'studio', label: 'STUDIO', icon: '[<>]' },
|
{ id: 'studio', label: 'Studio', icon: '\u27E8\u27E9' },
|
||||||
{ id: 'shell', label: 'SHELL', icon: '[$]' },
|
{ id: 'shell', label: 'Shell', icon: '$' },
|
||||||
{ id: 'config', label: 'CONFIG', icon: '[//]' },
|
{ id: 'config', label: 'Config', icon: '\u2699' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -19,41 +19,43 @@ export default function App() {
|
|||||||
const [clock, setClock] = useState(new Date())
|
const [clock, setClock] = useState(new Date())
|
||||||
const [updates, setUpdates] = useState([])
|
const [updates, setUpdates] = useState([])
|
||||||
const [tools, setTools] = useState([])
|
const [tools, setTools] = useState([])
|
||||||
const [transition, setTransition] = useState(false)
|
|
||||||
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
|
||||||
// api is imported directly
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getInfo().then(setInfo).catch(() => {})
|
api.getInfo().then(setInfo).catch(() => {})
|
||||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||||
|
applyTheme(getTheme('cyberpunk-red'))
|
||||||
const theme = getTheme(currentTheme)
|
|
||||||
applyTheme(theme)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setClock(new Date()), 1000)
|
const id = setInterval(() => setClock(new Date()), 1000)
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const switchTab = useCallback((tabId) => {
|
useEffect(() => {
|
||||||
if (tabId === activeTab) return
|
const onKey = (e) => {
|
||||||
setTransition(true)
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||||
setTimeout(() => {
|
const map = { '1': 'dash', '2': 'studio', '3': 'shell', '4': 'config' }
|
||||||
setActiveTab(tabId)
|
if (map[e.key]) {
|
||||||
setTimeout(() => setTransition(false), 150)
|
e.preventDefault()
|
||||||
}, 100)
|
setActiveTab(map[e.key])
|
||||||
}, [activeTab])
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
|
||||||
|
|
||||||
const hasUpdates = updates.some(u => u.needsUpdate)
|
const hasUpdates = updates.some(u => u.needsUpdate)
|
||||||
|
const installed = tools.filter(t => t.installed).length
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={(t) => setTools(t)} />
|
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={t => setTools(t)} />
|
||||||
case 'studio': return <Studio api={api} />
|
case 'studio': return <Studio api={api} />
|
||||||
case 'shell': return <Shell api={api} />
|
case 'shell': return <Shell api={api} />
|
||||||
case 'config': return <Config api={api} theme={currentTheme} onThemeChange={setCurrentTheme} />
|
case 'config': return <Config api={api} onThemeChange={() => {}} />
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,44 +63,50 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="app-layout">
|
<div className="app-layout">
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<span className="header-logo">MUYUE</span>
|
<div className="header-brand">
|
||||||
<span className="header-version">v{info.version || '...'}</span>
|
<span className="header-logo">MUYUE</span>
|
||||||
|
<span className="header-version">v{info.version || '...'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="header-tabs">
|
<nav className="header-nav">
|
||||||
{TABS.map(tab => (
|
{TABS.map(tab => (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
className={`header-tab ${activeTab === tab.id ? 'active' : ''}`}
|
className={`nav-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||||
onClick={() => switchTab(tab.id)}
|
onClick={() => switchTab(tab.id)}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === tab.id}
|
||||||
>
|
>
|
||||||
{tab.icon} {tab.label}
|
<span className="tab-icon">{tab.icon}</span>
|
||||||
|
{tab.label}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
<div className="header-spacer" />
|
<div className="header-spacer" />
|
||||||
|
|
||||||
<div className="header-status">
|
<div className="header-indicators">
|
||||||
<span className={`status-dot ${tools.length > 0 ? 'ok' : 'off'}`} title="System" />
|
<span className={`indicator ${installed > 0 ? 'ok' : 'off'}`} title={`${installed} tools installed`} />
|
||||||
<span className={`status-dot ${hasUpdates ? 'warn' : 'ok'}`} title="Updates" />
|
<span className={`indicator ${hasUpdates ? 'warn' : 'ok'}`} title={hasUpdates ? 'Updates available' : 'Up to date'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="header-date">{clock.toLocaleDateString('fr-FR')}</span>
|
<span className="header-clock">
|
||||||
<span className="header-clock">{clock.toLocaleTimeString('fr-FR')}</span>
|
{clock.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className={`content ${transition ? 'glitch-text' : 'fade-in tab-transition'}`}>
|
<main className="content fade-in" key={activeTab}>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<footer className="footer">
|
<footer className="statusbar">
|
||||||
<span className="footer-shortcuts">
|
<div className="statusbar-left">
|
||||||
<kbd>1-4</kbd> tabs · <kbd>Ctrl+T</kbd> switcher · <kbd>Ctrl+C</kbd> quit
|
<span>Press 1-4 to switch tabs</span>
|
||||||
</span>
|
</div>
|
||||||
<span className={`footer-update ${hasUpdates ? 'available' : 'uptodate'}`}>
|
<div className="statusbar-right">
|
||||||
{hasUpdates ? '[UPD] Updates available' : '[OK] Up to date'}
|
{hasUpdates && <span style={{ color: 'var(--warning)' }}>Updates available</span>}
|
||||||
</span>
|
<span>v{info.version || '...'}</span>
|
||||||
<span className="footer-version">v{info.version || '...'}</span>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { getThemeNames, applyTheme, getTheme } from '../themes'
|
import { getThemeNames, applyTheme, getTheme } from '../themes'
|
||||||
|
|
||||||
export default function Config({ api, theme, onThemeChange }) {
|
export default function Config({ api }) {
|
||||||
const [config, setConfig] = useState(null)
|
const [config, setConfig] = useState(null)
|
||||||
const [providers, setProviders] = useState([])
|
const [providers, setProviders] = useState([])
|
||||||
const [skillList, setSkillList] = useState([])
|
const [skillList, setSkillList] = useState([])
|
||||||
|
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getConfig().then(d => setConfig(d)).catch(() => {})
|
api.getConfig().then(d => setConfig(d)).catch(() => {})
|
||||||
@@ -15,71 +16,84 @@ export default function Config({ api, theme, onThemeChange }) {
|
|||||||
const themes = getThemeNames()
|
const themes = getThemeNames()
|
||||||
|
|
||||||
const handleThemeChange = (themeId) => {
|
const handleThemeChange = (themeId) => {
|
||||||
const t = getTheme(themeId)
|
applyTheme(getTheme(themeId))
|
||||||
applyTheme(t)
|
setCurrentTheme(themeId)
|
||||||
onThemeChange(themeId)
|
}
|
||||||
|
|
||||||
|
const themeColors = {
|
||||||
|
'cyberpunk-red': '#FF0033',
|
||||||
|
'cyberpunk-pink': '#FF1A8C',
|
||||||
|
'midnight-blue': '#0088FF',
|
||||||
|
'matrix-green': '#00FF41',
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-container">
|
<div className="config-layout">
|
||||||
<div className="config-section">
|
<div className="config-section">
|
||||||
<div className="section-header">Profile</div>
|
<div className="config-section-title">Profile</div>
|
||||||
{config?.profile && (
|
{config?.profile ? (
|
||||||
<div>
|
<div>
|
||||||
<Field label="Name" value={config.profile.name} />
|
<FieldRow label="Name" value={config.profile.name} />
|
||||||
<Field label="Pseudo" value={config.profile.pseudo} />
|
<FieldRow label="Pseudo" value={config.profile.pseudo} />
|
||||||
<Field label="Email" value={config.profile.email} />
|
<FieldRow label="Email" value={config.profile.email} />
|
||||||
<Field label="Editor" value={config.profile.preferences?.editor} />
|
<FieldRow label="Editor" value={config.profile.preferences?.editor} />
|
||||||
<Field label="Shell" value={config.profile.preferences?.shell} />
|
<FieldRow label="Shell" value={config.profile.preferences?.shell} />
|
||||||
<Field label="Default AI" value={config.profile.preferences?.defaultAI} />
|
<FieldRow label="Default AI" value={config.profile.preferences?.defaultAI} />
|
||||||
<Field label="Languages" value={config.profile.languages?.join(', ')} />
|
<FieldRow label="Languages" value={config.profile.languages?.join(', ')} />
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">Loading profile...</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="config-section">
|
<div className="config-section">
|
||||||
<div className="section-header">AI Providers</div>
|
<div className="config-section-title">AI Providers</div>
|
||||||
{providers.map((p, i) => (
|
{providers.map((p, i) => (
|
||||||
<div key={i} className="config-field" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 4 }}>
|
<div key={i} className="provider-card">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div className="provider-info">
|
||||||
<span style={{ color: 'var(--text-bright)', fontWeight: 700 }}>{p.name}</span>
|
<div className="provider-name">
|
||||||
{p.active && <span style={{ color: 'var(--cyber-red)', fontSize: 11, fontWeight: 700 }}>{'>>'}</span>}
|
{p.name}
|
||||||
</div>
|
{p.active && <span className="badge accent" style={{ marginLeft: 8 }}>Active</span>}
|
||||||
<div style={{ display: 'flex', gap: 16, fontSize: 12 }}>
|
</div>
|
||||||
<span style={{ color: 'var(--dim-red)' }}>model={p.model}</span>
|
<div className="provider-meta">
|
||||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
<span>{p.model}</span>
|
||||||
key={p.apiKey ? 'configured' : 'no key'}
|
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||||
</span>
|
{p.apiKey ? 'Key configured' : 'No key'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="config-section">
|
<div className="config-section">
|
||||||
<div className="section-header">Theme</div>
|
<div className="config-section-title">Theme</div>
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
<div className="theme-picker">
|
||||||
{themes.map(t => (
|
{themes.map(t => (
|
||||||
<button
|
<div
|
||||||
key={t.id}
|
key={t.id}
|
||||||
className={theme === t.id ? 'primary' : ''}
|
className={`theme-swatch ${currentTheme === t.id ? 'active' : ''}`}
|
||||||
|
style={{ background: themeColors[t.id] || '#FF0033' }}
|
||||||
onClick={() => handleThemeChange(t.id)}
|
onClick={() => handleThemeChange(t.id)}
|
||||||
>
|
title={t.name}
|
||||||
{t.name}
|
/>
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="config-section">
|
<div className="config-section">
|
||||||
<div className="section-header">Skills ({skillList.length})</div>
|
<div className="config-section-title">Skills ({skillList.length})</div>
|
||||||
{skillList.length === 0 ? (
|
{skillList.length === 0 ? (
|
||||||
<span style={{ color: 'var(--text-muted)', fontSize: 12 }}>No skills. Run `muyue skills init`.</span>
|
<div className="empty-state">
|
||||||
|
No skills installed.
|
||||||
|
<span style={{ fontFamily: 'var(--font-mono)' }}>Run muyue skills init</span>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
skillList.map((s, i) => (
|
skillList.map((s, i) => (
|
||||||
<div key={i} className="tool-item">
|
<div key={i} className="tool-row">
|
||||||
<span className="tool-name">{s.name}</span>
|
<span className="tool-name">{s.name}</span>
|
||||||
<span style={{ color: 'var(--cyber-red)', fontSize: 11 }}>[{s.target || 'both'}]</span>
|
<span className={`badge neutral`}>{s.target || 'both'}</span>
|
||||||
<span style={{ color: 'var(--dim-red)', fontSize: 11 }}>{s.description}</span>
|
<span style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>{s.description}</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -88,11 +102,11 @@ export default function Config({ api, theme, onThemeChange }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Field({ label, value }) {
|
function FieldRow({ label, value }) {
|
||||||
return (
|
return (
|
||||||
<div className="config-field">
|
<div className="field-row">
|
||||||
<span className="config-label">{label}:</span>
|
<span className="field-label">{label}</span>
|
||||||
<span className="config-value">{value || '-'}</span>
|
<span className={`field-value ${!value ? 'empty' : ''}`}>{value || 'Not set'}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,129 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
export default function Dashboard({ tools, updates, api, onRescan }) {
|
export default function Dashboard({ tools, updates, api, onRescan }) {
|
||||||
const [installing, setInstalling] = useState(false)
|
const [installing, setInstalling] = useState(false)
|
||||||
const [installLog, setInstallLog] = useState([])
|
const [log, setLog] = useState([])
|
||||||
|
|
||||||
const installed = tools.filter(t => t.installed).length
|
const installed = tools.filter(t => t.installed).length
|
||||||
const total = tools.length
|
const total = tools.length
|
||||||
const pct = total > 0 ? (installed / total) * 100 : 0
|
const pct = total > 0 ? Math.round((installed / total) * 100) : 0
|
||||||
const missing = tools.filter(t => !t.installed).map(t => t.Name || t.name)
|
const missing = tools.filter(t => !t.installed).map(t => t.name || t.Name)
|
||||||
|
|
||||||
const handleInstall = async () => {
|
const handleInstall = async () => {
|
||||||
if (missing.length === 0) return
|
if (missing.length === 0) return
|
||||||
setInstalling(true)
|
setInstalling(true)
|
||||||
setInstallLog(prev => [...prev, { text: `Installing ${missing.length} tools...`, type: 'info' }])
|
addLog(`Installing ${missing.length} tools...`, 'info')
|
||||||
try {
|
try {
|
||||||
await api.installTools(missing)
|
await api.installTools(missing)
|
||||||
setInstallLog(prev => [...prev, { text: 'Install started. Rescan to see changes.', type: 'ok' }])
|
addLog('Install started. Rescanning...', 'ok')
|
||||||
const data = await api.runScan()
|
await api.runScan()
|
||||||
const toolData = await api.getTools()
|
const data = await api.getTools()
|
||||||
onRescan(toolData.tools || [])
|
onRescan(data.tools || [])
|
||||||
|
addLog('Done.', 'ok')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setInstallLog(prev => [...prev, { text: err.message, type: 'error' }])
|
addLog(err.message, 'error')
|
||||||
}
|
}
|
||||||
setInstalling(false)
|
setInstalling(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
|
addLog('Scanning system...', 'info')
|
||||||
await api.runScan()
|
await api.runScan()
|
||||||
const data = await api.getTools()
|
const data = await api.getTools()
|
||||||
onRescan(data.tools || [])
|
onRescan(data.tools || [])
|
||||||
|
addLog('Scan complete.', 'ok')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCheckUpdates = async () => {
|
||||||
|
const data = await api.getUpdates().catch(() => ({ updates: [] }))
|
||||||
|
const count = (data.updates || []).filter(u => u.needsUpdate).length
|
||||||
|
addLog(count > 0 ? `${count} updates available.` : 'All tools up to date.', count > 0 ? 'warn' : 'ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLog = (text, type) => setLog(prev => [...prev, { text, type, id: Date.now() }])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
<div style={{ overflow: 'auto' }}>
|
||||||
<div className="section-header">System</div>
|
<div className="card">
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div className="card-header">
|
||||||
<span style={{ color: 'var(--text-main)' }}>{installed}/{total} tools installed</span>
|
System Overview — {installed}/{total} tools ({pct}%)
|
||||||
</div>
|
</div>
|
||||||
|
<div className="progress" style={{ marginBottom: 16 }}>
|
||||||
<div className="section-header">Installed Tools</div>
|
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||||
<div style={{ marginBottom: 8 }}>
|
|
||||||
{tools.map((t, i) => (
|
|
||||||
<div key={i} className="tool-item">
|
|
||||||
<span className={`tool-status ${t.installed ? 'ok' : 'missing'}`}>
|
|
||||||
{t.installed ? '[OK]' : '[--]'}
|
|
||||||
</span>
|
|
||||||
<span className="tool-name">{t.Name || t.name}</span>
|
|
||||||
{(t.Version || t.version) && (
|
|
||||||
<span className="tool-version">{extractVersion(t.Version || t.version)}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="progress-bar" style={{ marginBottom: 16 }}>
|
|
||||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{installing && (
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
<span className="loading-spinner"> Installing...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{installLog.length > 0 && (
|
|
||||||
<div>
|
<div>
|
||||||
<div className="section-header">Install Log</div>
|
{tools.map((t, i) => {
|
||||||
{installLog.map((log, i) => (
|
const name = t.name || t.Name
|
||||||
<div key={i} style={{
|
const ver = extractVersion(t.Version || t.version)
|
||||||
color: log.type === 'error' ? 'var(--error)' :
|
return (
|
||||||
log.type === 'ok' ? 'var(--success)' : 'var(--text-dim)',
|
<div key={i} className="tool-row">
|
||||||
fontSize: 12, padding: '2px 0'
|
<span className={`badge ${t.installed ? 'ok' : 'error'}`}>
|
||||||
|
{t.installed ? 'Installed' : 'Missing'}
|
||||||
|
</span>
|
||||||
|
<span className="tool-name">{name}</span>
|
||||||
|
{ver && <span className="tool-version">{ver}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">Quick Actions</div>
|
||||||
|
<div className="actions-stack">
|
||||||
|
<button onClick={handleInstall} disabled={installing || missing.length === 0}>
|
||||||
|
{installing && <span className="spinner" />}
|
||||||
|
Install missing ({missing.length})
|
||||||
|
</button>
|
||||||
|
<button onClick={handleCheckUpdates}>Check for updates</button>
|
||||||
|
<button onClick={handleScan}>Rescan system</button>
|
||||||
|
<button onClick={() => api.configureMCP().then(() => addLog('MCP configured.', 'ok'))}>
|
||||||
|
Configure MCP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card" style={{ flex: 1 }}>
|
||||||
|
<div className="card-header">Updates</div>
|
||||||
|
{updates.length === 0 ? (
|
||||||
|
<div className="empty-state">No update data yet.</div>
|
||||||
|
) : (
|
||||||
|
updates.map((u, i) => (
|
||||||
|
<div key={i} className="tool-row">
|
||||||
|
<span className={`badge ${u.needsUpdate ? 'warn' : 'ok'}`}>
|
||||||
|
{u.needsUpdate ? 'Update' : 'Latest'}
|
||||||
|
</span>
|
||||||
|
<span className="tool-name">{u.tool}</span>
|
||||||
|
{u.needsUpdate && (
|
||||||
|
<span style={{ color: 'var(--warning)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
|
||||||
|
{u.current} → {u.latest}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{log.length > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">Activity Log</div>
|
||||||
|
{log.map(entry => (
|
||||||
|
<div key={entry.id} style={{
|
||||||
|
fontSize: 12,
|
||||||
|
padding: '4px 0',
|
||||||
|
color: entry.type === 'error' ? 'var(--error)' :
|
||||||
|
entry.type === 'warn' ? 'var(--warning)' :
|
||||||
|
entry.type === 'ok' ? 'var(--success)' : 'var(--text-tertiary)',
|
||||||
}}>
|
}}>
|
||||||
{log.text}
|
{entry.text}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
|
||||||
<div className="section-header">Quick Actions</div>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16 }}>
|
|
||||||
<button onClick={handleInstall} disabled={installing || missing.length === 0}>
|
|
||||||
[i] Install missing ({missing.length})
|
|
||||||
</button>
|
|
||||||
<button onClick={() => api.getUpdates().then(d => {})}> [u] Check updates</button>
|
|
||||||
<button onClick={handleScan}>[s] Rescan system</button>
|
|
||||||
<button onClick={() => api.configureMCP()}>[m] Configure MCP</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="section-header">Updates</div>
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
|
||||||
{updates.length === 0 ? (
|
|
||||||
<span style={{ color: 'var(--text-muted)' }}>No update data yet</span>
|
|
||||||
) : updates.map((u, i) => (
|
|
||||||
<div key={i} className="tool-item">
|
|
||||||
<span className={`tool-status ${u.needsUpdate ? 'missing' : 'ok'}`}>
|
|
||||||
{u.needsUpdate ? '[!!]' : '[OK]'}
|
|
||||||
</span>
|
|
||||||
<span className="tool-name">{u.tool}</span>
|
|
||||||
{u.needsUpdate && (
|
|
||||||
<span style={{ color: 'var(--warning)', fontSize: 11 }}>
|
|
||||||
{u.current} → {u.latest}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ export default function Shell({ api }) {
|
|||||||
const [history, setHistory] = useState([])
|
const [history, setHistory] = useState([])
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [cwd, setCwd] = useState('~')
|
const [cwd, setCwd] = useState('~')
|
||||||
const [aiPanel, setAiPanel] = useState(true)
|
const [showAi, setShowAi] = useState(false)
|
||||||
const [aiMessages, setAiMessages] = useState([
|
const [aiMessages, setAiMessages] = useState([
|
||||||
{ role: 'ai', content: '>> I know your system inside out. Ask me anything.' }
|
{ role: 'ai', content: 'I know your system inside out. Ask me anything.' }
|
||||||
])
|
])
|
||||||
const [aiInput, setAiInput] = useState('')
|
const [aiInput, setAiInput] = useState('')
|
||||||
const [aiLoading, setAiLoading] = useState(false)
|
const [aiLoading, setAiLoading] = useState(false)
|
||||||
|
const [cmdHistory, setCmdHistory] = useState([])
|
||||||
|
const [histIdx, setHistIdx] = useState(-1)
|
||||||
const outputRef = useRef(null)
|
const outputRef = useRef(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -18,28 +20,22 @@ export default function Shell({ api }) {
|
|||||||
|
|
||||||
const handleCommand = async (cmd) => {
|
const handleCommand = async (cmd) => {
|
||||||
if (!cmd.trim()) return
|
if (!cmd.trim()) return
|
||||||
if (cmd === 'clear') {
|
if (cmd === 'clear') { setHistory([]); return }
|
||||||
setHistory([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (cmd === 'exit' || cmd === 'quit') return
|
|
||||||
|
|
||||||
setHistory(prev => [...prev, { type: 'input', text: `${cwd} $ ${cmd}` }])
|
setCmdHistory(prev => [...prev, cmd])
|
||||||
|
setHistIdx(-1)
|
||||||
|
setHistory(prev => [...prev, { type: 'cmd', text: `${cwd} $ ${cmd}` }])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api.runCommand(cmd, cwd === '~' ? '' : cwd)
|
const res = await api.runCommand(cmd, cwd === '~' ? '' : cwd)
|
||||||
if (res.output) {
|
if (res.output) setHistory(prev => [...prev, { type: 'out', text: res.output }])
|
||||||
setHistory(prev => [...prev, { type: 'output', text: res.output }])
|
if (res.error) setHistory(prev => [...prev, { type: 'err', text: res.error }])
|
||||||
}
|
|
||||||
if (res.error) {
|
|
||||||
setHistory(prev => [...prev, { type: 'error', text: res.error }])
|
|
||||||
}
|
|
||||||
if (cmd.startsWith('cd ')) {
|
if (cmd.startsWith('cd ')) {
|
||||||
const dir = cmd.slice(3).trim()
|
const dir = cmd.slice(3).trim()
|
||||||
setCwd(dir === '~' ? '~' : dir)
|
setCwd(dir === '~' ? '~' : dir)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setHistory(prev => [...prev, { type: 'error', text: err.message }])
|
setHistory(prev => [...prev, { type: 'err', text: err.message }])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,52 +44,60 @@ export default function Shell({ api }) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleCommand(input)
|
handleCommand(input)
|
||||||
setInput('')
|
setInput('')
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (cmdHistory.length === 0) return
|
||||||
|
const newIdx = histIdx === -1 ? cmdHistory.length - 1 : Math.max(0, histIdx - 1)
|
||||||
|
setHistIdx(newIdx)
|
||||||
|
setInput(cmdHistory[newIdx])
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (histIdx === -1) return
|
||||||
|
const newIdx = histIdx + 1
|
||||||
|
if (newIdx >= cmdHistory.length) { setHistIdx(-1); setInput('') }
|
||||||
|
else { setHistIdx(newIdx); setInput(cmdHistory[newIdx]) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAiSend = async () => {
|
const handleAiSend = async () => {
|
||||||
if (!aiInput.trim() || aiLoading) return
|
if (!aiInput.trim() || aiLoading) return
|
||||||
const text = aiInput.trim()
|
const text = aiInput.trim()
|
||||||
setAiMessages(prev => [...prev, { role: 'user', content: '>> ' + text }])
|
setAiMessages(prev => [...prev, { role: 'user', content: text }])
|
||||||
setAiInput('')
|
setAiInput('')
|
||||||
setAiLoading(true)
|
setAiLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
||||||
setAiMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || 'No response') }])
|
setAiMessages(prev => [...prev, { role: 'ai', content: res.output || 'No response' }])
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAiMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
setAiMessages(prev => [...prev, { role: 'ai', content: `Error: ${err.message}` }])
|
||||||
}
|
}
|
||||||
setAiLoading(false)
|
setAiLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="split-horizontal" style={{ height: '100%' }}>
|
<div className="split-horizontal" style={{ height: '100%' }}>
|
||||||
<div className="terminal-container" style={{ flex: 1 }}>
|
<div className="terminal" style={{ flex: 1 }}>
|
||||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-dim)' }}>
|
<div className="panel-header">
|
||||||
<div className="section-header" style={{ margin: 0 }}>
|
<span className="panel-title">
|
||||||
Terminal
|
Terminal
|
||||||
<span style={{ color: 'var(--dim-red)', fontWeight: 400, marginLeft: 12, fontSize: 11 }}>{cwd}</span>
|
<span className="panel-subtitle">{cwd}</span>
|
||||||
</div>
|
</span>
|
||||||
|
<button className="ghost sm" onClick={() => setShowAi(!showAi)}>
|
||||||
|
{showAi ? 'Hide AI' : 'AI Assistant'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="terminal-output" ref={outputRef}>
|
<div className="terminal-output" ref={outputRef}>
|
||||||
{history.map((line, i) => (
|
{history.map((line, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} className={`terminal-line ${line.type}`}>
|
||||||
color: line.type === 'input' ? 'var(--dim-red)' :
|
|
||||||
line.type === 'error' ? 'var(--error)' : 'var(--text-main)',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
fontSize: 13,
|
|
||||||
lineHeight: 1.4,
|
|
||||||
}}>
|
|
||||||
{line.text}
|
{line.text}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="terminal-input-row">
|
<div className="terminal-input-bar">
|
||||||
<span className="terminal-prompt">{'>'}</span>
|
<span className="terminal-prompt">›</span>
|
||||||
<input
|
<input
|
||||||
className="terminal-input"
|
className="terminal-input"
|
||||||
value={input}
|
value={input}
|
||||||
@@ -104,44 +108,25 @@ export default function Shell({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiPanel && (
|
{showAi && (
|
||||||
<div style={{
|
<div className="ai-panel">
|
||||||
width: 320,
|
<div className="ai-panel-header">AI Assistant</div>
|
||||||
borderLeft: '1px solid var(--border-dim)',
|
<div className="ai-panel-messages">
|
||||||
background: 'var(--bg-surface)',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}>
|
|
||||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-dim)' }}>
|
|
||||||
<div className="section-header" style={{ margin: 0 }}>AI Assistant</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
|
|
||||||
{aiMessages.map((msg, i) => (
|
{aiMessages.map((msg, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} className={`ai-message ${msg.role}`}>
|
||||||
marginBottom: 8,
|
|
||||||
padding: '6px 8px',
|
|
||||||
borderRadius: 'var(--radius)',
|
|
||||||
background: msg.role === 'ai' ? 'var(--bg-card)' : 'var(--muted-red)',
|
|
||||||
borderLeft: `3px solid ${msg.role === 'ai' ? 'var(--cyber-red)' : 'var(--cyber-rose)'}`,
|
|
||||||
fontSize: 12,
|
|
||||||
lineHeight: 1.4,
|
|
||||||
}}>
|
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{aiLoading && <span className="loading-spinner"> thinking...</span>}
|
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="ai-panel-input">
|
||||||
<div style={{ padding: '8px 12px', borderTop: '1px solid var(--border-dim)', display: 'flex', gap: 6 }}>
|
|
||||||
<input
|
<input
|
||||||
style={{ flex: 1, padding: '4px 8px', fontSize: 12 }}
|
|
||||||
value={aiInput}
|
value={aiInput}
|
||||||
onChange={e => setAiInput(e.target.value)}
|
onChange={e => setAiInput(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
|
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
|
||||||
placeholder="Ask AI..."
|
placeholder="Ask AI..."
|
||||||
/>
|
/>
|
||||||
<button style={{ padding: '4px 8px' }} onClick={handleAiSend}>Send</button>
|
<button className="sm" onClick={handleAiSend} disabled={!aiInput.trim()}>Send</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useState, useRef, useEffect } from 'react'
|
|||||||
|
|
||||||
export default function Studio({ api }) {
|
export default function Studio({ api }) {
|
||||||
const [messages, setMessages] = useState([
|
const [messages, setMessages] = useState([
|
||||||
{ role: 'ai', content: '>> Welcome to Studio! Chat with your AI assistant here.' },
|
{ role: 'ai', content: 'Welcome to Studio! Chat with your AI assistant here.' },
|
||||||
{ role: 'ai', content: '>> Configure agents and workflows from the sidebar.' },
|
{ role: 'ai', content: 'Configure agents and workflows from the sidebar.' },
|
||||||
])
|
])
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
const [sidebarPanel, setSidebarPanel] = useState('chat')
|
const [sidebarPanel, setSidebarPanel] = useState('chat')
|
||||||
@@ -17,16 +17,16 @@ export default function Studio({ api }) {
|
|||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (!input.trim() || loading) return
|
if (!input.trim() || loading) return
|
||||||
const text = input.trim()
|
const text = input.trim()
|
||||||
setMessages(prev => [...prev, { role: 'user', content: '>> ' + text }])
|
setMessages(prev => [...prev, { role: 'user', content: text }])
|
||||||
setInput('')
|
setInput('')
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
api.runCommand(`echo "AI response simulation for: ${text}"`, '')
|
api.runCommand(`echo "AI response simulation for: ${text}"`, '')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
setMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || res.error || 'No response') }])
|
setMessages(prev => [...prev, { role: 'ai', content: res.output || res.error || 'No response' }])
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
setMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
setMessages(prev => [...prev, { role: 'ai', content: `Error: ${err.message}` }])
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}
|
}
|
||||||
@@ -38,32 +38,37 @@ export default function Studio({ api }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sidebarItems = [
|
||||||
|
{ id: 'chat', label: 'Chat', icon: '#' },
|
||||||
|
{ id: 'agents', label: 'Agents', icon: '*' },
|
||||||
|
{ id: 'workflows', label: 'Workflows', icon: '~' },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="split-horizontal">
|
<div className="split-horizontal">
|
||||||
<div className="chat-container" style={{ flex: 1, borderRight: '1px solid var(--border-dim)' }}>
|
<div className="chat-layout" style={{ flex: 1, borderRight: '1px solid var(--border)' }}>
|
||||||
<div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-dim)' }}>
|
<div className="panel-header">
|
||||||
<div className="section-header" style={{ margin: 0 }}>
|
<span className="panel-title">
|
||||||
Chat
|
Chat
|
||||||
{loading && <span className="loading-spinner" style={{ marginLeft: 8 }}> thinking...</span>}
|
{loading && <span className="spinner" />}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chat-messages">
|
<div className="chat-messages">
|
||||||
{messages.map((msg, i) => (
|
{messages.map((msg, i) => (
|
||||||
<div key={i} className={`chat-message ${msg.role}`}>
|
<div key={i} className={`message ${msg.role}`}>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={messagesEnd} />
|
<div ref={messagesEnd} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chat-input-container">
|
<div className="chat-input-bar">
|
||||||
<input
|
<input
|
||||||
className="chat-input"
|
|
||||||
value={input}
|
value={input}
|
||||||
onChange={e => setInput(e.target.value)}
|
onChange={e => setInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Type a message... (/plan <goal> for workflows)"
|
placeholder="Type a message... (Enter to send)"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<button className="primary" onClick={handleSend} disabled={loading || !input.trim()}>
|
<button className="primary" onClick={handleSend} disabled={loading || !input.trim()}>
|
||||||
@@ -73,53 +78,60 @@ export default function Studio({ api }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="split-right">
|
<div className="split-right">
|
||||||
<div className="sidebar-section">
|
<div className="sidebar-nav">
|
||||||
<div className="section-header">Studio</div>
|
{sidebarItems.map(item => (
|
||||||
{['chat', 'agents', 'workflows'].map(panel => (
|
|
||||||
<div
|
<div
|
||||||
key={panel}
|
key={item.id}
|
||||||
className={`sidebar-item ${sidebarPanel === panel ? 'active' : ''}`}
|
className={`sidebar-tab ${sidebarPanel === item.id ? 'active' : ''}`}
|
||||||
onClick={() => setSidebarPanel(panel)}
|
onClick={() => setSidebarPanel(item.id)}
|
||||||
>
|
>
|
||||||
[{panel === 'chat' ? '#' : panel === 'agents' ? '*' : '~'}] {panel.charAt(0).toUpperCase() + panel.slice(1)}
|
<span style={{ fontFamily: 'var(--font-mono)', width: 16 }}>{item.icon}</span>
|
||||||
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ borderTop: '1px solid var(--border-dim)', paddingTop: 12 }}>
|
{sidebarPanel === 'chat' && (
|
||||||
{sidebarPanel === 'chat' && (
|
<div>
|
||||||
<div>
|
<div className="section-title">Commands</div>
|
||||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, marginBottom: 8 }}>Commands</div>
|
<div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--text-tertiary)' }}>
|
||||||
<div style={{ color: 'var(--dim-red)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
|
/plan <goal><br />
|
||||||
/plan {'<goal>'}<br/>
|
/help
|
||||||
/help
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sidebarPanel === 'agents' && (
|
{sidebarPanel === 'agents' && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, marginBottom: 8 }}>Active Agents</div>
|
<div className="section-title">Active Agents</div>
|
||||||
<div style={{ padding: '4px 0' }}>
|
<div className="agent-card">
|
||||||
<span style={{ color: 'var(--text-main)', fontWeight: 600 }}>Crush</span>
|
<div className="agent-avatar">C</div>
|
||||||
<span style={{ color: 'var(--text-muted)', marginLeft: 8, fontSize: 11 }}>[|| stopped]</span>
|
<div>
|
||||||
</div>
|
<div className="agent-name">Crush</div>
|
||||||
<div style={{ padding: '4px 0' }}>
|
<div className="agent-status">Stopped</div>
|
||||||
<span style={{ color: 'var(--text-main)', fontWeight: 600 }}>Claude Code</span>
|
|
||||||
<span style={{ color: 'var(--text-muted)', marginLeft: 8, fontSize: 11 }}>[|| stopped]</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="badge neutral" style={{ marginLeft: 'auto' }}>Inactive</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="agent-card">
|
||||||
|
<div className="agent-avatar">CC</div>
|
||||||
|
<div>
|
||||||
|
<div className="agent-name">Claude Code</div>
|
||||||
|
<div className="agent-status">Stopped</div>
|
||||||
|
</div>
|
||||||
|
<span className="badge neutral" style={{ marginLeft: 'auto' }}>Inactive</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sidebarPanel === 'workflows' && (
|
{sidebarPanel === 'workflows' && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No active workflow.</div>
|
<div className="section-title">Workflows</div>
|
||||||
<div style={{ color: 'var(--dim-red)', fontSize: 12, marginTop: 8 }}>
|
<div className="empty-state">
|
||||||
Use /plan {'<goal>'} in chat to start.
|
No active workflow.
|
||||||
</div>
|
<span style={{ fontFamily: 'var(--font-mono)' }}>Use /plan <goal> in chat to start.</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import './styles/global.css'
|
||||||
import App from './components/App'
|
import App from './components/App'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
|||||||
@@ -1,576 +1,341 @@
|
|||||||
:root {
|
:root {
|
||||||
--bg-void: #0A0A0C;
|
--bg: #0A0A0C;
|
||||||
--bg-base: #0F0D10;
|
--bg-base: #0F0D10;
|
||||||
--bg-surface: #161218;
|
--bg-surface: #161218;
|
||||||
--bg-panel: #1C1719;
|
--bg-elevated: #1C1719;
|
||||||
--bg-card: #221B1E;
|
--bg-card: #221B1E;
|
||||||
--bg-input: #2A2225;
|
--bg-input: #2A2225;
|
||||||
--bg-hover: #332528;
|
--bg-hover: #332528;
|
||||||
|
|
||||||
--cyber-red: #FF0033;
|
--accent: #FF0033;
|
||||||
--cyber-red-dark: #8B0020;
|
--accent-dark: #8B0020;
|
||||||
--cyber-red-deep: #5C0015;
|
--accent-deep: #5C0015;
|
||||||
--cyber-pink: #FF1A5E;
|
--accent-light: #FF1A5E;
|
||||||
--cyber-rose: #FF4D6D;
|
--accent-muted: #FF4D6D;
|
||||||
--neon-red: #FF1744;
|
--accent-bright: #FF1744;
|
||||||
--bright-red: #FF5252;
|
--accent-soft: #FF5252;
|
||||||
--dim-red: #6B2033;
|
--accent-dim: #6B2033;
|
||||||
--muted-red: #4A1525;
|
--accent-bg: #4A1525;
|
||||||
|
|
||||||
--text-bright: #EAE0E2;
|
--text-primary: #EAE0E2;
|
||||||
--text-main: #D4C4C8;
|
--text-secondary: #D4C4C8;
|
||||||
--text-dim: #8A7A7E;
|
--text-tertiary: #8A7A7E;
|
||||||
--text-muted: #5A4F52;
|
--text-disabled: #5A4F52;
|
||||||
|
|
||||||
--success: #00E676;
|
--success: #00E676;
|
||||||
--warning: #FFD740;
|
--warning: #FFD740;
|
||||||
--error: #FF1744;
|
--error: #FF1744;
|
||||||
|
--info: #448AFF;
|
||||||
|
|
||||||
--border-dim: #2A1F22;
|
--border: #2A1F22;
|
||||||
--border-red: #FF003344;
|
--border-accent: #FF003344;
|
||||||
--border-red-full: #FF0033;
|
--border-accent-full: #FF0033;
|
||||||
|
|
||||||
|
--radius-sm: 6px;
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace;
|
||||||
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
||||||
|
|
||||||
--header-h: 48px;
|
--header-h: 52px;
|
||||||
--footer-h: 32px;
|
--sidebar-w: 280px;
|
||||||
--tab-h: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body, #root {
|
html, body, #root { height: 100%; width: 100%; overflow: hidden; }
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--bg-void);
|
background: var(--bg);
|
||||||
color: var(--text-main);
|
color: var(--text-secondary);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-sans);
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
user-select: none;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::selection { background: var(--accent); color: #fff; }
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
background: transparent;
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
}
|
::-webkit-scrollbar-thumb { background: var(--accent-dim); border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--accent-dark); }
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
a { color: var(--accent); text-decoration: none; cursor: pointer; }
|
||||||
background: var(--dim-red);
|
a:hover { color: var(--accent-light); }
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
button {
|
||||||
background: var(--cyber-red-dark);
|
font-family: var(--font-sans);
|
||||||
}
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
::selection {
|
padding: 8px 16px;
|
||||||
background: var(--cyber-red);
|
border-radius: var(--radius);
|
||||||
color: #fff;
|
border: 1px solid var(--border);
|
||||||
}
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
a {
|
cursor: pointer;
|
||||||
color: var(--cyber-red);
|
transition: all 0.15s ease;
|
||||||
text-decoration: none;
|
display: inline-flex;
|
||||||
}
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
a:hover {
|
white-space: nowrap;
|
||||||
color: var(--cyber-pink);
|
|
||||||
}
|
}
|
||||||
|
button:hover:not(:disabled) { background: var(--bg-hover); border-color: var(--accent-dark); color: var(--text-primary); }
|
||||||
|
button:active:not(:disabled) { transform: scale(0.97); }
|
||||||
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
button.primary:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); }
|
||||||
|
button.ghost { background: transparent; border-color: transparent; color: var(--text-tertiary); }
|
||||||
|
button.ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
|
||||||
|
button.sm { font-size: 12px; padding: 4px 10px; }
|
||||||
|
|
||||||
input, textarea {
|
input, textarea {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-sans);
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
color: var(--text-main);
|
color: var(--text-primary);
|
||||||
border: 1px solid var(--border-dim);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
|
||||||
|
input::placeholder { color: var(--text-disabled); }
|
||||||
|
|
||||||
input:focus, textarea:focus {
|
.app-layout { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
|
||||||
border-color: var(--cyber-red);
|
|
||||||
box-shadow: 0 0 0 2px var(--border-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 6px 14px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
background: var(--bg-card);
|
|
||||||
color: var(--text-main);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
border-color: var(--cyber-red-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary {
|
|
||||||
background: var(--cyber-red);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--cyber-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:hover {
|
|
||||||
background: var(--neon-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
height: var(--header-h);
|
height: var(--header-h);
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-bottom: 1px solid var(--border-dim);
|
border-bottom: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
padding: 0 20px;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-brand { display: flex; align-items: center; gap: 8px; }
|
||||||
font-family: var(--font-mono);
|
.header-logo { font-family: var(--font-mono); font-weight: 900; font-size: 18px; color: var(--accent); letter-spacing: 3px; user-select: none; }
|
||||||
font-weight: 900;
|
.header-version { font-size: 11px; color: var(--accent-dim); font-family: var(--font-mono); }
|
||||||
font-size: 16px;
|
|
||||||
color: var(--cyber-red);
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-version {
|
.header-nav { display: flex; gap: 4px; margin-left: 32px; }
|
||||||
font-size: 11px;
|
|
||||||
color: var(--dim-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tabs {
|
.nav-tab {
|
||||||
display: flex;
|
padding: 8px 18px;
|
||||||
gap: 2px;
|
border-radius: var(--radius);
|
||||||
margin-left: 24px;
|
font-size: 13px;
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab {
|
|
||||||
padding: 6px 16px;
|
|
||||||
border-radius: var(--radius) var(--radius) 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 1px;
|
color: var(--text-tertiary);
|
||||||
color: var(--text-dim);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s ease;
|
||||||
border: 1px solid transparent;
|
|
||||||
border-bottom: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab:hover {
|
|
||||||
color: var(--text-main);
|
|
||||||
background: var(--bg-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab.active {
|
|
||||||
color: #fff;
|
|
||||||
background: var(--cyber-red);
|
|
||||||
border-color: var(--cyber-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-status {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.nav-tab:hover { color: var(--text-primary); background: var(--bg-card); }
|
||||||
|
.nav-tab.active { color: #fff; background: var(--accent); }
|
||||||
|
|
||||||
.status-dot {
|
.header-spacer { flex: 1; }
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.ok { background: var(--success); }
|
.header-indicators { display: flex; align-items: center; gap: 12px; }
|
||||||
.status-dot.warn { background: var(--warning); }
|
.indicator { width: 8px; height: 8px; border-radius: 50%; transition: background 0.3s; }
|
||||||
.status-dot.error { background: var(--error); }
|
.indicator.ok { background: var(--success); }
|
||||||
.status-dot.off { background: var(--text-muted); }
|
.indicator.warn { background: var(--warning); }
|
||||||
|
.indicator.error { background: var(--error); }
|
||||||
|
.indicator.off { background: var(--text-disabled); }
|
||||||
|
|
||||||
.header-clock {
|
.header-clock { font-family: var(--font-mono); font-size: 12px; color: var(--accent); font-weight: 600; }
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--cyber-red);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-date {
|
.content { flex: 1; overflow: hidden; }
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.statusbar {
|
||||||
flex: 1;
|
height: 28px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
height: var(--footer-h);
|
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-top: 1px solid var(--border-dim);
|
border-top: 1px solid var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.footer-shortcuts {
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-disabled);
|
||||||
}
|
|
||||||
|
|
||||||
.footer-shortcuts kbd {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 1px 5px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-version {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--dim-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-update {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-update.available {
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-update.uptodate {
|
|
||||||
color: var(--success);
|
|
||||||
}
|
}
|
||||||
|
.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 12px; }
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border-dim);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-lg);
|
||||||
padding: 16px;
|
padding: 20px;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
.card:hover { border-color: var(--accent-dim); }
|
||||||
.card:hover {
|
.card-header {
|
||||||
border-color: var(--dim-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--cyber-red);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--cyber-red);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header::before {
|
|
||||||
content: '■';
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 4px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-status {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 11px;
|
|
||||||
width: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-status.ok { color: var(--success); }
|
|
||||||
.tool-status.missing { color: var(--error); }
|
|
||||||
|
|
||||||
.tool-name {
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-version {
|
|
||||||
color: var(--dim-red);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 6px;
|
|
||||||
background: var(--bg-input);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, var(--cyber-red), var(--cyber-pink));
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: width 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-2 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
height: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-horizontal {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-left {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-right {
|
|
||||||
width: 320px;
|
|
||||||
border-left: 1px solid var(--border-dim);
|
|
||||||
background: var(--bg-surface);
|
|
||||||
overflow: auto;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-messages {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message.ai {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border-left: 3px solid var(--cyber-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message.user {
|
|
||||||
background: var(--muted-red);
|
|
||||||
border-left: 3px solid var(--cyber-rose);
|
|
||||||
margin-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 8px;
|
|
||||||
border-top: 1px solid var(--border-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input {
|
|
||||||
flex: 1;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-item {
|
|
||||||
padding: 6px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-dim);
|
font-weight: 700;
|
||||||
transition: all 0.15s;
|
color: var(--accent);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item:hover {
|
.badge {
|
||||||
background: var(--bg-card);
|
display: inline-flex;
|
||||||
color: var(--text-main);
|
align-items: center;
|
||||||
}
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
.sidebar-item.active {
|
border-radius: 99px;
|
||||||
background: var(--cyber-red);
|
font-size: 11px;
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.badge.ok { background: rgba(0,230,118,0.15); color: var(--success); }
|
||||||
|
.badge.error { background: rgba(255,23,68,0.15); color: var(--error); }
|
||||||
|
.badge.warn { background: rgba(255,215,64,0.15); color: var(--warning); }
|
||||||
|
.badge.info { background: rgba(68,138,255,0.15); color: var(--info); }
|
||||||
|
.badge.neutral { background: var(--bg-hover); color: var(--text-tertiary); }
|
||||||
|
.badge.accent { background: var(--accent-bg); color: var(--accent); }
|
||||||
|
|
||||||
.terminal-container {
|
.chip {
|
||||||
height: 100%;
|
display: inline-flex;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-output {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 13px;
|
|
||||||
overflow-y: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
background: var(--bg-void);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-input-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
gap: 6px;
|
||||||
background: var(--bg-input);
|
padding: 6px 12px;
|
||||||
border-top: 1px solid var(--border-dim);
|
border-radius: var(--radius);
|
||||||
}
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
.terminal-prompt {
|
|
||||||
color: var(--success);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-input {
|
|
||||||
flex: 1;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-field {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid var(--border-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-label {
|
|
||||||
width: 140px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
.chip:hover { border-color: var(--accent-dark); background: var(--bg-hover); }
|
||||||
|
.chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
|
|
||||||
.config-value {
|
.progress { height: 6px; background: var(--bg-input); border-radius: 3px; overflow: hidden; }
|
||||||
color: var(--text-main);
|
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-light)); border-radius: 3px; transition: width 0.4s ease; }
|
||||||
|
|
||||||
|
.tool-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.tool-row:last-child { border-bottom: none; }
|
||||||
|
.tool-name { flex: 1; color: var(--text-primary); font-weight: 500; }
|
||||||
|
.tool-version { color: var(--text-tertiary); font-size: 12px; font-family: var(--font-mono); }
|
||||||
|
|
||||||
@keyframes glitch {
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; height: 100%; padding: 20px; overflow: auto; }
|
||||||
0% { transform: translate(0); }
|
.split-horizontal { display: flex; height: 100%; }
|
||||||
20% { transform: translate(-2px, 2px); }
|
.split-right { width: var(--sidebar-w); border-left: 1px solid var(--border); background: var(--bg-surface); overflow: auto; padding: 16px; }
|
||||||
40% { transform: translate(-2px, -2px); }
|
|
||||||
60% { transform: translate(2px, 2px); }
|
.chat-layout { display: flex; flex-direction: column; height: 100%; }
|
||||||
80% { transform: translate(2px, -2px); }
|
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
100% { transform: translate(0); }
|
.message {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.message.user { align-self: flex-end; background: var(--accent-bg); color: var(--text-primary); border-bottom-right-radius: 4px; }
|
||||||
|
.message.ai { align-self: flex-start; background: var(--bg-card); color: var(--text-primary); border-bottom-left-radius: 4px; border-left: 3px solid var(--accent); }
|
||||||
|
.chat-input-bar { display: flex; gap: 8px; padding: 16px 20px; border-top: 1px solid var(--border); background: var(--bg-surface); }
|
||||||
|
.chat-input-bar input { flex: 1; }
|
||||||
|
|
||||||
|
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; margin-bottom: 20px; }
|
||||||
|
.sidebar-tab {
|
||||||
|
display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: var(--radius);
|
||||||
|
font-size: 13px; color: var(--text-tertiary); cursor: pointer; transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.sidebar-tab:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||||
|
.sidebar-tab.active { background: var(--accent); color: #fff; font-weight: 600; }
|
||||||
|
|
||||||
|
.terminal { display: flex; flex-direction: column; height: 100%; background: var(--bg); }
|
||||||
|
.terminal-output { flex: 1; padding: 16px; font-family: var(--font-mono); font-size: 13px; line-height: 1.6; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
||||||
|
.terminal-line { margin-bottom: 2px; }
|
||||||
|
.terminal-line.cmd { color: var(--accent-dim); }
|
||||||
|
.terminal-line.out { color: var(--text-primary); }
|
||||||
|
.terminal-line.err { color: var(--error); }
|
||||||
|
.terminal-input-bar { display: flex; align-items: center; padding: 10px 16px; background: var(--bg-surface); border-top: 1px solid var(--border); gap: 8px; }
|
||||||
|
.terminal-prompt { color: var(--success); font-family: var(--font-mono); font-weight: 700; font-size: 14px; flex-shrink: 0; }
|
||||||
|
.terminal-input { flex: 1; background: transparent; border: none; outline: none; color: var(--text-primary); font-family: var(--font-mono); font-size: 13px; padding: 0; }
|
||||||
|
.terminal-input:focus { box-shadow: none; border-color: transparent; }
|
||||||
|
|
||||||
|
.config-layout { max-width: 840px; margin: 0 auto; padding: 24px; overflow-y: auto; height: 100%; }
|
||||||
|
.config-section { margin-bottom: 28px; }
|
||||||
|
.config-section-title {
|
||||||
|
font-size: 12px; font-weight: 700; color: var(--accent); text-transform: uppercase;
|
||||||
|
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.field-row { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); gap: 12px; }
|
||||||
|
.field-row:last-child { border-bottom: none; }
|
||||||
|
.field-label { width: 140px; flex-shrink: 0; color: var(--text-tertiary); font-size: 13px; }
|
||||||
|
.field-value { color: var(--text-primary); font-size: 14px; flex: 1; }
|
||||||
|
.field-value.empty { color: var(--text-disabled); font-style: italic; }
|
||||||
|
|
||||||
|
.provider-card {
|
||||||
|
background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
padding: 14px 16px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.provider-card:hover { border-color: var(--accent-dim); }
|
||||||
|
.provider-info { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.provider-name { font-weight: 600; color: var(--text-primary); font-size: 14px; }
|
||||||
|
.provider-meta { display: flex; gap: 12px; font-size: 12px; color: var(--text-tertiary); font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
.theme-picker { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.theme-swatch {
|
||||||
|
width: 48px; height: 48px; border-radius: var(--radius); border: 2px solid var(--border);
|
||||||
|
cursor: pointer; transition: all 0.15s; position: relative;
|
||||||
|
}
|
||||||
|
.theme-swatch:hover { transform: scale(1.1); border-color: var(--accent-dim); }
|
||||||
|
.theme-swatch.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
|
||||||
|
.theme-swatch.active::after {
|
||||||
|
content: '\2713'; position: absolute; inset: 0; display: flex; align-items: center;
|
||||||
|
justify-content: center; color: #fff; font-size: 18px; font-weight: 700; text-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scanline {
|
.section-title { font-size: 12px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; }
|
||||||
0% { top: -10%; }
|
.actions-stack { display: flex; flex-direction: column; gap: 6px; }
|
||||||
100% { top: 110%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes typewriter {
|
.agent-card { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: var(--radius); background: var(--bg-card); margin-bottom: 6px; }
|
||||||
from { width: 0; }
|
.agent-avatar {
|
||||||
to { width: 100%; }
|
width: 32px; height: 32px; border-radius: 50%; background: var(--accent-bg); color: var(--accent);
|
||||||
|
display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.agent-name { font-weight: 600; color: var(--text-primary); font-size: 13px; }
|
||||||
|
.agent-status { font-size: 11px; color: var(--text-tertiary); margin-top: 2px; }
|
||||||
|
|
||||||
@keyframes pulse {
|
.ai-panel { width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; }
|
||||||
0%, 100% { opacity: 1; }
|
.ai-panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 13px; color: var(--accent); }
|
||||||
50% { opacity: 0.5; }
|
.ai-panel-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
}
|
.ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; }
|
||||||
|
.ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); }
|
||||||
|
.ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); }
|
||||||
|
.ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); }
|
||||||
|
.ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; }
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; color: var(--text-disabled); font-size: 13px; text-align: center; gap: 8px; }
|
||||||
from { opacity: 0; transform: translateY(4px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in {
|
.panel-header {
|
||||||
animation: fadeIn 0.3s ease-out;
|
display: flex; align-items: center; justify-content: space-between; padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--border); background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
|
.panel-title { font-weight: 700; font-size: 13px; color: var(--accent); display: flex; align-items: center; gap: 8px; }
|
||||||
|
.panel-subtitle { font-weight: 400; font-size: 12px; color: var(--text-tertiary); margin-left: 8px; }
|
||||||
|
|
||||||
.tab-transition {
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
animation: fadeIn 0.2s ease-out;
|
.spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.6s linear infinite; display: inline-block; vertical-align: middle; }
|
||||||
}
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-in { animation: fadeIn 0.2s ease-out; }
|
||||||
.glitch-text {
|
|
||||||
animation: glitch 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
display: inline-block;
|
|
||||||
animation: pulse 1s infinite;
|
|
||||||
color: var(--cyber-red);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,43 +1,33 @@
|
|||||||
const defaultTheme = {
|
const defaultTheme = {
|
||||||
name: 'Cyberpunk Red',
|
name: 'Cyberpunk Red',
|
||||||
colors: {
|
colors: {
|
||||||
bgVoid: '#0A0A0C',
|
bg: '#0A0A0C',
|
||||||
bgBase: '#0F0D10',
|
bgBase: '#0F0D10',
|
||||||
bgSurface: '#161218',
|
bgSurface: '#161218',
|
||||||
bgPanel: '#1C1719',
|
bgElevated: '#1C1719',
|
||||||
bgCard: '#221B1E',
|
bgCard: '#221B1E',
|
||||||
bgInput: '#2A2225',
|
bgInput: '#2A2225',
|
||||||
bgHover: '#332528',
|
bgHover: '#332528',
|
||||||
cyberRed: '#FF0033',
|
accent: '#FF0033',
|
||||||
cyberRedDark: '#8B0020',
|
accentDark: '#8B0020',
|
||||||
cyberRedDeep: '#5C0015',
|
accentDeep: '#5C0015',
|
||||||
cyberPink: '#FF1A5E',
|
accentLight: '#FF1A5E',
|
||||||
cyberRose: '#FF4D6D',
|
accentMuted: '#FF4D6D',
|
||||||
neonRed: '#FF1744',
|
accentBright: '#FF1744',
|
||||||
brightRed: '#FF5252',
|
accentSoft: '#FF5252',
|
||||||
dimRed: '#6B2033',
|
accentDim: '#6B2033',
|
||||||
mutedRed: '#4A1525',
|
accentBg: '#4A1525',
|
||||||
textBright: '#EAE0E2',
|
textPrimary: '#EAE0E2',
|
||||||
textMain: '#D4C4C8',
|
textSecondary: '#D4C4C8',
|
||||||
textDim: '#8A7A7E',
|
textTertiary: '#8A7A7E',
|
||||||
textMuted: '#5A4F52',
|
textDisabled: '#5A4F52',
|
||||||
success: '#00E676',
|
success: '#00E676',
|
||||||
warning: '#FFD740',
|
warning: '#FFD740',
|
||||||
error: '#FF1744',
|
error: '#FF1744',
|
||||||
borderDim: '#2A1F22',
|
info: '#448AFF',
|
||||||
borderRed: '#FF003344',
|
border: '#2A1F22',
|
||||||
borderRedFull: '#FF0033',
|
borderAccent: '#FF003344',
|
||||||
},
|
borderAccentFull: '#FF0033',
|
||||||
fonts: {
|
|
||||||
mono: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
|
||||||
ui: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
|
||||||
},
|
|
||||||
borderRadius: '8px',
|
|
||||||
animations: {
|
|
||||||
glitch: true,
|
|
||||||
scanline: true,
|
|
||||||
typewriter: true,
|
|
||||||
pulse: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,15 +38,15 @@ const themes = {
|
|||||||
name: 'Cyberpunk Pink',
|
name: 'Cyberpunk Pink',
|
||||||
colors: {
|
colors: {
|
||||||
...defaultTheme.colors,
|
...defaultTheme.colors,
|
||||||
cyberRed: '#FF1A8C',
|
accent: '#FF1A8C',
|
||||||
cyberRedDark: '#8B1050',
|
accentDark: '#8B1050',
|
||||||
cyberRedDeep: '#5C0A35',
|
accentDeep: '#5C0A35',
|
||||||
cyberPink: '#FF4DAE',
|
accentLight: '#FF4DAE',
|
||||||
cyberRose: '#FF6DC2',
|
accentMuted: '#FF6DC2',
|
||||||
neonRed: '#FF1A8C',
|
accentBright: '#FF1A8C',
|
||||||
brightRed: '#FF6DC2',
|
accentSoft: '#FF6DC2',
|
||||||
dimRed: '#6B2050',
|
accentDim: '#6B2050',
|
||||||
mutedRed: '#4A1535',
|
accentBg: '#4A1535',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'midnight-blue': {
|
'midnight-blue': {
|
||||||
@@ -64,15 +54,15 @@ const themes = {
|
|||||||
name: 'Midnight Blue',
|
name: 'Midnight Blue',
|
||||||
colors: {
|
colors: {
|
||||||
...defaultTheme.colors,
|
...defaultTheme.colors,
|
||||||
cyberRed: '#0088FF',
|
accent: '#0088FF',
|
||||||
cyberRedDark: '#004488',
|
accentDark: '#004488',
|
||||||
cyberRedDeep: '#002255',
|
accentDeep: '#002255',
|
||||||
cyberPink: '#00AAFF',
|
accentLight: '#00AAFF',
|
||||||
cyberRose: '#44CCFF',
|
accentMuted: '#44CCFF',
|
||||||
neonRed: '#0088FF',
|
accentBright: '#0088FF',
|
||||||
brightRed: '#44CCFF',
|
accentSoft: '#44CCFF',
|
||||||
dimRed: '#203366',
|
accentDim: '#203366',
|
||||||
mutedRed: '#152244',
|
accentBg: '#152244',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'matrix-green': {
|
'matrix-green': {
|
||||||
@@ -80,15 +70,15 @@ const themes = {
|
|||||||
name: 'Matrix Green',
|
name: 'Matrix Green',
|
||||||
colors: {
|
colors: {
|
||||||
...defaultTheme.colors,
|
...defaultTheme.colors,
|
||||||
cyberRed: '#00FF41',
|
accent: '#00FF41',
|
||||||
cyberRedDark: '#008822',
|
accentDark: '#008822',
|
||||||
cyberRedDeep: '#005515',
|
accentDeep: '#005515',
|
||||||
cyberPink: '#33FF66',
|
accentLight: '#33FF66',
|
||||||
cyberRose: '#66FF99',
|
accentMuted: '#66FF99',
|
||||||
neonRed: '#00FF41',
|
accentBright: '#00FF41',
|
||||||
brightRed: '#66FF99',
|
accentSoft: '#66FF99',
|
||||||
dimRed: '#206630',
|
accentDim: '#206630',
|
||||||
mutedRed: '#154420',
|
accentBg: '#154420',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -105,32 +95,33 @@ export function applyTheme(theme) {
|
|||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
const c = theme.colors
|
const c = theme.colors
|
||||||
const map = {
|
const map = {
|
||||||
'--bg-void': c.bgVoid,
|
'--bg': c.bg,
|
||||||
'--bg-base': c.bgBase,
|
'--bg-base': c.bgBase,
|
||||||
'--bg-surface': c.bgSurface,
|
'--bg-surface': c.bgSurface,
|
||||||
'--bg-panel': c.bgPanel,
|
'--bg-elevated': c.bgElevated,
|
||||||
'--bg-card': c.bgCard,
|
'--bg-card': c.bgCard,
|
||||||
'--bg-input': c.bgInput,
|
'--bg-input': c.bgInput,
|
||||||
'--bg-hover': c.bgHover,
|
'--bg-hover': c.bgHover,
|
||||||
'--cyber-red': c.cyberRed,
|
'--accent': c.accent,
|
||||||
'--cyber-red-dark': c.cyberRedDark,
|
'--accent-dark': c.accentDark,
|
||||||
'--cyber-red-deep': c.cyberRedDeep,
|
'--accent-deep': c.accentDeep,
|
||||||
'--cyber-pink': c.cyberPink,
|
'--accent-light': c.accentLight,
|
||||||
'--cyber-rose': c.cyberRose,
|
'--accent-muted': c.accentMuted,
|
||||||
'--neon-red': c.neonRed,
|
'--accent-bright': c.accentBright,
|
||||||
'--bright-red': c.brightRed,
|
'--accent-soft': c.accentSoft,
|
||||||
'--dim-red': c.dimRed,
|
'--accent-dim': c.accentDim,
|
||||||
'--muted-red': c.mutedRed,
|
'--accent-bg': c.accentBg,
|
||||||
'--text-bright': c.textBright,
|
'--text-primary': c.textPrimary,
|
||||||
'--text-main': c.textMain,
|
'--text-secondary': c.textSecondary,
|
||||||
'--text-dim': c.textDim,
|
'--text-tertiary': c.textTertiary,
|
||||||
'--text-muted': c.textMuted,
|
'--text-disabled': c.textDisabled,
|
||||||
'--success': c.success,
|
'--success': c.success,
|
||||||
'--warning': c.warning,
|
'--warning': c.warning,
|
||||||
'--error': c.error,
|
'--error': c.error,
|
||||||
'--border-dim': c.borderDim,
|
'--info': c.info,
|
||||||
'--border-red': c.borderRed,
|
'--border': c.border,
|
||||||
'--border-red-full': c.borderRedFull,
|
'--border-accent': c.borderAccent,
|
||||||
|
'--border-accent-full': c.borderAccentFull,
|
||||||
}
|
}
|
||||||
Object.entries(map).forEach(([k, v]) => root.style.setProperty(k, v))
|
Object.entries(map).forEach(([k, v]) => root.style.setProperty(k, v))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user