页面美化
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
"height": 600,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600,
|
||||
"decorations": true
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
130
src/App.vue
130
src/App.vue
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
202
src/tailwind.css
202
src/tailwind.css
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user