t193 二修:側邊欄版面+共用 CSS+App 內更新+兩支視覺機械閘
leo 08-04 看過 v0.17.0 後的四點,逐條回應:
## ① 「不會這裡又寫了一個新的 CSS?」——是,我又寫了一份
真兇:portal 的樣式內嵌在 HTML 裡,沒有獨立檔 ⇒ 四個站各抄一份、本來就在漂移。
解:抽出 InkStoneCo/arcrun-cis/css/arcrun-cis.css 當唯一真相源。
daemon 的 style.css 現在**一個 hex 都沒有**,顏色全走 var(--...)。
## ② 「完全模仿 gdrive,左方側邊欄,在右方替換頁面」
底部 nav bar 拿掉(leo:「幾乎沒看過這種 nav bar 在下方的」)。
改成左側邊欄四頁:首頁/同步資料夾/AI 設定/版本與更新,右側換頁。
設定不再開 popup;只有「確認移除資料夾」才用覆蓋層。
## ③ 「既然是 Web,你做的時候無法檢視?」——可以,我之前沒做
新增 check-render.sh:headless Chrome 真的渲染一次並**量像素**。
它抓到一個肉眼容易誤判的真 bug:
arcrun-lockup-h-paper-on-ink.png **自帶不透明 Ink 底板**(實測左上像素 RGBA=23,24,26,255),
設計前提是貼在純 Ink 表面;我們背景有紙張紋理 ⇒ 出現突兀方塊。
修:深色模式把品牌區做成純 Ink。實測落差 4(原本會是一個明顯方塊)。
過程中也發現自己對著**舊的 dist**除錯,浪費一輪——這支閘同時防這個。
## ④ 「檢查更新連到 docs,不正確,是直接在這裡進行」
新增「版本與更新」頁:顯示**現在版本 vs 最新版本**,
三段式全在 App 內完成——檢查/下載並安裝/重新啟動套用。
selfupdate 邏輯逐字沿用 arcrun-tray(含 t186 走 raw、t184 蓋正在跑的 .app),不重寫。
## 交貨閘(leo:「你做為檢查有嗎?沒有怎麼交貨?」)
check-cis.sh 擴充:版面檔不准出現任何 hex + 共用底層色票逐項比對。
兩支閘實跑全過。
This commit is contained in:
@@ -4,37 +4,47 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Arcrun</title>
|
||||
<!-- 🔴 共用底層:色票/字體/紙張紋理的**唯一真相源**是 arcrun-cis/css/arcrun-cis.css。
|
||||
leo 08-04:「不會這裡又寫了一個新的 CSS?前面剛剛才說過不要創造新的?」
|
||||
⇒ 這份是複製進來給 build 用的,**要改配色去改 arcrun-cis/css/ 那份**。 -->
|
||||
<link rel="stylesheet" href="./src/arcrun-cis.css"/>
|
||||
<link rel="stylesheet" href="./src/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<header>
|
||||
<!-- CIS lockup(硬要求):官方 PNG,淺色用 ink 版、深色用 paper 版。
|
||||
⚠️ 不可用自產 SVG 描邊版——字腔會缺,08-01 已作廢(arcrun-cis/README.md)。 -->
|
||||
<!-- ── 左側邊欄(Google Drive 風格;leo:「左方側邊欄,在右方替換頁面」)── -->
|
||||
<aside id="side">
|
||||
<div class="brand">
|
||||
<img class="wm-ink" src="./src/assets/arcrun-lockup-h-ink.png" alt="Arcrun"/>
|
||||
<img class="wm-paper" src="./src/assets/arcrun-lockup-h-paper-on-ink.png" alt="Arcrun"/>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div class="big" id="statusBig">啟動中…</div>
|
||||
<div class="sub" id="statusSub"></div>
|
||||
|
||||
<nav>
|
||||
<div class="nav on" data-page="home"> <span class="ic">◫</span>首頁</div>
|
||||
<div class="nav" data-page="folders"> <span class="ic">▤</span>同步資料夾</div>
|
||||
<div class="nav" data-page="ai"> <span class="ic">✧</span>AI 設定</div>
|
||||
<div class="nav" data-page="update"> <span class="ic">↧</span>版本與更新</div>
|
||||
</nav>
|
||||
|
||||
<div class="side-foot">
|
||||
<div class="ver" id="ver"></div>
|
||||
</div>
|
||||
<button class="primary" id="btnSync">立刻同步</button>
|
||||
</header>
|
||||
</aside>
|
||||
|
||||
<main id="list"></main>
|
||||
|
||||
<footer>
|
||||
<button id="btnAddFolder">加入資料夾</button>
|
||||
<button id="btnAddAccount">新增知識庫帳號</button>
|
||||
<button id="btnAI">AI 設定</button>
|
||||
<button id="btnUpdate">檢查更新</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="ver" id="ver"></span>
|
||||
</footer>
|
||||
<!-- ── 右側內容區(換頁)── -->
|
||||
<section id="content">
|
||||
<header>
|
||||
<div class="status">
|
||||
<div class="big" id="statusBig">啟動中…</div>
|
||||
<div class="sub" id="statusSub"></div>
|
||||
</div>
|
||||
<button class="primary" id="btnSync">立刻同步</button>
|
||||
</header>
|
||||
<main id="page"></main>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 內嵌覆蓋層,不另開小視窗(leo:「每個功能都開一個小小的 popup,非常缺乏整體感」) -->
|
||||
<!-- 內嵌覆蓋層,不另開小視窗 -->
|
||||
<div class="overlay" id="overlay"><div class="sheet" id="sheet"></div></div>
|
||||
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* arcrun-cis.css — Arcrun 全站共用樣式底層(唯一真相源)
|
||||
*
|
||||
* 🔴 leo 2026-08-04:「不會這裡又寫了一個新的 CSS?前面剛剛才說過**不要創造新的**?」
|
||||
*
|
||||
* 病根:portal 的樣式**內嵌在 HTML 裡**(單一 <style>),沒有獨立檔
|
||||
* ⇒ portal/landing/install/daemon **各自抄一份** ⇒ 必然漂移。
|
||||
* ⇒ 抽出這一份,四邊都引用它。**要改配色只改這裡。**
|
||||
*
|
||||
* 內容=CIS 官方色票(arcrun-cis/README.md 的 Colour 表)+字體+紙張紋理。
|
||||
* 這裡**只放共用底層**,各站自己的版面/元件樣式仍在各站。
|
||||
*/
|
||||
|
||||
: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; --ok-soft: #1d6b40;
|
||||
--err: #b03a26; --err-rgb: 176,58,38;
|
||||
--well: rgba(23,24,26,.06); --well2: rgba(23,24,26,.08);
|
||||
--bar-bg: #F2F1ED; --track: rgba(23,24,26,.12); --knob: #FDFCFB;
|
||||
}
|
||||
: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; --ok-soft: #a8e8c4;
|
||||
--err: #e58575; --err-rgb: 217,95,76;
|
||||
--well: rgba(0,0,0,.25); --well2: rgba(0,0,0,.3);
|
||||
--bar-bg: #17181A; --track: rgba(255,255,255,.08); --knob: #FDFCFB;
|
||||
}
|
||||
|
||||
html, body { margin: 0; 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; }
|
||||
+211
-105
@@ -1,62 +1,70 @@
|
||||
// Arcrun 桌面 App — 前端(t193)
|
||||
//
|
||||
// 全部畫面都在**同一個視窗**裡:狀態、清單、設定都用內嵌覆蓋層,
|
||||
// 不再像 fyne 版那樣「每個功能開一個小小的 popup」(leo 08-04 點破)。
|
||||
import './style.css';
|
||||
// 版面照 leo 08-04 指定(附 Google Drive 截圖):
|
||||
// 「可以完全模仿 gdrive,**左方側邊欄,在右方替換頁面**」
|
||||
// ⇒ 設定不再開 popup,全部是右側的頁;只有「確認刪除」才用覆蓋層。
|
||||
import './arcrun-cis.css'; // 共用底層(色票/字體/紋理)——唯一真相源在 arcrun-cis/css/
|
||||
import './style.css'; // 本 App 的版面
|
||||
|
||||
const go = window.go.main.App;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
// 跟隨系統深淺色——CIS 兩套色票都定義好了,這裡只負責切 data-theme
|
||||
// 跟隨系統深淺色(CIS 兩套色票都在 arcrun-cis.css 定義好了)
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const applyTheme = () =>
|
||||
document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light');
|
||||
mq.addEventListener('change', applyTheme);
|
||||
applyTheme();
|
||||
|
||||
// ── 覆蓋層(取代 popup 視窗)──
|
||||
let state = null;
|
||||
let page = 'home';
|
||||
let updateInfo = null; // { current, latest, available, notes, staged }
|
||||
|
||||
// ── 覆蓋層(只給必須打斷的確認)──
|
||||
function openSheet(html, wire) {
|
||||
$('sheet').innerHTML = html;
|
||||
$('overlay').classList.add('on');
|
||||
if (wire) wire();
|
||||
$('sheet').innerHTML = html; $('overlay').classList.add('on'); if (wire) wire();
|
||||
}
|
||||
function closeSheet() { $('overlay').classList.remove('on'); }
|
||||
$('overlay').addEventListener('click', (e) => { if (e.target.id === 'overlay') closeSheet(); });
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSheet(); });
|
||||
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
// ── 側邊欄換頁 ──
|
||||
document.querySelectorAll('#side .nav').forEach((el) => {
|
||||
el.onclick = () => {
|
||||
page = el.dataset.page;
|
||||
document.querySelectorAll('#side .nav').forEach((n) => n.classList.toggle('on', n === el));
|
||||
renderPage();
|
||||
};
|
||||
});
|
||||
|
||||
// ── 主畫面渲染 ──
|
||||
let state = null;
|
||||
// ── 各頁 ──
|
||||
function pageHome(s) {
|
||||
const folders = (s.accounts || []).reduce((n, a) => n + (a.folders || []).length, 0);
|
||||
if (!s.accounts || !s.accounts.length) return onboarding();
|
||||
return `
|
||||
<div class="card">
|
||||
<h3>目前狀況</h3>
|
||||
<div class="kv" style="margin-top:10px">
|
||||
<div><div class="big-num">${s.accounts.length}</div><div class="k">個知識庫</div></div>
|
||||
<div><div class="big-num">${folders}</div><div class="k">個資料夾在看守</div></div>
|
||||
<div><div class="big-num">${s.extractedOK || 0}</div><div class="k">份已整理</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>快速動作</h3>
|
||||
<div class="acts">
|
||||
<button id="qAdd">加入資料夾</button>
|
||||
<button id="qPortal">開啟知識庫網頁</button>
|
||||
<button id="qDocs">使用說明</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
state = s;
|
||||
$('ver').textContent = s.version ? '版本 ' + s.version : '';
|
||||
$('statusBig').textContent = s.statusBig;
|
||||
$('statusBig').classList.toggle('syncing', !!s.syncing);
|
||||
$('statusSub').textContent = s.statusSub;
|
||||
|
||||
const list = $('list');
|
||||
if (!s.accounts || s.accounts.length === 0) {
|
||||
// 空狀態=onboarding,不是空白面板
|
||||
list.innerHTML = `
|
||||
<div class="empty">
|
||||
<div class="t">先連上你的知識庫</div>
|
||||
<div class="d">
|
||||
Arcrun 會看守你指定的資料夾,<br/>
|
||||
自動把文件整理成 AI 查得到的知識卡。<br/>
|
||||
還沒有知識庫?先到 install.arcrun.dev 裝一個(免費)。
|
||||
</div>
|
||||
<button class="primary" id="obConnect">新增知識庫帳號</button>
|
||||
<button id="obInstall">我還沒有知識庫</button>
|
||||
</div>`;
|
||||
$('obConnect').onclick = showConnect;
|
||||
$('obInstall').onclick = () => go.OpenURL('https://install.arcrun.dev/');
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = s.accounts.map((a) => `
|
||||
function pageFolders(s) {
|
||||
if (!s.accounts || !s.accounts.length) return onboarding();
|
||||
return (s.accounts).map((a) => `
|
||||
<div class="acct"><span class="name">${esc(a.name)}</span><span class="host">${esc(a.host)}</span></div>
|
||||
${(a.folders || []).map((f) => `
|
||||
<div class="folder">
|
||||
@@ -64,100 +72,198 @@ function render(s) {
|
||||
<span class="tag">自動同步中</span>
|
||||
<button class="ghost" data-rm="${esc(f.path)}" data-acc="${f.accIdx}">移除</button>
|
||||
</div>`).join('')
|
||||
|| `<div class="folder"><span class="path dim">還沒有資料夾——按下面的「加入資料夾」</span></div>`}
|
||||
`).join('');
|
||||
|| `<div class="folder"><span class="path dim">還沒有資料夾</span></div>`}
|
||||
`).join('') + `
|
||||
<div class="acts">
|
||||
<button class="primary" id="fAdd">加入資料夾</button>
|
||||
<button id="fAcct">新增知識庫帳號</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
list.querySelectorAll('[data-rm]').forEach((b) => {
|
||||
function pageAI(s) {
|
||||
const gem = s.engine === 'gemma';
|
||||
return `
|
||||
<div class="card">
|
||||
<h3>用哪個 AI 幫你整理文件?</h3>
|
||||
<label class="radio"><input type="radio" name="ai" value="cloud" ${gem ? '' : 'checked'}/>
|
||||
<span><b>雲端 AI</b>(推薦・不必申請任何金鑰)<br/>
|
||||
<span class="d">用你自己 Cloudflare 帳號內建的 AI,不需要任何金鑰、不必去別的網站申請。</span></span></label>
|
||||
<label class="radio"><input type="radio" name="ai" value="gemini" ${gem ? 'checked' : ''}/>
|
||||
<span><b>Google Gemini</b>(需要自己申請金鑰)<br/>
|
||||
<span class="d">進階選項。要自己去 aistudio.google.com 申請一把 API Key。</span></span></label>
|
||||
<div class="field">
|
||||
<div class="lb">Gemini 金鑰${s.geminiKey ? '(目前已設定,清空並儲存即可刪除)' : ''}</div>
|
||||
<input type="text" id="k" placeholder="貼上你的 Gemini API Key"/>
|
||||
</div>
|
||||
<div class="err" id="aiErr" style="display:none"></div>
|
||||
<div class="acts"><button class="primary" id="aiSave">儲存</button></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// leo:「檢查更新連到 docs 的檢查更新,不正確,**是直接在這裡進行檢查更新**,
|
||||
// 應該是跳到一個頁面顯示**現在版本與最新版本**,然後**下載可以進行安裝**」
|
||||
function pageUpdate(s) {
|
||||
const u = updateInfo;
|
||||
const latest = u ? (u.latest || '查詢中…') : '查詢中…';
|
||||
let action = `<button class="primary" id="uCheck">檢查更新</button>`;
|
||||
let note = '';
|
||||
if (u && u.staged) {
|
||||
action = `<button class="primary" id="uApply">重新啟動以完成更新</button>`;
|
||||
note = `<div class="d">新版 ${esc(u.latest)} 已下載完成,重新啟動就會套用。</div>`;
|
||||
} else if (u && u.available) {
|
||||
action = `<button class="primary" id="uDownload">下載並安裝 ${esc(u.latest)}</button>`;
|
||||
note = u.notes ? `<div class="d">${esc(u.notes)}</div>` : '';
|
||||
} else if (u && u.err) {
|
||||
note = `<div class="err">${esc(u.err)}</div>`;
|
||||
} else if (u) {
|
||||
note = `<div class="d">你已經是最新版本。</div>`;
|
||||
}
|
||||
return `
|
||||
<div class="card">
|
||||
<h3>版本與更新</h3>
|
||||
<div class="kv" style="margin-top:12px">
|
||||
<div><div class="big-num">${esc(s.version || '—')}</div><div class="k">目前版本</div></div>
|
||||
<div><div class="big-num">${esc(latest)}</div><div class="k">最新版本</div></div>
|
||||
</div>
|
||||
<div style="margin-top:14px">${note}</div>
|
||||
<div class="acts">${action}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>需要協助?</h3>
|
||||
<div class="d">安裝、更新、把知識庫接到你的 AI,說明文件都寫在這裡。</div>
|
||||
<div class="acts"><button id="uDocs">開啟使用說明</button></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function onboarding() {
|
||||
return `
|
||||
<div class="empty">
|
||||
<div class="t">先連上你的知識庫</div>
|
||||
<div class="d">
|
||||
Arcrun 會看守你指定的資料夾,<br/>
|
||||
自動把文件整理成 AI 查得到的知識卡。<br/>
|
||||
還沒有知識庫?先到 install.arcrun.dev 裝一個(免費)。
|
||||
</div>
|
||||
<button class="primary" id="obConnect">新增知識庫帳號</button>
|
||||
<button id="obInstall">我還沒有知識庫</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 渲染 ──
|
||||
function renderPage() {
|
||||
if (!state) return;
|
||||
const el = $('page');
|
||||
el.innerHTML = page === 'folders' ? pageFolders(state)
|
||||
: page === 'ai' ? pageAI(state)
|
||||
: page === 'update' ? pageUpdate(state)
|
||||
: pageHome(state);
|
||||
wire();
|
||||
}
|
||||
|
||||
function wire() {
|
||||
const on = (id, fn) => { const e = $(id); if (e) e.onclick = fn; };
|
||||
on('qAdd', addFolder); on('fAdd', addFolder);
|
||||
on('qDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/'));
|
||||
on('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/use/update/'));
|
||||
on('qPortal', () => {
|
||||
const a = state.accounts && state.accounts[0];
|
||||
if (a) go.OpenURL('https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/');
|
||||
});
|
||||
on('fAcct', showConnect); on('obConnect', showConnect);
|
||||
on('obInstall', () => go.OpenURL('https://install.arcrun.dev/'));
|
||||
on('aiSave', saveAI);
|
||||
on('uCheck', checkUpdate);
|
||||
on('uDownload', downloadUpdate);
|
||||
on('uApply', applyUpdate);
|
||||
document.querySelectorAll('[data-rm]').forEach((b) => {
|
||||
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
|
||||
});
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
const first = !state;
|
||||
state = s;
|
||||
$('ver').textContent = s.version ? s.version : '';
|
||||
$('statusBig').textContent = s.statusBig;
|
||||
$('statusBig').classList.toggle('syncing', !!s.syncing);
|
||||
$('statusSub').textContent = s.statusSub;
|
||||
// 只有狀態列每秒更新;頁面內容不重繪,避免打斷輸入
|
||||
if (first) renderPage();
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來,下一輪再試 */ }
|
||||
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來 */ }
|
||||
}
|
||||
|
||||
// ── 動作 ──
|
||||
$('btnSync').onclick = async () => { await go.SyncNow(); tick(); };
|
||||
|
||||
$('btnAddFolder').onclick = async () => {
|
||||
async function addFolder() {
|
||||
if (!state || !state.accounts.length) { showConnect(); return; }
|
||||
const p = await go.PickFolder(); // 系統原生面板(macOS powerbox 會授權)
|
||||
const p = await go.PickFolder();
|
||||
if (!p) return;
|
||||
await go.AddFolder(0, p);
|
||||
tick();
|
||||
};
|
||||
state = await go.GetState(); renderPage();
|
||||
}
|
||||
|
||||
function confirmRemove(accIdx, path) {
|
||||
openSheet(`
|
||||
<h2>移除這個資料夾?</h2>
|
||||
<p>「${esc(path)}」不再自動同步。<br/>已經上傳的知識卡不會被刪除。</p>
|
||||
<div class="acts">
|
||||
<button id="c1">取消</button>
|
||||
<button class="primary" id="c2">移除</button>
|
||||
</div>`, () => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => { await go.RemoveFolder(accIdx, path); closeSheet(); tick(); };
|
||||
});
|
||||
<div class="acts"><button id="c1">取消</button><button class="primary" id="c2">移除</button></div>`,
|
||||
() => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
await go.RemoveFolder(accIdx, path); closeSheet();
|
||||
state = await go.GetState(); renderPage();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function showConnect() {
|
||||
openSheet(`
|
||||
<h2>連上你的知識庫</h2>
|
||||
<p>貼上你的知識庫網址,再輸入你在網站上設定的帳號密碼。</p>
|
||||
<label>知識庫網址</label>
|
||||
<input type="text" id="u" placeholder="https://arcrun-cypher-executor.xxxx.workers.dev"/>
|
||||
<label>帳號(Email)</label>
|
||||
<input type="text" id="e" placeholder="you@example.com"/>
|
||||
<label>密碼</label>
|
||||
<input type="password" id="p"/>
|
||||
<div class="field"><div class="lb">知識庫網址</div>
|
||||
<input type="text" id="u" placeholder="https://arcrun-cypher-executor.xxxx.workers.dev"/></div>
|
||||
<div class="field"><div class="lb">帳號(Email)</div>
|
||||
<input type="text" id="e" placeholder="you@example.com"/></div>
|
||||
<div class="field"><div class="lb">密碼</div><input type="password" id="p"/></div>
|
||||
<div class="err" id="err" style="display:none"></div>
|
||||
<div class="acts">
|
||||
<button id="c1">取消</button>
|
||||
<button class="primary" id="c2">連線</button>
|
||||
</div>`, () => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
const err = $('err');
|
||||
try {
|
||||
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
|
||||
closeSheet(); tick();
|
||||
} catch (ex) {
|
||||
err.textContent = String(ex); err.style.display = 'block';
|
||||
}
|
||||
};
|
||||
});
|
||||
<div class="acts"><button id="c1">取消</button><button class="primary" id="c2">連線</button></div>`,
|
||||
() => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
try {
|
||||
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
|
||||
closeSheet(); state = await go.GetState(); renderPage();
|
||||
} catch (ex) { $('err').textContent = String(ex); $('err').style.display = 'block'; }
|
||||
};
|
||||
});
|
||||
}
|
||||
$('btnAddAccount').onclick = showConnect;
|
||||
|
||||
$('btnAI').onclick = () => {
|
||||
const gem = state && state.engine === 'gemma';
|
||||
openSheet(`
|
||||
<h2>用哪個 AI 幫你整理文件?</h2>
|
||||
<label class="radio"><input type="radio" name="ai" value="cloud" ${gem ? '' : 'checked'}/>
|
||||
<span><b>雲端 AI</b>(推薦・不必申請任何金鑰)<br/>
|
||||
<span class="muted">用你自己 Cloudflare 帳號內建的 AI,不需要任何金鑰。</span></span></label>
|
||||
<label class="radio"><input type="radio" name="ai" value="gemini" ${gem ? 'checked' : ''}/>
|
||||
<span><b>Google Gemini</b>(需要自己申請金鑰)</span></label>
|
||||
<label>Gemini 金鑰${state && state.geminiKey ? '(目前已設定,清空即可刪除)' : ''}</label>
|
||||
<input type="text" id="k" placeholder="貼上你的 Gemini API Key"/>
|
||||
<div class="err" id="err" style="display:none"></div>
|
||||
<div class="acts">
|
||||
<button id="c1">取消</button>
|
||||
<button class="primary" id="c2">儲存</button>
|
||||
</div>`, () => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
const useGemini = document.querySelector('input[name=ai]:checked').value === 'gemini';
|
||||
const err = $('err');
|
||||
try {
|
||||
await go.SetAI(useGemini, $('k').value.trim());
|
||||
closeSheet(); tick();
|
||||
} catch (ex) { err.textContent = String(ex); err.style.display = 'block'; }
|
||||
};
|
||||
});
|
||||
};
|
||||
async function saveAI() {
|
||||
const useGemini = document.querySelector('input[name=ai]:checked').value === 'gemini';
|
||||
try {
|
||||
await go.SetAI(useGemini, $('k').value.trim());
|
||||
state = await go.GetState(); renderPage();
|
||||
} catch (ex) { $('aiErr').textContent = String(ex); $('aiErr').style.display = 'block'; }
|
||||
}
|
||||
|
||||
$('btnUpdate').onclick = () => go.OpenURL('https://rag.arcrun.dev/docs/use/update/');
|
||||
// 更新:**在 App 裡完成**(leo 指定),不是丟去 docs
|
||||
async function checkUpdate() {
|
||||
updateInfo = { latest: '查詢中…' }; renderPage();
|
||||
try { updateInfo = await go.CheckUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; }
|
||||
renderPage();
|
||||
}
|
||||
async function downloadUpdate() {
|
||||
updateInfo = Object.assign({}, updateInfo, { notes: '下載中…請稍候' }); renderPage();
|
||||
try { updateInfo = await go.DownloadUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; }
|
||||
renderPage();
|
||||
}
|
||||
async function applyUpdate() {
|
||||
try { await go.ApplyUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; renderPage(); }
|
||||
}
|
||||
|
||||
tick();
|
||||
setInterval(tick, 1000); // 每秒刷新 ⇒ 同步中看得到在動
|
||||
setInterval(tick, 1000);
|
||||
|
||||
@@ -1,72 +1,62 @@
|
||||
/* Arcrun 桌面 App — 樣式(t193)
|
||||
/* Arcrun 桌面 App — **版面**樣式(t193)
|
||||
*
|
||||
* 🔴 leo 2026-08-04:「我的要求是**符合 CIS**,在風格上**跟 portal 一樣**」
|
||||
* 「CIS 已經提供規範,你做的**連 Logo 都沒放上去**,這跟美不美有關係嗎?
|
||||
* 要求放進 CIS 是**硬要求**,你做為檢查有嗎?沒有怎麼交貨?」
|
||||
* 🔴 色票/字體/紙張紋理**不在這裡**——那些在 `arcrun-cis.css`(唯一真相源
|
||||
* = InkStoneCo/arcrun-cis/css/arcrun-cis.css,portal/landing/install 共用同一份)。
|
||||
* leo 08-04:「不會這裡又寫了一個新的 CSS?前面剛剛才說過**不要創造新的**?」
|
||||
* ⇒ 本檔**只准出現版面與元件**,一律用 var(--...),**不准寫任何 hex 色碼**。
|
||||
*
|
||||
* ⇒ 本檔的色票/字體/背景紋理**逐字取自 portal**
|
||||
* (matrix/arcrun:console-ui/public/portal/index.html 的 :root 區塊),
|
||||
* **不自創任何顏色**。要改就去改 portal 那份、再同步過來。
|
||||
*
|
||||
* 這也是換掉 fyne 的理由:fyne 全自繪、CSS 套不進去,
|
||||
* 換成 Wails(WebView)後 daemon 與 portal 才可能是**同一套視覺**。
|
||||
* 版面(leo 08-04 指定,附 Google Drive 截圖):
|
||||
* 「可以完全模仿 gdrive,**左方側邊欄,在右方替換頁面**,
|
||||
* 你現在做成在底下產生 nav bar,幾乎沒看過這種 nav bar 在下方的,會令人十分不習慣」
|
||||
*/
|
||||
|
||||
: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);
|
||||
}
|
||||
html, body { overflow: hidden; height: 100%; }
|
||||
|
||||
/* 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; height: 100vh; }
|
||||
|
||||
/* ── 版面:上(品牌+狀態)/中(可捲清單)/下(動作列)── */
|
||||
#app { display: flex; flex-direction: column; height: 100vh; }
|
||||
#side {
|
||||
width: 232px; flex: none;
|
||||
display: flex; flex-direction: column;
|
||||
padding: 22px 0 14px;
|
||||
border-right: 1px solid rgba(var(--ink-rgb), .1);
|
||||
}
|
||||
/* 🔴 lockup 底色(實測發現):`arcrun-lockup-h-paper-on-ink.png` **自帶不透明 Ink 底板**
|
||||
(左上像素 RGBA=23,24,26,255),它的設計前提是「貼在純 Ink 表面上」。
|
||||
我們的背景有紙張紋理 ⇒ 直接放會看到一個突兀的方塊(leo 截圖裡那個白/黑框)。
|
||||
⇒ 深色模式下把品牌區做成純 Ink 表面,與該圖的底板無縫接合。
|
||||
淺色模式用 `-ink.png`(左上像素 alpha=0,透明)⇒ 不需要處理。 */
|
||||
#side .brand { padding: 14px 22px 18px; }
|
||||
#side .brand img { height: 22px; display: block; }
|
||||
:root[data-theme="dark"] #side .brand { background: var(--ink); }
|
||||
#side .brand .wm-paper { display: none; }
|
||||
:root[data-theme="dark"] #side .brand .wm-ink { display: none; }
|
||||
:root[data-theme="dark"] #side .brand .wm-paper { display: block; }
|
||||
|
||||
/* 側邊欄項目:選中用 Relation(CIS 唯一強調色,≤5% 畫面 ⇒ 一次只有一項) */
|
||||
#side nav { display: flex; flex-direction: column; gap: 2px; padding: 0 12px; }
|
||||
#side .nav {
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
padding: 10px 14px; border-radius: 999px;
|
||||
font-size: 14.5px; color: rgba(var(--ink-rgb), .62);
|
||||
cursor: pointer; user-select: none;
|
||||
}
|
||||
#side .nav:hover { background: var(--well); color: var(--ink); }
|
||||
#side .nav.on { background: var(--amber); color: var(--knob); font-weight: 600; }
|
||||
#side .nav .ic { width: 18px; text-align: center; font-size: 15px; opacity: .9; }
|
||||
#side .side-foot { margin-top: auto; padding: 12px 22px 0; }
|
||||
#side .ver { font-size: 12px; color: rgba(var(--ink-rgb), .35); font-family: ui-monospace, Menlo, monospace; }
|
||||
|
||||
#content { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
|
||||
header {
|
||||
padding: 18px 24px 14px;
|
||||
padding: 22px 28px 16px;
|
||||
display: flex; align-items: flex-start; gap: 18px;
|
||||
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); }
|
||||
.status .big { font-size: 18px; font-weight: 600; letter-spacing: .02em; }
|
||||
.status .sub { margin-top: 5px; 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;
|
||||
@@ -75,54 +65,63 @@ header {
|
||||
}
|
||||
@keyframes pulse { 0%,100% { opacity: .25 } 50% { opacity: 1 } }
|
||||
|
||||
/* 按鈕:Relation 是**唯一重點色**,CIS 規定 ≤ 一個畫面的 5% ⇒ 只有主要動作用它 */
|
||||
main { flex: 1; overflow-y: auto; padding: 20px 28px 28px; }
|
||||
|
||||
/* ── 元件 ── */
|
||||
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;
|
||||
padding: 8px 15px; 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 { background: var(--amber); border-color: var(--amber); color: var(--knob); 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 { border-color: transparent; color: rgba(var(--ink-rgb), .45); padding: 6px 9px; font-size: 13px; }
|
||||
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;
|
||||
.card {
|
||||
background: var(--well); border-radius: 12px;
|
||||
padding: 18px 20px; margin-bottom: 14px;
|
||||
}
|
||||
.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; }
|
||||
.card h3 { margin: 0 0 4px; font-size: 15px; }
|
||||
.card .d { font-size: 13.5px; color: rgba(var(--ink-rgb), .55); line-height: 1.8; }
|
||||
.card .row { display: flex; align-items: center; gap: 12px; }
|
||||
.card .row .g { flex: 1; min-width: 0; }
|
||||
|
||||
.acct { padding: 4px 2px 8px; display: flex; align-items: baseline; gap: 10px; }
|
||||
.acct .name { font-size: 14.5px; font-weight: 600; }
|
||||
.acct .host { font-size: 12px; color: rgba(var(--ink-rgb), .4); font-family: ui-monospace, Menlo, monospace; }
|
||||
|
||||
.folder {
|
||||
margin: 0 16px 6px; padding: 11px 14px;
|
||||
padding: 11px 15px; margin-bottom: 7px;
|
||||
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; }
|
||||
.big-num { font-size: 26px; font-weight: 600; letter-spacing: .01em; }
|
||||
.kv { display: flex; gap: 26px; margin-top: 4px; }
|
||||
.kv .k { font-size: 12.5px; color: rgba(var(--ink-rgb), .45); }
|
||||
|
||||
.empty { padding: 52px 24px; 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; }
|
||||
.empty .d { font-size: 14px; color: rgba(var(--ink-rgb), .55); line-height: 1.9; margin-bottom: 20px; }
|
||||
|
||||
footer {
|
||||
padding: 12px 24px; border-top: 1px solid rgba(var(--ink-rgb), .1);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
label.radio { display: flex; align-items: flex-start; gap: 10px; margin: 12px 0; font-size: 14px; cursor: pointer; }
|
||||
label.radio input { margin-top: 3px; }
|
||||
input[type=text], input[type=password] {
|
||||
width: 100%; box-sizing: border-box; padding: 9px 12px; font-size: 14px;
|
||||
border: 1px solid rgba(var(--ink-rgb), .18); border-radius: 8px;
|
||||
background: var(--paper-b); color: var(--ink);
|
||||
}
|
||||
footer .spacer { flex: 1; }
|
||||
footer .ver { font-size: 12.5px; color: rgba(var(--ink-rgb), .35); font-family: ui-monospace, Menlo, monospace; }
|
||||
.field { margin: 14px 0; }
|
||||
.field .lb { font-size: 13px; margin-bottom: 6px; color: rgba(var(--ink-rgb), .6); }
|
||||
.err { color: var(--err); font-size: 13px; margin-top: 10px; }
|
||||
.acts { display: flex; gap: 8px; margin-top: 18px; }
|
||||
|
||||
/* 對話框:**內嵌覆蓋層**,不是另開一個小 popup 視窗
|
||||
(leo:「每個功能都開一個小小的 popup 視窗,非常缺乏整體感」) */
|
||||
/* 覆蓋層:只給「確認刪除」這類必須打斷的動作,設定頁一律走右側換頁 */
|
||||
.overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.42);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@@ -130,21 +129,12 @@ footer .ver { font-size: 12.5px; color: rgba(var(--ink-rgb), .35); font-family:
|
||||
}
|
||||
.overlay.on { opacity: 1; pointer-events: auto; }
|
||||
.sheet {
|
||||
width: min(520px, calc(100vw - 64px));
|
||||
width: min(460px, calc(100vw - 64px));
|
||||
background: var(--paper-a); color: var(--ink);
|
||||
border: 1px solid rgba(var(--ink-rgb), .12);
|
||||
border-radius: 14px; padding: 24px;
|
||||
border-radius: 14px; padding: 22px 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; }
|
||||
.sheet h2 { margin: 0 0 6px; font-size: 16.5px; }
|
||||
.sheet p { margin: 0 0 4px; font-size: 14px; color: rgba(var(--ink-rgb), .55); line-height: 1.7; }
|
||||
.sheet .acts { justify-content: flex-end; }
|
||||
|
||||
Reference in New Issue
Block a user