Developer Docs

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:

  1. Auth Guard โ€” Redirects unauthenticated users to /auth
  2. Sidebar Navigation โ€” Renders the nav items and tracks the active section via IntersectionObserver
  3. 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