code6 · дизайн-система «Federal»

Единая система дизайна

Форум, база знаний, TERMINAL, страница приёма и лендинги живут в разных технологиях, но должны опознаваться как один продукт. Обеспечивается это не одинаковой вёрсткой, а общими именами токенов, одним шрифтом, одним набором иконок и едиными правилами применения.

Цвет

Бренд

Четыре цвета. Чёрный и белый являются основными, синий отвечает за действия на страницах, красный — за акцент и предупреждение.

Чёрный

--c6ColorBlack · #0F0F0F

Белый

--c6ColorWhite · #FFFFFF

Синий

--c6ColorBlue · #1B6FE0

Красный

--c6ColorRed · #C8102E

Заливка и текст разведены. --c6ActionFill используется для кнопок, активных переключателей и заполнения прогресса. --c6ActionInk — для ссылок и текстовых действий. Если элемент только сообщает состояние, он берёт статусный токен, а не синий цвет действия.

Действия и статусы

Синий связан с действием, переходом или активным процессом. Статусы описывают результат или состояние и не меняются при смене акцента: одно значение — один смысл на всех поверхностях.

ТокенСветлаяТёмнаяЗначение
--c6ActionFill
#1B6FE0
#4A93FFГлавное действие, активный выбор, заполнение прогресса
--c6StatusWaiting
#B8791A
#E0A33CОжидание, срок на исходе, ресурс заканчивается
--c6StatusSuccess
#037A58
#1FC08EОдобрено, пройдено, набор открыт
--c6StatusDanger
#C8102E
#FF6B75Отказ, блокировка, стоп-фактор
--c6StatusPro
#7A3EA1
#B98AE0Подписка Boosty: приоритет, объединения, платные механики
Фиолетовый нигде больше не появляется. Именно поэтому он опознаётся мгновенно: увидел фиолетовое — это про подписку. Как только он станет декоративным хоть в одном месте, значение размоется и восстановить его будет нельзя.
Инвертированный элемент меняет местами текст и поверхность. Если фон взят из --c6TextPrimary, то цветом текста становится --c6SurfaceRaised — и никак иначе. Токен --c6TextOnInverse здесь не подходит: он про тёмную плашку подвала, которая тёмная в обеих схемах, поэтому в тёмной схеме даёт белое по белому.

Поверхности и текст

ТокенСветлаяТёмнаяНазначение
--c6SurfaceCanvas#F1F3F7#131A24Полотно страницы
--c6SurfaceRaised#FFFFFF#1A2130Карточки и панели: всегда светлее полотна
--c6SurfaceSunken#F7F8FC#141B25Поля ввода, блоки кода
--c6SurfaceInverse#212936#0C1119Подвал, плашки медиа
--c6TextPrimary#212936#E8ECF3Заголовки и основной текст
--c6TextSecondary#5C6377#A2ACBDАбзацы и описания
--c6TextFaint#667085#8A94A6Подписи и метаданные
--c6TextDisabled#8A91A3#6E7A8CВыключенные элементы, недоступные подписи, неактивные иконки
--c6BorderDefault#E1E5EE#28313FГраницы карточек и секций

Департаменты

Доменная подпалитра под префиксом c6. Цвет опознаёт службу и живёт только в шапке карточки, полосе, маркере списка или бейдже с аббревиатурой. Карточка остаётся нейтральной, ссылки внутри остаются синими.

Гражданский — зелёный, и он равноправен. Нейтральный оттенок ставил бы CIV ниже служб в развилке на лендинге, хотя по устройству мира всё наоборот: гражданские создают события, на которые службы реагируют. Зелёный держит вес рядом с синим LSPD и красным LSCoFD, поэтому цветная плашка у CIV теперь есть и в развилке тоже.
Цвет департамента никогда не единственный носитель информации. Рядом с цветом всегда стоит аббревиатура или название: CIV, LSPD, SAHP, LSCoFD, FIB, DOT, DD или Связь. Плашка без подписи не используется.

CIV · --c6DepartmentCiv

LSPD / LSSD · --c6DepartmentLspd

SAHP · --c6DepartmentSahp

LSCoFD · --c6DepartmentFd

FIB · --c6DepartmentFib

DOT · --c6DepartmentDot

DD · --c6DepartmentDd

Связь · --c6DepartmentComms

Ни одно значение подпалитры не повторяет семантический токен. Иначе на экране возникает вопрос «это статус или принадлежность». Поэтому у пожарных свой красный, а не тот же, что у отказа, а диспетчерская не бывает фиолетовой: фиолетовый зарезервирован за подпиской, и плашка департамента в том же оттенке нарушала бы правило прямо в интерфейсе.

Где брать ступень

У каждого департамента есть несколько ступеней одного цвета. Выбирается не новый оттенок, а подходящая ступень из уже заданной подпалитры.

КонтекстКандидатУчастникКомандование
Сайт, светлая схемаDeep, белый текстBase, белый текстLight, тёмный текст
Сайт, тёмная схемаBaseLightBright
DiscordLightBrightBright, секция и иконка
В тёмной схеме департаменты не переопределяются. Компонент выбирает следующую ступень той же подпалитры. Значение токена остаётся прежним, меняется только то, какая ступень применяется в конкретном месте.

Правила подпалитры

ПравилоЗначение
Где живёт цветШапка карточки и маркер списка. Заливкой карточки, текстом и кнопками не бывает
СтолкновенияНи одно значение не совпадает с семантическим токеном и не лежит рядом с ним
ГражданскийЗелёный, равноправный со службами. Показывается с плашкой везде, включая развилку на лендинге
Цвет не одинРядом с плашкой всегда аббревиатура или название. Цвет опознаёт быстрее, но информацию несёт текст
Слитые департаментыLSPD и LSSD в US — один департамент. --c6DepartmentLssd оставлен алиасом на --c6DepartmentLspd
Текст на плашкеНа тёмной ступени используется белый текст, на светлой — основной текстовый токен
Тёмная схемаБерётся следующая, более светлая ступень той же подпалитры
Новый департаментДобавляется только как новая доменная подпалитра --c6Department*, без заимствования статусных цветов

Именование

Federal — имя дизайн-системы продуктов code6, но не технический namespace. В коде всё живёт под единым префиксом c6: базовые значения, семантические токены и компоненты. Семантический токен может ссылаться на базовый, но обратное направление запрещено: иначе смена акцента потребует правки каждого компонента.

СлойПрефиксОтвечает на вопросПримеры
Базовое значение--c6Color*, --c6Department*Какой это цвет или доменная подпалитра?--c6ColorBlue, --c6ColorRed, --c6DepartmentLssd
Семантический токен--c6Surface*, --c6Text*, --c6Action*Для чего это используется?--c6SurfaceCanvas, --c6TextPrimary, --c6ActionFill
Компонент.c6*Где применяется?.c6ButtonPrimary, .c6StatusPillWaiting, .c6DepartmentBandSahp
:root {
  --c6ColorBlue: #1B6FE0;                  /* базовое значение */
  --c6ActionFill: var(--c6ColorBlue);      /* семантический токен */
}

.c6ButtonPrimary {                         /* компонент читает семантику */
  background: var(--c6ActionFill);
  color: var(--c6TextOnFill);
}
Почему классы тоже с префиксом. Bootstrap владеет именами card, btn, row, badge. Любой наш класс без префикса рано или поздно столкнётся с его обновлением. Префикс c6 снимает вопрос навсегда и заодно делает видимым, где в разметке наш компонент, а где утилита фреймворка.

Поверхности

Схема — свойство поверхности, а не продукта. Форум и TERMINAL тёмные по своей природе: первый показывает фоновое видео и рендеры, второй имитирует служебную систему, в которую смотрят часами. База знаний и страница приёма светлые: там читают длинные тексты и принимают решения. Требовать от всех одной схемы бессмысленно, требовать одинаковых токенов обязательно.

ПоверхностьТехнологияСхемаОсобенности
ФорумInvision CommunityтёмнаяФоновое видео и рендеры, максимальная плотность навигации
База знанийDocsобе, тёмная по умолчаниюДлинные тексты, переключатель схемы у пользователя
TERMINAL и Элкавнутренняя CAD/MDTтёмнаяПлотные таблицы, статусы вызовов, долгие сессии
ПриёмPHP и Bootstrapобе, светлая по умолчаниюВоронка состояний, один целевой сценарий на экран
Лендинги и медиаразноепо задачеОбязательны шрифт, иконки и палитра
Критерий консистентности, который можно проверить. Компонент, перенесённый с одной поверхности на другую, не должен требовать правки цветов. Если требует — где-то остался хардкод вместо токена. Это единственная формулировка, которую можно вынести в линтер, а не обсуждать на глаз.

Типографика

Inter Variable — единственный интерфейсный шрифт на всех поверхностях. Переменное начертание даёт весь диапазон толщин одним файлом и оптический размер, за счёт которого мелкий текст остаётся читаемым, а крупный не выглядит рыхлым. Atlantis International — только логотип и вордмарк.

@font-face {
  font-family: "Inter";
  src: url("/fonts/InterVariable.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-display: swap;
}

:root {
  --c6FontSans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  --c6FontMono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --c6FontLogo: "Atlantis International", var(--c6FontSans);
}

Шкала

Девять ступеней, ни одной сверх. Крупные кегли адаптивны через clamp() и не требуют брейкпоинтов; мелкие фиксированы, потому что уменьшать их на мобильном нельзя.

ТокенДесктопМобильныйИнтерлиньяжВесГде
--c6FontSizeDisplay40301.12600Первый экран лендинга
--c6FontSizeH132261.18600Заголовок страницы
--c6FontSizeH224211.25600Заголовок блока
--c6FontSizeH319171.35600Заголовок карточки
--c6FontSizeLead17171.6400Подводка под заголовком
--c6FontSizeBody15151.6400Основной текст, кнопки
--c6FontSizeSmall13131.55400Текст в карточках, ссылки
--c6FontSizeCaption12121.5400Подписи, метаданные
--c6FontSizeMicro11111.45500Надзаголовки, капслок

Здесь всё решают люди

Заголовок страницы

Заголовок блока

Заголовок карточки

Подводка под заголовком — 17 пунктов, вторичный цвет

Основной текст интерфейса. Длина строки ограничена 74 знаками, дальше глаз теряет начало следующей.

Текст внутри карточек и списков

Подписи, даты, счётчики

Надзаголовок секции

7KQ2MX4B1P — моноширинный только для кодов и ключей

Обязательная адаптивность

ПравилоЗначение
Минимальный кегль12 px. Ниже нельзя нигде, включая подписи и сноски
Основной текст на мобильномНе меньше 15 px. Уменьшение на узком экране запрещено
Цель нажатияНе меньше 44 × 44 px, включая ссылки в плотных списках
Длина строки60–78 знаков. На широком экране ограничивается max-width, а не растягивается
Крупные кеглиТолько через clamp(), без брейкпоинтов и голых vw
Веса400 — текст, 500 — интерфейс и микро, 600 — заголовки. 300 и тоньше не применяются: на тёмной схеме разваливаются
Горизонтальный скроллОтсутствует на любой ширине от 320 px

Иконки

Lucide — единственный иконочный пак. Он контурный, с постоянной толщиной обводки и сеткой 24, поэтому не спорит с Inter и одинаково ведёт себя в обеих схемах. Смешивать паки нельзя: разная толщина линии заметна даже неподготовленному глазу и читается как небрежность.

shield · департаменты clock · сроки и ожидание triangle-alert · предупреждение check · пройденный шаг lock · недоступный шаг arrow-up · приоритет copy · копирование кода siren · службы
ПравилоЗначение
Размеры14, 16, 18, 20, 24. Других нет
Соответствие текстуИконка рядом с текстом равна его интерлиньяжу: 16 к тексту 13, 20 к тексту 15
Толщина обводки2 по умолчанию, 1.75 для размера 24 и выше
ЦветВсегда currentColor. Собственного цвета у иконки нет
СмыслОдна иконка — одно значение во всех продуктах. Список пар ведётся вместе с системой
ДоступностьДекоративная — aria-hidden="true". Несущая смысл — с текстовой подписью рядом
<i data-lucide="shield" width="20" height="20" aria-hidden="true"></i>

// После каждой перерисовки DOM иконки нужно пересоздать,
// иначе новые узлы останутся пустыми плейсхолдерами.
function renderScreen(screenName) {
  pageElement.innerHTML = buildScreenMarkup(screenName);
  if (window.lucide) {
    window.lucide.createIcons();
  }
}

Спейсинг и сетка

Базой берётся шкала Bootstrap. Она покрывает почти все случаи утилитами и избавляет от самодельных отступов, которые расходятся между страницами. Расстояния задаются в разметке утилитами, а не в CSS: p-4, px-lg-5, g-3, gap-2, mt-3. Токены --c6Space* нужны там, где утилита неприменима — внутри компонента.

ТокенУтилитаЗначениеГде применяется
--c6Space114 pxМежду иконкой и подписью, внутри чипа
--c6Space228 pxМежду строками карточки, gap в списках чипов
--c6Space3316 pxМежду карточками в ряду, отступ абзаца
--c6Space4424 pxПоля карточек и секций на мобильном
--c6Space5548 pxПоля секций на десктопе
ПравилоЗначение
Секцияp-4 p-lg-5 — 24 px на мобильном, 48 px на десктопе
Карточкаp-3, крупная — p-4
Рядыrow и g-3. Собственные grid-контейнеры только там, где Bootstrap не выражает раскладку
Вертикальный ритмОтступ между блоками кратен 8. Значения вне шкалы не вводятся
Порядок утилитРаскладка, отступы, оформление: d-flex justify-content-between align-items-center px-4 py-3
Почему Bootstrap, а не своя сетка. Своя сетка требует документации, поддержки и обучения каждого нового человека. Bootstrap уже знают, он закрывает адаптивность из коробки, а его минималистичная база не навязывает визуальный стиль — внешний вид всё равно задают наши токены. Экономия здесь не в коде, а в том, что раскладку не нужно объяснять.

Компоненты

Полный набор с вариациями собран в интерфейсе приёма. Здесь базовые — те, что обязаны выглядеть одинаково на всех поверхностях, и решения, из которых они выросли.

Кнопки

На экране одно главное действие. Вторичная кнопка появляется только там, где есть настоящий второй путь. Заблокированная главная кнопка не используется: если действие недоступно, главной становится другая.

Статусы

Два регистра одного смысла. Плашка — когда статус и есть содержание экрана. Точка со словом — когда статус сопровождает. Цвет означает одно и то же везде: в очереди на приёме, в списке у проверяющих, в карточке вызова TERMINAL.
Ожидает проверки Одобрено Отклонено Истекло Приоритет Boosty Так начинают все
Приём закрыт На проверке Одобрено Отклонено Истекло

Карточки и прогресс

Базовая

Поверхность на ступень выше полотна, тонкая граница, радиус 12.

Акцентная

Двухпиксельная синяя рамка вместо заливки — вес без введения пятого цвета.

Проблесковые огни

Рисунок вспышек повторяет настоящую мигалку: синяя сторона даёт две короткие вспышки, затем две даёт красная. Это не декоративный пульс, а узнаваемый ритм — он опознаётся боковым зрением и работает даже на четырёх пикселях высоты.

Полоса

Основной вариант. Ставится над шапкой страницы, над первым экраном или как разделитель перед важным блоком.

На светлом

На чёрном — свечение сильнее, приглушённое состояние темнее

Пара точек

Компактный вариант для строки статуса: смена идёт, сервер онлайн, трансляция в эфире. Рядом обязателен текст — сами по себе точки значения не несут.

Смена идёт · 34 человека на дежурстве

Бегущий отблеск

Индикатор процесса: загрузка страницы, ожидание ответа сервера, отправка формы. Заменяет обычный прогресс-бар там, где длительность неизвестна.

Фоновое свечение

Самый тихий вариант: два размытых пятна по краям, медленно дышащих в противофазе. Для первого экрана и пустых состояний. Под текстом допустим только в этом виде — размытие 60 пикселей и низкая непрозрачность не мешают читать.

Здесь всё решают люди

Отбор проходит каждый

Разметка

<div class="c6LightBar" role="presentation" aria-hidden="true">
  <span class="c6LightBarSegment"></span>
  <span class="c6LightBarSegment"></span>
</div>

<span class="c6LightDots" role="presentation" aria-hidden="true">
  <i class="c6LightDot c6LightDotLead"></i>
  <i class="c6LightDot c6LightDotTrail"></i>
</span>

<div class="c6LightSweep" role="presentation" aria-hidden="true"></div>

<section class="c6LightAmbient"> ... первый экран ... </section>

Правила огней

Главное ограничение. Красный в системе означает отказ и предупреждение. Мигающий красный рядом с формой или статусом кандидат прочитает как ошибку. Поэтому огни живут только в брендовых контекстах и никогда не появляются в одном экране со статусными элементами.
ПравилоЗначение
Частота2.5 вспышки в секунду. Порог WCAG 2.3.1 — три в секунду, превышать нельзя ни при каких обстоятельствах
КоличествоОдин элемент огней на экран. Два — уже аттракцион
СоседствоНе размещается рядом с чипами статуса, полем ошибки, карточкой отказа
Под текстомТолько фоновое свечение. Полоса и точки текст не подложены никогда
Порядок цветаСиний ведёт, красный отвечает. Обратный порядок не применяется
Доступностьaria-hidden="true" и role="presentation". Смысл несёт текст рядом, не анимация
Снижение движенияПри prefers-reduced-motion анимация останавливается, огни остаются статичными

Где применяется

Где не применяется

Логотип и маскот

У знака четыре исполнения и у каждого своя роль в движении. Основной логотип появляется, мини-логотип работает индикатором, маскот живёт в пустых состояниях. Общее ограничение одно: логотип нельзя искажать, поэтому анимации используют только непрозрачность, смещение и равномерный масштаб — ни перекоса, ни разного растяжения по осям.

Появление логотипа

Логотип поднимается на восемь пикселей и проявляется, следом под ним включаются огни. Порядок важен: сначала знак, потом мигалка — иначе внимание уходит на движение, а не на бренд.

Мини-логотип как индикатор

Самый узнаваемый индикатор из возможных: это не абстрактное кольцо, а собственный знак. Применяется там, где спиннер выглядел бы безлико — при первой загрузке страницы, в модальном окне, на заставке.

20 · 28 · 40

Скобки в движении

Развитие того же приёма: половины знака расходятся и сходятся, пока идёт работа. Здесь настоящие пути из code6_logo-mini.svg — именно потому, что знак живёт в разметке, а не картинкой, его половины можно двигать по отдельности.

Загружаем данные

Маскот

Кодик закрывает пустые состояния и технические экраны. Версия в очках закреплена за машинными контекстами: рядом со сгенерированным блоком и на долгом ожидании ответа модели. Это удобная договорённость — по одной картинке видно, что отвечает система, а не человек.

Маскот Кодик

Спокойное парение · пустые состояния

Маскот Кодик в очках

Ускоренное парение · система думает

Пустое состояние целиком

Пока ничего нет

Записи появятся, когда пройдёт первая смена

Правила

ПравилоЗначение
Свойства анимацииТолько opacity, translate и равномерный scale. Перекос и неравномерное растяжение искажают знак
Маскот и логотипКодик логотип не заменяет и рядом с ним в одной связке не ставится
ОчкиВерсия в очках закреплена за машинными контекстами. В остальных местах — обычная
Где маскота нетЭкраны отказа, истечения срока и блокировки. Там тон сухой, и милый персонаж читается как насмешка
КоличествоОдин маскот на экран. Минимальный размер 64 px
Мини-логотипИндикатор, фавикон, иконка приложения, водяной знак. Как подпись бренда в шапке не используется
Порядок в lockupСначала появляется логотип, затем включаются огни
Снижение движенияПри prefers-reduced-motion всё статично: логотип виден сразу, маскот не парит, знак-индикатор просто приглушён
<!-- связка: знак и огни -->
<div class="c6LogoLockup">
  <svg class="c6LogoSvg c6LogoFull c6LogoSizeSm c6LogoRise" viewBox="0 0 1057 217" role="img" aria-label="code6">
    <path class="c6LogoInk" d="..."/>
    <path class="c6LogoLead" d="..."/>
    <path class="c6LogoTrail" d="..."/>
  </svg>
  <div class="c6LightBar" role="presentation" aria-hidden="true">
    <span class="c6LightBarSegment"></span>
    <span class="c6LightBarSegment"></span>
  </div>
</div>

<!-- знак как индикатор: SVG встроен, поэтому красится токенами -->
<span class="c6MarkLoader c6MarkLoaderMd" role="presentation" aria-hidden="true">
  <svg class="c6LogoSvg c6LogoMark" viewBox="0 0 208 217">
    <path class="c6LogoLead" d="..."/>
    <path class="c6LogoTrail" d="..."/>
  </svg>
</span>

<!-- пустое состояние -->
<div class="c6EmptyState">
  <img class="c6Mascot c6MascotSm c6MascotIdle" src="/code6_codic_glasses.png" alt="">
  <p>Пока ничего нет</p>
</div>

Загрузка

Динамического содержимого у нас больше, чем статического: счётчик мест опрашивается раз в минуту, позиция в очереди обновляется на глазах у кандидата, таблицы вызовов в TERMINAL живут постоянно. Поэтому состояние загрузки — не исключение, а один из штатных видов блока, и выглядеть оно должно так же продуманно, как готовый экран.

Правило выбора одно. Форма будущего содержимого известна — скелет. Неизвестна или места нет — спиннер. Значение уже на экране и просто обновляется — ни то, ни другое, а подсветка на месте. Ошибка почти всегда в третьем случае: подменять число скелетом при каждом опросе значит мигать пользователю в лицо раз в минуту.

Скелет карточки

Геометрия скелета обязана совпадать с геометрией готового блока. Если после подстановки контента страница дёрнулась — скелет сделан неправильно, и лучше бы там стоял спиннер.

Скелет строки таблицы

В плотных таблицах скелет ставится внутрь настоящих ячеек — тогда ширины колонок держит сама таблица и после загрузки ничего не перестраивается.

КодДепартаментСтатусВремя

Спиннер

Три размера. Кольцо берёт цвет границы, дуга — currentColor, поэтому спиннер одинаково работает на карточке, в кнопке и на тёмной плашке без переопределений.

14 · 18 · 24

Кнопка

Подпись не удаляется, а скрывается — ширина кнопки не меняется, соседние элементы не сдвигаются, повторное нажатие блокируется через pointer-events.

Индикатор страницы

Тонкая полоса под шапкой на время перехода или тяжёлого запроса. В отличие от бегущего отблеска с огнями, здесь только синий: это процесс, а не брендовый момент.

Содержимое страницы

Изображение

Рамка держит размер и показывает подложку скелета, изображение появляется по load с плавным проявлением. Ни одного сдвига раскладки.

<div class="c6MediaFrame" style="aspect-ratio:16/9">
  <img class="c6MediaImage" src="..." alt="">
</div>

// Класс добавляется по событию загрузки, а не по таймеру:
// иначе на медленной сети картинка проявится раньше, чем придёт.
function bindMediaFadeIn(imageElement) {
  if (imageElement.complete) {
    imageElement.classList.add('c6MediaImageLoaded');
    return;
  }
  imageElement.addEventListener('load', () => {
    imageElement.classList.add('c6MediaImageLoaded');
  }, { once: true });
}

Живые значения

Числа, которые обновляются сами: свободные места, позиция в очереди, количество проверенных за сутки заявок, счётчик людей на смене. Их нельзя заменять скелетом при каждом опросе — вместо этого значение подсвечивается ровно в момент изменения.

Попробуйте

Свободно мест: 56

Устаревшие данные

Если опрос не прошёл, значение не прячется и не обнуляется. Блок приглушается, рядом появляется отметка времени последнего успешного обновления. Показать старое значение честно лучше, чем показать его уверенно.

56 мест

Обновляется каждую минуту

Данные от 21:04, связь потеряна

Обновление на месте

function updateLiveValue(valueElement, nextValue) {
  const nextText = String(nextValue);

  // Ничего не мигаем, если значение не изменилось:
  // подсветка без изменения читается как сбой.
  if (valueElement.textContent === nextText) {
    return;
  }

  valueElement.textContent = nextText;

  // Снять и вернуть класс мало — браузер не перезапустит анимацию,
  // пока не произойдёт пересчёт раскладки. Обращение к offsetWidth
  // и есть этот принудительный пересчёт.
  valueElement.classList.remove('c6ValueChanged');
  void valueElement.offsetWidth;
  valueElement.classList.add('c6ValueChanged');
}

Правила загрузки

ПравилоЗначение
Порог показаИндикатор не появляется первые 200 мс. Мелькание раздражает сильнее, чем ожидание
Минимальная жизньПоказанный индикатор держится не меньше 400 мс, иначе он мигнёт и исчезнет
Совпадение геометрииСкелет повторяет размеры готового содержимого. Сдвиг раскладки после загрузки — ошибка
Ширины скелетаМодификаторами c6SkeletonWide*, не атрибутом style
Пусто и загружаетсяРазные состояния. «Ничего не найдено» во время загрузки не показывается никогда
Повторный опросНе заменяет содержимое скелетом. Меняется только само значение
Ошибка опросаСтарое значение остаётся, блок приглушается, появляется отметка времени
Огни против спиннераБегущий отблеск с огнями — брендовый момент: загрузка страницы, заставка. Обычная загрузка данных — синий спиннер или скелет
Снижение движенияПри prefers-reduced-motion все шиммеры и вращения останавливаются, подложки остаются
Индикатор навигацииЗапускается по факту перехода, а не по клику: отменённый клик, модальное окно и переключение схемы его не включают
Повторный запускКаждый новый переход перезапускает анимацию с нулевого кадра. Повторное добавление класса само по себе её не перезапускает — нужен принудительный пересчёт раскладки
ОстановкаГасит индикатор только последний запрос. Ранний ответ не должен снимать индикатор более позднего перехода

Порог и минимальная жизнь индикатора

async function runWithLoader(task, loaderElement, options = {}) {
  const minimumDelay = options.minimumDelay || 200;
  const minimumVisible = options.minimumVisible || 400;
  let shownAt = 0;

  const showTimer = setTimeout(() => {
    loaderElement.hidden = false;
    shownAt = Date.now();
  }, minimumDelay);

  try {
    return await task;
  } finally {
    clearTimeout(showTimer);

    // Если индикатор успел появиться, он обязан прожить минимум,
    // иначе пользователь увидит вспышку вместо загрузки.
    if (shownAt) {
      const visibleFor = Date.now() - shownAt;
      if (visibleFor < minimumVisible) {
        await new Promise(resolve => setTimeout(resolve, minimumVisible - visibleFor));
      }
    }

    loaderElement.hidden = true;
  }
}

Сгенерированный блок

Контейнер для всего, что написала модель: ответы ассистента, автоматические сводки, предварительная оценка заявок, реплики NPC, подсказки в игре. У блока три обязательных признака — штриховая кромка, маркер происхождения и подпись с моделью и временем.

Почему без цвета. Синий занят действием, янтарный — ожиданием, зелёный — успехом, красный — отказом, фиолетовый — подпиской. Шестого свободного оттенка нет, а вводить его значит ослабить остальные пять. Происхождение контента — не статус, поэтому выражается текстурой: штриховка читается как «машинное» и не спорит ни с чем.

Готовый ответ

Сгенерировано terminal-assist · 16 сент, 21:04

По номеру 7KQ2MX4B1P найдено три записи: остановка на трассе 12 сентября, протокол о превышении 14 сентября, вызов эвакуатора 15 сентября.

Совпадений по розыску нет.

Сводка составлена автоматически и может содержать ошибки. Решение принимает человек.

Во время генерации

Генерируется terminal-assist

По номеру 7KQ2MX4B1P найдено три записи: остановка на трассе 12 сентября

До первого токена

Генерируется

Разметка

<article class="c6Generated c6GeneratedStreaming">
  <div class="c6GeneratedHead">
    <span class="c6GeneratedMark">
      <i data-lucide="sparkles" width="13" height="13" aria-hidden="true"></i>Генерируется
    </span>
    <span class="c6GeneratedMeta">terminal-assist · 16 сент, 21:04</span>
  </div>

  <div class="c6GeneratedBody">
    <p>Текст ответа<span class="c6GeneratedCaret" aria-hidden="true"></span></p>
  </div>

  <p class="c6GeneratedFoot">Сводка составлена автоматически и может содержать ошибки.</p>
</article>
// Класс состояния снимается, когда поток закончился.
// Кроме анимации он ничего не меняет, поэтому блок
// не «прыгает» в момент завершения.
function finishGeneratedBlock(blockElement, modelName, finishedAt) {
  blockElement.classList.remove('c6GeneratedStreaming');
  blockElement.querySelector('.c6GeneratedMark').lastChild.textContent = 'Сгенерировано';
  blockElement.querySelector('.c6GeneratedMeta').textContent = modelName + ' · ' + finishedAt;

  const caret = blockElement.querySelector('.c6GeneratedCaret');
  if (caret) {
    caret.remove();
  }
}

Правила разметки сгенерированного

ПравилоЗначение
Обязательные признакиШтриховая кромка, маркер, подпись с моделью и временем. Ни один не опускается
Никогда не как человеческоеСгенерированный текст не оформляется так же, как написанный участником или администрацией
ЦветНе берёт ни один статусный оттенок. Синий допустим только в индикаторе процесса
АнимацияТолько во время генерации. После завершения блок статичен
Смена состоянияСнятие класса не меняет геометрию: блок не смещается в момент завершения
ОговоркаТам, где на основе текста принимают решение, внизу стоит строка об автоматическом происхождении
ВложенностьБлок не вкладывается в другой такой же. Несколько ответов — несколько блоков подряд
Связь с правилом раскрытия. Автоматическая оценка заявки видна проверяющим, но не кандидату — решения по заявкам не комментируются. Маркер нужен именно внутренним поверхностям: он не даёт спутать машинную сводку с выводом человека и защищает от ситуации, когда модель ошиблась, а её текст прочитали как позицию администрации.

Где применяется

Правила кода

Именование и читаемость

ПравилоЗначение
РегистрcamelCase для переменных, функций, ключей объектов, CSS-токенов и классов
ИменаПолные слова. departmentIndex, а не di. currentScreen, а не s
СокращенияТолько общепринятые: id, url, css
КомментарииТам, где логика неочевидна: почему так, а не что делает строка
ОбфускацияВ исходниках не применяется. Минификация — задача сборки
ФункцииОдна функция — одна ответственность. Сборка разметки отдельно от логики состояния
function renderScreen(screenName) {
  const screen = screens[screenName];

  // Класс режима нужен вёрстке: от него зависит,
  // какие соседние блоки встают в пару на десктопе.
  pageElement.className = 'page mode-' + screen.mode;
  pageElement.innerHTML = screen.blocks
    .map(blockName => blockRenderers[blockName](screen.data, screen))
    .join('');

  bindInteractiveControls();
  refreshIcons();
}

CSS

ПравилоЗначение
Инлайн-стилиНе применяются. Ни атрибутом style, ни строкой стиля из JS
Динамические значенияЧерез класс-модификатор или CSS-переменную на элементе
ГруппировкаФайл организован по токенам: объявление, затем всё, что его читает. Блок компонента начинается с перечня токенов, которые он использует
АдаптивыМедиазапрос компонента лежит рядом с компонентом, а не в общей секции в конце файла
Хардкод цветаНе применяется. Любой цвет — только токен
Утилиты против CSSСначала утилита Bootstrap в разметке, свой класс — если утилиты не хватает
/* --- прогресс -----------------------------------------------------------
   Токены: --c6TrackEmpty, --c6ActionFill, --c6StatusWaiting
   ------------------------------------------------------------------------ */
.c6ProgressTrack { height: 8px; border-radius: 5px; background: var(--c6TrackEmpty); overflow: hidden; }
.c6ProgressFill { display: block; height: 100%; border-radius: 5px; }
.c6ProgressFillAction { background: var(--c6ActionFill); }
.c6ProgressFillWaiting { background: var(--c6StatusWaiting); }

@media (min-width: 900px) {
  .c6ProgressTrack { height: 10px; }
}
Почему запрет на инлайн-стили не формальность. Пока цвет живёт строкой в JS, тёмная схема физически невозможна: переменная не подставится в уже собранную строку. Один инлайн-стиль ломает всю двухсхемность — поэтому правило абсолютное.

Тон и раскрытие информации

Часть системы, а не отдельная редполитика: от неё зависит, какие элементы вообще существуют. Подробная инструкция обучает кандидата, а обученный кандидат проходит отбор, не будучи подходящим.

ПоказываемНе показываем
Статусы, даты, коды, срокиПричины отказа и комментарии проверяющих
Позицию в очереди и скорость обработкиОбещания сроков проверки
Требования и стоп-факторыСоветы по подготовке и разбор ошибок
Названия шагов отбораВнутренние процедуры, обучение, наставничество
Характер департаментовУстройство служб изнутри

Сдержанность касается раскрытия информации, а не интонации. «В этот раз не получилось» — нормально. «Вот что стоит подтянуть» — нет. Тему закрывает формулировка из правил сообщества: «Решения по заявкам не комментируются».