Initial commit: Helix Git GUI with Apple design, Electron 43, i18n support

This commit is contained in:
TuxMonkey
2026-08-03 21:02:17 +08:00
commit 122aa87154
10 changed files with 5267 additions and 0 deletions

169
src/i18n.js Normal file
View File

@@ -0,0 +1,169 @@
window._ = (() => {
const langs = {
'zh-CN': {
appTitle: 'Helix',
sidebarRepos: '仓库',
sectionBranches: '分支',
sectionWorkingDir: '工作区',
openRepo: '打开仓库',
noBranches: '打开仓库以查看分支',
newBranch: '+ 新建分支',
fetch: '获取',
pull: '拉取',
push: '推送',
cleanTree: '工作区干净',
noChanges: '没有变更',
commitHistory: '提交历史',
changes: '变更',
unstaged: '未暂存',
staged: '已暂存',
diff: '差异',
selectDetail: '选择提交或文件查看详情',
refresh: '刷新',
commit: '提交',
welcomeTitle: '欢迎使用 Helix',
welcomeDesc: '打开 Git 仓库以可视化提交历史、管理分支并查看变更 — 一切尽在流畅的界面中。',
ready: '就绪',
refreshing: '刷新中...',
on: 'on',
switchedTo: '已切换到',
createdBranch: '已创建分支',
stagedF: '已暂存',
unstagedF: '已取消暂存',
discardedF: '已丢弃',
committed: '已提交',
fetched: '已获取',
pulled: '已拉取',
pushed: '已推送',
failedStatus: '加载状态失败',
failedBranches: '加载分支失败',
failedCommits: '加载提交失败',
failedDiff: '加载差异失败',
failedStage: '暂存失败',
failedUnstage: '取消暂存失败',
failedDiscard: '丢弃失败',
failedCommit: '提交失败',
failedFetch: '获取失败',
failedPull: '拉取失败',
failedPush: '推送失败',
failedSwitch: '切换失败',
failedCreateBranch: '创建分支失败',
createBranch: '新建分支',
createBranchDesc: '从当前分支创建新分支。',
branchName: '分支名称',
cancel: '取消',
create: '创建',
commitChanges: '提交更改',
commitDesc: '使用已暂存的更改创建提交。',
commitMessage: '提交信息',
discardTitle: '丢弃更改',
discardDesc: '确定要丢弃对',
discardDesc2: '的更改吗?此操作无法撤销。',
discardBtn: '丢弃',
stageBtn: '暂存',
unstageBtn: '取消暂存',
discardFileBtn: '丢弃',
settings: '设置',
language: '语言',
systemDefault: '系统默认字体',
cnFont: 'MiSans(中文字体)',
languageLabel: '界面语言',
themeLabel: '主题',
followSystem: '跟随系统'
},
'en': {
appTitle: 'Helix',
sidebarRepos: 'Repositories',
sectionBranches: 'Branches',
sectionWorkingDir: 'Working Directory',
openRepo: 'Open Repository',
noBranches: 'Open a repository to view branches',
newBranch: '+ New Branch',
fetch: 'Fetch',
pull: 'Pull',
push: 'Push',
cleanTree: 'Working tree clean',
noChanges: 'No changes',
commitHistory: 'Commit History',
changes: 'Changes',
unstaged: 'Unstaged',
staged: 'Staged',
diff: 'Diff',
selectDetail: 'Select a commit or file to view details',
refresh: 'Refresh',
commit: 'Commit',
welcomeTitle: 'Welcome to Helix',
welcomeDesc: 'Open a Git repository to visualize your commit history, manage branches, and review changes — all in one fluid interface.',
ready: 'Ready',
refreshing: 'Refreshing...',
on: 'on',
switchedTo: 'Switched to',
createdBranch: 'Created branch',
stagedF: 'Staged',
unstagedF: 'Unstaged',
discardedF: 'Discarded',
committed: 'Committed',
fetched: 'Fetched',
pulled: 'Pulled',
pushed: 'Pushed',
failedStatus: 'Failed to load status',
failedBranches: 'Failed to load branches',
failedCommits: 'Failed to load commits',
failedDiff: 'Failed to load diff',
failedStage: 'Failed to stage',
failedUnstage: 'Failed to unstage',
failedDiscard: 'Failed to discard',
failedCommit: 'Commit failed',
failedFetch: 'Fetch failed',
failedPull: 'Pull failed',
failedPush: 'Push failed',
failedSwitch: 'Failed to switch',
failedCreateBranch: 'Failed to create branch',
createBranch: 'Create Branch',
createBranchDesc: 'Create a new branch from the current branch.',
branchName: 'Branch name',
cancel: 'Cancel',
create: 'Create',
commitChanges: 'Commit Changes',
commitDesc: 'Create a commit with your staged changes.',
commitMessage: 'Commit message',
discardTitle: 'Discard Changes',
discardDesc: 'Are you sure you want to discard changes to',
discardDesc2: '? This cannot be undone.',
discardBtn: 'Discard',
stageBtn: 'Stage',
unstageBtn: 'Unstage',
discardFileBtn: 'Discard',
settings: 'Settings',
language: 'Language',
systemDefault: 'System Default Font',
cnFont: 'MiSans (Chinese Font)',
languageLabel: 'Interface Language',
themeLabel: 'Theme',
followSystem: 'Follow System'
}
};
let currentLang = 'zh-CN';
function t(key) {
return langs[currentLang]?.[key] || langs['en']?.[key] || key;
}
function setLang(lang) {
if (langs[lang]) {
currentLang = lang;
document.documentElement.lang = lang;
return true;
}
return false;
}
function getLang() {
return currentLang;
}
return { t, setLang, getLang, langs };
})();
window.i18nReady = true;

144
src/index.html Normal file
View File

@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Helix</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="title-bar">
<div class="title-bar-drag"></div>
<div class="title-bar-title">Helix</div>
<div class="title-bar-controls">
<button class="title-btn" id="btn-minimize" title="Minimize">
<svg width="12" height="12" viewBox="0 0 12 12"><rect x="1" y="5.5" width="10" height="1" fill="currentColor"/></svg>
</button>
<button class="title-btn" id="btn-maximize" title="Maximize">
<svg width="12" height="12" viewBox="0 0 12 12"><rect x="1.5" y="1.5" width="9" height="9" rx="1" fill="none" stroke="currentColor"/></svg>
</button>
<button class="title-btn title-btn-close" id="btn-close" title="Close">
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M1 1l10 10M11 1L1 11" stroke="currentColor" stroke-width="1.2"/></svg>
</button>
</div>
</div>
<div id="app">
<section id="sidebar">
<div class="sidebar-header">
<button id="open-repo-btn" class="btn-icon" title="打开仓库">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M15.5 5.5H5.5C4.95 5.5 4.5 5.95 4.5 6.5V14.5C4.5 15.05 4.95 15.5 5.5 15.5H13.5C14.05 15.5 14.5 15.05 14.5 14.5V2.5C14.5 1.95 14.05 1.5 13.5 1.5H2.5C1.95 1.5 1.5 1.95 1.5 2.5V14.5C1.5 15.05 1.95 15.5 2.5 15.5"/>
</svg>
</button>
<span class="sidebar-title" data-i18n="sidebarRepos">仓库</span>
<button id="settings-btn" class="btn-icon btn-settings" title="设置">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round">
<circle cx="8" cy="8" r="2.5"/>
<path d="M8 1.5v1M8 13.5v1M1.5 8h1M13.5 8h1M3.4 3.4l.7.7M11.9 11.9l.7.7M3.4 12.6l.7-.7M11.9 4.1l.7-.7"/>
</svg>
</button>
</div>
<div id="repo-info" class="repo-info hidden">
<div class="repo-name" id="repo-name"></div>
<div class="repo-branch" id="repo-branch"></div>
</div>
<div class="sidebar-section">
<div class="section-label" data-i18n="sectionBranches">分支</div>
<div id="branch-list" class="branch-list">
<div class="empty-state" data-i18n="noBranches">打开仓库以查看分支</div>
</div>
<button id="new-branch-btn" class="btn-secondary hidden" data-i18n="newBranch">+ 新建分支</button>
</div>
<div class="sidebar-section">
<div class="section-label" data-i18n="sectionWorkingDir">工作区</div>
<div id="status-summary" class="status-summary">
<div class="empty-state" data-i18n="cleanTree">工作区干净</div>
</div>
</div>
<div class="sidebar-actions">
<button id="fetch-btn" class="btn-secondary hidden" data-i18n="fetch">获取</button>
<button id="pull-btn" class="btn-secondary hidden" data-i18n="pull">拉取</button>
<button id="push-btn" class="btn-secondary hidden" data-i18n="push">推送</button>
</div>
</section>
<main id="main-content">
<header id="toolbar">
<div class="toolbar-left">
<button id="refresh-btn" class="btn-icon" title="刷新">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M14 8a6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6"/>
<polyline points="10,1 14,1 14,5"/>
</svg>
</button>
<div class="toolbar-title" id="toolbar-title">Helix</div>
</div>
<div class="toolbar-right" id="toolbar-actions"></div>
</header>
<div id="content-area">
<div id="commit-section" class="panel">
<div class="panel-header">
<span data-i18n="commitHistory">提交历史</span>
<span class="badge" id="commit-count">0</span>
</div>
<div id="commit-list" class="commit-list">
<div class="welcome-state">
<div class="welcome-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.2" opacity="0.4">
<circle cx="24" cy="24" r="20"/>
<path d="M24 4a20 20 0 0 1 0 40"/>
<circle cx="24" cy="24" r="6"/>
</svg>
</div>
<h2 id="welcome-title" data-i18n="welcomeTitle">欢迎使用 Helix</h2>
<p id="welcome-desc" data-i18n="welcomeDesc">打开 Git 仓库以可视化提交历史、管理分支并查看变更 — 一切尽在流畅的界面中。</p>
<button id="welcome-open-btn" class="btn-primary" data-i18n="openRepo">打开仓库</button>
</div>
</div>
</div>
<div id="detail-section" class="panel">
<div class="panel-header">
<span data-i18n="changes">变更</span>
<div class="panel-tabs">
<button class="tab active" data-tab="unstaged" data-i18n="unstaged">未暂存</button>
<button class="tab" data-tab="staged" data-i18n="staged">已暂存</button>
<button class="tab" data-tab="diff" data-i18n="diff">差异</button>
</div>
</div>
<div id="detail-content" class="detail-content">
<div class="empty-state" data-i18n="selectDetail">选择提交或文件查看详情</div>
</div>
</div>
</div>
<footer id="status-bar">
<div class="status-left" id="status-left" data-i18n="ready">就绪</div>
<div class="status-right"><span id="status-branch"></span></div>
</footer>
</main>
</div>
<div id="modal-overlay" class="modal-overlay hidden">
<div class="modal">
<div class="modal-header">
<span id="modal-title"></span>
<button class="btn-icon modal-close" id="modal-close">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2">
<line x1="1" y1="1" x2="13" y2="13"/><line x1="13" y1="1" x2="1" y2="13"/>
</svg>
</button>
</div>
<div id="modal-content"></div>
<div class="modal-footer" id="modal-footer"></div>
</div>
</div>
<div id="toast" class="toast hidden"></div>
<script src="i18n.js"></script>
<script src="renderer.js"></script>
</body>
</html>

438
src/renderer.js Normal file
View File

@@ -0,0 +1,438 @@
(function () {
const { helix } = window;
const { t, setLang, getLang } = window._;
let state = {
repoPath: null,
branches: [],
status: { branch: '', files: [] },
commits: [],
selectedCommit: null,
activeTab: 'unstaged'
};
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
function applyI18n() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
el.textContent = t(key);
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.dataset.i18nTitle);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.dataset.i18nPlaceholder);
});
document.getElementById('welcome-title').textContent = t('welcomeTitle');
document.getElementById('welcome-desc').textContent = t('welcomeDesc');
document.getElementById('status-left').textContent = state.repoPath ? t('ready') : t('ready');
if (!state.repoPath) {
$('#appTitle') || placeholderCheck();
}
}
function placeholderCheck() {
const statusLeft = document.getElementById('status-left');
if (statusLeft && statusLeft.textContent === '\u5C31\u7EEA') statusLeft.textContent = t('ready');
}
function showToast(message) {
var toast = $('#toast');
toast.textContent = message;
toast.classList.add('visible');
clearTimeout(toast._timeout);
toast._timeout = setTimeout(function () { toast.classList.remove('visible'); }, 2200);
}
function showModal(title, content, footer) {
$('#modal-title').textContent = title;
$('#modal-content').innerHTML = content;
$('#modal-footer').innerHTML = footer || '';
$('#modal-overlay').classList.remove('hidden');
requestAnimationFrame(function () {
$('#modal-overlay').classList.add('visible');
});
}
function hideModal() {
$('#modal-overlay').classList.remove('visible');
setTimeout(function () { $('#modal-overlay').classList.add('hidden'); }, 220);
}
$('#modal-close').addEventListener('click', hideModal);
$('#modal-overlay').addEventListener('click', function (e) {
if (e.target === $('#modal-overlay')) hideModal();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') hideModal();
});
async function openRepo() {
try {
const repoPath = await helix.openRepo();
if (!repoPath) return;
state.repoPath = repoPath;
const name = repoPath.split(/[/\\]/).pop();
$('#repo-name').textContent = name;
$('#repo-info').classList.remove('hidden');
$('#toolbar-title').textContent = name;
$$('.sidebar-actions .btn-secondary').forEach(function (b) { b.classList.remove('hidden'); });
$('#new-branch-btn').classList.remove('hidden');
await refreshAll();
} catch (err) {
showToast(t('failedStatus'));
}
}
async function refreshAll() {
if (!state.repoPath) return;
$('#status-left').textContent = t('refreshing');
try {
await Promise.all([loadStatus(), loadBranches(), loadCommits()]);
} catch (e) { /* ignore individual failures */ }
$('#status-left').textContent = t('ready');
}
async function loadStatus() {
if (!state.repoPath) return;
try {
state.status = await helix.getStatus(state.repoPath);
renderStatus();
renderFileList();
$('#repo-branch').textContent = state.status.branch || '\u2014';
$('#status-branch').textContent = state.status.branch ? (t('on') + ' ' + state.status.branch) : '\u2014';
} catch (e) { showToast(t('failedStatus')); }
}
async function loadBranches() {
if (!state.repoPath) return;
try {
state.branches = await helix.getBranches(state.repoPath);
renderBranches();
} catch (e) { showToast(t('failedBranches')); }
}
async function loadCommits() {
if (!state.repoPath) return;
try {
state.commits = await helix.getLog(state.repoPath);
renderCommits();
$('#commit-count').textContent = state.commits.length;
} catch (e) { showToast(t('failedCommits')); }
}
function renderBranches() {
var container = $('#branch-list');
if (state.branches.length === 0) {
container.innerHTML = '<div class="empty-state">' + t('noBranches') + '</div>';
return;
}
container.innerHTML = state.branches.map(function (b) {
var classes = ['branch-item'];
if (b.isCurrent) classes.push('current');
return '<div class="' + classes.join(' ') + '" data-branch="' + escapeAttr(b.name) + '"><span>' + escapeHtml(b.name) + '</span><span class="branch-hash">' + escapeHtml(b.hash || '') + '</span></div>';
}).join('');
container.querySelectorAll('.branch-item').forEach(function (el) {
el.addEventListener('click', async function () {
var branch = el.dataset.branch;
try {
await helix.checkout(state.repoPath, branch);
showToast(t('switchedTo') + ' ' + branch);
await refreshAll();
} catch (e) { showToast(t('failedSwitch') + ': ' + e.message); }
});
});
}
function renderStatus() {
var container = $('#status-summary');
var files = state.status.files;
if (files.length === 0) {
container.innerHTML = '<div class="empty-state">' + t('cleanTree') + '</div>';
return;
}
container.innerHTML = files.map(function (f) {
var mc = getMarkerClass(f.status);
return '<div class="status-file" data-file="' + escapeAttr(f.filename) + '" data-status="' + f.status + '"><span class="status-marker ' + mc + '">' + f.status + '</span><span>' + escapeHtml(f.filename) + '</span></div>';
}).join('');
}
function getMarkerClass(status) {
if (status && status.indexOf('A') >= 0) return 'added';
if (status && status.indexOf('D') >= 0) return 'deleted';
if (status && status.indexOf('R') >= 0) return 'renamed';
if (status && status.indexOf('M') >= 0) return 'modified';
if (status === '??') return 'untracked';
return 'modified';
}
function renderCommits() {
var container = $('#commit-list');
if (state.commits.length === 0) return;
container.innerHTML = state.commits.map(function (c) {
var initials = (c.author || '?').split(' ').map(function (w) { return w[0]; }).join('').slice(0, 2).toUpperCase();
var refs = parseRefs(c.refs);
return '<div class="commit-item" data-hash="' + c.hash + '">' +
'<div class="commit-avatar">' + initials + '</div>' +
'<div class="commit-body">' +
'<div class="commit-subject">' + escapeHtml(c.subject) + '</div>' +
'<div class="commit-meta">' +
'<span class="commit-author">' + escapeHtml(c.author) + '</span>' +
'<span class="commit-hash">' + escapeHtml(c.shortHash || '') + '</span>' +
'<span>' + escapeHtml(c.date || '') + '</span>' +
'</div>' +
(refs.length ? '<div class="commit-refs">' + refs.map(function (r) { return '<span class="ref-tag ' + r.type + '">' + escapeHtml(r.name) + '</span>'; }).join('') + '</div>' : '') +
'</div></div>';
}).join('');
container.querySelectorAll('.commit-item').forEach(function (el) {
el.addEventListener('click', async function () {
container.querySelectorAll('.commit-item').forEach(function (e) { e.classList.remove('selected'); });
el.classList.add('selected');
state.selectedCommit = el.dataset.hash;
state.activeTab = 'diff';
$$('.tab').forEach(function (t) { t.classList.remove('active'); });
var diffTab = document.querySelector('.tab[data-tab="diff"]');
if (diffTab) diffTab.classList.add('active');
await loadDiff(state.selectedCommit);
});
});
}
function parseRefs(refsStr) {
if (!refsStr) return [];
return refsStr.split(',').map(function (r) {
var trimmed = r.trim();
if (trimmed.indexOf('HEAD ->') === 0) return { name: trimmed, type: 'branch' };
if (trimmed.indexOf('tag:') === 0) return { name: trimmed, type: 'branch' };
if (trimmed.indexOf('origin/') >= 0) return { name: trimmed, type: 'remote' };
return { name: trimmed, type: 'branch' };
});
}
async function loadDiff(hash) {
if (!state.repoPath) return;
try {
var diff = await helix.getDiff(state.repoPath, hash || null);
var content = $('#detail-content');
content.innerHTML = diff ? formatDiff(diff) : '<div class="empty-state">' + t('noChanges') + '</div>';
} catch (e) {
$('#detail-content').innerHTML = '<div class="empty-state">' + t('failedDiff') + '</div>';
}
}
function formatDiff(text) {
return text.split('\n').map(function (line) {
if (line.match(/^\s+\S.*\|/)) return '<div class="diff-file">' + escapeHtml(line) + '</div>';
return escapeHtml(line);
}).join('\n');
}
async function renderFileList() {
var content = $('#detail-content');
var files = state.status.files;
var filtered;
if (state.activeTab === 'staged') {
filtered = files.filter(function (f) { return f.status !== '??' && f.status !== ' M' && f.status !== ' D'; });
} else if (state.activeTab === 'unstaged') {
filtered = files.filter(function (f) { return f.status === '??' || f.status === ' M' || f.status === ' D'; });
} else {
filtered = files;
}
if (filtered.length === 0) {
content.innerHTML = '<div class="empty-state">' + t('noChanges') + '</div>';
return;
}
content.innerHTML = filtered.map(function (f) {
return '<div class="file-list-item" data-file="' + escapeAttr(f.filename) + '" data-status="' + f.status + '">' +
'<span><span class="status-marker ' + getMarkerClass(f.status) + '">' + f.status + '</span> ' + escapeHtml(f.filename) + '</span>' +
'<span class="file-actions">' +
(state.activeTab === 'unstaged' ? '<button class="btn-small stage-btn">' + t('stageBtn') + '</button>' : '') +
(state.activeTab === 'staged' ? '<button class="btn-small unstage-btn">' + t('unstageBtn') + '</button>' : '') +
(state.activeTab === 'unstaged' ? '<button class="btn-small discard-btn">' + t('discardFileBtn') + '</button>' : '') +
'</span></div>';
}).join('');
content.querySelectorAll('.stage-btn').forEach(function (btn) {
btn.addEventListener('click', async function (e) {
e.stopPropagation();
var file = btn.closest('.file-list-item').dataset.file;
try { await helix.stage(state.repoPath, [file]); showToast(t('stagedF') + ' ' + file); await refreshAll(); }
catch (e) { showToast(t('failedStage')); }
});
});
content.querySelectorAll('.unstage-btn').forEach(function (btn) {
btn.addEventListener('click', async function (e) {
e.stopPropagation();
var file = btn.closest('.file-list-item').dataset.file;
try { await helix.unstage(state.repoPath, [file]); showToast(t('unstagedF') + ' ' + file); await refreshAll(); }
catch (e) { showToast(t('failedUnstage')); }
});
});
content.querySelectorAll('.discard-btn').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var file = btn.closest('.file-list-item').dataset.file;
showModal(t('discardTitle'),
'<p style="font-size:0.875rem;color:var(--text-secondary);margin-bottom:8px">' + t('discardDesc') + ' <strong>' + escapeHtml(file) + '</strong>' + t('discardDesc2') + '</p>',
'<button class="btn-secondary cancel-discard-btn">' + t('cancel') + '</button><button class="btn-primary confirm-discard-btn" style="background:var(--red)">' + t('discardBtn') + '</button>');
document.querySelector('.cancel-discard-btn').addEventListener('click', hideModal);
document.querySelector('.confirm-discard-btn').addEventListener('click', async function () {
hideModal();
try { await helix.discard(state.repoPath, file); showToast(t('discardedF') + ' ' + file); await refreshAll(); }
catch (e) { showToast(t('failedDiscard')); }
});
});
});
}
$('#detail-section').addEventListener('click', function (e) {
if (e.target.classList.contains('tab')) {
$$('.tab').forEach(function (t) { t.classList.remove('active'); });
e.target.classList.add('active');
state.activeTab = e.target.dataset.tab;
state.selectedCommit = null;
$$('.commit-item').forEach(function (el) { el.classList.remove('selected'); });
if (state.activeTab === 'diff' && state.selectedCommit) {
loadDiff(state.selectedCommit);
} else {
renderFileList();
}
}
});
$('#open-repo-btn').addEventListener('click', openRepo);
$('#welcome-open-btn').addEventListener('click', openRepo);
$('#refresh-btn').addEventListener('click', refreshAll);
$('#new-branch-btn').addEventListener('click', function () {
showModal(t('createBranch'),
'<p style="font-size:0.8125rem;color:var(--text-secondary);margin-bottom:12px">' + t('createBranchDesc') + '</p>' +
'<input class="modal-input" id="branch-name-input" placeholder="' + t('branchName') + '" autofocus>',
'<button class="btn-secondary cancel-branch-btn">' + t('cancel') + '</button><button class="btn-primary confirm-branch-btn">' + t('create') + '</button>');
document.querySelector('.cancel-branch-btn').addEventListener('click', hideModal);
document.querySelector('.confirm-branch-btn').addEventListener('click', async function () {
var name = document.getElementById('branch-name-input').value.trim();
if (!name) return;
hideModal();
try { await helix.createBranch(state.repoPath, name); showToast(t('createdBranch') + ' ' + name); await refreshAll(); }
catch (e) { showToast(t('failedCreateBranch') + ': ' + e.message); }
});
});
$('#fetch-btn').addEventListener('click', async function () {
try { await helix.fetch(state.repoPath); showToast(t('fetched')); await refreshAll(); }
catch (e) { showToast(t('failedFetch')); }
});
$('#pull-btn').addEventListener('click', async function () {
try { await helix.pull(state.repoPath); showToast(t('pulled')); await refreshAll(); }
catch (e) { showToast(t('failedPull') + ': ' + e.message); }
});
$('#push-btn').addEventListener('click', async function () {
try { await helix.push(state.repoPath); showToast(t('pushed')); await refreshAll(); }
catch (e) { showToast(t('failedPush') + ': ' + e.message); }
});
function showCommitDialog() {
showModal(t('commitChanges'),
'<p style="font-size:0.8125rem;color:var(--text-secondary);margin-bottom:12px">' + t('commitDesc') + '</p>' +
'<input class="modal-input" id="commit-message-input" placeholder="' + t('commitMessage') + '" autofocus>',
'<button class="btn-secondary cancel-commit-btn">' + t('cancel') + '</button><button class="btn-primary confirm-commit-btn">' + t('commit') + '</button>');
document.querySelector('.cancel-commit-btn').addEventListener('click', hideModal);
document.querySelector('.confirm-commit-btn').addEventListener('click', async function () {
var message = document.getElementById('commit-message-input').value.trim();
if (!message) return;
hideModal();
try { await helix.commit(state.repoPath, message); showToast(t('committed')); await refreshAll(); }
catch (e) { showToast(t('failedCommit') + ': ' + e.message); }
});
}
var commitBtn = document.createElement('button');
commitBtn.className = 'btn-primary';
commitBtn.id = 'commit-btn';
commitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 1v9M4 7l3 3 3-3"/></svg> ' + t('commit');
commitBtn.addEventListener('click', showCommitDialog);
$('#toolbar-actions').appendChild(commitBtn);
function escapeHtml(str) {
if (!str) return '';
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function escapeAttr(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/* ── Settings ── */
$('#settings-btn').addEventListener('click', function () {
var currentLang = getLang();
showModal(t('settings'),
'<div class="settings-section">' +
'<label>' + t('languageLabel') + '</label>' +
'<div class="settings-row">' +
'<button class="settings-option' + (currentLang === 'zh-CN' ? ' active' : '') + '" data-lang="zh-CN">\u4e2d\u6587</button>' +
'<button class="settings-option' + (currentLang === 'en' ? ' active' : '') + '" data-lang="en">English</button>' +
'</div>' +
'</div>',
'<button class="btn-secondary settings-close-btn">' + t('cancel') + '</button><button class="btn-primary settings-save-btn">\u2714</button>');
document.querySelector('.settings-close-btn').addEventListener('click', hideModal);
document.querySelectorAll('.settings-option').forEach(function (btn) {
btn.addEventListener('click', function () {
document.querySelectorAll('.settings-option').forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active');
});
});
document.querySelector('.settings-save-btn').addEventListener('click', async function () {
var selected = document.querySelector('.settings-option.active');
var newLang = selected ? selected.dataset.lang : 'zh-CN';
if (newLang !== getLang()) {
setLang(newLang);
await helix.saveSettings({ language: newLang });
applyI18n();
commitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 1v9M4 7l3 3 3-3"/></svg> ' + t('commit');
if (state.repoPath) await refreshAll();
}
hideModal();
});
document.addEventListener('keydown', function handler(e) {
if (e.key === 'Escape') { hideModal(); document.removeEventListener('keydown', handler); }
});
});
/* ── Init ── */
async function init() {
try {
var settings = await helix.loadSettings();
if (settings && settings.language) {
setLang(settings.language);
document.documentElement.lang = settings.language;
}
} catch (e) { /* default zh-CN */ }
applyI18n();
commitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 1v9M4 7l3 3 3-3"/></svg> ' + t('commit');
document.getElementById('status-left').textContent = t('ready');
/* ── Title bar buttons ── */
document.getElementById('btn-minimize').addEventListener('click', function () { helix.minimize(); });
document.getElementById('btn-maximize').addEventListener('click', function () { helix.maximize(); });
document.getElementById('btn-close').addEventListener('click', function () { helix.close(); });
}
init();
})();

632
src/styles.css Normal file
View File

@@ -0,0 +1,632 @@
@font-face {
font-family: 'MiSans';
src: local('MiSans'), local('MiSans Regular');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'MiSans';
src: local('MiSans Medium'), local('MiSans-Medium');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'MiSans';
src: local('MiSans Semibold'), local('MiSans-Semibold');
font-weight: 600;
font-style: normal;
}
:root {
--font-display: "MiSans", "SF Pro Display", "Microsoft YaHei UI", "PingFang SC", system-ui, -apple-system, sans-serif;
--font-body: "MiSans", "SF Pro Text", "Microsoft YaHei UI", "PingFang SC", system-ui, -apple-system, sans-serif;
--font-mono: "Cascadia Code", "SF Mono", "Fira Code", "Consolas", monospace;
--blur-sidebar: 12px;
--blur-toolbar: 10px;
--blur-panel: 8px;
--blur-modal: 30px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 24px;
font: 100%/1.6 var(--font-body);
color-scheme: light dark;
}
@media (prefers-color-scheme: light) {
:root {
--bg-root: #f5f5f7;
--bg-sidebar: #f0f0f3;
--bg-toolbar: #fafafc;
--bg-panel: #ffffff;
--bg-panel-alt: #fafafc;
--bg-hover: rgba(0, 0, 0, 0.05);
--bg-active: rgba(0, 0, 0, 0.08);
--bg-selected: rgba(0, 122, 255, 0.1);
--bg-modal: rgba(255, 255, 255, 0.97);
--bg-tag: rgba(0, 0, 0, 0.06);
--bg-red-soft: rgba(255, 59, 48, 0.08);
--bg-green-soft: rgba(52, 199, 89, 0.08);
--bg-blue-soft: rgba(0, 122, 255, 0.08);
--bg-titlebar: #f0f0f3;
--text-primary: #1d1d1f;
--text-secondary: #6e6e73;
--text-tertiary: #aeaeb2;
--text-link: #0066cc;
--border-subtle: rgba(0, 0, 0, 0.08);
--border-soft: rgba(0, 0, 0, 0.12);
--border-prominent: rgba(0, 0, 0, 0.2);
--accent: #007aff;
--accent-hover: #0070e9;
--accent-pressed: #0062cc;
--red: #ff3b30;
--green: #34c759;
--orange: #ff9500;
--blue: #007aff;
--yellow: #ffcc00;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.08);
--shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.15);
--scrim: rgba(0, 0, 0, 0.2);
}
}
@media (prefers-color-scheme: dark) {
:root {
--bg-root: #1c1c1e;
--bg-sidebar: #252528;
--bg-toolbar: #2c2c2e;
--bg-panel: #2c2c2e;
--bg-panel-alt: #242426;
--bg-hover: rgba(255, 255, 255, 0.06);
--bg-active: rgba(255, 255, 255, 0.1);
--bg-selected: rgba(10, 132, 255, 0.2);
--bg-modal: rgba(44, 44, 46, 0.98);
--bg-tag: rgba(255, 255, 255, 0.08);
--bg-red-soft: rgba(255, 69, 58, 0.12);
--bg-green-soft: rgba(48, 209, 88, 0.12);
--bg-blue-soft: rgba(10, 132, 255, 0.12);
--bg-titlebar: #252528;
--text-primary: #f5f5f7;
--text-secondary: #98989d;
--text-tertiary: #636366;
--text-link: #409cff;
--border-subtle: rgba(255, 255, 255, 0.06);
--border-soft: rgba(255, 255, 255, 0.1);
--border-prominent: rgba(255, 255, 255, 0.18);
--accent: #0a84ff;
--accent-hover: #409cff;
--accent-pressed: #0070d6;
--red: #ff453a;
--green: #30d158;
--orange: #ff9f0a;
--blue: #0a84ff;
--yellow: #ffd60a;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.5);
--shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.6);
--scrim: rgba(0, 0, 0, 0.5);
}
}
*, *::before, *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
html, body {
height: 100%; overflow: hidden;
background: var(--bg-root);
color: var(--text-primary);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
user-select: none;
}
#title-bar {
display: flex;
align-items: center;
height: 32px;
background: var(--bg-titlebar);
border-bottom: 1px solid var(--border-subtle);
-webkit-app-region: drag;
z-index: 100;
position: relative;
}
.title-bar-drag {
flex: 1;
height: 100%;
}
.title-bar-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
font-weight: 590;
color: var(--text-secondary);
white-space: nowrap;
pointer-events: none;
}
.title-bar-controls {
display: flex;
height: 100%;
-webkit-app-region: no-drag;
}
.title-btn {
width: 46px;
height: 100%;
border: none;
background: transparent;
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 80ms;
}
.title-btn:hover { background: var(--bg-hover); }
.title-btn-close:hover { background: var(--red); color: white; }
#app {
display: grid;
grid-template-columns: 280px 1fr;
height: calc(100vh - 32px);
overflow: hidden;
}
#sidebar {
background: var(--bg-sidebar);
border-right: 1px solid var(--border-subtle);
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 0 var(--spacing-md);
}
.sidebar-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) 0;
}
.sidebar-title {
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-tertiary);
flex: 1;
}
.btn-settings { margin-left: auto; }
.repo-info {
padding: var(--spacing-md) 0;
border-bottom: 1px solid var(--border-subtle);
margin-bottom: var(--spacing-md);
}
.repo-name {
font-size: 1rem;
font-weight: 590;
color: var(--text-primary);
line-height: 1.2;
word-break: break-all;
}
.repo-branch {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 2px;
}
.sidebar-section { margin-bottom: var(--spacing-lg); }
.section-label {
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-tertiary);
margin-bottom: var(--spacing-xs);
padding: 0 var(--spacing-xs);
}
.branch-list { display: flex; flex-direction: column; gap: 1px; }
.branch-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px var(--spacing-sm);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8125rem;
transition: background 80ms;
}
.branch-item:hover { background: var(--bg-hover); }
.branch-item:active { background: var(--bg-active); transform: scale(0.97); }
.branch-item.current { background: var(--bg-selected); font-weight: 500; color: var(--accent); }
.branch-hash { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-tertiary); }
.status-file {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: 4px var(--spacing-sm);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.75rem;
font-family: var(--font-mono);
}
.status-file:hover { background: var(--bg-hover); }
.status-file:active { background: var(--bg-active); transform: scale(0.97); }
.status-marker {
font-weight: 600; font-size: 0.6875rem;
width: 16px; text-align: center; flex-shrink: 0;
}
.status-marker.modified { color: var(--orange); }
.status-marker.added { color: var(--green); }
.status-marker.deleted { color: var(--red); }
.status-marker.renamed { color: var(--blue); }
.status-marker.untracked { color: var(--text-tertiary); }
.sidebar-actions {
display: flex; gap: var(--spacing-xs);
padding: var(--spacing-md) 0;
border-top: 1px solid var(--border-subtle);
margin-top: auto;
}
.btn-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px;
border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text-primary);
cursor: pointer; transition: background 80ms;
}
.btn-icon:hover { background: var(--bg-hover); }
.btn-icon:active { transform: scale(0.92); background: var(--bg-active); }
.btn-primary {
display: inline-flex; align-items: center; justify-content: center;
gap: var(--spacing-xs); padding: 7px var(--spacing-lg);
border: none; border-radius: var(--radius-md);
background: var(--accent); color: white;
font-size: 0.8125rem; font-weight: 500;
cursor: pointer; transition: background 120ms;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { transform: scale(0.96); background: var(--accent-pressed); }
.btn-secondary {
display: inline-flex; align-items: center; justify-content: center;
gap: var(--spacing-xs); padding: 5px var(--spacing-md);
border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
background: rgba(128,128,128,0.08); color: var(--text-primary);
font-size: 0.75rem; font-weight: 500;
cursor: pointer; transition: background 80ms;
}
.btn-secondary:hover { background: var(--bg-active); }
.btn-secondary:active { transform: scale(0.96); }
.btn-small {
display: inline-flex; align-items: center;
padding: 2px 8px; border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text-secondary);
font-size: 0.6875rem; cursor: pointer;
}
.btn-small:hover { background: var(--bg-hover); color: var(--text-primary); }
#toolbar {
background: var(--bg-toolbar);
border-bottom: 1px solid var(--border-subtle);
display: flex; align-items: center; justify-content: space-between;
padding: 0 var(--spacing-lg); height: 44px;
}
.toolbar-left { display: flex; align-items: center; gap: var(--spacing-sm); }
.toolbar-title {
font-size: 0.9375rem; font-weight: 590;
color: var(--text-primary); letter-spacing: -0.01em;
}
.toolbar-right { display: flex; align-items: center; gap: var(--spacing-sm); }
#content-area {
display: grid;
grid-template-rows: 1fr 1fr;
height: calc(100vh - 32px - 44px - 24px);
overflow: hidden;
}
.panel {
display: flex; flex-direction: column;
overflow: hidden; border-bottom: 1px solid var(--border-subtle);
}
.panel:last-child { border-bottom: none; }
.panel-header {
display: flex; align-items: center; justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-lg);
font-size: 0.75rem; font-weight: 600;
letter-spacing: 0.02em; color: var(--text-secondary);
background: var(--bg-panel-alt);
border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; height: 36px;
}
.badge {
font-size: 0.625rem; font-weight: 600;
padding: 1px 6px; border-radius: 10px;
background: var(--bg-tag); color: var(--text-secondary);
}
.panel-tabs { display: flex; gap: 1px; }
.tab {
padding: 4px var(--spacing-md);
border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text-tertiary);
font-size: 0.6875rem; font-weight: 500;
cursor: pointer; transition: all 80ms;
}
.tab:hover { color: var(--text-primary); background: var(--bg-hover); }
.tab:active { transform: scale(0.96); }
.tab.active { color: var(--accent); background: var(--bg-selected); }
.commit-list { flex: 1; overflow-y: auto; }
.commit-item {
display: flex; align-items: flex-start;
gap: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg);
cursor: pointer; border-bottom: 1px solid var(--border-subtle);
transition: background 80ms;
}
.commit-item:hover { background: var(--bg-hover); }
.commit-item:active { background: var(--bg-active); transform: scale(0.995); }
.commit-item.selected { background: var(--bg-selected); }
.commit-avatar {
width: 28px; height: 28px; border-radius: 50%;
background: linear-gradient(135deg, var(--accent), #5856d6);
color: white; display: flex; align-items: center; justify-content: center;
font-size: 0.6875rem; font-weight: 600; flex-shrink: 0; margin-top: 2px;
}
.commit-body { flex: 1; min-width: 0; }
.commit-subject {
font-size: 0.875rem; font-weight: 500;
color: var(--text-primary); line-height: 1.3; letter-spacing: -0.01em;
}
.commit-meta {
display: flex; align-items: center; gap: var(--spacing-md);
margin-top: 4px; font-size: 0.75rem; color: var(--text-tertiary);
}
.commit-author { color: var(--text-secondary); font-weight: 500; }
.commit-hash { font-family: var(--font-mono); font-size: 0.6875rem; }
.commit-refs { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
.ref-tag {
font-size: 0.625rem; font-weight: 600; padding: 1px 6px;
border-radius: 4px; background: var(--bg-tag);
color: var(--text-secondary); font-family: var(--font-mono);
}
.ref-tag.branch { background: var(--bg-green-soft); color: var(--green); }
.ref-tag.remote { background: var(--bg-blue-soft); color: var(--blue); }
.detail-content {
flex: 1; overflow-y: auto; padding: var(--spacing-lg);
font-family: var(--font-mono); font-size: 0.8125rem;
line-height: 1.6; white-space: pre-wrap; word-break: break-all;
color: var(--text-secondary);
}
.diff-file {
color: var(--text-primary); font-weight: 600;
font-family: var(--font-body); font-size: 0.875rem;
margin-top: var(--spacing-md); letter-spacing: -0.01em;
}
.file-list-item {
display: flex; align-items: center; justify-content: space-between;
padding: 6px var(--spacing-sm); border-radius: var(--radius-sm);
cursor: pointer; font-size: 0.8125rem; font-family: var(--font-mono);
}
.file-list-item:hover { background: var(--bg-hover); }
.file-list-item:active { transform: scale(0.99); background: var(--bg-active); }
.file-actions { display: flex; gap: var(--spacing-xs); }
#status-bar {
background: var(--bg-toolbar);
border-top: 1px solid var(--border-subtle);
display: flex; align-items: center; justify-content: space-between;
padding: 0 var(--spacing-lg); height: 24px;
font-size: 0.6875rem; color: var(--text-tertiary);
}
.modal-overlay {
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
background: var(--scrim); opacity: 0; pointer-events: none;
transition: opacity 200ms;
}
.modal-overlay.visible { opacity: 1; pointer-events: auto; }
.modal {
background: var(--bg-modal);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-modal);
border: 1px solid var(--border-subtle);
width: 440px; max-height: 80vh;
display: flex; flex-direction: column; overflow: hidden;
transform: scale(0.94) translateY(12px);
transition: transform 250ms cubic-bezier(0.22, 1, 0.36, 1);
}
.modal-overlay.visible .modal { transform: scale(1) translateY(0); }
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: var(--spacing-lg); border-bottom: 1px solid var(--border-subtle);
}
.modal-header span {
font-size: 0.9375rem; font-weight: 590; letter-spacing: -0.01em;
}
#modal-content { padding: var(--spacing-lg); overflow-y: auto; flex: 1; }
.modal-footer {
display: flex; justify-content: flex-end; gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-lg);
border-top: 1px solid var(--border-subtle);
}
.modal-input {
width: 100%; padding: 8px var(--spacing-md);
border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
background: var(--bg-panel); color: var(--text-primary);
font-family: var(--font-body); font-size: 0.875rem;
outline: none; transition: border-color 120ms;
}
.modal-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
}
.toast {
position: fixed; bottom: 32px; left: 50%;
transform: translateX(-50%) translateY(16px);
background: var(--bg-modal);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: 10px var(--spacing-xl);
font-size: 0.8125rem; font-weight: 500;
color: var(--text-primary);
box-shadow: var(--shadow-lg);
z-index: 200; opacity: 0; pointer-events: none;
transition: all 250ms cubic-bezier(0.22, 1, 0.36, 1);
}
.toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
.welcome-state {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
height: 100%; padding: var(--spacing-xl);
text-align: center; gap: var(--spacing-md);
}
.welcome-icon { margin-bottom: var(--spacing-sm); }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@media (prefers-reduced-motion: no-preference) {
.welcome-icon { animation: float 3s ease-in-out infinite; }
}
.welcome-state h2 {
font-size: 1.5rem; font-weight: 590;
letter-spacing: -0.02em; color: var(--text-primary);
font-family: var(--font-display);
}
.welcome-state p {
font-size: 0.875rem; color: var(--text-secondary);
max-width: 360px; line-height: 1.5;
}
.empty-state {
font-size: 0.75rem; color: var(--text-tertiary);
text-align: center; padding: var(--spacing-xl) var(--spacing-md);
}
.hidden { display: none !important; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-soft); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-prominent); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.welcome-icon { animation: none; }
.btn-icon:active, .btn-primary:active, .btn-secondary:active,
.btn-small:active, .branch-item:active, .commit-item:active,
.status-file:active, .tab:active, .file-list-item:active {
transform: none;
}
}
@media (prefers-contrast: more) {
:root {
--border-subtle: rgba(128,128,128,0.4);
--border-soft: rgba(128,128,128,0.6);
--text-secondary: #000;
--text-tertiary: #333;
}
}
.settings-section {
margin-bottom: var(--spacing-lg);
}
.settings-section label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
letter-spacing: 0.02em;
text-transform: uppercase;
}
.settings-row {
display: flex;
gap: var(--spacing-sm);
}
.settings-option {
flex: 1;
padding: 8px var(--spacing-md);
border: 2px solid var(--border-soft);
border-radius: var(--radius-md);
background: var(--bg-panel);
color: var(--text-primary);
font-size: 0.8125rem;
font-family: var(--font-body);
cursor: pointer;
text-align: center;
transition: all 120ms;
}
.settings-option:hover {
border-color: var(--border-prominent);
background: var(--bg-hover);
}
.settings-option.active {
border-color: var(--accent);
background: var(--bg-selected);
color: var(--accent);
font-weight: 500;
}