t193:daemon UI 換 Wails——CIS 是硬要求,fyne 做不到

leo 2026-08-04 看過 v0.16.0 畫面後:
「功能都有了,但美感非常糟糕⋯⋯跟 CIS 完全無關,每個功能都開一個小小的 popup 視窗,
 非常缺乏整體感,這要理解的是原始的技術選擇是否出錯?」
「我的要求是符合 CIS,在風格上跟 portal 一樣」
「CIS 已經提供規範,你做的連 Logo 都沒放上去,這跟美不美有關係嗎?
 要求放進 CIS 是硬要求,你做為檢查有嗎?沒有怎麼交貨?」

## 我的兩個錯
1. **動工前沒提醒做不到**——decisions-summary.md:314 的 D-daemon-UI
   是我 07-27 自己寫的調研(結論:fyne 全自繪、CSS 套不進去),寫完就沒再看。
   今天動工前該查它卻直接開寫,浪費 leo 的時間與 token。
2. **沒做 CIS 檢查就交貨**——連 logo 都沒放。

## 換 Wails(WebView 殼)
前端就是 HTML/CSS ⇒ 可以直接用 portal 那份色票與 lockup。
- style.css 的色票/字體/紙張紋理**逐字取自** portal 的 :root
  (matrix/arcrun:console-ui/public/portal/index.html),不自創任何顏色
- CIS lockup 官方 PNG,淺色 ink 版/深色 paper 版,切換規則同 portal
  (避開 08-01 作廢的自產 SVG——字腔缺失)
- 對話框改**內嵌覆蓋層**,不再每個功能開一個小 popup
- 原生資料夾選擇器(macOS powerbox 會自動授予該資料夾存取權,
  fyne 自繪 picker 拿不到;未來上 Mac App Store 是硬需求)
- 同步中的圓點會呼吸 ⇒ 看得出來在動(issue #17)
- 無帳號時是 onboarding 引導,不是空白面板

## 連線邏輯逐字沿用,不重寫
connect.go 的 normalizePortalURL/fetchConfigByLogin 取自 arcrun-tray/main.go,
含 t86 個資外洩事故的防線(不同實例=新增帳號,絕不覆蓋舊帳號的資料夾)。
今天已犯過一次「重寫別人修好的東西還修得更差」,不再犯第二次。

## 新增 check-cis.sh(交貨前必跑)
機械檢查六類:官方色票逐個到齊/**不准出現非 CIS 色**/logo 真的放進去/
不可用作廢 SVG/深色模式/字體與紙張紋理同 portal/app icon。
實跑 14 項全過。以後「沒跑過就不准交」。

⚠️ 未驗:實際畫面要 leo 開來看(我開不了視窗)。
This commit is contained in:
2026-08-04 22:19:41 +08:00
parent cc6534dd39
commit bd0cead00e
30 changed files with 2856 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
/* Arcrun 桌面 App — 樣式(t193
*
* 🔴 leo 2026-08-04:「我的要求是**符合 CIS**,在風格上**跟 portal 一樣**」
* 「CIS 已經提供規範,你做的**連 Logo 都沒放上去**,這跟美不美有關係嗎?
* 要求放進 CIS 是**硬要求**,你做為檢查有嗎?沒有怎麼交貨?」
*
* ⇒ 本檔的色票/字體/背景紋理**逐字取自 portal**
* matrix/arcrun:console-ui/public/portal/index.html 的 :root 區塊),
* **不自創任何顏色**。要改就去改 portal 那份、再同步過來。
*
* 這也是換掉 fyne 的理由:fyne 全自繪、CSS 套不進去,
* 換成 WailsWebView)後 daemon 與 portal 才可能是**同一套視覺**。
*/
:root {
--paper-a: #FDFCFB; --paper-b: #F2F1ED;
--ink: #17181A; --ink-rgb: 23,24,26;
--amber: #B04A2F; --amber-rgb: 176,74,47;
--ok: #1d7a48; --ok-rgb: 29,122,72;
--err: #b03a26; --err-rgb: 176,58,38;
--well: rgba(23,24,26,.06); --well2: rgba(23,24,26,.08);
}
:root[data-theme="dark"] {
--paper-a: #1b1c1e; --paper-b: #1e1f21;
--ink: #FDFCFB; --ink-rgb: 253,252,251;
--amber: #D9784F; --amber-rgb: 217,120,79;
--ok: #7fe0a8; --ok-rgb: 63,190,120;
--err: #e58575; --err-rgb: 217,95,76;
--well: rgba(0,0,0,.25); --well2: rgba(0,0,0,.3);
}
/* portal 的紙張紋理背景——三站一致的識別(不是裝飾,是 CIS 的一部分) */
html, body {
margin: 0; height: 100%;
background: repeating-linear-gradient(0deg,
var(--paper-a) 0px, var(--paper-a) 3px,
var(--paper-b) 3px, var(--paper-b) 4px);
color: var(--ink);
font-family: -apple-system, "IBM Plex Sans", "PingFang TC", "Noto Sans TC",
"Microsoft JhengHei", system-ui, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
input, textarea, button, select { font-family: inherit; }
::placeholder { color: rgba(var(--ink-rgb), .35); }
input:focus, select:focus { outline: 2px solid rgba(var(--amber-rgb), .5); outline-offset: 1px; }
a { color: var(--amber); }
.muted { color: rgba(var(--ink-rgb), .55); }
.dim { color: rgba(var(--ink-rgb), .4); }
/* ── 版面:上(品牌+狀態)/中(可捲清單)/下(動作列)── */
#app { display: flex; flex-direction: column; height: 100vh; }
header {
padding: 18px 24px 14px;
border-bottom: 1px solid rgba(var(--ink-rgb), .1);
display: flex; align-items: flex-start; gap: 20px;
}
/* CIS lockup:淺色用 ink 版、深色用 paper 版(與 portal 同一組 PNG、同一套切換規則)。
⚠️ 不可用自產的 SVG 描邊版(字腔會缺,08-01 已作廢,見 arcrun-cis/README.md)。 */
.brand img { height: 26px; display: block; }
.brand .wm-paper { display: none; }
:root[data-theme="dark"] .brand .wm-ink { display: none; }
:root[data-theme="dark"] .brand .wm-paper { display: block; }
.status { flex: 1; min-width: 0; }
.status .big { font-size: 17px; font-weight: 600; letter-spacing: .02em; }
.status .sub { margin-top: 4px; font-size: 13.5px; color: rgba(var(--ink-rgb), .5); }
/* 同步中會呼吸——「看得出來在動」(issue #17) */
.status .big.syncing::after {
content: ""; display: inline-block; width: 7px; height: 7px; margin-left: 9px;
border-radius: 50%; background: var(--amber); vertical-align: middle;
animation: pulse 1.1s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: .25 } 50% { opacity: 1 } }
/* 按鈕:Relation 是**唯一重點色**,CIS 規定 ≤ 一個畫面的 5% ⇒ 只有主要動作用它 */
button {
border: 1px solid rgba(var(--ink-rgb), .16);
background: transparent; color: var(--ink);
padding: 8px 14px; border-radius: 8px; font-size: 14px; cursor: pointer;
transition: background .12s, border-color .12s;
}
button:hover { background: var(--well); }
button.primary {
background: var(--amber); border-color: var(--amber);
color: #FDFCFB; font-weight: 600;
}
button.primary:hover { filter: brightness(1.06); }
button.ghost { border-color: transparent; color: rgba(var(--ink-rgb), .5); padding: 6px 8px; }
button.ghost:hover { color: var(--err); background: transparent; }
/* ── 中:資料夾清單(leo:「幾十個,根本塞不下」⇒ 這一區才捲動)── */
main { flex: 1; overflow-y: auto; padding: 6px 0; }
.acct {
padding: 16px 24px 6px;
display: flex; align-items: baseline; gap: 10px;
}
.acct .name { font-size: 15px; font-weight: 600; }
.acct .host { font-size: 12.5px; color: rgba(var(--ink-rgb), .4); font-family: ui-monospace, Menlo, monospace; }
.folder {
margin: 0 16px 6px; padding: 11px 14px;
background: var(--well); border-radius: 10px;
display: flex; align-items: center; gap: 12px;
}
.folder .path { flex: 1; min-width: 0; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.folder .tag { font-size: 12px; color: var(--ok); white-space: nowrap; }
/* 空狀態=onboarding(不是空白面板) */
.empty { padding: 56px 32px; text-align: center; }
.empty .t { font-size: 17px; font-weight: 600; margin-bottom: 10px; }
.empty .d { font-size: 14px; color: rgba(var(--ink-rgb), .55); line-height: 1.9; margin-bottom: 22px; }
footer {
padding: 12px 24px; border-top: 1px solid rgba(var(--ink-rgb), .1);
display: flex; align-items: center; gap: 8px;
}
footer .spacer { flex: 1; }
footer .ver { font-size: 12.5px; color: rgba(var(--ink-rgb), .35); font-family: ui-monospace, Menlo, monospace; }
/* 對話框:**內嵌覆蓋層**,不是另開一個小 popup 視窗
(leo:「每個功能都開一個小小的 popup 視窗,非常缺乏整體感」) */
.overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.42);
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .14s;
}
.overlay.on { opacity: 1; pointer-events: auto; }
.sheet {
width: min(520px, calc(100vw - 64px));
background: var(--paper-a); color: var(--ink);
border: 1px solid rgba(var(--ink-rgb), .12);
border-radius: 14px; padding: 24px;
box-shadow: 0 18px 50px rgba(0,0,0,.28);
}
.sheet h2 { margin: 0 0 6px; font-size: 17px; }
.sheet p { margin: 0 0 16px; font-size: 14px; color: rgba(var(--ink-rgb), .55); line-height: 1.7; }
.sheet label { display: block; font-size: 13px; margin: 12px 0 5px; color: rgba(var(--ink-rgb), .6); }
.sheet input[type=text], .sheet input[type=password] {
width: 100%; box-sizing: border-box; padding: 9px 11px; font-size: 14px;
border: 1px solid rgba(var(--ink-rgb), .18); border-radius: 8px;
background: var(--paper-b); color: var(--ink);
}
.sheet .radio { display: flex; align-items: flex-start; gap: 9px; margin: 10px 0; font-size: 14px; cursor: pointer; }
.sheet .radio input { margin-top: 3px; }
.sheet .acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; }
.sheet .err { color: var(--err); font-size: 13px; margin-top: 10px; }