Settings Page Architecture
Overview
The /settings route is the central hub where Visitors, Buyers, and Malet Owners manage their account, privacy, notifications, and security. As features grew (MFA, passkeys, cookie consent, GDPR export, session monitoring, security audit log), the page exceeded 3,000 lines.
To maintain developer velocity and prevent merge conflicts, the settings page was modularized into 9 self-contained section components backed by a shared CSS foundation.
src/routes/settings/+page.svelte (448 lines โ thin orchestrator)
src/lib/components/settings/
โโโ settings-shared.css (shared design patterns)
โโโ ProfileSection.svelte
โโโ ContactInfoSection.svelte
โโโ ConnectedAccountsSection.svelte
โโโ PrivacySecuritySection.svelte
โโโ DataCookiesSection.svelte
โโโ NotificationsSection.svelte
โโโ AdminSection.svelte
โโโ SessionSecuritySection.svelte
โโโ DangerZoneSection.svelte
โโโ SessionActivityPanel.svelte (sub-panel)
โโโ SecurityEventLog.svelte (sub-panel)
โโโ DataExportPanel.svelte (sub-panel)
โโโ AgeVerificationPanel.svelte (sub-panel)
Architecture
Parent Page (Orchestrator)
The parent +page.svelte is responsible for exactly three things:
- Auth Guard โ Redirects unauthenticated users to
/auth - Sidebar Navigation โ Renders the nav items and tracks the active section via
IntersectionObserver - Section Composition โ Renders each section component in order
<!-- Simplified structure -->
<ProfileSection />
<ContactInfoSection />
<ConnectedAccountsSection />
<PrivacySecuritySection />
<DataCookiesSection />
<NotificationsSection />
{#if $currentUser?.is_privileged}
<AdminSection />
{/if}
<SessionSecuritySection />
<DangerZoneSection />
The parent owns no business logic โ all state, handlers, and API calls live inside each section component.
Section Components
Each section component follows this contract:
| Requirement | Implementation |
|---|---|
| Section ID | Renders <section id="section-{name}"> so the parent's IntersectionObserver can track scroll position |
| Self-contained state | All reactive variables ($state, $derived) are declared inside the component |
| Direct store access | Components import authStore, currentUser, notificationStore, etc. directly โ no prop drilling |
| Own styles | CSS is scoped via <style> block; shared patterns are imported via @import './settings-shared.css' |
| Dark mode | All colors use $mall design tokens with :global(body.dark-mode) overrides |
Design Decision: Stores vs Props
Following Svelte 5 best practices:
- Stores (direct import): Used for global singletons like
authStore,currentUser,notificationStoreโ these are accessed by dozens of components across the app - Props (
$props()): Not used here because the component tree is only 1 level deep โ props would add verbosity with no testability benefit - Context (
setContext/getContext): Not needed โ no deep nesting
Shared CSS: `settings-shared.css`
The shared CSS file contains reusable patterns used across all section components. Each component imports it:
<style>
@import './settings-shared.css';
/* Component-specific styles below */
.my-special-icon {
background: rgba(102, 126, 234, 0.12);
color: var(--mall-accent);
}
</style>
What's in `settings-shared.css`
| Pattern | Classes | Purpose |
|---|---|---|
| Section Cards | .settings-section, .section-header, .section-icon, .section-desc |
Card containers with shadow, rounded corners, scroll-margin |
| Setting Rows | .setting-row, .setting-label, .label-text, .label-hint, .setting-control |
Two-column key-value layout |
| Inline Edit | .inline-edit, .inline-edit input |
Compact edit forms within setting rows |
| Buttons | .edit-btn, .save-btn, .cancel-btn, .btn-link, .btn-primary-small, .btn-danger-small |
Action buttons with accent/danger variants |
| Toggle Switch | .toggle-switch, .toggle-slider |
iOS-style toggle with dark mode support |
| Badges | .coming-soon-badge, .saved-badge |
Status indicators |
| Subsections | .subsection-block, .subsection-title, .subsection-desc, .subsection-divider |
Nested section layout for panels within a card |
| Animations | @keyframes fadeInUp, shake, fadeSlideIn |
Micro-interactions |
| Responsive | @media (max-width: 600px) |
Stack layout on mobile |
Section Reference
ProfileSection
State: displayName, editingName, nameInput, nameSaved
Displays the user banner (avatar + email), display name editing with save/cancel, username (read-only, "Coming Soon"), and user ID.
ContactInfoSection
State: Email and phone update flows with OTP verification
Two parallel flows (email and phone), each with: input โ request OTP โ enter OTP โ confirm. Supports optimistic success messages.
ConnectedAccountsSection
State: unlinkLoading, activeProviders
Lists OAuth providers (Google, Apple, X, Facebook) with connect/unlink actions. Uses AUTH_API_URL for OAuth redirect URIs.
PrivacySecuritySection
State: 12 MFA-related variables, visibility toggle
The largest section. Contains:
- Profile visibility toggle (PUBLIC/PRIVATE)
- Full MFA wizard (enroll โ QR โ verify โ backup codes)
- MFA management (disable, regenerate backup codes)
- Passkey registration
DataCookiesSection
State: None (delegates to sub-panels)
Thin wrapper that composes DataExportPanel (GDPR Article 20) and AgeVerificationPanel.
NotificationsSection
State: Notification categories (from notificationStore)
Per-category rows with email/SMS/push toggles and a master toggle.
AdminSection
State: None
Simple link to /admin dashboard. Only rendered when $currentUser?.is_privileged is true.
SessionSecuritySection
State: None (delegates to sub-panels)
Composes SessionActivityPanel (active device monitoring) and SecurityEventLog (audit trail), plus the Sign Out button.
DangerZoneSection
State: showDeactivateConfirm, showDeleteConfirm, deactivateInput, deactivateReason, dangerLoading, dangerError
Account deactivation (amber, reversible, 30-day window) and deletion (red, irreversible, requires typing "DELETE").
Adding a New Section
Step 1: Create the Component
touch src/lib/components/settings/MyNewSection.svelte
Step 2: Follow the Template
<script lang="ts">
// Import stores directly
import { currentUser } from '../../../stores/auth';
// Self-contained state
let myValue = $state('');
</script>
<section class="settings-section" id="section-mynew" data-testid="settings-mynew">
<div class="section-header">
<div class="section-icon mynew-icon">
<!-- SVG icon -->
</div>
<div>
<h2>My New Section</h2>
<p class="section-desc">Description of this section</p>
</div>
</div>
<!-- Content using setting-row pattern -->
<div class="setting-row">
<div class="setting-label">
<span class="label-text">Setting Name</span>
<span class="label-hint">Helpful description</span>
</div>
<div class="setting-control">
<span class="current-value">{myValue || 'Not set'}</span>
</div>
</div>
</section>
<style>
@import './settings-shared.css';
.mynew-icon {
background: rgba(59, 130, 246, 0.12);
color: #3b82f6;
}
</style>
Step 3: Register in Parent
Add to +page.svelte:
import MyNewSection from '$lib/components/settings/MyNewSection.svelte';
Add to the navSections array:
{ id: 'mynew', label: 'My New Section', icon: 'star' }
Add to template:
<MyNewSection />
The IntersectionObserver will automatically detect the new id="section-mynew" and update the sidebar highlight.
File Reference
| File | Purpose | Lines |
|---|---|---|
settings/+page.svelte |
Orchestrator (nav, auth guard, scroll tracking) | 448 |
settings-shared.css |
Shared layout patterns, buttons, toggles | 491 |
ProfileSection.svelte |
Display name, username, user ID | 252 |
ContactInfoSection.svelte |
Email/phone with OTP | 204 |
ConnectedAccountsSection.svelte |
OAuth link/unlink | 130 |
PrivacySecuritySection.svelte |
MFA, passkeys, visibility | 383 |
DataCookiesSection.svelte |
Data export + age verification | 60 |
NotificationsSection.svelte |
Channel toggles | 112 |
AdminSection.svelte |
Admin dashboard link | 37 |
SessionSecuritySection.svelte |
Sessions + security log | 66 |
DangerZoneSection.svelte |
Deactivate/delete | 163 |
Related
- Privacy & Security APIs โ Backend APIs for cookie consent, data export, sessions, and security events
- MFA & Passkeys โ Two-factor authentication and passkey enrollment managed via settings
- Organization & Malet Management โ Organization-level settings (billing, notifications)
- Alerts & Notifications โ Frontend notification system and channel toggles
- Handle System & Sigil Taxonomy โ User handle claim/change UI in ProfileSection
- Malet Navigation Architecture โ Customizable Malet nav bar (NavEditor in owner settings)