添加设置页面
This commit is contained in:
82
src/App.vue
82
src/App.vue
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
|
||||
import {listen} from "@tauri-apps/api/event";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
||||
import {invoke, isTauri} from "@tauri-apps/api/core";
|
||||
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||
@@ -11,6 +11,27 @@ const wordCount = ref(0);
|
||||
const charCount = ref(0);
|
||||
|
||||
type DocumentMode = "preview" | "edit" | "mixed";
|
||||
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
|
||||
|
||||
function isDocumentMode(value: string | null): value is DocumentMode {
|
||||
return value === "preview" || value === "edit" || value === "mixed";
|
||||
}
|
||||
|
||||
function loadDefaultDocumentMode(): DocumentMode {
|
||||
try {
|
||||
const stored = localStorage.getItem(DEFAULT_DOCUMENT_MODE_KEY);
|
||||
if (isDocumentMode(stored)) return stored;
|
||||
} catch { /* localStorage unavailable */
|
||||
}
|
||||
return "edit";
|
||||
}
|
||||
|
||||
function saveDefaultDocumentMode(mode: DocumentMode) {
|
||||
try {
|
||||
localStorage.setItem(DEFAULT_DOCUMENT_MODE_KEY, mode);
|
||||
} catch { /* noop */
|
||||
}
|
||||
}
|
||||
|
||||
// File state
|
||||
const currentFilePath = ref<string | null>(null);
|
||||
@@ -81,13 +102,31 @@ function onEditorWheel(e: WheelEvent) {
|
||||
|
||||
// ---- toolbar state ----
|
||||
const showOutline = ref(false);
|
||||
const documentMode = ref<DocumentMode>("edit");
|
||||
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
|
||||
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
|
||||
const showMoreMenu = ref(false);
|
||||
const settingsRequest = ref(0);
|
||||
const settingsInitialPage = ref<"about" | "editor">("about");
|
||||
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
|
||||
const showStartActions = computed(() => {
|
||||
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
|
||||
});
|
||||
|
||||
function openSettings(page: "about" | "editor" = "about") {
|
||||
settingsInitialPage.value = page;
|
||||
settingsRequest.value += 1;
|
||||
}
|
||||
|
||||
function setDocumentMode(mode: DocumentMode) {
|
||||
documentMode.value = mode;
|
||||
}
|
||||
|
||||
function updateDefaultDocumentMode(mode: DocumentMode) {
|
||||
defaultDocumentMode.value = mode;
|
||||
documentMode.value = mode;
|
||||
saveDefaultDocumentMode(mode);
|
||||
}
|
||||
|
||||
function handleExportPdf() {
|
||||
showMoreMenu.value = false;
|
||||
// TODO: implement PDF export
|
||||
@@ -162,7 +201,7 @@ function resetMarkdownEditorState() {
|
||||
hasDraftDocument.value = false;
|
||||
saveStatus.value = "";
|
||||
showOutline.value = false;
|
||||
documentMode.value = "edit";
|
||||
documentMode.value = defaultDocumentMode.value;
|
||||
showMoreMenu.value = false;
|
||||
editorResetKey.value += 1;
|
||||
}
|
||||
@@ -345,27 +384,40 @@ function doNew() {
|
||||
currentFilePath.value = null;
|
||||
hasDraftDocument.value = true;
|
||||
saveStatus.value = "";
|
||||
documentMode.value = defaultDocumentMode.value;
|
||||
editorResetKey.value += 1;
|
||||
}
|
||||
|
||||
function registerTauriListener<T>(eventName: EventName, handler: EventCallback<T>) {
|
||||
if (!isTauri()) return;
|
||||
|
||||
void listen<T>(eventName, handler).catch((error) => {
|
||||
console.error(`Failed to listen for ${eventName}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for menu events from Rust
|
||||
listen<string>("folder-opened", (event) => {
|
||||
registerTauriListener<string>("folder-opened", (event) => {
|
||||
void switchWorkspace(event.payload);
|
||||
});
|
||||
|
||||
listen("menu-save", () => {
|
||||
registerTauriListener("menu-save", () => {
|
||||
doSave();
|
||||
});
|
||||
|
||||
listen("menu-save-as", () => {
|
||||
registerTauriListener("menu-save-as", () => {
|
||||
doSaveAs();
|
||||
});
|
||||
|
||||
listen("menu-new", () => {
|
||||
registerTauriListener("menu-new", () => {
|
||||
doNew();
|
||||
});
|
||||
|
||||
listen("menu-event", (event) => {
|
||||
registerTauriListener("show-about", () => {
|
||||
openSettings("about");
|
||||
});
|
||||
|
||||
registerTauriListener("menu-event", (event) => {
|
||||
console.log("Menu event:", event.payload);
|
||||
});
|
||||
</script>
|
||||
@@ -379,9 +431,13 @@ listen("menu-event", (event) => {
|
||||
:folder-path="folderPath"
|
||||
:entries="dirEntries"
|
||||
:current-file-path="currentFilePath"
|
||||
:settings-request="settingsRequest"
|
||||
:settings-initial-page="settingsInitialPage"
|
||||
:default-document-mode="defaultDocumentMode"
|
||||
@open-folder="onOpenFolder"
|
||||
@toggle-folder="onToggleFolder"
|
||||
@open-file="onOpenFile"
|
||||
@update-default-document-mode="updateDefaultDocumentMode"
|
||||
/>
|
||||
|
||||
<!-- Right: Editor area -->
|
||||
@@ -414,7 +470,7 @@ listen("menu-event", (event) => {
|
||||
class="mode-toggle-btn"
|
||||
:class="{ 'mode-toggle-btn-active': documentMode === 'preview' }"
|
||||
title="预览模式"
|
||||
@click="documentMode = 'preview'"
|
||||
@click="setDocumentMode('preview')"
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
@@ -422,7 +478,7 @@ listen("menu-event", (event) => {
|
||||
class="mode-toggle-btn"
|
||||
:class="{ 'mode-toggle-btn-active': documentMode === 'edit' }"
|
||||
title="编辑模式"
|
||||
@click="documentMode = 'edit'"
|
||||
@click="setDocumentMode('edit')"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
@@ -430,7 +486,7 @@ listen("menu-event", (event) => {
|
||||
class="mode-toggle-btn"
|
||||
:class="{ 'mode-toggle-btn-active': documentMode === 'mixed' }"
|
||||
title="混合模式"
|
||||
@click="documentMode = 'mixed'"
|
||||
@click="setDocumentMode('mixed')"
|
||||
>
|
||||
混合
|
||||
</button>
|
||||
@@ -481,7 +537,7 @@ listen("menu-event", (event) => {
|
||||
ref="markdownEditorRef"
|
||||
:model-value="content"
|
||||
:document-mode="documentMode"
|
||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
|
||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
|
||||
@update:model-value="onUpdate"
|
||||
@save-shortcut="doSave"
|
||||
class="flex-1 w-full min-w-0"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from "vue";
|
||||
import {ref, computed, watch} from "vue";
|
||||
import SettingsModal from "./SettingsModal.vue";
|
||||
import TreeEntry from "./TreeEntry.vue";
|
||||
|
||||
type SettingsPage = "about" | "editor";
|
||||
type DocumentMode = "preview" | "edit" | "mixed";
|
||||
|
||||
interface DirEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
@@ -14,9 +18,15 @@ const props = withDefaults(
|
||||
folderPath: string;
|
||||
entries: DirEntry[];
|
||||
currentFilePath?: string | null;
|
||||
settingsRequest?: number;
|
||||
settingsInitialPage?: SettingsPage;
|
||||
defaultDocumentMode?: DocumentMode;
|
||||
}>(),
|
||||
{
|
||||
currentFilePath: null,
|
||||
settingsRequest: 0,
|
||||
settingsInitialPage: "about",
|
||||
defaultDocumentMode: "edit",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -26,6 +36,7 @@ const emit = defineEmits<{
|
||||
openFile: [path: string];
|
||||
createFile: [];
|
||||
createFolder: [];
|
||||
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||
}>();
|
||||
|
||||
// Wrap the workspace root as a top-level tree node
|
||||
@@ -56,6 +67,17 @@ function switchView(view: ViewMode) {
|
||||
|
||||
// ---- settings modal ----
|
||||
const showSettings = ref(false);
|
||||
const settingsPage = ref<SettingsPage>("about");
|
||||
|
||||
watch(() => props.settingsRequest, (request) => {
|
||||
if (request <= 0) return;
|
||||
openSettings(props.settingsInitialPage);
|
||||
});
|
||||
|
||||
function openSettings(page: SettingsPage) {
|
||||
settingsPage.value = page;
|
||||
showSettings.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,9 +107,9 @@ const showSettings = ref(false);
|
||||
<!-- ===== Settings ===== -->
|
||||
<button
|
||||
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
|
||||
:class="activeView === 'setting' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
|
||||
:class="showSettings ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
|
||||
title="设置"
|
||||
@click="showSettings = true"
|
||||
@click="openSettings('about')"
|
||||
>
|
||||
<i class="ri-settings-line text-sm"></i>
|
||||
</button>
|
||||
@@ -147,26 +169,10 @@ const showSettings = ref(false);
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ===== SETTINGS MODAL ===== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showSettings"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-[#38342e]/25 backdrop-blur-sm"
|
||||
@click.self="showSettings = false"
|
||||
>
|
||||
<div class="bg-white border border-[#e0dbcf] rounded-xl shadow-2xl w-80 max-h-[70vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[#e8e4da]">
|
||||
<span class="text-sm font-semibold text-[#38342e]">设置</span>
|
||||
<button
|
||||
class="p-1 rounded-md text-[#8c877d] hover:text-[#38342e] hover:bg-[#e0dbcf] transition-colors"
|
||||
@click="showSettings = false"
|
||||
>
|
||||
<i class="ri-close-line text-base"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-4 py-6 space-y-5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<SettingsModal
|
||||
v-model:show="showSettings"
|
||||
:initial-page="settingsPage"
|
||||
:default-document-mode="defaultDocumentMode"
|
||||
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
632
src/components/SettingsModal.vue
Normal file
632
src/components/SettingsModal.vue
Normal file
@@ -0,0 +1,632 @@
|
||||
<script setup lang="ts">
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
|
||||
type SettingsPage = "about" | "editor";
|
||||
type DocumentMode = "preview" | "edit" | "mixed";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
initialPage?: SettingsPage;
|
||||
defaultDocumentMode?: DocumentMode;
|
||||
}>(),
|
||||
{
|
||||
initialPage: "about",
|
||||
defaultDocumentMode: "edit",
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:show": [value: boolean];
|
||||
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||
}>();
|
||||
|
||||
const APP_NAME = "yurou";
|
||||
const FALLBACK_VERSION = "0.1.0";
|
||||
const AUTO_UPDATE_KEY = "yurou:auto-update";
|
||||
const LANGUAGE_KEY = "yurou:language";
|
||||
|
||||
const activePage = ref<SettingsPage>(props.initialPage);
|
||||
const appVersion = ref(FALLBACK_VERSION);
|
||||
const autoUpdate = ref(loadBooleanPref(AUTO_UPDATE_KEY, true));
|
||||
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 menuItems: Array<{
|
||||
id: SettingsPage;
|
||||
label: string;
|
||||
icon: string;
|
||||
}> = [
|
||||
{ id: "about", label: "关于", icon: "ri-information-line" },
|
||||
{ id: "editor", label: "编辑器", icon: "ri-edit-2-line" },
|
||||
];
|
||||
|
||||
const documentModeOptions: Array<{
|
||||
value: DocumentMode;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "preview", label: "预览" },
|
||||
{ value: "edit", label: "编辑" },
|
||||
{ value: "mixed", label: "混合" },
|
||||
];
|
||||
|
||||
function loadBooleanPref(key: string, fallback: boolean) {
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === "true") return true;
|
||||
if (stored === "false") return false;
|
||||
} catch {
|
||||
// localStorage may be unavailable in restricted environments.
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function loadStringPref(key: string, fallback: string) {
|
||||
try {
|
||||
return localStorage.getItem(key) || fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function savePref(key: string, value: string) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit("update:show", false);
|
||||
}
|
||||
|
||||
function onOverlayKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") close();
|
||||
}
|
||||
|
||||
async function loadAppVersion() {
|
||||
try {
|
||||
appVersion.value = await getVersion();
|
||||
} catch {
|
||||
appVersion.value = FALLBACK_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
function checkForUpdates() {
|
||||
updateStatus.value = "当前已是最新版本";
|
||||
if (updateStatusTimer) clearTimeout(updateStatusTimer);
|
||||
updateStatusTimer = setTimeout(() => {
|
||||
updateStatus.value = "";
|
||||
}, 2400);
|
||||
}
|
||||
|
||||
function openReleaseNotes() {
|
||||
window.open("https://gitea.cirry.cn/cirry/yurou/releases", "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function openHelp() {
|
||||
window.open("https://gitea.cirry.cn/cirry/yurou", "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function onDefaultDocumentModeChange(event: Event) {
|
||||
const mode = (event.target as HTMLSelectElement).value as DocumentMode;
|
||||
emit("updateDefaultDocumentMode", mode);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (!show) return;
|
||||
activePage.value = props.initialPage;
|
||||
updateStatus.value = "";
|
||||
void loadAppVersion();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.initialPage,
|
||||
(page) => {
|
||||
if (props.show) activePage.value = page;
|
||||
},
|
||||
);
|
||||
|
||||
watch(autoUpdate, (value) => {
|
||||
savePref(AUTO_UPDATE_KEY, String(value));
|
||||
});
|
||||
|
||||
watch(language, (value) => {
|
||||
savePref(LANGUAGE_KEY, value);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (updateStatusTimer) clearTimeout(updateStatusTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="show"
|
||||
class="settings-overlay"
|
||||
role="presentation"
|
||||
@click.self="close"
|
||||
@keydown="onOverlayKeydown"
|
||||
>
|
||||
<section
|
||||
class="settings-shell"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="`设置 - ${pageTitle}`"
|
||||
>
|
||||
<aside class="settings-menu" aria-label="设置菜单">
|
||||
<div class="settings-menu-title">设置</div>
|
||||
<button
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
class="settings-menu-item"
|
||||
:class="{ 'settings-menu-item-active': activePage === item.id }"
|
||||
type="button"
|
||||
@click="activePage = item.id"
|
||||
>
|
||||
<i :class="[item.icon, 'text-base']"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<div class="settings-main">
|
||||
<header class="settings-header">
|
||||
<h2>{{ pageTitle }}</h2>
|
||||
<button class="settings-close-btn" type="button" title="关闭设置" @click="close">
|
||||
<i class="ri-close-line text-lg"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="activePage === 'about'" class="settings-content">
|
||||
<section class="settings-about-card" aria-label="关于 yurou">
|
||||
<div class="settings-row settings-row-top">
|
||||
<div class="settings-copy">
|
||||
<h3>版本 {{ appVersion }}</h3>
|
||||
<p>安装程序版本: {{ appVersion }}</p>
|
||||
<button class="settings-link" type="button" @click="openReleaseNotes">
|
||||
阅读更新日志
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings-action-stack">
|
||||
<button class="settings-primary-btn" type="button" @click="checkForUpdates">
|
||||
检查更新
|
||||
</button>
|
||||
<span v-if="updateStatus" class="settings-status">{{ updateStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-copy">
|
||||
<h3>自动更新</h3>
|
||||
<p>关闭后 {{ APP_NAME }} 将不会自动更新。</p>
|
||||
</div>
|
||||
<label class="settings-switch" title="自动更新">
|
||||
<input v-model="autoUpdate" type="checkbox" />
|
||||
<span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-copy">
|
||||
<h3>语言</h3>
|
||||
<p>更改界面语言。</p>
|
||||
<button class="settings-link" type="button">
|
||||
显示如何给 {{ APP_NAME }} 添加你的母语
|
||||
</button>
|
||||
</div>
|
||||
<select v-model="language" class="settings-select" aria-label="界面语言">
|
||||
<option value="zh-Hans">简体中文</option>
|
||||
<option value="zh-Hant">繁体中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-copy">
|
||||
<h3>获取帮助</h3>
|
||||
<p>获取关于使用 {{ APP_NAME }} 的帮助。</p>
|
||||
</div>
|
||||
<button class="settings-secondary-btn" type="button" @click="openHelp">
|
||||
打开
|
||||
</button>
|
||||
</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>在编辑视图下,新标签页默认采用的编辑模式。</p>
|
||||
</div>
|
||||
<select
|
||||
class="settings-select"
|
||||
aria-label="默认编辑模式"
|
||||
:value="defaultDocumentMode"
|
||||
@change="onDefaultDocumentModeChange"
|
||||
>
|
||||
<option
|
||||
v-for="option in documentModeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px;
|
||||
background: rgba(56, 52, 46, 0.22);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.settings-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 176px minmax(0, 1fr);
|
||||
width: min(820px, calc(100vw - 48px));
|
||||
min-height: 430px;
|
||||
max-height: min(680px, calc(100vh - 48px));
|
||||
overflow: hidden;
|
||||
border: 1px solid #e0dbcf;
|
||||
border-radius: 12px;
|
||||
background: #fffdfa;
|
||||
box-shadow:
|
||||
0 24px 70px rgba(56, 52, 46, 0.2),
|
||||
0 2px 10px rgba(56, 52, 46, 0.08);
|
||||
}
|
||||
|
||||
.settings-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 14px 10px;
|
||||
border-right: 1px solid #e8e4da;
|
||||
background: #f4f1eb;
|
||||
}
|
||||
|
||||
.settings-menu-title {
|
||||
padding: 0 10px 8px;
|
||||
color: #8c877d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: #5c574e;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-menu-item:hover {
|
||||
background: #ede8dd;
|
||||
color: #38342e;
|
||||
}
|
||||
|
||||
.settings-menu-item-active {
|
||||
background: #fdf0e5;
|
||||
color: #bf6a3b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
background: #faf9f6;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 54px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #e8e4da;
|
||||
background: rgba(255, 253, 250, 0.72);
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
margin: 0;
|
||||
color: #38342e;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settings-close-btn {
|
||||
display: inline-flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #8c877d;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-close-btn:hover {
|
||||
background: #ede8dd;
|
||||
color: #38342e;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.settings-about-card {
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: #fffdfa;
|
||||
box-shadow: inset 0 0 0 1px #eee9de;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
min-height: 78px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e0dbcf;
|
||||
}
|
||||
|
||||
.settings-row-top {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.settings-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-copy h3,
|
||||
.settings-empty-page h3 {
|
||||
margin: 0 0 6px;
|
||||
color: #2d2a26;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.settings-copy p,
|
||||
.settings-empty-page p {
|
||||
margin: 0;
|
||||
color: #5c574e;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-link {
|
||||
display: inline;
|
||||
margin: 2px 0 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #7a55d9;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-link:hover {
|
||||
color: #6342bd;
|
||||
}
|
||||
|
||||
.settings-action-stack {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.settings-primary-btn,
|
||||
.settings-secondary-btn {
|
||||
display: inline-flex;
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-primary-btn {
|
||||
min-width: 78px;
|
||||
border: none;
|
||||
background: #8c6be8;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 7px rgba(100, 65, 190, 0.26);
|
||||
}
|
||||
|
||||
.settings-primary-btn:hover {
|
||||
background: #7a55d9;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.settings-secondary-btn:hover {
|
||||
background: #f4f1ea;
|
||||
}
|
||||
|
||||
.settings-status {
|
||||
color: #6f6a60;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-switch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-switch input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.settings-switch span {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
background: #d4cfc4;
|
||||
transition: background-color 0.16s ease;
|
||||
}
|
||||
|
||||
.settings-switch span::after {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(56, 52, 46, 0.24);
|
||||
content: "";
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.settings-switch input:checked + span {
|
||||
background: #8c6be8;
|
||||
}
|
||||
|
||||
.settings-switch input:checked + span::after {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
.settings-switch input:focus-visible + span {
|
||||
outline: 2px solid #d6aa8e;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
width: 158px;
|
||||
height: 30px;
|
||||
border: 1px solid #e0dbcf;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #38342e;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
padding: 0 10px;
|
||||
box-shadow: 0 1px 4px rgba(56, 52, 46, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-select:focus {
|
||||
border-color: #d6aa8e;
|
||||
box-shadow: 0 0 0 3px rgba(191, 106, 59, 0.1);
|
||||
}
|
||||
|
||||
.settings-empty-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c877d;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-empty-page > div {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.settings-overlay {
|
||||
align-items: stretch;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.settings-shell {
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.settings-menu {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e8e4da;
|
||||
}
|
||||
|
||||
.settings-menu-title {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.settings-menu-item {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-action-stack {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user