t193 三修:每庫一頁+狀態時間軸+預設淺色(leo 六點回饋)
## ① logo 尺寸不對導致白邊
真兇:我寫死 height:22px **沒給 width:auto** ⇒ 容器一壓就變形、露出底板邊緣。
修:照 portal 側欄同一條 clamp(22px,5vw,28px) + width:auto + max-width:100%,
並依 CIS「clear space = one chevron height」在四邊留白。
實測:logo 底板與品牌區落差 **0**(原本是明顯方塊)。
## ② 深色跟網頁不同/「它有淺色佈景?」
是——**portal 預設淺色**,深色由使用者切換並存 localStorage。
我卻硬跟系統走 ⇒ 兩邊當然不同。
修:改成與 portal 同一套(預設淺色+切換鈕+localStorage),
按鈕規格也逐字對齊 portal 的 .btn/.btn3(padding/font-size/radius/border)。
實測淺色:品牌區(253,252,251)=Paper、側欄(242,241,237)=Canvas,與 CIS 一致。
## ③ 「開啟知識庫網頁」開到錯的庫
真兇:它在首頁、寫死取 accounts[0] ⇒ 有兩個庫必然開錯。
修:移到**各庫頁的上方**,開的就是那個庫。
## ④⑤ 30 個資料夾放不下/「加入資料夾」會加到哪個帳號?
修:**每個知識庫一個獨立分頁**(側邊欄動態列出,附資料夾數)。
「加入資料夾」在庫頁裡,**作用對象就是那個庫**,不會加錯。
## ⑥ 首頁要顯示 status(看守/發現變化/萃取/上傳)
修:首頁改成**狀態時間軸**四步,進行中那步的圓點會呼吸。
誠實邊界:collector 目前回報「整輪」而非逐檔階段,
所以同步中時後三步一起標進行中,**不假裝有更細的進度**。
兩支機械閘全過(check-cis.sh 14 項/check-render.sh 深淺色皆驗)。
style.css 仍是 0 個 hex(顏色全走 var)。
This commit is contained in:
@@ -138,6 +138,50 @@ type UIState struct {
|
||||
Engine string `json:"engine"` // "workers-ai" | "gemma"
|
||||
GeminiKey string `json:"geminiKey"` // 只回遮罩,不回真值
|
||||
ExtractedOK int `json:"extractedOK"` // 首頁「已整理幾份」
|
||||
Steps []Step `json:"steps"` // 首頁狀態時間軸(leo #6)
|
||||
}
|
||||
|
||||
// Step 是首頁狀態時間軸的一格。
|
||||
// 🔴 leo 08-04:「首頁要顯示的應該是 status,如果**看守、發現變化、萃取、上傳…
|
||||
// 不同 status 在哪裡顯示**?」
|
||||
// ⇒ 把一輪同步拆成四步,讓使用者看得到「現在走到哪」,而不是只有一句「看守中」。
|
||||
type Step struct {
|
||||
Title string `json:"title"`
|
||||
Meta string `json:"meta,omitempty"`
|
||||
State string `json:"state"` // "done"(已完成)|"now"(進行中)|""(還沒輪到)
|
||||
}
|
||||
|
||||
// buildSteps 依 status.json 與訊號檔推出四步的狀態。
|
||||
// 誠實邊界:collector 目前回報的是「整輪」而非逐檔階段,所以同步中時
|
||||
// 「發現變化→萃取→上傳」一起標進行中;不假裝有更細的進度。
|
||||
func buildSteps(s syncStatus, syncing bool) []Step {
|
||||
watch := Step{Title: "看守資料夾", Meta: "有變動就自動開始", State: "done"}
|
||||
if syncing {
|
||||
return []Step{
|
||||
watch,
|
||||
{Title: "發現變化", State: "done"},
|
||||
{Title: "用 AI 整理成知識卡", Meta: "進行中", State: "now"},
|
||||
{Title: "上傳到你的知識庫", State: ""},
|
||||
}
|
||||
}
|
||||
done := ""
|
||||
if s.ExtractedOK > 0 || s.LastSync != "" {
|
||||
done = "done"
|
||||
}
|
||||
meta := ""
|
||||
if s.ExtractedOK > 0 {
|
||||
meta = fmt.Sprintf("上一輪 %d 份", s.ExtractedOK)
|
||||
}
|
||||
up := Step{Title: "上傳到你的知識庫", Meta: meta, State: done}
|
||||
if s.ExtractFailed > 0 {
|
||||
up.Meta = fmt.Sprintf("%s · ⚠ %d 份失敗", meta, s.ExtractFailed)
|
||||
}
|
||||
return []Step{
|
||||
watch,
|
||||
{Title: "發現變化", Meta: "等待中", State: done},
|
||||
{Title: "用 AI 整理成知識卡", State: done},
|
||||
up,
|
||||
}
|
||||
}
|
||||
|
||||
// GetState 是前端每秒拉一次的單一入口。
|
||||
@@ -164,6 +208,7 @@ func (a *App) GetState() UIState {
|
||||
sync := loadSyncStatus()
|
||||
st.ExtractedOK = sync.ExtractedOK
|
||||
st.Syncing, st.StatusBig, st.StatusSub = describeStatus(sync)
|
||||
st.Steps = buildSteps(sync, st.Syncing)
|
||||
return st
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,8 @@
|
||||
<img class="wm-paper" src="./src/assets/arcrun-lockup-h-paper-on-ink.png" alt="Arcrun"/>
|
||||
</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>
|
||||
<!-- 側邊欄由 JS 依帳號數動態產生(每個知識庫一頁,leo 08-04 指定) -->
|
||||
<nav id="nav"></nav>
|
||||
|
||||
<div class="side-foot">
|
||||
<div class="ver" id="ver"></div>
|
||||
@@ -38,6 +34,7 @@
|
||||
<div class="big" id="statusBig">啟動中…</div>
|
||||
<div class="sub" id="statusSub"></div>
|
||||
</div>
|
||||
<button id="themeBtn" title="切換淺色/深色">◐</button>
|
||||
<button class="primary" id="btnSync">立刻同步</button>
|
||||
</header>
|
||||
<main id="page"></main>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Arcrun 桌面 App — 前端(t193)
|
||||
//
|
||||
// 版面照 leo 08-04 指定(附 Google Drive 截圖):
|
||||
// 「可以完全模仿 gdrive,**左方側邊欄,在右方替換頁面**」
|
||||
// ⇒ 設定不再開 popup,全部是右側的頁;只有「確認刪除」才用覆蓋層。
|
||||
// 版面(leo 08-04 指定,附 Google Drive 截圖):左側邊欄、右側換頁。
|
||||
// 🔴 08-04 二輪回饋(本次處理):
|
||||
// ③「開啟知識庫網頁」不該在首頁(會開到錯的庫)⇒ 移到**各庫頁的上方**
|
||||
// ④ 一個庫 30 個資料夾放不下 ⇒ **每個知識庫一個獨立分頁**
|
||||
// ⑤「加入資料夾」會加到哪個帳號?⇒ 在庫頁裡加,**作用對象就是那個庫**,不會加錯
|
||||
// ⑥ 首頁要顯示 status:看守/發現變化/萃取/上傳 ⇒ **狀態時間軸**
|
||||
import './arcrun-cis.css'; // 共用底層(色票/字體/紋理)——唯一真相源在 arcrun-cis/css/
|
||||
import './style.css'; // 本 App 的版面
|
||||
|
||||
@@ -11,73 +14,96 @@ const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
// 跟隨系統深淺色(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();
|
||||
// 主題:**與 portal 同一套規則**——預設淺色,使用者切換後存 localStorage
|
||||
// (leo 問「它有淺色佈景?」⇒ 是,portal 預設就是淺色)
|
||||
const THEME_KEY = 'arcrun_app_theme';
|
||||
function applyTheme(t) {
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
try { localStorage.setItem(THEME_KEY, t); } catch (e) {}
|
||||
const b = $('themeBtn'); if (b) b.textContent = t === 'dark' ? '☀' : '☾';
|
||||
}
|
||||
applyTheme((() => { try { return localStorage.getItem(THEME_KEY) === 'dark' ? 'dark' : 'light'; } catch (e) { return 'light'; } })());
|
||||
$('themeBtn').onclick = () =>
|
||||
applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
||||
|
||||
let state = null;
|
||||
let page = 'home';
|
||||
let updateInfo = null; // { current, latest, available, notes, staged }
|
||||
let page = 'home'; // 'home' | 'ai' | 'update' | 'lib:<idx>'
|
||||
let updateInfo = null;
|
||||
|
||||
// ── 覆蓋層(只給必須打斷的確認)──
|
||||
function openSheet(html, wire) {
|
||||
$('sheet').innerHTML = html; $('overlay').classList.add('on'); if (wire) wire();
|
||||
}
|
||||
function openSheet(html, 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(); });
|
||||
|
||||
// ── 側邊欄換頁 ──
|
||||
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();
|
||||
};
|
||||
});
|
||||
// ── 側邊欄:每個知識庫一項(leo:「每個帳號有獨立的一個頁面」)──
|
||||
function renderNav() {
|
||||
const accs = (state && state.accounts) || [];
|
||||
$('nav').innerHTML = `
|
||||
<div class="nav" data-p="home"><span class="ic">◫</span><span class="nm">首頁</span></div>
|
||||
${accs.length ? `<div class="sec">知識庫</div>` : ''}
|
||||
${accs.map((a, i) => `
|
||||
<div class="nav" data-p="lib:${i}">
|
||||
<span class="ic">▤</span><span class="nm">${esc(a.name)}</span>
|
||||
<span class="cnt">${(a.folders || []).length}</span>
|
||||
</div>`).join('')}
|
||||
<div class="sec">設定</div>
|
||||
<div class="nav" data-p="ai"><span class="ic">✧</span><span class="nm">AI 設定</span></div>
|
||||
<div class="nav" data-p="update"><span class="ic">↧</span><span class="nm">版本與更新</span></div>`;
|
||||
$('nav').querySelectorAll('.nav').forEach((el) => {
|
||||
el.classList.toggle('on', el.dataset.p === page);
|
||||
el.onclick = () => { page = el.dataset.p; renderNav(); renderPage(); };
|
||||
});
|
||||
}
|
||||
|
||||
// ── 各頁 ──
|
||||
// ── 首頁:狀態時間軸(leo:「看守、發現變化、萃取、上傳… 不同 status 在哪裡顯示?」)──
|
||||
function pageHome(s) {
|
||||
const folders = (s.accounts || []).reduce((n, a) => n + (a.folders || []).length, 0);
|
||||
if (!s.accounts || !s.accounts.length) return onboarding();
|
||||
const st = s.steps || [];
|
||||
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>
|
||||
<h3>現在的狀態</h3>
|
||||
<div class="steps">
|
||||
${st.map((x) => `
|
||||
<div class="step ${esc(x.state)}">
|
||||
<span class="dot"></span>
|
||||
<span class="t">${esc(x.title)}</span>
|
||||
<span class="m">${esc(x.meta || '')}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>快速動作</h3>
|
||||
<div class="acts">
|
||||
<button id="qAdd">加入資料夾</button>
|
||||
<button id="qPortal">開啟知識庫網頁</button>
|
||||
<button id="qDocs">使用說明</button>
|
||||
<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">${s.accounts.reduce((n,a)=>n+(a.folders||[]).length,0)}</div><div class="k">個資料夾在看守</div></div>
|
||||
<div><div class="big-num">${s.extractedOK || 0}</div><div class="k">份已整理</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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>
|
||||
// ── 各庫頁:動作全部作用在這個庫(不會加錯帳號)──
|
||||
function pageLib(s, idx) {
|
||||
const a = s.accounts[idx];
|
||||
if (!a) return `<div class="empty"><div class="t">找不到這個知識庫</div></div>`;
|
||||
const portal = 'https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/';
|
||||
return `
|
||||
<div class="libhead">
|
||||
<div class="g">
|
||||
<div class="nm">${esc(a.name)}</div>
|
||||
<div class="host">${esc(a.host)}</div>
|
||||
</div>
|
||||
<button data-portal="${esc(portal)}">開啟知識庫網頁</button>
|
||||
<button class="primary" data-addto="${idx}">加入資料夾</button>
|
||||
</div>
|
||||
${(a.folders || []).map((f) => `
|
||||
<div class="folder">
|
||||
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
|
||||
<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="acts">
|
||||
<button class="primary" id="fAdd">加入資料夾</button>
|
||||
<button id="fAcct">新增知識庫帳號</button>
|
||||
</div>`;
|
||||
|| `<div class="empty"><div class="t">這個知識庫還沒有資料夾</div>
|
||||
<div class="d">按右上的「加入資料夾」,選一個要自動整理的資料夾。</div></div>`}`;
|
||||
}
|
||||
|
||||
function pageAI(s) {
|
||||
@@ -100,11 +126,9 @@ function pageAI(s) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// leo:「檢查更新連到 docs 的檢查更新,不正確,**是直接在這裡進行檢查更新**,
|
||||
// 應該是跳到一個頁面顯示**現在版本與最新版本**,然後**下載可以進行安裝**」
|
||||
function pageUpdate(s) {
|
||||
const u = updateInfo;
|
||||
const latest = u ? (u.latest || '查詢中…') : '查詢中…';
|
||||
const latest = u ? (u.latest || '查詢中…') : '按「檢查更新」查詢';
|
||||
let action = `<button class="primary" id="uCheck">檢查更新</button>`;
|
||||
let note = '';
|
||||
if (u && u.staged) {
|
||||
@@ -149,32 +173,30 @@ function onboarding() {
|
||||
</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);
|
||||
let html;
|
||||
if (page.startsWith('lib:')) html = pageLib(state, Number(page.slice(4)));
|
||||
else if (page === 'ai') html = pageAI(state);
|
||||
else if (page === 'update') html = pageUpdate(state);
|
||||
else html = pageHome(state);
|
||||
// 每個庫頁底下都給「新增知識庫帳號」入口
|
||||
if (page === 'home' && state.accounts && state.accounts.length) {
|
||||
html += `<div class="acts"><button id="hAcct">新增知識庫帳號</button></div>`;
|
||||
}
|
||||
$('page').innerHTML = html;
|
||||
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('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/'));
|
||||
on('hAcct', showConnect); on('obConnect', showConnect);
|
||||
on('obInstall', () => go.OpenURL('https://install.arcrun.dev/'));
|
||||
on('aiSave', saveAI);
|
||||
on('uCheck', checkUpdate);
|
||||
on('uDownload', downloadUpdate);
|
||||
on('uApply', applyUpdate);
|
||||
on('uCheck', checkUpdate); on('uDownload', downloadUpdate); on('uApply', applyUpdate);
|
||||
document.querySelectorAll('[data-portal]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.portal); });
|
||||
document.querySelectorAll('[data-addto]').forEach((b) => { b.onclick = () => addFolder(Number(b.dataset.addto)); });
|
||||
document.querySelectorAll('[data-rm]').forEach((b) => {
|
||||
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
|
||||
});
|
||||
@@ -182,28 +204,27 @@ function wire() {
|
||||
|
||||
function render(s) {
|
||||
const first = !state;
|
||||
const navChanged = state && JSON.stringify((state.accounts||[]).map(a=>[a.name,(a.folders||[]).length]))
|
||||
!== JSON.stringify((s.accounts||[]).map(a=>[a.name,(a.folders||[]).length]));
|
||||
state = s;
|
||||
$('ver').textContent = s.version ? s.version : '';
|
||||
$('ver').textContent = s.version || '';
|
||||
$('statusBig').textContent = s.statusBig;
|
||||
$('statusBig').classList.toggle('syncing', !!s.syncing);
|
||||
$('statusSub').textContent = s.statusSub;
|
||||
// 只有狀態列每秒更新;頁面內容不重繪,避免打斷輸入
|
||||
if (first) renderPage();
|
||||
if (first || navChanged) { renderNav(); renderPage(); }
|
||||
else if (page === 'home') renderPage(); // 首頁的狀態時間軸要跟著跳
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來 */ }
|
||||
}
|
||||
async function tick() { try { render(await go.GetState()); } catch (e) {} }
|
||||
|
||||
// ── 動作 ──
|
||||
$('btnSync').onclick = async () => { await go.SyncNow(); tick(); };
|
||||
|
||||
async function addFolder() {
|
||||
if (!state || !state.accounts.length) { showConnect(); return; }
|
||||
async function addFolder(accIdx) {
|
||||
const p = await go.PickFolder();
|
||||
if (!p) return;
|
||||
await go.AddFolder(0, p);
|
||||
state = await go.GetState(); renderPage();
|
||||
await go.AddFolder(accIdx, p);
|
||||
state = await go.GetState(); renderNav(); renderPage();
|
||||
}
|
||||
|
||||
function confirmRemove(accIdx, path) {
|
||||
@@ -215,7 +236,7 @@ function confirmRemove(accIdx, path) {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
await go.RemoveFolder(accIdx, path); closeSheet();
|
||||
state = await go.GetState(); renderPage();
|
||||
state = await go.GetState(); renderNav(); renderPage();
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -236,7 +257,7 @@ function showConnect() {
|
||||
$('c2').onclick = async () => {
|
||||
try {
|
||||
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
|
||||
closeSheet(); state = await go.GetState(); renderPage();
|
||||
closeSheet(); state = await go.GetState(); renderNav(); renderPage();
|
||||
} catch (ex) { $('err').textContent = String(ex); $('err').style.display = 'block'; }
|
||||
};
|
||||
});
|
||||
@@ -250,7 +271,6 @@ async function saveAI() {
|
||||
} catch (ex) { $('aiErr').textContent = String(ex); $('aiErr').style.display = 'block'; }
|
||||
}
|
||||
|
||||
// 更新:**在 App 裡完成**(leo 指定),不是丟去 docs
|
||||
async function checkUpdate() {
|
||||
updateInfo = { latest: '查詢中…' }; renderPage();
|
||||
try { updateInfo = await go.CheckUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; }
|
||||
|
||||
@@ -21,14 +21,21 @@ html, body { overflow: hidden; height: 100%; }
|
||||
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); }
|
||||
/* 🔴 lockup(CIS 硬要求,leo 兩次點名):
|
||||
① 尺寸照 portal 側欄同一條:clamp(22px,5vw,28px) + **width:auto**
|
||||
(只寫 height 不寫 width,容器一壓就變形/露出底板邊緣=leo 看到的白邊)
|
||||
② `-paper-on-ink.png` **自帶不透明 Ink 底板**(實測左上像素 RGBA=23,24,26,255),
|
||||
設計前提是貼在**純 Ink 表面** ⇒ 深色模式把品牌區做成純 Ink,並留 clear space
|
||||
(CIS:clear space = one chevron height on all four sides)。
|
||||
③ 淺色的 `-ink.png` 是透明的,不需要底板。 */
|
||||
#side .brand { padding: 16px 22px 18px; }
|
||||
#side .brand img {
|
||||
height: clamp(22px, 5vw, 28px); width: auto; max-width: 100%; display: block;
|
||||
}
|
||||
:root[data-theme="dark"] #side .brand {
|
||||
background: var(--ink);
|
||||
padding: 16px 22px 18px; /* 上下左右都留白=clear space,底板不會貼邊 */
|
||||
}
|
||||
#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; }
|
||||
@@ -67,19 +74,32 @@ header {
|
||||
|
||||
main { flex: 1; overflow-y: auto; padding: 20px 28px 28px; }
|
||||
|
||||
/* ── 元件 ── */
|
||||
/* ── 元件:規格**逐字對齊 portal**(leo:「網頁的按鈕比較精緻,跟現有的差異較大」)──
|
||||
portal: .btn { padding:13px; font-size:16px; font-weight:600; border-radius:10px; border:none; background:var(--amber) }
|
||||
.btn3 { padding:11px 16px; font-size:15px; border-radius:10px; border:1px solid rgba(ink,.25); background:none }
|
||||
這裡只把 padding 依桌面視窗略收(網頁是手機優先、桌面不需要那麼大的點擊區)。 */
|
||||
button {
|
||||
border: 1px solid rgba(var(--ink-rgb), .16);
|
||||
background: transparent; color: var(--ink);
|
||||
padding: 8px 15px; border-radius: 8px; font-size: 14px; cursor: pointer;
|
||||
transition: background .12s, border-color .12s;
|
||||
padding: 10px 16px; font-size: 14.5px; border-radius: 10px;
|
||||
border: 1px solid rgba(var(--ink-rgb), .25);
|
||||
background: none; color: rgba(var(--ink-rgb), .7);
|
||||
cursor: pointer; transition: background .12s, color .12s, border-color .12s;
|
||||
}
|
||||
button:hover { background: var(--well); color: var(--ink); }
|
||||
button:disabled { opacity: .45; cursor: default; }
|
||||
button.primary {
|
||||
border: none; background: var(--amber); color: var(--knob); font-weight: 600;
|
||||
}
|
||||
button.primary:hover { filter: brightness(1.06); background: var(--amber); }
|
||||
button.ghost {
|
||||
border-color: transparent; background: none;
|
||||
color: rgba(var(--ink-rgb), .45); padding: 7px 10px; font-size: 13px;
|
||||
}
|
||||
button:hover { background: var(--well); }
|
||||
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), .45); padding: 6px 9px; font-size: 13px; }
|
||||
button.ghost:hover { color: var(--err); background: transparent; }
|
||||
|
||||
/* 主題切換(portal 有,daemon 也要有——leo:「它有淺色佈景?」
|
||||
⇒ 是,portal **預設淺色**,深色由使用者切換並存 localStorage。 */
|
||||
#themeBtn { padding: 7px 11px; font-size: 13px; }
|
||||
|
||||
.card {
|
||||
background: var(--well); border-radius: 12px;
|
||||
padding: 18px 20px; margin-bottom: 14px;
|
||||
@@ -138,3 +158,45 @@ input[type=text], input[type=password] {
|
||||
.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; }
|
||||
|
||||
/* ── 每個知識庫一個獨立分頁(leo 08-04)──
|
||||
「同步資料夾那個頁面如果有 2 個庫,每個庫有 30 個同步資料夾,**放得下嗎**?
|
||||
應該**一個庫有一個頁簽**或是其他方法可以切換。」
|
||||
「『加入資料夾』按下後**會加在哪個帳號**?**有必要每個帳號有獨立的一個頁面**。」
|
||||
⇒ 側邊欄直接列出每個知識庫;點進去就是那個庫的頁,動作全部作用在它身上,不會加錯。 */
|
||||
#side .sec {
|
||||
padding: 16px 26px 6px; font-size: 11.5px; letter-spacing: .08em;
|
||||
color: rgba(var(--ink-rgb), .35);
|
||||
}
|
||||
#side .nav .cnt {
|
||||
margin-left: auto; font-size: 11.5px; opacity: .7;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
}
|
||||
#side .nav .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* 每個庫的頁首:庫名+網址+「開啟知識庫網頁」(leo:不該放首頁,要在各庫上方) */
|
||||
.libhead {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding-bottom: 14px; margin-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(var(--ink-rgb), .1);
|
||||
}
|
||||
.libhead .g { flex: 1; min-width: 0; }
|
||||
.libhead .nm { font-size: 16px; font-weight: 600; }
|
||||
.libhead .host {
|
||||
font-size: 12px; color: rgba(var(--ink-rgb), .4);
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 狀態時間軸(leo:「如果看守、發現變化、萃取、上傳… 不同 status 在哪裡顯示?」) */
|
||||
.steps { display: flex; flex-direction: column; gap: 2px; margin-top: 12px; }
|
||||
.step { display: flex; align-items: center; gap: 11px; padding: 7px 0; font-size: 14px; }
|
||||
.step .dot {
|
||||
width: 9px; height: 9px; border-radius: 50%; flex: none;
|
||||
background: rgba(var(--ink-rgb), .18);
|
||||
}
|
||||
.step.done .dot { background: var(--ok); }
|
||||
.step.now .dot { background: var(--amber); animation: pulse 1.1s ease-in-out infinite; }
|
||||
.step.now { font-weight: 600; }
|
||||
.step .t { flex: 1; }
|
||||
.step .m { font-size: 12.5px; color: rgba(var(--ink-rgb), .45); }
|
||||
|
||||
Reference in New Issue
Block a user