Şablonlar
Ayarlar
Sekmelere ayrılmış ayarlar; form için açık kaydetme, anahtarlar için anında kaydetme, tek düzenleme için iletişim kutusu, uyarı iletişim kutusunda adla silme onayı ve sonuçlar için kısa bildirimler.
Ayarlar bloğu dört bölümü sekmelerde toplar. Her bölüm kendi kaydetme yöntemini kullanır: alanlar birlikte kaydedilir, anahtarlar tek tek ve hemen kaydedilir.
Önizleme
Bölüm başlığı “Önizleme”Genel sekmesinde bir alanı değiştirin: kaydetme çubuğu çıkar. Bildirimler sekmesinde anahtarlar kendiliğinden kaydedilir. Güvenlik sekmesinde kurtarma e-postasını iletişim kutusunda değiştirin ve silme onayını deneyin. Sayfa başlığındaki marka ve mod seçimi önizlemeyi de değiştirir.
pnpm add @bigbang/ui| Veri | Tür | Not |
|---|---|---|
| Genel ayarlar | { name, language, timezone, about } |
Kaydedilen değerle karşılaştırılır; fark varsa çubuk görünür. |
| Bildirimler | boolean[] |
Her anahtar değişince ayrı istek; bekleme sırasında Switch loading. |
| Kurtarma e-postası | string |
Dialog içinde düzenlenir; gönderince doğrulanır. |
defaultTab |
'general' | 'notifications' | 'security' | 'appearance' |
Açılışta seçili sekme. |
import { AlertDialog, Button, Card, Dialog, Heading, Inline, Radio, RadioGroup, Select, Stack, Switch, Tab, TabList, TabPanel, Tabs, Text, TextInput, Textarea, motion, useToast } from '@bigbang/ui';import { useState, type FormEvent } from 'react';import type { Lang } from '../../patterns/lang';import { AppShellTemplate } from '../../templates/app-shell/AppShellTemplate';
const copy = { tr: { title: 'Ayarlar', description: 'Çalışma alanının genel, bildirim, güvenlik ve görünüm ayarları.', sections: 'Ayar bölümleri', tabs: { general: 'Genel', notifications: 'Bildirimler', security: 'Güvenlik', appearance: 'Görünüm' }, name: 'Çalışma alanı adı', language: 'Dil', timezone: 'Saat dilimi', zones: [ { value: 'istanbul', label: 'İstanbul (GMT+3)' }, { value: 'london', label: 'Londra (GMT+0)' }, ], about: 'Açıklama', save: 'Değişiklikleri kaydet', cancel: 'Vazgeç', unsaved: 'Kaydedilmemiş değişiklikler var.', savedStatus: 'Değişiklikler kaydedildi.', notifications: ['E-posta bildirimleri', 'Haftalık özet', 'Görev atandığında'], notificationSaved: 'Bildirim ayarı kaydedildi.', lastSignIn: 'Son giriş', lastSignInValue: '28 Ekim 2026, 09:00', twoStep: 'İki adımlı doğrulama', twoStepHint: 'Girişte telefonunuza gönderilen kod da istenir.', danger: 'Tehlikeli bölge', deleteText: 'Çalışma alanı, içindeki projeler ve kayıtlarla birlikte kalıcı olarak silinir.', deleteAction: 'Çalışma alanını sil', deleteTitle: 'Çalışma alanı silinsin mi?', deleteDescription: 'Projeler ve kayıtlar da silinir. Bu işlem geri alınamaz.', confirmLabel: 'Onaylamak için çalışma alanı adını yazın', confirmMismatch: 'Ad eşleşmiyor. Çalışma alanı adını aynen yazın.', confirmDelete: 'Kalıcı olarak sil', deleted: 'Çalışma alanı silindi.', recovery: 'Kurtarma e-postası', recoveryEdit: 'Değiştir', recoveryTitle: 'Kurtarma e-postasını değiştir', recoveryHint: 'Parolanızı unutursanız sıfırlama bağlantısı bu adrese gider.', recoveryLabel: 'Yeni e-posta adresi', recoveryInvalid: 'Geçerli bir e-posta adresi girin, örneğin ad@ornek.com.', recoverySaved: 'Kurtarma e-postası güncellendi.', theme: 'Tema', themes: { light: 'Açık', dark: 'Koyu', system: 'Sistem' }, reduceMotion: 'Hareketi azalt', reduceMotionHint: 'Kayma ve ölçek hareketleri kalkar; yalnızca kısa solmalar kalır.', }, en: { title: 'Settings', description: 'General, notification, security and appearance settings of the workspace.', sections: 'Settings sections', tabs: { general: 'General', notifications: 'Notifications', security: 'Security', appearance: 'Appearance' }, name: 'Workspace name', language: 'Language', timezone: 'Time zone', zones: [ { value: 'istanbul', label: 'Istanbul (GMT+3)' }, { value: 'london', label: 'London (GMT+0)' }, ], about: 'Description', save: 'Save changes', cancel: 'Cancel', unsaved: 'You have unsaved changes.', savedStatus: 'Changes saved.', notifications: ['Email notifications', 'Weekly summary', 'When a task is assigned'], notificationSaved: 'Notification setting saved.', lastSignIn: 'Last sign-in', lastSignInValue: '28 October 2026, 09:00', twoStep: 'Two-step verification', twoStepHint: 'Sign-in also asks for a code sent to your phone.', danger: 'Danger zone', deleteText: 'The workspace is deleted for good, with its projects and records.', deleteAction: 'Delete workspace', deleteTitle: 'Delete the workspace?', deleteDescription: 'Its projects and records are deleted too. This cannot be undone.', confirmLabel: 'Type the workspace name to confirm', confirmMismatch: 'The name does not match. Type the workspace name exactly.', confirmDelete: 'Delete for good', deleted: 'Workspace deleted.', recovery: 'Recovery email', recoveryEdit: 'Change', recoveryTitle: 'Change the recovery email', recoveryHint: 'If you forget your password, the reset link goes to this address.', recoveryLabel: 'New email address', recoveryInvalid: 'Enter a valid email address, such as name@example.com.', recoverySaved: 'Recovery email updated.', theme: 'Theme', themes: { light: 'Light', dark: 'Dark', system: 'System' }, reduceMotion: 'Reduce motion', reduceMotionHint: 'Slides and scaling stop; only short fades stay.', },};
type Section = 'general' | 'notifications' | 'security' | 'appearance';type General = { name: string; language: string | null; timezone: string | null; about: string };
const WORKSPACE = 'Çalışma alanı 1';const RECOVERY = 'kullanici.a@ornek.com';const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;const initialGeneral: General = { name: WORKSPACE, language: 'tr', timezone: 'istanbul', about: '' };
export type SettingsBlockProps = { lang?: Lang; defaultTab?: Section;};
/** * Settings in tabs: explicit save for the form, instant save for switches, a Dialog for one edit, * an AlertDialog that confirms deletion by name, and toasts for the results. */export function SettingsBlock({ lang = 'tr', defaultTab = 'general' }: SettingsBlockProps) { const t = copy[lang]; return ( <AppShellTemplate lang={lang} current="settings" title={t.title} description={t.description} width="md"> <SettingsContent lang={lang} defaultTab={defaultTab} /> </AppShellTemplate> );}
// Inside the template, so `useToast` reaches the template's Toaster.function SettingsContent({ lang, defaultTab }: Required<SettingsBlockProps>) { const t = copy[lang]; const toast = useToast(); const [general, setGeneral] = useState(initialGeneral); const [saved, setSaved] = useState(initialGeneral); const [saving, setSaving] = useState<'idle' | 'saving' | 'saved'>('idle'); const [toggles, setToggles] = useState([true, false, true]); const [pendingToggle, setPendingToggle] = useState<number | null>(null); const [twoStep, setTwoStep] = useState(true); const [typed, setTyped] = useState(''); const [mismatch, setMismatch] = useState(false); const [recovery, setRecovery] = useState(RECOVERY); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(''); const [draftError, setDraftError] = useState(false);
const dirty = JSON.stringify(general) !== JSON.stringify(saved); const edit = (next: Partial<General>) => { setGeneral({ ...general, ...next }); setSaving('idle'); };
const save = (event: FormEvent) => { event.preventDefault(); setSaving('saving'); setTimeout(() => { setSaved(general); setSaving('saved'); toast.show({ title: t.savedStatus, tone: 'success' }); }, 800); };
const toggle = (index: number, checked: boolean) => { setPendingToggle(index); setTimeout(() => { setToggles((list) => list.map((value, i) => (i === index ? checked : value))); setPendingToggle(null); toast.show({ title: t.notificationSaved, tone: 'success' }); }, 800); };
const resetConfirm = (open: boolean) => { if (open) return; setTyped(''); setMismatch(false); }; // A rejected promise keeps the AlertDialog open with the error under the field. const confirmDelete = () => { if (typed.trim() !== WORKSPACE) { setMismatch(true); return Promise.reject(new Error('name mismatch')); } return new Promise<void>((resolve) => setTimeout(() => { toast.show({ title: t.deleted }); resolve(); }, 800), ); };
const openRecovery = (open: boolean) => { setEditing(open); if (open) { setDraft(recovery); setDraftError(false); } }; const saveRecovery = (event: FormEvent) => { event.preventDefault(); if (!EMAIL.test(draft.trim())) { setDraftError(true); return; } setRecovery(draft.trim()); setEditing(false); toast.show({ title: t.recoverySaved, tone: 'success' }); };
return ( <Tabs defaultValue={defaultTab} variant="line"> <TabList label={t.sections}> {(Object.keys(t.tabs) as Section[]).map((key) => ( <Tab key={key} value={key}> {t.tabs[key]} </Tab> ))} </TabList>
<TabPanel value="general"> <form onSubmit={save} className="pt-xl" aria-label={t.tabs.general}> <Stack gap="lg"> <TextInput label={t.name} name="name" required value={general.name} onValueChange={(value) => edit({ name: value })} /> <Select label={t.language} items={[{ value: 'tr', label: 'Türkçe' }, { value: 'en', label: 'English' }]} value={general.language} onValueChange={(value) => edit({ language: value })} /> <Select label={t.timezone} items={t.zones} value={general.timezone} onValueChange={(value) => edit({ timezone: value })} /> <Textarea label={t.about} necessityIndicator="optional" value={general.about} onValueChange={(value) => edit({ about: value })} /> {dirty || saving !== 'idle' ? ( <div className={['sticky bottom-0 flex flex-wrap items-center justify-between gap-md rounded-md border border-line-subtle bg-surface-raised p-md shadow-raised', motion.reveal].join(' ')}> <Text size="sm" tone="subtle"> {dirty ? t.unsaved : ''} </Text> <Inline gap="sm"> <Button variant="tertiary" size="sm" onClick={() => { setGeneral(saved); setSaving('idle'); }} > {t.cancel} </Button> <Button type="submit" size="sm" loading={saving === 'saving'} success={saving === 'saved'} successLabel=""> {t.save} </Button> </Inline> </div> ) : null} </Stack> </form> </TabPanel>
<TabPanel value="notifications"> <Stack gap="lg" className="pt-xl"> {t.notifications.map((label, index) => ( <Switch key={label} label={label} checked={toggles[index] ?? false} loading={pendingToggle === index} onCheckedChange={(checked) => toggle(index, checked)} /> ))} </Stack> </TabPanel>
<TabPanel value="security"> <Stack gap="xl" className="pt-xl"> <TextInput label={t.lastSignIn} readOnly value={t.lastSignInValue} /> <Inline justify="between" align="center" gap="md" wrap> <Stack gap="xxs"> <Text weight="medium">{t.recovery}</Text> <Text tone="subtle" numeric> {recovery} </Text> </Stack> <Dialog title={t.recoveryTitle} description={t.recoveryHint} size="sm" open={editing} onOpenChange={openRecovery} trigger={ <Button variant="secondary" size="sm" aria-label={`${t.recoveryEdit}: ${t.recovery}`}> {t.recoveryEdit} </Button> } footer={ <> <Dialog.Close> <Button variant="secondary">{t.cancel}</Button> </Dialog.Close> <Button type="submit" form={`recovery-${lang}`}> {t.save} </Button> </> } > <form id={`recovery-${lang}`} noValidate onSubmit={saveRecovery}> <TextInput label={t.recoveryLabel} type="email" autoComplete="email" required value={draft} onValueChange={(value) => { setDraft(value); setDraftError(false); }} {...(draftError ? { error: t.recoveryInvalid } : {})} /> </form> </Dialog> </Inline> <Switch label={t.twoStep} description={t.twoStepHint} checked={twoStep} onCheckedChange={setTwoStep} /> <Card variant="outlined" padding="lg" className="border-status-danger-border"> <section aria-labelledby={`danger-${lang}`}> <Stack gap="md"> <Heading level={2} size="sm" id={`danger-${lang}`}> {t.danger} </Heading> <Text tone="subtle">{t.deleteText}</Text> <div> <AlertDialog tone="danger" title={t.deleteTitle} description={t.deleteDescription} confirmLabel={t.confirmDelete} cancelLabel={t.cancel} onConfirm={confirmDelete} onOpenChange={resetConfirm} trigger={ <Button variant="tertiary" tone="danger"> {t.deleteAction} </Button> } > <TextInput label={t.confirmLabel} description={WORKSPACE} autoComplete="off" value={typed} onValueChange={(value) => { setTyped(value); setMismatch(false); }} {...(mismatch ? { error: t.confirmMismatch } : {})} /> </AlertDialog> </div> </Stack> </section> </Card> </Stack> </TabPanel>
<TabPanel value="appearance"> <Stack gap="xl" className="pt-xl"> <RadioGroup label={t.theme} defaultValue="system" orientation="horizontal"> {(['light', 'dark', 'system'] as const).map((value) => ( <Radio key={value} value={value} label={t.themes[value]} /> ))} </RadioGroup> <Switch label={t.reduceMotion} description={t.reduceMotionHint} /> </Stack> </TabPanel> </Tabs> );}- AppShell şablonu (
width="md") veh1“Ayarlar”. - Tabs: Genel, Bildirimler, Güvenlik, Görünüm.
- Genel: TextInput, iki Select, Textarea ve yalnız değişiklik varken görünen yapışık kaydetme çubuğu.
- Bildirimler: üç Switch, her biri kendini kaydeder.
- Güvenlik: salt okunur “Son giriş”, Dialog açan “Değiştir” düğmesiyle kurtarma e-postası, iki adımlı doğrulama ve tehlikeli bölge kartı. “Çalışma alanını sil”, çalışma alanı adını isteyen bir AlertDialog açar.
- Görünüm: tema RadioGroup ve “Hareketi azalt” anahtarı.
- Sonuçlar (kaydedildi, güncellendi, silindi) şablondaki
Toasterüzerinden Toast olarak görünür.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- Sekme listesi “Ayar bölümleri” adını taşır; ok tuşlarıyla gezilir.
- Kaydetme, güncelleme ve silme sonuçları kibar “Bildirimler” bölgesindeki kısa bildirimlerdir (F6 ile ulaşılır); düğmenin kendi başarı duyurusu kapalıdır, iki kez okunmaz.
- “Son giriş” salt okunurdur: odak alır, kopyalanır, değiştirilemez (Devre dışı ve salt okunur).
- Silme AlertDialog’u
alertdialogrolündedir, dışarı tıklamayla kapanmaz ve odak önce Vazgeç’e gider. Ad yanlışsa kutu açık kalır (onConfirmsözü reddedilir) ve alanın altında hata yazılır; silme sürerken onay düğmesi meşguldür ve kutu kapanmaz. Sonra odak “Çalışma alanını sil” düğmesine döner. - “Değiştir” düğmesinin adı “Değiştir: Kurtarma e-postası”dır; iletişim kutusu açılınca odak e-posta alanına gider, Escape ya da Vazgeç kaydetmeden kapatır.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”Tabs, TextInput, Select, Textarea, Switch, RadioGroup, Card, Button, Dialog, AlertDialog, Toast. Kalıplar: Kaydetme, Formlar ve doğrulama, Geri alınamayan işlemleri onaylama, Devre dışı ve salt okunur.