راهنمای طراحی
قواعدی که صفحهای ساختهشده از کامپوننتهای ranui باید رعایت کند تا یک سامانه خوانده شود، نه تلی از قطعه.
موضوع این صفحه قضاوت است: سراغ کدام توکن بروید و پیش از انتشار چه چیزی را بررسی کنید. خودِ فهرست توکنها سیستم طراحی است و تعویض و بازنویسی در زمان اجرا پوستهبندی. نسخه کامل و ماشیناجباریِ این قواعد در مخزن است، در packages/ranui/docs/DESIGN.md.
کجا به کار میآید: وقتی صفحهای را میچینید یا از عناصر
<r-*>کامپوننتی در سطح برنامه میسازید و باید درباره یک رنگ، یک فاصله، یک اندازه متن، یک سایه یا یک مدت حرکت تصمیم بگیرید. پاسخ کوتاه همیشه یکی است: نقش را انتخاب کنید و مقدار را به توکن بسپارید.
اصول
- روشنی پیش از شخصیت. کار اصلی و کنش اصلی باید پیش از هر ملاحظه دیگری بیابهام باشند.
- ترکیب کنید، از نو نسازید. پیش از ساختن یک عنصر پایه از
div، سراغr-button،r-input،r-selectوr-modalبروید: این کامپوننتها رفتار فوکوس و صفحهکلید و ARIA را از پیش دارند، رفتاری که وگرنه باید دوباره استخراجش کنید. - توکن، نه مقدار خام. یک کد شانزدهشانزدهی، یک فاصله
20pxیا سایهای که با دست انتخاب شده، تصمیمی است که از پوسته پیروی نخواهد کرد. - با نقش و وضعیت تصمیم بگیرید، نه با چشم. «این متن چیست؟» (عنوان / برچسب / بدنه / دکمه) پاسخ دارد؛ «چه اندازهای خوب به نظر میرسد؟» ندارد.
- هر وضعیت دستیافتنی را طراحی کنید. پیشفرض، هاور، فعال، فوکوس، غیرفعال، در حال بارگذاری، خالی، خطا: وضعیت پیشفرض یکی از هشت تاست.
- آنچه رسم شده را وارسی کنید. در روشن و تیره، باریک و پهن، با ماوس و با انگشت. بازبینی، سایهای را که دیده نمیشود نمیگیرد.
ترتیب اولویت وقتی دو قاعده به دو سو میکشند: هدف کاربر ← شواهد وارسیشده ← همین راهنما ← الگوهای منتشرشده ← قواعد سرانگشتی عمومی.
انتخاب رنگ
رنگ بر پایه نقش و وضعیت تخصیص مییابد، نه با چشم انتخاب میشود. نردبان از پیش تعیین کرده هاور و فعال چه شکلیاند؛ کار شما نامگذاری نقش است.
| این عنصر… | این را به کار ببرید |
|---|---|
| پسزمینه صفحه یا یک سطح | --ran-color-bg / -bg-subtle / -bg-elevated / -bg-muted |
| زیر نشانگر / در حال فشرده شدن | --ran-color-bg-hover / -bg-active |
| متن | --ran-color-text / -text-secondary / -text-disabled |
| یک کادر | --ran-color-border / -hover / -active |
| همان کنشی که صفحه برایش هست | --ran-color-primary (و روی آن --ran-color-primary-text) |
| یک وضعیت | --ran-color-success / -warning / -danger |
| یک پیوند | --ran-color-link |
هر رنگ تأکیدی تنها یک معنا دارد. primary تکرنگ است (در روشن سیاه روی سفید، در تیره سفید روی سیاه)، پس برایش از آبی استفاده نکنید: آبی از آنِ پیوندها و حلقه فوکوس است. سبز موفقیت است، کهربایی هشدار و قرمز خطر؛ اگر قرمز را برای تأکید مصرف کنید، بعداً راهی برای نشان دادن خطر نمیماند.
سه قاعده که جلوی خرابیِ بیصدا را میگیرد:
- هرگز برای مقداری که باید از پوسته پیروی کند، یک کد شانزدهشانزدهی یا
rgb()را دستی ننویسید. - مقدار جایگزین باید توکنی را نام ببرد که با پوسته میچرخد:
var(--ran-color-text, var(--ran-gray-1000))، نهvar(--ran-color-text, #171717)؛ مقدار ثابتی که فقط برای حالت روشن است در تیره ناپدید میشود. - مقدار جایگزین باید توکنی را نام ببرد که وجود دارد.
var()روی ویژگی اعلامنشده به هیچ حل نمیشود، کل اعلان دور ریخته میشود و عنصر همان چیزی را نگه میدارد که به ارث برده — که معمولاً تقریباً درست به نظر میرسد. (--ran-color-errorوجود ندارد؛ نامش--ran-color-dangerاست.)
فاصله و ریتم
هر فاصله را از مقیاس نهمقداری بردارید و بگذارید خودِ فاصله معنا برساند:
- ۸ پیکسل میان عناصر درون یک گروه.
- ۱۶ پیکسل میان گروهها.
- ۳۲ تا ۴۰ پیکسل میان بخشها.
20px یا 28px اختراع نکنید. ریتم صفحه را همان مجموعه محدود میسازد و یک فاصله بیرون از مقیاس آن را میشکند. ستون فقرات مشترک میان نواحی را حفظ کنید (لبهها، خطهای مبنا و ستونهایی که همراستا میشوند) و همترازی را با پیکسلهای رسمشده بسنجید نه با چشم.
انتخاب تایپوگرافی
بپرسید متن چه نقشی دارد (عنوان، برچسب، بدنه، دکمه، تکعرض) و آنگاه قلم و اندازه و وزن و ارتفاع خط، همه از مقیاس تایپوگرافی درمیآیند. برای هر مورد جداگانه پیکسل خام انتخاب نکنید.
نقش یک ابزار است نه یک قانون: متن تزئینیِ واقعاً یکباره (لایهای که با یک حرکت میدرخشد، کمی وزن بیشتر برای پیوند فعال) با یک توکن کامپوننتیِ خودش بهتر درمیآید تا اینکه به نزدیکترین نقش تحمیل شود.
عمق: سایه و چیدمان لایهها
رده سایه را بر پایه اینکه عنصر چیست انتخاب کنید (سطحی در جریان صفحه، لایه شناور، یا دیالوگی که راه را میبندد) و مطمئن شوید واقعاً دیده میشود. سایهای که دیده نمیشود هیچ نشانهای از عمق نمیدهد، و لایه شناوری که به رده کارت سقوط کند انگار به صفحه سنجاق شده است.
جاسازی لایههای ranui در قاب خودتان. نردبان z-index دقیقاً از ۱۰۰۰ آغاز میشود تا از قاب معمول صفحه بالاتر برود. پس لایهای که پرتال شده به کمک شما نیاز ندارد. اما لایهای با position: fixed که درون Shadow DOM خودش میماند (دیالوگ r-modal) تنها تا نزدیکترین زمینه چیدمانِ نیای خود بیرون میزند؛ پس اگر محتوای جاسازیشده را در چیزی بپیچید که چنین زمینهای میسازد (isolation، opacity < 1، transform، filter، will-change)، باید سطح چیدمان آن پوشش را بالا ببرید تا دیالوگ دوباره رویش بنشیند. این بالا بردن را به زمانی محدود کنید که لایهای واقعاً باز است:
.embed {
isolation: isolate; /* ارزان: z-index مخصوص خودش ندارد، پس چیزی ارتقا نمییابد */
}
/* فقط تا وقتی لایهای واقعی باز است بالا ببرید — هرگز «محض احتیاط» */
.embed:has(r-modal[open]),
.embed:has(r-modal[closing]) {
position: relative;
z-index: 100;
}یک z-index فراگیر روی پوشش، همه چیزِ درونش را (حتی محتوای کاملاً ثابت را) در تمام طول اسکرول بالای سربرگ چسبانتان میبرد. همین اشکال یک بار در همین سایت منتشر شد. علاوه بر open با closing هم تطبیق دهید: ماسک پس از برداشتهشدن open به اندازه گذارش همچنان رسم میشود.
حرکت
هرچه تغییر بزرگتر، زمان بیشتر؛ زیر آن آستانه، اصلاً انیمیشن ندهید. بازخورد هاور و فعال حدود ۱۵۰ میلیثانیه، منوها حدود ۲۰۰، دیالوگها حدود ۳۰۰، و تغییری که از پیش آشکار است صفر. به prefers-reduced-motion احترام بگذارید.
هرگز نگذارید ویژگیهای پالت گذار کنند. CSS نمیداند رنگ چرا عوض شده، پس یک transition روی background-color، color، border-color، box-shadow، fill یا stroke هنگام چرخش پوسته هم شلیک میکند و هر عنصر با سرعت خودش محو میشود، در حالی که بقیه صفحه پیشاپیش عوض شده است. بهجایش ویژگیهای حرکتی را متحرک کنید (transform، opacity، هندسه). transition: all و کوتهنوشتهای لخت مثل transition: 0.2s یعنی همهچیز، از جمله ویژگیهای پالت؛ هر دو در استایلهای خودِ ranui ممنوعاند و در استایلهای شما هم فکر بدی هستند.
وضعیتها و متنها
هر وضعیت دستیافتنی بخشی از طراحی است: هاور، فعال، فوکوس، غیرفعال، در حال بارگذاری، خالی، خطا. آنها را روی نردبان بنشانید: هاور ← bg-hover / border-hover؛ فعال ← bg-active؛ غیرفعال ← text-disabled بهعلاوه کاهش کدری؛ فوکوس ← حلقه فوکوس.
هیچ چیزِ غیرتعاملی نباید تعاملی به نظر برسد. r-card تنها با اتریبیوت hoverable به هاور واکنش میدهد؛ برای کارتهایی که کلیک نمیشوند آن را نگذارید.
متن هم بخشی از سامانه است:
- دکمهها یک کنش و یک مفعول میگیرند. ✅ «حذف عضو» ❌ «حذف»، «تأیید».
- خطاها میگویند چه شد و سپس چگونه درستش کنیم. ✅ «ساخت شکست خورد: باندل از سقف حجم گذشته است. کوچکش کنید یا سقف را بالا ببرید.» ❌ «عملیات ناموفق بود، دوباره تلاش کنید.»
- تأییدها و اعلانها تغییر را میگویند، نه موفقیت را. ✅ «پروژه حذف شد» ❌ «با موفقیت حذف شد» (ظاهر شدن اعلان خودش موفقیت را گفته است).
- بگذارید بافت، زیادیها را حذف کند: دیالوگی با عنوان «حذف پروژه» به دکمهای با نوشته «حذف کامل و همیشگی پروژه» نیاز ندارد.
دسترسپذیری
- کنتراست متن نسبت به پسزمینهاش را در حد WCAG AA برسانید.
- هرگز وضعیت را تنها با رنگ اعلام نکنید: آن را با آیکن، برچسب یا متن همراه کنید.
- هر عنصر تعاملی یک حلقه فوکوس دیدنی نگه میدارد (
--ran-focus-ring، یاoutline: 2px solid var(--ran-color-primary); outline-offset: 2px). برای مرتبتر شدن ظاهر حذفش نکنید. - همهچیز با صفحهکلید دستیافتنی است. هیچچیز فقط با ماوس نیست.
- به
prefers-reduced-motionوprefers-color-schemeاحترام بگذارید.
ماوس و لمس، باریک و پهن
نه شیوه اشاره و نه اندازه قاب دید، هیچکدام هدف درجه دو نیستند.
- کشیدن، لغزنده و حرکتهای اشارهای با Pointer Events کار میکنند (
pointerdown/pointermove/pointerup/pointercancel)، نه تنها باmouse*، بههمراهtouch-action: noneروی همان سطحی که کشیده میشود. CSSی کهtouch-action: noneرا بدون هندلر اشارهگر پشت آن اعلام کند، یک کنترل خراب است نه یک خط بیآزار. - نشانهای که فقط با هاور پیدا میشود، به جایگزینی برای لمس نیاز دارد.
trigger="hover"رویr-selectیاr-popoverدر دستگاههای لمسی به کلیک برمیگردد؛ هرچه میسازید هم باید همین کار را بکند. - بهجای اختراع نقطه شکست، اندازههای نسبت به قاب دید را ترجیح دهید (
%،min()،max()،clamp()،vw/vh، مثلاًmin(560px, calc(100vw - 32px))). ranui توکن مشترکی برای نقطه شکست ندارد، پس هر نقطه شکستِ سفتوسخت عددی یکباره است که کسی باید نگهش دارد. - هرگز در موبایل تنها راه انجام یک کار را پنهان نکنید. بهجای
display: noneچیدمان را از نو بریزید. - موقعیتی که اندازه گرفتهاید تنها تا بازچینش بعدی درست است. هرچه از
getBoundingClientRect()بیرون آمده، با تغییر اندازه، با بازچینش ظرف و (برای پنلی که پرتال شده) با اسکرول کهنه میشود. در همان رویدادها دوباره اندازه بگیرید، نه فقط در تعاملی که نخستین اندازهگیری را برانگیخت. باز کردن صفحه در عرض باریک، چیدمان اولیه را میآزماید؛ اما تغییر اندازه به آن را نمیآزماید، و این رده اشکال دقیقاً همانجا پیدا میشود.
آنچه کتابخانه ماشینی اجبار میکند
نه تا از این قواعد را pnpm -F ranui verify:design بررسی میکند و CI آن را روی سورس خودِ ranui اجرا میکند: مقدارهای جایگزین رنگ که در تیره ناامناند، مقادیر رنگ خام، مقیاس فاصله، مقیاس اندازه، حلقههای کشیدنِ فقطماوسی، قواعد display روی :host که hidden را میشکنند، مقدارهای جایگزینی که توکن اعلامنشده را نام میبرند، کامپوننتهایی که درخت shadow خودشان را query میکنند، و درختهای shadowی که بیرون از سازنده ساخته شدهاند. تخلفهای شناختهشده در یک فایل مبنا قفل شدهاند، پس نه میشود تخلف تازهای افزود و نه اصلاحی را بیصدا پس گرفت.
این دروازه کتابخانه را میپوشاند نه برنامه شما را، اما شکلهای شکستی که میگیرد (مقدار جایگزینی که توکن ناموجود را نام میبرد؛ رنگی که فقط در حالت روشن کار میکند) دقیقاً همانهایی هستند که در بازبینی سالم به نظر میرسند. پس ارزشش را دارد که همین قواعد را روی CSS خودتان هم اعمال کنید.
سیاهه پیش از انتشار رابط کاربری
- [ ] کار اصلی و کنش اصلی بیابهاماند.
- [ ] در روشن و تیره، و در عرض باریک و پهن کار میکند.
- [ ] با ماوس و لمس کار میکند؛ هر محرک هاور جایگزین لمسی دارد.
- [ ] همه وضعیتها آزموده شد: هاور، فعال، فوکوس، غیرفعال، در حال بارگذاری، خالی، خطا.
- [ ] صفحهکلید و فوکوس وارسی شد؛ فوکوس همهجا دیده میشود.
- [ ] حالتهای مرزی: متن بلند، عدد بزرگ، هر دو زبان.
- [ ] فاصلهها از مقیاس، تایپوگرافی بر پایه نقش، رنگ از توکنهای معنایی.
- [ ] هیچ ویژگی پالتی در
transitionنیست؛ هیچtransition: allنیست. - [ ] متن، مفعول را نام میبرد؛ هیچچیز وضعیت را تنها با رنگ اعلام نمیکند.