Kalıplar
Kaydetme
Açık kaydetme ile otomatik kaydetme arasında seçim, sonucun gösterimi ve girilen verinin korunması.
Kullanıcı verisini kaydetmek, ürüne duyulan güvenin temelidir. Bu kalıp iki yolu tanımlar: kullanıcının düğmeyle kaydettiği form ve kendiliğinden kaydedilen belge.
Ne zaman kullanılır
Bölüm başlığı “Ne zaman kullanılır”- Açık kaydetme: doğrulaması veya yan etkisi olan formlarda (ayarlar, profil, kayıt oluşturma).
- Otomatik kaydetme: belge ve not gibi sürekli yazılan alanlarda.
- İkisi aynı ekranda karışmaz; bir ekran tek yöntem kullanır.
Canlı örnek
Bölüm başlığı “Canlı örnek”Açık kaydetmede düğme yüklenir, sonra başarı işaretini gösterir ve “Değişiklikler kaydedildi” toast’ı çıkar. Otomatik kaydetmede yazmayı bıraktıktan 1000 ms sonra kayıt başlar. Hata hikâyelerinde ilk deneme başarısız olur.
import { Button, InlineAlert, Inline, Stack, Text, TextInput, useToast } from '@bigbang/ui';import { useEffect, useRef, useState, type FormEvent } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { title: 'Proje ayarları', name: 'Proje adı', description: 'Ekip üyeleri bu adı listede görür.', save: 'Kaydet', saving: 'Kaydediliyor', saved: 'Değişiklikler kaydedildi', failedTitle: 'Kaydedilemedi', failed: 'Değişiklikler kaydedilemedi. Girdiğiniz bilgiler korundu; bağlantınızı denetleyip tekrar deneyin.', retry: 'Tekrar dene', initial: 'Proje 1', }, en: { title: 'Project settings', name: 'Project name', description: 'Team members see this name in the list.', save: 'Save', saving: 'Saving', saved: 'Changes saved', failedTitle: 'Not saved', failed: 'The changes were not saved. What you entered is kept; check your connection and try again.', retry: 'Try again', initial: 'Project 1', },};
type Status = 'idle' | 'saving' | 'saved' | 'failed';
/** How long the success mark stays on the button. */export const SUCCESS_HOLD = 2000;
type Props = { lang?: Lang; failFirst?: boolean; delay?: number };
function Form({ lang = 'tr', failFirst = false, delay = 1200 }: Props) { const t = copy[lang]; const toast = useToast(); const [value, setValue] = useState(t.initial); const [status, setStatus] = useState<Status>('idle'); const [mark, setMark] = useState(false); const [dirty, setDirty] = useState(false); const attemptsRef = useRef(0); const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
useEffect(() => { const pending = timersRef.current; return () => pending.forEach(clearTimeout); }, []);
// Leaving with unsaved input asks the browser to confirm. useEffect(() => { if (!dirty) return; const guard = (event: BeforeUnloadEvent) => event.preventDefault(); window.addEventListener('beforeunload', guard); return () => window.removeEventListener('beforeunload', guard); }, [dirty]);
const save = () => { if (status === 'saving') return; setStatus('saving'); setMark(false); const attempt = attemptsRef.current++; timersRef.current.push( setTimeout(() => { if (failFirst && attempt === 0) { setStatus('failed'); return; } setStatus('saved'); // The toast announces the result once; the button's own success announcement is off (`successLabel=""`). toast.show({ title: t.saved, tone: 'success' }); setDirty(false); setMark(true); timersRef.current.push(setTimeout(() => setMark(false), SUCCESS_HOLD)); }, delay), ); };
const onSubmit = (event: FormEvent) => { event.preventDefault(); save(); };
return ( <form onSubmit={onSubmit} noValidate className="w-full max-w-md" aria-labelledby={`save-title-${lang}`}> <Stack gap="lg"> <Text weight="medium" id={`save-title-${lang}`}> {t.title} </Text> <TextInput label={t.name} description={t.description} name="project" value={value} onValueChange={(next) => { setValue(next); setDirty(true); if (status !== 'saving') setStatus('idle'); }} /> <Inline gap="md" align="center" wrap> <Button type="submit" loading={status === 'saving'} loadingLabel={t.saving} success={mark} successLabel=""> {t.save} </Button> </Inline> {status === 'failed' ? ( <InlineAlert tone="danger" title={t.failedTitle} announce actions={ <Button variant="secondary" size="sm" onClick={save}> {t.retry} </Button> } > {t.failed} </InlineAlert> ) : null} </Stack> </form> );}
/** * Explicit save: the button shows progress, then a success mark held for 2000 ms, and a success toast reports the result. * On failure the input stays, a danger `InlineAlert` says what happened and its "Try again" action repeats the save. * `failFirst` makes the first attempt fail so the failure path can be shown. */export function ExplicitSave(props: Props) { return ( <LangProvider lang={props.lang ?? 'tr'}> <Form {...props} /> </LangProvider> );}import { Button, InlineAlert, Inline, Spinner, Stack, Text, Textarea } from '@bigbang/ui';import { useEffect, useRef, useState } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { label: 'Toplantı notu', initial: 'Gündem: rapor 12 ve proje 3.', saving: 'Kaydediliyor…', saved: (time: string) => `Kaydedildi · ${time}`, failed: 'Kaydedilemedi', failedHint: 'Yazdığınız metin burada duruyor. Bağlantı gelince tekrar deneyin.', retry: 'Tekrar dene', }, en: { label: 'Meeting note', initial: 'Agenda: report 12 and project 3.', saving: 'Saving…', saved: (time: string) => `Saved · ${time}`, failed: 'Not saved', failedHint: 'Your text is still here. Try again when the connection is back.', retry: 'Try again', },};
type Status = 'idle' | 'saving' | 'saved' | 'failed';
/** The save starts this long after the user stops typing. */export const DEBOUNCE = 1000;
const clock = (date: Date) => date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
/** * Autosave: typing restarts a 1000 ms debounce; the status line shows progress and the time of the last save. * A failed save keeps the text and shows a warning `InlineAlert` with a retry action; a successful save does not toast. `failFirst` makes the first save fail. */export function Autosave({ lang = 'tr', failFirst = false, delay = 600 }: { lang?: Lang; failFirst?: boolean; delay?: number }) { const t = copy[lang]; const [text, setText] = useState(t.initial); const [status, setStatus] = useState<Status>('idle'); const [savedAt, setSavedAt] = useState(''); const latestRef = useRef(text); const attemptsRef = useRef(0); const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const workRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); const dirtyRef = useRef(false);
useEffect( () => () => { clearTimeout(debounceRef.current); clearTimeout(workRef.current); }, [], );
useEffect(() => { const guard = (event: BeforeUnloadEvent) => { if (dirtyRef.current) event.preventDefault(); }; window.addEventListener('beforeunload', guard); return () => window.removeEventListener('beforeunload', guard); }, []);
const save = (snapshot: string) => { setStatus('saving'); const attempt = attemptsRef.current++; workRef.current = setTimeout(() => { // Text typed meanwhile starts its own debounce; this result is stale. if (latestRef.current !== snapshot) return; if (failFirst && attempt === 0) { setStatus('failed'); return; } dirtyRef.current = false; setSavedAt(clock(new Date())); setStatus('saved'); }, delay); };
const onChange = (next: string) => { setText(next); latestRef.current = next; dirtyRef.current = true; clearTimeout(debounceRef.current); clearTimeout(workRef.current); setStatus('idle'); debounceRef.current = setTimeout(() => save(next), DEBOUNCE); };
return ( <LangProvider lang={lang}> <Stack gap="sm" className="w-full max-w-md"> <Textarea label={t.label} rows={4} value={text} onValueChange={onChange} /> <div role="status" className="min-h-6"> {status === 'saving' ? ( <Inline gap="xs" align="center"> <Spinner size="xs" decorative /> <Text size="sm" tone="subtle"> {t.saving} </Text> </Inline> ) : status === 'saved' ? ( <Text size="sm" tone="success"> {t.saved(savedAt)} </Text> ) : null} </div> {status === 'failed' ? ( <InlineAlert tone="warning" title={t.failed} announce actions={ <Button variant="secondary" size="sm" onClick={() => save(latestRef.current)}> {t.retry} </Button> } > {t.failedHint} </InlineAlert> ) : null} </Stack> </LangProvider> );}Kurallar
Bölüm başlığı “Kurallar”| Kural | Değer |
|---|---|
| Açık kaydetme | Doğrulaması veya yan etkisi olan formlar için. |
| Otomatik kaydetme | Belge ve notlar için; yazma bittikten 1000 ms sonra başlar. |
| Düğme | Yüklenirken loading, bitince success; başarı işareti 2000 ms kalır. |
| Sonuç | Açık kaydetmede başarı toast’ı: “Değişiklikler kaydedildi”. Otomatik kaydetmede sessiz durum satırı role="status": “Kaydedildi · 10:00”; her kayıtta toast çıkmaz. |
| Hata | Girilen veri asla silinmez. Açık kaydetmede tehlike düzeyinde InlineAlert, otomatik kaydetmede uyarı düzeyinde InlineAlert; ikisinde de “Tekrar dene” eylemi vardır. |
| Ayrılma koruması | Kaydedilmemiş veri varken tarayıcıda beforeunload; uygulama içinde onay iletişim kutusu. |
| Çifte duyuru | Düğmenin kendi başarı duyurusu kapalıdır (successLabel=""); sonucu toast duyurur. |
Davranış
Bölüm başlığı “Davranış”- Açık kaydetmede kullanıcı alanları değiştirir ve “Kaydet” düğmesine basar.
- Düğme yükleniyor durumuna geçer ve bir kez “Kaydediliyor” duyurulur.
- Başarıda düğme başarı işaretini 2000 ms gösterir; toast “Değişiklikler kaydedildi” der. Düğme ayrıca duyuru yapmaz.
- Başarısızlıkta alan değerleri durur. Tehlike düzeyindeki
InlineAlertne olduğunu ve ne yapılacağını söyler; “Tekrar dene” eylemi aynı kaydı yeniden gönderir. - Otomatik kaydetmede her tuş vuruşu 1000 ms’lik bekleme süresini sıfırlar. Süre dolunca “Kaydediliyor…”, ardından “Kaydedildi · 10:00” görünür. Kayıt başarısız olursa uyarı düzeyinde
InlineAlert“Tekrar dene” eylemiyle çıkar. - Kayıt sürerken yazılan metin yeni bir bekleme başlatır; eski kaydın sonucu o metni geçersiz kılmaz.
Doğrulama hataları için Formlar ve doğrulama kalıbına bakın.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- Açık kaydetmede başarı toast’ı nazik duyurulur; otomatik kaydetmede ilerleme ve sonuç
role="status"bölgesinde nazik duyurulur. Yazarken kesinti olmaz. - Başarısız açık kayıt
role="alert"ile duyurulur, çünkü kullanıcının harekete geçmesi gerekir. Başarısız otomatik kayıt uyarı düzeyindedir veannounceile nazik duyurulur. - Başarı işareti tek başına bilgi taşımaz; toast metni her zaman vardır.
- “Tekrar dene” düğmesi klavyeyle erişilebilir ve metin kutusunun hemen altındadır.
- Kaydedilmemiş veriyle sayfadan çıkış denemesi tarayıcı onayına bağlanır.
İçerik
Bölüm başlığı “İçerik”- Sonucu söyleyin: “Değişiklikler kaydedildi”, “Kaydedilemedi”.
- Hatada verinin korunduğunu yazın: “Girdiğiniz bilgiler korundu.”
- Saat 24 saatlik ve kısa yazılır: “Kaydedildi · 10:00”.
- Düğme fiil ile başlar ve noktasızdır: “Kaydet”, “Tekrar dene”.
Hareket
Bölüm başlığı “Hareket”| Öğe | Hareket | Preset |
|---|---|---|
| Kaydet düğmesi | Basınca 0,97 ölçek; 70 ms aşağı, 230 ms geri | motion.press |
| Başarı işareti | 0,6 ile 1 arası 240 ms, 2000 ms görünür kalır | motion.success |
| Toast ve durum satırı | Toast girişte 240 ms; durum satırı 110 ms solma | motion.toast, motion.reveal |
Hareketi azaltma tercihinde ölçek kalkar; başarı işareti durağan olarak görünür.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”Button, InlineAlert, Toast, TextInput, Textarea, Spinner. Sonraki kalıplar: Yükleme durumları ve Hatadan kurtarma.
Uygulama içinde kaydedilmemiş değişiklikle ayrılma onayı için AlertDialog kullanın; ayrıntı: Onay.