You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
dsh-launcher/ui-design/dsh-launcher-ui.html

707 lines
25 KiB

<!doctype html>
<!--
dsh-launcher UI redesign — high-fidelity prototype.
Drop-in replacement for /frontend/dist/ in the Wails app (1040×1280, dark mode).
Bound methods: CheckEnv / Start / Stop / ShowWindow / HideWindow / GetStatus.
All buttons are wired with onclick stubs that match the real Wails bindings.
-->
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>dsh-launcher</title>
<style>
/* ─── tokens — dark modern-minimal, bound verbatim from direction lib ─── */
:root {
--bg: oklch(17% 0.012 260);
--surface: oklch(21% 0.013 260);
--fg: oklch(96% 0.005 260);
--muted: oklch(66% 0.012 260);
--border: oklch(28% 0.012 260);
--accent: oklch(72% 0.14 220); /* cool cyan — dev-tool feel */
--accent-soft: color-mix(in oklch, var(--accent) 16%, transparent);
--accent-fg-on: oklch(15% 0.02 220);
--fg-soft: color-mix(in oklch, var(--fg) 5%, transparent);
/* status hues — derived, not new raw colors */
--ok: oklch(74% 0.14 155); /* green */
--warn: oklch(78% 0.14 75); /* amber */
--err: oklch(68% 0.18 25); /* red */
--ok-soft: color-mix(in oklch, var(--ok) 14%, transparent);
--warn-soft: color-mix(in oklch, var(--warn) 14%, transparent);
--err-soft: color-mix(in oklch, var(--err) 14%, transparent);
/* type — system-first (no Google fonts), mono for numerics */
--font-display: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
--font-body: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
--font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace;
/* scale — desktop window, not marketing page */
--fs-h1: 22px;
--fs-h2: 14px;
--fs-h3: 13px;
--fs-body: 14px;
--fs-meta: 12px;
--fs-mono: 12px;
/* spacing — tight on purpose, this is a tool not a brochure */
--gap-xs: 6px;
--gap-sm: 10px;
--gap-md: 16px;
--gap-lg: 24px;
--gap-xl: 36px;
--radius: 8px;
--radius-lg: 12px;
--win-w: 1040px;
--win-h: 1280px;
}
/* ─── reset & base ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: var(--fs-body);
line-height: 1.5;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
button { font: inherit; cursor: pointer; border: 0; background: none; color: inherit; }
button:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
p { margin: 0; }
/* ─── shell — Wails window frame ──────────────────────────────── */
.shell {
max-width: var(--win-w);
margin: 0 auto;
display: flex;
flex-direction: column;
/* draggable region for Wails macOS titlebar */
-webkit-app-region: drag;
}
.shell > * { -webkit-app-region: no-drag; }
/* ─── titlebar (macOS) ────────────────────────────────────────── */
.titlebar {
height: 38px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 14px;
border-bottom: 1px solid var(--border);
background: color-mix(in oklch, var(--bg) 90%, transparent);
-webkit-app-region: drag;
position: relative;
}
.titlebar-title {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.04em;
color: var(--muted);
}
.titlebar-actions {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 4px;
-webkit-app-region: no-drag;
}
.titlebar-btn {
width: 28px;
height: 22px;
display: grid;
place-items: center;
border-radius: 6px;
color: var(--muted);
transition: background 0.15s ease, color 0.15s ease;
}
.titlebar-btn:hover { background: var(--fg-soft); color: var(--fg); }
.titlebar-btn svg { width: 12px; height: 12px; }
/* ─── main content layout ─────────────────────────────────────── */
.main {
padding: 28px 36px 40px;
display: flex;
flex-direction: column;
gap: 28px;
}
/* ─── hero — "what this app does" + big start button ─────────── */
.hero {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
column-gap: 28px;
padding: 22px 28px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
}
.hero-fish {
width: 96px;
height: 96px;
border-radius: 16px;
object-fit: cover;
flex-shrink: 0;
/* 源图 256×256 → 96px 显示,浏览器缩小渲染比原生 96 清晰 */
image-rendering: -webkit-optimize-contrast;
box-shadow: 0 0 0 1px var(--border), 0 4px 16px -8px var(--accent-soft);
}
/* subtle accent glow on the active card */
.hero::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(60% 120% at 100% 0%, var(--accent-soft), transparent 60%);
pointer-events: none;
}
.hero > * { position: relative; }
.hero-text { display: flex; flex-direction: column; gap: 8px; min-width: 0; justify-self: start; margin-left: 32px; }
.hero-eyebrow {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent);
}
.hero h1 {
font-family: var(--font-display);
font-size: var(--fs-h1);
line-height: 1.25;
color: var(--fg);
}
.hero-sub {
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
/* ─── status pill (idle / live) ───────────────────────────────── */
.status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 10px 5px 8px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 999px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--muted);
width: fit-content;
}
.status .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--muted);
}
.status[data-state="idle"] .dot { background: var(--muted); }
.status[data-state="running"] .dot {
background: var(--ok);
box-shadow: 0 0 0 3px var(--ok-soft);
animation: pulse 2.4s ease-in-out infinite;
}
.status[data-state="error"] .dot { background: var(--err); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
/* ─── primary action button ──────────────────────────────────── */
.start-btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 22px;
border-radius: var(--radius);
background: var(--accent);
color: var(--accent-fg-on);
font-size: 15px;
font-weight: 600;
letter-spacing: -0.005em;
transition: transform 0.05s ease, background 0.15s ease, box-shadow 0.15s ease;
box-shadow: 0 0 0 1px var(--accent), 0 6px 24px -8px var(--accent-soft);
white-space: nowrap;
}
.start-btn:hover { background: color-mix(in oklch, var(--accent) 90%, white); }
.start-btn:active { transform: translateY(1px); }
.start-btn[disabled] {
background: var(--surface);
color: var(--muted);
box-shadow: 0 0 0 1px var(--border);
cursor: not-allowed;
}
.start-btn .kbd {
font-family: var(--font-mono);
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background: color-mix(in oklch, var(--accent-fg-on) 18%, transparent);
}
.start-btn .icon { width: 16px; height: 16px; }
.stop-btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 22px;
border-radius: var(--radius);
background: var(--surface);
color: var(--fg);
font-size: 15px;
font-weight: 600;
border: 1px solid var(--border);
transition: border-color 0.15s ease, color 0.15s ease;
}
.stop-btn:hover { border-color: var(--err); color: var(--err); }
.stop-btn .icon { width: 14px; height: 14px; }
.row-actions { display: flex; gap: 10px; flex-wrap: wrap; }
/* ─── env check panel — 2-col grid of small status rows ──────── */
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px 24px;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.panel-title {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.panel-meta {
font-family: var(--font-mono);
font-size: var(--fs-mono);
color: var(--muted);
}
.panel-action {
font-size: 12px;
color: var(--muted);
padding: 4px 8px;
border-radius: 6px;
transition: background 0.15s ease, color 0.15s ease;
}
.panel-action:hover { background: var(--fg-soft); color: var(--fg); }
.env-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.env-cell {
background: var(--surface);
padding: 14px 16px;
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
.env-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: grid;
place-items: center;
background: var(--fg-soft);
color: var(--muted);
flex-shrink: 0;
}
.env-icon svg { width: 14px; height: 14px; }
.env-icon[data-ok="true"] { background: var(--ok-soft); color: var(--ok); }
.env-icon[data-ok="false"] { background: var(--err-soft); color: var(--err); }
.env-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.env-name {
font-size: 13px;
font-weight: 500;
color: var(--fg);
}
.env-value {
font-family: var(--font-mono);
font-size: var(--fs-mono);
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ─── issues / warnings list ──────────────────────────────────── */
.issue-list {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.issue {
display: flex;
gap: 8px;
align-items: flex-start;
padding: 8px 12px;
background: var(--warn-soft);
border: 1px solid color-mix(in oklch, var(--warn) 30%, transparent);
border-radius: 8px;
font-size: 12.5px;
color: var(--fg);
line-height: 1.45;
}
.issue .icon {
flex-shrink: 0;
color: var(--warn);
width: 14px;
height: 14px;
margin-top: 1px;
}
/* ─── session info (when running) ─────────────────────────────── */
.session {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1px;
background: var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.session-cell {
background: var(--surface);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.session-label {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.session-value {
font-family: var(--font-mono);
font-size: 14px;
color: var(--fg);
font-variant-numeric: tabular-nums;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ─── log / output panel ─────────────────────────────────────── */
.log {
background: oklch(13% 0.012 260);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.log-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.log-head .panel-title { margin: 0; }
.log-head-actions { display: flex; gap: 4px; }
.log-head-actions button {
width: 24px;
height: 24px;
border-radius: 5px;
color: var(--muted);
display: grid;
place-items: center;
transition: background 0.15s ease, color 0.15s ease;
}
.log-head-actions button:hover { background: var(--fg-soft); color: var(--fg); }
.log-head-actions svg { width: 12px; height: 12px; }
.log-body {
max-height: 220px;
overflow-y: auto;
padding: 12px 16px;
font-family: var(--font-mono);
font-size: var(--fs-mono);
line-height: 1.65;
color: oklch(82% 0.008 260);
}
.log-body::-webkit-scrollbar { width: 8px; }
.log-body::-webkit-scrollbar-track { background: transparent; }
.log-body::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.log-line {
display: flex;
gap: 10px;
padding: 2px 0;
}
.log-ts {
color: var(--muted);
flex-shrink: 0;
opacity: 0.6;
}
.log-msg { flex: 1; word-break: break-all; }
.log-line[data-kind="ok"] .log-msg { color: oklch(85% 0.12 155); }
.log-line[data-kind="err"] .log-msg { color: oklch(82% 0.16 25); }
.log-line[data-kind="info"] .log-msg { color: oklch(82% 0.08 220); }
.log-empty {
color: var(--muted);
font-style: italic;
padding: 24px 0;
text-align: center;
}
/* ─── footer ─────────────────────────────────────────────────── */
.foot {
padding: 16px 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--muted);
letter-spacing: 0.04em;
}
.foot-links { display: flex; gap: 16px; }
.foot-links a { color: var(--muted); transition: color 0.15s ease; }
.foot-links a:hover { color: var(--fg); }
.foot-shortcut {
display: inline-flex;
align-items: center;
gap: 6px;
}
.foot-shortcut .kbd {
padding: 1px 5px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 10.5px;
color: var(--fg);
background: var(--surface);
}
/* ─── responsive — narrow window (sidebar collapses) ─────────── */
@media (max-width: 920px) {
.main { padding: 20px 20px 32px; }
.hero { grid-template-columns: 1fr; row-gap: 18px; }
.hero-fish { justify-self: start; }
.env-grid { grid-template-columns: 1fr; }
.session { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="shell" data-od-id="shell">
<!-- ─── macOS titlebar ──────────────────────────────────────────── -->
<div class="titlebar" data-od-id="titlebar">
<span class="titlebar-title">dsh-launcher</span>
<div class="titlebar-actions">
<button class="titlebar-btn" aria-label="Hide window" onclick="HideWindow()">
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 6h8"/></svg>
</button>
</div>
</div>
<!-- ─── main content ────────────────────────────────────────────── -->
<div class="main" data-od-id="main">
<!-- HERO: title + status + start/stop -->
<section class="hero" data-od-id="hero">
<img class="hero-fish" src="assets/appicon.png" alt="" data-od-id="hero-fish" />
<div class="hero-text">
<span class="hero-eyebrow">本地启动器</span>
<h1>dsh web UI</h1>
<p class="hero-sub">检查环境 · 启动本地服务<br/>自动打开浏览器,启动后窗口隐藏到状态栏。</p>
<span class="status" id="status-pill" data-state="idle">
<span class="dot"></span>
<span id="status-text">未运行</span>
</span>
</div>
<div class="row-actions">
<button class="start-btn" id="btn-start" onclick="Start()">
<svg class="icon" viewBox="0 0 16 16" fill="currentColor"><path d="M4 3.5v9l8-4.5-8-4.5z"/></svg>
启动 dsh web
<span class="kbd">⌘ ⏎</span>
</button>
<button class="stop-btn" id="btn-stop" disabled onclick="Stop()">
<svg class="icon" viewBox="0 0 16 16" fill="currentColor"><rect x="4" y="4" width="8" height="8" rx="1"/></svg>
停止
</button>
</div>
</section>
<!-- ENVIRONMENT: 4 cells, dense -->
<section class="panel" data-od-id="env">
<div class="panel-head">
<span class="panel-title">环境检查</span>
<button class="panel-action" onclick="CheckEnv()">重新检查</button>
</div>
<div class="env-grid">
<div class="env-cell" data-od-id="env-node">
<div class="env-icon" data-ok="true" id="env-node-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 3 4 7v10l8 4 8-4V7l-8-4z"/>
<path d="m4 7 8 4 8-4M12 11v10"/>
</svg>
</div>
<div class="env-body">
<span class="env-name">Node.js</span>
<span class="env-value" id="env-node-val">v20.14.0 · /opt/homebrew/bin/node</span>
</div>
</div>
<div class="env-cell" data-od-id="env-dsh">
<div class="env-icon" data-ok="true" id="env-dsh-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M5 4h14M9 4v16M15 4v16M5 20h14"/>
</svg>
</div>
<div class="env-body">
<span class="env-name">dsh CLI</span>
<span class="env-value" id="env-dsh-val">0.5.2 · /opt/homebrew/bin/dsh</span>
</div>
</div>
<div class="env-cell" data-od-id="env-cred">
<div class="env-icon" data-ok="true" id="env-cred-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<rect x="4" y="9" width="16" height="11" rx="2"/>
<path d="M8 9V6a4 4 0 0 1 8 0v3"/>
</svg>
</div>
<div class="env-body">
<span class="env-name">登录凭证</span>
<span class="env-value" id="env-cred-val">~/.dsh/.credentials.yaml</span>
</div>
</div>
<div class="env-cell" data-od-id="env-port">
<div class="env-icon" data-ok="true" id="env-port-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M4 17h16M4 17v-3a8 8 0 0 1 16 0v3"/>
<circle cx="12" cy="17" r="1.5" fill="currentColor"/>
</svg>
</div>
<div class="env-body">
<span class="env-name">端口 3080</span>
<span class="env-value" id="env-port-val">空闲</span>
</div>
</div>
</div>
<!-- issues only show when present (hidden by default) -->
<div class="issue-list" id="issue-list" hidden></div>
</section>
<!-- SESSION: only when running -->
<section class="panel" data-od-id="session" id="session-panel" hidden>
<div class="panel-head">
<span class="panel-title">当前会话</span>
<span class="panel-meta" id="session-meta">运行中</span>
</div>
<div class="session">
<div class="session-cell">
<span class="session-label">URL</span>
<span class="session-value" id="session-url">http://127.0.0.1:3080/?token=…</span>
</div>
<div class="session-cell">
<span class="session-label">PID</span>
<span class="session-value" id="session-pid"></span>
</div>
<div class="session-cell">
<span class="session-label">启动于</span>
<span class="session-value" id="session-started"></span>
</div>
</div>
</section>
<!-- LOG: real-time output -->
<section class="log" data-od-id="log">
<div class="log-head">
<span class="panel-title">输出日志</span>
<div class="log-head-actions">
<button aria-label="清空日志" onclick="clearLog()">
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.4">
<path d="M3 4h6m-5 0V3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1M4 4l.5 6a1 1 0 0 0 1 .9h1a1 1 0 0 0 1-.9L8 4"/>
</svg>
</button>
</div>
</div>
<div class="log-body" id="log-body">
<div class="log-line" data-kind="info">
<span class="log-ts">12:42:08</span>
<span class="log-msg">环境检查通过 · node v20.14.0 · dsh 0.5.2 · 端口 3080 空闲</span>
</div>
<div class="log-empty" id="log-empty" hidden>暂无输出</div>
</div>
</section>
<!-- FOOTER -->
<footer class="foot" data-od-id="footer">
<span>dsh-launcher · local-only</span>
<div class="foot-links">
<span class="foot-shortcut">退出 <span class="kbd">⌘ Q</span></span>
<a href="#" onclick="event.preventDefault(); ShowWindow()">显示窗口</a>
<a href="#" onclick="event.preventDefault(); HideWindow()">隐藏窗口</a>
</div>
</footer>
</div>
</div>
<script>
/* ─── Wails-bound stubs (real app replaces these) ─────────────── */
function CheckEnv() { /* wails: App.CheckEnv() */ }
function Start() {
/* wails: App.Start() — populates session panel + log */
document.getElementById('status-pill').setAttribute('data-state', 'running');
document.getElementById('status-text').textContent = '启动中…';
appendLog('info', '执行 dsh --profile web --no-open');
}
function Stop() { /* wails: App.Stop() */ }
function ShowWindow() { /* wails: App.ShowWindow() */ }
function HideWindow() { /* wails: App.HideWindow() */ }
function GetStatus() { /* wails: App.GetStatus() */ }
/* ─── log helper (demo only) ──────────────────────────────────── */
function appendLog(kind, msg) {
const body = document.getElementById('log-body');
const empty = document.getElementById('log-empty');
if (empty) empty.hidden = true;
const ts = new Date().toTimeString().slice(0, 8);
const line = document.createElement('div');
line.className = 'log-line';
line.setAttribute('data-kind', kind);
line.innerHTML = '<span class="log-ts">' + ts + '</span><span class="log-msg"></span>';
line.querySelector('.log-msg').textContent = msg;
body.appendChild(line);
body.scrollTop = body.scrollHeight;
}
function clearLog() {
const body = document.getElementById('log-body');
body.innerHTML = '<div class="log-empty" id="log-empty">暂无输出</div>';
}
</script>
</body>
</html>