Initial Mission Control deployment

This commit is contained in:
Aether 2026-02-23 12:12:42 +00:00
parent a7aae4db21
commit 7c0758244c
No known key found for this signature in database
GPG Key ID: 95AFEE837E39AFD2
20 changed files with 1277 additions and 2 deletions

View File

@ -1,3 +1,43 @@
# aether-core-memory
# Aether Core Memory
Project Ledger and persistent memory for Aether Collective
## Mission Control
Autonomous orchestration system for the Aether Collective.
## Components
### 1. System Pulse
- **Schedule:** Every 20 minutes
- **Script:** `/aether_core/pulse.sh`
- **Log:** `/aether_core/memory/pulse_history.log`
### 2. Mission Control Dashboard (AetherOS)
- **URL:** http://localhost:3001
- **Features:**
- Collective Kanban - Task tracking for sub-agents
- ClawHub Marketplace - Skill management
- Session Tracker - Work session logging
- Agent Wiki - System documentation
### 3. Infrastructure
| Service | Endpoint | Status |
|---------|----------|--------|
| Gitea | http://192.168.5.35:3000 | Online |
| Vaultwarden | http://localhost:8080 | Online |
| AetherOS | http://localhost:3001 | Online |
## The Collective
| Agent | Role | Status |
|-------|------|--------|
| Architect | Project scoping | Ready |
| Researcher | Documentation | Ready |
| Engineer | Implementation | Ready |
| QA Auditor | Testing | Ready |
| Publisher | GitHub promotion | Ready |
## SSH Key
```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILPo9xg6+X0F+C5gWXOIoVUi6Ipsnf4JPK05hSLcT9n2 aether@openclaw
```
---
*Last updated: 2026-02-23*

View File

@ -0,0 +1 @@
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Segoe UI,system-ui,-apple-system,sans-serif;overflow:hidden;background:#0a0a0f;color:#e0e0ff}#root{width:100vw;height:100vh}.desktop{width:100%;height:100%;background:linear-gradient(135deg,#0a0a1a,#1a1a2e,#16213e);position:relative;overflow:hidden}.desktop:before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 20% 80%,rgba(99,102,241,.1) 0%,transparent 50%),radial-gradient(circle at 80% 20%,rgba(139,92,246,.1) 0%,transparent 50%);pointer-events:none}.window{position:absolute;background:#141423f2;border:1px solid rgba(99,102,241,.3);border-radius:12px;box-shadow:0 25px 50px -12px #00000080;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);min-width:400px;min-height:300px;overflow:hidden}.window-header{background:linear-gradient(90deg,#6366f133,#8b5cf633);padding:12px 16px;display:flex;align-items:center;justify-content:space-between;cursor:move;border-bottom:1px solid rgba(99,102,241,.2)}.window-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px;color:#e0e0ff}.window-controls{display:flex;gap:8px}.window-btn{width:12px;height:12px;border-radius:50%;border:none;cursor:pointer;transition:all .2s}.window-btn:hover{transform:scale(1.2)}.window-btn.close{background:#ef4444}.window-btn.minimize{background:#f59e0b}.window-btn.maximize{background:#10b981}.window-content{padding:20px;height:calc(100% - 48px);overflow:auto}.taskbar{position:fixed;bottom:0;left:0;right:0;height:48px;background:#0a0a14f2;border-top:1px solid rgba(99,102,241,.2);display:flex;align-items:center;padding:0 16px;gap:12px;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);z-index:1000}.start-btn{display:flex;align-items:center;gap:8px;padding:8px 16px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border:none;border-radius:8px;color:#fff;font-weight:600;cursor:pointer;transition:all .2s}.start-btn:hover{transform:translateY(-2px);box-shadow:0 8px 25px #6366f166}.taskbar-icons{display:flex;gap:8px;flex:1}.taskbar-icon{width:40px;height:40px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;transition:all .2s;color:#a0a0c0}.taskbar-icon:hover,.taskbar-icon.active{background:#6366f133;color:#6366f1}.system-tray{display:flex;align-items:center;gap:12px;padding:0 12px;border-left:1px solid rgba(99,102,241,.2)}.tray-icon{width:20px;height:20px;color:#a0a0c0;cursor:pointer}.tray-icon:hover{color:#6366f1}.clock{font-size:13px;font-weight:500;color:#e0e0ff;min-width:60px;text-align:center}.start-menu{position:fixed;bottom:60px;left:16px;width:320px;background:#0f0f1efa;border:1px solid rgba(99,102,241,.3);border-radius:16px;padding:20px;box-shadow:0 25px 50px -12px #00000080;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);z-index:999}.start-menu h3{font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#6366f1;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid rgba(99,102,241,.2)}.menu-items{display:flex;flex-direction:column;gap:4px}.menu-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;cursor:pointer;transition:all .2s;color:#e0e0ff}.menu-item:hover{background:#6366f126}.menu-item svg{color:#6366f1}.desktop-icons{position:absolute;top:20px;left:20px;display:flex;flex-direction:column;gap:20px}.desktop-icon{display:flex;flex-direction:column;align-items:center;gap:8px;width:80px;padding:12px;border-radius:12px;cursor:pointer;transition:all .2s;color:#e0e0ff}.desktop-icon:hover{background:#6366f126}.desktop-icon svg{width:40px;height:40px;color:#6366f1}.desktop-icon span{font-size:12px;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.5)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#0003;border-radius:4px}::-webkit-scrollbar-thumb{background:#6366f180;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#6366f1b3}

File diff suppressed because one or more lines are too long

14
mission-control/dist/index.html vendored Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>aether-os</title>
<script type="module" crossorigin src="/assets/index-D2vOcu2u.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bi1l6ELE.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

1
mission-control/dist/vite.svg vendored Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

290
mission-control/src/App.css Normal file
View File

@ -0,0 +1,290 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
overflow: hidden;
background: #0a0a0f;
color: #e0e0ff;
}
#root {
width: 100vw;
height: 100vh;
}
.desktop {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 50%, #16213e 100%);
position: relative;
overflow: hidden;
}
.desktop::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.window {
position: absolute;
background: rgba(20, 20, 35, 0.95);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(20px);
min-width: 400px;
min-height: 300px;
overflow: hidden;
}
.window-header {
background: linear-gradient(90deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: move;
border-bottom: 1px solid rgba(99, 102, 241, 0.2);
}
.window-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
color: #e0e0ff;
}
.window-controls {
display: flex;
gap: 8px;
}
.window-btn {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.window-btn:hover {
transform: scale(1.2);
}
.window-btn.close { background: #ef4444; }
.window-btn.minimize { background: #f59e0b; }
.window-btn.maximize { background: #10b981; }
.window-content {
padding: 20px;
height: calc(100% - 48px);
overflow: auto;
}
.taskbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: rgba(10, 10, 20, 0.95);
border-top: 1px solid rgba(99, 102, 241, 0.2);
display: flex;
align-items: center;
padding: 0 16px;
gap: 12px;
backdrop-filter: blur(20px);
z-index: 1000;
}
.start-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4);
}
.taskbar-icons {
display: flex;
gap: 8px;
flex: 1;
}
.taskbar-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #a0a0c0;
}
.taskbar-icon:hover,
.taskbar-icon.active {
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
}
.system-tray {
display: flex;
align-items: center;
gap: 12px;
padding: 0 12px;
border-left: 1px solid rgba(99, 102, 241, 0.2);
}
.tray-icon {
width: 20px;
height: 20px;
color: #a0a0c0;
cursor: pointer;
}
.tray-icon:hover {
color: #6366f1;
}
.clock {
font-size: 13px;
font-weight: 500;
color: #e0e0ff;
min-width: 60px;
text-align: center;
}
.start-menu {
position: fixed;
bottom: 60px;
left: 16px;
width: 320px;
background: rgba(15, 15, 30, 0.98);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 16px;
padding: 20px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(20px);
z-index: 999;
}
.start-menu h3 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: #6366f1;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(99, 102, 241, 0.2);
}
.menu-items {
display: flex;
flex-direction: column;
gap: 4px;
}
.menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #e0e0ff;
}
.menu-item:hover {
background: rgba(99, 102, 241, 0.15);
}
.menu-item svg {
color: #6366f1;
}
.desktop-icons {
position: absolute;
top: 20px;
left: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.desktop-icon {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 80px;
padding: 12px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
color: #e0e0ff;
}
.desktop-icon:hover {
background: rgba(99, 102, 241, 0.15);
}
.desktop-icon svg {
width: 40px;
height: 40px;
color: #6366f1;
}
.desktop-icon span {
font-size: 12px;
text-align: center;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0,0,0,0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(99, 102, 241, 0.7);
}

View File

@ -0,0 +1,14 @@
import { useState, useEffect } from 'react'
import './App.css'
import Desktop from './components/Desktop'
import { AppProvider } from './context/AppContext'
function App() {
return (
<AppProvider>
<Desktop />
</AppProvider>
)
}
export default App

View File

@ -0,0 +1,76 @@
import { useState } from 'react'
import { Download, Star, GitBranch, Shield, Terminal, Database, Globe } from 'lucide-react'
const SKILLS = [
{ id: 1, name: 'OpenClaw Core', description: 'Multi-channel AI gateway', version: '2.1.0', icon: Globe, installed: true, rating: 5 },
{ id: 2, name: 'Session Manager', description: 'Advanced session tracking', version: '1.4.2', icon: Terminal, installed: true, rating: 4 },
{ id: 3, name: 'Vaultwarden Sync', description: 'Credential management', version: '1.32.0', icon: Shield, installed: false, rating: 5 },
{ id: 4, name: 'Git Integration', description: 'Git operations bridge', version: '3.0.1', icon: GitBranch, installed: false, rating: 4 },
{ id: 5, name: 'ChromaDB Memory', description: 'Vector database', version: '0.5.0', icon: Database, installed: false, rating: 4 }
]
const CATEGORIES = ['All', 'Core', 'Security', 'Development', 'Memory']
function ClawHub() {
const [activeCategory, setActiveCategory] = useState('All')
const [searchQuery, setSearchQuery] = useState('')
const filteredSkills = SKILLS.filter(skill => {
const matchesCategory = activeCategory === 'All' || skill.category === activeCategory
const matchesSearch = skill.name.toLowerCase().includes(searchQuery.toLowerCase())
return matchesCategory && matchesSearch
})
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid rgba(99, 102, 241, 0.2)' }}>
<h2 style={{ fontSize: '20px', fontWeight: 600 }}>ClawHub Marketplace</h2>
<input
type="text"
placeholder="Search skills..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ padding: '10px 16px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(99, 102, 241, 0.2)', borderRadius: '8px', color: 'inherit', fontSize: '14px', width: '250px' }}
/>
</div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
{CATEGORIES.map(cat => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
style={{ padding: '8px 16px', background: activeCategory === cat ? '#6366f1' : 'rgba(255,255,255,0.05)', border: 'none', borderRadius: '20px', color: 'inherit', fontSize: '13px', cursor: 'pointer' }}
>
{cat}
</button>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '16px', overflowY: 'auto', flex: 1 }}>
{filteredSkills.map(skill => {
const Icon = skill.icon
return (
<div key={skill.id} style={{ background: 'rgba(0,0,0,0.2)', borderRadius: '12px', padding: '20px', border: skill.installed ? '1px solid rgba(16, 185, 129, 0.3)' : '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '12px', background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Icon size={24} />
</div>
{skill.installed && <span style={{ fontSize: '11px', padding: '4px 10px', background: 'rgba(16, 185, 129, 0.2)', color: '#10b981', borderRadius: '10px', fontWeight: 600 }}>Installed</span>}
</div>
<h3 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '8px' }}>{skill.name}</h3>
<p style={{ fontSize: '14px', color: '#a0a0c0', marginBottom: '16px' }}>{skill.description}</p>
<button style={{ width: '100%', padding: '10px', background: skill.installed ? 'rgba(255,255,255,0.1)' : '#6366f1', border: 'none', borderRadius: '8px', color: 'inherit', fontSize: '14px', cursor: skill.installed ? 'default' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }} disabled={skill.installed}>
<Download size={16} />
{skill.installed ? 'Installed' : 'Install'}
</button>
</div>
)
})}
</div>
</div>
)
}
export default ClawHub

View File

View File

@ -0,0 +1,66 @@
import { useState } from 'react'
import { Plus, MoreHorizontal, Clock, CheckCircle2, Circle, AlertCircle } from 'lucide-react'
const INITIAL_COLUMNS = [
{ id: 'backlog', title: 'Backlog', color: '#6366f1', tasks: [
{ id: 1, title: 'Deploy Vaultwarden', priority: 'high', assignee: 'Aether', status: 'done' }
]},
{ id: 'todo', title: 'To Do', color: '#8b5cf6', tasks: [
{ id: 3, title: 'Complete AetherOS dashboard', priority: 'high', assignee: 'Engineer', status: 'todo' }
]},
{ id: 'in-progress', title: 'In Progress', color: '#f59e0b', tasks: [
{ id: 5, title: 'Build Session Tracker', priority: 'high', assignee: 'Engineer', status: 'in-progress' }
]},
{ id: 'done', title: 'Done', color: '#10b981', tasks: [
{ id: 6, title: 'Install Docker', priority: 'high', assignee: 'Aether', status: 'done' }
]}
]
function Kanban() {
const [columns] = useState(INITIAL_COLUMNS)
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid rgba(99, 102, 241, 0.2)' }}>
<h2 style={{ fontSize: '20px', fontWeight: 600 }}>Collective Kanban</h2>
</div>
<div style={{ display: 'flex', gap: '16px', overflowX: 'auto', flex: 1 }}>
{columns.map(column => (
<div key={column.id} style={{ minWidth: '280px', maxWidth: '280px', background: 'rgba(0,0,0,0.2)', borderRadius: '12px', padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: column.color }} />
<span style={{ fontWeight: 600, fontSize: '14px' }}>{column.title}</span>
<span style={{ fontSize: '12px', color: '#a0a0c0', background: 'rgba(255,255,255,0.1)', padding: '2px 8px', borderRadius: '10px' }}>
{column.tasks.length}
</span>
</div>
<MoreHorizontal size={16} color="#a0a0c0" />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{column.tasks.map(task => (
<div key={task.id} style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '12px' }}>
<p style={{ fontSize: '14px', marginBottom: '8px' }}>{task.title}</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ width: '24px', height: '24px', borderRadius: '50%', background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px', fontWeight: 600 }}>
{task.assignee.charAt(0)}
</div>
</div>
</div>
))}
<button style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '10px', background: 'transparent', border: '1px dashed rgba(255,255,255,0.2)', borderRadius: '8px', color: '#a0a0c0', fontSize: '14px', cursor: 'pointer', marginTop: '8px' }}>
<Plus size={16} />
Add task
</button>
</div>
</div>
))}
</div>
</div>
)
}
export default Kanban

View File

@ -0,0 +1,86 @@
import { useState, useEffect } from 'react'
import { Play, Square, Coffee, Clock, Calendar } from 'lucide-react'
function SessionTracker() {
const [sessionActive, setSessionActive] = useState(false)
const [elapsed, setElapsed] = useState(0)
const [sessionStartTime, setSessionStartTime] = useState(null)
const [sessions] = useState([
{ id: 1, date: '2026-02-23', duration: 45, tasks: 3 },
{ id: 2, date: '2026-02-23', duration: 30, tasks: 2 }
])
useEffect(() => {
let interval
if (sessionActive) {
interval = setInterval(() => setElapsed(e => e + 1), 1000)
}
return () => clearInterval(interval)
}, [sessionActive])
const formatTime = (seconds) => {
const hrs = Math.floor(seconds / 3600)
const mins = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ marginBottom: '20px', paddingBottom: '15px', borderBottom: '1px solid rgba(99, 102, 241, 0.2)' }}>
<h2 style={{ fontSize: '20px', fontWeight: 600 }}>Session Tracker</h2>
</div>
<div style={{ background: sessionActive ? 'linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(16, 185, 129, 0.05))' : 'rgba(0,0,0,0.2)', borderRadius: '12px', padding: '24px', marginBottom: '20px', border: sessionActive ? '1px solid rgba(16, 185, 129, 0.3)' : '1px solid rgba(255,255,255,0.05)', textAlign: 'center' }}>
<div style={{ fontSize: '48px', fontWeight: 700, fontFamily: 'monospace', marginBottom: '24px', color: sessionActive ? '#10b981' : 'inherit' }}>
{formatTime(elapsed)}
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
{!sessionActive ? (
<button onClick={() => { setSessionActive(true); setSessionStartTime(new Date()); }} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '12px 24px', background: '#10b981', border: 'none', borderRadius: '8px', color: 'white', fontSize: '14px', fontWeight: 600, cursor: 'pointer' }}>
<Play size={18} />
Start Session
</button>
) : (
<>
<button onClick={() => {}} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '12px 20px', background: 'rgba(245, 158, 11, 0.2)', border: '1px solid rgba(245, 158, 11, 0.3)', borderRadius: '8px', color: '#f59e0b', fontSize: '14px', cursor: 'pointer' }}>
<Coffee size={18} />
Break
</button>
<button onClick={() => setSessionActive(false)} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '12px 20px', background: '#ef4444', border: 'none', borderRadius: '8px', color: 'white', fontSize: '14px', fontWeight: 600, cursor: 'pointer' }}>
<Square size={18} fill="currentColor" />
End
</button>
</>
)}
</div>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
<h3 style={{ fontSize: '14px', textTransform: 'uppercase', letterSpacing: '1px', color: '#6366f1', marginBottom: '12px' }}>Recent Sessions</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{sessions.map(session => (
<div key={session.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px', background: 'rgba(0,0,0,0.2)', borderRadius: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ width: '40px', height: '40px', borderRadius: '10px', background: 'rgba(99, 102, 241, 0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Calendar size={18} />
</div>
<div>
<div style={{ fontWeight: 600 }}>Work Session</div>
<div style={{ fontSize: '13px', color: '#a0a0c0' }}>{session.date}</div>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '13px', color: '#a0a0c0' }}>Duration</div>
<div style={{ fontWeight: 600 }}>{session.duration} min</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
export default SessionTracker

View File

@ -0,0 +1,104 @@
import { useState } from 'react'
import { FileText, Folder, Search, ExternalLink } from 'lucide-react'
const WIKI_PAGES = [
{
id: 'overview',
title: 'System Overview',
content: `Aether System Overview
Identity
Name: Aether
Role: Autonomous Orchestrator
Initialized: 2026-02-23
Core Directive
Transform high-level visions into production-ready software.
Infrastructure
- Gitea: http://192.168.5.35:3000
- Vaultwarden: http://localhost:8080
- AetherOS: http://localhost:3001
The Collective
- Architect: Project scoping
- Researcher: Documentation
- Engineer: Implementation
- QA Auditor: Testing
- Publisher: GitHub releases`
},
{
id: 'architecture',
title: 'Architecture',
content: `Aether Architecture
Component Flow
User Input -> Aether Orchestrator -> Sub-agents -> Output
Data Flow
1. Planning: Architect scopes project
2. Implementation: Engineer writes code
3. Release: Publisher promotes to GitHub
Memory Architecture
- Short-term: Session context
- Long-term: /aether_core/memory/`
},
{
id: 'security',
title: 'Security Policy',
content: `Security Policy
Credential Management
All credentials stored in Vaultwarden:
- SSH private keys
- API tokens
- Service passwords
Access Control
- SSH key authentication only
- No password authentication
Key Rotation
- SSH Keys: 90 days
- API Tokens: 60 days`
}
]
function Wiki() {
const [activePage, setActivePage] = useState('overview')
const activeContent = WIKI_PAGES.find(p => p.id === activePage)?.content || ''
return (
<div style={{ height: '100%', display: 'flex', gap: '20px' }}>
<div style={{ width: '240px', minWidth: '240px', borderRight: '1px solid rgba(99, 102, 241, 0.2)', paddingRight: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 12px', background: 'rgba(255,255,255,0.05)', borderRadius: '8px' }}>
<Search size={16} color="#a0a0c0" />
<input type="text" placeholder="Search wiki..." style={{ background: 'transparent', border: 'none', color: 'inherit', fontSize: '14px', outline: 'none', width: '100%' }} />
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{WIKI_PAGES.map(page => (
<button key={page.id} onClick={() => setActivePage(page.id)} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '10px 12px', background: activePage === page.id ? 'rgba(99, 102, 241, 0.2)' : 'transparent', border: 'none', borderRadius: '8px', color: activePage === page.id ? '#6366f1' : 'inherit', fontSize: '14px', cursor: 'pointer', textAlign: 'left' }}>
<FileText size={16} />
{page.title}
</button>
))}
</div>
</div>
<div style={{ flex: 1, overflow: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.7 }}>
{activeContent}
<div style={{ marginTop: '40px', paddingTop: '20px', borderTop: '1px solid rgba(99, 102, 241, 0.2)', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px', color: '#a0a0c0' }}>
<ExternalLink size={14} />
<span>Last updated: 2026-02-23 by Aether</span>
</div>
</div>
</div>
)
}
export default Wiki

View File

@ -0,0 +1,37 @@
import re
import os
def fix_jsx_file(filepath):
with open(filepath, 'r') as f:
content = f.read()
lines = content.split('\n')
fixed_lines = []
for line in lines:
# If line contains '}\u003e' and 'style={{'
if '}\u003e' in line and 'style={{' in line:
# Count braces before the }\u003e
open_count = line.count('{')
close_count = line.count('}')
# If unbalanced, fix it
if open_count > close_count:
# Replace only the last occurrence of }\u003e
line = line.rsplit('}\u003e', 1)[0] + '}}\u003e'
fixed_lines.append(line)
fixed_content = '\n'.join(fixed_lines)
with open(filepath, 'w') as f:
f.write(fixed_content)
print(f"Fixed: {filepath}")
# Fix all JSX files
for filename in os.listdir('.'):
if filename.endswith('.jsx'):
fix_jsx_file(filename)
print("Done!")

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,164 @@
import { useState, useEffect } from 'react'
import { useApp } from '../context/AppContext'
import Window from './Window'
import StartMenu from './StartMenu'
import {
Menu,
LayoutGrid,
ShoppingBag,
Clock,
BookOpen,
Wifi,
Shield,
Power
} from 'lucide-react'
const APPS = [
{
id: 'kanban',
title: 'Collective Kanban',
icon: LayoutGrid,
defaultWidth: 900,
defaultHeight: 600
},
{
id: 'clawhub',
title: 'ClawHub Marketplace',
icon: ShoppingBag,
defaultWidth: 1000,
defaultHeight: 700
},
{
id: 'session',
title: 'Session Tracker',
icon: Clock,
defaultWidth: 500,
defaultHeight: 400
},
{
id: 'wiki',
title: 'Agent Wiki',
icon: BookOpen,
defaultWidth: 800,
defaultHeight: 600
}
]
function Desktop() {
const {
windows,
activeWindow,
startMenuOpen,
setStartMenuOpen,
openWindow,
restoreWindow,
focusWindow,
sessionActive
} = useApp()
const [currentTime, setCurrentTime] = useState(new Date())
useEffect(() => {
const timer = setInterval(() => setCurrentTime(new Date()), 1000)
return () => clearInterval(timer)
}, [])
return (
<div className="desktop" onClick={() => setStartMenuOpen(false)}>
{/* Desktop Icons */}
<div className="desktop-icons" onClick={(e) => e.stopPropagation()}>
{APPS.map(app => (
<div
key={app.id}
className="desktop-icon"
onClick={() => openWindow(app)}
>
<app.icon />
<span>{app.title}</span>
</div>
))}
</div>
{/* Windows */}
{windows.map(window => (
!window.minimized && (
<Window
key={window.id}
window={window}
isActive={activeWindow === window.id}
app={APPS.find(a => a.id === window.app)}
/>
)
))}
{/* Start Menu */}
{startMenuOpen && (
<StartMenu
apps={APPS}
onAppClick={openWindow}
/>
)}
{/* Taskbar */}
<div className="taskbar">
<button
className="start-btn"
onClick={(e) => {
e.stopPropagation()
setStartMenuOpen(!startMenuOpen)
}}
>
<Menu size={20} />
<span>Start</span>
</button>
<div className="taskbar-icons">
{windows.map(window => (
<div
key={window.id}
className={`taskbar-icon ${activeWindow === window.id ? 'active' : ''}`}
onClick={() => {
if (window.minimized || activeWindow !== window.id) {
restoreWindow(window.id)
} else {
focusWindow(window.id)
}
}}
title={window.title}
>
{(() => {
const Icon = APPS.find(a => a.id === window.app)?.icon
return Icon ? <Icon size={22} /> : null
})()}
</div>
))}
</div>
<div className="system-tray">
{sessionActive && (
<div
className="tray-icon"
title="Session Active"
style={{ color: '#10b981' }}
>
<Shield size={18} />
</div>
)}
<div className="tray-icon" title="Network Connected">
<Wifi size={18} />
</div>
<div className="tray-icon" title="Power">
<Power size={18} />
</div>
<div className="clock">
{currentTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
</div>
)
}
export default Desktop

View File

@ -0,0 +1,35 @@
import { LayoutGrid, ShoppingBag, Clock, BookOpen, Settings, Power } from 'lucide-react'
function StartMenu({ apps, onAppClick }) {
return (
<div className="start-menu" onClick={(e) => e.stopPropagation()}>
<h3>Applications</h3>
<div className="menu-items">
{apps.map(app => (
<div
key={app.id}
className="menu-item"
onClick={() => onAppClick(app)}
>
<app.icon size={20} />
<span>{app.title}</span>
</div>
))}
</div>
<h3 style={{ marginTop: '20px' }}>System</h3>
<div className="menu-items">
<div className="menu-item">
<Settings size={20} />
<span>Settings</span>
</div>
<div className="menu-item">
<Power size={20} />
<span>Shutdown</span>
</div>
</div>
</div>
)
}
export default StartMenu

View File

@ -0,0 +1,114 @@
import { useRef, useEffect, useState } from 'react'
import { useApp } from '../context/AppContext'
import Kanban from '../apps/Kanban'
import ClawHub from '../apps/ClawHub'
import SessionTracker from '../apps/SessionTracker'
import Wiki from '../apps/Wiki'
import { X, Minus, Square } from 'lucide-react'
const APP_COMPONENTS = {
kanban: Kanban,
clawhub: ClawHub,
session: SessionTracker,
wiki: Wiki
}
function Window({ window, isActive, app }) {
const {
closeWindow,
minimizeWindow,
focusWindow,
updateWindowPosition
} = useApp()
const [isDragging, setIsDragging] = useState(false)
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 })
const windowRef = useRef(null)
const handleMouseDown = (e) => {
if (e.target.closest('.window-controls')) return
focusWindow(window.id)
setIsDragging(true)
const rect = windowRef.current.getBoundingClientRect()
setDragOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top
})
}
useEffect(() => {
const handleMouseMove = (e) => {
if (!isDragging) return
const newX = e.clientX - dragOffset.x
const newY = e.clientY - dragOffset.y
updateWindowPosition(window.id, newX, newY)
}
const handleMouseUp = () => {
setIsDragging(false)
}
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
}, [isDragging, dragOffset, window.id, updateWindowPosition])
const AppComponent = APP_COMPONENTS[window.app]
const Icon = app?.icon
return (
<div
ref={windowRef}
className="window"
style={{
left: window.x,
top: window.y,
width: window.width,
height: window.height,
zIndex: isActive ? 100 : 1,
opacity: isDragging ? 0.9 : 1
}}
onMouseDown={() => focusWindow(window.id)}
>
<div
className="window-header"
onMouseDown={handleMouseDown}
>
<div className="window-title">
{Icon && <Icon size={16} />}
<span>{window.title}</span>
</div>
<div className="window-controls">
<button
className="window-btn minimize"
onClick={() => minimizeWindow(window.id)}
title="Minimize"
/>
<button
className="window-btn maximize"
title="Maximize"
/>
<button
className="window-btn close"
onClick={() => closeWindow(window.id)}
title="Close"
>
<X size={10} color="white" />
</button>
</div>
</div>
<div className="window-content">
{AppComponent && <AppComponent />}
</div>
</div>
)
}
export default Window

View File

@ -0,0 +1,99 @@
import { createContext, useContext, useState, useCallback } from 'react'
const AppContext = createContext()
export function AppProvider({ children }) {
const [windows, setWindows] = useState([])
const [activeWindow, setActiveWindow] = useState(null)
const [startMenuOpen, setStartMenuOpen] = useState(false)
const [sessionActive, setSessionActive] = useState(false)
const [sessionStartTime, setSessionStartTime] = useState(null)
const openWindow = useCallback((app) => {
const id = Date.now()
const newWindow = {
id,
app: app.id,
title: app.title,
icon: app.icon,
x: 100 + (windows.length * 30),
y: 50 + (windows.length * 30),
width: app.defaultWidth || 800,
height: app.defaultHeight || 600,
minimized: false,
maximized: false
}
setWindows(prev => [...prev, newWindow])
setActiveWindow(id)
setStartMenuOpen(false)
}, [windows.length])
const closeWindow = useCallback((id) => {
setWindows(prev => prev.filter(w => w.id !== id))
if (activeWindow === id) {
setActiveWindow(null)
}
}, [activeWindow])
const minimizeWindow = useCallback((id) => {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, minimized: true } : w
))
setActiveWindow(null)
}, [])
const restoreWindow = useCallback((id) => {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, minimized: false } : w
))
setActiveWindow(id)
}, [])
const focusWindow = useCallback((id) => {
setActiveWindow(id)
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, minimized: false } : w
))
}, [])
const updateWindowPosition = useCallback((id, x, y) => {
setWindows(prev => prev.map(w =>
w.id === id ? { ...w, x, y } : w
))
}, [])
const startSession = useCallback(() => {
setSessionActive(true)
setSessionStartTime(new Date())
}, [])
const endSession = useCallback(() => {
setSessionActive(false)
setSessionStartTime(null)
}, [])
return (
<AppContext.Provider value={{
windows,
activeWindow,
startMenuOpen,
setStartMenuOpen,
openWindow,
closeWindow,
minimizeWindow,
restoreWindow,
focusWindow,
updateWindowPosition,
sessionActive,
sessionStartTime,
startSession,
endSession
}}>
{children}
</AppContext.Provider>
)
}
export function useApp() {
return useContext(AppContext)
}

View File

@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)