Kalıplar
Yükleme durumları
Beklemeyi ne zaman göstereceğiniz, hangi göstergeyi seçeceğiniz ve göstergenin ne kadar kalacağı.
Bekleme kısaysa gösterge yanıp söner, uzunsa kullanıcı sayfanın takıldığını düşünür. Bu kalıp süreye göre hangi göstergenin çıkacağını ve ne kadar kalacağını sabitler.
Ne zaman kullanılır
Bölüm başlığı “Ne zaman kullanılır”- Bir işlem veya veri yüklemesi sonucu hemen gelmediğinde.
- Sayfanın bir bölümü (kart, tablo, liste) ilk kez yüklendiğinde.
- Düğmeyle başlatılan işlem birkaç saniye sürebildiğinde.
Canlı örnek
Bölüm başlığı “Canlı örnek”Üç işi başlatın: 600 ms süren işte gösterge çıkmaz, 2000 ms süren işte düğmede dönen gösterge görünür, 6000 ms süren işte yüzdeyi gösteren ilerleme çubuğu (ProgressBar) eklenir. Kart ilk saniyeden sonra iskelet gösterir.
import { Button, Card, Inline, ProgressBar, Skeleton, Stack, Text } from '@bigbang/ui';import { useEffect, useRef, useState } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { actions: 'İşlemler', run: (ms: number) => `${ms} ms süren işi başlat`, reached: 'Ulaşılan aşama', phases: { idle: 'Gösterge yok', waiting: 'Gösterge yok (1 sn beklenir)', spinner: 'Dönen gösterge', progress: 'İlerleme çubuğu' }, progress: 'Kayıtlar hazırlanıyor', progressText: (percent: number) => `%${percent} tamamlandı`, cardTitle: 'Proje özeti', cardText: 'Bu ay 12 rapor tamamlandı, 3 rapor incelemede bekliyor.', reload: 'Özeti yenile', loadLabel: 'Özet yükleniyor', }, en: { actions: 'Actions', run: (ms: number) => `Start the ${ms} ms job`, reached: 'Phase reached', phases: { idle: 'No indicator', waiting: 'No indicator (waits 1 s)', spinner: 'Spinner', progress: 'Progress bar' }, progress: 'Preparing records', progressText: (percent: number) => `${percent}% complete`, cardTitle: 'Project summary', cardText: '12 reports were completed this month and 3 reports are waiting for review.', reload: 'Reload summary', loadLabel: 'Loading summary', },};
export type LoadingPhase = 'idle' | 'waiting' | 'spinner' | 'progress';
/** Under 1 s nothing shows, 1 to 3 s a spinner or skeleton, over 3 s a determinate `ProgressBar` with a label. */export const SPINNER_AFTER = 1000;export const PROGRESS_AFTER = 3000;/** Once an indicator shows, it stays at least this long so it does not flicker. */export const MIN_DISPLAY = 500;
/** Maps a loading flag to the indicator that should show now. */export function useLoadingPhase(loading: boolean): LoadingPhase { const [phase, setPhase] = useState<LoadingPhase>('idle'); const shownAtRef = useRef<number | null>(null);
useEffect(() => { if (loading) { shownAtRef.current = null; const toSpinner = setTimeout(() => { shownAtRef.current = Date.now(); setPhase('spinner'); }, SPINNER_AFTER); const toProgress = setTimeout(() => setPhase('progress'), PROGRESS_AFTER); return () => { clearTimeout(toSpinner); clearTimeout(toProgress); }; } // Nothing was shown, so the phase is still idle. if (shownAtRef.current === null) return; const remaining = Math.max(0, MIN_DISPLAY - (Date.now() - shownAtRef.current)); const hide = setTimeout(() => { shownAtRef.current = null; setPhase('idle'); }, remaining); return () => clearTimeout(hide); }, [loading]);
// Before the first timer fires the work is under way but nothing shows yet. return loading && phase === 'idle' ? 'waiting' : phase;}
const rank: Record<LoadingPhase, number> = { idle: 0, waiting: 1, spinner: 2, progress: 3 };
function Job({ ms, lang }: { ms: number; lang: Lang }) { const t = copy[lang]; const [running, setRunning] = useState(false); const [percent, setPercent] = useState(0); const [reached, setReached] = useState<LoadingPhase>('idle'); const phase = useLoadingPhase(running);
// The furthest phase of this run; adjusting state during render avoids an extra effect pass. if (rank[phase] > rank[reached]) setReached(phase);
useEffect(() => { if (!running) return; const started = Date.now(); const tick = setInterval(() => setPercent(Math.min(99, Math.floor(((Date.now() - started) / ms) * 100))), 250); const done = setTimeout(() => setRunning(false), ms); return () => { clearInterval(tick); clearTimeout(done); }; }, [running, ms]);
const start = () => { if (running) return; setReached('waiting'); setPercent(0); setRunning(true); };
return ( <Stack gap="xs"> <Inline gap="md" align="center" wrap> <Button variant="secondary" loading={phase === 'spinner' || phase === 'progress'} onClick={start}> {t.run(ms)} </Button> <Text size="sm" tone="subtle"> {t.reached}: {t.phases[reached]} </Text> </Inline> {phase === 'progress' ? ( <ProgressBar label={t.progress} value={percent} valueText={t.progressText(percent)} /> ) : null} </Stack> );}
function SummaryCard({ lang }: { lang: Lang }) { const t = copy[lang]; const [loading, setLoading] = useState(true); const phase = useLoadingPhase(loading);
useEffect(() => { if (!loading) return; const done = setTimeout(() => setLoading(false), 2000); return () => clearTimeout(done); }, [loading]);
const busy = loading || phase !== 'idle';
return ( <Card> <Card.Header> <Card.Title>{t.cardTitle}</Card.Title> <Card.Actions> <Button variant="tertiary" size="sm" disabled={busy} onClick={() => setLoading(true)}> {t.reload} </Button> </Card.Actions> </Card.Header> <Card.Body> <div aria-busy={busy} className="min-h-16"> {phase === 'waiting' ? null : ( <Skeleton loading={busy} lines={3} label={t.loadLabel}> <Text>{t.cardText}</Text> </Skeleton> )} </div> </Card.Body> </Card> );}
/** * Three jobs of different length show which indicator each one reaches; a content card shows a skeleton * that appears only after 1 s and reveals the content when the load ends. */export function LoadingStates({ lang = 'tr' }: { lang?: Lang }) { const t = copy[lang]; return ( <LangProvider lang={lang}> <Stack gap="xl" className="w-full max-w-xl"> <Stack gap="md"> <Text weight="medium">{t.actions}</Text> <Job ms={600} lang={lang} /> <Job ms={2000} lang={lang} /> <Job ms={6000} lang={lang} /> </Stack> <SummaryCard lang={lang} /> </Stack> </LangProvider> );}Kurallar
Bölüm başlığı “Kurallar”| Kural | Değer |
|---|---|
| 1 sn altı | Hiçbir şey gösterilmez; kısa bekleme yanıp sönmeye yol açar. |
| 1 ile 3 sn | İşlemde düğmenin içinde Spinner (Button loading), içerikte Skeleton. |
| 3 sn üstü | ProgressBar: ilerleme biliniyorsa belirli (yüzde veya adım), bilinmiyorsa belirsiz (value verilmez); etiketi neyin yapıldığını söyler. |
| En kısa görünme | Gösterge çıktıktan sonra en az 500 ms kalır. |
| İlk yükleme | Bölgenin ilk yüklemesinde iskelet kullanın; içerik 110 ms solarak gelir. |
| İşlem | Spinner kontrolün içinde durur, düğme genişliğini korur. |
| Kapsam | Bölüm yüklemesi tüm sayfayı kilitlemez. |
| Bölge işareti | Yüklenen bölgede aria-busy="true". |
Davranış
Bölüm başlığı “Davranış”- İş başlar. İlk saniye boyunca ekranda gösterge yoktur.
- 1 sn dolduğunda ve iş sürüyorsa gösterge çıkar: işlemde Spinner, içerikte Skeleton.
- 3 sn dolduğunda ve iş sürüyorsa ilerleme çubuğu eklenir; etiketi ne olduğunu, değeri yüzdeyi söyler: “Kayıtlar hazırlanıyor”, %40. İlerleme bilinmiyorsa çubuk belirsiz kalır ve süpürür.
- İş bittiğinde gösterge en az 500 ms görünmüş olacak şekilde kalkar; sonuç gelir.
- Sonuç ekranda zaten görünüyorsa ayrıca duyuru yapılmaz. Görünmüyorsa (örneğin dışa aktarma) bitiş bir kez duyurulur.
Bölümün yüklenmesi sırasında sayfanın geri kalanı kullanılabilir kalır. Hata olursa Hatadan kurtarma kalıbına geçin.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- Yüklenen bölge
aria-busy="true"taşır; iş bitince kalkar. - Başlangıçta bir kez nazik (polite) duyuru yapılır: “Yükleniyor”. Skeleton ve Spinner bunu kendisi yapar.
- Bitişte duyuru yalnızca sonuç başka yoldan görünmüyorsa yapılır; aksi halde ekran okuyucu gereksiz tekrar duyar.
ProgressBarrole="progressbar"taşır; etiketi adı, yüzdesi değeridir (valueTextile “%40 tamamlandı” gibi okunur). Değer duyurusu her adımda değil, anlamlı aralıklarla güncellenir.- Gösterge tek başına renkle anlatılmaz; metin veya erişilebilir ad her zaman vardır.
İçerik
Bölüm başlığı “İçerik”- Neyin yüklendiğini söyleyin: “Kayıtlar hazırlanıyor”, “Özet yükleniyor”.
- Etiket neyin yapıldığını söyler (“Kayıtlar hazırlanıyor”); yüzdeyi çubuk gösterir, etikete yinelemeyin.
- Dolgu cümleler yazmayın; yalnızca durumu söyleyin.
Hareket
Bölüm başlığı “Hareket”| Öğe | Hareket | Preset |
|---|---|---|
| Skeleton | 1500 ms parlama, 3 tur, sonra durur | motion.skeleton |
| İçeriğin gelişi | 110 ms solma | motion.reveal |
| Spinner ve belirsiz çubuk | 1000 ms doğrusal dönüş ve süpürme | motion.indeterminate |
| Belirli ilerleme | 150 ms | motion.progress |
Hareketi azaltma tercihinde parlama ve dönüş durur; metin ve erişilebilir ad durumu anlatmaya devam eder.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”Button, ProgressBar, Spinner, Skeleton, Card, Text. Sonraki kalıplar: Kaydetme ve Hatadan kurtarma.