Settings
A grouped settings screen composed from touchcn components.
A grouped-list settings screen: a profile header, an appearance section wired to the live appearance, notification toggles, about rows and a destructive sign-out row that confirms through a dialog before firing its event.
Installation
npx touchcn add settings
This also adds the components the block composes:
avatar · button · dialog · list · navbar · page · switch
Usage
<tcn-settings-block name="Ada Lovelace" email="ada@example.com" (signOut)="signOut()" />import { TcnSettingsBlock } from '@/components/blocks/settings';
<TcnSettingsBlock name="Ada Lovelace" email="ada@example.com" onSignOut={signOut} /><script setup lang="ts">
import { TcnSettingsBlock } from '@/components/blocks/settings';
</script>
<template>
<TcnSettingsBlock name="Ada Lovelace" email="ada@example.com" @sign-out="signOut" />
</template>The appearance rows read and write the active theme and color scheme through
TouchcnAppearance (Angular) / useTouchcnAppearance (React), so they control the
whole app. The notification toggles hold local state — wire them to your own settings.
Set showBack to render a back control in the navbar’s leading slot; it emits
back (Angular) / onBack (React) when tapped — wire it to your router.
Source · Angular
export * from './tcn-settings-block';import { booleanAttribute, Component, computed, inject, input, output, signal } from '@angular/core';
import { TouchcnAppearance } from '@touchcn/angular';
import { TcnAvatar } from '@/components/ui/avatar';
import { TcnButton } from '@/components/ui/button';
import { TcnDialog } from '@/components/ui/dialog';
import { TcnList, TcnListItem } from '@/components/ui/list';
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnPage } from '@/components/ui/page';
import { TcnSwitch } from '@/components/ui/switch';
/**
* Settings screen — a grouped-list settings surface. The appearance section is
* wired to the live theme and color scheme via the `TouchcnAppearance` service;
* the remaining toggles hold local presentational state, and sign-out is
* confirmed through a dialog before `signOut` fires.
*/
@Component({
selector: 'tcn-settings-block',
imports: [TcnPage, TcnNavbar, TcnAvatar, TcnList, TcnListItem, TcnSwitch, TcnDialog, TcnButton],
template: `
<tcn-page>
<tcn-navbar title="Settings">
@if (showBack()) {
<button navbar-leading type="button" class="tcn-navbar-back" aria-label="Back" (click)="back.emit()">
<svg class="if-ios" width="11" height="18" viewBox="0 0 12 20" fill="none" aria-hidden="true">
<path d="M10 2 2 10l8 8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<svg class="if-md" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 12H4m0 0 6-6m-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
}
</tcn-navbar>
<div class="mx-auto min-h-full max-w-[430px] px-4 pt-20 pb-12">
<div class="mb-6 flex items-center gap-4 px-1 py-2">
<tcn-avatar size="lg" [src]="avatarSrc()" [initials]="initials()" [alt]="name()" />
<div class="min-w-0">
<div class="truncate text-lg font-semibold">{{ name() }}</div>
<div class="truncate text-sm text-[var(--color-on-surface-variant)]">{{ email() }}</div>
</div>
</div>
<section class="not-first:mt-6">
<h3 class="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]">
Appearance
</h3>
<tcn-list>
<tcn-list-item class="cursor-pointer" (click)="appearance.setTheme('ios')">
iOS
@if (appearance.theme() === 'ios') {
<span item-trailing class="text-[var(--color-primary)]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
}
</tcn-list-item>
<tcn-list-item class="cursor-pointer" (click)="appearance.setTheme('md')">
Material
@if (appearance.theme() === 'md') {
<span item-trailing class="text-[var(--color-primary)]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
}
</tcn-list-item>
<tcn-list-item>
Dark mode
<tcn-switch
item-trailing
[checked]="appearance.scheme() === 'dark'"
(checkedChange)="appearance.setScheme($event ? 'dark' : 'light')"
/>
</tcn-list-item>
</tcn-list>
</section>
<section class="not-first:mt-6">
<h3 class="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]">
Notifications
</h3>
<tcn-list>
<tcn-list-item>
Push notifications
<tcn-switch item-trailing [(checked)]="push" />
</tcn-list-item>
<tcn-list-item>
Email digest
<tcn-switch item-trailing [(checked)]="emailDigest" />
</tcn-list-item>
</tcn-list>
</section>
<section class="not-first:mt-6">
<h3 class="mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]">
About
</h3>
<tcn-list>
<tcn-list-item chevron class="cursor-pointer">Terms of Service</tcn-list-item>
<tcn-list-item chevron class="cursor-pointer">Privacy Policy</tcn-list-item>
<tcn-list-item>
Version
<span item-trailing class="text-sm text-[var(--color-on-surface-variant)]">1.0.0</span>
</tcn-list-item>
</tcn-list>
</section>
<section class="not-first:mt-6">
<tcn-list>
<tcn-list-item class="cursor-pointer text-[var(--color-destructive)]" (click)="confirmOpen.set(true)">
Sign out
</tcn-list-item>
</tcn-list>
</section>
</div>
<tcn-dialog [(open)]="confirmOpen" heading="Sign out?" message="You will need to sign in again to access your account.">
<button dialog-action tcnButton variant="ghost" type="button" (click)="confirmOpen.set(false)">Cancel</button>
<button dialog-action tcnButton variant="destructive" type="button" (click)="confirmSignOut()">Sign out</button>
</tcn-dialog>
</tcn-page>
`,
})
export class TcnSettingsBlock {
protected readonly appearance = inject(TouchcnAppearance);
readonly name = input('Ada Lovelace');
readonly email = input('ada@example.com');
readonly avatarSrc = input('');
/** Renders a back control in the navbar's leading slot. */
readonly showBack = input(false, { transform: booleanAttribute });
/** Fires after the user confirms the sign-out dialog. */
readonly signOut = output<void>();
readonly back = output<void>();
protected readonly push = signal(true);
protected readonly emailDigest = signal(false);
protected readonly confirmOpen = signal(false);
protected readonly initials = computed(() =>
this.name()
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? '')
.join(''),
);
protected confirmSignOut(): void {
this.confirmOpen.set(false);
this.signOut.emit();
}
}Source · React
export * from './tcn-settings-block';import { useState } from 'react';
import { useTouchcnAppearance } from '@touchcn/react';
import { TcnAvatar } from '@/components/ui/avatar';
import { TcnButton } from '@/components/ui/button';
import { TcnDialog } from '@/components/ui/dialog';
import { TcnList, TcnListItem } from '@/components/ui/list';
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnPage } from '@/components/ui/page';
import { TcnSwitch } from '@/components/ui/switch';
export interface TcnSettingsBlockProps {
name?: string;
email?: string;
avatarSrc?: string;
/** Renders a back control in the navbar's leading slot. */
showBack?: boolean;
/** Fires after the user confirms the sign-out dialog. */
onSignOut?(): void;
/** Fires when the navbar back control is activated. */
onBack?(): void;
}
const Check = () => (
<span className="text-[var(--color-primary)]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 13l4 4L19 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
);
function initialsOf(name: string): string {
return name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? '')
.join('');
}
const sectionHeading = 'mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]';
/**
* Settings screen — a grouped-list settings surface. The appearance section is
* wired to the live theme and scheme via `useTouchcnAppearance`; the remaining
* toggles hold local presentational state, and sign-out is confirmed through a
* dialog before `onSignOut` fires.
*/
export function TcnSettingsBlock({
name = 'Ada Lovelace',
email = 'ada@example.com',
avatarSrc,
showBack = false,
onSignOut,
onBack,
}: TcnSettingsBlockProps) {
const { theme, scheme, setTheme, setScheme } = useTouchcnAppearance();
const [push, setPush] = useState(true);
const [emailDigest, setEmailDigest] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const confirmSignOut = () => {
setConfirmOpen(false);
onSignOut?.();
};
return (
<TcnPage>
<TcnNavbar
title="Settings"
leading={
showBack ? (
<button type="button" className="tcn-navbar-back" aria-label="Back" onClick={() => onBack?.()}>
<svg className="if-ios" width="11" height="18" viewBox="0 0 12 20" fill="none" aria-hidden="true">
<path d="M10 2 2 10l8 8" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<svg className="if-md" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 12H4m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
) : undefined
}
/>
<div className="mx-auto min-h-full max-w-[430px] px-4 pt-20 pb-12">
<div className="mb-6 flex items-center gap-4 px-1 py-2">
<TcnAvatar src={avatarSrc} initials={initialsOf(name)} size="lg" alt={name} />
<div className="min-w-0">
<div className="truncate text-lg font-semibold">{name}</div>
<div className="truncate text-sm text-[var(--color-on-surface-variant)]">{email}</div>
</div>
</div>
<section className="not-first:mt-6">
<h3 className={sectionHeading}>Appearance</h3>
<TcnList>
<TcnListItem className="cursor-pointer" onClick={() => setTheme('ios')} trailing={theme === 'ios' ? <Check /> : undefined}>
iOS
</TcnListItem>
<TcnListItem className="cursor-pointer" onClick={() => setTheme('md')} trailing={theme === 'md' ? <Check /> : undefined}>
Material
</TcnListItem>
<TcnListItem trailing={<TcnSwitch checked={scheme === 'dark'} onCheckedChange={(next) => setScheme(next ? 'dark' : 'light')} />}>
Dark mode
</TcnListItem>
</TcnList>
</section>
<section className="not-first:mt-6">
<h3 className={sectionHeading}>Notifications</h3>
<TcnList>
<TcnListItem trailing={<TcnSwitch checked={push} onCheckedChange={setPush} />}>Push notifications</TcnListItem>
<TcnListItem trailing={<TcnSwitch checked={emailDigest} onCheckedChange={setEmailDigest} />}>Email digest</TcnListItem>
</TcnList>
</section>
<section className="not-first:mt-6">
<h3 className={sectionHeading}>About</h3>
<TcnList>
<TcnListItem chevron className="cursor-pointer">Terms of Service</TcnListItem>
<TcnListItem chevron className="cursor-pointer">Privacy Policy</TcnListItem>
<TcnListItem trailing={<span className="text-sm text-[var(--color-on-surface-variant)]">1.0.0</span>}>Version</TcnListItem>
</TcnList>
</section>
<section className="not-first:mt-6">
<TcnList>
<TcnListItem
className="cursor-pointer text-[var(--color-destructive)]"
onClick={() => setConfirmOpen(true)}
>
Sign out
</TcnListItem>
</TcnList>
</section>
</div>
<TcnDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
heading="Sign out?"
message="You will need to sign in again to access your account."
actions={
<>
<TcnButton variant="ghost" onClick={() => setConfirmOpen(false)}>
Cancel
</TcnButton>
<TcnButton variant="destructive" onClick={confirmSignOut}>
Sign out
</TcnButton>
</>
}
/>
</TcnPage>
);
}Source · Vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useTouchcnAppearance } from '@touchcn/vue';
import { TcnAvatar } from '@/components/ui/avatar';
import { TcnButton } from '@/components/ui/button';
import { TcnDialog } from '@/components/ui/dialog';
import { TcnList, TcnListItem } from '@/components/ui/list';
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnPage } from '@/components/ui/page';
import { TcnSwitch } from '@/components/ui/switch';
/**
* Settings screen — a grouped-list settings surface. The appearance section is
* wired to the live appearance state via `useTouchcnAppearance`; the remaining
* toggles hold local presentational state, and sign-out is confirmed through a
* dialog before `signOut` fires.
*/
const props = withDefaults(
defineProps<{
name?: string;
email?: string;
avatarSrc?: string;
/** Renders a back control in the navbar's leading slot. */
showBack?: boolean;
}>(),
{ name: 'Ada Lovelace', email: 'ada@example.com', showBack: false },
);
const emit = defineEmits<{
/** Fires after the user confirms the sign-out dialog. */
signOut: [];
/** Fires when the navbar back control is activated. */
back: [];
}>();
const { theme, scheme, setTheme, setScheme } = useTouchcnAppearance();
const push = ref(true);
const emailDigest = ref(false);
const confirmOpen = ref(false);
const initials = computed(() =>
props.name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? '')
.join(''),
);
const sectionHeading = 'mb-1.5 px-1 text-xs font-semibold uppercase tracking-wide text-[var(--color-on-surface-variant)]';
const confirmSignOut = (): void => {
confirmOpen.value = false;
emit('signOut');
};
</script>
<template>
<TcnPage>
<TcnNavbar title="Settings">
<template v-if="showBack" #leading>
<button type="button" class="tcn-navbar-back" aria-label="Back" @click="emit('back')">
<svg class="if-ios" width="11" height="18" viewBox="0 0 12 20" fill="none" aria-hidden="true">
<path d="M10 2 2 10l8 8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<svg class="if-md" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 12H4m0 0 6-6m-6 6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</template>
</TcnNavbar>
<div class="mx-auto min-h-full max-w-[430px] px-4 pt-20 pb-12">
<div class="mb-6 flex items-center gap-4 px-1 py-2">
<TcnAvatar :src="avatarSrc" :initials="initials" size="lg" :alt="name" />
<div class="min-w-0">
<div class="truncate text-lg font-semibold">{{ name }}</div>
<div class="truncate text-sm text-[var(--color-on-surface-variant)]">{{ email }}</div>
</div>
</div>
<section class="not-first:mt-6">
<h3 :class="sectionHeading">Appearance</h3>
<TcnList>
<TcnListItem class="cursor-pointer" @click="setTheme('ios')">
<template v-if="theme === 'ios'" #trailing>
<span class="text-[var(--color-primary)]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</template>
iOS
</TcnListItem>
<TcnListItem class="cursor-pointer" @click="setTheme('md')">
<template v-if="theme === 'md'" #trailing>
<span class="text-[var(--color-primary)]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 13l4 4L19 7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</template>
Material
</TcnListItem>
<TcnListItem>
<template #trailing>
<TcnSwitch
:model-value="scheme === 'dark'"
@update:model-value="(next) => setScheme(next ? 'dark' : 'light')"
/>
</template>
Dark mode
</TcnListItem>
</TcnList>
</section>
<section class="not-first:mt-6">
<h3 :class="sectionHeading">Notifications</h3>
<TcnList>
<TcnListItem>
<template #trailing><TcnSwitch v-model="push" /></template>
Push notifications
</TcnListItem>
<TcnListItem>
<template #trailing><TcnSwitch v-model="emailDigest" /></template>
Email digest
</TcnListItem>
</TcnList>
</section>
<section class="not-first:mt-6">
<h3 :class="sectionHeading">About</h3>
<TcnList>
<TcnListItem chevron class="cursor-pointer">Terms of Service</TcnListItem>
<TcnListItem chevron class="cursor-pointer">Privacy Policy</TcnListItem>
<TcnListItem>
<template #trailing>
<span class="text-sm text-[var(--color-on-surface-variant)]">1.0.0</span>
</template>
Version
</TcnListItem>
</TcnList>
</section>
<section class="not-first:mt-6">
<TcnList>
<TcnListItem class="cursor-pointer text-[var(--color-destructive)]" @click="confirmOpen = true">
Sign out
</TcnListItem>
</TcnList>
</section>
</div>
<TcnDialog
v-model:open="confirmOpen"
heading="Sign out?"
message="You will need to sign in again to access your account."
>
<template #actions>
<TcnButton variant="ghost" @click="confirmOpen = false">Cancel</TcnButton>
<TcnButton variant="destructive" @click="confirmSignOut">Sign out</TcnButton>
</template>
</TcnDialog>
</TcnPage>
</template>export { default as TcnSettingsBlock } from './TcnSettingsBlock.vue';