griffincss-ui 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scss/_nav.scss ADDED
@@ -0,0 +1,233 @@
1
+ // Griffincss UI — Nav
2
+ // Навигация: ряд ссылок, шапка сайта и её сворачивание на узком экране.
3
+ //
4
+ // Класс ставится на любую разметку: <nav> со ссылками, <ul> со списком —
5
+ // список обнуляется здесь же, потому что ресет опционален.
6
+ //
7
+ // Активный пункт помечается в разметке атрибутом aria-current="page",
8
+ // а не отдельным классом: тот же признак, по которому пункт находит
9
+ // программа чтения с экрана, красит его и на экране. Класс без атрибута
10
+ // подсветил бы пункт только зрячему.
11
+
12
+ @use 'sass:map';
13
+ @use 'griffincss-core/scss/breakpoints' as bp;
14
+
15
+ // Экран, с которого шапка перестаёт сворачиваться.
16
+ $gr-nav-expand: map.get(bp.$gr-breakpoints, md);
17
+
18
+ // --- Ряд ссылок -------------------------------------------------------------
19
+
20
+ .gr-nav {
21
+ display: flex;
22
+ box-sizing: border-box;
23
+ flex-wrap: wrap;
24
+ align-items: center;
25
+ gap: var(--gr-ui-gap);
26
+ margin: 0;
27
+ padding: 0;
28
+ list-style: none;
29
+ }
30
+
31
+ // Ряд на <ul>: обёртка <li> не должна ни добавлять полей, ни мешать пункту
32
+ // растянуться по ширине колонки. :where() держит специфичность нулевой,
33
+ // чтобы класс на самом <li> оставался сильнее.
34
+ .gr-nav > :where(li) {
35
+ display: flex;
36
+ margin: 0;
37
+ padding: 0;
38
+ list-style: none;
39
+ }
40
+
41
+ .gr-nav-vertical > :where(li) {
42
+ align-items: stretch;
43
+ flex-direction: column;
44
+ }
45
+
46
+ // Вертикальная — боковое меню раздела. Пункты растянуты по ширине колонки,
47
+ // иначе подсветка активного обрывалась бы по длине слова.
48
+ .gr-nav-vertical {
49
+ flex-flow: column nowrap;
50
+ align-items: stretch;
51
+ }
52
+
53
+ .gr-nav-link {
54
+ --gr-nav-fg: var(--gr-color-text-secondary);
55
+
56
+ display: flex;
57
+ box-sizing: border-box;
58
+ align-items: center;
59
+ gap: var(--gr-ui-gap);
60
+ min-block-size: var(--gr-control-height-sm);
61
+ padding-block: 0;
62
+ padding-inline: calc(var(--gr-control-padding-x) * 0.75);
63
+ border: 0;
64
+ border-radius: var(--gr-radius);
65
+ background-color: transparent;
66
+ color: var(--gr-nav-fg);
67
+ font-family: inherit;
68
+ font-size: var(--gr-control-font-size);
69
+ line-height: 1.2;
70
+ text-decoration: none;
71
+ cursor: pointer;
72
+ }
73
+
74
+ // Текущий пункт из подсветки исключён: правило с :hover весомее правила
75
+ // состояния, и наведение гасило бы акцентную подложку текущего раздела.
76
+ @media (hover: hover) {
77
+ .gr-nav-link:hover:not([aria-current], [aria-disabled="true"]) {
78
+ --gr-nav-fg: var(--gr-color-text);
79
+
80
+ background-color: color-mix(in srgb, currentcolor 8%, transparent);
81
+ }
82
+ }
83
+
84
+ .gr-nav-link:focus-visible {
85
+ outline: var(--gr-focus-width) solid var(--gr-color-focus);
86
+ outline-offset: 2px;
87
+ }
88
+
89
+ // Текущий раздел. Цвет — не единственный признак: у активного пункта
90
+ // ещё и подложка, а вес шрифта тяжелее, поэтому он находится и на
91
+ // чёрно-белой печати.
92
+ .gr-nav-link[aria-current] {
93
+ --gr-nav-fg: var(--gr-color-accent);
94
+
95
+ background-color: color-mix(in srgb, var(--gr-color-accent) 12%, transparent);
96
+ font-weight: 600;
97
+ }
98
+
99
+ .gr-nav-link[aria-disabled="true"] {
100
+ color: var(--gr-color-text-disabled);
101
+ cursor: not-allowed;
102
+ }
103
+
104
+ // --- Подсветка линией -------------------------------------------------------
105
+
106
+ // Вариант без подложки: активный пункт подчёркнут. Линия объявлена
107
+ // прозрачной у всех пунктов сразу — иначе ряд подпрыгивал бы на её толщину
108
+ // в момент, когда пункт становится текущим.
109
+ .gr-nav-underline .gr-nav-link {
110
+ border-block-end: calc(var(--gr-border-width) * 2) solid transparent;
111
+ border-radius: 0;
112
+ }
113
+
114
+ .gr-nav-underline .gr-nav-link[aria-current] {
115
+ border-block-end-color: var(--gr-color-accent);
116
+ background-color: transparent;
117
+ }
118
+
119
+ @media (hover: hover) {
120
+ .gr-nav-underline .gr-nav-link:hover:not([aria-current], [aria-disabled="true"]) {
121
+ border-block-end-color: var(--gr-color-border-strong);
122
+ background-color: transparent;
123
+ }
124
+ }
125
+
126
+ // Вертикальная разновидность того же варианта отбивает линией начало строки,
127
+ // а не низ: подчёркивание в столбце читалось бы как разделитель пунктов.
128
+ .gr-nav-underline.gr-nav-vertical .gr-nav-link {
129
+ border-block-end: 0;
130
+ border-inline-start: calc(var(--gr-border-width) * 2) solid transparent;
131
+ }
132
+
133
+ .gr-nav-underline.gr-nav-vertical .gr-nav-link[aria-current] {
134
+ border-inline-start-color: var(--gr-color-accent);
135
+ }
136
+
137
+ // --- Шапка ------------------------------------------------------------------
138
+
139
+ .gr-navbar {
140
+ display: flex;
141
+ box-sizing: border-box;
142
+ flex-wrap: wrap;
143
+ align-items: center;
144
+ gap: var(--gr-ui-gap);
145
+ padding-block: calc(var(--gr-gap) * 0.5);
146
+ padding-inline: var(--gr-gap);
147
+ border-block-end: var(--gr-border-width) solid var(--gr-color-border);
148
+ background-color: var(--gr-color-surface);
149
+ color: var(--gr-color-text);
150
+ }
151
+
152
+ // Название сайта. Забирает остаток строки, поэтому навигация и кнопки
153
+ // прижимаются к её концу без единой утилиты.
154
+ .gr-navbar-brand {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: var(--gr-ui-gap);
158
+ margin-inline-end: auto;
159
+ color: var(--gr-color-text);
160
+ font-size: 1.125rem;
161
+ font-weight: 700;
162
+ text-decoration: none;
163
+ }
164
+
165
+ // --- Сворачивание на узком экране -------------------------------------------
166
+
167
+ // Переключатель — <details>, но навигация лежит не внутри него, а следующим
168
+ // соседом: содержимое <details> скрывается самим браузером, и на широком
169
+ // экране раскрыть его без скрипта или ::details-content нельзя. Соседом же
170
+ // управляет обычный селектор, и медиазапрос отменяет сворачивание целиком.
171
+ //
172
+ // <div class="gr-navbar">
173
+ // <a class="gr-navbar-brand" href="/">Сайт</a>
174
+ // <details class="gr-nav-toggle"><summary>Меню</summary></details>
175
+ // <nav class="gr-nav gr-nav-collapse">…</nav>
176
+ // </div>
177
+ .gr-nav-toggle > summary {
178
+ display: flex;
179
+ box-sizing: border-box;
180
+ align-items: center;
181
+ justify-content: center;
182
+ min-inline-size: var(--gr-control-height);
183
+ min-block-size: var(--gr-control-height);
184
+ padding-inline: calc(var(--gr-control-padding-x) * 0.5);
185
+ border: var(--gr-border-width) solid var(--gr-color-border);
186
+ border-radius: var(--gr-radius);
187
+ color: var(--gr-color-text);
188
+ font-size: var(--gr-control-font-size);
189
+ list-style: none;
190
+ cursor: pointer;
191
+ }
192
+
193
+ // Родной треугольник снимается в обоих движках: в WebKit — своим
194
+ // псевдоэлементом, в остальных — через list-style выше.
195
+ .gr-nav-toggle > summary::-webkit-details-marker {
196
+ display: none;
197
+ }
198
+
199
+ .gr-nav-toggle > summary:focus-visible {
200
+ outline: var(--gr-focus-width) solid var(--gr-color-focus);
201
+ outline-offset: 2px;
202
+ }
203
+
204
+ // Свёрнутая навигация занимает всю строку под шапкой, а не втискивается
205
+ // в её ряд: пункты должны читаться столбцом, как в боковом меню.
206
+ .gr-nav-collapse {
207
+ display: none;
208
+ flex-basis: 100%;
209
+ flex-direction: column;
210
+ align-items: stretch;
211
+ }
212
+
213
+ // Общий сосед, а не следующий: между переключателем и навигацией в шапке
214
+ // может стоять что угодно — поиск, кнопка входа, — и раскрытие не должно
215
+ // от этого ломаться.
216
+ .gr-nav-toggle[open] ~ .gr-nav-collapse {
217
+ display: flex;
218
+ }
219
+
220
+ // Со средних экранов сворачивания нет вовсе: кнопка убирается, навигация
221
+ // возвращается в строку шапки независимо от того, открыт <details> или нет.
222
+ @media (width >= $gr-nav-expand) {
223
+ .gr-nav-toggle {
224
+ display: none;
225
+ }
226
+
227
+ .gr-nav-collapse {
228
+ display: flex;
229
+ flex-basis: auto;
230
+ flex-direction: row;
231
+ align-items: center;
232
+ }
233
+ }
@@ -0,0 +1,142 @@
1
+ // Griffincss UI — Pagination
2
+ // Постраничная навигация. Разметка — <nav aria-label="Страницы"> со списком
3
+ // ссылок: каждая страница обязана быть настоящей ссылкой с href, иначе она
4
+ // не откроется в новой вкладке и не попадёт в историю.
5
+ //
6
+ // Текущая страница помечается aria-current="page" — тем же атрибутом, что
7
+ // и активный пункт навигации. Кнопка «дальше» на последней странице
8
+ // получает aria-disabled="true" и остаётся в порядке обхода: исчезнувшая
9
+ // кнопка сбивает счёт нажатий Tab, приглушённая — нет.
10
+
11
+ .gr-pagination {
12
+ display: flex;
13
+ box-sizing: border-box;
14
+ flex-wrap: wrap;
15
+ align-items: center;
16
+ gap: calc(var(--gr-ui-gap) * 0.5);
17
+ margin: 0;
18
+ padding: 0;
19
+ list-style: none;
20
+ }
21
+
22
+ // Ряд на <ul>: обёртка <li> не приносит с собой ни полей, ни маркера.
23
+ // :where() оставляет специфичность нулевой — класс на самом <li> сильнее.
24
+ .gr-pagination > :where(li) {
25
+ display: flex;
26
+ margin: 0;
27
+ padding: 0;
28
+ list-style: none;
29
+ }
30
+
31
+ .gr-page {
32
+ --gr-page-size: var(--gr-control-height);
33
+ --gr-page-fg: var(--gr-color-text);
34
+
35
+ display: inline-flex;
36
+ box-sizing: border-box;
37
+ align-items: center;
38
+ justify-content: center;
39
+ min-inline-size: var(--gr-page-size);
40
+ min-block-size: var(--gr-page-size);
41
+ padding-inline: calc(var(--gr-control-padding-x) * 0.5);
42
+ border: var(--gr-border-width) solid var(--gr-color-border);
43
+ border-radius: var(--gr-radius);
44
+ background-color: var(--gr-color-surface);
45
+ color: var(--gr-page-fg);
46
+ font-family: inherit;
47
+ font-size: var(--gr-control-font-size);
48
+ font-variant-numeric: tabular-nums;
49
+ line-height: 1;
50
+ text-decoration: none;
51
+ cursor: pointer;
52
+ }
53
+
54
+ @media (hover: hover) {
55
+ .gr-page:hover:not([aria-current], [aria-disabled="true"], :disabled) {
56
+ border-color: var(--gr-color-border-strong);
57
+ background-color: var(--gr-color-surface-raised);
58
+ }
59
+ }
60
+
61
+ .gr-page:focus-visible {
62
+ outline: var(--gr-focus-width) solid var(--gr-color-focus);
63
+ outline-offset: 2px;
64
+ }
65
+
66
+ // Текущая страница. Заливка, а не только цвет текста: в ряду одинаковых
67
+ // цифр разница в оттенке замечается хуже, чем разница в весе пятна.
68
+ .gr-page[aria-current] {
69
+ border-color: var(--gr-color-accent);
70
+ background-color: var(--gr-color-accent);
71
+ color: var(--gr-color-on-accent);
72
+ font-weight: 600;
73
+ cursor: default;
74
+ }
75
+
76
+ .gr-page[aria-disabled="true"],
77
+ .gr-page:disabled {
78
+ color: var(--gr-color-text-disabled);
79
+ opacity: 0.7;
80
+ cursor: not-allowed;
81
+ }
82
+
83
+ // Пропуск страниц. Не ссылка и не кнопка — знак того, что ряд прерван,
84
+ // поэтому у него нет ни рамки, ни фона, ни курсора указателя.
85
+ .gr-page-gap {
86
+ display: inline-flex;
87
+ align-items: flex-end;
88
+ justify-content: center;
89
+ min-inline-size: calc(var(--gr-control-height) * 0.75);
90
+ min-block-size: var(--gr-control-height);
91
+ color: var(--gr-color-text-muted);
92
+ user-select: none;
93
+ }
94
+
95
+ // --- Размеры и варианты -----------------------------------------------------
96
+
97
+ // Компактный ряд — для подвала таблицы, где строка и без того плотная.
98
+ .gr-pagination-compact .gr-page {
99
+ --gr-page-size: var(--gr-control-height-sm);
100
+
101
+ font-size: 0.875rem;
102
+ }
103
+
104
+ .gr-pagination-compact .gr-page-gap {
105
+ min-block-size: var(--gr-control-height-sm);
106
+ }
107
+
108
+ // Слипшийся ряд — одна лента вместо отдельных клавиш. Соседние границы
109
+ // наезжают друг на друга на свою толщину, иначе на стыке была бы двойная
110
+ // линия; приём тот же, что у .gr-btn-group.
111
+ .gr-pagination-joined {
112
+ gap: 0;
113
+ }
114
+
115
+ .gr-pagination-joined .gr-page {
116
+ border-radius: 0;
117
+ }
118
+
119
+ .gr-pagination-joined > :first-child .gr-page,
120
+ .gr-pagination-joined > .gr-page:first-child {
121
+ border-start-start-radius: var(--gr-radius);
122
+ border-end-start-radius: var(--gr-radius);
123
+ }
124
+
125
+ .gr-pagination-joined > :last-child .gr-page,
126
+ .gr-pagination-joined > .gr-page:last-child {
127
+ border-start-end-radius: var(--gr-radius);
128
+ border-end-end-radius: var(--gr-radius);
129
+ }
130
+
131
+ .gr-pagination-joined .gr-page + .gr-page,
132
+ .gr-pagination-joined > :where(* + *) .gr-page {
133
+ margin-inline-start: calc(-1 * var(--gr-border-width));
134
+ }
135
+
136
+ // Активная клавиша поднимается над соседями: иначе её граница и контур
137
+ // фокуса оказались бы наполовину закрыты следующей.
138
+ .gr-pagination-joined .gr-page:focus-visible,
139
+ .gr-pagination-joined .gr-page[aria-current] {
140
+ position: relative;
141
+ z-index: 1;
142
+ }
@@ -0,0 +1,188 @@
1
+ // Griffincss UI — Progress
2
+ // Полоса выполнения на <progress> и шкала на <meter>. Значение показывает
3
+ // сам элемент — классу остаётся снять системную отрисовку и покрасить
4
+ // дорожку и заполнение.
5
+ //
6
+ // <progress class="gr-progress" value="40" max="100">40 %</progress>
7
+ // <meter class="gr-meter" value="0.7" low="0.3" high="0.8" optimum="1">70 %</meter>
8
+ //
9
+ // Текст внутри элемента — не подпись, а запасной путь для движка, который
10
+ // <progress> не знает вовсе; видимую подпись ставят рядом.
11
+ //
12
+ // Вендорные псевдоэлементы объявлены КАЖДЫЙ своим правилом. Неизвестный
13
+ // селектор в группе делает невалидной всю группу: WebKit, встретив
14
+ // ::-moz-progress-bar рядом со своим псевдоэлементом, выбрасывает правило
15
+ // целиком — и полоса остаётся некрашеной в обоих движках. Это условие
16
+ // приёмки модуля, а не стилевое предпочтение.
17
+ //
18
+ // Каждому из них вдобавок снимается background-image. UA-стиль рисует части
19
+ // шкалы градиентом, а он лежит ПОВЕРХ цвета фона: заданный background-color
20
+ // оказывается под непрозрачной картинкой и не виден вовсе. Снаружи это
21
+ // выглядело как «класс не применился» — на деле применился и был закрашен
22
+ // системным градиентом. appearance: none этого не снимает.
23
+
24
+ .gr-progress {
25
+ --gr-progress-fill: var(--gr-color-accent);
26
+ --gr-progress-track: color-mix(in srgb, currentcolor 12%, transparent);
27
+ --gr-progress-height: 0.5rem;
28
+
29
+ display: block;
30
+ box-sizing: border-box;
31
+ inline-size: 100%;
32
+ block-size: var(--gr-progress-height);
33
+ border: 0;
34
+ border-radius: var(--gr-radius-pill);
35
+ background-color: var(--gr-progress-track);
36
+
37
+ // Дорожка — оттенок currentcolor, а не готовая поверхность: непрозрачный
38
+ // серый совпал бы с одной из подложек, на которых полоса может лежать,
39
+ // и исчез бы вместе с ней.
40
+ appearance: none;
41
+ overflow: hidden;
42
+ }
43
+
44
+ // --- Дорожка и заполнение ---------------------------------------------------
45
+
46
+ .gr-progress::-webkit-progress-bar {
47
+ background-image: none;
48
+ background-color: var(--gr-progress-track);
49
+ border-radius: var(--gr-radius-pill);
50
+ }
51
+
52
+ .gr-progress::-webkit-progress-value {
53
+ background-image: none;
54
+ background-color: var(--gr-progress-fill);
55
+ border-radius: var(--gr-radius-pill);
56
+ transition: inline-size var(--gr-transition);
57
+ }
58
+
59
+ .gr-progress::-moz-progress-bar {
60
+ background-image: none;
61
+ background-color: var(--gr-progress-fill);
62
+ border-radius: var(--gr-radius-pill);
63
+ }
64
+
65
+ // --- Размеры ----------------------------------------------------------------
66
+
67
+ .gr-progress-sm {
68
+ --gr-progress-height: 0.25rem;
69
+ }
70
+
71
+ .gr-progress-lg {
72
+ --gr-progress-height: 0.75rem;
73
+ }
74
+
75
+ // --- Статусы ----------------------------------------------------------------
76
+
77
+ // Полоса выполнения — фигура, а не текст, поэтому предупреждение красится
78
+ // поверхностным цветом: текстовый в светлой теме тёмно-коричневый и читается
79
+ // не как «внимание», а как грязь.
80
+ $gr-progress-statuses: (
81
+ success: var(--gr-color-success),
82
+ warning: var(--gr-color-warning-surface),
83
+ danger: var(--gr-color-danger)
84
+ );
85
+
86
+ @each $gr-name, $gr-value in $gr-progress-statuses {
87
+ .gr-progress-#{$gr-name} {
88
+ --gr-progress-fill: #{$gr-value};
89
+ }
90
+ }
91
+
92
+ // --- Неопределённое состояние -----------------------------------------------
93
+
94
+ // <progress> без атрибута value. Системную бегущую полосу забирает
95
+ // appearance: none, поэтому её рисует фон самого элемента: отрезок в 40 %
96
+ // ширины ездит от края до края. Псевдоэлементы при этом обнуляются —
97
+ // иначе дорожка WebKit закрыла бы фон элемента собой.
98
+ .gr-progress-indeterminate {
99
+ background-image: linear-gradient(to right, var(--gr-progress-fill) 0 100%);
100
+ background-repeat: no-repeat;
101
+ background-size: 40% 100%;
102
+ animation: gr-progress-slide 1.4s ease-in-out infinite alternate;
103
+ }
104
+
105
+ .gr-progress-indeterminate::-webkit-progress-bar {
106
+ background-color: transparent;
107
+ }
108
+
109
+ .gr-progress-indeterminate::-webkit-progress-value {
110
+ background-color: transparent;
111
+ }
112
+
113
+ .gr-progress-indeterminate::-moz-progress-bar {
114
+ background-color: transparent;
115
+ }
116
+
117
+ // Бег — единственный признак того, что работа идёт, поэтому по настройке
118
+ // уменьшенной анимации он замедляется, а не снимается: замершая полоса
119
+ // сообщала бы «зависло».
120
+ @media (prefers-reduced-motion: reduce) {
121
+ .gr-progress-indeterminate {
122
+ animation-duration: 3s;
123
+ }
124
+ }
125
+
126
+ // --- Шкала ------------------------------------------------------------------
127
+
128
+ // <meter> — не прогресс: он показывает измеренную величину в известном
129
+ // диапазоне (заполненность диска, оценку, запас). Цвет заливки берётся
130
+ // у платформы: браузер сам вычисляет по low / high / optimum, попало ли
131
+ // значение в хороший диапазон. Класс-статус разошёлся бы с этим
132
+ // вычислением, поэтому у шкалы их и нет.
133
+ .gr-meter {
134
+ --gr-meter-height: 0.75rem;
135
+
136
+ display: block;
137
+ box-sizing: border-box;
138
+ inline-size: 100%;
139
+ block-size: var(--gr-meter-height);
140
+ border: 0;
141
+ border-radius: var(--gr-radius-pill);
142
+ background-color: color-mix(in srgb, currentcolor 12%, transparent);
143
+ appearance: none;
144
+ overflow: hidden;
145
+ }
146
+
147
+ .gr-meter::-webkit-meter-bar {
148
+ background-image: none;
149
+ background-color: color-mix(in srgb, currentcolor 12%, transparent);
150
+ border: 0;
151
+ border-radius: var(--gr-radius-pill);
152
+ }
153
+
154
+ .gr-meter::-webkit-meter-optimum-value {
155
+ background-image: none;
156
+ background-color: var(--gr-color-success);
157
+ border-radius: var(--gr-radius-pill);
158
+ }
159
+
160
+ .gr-meter::-webkit-meter-suboptimum-value {
161
+ background-image: none;
162
+ background-color: var(--gr-color-warning-surface);
163
+ border-radius: var(--gr-radius-pill);
164
+ }
165
+
166
+ .gr-meter::-webkit-meter-even-less-good-value {
167
+ background-image: none;
168
+ background-color: var(--gr-color-danger);
169
+ border-radius: var(--gr-radius-pill);
170
+ }
171
+
172
+ .gr-meter:-moz-meter-optimum::-moz-meter-bar {
173
+ background-image: none;
174
+ background-color: var(--gr-color-success);
175
+ border-radius: var(--gr-radius-pill);
176
+ }
177
+
178
+ .gr-meter:-moz-meter-sub-optimum::-moz-meter-bar {
179
+ background-image: none;
180
+ background-color: var(--gr-color-warning-surface);
181
+ border-radius: var(--gr-radius-pill);
182
+ }
183
+
184
+ .gr-meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {
185
+ background-image: none;
186
+ background-color: var(--gr-color-danger);
187
+ border-radius: var(--gr-radius-pill);
188
+ }
@@ -0,0 +1,95 @@
1
+ // Griffincss UI — Skeleton
2
+ // Заглушка на время загрузки: серые фигуры на месте будущего содержимого.
3
+ //
4
+ // <div aria-hidden="true">
5
+ // <span class="gr-skeleton gr-skeleton-circle"></span>
6
+ // <span class="gr-skeleton gr-skeleton-text"></span>
7
+ // </div>
8
+ // <span class="gr-sr-only" role="status">Загружаем список</span>
9
+ //
10
+ // aria-hidden на обёртке обязателен: пустые прямоугольники не значат ничего
11
+ // и в озвучке превращаются в шум. Сообщение о загрузке ставят рядом словом —
12
+ // это тот же принцип, что у сообщения о статусе.
13
+ //
14
+ // Залито оттенком currentcolor, а не готовой поверхностью: непрозрачный
15
+ // серый совпал бы с одной из подложек, на которых заглушка может лежать,
16
+ // и стал бы невидимым — так уже было с полосами таблицы.
17
+
18
+ .gr-skeleton {
19
+ display: block;
20
+ box-sizing: border-box;
21
+ min-block-size: 1em;
22
+ border-radius: calc(var(--gr-radius) * 0.5);
23
+ background-color: color-mix(in srgb, currentcolor 12%, transparent);
24
+ animation: gr-skeleton-pulse 1.6s ease-in-out infinite;
25
+ }
26
+
27
+ // Строка текста. Высота — по кеглю строки, на месте которой она стоит,
28
+ // поэтому заглушка абзаца собирается из нескольких таких строк
29
+ // и выглядит абзацем, а не одним серым прямоугольником.
30
+ .gr-skeleton-text {
31
+ block-size: 1em;
32
+ min-block-size: 0;
33
+ border-radius: var(--gr-radius-pill);
34
+
35
+ // Поля — по половине междустрочного зазора с каждой стороны: строки
36
+ // заглушки встают с тем же шагом, что и настоящие.
37
+ margin-block: 0.25em;
38
+ }
39
+
40
+ // Последняя строка абзаца короче остальных — иначе ровный правый край
41
+ // выдаёт, что это фигура, а не текст. Ширину переопределяют на месте,
42
+ // когда порядок строк другой.
43
+ .gr-skeleton-text:last-child {
44
+ inline-size: 60%;
45
+ }
46
+
47
+ // Круг под аватар или иконку. flex: none — чтобы в строке рядом с текстом
48
+ // его не сжимало.
49
+ .gr-skeleton-circle {
50
+ flex: none;
51
+ inline-size: 2.5rem;
52
+ block-size: 2.5rem;
53
+ border-radius: var(--gr-radius-pill);
54
+ }
55
+
56
+ // --- Готовая коробка --------------------------------------------------------
57
+
58
+ // Типовая заглушка блока: круг слева, колонка строк справа. Отступы и зазоры
59
+ // задаёт она сама и потому встаёт в карточке ВМЕСТО .gr-card-body — пока
60
+ // содержимого нет, она и есть тело. Без неё фигуры расставляют утилитами,
61
+ // а прижатые к краям коробки они читаются не как заглушка, а как сломанная
62
+ // вёрстка.
63
+ .gr-skeleton-group {
64
+ display: flex;
65
+ box-sizing: border-box;
66
+ flex-wrap: wrap;
67
+ align-items: flex-start;
68
+ gap: var(--gr-ui-gap);
69
+ padding: var(--gr-gap);
70
+ }
71
+
72
+ // Колонка строк: забирает остаток строки рядом с кругом. min-inline-size: 0
73
+ // обязателен — без него flex-элемент не сжимается ниже своего содержимого
74
+ // и строки вылезают за край на узком экране.
75
+ .gr-skeleton-stack {
76
+ display: flex;
77
+ flex: 1 1 0;
78
+ flex-direction: column;
79
+ gap: 0.5em;
80
+ min-inline-size: 0;
81
+ }
82
+
83
+ // В колонке шаг задаёт gap: собственные поля строк удвоили бы его.
84
+ .gr-skeleton-stack .gr-skeleton-text {
85
+ margin-block: 0;
86
+ }
87
+
88
+ // Мерцание снимается по настройке совсем, в отличие от кольца спиннера:
89
+ // заглушка сообщает «здесь будет содержимое» самим своим присутствием,
90
+ // и без анимации это сообщение не теряется.
91
+ @media (prefers-reduced-motion: reduce) {
92
+ .gr-skeleton {
93
+ animation: none;
94
+ }
95
+ }