Files
dsh-launcher/ui-design/dsh-launcher-ui.html
T
toor 218ee31adb chore: initial commit
- Wails v2 + Go 1.21+ + Vue 3 dark UI
- 3 buttons: Check env (auto-install dsh) / Start dsh web / Stop
- 升级 / 检查版本 一键按钮
- 启动后窗口自动最小化到 Dock
- 顶部 logo 鼠标 hover 显示大图预览
- scripts/: build-mac.sh, build-windows.bat, install-app.sh
- ui-design/: 视觉设计稿 (HTML + tokens)
2026-09-14 15:06:14 +08:00

707 lines
25 KiB
HTML
Raw 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>
<!--
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>