multi-ccp 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -12
- package/LICENSE +21 -21
- package/README.md +306 -306
- package/README.zh-CN.md +306 -306
- package/dist/cli/program.js +38 -2
- package/dist/cli/program.js.map +1 -1
- package/dist/core/ccr.d.ts +25 -0
- package/dist/core/ccr.js +97 -3
- package/dist/core/ccr.js.map +1 -1
- package/dist/core/presets.d.ts +8 -0
- package/dist/core/presets.js +25 -12
- package/dist/core/presets.js.map +1 -1
- package/dist/web/assets/app.js +30 -19
- package/dist/web/assets/index.html +112 -111
- package/dist/web/assets/styles.css +1 -1
- package/dist/web/server.js +174 -21
- package/dist/web/server.js.map +1 -1
- package/package.json +46 -46
package/dist/web/assets/app.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const token = document.querySelector('meta[name="ccp-ui-token"]').content;
|
|
2
|
-
const state = { profiles: [], dashboard: null, selected: null, filter: 'all', query: '', view: 'cards', ccr: null, ccrRoutes: [], presets: [], selectedPreset: 'custom-api', lastPresetName: '', presetQuery: '', presetFilter: 'all' };
|
|
2
|
+
const state = { profiles: [], dashboard: null, selected: null, filter: 'all', query: '', view: 'cards', ccr: null, ccrRoutes: [], ccrRoutesReason: '', ccrRoutesMessage: '', presets: [], selectedPreset: 'custom-api', lastPresetName: '', presetQuery: '', presetFilter: 'all' };
|
|
3
3
|
const $ = (id) => document.getElementById(id);
|
|
4
|
-
const api = async (path, options = {}) => {
|
|
5
|
-
const res = await fetch(path, { ...options, headers: { 'content-type': 'application/json', 'x-ccp-ui-token': token, ...(options.headers || {}) } });
|
|
6
|
-
const data = await res.json().catch(() => ({}));
|
|
7
|
-
if (!res.ok) throw new Error(data.error || 'Request failed');
|
|
8
|
-
return data;
|
|
9
|
-
};
|
|
10
|
-
function escapeHtml(v){return String(v ?? '').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
|
4
|
+
const api = async (path, options = {}) => {
|
|
5
|
+
const res = await fetch(path, { ...options, headers: { 'content-type': 'application/json', 'x-ccp-ui-token': token, ...(options.headers || {}) } });
|
|
6
|
+
const data = await res.json().catch(() => ({}));
|
|
7
|
+
if (!res.ok) throw new Error(data.error || 'Request failed');
|
|
8
|
+
return data;
|
|
9
|
+
};
|
|
10
|
+
function escapeHtml(v){return String(v ?? '').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
|
11
11
|
function tagClass(tag){ if(['Ready','Running'].includes(tag)) return 'ready'; if(['Need Attention','Missing Token','Missing Base URL','CCR Offline','No Token'].includes(tag)) return 'warn'; if(['Invalid','Path Missing','Conflict'].includes(tag)) return 'bad'; return ''; }
|
|
12
12
|
function tags(items){ return `<div class="tag-row">${items.slice(0,3).map(t=>`<span class="tag ${tagClass(t)}">${escapeHtml(t)}</span>`).join('')}</div>`; }
|
|
13
13
|
function toast(message){ const el=document.createElement('div'); el.className='toast'; el.textContent=message; const openDialog=document.querySelector('dialog[open]'); let region=openDialog?.querySelector('.dialog-toast-region'); if(openDialog&&!region){ region=document.createElement('div'); region.className='dialog-toast-region'; (openDialog.querySelector('.modal-card')||openDialog).append(region); } (region || $('toastRegion')).append(el); setTimeout(()=>el.remove(),3600); }
|
|
@@ -15,7 +15,7 @@ function brief(profile){ if(profile.type==='api') return `<div><strong>Model</st
|
|
|
15
15
|
function hostname(url){ try{return new URL(url).hostname}catch{return url||''} }
|
|
16
16
|
function shortPath(p){ return String(p||'').replace(/^.*?\.claude-profiles/, '~/.claude-profiles').replace(/^.*?\.claude$/, '~/.claude'); }
|
|
17
17
|
function filtered(){ return state.profiles.filter(p=>{ const q=state.query.toLowerCase(); const hay=[p.name,p.type,p.model,p.baseUrl,p.statusText,...(p.tags||[])].join(' ').toLowerCase(); const okQ=!q||hay.includes(q); const okF=state.filter==='all'||p.type===state.filter||(state.filter==='attention'&&p.status!=='ready'); return okQ&&okF; }); }
|
|
18
|
-
function renderSummary(){ const d=state.dashboard?.profiles || {}; const c=state.dashboard?.ccr || {}; const metrics=[['Profiles',d.total??0,'all'],['API',d.api??0,'api'],['Login',d.login??0,'login'],['CCR',d.ccr??0,'ccr'],['Attention',d.needsAttention??0,'attention']]; $('summaryGrid').innerHTML = metrics.map(([label,val,kind])=>`<article class="metric ${kind}"><span>${label}</span><b>${val}</b></article>`).join('') + `<article class="metric ccr-status" role="button" id="ccrMetric"><span>Router</span><b>${c.running?'Running':'Offline'}</b></article>`; $('ccrMetric').onclick=openCcrPanel; }
|
|
18
|
+
function renderSummary(){ const d=state.dashboard?.profiles || {}; const c=state.dashboard?.ccr || {}; const metrics=[['Profiles',d.total??0,'all'],['API',d.api??0,'api'],['Login',d.login??0,'login'],['CCR',d.ccr??0,'ccr'],['Attention',d.needsAttention??0,'attention']]; $('summaryGrid').innerHTML = metrics.map(([label,val,kind])=>`<article class="metric ${kind}"><span>${label}</span><b>${val}</b></article>`).join('') + `<article class="metric ccr-status" role="button" id="ccrMetric"><span>Router</span><b>${escapeHtml(c.statusText || (c.running?'Running':'Offline'))}</b></article>`; $('ccrMetric').onclick=openCcrPanel; }
|
|
19
19
|
function iconSvg(name){
|
|
20
20
|
const icons={
|
|
21
21
|
home:'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 11.4 12 4l8 7.4v7.1a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 18.5v-7.1Z"/><path d="M9 20v-6h6v6"/></svg>',
|
|
@@ -34,25 +34,34 @@ function iconSvg(name){
|
|
|
34
34
|
function hydrateIcons(){ document.querySelectorAll('[data-icon]').forEach(el=>{ el.innerHTML=iconSvg(el.dataset.icon); }); }
|
|
35
35
|
function iconFor(type){ return iconSvg({main:'home',api:'key',login:'user',ccr:'route',unknown:'circleHelp'}[type]||'circleHelp'); }
|
|
36
36
|
function actionHint(p){ if(p.type==='api') return p.tokenStatus==='set'?'API key ready':'Needs token'; if(p.type==='ccr') return p.statusText; if(p.type==='login') return 'Login isolated'; if(p.type==='main') return 'Default config'; return p.statusText; }
|
|
37
|
-
function renderBoard(options={}){ const board=$('profileBoard'); const items=filtered(); if(!items.length){ board.innerHTML=`${boardToolbar()}<div class="empty-state"><p class="eyebrow">empty result</p><h2>没有匹配的 Profile</h2><p>调整搜索或筛选条件,或者创建一个新的 Profile。</p></div>`; bindBoardControls(board); restoreBoardFocus(options); return; } board.innerHTML=`${boardToolbar()}<div class="board-head"><div><p class="eyebrow">profiles</p><h2>${items.length} visible profiles</h2></div><p>${items.filter(p=>p.status!=='ready').length} need attention</p></div>${state.view==='cards'?renderCards(items):renderList(items)}`; bindBoardControls(board); restoreBoardFocus(options); board.querySelectorAll('[data-select]').forEach(el=>el.addEventListener('click',()=>selectProfile(el.dataset.select))); board.querySelectorAll('[data-
|
|
37
|
+
function renderBoard(options={}){ const board=$('profileBoard'); const items=filtered(); if(!items.length){ board.innerHTML=`${boardToolbar()}<div class="empty-state"><p class="eyebrow">empty result</p><h2>没有匹配的 Profile</h2><p>调整搜索或筛选条件,或者创建一个新的 Profile。</p></div>`; bindBoardControls(board); restoreBoardFocus(options); return; } board.innerHTML=`${boardToolbar()}<div class="board-head"><div><p class="eyebrow">profiles</p><h2>${items.length} visible profiles</h2></div><p>${items.filter(p=>p.status!=='ready').length} need attention</p></div>${state.view==='cards'?renderCards(items):renderList(items)}`; bindBoardControls(board); restoreBoardFocus(options); board.querySelectorAll('[data-select]').forEach(el=>el.addEventListener('click',()=>selectProfile(el.dataset.select))); board.querySelectorAll('[data-term]').forEach(el=>el.addEventListener('click',e=>{e.stopPropagation(); launchTerminal(el.dataset.term)})); }
|
|
38
38
|
function restoreBoardFocus(options){ if(!options.focusSearch) return; const input=$('profileBoard').querySelector('#searchInput'); if(!input) return; input.focus(); const pos=input.value.length; input.setSelectionRange(pos,pos); }
|
|
39
39
|
function boardToolbar(){ return `<div class="board-toolbar"><div class="board-tools-left"><div class="search-wrap"><span>${iconSvg('search')}</span><input id="searchInput" type="search" placeholder="搜索 profile、模型、endpoint..." value="${escapeHtml(state.query)}" /></div><div class="filters" id="typeFilters"><button class="chip ${state.filter==='all'?'active':''}" data-filter="all" type="button">All</button><button class="chip ${state.filter==='main'?'active':''}" data-filter="main" type="button">Main</button><button class="chip ${state.filter==='api'?'active':''}" data-filter="api" type="button">API</button><button class="chip ${state.filter==='login'?'active':''}" data-filter="login" type="button">Login</button><button class="chip ${state.filter==='ccr'?'active':''}" data-filter="ccr" type="button">CCR</button><button class="chip ${state.filter==='attention'?'active':''}" data-filter="attention" type="button">Attention</button></div></div><div class="board-tools-right"><button class="chip ${state.view==='cards'?'active':''}" id="cardViewBtn" type="button">Cards</button><button class="chip ${state.view==='list'?'active':''}" id="listViewBtn" type="button">List</button></div></div>`; }
|
|
40
40
|
function bindBoardControls(scope){ const search=scope.querySelector('#searchInput'); if(search) search.oninput=e=>{state.query=e.target.value;renderBoard({focusSearch:true});}; const filters=scope.querySelector('#typeFilters'); if(filters) filters.onclick=e=>{ if(!e.target.dataset.filter)return; state.filter=e.target.dataset.filter; renderBoard(); }; const card=scope.querySelector('#cardViewBtn'); if(card) card.onclick=()=>{state.view='cards';renderBoard();}; const list=scope.querySelector('#listViewBtn'); if(list) list.onclick=()=>{state.view='list';renderBoard();}; }
|
|
41
|
-
function renderCards(arr){ return `<div class="cards">${arr.map(p=>`<article class="profile-card ${state.selected===p.name?'selected':''}" data-select="${escapeHtml(p.name)}"><div class="card-top"><div class="profile-icon ${p.type}">${iconFor(p.type)}</div><div class="card-title"><h3>${escapeHtml(p.name)}</h3><p>${escapeHtml(actionHint(p))}</p></div></div>${tags(p.tags)}<div class="profile-meta">${brief(p)}</div><div class="card-actions"><button class="ghost tiny" data-
|
|
42
|
-
function renderList(arr){ return `<table class="list-table"><thead><tr><th>Name</th><th>Tags</th><th>Model / Route</th><th>Base / Path</th><th>Actions</th></tr></thead><tbody>${arr.map(p=>`<tr class="${state.selected===p.name?'selected':''}" data-select="${escapeHtml(p.name)}"><td><strong>${escapeHtml(p.name)}</strong></td><td>${tags(p.tags)}</td><td>${escapeHtml(p.model || p.meta?.ccrRoute || '—')}</td><td>${escapeHtml(hostname(p.baseUrl)||shortPath(p.dir))}</td><td><button class="ghost tiny" data-
|
|
41
|
+
function renderCards(arr){ return `<div class="cards">${arr.map(p=>`<article class="profile-card ${state.selected===p.name?'selected':''}" data-select="${escapeHtml(p.name)}"><div class="card-top"><div class="profile-icon ${p.type}">${iconFor(p.type)}</div><div class="card-title"><h3>${escapeHtml(p.name)}</h3><p>${escapeHtml(actionHint(p))}</p></div></div>${tags(p.tags)}<div class="profile-meta">${brief(p)}</div><div class="card-actions"><button class="ghost tiny" data-term="${escapeHtml(p.name)}">term ↗</button></div></article>`).join('')}</div>`; }
|
|
42
|
+
function renderList(arr){ return `<table class="list-table"><thead><tr><th>Name</th><th>Tags</th><th>Model / Route</th><th>Base / Path</th><th>Actions</th></tr></thead><tbody>${arr.map(p=>`<tr class="${state.selected===p.name?'selected':''}" data-select="${escapeHtml(p.name)}"><td><strong>${escapeHtml(p.name)}</strong></td><td>${tags(p.tags)}</td><td>${escapeHtml(p.model || p.meta?.ccrRoute || '—')}</td><td>${escapeHtml(hostname(p.baseUrl)||shortPath(p.dir))}</td><td><button class="ghost tiny" data-term="${escapeHtml(p.name)}">term ↗</button></td></tr>`).join('')}</tbody></table>`; }
|
|
43
43
|
async function selectProfile(name){ const data=await api(`/api/profiles/${encodeURIComponent(name)}`); if(data.profile?.type==='ccr') await loadRoutes(); state.selected=name; $('workspace').classList.add('drawer-open'); $('drawer').setAttribute('aria-hidden','false'); renderDrawer(data.profile); renderBoard(); }
|
|
44
|
-
function renderDrawer(p){ const env=p.settings?.env||{}; $('drawer').innerHTML=`<div class="drawer-rail"><button class="icon-btn" id="drawerClose" type="button" title="关闭">×</button></div><div class="drawer-fixed"><p class="eyebrow">${escapeHtml(p.type)} profile</p><h2>${escapeHtml(p.name)}</h2>${tags(p.tags)}<div class="drawer-section launch-section"><p class="eyebrow">launch</p><div class="command"><code>${escapeHtml(p.startCommand)}</code><button class="ghost tiny" id="copyStart">Copy</button></div></div></div><div class="drawer-scroll"><div class="profile-summary"><div class="drawer-section profile-info"><div class="kv"><span>Status</span><strong>${escapeHtml(p.statusText)}</strong><span>Path</span><strong>${escapeHtml(p.settingsPath)}</strong></div>${fullConfigBlock(p)}</div></div>${settingsForm(p,env)}<div class="drawer-section"><p class="eyebrow">sessions</p><button class="ghost" onclick="alert('Session Sync Workspace 将在下一阶段实现')">Use in Sync Workspace</button></div>${p.type!=='main'?`<div class="drawer-section"><p class="eyebrow">danger zone</p><p class="hint">删除操作不可撤销。请输入 profile 名称确认。</p><div class="danger-actions"><input id="deleteConfirm" placeholder="${escapeHtml(p.name)}"/><button class="ghost" id="deleteBtn">Delete Profile</button></div></div>`:''}</div>`; $('drawerClose').onclick=closeDrawer; $('copyStart').onclick=()=>copy(p.startCommand); const openCcr=$('openCcrUiFromDrawer'); if(openCcr) openCcr.onclick=e=>{e.preventDefault();openCcrUi();}; const save=$('saveSettings'); if(save) save.onclick=()=>saveProfile(p); const del=$('deleteBtn'); if(del) del.onclick=()=>deleteProfile(p.name); }
|
|
44
|
+
function renderDrawer(p){ const env=p.settings?.env||{}; $('drawer').innerHTML=`<div class="drawer-rail"><button class="icon-btn" id="drawerClose" type="button" title="关闭">×</button></div><div class="drawer-fixed"><p class="eyebrow">${escapeHtml(p.type)} profile</p><h2>${escapeHtml(p.name)}</h2>${tags(p.tags)}<div class="drawer-section launch-section"><p class="eyebrow">launch</p><div class="command"><code>${escapeHtml(p.startCommand)}</code><span class="command-actions"><button class="ghost tiny" id="copyStart">Copy</button><button class="ghost tiny" id="termStart">term ↗</button></span></div></div></div><div class="drawer-scroll"><div class="profile-summary"><div class="drawer-section profile-info"><div class="kv"><span>Status</span><strong>${escapeHtml(p.statusText)}</strong><span>Path</span><strong><button class="path-link" id="revealSettings" type="button" title="在文件管理器中显示">${escapeHtml(p.settingsPath)}</button></strong></div>${fullConfigBlock(p)}</div></div>${settingsForm(p,env)}<div class="drawer-section"><p class="eyebrow">sessions</p><button class="ghost" onclick="alert('Session Sync Workspace 将在下一阶段实现')">Use in Sync Workspace</button></div>${p.type!=='main'?`<div class="drawer-section"><p class="eyebrow">danger zone</p><p class="hint">删除操作不可撤销。请输入 profile 名称确认。</p><div class="danger-actions"><input id="deleteConfirm" placeholder="${escapeHtml(p.name)}"/><button class="ghost" id="deleteBtn">Delete Profile</button></div></div>`:''}</div>`; $('drawerClose').onclick=closeDrawer; $('copyStart').onclick=()=>copy(p.startCommand); $('termStart').onclick=()=>launchTerminal(p.name); $('revealSettings').onclick=()=>revealSettings(p.name); const openCcr=$('openCcrUiFromDrawer'); if(openCcr) openCcr.onclick=e=>{e.preventDefault();openCcrUi();}; const save=$('saveSettings'); if(save) save.onclick=()=>saveProfile(p); const del=$('deleteBtn'); if(del) del.onclick=()=>deleteProfile(p.name); }
|
|
45
45
|
function closeDrawer(){ state.selected=null; $('workspace').classList.remove('drawer-open'); $('drawer').setAttribute('aria-hidden','true'); $('drawer').innerHTML='<div class="drawer-rail"><button class="icon-btn" id="drawerClose" type="button" title="关闭">×</button></div><div class="empty-drawer"><p class="eyebrow">profile details</p><h2>选择一个 Profile</h2><p>点击左侧卡片后,详情和编辑面板会从右侧展开。</p></div>'; $('drawerClose').onclick=closeDrawer; renderBoard(); }
|
|
46
|
-
function ccrRouteOptions(selected=''){ const routes=state.ccrRoutes||[]; if(!routes.length) return
|
|
46
|
+
function ccrRouteOptions(selected=''){ const routes=state.ccrRoutes||[]; if(!routes.length) return `<option value="">${escapeHtml(state.ccrRoutesMessage || '没有可用 CCR 路由')}</option>`; const missing=selected&&!routes.includes(selected)?`<option value="" selected>当前路由不可用:${escapeHtml(selected)}</option>`:''; const placeholder=selected&&routes.includes(selected)?'<option value="">选择模型路由</option>':'<option value="" selected>选择模型路由</option>'; return [missing||placeholder,...routes.map(route=>`<option value="${escapeHtml(route)}" ${route===selected?'selected':''}>${escapeHtml(route)}</option>`)].join(''); }
|
|
47
47
|
function fullConfigBlock(p){ const config={settings:p.settings||{}, ...(p.meta?{ccp:p.meta}:{})}; return `<details class="preset-config drawer-config"><summary>完整配置</summary><pre>${escapeHtml(JSON.stringify(config,null,2))}</pre></details>`; }
|
|
48
48
|
function settingsForm(p,env){ if(p.type==='api') return `<div class="drawer-section"><p class="eyebrow">settings</p><label>Base URL<input id="baseUrl" value="${escapeHtml(env.ANTHROPIC_BASE_URL||'')}"></label><label>New Token<input id="token" type="password" placeholder="留空保持不变"></label><label>Model<input id="model" value="${escapeHtml(env.ANTHROPIC_MODEL||'')}"></label><label>Opus Model<input id="opusModel" value="${escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL||'')}"></label><label>Sonnet Model<input id="sonnetModel" value="${escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL||'')}"></label><label>Haiku Model<input id="haikuModel" value="${escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL||'')}"></label><label>Subagent Model<input id="subagentModel" value="${escapeHtml(env.CLAUDE_CODE_SUBAGENT_MODEL||'')}"></label><button class="primary" id="saveSettings">Save Settings</button></div>`; if(p.type==='ccr') return `<div class="drawer-section"><p class="eyebrow">ccr router</p><label>模型路由<select id="route" required>${ccrRouteOptions(p.meta?.ccrRoute||'')}</select></label><div class="kv"><span>Preset</span><strong>${escapeHtml(p.meta?.ccrPreset||p.name)}</strong><span>Endpoint</span><strong>${escapeHtml(env.ANTHROPIC_BASE_URL||p.baseUrl||'')}</strong></div><p class="hint">保存后 multi-ccp 会根据模型路由重新生成该 CCR preset。provider/model 请在 <a href="#" id="openCcrUiFromDrawer">CCR UI</a> 中管理。</p><button class="primary" id="saveSettings">Save Route</button></div>`; return `<div class="drawer-section"><p class="eyebrow">settings</p><p class="hint">该 Profile 当前以只读方式展示。</p></div>`; }
|
|
49
49
|
async function saveProfile(p){ const body=p.type==='ccr'?{kind:'ccr',route:$('route').value}:{kind:'api',baseUrl:$('baseUrl').value,token:$('token').value,model:$('model').value,opusModel:$('opusModel').value,sonnetModel:$('sonnetModel').value,haikuModel:$('haikuModel').value,subagentModel:$('subagentModel').value}; const data=await api(`/api/profiles/${encodeURIComponent(p.name)}`,{method:'PUT',body:JSON.stringify(body)}); toast('已保存'); await load(); renderDrawer(data.profile); }
|
|
50
50
|
async function deleteProfile(name){ if(($('deleteConfirm').value||'')!==name){toast('请输入完整 Profile 名称确认');return;} if(!confirm(`确认删除 profile "${name}"?此操作不可撤销。`)) return; await api(`/api/profiles/${encodeURIComponent(name)}`,{method:'DELETE',body:JSON.stringify({confirmName:name})}); state.selected=null; toast('已删除'); await load(); closeDrawer(); }
|
|
51
51
|
async function copy(text){ await navigator.clipboard.writeText(text); toast('已复制'); }
|
|
52
|
+
async function launchTerminal(name){ try{ await api(`/api/profiles/${encodeURIComponent(name)}/terminal`,{method:'POST'}); toast('已拉起终端'); }catch(err){ toast(err.message); } }
|
|
53
|
+
async function revealSettings(name){ try{ await api(`/api/profiles/${encodeURIComponent(name)}/reveal-settings`,{method:'POST'}); toast('已打开文件位置'); }catch(err){ toast(err.message); } }
|
|
52
54
|
async function openCcrUi(){ const data=state.ccr || await api('/api/ccr/status'); window.open(data.uiUrl||data.endpoint,'_blank'); }
|
|
53
|
-
|
|
55
|
+
function ccrChecklist(data,options={}){ const items=[['Installed',data.installed],['Config',data.configExists && data.hasProviders],...(options.hideRoutes?[]:[['Routes',Number(data.routeCount||0)>0]]),['Running',data.running]]; return `<div class="ccr-checklist">${items.map(([label,ok])=>`<div class="check ${ok?'ok':'warn'}"><span>${ok?'✓':'!'}</span><strong>${label}</strong></div>`).join('')}</div>`; }
|
|
56
|
+
function withBusyButton(buttonId, busyText, task){ const button=$(buttonId); if(!button) return task(); const prevText=button.textContent; button.disabled=true; button.dataset.busy='1'; button.textContent=busyText; return Promise.resolve().then(task).finally(()=>{ const next=$(buttonId); if(next){ next.disabled=false; delete next.dataset.busy; next.textContent=prevText; } }); }
|
|
57
|
+
async function installCcrFromUi(){ if(!confirm('Install CCR globally now? This runs: npm install -g @musistudio/claude-code-router')) return; try{ await withBusyButton('ccrInstall','Installing…',()=>api('/api/ccr/install',{method:'POST'})); toast('CCR 已安装'); await load(); await openCcrPanel(); }catch(err){ toast(err.message); } }
|
|
58
|
+
async function startCcrFromUi(){ try{ await api('/api/ccr/start',{method:'POST'}); toast('CCR 启动命令已发送'); await load(); await openCcrPanel(); }catch(err){ toast(err.message); } }
|
|
59
|
+
async function restartCcrFromUi(){ try{ await api('/api/ccr/restart',{method:'POST'}); toast('CCR 重启命令已发送'); await load(); await openCcrPanel(); }catch(err){ toast(err.message); } }
|
|
60
|
+
async function stopCcrFromUi(){ try{ await api('/api/ccr/stop',{method:'POST'}); toast('CCR 停止命令已发送'); await load(); await openCcrPanel(); }catch(err){ toast(err.message); } }
|
|
61
|
+
async function openCcrPanel(){ const data=await api('/api/ccr/status'); state.ccr=data; const primary=data.nextAction==='install'?'<button class="primary" id="ccrInstall">Install CCR</button>':data.nextAction==='start'?'<button class="primary" id="ccrStart">Start CCR</button>':`<button class="primary" id="ccrOpen">${data.nextAction==='configure'?'Open CCR Setup':'Open CCR UI'}</button>`; $('ccrPanel').innerHTML=`<div class="modal-head"><div><p class="eyebrow">claude code router</p><h2>CCR ${escapeHtml(data.statusText || 'Unknown')}</h2></div><button class="icon-btn" onclick="ccrDialog.close()">×</button></div><div class="drawer-section"><div class="kv"><span>Endpoint</span><strong>${escapeHtml(data.endpoint)}</strong><span>Routes</span><strong>${escapeHtml(data.routeCount||0)}</strong><span>Profiles</span><strong>${escapeHtml(data.profilesUsingCcr||0)}</strong></div>${ccrChecklist(data)}</div><p class="hint">CCR provider、model、route 配置请在 Claude Code Router UI 中修改。</p><menu class="modal-actions">${primary}<button class="ghost" id="ccrRestart">Restart</button><button class="ghost" id="ccrStop">Stop</button></menu>`; $('ccrDialog').showModal(); const install=$('ccrInstall'); if(install) install.onclick=installCcrFromUi; const start=$('ccrStart'); if(start) start.onclick=startCcrFromUi; const open=$('ccrOpen'); if(open) open.onclick=openCcrUi; $('ccrRestart').onclick=restartCcrFromUi; $('ccrStop').onclick=stopCcrFromUi; }
|
|
54
62
|
async function load(){ const [d,p]=await Promise.all([api('/api/dashboard'),api('/api/profiles')]); state.dashboard=d; state.profiles=p.profiles; renderSummary(); renderBoard(); }
|
|
55
63
|
async function loadPresets(){ if(state.presets.length) return; const data=await api('/api/presets'); state.presets=data.presets||[]; renderPresetPicker(); }
|
|
64
|
+
function presetHasProviderTemplate(preset){ return Boolean(preset?.type==='ccr' && preset.providerTemplate); }
|
|
56
65
|
function presetIcon(type){ return iconSvg(type==='ccr'||type==='manual-ccr'?'route':type==='login'?'user':'key'); }
|
|
57
66
|
function presetTypeLabel(type){ return type==='custom-api'?'API':type==='manual-ccr'?'CCR':String(type).toUpperCase(); }
|
|
58
67
|
function presetCategory(p){ return p.category || (p.type==='api'?'api':p.type==='ccr'?'ccr':p.type==='login'?'login':'custom'); }
|
|
@@ -61,9 +70,11 @@ function selectedPreset(){ return state.presets.find(p=>p.id===state.selectedPre
|
|
|
61
70
|
function renderPresetPicker(){ const list=$('presetList'); if(!list) return; const items=filteredPresets(); if(!state.presets.find(p=>p.id===state.selectedPreset) && state.presets[0]) state.selectedPreset=state.presets[0].id; list.innerHTML=items.length?items.map(p=>`<button type="button" class="preset-option ${p.id===state.selectedPreset?'active':''}" data-preset="${escapeHtml(p.id)}"><span class="preset-icon">${presetIcon(p.type)}</span><span><strong>${escapeHtml(p.label)}</strong><small>${escapeHtml(presetTypeLabel(p.type))} · ${escapeHtml(p.modelSummary||'')}</small></span></button>`).join(''):'<div class="preset-empty">没有匹配的预设</div>'; list.querySelectorAll('[data-preset]').forEach(btn=>btn.onclick=()=>selectPreset(btn.dataset.preset)); bindPresetControls(); renderPresetDetail(); }
|
|
62
71
|
function selectPreset(id){ state.selectedPreset=id; renderPresetPicker(); }
|
|
63
72
|
function bindPresetControls(){ const search=$('presetSearch'); if(search && !search.dataset.bound){ search.dataset.bound='1'; search.oninput=e=>{ state.presetQuery=e.target.value; renderPresetPicker(); const next=$('presetSearch'); if(next){ next.focus(); next.setSelectionRange(next.value.length,next.value.length); } }; } if(search) search.value=state.presetQuery; const filters=$('presetFilters'); if(filters && !filters.dataset.bound){ filters.dataset.bound='1'; filters.onclick=e=>{ const value=e.target.dataset.presetFilter; if(!value) return; state.presetFilter=value; renderPresetPicker(); }; } document.querySelectorAll('#presetFilters [data-preset-filter]').forEach(btn=>btn.classList.toggle('active',btn.dataset.presetFilter===state.presetFilter)); }
|
|
64
|
-
function
|
|
73
|
+
function ccrReadinessBlock(ccrUi='',preset=null){ const c=state.ccr; if(!c) return ''; const isTemplate=presetHasProviderTemplate(preset); const message=isTemplate?'该模板会自动写入所需 CCR provider/model;你只需要填写 Provider API Key。':state.ccrRoutesMessage || (c.ready?'CCR 已就绪':'请先完成 CCR 环境准备'); return `<div class="ccr-readiness"><p class="eyebrow">CCR readiness</p>${ccrChecklist(c,{hideRoutes:isTemplate})}<p class="hint">${escapeHtml(message)}</p><a class="ghost tiny" href="${escapeHtml(ccrUi)}" target="_blank" rel="noreferrer">CCR UI ↗</a><button class="ghost tiny" type="button" id="ccrRefreshRoutes">刷新路由</button></div>`; }
|
|
74
|
+
function renderPresetDetail(){ const preset=selectedPreset(); if(!preset) return; $('presetId').value=preset.id; $('newKind').value=preset.type; const name=$('newProfileName'); if(!name.value || state.lastPresetName===name.value) name.value=preset.defaultProfileName||''; state.lastPresetName=name.value; document.querySelectorAll('[data-kind-fields]').forEach(el=>{ const active=el.dataset.kindFields===preset.type; el.hidden=!active; el.querySelectorAll('input,select,textarea,button').forEach(field=>{ field.disabled=!active; }); }); const env=preset.env||{}; const rows=[]; const ccrUi=state.dashboard?.ccr?.uiUrl || 'http://127.0.0.1:3456/ui/'; if(env.ANTHROPIC_BASE_URL) rows.push(['Base URL',env.ANTHROPIC_BASE_URL]); if(preset.ccrPreset) rows.push(['CCR Preset',preset.ccrPreset]); if(preset.ccrRoute) rows.push(['CCR Route',preset.ccrRoute]); if(preset.providerTemplate){ rows.push(['Provider',preset.providerTemplate.name]); rows.push(['Endpoint',preset.providerTemplate.api_base_url]); } if(preset.modelSummary) rows.push(['Model',preset.modelSummary]); const fullConfig=JSON.stringify(preset.type==='api'?{env:{...env,ANTHROPIC_AUTH_TOKEN:'<API_KEY>'}}:preset.type==='ccr'?{ccr:{Providers:[{...(preset.providerTemplate||{}),api_key:'<PROVIDER_API_KEY>'}]},ccp:{type:'ccr',ccrPreset:preset.ccrPreset,ccrRoute:preset.ccrRoute},env:{ANTHROPIC_BASE_URL:`http://127.0.0.1:3456/preset/${preset.ccrPreset}`,ANTHROPIC_AUTH_TOKEN:'<CCR_TOKEN>',NO_PROXY:'127.0.0.1,localhost',DISABLE_TELEMETRY:'1',DISABLE_COST_WARNINGS:'1',API_TIMEOUT_MS:'600000'}}:{},null,2); $('presetSummary').innerHTML=`<p class="eyebrow">${escapeHtml(presetTypeLabel(preset.type))} preset</p><h3>${escapeHtml(preset.label)}</h3><p>${escapeHtml(preset.description||'')}</p>${preset.type==='ccr'||preset.type==='manual-ccr'?ccrReadinessBlock(ccrUi,preset):''}${rows.length?`<dl>${rows.map(([k,v])=>`<dt>${escapeHtml(k)}</dt><dd>${escapeHtml(v)}</dd>`).join('')}</dl>`:''}${fullConfig!=='{}'?`<details class="preset-config"><summary>完整配置</summary><pre>${escapeHtml(fullConfig)}</pre></details>`:''}`; const refresh=$('ccrRefreshRoutes'); if(refresh) refresh.onclick=()=>loadRoutes().then(()=>toast('CCR 路由已刷新')); }
|
|
65
75
|
function bind(){ hydrateIcons(); $('refreshBtn').onclick=()=>load().then(()=>toast('已刷新')); $('drawerClose').onclick=closeDrawer; document.querySelectorAll('[data-dialog-close]').forEach(btn=>btn.addEventListener('click',()=>{ resetNewProfileForm(); $(btn.dataset.dialogClose).close(); })); document.querySelectorAll('dialog').forEach(dialog=>dialog.addEventListener('click',event=>{ if(event.target!==dialog) return; if(dialog.id==='newProfileDialog') resetNewProfileForm(); dialog.close(); })); $('themeToggle').onclick=()=>{ const dark=document.documentElement.dataset.theme==='dark'; document.documentElement.dataset.theme=dark?'light':'dark'; localStorage.setItem('ccp-ui-theme',dark?'light':'dark'); $('themeToggle').innerHTML=dark?iconSvg('moon'):iconSvg('sun'); $('themeToggle').title=dark?'切换深色':'切换浅色'; $('themeToggle').setAttribute('aria-label',dark?'切换深色':'切换浅色'); }; const saved=localStorage.getItem('ccp-ui-theme')||'light'; document.documentElement.dataset.theme=saved; $('themeToggle').innerHTML=saved==='dark'?iconSvg('sun'):iconSvg('moon'); $('themeToggle').title=saved==='dark'?'切换浅色':'切换深色'; $('themeToggle').setAttribute('aria-label',saved==='dark'?'切换浅色':'切换深色'); $('newProfileBtn').onclick=async()=>{ resetNewProfileForm(); await Promise.all([loadRoutes(),loadPresets()]); renderPresetPicker(); $('newProfileDialog').showModal(); }; $('createProfileSubmit').onclick=createProfile; }
|
|
66
|
-
async function loadRoutes(){ try{ const data=await api('/api/ccr/routes'); state.ccrRoutes=data.routes||[]; const list=$('manualCcrRoute'); if(list) list.innerHTML=ccrRouteOptions(list.value); }catch{ state.ccrRoutes=[]; const list=$('manualCcrRoute'); if(list) list.innerHTML='<option value="">无法加载 CCR 路由</option>'; } }
|
|
76
|
+
async function loadRoutes(){ try{ const [status,data]=await Promise.all([api('/api/ccr/status'),api('/api/ccr/routes')]); state.ccr=status; state.ccrRoutes=data.routes||[]; state.ccrRoutesReason=data.reason||''; state.ccrRoutesMessage=data.message||''; const list=$('manualCcrRoute'); if(list) list.innerHTML=ccrRouteOptions(list.value); renderPresetDetail(); }catch(err){ state.ccrRoutes=[]; state.ccrRoutesReason='unknown'; state.ccrRoutesMessage=err.message; const list=$('manualCcrRoute'); if(list) list.innerHTML='<option value="">无法加载 CCR 路由</option>'; renderPresetDetail(); } }
|
|
67
77
|
function resetNewProfileForm(){ const formEl=$('newProfileForm'); if(!formEl) return; formEl.reset(); state.selectedPreset='custom-api'; state.lastPresetName=''; state.presetQuery=''; state.presetFilter='all'; if(state.presets.length) renderPresetPicker(); }
|
|
68
|
-
function
|
|
69
|
-
|
|
78
|
+
async function ccrCreateBlocked(kind,preset){ if(kind!=='ccr'&&kind!=='manual-ccr') return false; let c=state.ccr; try{ c=await api('/api/ccr/status'); state.ccr=c; }catch(err){ toast(err.message); return true; } if(!c.installed){ toast('请先安装 CCR'); openCcrPanel(); return true; } if(presetHasProviderTemplate(preset)) return false; if(!c.configExists || !c.hasProviders || !state.ccrRoutes.length){ toast(state.ccrRoutesMessage || '请先在 CCR UI 中配置 provider/model'); return true; } return false; }
|
|
79
|
+
async function createProfile(){ const formEl=$('newProfileForm'); if(!formEl.reportValidity()){ const invalid=formEl.querySelector(':invalid'); toast(invalid?.closest('label')?.textContent?.trim() ? `请检查:${invalid.closest('label').textContent.trim()}` : '请完善必填项'); invalid?.focus(); return; } const form=new FormData(formEl); const preset=selectedPreset(); const kind=preset?.type || form.get('kind'); if(await ccrCreateBlocked(kind,preset)) return; const raw=Object.fromEntries(form.entries()); let url='/api/profiles/preset'; let body={presetId:raw.presetId,name:raw.name,kind,token:raw.token}; if(kind==='custom-api'){ url='/api/profiles/api'; body={name:raw.name,baseUrl:raw.baseUrl,token:raw.customToken||'',model:raw.model||''}; } else if(kind==='manual-ccr'){ url='/api/profiles/ccr'; body={name:raw.name,presetName:raw.manualCcrPreset||raw.name,route:raw.route,token:raw.manualCcrToken||''}; } else if(kind==='login'){ url='/api/profiles/login'; body={name:raw.name}; } else if(kind==='ccr'){ body={presetId:raw.presetId,name:raw.name,kind:'ccr',token:raw.ccrToken||'',providerApiKey:raw.ccrProviderApiKey||''}; } api(url,{method:'POST',body:JSON.stringify(body)}).then(async()=>{ $('newProfileDialog').close(); resetNewProfileForm(); toast('Profile 已创建'); if(kind==='ccr'||kind==='manual-ccr'){ const current=await api('/api/ccr/status'); if(current.installed&&!current.running&¤t.routeCount>0&&confirm('CCR 尚未运行,是否立即启动?')) await api('/api/ccr/start',{method:'POST'}); } await load(); }).catch(err=>toast(err.message)); }
|
|
80
|
+
bind(); load().catch(err=>toast(err.message));
|
|
@@ -1,111 +1,112 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<meta name="ccp-ui-token" content="__CCP_UI_TOKEN__" />
|
|
7
|
-
<title>multi-ccp</title>
|
|
8
|
-
<link rel="stylesheet" href="/styles.css" />
|
|
9
|
-
</head>
|
|
10
|
-
<body>
|
|
11
|
-
<div class="grain" aria-hidden="true"></div>
|
|
12
|
-
<main class="shell">
|
|
13
|
-
<header class="topbar">
|
|
14
|
-
<section class="brand-block">
|
|
15
|
-
<div class="brand-mark">ccp</div>
|
|
16
|
-
<div>
|
|
17
|
-
<h1>multi-ccp</h1>
|
|
18
|
-
</div>
|
|
19
|
-
</section>
|
|
20
|
-
<section class="top-actions">
|
|
21
|
-
<button class="ghost icon-action icon-only" id="themeToggle" type="button" aria-label="切换深色" title="切换深色"><span class="ui-icon" data-icon="moon"></span></button>
|
|
22
|
-
<button class="ghost icon-action icon-only" id="refreshBtn" type="button" aria-label="刷新" title="刷新"><span class="ui-icon" data-icon="refresh"></span></button>
|
|
23
|
-
<button class="primary icon-action" id="newProfileBtn" type="button"><span class="ui-icon" data-icon="plus"></span><span>New</span></button>
|
|
24
|
-
</section>
|
|
25
|
-
</header>
|
|
26
|
-
|
|
27
|
-
<section class="summary-grid" id="summaryGrid" aria-live="polite"></section>
|
|
28
|
-
|
|
29
|
-
<section class="workspace" id="workspace">
|
|
30
|
-
<div class="board" id="profileBoard"></div>
|
|
31
|
-
<aside class="drawer" id="drawer" aria-live="polite" aria-hidden="true">
|
|
32
|
-
<div class="drawer-rail">
|
|
33
|
-
<button class="icon-btn" id="drawerClose" type="button" title="关闭">×</button>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="empty-drawer">
|
|
36
|
-
<p class="eyebrow">profile details</p>
|
|
37
|
-
<h2>选择一个 Profile</h2>
|
|
38
|
-
<p>点击左侧卡片后,详情和编辑面板会从右侧展开。</p>
|
|
39
|
-
</div>
|
|
40
|
-
</aside>
|
|
41
|
-
</section>
|
|
42
|
-
</main>
|
|
43
|
-
|
|
44
|
-
<dialog id="newProfileDialog" class="modal">
|
|
45
|
-
<form method="dialog" class="modal-card" id="newProfileForm">
|
|
46
|
-
<div class="modal-head">
|
|
47
|
-
<div>
|
|
48
|
-
<p class="eyebrow">create profile</p>
|
|
49
|
-
<h2>New Profile</h2>
|
|
50
|
-
</div>
|
|
51
|
-
<button class="icon-btn" value="cancel" formnovalidate type="button" data-dialog-close="newProfileDialog">×</button>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="preset-layout">
|
|
54
|
-
<aside class="preset-picker">
|
|
55
|
-
<div class="preset-search">
|
|
56
|
-
<span class="ui-icon" data-icon="search"></span>
|
|
57
|
-
<input id="presetSearch" type="search" placeholder="搜索预设..." autocomplete="off" />
|
|
58
|
-
</div>
|
|
59
|
-
<div class="preset-filters" id="presetFilters">
|
|
60
|
-
<button class="chip active" type="button" data-preset-filter="all">All</button>
|
|
61
|
-
<button class="chip" type="button" data-preset-filter="api">API</button>
|
|
62
|
-
<button class="chip" type="button" data-preset-filter="ccr">CCR</button>
|
|
63
|
-
<button class="chip" type="button" data-preset-filter="custom">Custom</button>
|
|
64
|
-
<button class="chip" type="button" data-preset-filter="login">Login</button>
|
|
65
|
-
</div>
|
|
66
|
-
<div class="preset-list" id="presetList"></div>
|
|
67
|
-
</aside>
|
|
68
|
-
<section class="preset-detail">
|
|
69
|
-
<input type="hidden" name="kind" id="newKind" value="api" />
|
|
70
|
-
<input type="hidden" name="presetId" id="presetId" value="custom-api" />
|
|
71
|
-
<div id="presetSummary" class="preset-summary"></div>
|
|
72
|
-
<label>Profile 名称 <input name="name" id="newProfileName" required autocomplete="off" /></label>
|
|
73
|
-
<div data-kind-fields="api">
|
|
74
|
-
<label>API Key <input name="token" id="newToken" type="password" placeholder="输入 key;留空则写入占位符" /></label>
|
|
75
|
-
</div>
|
|
76
|
-
<div data-kind-fields="custom-api" hidden>
|
|
77
|
-
<label>Base URL <input name="baseUrl" required placeholder="https://api.example.com/anthropic" /></label>
|
|
78
|
-
<label>Auth Token <input name="customToken" type="password" placeholder="留空则写入占位符" /></label>
|
|
79
|
-
<label>Model <input name="model" placeholder="留空使用 Claude Code 默认模型" /></label>
|
|
80
|
-
</div>
|
|
81
|
-
<div data-kind-fields="ccr" hidden>
|
|
82
|
-
<label>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<label
|
|
88
|
-
<label>CCR
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<button class="
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
</
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="ccp-ui-token" content="__CCP_UI_TOKEN__" />
|
|
7
|
+
<title>multi-ccp</title>
|
|
8
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="grain" aria-hidden="true"></div>
|
|
12
|
+
<main class="shell">
|
|
13
|
+
<header class="topbar">
|
|
14
|
+
<section class="brand-block">
|
|
15
|
+
<div class="brand-mark">ccp</div>
|
|
16
|
+
<div>
|
|
17
|
+
<h1>multi-ccp</h1>
|
|
18
|
+
</div>
|
|
19
|
+
</section>
|
|
20
|
+
<section class="top-actions">
|
|
21
|
+
<button class="ghost icon-action icon-only" id="themeToggle" type="button" aria-label="切换深色" title="切换深色"><span class="ui-icon" data-icon="moon"></span></button>
|
|
22
|
+
<button class="ghost icon-action icon-only" id="refreshBtn" type="button" aria-label="刷新" title="刷新"><span class="ui-icon" data-icon="refresh"></span></button>
|
|
23
|
+
<button class="primary icon-action" id="newProfileBtn" type="button"><span class="ui-icon" data-icon="plus"></span><span>New</span></button>
|
|
24
|
+
</section>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<section class="summary-grid" id="summaryGrid" aria-live="polite"></section>
|
|
28
|
+
|
|
29
|
+
<section class="workspace" id="workspace">
|
|
30
|
+
<div class="board" id="profileBoard"></div>
|
|
31
|
+
<aside class="drawer" id="drawer" aria-live="polite" aria-hidden="true">
|
|
32
|
+
<div class="drawer-rail">
|
|
33
|
+
<button class="icon-btn" id="drawerClose" type="button" title="关闭">×</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="empty-drawer">
|
|
36
|
+
<p class="eyebrow">profile details</p>
|
|
37
|
+
<h2>选择一个 Profile</h2>
|
|
38
|
+
<p>点击左侧卡片后,详情和编辑面板会从右侧展开。</p>
|
|
39
|
+
</div>
|
|
40
|
+
</aside>
|
|
41
|
+
</section>
|
|
42
|
+
</main>
|
|
43
|
+
|
|
44
|
+
<dialog id="newProfileDialog" class="modal">
|
|
45
|
+
<form method="dialog" class="modal-card" id="newProfileForm">
|
|
46
|
+
<div class="modal-head">
|
|
47
|
+
<div>
|
|
48
|
+
<p class="eyebrow">create profile</p>
|
|
49
|
+
<h2>New Profile</h2>
|
|
50
|
+
</div>
|
|
51
|
+
<button class="icon-btn" value="cancel" formnovalidate type="button" data-dialog-close="newProfileDialog">×</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="preset-layout">
|
|
54
|
+
<aside class="preset-picker">
|
|
55
|
+
<div class="preset-search">
|
|
56
|
+
<span class="ui-icon" data-icon="search"></span>
|
|
57
|
+
<input id="presetSearch" type="search" placeholder="搜索预设..." autocomplete="off" />
|
|
58
|
+
</div>
|
|
59
|
+
<div class="preset-filters" id="presetFilters">
|
|
60
|
+
<button class="chip active" type="button" data-preset-filter="all">All</button>
|
|
61
|
+
<button class="chip" type="button" data-preset-filter="api">API</button>
|
|
62
|
+
<button class="chip" type="button" data-preset-filter="ccr">CCR</button>
|
|
63
|
+
<button class="chip" type="button" data-preset-filter="custom">Custom</button>
|
|
64
|
+
<button class="chip" type="button" data-preset-filter="login">Login</button>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="preset-list" id="presetList"></div>
|
|
67
|
+
</aside>
|
|
68
|
+
<section class="preset-detail">
|
|
69
|
+
<input type="hidden" name="kind" id="newKind" value="api" />
|
|
70
|
+
<input type="hidden" name="presetId" id="presetId" value="custom-api" />
|
|
71
|
+
<div id="presetSummary" class="preset-summary"></div>
|
|
72
|
+
<label>Profile 名称 <input name="name" id="newProfileName" required autocomplete="off" /></label>
|
|
73
|
+
<div data-kind-fields="api">
|
|
74
|
+
<label>API Key <input name="token" id="newToken" type="password" placeholder="输入 key;留空则写入占位符" /></label>
|
|
75
|
+
</div>
|
|
76
|
+
<div data-kind-fields="custom-api" hidden>
|
|
77
|
+
<label>Base URL <input name="baseUrl" required placeholder="https://api.example.com/anthropic" /></label>
|
|
78
|
+
<label>Auth Token <input name="customToken" type="password" placeholder="留空则写入占位符" /></label>
|
|
79
|
+
<label>Model <input name="model" placeholder="留空使用 Claude Code 默认模型" /></label>
|
|
80
|
+
</div>
|
|
81
|
+
<div data-kind-fields="ccr" hidden>
|
|
82
|
+
<label>Provider API Key <input name="ccrProviderApiKey" type="password" placeholder="填写该 CCR 模板的 provider key" /></label>
|
|
83
|
+
<label>CCR Token <input name="ccrToken" type="password" placeholder="留空使用预设默认值" /></label>
|
|
84
|
+
<p class="hint">该模板会自动写入所需 CCR provider/model,并生成对应 CCR preset。</p>
|
|
85
|
+
</div>
|
|
86
|
+
<div data-kind-fields="manual-ccr" hidden>
|
|
87
|
+
<label>模型路由 <select name="route" id="manualCcrRoute" required><option value="">加载 CCR 路由中...</option></select></label>
|
|
88
|
+
<label>CCR Preset 名称 <input name="manualCcrPreset" placeholder="默认使用 Profile 名称" autocomplete="off" /></label>
|
|
89
|
+
<label>CCR Token <input name="manualCcrToken" type="password" placeholder="留空使用 ccr-local-secret" /></label>
|
|
90
|
+
<p class="hint">模型路由来自 Claude Code Router 的 provider/model 配置;如果没有想要的模型,请先打开 CCR UI 添加。</p>
|
|
91
|
+
</div>
|
|
92
|
+
<div data-kind-fields="login" hidden>
|
|
93
|
+
<p class="hint">Login Profile 不保存 Claude 账号密码,只隔离 Claude Code 的登录状态。</p>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
</div>
|
|
97
|
+
<menu class="modal-actions">
|
|
98
|
+
<button class="ghost" value="cancel" formnovalidate type="button" data-dialog-close="newProfileDialog">取消</button>
|
|
99
|
+
<button class="primary" id="createProfileSubmit" value="default" type="button">创建</button>
|
|
100
|
+
</menu>
|
|
101
|
+
<div class="dialog-toast-region"></div>
|
|
102
|
+
</form>
|
|
103
|
+
</dialog>
|
|
104
|
+
|
|
105
|
+
<dialog id="ccrDialog" class="modal">
|
|
106
|
+
<div class="modal-card ccr-card" id="ccrPanel"></div>
|
|
107
|
+
</dialog>
|
|
108
|
+
|
|
109
|
+
<div class="toast-region" id="toastRegion"></div>
|
|
110
|
+
<script src="/app.js" type="module"></script>
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{color-scheme:light;--bg:#f5f3ee;--panel:#fffdf8;--panel-2:#ffffff;--ink:#191815;--muted:#726d64;--faint:#a29a90;--line:#ded8cf;--accent:#9b5a36;--blue:#405f9f;--green:#2f7358;--orange:#a86722;--red:#ad4747;--chip:#efe8dd;--shadow:0 10px 30px rgba(40,34,26,.08);--radius:16px;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--sans:"Aptos","Segoe UI Variable","Segoe UI",system-ui,-apple-system,sans-serif}html[data-theme=dark]{color-scheme:dark;--bg:#141412;--panel:#1d1c19;--panel-2:#24231f;--ink:#eee8de;--muted:#aaa196;--faint:#7e766d;--line:#39352e;--accent:#d98b5d;--blue:#91aef0;--green:#73c49f;--orange:#dfb168;--red:#e07b77;--chip:#2c2822;--shadow:0 12px 34px rgba(0,0,0,.32)}*{box-sizing:border-box}html,body{height:100%}body{margin:0;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased}body:before{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(circle at 8% -8%,rgba(155,90,54,.12),transparent 34%),radial-gradient(circle at 100% 0,rgba(64,95,159,.1),transparent 28%)}.grain{display:none}button,input,select{font:inherit}button{border:0;cursor:pointer}.shell{position:relative;width:min(1500px,100% - 24px);height:100vh;margin:0 auto;padding:12px 0;display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:8px}.topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px;border:1px solid var(--line);border-radius:18px;background:color-mix(in srgb,var(--panel) 94%,transparent);box-shadow:var(--shadow)}.brand-block{display:flex;align-items:center;gap:10px}.brand-mark{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:var(--ink);color:var(--bg);font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:-.04em}.eyebrow{margin:0 0 2px;color:var(--accent);text-transform:uppercase;letter-spacing:.16em;font-family:var(--mono);font-size:9.5px;font-weight:800}.topbar h1{margin:0;font-size:18px;line-height:1;letter-spacing:-.035em}.top-actions,.filters,.view-switch,.modal-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.ui-icon,.icon-action>svg,.icon-action .ui-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.icon-action{white-space:nowrap}.icon-only{width:36px;min-width:36px;height:36px;padding:0}.icon-only svg{display:block;margin:auto}.primary,.ghost,.chip,.icon-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:31px;border-radius:999px;padding:7px 11px;color:var(--ink);background:var(--panel);border:1px solid var(--line);transition:background .16s,border-color .16s,transform .16s}.primary{background:var(--ink);color:var(--bg);border-color:var(--ink);font-weight:760}.ghost:hover,.chip:hover,.icon-btn:hover{background:var(--chip);border-color:color-mix(in srgb,var(--accent) 35%,var(--line))}.chip{min-height:27px;padding:5px 9px;font-size:11.5px;color:var(--muted)}.chip.active{background:var(--ink);border-color:var(--ink);color:var(--bg)}.summary-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:7px}.metric{height:48px;padding:8px 11px;border:1px solid var(--line);background:var(--panel);border-radius:14px;box-shadow:0 5px 14px rgba(40,34,26,.035);display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px;position:relative;overflow:hidden}.metric:before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:3px;border-radius:999px;background:var(--faint);opacity:.55}.metric.api:before{background:var(--blue)}.metric.login:before{background:var(--green)}.metric.ccr:before,.metric.ccr-status:before{background:var(--orange)}.metric.attention:before{background:var(--red)}.metric b{font-size:17px;line-height:1;letter-spacing:-.045em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.metric span{color:var(--muted);font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.08em}#ccrMetric{cursor:pointer}#ccrMetric b{display:inline-block;border-bottom:1px solid var(--accent);padding-bottom:2px;line-height:1.05}.workspace{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 0;gap:0;transition:grid-template-columns .28s cubic-bezier(.2,.8,.2,1),gap .28s cubic-bezier(.2,.8,.2,1)}.workspace.drawer-open{grid-template-columns:minmax(0,1fr) 390px;gap:10px}.board,.drawer{min-height:0;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.board{overflow:auto;padding:2px 2px 18px}.board-toolbar{position:sticky;top:0;z-index:4;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 8px;padding:7px;border:1px solid var(--line);border-radius:16px;background:color-mix(in srgb,var(--panel) 96%,transparent);box-shadow:0 6px 18px rgba(40,34,26,.045)}.board-tools-left,.board-tools-right{display:flex;align-items:center;gap:7px;min-width:0}.board-tools-right{flex:0 0 auto}.search-wrap{width:min(340px,34vw);height:30px;display:flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel-2)}.search-wrap span{color:var(--accent);font-family:var(--mono);font-size:13px}.search-wrap input{width:100%;border:0;outline:0;background:transparent;color:var(--ink);font-size:12.5px}.board-head{display:flex;align-items:center;justify-content:space-between;margin:2px 0 8px;padding:0 2px}.board-head h2{margin:0;font-size:15px;letter-spacing:-.03em}.board-head p{margin:0;color:var(--muted);font-size:11.5px;font-family:var(--mono)}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(218px,1fr));gap:8px;contain:layout}.workspace.drawer-open .cards{grid-template-columns:repeat(auto-fill,minmax(212px,1fr))}.profile-card{cursor:pointer;position:relative;min-height:132px;padding:12px;border:1px solid var(--line);background:var(--panel);border-radius:16px;box-shadow:0 7px 18px rgba(40,34,26,.045);transition:transform .16s,border-color .16s,background .16s,box-shadow .16s}.profile-card:before{content:"";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 7%,transparent),transparent 42%);opacity:0;transition:opacity .16s;pointer-events:none}.profile-card:hover,.profile-card.selected{transform:translateY(-2px);border-color:var(--accent);background:var(--panel-2);box-shadow:0 12px 26px rgba(40,34,26,.08)}.profile-card:hover:before,.profile-card.selected:before{opacity:1}.profile-card.selected:after{content:"";position:absolute;inset:auto 10px 0;height:2px;border-radius:999px;background:var(--accent)}.card-top{display:flex;align-items:center;gap:9px;min-width:0}.profile-icon{width:32px;height:32px;flex:0 0 auto;display:grid;place-items:center;border-radius:11px;background:var(--chip);color:var(--accent);box-shadow:inset 0 0 0 1px color-mix(in srgb,currentColor 18%,transparent)}.profile-icon svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.profile-icon.api{color:var(--blue)}.profile-icon.login{color:var(--green)}.profile-icon.ccr{color:var(--orange)}.profile-icon.main{color:var(--ink)}.card-title{min-width:0}.profile-card h3{margin:0;font-size:16px;line-height:1.1;letter-spacing:-.035em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card-title p{margin:3px 0 0;color:var(--muted);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tag-row{display:flex;gap:4px;flex-wrap:wrap;margin:8px 0}.tag{display:inline-flex;align-items:center;border-radius:999px;padding:2px 6px;font-size:10px;font-weight:760;background:var(--chip);color:var(--muted);border:1px solid var(--line);white-space:nowrap}.tag.ready{color:var(--green)}.tag.warn{color:var(--orange)}.tag.bad{color:var(--red)}.profile-meta{display:grid;gap:2px;margin:0 0 29px;color:var(--muted);font-size:11.2px;line-height:1.24}.profile-meta div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.profile-meta strong{color:var(--ink);font-weight:760;margin-right:5px}.card-actions{position:absolute;left:11px;right:11px;bottom:10px;display:flex;gap:5px}.tiny{min-height:25px;padding:4px 8px;font-size:11px}.list-table{width:100%;border-collapse:separate;border-spacing:0 5px;font-size:12px}.list-table td,.list-table th{text-align:left;padding:9px 10px;background:var(--panel);border-top:1px solid var(--line);border-bottom:1px solid var(--line);transition:background .16s,border-color .16s,transform .16s}.list-table th{color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.12em;font-family:var(--mono);background:transparent;border-color:transparent}.list-table td:first-child{border-left:1px solid var(--line);border-radius:12px 0 0 12px}.list-table td:last-child{border-right:1px solid var(--line);border-radius:0 12px 12px 0}.list-table tbody tr{cursor:pointer}.list-table tbody tr:hover td,.list-table tbody tr.selected td{background:var(--panel-2);border-color:color-mix(in srgb,var(--accent) 42%,var(--line))}.list-table tbody tr.selected td:first-child{box-shadow:inset 3px 0 0 var(--accent)}.drawer{position:relative;min-height:0;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--line);background:var(--panel);border-radius:18px;box-shadow:var(--shadow);padding:14px;transform:translateX(16px);opacity:0;pointer-events:none;transition:transform .24s ease,opacity .18s ease}.workspace.drawer-open .drawer{transform:translateX(0);opacity:1;pointer-events:auto}.drawer-rail{display:flex;justify-content:flex-end;margin-bottom:4px}.empty-drawer{display:grid;place-content:center;min-height:70%;padding:22px;text-align:center;color:var(--muted)}.empty-state{min-height:280px;display:grid;place-content:center;text-align:center;color:var(--muted);border:1px dashed var(--line);border-radius:18px;background:var(--panel)}.drawer h2,.empty-state h2{margin:0 0 8px;font-size:24px;line-height:1.1;letter-spacing:-.045em;color:var(--ink);overflow-wrap:anywhere}.drawer-fixed{flex:0 0 auto}.launch-section{border-top:0;padding-top:0}.drawer-scroll{min-height:0;overflow:auto;flex:1 1 auto;padding-right:4px;border-top:1px solid var(--line);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.drawer-scroll::-webkit-scrollbar{width:8px}.drawer-scroll::-webkit-scrollbar-track{background:transparent}.drawer-scroll::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.drawer-scroll::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.drawer-section{padding:12px 0;border-top:1px solid var(--line)}.drawer-scroll>.profile-summary>.drawer-section:first-child{border-top:0}.kv{display:grid;grid-template-columns:78px minmax(0,1fr);gap:6px;font-size:11.5px;line-height:1.35}.kv span{color:var(--muted)}.kv strong{overflow-wrap:anywhere}.command{display:flex;justify-content:space-between;gap:8px;align-items:center;padding:8px 9px;background:var(--chip);border-radius:11px;font-family:var(--mono);font-size:11px;overflow:hidden}.command code{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}label{display:grid;gap:4px;margin:8px 0;color:var(--muted);font-size:11.5px}input,select{width:100%;border:1px solid var(--line);border-radius:11px;padding:8px 34px 8px 9px;background:var(--panel-2);color:var(--ink);outline:0;font-size:12.5px}select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 16px) 52%,calc(100% - 11px) 52%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}input:focus,select:focus{border-color:var(--accent)}.danger-actions{display:grid;gap:9px;margin-top:10px}.danger-actions .ghost{justify-self:start}.modal{border:0;background:transparent;opacity:0;transform:translateY(10px) scale(.985);transition:opacity .18s ease,transform .22s cubic-bezier(.2,.8,.2,1),overlay .22s allow-discrete,display .22s allow-discrete}.modal[open]{opacity:1;transform:translateY(0) scale(1)}@starting-style{.modal[open]{opacity:0;transform:translateY(10px) scale(.985)}}.modal::backdrop{background:rgba(28,24,20,.38);backdrop-filter:blur(8px);opacity:0;transition:opacity .22s ease,overlay .22s allow-discrete,display .22s allow-discrete}.modal[open]::backdrop{opacity:1}@starting-style{.modal[open]::backdrop{opacity:0}}.modal-card{width:min(820px,calc(100vw - 28px));max-height:min(76vh,720px);overflow:hidden;display:flex;flex-direction:column;padding:16px;border:1px solid var(--line);border-radius:22px;background:var(--panel-2);color:var(--ink);box-shadow:var(--shadow);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.modal-card::-webkit-scrollbar{width:8px;height:8px}.modal-card::-webkit-scrollbar-track{background:transparent}.modal-card::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.modal-card::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.ccr-card{width:min(520px,calc(100vw - 28px));max-height:min(70vh,560px)}.modal-head{display:flex;justify-content:space-between;gap:16px;align-items:start}.modal-head h2{margin:0;font-size:18px;letter-spacing:-.035em}.icon-btn{width:30px;height:30px;padding:0}.hint{color:var(--muted);line-height:1.5;font-size:12px}.hint a{display:inline-flex;align-items:center;padding:1px 6px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--line));border-radius:999px;background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--accent);text-decoration:none;font-weight:650;line-height:1.35}.hint a:hover{background:var(--chip);border-color:var(--accent)}.preset-layout{display:grid;grid-template-columns:280px minmax(0,1fr);gap:14px;margin-top:12px;min-height:0;flex:1 1 auto;overflow:hidden}.preset-picker{min-height:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:8px;padding:8px;border:1px solid var(--line);border-radius:16px;background:var(--panel)}.preset-search{height:30px;display:flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel-2)}.preset-search svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--accent)}.preset-search input{border:0;background:transparent;padding:0;width:100%;font-size:12px}.preset-filters{display:flex;gap:5px;overflow:auto;padding-bottom:2px}.preset-filters .chip{white-space:nowrap}.preset-list{min-height:0;overflow:auto;display:grid;gap:7px;align-content:start;padding-right:2px;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preset-list::-webkit-scrollbar{width:8px}.preset-list::-webkit-scrollbar-track{background:transparent}.preset-list::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.preset-option{width:100%;display:grid;grid-template-columns:32px minmax(0,1fr);gap:9px;align-items:center;text-align:left;padding:9px;border:1px solid transparent;border-radius:13px;background:transparent;color:var(--ink);transition:background .16s ease,border-color .16s ease,box-shadow .16s ease}.preset-option:hover{background:var(--chip);border-color:var(--line)}.preset-option.active{background:var(--panel-2);border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}.preset-icon{width:32px;height:32px;display:grid;place-items:center;border-radius:10px;background:var(--chip);color:var(--accent)}.preset-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.preset-option strong{display:block;font-size:13px;line-height:1.15;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.preset-option small,.preset-option em{display:block;margin-top:3px;color:var(--muted);font-size:10.5px;font-style:normal;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.preset-option em{color:var(--faint)}.preset-empty{padding:18px 10px;text-align:center;color:var(--muted);font-size:12px;border:1px dashed var(--line);border-radius:12px}.preset-detail{min-width:0;overflow:auto;padding:8px 2px;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preset-detail::-webkit-scrollbar{width:8px;height:8px}.preset-detail::-webkit-scrollbar-track{background:transparent}.preset-detail::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.preset-detail::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.preset-summary{position:relative;padding:12px;border:1px solid var(--line);border-radius:16px;background:var(--panel);margin-bottom:12px}.preset-summary h3{margin:0 0 5px;font-size:18px;letter-spacing:-.035em}.preset-summary p{margin:0;color:var(--muted);font-size:12.5px;line-height:1.45}.preset-summary dl{display:grid;grid-template-columns:92px minmax(0,1fr);gap:6px;margin:12px 0 0;font-size:11.5px}.preset-summary dt{color:var(--muted)}.preset-summary dd{margin:0;overflow-wrap:anywhere}.preset-adjust{position:absolute;right:10px;top:10px;display:inline-flex;padding:4px 8px;border:1px solid var(--line);border-radius:999px;color:var(--accent);text-decoration:none;font-size:11px;font-weight:500;background:var(--panel-2)}.preset-adjust:hover{border-color:var(--accent);background:var(--chip)}.preset-config{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}.preset-config summary{cursor:pointer;color:var(--accent);font-size:12px;font-weight:500}.preset-config pre{max-height:190px;overflow:auto;margin:9px 0 0;padding:10px;border-radius:12px;background:var(--chip);font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;scrollbar-width:thin;scrollbar-color:var(--line-strong,var(--line)) transparent}.preset-config pre::-webkit-scrollbar{width:8px;height:8px}.preset-config pre::-webkit-scrollbar-track{background:transparent}.preset-config pre::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.preset-config pre::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.modal-actions{justify-content:flex-end;margin:14px 0 0;padding:0}.toast-region{position:fixed;right:18px;bottom:18px;display:grid;gap:8px;z-index:30}.dialog-toast-region{position:absolute;left:50%;top:14px;transform:translateX(-50%);width:min(420px,calc(100% - 32px));display:grid;justify-items:center;gap:8px;z-index:10000;pointer-events:none}.toast{padding:9px 12px;border:1px solid var(--line);border-radius:13px;background:var(--panel-2);box-shadow:var(--shadow);font-size:12.5px;animation:toast-in .22s ease both,toast-out .28s ease 3.25s forwards}.dialog-toast-region .toast{pointer-events:auto;animation:dialog-toast-in .22s ease both,dialog-toast-out .28s ease 3.25s forwards}@keyframes toast-in{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes dialog-toast-in{from{opacity:0;transform:translateY(-8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes toast-out{to{opacity:0;transform:translateY(6px) scale(.98)}}@keyframes dialog-toast-out{to{opacity:0;transform:translateY(6px) scale(.98)}}@media(max-width:1080px){body{overflow:auto}.shell{height:auto;min-height:100vh}.workspace,.workspace.drawer-open{grid-template-columns:1fr}.drawer{display:none}.workspace.drawer-open .drawer{display:flex;position:fixed;inset:10px;z-index:20;overflow:hidden}.board-toolbar{position:static;align-items:stretch;flex-direction:column}.board-tools-left,.board-tools-right{width:100%;overflow:auto}.search-wrap{width:100%}.summary-grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:760px){.preset-layout{grid-template-columns:1fr}.preset-picker{max-height:330px}.preset-list{max-height:210px;overflow:auto}}@media(max-width:720px){.shell{width:calc(100% - 16px);padding:8px 0}.topbar{align-items:flex-start}.summary-grid{grid-template-columns:repeat(2,1fr)}.cards{grid-template-columns:1fr}.filters,.view-switch{overflow:auto;flex-wrap:nowrap}.chip{white-space:nowrap}.top-actions{justify-content:flex-end}.brand-mark{display:none}}
|
|
1
|
+
:root{color-scheme:light;--bg:#f5f3ee;--panel:#fffdf8;--panel-2:#ffffff;--ink:#191815;--muted:#726d64;--faint:#a29a90;--line:#ded8cf;--accent:#9b5a36;--blue:#405f9f;--green:#2f7358;--orange:#a86722;--red:#ad4747;--chip:#efe8dd;--shadow:0 10px 30px rgba(40,34,26,.08);--radius:16px;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--sans:"Aptos","Segoe UI Variable","Segoe UI",system-ui,-apple-system,sans-serif}html[data-theme=dark]{color-scheme:dark;--bg:#141412;--panel:#1d1c19;--panel-2:#24231f;--ink:#eee8de;--muted:#aaa196;--faint:#7e766d;--line:#39352e;--accent:#d98b5d;--blue:#91aef0;--green:#73c49f;--orange:#dfb168;--red:#e07b77;--chip:#2c2822;--shadow:0 12px 34px rgba(0,0,0,.32)}*{box-sizing:border-box}html,body{height:100%}body{margin:0;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased}body:before{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(circle at 8% -8%,rgba(155,90,54,.12),transparent 34%),radial-gradient(circle at 100% 0,rgba(64,95,159,.1),transparent 28%)}.grain{display:none}button,input,select{font:inherit}button{border:0;cursor:pointer}button:disabled{cursor:wait;opacity:.68}.shell{position:relative;width:min(1500px,100% - 24px);height:100vh;margin:0 auto;padding:12px 0;display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:8px}.topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 10px;border:1px solid var(--line);border-radius:18px;background:color-mix(in srgb,var(--panel) 94%,transparent);box-shadow:var(--shadow)}.brand-block{display:flex;align-items:center;gap:10px}.brand-mark{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:var(--ink);color:var(--bg);font-family:var(--mono);font-size:11px;font-weight:800;letter-spacing:-.04em}.eyebrow{margin:0 0 2px;color:var(--accent);text-transform:uppercase;letter-spacing:.16em;font-family:var(--mono);font-size:9.5px;font-weight:800}.topbar h1{margin:0;font-size:18px;line-height:1;letter-spacing:-.035em}.top-actions,.filters,.view-switch,.modal-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.ui-icon,.icon-action>svg,.icon-action .ui-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.icon-action{white-space:nowrap}.icon-only{width:36px;min-width:36px;height:36px;padding:0}.icon-only svg{display:block;margin:auto}.primary,.ghost,.chip,.icon-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:31px;border-radius:999px;padding:7px 11px;color:var(--ink);background:var(--panel);border:1px solid var(--line);transition:background .16s,border-color .16s,transform .16s}.primary{background:var(--ink);color:var(--bg);border-color:var(--ink);font-weight:760}.ghost:hover,.chip:hover,.icon-btn:hover{background:var(--chip);border-color:color-mix(in srgb,var(--accent) 35%,var(--line))}.chip{min-height:27px;padding:5px 9px;font-size:11.5px;color:var(--muted)}.chip.active{background:var(--ink);border-color:var(--ink);color:var(--bg)}.summary-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:7px}.metric{height:48px;padding:8px 11px;border:1px solid var(--line);background:var(--panel);border-radius:14px;box-shadow:0 5px 14px rgba(40,34,26,.035);display:grid;grid-template-columns:1fr auto;align-items:center;gap:8px;position:relative;overflow:hidden}.metric:before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:3px;border-radius:999px;background:var(--faint);opacity:.55}.metric.api:before{background:var(--blue)}.metric.login:before{background:var(--green)}.metric.ccr:before,.metric.ccr-status:before{background:var(--orange)}.metric.attention:before{background:var(--red)}.metric b{font-size:17px;line-height:1;letter-spacing:-.045em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.metric span{color:var(--muted);font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.08em}#ccrMetric{cursor:pointer}#ccrMetric b{display:inline-block;border-bottom:1px solid var(--accent);padding-bottom:2px;line-height:1.05}.workspace{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 0;gap:0;transition:grid-template-columns .28s cubic-bezier(.2,.8,.2,1),gap .28s cubic-bezier(.2,.8,.2,1)}.workspace.drawer-open{grid-template-columns:minmax(0,1fr) 390px;gap:10px}.board,.drawer{min-height:0;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.board{overflow:auto;padding:2px 2px 18px}.board-toolbar{position:sticky;top:0;z-index:4;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 8px;padding:7px;border:1px solid var(--line);border-radius:16px;background:color-mix(in srgb,var(--panel) 96%,transparent);box-shadow:0 6px 18px rgba(40,34,26,.045)}.board-tools-left,.board-tools-right{display:flex;align-items:center;gap:7px;min-width:0}.board-tools-right{flex:0 0 auto}.search-wrap{width:min(340px,34vw);height:30px;display:flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel-2)}.search-wrap span{color:var(--accent);font-family:var(--mono);font-size:13px}.search-wrap input{width:100%;border:0;outline:0;background:transparent;color:var(--ink);font-size:12.5px}.board-head{display:flex;align-items:center;justify-content:space-between;margin:2px 0 8px;padding:0 2px}.board-head h2{margin:0;font-size:15px;letter-spacing:-.03em}.board-head p{margin:0;color:var(--muted);font-size:11.5px;font-family:var(--mono)}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(218px,1fr));gap:8px;contain:layout}.workspace.drawer-open .cards{grid-template-columns:repeat(auto-fill,minmax(212px,1fr))}.profile-card{cursor:pointer;position:relative;min-height:132px;padding:12px;border:1px solid var(--line);background:var(--panel);border-radius:16px;box-shadow:0 7px 18px rgba(40,34,26,.045);transition:transform .16s,border-color .16s,background .16s,box-shadow .16s}.profile-card:before{content:"";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 7%,transparent),transparent 42%);opacity:0;transition:opacity .16s;pointer-events:none}.profile-card:hover,.profile-card.selected{transform:translateY(-2px);border-color:var(--accent);background:var(--panel-2);box-shadow:0 12px 26px rgba(40,34,26,.08)}.profile-card:hover:before,.profile-card.selected:before{opacity:1}.profile-card.selected:after{content:"";position:absolute;inset:auto 10px 0;height:2px;border-radius:999px;background:var(--accent)}.card-top{display:flex;align-items:center;gap:9px;min-width:0}.profile-icon{width:32px;height:32px;flex:0 0 auto;display:grid;place-items:center;border-radius:11px;background:var(--chip);color:var(--accent);box-shadow:inset 0 0 0 1px color-mix(in srgb,currentColor 18%,transparent)}.profile-icon svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.profile-icon.api{color:var(--blue)}.profile-icon.login{color:var(--green)}.profile-icon.ccr{color:var(--orange)}.profile-icon.main{color:var(--ink)}.card-title{min-width:0}.profile-card h3{margin:0;font-size:16px;line-height:1.1;letter-spacing:-.035em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card-title p{margin:3px 0 0;color:var(--muted);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tag-row{display:flex;gap:4px;flex-wrap:wrap;margin:8px 0}.tag{display:inline-flex;align-items:center;border-radius:999px;padding:2px 6px;font-size:10px;font-weight:760;background:var(--chip);color:var(--muted);border:1px solid var(--line);white-space:nowrap}.tag.ready{color:var(--green)}.tag.warn{color:var(--orange)}.tag.bad{color:var(--red)}.profile-meta{display:grid;gap:2px;margin:0 0 29px;color:var(--muted);font-size:11.2px;line-height:1.24}.profile-meta div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.profile-meta strong{color:var(--ink);font-weight:760;margin-right:5px}.card-actions{position:absolute;left:11px;right:11px;bottom:10px;display:flex;gap:5px}.tiny{min-height:25px;padding:4px 8px;font-size:11px}.list-table{width:100%;border-collapse:separate;border-spacing:0 5px;font-size:12px}.list-table td,.list-table th{text-align:left;padding:9px 10px;background:var(--panel);border-top:1px solid var(--line);border-bottom:1px solid var(--line);transition:background .16s,border-color .16s,transform .16s}.list-table th{color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.12em;font-family:var(--mono);background:transparent;border-color:transparent}.list-table td:first-child{border-left:1px solid var(--line);border-radius:12px 0 0 12px}.list-table td:last-child{border-right:1px solid var(--line);border-radius:0 12px 12px 0}.list-table tbody tr{cursor:pointer}.list-table tbody tr:hover td,.list-table tbody tr.selected td{background:var(--panel-2);border-color:color-mix(in srgb,var(--accent) 42%,var(--line))}.list-table tbody tr.selected td:first-child{box-shadow:inset 3px 0 0 var(--accent)}.drawer{position:relative;min-height:0;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--line);background:var(--panel);border-radius:18px;box-shadow:var(--shadow);padding:14px;transform:translateX(16px);opacity:0;pointer-events:none;transition:transform .24s ease,opacity .18s ease}.workspace.drawer-open .drawer{transform:translateX(0);opacity:1;pointer-events:auto}.drawer-rail{display:flex;justify-content:flex-end;margin-bottom:4px}.empty-drawer{display:grid;place-content:center;min-height:70%;padding:22px;text-align:center;color:var(--muted)}.empty-state{min-height:280px;display:grid;place-content:center;text-align:center;color:var(--muted);border:1px dashed var(--line);border-radius:18px;background:var(--panel)}.drawer h2,.empty-state h2{margin:0 0 8px;font-size:24px;line-height:1.1;letter-spacing:-.045em;color:var(--ink);overflow-wrap:anywhere}.drawer-fixed{flex:0 0 auto}.launch-section{border-top:0;padding-top:0}.drawer-scroll{min-height:0;overflow:auto;flex:1 1 auto;padding-right:4px;border-top:1px solid var(--line);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.drawer-scroll::-webkit-scrollbar{width:8px}.drawer-scroll::-webkit-scrollbar-track{background:transparent}.drawer-scroll::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.drawer-scroll::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.drawer-section{padding:12px 0;border-top:1px solid var(--line)}.drawer-scroll>.profile-summary>.drawer-section:first-child{border-top:0}.kv{display:grid;grid-template-columns:78px minmax(0,1fr);gap:6px;font-size:11.5px;line-height:1.35}.kv span{color:var(--muted)}.kv strong{overflow-wrap:anywhere}.path-link{background:none;border:0;padding:0;margin:0;color:var(--accent);text-decoration:underline;cursor:pointer;font:inherit;line-height:inherit;word-break:break-all;text-align:left;display:block}.path-link:hover{color:var(--ink)}.command{display:flex;justify-content:space-between;gap:8px;align-items:center;padding:8px 9px;background:var(--chip);border-radius:11px;font-family:var(--mono);font-size:11px;overflow:hidden}.command code{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.command-actions{display:flex;gap:5px;flex:0 0 auto}label{display:grid;gap:4px;margin:8px 0;color:var(--muted);font-size:11.5px}input,select{width:100%;border:1px solid var(--line);border-radius:11px;padding:8px 34px 8px 9px;background:var(--panel-2);color:var(--ink);outline:0;font-size:12.5px}select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 16px) 52%,calc(100% - 11px) 52%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}input:focus,select:focus{border-color:var(--accent)}.danger-actions{display:grid;gap:9px;margin-top:10px}.danger-actions .ghost{justify-self:start}.modal{border:0;background:transparent;opacity:0;transform:translateY(10px) scale(.985);transition:opacity .18s ease,transform .22s cubic-bezier(.2,.8,.2,1),overlay .22s allow-discrete,display .22s allow-discrete}.modal[open]{opacity:1;transform:translateY(0) scale(1)}@starting-style{.modal[open]{opacity:0;transform:translateY(10px) scale(.985)}}.modal::backdrop{background:rgba(28,24,20,.38);backdrop-filter:blur(8px);opacity:0;transition:opacity .22s ease,overlay .22s allow-discrete,display .22s allow-discrete}.modal[open]::backdrop{opacity:1}@starting-style{.modal[open]::backdrop{opacity:0}}.modal-card{width:min(820px,calc(100vw - 28px));max-height:min(76vh,720px);overflow:hidden;display:flex;flex-direction:column;padding:16px;border:1px solid var(--line);border-radius:22px;background:var(--panel-2);color:var(--ink);box-shadow:var(--shadow);scrollbar-width:thin;scrollbar-color:var(--line) transparent}.modal-card::-webkit-scrollbar{width:8px;height:8px}.modal-card::-webkit-scrollbar-track{background:transparent}.modal-card::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.modal-card::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.ccr-card{width:min(520px,calc(100vw - 28px));max-height:min(70vh,560px)}.modal-head{display:flex;justify-content:space-between;gap:16px;align-items:start}.modal-head h2{margin:0;font-size:18px;letter-spacing:-.035em}.icon-btn{width:30px;height:30px;padding:0}.hint{color:var(--muted);line-height:1.5;font-size:12px}.hint a{display:inline-flex;align-items:center;padding:1px 6px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--line));border-radius:999px;background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--accent);text-decoration:none;font-weight:650;line-height:1.35}.hint a:hover{background:var(--chip);border-color:var(--accent)}.preset-layout{display:grid;grid-template-columns:280px minmax(0,1fr);gap:14px;margin-top:12px;min-height:0;flex:1 1 auto;overflow:hidden}.preset-picker{min-height:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);gap:8px;padding:8px;border:1px solid var(--line);border-radius:16px;background:var(--panel)}.preset-search{height:30px;display:flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel-2)}.preset-search svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--accent)}.preset-search input{border:0;background:transparent;padding:0;width:100%;font-size:12px}.preset-filters{display:flex;gap:5px;overflow:auto;padding-bottom:2px}.preset-filters .chip{white-space:nowrap}.preset-list{min-height:0;overflow:auto;display:grid;gap:7px;align-content:start;padding-right:2px;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preset-list::-webkit-scrollbar{width:8px}.preset-list::-webkit-scrollbar-track{background:transparent}.preset-list::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.preset-option{width:100%;display:grid;grid-template-columns:32px minmax(0,1fr);gap:9px;align-items:center;text-align:left;padding:9px;border:1px solid transparent;border-radius:13px;background:transparent;color:var(--ink);transition:background .16s ease,border-color .16s ease,box-shadow .16s ease}.preset-option:hover{background:var(--chip);border-color:var(--line)}.preset-option.active{background:var(--panel-2);border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent)}.preset-icon{width:32px;height:32px;display:grid;place-items:center;border-radius:10px;background:var(--chip);color:var(--accent)}.preset-icon svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.preset-option strong{display:block;font-size:13px;line-height:1.15;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.preset-option small,.preset-option em{display:block;margin-top:3px;color:var(--muted);font-size:10.5px;font-style:normal;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.preset-option em{color:var(--faint)}.preset-empty{padding:18px 10px;text-align:center;color:var(--muted);font-size:12px;border:1px dashed var(--line);border-radius:12px}.preset-detail{min-width:0;overflow:auto;padding:8px 2px;scrollbar-width:thin;scrollbar-color:var(--line) transparent}.preset-detail::-webkit-scrollbar{width:8px;height:8px}.preset-detail::-webkit-scrollbar-track{background:transparent}.preset-detail::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.preset-detail::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.preset-summary{position:relative;padding:12px;border:1px solid var(--line);border-radius:16px;background:var(--panel);margin-bottom:12px}.preset-summary h3{margin:0 0 5px;font-size:18px;letter-spacing:-.035em}.preset-summary p{margin:0;color:var(--muted);font-size:12.5px;line-height:1.45}.preset-summary dl{display:grid;grid-template-columns:92px minmax(0,1fr);gap:6px;margin:12px 0 0;font-size:11.5px}.preset-summary dt{color:var(--muted)}.preset-summary dd{margin:0;overflow-wrap:anywhere}.preset-adjust{position:absolute;right:10px;top:10px;display:inline-flex;padding:4px 8px;border:1px solid var(--line);border-radius:999px;color:var(--accent);text-decoration:none;font-size:11px;font-weight:500;background:var(--panel-2)}.preset-adjust:hover{border-color:var(--accent);background:var(--chip)}.preset-config{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}.preset-config summary{cursor:pointer;color:var(--accent);font-size:12px;font-weight:500}.preset-config pre{max-height:190px;overflow:auto;margin:9px 0 0;padding:10px;border-radius:12px;background:var(--chip);font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;scrollbar-width:thin;scrollbar-color:var(--line-strong,var(--line)) transparent}.preset-config pre::-webkit-scrollbar{width:8px;height:8px}.preset-config pre::-webkit-scrollbar-track{background:transparent}.preset-config pre::-webkit-scrollbar-thumb{background:var(--line);border:2px solid transparent;border-radius:999px;background-clip:padding-box}.preset-config pre::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:padding-box}.modal-actions{justify-content:flex-end;margin:14px 0 0;padding:0}.toast-region{position:fixed;right:18px;bottom:18px;display:grid;gap:8px;z-index:30}.dialog-toast-region{position:absolute;left:50%;top:14px;transform:translateX(-50%);width:min(420px,calc(100% - 32px));display:grid;justify-items:center;gap:8px;z-index:10000;pointer-events:none}.toast{padding:9px 12px;border:1px solid var(--line);border-radius:13px;background:var(--panel-2);box-shadow:var(--shadow);font-size:12.5px;animation:toast-in .22s ease both,toast-out .28s ease 3.25s forwards}.dialog-toast-region .toast{pointer-events:auto;animation:dialog-toast-in .22s ease both,dialog-toast-out .28s ease 3.25s forwards}@keyframes toast-in{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes dialog-toast-in{from{opacity:0;transform:translateY(-8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes toast-out{to{opacity:0;transform:translateY(6px) scale(.98)}}@keyframes dialog-toast-out{to{opacity:0;transform:translateY(6px) scale(.98)}}@media(max-width:1080px){body{overflow:auto}.shell{height:auto;min-height:100vh}.workspace,.workspace.drawer-open{grid-template-columns:1fr}.drawer{display:none}.workspace.drawer-open .drawer{display:flex;position:fixed;inset:10px;z-index:20;overflow:hidden}.board-toolbar{position:static;align-items:stretch;flex-direction:column}.board-tools-left,.board-tools-right{width:100%;overflow:auto}.search-wrap{width:100%}.summary-grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:760px){.preset-layout{grid-template-columns:1fr}.preset-picker{max-height:330px}.preset-list{max-height:210px;overflow:auto}}.ccr-checklist{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:10px}.check{display:flex;align-items:center;gap:7px;padding:7px 9px;border:1px solid var(--line);border-radius:12px;background:var(--panel-2);font-size:11.5px}.check span{width:18px;height:18px;display:grid;place-items:center;border-radius:999px;font-family:var(--mono);font-size:10px;font-weight:800}.check.ok span{background:color-mix(in srgb,var(--green) 16%,transparent);color:var(--green)}.check.warn span{background:color-mix(in srgb,var(--orange) 16%,transparent);color:var(--orange)}.ccr-readiness{margin:12px 0 0;padding:10px;border:1px solid var(--line);border-radius:14px;background:var(--panel-2)}.ccr-readiness .hint{margin:8px 0}.ccr-readiness a.ghost{text-decoration:none}.ccr-readiness .tiny{margin-right:5px}@media(max-width:720px){.shell{width:calc(100% - 16px);padding:8px 0}.topbar{align-items:flex-start}.summary-grid{grid-template-columns:repeat(2,1fr)}.cards{grid-template-columns:1fr}.filters,.view-switch{overflow:auto;flex-wrap:nowrap}.chip{white-space:nowrap}.top-actions{justify-content:flex-end}.brand-mark{display:none}}
|