Settings: reorganise into tabs (General, Account, Notifications, Integrations, Data, Admin)
Replaces the single long scrolling page with a tab bar. Active tab persists to localStorage across navigation. Admin tab only visible to admins. Push notifications description clarified to mention HTTPS requirement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+249
-171
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, watch, onMounted } from "vue";
|
||||||
import { useSettingsStore } from "@/stores/settings";
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useToastStore } from "@/stores/toast";
|
import { useToastStore } from "@/stores/toast";
|
||||||
@@ -28,6 +28,10 @@ const exporting = ref(false);
|
|||||||
const restoring = ref(false);
|
const restoring = ref(false);
|
||||||
const restoreFileInput = ref<HTMLInputElement | null>(null);
|
const restoreFileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
// Tab state — persisted across navigation
|
||||||
|
const activeTab = ref(localStorage.getItem("settings_tab") ?? "general");
|
||||||
|
watch(activeTab, (v) => localStorage.setItem("settings_tab", v));
|
||||||
|
|
||||||
// Chat retention
|
// Chat retention
|
||||||
const chatRetentionDays = ref(90);
|
const chatRetentionDays = ref(90);
|
||||||
const savingRetention = ref(false);
|
const savingRetention = ref(false);
|
||||||
@@ -439,9 +443,24 @@ function hostname(url: string): string {
|
|||||||
<main class="settings-page">
|
<main class="settings-page">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
|
|
||||||
<div class="settings-grid">
|
<!-- Tab bar -->
|
||||||
|
<div class="settings-tabs" role="tablist">
|
||||||
|
<button
|
||||||
|
v-for="tab in (authStore.isAdmin
|
||||||
|
? ['general', 'account', 'notifications', 'integrations', 'data', 'admin']
|
||||||
|
: ['general', 'account', 'notifications', 'integrations', 'data'])"
|
||||||
|
:key="tab"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="activeTab === tab"
|
||||||
|
:class="['tab-btn', { active: activeTab === tab }]"
|
||||||
|
@click="activeTab = tab"
|
||||||
|
>
|
||||||
|
{{ tab.charAt(0).toUpperCase() + tab.slice(1) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Assistant ── full width ── -->
|
<!-- ── General ── -->
|
||||||
|
<div v-show="activeTab === 'general'" class="settings-grid">
|
||||||
<section class="settings-section full-width">
|
<section class="settings-section full-width">
|
||||||
<h2>Assistant</h2>
|
<h2>Assistant</h2>
|
||||||
<div class="assistant-grid">
|
<div class="assistant-grid">
|
||||||
@@ -472,8 +491,11 @@ function hostname(url: string): string {
|
|||||||
<span v-if="saved" class="saved-msg">Saved!</span>
|
<span v-if="saved" class="saved-msg">Saved!</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Account ── -->
|
||||||
|
<div v-show="activeTab === 'account'" class="settings-grid">
|
||||||
|
|
||||||
<!-- ── Email Address ── half width ── -->
|
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2>Email Address</h2>
|
<h2>Email Address</h2>
|
||||||
<p class="section-desc">Used for password resets and notifications.</p>
|
<p class="section-desc">Used for password resets and notifications.</p>
|
||||||
@@ -509,21 +531,6 @@ function hostname(url: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Invalidate Sessions ── half width ── -->
|
|
||||||
<section class="settings-section">
|
|
||||||
<h2>Active Sessions</h2>
|
|
||||||
<p class="section-desc">
|
|
||||||
Sign out all other devices and sessions. Use this after an SSO password change
|
|
||||||
to ensure stale sessions are revoked.
|
|
||||||
</p>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="btn-danger-outline" @click="invalidateSessions" :disabled="invalidatingSessions">
|
|
||||||
{{ invalidatingSessions ? "Invalidating..." : "Invalidate All Other Sessions" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Change Password ── half width ── -->
|
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2>Change Password</h2>
|
<h2>Change Password</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -572,9 +579,26 @@ function hostname(url: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Notifications ── half width ── -->
|
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2>Notifications</h2>
|
<h2>Active Sessions</h2>
|
||||||
|
<p class="section-desc">
|
||||||
|
Sign out all other devices and sessions. Use this after an SSO password change
|
||||||
|
to ensure stale sessions are revoked.
|
||||||
|
</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn-danger-outline" @click="invalidateSessions" :disabled="invalidatingSessions">
|
||||||
|
{{ invalidatingSessions ? "Invalidating..." : "Invalidate All Other Sessions" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Notifications ── -->
|
||||||
|
<div v-show="activeTab === 'notifications'" class="settings-grid">
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2>Email Notifications</h2>
|
||||||
<p class="section-desc">
|
<p class="section-desc">
|
||||||
Email notifications when SMTP is configured by an admin.
|
Email notifications when SMTP is configured by an admin.
|
||||||
</p>
|
</p>
|
||||||
@@ -600,153 +624,13 @@ function hostname(url: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Chat Retention ── half width ── -->
|
|
||||||
<section class="settings-section">
|
|
||||||
<h2>Chat History</h2>
|
|
||||||
<p class="section-desc">
|
|
||||||
Conversations older than this many days are automatically deleted when you load the chat.
|
|
||||||
Set to <strong>0</strong> to keep conversations forever.
|
|
||||||
</p>
|
|
||||||
<div class="field retention-field">
|
|
||||||
<label class="field-label">Retention period (days)</label>
|
|
||||||
<div class="retention-row">
|
|
||||||
<input
|
|
||||||
v-model.number="chatRetentionDays"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="3650"
|
|
||||||
class="input retention-input"
|
|
||||||
/>
|
|
||||||
<button class="btn-primary" :disabled="savingRetention" @click="saveRetention">
|
|
||||||
{{ savingRetention ? 'Saving...' : 'Save' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Data ── half width ── -->
|
|
||||||
<section class="settings-section">
|
|
||||||
<h2>Data</h2>
|
|
||||||
<p class="section-desc">Export your data or restore from a backup.</p>
|
|
||||||
<div class="data-actions">
|
|
||||||
<button class="btn-secondary" @click="exportNotes('markdown')" :disabled="exportingNotes">
|
|
||||||
{{ exportingNotes ? "Exporting..." : "Export as Markdown" }}
|
|
||||||
</button>
|
|
||||||
<button class="btn-secondary" @click="exportNotes('json')" :disabled="exportingNotes">
|
|
||||||
{{ exportingNotes ? "Exporting..." : "Export as JSON" }}
|
|
||||||
</button>
|
|
||||||
<button class="btn-secondary" @click="exportData('user')" :disabled="exporting">
|
|
||||||
{{ exporting ? "Exporting..." : "Export My Data" }}
|
|
||||||
</button>
|
|
||||||
<template v-if="authStore.isAdmin">
|
|
||||||
<button class="btn-secondary" @click="exportData('full')" :disabled="exporting">
|
|
||||||
{{ exporting ? "Exporting..." : "Full Backup" }}
|
|
||||||
</button>
|
|
||||||
<button class="btn-secondary btn-warn" @click="triggerRestoreUpload" :disabled="restoring">
|
|
||||||
{{ restoring ? "Restoring..." : "Restore from Backup" }}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
ref="restoreFileInput"
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
class="hidden-file-input"
|
|
||||||
@change="handleRestoreFile"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── CalDAV ── full width ── -->
|
|
||||||
<section class="settings-section full-width">
|
|
||||||
<h2>Calendar (CalDAV)</h2>
|
|
||||||
<p class="section-desc">
|
|
||||||
Connect to a CalDAV server (Nextcloud, iCloud, Radicale, Baikal) to create and view calendar events from chat.
|
|
||||||
</p>
|
|
||||||
<div class="caldav-grid">
|
|
||||||
<div class="field">
|
|
||||||
<label for="caldav-url">CalDAV URL</label>
|
|
||||||
<input id="caldav-url" v-model="caldav.caldav_url" type="url" placeholder="https://cloud.example.com/remote.php/dav" class="input" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="caldav-username">Username</label>
|
|
||||||
<input id="caldav-username" v-model="caldav.caldav_username" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="caldav-password">Password</label>
|
|
||||||
<input id="caldav-password" v-model="caldav.caldav_password" type="password" class="input" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="caldav-calendar-name">Calendar Name</label>
|
|
||||||
<input id="caldav-calendar-name" v-model="caldav.caldav_calendar_name" type="text" placeholder="Leave empty to use first available" class="input" />
|
|
||||||
<p class="field-hint">Optional. Exact calendar name to use.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions" style="margin-bottom: 1rem;">
|
|
||||||
<button class="btn-save" @click="saveCaldav" :disabled="savingCaldav">
|
|
||||||
{{ savingCaldav ? "Saving..." : "Save" }}
|
|
||||||
</button>
|
|
||||||
<span v-if="caldavSaved" class="saved-msg">Saved!</span>
|
|
||||||
<button class="btn-secondary" @click="testCaldav" :disabled="testingCaldav">
|
|
||||||
{{ testingCaldav ? "Testing..." : "Test Connection" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="caldavTestResult" class="test-result" :class="{ success: caldavTestResult.success, error: !caldavTestResult.success }">
|
|
||||||
<template v-if="caldavTestResult.success">
|
|
||||||
{{ caldavTestResult.message }}
|
|
||||||
<div v-if="caldavTestResult.calendars?.length" class="test-calendars">
|
|
||||||
Calendars: {{ caldavTestResult.calendars.join(", ") }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>{{ caldavTestResult.error }}</template>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Web Search ── full width ── -->
|
|
||||||
<section class="settings-section full-width">
|
|
||||||
<h2>Web Search (SearXNG)</h2>
|
|
||||||
<template v-if="searxngConfigured">
|
|
||||||
<p class="section-desc">
|
|
||||||
Connected to <code class="url-chip">{{ searxngUrl }}</code>.
|
|
||||||
Test a query below to verify results and rate limiting.
|
|
||||||
</p>
|
|
||||||
<div class="search-row">
|
|
||||||
<input
|
|
||||||
v-model="searchQuery"
|
|
||||||
type="text"
|
|
||||||
class="input"
|
|
||||||
placeholder="Enter a search query..."
|
|
||||||
@keydown="onSearchKeydown"
|
|
||||||
/>
|
|
||||||
<button class="btn-save" @click="testSearch" :disabled="searchLoading || !searchQuery.trim()">
|
|
||||||
{{ searchLoading ? "Searching..." : "Search" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="searchError" class="search-error">{{ searchError }}</p>
|
|
||||||
<ul v-if="searchResults.length" class="search-results">
|
|
||||||
<li v-for="(r, i) in searchResults" :key="i" class="search-result">
|
|
||||||
<div class="result-header">
|
|
||||||
<a :href="r.url" target="_blank" rel="noopener noreferrer" class="result-title">{{ r.title || r.url }}</a>
|
|
||||||
<span class="result-host">{{ hostname(r.url) }}</span>
|
|
||||||
</div>
|
|
||||||
<p v-if="r.snippet" class="result-snippet">{{ r.snippet }}</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<p class="section-desc not-configured">
|
|
||||||
Not configured. Set <code>SEARXNG_URL</code> in docker-compose to enable web research from chat.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── Push Notifications ── half width ── -->
|
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2>Push Notifications</h2>
|
<h2>Push Notifications</h2>
|
||||||
<p class="section-desc">
|
<p class="section-desc">
|
||||||
Receive browser push notifications when a response is ready.
|
Receive browser push notifications when a response is ready. Requires HTTPS.
|
||||||
</p>
|
</p>
|
||||||
<template v-if="!pushStore.isSupported">
|
<template v-if="!pushStore.isSupported">
|
||||||
<p class="push-unsupported">Push notifications are not supported in this browser.</p>
|
<p class="push-unsupported">Push notifications are not supported in this browser or connection (requires HTTPS).</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="push-status-row">
|
<div class="push-status-row">
|
||||||
@@ -792,8 +676,164 @@ function hostname(url: string): string {
|
|||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Application URL (admin) ── full width ── -->
|
<section class="settings-section">
|
||||||
<section v-if="authStore.isAdmin" class="settings-section full-width">
|
<h2>Chat History</h2>
|
||||||
|
<p class="section-desc">
|
||||||
|
Conversations older than this many days are automatically deleted when you load the chat.
|
||||||
|
Set to <strong>0</strong> to keep conversations forever.
|
||||||
|
</p>
|
||||||
|
<div class="field retention-field">
|
||||||
|
<label class="field-label">Retention period (days)</label>
|
||||||
|
<div class="retention-row">
|
||||||
|
<input
|
||||||
|
v-model.number="chatRetentionDays"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="3650"
|
||||||
|
class="input retention-input"
|
||||||
|
/>
|
||||||
|
<button class="btn-primary" :disabled="savingRetention" @click="saveRetention">
|
||||||
|
{{ savingRetention ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Integrations ── -->
|
||||||
|
<div v-show="activeTab === 'integrations'" class="settings-grid">
|
||||||
|
|
||||||
|
<section class="settings-section full-width">
|
||||||
|
<h2>Calendar (CalDAV)</h2>
|
||||||
|
<p class="section-desc">
|
||||||
|
Connect to a CalDAV server (Nextcloud, iCloud, Radicale, Baikal) to create and view calendar events from chat.
|
||||||
|
</p>
|
||||||
|
<div class="caldav-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="caldav-url">CalDAV URL</label>
|
||||||
|
<input id="caldav-url" v-model="caldav.caldav_url" type="url" placeholder="https://cloud.example.com/remote.php/dav" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="caldav-username">Username</label>
|
||||||
|
<input id="caldav-username" v-model="caldav.caldav_username" type="text" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="caldav-password">Password</label>
|
||||||
|
<input id="caldav-password" v-model="caldav.caldav_password" type="password" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="caldav-calendar-name">Calendar Name</label>
|
||||||
|
<input id="caldav-calendar-name" v-model="caldav.caldav_calendar_name" type="text" placeholder="Leave empty to use first available" class="input" />
|
||||||
|
<p class="field-hint">Optional. Exact calendar name to use.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions" style="margin-bottom: 1rem;">
|
||||||
|
<button class="btn-save" @click="saveCaldav" :disabled="savingCaldav">
|
||||||
|
{{ savingCaldav ? "Saving..." : "Save" }}
|
||||||
|
</button>
|
||||||
|
<span v-if="caldavSaved" class="saved-msg">Saved!</span>
|
||||||
|
<button class="btn-secondary" @click="testCaldav" :disabled="testingCaldav">
|
||||||
|
{{ testingCaldav ? "Testing..." : "Test Connection" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="caldavTestResult" class="test-result" :class="{ success: caldavTestResult.success, error: !caldavTestResult.success }">
|
||||||
|
<template v-if="caldavTestResult.success">
|
||||||
|
{{ caldavTestResult.message }}
|
||||||
|
<div v-if="caldavTestResult.calendars?.length" class="test-calendars">
|
||||||
|
Calendars: {{ caldavTestResult.calendars.join(", ") }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ caldavTestResult.error }}</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section full-width">
|
||||||
|
<h2>Web Search (SearXNG)</h2>
|
||||||
|
<template v-if="searxngConfigured">
|
||||||
|
<p class="section-desc">
|
||||||
|
Connected to <code class="url-chip">{{ searxngUrl }}</code>.
|
||||||
|
Test a query below to verify results and rate limiting.
|
||||||
|
</p>
|
||||||
|
<div class="search-row">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
placeholder="Enter a search query..."
|
||||||
|
@keydown="onSearchKeydown"
|
||||||
|
/>
|
||||||
|
<button class="btn-save" @click="testSearch" :disabled="searchLoading || !searchQuery.trim()">
|
||||||
|
{{ searchLoading ? "Searching..." : "Search" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="searchError" class="search-error">{{ searchError }}</p>
|
||||||
|
<ul v-if="searchResults.length" class="search-results">
|
||||||
|
<li v-for="(r, i) in searchResults" :key="i" class="search-result">
|
||||||
|
<div class="result-header">
|
||||||
|
<a :href="r.url" target="_blank" rel="noopener noreferrer" class="result-title">{{ r.title || r.url }}</a>
|
||||||
|
<span class="result-host">{{ hostname(r.url) }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="r.snippet" class="result-snippet">{{ r.snippet }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="section-desc not-configured">
|
||||||
|
Not configured. Set <code>SEARXNG_URL</code> in docker-compose to enable web research from chat.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Data ── -->
|
||||||
|
<div v-show="activeTab === 'data'" class="settings-grid">
|
||||||
|
|
||||||
|
<section class="settings-section full-width">
|
||||||
|
<h2>Export</h2>
|
||||||
|
<p class="section-desc">Download your notes and tasks in portable formats.</p>
|
||||||
|
<div class="data-actions">
|
||||||
|
<button class="btn-secondary" @click="exportNotes('markdown')" :disabled="exportingNotes">
|
||||||
|
{{ exportingNotes ? "Exporting..." : "Export as Markdown" }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" @click="exportNotes('json')" :disabled="exportingNotes">
|
||||||
|
{{ exportingNotes ? "Exporting..." : "Export as JSON" }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary" @click="exportData('user')" :disabled="exporting">
|
||||||
|
{{ exporting ? "Exporting..." : "Export My Data" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template v-if="authStore.isAdmin">
|
||||||
|
<section class="settings-section full-width">
|
||||||
|
<h2>Backup & Restore</h2>
|
||||||
|
<p class="section-desc">Full application backup includes all users and their data.</p>
|
||||||
|
<div class="data-actions">
|
||||||
|
<button class="btn-secondary" @click="exportData('full')" :disabled="exporting">
|
||||||
|
{{ exporting ? "Exporting..." : "Full Backup" }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-secondary btn-warn" @click="triggerRestoreUpload" :disabled="restoring">
|
||||||
|
{{ restoring ? "Restoring..." : "Restore from Backup" }}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref="restoreFileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
class="hidden-file-input"
|
||||||
|
@change="handleRestoreFile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Admin ── -->
|
||||||
|
<div v-if="authStore.isAdmin" v-show="activeTab === 'admin'" class="settings-grid">
|
||||||
|
|
||||||
|
<section class="settings-section full-width">
|
||||||
<h2>Application URL</h2>
|
<h2>Application URL</h2>
|
||||||
<p class="section-desc">
|
<p class="section-desc">
|
||||||
Public URL used in email links (invitations, password resets). Example: https://notes.example.com
|
Public URL used in email links (invitations, password resets). Example: https://notes.example.com
|
||||||
@@ -816,8 +856,7 @@ function hostname(url: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── SMTP (admin) ── full width ── -->
|
<section class="settings-section full-width">
|
||||||
<section v-if="authStore.isAdmin" class="settings-section full-width">
|
|
||||||
<h2>Email / SMTP</h2>
|
<h2>Email / SMTP</h2>
|
||||||
<p class="section-desc">Configure SMTP to enable email notifications for all users.</p>
|
<p class="section-desc">Configure SMTP to enable email notifications for all users.</p>
|
||||||
<div class="smtp-grid">
|
<div class="smtp-grid">
|
||||||
@@ -876,6 +915,7 @@ function hostname(url: string): string {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -886,7 +926,39 @@ function hostname(url: string): string {
|
|||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
}
|
}
|
||||||
.settings-page h1 {
|
.settings-page h1 {
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab bar */
|
||||||
|
.settings-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||||
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-bottom-color: var(--color-primary);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two-column grid — small sections pair up, full-width sections span both */
|
/* Two-column grid — small sections pair up, full-width sections span both */
|
||||||
@@ -916,10 +988,10 @@ function hostname(url: string): string {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Assistant — 3-col internal grid */
|
/* Assistant — 2-col internal grid */
|
||||||
.assistant-grid {
|
.assistant-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 0 1rem;
|
gap: 0 1rem;
|
||||||
}
|
}
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
@@ -1035,7 +1107,6 @@ function hostname(url: string): string {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.retention-input {
|
.retention-input {
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
}
|
}
|
||||||
@@ -1251,5 +1322,12 @@ function hostname(url: string): string {
|
|||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
}
|
}
|
||||||
|
.settings-tabs {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user