Tarkibga o'tish

26 β€” Yakuniy loyiha: to'liq dashboard/landing

⬅️ Oldingi: 25 β€” Best practices, arxitektura va migratsiya Β· 🏠 README Β· πŸŽ‰ Kitob yakuni: README

Bu bobda: O'rgangan hamma narsani bitta, haqiqiy loyihada birlashtiramiz β€” SaaS landing + dashboard ilovasini noldan quramiz. Setupdan boshlab (@tailwindcss/vite), @theme dizayn tizimini (18-bob), responsive app shellni, gradientli hero'ni, @container'li feature kartalarni (10-bob), @tailwindcss/forms bilan validatsiyali formani (23-bob), dashboard widgetlarini, ishlaydigan dark-mode toggle'ni (16-bob), cva+cn bilan qayta ishlatiluvchi <Button> komponentini (22-bob) va production'ga jo'natishgacha (24-bob). Har bir qarorni qaysi bob bergani aytib boriladi. Oxirida β€” tabriklash va keyingi qadamlar.


26.1 Avval nega? β€” nega bitta katta loyiha?

Yigirma besh bob davomida har bir tushunchani alohida o'rgandingiz: spacing, flex, grid, ranglar, holatlar, @theme, komponentlar... Bu β€” to'g'ri yo'l, chunki bir vaqtning o'zida hammasini o'rganib bo'lmaydi.

Lekin haqiqiy ish bunday bo'lmaydi. Real loyihada bu tushunchalarning hammasi bir vaqtda, bir-biriga bog'lanib ishlaydi: hero'dagi tugma @theme'dagi brend rangini ishlatadi, u dark rejimda o'zgaradi, feature kartasi konteyner so'rovi bilan moslashadi, forma esa fokus halqasini brend rangidan oladi. Mana shu bog'lanish β€” ekspertlikning asl belgisi.

Shuning uchun bu kapston bob β€” yangi tushuncha o'rgatmaydi. U o'rganganlaringizni bir butunga ulaydi. Biz "DashKit" deb nomlaymiz β€” kichik SaaS mahsuloti uchun bitta sahifali landing + himoyalangan dashboard. Bitta brend, bitta dizayn tizimi, boshdan oxir.

πŸ’‘ Analogiya. Avvalgi boblar β€” gammalar va akkordlar mashqi. Bu bob β€” birinchi marta to'liq kuy chalish. Notalar o'zgarmaydi; faqat endi ular birga yangraydi.

Loyiha anatomiyasi: app.css markaziy manba bo'lib, header, hero, feature kartalar, forma, dashboard, dark-toggle, Button komponentini oziqlantiradi; har blok ostida manba bob ko'rsatilgan

Yuqoridagi xarita butun loyihaning skeletini ko'rsatadi: tepada app.css β€” bitta dizayn manbai, undan har bir qism oziqlanadi. Pastda β€” qurish tartibi. Shu tartibda boramiz.


26.2 1-qadam: Setup (02 va 24-bob)

Loyihani Vite bilan boshlaymiz β€” bu 02-bobda ko'rgan to'rt yo'ldan eng tavsiya etilgani, va 24-bobda bilganimizdek, eng tez build beradi.

npm create vite@latest dashkit -- --template react
cd dashkit
npm install tailwindcss @tailwindcss/vite
npm install @tailwindcss/forms @tailwindcss/typography
npm install class-variance-authority clsx tailwind-merge

vite.config.js'da Tailwind plaginini ulaymiz β€” bu PostCSS sozlamasini ham, content scanningni ham o'zi qiladi:

// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Endi src/app.css faylini yaratamiz va uni main.jsx'da import qilamiz:

// src/main.jsx
import "./app.css";

πŸ“Œ Atamalar. Vite β€” zamonaviy build asbobi (dev-server + bundler). @tailwindcss/vite β€” Tailwind'ni Vite'ga ulaydigan rasmiy plugin; PostCSS yoki CLI'siz, eng tez yo'l (02-bob). plugin (@plugin) β€” Tailwind funksionalligini kengaytiruvchi paket; biz forms va typography'ni qo'shamiz (21-bob).


26.3 2-qadam: Dizayn tizimi β€” to'liq app.css (18-bob)

Mana loyihaning yuragi. 18-bobda o'rgangan har bir g'oya shu yerda: brend rang shkalasi (OKLCH), display shrifti, custom radius/breakpoint, @custom-variant dark (16-bob), va dark rejimda avtomatik temalanuvchi semantik tokenlar (@theme inline).

Bu β€” loyihaning kanonik app.css'i. Uni to'liq beraman, chunki qolgan hammasi shundan oziqlanadi:

/* src/app.css */
@import "tailwindcss";

/* Rasmiy plaginlar (21-bob) β€” forma elementlari va prose tipografiyasi */
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";

/* Dark mode = class strategiya (16-bob). v4 da bu CSS'da, JS config'da emas. */
@custom-variant dark (&:where(.dark, .dark *));

@theme {
  /* 1. Brend rang shkalasi β€” shkala SHART, bitta ottenok emas (18-bob) */
  --color-brand-50:  oklch(0.97 0.02 264);
  --color-brand-100: oklch(0.93 0.05 264);
  --color-brand-200: oklch(0.86 0.10 264);
  --color-brand-300: oklch(0.78 0.14 264);
  --color-brand-400: oklch(0.69 0.18 264);
  --color-brand-500: oklch(0.60 0.20 264);
  --color-brand-600: oklch(0.53 0.20 264);
  --color-brand-700: oklch(0.46 0.18 264);
  --color-brand-800: oklch(0.39 0.15 264);
  --color-brand-900: oklch(0.31 0.11 264);

  /* 2. Display shrifti (12-bob) */
  --font-display: "Satoshi", "Segoe UI", system-ui, sans-serif;

  /* 3. Custom radius, soya va breakpoint (13, 14, 09-bob) */
  --radius-card: 1rem;
  --shadow-card: 0 8px 30px oklch(0.31 0.11 264 / 0.12);
  --breakpoint-3xl: 120rem;   /* β†’ 3xl: varianti */
}

/* 4. Semantik qatlam β€” @theme inline, chunki dark'da qiymat o'zgaradi (18-bob) */
:root {
  --surface:        oklch(1 0 0);          /* oq */
  --surface-2:      oklch(0.98 0.005 264); /* biroz to'qroq fon */
  --ink:            oklch(0.22 0.02 264);  /* matn */
  --ink-muted:      oklch(0.50 0.02 264);  /* yordamchi matn */
  --line:           oklch(0.92 0.01 264);  /* chegara */
}
.dark {
  --surface:        oklch(0.21 0.02 264);
  --surface-2:      oklch(0.26 0.02 264);
  --ink:            oklch(0.96 0.01 264);
  --ink-muted:      oklch(0.72 0.02 264);
  --line:           oklch(0.34 0.02 264);
}
@theme inline {
  --color-surface:   var(--surface);
  --color-surface-2: var(--surface-2);
  --color-ink:       var(--ink);
  --color-muted:     var(--ink-muted);
  --color-line:      var(--line);
}

/* 5. Asos resetlar (@layer base) */
@layer base {
  body {
    background-color: var(--color-surface);
    color: var(--color-ink);
    font-family: var(--font-display);
  }
  a {
    color: var(--color-brand-600);
  }
}

Diqqat qiling β€” bu app.css'da loyihaning butun dizayn tili bor. Endi markup'da bg-surface text-ink yozsangiz, u ikkala rejimda ham o'zini to'g'ri bo'yaydi, hech qanday qo'shimcha dark: yozmasdan. bg-brand-500, rounded-card, shadow-card, font-display β€” hammasi tayyor.

πŸ’‘ Nega @theme inline? Semantik token (--color-surface) qiymati boshqa o'zgaruvchiga (--surface) ishora qiladi, u esa .dark'da o'zgaradi. inline bo'lmasa, utility qiymatni "qotirib" olardi va dark rejimni sezmasdi. Buni 18.8-bobda batafsil ko'rgansiz.


26.4 3-qadam: App shell β€” responsive skelet (06, 07, 08-bob)

Endi ilova "ramkasi" β€” sticky header + sidebar + main maydon. Bu yerda 06-bob (flex header ichida), 07-bob (grid-cols-[260px_1fr]) va 08-bob (sticky, z-50) birga ishlaydi.

Responsive app shell: desktopda sticky header + grid-cols-[260px_1fr] sidebar/main; mobilda sidebar yashirilib qism stacked bo'ladi

Asosiy g'oya: desktopda ikki ustun (260px sidebar + 1fr main), mobilda esa bir ustun β€” sidebar yashirinadi. Mobile-first bo'lgani uchun (09-bob), asos = bir ustun, lg:'da ikkiga bo'linadi.

<body class="min-h-screen bg-surface text-ink">
  <!-- Sticky header β€” butun en bo'ylab (08-bob: sticky, z-50) -->
  <header class="sticky top-0 z-50 border-b border-line
                 bg-surface/80 backdrop-blur
                 supports-[backdrop-filter]:bg-surface/60">
    <div class="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-3">
      <a href="#" class="flex items-center gap-2 text-lg font-bold">
        <span class="grid size-8 place-items-center rounded-lg
                     bg-brand-600 text-white">D</span>
        DashKit
      </a>

      <nav class="hidden items-center gap-6 text-sm md:flex">
        <a href="#features" class="text-muted hover:text-ink transition-colors">Imkoniyatlar</a>
        <a href="#newsletter" class="text-muted hover:text-ink transition-colors">Aloqa</a>
      </nav>

      <div class="flex items-center gap-2">
        <button id="theme-toggle" type="button"
                aria-label="Mavzuni almashtirish"
                class="grid size-9 place-items-center rounded-lg
                       text-muted hover:bg-surface-2 hover:text-ink
                       focus-visible:outline-2 focus-visible:outline-brand-500 transition-colors">
          πŸŒ™
        </button>
      </div>
    </div>
  </header>

  <!-- Dashboard layout: mobilda 1 ustun, lg da 260px + 1fr (07-bob) -->
  <div class="mx-auto grid max-w-7xl grid-cols-1 lg:grid-cols-[260px_1fr]">
    <aside class="hidden border-r border-line p-4 lg:block">
      <!-- sidebar navigatsiyasi -->
    </aside>
    <main class="p-4 sm:p-6 lg:p-8">
      <!-- hero, kartalar, dashboard shu yerda -->
    </main>
  </div>
</body>

πŸ’‘ bg-surface/80 backdrop-blur β€” bu yarim shaffof, orqasini xiralashtiradigan header (frosted glass), 14-bobda ko'rgan backdrop-* filteridan. supports-[backdrop-filter]: β€” eski brauzerlarni hisobga olib, blur ishlamasa to'qroq fon beradi (progressive enhancement). size-8/size-9 β€” bir vaqtda w va h (04-bob).

πŸ“ Mobil sidebar. Bu yerda sidebar mobilda hidden. Real ilovada uni ☰ tugmasi bosilganda chiqadigan drawer qilasiz (peer-checked: yoki ozgina JS bilan). Mashqlarda shuni kengaytirasiz.


26.5 4-qadam: Hero seksiyasi (11, 13, 17-bob)

Landing'ning birinchi ekrani β€” diqqatni tortuvchi gradientli hero. Bu yerda 13-bob (bg-linear-to-br), 11-bob (rang shkalasi) va 17-bob (hover-lift, active:scale) birga ishlaydi.

<section class="relative overflow-hidden rounded-card
                bg-linear-to-br from-brand-600 via-brand-500 to-brand-400
                px-6 py-16 text-center text-white sm:py-24">
  <!-- yengil radial yorug'lik bezagi -->
  <div class="pointer-events-none absolute inset-0
              bg-radial from-white/15 to-transparent"></div>

  <div class="relative mx-auto max-w-2xl">
    <span class="inline-block rounded-full bg-white/15 px-4 py-1 text-sm font-medium
                 ring-1 ring-white/30 ring-inset">
      v4 bilan qurilgan
    </span>

    <h1 class="mt-6 text-4xl font-bold tracking-tight
               text-shadow-lg sm:text-6xl">
      Mahsulotingizni tezroq oshiring
    </h1>

    <p class="mt-5 text-lg text-white/85 sm:text-xl">
      DashKit β€” jamoangiz uchun tahlil va boshqaruv paneli. Bir necha daqiqada ishga tushadi.
    </p>

    <div class="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
      <a href="#newsletter"
         class="rounded-lg bg-white px-6 py-3 font-semibold text-brand-700 shadow-lg
                transition hover:-translate-y-1 hover:shadow-xl
                active:scale-95
                focus-visible:outline-2 focus-visible:outline-white
                motion-reduce:transform-none motion-reduce:transition-none">
        Bepul boshlash
      </a>
      <a href="#features"
         class="rounded-lg px-6 py-3 font-semibold text-white
                ring-1 ring-white/40 ring-inset
                transition hover:bg-white/10 active:scale-95">
        Batafsil
      </a>
    </div>
  </div>
</section>

Diqqat qiling:

  • bg-linear-to-br from-... via-... to-... β€” bu v4 sintaksisi. Eski bg-gradient-to-br (v3) emas (13-bob).
  • text-shadow-lg β€” sarlavhaga yengil soya (v4.1+ imkoniyati, 14-bob) gradient fonda o'qishni yaxshilaydi.
  • hover:-translate-y-1 + active:scale-95 β€” tugma "tirik" bo'ladi (17-bob).
  • motion-reduce:transform-none β€” harakatni kamaytirishni so'ragan foydalanuvchiga animatsiyani o'chiramiz (accessibility, 17-bob).
  • ring-1 ring-white/30 ring-inset β€” chegara ring orqali; rang currentColor emas, aniq white/30 (13-bob).

26.6 5-qadam: Feature kartalar β€” @container bilan (07, 10-bob)

Endi imkoniyat kartalari. Ikki darajali responsivlik: tashqi grid ekran kengligiga qarab ustun sonini o'zgartiradi (07-bob), ichkarida esa har bir karta o'z ustuni kengligiga qarab moslashadi β€” bu 10-bobning yuragi: @container.

<section id="features" class="mt-12">
  <h2 class="text-2xl font-bold tracking-tight sm:text-3xl">Nega DashKit?</h2>

  <div class="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
    <!-- Har karta = konteyner; @container bilan ichki layout o'zgaradi -->
    <article class="@container group rounded-card border border-line bg-surface-2 p-5
                    shadow-card transition hover:-translate-y-1 hover:border-brand-300">
      <!-- Ensiz ustunda ustma-ust, keng ustunda yonma-yon (@md: = konteyner so'rovi) -->
      <div class="flex flex-col gap-3 @md:flex-row @md:items-start">
        <span class="grid size-11 shrink-0 place-items-center rounded-xl
                     bg-brand-100 text-brand-700
                     transition group-hover:bg-brand-600 group-hover:text-white
                     dark:bg-brand-900/40">
          ⚑
        </span>
        <div>
          <h3 class="font-semibold">Tezkor tahlil</h3>
          <p class="mt-1 text-sm text-muted">
            Real vaqtda ko'rsatkichlar β€” sahifani yangilamasdan.
          </p>
        </div>
      </div>
    </article>
    <!-- ...yana kartalar (bir xil naqsh) -->
  </div>
</section>

Bu yerdagi nozik joy: @md:flex-row β€” bu ekran kengligini emas, kartaning o'z kengligini tekshiradi. Demak xuddi shu karta keng ustunda (xl grid) yonma-yon, ensiz ustunda (mobil) ustma-ust joylashadi β€” bir komponent, ikki kontekst, hech qanday media query'siz. group-hover: esa karta ustiga kelganda ichkarisidagi ikonni bo'yaydi (15-bob).

πŸ’‘ @container ishlashi uchun ota element @container klassiga ega bo'lishi shart β€” biz uni <article>'ga qo'ydik. Tafsilotlar 10-bobda.


26.7 6-qadam: Forma β€” validatsiya bilan (15, 23-bob)

Newsletter/aloqa formasi. @tailwindcss/forms (21-bob) maydonlarga toza asos beradi, biz esa ustiga brend stilini qo'yamiz. Validatsiya uchun 15-bobdagi user-invalid: holatini va focus-visible: halqasini ishlatamiz β€” bu 23-bobning amaliy formasi.

<section id="newsletter" class="mt-12">
  <div class="mx-auto max-w-md rounded-card border border-line bg-surface-2 p-6 shadow-card">
    <h2 class="text-xl font-bold">Yangiliklardan xabardor bo'ling</h2>
    <p class="mt-1 text-sm text-muted">Haftada bir marta. Spam yo'q.</p>

    <form id="newsletter-form" class="mt-5 space-y-4" novalidate>
      <div>
        <label for="email" class="block text-sm font-medium">Email</label>
        <input id="email" name="email" type="email" required
               placeholder="siz@example.com"
               class="mt-1 block w-full rounded-lg border-line bg-surface
                      text-ink placeholder:text-muted
                      focus:border-brand-500 focus:ring-2 focus:ring-brand-500/40
                      user-invalid:border-red-500 user-invalid:ring-red-500/30" />
        <!-- Faqat noto'g'ri va tegingandan keyin ko'rinadigan xato matni -->
        <p class="mt-1 hidden text-sm text-red-600 peer-user-invalid:block">
          To'g'ri email kiriting.
        </p>
      </div>

      <button type="submit" data-loading="false"
              class="group inline-flex w-full items-center justify-center gap-2
                     rounded-lg bg-brand-600 px-4 py-2.5 font-semibold text-white
                     transition hover:bg-brand-700 active:scale-95
                     focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500
                     disabled:opacity-60 data-[loading=true]:cursor-wait">
        <!-- Yuklanish spinneri β€” faqat data-loading=true bo'lganda (15-bob: data-*) -->
        <span class="hidden size-4 animate-spin rounded-full border-2 border-white/40
                     border-t-white group-data-[loading=true]:inline-block"></span>
        <span>Obuna bo'lish</span>
      </button>
    </form>
  </div>
</section>

user-invalid: β€” bu muhim detal: oddiy :invalid bo'sh maydonni ham darrov qizartiradi (foydalanuvchi hali yozmasdan). user-invalid: esa faqat foydalanuvchi teginib, noto'g'ri qoldirganda ishlaydi β€” bu ancha xushmuomala UX (15-bob). focus:ring-brand-500/40 β€” fokus halqasi brend rangidan, /40 shaffoflik bilan (11-bob).

Yuklanish holatini JS bilan boshqaramiz (data-loading'ni almashtiramiz):

<script>
  document.getElementById('newsletter-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const btn = e.target.querySelector('button[type=submit]');
    btn.dataset.loading = 'true';   // spinner ko'rinadi (data-[loading=true]:)
    btn.disabled = true;
    // ...so'rov yuborish (fetch) β€” keyin data-loading='false' qaytariladi
  });
</script>

26.8 7-qadam: Dashboard widgetlari (04, 05, 12, 14-bob)

Endi himoyalangan tomon β€” dashboard. KPI stat kartalari, oddiy ro'yxat (divide-y, 05-bob), badge'lar va prose kontent paneli (12-bob).

<!-- KPI stat kartalari -->
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
  <div class="rounded-card border border-line bg-surface-2 p-5 shadow-card">
    <p class="text-sm text-muted">Foydalanuvchilar</p>
    <p class="mt-2 text-3xl font-bold tracking-tight">12,480</p>
    <p class="mt-1 inline-flex items-center gap-1 rounded-full
              bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700
              dark:bg-green-900/40 dark:text-green-300">
      ↑ 12%
    </p>
  </div>
  <!-- ...yana 3 ta KPI -->
</div>

<!-- Ro'yxat β€” divide-y bilan qatorlar ajratiladi (05-bob) -->
<div class="mt-6 rounded-card border border-line bg-surface-2 shadow-card">
  <div class="border-b border-line px-5 py-3 font-semibold">So'nggi buyurtmalar</div>
  <ul class="divide-y divide-line">
    <li class="flex items-center justify-between px-5 py-3">
      <span>Pro tarif Γ— 1</span>
      <span class="rounded-full bg-brand-100 px-2.5 py-0.5 text-xs font-medium
                   text-brand-700 dark:bg-brand-900/40 dark:text-brand-200">
        To'langan
      </span>
    </li>
    <li class="flex items-center justify-between px-5 py-3">
      <span>Team tarif Γ— 3</span>
      <span class="rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium
                   text-amber-700 dark:bg-amber-900/40 dark:text-amber-200">
        Kutilmoqda
      </span>
    </li>
  </ul>
</div>

<!-- prose kontent paneli (12-bob: @tailwindcss/typography) -->
<article class="prose prose-slate mt-6 max-w-none dark:prose-invert
                rounded-card border border-line bg-surface-2 p-6 shadow-card">
  <h3>Hisobot</h3>
  <p>Bu oy daromad <strong>22% ga oshdi</strong>. Asosiy o'sish Pro tarifdan.</p>
  <ul>
    <li>Yangi obunalar: 1,240</li>
    <li>Bekor qilishlar: 86</li>
  </ul>
</article>

divide-y divide-line β€” qatorlar orasiga avtomatik chegara qo'yadi, har bir <li>'ga alohida border yozmaysiz (05-bob). prose dark:prose-invert β€” uzun matnga tayyor tipografiya, dark rejimda esa avtomatik teskari (12-bob).


26.9 8-qadam: Dark mode β€” ishlaydigan toggle (16-bob)

Hozirgacha hamma sirtni bg-surface, text-ink, border-line bilan yozdik β€” bu semantik tokenlar app.css'da .dark'da o'zgaradi, demak dark mode allaqachon tayyor. Endi faqat .dark klassini almashtirish kerak.

Eng avval β€” FOUC (noto'g'ri rejim chaqnashi)ni oldini olish. <head>'ga, CSS'dan oldin, inline skript (16.6-bob):

<head>
  <!-- Sahifa bo'yalishidan OLDIN ishlaydi β†’ chaqnash yo'q -->
  <script>
    (function () {
      const saved = localStorage.getItem('theme') || 'system';
      const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
      if (saved === 'dark' || (saved === 'system' && systemDark)) {
        document.documentElement.classList.add('dark');
      }
    })();
  </script>
  <link rel="stylesheet" href="/src/app.css" />
</head>

Endi header'dagi toggle tugmasiga mantiq:

<script>
  const btn = document.getElementById('theme-toggle');
  btn.addEventListener('click', () => {
    const isDark = document.documentElement.classList.toggle('dark');
    localStorage.setItem('theme', isDark ? 'dark' : 'light');
    btn.textContent = isDark ? 'β˜€οΈ' : 'πŸŒ™';
  });
</script>

classList.toggle('dark') <html>'ga .dark'ni qo'shadi/oladi va true/false qaytaradi; uni localStorage'ga saqlaymiz (16-bob). Inline skript bo'lgani uchun keyingi yuklanishda chaqnash bo'lmaydi.

⚠️ Eslatma. Biz har sirtni dark:bg-... bilan emas, semantik token bilan yozdik. Shuning uchun dark rejim deyarli "tekin" keldi β€” app.css'da .dark { --surface: ... } yetarli edi. Faqat badge'lar kabi maxsus ranglarda (dark:bg-green-900/40) qo'shimcha dark: ishlatdik. Bu β€” 18-bobdagi semantik token naqshining kuchi.


26.10 9-qadam: Qayta ishlatiluvchi <Button> β€” cva + cn (22-bob)

Loyiha bo'ylab tugma bir necha xil ko'rinishda kerak: asosiy (primary), ikkilamchi (secondary), kichik/katta. Har joyga uzun klass qatorini nusxa qilish o'rniga β€” bitta komponent. Bu 22-bobning asosiy naqshi: cva (variantlar) + cn (klasslarni xavfsiz birlashtirish).

Avval cn yordamchisi (clsx + tailwind-merge):

// src/lib/cn.js
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs) {
  return twMerge(clsx(inputs));
}

Endi Button komponenti cva bilan:

// src/components/Button.jsx
import { cva } from "class-variance-authority";
import { cn } from "../lib/cn";

const button = cva(
  // har variant uchun umumiy asos
  "inline-flex items-center justify-center gap-2 rounded-lg font-semibold " +
    "transition active:scale-95 " +
    "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 " +
    "disabled:opacity-60 disabled:pointer-events-none " +
    "motion-reduce:transition-none motion-reduce:active:scale-100",
  {
    variants: {
      variant: {
        primary: "bg-brand-600 text-white hover:bg-brand-700",
        secondary:
          "bg-surface-2 text-ink ring-1 ring-line ring-inset hover:bg-surface",
        ghost: "text-brand-700 hover:bg-brand-50 dark:text-brand-300 dark:hover:bg-brand-900/30",
      },
      size: {
        sm: "px-3 py-1.5 text-sm",
        md: "px-4 py-2.5",
        lg: "px-6 py-3 text-lg",
      },
    },
    defaultVariants: { variant: "primary", size: "md" },
  }
);

export function Button({ variant, size, className, ...props }) {
  return <button className={cn(button({ variant, size }), className)} {...props} />;
}

Ishlatish β€” butun loyihada bir xil:

<Button>Bepul boshlash</Button>
<Button variant="secondary">Batafsil</Button>
<Button variant="ghost" size="sm">Bekor qilish</Button>

cva har variantga to'g'ri klasslarni tanlaydi; cn esa className orqali kelgan ustdagi klasslarni xavfsiz qo'shadi (tailwind-merge ziddiyatlarni yechadi β€” masalan, tashqaridan bg-red-600 bersangiz, asosdagi bg-brand-600'ni to'g'ri ustiga yozadi). Bu naqsh 22-bobda batafsil β€” bu yerda loyihaga ulab ko'rsatdik.

πŸ’‘ Endi hero'dagi va dashboard'dagi har bir tugma shu bitta <Button>'dan. Brendni o'zgartirsangiz β€” app.css'dagi --color-brand-* β€” barcha tugma birga yangilanadi. Bitta manba, ko'p joy.


26.11 10-qadam: Polish va ship (24, 25-bob)

Loyiha tayyor β€” endi uni productionga jo'natishdan oldin sayqal. Bu 24-bob va 25-bob bo'limi.

1) Accessibility o'tishi.

  • Har bir interaktiv element focus-visible: halqasiga ega bo'lsin (klaviatura foydalanuvchilari uchun) β€” biz buni tugma va inputlarga qo'shdik.
  • Ikon-tugmalarga aria-label (masalan toggle: aria-label="Mavzuni almashtirish").
  • Faqat ko'z uchun yashirin matn β€” sr-only:
<button aria-label="Menyuni ochish" class="lg:hidden ...">
  <span class="sr-only">Menyu</span>
  ☰
</button>
  • Kontrast: matn/fon juftliklari yetarli kontrastga ega bo'lsin β€” text-muted'ni juda och qilmang (11-bob).
  • Harakat: animatsiyalarda motion-reduce: jufti (17-bob).

2) Dinamik klass tekshiruvi (24-bob). Eng ko'p uchraydigan production xatosi β€” JS'da string yopishtirilgan klass generatsiya qilinmaydi, chunki Tailwind faylni matn sifatida skanerlaydi:

// ❌ NOTO'G'RI β€” `bg-brand-${shade}` Tailwind tomonidan TOPILMAYDI, build'da yo'qoladi
<div className={`bg-brand-${shade}`} />

// βœ… TO'G'RI β€” to'liq klass nomlari ko'rinib tursin
const tone = { green: "bg-green-100 text-green-700", amber: "bg-amber-100 text-amber-700" };
<div className={tone[status]} />

Buni 24-bobda batafsil ko'rgansiz β€” har bir klass to'liq, statik string bo'lsin.

3) Production build. Vite avtomatik minify qiladi:

npm run build      # dist/ β€” minifikatsiyalangan, faqat ishlatilgan klasslar
npm run preview    # natijani lokal ko'rib chiqish

Faqat Tailwind CLI ishlatsangiz, --minify qo'lda:

npx @tailwindcss/cli -i src/app.css -o dist/app.css --minify

Natijada CSS faqat siz haqiqatan ishlatgan klasslarni o'z ichiga oladi (Oxide dvigateli avtomatik content-detection bilan) β€” odatda bir necha o'n kilobayt, gzip'dan keyin yana kichik (24-bob).

βœ… Yakuniy tekshiruv (25-bob). Build toza o'tdimi? Klaviatura bilan butun sahifani aylanib chiqa olasizmi? Dark rejim chaqnamasdan ishlaydimi? Mobilda layout buzilmaydimi? Hammasi "ha" bo'lsa β€” tayyor.


26.12 Recap β€” qaysi bob qayerda ishladi

Bitta loyihada butun kitob qatnashdi. Mana xarita:

Loyiha qismi Manba bob(lar)
Setup, Vite plugin, build/--minify 02, 24
app.css, @theme, semantik tokenlar 18
Plaginlar (forms, typography) 21
App shell (flex / grid / sticky / z-index) 06, 07, 08
Responsive (mobile-first, lg:) 09
Hero gradienti, ring/border, soya 11, 13, 14
Hover-lift, active:scale, motion-reduce: 17
Feature kartalar, @container 07, 10
Forma, user-invalid:, focus-visible: 15, 23
Dashboard: spacing, divide-y, prose 04, 05, 12
Dark mode toggle + no-FOUC 16
<Button> (cva + cn) 22
Accessibility, dinamik klass, ship 24, 25

Egallangan bilim xaritasi: markazda Tailwind v4 ekspert, atrofda kitobning ustunlari β€” utility-first, layout, responsive+container, ranglar/tipografiya, holatlar/dark, @theme, custom utility/variant, komponentlar, production


26.13 Tabriklaymiz! πŸŽ‰

Mana, yetib keldingiz. "Utility-first nima?" degan savoldan boshlab β€” endi to'liq responsive, dark-mode'li, hammabop (accessible), production'ga tayyor SaaS ilovani noldan qura olasiz.

Eng muhimi β€” siz Tailwind'ni "sehrli klasslar to'plami" sifatida emas, o'zingiz boshqaradigan dizayn tizimi sifatida o'rgandingiz. Har bir klass ortida qaysi CSS turishini bilasiz; @theme'da bitta token o'zgartirib butun ilova ko'rinishini boshqara olasiz; v3 va v4 farqini ajrata olasiz. Bu β€” yuzaki "klass yodlash" emas, chuqur tushunish.

Keyingi qadamlar

  • O'zingizning loyihangizni quring. Eng yaxshi mustahkamlash β€” yangi narsa qurish. Bu DashKit'ni kengaytiring (mashqlarga qarang) yoki butunlay yangi g'oyani amalga oshiring.
  • React yoki Vue'ga chuqurroq kiring. Komponent naqshini (22-bob) butun ilovaga tatbiq qiling β€” cva bilan dizayn tizimi komponent kutubxonasini yarating.
  • Komponent kutubxonalarini ko'ring. shadcn/ui (React) va daisyUI (21-bob) β€” Tailwind ustiga qurilgan tayyor naqshlar; ularning kodini o'qish ko'p narsa o'rgatadi.
  • Rasmiy hujjat β€” eng yaxshi do'st. tailwindcss.com doim eng so'nggi v4 manbasi. Yangi utility chiqsa, avval shu yerdan tekshiring.

Yo'l shu yerda tugamaydi β€” bu boshlanish. Endi sizda mustahkam poydevor bor; ustiga nima qurish β€” sizning qo'lingizda. Omad! πŸš€


Mashqlar

Bu safar mashqlar β€” loyihani kengaytirish chaqiruvlari. To'liq kod o'rniga yechim eskizini beramiz; qolganini siz qurasiz.

1-mashq. App shelldagi sidebar hozir mobilda butunlay hidden. Uni ☰ tugmasi bosilganda chiqadigan drawer qiling β€” iloji bo'lsa JS'siz, faqat peer bilan (15-bob).

Yechim eskizi

Yashirin checkbox + peer naqshi: checkbox holatiga qarab sidebar translate-x'ini almashtiramiz.

<input type="checkbox" id="menu" class="peer hidden" />
<label for="menu" class="lg:hidden cursor-pointer" aria-label="Menyu">☰</label>

<aside class="fixed inset-y-0 left-0 w-64 -translate-x-full
              border-r border-line bg-surface p-4 transition-transform
              peer-checked:translate-x-0
              lg:static lg:translate-x-0 lg:block">
  <!-- nav -->
</aside>

peer-checked:translate-x-0 β€” checkbox belgilanganda sidebar sirpanib chiqadi; lg:'da u doim ko'rinadi va peer mantiqi e'tiborga olinmaydi. Yaxshilab qilsangiz, orqaga peer-checked: bilan qorong'i overlay ham qo'shing.

2-mashq. Hero'dagi ikkita CTA tugmasini 26.10-dagi <Button> komponenti bilan almashtiring. Oq fonli tugma uchun yangi variant kerakmi?

Yechim eskizi

Hero foni gradient, shuning uchun oq tugma kerak β€” cva variantlariga onBrand qo'shamiz:

variant: {
  // ...mavjudlar...
  onBrand: "bg-white text-brand-700 shadow-lg hover:-translate-y-1 hover:shadow-xl",
}
<Button variant="onBrand" size="lg">Bepul boshlash</Button>
<Button variant="ghost" size="lg" className="text-white ring-1 ring-white/40">Batafsil</Button>

className orqali maxsus stilni qo'shdik β€” cn (tailwind-merge) ziddiyatni to'g'ri yechadi. Bu β€” komponentni moslashuvchan qilishning naqshi (22-bob).

3-mashq. KPI kartalariga @container qo'shing β€” keng ustunda raqam va o'sish foizi yonma-yon, ensiz ustunda ustma-ust bo'lsin (10-bob).

Yechim eskizi

Kartaning o'ziga @container, ichidagi joylashuvga konteyner so'rovi:

<div class="@container rounded-card border border-line bg-surface-2 p-5 shadow-card">
  <p class="text-sm text-muted">Foydalanuvchilar</p>
  <div class="mt-2 flex flex-col gap-1 @xs:flex-row @xs:items-baseline @xs:gap-3">
    <p class="text-3xl font-bold tracking-tight">12,480</p>
    <p class="text-xs font-medium text-green-700">↑ 12%</p>
  </div>
</div>

@xs:flex-row β€” karta kengaygach yonma-yon. E'tibor bering: bu ekran emas, kartaning o'lchamiga qarab β€” shuning uchun bir karta keng ustunda, boshqasi ensiz ustunda turli ko'rinadi.

4-mashq. Loyihada uchinchi tema β€” "sepia" (issiq, qog'ozsimon) qo'shing. Foydalanuvchi system / light / dark / sepia orasidan tanlay olsin.

Yechim eskizi

.dark kabi .sepia uchun ham semantik o'zgaruvchilarni belgilaymiz (18-bob):

.sepia {
  --surface:   oklch(0.96 0.03 85);
  --surface-2: oklch(0.92 0.04 85);
  --ink:       oklch(0.30 0.04 60);
  --ink-muted: oklch(0.50 0.04 60);
  --line:      oklch(0.85 0.04 85);
}

JS toggle'ni klass o'rniga data-theme ga o'tkazib, har temada bitta klassni qo'shasiz (avval barchasini olib tashlab): document.documentElement.className = choice. bg-surface text-ink hamma temada avtomatik ishlaydi β€” chunki ular semantik token. Faqat @custom-variant'ni atribut variantiga moslashtiring agar dark: ham kerak bo'lsa.

5-mashq. Newsletter formasini haqiqiy muvaffaqiyat holatiga ulang: yuborilgach, forma o'rniga "Rahmat!" xabari ko'rinsin β€” data-* holat bilan (15-bob).

Yechim eskizi

Konteynerga data-sent holatini berib, ikki blokni data-[sent=...] bilan almashtiramiz:

<div id="nl" data-sent="false">
  <form class="space-y-4 data-[sent=true]:hidden ...">...</form>
  <p class="hidden text-center font-semibold text-green-600
            group-data-[sent=true]:block">Rahmat! Obuna tasdiqlandi. πŸŽ‰</p>
</div>
btn.closest('#nl').dataset.sent = 'true';

data-[sent=true]:hidden β€” yuborilgach formani yashiradi, xabarni ko'rsatadi. Hech qanday if/else DOM manipulyatsiyasi yo'q β€” holat atributda, ko'rinish Tailwind variantida.

6-mashq (debugging). Dasturchi dashboard badge rangini dinamik qildi va production'da rang yo'qoldi:

<span className={`bg-${color}-100 text-${color}-700`}>{label}</span>

Sababini toping va to'g'rilang (24-bob).

Yechim

Tailwind kodni matn sifatida skanerlaydi β€” bg-${color}-100 kabi yopishtirilgan string'ni topa olmaydi, shuning uchun u klass build'ga umuman kirmaydi. Yechim β€” to'liq, statik klass nomlarini xaritaga yozish:

const tone = {
  green: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
  amber: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200",
  brand: "bg-brand-100 text-brand-700 dark:bg-brand-900/40 dark:text-brand-200",
};
<span className={tone[color]}>{label}</span>

Endi har bir to'liq klass nomi kodda ko'rinib turadi β†’ Tailwind ularni topadi va build'ga qo'shadi. Qoida: Tailwind klasslari har doim to'liq, uzilmagan string bo'lsin (24-bob).


⬅️ Oldingi: 25 β€” Best practices, arxitektura va migratsiya Β· 🏠 README