## 摘要 (Summary)
本 PR 整合了 `test` 分支中关于 **AI Agent 运维助手系统**
的核心实现、后续优化以及一系列高并发、竞态条件、连接保活及接口报错的修复工作。项目现已具备功能完整、安全可控、交互流畅且高度稳定的 AI
助手侧边栏。
---
## 核心变更内容 (Key Changes)
### 1. AI Agent 核心功能与工具链扩展
* **工具箱扩展**:实现了 8 个运维相关的核心工具,包括
`detect_environment`(环境探测)、`list_processes`(进程列表)、`service_manage`(服务管理)、`docker_manage`(容器管理)以及基础终端执行工具。
* **流式输出与 Markdown**:AI 助手的回复支持逐字流式加载显示,完成后使用 `marked` + `DOMPurify`
渲染(支持 GFM 表格),替代了原有的手写解析器。
*
**上下文管理**:引入了对话历史摘要机制(`trimMessages`),优化了上下文管理策略,同时将终端及环境上下文移入系统提示词(System
Prompt),防止跨请求历史数据重复与丢失。
### 2. 前端交互与 UX 深度优化
* **折叠式思考过程**:实现了 AI
思考过程折叠容器,工具调用不再占用主回复空间;支持思考步骤的实时预览与交互,完成后展开可查看完整执行链路。
* **样式与主题调整**:优化了 Agent 侧边栏宽度、输入框 Placeholder 颜色、发送按钮样式、自动换行与主题颜色的同步预览。
* **输入流并发保护**:在安全弹窗确认期间显式禁用输入,以“完成/取消”代替粗暴中断,避免指令状态冲突。
### 3. 并发、安全与系统稳定性修复 (核心痛点)
* **安全双层拦截**:重构安全分级策略,采用“LLM 主观判断 + 工具层硬编码规则兜底拦截”的双层过滤。对高危操作(如包管理
`apt/yum` 等)以及 `sudo` 操作增加强制用户确认流程。
* **彻底消除 API 400 错误**:修复了在消息裁剪、指令 Abort、用户取消等场景下,因 `tool_calls`
孤儿消息或消息不配对导致的大模型接口报 400 错误的问题;增加了消息完整性校验,自动过滤残留/不配对的工具消息。
* **防止 DO 冬眠与卡死**:
* 在 `runLoop` 中引入 5 秒一次的 `keepAlive` 保活机制,防止 Cloudflare Durable Object
在等待大模型响应或等待用户确认期间因无操作而冬眠。
* 将保活定时器提升为类字段,防止关闭连接时发生定时器泄漏。
* **网络与并发修复**:
* 修复了多命令执行导致的 SSH 频繁断开和前端 WebSocket 重连风暴。
* 解决了 `Channel removed before open` 竞态条件,以及 exec channel 未释放导致 SSH 报错
`Channel open failed (reason=2)` 的 channel 泄漏问题。
* 为 WebSocket Handler 增加了全局错误边界,避免未捕获异常导致 DO 意外崩溃断开。
### 4. SSH 基础体验优化
* **主机密钥验证优化 (TOFU)**:重构了主机密钥指纹变更提示,拆分多行展示已知指纹与实际指纹的对比,并动态识别并展示密钥算法类型(如
`ssh-ed25519`)。
### 5. 文档与部署优化
* **一键部署**:为 README 添加了 Cloudflare 一键部署按钮,并更新了最新的演示视频。
* **开发规约**:更新了 `AGENTS.md`,同步了 AI Agent 相关的 8 大工具、保活看门狗及两层安全策略的开发维护说明。
---------
Co-authored-by: TanXin <tanxin@example.com>
1972 lines
60 KiB
HTML
1972 lines
60 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CloudSSH Theme Editor</title>
|
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
|
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
background: #0f0f0f;
|
|
color: #ccc;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.editor-layout {
|
|
display: flex;
|
|
height: 100vh;
|
|
}
|
|
|
|
/* ===== Sidebar ===== */
|
|
.sidebar {
|
|
width: 340px;
|
|
min-width: 340px;
|
|
background: #161616;
|
|
border-right: 1px solid #2a2a2a;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #2a2a2a;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-header h1 {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #4af626;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.sidebar-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.sidebar-actions button {
|
|
background: transparent;
|
|
border: 1px solid #333;
|
|
color: #aaa;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 10px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.sidebar-actions button:hover {
|
|
border-color: #4af626;
|
|
color: #4af626;
|
|
}
|
|
|
|
.sidebar-actions button.primary {
|
|
border-color: #4af626;
|
|
color: #4af626;
|
|
}
|
|
|
|
.sidebar-actions button.primary:hover {
|
|
background: #4af626;
|
|
color: #000;
|
|
}
|
|
|
|
.presets-bar {
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid #2a2a2a;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.presets-bar label {
|
|
font-size: 10px;
|
|
color: #666;
|
|
letter-spacing: 0.1em;
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.preset-chips {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.preset-chip {
|
|
background: transparent;
|
|
border: 1px solid #333;
|
|
color: #888;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 10px;
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.preset-chip:hover, .preset-chip.active {
|
|
border-color: #4af626;
|
|
color: #4af626;
|
|
}
|
|
|
|
.sidebar-scroll {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 12px 20px 40px;
|
|
}
|
|
|
|
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
|
|
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
.sidebar-scroll::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
|
|
.section-title {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
color: #555;
|
|
letter-spacing: 0.15em;
|
|
margin: 16px 0 10px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1px solid #222;
|
|
}
|
|
|
|
.section-title:first-child { margin-top: 0; }
|
|
|
|
.color-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.color-row label {
|
|
font-size: 11px;
|
|
color: #888;
|
|
min-width: 140px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.color-row input[type="color"] {
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 1px solid #333;
|
|
border-radius: 4px;
|
|
padding: 2px;
|
|
background: #1a1a1a;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.color-row input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
|
|
.color-row input[type="color"]::-webkit-color-swatch { border: none; border-radius: 2px; }
|
|
|
|
.color-row input[type="text"] {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: #1a1a1a;
|
|
border: 1px solid #2a2a2a;
|
|
color: #ccc;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 11px;
|
|
padding: 5px 8px;
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.color-row input[type="text"]:focus {
|
|
border-color: #4af626;
|
|
}
|
|
|
|
/* ===== Preview Area ===== */
|
|
.preview-area {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
background: #0a0a0a;
|
|
}
|
|
|
|
.preview-area::-webkit-scrollbar { width: 8px; }
|
|
.preview-area::-webkit-scrollbar-track { background: #111; }
|
|
.preview-area::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
|
|
.preview-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: auto auto;
|
|
gap: 16px;
|
|
}
|
|
|
|
.preview-grid .col-terminal {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.preview-section {
|
|
min-width: 0;
|
|
}
|
|
|
|
.preview-label {
|
|
font-size: 10px;
|
|
color: #555;
|
|
letter-spacing: 0.15em;
|
|
margin-bottom: 8px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.preview-frame {
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid #222;
|
|
}
|
|
|
|
/* ===== Theme Variables (applied to preview) ===== */
|
|
.theme-scope {
|
|
--bg: #0a0a0a;
|
|
--bg-surface: #121212;
|
|
--bg-elevated: #131313;
|
|
--bg-terminal: #0e0e0e;
|
|
--text: #4af626;
|
|
--text-muted: #bbccb0;
|
|
--text-dim: #3c4b36;
|
|
--accent: #4af626;
|
|
--accent-secondary: #14d1ff;
|
|
--accent-secondary-light: #b7eaff;
|
|
--border: #1f1f1f;
|
|
--border-strong: #3c4b36;
|
|
--error: #ffb4ab;
|
|
--error-bg: #93000a;
|
|
--on-accent: #022100;
|
|
--surface-dot: #353534;
|
|
--scanline-tint: rgba(74, 246, 38, 0.02);
|
|
--accent-glow: rgba(74, 246, 38, 0.08);
|
|
--modal-overlay: rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
/* ===== Login Preview ===== */
|
|
.login-preview {
|
|
background: var(--bg);
|
|
padding: 24px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.login-preview .scanlines {
|
|
position: absolute; inset: 0;
|
|
background: linear-gradient(to bottom, transparent, transparent 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1));
|
|
background-size: 100% 4px;
|
|
pointer-events: none; z-index: 1;
|
|
}
|
|
|
|
.login-preview .flicker {
|
|
position: absolute; inset: 0;
|
|
background: var(--scanline-tint);
|
|
pointer-events: none; z-index: 1;
|
|
}
|
|
|
|
.login-card {
|
|
width: 100%;
|
|
max-width: 380px;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.login-title {
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.login-title .logo {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
letter-spacing: -0.05em;
|
|
}
|
|
|
|
.login-title .cursor {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 20px;
|
|
background: var(--accent-secondary);
|
|
margin-left: 2px;
|
|
vertical-align: middle;
|
|
animation: blink 1s step-end infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
.login-box {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
padding: 20px;
|
|
position: relative;
|
|
}
|
|
|
|
.login-box .top-bar {
|
|
position: absolute; top: 0; left: 0; right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(to right, transparent, var(--accent), transparent);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.login-box .header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
}
|
|
|
|
.login-box .header span {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: var(--accent-secondary);
|
|
}
|
|
|
|
.login-field {
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.login-field label {
|
|
display: block;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-muted);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.login-field .input-line {
|
|
display: flex;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.login-field .input-line .prefix {
|
|
color: var(--text-muted);
|
|
margin-right: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.login-field .input-line .placeholder-text {
|
|
color: var(--text-dim);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.login-field .input-line:focus-within {
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
.auth-tabs {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.auth-tab {
|
|
padding: 3px 10px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
border: 1px solid var(--border-strong);
|
|
color: var(--text-muted);
|
|
background: transparent;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.auth-tab.active {
|
|
background: var(--accent);
|
|
color: var(--on-accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.connect-btn {
|
|
width: 100%;
|
|
padding: 10px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
background: var(--accent);
|
|
color: var(--on-accent);
|
|
border: 1px solid var(--accent);
|
|
cursor: pointer;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
margin-top: 16px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.connect-btn:hover {
|
|
background: var(--accent-secondary-light);
|
|
border-color: var(--accent-secondary-light);
|
|
color: var(--bg);
|
|
}
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 14px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.status-dot {
|
|
display: inline-block;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--surface-dot);
|
|
margin-right: 4px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.status-text { color: var(--text-muted); }
|
|
|
|
.login-footer {
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.login-footer a {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
opacity: 0.6;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* ===== Terminal Preview ===== */
|
|
.terminal-preview {
|
|
background: var(--bg-terminal);
|
|
padding: 16px;
|
|
min-height: 240px;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.terminal-preview .scanline {
|
|
position: absolute; inset: 0;
|
|
background: linear-gradient(to bottom, transparent, transparent 50%, rgba(0,0,0,0.08) 50%, rgba(0,0,0,0.08));
|
|
background-size: 100% 4px;
|
|
pointer-events: none; z-index: 1;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.terminal-statusbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 10px;
|
|
color: var(--on-surface-variant);
|
|
padding-bottom: 10px;
|
|
margin-bottom: 10px;
|
|
border-bottom: 1px solid var(--border);
|
|
opacity: 0.8;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.terminal-statusbar .connected {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.terminal-statusbar .dot-green {
|
|
width: 6px; height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
}
|
|
|
|
.terminal-body {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.terminal-body .line { margin-bottom: 2px; }
|
|
.terminal-body .green { color: var(--accent); }
|
|
.terminal-body .cyan { color: var(--accent-secondary); }
|
|
.terminal-body .yellow { color: #e6db74; }
|
|
.terminal-body .red { color: var(--error); }
|
|
.terminal-body .gray { color: var(--text-dim); }
|
|
.terminal-body .white { color: var(--text); }
|
|
.terminal-body .prompt { color: var(--accent); font-weight: 600; }
|
|
.terminal-body .path { color: var(--accent-secondary); }
|
|
|
|
.terminal-input-mock {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-top: 6px;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.terminal-input-mock .prompt-sign {
|
|
color: var(--accent);
|
|
margin-right: 6px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.terminal-input-mock .cursor-block {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 14px;
|
|
background: var(--accent-secondary);
|
|
animation: blink 1s step-end infinite;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
/* ===== User Space Preview ===== */
|
|
.userspace-preview {
|
|
background: var(--bg-elevated);
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100%;
|
|
}
|
|
|
|
.us-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 16px;
|
|
height: 48px;
|
|
background: var(--bg-elevated);
|
|
border-bottom: 1px solid var(--border-strong);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.us-header .logo {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
letter-spacing: -0.05em;
|
|
}
|
|
|
|
.us-header .user-area {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.us-header .avatar {
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--accent);
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.us-header .username {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.us-header .logout-icon {
|
|
color: var(--error);
|
|
font-size: 16px;
|
|
}
|
|
|
|
.us-body {
|
|
flex: 1;
|
|
padding: 16px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.us-body .scanlines {
|
|
position: absolute; inset: 0;
|
|
background: var(--modal-overlay);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.us-section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.us-section-header h2 {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.us-section-header .sub {
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
opacity: 0.6;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.us-section-header .add-btn {
|
|
background: transparent;
|
|
border: 1px solid var(--accent);
|
|
color: var(--accent);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.us-cards-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.us-footer {
|
|
padding: 0 16px;
|
|
height: 36px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-top: 1px solid var(--border-strong);
|
|
font-size: 9px;
|
|
color: var(--on-surface-variant);
|
|
opacity: 0.6;
|
|
flex-shrink: 0;
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
/* ===== Server Cards Preview ===== */
|
|
.cards-preview {
|
|
background: var(--bg);
|
|
padding: 0;
|
|
}
|
|
|
|
.cards-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.cards-header h2 {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.cards-header .add-btn {
|
|
background: transparent;
|
|
border: 1px solid var(--accent);
|
|
color: var(--accent);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
padding: 5px 12px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.server-card-mock {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
padding: 14px;
|
|
position: relative;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.server-card-mock .top-line {
|
|
position: absolute; top: 0; left: 0; right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(to right, transparent, var(--border-strong), transparent);
|
|
}
|
|
|
|
.server-card-mock:hover {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 4px 20px var(--accent-glow);
|
|
}
|
|
|
|
.server-card-mock:hover .top-line {
|
|
background: linear-gradient(to right, transparent, var(--accent), transparent);
|
|
}
|
|
|
|
.card-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.card-title-row .icon {
|
|
color: var(--accent);
|
|
font-size: 16px;
|
|
}
|
|
|
|
.card-title-row h3 {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.card-badge {
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--border-strong);
|
|
padding: 2px 6px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.card-info {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.card-info div {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.card-info .label { color: var(--text-dim); }
|
|
.card-info .value { color: var(--on-surface); }
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.card-btn {
|
|
flex: 1;
|
|
padding: 4px 8px;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
background: transparent;
|
|
border: 1px solid var(--accent);
|
|
color: var(--accent);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
cursor: pointer;
|
|
text-transform: uppercase;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.card-btn .material-symbols-outlined { font-size: 12px; }
|
|
|
|
.card-btn.delete {
|
|
border-color: var(--error);
|
|
color: var(--error);
|
|
}
|
|
|
|
.card-btn.delete:hover {
|
|
background: var(--error-bg);
|
|
color: var(--error);
|
|
}
|
|
|
|
/* ===== Terminal + SFTP Layout ===== */
|
|
.terminal-sftp-preview {
|
|
display: flex;
|
|
min-height: 280px;
|
|
}
|
|
|
|
.terminal-sftp-preview .terminal-preview {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ===== SFTP Preview ===== */
|
|
.sftp-preview {
|
|
width: 320px;
|
|
min-width: 320px;
|
|
background: var(--bg-surface);
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-left: 1px solid var(--border-strong);
|
|
}
|
|
|
|
.sftp-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 12px;
|
|
height: 36px;
|
|
background: var(--bg-elevated);
|
|
border-bottom: 1px solid var(--border-strong);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-header .title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.sftp-header .title .icon {
|
|
color: var(--accent);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.sftp-header .title span {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.sftp-header .close-icon {
|
|
color: var(--on-surface-variant);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.sftp-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 6px 10px;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-toolbar .nav-icon {
|
|
color: var(--on-surface-variant);
|
|
font-size: 14px;
|
|
padding: 2px;
|
|
}
|
|
|
|
.sftp-toolbar .path-input {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
color: var(--on-surface);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 10px;
|
|
padding: 3px 6px;
|
|
outline: none;
|
|
}
|
|
|
|
.sftp-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding: 4px 10px;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding: 3px 6px;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
color: var(--accent);
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.sftp-action-btn.secondary {
|
|
color: var(--accent-secondary);
|
|
}
|
|
|
|
.sftp-action-btn .material-symbols-outlined {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.sftp-file-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.sftp-file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 5px 12px;
|
|
font-size: 11px;
|
|
color: var(--on-surface);
|
|
border-bottom: 1px solid rgba(255,255,255,0.03);
|
|
}
|
|
|
|
.sftp-file-item:hover {
|
|
background: var(--surface-dot);
|
|
}
|
|
|
|
.sftp-file-item.selected {
|
|
background: var(--surface-dot);
|
|
}
|
|
|
|
.sftp-file-item .file-icon {
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-file-item .file-icon.dir {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.sftp-file-item .file-icon.file {
|
|
color: var(--on-surface-variant);
|
|
}
|
|
|
|
.sftp-file-item .file-icon.link {
|
|
color: var(--accent-secondary);
|
|
}
|
|
|
|
.sftp-file-item .file-name {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sftp-file-item .file-name.dir {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.sftp-file-item .file-size {
|
|
color: var(--on-surface-variant);
|
|
font-size: 10px;
|
|
width: 60px;
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-file-item .file-perms {
|
|
color: var(--on-surface-variant);
|
|
font-size: 9px;
|
|
width: 70px;
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-file-item .file-date {
|
|
color: var(--on-surface-variant);
|
|
font-size: 9px;
|
|
width: 80px;
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sftp-statusbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 4px 12px;
|
|
background: var(--bg-elevated);
|
|
border-top: 1px solid var(--border-strong);
|
|
font-size: 9px;
|
|
color: var(--on-surface-variant);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ===== Tab Bar Preview ===== */
|
|
.preview-tab-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
background: var(--bg);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 8px;
|
|
height: 32px;
|
|
overflow-x: auto;
|
|
gap: 2px;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.preview-tab-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 8px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-bottom: none;
|
|
border-radius: 4px 4px 0 0;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
position: relative;
|
|
}
|
|
|
|
.preview-tab-item:hover {
|
|
color: var(--text);
|
|
background: var(--bg-elevated);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
.preview-tab-item.active {
|
|
color: var(--accent);
|
|
background: var(--bg-elevated);
|
|
border-color: var(--border-strong);
|
|
}
|
|
|
|
.preview-tab-item.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: var(--accent);
|
|
}
|
|
|
|
.preview-tab-item.disconnected {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.preview-tab-item.disconnected .tab-label {
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.preview-tab-dot {
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.preview-tab-dot.connected {
|
|
background: var(--accent);
|
|
box-shadow: 0 0 3px var(--accent);
|
|
}
|
|
|
|
.preview-tab-dot.disconnected {
|
|
background: var(--text-dim);
|
|
}
|
|
|
|
.preview-tab-close {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.preview-tab-item:hover .preview-tab-close,
|
|
.preview-tab-item.active .preview-tab-close {
|
|
opacity: 1;
|
|
}
|
|
|
|
.preview-tab-close:hover {
|
|
color: var(--error);
|
|
}
|
|
|
|
.preview-tab-new-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2px;
|
|
border: 1px dashed var(--border);
|
|
background: transparent;
|
|
color: var(--text-dim);
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.preview-tab-new-btn:hover {
|
|
color: var(--accent);
|
|
border-color: var(--accent);
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
/* ===== Agent Panel Preview ===== */
|
|
.preview-grid .col-agent {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.agent-preview {
|
|
background: var(--bg);
|
|
border-left: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
min-height: 260px;
|
|
}
|
|
|
|
.agent-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.agent-header-title {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
color: var(--accent-secondary);
|
|
}
|
|
|
|
.agent-close-icon {
|
|
font-size: 14px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.agent-messages {
|
|
flex: 1;
|
|
padding: 10px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* User message bubble */
|
|
.agent-msg-user {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 6px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.agent-msg-user .agent-msg-text {
|
|
background: color-mix(in srgb, var(--agent-user-color) 12%, transparent);
|
|
border: 1px solid color-mix(in srgb, var(--agent-user-color) 30%, transparent);
|
|
padding: 6px 10px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
color: var(--agent-user-color);
|
|
max-width: 70%;
|
|
}
|
|
|
|
.agent-icon-user {
|
|
font-size: 14px;
|
|
color: var(--agent-user-color);
|
|
margin-top: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Agent message */
|
|
.agent-msg-agent {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.agent-icon-bot {
|
|
font-size: 14px;
|
|
color: var(--agent-agent-color);
|
|
margin-top: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.agent-response-body {
|
|
font-size: 11px;
|
|
color: var(--on-surface);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.agent-response-body h3 {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: var(--agent-agent-color);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.agent-response-body p {
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.agent-table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
font-size: 10px;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.agent-table th,
|
|
.agent-table td {
|
|
padding: 3px 8px;
|
|
border-bottom: 1px solid color-mix(in srgb, var(--agent-agent-color) 20%, transparent);
|
|
text-align: left;
|
|
}
|
|
|
|
.agent-table th {
|
|
font-weight: 700;
|
|
color: var(--agent-agent-color);
|
|
background: color-mix(in srgb, var(--agent-agent-color) 10%, transparent);
|
|
}
|
|
|
|
.agent-code {
|
|
background: color-mix(in srgb, var(--agent-agent-color) 10%, transparent);
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
font-size: 10px;
|
|
}
|
|
|
|
/* Thinking Process (collapsed) */
|
|
.tp-container {
|
|
border: 1px solid color-mix(in srgb, var(--agent-agent-color) 20%, transparent);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tp-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 10px;
|
|
background: color-mix(in srgb, var(--agent-agent-color) 6%, transparent);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.tp-header .tp-chevron {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.tp-header .tp-icon {
|
|
font-size: 14px;
|
|
color: var(--agent-agent-color);
|
|
}
|
|
|
|
.tp-status-text {
|
|
color: var(--on-surface-variant);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Input bar */
|
|
.agent-input-bar {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: flex-end;
|
|
padding: 8px 12px;
|
|
border-top: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.agent-input {
|
|
flex: 1;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 6px 10px;
|
|
font-size: 11px;
|
|
color: var(--on-surface);
|
|
resize: none;
|
|
font-family: inherit;
|
|
min-height: 32px;
|
|
}
|
|
|
|
.agent-input::placeholder {
|
|
color: var(--on-surface-variant);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.agent-send-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: var(--accent);
|
|
color: var(--on-accent);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ===== Hidden file input ===== */
|
|
#import-input { display: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="editor-layout">
|
|
<!-- Sidebar -->
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>THEME_EDITOR</h1>
|
|
<div class="sidebar-actions">
|
|
<button id="btn-import" title="Import JSON">IMPORT</button>
|
|
<button id="btn-export" class="primary" title="Export JSON">EXPORT</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="presets-bar">
|
|
<label>PRESETS</label>
|
|
<div class="preset-chips">
|
|
<button class="preset-chip active" data-preset="cyberpunk">Cyberpunk</button>
|
|
<button class="preset-chip" data-preset="glacier">Glacier</button>
|
|
<button class="preset-chip" data-preset="gruvbox">Gruvbox</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-scroll" id="controls"></div>
|
|
</div>
|
|
|
|
<!-- Preview -->
|
|
<div class="preview-area">
|
|
<div class="theme-scope" id="theme-scope">
|
|
<div class="preview-grid">
|
|
|
|
<!-- Login Page (Top Left) -->
|
|
<div class="preview-section">
|
|
<div class="preview-label">LOGIN PAGE</div>
|
|
<div class="preview-frame">
|
|
<div class="login-preview">
|
|
<div class="scanlines"></div>
|
|
<div class="flicker"></div>
|
|
<div class="login-card">
|
|
<div class="login-title">
|
|
<span class="logo">CloudSSH</span><span class="cursor"></span>
|
|
</div>
|
|
<div class="login-box">
|
|
<div class="top-bar"></div>
|
|
<div class="header">
|
|
<span>CONNECTION_PARAMETERS</span>
|
|
<span class="material-symbols-outlined" style="font-size:14px;color:var(--accent-secondary);font-variation-settings:'FILL' 0">terminal</span>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:3fr 1fr;gap:12px;margin-bottom:12px">
|
|
<div class="login-field" style="margin-bottom:0">
|
|
<label>HOST_ADDRESS</label>
|
|
<div class="input-line">
|
|
<span class="prefix">></span>
|
|
<span class="placeholder-text">192.168.1.1</span>
|
|
</div>
|
|
</div>
|
|
<div class="login-field" style="margin-bottom:0">
|
|
<label>PORT</label>
|
|
<div class="input-line">
|
|
<span class="prefix">:</span>
|
|
<span class="placeholder-text">22</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="login-field">
|
|
<label>AUTH_USER</label>
|
|
<div class="input-line">
|
|
<span class="material-symbols-outlined prefix" style="font-size:14px">person</span>
|
|
<span class="placeholder-text">admin</span>
|
|
</div>
|
|
</div>
|
|
<div class="login-field">
|
|
<label>AUTH_METHOD</label>
|
|
<div class="auth-tabs">
|
|
<span class="auth-tab active">PASSWORD</span>
|
|
<span class="auth-tab">PRIVATE_KEY</span>
|
|
</div>
|
|
<div class="input-line" style="border-bottom:1px solid var(--border-strong);padding-bottom:6px">
|
|
<span class="material-symbols-outlined prefix" style="font-size:14px">key</span>
|
|
<span class="placeholder-text">••••••••</span>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:6px;margin-top:8px;margin-bottom:4px">
|
|
<span style="width:14px;height:14px;border:1px solid var(--border-strong);border-radius:2px;display:inline-block;background:var(--bg-surface)"></span>
|
|
<label style="font-size:10px;color:var(--text-muted);cursor:pointer">REMEMBER_CONNECTION</label>
|
|
</div>
|
|
<button class="connect-btn" style="margin-top:12px">
|
|
<span class="material-symbols-outlined" style="font-size:14px">power_settings_new</span>
|
|
Execute_Connection
|
|
</button>
|
|
<div class="status-bar">
|
|
<span class="status-text"><span class="status-dot"></span> STATUS: OFFLINE</span>
|
|
<span style="font-size:10px;color:var(--text-muted);opacity:0.5">GitHub</span>
|
|
</div>
|
|
<!-- Recent Connections Preview -->
|
|
<div style="margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); display: block;">
|
|
<label style="display: block; font-size: 10px; font-weight: 700; letter-spacing: 0.1em; color: var(--accent-secondary); margin-bottom: 8px;">RECENT_CONNECTIONS</label>
|
|
<div style="display: flex; flex-direction: column; gap: 6px;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; padding: 6px 8px; border: 1px solid var(--border); background: rgba(255,255,255,0.02); cursor: pointer;">
|
|
<div style="display: flex; align-items: center; gap: 6px; overflow: hidden; margin-right: 6px;">
|
|
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-muted)">history</span>
|
|
<span style="color: var(--on-surface); text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">admin@192.168.1.1:22</span>
|
|
<span style="font-size: 8px; font-weight: 700; letter-spacing: 0.05em; color: var(--text-muted); border: 1px solid var(--border-strong); padding: 1px 3px;">PWD</span>
|
|
</div>
|
|
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-dim)">close</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; padding: 6px 8px; border: 1px solid var(--border); background: rgba(255,255,255,0.02); cursor: pointer;">
|
|
<div style="display: flex; align-items: center; gap: 6px; overflow: hidden; margin-right: 6px;">
|
|
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-muted)">history</span>
|
|
<span style="color: var(--on-surface); text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">deploy@10.0.0.5:2222</span>
|
|
<span style="font-size: 8px; font-weight: 700; letter-spacing: 0.05em; color: var(--text-muted); border: 1px solid var(--border-strong); padding: 1px 3px;">KEY</span>
|
|
</div>
|
|
<span class="material-symbols-outlined" style="font-size: 12px; color: var(--text-dim)">close</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="login-footer">
|
|
<a href="#">[ GitHub Open Source ]</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Space (Top Right) -->
|
|
<div class="preview-section">
|
|
<div class="preview-label">USER SPACE</div>
|
|
<div class="preview-frame">
|
|
<div class="userspace-preview">
|
|
<div class="us-header">
|
|
<span class="logo">CloudSSH</span>
|
|
<div class="user-area">
|
|
<div class="avatar"></div>
|
|
<span class="username">admin</span>
|
|
<span class="material-symbols-outlined logout-icon" style="font-variation-settings:'FILL' 0">logout</span>
|
|
</div>
|
|
</div>
|
|
<div class="us-body">
|
|
<div class="scanlines"></div>
|
|
<div class="us-section-header">
|
|
<div>
|
|
<h2>SERVER_LIST</h2>
|
|
<div class="sub">Manage your SSH connections</div>
|
|
</div>
|
|
<span class="add-btn">
|
|
<span class="material-symbols-outlined" style="font-size:13px">add</span>
|
|
ADD_SERVER
|
|
</span>
|
|
</div>
|
|
<div class="us-cards-grid">
|
|
<div class="server-card-mock">
|
|
<div class="top-line"></div>
|
|
<div class="card-title-row">
|
|
<span class="material-symbols-outlined icon" style="font-variation-settings:'FILL' 0">dns</span>
|
|
<h3>Production</h3>
|
|
<span class="card-badge">PWD</span>
|
|
</div>
|
|
<div class="card-info">
|
|
<div><span class="label">HOST</span><span class="value">192.168.1.10:22</span></div>
|
|
<div><span class="label">USER</span><span class="value">root</span></div>
|
|
</div>
|
|
<div class="card-actions">
|
|
<span class="card-btn"><span class="material-symbols-outlined">power_settings_new</span> CONNECT</span>
|
|
<span class="card-btn"><span class="material-symbols-outlined">edit</span></span>
|
|
<span class="card-btn delete"><span class="material-symbols-outlined">delete</span></span>
|
|
</div>
|
|
</div>
|
|
<div class="server-card-mock">
|
|
<div class="top-line"></div>
|
|
<div class="card-title-row">
|
|
<span class="material-symbols-outlined icon" style="font-variation-settings:'FILL' 0">dns</span>
|
|
<h3>Staging</h3>
|
|
<span class="card-badge">KEY</span>
|
|
</div>
|
|
<div class="card-info">
|
|
<div><span class="label">HOST</span><span class="value">10.0.0.5:2222</span></div>
|
|
<div><span class="label">USER</span><span class="value">deploy</span></div>
|
|
</div>
|
|
<div class="card-actions">
|
|
<span class="card-btn"><span class="material-symbols-outlined">power_settings_new</span> CONNECT</span>
|
|
<span class="card-btn"><span class="material-symbols-outlined">edit</span></span>
|
|
<span class="card-btn delete"><span class="material-symbols-outlined">delete</span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="us-footer">
|
|
<span>© 2025 CLOUD_SSH_TERMINAL</span>
|
|
<span>Documentation</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Terminal + SFTP (Bottom, Full Width) -->
|
|
<div class="preview-section col-terminal">
|
|
<div class="preview-label">TERMINAL + SFTP</div>
|
|
<div class="preview-frame">
|
|
<!-- Tab Bar Preview -->
|
|
<div class="preview-tab-bar">
|
|
<div class="preview-tab-item active">
|
|
<span class="preview-tab-dot connected"></span>
|
|
<span class="tab-label">root@ubuntu</span>
|
|
<span class="material-symbols-outlined preview-tab-close" style="font-size:12px">close</span>
|
|
</div>
|
|
<div class="preview-tab-item">
|
|
<span class="preview-tab-dot connected"></span>
|
|
<span class="tab-label">Production</span>
|
|
<span class="material-symbols-outlined preview-tab-close" style="font-size:12px">close</span>
|
|
</div>
|
|
<div class="preview-tab-item disconnected">
|
|
<span class="preview-tab-dot disconnected"></span>
|
|
<span class="tab-label">10.0.0.5</span>
|
|
<span class="material-symbols-outlined preview-tab-close" style="font-size:12px">close</span>
|
|
</div>
|
|
<div class="preview-tab-new-btn">
|
|
<span class="material-symbols-outlined" style="font-size:12px">add</span>
|
|
</div>
|
|
</div>
|
|
<div class="terminal-sftp-preview">
|
|
<!-- Terminal Area -->
|
|
<div class="terminal-preview">
|
|
<div class="scanline"></div>
|
|
<div class="terminal-statusbar" style="display:flex;justify-content:space-between;">
|
|
<div style="display:flex;gap:20px;">
|
|
<span class="connected"><span class="dot-green"></span> Connected</span>
|
|
<span>Host: 192.168.1.1</span>
|
|
<span>User: root</span>
|
|
<span>Port: 22</span>
|
|
</div>
|
|
<div>
|
|
<span>⚡ CF-HKG: 34ms | RTT: 12ms</span>
|
|
</div>
|
|
</div>
|
|
<div class="terminal-body">
|
|
<div class="line green">[+] WebSocket connected, sending credentials...</div>
|
|
<div class="line green">[*] Authenticating...</div>
|
|
<div class="line green">[*] Authentication successful</div>
|
|
<div class="line white">Welcome to Ubuntu 22.04.3 LTS</div>
|
|
<div class="line white"> </div>
|
|
<div class="line"><span class="prompt">root@server</span><span class="white">:</span><span class="path">~</span><span class="white">$ ls -la</span></div>
|
|
<div class="line white">drwxr-xr-x 5 root root 4096 Jan 15 10:23 .</div>
|
|
<div class="line white">drwxr-xr-x 3 root root 4096 Jan 15 10:20 ..</div>
|
|
<div class="line cyan">-rw-r--r-- 1 root root 220 Jan 15 10:20 .bashrc</div>
|
|
<div class="line cyan">drwxr-xr-x 2 root root 4096 Jan 15 10:23 config</div>
|
|
<div class="line cyan">-rwxr-xr-x 1 root root 1024 Jan 15 10:23 deploy.sh</div>
|
|
<div class="line yellow">-rw-r--r-- 1 root root 512 Jan 15 10:22 README.md</div>
|
|
<div class="line white"> </div>
|
|
<div class="line"><span class="prompt">root@server</span><span class="white">:</span><span class="path">~</span><span class="white">$ </span><span class="cursor-block"></span></div>
|
|
</div>
|
|
</div>
|
|
<!-- SFTP Panel (Right Side) -->
|
|
<div class="sftp-preview">
|
|
<div class="sftp-header">
|
|
<div class="title">
|
|
<span class="material-symbols-outlined icon" style="font-variation-settings:'FILL' 1">folder_open</span>
|
|
<span>SFTP_FILE_MANAGER</span>
|
|
</div>
|
|
<span class="material-symbols-outlined close-icon">close</span>
|
|
</div>
|
|
<div class="sftp-toolbar">
|
|
<span class="material-symbols-outlined nav-icon">arrow_back</span>
|
|
<span class="material-symbols-outlined nav-icon">home</span>
|
|
<span class="material-symbols-outlined nav-icon">refresh</span>
|
|
<input class="path-input" value="/home/admin" readonly>
|
|
<span class="material-symbols-outlined nav-icon" style="color:var(--accent)">arrow_forward</span>
|
|
</div>
|
|
<div class="sftp-actions">
|
|
<button class="sftp-action-btn"><span class="material-symbols-outlined">upload_file</span>UPLOAD</button>
|
|
<button class="sftp-action-btn secondary"><span class="material-symbols-outlined">create_new_folder</span>MKDIR</button>
|
|
<button class="sftp-action-btn" style="opacity:0.3"><span class="material-symbols-outlined">download</span>DOWNLOAD</button>
|
|
<button class="sftp-action-btn" style="opacity:0.3;color:var(--error)"><span class="material-symbols-outlined">delete</span>DELETE</button>
|
|
</div>
|
|
<div class="sftp-file-list">
|
|
<div class="sftp-file-item">
|
|
<span class="material-symbols-outlined file-icon dir" style="font-variation-settings:'FILL' 1">folder</span>
|
|
<span class="file-name dir">.ssh</span>
|
|
<span class="file-size">-</span>
|
|
<span class="file-perms">drwx------</span>
|
|
<span class="file-date">Jan 10 09:15</span>
|
|
</div>
|
|
<div class="sftp-file-item">
|
|
<span class="material-symbols-outlined file-icon dir" style="font-variation-settings:'FILL' 1">folder</span>
|
|
<span class="file-name dir">projects</span>
|
|
<span class="file-size">-</span>
|
|
<span class="file-perms">drwxr-xr-x</span>
|
|
<span class="file-date">Jan 14 16:30</span>
|
|
</div>
|
|
<div class="sftp-file-item selected">
|
|
<span class="material-symbols-outlined file-icon dir" style="font-variation-settings:'FILL' 1">folder</span>
|
|
<span class="file-name dir">config</span>
|
|
<span class="file-size">-</span>
|
|
<span class="file-perms">drwxr-xr-x</span>
|
|
<span class="file-date">Jan 15 10:23</span>
|
|
</div>
|
|
<div class="sftp-file-item">
|
|
<span class="material-symbols-outlined file-icon file">draft</span>
|
|
<span class="file-name">.bashrc</span>
|
|
<span class="file-size">4.2 KB</span>
|
|
<span class="file-perms">-rw-r--r--</span>
|
|
<span class="file-date">Jan 10 09:00</span>
|
|
</div>
|
|
<div class="sftp-file-item">
|
|
<span class="material-symbols-outlined file-icon file">terminal</span>
|
|
<span class="file-name">deploy.sh</span>
|
|
<span class="file-size">1.0 KB</span>
|
|
<span class="file-perms">-rwxr-xr-x</span>
|
|
<span class="file-date">Jan 15 10:23</span>
|
|
</div>
|
|
<div class="sftp-file-item">
|
|
<span class="material-symbols-outlined file-icon file">description</span>
|
|
<span class="file-name">README.md</span>
|
|
<span class="file-size">512 B</span>
|
|
<span class="file-perms">-rw-r--r--</span>
|
|
<span class="file-date">Jan 12 14:20</span>
|
|
</div>
|
|
<div class="sftp-file-item">
|
|
<span class="material-symbols-outlined file-icon link">link</span>
|
|
<span class="file-name">logs -> /var/log/app</span>
|
|
<span class="file-size">-</span>
|
|
<span class="file-perms">lrwxrwxrwx</span>
|
|
<span class="file-date">Jan 08 11:00</span>
|
|
</div>
|
|
</div>
|
|
<div class="sftp-statusbar">
|
|
<span>Ready</span>
|
|
<span>2 dirs, 4 files</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Agent Panel (Below, Full Width) -->
|
|
<div class="preview-section col-agent">
|
|
<div class="preview-label">AI AGENT PANEL (THINKING PROGRESS)</div>
|
|
<div class="preview-frame">
|
|
<div class="agent-preview">
|
|
<!-- Agent Panel Header -->
|
|
<div class="agent-header">
|
|
<span class="agent-header-title">AI_AGENT</span>
|
|
<span class="material-symbols-outlined agent-close-icon">close</span>
|
|
</div>
|
|
<!-- Message List -->
|
|
<div class="agent-messages">
|
|
<!-- User Message -->
|
|
<div class="agent-msg-user">
|
|
<span class="agent-msg-text">帮我检查服务器的配置信息</span>
|
|
<span class="material-symbols-outlined agent-icon-user" style="font-variation-settings:'FILL' 1">person</span>
|
|
</div>
|
|
<!-- Thinking Process Container (Collapsed) -->
|
|
<div class="tp-container">
|
|
<div class="tp-header">
|
|
<span class="material-symbols-outlined tp-chevron">expand_more</span>
|
|
<span class="material-symbols-outlined tp-icon" style="font-variation-settings:'FILL' 1">check_circle</span>
|
|
<span class="tp-status-text">已完成 5 个步骤</span>
|
|
</div>
|
|
</div>
|
|
<!-- Agent Response (Markdown) -->
|
|
<div class="agent-msg-agent">
|
|
<span class="material-symbols-outlined agent-icon-bot" style="font-variation-settings:'FILL' 1">smart_toy</span>
|
|
<div class="agent-response-body">
|
|
<h3>服务器配置信息汇总</h3>
|
|
<table class="agent-table">
|
|
<tr><th>项目</th><th>值</th></tr>
|
|
<tr><td>操作系统</td><td>Ubuntu 24.04.4 LTS</td></tr>
|
|
<tr><td>内核版本</td><td>Linux 6.8.0 x86_64</td></tr>
|
|
<tr><td>CPU</td><td>1 vCPU @ 2494 MHz</td></tr>
|
|
<tr><td>内存</td><td>512 MiB (45% used)</td></tr>
|
|
</table>
|
|
<p>建议定期执行 <code class="agent-code">apt update && apt upgrade</code></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Input Area -->
|
|
<div class="agent-input-bar">
|
|
<textarea class="agent-input" placeholder="描述你想做的事..." readonly>描述你想做的事...</textarea>
|
|
<button class="agent-send-btn">
|
|
<span class="material-symbols-outlined" style="font-size:14px">arrow_upward</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="file" id="import-input" accept=".json">
|
|
|
|
<script>
|
|
const PRESETS = {
|
|
cyberpunk: {
|
|
name: 'Cyberpunk',
|
|
terminal: { background: '#0a0a0a', foreground: '#4af626', cursor: '#14d1ff', cursorAccent: '#0a0a0a', selectionBackground: '#273747' },
|
|
ui: {
|
|
'--bg': '#0a0a0a', '--bg-surface': '#121212', '--bg-elevated': '#131313', '--bg-terminal': '#0e0e0e',
|
|
'--text': '#4af626', '--text-muted': '#bbccb0', '--text-dim': '#3c4b36',
|
|
'--accent': '#4af626', '--accent-secondary': '#14d1ff', '--accent-secondary-light': '#b7eaff',
|
|
'--border': '#1f1f1f', '--border-strong': '#3c4b36',
|
|
'--error': '#ffb4ab', '--error-bg': '#93000a',
|
|
'--on-accent': '#022100', '--surface-dot': '#353534',
|
|
'--scanline-tint': 'rgba(74, 246, 38, 0.02)',
|
|
'--accent-glow': 'rgba(74, 246, 38, 0.08)',
|
|
'--modal-overlay': 'rgba(0, 0, 0, 0.8)',
|
|
'--on-surface': '#e5e2e1', '--on-surface-variant': '#bbccb0',
|
|
'--agent-user-color': '#4af626', '--agent-agent-color': '#14d1ff',
|
|
}
|
|
},
|
|
glacier: {
|
|
name: 'Glacier',
|
|
terminal: { background: '#0a192f', foreground: '#64ffda', cursor: '#e6f1ff', cursorAccent: '#0a192f', selectionBackground: '#112240' },
|
|
ui: {
|
|
'--bg': '#0a192f', '--bg-surface': '#0d2137', '--bg-elevated': '#112240', '--bg-terminal': '#061526',
|
|
'--text': '#64ffda', '--text-muted': '#8892b0', '--text-dim': '#495670',
|
|
'--accent': '#64ffda', '--accent-secondary': '#e6f1ff', '--accent-secondary-light': '#ccd6f6',
|
|
'--border': '#1d3557', '--border-strong': '#495670',
|
|
'--error': '#ff6b6b', '--error-bg': '#3d0000',
|
|
'--on-accent': '#0a192f', '--surface-dot': '#1d3557',
|
|
'--scanline-tint': 'rgba(100, 255, 218, 0.02)',
|
|
'--accent-glow': 'rgba(100, 255, 218, 0.08)',
|
|
'--modal-overlay': 'rgba(0, 0, 0, 0.85)',
|
|
'--on-surface': '#e6f1ff', '--on-surface-variant': '#8892b0',
|
|
'--agent-user-color': '#64ffda', '--agent-agent-color': '#e6f1ff',
|
|
}
|
|
},
|
|
gruvbox: {
|
|
name: 'Gruvbox',
|
|
terminal: { background: '#282828', foreground: '#ebdbb2', cursor: '#d3869b', cursorAccent: '#282828', selectionBackground: '#504945' },
|
|
ui: {
|
|
'--bg': '#282828', '--bg-surface': '#303030', '--bg-elevated': '#282828', '--bg-terminal': '#1d2021',
|
|
'--text': '#ebdbb2', '--text-muted': '#a89984', '--text-dim': '#665c54',
|
|
'--accent': '#b8bb26', '--accent-secondary': '#83a598', '--accent-secondary-light': '#8ec07c',
|
|
'--border': '#3c3836', '--border-strong': '#665c54',
|
|
'--error': '#fb4934', '--error-bg': '#3d0000',
|
|
'--on-accent': '#282828', '--surface-dot': '#3c3836',
|
|
'--scanline-tint': 'rgba(184, 187, 38, 0.02)',
|
|
'--accent-glow': 'rgba(184, 187, 38, 0.08)',
|
|
'--modal-overlay': 'rgba(0, 0, 0, 0.75)',
|
|
'--on-surface': '#ebdbb2', '--on-surface-variant': '#a89984',
|
|
'--agent-user-color': '#b8bb26', '--agent-agent-color': '#83a598',
|
|
}
|
|
}
|
|
};
|
|
|
|
const VARIABLE_GROUPS = [
|
|
{ title: 'BACKGROUNDS', vars: ['--bg', '--bg-surface', '--bg-elevated', '--bg-terminal'] },
|
|
{ title: 'TEXT COLORS', vars: ['--text', '--text-muted', '--text-dim', '--on-surface', '--on-surface-variant'] },
|
|
{ title: 'ACCENT COLORS', vars: ['--accent', '--accent-secondary', '--accent-secondary-light', '--on-accent'] },
|
|
{ title: 'BORDERS', vars: ['--border', '--border-strong'] },
|
|
{ title: 'ERROR', vars: ['--error', '--error-bg'] },
|
|
{ title: 'COMPONENTS', vars: ['--surface-dot'] },
|
|
{ title: 'AI AGENT', vars: ['--agent-user-color', '--agent-agent-color'] },
|
|
{ title: 'EFFECTS', vars: ['--scanline-tint', '--accent-glow', '--modal-overlay'] },
|
|
];
|
|
|
|
const VARIABLE_LABELS = {
|
|
'--bg': 'Page Background',
|
|
'--bg-surface': 'Card / Surface',
|
|
'--bg-elevated': 'Elevated Surface',
|
|
'--bg-terminal': 'Terminal Background',
|
|
'--text': 'Primary Text',
|
|
'--text-muted': 'Muted Text',
|
|
'--text-dim': 'Dim Text',
|
|
'--accent': 'Primary Accent',
|
|
'--accent-secondary': 'Secondary Accent',
|
|
'--accent-secondary-light': 'Secondary Light',
|
|
'--on-accent': 'Text on Accent',
|
|
'--border': 'Border',
|
|
'--border-strong': 'Strong Border',
|
|
'--error': 'Error Text',
|
|
'--error-bg': 'Error Background',
|
|
'--surface-dot': 'Status Dot',
|
|
'--agent-user-color': 'Agent User Color',
|
|
'--agent-agent-color': 'Agent Bot Color',
|
|
'--scanline-tint': 'Scanline Overlay',
|
|
'--accent-glow': 'Accent Glow',
|
|
'--modal-overlay': 'Modal Overlay',
|
|
'--on-surface': 'Surface Text',
|
|
'--on-surface-variant': 'Surface Text Variant',
|
|
};
|
|
|
|
let theme = {};
|
|
let activePreset = 'cyberpunk';
|
|
let customized = false;
|
|
|
|
function initTheme(presetName) {
|
|
const preset = PRESETS[presetName];
|
|
theme = { ...preset.ui };
|
|
activePreset = presetName;
|
|
customized = false;
|
|
renderControls();
|
|
applyTheme();
|
|
document.querySelectorAll('.preset-chip').forEach(c => {
|
|
c.classList.toggle('active', c.dataset.preset === presetName);
|
|
});
|
|
}
|
|
|
|
function isHexColor(val) {
|
|
return /^#[0-9a-fA-F]{6}$/.test(val) || /^#[0-9a-fA-F]{3}$/.test(val);
|
|
}
|
|
|
|
function hexToRgba(hex) {
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
return `rgba(${r}, ${g}, ${b}, 1)`;
|
|
}
|
|
|
|
function renderControls() {
|
|
const container = document.getElementById('controls');
|
|
let html = '';
|
|
|
|
for (const group of VARIABLE_GROUPS) {
|
|
html += `<div class="section-title">${group.title}</div>`;
|
|
for (const varName of group.vars) {
|
|
const val = theme[varName] || '';
|
|
const label = VARIABLE_LABELS[varName] || varName;
|
|
const isHex = isHexColor(val);
|
|
|
|
html += `<div class="color-row">`;
|
|
html += `<label>${label}</label>`;
|
|
if (isHex) {
|
|
html += `<input type="color" value="${val}" data-var="${varName}">`;
|
|
}
|
|
html += `<input type="text" value="${val}" data-var="${varName}" spellcheck="false">`;
|
|
html += `</div>`;
|
|
}
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
|
|
// Bind events
|
|
container.querySelectorAll('input[type="color"]').forEach(input => {
|
|
input.addEventListener('input', (e) => {
|
|
const varName = e.target.dataset.var;
|
|
theme[varName] = e.target.value;
|
|
const textInput = container.querySelector(`input[type="text"][data-var="${varName}"]`);
|
|
if (textInput) textInput.value = e.target.value;
|
|
customized = true;
|
|
applyTheme();
|
|
});
|
|
});
|
|
|
|
container.querySelectorAll('input[type="text"]').forEach(input => {
|
|
input.addEventListener('input', (e) => {
|
|
const varName = e.target.dataset.var;
|
|
const val = e.target.value;
|
|
theme[varName] = val;
|
|
const colorInput = container.querySelector(`input[type="color"][data-var="${varName}"]`);
|
|
if (colorInput && isHexColor(val)) {
|
|
colorInput.value = val.length === 4
|
|
? '#' + val[1]+val[1]+val[2]+val[2]+val[3]+val[3]
|
|
: val;
|
|
}
|
|
customized = true;
|
|
applyTheme();
|
|
});
|
|
});
|
|
}
|
|
|
|
function applyTheme() {
|
|
const scope = document.getElementById('theme-scope');
|
|
for (const [varName, val] of Object.entries(theme)) {
|
|
scope.style.setProperty(varName, val);
|
|
}
|
|
}
|
|
|
|
// Preset chips
|
|
document.querySelectorAll('.preset-chip').forEach(chip => {
|
|
chip.addEventListener('click', () => initTheme(chip.dataset.preset));
|
|
});
|
|
|
|
// Import
|
|
document.getElementById('btn-import').addEventListener('click', () => {
|
|
document.getElementById('import-input').click();
|
|
});
|
|
|
|
document.getElementById('import-input').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
try {
|
|
const data = JSON.parse(ev.target.result);
|
|
if (data.ui && typeof data.ui === 'object') {
|
|
theme = { ...data.ui };
|
|
customized = false;
|
|
if (data.name) {
|
|
document.querySelector('.preset-chip.active')?.classList.remove('active');
|
|
activePreset = null;
|
|
}
|
|
renderControls();
|
|
applyTheme();
|
|
} else {
|
|
alert('Invalid theme file: missing "ui" field');
|
|
}
|
|
} catch {
|
|
alert('Invalid JSON file');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
e.target.value = '';
|
|
});
|
|
|
|
// Export
|
|
document.getElementById('btn-export').addEventListener('click', () => {
|
|
const isCustom = customized || !activePreset;
|
|
const preset = activePreset ? PRESETS[activePreset] : null;
|
|
const themeName = isCustom ? 'Custom' : (preset?.name || 'Custom');
|
|
|
|
const output = {
|
|
name: themeName,
|
|
terminal: (isCustom || !preset) ? {
|
|
background: theme['--bg-terminal'] || '#0a0a0a',
|
|
foreground: theme['--text'] || '#cccccc',
|
|
cursor: theme['--accent-secondary'] || '#ffffff',
|
|
cursorAccent: theme['--bg-terminal'] || '#0a0a0a',
|
|
selectionBackground: theme['--border-strong'] || '#333333',
|
|
} : preset.terminal,
|
|
ui: { ...theme },
|
|
};
|
|
|
|
const json = JSON.stringify(output, null, 2);
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `cloudssh-theme-${themeName.toLowerCase().replace(/\s+/g, '-')}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
// Init
|
|
initTheme('cyberpunk');
|
|
</script>
|
|
</body>
|
|
</html>
|