页面美化

This commit is contained in:
2026-07-08 14:39:08 +08:00
parent bb4e376528
commit a7bcd03534
8 changed files with 649 additions and 149 deletions

View File

@@ -17,7 +17,7 @@
"height": 600,
"minWidth": 900,
"minHeight": 600,
"decorations": true
"decorations": false
}
],
"security": {

View File

@@ -11,7 +11,9 @@ const wordCount = ref(0);
const charCount = ref(0);
type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
const APP_THEME_KEY = "yurou:app-theme";
function isDocumentMode(value: string | null): value is DocumentMode {
return value === "preview" || value === "edit" || value === "mixed";
@@ -33,6 +35,30 @@ function saveDefaultDocumentMode(mode: DocumentMode) {
}
}
function isAppTheme(value: string | null): value is AppTheme {
return value === "system" || value === "warm" || value === "white" || value === "black" || value === "gruvbox" || value === "gray";
}
function loadAppTheme(): AppTheme {
try {
const stored = localStorage.getItem(APP_THEME_KEY);
if (isAppTheme(stored)) return stored;
} catch { /* localStorage unavailable */
}
return "warm";
}
function saveAppTheme(theme: AppTheme) {
try {
localStorage.setItem(APP_THEME_KEY, theme);
} catch { /* noop */
}
}
function applyAppTheme(theme: AppTheme) {
document.documentElement.dataset.theme = theme;
}
// File state
const currentFilePath = ref<string | null>(null);
const saveStatus = ref("");
@@ -104,6 +130,7 @@ function onEditorWheel(e: WheelEvent) {
const showOutline = ref(false);
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
const appTheme = ref<AppTheme>(loadAppTheme());
const showMoreMenu = ref(false);
const settingsRequest = ref(0);
const settingsInitialPage = ref<"about" | "editor">("about");
@@ -127,6 +154,12 @@ function updateDefaultDocumentMode(mode: DocumentMode) {
saveDefaultDocumentMode(mode);
}
function updateAppTheme(theme: AppTheme) {
appTheme.value = theme;
applyAppTheme(theme);
saveAppTheme(theme);
}
function handleExportPdf() {
showMoreMenu.value = false;
// TODO: implement PDF export
@@ -145,6 +178,7 @@ watch(showMoreMenu, (val) => {
});
onMounted(() => {
applyAppTheme(appTheme.value);
applyZoomCssVars();
window.addEventListener("wheel", onEditorWheel, {passive: false});
void restoreLastWorkspace();
@@ -423,9 +457,9 @@ registerTauriListener("menu-event", (event) => {
</script>
<template>
<div class="h-screen bg-[#faf9f6] text-[#38342e] flex flex-col">
<div class="h-screen app-root flex flex-col">
<!-- Three-column body -->
<div class="flex-1 flex overflow-hidden bg-[#faf9f6]">
<div class="flex-1 flex overflow-hidden app-root">
<!-- Left: Directory sidebar -->
<DirectorySidebar
:folder-path="folderPath"
@@ -434,10 +468,12 @@ registerTauriListener("menu-event", (event) => {
:settings-request="settingsRequest"
:settings-initial-page="settingsInitialPage"
:default-document-mode="defaultDocumentMode"
:app-theme="appTheme"
@open-folder="onOpenFolder"
@toggle-folder="onToggleFolder"
@open-file="onOpenFile"
@update-default-document-mode="updateDefaultDocumentMode"
@update-app-theme="updateAppTheme"
/>
<!-- Right: Editor area -->
@@ -445,7 +481,7 @@ registerTauriListener("menu-event", (event) => {
<!-- Toolbar -->
<div
v-if="!showStartActions"
class="flex w-full items-center h-10 px-3 bg-[#faf9f6] border-b border-[#e0dbcf] select-none shrink-0"
class="editor-toolbar flex w-full items-center h-10 px-3 select-none shrink-0"
>
<!-- Left: Outline toggle -->
<button
@@ -458,7 +494,7 @@ registerTauriListener("menu-event", (event) => {
</button>
<!-- Center: Filename -->
<div class="flex-1 text-center text-sm text-[#8c877d] font-medium truncate px-3 select-none">
<div class="app-muted-text flex-1 text-center text-sm font-medium truncate px-3 select-none">
{{ showStartActions ? '未选择工作目录' : currentFilePath ? getFileName(currentFilePath) : '未命名文档' }}
</div>
@@ -561,14 +597,14 @@ registerTauriListener("menu-event", (event) => {
<Transition name="zoom-fade">
<div
v-if="showZoomIndicator"
class="fixed top-4 right-4 z-50 px-2.5 py-1 rounded-md bg-[#38342e]/80 text-white text-xs font-medium pointer-events-none select-none backdrop-blur-sm"
class="zoom-indicator fixed top-4 right-4 z-50 px-2.5 py-1 rounded-md text-xs font-medium pointer-events-none select-none backdrop-blur-sm"
>
{{ Math.round(editorZoom * 100) }}%
</div>
</Transition>
</main>
</div>
<footer class="border-t border-[#e8e4da] bg-[#f4f1eb]/60 backdrop-blur-sm">
<footer class="app-footer backdrop-blur-sm">
<div class="flex items-center gap-4 px-4 py-1.5">
<!-- Current file / save status -->
@@ -577,16 +613,16 @@ registerTauriListener("menu-event", (event) => {
class="text-xs"
:class="saveStatus.startsWith('保存失败') ? 'text-red-500' : 'text-emerald-600'"
>{{ saveStatus }}</span>
<span v-else-if="currentFilePath" class="text-xs text-[#8c877d]">
<span v-else-if="currentFilePath" class="app-muted-text text-xs">
{{ currentFilePath.split(/[/\\]/).pop() }}
</span>
<span v-else class="text-xs text-[#b8b3a8]">未保存</span>
<span v-else class="app-subtle-text text-xs">未保存</span>
<span class="flex-1"/>
<span class="text-xs text-[#8c877d]">{{ wordCount.toLocaleString() }} </span>
<span class="w-px h-3 bg-[#d4cfc4]"/>
<span class="text-xs text-[#8c877d]">{{ charCount.toLocaleString() }} </span>
<span class="app-muted-text text-xs">{{ wordCount.toLocaleString() }} </span>
<span class="app-divider w-px h-3"/>
<span class="app-muted-text text-xs">{{ charCount.toLocaleString() }} </span>
</div>
</footer>
</div>
@@ -616,6 +652,38 @@ registerTauriListener("menu-event", (event) => {
opacity: 0;
}
.app-root {
background: var(--app-bg);
color: var(--app-text);
}
.editor-toolbar {
background: var(--app-bg);
border-bottom: 1px solid var(--app-border-strong);
}
.app-footer {
border-top: 1px solid var(--app-border);
background: color-mix(in srgb, var(--app-sidebar-bg) 72%, transparent);
}
.app-muted-text {
color: var(--app-muted);
}
.app-subtle-text {
color: var(--app-subtle);
}
.app-divider {
background: var(--app-border-strong);
}
.zoom-indicator {
background: var(--app-toast-bg);
color: var(--app-control-contrast);
}
/* ── Toolbar ───────────────────────────────────────────────────── */
.toolbar-btn {
@@ -625,7 +693,7 @@ registerTauriListener("menu-event", (event) => {
width: 32px;
height: 32px;
border-radius: 6px;
color: #8c877d;
color: var(--app-muted);
background: transparent;
border: none;
cursor: pointer;
@@ -634,13 +702,13 @@ registerTauriListener("menu-event", (event) => {
}
.toolbar-btn:hover {
background-color: #ede8dd;
color: #38342e;
background-color: var(--app-hover);
color: var(--app-text);
}
.toolbar-btn-active {
color: #bf6a3b;
background-color: #f4f1ea;
color: var(--app-accent);
background-color: var(--app-surface-muted);
}
.mode-toggle {
@@ -648,9 +716,9 @@ registerTauriListener("menu-event", (event) => {
align-items: center;
height: 30px;
padding: 2px;
border: 1px solid #e0dbcf;
border: 1px solid var(--app-border-strong);
border-radius: 7px;
background-color: #f4f1ea;
background-color: var(--app-surface-muted);
flex-shrink: 0;
}
@@ -664,7 +732,7 @@ registerTauriListener("menu-event", (event) => {
border: none;
border-radius: 5px;
background: transparent;
color: #8c877d;
color: var(--app-muted);
font-size: 12px;
line-height: 1;
cursor: pointer;
@@ -675,13 +743,13 @@ registerTauriListener("menu-event", (event) => {
}
.mode-toggle-btn:hover {
color: #38342e;
color: var(--app-text);
}
.mode-toggle-btn-active {
background-color: #fffdfa;
color: #bf6a3b;
box-shadow: 0 1px 3px rgba(56, 52, 46, 0.12);
background-color: var(--app-surface);
color: var(--app-accent);
box-shadow: 0 1px 3px var(--app-shadow);
}
.dropdown-menu {
@@ -689,8 +757,8 @@ registerTauriListener("menu-event", (event) => {
right: 0;
top: 100%;
margin-top: 4px;
background: white;
border: 1px solid #e0dbcf;
background: var(--app-surface);
border: 1px solid var(--app-border-strong);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
min-width: 140px;
@@ -706,7 +774,7 @@ registerTauriListener("menu-event", (event) => {
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
color: #38342e;
color: var(--app-text);
background: transparent;
border: none;
cursor: pointer;
@@ -714,19 +782,19 @@ registerTauriListener("menu-event", (event) => {
}
.dropdown-item:hover {
background-color: #f4f1ea;
background-color: var(--app-surface-muted);
}
.start-actions {
background:
linear-gradient(180deg, rgba(255, 253, 250, 0.35), rgba(244, 241, 235, 0.2)),
#faf9f6;
linear-gradient(180deg, color-mix(in srgb, var(--app-surface) 35%, transparent), color-mix(in srgb, var(--app-sidebar-bg) 20%, transparent)),
var(--app-bg);
}
.start-action-btn {
border: none;
background: transparent;
color: #8c877d;
color: var(--app-muted);
font-size: 15px;
line-height: 1.4;
cursor: pointer;
@@ -742,7 +810,7 @@ registerTauriListener("menu-event", (event) => {
.start-action-btn:hover,
.start-action-btn:focus-visible {
color: #bf6a3b;
color: var(--app-accent);
text-decoration-color: currentColor;
outline: none;
}

View File

@@ -5,6 +5,7 @@ import TreeEntry from "./TreeEntry.vue";
type SettingsPage = "about" | "editor";
type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";
interface DirEntry {
name: string;
@@ -21,12 +22,14 @@ const props = withDefaults(
settingsRequest?: number;
settingsInitialPage?: SettingsPage;
defaultDocumentMode?: DocumentMode;
appTheme?: AppTheme;
}>(),
{
currentFilePath: null,
settingsRequest: 0,
settingsInitialPage: "about",
defaultDocumentMode: "edit",
appTheme: "warm",
},
);
@@ -37,6 +40,7 @@ const emit = defineEmits<{
createFile: [];
createFolder: [];
updateDefaultDocumentMode: [mode: DocumentMode];
updateAppTheme: [theme: AppTheme];
}>();
// Wrap the workspace root as a top-level tree node
@@ -82,14 +86,14 @@ function openSettings(page: SettingsPage) {
<template>
<aside
:class="['flex flex-row border-r border-[#e8e4da] bg-[#f4f1eb]/80 transition-all duration-300 ease-in-out overflow-hidden shrink-0', expanded ? 'w-72' : 'w-12']"
:class="['directory-sidebar flex flex-row transition-all duration-300 ease-in-out overflow-hidden shrink-0', expanded ? 'w-72' : 'w-12']"
>
<!-- ============ Icon bar ============ -->
<div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
<!-- Folder -->
<button
class="px-2 py-1 cursor-pointer rounded-md transition-colors"
:class="activeView === 'folder' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
class="sidebar-icon-btn px-2 py-1 cursor-pointer rounded-md transition-colors"
:class="{ 'sidebar-icon-btn-active': activeView === 'folder' }"
title="文件"
@click="switchView('folder')"
>
@@ -97,8 +101,8 @@ function openSettings(page: SettingsPage) {
</button>
<!-- Search -->
<button
class="px-2 py-1 cursor-pointer rounded-md transition-colors"
:class="activeView === 'search' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
class="sidebar-icon-btn px-2 py-1 cursor-pointer rounded-md transition-colors"
:class="{ 'sidebar-icon-btn-active': activeView === 'search' }"
title="搜索"
@click="switchView('search')"
>
@@ -106,8 +110,8 @@ function openSettings(page: SettingsPage) {
</button>
<!-- ===== Settings ===== -->
<button
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
:class="showSettings ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
class="sidebar-icon-btn px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
:class="{ 'sidebar-icon-btn-active': showSettings }"
title="设置"
@click="openSettings('about')"
>
@@ -116,7 +120,7 @@ function openSettings(page: SettingsPage) {
</div>
<!-- ============ Expanded panel ============ -->
<div v-show="expanded" class="flex-1 flex flex-col overflow-hidden border-l border-[#e8e4da]">
<div v-show="expanded" class="directory-sidebar-panel flex-1 flex flex-col overflow-hidden">
<!-- ===== FOLDER VIEW ===== -->
<div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden">
<!-- File tree -->
@@ -134,11 +138,11 @@ function openSettings(page: SettingsPage) {
<!-- Empty state -->
<div
v-if="!folderPath"
class="px-2 py-4 text-xs text-[#b8b3a8] text-center"
class="sidebar-empty px-2 py-4 text-xs text-center"
>
<p class="mb-3">暂无打开的文件夹</p>
<button
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs cursor-pointer rounded-md bg-[#fdf0e5] text-[#bf6a3b] border border-[#d4a574]/40 hover:bg-[#fdf0e5]/80 hover:text-[#bf6a3b] transition-colors"
class="sidebar-empty-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-xs cursor-pointer rounded-md transition-colors"
@click="$emit('openFolder')"
>
<i class="ri-folder-open-line"></i>
@@ -152,16 +156,16 @@ function openSettings(page: SettingsPage) {
<div v-if="activeView === 'search'" class="flex flex-col flex-1 overflow-hidden">
<div class="px-3 py-3">
<div class="relative">
<i class="ri-search-line absolute left-2.5 top-1/2 -translate-y-1/2 text-sm text-[#b8b3a8]"></i>
<i class="sidebar-search-icon ri-search-line absolute left-2.5 top-1/2 -translate-y-1/2 text-sm"></i>
<input
class="w-full pl-8 pr-3 py-1.5 text-xs bg-[#ede8de] border border-[#e0dbcf] rounded-md text-[#38342e] outline-none focus:border-[#bf6a3b] placeholder-[#b8b3a8]"
class="sidebar-search-input w-full pl-8 pr-3 py-1.5 text-xs rounded-md outline-none"
placeholder="搜索文件内容..."
disabled
/>
</div>
</div>
<div class="flex-1 flex items-center justify-center px-3 pb-8">
<p class="text-xs text-[#b8b3a8] text-center leading-relaxed">
<p class="sidebar-empty text-xs text-center leading-relaxed">
搜索功能即将上线
</p>
</div>
@@ -173,6 +177,59 @@ function openSettings(page: SettingsPage) {
v-model:show="showSettings"
:initial-page="settingsPage"
:default-document-mode="defaultDocumentMode"
:app-theme="appTheme"
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
@update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)"
/>
</template>
<style scoped>
.directory-sidebar {
border-right: 1px solid var(--app-border);
background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
}
.directory-sidebar-panel {
border-left: 1px solid var(--app-border);
}
.sidebar-icon-btn {
color: var(--app-subtle);
}
.sidebar-icon-btn:hover,
.sidebar-icon-btn-active {
background: var(--app-hover);
color: var(--app-accent);
}
.sidebar-empty,
.sidebar-search-icon {
color: var(--app-subtle);
}
.sidebar-empty-btn {
border: 1px solid color-mix(in srgb, var(--app-accent-muted) 42%, transparent);
background: var(--app-active-bg);
color: var(--app-accent);
}
.sidebar-empty-btn:hover {
background: color-mix(in srgb, var(--app-active-bg) 84%, var(--app-surface));
color: var(--app-accent);
}
.sidebar-search-input {
border: 1px solid var(--app-border-strong);
background: var(--app-hover);
color: var(--app-text);
}
.sidebar-search-input:focus {
border-color: var(--app-accent);
}
.sidebar-search-input::placeholder {
color: var(--app-subtle);
}
</style>

View File

@@ -501,8 +501,7 @@ defineExpose({
class="
markdown-source-editor
w-full min-h-[60vh] resize-none overflow-hidden bg-transparent outline-none
font-serif text-[#38342e] leading-relaxed
placeholder:text-[#b8b3a8]
font-serif leading-relaxed
"
@input="onEditorInput"
@keydown="onEditorKeydown"
@@ -583,7 +582,7 @@ defineExpose({
.markdown-preview {
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
line-height: 1.625;
color: #38342e;
color: var(--app-text);
word-break: break-word;
}
@@ -592,6 +591,11 @@ defineExpose({
.markdown-source-editor {
display: block;
height: auto;
color: var(--app-text);
}
.markdown-source-editor::placeholder {
color: var(--app-subtle);
}
.markdown-mixed > .markdown-block:first-child .markdown-block-preview > *:first-child {
@@ -605,7 +609,7 @@ defineExpose({
.markdown-preview h5,
.markdown-preview h6 {
font-weight: 700;
color: #2d2a26;
color: var(--app-text-strong);
line-height: 1.3;
margin-top: 1.5em;
margin-bottom: 0.5em;
@@ -617,13 +621,13 @@ defineExpose({
.markdown-preview p { margin: 0.5em 0; }
.markdown-preview strong { font-weight: 700; color: #2d2a26; }
.markdown-preview strong { font-weight: 700; color: var(--app-text-strong); }
.markdown-preview em { font-style: italic; }
.markdown-preview del { text-decoration: line-through; color: #8c877d; }
.markdown-preview del { text-decoration: line-through; color: var(--app-muted); }
.markdown-preview code {
background-color: #f4f1ea;
color: #bf6a3b;
background-color: var(--app-surface-muted);
color: var(--app-accent);
border-radius: 0.25rem;
padding: 0.125rem 0.375rem;
font-size: 0.875em;
@@ -632,7 +636,7 @@ defineExpose({
}
.markdown-preview pre {
background-color: #f4f1ea;
background-color: var(--app-surface-muted);
border-radius: 0.375rem;
padding: 1rem;
overflow-x: auto;
@@ -641,19 +645,19 @@ defineExpose({
.markdown-preview pre code {
background: none;
padding: 0;
color: #38342e;
color: var(--app-text);
font-size: 0.875em;
border-radius: 0;
word-break: normal;
}
.markdown-preview a { color: #bf6a3b; text-decoration: underline; }
.markdown-preview a { color: var(--app-accent); text-decoration: underline; }
.markdown-preview blockquote {
border-left: 4px solid #bf6a3b;
border-left: 4px solid var(--app-accent);
padding-left: 1rem;
margin: 0.75em 0;
color: #8c877d;
color: var(--app-muted);
font-style: italic;
}
.markdown-preview blockquote p { margin: 0.25em 0; }
@@ -664,12 +668,12 @@ defineExpose({
.markdown-preview li input[type="checkbox"] {
margin-right: 0.375rem;
accent-color: #bf6a3b;
accent-color: var(--app-accent);
}
.markdown-preview hr {
border: none;
border-top: 1px solid #e0dbcf;
border-top: 1px solid var(--app-border-strong);
margin: 1.5em 0;
}
@@ -680,16 +684,16 @@ defineExpose({
}
.markdown-preview th,
.markdown-preview td {
border: 1px solid #e0dbcf;
border: 1px solid var(--app-border-strong);
padding: 0.5rem 0.75rem;
text-align: left;
}
.markdown-preview th {
background-color: #f4f1ea;
background-color: var(--app-surface-muted);
font-weight: 700;
color: #2d2a26;
color: var(--app-text-strong);
}
.markdown-preview tr:nth-child(even) td { background-color: #faf9f6; }
.markdown-preview tr:nth-child(even) td { background-color: var(--app-panel-bg); }
.markdown-preview img {
max-width: 100%;
@@ -716,7 +720,7 @@ defineExpose({
}
.markdown-block:not(.markdown-block-editing):hover > .markdown-block-preview > * {
background-color: #f4f1ea;
background-color: var(--app-surface-muted);
cursor: text;
}
@@ -730,13 +734,13 @@ defineExpose({
}
.markdown-block-preview:focus-visible > * {
outline: 2px solid #d6aa8e;
outline: 2px solid var(--app-accent-muted);
outline-offset: 2px;
border-radius: 0.25rem;
}
.markdown-empty-placeholder:focus-visible {
outline: 2px solid #d6aa8e;
outline: 2px solid var(--app-accent-muted);
outline-offset: 2px;
border-radius: 0.25rem;
}
@@ -747,22 +751,22 @@ defineExpose({
min-height: calc((1em * 1.6) + 1.5rem + 2px);
resize: none;
overflow: hidden;
border: 1px solid #d6aa8e;
border: 1px solid var(--app-accent-muted);
border-radius: 0.375rem;
background-color: #fffdfa;
color: #38342e;
background-color: var(--app-surface);
color: var(--app-text);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.92em;
line-height: 1.6;
margin: 0.375rem 0;
padding: 0.75rem 0.875rem;
outline: none;
box-shadow: 0 0 0 3px rgba(191, 106, 59, 0.08);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent) 12%, transparent);
}
.markdown-block-editor::placeholder,
.markdown-empty-placeholder {
color: #b8b3a8;
color: var(--app-subtle);
}
.markdown-empty-placeholder {

View File

@@ -46,11 +46,11 @@ const onHeadingClick = (item: OutlineItem) => {
<template>
<aside
class="flex flex-col border-r border-[#e8e4da] bg-[#f4f1eb]/80 w-52 shrink-0 overflow-hidden"
class="outline-panel flex flex-col w-52 shrink-0 overflow-hidden"
>
<!-- Header -->
<div class="flex items-center px-3 py-3 border-b border-[#e8e4da]">
<span class="text-xs font-semibold text-[#8c877d] uppercase tracking-wider">
<div class="outline-panel-header flex items-center px-3 py-3">
<span class="outline-panel-title text-xs font-semibold uppercase tracking-wider">
大纲
</span>
</div>
@@ -58,7 +58,7 @@ const onHeadingClick = (item: OutlineItem) => {
<!-- Outline content -->
<div class="flex-1 overflow-y-auto px-2 py-2">
<template v-if="headings.length === 0">
<p class="text-xs text-[#b8b3a8] px-2 py-4 text-center leading-relaxed">
<p class="outline-empty text-xs px-2 py-4 text-center leading-relaxed">
暂无标题<br />使用 H1-H3 标题<br />自动生成大纲
</p>
</template>
@@ -67,12 +67,12 @@ const onHeadingClick = (item: OutlineItem) => {
v-for="item in headings"
:key="item.id"
:class="[
'px-2 py-1 rounded-md text-sm cursor-pointer hover:bg-[#ede8de] transition-colors truncate',
'outline-item px-2 py-1 rounded-md text-sm cursor-pointer transition-colors truncate',
item.level === 1
? 'text-[#38342e] font-medium ml-0'
? 'outline-item-level-1 font-medium ml-0'
: item.level === 2
? 'text-[#8c877d] ml-3'
: 'text-[#b8b3a8] ml-6 text-xs',
? 'outline-item-level-2 ml-3'
: 'outline-item-level-3 ml-6 text-xs',
]"
:title="item.text"
@click="onHeadingClick(item)"
@@ -83,3 +83,32 @@ const onHeadingClick = (item: OutlineItem) => {
</div>
</aside>
</template>
<style scoped>
.outline-panel {
border-right: 1px solid var(--app-border);
background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
}
.outline-panel-header {
border-bottom: 1px solid var(--app-border);
}
.outline-panel-title,
.outline-item-level-2 {
color: var(--app-muted);
}
.outline-empty,
.outline-item-level-3 {
color: var(--app-subtle);
}
.outline-item:hover {
background: var(--app-hover);
}
.outline-item-level-1 {
color: var(--app-text);
}
</style>

View File

@@ -2,24 +2,28 @@
import { getVersion } from "@tauri-apps/api/app";
import { computed, onBeforeUnmount, ref, watch } from "vue";
type SettingsPage = "about" | "editor";
type SettingsPage = "about" | "editor" | "appearance";
type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";
const props = withDefaults(
defineProps<{
show: boolean;
initialPage?: SettingsPage;
defaultDocumentMode?: DocumentMode;
appTheme?: AppTheme;
}>(),
{
initialPage: "about",
defaultDocumentMode: "edit",
appTheme: "warm",
},
);
const emit = defineEmits<{
"update:show": [value: boolean];
updateDefaultDocumentMode: [mode: DocumentMode];
updateAppTheme: [theme: AppTheme];
}>();
const APP_NAME = "yurou";
@@ -34,7 +38,11 @@ const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans"));
const updateStatus = ref("");
let updateStatusTimer: ReturnType<typeof setTimeout> | null = null;
const pageTitle = computed(() => (activePage.value === "about" ? "关于" : "编辑器"));
const pageTitle = computed(() => {
if (activePage.value === "about") return "关于";
if (activePage.value === "editor") return "编辑器";
return "外观";
});
const menuItems: Array<{
id: SettingsPage;
@@ -43,6 +51,7 @@ const menuItems: Array<{
}> = [
{ id: "about", label: "关于", icon: "ri-information-line" },
{ id: "editor", label: "编辑器", icon: "ri-edit-2-line" },
{ id: "appearance", label: "外观", icon: "ri-palette-line" },
];
const documentModeOptions: Array<{
@@ -54,6 +63,19 @@ const documentModeOptions: Array<{
{ value: "mixed", label: "混合" },
];
const themeOptions: Array<{
value: AppTheme;
label: string;
swatch: string;
}> = [
{ value: "system", label: "跟随系统", swatch: "linear-gradient(135deg, #ffffff 0 50%, #17191d 50% 100%)" },
{ value: "warm", label: "柔和暖色", swatch: "#bf6a3b" },
{ value: "white", label: "白色", swatch: "#2563eb" },
{ value: "black", label: "黑色", swatch: "#8ab4ff" },
{ value: "gruvbox", label: "Gruvbox", swatch: "#fe8019" },
{ value: "gray", label: "灰色", swatch: "#4f6f93" },
];
function loadBooleanPref(key: string, fallback: boolean) {
try {
const stored = localStorage.getItem(key);
@@ -118,6 +140,15 @@ function onDefaultDocumentModeChange(event: Event) {
emit("updateDefaultDocumentMode", mode);
}
function onThemeChange(event: Event) {
const theme = (event.target as HTMLSelectElement).value as AppTheme;
emit("updateAppTheme", theme);
}
function resetTheme() {
emit("updateAppTheme", "warm");
}
watch(
() => props.show,
(show) => {
@@ -242,7 +273,7 @@ onBeforeUnmount(() => {
</section>
</div>
<div v-else class="settings-content">
<div v-else-if="activePage === 'editor'" class="settings-content">
<section class="settings-about-card" aria-label="编辑器设置">
<div class="settings-row settings-row-top">
<div class="settings-copy">
@@ -266,6 +297,59 @@ onBeforeUnmount(() => {
</div>
</section>
</div>
<div v-else class="settings-content">
<section class="settings-about-card" aria-label="外观设置">
<div class="settings-row settings-row-top">
<div class="settings-copy">
<h3>基础颜色</h3>
<p>设置 {{ APP_NAME }} 的基础颜色</p>
</div>
<select
class="settings-select"
aria-label="基础颜色"
:value="appTheme"
@change="onThemeChange"
>
<option
v-for="option in themeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="settings-row">
<div class="settings-copy">
<h3>主题色</h3>
<p> {{ APP_NAME }} 选择一个主题色主题色将影响选中链接等元素的颜色</p>
</div>
<div class="theme-swatch-group" aria-label="主题色">
<button
class="theme-reset-btn"
type="button"
title="重置主题色"
@click="resetTheme"
>
<i class="ri-reset-left-line text-lg"></i>
</button>
<button
v-for="option in themeOptions.filter((item) => item.value !== 'system')"
:key="option.value"
class="theme-swatch"
:class="{ 'theme-swatch-active': appTheme === option.value }"
type="button"
:title="option.label"
:aria-label="option.label"
:style="{ background: option.swatch }"
@click="emit('updateAppTheme', option.value)"
></button>
</div>
</div>
</section>
</div>
</div>
</section>
</div>
@@ -281,7 +365,7 @@ onBeforeUnmount(() => {
align-items: center;
justify-content: center;
padding: 28px;
background: rgba(56, 52, 46, 0.22);
background: var(--app-overlay);
backdrop-filter: blur(8px);
}
@@ -292,12 +376,12 @@ onBeforeUnmount(() => {
min-height: 430px;
max-height: min(680px, calc(100vh - 48px));
overflow: hidden;
border: 1px solid #e0dbcf;
border: 1px solid var(--app-border-strong);
border-radius: 12px;
background: #fffdfa;
background: var(--app-surface);
box-shadow:
0 24px 70px rgba(56, 52, 46, 0.2),
0 2px 10px rgba(56, 52, 46, 0.08);
0 24px 70px var(--app-shadow),
0 2px 10px color-mix(in srgb, var(--app-shadow) 46%, transparent);
}
.settings-menu {
@@ -305,13 +389,13 @@ onBeforeUnmount(() => {
flex-direction: column;
gap: 4px;
padding: 14px 10px;
border-right: 1px solid #e8e4da;
background: #f4f1eb;
border-right: 1px solid var(--app-border);
background: var(--app-sidebar-bg);
}
.settings-menu-title {
padding: 0 10px 8px;
color: #8c877d;
color: var(--app-muted);
font-size: 12px;
font-weight: 600;
}
@@ -326,7 +410,7 @@ onBeforeUnmount(() => {
border: none;
border-radius: 7px;
background: transparent;
color: #5c574e;
color: var(--app-text-soft);
font-size: 13px;
line-height: 1;
text-align: left;
@@ -337,13 +421,13 @@ onBeforeUnmount(() => {
}
.settings-menu-item:hover {
background: #ede8dd;
color: #38342e;
background: var(--app-hover);
color: var(--app-text);
}
.settings-menu-item-active {
background: #fdf0e5;
color: #bf6a3b;
background: var(--app-active-bg);
color: var(--app-accent);
font-weight: 600;
}
@@ -351,7 +435,7 @@ onBeforeUnmount(() => {
display: flex;
min-width: 0;
flex-direction: column;
background: #faf9f6;
background: var(--app-panel-bg);
}
.settings-header {
@@ -360,13 +444,13 @@ onBeforeUnmount(() => {
justify-content: space-between;
min-height: 54px;
padding: 0 20px;
border-bottom: 1px solid #e8e4da;
background: rgba(255, 253, 250, 0.72);
border-bottom: 1px solid var(--app-border);
background: color-mix(in srgb, var(--app-surface) 72%, transparent);
}
.settings-header h2 {
margin: 0;
color: #38342e;
color: var(--app-text);
font-size: 15px;
font-weight: 700;
}
@@ -380,7 +464,7 @@ onBeforeUnmount(() => {
border: none;
border-radius: 6px;
background: transparent;
color: #8c877d;
color: var(--app-muted);
cursor: pointer;
transition:
background-color 0.15s ease,
@@ -388,8 +472,8 @@ onBeforeUnmount(() => {
}
.settings-close-btn:hover {
background: #ede8dd;
color: #38342e;
background: var(--app-hover);
color: var(--app-text);
}
.settings-content {
@@ -402,8 +486,8 @@ onBeforeUnmount(() => {
.settings-about-card {
overflow: hidden;
border-radius: 10px;
background: #fffdfa;
box-shadow: inset 0 0 0 1px #eee9de;
background: var(--app-surface);
box-shadow: inset 0 0 0 1px var(--app-border);
}
.settings-row {
@@ -413,7 +497,7 @@ onBeforeUnmount(() => {
align-items: center;
min-height: 78px;
padding: 16px 20px;
border-top: 1px solid #e0dbcf;
border-top: 1px solid var(--app-border-strong);
}
.settings-row-top {
@@ -427,7 +511,7 @@ onBeforeUnmount(() => {
.settings-copy h3,
.settings-empty-page h3 {
margin: 0 0 6px;
color: #2d2a26;
color: var(--app-text-strong);
font-size: 15px;
font-weight: 700;
line-height: 1.25;
@@ -436,7 +520,7 @@ onBeforeUnmount(() => {
.settings-copy p,
.settings-empty-page p {
margin: 0;
color: #5c574e;
color: var(--app-text-soft);
font-size: 12px;
line-height: 1.5;
}
@@ -447,7 +531,7 @@ onBeforeUnmount(() => {
padding: 0;
border: none;
background: transparent;
color: #7a55d9;
color: var(--app-link);
font-size: 12px;
line-height: 1.4;
text-align: left;
@@ -457,7 +541,7 @@ onBeforeUnmount(() => {
}
.settings-link:hover {
color: #6342bd;
color: var(--app-link-hover);
}
.settings-action-stack {
@@ -487,29 +571,29 @@ onBeforeUnmount(() => {
.settings-primary-btn {
min-width: 78px;
border: none;
background: #8c6be8;
color: #fff;
box-shadow: 0 2px 7px rgba(100, 65, 190, 0.26);
background: var(--app-control);
color: var(--app-control-contrast);
box-shadow: 0 2px 7px color-mix(in srgb, var(--app-control) 32%, transparent);
}
.settings-primary-btn:hover {
background: #7a55d9;
background: var(--app-control-hover);
}
.settings-secondary-btn {
min-width: 50px;
border: 1px solid #e0dbcf;
background: #fff;
color: #38342e;
box-shadow: 0 1px 4px rgba(56, 52, 46, 0.1);
border: 1px solid var(--app-border-strong);
background: var(--app-surface);
color: var(--app-text);
box-shadow: 0 1px 4px color-mix(in srgb, var(--app-shadow) 58%, transparent);
}
.settings-secondary-btn:hover {
background: #f4f1ea;
background: var(--app-surface-muted);
}
.settings-status {
color: #6f6a60;
color: var(--app-muted);
font-size: 11px;
white-space: nowrap;
}
@@ -532,7 +616,7 @@ onBeforeUnmount(() => {
position: absolute;
inset: 0;
border-radius: 999px;
background: #d4cfc4;
background: var(--app-border-strong);
transition: background-color 0.16s ease;
}
@@ -543,14 +627,14 @@ onBeforeUnmount(() => {
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(56, 52, 46, 0.24);
background: var(--app-surface);
box-shadow: 0 1px 3px var(--app-shadow);
content: "";
transition: transform 0.16s ease;
}
.settings-switch input:checked + span {
background: #8c6be8;
background: var(--app-control);
}
.settings-switch input:checked + span::after {
@@ -558,34 +642,76 @@ onBeforeUnmount(() => {
}
.settings-switch input:focus-visible + span {
outline: 2px solid #d6aa8e;
outline: 2px solid var(--app-accent-muted);
outline-offset: 2px;
}
.settings-select {
width: 158px;
height: 30px;
border: 1px solid #e0dbcf;
border: 1px solid var(--app-border-strong);
border-radius: 6px;
background: #fff;
color: #38342e;
background: var(--app-surface);
color: var(--app-text);
font-size: 13px;
line-height: 1;
padding: 0 10px;
box-shadow: 0 1px 4px rgba(56, 52, 46, 0.1);
box-shadow: 0 1px 4px color-mix(in srgb, var(--app-shadow) 58%, transparent);
outline: none;
}
.settings-select:focus {
border-color: #d6aa8e;
box-shadow: 0 0 0 3px rgba(191, 106, 59, 0.1);
border-color: var(--app-accent-muted);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-accent) 12%, transparent);
}
.theme-swatch-group {
display: inline-flex;
align-items: center;
gap: 10px;
}
.theme-reset-btn,
.theme-swatch {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
}
.theme-reset-btn {
width: 30px;
height: 30px;
border-radius: 6px;
background: transparent;
color: var(--app-muted);
}
.theme-reset-btn:hover {
background: var(--app-hover);
color: var(--app-text);
}
.theme-swatch {
width: 22px;
height: 22px;
border-radius: 999px;
box-shadow:
0 0 0 1px color-mix(in srgb, var(--app-text) 18%, transparent),
0 2px 5px color-mix(in srgb, var(--app-shadow) 70%, transparent);
}
.theme-swatch-active {
outline: 2px solid var(--app-accent);
outline-offset: 3px;
}
.settings-empty-page {
display: flex;
align-items: center;
justify-content: center;
color: #8c877d;
color: var(--app-muted);
text-align: center;
}
@@ -609,7 +735,7 @@ onBeforeUnmount(() => {
flex-direction: row;
align-items: center;
border-right: none;
border-bottom: 1px solid #e8e4da;
border-bottom: 1px solid var(--app-border);
}
.settings-menu-title {

View File

@@ -57,15 +57,15 @@ function onClick() {
<template v-if="entry.is_dir">
<!-- Folder label -->
<div
class="flex items-center gap-1 px-2 py-1 rounded-md text-sm text-[#5c574e] hover:bg-[#ede8de] cursor-pointer select-none"
class="tree-folder flex items-center gap-1 px-2 py-1 rounded-md text-sm cursor-pointer select-none"
:style="{ paddingLeft: `${depth * 16 + 8}px` }"
@click="onClick"
>
<i
class="ri-arrow-right-s-line text-sm text-[#b8b3a8] transition-transform shrink-0"
class="tree-arrow ri-arrow-right-s-line text-sm transition-transform shrink-0"
:class="expanded && entry.children ? 'rotate-90' : ''"
></i>
<i class="ri-folder-line text-base text-[#bf6a3b]/70 shrink-0"></i>
<i class="tree-entry-icon ri-folder-line text-base shrink-0"></i>
<span class="truncate">{{ entry.name }}</span>
</div>
@@ -87,15 +87,47 @@ function onClick() {
<div
v-else
:class="[
'flex items-center gap-1.5 px-2 py-1 rounded-md text-sm cursor-pointer transition-colors',
'tree-file flex items-center gap-1.5 px-2 py-1 rounded-md text-sm cursor-pointer transition-colors',
isActive
? 'bg-[#ede8de] text-[#bf6a3b] font-medium'
: 'text-[#8c877d] hover:bg-[#ede8de] hover:text-[#38342e]',
? 'tree-file-active font-medium'
: 'tree-file-idle',
]"
:style="{ paddingLeft: `${depth * 16 + 8}px` }"
@click="onClick"
>
<i class="ri-file-line text-sm text-[#bf6a3b]/60 shrink-0"></i>
<i class="tree-entry-icon ri-file-line text-sm shrink-0"></i>
<span class="truncate">{{ entry.name }}</span>
</div>
</template>
<style scoped>
.tree-folder {
color: var(--app-text-soft);
}
.tree-folder:hover {
background: var(--app-hover);
}
.tree-arrow {
color: var(--app-subtle);
}
.tree-entry-icon {
color: color-mix(in srgb, var(--app-accent) 68%, transparent);
}
.tree-file-idle {
color: var(--app-muted);
}
.tree-file-idle:hover {
background: var(--app-hover);
color: var(--app-text);
}
.tree-file-active {
background: var(--app-hover);
color: var(--app-accent);
}
</style>

View File

@@ -2,9 +2,193 @@
@source "../src/**/*.vue";
/* ============================================
Global base styles — "Soft Stone" light theme
Global base styles and app themes
============================================ */
:root,
:root[data-theme="warm"],
:root[data-theme="system"] {
color-scheme: light;
--app-bg: #faf9f6;
--app-text: #38342e;
--app-text-strong: #2d2a26;
--app-text-soft: #5c574e;
--app-muted: #8c877d;
--app-subtle: #b8b3a8;
--app-border: #e8e4da;
--app-border-strong: #e0dbcf;
--app-sidebar-bg: #f4f1eb;
--app-panel-bg: #faf9f6;
--app-surface: #fffdfa;
--app-surface-muted: #f4f1ea;
--app-hover: #ede8dd;
--app-active-bg: #fdf0e5;
--app-accent: #bf6a3b;
--app-accent-muted: #d6aa8e;
--app-accent-strong: #9f4f28;
--app-link: #7a55d9;
--app-link-hover: #6342bd;
--app-control: #8c6be8;
--app-control-hover: #7a55d9;
--app-control-contrast: #ffffff;
--app-selection: #e8d5b0;
--app-overlay: rgba(56, 52, 46, 0.22);
--app-toast-bg: rgba(56, 52, 46, 0.8);
--app-shadow: rgba(56, 52, 46, 0.16);
}
:root[data-theme="white"] {
color-scheme: light;
--app-bg: #ffffff;
--app-text: #202124;
--app-text-strong: #111315;
--app-text-soft: #4f5660;
--app-muted: #767d86;
--app-subtle: #a4aab2;
--app-border: #e8eaed;
--app-border-strong: #d7dbe0;
--app-sidebar-bg: #f7f8fa;
--app-panel-bg: #ffffff;
--app-surface: #ffffff;
--app-surface-muted: #f4f6f8;
--app-hover: #eef1f4;
--app-active-bg: #e9f1ff;
--app-accent: #2563eb;
--app-accent-muted: #93b4f8;
--app-accent-strong: #1d4ed8;
--app-link: #2563eb;
--app-link-hover: #1e40af;
--app-control: #2563eb;
--app-control-hover: #1d4ed8;
--app-control-contrast: #ffffff;
--app-selection: #bfdbfe;
--app-overlay: rgba(17, 24, 39, 0.2);
--app-toast-bg: rgba(17, 24, 39, 0.82);
--app-shadow: rgba(17, 24, 39, 0.15);
}
:root[data-theme="gray"] {
color-scheme: light;
--app-bg: #f3f4f6;
--app-text: #2f3337;
--app-text-strong: #1f2328;
--app-text-soft: #555b62;
--app-muted: #737a82;
--app-subtle: #a3a9b0;
--app-border: #dde1e6;
--app-border-strong: #cfd5dc;
--app-sidebar-bg: #e9ecef;
--app-panel-bg: #f3f4f6;
--app-surface: #ffffff;
--app-surface-muted: #eceff2;
--app-hover: #dfe3e8;
--app-active-bg: #e5ecf5;
--app-accent: #4f6f93;
--app-accent-muted: #8aa1ba;
--app-accent-strong: #3b5674;
--app-link: #4f6f93;
--app-link-hover: #334e68;
--app-control: #4f6f93;
--app-control-hover: #3b5674;
--app-control-contrast: #ffffff;
--app-selection: #cbd5e1;
--app-overlay: rgba(31, 35, 40, 0.24);
--app-toast-bg: rgba(31, 35, 40, 0.82);
--app-shadow: rgba(31, 35, 40, 0.14);
}
:root[data-theme="black"] {
color-scheme: dark;
--app-bg: #101113;
--app-text: #e7e9ed;
--app-text-strong: #f6f7f9;
--app-text-soft: #c8ccd2;
--app-muted: #9aa1aa;
--app-subtle: #6f7782;
--app-border: #2b2f35;
--app-border-strong: #383d45;
--app-sidebar-bg: #17191d;
--app-panel-bg: #101113;
--app-surface: #1b1e23;
--app-surface-muted: #242830;
--app-hover: #242830;
--app-active-bg: #253049;
--app-accent: #8ab4ff;
--app-accent-muted: #5d7fac;
--app-accent-strong: #bdd4ff;
--app-link: #9dbdff;
--app-link-hover: #c6d8ff;
--app-control: #7c9cff;
--app-control-hover: #9db8ff;
--app-control-contrast: #101113;
--app-selection: #30415f;
--app-overlay: rgba(0, 0, 0, 0.48);
--app-toast-bg: rgba(0, 0, 0, 0.82);
--app-shadow: rgba(0, 0, 0, 0.36);
}
:root[data-theme="gruvbox"] {
color-scheme: dark;
--app-bg: #282828;
--app-text: #ebdbb2;
--app-text-strong: #fbf1c7;
--app-text-soft: #d5c4a1;
--app-muted: #bdae93;
--app-subtle: #928374;
--app-border: #3c3836;
--app-border-strong: #504945;
--app-sidebar-bg: #32302f;
--app-panel-bg: #282828;
--app-surface: #1d2021;
--app-surface-muted: #3c3836;
--app-hover: #3c3836;
--app-active-bg: #4a3627;
--app-accent: #fe8019;
--app-accent-muted: #d79921;
--app-accent-strong: #fabd2f;
--app-link: #83a598;
--app-link-hover: #8ec07c;
--app-control: #d65d0e;
--app-control-hover: #fe8019;
--app-control-contrast: #1d2021;
--app-selection: #665c54;
--app-overlay: rgba(29, 32, 33, 0.52);
--app-toast-bg: rgba(29, 32, 33, 0.9);
--app-shadow: rgba(0, 0, 0, 0.38);
}
@media (prefers-color-scheme: dark) {
:root[data-theme="system"] {
color-scheme: dark;
--app-bg: #101113;
--app-text: #e7e9ed;
--app-text-strong: #f6f7f9;
--app-text-soft: #c8ccd2;
--app-muted: #9aa1aa;
--app-subtle: #6f7782;
--app-border: #2b2f35;
--app-border-strong: #383d45;
--app-sidebar-bg: #17191d;
--app-panel-bg: #101113;
--app-surface: #1b1e23;
--app-surface-muted: #242830;
--app-hover: #242830;
--app-active-bg: #253049;
--app-accent: #8ab4ff;
--app-accent-muted: #5d7fac;
--app-accent-strong: #bdd4ff;
--app-link: #9dbdff;
--app-link-hover: #c6d8ff;
--app-control: #7c9cff;
--app-control-hover: #9db8ff;
--app-control-contrast: #101113;
--app-selection: #30415f;
--app-overlay: rgba(0, 0, 0, 0.48);
--app-toast-bg: rgba(0, 0, 0, 0.82);
--app-shadow: rgba(0, 0, 0, 0.36);
}
}
html {
/* ── Font stack: refined system serif for Chinese + English ── */
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
@@ -16,14 +200,14 @@ html {
body {
margin: 0;
background: #faf9f6;
color: #38342e;
background: var(--app-bg);
color: var(--app-text);
}
/* ── Selection ── */
::selection {
background: #e8d5b0;
color: #38342e;
background: var(--app-selection);
color: var(--app-text);
}
/* ============================================
@@ -33,7 +217,7 @@ body {
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #d4cfc4 transparent;
scrollbar-color: var(--app-border-strong) transparent;
}
/* WebKit (Chrome, Edge, Safari, Tauri WebView2) */
@@ -47,17 +231,17 @@ body {
}
::-webkit-scrollbar-thumb {
background: #d4cfc4;
background: var(--app-border-strong);
border-radius: 9999px;
transition: background 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: #b0a99b;
background: var(--app-muted);
}
::-webkit-scrollbar-thumb:active {
background: #9a9282;
background: var(--app-subtle);
}
::-webkit-scrollbar-corner {