Дизайн-система консоли Arkvory
Реализованы светлая, тёмная и системная темы, переключение RU/EN, адаптивные экраны каталога, загрузки, истории и свойств файла. Это интерфейс рабочего хранилища: таблицы показывают данные API, индикатор подключения отражает успешное чтение каталога, без вымышленных графиков ёмкости или состояния кластера.
Добавлены вход по имени/паролю, экран зарегистрированных пакетов с фильтрами/сортировкой/группировкой и административные формы пользователей/групп. Пароль очищается после успешного входа или создания пользователя; сессия остаётся только в поле текущей вкладки и очищается при выходе. Сервер остаётся источником прав и результатов каталога.
Файлы и ответственность
| Файл | Ответственность |
|---|---|
apps/web/tokens.css | Палитра, семантические цвета, шрифты, размеры текста, отступы, радиусы, границы, тени, motion, размеры элементов и layout |
apps/web/style.css | Точка сборки CSS: явный порядок импортов, без копий компонентов |
apps/web/foundation.css, shell.css | Базовые элементы, типографика, навигация и каркас страницы |
apps/web/components.css, workspace.css, responsive.css | Общие карточки/формы/таблицы, предметные экраны и адаптивная компоновка |
apps/web/tooltips.css, src/tooltips.ts, motion.css | Контекстные подсказки и короткие анимации с поддержкой reduced motion |
apps/web/index.html | Семантическая разметка, labels, навигация, fallback-текст и ключи переводов |
apps/web/src/messages.ts, messages-en.ts, messages-ru.ts, guide-messages.ts | Типизированные EN/RU словари и интерполяция |
apps/web/src/i18n.ts | Обновление текста, placeholder, aria-label, статусов и дат без пересоздания форм |
apps/web/src/preferences.ts | Валидация и сохранение настроек, разрешение системной темы |
apps/web/src/appearance-init.ts | Применение темы до загрузки CSS и отрисовки интерфейса |
apps/web/src/shell.ts | Навигация экранов, переключатели языка и темы |
apps/web/src/feedback.ts | Понятные локализованные ошибки и состояния операций |
apps/web/src/guides.ts, apps/web/guides.css | Знакомство после установки и справочник API/CLI с общими токенами |
Модель токенов
tokens.css разбит на разделы: шрифт, отступы, форма, размеры, раскладка, размеры компонентов, слои, движение, светлая палитра с ролями, тёмные роли. Примитивы --palette-* задают исходные цвета. Компоненты используют роли --color-bg, --color-surface, --color-raised, --color-subtle (заголовки таблиц, наведение на строку, блоки команд), --color-text, --color-muted, --color-border, --color-accent, --color-on-accent и другие. Тёмная тема переопределяет роли в :root[data-theme='dark']; отдельная копия компонентов не нужна.
Состояния имеют по три роли: цвет текста, тонированный фон и рамку — --color-success*, --color-warning*, --color-error*, --color-info-bg/--color-info-border. Контраст цвета состояния на своём фоне проверяется тестом в обеих темах. Значок (.badge[data-tone], состояния зеркал и подключения) и баннер (.update-banner[data-tone] со значком alert/info) берут все три роли: состояние видно и словом, и фоном, а не одним цветом текста.
Геометрия: отступы --space-* идут по сетке 4 px (4, 8, 12, 16, 20, 24, 32, 40, 48); --radius-* (8 px у полей и кнопок, 12 px у карточек), --border-width, --control-height, --icon-size, --topbar-height (общая высота верхней полосы и строки бренда, чтобы их нижние границы совпадали), --sidebar-width, --aside-width, --page-gutter, --panel-padding. Типографика: --font-*, шкала --text-* 12/13/14/16/20 px (и 18 px у названия продукта, --text-brand), --weight-*, --leading-*, --tracking-*. Эффекты: --shadow-*, --motion-*, --opacity-disabled, --z-*.
Плотность рассчитана на рабочий инструмент:
- для мыши и тачпада поля и кнопки 32 px (
--control-height), малые 28 px; все элементы верхней полосы одной высоты; - текст 14 px, кнопки и подписи 13 px, заголовок страницы 20 px, заголовок карточки 16 px, подраздел 14 px;
- поля показывают значение обычным начертанием, а не весом подписи вокруг;
- отступы панелей 16 px (12 px на телефоне), между карточками и баннерами экрана один шаг
--section-gap(12 px), пункты меню 32 px, верхняя полоса 52 px; строки таблиц около 36–56 px; заголовки таблиц выровнены по своим ячейкам, действия строки — по концу строки; - без «воды»: экран не пересказывает свой заголовок вводным абзацем; неактивная «Загрузить ещё» скрыта; ход операции, который и так виден на экране (поиск, число строк, открытый артефакт), обновляет текст статуса через
quietFeedback, но не поднимает полосу статуса — её поднимают результаты и ошибки.
Сенсорный ввод (@media (pointer: coarse) в tokens.css) возвращает цели 44 px (--control-height, --control-small-height, --help-target, --nav-height) без отдельной копии компонентов. Это не layout-граница: тест брейкпоинтов проверяет только ширины. Для изменения плотности начните с --control-height, --panel-padding, --page-gutter и шкалы отступов. Для брендинга изменяйте семантические цвета обеих тем вместе. Не вставляйте hex/rgb/цветовые литералы в компоненты, обработчики или HTML. Не меняйте --color-muted только ради декоративной бледности: это цвет читаемого текста и placeholder.
Структурные значения 0, 100%, grid fractions, SVG-геометрия и техника visually hidden не являются визуальными токенами. Две layout-границы — 1100 и 760 CSS px — указаны в CSS media queries: custom properties не поддерживаются в условиях media queries. При изменении границ синхронно обновляйте media queries в tokens.css и соответствующих CSS-модулях. На узком экране навигация раскрывается кнопкой «Разделы», формы выстраиваются в колонку, каталог адаптируется без горизонтальной прокрутки, остальные широкие таблицы прокручиваются внутри именованного контейнера с keyboard focus.
Шрифты — локальный стек системных sans/mono. Внешние CDN, tracking и загрузка веб-шрифтов не используются. Название Inter в стеке используется только при наличии установленного шрифта.
Визуальный язык и иерархия
Светлая палитра использует холодные нейтральные поверхности и синий акцент; тёмная — глубокий синий фон и светлый циан. Семантический успех остаётся зелёным, ошибка — красной/розовой. Цвет сам по себе не сообщает результат операции. Градиент применяется к знаку бренда и мягкому фону рабочего пространства. Таблицы и формы имеют непрозрачные поверхности.
--color-input отделяет редактируемые поля от карточек, --color-tooltip задаёт поверхность подсказки. Все тени, ширина подсказок, слои и параметры движения принадлежат tokens.css. Контраст основных текстовых пар — не ниже 4.5:1 в обеих темах, включая подсказки, поля, навигацию и hover основной кнопки. Проверка цветов/ссылок на токены автоматически охватывает каждый CSS-модуль консоли.
Ссылки окрашены --color-nav-text и подчёркнуты; имя, которое открывает объект (репозиторий в таблице зеркал), — button.link-button: выглядит как ссылка, остаётся кнопкой. Команды и пути — блок pre на --color-subtle с переносом строк, без горизонтальной прокрутки страницы. Все раскрывающиеся блоки (details, кроме карточки подключения) открываются одной и той же стрелкой; системный треугольник не используется.
На телефоне шапка — одна строка с брендом и кнопкой «Разделы»; заголовок экрана делит строку с настройками, обратная связь становится кнопкой-значком, основное действие занимает следующую строку. В компактной полосе навигации названия групп становятся тонкими разделителями и остаются доступными для чтения с экрана.
Навигация разделена на библиотеку, передачи и ресурсы. Начало работы и справка используют ту же навигацию и aria-current, что остальные экраны. Выбранный раздел отмечен фоном и боковым индикатором. На телефоне разделы открываются кнопкой; заголовки групп скрываются для компактности. Данные и операции остаются реальными; декоративной статистики нет.
Контекстная помощь
Выделение текста использует отдельную пару --color-selection / --color-on-selection с проверкой контраста в обеих темах. Подписи кнопок не выделяются случайно при повторном нажатии; данные и поля остаются выделяемыми. Hover кнопок включён только для точного указателя с поддержкой наведения. Подсказки действий сохраняются при клавиатурном фокусе, не открываются повторно после Escape от движения между SVG-частями и исчезают при отключении или скрытии своего элемента. Наблюдатель ограничен активным элементом и его предками, без обхода постоянно обновляемых строк. Очередь переносит фокус с недоступного действия в ту же строку или панель управления без прокрутки страницы.
Все значки консоли — один набор в icons.ts: контуры на сетке 24 px внутри поля 3…21 (ничего не обрезается), одна толщина --icon-stroke, скруглённые концы и углы. Из него же берутся значки навигации (<span class="nav-icon" data-icon="…">), баннеров и знак справки «?»; встроенных SVG в разметке и текстовых знаков вместо значков нет. Кнопка с подписью получает значок через action-presentation.ts, если рядом есть кнопки со значками; кнопки отмены в диалогах остаются текстовыми.
Частые действия (поиск, обновление, скачивание, пауза, переход страницы, копирование) используют компактные кнопки с локальными контурными SVG из icons.ts. Размер цели — --control-height: 32 px для мыши, 44 px для касания. В строках таблиц используется --control-small-height. Геометрия и темы берутся из токенов. Подписи действий (#action-tooltip) не перехватывают указатель. Подпись сохраняется в aria-label и доступном DOM-тексте. action-presentation.ts обновляет стабильный узел подписи, не заменяя кнопку или обработчики; это явная ветка локализации для кнопок с иконками. Динамические строки получают ту же презентацию через message. Для удаления, отзыва прав, публикации и сложных команд остаётся иконка с текстом.
Desktop-навигация начинается с компактной полосы значков и раскрывается кнопкой внизу. На мобильном меню сохраняет текстовые подписи. Настройки используют значок солнца/луны/монитора и компактный RU/EN; нативные select остаются доступны с клавиатуры. Подсказки действий — один переиспользуемый popover вне обрезаемых таблиц, CSS anchor positioning с запасным расположением у края экрана. Они закрываются по Escape, действию, прокрутке и смене контекста, без переноса фокуса. При отсутствии Popover API остаётся нативный title. Reduced motion отключает появление; постоянных анимаций нет.
У экрана один видимый заголовок: он стоит в верхней полосе вместе с основным действием и настройками; основной section использует его через aria-labelledby. Хлебных крошек и названия продукта в шапке нет, бренд находится в боковой панели. Подзаголовок карточки, повторяющий название экрана, скрывается визуально (sr-only), но остаётся доступным. Карточки получают отдельный заголовок только для самостоятельного подраздела. Не добавляйте подзаголовок с пересказом названия, слоганы и повторяющиеся подписи форматов. Пустое состояние кратко называет ситуацию и при необходимости подсказывает следующий шаг. Общий status bar скрыт до сообщения об операции. Подробности восстановления находятся в раскрываемой справке; последствия удаления, ротации ключей и обновления остаются у действия.
Для второстепенного объяснения назначьте переведённому текстовому узлу data-help="labelMessageKey". Ключ задаёт локализованное доступное имя кнопки, например «О метаданных». initializeTooltips заменяет этот узел оболочкой с кнопкой и переносит исходный текст в role="tooltip"; связь задаётся через aria-describedby. HTML из пользовательских данных не создаётся. Не размещайте подсказку внутри label, summary или другой кнопки.
Подсказка открывается по наведению, фокусу или нажатию. Указатель можно перевести на её текст. Escape закрывает её без потери фокуса; повторное нажатие, уход фокуса, нажатие снаружи или изменение размера окна закрывают открытую подсказку. Одновременно открыта одна. На широком экране выбирается сторона относительно кнопки; на телефоне текст расположен у свободного края экрана, не перекрывает кнопку вызова и при необходимости прокручивается. Нет обязательного таймера чтения. RU/EN переключается и у открытой подсказки без пересоздания формы.
В подсказки вынесены пояснения к выбору репозитория, токенам и ключам, артефактам, фильтрам каталога и пакетов, меткам, metadata, ссылкам, вложениям, истории, загрузке и её продолжению, восстановлению скачиваний, хранению и очистке, продвижению и переносу, сервисной политике, ключу владельца, расписанию обновлений и каталогу операций. Кнопка «?» стоит в строке формы или заголовка, к которому относится пояснение. Узлы, созданные контроллерами позже, получают подсказку так же через data-help (наблюдатель DOM) или helpText() из management-dom.ts. Раскрывающийся блок, внутри которого осталась только подсказка, не нужен. Поля фильтров используют невидимую подпись и placeholder; у выпадающих списков остаются мелкие подписи, если без них значение неоднозначно. Ошибки, состояние передачи, ограничения очереди и предупреждения перед удалением/автообновлением остаются видимыми. Не помещайте действия, обязательные инструкции или критичные предупреждения в tooltip.
Появление экрана и подсказки использует короткую анимацию прозрачности и смещения. Кнопки имеют мягкое состояние нажатия. Постоянных анимаций вокруг прогресса нет. prefers-reduced-motion: reduce отключает анимации и смещение при нажатии, сохраняя фокус и функциональность.
Переходы согласованы токенами: короткие состояния — 160 мс, вход экрана — 220 мс, смена темы — 180 мс. Тема плавно меняет основные поверхности после инициализации, без анимации первого кадра. Вход экранов и мобильного меню использует только opacity/transform, раскрытие содержимого — opacity; размеры таблиц и прогресс не анимируются. Нет постоянного will-change, таймеров анимации или дублирования DOM. Стабильный scrollbar gutter исключает сдвиг ширины при смене длинного экрана на короткий. После навигации заголовок получает фокус и при необходимости возвращается в видимую область; значения форм сохраняются.
Числовые колонки используют tabular-nums. Строки таблицы выделяются при keyboard focus; hover применяется на устройствах с точным указателем. Отключённые кнопки не получают активную hover-подсветку. Переводчик сравнивает текст и атрибуты перед записью: неизменившиеся статусы передачи не пересоздаются и не создают повторных DOM-изменений в live regions. При смене языка и параметров сообщения содержимое обновляется, устаревшие параметры удаляются.
Темы и предпочтения
light, dark, system; последний режим следует prefers-color-scheme, включая изменения во время открытой вкладки. Ранний небольшой скрипт загружается с собственного origin, разрешён текущей CSP и выставляет тему до CSS. Inline scripts и ослабление CSP не нужны.
В localStorage сохраняются только arkvory.ui.theme и arkvory.ui.language. Некорректные значения отклоняются, запрет storage не мешает работе текущей вкладки. По умолчанию тема системная; язык русский при русской локали браузера, иначе английский. Ключи доступа, пути файлов, metadata и содержимое не сохраняются этим механизмом.
Локализация
Каждый ключ EN имеет русский перевод, соответствие проверяется TypeScript и тестом. data-i18n назначается текстовому узлу, не родителю с input/button: локализация не должна уничтожать дочерние элементы или сбрасывать поля. Placeholder и aria-label имеют отдельные атрибуты. Динамический текст формируется через message/feedback, параметры — через именованные {count}, {revision}, {id}. Интерполяция всегда выводится как textContent, а не HTML. Четвёртый аргумент message(node, key, params, ['size']) перечисляет параметры-размеры: точное число байт хранится в data-param-*, а formatBytes форматирует его двоичными единицами при каждом переключении языка. Отдельное значение без текста выводит bytesMessage.
dateMessage хранит исходную дату и форматирует её через Intl.DateTimeFormat для выбранного языка и часового пояса браузера; третий аргумент ('UTC') показывает время явного пояса с его названием в тексте (data-date-zone). relativeMessage выводит возраст через Intl.RelativeTimeFormat («5 минут назад», часы до двух суток), а точное местное время — в title для наведения; оба пересчитываются при смене языка. Правило единиц — relative-time.ts. Переключение языка обновляет уже загруженную историю и сообщения об ошибках. Пользовательские имена, пути, метки и JSON не переводятся. Кнопка выбора файла и пустое состояние внутри страницы локализуются через file-inputs.ts и file-inputs.css. Имя выбранного файла остаётся пользовательскими данными; смена языка не сбрасывает FileList, reset формы обновляет подпись после нативного сброса. Сам системный диалог выбора и стандартная browser validation используют язык ОС/браузера.
Новый язык: расширить Language, добавить полный словарь, выбор языка в preferences и option в HTML; затем проверить плейсхолдеры, все четыре экрана, длинные строки и даты. Не добавляйте двуязычные строки вида «Сохранить / Save» в один элемент.
Состояния и доступность
У кнопок/полей есть hover, focus-visible и disabled; ошибки и успех сообщаются текстом, не только цветом. Есть skip link, семантические заголовки, labels, aria-current навигации и aria-live для результатов. Смена экрана переводит фокус на его заголовок. Reduced motion отключает переходы; forced-colors добавляет системные границы ключевым элементам.
Пустой каталог отличается от отсутствия результатов поиска. Смена ключа или репозитория очищает предыдущий выбор, результаты и историю. Старый ответ каталога не возвращает данные в уже отключённый интерфейс. Во время upload блокируются смена подключения и параметры загрузки; пауза доступна, язык и тема остаются переключаемыми. Ошибка обновления каталога после успешной публикации не выдаётся за неудачную загрузку.
Автоматические проверки: полнота ключей/параметров, безопасные fallback preferences, наличие используемых токенов, контраст основных текстовых пар не ниже 4.5:1 в обеих темах, HTTP-раздача CSS и раннего скрипта с CSP, а также исполняемые правила дизайн-системы и локализации ниже. Это не заменяет полного аудита WCAG или проверки screen reader.
Исполняемые правила
tests/web-tokens.test.mjs входит в unit-гейт (npm run gate -- quick) и разбирает каждый CSS-модуль консоли, кроме tokens.css, а также index.html и apps/web/src/*.ts:
- Цвет. Нет hex,
rgb()/rgba()/hsl()/hsla()/hwb()/lab()/lch()/oklch()/color()и именованных цветов CSS. Разрешеныtransparent,currentColor,inheritи системные цвета дляforced-colors. В HTML нет атрибутаstyle, элемента<style>и цветов вfill/stroke; в контроллерах нетelement.style,cssTextи цветовых строк. - Геометрия.
border-radius— толькоvar(--radius-*)или0.font-size,font-weight,line-height,letter-spacing— только--text-*,--weight-*,--leading-*,--tracking-*илиinherit.margin,padding,gap— токены,0,autoиcalc()из токенов с числовыми множителями. Размеры,flex, сетки,inset/top/leftи ширины рамок не содержат длин, кроме структурных процентов,100vw/100dvhи1pxтехники visually hidden. - Глубина и движение.
box-shadow— токены--shadow-*или составные значения из токенов и0;z-index—var(--z-*),autoили0; длительности и задержкиtransition/animation—--motion-*, безcubic-bezier()вне токенов. - Структура. Компоненты не объявляют собственных custom properties: контекстные значения (например, ширина компактной полосы) объявлены в
tokens.css. Media queries используют только 760/761 и 1100 px. - Тексты разметки. Каждый видимый текстовый узел
index.htmlнаходится непосредственно в элементе сdata-i18n; бренд, копирайт, коды языков и примеры CLI тоже берутся из словаря.aria-labelиplaceholderсопровождаютсяdata-i18n-label/data-i18n-placeholder, атрибутtitleв разметке запрещён (пояснения —data-help). Без перевода допускаются только знаки— × ? ↻ · / . ….<noscript>помеченdata-i18n, но его запасной текст остаётся двуязычным: без JavaScript перевод невозможен. - Тексты контроллеров. Строковые и шаблонные литералы не присваиваются
textContent,innerText,title,placeholder,ariaLabelи не передаются вsetAttribute('aria-label'|'title'|'placeholder'|'alt'),append,prepend,replaceChildren,createTextNode, кроме пустой строки и перечисленных знаков. Проверяются обе ветви условных выражений;innerHTML/outerHTMLзапрещены. Составные строки данных («репозиторий: права», «метод путь») — отдельные ключи с параметрами. - Словарь. Каждый ключ EN используется разметкой (
data-i18n*,data-help) или строковым литералом в контроллере; префиксы шаблонов вродеpermission.${...}учитываются. Неиспользуемые ключи удаляются из обоих языков. Параметры RU/EN совпадают.
Тест также содержит контрольные примеры нарушений, чтобы правило нельзя было ослабить незаметно. Новое значение оформления добавляется токеном в обе темы, а не исключением в тесте.
В браузере проверены обе темы, RU/EN, история/restore, открытие артефакта, изменение метаданных, ошибки JSON, сохранение темы/языка после reload и layout на ширине 390 px. Через нативный file chooser выбран небольшой искусственный файл, выполнены hash/upload/публикация, проверено появление в каталоге; ошибок JavaScript не зарегистрировано. Полная автоматизация UI, большие передачи и все нативные файловые диалоги остаются отдельной проверкой.
Экран «Скачивания / Downloads» использует общие card/table/semantic tokens, RU/EN, кнопки pause/resume/cancel и отдельные действия очистки. Строки обновляются на месте, чтобы прогресс не терял фокус. Временные байты хранятся в OPFS, но токен и очередь — только в памяти вкладки. Поведение и ограничения.
Подключение показывает вход по имени и паролю первым, ключ сервиса — в раскрываемом блоке; на телефоне поля и кнопки идут в колонку, а .action-label использует overflow-wrap: break-word, поэтому подписи не разрываются внутри слова. Дополнительные настройки очереди и административные операции используют раскрываемые блоки; удаляющие доступ действия подтверждаются в модальном окне .management-dialog. Контекстные действия, пустые результаты, сообщения и проверка семи экранов в браузере описаны в CONSOLE_UX. Checkbox отделён от геометрии input. Новый сценарий приёмки дополняет проверки темы/языка и скачивания, не заменяя проверки на реальных пользователях.
Редактор metadata и карточки вложений используют общие tokens и RU/EN словари. На узком экране строки полей/вложений складываются вертикально; назначение файла показано текстом. Произвольные metadata, имена и описания выводятся только как textContent/input values. Сценарии.
Удаление артефакта — отдельный раскрываемый блок с проверкой зависимостей, RU/EN объяснением и подтверждением ID. Используются существующие spacing/control tokens. Managed key подключается через paginated repository discovery; legacy write не показывает удаление. Контракт.
Экран «Резервные копии» (backups.css) выражает состояние текстом с точкой и боковой полосой предупреждений, цвета — только --color-success/warning/error. Общее окно подтверждения (confirmAction) принимает необязательный узел details под текстом, например списки удаляемых и сохраняемых точек; окно прокручивает его в пределах --confirm-details-max-height. До 760 px строки таблиц этого экрана становятся карточками: ячейка несёт свою переведённую подпись (.backup-cell-label, скрыта рядом с заголовком таблицы на широком экране), горизонтальной прокрутки нет. Сценарии.
Раздел «Хранение и автоочистка» использует общие tokens и RU/EN, структурированные настройки, отдельное подтверждение автоматического удаления и предпросмотр сохранённой политики. Смена ключа/репозитория отменяет запросы и очищает приватные данные; тема/язык сохраняют несохранённые значения формы.