Bu eski versiya hujjati
Siz v0.2 hujjatini o'qiyapsiz. Eng so'nggi versiya — v0.3.
Mavzu va tokenlar
Ranglar, tipografiya, radius, soya, animatsiya va brend rangini o'zgartirish.
Butun dizayn bitta faylda — @akhatmirzo/tokens/theme.css. Unda CSS o'zgaruvchilari
(--ak-*) va ularni Tailwind utilitalariga ulaydigan @theme bloki bor. Komponentlar hech
qachon indigo-600 kabi xom rangni ishlatmaydi — faqat bg-primary, text-text-muted kabi
semantik tokenlarni. Shuning uchun dark mode va brend rangi bitta joyda almashadi.
@import 'tailwindcss';
@import '@akhatmirzo/tokens/theme.css';Tokenlar ikki qavatli:
- Bazaviy shkalalar —
--ak-indigo-600,--ak-slate-200... Faqat semantik tokenlarni yasash uchun. Komponentlarda va ilova kodida to'g'ridan-to'g'ri ishlatmang. - Semantik tokenlar —
--ak-surface,--ak-text-muted,--ak-primary... Ular light va dark rejimda turli bazaviy qiymatga ishora qiladi va Tailwind'dabg-surface,text-text-muted,bg-primarybo'lib chiqadi.
Ranglar palitrasi
Asosiy rang — indigo, neytral — slate. Status ranglari: emerald (success), amber (warning), rose
(danger) va sky (info). Qiymatlar to'g'ridan-to'g'ri theme.css dan o'qiladi.
'use client';
import { useSyncExternalStore } from 'react';
const SCALES = {
indigo: ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'],
slate: ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'],
emerald: ['50', '100', '300', '400', '500', '600', '700', '950'],
amber: ['50', '100', '300', '400', '500', '600', '700', '950'],
rose: ['50', '100', '300', '400', '500', '600', '700', '950'],
sky: ['50', '100', '300', '400', '500', '600', '700', '950'],
};
const subscribe = () => () => {};
// CSS o'zgaruvchilarning haqiqiy qiymatlari (serverda — bo'sh)
const readValues = () => {
const style = getComputedStyle(document.documentElement);
return Object.entries(SCALES)
.flatMap(([name, steps]) =>
steps.map((s) => style.getPropertyValue(`--ak-${name}-${s}`).trim()),
)
.join(',');
};
export default function ThemingPalette() {
const values = useSyncExternalStore(subscribe, readValues, () => '').split(',');
let index = 0;
return (
<div className="flex w-full flex-col gap-5">
{Object.entries(SCALES).map(([name, steps]) => (
<div key={name} className="flex flex-col gap-2">
<span className="text-sm font-semibold text-text">{name}</span>
<div className="grid grid-cols-4 gap-2 sm:grid-cols-6 lg:grid-cols-11">
{steps.map((step) => {
const value = values[index++];
return (
<div key={step} className="flex flex-col gap-1">
<div
className="h-10 rounded-md border border-border"
style={{ background: `var(--ak-${name}-${step})` }}
/>
<span className="text-xs font-medium text-text">{step}</span>
<span className="font-mono text-xs text-text-muted uppercase">{value}</span>
</div>
);
})}
</div>
</div>
))}
</div>
);
}
Semantik tokenlar
Kod yozganda shu ro'yxatdan tanlang. Sayt mavzusini almashtirib ko'ring — rang namunalari va qiymatlar dark rejimga o'tadi.
'use client';
import { useSyncExternalStore } from 'react';
const TOKENS = [
['background', 'Sahifa foni'],
['surface', 'Karta, input, modal'],
['surface-muted', 'Hover, ikkilamchi fon'],
['surface-raised', 'Popover, toast'],
['border', 'Oddiy chegara'],
['border-strong', 'Hover chegara'],
['text', 'Asosiy matn'],
['text-subtle', 'Ghost tugma, ikonka'],
['text-muted', 'Yordamchi matn'],
['text-faint', 'Placeholder'],
['primary', 'Asosiy tugma'],
['primary-soft', 'Soft tugma, badge'],
['primary-soft-foreground', 'Soft ustidagi matn'],
['link', 'Havola'],
['danger', 'Xavfli tugma'],
['ring', 'Fokus halqasi'],
] as const;
// Mavzu almashganda (<html data-theme>) qiymatlar qayta o'qiladi
const subscribe = (onChange: () => void) => {
const observer = new MutationObserver(onChange);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
return () => observer.disconnect();
};
const readValues = () => {
const style = getComputedStyle(document.documentElement);
return TOKENS.map(([token]) => style.getPropertyValue(`--ak-${token}`).trim()).join('|');
};
export default function ThemingSemantic() {
const values = useSyncExternalStore(subscribe, readValues, () => '').split('|');
return (
<div className="grid w-full gap-3 sm:grid-cols-2">
{TOKENS.map(([token, note], i) => (
<div key={token} className="flex items-center gap-3">
<div
className="size-10 shrink-0 rounded-lg border border-border"
style={{ background: `var(--ak-${token})` }}
/>
<div className="flex min-w-0 flex-col text-xs">
<span className="truncate font-mono font-medium text-text">{token}</span>
<span className="truncate text-text-muted">{note}</span>
<span className="truncate font-mono text-text-faint">{values[i] || '—'}</span>
</div>
</div>
))}
</div>
);
}
| Token | Tailwind | Light | Dark | Qayerda |
|---|---|---|---|---|
background | bg-background | slate-50 | slate-900 | Sahifa foni |
surface | bg-surface | oq | slate-800 | Karta, input, modal |
surface-muted | bg-surface-muted | slate-100 | slate-700 | Hover, ikkilamchi fon |
surface-raised | bg-surface-raised | oq | slate-750 | Popover, toast, menyu |
border | border-border | slate-200 | slate-700 | Oddiy chegara |
border-strong | border-border-strong | slate-300 | slate-600 | Hover chegara |
text | text-text | slate-900 | slate-100 | Asosiy matn |
text-subtle | text-text-subtle | slate-700 | slate-300 | Ghost tugma, ikonkalar |
text-muted | text-text-muted | slate-500 | slate-400 | Yordamchi matn |
text-faint | text-text-faint | slate-400 | slate-500 | Placeholder |
primary | bg-primary | indigo-600 | indigo-600 | Asosiy tugma |
primary-hover | bg-primary-hover | indigo-700 | indigo-500 | Asosiy tugma hover |
primary-foreground | text-primary-foreground | oq | oq | Primary ustidagi matn |
primary-soft | bg-primary-soft | indigo-50 | indigo-950 | Soft tugma, badge |
primary-soft-hover | bg-primary-soft-hover | indigo-100 | indigo-900 | Soft tugma hover |
primary-soft-foreground | text-primary-soft-foreground | indigo-700 | indigo-300 | Soft ustidagi matn |
link | text-link | indigo-600 | indigo-300 | Havolalar |
ring | outline-ring | indigo-500 / 40% | indigo-400 / 50% | Fokus halqasi |
overlay | bg-overlay | slate-900 / 40% | slate-950 / 60% | Modal ortidagi fon |
danger | bg-danger | rose-600 | rose-500 | Xavfli tugma |
danger-hover | bg-danger-hover | rose-700 | rose-600 | Xavfli tugma hover |
danger-ring | outline-danger-ring | rose-500 / 25% | rose-500 / 35% | Xato holatidagi fokus |
Dark rejimda primary o'zgarmaydi
Asosiy tugma ikkala rejimda ham indigo-600. Indigo-500 oq matn bilan WCAG AA kontrast chegarasiga yetmaydi, shuning uchun dark rejimda faqat hover ochroq bo'ladi.
Status ranglari
Har bir status uchun ikki to'plam: soft — badge va xabarlar foni, solid — nuqta va to'liq bo'yalgan badge'lar.
softsuccesswarningdangerinfosolidsuccesswarningdangerinfo'use client';
const STATUSES = ['success', 'warning', 'danger', 'info'] as const;
const SOFT = {
success: 'bg-success-soft text-success-soft-foreground',
warning: 'bg-warning-soft text-warning-soft-foreground',
danger: 'bg-danger-soft text-danger-soft-foreground',
info: 'bg-info-soft text-info-soft-foreground',
};
const SOLID = {
success: 'bg-success text-success-foreground',
warning: 'bg-warning text-warning-foreground',
danger: 'bg-danger text-danger-foreground',
info: 'bg-info text-info-foreground',
};
export default function ThemingStatus() {
return (
<div className="flex flex-col gap-3">
{[SOFT, SOLID].map((styles, row) => (
<div key={row} className="flex flex-wrap items-center gap-2">
<code className="w-12 font-mono text-xs text-text-muted">
{row === 0 ? 'soft' : 'solid'}
</code>
{STATUSES.map((status) => (
<span
key={status}
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${styles[status]}`}
>
{status}
</span>
))}
</div>
))}
</div>
);
}
| Status | Soft | Solid |
|---|---|---|
success | bg-success-soft text-success-soft-foreground | bg-success text-success-foreground |
warning | bg-warning-soft text-warning-soft-foreground | bg-warning text-warning-foreground |
danger | bg-danger-soft text-danger-soft-foreground | bg-danger text-danger-foreground |
info | bg-info-soft text-info-soft-foreground | bg-info text-info-foreground |
Tipografiya
Asosiy shrift — Manrope (lotin va kirill), kod uchun — JetBrains Mono. theme.css
shriftlarni yuklamaydi, faqat nomini beradi (font-sans, font-mono). Ularni alohida ulang:
pnpm add @fontsource-variable/manrope @fontsource-variable/jetbrains-mono@import '@fontsource-variable/manrope';
@import '@fontsource-variable/jetbrains-mono';Shrift yuklanmasa, tizim shrifti (ui-sans-serif, system-ui) ishlatiladi. O'lchamlar shkalasi
qator balandligi va harf oralig'i bilan birga keladi:
text-4xl36 / 40O'zbekiston — Ўзбекистон
text-3xl30 / 36O'zbekiston — Ўзбекистон
text-2xl24 / 32O'zbekiston — Ўзбекистон
text-xl20 / 28O'zbekiston — Ўзбекистон
text-lg18 / 28O'zbekiston — Ўзбекистон
text-base16 / 24O'zbekiston — Ўзбекистон
text-sm14 / 20O'zbekiston — Ўзбекистон
text-xs12 / 16O'zbekiston — Ўзбекистон
font-mono: 1 250 000 so'm · +998 90
'use client';
const SCALE = [
{ className: 'text-4xl font-bold', name: 'text-4xl', spec: '36 / 40' },
{ className: 'text-3xl font-bold', name: 'text-3xl', spec: '30 / 36' },
{ className: 'text-2xl font-semibold', name: 'text-2xl', spec: '24 / 32' },
{ className: 'text-xl font-semibold', name: 'text-xl', spec: '20 / 28' },
{ className: 'text-lg font-medium', name: 'text-lg', spec: '18 / 28' },
{ className: 'text-base', name: 'text-base', spec: '16 / 24' },
{ className: 'text-sm', name: 'text-sm', spec: '14 / 20' },
{ className: 'text-xs font-medium', name: 'text-xs', spec: '12 / 16' },
];
export default function ThemingTypography() {
return (
<div className="flex w-full flex-col divide-y divide-border">
{SCALE.map((item) => (
<div key={item.name} className="flex items-baseline gap-4 py-2">
<div className="flex w-24 shrink-0 flex-col">
<code className="font-mono text-xs text-text">{item.name}</code>
<span className="text-xs text-text-muted">{item.spec}</span>
</div>
<p className={`${item.className} truncate text-text`}>O'zbekiston — Ўзбекистон</p>
</div>
))}
<p className="pt-3 font-mono text-sm text-text-muted">font-mono: 1 250 000 so'm · +998 90</p>
</div>
);
}
Qalinlik o'lchamga bog'lanmagan — har bir komponent font-medium, font-semibold ni o'zi
belgilaydi. Tavsiya: tugma va label — 500, sarlavha — 600, katta sarlavha — 700. Jadval va narxlarda
raqamlar tekis turishi uchun tabular-nums qo'shing.
Radius va soyalar
Tugma va input — rounded-lg (12px), karta va popover — rounded-xl (16px), modal va drawer —
rounded-2xl (20px). Soyalar qora emas, slate-900 rangida — yumshoqroq ko'rinadi. Dark rejimda
shadow-md dan boshlab soyaga 1px halqa qo'shiladi, chunki qorong'i fonda soyaning o'zi deyarli
ko'rinmaydi.
rounded-sm6pxrounded-md8pxrounded-lg12pxrounded-xl16pxrounded-2xl20pxrounded-full9999pxshadow-xsInput, tugmashadow-smKartashadow-mdDropdownshadow-lgPopover, toastshadow-xlModal, drawer'use client';
const RADII = [
['rounded-sm', '6px'],
['rounded-md', '8px'],
['rounded-lg', '12px'],
['rounded-xl', '16px'],
['rounded-2xl', '20px'],
['rounded-full', '9999px'],
];
const SHADOWS = [
['shadow-xs', 'Input, tugma'],
['shadow-sm', 'Karta'],
['shadow-md', 'Dropdown'],
['shadow-lg', 'Popover, toast'],
['shadow-xl', 'Modal, drawer'],
];
export default function ThemingRadiusShadow() {
return (
<div className="flex w-full flex-col gap-8">
<div className="grid grid-cols-3 gap-4 sm:grid-cols-6">
{RADII.map(([className, value]) => (
<div key={className} className="flex flex-col items-center gap-2 text-xs">
<div className={`size-14 border-2 border-primary bg-primary-soft ${className}`} />
<code className="font-mono text-text">{className}</code>
<span className="text-text-muted">{value}</span>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-5 sm:grid-cols-5">
{SHADOWS.map(([className, note]) => (
<div
key={className}
className={`flex h-20 flex-col items-center justify-center rounded-xl bg-surface-raised text-xs ${className}`}
>
<code className="font-mono text-text">{className}</code>
<span className="text-text-muted">{note}</span>
</div>
))}
</div>
</div>
);
}
Animatsiya
Animatsiya — bezak emas, ma'lumot: element qayerdan kelib, qayerga ketganini ko'rsatadi. Kirish
sekinroq va ease-out, chiqish tezroq va ease-in.
ease-outKirishlar (asosiy)ease-inChiqishlarease-in-outJoyida o'zgarishease-springToast, switch'use client';
import { Button } from '@akhatmirzo/ui';
import { useState } from 'react';
const EASINGS = [
['ease-out', 'Kirishlar (asosiy)'],
['ease-in', 'Chiqishlar'],
['ease-in-out', "Joyida o'zgarish"],
['ease-spring', 'Toast, switch'],
];
export default function ThemingMotion() {
const [on, setOn] = useState(false);
return (
<div className="flex w-full max-w-lg flex-col gap-4">
<Button size="sm" variant="primary" className="self-start" onClick={() => setOn((v) => !v)}>
{on ? 'Qaytarish' : 'Ishga tushirish'}
</Button>
{EASINGS.map(([easing, note]) => (
<div key={easing} className="flex items-center gap-3">
<div className="flex w-28 shrink-0 flex-col text-xs">
<code className="font-mono text-text">{easing}</code>
<span className="text-text-muted">{note}</span>
</div>
<div className="relative h-6 flex-1 rounded-full bg-surface-muted">
{/* Qatlam kengligi = yo'lak − sharcha, shuning uchun translate-x-full oxirigacha olib boradi */}
<div
className={`absolute inset-y-0 left-0 w-[calc(100%-1.5rem)] transition-transform duration-slow ${easing} ${on ? 'translate-x-full' : ''}`}
>
<div className="size-6 rounded-full bg-primary" />
</div>
</div>
</div>
))}
</div>
);
}
| Davomiylik | Qiymat | Qayerda |
|---|---|---|
duration-fast | 100ms | Hover, bosish, dropdown yopilishi |
duration-base | 150ms | Dropdown ochilishi, tooltip |
duration-moderate | 200ms | Modal ochilishi, accordion, tabs |
duration-slow | 300ms | Drawer, toast kirishi |
Easing: ease-out (asosiy), ease-in, ease-in-out, ease-spring (kamdan-kam — toast,
switch). Overlay'lar uchun tayyor keyframe'lar ham bor: animate-dialog-in/out,
animate-popover-in/out, animate-tooltip-in/out, animate-slide-in-right (va left,
top, bottom), animate-toast-in/out, animate-accordion-down/up, shuningdek
animate-spinner va animate-shimmer.
prefers-reduced-motion
Foydalanuvchi tizimda "kam animatsiya"ni yoqqan bo'lsa, popover siljishi o'chadi. O'z
animatsiyalaringizda motion-reduce: variantidan foydalaning.
Qatlamlar (z-index)
Tailwind'ga ulanmagan — CSS o'zgaruvchisi orqali ishlating: z-(--ak-z-modal).
| O'zgaruvchi | Qiymat | Kim |
|---|---|---|
--ak-z-dropdown | 1000 | Dropdown, Select, Combobox, Popover |
--ak-z-overlay | 1100 | Dialog, Drawer va Command Palette foni |
--ak-z-modal | 1110 | Dialog, Drawer va Command Palette paneli |
--ak-z-toast | 1200 | Toast |
--ak-z-tooltip | 1300 | Tooltip |
Brend rangini almashtirish
Brend rangi --ak-primary* guruhida. Ularni theme.css dan keyin qayta belgilang —
komponentlarga tegish shart emas. Quyida misol: brend rangi blok ichida almashadi.
'use client';
import { Badge, Button, Field, Input, Switch } from '@akhatmirzo/ui';
import { useState, type CSSProperties } from 'react';
const BRANDS = ['indigo', 'emerald', 'sky', 'rose'] as const;
// Brend rangini faqat shu blok ichida almashtiramiz. color-mix — light va dark'da birdek ishlaydi.
const brandVars = (c: string) =>
({
'--ak-primary': `var(--ak-${c}-600)`,
'--ak-primary-hover': `var(--ak-${c}-700)`,
'--ak-primary-soft': `color-mix(in srgb, var(--ak-${c}-500) 14%, var(--ak-surface))`,
'--ak-primary-soft-hover': `color-mix(in srgb, var(--ak-${c}-500) 22%, var(--ak-surface))`,
'--ak-primary-soft-foreground': `color-mix(in srgb, var(--ak-${c}-600) 65%, var(--ak-text))`,
'--ak-ring': `color-mix(in srgb, var(--ak-${c}-500) 45%, transparent)`,
}) as CSSProperties;
export default function ThemingBrand() {
const [brand, setBrand] = useState<(typeof BRANDS)[number]>('emerald');
return (
<div className="flex w-full max-w-md flex-col gap-4">
<div className="flex flex-wrap gap-2">
{BRANDS.map((b) => (
<Button
key={b}
size="sm"
variant={b === brand ? 'outline' : 'ghost'}
onClick={() => setBrand(b)}
>
{b}
</Button>
))}
</div>
<div
style={brandVars(brand)}
className="flex flex-col gap-4 rounded-xl border border-border bg-surface p-5"
>
<Field label="Telefon">
<Input placeholder="+998 90 123 45 67" />
</Field>
<Switch defaultChecked label="Yangiliklarga obuna" />
<div className="flex flex-wrap items-center gap-2">
<Button variant="primary">Saqlash</Button>
<Button>Bekor qilish</Button>
<Badge color="primary">Yangi</Badge>
</div>
</div>
</div>
);
}
Ilova bo'ylab almashtirish uchun light va dark qiymatlarni alohida yozing. Bazaviy shkaladagi istalgan rangni yoki o'z HEX qiymatlaringizni berishingiz mumkin:
@import 'tailwindcss';
@import '@akhatmirzo/tokens/theme.css';
:root,
[data-theme='light'] {
--ak-primary: var(--ak-emerald-600);
--ak-primary-hover: var(--ak-emerald-700);
--ak-primary-soft: var(--ak-emerald-50);
--ak-primary-soft-hover: var(--ak-emerald-100);
--ak-primary-soft-foreground: var(--ak-emerald-700);
--ak-ring: rgb(16 185 129 / 0.4);
--ak-link: var(--ak-emerald-700);
}
[data-theme='dark'] {
--ak-primary: var(--ak-emerald-600);
--ak-primary-hover: var(--ak-emerald-500);
--ak-primary-soft: var(--ak-emerald-950);
--ak-primary-soft-hover: var(--ak-emerald-950);
--ak-primary-soft-foreground: var(--ak-emerald-300);
--ak-ring: rgb(52 211 153 / 0.5);
--ak-link: var(--ak-emerald-300);
}Dark blokni ham yozing
<html> dagi :root qiymati dark rejimga ham o'tadi: selektorlar kuchi bir xil, sizning
faylingiz esa keyin keladi. Lekin rejimga qarab farq qiladigan tokenlar (soft, ring, link)
va ichki data-theme="dark" bloklari uchun [data-theme='dark'] blokini to'liq yozing.
Primary ustidagi matn oq (--ak-primary-foreground). Och rang tanlasangiz (masalan amber),
kontrastni tekshiring va kerak bo'lsa --ak-primary-foreground ni ham o'zgartiring.
Tailwind utilitalari
Ranglar, radius va soyalardan tashqari theme.css bir nechta tayyor utilita qo'shadi:
| Utilita | Nima qiladi |
|---|---|
focus-ring | Faqat klaviatura fokusida (:focus-visible) 2px halqa |
focus-ring-within | Input konteyneri uchun: ichida fokus bo'lsa chegara va halqa; xatoda — danger |
duration-fast … slow | Davomiylik tokenlari (transition-* bilan) |
ease-out, ease-spring ... | Easing tokenlari |
dark: | data-theme="dark" ichida ishlaydi (class="dark" emas) |
<button className="focus-ring rounded-lg bg-primary-soft px-4 py-2 text-sm font-medium text-primary-soft-foreground transition-colors duration-base hover:bg-primary-soft-hover">
Filtr
</button>Tailwind ishlatilmaydigan joyda (o'z CSS'ingiz, inline style, grafik kutubxonasi) ranglar, soyalar, davomiylik, easing va qatlamlar CSS o'zgaruvchilari orqali mavjud:
.chart-tooltip {
background: var(--ak-surface-raised);
color: var(--ak-text);
box-shadow: var(--ak-shadow-lg);
transition: opacity var(--ak-duration-base) var(--ak-ease-out);
z-index: var(--ak-z-tooltip);
}