İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    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.

    • 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.

    Üç 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.

    Canlı örnek, StorybookStorybook'ta aç
    İşlemler
    Ulaşılan aşama: Gösterge yok
    Ulaşılan aşama: Gösterge yok
    Ulaşılan aşama: Gösterge yok

    Proje özeti

    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>
    );
    }
    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".
    Kaydediliyor
    Yapınİşlem düğmenin içinde gösterilir; düğme yerinde kalır.
    YapmayınGösterge olmadan devre dışı düğme, işlemin sürdüğünü söylemez.
    Özet yükleniyor
    Yapınİlk yüklemede içeriğin yerini iskelet tutar.
    YapmayınBoş alan, içeriğin gelip gelmediğini belli etmez.
    1. İş başlar. İlk saniye boyunca ekranda gösterge yoktur.
    2. 1 sn dolduğunda ve iş sürüyorsa gösterge çıkar: işlemde Spinner, içerikte Skeleton.
    3. 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.
    4. İş bittiğinde gösterge en az 500 ms görünmüş olacak şekilde kalkar; sonuç gelir.
    5. 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.

    • 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.
    • ProgressBar role="progressbar" taşır; etiketi adı, yüzdesi değeridir (valueText ile “%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.
    • 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.
    Öğ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.

    Button, ProgressBar, Spinner, Skeleton, Card, Text. Sonraki kalıplar: Kaydetme ve Hatadan kurtarma.