Компонент · рівень 2

Щабель лояльності

Два незалежні механізми на одному екрані: драбина персональної знижки за суму покупок за весь час і журнал руху бонусів. Плюс значок рівня, який ходить по всьому кабінету. Найдорожча помилка етапу знайшлась саме тут: колонку залишку бонусів на телефоні неможливо було побачити.

рівень 2design/system/components/loyalty-rung.css260 рядків90 селекторів84 правила3 екрани + значок на 14
Анатомія: драбина

Розмітка звідси - та сама, що на account-loyalty.html, скопійована рядок у рядок. Значок рівня ставить uivTiers() з design/_nav.js, як і на екрані.

Бонусний рахунок
124 ₴
бонусів на рахунку · 1 бонус = 1 ₴ · нараховуємо ~1% з покупки
Рівень лояльності
🥈 Срібло · персональна знижка −5%
Куплено на 18 400 ₴ за весь час · до 🥇 Золота ще 6 600 ₴
🥉 Бронза−3%✓ отримано
🥈 Срібло−5%ваш рівень
.loy-two ставить два механізми поруч від 760 і одне під одним нижче. Вони не зливаються в одну картку навмисно: у них різні правила, і головне - сума покупок не згорає, а бонуси згорають через 3 місяці
1Рядок рівня - .lt, 18px дисплейною. Значок метала попереду ставить не розмітка, а обхід тексту
2Смуга - .lbar 8px, заливка --bg-action у капсулі. Єдине місце в компоненті, де з'являється помаранчевий
3Підпис - .ls, і числа в ньому моноширинні: суму порівнюють з сумою, а не читають як текст
4Драбина - .ladder, три щаблі в ряд. Показані ВСІ, а не тільки наступний: людина має бачити, куди веде дорога
5Щабель - .lrung у чотирьох станах, нижче
Чотири стани щабля
покупець, який уже купував - account-loyalty
🥉 Бронза−3%✓ отримано
🥈 Срібло−5%ваш рівень
покупець до першого замовлення - account-empty
🥈 Срібло−5%ще не відкрито
🥇 Золото−8%ще не відкрито
драбина завжди має рівно три щаблі - стільки рівнів у програмі. Чотирьох станів на одній драбині не буває: .done, .now і .next - це людина, яка вже купувала, .next + два .locked - та, яка ще ні
СтанЩо робитьЧому так
.doneсвітле тло, щільна рамка, підпис --text-successпройдене не святкують: воно просто перестає бути питанням
.nowрамка --line-action + смужка 2px згори всерединієдиний акцент на драбині. Помаранчевий тут маркер поточного місця, а не дія
.nextпунктирна рамкапунктир каже «ще не ваше» без жодного слова і без жодного кольору
.lockedназва щабля йде в --text-mutedдля нового покупця відкритих щаблів немає, і драбина не має виглядати як помилка

.done, .now і .next стоять на account-loyalty та account; .locked - на account-empty, де покупець ще не зробив жодного замовлення.

Журнал, і колонка, до якої не можна було дотягнутись

Головна знахідка кроку. .led-wrap ніс overflow: hidden, а таблиця журналу на телефоні ширша за обгортку.

Рух бонусів

Як нараховуються →
ДатаОпераціяБонусиБаланс
12.06.2026Нараховано за замовлення № 10432+23124 ₴
03.06.2026Використано в кошику (№ 10418)−40101 ₴
21.05.2026Згоріли бонуси (термін дії 3 міс.)−12129 ₴
02.05.2026Нараховано за замовлення № 10355+6110 ₴
Показано і нарахування, і списання. Ставка нарахування й термін дії - попередні [?].
звузьте вікно нижче 480 і потягніть таблицю вбік: колонка «Баланс» доїжджає, а шапка і виноска лишаються на місці
ШиринаОбгорткаТаблицяЗрізано від колонки «Баланс» (87px)
360328411видно 3px із 87
390358411видно 33 із 87
430398411видно 72 із 87
720+688686вміщується цілком

І доскролити було неможливо - саме це робить hidden. Перевірено справжнім жестом, а не присвоєнням scrollLeft: програмна прокрутка працює і при hidden, тому та перевірка нічого не доводила. Свайп на 360 до правки: scrollLeft лишається 0, від колонки видно 3px. Після: scrollLeft 85, видно всі 87.

Змінна: як поводиться таблиця, ширша за свою обгортку. Значення: вона прокручується. Чому: інакше покупець не бачить власного залишку бонусів на пристрої, під який магазин будують у першу чергу. Візерунок не вигаданий - overflow-x: auto з прихованою смугою вже стоїть у account-shell.css, chip.css, hero.css і pdp-tabs.css.

Друга половина правки знайшлась у прийманні, а не в декларації

Шапка й виноска - діти обгортки, тож щойно обгортка поїхала, вони поїхали з нею: заголовок «Рух бонусів» опинявся на -68 і зникав з екрана рівно тоді, коли приїжджала колонка балансу. Пришпилені до лівого краю через position: sticky; left: 0 - лишаються на +1, а таблиця йде під ними. Рухатись мала тільки таблиця.

Чого тут навмисно немає

Смуга прокрутки прихована (scrollbar-width: none), як у чотирьох інших прокрутках системи. Це означає, що єдина підказка «далі є ще» - три видимі пікселі колонки. Достатньо цього чи потрібен градієнт біля краю - питання етапу 09, і я його не вирішую вигаданою тінню.
Журнал: три види рядка
РядокЩо робитьЧому так
.amt.pos--text-bonus, найважча ваганарахування - це те, за чим приходять у журнал
.amt.neg--text-secondaryсписання не помилка: людина сама витратила бонуси в кошику
tr.burnтло --bg-bonus-soft, сума --text-warningзгоряння - єдине, що сталось без участі покупця, і єдине, що варте окремого кольору. Бонуси діють 3 місяці, і журнал це показує, а не ховає

Дата й обидві числові колонки моноширинні: у журналі колонку читають згори вниз, порівнюючи, а не зліва направо.

Два стани, що є в сірому шарі й немає в кольорі

Перепись каже, що чотири селектори файлу не знаходять нічого на 40 кольорових екранах. Перевірив кожен - жоден не мертвий код.

🥇 Золото · персональна знижка −8% · максимальний рівень
Куплено на 24 800 ₴ за весь час · максимальний рівень досягнуто
Ще немає руху бонусів Зробіть перше замовлення - і тут з'являться нарахування та списання.
розмітка обох узята з wireframes/account-loyalty-max.html і wireframes/account-loyalty-empty.html - вони існують, але лише в сірому шарі
СелекторКольорових екранівЩо це насправді
.lbar.maxed i0сірий близнюк є: wireframes/account-loyalty-max.html, і _wf.css:1307 це правило має. Той самий випадок, що .tband на 7.44
.led-empty, .led-empty b0сірий близнюк є: wireframes/account-loyalty-empty.html, _wf.css:1325-1326
.uiv-tier.t0було 0, стало 3не стан, якого немає - помилка, яка його ховала. Нижче
Значок рівня, і нуль, який виявився хибним
t0 базовий
t1 бронза
t2 срібло
t3 золото
одне креслення, чотири класи: метал несе клас, а не окремий файл. t0 приглушений, t1 бронзовий, t2 сталевий, t3 акцентний

Значок ставить не розмітка: uivTiers() обходить текст сторінки і міняє емодзі металу на знак системи - так само, як uivCurrency() робить з гривнею. Тому значок стоїть на 14 екранах кабінету, а не тільки на трьох екранах лояльності.

Помилка, яку знайшло приймання

Обхід питав if(UIV_TIER_EMOJI[part]), а базовий рівень у таблиці має номер 0, який у JavaScript хибний. Три метали з чотирьох ставали знаком системи, а четвертий лишався сирим кольоровим емодзі - на account-empty.html, екрані, який бачить покупець до першого замовлення. Заміряно до правки: 3 сирі емодзі на сторінці, 0 значків .uiv-tier.t0. Питання виправлено на те, яке код мав ставити: if(part in UIV_TIER_EMOJI) - чи є цей емодзі в таблиці, а не чи ненульовий його рівень. Після: 0 сирих, 3 значки.
Вузол 8.7: публічна сторінка лояльності -> 12.8

Це відкрило рішення власника: «журнал бонусів належить і 7.4, і 8.7». Ця відповідь варта більше за два екрани, які вона розсудила, бо вона вирішує форму всього додатку. table.led, .led-wrap, .led-head і .led-note тут ПЕРЕВИКОРИСТАНІ байт у байт - публічна сторінка показує той самий журнал, що й кабінет - а цей блок додає лише те, чого в кабінеті справді немає.

Що зміряно, перш ніж написано правило, і це більша частина екрана. Сірий блок вузла 8.7 оголошує .loy-mechs, .loy-card, .loy-tbl, .loy-faq, .loy-q, .loy-h2, .loy-qmark і десять імен me-*. Прочитані проти кольорового вузла 7.4, усі вони вже існують під іншими іменами: .loy-two - це сітка двох механізмів, .acard + .ah + .mech-kicker - картка механізму, .loy-tbl - це table.led, .loy-q - це .qaitem, .loy-h2 - це section-head, .loy-qmark - це .qmark з base.css, а вся родина me-* - це .lt + .lbar + .ls поруч із .big + .u + .warn. Опублікувати їх означало б записати ті самі десять рішень удруге під новими іменами.

Тому додано три речі й більше нічого: герой лендинга (кабінет нікому не представляється), драбина РЯДКАМИ і персональна смуга, яка є КОРОБКОЮ навколо вже наявних частин.

Знижка, яка не згорає, і бонуси, які повертаються

Два незалежні механізми: персональна знижка за суму покупок за весь час і бонусний рахунок з нарахуванням близько [?]1% з кожного замовлення.

Персональна смуга, якою вона замінюється для того, хто вже увійшов:
🥈 Срібло · персональна знижка -5%
Куплено на 8 400 ₴ за весь час · до 🥇 Золота ще 6 600 ₴
124 ₴
бонусів на рахунку · 1 бонус = 1 ₴
Драбина рядками - пояснювач, а не смуга стану:
🥉Бронзавід 5 000 ₴ за весь час-3%
🥈Срібловід 15 000 ₴ за весь часви тут-5%

Персональна смуга несе class="loy loy-me" навмисне: усе всередині неї - це власні .lt, .lbar і .ls обсягу .loy, а коробка додає край і другу колонку. Розділ на 620, не на 760 чи 800 із сірих блоків: жодне з тих чисел не стоїть у реєстрі точок, а bp.mjs валить прогін на третій.

Пороги в демо - [?]. 5 000 / 15 000 / 30 000 і три відсотки не мають джерела в даних; рішення 3 в CLAUDE.md тримає їх відкритими, і мітка каже це вголос замість того, щоб число тихо стало правдою.

Коли вживати

Драбина - в кабінеті: на огляді й на сторінці лояльності. Журнал - тільки там, де є бонуси, тобто на сторінці лояльності. Значок рівня - будь-де, де названо метал, і ставить його обхід, а не автор сторінки.

Два механізми не змішуються. Знижка за суму покупок за весь час не згорає ніколи; бонуси згорають через 3 місяці. Це різні речі з різними правилами, тому в них дві картки, а не одна з двома числами.

Правило і антиправило

Правило

Показуй усю драбину, включно з тим, що вже пройдено. Щабель має сенс лише в ряду інших: «−5%» саме по собі не число, а «−3% · −5% · −8%» це дорога.

Антиправило

Не роби з лояльності гру. Ні відліку до згоряння, ні святкування переходу, ні бейджів. Принцип 4: серйозно для тих, хто ставиться серйозно. Згоряння бонусів названо прямо і один раз.
Холостий контроль

Сторінка звіряє себе з файлом: 19 власних класів і 9 станів оголошено в loyalty-rung.css. Нижче - ті, яких на цій сторінці немає ні в демо, ні в розмітці.

ПОВЕДІНКА НА ШИРИНІ - етап 10, крок 4

Від якого порогу: жодного - запит зник. Було @media (min-width: 760px), числа немає в реєстрі. Що саме міняється: пара карток лояльності стає поруч, коли для кожної є --grid-col-min-panel (19rem), через auto-fit. Карток дві, тож auto-fit тут точний, а не приблизний: третьої розкладки він вигадати не може. Підлога спершу була 22rem і це було зміряно неправильно - вона сходилась із запитом на 360 і на 1280 і розходилась між ними, бо кабінет бере собі колонку навігації на 960 і лишає сітці 628px.
Рух

Робота: ВІДПОВІДЬ. Рухається: колір посилання в журналі. Тривалість --dur-fast (150ms), крива --ease-standard. Значення читаються через var(): підміна токена в браузері веде за собою кожне з них, тобто числа у файлі немає. При prefers-reduced-motion рух зникає, а стан лишається.

Токени

Прочитані з loyalty-rung.css, не переписані руками: 20 семантичних ролей і 22 примітиви.

Семантичні ролі (20)Примітиви (29)
--bg-action --bg-bonus-soft --bg-inverse --bg-page --bg-sunken --bg-surface --elevation-1 --line-action --line-hair --line-strong --mark-tier-bronze --mark-tier-silver --text-action --text-body --text-bonus --text-muted --text-primary --text-secondary --text-success --text-warning--container-text --dur-fast --ease-standard --font-body --font-display --font-mono --fs-12 --fs-14 --fs-16 --fs-18 --fs-20 --fw-black --fw-bold --fw-semibold --grid-col-min-panel --lh-airy --ls-caps --radius-12 --radius-16 --radius-8 --radius-pill --size-26 --space-12 --space-16 --space-2 --space-24 --space-32 --space-4 --space-8

Два метали лояльності стали ролями. До кроку 7 етапу 08 файл читав примітиви --brown-600 і --slate-400 напряму, і це було єдине таке місце в компоненті: дві з чотирьох міток рангу не йшли за темою, поки дві сусідні (t0 на --text-muted, t3 на --text-action) йшли. Тепер там --mark-tier-bronze і --mark-tier-silver, а значення лишилися ті самі: мітка це гліф, а не текст, її межа 3:1, і обидві її проходять в обох темах - бронза 4.66 у світлій і 3.87 у темній, срібло 3.86 і 4.67. Метал упізнають за власною світлістю, тож підганяти темну редакцію під світлу означало б перестати бути сріблом.

Розмітка
<div class="acard loy"> <div class="lt">🥈 Срібло · персональна знижка −5%</div> <div class="lbar"><i style="width:62%"></i></div> <div class="ls">Куплено на <b>18 400 ₴</b> за весь час…</div> <div class="ladder"> <div class="lrung done"><span class="rn">🥉 Бронза</span>−3%<span class="rs">✓ отримано</span></div> … </div> </div>

Ширина смуги стоїть у style прямо в розмітці, бо це дані, а не оформлення: 62% це стан рахунку, і в CSS йому місця немає. Так само працює прогрес у кошику.

Де вживається

Якорі: .loy .led-wrap .uiv-tier

Заміряно в браузері на 360, 390, 720 і 1280 по всіх 40 кольорових екранах: драбина 3 рази (account, account-loyalty, account-empty), журнал 1 раз (account-loyalty, 6 рядків, з них один .burn), значок рівня - на 14 екранах кабінету й чекауту.

Мертвих селекторів: 0 з 49. Чотири не знаходять розмітки, і жоден з них не є зайвим кодом - три чекають кольорового близнюка, четвертий чекав виправлення помилки.

Немає в коді -> етап 09

Чого бракує

  • Двох кольорових екранів. «Максимальний рівень» і «порожній журнал» існують у сірому шарі й не мають кольорових двійників. Це не рішення про значення, це робота, яку видно й досі не зроблено
  • Підказки, що таблиця прокручується. Смуга прихована, і єдиний сигнал - три видимі пікселі колонки
  • Усі числа тут [?]: пороги драбини, відсотки знижки, ставка нарахування, 3 місяці до згоряння. Так і має бути до реальних даних, і стенд називає це вголос, а не малює правдоподібне
  • Немає стану «бонуси згорять днями» у самій драбині - попередження живе в сусідній картці .abonus, а щабель про це не знає

Не домальовано наперед і не замовчано: етап 09 стартує з цього списку, зведеного в design/kit/docs/architecture.md.