Blocks
Account settings
The settings page every product needs: profile, preferences, notifications, security and the account itself. Each section saves on its own, and only offers to once something has changed.
Examples
A scholar’s account
Change anything and a bar slides up to save or discard it. The username ibn-sina is taken; the first notifications save fails, to show what happens. In Security, the current password is aleppo-1000 and the two-step code is 123456.
Profile
How you appear to others.
Only used to sign in.
87 characters left
Preferences
How things look and read for you.
Notifications
What to be told about, and where.
| Topic | Push | |
|---|---|---|
| Loans Due dates and returns | ||
| Mentions When someone tags you in a note | ||
| Lectures New talks in the reading room | ||
| Library news |
Security
Your password, two-step sign-in and devices.
Password
Two-step sign-in
Where you’re signed in
- MacBook · Safari Aleppo · Active now This device
- iPhone · Bayt app Aleppo · 2 hours ago
- Windows · Firefox Damascus · 2 days ago
Account
Take your data with you, or close your account.
<script lang="ts">
import Settings, {
type NotificationData,
type PreferencesData,
type ProfileData,
type Session
} from '#lib/blocks/settings/Settings.svelte';
// Demo stand-ins for your API: a little latency, and the errors it would send.
const wait = (ms = 600) => new Promise<void>((r) => setTimeout(r, ms));
const fail = (code: string) => Object.assign(new Error(code), { code });
let profile = $state<ProfileData>({
name: 'Maryam al-Asturlabi',
username: 'maryam',
email: 'maryam@bayt-al-hikma.org',
phone: '+963211234567',
bio: 'Astrolabe maker in Aleppo. Instruments for the observatory and the court.'
});
let preferences = $state<PreferencesData>({
theme: 'system',
language: 'en',
timeZone: 'Asia/Damascus'
});
let notifications = $state<NotificationData>({
channels: {
loans: { email: true, push: true },
mentions: { email: false, push: true },
lectures: { email: true, push: false }
},
digest: 'weekly',
pausedUntil: null
});
const categories = [
{ id: 'loans', label: 'Loans', description: 'Due dates and returns' },
{ id: 'mentions', label: 'Mentions', description: 'When someone tags you in a note' },
{ id: 'lectures', label: 'Lectures', description: 'New talks in the reading room' },
{ id: 'news', label: 'Library news' }
];
const hour = 3600_000;
let sessions = $state<Session[]>([
{
id: 'a',
device: 'MacBook · Safari',
location: 'Aleppo',
lastActive: new Date(),
current: true
},
{
id: 'b',
device: 'iPhone · Bayt app',
location: 'Aleppo',
lastActive: new Date(Date.now() - 2 * hour),
kind: 'phone'
},
{
id: 'c',
device: 'Windows · Firefox',
location: 'Damascus',
lastActive: new Date(Date.now() - 50 * hour)
}
]);
let notificationTries = 0;
</script>
<div class="frame">
<Settings
bind:profile
bind:preferences
bind:notifications
bind:sessions
{categories}
checkUsername={async (u) => {
await wait(400);
return !['ibn-sina', 'admin', 'khwarizmi'].includes(u);
}}
onsaveprofile={async (p) => {
await wait();
if (p.username === 'ibn-sina') throw fail('username_taken');
}}
onsavepreferences={() => wait()}
onsavenotifications={async () => {
await wait();
// The first save fails, to show the message and a second try.
if (notificationTries++ === 0) throw fail('network');
}}
onchangepassword={async (current) => {
await wait();
if (current !== 'aleppo-1000') throw fail('wrong_password');
}}
onstarttwofactor={async () => {
await wait(500);
return { secret: 'JBSW Y3DP EHPK 3PXP' };
}}
onenabletwofactor={async (code) => {
await wait();
if (code !== '123456') throw fail('invalid_code');
return ['7KQ2-M9XD', 'P4TR-8HWN', 'C6VB-2LZE', 'Y3NF-5QJA', 'H8GS-4UKM', 'R2DW-9TXC'];
}}
ondisabletwofactor={() => wait()}
onsignout={() => wait()}
onsignoutothers={() => wait()}
onexport={() => wait(900)}
ondelete={() => wait(800)}
/>
</div>
<style>
/* A framed page for the example; in your app, give it the page. */
.frame {
inline-size: 100%;
padding: 1rem;
border-radius: calc(var(--ui-radius) * 1.5);
background: var(--ui-bg);
}
</style>Installation
npx sinaui add blocks/settingsCopy src/lib/blocks/settings/, with ActionButton, AlertDialog, Avatar, Badge, Button, CopyButton, Dialog, HoldToConfirm, Input, OtpInput, PasswordInput, PhoneInput, Segmented, Select, Spinner, Switch, Textarea, tabs/, toast/ and tokens.css.
Phone input needs one package:
pnpm add libphonenumber-jsProps
| Prop | Type | Default |
|---|---|---|
profile | { name, username, email, phone, bio, photo? } (bindable) | required |
preferences | { theme: 'system' | 'light' | 'dark', language, timeZone } (bindable) | required |
notifications | { channels: { [id]: { email, push } }, digest, pausedUntil? } (bindable) | required |
categories | { id, label, description? }[] | required |
sessions | { id, device, location, lastActive, kind?, current? }[] (bindable) | [] |
twoFactor, recoveryCodes | boolean, string[] (bindable) | false, [] |
section | 'profile' | 'preferences' | 'notifications' | 'security' | 'account' (bindable) | 'profile' |
languages | { value, label }[] | six |
checkUsername | (username) => Promise<boolean>: true when free | |
onphoto | (file) => Promise<string>: its address | |
onsaveprofile, onsavepreferences, onsavenotifications | (data) => Promise<void> | |
onchangepassword | (current, next) => Promise<void> | |
onstarttwofactor | () => Promise<{ secret }>: the key for the app | |
onenabletwofactor | (code) => Promise<string[]>: recovery codes | |
ondisabletwofactor, onsignout, onsignoutothers, onexport, ondelete | () => Promise<void> (onsignout gets the id) |
Throw an Error with a code and people read a plain sentence: network, username_taken, wrong_password, invalid_code, too_large, not_image. Anything else reads
“Something went wrong. Try again.” Saves are confirmed with a toast (mount <Toaster /> once in your layout).
Keyboard
| Key | Does |
|---|---|
| ← → / ↑ ↓ | Move between sections |
| Tab | Into the section, then the save bar when it shows |
| Hold Space or Enter | Delete the account |
Accessibility
- Sections are tabs: down the side when there’s room, scrolling across the top on phones.
- The save bar is a labeled region, skipped by Tab until something has changed.
- Field errors are read with their field; save errors are read as they appear.
- Turning two-step sign-in on or off always asks first: a setup dialog or a confirmation.
- Each sign-out button names the device and place it signs out.
- With reduced motion, the save bar and lists change without sliding.