添加设置页面

This commit is contained in:
2026-07-08 14:04:39 +08:00
parent 519aa5aff0
commit bb4e376528
3 changed files with 732 additions and 38 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, watch, onMounted, onUnmounted} from "vue"; import {computed, ref, watch, onMounted, onUnmounted} from "vue";
import {listen} from "@tauri-apps/api/event"; import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
import {invoke} from "@tauri-apps/api/core"; import {invoke, isTauri} from "@tauri-apps/api/core";
import MarkdownEditor from "./components/MarkdownEditor.vue"; import MarkdownEditor from "./components/MarkdownEditor.vue";
import DirectorySidebar from "./components/DirectorySidebar.vue"; import DirectorySidebar from "./components/DirectorySidebar.vue";
import OutlinePanel from "./components/OutlinePanel.vue"; import OutlinePanel from "./components/OutlinePanel.vue";
@@ -11,6 +11,27 @@ const wordCount = ref(0);
const charCount = ref(0); const charCount = ref(0);
type DocumentMode = "preview" | "edit" | "mixed"; 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 // File state
const currentFilePath = ref<string | null>(null); const currentFilePath = ref<string | null>(null);
@@ -81,13 +102,31 @@ function onEditorWheel(e: WheelEvent) {
// ---- toolbar state ---- // ---- toolbar state ----
const showOutline = ref(false); 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 showMoreMenu = ref(false);
const settingsRequest = ref(0);
const settingsInitialPage = ref<"about" | "editor">("about");
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null); const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
const showStartActions = computed(() => { const showStartActions = computed(() => {
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value; 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() { function handleExportPdf() {
showMoreMenu.value = false; showMoreMenu.value = false;
// TODO: implement PDF export // TODO: implement PDF export
@@ -162,7 +201,7 @@ function resetMarkdownEditorState() {
hasDraftDocument.value = false; hasDraftDocument.value = false;
saveStatus.value = ""; saveStatus.value = "";
showOutline.value = false; showOutline.value = false;
documentMode.value = "edit"; documentMode.value = defaultDocumentMode.value;
showMoreMenu.value = false; showMoreMenu.value = false;
editorResetKey.value += 1; editorResetKey.value += 1;
} }
@@ -345,27 +384,40 @@ function doNew() {
currentFilePath.value = null; currentFilePath.value = null;
hasDraftDocument.value = true; hasDraftDocument.value = true;
saveStatus.value = ""; saveStatus.value = "";
documentMode.value = defaultDocumentMode.value;
editorResetKey.value += 1; 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 for menu events from Rust
listen<string>("folder-opened", (event) => { registerTauriListener<string>("folder-opened", (event) => {
void switchWorkspace(event.payload); void switchWorkspace(event.payload);
}); });
listen("menu-save", () => { registerTauriListener("menu-save", () => {
doSave(); doSave();
}); });
listen("menu-save-as", () => { registerTauriListener("menu-save-as", () => {
doSaveAs(); doSaveAs();
}); });
listen("menu-new", () => { registerTauriListener("menu-new", () => {
doNew(); doNew();
}); });
listen("menu-event", (event) => { registerTauriListener("show-about", () => {
openSettings("about");
});
registerTauriListener("menu-event", (event) => {
console.log("Menu event:", event.payload); console.log("Menu event:", event.payload);
}); });
</script> </script>
@@ -379,9 +431,13 @@ listen("menu-event", (event) => {
:folder-path="folderPath" :folder-path="folderPath"
:entries="dirEntries" :entries="dirEntries"
:current-file-path="currentFilePath" :current-file-path="currentFilePath"
:settings-request="settingsRequest"
:settings-initial-page="settingsInitialPage"
:default-document-mode="defaultDocumentMode"
@open-folder="onOpenFolder" @open-folder="onOpenFolder"
@toggle-folder="onToggleFolder" @toggle-folder="onToggleFolder"
@open-file="onOpenFile" @open-file="onOpenFile"
@update-default-document-mode="updateDefaultDocumentMode"
/> />
<!-- Right: Editor area --> <!-- Right: Editor area -->
@@ -414,7 +470,7 @@ listen("menu-event", (event) => {
class="mode-toggle-btn" class="mode-toggle-btn"
:class="{ 'mode-toggle-btn-active': documentMode === 'preview' }" :class="{ 'mode-toggle-btn-active': documentMode === 'preview' }"
title="预览模式" title="预览模式"
@click="documentMode = 'preview'" @click="setDocumentMode('preview')"
> >
预览 预览
</button> </button>
@@ -422,7 +478,7 @@ listen("menu-event", (event) => {
class="mode-toggle-btn" class="mode-toggle-btn"
:class="{ 'mode-toggle-btn-active': documentMode === 'edit' }" :class="{ 'mode-toggle-btn-active': documentMode === 'edit' }"
title="编辑模式" title="编辑模式"
@click="documentMode = 'edit'" @click="setDocumentMode('edit')"
> >
编辑 编辑
</button> </button>
@@ -430,7 +486,7 @@ listen("menu-event", (event) => {
class="mode-toggle-btn" class="mode-toggle-btn"
:class="{ 'mode-toggle-btn-active': documentMode === 'mixed' }" :class="{ 'mode-toggle-btn-active': documentMode === 'mixed' }"
title="混合模式" title="混合模式"
@click="documentMode = 'mixed'" @click="setDocumentMode('mixed')"
> >
混合 混合
</button> </button>
@@ -481,7 +537,7 @@ listen("menu-event", (event) => {
ref="markdownEditorRef" ref="markdownEditorRef"
:model-value="content" :model-value="content"
:document-mode="documentMode" :document-mode="documentMode"
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等" placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
@update:model-value="onUpdate" @update:model-value="onUpdate"
@save-shortcut="doSave" @save-shortcut="doSave"
class="flex-1 w-full min-w-0" class="flex-1 w-full min-w-0"

View File

@@ -1,7 +1,11 @@
<script setup lang="ts"> <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"; import TreeEntry from "./TreeEntry.vue";
type SettingsPage = "about" | "editor";
type DocumentMode = "preview" | "edit" | "mixed";
interface DirEntry { interface DirEntry {
name: string; name: string;
path: string; path: string;
@@ -14,9 +18,15 @@ const props = withDefaults(
folderPath: string; folderPath: string;
entries: DirEntry[]; entries: DirEntry[];
currentFilePath?: string | null; currentFilePath?: string | null;
settingsRequest?: number;
settingsInitialPage?: SettingsPage;
defaultDocumentMode?: DocumentMode;
}>(), }>(),
{ {
currentFilePath: null, currentFilePath: null,
settingsRequest: 0,
settingsInitialPage: "about",
defaultDocumentMode: "edit",
}, },
); );
@@ -26,6 +36,7 @@ const emit = defineEmits<{
openFile: [path: string]; openFile: [path: string];
createFile: []; createFile: [];
createFolder: []; createFolder: [];
updateDefaultDocumentMode: [mode: DocumentMode];
}>(); }>();
// Wrap the workspace root as a top-level tree node // Wrap the workspace root as a top-level tree node
@@ -56,6 +67,17 @@ function switchView(view: ViewMode) {
// ---- settings modal ---- // ---- settings modal ----
const showSettings = ref(false); 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> </script>
<template> <template>
@@ -85,9 +107,9 @@ const showSettings = ref(false);
<!-- ===== Settings ===== --> <!-- ===== Settings ===== -->
<button <button
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto" 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="设置" title="设置"
@click="showSettings = true" @click="openSettings('about')"
> >
<i class="ri-settings-line text-sm"></i> <i class="ri-settings-line text-sm"></i>
</button> </button>
@@ -147,26 +169,10 @@ const showSettings = ref(false);
</div> </div>
</aside> </aside>
<!-- ===== SETTINGS MODAL ===== --> <SettingsModal
<Teleport to="body"> v-model:show="showSettings"
<div :initial-page="settingsPage"
v-if="showSettings" :default-document-mode="defaultDocumentMode"
class="fixed inset-0 z-50 flex items-center justify-center bg-[#38342e]/25 backdrop-blur-sm" @update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
@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>
</template> </template>

View 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>