تم و توکنها
همهٔ رنگها، شعاع گوشهها و فونت دیگ با متغیرهای CSS تعریف شدهاند. برای ساخت تم اختصاصی سازمان خودتان کافی است همین متغیرها را بازتعریف کنید؛ هیچ کامپوننتی نیاز به تغییر ندارد.
توکنهای رنگی
نمونههای زیر مقدار زندهٔ تم فعلی (روشن یا تیره) را نشان میدهند:
سطحها
--background--foreground--card--popover--muted--secondary--accentبرند
--primary--brand-secondary--primary-subtle--ringوضعیت
--destructive--success--warning--infoخط و ورودی
--border--inputساخت تم اختصاصی
هر توکن یک نسخهٔ روشن در :root و یک نسخهٔ تیره در .dark دارد. مثلاً برای تغییر رنگ برند به آبی سازمانی:
:root {
--primary: #1d4ed8;
--ring: #1d4ed8;
}
.dark {
--primary: #60a5fa;
--ring: #60a5fa;
}فایل کامل globals.css
این همان فایلی است که سایت جاری از آن استفاده میکند، همیشه بهروز:
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
/* ==================================================================== *
* رنگهای دیگ در سه لایه تعریف میشوند. هر لایه فقط به لایهٔ بالاتر از
* خودش نگاه میکند و هیچوقت از آن رد نمیشود:
*
* ۱) پالت پایه --dig-* مقدار خام، بدون معنی و بدون برند
* ۲) برند --brand-* نام مستعارِ یکی از رمپهای پایه
* ۳) معنایی --primary, ... نقشِ رنگ در رابط کاربری
*
* سود این چیدمان برای مولتیبرند: یک برند تازه فقط لایهٔ ۲ را بازتعریف
* میکند (۲۲ سطر)، نه ۵۰ توکن معنایی را. تصمیمهای دسترسیپذیری، اینکه
* کدام Shade متن سفید میگیرد و کدام متن تیره، در لایهٔ ۳ میمانند و مالِ
* دیزاینسیستماند، نه مالِ برند. پس هیچ برندی نمیتواند ناخواسته
* کنتراست را خراب کند.
* ==================================================================== */
/* -------------------------------------------------------------------- *
* لایهٔ ۱، پالت پایه
*
* مقادیر دقیقاً همانهایی هستند که Tailwind v4 در theme.css میدهد و
* عمداً اینجا پین شدهاند (نه var(--color-*)) تا سه چیز تضمین شود:
* فایل خروجیِ رجیستری خودبسنده بماند، ارتقای Tailwind رنگها را زیر پا
* عوض نکند، و tree-shaking متغیرهای بلااستفادهٔ Tailwind به ما آسیب نزند.
* -------------------------------------------------------------------- */
:root {
--dig-white: oklch(100% 0 0);
--dig-black: oklch(0% 0 0);
/* خاکستری پایه */
--dig-slate-50: oklch(98.4% 0.003 247.858);
--dig-slate-100: oklch(96.8% 0.007 247.896);
--dig-slate-200: oklch(92.9% 0.013 255.508);
--dig-slate-300: oklch(86.9% 0.022 252.894);
--dig-slate-400: oklch(70.4% 0.04 256.788);
--dig-slate-500: oklch(55.4% 0.046 257.417);
--dig-slate-600: oklch(44.6% 0.043 257.281);
--dig-slate-700: oklch(37.2% 0.044 257.287);
--dig-slate-800: oklch(27.9% 0.041 260.031);
--dig-slate-900: oklch(20.8% 0.042 265.755);
--dig-slate-950: oklch(12.9% 0.042 264.695);
--dig-indigo-50: oklch(96.2% 0.018 272.314);
--dig-indigo-100: oklch(93% 0.034 272.788);
--dig-indigo-200: oklch(87% 0.065 274.039);
--dig-indigo-300: oklch(78.5% 0.115 274.713);
--dig-indigo-400: oklch(67.3% 0.182 276.935);
--dig-indigo-500: oklch(58.5% 0.233 277.117);
--dig-indigo-600: oklch(51.1% 0.262 276.966);
--dig-indigo-700: oklch(45.7% 0.24 277.023);
--dig-indigo-800: oklch(39.8% 0.195 277.366);
--dig-indigo-900: oklch(35.9% 0.144 278.697);
--dig-indigo-950: oklch(25.7% 0.09 281.288);
--dig-cyan-50: oklch(98.4% 0.019 200.873);
--dig-cyan-100: oklch(95.6% 0.045 203.388);
--dig-cyan-200: oklch(91.7% 0.08 205.041);
--dig-cyan-300: oklch(86.5% 0.127 207.078);
--dig-cyan-400: oklch(78.9% 0.154 211.53);
--dig-cyan-500: oklch(71.5% 0.143 215.221);
--dig-cyan-600: oklch(60.9% 0.126 221.723);
--dig-cyan-700: oklch(52% 0.105 223.128);
--dig-cyan-800: oklch(45% 0.085 224.283);
--dig-cyan-900: oklch(39.8% 0.07 227.392);
--dig-cyan-950: oklch(30.2% 0.056 229.695);
--dig-violet-50: oklch(96.9% 0.016 293.756);
--dig-violet-100: oklch(94.3% 0.029 294.588);
--dig-violet-200: oklch(89.4% 0.057 293.283);
--dig-violet-300: oklch(81.1% 0.111 293.571);
--dig-violet-400: oklch(70.2% 0.183 293.541);
--dig-violet-500: oklch(60.6% 0.25 292.717);
--dig-violet-600: oklch(54.1% 0.281 293.009);
--dig-violet-700: oklch(49.1% 0.27 292.581);
--dig-violet-800: oklch(43.2% 0.232 292.759);
--dig-violet-900: oklch(38% 0.189 293.745);
--dig-violet-950: oklch(28.3% 0.141 291.089);
--dig-blue-50: oklch(97% 0.014 254.604);
--dig-blue-100: oklch(93.2% 0.032 255.585);
--dig-blue-200: oklch(88.2% 0.059 254.128);
--dig-blue-300: oklch(80.9% 0.105 251.813);
--dig-blue-400: oklch(70.7% 0.165 254.624);
--dig-blue-500: oklch(62.3% 0.214 259.815);
--dig-blue-600: oklch(54.6% 0.245 262.881);
--dig-blue-700: oklch(48.8% 0.243 264.376);
--dig-blue-800: oklch(42.4% 0.199 265.638);
--dig-blue-900: oklch(37.9% 0.146 265.522);
--dig-blue-950: oklch(28.2% 0.091 267.935);
--dig-green-50: oklch(97.6% 0.0173 159.13);
--dig-green-100: oklch(94.1% 0.0395 156.86);
--dig-green-200: oklch(88.7% 0.0731 158.64);
--dig-green-300: oklch(81.1% 0.1083 159.87);
--dig-green-400: oklch(72.0% 0.1352 158.80);
--dig-green-500: oklch(63.3% 0.1351 158.19);
--dig-green-600: oklch(54.1% 0.1182 158.28);
--dig-green-700: oklch(46.0% 0.0952 160.71);
--dig-green-800: oklch(39.5% 0.0779 161.78);
--dig-green-900: oklch(34.7% 0.0648 163.36);
--dig-green-950: oklch(24.2% 0.0416 165.51);
--dig-amber-50: oklch(98.7% 0.022 95.277);
--dig-amber-100: oklch(96.2% 0.059 95.617);
--dig-amber-200: oklch(92.4% 0.12 95.746);
--dig-amber-300: oklch(87.9% 0.169 91.605);
--dig-amber-400: oklch(82.8% 0.189 84.429);
--dig-amber-500: oklch(76.9% 0.188 70.08);
--dig-amber-600: oklch(66.6% 0.179 58.318);
--dig-amber-700: oklch(55.5% 0.163 48.998);
--dig-amber-800: oklch(47.3% 0.137 46.201);
--dig-amber-900: oklch(41.4% 0.112 45.904);
--dig-amber-950: oklch(27.9% 0.077 45.635);
--dig-orange-50: oklch(98% 0.016 73.684);
--dig-orange-100: oklch(95.4% 0.038 75.164);
--dig-orange-200: oklch(90.1% 0.076 70.697);
--dig-orange-300: oklch(83.7% 0.128 66.29);
--dig-orange-400: oklch(75% 0.183 55.934);
--dig-orange-500: oklch(70.5% 0.213 47.604);
--dig-orange-600: oklch(64.6% 0.222 41.116);
--dig-orange-700: oklch(55.3% 0.195 38.402);
--dig-orange-800: oklch(47% 0.157 37.304);
--dig-orange-900: oklch(40.8% 0.123 38.172);
--dig-orange-950: oklch(26.6% 0.079 36.259);
--dig-red-50: oklch(97.1% 0.013 17.38);
--dig-red-100: oklch(93.6% 0.032 17.717);
--dig-red-200: oklch(88.5% 0.062 18.334);
--dig-red-300: oklch(80.8% 0.114 19.571);
--dig-red-400: oklch(70.4% 0.191 22.216);
--dig-red-500: oklch(63.7% 0.237 25.331);
--dig-red-600: oklch(57.7% 0.245 27.325);
--dig-red-700: oklch(50.5% 0.213 27.518);
--dig-red-800: oklch(44.4% 0.177 26.899);
--dig-red-900: oklch(39.6% 0.141 25.723);
--dig-red-950: oklch(25.8% 0.092 26.042);
/* خاکستریِ خنثی، بدون هیچ رنگمایهای (chroma صفر). این همان رمپِ
neutral در Tailwind است و برخلاف --dig-slate-* که کمی به آبی میزند،
کاملاً بیطرف است. فقط برای Gray Dark به کار میرود؛ نگاه کنید
به لایهٔ آلیاسِ --neutral-* پایینتر. */
--dig-neutral-50: oklch(98.5% 0 0);
--dig-neutral-100: oklch(97% 0 0);
--dig-neutral-200: oklch(92.2% 0 0);
--dig-neutral-300: oklch(87% 0 0);
--dig-neutral-400: oklch(70.8% 0 0);
--dig-neutral-500: oklch(55.6% 0 0);
--dig-neutral-600: oklch(43.9% 0 0);
--dig-neutral-700: oklch(37.1% 0 0);
--dig-neutral-800: oklch(26.9% 0 0);
--dig-neutral-900: oklch(20.5% 0 0);
--dig-neutral-950: oklch(14.5% 0 0);
}
/* -------------------------------------------------------------------- *
* لایهٔ ۲، برند
*
* تنها لایهای که یک برند تازه بازنویسی میکند. برای ساخت برند، همین دو
* بلوک را با رمپ دیگری جایگزین کنید:
*
* :root { --brand-600: var(--dig-violet-600); ... }
*
* شرط تنها: رمپ باید یازده Shade داشته باشد (مثل Tailwind) و Shade ۶۰۰ آن با
* متن سفید حداقل ۴.۵ کنتراست بدهد. همهٔ رمپهای پایهٔ بالا این شرط را
* دارند (اندازهگیریشده).
* -------------------------------------------------------------------- */
:root {
--brand-50: var(--dig-indigo-50);
--brand-100: var(--dig-indigo-100);
--brand-200: var(--dig-indigo-200);
--brand-300: var(--dig-indigo-300);
--brand-400: var(--dig-indigo-400);
--brand-500: var(--dig-indigo-500);
--brand-600: var(--dig-indigo-600);
--brand-700: var(--dig-indigo-700);
--brand-800: var(--dig-indigo-800);
--brand-900: var(--dig-indigo-900);
--brand-950: var(--dig-indigo-950);
--brand-secondary-50: var(--dig-cyan-50);
--brand-secondary-100: var(--dig-cyan-100);
--brand-secondary-200: var(--dig-cyan-200);
--brand-secondary-300: var(--dig-cyan-300);
--brand-secondary-400: var(--dig-cyan-400);
--brand-secondary-500: var(--dig-cyan-500);
--brand-secondary-600: var(--dig-cyan-600);
--brand-secondary-700: var(--dig-cyan-700);
--brand-secondary-800: var(--dig-cyan-800);
--brand-secondary-900: var(--dig-cyan-900);
--brand-secondary-950: var(--dig-cyan-950);
}
/* -------------------------------------------------------------------- *
* لایهٔ ۲، خاکستریِ خنثی (قابل انتخاب توسط دولوپر)
*
* درست مثل --brand-*، این هم فقط نام مستعارِ یک رمپ پایه است، نه توکن
* معنایی. تم تیره پیشفرض (Blue Dark) از --dig-slate-* میآید. برای
* Gray Dark کافیست همین ۱۱ سطر در .dark[data-dark-theme="gray"]
* بازنویسی شود؛ هیچ توکن معنایی یا کامپوننتی دست نمیخورد.
* -------------------------------------------------------------------- */
:root {
--neutral-50: var(--dig-slate-50);
--neutral-100: var(--dig-slate-100);
--neutral-200: var(--dig-slate-200);
--neutral-300: var(--dig-slate-300);
--neutral-400: var(--dig-slate-400);
--neutral-500: var(--dig-slate-500);
--neutral-600: var(--dig-slate-600);
--neutral-700: var(--dig-slate-700);
--neutral-800: var(--dig-slate-800);
--neutral-900: var(--dig-slate-900);
--neutral-950: var(--dig-slate-950);
}
/* -------------------------------------------------------------------- *
* لایهٔ ۳، توکنهای معنایی (تم روشن)
*
* نامها قراردادِ پایدار دیگاند تا هر کامپوننت یا بلوکی که اضافه
* میشود بدون تغییر کار کند. دو نکتهٔ مهم در همین قرارداد:
*
* • --accent رنگ برند نیست؛ سطحِ hover خنثی است. رنگیکردنش باعث میشود
* هر هاور در برنامه رنگ برند بگیرد.
* • --secondary هم خاکستری است (واریانت secondary دکمه از آن میخورد).
* برای همین رنگ برند دوم توکن مستقل خودش را دارد: --brand-secondary
*
* هر جفت زمینه/متن اینجا اندازهگیری شده و WCAG AA را رد میکند؛ ضعیفترین
* جفت در تم روشن destructive با ۴.۷۶ است.
* -------------------------------------------------------------------- */
:root {
--radius: calc(0.5rem * var(--font-scale));
--background: var(--dig-white);
--foreground: var(--dig-slate-900);
--card: var(--dig-white);
--card-foreground: var(--dig-slate-900);
--popover: var(--dig-white);
--popover-foreground: var(--dig-slate-900);
--primary: var(--brand-600);
--primary-foreground: var(--dig-white);
--secondary: var(--dig-slate-100);
--secondary-foreground: var(--dig-slate-900);
--muted: var(--dig-slate-100);
--muted-foreground: var(--dig-slate-600);
--accent: var(--dig-slate-100);
--accent-foreground: var(--dig-slate-900);
--brand-secondary: var(--brand-secondary-700);
--brand-secondary-foreground: var(--dig-white);
/* رنگهای وضعیت. سبز و کهربایی در آن Shade که متن سفید میپذیرد مات و
گِلی میشوند؛ بهجایش Shade زندهٔ ۵۰۰ با متن تیره مینشیند و همانجا
کنتراست از ۴.۵ بیشتر است (۶.۷۱ و ۶.۹۹). قرمز برعکس است و در ۶۰۰ با
متن سفید کار میکند. */
--destructive: var(--dig-red-600);
--destructive-foreground: var(--dig-white);
--success: var(--dig-green-500);
--success-foreground: var(--dig-green-950);
--warning: var(--dig-amber-500);
--warning-foreground: var(--dig-amber-950);
--info: var(--dig-blue-600);
--info-foreground: var(--dig-white);
/* نسخهٔ کمرنگ هر وضعیت: زمینهٔ ملایم برای Alert و Badge، و متنی که
روی آن، و روی زمینهٔ صفحه، خوانا بماند. */
--primary-subtle: var(--brand-50);
--primary-subtle-foreground: var(--brand-700);
--destructive-subtle: var(--dig-red-50);
--destructive-subtle-foreground: var(--dig-red-700);
--success-subtle: var(--dig-green-50);
--success-subtle-foreground: var(--dig-green-700);
--warning-subtle: var(--dig-amber-50);
--warning-subtle-foreground: var(--dig-amber-700);
--info-subtle: var(--dig-blue-50);
--info-subtle-foreground: var(--dig-blue-700);
--border: var(--dig-slate-200);
--input: var(--dig-slate-200);
--ring: var(--brand-600);
/* نمودارها: رنگمایهها عمداً دور از هم انتخاب شدهاند تا سریها در کنار
هم تفکیکپذیر بمانند. سری اول از برند میآید، پس با تعویض برند
نمودارها هم همراه میشوند. */
--chart-1: var(--brand-500);
--chart-2: var(--dig-cyan-600);
--chart-3: var(--dig-green-500);
--chart-4: var(--dig-amber-500);
--chart-5: var(--dig-red-500);
--sidebar: var(--dig-slate-50);
--sidebar-foreground: var(--dig-slate-900);
--sidebar-primary: var(--brand-600);
--sidebar-primary-foreground: var(--dig-white);
--sidebar-accent: var(--dig-slate-100);
--sidebar-accent-foreground: var(--dig-slate-900);
--sidebar-border: var(--dig-slate-200);
--sidebar-ring: var(--brand-600);
/* ------------------------------------------------------------------ *
* سایه، چهار پلهٔ معنایی بر اساس ارتفاع، نه اندازه
*
* مثل فاصلهگذاری، سایه هم یک عدد نیست؛ چهار نقش است و هر کدام معنای
* خودش را دارد. کسی که popover را با shadow-lg تیره میکند فردا مودال
* را هم با همان کلاس تیره میکند، چون هر دو «اتفاقاً بزرگ» بودند. اسم
* بردن جلوی این را میگیرد.
*
* control حالت استراحتِ دکمه، اینپوت، آیتم لیست — تقریباً نامحسوس
* card کارت و تایلی که کمی از پسزمینه بلند شده
* panel popover، dropdown-menu، select، date-picker، tooltip
* overlay dialog، sheet، drawer، alert-dialog — بالاترین ارتفاع
*
* هر پله دو سایه دارد (نزدیک + دور)، همان رسم Material و shadcn، تا
* لبه هم تیز بماند هم پخش. تیرگی رنگ تیم تیره را جدا تعریف کردهایم؛
* سیاهِ کمرنگِ تم روشن روی پسزمینهٔ نزدیکبهسیاه دیده نمیشود.
* ------------------------------------------------------------------ */
--shadow-control: 0 1px 2px 0 rgb(0 0 0 / 0.06);
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.08), 0 1px 2px -1px rgb(0 0 0 / 0.08);
--shadow-panel: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-overlay: 0 10px 15px -3px rgb(0 0 0 / 0.12), 0 4px 6px -4px rgb(0 0 0 / 0.12);
}
/* -------------------------------------------------------------------- *
* لایهٔ ۳، توکنهای معنایی (تم تیره)
*
* قاعدهٔ وارونه: در تم تیره رنگهای توپُر روشنتر میشوند (Shade ۴۰۰) و متن
* روی آنها تیره میشود (Shade ۹۵۰ همان رمپ). ضعیفترین جفت اینجا
* primary با ۵.۱۴ است.
* -------------------------------------------------------------------- */
.dark {
--background: var(--neutral-950);
--foreground: var(--neutral-100);
--card: var(--neutral-900);
--card-foreground: var(--neutral-100);
--popover: var(--neutral-900);
--popover-foreground: var(--neutral-100);
--primary: var(--brand-400);
--primary-foreground: var(--brand-950);
--secondary: var(--neutral-800);
--secondary-foreground: var(--neutral-100);
--muted: var(--neutral-800);
--muted-foreground: var(--neutral-400);
--accent: var(--neutral-800);
--accent-foreground: var(--neutral-100);
--brand-secondary: var(--brand-secondary-400);
--brand-secondary-foreground: var(--brand-secondary-950);
--destructive: var(--dig-red-400);
--destructive-foreground: var(--dig-red-950);
--success: var(--dig-green-400);
--success-foreground: var(--dig-green-950);
--warning: var(--dig-amber-400);
--warning-foreground: var(--dig-amber-950);
--info: var(--dig-blue-400);
--info-foreground: var(--dig-blue-950);
/* Shade ۹۵۰ روی پسزمینهٔ صفحه (که خودش هم نزدیک همان تیرگی است) تقریباً
محو میشود؛ Shade ۹۰۰ همان فاصلهٔ کنتراستِ زمینه/متنِ روشن را حفظ
میکند ولی بهعنوان سطحِ رنگی از پسزمینه جدا دیده میشود. */
--primary-subtle: var(--brand-900);
--primary-subtle-foreground: var(--brand-200);
--destructive-subtle: var(--dig-red-900);
--destructive-subtle-foreground: var(--dig-red-200);
--success-subtle: var(--dig-green-900);
--success-subtle-foreground: var(--dig-green-200);
--warning-subtle: var(--dig-amber-900);
--warning-subtle-foreground: var(--dig-amber-200);
--info-subtle: var(--dig-blue-900);
--info-subtle-foreground: var(--dig-blue-200);
--border: var(--neutral-800);
--input: var(--neutral-800);
--ring: var(--brand-500);
--chart-1: var(--brand-400);
--chart-2: var(--dig-cyan-400);
--chart-3: var(--dig-green-400);
--chart-4: var(--dig-amber-400);
--chart-5: var(--dig-red-400);
--sidebar: var(--neutral-900);
--sidebar-foreground: var(--neutral-100);
--sidebar-primary: var(--brand-400);
--sidebar-primary-foreground: var(--brand-950);
--sidebar-accent: var(--neutral-800);
--sidebar-accent-foreground: var(--neutral-100);
--sidebar-border: var(--neutral-800);
--sidebar-ring: var(--brand-500);
/* سیاهِ کمرنگِ تم روشن روی زمینهٔ slate-950 عملاً محو است، پس تیرگی
بالا رفته تا لبهٔ سطح از پسزمینه جدا بماند. */
--shadow-control: 0 1px 2px 0 rgb(0 0 0 / 0.24);
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.32), 0 1px 2px -1px rgb(0 0 0 / 0.32);
--shadow-panel: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4);
--shadow-overlay: 0 10px 15px -3px rgb(0 0 0 / 0.48), 0 4px 6px -4px rgb(0 0 0 / 0.48);
}
/* -------------------------------------------------------------------- *
* پرچمِ انتخابیِ تم تیره، Blue Dark در برابر Gray Dark
*
* .dark پیشفرض (Blue Dark) از --dig-slate-* میآید که کمی به سمت آبی
* سرمهای میزند. دولوپری که خاکستریِ خنثیتر میخواهد، همان عنصری که
* next-themes کلاس dark را رویش میگذارد (معمولاً <html>) را با
* data-dark-theme="gray" علامت میزند:
*
* <html class="dark" data-dark-theme="gray">
*
* فقط لایهٔ آلیاسِ --neutral-* عوض میشود؛ چون هر توکن معنایی در .dark
* (پسزمینه، کارت، border و...) از همین آلیاس میخواند، هیچجای دیگری
* دستکاری نمیشود. کنتراستها همزاد و بودِ رمپ slateاند (اندازهگیری
* شده: هر دو رمپ روی همین Shadeها با هم کمتر از ۰.۳ اختلاف نسبت کنتراست
* دارند).
* -------------------------------------------------------------------- */
.dark[data-dark-theme="gray"] {
--neutral-50: var(--dig-neutral-50);
--neutral-100: var(--dig-neutral-100);
--neutral-200: var(--dig-neutral-200);
--neutral-300: var(--dig-neutral-300);
--neutral-400: var(--dig-neutral-400);
--neutral-500: var(--dig-neutral-500);
--neutral-600: var(--dig-neutral-600);
--neutral-700: var(--dig-neutral-700);
--neutral-800: var(--dig-neutral-800);
--neutral-900: var(--dig-neutral-900);
--neutral-950: var(--dig-neutral-950);
}
/* -------------------------------------------------------------------- *
* ترازِ نمایش، سه لایه بهجای طیف پیوسته
*
* برخلاف سایه که چهار پله دارد، اینجا فقط سه لایهایم و عمداً «popover»
* و «modal» عدد جدا نمیگیرند. دلیلش یک باگ واقعی است: اگر Select عددِ
* کوچکتری از Dialog داشته باشد، وقتی Select از داخل یک Dialog باز شود
* دقیقاً پشتِ همان Dialog قایم میشود، چون z-index یک استکینگکانتکست
* جدید میسازد و دیگر ترتیب DOM تصمیم نمیگیرد. راهحلِ درست این نیست
* که عدد Select را دستی بالاتر ببریم؛ راهحل این است که هر چیزی که
* میتواند از دلِ چیز دیگری باز شود (popover، dropdown، dialog، sheet،
* tooltip) در یک لایهٔ مشترک بماند و تساویِ z-index را ترتیبِ DOM (یعنی
* کدام portal دیرتر مونت شده) حل کند. توست تنها لایهای است که باید
* همیشه بالای همه بماند، حتی بالای یک Dialog باز، پس تنها آن یک عدد
* جداگانه و بزرگتر دارد.
*
* sticky هدر چسبانِ جدول، نوار ابزار چسبان — بالای محتوا، زیر همهچیزِ شناور
* overlay هر چیز شناور: backdrop، dialog، sheet، drawer، popover،
* dropdown-menu، select، combobox، context-menu، tooltip
* toast بالاترین لایه؛ باید حتی روی یک modal باز هم دیده شود
* -------------------------------------------------------------------- */
:root {
--z-sticky: 10;
--z-overlay: 50;
--z-toast: 100;
}
/* -------------------------------------------------------------------- *
* حاشیهٔ ظریف، نیمپیکسل
*
* مقیاسِ حاشیهٔ Tailwind (۰، پیشفرض ۱px، ۲) برای سطح و ورودی کافی است،
* ولی روی عنصرهای ریز و متراکم (Chip، Tag) همان ۱px هم سنگین دیده
* میشود؛ لبه از خودِ رنگِ پرشدگی بلندتر میزند. ۰.۵px روی نمایشگرهای
* ≥2x (رتینا و بیشترِ موبایلها) واقعاً نیمپیکسل رندر میشود و لبهای
* هموزنِ محتوا میدهد؛ روی نمایشگرهای ۱x مرورگر به نزدیکترین پیکسل
* گرد میکند و همان ۱px قبلی را میبینید، نه شکستگی یا حذف حاشیه.
* -------------------------------------------------------------------- */
:root {
--border-width-fine: 0.5px;
}
/* -------------------------------------------------------------------- *
* فونت، همان چیدمان سهلایهٔ رنگ، این بار برای تایپوگرافی
*
* لایهٔ ۱ (--font-anchor) انتخابِ پروژه است، لایهٔ ۲ (--font-sans) نقشِ
* فونت در رابط کاربری. پروژهای که میخواهد ایرانسنس یا هر فونت دیگری
* بگذارد، فقط لنگر را عوض میکند، نه توکن معنایی را، نه fallbackها را:
*
* const iranSans = localFont({ variable: "--font-iransans", src: [...] })
* :root { --font-anchor: var(--font-iransans), "IRANSansX"; }
*
* قبلاً نام فونتِ خودِ دیگ داخل توکن معنایی پخته شده بود، پس هر پروژه
* مجبور بود کل خط را بازنویسی کند و fallbackها را دستی تکرار کند.
* -------------------------------------------------------------------- */
:root {
/* لنگر فونت، تنها چیزی که یک پروژه برای تعویض فونت عوض میکند. */
--font-anchor: var(--font-iransans), "IRANSansX";
--font-anchor-mono: ui-monospace;
/* زنجیرهٔ جایگزین، مشترک بین همهٔ پروژهها. */
--font-fallback: ui-sans-serif, system-ui, sans-serif;
/* ------------------------------------------------------------------ *
* ضریب اندازه، تنها پیچِ تنظیمِ فاصلهٔ دید
*
* موبایل و دسکتاپ هر دو از فاصلهٔ بازو خوانده میشوند، پس ضریبشان ۱
* است و تفاوتشان را خودِ clamp با عرض viewport میگیرد. تلویزیون فرق
* دارد: از سه متری دیده میشود و همان ۱۳px عملاً ناخواناست، پس کلِ
* مقیاس بزرگ میشود نه فقط تیترها.
*
* پروژهها میتوانند دستی هم عوضش کنند، مثلاً کیوسکِ ایستاده یا
* حالتِ «متن بزرگ» برای کاربر کمبینا: :root { --font-scale: 1.25 }
* ------------------------------------------------------------------ */
--font-scale: 1;
}
/* تلویزیون و کیوسک، عمداً با عرض صفحه فعال نمیشود.
*
* وسوسهکننده است که بگوییم «عرض ۱۹۲۰ یعنی تلویزیون»، ولی ۱۹۲۰ رایجترین
* مانیتور دسکتاپ است. با آن قاعده، کاربرِ دسکتاپِ پهن متنِ اندازهٔ تلویزیون
* میگرفت. عرض چیزی دربارهٔ فاصلهٔ دید نمیگوید.
*
* پس این یک پیچِ صریح است: اپلیکیشنِ تلویزیون یا کیوسک، یا حالتِ «متن بزرگ»
* برای کاربر کمبینا، خودش آن را میچرخاند:
*
* :root { --font-scale: 1.35 } → تلویزیون، فاصلهٔ سه متری
* :root { --font-scale: 1.25 } → کیوسک ایستاده یا متن بزرگ
*/
/* کاربری که در سیستمعاملش «کاهش حرکت» را زده معمولاً تنظیمات دسترسیپذیری
دیگری هم دارد؛ اینجا کاری نمیکنیم، فقط یادآوری که --font-scale نقطهٔ
اتصالِ آمادهٔ چنین تنظیماتی است. */
@theme inline {
--font-sans: var(--font-anchor), var(--font-fallback);
--font-mono: var(--font-anchor-mono), monospace;
/* ------------------------------------------------------------------ *
* مقیاس تایپوگرافی، فارسیمحور
*
* مقیاس پیشفرض Tailwind برای متن لاتین تنظیم شده. حروف فارسی در همان
* اندازهٔ اسمی، نوری کوچکتر دیده میشوند و کشیدگیها فاصلهٔ سطر بیشتری
* میخواهند. دو تفاوت با Tailwind:
*
* --text-2xs تازه است (۱۱px)، ریزنویسیِ جدول و متادیتا
* --text-sm از ۱۴px به ۱۳px آمده، بدنهٔ واقعیِ رابطهای فارسی
*
* بقیهٔ پلهها دستنخوردهاند. هدف این است که پروژهها به
* text-[13px] و text-[11.5px] پناه نبرند.
* ------------------------------------------------------------------ */
--text-2xs: calc(0.6875rem * var(--font-scale));
--text-2xs--line-height: calc(1 / 0.6875);
--text-xs: calc(0.75rem * var(--font-scale));
--text-xs--line-height: calc(1.125 / 0.75);
--text-sm: calc(0.8125rem * var(--font-scale));
--text-sm--line-height: calc(1.375 / 0.8125);
/* ------------------------------------------------------------------ *
* نقشها، چیزی که در کد مینویسید
*
* پلههای بالا خاماند؛ اینها میگویند «این متن چه کاره است». بهجای
* اینکه هر توسعهدهنده حدس بزند لیبل ۱۲ است یا ۱۳، مینویسد
* text-label. اگر فردا مقیاس عوض شود، همهجا با هم عوض میشود.
*
* هر نقش با clamp بین موبایل و دسکتاپ سیّال است و در ضریبِ فاصلهٔ دید
* ضرب میشود. عددهای کوچکِ رابط (لیبل و ریزنویسی) عمداً سیّال نیستند:
* در جدولِ فشرده، بزرگشدنشان با عرض صفحه چیدمان را بههم میریزد.
* ------------------------------------------------------------------ */
--text-caption: calc(0.6875rem * var(--font-scale));
--text-caption--line-height: 1.6;
--text-label: calc(0.75rem * var(--font-scale));
--text-label--line-height: 1.5;
--text-description: calc(clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem) * var(--font-scale));
--text-description--line-height: 1.75;
--text-body: calc(clamp(0.8125rem, 0.79rem + 0.12vw, 0.875rem) * var(--font-scale));
--text-body--line-height: 1.75;
--text-subheading: calc(clamp(0.9375rem, 0.9rem + 0.2vw, 1rem) * var(--font-scale));
--text-subheading--line-height: 1.5;
--text-heading: calc(clamp(1rem, 0.94rem + 0.3vw, 1.125rem) * var(--font-scale));
--text-heading--line-height: 1.45;
--text-title: calc(clamp(1.25rem, 1.14rem + 0.55vw, 1.5rem) * var(--font-scale));
--text-title--line-height: 1.35;
--text-display: calc(clamp(1.75rem, 1.4rem + 1.75vw, 2.5rem) * var(--font-scale));
--text-display--line-height: 1.2;
/* تیترها در فارسی با فاصلهٔ حروفِ کمی جمعتر بهتر مینشینند. */
--tracking-heading: -0.01em;
/* ------------------------------------------------------------------ *
* فاصلهگذاری
*
* واحد پایهٔ Tailwind است؛ همهٔ p-* و m-* و gap-* و w-* از همین ضرب
* میشوند. چون در ضریبِ فاصلهٔ دید ضرب شده، روی تلویزیون کل چیدمان با
* متن هماهنگ بزرگ میشود، نه اینکه متن بزرگ شود و پدینگها همان
* بمانند و همهچیز خفه شود.
* ------------------------------------------------------------------ */
--spacing: calc(0.25rem * var(--font-scale));
/* ------------------------------------------------------------------ *
* نقشهای فاصله، سه محور
*
* فاصله در رابط کاربری یک چیز نیست، سه چیز است و هر کدام منطق خودش را
* دارد. نامبردنشان جدا از هم، جلوی این را میگیرد که کسی پدینگِ کارت را
* برای فاصلهٔ دو دکمه به کار ببرد چون «هر دو ۱۲ بودند».
*
* inset: پدینگِ درونِ یک کانتینر
* stack: فاصلهٔ عمودیِ چیزهای روی هم
* inline: فاصلهٔ افقیِ چیزهای کنار هم
* ------------------------------------------------------------------ */
/* قاعدهٔ اعداد: پایه ۴ است، ریتم ۸.
*
* هر مقدارِ ۱۶ به بالا مضرب ۸ است. زیر ۱۶ نیمپلههای ۴ و ۱۲ هم مجازند،
* چون در رابطهای فشرده (جدول بانکی، فرم چندستونی) پرش ۸ به ۱۶ درشت
* است و «آیکون تا متن» با «دکمه تا دکمه» یکی میشود. پایهٔ ۴ همان چیزی
* است که Tailwind و Material و HIG هم دارند؛ شبکهٔ ۸ ریتمِ روی آن است،
* نه جایگزینش.
*/
/* inset: از کوچکترین کانتینر تا لبهٔ صفحه */
--spacing-control: calc(0.5rem * var(--font-scale));
--spacing-field: calc(0.75rem * var(--font-scale));
--spacing-card: calc(1.5rem * var(--font-scale));
--spacing-panel: calc(2rem * var(--font-scale));
--spacing-gutter: calc(clamp(1rem, 0.5rem + 2vw, 2rem) * var(--font-scale));
/* stack، عمودی */
--spacing-stack-tight: calc(0.25rem * var(--font-scale));
--spacing-stack: calc(0.5rem * var(--font-scale));
--spacing-stack-loose: calc(1rem * var(--font-scale));
--spacing-section: calc(1.5rem * var(--font-scale));
/* inline، افقی */
--spacing-inline-tight: calc(0.25rem * var(--font-scale));
--spacing-inline: calc(0.5rem * var(--font-scale));
--spacing-inline-loose: calc(0.75rem * var(--font-scale));
/* ------------------------------------------------------------------ *
* هدف لمسی
*
* روی موبایل، اندازهٔ دیداریِ یک آیکون ربطی به ناحیهٔ قابللمسش ندارد.
* آیکونِ ۱۶ پیکسلی میتواند داخل دکمهای بنشیند که ۴۴ پیکسل است و باید
* باشد. دو عدد لازم است، نه یکی: اندازهٔ هدف، و فاصلهٔ بین دو هدف.
*
* WCAG 2.2 §2.5.8 ۲۴×۲۴ کمینهٔ AA
* WCAG 2.2 §2.5.5 ۴۴×۴۴ سطح AAA
* Apple HIG ۴۴×۴۴
* Material ۴۸×۴۸ با کمینه ۸ فاصله
*
* بدون فاصله، دو دکمهٔ ۴۴ پیکسلیِ چسبیده هم لمس اشتباه میدهند، چون
* انگشت مرکز دقیق را نمیزند.
* ------------------------------------------------------------------ */
--size-touch: 2.75rem;
--size-touch-lg: 3rem;
--spacing-touch: 0.5rem;
/* ------------------------------------------------------------------ *
* عرضهای نامدار
*
* اینها را هر پروژه دستی اختراع میکند (max-w-[620px] و max-w-[1240px])
* و بعد در دو صفحه دو عدد متفاوت میشود.
* ------------------------------------------------------------------ */
--container-prose: 65ch;
--container-form: 38.75rem;
--container-page: 77.5rem;
/* گردی گوشهها هم با مقیاس بزرگ میشود وگرنه روی تلویزیون تیز به نظر
میرسد؛ پلههای مشتق از --radius خودشان دنبالش میآیند. */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-brand-secondary: var(--brand-secondary);
--color-brand-secondary-foreground: var(--brand-secondary-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-primary-subtle: var(--primary-subtle);
--color-primary-subtle-foreground: var(--primary-subtle-foreground);
--color-destructive-subtle: var(--destructive-subtle);
--color-destructive-subtle-foreground: var(--destructive-subtle-foreground);
--color-success-subtle: var(--success-subtle);
--color-success-subtle-foreground: var(--success-subtle-foreground);
--color-warning-subtle: var(--warning-subtle);
--color-warning-subtle-foreground: var(--warning-subtle-foreground);
--color-info-subtle: var(--info-subtle);
--color-info-subtle-foreground: var(--info-subtle-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* رمپ برند بهصورت یوتیلیتی هم در دسترس است: bg-brand-500، text-brand-700 */
--color-brand-50: var(--brand-50);
--color-brand-100: var(--brand-100);
--color-brand-200: var(--brand-200);
--color-brand-300: var(--brand-300);
--color-brand-400: var(--brand-400);
--color-brand-500: var(--brand-500);
--color-brand-600: var(--brand-600);
--color-brand-700: var(--brand-700);
--color-brand-800: var(--brand-800);
--color-brand-900: var(--brand-900);
--color-brand-950: var(--brand-950);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* چهار پلهٔ سایه بهعنوان یوتیلیتی: shadow-control، shadow-card،
shadow-panel، shadow-overlay */
--shadow-control: var(--shadow-control);
--shadow-card: var(--shadow-card);
--shadow-panel: var(--shadow-panel);
--shadow-overlay: var(--shadow-overlay);
/* سه لایهٔ نمایش بهعنوان یوتیلیتی: z-sticky، z-overlay، z-toast */
--z-sticky: var(--z-sticky);
--z-overlay: var(--z-overlay);
--z-toast: var(--z-toast);
--animate-skeleton-shimmer: skeleton-shimmer 1.8s ease-in-out infinite;
@keyframes skeleton-shimmer {
100% {
transform: translateX(100%);
}
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "ss01" 1, "ss02" 1;
}
}
/* ------------------------------------------------------------------ *
* سوییچرهای سراسری: فریمورک و پکیجمنیجر
*
* انتخاب کاربر روی <html data-framework> و <html data-pm> مینشیند و
* همینجا با CSS اعمال میشود، نه با ریاکت. برای همین تعویض بدون رندر
* دوباره و حتی پیش از هیدریشن هم درست کار میکند.
* ------------------------------------------------------------------ */
@layer components {
/* فقط نسخهٔ فعال جا میگیرد؛ بقیه از جریان صفحه بیروناند تا زیر
حالتهای کوتاه فضای خالی نماند. نرمشدنِ تغییر ارتفاع کار
AutoHeight است، نه CSS. */
[data-fw-pane],
[data-pm-pane] {
display: none;
}
html[data-framework="react"] [data-fw-pane="react"],
html[data-framework="vue"] [data-fw-pane="vue"],
html[data-framework="svelte"] [data-fw-pane="svelte"],
html[data-framework="angular"] [data-fw-pane="angular"],
html[data-pm="pnpm"] [data-pm-pane="pnpm"],
html[data-pm="npm"] [data-pm-pane="npm"],
html[data-pm="yarn"] [data-pm-pane="yarn"],
html[data-pm="bun"] [data-pm-pane="bun"] {
display: block;
}
html[data-framework="react"] .fw-trigger[data-fw-trigger="react"],
html[data-framework="vue"] .fw-trigger[data-fw-trigger="vue"],
html[data-framework="svelte"] .fw-trigger[data-fw-trigger="svelte"],
html[data-framework="angular"] .fw-trigger[data-fw-trigger="angular"],
html[data-pm="pnpm"] .pm-trigger[data-pm-trigger="pnpm"],
html[data-pm="npm"] .pm-trigger[data-pm-trigger="npm"],
html[data-pm="yarn"] .pm-trigger[data-pm-trigger="yarn"],
html[data-pm="bun"] .pm-trigger[data-pm-trigger="bun"] {
background-color: var(--background);
color: var(--foreground);
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.06);
}
/* گزینهٔ فعالِ پکیجمنیجر داخل نوار بالای بلوک کد کادر میگیرد.
کادرِ شفاف روی همه، تا فعالشدن ارتفاع را جابهجا نکند. */
.pm-trigger {
border: 1px solid transparent;
font-family: var(--font-mono, ui-monospace, monospace);
}
html[data-pm="pnpm"] .pm-trigger[data-pm-trigger="pnpm"],
html[data-pm="npm"] .pm-trigger[data-pm-trigger="npm"],
html[data-pm="yarn"] .pm-trigger[data-pm-trigger="yarn"],
html[data-pm="bun"] .pm-trigger[data-pm-trigger="bun"] {
border: 1px solid var(--border);
}
}
/* ------------------------------------------------------------------ *
* اسکلت: شیمر تکی و شیمر هماهنگِ گروه
*
* هر Skeleton با animation="shimmer" یک باند نور از روی خودش رد میکند.
* وقتی چند تا از اینها داخل یک SkeletonGroup باشند، :has() آن را
* تشخیص میدهد، شیمرِ تکتکِ فرزندان (::after زیرشان) را خاموش میکند و
* یک باند نور واحد از روی کل گروه رد میشود، نه یک شیمر جدا روی هر خانه.
* ------------------------------------------------------------------ */
@layer components {
[data-slot="skeleton"][data-animation="shimmer"]::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
animation: var(--animate-skeleton-shimmer);
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in oklab, var(--color-foreground) 12%, transparent) 50%,
transparent 100%
);
}
[data-slot="skeleton-group"]:has([data-slot="skeleton"][data-animation="shimmer"])::after {
content: "";
position: absolute;
inset: 0;
z-index: 10;
pointer-events: none;
transform: translateX(-100%);
mix-blend-mode: overlay;
animation: var(--animate-skeleton-shimmer);
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in oklab, white 55%, transparent) 50%,
transparent 100%
);
}
[data-slot="skeleton-group"] [data-slot="skeleton"][data-animation="shimmer"]::after {
content: none;
}
}