CSS چیست؟ آموزش کامل CSS از صفر تا Flexbox و Grid با پروژه هوش مصنوعی

در این آموزش CSS را از صفر تا سطح کاربردی یاد می‌گیرید؛ از Selector، Cascade و Box Model تا Flexbox، Grid، Responsive Design و CSS Variables. در پایان رابط حرفه‌ای یک دستیار هوش مصنوعی درواره را طراحی می‌کنیم.

Share
CSS چیست؟ آموزش کامل CSS از صفر تا Flexbox و Grid با پروژه هوش مصنوعی

CSS یکی از سه فناوری اصلی توسعه وب است. HTML ساختار صفحه را مشخص می‌کند، CSS ظاهر و چیدمان آن را می‌سازد و JavaScript رفتار تعاملی صفحه را کنترل می‌کند.

تقریباً هر چیزی که در ظاهر یک وب‌سایت می‌بینید، از رنگ و اندازه متن گرفته تا فاصله‌ها، ستون‌بندی، منو، کارت، دکمه، انیمیشن و نمایش صحیح در موبایل، با کمک CSS پیاده‌سازی می‌شود.

در این آموزش فقط چند ویژگی پراکنده CSS را معرفی نمی‌کنیم. هدف این است که منطق واقعی CSS را یاد بگیرید، بتوانید مشکلات ظاهری صفحات را تحلیل کنید و در پایان یک رابط فارسی، واکنش‌گرا و کاربردی برای دستیار هوش مصنوعی بسازید.

پروژه نهایی به‌گونه‌ای طراحی می‌شود که کلید API در مرورگر قرار نگیرد و درخواست‌ها از طریق یک Backend کوچک به API هوش مصنوعی درواره ارسال شوند.

CSS چیست؟

CSS مخفف عبارت Cascading Style Sheets و به معنای «برگه‌های استایل آبشاری» است.

CSS یک زبان برنامه‌نویسی عمومی مانند Python یا JavaScript نیست؛ بلکه زبان Style Sheet است. با استفاده از آن مشخص می‌کنیم عناصر HTML چگونه نمایش داده شوند.

برای مثال، HTML زیر یک دکمه ایجاد می‌کند:

<button>ارسال پیام</button>

بدون CSS، مرورگر دکمه را با ظاهر پیش‌فرض نمایش می‌دهد. با CSS می‌توان رنگ، اندازه، فونت، فاصله داخلی و گوشه‌های آن را تغییر داد:

button {
  padding: 12px 20px;
  color: #ffffff;
  background-color: #6d4aff;
  border: 0;
  border-radius: 12px;
  cursor: pointer;
}

براساس مستندات MDN درباره CSS، این زبان برای توصیف نحوه نمایش اسناد HTML یا XML روی صفحه، کاغذ و رسانه‌های دیگر استفاده می‌شود.

تفاوت HTML، CSS و JavaScript

این سه فناوری وظایف متفاوتی دارند:

فناوریمسئولیت اصلیمثال
HTMLساختار و معنای محتواعنوان، پاراگراف، فرم و دکمه
CSSظاهر و چیدمانرنگ، فونت، فاصله، Grid و Responsive Design
JavaScriptرفتار و تعاملارسال فرم، دریافت API و به‌روزرسانی صفحه

یک تشبیه ساده:

  • HTML اسکلت ساختمان است.
  • CSS طراحی داخلی، رنگ و چیدمان ساختمان است.
  • JavaScript تجهیزات و رفتارهای تعاملی ساختمان است.

CSS نمی‌تواند به‌تنهایی درخواست API ارسال کند یا پاسخ هوش مصنوعی را دریافت کند. برای این کار به JavaScript و معمولاً یک Backend نیاز داریم.

ساختار یک قانون CSS

یک قانون ساده CSS از Selector و یک یا چند Declaration تشکیل می‌شود:

.message {
  color: #1f2937;
  font-size: 1rem;
  line-height: 1.8;
}

در این مثال:

  • .message سلکتور یا Selector است.
  • color یک Property یا ویژگی است.
  • #1f2937 مقدار ویژگی است.
  • font-size: 1rem یک Declaration است.
  • مجموعه Declarationها داخل {} قرار می‌گیرد.

کامنت در CSS به شکل زیر نوشته می‌شود:

/* رنگ اصلی رابط */
:root {
  --color-primary: #6d4aff;
}

سه روش اضافه‌کردن CSS به HTML

استفاده از Inline CSS

در این روش استایل مستقیماً در ویژگی style عنصر نوشته می‌شود:

<button style="background: purple; color: white;">
  ارسال
</button>

Inline CSS برای پروژه‌های واقعی انتخاب مناسبی نیست؛ زیرا کد HTML را شلوغ می‌کند و استفاده مجدد از استایل‌ها دشوار می‌شود.

استفاده از تگ style

می‌توان CSS را داخل بخش head فایل HTML قرار داد:

<head>
  <style>
    button {
      background-color: purple;
      color: white;
    }
  </style>
</head>

این روش برای نمونه‌های کوچک و صفحات آزمایشی قابل‌قبول است.

استفاده از فایل خارجی

روش استاندارد پروژه‌های واقعی، قراردادن CSS در یک فایل جداگانه است:

<link rel="stylesheet" href="./styles.css">

ساختار پروژه می‌تواند چنین باشد:

ai-assistant/
├── index.html
├── styles.css
├── app.js
├── app.py
├── requirements.txt
└── .env

فایل CSS جداگانه مزایای مهمی دارد:

  • نگهداری کد آسان‌تر می‌شود.
  • مرورگر می‌تواند فایل را Cache کند.
  • چند صفحه می‌توانند از استایل مشترک استفاده کنند.
  • مسئولیت HTML، CSS و JavaScript از یکدیگر جدا می‌شود.
  • همکاری اعضای تیم ساده‌تر خواهد بود.

Selector در CSS چیست؟

Selector مشخص می‌کند استایل باید روی کدام عناصر اعمال شود.

انتخاب براساس نام تگ

p {
  color: #374151;
}

این قانون روی تمام تگ‌های p اعمال می‌شود.

انتخاب براساس Class

<p class="description">توضیحات سرویس</p>
.description {
  color: #6b7280;
}

Class با نقطه شروع می‌شود و می‌تواند روی چند عنصر استفاده شود.

انتخاب براساس ID

<main id="chat">
  ...
</main>
#chat {
  min-height: 500px;
}

ID باید در هر صفحه یکتا باشد. برای استایل‌دهی عمومی، معمولاً Class انعطاف بیشتری دارد.

Selector ویژگی

input[type="email"] {
  direction: ltr;
}

انتخاب عنصر داخل عنصر دیگر

.sidebar a {
  color: #475569;
}

این قانون تمام لینک‌های داخل .sidebar را انتخاب می‌کند.

انتخاب فرزند مستقیم

.message-list > article {
  margin-block-end: 16px;
}

علامت > فقط فرزندهای مستقیم را انتخاب می‌کند.

انتخاب چند Selector

h1,
h2,
h3 {
  line-height: 1.4;
}

Pseudo-class

Pseudo-class وضعیت خاص یک عنصر را انتخاب می‌کند:

button:hover {
  background-color: #5837e8;
}

button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

input:focus {
  border-color: #6d4aff;
}

Pseudo-element

Pseudo-element بخشی مجازی از عنصر ایجاد یا انتخاب می‌کند:

.title::before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-inline-end: 8px;
  background-color: #6d4aff;
  border-radius: 50%;
}

Cascade یا آبشار در CSS چیست؟

حرف C در CSS به Cascading اشاره می‌کند. ممکن است چند قانون مختلف بخواهند مقدار یک ویژگی را برای یک عنصر تعیین کنند. مرورگر باید تصمیم بگیرد کدام قانون برنده شود.

عوامل اصلی این تصمیم عبارت‌اند از:

  • منبع استایل
  • اهمیت قانون
  • لایه‌های Cascade
  • Specificity
  • ترتیب قرارگیری قانون
  • ارث‌بری

مثال:

p {
  color: blue;
}

.description {
  color: purple;
}

اگر یک پاراگراف دارای Class برابر با description باشد، رنگ بنفش اعمال می‌شود؛ زیرا Selector مربوط به Class اختصاصی‌تر است.

Specificity چیست؟

Specificity یا میزان اختصاصی‌بودن Selector مشخص می‌کند در رقابت میان قوانین، کدام Selector اولویت بیشتری دارد.

به‌طور ساده:

Inline Style
ID
Class، Attribute و Pseudo-class
Element و Pseudo-element

مثال:

p {
  color: blue;
}

.content p {
  color: green;
}

#main p {
  color: purple;
}

در این مثال، قانون #main p اختصاصی‌تر است.

استفاده بیش از حد از ID و !important نگهداری CSS را دشوار می‌کند. بهتر است ساختار Classها ساده و قابل‌پیش‌بینی باشد.

/* معمولاً از این الگو اجتناب کنید */
.message {
  color: red !important;
}

قبل از استفاده از !important بررسی کنید آیا مشکل با اصلاح Selector، ترتیب فایل یا معماری CSS حل می‌شود.

Inheritance یا ارث‌بری

بعضی ویژگی‌ها از والد به فرزند منتقل می‌شوند. برای مثال، color و font-family معمولاً ارث‌بری می‌شوند:

body {
  color: #111827;
  font-family: Tahoma, Arial, sans-serif;
}

متن بیشتر عناصر داخل body این تنظیمات را دریافت می‌کند.

ویژگی‌هایی مانند margin، padding و border معمولاً ارث‌بری نمی‌شوند.

Box Model در CSS

مرورگر هر عنصر را به‌صورت یک جعبه در نظر می‌گیرد. این جعبه از چهار بخش تشکیل می‌شود:

  • Content
  • Padding
  • Border
  • Margin

مثال:

.card {
  width: 300px;
  padding: 24px;
  border: 1px solid #e5e7eb;
  margin: 16px;
}

در حالت پیش‌فرض، مقدار width فقط عرض محتوا را مشخص می‌کند. Padding و Border به آن اضافه می‌شوند. این رفتار ممکن است محاسبه اندازه‌ها را دشوار کند.

به همین دلیل معمولاً در ابتدای CSS از Reset زیر استفاده می‌شود:

*,
*::before,
*::after {
  box-sizing: border-box;
}

با border-box، عرض و ارتفاع اعلام‌شده شامل Padding و Border نیز می‌شود.

برای آشنایی عمیق‌تر می‌توانید مستندات رسمی CSS Box Model در MDN را مطالعه کنید.

تفاوت Margin و Padding

padding فاصله محتوای عنصر تا Border آن است:

.card {
  padding: 24px;
}

margin فاصله عنصر با عناصر اطراف است:

.card {
  margin-block-end: 24px;
}

اگر می‌خواهید پس‌زمینه عنصر فضای بیشتری را پوشش دهد، معمولاً باید Padding را افزایش دهید. اگر فاصله میان دو عنصر مدنظر است، Margin یا gap انتخاب مناسب‌تری است.

واحدهای اندازه‌گیری در CSS

واحد px

.icon {
  width: 24px;
  height: 24px;
}

px برای Border، آیکون و اندازه‌های کوچک و دقیق مفید است.

واحد rem

h1 {
  font-size: 2rem;
}

rem براساس اندازه فونت عنصر ریشه محاسبه می‌شود و برای تایپوگرافی و فاصله‌گذاری مقیاس‌پذیر مناسب است.

واحد em

button {
  padding: 0.75em 1.25em;
}

em معمولاً نسبت به اندازه فونت عنصر محاسبه می‌شود. استفاده تو‌در‌توی آن می‌تواند محاسبه اندازه را پیچیده کند.

درصد

.container {
  width: 90%;
}

درصد معمولاً نسبت به اندازه عنصر والد محاسبه می‌شود.

واحدهای Viewport

.app {
  min-height: 100dvh;
}

واحدهای vw و vh نسبت به عرض و ارتفاع Viewport هستند. واحد dvh ارتفاع پویای Viewport را در مرورگرهای موبایل بهتر مدیریت می‌کند.

تابع clamp

تابع clamp() برای ایجاد اندازه‌های سیال مفید است:

.hero-title {
  font-size: clamp(1.75rem, 4vw, 3.25rem);
}

این مقدار:

  • کمتر از 1.75rem نمی‌شود.
  • متناسب با عرض صفحه رشد می‌کند.
  • بیشتر از 3.25rem نمی‌شود.

رنگ‌ها در CSS

CSS چند روش برای تعریف رنگ دارد:

.example {
  color: purple;
  border-color: #e5e7eb;
  background-color: rgb(109 74 255);
  box-shadow: 0 10px 30px rgb(15 23 42 / 10%);
}

برای پروژه‌های واقعی بهتر است رنگ‌های اصلی را به‌صورت CSS Variable تعریف کنید تا تغییر Theme آسان شود.

CSS Variables چیست؟

CSS Variables یا Custom Properties مقادیر قابل‌استفاده مجدد هستند.

:root {
  --color-primary: #6d4aff;
  --color-primary-hover: #5837e8;
  --color-text: #172033;
  --color-muted: #667085;
  --color-surface: #ffffff;
  --radius-md: 14px;
  --shadow-card: 0 16px 40px rgb(15 23 42 / 8%);
}

سپس می‌توان آن‌ها را با تابع var() استفاده کرد:

.button {
  color: #ffffff;
  background-color: var(--color-primary);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
}

برای مقدار جایگزین نیز می‌توان نوشت:

.element {
  color: var(--color-text, #111827);
}

CSS Variables برای موارد زیر بسیار مفید هستند:

  • مدیریت رنگ‌های برند
  • ساخت Dark Mode
  • یکسان‌سازی فاصله‌ها
  • تعریف اندازه Border Radius
  • ساخت Design Token
  • تغییر Theme بدون بازنویسی تمام فایل

ویژگی‌های منطقی CSS برای زبان فارسی

در رابط فارسی، بهتر است در بسیاری از موارد به‌جای left و right از Logical Properties استفاده کنیم.

مثال قدیمی:

.icon {
  margin-left: 8px;
}

نسخه منعطف‌تر:

.icon {
  margin-inline-start: 8px;
}

ویژگی‌های مهم منطقی عبارت‌اند از:

ویژگی منطقیمفهوم
margin-inline-startحاشیه ابتدای جهت نوشتار
margin-inline-endحاشیه انتهای جهت نوشتار
padding-inlinePadding افقی متناسب با جهت
padding-blockPadding عمودی
inset-inline-startموقعیت ابتدای محور افقی
border-inline-startBorder در ابتدای جهت نوشتار
inline-sizeاندازه در محور Inline
block-sizeاندازه در محور Block

این ویژگی‌ها باعث می‌شوند رابط در حالت RTL و LTR انعطاف بیشتری داشته باشد. مرجع کامل آن‌ها در مستندات CSS Logical Properties موجود است.

Display در CSS

ویژگی display مشخص می‌کند عنصر چگونه در جریان Layout قرار گیرد.

block

section {
  display: block;
}

عنصر Block معمولاً تمام عرض در دسترس را اشغال می‌کند و از خط جدید شروع می‌شود.

inline

strong {
  display: inline;
}

عنصر Inline در جریان متن قرار می‌گیرد و معمولاً Width و Height مستقیم روی آن مانند Block عمل نمی‌کند.

inline-block

.badge {
  display: inline-block;
}

این حالت رفتار Inline را با امکان تعیین اندازه ترکیب می‌کند.

none

.mobile-only {
  display: none;
}

عنصر از Layout حذف می‌شود. توجه کنید که display: none معمولاً آن را از دسترس فناوری‌های کمکی نیز خارج می‌کند.

flex و grid

این دو مقدار سیستم‌های مدرن چیدمان را فعال می‌کنند و در ادامه بررسی می‌شوند.

Flexbox چیست؟

Flexbox یک سیستم چیدمان یک‌بعدی است. یعنی برای مدیریت عناصر در یک ردیف یا یک ستون طراحی شده است.

نمونه ساده:

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

ویژگی‌های مهم Flexbox:

ویژگیکاربرد
flex-directionتعیین جهت ردیف یا ستون
justify-contentتراز روی محور اصلی
align-itemsتراز روی محور فرعی
gapفاصله میان آیتم‌ها
flex-wrapانتقال آیتم‌ها به خط بعد
flex-growمیزان رشد آیتم
flex-shrinkمیزان کوچک‌شدن آیتم
flex-basisاندازه اولیه آیتم
align-selfتراز مستقل یک آیتم

مثال یک فرم ارسال پیام:

.composer {
  display: flex;
  align-items: flex-end;
  gap: 12px;
}

.composer textarea {
  flex: 1;
  min-width: 0;
}

.composer button {
  flex: 0 0 auto;
}

عبارت flex: 1 باعث می‌شود Textarea فضای باقی‌مانده را بگیرد.

مستندات Flexbox در MDN محور اصلی، محور فرعی، Wrapping و تنظیم اندازه آیتم‌ها را با جزئیات توضیح می‌دهد.

CSS Grid چیست؟

CSS Grid یک سیستم چیدمان دوبعدی است و می‌تواند هم‌زمان ردیف‌ها و ستون‌ها را مدیریت کند.

مثال:

.feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

این کد سه ستون هم‌اندازه ایجاد می‌کند.

نسخه واکنش‌گرای بدون Media Query:

.feature-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(100%, 240px), 1fr)
  );
  gap: 20px;
}

در این حالت:

  • کارت‌ها حداقل عرض مناسبی دارند.
  • تعداد ستون‌ها براساس فضای موجود تغییر می‌کند.
  • در صفحه کوچک، کارت‌ها به یک ستون تبدیل می‌شوند.

برای صفحه اصلی اپلیکیشن نیز می‌توان نوشت:

.app-shell {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  min-height: 100dvh;
}

این Layout شامل یک Sidebar با عرض ۲۸۰ پیکسل و یک بخش اصلی منعطف است.

تفاوت کلی Flexbox و Grid:

ویژگیFlexboxGrid
نوع چیدمانیک‌بعدیدوبعدی
کاربرد اصلیردیف، ستون، Toolbar و فرمصفحه، داشبورد و مجموعه کارت‌ها
مدیریت ردیف و ستونیکی در هر لحظههم‌زمان
محل قرارگیری آیتم‌هابیشتر براساس محتوابیشتر براساس Layout

Flexbox و Grid رقیب یکدیگر نیستند. معمولاً Grid برای ساختار کلی و Flexbox برای اجزای داخلی استفاده می‌شود.

مرجع تکمیلی: CSS Grid در MDN

طراحی Responsive یا واکنش‌گرا

Responsive Design یعنی رابط در نمایشگرهای مختلف قابل‌استفاده باقی بماند.

تنها کوچک‌کردن اندازه عناصر کافی نیست. در موبایل ممکن است لازم باشد:

  • Sidebar مخفی یا به Drawer تبدیل شود.
  • چند ستون به یک ستون تبدیل شوند.
  • فاصله‌ها کاهش یابند.
  • دکمه‌ها فضای لمس کافی داشته باشند.
  • متن‌ها بدون خروج از صفحه شکسته شوند.
  • فرم ارسال پیام ساختار متفاوتی پیدا کند.

ابتدا Meta Viewport را در HTML قرار دهید:

<meta
  name="viewport"
  content="width=device-width, initial-scale=1"
>

سپس از Media Query استفاده کنید:

@media (max-width: 768px) {
  .app-shell {
    grid-template-columns: 1fr;
  }

  .sidebar {
    display: none;
  }

  .chat-panel {
    border-radius: 0;
  }
}

Media Query می‌تواند ویژگی‌هایی مانند عرض، ارتفاع، Orientation و ترجیحات دسترسی کاربر را بررسی کند. جزئیات بیشتر در راهنمای Media Query در MDN آمده است.

رویکرد Mobile First

در Mobile First ابتدا استایل صفحه کوچک نوشته می‌شود و سپس برای صفحه‌های بزرگ‌تر توسعه می‌یابد:

.cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

@media (min-width: 768px) {
  .cards {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1100px) {
  .cards {
    grid-template-columns: repeat(3, 1fr);
  }
}

این رویکرد در بسیاری از پروژه‌ها باعث می‌شود استایل پایه ساده‌تر باشد، اما الزام مطلق نیست. مهم این است که Breakpoint براساس نیاز واقعی Layout انتخاب شود، نه صرفاً مدل یک دستگاه خاص.

Position در CSS

ویژگی position نحوه موقعیت‌دهی عنصر را کنترل می‌کند.

static

حالت پیش‌فرض عناصر است.

relative

عنصر در جریان عادی باقی می‌ماند و می‌تواند مرجع عناصر Absolute باشد:

.input-wrapper {
  position: relative;
}

absolute

عنصر نسبت به نزدیک‌ترین والد دارای Position مناسب قرار می‌گیرد:

.input-action {
  position: absolute;
  inset-inline-end: 12px;
  inset-block-end: 12px;
}

fixed

عنصر نسبت به Viewport ثابت می‌ماند:

.support-button {
  position: fixed;
  inset-inline-end: 24px;
  inset-block-end: 24px;
}

sticky

عنصر تا رسیدن به موقعیت مشخص مانند عنصر عادی حرکت می‌کند و سپس ثابت می‌شود:

.chat-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 10;
}

position: sticky ممکن است به‌دلیل تنظیمات overflow والد عمل نکند. هنگام Debug، والدهای عنصر را بررسی کنید.

z-index و Stacking Context

z-index مشخص می‌کند عناصر روی‌هم‌افتاده با چه ترتیبی نمایش داده شوند:

.modal {
  position: fixed;
  z-index: 100;
}

اما z-index فقط یک عدد جهانی ساده نیست. بعضی ویژگی‌ها مانند transform، opacity و Position می‌توانند Stacking Context جدید ایجاد کنند.

استفاده از اعداد تصادفی بسیار بزرگ معمولاً مشکل معماری را پنهان می‌کند:

/* راه‌حل قابل نگهداری نیست */
.modal {
  z-index: 999999999;
}

بهتر است مقیاس مشخصی تعریف شود:

:root {
  --z-header: 10;
  --z-dropdown: 20;
  --z-modal: 30;
  --z-toast: 40;
}

Overflow و شکستن متن

پاسخ یک مدل هوش مصنوعی ممکن است شامل URL، کد یا متن طولانی باشد. برای جلوگیری از خروج محتوا از صفحه:

.message-content {
  overflow-wrap: anywhere;
  word-break: normal;
}

.message-content pre {
  max-width: 100%;
  overflow-x: auto;
}

برای ناحیه پیام‌ها:

.message-list {
  overflow-y: auto;
  overscroll-behavior: contain;
}

فونت و خوانایی متن فارسی

برای متن فارسی فقط انتخاب فونت کافی نیست. line-height، طول خط و فاصله پاراگراف‌ها نیز اهمیت دارند.

body {
  font-family:
    Vazirmatn,
    Tahoma,
    Arial,
    sans-serif;
  line-height: 1.8;
}

.article-text {
  max-width: 70ch;
}

برای رابط چت، خط‌های بسیار بلند خوانایی را کاهش می‌دهند:

.message {
  max-width: min(78%, 760px);
}

برای کد و Model ID بهتر است جهت متن مستقل تنظیم شود:

code,
pre,
.model-id {
  direction: ltr;
  text-align: left;
}

Focus و دسترس‌پذیری

حذف کامل Outline بدون جایگزین، استفاده صفحه با Keyboard را دشوار می‌کند:

/* مناسب نیست */
button:focus {
  outline: none;
}

نسخه بهتر:

button:focus-visible,
textarea:focus-visible,
a:focus-visible {
  outline: 3px solid rgb(109 74 255 / 28%);
  outline-offset: 3px;
}

همچنین:

  • متن باید Contrast کافی با پس‌زمینه داشته باشد.
  • اطلاعات فقط با رنگ منتقل نشوند.
  • دکمه باید متن یا Accessible Name مناسب داشته باشد.
  • حالت Disabled باید علاوه بر رنگ، از نظر رفتاری نیز غیرفعال باشد.
  • اندازه ناحیه قابل‌لمس در موبایل نباید بسیار کوچک باشد.

Transition و Animation

Transition تغییر حالت را نرم می‌کند:

.primary-button {
  transition:
    background-color 160ms ease,
    transform 160ms ease,
    box-shadow 160ms ease;
}

.primary-button:hover {
  transform: translateY(-1px);
}

انیمیشن تایپ ساده:

@keyframes pulse {
  0%,
  100% {
    opacity: 0.35;
  }

  50% {
    opacity: 1;
  }
}

.typing-dot {
  animation: pulse 1s infinite;
}

باید ترجیح کاربر برای کاهش حرکت را نیز رعایت کنیم:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

پروژه عملی: ساخت رابط دستیار هوش مصنوعی فارسی

در این پروژه رابطی می‌سازیم که ویژگی‌های زیر را دارد:

  • طراحی فارسی و RTL
  • Sidebar دسکتاپ
  • Header ثابت
  • فهرست پیام‌های قابل اسکرول
  • پیام متفاوت برای کاربر و دستیار
  • فرم ارسال پیام
  • وضعیت در حال دریافت پاسخ
  • طراحی Responsive
  • CSS Variables
  • Flexbox و Grid
  • Focus قابل‌مشاهده
  • رعایت prefers-reduced-motion
  • اتصال امن به Backend
  • استفاده از API درواره بدون افشای API Key

معماری پروژه:

مرورگر کاربر
    ↓
HTML + CSS + JavaScript
    ↓
Backend محلی با FastAPI
    ↓
API هوش مصنوعی درواره
    ↓
مدل انتخاب‌شده

کلید API فقط در Backend نگهداری می‌شود و هرگز وارد فایل JavaScript مرورگر نمی‌شود.

مرحله اول: ساخت پوشه پروژه

mkdir darvareh-css-assistant
cd darvareh-css-assistant

ساختار فایل‌ها:

darvareh-css-assistant/
├── static/
│   ├── index.html
│   ├── styles.css
│   └── app.js
├── app.py
├── requirements.txt
└── .env

مرحله دوم: ساخت HTML

فایل static/index.html:

<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="UTF-8">

  <meta
    name="viewport"
    content="width=device-width, initial-scale=1"
  >

  <meta
    name="description"
    content="رابط دستیار هوش مصنوعی فارسی با API درواره"
  >

  <title>دستیار هوش مصنوعی درواره</title>

  <link rel="stylesheet" href="/static/styles.css">
</head>

<body>
  <div class="app-shell">
    <aside class="sidebar" aria-label="منوی اصلی">
      <a
        class="brand"
        href="https://darvareh.ir"
        target="_blank"
        rel="noopener noreferrer"
      >
        <span class="brand__mark" aria-hidden="true">د</span>

        <span>
          <strong>درواره</strong>
          <small>دسترسی یکپارچه به مدل‌های هوش مصنوعی</small>
        </span>
      </a>

      <button class="new-chat-button" id="new-chat" type="button">
        <span aria-hidden="true">+</span>
        گفت‌وگوی جدید
      </button>

      <nav class="sidebar__nav">
        <p class="sidebar__label">نمونه پرسش‌ها</p>

        <button
          class="suggestion"
          type="button"
          data-prompt="یک برنامه هفتگی برای یادگیری CSS طراحی کن."
        >
          برنامه یادگیری CSS
        </button>

        <button
          class="suggestion"
          type="button"
          data-prompt="تفاوت Flexbox و CSS Grid را با مثال توضیح بده."
        >
          مقایسه Flexbox و Grid
        </button>

        <button
          class="suggestion"
          type="button"
          data-prompt="این رابط کاربری را از نظر دسترس‌پذیری بررسی کن."
        >
          بررسی دسترس‌پذیری
        </button>
      </nav>

      <div class="sidebar__footer">
        <a
          href="https://darvareh.ir/models"
          target="_blank"
          rel="noopener noreferrer"
        >
          مشاهده مدل‌ها و قیمت‌ها
        </a>
      </div>
    </aside>

    <main class="chat-panel">
      <header class="chat-header">
        <div>
          <p class="eyebrow">دستیار آنلاین</p>
          <h1>دستیار هوش مصنوعی</h1>
        </div>

        <div class="status" aria-label="وضعیت سرویس">
          <span class="status__dot" aria-hidden="true"></span>
          آماده
        </div>
      </header>

      <section
        class="message-list"
        id="message-list"
        aria-label="پیام‌های گفت‌وگو"
        aria-live="polite"
      >
        <article class="message message--assistant">
          <div class="message__avatar" aria-hidden="true">د</div>

          <div class="message__body">
            <p class="message__name">دستیار درواره</p>

            <div class="message__content">
              سلام! سؤال خود را بنویسید تا پاسخ را از مدل هوش مصنوعی
              دریافت کنم.
            </div>
          </div>
        </article>
      </section>

      <div class="composer-wrapper">
        <form class="composer" id="chat-form">
          <label class="sr-only" for="prompt">
            متن پیام
          </label>

          <textarea
            id="prompt"
            name="prompt"
            rows="1"
            maxlength="4000"
            placeholder="پیام خود را بنویسید..."
            required
          ></textarea>

          <button
            class="send-button"
            id="send-button"
            type="submit"
            aria-label="ارسال پیام"
          >
            <span class="send-button__text">ارسال</span>
            <span aria-hidden="true">←</span>
          </button>
        </form>

        <p class="composer-note">
          پاسخ‌های هوش مصنوعی ممکن است نیازمند بررسی باشند.
        </p>
      </div>
    </main>
  </div>

  <script src="/static/app.js" defer></script>
</body>
</html>

مرحله سوم: نوشتن CSS کامل پروژه

فایل static/styles.css:

:root {
  color-scheme: light;

  --color-primary: #6d4aff;
  --color-primary-dark: #5735e8;
  --color-primary-soft: #f0edff;

  --color-text: #172033;
  --color-muted: #667085;
  --color-border: #e6e8ef;
  --color-page: #f5f6fb;
  --color-surface: #ffffff;
  --color-sidebar: #17142a;
  --color-sidebar-text: #f7f5ff;

  --color-success: #12b76a;
  --color-user-message: #6d4aff;
  --color-assistant-message: #ffffff;

  --radius-sm: 10px;
  --radius-md: 16px;
  --radius-lg: 24px;

  --shadow-panel:
    0 24px 70px rgb(20 24 40 / 10%);

  --shadow-message:
    0 10px 30px rgb(20 24 40 / 6%);

  --sidebar-width: 280px;
  --content-max-width: 900px;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  min-height: 100%;
  background-color: var(--color-page);
}

body {
  min-width: 320px;
  min-height: 100dvh;
  margin: 0;

  color: var(--color-text);
  background:
    radial-gradient(
      circle at 85% 10%,
      rgb(109 74 255 / 10%),
      transparent 30%
    ),
    var(--color-page);

  font-family:
    Vazirmatn,
    Tahoma,
    Arial,
    sans-serif;

  line-height: 1.7;
}

button,
textarea,
input {
  font: inherit;
}

button,
a {
  -webkit-tap-highlight-color: transparent;
}

button {
  border: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

.app-shell {
  display: grid;
  grid-template-columns:
    var(--sidebar-width)
    minmax(0, 1fr);

  min-height: 100dvh;
}

.sidebar {
  position: sticky;
  inset-block-start: 0;

  display: flex;
  flex-direction: column;
  gap: 24px;

  height: 100dvh;
  padding: 24px 18px;

  color: var(--color-sidebar-text);
  background:
    linear-gradient(
      180deg,
      rgb(109 74 255 / 20%),
      transparent 35%
    ),
    var(--color-sidebar);

  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-inline: 8px;
}

.brand__mark {
  display: grid;
  place-items: center;

  flex: 0 0 auto;
  inline-size: 44px;
  block-size: 44px;

  color: #ffffff;
  background:
    linear-gradient(
      135deg,
      #8d72ff,
      var(--color-primary)
    );

  border-radius: 14px;
  font-size: 1.3rem;
  font-weight: 800;
  box-shadow:
    0 12px 28px rgb(109 74 255 / 32%);
}

.brand strong,
.brand small {
  display: block;
}

.brand strong {
  font-size: 1.05rem;
}

.brand small {
  margin-block-start: 2px;
  color: rgb(247 245 255 / 65%);
  font-size: 0.72rem;
  line-height: 1.5;
}

.new-chat-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;

  width: 100%;
  min-height: 48px;
  padding: 10px 16px;

  color: #ffffff;
  background-color: var(--color-primary);

  border-radius: var(--radius-md);
  cursor: pointer;

  transition:
    background-color 160ms ease,
    transform 160ms ease,
    box-shadow 160ms ease;
}

.new-chat-button:hover {
  background-color: var(--color-primary-dark);
  box-shadow:
    0 12px 24px rgb(109 74 255 / 24%);
  transform: translateY(-1px);
}

.sidebar__nav {
  display: grid;
  gap: 8px;
}

.sidebar__label {
  margin: 0 8px 4px;
  color: rgb(247 245 255 / 48%);
  font-size: 0.75rem;
}

.suggestion {
  width: 100%;
  padding: 12px;

  color: rgb(247 245 255 / 78%);
  background-color: transparent;

  border: 1px solid transparent;
  border-radius: var(--radius-sm);

  text-align: start;
  cursor: pointer;

  transition:
    color 160ms ease,
    background-color 160ms ease,
    border-color 160ms ease;
}

.suggestion:hover {
  color: #ffffff;
  background-color: rgb(255 255 255 / 7%);
  border-color: rgb(255 255 255 / 9%);
}

.sidebar__footer {
  margin-block-start: auto;
  padding-block-start: 16px;
  border-block-start:
    1px solid rgb(255 255 255 / 10%);
}

.sidebar__footer a {
  display: block;
  padding: 10px 8px;
  color: rgb(247 245 255 / 70%);
  font-size: 0.82rem;
}

.sidebar__footer a:hover {
  color: #ffffff;
}

.chat-panel {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-width: 0;
  min-height: 100dvh;
}

.chat-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 10;

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;

  padding: 18px clamp(20px, 4vw, 48px);

  background-color: rgb(245 246 251 / 82%);
  border-block-end: 1px solid rgb(230 232 239 / 75%);
  backdrop-filter: blur(14px);
}

.chat-header h1 {
  margin: 0;
  font-size: clamp(1.1rem, 2vw, 1.35rem);
  line-height: 1.4;
}

.eyebrow {
  margin: 0 0 2px;
  color: var(--color-primary);
  font-size: 0.72rem;
  font-weight: 700;
}

.status {
  display: inline-flex;
  align-items: center;
  gap: 7px;

  padding: 7px 11px;

  color: #087443;
  background-color: #eafbf3;

  border: 1px solid #c8f1dd;
  border-radius: 999px;
  font-size: 0.75rem;
}

.status__dot {
  inline-size: 7px;
  block-size: 7px;

  background-color: var(--color-success);
  border-radius: 50%;
  box-shadow:
    0 0 0 4px rgb(18 183 106 / 13%);
}

.message-list {
  width: 100%;
  max-width: var(--content-max-width);
  margin-inline: auto;
  padding:
    clamp(24px, 5vw, 52px)
    clamp(16px, 4vw, 36px);

  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-behavior: smooth;
}

.message {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-block-end: 24px;
}

.message--user {
  flex-direction: row-reverse;
}

.message__avatar {
  display: grid;
  place-items: center;

  flex: 0 0 auto;
  inline-size: 38px;
  block-size: 38px;

  color: #ffffff;
  background-color: var(--color-primary);

  border-radius: 12px;
  font-size: 0.9rem;
  font-weight: 800;
}

.message--user .message__avatar {
  color: var(--color-primary);
  background-color: var(--color-primary-soft);
}

.message__body {
  max-width: min(78%, 720px);
}

.message--user .message__body {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.message__name {
  margin: 0 4px 5px;
  color: var(--color-muted);
  font-size: 0.72rem;
}

.message__content {
  padding: 14px 17px;

  color: var(--color-text);
  background-color: var(--color-assistant-message);

  border: 1px solid var(--color-border);
  border-radius:
    6px
    var(--radius-md)
    var(--radius-md)
    var(--radius-md);

  box-shadow: var(--shadow-message);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.message--user .message__content {
  color: #ffffff;
  background-color: var(--color-user-message);
  border-color: var(--color-user-message);
  border-radius:
    var(--radius-md)
    6px
    var(--radius-md)
    var(--radius-md);
}

.message--loading .message__content {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: 50px;
}

.typing-dot {
  inline-size: 7px;
  block-size: 7px;

  background-color: var(--color-primary);
  border-radius: 50%;

  animation: typing-pulse 1s ease-in-out infinite;
}

.typing-dot:nth-child(2) {
  animation-delay: 120ms;
}

.typing-dot:nth-child(3) {
  animation-delay: 240ms;
}

@keyframes typing-pulse {
  0%,
  100% {
    opacity: 0.3;
    transform: translateY(0);
  }

  50% {
    opacity: 1;
    transform: translateY(-3px);
  }
}

.composer-wrapper {
  position: sticky;
  inset-block-end: 0;

  padding:
    14px
    clamp(16px, 4vw, 36px)
    18px;

  background:
    linear-gradient(
      to top,
      var(--color-page) 72%,
      rgb(245 246 251 / 0%)
    );
}

.composer {
  display: flex;
  align-items: flex-end;
  gap: 10px;

  width: 100%;
  max-width: var(--content-max-width);
  margin-inline: auto;
  padding: 8px;

  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 20px;
  box-shadow: var(--shadow-panel);
}

.composer:focus-within {
  border-color: rgb(109 74 255 / 55%);
  box-shadow:
    0 0 0 4px rgb(109 74 255 / 8%),
    var(--shadow-panel);
}

.composer textarea {
  flex: 1;
  min-width: 0;
  max-height: 180px;
  padding: 11px 12px;

  color: var(--color-text);
  background-color: transparent;

  border: 0;
  outline: 0;
  resize: none;
  line-height: 1.7;
}

.composer textarea::placeholder {
  color: #98a2b3;
}

.send-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;

  flex: 0 0 auto;
  min-height: 44px;
  padding: 9px 16px;

  color: #ffffff;
  background-color: var(--color-primary);

  border-radius: 14px;
  cursor: pointer;

  transition:
    background-color 160ms ease,
    transform 160ms ease,
    opacity 160ms ease;
}

.send-button:hover:not(:disabled) {
  background-color: var(--color-primary-dark);
  transform: translateY(-1px);
}

.send-button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.composer-note {
  max-width: var(--content-max-width);
  margin: 8px auto 0;

  color: var(--color-muted);
  font-size: 0.7rem;
  text-align: center;
}

button:focus-visible,
a:focus-visible,
textarea:focus-visible {
  outline: 3px solid rgb(109 74 255 / 28%);
  outline-offset: 3px;
}

.sr-only {
  position: absolute;

  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;

  overflow: hidden;
  clip: rect(0, 0, 0, 0);

  white-space: nowrap;
  border: 0;
}

@media (max-width: 820px) {
  .app-shell {
    grid-template-columns: 1fr;
  }

  .sidebar {
    display: none;
  }

  .chat-header {
    padding-inline: 18px;
  }

  .message-list {
    padding-block-start: 28px;
  }

  .message__body {
    max-width: 86%;
  }
}

@media (max-width: 520px) {
  .chat-header {
    padding-block: 14px;
  }

  .status {
    padding: 6px 9px;
  }

  .message {
    gap: 8px;
  }

  .message__avatar {
    inline-size: 32px;
    block-size: 32px;
    border-radius: 10px;
  }

  .message__body {
    max-width: calc(100% - 40px);
  }

  .message__content {
    padding: 12px 14px;
  }

  .send-button {
    inline-size: 44px;
    padding-inline: 0;
  }

  .send-button__text {
    position: absolute;

    width: 1px;
    height: 1px;

    overflow: hidden;
    clip: rect(0, 0, 0, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

تحلیل CSS پروژه

در این پروژه از Grid برای ساخت Layout اصلی استفاده شده است:

.app-shell {
  display: grid;
  grid-template-columns:
    var(--sidebar-width)
    minmax(0, 1fr);
}

تابع minmax(0, 1fr) اهمیت زیادی دارد. مقدار صفر اجازه می‌دهد ستون اصلی در صورت وجود محتوای طولانی کوچک شود و از صفحه بیرون نزند.

داخل Sidebar از Flexbox استفاده شده است:

.sidebar {
  display: flex;
  flex-direction: column;
}

سپس Footer با این قانون به پایین Sidebar منتقل می‌شود:

.sidebar__footer {
  margin-block-start: auto;
}

برای فرم ارسال نیز Flexbox مناسب است؛ زیرا Textarea و دکمه در یک ردیف قرار دارند:

.composer {
  display: flex;
  align-items: flex-end;
}

CSS Variables امکان تغییر Theme را در یک نقطه فراهم می‌کنند. همچنین Logical Properties باعث می‌شوند Layout با جهت RTL سازگارتر باشد.

مرحله چهارم: افزودن JavaScript

فایل static/app.js:

const form = document.querySelector("#chat-form");
const input = document.querySelector("#prompt");
const sendButton = document.querySelector("#send-button");
const messageList = document.querySelector("#message-list");
const newChatButton = document.querySelector("#new-chat");
const suggestions = document.querySelectorAll(".suggestion");

const messages = [];

function scrollToLatestMessage() {
  messageList.scrollTop = messageList.scrollHeight;
}

function createMessageElement(role, text) {
  const article = document.createElement("article");
  article.className = `message message--${role}`;

  const avatar = document.createElement("div");
  avatar.className = "message__avatar";
  avatar.setAttribute("aria-hidden", "true");
  avatar.textContent = role === "user" ? "ش" : "د";

  const body = document.createElement("div");
  body.className = "message__body";

  const name = document.createElement("p");
  name.className = "message__name";
  name.textContent =
    role === "user" ? "شما" : "دستیار درواره";

  const content = document.createElement("div");
  content.className = "message__content";
  content.textContent = text;

  body.append(name, content);
  article.append(avatar, body);

  return article;
}

function createLoadingElement() {
  const article = document.createElement("article");

  article.className =
    "message message--assistant message--loading";

  article.id = "loading-message";

  const avatar = document.createElement("div");
  avatar.className = "message__avatar";
  avatar.setAttribute("aria-hidden", "true");
  avatar.textContent = "د";

  const body = document.createElement("div");
  body.className = "message__body";

  const name = document.createElement("p");
  name.className = "message__name";
  name.textContent = "در حال آماده‌سازی پاسخ";

  const content = document.createElement("div");
  content.className = "message__content";
  content.setAttribute("aria-label", "در حال دریافت پاسخ");

  for (let index = 0; index < 3; index += 1) {
    const dot = document.createElement("span");
    dot.className = "typing-dot";
    content.append(dot);
  }

  body.append(name, content);
  article.append(avatar, body);

  return article;
}

function setLoading(isLoading) {
  sendButton.disabled = isLoading;
  input.disabled = isLoading;

  if (isLoading) {
    messageList.append(createLoadingElement());
    scrollToLatestMessage();
    return;
  }

  document.querySelector("#loading-message")?.remove();
  input.disabled = false;
  input.focus();
}

function resizeTextarea() {
  input.style.height = "auto";

  input.style.height =
    `${Math.min(input.scrollHeight, 180)}px`;
}

async function sendMessage(prompt) {
  messageList.append(
    createMessageElement("user", prompt)
  );

  messages.push({
    role: "user",
    content: prompt
  });

  setLoading(true);
  scrollToLatestMessage();

  try {
    const response = await fetch("/api/chat", {
      method: "POST",

      headers: {
        "Content-Type": "application/json"
      },

      body: JSON.stringify({
        messages
      })
    });

    const payload = await response.json();

    if (!response.ok) {
      throw new Error(
        payload.detail || "دریافت پاسخ ناموفق بود."
      );
    }

    const answer =
      payload.answer || "پاسخی دریافت نشد.";

    messages.push({
      role: "assistant",
      content: answer
    });

    messageList.append(
      createMessageElement("assistant", answer)
    );
  } catch (error) {
    messageList.append(
      createMessageElement(
        "assistant",
        `خطا: ${error.message}`
      )
    );
  } finally {
    setLoading(false);
    scrollToLatestMessage();
  }
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const prompt = input.value.trim();

  if (!prompt || sendButton.disabled) {
    return;
  }

  input.value = "";
  resizeTextarea();

  await sendMessage(prompt);
});

input.addEventListener("input", resizeTextarea);

input.addEventListener("keydown", (event) => {
  if (
    event.key === "Enter" &&
    !event.shiftKey &&
    !event.isComposing
  ) {
    event.preventDefault();
    form.requestSubmit();
  }
});

suggestions.forEach((button) => {
  button.addEventListener("click", () => {
    input.value = button.dataset.prompt || "";
    resizeTextarea();
    input.focus();
  });
});

newChatButton.addEventListener("click", () => {
  messages.length = 0;

  messageList.replaceChildren(
    createMessageElement(
      "assistant",
      "گفت‌وگوی جدید شروع شد. سؤال خود را بنویسید."
    )
  );

  input.value = "";
  resizeTextarea();
  input.focus();
});

در این کد پاسخ مدل با textContent در صفحه قرار می‌گیرد، نه innerHTML. این انتخاب باعث می‌شود متن پاسخ به‌صورت HTML تفسیر نشود.

اگر در آینده بخواهید Markdown نمایش دهید، بهتر است از یک Parser معتبر استفاده کنید و خروجی HTML را قبل از نمایش Sanitization کنید.

مرحله پنجم: ساخت Backend امن با FastAPI

API Key نباید داخل app.js یا کد قابل‌مشاهده مرورگر قرار گیرد. Backend درخواست مرورگر را دریافت می‌کند و سپس با استفاده از کلید موجود در متغیر محیطی به درواره متصل می‌شود.

فایل app.py:

import os
from typing import Literal

import httpx
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field

load_dotenv()

DARVAREH_API_KEY = os.getenv("DARVAREH_API_KEY")
DARVAREH_MODEL_ID = os.getenv(
    "DARVAREH_MODEL_ID",
    "YOUR_MODEL_ID",
)

DARVAREH_CHAT_URL = (
    "https://api.darvareh.ir/v1/chat/completions"
)

app = FastAPI(title="Darvareh CSS Assistant")


class Message(BaseModel):
    role: Literal["user", "assistant", "system"]
    content: str = Field(min_length=1, max_length=10000)


class ChatRequest(BaseModel):
    messages: list[Message] = Field(
        min_length=1,
        max_length=30,
    )


@app.post("/api/chat")
async def chat(request: ChatRequest):
    if not DARVAREH_API_KEY:
        raise HTTPException(
            status_code=500,
            detail="کلید API در سرور تنظیم نشده است.",
        )

    payload = {
        "model": DARVAREH_MODEL_ID,
        "messages": [
            {
                "role": "system",
                "content": (
                    "شما یک دستیار فارسی، دقیق و مفید هستید. "
                    "پاسخ را روشن و ساختاریافته ارائه کنید."
                ),
            },
            *[
                {
                    "role": message.role,
                    "content": message.content,
                }
                for message in request.messages
            ],
        ],
        "temperature": 0.4,
        "max_tokens": 1200,
    }

    headers = {
        "Authorization": f"Bearer {DARVAREH_API_KEY}",
        "Content-Type": "application/json",
    }

    try:
        async with httpx.AsyncClient(
            timeout=httpx.Timeout(60.0)
        ) as client:
            response = await client.post(
                DARVAREH_CHAT_URL,
                headers=headers,
                json=payload,
            )

        if response.status_code >= 400:
            raise HTTPException(
                status_code=502,
                detail=(
                    "سرویس مدل پاسخ موفقی ارسال نکرد. "
                    f"کد وضعیت: {response.status_code}"
                ),
            )

        data = response.json()

        answer = data["choices"][0]["message"]["content"]

        if not isinstance(answer, str):
            raise ValueError("ساختار پاسخ معتبر نیست.")

        return {"answer": answer}

    except httpx.TimeoutException as exc:
        raise HTTPException(
            status_code=504,
            detail="زمان انتظار برای پاسخ به پایان رسید.",
        ) from exc

    except httpx.RequestError as exc:
        raise HTTPException(
            status_code=502,
            detail="ارتباط با سرویس مدل برقرار نشد.",
        ) from exc

    except (KeyError, IndexError, TypeError, ValueError) as exc:
        raise HTTPException(
            status_code=502,
            detail="ساختار پاسخ سرویس قابل پردازش نبود.",
        ) from exc


app.mount(
    "/",
    StaticFiles(directory="static", html=True),
    name="static",
)

مرحله ششم: نصب وابستگی‌ها

فایل requirements.txt:

fastapi
uvicorn[standard]
httpx
python-dotenv

محیط مجازی بسازید:

python -m venv .venv

فعال‌سازی در Linux و macOS:

source .venv/bin/activate

فعال‌سازی در PowerShell ویندوز:

.venv\Scripts\Activate.ps1

نصب وابستگی‌ها:

pip install -r requirements.txt

مرحله هفتم: تنظیم متغیرهای محیطی

فایل .env:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID

برای دریافت دسترسی و استفاده از API می‌توانید در درواره ثبت‌نام کنید.

مدل مناسب و اطلاعات قیمت را از صفحه مدل‌های درواره بررسی کنید و Model ID نمایش‌داده‌شده در همان صفحه را در متغیر DARVAREH_MODEL_ID قرار دهید.

فایل .env را وارد Git نکنید:

.env
.venv/
__pycache__/

مرحله هشتم: اجرای پروژه

uvicorn app:app --reload

سپس آدرس زیر را باز کنید:

http://127.0.0.1:8000

اکنون می‌توانید پیام خود را در رابط فارسی وارد کنید و پاسخ را از مدل انتخاب‌شده دریافت کنید.

چرا API Key نباید در Frontend باشد؟

هر فایلی که به مرورگر ارسال می‌شود، از جمله JavaScript، برای کاربر قابل‌مشاهده است.

قرار دادن کلید به شکل زیر امن نیست:

const API_KEY = "YOUR_DARVAREH_API_KEY";

حتی اگر کد Minify یا Obfuscate شود، کلید همچنان قابل استخراج است. متغیرهای محیطی ابزارهای Frontend نیز اگر در Bundle نهایی قرار گیرند، Secret محسوب نمی‌شوند.

الگوی مناسب:

Frontend
  ↓
Backend شما
  ↓
API درواره

Backend امکان اضافه‌کردن کنترل‌های زیر را نیز فراهم می‌کند:

  • محدودیت اندازه پیام
  • محدودیت تعداد پیام‌ها
  • احراز هویت کاربران
  • Rate Limiting
  • ثبت مصرف
  • انتخاب مدل
  • Timeout
  • مدیریت خطا
  • جلوگیری از ارسال مستقیم کلید به مرورگر

ساخت Dark Mode با CSS Variables

ابتدا متغیرهای حالت تیره را تعریف کنید:

[data-theme="dark"] {
  color-scheme: dark;

  --color-text: #f5f7ff;
  --color-muted: #a7afc0;
  --color-border: #30364a;
  --color-page: #11131a;
  --color-surface: #1b1f2a;
  --color-assistant-message: #1b1f2a;
}

سپس با JavaScript ویژگی Theme را روی عنصر ریشه قرار دهید:

document.documentElement.dataset.theme = "dark";

برای استفاده از تنظیم سیستم‌عامل نیز می‌توان از Media Query استفاده کرد:

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --color-text: #f5f7ff;
    --color-muted: #a7afc0;
    --color-border: #30364a;
    --color-page: #11131a;
    --color-surface: #1b1f2a;
    --color-assistant-message: #1b1f2a;
  }
}

اگر کاربر دکمه انتخاب Theme دارد، انتخاب صریح کاربر باید بر تنظیم خودکار سیستم اولویت داشته باشد.

روش صحیح سازمان‌دهی CSS

در پروژه‌های متوسط می‌توان فایل را به بخش‌های مشخص تقسیم کرد:

styles/
├── tokens.css
├── reset.css
├── base.css
├── layout.css
├── components.css
├── utilities.css
└── responsive.css

وظیفه هر فایل:

فایلمحتوا
tokens.cssرنگ، فاصله، فونت، Radius و Shadow
reset.cssاصلاح پیش‌فرض مرورگر
base.cssاستایل تگ‌های عمومی
layout.cssساختار صفحه
components.cssدکمه، کارت، فرم و پیام
utilities.cssClassهای کمکی محدود
responsive.cssقواعد Media Query در صورت نیاز

برای پروژه کوچک، یک فایل منظم معمولاً بهتر از چندین فایل پراکنده است. با رشد پروژه می‌توان ساختار را تفکیک کرد.

نام‌گذاری Classها

نام Class باید هدف عنصر را نشان دهد:

.chat-header {}
.message-list {}
.message {}
.message--user {}
.message__content {}

این نمونه از ایده‌های روش BEM استفاده می‌کند:

block
block__element
block--modifier

مثال:

.message {}
.message__avatar {}
.message__content {}
.message--user {}
.message--assistant {}

لازم نیست در تمام پروژه‌ها BEM را کاملاً سخت‌گیرانه اجرا کنید. هدف اصلی باید نام‌گذاری قابل‌فهم و جلوگیری از وابستگی شدید CSS به ساختار تو‌در‌توی HTML باشد.

CSS Nesting

مرورگرهای مدرن از CSS Nesting پشتیبانی می‌کنند، اما استفاده بیش از حد از Nesting می‌تواند Specificity و خوانایی را پیچیده کند.

نمونه:

.card {
  padding: 20px;
  border: 1px solid var(--color-border);

  & h2 {
    margin-block-start: 0;
  }

  &:hover {
    border-color: var(--color-primary);
  }
}

حتی با پشتیبانی مرورگر، بهتر است عمق Nesting محدود بماند.

Container Query چیست؟

Media Query معمولاً اندازه Viewport را بررسی می‌کند. Container Query اندازه Container والد را در نظر می‌گیرد.

این قابلیت برای Componentهایی مناسب است که ممکن است در Sidebar، صفحه اصلی یا Modal قرار گیرند.

.card-wrapper {
  container-type: inline-size;
}

.card {
  display: grid;
  gap: 12px;
}

@container (min-width: 500px) {
  .card {
    grid-template-columns: 140px 1fr;
  }
}

در این مثال، چیدمان کارت براساس عرض Container خودش تغییر می‌کند، نه عرض کل صفحه.

خطاهای رایج CSS

استایل اعمال نمی‌شود

موارد زیر را بررسی کنید:

  • مسیر فایل CSS صحیح است؟
  • فایل در Network مرورگر با وضعیت موفق بارگذاری شده؟
  • Selector با HTML مطابقت دارد؟
  • قانون دیگری آن را Override کرده؟
  • مقدار Property معتبر است؟
  • فایل Cache نشده؟
  • اشتباه نگارشی در Class وجود ندارد؟

از بخش Styles در DevTools استفاده کنید. قانون خط‌خورده معمولاً نشان می‌دهد قانون دیگری برنده شده است.

Width از Container بیرون می‌زند

Reset زیر را اضافه کنید:

*,
*::before,
*::after {
  box-sizing: border-box;
}

برای فرزندهای Grid یا Flex نیز گاهی لازم است:

.content {
  min-width: 0;
}

برای تصویر:

img {
  display: block;
  max-width: 100%;
  height: auto;
}

Ellipsis کار نمی‌کند

برای متن تک‌خطی:

.title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

در Flexbox ممکن است والد یا فرزند به min-width: 0 نیاز داشته باشد.

z-index اثر ندارد

بررسی کنید:

  • عنصر Position مناسب دارد؟
  • والد Stacking Context ساخته است؟
  • عنصر داخل Stacking Context محدودشده قرار دارد؟
  • transform یا opacity روی والد وجود دارد؟

position: sticky کار نمی‌کند

موارد مهم:

  • مقدار top یا inset-block-start تعیین شده؟
  • یکی از والدها overflow: hidden یا overflow: auto دارد؟
  • ارتفاع کافی برای Scroll وجود دارد؟
  • ساختار Grid یا Flex مانع کشیده‌شدن صحیح عنصر شده است؟

ارتفاع 100vh در موبایل مشکل دارد

در مرورگر موبایل نوار آدرس می‌تواند ارتفاع قابل‌مشاهده را تغییر دهد. در مرورگرهای جدید می‌توان از 100dvh استفاده کرد:

.app {
  min-height: 100dvh;
}

متن فارسی و انگلیسی به‌هم می‌ریزد

برای صفحه:

<html lang="fa" dir="rtl">

برای کد و شناسه انگلیسی:

code,
pre,
.model-id {
  direction: ltr;
  text-align: left;
}

در موارد ترکیبی می‌توانید از unicode-bidi و عنصر HTML مناسب مانند bdi نیز استفاده کنید.

استفاده از DevTools برای Debug کردن CSS

مرورگرها ابزارهای قدرتمندی برای بررسی CSS دارند.

در Chrome، Edge یا Firefox:

  1. روی عنصر کلیک راست کنید.
  2. گزینه Inspect را بزنید.
  3. در بخش Elements، عنصر را انتخاب کنید.
  4. قوانین قسمت Styles را بررسی کنید.
  5. در بخش Computed مقدار نهایی Property را ببینید.
  6. Box Model را بررسی کنید.
  7. قوانین را موقتاً فعال یا غیرفعال کنید.
  8. حالت موبایل را با Device Toolbar آزمایش کنید.
  9. وضعیت‌های :hover و :focus را شبیه‌سازی کنید.
  10. Grid و Flex Overlay را فعال کنید.

ویرایش در DevTools موقتی است. پس از پیدا کردن راه‌حل، تغییر را در فایل اصلی ثبت کنید.

چگونه CSS تولیدشده با هوش مصنوعی را بررسی کنیم؟

هوش مصنوعی می‌تواند در تولید CSS، تبدیل طرح به کد و رفع مشکلات Layout کمک کند؛ اما خروجی باید بررسی شود.

یک پرامپت مناسب:

این Component را با CSS استاندارد و بدون Framework طراحی کن.

نیازمندی‌ها:
- زبان صفحه فارسی و جهت RTL است.
- از Flexbox یا Grid براساس کاربرد درست استفاده کن.
- از CSS Variables برای رنگ‌ها و فاصله‌ها استفاده کن.
- طراحی باید از عرض 320px تا Desktop واکنش‌گرا باشد.
- Focus قابل‌مشاهده برای Keyboard ایجاد کن.
- prefers-reduced-motion را رعایت کن.
- از !important استفاده نکن.
- Selectorها را کوتاه و قابل‌نگهداری نگه دار.
- برای محتوای طولانی overflow مناسب در نظر بگیر.
- HTML و CSS را جداگانه ارائه کن.

بعد از دریافت خروجی بررسی کنید:

  • آیا HTML معنایی است؟
  • آیا در موبایل Overflow ایجاد می‌شود؟
  • آیا Contrast مناسب است؟
  • آیا Keyboard Navigation قابل‌استفاده است؟
  • آیا CSS تکراری وجود دارد؟
  • آیا Selectorها بیش از حد اختصاصی هستند؟
  • آیا !important بدون ضرورت استفاده شده؟
  • آیا پاسخ مدل شامل ویژگی‌های آزمایشی بدون Fallback است؟
  • آیا کد در مرورگرهای هدف پروژه پشتیبانی می‌شود؟

بهینه‌سازی عملکرد CSS

CSS معمولاً سبک است، اما در پروژه‌های بزرگ می‌تواند روی عملکرد اثر بگذارد.

راهکارهای عملی:

  • CSS بلااستفاده را حذف کنید.
  • از Framework کامل فقط برای چند Component استفاده نکنید.
  • فایل‌های حیاتی را بی‌دلیل به چند درخواست کوچک تقسیم نکنید.
  • فونت‌ها و وزن‌های غیرضروری را کاهش دهید.
  • Animation سنگین روی width، height و موقعیت Layout را محدود کنید.
  • برای حرکت‌های ساده معمولاً transform و opacity مناسب‌ترند.
  • Selectorهای بسیار پیچیده و تو‌در‌تو نسازید.
  • تصاویر پس‌زمینه بزرگ را بهینه کنید.
  • CSS تولیدشده را قبل از انتشار Minify کنید.
  • تغییرات را با DevTools و ابزارهای سنجش واقعی بررسی کنید.

آیا باید از Bootstrap یا Tailwind استفاده کنیم؟

Frameworkها می‌توانند سرعت توسعه را افزایش دهند، اما جای یادگیری CSS را نمی‌گیرند.

گزینهمزیتمحدودیت
CSS خالصکنترل کامل و وابستگی کمنیازمند طراحی ساختار
BootstrapComponentهای آمادهظاهر پیش‌فرض و Override بیشتر
Tailwind CSSتوسعه سریع با Utility ClassHTML شلوغ‌تر و نیازمند Build
CSS ModulesScope محلی در Componentوابسته به ابزار Build
CSS-in-JSترکیب Style با Componentهزینه Runtime یا پیچیدگی ابزار
SassVariable، Mixin و ساختار بیشترنیازمند Compile

اگر CSS پایه را درک نکنید، رفع مشکلات هر Framework دشوار می‌شود. ابتدا Box Model، Cascade، Flexbox، Grid و Responsive Design را یاد بگیرید؛ سپس ابزار مناسب پروژه را انتخاب کنید.

چک‌لیست CSS برای پروژه واقعی

پیش از انتشار رابط، این موارد را بررسی کنید:

  • box-sizing: border-box تنظیم شده است.
  • رابط در عرض ۳۲۰ پیکسل آزمایش شده است.
  • در Desktop فضای خالی نامناسب وجود ندارد.
  • متن طولانی از Container خارج نمی‌شود.
  • تصاویر max-width: 100% دارند.
  • Focus در Keyboard قابل‌مشاهده است.
  • Contrast متن و پس‌زمینه مناسب است.
  • حالت Hover تنها روش نمایش اطلاعات نیست.
  • حالت Loading مشخص است.
  • دکمه Disabled واقعاً غیرفعال می‌شود.
  • prefers-reduced-motion رعایت شده است.
  • Logical Properties برای RTL بررسی شده‌اند.
  • !important غیرضروری وجود ندارد.
  • Selectorهای بسیار پیچیده حذف شده‌اند.
  • CSS Variables برای مقادیر پرتکرار استفاده شده‌اند.
  • صفحه با بزرگ‌نمایی مرورگر بررسی شده است.
  • رابط در محتوای فارسی و انگلیسی آزمایش شده است.
  • کلید API در Frontend وجود ندارد.
  • خطای Backend به شکل قابل‌فهم نمایش داده می‌شود.
  • قیمت و Model ID به‌صورت Hardcode نامطمئن درج نشده‌اند.

نقشه راه یادگیری CSS

یک مسیر عملی برای یادگیری:

مرحله اول: مبانی

  • Syntax
  • Selector
  • Color
  • Font
  • Background
  • Border
  • Margin
  • Padding

مرحله دوم: منطق CSS

  • Cascade
  • Specificity
  • Inheritance
  • Box Model
  • واحدهای اندازه‌گیری

مرحله سوم: Layout

  • Normal Flow
  • Display
  • Position
  • Flexbox
  • Grid
  • Overflow

مرحله چهارم: Responsive Design

  • Meta Viewport
  • Media Query
  • Mobile First
  • تصاویر واکنش‌گرا
  • clamp()
  • Container Query

مرحله پنجم: کیفیت رابط

  • CSS Variables
  • دسترس‌پذیری
  • Focus
  • Dark Mode
  • Animation
  • prefers-reduced-motion
  • Logical Properties

مرحله ششم: معماری پروژه

  • نام‌گذاری Class
  • Component Styling
  • Design Token
  • حذف CSS تکراری
  • تست مرورگر
  • بررسی عملکرد

سؤال‌های متداول

CSS چیست؟

CSS زبان Style Sheet برای تعیین ظاهر و چیدمان اسناد HTML است. رنگ، فونت، فاصله، Grid، Flexbox، Responsive Design و انیمیشن صفحات وب با CSS کنترل می‌شوند.

آیا CSS زبان برنامه‌نویسی است؟

CSS معمولاً زبان برنامه‌نویسی عمومی محسوب نمی‌شود. این زبان برای تعریف Presentation و Style اسناد طراحی شده است و منطق برنامه را مانند JavaScript اجرا نمی‌کند.

CSS3 چیست؟

اصطلاح CSS3 هنوز در جست‌وجوها و آموزش‌ها دیده می‌شود، اما CSS مدرن به مجموعه‌ای از Moduleهای مستقل تقسیم شده است. طبق توضیح MDN، CSS امروزی یک نسخه واحد با عنوان CSS4 ندارد و Moduleها جداگانه تکامل پیدا می‌کنند.

Flexbox بهتر است یا Grid؟

هیچ‌کدام به‌طور مطلق بهتر نیستند. Flexbox برای چیدمان یک‌بعدی و Grid برای چیدمان دوبعدی مناسب است. در یک پروژه واقعی معمولاً هر دو استفاده می‌شوند.

چرا CSS من اعمال نمی‌شود؟

مسیر اشتباه فایل، Selector نادرست، Specificity، ترتیب قوانین، Cache مرورگر یا مقدار نامعتبر از علت‌های رایج هستند. DevTools مرورگر سریع‌ترین راه تشخیص مشکل است.

چرا از !important استفاده نکنیم؟

استفاده زیاد از !important منطق Cascade را دشوار و Override کردن استایل‌ها را پیچیده می‌کند. ابتدا Selector، ترتیب فایل و معماری CSS را اصلاح کنید.

rem بهتر است یا px؟

برای فونت و فاصله‌های مقیاس‌پذیر معمولاً rem مناسب است. برای Border، آیکون و مقادیر دقیق کوچک می‌توان از px استفاده کرد. انتخاب باید براساس کاربرد باشد.

چگونه رابط فارسی RTL بسازیم؟

در HTML از dir="rtl" و lang="fa" استفاده کنید. در CSS نیز Logical Properties مانند margin-inline-start و padding-inline را جایگزین وابستگی مستقیم به چپ و راست کنید.

آیا می‌توان با CSS به API هوش مصنوعی متصل شد؟

خیر. CSS فقط ظاهر صفحه را کنترل می‌کند. برای ارسال درخواست به API به JavaScript و برای محافظت از API Key به Backend نیاز دارید.

آیا API Key را می‌توان در CSS مخفی کرد؟

خیر. CSS، HTML و JavaScript ارسال‌شده به مرورگر عمومی هستند. API Key باید در Backend یا Secret Manager نگهداری شود.

چگونه Dark Mode بسازیم؟

می‌توانید رنگ‌ها را با CSS Variables تعریف کنید و با prefers-color-scheme یا یک ویژگی مانند data-theme="dark" مقادیر متغیرها را تغییر دهید.

CSS Variable چه تفاوتی با متغیر Sass دارد؟

CSS Variable در مرورگر و Runtime فعال است و می‌تواند براساس DOM، Media Query و Theme تغییر کند. متغیر Sass هنگام Build پردازش می‌شود و در Runtime وجود ندارد.

آیا CSS روی سئوی سایت اثر دارد؟

CSS به‌طور مستقیم جای محتوای مفید و HTML معنایی را نمی‌گیرد، اما می‌تواند بر تجربه کاربری، نمایش موبایل، خوانایی و عملکرد صفحه اثر بگذارد. مخفی‌کردن محتوای غیرطبیعی یا ایجاد تجربه ضعیف نیز می‌تواند به کیفیت صفحه آسیب بزند.

جمع‌بندی

CSS فقط ابزاری برای تغییر رنگ و فونت نیست. این زبان سیستم اصلی طراحی و چیدمان رابط‌های وب است.

برای تسلط واقعی باید این مفاهیم را درک کنید:

  • Selector و Syntax
  • Cascade و Specificity
  • Inheritance
  • Box Model
  • واحدهای اندازه‌گیری
  • Flexbox
  • CSS Grid
  • Responsive Design
  • CSS Variables
  • Logical Properties
  • دسترس‌پذیری
  • Debug با DevTools

در پروژه عملی این مقاله یک رابط کامل فارسی ساختیم که از Grid برای Layout اصلی، Flexbox برای اجزای داخلی، Media Query برای موبایل و CSS Variables برای مدیریت Design Tokenها استفاده می‌کند.

همچنین رابط را از طریق یک Backend کوچک به API درواره متصل کردیم تا API Key در مرورگر افشا نشود.

برای شروع استفاده از API هوش مصنوعی، ساخت کلید و اتصال پروژه خود می‌توانید وارد وب‌سایت درواره شوید. برای مشاهده مدل‌های قابل‌استفاده، Model ID و اطلاعات به‌روز قیمت نیز صفحه مدل‌های درواره را بررسی کنید.

منابع تکمیلی

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.