Files
TanXinandTanXin 7e20e2d117 release: 发布 v1.13.0 Theme V3 主题系统版本 (#107)
## 发布说明

**v1.13.0 Theme V3 主题系统版本**

### 关联 Issue

- 关联 #99(「能不能直接在网页加个密码?」):本版本实现其中**主题苹果风**部分——新增 Apple 内置主题(macOS
风格浅色)并落地 Theme V3 主题系统;**网页访问密码**部分未包含在本版本,issue 保持开启、留待后续版本。

### 版本亮点

- **内置主题 5 → 7 款**:新增 CRT Amber(CRT 磷光深色:径向暗角背景 + 扫描线/闪烁/辉光/噪点全开 + 方形
mono 造型)与 Glass(蓝紫浅色玻璃态:mesh 粉彩漂移背景 + 强表面模糊 + 1.4× 圆角 + 分段控件);Glacier
替换为 Apple(macOS 风格浅色,完整 ANSI 16 色无障碍变体)
- **Theme V3 声明式令牌系统**(schemaVersion
3):`background`(solid/linear/radial/mesh 四类型、停靠点白名单、读性遮罩下限
0.25/0.35、drift 动画)、`effects`(scanline/flicker/glow/noise 强度钳制
0-1、data-fx-* 门控、Cyberpunk
默认视觉不变)、`typography`(字号/圆角缩放)、`appearance.blur` 独立表面模糊档位
- **零 DOM 操作、前后端同源校验**:背景/效果全部 CSS 变量 + 属性驱动;V2 主题数据自动升级、glacier
baseTheme 优雅降级;导入/导出双向兼容
- **在线主题编辑器支持 V3**:背景层/效果/版式三组面板 + blur 档位,导出 schemaVersion 3
- **全量文档对齐**:README/tests README/AGENTS/CHANGELOG 与当前实现同步

### 提交列表

| 提交 | 说明 |
| ---- | ---- |
| 268a892 | feat: 内置主题 Glacier 替换为 Apple 浅色主题 |
| 86e3ddf | feat: Theme V3 主题系统落地——背景层、效果注册表与版式缩放 |
| 9092e2a | release: 发布 v1.13.0(版本号 + CHANGELOG) |
| c098a7c | docs: 全量文档与当前实现对齐(测试清单 + 措辞修正) |

### 验证结果

- ✅ typecheck(worker + frontend + tests 三工程)
- ✅ vitest 54 文件 / 649 用例
- ✅ build:frontend 可复现内联构建(html.ts 再生成)
- ✅ 主题 e2e 6/6(含 CRT/Glass 切换、drift 属性、强模糊与 body::before 背景计算值断言)

---------

Co-authored-by: TanXin <tanxin@example.com>
2026-08-27 16:05:14 +08:00

4292 lines
142 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--editor-bg: #0a0a0a;
--editor-surface: #121212;
--editor-elevated: #131313;
--editor-text: #e5e2e1;
--editor-muted: #bbccb0;
--editor-dim: #3c4b36;
--editor-accent: #4af626;
--editor-secondary: #14d1ff;
--editor-border: #1f1f1f;
--editor-border-strong: #3c4b36;
--editor-error: #ffb4ab;
--editor-glow: rgba(74, 246, 38, 0.08);
}
body {
font-family: 'JetBrains Mono', monospace;
background: var(--editor-bg);
color: var(--editor-text);
height: 100vh;
overflow: hidden;
}
.editor-layout {
display: flex;
height: 100vh;
}
/* ===== Sidebar ===== */
.sidebar {
width: 340px;
min-width: 340px;
background: var(--editor-elevated);
border-right: 1px solid var(--editor-border-strong);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid var(--editor-border-strong);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.editor-brand {
min-width: 0;
}
.editor-brand__logo {
font-size: 15px;
font-weight: 700;
color: var(--editor-accent);
letter-spacing: -0.04em;
}
.editor-brand__title {
margin-top: 2px;
font-size: 9px;
font-weight: 700;
color: var(--editor-secondary);
letter-spacing: 0.12em;
}
.sidebar-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.sidebar-actions button {
background: transparent;
border: 1px solid var(--editor-border-strong);
color: var(--editor-muted);
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 4px 8px;
cursor: pointer;
transition: color 0.15s, background-color 0.15s, border-color 0.15s,
box-shadow 0.15s, transform 0.15s, opacity 0.15s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.sidebar-actions button:hover {
border-color: var(--editor-accent);
color: var(--editor-accent);
box-shadow: 0 0 10px var(--editor-glow);
}
.sidebar-actions button.primary {
border-color: var(--editor-accent);
color: var(--editor-accent);
}
.sidebar-actions button.primary:hover {
background: var(--editor-accent);
color: #000;
}
.presets-bar {
padding: 12px 20px;
border-bottom: 1px solid var(--editor-border-strong);
flex-shrink: 0;
}
.presets-bar label {
font-size: 10px;
color: var(--editor-muted);
opacity: 0.65;
letter-spacing: 0.1em;
display: block;
margin-bottom: 8px;
}
.preset-select-wrap {
position: relative;
}
.preset-select {
width: 100%;
min-width: 0;
appearance: none;
background: var(--editor-bg);
border: 1px solid var(--editor-border-strong);
color: var(--editor-text);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
line-height: 1.4;
padding: 7px 32px 7px 10px;
cursor: pointer;
transition: color 0.15s, background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.preset-select:hover,
.preset-select:focus {
border-color: var(--editor-accent);
color: var(--editor-accent);
outline: none;
box-shadow: 0 0 10px var(--editor-glow);
}
.preset-select-wrap .material-symbols-outlined {
position: absolute;
top: 50%;
right: 9px;
color: var(--editor-muted);
font-size: 16px;
pointer-events: none;
transform: translateY(-50%);
}
.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: var(--editor-border-strong); border-radius: 3px; }
.section-title {
font-size: 10px;
font-weight: 700;
color: var(--editor-muted);
opacity: 0.65;
letter-spacing: 0.15em;
margin: 16px 0 10px;
padding-bottom: 6px;
border-bottom: 1px solid var(--editor-border);
}
.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: var(--editor-muted);
min-width: 140px;
flex-shrink: 0;
}
.color-row input[type="color"] {
width: 28px;
height: 28px;
border: 1px solid var(--editor-border-strong);
border-radius: 4px;
padding: 2px;
background: var(--editor-surface);
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: var(--editor-surface);
border: 1px solid var(--editor-border);
color: var(--editor-text);
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: var(--editor-accent);
}
.color-row input[type="text"][aria-invalid="true"] {
border-color: #ff5f57;
box-shadow: 0 0 0 1px rgba(255, 95, 87, 0.2);
}
.sidebar-actions button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.setting-row {
display: grid;
grid-template-columns: minmax(110px, 1fr) minmax(118px, 1fr);
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.setting-row label {
min-width: 0;
color: var(--editor-muted);
font-size: 11px;
}
.setting-row input:not([type='range']),
.setting-row select {
width: 100%;
min-width: 0;
background: var(--editor-surface);
border: 1px solid var(--editor-border);
color: var(--editor-text);
font: inherit;
font-size: 11px;
padding: 6px 8px;
outline: none;
}
.setting-row:has(input[type='range']) {
grid-template-columns: minmax(110px, 1fr) minmax(96px, 1fr) 44px;
}
.setting-row .range-value {
font-size: 11px;
color: var(--editor-muted);
text-align: right;
}
.setting-row input:focus,
.setting-row select:focus {
border-color: var(--editor-accent);
}
/* ===== Preview Area ===== */
.preview-area {
flex: 1;
overflow-y: auto;
padding: 20px;
background: var(--editor-bg);
}
.preview-area::-webkit-scrollbar { width: 8px; }
.preview-area::-webkit-scrollbar-track { background: var(--editor-surface); }
.preview-area::-webkit-scrollbar-thumb { background: var(--editor-border-strong); 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: var(--editor-muted);
opacity: 0.65;
letter-spacing: 0.15em;
margin-bottom: 8px;
font-weight: 700;
}
.preview-frame {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--editor-border);
}
/* ===== 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;
--scrollbar-track: rgba(28, 27, 27, 0.5);
--scrollbar-thumb: rgba(60, 75, 54, 0.8);
--scrollbar-thumb-hover: rgba(134, 149, 125, 0.8);
--scanline-tint: rgba(74, 246, 38, 0.02);
--accent-glow: rgba(74, 246, 38, 0.08);
--accent-bg: rgba(74, 246, 38, 0.1);
--modal-overlay: rgba(0, 0, 0, 0.8);
--on-surface: #e5e2e1;
--on-surface-variant: #bbccb0;
--agent-user-color: #4af626;
--agent-agent-color: #14d1ff;
--font-ui: 'JetBrains Mono', ui-monospace, monospace;
--font-size-scale: 0.94;
--space-scale: 0.84;
--control-radius: 2px;
--card-radius: 0px;
--control-padding-x: 8px;
--control-padding-y: 5px;
--card-padding: 14px;
--panel-gap: 10px;
--shadow-card: none;
--motion-fast: 140ms;
--motion-normal: 220ms;
--hover-lift: -2px;
font-family: var(--font-ui);
font-size: calc(1em * var(--font-size-scale));
position: relative;
isolation: isolate;
background-color: var(--bg);
background-image: var(--app-bg-stack, none);
}
/* V3:预览背景漂移(motion=none 时显式停止) */
.theme-scope[data-bg-animation='drift'] {
background-size: 200% 200%;
animation: theme-editor-bg-drift 40s ease-in-out infinite alternate;
}
.theme-scope[data-ui-motion='none'] {
animation: none;
}
@keyframes theme-editor-bg-drift {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
/* V3:预览噪点层(垫在内容之下,仅作用于背景) */
.theme-scope[data-fx-noise='on']::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
opacity: var(--fx-noise-opacity, 0.05);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='en'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23en)'/%3E%3C/svg%3E");
}
/* V3:表面模糊档位(后置覆盖 shadow 派生值) */
.theme-scope[data-ui-blur='none'] {
--surface-blur: 0px;
}
.theme-scope[data-ui-blur='subtle'] {
--surface-blur: 6px;
}
.theme-scope[data-ui-blur='strong'] {
--surface-blur: 16px;
}
/* V3:预览辉光 */
.theme-scope[data-fx-glow='on'] .server-card {
box-shadow: var(--shadow-card), 0 4px calc(24px * var(--fx-glow-strength, 0.6)) var(--accent-glow);
}
.theme-scope[data-ui-shape="square"] {
--control-radius: 2px;
--card-radius: 0px;
}
.theme-scope[data-ui-shape="rounded"] {
--control-radius: 6px;
--card-radius: 9px;
}
.theme-scope[data-ui-shape="soft"] {
--control-radius: 10px;
--card-radius: 15px;
}
.theme-scope[data-ui-density="compact"] {
--font-size-scale: 0.94;
--space-scale: 0.84;
--control-padding-x: 7px;
--control-padding-y: 4px;
--card-padding: 12px;
--panel-gap: 8px;
}
.theme-scope[data-ui-density="comfortable"] {
--font-size-scale: 1;
--space-scale: 1;
--control-padding-x: 9px;
--control-padding-y: 6px;
--card-padding: 14px;
--panel-gap: 12px;
}
.theme-scope[data-ui-density="spacious"] {
--font-size-scale: 1.04;
--space-scale: 1.12;
--control-padding-x: 11px;
--control-padding-y: 8px;
--card-padding: 18px;
--panel-gap: 16px;
}
.theme-scope[data-ui-font="mono"] {
--font-ui: 'JetBrains Mono', ui-monospace, monospace;
}
.theme-scope[data-ui-font="system"] {
--font-ui: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.theme-scope[data-ui-shadow="none"] {
--shadow-card: none;
}
.theme-scope[data-ui-shadow="subtle"] {
--shadow-card: 0 4px 16px color-mix(in srgb, var(--bg) 24%, transparent);
}
.theme-scope[data-ui-shadow="elevated"] {
--shadow-card: 0 12px 32px color-mix(in srgb, var(--bg) 32%, transparent);
}
.theme-scope[data-ui-motion="none"] {
--motion-fast: 0ms;
--motion-normal: 0ms;
--hover-lift: 0px;
}
.theme-scope[data-ui-motion="reduced"] {
--motion-fast: 80ms;
--motion-normal: 140ms;
--hover-lift: -1px;
}
.theme-scope[data-ui-motion="full"] {
--motion-fast: 140ms;
--motion-normal: 220ms;
--hover-lift: -2px;
}
.theme-scope :is(
.preview-frame,
.login-box,
.server-card-mock,
.terminal-preview,
.sftp-preview,
.agent-preview
) {
border-radius: var(--card-radius);
box-shadow: var(--shadow-card);
transition: background-color var(--motion-normal) ease,
border-color var(--motion-normal) ease,
border-radius var(--motion-normal) ease,
box-shadow var(--motion-normal) ease,
color var(--motion-normal) ease,
transform var(--motion-normal) ease;
}
.theme-scope :is(
.login-field .input-line,
.search-box,
.sftp-toolbar .path-input,
.agent-input
) {
border-radius: var(--control-radius);
padding: var(--control-padding-y) var(--control-padding-x);
}
.theme-scope :is(
.connect-btn,
.cards-header .add-btn,
.card-connect,
.sftp-action-btn,
.agent-send-btn,
.agent-md-code-action
) {
border-radius: var(--control-radius);
padding: var(--control-padding-y) var(--control-padding-x);
transition: background-color var(--motion-fast) ease,
border-color var(--motion-fast) ease,
border-radius var(--motion-fast) ease,
box-shadow var(--motion-fast) ease,
color var(--motion-fast) ease,
transform var(--motion-fast) ease;
}
.theme-scope :is(.server-card-mock, .login-box) {
padding: var(--card-padding);
}
.theme-scope :is(.cards-grid, .terminal-sftp-preview) {
gap: var(--panel-gap);
}
.theme-scope[data-component-input="underline"] :is(
.login-field .input-line,
.search-box,
.sftp-toolbar .path-input,
.agent-input
) {
background: transparent;
border: 0;
border-bottom: 1px solid var(--border-strong);
border-radius: 0;
}
.theme-scope[data-component-input="boxed"] :is(
.login-field .input-line,
.search-box,
.sftp-toolbar .path-input,
.agent-input
) {
background: color-mix(in srgb, var(--bg-terminal) 88%, var(--bg-surface));
border: 1px solid var(--border-strong);
}
.theme-scope[data-component-button="outline"] :is(
.connect-btn,
.cards-header .add-btn,
.card-connect,
.sftp-action-btn,
.agent-send-btn
) {
color: var(--accent);
background: transparent;
border-color: var(--accent);
}
.theme-scope[data-component-button="solid"] :is(
.connect-btn,
.cards-header .add-btn,
.card-connect,
.sftp-action-btn,
.agent-send-btn
) {
color: var(--on-accent);
background: var(--accent);
border-color: var(--accent);
}
.theme-scope[data-component-button="soft"] :is(
.connect-btn,
.cards-header .add-btn,
.card-connect,
.sftp-action-btn,
.agent-send-btn
) {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, var(--bg-surface));
border-color: color-mix(in srgb, var(--accent) 38%, var(--border));
}
.theme-scope[data-component-card="flat"] :is(.login-box, .server-card-mock, .agent-preview) {
border-color: transparent;
box-shadow: none;
}
.theme-scope[data-component-card="outlined"] :is(.login-box, .server-card-mock, .agent-preview) {
border-color: var(--border);
}
.theme-scope[data-component-card="elevated"] :is(.login-box, .server-card-mock, .agent-preview) {
background: color-mix(in srgb, var(--bg-surface) 96%, var(--accent) 4%);
border-color: color-mix(in srgb, var(--border) 72%, transparent);
}
.theme-scope[data-component-tabs="segmented"] :is(.auth-tab, .preview-tab-item) {
margin: 3px 2px;
border: 1px solid transparent;
border-radius: var(--control-radius);
background: color-mix(in srgb, var(--bg-elevated) 86%, transparent);
}
.theme-scope[data-component-tabs="segmented"] :is(.auth-tab.active, .preview-tab-item.active) {
background: var(--accent-bg);
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.theme-scope[data-component-tabs="segmented"] .preview-tab-item.active::after {
display: none;
}
.theme-scope:not([data-fx-scanline='on']) :is(.scanlines, .scanline),
.theme-scope:not([data-fx-flicker='on']) .flicker {
display: none;
}
.theme-scope[data-fx-scanline='on'] .scanlines {
opacity: var(--fx-scanline-opacity, 1);
}
.theme-scope[data-fx-flicker='on'] .flicker {
animation-duration: var(--fx-flicker-speed, 0.15s);
}
.theme-scope[data-ui-motion="none"] * {
animation: none !important;
transition: none !important;
}
/* ===== 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: background-color 0.2s, border-color 0.2s, color 0.2s,
box-shadow 0.2s, transform 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);
}
.network-latency-preview {
display: flex;
align-items: center;
gap: 9px;
white-space: nowrap;
}
.network-latency-item {
display: inline-flex;
align-items: center;
gap: 4px;
}
.network-quality-dot {
width: 7px;
height: 7px;
border-radius: 999px;
flex-shrink: 0;
}
.network-quality-good {
background: #22c55e;
box-shadow: 0 0 5px rgba(34, 197, 94, 0.65);
}
.network-quality-fair {
background: #f59e0b;
box-shadow: 0 0 5px rgba(245, 158, 11, 0.65);
}
.network-quality-poor {
background: #ef4444;
box-shadow: 0 0 5px rgba(239, 68, 68, 0.65);
}
.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); }
.ask-ai-selection-preview {
position: absolute;
right: 18px;
bottom: 16px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 7px;
color: var(--accent);
background: var(--bg-elevated);
border: 1px solid var(--accent);
border-radius: 4px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
font-size: 9px;
font-weight: 700;
white-space: nowrap;
}
.ask-ai-selection-preview .material-symbols-outlined {
font-size: 13px;
}
.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-search-preview {
position: relative;
z-index: 1;
width: min(260px, 100%);
margin-bottom: 12px;
}
.us-search-preview .material-symbols-outlined {
position: absolute;
left: 9px;
top: 50%;
color: var(--text-dim);
font-size: 14px;
pointer-events: none;
transform: translateY(-50%);
}
.us-search-preview input {
width: 100%;
min-width: 0;
padding: 6px 10px 6px 30px;
color: var(--on-surface);
background: var(--bg-surface);
border: 1px solid var(--border-strong);
border-radius: 4px;
font: inherit;
font-size: 10px;
}
.us-search-preview input::placeholder {
color: var(--text-muted);
opacity: 0.75;
}
.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);
flex-shrink: 0;
}
.card-info .value { color: var(--on-surface); }
.card-info .card-region-row {
align-items: center;
flex-wrap: nowrap;
}
.card-info .region-value {
display: inline-flex;
align-items: center;
gap: 3px;
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-info .region-value .material-symbols-outlined {
color: var(--accent-secondary);
font-size: 10px;
}
.card-region-tag {
margin-left: 2px;
padding: 1px 3px;
color: var(--text-dim);
border: 1px solid var(--border-strong);
font-size: 8px;
line-height: 1;
flex-shrink: 0;
}
.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;
overflow-x: auto;
}
.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;
flex-shrink: 0;
}
.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: color 0.15s, opacity 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-response-body .agent-md-code-block {
margin-top: 8px;
overflow: hidden;
background: color-mix(in srgb, var(--agent-agent-color) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--agent-agent-color) 15%, transparent);
border-radius: 6px;
}
.agent-response-body .agent-md-code-toolbar {
min-height: 28px;
display: flex;
align-items: center;
gap: 6px;
padding: 3px 5px 3px 8px;
border-bottom: 1px solid color-mix(in srgb, var(--agent-agent-color) 12%, transparent);
}
.agent-response-body .agent-md-lang {
color: var(--agent-agent-color);
font-size: 9px;
opacity: 0.65;
flex-shrink: 0;
}
.agent-response-body .agent-md-code-meta {
min-width: 0;
flex: 1;
overflow: hidden;
color: var(--on-surface-variant);
font-size: 9px;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-response-body .agent-md-code-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.agent-response-body .agent-md-code-action {
min-height: 22px;
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 5px;
color: var(--agent-agent-color);
background: color-mix(in srgb, var(--agent-agent-color) 6%, transparent);
border: 1px solid color-mix(in srgb, var(--agent-agent-color) 22%, transparent);
border-radius: 4px;
font: inherit;
font-size: 9px;
}
.agent-response-body .agent-md-code-action .material-symbols-outlined {
font-size: 12px;
}
.agent-response-body .agent-md-pre {
margin: 0;
padding: 8px 10px;
overflow-x: auto;
color: var(--agent-agent-color);
font-size: 10px;
white-space: pre-wrap;
}
/* 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;
}
/* ===== Shared latest-project controls ===== */
.language-toggle {
display: inline-flex;
align-items: center;
gap: 5px;
color: var(--editor-muted);
background: color-mix(in srgb, var(--editor-surface) 82%, transparent);
border: 1px solid var(--editor-border-strong);
border-radius: 4px;
padding: 5px 8px;
font: inherit;
font-size: 10px;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: color 160ms ease, border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
}
.language-toggle:hover {
color: var(--editor-secondary);
border-color: var(--editor-secondary);
background: var(--editor-glow);
box-shadow: 0 0 10px var(--editor-glow);
}
.language-toggle:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 2px solid var(--editor-secondary);
outline-offset: 2px;
}
.language-toggle .material-symbols-outlined {
color: var(--editor-secondary);
font-size: 14px;
}
.preview-language-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--on-surface-variant);
background: color-mix(in srgb, var(--bg-surface) 82%, transparent);
border: 1px solid var(--border-strong);
border-radius: 4px;
padding: 3px 6px;
font-size: 8px;
font-weight: 600;
line-height: 1;
}
.preview-language-toggle .material-symbols-outlined {
color: var(--accent-secondary);
font-size: 11px;
}
.terminal-appbar {
min-height: 44px;
padding: 6px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: var(--bg);
border-bottom: 1px solid var(--border-strong);
}
.terminal-appbar__brand {
color: var(--accent);
font-size: 13px;
font-weight: 700;
letter-spacing: -0.04em;
}
.terminal-appbar__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
color: var(--accent);
}
.terminal-theme-select {
color: var(--on-surface-variant);
background: var(--surface-dot);
border: 0;
border-radius: 3px;
padding: 3px 5px;
font: inherit;
font-size: 8px;
}
.terminal-tool-icon {
font-size: 14px;
color: var(--accent);
}
.toast-region {
position: fixed;
top: 16px;
right: 16px;
z-index: 1000;
width: min(360px, calc(100vw - 32px));
pointer-events: none;
}
.editor-toast {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
color: var(--editor-text);
background: var(--editor-elevated);
border: 1px solid var(--editor-secondary);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
font-size: 11px;
line-height: 1.5;
pointer-events: auto;
animation: toast-in 160ms ease-out;
}
.editor-toast.error {
border-color: var(--editor-error);
}
.editor-toast .material-symbols-outlined {
color: var(--editor-secondary);
font-size: 16px;
flex-shrink: 0;
}
.editor-toast.error .material-symbols-outlined {
color: var(--editor-error);
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 1180px) {
.preview-grid {
grid-template-columns: 1fr;
}
.preview-grid .col-terminal,
.preview-grid .col-agent {
grid-column: 1;
}
}
@media (max-width: 760px) {
body {
height: auto;
min-height: 100vh;
overflow: auto;
}
.editor-layout {
display: block;
height: auto;
min-height: 100vh;
}
.sidebar {
width: 100%;
min-width: 0;
max-height: 54vh;
border-right: 0;
border-bottom: 1px solid var(--editor-border-strong);
}
.sidebar-header {
align-items: flex-start;
gap: 12px;
}
.preview-area {
padding: 12px;
overflow: visible;
}
.terminal-sftp-preview {
flex-direction: column;
}
.sftp-preview {
width: 100%;
min-width: 0;
border-left: 0;
border-top: 1px solid var(--border-strong);
}
.terminal-statusbar > div:first-child {
gap: 8px !important;
flex-wrap: wrap;
}
.sftp-file-item .file-perms,
.sftp-file-item .file-date {
display: none;
}
.us-cards-grid,
.cards-grid {
grid-template-columns: 1fr;
}
}
/* ===== Hidden file input ===== */
#import-input { display: none; }
</style>
</head>
<body>
<div class="editor-layout">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<div class="editor-brand">
<div class="editor-brand__logo">CloudSSH</div>
<div class="editor-brand__title" data-i18n="editor.title">主题在线编辑器</div>
</div>
<div class="sidebar-actions">
<button id="language-toggle" class="language-toggle" type="button">
<span class="material-symbols-outlined" aria-hidden="true">language</span>
<span id="language-toggle-label">English</span>
</button>
<button id="btn-import" data-i18n-title="editor.importTitle" title="导入 JSON">
<span class="material-symbols-outlined" style="font-size:13px">upload_file</span>
<span data-i18n="editor.import">导入</span>
</button>
<button id="btn-export" class="primary" data-i18n-title="editor.exportTitle" title="导出 JSON">
<span class="material-symbols-outlined" style="font-size:13px">download</span>
<span data-i18n="editor.export">导出</span>
</button>
</div>
</div>
<div class="presets-bar">
<label for="preset-select" data-i18n="editor.presets">预设主题</label>
<div class="preset-select-wrap">
<select id="preset-select" class="preset-select">
<option value="custom" data-i18n="editor.custom" disabled>自定义</option>
<option value="standard-dark">Standard Dark</option>
<option value="standard-light">Standard Light</option>
<option value="cyberpunk" selected>Cyberpunk</option>
<option value="apple">Apple</option>
<option value="gruvbox">Gruvbox</option>
<option value="crt">CRT Amber</option>
<option value="glass">Glass</option>
</select>
<span class="material-symbols-outlined" aria-hidden="true">expand_more</span>
</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" data-i18n="preview.login">匿名连接</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 data-i18n="auth.connectionParameters">连接参数</span>
<span style="display:flex;align-items:center;gap:6px">
<span class="preview-language-toggle">
<span class="material-symbols-outlined" aria-hidden="true">language</span>
<span data-language-preview-label>English</span>
</span>
<span class="material-symbols-outlined" style="font-size:14px;color:var(--accent-secondary);font-variation-settings:'FILL' 0">terminal</span>
</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 data-i18n="auth.host">主机地址</label>
<div class="input-line">
<span class="prefix">&gt;</span>
<span class="placeholder-text">192.168.1.1</span>
</div>
</div>
<div class="login-field" style="margin-bottom:0">
<label data-i18n="auth.port">端口</label>
<div class="input-line">
<span class="prefix">:</span>
<span class="placeholder-text">22</span>
</div>
</div>
</div>
<div class="login-field">
<label data-i18n="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 data-i18n="auth.method">认证方式</label>
<div class="auth-tabs">
<span class="auth-tab active" data-i18n="common.password">密码</span>
<span class="auth-tab" data-i18n="common.privateKey">私钥</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 class="login-field">
<label><span data-i18n="auth.regionHint">连接区域</span> <span style="font-size:8px;opacity:0.6;font-weight:400" data-i18n="common.optional">可选</span></label>
<div class="input-line" style="border-bottom:1px solid var(--border-strong);padding-bottom:6px;justify-content:space-between">
<span class="placeholder-text" data-i18n="region.auto">自动(保存时由系统推断)</span>
<span class="material-symbols-outlined" style="font-size:12px;color:var(--text-muted)">expand_more</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" data-i18n="auth.remember">记住连接信息</label>
</div>
<button class="connect-btn" style="margin-top:12px">
<span class="material-symbols-outlined" style="font-size:14px">power_settings_new</span>
<span data-i18n="auth.execute">建立连接</span>
</button>
<div class="status-bar">
<span class="status-text"><span class="status-dot"></span> <span data-i18n="auth.statusOffline">状态:离线</span></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;" data-i18n="auth.recent">最近连接</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" data-i18n="preview.userSpace">用户空间</div>
<div class="preview-frame">
<div class="userspace-preview">
<div class="us-header">
<span class="logo">CloudSSH</span>
<div class="user-area">
<span class="preview-language-toggle">
<span class="material-symbols-outlined" aria-hidden="true">language</span>
<span data-language-preview-label>English</span>
</span>
<div class="avatar"></div>
<span class="username">admin</span>
<span class="material-symbols-outlined" style="color:var(--on-surface-variant);font-size:16px;font-variation-settings:'FILL' 0">psychology</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 data-i18n="server.list">服务器列表</h2>
<div class="sub" data-i18n="server.manage">管理你的 SSH 连接</div>
</div>
<span class="add-btn">
<span class="material-symbols-outlined" style="font-size:13px">add</span>
<span data-i18n="server.add">添加服务器</span>
</span>
</div>
<div class="us-search-preview">
<span class="material-symbols-outlined" aria-hidden="true">search</span>
<input type="search" data-i18n-placeholder="server.searchPlaceholder" placeholder="按名称、主机或用户名搜索" readonly>
</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" data-i18n="server.hostLabel">主机</span><span class="value">192.168.1.10:22</span></div>
<div><span class="label" data-i18n="server.userLabel">用户</span><span class="value">root</span></div>
<div class="card-region-row">
<span class="label" data-i18n="server.regionLabel">区域</span>
<span class="value region-value"><span class="material-symbols-outlined">my_location</span>APAC NE</span>
<span class="card-region-tag" data-i18n="server.regionAuto">自动</span>
</div>
</div>
<div class="card-actions">
<span class="card-btn"><span class="material-symbols-outlined">power_settings_new</span> <span data-i18n="common.connect">连接</span></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" data-i18n="server.hostLabel">主机</span><span class="value">10.0.0.5:2222</span></div>
<div><span class="label" data-i18n="server.userLabel">用户</span><span class="value">deploy</span></div>
<div class="card-region-row">
<span class="label" data-i18n="server.regionLabel">区域</span>
<span class="value region-value"><span class="material-symbols-outlined">my_location</span>WEUR</span>
<span class="card-region-tag" data-i18n="server.regionManual">手动</span>
</div>
</div>
<div class="card-actions">
<span class="card-btn"><span class="material-symbols-outlined">power_settings_new</span> <span data-i18n="common.connect">连接</span></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>
<!-- Server Modal Preview -->
<div class="us-section-header" style="margin-top:16px">
<div>
<h2><span data-i18n="server.add">添加服务器</span> <span style="font-size:10px;color:var(--text-muted);font-weight:400" data-i18n="preview.modal">(弹窗预览)</span></h2>
</div>
</div>
<div style="background:var(--bg-surface);border:1px solid var(--border-strong);border-radius:8px;padding:16px;position:relative">
<div style="position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(to right,transparent,var(--accent-secondary),transparent);opacity:0.5;border-radius:8px 8px 0 0"></div>
<div style="font-size:10px;font-weight:700;letter-spacing:0.1em;color:var(--accent-secondary);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border-strong)" data-i18n="server.add">添加服务器</div>
<div class="login-field">
<label data-i18n="server.name">服务器名称</label>
<div class="input-line">
<span class="material-symbols-outlined prefix" style="font-size:14px">label</span>
<span class="placeholder-text">My Server</span>
</div>
</div>
<div style="display:grid;grid-template-columns:3fr 1fr;gap:12px;margin-bottom:12px">
<div class="login-field" style="margin-bottom:0">
<label data-i18n="auth.host">主机地址</label>
<div class="input-line">
<span class="prefix">&gt;</span>
<span class="placeholder-text">192.168.1.1</span>
</div>
</div>
<div class="login-field" style="margin-bottom:0">
<label data-i18n="auth.port">端口</label>
<div class="input-line">
<span class="prefix">:</span>
<span class="placeholder-text">22</span>
</div>
</div>
</div>
<div class="login-field">
<label data-i18n="auth.user">用户名</label>
<div class="input-line">
<span class="material-symbols-outlined prefix" style="font-size:14px">person</span>
<span class="placeholder-text">root</span>
</div>
</div>
<div class="login-field">
<label data-i18n="auth.method">认证方式</label>
<div class="auth-tabs">
<span class="auth-tab active" data-i18n="common.password">密码</span>
<span class="auth-tab" data-i18n="common.privateKey">私钥</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 class="login-field">
<label data-i18n="auth.regionHint">连接区域</label>
<div class="input-line" style="border-bottom:1px solid var(--border-strong);padding-bottom:6px;justify-content:space-between">
<span class="placeholder-text" data-i18n="region.auto">自动(保存时由系统推断)</span>
<span class="material-symbols-outlined" style="font-size:12px;color:var(--text-muted)">expand_more</span>
</div>
</div>
<button class="connect-btn" style="margin-top:12px">
<span class="material-symbols-outlined" style="font-size:14px">save</span>
<span data-i18n="server.save">保存服务器</span>
</button>
</div>
</div>
<div class="us-footer">
<span>© <span data-current-year></span> CLOUD_SSH_TERMINAL</span>
<span data-i18n="common.documentation">文档</span>
</div>
</div>
</div>
</div>
<!-- Terminal + SFTP (Bottom, Full Width) -->
<div class="preview-section col-terminal">
<div class="preview-label" data-i18n="preview.terminalSftp">终端与 SFTP</div>
<div class="preview-frame">
<div class="terminal-appbar">
<span class="terminal-appbar__brand">CloudSSH</span>
<div class="terminal-appbar__actions">
<select class="terminal-theme-select" aria-label="Theme">
<option value="custom" data-i18n="editor.custom" disabled>自定义</option>
<option value="standard-dark">Standard Dark</option>
<option value="standard-light">Standard Light</option>
<option value="cyberpunk" selected>Cyberpunk</option>
<option value="apple">Apple</option>
<option value="gruvbox">Gruvbox</option>
<option value="crt">CRT Amber</option>
<option value="glass">Glass</option>
</select>
<span class="material-symbols-outlined terminal-tool-icon">upload_file</span>
<span class="material-symbols-outlined terminal-tool-icon">folder_open</span>
<span class="material-symbols-outlined terminal-tool-icon">search</span>
<span class="material-symbols-outlined terminal-tool-icon">download</span>
<span class="material-symbols-outlined terminal-tool-icon">psychology</span>
<span class="material-symbols-outlined terminal-tool-icon">power_settings_new</span>
</div>
</div>
<!-- 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> <span data-i18n="terminal.connected">已连接</span></span>
<span data-i18n-template="terminal.host" data-value="192.168.1.1">主机:192.168.1.1</span>
<span data-i18n-template="terminal.user" data-value="root">用户:root</span>
<span data-i18n-template="terminal.port" data-value="22">端口:22</span>
</div>
<div class="network-latency-preview">
<span class="network-latency-item"><span class="network-quality-dot network-quality-good" aria-hidden="true"></span>CF-HKG: 34ms</span>
<span class="network-latency-item"><span class="network-quality-dot network-quality-fair" aria-hidden="true"></span>RTT: 126ms</span>
</div>
</div>
<div class="terminal-body">
<div class="line green" data-i18n="terminal.wsSendingCredentials">[+] WebSocket 已连接,正在发送凭据…</div>
<div class="line green" data-i18n="terminal.wsAuthenticating">[*] WebSocket 已连接,正在认证…</div>
<div class="line green" data-i18n="terminal.authSuccess">[*] 认证成功</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 class="ask-ai-selection-preview">
<span class="material-symbols-outlined" aria-hidden="true">quick_phrases</span>
<span data-i18n="terminal.askAISelection">询问 AI 助手</span>
</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 data-i18n="sftp.title">SFTP 文件管理器</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><span data-i18n="sftp.uploadAction">上传文件</span></button>
<button class="sftp-action-btn secondary"><span class="material-symbols-outlined">create_new_folder</span><span data-i18n="sftp.mkdirAction">新建文件夹</span></button>
<button class="sftp-action-btn" style="opacity:0.3"><span class="material-symbols-outlined">download</span><span data-i18n="sftp.downloadAction">下载</span></button>
<button class="sftp-action-btn" style="opacity:0.3;color:var(--error)"><span class="material-symbols-outlined">delete</span><span data-i18n="sftp.deleteAction">删除</span></button>
<button class="sftp-action-btn" style="opacity:0.3;color:var(--on-surface-variant)"><span class="material-symbols-outlined">edit</span><span data-i18n="sftp.renameAction">重命名</span></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 data-i18n="sftp.ready">就绪</span>
<span data-i18n="sftp.count">2 个目录,4 个文件</span>
</div>
</div>
</div>
</div>
</div>
<!-- AI Agent Panel (Below, Full Width) -->
<div class="preview-section col-agent">
<div class="preview-label" data-i18n="preview.agent">AI Agent 面板(思考进度)</div>
<div class="preview-frame">
<div class="agent-preview">
<!-- Agent Panel Header -->
<div class="agent-header">
<span class="agent-header-title" data-i18n="agent.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" data-i18n="agent.sampleRequest">帮我检查服务器的配置信息</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" data-i18n="agent.completedSteps">已完成 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 data-i18n="agent.sampleTitle">服务器配置信息汇总</h3>
<table class="agent-table">
<tr><th data-i18n="agent.item">项目</th><th data-i18n="agent.value">值</th></tr>
<tr><td data-i18n="agent.os">操作系统</td><td>Ubuntu 24.04.4 LTS</td></tr>
<tr><td data-i18n="agent.kernel">内核版本</td><td>Linux 6.8.0 x86_64</td></tr>
<tr><td>CPU</td><td>1 vCPU @ 2494 MHz</td></tr>
<tr><td data-i18n="agent.memory">内存</td><td>512 MiB (45% used)</td></tr>
</table>
<p data-i18n="agent.sampleAdvice">建议先检查服务状态:</p>
<div class="agent-md-code-block">
<div class="agent-md-code-toolbar">
<span class="agent-md-lang">bash</span>
<span class="agent-md-code-meta" data-i18n-template="agent.codeTarget" data-value="Production · root@192.168.1.10:22">目标:Production · root@192.168.1.10:22</span>
<div class="agent-md-code-actions">
<button class="agent-md-code-action" type="button">
<span class="material-symbols-outlined" aria-hidden="true">content_copy</span>
<span data-i18n="agent.codeCopy">复制</span>
</button>
<button class="agent-md-code-action" type="button">
<span class="material-symbols-outlined" aria-hidden="true">input</span>
<span data-i18n="agent.codeFill">填入终端</span>
</button>
</div>
</div>
<pre class="agent-md-pre"><code>systemctl status nginx</code></pre>
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="agent-input-bar">
<textarea class="agent-input" data-i18n-placeholder="agent.placeholder" placeholder="描述你希望 Agent 完成的任务…" 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">
<div id="toast-region" class="toast-region" role="status" aria-live="polite"></div>
<script>
const TRANSLATIONS = {
'zh-CN': {
'page.title': 'CloudSSH 主题在线编辑器',
'language.zhCN': '简体中文',
'language.enUS': 'English',
'language.switchTo': '切换到{language}',
'editor.title': '主题在线编辑器',
'editor.import': '导入',
'editor.export': '导出',
'editor.importTitle': '导入主题 JSON',
'editor.exportTitle': '导出主题 JSON',
'editor.presets': '预设主题',
'editor.custom': '自定义',
'preview.login': '匿名连接',
'preview.userSpace': '用户空间',
'preview.modal': '(弹窗预览)',
'preview.terminalSftp': '终端与 SFTP',
'preview.agent': 'AI Agent 面板(思考进度)',
'common.optional': '可选',
'common.password': '密码',
'common.privateKey': '私钥',
'common.connect': '连接',
'common.documentation': '文档',
'auth.connectionParameters': '连接参数',
'auth.host': '主机地址',
'auth.port': '端口',
'auth.user': '用户名',
'auth.method': '认证方式',
'auth.regionHint': '连接区域',
'auth.remember': '记住连接信息',
'auth.execute': '建立连接',
'auth.statusOffline': '状态:离线',
'auth.recent': '最近连接',
'region.auto': '自动(保存时由系统推断)',
'server.list': '服务器列表',
'server.manage': '管理你的 SSH 连接',
'server.add': '添加服务器',
'server.searchPlaceholder': '按名称、主机或用户名搜索',
'server.hostLabel': '主机',
'server.userLabel': '用户',
'server.regionLabel': '区域',
'server.regionAuto': '自动',
'server.regionManual': '手动',
'server.name': '服务器名称',
'server.save': '保存服务器',
'terminal.connected': '已连接',
'terminal.host': '主机:{value}',
'terminal.user': '用户:{value}',
'terminal.port': '端口:{value}',
'terminal.wsSendingCredentials': '[+] WebSocket 已连接,正在发送凭据…',
'terminal.wsAuthenticating': '[*] WebSocket 已连接,正在认证…',
'terminal.authSuccess': '[*] 认证成功',
'terminal.askAISelection': '询问 AI 助手',
'sftp.title': 'SFTP 文件管理器',
'sftp.uploadAction': '上传文件',
'sftp.mkdirAction': '新建文件夹',
'sftp.downloadAction': '下载',
'sftp.deleteAction': '删除',
'sftp.renameAction': '重命名',
'sftp.ready': '就绪',
'sftp.count': '2 个目录,4 个文件',
'agent.title': 'AI Agent 助手',
'agent.sampleRequest': '帮我检查服务器的配置信息',
'agent.completedSteps': '已完成 5 个步骤',
'agent.sampleTitle': '服务器配置信息汇总',
'agent.item': '项目',
'agent.value': '值',
'agent.os': '操作系统',
'agent.kernel': '内核版本',
'agent.memory': '内存',
'agent.sampleAdvice': '建议先检查服务状态:',
'agent.codeTarget': '目标:{value}',
'agent.codeCopy': '复制',
'agent.codeFill': '填入终端',
'agent.placeholder': '描述你希望 Agent 完成的任务…',
'feedback.imported': '主题导入成功。',
'feedback.exported': '主题 JSON 已导出。',
'feedback.missingUi': '主题文件不包含有效的颜色、终端或外观配置。',
'feedback.invalidJson': '文件不是有效的主题 JSON。',
'feedback.fileTooLarge': '主题文件不能超过 64 KiB。',
'feedback.invalidColor': '请先修正标红的无效颜色值。',
'group.basic': '主题信息',
'group.appearance': '整体外观',
'group.componentStyles': '组件样式',
'group.themeV3Background': '背景层(V3)',
'group.themeV3Effects': '效果(V3)',
'group.themeV3Typography': '版式缩放(V3)',
'background.type': '背景类型',
'background.angle': '渐变角度',
'background.scrim': '读性遮罩',
'background.animation': '背景动画',
'background.stop1': '停靠点 1',
'background.stop2': '停靠点 2',
'background.stop3': '停靠点 3',
'background.stop4': '停靠点 4',
'effects.scanline': '扫描线',
'effects.flicker': '闪烁',
'effects.glow': '辉光',
'effects.noise': '噪点',
'typography.fontScale': '字号缩放',
'typography.radiusScale': '圆角缩放',
'appearance.name': '主题名称',
'appearance.colorScheme': '明暗模式',
'appearance.style': '整体风格',
'appearance.shape': '形状',
'appearance.density': '密度',
'appearance.font': '字体',
'appearance.shadow': '阴影',
'appearance.motion': '动效',
'appearance.blur': '表面模糊',
'appearance.button': '按钮',
'appearance.input': '输入框',
'appearance.card': '卡片',
'appearance.tabs': '标签页',
'option.dark': '深色',
'option.light': '浅色',
'option.standard': '标准',
'option.cyberpunk': '赛博朋克',
'option.soft': '柔和',
'option.dense': '紧凑',
'option.square': '方正',
'option.rounded': '圆角',
'option.compact': '紧凑',
'option.comfortable': '舒适',
'option.spacious': '宽松',
'option.mono': '等宽字体',
'option.system': '系统字体',
'option.none': '无',
'option.subtle': '轻微',
'option.elevated': '悬浮',
'option.solid': '纯色',
'option.linear': '线性渐变',
'option.radial': '径向渐变',
'option.mesh': '网格渐变',
'option.drift': '缓慢漂移',
'option.reduced': '精简',
'option.full': '完整',
'option.outline': '描边',
'option.solid': '实心',
'option.underline': '下划线',
'option.boxed': '方框',
'option.outlined': '描边',
'option.flat': '扁平',
'option.segmented': '分段',
'group.backgrounds': '背景颜色',
'group.text': '文本颜色',
'group.accents': '强调颜色',
'group.borders': '边框',
'group.error': '错误状态',
'group.components': '组件',
'group.agent': 'AI Agent',
'group.effects': '视觉效果',
'variable.bg': '页面背景',
'variable.bgSurface': '卡片 / 表面',
'variable.bgElevated': '浮层表面',
'variable.bgTerminal': '终端背景',
'variable.text': '主要文本',
'variable.textMuted': '次要文本',
'variable.textDim': '弱化文本',
'variable.accent': '主要强调色',
'variable.accentSecondary': '次要强调色',
'variable.accentSecondaryLight': '次要浅色',
'variable.onAccent': '强调色上的文本',
'variable.border': '边框',
'variable.borderStrong': '强边框',
'variable.error': '错误文本',
'variable.errorBg': '错误背景',
'variable.surfaceDot': '状态点',
'variable.scrollbarTrack': '滚动条轨道',
'variable.scrollbarThumb': '滚动条滑块',
'variable.scrollbarThumbHover': '滚动条悬停',
'variable.agentUser': '用户消息',
'variable.agentBot': 'Agent 消息',
'variable.scanline': '扫描线叠层',
'variable.accentGlow': '强调色光晕',
'variable.accentBg': '强调色背景',
'variable.modalOverlay': '弹窗遮罩',
'variable.onSurface': '表面文本',
'variable.onSurfaceVariant': '表面次要文本',
},
'en-US': {
'page.title': 'CloudSSH Theme Editor',
'language.zhCN': '简体中文',
'language.enUS': 'English',
'language.switchTo': 'Switch to {language}',
'editor.title': 'THEME EDITOR',
'editor.import': 'IMPORT',
'editor.export': 'EXPORT',
'editor.importTitle': 'Import theme JSON',
'editor.exportTitle': 'Export theme JSON',
'editor.presets': 'PRESETS',
'editor.custom': 'CUSTOM',
'preview.login': 'LOGIN PAGE',
'preview.userSpace': 'USER SPACE',
'preview.modal': '(MODAL PREVIEW)',
'preview.terminalSftp': 'TERMINAL + SFTP',
'preview.agent': 'AI AGENT PANEL (THINKING PROGRESS)',
'common.optional': 'Optional',
'common.password': 'PASSWORD',
'common.privateKey': 'PRIVATE KEY',
'common.connect': 'CONNECT',
'common.documentation': 'Documentation',
'auth.connectionParameters': 'CONNECTION PARAMETERS',
'auth.host': 'HOST ADDRESS',
'auth.port': 'PORT',
'auth.user': 'USERNAME',
'auth.method': 'AUTHENTICATION METHOD',
'auth.regionHint': 'CONNECTION REGION',
'auth.remember': 'REMEMBER CONNECTION',
'auth.execute': 'CONNECT',
'auth.statusOffline': 'STATUS: OFFLINE',
'auth.recent': 'RECENT CONNECTIONS',
'region.auto': 'Auto (detected when saved)',
'server.list': 'SERVER LIST',
'server.manage': 'Manage your SSH connections',
'server.add': 'ADD SERVER',
'server.searchPlaceholder': 'Search by name, host, or username',
'server.hostLabel': 'HOST',
'server.userLabel': 'USER',
'server.regionLabel': 'REGION',
'server.regionAuto': 'AUTO',
'server.regionManual': 'MANUAL',
'server.name': 'SERVER NAME',
'server.save': 'SAVE SERVER',
'terminal.connected': 'Connected',
'terminal.host': 'Host: {value}',
'terminal.user': 'User: {value}',
'terminal.port': 'Port: {value}',
'terminal.wsSendingCredentials': '[+] WebSocket connected, sending credentials…',
'terminal.wsAuthenticating': '[*] WebSocket connected, authenticating…',
'terminal.authSuccess': '[*] Authentication successful',
'terminal.askAISelection': 'Ask AI assistant',
'sftp.title': 'SFTP FILE MANAGER',
'sftp.uploadAction': 'UPLOAD',
'sftp.mkdirAction': 'MKDIR',
'sftp.downloadAction': 'DOWNLOAD',
'sftp.deleteAction': 'DELETE',
'sftp.renameAction': 'RENAME',
'sftp.ready': 'Ready',
'sftp.count': '2 dirs, 4 files',
'agent.title': 'AI AGENT ASSISTANT',
'agent.sampleRequest': 'Check the server configuration for me',
'agent.completedSteps': 'Completed 5 steps',
'agent.sampleTitle': 'Server configuration summary',
'agent.item': 'Item',
'agent.value': 'Value',
'agent.os': 'Operating system',
'agent.kernel': 'Kernel',
'agent.memory': 'Memory',
'agent.sampleAdvice': 'Check the service status first:',
'agent.codeTarget': 'Target: {value}',
'agent.codeCopy': 'Copy',
'agent.codeFill': 'Fill terminal',
'agent.placeholder': 'Describe what you want the Agent to do…',
'feedback.imported': 'Theme imported.',
'feedback.exported': 'Theme JSON exported.',
'feedback.missingUi': 'The theme file has no valid color, terminal, or appearance settings.',
'feedback.invalidJson': 'The file is not valid theme JSON.',
'feedback.fileTooLarge': 'Theme files must not exceed 64 KiB.',
'feedback.invalidColor': 'Fix the invalid color values highlighted in red first.',
'group.basic': 'THEME INFORMATION',
'group.appearance': 'APPEARANCE',
'group.componentStyles': 'COMPONENT STYLES',
'group.themeV3Background': 'BACKGROUND (V3)',
'group.themeV3Effects': 'EFFECTS (V3)',
'group.themeV3Typography': 'TYPOGRAPHY SCALE (V3)',
'background.type': 'Background type',
'background.angle': 'Gradient angle',
'background.scrim': 'Readability scrim',
'background.animation': 'Background animation',
'background.stop1': 'Stop 1',
'background.stop2': 'Stop 2',
'background.stop3': 'Stop 3',
'background.stop4': 'Stop 4',
'effects.scanline': 'Scanline',
'effects.flicker': 'Flicker',
'effects.glow': 'Glow',
'effects.noise': 'Noise',
'typography.fontScale': 'Font scale',
'typography.radiusScale': 'Radius scale',
'appearance.name': 'Theme name',
'appearance.colorScheme': 'Color scheme',
'appearance.style': 'Style',
'appearance.shape': 'Shape',
'appearance.density': 'Density',
'appearance.font': 'Font',
'appearance.shadow': 'Shadow',
'appearance.motion': 'Motion',
'appearance.blur': 'Surface blur',
'appearance.button': 'Button',
'appearance.input': 'Input',
'appearance.card': 'Card',
'appearance.tabs': 'Tabs',
'option.dark': 'Dark',
'option.light': 'Light',
'option.standard': 'Standard',
'option.cyberpunk': 'Cyberpunk',
'option.soft': 'Soft',
'option.dense': 'Dense',
'option.square': 'Square',
'option.rounded': 'Rounded',
'option.compact': 'Compact',
'option.comfortable': 'Comfortable',
'option.spacious': 'Spacious',
'option.mono': 'Monospace',
'option.system': 'System',
'option.none': 'None',
'option.subtle': 'Subtle',
'option.elevated': 'Elevated',
'option.solid': 'Solid',
'option.linear': 'Linear',
'option.radial': 'Radial',
'option.mesh': 'Mesh',
'option.drift': 'Drift',
'option.reduced': 'Reduced',
'option.full': 'Full',
'option.outline': 'Outline',
'option.solid': 'Solid',
'option.underline': 'Underline',
'option.boxed': 'Boxed',
'option.outlined': 'Outlined',
'option.flat': 'Flat',
'option.segmented': 'Segmented',
'group.backgrounds': 'BACKGROUNDS',
'group.text': 'TEXT COLORS',
'group.accents': 'ACCENT COLORS',
'group.borders': 'BORDERS',
'group.error': 'ERROR',
'group.components': 'COMPONENTS',
'group.agent': 'AI AGENT',
'group.effects': 'EFFECTS',
'variable.bg': 'Page Background',
'variable.bgSurface': 'Card / Surface',
'variable.bgElevated': 'Elevated Surface',
'variable.bgTerminal': 'Terminal Background',
'variable.text': 'Primary Text',
'variable.textMuted': 'Muted Text',
'variable.textDim': 'Dim Text',
'variable.accent': 'Primary Accent',
'variable.accentSecondary': 'Secondary Accent',
'variable.accentSecondaryLight': 'Secondary Light',
'variable.onAccent': 'Text on Accent',
'variable.border': 'Border',
'variable.borderStrong': 'Strong Border',
'variable.error': 'Error Text',
'variable.errorBg': 'Error Background',
'variable.surfaceDot': 'Status Dot',
'variable.scrollbarTrack': 'Scrollbar Track',
'variable.scrollbarThumb': 'Scrollbar Thumb',
'variable.scrollbarThumbHover': 'Scrollbar Hover',
'variable.agentUser': 'User Message',
'variable.agentBot': 'Agent Message',
'variable.scanline': 'Scanline Overlay',
'variable.accentGlow': 'Accent Glow',
'variable.accentBg': 'Accent Background',
'variable.modalOverlay': 'Modal Overlay',
'variable.onSurface': 'Surface Text',
'variable.onSurfaceVariant': 'Surface Text Variant',
}
};
const LOCALE_STORAGE_KEY = 'cloudssh_locale';
let locale = 'zh-CN';
function normalizeLocale(value) {
if (!value) return null;
const normalized = String(value).replace('_', '-').toLowerCase();
if (normalized === 'zh' || normalized.startsWith('zh-')) return 'zh-CN';
if (normalized === 'en' || normalized.startsWith('en-')) return 'en-US';
return null;
}
function t(key, params = {}) {
const template = TRANSLATIONS[locale][key] || TRANSLATIONS['zh-CN'][key] || key;
return template.replace(/\{(\w+)\}/g, (match, name) =>
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match
);
}
function resolveInitialLocale() {
let stored = null;
try { stored = localStorage.getItem(LOCALE_STORAGE_KEY); } catch { /* storage may be unavailable */ }
return normalizeLocale(new URLSearchParams(window.location.search).get('lang'))
|| normalizeLocale(stored)
|| (navigator.languages || [navigator.language]).map(normalizeLocale).find(Boolean)
|| 'zh-CN';
}
function translateDocument() {
document.documentElement.lang = locale;
document.title = t('page.title');
document.querySelectorAll('[data-i18n]').forEach((element) => {
element.textContent = t(element.dataset.i18n);
});
document.querySelectorAll('[data-i18n-title]').forEach((element) => {
element.title = t(element.dataset.i18nTitle);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
element.placeholder = t(element.dataset.i18nPlaceholder);
});
document.querySelectorAll('[data-i18n-template]').forEach((element) => {
element.textContent = t(element.dataset.i18nTemplate, { value: element.dataset.value || '' });
});
const alternateLocale = locale === 'zh-CN' ? 'en-US' : 'zh-CN';
const alternateLabel = TRANSLATIONS[alternateLocale][`language.${alternateLocale === 'zh-CN' ? 'zhCN' : 'enUS'}`];
const languageButton = document.getElementById('language-toggle');
document.getElementById('language-toggle-label').textContent = alternateLabel;
languageButton.title = t('language.switchTo', { language: alternateLabel });
languageButton.setAttribute('aria-label', languageButton.title);
document.querySelectorAll('[data-language-preview-label]').forEach((element) => {
element.textContent = alternateLabel;
});
}
function setLocale(nextLocale, persist = true) {
locale = nextLocale;
if (persist) {
try { localStorage.setItem(LOCALE_STORAGE_KEY, locale); } catch { /* storage may be unavailable */ }
}
translateDocument();
if (Object.keys(theme).length) renderControls();
}
function showToast(messageKey, type = 'success') {
const region = document.getElementById('toast-region');
region.innerHTML = '';
const toast = document.createElement('div');
toast.className = `editor-toast${type === 'error' ? ' error' : ''}`;
toast.innerHTML = `<span class="material-symbols-outlined">${type === 'error' ? 'error' : 'check_circle'}</span><span></span>`;
toast.querySelector('span:last-child').textContent = t(messageKey);
region.appendChild(toast);
window.setTimeout(() => toast.remove(), 3200);
}
const PRESETS = /* THEME_PRESETS_START */ {
"standard-dark": {
"name": "Standard Dark",
"colorScheme": "dark",
"appearance": {
"style": "standard",
"shape": "rounded",
"density": "comfortable",
"font": "system",
"shadow": "subtle",
"motion": "reduced",
"blur": "subtle",
"components": {
"button": "solid",
"input": "boxed",
"card": "outlined",
"tabs": "underline"
}
},
"terminal": {
"background": "#0b0e14",
"foreground": "#d6deeb",
"cursor": "#58a6ff",
"cursorAccent": "#0b0e14",
"selectionBackground": "#264f78",
"selectionInactiveBackground": "#1f3a56",
"black": "#0d1117",
"red": "#ff7b72",
"green": "#3fb950",
"yellow": "#d29922",
"blue": "#58a6ff",
"magenta": "#bc8cff",
"cyan": "#39c5cf",
"white": "#b1bac4",
"brightBlack": "#6e7681",
"brightRed": "#ffa198",
"brightGreen": "#56d364",
"brightYellow": "#e3b341",
"brightBlue": "#79c0ff",
"brightMagenta": "#d2a8ff",
"brightCyan": "#56d4dd",
"brightWhite": "#f0f6fc"
},
"ui": {
"--bg": "#0f1115",
"--bg-surface": "#161a22",
"--bg-elevated": "#1c222d",
"--bg-terminal": "#0b0e14",
"--text": "#e6edf3",
"--text-muted": "#9ba7b4",
"--text-dim": "#7d8590",
"--accent": "#58a6ff",
"--accent-secondary": "#79c0ff",
"--accent-secondary-light": "#a5d6ff",
"--border": "#30363d",
"--border-strong": "#484f58",
"--error": "#ff7b72",
"--error-bg": "#3d1418",
"--on-accent": "#07111f",
"--surface-dot": "#30363d",
"--scrollbar-track": "rgba(22, 26, 34, 0.7)",
"--scrollbar-thumb": "rgba(155, 167, 180, 0.35)",
"--scrollbar-thumb-hover": "rgba(155, 167, 180, 0.55)",
"--scanline-tint": "transparent",
"--accent-glow": "rgba(88, 166, 255, 0.08)",
"--accent-bg": "rgba(88, 166, 255, 0.1)",
"--modal-overlay": "rgba(0, 0, 0, 0.72)",
"--on-surface": "#e6edf3",
"--on-surface-variant": "#9ba7b4",
"--agent-user-color": "#58a6ff",
"--agent-agent-color": "#79c0ff"
}
},
"standard-light": {
"name": "Standard Light",
"colorScheme": "light",
"appearance": {
"style": "standard",
"shape": "rounded",
"density": "comfortable",
"font": "system",
"shadow": "subtle",
"motion": "reduced",
"blur": "subtle",
"components": {
"button": "solid",
"input": "boxed",
"card": "outlined",
"tabs": "underline"
}
},
"terminal": {
"background": "#ffffff",
"foreground": "#24292f",
"cursor": "#0969da",
"cursorAccent": "#ffffff",
"selectionBackground": "#b6d7ff",
"selectionForeground": "#1f2328",
"selectionInactiveBackground": "#dbeafe",
"black": "#24292f",
"red": "#cf222e",
"green": "#1a7f37",
"yellow": "#9a6700",
"blue": "#0969da",
"magenta": "#8250df",
"cyan": "#1b7c83",
"white": "#6e7781",
"brightBlack": "#57606a",
"brightRed": "#a40e26",
"brightGreen": "#116329",
"brightYellow": "#7d4e00",
"brightBlue": "#0550ae",
"brightMagenta": "#6639ba",
"brightCyan": "#0a6c74",
"brightWhite": "#1f2328"
},
"ui": {
"--bg": "#f6f8fa",
"--bg-surface": "#ffffff",
"--bg-elevated": "#eef2f6",
"--bg-terminal": "#ffffff",
"--text": "#1f2328",
"--text-muted": "#57606a",
"--text-dim": "#656d76",
"--accent": "#0969da",
"--accent-secondary": "#0550ae",
"--accent-secondary-light": "#0550ae",
"--border": "#d0d7de",
"--border-strong": "#afb8c1",
"--error": "#cf222e",
"--error-bg": "#ffebe9",
"--on-accent": "#ffffff",
"--surface-dot": "#eaeef2",
"--scrollbar-track": "rgba(208, 215, 222, 0.45)",
"--scrollbar-thumb": "rgba(87, 96, 106, 0.35)",
"--scrollbar-thumb-hover": "rgba(87, 96, 106, 0.55)",
"--scanline-tint": "transparent",
"--accent-glow": "rgba(9, 105, 218, 0.08)",
"--accent-bg": "rgba(9, 105, 218, 0.1)",
"--modal-overlay": "rgba(31, 35, 40, 0.48)",
"--on-surface": "#1f2328",
"--on-surface-variant": "#57606a",
"--agent-user-color": "#0969da",
"--agent-agent-color": "#8250df"
}
},
"cyberpunk": {
"name": "Cyberpunk",
"colorScheme": "dark",
"appearance": {
"style": "cyberpunk",
"shape": "square",
"density": "compact",
"font": "mono",
"shadow": "none",
"motion": "full",
"blur": "none",
"components": {
"button": "outline",
"input": "underline",
"card": "outlined",
"tabs": "underline"
}
},
"background": {
"type": "linear",
"stops": [
"#0a0a0a",
"#11170c"
],
"angle": 165,
"scrim": 0.3,
"animation": "none"
},
"effects": {
"scanline": 1,
"flicker": 1
},
"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",
"--scrollbar-track": "rgba(28, 27, 27, 0.5)",
"--scrollbar-thumb": "rgba(60, 75, 54, 0.8)",
"--scrollbar-thumb-hover": "rgba(134, 149, 125, 0.8)",
"--scanline-tint": "rgba(74, 246, 38, 0.02)",
"--accent-glow": "rgba(74, 246, 38, 0.08)",
"--accent-bg": "rgba(74, 246, 38, 0.1)",
"--modal-overlay": "rgba(0, 0, 0, 0.8)",
"--on-surface": "#e5e2e1",
"--on-surface-variant": "#bbccb0",
"--agent-user-color": "#4af626",
"--agent-agent-color": "#14d1ff"
}
},
"apple": {
"name": "Apple",
"colorScheme": "light",
"appearance": {
"style": "soft",
"shape": "soft",
"density": "comfortable",
"font": "system",
"shadow": "elevated",
"motion": "reduced",
"blur": "strong",
"components": {
"button": "soft",
"input": "boxed",
"card": "elevated",
"tabs": "segmented"
}
},
"terminal": {
"background": "#ffffff",
"foreground": "#1d1d1f",
"cursor": "#0066da",
"cursorAccent": "#ffffff",
"selectionBackground": "#b3d7ff",
"selectionForeground": "#1d1d1f",
"selectionInactiveBackground": "#dde8f7",
"black": "#1d1d1f",
"red": "#d70015",
"green": "#248a3d",
"yellow": "#b25000",
"blue": "#0040dd",
"magenta": "#8944ab",
"cyan": "#0b7285",
"white": "#8e8e93",
"brightBlack": "#6e6e73",
"brightRed": "#ff3b30",
"brightGreen": "#34c759",
"brightYellow": "#c93400",
"brightBlue": "#0071e3",
"brightMagenta": "#af52de",
"brightCyan": "#30b0c7",
"brightWhite": "#1d1d1f"
},
"ui": {
"--bg": "#f5f5f7",
"--bg-surface": "#ffffff",
"--bg-elevated": "#ffffff",
"--bg-terminal": "#ffffff",
"--text": "#1d1d1f",
"--text-muted": "#636368",
"--text-dim": "#6e6e73",
"--accent": "#0066da",
"--accent-secondary": "#0052b4",
"--accent-secondary-light": "#cce4ff",
"--border": "#d2d2d7",
"--border-strong": "#aeaeb2",
"--error": "#d70015",
"--error-bg": "#ffebe9",
"--on-accent": "#ffffff",
"--surface-dot": "#e8e8ed",
"--scrollbar-track": "rgba(210, 210, 215, 0.45)",
"--scrollbar-thumb": "rgba(99, 99, 104, 0.35)",
"--scrollbar-thumb-hover": "rgba(99, 99, 104, 0.55)",
"--scanline-tint": "transparent",
"--accent-glow": "rgba(0, 102, 218, 0.08)",
"--accent-bg": "rgba(0, 102, 218, 0.1)",
"--modal-overlay": "rgba(0, 0, 0, 0.32)",
"--on-surface": "#1d1d1f",
"--on-surface-variant": "#636368",
"--agent-user-color": "#0066da",
"--agent-agent-color": "#5856d6"
}
},
"gruvbox": {
"name": "Gruvbox",
"colorScheme": "dark",
"appearance": {
"style": "dense",
"shape": "rounded",
"density": "compact",
"font": "mono",
"shadow": "none",
"motion": "reduced",
"blur": "none",
"components": {
"button": "outline",
"input": "boxed",
"card": "flat",
"tabs": "segmented"
}
},
"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",
"--scrollbar-track": "rgba(40, 40, 40, 0.5)",
"--scrollbar-thumb": "rgba(168, 153, 132, 0.3)",
"--scrollbar-thumb-hover": "rgba(168, 153, 132, 0.5)",
"--scanline-tint": "rgba(184, 187, 38, 0.02)",
"--accent-glow": "rgba(184, 187, 38, 0.08)",
"--accent-bg": "rgba(184, 187, 38, 0.1)",
"--modal-overlay": "rgba(0, 0, 0, 0.75)",
"--on-surface": "#ebdbb2",
"--on-surface-variant": "#a89984",
"--agent-user-color": "#b8bb26",
"--agent-agent-color": "#83a598"
}
},
"crt": {
"name": "CRT Amber",
"colorScheme": "dark",
"appearance": {
"style": "cyberpunk",
"shape": "square",
"density": "compact",
"font": "mono",
"shadow": "none",
"motion": "full",
"blur": "none",
"components": {
"button": "outline",
"input": "underline",
"card": "outlined",
"tabs": "underline"
}
},
"background": {
"type": "radial",
"stops": [
"#261b00",
"#0f0a00"
],
"angle": 160,
"scrim": 0.3,
"animation": "none"
},
"effects": {
"scanline": 1,
"flicker": 0.6,
"glow": 0.5
},
"typography": {
"fontScale": 1,
"radiusScale": 0.75
},
"terminal": {
"background": "#0f0a00",
"foreground": "#ffb000",
"cursor": "#ffd257",
"cursorAccent": "#0f0a00",
"selectionBackground": "#3d2e00",
"selectionForeground": "#ffb000",
"selectionInactiveBackground": "#2a2000",
"black": "#241a00",
"red": "#ff6b4a",
"green": "#d4a017",
"yellow": "#ffb000",
"blue": "#f0c040",
"magenta": "#e08e39",
"cyan": "#c8b04a",
"white": "#d9c08a",
"brightBlack": "#8a7440",
"brightRed": "#ffa080",
"brightGreen": "#ffd257",
"brightYellow": "#ffe08a",
"brightBlue": "#ffe8b0",
"brightMagenta": "#ffc27a",
"brightCyan": "#e8d89a",
"brightWhite": "#fff3c4"
},
"ui": {
"--bg": "#0f0a00",
"--bg-surface": "#171000",
"--bg-elevated": "#1d1600",
"--bg-terminal": "#0f0a00",
"--text": "#ffb000",
"--text-muted": "#bfa06a",
"--text-dim": "#937c46",
"--accent": "#ffb000",
"--accent-secondary": "#ffd257",
"--accent-secondary-light": "#ffe08a",
"--border": "#3d2e00",
"--border-strong": "#5c470a",
"--error": "#ff6b4a",
"--error-bg": "#3d1200",
"--on-accent": "#0f0a00",
"--surface-dot": "#3d2e00",
"--scrollbar-track": "rgba(23, 16, 0, 0.5)",
"--scrollbar-thumb": "rgba(255, 176, 0, 0.25)",
"--scrollbar-thumb-hover": "rgba(255, 176, 0, 0.45)",
"--scanline-tint": "rgba(255, 176, 0, 0.03)",
"--accent-glow": "rgba(255, 176, 0, 0.12)",
"--accent-bg": "rgba(255, 176, 0, 0.12)",
"--modal-overlay": "rgba(0, 0, 0, 0.8)",
"--on-surface": "#fff3c4",
"--on-surface-variant": "#bfa06a",
"--agent-user-color": "#ffb000",
"--agent-agent-color": "#ffd257"
}
},
"glass": {
"name": "Glass",
"colorScheme": "light",
"appearance": {
"style": "soft",
"shape": "soft",
"density": "comfortable",
"font": "system",
"shadow": "elevated",
"motion": "reduced",
"blur": "strong",
"components": {
"button": "soft",
"input": "boxed",
"card": "elevated",
"tabs": "segmented"
}
},
"background": {
"type": "mesh",
"stops": [
"#d3e3f8",
"#e8edf5",
"#ece0f6"
],
"angle": 135,
"scrim": 0.35,
"animation": "drift"
},
"effects": {
"glow": 0.4
},
"typography": {
"fontScale": 1.05,
"radiusScale": 1.4
},
"terminal": {
"background": "#ffffff",
"foreground": "#1a2332",
"cursor": "#235cd8",
"cursorAccent": "#ffffff",
"selectionBackground": "#c4d8f7",
"selectionForeground": "#1a2332",
"selectionInactiveBackground": "#dde8f7",
"black": "#1a2332",
"red": "#c03528",
"green": "#1e7d3c",
"yellow": "#8a5a00",
"blue": "#235cd8",
"magenta": "#7c4dcc",
"cyan": "#0e6e86",
"white": "#56637a",
"brightBlack": "#56637a",
"brightRed": "#d9483b",
"brightGreen": "#2a944f",
"brightYellow": "#a86e10",
"brightBlue": "#3870e8",
"brightMagenta": "#8f5ce6",
"brightCyan": "#2a8ba3",
"brightWhite": "#0e1626"
},
"ui": {
"--bg": "#e8edf5",
"--bg-surface": "#f4f7fb",
"--bg-elevated": "#ffffff",
"--bg-terminal": "#ffffff",
"--text": "#1a2332",
"--text-muted": "#4d5a70",
"--text-dim": "#56637a",
"--accent": "#235cd8",
"--accent-secondary": "#3870e8",
"--accent-secondary-light": "#c4d8f7",
"--border": "#c9d4e3",
"--border-strong": "#a9b8cc",
"--error": "#c03528",
"--error-bg": "#f9e3e0",
"--on-accent": "#ffffff",
"--surface-dot": "#d5deea",
"--scrollbar-track": "rgba(201, 212, 227, 0.45)",
"--scrollbar-thumb": "rgba(77, 90, 112, 0.3)",
"--scrollbar-thumb-hover": "rgba(77, 90, 112, 0.5)",
"--scanline-tint": "transparent",
"--accent-glow": "rgba(35, 92, 216, 0.1)",
"--accent-bg": "rgba(35, 92, 216, 0.08)",
"--modal-overlay": "rgba(26, 35, 50, 0.35)",
"--on-surface": "#1a2332",
"--on-surface-variant": "#4d5a70",
"--agent-user-color": "#235cd8",
"--agent-agent-color": "#7c4dcc"
}
}
} /* THEME_PRESETS_END */;
const VARIABLE_GROUPS = [
{ titleKey: 'group.backgrounds', vars: ['--bg', '--bg-surface', '--bg-elevated', '--bg-terminal'] },
{ titleKey: 'group.text', vars: ['--text', '--text-muted', '--text-dim', '--on-surface', '--on-surface-variant'] },
{ titleKey: 'group.accents', vars: ['--accent', '--accent-secondary', '--accent-secondary-light', '--on-accent'] },
{ titleKey: 'group.borders', vars: ['--border', '--border-strong'] },
{ titleKey: 'group.error', vars: ['--error', '--error-bg'] },
{ titleKey: 'group.components', vars: ['--surface-dot', '--scrollbar-track', '--scrollbar-thumb', '--scrollbar-thumb-hover'] },
{ titleKey: 'group.agent', vars: ['--agent-user-color', '--agent-agent-color'] },
{ titleKey: 'group.effects', vars: ['--scanline-tint', '--accent-glow', '--accent-bg', '--modal-overlay'] },
];
const VARIABLE_LABELS = {
'--bg': 'variable.bg',
'--bg-surface': 'variable.bgSurface',
'--bg-elevated': 'variable.bgElevated',
'--bg-terminal': 'variable.bgTerminal',
'--text': 'variable.text',
'--text-muted': 'variable.textMuted',
'--text-dim': 'variable.textDim',
'--accent': 'variable.accent',
'--accent-secondary': 'variable.accentSecondary',
'--accent-secondary-light': 'variable.accentSecondaryLight',
'--on-accent': 'variable.onAccent',
'--border': 'variable.border',
'--border-strong': 'variable.borderStrong',
'--error': 'variable.error',
'--error-bg': 'variable.errorBg',
'--surface-dot': 'variable.surfaceDot',
'--scrollbar-track': 'variable.scrollbarTrack',
'--scrollbar-thumb': 'variable.scrollbarThumb',
'--scrollbar-thumb-hover': 'variable.scrollbarThumbHover',
'--agent-user-color': 'variable.agentUser',
'--agent-agent-color': 'variable.agentBot',
'--scanline-tint': 'variable.scanline',
'--accent-glow': 'variable.accentGlow',
'--accent-bg': 'variable.accentBg',
'--modal-overlay': 'variable.modalOverlay',
'--on-surface': 'variable.onSurface',
'--on-surface-variant': 'variable.onSurfaceVariant',
};
let theme = {};
let terminalTheme = {};
let activePreset = 'cyberpunk';
let colorScheme = 'dark';
let themeName = 'Cyberpunk';
let appearance = {};
let background = null;
let effects = {};
let typography = null;
const THEME_MAX_BYTES = 64 * 1024;
const invalidColorProperties = new Set();
const APPEARANCE_FIELDS = [
{ key: 'style', labelKey: 'appearance.style', values: ['standard', 'cyberpunk', 'soft', 'dense'] },
{ key: 'shape', labelKey: 'appearance.shape', values: ['square', 'rounded', 'soft'] },
{ key: 'density', labelKey: 'appearance.density', values: ['compact', 'comfortable', 'spacious'] },
{ key: 'font', labelKey: 'appearance.font', values: ['mono', 'system'] },
{ key: 'shadow', labelKey: 'appearance.shadow', values: ['none', 'subtle', 'elevated'] },
{ key: 'motion', labelKey: 'appearance.motion', values: ['none', 'reduced', 'full'] },
{ key: 'blur', labelKey: 'appearance.blur', values: ['none', 'subtle', 'strong'] },
];
const COMPONENT_FIELDS = [
{ key: 'button', labelKey: 'appearance.button', values: ['outline', 'solid', 'soft'] },
{ key: 'input', labelKey: 'appearance.input', values: ['underline', 'boxed'] },
{ key: 'card', labelKey: 'appearance.card', values: ['outlined', 'flat', 'elevated'] },
{ key: 'tabs', labelKey: 'appearance.tabs', values: ['underline', 'segmented'] },
];
function syncThemeSelectors(value) {
document.getElementById('preset-select').value = value;
document.querySelectorAll('.terminal-theme-select').forEach(select => {
select.value = value;
});
}
function initTheme(presetName) {
const preset = PRESETS[presetName];
if (!preset) return;
invalidColorProperties.clear();
theme = { ...preset.ui };
terminalTheme = { ...preset.terminal };
activePreset = presetName;
colorScheme = preset.colorScheme;
themeName = preset.name;
appearance = {
...preset.appearance,
components: { ...preset.appearance.components },
};
background = preset.background ? structuredClone(preset.background) : null;
effects = { ...(preset.effects || {}) };
typography = preset.typography ? { ...preset.typography } : null;
renderControls();
applyTheme();
syncThemeSelectors(presetName);
updateExportAvailability();
}
function markThemeCustomized() {
syncThemeSelectors('custom');
}
function isHexColor(val) {
return /^#[0-9a-fA-F]{6}$/.test(val) || /^#[0-9a-fA-F]{3}$/.test(val);
}
function escapeAttribute(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function renderControls() {
const container = document.getElementById('controls');
let html = `<div class="section-title">${t('group.basic')}</div>`;
html += settingInput('appearance.name', themeName, 'data-theme-name');
html += settingSelect('appearance.colorScheme', colorScheme, ['dark', 'light'], 'data-color-scheme');
html += `<div class="section-title">${t('group.appearance')}</div>`;
html += APPEARANCE_FIELDS
.map(field => settingSelect(field.labelKey, appearance[field.key], field.values, `data-appearance="${field.key}"`))
.join('');
html += `<div class="section-title">${t('group.componentStyles')}</div>`;
html += COMPONENT_FIELDS
.map(field => settingSelect(
field.labelKey,
appearance.components?.[field.key],
field.values,
`data-component="${field.key}"`,
))
.join('');
// V3:背景层 / 效果 / 版式面板
html += `<div class="section-title">${t('group.themeV3Background')}</div>`;
html += settingSelect('background.type', background?.type || 'solid', ['solid', 'linear', 'radial', 'mesh'], 'data-background="type"');
html += settingRange('background.angle', background?.angle ?? 160, 0, 360, 1, 'data-background="angle"');
html += settingRange('background.scrim', background?.scrim ?? 0.3, 0, 0.85, 0.01, 'data-background="scrim"');
html += settingSelect('background.animation', background?.animation || 'none', ['none', 'drift'], 'data-background="animation"');
for (let i = 0; i < 4; i++) {
html += settingInput(`background.stop${i + 1}`, background?.stops?.[i] || '', `data-background-stop="${i}"`);
}
html += `<div class="section-title">${t('group.themeV3Effects')}</div>`;
html += settingRange('effects.scanline', effects.scanline ?? 0, 0, 1, 0.05, 'data-effect="scanline"');
html += settingRange('effects.flicker', effects.flicker ?? 0, 0, 1, 0.05, 'data-effect="flicker"');
html += settingRange('effects.glow', effects.glow ?? 0, 0, 1, 0.05, 'data-effect="glow"');
html += settingRange('effects.noise', effects.noise ?? 0, 0, 1, 0.01, 'data-effect="noise"');
html += `<div class="section-title">${t('group.themeV3Typography')}</div>`;
html += settingRange('typography.fontScale', typography?.fontScale ?? 1, 0.85, 1.25, 0.01, 'data-typography="fontScale"');
html += settingRange('typography.radiusScale', typography?.radiusScale ?? 1, 0.5, 2, 0.05, 'data-typography="radiusScale"');
for (const group of VARIABLE_GROUPS) {
html += `<div class="section-title">${t(group.titleKey)}</div>`;
for (const varName of group.vars) {
const val = theme[varName] || '';
const label = t(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}" aria-label="${escapeAttribute(label)}">`;
}
html += `<input type="text" value="${escapeAttribute(val)}" data-var="${varName}" aria-label="${escapeAttribute(label)}" spellcheck="false">`;
html += `</div>`;
}
}
container.innerHTML = html;
container.querySelector('[data-theme-name]').addEventListener('input', (event) => {
themeName = event.target.value.slice(0, 80);
markThemeCustomized();
});
container.querySelector('[data-color-scheme]').addEventListener('change', (event) => {
colorScheme = event.target.value;
markThemeCustomized();
applyTheme();
});
container.querySelectorAll('[data-appearance]').forEach(select => {
select.addEventListener('change', (event) => {
appearance[event.target.dataset.appearance] = event.target.value;
markThemeCustomized();
applyTheme();
});
});
container.querySelectorAll('[data-component]').forEach(select => {
select.addEventListener('change', (event) => {
appearance.components[event.target.dataset.component] = event.target.value;
markThemeCustomized();
applyTheme();
});
});
// V3:背景层 / 效果 / 版式控件事件
container.querySelectorAll('[data-background]').forEach(control => {
control.addEventListener('input', (event) => {
const target = event.target;
const key = target.dataset.background;
background = normalizeBackgroundState(background);
if (key === 'angle' || key === 'scrim') {
background[key] = Number(target.value);
} else {
background[key] = target.value;
}
markThemeCustomized();
applyTheme();
});
});
container.querySelectorAll('[data-background-stop]').forEach(input => {
input.addEventListener('input', (event) => {
const target = event.target;
const index = Number(target.dataset.backgroundStop);
background = normalizeBackgroundState(background);
const stops = [...(background.stops || [])];
const value = target.value.trim();
if (value === '') {
delete stops[index];
} else if (isSafeColor(value)) {
stops[index] = value;
target.setAttribute('aria-invalid', 'false');
} else {
target.setAttribute('aria-invalid', 'true');
}
background.stops = stops.filter(stop => stop !== undefined);
markThemeCustomized();
applyTheme();
});
});
container.querySelectorAll('[data-effect]').forEach(input => {
input.addEventListener('input', (event) => {
const key = event.target.dataset.effect;
const value = Number(event.target.value);
if (value > 0) effects[key] = value;
else delete effects[key];
markThemeCustomized();
applyTheme();
});
});
container.querySelectorAll('[data-typography]').forEach(input => {
input.addEventListener('input', (event) => {
const key = event.target.dataset.typography;
const value = Number(event.target.value);
typography = {
fontScale: typography?.fontScale ?? 1,
radiusScale: typography?.radiusScale ?? 1,
[key]: value,
};
if (typography.fontScale === 1 && typography.radiusScale === 1) typography = null;
markThemeCustomized();
applyTheme();
});
});
container.querySelectorAll('input[type="color"]').forEach(input => {
input.addEventListener('input', (e) => {
const varName = e.target.dataset.var;
theme[varName] = e.target.value;
invalidColorProperties.delete(varName);
const textInput = container.querySelector(`input[type="text"][data-var="${varName}"]`);
if (textInput) {
textInput.value = e.target.value;
textInput.setAttribute('aria-invalid', 'false');
}
markThemeCustomized();
updateExportAvailability();
applyTheme();
});
});
container.querySelectorAll('input[type="text"][data-var]').forEach(input => {
input.addEventListener('input', (e) => {
const varName = e.target.dataset.var;
const val = e.target.value;
const isValid = isSafeColor(val);
e.target.setAttribute('aria-invalid', String(!isValid));
if (!isValid) {
invalidColorProperties.add(varName);
markThemeCustomized();
updateExportAvailability();
return;
}
invalidColorProperties.delete(varName);
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;
}
markThemeCustomized();
updateExportAvailability();
applyTheme();
});
});
updateExportAvailability();
}
function updateExportAvailability() {
const exportButton = document.getElementById('btn-export');
exportButton.disabled = invalidColorProperties.size > 0;
exportButton.title = invalidColorProperties.size > 0
? t('feedback.invalidColor')
: t('editor.exportTitle');
}
function settingInput(labelKey, value, attribute) {
return `<div class="setting-row"><label>${t(labelKey)}</label>`
+ `<input type="text" value="${escapeAttribute(value)}" ${attribute} aria-label="${escapeAttribute(t(labelKey))}" maxlength="80"></div>`;
}
function settingSelect(labelKey, value, values, attribute) {
const options = values.map(option =>
`<option value="${option}"${option === value ? ' selected' : ''}>${t(`option.${option}`)}</option>`
).join('');
return `<div class="setting-row"><label>${t(labelKey)}</label>`
+ `<select ${attribute} aria-label="${escapeAttribute(t(labelKey))}">${options}</select></div>`;
}
function settingRange(labelKey, value, min, max, step, attribute) {
return `<div class="setting-row"><label>${t(labelKey)}</label>`
+ `<input type="range" min="${min}" max="${max}" step="${step}" value="${value}" ${attribute} aria-label="${escapeAttribute(t(labelKey))}">`
+ `<span class="range-value">${value}</span></div>`;
}
function normalizeBackgroundState(current) {
if (current) return current;
return {
type: 'linear',
stops: ['#0a0a0a'],
angle: 160,
scrim: 0.3,
animation: 'none',
};
}
function applyTheme() {
const scope = document.getElementById('theme-scope');
scope.style.colorScheme = colorScheme;
scope.dataset.colorScheme = colorScheme;
scope.dataset.uiStyle = appearance.style;
scope.dataset.uiShape = appearance.shape;
scope.dataset.uiDensity = appearance.density;
scope.dataset.uiFont = appearance.font;
scope.dataset.uiShadow = appearance.shadow;
scope.dataset.uiMotion = appearance.motion;
scope.dataset.uiBlur = appearance.blur;
scope.dataset.componentButton = appearance.components.button;
scope.dataset.componentInput = appearance.components.input;
scope.dataset.componentCard = appearance.components.card;
scope.dataset.componentTabs = appearance.components.tabs;
for (const [varName, val] of Object.entries(theme)) {
scope.style.setProperty(varName, val);
}
// V3:背景栈(含读性遮罩)与效果开关(与应用同构)
if (background && background.type !== 'solid' && (background.stops || []).length >= 2 && background.stops.every(isSafeColor)) {
const channel = colorScheme === 'dark' ? '0, 0, 0' : '255, 255, 255';
const scrimAlpha = background.scrim ?? 0.3;
const scrim = `linear-gradient(rgba(${channel}, ${scrimAlpha}), rgba(${channel}, ${scrimAlpha}))`;
let gradient;
if (background.type === 'linear') {
gradient = `linear-gradient(${background.angle ?? 160}deg, ${background.stops.join(', ')})`;
} else if (background.type === 'radial') {
gradient = `radial-gradient(ellipse at 50% 25%, ${background.stops.join(', ')})`;
} else {
const [a, b, c] = background.stops;
gradient = [
`radial-gradient(at 18% 22%, ${a} 0px, transparent 55%)`,
`radial-gradient(at 82% 28%, ${b || a} 0px, transparent 50%)`,
`radial-gradient(at 50% 88%, ${c || b || a} 0px, transparent 60%)`,
].join(', ');
}
scope.style.setProperty('--app-bg-stack', `${scrim}, ${gradient}`);
scope.dataset.bgAnimation = background.animation === 'drift' ? 'drift' : 'none';
} else {
scope.style.setProperty('--app-bg-stack', 'none');
scope.dataset.bgAnimation = 'none';
}
scope.dataset.fxScanline = effects.scanline ? 'on' : 'off';
scope.dataset.fxFlicker = effects.flicker ? 'on' : 'off';
scope.dataset.fxGlow = effects.glow ? 'on' : 'off';
scope.dataset.fxNoise = effects.noise ? 'on' : 'off';
scope.style.setProperty('--fx-scanline-opacity', String(effects.scanline ?? 1));
scope.style.setProperty('--fx-flicker-speed', `${0.32 - 0.2 * (effects.flicker ?? 1)}s`);
scope.style.setProperty('--fx-glow-strength', String(effects.glow ?? 0.6));
scope.style.setProperty('--fx-noise-opacity', String(effects.noise ?? 0.05));
scope.style.setProperty('--radius-scale', String(typography?.radiusScale ?? 1));
scope.style.setProperty('--type-scale', String(typography?.fontScale ?? 1));
}
// Preset selector
document.getElementById('preset-select').addEventListener('change', (event) => {
initTheme(event.target.value);
});
document.querySelectorAll('.terminal-theme-select').forEach(select => {
select.addEventListener('change', (event) => initTheme(event.target.value));
});
// 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;
if (file.size > THEME_MAX_BYTES) {
showToast('feedback.fileTooLarge', 'error');
e.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target.result);
const importedVariables = sanitizeColorRecord(data.ui, Object.keys(PRESETS.cyberpunk.ui));
const importedTerminal = sanitizeColorRecord(data.terminal, Object.keys(PRESETS['standard-dark'].terminal));
const importedAppearance = sanitizeAppearance(data.appearance);
if (!Object.keys(importedVariables).length
&& !Object.keys(importedTerminal).length
&& !importedAppearance) {
showToast('feedback.missingUi', 'error');
return;
}
const importedScheme = data.colorScheme === 'light' || data.colorScheme === 'dark'
? data.colorScheme
: inferColorScheme(importedVariables['--bg'] || importedTerminal.background);
const requestedBase = typeof data.baseTheme === 'string' && PRESETS[data.baseTheme]
? data.baseTheme
: (importedScheme === 'light' ? 'standard-light' : 'cyberpunk');
const fallbackPreset = PRESETS[requestedBase];
theme = { ...fallbackPreset.ui, ...importedVariables };
terminalTheme = { ...fallbackPreset.terminal, ...importedTerminal };
colorScheme = importedScheme;
themeName = typeof data.name === 'string' && data.name.trim()
? data.name.trim().slice(0, 80)
: 'Custom';
appearance = mergeAppearance(fallbackPreset.appearance, importedAppearance);
background = sanitizeBackground(data.background, importedScheme)
|| structuredClone(fallbackPreset.background || null);
effects = { ...(fallbackPreset.effects || {}), ...(sanitizeEffects(data.effects) || {}) };
typography = sanitizeTypography(data.typography)
|| (fallbackPreset.typography ? { ...fallbackPreset.typography } : null);
activePreset = requestedBase;
invalidColorProperties.clear();
syncThemeSelectors('custom');
renderControls();
applyTheme();
updateExportAvailability();
showToast('feedback.imported');
} catch {
showToast('feedback.invalidJson', 'error');
}
};
reader.readAsText(file);
e.target.value = '';
});
// Export
document.getElementById('btn-export').addEventListener('click', () => {
if (invalidColorProperties.size > 0) {
showToast('feedback.invalidColor', 'error');
return;
}
const safeUiTheme = sanitizeColorRecord(theme, Object.keys(PRESETS.cyberpunk.ui));
const safeTerminalTheme = sanitizeColorRecord(
terminalTheme,
Object.keys(PRESETS['standard-dark'].terminal),
);
const output = {
schemaVersion: 3,
name: themeName.trim() || 'Custom',
baseTheme: activePreset || (colorScheme === 'light' ? 'standard-light' : 'cyberpunk'),
colorScheme,
appearance: {
...appearance,
components: { ...appearance.components },
},
terminal: {
...safeTerminalTheme,
background: safeUiTheme['--bg-terminal'] || safeTerminalTheme.background,
foreground: safeUiTheme['--text'] || safeTerminalTheme.foreground,
cursor: safeUiTheme['--accent-secondary'] || safeTerminalTheme.cursor,
cursorAccent: safeUiTheme['--bg-terminal'] || safeTerminalTheme.cursorAccent,
selectionBackground: safeUiTheme['--border-strong'] || safeTerminalTheme.selectionBackground,
},
ui: safeUiTheme,
};
const safeBackground = sanitizeBackground(background, colorScheme);
if (safeBackground) output.background = safeBackground;
const safeEffects = sanitizeEffects(effects);
if (safeEffects) output.effects = safeEffects;
const safeTypography = sanitizeTypography(typography);
if (safeTypography) output.typography = safeTypography;
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;
const safeName = output.name.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff_-]+/gi, '-')
.replace(/^-+|-+$/g, '') || 'custom';
a.download = `cloudssh-theme-${safeName}.json`;
a.click();
a.remove();
URL.revokeObjectURL(url);
showToast('feedback.exported');
});
function sanitizeColorRecord(value, allowedProperties) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
const allowed = new Set(allowedProperties);
return Object.fromEntries(
Object.entries(value).filter(([key, color]) =>
allowed.has(key) && typeof color === 'string' && isSafeColor(color)
)
);
}
function isSafeColor(value) {
const normalized = value.trim();
if (!normalized || normalized.length > 96 || /url\s*\(|var\s*\(|expression\s*\(/i.test(normalized)) {
return false;
}
return normalized === 'transparent'
|| /^#[0-9a-f]{3,8}$/i.test(normalized)
|| /^rgba?\(\s*[\d.\s,%+-]+\)$/i.test(normalized)
|| /^hsla?\(\s*[\d.\s,%+-]+(?:deg|rad|turn)?[\d.\s,%+-]*\)$/i.test(normalized);
}
function sanitizeAppearance(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const result = { components: {} };
for (const field of APPEARANCE_FIELDS) {
if (field.values.includes(value[field.key])) result[field.key] = value[field.key];
}
const components = value.components && typeof value.components === 'object'
? value.components
: {};
for (const field of COMPONENT_FIELDS) {
if (field.values.includes(components[field.key])) result.components[field.key] = components[field.key];
}
return Object.keys(result).length > 1 || Object.keys(result.components).length ? result : null;
}
function sanitizeBackground(value, scheme) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const stops = Array.isArray(value.stops)
? value.stops.filter(s => typeof s === 'string' && isSafeColor(s)).slice(0, 5)
: [];
if (stops.length === 0) return null;
const type = ['solid', 'linear', 'radial', 'mesh'].includes(value.type) ? value.type : 'linear';
const animation = value.animation === 'drift' ? 'drift' : 'none';
const angle = Math.min(360, Math.max(0, Number(value.angle) || 160));
let scrim = Math.min(0.85, Math.max(0, Number(value.scrim) || 0));
if (type !== 'solid') scrim = Math.max(scrim, scheme === 'light' ? 0.35 : 0.25);
return { type, stops, angle, scrim, animation };
}
function sanitizeEffects(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
const result = {};
for (const name of ['scanline', 'flicker', 'glow', 'noise']) {
const raw = Number(value[name]);
if (Number.isFinite(raw) && raw > 0) result[name] = Math.min(1, raw);
}
return Object.keys(result).length ? result : undefined;
}
function sanitizeTypography(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const fontScale = Math.min(1.25, Math.max(0.85, Number(value.fontScale) || 1));
const radiusScale = Math.min(2, Math.max(0.5, Number(value.radiusScale) || 1));
if (fontScale === 1 && radiusScale === 1) return null;
return { fontScale, radiusScale };
}
function mergeAppearance(fallback, imported) {
return {
...fallback,
...(imported || {}),
components: {
...fallback.components,
...(imported?.components || {}),
},
};
}
function inferColorScheme(background) {
if (!background || !/^#[0-9a-fA-F]{6}$/.test(background)) return 'dark';
const channels = background.slice(1).match(/.{2}/g).map(value => parseInt(value, 16) / 255);
const luminance = channels
.map(value => value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4))
.reduce((sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index], 0);
return luminance > 0.5 ? 'light' : 'dark';
}
document.getElementById('language-toggle').addEventListener('click', () => {
setLocale(locale === 'zh-CN' ? 'en-US' : 'zh-CN');
});
document.querySelectorAll('[data-current-year]').forEach((element) => {
element.textContent = new Date().getFullYear();
});
// Init
locale = resolveInitialLocale();
translateDocument();
initTheme('cyberpunk');
</script>
</body>
</html>