Files
bare/web/app.css
T

1150 lines
24 KiB
CSS

/* айдентика «скобы» — docs/identity/brief.md, ADR-024 */
:root {
--bone: #F7F5F0;
--ink: #1B1917;
--text2: #3C3B38;
--mute: #6E6D68;
--stone: #A9A59D;
--line: #E7E3DA;
--edge: #DEDCD6;
--mark: #C82D40;
--mark: oklch(55% 0.19 20);
--mono: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
/* тема одна, светлая (ADR-024): системная тёмная ничего не перекрашивает */
color-scheme: light;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
/* масштабирования нет (ADR-075). manipulation снимает зум по двойному тапу
и задержку 300 мс, которую браузер держит, ожидая второго касания;
прокрутку и щипок правило не трогает — щипок запрещён в viewport.
Правило стоит на html: двойной тап — жест документа, и браузер берёт
пересечение значений всех предков */
html {
touch-action: manipulation;
overscroll-behavior: none;
}
body {
margin: 0;
/* виден вырез: viewport-fit=cover пускает страницу под скруглённые углы,
под боковой вырез в альбомной ориентации и под верхнюю системную
полосу. Полосу над страницей держит мета-строка о стиле полосы
(ADR-075), поэтому верхняя вставка обычно нулевая и не делает ничего;
там, где она не нулевая, шапка не уезжает под часы */
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
background: var(--bone);
color: var(--ink);
font-family: var(--mono);
font-size: 14px;
line-height: 1.55;
overscroll-behavior: none;
-webkit-text-size-adjust: 100%;
}
/* интерфейс не выделяется: долгое нажатие по шапке, списку и кнопкам
вызывает лупу и «копировать», а копировать там нечего. Текст сообщений,
отпечатки и поля ввода выделяются как обычно — их именно копируют
(ADR-075) */
.side,
.head,
.brand,
.divider,
.input .p,
.counter,
.enter,
button {
-webkit-user-select: none;
user-select: none;
}
/* прокрутка внутренних лент не уходит в страницу: отскок и «потянуть
для обновления» ощущения приложения не добавляют */
.list,
.body,
.feed,
.auth {
overscroll-behavior: contain;
}
/* #app — колонка ровно в высоту окна: от неё считают высоту экраны,
поэтому сайдбар с «ты: @nick» стоит на месте, а прокручивается
только содержимое */
#app {
display: flex;
flex-direction: column;
height: 100%;
}
:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 2px;
}
/* знак: четыре угла, один цвет, без скруглений */
.mark {
width: 18px;
height: 18px;
flex: none;
}
/* заставка: знак и слово, пока не поднялись модули */
.boot {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.boot .mark {
width: 20px;
height: 20px;
}
.boot .word {
font-size: 15px;
letter-spacing: -0.02em;
}
/* формы: рамка 1 px ink, прямые углы, цель нажатия 44 px */
.form {
max-width: 360px;
}
.field {
display: block;
margin-bottom: 14px;
}
.field > span {
display: block;
margin-bottom: 6px;
font-size: 11px;
color: var(--mute);
}
input[type="text"],
input[type="password"] {
display: block;
width: 100%;
min-height: 44px;
padding: 11px 14px;
border: 1px solid var(--ink);
border-radius: 0;
background: var(--bone);
color: var(--ink);
font: inherit;
font-size: 14px;
}
.button {
display: block;
width: 100%;
min-height: 44px;
padding: 11px 14px;
border: 1px solid var(--ink);
border-radius: 0;
background: var(--bone);
color: var(--ink);
font: inherit;
font-size: 14px;
text-align: center;
cursor: pointer;
}
.button[disabled] {
border-color: var(--edge);
color: var(--stone);
cursor: default;
}
/* hidden обязан прятать: display у .button перебивает таблицу браузера,
поэтому кнопке нужно отдельное правило */
.button[hidden] {
display: none;
}
.check {
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
font-size: 12px;
color: var(--text2);
cursor: pointer;
}
.check input {
width: 15px;
height: 15px;
accent-color: var(--ink);
}
.row {
display: flex;
gap: 10px;
margin-top: 12px;
}
.row .button {
flex: 1;
width: auto;
}
/* три кнопки подтверждения в один ряд помещаются не всегда: переносятся
и делят место поровну (ADR-051) */
.row--wrap {
flex-wrap: wrap;
}
.row--wrap .button {
flex: 1 1 120px;
}
/* подписи и строки состояния: акцент mark — только ошибка */
.hint {
margin: -4px 0 14px;
font-size: 11px;
line-height: 1.5;
color: var(--mute);
}
.message {
min-height: 18px;
margin: 10px 0 0;
font-size: 12px;
color: var(--mute);
}
.message--error {
color: var(--mark);
}
.confirm {
margin: 12px 0 0;
font-size: 12px;
color: var(--text2);
}
/* вход и регистрация */
.auth {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
overflow-y: auto;
}
.auth__inner {
width: 100%;
max-width: 320px;
}
.auth .form {
max-width: none;
}
.auth__brand {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 28px;
font-size: 15px;
letter-spacing: -0.02em;
}
.tabs {
display: flex;
align-items: center;
gap: 2px;
margin-bottom: 22px;
}
.tab {
min-height: 32px;
padding: 6px 10px;
border: 0;
background: none;
color: var(--mute);
font: inherit;
font-size: 13px;
cursor: pointer;
}
.tab.is-on {
background: var(--ink);
color: var(--bone);
}
.tabs__sep {
color: var(--stone);
font-size: 13px;
}
/* новый чат — одно действие за раз (ADR-078). Подсказки стоят до формы,
чтобы смысл выбора был понятен до фокуса в поле */
.new {
max-width: 460px;
}
.new__tabs {
margin-bottom: 18px;
}
.new__panel[hidden] {
display: none;
}
.new__copy {
max-width: 420px;
margin: 0 0 18px;
color: var(--text2);
font-size: 12px;
line-height: 1.6;
}
.new__aside {
display: block;
margin-top: 8px;
color: var(--mute);
font-size: 11px;
}
/* каркас: сайдбар и экран */
.shell {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.side {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.brand {
flex: none;
display: flex;
align-items: center;
gap: 10px;
height: 56px;
padding: 0 20px;
border-bottom: 1px solid var(--line);
font-size: 15px;
letter-spacing: -0.02em;
}
.list {
flex: 1;
min-height: 0;
padding: 20px;
overflow-y: auto;
}
.section {
margin: 0;
padding: 0 8px 10px;
font-size: 10px;
font-weight: 400;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--stone);
}
.items {
margin: 0 0 22px;
padding: 0;
list-style: none;
}
.items:last-child {
margin-bottom: 0;
}
/* строка списка: чат или «+ новый чат» */
.item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 6px 8px;
border: 0;
border-radius: 0;
background: none;
color: var(--text2);
font: inherit;
font-size: 13px;
text-align: left;
cursor: pointer;
}
.item__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item .n {
margin-left: auto;
color: var(--mark);
}
/* активный чат — инверсия (docs/identity/brief.md) */
.item.is-active {
background: var(--ink);
color: var(--bone);
}
.item.is-active .n {
color: var(--bone);
}
.item--new {
margin-bottom: 18px;
color: var(--mute);
}
/* подвал сайдбара: «ты: @nick» и версия с временем коммита (ADR-067).
Версия не кнопка: нажимать в ней нечего, поэтому она стоит рядом
с кнопкой, а не внутри неё. В сайдбаре 224 px они рядом не помещаются
никогда — версия занимает вторую строку и остаётся справа; на мобильном,
где сайдбар во всю ширину, обе стоят в одной строке */
.foot {
flex: none;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px 8px;
margin-top: auto;
/* нижняя системная полоса iPhone проходит поверх страницы: подвал
отступает от неё, а не прячется под ней (ADR-075) */
padding: 6px 20px calc(6px + env(safe-area-inset-bottom));
border-top: 1px solid var(--line);
}
.me {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 auto;
min-width: 0;
min-height: 44px;
padding: 0;
border: 0;
background: none;
color: var(--mute);
font: inherit;
font-size: 12px;
text-align: left;
cursor: pointer;
}
.me i {
flex: none;
width: 6px;
height: 6px;
background: var(--ink);
}
/* ник бывает в 32 символа (ADR-019): он обрезается, а версия остаётся
целой — читать половину хеша незачем */
.me__nick {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* версия выделяется: с неё начинается разговор о поломке (ADR-067),
а на iPhone скопировать её иначе нечем — ни строки адреса, ни консоли
там нет. Общий запрет выделения снят с неё так же, как с текста
сообщений и отпечатков (ADR-075) */
.build {
flex: none;
margin-left: auto;
color: var(--stone);
font-size: 11px;
-webkit-user-select: text;
user-select: text;
}
.main {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.head {
flex: none;
display: flex;
align-items: center;
gap: 14px;
height: 56px;
padding: 0 20px;
border-bottom: 1px solid var(--line);
font-size: 15px;
}
/* имя комнаты бывает длиной в 64 символа (ADR-018): в шапке оно
обрезается, как и в строке сайдбара, — иначе страница едет вбок */
.head .chat-title,
.head .title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.back {
flex: none;
min-height: 32px;
padding: 6px 10px;
border: 1px solid var(--edge);
border-radius: 0;
background: none;
color: var(--mute);
font: inherit;
font-size: 12px;
cursor: pointer;
}
.body {
flex: 1;
min-height: 0;
padding: 20px;
overflow-y: auto;
}
/* настройки */
.settings {
max-width: 460px;
}
.block {
padding: 20px 0;
border-top: 1px solid var(--line);
}
.block--first {
padding-top: 0;
border-top: 0;
}
.self {
margin: 0 0 10px;
font-size: 13px;
color: var(--text2);
}
.fp {
margin: 0;
font-size: 12px;
color: var(--mute);
}
.block .form {
margin-top: 14px;
}
.fp-label {
margin: 12px 0 4px;
font-size: 11px;
color: var(--stone);
}
.block--first > .fp-label:first-child {
margin-top: 0;
}
.fp-hint {
margin: 10px 0 0;
font-size: 11px;
color: var(--mute);
}
/* уведомления и установка — docs/ui.md, «Настройки» */
.state {
margin: 0 0 12px;
font-size: 13px;
color: var(--text2);
}
.install {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--mute);
}
/* устройства — docs/ui.md, «Настройки». Строка: восемь символов
идентификатора, дата появления и «удалить»; у своей строки вместо
кнопки пометка (ADR-052) */
.devices {
margin: 0 0 12px;
padding: 0;
list-style: none;
}
.device {
display: flex;
align-items: center;
gap: 10px;
min-height: 28px;
font-size: 13px;
color: var(--text2);
}
.device__id {
flex: none;
}
.device .tag {
color: var(--stone);
font-size: 11px;
}
.device .tag:last-child {
margin-left: auto;
}
.device .link {
margin-left: auto;
color: var(--mute);
font-size: 12px;
}
/* баннер установки — docs/ui.md, «Баннер установки (iOS)».
Цель нажатия у крестика — 44 px, отрицательные поля не дают ей
растянуть сам баннер */
.banner-slot {
flex: none;
}
.banner {
display: flex;
align-items: flex-start;
padding: 12px 20px;
border-bottom: 1px solid var(--line);
}
.banner__text {
margin: 0;
font-size: 11px;
line-height: 1.5;
color: var(--mute);
}
.banner__close {
flex: none;
width: 44px;
height: 44px;
margin: -12px -14px -12px auto;
padding: 0;
border: 0;
background: none;
color: var(--stone);
font: inherit;
font-size: 15px;
line-height: 1;
cursor: pointer;
}
/* участники комнаты — docs/ui.md, «Участники» */
.members {
margin: 0;
padding: 0;
list-style: none;
}
.member {
display: flex;
align-items: center;
gap: 10px;
min-height: 28px;
font-size: 13px;
color: var(--text2);
}
.member__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member .tag {
color: var(--stone);
font-size: 11px;
}
.member .link {
margin-left: auto;
color: var(--mute);
font-size: 12px;
}
/* строка ввода как отдельная форма: «новый чат» и «добавить участника» */
.form--row + .form--row {
margin-top: 12px;
}
.form--row .button {
margin-top: 10px;
}
/* две кнопки подряд не слипаются в двойную рамку */
.block > .button + .button {
margin-top: 10px;
}
/* чат: шапка, лента, ввод — docs/identity/screens.html */
.chat-title {
display: flex;
align-items: center;
gap: 8px;
padding: 0;
border: 0;
border-radius: 0;
background: none;
color: var(--ink);
font: inherit;
font-size: 15px;
text-align: left;
cursor: pointer;
}
.chat-title__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-title__members {
flex: none;
color: var(--mute);
font-size: 11px;
}
/* лента прижата к низу: короткая переписка не висит под шапкой */
.feed {
flex: 1;
min-height: 0;
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.feed-list {
margin-top: auto;
display: flex;
flex-direction: column;
align-items: flex-start;
font-size: 14px;
line-height: 1.55;
}
.feed-list > :first-child {
margin-top: 0;
}
/* строка сообщения: автор над текстом. На телефоне и планшете своё стоит
у правого края; на десктопе все сообщения идут слева (ADR-077). */
.line {
display: flex;
flex-direction: column;
max-width: min(640px, 85%);
gap: 4px;
margin-top: 4px;
}
.line.is-head {
margin-top: 14px;
}
.line--me {
align-self: flex-end;
align-items: flex-end;
}
/* Ширина окна не отличает планшет от десктопа: iPad бывает шире ноутбука.
Точный указатель с hover — признак десктопного способа работы; там сторона
не помогает и только заставляет взгляд ходить через всю ленту. */
@media (hover: hover) and (pointer: fine) {
.line--me {
align-self: flex-start;
align-items: flex-start;
}
}
.author {
display: flex;
flex-wrap: wrap;
column-gap: 6px;
color: var(--mute);
font-size: 11px;
overflow-wrap: anywhere;
}
.author:empty {
display: none;
}
.author--me {
color: var(--mark);
}
.author .t {
color: var(--stone);
}
.text {
min-width: 0;
}
.text__body {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.text--pending {
color: var(--stone);
}
.text--none {
color: var(--mute);
font-style: italic;
}
.fail {
display: flex;
align-items: center;
flex-wrap: wrap;
/* пробел вокруг «·» держит gap: у соседних элементов строки он
схлопывается */
gap: 0 5px;
margin: 2px 0 0;
color: var(--mark);
font-size: 12px;
}
.link {
padding: 0;
border: 0;
border-radius: 0;
background: none;
color: inherit;
font: inherit;
text-decoration: underline;
cursor: pointer;
}
/* разделители: дата — линией line, «новые» — линией mark; во всю ширину
ленты, а не по ширине сообщения (feed-list выравнивает по краю только
.line, ADR-065) */
.divider {
display: flex;
align-items: center;
align-self: stretch;
gap: 14px;
margin: 14px 0;
color: var(--stone);
font-size: 10px;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--line);
}
.divider--new {
color: var(--mark);
}
.divider--new::before,
.divider--new::after {
background: var(--mark);
}
/* ввод: рамка 1 px ink, слева «>» цветом mark */
.compose {
flex: none;
/* то же, что у подвала: строка ввода не уезжает под системную полосу */
padding: 0 16px calc(20px + env(safe-area-inset-bottom));
}
/* При открытой экранной клавиатуре нижний край уже закрыт её прямоугольной
поверхностью. Safari продолжает отдавать safe-area домашнего индикатора;
заменяем весь нижний отступ на компактные 6 px (ADR-076). */
.compose.is-keyboard {
padding-bottom: 6px;
}
.input {
display: flex;
align-items: center;
gap: 10px;
padding: 0 14px;
border: 1px solid var(--ink);
font-size: 14px;
}
.input .p {
flex: none;
color: var(--mark);
}
/* фокус показывает вся строка ввода: рамка у неё одна, второй рамки
вокруг поля внутри не нужно */
.input:focus-within {
outline: 2px solid var(--ink);
outline-offset: 2px;
}
.input .input__field:focus-visible {
outline: none;
}
.input .input__field {
flex: 1;
min-width: 0;
min-height: 0;
/* отступ назван: высота блока считается вместе с ним (box-sizing —
border-box), и предел роста строки сообщения меряется от него */
--field-pad: 12px;
padding: var(--field-pad) 0;
border: 0;
border-radius: 0;
background: none;
color: var(--ink);
font: inherit;
font-size: 14px;
line-height: 1.5;
}
/* строка сообщения — textarea (ADR-076): браузер сам держит вставку,
выделение, отмену и предел. Число rows растёт под текст до семи строк,
дальше поле прокручивается */
.input .input__field--text {
resize: none;
overflow-y: auto;
/* семь строк — это семь высот строки плюс вертикальные отступы:
без них предел вышел бы вдвое ниже обещанного */
max-height: calc(7 * 1.5em + 2 * var(--field-pad));
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.input .input__field::placeholder {
color: var(--stone);
}
.input__send {
flex: none;
align-self: stretch;
min-width: 44px;
padding: 0;
border: 0;
border-radius: 0;
background: none;
color: var(--ink);
font: inherit;
font-size: 14px;
cursor: pointer;
}
/* ввод заблокирован предупреждением о ключе: кнопка «>» тоже гаснет */
.input__send[disabled] {
color: var(--stone);
cursor: default;
}
.counter,
.enter {
flex: none;
margin-left: auto;
color: var(--stone);
font-size: 11px;
}
.counter + .enter {
margin-left: 0;
}
/* полоса над вводом: нет соединения — stone, отказ отправки и смена
ключа — mark. У полосы про ключ есть кнопка, поэтому строка — flex */
.bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 8px;
margin: 0 0 10px;
font-size: 11px;
line-height: 1.5;
color: var(--stone);
}
.bar[hidden] {
display: none;
}
.bar--mark {
color: var(--mark);
}
@media (min-width: 760px) {
.shell {
display: grid;
grid-template-columns: 224px 1fr;
}
.side {
border-right: 1px solid var(--line);
}
.brand {
height: 64px;
}
.head {
height: 64px;
padding: 0 32px;
}
.body {
padding: 28px 32px;
}
/* сайдбар всегда на месте: «назад» из чата некуда */
.back--chat {
display: none;
}
.feed {
padding: 28px 32px;
}
.compose {
padding: 0 32px calc(28px + env(safe-area-inset-bottom));
}
.input {
gap: 12px;
padding: 0 16px;
}
.input .input__field {
--field-pad: 13px;
}
/* на десктопе чат отправляет enter — кнопка `>` не нужна: в рамке
ввода она остаётся одна, слева (docs/identity/brief.md, «Компоновка»).
Остальные формы используют обычные текстовые `.button` */
.input__send {
display: none;
}
}
/* мобильный: один экран за раз, цели нажатия не меньше 44 px */
@media (max-width: 759px) {
.shell[data-screen="list"] .main {
display: none;
}
.shell[data-screen="screen"] .side {
display: none;
}
.tab,
.back,
.item,
.chat-title,
.link {
min-height: 44px;
/* цель нажатия не меньше 44 px в обе стороны (docs/ui.md,
«Доступность»): у коротких надписей вроде «убрать» ширины
по тексту не хватает */
min-width: 44px;
}
/* подсказку «enter — отправить» видит только десктоп: на мобильном
enter переносит строку (docs/ui.md, «Чат») */
.enter {
display: none;
}
/* строка сообщения — тоже цель нажатия не меньше 44 px (docs/ui.md,
«Доступность»): своей высоты у блока нет, её задаёт содержимое */
.input .input__field--text {
min-height: 44px;
}
}