**فرشاد** یک عادت خوب و شاید هم بد دارد: هر سایتی را که خوشش بیاید تا آخر اسکرول میکند! حتی وقتی کاری به محتوایش ندارد. او هفتهٔ پیش به سایتی رسید که با اسکرول بیشتر، مسیر روی صفحه نیز بهتدریج رسم میشد و ظاهری شبیه جاده داشت! او پس از چند بار پیمایش صفحه، تصمیم گرفت نمونهٔ مشابهی را پیادهسازی کند.
در این سوال صفحهٔ `landing` **جاده افتخار** را کامل میکنید؛ صفحهای که مسیر رسیدن تیمها به **فینال جام جهانی پردیس** را در طول اسکرول روایت میکند. **استفاده از `JavaScript` در این سوال مجاز نیست.**
|  |
| :-: |
**هدف این سوال** پیادهسازی انیمیشنهای وابسته به اسکرول (`scroll-driven animations`)، پیمایش صفحه (`scroll snap` و لنگرها و هدر چسبان)، تب بدون `JavaScript` و ساختار معنایی و دسترسپذیری است. **همچنین تمام طراحیهای بصری از قبل پیادهسازی شده و در قالب پروژه اولیه در اختیار شما قرار گرفتهاند.**
# **پروژهٔ اولیه**
پروژهٔ اولیه را از [این لینک](/contest/assignments/103143/download_problem_initial_project/356838/) دانلود کنید.
```plaintext
initial_project/
├── index.html
└── styles.css
```
+ **نکته:** داخل فایلهای پروژهٔ اولیه، در هر بخش کامنتهایی جهت انجام راهنمایی برای پیادهسازی قرار گرفتهاند: قرارداد هر تابع، حالتهای مرزی و نکتههای پیادهسازی. پیش از شروع هر فایل، کامنتهای بالای آن را بخوانید.
+ **نکته:** پروژهٔ اولیه یک صفحهٔ **کامل ولی کاملاً ثابت** است. اگر همین حالا `index.html` را در مرورگر باز کنید، تمام بخشها، متنها، جدولها، کارت شهرها، کاشیهای آمار و هر دو تصویر `SVG` (مسیر جاده و جام) را سر جایشان میبینید. هیچکدام از اینها را دوباره نسازید و طراحی را **تغییر ندهید!**
+ **نکته:** آنچه عمداً حذف شده همان چیزی است که در این سوال از شما خواسته میشود: صفحه **هیچ حرکتی ندارد،** ساختارش از `div` ساخته شده و شناسههای اصلی و ویژگیهای دسترسپذیری در آن **موجود نیست.** جای هر مورد با کامنت `TODO` در همان فایل مشخص شده است.
## **آنچه باید اضافه کنید**
پیادهسازی شما در این سوال به سه دسته تقسیم میشود و سیستم داوری کوئرا هم فقط همین سه دسته را میسنجد:
| **دسته** | **چه کاری** |
| :-: | :-: |
| ساختار معنایی | `div`های اسکلت را به `header` و `nav` و `main` و `section` و `footer` تبدیل کنید و عنوانها را از `p` به `h1` و `h2` ببرید |
| شناسه و دسترسپذیری | مقدار `data-testid` عنصرهای اصلی و ویژگیهای `aria-label` و `aria-labelledby` و `aria-hidden` را بگذارید |
| حرکت و تعامل | انیمیشنهای وابسته به اسکرول، `scroll snap`، هدر چسبان، نوار متحرک، جابهجایی تبها و حالت کاهش حرکت |
+ **نکته:** شناسههای تکرارشونده از قبل در فایل پروژه اولیه قرار گرفتهاند تا وقتتان را صرف تایپ نکنید. دوازده چیپ تیمها، سه نوار شهر و لایههای پارالاکسشان، پنج کاشی آمار و مقدارهایشان، هفت نقطهٔ نوار پیشرفت، سه پنل و سه رادیو و سه برچسب تب، همگی `data-testid` دارند. شما فقط شناسهٔ عنصرهای اصلی را اضافه میکنید که در جدولهای متن سوال ذکر شدهاند.
+ **نکته:** تمام `id`های لازم برای لنگرها از قبل روی عنصرها هستند، پس لینکهای منو و نوار پیشرفت از همان ابتدا به بخش درست میروند. `id`ها را عوض نکنید.
+ **نکته:** تمام متنهای قابل نمایش در صفحه باید انگلیسی باشند و عنصر `html` باید `lang="en"` داشته باشد. این دو مورد از قبل در پروژهٔ اولیه تنظیم شدهاند.
# **ساختار کلی صفحه**
**جاده افتخار فرشاد** شامل یک `header`، یک نوار پیشرفت ثابت، هفت بخش اصلی و یک `footer` خواهد بود. هفت بخش اصلی باید داخل عنصر `main` قرار بگیرند و آن عنصر `data-testid="journey"` بگیرد.
|  |
| :-: |
| کشیده شدن مسیر SVG همراه با اسکرول؛ همین جریان را در اجرای واقعی برنامه نشان میدهد |
هر هفت بخش با محتوای کاملشان در **فایل پروژهٔ اولیه** قرار گرفتند و `id` هر کدام هم از قبل در فایل موجود است، پس لینکهای منو و نوار پیشرفت از همان ابتدا کار میکنند. کاری که با این جدول میکنید این است که به هر بخش `data-testid` متناظرش را بدهید و `div` آن را به `section` تبدیل کنید.
| **بخش** | `data-testid` |
| :-: | :-: |
| شروع (شامل تنها `h1` صفحه با `data-testid="hero-title"`) | `chapter-hero` |
| مرحلهٔ گروهی | `chapter-group-stage` |
| مرحلهٔ حذفی | `chapter-knockouts` |
| شهرهای میزبان | `chapter-cities` |
| تیمهای صعودکرده | `chapter-nations` |
| آمار | `chapter-stats` |
| فینال | `chapter-final` |
|  |
| :-: |
| ترتیب اجرا از ورودی کاربر تا بهروزرسانی صفحه |
# **پیادهسازی الزامات** *HTML*
## **پیادهسازی** `header` **و منو**
هدر با لوگو، پنج لینک و دکمهٔ اصلی از قبل ساخته شده است و مقدار `href` همهٔ لینکها هم درست است. پیادهسازی سه مورد روی آن باقیمانده است: `div` بیرونی را به `header` و `div` داخلی را به `nav` تبدیل کنید، `data-testid`ها و `aria-label` را بگذارید و در `CSS` هدر را چسبان کنید. برای چسبیدن، `position: sticky` بهتنهایی کافی نیست و کنارش `top: 0` هم لازم است!
| **عنصر** | `data-testid` | **نوع** | **شرط** |
| :-: | :-: | :-: | :-: |
| لوگو | `nav-brand` | `a` | مقدار `href` برابر `#top` |
| فهرست لینکها | `nav-links` | `ul` یا `ol` | شامل پنج لینک زیر |
| لینک اصلی | `nav-cta` | `a` | باید دیده شود و `href` داشته باشد |
- پنج لینک فهرست باید این `data-testid`ها را بگیرند: `nav-link-group-stage`، `nav-link-knockouts`، `nav-link-cities`، `nav-link-nations` و `nav-link-final`. مقدار `href` هرکدام از قبل به بخش درست اشاره میکند.
برای حالت شیشهای هدر از `backdrop-filter: blur(...)` استفاده کنید. برای اینکه این افکت دیده شود، **پسزمینهٔ هدر باید نیمهشفاف باشد.**
## **رسم تدریجی مسیر** `SVG` **با اسکرول**
**خودِ تصویر `SVG` از قبل کشیده شده است.** هر دو مسیر، چهار نقطهٔ روی جاده و گرادیانها داخل `index.html` هستند و لازم نیست چیزی طراحی کنید؛ کار شما فقط **به حرکت درآوردن** همین مسیر آماده است.
|  |
| :-: |
| شروع مسیر `SVG` در ابتدای صفحه |
روی مسیر آماده مقدار `pathLength="1"` گذاشته شده است تا لازم نباشد طول واقعی مسیر را حساب کنید؛ پس `stroke-dasharray: 1` و بردن `stroke-dashoffset` از `1` تا `0` کافی است. آنچه باید به این بخش اضافه کنید:
+ روی عنصر `svg` مقدار `data-testid="journey-path"` و یک `aria-label` بگذارید. عنصر `viewBox` و `title` داخلش از قبل هست.
+ روی مسیر متحرک مقدار `data-testid="path-fill"` بگذارید. مسیر ثابت پسزمینه از قبل کلاس `path-track` دارد.
+ عنوان این بخش را به `h2` تبدیل کنید و `data-testid="path-title"` بگیرد.
+ انیمیشن رسم مسیر را در `styles.css` بنویسید.
+ **نکته:** `path-title` باید `h2` باشد، چون تنها `h1` صفحه `hero-title` است.
انیمیشن رسم مسیر باید با `animation-timeline: view()` انجام شود و **دقیقاً به همین شکل، بدون آرگومان** نوشته شود. `view()` یک خط زمان بر پایهٔ میزان دیدهشدن عنصر در ناحیهٔ اسکرول میسازد؛ پیشرفت انیمیشن به **موقعیت عنصر نسبت** به *viewport* وابسته میشود.
اگر میخواهید بازهٔ اجرای انیمیشن را تنظیم کنید، از خاصیت جداگانهٔ `animation-range` استفاده کنید، نه از آرگومانهای داخل `view()`. مثلاً `animation-range: cover 5% cover 70%` **یعنی انیمیشن از وقتی ۵٪ مسیر پیموده شد شروع و در ۷۰٪ تمام شود!**
+ **نکته:** نوشتن `animation` به شکل خلاصه، مقدار `animation-timeline` را به حالت پیشفرض برمیگرداند. پس اول `animation` را بنویسید و بعد `animation-timeline` را، نه برعکس:
```css style.css css
[data-testid="path-fill"] {
animation: draw linear both;
animation-timeline: view();
animation-range: cover 5% cover 70%;
}
```
## **پیادهسازی بخشهای مختلف و** `scroll snap`
هفت بخش در جاده افتخار پشت سر هم میآیند و اسکرول میتواند روی هر بخش `snap` شود. ویژگی `scroll-snap-type` باید روی عنصر اسکرولکنندهٔ صفحه، یعنی `html`، اعمال شود و `scroll-snap-align` روی هر بخش:
```css style.css css
html {
scroll-behavior: smooth;
scroll-snap-type: y proximity;
}
[data-testid^="chapter-"] {
scroll-snap-align: start;
min-height: 100vh;
}
```
+ **نکته:** در این سوال اسکرولکنندهٔ اصلی صفحه `html` است، پس `scroll-snap-type` را روی `html` بگذارید، نه روی `main`.
+ **نکته:** از مقدار `proximity` بهجای `mandatory` استفاده کنید. با `proximity` مرورگر فقط وقتی به نقطهٔ *snap* نزدیک باشید آن را اعمال میکند ولی `mandatory` پیمایش را **سختگیرانهتر** میکند و کاربر را بین نقاط *snap* جابهجا میکند.
سایر شرطهایی که در پیادهسازی این بخش باید در نظر بگیرید:
+ بخشهای `chapter-group-stage` و `chapter-final` باید `aria-labelledby` داشته باشند که به `id` عنوان خودشان اشاره کند. مثلاً `<h2 id="group-stage-title">` و `<section aria-labelledby="group-stage-title">`.
+ شمارهگذاری: عنصرهای شماره با متن `01` و `02` و شمارهٔ بخش پایانی از قبل در صفحه هستند؛ فقط `data-testid`های `chapter-index-1` و `chapter-index-2` و `chapter-index-final` را روی همانها بگذارید. شمارهگذاری بخشهای میانی سنجیده نمیشود.
+ دو عنصر آمادهٔ داخل دو بخش اول را با `data-testid`های `reveal-group-stage` و `reveal-knockouts` نشان کنید و برایشان انیمیشن ظاهرشدن هنگام ورود به `viewport` بنویسید.
## **پیادهسازی شهرهای میزبان و افکت پارالاکس**
سه نوار شهر با نامشان و لایهٔ پارالاکسشان از قبل ساخته شدهاند و `data-testid` هر شش عنصر هم روی آنهاست. کاری که میماند این است که لایهٔ پارالاکس هر شهر را **به حرکت دربیاورید** تا با سرعتی متفاوت از اسکرول صفحه جابهجا شود.
| **شهر** | **نوار** | **لایهٔ پارالاکس** |
| :-: | :-: | :-: |
| **نیویورک** | `city-band-newyork` | `city-parallax-newyork` |
| **تورنتو** | `city-band-toronto` | `city-parallax-toronto` |
| **مکزیکوسیتی** | `city-band-mexico` | `city-parallax-mexico` |
+ **لایههای پارالاکس تزئینی هستند** و محتوای مستقلی برای فناوریهای کمکی ندارند، پس روی آنها `aria-hidden="true"` بگذارید.
+ عنوان آمادهٔ بالای این بخش را به `h2` تبدیل کنید و `data-testid="cities-title"` بگیرد.
+ حرکت هر لایه باید با خط زمان اسکرول ساخته شود، نه با یک انیمیشن همیشگی.
|  |
| :-: |
| سه نوار شهر که لایهشان با سرعت متفاوت حرکت میکند |
## **پیادهسازی نوار متحرک تیمهای صعودکرده**
+ عنصر بیرونی با `data-testid="nations-marquee"` که دارای `aria-label` باشد.
+ فهرست دوازده تیم از قبل به شکل `ul` در صفحه است و `data-testid` چیپها هم رویشان هست؛ فقط `data-testid="marquee-track"` را به خود فهرست بدهید.
+ عنوان آمادهٔ این بخش را به `h2` تبدیل کنید و `data-testid="nations-title"` بگیرد.
+ نام انیمیشن این نوار باید `marquee` باشد و مقدار `animation-name` روی `marquee-track` نباید `none` باشد.
+ برای اینکه لبههای ابتدا و انتهای نوار بهجای قطع شدن، محو شوند، از `mask-image` استفاده کنید.
|  |
| :-: |
| نواحی اصلی صفحه و نام هرکدام |
## **پیادهسازی جدولهای گروهی با** `input` **از نوع** `radio`
سه جدول گروهی با محتوایشان، سه `input` از نوع `radio` و سه `label` از قبل در صفحه هستند و به هم وصل شدهاند. چیزی که کار نمیکند **جابهجایی** است: در پروژهٔ اولیه همیشه پنل اول دیده میشود. کار شما این است که نمایش و مخفیسازی پنلها را با سلکتور `:checked` بنویسید تا کلیک روی هر برچسب، جدول متناظرش را بیاورد، آن هم بدون حتی یک خط `JavaScript`!
|  |
| :-: |
| جابهجایی جدولها فقط با `input` رادیویی و `:checked` |
این بخش باید **داخل `chapter-group-stage`** باشد:
| **عنصر** | `data-testid` | **شرط** |
| :-: | :-: | :-: |
| عنصر دربرگیرنده | `group-tabs` | باید `aria-label` داشته باشد |
| نوار برچسبها | `tab-strip` | شرط خاصی ندارد؛ فقط باید وجود داشته باشد |
| سه `input[type="radio"]` | `tab-radio-a` تا `tab-radio-c` | یک مقدار `name` مشترک و غیرخالی؛ اولی از ابتدا `checked` |
| سه `label` | `tab-label-a` تا `tab-label-c` | مقدار `for` برابر `id` عنصر `radio` متناظر |
| سه پنل | `panel-group-a` تا `panel-group-c` | فقط پنل فعال باید قابل مشاهده باشد |
| سه جدول | `table-group-a` تا `table-group-c` | هرکدام یک `thead` با **دستکم سه** عنصر `th` |
+ جدول `table-group-a` باید دقیقاً چهار عنصر `tr` در `tbody` داشته باشد و یکی از تیمهایش `Iran` باشد.
+ پنلهای غیرفعال نباید قابل مشاهده یا قابل تعامل باشند. میتوانید از `display: none` یا `visibility: hidden` استفاده کنید. فقط کم کردن `opacity` کافی نیست، چون عنصر با `opacity: 0` هنوز از نظر سیستم داوری قابل مشاهده خواهد بود.
+ استایل برچسب فعال باید با سلکتور `:checked` نوشته شود، نه با یک کلاس دستی.
## **آمار و جام**
آمارها داخل عنصری با `data-testid="bento-grid"` قرار میگیرند که در `CSS` مقدار `display: grid` میگیرد. پنج کاشی دارد:
`stat-tile-matches`، `stat-tile-nations`، `stat-tile-cities`، `stat-tile-goals` و `stat-tile-days`
+ پنج کاشی آمار با عددهایشان و چیدمان `grid` از قبل آمادهاند و `data-testid` خودشان را دارند. عدد `104` هم بهصورت **متن واقعی داخل `HTML`** نوشته شده و باید همانجا بماند؛ اگر بعداً با `counter()` کار کردید، متن اصلی را از `DOM` برندارید چون سیستم داوری متن را از `DOM` میخواند.
+ عنوان آمادهٔ این بخش را به `h2` تبدیل کنید و `data-testid="stats-title"` بگیرد.
+ کاری که در این بخش میماند، شمارش تدریجی عددهاست.
|  |
| :-: |
| کاشیهای آمار با شمارش تدریجی اعداد |
متغیرهای معمولی `CSS` نوع دادهٔ مشخصی ندارند و مرورگر نمیتواند مقدارشان را بهتدریج تغییر بدهد، پس نمیشود مستقیماً با آنها عدد را شمارشی بالا برد. با `@property` نوع متغیر را ثبت کنید و آنوقت مرورگر میتواند مقدارش را از عددی به عدد دیگر ببرد. سپس همان مقدار را با `counter()` نمایش دهید:
```css style.css css
@property --num {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
```
در قسمت آخر یک جام قرار دارد. **خود تصویر جام از قبل با `SVG` کشیده شده است** و لازم نیست چیزی طراحی کنید:
+ روی عنصر `svg` جام مقدار `data-testid="trophy-svg"` را بگذارید؛ عنصر `title` داخلش از قبل هست.
+ برای همین `svg` انیمیشن ورود بنویسید تا با رسیدن به بخش پایانی ظاهر شود؛ مقدار `animation-name` آن نباید `none` بماند.
## **پیادهسازی نوار پیشرفت کنار صفحه**
+ عنصر با `data-testid="progress-rail"` که باید `nav` باشد، `aria-label` داشته باشد و در `CSS` مقدار `position: fixed` بگیرد.
+ داخل آن عنصری با `data-testid="rail-dots"` که **دقیقاً هفت** عنصر `a` دارد، یکی برای هر بخش: `rail-dot-hero`، `rail-dot-group-stage`، `rail-dot-knockouts`، `rail-dot-cities`، `rail-dot-nations`، `rail-dot-stats` و `rail-dot-final`. هرکدام `href` دارند که به `id` بخش متناظر اشاره میکند و هرکدام یا متن دارند یا `aria-label`.
+ دو عنصر روی هم: `rail-track` که مسیر خالی است و `rail-fill` که پر میشود. هر دو تزئینیاند و باید `aria-hidden="true"` بگیرند.
پیشرفت نوار باید به اسکرول کل صفحه متصل باشد و نباید به ورود یک عنصر به `viewport` وابسته باشد. پس اینجا از `scroll()` استفاده کنید و تغییر ارتفاع را با `scaleY` انجام دهید:
```css style.css css
[data-testid="rail-fill"] {
transform-origin: top;
animation: fill linear both;
animation-timeline: scroll(root block);
}
```
## **پیادهسازی** `footer` **و حالت کاهش حرکت**
محتوای فوتر از قبل نوشته شده است؛ `div` بیرونی را به `footer` و `div` منوی داخلش را به `nav` تبدیل کنید و این `data-testid`ها را بگذارید:
+ `footer-brand` روی عنصری که متن `Road to Glory` را دارد.
+ `footer-nav` روی منوی فوتر، که `aria-label` هم میخواهد.
+ `footer-copy` روی عنصری که عدد `2026` را دارد.
+ `back-to-top` روی لینک بازگشت به بالا؛ مقدار `href` آن از قبل `#top` است.
در `CSS` یک مورد باقی میماند: یک بلوک `@media (prefers-reduced-motion: reduce)`.
برخی کاربران به دلیل حساسیت به حرکت، انیمیشن را در سیستمعاملشان غیرفعال میکنند. در صورت فعال بودن این تنظیم، انیمیشنهای تزئینی نباید اجرا شوند. **داخل همین بلوک باید برای `rail-fill` هم قانون بنویسید** و آن را در حالت پرشدهٔ نهایی ثابت کنید تا کاربر همچنان نوار پیشرفت را ببیند.
# **آنچه سیستم داوری بررسی میکند**
سیستم داوری کوئرا رنگها، فونتها، فاصلهها و اندازههای پیکسلی را بررسی نمیکند. ظاهر صفحه لازم نیست دقیقاً مثل تصاویر نمونه باشد. آنچه سیستم داوری کوئرا بررسی میکند به شرح زیر است:
+ **وجود عناصر** با `data-testid`های گفتهشده و نوعشان در مواردی که ذکر شده
+ **تعداد عناصر** در مواردی که ذکر شده (هفت نقطهٔ نوار، پنج کاشی، دستکم دوازده `li`، چهار `tr`)
+ **مقادیر** `href` و ویژگیهای `aria`
+ چند ویژگی محاسبهشدهٔ `CSS`: `position`، `overflow`، `scroll-snap-align`، `animation-name`
+ **رفتار** برچسبهای گروه پس از کلیک
+ در **بلوک** `prefers-reduced-motion` باید نام `rail-fill` هم بیاید. انیمیشنهای تزئینی را متوقف کنید ولی نوار پیشرفت را در حالت پرشدهٔ نهایی نگه دارید تا اطلاعات پیشرفت همچنان در دسترس بماند.
# **آنچه باید آپلود کنید**
- **توجه:** فایل زیپ شما باید دقیقاً شامل این دو فایل باشد:
```plaintext
index.html
styles.css
```
- **توجه:** هر دو باید در **ریشهٔ** فایل زیپ باشند، نه داخل یک پوشهٔ اضافه. اگر مثلاً همهچیز را داخل پوشهای به اسم `answer` بگذارید، مسیرهای موردانتظار پیدا نمیشوند و داوری اجرا نمیشود.
- **توجه:** نام فایلها را **تغییر ندهید** و فایل سومی اضافه نکنید؛ سیستم داوری کوئرا فقط همین دو فایل را بررسی خواهد کرد.
- **توجه:** انیمیشنهای وابسته به اسکرول یعنی `animation-timeline` و `view()` و `scroll()` هنوز در همهٔ مرورگرها یکسان پشتیبانی نمیشوند. `@property` وضعیت بهتری دارد و در نسخههای جدید همهٔ مرورگرهای اصلی در دسترس است. ارزیابی در سیستم داوری روی `Chromium` انجام میشود، پس پیش از ارسال پاسخ را در `Chrome` اجرا و آزمایش کنید.
ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.