页面美化

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, "height": 600,
"minWidth": 900, "minWidth": 900,
"minHeight": 600, "minHeight": 600,
"decorations": true "decorations": false
} }
], ],
"security": { "security": {

View File

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

View File

@@ -5,6 +5,7 @@ import TreeEntry from "./TreeEntry.vue";
type SettingsPage = "about" | "editor"; type SettingsPage = "about" | "editor";
type DocumentMode = "preview" | "edit" | "mixed"; type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";
interface DirEntry { interface DirEntry {
name: string; name: string;
@@ -21,12 +22,14 @@ const props = withDefaults(
settingsRequest?: number; settingsRequest?: number;
settingsInitialPage?: SettingsPage; settingsInitialPage?: SettingsPage;
defaultDocumentMode?: DocumentMode; defaultDocumentMode?: DocumentMode;
appTheme?: AppTheme;
}>(), }>(),
{ {
currentFilePath: null, currentFilePath: null,
settingsRequest: 0, settingsRequest: 0,
settingsInitialPage: "about", settingsInitialPage: "about",
defaultDocumentMode: "edit", defaultDocumentMode: "edit",
appTheme: "warm",
}, },
); );
@@ -37,6 +40,7 @@ const emit = defineEmits<{
createFile: []; createFile: [];
createFolder: []; createFolder: [];
updateDefaultDocumentMode: [mode: DocumentMode]; updateDefaultDocumentMode: [mode: DocumentMode];
updateAppTheme: [theme: AppTheme];
}>(); }>();
// Wrap the workspace root as a top-level tree node // Wrap the workspace root as a top-level tree node
@@ -82,14 +86,14 @@ function openSettings(page: SettingsPage) {
<template> <template>
<aside <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 ============ --> <!-- ============ Icon bar ============ -->
<div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3"> <div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
<!-- Folder --> <!-- Folder -->
<button <button
class="px-2 py-1 cursor-pointer rounded-md transition-colors" class="sidebar-icon-btn 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-active': activeView === 'folder' }"
title="文件" title="文件"
@click="switchView('folder')" @click="switchView('folder')"
> >
@@ -97,8 +101,8 @@ function openSettings(page: SettingsPage) {
</button> </button>
<!-- Search --> <!-- Search -->
<button <button
class="px-2 py-1 cursor-pointer rounded-md transition-colors" class="sidebar-icon-btn 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-active': activeView === 'search' }"
title="搜索" title="搜索"
@click="switchView('search')" @click="switchView('search')"
> >
@@ -106,8 +110,8 @@ function openSettings(page: SettingsPage) {
</button> </button>
<!-- ===== Settings ===== --> <!-- ===== Settings ===== -->
<button <button
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto" class="sidebar-icon-btn 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-active': showSettings }"
title="设置" title="设置"
@click="openSettings('about')" @click="openSettings('about')"
> >
@@ -116,7 +120,7 @@ function openSettings(page: SettingsPage) {
</div> </div>
<!-- ============ Expanded panel ============ --> <!-- ============ 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 ===== --> <!-- ===== FOLDER VIEW ===== -->
<div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden"> <div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden">
<!-- File tree --> <!-- File tree -->
@@ -134,11 +138,11 @@ function openSettings(page: SettingsPage) {
<!-- Empty state --> <!-- Empty state -->
<div <div
v-if="!folderPath" 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> <p class="mb-3">暂无打开的文件夹</p>
<button <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')" @click="$emit('openFolder')"
> >
<i class="ri-folder-open-line"></i> <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 v-if="activeView === 'search'" class="flex flex-col flex-1 overflow-hidden">
<div class="px-3 py-3"> <div class="px-3 py-3">
<div class="relative"> <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 <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="搜索文件内容..." placeholder="搜索文件内容..."
disabled disabled
/> />
</div> </div>
</div> </div>
<div class="flex-1 flex items-center justify-center px-3 pb-8"> <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> </p>
</div> </div>
@@ -173,6 +177,59 @@ function openSettings(page: SettingsPage) {
v-model:show="showSettings" v-model:show="showSettings"
:initial-page="settingsPage" :initial-page="settingsPage"
:default-document-mode="defaultDocumentMode" :default-document-mode="defaultDocumentMode"
:app-theme="appTheme"
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)" @update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
@update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)"
/> />
</template> </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=" class="
markdown-source-editor markdown-source-editor
w-full min-h-[60vh] resize-none overflow-hidden bg-transparent outline-none w-full min-h-[60vh] resize-none overflow-hidden bg-transparent outline-none
font-serif text-[#38342e] leading-relaxed font-serif leading-relaxed
placeholder:text-[#b8b3a8]
" "
@input="onEditorInput" @input="onEditorInput"
@keydown="onEditorKeydown" @keydown="onEditorKeydown"
@@ -583,7 +582,7 @@ defineExpose({
.markdown-preview { .markdown-preview {
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
line-height: 1.625; line-height: 1.625;
color: #38342e; color: var(--app-text);
word-break: break-word; word-break: break-word;
} }
@@ -592,6 +591,11 @@ defineExpose({
.markdown-source-editor { .markdown-source-editor {
display: block; display: block;
height: auto; 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 { .markdown-mixed > .markdown-block:first-child .markdown-block-preview > *:first-child {
@@ -605,7 +609,7 @@ defineExpose({
.markdown-preview h5, .markdown-preview h5,
.markdown-preview h6 { .markdown-preview h6 {
font-weight: 700; font-weight: 700;
color: #2d2a26; color: var(--app-text-strong);
line-height: 1.3; line-height: 1.3;
margin-top: 1.5em; margin-top: 1.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
@@ -617,13 +621,13 @@ defineExpose({
.markdown-preview p { margin: 0.5em 0; } .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 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 { .markdown-preview code {
background-color: #f4f1ea; background-color: var(--app-surface-muted);
color: #bf6a3b; color: var(--app-accent);
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 0.125rem 0.375rem; padding: 0.125rem 0.375rem;
font-size: 0.875em; font-size: 0.875em;
@@ -632,7 +636,7 @@ defineExpose({
} }
.markdown-preview pre { .markdown-preview pre {
background-color: #f4f1ea; background-color: var(--app-surface-muted);
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 1rem; padding: 1rem;
overflow-x: auto; overflow-x: auto;
@@ -641,19 +645,19 @@ defineExpose({
.markdown-preview pre code { .markdown-preview pre code {
background: none; background: none;
padding: 0; padding: 0;
color: #38342e; color: var(--app-text);
font-size: 0.875em; font-size: 0.875em;
border-radius: 0; border-radius: 0;
word-break: normal; word-break: normal;
} }
.markdown-preview a { color: #bf6a3b; text-decoration: underline; } .markdown-preview a { color: var(--app-accent); text-decoration: underline; }
.markdown-preview blockquote { .markdown-preview blockquote {
border-left: 4px solid #bf6a3b; border-left: 4px solid var(--app-accent);
padding-left: 1rem; padding-left: 1rem;
margin: 0.75em 0; margin: 0.75em 0;
color: #8c877d; color: var(--app-muted);
font-style: italic; font-style: italic;
} }
.markdown-preview blockquote p { margin: 0.25em 0; } .markdown-preview blockquote p { margin: 0.25em 0; }
@@ -664,12 +668,12 @@ defineExpose({
.markdown-preview li input[type="checkbox"] { .markdown-preview li input[type="checkbox"] {
margin-right: 0.375rem; margin-right: 0.375rem;
accent-color: #bf6a3b; accent-color: var(--app-accent);
} }
.markdown-preview hr { .markdown-preview hr {
border: none; border: none;
border-top: 1px solid #e0dbcf; border-top: 1px solid var(--app-border-strong);
margin: 1.5em 0; margin: 1.5em 0;
} }
@@ -680,16 +684,16 @@ defineExpose({
} }
.markdown-preview th, .markdown-preview th,
.markdown-preview td { .markdown-preview td {
border: 1px solid #e0dbcf; border: 1px solid var(--app-border-strong);
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
text-align: left; text-align: left;
} }
.markdown-preview th { .markdown-preview th {
background-color: #f4f1ea; background-color: var(--app-surface-muted);
font-weight: 700; 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 { .markdown-preview img {
max-width: 100%; max-width: 100%;
@@ -716,7 +720,7 @@ defineExpose({
} }
.markdown-block:not(.markdown-block-editing):hover > .markdown-block-preview > * { .markdown-block:not(.markdown-block-editing):hover > .markdown-block-preview > * {
background-color: #f4f1ea; background-color: var(--app-surface-muted);
cursor: text; cursor: text;
} }
@@ -730,13 +734,13 @@ defineExpose({
} }
.markdown-block-preview:focus-visible > * { .markdown-block-preview:focus-visible > * {
outline: 2px solid #d6aa8e; outline: 2px solid var(--app-accent-muted);
outline-offset: 2px; outline-offset: 2px;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.markdown-empty-placeholder:focus-visible { .markdown-empty-placeholder:focus-visible {
outline: 2px solid #d6aa8e; outline: 2px solid var(--app-accent-muted);
outline-offset: 2px; outline-offset: 2px;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
@@ -747,22 +751,22 @@ defineExpose({
min-height: calc((1em * 1.6) + 1.5rem + 2px); min-height: calc((1em * 1.6) + 1.5rem + 2px);
resize: none; resize: none;
overflow: hidden; overflow: hidden;
border: 1px solid #d6aa8e; border: 1px solid var(--app-accent-muted);
border-radius: 0.375rem; border-radius: 0.375rem;
background-color: #fffdfa; background-color: var(--app-surface);
color: #38342e; color: var(--app-text);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.92em; font-size: 0.92em;
line-height: 1.6; line-height: 1.6;
margin: 0.375rem 0; margin: 0.375rem 0;
padding: 0.75rem 0.875rem; padding: 0.75rem 0.875rem;
outline: none; 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-block-editor::placeholder,
.markdown-empty-placeholder { .markdown-empty-placeholder {
color: #b8b3a8; color: var(--app-subtle);
} }
.markdown-empty-placeholder { .markdown-empty-placeholder {

View File

@@ -46,11 +46,11 @@ const onHeadingClick = (item: OutlineItem) => {
<template> <template>
<aside <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 --> <!-- Header -->
<div class="flex items-center px-3 py-3 border-b border-[#e8e4da]"> <div class="outline-panel-header flex items-center px-3 py-3">
<span class="text-xs font-semibold text-[#8c877d] uppercase tracking-wider"> <span class="outline-panel-title text-xs font-semibold uppercase tracking-wider">
大纲 大纲
</span> </span>
</div> </div>
@@ -58,7 +58,7 @@ const onHeadingClick = (item: OutlineItem) => {
<!-- Outline content --> <!-- Outline content -->
<div class="flex-1 overflow-y-auto px-2 py-2"> <div class="flex-1 overflow-y-auto px-2 py-2">
<template v-if="headings.length === 0"> <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 />自动生成大纲 暂无标题<br />使用 H1-H3 标题<br />自动生成大纲
</p> </p>
</template> </template>
@@ -67,12 +67,12 @@ const onHeadingClick = (item: OutlineItem) => {
v-for="item in headings" v-for="item in headings"
:key="item.id" :key="item.id"
:class="[ :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 item.level === 1
? 'text-[#38342e] font-medium ml-0' ? 'outline-item-level-1 font-medium ml-0'
: item.level === 2 : item.level === 2
? 'text-[#8c877d] ml-3' ? 'outline-item-level-2 ml-3'
: 'text-[#b8b3a8] ml-6 text-xs', : 'outline-item-level-3 ml-6 text-xs',
]" ]"
:title="item.text" :title="item.text"
@click="onHeadingClick(item)" @click="onHeadingClick(item)"
@@ -83,3 +83,32 @@ const onHeadingClick = (item: OutlineItem) => {
</div> </div>
</aside> </aside>
</template> </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 { getVersion } from "@tauri-apps/api/app";
import { computed, onBeforeUnmount, ref, watch } from "vue"; import { computed, onBeforeUnmount, ref, watch } from "vue";
type SettingsPage = "about" | "editor"; type SettingsPage = "about" | "editor" | "appearance";
type DocumentMode = "preview" | "edit" | "mixed"; type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
show: boolean; show: boolean;
initialPage?: SettingsPage; initialPage?: SettingsPage;
defaultDocumentMode?: DocumentMode; defaultDocumentMode?: DocumentMode;
appTheme?: AppTheme;
}>(), }>(),
{ {
initialPage: "about", initialPage: "about",
defaultDocumentMode: "edit", defaultDocumentMode: "edit",
appTheme: "warm",
}, },
); );
const emit = defineEmits<{ const emit = defineEmits<{
"update:show": [value: boolean]; "update:show": [value: boolean];
updateDefaultDocumentMode: [mode: DocumentMode]; updateDefaultDocumentMode: [mode: DocumentMode];
updateAppTheme: [theme: AppTheme];
}>(); }>();
const APP_NAME = "yurou"; const APP_NAME = "yurou";
@@ -34,7 +38,11 @@ const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans"));
const updateStatus = ref(""); const updateStatus = ref("");
let updateStatusTimer: ReturnType<typeof setTimeout> | null = null; 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<{ const menuItems: Array<{
id: SettingsPage; id: SettingsPage;
@@ -43,6 +51,7 @@ const menuItems: Array<{
}> = [ }> = [
{ id: "about", label: "关于", icon: "ri-information-line" }, { id: "about", label: "关于", icon: "ri-information-line" },
{ id: "editor", label: "编辑器", icon: "ri-edit-2-line" }, { id: "editor", label: "编辑器", icon: "ri-edit-2-line" },
{ id: "appearance", label: "外观", icon: "ri-palette-line" },
]; ];
const documentModeOptions: Array<{ const documentModeOptions: Array<{
@@ -54,6 +63,19 @@ const documentModeOptions: Array<{
{ value: "mixed", label: "混合" }, { 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) { function loadBooleanPref(key: string, fallback: boolean) {
try { try {
const stored = localStorage.getItem(key); const stored = localStorage.getItem(key);
@@ -118,6 +140,15 @@ function onDefaultDocumentModeChange(event: Event) {
emit("updateDefaultDocumentMode", mode); 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( watch(
() => props.show, () => props.show,
(show) => { (show) => {
@@ -242,7 +273,7 @@ onBeforeUnmount(() => {
</section> </section>
</div> </div>
<div v-else class="settings-content"> <div v-else-if="activePage === 'editor'" class="settings-content">
<section class="settings-about-card" aria-label="编辑器设置"> <section class="settings-about-card" aria-label="编辑器设置">
<div class="settings-row settings-row-top"> <div class="settings-row settings-row-top">
<div class="settings-copy"> <div class="settings-copy">
@@ -266,6 +297,59 @@ onBeforeUnmount(() => {
</div> </div>
</section> </section>
</div> </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> </div>
</section> </section>
</div> </div>
@@ -281,7 +365,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 28px; padding: 28px;
background: rgba(56, 52, 46, 0.22); background: var(--app-overlay);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
@@ -292,12 +376,12 @@ onBeforeUnmount(() => {
min-height: 430px; min-height: 430px;
max-height: min(680px, calc(100vh - 48px)); max-height: min(680px, calc(100vh - 48px));
overflow: hidden; overflow: hidden;
border: 1px solid #e0dbcf; border: 1px solid var(--app-border-strong);
border-radius: 12px; border-radius: 12px;
background: #fffdfa; background: var(--app-surface);
box-shadow: box-shadow:
0 24px 70px rgba(56, 52, 46, 0.2), 0 24px 70px var(--app-shadow),
0 2px 10px rgba(56, 52, 46, 0.08); 0 2px 10px color-mix(in srgb, var(--app-shadow) 46%, transparent);
} }
.settings-menu { .settings-menu {
@@ -305,13 +389,13 @@ onBeforeUnmount(() => {
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
padding: 14px 10px; padding: 14px 10px;
border-right: 1px solid #e8e4da; border-right: 1px solid var(--app-border);
background: #f4f1eb; background: var(--app-sidebar-bg);
} }
.settings-menu-title { .settings-menu-title {
padding: 0 10px 8px; padding: 0 10px 8px;
color: #8c877d; color: var(--app-muted);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
} }
@@ -326,7 +410,7 @@ onBeforeUnmount(() => {
border: none; border: none;
border-radius: 7px; border-radius: 7px;
background: transparent; background: transparent;
color: #5c574e; color: var(--app-text-soft);
font-size: 13px; font-size: 13px;
line-height: 1; line-height: 1;
text-align: left; text-align: left;
@@ -337,13 +421,13 @@ onBeforeUnmount(() => {
} }
.settings-menu-item:hover { .settings-menu-item:hover {
background: #ede8dd; background: var(--app-hover);
color: #38342e; color: var(--app-text);
} }
.settings-menu-item-active { .settings-menu-item-active {
background: #fdf0e5; background: var(--app-active-bg);
color: #bf6a3b; color: var(--app-accent);
font-weight: 600; font-weight: 600;
} }
@@ -351,7 +435,7 @@ onBeforeUnmount(() => {
display: flex; display: flex;
min-width: 0; min-width: 0;
flex-direction: column; flex-direction: column;
background: #faf9f6; background: var(--app-panel-bg);
} }
.settings-header { .settings-header {
@@ -360,13 +444,13 @@ onBeforeUnmount(() => {
justify-content: space-between; justify-content: space-between;
min-height: 54px; min-height: 54px;
padding: 0 20px; padding: 0 20px;
border-bottom: 1px solid #e8e4da; border-bottom: 1px solid var(--app-border);
background: rgba(255, 253, 250, 0.72); background: color-mix(in srgb, var(--app-surface) 72%, transparent);
} }
.settings-header h2 { .settings-header h2 {
margin: 0; margin: 0;
color: #38342e; color: var(--app-text);
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
} }
@@ -380,7 +464,7 @@ onBeforeUnmount(() => {
border: none; border: none;
border-radius: 6px; border-radius: 6px;
background: transparent; background: transparent;
color: #8c877d; color: var(--app-muted);
cursor: pointer; cursor: pointer;
transition: transition:
background-color 0.15s ease, background-color 0.15s ease,
@@ -388,8 +472,8 @@ onBeforeUnmount(() => {
} }
.settings-close-btn:hover { .settings-close-btn:hover {
background: #ede8dd; background: var(--app-hover);
color: #38342e; color: var(--app-text);
} }
.settings-content { .settings-content {
@@ -402,8 +486,8 @@ onBeforeUnmount(() => {
.settings-about-card { .settings-about-card {
overflow: hidden; overflow: hidden;
border-radius: 10px; border-radius: 10px;
background: #fffdfa; background: var(--app-surface);
box-shadow: inset 0 0 0 1px #eee9de; box-shadow: inset 0 0 0 1px var(--app-border);
} }
.settings-row { .settings-row {
@@ -413,7 +497,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
min-height: 78px; min-height: 78px;
padding: 16px 20px; padding: 16px 20px;
border-top: 1px solid #e0dbcf; border-top: 1px solid var(--app-border-strong);
} }
.settings-row-top { .settings-row-top {
@@ -427,7 +511,7 @@ onBeforeUnmount(() => {
.settings-copy h3, .settings-copy h3,
.settings-empty-page h3 { .settings-empty-page h3 {
margin: 0 0 6px; margin: 0 0 6px;
color: #2d2a26; color: var(--app-text-strong);
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 1.25; line-height: 1.25;
@@ -436,7 +520,7 @@ onBeforeUnmount(() => {
.settings-copy p, .settings-copy p,
.settings-empty-page p { .settings-empty-page p {
margin: 0; margin: 0;
color: #5c574e; color: var(--app-text-soft);
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
} }
@@ -447,7 +531,7 @@ onBeforeUnmount(() => {
padding: 0; padding: 0;
border: none; border: none;
background: transparent; background: transparent;
color: #7a55d9; color: var(--app-link);
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
text-align: left; text-align: left;
@@ -457,7 +541,7 @@ onBeforeUnmount(() => {
} }
.settings-link:hover { .settings-link:hover {
color: #6342bd; color: var(--app-link-hover);
} }
.settings-action-stack { .settings-action-stack {
@@ -487,29 +571,29 @@ onBeforeUnmount(() => {
.settings-primary-btn { .settings-primary-btn {
min-width: 78px; min-width: 78px;
border: none; border: none;
background: #8c6be8; background: var(--app-control);
color: #fff; color: var(--app-control-contrast);
box-shadow: 0 2px 7px rgba(100, 65, 190, 0.26); box-shadow: 0 2px 7px color-mix(in srgb, var(--app-control) 32%, transparent);
} }
.settings-primary-btn:hover { .settings-primary-btn:hover {
background: #7a55d9; background: var(--app-control-hover);
} }
.settings-secondary-btn { .settings-secondary-btn {
min-width: 50px; min-width: 50px;
border: 1px solid #e0dbcf; border: 1px solid var(--app-border-strong);
background: #fff; background: var(--app-surface);
color: #38342e; color: var(--app-text);
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);
} }
.settings-secondary-btn:hover { .settings-secondary-btn:hover {
background: #f4f1ea; background: var(--app-surface-muted);
} }
.settings-status { .settings-status {
color: #6f6a60; color: var(--app-muted);
font-size: 11px; font-size: 11px;
white-space: nowrap; white-space: nowrap;
} }
@@ -532,7 +616,7 @@ onBeforeUnmount(() => {
position: absolute; position: absolute;
inset: 0; inset: 0;
border-radius: 999px; border-radius: 999px;
background: #d4cfc4; background: var(--app-border-strong);
transition: background-color 0.16s ease; transition: background-color 0.16s ease;
} }
@@ -543,14 +627,14 @@ onBeforeUnmount(() => {
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 50%; border-radius: 50%;
background: #fff; background: var(--app-surface);
box-shadow: 0 1px 3px rgba(56, 52, 46, 0.24); box-shadow: 0 1px 3px var(--app-shadow);
content: ""; content: "";
transition: transform 0.16s ease; transition: transform 0.16s ease;
} }
.settings-switch input:checked + span { .settings-switch input:checked + span {
background: #8c6be8; background: var(--app-control);
} }
.settings-switch input:checked + span::after { .settings-switch input:checked + span::after {
@@ -558,34 +642,76 @@ onBeforeUnmount(() => {
} }
.settings-switch input:focus-visible + span { .settings-switch input:focus-visible + span {
outline: 2px solid #d6aa8e; outline: 2px solid var(--app-accent-muted);
outline-offset: 2px; outline-offset: 2px;
} }
.settings-select { .settings-select {
width: 158px; width: 158px;
height: 30px; height: 30px;
border: 1px solid #e0dbcf; border: 1px solid var(--app-border-strong);
border-radius: 6px; border-radius: 6px;
background: #fff; background: var(--app-surface);
color: #38342e; color: var(--app-text);
font-size: 13px; font-size: 13px;
line-height: 1; line-height: 1;
padding: 0 10px; 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; outline: none;
} }
.settings-select:focus { .settings-select:focus {
border-color: #d6aa8e; border-color: var(--app-accent-muted);
box-shadow: 0 0 0 3px rgba(191, 106, 59, 0.1); 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 { .settings-empty-page {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #8c877d; color: var(--app-muted);
text-align: center; text-align: center;
} }
@@ -609,7 +735,7 @@ onBeforeUnmount(() => {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
border-right: none; border-right: none;
border-bottom: 1px solid #e8e4da; border-bottom: 1px solid var(--app-border);
} }
.settings-menu-title { .settings-menu-title {

View File

@@ -57,15 +57,15 @@ function onClick() {
<template v-if="entry.is_dir"> <template v-if="entry.is_dir">
<!-- Folder label --> <!-- Folder label -->
<div <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` }" :style="{ paddingLeft: `${depth * 16 + 8}px` }"
@click="onClick" @click="onClick"
> >
<i <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' : ''" :class="expanded && entry.children ? 'rotate-90' : ''"
></i> ></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> <span class="truncate">{{ entry.name }}</span>
</div> </div>
@@ -87,15 +87,47 @@ function onClick() {
<div <div
v-else v-else
:class="[ :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 isActive
? 'bg-[#ede8de] text-[#bf6a3b] font-medium' ? 'tree-file-active font-medium'
: 'text-[#8c877d] hover:bg-[#ede8de] hover:text-[#38342e]', : 'tree-file-idle',
]" ]"
:style="{ paddingLeft: `${depth * 16 + 8}px` }" :style="{ paddingLeft: `${depth * 16 + 8}px` }"
@click="onClick" @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> <span class="truncate">{{ entry.name }}</span>
</div> </div>
</template> </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"; @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 { html {
/* ── Font stack: refined system serif for Chinese + English ── */ /* ── Font stack: refined system serif for Chinese + English ── */
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
@@ -16,14 +200,14 @@ html {
body { body {
margin: 0; margin: 0;
background: #faf9f6; background: var(--app-bg);
color: #38342e; color: var(--app-text);
} }
/* ── Selection ── */ /* ── Selection ── */
::selection { ::selection {
background: #e8d5b0; background: var(--app-selection);
color: #38342e; color: var(--app-text);
} }
/* ============================================ /* ============================================
@@ -33,7 +217,7 @@ body {
/* Firefox */ /* Firefox */
* { * {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #d4cfc4 transparent; scrollbar-color: var(--app-border-strong) transparent;
} }
/* WebKit (Chrome, Edge, Safari, Tauri WebView2) */ /* WebKit (Chrome, Edge, Safari, Tauri WebView2) */
@@ -47,17 +231,17 @@ body {
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #d4cfc4; background: var(--app-border-strong);
border-radius: 9999px; border-radius: 9999px;
transition: background 0.2s ease; transition: background 0.2s ease;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #b0a99b; background: var(--app-muted);
} }
::-webkit-scrollbar-thumb:active { ::-webkit-scrollbar-thumb:active {
background: #9a9282; background: var(--app-subtle);
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {